Skip to content

Commit

Permalink
Implemented Handle mechanism with unique identifiers to manage pool o…
Browse files Browse the repository at this point in the history
…bject lifecycles, allowing for precise object retrieval and spawn request cancellation.
  • Loading branch information
JanSeliv committed Nov 3, 2023
1 parent 0ff09b1 commit c0ee9d3
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 73 deletions.
11 changes: 8 additions & 3 deletions Source/PoolManager/Private/Factories/PoolFactory_Actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,14 @@ UObject* UPoolFactory_Actor::SpawnNow_Implementation(const FSpawnRequest& Reques
AActor* NewActor = World->SpawnActor(ClassToSpawn, &Request.Transform, SpawnParameters);
checkf(NewActor, TEXT("ERROR: [%i] %s:\n'NewActor' was not spawned!"), __LINE__, *FString(__FUNCTION__));

if (Request.Callbacks.OnPreConstructed != nullptr)
FPoolObjectData PoolObjectData;
PoolObjectData.bIsActive = true;
PoolObjectData.PoolObject = NewActor;
PoolObjectData.Handle = Request.Handle;

if (Request.Callbacks.OnPreRegistered != nullptr)
{
Request.Callbacks.OnPreConstructed(NewActor);
Request.Callbacks.OnPreRegistered(PoolObjectData);
}

if (AActor* SpawnedActor = Cast<AActor>(NewActor))
Expand All @@ -52,7 +57,7 @@ UObject* UPoolFactory_Actor::SpawnNow_Implementation(const FSpawnRequest& Reques

if (Request.Callbacks.OnPostSpawned != nullptr)
{
Request.Callbacks.OnPostSpawned(NewActor);
Request.Callbacks.OnPostSpawned(PoolObjectData);
}

return NewActor;
Expand Down
54 changes: 46 additions & 8 deletions Source/PoolManager/Private/Factories/PoolFactory_UObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,18 @@
// Method to queue object spawn requests
void UPoolFactory_UObject::RequestSpawn_Implementation(const FSpawnRequest& Request)
{
if (!ensureMsgf(Request.IsValid(), TEXT("ASSERT: [%i] %s:\n'Request' is not valid and can't be processed!"), __LINE__, *FString(__FUNCTION__)))
{
return;
}

// Add request to queue
SpawnQueueInternal.Enqueue(Request);
SpawnQueueInternal.Emplace(Request);

// If this is the first object in the queue, schedule the OnNextTickProcessSpawn to be called on the next frame
// Creating UObjects on separate threads is not thread-safe and leads to problems with garbage collection,
// so we will create them on the game thread, but defer to next frame to avoid hitches
if (++SpawnQueueSize == 1)
if (SpawnQueueInternal.Num() == 1)
{
const UWorld* World = GetWorld();
checkf(World, TEXT("ERROR: [%i] %s:\n'World' is null!"), __LINE__, *FString(__FUNCTION__));
Expand All @@ -29,14 +34,19 @@ UObject* UPoolFactory_UObject::SpawnNow_Implementation(const FSpawnRequest& Requ
{
UObject* CreatedObject = NewObject<UObject>(GetOuter(), Request.Class);

if (Request.Callbacks.OnPreConstructed != nullptr)
FPoolObjectData PoolObjectData;
PoolObjectData.bIsActive = true;
PoolObjectData.PoolObject = CreatedObject;
PoolObjectData.Handle = Request.Handle;

if (Request.Callbacks.OnPreRegistered != nullptr)
{
Request.Callbacks.OnPreConstructed(CreatedObject);
Request.Callbacks.OnPreRegistered(PoolObjectData);
}

if (Request.Callbacks.OnPostSpawned != nullptr)
{
Request.Callbacks.OnPostSpawned(CreatedObject);
Request.Callbacks.OnPostSpawned(PoolObjectData);
}

return CreatedObject;
Expand All @@ -45,8 +55,36 @@ UObject* UPoolFactory_UObject::SpawnNow_Implementation(const FSpawnRequest& Requ
// Removes the first spawn request from the queue and returns it
bool UPoolFactory_UObject::DequeueSpawnRequest(FSpawnRequest& OutRequest)
{
--SpawnQueueSize;
return SpawnQueueInternal.Dequeue(OutRequest);
if (!SpawnQueueInternal.IsValidIndex(0))
{
return false;
}

// Copy and remove first request from the queue (without Swap to keep order)
OutRequest = SpawnQueueInternal[0];
SpawnQueueInternal.RemoveAt(0);

return OutRequest.IsValid();
}

// Alternative method to remove specific spawn request from the queue and returns it.
bool UPoolFactory_UObject::DequeueSpawnRequestByHandle(const FPoolObjectHandle& Handle, FSpawnRequest& OutRequest)
{
const int32 Idx = SpawnQueueInternal.IndexOfByPredicate([&Handle](const FSpawnRequest& Request)
{
return Request.Handle == Handle;
});

if (!SpawnQueueInternal.IsValidIndex(Idx))
{
return false;
}

// Copy and remove first request from the queue (without Swap to keep order)
OutRequest = SpawnQueueInternal[Idx];
SpawnQueueInternal.RemoveAt(Idx);

return OutRequest.IsValid();
}

// Is called on next frame to process a chunk of the spawn queue
Expand All @@ -58,7 +96,7 @@ void UPoolFactory_UObject::OnNextTickProcessSpawn_Implementation()
ObjectsPerFrame = 1;
}

for (int32 Index = 0; Index < FMath::Min(ObjectsPerFrame, SpawnQueueSize); ++Index)
for (int32 Index = 0; Index < FMath::Min(ObjectsPerFrame, SpawnQueueInternal.Num()); ++Index)
{
FSpawnRequest OutRequest;
if (DequeueSpawnRequest(OutRequest))
Expand Down
135 changes: 102 additions & 33 deletions Source/PoolManager/Private/PoolManagerSubsystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,46 +58,47 @@ UPoolManagerSubsystem* UPoolManagerSubsystem::GetPoolManagerByClass(TSubclassOf<
// Async version of TakeFromPool() that returns the object by specified class
void UPoolManagerSubsystem::BPTakeFromPool(const UClass* ObjectClass, const FTransform& Transform, const FOnTakenFromPool& Completed)
{
UObject* PoolObject = TakeFromPoolOrNull(ObjectClass, Transform);
if (PoolObject)
const FPoolObjectData* ObjectData = TakeFromPoolOrNull(ObjectClass, Transform);
if (ObjectData)
{
// Found in pool
Completed.ExecuteIfBound(PoolObject);
Completed.ExecuteIfBound(ObjectData->PoolObject);
return;
}

FSpawnRequest Request;
Request.Class = ObjectClass;
Request.Transform = Transform;
Request.Callbacks.OnPostSpawned = [Completed](UObject* SpawnedObject)
Request.Callbacks.OnPostSpawned = [Completed](const FPoolObjectData& ObjectData)
{
Completed.ExecuteIfBound(SpawnedObject);
Completed.ExecuteIfBound(ObjectData.PoolObject);
};
CreateNewObjectInPool(Request);
}

// Is code async version of TakeFromPool() that calls callback functions when the object is ready
void UPoolManagerSubsystem::TakeFromPool(const UClass* ObjectClass, const FTransform& Transform/* = FTransform::Identity*/, const TFunction<void(UObject*)>& Completed/* = nullptr*/)
FPoolObjectHandle UPoolManagerSubsystem::TakeFromPool(const UClass* ObjectClass, const FTransform& Transform/* = FTransform::Identity*/, const FOnSpawnCallback& Completed/* = nullptr*/)
{
UObject* PoolObject = TakeFromPoolOrNull(ObjectClass, Transform);
if (PoolObject)
const FPoolObjectData* ObjectData = TakeFromPoolOrNull(ObjectClass, Transform);
if (ObjectData)
{
if (Completed != nullptr)
{
Completed(PoolObject);
Completed(*ObjectData);
}
return;

return ObjectData->Handle;
}

FSpawnRequest Request;
Request.Class = ObjectClass;
Request.Transform = Transform;
Request.Callbacks.OnPostSpawned = Completed;
CreateNewObjectInPool(Request);
return CreateNewObjectInPool(Request);
}

// Is internal function to find object in pool or return null
UObject* UPoolManagerSubsystem::TakeFromPoolOrNull_Implementation(const UClass* ObjectClass, const FTransform& Transform)
const FPoolObjectData* UPoolManagerSubsystem::TakeFromPoolOrNull(const UClass* ObjectClass, const FTransform& Transform)
{
if (!ensureMsgf(ObjectClass, TEXT("%s: 'ObjectClass' is not specified"), *FString(__FUNCTION__)))
{
Expand All @@ -112,36 +113,38 @@ UObject* UPoolManagerSubsystem::TakeFromPoolOrNull_Implementation(const UClass*
}

// Try to find first object contained in the Pool by its class that is inactive and ready to be taken from pool
UObject* FoundObject = nullptr;
for (const FPoolObjectData& PoolObjectIt : Pool->PoolObjects)
const FPoolObjectData* FoundData = nullptr;
for (const FPoolObjectData& DataIt : Pool->PoolObjects)
{
if (PoolObjectIt.IsFree())
if (DataIt.IsFree())
{
FoundObject = PoolObjectIt.Get();
FoundData = &DataIt;
break;
}
}

if (!FoundObject)
if (!FoundData)
{
// No free objects in pool
return nullptr;
}

Pool->GetFactoryChecked().OnTakeFromPool(FoundObject, Transform);
UObject& InObject = FoundData->GetChecked();

Pool->GetFactoryChecked().OnTakeFromPool(&InObject, Transform);

SetObjectStateInPool(EPoolObjectState::Active, *FoundObject, *Pool);
SetObjectStateInPool(EPoolObjectState::Active, InObject, *Pool);

return FoundObject;
return FoundData;
}

// Returns the specified object to the pool and deactivates it if the object was taken from the pool before
void UPoolManagerSubsystem::ReturnToPool_Implementation(UObject* Object)
bool UPoolManagerSubsystem::ReturnToPool_Implementation(UObject* Object)
{
FPoolContainer* Pool = Object ? FindPool(Object->GetClass()) : nullptr;
if (!ensureMsgf(Pool, TEXT("ASSERT: [%i] %s:\n'Pool' is not not registered for '%s' object, can not return it to pool!"), __LINE__, *FString(__FUNCTION__), *GetNameSafe(Object)))
{
return;
return false;
}

UPoolFactory_UObject& Factory = Pool->GetFactoryChecked();
Expand All @@ -157,54 +160,96 @@ void UPoolManagerSubsystem::ReturnToPool_Implementation(UObject* Object)
Factory.DequeueSpawnRequest(OutRequest);
TakeFromPool(OutRequest.Class, OutRequest.Transform, OutRequest.Callbacks.OnPostSpawned);
}

return true;
}

// Alternative to ReturnToPool() to return object to the pool by its handle
bool UPoolManagerSubsystem::ReturnToPool(const FPoolObjectHandle& Handle)
{
if (UObject* PoolObject = FindPoolObjectByHandle(Handle))
{
return ReturnToPool(PoolObject);
}

// Object is not found, attempt to cancel spawn request if is in queue
for (const TTuple<TObjectPtr<const UClass>, TObjectPtr<UPoolFactory_UObject>>& It : AllFactoriesInternal)
{
FSpawnRequest OutRequest;
if (It.Value && It.Value->DequeueSpawnRequestByHandle(Handle, OutRequest))
{
// Spawn request is found and removed from the queue
return true;
}
}

return false;
}

/*********************************************************************************************
* Advanced
********************************************************************************************* */

// Adds specified object as is to the pool by its class to be handled by the Pool Manager
bool UPoolManagerSubsystem::RegisterObjectInPool_Implementation(UObject* Object, EPoolObjectState PoolObjectState/* = EPoolObjectState::Inactive*/)
bool UPoolManagerSubsystem::RegisterObjectInPool_Implementation(const FPoolObjectData& InData)
{
if (!ensureMsgf(Object, TEXT("ASSERT: [%i] %s:\n'Object' is null, can't register!"), __LINE__, *FString(__FUNCTION__)))
if (!ensureMsgf(InData.PoolObject, TEXT("ASSERT: [%i] %s:\n'PoolObject' is not valid, can't registed it in the Pool!"), __LINE__, *FString(__FUNCTION__)))
{
return false;
}

const UClass* ObjectClass = Object->GetClass();
const UClass* ObjectClass = InData.PoolObject.GetClass();
FPoolContainer& Pool = FindPoolOrAdd(ObjectClass);

if (Pool.FindInPool(*Object))
if (Pool.FindInPool(*InData.PoolObject))
{
// Already contains in pool
return false;
}

FPoolObjectData& ObjectDataRef = Pool.PoolObjects.Emplace_GetRef(Object);
ObjectDataRef.bIsActive = PoolObjectState == EPoolObjectState::Active;
FPoolObjectData Data = InData;
if (!Data.Handle.IsValid())
{
// Hash can be unset that is fine, generate new one
Data.Handle = FPoolObjectHandle::NewHandle(*ObjectClass);
}

SetObjectStateInPool(PoolObjectState, *Object, Pool);
Pool.PoolObjects.Emplace(Data);

SetObjectStateInPool(Data.GetState(), *Data.PoolObject, Pool);

return true;
}

// Always creates new object and adds it to the pool by its class
void UPoolManagerSubsystem::CreateNewObjectInPool_Implementation(const FSpawnRequest& InRequest)
FPoolObjectHandle UPoolManagerSubsystem::CreateNewObjectInPool_Implementation(const FSpawnRequest& InRequest)
{
if (!ensureMsgf(InRequest.Class, TEXT("ASSERT: [%i] %s:\n'Class' is not null in the Spawn Request!"), __LINE__, *FString(__FUNCTION__)))
{
return FPoolObjectHandle::EmptyHandle;
}

FSpawnRequest Request = InRequest;
if (!Request.Handle.IsValid())
{
// Hash can be unset that is fine, generate new one
Request.Handle = FPoolObjectHandle::NewHandle(*Request.Class);
}

// Always register new object in pool once it is spawned
const TWeakObjectPtr<ThisClass> WeakThis(this);
Request.Callbacks.OnPreConstructed = [WeakThis](UObject* Object)
Request.Callbacks.OnPreRegistered = [WeakThis](const FPoolObjectData& ObjectData)
{
if (UPoolManagerSubsystem* PoolManager = WeakThis.Get())
{
PoolManager->RegisterObjectInPool(Object, EPoolObjectState::Active);
PoolManager->RegisterObjectInPool(ObjectData);
}
};

const FPoolContainer& Pool = FindPoolOrAdd(Request.Class);
Pool.GetFactoryChecked().RequestSpawn(Request);

return Request.Handle;
}

/*********************************************************************************************
Expand Down Expand Up @@ -408,7 +453,7 @@ EPoolObjectState UPoolManagerSubsystem::GetPoolObjectState_Implementation(const
return EPoolObjectState::None;
}

return PoolObject->IsActive() ? EPoolObjectState::Active : EPoolObjectState::Inactive;
return PoolObject->GetState();
}

// Returns true is specified object is handled by Pool Manager
Expand Down Expand Up @@ -481,6 +526,30 @@ int32 UPoolManagerSubsystem::GetRegisteredObjectsNum_Implementation(const UClass
return RegisteredObjectsNum;
}

// Returns the object associated with given handle
UObject* UPoolManagerSubsystem::FindPoolObjectByHandle(const FPoolObjectHandle& Handle) const
{
if (!Handle.IsValid())
{
// Handle is empty, nothing to find
return nullptr;
}

const FPoolContainer* Pool = FindPool(Handle.GetObjectClass());
if (!Pool)
{
// Pool is not registered
return nullptr;
}

const FPoolObjectData* FoundData = Pool->PoolObjects.FindByPredicate([&Handle](const FPoolObjectData& PoolObjectIt)
{
return PoolObjectIt.Handle == Handle;
});

return FoundData ? FoundData->PoolObject : nullptr;
}

/*********************************************************************************************
* Protected methods
********************************************************************************************* */
Expand Down
Loading

0 comments on commit c0ee9d3

Please sign in to comment.