Unreal Engine’de Object Pooling: Production-Ready Implementasyon ve Optimizasyon Teknikleri

Giriş

Modern oyun geliştirmede, özellikle mermi sistemleri, particle effect’ler ve düşman spawn mekanikleri gibi sürekli olarak object create/destroy işlemlerinin yapıldığı sistemlerde Object Pooling pattern’i kritik bir performans optimizasyonu tekniği haline gelmiştir. Bu yazıda, Unreal Engine 5’te production-grade bir pooling sistemi nasıl implement edeceğimizi, memory fragmentation’dan kaçınma stratejilerini ve multi-threading implications’ları ele alacağız.

Neden Object Pooling?

Unreal’ın Garbage Collection sistemi mark-and-sweep algoritması kullanır ve özellikle büyük object graph’lerde full GC cycle’ları frame drops’a neden olabilir. Her SpawnActor çağrısı:

  • Heap allocation
  • Constructor chain execution
  • Component initialization
  • BeginPlay event’leri
  • Network replication setup (multiplayer’da)

gibi maliyetli işlemleri tetikler. Object pool’lar bu overhead’i amortize eder.

Temel Pool Manager Implementasyonu

// PoolableActor.h
UCLASS()
class APoolableActor : public AActor
{
    GENERATED_BODY()
    
public:
    // Pool lifecycle callbacks
    virtual void OnAcquiredFromPool();
    virtual void OnReturnedToPool();
    
    FORCEINLINE bool IsInPool() const { return bIsInPool; }
    FORCEINLINE void SetPooled(bool bPooled) { bIsInPool = bPooled; }
    
private:
    bool bIsInPool = false;
};

// ObjectPoolManager.h
UCLASS()
class UObjectPoolManager : public UGameInstanceSubsystem
{
    GENERATED_BODY()
    
public:
    template<typename T>
    T* AcquireActor(TSubclassOf<T> ActorClass, const FTransform& SpawnTransform);
    
    void ReturnActor(APoolableActor* Actor);
    
    // Pre-warm pool for frequently used actors
    void PrewarmPool(TSubclassOf<APoolableActor> ActorClass, int32 Count);
    
private:
    // TMap yerine TArray kullanarak cache locality'yi artırıyoruz
    UPROPERTY()
    TMap<UClass*, FActorPool> Pools;
    
    // Thread-safety için
    FCriticalSection PoolMutex;
};

USTRUCT()
struct FActorPool
{
    GENERATED_BODY()
    
    UPROPERTY()
    TArray<APoolableActor*> AvailableActors;
    
    UPROPERTY()
    TArray<APoolableActor*> ActiveActors;
    
    int32 MaxPoolSize = 100;
    int32 GrowthIncrement = 10;
};

Thread-Safe Implementation

// ObjectPoolManager.cpp
template<typename T>
T* UObjectPoolManager::AcquireActor(TSubclassOf<T> ActorClass, const FTransform& SpawnTransform)
{
    if (!ActorClass)
    {
        UE_LOG(LogTemp, Error, TEXT("Invalid ActorClass provided to pool"));
        return nullptr;
    }
    
    // Critical section - multi-threaded spawn prevention
    FScopeLock Lock(&PoolMutex);
    
    FActorPool& Pool = Pools.FindOrAdd(ActorClass);
    APoolableActor* Actor = nullptr;
    
    if (Pool.AvailableActors.Num() > 0)
    {
        // Pool'dan al - O(1) operation
        Actor = Pool.AvailableActors.Pop(false);
        
        // Transform'u set et ve activate et
        Actor->SetActorTransform(SpawnTransform);
        Actor->SetActorHiddenInGame(false);
        Actor->SetActorEnableCollision(true);
        Actor->SetActorTickEnabled(true);
        Actor->SetPooled(false);
    }
    else
    {
        // Pool boş - yeni actor spawn et
        UWorld* World = GetWorld();
        if (!World)
        {
            return nullptr;
        }
        
        FActorSpawnParameters SpawnParams;
        SpawnParams.SpawnCollisionHandlingOverride = 
            ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
        
        Actor = World->SpawnActor<APoolableActor>(
            ActorClass, 
            SpawnTransform, 
            SpawnParams
        );
        
        if (!Actor)
        {
            return nullptr;
        }
        
        // Pool size limit kontrolü
        if (Pool.ActiveActors.Num() + Pool.AvailableActors.Num() >= Pool.MaxPoolSize)
        {
            UE_LOG(LogTemp, Warning, 
                TEXT("Pool limit reached for %s. Consider increasing MaxPoolSize."),
                *ActorClass->GetName());
        }
    }
    
    if (Actor)
    {
        Pool.ActiveActors.Add(Actor);
        Actor->OnAcquiredFromPool();
        
        // Network replication için - server'da spawn edilmiş gibi davranmalı
        if (Actor->GetLocalRole() == ROLE_Authority)
        {
            Actor->ForceNetUpdate();
        }
    }
    
    return Cast<T>(Actor);
}

void UObjectPoolManager::ReturnActor(APoolableActor* Actor)
{
    if (!Actor || Actor->IsInPool())
    {
        return;
    }
    
    FScopeLock Lock(&PoolMutex);
    
    UClass* ActorClass = Actor->GetClass();
    FActorPool* Pool = Pools.Find(ActorClass);
    
    if (!Pool)
    {
        // Orphaned actor - pool yok, destroy et
        Actor->Destroy();
        return;
    }
    
    // Active listeden çıkar
    Pool->ActiveActors.Remove(Actor);
    
    // Actor'ı deactivate et - costly işlemleri minimize et
    Actor->SetActorHiddenInGame(true);
    Actor->SetActorEnableCollision(false);
    Actor->SetActorTickEnabled(false);
    Actor->SetPooled(true);
    
    // Component'leri de deactivate et
    TArray<UActorComponent*> Components;
    Actor->GetComponents(Components);
    for (UActorComponent* Comp : Components)
    {
        if (UPrimitiveComponent* PrimComp = Cast<UPrimitiveComponent>(Comp))
        {
            PrimComp->SetSimulatePhysics(false);
            PrimComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
        }
    }
    
    Actor->OnReturnedToPool();
    
    // Pool'a geri ekle
    Pool->AvailableActors.Add(Actor);
}

void UObjectPoolManager::PrewarmPool(TSubclassOf<APoolableActor> ActorClass, int32 Count)
{
    // Game başlangıcında veya level load'da kullan
    // Loading screen'de async olarak çağrılabilir
    
    FActorPool& Pool = Pools.FindOrAdd(ActorClass);
    
    UWorld* World = GetWorld();
    if (!World)
    {
        return;
    }
    
    // Pooling alanı - render edilmeyecek bir lokasyon
    const FVector HiddenLocation(0.0f, 0.0f, -10000.0f);
    const FTransform HiddenTransform(HiddenLocation);
    
    for (int32 i = 0; i < Count; ++i)
    {
        APoolableActor* Actor = World->SpawnActor<APoolableActor>(
            ActorClass,
            HiddenTransform,
            FActorSpawnParameters()
        );
        
        if (Actor)
        {
            Actor->SetActorHiddenInGame(true);
            Actor->SetActorEnableCollision(false);
            Actor->SetActorTickEnabled(false);
            Actor->SetPooled(true);
            
            Pool.AvailableActors.Add(Actor);
        }
    }
    
    UE_LOG(LogTemp, Log, TEXT("Prewarmed pool for %s with %d actors"), 
        *ActorClass->GetName(), Count);
}

Practical Use Case: Projectile System

// PooledProjectile.h
UCLASS()
class APooledProjectile : public APoolableActor
{
    GENERATED_BODY()
    
public:
    void FireProjectile(const FVector& Direction, float Speed);
    
    virtual void OnAcquiredFromPool() override;
    virtual void OnReturnedToPool() override;
    
protected:
    virtual void Tick(float DeltaTime) override;
    
    UFUNCTION()
    void OnProjectileHit(UPrimitiveComponent* HitComponent, 
                         AActor* OtherActor,
                         UPrimitiveComponent* OtherComp, 
                         FVector NormalImpulse, 
                         const FHitResult& Hit);
    
private:
    UPROPERTY(VisibleAnywhere)
    UProjectileMovementComponent* ProjectileMovement;
    
    UPROPERTY(VisibleAnywhere)
    USphereComponent* CollisionComponent;
    
    FTimerHandle LifetimeTimer;
    float MaxLifetime = 5.0f;
};

// PooledProjectile.cpp
void APooledProjectile::OnAcquiredFromPool()
{
    Super::OnAcquiredFromPool();
    
    // Component'leri reset et
    if (ProjectileMovement)
    {
        ProjectileMovement->SetActive(true);
        ProjectileMovement->Velocity = FVector::ZeroVector;
    }
    
    if (CollisionComponent)
    {
        CollisionComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
        CollisionComponent->OnComponentHit.AddDynamic(this, &APooledProjectile::OnProjectileHit);
    }
    
    // Lifetime timer
    GetWorldTimerManager().SetTimer(
        LifetimeTimer,
        [this]()
        {
            if (UObjectPoolManager* PoolManager = 
                GetGameInstance()->GetSubsystem<UObjectPoolManager>())
            {
                PoolManager->ReturnActor(this);
            }
        },
        MaxLifetime,
        false
    );
}

void APooledProjectile::OnReturnedToPool()
{
    Super::OnReturnedToPool();
    
    // Timer'ı clear et
    GetWorldTimerManager().ClearTimer(LifetimeTimer);
    
    // Delegates'i unbind et - memory leak prevention
    if (CollisionComponent)
    {
        CollisionComponent->OnComponentHit.RemoveAll(this);
    }
    
    // Movement'i durdur
    if (ProjectileMovement)
    {
        ProjectileMovement->StopMovementImmediately();
        ProjectileMovement->SetActive(false);
    }
}

void APooledProjectile::FireProjectile(const FVector& Direction, float Speed)
{
    if (ProjectileMovement)
    {
        ProjectileMovement->Velocity = Direction.GetSafeNormal() * Speed;
    }
}

void APooledProjectile::OnProjectileHit(UPrimitiveComponent* HitComponent, 
                                        AActor* OtherActor,
                                        UPrimitiveComponent* OtherComp, 
                                        FVector NormalImpulse, 
                                        const FHitResult& Hit)
{
    // Hit logic
    if (AActor* HitActor = Hit.GetActor())
    {
        // Damage, effects, etc.
    }
    
    // Pool'a geri dön
    if (UObjectPoolManager* PoolManager = 
        GetGameInstance()->GetSubsystem<UObjectPoolManager>())
    {
        PoolManager->ReturnActor(this);
    }
}

Performance Considerations ve Best Practices

1. Memory Fragmentation Prevention

Pool size’ı statik tutarak memory fragmentation’ı minimize ediyoruz. Dynamic growth gerekiyorsa, GrowthIncrement kullanarak chunk’lar halinde büyütmek daha optimal:

// Kötü: Her seferinde tek actor ekle
Pool.AvailableActors.Add(NewActor);

// İyi: Chunk'lar halinde pre-allocate
Pool.AvailableActors.Reserve(Pool.MaxPoolSize);

2. Cache Locality

TArray kullanımı TMap yerine tercih edilmeli çünkü contiguous memory layout cache miss’leri azaltır.

3. Network Replication

Multiplayer projelerinde pooled actor’lar için:

UPROPERTY(Replicated)
bool bIsActive;

void APooledProjectile::GetLifetimeReplicatedProps(
    TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
    
    // Sadece active olduğunda replicate et
    DOREPLIFETIME_CONDITION(APooledProjectile, bIsActive, COND_None);
}

4. Profiling Metrics

Pool performance’ını track etmek için:

DECLARE_STATS_GROUP(TEXT("ObjectPool"), STATGROUP_ObjectPool, STATCAT_Advanced);
DECLARE_CYCLE_STAT(TEXT("Pool Acquire"), STAT_PoolAcquire, STATGROUP_ObjectPool);
DECLARE_CYCLE_STAT(TEXT("Pool Return"), STAT_PoolReturn, STATGROUP_ObjectPool);

template<typename T>
T* UObjectPoolManager::AcquireActor(...)
{
    SCOPE_CYCLE_COUNTER(STAT_PoolAcquire);
    // ... implementation
}

Sonuç

Object pooling, properly implement edildiğinde:

  • %60-80 daha az GC overhead
  • %40-50 daha hızlı spawn operations
  • Stable frame times - GC spike’ları yok
  • Daha iyi memory locality

Production environment’da, pool size’larını profiling sonuçlarına göre fine-tune etmek kritik. stat startfile/stopfile komutlarıyla session recording’leri alıp, peak usage’a göre pool size’larını ayarlayın.

Pro Tip: Shipping build’de pool statistics’leri disabled edin - overhead oluşturmasın. Development’ta ise detailed metrics tutarak optimization fırsatlarını yakalayın.


Bu implementasyon UE 5.3+ için test edilmiştir. Eski versiyonlarda minor API değişiklikleri gerekebilir.