Chapter 10. Pool Manager
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 10. Pool Manager
🚀 Object Pool
- 오브젝트 풀을 사용하는 이유
- 리소스 폴더에 있는 것을 Instantiate 하는 일련의 과정은 어마어마하게 느리다.
- SSD와 CPU는 여전히 물리적으로 거리가 떨어져 있기 때문이다.
- 👉 게임 시작 전에 미리 가져와서 로드해놓고 그를 재활용. 미리 로드해놓은 것을 켜주고 꺼주고 하는 식으로 관리하는 것.
- 리소스 폴더에 있는 것을 Instantiate 하는 일련의 과정은 어마어마하게 느리다.
런타임 도중의 생성이 빈번하게 일어날 오브젝트에 대해서만 풀링을 진행하며 된다.
풀링을 할 오브젝트, 풀링을 안 할 오브젝트를 구분해야 한다.
👉 이 구분을 풀링을 할 오브젝트들에만 📜Poolable 컴포넌트(스크립트)를 붙여 구분한다.
- 📜PoolManager
- 오브젝트 풀링 관리
- 📜Manager로 부터 사용
- 📜ResourceManager 를 보조 하는 역할.
- 📜Poolable
- 풀링 할 오브젝트들에 이 스크립트를 붙여 구분
- 즉 풀링할 프리팹에 붙여주면 된다.
- 풀링 할 오브젝트들에 이 스크립트를 붙여 구분
프리팹(원본)을 통해 그때 그때 Instantiate 해 클론 오브젝트를 생성하기보단 한번 Instantiate 한 것을 계속 재활용.
풀은 미리 만들어두고 사용되기를 기다리는 오브젝트들의 대기 모임
- 풀에 미리 Instantiate 한 오브젝트들을 모아놓고 평소엔 비활성화 한다.
- 그리고 사용할 때만 활성화하여 풀에서 빼내온다. 👉 Pop
- 이제 사용하지 않을 때만 마치 파괴되는 것처럼 비활성화만 하고 다시 풀에 넣는다. 👉 Push
📜 Poolable
풀링 할 프리팹에 이 스크립트를 붙여 구분
UnityChan
프리팹에 붙어 있는 상태
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Poolable : MonoBehaviour
{
public bool IsUsing;
}
별 내용 없다! 그냥 이 스크립트가 붙어있으면 풀링 대상이라고 보기 위해 구분해주기 위해서 만든 스크립트.
📜 Manager
PoolManager _pool = new PoolManager();
public static PoolManager Pool { get { return Instance._pool; } }
static void Init()
{
// ...
s_instance._pool.Init();
}
public static void Clear()
{
// ...
Pool.Clear();
}
📜 Pool 클래스
PoolManager 는 여러개의 Pool 들을 가지고 있다.
하나의 풀 Pool
- @Pool_Root 👉 전체의 풀들을 한데 모음
- UnityChan_Root 👉 UnityChan_Root 원본 프리팹을 통해 만든 대기중인 UnityChan_Root 오브젝트들 모아둔 부모오브젝트, 풀
- Bird_Root 👉 Bird_Root 원본 프리팹을 통해 만든 대기중인 Bird_Root 들 모아둔 부모오브젝트, 풀
UnityChan_Root
, Bird_Root
같은게 각각 하나의 Pool 객체가 된다.
예시. 현재 UnityChan_Root
2개가 풀링 中. 즉 비활성화 상태로 대기하며, 재활용되어 사용되기를 기다리고 있다. 이렇게 풀링 중인 상태일 때는 @Pool_Root
- UnityChan_Root
빈 오브젝트 산하에서 비활성화 상태로 있는다. (스택에 Push가 되어 있는 상태)
예시. 마치 Instantiate 된 것 같은 효과로, 풀링 되어 있던 UnityChan_Root
오브젝트 2개를 활성화하고 @Pool_Root
- UnityChan_Root
로부터 벗어나, 원래 Hierarchy상에서의 부모 산하로 옮겨준다. 이젠 풀링 대기 상태가 아닌 사용 중인 상태. (스택에서 Pop가 되어 있는 상태)
class Pool
{
public GameObject Original { get; private set; }
public Transform Root { get; set; }
Stack<Poolable> _poolStack = new Stack<Poolable>();
Original
원본 프리팹Root
풀 이름 ex. UnityChan_Root, Bird_Root_poolStack
풀에 모여 있는 오브젝트(📜Poolable 붙어있는 상태)들 스택으로 관리
public void Init(GameObject original, int count = 5)
{
Original = original;
// UnityChan_Root 빈 오브젝트 생성.
Root = new GameObject().transform;
Root.name = $"{original.name}_Root";
// count 개수의 오브젝트들을 UnityChan_Root의 자식으로. 이 5 개를 재활용할 것 👉 오브젝트 풀링
for (int i = 0; i < count; i++)
Push(Create());
}
- 하나의 풀 초기화 (원본 프리팹
original
, 풀링할 오브젝트 개수count
)Original
원본프리팹- 풀링에 사용할 오브젝트들을
Root
(ex. UnityChan_Root) 오브젝트 산하에 둘 것 count
개수의 오브젝트를 생성하고 풀링하기 위해 스택에 넣어주기. 밑에 Push 참고
Poolable Create()
{
GameObject go = Object.Instantiate<GameObject>(Original);
go.name = Original.name; // 뒤에 붙는 (Clone) 없앰. 원본 프리팹과 이름 같게.
return go.GetOrAddComponent<Poolable>();
}
- 원본 프리팹으로부터 풀링에 사용할 오브젝트를 생성한다. 그리고 이 오브젝트를 📜Poolable로서 리턴.
- 이름은 원본 프리팹과 이름 같게.
public void Push(Poolable poolable) // 풀에 넣어주기 (오브젝트 비활성화)
{
if (poolable == null)
return;
poolable.transform.parent = Root;
poolable.gameObject.SetActive(false);
poolable.IsUsing = false;
_poolStack.Push(poolable);
}
- 풀에 넣어준다는 것은 곧 오브젝트를 비활성화 해놓고 사용될 때까지 대기한다는 것이다. (마치 Destroy 하는 효과)
- 풀에서 대기중인 오브젝트는
Root
의 자식이어야 함- 풀에서 대기중일땐 UnityChan_Root 의 자식이다가 진짜 활성화되어 사용될 떈 풀에서 빠져나와 게임 중에서의 원래 부모의 자식으로 부모 바꿔 설정할 것
- 풀에서 대기중인 오브젝트는
- 부모는
Root
로, 비활성화, 스택에 넣어 대기시키기
public Poolable Pop(Transform parent) // 풀로부터 꺼내오기 (오브젝트 활성화)
{
Poolable poolable;
if (_poolStack.Count > 0) // 스택(대기상태)이 빈 크기 X 즉 하나라도 재활용 할 수 있는 애가 있다면
poolable = _poolStack.Pop();
else // 스택(대기상태)이 지금 비었다면 재활용 할 수 있는 애가 없으므로 새로 만들어야
poolable = Create();
poolable.gameObject.SetActive(true); // 활성화 (poolable.gameObject로 접근해서 활성화)
// DontDestroyOnLoad 해제 용도
if (parent == null)
poolable.transform.parent = Managers.Scene.CurrentScene.transform;
// poolable 👉 풀에서 꺼낸 오브젝트의 Poolable
poolable.transform.parent = parent; // 파라미터로 받은 parent 를 부모로 설정
poolable.IsUsing = true;
return poolable;
}
}
parent
👉 대기 상태가 아닌 활성화 상태로 풀 밖에서 게임 안에서 사용될 때의 부모. 원래 부모.- 풀에 빼낸다는 것은 곧 오브젝트를 활성화 해서 사용하는 것이다. 생성되는 것 같은효과.
poolable
에다가 오브젝트 받고 리턴- 스택이 비어있지 않다면 재활용할 수 있는 대기 상태인 오브젝트가 있다는 것이니 그것을 사용하도록 한다. 스택에서 빼서 사용
- 스택이 비어있다면 새로 만들어야한다. Instantiate 필요. Create 호출.
- 활성화 (
poolable.gameObject
로 접근해서 활성화) - 풀에서 대기 중일때의 부모로부터 원래 게임에서의 부모로 설정.
- 아래 부분에 대한 설명은 맨 밑에 DontDestroyOnLoad 의 특성 참고
// DontDestroyOnLoad 해제 용도 if (parent == null) poolable.transform.parent = Managers.Scene.CurrentScene.transform;
📜 PoolManager
📜ResourceManager 를 보조 하는 역할.
여러개의 📜Pool 객체들을 관리. 즉 여러개의 풀 관리.
@Pool_Root
👉 전체 풀 관리- 여러개의 풀
- 각각의 풀에 속한 오브젝트들(재활용 대상)
- 여러개의 풀
멤버
Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
Transform _root;
_pool
풀들을 미리 로드해와 모아둘 그 ‘Pool’- 관련있는 오브젝트들을 모으는 것도 하나의
Pool
이다. (위의 Pool 클래스) - 풀도 여러개일 수 있다.
- ex)
- 무기 프리팹으로 생성되어 재활용할 무기 오브젝트들 모여있는 풀
- 플레이어 프리팹으로 생성되어 재활용할 플레이어 오브젝트들 모여있는 풀
- 나무 프리팹으로 생성되어 재활용할 나무 오브젝트들 모여있는 풀
- ex)
- 이들을 모아둔 Dictionary이므로 즉, 게임 내의 모든 전체 풀들을
_pool
에서 관리. - Key는 원본 프리팹의 이름으로 쓸 것!
- 관련있는 오브젝트들을 모으는 것도 하나의
- 풀들을
_root
(@Pool_Root
)의 자식으로 묶어 정리할 것이다.
Init 초기화
public void Init()
{
if (_root == null)
{
_root = new GameObject { name = "@Pool_Root" }.transform;
Object.DontDestroyOnLoad(_root);
}
}
- 풀링 할 오브젝트들을 모아서 그룹화해 정리할
@Pool_Root
오브젝트를 만든다.- 풀링 오브젝트들은 이 오브젝트의 자식으로 묶일 것이며
- 게임 내내 유지되도록
@Pool_Root
오브젝트를 DontDestroyOnLoad 처리 한다.
Push 다 사용한 오브젝트 풀에 다시 넣어 대기 상태로 만들기
public void Push(Poolable poolable)
{
string name = poolable.gameObject.name;
if (_pool.ContainsKey(name) == false)
{
GameObject.Destroy(poolable.gameObject);
return;
}
_pool[name].Push(poolable);
}
- 그냥
_pool[name].Push(poolable)
을 해주면 땡- 이름(Key)과 일치하는 해당 풀에 해당 오브젝트
poolable
을 Push 함수 호출해 넣어줌
- 이름(Key)과 일치하는 해당 풀에 해당 오브젝트
- 풀링하지 않는 오브젝트는 파괴
CreatePool 풀 만들기
public void CreatePool(GameObject original, int count = 5)
{
Pool pool = new Pool();
pool.Init(original, count); // Init 을 통해 해당 Pool은 DontDestroyOnLoad가 된다.
pool.Root.parent = _root;
_pool.Add(original.name, pool);
}
- 풀을 생성하고 풀의 Init 함수 호출
- 풀들은
@Pool_Root
(_root
)의 자식이어야 한다. _pool
Dictionary에 추가해준다.- Key는 프리팹 이름인
original.name
으로 풀을 추가해준다.
- Key는 프리팹 이름인
Pop 풀로부터 사용할 오브젝트 리턴
public Poolable Pop(GameObject original, Transform parent = null)
{
if (_pool.ContainsKey(original.name) == false) // Key는 원본 프리팹 이름으로 저장되므로 해당 프리팹으로 만든 오브젝트풀이 있나 검색.
CreatePool(original); // 없다면 새로운 풀을 만든다.
return _pool[original.name].Pop(parent); // 풀이 없다면 여기서 런타임 에러 날 것이므로 위의 과정을 해주는 것. 아룸아 original.name인 풀이 아직 없다면 만들어주기.
}
_pool
Dictionary에서 보관 중인 original
프리팹 이름에 해당하는 Key의 Value인 풀을 리턴한다.
- 리턴한
Pool
에서 Pop 호출- 풀 Stack (풀 마다 본인의 오브젝트들 보관하는_
poolStack
)에서 가장 위에 있는 오브젝트를 pop하고(후입선출) 활성화하고 그 오브젝트의 부모를parent
로 한다.
- 풀 Stack (풀 마다 본인의 오브젝트들 보관하는_
- CreatePool(original); 👉 디폴트로 5 개 생성
GetOriginal 프리팹 가져오기
public GameObject GetOriginal(string name)
{
if (_pool.ContainsKey(name) == false)
return null;
return _pool[name].Original;
}
- 📜 ResourceManager 의 Load 함수에서 호출 시킬 것이다.
- 그래서 public 이고
original.name
을 사용하지 않고 그냥name
매개 변수로 설정.
_pool
Dictionary을 통해Pool
Value의Original
에 원본 프리팹 담고 있으니 이를 리턴해주면 된다.- Key가 없을 수도 있으니 위에 미리 체크. 없다면 null 리턴.
Clear 풀 날리기
public void Clear()
{
foreach (Transform child in _root)
GameObject.Destroy(child.gameObject);
_pool.Clear();
}
여러 가지의 Pool
을 전부 날리자. Dictionary도 비우기. 풀에 비활성화 상태로 대기 중인 오브젝트들은 _root
(@Pool_Root) child
(UnityChan_Root)의 자식들로 있는 상태일테니 이것들도 다 날라갈 것.. 다른 씬에서는 해당 풀에 있는 오브젝트들을 다신 안 쓰는 경우가 생기면 이렇게 풀을 다 날려 버리는 기능이 필요할 것이다.
📜 ResourceManager
풀링이 필요한 오브젝트인지 아닌지를 구분해서 로드 및 생성 및 파괴할 필요가 있음.
- Init
- 원본(프리팹)을 이미 들고 있다면 바로 사용하기
- 매번 Instantiate으로 사본 생성하는 것이 아니라 혹시 풀링된 애가 있으면 그것을 재활용하기
- Destroy
- 만약 풀링이 필요한 애라면 파괴하는게 아니라 풀링 매니저에게 위탁해서 단순 비활성화시키기
Load
public T Load<T>(string path) where T : Object
{
if (typeof(T) == typeof(GameObject))
{
string name = path;
int index = name.LastIndexOf('/'); // '/' 뒤의 이름 추출.
if (index >= 0)
name = name.Substring(index + 1); // 이게 바로 프리팹의 이름.
GameObject go = Managers.Pool.GetOriginal(name);
if (go != null)
return go as T;
}
// 풀에서 못 찾았다면 힘들게 로딩
return Resources.Load<T>(path); // UnityEngine의 Resource.
}
프리팹을 로드하는 것 또한 풀에 있으면 풀에서 가져온다. Instantiate을 줄이려고 하듯, 로드 또한 최대한 줄이기 위해!
- 프리팹을 로드할 때 프리팹 또한 Pool에 있으면 로드하지 않고 거기서 가져 온다.
- 이미 Pool 에 프리팹으로 생성한 오브젝트가 있다면
Pool
의Original
에 저장되어 있을 것이기 때문에 GetOriginal 함수를 통해 가져올 수 있다. - 풀에 없는 프리팹이라면 힘겹게 로컬 폴더로부터 Resources.Load<T>(path)을 호출해 로딩.
- 이미 Pool 에 프리팹으로 생성한 오브젝트가 있다면
Instantiate
public GameObject Instantiate(string path, Transform parent = null)
{
GameObject original = Load<GameObject>($"Prefabs/{path}");
if (original == null)
{
Debug.Log($"Failed to load prefab : {path}");
return null;
}
if (original.GetComponent<Poolable>() != null)
return Managers.Pool.Pop(original, parent).gameObject;
GameObject go = Object.Instantiate(original, parent);
go.name = original.name;
return go;
}
Load 함수로부터 받은 프리팹을 통해 오브젝트를 Instantiate 한다.
- 해당 프리팹에 📜Poolable 이 있다는 것은 풀링으로 관리된다는 오브젝트라는 것이다. 따라서 이 경우엔 Instantiate 하지 않고 풀에서 대기 중인 비활성화 오브젝트를 가져와 활성화하여 재사용한다.
- 스택에서 pop (Pop 함수에서 해줄 것.)
- 해당 프리팹에 📜Poolable 이 없다는 것은 풀링으로 관리되지 않는다는 것이니 Instantiate 해야 한다.
Destroy
public void Destroy(GameObject go)
{
if (go == null)
return;
Poolable poolable = go.GetComponent<Poolable>();
if (poolable != null)
{
Managers.Pool.Push(poolable);
return;
}
Object.Destroy(go);
}
- 해당 프리팹에 📜Poolable 이 있다는 것은 풀링으로 관리된다는 오브젝트라는 것이다. 따라서 이 경우엔 Destroy 하지 않고 나중에 다시 재사용할 수 있도록 비활성화해서 풀의 스택에 다시 Push 해준다.
- 해당 프리팹에 📜Poolable 이 없다는 것은 풀링으로 관리되지 않는다는 것이니 그냥 Destroy 한다.
📜테스트
public class LoginScene : BaseScene
{
protected override void Init()
{
base.Init();
SceneType = Define.Scene.Login;
for (int i = 0; i < 10; i++)
Managers.Resource.Instantiate("UnityChan");
}
“LoginScene” 씬이 시작되면 UnityChan
오브젝트를 10 개 만들도록 한다.
- (i = 0 ~ i = 4) 5 개
UnityChan
오브젝트- DontDestroyOnLoad인
@Pool_Root
생성 (이 자식들도 전부 DontDestroyOnLoad) - 첫번째
UnityChan
오브젝트 생성시- 📜Resource의 Instantiate 부분 안에서 📜PoolManager의 Pop 함수 호출 👉 풀이 존재하지 않는 상태이므로 CreatePool 호출 👉 풀을 생성하고
Pool
클래스의 Init 호출 👉 count 의 디폴트 값은 5 이므로 기본적으로 5 개의 게임 오브젝트를 생성해서 스택에 넣어준다.- 스택에 넣어주는
Pool
클래스의 Push 함수 과정에서 이 오브젝트들의 부모는@Pool_Root
산하가 된다.
- 스택에 넣어주는
- 첫번째 오브젝트는 풀을 만들고 스택에 5 개 오브젝트를 생성해 넣어주는 위 과정을 끝내고 여기에서 바로 Pop 된다.
- 📜Resource의 Instantiate 부분 안에서 📜PoolManager의 Pop 함수 호출 👉 풀이 존재하지 않는 상태이므로 CreatePool 호출 👉 풀을 생성하고
- 두번째 ~ 다섯번재 오브젝트들은 위에서 만든 5 크기의 스택에서(현재 크기 4) 하나 하나 빼와서 재활용한다. 비활성화 된 상태에서 풀에 있던 오브젝트들을 뺴와 활성화함.
- 이 과정에서 첫 번째 오브젝트 생성시 만들어둔 풀 스택에 비활성화 상태로 대기하고 있던 5 개의 오브젝트들을 Pop 하고 활성화되면서 더 이상
@Pool_Root
오브젝트의 자식들이 아니게 된다.- 그러나 Pop을 통해 이제 transform.parent = null 부모가 없는 상태가 되고 활성화되더라도 DontDestroyOnLoad 는 한번 이 DontDestroyOnLoad 범위 안에서 생성이 되었으면 DontDestroyOnLoad을 벗어나지 못하기 때문에 위 사진처럼 5개의 오브젝트는 `@Pool_Root` 부모로부터는 벗어났지만 여전히 DontDestroyOnLoad을 범위 안에 있는 것을 확인할 수 있다.
- DontDestroyOnLoad인
- (i = 5 ~ i = 9) 5 개
UnityChan
오브젝트- 풀은 존재하긴 하지만 스택에 아무것도 없기 때문에 (i = 0 ~ i = 4 에서 다 Pop 해가서 실사용中..) 재사용을 할 수 없어 새로 만들어야 한다.
Pool
클래스의 Pop에서 else에 걸려 Create() 된다.- 이 경우엔 CreatePool을 통한 Init 을 거치지 않아서
@Pool_Root
산하에 있지 않는다. 그냥!!! 이건 풀에 넣어주기 위해 생성하는게 아니라 그냥 바로 실사용으로 쓰기 위해 Instantiate 하는 것이기 때문이다.
- 이 경우엔 CreatePool을 통한 Init 을 거치지 않아서
- 따라서 이때 생성된 6 ~ 10번째 오브젝트들은 DontDestroyOnLoad 범위에 있지 않는다.
- 풀은 존재하긴 하지만 스택에 아무것도 없기 때문에 (i = 0 ~ i = 4 에서 다 Pop 해가서 실사용中..) 재사용을 할 수 없어 새로 만들어야 한다.
DontDestroyOnLoad 의 특성
// 📜PoolManager
public void Init()
{
if (_root == null)
{
_root = new GameObject { name = "@Pool_Root" }.transform;
Object.DontDestroyOnLoad(_root);
}
}
📜PoolManager 의 Init 에서 @Pool_Root
라는 이름의 오브젝트를 생성하고 모든 풀들을 이 오브젝트의 자식으로 묶어서 관리를 할 것이다. 그리고 이 오브젝트는 DontDestroyOnLoad 되게끔 하여 풀들이 씬이 바뀌어도 삭제되지 않고 유지되도록 한다.
- 풀에서 빠져나와 활성화되어 진짜 사용이 될 때는 부모가
@Pool_Root
산하로부터 바뀌어야 한다. 원래 게임 Hierarchy상에서의 부모의 자식으로!
그러나 문제는 DontDestroyOnLoad 가 되면 DontDestroyOnLoad 를 빠져나갈 수 없다. transform.parent = null이 되어도 DontDestroyOnLoad 안에서만 빠져나가게 된다.
- 씬 이동을 할 때 5 개만 삭제되지 않고 나머지 5 개는 삭제되니 좀 일관적이지 못하다. 사실은 10개 다 실사용 하게 된 것이므로 10개 다 밖에 있어야 예쁜 모양인데 말이다. (정확히는 5개는 이미 생성된 풀에서 가져온 것, 5개는 Instantiate)
public Poolable Pop(Transform parent) // 풀로부터 꺼내오기 (오브젝트 활성화)
{
//...
// DontDestroyOnLoad 해제 용도
if (parent == null)
poolable.transform.parent = Managers.Scene.CurrentScene.transform; // @Scene 오브젝트
poolable.transform.parent = parent;
// ...
}
}
parent == null
상태라면 DontDestroyOnLoad 범위 안에서만 parent == null
상태가 될 것이므로 꼼수를 써서 Pop이 될 때는 DontDestroyOnLoad 밖에 있을 수 있도록 한번 @Scene
오브젝트의 자식으로 설정해주어 DontDestroyOnLoad 를 빠져나가게 한 다음에 poolable.transform.parent = parent;에 의해 부모가 null 이 될 것이다.
해결된 것 확인!
public class LoginScene : BaseScene
{
protected override void Init()
{
base.Init();
SceneType = Define.Scene.Login;
for (int i = 0; i < 2; i++)
Managers.Resource.Instantiate("UnityChan");
}
5개보다 작은 2개로 테스트 했을 땐 2개만 실사용되고 3개는 비활성화 상태로 풀에서 대기 중인 것을 확인할 수 있다.
public class LoginScene : BaseScene
{
protected override void Init()
{
base.Init();
SceneType = Define.Scene.Login;
List<GameObject> list = new List<GameObject>();
for (int i = 0; i < 5; i++)
list.Add(Managers.Resource.Instantiate("UnityChan"));
foreach (GameObject obj in list)
{
Managers.Resource.Destroy(obj);
}
}
전부 Destroy 해버리니까 진짜 파괴가 되버리는게 아니라 @Pool_Root
산하의 자식이 되어 비활성화 상태로 전환된 것을 확인할 수 있다.
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기