diff --git a/Config/BasePoolManager.ini b/Config/BasePoolManager.ini new file mode 100644 index 0000000..c35f7d0 --- /dev/null +++ b/Config/BasePoolManager.ini @@ -0,0 +1,2 @@ +[/Script/PoolManager.PoolManagerSettings] +SpawnObjectsPerFrame=5 \ No newline at end of file diff --git a/Config/FilterPlugin.ini b/Config/FilterPlugin.ini new file mode 100644 index 0000000..a796e29 --- /dev/null +++ b/Config/FilterPlugin.ini @@ -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 diff --git a/PoolManager.uplugin b/PoolManager.uplugin index c5cf2f3..2a4bdff 100644 --- a/PoolManager.uplugin +++ b/PoolManager.uplugin @@ -20,6 +20,11 @@ "Name": "PoolManager", "Type": "Runtime", "LoadingPhase": "EarliestPossible" + }, + { + "Name": "PoolManagerEditor", + "Type": "UncookedOnly", + "LoadingPhase": "EarliestPossible" } ] } \ No newline at end of file diff --git a/README.md b/README.md index b49b137..2f41be7 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/Source/PoolManager/PoolManager.Build.cs b/Source/PoolManager/PoolManager.Build.cs index b4db687..8e810ba 100644 --- a/Source/PoolManager/PoolManager.Build.cs +++ b/Source/PoolManager/PoolManager.Build.cs @@ -13,6 +13,7 @@ public PoolManager(ReadOnlyTargetRules Target) : base(Target) PublicDependencyModuleNames.AddRange(new[] { "Core" + , "DeveloperSettings" // Created UPoolManagerSettings } ); diff --git a/Source/PoolManager/Private/Factories/PoolFactory_Actor.cpp b/Source/PoolManager/Private/Factories/PoolFactory_Actor.cpp new file mode 100644 index 0000000..dd29404 --- /dev/null +++ b/Source/PoolManager/Private/Factories/PoolFactory_Actor.cpp @@ -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 ClassToSpawn = const_cast(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(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(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(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(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(InObject); + const bool bActivate = NewState == EPoolObjectState::Active; + + Actor->SetActorHiddenInGame(!bActivate); + Actor->SetActorEnableCollision(bActivate); + Actor->SetActorTickEnabled(bActivate); +} diff --git a/Source/PoolManager/Private/Factories/PoolFactory_UObject.cpp b/Source/PoolManager/Private/Factories/PoolFactory_UObject.cpp new file mode 100644 index 0000000..78dd4de --- /dev/null +++ b/Source/PoolManager/Private/Factories/PoolFactory_UObject.cpp @@ -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(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(); +} diff --git a/Source/PoolManager/Private/PoolManagerSubsystem.cpp b/Source/PoolManager/Private/PoolManagerSubsystem.cpp index 73c234d..28a4490 100644 --- a/Source/PoolManager/Private/PoolManagerSubsystem.cpp +++ b/Source/PoolManager/Private/PoolManagerSubsystem.cpp @@ -2,8 +2,9 @@ #include "PoolManagerSubsystem.h" //--- +#include "Factories/PoolFactory_UObject.h" +//--- #include "Engine/World.h" -#include "GameFramework/Actor.h" //--- #if WITH_EDITOR #include "Editor.h" @@ -11,8 +12,9 @@ //--- #include UE_INLINE_GENERATED_CPP_BY_NAME(PoolManagerSubsystem) -// 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) +/********************************************************************************************* + * Static Getters + ********************************************************************************************* */ // Returns the pointer to your Pool Manager UPoolManagerSubsystem* UPoolManagerSubsystem::GetPoolManagerByClass(TSubclassOf OptionalClass/* = nullptr*/, const UObject* OptionalWorldContext/* = nullptr*/) @@ -49,37 +51,108 @@ UPoolManagerSubsystem* UPoolManagerSubsystem::GetPoolManagerByClass(TSubclassOf< return FoundPoolManager; } -// Get the object from a pool by specified class -UObject* UPoolManagerSubsystem::TakeFromPool_Implementation(const UClass* ClassInPool, const FTransform& Transform) +/********************************************************************************************* + * Main API + ********************************************************************************************* */ + +// Async version of TakeFromPool() that returns the object by specified class +void UPoolManagerSubsystem::BPTakeFromPool(const UClass* ObjectClass, const FTransform& Transform, const FOnTakenFromPool& Completed) { - if (!ensureMsgf(ClassInPool, TEXT("%s: 'ClassInPool' is not specified"), *FString(__FUNCTION__))) + UObject* PoolObject = TakeFromPoolOrNull(ObjectClass, Transform); + if (PoolObject) { - return nullptr; + // Found in pool + Completed.ExecuteIfBound(PoolObject); + return; } - // Try to get free object from the pool and return - if (UObject* PoolObject = GetFreeObjectInPool(ClassInPool)) + FSpawnRequest Request; + Request.Class = ObjectClass; + Request.Transform = Transform; + Request.Callbacks.OnPostSpawned = [Completed](UObject* SpawnedObject) { - if (AActor* Actor = Cast(PoolObject)) + Completed.ExecuteIfBound(SpawnedObject); + }; + 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& Completed/* = nullptr*/) +{ + UObject* PoolObject = TakeFromPoolOrNull(ObjectClass, Transform); + if (PoolObject) + { + if (Completed != nullptr) { - Actor->SetActorTransform(Transform); + Completed(PoolObject); } + return; + } + + FSpawnRequest Request; + Request.Class = ObjectClass; + Request.Transform = Transform; + Request.Callbacks.OnPostSpawned = Completed; + CreateNewObjectInPool(Request); +} + +// Is internal function to find object in pool or return null +UObject* UPoolManagerSubsystem::TakeFromPoolOrNull_Implementation(const UClass* ObjectClass, const FTransform& Transform) +{ + if (!ensureMsgf(ObjectClass, TEXT("%s: 'ObjectClass' is not specified"), *FString(__FUNCTION__))) + { + return nullptr; + } + + FPoolContainer* Pool = FindPool(ObjectClass); + if (!Pool) + { + // Pool is not registered + return nullptr; + } - SetActive(true, PoolObject); + // 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) + { + if (PoolObjectIt.IsFree()) + { + FoundObject = PoolObjectIt.Get(); + break; + } + } - return PoolObject; + if (!FoundObject) + { + // No free objects in pool + return nullptr; } - // Since there is no free object in the pool, create a new one - return CreateNewObjectInPool(ClassInPool, Transform, EPoolObjectState::Active); + Pool->GetFactoryChecked().OnTakeFromPool(FoundObject, Transform); + + SetObjectStateInPool(EPoolObjectState::Active, FoundObject, *Pool); + + return FoundObject; } // 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) { - SetActive(false, 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; + } + + Pool->GetFactoryChecked().OnReturnToPool(Object); + + SetObjectStateInPool(EPoolObjectState::Inactive, Object, *Pool); } +/********************************************************************************************* + * 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*/) { @@ -88,127 +161,171 @@ bool UPoolManagerSubsystem::RegisterObjectInPool_Implementation(UObject* Object, return false; } - const UClass* ActorClass = Object->GetClass(); - FPoolContainer* Pool = FindPool(ActorClass); - if (!Pool) - { - const int32 PoolIndex = PoolsInternal.Emplace(FPoolContainer(ActorClass)); - Pool = &PoolsInternal[PoolIndex]; - } - - if (!ensureMsgf(Pool, TEXT("%s: 'Pool' is not valid"), *FString(__FUNCTION__))) - { - return false; - } + const UClass* ObjectClass = Object->GetClass(); + FPoolContainer& Pool = FindPoolOrAdd(ObjectClass); - if (Pool->FindInPool(Object)) + if (Pool.FindInPool(Object)) { // Already contains in pool return false; } - FPoolObjectData PoolObject(Object); + FPoolObjectData& ObjectDataRef = Pool.PoolObjects.Emplace_GetRef(Object); + ObjectDataRef.bIsActive = PoolObjectState == EPoolObjectState::Active; + + SetObjectStateInPool(PoolObjectState, Object, Pool); - if (const AActor* Actor = Cast(Object)) + return true; +} + +// Always creates new object and adds it to the pool by its class +void UPoolManagerSubsystem::CreateNewObjectInPool_Implementation(FSpawnRequest& InRequest) +{ + // Always register new object in pool once it is spawned + const TWeakObjectPtr WeakThis = this; + InRequest.Callbacks.OnPreConstructed = [WeakThis, InRequest](UObject* Object) { - // Decide by its location should it be activated or not if only state is not specified - switch (PoolObjectState) + if (UPoolManagerSubsystem* PoolManager = WeakThis.Get()) { - case EPoolObjectState::None: - PoolObject.bIsActive = !Actor->GetActorLocation().Equals(VECTOR_HALF_WORLD_MAX); - break; - case EPoolObjectState::Active: - PoolObject.bIsActive = true; - break; - case EPoolObjectState::Inactive: - PoolObject.bIsActive = false; - break; - default: - checkf(false, TEXT("%s: Invalid plugin enumeration type. Need to add a handle for that case here"), *FString(__FUNCTION__)); - break; + PoolManager->RegisterObjectInPool(Object, EPoolObjectState::Active); } - } + }; - Pool->PoolObjects.Emplace(PoolObject); + FindPoolOrAdd(InRequest.Class).GetFactoryChecked().RequestSpawn(InRequest); +} - SetActive(PoolObject.bIsActive, Object); +/********************************************************************************************* + * Advanced - Factories + ********************************************************************************************* */ - return true; +// Registers new factory to be used by the Pool Manager when dealing with objects of specific class and its children +void UPoolManagerSubsystem::AddFactory(TSubclassOf FactoryClass) +{ + const UClass* ObjectClass = GetObjectClassByFactory(FactoryClass); + if (!ensureMsgf(ObjectClass, TEXT("ASSERT: [%i] %s:\n'ObjectClass' is not set for next factory: %s"), __LINE__, *FString(__FUNCTION__), *FactoryClass->GetName())) + { + return; + } + + UPoolFactory_UObject* NewFactory = NewObject(this, FactoryClass); + AllFactoriesInternal.Emplace(ObjectClass, NewFactory); } -// Always creates new object and adds it to the pool by its class -UObject* UPoolManagerSubsystem::CreateNewObjectInPool_Implementation(const UClass* ObjectClass, const FTransform& Transform, EPoolObjectState PoolObjectState) +// Removes factory from the Pool Manager by its class +void UPoolManagerSubsystem::RemoveFactory(TSubclassOf FactoryClass) { - UWorld* World = GetWorld(); - if (!ensureMsgf(World, TEXT("%s: 'World' is not valid"), *FString(__FUNCTION__))) + const UClass* ObjectClass = GetObjectClassByFactory(FactoryClass); + if (!ensureMsgf(ObjectClass, TEXT("ASSERT: [%i] %s:\n'ObjectClass' is not set for next factory: %s"), __LINE__, *FString(__FUNCTION__), *FactoryClass->GetName())) { - return nullptr; + return; } - FPoolContainer* Pool = FindPool(ObjectClass); - if (!Pool) + const TObjectPtr* FactoryPtr = AllFactoriesInternal.Find(ObjectClass); + if (!ensureMsgf(FactoryPtr, TEXT("ASSERT: [%i] %s:\nFactory is not found for next class: %s"), __LINE__, *FString(__FUNCTION__), *ObjectClass->GetName())) { - const int32 PoolIndex = PoolsInternal.Emplace(FPoolContainer(ObjectClass)); - Pool = &PoolsInternal[PoolIndex]; + return; } - UObject* CreatedObject; - if (ObjectClass->IsChildOf()) + UPoolFactory_UObject* Factory = *FactoryPtr; + if (IsValid(Factory)) { - UClass* ClassToSpawn = const_cast(ObjectClass); - 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 - CreatedObject = World->SpawnActor(ClassToSpawn, &Transform, SpawnParameters); + Factory->ConditionalBeginDestroy(); } - else + + AllFactoriesInternal.Remove(ObjectClass); +} + +// Traverses the class hierarchy to find the closest registered factory for a given object type or its ancestors +UPoolFactory_UObject* UPoolManagerSubsystem::FindPoolFactoryChecked(const UClass* ObjectClass) const +{ + checkf(ObjectClass, TEXT("ERROR: [%i] %s:\n'ObjectClass' is null!"), __LINE__, *FString(__FUNCTION__)); + + const TObjectPtr* FoundFactory = nullptr; + const UClass* CurrentClass = ObjectClass; + + // This loop will keep traversing up the hierarchy until a registered factory is found or the root is reached + while (CurrentClass != nullptr) { - CreatedObject = NewObject(World, ObjectClass); + FoundFactory = AllFactoriesInternal.Find(CurrentClass); + if (FoundFactory) + { + break; // Exit the loop if a factory is found + } + CurrentClass = CurrentClass->GetSuperClass(); // Otherwise, move up the class hierarchy } - checkf(CreatedObject, TEXT("CRITICAL ERROR: %s: 'CreatedObject' is not valid"), *FString(__FUNCTION__)) - - FPoolObjectData PoolObjectData; - PoolObjectData.PoolObject = CreatedObject; - // Set activity here instead of calling UPoolManagerSubsystem::SetActive since new object never was inactivated before to switch the state - PoolObjectData.bIsActive = PoolObjectState == EPoolObjectState::Active; - Pool->PoolObjects.Emplace(MoveTemp(PoolObjectData)); + checkf(FoundFactory, TEXT("ERROR: [%i] %s:\n'FoundFactory' is null for next object class: %s"), __LINE__, *FString(__FUNCTION__), *GetNameSafe(ObjectClass)); + return *FoundFactory; +} - if (AActor* SpawnedActor = Cast(CreatedObject)) +// Returns default class of object that is handled by given factory +const UClass* UPoolManagerSubsystem::GetObjectClassByFactory(const TSubclassOf& FactoryClass) +{ + if (!ensureMsgf(FactoryClass, TEXT("ASSERT: [%i] %s:\n'FactoryClass' is null!"), __LINE__, *FString(__FUNCTION__))) { - // Call construction script since it was delayed before to add it to the pool first - SpawnedActor->FinishSpawning(Transform); + return nullptr; } - return CreatedObject; + const UPoolFactory_UObject* FactoryCDO = CastChecked(FactoryClass->GetDefaultObject()); + return FactoryCDO->GetObjectClass(); } -// Destroy all object of a pool by a given class -void UPoolManagerSubsystem::EmptyPool_Implementation(const UClass* ClassInPool) +// Creates all possible Pool Factories to be used by the Pool Manager when dealing with objects +void UPoolManagerSubsystem::InitializeAllFactories() { - FPoolContainer* Pool = FindPool(ClassInPool); - if (!ensureMsgf(Pool, TEXT("%s: 'Pool' is not valid"), *FString(__FUNCTION__))) + if (!AllFactoriesInternal.IsEmpty()) { + // Already initialized return; } - TArray& PoolObjects = Pool->PoolObjects; - for (int32 Index = PoolObjects.Num() - 1; Index >= 0; --Index) + for (TObjectIterator It; It; ++It) { - UObject* ObjectIt = PoolObjects.IsValidIndex(Index) ? PoolObjects[Index].Get() : nullptr; - if (!IsValid(ObjectIt)) + if (!It->IsChildOf(UPoolFactory_UObject::StaticClass()) + || It->HasAnyClassFlags(CLASS_Abstract)) { continue; } - if (AActor* Actor = Cast(ObjectIt)) + AddFactory(*It); + } +} + +// Destroys all Pool Factories that are used by the Pool Manager when dealing with objects +void UPoolManagerSubsystem::ClearAllFactories() +{ + for (const TTuple, TObjectPtr>& FactoryIt : AllFactoriesInternal) + { + if (IsValid(FactoryIt.Value)) { - Actor->Destroy(); + FactoryIt.Value->ConditionalBeginDestroy(); } - else + } + + AllFactoriesInternal.Empty(); +} + +/********************************************************************************************* + * Empty Pool + ********************************************************************************************* */ + +// Destroy all object of a pool by a given class +void UPoolManagerSubsystem::EmptyPool_Implementation(const UClass* ObjectClass) +{ + FPoolContainer* Pool = FindPool(ObjectClass); + if (!ensureMsgf(Pool, TEXT("%s: 'Pool' is not valid"), *FString(__FUNCTION__))) + { + return; + } + + UPoolFactory_UObject& Factory = Pool->GetFactoryChecked(); + TArray& PoolObjects = Pool->PoolObjects; + for (int32 Index = PoolObjects.Num() - 1; Index >= 0; --Index) + { + UObject* ObjectIt = PoolObjects.IsValidIndex(Index) ? PoolObjects[Index].Get() : nullptr; + if (IsValid(ObjectIt)) { - ObjectIt->ConditionalBeginDestroy(); + Factory.Destroy(ObjectIt); } } @@ -221,8 +338,8 @@ void UPoolManagerSubsystem::EmptyAllPools_Implementation() const int32 PoolsNum = PoolsInternal.Num(); for (int32 Index = PoolsNum - 1; Index >= 0; --Index) { - const UClass* ClassInPool = PoolsInternal.IsValidIndex(Index) ? PoolsInternal[Index].ClassInPool : nullptr; - EmptyPool(ClassInPool); + const UClass* ObjectClass = PoolsInternal.IsValidIndex(Index) ? PoolsInternal[Index].ObjectClass : nullptr; + EmptyPool(ObjectClass); } PoolsInternal.Empty(); @@ -239,7 +356,10 @@ void UPoolManagerSubsystem::EmptyAllByPredicate(const TFunctionRef& PoolObjectsRef = PoolsInternal[PoolIndex].PoolObjects; + FPoolContainer& PoolIt = PoolsInternal[PoolIndex]; + UPoolFactory_UObject& Factory = PoolIt.GetFactoryChecked(); + TArray& PoolObjectsRef = PoolIt.PoolObjects; + const int32 ObjectsNum = PoolObjectsRef.Num(); for (int32 ObjectIndex = ObjectsNum - 1; ObjectIndex >= 0; --ObjectIndex) { @@ -250,62 +370,22 @@ void UPoolManagerSubsystem::EmptyAllByPredicate(const TFunctionRef(ObjectIt)) - { - Actor->Destroy(); - } - else - { - ObjectIt->ConditionalBeginDestroy(); - } + Factory.Destroy(ObjectIt); PoolObjectsRef.RemoveAt(ObjectIndex); } } } -// Activates or deactivates the object if such object is handled by the Pool Manager -void UPoolManagerSubsystem::SetActive(bool bShouldActivate, UObject* Object) -{ - const UWorld* World = Object ? Object->GetWorld() : nullptr; - if (!World) - { - return; - } - - const UClass* ClassInPool = Object ? Object->GetClass() : nullptr; - FPoolContainer* Pool = FindPool(ClassInPool); - FPoolObjectData* PoolObject = Pool ? Pool->FindInPool(Object) : nullptr; - if (!PoolObject - || !PoolObject->IsValid()) - { - return; - } - - PoolObject->bIsActive = bShouldActivate; - - AActor* Actor = PoolObject->Get(); - if (!Actor) - { - return; - } - - if (!bShouldActivate) - { - // SetCollisionEnabled is not replicated, client collides with hidden actor, so move it - Actor->SetActorLocation(VECTOR_HALF_WORLD_MAX); - } - - Actor->SetActorHiddenInGame(!bShouldActivate); - Actor->SetActorEnableCollision(bShouldActivate); - Actor->SetActorTickEnabled(bShouldActivate); -} +/********************************************************************************************* + * Getters + ********************************************************************************************* */ // Returns current state of specified object EPoolObjectState UPoolManagerSubsystem::GetPoolObjectState_Implementation(const UObject* Object) const { - const UClass* ClassInPool = Object ? Object->GetClass() : nullptr; - const FPoolContainer* Pool = FindPool(ClassInPool); + const UClass* ObjectClass = Object ? Object->GetClass() : nullptr; + const FPoolContainer* Pool = FindPool(ObjectClass); const FPoolObjectData* PoolObject = Pool ? Pool->FindInPool(Object) : nullptr; if (!PoolObject @@ -324,9 +404,9 @@ bool UPoolManagerSubsystem::ContainsObjectInPool_Implementation(const UObject* O return GetPoolObjectState(Object) != EPoolObjectState::None; } -bool UPoolManagerSubsystem::ContainsClassInPool_Implementation(const UClass* ClassInPool) const +bool UPoolManagerSubsystem::ContainsClassInPool_Implementation(const UClass* ObjectClass) const { - return FindPool(ClassInPool) != nullptr; + return FindPool(ObjectClass) != nullptr; } // Returns true if specified object is handled by the Pool Manager and was taken from its pool @@ -341,39 +421,23 @@ bool UPoolManagerSubsystem::IsFreeObjectInPool_Implementation(const UObject* Obj return GetPoolObjectState(Object) == EPoolObjectState::Inactive; } -// Returns first object contained in the Pool by its class that is inactive and ready to be taken from pool -UObject* UPoolManagerSubsystem::GetFreeObjectInPool_Implementation(const UClass* ObjectClass) const -{ - const FPoolContainer* Pool = FindPool(ObjectClass); - if (!Pool) - { - return nullptr; - } - - // Try to find ready object to return - for (const FPoolObjectData& PoolObjectIt : Pool->PoolObjects) - { - if (PoolObjectIt.IsFree()) - { - return PoolObjectIt.Get(); - } - } - - // There is no free object to be taken - return nullptr; -} - // Returns true if object is known by Pool Manager bool UPoolManagerSubsystem::IsRegistered_Implementation(const UObject* Object) const { return GetPoolObjectState(Object) != EPoolObjectState::None; } +/********************************************************************************************* + * Protected methods + ********************************************************************************************* */ + // Is called on initialization of the Pool Manager instance void UPoolManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); + InitializeAllFactories(); + #if WITH_EDITOR if (GEditor && !GEditor->IsPlaySessionInProgress() // Is Editor and not in PIE @@ -402,16 +466,52 @@ void UPoolManagerSubsystem::Initialize(FSubsystemCollectionBase& Collection) #endif // WITH_EDITOR } +// Is called on deinitialization of the Pool Manager instance +void UPoolManagerSubsystem::Deinitialize() +{ + Super::Deinitialize(); + + ClearAllFactories(); +} + +// Returns the pointer to found pool by specified class +FPoolContainer& UPoolManagerSubsystem::FindPoolOrAdd(const UClass* ObjectClass) +{ + if (FPoolContainer* Pool = FindPool(ObjectClass)) + { + return *Pool; + } + + FPoolContainer& Pool = PoolsInternal.AddDefaulted_GetRef(); + Pool.ObjectClass = ObjectClass; + Pool.Factory = FindPoolFactoryChecked(ObjectClass); + return Pool; +} + // Returns the pointer to found pool by specified class -FPoolContainer* UPoolManagerSubsystem::FindPool(const UClass* ClassInPool) +FPoolContainer* UPoolManagerSubsystem::FindPool(const UClass* ObjectClass) { - if (!ClassInPool) + if (!ObjectClass) { return nullptr; } - return PoolsInternal.FindByPredicate([ClassInPool](const FPoolContainer& It) + return PoolsInternal.FindByPredicate([ObjectClass](const FPoolContainer& It) { - return It.ClassInPool == ClassInPool; + return It.ObjectClass == ObjectClass; }); } + +// Activates or deactivates the object if such object is handled by the Pool Manager +void UPoolManagerSubsystem::SetObjectStateInPool_Implementation(EPoolObjectState NewState, UObject* InObject, FPoolContainer& InPool) +{ + FPoolObjectData* PoolObject = InPool.FindInPool(InObject); + if (!ensureMsgf(PoolObject && PoolObject->IsValid(), TEXT("ASSERT: [%i] %s:\n'PoolObject' is not registered in given pool for class: %s"), __LINE__, *FString(__FUNCTION__), *GetNameSafe(InPool.ObjectClass))) + { + return; + } + + PoolObject->bIsActive = NewState == EPoolObjectState::Active; + + InPool.GetFactoryChecked().OnChangedStateInPool(NewState, InObject); +} diff --git a/Source/PoolManager/Private/PoolManagerTypes.cpp b/Source/PoolManager/Private/PoolManagerTypes.cpp index 3fbb79a..770dd23 100644 --- a/Source/PoolManager/Private/PoolManagerTypes.cpp +++ b/Source/PoolManager/Private/PoolManagerTypes.cpp @@ -19,7 +19,7 @@ FPoolObjectData::FPoolObjectData(UObject* InPoolObject) // Parameterized constructor that takes a class of the pool FPoolContainer::FPoolContainer(const UClass* InClass) { - ClassInPool = InClass; + ObjectClass = InClass; } // Returns the pointer to the Pool element by specified object @@ -35,3 +35,10 @@ FPoolObjectData* FPoolContainer::FindInPool(const UObject* Object) return It.PoolObject == Object; }); } + +// Returns factory or crashes as critical error if it is not set +UPoolFactory_UObject& FPoolContainer::GetFactoryChecked() const +{ + checkf(Factory, TEXT("ERROR: [%i] %s:\n'Factory' is null!"), __LINE__, *FString(__FUNCTION__)); + return *Factory; +} diff --git a/Source/PoolManager/Public/Data/PoolManagerSettings.h b/Source/PoolManager/Public/Data/PoolManagerSettings.h new file mode 100644 index 0000000..bd4b388 --- /dev/null +++ b/Source/PoolManager/Public/Data/PoolManagerSettings.h @@ -0,0 +1,40 @@ +// Copyright (c) Yevhenii Selivanov + +#pragma once + +#include "Engine/DeveloperSettings.h" +//--- +#include "PoolManagerSettings.generated.h" + +/** + * Contains common settings data of the Pool Manager plugin. + * Is set up in 'Project Settings' -> "Plugins" -> "Pool Manager". + */ +UCLASS(Config = "PoolManager", DefaultConfig, meta = (DisplayName = "Pool Manager")) +class POOLMANAGER_API UPoolManagerSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + /** Returns Project Settings data of the Pool Manager plugin. */ + static const FORCEINLINE UPoolManagerSettings& Get() { return *GetDefault(); } + + /** Returns Project Settings data of the Pool Manager plugin. */ + UFUNCTION(BlueprintPure, Category = "Pool Manager") + static const FORCEINLINE UPoolManagerSettings* GetPoolManagerSettings() { return &Get(); } + + /** Gets the settings container name for the settings, either Project or Editor */ + virtual FName GetContainerName() const override { return TEXT("Project"); } + + /** Gets the category for the settings, some high level grouping like, Editor, Engine, Game...etc. */ + virtual FName GetCategoryName() const override { return TEXT("Plugins"); } + + /** Returns a limit of how many actors to spawn per frame. */ + UFUNCTION(BlueprintPure, Category = "Pool Manager") + int32 GetSpawnObjectsPerFrame() const { return SpawnObjectsPerFrame; } + +protected: + /** Set a limit of how many actors to spawn per frame. */ + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category = "Pool Manager", meta = (BlueprintProtected = "true")) + int32 SpawnObjectsPerFrame; +}; diff --git a/Source/PoolManager/Public/Factories/PoolFactory_Actor.h b/Source/PoolManager/Public/Factories/PoolFactory_Actor.h new file mode 100644 index 0000000..400b00f --- /dev/null +++ b/Source/PoolManager/Public/Factories/PoolFactory_Actor.h @@ -0,0 +1,50 @@ +// Copyright (c) Yevhenii Selivanov + +#pragma once + +#include "PoolFactory_UObject.h" +//--- +#include "PoolFactory_Actor.generated.h" + +/** + * Is responsible for managing actors, it handles such differences in actors as: + * Creation: call SpawnActor. + * Destruction: call DestroyActor. + * Pool: change visibility, collision, ticking, etc. + */ +UCLASS() +class POOLMANAGER_API UPoolFactory_Actor : public UPoolFactory_UObject +{ + GENERATED_BODY() + +public: + /** Is overridden to handle Actors-inherited classes. */ + virtual const UClass* GetObjectClass_Implementation() const override; + + /********************************************************************************************* + * Creation + ********************************************************************************************* */ +public: + /** Is overridden to spawn actors using its engine's Spawn Actor method. */ + virtual UObject* SpawnNow_Implementation(const FSpawnRequest& Request) override; + + /********************************************************************************************* + * Destruction + ********************************************************************************************* */ +public: + /** Is overridden to destroy given actor using its engine's Destroy Actor method. */ + virtual void Destroy_Implementation(UObject* Object) override; + + /********************************************************************************************* + * Pool + ********************************************************************************************* */ +public: + /** Is overridden to set transform to the actor before taking the object from its pool. */ + virtual void OnTakeFromPool_Implementation(UObject* Object, const FTransform& Transform) override; + + /** Is overridden to reset transform to the actor before returning the object to its pool. */ + virtual void OnReturnToPool_Implementation(UObject* Object) override; + + /** Is overridden to change visibility, collision, ticking, etc. according new state. */ + virtual void OnChangedStateInPool_Implementation(EPoolObjectState NewState, UObject* InObject) override; +}; diff --git a/Source/PoolManager/Public/Factories/PoolFactory_UObject.h b/Source/PoolManager/Public/Factories/PoolFactory_UObject.h new file mode 100644 index 0000000..e9ec032 --- /dev/null +++ b/Source/PoolManager/Public/Factories/PoolFactory_UObject.h @@ -0,0 +1,89 @@ +// Copyright (c) Yevhenii Selivanov + +#pragma once + +#include "UObject/Object.h" +//--- +#include "PoolManagerTypes.h" +//--- +#include "PoolFactory_UObject.generated.h" + +/** + * Each factory implements specific logic of creating and managing objects of its class and its children. + * Factories are designed to handle such differences as: + * Creation: UObjects call NewObject; Actors call SpawnActor, Components call NewObject+RegisterComponent, Widgets call CreateWidget etc. + * Destruction: UObjects call ConditionalBeginDestroy; Actors call DestroyActor, Components call DestroyComponent, Widgets call RemoveFromParent etc. + * Pool: Actors and Scene Components are changing visibility, collision, ticking, etc. UObjects and Widgets are not. + * + * To create new factory just inherit from this/child class and override GetObjectClass() method. + * Pool Manager will automatically create and use your factory for objects of your class and its children. +*/ +UCLASS(Blueprintable, BlueprintType) +class POOLMANAGER_API UPoolFactory_UObject : public UObject +{ + GENERATED_BODY() + +public: + /** Returns the class of object that this factory will create and manage. + * Has to be overridden by child classes if it wants to handle logic for specific class and its children. + * E.g: UObject for UPoolFactory_UObject, AActor for UPoolFactory_Actor, UWidget for UPoolFactory_Widget etc. */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Factory") + const UClass* GetObjectClass() const; + virtual FORCEINLINE const UClass* GetObjectClass_Implementation() const { return UObject::StaticClass(); } + + /********************************************************************************************* + * Creation + ********************************************************************************************* */ +public: + /** Method to queue object spawn requests. */ + UFUNCTION(BlueprintNativeEvent, Blueprintable, Category = "Pool Factory", meta = (AutoCreateRefTerm = "Request")) + void RequestSpawn(const FSpawnRequest& Request); + virtual void RequestSpawn_Implementation(const FSpawnRequest& Request); + + /** Method to immediately spawn requested object. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory", meta = (AutoCreateRefTerm = "Request")) + UObject* SpawnNow(const FSpawnRequest& Request); + virtual UObject* SpawnNow_Implementation(const FSpawnRequest& Request); + +protected: + /** Is called on next frame to process a chunk of the spawn queue. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory", meta = (BlueprintProtected)) + void OnNextTickProcessSpawn(); + virtual void OnNextTickProcessSpawn_Implementation(); + + /********************************************************************************************* + * Destruction + ********************************************************************************************* */ +public: + /** Method to destroy given object. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory") + void Destroy(UObject* Object); + virtual void Destroy_Implementation(UObject* Object); + + /********************************************************************************************* + * Pool + ********************************************************************************************* */ +public: + /** Is called right before taking the object from its pool. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory", meta = (AutoCreateRefTerm = "Transform")) + void OnTakeFromPool(UObject* Object, const FTransform& Transform); + virtual void OnTakeFromPool_Implementation(UObject* Object, const FTransform& Transform) {} + + /** Is called right before returning the object back to its pool. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory") + void OnReturnToPool(UObject* Object); + virtual void OnReturnToPool_Implementation(UObject* Object) {} + + /** Is called when activates the object to take it from pool or deactivate when is returned back. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Factory") + void OnChangedStateInPool(EPoolObjectState NewState, UObject* InObject); + virtual void OnChangedStateInPool_Implementation(EPoolObjectState NewState, UObject* InObject) {} + + /********************************************************************************************* + * Data + ********************************************************************************************* */ +protected: + /** Request to spawn. */ + TQueue SpawnQueue; + int32 SpawnQueueSize = 0; +}; diff --git a/Source/PoolManager/Public/PoolManagerSubsystem.h b/Source/PoolManager/Public/PoolManagerSubsystem.h index 9f781bb..d0e94c7 100644 --- a/Source/PoolManager/Public/PoolManagerSubsystem.h +++ b/Source/PoolManager/Public/PoolManagerSubsystem.h @@ -23,8 +23,14 @@ * Objects are taken from and returned to the Pool Manager when not in use, which makes them 'inactive'. * In case of actors, they are moved outside the game level, hidden, and they don't interact with anything or use up resources. * - * Tips: - * - You can use the Pool Manager in the Editor before you start the game. + * Code architecture: + * - Pool Manager is a subsystem that is created automatically including all children. + * - It stores and manages only the data (Pools and objects). + * - It does not manage any specific logic for handling objects, only base pooling logic related to data. + * - Pool Factories are used to handle specific logic about objects behavior (creation, destruction, visibility etc). + * + * Optional features: + * - Pool Manager is available even before starting the game that allows to preconstruct your actors on the level. * - You can implement your own Pool Manager by inheriting from this class and overriding the functions. */ UCLASS(BlueprintType, Blueprintable) @@ -37,42 +43,66 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem ********************************************************************************************* */ public: /** Returns the Pool Manager, is checked and wil crash if can't be obtained. - * UPoolManagerSubsystem::Get(). with no parameters can be used in most cases if there is no specific set up. - * @tparam T is optional, put your child class if you implemented your own Pull Manager in code. - * @param OptionalClass is optional, specify the class if you implemented your own Pool Manager in blueprints. - * @param OptionalWorldContext is optional, can be null in most cases, could be useful to avoid obtaining the automatically. */ + * Is useful in code, in most cases can be used with no parameters to obtain default Pool Manager. + * E.g: + * - UPoolManagerSubsystem::Get().TakeFromPool... + * - UPoolManagerSubsystem::Get().TakeFromPool... + * - UPoolManagerSubsystem::Get(CustomBlueprintPoolManager).TakeFromPool... + * @tparam T is optional, put your child class if you implemented your own Pull Manager in code. + * @param OptionalClass is optional, specify the class if you implemented your own Pool Manager in blueprints. + * @param OptionalWorldContext is optional, can be null in most cases, could be useful to avoid obtaining the automatically. */ template static FORCEINLINE T& Get(TSubclassOf OptionalClass = T::StaticClass(), const UObject* OptionalWorldContext = nullptr) { return *CastChecked(GetPoolManagerByClass(OptionalClass, OptionalWorldContext)); } /** Returns the pointer to the Pool Manager. + * Is useful for blueprints to obtain **default** Pool Manager. * @param OptionalWorldContext is optional parameter and hidden in blueprints, can be null in most cases, could be useful to avoid obtaining the world automatically. */ UFUNCTION(BlueprintPure, meta = (WorldContext = "OptionalWorldContext")) static UPoolManagerSubsystem* GetPoolManager(const UObject* OptionalWorldContext = nullptr) { return GetPoolManagerByClass(StaticClass(), OptionalWorldContext); } /** Returns the pointer to custom Pool Manager by given class. - * @param OptionalClass is optional, specify the class if you implemented your own Pool Manager. - * @param OptionalWorldContext is optional parameter and hidden in blueprints, can be null in most cases, could be useful to avoid obtaining the world automatically. */ + * Is useful for blueprints to obtain your **custom** Pool Manager. + * @param OptionalClass is optional, specify the class if you implemented your own Pool Manager. + * @param OptionalWorldContext is optional parameter and hidden in blueprints, can be null in most cases, could be useful to avoid obtaining the world automatically. */ UFUNCTION(BlueprintPure, meta = (WorldContext = "OptionalWorldContext", DeterminesOutputType = "OptionalClass", BlueprintAutocast)) static UPoolManagerSubsystem* GetPoolManagerByClass(TSubclassOf OptionalClass = nullptr, const UObject* OptionalWorldContext = nullptr); /********************************************************************************************* - * Main + * Main API * * Use TakeFromPool() to get it instead of creating by your own. * Use ReturnToPool() to return it back to the pool instead of destroying by your own. ********************************************************************************************* */ public: - /** Get the object from a pool by specified class. - * It creates new object if there no free objects contained in pool or does not exist any. - * @return Activated object requested from the pool. */ - UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (AutoCreateRefTerm = "Transform")) - UObject* TakeFromPool(const UClass* ClassInPool, const FTransform& Transform); - virtual UObject* TakeFromPool_Implementation(const UClass* ClassInPool, const FTransform& Transform); + DECLARE_DYNAMIC_DELEGATE_OneParam(FOnTakenFromPool, UObject*, Object); - /** The templated alternative to get the object from a pool by specified class. */ + /** Get the object from a pool by specified class, where output is async that returns the object when is ready. + * It creates new object if there no free objects contained in pool or does not exist any. + * @param ObjectClass The class of object to get from the pool. + * @param Transform The transform to set for the object (if actor). + * @param Completed The callback function that is called when the object is ready. + * @return if any is found and free, activates and returns object from the pool, otherwise async spawns new one next frames and register in the pool. + * @warning 'SpawnObjectsPerFrame' affects how fast new objects are created, it can be changed in'Project Settings' -> "Plugins" -> "Pool Manager". + * @warning Is custom blueprint node implemented in K2Node_TakeFromPool.h, so can't be overridden and accessible on graph (not inside functions). + * @warning Node's output will node work in for/while loop in blueprints because of Completed delegate: to achieve loop connect output exec to input exec. */ + UFUNCTION(BlueprintCallable, Category = "Pool Manager", meta = (BlueprintInternalUseOnly = "true")) + void BPTakeFromPool(const UClass* ObjectClass, const FTransform& Transform, const FOnTakenFromPool& Completed); + + /** Is code-overridable version of TakeFromPool() that calls callback functions when the object is ready. + * Can be overridden by child code classes. + * Is useful in code with blueprint classes, e.g: TakeFromPool(SomeBlueprintClass); */ + virtual void TakeFromPool(const UClass* ObjectClass, const FTransform& Transform = FTransform::Identity, const TFunction& Completed = nullptr); + + /** A templated alternative to get the object from a pool by class in template. + * Is useful in code with code classes, e.g: TakeFromPool(); */ template - FORCEINLINE T* TakeFromPool(const UClass* ClassInPool = T::StaticClass(), const FTransform& Transform = FTransform::Identity) { return Cast(TakeFromPool(ClassInPool, Transform)); } + void TakeFromPool(const FTransform& Transform = FTransform::Identity, const TFunction& Completed = nullptr) { return TakeFromPool(T::StaticClass(), Transform, Completed); } + + /** Is blueprint-overridable version of TakeFromPool() to find object in pool or return null. */ + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (AutoCreateRefTerm = "Transform")) + UObject* TakeFromPoolOrNull(const UClass* ObjectClass, const FTransform& Transform); +public: /** Returns the specified object to the pool and deactivates it if the object was taken from the pool before. */ UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) void ReturnToPool(UObject* Object); @@ -85,21 +115,60 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem ********************************************************************************************* */ public: /** Adds specified object as is to the pool by its class to be handled by the Pool Manager. - * It's designed to be used only on already existed objects unknown for the Pool Manager. */ + * Should not be used directly in most cases since is called automatically. + * Could be useful to add already existed objects (spawned by outer code) to the pool. + * It's designed to be used only on already existed objects unknown for the Pool Manager. + * @param Object The object to register in the pool. + * @param PoolObjectState The state of the object to register in the pool. */ UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) bool RegisterObjectInPool(UObject* Object = nullptr, EPoolObjectState PoolObjectState = EPoolObjectState::Inactive); virtual bool RegisterObjectInPool_Implementation(UObject* Object = nullptr, EPoolObjectState PoolObjectState = EPoolObjectState::Inactive); /** Always creates new object and adds it to the pool by its class. * Use carefully if only there is no free objects contained in pool. */ - UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) - UObject* CreateNewObjectInPool(const UClass* ObjectClass, const FTransform& Transform, EPoolObjectState PoolObjectState = EPoolObjectState::Active); - virtual UObject* CreateNewObjectInPool_Implementation(const UClass* ObjectClass, const FTransform& Transform, EPoolObjectState PoolObjectState = EPoolObjectState::Active); + UFUNCTION(BlueprintNativeEvent, BlueprintCallable, Category = "Pool Manager", meta = (DisplayName = "Create New Object In Pool", DefaultToSelf = "Object")) + void CreateNewObjectInPool(UPARAM(ref) FSpawnRequest& InRequest); + virtual void CreateNewObjectInPool_Implementation(UPARAM(ref) FSpawnRequest& InRequest); + + /********************************************************************************************* + * Advanced - Factories + ********************************************************************************************* */ +public: + /** Registers new factory to be used by the Pool Manager when dealing with objects of specific class and its children. + * In most cases, you don't need to use this function since factories are registered automatically. + * Could be useful to register factory from different plugin or module. */ + UFUNCTION(BlueprintCallable, Category = "Pool Manager") + virtual void AddFactory(TSubclassOf FactoryClass); + + /** Removes factory from the Pool Manager by its class. + * In most cases, you don't need to use this function since factories are removed with destroying the Pool Manager. + * Could be useful to remove factory in runtime when some class is not needed anymore. */ + UFUNCTION(BlueprintCallable, Category = "Pool Manager") + virtual void RemoveFactory(TSubclassOf FactoryClass); + + /** Traverses the class hierarchy to find the closest registered factory for a given object type or its ancestors. */ + UFUNCTION(BlueprintPure, Category = "Pool Manager") + UPoolFactory_UObject* FindPoolFactoryChecked(const UClass* ObjectClass) const; + + /** Returns default class of object that is handled by given factory. */ + UFUNCTION(BlueprintPure, Category = "Pool Manager") + static const UClass* GetObjectClassByFactory(const TSubclassOf& FactoryClass); +protected: + /** Creates all possible Pool Factories to be used by the Pool Manager when dealing with objects. */ + virtual void InitializeAllFactories(); + + /** Destroys all Pool Factories that are used by the Pool Manager when dealing with objects. */ + virtual void ClearAllFactories(); + + /********************************************************************************************* + * Empty Pool + ********************************************************************************************* */ +public: /** Destroy all object of a pool by a given class. */ UFUNCTION(BlueprintNativeEvent, BlueprintCallable) - void EmptyPool(const UClass* ClassInPool); - virtual void EmptyPool_Implementation(const UClass* ClassInPool); + void EmptyPool(const UClass* ObjectClass); + virtual void EmptyPool_Implementation(const UClass* ObjectClass); /** Destroy all objects in all pools that are handled by the Pool Manager. */ UFUNCTION(BlueprintNativeEvent, BlueprintCallable) @@ -124,9 +193,9 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem virtual bool ContainsObjectInPool_Implementation(const UObject* Object) const; /** Returns true is specified class is handled by Pool Manager. */ - UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) - bool ContainsClassInPool(const UClass* ClassInPool) const; - virtual bool ContainsClassInPool_Implementation(const UClass* ClassInPool) const; + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager") + bool ContainsClassInPool(const UClass* ObjectClass) const; + virtual bool ContainsClassInPool_Implementation(const UClass* ObjectClass) const; /** Returns true if specified object is handled by the Pool Manager and was taken from its pool. */ UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) @@ -138,13 +207,6 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem bool IsFreeObjectInPool(const UObject* Object) const; virtual bool IsFreeObjectInPool_Implementation(const UObject* Object) const; - /** Returns first object contained in the Pool by its class that is inactive and ready to be taken from pool. - * Return null if there no free objects contained in pool or does not exist any. - * Consider to use TakeFromPool() instead to create new object if there no free objects. */ - UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) - UObject* GetFreeObjectInPool(const UClass* ObjectClass) const; - virtual UObject* GetFreeObjectInPool_Implementation(const UClass* ObjectClass) const; - /** Returns true if object is known by Pool Manager. */ UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) bool IsRegistered(const UObject* Object) const; @@ -158,6 +220,11 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem UPROPERTY(BlueprintReadWrite, Transient, Category = "Pool Manager", meta = (BlueprintProtected, DisplayName = "Pools")) TArray PoolsInternal; + /** Map to store registered factories against the class types they handle. + * @see UPoolFactory_UObject's description. */ + UPROPERTY(BlueprintReadWrite, Transient, Category = "Pool Manager", meta = (BlueprintProtected, DisplayName = "All Factories")) + TMap, TObjectPtr> AllFactoriesInternal; + /********************************************************************************************* * Protected methods ********************************************************************************************* */ @@ -165,11 +232,20 @@ class POOLMANAGER_API UPoolManagerSubsystem : public UWorldSubsystem /** Is called on initialization of the Pool Manager instance. */ virtual void Initialize(FSubsystemCollectionBase& Collection) override; - /** Returns the pointer to found pool by specified class. */ - virtual FPoolContainer* FindPool(const UClass* ClassInPool); - const FORCEINLINE FPoolContainer* FindPool(const UClass* ClassInPool) const { return const_cast(this)->FindPool(ClassInPool); } + /** Is called on deinitialization of the Pool Manager instance. */ + virtual void Deinitialize() override; - /** Activates or deactivates the object if such object is handled by the Pool Manager. */ - UFUNCTION(BlueprintCallable, Category = "Pool Manager", meta = (BlueprintProtected, DefaultToSelf = "Object")) - virtual void SetActive(bool bShouldActivate, UObject* Object); + /** Returns the pointer to found pool by specified class. */ + virtual FPoolContainer& FindPoolOrAdd(const UClass* ObjectClass); + virtual FPoolContainer* FindPool(const UClass* ObjectClass); + const FORCEINLINE FPoolContainer* FindPool(const UClass* ObjectClass) const { return const_cast(this)->FindPool(ObjectClass); } + + /** Activates or deactivates the object if such object is handled by the Pool Manager. + * Is called when the object is taken from, registered or returned to the pool. + * @param NewState If true, the object will be activated, otherwise deactivated. + * @param InObject The object to activate or deactivate. + * @param InPool The pool that contains the object. */ + UFUNCTION(BlueprintNativeEvent, BlueprintPure, Category = "Pool Manager", meta = (DefaultToSelf = "Object")) + void SetObjectStateInPool(EPoolObjectState NewState, UObject* InObject, UPARAM(ref) FPoolContainer& InPool); + virtual void SetObjectStateInPool_Implementation(EPoolObjectState NewState, UObject* InObject, FPoolContainer& InPool); }; diff --git a/Source/PoolManager/Public/PoolManagerTypes.h b/Source/PoolManager/Public/PoolManagerTypes.h index ce96a45..96bd9a9 100644 --- a/Source/PoolManager/Public/PoolManagerTypes.h +++ b/Source/PoolManager/Public/PoolManagerTypes.h @@ -61,6 +61,10 @@ struct POOLMANAGER_API FPoolObjectData template FORCEINLINE T* Get() const { return Cast(PoolObject.Get()); } + /** Element access, crash if object is not valid. */ + template + FORCEINLINE T& GetChecked() const { return *CastChecked(PoolObject.Get()); } + /** Element access. */ FORCEINLINE UObject* operator->() const { return PoolObject.Get(); } }; @@ -84,7 +88,11 @@ struct POOLMANAGER_API FPoolContainer /** Class of all objects in this pool. */ UPROPERTY(EditAnywhere, BlueprintReadWrite) - TObjectPtr ClassInPool = nullptr; + TObjectPtr ObjectClass = nullptr; + + /** Factory that manages objects for this pool. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TObjectPtr Factory = nullptr; /** All objects in this pool that are handled by the Pool Manager. */ UPROPERTY(EditAnywhere, BlueprintReadWrite) @@ -94,6 +102,41 @@ struct POOLMANAGER_API FPoolContainer FPoolObjectData* FindInPool(const UObject* Object); const FORCEINLINE FPoolObjectData* FindInPool(const UObject* Object) const { return const_cast(this)->FindInPool(Object); } + /** Returns factory or crashes as critical error if it is not set. */ + UPoolFactory_UObject& GetFactoryChecked() const; + /** Returns true if the class is set for the Pool. */ - FORCEINLINE bool IsValid() const { return ClassInPool != nullptr; } + FORCEINLINE bool IsValid() const { return ObjectClass != nullptr; } +}; + +/** + * Contains the functions that are called when the object is spawned. + */ +struct POOLMANAGER_API FSpawnCallbacks +{ + /** Returns spawned object if it finished spawning successfully. */ + TFunction OnPreConstructed = nullptr; + + /** Returns spawned object when it finished spawning successfully. */ + TFunction OnPostSpawned = nullptr; +}; + +/** + * Define a structure to hold the necessary information for spawning an object. + */ +USTRUCT(BlueprintType) +struct POOLMANAGER_API FSpawnRequest +{ + GENERATED_BODY() + + /** Class of the object to spawn. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TObjectPtr Class = nullptr; + + /** Transform of the object to spawn. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + FTransform Transform = FTransform::Identity; + + // Contains the functions that are called when the object is spawned + FSpawnCallbacks Callbacks; }; diff --git a/Source/PoolManagerEditor/PoolManagerEditor.Build.cs b/Source/PoolManagerEditor/PoolManagerEditor.Build.cs new file mode 100644 index 0000000..3788ca1 --- /dev/null +++ b/Source/PoolManagerEditor/PoolManagerEditor.Build.cs @@ -0,0 +1,30 @@ +// Copyright (c) Yevhenii Selivanov. + +using UnrealBuildTool; + +public class PoolManagerEditor : ModuleRules +{ + public PoolManagerEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + CppStandard = CppStandardVersion.Latest; + bEnableNonInlinedGenCppWarnings = true; + + PublicDependencyModuleNames.AddRange(new[] + { + "Core" + , "BlueprintGraph" // Created UK2Node_TakeFromPool + } + ); + + PrivateDependencyModuleNames.AddRange(new[] + { + "CoreUObject", "Engine", "Slate", "SlateCore" // Core + , "UnrealEd" + , "KismetCompiler" + // My modules + , "PoolManager" + } + ); + } +} \ No newline at end of file diff --git a/Source/PoolManagerEditor/Private/K2Node_TakeFromPool.cpp b/Source/PoolManagerEditor/Private/K2Node_TakeFromPool.cpp new file mode 100644 index 0000000..585043e --- /dev/null +++ b/Source/PoolManagerEditor/Private/K2Node_TakeFromPool.cpp @@ -0,0 +1,308 @@ +// Copyright (c) Yevhenii Selivanov + +#include "K2Node_TakeFromPool.h" +//--- +#include "PoolManagerSubsystem.h" +//--- +#include "BlueprintActionDatabaseRegistrar.h" +#include "BlueprintNodeSpawner.h" +#include "K2Node_AssignmentStatement.h" +#include "K2Node_CallFunction.h" +#include "K2Node_CustomEvent.h" +#include "K2Node_ExecutionSequence.h" +#include "K2Node_TemporaryVariable.h" +#include "KismetCompiler.h" +//--- +#include UE_INLINE_GENERATED_CPP_BY_NAME(K2Node_TakeFromPool) + +#define LOCTEXT_NAMESPACE "K2Node_TakeFromPool" + +void UK2Node_TakeFromPool::AllocateDefaultPins() +{ + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Execute); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Exec, UEdGraphSchema_K2::PN_Completed); + + UEdGraphPin* TargetPin = CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Object, UPoolManagerSubsystem::StaticClass(), UEdGraphSchema_K2::PN_Self); + checkf(TargetPin, TEXT("ERROR: [%i] %s:\n'TargetPin' is null!"), __LINE__, *FString(__FUNCTION__)); + TargetPin->PinFriendlyName = LOCTEXT("Target", "Target"); + TargetPin->bDefaultValueIsIgnored = true; + + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Class, UObject::StaticClass(), ClassInputName); + CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_Struct, TBaseStructure::Get(), TransformInputName); + CreatePin(EGPD_Output, UEdGraphSchema_K2::PC_Object, UObject::StaticClass(), ObjectOutputName); +} + +void UK2Node_TakeFromPool::ReallocatePinsDuringReconstruction(TArray& OldPins) +{ + Super::ReallocatePinsDuringReconstruction(OldPins); + + UEdGraphPin* OldThenPin = nullptr; + const UEdGraphPin* OldCompletedPin = nullptr; + + for (UEdGraphPin* CurrentPin : OldPins) + { + if (CurrentPin->PinName == UEdGraphSchema_K2::PN_Then) + { + OldThenPin = CurrentPin; + } + else if (CurrentPin->PinName == UEdGraphSchema_K2::PN_Completed) + { + OldCompletedPin = CurrentPin; + } + } + + if (OldThenPin && !OldCompletedPin) + { + // This is an old node from when Completed was called then, rename the node to Completed and allow normal rewire to take place + OldThenPin->PinName = UEdGraphSchema_K2::PN_Completed; + } +} + +void UK2Node_TakeFromPool::ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) +{ + Super::ExpandNode(CompilerContext, SourceGraph); + const UEdGraphSchema_K2* Schema = CompilerContext.GetSchema(); + check(Schema); + bool bIsErrorFree = true; + + // Sequence node, defaults to two output pins + UK2Node_ExecutionSequence* SequenceNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + SequenceNode->AllocateDefaultPins(); + + // connect to input exe + { + UEdGraphPin* InputExePin = GetExecPin(); + UEdGraphPin* SequenceInputExePin = SequenceNode->GetExecPin(); + bIsErrorFree &= InputExePin && SequenceInputExePin && CompilerContext.MovePinLinksToIntermediate(*InputExePin, *SequenceInputExePin).CanSafeConnect(); + } + + // Create TakeFromPool function call + UK2Node_CallFunction* CallTakeFromPoolNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + CallTakeFromPoolNode->FunctionReference.SetExternalMember(NativeFunctionName(), UPoolManagerSubsystem::StaticClass()); + CallTakeFromPoolNode->AllocateDefaultPins(); + + // connect load to first sequence pin + { + UEdGraphPin* CallFunctionInputExePin = CallTakeFromPoolNode->GetExecPin(); + UEdGraphPin* SequenceFirstExePin = SequenceNode->GetThenPinGivenIndex(0); + bIsErrorFree &= SequenceFirstExePin && CallFunctionInputExePin && Schema->TryCreateConnection(CallFunctionInputExePin, SequenceFirstExePin); + } + + // Create Local Variable + UK2Node_TemporaryVariable* TempVarOutput = CompilerContext.SpawnInternalVariable(this, UEdGraphSchema_K2::PC_Object, NAME_None, UObject::StaticClass()); + + // Create assign node + UK2Node_AssignmentStatement* AssignNode = CompilerContext.SpawnIntermediateNode(this, SourceGraph); + AssignNode->AllocateDefaultPins(); + + UEdGraphPin* LoadedObjectVariablePin = TempVarOutput->GetVariablePin(); + + // connect local variable to assign node + { + UEdGraphPin* AssignLHSPPin = AssignNode->GetVariablePin(); + bIsErrorFree &= AssignLHSPPin && LoadedObjectVariablePin && Schema->TryCreateConnection(AssignLHSPPin, LoadedObjectVariablePin); + } + + // connect local variable to output + { + UEdGraphPin* OutputObjectPinPin = FindPin(ObjectOutputName); + bIsErrorFree &= LoadedObjectVariablePin && OutputObjectPinPin && CompilerContext.MovePinLinksToIntermediate(*OutputObjectPinPin, *LoadedObjectVariablePin).CanSafeConnect(); + } + + // connect to Pool Manager input from self + UEdGraphPin* CallPoolManagerPin = CallTakeFromPoolNode->FindPin(UEdGraphSchema_K2::PN_Self); + { + UEdGraphPin* PoolManagerPin = FindPin(UEdGraphSchema_K2::PN_Self); + ensure(CallPoolManagerPin); + + if (PoolManagerPin && CallPoolManagerPin) + { + if (PoolManagerPin->LinkedTo.Num() > 0) + { + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*PoolManagerPin, *CallPoolManagerPin).CanSafeConnect(); + } + else + { + // Copy literal value + CallPoolManagerPin->DefaultValue = PoolManagerPin->DefaultValue; + } + } + else + { + bIsErrorFree = false; + } + } + + // connect to class input + UEdGraphPin* CallClassPin = CallTakeFromPoolNode->FindPin(ClassInputName); + { + UEdGraphPin* ClassPin = FindPin(ClassInputName); + ensure(CallClassPin); + + if (ClassPin && CallClassPin) + { + if (ClassPin->LinkedTo.Num() > 0) + { + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*ClassPin, *CallClassPin).CanSafeConnect(); + } + else + { + // Copy literal value + CallClassPin->DefaultValue = ClassPin->DefaultValue; + } + } + else + { + bIsErrorFree = false; + } + } + + // connect to transform input + UEdGraphPin* CallTransformPin = CallTakeFromPoolNode->FindPin(TransformInputName); + { + UEdGraphPin* TransformPin = FindPin(TransformInputName); + ensure(CallTransformPin); + + if (TransformPin && CallTransformPin) + { + if (TransformPin->LinkedTo.Num() > 0) + { + bIsErrorFree &= CompilerContext.MovePinLinksToIntermediate(*TransformPin, *CallTransformPin).CanSafeConnect(); + } + else + { + // Copy literal value + CallTransformPin->DefaultValue = TransformPin->DefaultValue; + } + } + else + { + bIsErrorFree = false; + } + } + + // Create Completed delegate parameter + const FName DelegateCompletedParamName(TEXT("Completed")); + UK2Node_CustomEvent* CompletedEventNode = CompilerContext.SpawnIntermediateEventNode(this, CallClassPin, SourceGraph); + CompletedEventNode->CustomFunctionName = *FString::Printf(TEXT("Completed_%s"), *CompilerContext.GetGuid(this)); + CompletedEventNode->AllocateDefaultPins(); + { + UFunction* TakeFromPoolFunction = CallTakeFromPoolNode->GetTargetFunction(); + FDelegateProperty* CompletedDelegateProperty = TakeFromPoolFunction ? FindFProperty(TakeFromPoolFunction, DelegateCompletedParamName) : nullptr; + UFunction* CompletedSignature = CompletedDelegateProperty ? CompletedDelegateProperty->SignatureFunction : nullptr; + ensure(CompletedSignature); + for (TFieldIterator PropIt(CompletedSignature); PropIt && PropIt->PropertyFlags & CPF_Parm; ++PropIt) + { + const FProperty* Param = *PropIt; + if (!Param->HasAnyPropertyFlags(CPF_OutParm) || Param->HasAnyPropertyFlags(CPF_ReferenceParm)) + { + FEdGraphPinType PinType; + bIsErrorFree &= Schema->ConvertPropertyToPinType(Param, /*out*/ PinType); + bIsErrorFree &= nullptr != CompletedEventNode->CreateUserDefinedPin(Param->GetFName(), PinType, EGPD_Output); + } + } + } + + // connect delegate + { + UEdGraphPin* CallFunctionDelegatePin = CallTakeFromPoolNode->FindPin(DelegateCompletedParamName); + ensure(CallFunctionDelegatePin); + UEdGraphPin* EventDelegatePin = CompletedEventNode->FindPin(UK2Node_CustomEvent::DelegateOutputName); + bIsErrorFree &= CallFunctionDelegatePin && EventDelegatePin && Schema->TryCreateConnection(CallFunctionDelegatePin, EventDelegatePin); + } + + // connect loaded object from event to assign + { + UEdGraphPin* LoadedAssetEventPin = CompletedEventNode->FindPin(TEXT("Object")); + ensure(LoadedAssetEventPin); + UEdGraphPin* AssignRHSPPin = AssignNode->GetValuePin(); + bIsErrorFree &= AssignRHSPPin && LoadedAssetEventPin && Schema->TryCreateConnection(LoadedAssetEventPin, AssignRHSPPin); + } + + // connect assign exec input to event output + { + UEdGraphPin* CompletedEventOutputPin = CompletedEventNode->FindPin(UEdGraphSchema_K2::PN_Then); + UEdGraphPin* AssignInputExePin = AssignNode->GetExecPin(); + bIsErrorFree &= AssignInputExePin && CompletedEventOutputPin && Schema->TryCreateConnection(AssignInputExePin, CompletedEventOutputPin); + } + + // connect assign exec output to output + { + UEdGraphPin* OutputCompletedPin = FindPin(UEdGraphSchema_K2::PN_Completed); + UEdGraphPin* AssignOutputExePin = AssignNode->GetThenPin(); + bIsErrorFree &= OutputCompletedPin && AssignOutputExePin && CompilerContext.MovePinLinksToIntermediate(*OutputCompletedPin, *AssignOutputExePin).CanSafeConnect(); + } + + if (!bIsErrorFree) + { + CompilerContext.MessageLog.Error(*LOCTEXT("InternalConnectionError", "K2Node_TakeFromPool: Internal connection error. @@").ToString(), this); + } + + BreakAllNodeLinks(); +} + +FText UK2Node_TakeFromPool::GetTooltipText() const +{ + return FText(LOCTEXT("UK2Node_TakeFromPoolGetTooltipText", + "Get the object from a pool by specified class, where output is async that returns the object when is ready. " + "It creates new object if there no free objects contained in pool or does not exist any. " + "If any is found and free, activates and returns object from the pool, otherwise async spawns new one next frames and register in the pool. " + "'SpawnObjectsPerFrame' affects how fast new objects are created, it can be changed in 'Project Settings' -> 'Plugins' -> 'Pool Manager'. " + "Is custom blueprint node implemented in K2Node_TakeFromPool.h, so can't be overridden and accessible on graph (not inside functions). " + "Node's output will not work in for/while loop in blueprints because of Completed delegate: to achieve loop connect output exec to input exec." + )); +} + +FText UK2Node_TakeFromPool::GetNodeTitle(ENodeTitleType::Type TitleType) const +{ + return FText(LOCTEXT("UK2Node_TakeFromPoolGetNodeTitle", "Take From Pool")); +} + +bool UK2Node_TakeFromPool::IsCompatibleWithGraph(const UEdGraph* TargetGraph) const +{ + bool bIsCompatible = false; + // Can only place events in ubergraphs and macros (other code will help prevent macros with latents from ending up in functions), and basicasync task creates an event node: + const EGraphType GraphType = TargetGraph->GetSchema()->GetGraphType(TargetGraph); + if (GraphType == EGraphType::GT_Ubergraph || GraphType == EGraphType::GT_Macro) + { + bIsCompatible = true; + } + return bIsCompatible && Super::IsCompatibleWithGraph(TargetGraph); +} + +FName UK2Node_TakeFromPool::GetCornerIcon() const +{ + return TEXT("Graph.Latent.LatentIcon"); +} + +void UK2Node_TakeFromPool::GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const +{ + // actions get registered under specific object-keys; the idea is that + // actions might have to be updated (or deleted) if their object-key is + // mutated (or removed)... here we use the node's class (so if the node + // type disappears, then the action should go with it) + const UClass* ActionKey = GetClass(); + // to keep from needlessly instantiating a UBlueprintNodeSpawner, first + // check to make sure that the registrar is looking for actions of this type + // (could be regenerating actions for a specific asset, and therefore the + // registrar would only accept actions corresponding to that asset) + if (ActionRegistrar.IsOpenForRegistration(ActionKey)) + { + UBlueprintNodeSpawner* NodeSpawner = UBlueprintNodeSpawner::Create(GetClass()); + check(NodeSpawner != nullptr); + + ActionRegistrar.AddBlueprintAction(ActionKey, NodeSpawner); + } +} + +FText UK2Node_TakeFromPool::GetMenuCategory() const +{ + return FText(LOCTEXT("UK2Node_TakeFromPoolGetMenuCategory", "Pool Manager")); +} + +FName UK2Node_TakeFromPool::NativeFunctionName() const +{ + return GET_FUNCTION_NAME_CHECKED(UPoolManagerSubsystem, BPTakeFromPool); +} + +#undef LOCTEXT_NAMESPACE diff --git a/Source/PoolManagerEditor/Private/PoolManagerEditorModule.cpp b/Source/PoolManagerEditor/Private/PoolManagerEditorModule.cpp new file mode 100644 index 0000000..fd18641 --- /dev/null +++ b/Source/PoolManagerEditor/Private/PoolManagerEditorModule.cpp @@ -0,0 +1,20 @@ +// Copyright (c) Yevhenii Selivanov. + +#include "PoolManagerEditorModule.h" + +#define LOCTEXT_NAMESPACE "FPoolManagerEditorModule" + +void FPoolManagerEditorModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FPoolManagerEditorModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FPoolManagerEditorModule, PoolManagerEditor) \ No newline at end of file diff --git a/Source/PoolManagerEditor/Public/K2Node_TakeFromPool.h b/Source/PoolManagerEditor/Public/K2Node_TakeFromPool.h new file mode 100644 index 0000000..fb4919e --- /dev/null +++ b/Source/PoolManagerEditor/Public/K2Node_TakeFromPool.h @@ -0,0 +1,39 @@ +// Copyright (c) Yevhenii Selivanov + +#pragma once + +#include "K2Node.h" +//--- +#include "K2Node_TakeFromPool.generated.h" + +UCLASS(MinimalAPI) +class UK2Node_TakeFromPool : public UK2Node +{ + GENERATED_BODY() + +public: + static inline const FName ClassInputName = TEXT("ObjectClass"); + static inline const FName TransformInputName = TEXT("Transform"); + static inline const FName ObjectOutputName = TEXT("Object"); + + // UEdGraphNode interface + virtual void AllocateDefaultPins() override; + virtual FText GetTooltipText() const override; + virtual FText GetNodeTitle(ENodeTitleType::Type TitleType) const override; + virtual bool IsCompatibleWithGraph(const UEdGraph* TargetGraph) const override; + // End of UEdGraphNode interface + + // UK2Node interface + virtual bool IsNodeSafeToIgnore() const override { return true; } + virtual bool IsNodePure() const override { return false; } + virtual void ExpandNode(class FKismetCompilerContext& CompilerContext, UEdGraph* SourceGraph) override; + virtual FName GetCornerIcon() const override; + virtual void GetMenuActions(FBlueprintActionDatabaseRegistrar& ActionRegistrar) const override; + virtual FText GetMenuCategory() const override; + virtual bool NodeCausesStructuralBlueprintChange() const override { return true; } + virtual void ReallocatePinsDuringReconstruction(TArray& OldPins) override; + // End of UK2Node interface + +protected: + virtual FName NativeFunctionName() const; +}; diff --git a/Source/PoolManagerEditor/Public/PoolManagerEditorModule.h b/Source/PoolManagerEditor/Public/PoolManagerEditorModule.h new file mode 100644 index 0000000..0741823 --- /dev/null +++ b/Source/PoolManagerEditor/Public/PoolManagerEditorModule.h @@ -0,0 +1,25 @@ +// Copyright (c) Yevhenii Selivanov. + +#pragma once + +#include "Modules/ModuleInterface.h" +//--- +#include "Modules/ModuleManager.h" + +class POOLMANAGEREDITOR_API FPoolManagerEditorModule : public IModuleInterface +{ +public: + /** + * Called right after the module DLL has been loaded and the module object has been created. + * Load dependent modules here, and they will be guaranteed to be available during ShutdownModule. + */ + virtual void StartupModule() override; + + /** + * Called before the module is unloaded, right before the module object is destroyed. + * During normal shutdown, this is called in reverse order that modules finish StartupModule(). + * This means that, as long as a module references dependent modules in it's StartupModule(), it + * can safely reference those dependencies in ShutdownModule() as well. + */ + virtual void ShutdownModule() override; +};