내일배움캠프 45일차 TIL - 오브젝트 풀 고찰

권태하·2024년 6월 19일
0
post-thumbnail

새 팀프로젝트가 시작되었다. 최종프로젝트를 앞둔 마지막 팀 프로젝트고 그에 맞춰 팀프로젝트의 게임 주제가 자유로 주어졌다. 구현 요소들은 정해져있지만 대부분 선택 구현과제이므로 원하는 게임을 만들고 그에 맞는 구현과제를 취사선택하면 된다는 것이다.

우리팀은 종스크롤 하이퍼캐쥬얼 게임을 하기로 했다

예시로는 Arrow a Row나 Last War: Surivor정도가 되겠다.
플레이어는 계속해서 나타나는 몬스터를 잡거나 아이템을 선택해서 획득하며 더 많은 진행도를 얻는 형식이다.

내가 맡게 된 구현 요소는 적 생성 로직이다.

Monster Spawner 
랜덤하게 위치에 몬스터 스폰 시키기
(왼쪽, 오른쪽, 가운데 중 랜덤)

플레이어가 게임을 진행한 거리나 시간에 따라 플레이어 앞에서 어떤 몬스터가 지속적으로 나타나야 하는 형태이고, 역시나 이 부분에 가장 걸맞는 기술은 오브젝트 풀 일 것이다.

기존에 여러번 써 본 오브젝트 풀 소스코드가 있긴 하지만, 과연 이 코드가 어떤 점에서 효율적일지 한 번 개발자의 생각으로 고찰해보는 시간을 가져보기로 했다.

public class ObjectPool : MonoBehaviour
{
    [System.Serializable]
    public class Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }

    public List<Pool> Pools;
    public Dictionary<string, Queue<GameObject>> PoolDictionary;

    private void Awake()
    {
        PoolDictionary = new Dictionary<string, Queue<GameObject>>();
        foreach (var pool in Pools)
        {
            Queue<GameObject> objectPool = new Queue<GameObject>();
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab, gameObject.transform);
                obj.SetActive(false);
                objectPool.Enqueue(obj);
            }
            PoolDictionary.Add(pool.tag, objectPool);
        }
    }

    private void Start()
    {
        GameManager.Instance.objectPool = this;
    }

    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;
    }
}

내가 계속해서 써 오던 위 코드는 Dictionary와 Queue를 이용해 오브젝트 풀을 관리한다.

Queue

오브젝트 풀 자체가 자주 사용되는 객체를 미리 생성해두고 필요할 때 재사용함으로써 메모리 할당과 가비지 컬렉션 비용을 줄이는 기법이다. 게임 오브젝트를 새로 생성하는 작업은 많은 메모리가 필요하다고 한다. Queue는 선입선출(FIFO) 구조로 이루어지며, 다음과 같은 장점으로 인해 오브젝트 풀과 잘 어울리는 구조이다.

  1. 효율적인 객체 재사용: 선입선출 구조 상, 구성 객체들이 공평하게 재사용될 수 있어, 모든 객체가 비슷한 사용 빈도를 가지게 된다.
  2. 간단한 관리: 객체를 풀에 추가하거나 풀에서 제거할 때 복잡한 로직 없이 Queue의 기본 연산(enqueue와 dequeue)만으로 간단히 관리할 수 있다.

Queue가 아닌 다른 자료구조들의 장단점은 다음과 같다.

  1. 스택(Stack): 스택은 후입선출(LIFO, Last-In-First-Out) 구조를 가지고 있다. 스택을 사용하면 가장 최근에 반환된 객체가 다음에 재사용된다. 이는 특정 상황에서 *캐시 지역성(cache locality)을 향상시킬 수 있지만, 객체 사용의 균등성 측면에서는 큐보다 떨어질 수 있다.
  2. 리스트(List): 리스트를 사용하면 객체를 순서에 상관없이 관리할 수 있다. 특정 조건에 따라 객체를 선택적으로 재사용하고 싶을 때 유용할 수 있다. 하지만, 리스트에서 객체를 검색하거나 추가, 제거하는 데 드는 비용이 큐나 스택에 비해 높을 수 있다.
  3. 해시 세트(HashSet): 해시 세트를 사용하면 객체의 존재 여부를 빠르게 확인할 수 있다. 이는 객체의 중복을 방지하고, 특정 객체를 빠르게 찾아 재사용하고자 할 때 유용하다. 하지만, 해시 세트는 객체의 순서를 보장하지 않는다.
  4. 우선순위 큐(Priority Queue): 우선순위 큐는 각 객체에 우선순위를 부여하여 우선순위가 높은 객체부터 재사용할 수 있게 한다. 이는 특정 기준(예: 사용 빈도, 최근 사용 시간 등)에 따라 객체를 재사용하고자 할 때 유용할 수 있다. 하지만, 우선순위 계산과 관리에 추가적인 비용이 발생할 수 있다.

캐시 지역성이란?
캐시 지역성(Cache Locality)은 컴퓨터 시스템에서 데이터를 캐시에 저장하고 접근하는 효율성과 관련된 개념입니다. 캐시는 CPU와 메모리 사이의 속도 차이를 극복하기 위해 도입된 작은 용량의 빠른 메모리입니다. 프로그램이 데이터를 처리할 때, 캐시 지역성이 좋으면 CPU는 더 빠르게 데이터에 접근할 수 있어 성능이 향상됩니다. 캐시 지역성은 크게 두 가지 유형으로 나뉩니다:
1. 시간 지역성(Temporal Locality): 시간 지역성은 특정 데이터나 자원이 한 번 접근된 후에 가까운 미래에 다시 접근될 가능성이 높은 성질을 말합니다. 예를 들어, 루프 내에서 반복적으로 같은 변수를 사용하는 경우, 해당 변수는 캐시에 저장되어 있어 빠르게 접근할 수 있습니다.
2. 공간 지역성(Spatial Locality): 공간 지역성은 메모리 상에서 서로 가까이 위치한 데이터나 자원이 연속적으로 접근될 가능성이 높은 성질을 말합니다. 예를 들어, 배열을 순차적으로 접근하는 경우, 인접한 배열 요소들은 메모리 상에서도 가까이 위치해 있으므로 한 번에 여러 요소가 캐시로 로드될 수 있습니다.
캐시 지역성을 최적화하는 것은 프로그램의 성능을 크게 향상시킬 수 있습니다. 메모리 접근 패턴을 최적화하여 시간 지역성과 공간 지역성을 높이는 것은, 특히 메모리 접근 시간이 전체 성능에 큰 영향을 미치는 대규모 계산이나 데이터 집약적 애플리케이션에서 중요합니다.

Dicionary

딕셔너리는 키 값과 밸류 값을 가지는 자료형이라고 정리중이다. 각 오브젝트풀을 딕셔너리 형식으로 가지고 있으면, 오브젝트 풀이란 스크립트가 하나만 있어도 여러 오브젝트풀을 관리할 수 있다. 생성과 불러옴에 있어 적절한 키 값을 사용해 주기만 하면 된다.

하지만, 오브젝트 풀의 키 값이 틀리지 않게 조심해야한다는 점, 그리고 오브젝트풀을 사용하는 객체가 적다면 굳이 쓸 필요가 없다는 정도가 주의점이라 생각된다.

Dictionary의 장단점
Dictionary는 키-값 쌍으로 데이터를 저장하는 자료구조로, 키를 통해 빠르게 값을 검색할 수 있는 특징을 가지고 있습니다. C#에서 Dictionary<TKey, TValue>는 일반적으로 해시 테이블을 기반으로 구현되어 있습니다. 이 자료구조의 장단점을 살펴보겠습니다.
장점
1. 빠른 검색 속도: Dictionary는 해시 테이블을 사용하여 구현되므로, 평균적으로 O(1)의 시간 복잡도로 키를 통해 값을 검색할 수 있습니다. 이는 대량의 데이터를 다룰 때 매우 효율적입니다.
2. 키-값 쌍으로 데이터 관리: 데이터를 키-값 쌍으로 관리함으로써 데이터의 구조화가 용이하며, 이를 통해 데이터에 대한 접근과 관리가 직관적이고 편리합니다.
3. 동적 크기 조정: Dictionary는 내부적으로 동적으로 크기가 조정되므로, 사용자는 컬렉션의 크기를 신경 쓰지 않고 필요에 따라 요소를 추가하거나 제거할 수 있습니다.
4. 키의 유일성: Dictionary에서 각 키는 유일해야 하므로, 데이터의 중복을 방지할 수 있습니다. 이는 데이터의 일관성을 유지하는 데 도움이 됩니다.
단점
1. 메모리 사용량: Dictionary는 내부적으로 해시 테이블을 사용하기 때문에, 배열 기반의 자료구조에 비해 상대적으로 더 많은 메모리를 사용할 수 있습니다. 특히, 요소의 수가 적은 경우에는 이러한 차이가 더욱 두드러질 수 있습니다.
2. 순서 보장 불가: Dictionary는 데이터를 키의 해시 값에 따라 저장하기 때문에, 요소들이 추가된 순서를 보장하지 않습니다. 순서가 중요한 경우에는 OrderedDictionary나 SortedList와 같은 다른 자료구조를 고려해야 합니다.
3. 해시 충돌: 비록 드문 경우지만, 서로 다른 두 키가 같은 해시 값을 가질 수 있는 해시 충돌이 발생할 수 있습니다. 이 경우, Dictionary의 성능이 저하될 수 있습니다.
Dictionary는 키를 통해 빠르게 데이터에 접근해야 하는 경우에 매우 유용한 자료구조입니다. 하지만, 사용하기 전에 애플리케이션의 요구 사항과 Dictionary의 특성을 고려하여, 가장 적합한 자료구조를 선택하는 것이 중요합니다.

profile
스터디 로그

0개의 댓글

관련 채용 정보