<?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>kismet on stillcooking.dev</title>
    <link>https://stillcooking.dev/en/tags/kismet/</link>
    <description>Personal portfolio and technical notes</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    
    <copyright>© 2026 stillcooking.dev — CC BY 4.0, https://creativecommons.org/licenses/by/4.0/</copyright>
    <lastBuildDate>Tue, 14 Jul 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://stillcooking.dev/en/tags/kismet/index.xml" rel="self" type="application/rss+xml" />
    
    <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>
    
  </channel>
</rss>
