From 900367fb37df8aa3451dd90e9b841aca23d490ef Mon Sep 17 00:00:00 2001 From: Jonas Reich Date: Wed, 22 Nov 2023 17:33:56 +0100 Subject: [PATCH] Added actor pool [git-p4j: depot-path = "//TQ2S/tq2-code/TQ2/Plugins/OpenUnrealUtilities/..."; change = 41978] --- .../Private/Pooling/OUUActorPool.cpp | 433 ++++++++++++++++++ .../OUURuntime/Public/Pooling/OUUActorPool.h | 212 +++++++++ 2 files changed, 645 insertions(+) create mode 100644 Source/OUURuntime/Private/Pooling/OUUActorPool.cpp create mode 100644 Source/OUURuntime/Public/Pooling/OUUActorPool.h diff --git a/Source/OUURuntime/Private/Pooling/OUUActorPool.cpp b/Source/OUURuntime/Private/Pooling/OUUActorPool.cpp new file mode 100644 index 0000000..e70a8a3 --- /dev/null +++ b/Source/OUURuntime/Private/Pooling/OUUActorPool.cpp @@ -0,0 +1,433 @@ +// Copyright (c) 2023 Jonas Reich & Contributors + +#include "Pooling/OUUActorPool.h" + +#include "LogOpenUnrealUtilities.h" +#include "ProfilingDebugging/CsvProfiler.h" +#include "Templates/InterfaceUtils.h" + +CSV_DEFINE_CATEGORY(OUUActorPool, true); + +namespace OUU::Runtime::ActorPool +{ + // default values taken from UMassSimulationSettings + + static auto CVar_MaxSpawnTime = TAutoConsoleVariable( + TEXT("ouu.ActorPool.MaxSpawnTimePerTick"), + 0.0015, + TEXT("The desired budget in seconds allowed to do pooled actor spawning per frame")); + + static auto CVar_MaxDestructTime = TAutoConsoleVariable( + TEXT("ouu.ActorPool.MaxDestructTimePerTick"), + 0.0005, + TEXT("The desired budget in seconds allowed to do pooled actor destruction per frame")); +} // namespace OUU::Runtime::ActorPool + +UOUUActorPool* UOUUActorPool::Get(UObject& WorldContext) +{ + return WorldContext.GetWorld()->GetSubsystem(); +} + +UOUUActorPool::FSpawnRequestHandle UOUUActorPool::RequestActorSpawn(const UOUUActorPool::FSpawnRequest& InSpawnRequest) +{ + // The handle manager has a freelist of the release indexes, so it can return us a index that we previously used. + const auto SpawnRequestHandle = SpawnRequestHandleManager.GetNextHandle(); + const int32 Index = SpawnRequestHandle.GetIndex(); + + // Check if we need to grow the array, otherwise it is a previously released index that was returned. + if (!SpawnRequests.IsValidIndex(Index)) + { + checkf( + SpawnRequests.Num() == Index, + TEXT("This case should only be when we need to grow the array of one element.")); + SpawnRequests.Emplace(InSpawnRequest); + } + else + { + SpawnRequests[Index] = InSpawnRequest; + } + + UWorld* World = GetWorld(); + check(World); + + // Initialize the spawn request status + auto& SpawnRequest = GetMutableSpawnRequest(SpawnRequestHandle); + SpawnRequest.Status = ESpawnRequestStatus::Pending; + SpawnRequest.SerialNumber = RequestSerialNumberCounter.fetch_add(1); + SpawnRequest.RequestedTime = World->GetTimeSeconds(); + + return SpawnRequestHandle; +} + +void UOUUActorPool::RetryActorSpawnRequest(const UOUUActorPool::FSpawnRequestHandle SpawnRequestHandle) +{ + check(SpawnRequestHandleManager.IsValidHandle(SpawnRequestHandle)); + const int32 Index = SpawnRequestHandle.GetIndex(); + check(SpawnRequests.IsValidIndex(Index)); + FSpawnRequest& SpawnRequest = SpawnRequests[SpawnRequestHandle.GetIndex()]; + if (ensureMsgf(SpawnRequest.Status == ESpawnRequestStatus::Failed, TEXT("Can only retry failed spawn requests"))) + { + UWorld* World = GetWorld(); + check(World); + + SpawnRequest.Status = ESpawnRequestStatus::RetryPending; + SpawnRequest.SerialNumber = RequestSerialNumberCounter.fetch_add(1); + SpawnRequest.RequestedTime = World->GetTimeSeconds(); + } +} + +bool UOUUActorPool::CancelActorSpawnRequest(UOUUActorPool::FSpawnRequestHandle& SpawnRequestHandle) +{ + if (!ensureMsgf(SpawnRequestHandleManager.RemoveHandle(SpawnRequestHandle), TEXT("Invalid spawn request handle"))) + { + return false; + } + + check(SpawnRequests.IsValidIndex(SpawnRequestHandle.GetIndex())); + FSpawnRequest& SpawnRequest = SpawnRequests[SpawnRequestHandle.GetIndex()]; + check(SpawnRequest.Status != ESpawnRequestStatus::Processing); + SpawnRequestHandle.Invalidate(); + SpawnRequest.Reset(); + return true; +} + +void UOUUActorPool::DestroyOrReleaseActor(AActor* Actor, bool bImmediate) +{ + if (bImmediate) + { + if (!TryReleaseActorToPool(Actor)) + { + UWorld* World = GetWorld(); + check(World); + + World->DestroyActor(Actor); + --NumActorSpawned; + } + } + else + { + ActorsToDestroy.Add(Actor); + } +} + +void UOUUActorPool::DestroyAllActors() +{ + if (UWorld* World = GetWorld()) + { + for (auto It = PooledActors.CreateIterator(); It; ++It) + { + TArray& ActorArray = It.Value(); + for (int i = 0; i < ActorArray.Num(); i++) + { + World->DestroyActor(ActorArray[i]); + } + NumActorSpawned -= ActorArray.Num(); + } + } + PooledActors.Empty(); + + NumActorPooled = 0; + CSV_CUSTOM_STAT(OUUActorPool, NumSpawned, NumActorSpawned, ECsvCustomStatOp::Accumulate); + CSV_CUSTOM_STAT(OUUActorPool, NumPooled, NumActorPooled, ECsvCustomStatOp::Accumulate); +} + +const UOUUActorPool::FSpawnRequest& UOUUActorPool::GetSpawnRequest(const FSpawnRequestHandle SpawnRequestHandle) const +{ + check(SpawnRequestHandleManager.IsValidHandle(SpawnRequestHandle)); + check(SpawnRequests.IsValidIndex(SpawnRequestHandle.GetIndex())); + return SpawnRequests[SpawnRequestHandle.GetIndex()]; +} + +UOUUActorPool::FSpawnRequest& UOUUActorPool::GetMutableSpawnRequest(const FSpawnRequestHandle SpawnRequestHandle) +{ + check(SpawnRequestHandleManager.IsValidHandle(SpawnRequestHandle)); + check(SpawnRequests.IsValidIndex(SpawnRequestHandle.GetIndex())); + return SpawnRequests[SpawnRequestHandle.GetIndex()]; +} + +bool UOUUActorPool::ShouldCreateSubsystem(UObject* Outer) const +{ + // Only create an instance if there is no derived implementation defined elsewhere + TArray ChildClasses; + GetDerivedClasses(GetClass(), ChildClasses, false); + if (ChildClasses.Num() > 0) + { + return false; + } + + return Super::ShouldCreateSubsystem(Outer); +} + +void UOUUActorPool::Tick(float DeltaTime) +{ + ProcessPendingDestruction( + static_cast(OUU::Runtime::ActorPool::CVar_MaxDestructTime.GetValueOnGameThread())); + ProcessPendingSpawningRequest( + static_cast(OUU::Runtime::ActorPool::CVar_MaxSpawnTime.GetValueOnGameThread())); + CSV_CUSTOM_STAT(OUUActorPool, NumSpawned, NumActorSpawned, ECsvCustomStatOp::Accumulate); + CSV_CUSTOM_STAT(OUUActorPool, NumPooled, NumActorPooled, ECsvCustomStatOp::Accumulate); +} + +TStatId UOUUActorPool::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(FOUUActorPool, STATGROUP_Quick) +} + +void UOUUActorPool::AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector) +{ + if (auto* CastedThis = Cast(InThis)) + { + for (auto& Entry : CastedThis->PooledActors) + { + Collector.AddReferencedObjects(Entry.Value); + } + } + + Super::AddReferencedObjects(InThis, Collector); +} + +UOUUActorPool::FSpawnRequestHandle UOUUActorPool::GetNextRequestToSpawn() const +{ + FSpawnRequestHandle BestSpawnRequestHandle; + float BestPriority = MAX_FLT; + bool bBestIsPending = false; + uint32 BestSerialNumber = MAX_uint32; + for (const FSpawnRequestHandle SpawnRequestHandle : SpawnRequestHandleManager.GetHandles()) + { + if (!SpawnRequestHandle.IsValid()) + { + continue; + } + const FSpawnRequest& SpawnRequest = GetSpawnRequest(SpawnRequestHandle); + if (SpawnRequest.Status == ESpawnRequestStatus::Pending) + { + if (!bBestIsPending || SpawnRequest.Priority < BestPriority + || (SpawnRequest.Priority == BestPriority && SpawnRequest.SerialNumber < BestSerialNumber)) + { + BestSpawnRequestHandle = SpawnRequestHandle; + BestSerialNumber = SpawnRequest.SerialNumber; + BestPriority = SpawnRequest.Priority; + bBestIsPending = true; + } + } + else if (!bBestIsPending && SpawnRequest.Status == ESpawnRequestStatus::RetryPending) + { + // No priority on retries just FIFO + if (SpawnRequest.SerialNumber < BestSerialNumber) + { + BestSpawnRequestHandle = SpawnRequestHandle; + BestSerialNumber = SpawnRequest.SerialNumber; + } + } + } + + return BestSpawnRequestHandle; +} + +AActor* UOUUActorPool::SpawnOrRetrieveFromPool( + const FSpawnRequestHandle SpawnRequestHandle, + FSpawnRequest& SpawnRequest) +{ + TArray* Pool = PooledActors.Find(SpawnRequest.Template); + + if (Pool && Pool->Num() > 0) + { + AActor* PooledActor = (*Pool)[0]; + Pool->RemoveAt(0); + --NumActorPooled; + ActivateActor(PooledActor); + PooledActor->SetActorTransform(SpawnRequest.Transform, false, nullptr, ETeleportType::ResetPhysics); + + CALL_INTERFACE(IOUUPoolableActor, OnRemovedFromPool, PooledActor); + + return PooledActor; + } + return SpawnActor(SpawnRequestHandle, SpawnRequest); +} + +AActor* UOUUActorPool::SpawnActor(const FSpawnRequestHandle SpawnRequestHandle, FSpawnRequest SpawnRequest) const +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UActorPool::SpawnActor); + + UWorld* World = GetWorld(); + check(World); + + if (AActor* SpawnedActor = World->SpawnActorDeferred( + SpawnRequest.Template, + SpawnRequest.Transform, + nullptr, + nullptr, + ESpawnActorCollisionHandlingMethod::AlwaysSpawn)) + { + // Call construction script + if (SpawnRequest.DelayFirstConstructionScript == false) + { + SpawnedActor->FinishSpawning(SpawnRequest.Transform); + } + ++NumActorSpawned; + // The finish spawning might have failed and the spawned actor is destroyed. + if (IsValidChecked(SpawnedActor)) + { + return SpawnedActor; + } + } + + UE_VLOG_CAPSULE( + this, + LogOpenUnrealUtilities, + Error, + SpawnRequest.Transform.GetLocation(), + SpawnRequest.Template.GetDefaultObject()->GetSimpleCollisionHalfHeight(), + SpawnRequest.Template.GetDefaultObject()->GetSimpleCollisionRadius(), + SpawnRequest.Transform.GetRotation(), + FColor::Red, + TEXT("Unable to spawn actor entity [%s]"), + *GetNameSafe(SpawnRequest.Template)); + return nullptr; +} + +void UOUUActorPool::ActivateActor(AActor* Actor) const +{ + Actor->SetActorHiddenInGame(false); +} + +void UOUUActorPool::DeactivateActor(AActor* Actor) const +{ + Actor->SetActorEnableCollision(false); + Actor->SetActorHiddenInGame(true); + Actor->SetActorTickEnabled(false); +} + +void UOUUActorPool::DeactivateActorFast(AActor* Actor) const +{ + Actor->SetActorHiddenInGame(true); +} + +void UOUUActorPool::ProcessPendingSpawningRequest(const double MaxTimeSlicePerTick) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UActorPool::ProcessPendingSpawningRequest); + SpawnRequestHandleManager.ShrinkHandles(); + + const double TimeSliceEnd = FPlatformTime::Seconds() + MaxTimeSlicePerTick; + + while (FPlatformTime::Seconds() < TimeSliceEnd) + { + auto SpawnRequestHandle = GetNextRequestToSpawn(); + if (!SpawnRequestHandle.IsValid() + || !ensureMsgf( + SpawnRequestHandleManager.IsValidHandle(SpawnRequestHandle), + TEXT("GetNextRequestToSpawn returned an invalid handle, expecting an empty one or a valid one."))) + { + return; + } + + auto SpawnRequest = SpawnRequests[SpawnRequestHandle.GetIndex()]; + + if (!ensureMsgf( + SpawnRequest.Status == ESpawnRequestStatus::Pending + || SpawnRequest.Status == ESpawnRequestStatus::RetryPending, + TEXT("GetNextRequestToSpawn returned a request that was already processed, need to return only request " + "with pending status."))) + { + return; + } + + // Do the spawning + SpawnRequest.Status = ESpawnRequestStatus::Processing; + + SpawnRequest.SpawnedActor = SpawnOrRetrieveFromPool(SpawnRequestHandle, SpawnRequest); + + SpawnRequest.Status = SpawnRequest.SpawnedActor ? ESpawnRequestStatus::Succeeded : ESpawnRequestStatus::Failed; + + // Call the post spawn delegate on the spawn request + if (SpawnRequest.PostSpawnDelegate.IsBound()) + { + if (SpawnRequest.PostSpawnDelegate.Execute(SpawnRequestHandle, SpawnRequest) + == EOUUActorPoolSpawnRequestAction::Remove) + { + SpawnRequestHandleManager.RemoveHandle(SpawnRequestHandle); + } + } + // ... or retry + else if (SpawnRequest.RetryIndefinitely == true && SpawnRequest.SpawnedActor == nullptr) + { + RetryActorSpawnRequest(SpawnRequestHandle); + } + // ... or remove + else + { + SpawnRequestHandleManager.RemoveHandle(SpawnRequestHandle); + } + } +} + +void UOUUActorPool::ProcessPendingDestruction(const double MaxTimeSlicePerTick) +{ + TRACE_CPUPROFILER_EVENT_SCOPE(UActorPool::ProcessPendingDestruction); + + UWorld* World = GetWorld(); + check(World); + + const ENetMode CurrentWorldNetMode = World->GetNetMode(); + const double HasToDestroyAllActorsOnServerSide = + CurrentWorldNetMode != NM_Client && CurrentWorldNetMode != NM_Standalone; + const double TimeSliceEnd = FPlatformTime::Seconds() + MaxTimeSlicePerTick; + + { + // Try release to pool actors or destroy them + TRACE_CPUPROFILER_EVENT_SCOPE(DestroyActors); + while ((DeactivatedActorsToDestroy.Num() || ActorsToDestroy.Num()) + && (HasToDestroyAllActorsOnServerSide || FPlatformTime::Seconds() <= TimeSliceEnd)) + { + AActor* ActorToDestroy = DeactivatedActorsToDestroy.Num() + ? DeactivatedActorsToDestroy.Pop(/*bAllowShrinking*/ false) + : ActorsToDestroy.Pop(/*bAllowShrinking*/ false); + if (!TryReleaseActorToPool(ActorToDestroy)) + { + // Couldn't release actor back to pool, so destroy it + World->DestroyActor(ActorToDestroy); + --NumActorSpawned; + } + } + } + + if (ActorsToDestroy.Num()) + { + // Try release to pool remaining actors or deactivate them + TRACE_CPUPROFILER_EVENT_SCOPE(DeactivateActors); + for (AActor* ActorToDestroy : ActorsToDestroy) + { + if (!TryReleaseActorToPool(ActorToDestroy)) + { + // Couldn't release actor back to pool this frame -> do a simple (hopefully time saving) deactivation + // and queue the actor destroy/deactivate for next frame. + DeactivateActorFast(ActorToDestroy); + DeactivatedActorsToDestroy.Add(ActorToDestroy); + } + } + ActorsToDestroy.Reset(); + } +} + +bool UOUUActorPool::TryReleaseActorToPool(AActor* Actor) +{ + const bool bIsPoolableActor = IsValidInterface(Actor); + if (bIsPoolableActor && CALL_INTERFACE(IOUUPoolableActor, CanBePooled, Actor)) + { + TArray& Pool = PooledActors.FindOrAdd(Actor->GetClass()); + + int32 MaxPoolSize = CALL_INTERFACE(IOUUPoolableActor, GetMaxPoolSize, Actor); + if (Pool.Num() >= MaxPoolSize) + return false; + + CALL_INTERFACE(IOUUPoolableActor, OnAddedToPool, Actor); + + DeactivateActor(Actor); + + checkf(Pool.Find(Actor) == INDEX_NONE, TEXT("Actor %s is already in the pool"), *AActor::GetDebugName(Actor)); + Pool.Add(Actor); + ++NumActorPooled; + return true; + } + return false; +} diff --git a/Source/OUURuntime/Public/Pooling/OUUActorPool.h b/Source/OUURuntime/Public/Pooling/OUUActorPool.h new file mode 100644 index 0000000..366d5f8 --- /dev/null +++ b/Source/OUURuntime/Public/Pooling/OUUActorPool.h @@ -0,0 +1,212 @@ +// Copyright (c) 2023 Jonas Reich & Contributors + +#pragma once + +#include "CoreMinimal.h" + +#include "IndexedHandle.h" +#include "UObject/Interface.h" + +#include "OUUActorPool.generated.h" + +struct FOUUActorPoolSpawnRequest; + +UINTERFACE(Blueprintable) +class OUURUNTIME_API UOUUPoolableActor : public UInterface +{ + GENERATED_BODY() +public: +}; + +class OUURUNTIME_API IOUUPoolableActor : public IInterface +{ + GENERATED_BODY() +public: + /** + * Override in derived classes to check if an actor can be released to the pool. + * Actors that are in an irrecoverable state and should not be re-used should return false. + */ + UFUNCTION(BlueprintNativeEvent, Category = "Open Unreal Utilities|Actor Pooling") + bool CanBePooled() const; + virtual bool CanBePooled_Implementation() const { return true; } + + /** + * Prepare an actor that was stored in pool for game. + * For poolable actors this is similar to BeginPlay(). + */ + UFUNCTION(BlueprintNativeEvent, Category = "Open Unreal Utilities|Actor Pooling") + void OnRemovedFromPool(); + + /** + * Called when an actor was initially added or returned to pool. + * Prepare an actor that was active in-game for pooling. + * For poolable actors this is similar to EndPlay(). + */ + UFUNCTION(BlueprintNativeEvent, Category = "Open Unreal Utilities|Actor Pooling") + void OnAddedToPool(); + + /** How many actors can be pooled (inactive) at the same time? Default: 10 */ + UFUNCTION(BlueprintNativeEvent, Category = "Open Unreal Utilities|Actor Pooling") + int32 GetMaxPoolSize() const; + int32 GetMaxPoolSize_Implementation() const { return 10; } +}; + +USTRUCT() +struct OUURUNTIME_API FOUUActorPoolSpawnRequestHandle : public FIndexedHandleBase +{ + GENERATED_BODY() + + FOUUActorPoolSpawnRequestHandle() = default; + + /** @note passing INDEX_NONE as index will make this handle Invalid */ + FOUUActorPoolSpawnRequestHandle(const int32 InIndex, const uint32 InSerialNumber) : + FIndexedHandleBase(InIndex, InSerialNumber) + { + } +}; + +// Managing class of spawning requests handles +typedef FIndexedHandleManager + FOUUActorPoolHandleManager_ActorSpawnRequest; + +UENUM() +enum class EOUUActorPoolSpawnRequestAction : uint8 +{ + Keep, // Will leave spawning request active and it will be users job to call Cancel or Retry depending on state. + Remove, // Will remove the spawning request from the queue once the callback ends +}; + +DECLARE_DELEGATE_RetVal_TwoParams( + EOUUActorPoolSpawnRequestAction, + FOUUActorPoolPostSpawnDelegate, + const FOUUActorPoolSpawnRequestHandle&, + const FOUUActorPoolSpawnRequest&); + +UENUM() +enum class EOUUActorPoolSpawnRequestStatus : uint8 +{ + None, // Not in the queue to be spawned + Pending, // Still in the queue to be spawned + Processing, // In the process of spawning the actor + Succeeded, // Successfully spawned the actor + Failed, // Error while spawning the actor + RetryPending, // Waiting to retry after a failed spawn request (lower priority) +}; + +/** + * Struct for spawn requests (opposite to Mass system, this is not meant to be subclassed). + */ +USTRUCT() +struct OUURUNTIME_API FOUUActorPoolSpawnRequest +{ + GENERATED_BODY() +public: + UPROPERTY(Transient) + TSubclassOf Template; + + FTransform Transform; + + // Priority of this spawn request in comparison with the others, the lower the value is, the higher the priority is + float Priority = MAX_FLT; + + FOUUActorPoolPostSpawnDelegate PostSpawnDelegate; + + EOUUActorPoolSpawnRequestStatus Status = EOUUActorPoolSpawnRequestStatus::None; + + // The pointer to the actor once it is spawned + UPROPERTY(Transient) + TObjectPtr SpawnedActor = nullptr; + + // Internal request serial number (used to cycle through next spawning request) + uint32 SerialNumber = 0; + + // Creation world time of the request in seconds + double RequestedTime = 0.; + + // If retries are enabled, the spawn request is retried indefinitely until it succeeds. + bool RetryIndefinitely = false; + + // If enabled the spawned actor's construction script is not run and must be executed by user. + bool DelayFirstConstructionScript = false; + + void Reset() + { + Template = nullptr; + Priority = MAX_FLT; + PostSpawnDelegate.Unbind(); + Status = EOUUActorPoolSpawnRequestStatus::None; + SpawnedActor = nullptr; + SerialNumber = 0; + RequestedTime = 0.0f; + } +}; + +/** + * Actor pool similar to the Mass Actor Pool, but without the Mass struct utils dependencies and some modifications + * that made it a bit easier to use with regular actors. + */ +UCLASS() +class OUURUNTIME_API UOUUActorPool : public UTickableWorldSubsystem +{ + GENERATED_BODY() +public: + using FSpawnRequest = FOUUActorPoolSpawnRequest; + using FSpawnRequestHandle = FOUUActorPoolSpawnRequestHandle; + using ESpawnRequestStatus = EOUUActorPoolSpawnRequestStatus; + + static UOUUActorPool* Get(UObject& WorldContext); + + FSpawnRequestHandle RequestActorSpawn(const FSpawnRequest& InSpawnRequest); + void RetryActorSpawnRequest(const FSpawnRequestHandle SpawnRequestHandle); + bool CancelActorSpawnRequest(FSpawnRequestHandle& SpawnRequestHandle); + + // Return back to pool and deactivate or destroy + void DestroyOrReleaseActor(AActor* Actor, bool bImmediate = false); + + // To release all resources + void DestroyAllActors(); + + const FSpawnRequest& GetSpawnRequest(const FSpawnRequestHandle SpawnRequestHandle) const; + FSpawnRequest& GetMutableSpawnRequest(const FSpawnRequestHandle SpawnRequestHandle); + + // - USubsystem + bool ShouldCreateSubsystem(UObject* Outer) const; + // - FTickableGameObject + void Tick(float DeltaTime) override; + TStatId GetStatId() const override; + // - UObject (via reflection) + static void AddReferencedObjects(UObject* InThis, FReferenceCollector& Collector); + // -- +protected: + virtual FSpawnRequestHandle GetNextRequestToSpawn() const; + virtual AActor* SpawnOrRetrieveFromPool(const FSpawnRequestHandle SpawnRequestHandle, FSpawnRequest& SpawnRequest); + virtual AActor* SpawnActor(const FSpawnRequestHandle SpawnRequestHandle, FSpawnRequest SpawnRequest) const; + virtual void ActivateActor(AActor* Actor) const; + virtual void DeactivateActor(AActor* Actor) const; + /** + * Called if too many actors are deactivated in the same frame on any remaining actors. + * This should do the minimum version of DeactivateActor(). + * The full DeactivateActor() implementation is called as soon as possible when the time budget allows it. + */ + virtual void DeactivateActorFast(AActor* Actor) const; + +private: + UPROPERTY() + TArray SpawnRequests; + + UPROPERTY() + TArray> ActorsToDestroy; + + UPROPERTY() + TArray> DeactivatedActorsToDestroy; + + TMap, TArray> PooledActors; + FOUUActorPoolHandleManager_ActorSpawnRequest SpawnRequestHandleManager; + std::atomic RequestSerialNumberCounter; + mutable int32 NumActorSpawned = 0; + mutable int32 NumActorPooled = 0; + + void ProcessPendingSpawningRequest(const double MaxTimeSlicePerTick); + void ProcessPendingDestruction(const double MaxTimeSlicePerTick); + bool TryReleaseActorToPool(AActor* Actor); +};