diff --git a/CHANGES.md b/CHANGES.md index 7ff116d..d575111 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.0] - 2023-04-06 +### Breaking changes +- The state chart debugger now is no longer a single node but a full scene. This allows to have more complex UI in the debugger. Please replace the old debugger node with the new scene which is located at `addons/godot-statecharts/utilities/state_chart_debugger.tscn`. The debugger will no longer appear in the node list. You can quickly add it using the "Instatiate child scene" button in the scene inspector. + +### Improved +- The state charts debugger now can collect history of state changes, which helps understanding the state machine behavior and debugging it. + +### Fixed +- When transitioning directly to a state nested below a compound state, the initial state of the compound state will no longer be entered and immediately exited again ([#1](https://github.com/derkork/godot-statecharts/issues/1)). + ## [0.0.2] - 2023-03-31 diff --git a/addons/godot_state_charts/animation_tree_state.gd b/addons/godot_state_charts/animation_tree_state.gd index d41bc52..0fdd9cf 100644 --- a/addons/godot_state_charts/animation_tree_state.gd +++ b/addons/godot_state_charts/animation_tree_state.gd @@ -27,7 +27,7 @@ func _ready(): push_error("The animation tree is invalid. This node will not work.") -func _state_enter(): +func _state_enter(expect_transition:bool = false): super._state_enter() if not is_instance_valid(_animation_tree_state_machine): diff --git a/addons/godot_state_charts/atomic_state.svg.import b/addons/godot_state_charts/atomic_state.svg.import index 1b34013..cdf426c 100644 --- a/addons/godot_state_charts/atomic_state.svg.import +++ b/addons/godot_state_charts/atomic_state.svg.import @@ -5,7 +5,7 @@ type="CompressedTexture2D" uid="uid://c4ojtah20jtxc" path="res://.godot/imported/atomic_state.svg-5ab16e5747cef5b5980c4bf84ef9b1af.ctex" metadata={ -"editor_scale": 2.0, +"editor_scale": 1.0, "has_editor_variant": true, "vram_texture": false } diff --git a/addons/godot_state_charts/compound_state.gd b/addons/godot_state_charts/compound_state.gd index 2c52354..d8e180e 100644 --- a/addons/godot_state_charts/compound_state.gd +++ b/addons/godot_state_charts/compound_state.gd @@ -43,14 +43,15 @@ func _state_init(): child_as_state._state_init() -func _state_enter(): +func _state_enter(expect_transition:bool = false): super._state_enter() - # activate the initial state - if _initial_state != null: - _active_state = _initial_state - _active_state._state_enter() - else: - push_error("No initial state set for state '" + name + "'.") + # activate the initial state unless we expect a transition + if not expect_transition: + if _initial_state != null: + _active_state = _initial_state + _active_state._state_enter() + else: + push_error("No initial state set for state '" + name + "'.") func _state_save(saved_state:SavedState, child_levels:int = -1): @@ -125,7 +126,7 @@ func _handle_transition(transition:Transition, source:State): # the target state can be # 1. a direct child of this state. this is the easy case in which - # we will deactivate the current _active_state and activate the targer + # we will deactivate the current _active_state and activate the target # 2. a descendant of this state. in this case we find the direct child which # is the ancestor of the target state, activate it and then ask it to perform # the transition. @@ -140,13 +141,13 @@ func _handle_transition(transition:Transition, source:State): # now check if the target is a history state, if this is the # case, we need to restore the saved state if target is HistoryState: - print("Target is history state, restoring saved state.") + # print("Target is history state, restoring saved state.") var saved_state = target.history if saved_state != null: # restore the saved state _state_restore(saved_state, -1 if target.deep else 1) return - print("No history saved so far, activating default state.") + # print("No history saved so far, activating default state.") # if we don't have history, we just activate the default state var default_state = target.get_node_or_null(target.default_state) if is_instance_valid(default_state): @@ -173,7 +174,10 @@ func _handle_transition(transition:Transition, source:State): _active_state._state_exit() _active_state = child - _active_state._state_enter() + # set the "expect_transition" flag to true because we will send + # the transition to the child state right after we activate it. + # this avoids the child needlessly entering the initial state + _active_state._state_enter(true) # ask child to handle the transition child._handle_transition(transition, source) diff --git a/addons/godot_state_charts/compound_state.svg.import b/addons/godot_state_charts/compound_state.svg.import index 3f077ea..1ddf2ce 100644 --- a/addons/godot_state_charts/compound_state.svg.import +++ b/addons/godot_state_charts/compound_state.svg.import @@ -5,7 +5,7 @@ type="CompressedTexture2D" uid="uid://bbudjoa3ds4qj" path="res://.godot/imported/compound_state.svg-84780d78ec1f15e1cbb9d20f4df031a7.ctex" metadata={ -"editor_scale": 2.0, +"editor_scale": 1.0, "has_editor_variant": true, "vram_texture": false } diff --git a/addons/godot_state_charts/parallel_state.gd b/addons/godot_state_charts/parallel_state.gd index 9c45f57..106bd91 100644 --- a/addons/godot_state_charts/parallel_state.gd +++ b/addons/godot_state_charts/parallel_state.gd @@ -52,7 +52,7 @@ func _handle_transition(transition:Transition, source:State): # ask the parent get_parent()._handle_transition(transition, source) -func _state_enter(): +func _state_enter(expect_transition:bool = false): super._state_enter() # enter all children for child in _sub_states: diff --git a/addons/godot_state_charts/parallel_state.svg.import b/addons/godot_state_charts/parallel_state.svg.import index 5ecf240..fa08d18 100644 --- a/addons/godot_state_charts/parallel_state.svg.import +++ b/addons/godot_state_charts/parallel_state.svg.import @@ -5,7 +5,7 @@ type="CompressedTexture2D" uid="uid://dsa1nco51br8d" path="res://.godot/imported/parallel_state.svg-33f40e94bafae79f072d67563e0adcd3.ctex" metadata={ -"editor_scale": 2.0, +"editor_scale": 1.0, "has_editor_variant": true, "vram_texture": false } diff --git a/addons/godot_state_charts/state.gd b/addons/godot_state_charts/state.gd index 7cd04d3..a34ada1 100644 --- a/addons/godot_state_charts/state.gd +++ b/addons/godot_state_charts/state.gd @@ -41,8 +41,12 @@ func _state_init(): if child is Transition: _transitions.append(child) -## Called when the state is entered. -func _state_enter(): +## Called when the state is entered. The parameter indicates whether the state +## is expected to immediately handle a transition after it has been entered. +## In this case the state should not automatically activate a default child state. +## This is to avoid a situation where a state is entered, activates a child then immediately +## exits and activates another child due to a transition. +func _state_enter(expect_transition:bool = false): # print("state_enter: " + name) process_mode = Node.PROCESS_MODE_INHERIT # emit the signal @@ -108,7 +112,7 @@ func _state_save(saved_state:SavedState, child_levels:int = -1): ## If the state was not active when it was saved, this method still will be called ## but the given SavedState object will not contain any data for this state. func _state_restore(saved_state:SavedState, child_levels:int = -1): - print("restoring state " + name) + # print("restoring state " + name) var our_saved_state = saved_state.get_substate_or_null(self) if our_saved_state == null: # if we are currently active, deactivate the state @@ -124,10 +128,10 @@ func _state_restore(saved_state:SavedState, child_levels:int = -1): _pending_transition = get_node_or_null(our_saved_state.pending_transition_name) as Transition _pending_transition_time = our_saved_state.pending_transition_time - if _pending_transition != null: - print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_time)) - else: - print("no pending transition restored") + # if _pending_transition != null: + # print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_time)) + # else: + # print("no pending transition restored") if child_levels == 0: return diff --git a/addons/godot_state_charts/state_chart.svg.import b/addons/godot_state_charts/state_chart.svg.import index aaca196..eed6a2d 100644 --- a/addons/godot_state_charts/state_chart.svg.import +++ b/addons/godot_state_charts/state_chart.svg.import @@ -5,7 +5,7 @@ type="CompressedTexture2D" uid="uid://vfbywtgh66nb" path="res://.godot/imported/state_chart.svg-5c268dd045b20d73dfacd5cdf7606676.ctex" metadata={ -"editor_scale": 2.0, +"editor_scale": 1.0, "has_editor_variant": true, "vram_texture": false } diff --git a/addons/godot_state_charts/utilities/state_chart_debugger.gd b/addons/godot_state_charts/utilities/state_chart_debugger.gd index c993ff0..0a3414b 100644 --- a/addons/godot_state_charts/utilities/state_chart_debugger.gd +++ b/addons/godot_state_charts/utilities/state_chart_debugger.gd @@ -1,6 +1,5 @@ @icon("state_chart_debugger.svg") -class_name StateChartDebugger -extends Tree +extends Control ## Whether or not the debugger is enabled. @export var enabled:bool = true: @@ -9,29 +8,47 @@ extends Tree if not Engine.is_editor_hint(): _setup_processing(enabled) +## Whether or not the debugger should automatically track state changes. +@export var auto_track_state_changes:bool = true + +## The list of collected events. +var _events:Array[Dictionary] = [] + ## The initial node that should be watched. Optional, if not set ## then no node will be watched. You can set the node that should ## be watched at runtime by calling debug_node(). @export var initial_node_to_watch:NodePath +## The tree that shows the state chart. +@onready var _tree:Tree = %Tree +## The text field with the history. +@onready var _historyEdit:TextEdit = %HistoryEdit + # the state chart we track var _state_chart:StateChart var _root:Node -func _init(): - scroll_horizontal_enabled = false - scroll_vertical_enabled = false - mouse_filter = Control.MOUSE_FILTER_IGNORE - - +# the states we are currently connected to +var _connected_states:Array[State] = [] + func _ready(): # always run, even if the game is paused process_mode = Node.PROCESS_MODE_ALWAYS + %CopyToClipboardButton.pressed.connect(func (): DisplayServer.clipboard_set(_historyEdit.text)) + %ClearButton.pressed.connect(func (): _historyEdit.text = "") + var to_watch = get_node_or_null(initial_node_to_watch) if is_instance_valid(to_watch): debug_node(to_watch) +## Adds an item to the history list. +func add_history_entry(text:String): + var seconds = Time.get_ticks_msec() / 1000.0 + _historyEdit.text += "[%.3f]: %s \n" % [seconds, text] + _historyEdit.scroll_vertical = _historyEdit.get_line_count() - 1 + + ## Sets up the debugger to track the given state chart. If the given node is not ## a state chart, it will search the children for a state chart. If no state chart ## is found, the debugger will be disabled. @@ -42,6 +59,9 @@ func debug_node(root:Node) -> bool: _root = root var success = _debug_node(root) + + # disconnect all existing signals + _disconnect_all_signals() # if we have no success, we disable the debugger if not success: @@ -49,6 +69,10 @@ func debug_node(root:Node) -> bool: _setup_processing(false) _state_chart = null else: + # find all state nodes below the state chart and connect their signals + _connect_all_signals() + # clear the history + _historyEdit.text = "" _setup_processing(true) return success @@ -77,15 +101,48 @@ func _setup_processing(enabled:bool): process_mode = Node.PROCESS_MODE_ALWAYS if enabled else Node.PROCESS_MODE_DISABLED visible = enabled +## Disconnects all signals from the currently connected states. +func _disconnect_all_signals(): + for state in _connected_states: + state.state_entered.disconnect(_on_state_entered) + state.state_exited.disconnect(_on_state_exited) + + +## Connects all signals from the currently processing state chart +func _connect_all_signals(): + _connected_states.clear() + + if not auto_track_state_changes: + return + + if not is_instance_valid(_state_chart): + return + + # find all state nodes below the state chart and connect their signals + for child in _state_chart.get_children(): + if child is State: + _connect_signals(child) + + +func _connect_signals(state:State): + state.state_entered.connect(_on_state_entered.bind(state)) + state.state_exited.connect(_on_state_exited.bind(state)) + _connected_states.append(state) + + # recurse into children + for child in state.get_children(): + if child is State: + _connect_signals(child) + func _process(delta): # Clear contents - clear() + _tree.clear() if not is_instance_valid(_state_chart): return - var root = create_item() + var root = _tree.create_item() root.set_text(0, _root.name) # walk over the state chart and find all active states @@ -109,17 +166,12 @@ func _process(delta): var property_line = properties_root.create_child() property_line.set_text(0, "%s = %s" % [item, value]) - - - - - func _collect_active_states(root:Node, parent:TreeItem): for child in root.get_children(): if child is State: if child.active: - var state_item = create_item(parent) + var state_item = _tree.create_item(parent) state_item.set_text(0, child.name) if is_instance_valid(child._pending_transition): @@ -129,3 +181,9 @@ func _collect_active_states(root:Node, parent:TreeItem): _collect_active_states(child, state_item) +func _on_state_entered(state:State): + add_history_entry("Enter: %s" % state.name) + + +func _on_state_exited(state:State): + add_history_entry("exiT : %s" % state.name) diff --git a/addons/godot_state_charts/utilities/state_chart_debugger.tscn b/addons/godot_state_charts/utilities/state_chart_debugger.tscn new file mode 100644 index 0000000..36a03c6 --- /dev/null +++ b/addons/godot_state_charts/utilities/state_chart_debugger.tscn @@ -0,0 +1,57 @@ +[gd_scene load_steps=2 format=3 uid="uid://bcwkugn6v3oy7"] + +[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="1_i74os"] + +[node name="StateChartDebugger" type="MarginContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_i74os") + +[node name="TabContainer" type="TabContainer" parent="."] +layout_mode = 2 + +[node name="StateChart" type="MarginContainer" parent="TabContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="Tree" type="Tree" parent="TabContainer/StateChart"] +unique_name_in_owner = true +layout_mode = 2 +scroll_horizontal_enabled = false +scroll_vertical_enabled = false + +[node name="History" type="MarginContainer" parent="TabContainer"] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 5 +theme_override_constants/margin_top = 5 +theme_override_constants/margin_right = 5 +theme_override_constants/margin_bottom = 5 + +[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/History"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="HistoryEdit" type="TextEdit" parent="TabContainer/History/VBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/History/VBoxContainer"] +layout_mode = 2 + +[node name="ClearButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Clear" + +[node name="CopyToClipboardButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"] +unique_name_in_owner = true +layout_mode = 2 +text = "Copy to Clipboard" diff --git a/godot_state_charts_examples/ant_hill/ant_hill_demo.tscn b/godot_state_charts_examples/ant_hill/ant_hill_demo.tscn index aee40c2..8c6e56e 100644 --- a/godot_state_charts_examples/ant_hill/ant_hill_demo.tscn +++ b/godot_state_charts_examples/ant_hill/ant_hill_demo.tscn @@ -1,12 +1,12 @@ -[gd_scene load_steps=10 format=3] +[gd_scene load_steps=10 format=3 uid="uid://bdqi413verijg"] [ext_resource type="TileSet" uid="uid://cd6hbvgl1e2xy" path="res://godot_state_charts_examples/platformer/terrain/terrain_tiles.tres" id="1_bqip5"] [ext_resource type="Script" path="res://godot_state_charts_examples/ant_hill/ant_hill_demo.gd" id="1_l75mo"] +[ext_resource type="PackedScene" uid="uid://bcwkugn6v3oy7" path="res://addons/godot_state_charts/utilities/state_chart_debugger.tscn" id="2_02gbl"] [ext_resource type="PackedScene" uid="uid://dy43c80qlfftx" path="res://godot_state_charts_examples/ant_hill/ant/ant.tscn" id="2_st63i"] [ext_resource type="PackedScene" path="res://godot_state_charts_examples/ant_hill/nest/nest.tscn" id="3_qp6b5"] [ext_resource type="PackedScene" path="res://godot_state_charts_examples/ant_hill/banana/banana.tscn" id="4_gdnva"] [ext_resource type="Theme" uid="uid://s2bj74tt0y7f" path="res://godot_state_charts_examples/new_theme.tres" id="5_qpq0w"] -[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="6_6m8lx"] [ext_resource type="Script" path="res://godot_state_charts_examples/ant_hill/pause_controller.gd" id="8_kxcui"] [sub_resource type="NavigationPolygon" id="NavigationPolygon_eas1h"] @@ -17,6 +17,16 @@ outlines = Array[PackedVector2Array]([PackedVector2Array(629, 16, 627, 468, 11, [node name="AntHill" type="Node2D"] script = ExtResource("1_l75mo") +[node name="StateChartDebugger" parent="." instance=ExtResource("2_02gbl")] +modulate = Color(1, 1, 1, 0.811765) +z_index = 5 +offset_left = 394.0 +offset_top = -1.0 +offset_right = 634.0 +offset_bottom = 235.0 +theme = ExtResource("5_qpq0w") +initial_node_to_watch = NodePath("../Ant") + [node name="TileMap" type="TileMap" parent="."] tile_set = ExtResource("1_bqip5") format = 2 @@ -103,14 +113,6 @@ position = Vector2(604, 35) [node name="banana9" parent="." instance=ExtResource("4_gdnva")] position = Vector2(585, 57) -[node name="StateChartDebugger" type="Tree" parent="."] -offset_left = 394.0 -offset_top = 3.0 -offset_right = 636.0 -offset_bottom = 241.0 -theme = ExtResource("5_qpq0w") -script = ExtResource("6_6m8lx") - [node name="NavigationRegion2D" type="NavigationRegion2D" parent="."] position = Vector2(2, -6) navigation_polygon = SubResource("NavigationPolygon_eas1h") diff --git a/godot_state_charts_examples/history_states/history_demo.gd b/godot_state_charts_examples/history_states/history_demo.gd index 973952a..afbfcf7 100644 --- a/godot_state_charts_examples/history_states/history_demo.gd +++ b/godot_state_charts_examples/history_states/history_demo.gd @@ -1,12 +1,6 @@ extends Node @onready var state_chart:StateChart = $StateChart -@onready var debugger:StateChartDebugger = $StateChartDebugger - - -func _ready(): - debugger.debug_node(state_chart) - func _on_area_2d_input_event(_viewport:Node, event:InputEvent, _shape_idx:int): if event is InputEventMouseButton: @@ -14,4 +8,4 @@ func _on_area_2d_input_event(_viewport:Node, event:InputEvent, _shape_idx:int): # on release send clicked event to state chart if not event.is_pressed(): state_chart.send_event("clicked") - \ No newline at end of file + diff --git a/godot_state_charts_examples/history_states/history_demo.tscn b/godot_state_charts_examples/history_states/history_demo.tscn index 3ffbb0d..906fc55 100644 --- a/godot_state_charts_examples/history_states/history_demo.tscn +++ b/godot_state_charts_examples/history_states/history_demo.tscn @@ -1,13 +1,13 @@ -[gd_scene load_steps=11 format=3] +[gd_scene load_steps=11 format=3 uid="uid://b18rv6o4duide"] [ext_resource type="Texture2D" uid="uid://bgw8xgbwc2flx" path="res://godot_state_charts_examples/history_states/white_rectangle.svg" id="1_3v23e"] +[ext_resource type="PackedScene" uid="uid://bcwkugn6v3oy7" path="res://addons/godot_state_charts/utilities/state_chart_debugger.tscn" id="2_fgw1q"] [ext_resource type="Script" path="res://addons/godot_state_charts/state_chart.gd" id="2_pqmip"] [ext_resource type="Script" path="res://godot_state_charts_examples/history_states/history_demo.gd" id="2_vphtk"] [ext_resource type="Script" path="res://addons/godot_state_charts/compound_state.gd" id="3_nsw2j"] [ext_resource type="Script" path="res://addons/godot_state_charts/history_state.gd" id="4_0qaqv"] [ext_resource type="Script" path="res://addons/godot_state_charts/atomic_state.gd" id="5_lh5sp"] [ext_resource type="Script" path="res://addons/godot_state_charts/transition.gd" id="6_xvm5g"] -[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="8_i07w7"] [ext_resource type="Theme" uid="uid://s2bj74tt0y7f" path="res://godot_state_charts_examples/new_theme.tres" id="8_najew"] [sub_resource type="RectangleShape2D" id="RectangleShape2D_kl3ga"] @@ -16,13 +16,13 @@ size = Vector2(34.243, 33.536) [node name="Node" type="Node"] script = ExtResource("2_vphtk") -[node name="StateChartDebugger" type="Tree" parent="."] -offset_left = 325.0 -offset_top = 5.0 -offset_right = 627.0 -offset_bottom = 167.0 +[node name="StateChartDebugger" parent="." instance=ExtResource("2_fgw1q")] +offset_left = 280.0 +offset_top = 9.0 +offset_right = -12.0 +offset_bottom = -190.0 theme = ExtResource("8_najew") -script = ExtResource("8_i07w7") +initial_node_to_watch = NodePath("../StateChart") [node name="Node2D" type="Sprite2D" parent="."] position = Vector2(145, 206) diff --git a/godot_state_charts_examples/platformer/checkpoint/checkpoint.gd b/godot_state_charts_examples/platformer/checkpoint/checkpoint.gd index ecc168e..523fee3 100644 --- a/godot_state_charts_examples/platformer/checkpoint/checkpoint.gd +++ b/godot_state_charts_examples/platformer/checkpoint/checkpoint.gd @@ -15,7 +15,7 @@ signal deactivated(checkpoint:Node2D) func _on_area_2d_input_event(_viewport:Node, event:InputEvent, _shape_idx:int): # if event was left mouse button up, emit clicked signal if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed() == false: - print("Checkpoint clicked") + # print("Checkpoint clicked") clicked.emit(self) diff --git a/godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn b/godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn index 8f90498..9254b48 100644 --- a/godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn +++ b/godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=18 format=3] +[gd_scene load_steps=18 format=3 uid="uid://bvrbfp870t0kd"] [ext_resource type="Script" path="res://godot_state_charts_examples/platformer/fireworks_box/fireworks_box.gd" id="1_6vg3k"] [ext_resource type="Texture2D" uid="uid://roilgifkgiii" path="res://godot_state_charts_examples/platformer/fireworks_box/box.png" id="1_aido8"] diff --git a/godot_state_charts_examples/platformer/platformer_demo.tscn b/godot_state_charts_examples/platformer/platformer_demo.tscn index 1fb19d9..80a3ae3 100644 --- a/godot_state_charts_examples/platformer/platformer_demo.tscn +++ b/godot_state_charts_examples/platformer/platformer_demo.tscn @@ -2,10 +2,10 @@ [ext_resource type="TileSet" uid="uid://cd6hbvgl1e2xy" path="res://godot_state_charts_examples/platformer/terrain/terrain_tiles.tres" id="1_l1t3g"] [ext_resource type="PackedScene" uid="uid://v5vg88it87oj" path="res://godot_state_charts_examples/platformer/ninja_frog/ninja_frog.tscn" id="2_3w7a1"] +[ext_resource type="PackedScene" uid="uid://bcwkugn6v3oy7" path="res://addons/godot_state_charts/utilities/state_chart_debugger.tscn" id="3_fqmqq"] [ext_resource type="Theme" uid="uid://s2bj74tt0y7f" path="res://godot_state_charts_examples/new_theme.tres" id="3_n1xun"] -[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="4_qtefp"] [ext_resource type="PackedScene" path="res://godot_state_charts_examples/platformer/checkpoint/checkpoint.tscn" id="6_spkwb"] -[ext_resource type="PackedScene" path="res://godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn" id="7_pg57e"] +[ext_resource type="PackedScene" uid="uid://bvrbfp870t0kd" path="res://godot_state_charts_examples/platformer/fireworks_box/fireworks_box.tscn" id="7_pg57e"] [node name="Level" type="Node2D"] @@ -20,13 +20,13 @@ layer_0/tile_data = PackedInt32Array(1507331, 393216, 4, 1572867, 393216, 5, 163 z_index = 2048 position = Vector2(78, 361) -[node name="StateChartDebugger" type="Tree" parent="."] -offset_left = 394.0 -offset_top = 3.0 -offset_right = 636.0 -offset_bottom = 241.0 +[node name="StateChartDebugger" parent="." instance=ExtResource("3_fqmqq")] +modulate = Color(1, 1, 1, 0.572549) +offset_left = 378.0 +offset_top = 34.0 +offset_right = 640.0 +offset_bottom = 265.0 theme = ExtResource("3_n1xun") -script = ExtResource("4_qtefp") initial_node_to_watch = NodePath("../NinjaFrog") [node name="Checkpoint" parent="." instance=ExtResource("6_spkwb")] diff --git a/manual/add_statechart_debugger.gif b/manual/add_statechart_debugger.gif new file mode 100644 index 0000000..4a4b3b2 Binary files /dev/null and b/manual/add_statechart_debugger.gif differ diff --git a/manual/debugger_history_tracking.png b/manual/debugger_history_tracking.png new file mode 100644 index 0000000..ab197d9 Binary files /dev/null and b/manual/debugger_history_tracking.png differ diff --git a/manual/manual.md b/manual/manual.md index 603443e..62de26c 100644 --- a/manual/manual.md +++ b/manual/manual.md @@ -139,11 +139,15 @@ These properties can then be used in your expressions. The following example sho ### Debugging - When the game is running it is very useful to see the current state of the state chart for debugging purposes. For this, this library contains a state chart debugger that you can add to your scene. You can add the debugger through the _Add Node_ dialog. Search for _StateChartDebugger_ and add it to your scene. It is a tree control that you can position anywhere in your scene where it makes sense (maybe you already have an in-game debugging screen where you can add it). + When the game is running it is very useful to see the current state of the state chart for debugging purposes. For this, this library contains a state chart debugger that you can add to your scene. You can add it to your scene by pressing the "Instantiate child scene" icon above the node tree and then looking for "debugger": + +![Adding the state chart debugger](add_statechart_debugger.gif) + + The debugger is a control node that you can position anywhere in your scene where it makes sense (maybe you already have an in-game debugging screen where you can add it). Since it is a control it can easily be integrated into an existing UI. ![The state chart debugger](state_chart_debugger.png) -The state chart debugger is has a property _Initial node to watch_ where you can set a node that should be watched. It doesn't necessarily need to be a state chart node, the debugger will search for a state chart anywhere below the node you set. This is usefuly when you have the state chart nested in a sub-scene and you want to watch the state chart from the root scene where you don't have access to the state chart node. +The state chart debugger is has a property _Initial node to watch_ where you can set a node that should be watched. It doesn't necessarily need to be a state chart node, the debugger will search for a state chart anywhere below the node you set. This is useful when you have the state chart nested in a sub-scene and you want to watch the state chart from the root scene where you don't have access to the state chart node. You can also use the `debug_node` function of the state chart debugger to change the node that is being watched at runtime. For example you could add code that changes the debugged node when clicking on a unit or object in your game @@ -162,6 +166,21 @@ At runtime, the state chart debugger will show the current state of the state ch ![Live view of the state chart debugger](state_chart_debugger_live.png) +By default, the state chart debugger will track state changes in the state chart it watches and print them into the "History" tab. This way you can see which state transitioned into which state and when. + +![Tracking history with the debugger](debugger_history_tracking.png) + + +You can add custom lines into the history by calling the `add_history_entry` function. This is useful if you want to have additional information in the history. + +```gdscript +debugger.add_history_entry("Player died") +``` + +The debugger will only track state changes of the currently watched state chart. If you connect the debugger to a different state chart, it will start tracking the state changes of the new state chart. + +If you want to disable the history tracking, you can unset the _Auto Track State Changes_ checkbox in the editor UI. + ## Tips & tricks ### Keep state and logic separate diff --git a/manual/state_chart_debugger.png b/manual/state_chart_debugger.png index bc47f57..c882c09 100644 Binary files a/manual/state_chart_debugger.png and b/manual/state_chart_debugger.png differ