add beehave

This commit is contained in:
2025-01-28 13:53:59 +08:00
parent d6b69c14ac
commit 144a01556d
112 changed files with 6075 additions and 205 deletions
+46
View File
@@ -0,0 +1,46 @@
@tool
class_name BeehaveNode extends Node
## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or
## `RUNNING` when ticked.
enum { SUCCESS, FAILURE, RUNNING }
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = []
if get_children().any(func(x): return not (x is BeehaveNode)):
warnings.append("All children of this node should inherit from BeehaveNode class.")
return warnings
## Executes this node and returns a status code.
## This method must be overwritten.
func tick(actor: Node, blackboard: Blackboard) -> int:
return SUCCESS
## Called when this node needs to be interrupted before it can return FAILURE or SUCCESS.
func interrupt(actor: Node, blackboard: Blackboard) -> void:
pass
## Called before the first time it ticks by the parent.
func before_run(actor: Node, blackboard: Blackboard) -> void:
pass
## Called after the last time it ticks and returns
## [code]SUCCESS[/code] or [code]FAILURE[/code].
func after_run(actor: Node, blackboard: Blackboard) -> void:
pass
func get_class_name() -> Array[StringName]:
return [&"BeehaveNode"]
func can_send_message(blackboard: Blackboard) -> bool:
return blackboard.get_value("can_send_message", false)
+329
View File
@@ -0,0 +1,329 @@
@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(), blackboard.get_debug_data())
if self.get_child_count() == 1:
tick()
if _can_send_message:
BeehaveDebuggerMessages.process_end(get_instance_id(), blackboard.get_debug_data())
# 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, blackboard.get_debug_data())
BeehaveDebuggerMessages.process_tick(get_instance_id(), status, blackboard.get_debug_data())
# 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")
@@ -0,0 +1,34 @@
@tool
@icon("../../icons/category_composite.svg")
class_name Composite extends BeehaveNode
## A Composite node controls the flow of execution of its children in a specific manner.
var running_child: BeehaveNode = null
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()
if get_children().filter(func(x): return x is BeehaveNode).size() < 2:
warnings.append(
"Any composite node should have at least two children. Otherwise it is not useful."
)
return warnings
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Composite")
return classes
@@ -0,0 +1,176 @@
@tool
class_name RandomizedComposite extends Composite
const WEIGHTS_PREFIX = "Weights/"
## Sets a predicable seed
@export var random_seed: int = 0:
set(rs):
random_seed = rs
if random_seed != 0:
seed(random_seed)
else:
randomize()
## Wether to use weights for every child or not.
@export var use_weights: bool:
set(value):
use_weights = value
if use_weights:
_update_weights(get_children())
_connect_children_changing_signals()
notify_property_list_changed()
var _weights: Dictionary
var _exiting_tree: bool
func _ready():
_connect_children_changing_signals()
func _connect_children_changing_signals():
if not child_entered_tree.is_connected(_on_child_entered_tree):
child_entered_tree.connect(_on_child_entered_tree)
if not child_exiting_tree.is_connected(_on_child_exiting_tree):
child_exiting_tree.connect(_on_child_exiting_tree)
func get_shuffled_children() -> Array[Node]:
var children_bag: Array[Node] = get_children().duplicate()
if use_weights:
var weights: Array[int]
weights.assign(children_bag.map(func(child): return _weights[child.name]))
children_bag.assign(_weighted_shuffle(children_bag, weights))
else:
children_bag.shuffle()
return children_bag
## Returns a shuffled version of a given array using the supplied array of weights.
## Think of weights as the chance of a given item being the first in the array.
func _weighted_shuffle(items: Array, weights: Array[int]) -> Array:
if len(items) != len(weights):
push_error(
(
"items and weights size mismatch: expected %d weights, got %d instead."
% [len(items), len(weights)]
)
)
return items
# This method is based on the weighted random sampling algorithm
# by Efraimidis, Spirakis; 2005. This runs in O(n log(n)).
# For each index, it will calculate random_value^(1/weight).
var chance_calc = func(i): return [i, randf() ** (1.0 / weights[i])]
var random_distribuition = range(len(items)).map(chance_calc)
# Now we just have to order by the calculated value, descending.
random_distribuition.sort_custom(func(a, b): return a[1] > b[1])
return random_distribuition.map(func(dist): return items[dist[0]])
func _get_property_list():
var properties = []
if use_weights:
for key in _weights.keys():
properties.append(
{
"name": WEIGHTS_PREFIX + key,
"type": TYPE_INT,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "1,100"
}
)
return properties
func _set(property: StringName, value: Variant) -> bool:
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
_weights[weight_name] = value
return true
return false
func _get(property: StringName):
if property.begins_with(WEIGHTS_PREFIX):
var weight_name = property.trim_prefix(WEIGHTS_PREFIX)
return _weights[weight_name]
return null
func _update_weights(children: Array[Node]) -> void:
var new_weights = {}
for c in children:
if _weights.has(c.name):
new_weights[c.name] = _weights[c.name]
else:
new_weights[c.name] = 1
_weights = new_weights
notify_property_list_changed()
func _exit_tree() -> void:
_exiting_tree = true
func _enter_tree() -> void:
_exiting_tree = false
func _on_child_entered_tree(node: Node):
_update_weights(get_children())
var renamed_callable = _on_child_renamed.bind(node.name, node)
if not node.renamed.is_connected(renamed_callable):
node.renamed.connect(renamed_callable)
if not node.tree_exited.is_connected(_on_child_tree_exited):
node.tree_exited.connect(_on_child_tree_exited.bind(node))
func _on_child_exiting_tree(node: Node):
var renamed_callable = _on_child_renamed.bind(node.name, node)
if node.renamed.is_connected(renamed_callable):
node.renamed.disconnect(renamed_callable)
func _on_child_tree_exited(node: Node) -> void:
# don't erase the individual child if the whole tree is exiting together
if not _exiting_tree:
var children = get_children()
children.erase(node)
_update_weights(children)
if node.tree_exited.is_connected(_on_child_tree_exited):
node.tree_exited.disconnect(_on_child_tree_exited)
func _on_child_renamed(old_name: String, renamed_child: Node):
if old_name == renamed_child.name:
return # No need to update the weights.
# Disconnect signal with old name...
renamed_child.renamed.disconnect(_on_child_renamed.bind(old_name, renamed_child))
# ...and connect with the new name.
renamed_child.renamed.connect(_on_child_renamed.bind(renamed_child.name, renamed_child))
var original_weight = _weights[old_name]
_weights.erase(old_name)
_weights[renamed_child.name] = original_weight
notify_property_list_changed()
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"RandomizedComposite")
return classes
@@ -0,0 +1,69 @@
@tool
@icon("../../icons/selector.svg")
class_name SelectorComposite extends Composite
## Selector nodes will attempt to execute each of its children until one of
## them return `SUCCESS`. If all children return `FAILURE`, this node will also
## return `FAILURE`.
## If a child returns `RUNNING` it will tick again.
var last_execution_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < last_execution_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_cleanup_running_task(c, actor, blackboard)
last_execution_index += 1
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
func after_run(actor: Node, blackboard: Blackboard) -> void:
last_execution_index = 0
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
last_execution_index = 0
super(actor, blackboard)
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name: String = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorComposite")
return classes
@@ -0,0 +1,82 @@
@tool
@icon("../../icons/selector_random.svg")
class_name SelectorRandomComposite extends RandomizedComposite
## This node will attempt to execute all of its children just like a
## [code]SelectorStar[/code] would, with the exception that the children
## will be executed in a random order.
## A shuffled list of the children that will be executed in reverse order.
var _children_bag: Array[Node] = []
var c: Node
func _ready() -> void:
super()
if random_seed == 0:
randomize()
func tick(actor: Node, blackboard: Blackboard) -> int:
if _children_bag.is_empty():
_reset()
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
_children_bag.erase(c)
c.after_run(actor, blackboard)
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
func after_run(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func _get_reversed_indexes() -> Array[int]:
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
func _reset() -> void:
var new_order = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorRandomComposite")
return classes
@@ -0,0 +1,47 @@
@tool
@icon("../../icons/selector_reactive.svg")
class_name SelectorReactiveComposite extends Composite
## Selector Reactive nodes will attempt to execute each of its children until one of
## them return `SUCCESS`. If all children return `FAILURE`, this node will also
## return `FAILURE`.
## If a child returns `RUNNING` it will restart.
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
# Interrupt any child that was RUNNING before.
if c != running_child:
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return SUCCESS
FAILURE:
c.after_run(actor, blackboard)
RUNNING:
if c != running_child:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return FAILURE
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SelectorReactiveComposite")
return classes
@@ -0,0 +1,75 @@
@tool
@icon("../../icons/sequence.svg")
class_name SequenceComposite extends Composite
## Sequence nodes will attempt to execute all of its children and report
## `SUCCESS` in case all of the children report a `SUCCESS` status code.
## If at least one child reports a `FAILURE` status code, this node will also
## return `FAILURE` and restart.
## In case a child returns `RUNNING` this node will tick again.
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_cleanup_running_task(c, actor, blackboard)
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
_cleanup_running_task(c, actor, blackboard)
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
if c != running_child:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name: String = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceComposite")
return classes
@@ -0,0 +1,96 @@
@tool
@icon("../../icons/sequence_random.svg")
class_name SequenceRandomComposite extends RandomizedComposite
## This node will attempt to execute all of its children just like a
## [code]SequenceStar[/code] would, with the exception that the children
## will be executed in a random order.
# Emitted whenever the children are shuffled.
signal reset(new_order: Array[Node])
## Whether the sequence should start where it left off after a previous failure.
@export var resume_on_failure: bool = false
## Whether the sequence should start where it left off after a previous interruption.
@export var resume_on_interrupt: bool = false
## A shuffled list of the children that will be executed in reverse order.
var _children_bag: Array[Node] = []
var c: Node
func _ready() -> void:
super()
if random_seed == 0:
randomize()
func tick(actor: Node, blackboard: Blackboard) -> int:
if _children_bag.is_empty():
_reset()
# We need to traverse the array in reverse since we will be manipulating it.
for i in _get_reversed_indexes():
c = _children_bag[i]
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
_children_bag.erase(c)
c.after_run(actor, blackboard)
FAILURE:
_children_bag.erase(c)
# Interrupt any child that was RUNNING before
# but do not reset!
super.interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
return SUCCESS
func after_run(actor: Node, blackboard: Blackboard) -> void:
if not resume_on_failure:
_reset()
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if not resume_on_interrupt:
_reset()
super(actor, blackboard)
func _get_reversed_indexes() -> Array[int]:
var reversed: Array[int]
reversed.assign(range(_children_bag.size()))
reversed.reverse()
return reversed
func _reset() -> void:
var new_order: Array[Node] = get_shuffled_children()
_children_bag = new_order.duplicate()
_children_bag.reverse() # It needs to run the children in reverse order.
reset.emit(new_order)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceRandomComposite")
return classes
@@ -0,0 +1,63 @@
@tool
@icon("../../icons/sequence_reactive.svg")
class_name SequenceReactiveComposite extends Composite
## Reactive Sequence nodes will attempt to execute all of its children and report
## `SUCCESS` in case all of the children report a `SUCCESS` status code.
## If at least one child reports a `FAILURE` status code, this node will also
## return `FAILURE` and restart.
## In case a child returns `RUNNING` this node will restart.
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
# Interrupt any child that was RUNNING before.
interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
_reset()
if running_child != c:
interrupt(actor, blackboard)
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceReactiveComposite")
return classes
@@ -0,0 +1,61 @@
@tool
@icon("../../icons/sequence_reactive.svg")
class_name SequenceStarComposite extends Composite
## Sequence Star nodes will attempt to execute all of its children and report
## `SUCCESS` in case all of the children report a `SUCCESS` status code.
## If at least one child reports a `FAILURE` status code, this node will also
## return `FAILURE` and tick again.
## In case a child returns `RUNNING` this node will tick again.
var successful_index: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
for c in get_children():
if c.get_index() < successful_index:
continue
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
successful_index += 1
c.after_run(actor, blackboard)
FAILURE:
# Interrupt any child that was RUNNING before
# but do not reset!
super.interrupt(actor, blackboard)
c.after_run(actor, blackboard)
return FAILURE
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_reset()
return SUCCESS
func interrupt(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func _reset() -> void:
successful_index = 0
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SequenceStarComposite")
return classes
@@ -0,0 +1,122 @@
@tool
@icon("../../icons/simple_parallel.svg")
class_name SimpleParallelComposite extends Composite
## Simple Parallel nodes will attampt to execute all chidren at same time and
## can only have exactly two children. First child as primary node, second
## child as secondary node.
## This node will always report primary node's state, and continue tick while
## primary node return 'RUNNING'. The state of secondary node will be ignored
## and executed like a subtree.
## If primary node return 'SUCCESS' or 'FAILURE', this node will interrupt
## secondary node and return primary node's result.
## If this node is running under delay mode, it will wait seconday node
## finish its action after primary node terminates.
#how many times should secondary node repeat, zero means loop forever
@export var secondary_node_repeat_count: int = 0
#wether to wait secondary node finish its current action after primary node finished
@export var delay_mode: bool = false
var delayed_result := SUCCESS
var main_task_finished: bool = false
var secondary_node_running: bool = false
var secondary_node_repeat_left: int = 0
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()
if get_child_count() != 2:
warnings.append("SimpleParallel should have exactly two child nodes.")
if not get_child(0) is ActionLeaf:
warnings.append("SimpleParallel should have an action leaf node as first child node.")
return warnings
func tick(actor, blackboard: Blackboard):
for c in get_children():
var node_index: int = c.get_index()
if node_index == 0 and not main_task_finished:
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
delayed_result = response
match response:
SUCCESS, FAILURE:
_cleanup_running_task(c, actor, blackboard)
c.after_run(actor, blackboard)
main_task_finished = true
if not delay_mode:
if secondary_node_running:
get_child(1).interrupt(actor, blackboard)
_reset()
return delayed_result
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
elif node_index == 1:
if secondary_node_repeat_count == 0 or secondary_node_repeat_left > 0:
if not secondary_node_running:
c.before_run(actor, blackboard)
var subtree_response = c.tick(actor, blackboard)
if subtree_response != RUNNING:
secondary_node_running = false
c.after_run(actor, blackboard)
if delay_mode and main_task_finished:
_reset()
return delayed_result
elif secondary_node_repeat_left > 0:
secondary_node_repeat_left -= 1
else:
secondary_node_running = true
return RUNNING
func before_run(actor: Node, blackboard: Blackboard) -> void:
secondary_node_repeat_left = secondary_node_repeat_count
super(actor, blackboard)
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if not main_task_finished:
get_child(0).interrupt(actor, blackboard)
if secondary_node_running:
get_child(1).interrupt(actor, blackboard)
_reset()
super(actor, blackboard)
func after_run(actor: Node, blackboard: Blackboard) -> void:
_reset()
super(actor, blackboard)
func _reset() -> void:
main_task_finished = false
secondary_node_running = false
## Changes `running_action` and `running_child` after the node finishes executing.
func _cleanup_running_task(finished_action: Node, actor: Node, blackboard: Blackboard):
var blackboard_name = str(actor.get_instance_id())
if finished_action == running_child:
running_child = null
if finished_action == blackboard.get_value("running_action", null, blackboard_name):
blackboard.set_value("running_action", null, blackboard_name)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"SimpleParallelComposite")
return classes
@@ -0,0 +1,49 @@
@tool
@icon("../../icons/cooldown.svg")
extends Decorator
class_name CooldownDecorator
## The Cooldown Decorator will return 'FAILURE' for a set amount of time
## after executing its child.
## The timer resets the next time its child is executed and it is not `RUNNING`
## The wait time in seconds
@export var wait_time := 0.0
@onready var cache_key = "cooldown_%s" % self.get_instance_id()
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
var remaining_time: float = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
var response: int
if c != running_child:
c.before_run(actor, blackboard)
if remaining_time > 0:
response = FAILURE
remaining_time -= get_physics_process_delta_time()
blackboard.set_value(cache_key, remaining_time, str(actor.get_instance_id()))
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response, blackboard.get_debug_data())
else:
response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING and c is ActionLeaf:
running_child = c
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
if response != RUNNING:
blackboard.set_value(cache_key, wait_time, str(actor.get_instance_id()))
return response
@@ -0,0 +1,33 @@
@tool
@icon("../../icons/category_decorator.svg")
class_name Decorator extends BeehaveNode
## Decorator nodes are used to transform the result received by its child.
## Must only have one child.
var running_child: BeehaveNode = null
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = super._get_configuration_warnings()
if get_child_count() != 1:
warnings.append("Decorator should have exactly one child node.")
return warnings
func interrupt(actor: Node, blackboard: Blackboard) -> void:
if running_child != null:
running_child.interrupt(actor, blackboard)
running_child = null
func after_run(actor: Node, blackboard: Blackboard) -> void:
running_child = null
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Decorator")
return classes
@@ -0,0 +1,49 @@
@tool
@icon("../../icons/delayer.svg")
extends Decorator
class_name DelayDecorator
## The Delay Decorator will return 'RUNNING' for a set amount of time
## before executing its child.
## The timer resets when both it and its child are not `RUNNING`
## The wait time in seconds
@export var wait_time := 0.0
@onready var cache_key = "time_limiter_%s" % self.get_instance_id()
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
var total_time: float = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
var response: int
if c != running_child:
c.before_run(actor, blackboard)
if total_time < wait_time:
response = RUNNING
total_time += get_physics_process_delta_time()
blackboard.set_value(cache_key, total_time, str(actor.get_instance_id()))
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(self.get_instance_id(), response, blackboard.get_debug_data())
else:
response = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING and c is ActionLeaf:
running_child = c
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
if response != RUNNING:
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
return response
+35
View File
@@ -0,0 +1,35 @@
@tool
@icon("../../icons/failer.svg")
class_name AlwaysFailDecorator extends Decorator
## A Failer node will always return a `FAILURE` status code.
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return FAILURE
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"AlwaysFailDecorator")
return classes
@@ -0,0 +1,43 @@
@tool
@icon("../../icons/inverter.svg")
class_name InverterDecorator extends Decorator
## An inverter will return `FAILURE` in case it's child returns a `SUCCESS` status
## code or `SUCCESS` in case its child returns a `FAILURE` status code.
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
match response:
SUCCESS:
c.after_run(actor, blackboard)
return FAILURE
FAILURE:
c.after_run(actor, blackboard)
return SUCCESS
RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
_:
push_error("This should be unreachable")
return -1
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"InverterDecorator")
return classes
@@ -0,0 +1,60 @@
@tool
@icon("../../icons/limiter.svg")
class_name LimiterDecorator extends Decorator
## The limiter will execute its `RUNNING` child `x` amount of times. When the number of
## maximum ticks is reached, it will return a `FAILURE` status code.
## The count resets the next time that a child is not `RUNNING`
@onready var cache_key = "limiter_%s" % self.get_instance_id()
@export var max_count: int = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
if not get_child_count() == 1:
return FAILURE
var child: BeehaveNode = get_child(0)
var current_count: int = blackboard.get_value(cache_key, 0, str(actor.get_instance_id()))
if current_count < max_count:
blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id()))
var response: int = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response, blackboard.get_debug_data())
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if child is ActionLeaf and response == RUNNING:
running_child = child
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
if response != RUNNING:
child.after_run(actor, blackboard)
return response
else:
interrupt(actor, blackboard)
child.after_run(actor, blackboard)
return FAILURE
func before_run(actor: Node, blackboard: Blackboard) -> void:
blackboard.set_value(cache_key, 0, str(actor.get_instance_id()))
if get_child_count() > 0:
get_child(0).before_run(actor, blackboard)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"LimiterDecorator")
return classes
func _get_configuration_warnings() -> PackedStringArray:
if not get_child_count() == 1:
return ["Requires exactly one child node"]
return []
@@ -0,0 +1,58 @@
## The repeater will execute its child until it returns `SUCCESS` a certain amount of times.
## When the number of maximum ticks is reached, it will return a `SUCCESS` status code.
## If the child returns `FAILURE`, the repeater will return `FAILURE` immediately.
@tool
@icon("../../icons/repeater.svg")
class_name RepeaterDecorator extends Decorator
@export var repetitions: int = 1
var current_count: int = 0
func before_run(actor: Node, blackboard: Blackboard):
current_count = 0
func tick(actor: Node, blackboard: Blackboard) -> int:
var child: BeehaveNode = get_child(0)
if current_count < repetitions:
if running_child == null:
child.before_run(actor, blackboard)
var response: int = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response, blackboard.get_debug_data())
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
return RUNNING
current_count += 1
child.after_run(actor, blackboard)
if running_child != null:
running_child = null
if response == FAILURE:
return FAILURE
if current_count >= repetitions:
return SUCCESS
return RUNNING
else:
return SUCCESS
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"LimiterDecorator")
return classes
@@ -0,0 +1,35 @@
@tool
@icon("../../icons/succeeder.svg")
class_name AlwaysSucceedDecorator extends Decorator
## A succeeder node will always return a `SUCCESS` status code.
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
else:
c.after_run(actor, blackboard)
return SUCCESS
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"AlwaysSucceedDecorator")
return classes
@@ -0,0 +1,60 @@
@tool
@icon("../../icons/limiter.svg")
class_name TimeLimiterDecorator extends Decorator
## The Time Limit Decorator will give its `RUNNING` child a set amount of time to finish
## before interrupting it and return a `FAILURE` status code.
## The timer resets the next time that a child is not `RUNNING`
@export var wait_time := 0.0
@onready var cache_key: String = "time_limiter_%s" % self.get_instance_id()
func tick(actor: Node, blackboard: Blackboard) -> int:
if not get_child_count() == 1:
return FAILURE
var child: BeehaveNode = self.get_child(0)
var time_left: float = blackboard.get_value(cache_key, 0.0, str(actor.get_instance_id()))
if time_left < wait_time:
time_left += get_physics_process_delta_time()
blackboard.set_value(cache_key, time_left, str(actor.get_instance_id()))
var response: int = child.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response, blackboard.get_debug_data())
if child is ConditionLeaf:
blackboard.set_value("last_condition", child, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = child
if child is ActionLeaf:
blackboard.set_value("running_action", child, str(actor.get_instance_id()))
else:
child.after_run(actor, blackboard)
return response
else:
interrupt(actor, blackboard)
child.after_run(actor, blackboard)
return FAILURE
func before_run(actor: Node, blackboard: Blackboard) -> void:
blackboard.set_value(cache_key, 0.0, str(actor.get_instance_id()))
if get_child_count() > 0:
get_child(0).before_run(actor, blackboard)
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"TimeLimiterDecorator")
return classes
func _get_configuration_warnings() -> PackedStringArray:
if not get_child_count() == 1:
return ["Requires exactly one child node"]
return []
@@ -0,0 +1,33 @@
@tool
@icon("../../icons/until_fail.svg")
class_name UntilFailDecorator
extends Decorator
## The UntilFail Decorator will return `RUNNING` if its child returns
## `SUCCESS` or `RUNNING` or it will return `SUCCESS` if its child returns
## `FAILURE`
func tick(actor: Node, blackboard: Blackboard) -> int:
var c: BeehaveNode = get_child(0)
if c != running_child:
c.before_run(actor, blackboard)
var response: int = c.tick(actor, blackboard)
if can_send_message(blackboard):
BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response, blackboard.get_debug_data())
if c is ConditionLeaf:
blackboard.set_value("last_condition", c, str(actor.get_instance_id()))
blackboard.set_value("last_condition_status", response, str(actor.get_instance_id()))
if response == RUNNING:
running_child = c
if c is ActionLeaf:
blackboard.set_value("running_action", c, str(actor.get_instance_id()))
return RUNNING
if response == SUCCESS:
return RUNNING
return SUCCESS
+14
View File
@@ -0,0 +1,14 @@
@tool
@icon("../../icons/action.svg")
class_name ActionLeaf extends Leaf
## Actions are leaf nodes that define a task to be performed by an actor.
## Their execution can be long running, potentially being called across multiple
## frame executions. In this case, the node should return `RUNNING` until the
## action is completed.
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"ActionLeaf")
return classes
@@ -0,0 +1,65 @@
@tool
class_name BlackboardCompareCondition extends ConditionLeaf
## Compares two values using the specified comparison operator.
## Returns [code]FAILURE[/code] if any of the expression fails or the
## comparison operation returns [code]false[/code], otherwise it returns [code]SUCCESS[/code].
enum Operators {
EQUAL,
NOT_EQUAL,
GREATER,
LESS,
GREATER_EQUAL,
LESS_EQUAL,
}
## Expression represetning left operand.
## This value can be any valid GDScript expression.
## In order to use the existing blackboard keys for comparison,
## use get_value("key_name") e.g. get_value("direction").length()
@export_placeholder(EXPRESSION_PLACEHOLDER) var left_operand: String = ""
## Comparison operator.
@export_enum("==", "!=", ">", "<", ">=", "<=") var operator: int = 0
## Expression represetning right operand.
## This value can be any valid GDScript expression.
## In order to use the existing blackboard keys for comparison,
## use get_value("key_name") e.g. get_value("direction").length()
@export_placeholder(EXPRESSION_PLACEHOLDER) var right_operand: String = ""
@onready var _left_expression: Expression = _parse_expression(left_operand)
@onready var _right_expression: Expression = _parse_expression(right_operand)
func tick(actor: Node, blackboard: Blackboard) -> int:
var left: Variant = _left_expression.execute([], blackboard)
if _left_expression.has_execute_failed():
return FAILURE
var right: Variant = _right_expression.execute([], blackboard)
if _right_expression.has_execute_failed():
return FAILURE
var result: bool = false
match operator:
Operators.EQUAL:
result = left == right
Operators.NOT_EQUAL:
result = left != right
Operators.GREATER:
result = left > right
Operators.LESS:
result = left < right
Operators.GREATER_EQUAL:
result = left >= right
Operators.LESS_EQUAL:
result = left <= right
return SUCCESS if result else FAILURE
func _get_expression_sources() -> Array[String]:
return [left_operand, right_operand]
@@ -0,0 +1,25 @@
@tool
class_name BlackboardEraseAction extends ActionLeaf
## Erases the specified key from the blackboard.
## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code].
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
@onready var _key_expression: Expression = _parse_expression(key)
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
blackboard.erase_value(key_value)
return SUCCESS
func _get_expression_sources() -> Array[String]:
return [key]
@@ -0,0 +1,23 @@
@tool
class_name BlackboardHasCondition extends ConditionLeaf
## Returns [code]FAILURE[/code] if expression execution fails or the specified key doesn't exist.
## Returns [code]SUCCESS[/code] if blackboard has the specified key.
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
@onready var _key_expression: Expression = _parse_expression(key)
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
return SUCCESS if blackboard.has_value(key_value) else FAILURE
func _get_expression_sources() -> Array[String]:
return [key]
@@ -0,0 +1,33 @@
@tool
class_name BlackboardSetAction extends ActionLeaf
## Sets the specified key to the specified value.
## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code].
## Expression representing a blackboard key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var key: String = ""
## Expression representing a blackboard value to assign to the specified key.
@export_placeholder(EXPRESSION_PLACEHOLDER) var value: String = ""
@onready var _key_expression: Expression = _parse_expression(key)
@onready var _value_expression: Expression = _parse_expression(value)
func tick(actor: Node, blackboard: Blackboard) -> int:
var key_value: Variant = _key_expression.execute([], blackboard)
if _key_expression.has_execute_failed():
return FAILURE
var value_value: Variant = _value_expression.execute([], blackboard)
if _value_expression.has_execute_failed():
return FAILURE
blackboard.set_value(key_value, value_value)
return SUCCESS
func _get_expression_sources() -> Array[String]:
return [key, value]
+12
View File
@@ -0,0 +1,12 @@
@tool
@icon("../../icons/condition.svg")
class_name ConditionLeaf extends Leaf
## Conditions are leaf nodes that either return SUCCESS or FAILURE depending on
## a single simple condition. They should never return `RUNNING`.
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"ConditionLeaf")
return classes
+48
View File
@@ -0,0 +1,48 @@
@tool
@icon("../../icons/category_leaf.svg")
class_name Leaf extends BeehaveNode
## Base class for all leaf nodes of the tree.
const EXPRESSION_PLACEHOLDER: String = "Insert an expression..."
func _get_configuration_warnings() -> PackedStringArray:
var warnings: PackedStringArray = []
var children: Array[Node] = get_children()
if children.any(func(x): return x is BeehaveNode):
warnings.append("Leaf nodes should not have any child nodes. They won't be ticked.")
for source in _get_expression_sources():
var error_text: String = _parse_expression(source).get_error_text()
if not error_text.is_empty():
warnings.append("Expression `%s` is invalid! Error text: `%s`" % [source, error_text])
return warnings
func _parse_expression(source: String) -> Expression:
var result: Expression = Expression.new()
var error: int = result.parse(source)
if not Engine.is_editor_hint() and error != OK:
push_error(
(
"[Leaf] Couldn't parse expression with source: `%s` Error text: `%s`"
% [source, result.get_error_text()]
)
)
return result
func _get_expression_sources() -> Array[String]: # virtual
return []
func get_class_name() -> Array[StringName]:
var classes := super()
classes.push_back(&"Leaf")
return classes