@tool @icon("../icons/tree.svg") class_name BeehaveTree extends Node ## Controls the flow of execution of the entire behavior tree. enum { SUCCESS, FAILURE, RUNNING } enum ProcessThread { IDLE, PHYSICS } signal tree_enabled signal tree_disabled ## Whether this behavior tree should be enabled or not. @export var enabled: bool = true: set(value): enabled = value set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) set_process(enabled and process_thread == ProcessThread.IDLE) if value: tree_enabled.emit() else: interrupt() tree_disabled.emit() get: return enabled ## How often the tree should tick, in frames. The default value of 1 means ## tick() runs every frame. @export var tick_rate: int = 1 ## An optional node path this behavior tree should apply to. @export_node_path var actor_node_path: NodePath: set(anp): actor_node_path = anp if actor_node_path != null and str(actor_node_path) != "..": actor = get_node(actor_node_path) else: actor = get_parent() if Engine.is_editor_hint(): update_configuration_warnings() ## Whether to run this tree in a physics or idle thread. @export var process_thread: ProcessThread = ProcessThread.PHYSICS: set(value): process_thread = value set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) set_process(enabled and process_thread == ProcessThread.IDLE) ## Custom blackboard node. An internal blackboard will be used ## if no blackboard is provided explicitly. @export var blackboard: Blackboard: set(b): blackboard = b if blackboard and _internal_blackboard: remove_child(_internal_blackboard) _internal_blackboard.free() _internal_blackboard = null elif not blackboard and not _internal_blackboard: _internal_blackboard = Blackboard.new() add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) get: # in case blackboard is accessed before this node is, # we need to ensure that the internal blackboard is used. if not blackboard and not _internal_blackboard: _internal_blackboard = Blackboard.new() add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) return blackboard if blackboard else _internal_blackboard ## When enabled, this tree is tracked individually ## as a custom monitor. @export var custom_monitor = false: set(b): custom_monitor = b if custom_monitor and _process_time_metric_name != "": Performance.add_custom_monitor( _process_time_metric_name, _get_process_time_metric_value ) _get_global_metrics().register_tree(self) else: if _process_time_metric_name != "": # Remove tree metric from the engine Performance.remove_custom_monitor(_process_time_metric_name) _get_global_metrics().unregister_tree(self) BeehaveDebuggerMessages.unregister_tree(get_instance_id()) @export var actor: Node: set(a): actor = a if actor == null: actor = get_parent() if Engine.is_editor_hint(): update_configuration_warnings() var status: int = -1 var last_tick: int = 0 var _internal_blackboard: Blackboard var _process_time_metric_name: String var _process_time_metric_value: float = 0.0 var _can_send_message: bool = false func _ready() -> void: var connect_scene_tree_signal = func(signal_name: String, is_added: bool): if not get_tree().is_connected(signal_name, _on_scene_tree_node_added_removed.bind(is_added)): get_tree().connect(signal_name, _on_scene_tree_node_added_removed.bind(is_added)) connect_scene_tree_signal.call("node_added", true) connect_scene_tree_signal.call("node_removed", false) if not process_thread: process_thread = ProcessThread.PHYSICS if not actor: if actor_node_path: actor = get_node(actor_node_path) else: actor = get_parent() if not blackboard: # invoke setter to auto-initialise the blackboard. self.blackboard = null # Get the name of the parent node name for metric _process_time_metric_name = ( "beehave [microseconds]/process_time_%s-%s" % [actor.name, get_instance_id()] ) set_physics_process(enabled and process_thread == ProcessThread.PHYSICS) set_process(enabled and process_thread == ProcessThread.IDLE) # Register custom metric to the engine if custom_monitor and not Engine.is_editor_hint(): Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value) _get_global_metrics().register_tree(self) if Engine.is_editor_hint(): update_configuration_warnings.call_deferred() else: _get_global_debugger().register_tree(self) BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)) # Randomize at what frames tick() will happen to avoid stutters last_tick = randi_range(0, tick_rate - 1) func _on_scene_tree_node_added_removed(node: Node, is_added: bool) -> void: if Engine.is_editor_hint(): return if node is BeehaveNode and is_ancestor_of(node): var sgnal := node.ready if is_added else node.tree_exited if is_added: sgnal.connect( func() -> void: BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)), CONNECT_ONE_SHOT ) else: sgnal.connect( func() -> void: BeehaveDebuggerMessages.unregister_tree(get_instance_id()) request_ready() ) func _physics_process(_delta: float) -> void: _process_internally() func _process(_delta: float) -> void: _process_internally() func _process_internally() -> void: if Engine.is_editor_hint(): return if last_tick < tick_rate - 1: last_tick += 1 return last_tick = 0 # Start timing for metric var start_time = Time.get_ticks_usec() blackboard.set_value("can_send_message", _can_send_message) if _can_send_message: BeehaveDebuggerMessages.process_begin(get_instance_id()) if self.get_child_count() == 1: tick() if _can_send_message: BeehaveDebuggerMessages.process_end(get_instance_id()) # Check the cost for this frame and save it for metric report _process_time_metric_value = Time.get_ticks_usec() - start_time func tick() -> int: if actor == null or get_child_count() == 0: return FAILURE var child := self.get_child(0) if status != RUNNING: child.before_run(actor, blackboard) status = child.tick(actor, blackboard) if _can_send_message: BeehaveDebuggerMessages.process_tick(child.get_instance_id(), status) BeehaveDebuggerMessages.process_tick(get_instance_id(), status) # Clear running action if nothing is running if status != RUNNING: blackboard.set_value("running_action", null, str(actor.get_instance_id())) child.after_run(actor, blackboard) return status func _get_configuration_warnings() -> PackedStringArray: var warnings: PackedStringArray = [] if actor == null: warnings.append("Configure target node on tree") if get_children().any(func(x): return not (x is BeehaveNode)): warnings.append("All children of this node should inherit from BeehaveNode class.") if get_child_count() != 1: warnings.append("BeehaveTree should have exactly one child node.") return warnings ## Returns the currently running action func get_running_action() -> ActionLeaf: return blackboard.get_value("running_action", null, str(actor.get_instance_id())) ## Returns the last condition that was executed func get_last_condition() -> ConditionLeaf: return blackboard.get_value("last_condition", null, str(actor.get_instance_id())) ## Returns the status of the last executed condition func get_last_condition_status() -> String: if blackboard.has_value("last_condition_status", str(actor.get_instance_id())): var status = blackboard.get_value( "last_condition_status", null, str(actor.get_instance_id()) ) if status == SUCCESS: return "SUCCESS" elif status == FAILURE: return "FAILURE" else: return "RUNNING" return "" ## interrupts this tree if anything was running func interrupt() -> void: if self.get_child_count() != 0: var first_child = self.get_child(0) if "interrupt" in first_child: first_child.interrupt(actor, blackboard) ## Enables this tree. func enable() -> void: self.enabled = true ## Disables this tree. func disable() -> void: self.enabled = false func _exit_tree() -> void: if custom_monitor: if _process_time_metric_name != "": # Remove tree metric from the engine Performance.remove_custom_monitor(_process_time_metric_name) _get_global_metrics().unregister_tree(self) BeehaveDebuggerMessages.unregister_tree(get_instance_id()) # Called by the engine to profile this tree func _get_process_time_metric_value() -> int: return int(_process_time_metric_value) func _get_debugger_data(node: Node) -> Dictionary: if not (node is BeehaveTree or node is BeehaveNode): return {} var data := { path = node.get_path(), name = node.name, type = node.get_class_name(), id = str(node.get_instance_id()) } if node.get_child_count() > 0: data.children = [] for child in node.get_children(): var child_data := _get_debugger_data(child) if not child_data.is_empty(): data.children.push_back(child_data) return data func get_class_name() -> Array[StringName]: return [&"BeehaveTree"] # required to avoid lifecycle issues on initial load # due to loading order problems with autoloads func _get_global_metrics() -> Node: return get_tree().root.get_node("BeehaveGlobalMetrics") # required to avoid lifecycle issues on initial load # due to loading order problems with autoloads func _get_global_debugger() -> Node: return get_tree().root.get_node("BeehaveGlobalDebugger")