
폭발 효과, 총알, 파편, 적 캐릭터 등 짧게 생존하는 오브젝트의 최적화하기 위한 기법이다.

위의 그림처럼 Pool에 미리 객체들을 생성해놓고 그 Pool에서 객체들을 꺼내쓰는 방식을 말한다.
게임 진행 도중에 Create, Delete와 같은 비용이 큰 함수 호출을 하는 건 절대 지양되기 때문! 성능의 문제도 있고 잦은 메모리 할당과 해제로 메모리 단편화 위험성이 높아지기 때문이다.
구현 자체가 어렵지는 않은데, ObjectManager와 같이 사용하거나 이펙트같이 게임오브젝트로 구현하지 않은 내용들과 같이 사용할 수 있도록 짜려면 조금 더 구조에 대한 고민과 포인터 관리에 대한 고민이 더 필요할 것 같다.
[여담]
이번 프로젝트에서 Effect를 하나씩 GameObject로 구현하는 건 비용이 너무 큰 거 같아서,
GameObject가 아닌 여러개의 구조체들을 스택으로 갖고 있는 ParticleSystem을 구현했다.
그럼 파티클들이 하나하나 Create/Delete 함수를 호출하는 것도 아닌데 굳이 Object들과 함께 Pool 사용하도록 확장하는 게 맞나 싶다.
지금 생각해보니 굳이 그럴 필요가 없을 것 같다!
Particle System에서도 이미 Pool과 비슷한 개념으로 관리하고있지만 Object들과 함께 Pool을 쓰는 건 의미가 없을 것 같다.
Pool의 기능인 Push랑 Pop만 구현해주면 된다!
BEGIN(Engine)
class CPool final : public CBase
{
public:
CPool();
virtual ~CPool() = default;
public:
void Push(class CGameObject* pGameObject);
class CGameObject* Pop();
private:
stack<class CGameObject*> m_Pools;
//_bool m_bIsFixed = true;
public:
static CPool* Create();
virtual void Free() override;
};
END
일단 내가 사용하던 프레임워크는 오브젝트들의 갱신을 담당하는 ObjectManager가 따로 있었다. 그리고 프로젝트 내의 모든 객체들은 CBase를 상속받고 있고, CBase는 레퍼런스 카운트를 관리할 수 있는 기능을 담당하고 있다. 이 기능을 사용해서 기존의 ObjectManager와 Pool간의 GameObject 포인터 관리 문제를 해결하도록 해봤다.
#include "Pool.h"
#include "GameInstance.h"
#include "GameObject.h"
CPool::CPool()
{
}
void CPool::Push(CPoolableObject* pGameObject)
{
m_Pools.push(pGameObject);
}
CPoolableObject* CPool::Pop()
{
if (m_Pools.empty())
return nullptr;
CGameObject* pGameObject = m_Pools.top();
m_Pools.pop();
Safe_AddRef(pGameObject);
return pGameObject;
}
CPool* CPool::Create()
{
return new CPool;
}
void CPool::Free()
{
__super::Free();
while (!m_Pools.empty())
{
Safe_Release(m_Pools.top());
m_Pools.pop();
}
}
Push할 때는 레퍼런스 카운트 조정을 안 하고, Pop할 때만 레퍼런스 카운트를 하나 증가시키고 있다.
ObjectManager로 객체 포인터를 넘기고 이제 모든 갱신과 객체의 생명주기도 ObjectManager가 관리하게 되는데, 객체에 DeadFlag가 세워져서 ObjectManager에서 Delete함수가 호출되더라도 실제로 객체가 Delete 되지 않도록 하기 위해서 증가시켜줬다.
즉 Pop할 때 증가시킨 레퍼런스를 ObjectManager의 Safe_Delete에서 하나 감소시켜주는 형태다.
Pool들을 관리하는 매니져 클래스!
BEGIN(Engine)
class CPool_Manager final : public CBase
{
private:
CPool_Manager();
virtual ~CPool_Manager() = default;
public:
void Push(const _wstring& strPoolTag, class CGameObject* pGameObject);
class CGameObject* Pop(const _wstring& strPoolTag);
void Clear();
private:
map<const _wstring, class CPool*>* m_Pools = { nullptr };
private:
class CPool* Find_Pools(const _wstring strPoolTag);
public:
static CPool_Manager* Create();
virtual void Free() override;
};
END
마찬가지로 Pool을 찾아서 해당 Pool에 Push, Pop을 호출하게 하는 역할을 한다.
#include "Pool_Manager.h"
#include "Pool.h"
#include "GameObject.h"
CPool_Manager::CPool_Manager()
{
}
void CPool_Manager::Push(_wstring& strPoolTag, CGameObject* pGameObject)
{
CPool* pPool = Find_Pools(strPoolTag);
if (nullptr == pPool)
{
pPool = CPool::Create();
m_Pools.emplace(strPoolTag, pPool);
}
if (nullptr != pGameObject)
{
pGameObject->Set_Active(false);
pPool->Push(pGameObject);
}
}
CGameObject* CPool_Manager::Pop(const _wstring& strPoolTag)
{
CPool* pPool = Find_Pools(strPoolTag);
return (nullptr == pPool) ? nullptr : pPool->Pop();
}
void CPool_Manager::Clear()
{
for (auto& Pair : m_Pools)
Safe_Release(Pair.second);
}
CPool* CPool_Manager::Find_Pools(const _wstring strPoolTag)
{
auto iter = m_Pools.find(strPoolTag);
if (iter == m_Pools.end())
return nullptr;
return iter->second;
}
CPool_Manager* CPool_Manager::Create()
{
return new CPool_Manager;
}
void CPool_Manager::Free()
{
__super::Free();
for (auto& iter : m_Pools) {
Safe_Release(iter.second);
}
m_pPools.clear();
}
GameInstance로 PoolManager의 Pop과 Push를 접근할 수 있도록 설계한 상태다.
CGameObject* pGameObject;
while (Pool에 구현해놓을 객체수 만큼)
{
pGameObject = dynamic_cast<CGameObject*>(m_pGameInstance->Clone_Prototype(PROTOTYPE::GAMEOBJECT, 프로토타입 인덱스, 프로토타입 태그, pArg));
m_pGameInstance->ReturnToPool(Pool 태그, pGameObject);
}
ObjectManager는 안에서 Layer단위로 오브젝트들을 관리하도록 설계되어있어서 ObjectManager에 넣어줄 때, 현재 레벨 인덱스와 LayerTag를 인자로 받고있다.
CGameObject* pGameObject = m_pGameInstance->GetFromPool(Pool 태그);
m_pGameInstance->Add_GameObject_ToLayer(iLayerLevelIndex, strLayerTag, pGameObject);
m_dead = true;
m_pGameInstance->ReturnToPool(Pool 태그, this);
ObjectManager 에서 Dead Flag가 true면 Safe_Release함수를 호출하게 설계되어있다.
Pop할 때 레퍼런스 카운틀르 증가시켜줬으니 실제로 객체에 Delete가 호출되지는 않고, 레퍼런스 카운트만 감소되며 ObjectManager의 LayerList에서는 삭제되게된다!
그리고 해당 포인터를 다시 Pool에 집어넣는 구조다.
끝!!