유니티로 게임 만드시면서 오브젝트 폴링을 모르시진 않으실겁니다.
총알, 몬스터 등 여러번 비활성화되고 다시 활성화되어야하는 오브젝트들은 재활용해야합니다.
Pool 안에 담아놨다가 필요할 때 하나씩 비활성화되어있는 오브젝트를 꺼내서 활성화 시켜 사용하고
비활성화되어있는 오브젝트가 없다면 새로 생성하는 방식으로 최적화하는 기법입니다.
다만 코드들이 항상 복잡해지고 버그가 자주 발생합니다.
상속해서 쓴다면 이 단점들을 해결할 수 있습니다.
일단 Better Object Pooling의 특징을 말씀드리겠습니다.
- 오브젝트 폴링 매니저가 필요없음
- 폴링될 오브젝트는 폴(매니저같은거)을 찾을 필요없이 스스로 비활성화시 들어가집니다.
- gameObject뿐 아니라 다양한 컴포넌트도 참조해둘 수 있습니다.
- 인터페이스를 활용해서 상속하기 편한 깔끔한 코드입니다.
유니티에서 기본적으로 제공하는 오브젝트 폴링 시스템도 있습니다. 관심있으시면 확인 해봐도 됩니다.
https://unity.com/kr/how-to/use-object-pooling-boost-performance-c-scripts-unity
구현해보기
이제 직접 만들면서 어떤 시스템인지 한번 봅시다.
Better Object Pooling에서는 인터페이스를 활용한다고 했습니다.
이 두가지 인터페이스가 핵심입니다.
폴링될 오브젝트는 해당 두 인터페이스를 통해 폴에 들어가고(Push) 꺼내집니다(Pull)
오른쪽 인터페이스는 대체로 폴이 될 오브젝트에 쓰입니다. ReturnToPool을 통해 개체를 폴에 들어가게 합니다.
폴 개체가 들어갈 폴 만들기
이제 폴 개체들을 담는 폴을 만들어봅시다.
ObjectPool.cs 를 생성해주시고 아래 코드를 입력해주세요.
using System.Collections.Generic;
using UnityEngine;
using System;
namespace ObjectPooling
{
public class ObjectPool<T> : IPool<T> where T : MonoBehaviour, IPoolable<T>
{
public ObjectPool(GameObject pooledObject, int numToSpawn = 0)
{
this.prefab = pooledObject;
Spawn(numToSpawn);
}
public ObjectPool(GameObject pooledObject, Transform parent, int numToSpawn = 0)
{
this.prefab = pooledObject;
Spawn(numToSpawn, parent);
}
public ObjectPool(GameObject pooledObject, Action<T> pullObject, Action<T> pushObject, int numToSpawn = 0)
{
this.prefab = pooledObject;
this.pullObject = pullObject;
this.pushObject = pushObject;
Spawn(numToSpawn);
}
private System.Action<T> pullObject;
private System.Action<T> pushObject;
private Stack<T> pooledObjects = new Stack<T>();
private GameObject prefab;
public int pooledCount
{
get
{
return pooledObjects.Count;
}
}
public T Pull()
{
T t;
if (pooledCount > 0)
t = pooledObjects.Pop();
else
t = GameObject.Instantiate(prefab).GetComponent<T>();
t.gameObject.SetActive(true); //ensure the object is on
t.Initialize(Push);
//allow default behavior and turning object back on
pullObject?.Invoke(t);
return t;
}
public T Pull(Vector3 position)
{
T t = Pull();
t.transform.position = position;
return t;
}
public T Pull(Vector3 position, Quaternion rotation)
{
T t = Pull();
t.transform.position = position;
t.transform.rotation = rotation;
return t;
}
public GameObject PullGameObject()
{
return Pull().gameObject;
}
public GameObject PullGameObject(Vector3 position)
{
GameObject go = Pull().gameObject;
go.transform.position = position;
return go;
}
public GameObject PullGameObject(Vector3 position, Quaternion rotation)
{
GameObject go = Pull().gameObject;
go.transform.position = position;
go.transform.rotation = rotation;
return go;
}
public GameObject PullGameObject(Vector3 position, Quaternion rotation, Transform parent)
{
GameObject go = Pull().gameObject;
go.transform.SetParent(parent);
go.transform.position = position;
go.transform.rotation = rotation;
return go;
}
public void Push(T t)
{
pooledObjects.Push(t);
//create default behavior to turn off objects
pushObject?.Invoke(t);
t.gameObject.SetActive(false);
}
private void Spawn(int number)
{
T t;
for (int i = 0; i < number; i++)
{
t = GameObject.Instantiate(prefab).GetComponent<T>();
pooledObjects.Push(t);
t.gameObject.SetActive(false);
}
}
private void Spawn(int number, Transform parent)
{
T t;
for (int i = 0; i < number; i++)
{
t = GameObject.Instantiate(prefab).GetComponent<T>();
pooledObjects.Push(t);
t.transform.SetParent(parent);
t.gameObject.SetActive(false);
}
}
}
public interface IPool<T>
{
T Pull(); //* 옵젝 폴에서 꺼내기
void Push(T t); //* 넣기
}
public interface IPoolable<T>
{
void Initialize(System.Action<T> returnAction);
void ReturnToPool();
}
}
일단 폴이 완성되었으니 폴 객체도 만들어봅시다. 코드 설명은 더 아래에서 하겠습니다.
폴 객체
PoolObject.cs
using System;
using UnityEngine;
namespace ObjectPooling
{
public class PoolObject : MonoBehaviour, IPoolable<PoolObject>
{
private Action<PoolObject> returnToPool; //* 다시 Push할 때 일어날 액션
protected virtual void OnDisable()
{
ReturnToPool();
}
public void Initialize(Action<PoolObject> returnAction)
{
//cache reference to return action
this.returnToPool = returnAction;
}
public void ReturnToPool()
{
//invoke and return this object to pool
returnToPool?.Invoke(this);
}
}
}
사용법
폴도 있고 폴 객체도 있고 이제 폴과 폴 객체를 일반적인 클래스에서 동시에 관리해봅시다.
Spawner.cs를 만들어 아래 코드를 작성해주세요.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 오브젝트 폴링으로 소환하는 예제 스포너입니다.
/// </summary>
namespace ObjectPooling
{
public class Spawner : MonoBehaviour
{
public GameObject cubePrefab;
public GameObject spherePrefab;
public GameObject capsulePrefab;
[Range(1f, 15f)]
public float range = 5f;
private static ObjectPool<PoolObject> cubePool;
private static ObjectPool<PoolObject> spherePool;
private static ObjectPool<PoolObject> capsulePool;
public bool canSpawn = true;
private void OnEnable()
{
cubePool = new ObjectPool<PoolObject>(cubePrefab, ResetOnPull, ResetOnPush);
spherePool = new ObjectPool<PoolObject>(spherePrefab);
capsulePool = new ObjectPool<PoolObject>(capsulePrefab);
StartCoroutine(SpawnOverTime());
}
IEnumerator SpawnOverTime()
{
while (canSpawn)
{
Spawn();
yield return null;
}
}
public void Spawn()
{
int random = Random.Range(0, 3);
Vector3 position = Random.insideUnitSphere * range + this.transform.position;
GameObject prefab;
switch (random)
{
case 0:
prefab = cubePool.PullGameObject(position, Random.rotation);
break;
case 1:
prefab = spherePool.PullGameObject(position, Random.rotation);
break;
case 2:
prefab = capsulePool.PullGameObject(position, Random.rotation);
break;
default:
prefab = cubePool.PullGameObject(position, Random.rotation);
break;
}
//* 이렇게 스폰시 호출될 구문은 여기말고 아래 호출 함수로 만드세요
// prefab.GetComponent<Rigidbody>().velocity = Vector3.zero;
}
//* 이렇게 스폰시 호출될 함수는 Pull시 호출될 액션에 포함할 함수로 만드는게 맞습니다.
private void ResetOnPull(PoolObject poolObject)
{
poolObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
}
private void ResetOnPush(PoolObject pushObject)
{
pushObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
}
}
}
코드를 보다시피 이런식으로 원하는 객체를 원하는 폴에서 관리할 수 있습니다.
폴과 폴 객체를 한 눈에 볼 수 있으니 굉장히 편하고 버그가 발생할 우려가 적습니다.
번외
저는 이러한 스포너가 자동으로 스폰되도록 구현하고 싶어서 자동스폰하는 추상 매니저 클래스를 하나 더 만들었습니다.
using System.Collections;
using System.Collections.Generic;
using ObjectPooling;
using UnityEngine;
public abstract class ObjectPoolManager : MonoBehaviour
{
public int activeObjCount = 0; // 현재 활성화된 오브젝트 수
protected abstract void ResetOnPull(PoolObject poolObject);
protected abstract void ResetOnPush(PoolObject poolObject);
protected abstract void ResetAllPools();
[Header("자동스폰 원할시 아래 설정")]
public bool autoSpawn = false;
public float spawnTimer = 4f;
public float spawnTimerMax = 4f;
}
이 클래스를 상속해서 자동 스폰되게 구현하시면 됩니다.
Unitask나 코루틴이나 Update문 셋 중 원하시는 방식으로 타이머 자동 스폰을 구현하시면 되겠습니다.
1년간 이 시스템을 통해 다양한 오브젝트를 폴링해봤습니다.
객체지향을 잘 다루신다면 정말 이만한 시스템이 없다고 생각합니다.
그만큼 장점이 많은 방식이니 오브젝트 폴링을 많이 사용하신다면 시간 절약겸 최적화 해보시기 바랍니다.
참조
https://onewheelstudio.com/blog/2022/1/17/object-pooling-20
'유니티 > 유니티 관련 지식' 카테고리의 다른 글
머티리얼을 위한 텍스처 사이트 모음 (0) | 2024.11.25 |
---|---|
유니티 에셋 다운로드 폴더 변경 - 윈도우 (2) | 2024.11.15 |
AsstStudio : 유니티로 만든 게임들 해부하기 (4) | 2024.11.14 |
[5] 유니티 MVVM으로 체력바 구현하는 법 (0) | 2024.11.13 |