Reference UE 5.8Versioned

Subsystem and manager actor lifecycles — who, when, and in what order

Five subsystem base classes, each with a different owner. Where the engine creates and tears down each collection, how world startup relates to actor BeginPlay, and where initialization order is no longer guaranteed.

Table of contents

Five collections, five owners

A subsystem derives from one of five base classes. Each subsystem type is associated with a different owner and inherits that owner’s lifetime: UEngineSubsystem, UEditorSubsystem, UGameInstanceSubsystem, UWorldSubsystem (plus UTickableWorldSubsystem), or ULocalPlayerSubsystem (Runtime/Engine/Public/Subsystems/Subsystem.h:12-20). Everything below comes from reading the UE 5.8 source.

Choosing the base class is how you declare the subsystem’s lifetime. Everything else happens automatically.

All five follow the same contract: ShouldCreateSubsystem(UObject* Outer) → Initialize → Deinitialize. The first call is the exception. It runs on the CDO before an instance even exists, as the header states explicitly: “Note: This function is called on the CDO prior to instances being created!” (Subsystem.h:49-56).

Under the hood, the collection gathers every non-abstract derived class through GetDerivedClasses(BaseType, …, true) and asks each CDO whether that subsystem should be created (Private/Subsystems/SubsystemCollection.cpp:256,373-391). It stores the resulting instances in a TMap<UClass*, USubsystem*> (Public/Subsystems/SubsystemCollection.h:116-143). One instance per class per Outer isn’t a convention; it follows from the container type. The key is a concrete class, not a hierarchy. If another non-abstract class derives from that base class — a Blueprint child, for instance — its CDO gets asked the same question separately, and the collection ends up holding two independent instances. How this affects which class provides the implementation is covered separately in ShouldCreateSubsystem and the subsystem class hierarchy.

Where each collection starts

CollectionCreatedTorn down
Engine (dynamic)collection: UEngine::Init — Private/UnrealEngine.cpp:2403; instances: module load — Subsystem.h:72-83UEngine::PreExit — :2757; instances: module unload
Editor (dynamic)instances: module load — Editor/EditorSubsystem/Public/EditorSubsystem.h:9-23module unload
GameInstanceUGameInstance::Init — Private/GameInstance.cpp:129UGameInstance::Shutdown — :167
WorldUWorld::InitWorld — Private/World.cpp:2414UWorld::CleanupWorldInternal — :6476,6486
LocalPlayerULocalPlayer::PlayerAdded — Private/LocalPlayer.cpp:262,270ULocalPlayer::PlayerRemoved — :280

The first two rows are the easiest to get wrong. UEditorSubsystem and UEngineSubsystem derive from UDynamicSubsystem, which automatically populates the collection when a module loads and empties it when that module unloads. Your subsystem won’t exist until its module is explicitly loaded, and the engine won’t report an error if it isn’t.

Warning
The trap in the LocalPlayer row is hidden in the owner’s name. The collection doesn’t start when the ULocalPlayer object is created. It starts in PlayerAdded, after the player has been attached to a UGameViewportClient. Both overloads of that method call SubsystemCollection.Initialize(this) (Private/LocalPlayer.cpp:257-271), while PlayerRemoved deinitializes the collection (:278-280).
Insight
In PIE, every client instance gets its own UGameInstance. This means the GameInstance, World, and LocalPlayer subsystems are created and destroyed with the PIE session, while Engine and Editor subsystems survive across many sessions in the same editor process. That difference is the most common source of state leaking between PIE runs.

The world startup timeline

By the time this timeline begins, the Engine and GameInstance collections have already been initialized. UEngine::Init runs during process startup, and UGameInstance::Init runs before the map load that creates the gameplay world.

Note
One exception breaks this intuition: UGameInstance::InitializeStandalone creates a dummy world before calling its own Init() (Private/GameInstance.cpp:190-202). The World subsystems for that dummy world are therefore initialized before the GameInstance subsystems. The dummy world is destroyed during the first LoadMap.

The relevant points in the timeline are all in Runtime/Engine/Private/World.cpp:

  1. UWorld::InitWorld() calls InitializeSubsystems (:2447) and, near the end, PostInitializeSubsystems (:2610). World subsystems receive both Initialize and PostInitialize.
  2. UWorld::InitializeActorsForPlay() runs (:5946). Only then does the engine start handling actors placed in the level.
  3. UWorld::BeginPlay() calls OnWorldBeginPlay on every World subsystem and then calls AGameModeBase::StartPlay() (:6165-6179).

The third point gives us a guarantee that no actor can provide: a World subsystem has been initialized and has already run OnWorldBeginPlay before GameMode starts gameplay. An actor placed in the level receives its BeginPlay during InitializeActorsForPlay/StartPlay, which puts it after the subsystems. That last part is my interpretation of the call order, not a guarantee stated on any single line of engine code, but it follows directly from that order.

An actor spawned during play is a different case. Its BeginPlay fires immediately after it is spawned, so the startup-order question doesn’t apply.

A manager actor comes close to providing the same guarantee, but only when three conditions are met: it derives from AInfo (or explicitly sets bIsSpatiallyLoaded = false), lives in the persistent level, and has no dependency on a streamed sublevel. The AInfo constructor sets bIsSpatiallyLoaded = false and bReplicates = false (Private/Info.cpp:11-45), which is exactly the kind of role Epic designed this class for.

A plain AActor placed in a World Partition level is spatially streamed by default and can be unloaded during gameplay. A subsystem is structurally immune to this class of bug because it doesn’t belong to any ULevel.

Where the ordering guarantee stops

The guarantee from the previous section applies only to this ordering. Everything below falls outside it, and that is where the assumption that “the subsystem is always there” breaks down.

There is no declarative ordering between collections. FSubsystemCollectionBase::InitializeDependency enforces ordering within a single collection and nowhere else. The header states this plainly: “Dependencies only work within a collection” (Public/Subsystems/SubsystemCollection.h:31-45). A World subsystem that accesses a GameInstance subsystem from inside Initialize has no declarative safeguard. You must either enforce the order yourself or defer that access until OnWorldBeginPlay, when the world has already been assembled.

A dynamic subsystem whose module hasn’t loaded never appears. A UEditorSubsystem placed in a module with the wrong LoadingPhase simply never reaches the collection (Subsystem.h:72-83, EditorSubsystem.h:9-23). The symptom is misleading: GetEditorSubsystem<T>() returns null even though the class compiles, loads, and looks perfectly fine in the editor.

A class that hasn’t been loaded never even makes the list. This is the same mechanism as the point above, seen from the other side. Non-dynamic collections run their GetDerivedClasses scan once, when the collection is created, and they only see the classes that are in memory at that moment. A Blueprint child of a subsystem with no hard references to that child isn’t loaded yet in a packaged build, so its CDO is never asked — and at the default log level a refusal and an absence look identical: both are just a missing line. The editor never shows the problem, because the Content Browser keeps the class in memory. I covered the whole case in A Blueprint subsystem does not get created in a packaged build.

A conditional ShouldCreateSubsystem eliminates the non-null guarantee. Epic does this in its own code: UInputDeviceSubsystem returns false on a dedicated server, in a commandlet, and when Slate hasn’t been initialized (Private/GameFramework/InputDeviceSubsystem.cpp:240-252). The consequence is visible in the subsystem’s own accessor. UInputDeviceSubsystem::Get() returns nullptr (:194-197), so every call within the engine is wrapped in an if, three times in ForceFeedbackEffect.cpp alone (:122, :190, :214).

Overriding this hook gives up the very property that often makes a subsystem appealing in the first place. From that point on, every GetSubsystem<T>() requires a null check, and the call site has no way to know whether it is running on a path where the subsystem was created. Sometimes that tradeoff is intentional. The condition in this method doubles as a way of declaring which class in the hierarchy should be the subsystem, and the null check becomes the price of moving the implementation one level down, into a Blueprint.

DoesSupportWorldType includes editor worlds by default. A gameplay manager that doesn’t override this method gets an instance in every world opened in the editor, not just in PIE. This happens because UWorldSubsystem overrides ShouldCreateSubsystem through DoesSupportWorldType, whose default implementation allows game, PIE, and editor worlds (Public/Subsystems/WorldSubsystem.h:33-66, especially :64-66). If the subsystem also derives from UTickableWorldSubsystem, it ticks from Initialize to Deinitialize for as long as the map remains open in the editor (WorldSubsystem.h:72-106).

Confirming this in your own project

You can verify the entire timeline above in a single editor run. Log four points and include the world type on every line:

void UMyWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
    Super::Initialize(Collection);
    UE_LOG(LogMyGame, Log, TEXT("[1] Subsystem::Initialize | World=%s Type=%d"),
        *GetWorld()->GetName(), (int32)GetWorld()->WorldType);
}

void UMyWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
    Super::OnWorldBeginPlay(InWorld);
    UE_LOG(LogMyGame, Log, TEXT("[2] Subsystem::OnWorldBeginPlay"));
}

// AMyManagerActor::BeginPlay  → [3]
// AMyGameMode::StartPlay      → [4]

Seeing 1 → 2 → 3 → 4 in the log confirms the guarantee for one specific project and one specific streaming setup. If [3] appears before [2], the actor is being spawned rather than placed in the level. Its BeginPlay fires immediately after the spawn and has nothing to do with the world startup timeline.

Checklist

Additional checks, each answering a different question:

  • Who created me, and from where? — set a breakpoint on FSubsystemCollectionBase::AddAndInitializeSubsystem. The call stack shows both the collection and the point in the engine that created it (UEngine::Init, UGameInstance::Init, UWorld::InitWorld, or a module load).
  • Am I cluttering up the editor? — if line [1] appears when you simply open a map without starting PIE, then DoesSupportWorldType hasn’t been overridden. Filtering for WorldType == EWorldType::Game || WorldType == EWorldType::PIE fixes it.
  • Does my manager actor really survive? — check the actor’s bIsSpatiallyLoaded value in the Details panel under World Partition, and confirm that it belongs to the persistent level. Both conditions must be satisfied.
  • Was my class asked at all? — run with -LogCmds="LogSubsystemCollection VeryVerbose". A CDO choose to not create line means the class was on the list and refused; the absence of that line alongside a missing instance means nobody ever asked it. In that second case the breakpoint from the first check never fires, so on its own it settles nothing.

The ordering guarantee is real, and the engine code enforces it, but it covers exactly one thing: a World subsystem exists before actors in that same world receive BeginPlay. It extends no further than that single axis. Everything above also covers startup only; the order in which a world and a session shut down is a separate question, and this note leaves it open.

The game code has to answer every other question for itself, and the engine won’t even signal that the question came up.

See also

Related pages