Skip to content

Instantly share code, notes, and snippets.

@drichardson
Created May 21, 2021 17:42
Show Gist options
  • Save drichardson/ed81addf000c63dd7beffa0c385f6697 to your computer and use it in GitHub Desktop.
Save drichardson/ed81addf000c63dd7beffa0c385f6697 to your computer and use it in GitHub Desktop.
#pragma once
#include "CoreMinimal.h"
#include "Engine/World.h"
#include "EngineUtils.h"
DECLARE_LOG_CATEGORY_EXTERN(LogWorldSingleton, Log, All);
/*
* TWorldSingleton is a weak pointer to a actor in a level, which is only a
* singleton by convention of only one actor of that type being in a UWorld.
* Unlike code based singletons, it is therefore okay to have multiple
* TWorldSingleton's pointing to the same actor, thought this is typically
* unnecessary and less performant since each instance of TWorldSingleton must
* do an actor search the first time it is used.
*
* Example Use Case
* If you have an single inventory catalog actor in your level that all other
* actors should be able to find, you can use TWorldSingleton to access it.
*/
template <typename ActorType>
class TWorldSingleton
{
private:
TWeakObjectPtr<ActorType> Singleton;
TWeakObjectPtr<UWorld> SingletonWorld;
public:
// Get the actor instance in this world, if any, logging errors if there is more than one.
// This function should only be called after all actors are loaded. You wouldn't want to use it,
// for example, in a constructor.
ActorType* Get(UWorld* World)
{
if (!World)
{
// Although Singleton.Get() could be returned here, don't. Rather,
// error out on null World now, so that anyone passing in a null
// world know about it immediately. Otherwise, if another call site is correctly called
// Get() with a valid world before the incorrect call, the incorrect call would succeed
// (in spite of the null world), making the bug hard to track down.
return nullptr;
}
if (SingletonWorld.Get() != World && World && SingletonWorld.IsValid() &&
Singleton.IsValid())
{
// Both SingletonWorld and World are non-null and different. Log a warning
// here since this is probably not what the caller wants. That said, this class handles
// this situation, but it isn't as performant since the singleton will be looked up
// whenever this happens. This situation is common in PIE since there are multiple
// worlds created for each player.
UE_CLOG(
!World->IsPlayInEditor(),
LogWorldSingleton,
Warning,
TEXT("Cached singleton %s (%p) is in valid world %s (%p), but caller is trying to "
"get a singleton in world %s (%p)."),
*GetNameSafe(Singleton.Get()),
Singleton.Get(),
*GetNameSafe(SingletonWorld.Get()),
SingletonWorld.Get(),
*GetNameSafe(World),
World);
Singleton = nullptr;
SingletonWorld = nullptr;
}
if (!Singleton.IsValid())
{
for (TActorIterator<ActorType> It(World); It; ++It)
{
if (!Singleton.IsValid())
{
Singleton = *It;
SingletonWorld = World;
}
else
{
UE_LOG(
LogWorldSingleton,
Error,
TEXT("TWorldSingleton::Get found another actor of class %s named %s. The "
"actor being used as the singleton is %s. To fix, delete all but one "
"of the actors."),
*GetNameSafe(ActorType::StaticClass()),
*GetNameSafe(*It),
*GetNameSafe(Singleton.Get()));
}
}
}
return Singleton.Get();
}
};
@drichardson
Copy link
Author

An example use of this singleton would be something like this:

UCLASS()
class AFortificationCatalog : public AInfo
{
	GENERATED_BODY()

public:
	AFortificationCatalog();
	static TWorldSingleton<AFortificationCatalog> Singleton;
...

And then using the singleton:

AFortificationCatalog* Catalog = AFortificationCatalog::Singleton.Get(GetWorld());
if (Catalog == nullptr)
{
	// no singleton actor in world
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment