<?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>tick on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/tags/tick/</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, 30 Jul 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/tags/tick/index.xml" rel="self" type="application/rss+xml" />
    
    <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>
    
  </channel>
</rss>
