Skip to content
Bit Quirky Behavior Trees and
State Machines

Search documentation

All topics and APIs

Close returns focus to Search.

What are you looking for?

Try a common topic or enter a complete API identifier.

Browse the reference instead

Esc closes · Tab reaches results · Enter opens the focused link

v0.6.0
Menu

Waiting and cancellation

Let a node hold Running while your coroutine, task, or event finishes, and know exactly what happens when the branch is taken away.

Work that takes time keeps its node Running until it finishes. When something else takes the branch away, the graph cancels that work instead of leaving it running behind the scenes.

A door takes a second to open. Bind its coroutine as an ordinary call and the graph starts a new one every frame, so the door never finishes opening and the branch never finishes waiting.

The Platformer beacon is the same shape with a different lever: the wind up belongs to the component, and the graph waits for WarmUpFinished rather than counting frames of its own. Either way the rule is that one activation owns one piece of work, and the node holds Running while that work is outstanding.

Three shapes wait, and nothing else does.

  • A method whose return is an IEnumerator, bound as a coroutine.
  • A method whose return is awaitable, bound as an awaitable.
  • An event condition on the GameObject path, which waits for one firing.

Everything else settles inside the evaluation that started it: a property, a field, a message, a member condition, and a method whose return is an ordinary value.

The awaitable contract is shape-based. Any return type with a public instance GetAwaiter(), whose awaiter exposes a public bool IsCompleted and a public GetResult(), qualifies. Task, Task<T>, ValueTask, UniTask, Unity’s Awaitable, and a handle of your own all take the same path, with no per-type special case. A callback API without that shape is not awaitable, so wrap it in one that is.

C#
using System.Collections;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
public sealed class Door : MonoBehaviour
{
public IEnumerator Open()
{
while (transform.localScale.y > 0.05f)
{
transform.localScale += Vector3.down * Time.deltaTime;
yield return null;
}
}
public async Task<bool> ReserveAsync(CancellationToken cancellationToken)
{
await Task.Yield();
cancellationToken.ThrowIfCancellationRequested();
return true;
}
}

Bind Open() and the node holds Running while the door shrinks. Bind ReserveAsync and its CancellationToken row is supplied by the runtime, so the cancellation the graph requests reaches your code.

The event condition holds Running for the whole 1.5 second warm up, then WarmUpFinished arrives once and the cursor moves straight on to the next node.
The event condition holds Running for the whole 1.5 second warm up, then WarmUpFinished arrives once and the cursor moves straight on to the next node.

The same lever, filmed on BeaconWarmUpLive.bqbehavior: the waiting node holds its RUNNING pill for the full wind up, and the branch moves on the tick the event arrives.

  1. Add a method node and pick the coroutine or awaitable declaration. The Result card switches to its deferred form.

  2. Read While waiting, which is locked to running. The waiting contract is fixed.

  3. Set the completion mapping: what a finished call reports, and for a value-returning awaitable, whether to compare or store the completed value.

  4. Set Give up after to On and enter a finite number of seconds when the gameplay action has a real deadline. Choose the result the node reports when the budget expires.

  5. Leave On interrupt alone. It is locked to interrupted, because a preempted branch did not fail.

Result

Success
The coroutine ran to its end, or the awaited call completed, and the mapping for that completion is succeeded.
Failure
The completion mapped to failed, or the give-up result you chose was failed.
Running
The coroutine or awaiter is outstanding, or an event condition has not fired yet.

One activation, one invocation

  • Starting the work stamps an activation generation on the pending slot. A completion that carries an older generation is ignored, so a late callback from before a reset cannot finish the node that came after it.
  • The method is not invoked again while its coroutine or awaitable is still outstanding.
  • Releasing an activation unsubscribes an event, cancels that invocation’s cancellation token, stops the owned coroutine wrapper, and disposes the iterator when it is disposable.

What cancels pending work

Reason Posted when
Transition A state machine transition leaves the state that owned the work
Reset The graph is replaced, or the agent’s runtime is reset
Lower priority A reactive observer preempts the branch that owned the work
Self A Self observer cancels its own guarded branch
Timeout A Time Limit decorator above the work expires
Peer failure A failed parallel state action, or a faulted hosted machine, ends its still-running peers
Interruption The host cancels active work explicitly

Disabling the agent pauses its member runtime instead: pending deferred work is cancelled, notification subscriptions are dropped, and the pending slots are released. Re-enabling keeps the runtime state it had.

Cancellation is cooperative for a task. The graph stops the polling coroutine it owns and cancels the token for that invocation. A Task that ignores its token keeps running in the background, unobserved, so read the token in code where the work is expensive. A coroutine, by contrast, is genuinely stopped, because the graph owns the Unity coroutine it started.

Give up after is a budget on one wait. Off means the node waits as long as the work does, which is why an event nobody raises waits forever and why Time Limit exists.

Faults

  • A thrown member call, a coroutine that throws, and a target lost mid-wait are recorded as faults, each naming the graph, the node, and the member. The trace on the agent holds the most recent entries and does not grow.
  • A fault reports the faulted result, which is a runtime fault and never an authored failure. The On fault control on the deferred Result card is authored and recorded in the bake; the runtime reports the fault itself, so treat a fault as a bug to fix rather than a branch to plan around.
  • A deferred start that hands back no usable handle, such as a method returning a null IEnumerator, faults immediately instead of quietly succeeding.
  • A call that throws commits no outputs. Outputs captured before a wait began stay committed, because they were already handed over.

When it goes wrong

SymptomCheckFix
The coroutine restarts every frame.Confirm the Result card is in its deferred form rather than mapping an ordinary return.Pick the IEnumerator declaration so the node owns the coroutine.
The node waits forever.Nothing completes the work: an event that is never raised, or a task that never finishes.Turn on Give up after, or wrap the node in Time Limit.
Your task keeps running after the branch was interrupted.The token is supplied, and your code has to observe it.Take a CancellationToken parameter and check it, since the graph cannot stop a task from outside.
The node faults immediately.Look for a method that returned a null iterator or a null awaitable.Return a real handle. A missing handle is a fault, not a failure.
An old completion lands after a reset.Generations differ, so the completion is discarded.Nothing to repair. The new activation waits for its own completion.
A give-up result of failed reads as a real failure in the log.Give-up reports the result you authored.Choose timeout as the give-up result when you want the reason to survive up the tree.
Full-size image