몬스터에 가하는 데미지를 구현하는데 추후에 수많은 캐릭터들이 수많은 몬스터를 때린다고 가정했을 때 데미지를 나타내주는 텍스트를 계속해서 생성하고 파괴했을 경우 비용이 적지않아 들거라 생각이 들어서 몬스터 각각에게 생성시에 데미지 Pooling UI를 달아주면 해결이 된다고 생각되어 이를 구현하게 되었다.
1. 기존 Pooling
기존에 오브젝트를 풀링하는 방식은 오브젝트를 Instantiate 할 때 클래스 (Poolable) 이 달려있다면, 이를 Pop하여 풀링하는 오브젝트로 만들어주는 방법으로 Pooling을 구현하였다. 이때 Pooling할 오브젝트의 프리팹 이름으로 Dictionary에 저장하여 Key값을 통해 Pop 과 Push를 불러오게끔 구현하는 방식이다.
2. Pooling 구현
GameManger의 하위에 DontDestroyOnLoad 하여 들고다닐 매니저들을 따로 구현하였는데, 그 중 하나가 PoolManger이다.
Pool Class
#region Pool
class Pool
{
public GameObject Original { get; protected set; }
public Transform Root { get; protected set; }
protected Stack<Poolable> _poolStack = new Stack<Poolable>();
public virtual void Init(GameObject original, int count = 20)
{
Original = original;
Root = new GameObject().transform;
Root.name = $"@{original.name}_Root";
for (int i = 0; i < count; i++)
Push(Create());
}
protected Poolable Create()
{
GameObject go = Object.Instantiate<GameObject>(Original);
go.name = Original.name;
return go.GetOrAddComponent<Poolable>();
}
public virtual void Push(Poolable poolable)
{
if (poolable == null) return;
poolable.transform.parent = Root;
poolable.gameObject.SetActive(false);
poolable.IsUsing = false;
_poolStack.Push(poolable);
}
public virtual Poolable Pop(Transform parent)
{
Poolable poolable;
if (_poolStack.Count > 0)
poolable = _poolStack.Pop();
else
poolable = Create();
poolable.gameObject.SetActive(true);
if (parent == null)
poolable.transform.parent = GameManager.Instance.Scene.CurrentScene.transform;
poolable.transform.parent = parent;
poolable.IsUsing = true;
return poolable;
}
}
#endregion
Stack으로 Pop과 Push를 통해 후입 선출로 구현하였으며, Init이 호출되면서 Root를 생성하고, 그 자식으로 Stack에 Push되면서 원하는 Count만큼 풀링할 오브젝트들을 대기시키게끔 구현하였다.
PoolingManager
Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
Transform _root;
public void Init()
{
_root = gameObject.transform;
}
public void CreatePool(GameObject original, int count = 20, Transform parent = null)
{
Pool pool = new Pool();
pool.Init(original, count);
if (parent != null)
pool.Root.parent = parent;
else
pool.Root.parent = _root.transform;
_pool.Add(original.name, pool);
}
public void Push(Poolable poolable)
{
string name = poolable.gameObject.name;
if (_pool.ContainsKey(name))
{
_pool[name].Push(poolable);
}
else
{
GameObject.Destroy(poolable.gameObject);
}
}
public Poolable Pop(GameObject original, int count = 20, Transform parent = null, Transform rootParent = null)
{
if (_pool.ContainsKey(original.name) == false)
CreatePool(original, count, rootParent);
return _pool[original.name].Pop(parent);
}
public GameObject GetOriginal(string name)
{
if (_pool.ContainsKey(name) == false)
return null;
return _pool[name].Original;
}
}
싱글톤 패턴의 GameManger에 등록되어 어디든 부를수 있는 PoolingManager에서 Pop을 통해 호출이 되면, 본격적으로 원하는 프리팹의 Pooling이 시작된다. 위에서 언급했듯이 Dictionary에 Prefabs의 이름을 통해 Key값으로 정해주고, 이를 Pooling하는 형식이다.
3. 문제점
오브젝트를 풀링하는 부분에서는 고유한 Prefab을 Pooling하는 형식이기 때문에 문제가 생기지 않는다. 그러나 UI 의 Pooling은 다른 문제이다. 가령 Slime이 20개 Pooling된다고 가정하면, 20개 내에서 또 각각의 데미지 UI가 Pooling되어야 한다. 그렇다면 1번 Slime에서도 풀링으로 등록한 데미지 텍스트를 가지고 있어야하고, 2번 3번 ... 20번 Slime들도 각각 Pooling된 데미지 텍스트를 가지고 있어야 하는데 이렇게 되면 문제는 Dictionary는 고유의 Key를 가지게 된다는 특성과, 이를 Prefab의 OriginalName으로 가지고 있다는 점이 문제가 된다. Slime이 여러마리 나오는 순간, Key값이 중복이 되어 오류가 나기 때문이다.
4. 해결책 제시
그렇다면 오브젝트와는 다른 UI Pooling을 비슷하지만 다르게 구현해야 한다. Dictionary의 Key값이 겹치기 때문에 Hash로 하려고 했으나 당연하지만 불러오는 과정에서 문제가 생긴다. 그렇다면 임의의 instance값을 통해 originalname옆에 _instance값 을 포함해서 Key값으로 호출한다면 Dictionary에 대한 문제는 해결될 것이다.
또한 이 Key값은 생성됨과 동시에 해당 오브젝트가 이 값을 가지게 된다면 추후 Push되더라도 다시 가져올 수 있게끔 구현하는것이 좋다고 생각한다. 계속 수를 늘리고 삭제한다면 무한정 챌린지의 경우 Slime의 데미지 텍스트의 Instance값이 무한하게 늘어나는 것을 방지하는 차원에서 이렇게 하는게 좋다고 판단하였다.
그리고 ObjectPooling과 Dicitonary의 Key값을 저장하는 방식 외에는 호출하는 부분에서는 비슷한 결을 가지기 때문에 중복되는 부분은 기존의 Pool을 상속받는 식으로 해주는것이 깔끔하다고 생각한다.
5. UI Pooling
#region UI Pool
class UIPool : Pool
{
public void Init(GameObject original, int instanceCount, int count = 20)
{
Original = original;
Root = new GameObject().transform;
Root.name = $"@{original.name}_Root{instanceCount}";
for (int i = 0; i < count; i++)
Push(Create(instanceCount));
}
public Poolable Create(int instanceCount)
{
GameObject go = Object.Instantiate<GameObject>(Original);
go.name = $"{Original.name}_{instanceCount}";
return go.GetOrAddComponent<Poolable>();
}
public override void Push(Poolable poolable)
{
base.Push(poolable);
}
public override Poolable Pop(Transform parent)
{
return base.Pop(parent);
}
}
#endregion
Pool하는 방식은 같기 때문에 같은 방식을 사용하되 OrigianlName만 뒤에 Instance값을 넣어 구분해주는 식으로 하였다.
PoolingManager (UI 부분)
Dictionary<string, Pool> _pool = new Dictionary<string, Pool>();
Dictionary<string, UIPool> _uiPool = new Dictionary<string, UIPool>();
Transform _root;
public void Init()
{
_root = gameObject.transform;
}
public void CreatePool(GameObject original, int count = 20, Transform parent = null)
{
Pool pool = new Pool();
pool.Init(original, count);
if (parent != null)
pool.Root.parent = parent;
else
pool.Root.parent = _root.transform;
_pool.Add(original.name, pool);
}
public void CreateUIPool(GameObject original, int instanceCount, int count = 20, Transform parent = null)
{
UIPool uiPool = new UIPool();
uiPool.Init(original, instanceCount, count);
if (parent != null)
uiPool.Root.parent = parent;
else
uiPool.Root.parent = _root.transform;
string originName = $"{original.name}_{instanceCount}";
_uiPool.Add(originName, uiPool);
}
public void Push(Poolable poolable)
{
string name = poolable.gameObject.name;
string customKey = poolable.CustomKey;
if (_pool.ContainsKey(name))
{
_pool[name].Push(poolable);
}
else if (_uiPool.ContainsKey(customKey))
{
_uiPool[customKey].Push(poolable);
}
else
{
GameObject.Destroy(poolable.gameObject);
}
}
public Poolable Pop(GameObject original, int count = 20, Transform parent = null, Transform rootParent = null)
{
if (_pool.ContainsKey(original.name) == false)
CreatePool(original, count, rootParent);
return _pool[original.name].Pop(parent);
}
// UI POP
public Poolable PopUI(GameObject original, int count = 20, Transform parent = null, Transform rootParent = null)
{
string poolKey = original.name;
int instanceCount = 0;
Transform topParent = original.transform;
while (topParent.parent != null)
{
topParent = topParent.parent;
}
bool hasMonsterController = topParent.GetComponent<MonsterController>() != null;
// 같은 이름의 풀 여러 개 생성 시 숫자 증가
while (!hasMonsterController && _uiPool.ContainsKey($"{poolKey}_{instanceCount}"))
{
instanceCount++;
}
if (!hasMonsterController && !_uiPool.ContainsKey($"{poolKey}_{instanceCount}"))
poolKey = $"{poolKey}_{instanceCount}";
if (_uiPool.ContainsKey(poolKey) == false)
CreateUIPool(original, instanceCount, count, rootParent);
Poolable poolable = _uiPool[poolKey].Pop(parent);
poolable.CustomKey = poolKey;
return poolable;
}
public GameObject GetOriginal(string name)
{
if (_pool.ContainsKey(name) == false)
return null;
return _pool[name].Original;
}
Push의 경우 Custom으로 만들어둔 Destroy에서 해당 오브젝트가 파괴되었을 때 Poolable 클래스를 가지고 있으면 Push하게끔 호출해뒀는데 이는 굳이 두개로 나눌 필요 없이 If문으로 UI 일경우와 오브젝트일 경우를 나눠서 Dicitonray에 값을 넣어주는 방식으로 하였다.
마찬가지로 PopUI로 시작되는데, instance의 수를 생성해주면서 만약 이전에 만든것이 있다면 수를 늘려주는 식으로 하였다.
이렇게 만들어진 수는 자신이 들고있는 식으로 구현해주었다.
'Unity' 카테고리의 다른 글
[Unity] Coroutine Manager (0) | 2024.11.18 |
---|---|
DataManager (0) | 2024.11.16 |
A* 알고리즘 및 최적화 (0) | 2024.08.15 |
[Unity] 화살표로 타겟 가리키기 (Quaternion.LookRotation) (0) | 2024.07.31 |
Extension Method (0) | 2024.06.25 |