From 74d19d2003668a7a20b3ff56fbdc69bcedc5c90d Mon Sep 17 00:00:00 2001 From: Jarrod Doyle Date: Fri, 23 Feb 2024 20:37:00 +0000 Subject: [PATCH] Add state charts plugin --- addons/godot_state_charts/LICENSE | 21 + addons/godot_state_charts/all_of_guard.gd | 15 + addons/godot_state_charts/all_of_guard.svg | 20 + .../all_of_guard.svg.import | 38 ++ .../animation_player_state.gd | 62 +++ .../animation_player_state.svg | 20 + .../animation_player_state.svg.import | 38 ++ .../animation_tree_state.gd | 62 +++ .../animation_tree_state.svg | 20 + .../animation_tree_state.svg.import | 38 ++ addons/godot_state_charts/any_of_guard.gd | 15 + addons/godot_state_charts/any_of_guard.svg | 23 ++ .../any_of_guard.svg.import | 38 ++ addons/godot_state_charts/atomic_state.gd | 25 ++ addons/godot_state_charts/atomic_state.svg | 17 + .../atomic_state.svg.import | 38 ++ addons/godot_state_charts/compound_state.gd | 245 ++++++++++++ addons/godot_state_charts/compound_state.svg | 24 ++ .../compound_state.svg.import | 38 ++ .../csharp/CompoundState.cs | 52 +++ .../godot_state_charts/csharp/NodeWrapper.cs | 33 ++ addons/godot_state_charts/csharp/State.cs | 93 +++++ .../godot_state_charts/csharp/StateChart.cs | 82 ++++ .../csharp/StateChartDebugger.cs | 53 +++ .../godot_state_charts/csharp/Transition.cs | 19 + addons/godot_state_charts/expression_guard.gd | 56 +++ .../godot_state_charts/expression_guard.svg | 20 + .../expression_guard.svg.import | 38 ++ .../godot_state_charts/godot_state_charts.gd | 127 ++++++ addons/godot_state_charts/guard.gd | 7 + addons/godot_state_charts/history_state.gd | 58 +++ addons/godot_state_charts/history_state.svg | 23 ++ .../history_state.svg.import | 38 ++ addons/godot_state_charts/not_guard.gd | 13 + addons/godot_state_charts/not_guard.svg | 20 + .../godot_state_charts/not_guard.svg.import | 38 ++ addons/godot_state_charts/parallel_state.gd | 116 ++++++ addons/godot_state_charts/parallel_state.svg | 23 ++ .../parallel_state.svg.import | 38 ++ addons/godot_state_charts/plugin.cfg | 7 + addons/godot_state_charts/saved_state.gd | 28 ++ addons/godot_state_charts/state.gd | 315 +++++++++++++++ addons/godot_state_charts/state_chart.gd | 167 ++++++++ addons/godot_state_charts/state_chart.svg | 20 + .../godot_state_charts/state_chart.svg.import | 38 ++ .../state_is_active_guard.gd | 15 + .../state_is_active_guard.svg | 20 + .../state_is_active_guard.svg.import | 38 ++ addons/godot_state_charts/toggle_sidebar.svg | 20 + .../toggle_sidebar.svg.import | 37 ++ addons/godot_state_charts/transition.gd | 86 ++++ addons/godot_state_charts/transition.svg | 23 ++ .../godot_state_charts/transition.svg.import | 38 ++ .../utilities/debugger_history.gd | 58 +++ .../editor_debugger/editor_debugger.gd | 371 ++++++++++++++++++ .../editor_debugger/editor_debugger.tscn | 120 ++++++ .../editor_debugger_message.gd | 95 +++++ .../editor_debugger/editor_debugger_plugin.gd | 53 +++ .../editor_debugger/editor_debugger_remote.gd | 104 +++++ .../editor_debugger_state_info.gd | 104 +++++ .../utilities/editor_sidebar.gd | 107 +++++ .../utilities/editor_sidebar.tscn | 138 +++++++ .../utilities/event_editor/event_editor.gd | 100 +++++ .../event_editor/event_inspector_plugin.gd | 29 ++ .../event_refactor/event_refactor.gd | 52 +++ .../event_refactor/event_refactor.tscn | 55 +++ .../utilities/ring_buffer.gd | 54 +++ .../utilities/state_chart_debugger.gd | 280 +++++++++++++ .../utilities/state_chart_debugger.svg | 36 ++ .../utilities/state_chart_debugger.svg.import | 38 ++ .../utilities/state_chart_debugger.tscn | 96 +++++ .../utilities/state_chart_util.gd | 49 +++ project.godot | 2 +- 73 files changed, 4536 insertions(+), 1 deletion(-) create mode 100644 addons/godot_state_charts/LICENSE create mode 100644 addons/godot_state_charts/all_of_guard.gd create mode 100644 addons/godot_state_charts/all_of_guard.svg create mode 100644 addons/godot_state_charts/all_of_guard.svg.import create mode 100644 addons/godot_state_charts/animation_player_state.gd create mode 100644 addons/godot_state_charts/animation_player_state.svg create mode 100644 addons/godot_state_charts/animation_player_state.svg.import create mode 100644 addons/godot_state_charts/animation_tree_state.gd create mode 100644 addons/godot_state_charts/animation_tree_state.svg create mode 100644 addons/godot_state_charts/animation_tree_state.svg.import create mode 100644 addons/godot_state_charts/any_of_guard.gd create mode 100644 addons/godot_state_charts/any_of_guard.svg create mode 100644 addons/godot_state_charts/any_of_guard.svg.import create mode 100644 addons/godot_state_charts/atomic_state.gd create mode 100644 addons/godot_state_charts/atomic_state.svg create mode 100644 addons/godot_state_charts/atomic_state.svg.import create mode 100644 addons/godot_state_charts/compound_state.gd create mode 100644 addons/godot_state_charts/compound_state.svg create mode 100644 addons/godot_state_charts/compound_state.svg.import create mode 100644 addons/godot_state_charts/csharp/CompoundState.cs create mode 100644 addons/godot_state_charts/csharp/NodeWrapper.cs create mode 100644 addons/godot_state_charts/csharp/State.cs create mode 100644 addons/godot_state_charts/csharp/StateChart.cs create mode 100644 addons/godot_state_charts/csharp/StateChartDebugger.cs create mode 100644 addons/godot_state_charts/csharp/Transition.cs create mode 100644 addons/godot_state_charts/expression_guard.gd create mode 100644 addons/godot_state_charts/expression_guard.svg create mode 100644 addons/godot_state_charts/expression_guard.svg.import create mode 100644 addons/godot_state_charts/godot_state_charts.gd create mode 100644 addons/godot_state_charts/guard.gd create mode 100644 addons/godot_state_charts/history_state.gd create mode 100644 addons/godot_state_charts/history_state.svg create mode 100644 addons/godot_state_charts/history_state.svg.import create mode 100644 addons/godot_state_charts/not_guard.gd create mode 100644 addons/godot_state_charts/not_guard.svg create mode 100644 addons/godot_state_charts/not_guard.svg.import create mode 100644 addons/godot_state_charts/parallel_state.gd create mode 100644 addons/godot_state_charts/parallel_state.svg create mode 100644 addons/godot_state_charts/parallel_state.svg.import create mode 100644 addons/godot_state_charts/plugin.cfg create mode 100644 addons/godot_state_charts/saved_state.gd create mode 100644 addons/godot_state_charts/state.gd create mode 100644 addons/godot_state_charts/state_chart.gd create mode 100644 addons/godot_state_charts/state_chart.svg create mode 100644 addons/godot_state_charts/state_chart.svg.import create mode 100644 addons/godot_state_charts/state_is_active_guard.gd create mode 100644 addons/godot_state_charts/state_is_active_guard.svg create mode 100644 addons/godot_state_charts/state_is_active_guard.svg.import create mode 100644 addons/godot_state_charts/toggle_sidebar.svg create mode 100644 addons/godot_state_charts/toggle_sidebar.svg.import create mode 100644 addons/godot_state_charts/transition.gd create mode 100644 addons/godot_state_charts/transition.svg create mode 100644 addons/godot_state_charts/transition.svg.import create mode 100644 addons/godot_state_charts/utilities/debugger_history.gd create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger.gd create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger.tscn create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger_message.gd create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger_plugin.gd create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger_remote.gd create mode 100644 addons/godot_state_charts/utilities/editor_debugger/editor_debugger_state_info.gd create mode 100644 addons/godot_state_charts/utilities/editor_sidebar.gd create mode 100644 addons/godot_state_charts/utilities/editor_sidebar.tscn create mode 100644 addons/godot_state_charts/utilities/event_editor/event_editor.gd create mode 100644 addons/godot_state_charts/utilities/event_editor/event_inspector_plugin.gd create mode 100644 addons/godot_state_charts/utilities/event_refactor/event_refactor.gd create mode 100644 addons/godot_state_charts/utilities/event_refactor/event_refactor.tscn create mode 100644 addons/godot_state_charts/utilities/ring_buffer.gd create mode 100644 addons/godot_state_charts/utilities/state_chart_debugger.gd create mode 100644 addons/godot_state_charts/utilities/state_chart_debugger.svg create mode 100644 addons/godot_state_charts/utilities/state_chart_debugger.svg.import create mode 100644 addons/godot_state_charts/utilities/state_chart_debugger.tscn create mode 100644 addons/godot_state_charts/utilities/state_chart_util.gd diff --git a/addons/godot_state_charts/LICENSE b/addons/godot_state_charts/LICENSE new file mode 100644 index 0000000..4542e9f --- /dev/null +++ b/addons/godot_state_charts/LICENSE @@ -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. diff --git a/addons/godot_state_charts/all_of_guard.gd b/addons/godot_state_charts/all_of_guard.gd new file mode 100644 index 0000000..9765122 --- /dev/null +++ b/addons/godot_state_charts/all_of_guard.gd @@ -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 diff --git a/addons/godot_state_charts/all_of_guard.svg b/addons/godot_state_charts/all_of_guard.svg new file mode 100644 index 0000000..76b10c4 --- /dev/null +++ b/addons/godot_state_charts/all_of_guard.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/all_of_guard.svg.import b/addons/godot_state_charts/all_of_guard.svg.import new file mode 100644 index 0000000..15a2f94 --- /dev/null +++ b/addons/godot_state_charts/all_of_guard.svg.import @@ -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 diff --git a/addons/godot_state_charts/animation_player_state.gd b/addons/godot_state_charts/animation_player_state.gd new file mode 100644 index 0000000..a5f5120 --- /dev/null +++ b/addons/godot_state_charts/animation_player_state.gd @@ -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 diff --git a/addons/godot_state_charts/animation_player_state.svg b/addons/godot_state_charts/animation_player_state.svg new file mode 100644 index 0000000..79b16db --- /dev/null +++ b/addons/godot_state_charts/animation_player_state.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/animation_player_state.svg.import b/addons/godot_state_charts/animation_player_state.svg.import new file mode 100644 index 0000000..dbe9b93 --- /dev/null +++ b/addons/godot_state_charts/animation_player_state.svg.import @@ -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 diff --git a/addons/godot_state_charts/animation_tree_state.gd b/addons/godot_state_charts/animation_tree_state.gd new file mode 100644 index 0000000..8af7186 --- /dev/null +++ b/addons/godot_state_charts/animation_tree_state.gd @@ -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 diff --git a/addons/godot_state_charts/animation_tree_state.svg b/addons/godot_state_charts/animation_tree_state.svg new file mode 100644 index 0000000..432dac9 --- /dev/null +++ b/addons/godot_state_charts/animation_tree_state.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/animation_tree_state.svg.import b/addons/godot_state_charts/animation_tree_state.svg.import new file mode 100644 index 0000000..920e561 --- /dev/null +++ b/addons/godot_state_charts/animation_tree_state.svg.import @@ -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 diff --git a/addons/godot_state_charts/any_of_guard.gd b/addons/godot_state_charts/any_of_guard.gd new file mode 100644 index 0000000..c560824 --- /dev/null +++ b/addons/godot_state_charts/any_of_guard.gd @@ -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 diff --git a/addons/godot_state_charts/any_of_guard.svg b/addons/godot_state_charts/any_of_guard.svg new file mode 100644 index 0000000..2bb1cfc --- /dev/null +++ b/addons/godot_state_charts/any_of_guard.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/any_of_guard.svg.import b/addons/godot_state_charts/any_of_guard.svg.import new file mode 100644 index 0000000..61a320c --- /dev/null +++ b/addons/godot_state_charts/any_of_guard.svg.import @@ -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 diff --git a/addons/godot_state_charts/atomic_state.gd b/addons/godot_state_charts/atomic_state.gd new file mode 100644 index 0000000..fb8fa16 --- /dev/null +++ b/addons/godot_state_charts/atomic_state.gd @@ -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 diff --git a/addons/godot_state_charts/atomic_state.svg b/addons/godot_state_charts/atomic_state.svg new file mode 100644 index 0000000..85aba01 --- /dev/null +++ b/addons/godot_state_charts/atomic_state.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/atomic_state.svg.import b/addons/godot_state_charts/atomic_state.svg.import new file mode 100644 index 0000000..9c1d9bc --- /dev/null +++ b/addons/godot_state_charts/atomic_state.svg.import @@ -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 diff --git a/addons/godot_state_charts/compound_state.gd b/addons/godot_state_charts/compound_state.gd new file mode 100644 index 0000000..2b713fd --- /dev/null +++ b/addons/godot_state_charts/compound_state.gd @@ -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 diff --git a/addons/godot_state_charts/compound_state.svg b/addons/godot_state_charts/compound_state.svg new file mode 100644 index 0000000..5a3c507 --- /dev/null +++ b/addons/godot_state_charts/compound_state.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/godot_state_charts/compound_state.svg.import b/addons/godot_state_charts/compound_state.svg.import new file mode 100644 index 0000000..3e5d01f --- /dev/null +++ b/addons/godot_state_charts/compound_state.svg.import @@ -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 diff --git a/addons/godot_state_charts/csharp/CompoundState.cs b/addons/godot_state_charts/csharp/CompoundState.cs new file mode 100644 index 0000000..45aa034 --- /dev/null +++ b/addons/godot_state_charts/csharp/CompoundState.cs @@ -0,0 +1,52 @@ + + +// ReSharper disable once CheckNamespace +namespace GodotStateCharts +{ + using System; + using Godot; + + /// + /// Wrapper around the compound state node. + /// + public class CompoundState : State + { + + private CompoundState(Node wrapped) : base(wrapped) + { + } + + /// + /// 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#. + /// + /// the node that is the state + /// a State wrapper. + /// ArgumentException if the node is not a state. + public new static CompoundState Of(Node state) + { + if (state.GetScript().As