오브젝트 풀링은 유니티에서 자주 사용되는 오브젝트를 생성과 파괴가 아닌 리스트(List)나 큐(Queue) 등에 저장해 두고 불러와 사용하고 사용 후엔 다시 반납하는 역할을 합니다.
유니티에서 ObjectPool이란 이름의 빈 오브젝트를 하나 만들고 아래 ObjectPool.cs를 넣어 줍니다.
[System.Serializable]
public class ObjectInfo {
public string objectName;
public GameObject perfab;
public int count;
}
public class ObjectPool : MonoBehaviour
{
public static ObjectPool instance;
[SerializeField]
ObjectInfo[] objectInfos = null;
[Header("오브젝트 풀의 위치")]
[SerializeField]
Transform tfPoolParent;
public List<Queue<GameObject>> objectPoolList;
void Awake() {
if (instance == null) {
instance = this;
} else {
Destroy(this.gameObject);
}
}
void Start() {
objectPoolList = new List<Queue<GameObject>>();
ObjectPoolState();
}
void ObjectPoolState() {
if (objectInfos != null) {
for (int i = 0; i < objectInfos.Length; i++) {
objectPoolList.Add(InsertQueue(objectInfos[i]));
}
}
}
Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
Queue<GameObject> test_queue = new Queue<GameObject>();
for (int i = 0; i < perfab_objectInfo.count; i++) {
GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
objectClone.SetActive(false);
objectClone.transform.SetParent(tfPoolParent);
test_queue.Enqueue(objectClone);
}
return test_queue;
}
}
[System.Serializable]
public class ObjectInfo {
public string objectName;
public GameObject perfab;
public int count;
}
오브젝트의 등록 정보를 관리합니다.
objectName -> 오브젝트의 이름
perfab -> 사용될 프리 펩 오브젝트
count -> 생성될 오브젝트의 수량
public static ObjectPool instance;
오브젝트 풀은 어디에서나 사용될 수 있도록 싱글톤 패턴을 줍니다.
[SerializeField]
ObjectInfo[] objectInfos = null;
등록될 오브젝트들의 목록을 여기에 저장합니다. (아래 이미지 참조)
[SerializeField]
Transform tfPoolParent;
생성된 클론(Clone) 오브젝트들이 Hierarchy창에서 위치할 곳을 정해 줍니다.
public List<Queue<GameObject>> objectPoolList;
위에서 설명된 objectInfos에 등록된 오브젝트들의 생성 수가 각각 다릅니다. 이 모든 걸 objectPoolList에 담아 둡니다.
여러 곳에서 오브젝트를 불러올 때 objectPoolList를 사용하게 됩니다.
void ObjectPoolState() {
if (objectInfos != null) {
for (int i = 0; i < objectInfos.Length; i++) {
objectPoolList.Add(InsertQueue(objectInfos[i]));
}
}
}
등록된 오브젝트의 목록을 List에 저장합니다.
Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
Queue<GameObject> test_queue = new Queue<GameObject>();
for (int i = 0; i < perfab_objectInfo.count; i++) {
GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
objectClone.SetActive(false);
objectClone.transform.SetParent(tfPoolParent);
test_queue.Enqueue(objectClone);
}
return test_queue;
}
이곳에서 등록된 오브젝트 목록의 수만큼 클론(Clone) 오브젝트를 생성 후 바로 위 소스의 objectPoolList로 넘겨줍니다.
GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
for문이 objectInfo의 count만큼 돌면서 objectClone을 생성 후
objectClone.SetActive(false)로 비활성화해 줍니다.
objectClone.transform.SetParent(tfPoolParent) 생성된 클론(Clone) 오브젝트의 부모 오브젝트를 지정해 줍니다.
이러면 Hierarchy창의 부모 오브젝트 밑으로 들어가게 됩니다. (아래 그림 참조)
test_queue.Enqueue(objectClone);
이렇게 만들어진 클론(Clone) 오브젝트를 큐(Queue)에 저장합니다.
for문만큼 돌며 만들어진 test_queue의 데이터를
return test_queue로 List<Queue<GameObject>> objectPoolList에 저장합니다.
이제 유니티로 가서 빈 오브젝트 3개를 만들고 이름을 각각 A, B, C로 하며 Add Component에서 Sprite Renderer를 추가하고 기본 사각 이미지와 컬러를 따로 지정해 줍니다 그런 후 프리펩(PerFab)으로 만든 후 Hierarchy창에서 삭제합니다. (아래 이미지 참조)
이제 맨 처음 만들어 둔 ObjectPool을 선택해 오브젝트를 등록해 줍니다. (아래 이미지 참조)
위 이미지를 보시면 A오브젝트는 5개를 만들며 B는 2개 C는 3개입니다.
게임을 플레이해 보시면
위 이미지와 같이 클론(Clone) 오브젝트가 수량에 맞게 생성되었으며 ObjectPool 밑으로 들어가 있습니다.
A오브젝트는 objectPoolList [0]에 들어가 있으며
B오브젝트는 objectPoolList [1]에 C오브젝트는 objectPoolList [2]에 저장되어 있습니다.
objectInfos의 순서대로 objectPoolList에 저장됩니다.
이제 마지막으로 이렇게 만들어진 오브젝트를 사용하면 됩니다.
플레이어가 스페이스(Space)를 누르면 A 클론(Clone) 오브젝트를 사용 후 반납까지 알아보겠습니다.
A 클론(Clone) 오브젝트를 총알이라 가정하고 보시면 편하겠네요.
Player.cs에서
void Update() {
if (Input.GetKeyDown(KeyCode.Space)) {
GameObject objTest = ObjectPool.instance.objectPoolList[0].Dequeue();
objTest.SetActive(true);
float pointX = Random.Range(- 1f, 1f);
objTest.transform.position = new Vector2(gameObject.transform.position.x + pointX,
gameObject.transform.position.y);
StartCoroutine(ObjReturn(objTest));
}
}
IEnumerator ObjReturn(GameObject _obj) {
yield return new WaitForSeconds(2);
ObjectPool.instance.objectPoolList[0].Enqueue(_obj);
_obj.SetActive(false);
}
스페이스(Space) 키를 누르면
objTest = ObjectPool.instance.objectPoolList[0].Dequeue() -> objTest에 objectPoolList[0] 오브젝트 하나를 Dequeue() 가져옵니다.
objTest.SetActive(true) 위쪽 클론(Clone)을 만들면서 비활성화 한 오브젝트를 다시 활성화시켜 줍니다.
float pointX = Random.Range(- 1f, 1f);
objTest.transform.position = new Vector2(gameObject.transform.position.x + pointX,
gameObject.transform.position.y);
StartCoroutine(ObjReturn(objTest));
오브젝트의 위치를 잡아 준 후 코루틴 호출
IEnumerator ObjReturn(GameObject _obj) {
yield return new WaitForSeconds(2);
ObjectPool.instance.objectPoolList[0].Enqueue(_obj);
_obj.SetActive(false);
}
사용된 클론(Clone) 오브젝트를 2초 후 다시 반납합니다.
큐(Queue)는 사용 후 Enqueue(_obj)로 반납해 주셔야 됩니다.
objectPoolList[0] 가져온 리스트의 Index 위치와 반납될 리스트의 위치가 같아야 합니다.
그런 후 _obj.SetActive(false)로 오브젝트를 비활성화해 주시면 됩니다.
게임을 플레이 후 화면에 보시면
스페이스(Space) 키를 누를 때마다 Object Pool 하위의 A(Clone) 오브젝트가 순차적으로 생성되며 2초 후 재 사용을 위해 순차적으로 사라지는 걸 확인할 수 있습니다.
마지막으로 오브젝트 풀 코드를 보시면 싱글톤 패턴을 사용했지만 objectPoolList만 필요할 뿐 나머진 외부에서 사용할 일이 없습니다.
이럴 땐 싱글톤 패턴이 적용된 예로 GamaManager가 싱글톤 패턴이 적용되어 있다면 여기에 델리게이트를 선언하여 objectPoolList를 연결해 사용하면 더 좋을 겁니다.
델리게이트에 연결된 코드도 한번 보시고 편한 걸로 사용해 보시기 바랍니다.
위에서도 예로 들었지만 GamaManager에 싱글톤 패턴이 적용되어 있습니다.
using System;
public class GameManager : MonoBehaviour {
public Func<int, GameObject> objectPool;
public Action<int, GameObject> returnPool;
}
Func와 Action을 사용하기 위해 using System을 선언합니다.
Func<int, GameObject> int를 넘겨주며 GameIObject를 받습니다.
Action<int, GameObject> int와 GameIObject 넘겨줍니다.
[System.Serializable]
public class ObjectInfo {
public string objectName;
public GameObject perfab;
public int count;
}
public class ObjectPool : MonoBehaviour
{
[SerializeField]
ObjectInfo[] objectInfos = null;
[Header("오브젝트 풀의 위치")]
[SerializeField]
Transform tfPoolParent;
List<Queue<GameObject>> objectPoolList;
void Start() {
GameManager.instance.objectPool += PoolList;
GameManager.instance.returnPool += ReturnPool;
objectPoolList = new List<Queue<GameObject>>();
ObjectPoolState();
}
//* Func 연결
GameObject PoolList(int _index) {
return objectPoolList[_index].Dequeue();
}
//* Action 연결
void ReturnPool(int _index, GameObject _poolObject) {
objectPoolList[_index].Enqueue(_poolObject);
}
void ObjectPoolState() {
if (objectInfos != null) {
for (int i = 0; i < objectInfos.Length; i++) {
objectPoolList.Add(InsertQueue(objectInfos[i]));
}
}
}
Queue<GameObject> InsertQueue(ObjectInfo perfab_objectInfo) {
Queue<GameObject> test_queue = new Queue<GameObject>();
for (int i = 0; i < perfab_objectInfo.count; i++) {
GameObject objectClone = Instantiate(perfab_objectInfo.perfab) as GameObject;
objectClone.SetActive(false);
objectClone.transform.SetParent(tfPoolParent);
test_queue.Enqueue(objectClone);
}
return test_queue;
}
}
ObjectPool.cs에 기존 싱글톤 패턴은 지웠습니다.
Start()에
GameManager.instance.objectPool += PoolList;
GameManager.instance.returnPool += ReturnPool;
PoolList()와 ReturnPool() 함수를 델리게이트에 연결해 줍니다.
//* Func 연결
GameObject PoolList(int _index) {
return objectPoolList[_index].Dequeue();
}
PoolList는 가져다 사용할때 사용합니다.
//* Action 연결
void ReturnPool(int _index, GameObject _poolObject) {
objectPoolList[_index].Enqueue(_poolObject);
}
ReturnPool은 반납할 때 사용합니다.
사용하는 방법은
GameObject damagePrefab = GameManager.instance.objectPool.Invoke(0);
GameManager.instance.objectPool.Invoke(0) -> objectPoolList[0] 0번 리스트에 저장된 오브젝트를 가져옵니다.
사용 후 반납은
GameManager.instance.returnPool.Invoke(0, this.gameObject);
objectPoolList[0] 0번 리스트에 오브젝트를 Enqueue 합니다.
간편하긴 하지만 델리게이트가 조금 어렵다 생각되시면 싱글톤 패턴을 사용하시면 됩니다.
이상 오브젝트 풀링(Object pooling)에 대해 알아봤습니다.
'Unity' 카테고리의 다른 글
유니티 c# - 델리게이트(Delegate) Action 사용해 보기 (0) | 2024.07.09 |
---|---|
유니티 - 게임 데이터 JSON으로 저장하기 (4) | 2024.07.09 |
유니티 (Unity) - 파일 데이터 비동기 방식으로 읽어 오기 (0) | 2024.07.09 |
유니티 - 싱글톤 패턴(Singleton) (0) | 2024.07.09 |
유니티 - 커스텀 에디터 (Custom Editor) (0) | 2024.07.09 |