
게임의 최적화와 관련된 디자인 패턴. 게임에서는 생성과 파괴에서 왜 파괴를 조심해야 되는지, 그리고 해결 방법이 무엇인지 알아보자
유니티 내장 오브젝트 풀
오브젝트 풀 패턴을 쓰는 이유는 가비지 컬렉터와 밀접한 연관이 있다C#의 가비지 컬렉터는 순환 메모리 참조 문제가 있다게임 오브젝트 인스턴스를 삭제 시 메모리 해제가 일어난다가비지 컬렉터는 자동으로 미사용 중인 메모리를 할당 해제한다C#에서 메모리 할당과 해제를 프로그래머가 직접 주소를 지정하는 것이 아닌, 자동으로 지정되어 할당되고 가비지 컬렉터에 의해서 해제된다오브젝트 풀 패턴을 사용한다




메모리 파편화가 일어난 빈 메모리 공간들을 활용하기 위해, 다시 메모리들을 앞으로 땡겨온다. 이때는 게임이 동작을 멈출 수밖에 없다. 즉, 프리징 현상이 일어난다. 이를 GC 스파이크라고 한다메모리 누수로 컴퓨터가 꺼지든, GC 스파이크로 게임이 프리징이 걸리든 둘 다 문제다. 이러한 문제를 해결하기 위해 오브젝트 풀 패턴을 사용한다게임 오브젝트를 미리 많~이 만들어 두고, 사용할 때는 그 오브젝트를 빌려서 사용하는 것이다. 이후 삭제 대신, 다시 오브젝트 풀로 반납한다오브젝트 풀로 쓰일 게임 오브젝트와 풀에서 꺼내 쓸 게임 오브젝트 프리팹을 준비한다MonoBehaviour를 상속받지 않기 때문에, 게임 오브젝트에 직접적으로 상속하지 못한다. 하지만 ObjectSpawner스크립트에서 인스턴스로 사용하기 때문에 상관 없다public class ObjectPool
{
// 생성된 게임오브젝트들을 담을 오브젝트 풀
private GameObject[] poolObject;
// 생성자로 오브젝트 풀 생성 및 초기화
public ObjectPool(int size, GameObject target, GameObject parent)
{
CreatePool(size, target, parent);
}
// 오브젝트 풀의 생성 및 초기화 함수
private void CreatePool(int size,GameObject target, GameObject parent)
{
// 입력받은 사이즈로 배열을 동적 할당
poolObject = new GameObject[size];
// 배열 순회
for(int i = 0; i<size; i++)
{
// 오브젝트를 생성하고 부모 객체의 자식 객체로 상속시킨다
// Instantiate는 Object 클래스의 함수
// MonoBehaviour <- Behaviour <- Component <- Object 순으로 상속
// 결과적으로 MonoBehaviour를 쓰면 함수를 쓸 수 있음
GameObject obj = MonoBehaviour.Instantiate(target,parent.transform);
// 비활성화
obj.SetActive(false);
// 오브젝트 풀에 추가
poolObject[i] = obj;
}
}
// 활성화 및 비활성화
public void Activate(bool select)
{
// 오브젝트 풀을 앞에서 부터 순회하며 순서대로 활성화 및 비활성화 가능
foreach(var element in poolObject)
{
if (element.activeSelf != select)
{
element.SetActive(select);
return;
}
}
// 1. 활성화
// - 만약 1,2,3 번까지 사용중인 상황이면 4번이 활성화
// 2. 비활성화
// - 만약 2,3,4,5가 활성화 상태이면 2번이 비활성화
}
// 비활성화 처리
// 객체 전부 파괴
public void DestroyAll()
{
// 오브젝트 풀 모든 요소 파괴
foreach(var element in poolObject)
{
MonoBehaviour.Destroy(element);
}
poolObject = null;
}
}
muzzlePointpublic class ObjectSpawner : MonoBehaviour
{
// 오브젝트 풀을 생성하기 위한 필드
[SerializeField] private GameObject prefab;
[SerializeField] private int poolSize;
private ObjectPool objectPool;
// 오브젝트 풀을 생성자로 통해 생성
private void Awake()
{
objectPool = new ObjectPool(poolSize, prefab, gameObject);
}
private void Update()
{
// 오브젝트 생성 : 빌리기
if(Input.GetKeyDown(KeyCode.A))
{
objectPool.Activate(true);
}
// 오브젝트 삭제 : 반납하기
if(Input.GetKeyDown(KeyCode.S))
{
objectPool.Activate(false);
}
}
// 오브젝트 풀 삭제하기
private void OnDestroy()
{
objectPool.DestroyAll();
}
}
prefab에 드래그&드롭으로 추가하면 된다public class MyObjectController : MonoBehaviour
{
// 어트리뷰트로 반납할 시간을 설정. 시간을 슬라이드로 정할 수 있다
[SerializeField][Range(0, 10)] private float returnTime;
private float timer;
private void OnEnable()
{
timer = returnTime;
}
private void Update()
{
// 시간이 실제 시간에 맞게 줄어들도록 한다
timer -= Time.deltaTime;
if(timer <=0)
{
gameObject.SetActive(false);
}
}
}
TIP
- 탄피는
오브젝트 풀, 발사음 같은 경우에는사운드 매니저로 중첩해서 쓸 수 잇는 방법이 있다. 발사 효과 이펙트는파티클 시스템을 이용하면 된다
public class Practice : MonoBehaviour
{
[SerializeField] public GameObject bulletPrefab;
//[SerializeField] GameObject muzzlePoint;
[SerializeField] public Transform muzzlePoint; // Transform이 GameObject를 상속받는다
private GameObject bulletOut;
[Range(1, 50)]
[SerializeField] public float speed;
// Update is called once per frame
void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
//Instantiate(bulletPrefab, muzzlePoint.transform.position, muzzlePoint.transform.rotation);
bulletOut = Instantiate(bulletPrefab, muzzlePoint.position, muzzlePoint.rotation);
Rigidbody rig = bulletOut.GetComponent<Rigidbody>();
rig.velocity = muzzlePoint.forward * speed;
}
}
}

MuzzlePoint 오브젝트를 만든다. Transform의 정보만 가지고 있는 오브젝트다.
탱크에 넣는다
Bullet Prefab는 Shell을, Muzzle Point는 MuzzlePoint를 넣는다public float clearTime;
private float timer;
void Start()
{
timer = clearTime;
}
void Update()
{
timer -= Time.deltaTime;
if(timer<=0)
{
Destroy(bulletOut);
}
}

private Queue<GameObject> que;
private Queue<float> timeQue;
public float clearTime;
private float timer;
void Awake()
{
que = new Queue<GameObject>();
timeQue = new Queue<float>();
}
void Update()
{
if (que.Count > 0 && timeQue.Peek() + clearTime <= Time.time)
{
Debug.Log("제거합니다");
Destroy(que.Dequeue());
timeQue.Dequeue();
}
}
Queue를 추가해 해결했다.Queue에 해당 오브젝트와 시간을 추가해서, 업데이트마다 Peek()으로 타이머를 현재시간과 비교해, Peek()의 시간 + 클리어 시간이 같거나 작다면 오브젝트를 지운다. 그리고 오브젝트 큐와 시간 큐를 각각 Dequeue()한다
-큐, 타이머 다 필요없다. 삭제하고 싶은 시간만 정하면 된다
Destroy(bulletOut,clearTime);

using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
[SerializeField] Stack<OutObject> bullets;
[SerializeField] Stack<OutObject> bursts;
[SerializeField] OutObject prefab;
[SerializeField] GameObject prefab1;
[SerializeField] float speed;
public Transform muzzlePoint;
public int magazineCapacity;
void Awake()
{
bullets = new Stack<OutObject>();
for (int i = 0; i < magazineCapacity; i++)
{
OutObject instance = Instantiate(prefab);
instance.gameObject.SetActive(false);
bullets.Push(instance);
}
}
public OutObject GetObjectPool()
{
if (bullets.Count == 0)
{
OutObject bulletOut1 = Instantiate(prefab, muzzlePoint.position, muzzlePoint.rotation);
Rigidbody rig1 = bulletOut1.GetComponent<Rigidbody>();
rig1.velocity = muzzlePoint.forward * speed;
return bulletOut1;
}
OutObject bulletOut = bullets.Pop();
bulletOut.returnPool = this;
bulletOut.transform.position = muzzlePoint.position;
bulletOut.transform.rotation = muzzlePoint.rotation;
bulletOut.gameObject.SetActive(true);
Rigidbody rig = bulletOut.GetComponent<Rigidbody>();
rig.velocity = muzzlePoint.forward * speed;
return bulletOut;
}
public void ReturnObjectPool(OutObject instance)
{
instance.transform.rotation = Quaternion.identity;
instance.gameObject.SetActive(false);
bullets.Push(instance);
Instantiate(prefab1,instance.transform.position,instance.transform.rotation);
}
}
using UnityEngine;
public class OutObject : MonoBehaviour
{
public ObjectPool returnPool;
[SerializeField] float clearTime;
private float timer;
private void OnEnable()
{
timer = clearTime;
}
private void Update()
{
timer -= Time.deltaTime;
if(timer<=0)
{
ReturnPool();
}
}
public void ReturnPool()
{
if (returnPool == null)
{
Destroy(gameObject);
}
else
{
returnPool.ReturnObjectPool(this);
}
}
}

Instantiate로 생성한 복제품을 stack으로 넣었다참고