diff --git a/state/assets/character_state_machine.json b/state/assets/character_state_machine.json index 0e3f861..3b6e3d4 100644 --- a/state/assets/character_state_machine.json +++ b/state/assets/character_state_machine.json @@ -127,37 +127,6 @@ } } ] - }, - { - "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/project.godot b/state/project.godot index de119e3..08c6976 100644 --- a/state/project.godot +++ b/state/project.godot @@ -23,6 +23,6 @@ window/stretch/mode="viewport" spawn_trash={ "deadzone": 0.5, -"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":2,"canceled":false,"pressed":false,"double_click":false,"script":null) ] } diff --git a/state/scenes/state/StateMachine.gd b/state/scenes/state/StateMachine.gd index afbffab..f0fcb78 100644 --- a/state/scenes/state/StateMachine.gd +++ b/state/scenes/state/StateMachine.gd @@ -82,7 +82,7 @@ func check_transitions() -> void: var condition_met: bool = transition_check_condition(condition) if not condition_met: all_conditions_met = false - %StateMachineInfoPanel.values["condition failed for " + transition["target"]] = condition + %StateMachineInfoPanel.values["condition failed for " + transition["target"]] = human_readable_condition(condition) break if not all_conditions_met: continue @@ -151,6 +151,7 @@ func transition_resolve_function(function: Dictionary) -> Variant: if function["function"] == "distance": var result = args[0].distance_to(args[1]) + %StateMachineInfoPanel.values["distance()"] = result # print("[resolve_function] distance(", args[0], ", ", args[1], ") = ", result) return result else: @@ -262,3 +263,67 @@ func objects_equal(a: Variant, b: Variant) -> bool: if typeof(a) != typeof(b): return false return a == b + + +func human_readable_transition(condition: Dictionary) -> String: + var parts: Array[Variant] = [] + + if condition.has("signal"): + parts.append(condition["signal"]) + + if condition.has("conditions"): + parts.append(human_readable_condition(condition)) + + return " & ".join(parts) + + +func human_readable_condition(condition: Dictionary) -> String: + if condition.has("conditions"): + var parts: Array[Variant] = [] + for cond in condition["conditions"]: + parts.append(human_readable_condition(cond)) + return " & ".join(parts) + + if condition.has("left") and condition.has("right") and condition.has("type"): + var left: Variant = condition["left"] + var right: Variant = condition["right"] + var type: String = condition["type"] + + return human_readable_parameter(left) + " " + type + " " + human_readable_parameter(right) + + return "Unknown condition" + + +func human_readable_parameter(parameter: Dictionary) -> String: + if "value" in parameter: + return str(parameter["value"]) + elif "function" in parameter: + return human_readable_function(parameter) + elif "accessor" in parameter: + return human_readable_accessor(parameter["accessor"]) + else: + return "Unknown parameter type" + + +func human_readable_function(function: Dictionary) -> String: + var args: Array = [] + if "args" in function: + for arg in function["args"]: + if arg is Dictionary: + args.append(human_readable_parameter(arg)) + else: + args.append(str(arg)) + + if function["function"] == "distance": + return "distance(" + ", ".join(args) + ")" + else: + return "Unknown function" + + +func human_readable_accessor(accessor: Array) -> String: + var parts: Array[Variant] = [] + var ignore_parts: Array[Variant] = ["signals", "child_nodes", "root_nodes"] + for part in accessor: + if not part in ignore_parts: + parts.append(str(part)) + return ".".join(parts) diff --git a/state/scenes/state/visualization/TreeVisualizer.tscn b/state/scenes/state/visualization/TreeVisualizer.tscn index 9806236..4896a6d 100644 --- a/state/scenes/state/visualization/TreeVisualizer.tscn +++ b/state/scenes/state/visualization/TreeVisualizer.tscn @@ -3,8 +3,8 @@ [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) +position = Vector2i(20, 331) +size = Vector2i(1115, 300) script = ExtResource("1_62kxn") [node name="GraphEdit" type="GraphEdit" parent="."] @@ -16,6 +16,7 @@ grow_horizontal = 2 grow_vertical = 2 size_flags_horizontal = 3 size_flags_vertical = 3 +scroll_offset = Vector2(-35, -30) show_menu = false show_zoom_buttons = false show_grid_buttons = false diff --git a/state/scenes/state/visualization/d_tree_node.gd b/state/scenes/state/visualization/d_tree_node.gd index 5ad27c1..bedba1d 100644 --- a/state/scenes/state/visualization/d_tree_node.gd +++ b/state/scenes/state/visualization/d_tree_node.gd @@ -3,10 +3,13 @@ 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: - pass + 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: @@ -27,4 +30,12 @@ func add_label(label_text: String, left: bool, right: bool) -> Vector3i: 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 + return Vector3i(child_index, left_slots.size() - 1, right_slots.size() - 1) + + +func set_highlighted(highlight: bool) -> void: + # set the background color of the node + if highlight: + self.add_theme_stylebox_override("panel", color_highlighted) + else: + self.add_theme_stylebox_override("panel", color_normal) diff --git a/state/scenes/state/visualization/d_tree_node.tscn b/state/scenes/state/visualization/d_tree_node.tscn index 1a3b9fb..f4b94f4 100644 --- a/state/scenes/state/visualization/d_tree_node.tscn +++ b/state/scenes/state/visualization/d_tree_node.tscn @@ -14,7 +14,5 @@ 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 index 15f597d..a358847 100644 --- a/state/scenes/state/visualization/state_machine_info_panel.gd +++ b/state/scenes/state/visualization/state_machine_info_panel.gd @@ -1,9 +1,10 @@ class_name InfoPanel extends VBoxContainer -var values: Dictionary = {} +var values: Dictionary = {} var last_values: Dictionary = {} -var value_order: Array = [] # Keep track of the order of values +var value_order: Array = [] # Keep track of the order of values + func to_str(value) -> String: if value is float: @@ -16,6 +17,7 @@ func to_str(value) -> String: 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: diff --git a/state/scenes/state/visualization/tree_visualizer.gd b/state/scenes/state/visualization/tree_visualizer.gd index cb67d9a..cef45a3 100644 --- a/state/scenes/state/visualization/tree_visualizer.gd +++ b/state/scenes/state/visualization/tree_visualizer.gd @@ -10,98 +10,112 @@ var all_nodes: Array[DTreeNode] = [] var all_node_names_to_nodes: Dictionary = {} -func _ready(): - pass +func _process(delta: float) -> void: + if Input.is_action_just_pressed("ui_accept"): + if is_visible(): + hide() + else: + show() + + # highlight the current state + for node in all_nodes: + node.set_highlighted(false) + + if state_machine and state_machine.current_state: + var current_state_name: StringName = state_machine.current_state.get_name() + if all_node_names_to_nodes.has(current_state_name): + all_node_names_to_nodes[current_state_name].set_highlighted(true) func build_tree(): - if not state_machine: - push_error("No state machine set.") - return + 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 node_positions: Array[DTreeNode] = [] + var y_spacing: int = 300 + var x_spacing: int = 600 + var depths: Dictionary = calculate_node_depths() - var states = state_machine.state_machine_data["states"] + 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] + # 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 + 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 + 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] + # 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) + # 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) + # 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]) + 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) + # 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(state_machine.human_readable_transition(transition), 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) + 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]) + # 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 + 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 + var nodes_at_depth: Array[Variant] = [] + for i in depths.keys(): + if depths[i] == depth: + nodes_at_depth.append(i) + return nodes_at_depth +