forked from 2121578/gai-ca2
FINALLY, A WORKING TREE DISTRIBUTION ALGORITHM
parent
b3220f968d
commit
fcbd269f68
|
@ -218,7 +218,6 @@ size_flags_vertical = 8
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
position = Vector2i(0, 36)
|
position = Vector2i(0, 36)
|
||||||
size = Vector2i(1075, 225)
|
size = Vector2i(1075, 225)
|
||||||
visible = false
|
|
||||||
script = ExtResource("5_ecfvx")
|
script = ExtResource("5_ecfvx")
|
||||||
|
|
||||||
[node name="GraphEdit" type="GraphEdit" parent="Camera2D/CanvasLayer/TreeVisualizer"]
|
[node name="GraphEdit" type="GraphEdit" parent="Camera2D/CanvasLayer/TreeVisualizer"]
|
||||||
|
|
|
@ -4,13 +4,15 @@ extends Window
|
||||||
const D_TREE_NODE: PackedScene = preload("res://scripts/visualization/d_tree_node.tscn")
|
const D_TREE_NODE: PackedScene = preload("res://scripts/visualization/d_tree_node.tscn")
|
||||||
@onready var graph_edit: GraphEdit = %GraphEdit
|
@onready var graph_edit: GraphEdit = %GraphEdit
|
||||||
|
|
||||||
# Input data
|
#
|
||||||
var behavior_tree: BehaviorTree
|
var behavior_tree: BehaviorTree
|
||||||
|
#
|
||||||
|
var x_spacing: int = 400
|
||||||
|
var y_spacing: int = 100
|
||||||
|
#
|
||||||
var all_nodes: Array[DTreeNode] = []
|
var all_nodes: Array[DTreeNode] = []
|
||||||
var all_node_names_to_nodes: Dictionary = {}
|
#
|
||||||
# Configuration
|
var current_lowest_node_pos: Vector2 = Vector2(0, 0)
|
||||||
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:
|
func build_tree() -> void:
|
||||||
|
@ -18,127 +20,88 @@ func build_tree() -> void:
|
||||||
push_error("No behavior tree set.")
|
push_error("No behavior tree set.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Reset current visualization
|
# reset current visualization
|
||||||
graph_edit.clear_connections()
|
graph_edit.clear_connections()
|
||||||
for node in all_nodes:
|
for node in all_nodes:
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
all_nodes.clear()
|
all_nodes.clear()
|
||||||
all_node_names_to_nodes.clear()
|
|
||||||
|
|
||||||
# Build tree starting from the root task
|
var root_task: Task = behavior_tree.behavior_tree
|
||||||
var root_node: Task = behavior_tree.behavior_tree
|
if not root_task:
|
||||||
if not root_node:
|
|
||||||
push_error("Root behavior tree node is null.")
|
push_error("Root behavior tree node is null.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Calculate positions for all nodes
|
current_lowest_node_pos = Vector2(0, 0)
|
||||||
var positions: Dictionary = calculate_tree_layout(root_node)
|
build_tree_from_task(root_task, 0)
|
||||||
|
|
||||||
# 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:
|
func build_tree_from_task(task: Task, depth: int) -> DTreeNode:
|
||||||
"""
|
var child_nodes: Array[DTreeNode] = []
|
||||||
Calculates the positions of all nodes in the tree, ensuring no overlaps.
|
var child_node_positions: Array[Vector2] = []
|
||||||
"""
|
|
||||||
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():
|
for child in task.get_children():
|
||||||
if child is Task:
|
var child_node: DTreeNode = build_tree_from_task(child, depth + 1)
|
||||||
queue.append({"task": child as Task, "depth": depth + 1})
|
child_nodes.append(child_node)
|
||||||
|
child_node_positions.append(child_node.position_offset)
|
||||||
|
|
||||||
return layers
|
var current_node: DTreeNode = D_TREE_NODE.instantiate()
|
||||||
|
graph_edit.add_child(current_node)
|
||||||
|
current_node.name = task.get_name() + str(randf())
|
||||||
|
current_node.title = transform_string(task.get_name())
|
||||||
|
current_node.add_label("status", true, true)
|
||||||
|
all_nodes.append(current_node)
|
||||||
|
|
||||||
|
for child in child_nodes:
|
||||||
|
graph_edit.connect_node(current_node.name, 0, child.name, 0)
|
||||||
|
|
||||||
func create_node(task: Task, position: Vector2) -> void:
|
if child_node_positions.size() > 0:
|
||||||
"""
|
var average_position: Vector2 = Vector2(0, 0)
|
||||||
Creates a visual node for the given task at the specified position.
|
for pos in child_node_positions:
|
||||||
"""
|
average_position += pos
|
||||||
var graph_node: DTreeNode = D_TREE_NODE.instantiate()
|
average_position /= child_node_positions.size()
|
||||||
graph_node.title = task.name
|
|
||||||
graph_node.position_offset = position
|
|
||||||
graph_edit.add_child(graph_node)
|
|
||||||
|
|
||||||
all_nodes.append(graph_node)
|
current_node.position_offset = average_position - Vector2(x_spacing, 0)
|
||||||
all_node_names_to_nodes[task.name] = graph_node
|
# print("as parent: ", current_node.name, " ", current_node.position_offset, " ", depth, " ", child_node_positions)
|
||||||
|
|
||||||
|
else:
|
||||||
|
current_node.position_offset = Vector2(current_lowest_node_pos)
|
||||||
|
current_node.position_offset.x += depth * x_spacing
|
||||||
|
current_lowest_node_pos.y += y_spacing
|
||||||
|
# print("as leaf: ", current_node.name, " ", current_node.position_offset, " ", depth)
|
||||||
|
pass
|
||||||
|
|
||||||
func connect_nodes(parent_name: String, child_name: String) -> void:
|
return current_node
|
||||||
"""
|
|
||||||
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 transform_string(input: String) -> String:
|
||||||
|
var prefixes: Dictionary = {"sl_": "Selector: ", "sq_": "Sequence: ", "Task": ""}
|
||||||
|
var selected_prefix: String = ""
|
||||||
|
|
||||||
|
for prefix in prefixes.keys():
|
||||||
|
if input.begins_with(prefix):
|
||||||
|
selected_prefix = prefix
|
||||||
|
input = input.substr(prefix.length())
|
||||||
|
break
|
||||||
|
|
||||||
|
var words: Array[Variant] = []
|
||||||
|
var current_word: String = ""
|
||||||
|
|
||||||
|
for i in range(input.length()):
|
||||||
|
var character = input[i]
|
||||||
|
if character.to_upper() == character and current_word.length() > 0:
|
||||||
|
words.append(current_word)
|
||||||
|
current_word = "" + character.to_lower()
|
||||||
|
elif character == "_":
|
||||||
|
if current_word.length() > 0:
|
||||||
|
words.append(current_word)
|
||||||
|
current_word = ""
|
||||||
|
else:
|
||||||
|
current_word += character.to_lower()
|
||||||
|
|
||||||
|
if current_word.length() > 0:
|
||||||
|
words.append(current_word)
|
||||||
|
|
||||||
|
var result: String = " ".join(words)
|
||||||
|
if selected_prefix in prefixes and prefixes[selected_prefix] != "":
|
||||||
|
result = prefixes[selected_prefix] + result
|
||||||
|
|
||||||
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 result
|
||||||
return null
|
|
Loading…
Reference in New Issue