initial commit: canny cat basic movement, bouncing, some gridmap tiles for levels

This commit is contained in:
Haze Weathers 2025-02-22 16:48:31 -05:00
commit e1b43c8bc5
120 changed files with 5785 additions and 0 deletions

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Jan Thomä
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,22 @@
@tool
@icon("all_of_guard.svg")
## A composite guard that is satisfied when all of its guards are satisfied.
class_name AllOfGuard
extends Guard
## The guards that need to be satisified. When empty, returns true.
@export var guards:Array[Guard] = []
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
for guard in guards:
if not guard.is_satisfied(context_transition, context_state):
return false
return true
func get_supported_trigger_types() -> int:
var supported_trigger_types:int = StateChart.TriggerType.NONE
for guard in guards:
supported_trigger_types |= guard.get_supported_trigger_types()
return supported_trigger_types

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 8 1 C 6.6741569 1 5.4373689 1.3647032 4.3828125 2 L 2 2 C 1.4460018 2 1 2.4460018 1 3 L 1 8 C 1 11.877988 4.1220117 15 8 15 C 11.877988 15 15 11.877988 15 8 L 15 3 C 15 2.4460018 14.553997 2 14 2 L 11.617188 2 C 10.562631 1.3647032 9.3258431 1 8 1 z M 8 3 C 8.6661496 3 9.341059 3.1508527 9.9296875 3.5429688 C 10.518315 3.9350846 11 4.6664108 11 5.5 C 11 6.9485493 9.7463158 7.7230237 9.5 7.8652344 C 9.2154316 8.0295298 8.9412084 8.1318679 8.6484375 8.234375 C 8.6681764 8.2541435 8.6871907 8.2731281 8.7070312 8.2929688 C 8.9070123 8.4929497 9.2811078 8.799985 9.7128906 9.1347656 C 9.9017773 8.6062498 10 8.1127633 10 8 L 12 8 C 12 8.6954536 11.795285 9.5095092 11.384766 10.361328 C 12.044312 10.824409 12.554688 11.167969 12.554688 11.167969 L 11.445312 12.832031 C 11.445313 12.832031 10.925524 12.485639 10.251953 12.013672 C 9.6766003 12.579442 8.9294983 13 8 13 C 6.8333369 13 5.8637218 12.710781 5.1503906 12.175781 C 4.4370594 11.640784 4 10.833331 4 10 C 4 9.2388379 4.3300936 8.5580855 4.765625 8.0820312 C 4.9713648 7.857149 5.1975274 7.6693505 5.4316406 7.5058594 C 5.1796661 7.0623217 5 6.566579 5 6 C 5 5.1666691 5.3858355 4.4501019 5.9179688 3.9179688 C 6.450102 3.3858354 7.1666691 3 8 3 z M 8 5 C 7.8333339 5 7.5498958 5.1141669 7.3320312 5.3320312 C 7.1141669 5.5498958 7 5.8333339 7 6 C 7 6.1264072 7.0667919 6.3402314 7.234375 6.6171875 C 7.3875379 6.5601425 7.53671 6.5060056 7.6738281 6.4570312 C 8.0933833 6.3071794 8.4471687 6.1652679 8.5 6.1347656 C 8.8373346 5.9400052 9 5.958085 9 5.5 C 9 5.3320472 8.9816816 5.3145276 8.8203125 5.2070312 C 8.6589434 5.0995351 8.3338474 5 8 5 z M 6.6777344 9.0839844 C 6.4967446 9.2009263 6.3438607 9.3203258 6.2402344 9.4335938 C 6.0610842 9.6294125 6 9.7611651 6 10 C 6 10.166667 6.0629436 10.359219 6.3496094 10.574219 C 6.6362752 10.789219 7.1666691 11 8 11 C 8.202884 11 8.4004221 10.931957 8.5917969 10.804688 C 8.0988221 10.427669 7.6364251 10.050488 7.2929688 9.7070312 C 7.0901233 9.5041858 6.8823271 9.2968491 6.6777344 9.0839844 z "/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ux4ia8xhhjrx"
path="res://.godot/imported/all_of_guard.svg-49642db22a4a20844b2d39e67c930c8b.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/all_of_guard.svg"
dest_files=["res://.godot/imported/all_of_guard.svg-49642db22a4a20844b2d39e67c930c8b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,63 @@
@tool
@icon("animation_player_state.svg")
class_name AnimationPlayerState
extends AtomicState
## Animation player that this state will use.
@export_node_path("AnimationPlayer") var animation_player: NodePath:
set(value):
animation_player = value
update_configuration_warnings()
## The name of the animation that should be played when this state is entered.
## When this is empty, the name of this state will be used.
@export var animation_name: StringName = ""
## A custom blend time for the animation. The default value of -1.0 will use the
## default blend time of the animation player.
@export var custom_blend: float = -1.0
## A custom speed for the animation. Use negative values to play the animation
## backwards.
@export var custom_speed: float = 1.0
## Whether the animation should be played from the end.
@export var from_end: bool = false
var _animation_player: AnimationPlayer
func _ready():
if Engine.is_editor_hint():
return
super._ready()
_animation_player = get_node_or_null(animation_player)
if not is_instance_valid(_animation_player):
push_error("The animation player is invalid. This node will not work.")
func _state_enter(transition_target:StateChartState):
super._state_enter(transition_target)
if not is_instance_valid(_animation_player):
return
var target_animation = animation_name
if target_animation == "":
target_animation = get_name()
if _animation_player.current_animation == target_animation and _animation_player.is_playing():
return
_animation_player.play(target_animation, custom_blend, custom_speed, from_end)
func _get_configuration_warnings():
var warnings = super._get_configuration_warnings()
warnings.append("This node is deprecated and will be removed in a future version.")
if animation_player.is_empty():
warnings.append("No animation player is set.")
elif get_node_or_null(animation_player) == null:
warnings.append("The animation player path is invalid.")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 8 3 A 5 5 0 0 0 3 8 A 5 5 0 0 0 8 13 A 5 5 0 0 0 13 8 A 5 5 0 0 0 8 3 z M 6 5 L 11 8 L 6 11 L 6 5 z "/></svg>

After

Width:  |  Height:  |  Size: 526 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b3m20gsesp4i0"
path="res://.godot/imported/animation_player_state.svg-1acd03c414690dd7446458c5293935cb.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/animation_player_state.svg"
dest_files=["res://.godot/imported/animation_player_state.svg-1acd03c414690dd7446458c5293935cb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,63 @@
@tool
@icon("animation_tree_state.svg")
class_name AnimationTreeState
extends AtomicState
## Animation tree that this state will use.
@export_node_path("AnimationTree") var animation_tree:NodePath:
set(value):
animation_tree = value
update_configuration_warnings()
## The name of the state that should be activated in the animation tree
## when this state is entered. If this is empty, the name of this state
## will be used.
@export var state_name:StringName = ""
var _animation_tree_state_machine:AnimationNodeStateMachinePlayback
func _ready():
if Engine.is_editor_hint():
return
super._ready()
_animation_tree_state_machine = null
var the_tree = get_node_or_null(animation_tree)
if is_instance_valid(the_tree):
var state_machine = the_tree.get("parameters/playback")
if state_machine is AnimationNodeStateMachinePlayback:
_animation_tree_state_machine = state_machine
else:
push_error("The animation tree does not have a state machine as root node. This node will not work.")
else:
push_error("The animation tree is invalid. This node will not work.")
func _state_enter(transition_target:StateChartState):
super._state_enter(transition_target)
if not is_instance_valid(_animation_tree_state_machine):
return
var target_state = state_name
if target_state == "":
target_state = get_name()
# mirror this state to the animation tree
_animation_tree_state_machine.travel(target_state)
func _get_configuration_warnings():
var warnings = super._get_configuration_warnings()
warnings.append("This node is deprecated and will be removed in a future version.")
if animation_tree.is_empty():
warnings.append("No animation tree is set.")
elif get_node_or_null(animation_tree) == null:
warnings.append("The animation tree path is invalid.")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380051 0 0 1.3380051 0 3 L 0 13 C 0 14.661994 1.3380051 16 3 16 L 13 16 C 14.661994 16 16 14.661994 16 13 L 16 3 C 16 1.3380051 14.661994 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107997 1 15 1.8920033 15 3 L 15 13 C 15 14.107997 14.107997 15 13 15 L 3 15 C 1.8920033 15 1 14.107997 1 13 L 1 3 C 1 1.8920033 1.8920033 1 3 1 z M 3 3 L 3 13 L 4 13 L 4 11 L 5 11 L 5 13 L 11 13 L 11 11 L 12 11 L 12 13 L 13 13 L 13 3 L 12 3 L 12 4 L 11 4 L 11 3 L 5 3 L 5 4 L 4 4 L 4 3 L 3 3 z M 4 5 L 5 5 L 5 7 L 4 7 L 4 5 z M 6 5 L 7 5 L 7 8 L 9 8 L 9 9 L 8 9 L 8 10 L 10 10 L 10 11.013672 L 7 11.013672 L 7 10 L 7 9 L 6 9 L 6 5 z M 11 5 L 12 5 L 12 7 L 11 7 L 11 5 z M 4 8 L 5 8 L 5 10 L 4 10 L 4 8 z M 11 8 L 12 8 L 12 10 L 11 10 L 11 8 z "/></svg>

After

Width:  |  Height:  |  Size: 821 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://3wqyduuj0fq"
path="res://.godot/imported/animation_tree_state.svg-b99077fc178cfa1e23b2c854c7735c4a.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/animation_tree_state.svg"
dest_files=["res://.godot/imported/animation_tree_state.svg-b99077fc178cfa1e23b2c854c7735c4a.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,21 @@
@tool
@icon("any_of_guard.svg")
## A composite guard, that is satisfied if any of the guards are satisfied.
class_name AnyOfGuard
extends Guard
## The guards of which at least one must be satisfied. If empty, this guard is not satisfied.
@export var guards: Array[Guard] = []
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
for guard in guards:
if guard.is_satisfied(context_transition, context_state):
return true
return false
func get_supported_trigger_types() -> int:
var supported_trigger_types:int = StateChart.TriggerType.NONE
for guard in guards:
supported_trigger_types |= guard.get_supported_trigger_types()
return supported_trigger_types

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 8 1 C 6.6741569 1 5.4373689 1.3647032 4.3828125 2 L 2 2 C 1.4460018 2 1 2.4460018 1 3 L 1 8 C 1 11.877988 4.1220117 15 8 15 C 11.877988 15 15 11.877988 15 8 L 15 3 C 15 2.4460018 14.553997 2 14 2 L 11.617188 2 C 10.562631 1.3647032 9.3258431 1 8 1 z M 5 4 L 7 4 L 7 12 L 5 12 L 5 4 z M 9 4 L 11 4 L 11 12 L 9 12 L 9 4 z "/></svg>

After

Width:  |  Height:  |  Size: 418 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dbf5ogymlonu4"
path="res://.godot/imported/any_of_guard.svg-3b1aa026a997dbfebde2cc5993b5c820.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/any_of_guard.svg"
dest_files=["res://.godot/imported/any_of_guard.svg-3b1aa026a997dbfebde2cc5993b5c820.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,25 @@
@tool
@icon("atomic_state.svg")
## This is a state that has no sub-states.
class_name AtomicState
extends StateChartState
func _handle_transition(transition:Transition, source:StateChartState):
# resolve the target state
var target = transition.resolve_target()
if not target is StateChartState:
push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
return
# atomic states cannot transition, so we need to ask the parent
# ask the parent
get_parent()._handle_transition(transition, source)
func _get_configuration_warnings() -> PackedStringArray :
var warnings = super._get_configuration_warnings()
# check if we have any child nodes which are not transitions
for child in get_children():
if child is StateChartState:
warnings.append("Atomic states cannot have child states. These will be ignored.")
break
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 8 3 A 5 5 0 0 0 3 8 A 5 5 0 0 0 8 13 A 5 5 0 0 0 13 8 A 5 5 0 0 0 8 3 z "/></svg>

After

Width:  |  Height:  |  Size: 498 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c4ojtah20jtxc"
path="res://.godot/imported/atomic_state.svg-5ab16e5747cef5b5980c4bf84ef9b1af.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/atomic_state.svg"
dest_files=["res://.godot/imported/atomic_state.svg-5ab16e5747cef5b5980c4bf84ef9b1af.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,269 @@
@tool
@icon("compound_state.svg")
## A compound state is a state that has multiple sub-states of which exactly one can
## be active at any given time.
class_name CompoundState
extends StateChartState
## Called when a child state is entered.
signal child_state_entered()
## Called when a child state is exited.
signal child_state_exited()
## The initial state which should be activated when this state is activated.
@export_node_path("StateChartState") var initial_state:NodePath:
get:
return initial_state
set(value):
initial_state = value
update_configuration_warnings()
## The currently active substate.
var _active_state:StateChartState = null
## The initial state
@onready var _initial_state:StateChartState = get_node_or_null(initial_state)
## The history states of this compound state.
var _history_states:Array[HistoryState] = []
## Whether any of the history states needs a deep history.
var _needs_deep_history:bool = false
func _init() -> void:
# subscribe to the child_entered_tree signal in edit mode so we can
# automatically set the initial state when a new sub-state is added
if Engine.is_editor_hint():
child_entered_tree.connect(
func(child:Node):
# when a child is added in the editor and the child is a state
# and we don't have an initial state yet, set the initial state
# to the newly added child
if child is StateChartState and initial_state.is_empty():
# the newly added node may have a random name now,
# so we need to defer the call to build a node path
# to the next frame, so the editor has time to rename
# the node to its final name
(func(): initial_state = get_path_to(child)).call_deferred()
)
func _state_init():
super._state_init()
# check if we have any history states
for child in get_children():
if child is HistoryState:
var child_as_history_state:HistoryState = child as HistoryState
_history_states.append(child_as_history_state)
# remember if any of the history states needs a deep history
_needs_deep_history = _needs_deep_history or child_as_history_state.deep
# initialize all substates. find all children of type State and call _state_init on them.
for child in get_children():
if child is StateChartState:
var child_as_state:StateChartState = child as StateChartState
child_as_state._state_init()
child_as_state.state_entered.connect(func(): child_state_entered.emit())
child_as_state.state_exited.connect(func(): child_state_exited.emit())
func _state_enter(transition_target:StateChartState):
super._state_enter(transition_target)
# activate the initial state _unless_ one of these are true
# - the transition target is a descendant of this state
# - we already have an active state because entering the state triggered an immediate transition to a child state
# - we are no longer active becasue entering the state triggered an immediate transition to some other state
var target_is_descendant := false
if transition_target != null and is_ancestor_of(transition_target):
target_is_descendant = true
if not target_is_descendant and not is_instance_valid(_active_state) and _state_active:
if _initial_state != null:
if _initial_state is HistoryState:
_restore_history_state(_initial_state)
else:
_active_state = _initial_state
_active_state._state_enter(null)
else:
push_error("No initial state set for state '" + name + "'.")
func _state_step():
super._state_step()
if _active_state != null:
_active_state._state_step()
func _state_save(saved_state:SavedState, child_levels:int = -1):
super._state_save(saved_state, child_levels)
# in addition save all history states, as they are never active and normally would not be saved
var parent = saved_state.get_substate_or_null(self)
if parent == null:
push_error("Probably a bug: The state of '" + name + "' was not saved.")
return
for history_state in _history_states:
history_state._state_save(parent, child_levels)
func _state_restore(saved_state:SavedState, child_levels:int = -1):
super._state_restore(saved_state, child_levels)
# in addition check if we are now active and if so determine the current active state
if active:
# find the currently active child
for child in get_children():
if child is StateChartState and child.active:
_active_state = child
break
func _state_exit():
# if we have any history states, we need to save the current active state
if _history_states.size() > 0:
var saved_state = SavedState.new()
# we save the entire hierarchy if any of the history states needs a deep history
# otherwise we only save this level. This way we can save memory and processing time
_state_save(saved_state, -1 if _needs_deep_history else 1)
# now save the saved state in all history states
for history_state in _history_states:
# when saving history it's ok when we save deep history in a history state that doesn't need it
# because at restore time we will use the state's deep flag to determine if we need to restore
# the entire hierarchy or just this level. This way we don't need multiple copies of the same
# state hierarchy.
history_state.history = saved_state
# deactivate the current state
if _active_state != null:
_active_state._state_exit()
_active_state = null
super._state_exit()
func _process_transitions(trigger_type:StateChart.TriggerType, event:StringName = "") -> bool:
if not active:
return false
# forward to the active state
if is_instance_valid(_active_state):
if _active_state._process_transitions(trigger_type, event):
# emit the event_received signal when the trigger type is event
if trigger_type == StateChart.TriggerType.EVENT:
self.event_received.emit(event)
return true
# if the event was not handled by the active state, we handle it here
# base class will also emit the event_received signal
return super._process_transitions(trigger_type, event)
func _handle_transition(transition:Transition, source:StateChartState):
# print("CompoundState._handle_transition: " + name + " from " + source.name + " to " + str(transition.to))
# resolve the target state
var target = transition.resolve_target()
if not target is StateChartState:
push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
return
# the target state can be
# 0. this state. in this case exit this state and re-enter it. This can happen when
# a child state transfers to its parent state.
# 1. a direct child of this state. this is the easy case in which
# 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.
# 3. no descendant of this state. in this case, we ask our parent state to
# perform the transition
if target == self:
# exit this state and re-enter it
_state_exit()
_state_enter(target)
return
if target in get_children():
# all good, now first deactivate the current state
if is_instance_valid(_active_state):
_active_state._state_exit()
# 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:
_restore_history_state(target)
return
# else, just activate the target state
_active_state = target
_active_state._state_enter(target)
return
if self.is_ancestor_of(target):
# find the child which is the ancestor of the new target.
for child in get_children():
if child is StateChartState and child.is_ancestor_of(target):
# found it.
# change active state if necessary
if _active_state != child:
if is_instance_valid(_active_state):
_active_state._state_exit()
_active_state = child
# give the transition target 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(target)
# ask child to handle the transition
child._handle_transition(transition, source)
return
return
# ask the parent
get_parent()._handle_transition(transition, source)
func _restore_history_state(target:HistoryState):
# 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.")
# 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):
_active_state = default_state
_active_state._state_enter(null)
return
else:
push_error("The default state '" + str(target.default_state) + "' of the history state '" + target.name + "' cannot be found.")
return
func _get_configuration_warnings() -> PackedStringArray:
var warnings = super._get_configuration_warnings()
# count the amount of child states
var child_count = 0
for child in get_children():
if child is StateChartState:
child_count += 1
if child_count < 1:
warnings.append("Compound states should have at least one child state.")
elif child_count < 2:
warnings.append("Compound states with only one child state are not very useful. Consider adding more child states or removing this compound state.")
var the_initial_state = get_node_or_null(initial_state)
if not is_instance_valid(the_initial_state):
warnings.append("Initial state could not be resolved, is the path correct?")
elif the_initial_state.get_parent() != self:
warnings.append("Initial state must be a direct child of this compound state.")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 8 2.5 A 2.5 2.5 0 0 0 5.5 5 A 2.5 2.5 0 0 0 5.9941406 6.4804688 L 4.8476562 8.4726562 A 2.5454545 2.5454545 0 0 0 4.5449219 8.4550781 A 2.5454545 2.5454545 0 0 0 2 11 A 2.5454545 2.5454545 0 0 0 4.5449219 13.544922 A 2.5454545 2.5454545 0 0 0 6.8828125 12 L 9.1953125 12 A 2.5 2.5 0 0 0 11.5 13.544922 A 2.5 2.5 0 0 0 14 11.044922 A 2.5 2.5 0 0 0 11.5 8.5449219 A 2.5 2.5 0 0 0 11.236328 8.5644531 L 10.013672 6.46875 A 2.5 2.5 0 0 0 10.5 5 A 2.5 2.5 0 0 0 8 2.5 z M 8.2890625 7.4785156 L 9.5 9.5546875 A 2.5 2.5 0 0 0 9.2304688 10 L 6.8847656 10 A 2.5454545 2.5454545 0 0 0 6.5800781 9.4707031 L 7.7265625 7.4804688 A 2.5 2.5 0 0 0 8 7.5 A 2.5 2.5 0 0 0 8.2890625 7.4785156 z "/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bbudjoa3ds4qj"
path="res://.godot/imported/compound_state.svg-84780d78ec1f15e1cbb9d20f4df031a7.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/compound_state.svg"
dest_files=["res://.godot/imported/compound_state.svg-84780d78ec1f15e1cbb9d20f4df031a7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,66 @@

// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using System;
using Godot;
/// <summary>
/// Wrapper around the compound state node.
/// </summary>
public class CompoundState : StateChartState
{
/// <summary>
/// Called when a child state is entered.
/// </summary>
public event Action ChildStateEntered
{
add => Wrapped.Connect(SignalName.ChildStateEntered, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.ChildStateEntered, Callable.From(value));
}
/// <summary>
/// Called when a child state is exited.
/// </summary>
public event Action ChildStateExited
{
add => Wrapped.Connect(SignalName.ChildStateExited, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.ChildStateExited, Callable.From(value));
}
private CompoundState(Node wrapped) : base(wrapped)
{
}
/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
/// is actually a compound state. The wrapper object can then be used to interact
/// with the compound state chart from C#.
/// </summary>
/// <param name="state">the node that is the state</param>
/// <returns>a State wrapper.</returns>
/// <throws>ArgumentException if the node is not a state.</throws>
public new static CompoundState Of(Node state)
{
if (state.GetScript().As<Script>() is not GDScript gdScript ||
!gdScript.ResourcePath.EndsWith("compound_state.gd"))
{
throw new ArgumentException("Given node is not a compound state.");
}
return new CompoundState(state);
}
public new class SignalName : StateChartState.SignalName
{
/// <see cref="CompoundState.ChildStateEntered"/>
public static readonly StringName ChildStateEntered = "child_state_entered";
/// <see cref="CompoundState.ChildStateExited"/>
public static readonly StringName ChildStateExited = "child_state_exited";
}
}
}

View file

@ -0,0 +1,49 @@
// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using Godot;
/// <summary>
/// Base class for all wrapper classes. Provides some common functionality. Not to be used directly.
/// </summary>
public abstract class NodeWrapper
{
/// <summary>
/// The wrapped node.
/// </summary>
protected readonly Node Wrapped;
protected NodeWrapper(Node wrapped)
{
Wrapped = wrapped;
}
/// <summary>
/// Allows to connect to signals on the wrapped node.
/// </summary>
/// <param name="signal"></param>
/// <param name="method"></param>
/// <param name="flags"></param>
public Error Connect(StringName signal, Callable method, uint flags = 0u)
{
return Wrapped.Connect(signal, method, flags);
}
/// <summary>
/// Allows to call methods on the wrapped node deferred.
/// </summary>
public Variant CallDeferred(string method, params Variant[] args)
{
return Wrapped.CallDeferred(method, args);
}
/// <summary>
/// Allows to call methods on the wrapped node.
/// </summary>
public Variant Call(string method, params Variant[] args)
{
return Wrapped.Call(method, args);
}
}
}

View file

@ -0,0 +1,129 @@
// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using Godot;
using System;
/// <summary>
/// Wrapper around the GDScript state chart node. Allows interacting with the state chart.
/// </summary>
public class StateChart : NodeWrapper
{
/// <summary>
/// Emitted when the state chart receives an event. This will be
/// emitted no matter which state is currently active and can be
/// useful to trigger additional logic elsewhere in the game
/// without having to create a custom event bus. It is also used
/// by the state chart debugger. Note that this will emit the
/// events in the order in which they are processed, which may
/// be different from the order in which they were received. This is
/// because the state chart will always finish processing one event
/// fully before processing the next. If an event is received
/// while another is still processing, it will be enqueued.
/// </summary>
public event Action<StringName> EventReceived
{
add => Wrapped.Connect(SignalName.EventReceived, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.EventReceived, Callable.From(value));
}
protected StateChart(Node wrapped) : base(wrapped)
{
}
/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
/// is actually a state chart. The wrapper object can then be used to interact
/// with the state chart from C#.
/// </summary>
/// <param name="stateChart">the node that is the state chart</param>
/// <returns>a StateChart wrapper.</returns>
/// <throws>ArgumentException if the node is not a state chart.</throws>
public static StateChart Of(Node stateChart)
{
if (stateChart.GetScript().As<Script>() is not GDScript gdScript
|| !gdScript.ResourcePath.EndsWith("state_chart.gd"))
{
throw new ArgumentException("Given node is not a state chart.");
}
return new StateChart(stateChart);
}
/// <summary>
/// Sends an event to the state chart node.
/// </summary>
/// <param name="eventName">the name of the event to send</param>
public void SendEvent(string eventName)
{
Call(MethodName.SendEvent, eventName);
}
/// <summary>
/// Sets an expression property on the state chart node for later use with expression guards.
/// </summary>
/// <param name="name">the name of the property to set. This is case sensitive.</param>
/// <param name="value">the value to set the property to.</param>
public void SetExpressionProperty(string name, Variant value)
{
Call(MethodName.SetExpressionProperty, name, value);
}
/// <summary>
/// Returns the value of an expression property on the state chart node.
/// </summary>
/// <param name="name">the name of the proeprty to read. This is case sensitive. </param>
/// <param name="defaultValue">the default value to be returned if no such property exists</param>
/// <returns>the value of the property</returns>
public T GetExpressionProperty<[MustBeVariant]T>(string name, T defaultValue = default)
{
return Call(MethodName.GetExpressionProperty, name, Variant.From(defaultValue)).As<T>();
}
/// <summary>
/// Steps the state chart node. This will invoke all <code>state_stepped</code> signals on the
/// currently active states in the state charts. See the "Stepping Mode" section of the manual
/// for more details.
/// </summary>
public void Step()
{
Call(MethodName.Step);
}
public class SignalName : Node.SignalName
{
/// <see cref="StateChart.EventReceived"/>
///
/// </summary>
public static readonly StringName EventReceived = "event_received";
}
public new class MethodName : Node.MethodName
{
/// <summary>
/// Sends an event to the state chart node.
/// </summary>
public static readonly StringName SendEvent = "send_event";
/// <summary>
/// Sets an expression property on the state chart node for later use with expression guards.
/// </summary>
public static readonly StringName SetExpressionProperty = "set_expression_property";
/// <summary>
/// Returns the value of an expression property on the state chart node.
/// </summary>
public static readonly StringName GetExpressionProperty = "get_expression_property";
/// <summary>
/// Steps the state chart node. This will invoke all <code>state_stepped</code> signals on the
/// currently active states in the state charts. See the "Stepping Mode" section of the manual
/// for more details.
/// </summary>
public static readonly StringName Step = "step";
}
}
}

View file

@ -0,0 +1,65 @@
// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using Godot;
using System;
/// <summary>
/// Wrapper around the state chart debugger node.
/// </summary>
public class StateChartDebugger : NodeWrapper
{
private StateChartDebugger(Node wrapped) : base(wrapped) {}
/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
/// is actually a state chart debugger. The wrapper object can then be used to interact
/// with the state chart debugger from C#.
/// </summary>
/// <param name="stateChartDebugger">the node that is the state chart debugger</param>
/// <returns>a StateChartDebugger wrapper.</returns>
/// <throws>ArgumentException if the node is not a state chart debugger.</throws>
public static StateChartDebugger Of(Node stateChartDebugger)
{
if (stateChartDebugger.GetScript().As<Script>() is not GDScript gdScript
|| !gdScript.ResourcePath.EndsWith("state_chart_debugger.gd"))
{
throw new ArgumentException("Given node is not a state chart debugger.");
}
return new StateChartDebugger(stateChartDebugger);
}
/// <summary>
/// Sets the node that the state chart debugger should debug.
/// </summary>
/// <param name="node">the the node that should be debugged. Can be a state chart or any
/// node above a state chart. The debugger will automatically pick the first state chart
/// node below the given one.</param>
public void DebugNode(Node node)
{
Call(MethodName.DebugNode, node);
}
/// <summary>
/// Adds a history entry to the history output.
/// </summary>
/// <param name="text">the text to add</param>
public void AddHistoryEntry(string text)
{
Call(MethodName.AddHistoryEntry, text);
}
public new class MethodName : Node.MethodName
{
/// <summary>
/// Sets the node that the state chart debugger should debug.
/// </summary>
public static readonly string DebugNode = "debug_node";
/// <summary>
/// Adds a history entry to the history output.
/// </summary>
public static readonly string AddHistoryEntry = "add_history_entry";
}
}
}

View file

@ -0,0 +1,156 @@
// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using Godot;
using System;
/// <summary>
/// A wrapper around the state node that allows interacting with it from C#.
/// </summary>
public class StateChartState : NodeWrapper
{
/// <summary>
/// Called when the state is entered.
/// </summary>
public event Action StateEntered
{
add => Wrapped.Connect(SignalName.StateEntered, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateEntered, Callable.From(value));
}
/// <summary>
/// Called when the state is exited.
/// </summary>
public event Action StateExited
{
add => Wrapped.Connect(SignalName.StateExited, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateExited, Callable.From(value));
}
/// <summary>
/// Called when the state receives an event. Only called if the state is active.
/// </summary>
public event Action<StringName> EventReceived
{
add => Wrapped.Connect(SignalName.EventReceived, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.EventReceived, Callable.From(value));
}
/// <summary>
/// Called when the state is processing.
/// </summary>
public event Action<float> StateProcessing
{
add => Wrapped.Connect(SignalName.StateProcessing, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateProcessing, Callable.From(value));
}
/// <summary>
/// Called when the state is physics processing.
/// </summary>
public event Action<float> StatePhysicsProcessing
{
add => Wrapped.Connect(SignalName.StatePhysicsProcessing, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StatePhysicsProcessing, Callable.From(value));
}
/// <summary>
/// Called when the state chart <code>Step</code> function is called.
/// </summary>
public event Action StateStepped
{
add => Wrapped.Connect(SignalName.StateStepped, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateStepped, Callable.From(value));
}
/// <summary>
/// Called when the state is receiving input.
/// </summary>
public event Action<InputEvent> StateInput
{
add => Wrapped.Connect(SignalName.StateInput, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateInput, Callable.From(value));
}
/// <summary>
/// Called when the state is receiving unhandled input.
/// </summary>
public event Action<InputEvent> StateUnhandledInput
{
add => Wrapped.Connect(SignalName.StateUnhandledInput, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.StateUnhandledInput, Callable.From(value));
}
/// <summary>
/// Called every frame while a delayed transition is pending for this state.
/// Returns the initial delay and the remaining delay of the transition.
/// </summary>
public event Action<float,float> TransitionPending
{
add => Wrapped.Connect(SignalName.TransitionPending, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.TransitionPending, Callable.From(value));
}
protected StateChartState(Node wrapped) : base(wrapped) {}
/// <summary>
/// Creates a wrapper object around the given node and verifies that the node
/// is actually a state. The wrapper object can then be used to interact
/// with the state chart from C#.
/// </summary>
/// <param name="state">the node that is the state</param>
/// <returns>a State wrapper.</returns>
/// <throws>ArgumentException if the node is not a state.</throws>
public static StateChartState Of(Node state)
{
if (state.GetScript().As<Script>() is not GDScript gdScript ||
!gdScript.ResourcePath.EndsWith("state.gd"))
{
throw new ArgumentException("Given node is not a state.");
}
return new StateChartState(state);
}
/// <summary>
/// Returns true if this state is currently active.
/// </summary>
public bool Active => Wrapped.Get("active").As<bool>();
public class SignalName : Godot.Node.SignalName
{
/// <see cref="StateChartState.StateEntered"/>
public static readonly StringName StateEntered = "state_entered";
/// <see cref="StateChartState.StateExited"/>
public static readonly StringName StateExited = "state_exited";
/// <see cref="StateChartState.EventReceived"/>
public static readonly StringName EventReceived = "event_received";
/// <see cref="StateChartState.StateProcessing"/>
public static readonly StringName StateProcessing = "state_processing";
/// <see cref="StateChartState.StatePhysicsProcessing"/>
public static readonly StringName StatePhysicsProcessing = "state_physics_processing";
/// <see cref="StateChartState.StateStepped"/>
public static readonly StringName StateStepped = "state_stepped";
/// <see cref="StateChartState.StateInput"/>
public static readonly StringName StateInput = "state_input";
/// <see cref="StateChartState.StateUnhandledInput"/>
public static readonly StringName StateUnhandledInput = "state_unhandled_input";
/// <see cref="StateChartState.TransitionPending"/>
public static readonly StringName TransitionPending = "transition_pending";
}
}
}

View file

@ -0,0 +1,53 @@
using System;
namespace GodotStateCharts
{
using Godot;
/// <summary>
/// A transition between two states.
/// </summary>
public class Transition : NodeWrapper {
/// <summary>
/// Called when the transition is taken.
/// </summary>
public event Action Taken {
add => Wrapped.Connect(SignalName.Taken, Callable.From(value));
remove => Wrapped.Disconnect(SignalName.Taken, Callable.From(value));
}
private Transition(Node transition) : base(transition) {}
public static Transition Of(Node transition) {
if (transition.GetScript().As<Script>() is not GDScript gdScript
|| !gdScript.ResourcePath.EndsWith("transition.gd"))
{
throw new ArgumentException("Given node is not a transition.");
}
return new Transition(transition);
}
/// <summary>
/// Takes the transition. The transition will be taken immediately by
/// default, even if it has a delay. If you want to wait for the delay
/// to pass, you can set the immediately parameter to false.
/// </summary>
public void Take(bool immediately = true) {
Call(MethodName.Take, immediately);
}
public class SignalName : Godot.Node.SignalName
{
/// <see cref="Transition.Taken"/>
public static readonly StringName Taken = "taken";
}
public class MethodName : Godot.Node.MethodName
{
/// <see cref="Transition.Take"/>
public static readonly StringName Take = "take";
}
}
}

View file

@ -0,0 +1,8 @@
## Returns the path of a node in the scene tree
## Returns the name of the node if the node is not in the tree.
static func path_of(node: Node) -> String:
if node == null:
return ""
if !node.is_inside_tree():
return node.name + " (not in tree)"
return str(node.get_path())

View file

@ -0,0 +1,39 @@
@tool
@icon("expression_guard.svg")
class_name ExpressionGuard
extends Guard
const ExpressionUtil = preload("expression_util.gd")
const DebugUtil = preload("debug_util.gd")
var expression:String = ""
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
var root:StateChart = context_state._chart
if not is_instance_valid(root):
push_error("Could not find root state chart node, cannot evaluate expression")
return false
var result:Variant = ExpressionUtil.evaluate_expression("guard in " + DebugUtil.path_of(context_transition), root, expression, false)
if typeof(result) != TYPE_BOOL:
push_error("Expression: ", expression ," result: ", result, " is not a boolean. Returning false.")
return false
return result
func get_supported_trigger_types() -> int:
return StateChart.TriggerType.PROPERTY_CHANGE
func _get_property_list() -> Array[Dictionary]:
var properties:Array[Dictionary] = []
properties.append({
"name": "expression",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_EXPRESSION
})
return properties

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 8 1 C 6.6741556 1 5.43737 1.3647026 4.3828125 2 L 2 2 C 1.4460012 2 1 2.4460012 1 3 L 1 8 C 1 11.877992 4.1220078 15 8 15 C 11.877992 15 15 11.877992 15 8 L 15 3 C 15 2.4460012 14.553998 2 14 2 L 11.617188 2 C 10.56263 1.3647026 9.3258444 1 8 1 z M 9 3 C 9.9835664 3 10.737359 3.3452173 11.203125 3.7226562 C 11.668891 4.1000952 11.90625 4.5761719 11.90625 4.5761719 L 10.09375 5.4238281 C 10.09375 5.4238281 10.096553 5.3999033 9.9453125 5.2773438 C 9.7940722 5.1547842 9.548398 5 9 5 C 9 5 8.708751 5.0137887 8.4472656 5.1445312 C 8.1857804 5.275274 8 5.3333354 8 6 L 8 7 L 10 7 L 10 9 L 8 9 L 8 10 C 8 10.833331 7.6389227 11.469349 7.3320312 11.929688 C 7.02514 12.390024 6.7070312 12.707031 6.7070312 12.707031 L 5.2929688 11.292969 C 5.2929687 11.292969 5.4748616 11.109973 5.6679688 10.820312 C 5.8610759 10.530652 6 10.166667 6 10 L 6 6 C 6 4.6666706 6.8142226 3.7247245 7.5527344 3.3554688 C 8.291246 2.9862128 9 3 9 3 z "/></svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://t4jcjthwq04d"
path="res://.godot/imported/expression_guard.svg-e0dc5b3c566ccd2411887df3fe1bbb2b.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/expression_guard.svg"
dest_files=["res://.godot/imported/expression_guard.svg-e0dc5b3c566ccd2411887df3fe1bbb2b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,22 @@
static func evaluate_expression(context:String, state_chart: StateChart, expression: String, default_value:Variant) -> Variant:
var the_expression := Expression.new()
var input_names = state_chart._expression_properties.keys()
var parse_result:int = the_expression.parse(expression, input_names)
if parse_result != OK:
push_error("(" + context + ") Expression parse error : " + the_expression.get_error_text() + " for expression " + expression)
return default_value
# input values need to be in the same order as the input names, so we build an array
# of values
var input_values:Array = []
for input_name in input_names:
input_values.append(state_chart._expression_properties[input_name])
var result = the_expression.execute(input_values, null, false)
if the_expression.has_execute_failed():
push_error("(" + context + ") Expression execute error: " + the_expression.get_error_text() + " for expression: " + expression)
return default_value
return result

View file

@ -0,0 +1,127 @@
@tool
extends EditorPlugin
## The sidebar control for 2D
var _ui_sidebar_canvas:Control
## The sidebar control for 3D
var _ui_sidebar_spatial:Control
## Scene holding the sidebar
var _sidebar_ui:PackedScene = preload("utilities/editor_sidebar.tscn")
var _debugger_plugin:EditorDebuggerPlugin
var _inspector_plugin:EditorInspectorPlugin
enum SidebarLocation {
LEFT = 1,
RIGHT = 2
}
## The current location of the sidebar. Default is left.
var _current_sidebar_location:SidebarLocation = SidebarLocation.LEFT
func _enter_tree():
# prepare a copy of the sidebar for both 2D and 3D.
_ui_sidebar_canvas = _sidebar_ui.instantiate()
_ui_sidebar_canvas.sidebar_toggle_requested.connect(_toggle_sidebar)
_ui_sidebar_canvas.hide()
_ui_sidebar_spatial = _sidebar_ui.instantiate()
_ui_sidebar_spatial.sidebar_toggle_requested.connect(_toggle_sidebar)
_ui_sidebar_spatial.hide()
# and add it to the right place in the editor ui
_add_sidebars()
# get notified when selection changes so we can
# update the sidebar contents accordingly
get_editor_interface().get_selection().selection_changed.connect(_on_selection_changed)
# Add the debugger plugin
_debugger_plugin = preload("utilities/editor_debugger/editor_debugger_plugin.gd").new()
_debugger_plugin.initialize(get_editor_interface().get_editor_settings())
add_debugger_plugin(_debugger_plugin)
# add the inspector plugin for events
_inspector_plugin = preload("utilities/event_editor/event_inspector_plugin.gd").new()
add_inspector_plugin(_inspector_plugin)
func _set_window_layout(configuration):
_remove_sidebars()
_current_sidebar_location = configuration.get_value("GodotStateCharts", "sidebar_location", SidebarLocation.LEFT)
_add_sidebars()
func _get_window_layout(configuration):
configuration.set_value("GodotStateCharts", "sidebar_location", _current_sidebar_location)
func _toggle_sidebar():
_remove_sidebars()
_current_sidebar_location = SidebarLocation.RIGHT if _current_sidebar_location == SidebarLocation.LEFT else SidebarLocation.LEFT
_add_sidebars()
queue_save_layout()
func _add_sidebars():
if _current_sidebar_location == SidebarLocation.LEFT:
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, _ui_sidebar_spatial)
add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_LEFT, _ui_sidebar_canvas)
else:
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _ui_sidebar_spatial)
add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_RIGHT, _ui_sidebar_canvas)
func _remove_sidebars():
if _current_sidebar_location == SidebarLocation.LEFT:
remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_LEFT,_ui_sidebar_canvas)
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, _ui_sidebar_spatial)
else:
remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_SIDE_RIGHT,_ui_sidebar_canvas)
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_RIGHT, _ui_sidebar_spatial)
func _ready():
# inititalize the side bars
_ui_sidebar_canvas.setup(get_editor_interface(), get_undo_redo())
_ui_sidebar_spatial.setup(get_editor_interface(), get_undo_redo())
_inspector_plugin.setup(get_undo_redo())
func _exit_tree():
# remove the debugger plugin
remove_debugger_plugin(_debugger_plugin)
# remove the inspector plugin
remove_inspector_plugin(_inspector_plugin)
# remove the side bars
_remove_sidebars()
if is_instance_valid(_ui_sidebar_canvas):
_ui_sidebar_canvas.queue_free()
if is_instance_valid(_ui_sidebar_spatial):
_ui_sidebar_spatial.queue_free()
func _on_selection_changed() -> void:
# get the current selection
var selection = get_editor_interface().get_selection().get_selected_nodes()
# show sidebar if we selected a chart or a state
if selection.size() == 1:
var selected_node = selection[0]
if selected_node is StateChart \
or selected_node is StateChartState \
or selected_node is Transition:
_ui_sidebar_canvas.show()
_ui_sidebar_canvas.change_selected_node(selected_node)
_ui_sidebar_spatial.show()
_ui_sidebar_spatial.change_selected_node(selected_node)
return
# otherwise hide it
_ui_sidebar_canvas.hide()
_ui_sidebar_spatial.hide()

View file

@ -0,0 +1,13 @@
@icon("guard.svg")
class_name Guard
extends Resource
## Returns true if the guard is satisfied, false otherwise.
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
push_error("Guard.is_satisfied() is not implemented. Did you forget to override it?")
return false
## Returns the triggers which should trigger the guard's evaluation. This is a bit mask of [StateChart.TriggerType].
func get_supported_trigger_types() -> int:
push_error("Guard._get_supported_trigger_types() is not implemented. Did you forget to override it?")
return StateChart.TriggerType.NONE

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#a2703a" d="M 8 1 C 6.6741582 1 5.4373678 1.3647038 4.3828125 2 L 2 2 C 1.4460024 2 1 2.4460024 1 3 L 1 8 C 1 11.877984 4.1220156 15 8 15 C 11.877984 15 15 11.877984 15 8 L 15 3 C 15 2.4460024 14.553996 2 14 2 L 11.617188 2 C 10.562632 1.3647038 9.3258418 1 8 1 z "/></svg>

After

Width:  |  Height:  |  Size: 348 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://hupxr7umhvwh"
path="res://.godot/imported/guard.svg-d0ead7a3baf32c0a7719fb3b73645353.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/guard.svg"
dest_files=["res://.godot/imported/guard.svg-d0ead7a3baf32c0a7719fb3b73645353.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,58 @@
@tool
@icon("history_state.svg")
class_name HistoryState
extends StateChartState
## Whether this state is a deep history state. A deep history state
## will remember all nested states, while a shallow history state will
## only remember the last active state of the parent state.
@export var deep:bool = false
## The default state to transition to if no history is available.
@export_node_path("StateChartState") var default_state:NodePath:
set(value):
default_state = value
update_configuration_warnings()
## The stored history, if any.
var history:SavedState = null
func _state_save(saved_state:SavedState, _child_levels:int = -1) -> void:
# History states are pseudo states, so they only save remembered history if any
var our_state := SavedState.new()
our_state.history = history
saved_state.add_substate(self, our_state)
func _state_restore(saved_state:SavedState, _child_levels:int = -1) -> void:
# History states are pseudo states, so they only restore remembered history if any
var our_state := saved_state.get_substate_or_null(self)
if our_state != null:
history = our_state.history
func _get_configuration_warnings() -> PackedStringArray:
var warnings := super._get_configuration_warnings()
# a history state must be a child of a compound state otherwise it is useless
var parent_state := get_parent()
if not parent_state is CompoundState:
warnings.append("A history state must be a child of a compound state.")
# the default state must be a state
var default_state_node := get_node_or_null(default_state)
if not default_state_node is StateChartState:
warnings.append("The default state is not set or is not a state.")
else:
# the default state must be a child of the parent state
if not get_parent().is_ancestor_of(default_state_node):
warnings.append("The default state must be a child of the parent state.")
# a history state must not have any children
if get_child_count() > 0:
warnings.append("History states cannot have child nodes.")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380051 0 0 1.3380051 0 3 L 0 5 L 1 5 L 1 3 C 1 1.8920033 1.8920033 1 3 1 L 13 1 C 14.107997 1 15 1.8920033 15 3 L 15 13 C 15 14.107997 14.107997 15 13 15 L 3 15 C 1.8920033 15 1 14.107997 1 13 L 1 10 L 0 10 L 0 13 C 0 14.661994 1.3380051 16 3 16 L 13 16 C 14.661994 16 16 14.661994 16 13 L 16 3 C 16 1.3380051 14.661994 0 13 0 L 3 0 z M 8 3 A 5 5 0 0 0 3.0742188 7.1816406 L 1.5546875 6.1679688 L 0.4453125 7.8320312 L 3.4453125 9.8320312 A 1.0001 1.0001 0 0 0 4.5546875 9.8320312 L 7.5546875 7.8320312 L 6.4453125 6.1679688 L 5.1699219 7.0195312 A 3 3 0 0 1 8 5 A 3 3 0 0 1 11 8 A 3 3 0 0 1 8 11 A 3 3 0 0 1 5.8867188 10.113281 L 4.4726562 11.527344 A 5 5 0 0 0 8 13 A 5 5 0 0 0 13 8 A 5 5 0 0 0 8 3 z "/></svg>

After

Width:  |  Height:  |  Size: 810 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bkf1e240ouleb"
path="res://.godot/imported/history_state.svg-7ed355ddc4d844fa3139e70c23187edd.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/history_state.svg"
dest_files=["res://.godot/imported/history_state.svg-7ed355ddc4d844fa3139e70c23187edd.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,18 @@
@tool
@icon("not_guard.svg")
## A guard which is satisfied when the given guard is not satisfied.
class_name NotGuard
extends Guard
## The guard that should not be satisfied. When null, this guard is always satisfied.
@export var guard: Guard
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
if guard == null:
return true
return not guard.is_satisfied(context_transition, context_state)
func get_supported_trigger_types() -> int:
if guard == null:
return StateChart.TriggerType.NONE
return guard.get_supported_trigger_types()

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 8 1 C 6.6741566 1 5.4373693 1.3647032 4.3828125 2 L 2 2 C 1.4460022 2 1 2.4460022 1 3 L 1 8 C 1 11.877988 4.1220118 15 8 15 C 11.877988 15 15 11.877988 15 8 L 15 3 C 15 2.4460022 14.553997 2 14 2 L 11.617188 2 C 10.562631 1.3647032 9.3258431 1 8 1 z M 7 3 L 9 3 L 9 10 L 7 10 L 7 3 z M 8 11 A 1 1 0 0 1 9 12 A 1 1 0 0 1 8 13 A 1 1 0 0 1 7 12 A 1 1 0 0 1 8 11 z "/></svg>

After

Width:  |  Height:  |  Size: 459 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnjw3kjyx1gbb"
path="res://.godot/imported/not_guard.svg-b2d127d6ec93eb4ce2d86c4aadb7bbfe.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/not_guard.svg"
dest_files=["res://.godot/imported/not_guard.svg-b2d127d6ec93eb4ce2d86c4aadb7bbfe.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,116 @@
@tool
@icon("parallel_state.svg")
## A parallel state is a state which can have sub-states, all of which are active
## when the parallel state is active.
class_name ParallelState
extends StateChartState
# all children of the state
var _sub_states:Array[StateChartState] = []
func _state_init():
super._state_init()
# find all children of this state which are states
for child in get_children():
if child is StateChartState:
_sub_states.append(child)
child._state_init()
# since there is no state transitions between parallel states, we don't need to
# subscribe to events from our children
func _handle_transition(transition:Transition, source:StateChartState):
# resolve the target state
var target = transition.resolve_target()
if not target is StateChartState:
push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
return
# the target state can be
# 0. this state. in this case just activate the state and all its children.
# this can happen when a child state transfers back to its parent state.
# 1. a direct child of this state. this is the easy case in which
# we will do nothing, because our direct children are always active.
# 2. a descendant of this state. in this case we find the direct child which
# is the ancestor of the target state and then ask it to perform
# the transition.
# 3. no descendant of this state. in this case, we ask our parent state to
# perform the transition
if target == self:
# exit this state
_state_exit()
# then re-enter it
_state_enter(target)
return
if target in get_children():
# all good, nothing to do.
return
if self.is_ancestor_of(target):
# find the child which is the ancestor of the new target.
for child in get_children():
if child is StateChartState and child.is_ancestor_of(target):
# ask child to handle the transition
child._handle_transition(transition, source)
return
return
# ask the parent
get_parent()._handle_transition(transition, source)
func _state_enter(transition_target:StateChartState):
super._state_enter(transition_target)
# enter all children
for child in _sub_states:
child._state_enter(transition_target)
func _state_exit():
# exit all children
for child in _sub_states:
child._state_exit()
super._state_exit()
func _state_step():
super._state_step()
for child in _sub_states:
child._state_step()
func _process_transitions(trigger_type:StateChart.TriggerType, event:StringName = "") -> bool:
if not active:
return false
# forward to all children
var handled := false
for child in _sub_states:
var child_handled_it = child._process_transitions(trigger_type, event)
handled = handled or child_handled_it
# if any child handled this, we don't touch it anymore
if handled:
# emit the event_received signal for completeness
# if the trigger type is event
if trigger_type == StateChart.TriggerType.EVENT:
self.event_received.emit(event)
return true
# otherwise handle it ourselves
# defer to the base class
return super._process_transitions(trigger_type, event)
func _get_configuration_warnings() -> PackedStringArray:
var warnings = super._get_configuration_warnings()
var child_count = 0
for child in get_children():
if child is StateChartState:
child_count += 1
if child_count < 2:
warnings.append("Parallel states should have at least two child states.")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 4 L 1 4 L 1 3 C 1 1.8920033 1.8920033 1 3 1 L 13 1 C 14.107997 1 15 1.8920033 15 3 L 15 4 L 16 4 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 7 2 L 7 14 L 9 14 L 9 2 L 7 2 z M 3 5 A 3 3 0 0 0 0 8 A 3 3 0 0 0 3 11 A 3 3 0 0 0 6 8 A 3 3 0 0 0 3 5 z M 13 5 A 3 3 0 0 0 10 8 A 3 3 0 0 0 13 11 A 3 3 0 0 0 16 8 A 3 3 0 0 0 13 5 z M 0 12 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 12 L 15 12 L 15 13 C 15 14.107997 14.107997 15 13 15 L 3 15 C 1.8920033 15 1 14.107997 1 13 L 1 12 L 0 12 z "/></svg>

After

Width:  |  Height:  |  Size: 666 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dsa1nco51br8d"
path="res://.godot/imported/parallel_state.svg-33f40e94bafae79f072d67563e0adcd3.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/parallel_state.svg"
dest_files=["res://.godot/imported/parallel_state.svg-33f40e94bafae79f072d67563e0adcd3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,7 @@
[plugin]
name="Godot State Charts"
description="A simple, yet powerful state charts library for Godot"
author="Jan Thomä & Contributors"
version="0.21.3"
script="godot_state_charts.gd"

View file

@ -0,0 +1,31 @@
## This represents the saved state of a state chart (or a part of it).
## It is used to save the state of a state chart to a file and to restore it later.
## It is also used in History states.
class_name SavedState
extends Resource
## The saved states of any active child states
## Key is the name of the child state, value is the SavedState of the child state
@export var child_states: Dictionary = {}
## The path to the currently pending transition, if any
@export var pending_transition_name: NodePath
## The remaining time of the active transition, if any
@export var pending_transition_remaining_delay: float = 0
## The initial time of the active transition, if any
@export var pending_transition_initial_delay: float = 0
## History of the state, if this state is a history state, otherwise null
@export var history:SavedState = null
## Adds the given substate to this saved state
func add_substate(state:StateChartState, saved_state:SavedState):
child_states[state.name] = saved_state
## Returns the saved state of the given substate, or null if it does not exist
func get_substate_or_null(state:StateChartState) -> SavedState:
return child_states.get(state.name)

View file

@ -0,0 +1,310 @@
@icon("state_chart.svg")
@tool
## This is statechart. It contains a root state (commonly a compound or parallel state) and is the entry point for
## the state machine.
class_name StateChart
extends Node
## The the remote debugger
const DebuggerRemote = preload("utilities/editor_debugger/editor_debugger_remote.gd")
## The state chart utility class.
const StateChartUtil = preload("utilities/state_chart_util.gd")
## Emitted when the state chart receives an event. This will be
## emitted no matter which state is currently active and can be
## useful to trigger additional logic elsewhere in the game
## without having to create a custom event bus. It is also used
## by the state chart debugger. Note that this will emit the
## events in the order in which they are processed, which may
## be different from the order in which they were received. This is
## because the state chart will always finish processing one event
## fully before processing the next. If an event is received
## while another is still processing, it will be enqueued.
signal event_received(event:StringName)
@export_group("Debugging")
## Flag indicating if this state chart should be tracked by the
## state chart debugger in the editor.
@export var track_in_editor:bool = false
## If set, the state chart will issue a warning when trying to
## send an event that is not configured for any transition of
## the state chart. It is usually a good idea to leave this
## enabled, but in certain cases this may get in the way so
## you can disable it here.
@export var warn_on_sending_unknown_events:bool = true
@export_group("")
## Initial values for the expression properties. These properties can be used in expressions, e.g
## for guards or transition delays. It is recommended to set an initial value for each property
## you use in an expression to ensure that this expression is always valid. If you don't set
## an initial value, some expressions may fail to be evaluated if they use a property that has
## not been set yet.
@export var initial_expression_properties:Dictionary = {}
## The root state of the state chart.
var _state:StateChartState = null
## This dictonary contains known properties used in expression guards. Use the
## [method set_expression_property] to add properties to this dictionary.
var _expression_properties:Dictionary = {
}
## A list of pending events
var _queued_events:Array[StringName] = []
## Whether or not a property change is pending.
var _property_change_pending:bool = false
## Whether or not a state change occured during processing and we need to re-run
## automatic transitions that may have been triggered by the state change.
var _state_change_pending:bool = false
## Flag indicating if the state chart is currently processing.
## Until a change is fully processed, no further changes can
## be introduced from the outside.
var _locked_down:bool = false
var _queued_transitions:Array[Dictionary] = []
var _transitions_processing_active:bool = false
var _debugger_remote:DebuggerRemote = null
var _valid_event_names:Array[StringName] = []
## A trigger type that defines events that can trigger a transition.
enum TriggerType {
## No trigger type. This usually should not happen and is used as a default value.
NONE = 0,
## The transition will be triggered by an event.
EVENT = 1,
## The transition is automatic and thus will be triggered when the state is entered.
STATE_ENTER = 2,
## The transition is automatic and will be triggered by a property change.
PROPERTY_CHANGE = 4,
## The transition is automatic and will be triggered by a state change.
STATE_CHANGE = 8,
}
func _ready() -> void:
if Engine.is_editor_hint():
return
# check if we have exactly one child that is a state
if get_child_count() != 1:
push_error("StateChart must have exactly one child")
return
# check if the child is a state
var child:Node = get_child(0)
if not child is StateChartState:
push_error("StateMachine's child must be a State")
return
# in debug builds, collect a list of valid event names
# to warn the developer when using an event that doesn't
# exist.
if OS.is_debug_build():
_valid_event_names = StateChartUtil.events_of(self)
# set the initial expression properties
if initial_expression_properties != null:
for key in initial_expression_properties.keys():
if not key is String and not key is StringName:
push_error("Expression property names must be strings. Ignoring initial expression property with key ", key)
continue
_expression_properties[key] = initial_expression_properties[key]
# initialize the state machine
_state = child as StateChartState
_state._state_init()
# We wait one frame before entering initial state, so
# parents of the state chart have a chance to run their
# _ready methods first and not get events from the state
# chart while they have not yet been initialized
_enter_initial_state.call_deferred()
# if we are in an editor build and this chart should be tracked
# by the debugger, create a debugger remote
if track_in_editor and OS.has_feature("editor") and not Engine.is_editor_hint():
_debugger_remote = DebuggerRemote.new(self)
# add the remote as a child, so it gets cleaned up when the state
# chart is deleted
add_child(_debugger_remote)
func _enter_initial_state():
# https://github.com/derkork/godot-statecharts/issues/143
# make sure that transitions resulting from state_enter handlers still
# adhere our transactional processing
_transitions_processing_active = true
_locked_down = true
# enter the state
_state._state_enter(null)
# run any queued transitions that may have come up during the enter
_run_queued_transitions()
# run any queued external events that may have come up during the enter
_run_changes()
## Sends an event to this state chart. The event will be passed to the innermost active state first and
## is then moving up in the tree until it is consumed. Events will trigger transitions and actions via emitted
## signals. There is no guarantee when the event will be processed. The state chart
## will process the event as soon as possible but there is no guarantee that the
## event will be fully processed when this method returns.
func send_event(event:StringName) -> void:
if not is_node_ready():
push_error("State chart is not yet ready. If you call `send_event` in _ready, please call it deferred, e.g. `state_chart.send_event.call_deferred(\"my_event\").")
return
if not is_instance_valid(_state):
push_error("State chart has no root state. Ignoring call to `send_event`.")
return
if warn_on_sending_unknown_events and event != "" and OS.is_debug_build() and not _valid_event_names.has(event):
push_warning("State chart does not have an event '", event , "' defined. Sending this event will do nothing.")
_queued_events.append(event)
if _locked_down:
return
_run_changes()
## Sets a property that can be used in expression guards. The property will be available as a global variable
## with the same name. E.g. if you set the property "foo" to 42, you can use the expression "foo == 42" in
## an expression guard.
func set_expression_property(name:StringName, value) -> void:
if not is_node_ready():
push_error("State chart is not yet ready. If you call `set_expression_property` in `_ready`, please call it deferred, e.g. `state_chart.set_expression_property.call_deferred(\"my_property\", 5).")
return
if not is_instance_valid(_state):
push_error("State chart has no root state. Ignoring call to `set_expression_property`.")
return
_expression_properties[name] = value
_property_change_pending = true
if not _locked_down:
_run_changes()
## Returns the value of a previously set expression property. If the property does not exist, the default value
## will be returned.
func get_expression_property(name:StringName, default:Variant = null) -> Variant:
return _expression_properties.get(name, default)
func _run_changes() -> void:
# enable the reentrance lock
_locked_down = true
while (not _queued_events.is_empty()) or _property_change_pending or _state_change_pending:
# We process stuff in this order:
# 1. State changes
if _state_change_pending:
_state_change_pending = false
_state._process_transitions(TriggerType.STATE_CHANGE)
# 2. Property changes
if _property_change_pending:
_property_change_pending = false
_state._process_transitions(TriggerType.PROPERTY_CHANGE)
# 3. Events
if not _queued_events.is_empty():
# process the next event
var next_event = _queued_events.pop_front()
event_received.emit(next_event)
_state._process_transitions(TriggerType.EVENT, next_event)
_locked_down = false
## Allows states to queue a transition for running. This will eventually run the transition
## once all currently running transitions have finished. States should call this method
## when they want to transition away from themselves.
func _run_transition(transition:Transition, source:StateChartState) -> void:
# Queue up the transition for running
_queued_transitions.append({transition : source})
# if we are currently inside of a transition, finish processing the queue so we
# get a predictable order. Queing can happen a state has an automatic transition on enter,
# or when a transition is triggered as part of a signal handler. In these cases, we want to
# finish the current transition before starting a new one because otherwise the transitions
# see really unpredictable state changes. In a sense, every transition is also a
# transaction that needs to be fully processed before the next one can start.
if _transitions_processing_active:
return
_run_queued_transitions()
## Runs all queued transitions until none are left. This also checks for infinite loops in transitions and
## ensures triggering guards on state changes.
func _run_queued_transitions() -> void:
_transitions_processing_active = true
var execution_count := 1
# if we still have transitions
while _queued_transitions.size() > 0:
var next_transition_entry = _queued_transitions.pop_front()
var next_transition = next_transition_entry.keys()[0]
var next_transition_source = next_transition_entry[next_transition]
_do_run_transition(next_transition, next_transition_source)
execution_count += 1
if execution_count > 100:
push_error("Infinite loop detected in transitions. Aborting. The state chart is now in an invalid state and no longer usable.")
break
_transitions_processing_active = false
# transitions trigger a state change which can in turn activate
# other transitions, so we need to handle these
if not _locked_down:
_run_changes()
## Runs the transition. Used internally by the state chart, do not call this directly.
func _do_run_transition(transition:Transition, source:StateChartState):
if source.active:
# Notify interested parties that the transition is about to be taken
transition.taken.emit()
source._handle_transition(transition, source)
_state_change_pending = true
else:
_warn_not_active(transition, source)
func _warn_not_active(transition:Transition, source:StateChartState):
push_warning("Ignoring request for transitioning from ", source.name, " to ", transition.to, " as the source state is no longer active. Check whether your trigger multiple state changes within a single frame.")
## Calls the `step` function in all active states. Used for situations where `state_processing` and
## `state_physics_processing` don't make sense (e.g. turn-based games, or games with a fixed timestep).
func step() -> void:
if not is_node_ready():
push_error("State chart is not yet ready. If you call `step` in `_ready`, please call it deferred, e.g. `state_chart.step.call_deferred()`.")
return
if not is_instance_valid(_state):
push_error("State chart has no root state. Ignoring call to `step`.")
return
_state._state_step()
func _get_configuration_warnings() -> PackedStringArray:
var warnings:PackedStringArray = []
if get_child_count() != 1:
warnings.append("StateChart must have exactly one child")
else:
var child:Node = get_child(0)
if not child is StateChartState:
warnings.append("StateChart's child must be a State")
return warnings

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 4 3 C 3.4460006 3 3 3.4460006 3 4 L 3 7 C 3 7.5539994 3.4460006 8 4 8 L 6 8 L 6 12 C 6 12.553999 6.4460006 13 7 13 L 12 13 C 12.553999 13 13 12.553999 13 12 L 13 7 C 13 6.4460006 12.553999 6 12 6 L 8 6 L 8 4 C 8 3.4460006 7.5539994 3 7 3 L 4 3 z "/></svg>

After

Width:  |  Height:  |  Size: 672 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vfbywtgh66nb"
path="res://.godot/imported/state_chart.svg-5c268dd045b20d73dfacd5cdf7606676.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/state_chart.svg"
dest_files=["res://.godot/imported/state_chart.svg-5c268dd045b20d73dfacd5cdf7606676.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,319 @@
@tool
## This class represents a state that can be either active or inactive.
class_name StateChartState
extends Node
## Called when the state is entered.
signal state_entered()
## Called when the state is exited.
signal state_exited()
## Called when the state receives an event. Only called if the state is active.
signal event_received(event:StringName)
## Called when the state is processing.
signal state_processing(delta:float)
## Called when the state is physics processing.
signal state_physics_processing(delta:float)
## Called when the state chart step function is called.
signal state_stepped()
## Called when the state is receiving input.
signal state_input(event:InputEvent)
## Called when the state is receiving unhandled input.
signal state_unhandled_input(event:InputEvent)
## Called every frame while a delayed transition is pending for this state.
## Returns the initial delay and the remaining delay of the transition.
signal transition_pending(initial_delay:float, remaining_delay:float)
## Whether the state is currently active (internal flag, use active).
var _state_active:bool = false
## Whether the current state is active.
var active:bool:
get: return _state_active
## The currently active pending transition.
var _pending_transition:Transition = null
## Remaining time in seconds until the pending transition is triggered.
var _pending_transition_remaining_delay:float = 0
## The initial time of the pending transition.
var _pending_transition_initial_delay:float = 0
## Transitions in this state that react on events.
var _transitions:Array[Transition] = []
## The state chart that owns this state.
var _chart:StateChart
func _ready() -> void:
# don't run in the editor
if Engine.is_editor_hint():
return
_chart = _find_chart(get_parent())
## Finds the owning state chart by moving upwards.
func _find_chart(parent:Node) -> StateChart:
if parent is StateChart:
return parent
return _find_chart(parent.get_parent())
## Runs a transition either immediately or delayed depending on the
## transition settings.
func _run_transition(transition:Transition, immediately:bool = false):
var initial_delay := transition.evaluate_delay()
if not immediately and initial_delay > 0:
_queue_transition(transition, initial_delay)
else:
_chart._run_transition(transition, self)
## Called when the state chart is built.
func _state_init():
# disable state by default
process_mode = Node.PROCESS_MODE_DISABLED
_state_active = false
_toggle_processing(false)
# load transitions
_transitions.clear()
for child in get_children():
if child is Transition:
_transitions.append(child)
## Called when the state is entered. The parameter gives the target of the transition
## that caused the entering of the state. This can be used determine whether an
## initial child state should be activated. If the state entering was not caused by a transition
## this can be null.
func _state_enter(_transition_target:StateChartState):
# print("state_enter: " + name)
_state_active = true
process_mode = Node.PROCESS_MODE_INHERIT
# enable processing if someone listens to our signal
_toggle_processing(true)
# emit the signal
state_entered.emit()
# process transitions that are triggered by entering the state
_process_transitions(StateChart.TriggerType.STATE_ENTER)
## Called when the state is exited.
func _state_exit():
# print("state_exit: " + name)
# cancel any pending transitions
_pending_transition = null
_pending_transition_remaining_delay = 0
_pending_transition_initial_delay = 0
_state_active = false
# stop processing
process_mode = Node.PROCESS_MODE_DISABLED
_toggle_processing(false)
# emit the signal
state_exited.emit()
## Called when the state should be saved. The parameter is is the SavedState object
## of the parent state. The state is expected to add a child to the SavedState object
## under its own name.
##
## The child_levels parameter indicates how many levels of children should be saved.
## If set to -1 (default), all children should be saved. If set to 0, no children should be saved.
##
## This method will only be called if the state is active and should only be called on
## active children if children should be saved.
func _state_save(saved_state:SavedState, child_levels:int = -1) -> void:
if not active:
push_error("_state_save should only be called if the state is active.")
return
# create a new SavedState object for this state
var our_saved_state := SavedState.new()
our_saved_state.pending_transition_name = _pending_transition.name if _pending_transition != null else ""
our_saved_state.pending_transition_remaining_delay = _pending_transition_remaining_delay
our_saved_state.pending_transition_initial_delay = _pending_transition_initial_delay
# add it to the parent
saved_state.add_substate(self, our_saved_state)
if child_levels == 0:
return
# calculate the child levels for the children, -1 means all children
var sub_child_levels:int = -1 if child_levels == -1 else child_levels - 1
# save all children
for child in get_children():
if child is StateChartState and child.active:
child._state_save(our_saved_state, sub_child_levels)
## Called when the state should be restored. The parameter is the SavedState object
## of the parent state. The state is expected to retrieve the SavedState object
## for itself from the parent and restore its state from it.
##
## The child_levels parameter indicates how many levels of children should be restored.
## If set to -1 (default), all children should be restored. If set to 0, no children should be restored.
##
## 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) -> void:
# 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
if active:
_state_exit()
# otherwise we are already inactive, so we don't need to do anything
return
# otherwise if we are currently inactive, activate the state
if not active:
_state_enter(null)
# and restore any pending transition
_pending_transition = get_node_or_null(our_saved_state.pending_transition_name) as Transition
_pending_transition_remaining_delay = our_saved_state.pending_transition_remaining_delay
_pending_transition_initial_delay = our_saved_state.pending_transition_initial_delay
# if _pending_transition != null:
# print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_remaining_delay))
# else:
# print("no pending transition restored")
if child_levels == 0:
return
# calculate the child levels for the children, -1 means all children
var sub_child_levels := -1 if child_levels == -1 else child_levels - 1
# restore all children
for child in get_children():
if child is StateChartState:
child._state_restore(our_saved_state, sub_child_levels)
## Called while the state is active.
func _process(delta:float) -> void:
if Engine.is_editor_hint():
return
# emit the processing signal
state_processing.emit(delta)
# check if there is a pending transition
if _pending_transition != null:
_pending_transition_remaining_delay -= delta
# Notify interested parties that currently a transition is pending.
transition_pending.emit(_pending_transition.delay_seconds, max(0, _pending_transition_remaining_delay))
# if the transition is ready, trigger it
# and clear it.
if _pending_transition_remaining_delay <= 0:
var transition_to_send := _pending_transition
_pending_transition = null
_pending_transition_remaining_delay = 0
# print("requesting transition from " + name + " to " + transition_to_send.to.get_concatenated_names() + " now")
_chart._run_transition(transition_to_send, self)
func _handle_transition(_transition:Transition, _source:StateChartState):
push_error("State " + name + " cannot handle transitions.")
func _physics_process(delta:float) -> void:
if Engine.is_editor_hint():
return
state_physics_processing.emit(delta)
## Called when the state chart step function is called.
func _state_step():
state_stepped.emit()
func _input(event:InputEvent):
state_input.emit(event)
func _unhandled_input(event:InputEvent):
state_unhandled_input.emit(event)
## Processes all transitions in this state.
func _process_transitions(trigger_type:StateChart.TriggerType, event:StringName = "") -> bool:
if not active:
return false
# emit an event received signal if this is an event trigger
if trigger_type == StateChart.TriggerType.EVENT:
event_received.emit(event)
# Walk over all transitions
for transition in _transitions:
# Check if the transition is triggered by the given trigger type
if transition.is_triggered_by(trigger_type) \
# if the event is given it needs to match the event of the transition
and (event == "" or transition.event == event) \
# and in every case the guard needs to match
and transition.evaluate_guard():
# print(name + ": consuming event " + event)
# first match wins
# if the winning transition is the currently pending transition, we do not replace it
if transition != _pending_transition:
_run_transition(transition)
# but in any case we return true, because we consumed the event
return true
return false
## Queues the transition to be triggered after the delay.
func _queue_transition(transition:Transition, initial_delay:float):
# print("transitioning from " + name + " to " + transition.to.get_concatenated_names() + " in " + str(transition.delay_seconds) + " seconds" )
# queue the transition for the delay time (0 means next frame)
_pending_transition = transition
_pending_transition_initial_delay = initial_delay
_pending_transition_remaining_delay = initial_delay
# enable processing when we have a transition
set_process(true)
func _get_configuration_warnings() -> PackedStringArray:
var result := []
# if not at least one of our ancestors is a StateChart add a warning
var parent := get_parent()
var found := false
while is_instance_valid(parent):
if parent is StateChart:
found = true
break
parent = parent.get_parent()
if not found:
result.append("State is not a child of a StateChart. This will not work.")
return result
func _toggle_processing(active:bool):
set_process(active and _has_connections(state_processing))
set_physics_process(active and _has_connections(state_physics_processing))
set_process_input(active and _has_connections(state_input))
set_process_unhandled_input(active and _has_connections(state_unhandled_input))
## Checks whether the given signal has connections.
func _has_connections(sgnl:Signal) -> bool:
return sgnl.get_connections().size() > 0

View file

@ -0,0 +1,22 @@
@tool
@icon("state_is_active_guard.svg")
## A guard that checks if a certain state is active.
class_name StateIsActiveGuard
extends Guard
const DebugUtil = preload("debug_util.gd")
## The state to be checked. When null this guard will return false.
@export_node_path("StateChartState") var state: NodePath
func is_satisfied(context_transition:Transition, context_state:StateChartState) -> bool:
## resolve the state, relative to the transition
var actual_state := context_transition.get_node_or_null(state)
if actual_state == null:
push_warning("State ", state , " referenced in StateIsActiveGuard below ", DebugUtil.path_of(context_state), " could not be resolved. Verify that the node path is correct.")
return false
return actual_state.active
func get_supported_trigger_types() -> int:
return StateChart.TriggerType.STATE_CHANGE

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 8 1 C 6.6741556 1 5.43737 1.3647026 4.3828125 2 L 2 2 C 1.4460012 2 1 2.4460012 1 3 L 1 8 C 1 11.877992 4.1220078 15 8 15 C 11.877992 15 15 11.877992 15 8 L 15 3 C 15 2.4460012 14.553998 2 14 2 L 11.617188 2 C 10.56263 1.3647026 9.3258444 1 8 1 z M 6 4 L 10 4 C 11.099348 4 12 4.9006519 12 6 L 12 10 C 12 11.099348 11.099348 12 10 12 L 6 12 C 4.9006519 12 4 11.099348 4 10 L 4 6 C 4 4.9006519 4.9006519 4 6 4 z M 6 5 C 5.4373531 5 5 5.4373531 5 6 L 5 10 C 5 10.562646 5.4373531 11 6 11 L 10 11 C 10.562646 11 11 10.562646 11 10 L 11 6 C 11 5.4373531 10.562646 5 10 5 L 6 5 z M 8 6 A 2 2 0 0 1 10 8 A 2 2 0 0 1 8 10 A 2 2 0 0 1 6 8 A 2 2 0 0 1 8 6 z "/></svg>

After

Width:  |  Height:  |  Size: 747 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://1713pry1l3cs"
path="res://.godot/imported/state_is_active_guard.svg-d4eaf044adc73632156a007f84651435.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/state_is_active_guard.svg"
dest_files=["res://.godot/imported/state_is_active_guard.svg-d4eaf044adc73632156a007f84651435.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 6 5 L 1 8 L 6 11 L 6 5 z M 10 5 L 10 11 L 15 8 L 10 5 z "/></svg>

After

Width:  |  Height:  |  Size: 154 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vga3avpb4gyh"
path="res://.godot/imported/toggle_sidebar.svg-99e4fe22fa516ab6214c0533adb07ec0.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/toggle_sidebar.svg"
dest_files=["res://.godot/imported/toggle_sidebar.svg-99e4fe22fa516ab6214c0533adb07ec0.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,193 @@
@tool
@icon("transition.svg")
class_name Transition
extends Node
const ExpressionUtil = preload("expression_util.gd")
const DebugUtil = preload("debug_util.gd")
## Flag indicating that this transition has been modified and needs to update its caches.
var _dirty:bool = true
## Cached target state of this transition. Will be resolved when [method resolve_target] is called.
var _target:StateChartState = null
## The trigger types that are supported by this transition. This is a bit mask of the supported trigger types.
## The trigger types are defined in the [StateChart] class.
var _supported_trigger_types:int = 0
## Fired when this transition is taken. For delayed transitions, this signal
## will be fired when the transition is actually executed (e.g. when its delay
## has elapsed and the transition has not been arborted before). The signal will
## always be fired before the state is exited.
signal taken()
## The target state to which the transition should switch
@export_node_path("StateChartState") var to:NodePath:
set(value):
to = value
_dirty = true
update_configuration_warnings()
## The event that should trigger this transition, can be empty in which case
## the transition will immediately be tried when the state is entered
@export var event:StringName = "":
set(value):
event = value
_dirty = true
update_configuration_warnings()
## An expression that must evaluate to true for the transition to be taken. Can be
## empty in which case the transition will always be taken
@export var guard:Guard:
set(value):
guard = value
_dirty = true
update_configuration_warnings()
## A delay in seconds before the transition is taken. Can be 0 in which case
## the transition will be taken immediately. The transition will only be taken
## if the state is still active when the delay has passed and has never been left.
## @deprecated: use the new delay_in_seconds property instead
var delay_seconds:float = 0.0:
set(value):
delay_in_seconds = str(value)
get:
if delay_in_seconds.is_valid_float():
return float(delay_in_seconds)
return 0.0
## An expression for the delay in seconds before the transition is taken.
## This expression can use all expression properties of the state chart.
## If the expression does not evaluate to a valid float or a negative value,
## the delay will be 0. When the delay is 0, the transition will be taken immediately.
## The transition will only be taken if the state is still active when the delay has
## passed and has never been left.
var delay_in_seconds:String = "0.0":
set(value):
delay_in_seconds = value
update_configuration_warnings()
## Read-only property that returns true if the transition has an event specified.
## @deprecated: this property is no longer needed. It will be removed in a future version.
var has_event:bool:
get:
return event != null and event.length() > 0
## Returns true if this transition is potentially triggered by the given trigger type.
func is_triggered_by(trigger_type:StateChart.TriggerType) -> bool:
if _dirty:
_refresh_caches()
return (_supported_trigger_types & trigger_type) != 0
## Takes this transition immediately or with a delay if defined
## Note: if there is a delay on this transition and immediately param is true, it forces the transition to be taken without the delay
func take(immediately:bool = true) -> void:
var parent_state:StateChartState = get_parent() as StateChartState
if parent_state == null:
push_error("Transitions must be children of states.")
return
parent_state._run_transition(self, immediately)
## Evaluates the guard expression and returns true if the transition should be taken.
## If no guard expression is specified, this function will always return true.
func evaluate_guard() -> bool:
if guard == null:
return true
var parent_state:StateChartState = get_parent() as StateChartState
if parent_state == null:
push_error("Transitions must be children of states.")
return false
return guard.is_satisfied(self, get_parent())
## Evaluates the delay of this transition.
func evaluate_delay() -> float:
# if the expression just is a single float, skip the evaluation and just
# return the float value. This is a performance optimization.
if delay_in_seconds.is_valid_float():
return float(delay_in_seconds)
# evaluate the expression
var parent_state:StateChartState = get_parent() as StateChartState
if parent_state == null:
push_error("Transitions must be children of states.")
return 0.0
var result = ExpressionUtil.evaluate_expression("delay of " + DebugUtil.path_of(self), parent_state._chart, delay_in_seconds, 0.0)
if typeof(result) != TYPE_FLOAT:
push_error("Expression: ", delay_in_seconds ," result: ", result, " is not a float. Returning 0.0.")
return 0.0
return result
## Resolves the target state and returns it. If the target state is not found,
## this function will return null.
func resolve_target() -> StateChartState:
if _dirty:
_refresh_caches()
return _target
func _get_configuration_warnings() -> PackedStringArray:
var warnings:Array = []
if get_child_count() > 0:
warnings.append("Transitions should not have children")
if to == null or to.is_empty():
warnings.append("The target state is not set")
elif resolve_target() == null:
warnings.append("The target state " + str(to) + " could not be found")
if not (get_parent() is StateChartState):
warnings.append("Transitions must be children of states.")
return warnings
func _get_property_list() -> Array:
var properties:Array = []
properties.append({
"name": "delay_in_seconds",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_EXPRESSION
})
# hide the old delay_seconds property
properties.append({
"name": "delay_seconds",
"type": TYPE_FLOAT,
"usage": PROPERTY_USAGE_NONE
})
return properties
func _refresh_caches():
_dirty = false
var is_automatic:bool = (event == null or event.length() == 0)
if to != null and not to.is_empty():
var result:Node = get_node_or_null(to)
if result is StateChartState:
_target = result
_supported_trigger_types = 0
if not is_automatic:
# non-automatic transitions can only be triggered by events
_supported_trigger_types |= StateChart.TriggerType.EVENT
else:
# automatic transitions can be triggered by multiple conditions.
# ALL automatic transitions can be triggered by state enter
_supported_trigger_types |= StateChart.TriggerType.STATE_ENTER
# ALL automatic transitions remain "in play" until the state is left. While
# "in play" they can be triggered by property changes or state changes.
# Which changes exactly are supported is determined by the used guard(s).
# Check the guard for trigger types
if guard != null:
_supported_trigger_types |= guard.get_supported_trigger_types()

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ef9f4c" d="M 0 2 L 0 4 L 1 4 L 1 12 L 0 12 L 0 14 L 2 14 A 1.0001 1.0001 0 0 0 3 13 L 3 3 A 1.0001 1.0001 0 0 0 2 2 L 0 2 z M 14 2 A 1.0001 1.0001 0 0 0 13 3 L 13 13 A 1.0001 1.0001 0 0 0 14 14 L 16 14 L 16 12 L 15 12 L 15 4 L 16 4 L 16 2 L 14 2 z M 8.8320312 4.4453125 L 7.1679688 5.5546875 L 8.1328125 7 L 5 7 L 5 9 L 8.1328125 9 L 7.1679688 10.445312 L 8.8320312 11.554688 L 10.832031 8.5546875 A 1.0001 1.0001 0 0 0 10.832031 7.4453125 L 8.8320312 4.4453125 z "/></svg>

After

Width:  |  Height:  |  Size: 549 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://chb8tq62aj2b2"
path="res://.godot/imported/transition.svg-20a1a52a85a71c731b2386952d47b2f7.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/transition.svg"
dest_files=["res://.godot/imported/transition.svg-20a1a52a85a71c731b2386952d47b2f7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,58 @@
const RingBuffer = preload("ring_buffer.gd")
var _buffer:RingBuffer = null
var _dirty:bool = false
## Whether the history has changed since the full
## history string was last requested.
var dirty:bool:
get: return _dirty
func _init(maximum_lines:int = 500):
_buffer = RingBuffer.new(maximum_lines)
_dirty = false
## Sets the maximum number of lines to store in the history.
## This will clear the history.
func set_maximum_lines(maximum_lines:int) -> void:
_buffer.set_maximum_lines(maximum_lines)
## Adds an item to the history list.
func add_history_entry(frame:int, text:String) -> void:
_buffer.append("[%s]: %s \n" % [frame, text])
_dirty = true
## Adds a transition to the history list.
func add_transition(frame:int, name:String, from:String, to:String) -> void:
add_history_entry(frame, "[»] Transition: %s from %s to %s" % [name, from, to])
## Adds an event to the history list.
func add_event(frame:int, event:StringName) -> void:
add_history_entry(frame, "[!] Event received: %s" % event)
## Adds a state entered event to the history list.
func add_state_entered(frame:int, name:StringName) -> void:
add_history_entry(frame, "[>] Enter: %s" % name)
## Adds a state exited event to the history list.
func add_state_exited(frame:int, name:StringName) -> void:
add_history_entry(frame, "[<] Exit: %s" % name)
## Clears the history.
func clear() -> void:
_buffer.clear()
_dirty = true
## Returns the full history as a string.
func get_history_text() -> String:
_dirty = false
return _buffer.join()

View file

@ -0,0 +1,388 @@
## UI for the in-editor state debugger
@tool
extends Control
## Utility class for holding state info
const DebuggerStateInfo = preload("editor_debugger_state_info.gd")
## Debugger history wrapper. Shared with in-game debugger.
const DebuggerHistory = preload("../debugger_history.gd")
## The debugger message
const DebuggerMessage = preload("editor_debugger_message.gd")
## The settings propapagor
const SettingsPropagator = preload("editor_debugger_settings_propagator.gd")
## Constants for the settings
const SETTINGS_ROOT:String = "godot_state_charts/debugger/"
const SETTINGS_IGNORE_EVENTS:String = SETTINGS_ROOT + "ignore_events"
const SETTINGS_IGNORE_STATE_CHANGES:String = SETTINGS_ROOT + "ignore_state_changes"
const SETTINGS_IGNORE_TRANSITIONS:String = SETTINGS_ROOT + "ignore_transitions"
const SETTINGS_MAXIMUM_LINES:String = SETTINGS_ROOT + "maximum_lines"
const SETTINGS_SPLIT_OFFSET:String = SETTINGS_ROOT + "split_offset"
## The tree that shows all state charts
@onready var _all_state_charts_tree:Tree = %AllStateChartsTree
## The tree that shows the current state chart
@onready var _current_state_chart_tree:Tree = %CurrentStateChartTree
## The history edit
@onready var _history_edit:TextEdit = %HistoryEdit
## The settings UI
@onready var _ignore_events_checkbox:CheckBox = %IgnoreEventsCheckbox
@onready var _ignore_state_changes_checkbox:CheckBox = %IgnoreStateChangesCheckbox
@onready var _ignore_transitions_checkbox:CheckBox = %IgnoreTransitionsCheckbox
@onready var _maximum_lines_spin_box:SpinBox = %MaximumLinesSpinBox
@onready var _split_container:HSplitContainer = %SplitContainer
## The actual settings
var _ignore_events:bool = true
var _ignore_state_changes:bool = false
var _ignore_transitions:bool = true
## The editor settings for storing all the settings across sessions
var _settings:EditorSettings = null
## The current session (EditorDebuggerSession)
## this does not exist in exported games, so this is deliberately not
## typed, to avoid compile errors after exporting
var _session = null
## Dictionary of all state charts and their states. Key is the path to the
## state chart, value is a dictionary of states. Key is the path to the state,
## value is the state info (an array).
var _state_infos:Dictionary = {}
## Dictionary of all state charts and their histories. Key is the path to the
## state chart, value is the history.
var _chart_histories:Dictionary = {}
## Path to the currently selected state chart.
var _current_chart:NodePath = ""
## Helper variable for debouncing the maximum lines setting. When
## the value is -1, the setting hasn't been changed yet. When it's
## >= 0, the setting has been changed and the timer is waiting for
## the next timeout to update the setting. The debouncing is done
## in the same function that updates the text edit.
var _debounced_maximum_lines:int = -1
## Initializes the debugger UI using the editor settings.
func initialize(settings:EditorSettings, session:EditorDebuggerSession):
clear()
_settings = settings
_session = session
# restore editor settings
_ignore_events = _get_setting_or_default(SETTINGS_IGNORE_EVENTS, true)
_ignore_state_changes = _get_setting_or_default(SETTINGS_IGNORE_STATE_CHANGES, false)
_ignore_transitions = _get_setting_or_default(SETTINGS_IGNORE_TRANSITIONS, true)
# initialize UI elements, so they match the settings
_ignore_events_checkbox.set_pressed_no_signal(_ignore_events)
_ignore_state_changes_checkbox.set_pressed_no_signal(_ignore_state_changes)
_ignore_transitions_checkbox.set_pressed_no_signal(_ignore_transitions)
_maximum_lines_spin_box.value = _get_setting_or_default(SETTINGS_MAXIMUM_LINES, 300)
_split_container.split_offset = _get_setting_or_default(SETTINGS_SPLIT_OFFSET, 0)
## Returns the given setting or the default value if the setting is not set.
## No clue, why this isn't a built-in function.
func _get_setting_or_default(key, default) -> Variant :
if _settings == null:
return default
if not _settings.has_setting(key):
return default
return _settings.get_setting(key)
## Sets the given setting and marks it as changed.
func _set_setting(key, value) -> void:
if _settings == null:
return
_settings.set_setting(key, value)
_settings.mark_setting_changed(key)
## Clears all state charts and state trees.
func clear():
_clear_all()
## Clears all state charts and state trees.
func _clear_all():
_state_infos.clear()
_chart_histories.clear()
_all_state_charts_tree.clear()
var root := _all_state_charts_tree.create_item()
root.set_text(0, "State Charts")
root.set_selectable(0, false)
_clear_current()
## Clears all data about the current chart from the ui
func _clear_current():
_current_chart = ""
_current_state_chart_tree.clear()
_history_edit.clear()
var root := _current_state_chart_tree.create_item()
root.set_text(0, "States")
root.set_selectable(0, false)
## Adds a new state chart to the debugger.
func add_chart(path:NodePath):
_state_infos[path] = {}
_chart_histories[path] = DebuggerHistory.new()
_repaint_charts()
# push the settings to the new chart remote
SettingsPropagator.send_settings_update(_session, path, _ignore_events, _ignore_transitions)
if _current_chart.is_empty():
_current_chart = path
_select_current_chart()
## Removes a state chart from the debugger.
func remove_chart(path:NodePath):
_state_infos.erase(path)
if _current_chart == path:
_clear_current()
_repaint_charts()
## Updates state information for a state chart.
func update_state(_frame:int, state_info:Array) -> void:
var chart := DebuggerStateInfo.get_chart(state_info)
var path := DebuggerStateInfo.get_state(state_info)
if not _state_infos.has(chart):
push_error("Probable bug: Received state info for unknown chart %s" % [chart])
return
_state_infos[chart][path] = state_info
## Called when a state is entered.
func state_entered(frame:int, chart:NodePath, state:NodePath) -> void:
if not _state_infos.has(chart):
return
if not _ignore_state_changes:
var history:DebuggerHistory = _chart_histories[chart]
history.add_state_entered(frame, _get_node_name(state))
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_active(state_info, true)
## Called when a state is exited.
func state_exited(frame:int, chart:NodePath, state:NodePath) -> void:
if not _state_infos.has(chart):
return
if not _ignore_state_changes:
var history:DebuggerHistory = _chart_histories[chart]
history.add_state_exited(frame, _get_node_name(state))
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_active(state_info, false)
## Called when an event is received.
func event_received(frame:int, chart:NodePath, event:StringName) -> void:
var history:DebuggerHistory = _chart_histories.get(chart, null)
history.add_event(frame, event)
## Called when a transition is pending
func transition_pending(_frame:int, chart:NodePath, state:NodePath, transition:NodePath, pending_time:float) -> void:
var state_info = _state_infos[chart][state]
DebuggerStateInfo.set_transition_pending(state_info, transition, pending_time)
func transition_taken(frame:int, chart:NodePath, transition:NodePath, source:NodePath, destination:NodePath) -> void:
var history:DebuggerHistory = _chart_histories.get(chart, null)
history.add_transition(frame, _get_node_name(transition), _get_node_name(source), _get_node_name(destination))
## Repaints the tree of all state charts.
func _repaint_charts() -> void:
for chart in _state_infos.keys():
_add_to_tree(_all_state_charts_tree, chart, preload("../../state_chart.svg"))
_clear_unused_items(_all_state_charts_tree.get_root())
## Selects the current chart in the main tree.
func _select_current_chart(item:TreeItem = _all_state_charts_tree.get_root()) -> void:
if item.has_meta("__path") and item.get_meta("__path") == _current_chart:
_all_state_charts_tree.set_selected(item, 0)
return
var first_child := item.get_first_child()
if first_child != null:
_select_current_chart(first_child)
var next := item.get_next()
while next != null:
_select_current_chart(next)
next = next.get_next()
## Repaints the tree of the currently selected state chart.
func _repaint_current_chart(force:bool = false) -> void:
if _current_chart.is_empty():
return
# get the history for this chart and update the history text edit
var history:DebuggerHistory = _chart_histories[_current_chart]
if history != null and (history.dirty or force):
_history_edit.text = history.get_history_text()
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
# update the tree
for state_info in _state_infos[_current_chart].values():
if DebuggerStateInfo.get_active(state_info):
_add_to_tree(_current_state_chart_tree, DebuggerStateInfo.get_state(state_info), DebuggerStateInfo.get_state_icon(state_info))
if DebuggerStateInfo.get_transition_pending(state_info):
var transition_path := DebuggerStateInfo.get_transition_path(state_info)
var transition_delay := DebuggerStateInfo.get_transition_delay(state_info)
var name := _get_node_name(transition_path)
_add_to_tree(_current_state_chart_tree, DebuggerStateInfo.get_transition_path(state_info), preload("../../transition.svg"), "%s (%.1fs)" % [name, transition_delay])
_clear_unused_items(_current_state_chart_tree.get_root())
## Walks over the tree and removes all items that are not marked as in use
## removes the "in-use" marker from all remaining items
func _clear_unused_items(root:TreeItem) -> void:
if root == null:
return
for child in root.get_children():
if not child.has_meta("__in_use"):
root.remove_child(child)
_free_all(child)
else:
child.remove_meta("__in_use")
_clear_unused_items(child)
## Frees this tree item and all its children
func _free_all(root:TreeItem) -> void:
if root == null:
return
for child in root.get_children():
root.remove_child(child)
_free_all(child)
root.free()
## Adds an item to the tree. Will re-use existing items if possible.
## The node path will be used as structure for the tree. The created
## leaf will have the given icon and text.
func _add_to_tree(tree:Tree, path:NodePath, icon:Texture2D, text:String = ""):
var ref := tree.get_root()
for i in path.get_name_count():
var segment := path.get_name(i)
# do we need to add a new child?
var needs_new := true
if ref != null:
for child in ref.get_children():
# re-use child if it exists
if child.get_text(0) == segment:
ref = child
ref.set_meta("__in_use", true)
needs_new = false
break
if needs_new:
ref = tree.create_item(ref)
ref.set_text(0, segment)
ref.set_meta("__in_use", true)
ref.set_selectable(0, false)
ref.set_meta("__path", path)
if text != "":
ref.set_text(0, text)
ref.set_icon(0, icon)
ref.set_selectable(0, true)
## Called when a state chart is selected in the tree.
func _on_all_state_charts_tree_item_selected():
var item := _all_state_charts_tree.get_selected()
if item == null:
return
if not item.has_meta("__path"):
return
var path = item.get_meta("__path")
_current_chart = path
_repaint_current_chart(true)
## Called every 0.5 seconds to update the history text edit and the maximum lines setting.
func _on_timer_timeout():
# update the maximum lines setting if it has changed
if _debounced_maximum_lines >= 0:
_set_setting(SETTINGS_MAXIMUM_LINES, _debounced_maximum_lines)
# walk over all histories and update their maximum lines
for history in _chart_histories.values():
history.set_maximum_lines(_debounced_maximum_lines)
# and reset the debounced value
_debounced_maximum_lines = -1
# repaint the current chart
_repaint_current_chart()
## Called when the ignore events checkbox is toggled.
func _on_ignore_events_checkbox_toggled(button_pressed:bool) -> void:
_ignore_events = button_pressed
_set_setting(SETTINGS_IGNORE_EVENTS, button_pressed)
# push the new setting to all remote charts
for chart in _state_infos.keys():
SettingsPropagator.send_settings_update(_session, chart, _ignore_events, _ignore_transitions)
## Called when the ignore state changes checkbox is toggled.
func _on_ignore_state_changes_checkbox_toggled(button_pressed:bool) -> void:
_ignore_state_changes = button_pressed
_set_setting(SETTINGS_IGNORE_STATE_CHANGES, button_pressed)
## Called when the ignore transitions checkbox is toggled.
func _on_ignore_transitions_checkbox_toggled(button_pressed:bool) -> void:
_ignore_transitions = button_pressed
_set_setting(SETTINGS_IGNORE_TRANSITIONS, button_pressed)
# push the new setting to all remote charts
for chart in _state_infos.keys():
SettingsPropagator.send_settings_update(_session, chart, _ignore_events, _ignore_transitions)
## Called when the maximum lines spin box value is changed.
func _on_maximum_lines_spin_box_value_changed(value:int) -> void:
_debounced_maximum_lines = value
## Called when the split container is dragged.
func _on_split_container_dragged(offset:int) -> void:
_set_setting(SETTINGS_SPLIT_OFFSET, offset)
## Helper to get the last element of a node path
func _get_node_name(path:NodePath) -> StringName:
return path.get_name(path.get_name_count() - 1)
## Called when the clear button is pressed.
func _on_clear_button_pressed() -> void:
_history_edit.text = ""
if _chart_histories.has(_current_chart):
var history:DebuggerHistory = _chart_histories[_current_chart]
history.clear()
## Called when the copy to clipboard button is pressed.
func _on_copy_to_clipboard_button_pressed() -> void:
DisplayServer.clipboard_set(_history_edit.text)

View file

@ -0,0 +1,120 @@
[gd_scene load_steps=2 format=3 uid="uid://donfbhh5giyfy"]
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/editor_debugger/editor_debugger.gd" id="1_ia1de"]
[node name="State Charts" type="VBoxContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_ia1de")
[node name="SplitContainer" type="HSplitContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
split_offset = 300
[node name="AllStateChartsTree" type="Tree" parent="SplitContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="TabContainer" type="TabContainer" parent="SplitContainer"]
layout_mode = 2
[node name="State Chart" type="MarginContainer" parent="SplitContainer/TabContainer"]
layout_mode = 2
[node name="CurrentStateChartTree" type="Tree" parent="SplitContainer/TabContainer/State Chart"]
unique_name_in_owner = true
custom_minimum_size = Vector2(200, 0)
layout_mode = 2
size_flags_vertical = 3
[node name="History" type="MarginContainer" parent="SplitContainer/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="SplitContainer/TabContainer/History"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="HistoryEdit" type="TextEdit" parent="SplitContainer/TabContainer/History/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="SplitContainer/TabContainer/History/VBoxContainer"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Clear"
[node name="CopyToClipboardButton" type="Button" parent="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Copy to Clipboard"
[node name="Settings" type="MarginContainer" parent="SplitContainer/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="SplitContainer/TabContainer/Settings"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="IgnoreEventsCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show events in the history."
text = "Ignore events"
[node name="IgnoreStateChangesCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show state changes in the history."
text = "Ignore state changes"
[node name="IgnoreTransitionsCheckbox" type="CheckBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show transitions in the history."
text = "Ignore transitions"
[node name="Label" type="Label" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
layout_mode = 2
text = "Maximum lines in history"
[node name="MaximumLinesSpinBox" type="SpinBox" parent="SplitContainer/TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
min_value = 50.0
max_value = 1000.0
value = 300.0
rounded = true
[node name="Timer" type="Timer" parent="."]
wait_time = 0.2
autostart = true
[connection signal="dragged" from="SplitContainer" to="." method="_on_split_container_dragged"]
[connection signal="item_selected" from="SplitContainer/AllStateChartsTree" to="." method="_on_all_state_charts_tree_item_selected"]
[connection signal="pressed" from="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer/ClearButton" to="." method="_on_clear_button_pressed"]
[connection signal="pressed" from="SplitContainer/TabContainer/History/VBoxContainer/HBoxContainer/CopyToClipboardButton" to="." method="_on_copy_to_clipboard_button_pressed"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreEventsCheckbox" to="." method="_on_ignore_events_checkbox_toggled"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreStateChangesCheckbox" to="." method="_on_ignore_state_changes_checkbox_toggled"]
[connection signal="toggled" from="SplitContainer/TabContainer/Settings/VBoxContainer/IgnoreTransitionsCheckbox" to="." method="_on_ignore_transitions_checkbox_toggled"]
[connection signal="value_changed" from="SplitContainer/TabContainer/Settings/VBoxContainer/MaximumLinesSpinBox" to="." method="_on_maximum_lines_spin_box_value_changed"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View file

@ -0,0 +1,117 @@
@tool
const MESSAGE_PREFIX = "gds"
const STATE_CHART_ADDED_MESSAGE = MESSAGE_PREFIX + ":sca"
const STATE_CHART_REMOVED_MESSAGE = MESSAGE_PREFIX + ":scr"
const STATE_UPDATED_MESSAGE = MESSAGE_PREFIX + ":stu"
const STATE_ENTERED_MESSAGE = MESSAGE_PREFIX + ":sten"
const STATE_EXITED_MESSAGE = MESSAGE_PREFIX + ":stex"
const TRANSITION_PENDING_MESSAGE = MESSAGE_PREFIX + ":trp"
const TRANSITION_TAKEN_MESSAGE = MESSAGE_PREFIX + ":trf"
const STATE_CHART_EVENT_RECEIVED_MESSAGE = MESSAGE_PREFIX + ":scev"
const DebuggerStateInfo = preload("editor_debugger_state_info.gd")
## Whether we can currently send debugger messages.
static func _can_send() -> bool:
return not Engine.is_editor_hint() and OS.has_feature("editor")
## Sends a state_chart_added message.
static func state_chart_added(chart:StateChart) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_ADDED_MESSAGE, [chart.get_path()])
## Sends a state_chart_removed message.
static func state_chart_removed(chart:StateChart) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_REMOVED_MESSAGE, [chart.get_path()])
## Sends a state_updated message
static func state_updated(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
var transition_path:NodePath = NodePath()
if is_instance_valid(state._pending_transition) and state._pending_transition.is_inside_tree():
transition_path = chart.get_path_to(state._pending_transition)
EngineDebugger.send_message(STATE_UPDATED_MESSAGE, [Engine.get_process_frames(), DebuggerStateInfo.make_array( \
chart.get_path(), \
chart.get_path_to(state), \
state.active, \
is_instance_valid(state._pending_transition), \
transition_path, \
state._pending_transition_remaining_delay, \
state)]
)
## Sends a state_entered message
static func state_entered(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_ENTERED_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(state)])
## Sends a state_exited message
static func state_exited(chart:StateChart, state:StateChartState) -> void:
if not _can_send():
return
if not state.is_inside_tree() or not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_EXITED_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(state)])
## Sends a transition taken message
static func transition_taken(chart:StateChart, source:StateChartState, transition:Transition) -> void:
if not _can_send():
return
var target:StateChartState = transition.resolve_target()
if not source.is_inside_tree() or not chart.is_inside_tree() or not transition.is_inside_tree() or target == null or not target.is_inside_tree():
return
EngineDebugger.send_message(TRANSITION_TAKEN_MESSAGE,[Engine.get_process_frames(), chart.get_path(), chart.get_path_to(transition), chart.get_path_to(source), chart.get_path_to(target)])
## Sends an event received message
static func event_received(chart:StateChart, event_name:StringName) -> void:
if not _can_send():
return
if not chart.is_inside_tree():
return
EngineDebugger.send_message(STATE_CHART_EVENT_RECEIVED_MESSAGE, [Engine.get_process_frames(), chart.get_path(), event_name])
## Sends a transition pending message
static func transition_pending(chart:StateChart, source:StateChartState, transition:Transition, pending_transition_remaining_delay:float) -> void:
if not _can_send():
return
if not source.is_inside_tree() or not chart.is_inside_tree() or not transition.is_inside_tree():
return
EngineDebugger.send_message(TRANSITION_PENDING_MESSAGE, [Engine.get_process_frames(), chart.get_path(), chart.get_path_to(source), chart.get_path_to(transition), pending_transition_remaining_delay])

View file

@ -0,0 +1,53 @@
## Debugger plugin to show state charts in the editor UI.
extends EditorDebuggerPlugin
## Debugger message network protocol
const DebuggerMessage = preload("editor_debugger_message.gd")
const DebuggerUI = preload("editor_debugger.gd")
## The UI scene holding the debugger UI
var _debugger_ui_scene:PackedScene = preload("editor_debugger.tscn")
## Current editor settings
var _settings:EditorSettings = null
func initialize(settings:EditorSettings):
_settings = settings
func _has_capture(prefix):
return prefix == DebuggerMessage.MESSAGE_PREFIX
func _capture(message, data, session_id):
var ui:DebuggerUI = get_session(session_id).get_meta("__state_charts_debugger_ui")
match(message):
DebuggerMessage.STATE_CHART_EVENT_RECEIVED_MESSAGE:
ui.event_received(data[0], data[1], data[2])
DebuggerMessage.STATE_CHART_ADDED_MESSAGE:
ui.add_chart(data[0])
DebuggerMessage.STATE_CHART_REMOVED_MESSAGE:
ui.remove_chart(data[0])
DebuggerMessage.STATE_UPDATED_MESSAGE:
ui.update_state(data[0], data[1])
DebuggerMessage.STATE_CHART_EVENT_RECEIVED_MESSAGE:
ui.event_received(data[0], data[1], data[2])
DebuggerMessage.STATE_ENTERED_MESSAGE:
ui.state_entered(data[0], data[1], data[2])
DebuggerMessage.STATE_EXITED_MESSAGE:
ui.state_exited(data[0], data[1], data[2])
DebuggerMessage.TRANSITION_PENDING_MESSAGE:
ui.transition_pending(data[0], data[1], data[2], data[3], data[4])
DebuggerMessage.TRANSITION_TAKEN_MESSAGE:
ui.transition_taken(data[0], data[1], data[2], data[3], data[4])
return true
func _setup_session(session_id):
# get the session
var session = get_session(session_id)
# Add a new tab in the debugger session UI containing a label.
var debugger_ui:DebuggerUI = _debugger_ui_scene.instantiate()
# add the session tab
session.add_session_tab(debugger_ui)
session.stopped.connect(debugger_ui.clear)
session.set_meta("__state_charts_debugger_ui", debugger_ui)
debugger_ui.initialize(_settings, session)

View file

@ -0,0 +1,102 @@
## This is the remote part of the editor debugger. It attaches to a state
## chart similar to the in-game debugger and forwards signals and debug
## information to the editor.
extends Node
const DebuggerMessage = preload("editor_debugger_message.gd")
const SettingsPropagator = preload("editor_debugger_settings_propagator.gd")
# The scene tree, needed to interface with the settings propagator
var _tree:SceneTree
# the state chart we track
var _state_chart:StateChart
# whether to send transitions to the editor
var _ignore_transitions:bool = true
# whether to send events to the editor
var _ignore_events:bool = true
## Sets up the debugger remote to track the given state chart.
func _init(state_chart:StateChart):
if not is_instance_valid(state_chart):
push_error("Probable bug: State chart is not valid. Please report this bug.")
return
if not state_chart.is_inside_tree():
push_error("Probably bug: State chart is not in tree. Please report this bug.")
return
_state_chart = state_chart
func _ready():
# subscribe to settings changes coming from the editor debugger
# will auto-unsubscribe when the chart goes away. We do this before
# sending state_chart_added, so we get the initial settings update delivered
SettingsPropagator.get_instance(get_tree()).settings_updated.connect(_on_settings_updated)
# send initial state chart
DebuggerMessage.state_chart_added(_state_chart)
# prepare signals and send initial state of all states
_prepare()
func _on_settings_updated(chart:NodePath, ignore_events:bool, ignore_transitions:bool):
if _state_chart.get_path() != chart:
return # doesn't affect this chart
_ignore_events = ignore_events
_ignore_transitions = ignore_transitions
## Connects all signals from the currently processing state chart
func _prepare():
_state_chart.event_received.connect(_on_event_received)
# find all state nodes below the state chart and connect their signals
for child in _state_chart.get_children():
if child is StateChartState:
_prepare_state(child)
func _prepare_state(state:StateChartState):
state.state_entered.connect(_on_state_entered.bind(state))
state.state_exited.connect(_on_state_exited.bind(state))
state.transition_pending.connect(_on_transition_pending.bind(state))
# send initial state
DebuggerMessage.state_updated(_state_chart, state)
# recurse into children
for child in state.get_children():
if child is StateChartState:
_prepare_state(child)
if child is Transition:
child.taken.connect(_on_transition_taken.bind(state, child))
func _on_transition_taken(source:StateChartState, transition:Transition):
if _ignore_transitions:
return
DebuggerMessage.transition_taken(_state_chart, source, transition)
func _on_event_received(event:StringName):
if _ignore_events:
return
DebuggerMessage.event_received(_state_chart, event)
func _on_state_entered(state:StateChartState):
DebuggerMessage.state_entered(_state_chart, state)
func _on_state_exited(state:StateChartState):
DebuggerMessage.state_exited(_state_chart, state)
func _on_transition_pending(num1, remaining, state:StateChartState):
DebuggerMessage.transition_pending(_state_chart, state, state._pending_transition, remaining)

View file

@ -0,0 +1,54 @@
@tool
## This node receives messages from the editor debugger and forwards them to the state chart. It
## the reverse of EditorDebuggerMessage.
extends Node
const DebuggerMessage = preload("editor_debugger_message.gd")
const SETTINGS_UPDATED_MESSAGE = DebuggerMessage.MESSAGE_PREFIX + ":scsu"
const NAME = "StateChartEditorRemoteControl"
signal settings_updated(chart:NodePath, ignore_events:bool, ignore_transitions:bool)
static func get_instance(tree:SceneTree):
# because we add the node deferred, callers in the same frame would not see
# the node. Therefore we put it as metadata on the tree root. If we set the
# minimum to Godot 4.2, we can use a static var for this which will
# avoid this trickery.
var result = tree.root.get_meta(NAME) if tree.root.has_meta(NAME) else null
if not is_instance_valid(result):
# we want to avoid using class_name to avoid polluting the namespace with
# classes that are internals. we cannot use preload (cylic dependency)
# so we have to do this abomination
result = load("res://addons/godot_state_charts/utilities/editor_debugger/editor_debugger_settings_propagator.gd").new()
result.name = NAME
tree.root.set_meta(NAME, result)
tree.root.add_child.call_deferred(result)
return result
## Sends a settings updated message.
## session is an EditorDebuggerSession but this does not exist after export
## so its not statically typed here. This code won't run after export anyways.
static func send_settings_update(session, chart:NodePath, ignore_events:bool, ignore_transitions:bool) -> void:
session.send_message(SETTINGS_UPDATED_MESSAGE, [chart, ignore_events, ignore_transitions])
func _enter_tree():
# When the node enters the tree, register a message capture to receive
# settings updates from the editor.
EngineDebugger.register_message_capture(DebuggerMessage.MESSAGE_PREFIX, _on_settings_updated)
func _exit_tree():
# When the node exits the tree, shut down the message capture.
EngineDebugger.unregister_message_capture(DebuggerMessage.MESSAGE_PREFIX)
func _on_settings_updated(key:String, data:Array) -> bool:
# Inform interested parties that
settings_updated.emit(data[0], data[1], data[2])
# accept the message
return true

View file

@ -0,0 +1,104 @@
@tool
## Helper class for serializing/deserializing state information from the game
## into a format that can be used by the editor.
## State types that can be serialized
enum StateTypes {
AtomicState = 1,
CompoundState = 2,
ParallelState = 3,
AnimationPlayerState = 4,
AnimationTreeState = 5
}
## Create an array from the given state information.
static func make_array( \
## The owning chart
chart:NodePath, \
## Path of the state
path:NodePath, \
## Whether it is currently active
active:bool, \
## Whether a transition is currently pending for this state
transition_pending:bool, \
## The path of the pending transition if any.
transition_path:NodePath, \
## The remaining transition delay for the pending transition if any.
transition_delay:float, \
## The kind of state
state:StateChartState \
) -> Array:
return [ \
chart, \
path, \
active, \
transition_pending, \
transition_path, \
transition_delay, \
type_for_state(state) ]
## Get the state type for the given state.
static func type_for_state(state:StateChartState) -> StateTypes:
if state is CompoundState:
return StateTypes.CompoundState
elif state is ParallelState:
return StateTypes.ParallelState
elif state is AnimationPlayerState:
return StateTypes.AnimationPlayerState
elif state is AnimationTreeState:
return StateTypes.AnimationTreeState
else:
return StateTypes.AtomicState
## Accessors for the array.
static func get_chart(array:Array) -> NodePath:
return array[0]
static func get_state(array:Array) -> NodePath:
return array[1]
static func get_active(array:Array) -> bool:
return array[2]
static func get_transition_pending(array:Array) -> bool:
return array[3]
static func get_transition_path(array:Array) -> NodePath:
return array[4]
static func get_transition_delay(array:Array) -> float:
return array[5]
static func get_state_type(array:Array) -> StateTypes:
return array[6]
## Returns an icon for the state type of the given array.
static func get_state_icon(array:Array) -> Texture2D:
var type = get_state_type(array)
if type == StateTypes.AtomicState:
return preload("../../atomic_state.svg")
elif type == StateTypes.CompoundState:
return preload("../../compound_state.svg")
elif type == StateTypes.ParallelState:
return preload("../../parallel_state.svg")
elif type == StateTypes.AnimationPlayerState:
return preload("../../animation_player_state.svg")
elif type == StateTypes.AnimationTreeState:
return preload("../../animation_tree_state.svg")
else:
return null
static func set_active(array:Array, active:bool) -> void:
array[2] = active
# if no longer active, clear the pending transition
if not active:
array[3] = false
array[4] = null
array[5] = 0.0
static func set_transition_pending(array:Array, transition:NodePath, pending_time:float) -> void:
array[3] = true
array[4] = transition
array[5] = pending_time

View file

@ -0,0 +1,99 @@
@tool
extends Control
## Emitted when the user requests to toggle the sidebar.
signal sidebar_toggle_requested()
## The currently selected node or null
var _selected_node:Node
## The editor interface
var _editor_interface:EditorInterface
## The undo/redo facility
var _undo_redo:EditorUndoRedoManager
@onready var _add_section:Control = %AddSection
@onready var _no_options_label:Control = %NoOptionsLabel
@onready var _add_node_name_line_edit:LineEdit = %AddNodeNameLineEdit
@onready var _add_grid_container:Control = %AddGridContainer
func setup(editor_interface:EditorInterface, undo_redo:EditorUndoRedoManager):
_editor_interface = editor_interface
_undo_redo = undo_redo
func change_selected_node(node):
_selected_node = node
_repaint()
func _repaint():
# we can add states to all composite states and to the
# root if the root has no child state yet.
var can_add_states = \
( _selected_node is StateChart and _selected_node.get_child_count() == 0 ) \
or _selected_node is ParallelState \
or _selected_node is CompoundState
# we can add transitions to all states
var can_add_transitions = \
_selected_node is StateChartState
_add_section.visible = can_add_states or can_add_transitions
_no_options_label.visible = not (can_add_states or can_add_transitions)
for btn in _add_grid_container.get_children():
if btn.is_in_group("statebutton"):
btn.visible = can_add_states
else:
btn.visible = can_add_transitions
func _create_node(type, name:StringName):
var final_name = _add_node_name_line_edit.text.strip_edges()
if final_name.length() == 0:
final_name = name
var new_node = type.new()
_undo_redo.create_action("Add " + final_name)
_undo_redo.add_do_method(_selected_node, "add_child", new_node)
_undo_redo.add_undo_method(_selected_node, "remove_child", new_node)
_undo_redo.add_do_reference(new_node)
_undo_redo.add_do_method(new_node, "set_owner", _selected_node.get_tree().edited_scene_root)
_undo_redo.add_do_property(new_node, "name", final_name)
_undo_redo.commit_action()
if Input.is_key_pressed(KEY_SHIFT):
_editor_interface.get_selection().clear()
_editor_interface.get_selection().add_node(new_node)
_add_node_name_line_edit.grab_focus()
_editor_interface.edit_node(new_node)
_repaint()
func _on_atomic_state_pressed():
_create_node(AtomicState, "AtomicState")
func _on_compound_state_pressed():
_create_node(CompoundState, "CompoundState")
func _on_parallel_state_pressed():
_create_node(ParallelState, "ParallelState")
func _on_history_state_pressed():
_create_node(HistoryState, "HistoryState")
func _on_transition_pressed():
_create_node(Transition, "Transition")
func _on_toggle_sidebar_button_pressed():
sidebar_toggle_requested.emit()

View file

@ -0,0 +1,117 @@
[gd_scene load_steps=8 format=3 uid="uid://bephgxrkhh3e2"]
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/editor_sidebar.gd" id="1_7kcy8"]
[ext_resource type="Texture2D" uid="uid://c4ojtah20jtxc" path="res://addons/godot_state_charts/atomic_state.svg" id="2_0k4pg"]
[ext_resource type="Texture2D" uid="uid://bbudjoa3ds4qj" path="res://addons/godot_state_charts/compound_state.svg" id="3_b4okj"]
[ext_resource type="Texture2D" uid="uid://dsa1nco51br8d" path="res://addons/godot_state_charts/parallel_state.svg" id="4_lmfic"]
[ext_resource type="Texture2D" uid="uid://bkf1e240ouleb" path="res://addons/godot_state_charts/history_state.svg" id="5_oj1t0"]
[ext_resource type="Texture2D" uid="uid://chb8tq62aj2b2" path="res://addons/godot_state_charts/transition.svg" id="6_72e5q"]
[ext_resource type="Texture2D" uid="uid://vga3avpb4gyh" path="res://addons/godot_state_charts/toggle_sidebar.svg" id="9_dqcj0"]
[node name="EditorSidebar" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 4
theme_override_constants/margin_top = 4
theme_override_constants/margin_bottom = 4
script = ExtResource("1_7kcy8")
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="AddSection" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="AddLabel" type="Label" parent="VBoxContainer/AddSection"]
layout_mode = 2
text = "Add"
horizontal_alignment = 1
vertical_alignment = 1
[node name="AddNodeNameLineEdit" type="LineEdit" parent="VBoxContainer/AddSection"]
unique_name_in_owner = true
layout_mode = 2
placeholder_text = "Name"
alignment = 1
expand_to_text_length = true
select_all_on_focus = true
caret_blink = true
caret_blink_interval = 0.5
[node name="AddGridContainer" type="HFlowContainer" parent="VBoxContainer/AddSection"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/h_separation = 5
theme_override_constants/v_separation = 5
alignment = 1
[node name="CompoundState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "CompoundState"
icon = ExtResource("3_b4okj")
icon_alignment = 1
[node name="ParallelState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "ParallelState"
icon = ExtResource("4_lmfic")
icon_alignment = 1
[node name="AtomicState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "AtomicState"
icon = ExtResource("2_0k4pg")
icon_alignment = 1
[node name="HistoryState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "HistoryState"
icon = ExtResource("5_oj1t0")
icon_alignment = 1
[node name="Transition" type="Button" parent="VBoxContainer/AddSection/AddGridContainer"]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "Transition"
icon = ExtResource("6_72e5q")
icon_alignment = 1
[node name="NoOptionsLabel" type="Label" parent="VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_vertical = 0
text = "This node cannot have further child nodes."
horizontal_alignment = 1
autowrap_mode = 2
[node name="Spacer" type="Control" parent="VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
[node name="ToggleSidebarButton" type="Button" parent="VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
tooltip_text = "Toggle sidebar location"
icon = ExtResource("9_dqcj0")
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/CompoundState" to="." method="_on_compound_state_pressed"]
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/ParallelState" to="." method="_on_parallel_state_pressed"]
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/AtomicState" to="." method="_on_atomic_state_pressed"]
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/HistoryState" to="." method="_on_history_state_pressed"]
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/Transition" to="." method="_on_transition_pressed"]
[connection signal="pressed" from="VBoxContainer/ToggleSidebarButton" to="." method="_on_toggle_sidebar_button_pressed"]

View file

@ -0,0 +1,100 @@
@tool
extends EditorProperty
const StateChartUtil = preload("../state_chart_util.gd")
var _refactor_window_scene:PackedScene = preload("../event_refactor/event_refactor.tscn")
# The main control for editing the property.
var _property_control:LineEdit = LineEdit.new()
# drop down button for the popup menu
var _dropdown_button:Button = Button.new()
# popup menu with event names
var _popup_menu:PopupMenu = PopupMenu.new()
# the state chart we are currently editing
var _chart:StateChart
# the undo redo manager
var _undo_redo:EditorUndoRedoManager
func _init(transition:Transition, undo_redo:EditorUndoRedoManager):
# save the variables
_chart = StateChartUtil.find_parent_state_chart(transition)
_undo_redo = undo_redo
# setup the ui
_popup_menu.index_pressed.connect(_on_event_selected)
_dropdown_button.icon = get_theme_icon("arrow", "OptionButton")
_dropdown_button.flat = true
_dropdown_button.pressed.connect(_show_popup)
# build the actual editor
var hbox := HBoxContainer.new()
hbox.add_child(_property_control)
hbox.add_child(_dropdown_button)
_property_control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
# Add the control as a direct child of EditorProperty node.
add_child(hbox)
add_child(_popup_menu)
# Make sure the control is able to retain the focus.
add_focusable(_property_control)
_property_control.text_changed.connect(_on_text_changed)
## Shows the popup when the user clicks the button.
func _show_popup():
# always show up-to-date information in selector
var known_events = StateChartUtil.events_of(_chart)
_popup_menu.clear()
_popup_menu.add_item("<empty>")
_popup_menu.add_icon_item(get_theme_icon("Tools", "EditorIcons"), "Manage...")
if known_events.size() > 0:
_popup_menu.add_separator()
for event in known_events:
_popup_menu.add_item(event)
# and show it relative to the dropdown button
var gt := _dropdown_button.get_global_rect()
_popup_menu.reset_size()
var ms := _popup_menu.get_contents_minimum_size().x
var popup_pos := gt.end - Vector2(ms, 0) + Vector2(DisplayServer.window_get_position())
_popup_menu.set_position(popup_pos)
_popup_menu.popup()
func _on_event_selected(index:int) -> void:
# index 1 == "Manage"
if index == 1:
# open refactor window
var window = _refactor_window_scene.instantiate()
add_child(window)
window.open(_chart, _undo_redo)
return
# replace content with selection from popup
var event := _popup_menu.get_item_text(index) if index > 0 else ""
_property_control.text = event
_on_text_changed(event)
_property_control.grab_focus()
func _on_text_changed(new_text:String):
emit_changed(get_edited_property(), new_text)
func _update_property() -> void:
# Read the current value from the property.
var new_value = get_edited_object()[get_edited_property()]
# if the text is already correct, don't change it.
if new_value == _property_control.text:
return
_property_control.text = new_value

View file

@ -0,0 +1,29 @@
@tool
extends EditorInspectorPlugin
const EventEditor = preload("event_editor.gd")
var _undo_redo:EditorUndoRedoManager
func setup(undo_redo:EditorUndoRedoManager):
_undo_redo = undo_redo
func _can_handle(_object):
# We support all objects in this example.
return true
func _parse_property(object, type, name, _hint_type, _hint_string, _usage_flags, _wide):
# We handle properties of type integer.
if object is Transition and name == "event" and type == TYPE_STRING_NAME:
# Create an instance of the custom property editor and register
# it to a specific property path.
var editor = EventEditor.new(object as Transition, _undo_redo)
add_property_editor(name, editor)
# Inform the editor to remove the default property editor for
# this property type.
return true
else:
return false

View file

@ -0,0 +1,52 @@
@tool
extends ConfirmationDialog
const StateChartUtil = preload("../state_chart_util.gd")
@onready var _event_list:ItemList = %EventList
@onready var _event_name_edit:LineEdit = %EventNameEdit
var _chart:StateChart
var _undo_redo:EditorUndoRedoManager
var _current_event_name:StringName = ""
func open(chart:StateChart, undo_redo:EditorUndoRedoManager):
title = "Events of " + chart.name
_chart = chart
_refresh_events()
_undo_redo = undo_redo
func _refresh_events():
_event_list.clear()
for item in StateChartUtil.events_of(_chart):
_event_list.add_item(item)
func _close():
hide()
queue_free()
func _on_event_list_item_selected(index:int):
_current_event_name = _event_list.get_item_text(index)
_event_name_edit.text = _current_event_name
_on_event_name_edit_text_changed(_current_event_name)
func _on_event_name_edit_text_changed(new_text):
# disable rename button if the event name is the same as the
# currently selected event
get_ok_button().disabled = new_text == _current_event_name
func _on_confirmed():
var new_event_name = _event_name_edit.text
var transitions = StateChartUtil.transitions_of(_chart)
_undo_redo.create_action("Rename state chart event")
for transition in transitions:
if transition.event == _current_event_name:
_undo_redo.add_do_property(transition, "event", new_event_name)
_undo_redo.add_undo_property(transition, "event", _current_event_name)
_undo_redo.commit_action()
_close()

View file

@ -0,0 +1,55 @@
[gd_scene load_steps=2 format=3 uid="uid://cvlabg8e2qbk3"]
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/event_refactor/event_refactor.gd" id="1_hh1x6"]
[node name="event_refactor" type="ConfirmationDialog"]
initial_position = 1
title = "Rename Event"
size = Vector2i(586, 562)
visible = true
ok_button_text = "Rename"
dialog_autowrap = true
script = ExtResource("1_hh1x6")
[node name="MarginContainer" type="MarginContainer" parent="."]
offset_left = 8.0
offset_top = 8.0
offset_right = 578.0
offset_bottom = 513.0
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="MarginContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Event"
[node name="EventList" type="ItemList" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(560, 330)
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Label2" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "New name"
[node name="EventNameEdit" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
caret_blink_interval = 0.5
[connection signal="canceled" from="." to="." method="_close"]
[connection signal="confirmed" from="." to="." method="_on_confirmed"]
[connection signal="item_selected" from="MarginContainer/VBoxContainer/EventList" to="." method="_on_event_list_item_selected"]
[connection signal="text_changed" from="MarginContainer/VBoxContainer/HBoxContainer/EventNameEdit" to="." method="_on_event_name_edit_text_changed"]

View file

@ -0,0 +1,54 @@
## The content of the ring buffer
var _content:Array[String] = []
## The current index in the ring buffer
var _index = 0
## The size of the ring buffer
var _size = 0
## Whether the buffer is fully populated
var _filled = false
func _init(size:int = 300):
_size = size
_content.resize(size)
## Sets the maximum number of lines to store. This clears the buffer.
func set_maximum_lines(lines:int):
_size = lines
_content.resize(lines)
clear()
## Adds an item to the ring buffer
func append(value:String):
_content[_index] = value
if _index + 1 < _size:
_index += 1
else:
_index = 0
_filled = true
## Joins the items of the ring buffer into a big string
func join():
var result = ""
if _filled:
# start by _index + 1, run to the end and then continue from the start
for i in range(_index, _size):
result += _content[i]
# when not filled, just start at the beginning
for i in _index:
result += _content[i]
return result
func clear():
_index = 0
_filled = false

View file

@ -0,0 +1,280 @@
@icon("state_chart_debugger.svg")
extends Control
const DebuggerHistory = preload("debugger_history.gd")
## Whether or not the debugger is enabled.
@export var enabled:bool = true:
set(value):
enabled = value
if not Engine.is_editor_hint():
_setup_processing(enabled)
## 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
## Maximum lines to display in the history. Keep at 300 or below
## for best performance.
@export var maximum_lines:int = 300
## If set to true, events will not be printed in the history panel.
## If you send a large amount of events then this may clutter the
## output so you can disable it here.
@export var ignore_events:bool = false
## If set to true, state changes will not be printed in the history
## panel. If you have a large amount of state changes, this may clutter
## the output so you can disable it here.
@export var ignore_state_changes:bool = false
## If set to true, transitions will not be printed in the history.
@export var ignore_transitions:bool = false
## The tree that shows the state chart.
@onready var _tree:Tree = %Tree
## The text field with the history.
@onready var _history_edit:TextEdit = %HistoryEdit
# the state chart we track
var _state_chart:StateChart
var _root:Node
# the states we are currently connected to
var _connected_states:Array[StateChartState] = []
# the transitions we are currently connected to
# key is the transition, value is the callable
var _connected_transitions:Dictionary = {}
# the debugger history in text form
var _history:DebuggerHistory = null
func _ready():
# always run, even if the game is paused
process_mode = Node.PROCESS_MODE_ALWAYS
# initialize the buffer
_history = DebuggerHistory.new(maximum_lines)
%CopyToClipboardButton.pressed.connect(func (): DisplayServer.clipboard_set(_history_edit.text))
%ClearButton.pressed.connect(_clear_history)
var to_watch = get_node_or_null(initial_node_to_watch)
if is_instance_valid(to_watch):
debug_node(to_watch)
# mirror the editor settings
%IgnoreEventsCheckbox.set_pressed_no_signal(ignore_events)
%IgnoreStateChangesCheckbox.set_pressed_no_signal(ignore_state_changes)
%IgnoreTransitionsCheckbox.set_pressed_no_signal(ignore_transitions)
## Adds an item to the history list.
func add_history_entry(text:String):
_history.add_history_entry(Engine.get_process_frames(), text)
## 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.
func debug_node(root:Node) -> bool:
# if we are not enabled, we do nothing
if not enabled:
return false
_root = root
# disconnect all existing signals
_disconnect_all_signals()
var success = _debug_node(root)
# 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_history()
_setup_processing(true)
return success
func _debug_node(root:Node) -> bool:
# if we have no root, we use the scene root
if not is_instance_valid(root):
return false
if root is StateChart:
_state_chart = root
return true
# no luck, search the children
for child in root.get_children():
if _debug_node(child):
# found one, return
return true
# no luck, return false
return false
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():
if is_instance_valid(_state_chart):
if not ignore_events:
_state_chart.event_received.disconnect(_on_event_received)
for state in _connected_states:
# in case the state has been destroyed meanwhile
if is_instance_valid(state):
state.state_entered.disconnect(_on_state_entered)
state.state_exited.disconnect(_on_state_exited)
for transition in _connected_transitions.keys():
# in case the transition has been destroyed meanwhile
if is_instance_valid(transition):
transition.taken.disconnect(_connected_transitions.get(transition))
## Connects all signals from the currently processing state chart
func _connect_all_signals():
_connected_states.clear()
_connected_transitions.clear()
if not is_instance_valid(_state_chart):
return
_state_chart.event_received.connect(_on_event_received)
# find all state nodes below the state chart and connect their signals
for child in _state_chart.get_children():
if child is StateChartState:
_connect_signals(child)
func _connect_signals(state:StateChartState):
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 StateChartState:
_connect_signals(child)
if child is Transition:
var callable = _on_before_transition.bind(child, state)
child.taken.connect(callable)
_connected_transitions[child] = callable
func _process(delta):
# Clear contents
_tree.clear()
if not is_instance_valid(_state_chart):
return
var root = _tree.create_item()
root.set_text(0, _root.name)
# walk over the state chart and find all active states
_collect_active_states(_state_chart, root )
# also show the values of all variables
var items = _state_chart._expression_properties.keys()
if items.size() <= 0:
return # nothing to show
# sort by name so it doesn't flicker all the time
items.sort()
var properties_root = root.create_child()
properties_root.set_text(0, "< Expression properties >")
for item in items:
var value = str(_state_chart._expression_properties.get(item))
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 StateChartState:
if child.active:
var state_item:TreeItem = _tree.create_item(parent)
state_item.set_text(0, child.name)
if is_instance_valid(child._pending_transition):
var transition_item: TreeItem = state_item.create_child()
transition_item.set_text(0, ">> %s (%.2f)" % [child._pending_transition.name, child._pending_transition_remaining_delay])
_collect_active_states(child, state_item)
func _clear_history():
_history_edit.text = ""
_history.clear()
func _on_before_transition(transition:Transition, source:StateChartState):
if ignore_transitions:
return
_history.add_transition(Engine.get_process_frames(), transition.name, _state_chart.get_path_to(source), _state_chart.get_path_to(transition.resolve_target()))
func _on_event_received(event:StringName):
if ignore_events:
return
_history.add_event(Engine.get_process_frames(), event)
func _on_state_entered(state:StateChartState):
if ignore_state_changes:
return
_history.add_state_entered(Engine.get_process_frames(), state.name)
func _on_state_exited(state:StateChartState):
if ignore_state_changes:
return
_history.add_state_exited(Engine.get_process_frames(), state.name)
func _on_timer_timeout():
# ignore the timer if the history edit isn't visible
if not _history_edit.visible or not _history.dirty:
return
# fill the history field
_history_edit.text = _history.get_history_text()
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
func _on_ignore_events_checkbox_toggled(button_pressed):
ignore_events = button_pressed
func _on_ignore_state_changes_checkbox_toggled(button_pressed):
ignore_state_changes = button_pressed
func _on_ignore_transitions_checkbox_toggled(button_pressed):
ignore_transitions = button_pressed

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#8eef97" d="M 3 0 C 1.3380034 0 0 1.3380034 0 3 L 0 13 C 0 14.661996 1.3380034 16 3 16 L 13 16 C 14.661996 16 16 14.661996 16 13 L 16 3 C 16 1.3380034 14.661996 0 13 0 L 3 0 z M 3 1 L 13 1 C 14.107998 1 15 1.8920022 15 3 L 15 13 C 15 14.107998 14.107998 15 13 15 L 3 15 C 1.8920022 15 1 14.107998 1 13 L 1 3 C 1 1.8920022 1.8920022 1 3 1 z M 7 3 L 7 4 L 6 4 L 6 5 L 10 5 L 10 4 L 9 4 L 9 3 L 7 3 z M 2 5 L 2 6 L 3 6 L 3 5 L 2 5 z M 3 6 L 3 7 L 4 7 L 4 6 L 3 6 z M 13 5 L 13 6 L 14 6 L 14 5 L 13 5 z M 13 6 L 12 6 L 12 7 L 13 7 L 13 6 z M 6 6 L 6 7 L 5 7 L 5 11 L 6 11 L 6 12 L 10 12 L 10 11 L 11 11 L 11 7 L 10 7 L 10 6 L 6 6 z M 2 8 L 2 9 L 4 9 L 4 8 L 2 8 z M 12 8 L 12 9 L 14 9 L 14 8 L 12 8 z M 3 10 L 3 11 L 4 11 L 4 10 L 3 10 z M 3 11 L 2 11 L 2 12 L 3 12 L 3 11 z M 12 10 L 12 11 L 13 11 L 13 10 L 12 10 z M 13 11 L 13 12 L 14 12 L 14 11 L 13 11 z "/></svg>

After

Width:  |  Height:  |  Size: 939 B

View file

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnsri4fr2bbu0"
path="res://.godot/imported/state_chart_debugger.svg-84b90904efaf4dffb8ff9ef4bed14dd2.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/godot_state_charts/utilities/state_chart_debugger.svg"
dest_files=["res://.godot/imported/state_chart_debugger.svg-84b90904efaf4dffb8ff9ef4bed14dd2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,97 @@
[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")
maximum_lines = 50
[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"
[node name="Settings" 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/Settings"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="IgnoreEventsCheckbox" type="CheckBox" parent="TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show events in the history."
text = "Ignore events"
[node name="IgnoreStateChangesCheckbox" type="CheckBox" parent="TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show state changes in the history."
text = "Ignore state changes"
[node name="IgnoreTransitionsCheckbox" type="CheckBox" parent="TabContainer/Settings/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Do not show transitions in the history."
text = "Ignore transitions"
[node name="Timer" type="Timer" parent="."]
wait_time = 0.5
autostart = true
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreEventsCheckbox" to="." method="_on_ignore_events_checkbox_toggled"]
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreStateChangesCheckbox" to="." method="_on_ignore_state_changes_checkbox_toggled"]
[connection signal="toggled" from="TabContainer/Settings/VBoxContainer/IgnoreTransitionsCheckbox" to="." method="_on_ignore_transitions_checkbox_toggled"]
[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]

View file

@ -0,0 +1,49 @@
@tool
## Finds the first ancestor of the given node which is a state
## chart. Returns null when none is found.
static func find_parent_state_chart(node:Node) -> StateChart:
if node is StateChart:
return node
var parent = node.get_parent()
while parent != null:
if parent is StateChart:
return parent
parent = parent.get_parent()
return null
## Returns an array with all event names currently used in the given
## state chart.
static func events_of(chart:StateChart) -> Array[StringName]:
var result:Array[StringName] = []
# now collect all events below the state chart
_collect_events(chart, result)
result.sort_custom(func(a, b): return a.naturalnocasecmp_to(b) < 0)
return result
static func _collect_events(node:Node, events:Array[StringName]):
if node is Transition:
if node.event != "" and not events.has(node.event):
events.append(node.event)
for child in node.get_children():
_collect_events(child, events)
## Returns all transitions of the given state chart.
static func transitions_of(chart:StateChart) -> Array[Transition]:
var result:Array[Transition] = []
_collect_transitions(chart, result)
return result
static func _collect_transitions(node:Node, result:Array[Transition]):
if node is Transition:
result.append(node)
for child in node.get_children():
_collect_transitions(child, result)