Download

Please have a look at my actor pool implementation

Hello,

as the actor pool will be an often used class in my project and I am not very comfortable with UE4 C++, I want to make sure that it is safe to use and will not cause any hard to find bugs later in the game. Could somebody have a look at it, please?

The pool consists of an actor component (ActorPoolComponent) and a class PooledActor derived from Actor.
The ActorPoolComponent should be added as a component to Blueprints where you can set the PoolSize and in the BeginPlay() of the actor component the PooledActors are being spawned. With ActorPoolComponent.TakeFromPool() you can take a pooled actor from the pool.
The PooledActor handles what happens when the actor is spawned (BeginPlay), when it’s released to the pool (OnReleaseToPool) and when it’s taken from the pool (OnTakenFromPool). OnReleaseToPool should handle how to disable the actor (e.g. disabling ticking), OnTakenFromPool should handle how to enable it (e.g. enable ticking). Also it can release itself to the pool with the function ReleaseToPool.



// ActorComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "Containers/Queue.h"
#include "PooledActor.h"
#include "Engine/World.h"
#include "ActorPoolComponent.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class BRICKGAMEVRCPP_API UActorPoolComponent : public UActorComponent
{
GENERATED_BODY()

private:
// spawn a pooled actor and add it to the pool
bool _spawnAndAddActorToPool();

// spawn count pooled actors and add them to the pool
bool _spawnAndAddActorsToPool(int32 count);

public:
UActorPoolComponent();

protected:
virtual void BeginPlay() override;

public:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

// if the pool is empty and TakeFromPool() is called, resize the pool and add ResizeAmount pooled actors?
UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool AutoResize = true;

// amount of pooled actors to add if the pool is empty and TakeFromPool() is called
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 ResizeAmount = 8;

// current size of the pool
UPROPERTY(BlueprintReadOnly)
int32 PoolSize = 0;

// initial size of the pool
UPROPERTY(EditAnywhere, BlueprintReadOnly)
int32 InitialPoolSize = 16;

// class of the pooled actors in this pool
UPROPERTY(EditAnywhere, BlueprintReadOnly)
TSubclassOf<class APooledActor> ActorClass;

// the container for the pool
TQueue<APooledActor*> ActorPool;

// take a pooled actor from this pool
UFUNCTION(BlueprintCallable)
bool TakeFromPool(APooledActor*& PooledActor);

// release a pooled actor to this pool
bool ReleaseToPool(APooledActor* PooledActor);
};






// ActorPoolComponent.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ActorPoolComponent.h"


bool UActorPoolComponent::_spawnAndAddActorToPool()
{
if (!ActorClass)
return false;

UWorld* world = GetWorld();
if (!world)
return false;

APooledActor* newPooledActor = world->SpawnActor<APooledActor>(ActorClass);
if (!newPooledActor)
return false;

// tell the pooled actor to which pool (this) he belongs, so that in it's ReleaseToPool()-function
// he knows to which pool he has to go back
newPooledActor->pool = this;

if (!ActorPool.Enqueue(newPooledActor))
return false;
else
PoolSize++;

return true;
}

bool UActorPoolComponent::_spawnAndAddActorsToPool(int32 count)
{
if (!ActorClass)
return false;

UWorld* world = GetWorld();
if (!world)
return false;

if (count <= 0)
return false;

for (int32 i = 0; i < count; ++i)
{
APooledActor* newPooledActor = world->SpawnActor<APooledActor>(ActorClass);
if (!newPooledActor)
return false;

// tell the pooled actor to which pool (this) he belongs, so that in it's ReleaseToPool()-function
// he knows to which pool he has to go back
newPooledActor->pool = this;

if (!ActorPool.Enqueue(newPooledActor))
return false;
else
PoolSize++;
}

return true;
}


UActorPoolComponent::UActorPoolComponent()
{
PrimaryComponentTick.bCanEverTick = false;
}


void UActorPoolComponent::BeginPlay()
{
Super::BeginPlay();


_spawnAndAddActorsToPool(InitialPoolSize);
}


void UActorPoolComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
}

bool UActorPoolComponent::ReleaseToPool(APooledActor* PooledActor)
{
return ActorPool.Enqueue(PooledActor);
}

bool UActorPoolComponent::TakeFromPool(APooledActor*& PooledActor)
{
// if the actor pool is empty and AutoResize true, spawn ResizeAmount new pooled actors
// and take one from the pool
if (ActorPool.IsEmpty())
if (AutoResize && ResizeAmount > 0)
{
if (!_spawnAndAddActorsToPool(ResizeAmount))
return false;
}
else
return false;

// the actor pool is not empty. Take one pooled actor and return it.
if (!ActorPool.Dequeue(PooledActor))
return false;

if (!PooledActor)
return false;

PooledActor->OnTakenFromPool();

return true;
}






// PooledActor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ActorPoolComponent.h"
#include "PooledActor.generated.h"

class UActorPoolComponent;

UCLASS()
class BRICKGAMEVRCPP_API APooledActor : public AActor
{
GENERATED_BODY()

public:
APooledActor();

protected:
virtual void BeginPlay() override;

public:
virtual void Tick(float DeltaTime) override;

// the pool this pooled actor belongs to
UActorPoolComponent* pool;

// called when this actor is taken from a pool. Should handle things like making the actor visible, enabling ticking etc.
UFUNCTION(BlueprintImplementableEvent)
void OnTakenFromPool();

// called when this actor is released to a pool. Should handle things like making the actor invisible, disabling ticking etc.
UFUNCTION(BlueprintImplementableEvent)
void OnReleasedToPool();

// release this actor to the pool. Should be called instead of DestroyActor().
UFUNCTION(BlueprintCallable)
bool ReleaseToPool();

};




// PooledActor.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "PooledActor.h"

APooledActor::APooledActor()
{
PrimaryActorTick.bCanEverTick = true;

}

void APooledActor::BeginPlay()
{
Super::BeginPlay();

}

void APooledActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);

}

bool APooledActor::ReleaseToPool()
{
if (!pool)
return false;

// release this actor the pool it belongs to
if (!pool->ReleaseToPool(this))
return false;

OnReleasedToPool();

return true;
}



Thank you,

Thilo

Generally looks ok, but some things I would improve:

  • Try to follow UE casing and naming conventions, e.g. SpawnAndAddActorToPool instead of _spawnAndAddActorToPool.
  • What reason do you have for using a TQueue over a TArray? Do you care that you get the oldest actor in the pool?
  • Code would be simpler if _spawnAndAddActorsToPool(int32 count) just called into _spawnAndAddActorToPool() count times.
  • You should make sure your spawn collision handling is set to AlwaysSpawn.
  • UActorPoolComponent* pool should be a UPROPERTY
  • Why does APooledActor set tick enabled if it does nothing in the tick?
  • Remove all redundant methods, e.g. APooledActor::BeginPlay, APooledActor::Tick

Thank you for your answer!

[QUOTE]

[li]What reason do you have for using a TQueue over a TArray? Do you care that you get the oldest actor in the pool?[/li][/QUOTE]

I just read about implementations of object pools that they often used queues. But as you say it… I could simply add the actor to a TArray if it returns to the pool and get and remove the last item of this array (not the first - to avoid index shifting) if it is taken from the pool.

[QUOTE]

[li]Code would be simpler if _spawnAndAddActorsToPool(int32 count) just called into _spawnAndAddActorToPool() count times.[/li][/QUOTE]

True.

[QUOTE]

[li]You should make sure your spawn collision handling is set to AlwaysSpawn.[/li][/QUOTE]

True.

[QUOTE]

[li]UActorPoolComponent* pool should be a UPROPERTY[/li][/QUOTE]

I read something about GC in UE. Should I do this because otherwise the GC wouldn’t count this as a reference and delete the UActorPoolComponent if no other UPROPERTY-reference to it consists but this non-UPROPERTY reference? Should the TArray also be a UPROPERTY? I ask because the TQueue doesn’t accept UPROPERTY.

[QUOTE]

[li]Why does APooledActor set tick enabled if it does nothing in the tick?[/li][/QUOTE]

Is it even possible to set tick enabled in the blueprint derived from APooledActor if it’s disabled in C++? I have to try that.

Yes I think it should. Generally anything that directly references UObjects should be a UPROPERTY.

I think if you implement ‘Event Tick’ then the actor will tick. Otherwise you would not be able to tick in a blueprint that directly derives from AActor