<?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>Topics on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/topics/</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>Thu, 13 Aug 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/topics/index.xml" rel="self" type="application/rss+xml" />
    
    <item>
      <title>ShouldCreateSubsystem and the subsystem class hierarchy</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/should-create-subsystem-picks-the-class/</link>
      <pubDate>Thu, 13 Aug 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/should-create-subsystem-picks-the-class/</guid>
      <description>The subsystem collection asks each non-abstract class&amp;rsquo;s CDO whether to create an instance. A subclass does not automatically replace its parent; both can exist side by side. This explains how to move the implementation into Blueprint, how to keep a C++ fallback when the child is missing, and why GetSubsystem&lt;T&gt;() can still find the derived instance.</description>
      <content:encoded><![CDATA[<p><code>ShouldCreateSubsystem</code> reads like an on/off switch: return <code>false</code>, and the subsystem is not created. That is how it behaves in its most common use, but that is only one use of a broader mechanism. The method decides whether an instance of each candidate class is created. Across a hierarchy, those individual decisions determine which implementations end up in the collection.</p>
<p>The difference only shows up once the hierarchy has more than one level, which happens as soon as someone creates a Blueprint child of a subsystem written in C++.</p>
<p>I covered the lifecycle contract and where each collection starts in a separate post: <a href="/en/topics/unreal-engine/gameplay-framework/subsystem-lifecycle-init-order">Subsystem and manager actor lifecycles</a>. Here, I&rsquo;m focusing on one specific part: the loop that picks the classes.</p>
<h2 id="a-subclass-does-not-replace-its-parent">A subclass does not replace its parent</h2>
<p>The collection gathers derived classes in a single call and runs <strong>every one</strong> of them through the same procedure (<code>Engine/Private/Subsystems/SubsystemCollection.cpp:209</code>):</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="n">TArray</span><span class="o">&lt;</span><span class="n">UClass</span><span class="o">*&gt;</span> <span class="n">SubsystemClasses</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">GetDerivedClasses</span><span class="p">(</span><span class="n">BaseType</span><span class="p">,</span> <span class="n">SubsystemClasses</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="n">UClass</span><span class="o">*</span> <span class="nl">SubsystemClass</span> <span class="p">:</span> <span class="n">SubsystemClasses</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">AddAndInitializeSubsystem</span><span class="p">(</span><span class="n">SubsystemClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>There is no selection of the most-derived class here, no collapsing of the hierarchy, no rule that the child wins over the parent. The question is asked separately of each entry’s own CDO (<code>SubsystemCollection.cpp:331</code>):</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="k">const</span> <span class="n">USubsystem</span><span class="o">*</span> <span class="n">CDO</span> <span class="o">=</span> <span class="n">SubsystemClass</span><span class="o">-&gt;</span><span class="n">GetDefaultObject</span><span class="o">&lt;</span><span class="n">USubsystem</span><span class="o">&gt;</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">CDO</span><span class="o">-&gt;</span><span class="n">ShouldCreateSubsystem</span><span class="p">(</span><span class="n">Outer</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">USubsystem</span><span class="o">*</span> <span class="n">Subsystem</span> <span class="o">=</span> <span class="n">NewObject</span><span class="o">&lt;</span><span class="n">USubsystem</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Outer</span><span class="p">,</span> <span class="n">SubsystemClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">SubsystemMap</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">SubsystemClass</span><span class="p">,</span> <span class="n">Subsystem</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div></div>
<p>The consequence is straightforward and easy to miss: a Blueprint child of a C++ class joins the parent as a <strong>second, independent instance</strong>. The base implementation returns <code>true</code> (<code>Subsystem.h:61</code>), so if nobody has overridden the method, two instances live in the collection. One of the C++ class, one of the Blueprint class. Both have run <code>Initialize</code>, both carry their own state, and neither knows about the other.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  Creating a Blueprint child of a subsystem takes two clicks and no C++ at all. From that point on, every counter, cache, and handler in that subsystem exists in duplicate. The engine reports nothing, because as far as the collection is concerned these are two different classes and two valid entries in <code>SubsystemMap</code>.
</div>

<p>Two filters run before that question is asked. The first rejects abstract classes (<code>:318</code>), the second rejects non-authoritative ones (<code>:324</code>):</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="c1">// Do not create instances of classes that aren&#39;t authoritative.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">SubsystemClass</span><span class="o">-&gt;</span><span class="n">GetAuthoritativeClass</span><span class="p">()</span> <span class="o">!=</span> <span class="n">SubsystemClass</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="k">return</span> <span class="k">nullptr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The second one says something about intent. <code>GetAuthoritativeClass</code> filters out the intermediate classes produced when Blueprints are recompiled, which means Epic hardened this path for Blueprint classes. A Blueprint subsystem is a scenario the engine anticipates.</p>
<h2 id="the-switch-false-in-c-true-in-blueprint">The switch: <code>false</code> in C++, <code>true</code> in Blueprint</h2>
<p>The question is asked separately of every CDO, and Blueprint defaults <strong>live on the CDO</strong> of their class. The same flag can therefore hold a different value at each level of the hierarchy, which turns it into a switch between implementation layers:</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="n">UCLASS</span><span class="p">(</span><span class="n">Blueprintable</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">MYGAME_API</span> <span class="nl">UMySubsystem</span> <span class="p">:</span> <span class="k">public</span> <span class="n">UGameInstanceSubsystem</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">GENERATED_BODY</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">protected</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="cm">/** Read from the CDO only - set to true in the Blueprint child that implements this. */</span>
</span></span><span class="line"><span class="cl">    <span class="n">UPROPERTY</span><span class="p">(</span><span class="n">EditDefaultsOnly</span><span class="p">,</span> <span class="n">Category</span> <span class="o">=</span> <span class="s">&#34;Config&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kt">bool</span> <span class="n">bShouldCreateSubsystem</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">virtual</span> <span class="kt">bool</span> <span class="nf">ShouldCreateSubsystem</span><span class="p">(</span><span class="n">UObject</span><span class="o">*</span> <span class="n">Outer</span><span class="p">)</span> <span class="k">const</span> <span class="k">override</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">Super</span><span class="o">::</span><span class="n">ShouldCreateSubsystem</span><span class="p">(</span><span class="n">Outer</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">bShouldCreateSubsystem</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="p">};</span></span></span></code></pre></div></div>
<p>With <code>false</code> on the C++ side and <code>true</code> in the Blueprint class defaults, exactly one instance ends up in the collection. The C++ class becomes a skeleton: it defines the interface, the <code>BlueprintImplementableEvent</code> declarations, and everything Blueprint cannot declare on its own, while the implementation lives one level below.</p>
<p>This works because <code>ShouldCreateSubsystem</code> runs on the CDO before an instance exists, which means it runs on the object that holds the default values stored in the Blueprint asset. That is also why the flag is marked <code>EditDefaultsOnly</code>. The value is read from the defaults and nowhere else, so per-instance editing changes nothing.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  The result of <code>Super::ShouldCreateSubsystem(Outer)</code> has to feed <strong>into the condition</strong>. The base implementation returns <code>true</code> (<code>Subsystem.h:61</code>), so calling it and discarding the result looks harmless. In classes derived from <code>UWorldSubsystem</code> the override checks <code>DoesSupportWorldType</code>, and skipping it allows the subsystem to be created in editor worlds.
</div>

<h2 id="getsubsystemt-finds-the-child-anyway"><code>GetSubsystem&lt;T&gt;()</code> finds the child anyway</h2>
<p>Instances live in a <code>TMap&lt;UClass*, USubsystem*&gt;</code> keyed by the <strong>concrete</strong> class. The natural conclusion is that querying for the base class will not find an instance of a derived one, and that conclusion is wrong. <code>GetSubsystemInternal</code> has a fallback (<code>SubsystemCollection.cpp:66</code>):</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="n">USubsystem</span><span class="o">*</span> <span class="n">SystemPtr</span> <span class="o">=</span> <span class="n">SubsystemMap</span><span class="p">.</span><span class="n">FindRef</span><span class="p">(</span><span class="n">SubsystemClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">SystemPtr</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="k">return</span> <span class="n">SystemPtr</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="k">else</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">const</span> <span class="n">FSubsystemArray</span><span class="o">&amp;</span> <span class="n">SystemPtrs</span> <span class="o">=</span> <span class="n">FindAndPopulateSubsystemArrayInternal</span><span class="p">(</span><span class="n">SubsystemClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">SystemPtrs</span><span class="p">.</span><span class="n">Subsystems</span><span class="p">.</span><span class="n">Num</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">0</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="k">return</span> <span class="n">SystemPtrs</span><span class="p">.</span><span class="n">Subsystems</span><span class="p">[</span><span class="mi">0</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="p">}</span></span></span></code></pre></div></div>
<p>A miss in the map triggers an <code>IsChildOf</code> sweep and returns the first hit. So <code>GetSubsystem&lt;UMySubsystem&gt;()</code> with the base class as the parameter returns the Blueprint child instance, even though nothing is stored under the base class key.</p>
<p>Without that fallback the switch would make no sense. Every piece of C++ that queries the subsystem by its own base class would stop seeing it the moment the implementation moved into Blueprint.</p>
<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  The fallback returns <code>Subsystems[0]</code>, the first hit in the array. With two instances alive (a parent that never overrode the method, plus a child), the order in that array decides the result. That is one more reason for the hierarchy to have exactly one winner.
</div>

<p>Listing every instance at once takes a separate accessor. In 5.7 it is called <code>GetSubsystemArrayCopy</code> and returns by value, both on the collection itself (<code>Public/Subsystems/SubsystemCollection.h:139</code>) and on the <code>UGameInstance</code> wrapper (<code>Classes/Engine/GameInstance.h:463</code>).</p>
<h2 id="the-flagless-variant-yielding-to-subclasses">The flagless variant: yielding to subclasses</h2>
<p>The flag has one unpleasant property. When the Blueprint child is gone, whether it was deleted, moved, or simply never loaded in time, <code>false</code> on the C++ side means <strong>nothing</strong> is created. The subsystem disappears entirely, including the part that was written in C++ and had nothing to do with Blueprint.</p>
<p>The condition can be phrased differently: let the class step aside, provided there is someone to step aside for.</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">bool</span> <span class="n">UMySubsystem</span><span class="o">::</span><span class="n">ShouldCreateSubsystem</span><span class="p">(</span><span class="n">UObject</span><span class="o">*</span> <span class="n">Outer</span><span class="p">)</span> <span class="k">const</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">Super</span><span class="o">::</span><span class="n">ShouldCreateSubsystem</span><span class="p">(</span><span class="n">Outer</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="k">return</span> <span class="nb">false</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">/// @Note: The collection instantiates every non-abstract subclass, so a Blueprint child
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">///        would live alongside this one. Yield so that only the most-derived class is created.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">TArray</span><span class="o">&lt;</span><span class="n">UClass</span><span class="o">*&gt;</span> <span class="n">DerivedClasses</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">GetDerivedClasses</span><span class="p">(</span><span class="n">GetClass</span><span class="p">(),</span> <span class="n">DerivedClasses</span><span class="p">,</span> <span class="cm">/*bRecursive*/</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">DerivedClasses</span><span class="p">.</span><span class="n">Num</span><span class="p">()</span> <span class="o">==</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>In the normal case the outcome is the same, since a child exists and the parent steps aside. The failure looks different though: a missing child leaves a working C++ instance instead of an empty slot. There is also no flag to forget to set in a new Blueprint&rsquo;s defaults.</p>
<p>I use this variant in a production subsystem, and I have tested it in PIE. With the Blueprint child present, exactly one instance is alive; with the child removed, the C++ instance is. I have not tested it in a packaged build, so that behavior remains an inference from the mechanism rather than an observed result.</p>
<p>There are two limits, and both follow directly from this implementation.</p>
<p><strong>Two children mean two instances.</strong> Each of them sees zero descendants and each concludes that it is the most-derived one. This case cannot be resolved inside the method, which leaves a convention: exactly one Blueprint child per class.</p>
<p><strong>A single permanently loaded C++ subclass disables the base class for good.</strong> A test class in an Editor module is enough. It is compiled, so it sits in memory from startup, so the parent yields to it in every editor session. In practice this means a test module must not derive from such a subsystem, and the test double has to be built on a plain <code>UObject</code> acting as a listener. This trap is worse than two Blueprint children, because nothing about it is visible in the assets. It lives in test code that nobody associates with runtime.</p>
<h2 id="common-mistakes">Common mistakes</h2>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Mistake</th>
        <th>Effect</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><code>Super::ShouldCreateSubsystem(Outer);</code> on its own line, result discarded</td>
        <td>The parent condition does nothing; in <code>UWorldSubsystem</code> the subsystem enters editor worlds</td>
      </tr>
      <tr>
        <td><code>EditAnywhere</code> instead of <code>EditDefaultsOnly</code> on the flag</td>
        <td>Implies per-instance editing, while the value is read from the CDO and nowhere else</td>
      </tr>
      <tr>
        <td>A Blueprint child with no overridden condition on the C++ side</td>
        <td>Two live instances, split state, and <code>GetSubsystem&lt;T&gt;()</code> returning the first hit in the array</td>
      </tr>
      <tr>
        <td>The flag set to <code>false</code> and a child that never loads in time</td>
        <td>Zero instances — <a href="/en/topics/unreal-engine/gameplay-framework/blueprint-subsystem-missing-in-packaged-build">a separate case, covered here</a></td>
      </tr>
    </tbody>
  </table>
</div>
<p>The last of those cannot be reproduced in the editor. It only surfaces in a packaged build.</p>
<h2 id="verification">Verification</h2>
<p>The number and classes of live instances, in one console command:</p>
<div class="highlight" data-lang="text">
  <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-text" data-lang="text"><span class="line"><span class="cl">obj list class=MySubsystem</span></span></code></pre></div></div>
<p>A query for the base class covers derived classes, so it shows the parent and the children at once, broken down by class. Two rows instead of one reveal the duplication described in the table above; a row for a <code>_C</code> class means the switch worked and the Blueprint implementation is the one running.</p>
<p>Each CDO decision is in the log once the category’s log level is raised:</p>
<div class="highlight" data-lang="text">
  <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-text" data-lang="text"><span class="line"><span class="cl">-LogCmds=&#34;LogSubsystemCollection VeryVerbose&#34;</span></span></code></pre></div></div>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line ue-log__line--verbose"><span class="ue-log__cat">LogSubsystemCollection</span>: <span class="ue-log__sev ue-log__sev--veryverbose">VeryVerbose</span>: <span class="ue-log__msg">Subsystem does not exist, but CDO choose to not create (MySubsystem)</span>
</span></code></pre>
</div>
<p>That line means the class was on the list and refused. Its <strong>absence</strong>, together with a missing instance, means something entirely different: the class was not on the list, so nobody asked it. Telling those two states apart is the only way to distinguish a working switch from a class that never loaded. At the default log level both of them look like silence, which is why the category’s log level has to be raised first.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A Blueprint subsystem does not get created in a packaged build</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/blueprint-subsystem-missing-in-packaged-build/</link>
      <pubDate>Tue, 11 Aug 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/blueprint-subsystem-missing-in-packaged-build/</guid>
      <description>The subsystem collection scans for subclasses once, when it is initialized, and sees only classes already in memory. In a packaged build, a Blueprint class that has not been loaded by then is missed entirely: its CDO is never asked whether to create an instance. The Blueprint logic never runs, and the missing class produces no error or log entry. In the editor, the Content Browser can keep the class loaded and hide the problem.</description>
      <content:encoded><![CDATA[<p>A Blueprint child of a subsystem can go an entire session in a packaged build without ever being created. Nothing is wrong with the code. It is the same code that worked in PIE a minute earlier. The problem lies earlier in the sequence: by the time the engine looks for subclasses, the Blueprint class is not in memory, so it never makes the list and its CDO is never queried.</p>
<h2 id="the-symptom-nothing-to-look-for">The symptom: nothing to look for</h2>
<p>No compile errors. No warnings. No lines in the log. <code>GetSubsystem&lt;T&gt;()</code> does not return <code>nullptr</code> either, because as long as the parent C++ class does not refuse to be created, an instance is created normally. What gets created is the parent. The Blueprint implementation that was supposed to replace it never runs, and the pointer looks perfectly valid.</p>
<p>There is a harsher variant, and it happens when the parent refuses on purpose. That is the case in <a href="/en/topics/unreal-engine/gameplay-framework/should-create-subsystem-picks-the-class">the pattern where the C++ class returns <code>false</code> to make room for a Blueprint implementation</a>. Then <strong>nothing</strong> is created at all: the parent refused, and nobody asked the child.</p>
<h2 id="two-runs-that-settle-it">Two runs that settle it</h2>
<p>The test project uses UE 5.7. It contains <code>UMyTestSubsystem : UGameInstanceSubsystem</code>, marked <code>UCLASS(Blueprintable)</code>, which logs a single line in <code>Initialize</code>. It also contains a Blueprint child, <code>BP_MyTestSubsystem</code>, with a <code>PrintString</code> in its graph. The build is packaged in the Development configuration.</p>
<p>First run, with nothing in the project referencing the Blueprint class:</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line ue-log__line--verbose"><span class="ue-log__cat">LogSubsystemCollection</span>: <span class="ue-log__sev ue-log__sev--verbose">Verbose</span>: <span class="ue-log__msg">Initializing subsystem collection for BP_GameInstance_Test_C_2147482547 with type GameInstanceSubsystem</span>
</span><span class="ue-log__line ue-log__line--verbose"><span class="ue-log__cat">LogSubsystemCollection</span>: <span class="ue-log__sev ue-log__sev--veryverbose">VeryVerbose</span>: <span class="ue-log__msg">Subsystem does not exist, but CDO choose to not create (MyTestSubsystem)</span>
</span></code></pre>
</div>
<p>The parent made it onto the list and refused. The child is not on that list at all. There is no second <code>choose to not create</code> line, and no line from <code>Initialize</code> either, which the child inherits from C++ and would have run had it been created. Counting objects confirms this independently:</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line">Obj List: class=MyTestSubsystem
</span><span class="ue-log__line">Objects:
</span><span class="ue-log__line">
</span><span class="ue-log__line">0 Objects (Total: 0.000M / Max: 0.000M / Res: 0.000M | ...)
</span></code></pre>
</div>
<p>Second run, after adding a single variable of type <code>MyTestSubsystem Class Reference</code> to the GameInstance class, with its default value pointing at the Blueprint child. The variable is never used anywhere. It exists for one reason: so that the asset holds a reference.</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line">                    Class    Count
</span><span class="ue-log__line">     BP_MyTestSubsystem_C        1
</span><span class="ue-log__line">
</span><span class="ue-log__line">1 Objects
</span></code></pre>
</div>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogTemp</span>: <span class="ue-log__msg">UMyTestSubsystem initialized.</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[None] PostInitialize(): Test Subsystem (BP_MyTestSubsystem)</span>
</span></code></pre>
</div>
<p>The instance exists, it is of the Blueprint class, and both the inherited C++ <code>Initialize</code> method and the Blueprint graph ran. The only difference between the two runs is an unused variable.</p>
<h2 id="where-it-disappears-the-scan-happens-once">Where it disappears: the scan happens once</h2>
<p><code>FSubsystemCollectionBase::Initialize</code> builds the class list in a single call, at the moment the collection is created (<code>Engine/Private/Subsystems/SubsystemCollection.cpp:209</code>):</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="n">TArray</span><span class="o">&lt;</span><span class="n">UClass</span><span class="o">*&gt;</span> <span class="n">SubsystemClasses</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">GetDerivedClasses</span><span class="p">(</span><span class="n">BaseType</span><span class="p">,</span> <span class="n">SubsystemClasses</span><span class="p">,</span> <span class="nb">true</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> <span class="p">(</span><span class="n">UClass</span><span class="o">*</span> <span class="nl">SubsystemClass</span> <span class="p">:</span> <span class="n">SubsystemClasses</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">AddAndInitializeSubsystem</span><span class="p">(</span><span class="n">SubsystemClass</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>GetDerivedClasses</code> sweeps the class hierarchy <strong>in memory</strong>. A class that nobody has loaded exists only as a file on disk: the asset registry knows about it, the type system does not. Since it never lands in <code>SubsystemClasses</code>, <code>AddAndInitializeSubsystem</code> never receives it, and <code>ShouldCreateSubsystem</code> has nothing to run on. A class missing from the scan produces no CDO decision entry. A class that is found but refuses creation produces the <code>CDO choose to not create</code> line at <code>VeryVerbose</code> verbosity.</p>
<p>That loop is in fact the <code>else</code> branch. The first branch handles a completely different case (<code>SubsystemCollection.cpp:194</code>):</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="k">if</span> <span class="p">(</span><span class="n">BaseType</span><span class="o">-&gt;</span><span class="n">IsChildOf</span><span class="p">(</span><span class="n">UDynamicSubsystem</span><span class="o">::</span><span class="n">StaticClass</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="k">for</span> <span class="p">(</span><span class="k">const</span> <span class="n">TPair</span><span class="o">&lt;</span><span class="n">FName</span><span class="p">,</span> <span class="n">TArray</span><span class="o">&lt;</span><span class="n">TSubclassOf</span><span class="o">&lt;</span><span class="n">UDynamicSubsystem</span><span class="o">&gt;&gt;&gt;&amp;</span> <span class="nl">SubsystemClasses</span> <span class="p">:</span> <span class="n">GlobalDynamicSystemModuleMap</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// GetDerivedClasses, once
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div></div>
<p><code>UDynamicSubsystem</code> gets incremental registration. <code>FSubsystemModuleWatcher</code> listens for module loads and calls <code>AddAllInstances</code> for classes that have just appeared (<code>SubsystemCollection.cpp:524</code>). <code>UGameInstanceSubsystem</code> does not belong to that branch. For it, the scan runs once and nothing repeats it, so a class loaded one second after the collection is initialized is too late for that collection.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  Timing is the only criterion here. One thing matters: whether the class is in memory <strong>before</strong> the subsystem collection is created. Having the asset in the pak does nothing on its own. An asset that is packaged but referenced by nothing is, as far as this scan is concerned, absent.
</div>

<h2 id="why-the-editor-can-hide-this">Why the editor can hide this</h2>
<p>In the editor, the Content Browser keeps the Blueprint class in memory. Opening the asset once is enough, and so is having it sit in a visible folder. By the time PIE starts, the class is already loaded, <code>GetDerivedClasses</code> sees it, and everything behaves exactly as designed.</p>
<p>In a real project the packaged build usually works too, but for a different reason: the reference exists by accident. A <code>Get Subsystem</code> node with the Blueprint class selected embeds a hard reference in the Blueprint that calls it, and that Blueprint ships with the level or with the GameInstance class. A reference from a Blueprint hides the problem if that Blueprint loads the subsystem class <strong>before</strong> the collection is initialized.</p>
<p>That is the whole trap. The mechanism breaks in exactly the place nobody tests it: when the subsystem is called from C++ and nowhere else.</p>
<h2 id="the-fix">The fix</h2>
<p>Anything that loads the class before the collection is initialized. The cheapest option, and the one that does not disappear when somebody rewires a graph, is a variable of type <code>&lt;Subsystem&gt; Class Reference</code> on an object that loads early, with its default value set to the Blueprint class. The GameInstance class is the natural place for the variable, because the GameInstance itself is created before its subsystem collection.</p>
<p>The variable does not have to be used for anything. What matters is that the GameInstance asset holds a hard reference to the Blueprint class, so loading one pulls in the other.</p>
<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  Leave a comment on the variable where it is declared. An unused <code>Class Reference</code> variable looks like a leftover from a refactor and is the first thing anyone deletes during cleanup, and deleting it breaks nothing in the editor.
</div>

<p>Getting the class loaded in time is the only real fix. How badly the failure hurts when nothing loads it is a separate question, and the answer depends on how the C++ class steps aside for the Blueprint. Overriding <code>ShouldCreateSubsystem</code> with a condition that checks for subclasses, rather than with a flag set in the Blueprint defaults, turns “zero instances” into “a C++ instance without the Blueprint logic”: a reduced system rather than a missing one. I wrote up the mechanism itself, and its limits, 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="checking-this-in-your-own-project">Checking this in your own project</h2>
<div class="section-panel section-panel--checklist">
  <div class="section-panel__label">Checklist</div>
  
<p>What to check when a subsystem works in PIE and its behavior in a packaged build is unknown:</p>
<ul class="checklist">
  <li>Package in the <strong>Development</strong> configuration rather than Shipping. A Shipping build compiles <code>Verbose</code> and <code>VeryVerbose</code> out, so raising the log level cannot bring them back.</li>
  <li>Run it with <code>&lt;Game&gt;.exe -log "-ExecCmds=obj list class=&lt;Subsystem&gt;"</code>. A query for the base class covers derived classes too, which is visible in the second run above: the answer to a question about the C++ class is an instance of the Blueprint class.</li>
  <li>Check the <strong>class</strong>, not only the object count. <code>1 Objects</code> of the parent class while a child exists is the same failure, only quieter.</li>
  <li>For each CDO decision separately, use <code>-LogCmds="LogSubsystemCollection VeryVerbose"</code>. A <code>CDO choose to not create</code> line means “asked and refused.” Its absence, alongside a missing instance, means “never asked.”</li>
</ul>

</div>

<p>Check two things separately: whether the instance exists, and whether the log contains a CDO decision for that class. A missing instance together with a missing CDO decision points to loading as the source of the problem. Neither observation is enough on its own.</p>
<h2 id="when-this-matters">When this matters</h2>
<p>As long as a Blueprint reference gets the class loaded in time, the trap stays asleep. The reference appears on its own and nobody finds out it was ever needed. I reproduced it in two setups: one where the subsystem serves C++ code only, and one where a Blueprint child was added to override a few default values and nothing called it afterwards.</p>
<p>A third setup follows from the mechanism, but I did not reproduce it. Removing the last <code>Get Subsystem</code> call from Blueprints during a refactor can remove the only reference to the class. If that happens, this is the worst of the three cases, because nothing obviously connects the regression to the commit that triggered it. A node disappears from one Blueprint, the subsystem stops existing in the packaged game, and everything still works in the editor.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Movie files in Content/Movies stay outside the pak — the two fields that change it</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/movies-staged-outside-pak/</link>
      <pubDate>Tue, 04 Aug 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/movies-staged-outside-pak/</guid>
      <description>By default, staging copies files from Content/Movies outside the pak as NonUFS, which is why .mp4 files sit loose in the build directory. Turning on bSkipMovies and listing the file under UFSMovies pulls it inside, and playback through UMediaPlayer with FileMediaSource keeps working: WmfMedia on Windows reads through IFileManager, which can access files inside the pak.</description>
      <content:encoded><![CDATA[<p>Package a project, and the <code>.mp4</code> files from <code>Content/Movies</code> end up loose in the build directory, sitting next to the <code>.pak</code> and playable in any video player. That is not a staging oversight. It is the default behavior, written into the staging script. Two fields in Project Settings reverse it, and playback through <code>UMediaPlayer</code> using <code>WmfMedia</code> on Windows keeps working without a single edit to game code.</p>
<h2 id="the-default-is-nonufs-which-means-outside-the-pak">The default is NonUFS, which means outside the pak</h2>
<p>The rule lives in the staging script, not in engine code. <code>Content/Movies</code> is walked recursively: anything that is not a <code>.uasset</code> or a <code>.umap</code> gets staged, and the engine&rsquo;s own movies in <code>Engine/Content/Movies</code> go along with the project&rsquo;s. The staged type is <code>NonUFS</code> in every scenario except when using a file server (<code>CopyBuildToStagingDirectory.Automation.cs:1982-2011</code>). A comment a few dozen lines above leaves no room for interpretation (<code>:1971</code>):</p>
<div class="highlight" data-lang="csharp">
  <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-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="c1">// NonUFS files are never in pak files and should always be remapped</span></span></span></code></pre></div></div>
<p>A <code>NonUFS</code> file is one that staging copies into the output directory and that the pak knows nothing about. Hence the loose <code>.mp4</code> files in the build. Packing them was always possible; nobody ever told staging to put them inside the pak.</p>
<h2 id="the-two-fields-that-reverse-it">The two fields that reverse it</h2>
<p>With <code>bSkipMovies</code> on, the script takes a different branch (<code>:2013</code>), where two explicit lists of movie names decide the staged type instead. In the editor they sit under Project Settings → Packaging → Advanced.</p>
<figure class="prose-figure prose-figure--medium">
  <img src="/screens/packaging-movies-settings.png" alt="The Project Settings panel in the Packaging → Advanced section, showing the labels of three fields: Exclude movie files when staging, Specific movies to Package, and Specific movies to Copy" loading="lazy" /></figure>
<p>The field names hide what they do:</p>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Field in the editor</th>
        <th><code>.ini</code> key</th>
        <th>Effect</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Exclude movie files when staging</td>
        <td><code>bSkipMovies</code></td>
        <td>switches modes: from “everything as NonUFS” to “only what is listed”</td>
      </tr>
      <tr>
        <td>Specific movies to Package</td>
        <td><code>UFSMovies</code></td>
        <td>the file goes <strong>into the pak</strong></td>
      </tr>
      <tr>
        <td>Specific movies to Copy</td>
        <td><code>NonUFSMovies</code></td>
        <td>the file goes next to the pak, as before</td>
      </tr>
    </tbody>
  </table>
</div>
<p>The description for <code>UFSMovies</code> states this explicitly: <code>will still be added to the .pak file</code> (<code>ProjectPackagingSettings.h:513</code>). In <code>DefaultGame.ini</code> it looks like this:</p>
<div class="highlight" data-lang="ini">
  <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-ini" data-lang="ini"><span class="line"><span class="cl"><span class="k">[/Script/UnrealEd.ProjectPackagingSettings]</span>
</span></span><span class="line"><span class="cl"><span class="na">bSkipMovies</span><span class="o">=</span><span class="s">True</span>
</span></span><span class="line"><span class="cl"><span class="na">+UFSMovies</span><span class="o">=</span><span class="s">Intro</span></span></span></code></pre></div></div>
<h2 id="why-playback-keeps-working">Why playback keeps working</h2>
<p>A movie inside the pak stops being a file the operating system can see. Only code that reads through <code>IPlatformFile</code> can reach it, and the pak is mounted there as another layer of the virtual file system.</p>
<p>Media Framework reads exactly that way. <code>UFileMediaSource</code> hands the path over as a URL with the <code>file://</code> scheme (<code>FileMediaSource.cpp:169</code>). <code>WmfMedia</code>, the player enabled by default on Windows, always opens such a URL through <code>IFileManager</code> and then passes the resulting byte stream to Media Foundation (<code>WmfMediaUtils.cpp:1285-1305</code>, <code>:1341</code>). Nothing in that chain needs the file on disk under its own name.</p>
<p>I checked this in my own build: an <code>.mp4</code> packaged inside the pak through <code>UFSMovies</code> kept playing at runtime through <code>UMediaPlayer</code> with <code>FileMediaSource</code>.</p>
<h2 id="when-this-matters">When this matters</h2>
<p>With one movie, the benefit is a cleaner build directory: the file stops sitting in plain view, and the output directory holds one archive instead of scattered assets alongside it. With a dozen or more it becomes the difference between a build you can scan at a glance and a file list you have to keep track of every time you distribute the build.</p>
<p>Verify this in a packaged build, not in PIE. In the editor every movie sits on disk, so the staging settings have no effect there at all.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A tickable UObject — how to build one, what to keep in mind</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/tickable-uobject/</link>
      <pubDate>Thu, 30 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/tickable-uobject/</guid>
      <description>How to build a base class for a ticking UObject in a UE project: the design decisions that matter, with the complete class available in StillCooking_Tools.</description>
      <content:encoded><![CDATA[<p>A <code>UObject</code> does not tick. It has no <code>PrimaryActorTick</code>, it belongs to no tick group, and the engine has no reason to visit it every frame. The standard answer is multiple inheritance: <code>UObject</code> plus <code>FTickableGameObject</code>. The catch is that inheritance alone does not give you a tick, only registration. Everything that determines <em>whether</em>, <em>when</em>, and <em>for how long</em> the object actually ticks is still yours to set up.</p>
<h2 id="what-ftickablegameobject-is">What <code>FTickableGameObject</code> is</h2>
<p><code>FTickableGameObject</code> is a plain C++ class from <code>Tickable.h</code> that you inherit from alongside <code>UObject</code>. Its constructor adds <code>this</code> to a pending queue (<code>Tickable.cpp:133-145</code>), and the object only moves into the real array at the next tick pass (<code>Tickable.cpp:67-95</code>). The destructor removes it from that array (<code>Tickable.cpp:147-152</code>), and the engine walks the array once per frame (<code>Tickable.cpp:167-208</code>). This is a separate track running alongside the <code>FTickFunction</code> machinery that actors and components use. It is not part of it.</p>
<p>The upside is that the object does not have to be in a world, does not have to be spawned, and has no transform. The price is that it also gets none of what the other track gives actors: no tick groups, no <code>AddTickPrerequisiteActor</code>, no <code>TickInterval</code>. The entire class declaration (<code>Tickable.h:134-210</code>) does not contain a single one of them. You either give up all three or build them by hand.</p>
<p>For an object tied to a world, the call sits in <code>UWorld::Tick</code> (<code>LevelTick.cpp:1792</code>), after <code>TG_PostPhysics</code> (<code>LevelTick.cpp:1749</code>) but before <code>TG_PostUpdateWork</code> (<code>LevelTick.cpp:1848</code>). An actor ticking in <code>TG_PostUpdateWork</code> sees the state this tick has already produced. An object with no world lands somewhere else: it ticks only after the loop over all worlds. The engine says so outright in the comment on the method (<code>Tickable.h:183</code>), and the call itself sits in <code>GameEngine.cpp:1947</code>.</p>
<h2 id="when-to-reach-for-this-and-when-not-to">When to reach for this, and when not to</h2>
<p><strong>Does this have to be an actor?</strong> If the object needs a transform, collision, replication, placement in a level, or tick groups, the answer is “actor” and the rest of this post does not apply. None of those can be bolted onto <code>FTickableGameObject</code>.</p>
<p><strong>Is the logic per-frame or event-driven?</strong> If it comes down to “in three seconds” or “every half second” and nothing in between, that is <code>FTimerManager</code>, not a tick. Polling state sixty times a second to respond once is a cost with no return.</p>
<p><strong>Should the object live exactly as long as the world?</strong> If so, the right answer is almost always <code>UTickableWorldSubsystem</code>: a ready, tested lifecycle, without a single line of what I describe below. Your own base class earns its place when you need <strong>multiple instances</strong> (a subsystem is one instance per class per owner by definition), or when a designer is meant to create subclasses in Blueprint, which a subsystem cannot do.</p>
<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  <code>UTickableWorldSubsystem</code> is the best possible source on this: it is Epic&rsquo;s own implementation of the pattern. The whole class fits in <code>WorldSubsystem.cpp:97-160</code>.
</div>

<h2 id="decision-one-when-you-register">Decision one: when you register</h2>
<p>The default answer: <strong>in the constructor</strong>. Inheritance is enough. The base constructor runs on its own, the object lands in the registry, no extra code.</p>
<p>And that is exactly what you must not do. The constructor documentation says so outright:</p>
<blockquote>
<p>If this is something like a UObject that could be created on a different thread (like for async loading), construct with a Never tick type and enable tick later.</p>
<p>— <code>Tickable.h:147</code> (UE 5.7)</p>
</blockquote>
<p>The base constructor in full:</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="c1">// UE 5.7 — Tickable.cpp:133-145
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">FTickableGameObject</span><span class="o">::</span><span class="n">FTickableGameObject</span><span class="p">(</span><span class="n">ETickableTickType</span> <span class="n">StartingTickType</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="k">if</span> <span class="p">(</span><span class="n">StartingTickType</span> <span class="o">!=</span> <span class="n">ETickableTickType</span><span class="o">::</span><span class="n">Never</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="c1">// It is only safe to create tickable game objects on the game thread, as otherwise there is a race condition between object initialize and the game thread tick
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="c1">// If you hit this ensure, change the constructor to use FTickableGameObject(ETickableTickType::Never) and call SetTickableTickType after initialization
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">ensure</span><span class="p">(</span><span class="n">IsInGameThread</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="c1">// Queue for creation, this can get called very early in startup
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">FTickableStatics</span><span class="o">&amp;</span> <span class="n">Statics</span> <span class="o">=</span> <span class="n">GetStatics</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="n">Statics</span><span class="p">.</span><span class="n">QueueTickableObjectForAdd</span><span class="p">(</span><span class="k">this</span><span class="p">,</span> <span class="n">StartingTickType</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="p">}</span></span></span></code></pre></div></div>
<p>The base constructor runs <strong>before</strong> the body of the derived class constructor, <strong>before</strong> the archetype and Blueprint-class defaults are applied over the native ones, and <strong>before</strong> the owner has a chance to configure anything. Defaults are not applied until <code>FObjectInitializer::PostConstructInit</code> (<code>UObjectGlobals.cpp:4239</code>), the copy itself in <code>UObjectGlobals.cpp:4350</code>, and <code>PostInitProperties</code> runs later still (<code>UObjectGlobals.cpp:4427</code>) — all three after the C++ constructor chain. The next tick pass will call <code>GetTickableTickType()</code>, and then <code>Tick()</code>, on a half-configured object. On top of that, the CDO registers along with the instances, because the <code>FTickableGameObject</code> constructor does not check <code>IsTemplate()</code> or anything of the sort (<code>Tickable.cpp:133-145</code>).</p>
<p>The correct answer is to construct with an explicit “do not tick” and enable ticking later:</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="n">UTickableObject</span><span class="o">::</span><span class="n">UTickableObject</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">:</span> <span class="n">FTickableGameObject</span><span class="p">(</span><span class="n">ETickableTickType</span><span class="o">::</span><span class="n">Never</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="c1">// Deliberately empty. Registering for tick here is exactly what Tickable.h forbids.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span></span></span></code></pre></div></div>
<h2 id="decision-two-when-you-unregister">Decision two: when you unregister</h2>
<p>The default answer: <strong>in the destructor</strong>. And again it is too late. A <code>UObject</code> destructor runs long after the object stopped being useful, and in the meantime the engine is free to tick it.</p>
<p>The right place is an explicit method called by the owner, plus a hard gate in <code>BeginDestroy()</code> as the last line of defense. The tick queries themselves are gates too, each in its own place:</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="n">ETickableTickType</span> <span class="n">UTickableObject</span><span class="o">::</span><span class="n">GetTickableTickType</span><span class="p">()</span> <span class="k">const</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Never for the CDO and before Initialize: the object stays out of the tickable
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// array entirely instead of sitting in it and being polled every frame.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="p">(</span><span class="n">IsTemplate</span><span class="p">()</span> <span class="o">||</span> <span class="o">!</span><span class="n">bInitialized</span><span class="p">)</span> <span class="o">?</span> <span class="n">ETickableTickType</span><span class="o">::</span><span class="nl">Never</span> <span class="p">:</span> <span class="n">ETickableTickType</span><span class="o">::</span><span class="n">Conditional</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">bool</span> <span class="n">UTickableObject</span><span class="o">::</span><span class="n">IsTickable</span><span class="p">()</span> <span class="k">const</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">bInitialized</span> <span class="o">&amp;&amp;</span> <span class="n">bTickEnabled</span> <span class="o">&amp;&amp;</span> <span class="n">CachedWorld</span><span class="p">.</span><span class="n">IsValid</span><span class="p">()</span> <span class="o">&amp;&amp;</span> <span class="n">IsValidChecked</span><span class="p">(</span><span class="k">this</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The split between these two methods is deliberate. <code>IsTemplate()</code> belongs in <code>GetTickableTickType</code>, not in <code>IsTickable</code>: that way the CDO never enters the array at all, instead of sitting in it and answering “no” every frame. <code>IsTickable</code> is left for the conditions that genuinely change over the object&rsquo;s lifetime.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  <p>Implement <code>DisableTick()</code> as <code>SetTickableTickType(ETickableTickType::Never)</code>, a real removal from the array, rather than as a flag read in <code>IsTickable</code>. It comes at one cost worth remembering: re-enabling takes effect from the next tick pass, because <code>SetTickableTickType</code> adds the object to a pending queue (<code>Tickable.cpp:59-63</code>) that the engine drains at the start of the following pass (<code>Tickable.cpp:67-95</code>). An object enabled from inside <code>Tick()</code> does not tick a second time in the same frame.</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="c1">// UE 5.7 — Tickable.cpp:59-63, indentation reduced (branch for an object not yet in the array)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">else</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Add to the pending list (which could override previous request), this will apply it next frame
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">NewTickableObjects</span><span class="p">.</span><span class="n">Add</span><span class="p">(</span><span class="n">InTickable</span><span class="p">,</span> <span class="n">NewTickType</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
</div>

<h2 id="decision-three-which-world-you-belong-to">Decision three: which world you belong to</h2>
<p>The default answer: <strong>none</strong>. Left unoverridden, <code>GetTickableGameObjectWorld()</code> returns <code>nullptr</code> (<code>Tickable.h:187-190</code>), and the object ticks in the global pass that follows all the worlds — knowing nothing about pause, nothing about PIE, nothing about the moment its world stops existing.</p>
<p>Overriding that method is cheap and handles all three at once:</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="n">UWorld</span><span class="o">*</span> <span class="n">UTickableObject</span><span class="o">::</span><span class="n">GetTickableGameObjectWorld</span><span class="p">()</span> <span class="k">const</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">CachedWorld</span><span class="p">.</span><span class="n">Get</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>I resolve the world once, in <code>Initialize()</code>, and hold it in a <code>TWeakObjectPtr&lt;UWorld&gt;</code>. Plain <code>GetWorld()</code> is enough: the default <code>UObject::GetWorld()</code> implementation walks the Outer chain (<code>Obj.cpp:1145-1150</code>), so an object created with a sensible Outer finds its world with no help at all.</p>
<p>Here is the trap I ran into. Once the world is gone, <code>CachedWorld.Get()</code> starts returning <code>nullptr</code>. You would expect that to be the end of the tick. The opposite is true. The gate in the engine reads <code>GetTickableGameObjectWorld() == World</code> (<code>Tickable.cpp:189</code>), and <code>TickObjects</code> is <strong>also</strong> called with <code>World == nullptr</code>, precisely for objects with no world (<code>GameEngine.cpp:1947</code>). The comparison <code>nullptr == nullptr</code> passes. The object does not stop ticking. It quietly <strong>migrates</strong> from its own world&rsquo;s tick to the global engine pass and keeps going.</p>
<p>The whole condition in the engine loop:</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="c1">// UE 5.7 — Tickable.cpp:186-189 (indentation reduced)
</span></span></span><span class="line"><span class="cl"><span class="c1">// If it is tickable and in this world
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">if</span> <span class="p">(</span><span class="n">TickableObject</span><span class="o">-&gt;</span><span class="n">IsAllowedToTick</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;&amp;</span> <span class="p">((</span><span class="n">TickableEntry</span><span class="p">.</span><span class="n">TickType</span> <span class="o">==</span> <span class="n">ETickableTickType</span><span class="o">::</span><span class="n">Always</span><span class="p">)</span> <span class="o">||</span> <span class="n">TickableObject</span><span class="o">-&gt;</span><span class="n">IsTickable</span><span class="p">())</span>
</span></span><span class="line"><span class="cl">    <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">TickableObject</span><span class="o">-&gt;</span><span class="n">GetTickableGameObjectWorld</span><span class="p">()</span> <span class="o">==</span> <span class="n">World</span><span class="p">))</span></span></span></code></pre></div></div>
<p>Closing this takes two things at once: the <code>CachedWorld.IsValid()</code> condition in <code>IsTickable()</code>, and a subscription to world cleanup, so that the object shuts itself down instead of just no longer being polled.</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">UTickableObject</span><span class="o">::</span><span class="n">HandleWorldCleanup</span><span class="p">(</span><span class="n">UWorld</span><span class="o">*</span> <span class="n">World</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">bSessionEnded</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">bCleanupResources</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="k">if</span> <span class="p">(</span><span class="n">World</span> <span class="o">==</span> <span class="n">CachedWorld</span><span class="p">.</span><span class="n">Get</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">Shutdown</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="p">}</span></span></span></code></pre></div></div>
<p>The delegate is <code>FWorldDelegates::OnWorldCleanup</code>, hooked up in <code>Initialize()</code> and removed in <code>Shutdown()</code>. Unregistering from inside your own broadcast is safe: Unreal&rsquo;s multicast delegates defer compacting the list until the call finishes (<code>MulticastDelegateBase.h:380-390</code>).</p>
<h2 id="decision-four-who-cleans-up">Decision four: who cleans up</h2>
<p>The default answer: <strong>nobody</strong>. A <code>UObject</code> with no <code>UPROPERTY</code> reference is collected at the next GC, and that is the correct behavior. It just has to be handled deliberately, instead of discovered halfway through a session when the object is already gone.</p>
<p>The reflex is <code>AddToRoot()</code>. That is not lifetime management; it is turning GC off for this object, with a manual <code>RemoveFromRoot()</code> as the only way out. The object survives a map change and everything else.</p>
<p>A sensible contract is simpler and puts both obligations on the owner: the object lives in a <code>UPROPERTY(TObjectPtr&lt;&gt;)</code>, and <code>Shutdown()</code> runs before that reference is dropped.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  <p><strong>Teardown does not belong in <code>BeginDestroy()</code>.</strong> It is tempting to hook it there — it looks like a fair way to close the lifecycle, one that saves a forgetful owner. It blows up Blueprint subclasses.</p>
<p>On the GC path the object already has the <code>Unreachable</code> flag set (<code>GarbageCollection.cpp:5264</code>, before <code>ConditionalBeginDestroy</code> is even called in <code>GarbageCollection.cpp:6155</code>), and a <code>BlueprintNativeEvent</code> overridden in Blueprint dispatches through <code>UObject::ProcessEvent</code>, which opens with <code>checkf(!IsUnreachable(), ...)</code> (<code>ScriptCore.cpp:2015-2020</code>).</p>
<p>The result: the first Blueprint subclass to implement the teardown event crashes the editor during an ordinary garbage collection. In a Shipping build the <code>check</code> is gone, so instead of a crash you get a silently skipped teardown — behavior that differs by build configuration, which is harder still to catch.</p>

</div>

<p>So <code>BeginDestroy()</code> does exactly as much as it has to and not an ounce more: it removes the delegate (a plain <code>Remove</code>, no dispatch into Blueprint), kills the tick, and reports that the owner never called <code>Shutdown()</code>.</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">UTickableObject</span><span class="o">::</span><span class="n">BeginDestroy</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">FWorldDelegates</span><span class="o">::</span><span class="n">OnWorldCleanup</span><span class="p">.</span><span class="n">Remove</span><span class="p">(</span><span class="n">WorldCleanupHandle</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">WorldCleanupHandle</span><span class="p">.</span><span class="n">Reset</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">SetTickableTickType</span><span class="p">(</span><span class="n">ETickableTickType</span><span class="o">::</span><span class="n">Never</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">bTickEnabled</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">ensureAlwaysMsgf</span><span class="p">(</span><span class="o">!</span><span class="n">bInitialized</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;%s: destroyed while still initialized - the owner never called Shutdown().&#34;</span><span class="p">),</span> <span class="o">*</span><span class="n">GetName</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">Super</span><span class="o">::</span><span class="n">BeginDestroy</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>ensureAlwaysMsgf</code>, not <code>ensureMsgf</code>: a plain <code>ensure</code> fires once per callsite per process. The <code>bEnsureHasExecuted</code> flag is keyed by a hash of <code>__FILE__</code> and <code>__LINE__</code>, and the <code>Always</code> variant skips that filter (<code>AssertionMacros.h:440-448</code>). The first leaked object would silence the diagnostic for every one after it.</p>
<p>Epic does the same in <code>UTickableWorldSubsystem::BeginDestroy</code> (<code>WorldSubsystem.cpp:154-159</code>): it does not call <code>Deinitialize</code> from there — and now the reason is clear. Two differences from the listing above are deliberate: Epic leaves a plain <code>ensureMsgf</code> there (<code>WorldSubsystem.cpp:158</code>) and calls <code>Super::BeginDestroy()</code> first (<code>WorldSubsystem.cpp:156</code>).</p>
<h2 id="the-whole-class">The whole class</h2>
<p>The complete implementation ships in <a href="/en/products/stillcooking-tools/">StillCooking_Tools</a> as
<code>USCTickableObject</code> — a free, MIT-licensed plugin distributed as C++ source
(<a href="https://github.com/StillCooking/StillCooking_Tools" data-external rel="noopener" target="_blank">GitHub</a>), module <code>StillCookingCore</code>, header
<code>Objects/SCTickableObject.h</code>. It is the class from this post, developed further: on top of the four
decisions above it adds an explicit tick intent that <code>Initialize()</code> applies, the <code>bTickWhenPaused</code>
and <code>bTickInEditor</code> gates, and a <code>protected</code> engine-facing interface with <code>IsTickable()</code> marked
<code>final</code>.</p>
<ul>
<li><a href="/en/products/stillcooking-tools/docs/reference/tickable-object/">Reference: <code>USCTickableObject</code></a> —
functions, properties, events and the messages it logs.</li>
<li><a href="/en/products/stillcooking-tools/docs/concepts/lifecycle/">Lifecycle</a> — the ownership contract and
what happens when the owner forgets <code>Shutdown()</code>.</li>
</ul>
<h2 id="checklist">Checklist</h2>
<p>Five things to check in your own class:</p>
<ol>
<li>The constructor calls <code>FTickableGameObject(ETickableTickType::Never)</code> and does <strong>nothing else</strong> tick-related.</li>
<li><code>GetTickableTickType()</code> returns <code>Never</code> for <code>IsTemplate()</code> and for the pre-initialization state, so the CDO never enters the array.</li>
<li><code>GetTickableGameObjectWorld()</code> returns a real world, and <code>IsTickable()</code> checks whether that world is still alive.</li>
<li>There is an explicit shutdown method called by the owner, plus a world-cleanup subscription for the case where the world goes first.</li>
<li><code>BeginDestroy()</code> kills the tick and reports the problem, but does <strong>not</strong> run subclass teardown.</li>
</ol>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A dispatcher broadcast from inside a handler does not see later bindings</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/event-dispatcher-bind-order/</link>
      <pubDate>Tue, 28 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/event-dispatcher-bind-order/</guid>
      <description>A Blueprint Event Dispatcher calls its bindings in registration order unless removals have reordered the list: ProcessMulticastDelegate iterates a copy from front to back. When one handler broadcasts another dispatcher, only bindings already registered with that dispatcher are included. Bindings added by later handlers do not exist yet. In this example, registration order follows from the lifecycle phases in which the Bind Event to&amp;hellip; nodes run across different Blueprints.</description>
      <content:encoded><![CDATA[<p>The dispatcher <code>OnDelegate_1</code> has two bindings. The first handler broadcasts <code>OnDelegate_2</code> while it runs. The second handler binds <code>CustomEvent_3</code> to <code>OnDelegate_2</code>, but by then the broadcast is already over.</p>
<p>The cause is not a race condition. A dispatcher broadcast is a synchronous loop over an array: one thread, one frame, the same result every time. The call order of the bindings is deterministic, and relying on it is legitimate, provided you know where that order comes from and what changes it. It comes from the lifecycle phase in which each <code>Bind Event to…</code> node ran.</p>
<h2 id="the-test-setup">The test setup</h2>
<p>UE 5.7, pure Blueprint, two separate Blueprints. <code>BP_GameInstance_Test</code> owns both dispatchers and <code>CustomEvent_1</code>, which is bound in <code>Init</code>:</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-order-a-gameinstance.txt" data-bp-height="400"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-order-a-gameinstance.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">BP_GameInstance_Test: Init binds CustomEvent_1 to OnDelegate_1. CustomEvent_1 itself logs [1] and then broadcasts OnDelegate_2.</figcaption></figure>
<p>The actor <code>BP_DispatcherTest</code> binds to the same <code>OnDelegate_1</code> in <code>BeginPlay</code>, and its handler is what creates the binding on <code>OnDelegate_2</code>:</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-order-a-actor.txt" data-bp-height="900"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-order-a-actor.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">BP_DispatcherTest: BeginPlay binds CustomEvent_2 to OnDelegate_1. CustomEvent_2 logs [2] and only then binds CustomEvent_3 (log [3]) to OnDelegate_2.</figcaption></figure>
<p>A key press in the Level Blueprint fires the broadcast. After calling <code>OnDelegate_1</code>, the graph logs <code>[END TEST]</code>. That marker marks the end of the synchronous execution triggered by the key press, and it makes the log comparisons further down readable:</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-order-a-levelbp.txt" data-bp-height="420"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-order-a-levelbp.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">LVL_DispatcherTest: the key press broadcasts OnDelegate_1 on the GameInstance, then logs [END TEST].</figcaption></figure>
<p>One key press, and the Output Log shows:</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span></code></pre>
</div>
<p><code>[3]</code> never appears. A breakpoint on <code>CustomEvent_3</code> is never hit. There is no compile error, no red node, and no warning. From the engine’s point of view nothing went wrong: the dispatcher called everything that was registered at the moment of the call, and <code>CustomEvent_3</code> was not registered then.</p>
<h2 id="this-is-not-a-race-condition">This is not a race condition</h2>
<p>The symptom suggests a race condition: the binding and the broadcast are competing, and the broadcast wins. That explanation is false and sends the diagnosis in the wrong direction.</p>
<p>Nothing here is racing anything. When <code>CustomEvent_1</code> broadcasts <code>OnDelegate_2</code>, <code>CustomEvent_2</code> has not started running yet. Not because it was too slow, but because it comes later in program order. Run the same project a hundred times and it behaves identically a hundred times.</p>
<p>The distinction has practical consequences. A race condition is something you look for in timing and try to fix with a delay added just in case. A delay does work here, but not for the reason that hypothesis suggests: it doesn&rsquo;t give another thread time to catch up, it moves the <code>OnDelegate_2</code> broadcast past the end of the entire <code>OnDelegate_1</code> handler list. It is worth knowing which of the two you are buying.</p>
<h2 id="proving-the-order-is-deterministic">Proving the order is deterministic</h2>
<p>The setup above shows where the invisible order comes from, but it mixes two variables at once. One Blueprint is enough to isolate it: all three custom events on <code>BP_GameInstance_Test</code>, both bindings to <code>OnDelegate_1</code> on a single exec wire in <code>Init</code>, no actor and no <code>BeginPlay</code>.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-order-b-first.txt" data-bp-height="1080"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-order-b-first.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">Arrangement one: in Init, CustomEvent_1 is bound first, then CustomEvent_2.</figcaption></figure>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span></code></pre>
</div>
<p>Now the only change in the entire project: the exec wire between the two <code>Bind Event to OnDelegate_1</code> nodes is swapped. The nodes, the events, their contents, and the Level Blueprint stay untouched.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-order-b-swapped.txt" data-bp-height="1080"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-order-b-swapped.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">Arrangement two: identical contents, the two Bind Event nodes in reverse order.</figcaption></figure>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[3]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span></code></pre>
</div>
<p><code>CustomEvent_2</code> ran first, bound <code>CustomEvent_3</code> to <code>OnDelegate_2</code>, and only then did <code>CustomEvent_1</code> broadcast that dispatcher. Reordering two nodes on an exec wire changed the result predictably and repeatably. The result follows directly from the order of execution: in one arrangement, registration happens after the broadcast; in the other, it happens before it.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  When you repeat this test, press the key <strong>once per run</strong> and restart PIE between arrangements. A second press in arrangement one does log <code>[3]</code>, because the binding that <code>CustomEvent_2</code> created during the first broadcast is still there. That looks like instability, but it is leftover state from the previous call.
</div>

<h2 id="where-the-order-comes-from">Where the order comes from</h2>
<p>A Blueprint dispatcher is an <code>FMulticastScriptDelegate</code>, and its broadcast lives in <code>ScriptDelegates.h:917</code> (5.7):</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="k">if</span><span class="p">(</span> <span class="n">InvocationList</span><span class="p">.</span><span class="n">Num</span><span class="p">()</span> <span class="o">&gt;</span> <span class="mi">0</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="c1">// Create a copy of the invocation list, just in case the list is modified by one of the callbacks during the broadcast
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">typedef</span> <span class="n">TArray</span><span class="o">&lt;</span> <span class="n">UnicastDelegateType</span><span class="p">,</span> <span class="n">TInlineAllocator</span><span class="o">&lt;</span> <span class="mi">4</span> <span class="o">&gt;</span> <span class="o">&gt;</span> <span class="n">FInlineInvocationList</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">FInlineInvocationList</span> <span class="n">InvocationListCopy</span> <span class="o">=</span> <span class="n">FInlineInvocationList</span><span class="p">(</span><span class="n">InvocationList</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1">// Invoke each bound function
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">for</span><span class="p">(</span> <span class="k">typename</span> <span class="n">FInlineInvocationList</span><span class="o">::</span><span class="n">TConstIterator</span> <span class="n">FunctionIt</span><span class="p">(</span> <span class="n">InvocationListCopy</span> <span class="p">);</span> <span class="n">FunctionIt</span><span class="p">;</span> <span class="o">++</span><span class="n">FunctionIt</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="k">if</span><span class="p">(</span> <span class="n">FunctionIt</span><span class="o">-&gt;</span><span class="n">IsBound</span><span class="p">()</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">FunctionIt</span><span class="o">-&gt;</span><span class="k">template</span> <span class="n">ProcessDelegate</span><span class="o">&lt;</span><span class="n">UObjectTemplate</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Parameters</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="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p><code>TConstIterator</code> runs front to back, and new bindings are appended to the end of the array. Hence the rule: <strong>in a Blueprint dispatcher, first bound is first called.</strong></p>
<p>That leaves the question of why <code>CustomEvent_1</code> runs first. Not the graph: neither of the two Blueprints contains a node that sets the call order. The lifecycle decides. <code>UGameInstance::Init</code> (<code>Private/GameInstance.cpp:129</code>) runs before the map is loaded, and therefore before <code>BeginPlay</code> on any actor in the world. The world startup timeline is laid out separately in <a href="/en/topics/unreal-engine/gameplay-framework/subsystem-lifecycle-init-order">Subsystem and manager actor lifecycles</a>.</p>
<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  In the original two-Blueprint setup, lifecycle phases determine the handler order: the binding in <code>Init</code> runs before the binding in <code>BeginPlay</code>. To trace the order in your own project, find every <code>Bind Event to…</code> node targeting that dispatcher and establish when each one executes, including the execution order within a single phase. That information may be spread across several graphs.
</div>

<h2 id="same-name-opposite-order">Same name, opposite order</h2>
<p>The C++ equivalent iterates the other way, with a comment that says why (<code>MulticastDelegateBase.h:292-293</code>):</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="c1">// call bound functions in reverse order, so we ignore any instances that may be added by callees
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">for</span> <span class="p">(</span><span class="n">int32</span> <span class="n">InvocationListIndex</span> <span class="o">=</span> <span class="n">LocalInvocationList</span><span class="p">.</span><span class="n">Num</span><span class="p">()</span> <span class="o">-</span> <span class="mi">1</span><span class="p">;</span> <span class="n">InvocationListIndex</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">;</span> <span class="o">--</span><span class="n">InvocationListIndex</span><span class="p">)</span></span></span></code></pre></div></div>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th></th>
        <th>Call order</th>
        <th>Binding added during the broadcast</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Blueprint dispatcher (<code>FMulticastScriptDelegate</code>)</td>
        <td>registration order — first bound is called first</td>
        <td>skipped; the list is copied before the loop</td>
      </tr>
      <tr>
        <td>C++ delegate (<code>TMulticastDelegate</code>)</td>
        <td>reverse of registration — last bound is called first</td>
        <td>skipped; the loop runs backwards</td>
      </tr>
    </tbody>
  </table>
</div>
<p>So one name, “multicast delegate,” covers two implementations with opposite iteration order. The order a graph relies on is a property of the specific implementation, not of the concept.</p>
<h2 id="same-symptom-different-cause">Same symptom, different cause</h2>
<p>The comment above the copy, <em>“just in case the list is modified by one of the callbacks during the broadcast,”</em> describes a different case with an identical symptom. The copy means that a binding added to a dispatcher <strong>that is currently broadcasting</strong> will not be called in that broadcast. If <code>CustomEvent_2</code> bound <code>CustomEvent_3</code> to <code>OnDelegate_1</code>, the same dispatcher that is calling it right now, <code>CustomEvent_3</code> would stay silent as well. The cause would be the frozen copy taken before the first handler, not the registration order.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  Both traps belong to the same family: <strong>the state of the binding list at the moment of the call is the only thing that counts</strong>. Anything bound after that does not exist for that call.
</div>

<h2 id="what-the-order-does-not-guarantee">What the order does not guarantee</h2>
<p>Moving a binding earlier in the lifecycle fixes the symptom, and it is a legitimate move. It is worth knowing what it assumes.</p>
<p>Epic does not promise this order. The multicast class documentation says so directly (<code>DelegateSignatureImpl.inl:1024-1025</code>):</p>
<blockquote>
<p>Multicast delegates offer no guarantees for the calling order of bound functions. As bindings get added and removed over time, the calling order may change.</p>
</blockquote>
<p>Removal from the list does not preserve order. <code>RemoveInternal</code> goes through <code>RemoveAtSwap</code> (<code>ScriptDelegates.h:1069</code>), so the last entry jumps into the freed index. The header warns about this on every removal method (<code>ScriptDelegates.h:731</code>, <code>771</code>, <code>1045</code>, <code>1056</code>):</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="o">*</span> <span class="n">Removes</span> <span class="n">a</span> <span class="n">function</span> <span class="n">from</span> <span class="k">this</span> <span class="n">multi</span><span class="o">-</span><span class="n">cast</span> <span class="n">delegate</span><span class="err">&#39;</span><span class="n">s</span> <span class="n">invocation</span> <span class="n">list</span> <span class="p">(</span><span class="n">performance</span> <span class="n">is</span> <span class="n">O</span><span class="p">(</span><span class="n">N</span><span class="p">)).</span>  <span class="n">Note</span> <span class="n">that</span> <span class="n">the</span>
</span></span><span class="line"><span class="cl"><span class="o">*</span> <span class="n">order</span> <span class="n">of</span> <span class="n">the</span> <span class="n">delegates</span> <span class="n">may</span> <span class="n">not</span> <span class="n">be</span> <span class="n">preserved</span><span class="o">!</span></span></span></code></pre></div></div>
<p>The practical takeaway: the order is stable as long as nothing unbinds from the dispatcher. Unbinding from this dispatcher anywhere in the project can reorder its remaining listeners.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  Binding again does not move the event to the end of the list. The <code>Bind Event to…</code> node compiles to <code>EX_AddMulticastDelegate</code> (<code>ScriptCore.cpp:3342</code>), which calls <code>FMulticastInlineDelegateProperty::AddDelegate</code> (<code>PropertyMulticastDelegate.cpp:497</code>), which calls <code>InvocationList.AddUnique</code> (<code>ScriptDelegates.h:1040</code>). <code>AddUnique</code> leaves an existing entry at its current position, so binding a second time does nothing.
</div>

<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  Relying on the order is fine as long as you can answer two questions: <strong>what sets it</strong> and <strong>what can change it</strong>. If the answer to both is “I do not know,” the graph works by accident.
</div>

<h2 id="what-to-do-about-it">What to do about it</h2>
<p>One pattern forces a decision: <strong>a dispatcher handler that broadcasts another dispatcher while it runs.</strong> It opens a window in which some listeners have not registered yet. Five ways out, each with its own condition. The first three change the graph and have results shown below. The last two are a project convention rather than a change in the nodes.</p>
<h3 id="defer-the-broadcast-by-one-tick">Defer the broadcast by one tick</h3>
<p>Instead of broadcasting <code>OnDelegate_2</code> straight from <code>CustomEvent_1</code>, route it through <code>Set Timer for Next Tick by Event</code> and a separate <code>BroadcastDelegate2</code> event. The <code>OnDelegate_1</code> broadcast then runs through every handler, each one gets to bind, and <code>OnDelegate_2</code> starts with a complete list.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-fix-next-tick.txt" data-bp-height="1340"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-fix-next-tick.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">CustomEvent_1 does not broadcast OnDelegate_2 directly. It hands that off to BroadcastDelegate2 through Set Timer for Next Tick by Event.</figcaption></figure>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[3]</span>
</span></code></pre>
</div>
<p><code>[3]</code> landing <strong>after</strong> the <code>[END TEST]</code> marker shows exactly what this solution does: the broadcast moved out of the synchronous execution triggered by the key press and into the next frame.</p>
<p>This applies when no <code>OnDelegate_2</code> listener binds later than the next frame. An actor spawned two frames later, or a widget created on demand, still misses out. It widens the window; it does not remove the dependency.</p>
<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  <code>Set Timer for Next Tick by Event</code> takes no time input, so it cannot accidentally be set to zero. <code>Set Timer by Event</code> can, and there <a href="/en/topics/unreal-engine/gameplay-framework/set-timer-by-event-time-zero"><code>Time = 0.0</code> clears the timer instead of firing it immediately</a>.
</div>

<h3 id="move-the-binding-to-a-dispatcher-that-is-guaranteed-to-fire-later">Move the binding to a dispatcher that is guaranteed to fire later</h3>
<p><code>CustomEvent_3</code> goes on <code>OnDelegate_3</code>, which is known to fire later. Here the same key press broadcasts it, right after <code>OnDelegate_1</code>.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-fix-later-dispatcher.txt" data-bp-height="1080"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-fix-later-dispatcher.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">CustomEvent_2 binds CustomEvent_3 to OnDelegate_3 instead of OnDelegate_2.</figcaption></figure>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[3]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span></code></pre>
</div>
<p>Cheap and effective, but it still depends on ordering: the order of separate broadcasts rather than the order of handlers within one broadcast. The condition is that nobody moves that call.</p>
<h3 id="turn-the-event-into-state">Turn the event into state</h3>
<p>A dispatcher carries information only at the moment of the call. Whoever arrives late never gets it. A <code>bDelegate2Broadcast</code> bool on the sender side changes that rule: <code>CustomEvent_1</code> sets it before the broadcast, and <code>CustomEvent_2</code>, once its binding is in place, checks it with a <code>Branch</code> and calls <code>CustomEvent_3</code> directly if needed.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/dispatcher-fix-event-to-state.txt" data-bp-height="1080"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/dispatcher-fix-event-to-state.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">CustomEvent_1 sets bDelegate2Broadcast before the call. After binding, CustomEvent_2 checks the flag with a Branch and makes up for the missed broadcast.</figcaption></figure>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[1]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[2]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[3]</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[END TEST]</span>
</span></code></pre>
</div>
<p><code>[3]</code> appears synchronously here, still within the same key press. The late listener does not wait for another broadcast. It reads the state and catches up immediately. This is the only option that survives a listener created after everything else, at the cost of maintaining extra state.</p>
<h3 id="declare-the-order-instead-of-relying-on-it-silently">Declare the order instead of relying on it silently</h3>
<p>If the order is meant to be a contract, write it down: a comment on both <code>Bind Event to…</code> nodes stating which one comes first and what depends on it, an event name that identifies the phase, and an explicit condition that nothing unbinds from this dispatcher. This option documents the existing ordering dependency without changing the graph.</p>
<h3 id="separate-the-phases-bindings-in-one-broadcasts-in-the-next">Separate the phases: bindings in one, broadcasts in the next</h3>
<p>If every <code>Bind Event to…</code> runs in a phase earlier than any call, the order within the list stops meaning anything. Here the dependency disappears instead of moving further out. The condition is that the phase boundary is clearly defined in the project and nobody binds after it.</p>
<h2 id="when-this-matters">When this matters</h2>
<p>As long as every binding to a given dispatcher lives in one Blueprint, the problem barely exists. The order is visible at a glance, and nobody changes it by accident.</p>
<p>The risk shows up once listeners of the same dispatcher spread across different lifecycle phases: GameInstance, a subsystem, GameMode, an actor placed in the level, a spawned actor, a widget created on demand. Call order stops being a property of the graph and becomes a property of the whole project: it is set by the phases in which all the <code>Bind Event to…</code> nodes attached to that dispatcher run. Move one of them to a different phase and the rest shift position in the list.</p>
]]></content:encoded>
      
    </item>
    
    <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>
    
    <item>
      <title>Set Timer by Event with Time = 0.0 never fires</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/set-timer-by-event-time-zero/</link>
      <pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/gameplay-framework/set-timer-by-event-time-zero/</guid>
      <description>Time = 0.0 on Set Timer by Event does not mean “immediately”. It means “clear the existing timer and do not set a new one.” The node runs without an error, the returned handle is invalid, and the only warning goes to the Output Log. In a default Shipping build, that warning isn&amp;rsquo;t emitted at all.</description>
      <content:encoded><![CDATA[<p><code>Set Timer by Event</code> with the <code>Time</code> pin set to <code>0.0</code> will never call the event bound to it. The node itself executes and the execution flow continues normally, but the returned <code>Timer Handle</code> is invalid. If a timer is already associated with that handle, the call clears it first.</p>
<p>Here, zero means “clear the timer.”</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/set-timer-zero-repro.txt" data-bp-height="520"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/set-timer-zero-repro.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">Repro: Event BeginPlay calls Set Timer by Event with Time = 0.0 and Looping disabled. The custom event OnTimerFired logs FIRED, and the Return Value passes through Is Valid Timer Handle into Log String — only the second of those two lines ever appears in the log.</figcaption></figure>
<h2 id="the-symptom-silence">The symptom: silence</h2>
<p>No compile error. No red node. No runtime exception. <code>Is Valid Timer Handle</code> returns <code>false</code> for the returned handle — assuming you think to check it at all.</p>
<p>The only visible signal is a warning in the Output Log, generated from this format string in <code>Engine/Private/KismetSystemLibrary.cpp:748</code> (UE 5.8):</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="n">FFrame</span><span class="o">::</span><span class="n">KismetExecutionMessage</span><span class="p">(</span><span class="o">*</span><span class="n">FString</span><span class="o">::</span><span class="n">Printf</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;%s %s SetTimer passed a negative or zero time. The associated timer may fail to be created/fire! &#34;</span>
</span></span><span class="line"><span class="cl">         <span class="s">&#34;If using InitialStartDelayVariance, be sure it is smaller than (Time + InitialStartDelay).&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">ObjectName</span><span class="p">,</span> <span class="o">*</span><span class="n">FunctionName</span><span class="p">),</span> <span class="n">ELogVerbosity</span><span class="o">::</span><span class="n">Warning</span><span class="p">);</span></span></span></code></pre></div></div>
<p>For the graph above, the output looks like this (the PIE path on the second line has been shortened):</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line ue-log__line--warning"><span class="ue-log__cat">LogScript</span>: <span class="ue-log__sev ue-log__sev--warning">Warning</span>: <span class="ue-log__msg">Script Msg: BP_TimerZero_C_UAID_74563C6B4F116DF302_1712491287 OnTimerFired SetTimer passed a negative or zero time. The associated timer may fail to be created/fire! If using InitialStartDelayVariance, be sure it is smaller than (Time + InitialStartDelay).</span>
</span><span class="ue-log__line ue-log__line--warning"><span class="ue-log__cat">LogScript</span>: <span class="ue-log__sev ue-log__sev--warning">Warning</span>: <span class="ue-log__msg">Script Msg called by: BP_TimerZero_C /Memory/UEDPIE_0_…:PersistentLevel.BP_TimerZero_C_UAID_74563C6B4F116DF302_1712491287</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">IsValidTimerHandle -&gt; false</span>
</span></code></pre>
</div>
<p>The first <code>%s</code> resolves to the actor instance name, and the second resolves to the function bound to the <code>Event</code> pin. The third line comes from the graph’s own <code>Log String</code>: the returned handle is invalid.</p>
<p>What the log does not contain is <code>FIRED</code>. The event never ran.</p>
<p>The phrase <code>may fail to be created/fire</code> is more cautious than the implementation warrants. There is no <em>may</em> here: when <code>Time &lt;= 0</code>, the timer is not created.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  The logging handler is guarded by <code>#if !NO_LOGGING</code> (<code>Core/Private/Misc/CoreMisc.cpp:411</code>). In a default Shipping build, this warning is not emitted at all. On a test device, the failure is completely silent: the event does not run, and nothing in the log explains why.
</div>

<h2 id="where-it-disappears-one-condition-in-ftimermanager">Where it disappears: one condition in <code>FTimerManager</code></h2>
<p>The call path is short. <code>K2_SetTimerDelegate</code> checks the time only to emit the warning, then forwards the value unchanged (<code>Engine/Private/KismetSystemLibrary.cpp:735</code>):</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="n">InitialStartDelay</span> <span class="o">+=</span> <span class="n">FMath</span><span class="o">::</span><span class="n">RandRange</span><span class="p">(</span><span class="o">-</span><span class="n">InitialStartDelayVariance</span><span class="p">,</span> <span class="n">InitialStartDelayVariance</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="n">Time</span> <span class="o">&lt;=</span> <span class="mf">0.f</span> <span class="o">||</span> <span class="p">(</span><span class="n">Time</span> <span class="o">+</span> <span class="n">InitialStartDelay</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mf">0.f</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="c1">// ... KismetExecutionMessage(..., ELogVerbosity::Warning);
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">FTimerManager</span><span class="o">&amp;</span> <span class="n">TimerManager</span> <span class="o">=</span> <span class="n">World</span><span class="o">-&gt;</span><span class="n">GetTimerManager</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="n">Handle</span> <span class="o">=</span> <span class="n">TimerManager</span><span class="p">.</span><span class="n">K2_FindDynamicTimerHandle</span><span class="p">(</span><span class="n">Delegate</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">TimerManager</span><span class="p">.</span><span class="n">SetTimer</span><span class="p">(</span><span class="n">Handle</span><span class="p">,</span> <span class="n">Delegate</span><span class="p">,</span> <span class="n">Time</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">FTimerManagerTimerParameters</span> <span class="p">{</span> <span class="p">.</span><span class="n">bLoop</span> <span class="o">=</span> <span class="n">bLooping</span><span class="p">,</span> <span class="p">.</span><span class="n">bMaxOncePerFrame</span> <span class="o">=</span> <span class="n">bMaxOncePerFrame</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">                                   <span class="p">.</span><span class="n">FirstDelay</span> <span class="o">=</span> <span class="n">Time</span> <span class="o">+</span> <span class="n">InitialStartDelay</span> <span class="p">});</span></span></span></code></pre></div></div>
<p>The actual decision happens one layer deeper, inside <code>FTimerManager::InternalSetTimer</code> (<code>Engine/Private/TimerManager.cpp:653</code>):</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="k">if</span> <span class="p">(</span><span class="n">FindTimer</span><span class="p">(</span><span class="n">InOutHandle</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="c1">// if the timer is already set, just clear it and we&#39;ll re-add it, since
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// there&#39;s no data to maintain.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">InternalClearTimer</span><span class="p">(</span><span class="n">InOutHandle</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="k">if</span> <span class="p">(</span><span class="n">InRate</span> <span class="o">&gt;</span> <span class="mf">0.f</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="c1">// ... the entire FTimerData setup, ExpireTime, heap push
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">InOutHandle</span><span class="p">.</span><span class="n">Invalidate</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>The entire timer initialization is guarded by <code>InRate &gt; 0.f</code>. When that condition fails, the function does exactly one thing: it invalidates the handle, with no log and no <code>ensure</code>.</p>
<p>If a timer already existed on the same handle, <code>InternalClearTimer</code> has already run by this point. Setting <code>Time</code> to <code>0.0</code> therefore does more than prevent a new timer from being created: it also clears the timer that was already running.</p>
<p>That is easier to hit from Blueprint than the code suggests, because the node has no handle input. <code>K2_FindDynamicTimerHandle</code> resolves the handle from the delegate, so a second call bound to the same event — this time with <code>Time = 0.0</code> — finds the existing timer and clears it.</p>
<p>The complete behavior matrix follows directly from that condition:</p>
<div class="table-wrap">
  <table>
    <thead>
      <tr>
        <th>Input</th>
        <th>What the engine does</th>
        <th>Signal</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><code>Time &gt; 0</code></td>
        <td>Timer is created normally</td>
        <td>—</td>
      </tr>
      <tr>
        <td><code>Time = 0.0</code></td>
        <td>Handle is invalidated and any existing timer is cleared</td>
        <td>Warning in <code>LogScript</code></td>
      </tr>
      <tr>
        <td><code>Time &lt; 0</code></td>
        <td>Same behavior as zero</td>
        <td>Warning in <code>LogScript</code></td>
      </tr>
      <tr>
        <td><code>Time = 0.0</code>, <code>InitialStartDelay = 5.0</code></td>
        <td>Same behavior as zero: the condition checks <code>InRate</code>, while <code>InitialStartDelay</code> affects only <code>FirstDelay</code></td>
        <td>Warning in <code>LogScript</code></td>
      </tr>
      <tr>
        <td><code>SetTimer(..., 0.f, ...)</code> from C++</td>
        <td>Handle is invalidated and any existing timer is cleared</td>
        <td><strong>No signal at all</strong></td>
      </tr>
    </tbody>
  </table>
</div>
<p>From C++, there is not even that one warning. A direct call to <code>GetWorldTimerManager().SetTimer(...)</code> reaches the same <code>InternalSetTimer</code> implementation but bypasses the Kismet layer entirely. <code>Set Timer by Function Name</code> is not a way out either: a different Kismet wrapper, the same condition underneath.</p>
<p>The condition itself is reasonable. A looping timer with a rate of zero could otherwise create an infinite loop within a single frame.</p>
<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  The engine deliberately rejects the timer, but communicates that rejection through nothing more than an <code>else</code> branch and a silent <code>Invalidate()</code>. The only warning disappears along with logging.
</div>

<h2 id="epic-documented-it--just-not-where-most-users-look">Epic documented it — just not where most users look</h2>
<p>This behavior is explicitly documented in the function comment (<code>Engine/Classes/Kismet/KismetSystemLibrary.h:723</code>):</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="o">*</span> <span class="err">@</span><span class="n">param</span> <span class="n">Time</span>    <span class="n">How</span> <span class="kt">long</span> <span class="n">to</span> <span class="n">wait</span> <span class="n">before</span> <span class="n">executing</span> <span class="n">the</span> <span class="n">delegate</span><span class="p">,</span> <span class="n">in</span> <span class="n">seconds</span><span class="p">.</span>
</span></span><span class="line"><span class="cl"><span class="o">*</span>                <span class="n">Setting</span> <span class="n">a</span> <span class="n">timer</span> <span class="n">to</span> <span class="o">&lt;=</span> <span class="mi">0</span> <span class="n">seconds</span> <span class="n">will</span> <span class="n">clear</span> <span class="n">it</span> <span class="k">if</span> <span class="n">it</span> <span class="n">is</span> <span class="n">set</span><span class="p">.</span></span></span></code></pre></div></div>
<p>That line never appears in the node’s main tooltip. The editor stops the function description at the first <code>@param</code> and moves the remaining descriptions to the individual pin tooltips.</p>
<div class="callout callout--note">
  <div class="callout__title">Note</div>
  The description of the <code>Time</code> pin appears when you hover over <strong>the pin itself</strong>, not the node header. This behavior applies to every <code>BlueprintCallable</code> function in the engine, not just timers. I covered the underlying mechanism separately in <a href="/en/topics/unreal-engine/editor-tooling/blueprint-node-tooltip-param-truncation">A Blueprint node tooltip stops at the first @param</a>.
</div>

<h2 id="what-to-do-instead">What to do instead</h2>
<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  If you want to run an event as soon as possible without running it inline on the current execution path, use the dedicated <strong><code>Set Timer for Next Tick by Event</code></strong> node (<code>KismetSystemLibrary.h:738</code>, function <code>K2_SetTimerForNextTickDelegate</code>). It has no time input, so it cannot accidentally be set to zero.
</div>

<p>When <code>Time</code> comes from data — a Data Asset, a curve, a gameplay attribute, or a value produced by difficulty scaling — validate it before passing it to the node. A <code>Branch</code> is the simplest way to do that: feed the same value into both the condition and the <code>Time</code> pin. If zero and negative values should mean “run on the next tick,” route them to <code>Set Timer for Next Tick by Event</code> and positive values to the regular timer node.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/set-timer-zero-branch-next-tick.txt" data-bp-height="700"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/set-timer-zero-branch-next-tick.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">The same value feeds both the &lt;= 0 condition and the Time pin. The true branch goes to Set Timer for Next Tick by Event, while the false branch goes to Set Timer by Event — both are bound to the same OnTimerFired event. The 0.0 literal stands in for a value that would come from data in real code.</figcaption></figure>
<p>That routing is a decision about the data contract, not a drop-in replacement. <code>Set Timer for Next Tick by Event</code> never loops, so it cannot stand in for a looping timer whose rate happened to arrive as zero. And if a timer is already running on the same handle, the two branches differ in effect: the regular node with a non-positive <code>Time</code> clears that timer, while the next-tick branch leaves it running and adds one extra call to the event. When neither outcome is the intended one, a non-positive value is simply bad data — log it and skip the call, or substitute a known default.</p>
<div class="section-panel section-panel--checklist">
  <div class="section-panel__label">Checklist</div>
  
<p>What to check, in order, when a timer never fires:</p>
<ul class="checklist">
  <li>Use <code>Print String</code> to display the <code>Time</code> value immediately before the node. The value that reaches the node at runtime is the only one that matters.</li>
  <li>Run <code>Is Valid Timer Handle</code> on the returned handle. A result of <code>false</code> means the timer was not created.</li>
  <li>Filter the Output Log by the <code>LogScript</code> category, which can be suppressed in <code>DefaultEngine.ini</code>.</li>
  <li>Check whether another call is setting the same timer again with a value of zero, since that call will clear the existing timer.</li>
  <li>In a Shipping build, the absence of a warning does not mean the call succeeded.</li>
</ul>

</div>

<h2 id="when-this-matters">When this matters</h2>
<p>This starts to matter once <code>Time</code> stops being a hard-coded literal and instead comes from a Data Asset, a gameplay attribute, a difficulty multiplier, a DPS-based interval, or a cooldown scaled by a character stat. In each of these cases, the value can end up being zero through an empty struct, an uninitialized field, a missing data value, or a division that happens to return zero under rare conditions.</p>
<p>And because <code>Time = 0.0</code> also clears a timer already running on the same handle, there are two possible failure modes instead of one: “it never started” and “it was running, and then it stopped.”</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>A Blueprint node tooltip stops at the first @param</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/blueprint-node-tooltip-param-truncation/</link>
      <pubDate>Thu, 09 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/editor-tooling/blueprint-node-tooltip-param-truncation/</guid>
      <description>The tooltip generator cuts the function comment off at the first @param and moves everything below it onto the individual pin tooltips. It affects every BlueprintCallable node, and it changes where you have to put warnings in your own API exposed to Blueprints.</description>
      <content:encoded><![CDATA[<p>A Blueprint node&rsquo;s tooltip shows the function comment only up to the first <code>@param</code>. Parameter descriptions, return value documentation, warnings attached to one specific argument: everything below that point ends up on the individual pin tooltips. Nobody checks those tooltips until they already know what they&rsquo;re looking for.</p>
<p>It comes down to how the tooltip generator splits the comment. That behavior affects every BlueprintCallable node, in the engine and in every plugin.</p>
<h2 id="the-symptom-nine-pins-a-two-sentence-tooltip">The symptom: nine pins, a two-sentence tooltip</h2>
<p><code>Set Timer by Event</code> is the textbook case. The function comment warns that a time less than or equal to zero clears the timer instead of setting it (<code>Engine/Classes/Kismet/KismetSystemLibrary.h:723</code>):</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="cm">/**
</span></span></span><span class="line"><span class="cl"><span class="cm"> * Set a timer to execute delegate. Setting an existing timer will reset that timer with updated parameters.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param Event   Event. Can be a K2 function or a Custom Event.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * @param Time    How long to wait before executing the delegate, in seconds.
</span></span></span><span class="line"><span class="cl"><span class="cm"> *                Setting a timer to &lt;= 0 seconds will clear it if it is set.
</span></span></span><span class="line"><span class="cl"><span class="cm"> * ...
</span></span></span><span class="line"><span class="cl"><span class="cm"> */</span></span></span></code></pre></div></div>
<p>The node tooltip itself reads, in full:</p>
<blockquote>
<p>Set a timer to execute delegate. Setting an existing timer will reset that timer with updated parameters.</p>
<p>Target is Kismet System Library</p>
</blockquote>
<figure class="prose-figure">
  <img src="/screens/tooltip-node-truncated.png" alt="Tooltip of the Set Timer by Event node: two sentences of description and the line Target is Kismet System Library, with no mention of the behavior when Time &lt;= 0" loading="lazy" /></figure>
<p>The sentence about <code>&lt;= 0</code> shows up when you hover the <code>Time</code> pin, and nowhere else:</p>
<figure class="prose-figure">
  <img src="/screens/tooltip-pin-time.png" alt="Tooltip of the Time pin on the same node: the full description, including the sentence about clearing the timer when the value is less than or equal to zero" loading="lazy" /></figure>
<p>I wrote up the consequences of this particular case separately in <a href="/en/topics/unreal-engine/gameplay-framework/set-timer-by-event-time-zero">Set Timer by Event with Time = 0.0 never fires</a>. What interests me here is the mechanism, because it affects far more than just this one node.</p>
<h2 id="the-cause-split-with-nullptr-on-the-right">The cause: <code>Split</code> with <code>nullptr</code> on the right</h2>
<p>The node tooltip comes from <code>ObjectTools::GetDefaultTooltipForFunction</code> (<code>Editor/UnrealEd/Private/ObjectTools.cpp:5396</code>), and the whole decision fits in two calls:</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="c1">// Strip off the doxygen nastiness
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="k">const</span> <span class="n">FString</span> <span class="nf">DoxygenParam</span><span class="p">(</span><span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;@param&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="k">const</span> <span class="n">FString</span> <span class="nf">DoxygenReturn</span><span class="p">(</span><span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;@return&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">Tooltip</span><span class="p">.</span><span class="n">Split</span><span class="p">(</span><span class="n">DoxygenParam</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Tooltip</span><span class="p">,</span> <span class="k">nullptr</span><span class="p">,</span> <span class="n">ESearchCase</span><span class="o">::</span><span class="n">IgnoreCase</span><span class="p">,</span> <span class="n">ESearchDir</span><span class="o">::</span><span class="n">FromStart</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">Tooltip</span><span class="p">.</span><span class="n">Split</span><span class="p">(</span><span class="n">DoxygenReturn</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Tooltip</span><span class="p">,</span> <span class="k">nullptr</span><span class="p">,</span> <span class="n">ESearchCase</span><span class="o">::</span><span class="n">IgnoreCase</span><span class="p">,</span> <span class="n">ESearchDir</span><span class="o">::</span><span class="n">FromStart</span><span class="p">);</span></span></span></code></pre></div></div>
<p><code>Split</code> with the result written back to <code>LeftString</code> and <code>nullptr</code> in place of <code>RightString</code> truncates the text at the first match. The right-hand side is computed and thrown away. The comment “strip off the doxygen nastiness” describes the intent precisely: the Doxygen tags were treated as noise and removed together with the content they describe.</p>
<p>Everything else follows from that one decision. Parameter descriptions take a separate route to the individual pin tooltips (<code>ObjectTools.cpp:5452</code>), and <code>@see</code> and <code>@note</code> survive the cut and render as <code>See:</code> and <code>Note:</code>, but only when they appear above the first <code>@param</code>.</p>
<h2 id="what-to-do-about-it">What to do about it</h2>
<p><strong>Reading someone else&rsquo;s nodes:</strong> if a node tooltip looks suspiciously thin for the number of pins, the documentation is most likely on the pins. Hovering a pin takes a second and routinely reveals information that was missing from the node tooltip.</p>
<p><strong>Exposing your own API to Blueprints</strong> (which covers every plugin with <code>UFUNCTION(BlueprintCallable)</code>):</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  Everything the user needs to know <strong>before</strong> wiring up the node has to sit in the description block above the first <code>@param</code>: edge cases, values that invalidate the call, initialization order requirements. A sentence attached to a parameter is technically correct documentation and effectively invisible: the user will not see it until they start hunting for the cause of a bug, which is one step too late.
</div>

<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  An <code>@note</code> placed above the parameter list renders as <code>Note:</code>. That is the simplest way to make such a warning stand out while keeping the comment compatible with Doxygen.
</div>

<h2 id="when-this-matters">When this matters</h2>
<p>On a node with two pins, not much. The whole description fits in the opening sentence anyway.</p>
<p>The stakes rise with the number of parameters, and with the number of values that are technically valid but mean something other than what the user assumes: zero for “cancel,” a negative value for “no limit,” an empty name for “use the default.” That kind of knowledge belongs next to the parameter by definition — and the text next to the parameter is exactly what the node tooltip will not show.</p>
<p>There&rsquo;s one more consequence for plugin authors: a user who falls into that trap despite a correctly written comment will file it as a documentation bug. The comment will be right where it belongs, and the report will still be justified.</p>
]]></content:encoded>
      
    </item>
    
    <item>
      <title>Casting to an interface misses Blueprint implementations</title>
      <link>https://stillcooking.dev/en/topics/unreal-engine/cpp-in-unreal/interface-cast-misses-blueprint/</link>
      <pubDate>Tue, 07 Jul 2026 00:00:00 +0000</pubDate>
      
      <guid isPermaLink="true">https://stillcooking.dev/en/topics/unreal-engine/cpp-in-unreal/interface-cast-misses-blueprint/</guid>
      <description>Cast&lt;IMyInterface&gt;(Obj) returns nullptr when an object&amp;rsquo;s interface implementation exists only in Blueprint, even though ImplementsInterface() returns true for that same object. The engine skips entries marked bImplementedByK2, so casting to an interface is safe only when that interface is explicitly blocked from Blueprint implementation.</description>
      <content:encoded><![CDATA[<h2 id="two-ways-to-call">Two ways to call</h2>
<p>There are two ways to call an interface function in Unreal: cast to the <code>I*</code> type or use the generated <code>Execute_</code> wrapper. They look interchangeable, but they ask the engine two different questions.</p>
<p><code>Cast&lt;IMyInterface&gt;(Obj)</code> returns <code>nullptr</code> for an object whose interface implementation exists only in Blueprint. For that same object, <code>Obj-&gt;GetClass()-&gt;ImplementsInterface(UMyInterface::StaticClass())</code> returns <code>true</code>. The engine deliberately skips interface entries added by the Blueprint compiler, so casting to <code>I*</code> is safe only when the interface is explicitly marked as non-implementable in Blueprint. Everywhere else, it is a silent bug waiting for the day a designer creates a Blueprint implementation.</p>
<p>The whole problem comes down to one specific configuration: a native interface declared in C++ and implemented in Blueprint.</p>
<h2 id="two-functions-one-condition-apart">Two functions, one condition apart</h2>
<p><code>GetInterfaceAddress</code> and <code>ImplementsInterface</code> both walk the same <code>UClass::Interfaces</code> array and test the same relationship with <code>IsChildOf</code>. Exactly one extra condition separates them.</p>
<p><code>UObjectBaseUtility::GetInterfaceAddress</code> (<code>Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectBaseUtility.cpp</code>), in the branch for a native interface:</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="k">for</span> <span class="p">(</span><span class="n">TArray</span><span class="o">&lt;</span><span class="n">FImplementedInterface</span><span class="o">&gt;::</span><span class="n">TIterator</span> <span class="n">It</span><span class="p">(</span><span class="n">CurrentClass</span><span class="o">-&gt;</span><span class="n">Interfaces</span><span class="p">);</span> <span class="n">It</span><span class="p">;</span> <span class="o">++</span><span class="n">It</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="c1">// See if this is the implementation we are looking for, and it was done natively, not in K2
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">FImplementedInterface</span><span class="o">&amp;</span> <span class="n">ImplInterface</span> <span class="o">=</span> <span class="o">*</span><span class="n">It</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span> <span class="o">!</span><span class="n">ImplInterface</span><span class="p">.</span><span class="n">bImplementedByK2</span> <span class="o">&amp;&amp;</span> <span class="n">ImplInterface</span><span class="p">.</span><span class="n">Class</span><span class="o">-&gt;</span><span class="n">IsChildOf</span><span class="p">(</span><span class="n">InterfaceClass</span><span class="p">)</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">Result</span> <span class="o">=</span> <span class="p">(</span><span class="n">uint8</span><span class="o">*</span><span class="p">)</span><span class="k">this</span> <span class="o">+</span> <span class="n">It</span><span class="o">-&gt;</span><span class="n">PointerOffset</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">break</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="p">}</span></span></span></code></pre></div></div>
<p><code>UClass::ImplementsInterface</code> (<code>Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp</code>), with the same loop:</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="k">for</span> <span class="p">(</span><span class="n">TArray</span><span class="o">&lt;</span><span class="n">FImplementedInterface</span><span class="o">&gt;::</span><span class="n">TConstIterator</span> <span class="n">It</span><span class="p">(</span><span class="n">CurrentClass</span><span class="o">-&gt;</span><span class="n">Interfaces</span><span class="p">);</span> <span class="n">It</span><span class="p">;</span> <span class="o">++</span><span class="n">It</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="k">const</span> <span class="n">UClass</span><span class="o">*</span> <span class="n">InterfaceClass</span> <span class="o">=</span> <span class="n">It</span><span class="o">-&gt;</span><span class="n">Class</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">InterfaceClass</span> <span class="o">&amp;&amp;</span> <span class="n">InterfaceClass</span><span class="o">-&gt;</span><span class="n">IsChildOf</span><span class="p">(</span><span class="n">SomeInterface</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="k">return</span> <span class="nb">true</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="p">}</span></span></span></code></pre></div></div>
<p>The entire difference is <code>!ImplInterface.bImplementedByK2</code>. That flag marks an entry produced by the Blueprint compiler, and the engine&rsquo;s own comment says exactly that: <em>&ldquo;and it was done natively, not in K2.&rdquo;</em> <code>ImplementsInterface</code> has no such filter, so it gives the broader answer: the class implements the interface. <code>GetInterfaceAddress</code> gives a narrower answer: the class implements the interface <strong>natively</strong>.</p>
<div class="callout callout--insight">
  <div class="callout__title">Insight</div>
  <code>ImplementsInterface</code> asks whether the class implements the interface. <code>GetInterfaceAddress</code>, the function behind <code>Cast&lt;I*&gt;</code>, asks a narrower question: whether the class implements it <strong>natively</strong>. For a class whose interface implementation exists only in Blueprint, the first returns <code>true</code>, while the second returns <code>nullptr</code>.
</div>

<p>And <code>GetInterfaceAddress</code> is the cast. Not figuratively but literally, in <code>Engine/Source/Runtime/CoreUObject/Public/Templates/Casts.h</code>:</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="k">if</span> <span class="nf">constexpr</span> <span class="p">(</span><span class="n">TIsIInterface</span><span class="o">&lt;</span><span class="n">To</span><span class="o">&gt;::</span><span class="n">Value</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="k">return</span> <span class="p">(</span><span class="n">To</span><span class="o">*</span><span class="p">)((</span><span class="n">UObject</span><span class="o">*</span><span class="p">)</span><span class="n">Src</span><span class="p">)</span><span class="o">-&gt;</span><span class="n">GetInterfaceAddress</span><span class="p">(</span><span class="n">To</span><span class="o">::</span><span class="n">UClassType</span><span class="o">::</span><span class="n">StaticClass</span><span class="p">());</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>A cast to an interface never touches the class-cast-flag path used by a plain <code>Cast&lt;AActor&gt;</code>. Instead, it calls a function designed specifically to exclude Blueprint implementations.</p>
<h2 id="why-there-is-nothing-to-return">Why there is nothing to return</h2>
<p>This looks like an oversight. It is a constraint of the memory layout.</p>
<p>A C++ class that implements <code>IMyInterface</code> inherits from it, so its memory layout contains an <code>IMyInterface</code> subobject at a known offset. That offset is exactly what the cast is built on: <code>(uint8*)this + It-&gt;PointerOffset</code>. If the interface is implemented only in Blueprint, there is no corresponding native <code>IMyInterface</code> subobject, so there is nothing for <code>PointerOffset</code> to describe. Without that subobject, there is no <code>IMyInterface</code> vtable either, and therefore no address for the cast to return.</p>
<p>A Blueprint implementation exists in a completely different form. It is a <code>UFunction</code> on the Blueprint class, invoked through the virtual machine. No valid pointer exists, so <code>nullptr</code> is the only honest answer.</p>
<p><code>Cast</code> itself is what makes this misleading: it looks like a uniform reflection mechanism, but in this case it is really asking about native memory layout.</p>
<h2 id="the-fix-execute_">The fix: <code>Execute_</code></h2>
<p>For every interface function, UHT generates a static <code>Execute_Foo</code> wrapper. <code>AppendInterfaceCallFunction</code> (<code>Engine/Source/Programs/Shared/EpicGames.UHT/Exporters/CodeGen/UhtHeaderCodeGeneratorCppFile.cs</code>) emits the body, which expands to this:</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="k">static</span> <span class="n">FName</span> <span class="n">NAME_UMyInterface_Foo</span> <span class="o">=</span> <span class="n">FName</span><span class="p">(</span><span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;Foo&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="n">IMyInterface</span><span class="o">::</span><span class="n">Execute_Foo</span><span class="p">(</span><span class="n">UObject</span><span class="o">*</span> <span class="n">O</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">check</span><span class="p">(</span><span class="n">O</span> <span class="o">!=</span> <span class="nb">NULL</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="n">check</span><span class="p">(</span><span class="n">O</span><span class="o">-&gt;</span><span class="n">GetClass</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">ImplementsInterface</span><span class="p">(</span><span class="n">UMyInterface</span><span class="o">::</span><span class="n">StaticClass</span><span class="p">()));</span>
</span></span><span class="line"><span class="cl">    <span class="n">UFunction</span><span class="o">*</span> <span class="k">const</span> <span class="n">Func</span> <span class="o">=</span> <span class="n">O</span><span class="o">-&gt;</span><span class="n">FindFunction</span><span class="p">(</span><span class="n">NAME_UMyInterface_Foo</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">Func</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">O</span><span class="o">-&gt;</span><span class="n">ProcessEvent</span><span class="p">(</span><span class="n">Func</span><span class="p">,</span> <span class="nb">NULL</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="k">else</span> <span class="nf">if</span> <span class="p">(</span><span class="k">auto</span> <span class="n">I</span> <span class="o">=</span> <span class="p">(</span><span class="n">IMyInterface</span><span class="o">*</span><span class="p">)(</span><span class="n">O</span><span class="o">-&gt;</span><span class="n">GetNativeInterfaceAddress</span><span class="p">(</span><span class="n">UMyInterface</span><span class="o">::</span><span class="n">StaticClass</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">I</span><span class="o">-&gt;</span><span class="n">Foo_Implementation</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="p">}</span></span></span></code></pre></div></div>
<p>The order is the opposite of what you would expect. <code>FindFunction</code> comes first. That is the reflection path, and it finds a Blueprint override just as well as a UHT-registered native function. Only when there is no <code>UFunction</code> does the wrapper retrieve the native subobject address and call <code>_Implementation</code> directly.</p>
<p><code>Execute_</code> gates itself on <code>ImplementsInterface</code>, the broader check, not on the existence of an interface address.</p>
<p>The correct call site therefore looks like this:</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="k">if</span> <span class="p">(</span><span class="n">Obj</span><span class="o">-&gt;</span><span class="n">Implements</span><span class="o">&lt;</span><span class="n">UMyInterface</span><span class="o">&gt;</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">IMyInterface</span><span class="o">::</span><span class="n">Execute_Foo</span><span class="p">(</span><span class="n">Obj</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<p>A second safeguard sits in the native body of the event function itself. For interfaces, UHT emits that body as a hard assertion:</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="n">check</span><span class="p">(</span><span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="s">&#34;Do not directly call Event functions in Interfaces. Call Execute_Foo instead.&#34;</span><span class="p">);</span></span></span></code></pre></div></div>
<p>That gives <code>Cast&lt;IMyInterface&gt;(Obj)-&gt;Foo()</code> two independent ways to fail. Either the cast returns <code>nullptr</code> and the program crashes on the dereference, or the cast succeeds and the call runs straight into the assertion. The <code>nullptr</code> failure is worse because it occurs only for objects whose interface implementation comes from Blueprint.</p>
<h2 id="when-the-cast-is-valid">When the cast is valid</h2>
<p>A cast to <code>I*</code> covers every implementation of the interface under exactly one condition: the interface must be explicitly non-implementable in Blueprint. Then <code>bImplementedByK2</code> is never set on any entry, so <code>GetInterfaceAddress</code> and <code>ImplementsInterface</code> always agree.</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="n">UINTERFACE</span><span class="p">(</span><span class="n">BlueprintType</span><span class="p">,</span> <span class="n">meta</span><span class="o">=</span><span class="p">(</span><span class="n">CannotImplementInterfaceInBlueprint</span><span class="p">),</span> <span class="n">MinimalAPI</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UDamageSource</span> <span class="o">:</span> <span class="k">public</span> <span class="n">UInterface</span> <span class="p">{</span> <span class="n">GENERATED_BODY</span><span class="p">()</span> <span class="p">};</span></span></span></code></pre></div></div>
<div class="callout callout--tip">
  <div class="callout__title">Tip</div>
  This leads to the rule I follow: casting to <code>I*</code> is allowed only when I declare the interface myself and explicitly prevent it from being implemented in Blueprint. Everywhere else, use <code>Implements&lt;&gt;</code> plus <code>Execute_</code>.
</div>

<h2 id="verification">Verification</h2>
<p>I verified this in the editor, not just by reading the source. The probe collects three readings for the same object: <code>ImplementsInterface</code>, a cast to the interface type, and <code>GetNativeInterfaceAddress</code>. It then calls <code>Execute_</code>. The test interface allows Blueprint implementations, so the test can cover the case where the cast fails:</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="n">UINTERFACE</span><span class="p">(</span><span class="n">Blueprintable</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UPickupTarget</span> <span class="o">:</span> <span class="k">public</span> <span class="n">UInterface</span> <span class="p">{</span> <span class="n">GENERATED_BODY</span><span class="p">()</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">IPickupTarget</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">GENERATED_BODY</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">public</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">UFUNCTION</span><span class="p">(</span><span class="n">BlueprintNativeEvent</span><span class="p">,</span> <span class="n">Category</span> <span class="o">=</span> <span class="s">&#34;Probe&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kt">void</span> <span class="n">OnPicked</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">// Control group: native implementation.
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">UCLASS</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ANativePickup</span> <span class="o">:</span> <span class="k">public</span> <span class="n">AActor</span><span class="p">,</span> <span class="k">public</span> <span class="n">IPickupTarget</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">GENERATED_BODY</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">public</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">virtual</span> <span class="kt">void</span> <span class="n">OnPicked_Implementation</span><span class="p">()</span> <span class="k">override</span>
</span></span><span class="line"><span class="cl">    <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">LogTemp</span><span class="p">,</span> <span class="n">Warning</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;[probe] OnPicked from C++&#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 class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">UCLASS</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UInterfaceProbe</span> <span class="o">:</span> <span class="k">public</span> <span class="n">UBlueprintFunctionLibrary</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">GENERATED_BODY</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="k">public</span><span class="o">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">UFUNCTION</span><span class="p">(</span><span class="n">BlueprintCallable</span><span class="p">,</span> <span class="n">Category</span> <span class="o">=</span> <span class="s">&#34;Probe&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">static</span> <span class="kt">void</span> <span class="n">Probe</span><span class="p">(</span><span class="n">UObject</span><span class="o">*</span> <span class="n">Obj</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">UInterfaceProbe</span><span class="o">::</span><span class="n">Probe</span><span class="p">(</span><span class="n">UObject</span><span class="o">*</span> <span class="n">Obj</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="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">Obj</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="k">return</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="k">const</span> <span class="kt">bool</span> <span class="n">bImplements</span> <span class="o">=</span> <span class="n">Obj</span><span class="o">-&gt;</span><span class="n">GetClass</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">ImplementsInterface</span><span class="p">(</span><span class="n">UPickupTarget</span><span class="o">::</span><span class="n">StaticClass</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">    <span class="n">IPickupTarget</span><span class="o">*</span> <span class="n">AsInterface</span> <span class="o">=</span> <span class="n">Cast</span><span class="o">&lt;</span><span class="n">IPickupTarget</span><span class="o">&gt;</span><span class="p">(</span><span class="n">Obj</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">void</span><span class="o">*</span> <span class="n">NativeActor</span> <span class="o">=</span> <span class="n">Obj</span><span class="o">-&gt;</span><span class="n">GetNativeInterfaceAddress</span><span class="p">(</span><span class="n">UPickupTarget</span><span class="o">::</span><span class="n">StaticClass</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">UE_LOG</span><span class="p">(</span><span class="n">LogTemp</span><span class="p">,</span> <span class="n">Warning</span><span class="p">,</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;[probe] %s | Implements=%s | Cast=%s | NativeActor=%s&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="o">*</span><span class="n">Obj</span><span class="o">-&gt;</span><span class="n">GetClass</span><span class="p">()</span><span class="o">-&gt;</span><span class="n">GetName</span><span class="p">(),</span>
</span></span><span class="line"><span class="cl">        <span class="n">bImplements</span> <span class="o">?</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;true&#34;</span><span class="p">)</span> <span class="o">:</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;false&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">AsInterface</span> <span class="o">?</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;ptr&#34;</span><span class="p">)</span> <span class="o">:</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;NULL&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="n">NativeActor</span> <span class="o">?</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;ptr&#34;</span><span class="p">)</span> <span class="o">:</span> <span class="n">TEXT</span><span class="p">(</span><span class="s">&#34;NULL&#34;</span><span class="p">));</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">bImplements</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">IPickupTarget</span><span class="o">::</span><span class="n">Execute_OnPicked</span><span class="p">(</span><span class="n">Obj</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="p">}</span></span></span></code></pre></div></div>
<p>The second implementation of the same interface is created in the editor: a Blueprint Actor class named <code>BP_BlueprintPickup</code>, with the <code>Pickup Target</code> interface added under Class Settings and an <code>On Picked</code> event that prints <code>[probe] OnPicked from BP</code>.</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/interface-cast-bp-pickup-event.txt" data-bp-height="340"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/interface-cast-bp-pickup-event.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">The BP_BlueprintPickup event graph. This On Picked implementation does not exist in any C&#43;&#43; file, and it is what makes the class&#39;s entry in UClass::Interfaces carry bImplementedByK2 = true.</figcaption></figure>
<p>The Level Blueprint of an empty map drives the test. <code>NativePickup</code> is spawned in <code>BeginPlay</code>. <code>BP_BlueprintPickup</code> is placed in the level and retrieved with <code>Get Actor of Class</code>. Both objects pass through the same <code>Probe</code> along a single execution chain:</p>
<figure class="bp-embed">
  <div class="bp-embed__canvas" data-bp-src="/blueprints/interface-cast-probe-beginplay.txt" data-bp-height="460"></div>
  <noscript>
    <p class="bp-embed__fallback">Viewing the graph requires JavaScript.</p>
    <a class="bp-embed__download" href="/blueprints/interface-cast-probe-beginplay.txt" download>Download Blueprint graph (.txt)</a>
  </noscript><figcaption class="bp-embed__caption">Level Blueprint of the test map. One BeginPlay, two actors, the same Probe function. The test compares native and Blueprint interface implementations.</figcaption></figure>
<p>The log from that run:</p>
<div class="highlight" data-lang="log">
  <button type="button" class="code-copy" data-code-copy data-label="Copy" data-copied="Copied!" aria-label="Copy code to clipboard">Copy</button><pre tabindex="0" class="ue-log"><code><span class="ue-log__line ue-log__line--warning"><span class="ue-log__cat">LogTemp</span>: <span class="ue-log__sev ue-log__sev--warning">Warning</span>: <span class="ue-log__msg">[probe] NativePickup | Implements=true | Cast=ptr | NativeActor=ptr</span>
</span><span class="ue-log__line ue-log__line--warning"><span class="ue-log__cat">LogTemp</span>: <span class="ue-log__sev ue-log__sev--warning">Warning</span>: <span class="ue-log__msg">[probe] OnPicked from C++</span>
</span><span class="ue-log__line ue-log__line--warning"><span class="ue-log__cat">LogTemp</span>: <span class="ue-log__sev ue-log__sev--warning">Warning</span>: <span class="ue-log__msg">[probe] BP_BlueprintPickup_C | Implements=true | Cast=NULL | NativeActor=NULL</span>
</span><span class="ue-log__line"><span class="ue-log__cat">LogBlueprintUserMessages</span>: <span class="ue-log__msg">[probe] OnPicked from BP</span>
</span></code></pre>
</div>
<p>In the third line, <code>Implements=true</code> sits next to <code>Cast=NULL</code> for the same object in the same call. The fourth line shows <code>Execute_</code> invoking the implementation despite the <code>NULL</code>. The <code>LogBlueprintUserMessages</code> category confirms that the call landed in the Blueprint graph rather than the native fallback.</p>
<p>The first two lines are the control group. For a native implementation, all three readings agree and the cast works. The comparison focuses on where the interface implementation comes from.</p>
<h2 id="limits">Limits</h2>
<p>An interface class declared in Blueprint rather than in C++ — that is, one without <code>CLASS_Native</code> — behaves differently. For such an interface, <code>GetInterfaceAddress</code> never enters the loop over <code>Interfaces</code>. It returns <code>this</code>, provided <code>ImplementsInterface</code> is true. That is a separate branch of the same function in <code>Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectBaseUtility.cpp</code>.</p>
<div class="callout callout--warning">
  <div class="callout__title">Warning</div>
  <code>CastChecked&lt;IMyInterface&gt;</code> does not make this pattern safe. It only changes where the failure occurs. With <code>DO_CHECK</code> enabled, it raises a fatal error, which is the behavior you want. In a build without <code>DO_CHECK</code>, the unchecked definition of <code>CastChecked</code> in <code>Casts.h</code> takes over and returns whatever <code>GetInterfaceAddress</code> gives it, without any check. That definition is itself marked <code>FUNCTION_NON_NULL_RETURN</code>, so it promises the caller a non-null pointer while potentially handing back <code>nullptr</code>. On that basis, the compiler is free to remove null checks at the call site. The behavior becomes least predictable in exactly the configuration where it is hardest to diagnose.
</div>

<p>This also has consequences for testing. A test built entirely from C++ classes will never catch the problem. Every implementation is native, <code>bImplementedByK2</code> is <code>false</code> everywhere, and the cast works every time. You can see that in the first two lines of the log above, where the control group passes. A test must include a Blueprint that implements the interface; otherwise, it exercises only the variant that was already correct.</p>
<p>As for the limits of the evidence: the mechanism comes from reading the UE 5.7 source (<code>UObjectBaseUtility.cpp</code>, <code>Class.cpp</code>, <code>Casts.h</code>, and the UHT generator) and was confirmed by one run in the editor on the same version. I have not checked whether <code>PointerOffset</code> and the <code>bImplementedByK2</code> filter behave the same way in older engine branches.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><code>UObjectBaseUtility::GetInterfaceAddress</code>, <code>GetNativeInterfaceAddress</code> — <code>Engine/Source/Runtime/CoreUObject/Private/UObject/UObjectBaseUtility.cpp</code></li>
<li><code>UClass::ImplementsInterface</code> — <code>Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp</code></li>
<li><code>Cast</code> / <code>CastChecked</code> for interfaces — <code>Engine/Source/Runtime/CoreUObject/Public/Templates/Casts.h</code></li>
<li><code>Execute_</code> generation — <code>Engine/Source/Programs/Shared/EpicGames.UHT/Exporters/CodeGen/UhtHeaderCodeGeneratorCppFile.cs</code>, <code>AppendInterfaceCallFunction</code></li>
<li><a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/interfaces-in-unreal-engine" data-external rel="noopener" target="_blank">Unreal Docs: Interfaces</a> — the official description of <code>UINTERFACE</code> and <code>Execute_</code> calls</li>
</ul>
]]></content:encoded>
      
    </item>
    
  </channel>
</rss>
