Skip to content

Instantly share code, notes, and snippets.

@sleeptightAnsiC
Last active August 29, 2023 20:08
Show Gist options
  • Save sleeptightAnsiC/a97825840f789f87448616d19a71dd80 to your computer and use it in GitHub Desktop.
Save sleeptightAnsiC/a97825840f789f87448616d19a71dd80 to your computer and use it in GitHub Desktop.
UE5 ActorSingleton

AActorSingleton

Sometimes, we want to have only one Actor of some Class in the World (something like a singleton).
To achieve that, we are always stroring reference to the single instance and destroying the duplicates as soon as they're being spawned.

AActorSingleton enforces said behavior in few cases such as:

  • Spawning Actor at Runtime
  • Placing Actor from the Content Browser to the Level Viewport
  • Loading the Level

How to use?

  1. Copy the .h and .cpp file to your project source
  2. Remember to change the "MYPROJECT_API" in the .h
  3. Add "UnrealEd" dependency module to the project's .Build.cs file
  4. Create new class derived from AActorSingleton
  5. Inside of the new class, overide the TreatDerivedClassInstancesAsSame function so it will always return 'true'

Disclaimer: This code hasn't been widely tested. There might be more cases that I'm unaware about.

#include "ActorSingleton.h"
#include "Kismet/GameplayStatics.h"
#include "Subsystems/EditorActorSubsystem.h"
#include "Editor.h"
DEFINE_LOG_CATEGORY(ActorSingleton);
void AActorSingleton::OnConstruction(const FTransform& Transform)
{
Super::OnConstruction(Transform);
TryBecomeNewInstanceOrSelfDestroy();
}
/* static */ AActorSingleton* AActorSingleton::Get(UWorld* WorldContext, TSubclassOf<AActorSingleton> Class)
{
check(IsValid(WorldContext))
auto& InstancesMap = UActorSingletonManager::GetChecked(WorldContext)->Instances;
if (!InstancesMap.Contains(Class))
{
return nullptr;
}
return InstancesMap[Class];
}
template<class T>
/* static */ static T* AActorSingleton::Get(UWorld* World)
{
static_assert(TIsDerivedFrom<T, AActorSingleton>::IsDerived, "T must be derived from AActorSingleton");
return AActorSingleton::Get(World, T::StaticClass())
}
template<class T>
/* static */ static T* AActorSingleton::GetChecked(UWorld* World)
{
T* Instance = AActorSingleton::Get<T>(World);
check(Instance)
return Instance;
}
void AActorSingleton::TryBecomeNewInstanceOrSelfDestroy()
{
/* Do nothing, if 'this' is either...
* ...not valid (such case has never happened but always worth cathing)
* ...being destroyed (IsValid does NOT catch this in some cases)
* ...marked as Transient (we omit "dummy" Actors that are often being used by the Editor) */
if(
!ensure(IsValid(this))
|| this->IsActorBeingDestroyed()
|| this->HasAnyFlags(EObjectFlags::RF_Transient)
)
{
return;
}
TSubclassOf<AActorSingleton> ThisClass = this->GetClass();
/* Do nothing, if 'this' is CDO */
if (this == ThisClass->GetDefaultObject())
{
return;
}
UWorld* ThisWorld = GetWorld();
auto* ActorSingletonManager = UActorSingletonManager::Get(ThisWorld);
/* UActorSingletonManager::Get can fail (and this is expected)
* There are cases where UActorSingletonManager might not be Initialized yet,
* e.g. during AActor::OnConstruction when opening Map in the Editor.
* We deal with said problem by re-firing this function later in the UActorSingletonManager::PostInitialize
* (this is not an ideal solution but works fine for now, see UActorSingletonManager::PostInitialize) */
if(!ActorSingletonManager)
{
return;
}
auto& InstancesMap = ActorSingletonManager->Instances;
/* Go through the UClass::GetSuperClass chain, from 'ThisClass' to 'AActorSingleton',
* and store said chain as we gonna traverse it backwards later. */
TArray<UClass*> ClassInheritanceChain;
for (UClass* ItClass = ThisClass; ItClass != AActorSingleton::StaticClass(); ItClass = ItClass->GetSuperClass())
{
ClassInheritanceChain.Add(ItClass);
}
ClassInheritanceChain.Add(AActorSingleton::StaticClass());
/* Traverse throught ClassInheritanceChain from the back (top parrent) to the front (ThisClass)
* We do this to find the highest parrent class that returns 'true' from TreatDerivedClassInstancesAsSame,
* in such case we stop traversing and start treating the said class as new 'ThisClass'. */
int32 ItClassIndex = ClassInheritanceChain.Num() - 1;
for (; ItClassIndex > 0; --ItClassIndex)
{
const auto* ItCDO = static_cast<AActorSingleton*>(ClassInheritanceChain[ItClassIndex]->GetDefaultObject());
if (ItCDO->TreatDerivedClassInstancesAsSame())
{
break;
}
}
ThisClass = ClassInheritanceChain[ItClassIndex];
if (!InstancesMap.Contains(ThisClass))
{
InstancesMap.Add(ThisClass, nullptr);
}
AActorSingleton*& CurrentInstance = InstancesMap[ThisClass];
if (this == CurrentInstance)
{
return;
}
/* TODO:
* 'ensure' is not a very elegant way to catch this problem,
* it only fires once, so this won't work for multiple classes.
* 'ensureAlways' also won't make a job as it will be hit too often causing a frame drop...
* Maybe a custom error handling? Also, catching this error might be done in a diffrent place? */
ensureMsgf
(
//expression (lambda):
[=](){
const auto* ThisCDO = static_cast<AActorSingleton*>(ThisClass->GetDefaultObject());
return ThisCDO->TreatDerivedClassInstancesAsSame();
}(),
//text body:
TEXT(
"There is no class in the Inheritance Chain going from '%s' to '%s',"
" which would return 'true' from 'TreatDerivedClassInstancesAsSame'!"
" Please make sure to override 'AActorSingleton::TreatDerivedClassInstancesAsSame'!"
" This function must return 'true' on the final base class!"
),
//text parameters:
*ThisClass->GetFName().ToString(),
*AActorSingleton::StaticClass()->GetFName().ToString()
);
if (!IsValid(CurrentInstance))
{
CurrentInstance = this;
UE_LOG(ActorSingleton, Warning, TEXT("'%s' is now a singleton instance of class '%s' in the World '%s'! "
"Adding/Spawning more instances of the same class is the same World will resul in them being instanently destroyed!"),
*AActor::GetDebugName(this), *ThisClass->GetFName().ToString(), *ThisWorld->GetFName().ToString());
return;
}
UE_LOG(ActorSingleton, Error, TEXT("World '%s' can have only one instance of '%s'! Destroying '%s' ..."),
*ThisWorld->GetFName().ToString(), *ThisClass->GetFName().ToString(), *AActor::GetDebugName(this));
/* In case of placing an Actor in the Level Viewport, we cannot simply Destroy it.
* Instead, we must "tell" the Editor to delete it, which will fire some additional clean up logic. */
#if WITH_EDITOR
if (ThisWorld->IsEditorWorld() && !ThisWorld->IsPlayInEditor())
{
auto* EditorActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
check(EditorActorSubsystem)
EditorActorSubsystem->ClearActorSelectionSet();
EditorActorSubsystem->SetActorSelectionState(this, true);
EditorActorSubsystem->DeleteSelectedActors(ThisWorld);
GEngine->ForceGarbageCollection(true);
/* Show Dialogue Message */
{
const FText MessageTitle = FText::FromString("SingletonActor - Destroyed Duplicate");
const FText MessageBody = FText::FromString(
"Duplicate instance of SingletonActor sub-class has been found and destroyed!"
"\nThere can be only one instance of said class, and there is already one existing."
"\n(check log for more detailed error)");
FMessageDialog::Debugf(MessageBody, &MessageTitle);
}
return;
}
#endif //WITH_EDITOR
this->Destroy(true, true);
}
void UActorSingletonManager::PostInitialize()
{
Super::PostInitialize();
FindInstancesAndDestroyDuplicates();
}
/* static */ UActorSingletonManager* UActorSingletonManager::Get(UWorld* World)
{
check(IsValid(World))
return World->GetSubsystem<UActorSingletonManager>();
}
/* static */ UActorSingletonManager* UActorSingletonManager::GetChecked(UWorld* World)
{
UActorSingletonManager* Instance = UActorSingletonManager::Get(World);
check(Instance)
return Instance;
}
void UActorSingletonManager::FindInstancesAndDestroyDuplicates()
{
TArray<AActor*> PossibleInstances;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AActorSingleton::StaticClass(), PossibleInstances);
for (AActor* Actor : PossibleInstances)
{
static_cast<AActorSingleton*>(Actor)->TryBecomeNewInstanceOrSelfDestroy();
}
}
#pragma once
/*================================================================================
= Actor Singleton:
=
= An Actor that is expected to have only one instance within UWorld.
= If we try to spawn an Actor derived from AActorSingleton,
= and there is already an existing instance of the same class (or sub-class),
= said Actor will be automatically destroyed.
=
= Make sure to override AActorSingleton::TreatDerivedClassInstancesAsSame,
= to make it return 'true' for the final class.
=
================================================================================*/
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ActorSingleton.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(ActorSingleton, Log, All);
/* An Actor that is expected to have only one spawned instance within UWorld
* Whenever a new isntance is being created, it will be automatically destroyed */
UCLASS(Abstract)
class MYPROJECT_API AActorSingleton : public AActor
{
GENERATED_BODY()
friend UActorSingletonManager;
public:
//~ Begin AActor Interface
virtual void OnConstruction(const FTransform& Transform) override;
//~ End AActor Interface
public:
/* Tells whenever the instances of sub-class will be considered the same as instances of base class
* e.g. B is a subclass of A, A::TreatDerivedClassInstancesAsSame returns 'true',
* so if there is already an instance of A, new instance of B will be destroyed (and vice-versa)
* This function is only called on CDO (it works like a 'static' function)! */
UFUNCTION(BlueprintNativeEvent)
bool TreatDerivedClassInstancesAsSame() const;
virtual bool TreatDerivedClassInstancesAsSame_Implementation() const { return false; }
/* Gets a reference to the single instance of chosen AActorSingleton subclass within current UWorld,
* may return 'nullptr' if it doesn't exist.
* This is a BP version of this function. For better typesafety in C++, use AActorSingleton::Get<T> */
UFUNCTION(BlueprintCallable, BlueprintPure,
meta = (DisplayName = "Get Actor Singleton Instance", DeterminesOutputType = "Class", WorldContext = "WorldContext"))
static AActorSingleton* Get(UWorld* WorldContext, TSubclassOf<AActorSingleton> Class);
/* Templated version of AActorSingleton::Get
* Will cause a compilation error if you try to call it on class not derived from AActorSingleton. */
template<class T>
static T* Get(UWorld* World);
/* Same as AActorSingleton::Get<T> but causes a crash in case of returning 'nullptr' */
template<class T>
static T* GetChecked(UWorld* World);
private:
/* Try to become a new single instance within current UWorld,
* if instance already exists, call this->Destroy
* Does nothing in few circumstances, e.g. when calling on CDO */
void TryBecomeNewInstanceOrSelfDestroy();
};
/* Helper class for storing "static" references to AActorSingleton instances.
* Each subclass of AActorSingleton is expected to have only one spawned instance within each UWorld,
* that's why we use World Subsystem as it is always has one instance per every UWorld. */
UCLASS(NotBlueprintable)
class TLOB_API UActorSingletonManager : public UWorldSubsystem
{
GENERATED_BODY()
friend AActorSingleton;
public:
//~ Begin UWorldSubsystem Interface
virtual void PostInitialize() override;
//~ End UWorldSubsystem Interface
private:
/* Wrapper for UWorld::GetSubsystem<UActorSingletonManager>
* May return 'nullptr' in case of Manager not being initialized yet. */
static UActorSingletonManager* Get(UWorld* World);
/* Wrapper for UWorld::GetSubsystem<UActorSingletonManager>
* This version causes a crash in case of returning 'nullptr'. */
static UActorSingletonManager* GetChecked(UWorld* World);
/* Gets all AActorSingleton in the current UWorld,
* and calls AActorSingleton::TryBecomeNewInstanceOrSelfDestroy on all of them. */
void FindInstancesAndDestroyDuplicates();
UPROPERTY()
TMap<TSubclassOf<AActorSingleton>, AActorSingleton*> Instances;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment