From d8bc800077c2065344b0fd8f4b06220f05beebd2 Mon Sep 17 00:00:00 2001 From: Yan Wittmann Date: Sun, 12 Jan 2025 17:45:21 +0100 Subject: [PATCH] Initial version of a tree visualizer --- project/main-scenes/island.tscn | 25 ++- project/scripts/global/GameManager.gd | 7 + .../visualization/BehaviorTreeVisualizer.gd | 144 ++++++++++++++++++ project/scripts/visualization/d_tree_node.gd | 40 +++++ .../scripts/visualization/d_tree_node.tscn | 18 +++ .../visualization/state_machine_info_panel.gd | 60 ++++++++ 6 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 project/scripts/visualization/BehaviorTreeVisualizer.gd create mode 100644 project/scripts/visualization/d_tree_node.gd create mode 100644 project/scripts/visualization/d_tree_node.tscn create mode 100644 project/scripts/visualization/state_machine_info_panel.gd diff --git a/project/main-scenes/island.tscn b/project/main-scenes/island.tscn index a1c9629..fa63fb1 100644 --- a/project/main-scenes/island.tscn +++ b/project/main-scenes/island.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=44 format=4 uid="uid://b88asko1ugyd2"] +[gd_scene load_steps=45 format=4 uid="uid://b88asko1ugyd2"] [ext_resource type="Script" path="res://scripts/global/GameManager.gd" id="1_eeg2d"] [ext_resource type="Script" path="res://scripts/tilemap/World.gd" id="1_k0rw8"] @@ -7,6 +7,7 @@ [ext_resource type="Material" uid="uid://ckg3be082ny3h" path="res://assets/shader/shader_vignette.tres" id="3_7waul"] [ext_resource type="Script" path="res://scripts/player/PlayerManager.gd" id="4_1xqo1"] [ext_resource type="Texture2D" uid="uid://1ae5agveqddp" path="res://icon.svg" id="4_o8ona"] +[ext_resource type="Script" path="res://scripts/visualization/BehaviorTreeVisualizer.gd" id="5_ecfvx"] [ext_resource type="Script" path="res://scripts/tilemap/StepVisualization.gd" id="5_sr2su"] [ext_resource type="Script" path="res://scripts/player/tree/BehaviorTree.gd" id="6_efs30"] [ext_resource type="Script" path="res://scripts/player/tree/impl/base/TaskSelector.gd" id="7_1jajd"] @@ -210,6 +211,28 @@ layout_mode = 2 size_flags_horizontal = 0 size_flags_vertical = 8 +[node name="TreeVisualizer" type="Window" parent="Camera2D/CanvasLayer"] +unique_name_in_owner = true +position = Vector2i(35, 405) +size = Vector2i(1075, 225) +script = ExtResource("5_ecfvx") + +[node name="GraphEdit" type="GraphEdit" parent="Camera2D/CanvasLayer/TreeVisualizer"] +unique_name_in_owner = true +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +scroll_offset = Vector2(-100, -100) +show_menu = false +show_zoom_buttons = false +show_grid_buttons = false +show_minimap_button = false +show_arrange_button = false + [node name="Tileset" type="Node2D" parent="."] script = ExtResource("1_k0rw8") diff --git a/project/scripts/global/GameManager.gd b/project/scripts/global/GameManager.gd index 35b03d1..a3c9a5f 100644 --- a/project/scripts/global/GameManager.gd +++ b/project/scripts/global/GameManager.gd @@ -15,6 +15,8 @@ var tilemap_types: TileMapTileTypes = TileMapTileTypes.new() var tilemap_navigation: TilemapNavigation = TilemapNavigation.new() +@onready var tree_visualizer: BehaviorTreeVisualizer = %TreeVisualizer + func _ready() -> void: tilemap_navigation.world = world @@ -24,6 +26,11 @@ func _ready() -> void: world.step_visualizer.game_manager = self world.step_visualizer.world = world update_bars() + call_deferred("defer_ready") + +func defer_ready() -> void: + tree_visualizer.behavior_tree = player.behavior_tree + tree_visualizer.build_tree() # game_ticker.start() diff --git a/project/scripts/visualization/BehaviorTreeVisualizer.gd b/project/scripts/visualization/BehaviorTreeVisualizer.gd new file mode 100644 index 0000000..c7bd750 --- /dev/null +++ b/project/scripts/visualization/BehaviorTreeVisualizer.gd @@ -0,0 +1,144 @@ +class_name BehaviorTreeVisualizer +extends Window + +const D_TREE_NODE: PackedScene = preload("res://scripts/visualization/d_tree_node.tscn") +@onready var graph_edit: GraphEdit = %GraphEdit + +# Input data +var behavior_tree: BehaviorTree +var all_nodes: Array[DTreeNode] = [] +var all_node_names_to_nodes: Dictionary = {} +# Configuration +var x_spacing: int = 300 # Base horizontal distance between parent and child nodes +var y_spacing: int = 200 # Vertical distance between sibling nodes + + +func build_tree() -> void: + if not behavior_tree: + push_error("No behavior tree set.") + return + + # Reset current visualization + graph_edit.clear_connections() + for node in all_nodes: + node.queue_free() + all_nodes.clear() + all_node_names_to_nodes.clear() + + # Build tree starting from the root task + var root_node: Task = behavior_tree.behavior_tree + if not root_node: + push_error("Root behavior tree node is null.") + return + + # Calculate positions for all nodes + var positions: Dictionary = calculate_tree_layout(root_node) + + # Create the nodes and connections in the GraphEdit + for task_name in positions.keys(): + var position = positions[task_name] + var task: Task = find_task_by_name(root_node, task_name) + if task: + create_node(task, position) + + # Connect the nodes + for node in all_nodes: + var task_name: String = node.title + var task: Task = find_task_by_name(root_node, task_name) + if task: + for child in task.get_children(): + if child is Task: + connect_nodes(task.name, child.name) + + +func calculate_tree_layout(root_task: Task) -> Dictionary: + """ + Calculates the positions of all nodes in the tree, ensuring no overlaps. + """ + var positions: Dictionary = {} + var layers = get_tree_layers(root_task) + var layer_count = layers.size() + var layer_heights: Array[float] = [] + + # Calculate height for each layer based on the number of nodes + for layer in layers: + layer_heights.append(float(layer.size() - 1) * y_spacing) + + # Position nodes starting from the deepest layer and working up + for layer_index in range(layer_count - 1, -1, -1): + var layer = layers[layer_index] + var layer_y_offset: float = 0.0 + if layer_index < layer_count - 1: + layer_y_offset = layer_heights[layer_index + 1] / 2.0 + + var x: float = float(layer_index) * x_spacing + var num_nodes = layer.size() + var start_y = -layer_heights[layer_index] / 2.0 + + for node_index in range(num_nodes): + var task = layer[node_index] + var y = start_y + float(node_index) * y_spacing + layer_y_offset + positions[task.name] = Vector2(x, y) + + return positions + + +func get_tree_layers(root_task: Task): + """ + Returns a list of layers, where each layer is a list of tasks at the same depth. + """ + var layers = [] + var queue: Array[Dictionary] = [{"task": root_task, "depth": 0}] + + while not queue.is_empty(): + var current_item = queue.pop_front() + var task = current_item.task + var depth = current_item.depth + + if layers.size() <= depth: + layers.append([]) + layers[depth].append(task) + + for child in task.get_children(): + if child is Task: + queue.append({"task": child as Task, "depth": depth + 1}) + + return layers + + +func create_node(task: Task, position: Vector2) -> void: + """ + Creates a visual node for the given task at the specified position. + """ + var graph_node: DTreeNode = D_TREE_NODE.instantiate() + graph_node.title = task.name + graph_node.position_offset = position + graph_edit.add_child(graph_node) + + all_nodes.append(graph_node) + all_node_names_to_nodes[task.name] = graph_node + + +func connect_nodes(parent_name: String, child_name: String) -> void: + """ + Connects two nodes in the GraphEdit. + """ + if all_node_names_to_nodes.has(parent_name) and all_node_names_to_nodes.has(child_name): + graph_edit.connect_node( + all_node_names_to_nodes[parent_name].get_name(), 0, # Parent's output port + all_node_names_to_nodes[child_name].get_name(), 0 # Child's input port + ) + + +func find_task_by_name(root: Task, name: String) -> Task: + """ + Finds a task by name within the behavior tree. + """ + if root.name == name: + return root + for child in root.get_children(): + if child is Task: + var result = find_task_by_name(child as Task, name) + if result: + return result + return null \ No newline at end of file diff --git a/project/scripts/visualization/d_tree_node.gd b/project/scripts/visualization/d_tree_node.gd new file mode 100644 index 0000000..c5172ff --- /dev/null +++ b/project/scripts/visualization/d_tree_node.gd @@ -0,0 +1,40 @@ +class_name DTreeNode +extends GraphNode + +var left_slots: Array[String] = [] +var right_slots: Array[String] = [] +var color_highlighted: StyleBoxFlat = StyleBoxFlat.new() +var color_normal: StyleBoxFlat = StyleBoxFlat.new() + + +func _ready() -> void: + color_highlighted.bg_color = Color(0.67058825, 1.0, 0.5411765) + color_normal.bg_color = Color(1.0, 1.0, 1.0) + + +func add_label(label_text: String, left: bool, right: bool) -> Vector3i: + var new_label: Label = Label.new() + new_label.text = label_text + new_label.add_theme_color_override("font_color", Color(0, 0, 0, 1)) + + self.add_child(new_label) + + var child_index: int = self.get_child_count() - 1 + if left: + self.set_slot_enabled_left(child_index, true) + self.set_slot_color_left(child_index, Color(0.9, 0.9, 0.9, 1)) + left_slots.append(label_text) + if right: + self.set_slot_enabled_right(child_index, true) + self.set_slot_color_right(child_index, Color(0.9, 0.9, 0.9, 1)) + right_slots.append(label_text) + + # the port index is counted separately from the left and right slots + return Vector3i(child_index, left_slots.size() - 1, right_slots.size() - 1) + + +func set_highlighted(highlight: bool) -> void: + if highlight: + self.add_theme_stylebox_override("panel", color_highlighted) + else: + self.add_theme_stylebox_override("panel", color_normal) diff --git a/project/scripts/visualization/d_tree_node.tscn b/project/scripts/visualization/d_tree_node.tscn new file mode 100644 index 0000000..4b5c9d8 --- /dev/null +++ b/project/scripts/visualization/d_tree_node.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=4 format=3 uid="uid://bmv75j2e8xc6n"] + +[ext_resource type="Script" path="res://scripts/visualization/d_tree_node.gd" id="1_o2ffa"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_k54lu"] +bg_color = Color(0.95158, 0.95158, 0.95158, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_dmqj2"] +bg_color = Color(0.6, 0.39, 0.39, 1) + +[node name="DTreeNode" type="GraphNode"] +offset_right = 41.0 +offset_bottom = 23.0 +mouse_filter = 1 +theme_override_styles/panel = SubResource("StyleBoxFlat_k54lu") +theme_override_styles/titlebar = SubResource("StyleBoxFlat_dmqj2") +title = "TITEL" +script = ExtResource("1_o2ffa") diff --git a/project/scripts/visualization/state_machine_info_panel.gd b/project/scripts/visualization/state_machine_info_panel.gd new file mode 100644 index 0000000..3ee7542 --- /dev/null +++ b/project/scripts/visualization/state_machine_info_panel.gd @@ -0,0 +1,60 @@ +class_name InfoPanel +extends VBoxContainer + +var values: Dictionary = {} +var last_values: Dictionary = {} +var value_order: Array = [] + + +func _ready() -> void: + hide() + + +func to_str(value) -> String: + if value is float: + return str(round(value * 100) / 100) + elif value is Vector2: + return "(" + to_str(value.x) + ", " + to_str(value.y) + ")" + elif value is Vector3: + return "(" + to_str(value.x) + ", " + to_str(value.y) + ", " + to_str(value.z) + ")" + elif value is Vector4: + return "(" + to_str(value.x) + ", " + to_str(value.y) + ", " + to_str(value.z) + ", " + to_str(value.w) + ")" + return str(value) + + +func _process(delta: float) -> void: + if Input.is_action_just_pressed("debug_vis_2"): + if is_visible(): + hide() + else: + show() + + for child in self.get_children(): + if child is Label: + child.queue_free() + + # 1. Update and Track Order of Values + for value in values.keys(): + if value in value_order: + # Move to the end (most recent) + value_order.erase(value) + value_order.append(value) + + # 2. Display Current Values (Most Recent First) + for value in value_order: + if values.has(value): + var new_label: Label = Label.new() + new_label.text = value + ": " + to_str(values[value]) + new_label.add_theme_color_override("font_color", Color(0, 0, 0, 1)) + self.add_child(new_label) + last_values[value] = values[value] + + # 3. Display Old Values + for value in last_values.keys(): + if not values.has(value): + var new_label: Label = Label.new() + new_label.text = value + ": " + to_str(last_values[value]) + new_label.add_theme_color_override("font_color", Color(0.5, 0.5, 0.5, 1)) + self.add_child(new_label) + + values.clear()