My take on a dynamic object pool that works with any actors. The pool is written in C++ and utilizes interfaces. Custom behaviors can be defined for activation and deactivation offering flexibility as needed.
by Shahin Mohseni, Nov 30, 2024 (updated: Dec 15, 2024)This blog is still being written and is yet not finished
While developing a zombie survival game, I encountered a persistent issue causing significant stuttering during gameplay.
I later identified the root cause: frequent spawning and destruction of actors within the level. This problem arose during key gameplay events such as shooting, enemy spawns, gem spawns, and the subsequent removal of these objects, all of which occurred frequently and contributed to the performance degradation.
To resolve this issue, I implemented an object pooling system. Instead of spawning actors dynamically during gameplay, all required actors were pre-allocated at the start of the level. These actors remained in memory and were activated or deactivated as needed. Since the objects were already initialized and managed within the pool, this approach eliminated the performance stutter caused by frequent spawning and destruction.
The logic for object pooling was within a static component class. This class provided functions to initialize and manage actors in the pool. To track the pooled actors, the class maintained a Map as a member variable.
The actor initialization process was implemented as follows:
void UObjectPoolManager::InitPool()
{
for (const auto& [Actor, Size] : ActorsToSpawn)
{
auto& [Actors] = Pools.Emplace(Actor);
for (int _ = 0; _ < Size; _++)
{
AActor* SpawnedActor = CreateActor(Actor);
SetIsActive(SpawnedActor, false);
Actors.Add(SpawnedActor);
}
}
UE_LOG(LogTemp, Warning, TEXT("UObjectPoolManager::InitPool"));
}
To activate and deactivate actors both in and outside the pool, a static method was created. This method takes in a pointer to the actor you’re trying to modify and a boolean whether you want to activate or deactivate it.
void UObjectPoolManager::SetIsActive(AActor* Actor, bool NewActive)
{
Actor->SetActorTickEnabled(NewActive);
Actor->SetActorHiddenInGame(!NewActive);
}
First iteration of the object pool (only hides the actor and disables it by setting its tick to false)
To fetch an actor from the pool, a call to the GetActor method is made. This method is quite complex as it does a lot of things to streamline the process of fetching an actor, even if it hasn’t been initialized in the pool. This decision was made to make it easier to test out the pool before specifying how many actors were needed. If a pool does not exist, it will throw a warning notifying the developer that said pool was not initialized from start, the developer can then add that actor to the pool when time fits.
AActor* UObjectPoolManager::GetActor(TSubclassOf<AActor> Actor)
{
FActorPool* Pool = Pools.Find(Actor);
if (Pool == nullptr)
{
FActorPool* NewPool = &Pools.Emplace(Actor);
AActor* SpawnedActor = CreateActor(Actor);
SetIsActive(SpawnedActor, false);
NewPool->Actors.Add(SpawnedActor);
UE_LOG(LogTemp, Error, TEXT("No pool found for %s, generating one"), *Actor->GetName());
Pool = NewPool;
}
if (AActor* FoundActor = Pool->GetAvailableActor())
{
return FoundActor;
}
AActor* SpawnedActor = CreateActor(Actor);
Pool->Actors.Emplace(SpawnedActor);
return SpawnedActor;
}
GetActor function
To view the full source of the object pool, you can follow this link to get to the Github repository.