.. _stdlib_behtree: ============= Behavior Tree ============= .. das:module:: behtree 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( new SequenceBehNode(name = "attack", children = array( new HasEnemy(name = "has enemy"), new ShootAtEnemy(name = "shoot") )), new SequenceBehNode(name = "wander", children = array( 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 } - ``SelectorBehNode`` tries children in order and succeeds on the first success (like boolean OR) - ``SequenceBehNode`` runs children in order and fails on the first failure (like boolean AND) - ``ParallelBehNode`` runs all children simultaneously - ``ChanceBehNode`` runs its child with a given probability - ``NotBehNode`` inverts 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( new SequenceBehNode(name = "enemy spotted", children = array( new DetectEnemy(name = "detect"), new ResetTree(name = "reset") )) )), children = array(...) ) 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( 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: " 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 ++++++++++++ .. _enum-behtree-BehResult: .. das:attribute:: 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 +++++++ .. _struct-behtree-BehNode: .. das:attribute:: BehNode :Fields: * **parent** : :ref:`BehNode `? - parent of the node * **name** : string - name of the node * **debugSymbol** : string - symbol displayed in debug output, e.g. "[seq]", "[sel]" * **lastResult** : :ref:`BehResult ` - last result of the node, useful for debugging * **reaction** : :ref:`BehNode `? - optional reaction subtree, evaluated bottom-up via react() .. _function-behtree_BehNode_rq_init_BehNode: .. das:function:: 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 .. _function-behtree_BehNode_rq_firstTick_BehNode_NodeId: .. das:function:: BehNode.firstTick(nodeId: NodeId) Called before the first tick of the node. Override to initialize any state needed for the behavior :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_BehNode_rq_lastTick_BehNode_NodeId: .. das:function:: 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** : :ref:`NodeId ` .. _function-behtree_BehNode_rq_update_BehNode_NodeId: .. das:function:: 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** : :ref:`NodeId ` .. _function-behtree_BehNode_rq_reset_BehNode_NodeId: .. das:function:: BehNode.reset(nodeId: NodeId) Reset the node to its initial state. Override to add custom reset logic :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_BehNode_rq_fullNodeName_BehNode: .. das:function:: BehNode.fullNodeName() : string Return the full hierarchical path of this node. E.g. "root/attack/search target" .. _function-behtree_BehNode_rq_activeChild_BehNode: .. das:function:: BehNode.activeChild() : BehNode? Return the deepest active child of the node. Returns itself for leaf nodes .. _function-behtree_BehNode_rq_debugState_BehNode: .. das:function:: BehNode.debugState() : string Return a formatted debug string with the active node path and last result .. _function-behtree_BehNode_rq_iter_BehNode_int_block_ls_var_node_c_BehNode_q_;depth_c_int_c_void_gr_: .. das:function:: 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: :ref:`BehNode `?;depth:int):void> .. _function-behtree_BehNode: .. das:function:: BehNode() : BehNode Default constructor .. _struct-behtree-DecoratorBehNode: .. das:attribute:: DecoratorBehNode : BehNode Abstract base class for decorator nodes that wrap a single child :Fields: * **child** : :ref:`BehNode `? - the single child node wrapped by this decorator .. _function-behtree_DecoratorBehNode_rq_init_DecoratorBehNode: .. das:function:: DecoratorBehNode.init() Set up the child's parent reference and initialize it .. _function-behtree_DecoratorBehNode_rq_firstTick_DecoratorBehNode_NodeId: .. das:function:: DecoratorBehNode.firstTick(nodeId: NodeId) Propagate firstTick to the child, then call the base implementation :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_DecoratorBehNode_rq_lastTick_DecoratorBehNode_NodeId: .. das:function:: DecoratorBehNode.lastTick(nodeId: NodeId) Propagate lastTick to the child, then call the base implementation :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_DecoratorBehNode_rq_reset_DecoratorBehNode_NodeId: .. das:function:: DecoratorBehNode.reset(nodeId: NodeId) Reset the child, then reset self :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_DecoratorBehNode_rq_activeChild_DecoratorBehNode: .. das:function:: DecoratorBehNode.activeChild() : BehNode? Delegate to the child's active child .. _function-behtree_DecoratorBehNode_rq_iter_DecoratorBehNode_int_block_ls_var_node_c_BehNode_q_;depth_c_int_c_void_gr_: .. das:function:: 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: :ref:`BehNode `?;depth:int):void> .. _function-behtree_DecoratorBehNode_BehNode_q_: .. das:function:: DecoratorBehNode(child: BehNode?) : DecoratorBehNode Construct with a child node :Arguments: * **child** : :ref:`BehNode `? .. _struct-behtree-NotBehNode: .. das:attribute:: NotBehNode : DecoratorBehNode Inverts the child's result: Success becomes Fail and vice versa. :Fields: * **debugSymbol** : string = "\{!\}" - Running is passed through unchanged .. _function-behtree_NotBehNode_rq_tick_NotBehNode_NodeId: .. das:function:: NotBehNode.tick(nodeId: NodeId) : BehResult Tick the child and invert Success/Fail :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-TrueBehNode: .. das:attribute:: TrueBehNode : BehNode :Fields: * **debugSymbol** : string = "[Yes]" - Leaf node that always returns Success .. _function-behtree_TrueBehNode_rq_tick_TrueBehNode_NodeId: .. das:function:: TrueBehNode.tick(nodeId: NodeId) : BehResult Always return Success :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-FalseBehNode: .. das:attribute:: FalseBehNode : BehNode :Fields: * **debugSymbol** : string = "[No]" - Leaf node that always returns Fail .. _function-behtree_FalseBehNode_rq_tick_FalseBehNode_NodeId: .. das:function:: FalseBehNode.tick(nodeId: NodeId) : BehResult Always return Fail :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-SuccessBehNode: .. das:attribute:: SuccessBehNode : DecoratorBehNode Forces Success regardless of the child's result. :Fields: * **debugSymbol** : string = "\{Success\}" - Running is passed through unchanged .. _function-behtree_SuccessBehNode_rq_tick_SuccessBehNode_NodeId: .. das:function:: SuccessBehNode.tick(nodeId: NodeId) : BehResult Tick the child and return Success unless it is Running :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-FailureBehNode: .. das:attribute:: FailureBehNode : DecoratorBehNode Forces Fail regardless of the child's result. :Fields: * **debugSymbol** : string = "\{Failure\}" - Running is passed through unchanged .. _function-behtree_FailureBehNode_rq_tick_FailureBehNode_NodeId: .. das:function:: FailureBehNode.tick(nodeId: NodeId) : BehResult Tick the child and return Fail unless it is Running :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-RunningBehNode: .. das:attribute:: 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 .. _function-behtree_RunningBehNode_rq_tick_RunningBehNode_NodeId: .. das:function:: RunningBehNode.tick(nodeId: NodeId) : BehResult Tick the child and return Running, unless an exit condition is met :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-WaitBehNode: .. das:attribute:: 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 .. _function-behtree_WaitBehNode_rq_firstTick_WaitBehNode_NodeId: .. das:function:: WaitBehNode.firstTick(nodeId: NodeId) Pick a random delay and record the expiry time :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_WaitBehNode_rq_tick_WaitBehNode_NodeId: .. das:function:: WaitBehNode.tick(nodeId: NodeId) : BehResult Return Running until the delay has elapsed, then return Success :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-CompositeBehNode: .. das:attribute:: CompositeBehNode : BehNode Abstract base class for composite nodes with multiple children :Fields: * **children** : array< :ref:`BehNode `?> - list of child nodes * **currentChild** : int = 0 - index of the currently executing child .. _function-behtree_CompositeBehNode_rq_init_CompositeBehNode: .. das:function:: CompositeBehNode.init() Set up parent references for all children and initialize them .. _function-behtree_CompositeBehNode_rq_firstTick_CompositeBehNode_NodeId: .. das:function:: CompositeBehNode.firstTick(nodeId: NodeId) Reset currentChild to 0 so execution starts from the first child :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_CompositeBehNode_rq_reset_CompositeBehNode_NodeId: .. das:function:: CompositeBehNode.reset(nodeId: NodeId) Reset all children and the currentChild index :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_CompositeBehNode_rq_activeChild_CompositeBehNode: .. das:function:: CompositeBehNode.activeChild() : BehNode? Return the active child of the currently executing child node .. _function-behtree_CompositeBehNode_rq_iter_CompositeBehNode_int_block_ls_var_node_c_BehNode_q_;depth_c_int_c_void_gr_: .. das:function:: 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: :ref:`BehNode `?;depth:int):void> .. _struct-behtree-SequenceBehNode: .. das:attribute:: 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 .. _function-behtree_SequenceBehNode_rq_tick_SequenceBehNode_NodeId: .. das:function:: SequenceBehNode.tick(nodeId: NodeId) : BehResult Advance through children; stop on Fail or Running, succeed if all pass :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-SelectorBehNode: .. das:attribute:: 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 .. _function-behtree_SelectorBehNode_rq_tick_SelectorBehNode_NodeId: .. das:function:: SelectorBehNode.tick(nodeId: NodeId) : BehResult Advance through children; stop on Success or Running, fail if all fail :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-ParallelBehNode: .. das:attribute:: 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 .. _function-behtree_ParallelBehNode_rq_tick_ParallelBehNode_NodeId: .. das:function:: ParallelBehNode.tick(nodeId: NodeId) : BehResult Tick all children and aggregate results :Arguments: * **nodeId** : :ref:`NodeId ` .. _struct-behtree-ChanceBehNode: .. das:attribute:: 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 .. _function-behtree_ChanceBehNode_rq_firstTick_ChanceBehNode_NodeId: .. das:function:: ChanceBehNode.firstTick(nodeId: NodeId) Roll the dice once on entry :Arguments: * **nodeId** : :ref:`NodeId ` .. _function-behtree_ChanceBehNode_rq_tick_ChanceBehNode_NodeId: .. das:function:: ChanceBehNode.tick(nodeId: NodeId) : BehResult Tick the child if the roll succeeded, otherwise return Fail :Arguments: * **nodeId** : :ref:`NodeId ` +++++++++ Functions +++++++++ * :ref:`BehResult (val: bool) : BehResult ` * :ref:`dump_tree (var node: BehNode?; include_reactions: bool = false) : string ` * :ref:`react (var node: BehNode?; nodeId: NodeId) : tuple\ ` .. _function-behtree_BehResult_bool: .. das:function:: BehResult(val: bool) : BehResult Explicit conversion from bool: true becomes Success, false becomes Fail :Arguments: * **val** : bool .. _function-behtree_dump_tree_BehNode_q__bool: .. das:function:: 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** : :ref:`BehNode `? * **include_reactions** : bool .. _function-behtree_react_BehNode_q__NodeId: .. das:function:: react(node: BehNode?; nodeId: NodeId) : tuple Evaluate reactions bottom-up from the active child to the root. Returns the first reaction that succeeds, or Fail if none trigger :Arguments: * **node** : :ref:`BehNode `? * **nodeId** : :ref:`NodeId `