Add state charts plugin

This commit is contained in:
Jarrod Doyle 2024-02-23 20:37:00 +00:00
parent 4133671b8f
commit 74d19d2003
Signed by: Jayrude
GPG Key ID: 38B57B16E7C0ADF7
73 changed files with 4536 additions and 1 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,15 @@
@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:State) -> bool:
for guard in guards:
if not guard.is_satisfied(context_transition, context_state):
return false
return true

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Guard">
<path d="M15.997,7.724C21.055,7.683 25.057,10.555 25.057,10.555C25.057,10.555 21.812,25.697 16.003,25.584C10.544,25.477 6.943,10.644 6.943,10.644C6.943,10.644 10.863,7.765 15.997,7.724Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1,0,0,1,-3.07883,3.66101)">
<g transform="matrix(16,0,0,16,13.8846,17.7302)">
<path d="M0.475,-0.084C0.446,-0.052 0.415,-0.028 0.38,-0.012C0.346,0.004 0.309,0.012 0.27,0.012C0.196,0.012 0.138,-0.013 0.095,-0.062C0.06,-0.102 0.043,-0.147 0.043,-0.197C0.043,-0.242 0.057,-0.281 0.086,-0.317C0.114,-0.353 0.157,-0.384 0.213,-0.411C0.181,-0.448 0.16,-0.478 0.149,-0.501C0.138,-0.525 0.133,-0.547 0.133,-0.568C0.133,-0.611 0.15,-0.649 0.183,-0.68C0.217,-0.712 0.259,-0.728 0.311,-0.728C0.359,-0.728 0.399,-0.713 0.43,-0.683C0.462,-0.653 0.477,-0.617 0.477,-0.575C0.477,-0.507 0.432,-0.449 0.342,-0.401L0.47,-0.237C0.485,-0.266 0.496,-0.299 0.504,-0.337L0.596,-0.317C0.58,-0.255 0.559,-0.203 0.532,-0.163C0.565,-0.119 0.602,-0.083 0.644,-0.053L0.585,0.017C0.549,-0.006 0.513,-0.04 0.475,-0.084ZM0.296,-0.458C0.334,-0.48 0.359,-0.5 0.37,-0.517C0.382,-0.534 0.387,-0.552 0.387,-0.573C0.387,-0.597 0.379,-0.617 0.364,-0.633C0.349,-0.648 0.329,-0.656 0.306,-0.656C0.282,-0.656 0.263,-0.648 0.247,-0.633C0.231,-0.618 0.223,-0.599 0.223,-0.577C0.223,-0.566 0.226,-0.554 0.232,-0.542C0.237,-0.53 0.246,-0.517 0.257,-0.503L0.296,-0.458ZM0.42,-0.154L0.259,-0.354C0.211,-0.325 0.179,-0.299 0.163,-0.275C0.146,-0.25 0.138,-0.226 0.138,-0.203C0.138,-0.174 0.149,-0.144 0.172,-0.112C0.195,-0.081 0.228,-0.065 0.271,-0.065C0.297,-0.065 0.324,-0.074 0.352,-0.09C0.38,-0.107 0.403,-0.128 0.42,-0.154Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,62 @@
@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(expect_transition: bool = false):
super._state_enter()
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()
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,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="AnimationPlayerState">
<g transform="matrix(3.01177,0,0,3.01177,-34.5681,-33.1274)">
<circle cx="16.79" cy="16.312" r="3.319" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(5.89427e-17,0.963968,-1,6.1152e-17,35.4409,-4.05915)">
<path d="M20.809,13.816L25.996,22.614L15.622,22.614L20.809,13.816Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,62 @@
@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(expect_transition:bool = false):
super._state_enter()
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()
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,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="AnimationTreeState">
<g transform="matrix(3.01177,0,0,3.01177,-34.5681,-33.1274)">
<circle cx="16.79" cy="16.312" r="3.319" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1,0,0,1,-0.944123,0.510573)">
<path d="M12.988,9.407L14.259,22.064L15.427,22.185L15.484,19.654L17.723,19.607L17.7,21.031L18.427,20.976L18.583,18.442L15.48,18.154L15.515,14.006L20.744,13.909L20.738,16.136L21.79,16.077L21.992,12.615L15.399,12.643L14.958,9.391L12.988,9.407Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,15 @@
@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:State) -> bool:
for guard in guards:
if guard.is_satisfied(context_transition, context_state):
return true
return false

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Guard">
<path d="M15.997,7.724C21.055,7.683 25.057,10.555 25.057,10.555C25.057,10.555 21.812,25.697 16.003,25.584C10.544,25.477 6.943,10.644 6.943,10.644C6.943,10.644 10.863,7.765 15.997,7.724Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1,0,0,1,-2.0463,2.81109)">
<g transform="matrix(16,0,0,16,13.8846,17.7302)">
<rect x="0.092" y="-0.728" width="0.077" height="0.938" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(16,0,0,16,18.0408,17.7302)">
<rect x="0.092" y="-0.728" width="0.077" height="0.938" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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=0.5
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 State
func _handle_transition(transition:Transition, source:State):
# resolve the target state
var target = transition.resolve_target()
if not target is State:
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 State:
warnings.append("Atomic states cannot have child states. These will be ignored.")
break
return warnings

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g transform="matrix(3.01177,0,0,3.01177,-34.5681,-33.1274)">
<g id="AtomicState">
<circle cx="16.79" cy="16.312" r="3.319" style="fill:rgb(225,142,57);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,245 @@
@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 State
## 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("State") var initial_state:NodePath:
get:
return initial_state
set(value):
initial_state = value
update_configuration_warnings()
## The currently active substate.
var _active_state:State = null
## The initial state
@onready var _initial_state:State = 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 = false
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 State:
var child_as_state:State = child as State
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(expect_transition:bool = false):
super._state_enter()
# activate the initial state unless we expect a transition
if not expect_transition:
if _initial_state != null:
_active_state = _initial_state
_active_state._state_enter()
else:
push_error("No initial state set for state '" + name + "'.")
func _state_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 State 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(event:StringName, property_change:bool = false) -> bool:
if not active:
return false
# forward to the active state
if is_instance_valid(_active_state):
if _active_state._process_transitions(event, property_change):
# emit the event_received signal, unless this is a property change
if not property_change:
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(event, property_change)
func _handle_transition(transition:Transition, source:State):
# 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 State:
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(false)
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:
# 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()
return
else:
push_error("The default state '" + target.default_state + "' of the history state '" + target.name + "' cannot be found.")
return
# else, just activate the target state
_active_state = target
_active_state._state_enter()
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 State 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
# set the "expect_transition" flag to true because we will send
# the transition to the child state right after we activate it.
# this avoids the child needlessly entering the initial state
_active_state._state_enter(true)
# ask child to handle the transition
child._handle_transition(transition, source)
return
return
# ask the parent
get_parent()._handle_transition(transition, source)
func add_child(node:Node, force_readable_name:bool = false, internal:InternalMode = INTERNAL_MODE_DISABLED) -> void:
super.add_child(node, force_readable_name, internal)
# 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 Engine.is_editor_hint() and node is State:
if 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(node)).call_deferred()
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 State:
child_count += 1
if child_count < 2:
warnings.append("Compound states should have at two child states.")
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,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="CompoundState">
<path d="M7.984,22.271L16.112,8.238L24.694,22.425L7.984,22.271Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1.36246,0,0,1.36246,-18.1682,-4.24482)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1.36246,0,0,1.36246,-2.16822,-4.24482)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1.36246,0,0,1.36246,-10.1682,-17.6299)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,52 @@

// ReSharper disable once CheckNamespace
namespace GodotStateCharts
{
using System;
using Godot;
/// <summary>
/// Wrapper around the compound state node.
/// </summary>
public class CompoundState : State
{
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 : State.SignalName
{
/// <summary>
/// Called when a child state is entered.
/// </summary>
public static readonly StringName ChildStateEntered = "child_state_entered";
/// <summary>
/// Called when a child state is exited.
/// </summary>
public static readonly StringName ChildStateExited = "child_state_exited";
}
}
}

View File

@ -0,0 +1,33 @@
// 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 void Connect(StringName signal, Callable method, uint flags = 0u)
{
Wrapped.Connect(signal, method, flags);
}
}
}

View File

@ -0,0 +1,93 @@
// 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 State : NodeWrapper
{
protected State(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 State 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 State(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
{
/// <summary>
/// Called when the state is entered.
/// </summary>
public static readonly StringName StateEntered = "state_entered";
/// <summary>
/// Called when the state is exited.
/// </summary>
public static readonly StringName StateExited = "state_exited";
/// <summary>
/// Called when the state receives an event. Only called if the state is active.
/// </summary>
public static readonly StringName EventReceived = "event_received";
/// <summary>
/// Called when the state is processing.
/// </summary>
public static readonly StringName StateProcessing = "state_processing";
/// <summary>
/// Called when the state is physics processing.
/// </summary>
public static readonly StringName StatePhysicsProcessing = "state_physics_processing";
/// <summary>
/// Called when the state chart <code>Step</code> function is called.
/// </summary>
public static readonly StringName StateStepped = "state_stepped";
/// <summary>
/// Called when the state is receiving input.
/// </summary>
public static readonly StringName StateInput = "state_input";
/// <summary>
/// Called when the state is receiving unhandled input.
/// </summary>
public static readonly StringName StateUnhandledInput = "state_unhandled_input";
/// <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 static readonly StringName TransitionPending = "transition_pending";
}
}
}

View File

@ -0,0 +1,82 @@
// 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
{
private 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)
{
Wrapped.Call("send_event", 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)
{
Wrapped.Call("set_expression_property", name, value);
}
/// <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()
{
Wrapped.Call("step");
}
public class SignalName : Node.SignalName
{
/// <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 static readonly StringName EventReceived = "event_received";
}
}
}

View File

@ -0,0 +1,53 @@
// 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)
{
Wrapped.Call("debug_node", node);
}
/// <summary>
/// Adds a history entry to the history output.
/// </summary>
/// <param name="text">the text to add</param>
public void AddHistoryEntry(string text)
{
Wrapped.Call("add_history_entry", text);
}
}
}

View File

@ -0,0 +1,19 @@
namespace GodotStateCharts
{
using Godot;
/// <summary>
/// A transition between two states. This class only exists to make the
/// signal names available in C#. It is not intended to be instantiated
/// or otherwise used.
/// </summary>
public class Transition {
public class SignalName : Godot.Node.SignalName
{
/// <summary>
/// Called when the transition is taken.
/// </summary>
public static readonly StringName Taken = "taken";
}
}
}

View File

@ -0,0 +1,56 @@
@tool
@icon("expression_guard.svg")
class_name ExpressionGuard
extends Guard
var expression:String = ""
func is_satisfied(context_transition:Transition, context_state:State) -> bool:
# walk up the tree to find the root state chart node
var root = context_state
while is_instance_valid(root) and not root is StateChart:
root = root.get_parent()
if not is_instance_valid(root):
push_error("Could not find root state chart node, cannot evaluate expression")
return false
var the_expression := Expression.new()
var input_names = root._expression_properties.keys()
var parse_result = the_expression.parse(expression, input_names)
if parse_result != OK:
push_error("Expression parse error: " + the_expression.get_error_text() + " for expression " + expression)
return false
# input values need to be in the same order as the input names, so we build an array
# of values
var input_values = []
for input_name in input_names:
input_values.append(root._expression_properties[input_name])
var result = the_expression.execute(input_values)
if the_expression.has_execute_failed():
push_error("Expression execute error: " + the_expression.get_error_text() + " for expression: " + expression)
return false
if typeof(result) != TYPE_BOOL:
push_error("Expression result is not a boolean. Returning false.")
return false
return result
func _get_property_list():
var properties = []
properties.append({
"name": "expression",
"type": TYPE_STRING,
"usage": PROPERTY_USAGE_DEFAULT,
"hint": PROPERTY_HINT_EXPRESSION
})
return properties

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Guard">
<path d="M15.997,7.724C21.055,7.683 25.057,10.555 25.057,10.555C25.057,10.555 21.812,25.697 16.003,25.584C10.544,25.477 6.943,10.644 6.943,10.644C6.943,10.644 10.863,7.765 15.997,7.724Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1,0,0,1,-0.935784,3.97014)">
<g transform="matrix(16,0,0,16,13.8846,17.7302)">
<path d="M0.045,-0L0.14,-0.45L0.061,-0.45L0.075,-0.519L0.154,-0.519L0.169,-0.592C0.177,-0.629 0.185,-0.656 0.193,-0.672C0.201,-0.688 0.215,-0.702 0.234,-0.712C0.252,-0.723 0.278,-0.728 0.31,-0.728C0.333,-0.728 0.365,-0.723 0.408,-0.714L0.392,-0.637C0.362,-0.645 0.337,-0.648 0.316,-0.648C0.299,-0.648 0.286,-0.644 0.277,-0.635C0.268,-0.627 0.26,-0.606 0.254,-0.574L0.242,-0.519L0.341,-0.519L0.327,-0.45L0.228,-0.45L0.134,-0L0.045,-0Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

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 State \
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,7 @@
class_name Guard
extends Resource
## Returns true if the guard is satisfied, false otherwise.
func is_satisfied(context_transition:Transition, context_state:State) -> bool:
push_error("Guard.is_satisfied() is not implemented. Did you forget to override it?")
return false

View File

@ -0,0 +1,58 @@
@tool
@icon("history_state.svg")
class_name HistoryState
extends State
## 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("State") 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 State:
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,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="HistoryState">
<g transform="matrix(1,0,0,1,0.326002,-0.0776194)">
<path d="M7.731,16.235C7.731,11.827 11.31,8.248 15.718,8.248C20.127,8.248 23.706,11.827 23.706,16.235C23.706,20.644 20.127,24.222 15.718,24.222C13.665,24.222 11.792,23.446 10.377,22.171" style="fill:none;stroke:rgb(225,142,57);stroke-width:2px;"/>
</g>
<g transform="matrix(1,0,0,1,0.465717,0)">
<path d="M7.731,16.235L10.089,16.228L7.707,18.393L5.332,16.235L7.731,16.235Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:2px;"/>
</g>
<g transform="matrix(1,0,0,1,0.294954,0.0776194)">
<path d="M15.646,12.205L15.568,16.908L18.021,18.771" style="fill:none;stroke:rgb(225,142,57);stroke-width:2px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,13 @@
@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:State) -> bool:
if guard == null:
return true
return not guard.is_satisfied(context_transition, context_state)

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Guard">
<path d="M15.997,7.724C21.055,7.683 25.057,10.555 25.057,10.555C25.057,10.555 21.812,25.697 16.003,25.584C10.544,25.477 6.943,10.644 6.943,10.644C6.943,10.644 10.863,7.765 15.997,7.724Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1,0,0,1,-0.100111,3.77816)">
<g transform="matrix(16,0,0,16,13.8846,17.7302)">
<path d="M0.113,-0.178L0.086,-0.557L0.086,-0.716L0.195,-0.716L0.195,-0.557L0.169,-0.178L0.113,-0.178ZM0.09,-0L0.09,-0.1L0.191,-0.1L0.191,-0L0.09,-0Z" style="fill-rule:nonzero;"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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=0.5
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 State
# all children of the state
var _sub_states:Array[State] = []
func _state_init():
super._state_init()
# find all children of this state which are states
for child in get_children():
if child is State:
_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:State):
# resolve the target state
var target = transition.resolve_target()
if not target is State:
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(false)
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 State 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(expect_transition:bool = false):
super._state_enter()
# enter all children
for child in _sub_states:
child._state_enter()
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(event:StringName, property_change:bool = false) -> 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(event, property_change)
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
# unless it was a property change
if not property_change:
self.event_received.emit(event)
return true
# otherwise handle it ourselves
# defer to the base class
return super._process_transitions(event, property_change)
func _get_configuration_warnings() -> PackedStringArray:
var warnings = super._get_configuration_warnings()
var child_count = 0
for child in get_children():
if child is State:
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,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="ParallelState">
<g transform="matrix(1.36246,0,0,1.36246,-10.1682,-18.2448)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1,0,0,1,-0.0397943,-0.229175)">
<path d="M4.306,16.267L27.54,16.341" style="fill:none;stroke:rgb(225,142,57);stroke-width:2px;"/>
</g>
<g transform="matrix(1.36246,0,0,1.36246,-10.1682,-2.24482)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

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=0.5
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.13.0"
script="godot_state_charts.gd"

View File

@ -0,0 +1,28 @@
## 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_time: 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:State, 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:State) -> SavedState:
return child_states.get(state.name)

View File

@ -0,0 +1,315 @@
@tool
## This class represents a state that can be either active or inactive.
class_name State
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 = 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_time: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():
# 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):
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):
if transition.delay_seconds > 0:
_queue_transition(transition)
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 indicates whether the state
## is expected to immediately handle a transition after it has been entered.
## In this case the state should not automatically activate a default child state.
## This is to avoid a situation where a state is entered, activates a child then immediately
## exits and activates another child due to a transition.
func _state_enter(expect_transition:bool = false):
# print("state_enter: " + name)
_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()
# run all automatic transitions
for transition in _transitions:
if not transition.has_event and transition.evaluate_guard():
# first match wins
_run_transition(transition)
## Called when the state is exited.
func _state_exit():
# print("state_exit: " + name)
# cancel any pending transitions
_pending_transition = null
_pending_transition_time = 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):
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_time = _pending_transition_time
# 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 = -1 if child_levels == -1 else child_levels - 1
# save all children
for child in get_children():
if child is State 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):
# 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()
# and restore any pending transition
_pending_transition = get_node_or_null(our_saved_state.pending_transition_name) as Transition
_pending_transition_time = our_saved_state.pending_transition_time
# if _pending_transition != null:
# print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_time))
# else:
# print("no pending transition restored")
if 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 State:
child._state_restore(our_saved_state, sub_child_levels)
## Called while the state is active.
func _process(delta:float):
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_time -= delta
# Notify interested parties that currently a transition is pending.
transition_pending.emit(_pending_transition.delay_seconds, max(0, _pending_transition_time))
# if the transition is ready, trigger it
# and clear it.
if _pending_transition_time <= 0:
var transition_to_send = _pending_transition
_pending_transition = null
_pending_transition_time = 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:State):
push_error("State " + name + " cannot handle transitions.")
func _physics_process(delta:float):
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. If the property_change parameter is true
## then only transitions which have no event are processed (eventless transitions/automatic transitions)
func _process_transitions(event:StringName, property_change:bool = false) -> bool:
if not active:
return false
# emit an event received signal if this is not a property change
if not property_change:
event_received.emit(event)
# Walk over all transitions
for transition in _transitions:
# the currently pending transition is not replaced by itself
if transition != _pending_transition \
# automatic transitions are always evaluated
# non-automatic only if this evaluation was not triggered
# by property change AND their event matches their current event
and (not transition.has_event or (not property_change and transition.event == event)) \
# and in every case the guard needs to match
and transition.evaluate_guard():
# print(name + ": consuming event " + event)
# first match wins
_run_transition(transition)
return true
return false
## Queues the transition to be triggered after the delay.
## Executes the transition immediately if the delay is 0.
func _queue_transition(transition:Transition):
# 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_time = transition.delay_seconds
# 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,167 @@
@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")
## 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)
## Flag indicating if this state chart should be tracked by the
## state chart debugger in the editor.
@export var track_in_editor:bool = false
## The root state of the state chart.
var _state:State = 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 events which are still pending resolution.
var _queued_events:Array[StringName] = []
## Flag indicating if the state chart is currently processing an
## event. Until an event is fully processed, new events will be queued
## and then processed later.
var _event_processing_active:bool = false
var _queued_transitions:Array[Dictionary] = []
var _transitions_processing_active:bool = false
var _debugger_remote:DebuggerRemote = null
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 = get_child(0)
if not child is State:
push_error("StateMachine's child must be a State")
return
# initialize the state machine
_state = child as State
_state._state_init()
# enter the state
_state._state_enter.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"):
_debugger_remote = DebuggerRemote.new(self)
## 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_instance_valid(_state):
push_error("StateMachine is not initialized")
return
if _event_processing_active:
# the state chart is currently processing an event
# therefore queue the event and process it later.
_queued_events.append(event)
return
# enable the reentrance lock for event processing
_event_processing_active = true
# first process this event.
event_received.emit(event)
_state._process_transitions(event, false)
# if other events have accumulated while the event was processing
# process them in order now
while _queued_events.size() > 0:
var next_event = _queued_events.pop_front()
event_received.emit(next_event)
_state._process_transitions(next_event, false)
_event_processing_active = 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:State):
# if we are currently inside of a transition, queue it up
if _transitions_processing_active:
_queued_transitions.append({transition : source})
return
# we can only transition away from a currently active state
# if for some reason the state no longer is active, ignore the transition
_do_run_transition(transition, source)
# 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)
## Runs the transition. Used internally by the state chart, do not call this directly.
func _do_run_transition(transition:Transition, source:State):
if source.active:
# Notify interested parties that the transition is about to be taken
transition.taken.emit()
source._handle_transition(transition, source)
else:
_warn_not_active(transition, source)
func _warn_not_active(transition:Transition, source:State):
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.")
## 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:
_expression_properties[name] = value
# run a property change event through the state chart to run automatic transitions
_state._process_transitions(&"", true)
## 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():
_state._state_step()
func _get_configuration_warnings() -> PackedStringArray:
var warnings = []
if get_child_count() != 1:
warnings.append("StateChart must have exactly one child")
else:
var child = get_child(0)
if not child is State:
warnings.append("StateChart's child must be a State")
return warnings

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Statechart">
<g transform="matrix(0.672421,0,0,0.672421,-1.95754,-0.281501)">
<path d="M26.032,10.926C26.032,8.533 24.089,6.591 21.696,6.591L13.025,6.591C10.632,6.591 8.689,8.533 8.689,10.926L8.689,20.247C8.689,22.639 10.632,24.582 13.025,24.582L21.696,24.582C24.089,24.582 26.032,22.639 26.032,20.247L26.032,10.926Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2.97px;"/>
</g>
<g transform="matrix(0.851682,0,0,0.851682,5.75568,7.00772)">
<path d="M26.032,10.926C26.032,8.533 24.089,6.591 21.696,6.591L13.025,6.591C10.632,6.591 8.689,8.533 8.689,10.926L8.689,20.247C8.689,22.639 10.632,24.582 13.025,24.582L21.696,24.582C24.089,24.582 26.032,22.639 26.032,20.247L26.032,10.926Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2.35px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,15 @@
## A guard that checks if a certain state is active.
class_name StateIsActiveGuard
extends Guard
## The state to be checked. When null this guard will return false.
@export_node_path("State") var state: NodePath
func is_satisfied(context_transition:Transition, context_state:State) -> 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 ", context_state.get_path(), " could not be resolved. Verify that the node path is correct.")
return false
return actual_state.active

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Guard">
<path d="M15.997,7.724C21.055,7.683 25.057,10.555 25.057,10.555C25.057,10.555 21.812,25.697 16.003,25.584C10.544,25.477 6.943,10.644 6.943,10.644C6.943,10.644 10.863,7.765 15.997,7.724Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:2px;"/>
<g transform="matrix(1,0,0,1,-2.28281,4.09404)">
<g transform="matrix(16,0,0,16,22.783,17.7302)">
</g>
<text x="13.885px" y="17.73px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:16px;">?</text>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="ToggleLeftRight">
<g transform="matrix(8.62672e-17,1.38138,-1.38138,8.62672e-17,44.8928,-18.2084)">
<path d="M24.764,11.883L28.902,18.886L20.626,18.886L24.764,11.883Z" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(8.2903e-17,-1.38138,1.38138,8.2903e-17,-12.812,50.2084)">
<path d="M24.764,11.883L28.902,18.886L20.626,18.886L24.764,11.883Z" style="fill:rgb(225,142,57);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

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,86 @@
@tool
@icon("transition.svg")
class_name Transition
extends Node
## 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("State") var to:NodePath:
set(value):
to = value
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
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
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.
@export var delay_seconds:float = 0.0:
set(value):
delay_seconds = value
update_configuration_warnings()
## Read-only property that returns true if the transition has an event specified.
var has_event:bool:
get:
return event != null and event.length() > 0
## 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 = get_parent()
if parent_state == null or not (parent_state is State):
push_error("Transitions must be children of states.")
return false
return guard.is_satisfied(self, get_parent())
## Resolves the target state and returns it. If the target state is not found,
## this function will return null.
func resolve_target() -> State:
if to == null or to.is_empty():
return null
var result = get_node_or_null(to)
if result is State:
return result
return null
func _get_configuration_warnings():
var warnings = []
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 State):
warnings.append("Transitions must be children of states.")
return warnings

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Transition">
<g transform="matrix(0.59386,-0.00129228,-0.00129228,0.999996,1.73018,-0.223543)">
<path d="M14.852,23.427C30.642,25.277 38.781,18.528 34.54,10.944" style="fill:none;stroke:rgb(225,142,57);stroke-width:2.44px;"/>
</g>
<g transform="matrix(1.36246,0,0,1.36246,-3.81851,-16.9755)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1.36246,0,0,1.36246,-16.9151,-2.76732)">
<circle cx="19.207" cy="19.263" r="4.404" style="fill:rgb(225,142,57);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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=0.5
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):
_buffer.set_maximum_lines(maximum_lines)
## Adds an item to the history list.
func add_history_entry(frame:int, text:String):
_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):
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):
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):
add_history_entry(frame, "Enter: %s" % name)
## Adds a state exited event to the history list.
func add_state_exited(frame:int, name:StringName):
add_history_entry(frame, "exiT: %s" % name)
## Clears the history.
func clear():
_buffer.clear()
_dirty = true
## Returns the full history as a string.
func get_history_text():
_dirty = false
return _buffer.join()

View File

@ -0,0 +1,371 @@
## 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")
## Constants for the settings
const SETTINGS_ROOT = "godot_state_charts/debugger/"
const SETTINGS_IGNORE_EVENTS = SETTINGS_ROOT + "ignore_events"
const SETTINGS_IGNORE_STATE_CHANGES = SETTINGS_ROOT + "ignore_state_changes"
const SETTINGS_IGNORE_TRANSITIONS = SETTINGS_ROOT + "ignore_transitions"
const SETTINGS_MAXIMUM_LINES = SETTINGS_ROOT + "maximum_lines"
const SETTINGS_SPLIT_OFFSET = 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):
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):
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
DebuggerMessage.settings_updated(_session, path, _ignore_events, _ignore_transitions)
## 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):
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):
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):
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):
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):
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):
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():
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())
## Repaints the tree of the currently selected state chart.
func _repaint_current_chart():
if _current_chart.is_empty():
return
# get the history for this chart and update the history text edit
var history = _chart_histories[_current_chart]
_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_time = DebuggerStateInfo.get_transition_time(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_time])
_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):
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):
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()
## 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()
var chart_history = _chart_histories.get(_current_chart, null)
# ignore the timer if the history edit isn't visible
if not _history_edit.visible or chart_history == null or not chart_history.dirty:
var dirty = false if chart_history == null else chart_history.dirty
return
# fill the history field
_history_edit.text = chart_history.get_history_text()
_history_edit.scroll_vertical = _history_edit.get_line_count() - 1
## Called when the ignore events checkbox is toggled.
func _on_ignore_events_checkbox_toggled(button_pressed:bool):
_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():
DebuggerMessage.settings_updated(_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):
_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):
_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():
DebuggerMessage.settings_updated(_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):
_debounced_maximum_lines = value
## Called when the split container is dragged.
func _on_split_container_dragged(offset:int):
_set_setting(SETTINGS_SPLIT_OFFSET, offset)
## Helper to get the last element of a node path
func _get_node_name(path:NodePath):
return path.get_name(path.get_name_count() - 1)
## Called when the clear button is pressed.
func _on_clear_button_pressed():
_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():
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,95 @@
const MESSAGE_PREFIX = "godot_state_charts"
const STATE_CHART_ADDED_MESSAGE = MESSAGE_PREFIX + ":state_chart_added"
const STATE_CHART_REMOVED_MESSAGE = MESSAGE_PREFIX + ":state_chart_removed"
const STATE_UPDATED_MESSAGE = MESSAGE_PREFIX + ":state_updated"
const STATE_ENTERED_MESSAGE = MESSAGE_PREFIX + ":state_entered"
const STATE_EXITED_MESSAGE = MESSAGE_PREFIX + ":state_exited"
const TRANSITION_PENDING_MESSAGE = MESSAGE_PREFIX + ":transition_pending"
const TRANSITION_TAKEN_MESSAGE = MESSAGE_PREFIX + ":transition_fired"
const STATE_CHART_EVENT_RECEIVED_MESSAGE = MESSAGE_PREFIX + ":state_chart_event_received"
const SETTINGS_UPDATED_MESSAGE = MESSAGE_PREFIX + "_settings_updated"
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
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
EngineDebugger.send_message(STATE_CHART_REMOVED_MESSAGE, [chart.get_path()])
## Sends a state_updated message
static func state_updated(chart:StateChart, state:State) -> void:
if not _can_send():
return
var transition_path = NodePath()
if is_instance_valid(state._pending_transition):
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_time, \
state)]
)
## Sends a state_entered message
static func state_entered(chart:StateChart, state:State) -> void:
if not _can_send():
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:State) -> void:
if not _can_send():
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:State, transition:Transition) -> void:
if not _can_send():
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(transition.resolve_target())])
## Sends an event received message
static func event_received(chart:StateChart, event_name:StringName) -> void:
if not _can_send():
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:State, transition:Transition, pending_transition_time:float) -> void:
if not _can_send():
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_time])
## 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 settings_updated(session, chart:NodePath, ignore_events:bool, ignore_transitions:bool) -> void:
# print("Sending settings updated message: ", SETTINGS_UPDATED_MESSAGE + str(chart) + ":updated")
session.send_message(SETTINGS_UPDATED_MESSAGE + str(chart) + ":updated", [ignore_events, ignore_transitions])

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,104 @@
## 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.
const DebuggerMessage = preload("editor_debugger_message.gd")
# 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):
_state_chart = state_chart
if not is_instance_valid(_state_chart):
push_error("Probable bug: State chart is not valid. Please report this bug.")
_register_settings_updates()
# send initial state chart
DebuggerMessage.state_chart_added(_state_chart)
# prepare signals and send initial state of all states
_prepare()
func _register_settings_updates():
# print("Registering settings updates for ", _state_chart.get_path())
EngineDebugger.register_message_capture(DebuggerMessage.SETTINGS_UPDATED_MESSAGE + str(_state_chart.get_path()), _on_settings_updated)
func _unregister_settings_updates():
# print("Unregistering settings updates for ", _state_chart.get_path())
EngineDebugger.unregister_message_capture(DebuggerMessage.SETTINGS_UPDATED_MESSAGE + str(_state_chart.get_path()))
func _on_settings_updated(key:String, data:Array) -> bool:
_ignore_events = data[0]
_ignore_transitions = data[1]
# print("New settings for " , _state_chart.get_path(), ": ignore_events=", _ignore_events, ", ignore_transitions=", _ignore_transitions)
return true
## 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 State:
_prepare_state(child)
func _prepare_state(state:State):
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 State:
_prepare_state(child)
if child is Transition:
child.taken.connect(_on_transition_taken.bind(state, child))
func _notification(what):
match(what):
Node.NOTIFICATION_ENTER_TREE:
DebuggerMessage.state_chart_added(_state_chart)
_register_settings_updates()
Node.NOTIFICATION_UNPARENTED:
DebuggerMessage.state_chart_removed(_state_chart)
_unregister_settings_updates()
func _on_transition_taken(source:State, 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:State):
DebuggerMessage.state_entered(_state_chart, state)
func _on_state_exited(state:State):
DebuggerMessage.state_exited(_state_chart, state)
func _on_transition_pending(num1, remaining, state:State):
DebuggerMessage.transition_pending(_state_chart, state, state._pending_transition, remaining)

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 time for the pending transition if any.
transition_time:float, \
## The kind of state
state:State \
) -> Array:
return [ \
chart, \
path, \
active, \
transition_pending, \
transition_path, \
transition_time, \
type_for_state(state) ]
## Get the state type for the given state.
static func type_for_state(state:State) -> 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_time(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,107 @@
@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 State
_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_animation_tree_state_pressed():
_create_node(AnimationTreeState, "AnimationTreeState")
func _on_animation_player_state_pressed():
_create_node(AnimationPlayerState, "AnimationPlayerState")
func _on_toggle_sidebar_button_pressed():
sidebar_toggle_requested.emit()

View File

@ -0,0 +1,138 @@
[gd_scene load_steps=10 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://3wqyduuj0fq" path="res://addons/godot_state_charts/animation_tree_state.svg" id="6_8npp8"]
[ext_resource type="Texture2D" uid="uid://chb8tq62aj2b2" path="res://addons/godot_state_charts/transition.svg" id="6_72e5q"]
[ext_resource type="Texture2D" uid="uid://b3m20gsesp4i0" path="res://addons/godot_state_charts/animation_player_state.svg" id="8_ci7iy"]
[ext_resource type="Texture2D" uid="uid://vga3avpb4gyh" path="res://addons/godot_state_charts/toggle_sidebar.svg" id="9_dqcj0"]
[node name="EditorSidebar" type="MarginContainer"]
custom_minimum_size = Vector2(192, 0)
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="AnimationTreeState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "AnimationTreeState"
icon = ExtResource("6_8npp8")
icon_alignment = 1
[node name="AnimationPlayerState" type="Button" parent="VBoxContainer/AddSection/AddGridContainer" groups=["statebutton"]]
layout_mode = 2
size_flags_horizontal = 0
size_flags_vertical = 0
tooltip_text = "AnimationPlayerState"
icon = ExtResource("8_ci7iy")
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/AddSection/AddGridContainer/AnimationTreeState" to="." method="_on_animation_tree_state_pressed"]
[connection signal="pressed" from="VBoxContainer/AddSection/AddGridContainer/AnimationPlayerState" to="." method="_on_animation_player_state_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:Rect2 = _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)
_popup_menu.set_position(popup_pos)
_popup_menu.popup()
func _on_event_selected(index:int):
# 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():
# 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[State] = []
# 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 State:
_connect_signals(child)
func _connect_signals(state:State):
state.state_entered.connect(_on_state_entered.bind(state))
state.state_exited.connect(_on_state_exited.bind(state))
_connected_states.append(state)
# recurse into children
for child in state.get_children():
if child is State:
_connect_signals(child)
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 State:
if child.active:
var state_item = _tree.create_item(parent)
state_item.set_text(0, child.name)
if is_instance_valid(child._pending_transition):
var transition_item = state_item.create_child()
transition_item.set_text(0, ">> %s (%.2f)" % [child._pending_transition.name, child._pending_transition_time])
_collect_active_states(child, state_item)
func _clear_history():
_history_edit.text = ""
_history.clear()
func _on_before_transition(transition:Transition, source:State):
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:State):
if ignore_state_changes:
return
_history.add_state_entered(Engine.get_process_frames(), state.name)
func _on_state_exited(state:State):
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,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<rect id="Artboard1" x="0" y="0" width="32" height="32" style="fill:none;"/>
<g id="Artboard11" serif:id="Artboard1">
<g transform="matrix(1.03705,0,0,1.03705,-0.460588,-0.659827)">
<g id="BG">
<path d="M30.337,8.833C30.337,4.841 27.096,1.601 23.104,1.601L8.64,1.601C4.649,1.601 1.408,4.841 1.408,8.833L1.408,23.297C1.408,27.288 4.649,30.529 8.64,30.529L23.104,30.529C27.096,30.529 30.337,27.288 30.337,23.297L30.337,8.833Z" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.96px;"/>
</g>
</g>
<g id="Debugger">
<g transform="matrix(0.552479,0,0,0.537459,5.72229,1.11992)">
<ellipse cx="18.776" cy="19.192" rx="6.24" ry="6.446" style="fill:rgb(225,142,57);"/>
</g>
<g transform="matrix(1.39721,0,0,0.455926,-8.33235,5.12711)">
<path d="M18.654,7.895C18.654,7.895 18.377,8.063 19.593,7.412C20.808,6.761 20.409,6.236 20.43,5.753" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.66px;"/>
</g>
<g transform="matrix(-1.4426,0,0,0.455926,41.3688,5.12711)">
<path d="M18.654,7.895C18.654,7.895 18.377,8.063 19.593,7.412C20.808,6.761 20.409,6.236 20.43,5.753" style="fill:none;stroke:rgb(225,142,57);stroke-width:0.64px;"/>
</g>
<path d="M21.228,16L24.567,14.899" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
<path d="M15.244,26.459C12.649,26.997 9.855,22.856 9.855,19.653C9.855,17.331 11.047,15.294 12.833,14.159C13.268,15.142 14.158,15.891 15.244,16.152L15.244,26.459ZM16.777,16.188C17.939,15.967 18.9,15.193 19.358,14.159C21.144,15.294 22.336,17.331 22.336,19.653C22.336,22.971 19.868,27.221 16.777,26.368L16.777,16.188Z" style="fill:rgb(225,142,57);stroke:rgb(225,142,57);stroke-width:1.05px;"/>
<path d="M21.228,22.929L24.567,24.168" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
<path d="M21.228,19.57L25.016,19.654" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
<g transform="matrix(-1,0,0,1,32.0145,0.0359697)">
<path d="M21.228,16L24.567,14.899" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
</g>
<g transform="matrix(-1,0,0,1,32.0145,0.0359697)">
<path d="M21.228,22.929L24.567,24.168" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
</g>
<g transform="matrix(-1,0,0,1,32.0145,0.0359697)">
<path d="M21.228,19.57L25.016,19.654" style="fill:none;stroke:rgb(225,142,57);stroke-width:1.05px;"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

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=0.5
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,96 @@
[gd_scene load_steps=2 format=3 uid="uid://bcwkugn6v3oy7"]
[ext_resource type="Script" path="res://addons/godot_state_charts/utilities/state_chart_debugger.gd" id="1_i74os"]
[node name="StateChartDebugger" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_i74os")
[node name="TabContainer" type="TabContainer" parent="."]
layout_mode = 2
[node name="StateChart" type="MarginContainer" parent="TabContainer"]
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="Tree" type="Tree" parent="TabContainer/StateChart"]
unique_name_in_owner = true
layout_mode = 2
scroll_horizontal_enabled = false
scroll_vertical_enabled = false
[node name="History" type="MarginContainer" parent="TabContainer"]
visible = false
layout_mode = 2
theme_override_constants/margin_left = 5
theme_override_constants/margin_top = 5
theme_override_constants/margin_right = 5
theme_override_constants/margin_bottom = 5
[node name="VBoxContainer" type="VBoxContainer" parent="TabContainer/History"]
layout_mode = 2
theme_override_constants/separation = 4
[node name="HistoryEdit" type="TextEdit" parent="TabContainer/History/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_vertical = 3
[node name="HBoxContainer" type="HBoxContainer" parent="TabContainer/History/VBoxContainer"]
layout_mode = 2
[node name="ClearButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Clear"
[node name="CopyToClipboardButton" type="Button" parent="TabContainer/History/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
text = "Copy to Clipboard"
[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)

View File

@ -29,7 +29,7 @@ project/assembly_name="Character Controller"
[editor_plugins]
enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg", "res://addons/qodot/plugin.cfg")
enabled=PackedStringArray("res://addons/format_on_save/plugin.cfg", "res://addons/godot_state_charts/plugin.cfg", "res://addons/qodot/plugin.cfg")
[input]