Skip to content

Commit

Permalink
Take From Pool now spreads out the creation of large pools of UObject…
Browse files Browse the repository at this point in the history
…s and Actors over multiple frames to avoid any hitches.
  • Loading branch information
JanSeliv committed Oct 27, 2023
1 parent c5cb4c0 commit 70dccf1
Show file tree
Hide file tree
Showing 19 changed files with 1,237 additions and 211 deletions.
2 changes: 2 additions & 0 deletions Config/BasePoolManager.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[/Script/PoolManager.PoolManagerSettings]
SpawnObjectsPerFrame=5
5 changes: 5 additions & 0 deletions Config/FilterPlugin.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.

[FilterPlugin]
/Config/BasePoolManager.ini
5 changes: 5 additions & 0 deletions PoolManager.uplugin
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"Name": "PoolManager",
"Type": "Runtime",
"LoadingPhase": "EarliestPossible"
},
{
"Name": "PoolManagerEditor",
"Type": "UncookedOnly",
"LoadingPhase": "EarliestPossible"
}
]
}
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Creating and destroying objects, like projectiles or explosions, can be slow and

The Pool Manager alleviates these problems by maintaining a pool of objects. Instead of creating and destroying objects all the time, the Pool Manager keeps these objects for reuse. This strategy improves the smoothness of the game.

![PoolManager](https://github.com/JanSeliv/PoolManager/assets/20540872/b3df793b-059b-4bf1-a04f-d06289fad5b5)
![PoolManager](https://github.com/JanSeliv/PoolManager/assets/20540872/0af55b33-732c-435d-a5b3-2d7e36cdebf2)

## 📚 Documentation

Expand All @@ -21,6 +21,9 @@ Also, explore this [game project repository](https://github.com/JanSeliv/Bomber)
## 📅 Changelog
####
- Updated to **Unreal Engine 5.3**.
- Introduced **Factories** to handle differences in pools by object archetypes (e.g.: uobjects, actors, components, widgets etc.).
- **Take From Pool** now spreads out the creation of large pools of UObjects and Actors over multiple frames to avoid any hitches.
![image](https://github.com/JanSeliv/PoolManager/assets/20540872/10bdf24f-d078-4dd8-96bf-de5d92421bc8)
#### 2023-05-28
- 🎉 Initial public release on Unreal Engine 5.2

Expand Down
1 change: 1 addition & 0 deletions Source/PoolManager/PoolManager.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public PoolManager(ReadOnlyTargetRules Target) : base(Target)
PublicDependencyModuleNames.AddRange(new[]
{
"Core"
, "DeveloperSettings" // Created UPoolManagerSettings
}
);

Expand Down
106 changes: 106 additions & 0 deletions Source/PoolManager/Private/Factories/PoolFactory_Actor.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Yevhenii Selivanov

#include "Factories/PoolFactory_Actor.h"
//---
#include "Engine/World.h"
#include "GameFramework/Actor.h"
//---
#include UE_INLINE_GENERATED_CPP_BY_NAME(PoolFactory_Actor)

// It's almost farthest possible location where deactivated actors are placed
#define VECTOR_HALF_WORLD_MAX FVector(HALF_WORLD_MAX - HALF_WORLD_MAX * THRESH_VECTOR_NORMALIZED)

// Is overridden to handle Actors-inherited classes
const UClass* UPoolFactory_Actor::GetObjectClass_Implementation() const
{
return AActor::StaticClass();
}

/*********************************************************************************************
* Creation
********************************************************************************************* */

// Is overridden to spawn actors using its engine's Spawn Actor method
UObject* UPoolFactory_Actor::SpawnNow_Implementation(const FSpawnRequest& Request)
{
// Super is not called to Spawn Actor instead of NewObject

UWorld* World = GetWorld();
checkf(World, TEXT("ERROR: [%i] %s:\n'World' is null!"), __LINE__, *FString(__FUNCTION__));

const TSubclassOf<AActor> ClassToSpawn = const_cast<UClass*>(Request.Class.Get());
FActorSpawnParameters SpawnParameters;
SpawnParameters.OverrideLevel = World->PersistentLevel; // Always keep new objects on Persistent level
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParameters.bDeferConstruction = true; // Delay construction to add it to the pool first
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)
{
Request.Callbacks.OnPreConstructed(NewActor);
}

if (AActor* SpawnedActor = Cast<AActor>(NewActor))
{
// Call construction script since it was delayed before to add it to the pool first
SpawnedActor->FinishSpawning(Request.Transform);
}

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

return NewActor;
}

/*********************************************************************************************
* Destruction
********************************************************************************************* */

// Is overridden to destroy given actor using its engine's Destroy Actor method
void UPoolFactory_Actor::Destroy_Implementation(UObject* Object)
{
// Super is not called to Destroy Actor instead of ConditionalBeginDestroy

AActor* Actor = CastChecked<AActor>(Object);
checkf(IsValid(Actor), TEXT("ERROR: [%i] %s:\n'IsValid(Actor)' is null!"), __LINE__, *FString(__FUNCTION__));
Actor->Destroy();
}

/*********************************************************************************************
* Pool
********************************************************************************************* */

// Is overridden to set transform to the actor before taking the object from its pool
void UPoolFactory_Actor::OnTakeFromPool_Implementation(UObject* Object, const FTransform& Transform)
{
Super::OnTakeFromPool_Implementation(Object, Transform);

AActor* Actor = CastChecked<AActor>(Object);
Actor->SetActorTransform(Transform);
}

// Is overridden to reset transform to the actor before returning the object to its pool
void UPoolFactory_Actor::OnReturnToPool_Implementation(UObject* Object)
{
Super::OnReturnToPool_Implementation(Object);

// SetCollisionEnabled is not replicated, client collides with hidden actor, so move it far away
AActor* Actor = CastChecked<AActor>(Object);
Actor->SetActorLocation(VECTOR_HALF_WORLD_MAX);
}

// Is overridden to change visibility, collision, ticking, etc. according new state
void UPoolFactory_Actor::OnChangedStateInPool_Implementation(EPoolObjectState NewState, UObject* InObject)
{
Super::OnChangedStateInPool_Implementation(NewState, InObject);

AActor* Actor = CastChecked<AActor>(InObject);
const bool bActivate = NewState == EPoolObjectState::Active;

Actor->SetActorHiddenInGame(!bActivate);
Actor->SetActorEnableCollision(bActivate);
Actor->SetActorTickEnabled(bActivate);
}
77 changes: 77 additions & 0 deletions Source/PoolManager/Private/Factories/PoolFactory_UObject.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Yevhenii Selivanov

#include "Factories/PoolFactory_UObject.h"
//---
#include "Data/PoolManagerSettings.h"
//---
#include UE_INLINE_GENERATED_CPP_BY_NAME(PoolFactory_UObject)

// Method to queue object spawn requests
void UPoolFactory_UObject::RequestSpawn_Implementation(const FSpawnRequest& Request)
{
// Add request to queue
SpawnQueue.Enqueue(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)
{
const UWorld* World = GetWorld();
checkf(World, TEXT("ERROR: [%i] %s:\n'World' is null!"), __LINE__, *FString(__FUNCTION__));

World->GetTimerManager().SetTimerForNextTick(this, &ThisClass::OnNextTickProcessSpawn);
}
}

// Method to immediately spawn requested object
UObject* UPoolFactory_UObject::SpawnNow_Implementation(const FSpawnRequest& Request)
{
UObject* CreatedObject = NewObject<UObject>(GetOuter(), Request.Class);

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

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

return CreatedObject;
}

// Is called on next frame to process a chunk of the spawn queue
void UPoolFactory_UObject::OnNextTickProcessSpawn_Implementation()
{
int32 ObjectsPerFrame = UPoolManagerSettings::Get().GetSpawnObjectsPerFrame();
if (!ensureMsgf(ObjectsPerFrame >= 1, TEXT("ASSERT: [%i] %s:\n'ObjectsPerFrame' is less than 1, set the config!"), __LINE__, *FString(__FUNCTION__)))
{
ObjectsPerFrame = 1;
}

for (int32 Index = 0; Index < FMath::Min(ObjectsPerFrame, SpawnQueueSize); ++Index)
{
FSpawnRequest OutRequest;
SpawnQueue.Dequeue(OutRequest);
SpawnNow(OutRequest);
--SpawnQueueSize;
}

// If there are more actors to spawn, schedule this function to be called again on the next frame
// Is deferred to next frame instead of doing it on other threads since spawning actors is not thread-safe operation
if (!SpawnQueue.IsEmpty())
{
const UWorld* World = GetWorld();
checkf(World, TEXT("ERROR: [%i] %s:\n'World' is null!"), __LINE__, *FString(__FUNCTION__));
World->GetTimerManager().SetTimerForNextTick(this, &ThisClass::OnNextTickProcessSpawn);
}
}

// Method to destroy given object
void UPoolFactory_UObject::Destroy_Implementation(UObject* Object)
{
checkf(IsValid(Object), TEXT("ERROR: [%i] %s:\n'IsValid(Object)' is not valid!"), __LINE__, *FString(__FUNCTION__));
Object->ConditionalBeginDestroy();
}
Loading

0 comments on commit 70dccf1

Please sign in to comment.