Behavior Tree
Behavior Tree module for building AI decision trees.
Provides base node types (leaf, decorator, composite) that can be combined
to create complex AI behaviors. Nodes return BehResult (Success, Fail, Running)
and support lifecycle hooks (firstTick, tick, lastTick), reactions, and debugging.
To use this module:
require engine.ai.behtree
Creating custom leaf nodes
Subclass BehNode and override tick to implement game-specific conditions or actions:
class HasEnemy : BehNode {
def override tick(nodeId : NodeId) : BehResult {
var agent = nodeId?.AIAgent
return BehResult(agent.enemy.valid())
}
}
class ShootAtEnemy : BehNode {
def override tick(nodeId : NodeId) : BehResult {
var agent = nodeId?.AIAgent
agent.fire_at(agent.enemy.worldPosition)
return BehResult.Success
}
}
Use firstTick and lastTick for setup and cleanup:
class WaitForTimer : BehNode {
time : float = 1.0
private timer : Timer
def override firstTick(nodeId : NodeId) : void {
timer = Timer(time = time, started = true)
}
def override tick(nodeId : NodeId) : BehResult {
return timer.advance(get_delta_time()) || timer.is_done() ? BehResult.Success : BehResult.Running
}
}
Building a behavior tree
Combine nodes into a tree using composites and decorators. Always call init() on the root after construction:
def create_enemy_ai() : BehNode? {
var root = new SelectorBehNode(
name = "root",
children = array<BehNode?>(
new SequenceBehNode(name = "attack", children = array<BehNode?>(
new HasEnemy(name = "has enemy"),
new ShootAtEnemy(name = "shoot")
)),
new SequenceBehNode(name = "wander", children = array<BehNode?>(
new ChanceBehNode(name = "pause chance", chance = 0.5,
child = new WaitBehNode(name = "pause", waitDelay = float2(0.5, 1.5))),
new ChooseRandomDirection(name = "pick direction"),
new WalkForward(name = "walk")
))
)
)
root.init()
return root
}
SelectorBehNodetries children in order and succeeds on the first success (like boolean OR)SequenceBehNoderuns children in order and fails on the first failure (like boolean AND)ParallelBehNoderuns all children simultaneouslyChanceBehNoderuns its child with a given probabilityNotBehNodeinverts Success/Fail
Ticking the tree from a component
Call update each frame, passing the entity’s nodeId:
class Enemy : Component {
ai : BehNode?
debugOutput : bool
@collapse behTreeDebug : string
def override on_update() {
if (ai != null) {
ai.update(nodeId)
if (debugOutput) {
behTreeDebug = dump_tree(ai)
}
}
}
}
Using reactions
Reactions are special subtrees checked periodically via react().
When a reaction succeeds, the tree can be reset to re-evaluate priorities:
var root = new SelectorBehNode(
name = "root",
reaction = new SelectorBehNode(name = "reactions", children = array<BehNode?>(
new SequenceBehNode(name = "enemy spotted", children = array<BehNode?>(
new DetectEnemy(name = "detect"),
new ResetTree(name = "reset")
))
)),
children = array<BehNode?>(...)
)
In the update loop, check reactions periodically:
var result = react(root, nodeId)
if (result.result == BehResult.Success) {
print("reaction fired: {result.node.fullNodeName()}")
}
Note
Reactions must not return BehResult.Running. If they do, the reaction is forcibly reset and an error is logged.
Parallel execution
Use ParallelBehNode to run multiple behaviors simultaneously.
It returns Fail if any child fails, Success if any succeeds, and Running if all are still running:
new ParallelBehNode(name = "shoot at range", children = array<BehNode?>(
new StrafeAroundEnemy(name = "strafe"),
new ShootAtEnemy(name = "shoot")
))
Debugging
Use dump_tree to visualize the tree state and debugState for the active node:
print(dump_tree(root, true)) // include reactions
print(root.debugState()) // e.g. "[sel] root/attack/shoot: <Success>"
The active node is marked with > in the dump output. Each node shows its debugSymbol and lastResult.
Resetting the tree
Call reset on the root to force all nodes back to initial state.
This is typically done from a custom node inside a reaction:
class ResetTree : BehNode {
def override tick(nodeId : NodeId) : BehResult {
var agent = nodeId?.AIAgent
agent.ai.reset(nodeId)
return BehResult.Success
}
}
You can also reset from external code, for example on collision:
nodeId.on_collision(@(collision) : void {
enemy.ai.reset(nodeId)
})
Enumerations
- BehResult
- Values:
Fail = 0 - the node has failed
Success = 1 - the node has succeeded
Running = 2 - the node is still running and needs more ticks
Classes
- BehNode
- Fields:
parent : BehNode? - parent of the node
name : string - name of the node
debugSymbol : string - symbol displayed in debug output, e.g. “[seq]”, “[sel]”
lastResult : BehResult - last result of the node, useful for debugging
reaction : BehNode? - optional reaction subtree, evaluated bottom-up via react()
- BehNode.init()
Initialize the node tree, setting up parent references and recursing into children. Must be called on the root node after constructing the tree
- BehNode.firstTick(nodeId: NodeId)
Called before the first tick of the node. Override to initialize any state needed for the behavior
- Arguments:
nodeId : NodeId
- BehNode.lastTick(nodeId: NodeId)
Called after the last tick of the node or when the node is reset. Override to clean up any state that is no longer needed
- Arguments:
nodeId : NodeId
- BehNode.update(nodeId: NodeId): BehResult
Main entry point for ticking the node each frame. Calls firstTick on entry, tick every frame, and lastTick on completion
- Arguments:
nodeId : NodeId
- BehNode.reset(nodeId: NodeId)
Reset the node to its initial state. Override to add custom reset logic
- Arguments:
nodeId : NodeId
- BehNode.fullNodeName(): string
Return the full hierarchical path of this node. E.g. “root/attack/search target”
- BehNode.activeChild(): BehNode?
Return the deepest active child of the node. Returns itself for leaf nodes
- BehNode.debugState(): string
Return a formatted debug string with the active node path and last result
- BehNode.iter(depth: int; cb: block<(var node:BehNode?;depth:int):void>)
Iterate the tree depth-first, calling the callback for each node
- Arguments:
depth : int
cb : block<(node: BehNode?;depth:int):void>
- BehNode(): BehNode
Default constructor
- DecoratorBehNode : BehNode
Abstract base class for decorator nodes that wrap a single child
- Fields:
child : BehNode? - the single child node wrapped by this decorator
- DecoratorBehNode.init()
Set up the child’s parent reference and initialize it
- DecoratorBehNode.firstTick(nodeId: NodeId)
Propagate firstTick to the child, then call the base implementation
- Arguments:
nodeId : NodeId
- DecoratorBehNode.lastTick(nodeId: NodeId)
Propagate lastTick to the child, then call the base implementation
- Arguments:
nodeId : NodeId
- DecoratorBehNode.reset(nodeId: NodeId)
Reset the child, then reset self
- Arguments:
nodeId : NodeId
- DecoratorBehNode.activeChild(): BehNode?
Delegate to the child’s active child
- DecoratorBehNode.iter(depth: int; cb: block<(var node:BehNode?;depth:int):void>)
Iterate self and then the child subtree
- Arguments:
depth : int
cb : block<(node: BehNode?;depth:int):void>
- DecoratorBehNode(child: BehNode?): DecoratorBehNode
Construct with a child node
- Arguments:
child : BehNode?
- NotBehNode : DecoratorBehNode
Inverts the child’s result: Success becomes Fail and vice versa.
- Fields:
debugSymbol : string = “{!}” - Running is passed through unchanged
- NotBehNode.tick(nodeId: NodeId): BehResult
Tick the child and invert Success/Fail
- Arguments:
nodeId : NodeId
- TrueBehNode : BehNode
- Fields:
debugSymbol : string = “[Yes]” - Leaf node that always returns Success
- TrueBehNode.tick(nodeId: NodeId): BehResult
Always return Success
- Arguments:
nodeId : NodeId
- FalseBehNode : BehNode
- Fields:
debugSymbol : string = “[No]” - Leaf node that always returns Fail
- FalseBehNode.tick(nodeId: NodeId): BehResult
Always return Fail
- Arguments:
nodeId : NodeId
- SuccessBehNode : DecoratorBehNode
Forces Success regardless of the child’s result.
- Fields:
debugSymbol : string = “{Success}” - Running is passed through unchanged
- SuccessBehNode.tick(nodeId: NodeId): BehResult
Tick the child and return Success unless it is Running
- Arguments:
nodeId : NodeId
- FailureBehNode : DecoratorBehNode
Forces Fail regardless of the child’s result.
- Fields:
debugSymbol : string = “{Failure}” - Running is passed through unchanged
- FailureBehNode.tick(nodeId: NodeId): BehResult
Tick the child and return Fail unless it is Running
- Arguments:
nodeId : NodeId
- RunningBehNode : DecoratorBehNode
Always returns Running regardless of the child’s result.
- Fields:
debugSymbol : string = “{Running}” - Use exitOnFailure or exitOnSuccess to allow specific results through
exitOnFailure : bool = false - when true, passes through Fail instead of returning Running
exitOnSuccess : bool = false - when true, passes through Success instead of returning Running
- RunningBehNode.tick(nodeId: NodeId): BehResult
Tick the child and return Running, unless an exit condition is met
- Arguments:
nodeId : NodeId
- WaitBehNode : BehNode
Leaf node that returns Running for a random duration, then succeeds. The delay is randomly chosen between waitDelay.x and waitDelay.y on each entry
- Fields:
debugSymbol : string = “[Wait]” - min and max wait duration in seconds (x = min, y = max)
waitDelay : float2 = float2(0.5f,1f) - timestamp when the wait expires
- WaitBehNode.firstTick(nodeId: NodeId)
Pick a random delay and record the expiry time
- Arguments:
nodeId : NodeId
- WaitBehNode.tick(nodeId: NodeId): BehResult
Return Running until the delay has elapsed, then return Success
- Arguments:
nodeId : NodeId
- CompositeBehNode : BehNode
Abstract base class for composite nodes with multiple children
- Fields:
children : array< BehNode?> - list of child nodes
currentChild : int = 0 - index of the currently executing child
- CompositeBehNode.init()
Set up parent references for all children and initialize them
- CompositeBehNode.firstTick(nodeId: NodeId)
Reset currentChild to 0 so execution starts from the first child
- Arguments:
nodeId : NodeId
- CompositeBehNode.reset(nodeId: NodeId)
Reset all children and the currentChild index
- Arguments:
nodeId : NodeId
- CompositeBehNode.activeChild(): BehNode?
Return the active child of the currently executing child node
- CompositeBehNode.iter(depth: int; cb: block<(var node:BehNode?;depth:int):void>)
Iterate self and then each child subtree at depth + 1
- Arguments:
depth : int
cb : block<(node: BehNode?;depth:int):void>
- SequenceBehNode : CompositeBehNode
Runs children in order until one fails or returns Running.
- Fields:
debugSymbol : string = “[seq]” - Succeeds only if all children succeed. Equivalent to boolean AND
- SequenceBehNode.tick(nodeId: NodeId): BehResult
Advance through children; stop on Fail or Running, succeed if all pass
- Arguments:
nodeId : NodeId
- SelectorBehNode : CompositeBehNode
Runs children in order until one succeeds or returns Running.
- Fields:
debugSymbol : string = “[sel]” - Fails only if all children fail. Equivalent to boolean OR
- SelectorBehNode.tick(nodeId: NodeId): BehResult
Advance through children; stop on Success or Running, fail if all fail
- Arguments:
nodeId : NodeId
- ParallelBehNode : CompositeBehNode
Runs all children every tick.
- Fields:
debugSymbol : string = “[parallel]” - Returns Fail if any child fails, Success if any succeeds, Running if all are still running
- ParallelBehNode.tick(nodeId: NodeId): BehResult
Tick all children and aggregate results
- Arguments:
nodeId : NodeId
- ChanceBehNode : DecoratorBehNode
Runs the child with a given probability; otherwise returns Fail. The roll is performed once on entry (in firstTick) and reused until the node completes
- Fields:
debugSymbol : string = “{Chance}” - chance to run the child, between 0 and 1
chance : float = 0.5f - result of the chance roll for the current tick cycle
- ChanceBehNode.firstTick(nodeId: NodeId)
Roll the dice once on entry
- Arguments:
nodeId : NodeId
- ChanceBehNode.tick(nodeId: NodeId): BehResult
Tick the child if the roll succeeded, otherwise return Fail
- Arguments:
nodeId : NodeId
Functions
- BehResult(val: bool): BehResult
Explicit conversion from bool: true becomes Success, false becomes Fail
- Arguments:
val : bool
- dump_tree(node: BehNode?; include_reactions: bool = false): string
Return a formatted string representation of the behavior tree. The active node is marked with “>”; each node shows its debugSymbol and lastResult
- Arguments:
node : BehNode?
include_reactions : bool
- react(node: BehNode?; nodeId: NodeId): tuple<node:BehNode?;result:BehResult>
Evaluate reactions bottom-up from the active child to the root. Returns the first reaction that succeeds, or Fail if none trigger