<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>world-partition on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/tags/world-partition/</link>
    <description>Personal portfolio and technical notes</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    
    <copyright>© 2026 stillcooking.dev — CC BY 4.0, https://creativecommons.org/licenses/by/4.0/</copyright>
    <lastBuildDate>Tue, 21 Jul 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/tags/world-partition/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>Subsystem and manager actor lifecycles — who, when, and in what order</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/subsystem-lifecycle-init-order/</link>
      <pubDate>Tue, 21 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/subsystem-lifecycle-init-order/</guid>
      <description>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.</description>
      <content:encoded><![CDATA[<h2 id="five-collections-five-owners">Five collections, five owners</h2>
<p>A subsystem derives from one of five base classes. Each subsystem type is associated with a different owner and inherits that owner’s lifetime: <code>UEngineSubsystem</code>, <code>UEditorSubsystem</code>, <code>UGameInstanceSubsystem</code>, <code>UWorldSubsystem</code> (plus <code>UTickableWorldSubsystem</code>), or <code>ULocalPlayerSubsystem</code> (<code>Runtime/Engine/Public/Subsystems/Subsystem.h:12-20</code>). Everything below comes from reading the UE 5.8 source.</p>
<p>Choosing the base class is how you declare the subsystem&rsquo;s lifetime. Everything else happens automatically.</p>
<p>All five follow the same contract: <code>ShouldCreateSubsystem(UObject* Outer)</code> → <code>Initialize</code> → <code>Deinitialize</code>. The first call is the exception. It runs on the CDO before an instance even exists, as the header states explicitly: <em>&ldquo;Note: This function is called on the CDO prior to instances being created!&rdquo;</em> (<code>Subsystem.h:49-56</code>).</p>
<p>Under the hood, the collection gathers every non-abstract derived class through <code>GetDerivedClasses(BaseType, …, true)</code> and asks each CDO whether that subsystem should be created (<code>Private/Subsystems/SubsystemCollection.cpp:256,373-391</code>). It stores the resulting instances in a <code>TMap&lt;UClass*, USubsystem*&gt;</code> (<code>Public/Subsystems/SubsystemCollection.h:116-143</code>). 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 <a href="/en/topics/unreal-engine/gameplay-framework/should-create-subsystem-picks-the-class">ShouldCreateSubsystem and the subsystem class hierarchy</a>.</p>
<h2 id="where-each-collection-starts">Where each collection starts</h2>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Collection</th>
        <th>Created</th>
        <th>Torn down</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Engine (dynamic)</td>
        <td>collection: <code>UEngine::Init</code> — <code>Private/UnrealEngine.cpp:2403</code>; instances: module load — <code>Subsystem.h:72-83</code></td>
        <td><code>UEngine::PreExit</code> — <code>:2757</code>; instances: module unload</td>
      </tr>
      <tr>
        <td>Editor (dynamic)</td>
        <td>instances: module load — <code>Editor/EditorSubsystem/Public/EditorSubsystem.h:9-23</code></td>
        <td>module unload</td>
      </tr>
      <tr>
        <td>GameInstance</td>
        <td><code>UGameInstance::Init</code> — <code>Private/GameInstance.cpp:129</code></td>
        <td><code>UGameInstance::Shutdown</code> — <code>:167</code></td>
      </tr>
      <tr>
        <td>World</td>
        <td><code>UWorld::InitWorld</code> — <code>Private/World.cpp:2414</code></td>
        <td><code>UWorld::CleanupWorldInternal</code> — <code>:6476,6486</code></td>
      </tr>
      <tr>
        <td>LocalPlayer</td>
        <td><code>ULocalPlayer::PlayerAdded</code> — <code>Private/LocalPlayer.cpp:262,270</code></td>
        <td><code>ULocalPlayer::PlayerRemoved</code> — <code>:280</code></td>
      </tr>
    </tbody>
  </table>
</div>
<p>The first two rows are the easiest to get wrong. <code>UEditorSubsystem</code> and <code>UEngineSubsystem</code> derive from <code>UDynamicSubsystem</code>, 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.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  The trap in the LocalPlayer row is hidden in the owner’s name. The collection doesn’t start when the <code>ULocalPlayer</code> object is created. It starts in <code>PlayerAdded</code>, after the player has been attached to a <code>UGameViewportClient</code>. Both overloads of that method call <code>SubsystemCollection.Initialize(this)</code> (<code>Private/LocalPlayer.cpp:257-271</code>), while <code>PlayerRemoved</code> deinitializes the collection (<code>:278-280</code>).
</div>

<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  In PIE, every client instance gets its own <code>UGameInstance</code>. 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.
</div>

<h2 id="the-world-startup-timeline">The world startup timeline</h2>
<p>By the time this timeline begins, the Engine and GameInstance collections have already been initialized. <code>UEngine::Init</code> runs during process startup, and <code>UGameInstance::Init</code> runs before the map load that creates the gameplay world.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  One exception breaks this intuition: <code>UGameInstance::InitializeStandalone</code> creates a dummy world before calling its own <code>Init()</code> (<code>Private/GameInstance.cpp:190-202</code>). The World subsystems for that dummy world are therefore initialized before the GameInstance subsystems. The dummy world is destroyed during the first <code>LoadMap</code>.
</div>

<p>The relevant points in the timeline are all in <code>Runtime/Engine/Private/World.cpp</code>:</p>
<ol>
<li><strong><code>UWorld::InitWorld()</code></strong> calls <code>InitializeSubsystems</code> (<code>:2447</code>) and, near the end, <code>PostInitializeSubsystems</code> (<code>:2610</code>). World subsystems receive both <code>Initialize</code> and <code>PostInitialize</code>.</li>
<li><strong><code>UWorld::InitializeActorsForPlay()</code></strong> runs (<code>:5946</code>). Only then does the engine start handling actors placed in the level.</li>
<li><strong><code>UWorld::BeginPlay()</code></strong> calls <code>OnWorldBeginPlay</code> on every World subsystem and <strong>then</strong> calls <code>AGameModeBase::StartPlay()</code> (<code>:6165-6179</code>).</li>
</ol>
<p>The third point gives us a guarantee that no actor can provide: a World subsystem has been initialized and has already run <code>OnWorldBeginPlay</code> before GameMode starts gameplay. An actor placed in the level receives its <code>BeginPlay</code> during <code>InitializeActorsForPlay</code>/<code>StartPlay</code>, 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.</p>
<p>An actor spawned during play is a different case. Its <code>BeginPlay</code> fires immediately after it is spawned, so the startup-order question doesn’t apply.</p>
<p>A manager actor comes close to providing the same guarantee, but only when three conditions are met: it derives from <code>AInfo</code> (or explicitly sets <code>bIsSpatiallyLoaded = false</code>), lives in the persistent level, and has no dependency on a streamed sublevel. The <code>AInfo</code> constructor sets <code>bIsSpatiallyLoaded = false</code> and <code>bReplicates = false</code> (<code>Private/Info.cpp:11-45</code>), which is exactly the kind of role Epic designed this class for.</p>
<p>A plain <code>AActor</code> 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 <code>ULevel</code>.</p>
<h2 id="where-the-ordering-guarantee-stops">Where the ordering guarantee stops</h2>
<p>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.</p>
<p><strong>There is no declarative ordering between collections.</strong> <code>FSubsystemCollectionBase::InitializeDependency</code> enforces ordering within a single collection and nowhere else. The header states this plainly: <em>&ldquo;Dependencies only work within a collection&rdquo;</em> (<code>Public/Subsystems/SubsystemCollection.h:31-45</code>). A World subsystem that accesses a GameInstance subsystem from inside <code>Initialize</code> has no declarative safeguard. You must either enforce the order yourself or defer that access until <code>OnWorldBeginPlay</code>, when the world has already been assembled.</p>
<p><strong>A dynamic subsystem whose module hasn’t loaded never appears.</strong> A <code>UEditorSubsystem</code> placed in a module with the wrong <code>LoadingPhase</code> simply never reaches the collection (<code>Subsystem.h:72-83</code>, <code>EditorSubsystem.h:9-23</code>). The symptom is misleading: <code>GetEditorSubsystem&lt;T&gt;()</code> returns null even though the class compiles, loads, and looks perfectly fine in the editor.</p>
<p><strong>A class that hasn&rsquo;t been loaded never even makes the list.</strong> This is the same mechanism as the point above, seen from the other side. Non-dynamic collections run their <code>GetDerivedClasses</code> 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&rsquo;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 href="/en/topics/unreal-engine/gameplay-framework/blueprint-subsystem-missing-in-packaged-build">A Blueprint subsystem does not get created in a packaged build</a>.</p>
<p><strong>A conditional <code>ShouldCreateSubsystem</code> eliminates the non-null guarantee.</strong> Epic does this in its own code: <code>UInputDeviceSubsystem</code> returns <code>false</code> on a dedicated server, in a commandlet, and when Slate hasn’t been initialized (<code>Private/GameFramework/InputDeviceSubsystem.cpp:240-252</code>). The consequence is visible in the subsystem’s own accessor. <code>UInputDeviceSubsystem::Get()</code> returns <code>nullptr</code> (<code>:194-197</code>), so every call within the engine is wrapped in an <code>if</code>, three times in <code>ForceFeedbackEffect.cpp</code> alone (<code>:122</code>, <code>:190</code>, <code>:214</code>).</p>
<p>Overriding this hook gives up the very property that often makes a subsystem appealing in the first place. From that point on, every <code>GetSubsystem&lt;T&gt;()</code> 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, <a href="/en/topics/unreal-engine/gameplay-framework/should-create-subsystem-picks-the-class">into a Blueprint</a>.</p>
<p><strong><code>DoesSupportWorldType</code> includes editor worlds by default.</strong> 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 <code>UWorldSubsystem</code> overrides <code>ShouldCreateSubsystem</code> through <code>DoesSupportWorldType</code>, whose default implementation allows game, PIE, <strong>and</strong> editor worlds (<code>Public/Subsystems/WorldSubsystem.h:33-66</code>, especially <code>:64-66</code>). If the subsystem also derives from <code>UTickableWorldSubsystem</code>, it ticks from <code>Initialize</code> to <code>Deinitialize</code> for as long as the map remains open in the editor (<code>WorldSubsystem.h:72-106</code>).</p>
<h2 id="confirming-this-in-your-own-project">Confirming this in your own project</h2>
<p>You can verify the entire timeline above in a single editor run. Log four points and include the world type on every line:</p>
<div class="highlight" data-lang="cpp">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-cpp" data-lang="cpp"><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">UMyWorldSubsystem</span><span class="o">::</span><span class="n">Initialize</span><span class="p">(</span><span class="n">FSubsystemCollectionBase</span><span class="o">&amp;</span> <span class="n">Collection</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Super</span><span class="o">::</span><span class="n">Initialize</span><span class="p">(</span><span class="n">Collection</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">UE_LOG</span><span class="p">(</span><span class="n">LogMyGame</span><span class="p">,</span> <span class="n">Log</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;[1] Subsystem::Initialize | World=%s Type=%d&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="o">*</span><span class="n">GetWorld</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">GetName</span><span class="p">(),</span> <span class="p">(</span><span class="n">int32</span><span class="p">)</span><span class="n">GetWorld</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">WorldType</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">UMyWorldSubsystem</span><span class="o">::</span><span class="n">OnWorldBeginPlay</span><span class="p">(</span><span class="n">UWorld</span><span class="o">&amp;</span> <span class="n">InWorld</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Super</span><span class="o">::</span><span class="n">OnWorldBeginPlay</span><span class="p">(</span><span class="n">InWorld</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">UE_LOG</span><span class="p">(</span><span class="n">LogMyGame</span><span class="p">,</span> <span class="n">Log</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;[2] Subsystem::OnWorldBeginPlay&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// AMyManagerActor::BeginPlay  → [3]
</span></span></span><span class="line"><span class="cl"><span class="c1">// AMyGameMode::StartPlay      → [4]
</span></span></span></code></pre></div></div>
<p>Seeing <code>1 → 2 → 3 → 4</code> in the log confirms the guarantee for one specific project and one specific streaming setup. If <code>[3]</code> appears before <code>[2]</code>, the actor is being spawned rather than placed in the level. Its <code>BeginPlay</code> fires immediately after the spawn and has nothing to do with the world startup timeline.</p>
<div class="section-panel section-panel--checklist">
  <div class="section-panel__label">Checklist</div>
  
<p>Additional checks, each answering a different question:</p>
<ul class="checklist">
  <li><strong>Who created me, and from where?</strong> — set a breakpoint on <code>FSubsystemCollectionBase::AddAndInitializeSubsystem</code>. The call stack shows both the collection and the point in the engine that created it (<code>UEngine::Init</code>, <code>UGameInstance::Init</code>, <code>UWorld::InitWorld</code>, or a module load).</li>
  <li><strong>Am I cluttering up the editor?</strong> — if line <code>[1]</code> appears when you simply open a map without starting PIE, then <code>DoesSupportWorldType</code> hasn’t been overridden. Filtering for <code>WorldType == EWorldType::Game || WorldType == EWorldType::PIE</code> fixes it.</li>
  <li><strong>Does my manager actor really survive?</strong> — check the actor’s <code>bIsSpatiallyLoaded</code> value in the Details panel under World Partition, and confirm that it belongs to the persistent level. Both conditions must be satisfied.</li>
  <li><strong>Was my class asked at all?</strong> — run with <code>-LogCmds="LogSubsystemCollection VeryVerbose"</code>. A <code>CDO choose to not create</code> 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.</li>
</ul>

</div>

<p>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 <code>BeginPlay</code>. 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.</p>
<p>The game code has to answer every other question for itself, and the engine won’t even signal that the question came up.</p>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
