Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Execution Model

Beetry executes behavior trees by repeatedly ticking nodes until the tree reaches a terminal state. Every node participates in the same runtime model, which keeps execution predictable even when different node kinds have different roles.

What is a tick?

Execution in Beetry is centered around the Node interface. Every executable node implements the same core lifecycle: it can be ticked, aborted, and reset.

tick is a single execution step in which the tree asks a node to make progress and report its current state.
reset clears any execution state so the node or subtree can start again from a clean state.
abort lets a node stop in-progress work when execution changes direction, for example when a parent decides that a child should no longer be ticked.

This gives all node kinds a common runtime contract while still allowing them to behave differently during execution.

This contract is fully synchronous. The tree can execute correctly only if every node implements it without blocking. If any node blocks during tick, it can delay or stall execution of the whole tree.

Important

When implementing Node, none of its methods should block.

The result of a tick is described by TickStatus:

  • Success means the node finished successfully
  • Failure means the node finished unsuccessfully
  • Running means the node has started work that has not finished yet

When to tick?

Now that the idea of ticking and TickStatus is clear, the next question is when ticks should happen. In many behavior tree systems, the tree is ticked periodically, for example every 20 ms. In Beetry, the application selects the ticking policy. Beetry exposes the Ticker interface so applications can define their own tick source. Beetry provides PeriodicTick implementation that is the default choice in many scenarios.

Action lifecycle

Actions often represent long-running work. That does not fit naturally into the synchronous tick interface, which expects a node to return a result immediately.

To bridge this gap, Beetry introduces an executor and task registration interfaces. Action implements the Node interface, but internally it models execution via a state machine. On the first tick, it uses the user-provided ActionBehavior to create an ActionTask and register it with the executor, which returns a task handle. On later ticks, Action uses that handle to query the task status, or to abort the task if execution changes direction.1

Once the task reaches a terminal state, Action returns to its idle state and is ready to create a new task on a later tick.

Hooks

ActionBehavior provides hooks as part of its API. They are used to inject non-blocking logic that should run depending on the current TickStatus.

This gives a synchronization mechanism between the background task and the action.

This also means a task status is not always the final action result. If the task reports Success, but the action fails to complete the associated hook call, the action is treated as failed for that tick.

Execution Flow

The following sequence shows the high-level interaction between an Action, task registration, the executor, and a task handle.

sequenceDiagram
    actor User
    User ->>+ Action: tick()
    Action ->>+ RegisterTask: Register(ActionTask)
    RegisterTask ->>+ ExecutorConcept: Send(Task)
    create participant TaskHandle
    ExecutorConcept ->>+ TaskHandle: create
    ExecutorConcept -->>- RegisterTask: ok
    RegisterTask -->>- Action: ok
    Action ->>- User: TickStatus::Running

    User ->>+ Action: tick()
    Action ->>+ TaskHandle: query()
    TaskHandle -->>- Action: status
    Action -->> Action: call hook based on status
    Action -->>- User: status

    User ->>+ Action: abort()
    Action ->>+ TaskHandle: abort()
    TaskHandle ->>+ ExecutorConcept: abort()
    ExecutorConcept -->>- TaskHandle: aborted
    TaskHandle -->>- Action: TaskStatus::Aborted
    destroy TaskHandle
    Action ->> TaskHandle: drop

    Action -->> Action: on_aborted()

    Action -->>- User: status

This approach keeps the node interface synchronous and non-blocking, while still allowing long-running work to execute in the background. As a result, multiple leaf nodes can make progress concurrently.

Important

Because Action is scheduled on the executor and returns Running on the first tick, non-memory control nodes such as Sequence may restart earlier children on later ticks instead of resuming from the currently running one. In practice, Action nodes should usually be combined with memory-based control nodes such as MemorySequence.


  1. Aborting is currently implemented by sending an abort request through the task handle and then polling until the task reports a terminal status. See also Caveats.