ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TopDownShooting Game / 오브젝트풀을 이용한 화살 만들기
    게임 개발기록 2024. 5. 24. 08:43

    화살 같은 오브젝트는 생성과 파괴를 반복 한다. 이것은 각각 Instantiate(생성), Destroy(파괴)를 사용해 하게 되는데 이 함수들은 매우 큰비용을 사용 하게 된다. 

     

    그러므로 화살을 쏠 때마다 이 기능들을 사용하게 되면 프로그래밍에 있어서 비효율적으로 할 수밖에 없게 된다. 그래서 만들어진 게 ObjectPool이라고 할 수가 있다. (투터님은 Instantiate라는 함수를 보고 끔직하다. 라고 표현 하셨다.)

     

    이 오브젝트 풀은 화살 같은 생성과 파괴를 반복 하는 것에 대해서 미리 지정해둔 갯수를 만들어내고 화살을 쏠 때 만들어 둔 거를 쏘고 다시 돌려받는 형식이다. 

     

    그리고 이러한 방식으로 말고도 쏠 때 만들고, 부족하면 만들고 등등 다른 방식으로도 사용이 가능하다. 그리고 만약 천 개를 쐈는데 천개가 다 나가고 나갈게 없을 경우 나가 있는 걸 다시 충전해서 바로 쏘게 하는 경우도 가능하다.

     

    그래서 지금 만들고 있는 TopDOwnShooting Game에 화살 오브젝트를 오브젝트풀을 이용해서 만들어 보겠다. 

     

    먼저 Class를 만들어 준다. 

     

    ObjectPool 이란 이름으로 만들고 MonoBehaviour 를 상속 받는다. 그리고 Collection 기능이 총 3개가 필요하다. 

     

    한 개는 화살을 만들 때 참고할 자료들을 저장할 List, 하나는 만들어진 화살을 저장 할 Dictionary, 하나는 화살을 선입선출(FIFO) 형태로 저장 할 Queue이다. 

     

    시작 할 때 List가 갖고 있는 정보로 화살을 만들고 그 화살을 Queue에다가 생성을 하고 그 생성 된 Queue를 Dictionary에 저장을 해서 사용을 한다. 

     

    왜 List를 사용 하는가?

    그것은 우리는 자료를 유니티에디터에서 추가를 할 것이고 그게 가능 한 것이 List다. 우리는 변수들이 담긴  클래스 하나를 더 만들어서 List에 그 클래스를 담을 것이다. 그리고 유니티 에디터를 통해서 값을 넣어준다.

    (Dictionary나 다른 복잡한 구조들은 유니티에디터를 통해서 값을 초기화 해줄 수 없다. 유니티가 못 하게 해놨다.)

     

    왜 Dictionary를 사용 하는가? 

    그것은 성능적인 문제라고 볼 수 있다. 딕셔너리는 기본적으로 키-값 쌍을 저장하며, 해시 테이블을 사용하여 키를 기반으로 값을 빠르게 검색할 수 있다. 객체 풀링 시스템에서 특정 태그에 해당하는 객체 풀을 빠르게 찾기 위해 Dictionary를 사용한다. 시간 복잡도는 평균적으로 O(1)이다. 이는 리스트에서 태그를 검색하는 O(n) 시간 복잡도에 비해 훨씬 빠르다.

    //**** O지O 튜터님(익명보장)의 추가 코멘트 : 해쉬를 이용한 연산은 사실 O(1)이지만 굉장히 느린 O(1)이랍니다~
    해쉬값을 찾는 연산을 한다음에 그걸 기반으로 찾아야되는데 그것 자체가 너무 느리다는거죠
    하지만 리스트로 하나하나 찾는 것보다는 빠른 경우가 많기 때문에(값이 늘어나면서 더) 일반적으로는 딕셔너리를 이런 경우엔 많이 사용해요! ****//

     

    왜 Queue를 사용 하는가?

    화살이 나가고 다시 돌려받는 형식을 사용 하게 될 것인데 이러한 순환구조는 Queue가 매우 효율적이다. 

     

    이러한 구조를 통해 객체 풀링 시스템은 성능을 최적화하고, 객체를 효율적으로 관리할 수 게 한다. 게임의 성능 향상에 큰 기여를 할 수 있으며, 특히 많은 객체가 반복적으로 생성되고 파괴되는 시나리오에서 유용하다고 한다. 

     

    그래서 먼저 List에 자료형을 정해줄  Class를 만들어 준다. 

     

       public class Pool
       {
           public string tag;
           public GameObject prefab;
           public int size;
       }

     

    이렇게 만들어 주면 되는데, 유니티에디터에서 수정을 해주려면 그것을 해야 하는 거 알고 있을 것이다. 시스템어쩌구 콘푸로스트어쩌구

     

    이 클래스를 List에 형태로 만들어 준다. 

     

    public List<Pool> pools = new List<Pool>();

     

    public이므로 유니티에디터에서 값을 초기화 해줄 수 있다. 이 때 +를 눌러줘야 List가 한 개 생긴다. 2개 누르면 2개, 우리는 이번에 플레이어가 쓸 화살통 하나만 만들 것이니까 1개만 추가 해주면 된다. 

    사진참고

     

    여기서 tag를 넣어줘서 Ditionary에 키값을 정해주고(현재 Arrow), prefab을 넣어줘서 화살 오브젝트를 넣게 해준다(미리 만들어둔 화살 Object). 그리고 size를 만들어 줘서 화살을 몇 개 만들지 정해줄 수 있게 한다.(현재 20개의 오브젝트를 만든다)

     

    그리고 이제 화살을 생성 하는 코드를 작성 해줘야 한다. 

     

    List를 참고 해서 화살을 생성 하게 한다. 

     

    private void Awake()
    {
        PoolDIctionary = new Dictionary<string, Queue<GameObject>>();

        foreach(var pool in pools)
        {
            Queue<GameObject> queue = new Queue<GameObject>();
            for(int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab, transform);
                obj.SetActive(false);
                queue.Enqueue(obj);
            }

            PoolDIctionary.Add(pool.tag, queue);
        }
    }

    (PoolDicionary는 필드에 선언 돼있음.)

     

    여기서 List는 한 개밖에 없기에 foreach는 한 번만 실행이 되고 Queue를 바로 선언 해준다. 그리고 for 반복문을 통해서 size 입력 한 만큼 화살을 생성 한다. 

     

    그리고 생성과 동시에 SetActive를 false를 해준다.(여기서 tranform은 자식객체로 오브젝트가 생성 되게 하는 기능이다.) 그리고 Enqueue로 넣어준다. 이걸 size만큼 반복 한 후에 Dictionary에 넣어준다. 

     

    이러면 Dictionary는 size만큼의 화살 오브젝트를 갖고 있는 Queue를 담고 있는 컬렉션이 되고 사용을 할 수가 있다. 

     

    이제 사용을 하는, 오브젝트를 꺼냈다가 다시 채워넣는 함수를 만들어서 사용 하면 된다. 

     

    public GameObject SpawnFromPool(string tag)
    {
        if (!PoolDIctionary.ContainsKey(tag))
        {
            return null;
        }

        GameObject obj = PoolDIctionary[tag].Dequeue();
        PoolDIctionary[tag].Enqueue(obj);

        obj.SetActive(true);
        return obj;
    }

     

    SpawnFromPool이란 함수로 만들고 

     

    매개변수를 사용 해서 PoolDIctionary에 객체가 있는지 확인을 한다. 만약 오브젝트가 없다면 return을 사용해서 함수가 실행 되지 않게 한다. ContainsKey는 해당 키값이 있는지 확인 하는 코드다. 

     

    PoolDIctionary에 해당 키값인 오브젝트가 있는지에 대한 확인 하는 코드

     

    만약 있다면 GameObject obj = PoolDIctionary[tag].Dequeue(); 로 Queue에서 꺼내와준다. 

     

    그리고 다시 바로 PoolDIctionary[tag].Enqueue(obj); 채워넣어준다. 

     

    이 때 넘겨주는 obj는 참조형으로써 메모리주소를 넘기게 된다. 그래서 이 obj가 return으로 반환이 돼서 어떠한 변경점이 있다면 같이 바뀐다. ex) 파괴 되면 이 Queue는 망가짐. 그래서 파괴 되지 않게 SetActive를 활성화, 비활성화를 통해서 생성과 파괴를 구현함. 

     

    그래서 이 obj가 SetActive true 가 된 상태로 나가면 list에도 true로 돼 있는 상태고 밖에서 파괴로직이 실행 되어 false가 되면 list에 들어있는 obj도 같이 false가 된다. 

     

    그래서 hirerarchy 화면에서 생성 된 화살 obj들의 위치가 변경 되지 않는 것이다. 

     

    그래서 

     obj.SetActive(true);
     return obj;

    라는 코드를 작성해서 List에서 꺼내온 obj를 활성화 하고 이거를 반환 해주게 한다. 그러면 오브젝트풀을 사용 하는 클래스 작성을 완성 할 수 있다. 

     

    이제 다른 클래스에서 이 오브젝트풀 클래스에 SpawnFromPool 함수를 사용 하여서 화살을 만들어 주면 된다. 

     

    GameObject obj = GameManager.Instance.ObjectPool.SpawnFromPool(rangedAttackSO.bullNameTag);

    이런 식으로 활용 해주면 된다. 

     

    GameManager의 코드 

    public ObjectPool ObjectPool { get; private set; }

    private void Awake()
    {
        if (Instance != null) Destroy(gameObject);
        Instance = this;

        Player = GameObject.FindGameObjectWithTag(playerTag).transform;
        ObjectPool = GetComponent<ObjectPool>();
    }

     

    게임매니저에서 오브젝트풀 객체를 생성 한 이유는 다들 알지

     

    그리고 여기서 CreateProjectile 함수를 보게 되면

     

     private void CreateProjectile(RangedAttackSO rangedAttackSO, float angle)
     {
         GameObject obj = GameManager.Instance.ObjectPool.SpawnFromPool(rangedAttackSO.bullNameTag);
         obj.transform.position = projectileSpawnPosition.position;
         ProjectileController attackController = obj.GetComponent<ProjectileController>();
         attackController.InitializeAttack(RotateVector2(aimDirection, angle), rangedAttackSO);
     }

     

    InitializeAttack이라는 함수가 있는데, 이거는 화살 오브젝트에 들어가 있는 스크립트, 컴포넌트 기능이다. 

     

    이 InitializeAttack 함수를 보면

     

     public void InitializeAttack(Vector2 direction, RangedAttackSO attackData)
     {
         this.attackData = attackData;
         this.direction = direction;

         UpdateProjectileSprite();
         trailRenderer.Clear();
         currentDuration = 0;
         spriteRenderer.color = attackData.projectileColor;

    }

     

    이렇게 direction이나 attackData를 적용 하고 컬러도 바꾸는데 CreateProjectile obj가 먼저 생성이 되더라도 이 obj가 갖고 있는 InitializeAttack 함수라서 생성이 된 obj가 적용이 되는 것이다. 그래서 코드에 this가 사용이 된 것을 확인 할 수 있다. 

     

    어떤 객체를 바꾸는 것이 아니라 이 스크립트를 갖고 있는 객체의 정보를 변경 하는 것이기 때문이다. 

     

    원래라면 객체를 갖고와서 정보를 변경 해야 할 것이다. 다들 무슨 소리인지 알지

     

    그리고 함수를 사용 하기 위해서 ProjectileController attackController = obj.GetComponent(); 코드를 작성한 것을 확인 할 수 있다. 

     

    과연 시간이 지난 내가 봤을 때도 무슨 소리인지 알까 아니면 내가 뭔 말을 적어둔거지 라고 생각 할까 궁금하다.

    난 노력 했다.

Designed by Tistory.