Implement object pooling for projectiles to avoid per-shot spawn cost
Implement a projectile pool so firing reuses pre-spawned actors instead of calling SpawnActor per shot. Prewarm creates the set once; Acquire hands out a free, deactivated projectile and re-activates it.
Constraints:
SpawnActorruns only inPrewarm; the firing path must never spawn orDestroy.- Idle pooled actors are deactivated (no tick, no collision, hidden).
- Reset projectile state on reuse; return
nullptrwhen the pool is exhausted.
void UProjectilePool::Prewarm(UWorld* World, TSubclassOf<AProjectile> Class, int32 Count)
{
// your code here
}
AProjectile* UProjectilePool::Acquire(const FVector& Loc, const FVector& Dir)
{
// your code here
}
Write the implementation.
Pre-spawn a fixed set of projectile actors once, keep them deactivated, and hand out a free one on fire instead of calling SpawnActor. Return spent projectiles to the pool by deactivating them rather than calling Destroy.
- ✗Calling
SpawnActor/Destroyinside the pool, defeating its whole purpose - ✗Leaving
Tickand collision enabled on idle pooled actors - ✗Failing to reset projectile state (velocity, damage, owner) on reuse
- →How do you handle the pool running dry under sustained fire?
- →Why must a returned projectile reset its velocity and collision state?
Idea
Spawning and destroying actors is expensive: allocation, component registration, and garbage-collection pressure. A pool creates projectiles once and recycles them.
UCLASS()
class AProjectile : public AActor
{
GENERATED_BODY()
public:
// Enable the projectile: return it to play at a given point
void Activate(const FVector& Location, const FVector& Direction);
// Disable the projectile: return it to the pool
void Deactivate();
};
void AProjectile::Activate(const FVector& Location, const FVector& Direction)
{
SetActorLocationAndRotation(Location, Direction.Rotation());
SetActorHiddenInGame(false);
SetActorEnableCollision(true);
SetActorTickEnabled(true);
if (UProjectileMovementComponent* Move = FindComponentByClass<UProjectileMovementComponent>())
{
Move->Velocity = Direction * Speed; // reset state on reuse
Move->Activate();
}
}
void AProjectile::Deactivate()
{
SetActorHiddenInGame(true);
SetActorEnableCollision(false);
SetActorTickEnabled(false); // an idle projectile does not tick
}
The pool:
UCLASS()
class UProjectilePool : public UObject
{
GENERATED_BODY()
UPROPERTY() TArray<AProjectile*> Pool;
public:
void Prewarm(UWorld* World, TSubclassOf<AProjectile> Class, int32 Count)
{
for (int32 i = 0; i < Count; ++i)
{
AProjectile* P = World->SpawnActor<AProjectile>(Class); // spawn once
P->Deactivate();
Pool.Add(P);
}
}
AProjectile* Acquire(const FVector& Loc, const FVector& Dir)
{
for (AProjectile* P : Pool)
{
if (P->IsHidden()) // a free instance
{
P->Activate(Loc, Dir);
return P;
}
}
return nullptr; // pool empty — grow it or skip the shot
}
};
On hit or timeout the projectile calls Deactivate() instead of Destroy() — SpawnActor is no longer called on the hot firing path.