Add state charts plugin
|
@ -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.
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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 |
|
@ -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
|
|
@ -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)
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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"
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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 |
|
@ -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
|
|
@ -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()
|
|
@ -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)
|
|
@ -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"]
|
|
@ -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])
|
|
@ -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)
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -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"]
|
|
@ -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
|
|
@ -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
|
|
@ -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 |
|
@ -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
|
|
@ -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"]
|
|
@ -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)
|
|
@ -29,7 +29,7 @@ project/assembly_name="Character Controller"
|
||||||
|
|
||||||
[editor_plugins]
|
[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]
|
[input]
|
||||||
|
|
||||||
|
|