Skip to content

Commit

Permalink
Merge pull request #2 from derkork/fix/issue1
Browse files Browse the repository at this point in the history
fix issue #1
  • Loading branch information
derkork authored Apr 6, 2023
2 parents 81f3365 + df81ec4 commit 56e2a3c
Show file tree
Hide file tree
Showing 21 changed files with 225 additions and 77 deletions.
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/animation_tree_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/atomic_state.svg.import
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
26 changes: 15 additions & 11 deletions addons/godot_state_charts/compound_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/compound_state.svg.import
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/parallel_state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/parallel_state.svg.import
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
18 changes: 11 additions & 7 deletions addons/godot_state_charts/state.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion addons/godot_state_charts/state_chart.svg.import
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
90 changes: 74 additions & 16 deletions addons/godot_state_charts/utilities/state_chart_debugger.gd
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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.
Expand All @@ -42,13 +59,20 @@ 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:
push_warning("No state chart found. Disabling debugger.")
_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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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)
57 changes: 57 additions & 0 deletions addons/godot_state_charts/utilities/state_chart_debugger.tscn
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit 56e2a3c

Please sign in to comment.