159 lines
5.4 KiB
GDScript
159 lines
5.4 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
|
|
|
|
#
|
|
var behavior_tree: BehaviorTree
|
|
#
|
|
var x_spacing: int = 430
|
|
var y_spacing: int = 100
|
|
#
|
|
var all_nodes: Array[DTreeNode] = []
|
|
# Dictionary[Task, DTreeNode]
|
|
var task_to_node: Dictionary = {}
|
|
var parent_nodes: Dictionary = {}
|
|
#
|
|
var current_lowest_node_pos: Vector2 = Vector2(0, 0)
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
if Input.is_action_just_pressed("toggle_graph_edit"):
|
|
if is_visible():
|
|
hide()
|
|
else:
|
|
show()
|
|
|
|
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()
|
|
|
|
var root_task: Task = behavior_tree.behavior_tree
|
|
if not root_task:
|
|
push_error("Root behavior tree node is null.")
|
|
return
|
|
|
|
current_lowest_node_pos = Vector2(0, 0)
|
|
build_tree_from_task(root_task, 0)
|
|
|
|
|
|
func build_tree_from_task(task: Task, depth: int) -> DTreeNode:
|
|
var child_nodes: Array[DTreeNode] = []
|
|
var child_node_positions: Array[Vector2] = []
|
|
|
|
for child in task.get_children():
|
|
var child_node: DTreeNode = build_tree_from_task(child, depth + 1)
|
|
child_nodes.append(child_node)
|
|
child_node_positions.append(child_node.position_offset)
|
|
|
|
var current_node: DTreeNode = D_TREE_NODE.instantiate()
|
|
graph_edit.add_child(current_node)
|
|
task_to_node[task] = current_node
|
|
current_node.name = task.get_name() + str(randf())
|
|
current_node.title = human_readable_task_name(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)
|
|
parent_nodes[child] = current_node
|
|
|
|
if child_node_positions.size() > 0:
|
|
var average_position: Vector2 = Vector2(0, 0)
|
|
for pos in child_node_positions:
|
|
average_position += pos
|
|
average_position /= child_node_positions.size()
|
|
|
|
current_node.position_offset = average_position - Vector2(x_spacing, 0)
|
|
# 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
|
|
|
|
return current_node
|
|
|
|
|
|
func update_task_statuses(blackboard: Dictionary) -> void:
|
|
for t in task_to_node.keys():
|
|
var task: Task = t as Task
|
|
var node: DTreeNode = task_to_node[task]
|
|
var status: int = task.status
|
|
var clear_status: String = task.clear_status()
|
|
var status_reason: String = task.status_reason
|
|
|
|
if status_reason != "":
|
|
node.set_label_text(0, status_reason)
|
|
else:
|
|
node.set_label_text(0, clear_status)
|
|
|
|
node.set_body_color(node.color_normal)
|
|
if status == Task.RUNNING or status == Task.SUCCESS or status == Task.SUCCESS_STOP:
|
|
node.set_body_color(node.color_success)
|
|
|
|
if blackboard.has("current_task"):
|
|
var selected_node = task_to_node[blackboard["current_task"]]
|
|
if selected_node:
|
|
center_view_on_position(selected_node.position_offset)
|
|
selected_node.set_body_color(selected_node.color_executed)
|
|
while parent_nodes.has(selected_node):
|
|
selected_node = parent_nodes[selected_node]
|
|
selected_node.set_body_color(selected_node.color_checked)
|
|
|
|
|
|
func center_view_on_position(target_position: Vector2) -> void:
|
|
var graph_edit_size: Vector2 = graph_edit.size
|
|
var zoom: float = graph_edit.zoom
|
|
var offset_x: float = target_position.x * zoom - graph_edit_size.x / 2
|
|
var offset_y: float = target_position.y * zoom - graph_edit_size.y / 2
|
|
graph_edit.scroll_offset = Vector2(offset_x, offset_y)
|
|
|
|
|
|
func human_readable_task_name(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: String = 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
|
|
|
|
return result
|
|
|
|
|
|
func _on_close_requested() -> void:
|
|
hide()
|