gai-ca2/project/scripts/visualization/BehaviorTreeVisualizer.gd

144 lines
4.7 KiB
GDScript

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