From 1aed9881496d2e3cd8cf36a553b238c6f4bdd7c4 Mon Sep 17 00:00:00 2001 From: Mathilde Grapin Date: Mon, 12 Jun 2023 16:48:35 +0200 Subject: [PATCH] Add a behavior tree to the enemy Add the Beehave addon. Make the enemy do the same thing as before but with a behavior tree. --- addons/beehave/LICENSE | 21 ++ addons/beehave/blackboard.gd | 33 +++ addons/beehave/debug/debugger.gd | 91 ++++++ addons/beehave/debug/debugger_messages.gd | 31 +++ addons/beehave/debug/debugger_tab.gd | 107 ++++++++ addons/beehave/debug/frames.gd | 33 +++ addons/beehave/debug/global_debugger.gd | 38 +++ addons/beehave/debug/graph_edit.gd | 259 ++++++++++++++++++ addons/beehave/debug/graph_node.gd | 145 ++++++++++ .../beehave/debug/icons/horizontal_layout.svg | 1 + .../debug/icons/horizontal_layout.svg.import | 37 +++ addons/beehave/debug/icons/port_bottom.svg | 1 + .../debug/icons/port_bottom.svg.import | 37 +++ addons/beehave/debug/icons/port_left.svg | 1 + .../beehave/debug/icons/port_left.svg.import | 37 +++ addons/beehave/debug/icons/port_right.svg | 1 + .../beehave/debug/icons/port_right.svg.import | 37 +++ addons/beehave/debug/icons/port_top.svg | 1 + .../beehave/debug/icons/port_top.svg.import | 37 +++ .../beehave/debug/icons/vertical_layout.svg | 1 + .../debug/icons/vertical_layout.svg.import | 37 +++ addons/beehave/debug/tree_node.gd | 255 +++++++++++++++++ addons/beehave/icons/action.svg | 38 +++ addons/beehave/icons/action.svg.import | 37 +++ addons/beehave/icons/blackboard.svg | 38 +++ addons/beehave/icons/blackboard.svg.import | 37 +++ addons/beehave/icons/category_bt.svg | 38 +++ addons/beehave/icons/category_bt.svg.import | 37 +++ addons/beehave/icons/category_composite.svg | 38 +++ .../icons/category_composite.svg.import | 37 +++ addons/beehave/icons/category_decorator.svg | 38 +++ .../icons/category_decorator.svg.import | 37 +++ addons/beehave/icons/category_leaf.svg | 38 +++ addons/beehave/icons/category_leaf.svg.import | 37 +++ addons/beehave/icons/condition.svg | 38 +++ addons/beehave/icons/condition.svg.import | 37 +++ addons/beehave/icons/failer.svg | 38 +++ addons/beehave/icons/failer.svg.import | 37 +++ addons/beehave/icons/inverter.svg | 38 +++ addons/beehave/icons/inverter.svg.import | 37 +++ addons/beehave/icons/limiter.svg | 38 +++ addons/beehave/icons/limiter.svg.import | 37 +++ addons/beehave/icons/selector.svg | 38 +++ addons/beehave/icons/selector.svg.import | 37 +++ addons/beehave/icons/selector_random.svg | 35 +++ .../beehave/icons/selector_random.svg.import | 37 +++ addons/beehave/icons/selector_reactive.svg | 45 +++ .../icons/selector_reactive.svg.import | 37 +++ addons/beehave/icons/sequence.svg | 38 +++ addons/beehave/icons/sequence.svg.import | 37 +++ addons/beehave/icons/sequence_random.svg | 38 +++ .../beehave/icons/sequence_random.svg.import | 37 +++ addons/beehave/icons/sequence_reactive.svg | 60 ++++ .../icons/sequence_reactive.svg.import | 37 +++ addons/beehave/icons/succeeder.svg | 38 +++ addons/beehave/icons/succeeder.svg.import | 37 +++ addons/beehave/icons/tree.svg | 38 +++ addons/beehave/icons/tree.svg.import | 37 +++ .../beehave/metrics/beehave_global_metrics.gd | 54 ++++ addons/beehave/nodes/beehave_node.gd | 49 ++++ addons/beehave/nodes/beehave_tree.gd | 224 +++++++++++++++ addons/beehave/nodes/composites/composite.gd | 40 +++ addons/beehave/nodes/composites/selector.gd | 57 ++++ .../nodes/composites/selector_random.gd | 88 ++++++ .../nodes/composites/selector_reactive.gd | 45 +++ addons/beehave/nodes/composites/sequence.gd | 64 +++++ .../nodes/composites/sequence_random.gd | 96 +++++++ .../nodes/composites/sequence_reactive.gd | 62 +++++ .../beehave/nodes/composites/sequence_star.gd | 58 ++++ addons/beehave/nodes/decorators/decorator.gd | 41 +++ addons/beehave/nodes/decorators/failer.gd | 34 +++ addons/beehave/nodes/decorators/inverter.gd | 42 +++ addons/beehave/nodes/decorators/limiter.gd | 41 +++ addons/beehave/nodes/decorators/succeeder.gd | 34 +++ .../beehave/nodes/decorators/time_limiter.gd | 46 ++++ addons/beehave/nodes/leaves/action.gd | 13 + .../nodes/leaves/blackboard_compare.gd | 61 +++++ .../beehave/nodes/leaves/blackboard_erase.gd | 25 ++ addons/beehave/nodes/leaves/blackboard_has.gd | 23 ++ addons/beehave/nodes/leaves/blackboard_set.gd | 34 +++ addons/beehave/nodes/leaves/condition.gd | 11 + addons/beehave/nodes/leaves/leaf.gd | 46 ++++ addons/beehave/plugin.cfg | 7 + addons/beehave/plugin.gd | 24 ++ addons/beehave/utils/utils.gd | 22 ++ project.godot | 9 + scenes/enemy.tscn | 5 +- scenes/enemy_behavior_tree.tscn | 22 ++ scripts/enemy.gd | 24 -- scripts/get_random_top_cell.gd | 11 + scripts/is_idle_condition.gd | 5 + scripts/move_to_target_action.gd | 16 ++ 92 files changed, 4025 insertions(+), 25 deletions(-) create mode 100644 addons/beehave/LICENSE create mode 100644 addons/beehave/blackboard.gd create mode 100644 addons/beehave/debug/debugger.gd create mode 100644 addons/beehave/debug/debugger_messages.gd create mode 100644 addons/beehave/debug/debugger_tab.gd create mode 100644 addons/beehave/debug/frames.gd create mode 100644 addons/beehave/debug/global_debugger.gd create mode 100644 addons/beehave/debug/graph_edit.gd create mode 100644 addons/beehave/debug/graph_node.gd create mode 100644 addons/beehave/debug/icons/horizontal_layout.svg create mode 100644 addons/beehave/debug/icons/horizontal_layout.svg.import create mode 100644 addons/beehave/debug/icons/port_bottom.svg create mode 100644 addons/beehave/debug/icons/port_bottom.svg.import create mode 100644 addons/beehave/debug/icons/port_left.svg create mode 100644 addons/beehave/debug/icons/port_left.svg.import create mode 100644 addons/beehave/debug/icons/port_right.svg create mode 100644 addons/beehave/debug/icons/port_right.svg.import create mode 100644 addons/beehave/debug/icons/port_top.svg create mode 100644 addons/beehave/debug/icons/port_top.svg.import create mode 100644 addons/beehave/debug/icons/vertical_layout.svg create mode 100644 addons/beehave/debug/icons/vertical_layout.svg.import create mode 100644 addons/beehave/debug/tree_node.gd create mode 100644 addons/beehave/icons/action.svg create mode 100644 addons/beehave/icons/action.svg.import create mode 100644 addons/beehave/icons/blackboard.svg create mode 100644 addons/beehave/icons/blackboard.svg.import create mode 100644 addons/beehave/icons/category_bt.svg create mode 100644 addons/beehave/icons/category_bt.svg.import create mode 100644 addons/beehave/icons/category_composite.svg create mode 100644 addons/beehave/icons/category_composite.svg.import create mode 100644 addons/beehave/icons/category_decorator.svg create mode 100644 addons/beehave/icons/category_decorator.svg.import create mode 100644 addons/beehave/icons/category_leaf.svg create mode 100644 addons/beehave/icons/category_leaf.svg.import create mode 100644 addons/beehave/icons/condition.svg create mode 100644 addons/beehave/icons/condition.svg.import create mode 100644 addons/beehave/icons/failer.svg create mode 100644 addons/beehave/icons/failer.svg.import create mode 100644 addons/beehave/icons/inverter.svg create mode 100644 addons/beehave/icons/inverter.svg.import create mode 100644 addons/beehave/icons/limiter.svg create mode 100644 addons/beehave/icons/limiter.svg.import create mode 100644 addons/beehave/icons/selector.svg create mode 100644 addons/beehave/icons/selector.svg.import create mode 100644 addons/beehave/icons/selector_random.svg create mode 100644 addons/beehave/icons/selector_random.svg.import create mode 100644 addons/beehave/icons/selector_reactive.svg create mode 100644 addons/beehave/icons/selector_reactive.svg.import create mode 100644 addons/beehave/icons/sequence.svg create mode 100644 addons/beehave/icons/sequence.svg.import create mode 100644 addons/beehave/icons/sequence_random.svg create mode 100644 addons/beehave/icons/sequence_random.svg.import create mode 100644 addons/beehave/icons/sequence_reactive.svg create mode 100644 addons/beehave/icons/sequence_reactive.svg.import create mode 100644 addons/beehave/icons/succeeder.svg create mode 100644 addons/beehave/icons/succeeder.svg.import create mode 100644 addons/beehave/icons/tree.svg create mode 100644 addons/beehave/icons/tree.svg.import create mode 100644 addons/beehave/metrics/beehave_global_metrics.gd create mode 100644 addons/beehave/nodes/beehave_node.gd create mode 100644 addons/beehave/nodes/beehave_tree.gd create mode 100644 addons/beehave/nodes/composites/composite.gd create mode 100644 addons/beehave/nodes/composites/selector.gd create mode 100644 addons/beehave/nodes/composites/selector_random.gd create mode 100644 addons/beehave/nodes/composites/selector_reactive.gd create mode 100644 addons/beehave/nodes/composites/sequence.gd create mode 100644 addons/beehave/nodes/composites/sequence_random.gd create mode 100644 addons/beehave/nodes/composites/sequence_reactive.gd create mode 100644 addons/beehave/nodes/composites/sequence_star.gd create mode 100644 addons/beehave/nodes/decorators/decorator.gd create mode 100644 addons/beehave/nodes/decorators/failer.gd create mode 100644 addons/beehave/nodes/decorators/inverter.gd create mode 100644 addons/beehave/nodes/decorators/limiter.gd create mode 100644 addons/beehave/nodes/decorators/succeeder.gd create mode 100644 addons/beehave/nodes/decorators/time_limiter.gd create mode 100644 addons/beehave/nodes/leaves/action.gd create mode 100644 addons/beehave/nodes/leaves/blackboard_compare.gd create mode 100644 addons/beehave/nodes/leaves/blackboard_erase.gd create mode 100644 addons/beehave/nodes/leaves/blackboard_has.gd create mode 100644 addons/beehave/nodes/leaves/blackboard_set.gd create mode 100644 addons/beehave/nodes/leaves/condition.gd create mode 100644 addons/beehave/nodes/leaves/leaf.gd create mode 100644 addons/beehave/plugin.cfg create mode 100644 addons/beehave/plugin.gd create mode 100644 addons/beehave/utils/utils.gd create mode 100644 scenes/enemy_behavior_tree.tscn create mode 100644 scripts/get_random_top_cell.gd create mode 100644 scripts/is_idle_condition.gd create mode 100644 scripts/move_to_target_action.gd diff --git a/addons/beehave/LICENSE b/addons/beehave/LICENSE new file mode 100644 index 0000000..caabbff --- /dev/null +++ b/addons/beehave/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 bitbrain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/beehave/blackboard.gd b/addons/beehave/blackboard.gd new file mode 100644 index 0000000..5655e89 --- /dev/null +++ b/addons/beehave/blackboard.gd @@ -0,0 +1,33 @@ +## The blackboard is an object that can be used to store and access data between +## multiple nodes of the behavior tree. +@icon("icons/blackboard.svg") +class_name Blackboard extends Node + +var blackboard: Dictionary = {} + +func keys() -> Array[String]: + var keys: Array[String] + keys.assign(blackboard.keys().duplicate()) + return keys + + +func set_value(key: Variant, value: Variant, blackboard_name: String = 'default') -> void: + if not blackboard.has(blackboard_name): + blackboard[blackboard_name] = {} + + blackboard[blackboard_name][key] = value + + +func get_value(key: Variant, default_value: Variant = null, blackboard_name: String = 'default') -> Variant: + if has_value(key, blackboard_name): + return blackboard[blackboard_name].get(key, default_value) + return default_value + + +func has_value(key: Variant, blackboard_name: String = 'default') -> bool: + return blackboard.has(blackboard_name) and blackboard[blackboard_name].has(key) and blackboard[blackboard_name][key] != null + + +func erase_value(key: Variant, blackboard_name: String = 'default') -> void: + if blackboard.has(blackboard_name): + blackboard[blackboard_name][key] = null diff --git a/addons/beehave/debug/debugger.gd b/addons/beehave/debug/debugger.gd new file mode 100644 index 0000000..1ccd097 --- /dev/null +++ b/addons/beehave/debug/debugger.gd @@ -0,0 +1,91 @@ +@tool +extends EditorDebuggerPlugin + +const DebuggerTab := preload("debugger_tab.gd") +var debugger_tab := DebuggerTab.new() +var floating_window: Window +var session: EditorDebuggerSession + + +func _has_capture(prefix: String) -> bool: + return prefix == "beehave" + + +func _capture(message: String, data: Array, session_id: int) -> bool: + # in case the behavior tree has invalid setup this might be null + if debugger_tab == null: + return false + + if message == "beehave:register_tree": + debugger_tab.register_tree(data[0]) + return true + if message == "beehave:unregister_tree": + debugger_tab.unregister_tree(data[0]) + return true + if message == "beehave:process_tick": + debugger_tab.graph.process_tick(data[0], data[1]) + return true + if message == "beehave:process_begin": + debugger_tab.graph.process_begin(data[0]) + return true + if message == "beehave:process_end": + debugger_tab.graph.process_end(data[0]) + return true + return false + + +func _setup_session(session_id: int) -> void: + session = get_session(session_id) + session.started.connect(debugger_tab.start) + session.stopped.connect(debugger_tab.stop) + + debugger_tab.name = "🐝 Beehave" + debugger_tab.make_floating.connect(_on_make_floating) + debugger_tab.session = session + session.add_session_tab(debugger_tab) + + +func _on_make_floating() -> void: + var plugin := BeehaveUtils.get_plugin() + if not plugin: + return + if floating_window: + _on_window_close_requested() + return + + var border_size := Vector2(4, 4) * BeehaveUtils.get_editor_scale() + var editor_interface: EditorInterface = plugin.get_editor_interface() + var editor_main_screen = editor_interface.get_editor_main_screen() + debugger_tab.get_parent().remove_child(debugger_tab) + + floating_window = Window.new() + + var panel := Panel.new() + panel.add_theme_stylebox_override("panel", editor_interface.get_base_control().get_theme_stylebox("PanelForeground", "EditorStyles")) + panel.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + floating_window.add_child(panel) + + var margin := MarginContainer.new() + margin.add_child(debugger_tab) + margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + margin.add_theme_constant_override("margin_right", border_size.x) + margin.add_theme_constant_override("margin_left", border_size.x) + margin.add_theme_constant_override("margin_top", border_size.y) + margin.add_theme_constant_override("margin_bottom", border_size.y) + panel.add_child(margin) + + floating_window.title = "🐝 Beehave" + floating_window.wrap_controls = true + floating_window.min_size = Vector2i(600, 350) + floating_window.size = debugger_tab.size + floating_window.position = editor_main_screen.global_position + floating_window.transient = true + floating_window.close_requested.connect(_on_window_close_requested) + editor_interface.get_base_control().add_child(floating_window) + + +func _on_window_close_requested() -> void: + debugger_tab.get_parent().remove_child(debugger_tab) + session.add_session_tab(debugger_tab) + floating_window.queue_free() + floating_window = null diff --git a/addons/beehave/debug/debugger_messages.gd b/addons/beehave/debug/debugger_messages.gd new file mode 100644 index 0000000..3fb49fb --- /dev/null +++ b/addons/beehave/debug/debugger_messages.gd @@ -0,0 +1,31 @@ +class_name BeehaveDebuggerMessages + + +static func can_send_message() -> bool: + return not Engine.is_editor_hint() and OS.has_feature("editor") + + +static func register_tree(beehave_tree: Dictionary) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:register_tree", [beehave_tree]) + + +static func unregister_tree(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:unregister_tree", [instance_id]) + + +static func process_tick(instance_id: int, status: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_tick", [instance_id, status]) + + +static func process_begin(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_begin", [instance_id]) + + +static func process_end(instance_id: int) -> void: + if can_send_message(): + EngineDebugger.send_message("beehave:process_end", [instance_id]) + diff --git a/addons/beehave/debug/debugger_tab.gd b/addons/beehave/debug/debugger_tab.gd new file mode 100644 index 0000000..3987d77 --- /dev/null +++ b/addons/beehave/debug/debugger_tab.gd @@ -0,0 +1,107 @@ +@tool +extends PanelContainer + +signal make_floating() + +const BeehaveGraphEdit := preload("graph_edit.gd") +const TREE_ICON := preload("../icons/tree.svg") + +var container: HSplitContainer +var item_list: ItemList +var graph: BeehaveGraphEdit +var message: Label + +var active_trees: Dictionary +var active_tree_id: int = -1 +var session: EditorDebuggerSession + + +func _ready() -> void: + container = HSplitContainer.new() + add_child(container) + + item_list = ItemList.new() + item_list.custom_minimum_size = Vector2(200, 0) + item_list.item_selected.connect(_on_item_selected) + container.add_child(item_list) + + graph = BeehaveGraphEdit.new() + container.add_child(graph) + + message = Label.new() + message.text = "Run Project for debugging" + message.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + message.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + message.set_anchors_preset(Control.PRESET_CENTER) + add_child(message) + + var button := Button.new() + button.flat = true + button.icon = get_theme_icon(&"ExternalLink", &"EditorIcons") + button.pressed.connect(func(): make_floating.emit()) + button.tooltip_text = "Make floating" + button.focus_mode = Control.FOCUS_NONE + graph.get_zoom_hbox().add_child(button) + + var toggle_button := Button.new() + toggle_button.flat = true + toggle_button.icon = get_theme_icon(&"Back", &"EditorIcons") + toggle_button.pressed.connect(_on_toggle_button_pressed.bind(toggle_button)) + toggle_button.tooltip_text = "Toggle Panel" + toggle_button.focus_mode = Control.FOCUS_NONE + graph.get_zoom_hbox().add_child(toggle_button) + graph.get_zoom_hbox().move_child(toggle_button, 0) + + stop() + visibility_changed.connect(_on_visibility_changed) + + +func start() -> void: + container.visible = true + message.visible = false + + +func stop() -> void: + container.visible = false + message.visible = true + + active_trees.clear() + item_list.clear() + graph.beehave_tree = {} + + +func register_tree(data: Dictionary) -> void: + var idx := item_list.add_item(data.name, TREE_ICON) + item_list.set_item_tooltip(idx, data.path) + item_list.set_item_metadata(idx, data.id) + active_trees[data.id] = data + + +func unregister_tree(instance_id: int) -> void: + var id := str(instance_id) + for i in item_list.item_count: + if item_list.get_item_metadata(i) == id: + item_list.remove_item(i) + break + + active_trees.erase(id) + + if graph.beehave_tree.get("id", "") == id: + graph.beehave_tree = {} + + +func _on_toggle_button_pressed(toggle_button: Button) -> void: + item_list.visible = !item_list.visible + toggle_button.icon = get_theme_icon(&"Back" if item_list.visible else &"Forward", &"EditorIcons") + + +func _on_item_selected(idx: int) -> void: + var id: StringName = item_list.get_item_metadata(idx) + graph.beehave_tree = active_trees.get(id, {}) + + active_tree_id = id.to_int() + session.send_message("beehave:activate_tree", [active_tree_id]) + + +func _on_visibility_changed() -> void: + session.send_message("beehave:visibility_changed", [visible and is_visible_in_tree()]) diff --git a/addons/beehave/debug/frames.gd b/addons/beehave/debug/frames.gd new file mode 100644 index 0000000..0fb6d2a --- /dev/null +++ b/addons/beehave/debug/frames.gd @@ -0,0 +1,33 @@ +@tool +extends RefCounted + +const SUCCESS_COLOR := Color("#009944c8") +const NORMAL_COLOR := Color("#15181e") +const FAILURE_COLOR := Color("#cf000f80") +const RUNNING_COLOR := Color("#ffcc00c8") + +var empty: StyleBoxEmpty +var normal: StyleBoxFlat +var success: StyleBoxFlat +var failure: StyleBoxFlat +var running: StyleBoxFlat + + +func _init() -> void: + var plugin := BeehaveUtils.get_plugin() + if not plugin: + return + + var editor_scale := BeehaveUtils.get_editor_scale() + + empty = StyleBoxEmpty.new() + + normal = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"frame", &"GraphNode").duplicate() + + success = plugin.get_editor_interface().get_base_control().get_theme_stylebox(&"selected_frame", &"GraphNode").duplicate() + failure = success.duplicate() + running = success.duplicate() + + success.border_color = SUCCESS_COLOR + failure.border_color = FAILURE_COLOR + running.border_color = RUNNING_COLOR diff --git a/addons/beehave/debug/global_debugger.gd b/addons/beehave/debug/global_debugger.gd new file mode 100644 index 0000000..27c72c2 --- /dev/null +++ b/addons/beehave/debug/global_debugger.gd @@ -0,0 +1,38 @@ +extends Node + +var _registered_trees: Dictionary +var _active_tree: BeehaveTree + + +func _enter_tree() -> void: + EngineDebugger.register_message_capture("beehave", _on_debug_message) + + +func _on_debug_message(message: String, data: Array) -> bool: + if message == "activate_tree": + _set_active_tree(data[0]) + return true + if message == "visibility_changed": + if _active_tree: + _active_tree._can_send_message = data[0] + return true + return false + + +func _set_active_tree(tree_id: int) -> void: + var tree: BeehaveTree = _registered_trees.get(tree_id, null) + if not tree: + return + + if _active_tree: + _active_tree._can_send_message = false + _active_tree = tree + _active_tree._can_send_message = true + + +func register_tree(tree: BeehaveTree) -> void: + _registered_trees[tree.get_instance_id()] = tree + + +func unregister_tree(tree: BeehaveTree) -> void: + _registered_trees.erase(tree.get_instance_id()) diff --git a/addons/beehave/debug/graph_edit.gd b/addons/beehave/debug/graph_edit.gd new file mode 100644 index 0000000..afdcd1e --- /dev/null +++ b/addons/beehave/debug/graph_edit.gd @@ -0,0 +1,259 @@ +@tool +extends GraphEdit + +const BeehaveGraphNode := preload("graph_node.gd") + +const HORIZONTAL_LAYOUT_ICON := preload("icons/horizontal_layout.svg") +const VERTICAL_LAYOUT_ICON := preload("icons/vertical_layout.svg") + +const PROGRESS_SHIFT: int = 50 +const INACTIVE_COLOR: Color = Color("#898989aa") +const ACTIVE_COLOR: Color = Color("#ffcc00c8") +const SUCCESS_COLOR: Color = Color("#009944c8") + + +var updating_graph: bool = false +var arraging_nodes: bool = false +var beehave_tree: Dictionary: + set(value): + if beehave_tree == value: + return + beehave_tree = value + active_nodes.clear() + _update_graph() + +var horizontal_layout: bool = false: + set(value): + if updating_graph or arraging_nodes: + return + if horizontal_layout == value: + return + horizontal_layout = value + _update_layout_button() + _update_graph() + + +var active_nodes: Array[String] +var progress: int = 0 +var layout_button: Button + + +func _ready() -> void: + custom_minimum_size = Vector2(100, 300) + arrange_nodes_button_hidden = true + minimap_enabled = false + + layout_button = Button.new() + layout_button.flat = true + layout_button.focus_mode = Control.FOCUS_NONE + layout_button.pressed.connect(func(): horizontal_layout = not horizontal_layout) + get_zoom_hbox().add_child(layout_button) + _update_layout_button() + + +func _update_graph() -> void: + if updating_graph: + return + + updating_graph = true + + clear_connections() + + for child in get_children(): + remove_child(child) + child.queue_free() + + if not beehave_tree.is_empty(): + _add_nodes(beehave_tree) + _connect_nodes(beehave_tree) + _arrange_nodes.call_deferred(beehave_tree) + + updating_graph = false + + +func _add_nodes(node: Dictionary) -> void: + if node.is_empty(): + return + var gnode := BeehaveGraphNode.new(horizontal_layout) + add_child(gnode) + gnode.title_text = node.name + gnode.name = node.id + gnode.icon = _get_icon(node.type.back()) + + if node.type.has(&"BeehaveTree"): + gnode.set_slots(false, true) + elif node.type.has(&"Leaf"): + gnode.set_slots(true, false) + elif node.type.has(&"Composite") or node.type.has(&"Decorator"): + gnode.set_slots(true, true) + + for child in node.get("children", []): + _add_nodes(child) + + +func _connect_nodes(node: Dictionary) -> void: + for child in node.get("children", []): + connect_node(node.id, 0, child.id, 0) + _connect_nodes(child) + + +func _arrange_nodes(node: Dictionary) -> void: + if arraging_nodes: + return + + arraging_nodes = true + + var tree_node := _create_tree_nodes(node) + tree_node.update_positions(horizontal_layout) + _place_nodes(tree_node) + + arraging_nodes = false + + +func _create_tree_nodes(node: Dictionary, root: TreeNode = null) -> TreeNode: + var tree_node := TreeNode.new(get_node(node.id), root) + for child in node.get("children", []): + var child_node := _create_tree_nodes(child, tree_node) + tree_node.children.push_back(child_node) + return tree_node + + +func _place_nodes(node: TreeNode) -> void: + node.item.position_offset = Vector2(node.x, node.y) + for child in node.children: + _place_nodes(child) + + +func _get_icon(type: StringName) -> Texture2D: + var classes := ProjectSettings.get_global_class_list() + for c in classes: + if c["class"] == type: + var icon_path := c.get("icon", String()) + if not icon_path.is_empty(): + return load(icon_path) + return null + + +func get_status(status: int) -> String: + if status == 0: + return "SUCCESS" + elif status == 1: + return "FAILURE" + return "RUNNING" + + +func process_begin(instance_id: int) -> void: + if not _is_same_tree(instance_id): + return + + for child in get_children(): + child.set_meta("status", -1) + + +func process_tick(instance_id: int, status: int) -> void: + var node := get_node_or_null(str(instance_id)) + if node: + node.text = "Status: %s" % get_status(status) + node.set_status(status) + node.set_meta("status", status) + if status == 0 or status == 2: + if not active_nodes.has(node.name): + active_nodes.push_back(node.name) + + +func process_end(instance_id: int) -> void: + if not _is_same_tree(instance_id): + return + + for child in get_children(): + var status := child.get_meta("status", -1) + match status: + 0: + active_nodes.erase(child.name) + child.set_color(SUCCESS_COLOR) + 1: + active_nodes.erase(child.name) + child.set_color(INACTIVE_COLOR) + 2: + child.set_color(ACTIVE_COLOR) + _: + child.text = " " + child.set_status(status) + child.set_color(INACTIVE_COLOR) + + +func _is_same_tree(instance_id: int) -> bool: + return str(instance_id) == beehave_tree.get("id", "") + + +func _get_connection_line(from_position: Vector2, to_position: Vector2) -> PackedVector2Array: + var points: PackedVector2Array + + from_position = from_position.round() + to_position = to_position.round() + + points.push_back(from_position) + + var mid_position := ((to_position + from_position) / 2).round() + if horizontal_layout: + points.push_back(Vector2(mid_position.x, from_position.y)) + points.push_back(Vector2(mid_position.x, to_position.y)) + else: + points.push_back(Vector2(from_position.x, mid_position.y)) + points.push_back(Vector2(to_position.x, mid_position.y)) + + points.push_back(to_position) + + return points + + +func _process(delta: float) -> void: + if not active_nodes.is_empty(): + progress += 10 if delta >= 0.05 else 1 + if progress >= 1000: + progress = 0 + queue_redraw() + + +func _draw() -> void: + if active_nodes.is_empty(): + return + + var circle_size: float = max(3, 6 * zoom) + var progress_shift: float = PROGRESS_SHIFT * zoom + + var connections := get_connection_list() + for c in connections: + if not c.from in active_nodes or not c.to in active_nodes: + continue + var from := get_node(String(c.from)) + var to := get_node(String(c.to)) + + if from.get_meta("status", -1) < 0 or to.get_meta("status", -1) < 0: + return + + var line := _get_connection_line(from.position + from.get_connection_output_position(c.from_port), to.position + to.get_connection_input_position(c.to_port)) + + var curve = Curve2D.new() + for l in line: + curve.add_point(l) + + var max_steps := int(curve.get_baked_length()) + var current_shift := progress % max_steps + var p := curve.sample_baked(current_shift) + draw_circle(p, circle_size, ACTIVE_COLOR) + + var shift := current_shift - progress_shift + while shift >= 0: + draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) + shift -= progress_shift + + shift = current_shift + progress_shift + while shift <= curve.get_baked_length(): + draw_circle(curve.sample_baked(shift), circle_size, ACTIVE_COLOR) + shift += progress_shift + + +func _update_layout_button() -> void: + layout_button.icon = VERTICAL_LAYOUT_ICON if horizontal_layout else HORIZONTAL_LAYOUT_ICON + layout_button.tooltip_text = "Switch to Vertical layout" if horizontal_layout else "Switch to Horizontal layout" diff --git a/addons/beehave/debug/graph_node.gd b/addons/beehave/debug/graph_node.gd new file mode 100644 index 0000000..6b262af --- /dev/null +++ b/addons/beehave/debug/graph_node.gd @@ -0,0 +1,145 @@ +@tool +extends GraphNode + +const DEFAULT_COLOR := Color("#dad4cb") + +const PORT_TOP_ICON := preload("icons/port_top.svg") +const PORT_BOTTOM_ICON := preload("icons/port_bottom.svg") +const PORT_LEFT_ICON := preload("icons/port_left.svg") +const PORT_RIGHT_ICON := preload("icons/port_right.svg") + + +@export var title_text: String: + set(value): + title_text = value + if title_label: + title_label.text = value + +@export var text: String: + set(value): + text = value + if label: + label.text = " " if text.is_empty() else text + +@export var icon: Texture2D: + set(value): + icon = value + if icon_rect: + icon_rect.texture = value + +var layout_size: float: + get: + return size.y if horizontal else size.x + + +var panel: PanelContainer +var icon_rect: TextureRect +var title_label: Label +var container: VBoxContainer +var label: Label + +var frames: RefCounted = BeehaveUtils.get_frames() +var horizontal: bool = false + + +func _init(horizontal: bool = false) -> void: + self.horizontal = horizontal + + +func _ready() -> void: + custom_minimum_size = Vector2(50, 50) * BeehaveUtils.get_editor_scale() + draggable = false + + add_theme_stylebox_override("frame", frames.empty) + add_theme_stylebox_override("selected_frame", frames.empty) + add_theme_color_override("close_color", Color.TRANSPARENT) + add_theme_icon_override("close", ImageTexture.new()) + + # For top port + add_child(Control.new()) + + panel = PanelContainer.new() + panel.mouse_filter = Control.MOUSE_FILTER_PASS + panel.add_theme_stylebox_override("panel", frames.normal) + add_child(panel) + + var vbox_container := VBoxContainer.new() + panel.add_child(vbox_container) + + var title_size := 24 * BeehaveUtils.get_editor_scale() + var margin_container := MarginContainer.new() + margin_container.add_theme_constant_override("margin_top", -title_size - 2 * BeehaveUtils.get_editor_scale()) + margin_container.mouse_filter = Control.MOUSE_FILTER_PASS + vbox_container.add_child(margin_container) + + var title_container := HBoxContainer.new() + title_container.add_child(Control.new()) + title_container.mouse_filter = Control.MOUSE_FILTER_PASS + title_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + margin_container.add_child(title_container) + + icon_rect = TextureRect.new() + icon_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + title_container.add_child(icon_rect) + + title_label = Label.new() + title_label.add_theme_color_override("font_color", DEFAULT_COLOR) + title_label.add_theme_font_override("font", get_theme_font("title_font")) + title_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL + title_label.text = title_text + title_container.add_child(title_label) + + title_container.add_child(Control.new()) + + container = VBoxContainer.new() + container.size_flags_vertical = Control.SIZE_EXPAND_FILL + container.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel.add_child(container) + + label = Label.new() + label.text = " " if text.is_empty() else text + container.add_child(label) + + # For bottom port + add_child(Control.new()) + + minimum_size_changed.connect(_on_size_changed) + _on_size_changed.call_deferred() + + +func set_status(status: int) -> void: + panel.add_theme_stylebox_override("panel", _get_stylebox(status)) + + +func _get_stylebox(status: int) -> StyleBox: + match status: + 0: return frames.success + 1: return frames.failure + 2: return frames.running + _: return frames.normal + + +func set_slots(left_enabled: bool, right_enabled: bool) -> void: + if horizontal: + set_slot(1, left_enabled, 0, Color.WHITE, right_enabled, 0, Color.WHITE, PORT_LEFT_ICON, PORT_RIGHT_ICON) + else: + set_slot(0, left_enabled, 0, Color.WHITE, false, -2, Color.TRANSPARENT, PORT_TOP_ICON, null) + set_slot(2, false, -1, Color.TRANSPARENT, right_enabled, 0, Color.WHITE, null, PORT_BOTTOM_ICON) + + +func set_color(color: Color) -> void: + set_input_color(color) + set_output_color(color) + + +func set_input_color(color: Color) -> void: + set_slot_color_left(1 if horizontal else 0, color) + + +func set_output_color(color: Color) -> void: + set_slot_color_right(1 if horizontal else 2, color) + + +func _on_size_changed(): + add_theme_constant_override("port_offset", 12 * BeehaveUtils.get_editor_scale() if horizontal else round(size.x / 2.0)) diff --git a/addons/beehave/debug/icons/horizontal_layout.svg b/addons/beehave/debug/icons/horizontal_layout.svg new file mode 100644 index 0000000..235dc62 --- /dev/null +++ b/addons/beehave/debug/icons/horizontal_layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/horizontal_layout.svg.import b/addons/beehave/debug/icons/horizontal_layout.svg.import new file mode 100644 index 0000000..ee765ea --- /dev/null +++ b/addons/beehave/debug/icons/horizontal_layout.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b4xw25mue1ktm" +path="res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/horizontal_layout.svg" +dest_files=["res://.godot/imported/horizontal_layout.svg-d2a7af351e44f9bf61d0c938b6f47fac.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/icons/port_bottom.svg b/addons/beehave/debug/icons/port_bottom.svg new file mode 100644 index 0000000..3ce70e7 --- /dev/null +++ b/addons/beehave/debug/icons/port_bottom.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_bottom.svg.import b/addons/beehave/debug/icons/port_bottom.svg.import new file mode 100644 index 0000000..202636d --- /dev/null +++ b/addons/beehave/debug/icons/port_bottom.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdivs6ajl6yo8" +path="res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_bottom.svg" +dest_files=["res://.godot/imported/port_bottom.svg-e5c5c61b642a79ab9c2b66ff56603d34.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/icons/port_left.svg b/addons/beehave/debug/icons/port_left.svg new file mode 100644 index 0000000..c1f6717 --- /dev/null +++ b/addons/beehave/debug/icons/port_left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_left.svg.import b/addons/beehave/debug/icons/port_left.svg.import new file mode 100644 index 0000000..7ff2fd5 --- /dev/null +++ b/addons/beehave/debug/icons/port_left.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cppwynbk7b67r" +path="res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_left.svg" +dest_files=["res://.godot/imported/port_left.svg-69cd927c4db555f1edbb8d1f553ea2fd.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/icons/port_right.svg b/addons/beehave/debug/icons/port_right.svg new file mode 100644 index 0000000..2560af5 --- /dev/null +++ b/addons/beehave/debug/icons/port_right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_right.svg.import b/addons/beehave/debug/icons/port_right.svg.import new file mode 100644 index 0000000..9d0efb5 --- /dev/null +++ b/addons/beehave/debug/icons/port_right.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5tp0xrmqa42x" +path="res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_right.svg" +dest_files=["res://.godot/imported/port_right.svg-f760bd8be2dd613d0d3848c998c92a2a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/icons/port_top.svg b/addons/beehave/debug/icons/port_top.svg new file mode 100644 index 0000000..d3b99e1 --- /dev/null +++ b/addons/beehave/debug/icons/port_top.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/port_top.svg.import b/addons/beehave/debug/icons/port_top.svg.import new file mode 100644 index 0000000..18d74de --- /dev/null +++ b/addons/beehave/debug/icons/port_top.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bky8qt47hmp65" +path="res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/port_top.svg" +dest_files=["res://.godot/imported/port_top.svg-d1b336cdc6a0dd570305782a1e56f61d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/icons/vertical_layout.svg b/addons/beehave/debug/icons/vertical_layout.svg new file mode 100644 index 0000000..d59ffb0 --- /dev/null +++ b/addons/beehave/debug/icons/vertical_layout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/beehave/debug/icons/vertical_layout.svg.import b/addons/beehave/debug/icons/vertical_layout.svg.import new file mode 100644 index 0000000..8688663 --- /dev/null +++ b/addons/beehave/debug/icons/vertical_layout.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://clb3m3iitw3hw" +path="res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/debug/icons/vertical_layout.svg" +dest_files=["res://.godot/imported/vertical_layout.svg-1a08fee4b09812a05bcf3defb8afcc4c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/debug/tree_node.gd b/addons/beehave/debug/tree_node.gd new file mode 100644 index 0000000..6c104d8 --- /dev/null +++ b/addons/beehave/debug/tree_node.gd @@ -0,0 +1,255 @@ +class_name TreeNode +extends RefCounted + +# Based on https://rachel53461.wordpress.com/2014/04/20/algorithm-for-drawing-trees/ + +const SIBLING_DISTANCE: float = 20.0 +const LEVEL_DISTANCE: float = 40.0 + +var x: float +var y: float +var mod: float +var parent: TreeNode +var children: Array[TreeNode] + +var item: GraphNode + + +func _init(p_item: GraphNode = null, p_parent: TreeNode = null) -> void: + parent = p_parent + item = p_item + + +func is_leaf() -> bool: + return children.is_empty() + + +func is_most_left() -> bool: + if not parent: + return true + return parent.children.front() == self + + +func is_most_right() -> bool: + if not parent: + return true + return parent.children.back() == self + + +func get_previous_sibling() -> TreeNode: + if not parent or is_most_left(): + return null + return parent.children[parent.children.find(self) - 1] + + +func get_next_sibling() -> TreeNode: + if not parent or is_most_right(): + return null + return parent.children[parent.children.find(self) + 1] + + +func get_most_left_sibling() -> TreeNode: + if not parent: + return null + + if is_most_left(): + return self + + return parent.children.front() + + +func get_most_left_child() -> TreeNode: + if children.is_empty(): + return null + return children.front() + + +func get_most_right_child() -> TreeNode: + if children.is_empty(): + return null + return children.back() + + +func update_positions(horizontally: bool = false) -> void: + _initialize_nodes(self, 0) + _calculate_initial_x(self) + + _check_all_children_on_screen(self) + _calculate_final_positions(self, 0) + + if horizontally: + _swap_x_y(self) + _calculate_x(self, 0) + else: + _calculate_y(self, 0) + + +func _initialize_nodes(node: TreeNode, depth: int) -> void: + node.x = -1 + node.y = depth + node.mod = 0 + + for child in node.children: + _initialize_nodes(child, depth + 1) + + +func _calculate_initial_x(node: TreeNode) -> void: + for child in node.children: + _calculate_initial_x(child) + if node.is_leaf(): + if not node.is_most_left(): + node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE + else: + node.x = 0 + else: + var mid: float + if node.children.size() == 1: + var offset: float = (node.children.front().item.layout_size - node.item.layout_size) / 2 + mid = node.children.front().x + offset + else: + var left_child := node.get_most_left_child() + var right_child := node.get_most_right_child() + mid = (left_child.x + right_child.x + right_child.item.layout_size - node.item.layout_size) / 2 + + if node.is_most_left(): + node.x = mid + else: + node.x = node.get_previous_sibling().x + node.get_previous_sibling().item.layout_size + SIBLING_DISTANCE + node.mod = node.x - mid + + if not node.is_leaf() and not node.is_most_left(): + _check_for_conflicts(node) + + +func _calculate_final_positions(node: TreeNode, mod_sum: float) -> void: + node.x += mod_sum + mod_sum += node.mod + + for child in node.children: + _calculate_final_positions(child, mod_sum) + + +func _check_all_children_on_screen(node: TreeNode) -> void: + var node_contour: Dictionary = {} + _get_left_contour(node, 0, node_contour) + + var shift_amount: float = 0 + for y in node_contour.keys(): + if node_contour[y] + shift_amount < 0: + shift_amount = (node_contour[y] * -1) + + if shift_amount > 0: + node.x += shift_amount + node.mod += shift_amount + + +func _check_for_conflicts(node: TreeNode) -> void: + var min_distance := SIBLING_DISTANCE + var shift_value: float = 0 + var shift_sibling: TreeNode = null + + var node_contour: Dictionary = {}# { int, float } + _get_left_contour(node, 0, node_contour) + + var sibling := node.get_most_left_sibling() + while sibling != null and sibling != node: + var sibling_contour: Dictionary = {} + _get_right_contour(sibling, 0, sibling_contour) + + for level in range(node.y + 1, min(sibling_contour.keys().max(), node_contour.keys().max()) + 1): + var distance: float = node_contour[level] - sibling_contour[level] + if distance + shift_value < min_distance: + shift_value = min_distance - distance + shift_sibling = sibling + + sibling = sibling.get_next_sibling() + + if shift_value > 0: + node.x += shift_value + node.mod += shift_value + _center_nodes_between(shift_sibling, node) + + +func _center_nodes_between(left_node: TreeNode, right_node: TreeNode) -> void: + var left_index := left_node.parent.children.find(left_node) + var right_index := left_node.parent.children.find(right_node) + + var num_nodes_between: int = (right_index - left_index) - 1 + if num_nodes_between > 0: + # The extra distance that needs to be split into num_nodes_between + 1 + # in order to find the new node spacing so that nodes are equally spaced + var distance_to_allocate: float = right_node.x - left_node.x - left_node.item.layout_size + # Subtract sizes on nodes in between + for i in range(left_index + 1, right_index): + distance_to_allocate -= left_node.parent.children[i].item.layout_size + # Divide space equally + var distance_between_nodes: float = distance_to_allocate / (num_nodes_between + 1) + + var prev_node := left_node + var middle_node := left_node.get_next_sibling() + while middle_node != right_node: + var desire_x: float = prev_node.x + prev_node.item.layout_size + distance_between_nodes + var offset := desire_x - middle_node.x + middle_node.x += offset + middle_node.mod += offset + prev_node = middle_node + middle_node = middle_node.get_next_sibling() + + +func _get_left_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void: + var node_left: float = node.x + mod_sum + var depth := int(node.y) + if not values.has(depth): + values[depth] = node_left + else: + values[depth] = min(values[depth], node_left) + + mod_sum += node.mod + for child in node.children: + _get_left_contour(child, mod_sum, values) + + +func _get_right_contour(node: TreeNode, mod_sum: float, values: Dictionary) -> void: + var node_right: float = node.x + mod_sum + node.item.layout_size + var depth := int(node.y) + if not values.has(depth): + values[depth] = node_right + else: + values[depth] = max(values[depth], node_right) + + mod_sum += node.mod + for child in node.children: + _get_right_contour(child, mod_sum, values) + + +func _swap_x_y(node: TreeNode) -> void: + for child in node.children: + _swap_x_y(child) + + var temp := node.x + node.x = node.y + node.y = temp + + +func _calculate_x(node: TreeNode, offset: int) -> void: + node.x = offset + var sibling := node.get_most_left_sibling() + var max_size: int = node.item.size.x + while sibling != null: + max_size = max(sibling.item.size.x, max_size) + sibling = sibling.get_next_sibling() + + for child in node.children: + _calculate_x(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale()) + + +func _calculate_y(node: TreeNode, offset: int) -> void: + node.y = offset + var sibling := node.get_most_left_sibling() + var max_size: int = node.item.size.y + while sibling != null: + max_size = max(sibling.item.size.y, max_size) + sibling = sibling.get_next_sibling() + + for child in node.children: + _calculate_y(child, max_size + offset + LEVEL_DISTANCE * BeehaveUtils.get_editor_scale()) diff --git a/addons/beehave/icons/action.svg b/addons/beehave/icons/action.svg new file mode 100644 index 0000000..3916c89 --- /dev/null +++ b/addons/beehave/icons/action.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/action.svg.import b/addons/beehave/icons/action.svg.import new file mode 100644 index 0000000..8950d42 --- /dev/null +++ b/addons/beehave/icons/action.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjeip5yvvptwj" +path="res://.godot/imported/action.svg-e8a91246d0ba9ba3cf84290d65648f06.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/action.svg" +dest_files=["res://.godot/imported/action.svg-e8a91246d0ba9ba3cf84290d65648f06.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/blackboard.svg b/addons/beehave/icons/blackboard.svg new file mode 100644 index 0000000..e4948a5 --- /dev/null +++ b/addons/beehave/icons/blackboard.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/blackboard.svg.import b/addons/beehave/icons/blackboard.svg.import new file mode 100644 index 0000000..71e2a4b --- /dev/null +++ b/addons/beehave/icons/blackboard.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cd8ge6anm44wd" +path="res://.godot/imported/blackboard.svg-18d4dfd4f6de558de250b67251ff1e69.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/blackboard.svg" +dest_files=["res://.godot/imported/blackboard.svg-18d4dfd4f6de558de250b67251ff1e69.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/category_bt.svg b/addons/beehave/icons/category_bt.svg new file mode 100644 index 0000000..8be61ae --- /dev/null +++ b/addons/beehave/icons/category_bt.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_bt.svg.import b/addons/beehave/icons/category_bt.svg.import new file mode 100644 index 0000000..9baf2e5 --- /dev/null +++ b/addons/beehave/icons/category_bt.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqda5kd75gtko" +path="res://.godot/imported/category_bt.svg-8537bebd1c5f62dca3d7ee7f17efeed4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_bt.svg" +dest_files=["res://.godot/imported/category_bt.svg-8537bebd1c5f62dca3d7ee7f17efeed4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/category_composite.svg b/addons/beehave/icons/category_composite.svg new file mode 100644 index 0000000..aa8b866 --- /dev/null +++ b/addons/beehave/icons/category_composite.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_composite.svg.import b/addons/beehave/icons/category_composite.svg.import new file mode 100644 index 0000000..7e60b0a --- /dev/null +++ b/addons/beehave/icons/category_composite.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tw8ql5cxqeo4" +path="res://.godot/imported/category_composite.svg-43f66e63a7ccfa5ac8ec6da0583b3246.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_composite.svg" +dest_files=["res://.godot/imported/category_composite.svg-43f66e63a7ccfa5ac8ec6da0583b3246.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/category_decorator.svg b/addons/beehave/icons/category_decorator.svg new file mode 100644 index 0000000..165e3d6 --- /dev/null +++ b/addons/beehave/icons/category_decorator.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_decorator.svg.import b/addons/beehave/icons/category_decorator.svg.import new file mode 100644 index 0000000..dbd219a --- /dev/null +++ b/addons/beehave/icons/category_decorator.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cpe6ntmjqkl2n" +path="res://.godot/imported/category_decorator.svg-79d598d6456f32724156248e09d6eaf3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_decorator.svg" +dest_files=["res://.godot/imported/category_decorator.svg-79d598d6456f32724156248e09d6eaf3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/category_leaf.svg b/addons/beehave/icons/category_leaf.svg new file mode 100644 index 0000000..1482fe6 --- /dev/null +++ b/addons/beehave/icons/category_leaf.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/category_leaf.svg.import b/addons/beehave/icons/category_leaf.svg.import new file mode 100644 index 0000000..03ff1a3 --- /dev/null +++ b/addons/beehave/icons/category_leaf.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ceraiow5o4ssv" +path="res://.godot/imported/category_leaf.svg-c740ecab6cfae632574ca5e39e46fd2e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/category_leaf.svg" +dest_files=["res://.godot/imported/category_leaf.svg-c740ecab6cfae632574ca5e39e46fd2e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/condition.svg b/addons/beehave/icons/condition.svg new file mode 100644 index 0000000..37b2c7a --- /dev/null +++ b/addons/beehave/icons/condition.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/condition.svg.import b/addons/beehave/icons/condition.svg.import new file mode 100644 index 0000000..204b4bb --- /dev/null +++ b/addons/beehave/icons/condition.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkixxh0kndfct" +path="res://.godot/imported/condition.svg-57892684b10a64086f68c09c388b17e5.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/condition.svg" +dest_files=["res://.godot/imported/condition.svg-57892684b10a64086f68c09c388b17e5.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/failer.svg b/addons/beehave/icons/failer.svg new file mode 100644 index 0000000..968f7e1 --- /dev/null +++ b/addons/beehave/icons/failer.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/failer.svg.import b/addons/beehave/icons/failer.svg.import new file mode 100644 index 0000000..b053b46 --- /dev/null +++ b/addons/beehave/icons/failer.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8qs25ku5wdvt" +path="res://.godot/imported/failer.svg-9a62b840e1eacc0437e7a67b14a302e4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/failer.svg" +dest_files=["res://.godot/imported/failer.svg-9a62b840e1eacc0437e7a67b14a302e4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/inverter.svg b/addons/beehave/icons/inverter.svg new file mode 100644 index 0000000..d4e791e --- /dev/null +++ b/addons/beehave/icons/inverter.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/inverter.svg.import b/addons/beehave/icons/inverter.svg.import new file mode 100644 index 0000000..280152f --- /dev/null +++ b/addons/beehave/icons/inverter.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://di1fnvn0j82nt" +path="res://.godot/imported/inverter.svg-1f1b976d95de42c4ad99a92fa9a6c5d0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/inverter.svg" +dest_files=["res://.godot/imported/inverter.svg-1f1b976d95de42c4ad99a92fa9a6c5d0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/limiter.svg b/addons/beehave/icons/limiter.svg new file mode 100644 index 0000000..7b3fa1d --- /dev/null +++ b/addons/beehave/icons/limiter.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/limiter.svg.import b/addons/beehave/icons/limiter.svg.import new file mode 100644 index 0000000..bf0fb84 --- /dev/null +++ b/addons/beehave/icons/limiter.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d0tbxe5pwd0h4" +path="res://.godot/imported/limiter.svg-b4c7646605c46f53c5e403fe21d8f584.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/limiter.svg" +dest_files=["res://.godot/imported/limiter.svg-b4c7646605c46f53c5e403fe21d8f584.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/selector.svg b/addons/beehave/icons/selector.svg new file mode 100644 index 0000000..0ae3b7a --- /dev/null +++ b/addons/beehave/icons/selector.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/selector.svg.import b/addons/beehave/icons/selector.svg.import new file mode 100644 index 0000000..b674369 --- /dev/null +++ b/addons/beehave/icons/selector.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://vd20qqjvikmk" +path="res://.godot/imported/selector.svg-78bccfc448bd1676b5a29bfde4b08e5b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector.svg" +dest_files=["res://.godot/imported/selector.svg-78bccfc448bd1676b5a29bfde4b08e5b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/selector_random.svg b/addons/beehave/icons/selector_random.svg new file mode 100644 index 0000000..6f631e9 --- /dev/null +++ b/addons/beehave/icons/selector_random.svg @@ -0,0 +1,35 @@ + + diff --git a/addons/beehave/icons/selector_random.svg.import b/addons/beehave/icons/selector_random.svg.import new file mode 100644 index 0000000..db0de1d --- /dev/null +++ b/addons/beehave/icons/selector_random.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://op2xj3wn1gv7" +path="res://.godot/imported/selector_random.svg-d52fea1352c24483ecd9dc8609cf00f3.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector_random.svg" +dest_files=["res://.godot/imported/selector_random.svg-d52fea1352c24483ecd9dc8609cf00f3.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/selector_reactive.svg b/addons/beehave/icons/selector_reactive.svg new file mode 100644 index 0000000..6db005f --- /dev/null +++ b/addons/beehave/icons/selector_reactive.svg @@ -0,0 +1,45 @@ + + diff --git a/addons/beehave/icons/selector_reactive.svg.import b/addons/beehave/icons/selector_reactive.svg.import new file mode 100644 index 0000000..35f191c --- /dev/null +++ b/addons/beehave/icons/selector_reactive.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cf6x41gdjw55" +path="res://.godot/imported/selector_reactive.svg-dd3b8fb8cd2ffe331605aaad1e021cc0.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/selector_reactive.svg" +dest_files=["res://.godot/imported/selector_reactive.svg-dd3b8fb8cd2ffe331605aaad1e021cc0.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/sequence.svg b/addons/beehave/icons/sequence.svg new file mode 100644 index 0000000..3ebedd9 --- /dev/null +++ b/addons/beehave/icons/sequence.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/sequence.svg.import b/addons/beehave/icons/sequence.svg.import new file mode 100644 index 0000000..9de43da --- /dev/null +++ b/addons/beehave/icons/sequence.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://1kiyv5175ytx" +path="res://.godot/imported/sequence.svg-76e5600611900cc81e9ec286977b8c6a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence.svg" +dest_files=["res://.godot/imported/sequence.svg-76e5600611900cc81e9ec286977b8c6a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/sequence_random.svg b/addons/beehave/icons/sequence_random.svg new file mode 100644 index 0000000..34e4a12 --- /dev/null +++ b/addons/beehave/icons/sequence_random.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/sequence_random.svg.import b/addons/beehave/icons/sequence_random.svg.import new file mode 100644 index 0000000..a4ebc26 --- /dev/null +++ b/addons/beehave/icons/sequence_random.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b3inan2jpcg03" +path="res://.godot/imported/sequence_random.svg-58cee9098c622ef87db941279206422a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence_random.svg" +dest_files=["res://.godot/imported/sequence_random.svg-58cee9098c622ef87db941279206422a.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/sequence_reactive.svg b/addons/beehave/icons/sequence_reactive.svg new file mode 100644 index 0000000..33d219b --- /dev/null +++ b/addons/beehave/icons/sequence_reactive.svg @@ -0,0 +1,60 @@ + + diff --git a/addons/beehave/icons/sequence_reactive.svg.import b/addons/beehave/icons/sequence_reactive.svg.import new file mode 100644 index 0000000..93ee0d9 --- /dev/null +++ b/addons/beehave/icons/sequence_reactive.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c88s6g4nixh5" +path="res://.godot/imported/sequence_reactive.svg-7d384ca290f7934adb9e17d9e7116b6c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/sequence_reactive.svg" +dest_files=["res://.godot/imported/sequence_reactive.svg-7d384ca290f7934adb9e17d9e7116b6c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/succeeder.svg b/addons/beehave/icons/succeeder.svg new file mode 100644 index 0000000..10f5912 --- /dev/null +++ b/addons/beehave/icons/succeeder.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/succeeder.svg.import b/addons/beehave/icons/succeeder.svg.import new file mode 100644 index 0000000..c919afe --- /dev/null +++ b/addons/beehave/icons/succeeder.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dyeh47y28xpax" +path="res://.godot/imported/succeeder.svg-e5cf6f6e04b9b862b82fd2cb479272aa.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/succeeder.svg" +dest_files=["res://.godot/imported/succeeder.svg-e5cf6f6e04b9b862b82fd2cb479272aa.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/icons/tree.svg b/addons/beehave/icons/tree.svg new file mode 100644 index 0000000..6c85ea1 --- /dev/null +++ b/addons/beehave/icons/tree.svg @@ -0,0 +1,38 @@ + + + + + + diff --git a/addons/beehave/icons/tree.svg.import b/addons/beehave/icons/tree.svg.import new file mode 100644 index 0000000..c7641f1 --- /dev/null +++ b/addons/beehave/icons/tree.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c2c881jv0gddb" +path="res://.godot/imported/tree.svg-c0b20ed88b2fe300c0296f7236049076.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/beehave/icons/tree.svg" +dest_files=["res://.godot/imported/tree.svg-c0b20ed88b2fe300c0296f7236049076.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/addons/beehave/metrics/beehave_global_metrics.gd b/addons/beehave/metrics/beehave_global_metrics.gd new file mode 100644 index 0000000..9a2ba4a --- /dev/null +++ b/addons/beehave/metrics/beehave_global_metrics.gd @@ -0,0 +1,54 @@ +extends Node + +var _tree_count: int = 0 +var _active_tree_count: int = 0 +var _registered_trees: Array[BeehaveTree] = [] + + +func _enter_tree() -> void: + Performance.add_custom_monitor("beehave/total_trees", _get_total_trees) + Performance.add_custom_monitor("beehave/total_enabled_trees", _get_total_enabled_trees) + + +func register_tree(tree: BeehaveTree) -> void: + if _registered_trees.has(tree): + return + + _registered_trees.append(tree) + _tree_count += 1 + + if tree.enabled: + _active_tree_count += 1 + + tree.tree_enabled.connect(_on_tree_enabled) + tree.tree_disabled.connect(_on_tree_disabled) + + +func unregister_tree(tree: BeehaveTree) -> void: + if not _registered_trees.has(tree): + return + + _registered_trees.erase(tree) + _tree_count -= 1 + + if tree.enabled: + _active_tree_count -= 1 + + tree.tree_enabled.disconnect(_on_tree_enabled) + tree.tree_disabled.disconnect(_on_tree_disabled) + + +func _get_total_trees() -> int: + return _tree_count + + +func _get_total_enabled_trees() -> int: + return _active_tree_count + + +func _on_tree_enabled() -> void: + _active_tree_count += 1 + + +func _on_tree_disabled() -> void: + _active_tree_count -= 1 diff --git a/addons/beehave/nodes/beehave_node.gd b/addons/beehave/nodes/beehave_node.gd new file mode 100644 index 0000000..0c94b8c --- /dev/null +++ b/addons/beehave/nodes/beehave_node.gd @@ -0,0 +1,49 @@ +## A node in the behavior tree. Every node must return `SUCCESS`, `FAILURE` or +## `RUNNING` when ticked. +@tool +class_name BeehaveNode extends Node + +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) diff --git a/addons/beehave/nodes/beehave_tree.gd b/addons/beehave/nodes/beehave_tree.gd new file mode 100644 index 0000000..7a8d145 --- /dev/null +++ b/addons/beehave/nodes/beehave_tree.gd @@ -0,0 +1,224 @@ +## Controls the flow of execution of the entire behavior tree. +@tool +@icon("../icons/tree.svg") +class_name BeehaveTree extends Node + +enum { + SUCCESS, + FAILURE, + RUNNING +} + +signal tree_enabled +signal tree_disabled + +## Wether this behavior tree should be enabled or not. +@export var enabled: bool = true: + set(value): + enabled = value + set_physics_process(enabled) + + if value: + tree_enabled.emit() + else: + interrupt() + tree_disabled.emit() + + get: + return enabled + +## An optional node path this behavior tree should apply to. +@export_node_path var actor_node_path : NodePath + +## 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: + 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) + BeehaveGlobalMetrics.register_tree(self) + else: + if _process_time_metric_name != '': + # Remove tree metric from the engine + Performance.remove_custom_monitor(_process_time_metric_name) + BeehaveGlobalMetrics.unregister_tree(self) + + BeehaveDebuggerMessages.unregister_tree(get_instance_id()) + +var actor : Node +var status : int = -1 + +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: + if Engine.is_editor_hint(): + return + + if self.get_child_count() > 0 and not self.get_child(0) is BeehaveNode: + push_warning("Beehave error: Root %s should have only one child of type BeehaveNode (NodePath: %s)" % [self.name, self.get_path()]) + disable() + return + + if not blackboard: + _internal_blackboard = Blackboard.new() + add_child(_internal_blackboard, false, Node.INTERNAL_MODE_BACK) + + actor = get_parent() + if actor_node_path: + actor = get_node(actor_node_path) + + # Get the name of the parent node name for metric + var parent_name = actor.name + _process_time_metric_name = "beehave [microseconds]/process_time_%s-%s" % [parent_name, get_instance_id()] + + # Register custom metric to the engine + if custom_monitor: + Performance.add_custom_monitor(_process_time_metric_name, _get_process_time_metric_value) + BeehaveGlobalMetrics.register_tree(self) + + set_physics_process(enabled) + BeehaveGlobalDebugger.register_tree(self) + BeehaveDebuggerMessages.register_tree(_get_debugger_data(self)) + + +func _physics_process(delta: float) -> void: + if Engine.is_editor_hint(): + return + + # 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: + 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 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) + BeehaveGlobalMetrics.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 _process_time_metric_value + + +func _get_debugger_data(node: Node) -> Dictionary: + if not node is BeehaveTree and not 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"] diff --git a/addons/beehave/nodes/composites/composite.gd b/addons/beehave/nodes/composites/composite.gd new file mode 100644 index 0000000..b959eea --- /dev/null +++ b/addons/beehave/nodes/composites/composite.gd @@ -0,0 +1,40 @@ +## A Composite node controls the flow of execution of its children in a specific manner. +@tool +@icon("../../icons/category_composite.svg") +class_name Composite extends BeehaveNode + + +var running_child: BeehaveNode = null + + +func _ready(): + if Engine.is_editor_hint(): + return + + if self.get_child_count() < 1: + push_warning("BehaviorTree Error: Composite %s should have at least one child (NodePath: %s)" % [self.name, self.get_path()]) + + +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 diff --git a/addons/beehave/nodes/composites/selector.gd b/addons/beehave/nodes/composites/selector.gd new file mode 100644 index 0000000..262f35e --- /dev/null +++ b/addons/beehave/nodes/composites/selector.gd @@ -0,0 +1,57 @@ +## 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. +@tool +@icon("../../icons/selector.svg") +class_name SelectorComposite extends Composite + +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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 SUCCESS + FAILURE: + 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: + after_run(actor, blackboard) + super(actor, blackboard) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SelectorComposite") + return classes diff --git a/addons/beehave/nodes/composites/selector_random.gd b/addons/beehave/nodes/composites/selector_random.gd new file mode 100644 index 0000000..412adb0 --- /dev/null +++ b/addons/beehave/nodes/composites/selector_random.gd @@ -0,0 +1,88 @@ +## 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. +@tool +@icon("../../icons/selector_random.svg") +class_name SelectorRandomComposite extends Composite + +## Sets a predicable seed +@export var random_seed:int = 0: + set(rs): + random_seed = rs + if random_seed != 0: + seed(random_seed) + else: + randomize() + + +## A shuffled list of the children that will be executed in reverse order. +var _children_bag: Array[Node] = [] +var c: Node + +func _ready() -> void: + 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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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: + after_run(actor, blackboard) + super(actor, blackboard) + + +func _get_reversed_indexes() -> Array[int]: + var reversed: Array[int] + reversed.assign(range(_children_bag.size())) + reversed.reverse() + return reversed + + +## Generates a new shuffled list of the children. +func _reset() -> void: + _children_bag = get_children().duplicate() + _children_bag.shuffle() + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SelectorRandomComposite") + return classes diff --git a/addons/beehave/nodes/composites/selector_reactive.gd b/addons/beehave/nodes/composites/selector_reactive.gd new file mode 100644 index 0000000..869ef8a --- /dev/null +++ b/addons/beehave/nodes/composites/selector_reactive.gd @@ -0,0 +1,45 @@ +## 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. +@tool +@icon("../../icons/selector_reactive.svg") +class_name SelectorReactiveComposite extends Composite + +func tick(actor: Node, blackboard: Blackboard) -> int: + for c in get_children(): + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 diff --git a/addons/beehave/nodes/composites/sequence.gd b/addons/beehave/nodes/composites/sequence.gd new file mode 100644 index 0000000..0635b9f --- /dev/null +++ b/addons/beehave/nodes/composites/sequence.gd @@ -0,0 +1,64 @@ +## 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. +@tool +@icon("../../icons/sequence.svg") +class_name SequenceComposite extends Composite + + +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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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: + 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 + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence_random.gd b/addons/beehave/nodes/composites/sequence_random.gd new file mode 100644 index 0000000..7e240fd --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_random.gd @@ -0,0 +1,96 @@ +## 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. +@tool +@icon("../../icons/sequence_random.svg") +class_name SequenceRandomComposite extends Composite + + +## 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 +## Sets a predicable seed +@export var random_seed: int = 0: + set(rs): + random_seed = rs + if random_seed != 0: + seed(random_seed) + else: + randomize() + +## A shuffled list of the children that will be executed in reverse order. +var _children_bag: Array[Node] = [] +var c: Node + + +func _ready() -> void: + 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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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) + 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 + + +## Generates a new shuffled list of the children. +func _reset() -> void: + _children_bag = get_children().duplicate() + _children_bag.shuffle() + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"SequenceRandomComposite") + return classes diff --git a/addons/beehave/nodes/composites/sequence_reactive.gd b/addons/beehave/nodes/composites/sequence_reactive.gd new file mode 100644 index 0000000..50e8b90 --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_reactive.gd @@ -0,0 +1,62 @@ +## 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. +@tool +@icon("../../icons/sequence_reactive.svg") +class_name SequenceReactiveComposite extends Composite + + +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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 diff --git a/addons/beehave/nodes/composites/sequence_star.gd b/addons/beehave/nodes/composites/sequence_star.gd new file mode 100644 index 0000000..42ceb07 --- /dev/null +++ b/addons/beehave/nodes/composites/sequence_star.gd @@ -0,0 +1,58 @@ +## 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 restart. +@tool +@icon("../../icons/sequence_reactive.svg") +class_name SequenceStarComposite extends Composite + + +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 = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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: + 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 diff --git a/addons/beehave/nodes/decorators/decorator.gd b/addons/beehave/nodes/decorators/decorator.gd new file mode 100644 index 0000000..8cc3944 --- /dev/null +++ b/addons/beehave/nodes/decorators/decorator.gd @@ -0,0 +1,41 @@ +## Decorator nodes are used to transform the result received by its child. +## Must only have one child. +@tool +@icon("../../icons/category_decorator.svg") +class_name Decorator extends BeehaveNode + + +var running_child: BeehaveNode = null + + +func _ready(): + if Engine.is_editor_hint(): + return + + if self.get_child_count() != 1: + push_warning("Beehave Error: Decorator %s should have only one child (NodePath: %s)" % [self.name, self.get_path()]) + + +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 diff --git a/addons/beehave/nodes/decorators/failer.gd b/addons/beehave/nodes/decorators/failer.gd new file mode 100644 index 0000000..4a818ed --- /dev/null +++ b/addons/beehave/nodes/decorators/failer.gd @@ -0,0 +1,34 @@ +## A Failer node will always return a `FAILURE` status code. +@tool +@icon("../../icons/failer.svg") +class_name AlwaysFailDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 diff --git a/addons/beehave/nodes/decorators/inverter.gd b/addons/beehave/nodes/decorators/inverter.gd new file mode 100644 index 0000000..16e3f36 --- /dev/null +++ b/addons/beehave/nodes/decorators/inverter.gd @@ -0,0 +1,42 @@ +## 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. +@tool +@icon("../../icons/inverter.svg") +class_name InverterDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 diff --git a/addons/beehave/nodes/decorators/limiter.gd b/addons/beehave/nodes/decorators/limiter.gd new file mode 100644 index 0000000..72e7356 --- /dev/null +++ b/addons/beehave/nodes/decorators/limiter.gd @@ -0,0 +1,41 @@ +## The limiter will execute its child `x` amount of times. When the number of +## maximum ticks is reached, it will return a `FAILURE` status code. +@tool +@icon("../../icons/limiter.svg") +class_name LimiterDecorator extends Decorator + +@onready var cache_key = 'limiter_%s' % self.get_instance_id() + +@export var max_count : float = 0 + +func tick(actor: Node, blackboard: Blackboard) -> int: + var child = self.get_child(0) + var current_count = blackboard.get_value(cache_key, 0, str(actor.get_instance_id())) + + if current_count == 0: + child.before_run(actor, blackboard) + + if current_count < max_count: + blackboard.set_value(cache_key, current_count + 1, str(actor.get_instance_id())) + var response = child.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response) + + 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())) + + return response + else: + child.after_run(actor, blackboard) + return FAILURE + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"LimiterDecorator") + return classes diff --git a/addons/beehave/nodes/decorators/succeeder.gd b/addons/beehave/nodes/decorators/succeeder.gd new file mode 100644 index 0000000..9d9664b --- /dev/null +++ b/addons/beehave/nodes/decorators/succeeder.gd @@ -0,0 +1,34 @@ +## A succeeder node will always return a `SUCCESS` status code. +@tool +@icon("../../icons/succeeder.svg") +class_name AlwaysSucceedDecorator extends Decorator + + +func tick(actor: Node, blackboard: Blackboard) -> int: + var c = get_child(0) + + if c != running_child: + c.before_run(actor, blackboard) + + var response = c.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(c.get_instance_id(), response) + + 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 diff --git a/addons/beehave/nodes/decorators/time_limiter.gd b/addons/beehave/nodes/decorators/time_limiter.gd new file mode 100644 index 0000000..cedb397 --- /dev/null +++ b/addons/beehave/nodes/decorators/time_limiter.gd @@ -0,0 +1,46 @@ +## The Time Limit Decorator will give its child a set amount of time to finish +## before interrupting it and return a `FAILURE` status code. The timer is reset +## every time before the node runs. +@tool +@icon("../../icons/limiter.svg") +class_name TimeLimiterDecorator extends Decorator + +@export var wait_time: = 0.0 + +var time_left: = 0.0 + +@onready var child: BeehaveNode = get_child(0) + + +func tick(actor: Node, blackboard: Blackboard) -> int: + if time_left < wait_time: + time_left += get_physics_process_delta_time() + var response = child.tick(actor, blackboard) + if can_send_message(blackboard): + BeehaveDebuggerMessages.process_tick(child.get_instance_id(), response) + + 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 response + else: + child.after_run(actor, blackboard) + interrupt(actor, blackboard) + return FAILURE + + +func before_run(actor: Node, blackboard: Blackboard) -> void: + time_left = 0.0 + child.before_run(actor, blackboard) + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"TimeLimiterDecorator") + return classes diff --git a/addons/beehave/nodes/leaves/action.gd b/addons/beehave/nodes/leaves/action.gd new file mode 100644 index 0000000..003bdb4 --- /dev/null +++ b/addons/beehave/nodes/leaves/action.gd @@ -0,0 +1,13 @@ +## 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. +@tool +@icon("../../icons/action.svg") +class_name ActionLeaf extends Leaf + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"ActionLeaf") + return classes diff --git a/addons/beehave/nodes/leaves/blackboard_compare.gd b/addons/beehave/nodes/leaves/blackboard_compare.gd new file mode 100644 index 0000000..84fb43f --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_compare.gd @@ -0,0 +1,61 @@ +## 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]. +@tool +class_name BlackboardCompareCondition extends ConditionLeaf + + +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] diff --git a/addons/beehave/nodes/leaves/blackboard_erase.gd b/addons/beehave/nodes/leaves/blackboard_erase.gd new file mode 100644 index 0000000..89508bd --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_erase.gd @@ -0,0 +1,25 @@ +## Erases the specified key from the blackboard. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. +@tool +class_name BlackboardEraseAction extends ActionLeaf + + +## 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] diff --git a/addons/beehave/nodes/leaves/blackboard_has.gd b/addons/beehave/nodes/leaves/blackboard_has.gd new file mode 100644 index 0000000..868fddf --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_has.gd @@ -0,0 +1,23 @@ +## 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. +@tool +class_name BlackboardHasCondition extends ConditionLeaf + + +## 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] diff --git a/addons/beehave/nodes/leaves/blackboard_set.gd b/addons/beehave/nodes/leaves/blackboard_set.gd new file mode 100644 index 0000000..d0d6455 --- /dev/null +++ b/addons/beehave/nodes/leaves/blackboard_set.gd @@ -0,0 +1,34 @@ +## Sets the specified key to the specified value. +## Returns [code]FAILURE[/code] if expression execution fails, otherwise [code]SUCCESS[/code]. +@tool +class_name BlackboardSetAction extends ActionLeaf + + +## 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] diff --git a/addons/beehave/nodes/leaves/condition.gd b/addons/beehave/nodes/leaves/condition.gd new file mode 100644 index 0000000..55ec6f9 --- /dev/null +++ b/addons/beehave/nodes/leaves/condition.gd @@ -0,0 +1,11 @@ +## Conditions are leaf nodes that either return SUCCESS or FAILURE depending on +## a single simple condition. They should never return `RUNNING`. +@tool +@icon("../../icons/condition.svg") +class_name ConditionLeaf extends Leaf + + +func get_class_name() -> Array[StringName]: + var classes := super() + classes.push_back(&"ConditionLeaf") + return classes diff --git a/addons/beehave/nodes/leaves/leaf.gd b/addons/beehave/nodes/leaves/leaf.gd new file mode 100644 index 0000000..cca02c3 --- /dev/null +++ b/addons/beehave/nodes/leaves/leaf.gd @@ -0,0 +1,46 @@ +## Base class for all leaf nodes of the tree. +@tool +@icon("../../icons/category_leaf.svg") +class_name Leaf extends BeehaveNode + + +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 \ No newline at end of file diff --git a/addons/beehave/plugin.cfg b/addons/beehave/plugin.cfg new file mode 100644 index 0000000..c7cfe94 --- /dev/null +++ b/addons/beehave/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Beehave" +description="🐝 Behavior Tree addon for Godot Engine" +author="bitbrain" +version="2.7.4" +script="plugin.gd" diff --git a/addons/beehave/plugin.gd b/addons/beehave/plugin.gd new file mode 100644 index 0000000..3c54627 --- /dev/null +++ b/addons/beehave/plugin.gd @@ -0,0 +1,24 @@ +@tool +extends EditorPlugin + +const BeehaveEditorDebugger := preload("debug/debugger.gd") +var editor_debugger: BeehaveEditorDebugger +var frames: RefCounted + +func _init(): + name = "BeehavePlugin" + add_autoload_singleton("BeehaveGlobalMetrics", "res://addons/beehave/metrics/beehave_global_metrics.gd") + add_autoload_singleton("BeehaveGlobalDebugger", "res://addons/beehave/debug/global_debugger.gd") + print("Beehave initialized!") + + +func _enter_tree() -> void: + editor_debugger = BeehaveEditorDebugger.new() + frames = preload("debug/frames.gd").new() + add_debugger_plugin(editor_debugger) + + +func _exit_tree() -> void: + remove_debugger_plugin(editor_debugger) + editor_debugger.free() + frames.free() diff --git a/addons/beehave/utils/utils.gd b/addons/beehave/utils/utils.gd new file mode 100644 index 0000000..32e3a03 --- /dev/null +++ b/addons/beehave/utils/utils.gd @@ -0,0 +1,22 @@ +@tool +class_name BeehaveUtils + + +static func get_plugin() -> EditorPlugin: + var tree: SceneTree = Engine.get_main_loop() + return tree.get_root().get_child(0).get_node_or_null("BeehavePlugin") + + +static func get_editor_scale() -> float: + var plugin := get_plugin() + if plugin: + return plugin.get_editor_interface().get_editor_scale() + return 1.0 + + +static func get_frames() -> RefCounted: + var plugin := get_plugin() + if plugin: + return plugin.frames + push_error("Can't find Beehave Plugin") + return null diff --git a/project.godot b/project.godot index 1cf2cde..afce394 100644 --- a/project.godot +++ b/project.godot @@ -15,12 +15,21 @@ run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.0", "Forward Plus") config/icon="res://icon.svg" +[autoload] + +BeehaveGlobalMetrics="*res://addons/beehave/metrics/beehave_global_metrics.gd" +BeehaveGlobalDebugger="*res://addons/beehave/debug/global_debugger.gd" + [display] window/size/viewport_width=1280 window/size/viewport_height=720 window/stretch/scale=1.8 +[editor_plugins] + +enabled=PackedStringArray("res://addons/beehave/plugin.cfg") + [input] move_right={ diff --git a/scenes/enemy.tscn b/scenes/enemy.tscn index 1c918e6..a51957b 100644 --- a/scenes/enemy.tscn +++ b/scenes/enemy.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=6 format=3 uid="uid://bktg1ypaav3q3"] +[gd_scene load_steps=7 format=3 uid="uid://bktg1ypaav3q3"] [ext_resource type="Texture2D" uid="uid://bbwsdd0jddmck" path="res://art/Pink_Monster_Walk_6.png" id="1_e5603"] [ext_resource type="Script" path="res://scripts/enemy.gd" id="1_l5lyv"] +[ext_resource type="PackedScene" uid="uid://b51tdt5kunai" path="res://scenes/enemy_behavior_tree.tscn" id="3_jk76t"] [sub_resource type="Animation" id="Animation_etqki"] resource_name = "walk" @@ -45,3 +46,5 @@ libraries = { [node name="CollisionShape2D" type="CollisionShape2D" parent="."] position = Vector2(-0.5, -5) shape = SubResource("RectangleShape2D_rlijp") + +[node name="EnemyBehaviorTree" parent="." instance=ExtResource("3_jk76t")] diff --git a/scenes/enemy_behavior_tree.tscn b/scenes/enemy_behavior_tree.tscn new file mode 100644 index 0000000..b2a3552 --- /dev/null +++ b/scenes/enemy_behavior_tree.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=6 format=3 uid="uid://b51tdt5kunai"] + +[ext_resource type="Script" path="res://addons/beehave/nodes/beehave_tree.gd" id="1_b2pc4"] +[ext_resource type="Script" path="res://addons/beehave/nodes/composites/sequence.gd" id="2_80fm4"] +[ext_resource type="Script" path="res://scripts/is_idle_condition.gd" id="3_5jur1"] +[ext_resource type="Script" path="res://scripts/move_to_target_action.gd" id="4_1on4v"] +[ext_resource type="Script" path="res://scripts/get_random_top_cell.gd" id="4_ouqf7"] + +[node name="EnemyBehaviorTree" type="Node"] +script = ExtResource("1_b2pc4") + +[node name="ReturnBallSequence" type="Node" parent="."] +script = ExtResource("2_80fm4") + +[node name="IsIdle" type="Node" parent="ReturnBallSequence"] +script = ExtResource("3_5jur1") + +[node name="GetRandomTopCell" type="Node" parent="ReturnBallSequence"] +script = ExtResource("4_ouqf7") + +[node name="MoveToTargetAction" type="Node" parent="ReturnBallSequence"] +script = ExtResource("4_1on4v") diff --git a/scripts/enemy.gd b/scripts/enemy.gd index 5a2f488..b187ffe 100644 --- a/scripts/enemy.gd +++ b/scripts/enemy.gd @@ -4,33 +4,9 @@ extends CharacterBody2D @onready var animation_player = $AnimationPlayer @export var speed = 80 var y_spawn_offset = -8 -var is_moving = false var destination: Vector2 = Vector2.ZERO - func _ready(): var spawn_cell: Vector2i = tile_map.get_top_spawn_cell() position = tile_map.map_to_local(spawn_cell) position.y += y_spawn_offset - -func _process(_delta): - if is_moving: - animation_player.play("walk") - else: - animation_player.stop() - -func _physics_process(delta): - move_to_rand_cell(delta) - -func move_to_rand_cell(delta): - if !is_moving: - var rand_cell: Vector2i = tile_map.get_random_top_cell() - tile_map.set_cell(0, Vector2i(rand_cell.x, rand_cell.y), 1, Vector2i(0, 0), 0) # debug purpose - destination = tile_map.map_to_local(rand_cell) - destination.y += y_spawn_offset - - is_moving = true - position = position.move_toward(destination, delta * speed) - - if position == destination: - is_moving = false diff --git a/scripts/get_random_top_cell.gd b/scripts/get_random_top_cell.gd new file mode 100644 index 0000000..9c1e3b9 --- /dev/null +++ b/scripts/get_random_top_cell.gd @@ -0,0 +1,11 @@ +class_name GetRandomTopCell +extends ActionLeaf + +func tick(actor, _blackboard): + var rand_cell: Vector2i = actor.tile_map.get_random_top_cell() + actor.tile_map.set_cell(0, Vector2i(rand_cell.x, rand_cell.y), 1, Vector2i(0, 0), 0) # debug purpose + + actor.destination = actor.tile_map.map_to_local(rand_cell) + actor.destination.y += actor.y_spawn_offset + + return SUCCESS diff --git a/scripts/is_idle_condition.gd b/scripts/is_idle_condition.gd new file mode 100644 index 0000000..aebe796 --- /dev/null +++ b/scripts/is_idle_condition.gd @@ -0,0 +1,5 @@ +class_name IsIdleCondition +extends ConditionLeaf + +func tick(actor, _blackboard): + return SUCCESS diff --git a/scripts/move_to_target_action.gd b/scripts/move_to_target_action.gd new file mode 100644 index 0000000..82dd4e5 --- /dev/null +++ b/scripts/move_to_target_action.gd @@ -0,0 +1,16 @@ +class_name MoveToTargetAction +extends ActionLeaf + +func before_run(actor, blackboard): + actor.animation_player.play("walk") + +func tick(actor, _blackboard): + var delta = get_physics_process_delta_time() + actor.position = actor.position.move_toward(actor.destination, delta * actor.speed) + + if actor.position == actor.destination: + return SUCCESS + return RUNNING + +func after_run(actor, blackboard): + actor.animation_player.stop()