diff --git a/state/assets/character_state_machine.json b/state/assets/character_state_machine.json index 3b6e3d4..0e3f861 100644 --- a/state/assets/character_state_machine.json +++ b/state/assets/character_state_machine.json @@ -127,6 +127,37 @@ } } ] + }, + { + "target": "Idle", + "conditions": [ + { + "type": ">=", + "left": { + "value": 10 + }, + "right": { + "function": "distance", + "args": [ + { + "accessor": [ + "character", + "position" + ] + }, + { + "accessor": [ + "root_nodes", + "StateMachineWorld", + "child_nodes", + "TrashBin", + "position" + ] + } + ] + } + } + ] } ] } diff --git a/state/scenes/state/StateMachine.gd b/state/scenes/state/StateMachine.gd index df7f988..afbffab 100644 --- a/state/scenes/state/StateMachine.gd +++ b/state/scenes/state/StateMachine.gd @@ -1,7 +1,9 @@ class_name StateMachine extends Node -var character: CharacterBody2D +@onready var tree_visualizer: Window = %TreeVisualizer +@onready var character: CharacterBody2D + # json read from file, contains all states and transitions var state_machine_data: Dictionary = {} # records all signals that were fired during the last frame, dictionary contains signal name and arguments @@ -44,6 +46,9 @@ func _ready() -> void: func defer_ready(): character.waste_detected.connect(Callable(self, "_on_waste_detected")) + # tree_visualizer.show() + tree_visualizer.state_machine = self + tree_visualizer.build_tree() # STATE MACHINE @@ -51,6 +56,16 @@ func _process(delta: float) -> void: if current_state: current_state.state_process(delta) check_transitions() + + %StateMachineInfoPanel.values["State"] = current_state.get_name() + %StateMachineInfoPanel.values["Position"] = character.position + for sgl in received_signals: + %StateMachineInfoPanel.values["signal_" + sgl["signal"]] = sgl["args"] + var transition_names: Array = [] + for transition in state_machine_data["states"][current_state.get_name()]["transitions"]: + transition_names.append(transition["target"]) + %StateMachineInfoPanel.values["Transitions"] = transition_names + received_signals = [] @@ -64,17 +79,22 @@ func check_transitions() -> void: if "conditions" in transition and transition["conditions"]: var all_conditions_met: bool = true for condition in transition["conditions"]: - if not transition_check_condition(condition): + var condition_met: bool = transition_check_condition(condition) + if not condition_met: all_conditions_met = false + %StateMachineInfoPanel.values["condition failed for " + transition["target"]] = condition break if not all_conditions_met: continue + %StateMachineInfoPanel.values["transitioned to"] = transition["target"] + # evaluate transfer parameters if any state_transfer_variables = {} if "transfer" in transition and transition["transfer"]: for key in transition["transfer"]: state_transfer_variables[key] = transition_resolve_parameter(transition["transfer"][key]) + %StateMachineInfoPanel.values["transfer [" + key + "]"] = state_transfer_variables[key] # allow current state to contribute to transfer variables current_state.contribute_transfer_variables(state_transfer_variables) @@ -104,7 +124,7 @@ func transition_check_condition(condition: Dictionary) -> bool: else: print("Unknown condition type [", type, "]") - print("[check_condition] ", left, " ", type, " ", right, " = ", result) + # print("[check_condition] ", left, " ", type, " ", right, " = ", result) return result @@ -131,7 +151,7 @@ func transition_resolve_function(function: Dictionary) -> Variant: if function["function"] == "distance": var result = args[0].distance_to(args[1]) - print("[resolve_function] distance(", args[0], ", ", args[1], ") = ", result) + # print("[resolve_function] distance(", args[0], ", ", args[1], ") = ", result) return result else: print("Unknown function [", function, "]") @@ -142,10 +162,10 @@ func transition_resolve_accessor(accessor: Array) -> Variant: var current_item: Variant = self for next_item in accessor: - print("[resolve_accessor] current_item: ", current_item, ' next_item: ', next_item) + # print("[resolve_accessor] current_item: ", current_item, ' next_item: ', next_item) if current_item == null: - print("[resolve_accessor] Null value in ", accessor) + # print("[resolve_accessor] Null value in ", accessor) break if objects_equal(current_item, self): @@ -175,7 +195,7 @@ func transition_resolve_accessor(accessor: Array) -> Variant: continue if current_item is Dictionary: - print("[resolve_accessor] Dictionary accessor, keys: ", current_item.keys()) + # print("[resolve_accessor] Dictionary accessor, keys: ", current_item.keys()) if next_item in current_item: current_item = current_item[next_item] continue @@ -185,9 +205,10 @@ func transition_resolve_accessor(accessor: Array) -> Variant: current_item = current_item.get(next_item) if current_item == null: - print("[resolve_accessor] Failed to resolve accessor ", accessor) + pass + # print("[resolve_accessor] Failed to resolve accessor ", accessor) - print("[resolve_accessor] resolved to ", current_item) + # print("[resolve_accessor] resolved to ", current_item) return current_item diff --git a/state/scenes/state/StateMachineWorld.tscn b/state/scenes/state/StateMachineWorld.tscn index 4ff66d0..84ae2e6 100644 --- a/state/scenes/state/StateMachineWorld.tscn +++ b/state/scenes/state/StateMachineWorld.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=12 format=3 uid="uid://cgdvptekjbpgq"] +[gd_scene load_steps=14 format=3 uid="uid://cgdvptekjbpgq"] [ext_resource type="Script" path="res://scenes/state/SMCharacter.gd" id="1_84313"] [ext_resource type="Script" path="res://scenes/state/SceneManagement.gd" id="1_s1suw"] +[ext_resource type="PackedScene" uid="uid://bvdodsxnqjk6x" path="res://scenes/state/visualization/TreeVisualizer.tscn" id="2_bgwhe"] [ext_resource type="Texture2D" uid="uid://cx04xknqfdscp" path="res://assets/cleaning_robot.png" id="2_cwwab"] [ext_resource type="Script" path="res://scenes/state/StateMachine.gd" id="2_d1xqo"] [ext_resource type="Script" path="res://scenes/state/state_idle.gd" id="3_r1btx"] @@ -9,6 +10,7 @@ [ext_resource type="Script" path="res://scenes/state/state_PickupTrash.gd" id="7_1rfah"] [ext_resource type="Script" path="res://scenes/state/state_ThrowTrashAway.gd" id="8_677fi"] [ext_resource type="PackedScene" uid="uid://dng6a8i8kp4kw" path="res://scenes/state/TrashBin.tscn" id="9_sd6r3"] +[ext_resource type="Script" path="res://scenes/state/visualization/state_machine_info_panel.gd" id="11_70vf4"] [sub_resource type="CapsuleShape2D" id="CapsuleShape2D_tr1gq"] radius = 60.0 @@ -20,8 +22,12 @@ size = Vector2(601.536, 254.293) [node name="StateMachineWorld" type="Node2D"] script = ExtResource("1_s1suw") +[node name="TreeVisualizer" parent="." instance=ExtResource("2_bgwhe")] +unique_name_in_owner = true +visible = false + [node name="Floor" type="Sprite2D" parent="."] -position = Vector2(573, 329) +position = Vector2(573, 323) scale = Vector2(0.635347, 0.635347) texture = ExtResource("6_15pjb") @@ -61,3 +67,11 @@ scale = Vector2(1, 0.48) shape = SubResource("RectangleShape2D_ji64e") [node name="Node2D" type="Node2D" parent="CleaningRobotCB2D"] + +[node name="StateMachineInfoPanel" type="VBoxContainer" parent="."] +unique_name_in_owner = true +offset_left = 6.0 +offset_top = 391.0 +offset_right = 205.0 +offset_bottom = 645.0 +script = ExtResource("11_70vf4") diff --git a/state/scenes/state/visualization/TreeVisualizer.tscn b/state/scenes/state/visualization/TreeVisualizer.tscn new file mode 100644 index 0000000..9806236 --- /dev/null +++ b/state/scenes/state/visualization/TreeVisualizer.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=3 uid="uid://bvdodsxnqjk6x"] + +[ext_resource type="Script" path="res://scenes/state/visualization/tree_visualizer.gd" id="1_62kxn"] + +[node name="TreeVisualizer" type="Window"] +position = Vector2i(50, 36) +size = Vector2i(1055, 560) +script = ExtResource("1_62kxn") + +[node name="GraphEdit" type="GraphEdit" parent="."] +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 +show_menu = false +show_zoom_buttons = false +show_grid_buttons = false +show_minimap_button = false +show_arrange_button = false + +[connection signal="connection_request" from="GraphEdit" to="." method="_on_graph_edit_connection_request"] diff --git a/state/scenes/state/visualization/d_tree_node.gd b/state/scenes/state/visualization/d_tree_node.gd new file mode 100644 index 0000000..5ad27c1 --- /dev/null +++ b/state/scenes/state/visualization/d_tree_node.gd @@ -0,0 +1,30 @@ +class_name DTreeNode +extends GraphNode + +var left_slots: Array[String] = [] +var right_slots: Array[String] = [] + + +func _ready() -> void: + pass + + +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) \ No newline at end of file diff --git a/state/scenes/state/visualization/d_tree_node.tscn b/state/scenes/state/visualization/d_tree_node.tscn new file mode 100644 index 0000000..1a3b9fb --- /dev/null +++ b/state/scenes/state/visualization/d_tree_node.tscn @@ -0,0 +1,20 @@ +[gd_scene load_steps=4 format=3 uid="uid://bmv75j2e8xc6n"] + +[ext_resource type="Script" path="res://scenes/state/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 = 128.0 +offset_bottom = 184.0 +mouse_filter = 1 +theme_override_styles/panel = SubResource("StyleBoxFlat_k54lu") +theme_override_styles/titlebar = SubResource("StyleBoxFlat_dmqj2") +draggable = false +selectable = false +title = "TITEL" +script = ExtResource("1_o2ffa") diff --git a/state/scenes/state/visualization/state_machine_info_panel.gd b/state/scenes/state/visualization/state_machine_info_panel.gd new file mode 100644 index 0000000..15f597d --- /dev/null +++ b/state/scenes/state/visualization/state_machine_info_panel.gd @@ -0,0 +1,47 @@ +class_name InfoPanel +extends VBoxContainer + +var values: Dictionary = {} +var last_values: Dictionary = {} +var value_order: Array = [] # Keep track of the order of values + +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: + 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: + value_order.erase(value) # Move to the end (most recent) + 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() \ No newline at end of file diff --git a/state/scenes/state/visualization/tree_visualizer.gd b/state/scenes/state/visualization/tree_visualizer.gd new file mode 100644 index 0000000..cb67d9a --- /dev/null +++ b/state/scenes/state/visualization/tree_visualizer.gd @@ -0,0 +1,107 @@ +extends Window + +const D_TREE_NODE: PackedScene = preload("res://scenes/state/visualization/d_tree_node.tscn") +@onready var graph_edit: GraphEdit = %GraphEdit + +# input data +var state_machine: StateMachine +# nodes +var all_nodes: Array[DTreeNode] = [] +var all_node_names_to_nodes: Dictionary = {} + + +func _ready(): + pass + + +func build_tree(): + if not state_machine: + push_error("No state machine set.") + return + + var node_positions: Array[DTreeNode] = [] + var y_spacing: int = 400 + var x_spacing: int = 400 + var depths: Dictionary = calculate_node_depths() + + var states = state_machine.state_machine_data["states"] + + # Place nodes + for i in range(states.size()): + var state_name = states.keys()[i] + var state_data = states[state_name] + + var graph_node: DTreeNode = D_TREE_NODE.instantiate() + graph_edit.add_child(graph_node) + graph_node.title = state_name + + all_nodes.append(graph_node) + all_node_names_to_nodes[state_name] = graph_node + + # Connect nodes + for i in range(states.size()): + var state_name = states.keys()[i] + var state_data = states[state_name] + var graph_node: DTreeNode = all_nodes[i] + + # Set up labels (input and output) + populate_node_labels(graph_node, state_name, state_data, all_node_names_to_nodes) + + # Calculate position based on tree depth + var depth: int = depths[i] + var siblings: Array = get_nodes_at_depth(depth, depths) + @warning_ignore("integer_division") + var y_pos: int = y_spacing * (siblings.find(i) - (siblings.size() - 1) / 2) + var x_pos: int = x_spacing * depth + y_pos += randi() % 200 - 100 + graph_node.position_offset = Vector2(x_pos, y_pos) + node_positions.append(graph_node) + + +func _on_graph_edit_connection_request(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> void: + print("Connection request from %s [%d] to %s [%d]" % [from_node, from_port, to_node, to_port]) + + +# using connect_node(from_node: StringName, from_port: int, to_node: StringName, to_port: int) -> int: +func populate_node_labels(node: DTreeNode, state_name: String, state_data: Dictionary, node_names_to_nodes: Dictionary) -> void: + # start from the node passed in and only use the output connections to determine the input connections on the other nodes + var output_transitions: Array = state_data.get("transitions", []) + for transition in output_transitions: + var target_state_name: String = transition['target'] + var port_out: Vector3i = node.add_label("%s" % [target_state_name], false, true) + + var target_node: DTreeNode = node_names_to_nodes[target_state_name] + if target_node: + var port_in: Vector3i = target_node.add_label("%s" % [state_name], true, false) + + # connect the nodes + graph_edit.connect_node( + node.get_name(), port_out.z, + target_node.get_name(), port_in.y + ) + print("Connecting %s [%d] to %s [%d]" % [node.get_name(), port_out, target_node.get_name(), port_in]) + + +func calculate_node_depths() -> Dictionary: + var depths: Dictionary = {} + depths[0] = 0 # Root is at depth 0 + var state_names = state_machine.state_machine_data["states"].keys() + for i in range(state_names.size()): + var state_name = state_names[i] + var state_data = state_machine.state_machine_data["states"][state_name] + if state_data.has("transitions"): + for transition in state_data["transitions"]: + if transition.has("target"): + var target_state_name = transition["target"] + var target_state_index = state_names.find(target_state_name) + if target_state_index != -1: + depths[target_state_index] = depths[i] + 1 + return depths + + +func get_nodes_at_depth(depth: int, depths: Dictionary) -> Array: + var nodes_at_depth: Array[Variant] = [] + for i in depths.keys(): + if depths[i] == depth: + nodes_at_depth.append(i) + return nodes_at_depth