The Singleton design pattern has a lot of baggage, but its utility can't be ignored. Luckily Unreal provides a way to get the benefits of a Singleton, with less of the drawbacks.
The Bad Way: C++ Static Singleton
class UMySingleton : public UObject
{
public:
static UMySingleton* GetInstance() { return Instance; }
private:
static UMySingleton* Instance;
};
Benefits and drawbacks of C++ static class singletons:
- Interface programmers are used to
- Interacts badly with the editor: Without work, instances are preserved between running game through the editor multiple times.
- Interacts badly class Class Default Objects: Without work, instances are created when CDOs are created.
- Lifetime unclear: Requires careful programming and clear intentions to manage the lifetime of singletons.
Unreal Subsystems
Unreal has something it calls Subsystems, that can be used to create globally-accessible modules that have explicitly-defined lifecycles.
Your subsystem's lifetime will match the lifetime of its parent. There are 5 different parent classes to choose from (see below). More info on their lifecycle can be found in the documentation.
Subsystem | Parent Class | Lifetime |
---|---|---|
Engine | UEngineSubsystem |
Both in editor and in-game, I think. |
Editor | UEditorSubsystem |
When the Editor starts. |
GameInstance | UGameInstanceSubsystem |
As soon as your game starts, stays alive until the game is closed. |
LocalPlayer | ULocalPlayerSubsystem |
Matches the lifetime of its parent ULocalPlayer , can move between levels. |
World | UWorldSubsystem |
Matches its parent UWorld , is effectively per-level. |
Benefits over vanilla C++ singletons:
- Lifetime is automatically managed: Subclassing the correct Subsystem ensures that the instance will be created and destroyed for you.
- Desired lifetime is made explicit: It is
clear that a Subsystem that inherits from
UWorldSubsystem
will only exist as long as a World. - Cleaner Blueprint access.
- Accessible in Python scripts.
- Requires some understanding of the lifecycles of a few Unreal classes.
- Have to learn Unreal's access style instead of
MyClass::GetInstance()
Accessing Subsystems from C++
UGameInstance* GameInstance = ...;
UMyGameSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameSubsystem>();
ULocalPlayer* LocalPlayer = ...;
UMyPlayerSubsystem* MySubsystem = LocalPlayer->GetSubsystem<UMyPlayerSubsystem>();
Example Usage
Imagine that we want to save player telemetry as they progress through the
game, from the main menu to in-game. We could create a subclass of
UGameInstanceSubsystem
called UTelemetrySubsystem
. Our telemetry class
would be instantiated as soon as the game starts, and all logic related to
telemetry would be stored within that subsystem.
Subsystem Base Class
UCLASS(Abstract)
class ENGINE_API USubsystem : public UObject
{
GENERATED_BODY()
public:
USubsystem();
/** Override to control if the Subsystem should be created at all.
* For example you could only have your system created on servers.
* It's important to note that if using this is becomes very important to null check whenever getting the Subsystem.
*
* Note: This function is called on the CDO prior to instances being created!
*/
virtual bool ShouldCreateSubsystem(UObject* Outer) const { return true; }
/** Implement this for initialization of instances of the system */
virtual void Initialize(FSubsystemCollectionBase& Collection) {}
/** Implement this for deinitialization of instances of the system */
virtual void Deinitialize() {}
private:
friend class FSubsystemCollectionBase;
FSubsystemCollectionBase* InternalOwningSubsystem;
};
Caveats
Massive thanks to Guillaume Pastor for letting me know some of the caveats with using Subsystems:
Working With Data
There is no way to make a blueprint subclass of a Subsystem that I know of, so to expose data for designers to control, you have to come up with an alternative method.
Guillaume hit upon using
a UDeveloperSettings
UPROPERTY()
, to let designers choose a DataTable, and then doing all the data
customization within that DataTable. It's not as nice as just Blueprint subclassing the Subsystem, but it works!