본문 바로가기
Games/게임 디자인 패턴

[게임 디자인 패턴] 오브젝트 풀 패턴

by NCTP 2024. 9. 10.

// 유니티로 배우는 게임 디자인 패턴 제 2판 (데이비드 바론, 구진수) 을 기준으로 작성되었습니다.

참고자료 : https://docs.unity3d.com/kr/2022.1/ScriptReference/Pool.ObjectPool_1.html

 

개요

  많은 게임들의 화면 속에서는 수많은 객체들이 각자 행동하고, 상호작용하는 모습들을 볼 수가 있다. 투사체가 이리저리 날아다니거나, 다양한 파티클 효과가 터지고, 갑자기 수많은 적의 무리가 덮칠 수도 있다. 

 

  다양한 객체를 효과적으로 다루기 위해서는 오브젝트 풀 패턴이 사용된다. 

 

  오브젝트 풀 패턴은 자주 사용되는 요소를 위해 일부 메모리를 예약한다. 최근 사라진 객체를 메모리에서 없애지 않고, 다시 사용할 수 있도록 오브젝트 풀에 추가한다. 이를 통해 새로운 인스턴스를 로드하는 초기화 비용을 아낄 수 있고, CPU에 가해지는 부담을 줄일 수가 있다. 

 

  즉, 오브젝트 풀 패턴은 자주 생성/삭제되는 자원들을 미리 생성하고, 필요할 때마다 꺼내서 재사용하는 디자인 패턴이다.

 

오브젝트 풀 패턴의 핵심 개념

  오브젝트 풀 패턴의 핵심 개념은 이렇다. 

 

  1. 오브젝트 재사용 : 게임이 시작되면, 반복적으로 자주 사용되고 삭제되는 오브젝트는 컨테이너 형식의 풀 안에 미리 일정량 생성해놓는다. 이 때 풀 안의 오브젝트들은 비활성 상태로 대기한다. 클라이언트가 풀 안의 오브젝트를 필요로 하게 되면, 풀에서 제거하고 클라이언트에게 제공한다. 일정 시간 안에 풀 안의 오브젝트 인스턴스가 충분하지 않다면, 새로운 인스턴스가 동적으로 생성되거나 오브젝트가 반환될 때까지 기다린다.
  2. 오브젝트 반환 : 오브젝트의 사용이 완료되면, 삭제되는 대신 다시 오브젝트 풀에 반환한다. 반환된 오브젝트는 풀 내에서 비활성 상태로 다시 대기한다.

오브젝트 풀 패턴의 장단점

장점

  • 예측할 수 있는 메모리 관리 : 오브젝트 풀을 사용하여 특정 종류의 오브젝트 인스턴스를 특정한 양만큼 유지하도록 하기 때문에, 메로리 사용량을 일정한 양으로 유지할 수 있고, 메모리를 예측 가능한 양만큼 할당할 수 있다.
  • 성능 향상 : 새로운 인스턴스를 로드하는 초기화 비용을 아끼므로 성능 향상에 도움이 된다. 
  • 가비지 콜렉션 부담 감소 : 오브젝트의 반복적인 생성과 삭제는 GC(가비지 컬렉션)를 자주 발생시켜 성능 저하를 유발하는데, 이를 방지할 수 있다.

오브젝트 풀 패턴의 UML 다이어그램

단점

  • 초기 메모리 소비 : 게임 시작 시 풀에 오브젝트를 생성하는 과정에서 많은 양의 오브젝트를 생성하므로, 초기의 메모리 소비가 많아진다.
  • 예측 불가능한 객체 상태 : 중간 처리과정에서 문제가 생겼을 경우, 풀에 반환하는 과정에서 오브젝트가 초기 상태가 아닌 현재 상태 그대로 반환될 가능성이 있다. 예를 들어, 플레이어가 처치한 적의 HP가 죽음에 이르는 수준인 그대로 풀에 반환된다면, 스폰되는 즉시 죽은 상태로 스폰되는 사태가 벌어질 수 있다. 풀에 반환할 때는 초기 상태로 돌아가도록 구현하자.

 

 

코드 작성하기

 

Enemy.cs

 

  오브젝트 풀 패턴을 적용할 오브젝트로 Enemy를 사용한다. 다량의 적이 나오는 게임이라면, 오브젝트 풀 패턴으로 적들을 관리함으로써 컴퓨터 자원을 아낄 수 있다.

 

  다음과 같이 스크립트를 작성할 수 있다.

 

using System.Collections;
using UnityEngine;
using UnityEngine.Pool;


// 부모 클래스로 Entity를 상속 
public class Enemy : Entity
{
    //나는 ~ 풀의 소속입니다!
    public IObjectPool<Enemy> Pool {get; set;} // 해당 게임 오브젝트라 포함된 Pool을 저장하는 변수. 
    private float timeToSelfDestruct = 3.0f; // 테스트를 위한 자살용 변수


    // Entity 클래스의 IDamagealbe 인터페이스에서 선언된 TakeDamage 함수
    // 데미지를 받을 때 사용된다.
    public void TakeDamage(DamageMessage damageMessage)
    {
        CurrentHealth -= damageMessage.amount;
        if(CurrentHealth <= 0.0f)
        {
            ReturnToPool();
        }

    }

    // 자살용 함수
    // 자기 자신을 코루틴을 통해 일정 시간 뒤에 파괴하고, 풀로 돌아간다.
    IEnumerator SelfDestruct()
    {
        yield return new WaitForSeconds(timeToSelfDestruct);
        Debug.Log("Return!");
        ReturnToPool();
    }

    // 풀에 들어갈 때 오브젝트를 초기 상태로 돌리기 위한 함수
    void ResetEnemy()
    {
        CurrentHealth = health;
    }

    // 풀에 돌아갈 때 호출하는 함수
    void ReturnToPool()
    {
        Pool.Release(this);
    }
    
    void OnEnable()
    {
        StartCoroutine(SelfDestruct());
    }
    void OnDisable()
    {
        ResetEnemy();
    }
}

 

  • 유니티의 오브젝트 풀 기능을 사용하기 위해서, UnityEngine.Pool 라이브러리를 불러온다.

 

 

EnemyObjectPool.cs

 

  Pool로 관리할 Enemy 스크립트를 작성했으니, 이제 Enemy들을 관리할 Pool을 만들어주어야 한다.

  

 다음과 같이 스크립트를 작성할 수 있다.

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

public class EnemyObjectPool : MonoBehaviour
{
    public int maxPoolSize = 10; // 풀에 보관할 오브젝트의 수 
    public int stackDefaultCapacity = 10; // 기본 스택 크기

    private IObjectPool<Enemy> _pool;
    
    public IObjectPool<Enemy> Pool // 풀 초기화 부분
    {
        get
        {
            if(_pool == null)
            {
                _pool = new ObjectPool<Enemy>(
                    CreatedPooledItem,
                    OnTakeFromPool,
                    OnReturnedToPool,
                    OnDestroyPoolObject,
                    true,
                    stackDefaultCapacity,
                    maxPoolSize);
            }
            return _pool;
        }
    }

    // Pooling할 오브젝트를 만드는 함수
    // Pool에 들어갈 오브젝트를 만드는 공장이 이곳
    private Enemy CreatedPooledItem()
    {
        var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
        Enemy enemy = go.AddComponent<Enemy>();
        go.name = "Enemy";
        enemy.Pool = Pool;
        return enemy;
    }

    // 오브젝트를 풀로 반환
    // 반환된 오브젝트는 비활성화된다.
    private void OnReturnedToPool(Enemy enemy)
    {
        enemy.gameObject.SetActive(false);
    }

    // 오브젝트를 사용하기 위해 풀에서 가져감.
    // 오브젝트는 초기 상태에서 활성화된다.
    private void OnTakeFromPool(Enemy enemy)
    {
        enemy.gameObject.SetActive(true);
    }

    // 풀에 더 이상 공간이 없을 때 호출된다.
    // 저장할 공간을 넘은 오브젝트를 삭제한다.
    private void OnDestroyPoolObject(Enemy enemy) 
    {
        Destroy(enemy.gameObject);
    }

    // 오브젝트를 스폰할 때 사용한다.
    // 풀에서 오브젝트를 꺼내고, 랜덤한 위치에 생성한다.
    public void Spawn()
    {
        var amount = Random.Range(1, 10);
        for(int i = 0; i < amount; i++)
        {
            var enemy = Pool.Get();
            enemy.transform.position = Random.insideUnitSphere * 10;
        }
    }
}

 

 

ClientObjectPool.cs

 

  게임에서 풀링할 오브젝트가 하나라도는 단정지을 수 없다. 여러 오브젝트를 풀링을 통해 관리한다면, 이를 관리할 하나의 객체가 필요하다. 이 역할을 ClientObjectPool이 해줄 것이다. 

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ClientObjectPool : MonoBehaviour
{
    private EnemyObjectPool _enemyPool;
    // Start is called before the first frame update
    void Start()
    {
        _enemyPool = gameObject.AddComponent<EnemyObjectPool>();
    }
    void OnGUI()
    {
        if(GUILayout.Button("Spawn Drones"))
        {
            _enemyPool.Spawn();
        }
    }
}

 

 

 

구현 결과물 확인하기

 

 

구현 결과

 

  • 오브젝트를 스폰하면 랜덤한 위치에 오브젝트가 스폰된다.
  • Hierarchy를 통해, 오브젝트라 풀링되는 과정을 확인할 수 있다.
  • 풀에 반환된 오브젝트는 비활성화된다.
  • 오브젝트가 정해진 수보다 많아지면 삭제된다.

댓글