From f5999da4122188be6a453f6c191a34564ca7ed26 Mon Sep 17 00:00:00 2001 From: Haze Weathers Date: Wed, 21 Aug 2024 21:02:35 -0400 Subject: [PATCH] Basic player controls and aiming --- 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 | 265 ++++++ addons/godot_state_charts/compound_state.svg | 24 + .../compound_state.svg.import | 38 + .../csharp/CompoundState.cs | 66 ++ .../godot_state_charts/csharp/NodeWrapper.cs | 49 + .../godot_state_charts/csharp/StateChart.cs | 129 +++ .../csharp/StateChartDebugger.cs | 65 ++ .../csharp/StateChartState.cs | 156 +++ .../godot_state_charts/csharp/Transition.cs | 38 + addons/godot_state_charts/debug_util.gd | 8 + addons/godot_state_charts/expression_guard.gd | 38 + .../godot_state_charts/expression_guard.svg | 20 + .../expression_guard.svg.import | 38 + addons/godot_state_charts/expression_util.gd | 22 + .../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 | 31 + addons/godot_state_charts/state_chart.gd | 229 +++++ addons/godot_state_charts/state_chart.svg | 20 + .../godot_state_charts/state_chart.svg.import | 38 + .../godot_state_charts/state_chart_state.gd | 322 +++++++ .../state_is_active_guard.gd | 17 + .../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 | 142 +++ addons/godot_state_charts/transition.svg | 23 + .../godot_state_charts/transition.svg.import | 38 + .../utilities/debugger_history.gd | 58 ++ .../editor_debugger/editor_debugger.gd | 386 ++++++++ .../editor_debugger/editor_debugger.tscn | 120 +++ .../editor_debugger_message.gd | 120 +++ .../editor_debugger/editor_debugger_plugin.gd | 53 ++ .../editor_debugger/editor_debugger_remote.gd | 110 +++ .../editor_debugger_state_info.gd | 104 ++ .../utilities/editor_sidebar.gd | 107 +++ .../utilities/editor_sidebar.tscn | 137 +++ .../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 + icon.svg.import | 2 +- objects/interactable.gd | 17 + objects/player/player.gd | 119 +++ objects/player/player.tscn | 889 ++++++++++++++++++ objects/weapons/projectiles/brick.tscn | 18 + objects/weapons/projectiles/bullet.gd | 9 + objects/weapons/projectiles/bullet.tscn | 22 + objects/weapons/projectiles/throwable.gd | 12 + objects/weapons/projectiles/throwable.tscn | 16 + project.godot | 61 ++ scripts/debug_visualization/draw_basis.gd | 17 + test_scene.tscn | 42 +- textures/player/arm_back.png | Bin 0 -> 589 bytes textures/player/arm_back.png.import | 34 + textures/player/arm_back_hand.png | Bin 0 -> 5488 bytes textures/player/arm_back_hand.png.import | 34 + textures/player/arm_front.png | Bin 0 -> 589 bytes textures/player/arm_front.png.import | 34 + textures/player/arm_front_hand.png | Bin 0 -> 592 bytes textures/player/arm_front_hand.png.import | 34 + textures/player/body.png | Bin 0 -> 6451 bytes textures/player/body.png.import | 34 + textures/player/foot_back.png | Bin 0 -> 599 bytes textures/player/foot_back.png.import | 34 + textures/player/foot_front.png | Bin 0 -> 599 bytes textures/player/foot_front.png.import | 34 + textures/player/head.png | Bin 0 -> 639 bytes textures/player/head.png.import | 34 + textures/player/leg_back.png | Bin 0 -> 574 bytes textures/player/leg_back.png.import | 34 + textures/player/leg_front.png | Bin 0 -> 574 bytes textures/player/leg_front.png.import | 34 + textures/player/skirt.png | Bin 0 -> 605 bytes textures/player/skirt.png.import | 34 + textures/player/torso.png | Bin 0 -> 623 bytes textures/player/torso.png.import | 34 + textures/player/weapons/pistol.png | Bin 0 -> 733 bytes textures/player/weapons/pistol.png.import | 34 + textures/projectiles/brick.png | Bin 0 -> 729 bytes textures/projectiles/brick.png.import | 34 + 114 files changed, 6611 insertions(+), 2 deletions(-) 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/StateChart.cs create mode 100644 addons/godot_state_charts/csharp/StateChartDebugger.cs create mode 100644 addons/godot_state_charts/csharp/StateChartState.cs create mode 100644 addons/godot_state_charts/csharp/Transition.cs create mode 100644 addons/godot_state_charts/debug_util.gd 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/expression_util.gd 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_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_chart_state.gd 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 create mode 100644 objects/interactable.gd create mode 100644 objects/player/player.gd create mode 100644 objects/player/player.tscn create mode 100644 objects/weapons/projectiles/brick.tscn create mode 100644 objects/weapons/projectiles/bullet.gd create mode 100644 objects/weapons/projectiles/bullet.tscn create mode 100644 objects/weapons/projectiles/throwable.gd create mode 100644 objects/weapons/projectiles/throwable.tscn create mode 100644 scripts/debug_visualization/draw_basis.gd create mode 100644 textures/player/arm_back.png create mode 100644 textures/player/arm_back.png.import create mode 100644 textures/player/arm_back_hand.png create mode 100644 textures/player/arm_back_hand.png.import create mode 100644 textures/player/arm_front.png create mode 100644 textures/player/arm_front.png.import create mode 100644 textures/player/arm_front_hand.png create mode 100644 textures/player/arm_front_hand.png.import create mode 100644 textures/player/body.png create mode 100644 textures/player/body.png.import create mode 100644 textures/player/foot_back.png create mode 100644 textures/player/foot_back.png.import create mode 100644 textures/player/foot_front.png create mode 100644 textures/player/foot_front.png.import create mode 100644 textures/player/head.png create mode 100644 textures/player/head.png.import create mode 100644 textures/player/leg_back.png create mode 100644 textures/player/leg_back.png.import create mode 100644 textures/player/leg_front.png create mode 100644 textures/player/leg_front.png.import create mode 100644 textures/player/skirt.png create mode 100644 textures/player/skirt.png.import create mode 100644 textures/player/torso.png create mode 100644 textures/player/torso.png.import create mode 100644 textures/player/weapons/pistol.png create mode 100644 textures/player/weapons/pistol.png.import create mode 100644 textures/projectiles/brick.png create mode 100644 textures/projectiles/brick.png.import 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..df4b6a5 --- /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:StateChartState) -> 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..e4f3053 --- /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:StateChartState) -> 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..3571095 --- /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 StateChartState + +func _handle_transition(transition:Transition, source:StateChartState): + # resolve the target state + var target = transition.resolve_target() + if not target is StateChartState: + push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.") + return + # atomic states cannot transition, so we need to ask the parent + # ask the parent + get_parent()._handle_transition(transition, source) + + +func _get_configuration_warnings() -> PackedStringArray : + var warnings = super._get_configuration_warnings() + # check if we have any child nodes which are not transitions + for child in get_children(): + if child is StateChartState: + warnings.append("Atomic states cannot have child states. These will be ignored.") + break + return warnings 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..ba13153 --- /dev/null +++ b/addons/godot_state_charts/compound_state.gd @@ -0,0 +1,265 @@ +@tool +@icon("compound_state.svg") +## A compound state is a state that has multiple sub-states of which exactly one can +## be active at any given time. +class_name CompoundState +extends StateChartState + +## Called when a child state is entered. +signal child_state_entered() + +## Called when a child state is exited. +signal child_state_exited() + +## The initial state which should be activated when this state is activated. +@export_node_path("StateChartState") var initial_state:NodePath: + get: + return initial_state + set(value): + initial_state = value + update_configuration_warnings() + + +## The currently active substate. +var _active_state:StateChartState = null + +## The initial state +@onready var _initial_state:StateChartState = get_node_or_null(initial_state) + +## The history states of this compound state. +var _history_states:Array[HistoryState] = [] +## Whether any of the history states needs a deep history. +var _needs_deep_history:bool = false + +func _init() -> void: + # subscribe to the child_entered_tree signal in edit mode so we can + # automatically set the initial state when a new sub-state is added + if Engine.is_editor_hint(): + child_entered_tree.connect( + func(child:Node): + # when a child is added in the editor and the child is a state + # and we don't have an initial state yet, set the initial state + # to the newly added child + if child is StateChartState and initial_state.is_empty(): + # the newly added node may have a random name now, + # so we need to defer the call to build a node path + # to the next frame, so the editor has time to rename + # the node to its final name + (func(): initial_state = get_path_to(child)).call_deferred() + ) + + +func _state_init(): + super._state_init() + + # check if we have any history states + for child in get_children(): + if child is HistoryState: + var child_as_history_state:HistoryState = child as HistoryState + _history_states.append(child_as_history_state) + # remember if any of the history states needs a deep history + _needs_deep_history = _needs_deep_history or child_as_history_state.deep + + # initialize all substates. find all children of type State and call _state_init on them. + for child in get_children(): + if child is StateChartState: + var child_as_state:StateChartState = child as StateChartState + child_as_state._state_init() + child_as_state.state_entered.connect(func(): child_state_entered.emit()) + child_as_state.state_exited.connect(func(): child_state_exited.emit()) + +func _state_enter(expect_transition:bool = false): + super._state_enter() + # activate the initial state _unless_ one of these are true + # - we expect a transition because we are entering this state just to activate child states further down + # - we already have an active state because entering the state triggered an immediate transition to a child state + # - we are no longer active becasue entering the state triggered an immediate transition to some other state + if not expect_transition and not is_instance_valid(_active_state) and _state_active: + if _initial_state != null: + if _initial_state is HistoryState: + _restore_history_state(_initial_state) + else: + _active_state = _initial_state + _active_state._state_enter() + else: + push_error("No initial state set for state '" + name + "'.") + +func _state_step(): + super._state_step() + if _active_state != null: + _active_state._state_step() + +func _state_save(saved_state:SavedState, child_levels:int = -1): + super._state_save(saved_state, child_levels) + + # in addition save all history states, as they are never active and normally would not be saved + var parent = saved_state.get_substate_or_null(self) + if parent == null: + push_error("Probably a bug: The state of '" + name + "' was not saved.") + return + + for history_state in _history_states: + history_state._state_save(parent, child_levels) + +func _state_restore(saved_state:SavedState, child_levels:int = -1): + super._state_restore(saved_state, child_levels) + + # in addition check if we are now active and if so determine the current active state + if active: + # find the currently active child + for child in get_children(): + if child is StateChartState and child.active: + _active_state = child + break + +func _state_exit(): + # if we have any history states, we need to save the current active state + if _history_states.size() > 0: + var saved_state = SavedState.new() + # we save the entire hierarchy if any of the history states needs a deep history + # otherwise we only save this level. This way we can save memory and processing time + _state_save(saved_state, -1 if _needs_deep_history else 1) + + # now save the saved state in all history states + for history_state in _history_states: + # when saving history it's ok when we save deep history in a history state that doesn't need it + # because at restore time we will use the state's deep flag to determine if we need to restore + # the entire hierarchy or just this level. This way we don't need multiple copies of the same + # state hierarchy. + history_state.history = saved_state + + # deactivate the current state + if _active_state != null: + _active_state._state_exit() + _active_state = null + super._state_exit() + + +func _process_transitions(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:StateChartState): + # print("CompoundState._handle_transition: " + name + " from " + source.name + " to " + str(transition.to)) + # resolve the target state + var target = transition.resolve_target() + if not target is StateChartState: + push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.") + return + + # the target state can be + # 0. this state. in this case exit this state and re-enter it. This can happen when + # a child state transfers to its parent state. + # 1. a direct child of this state. this is the easy case in which + # we will deactivate the current _active_state and activate the target + # 2. a descendant of this state. in this case we find the direct child which + # is the ancestor of the target state, activate it and then ask it to perform + # the transition. + # 3. no descendant of this state. in this case, we ask our parent state to + # perform the transition + + if target == self: + # exit this state and re-enter it + _state_exit() + _state_enter(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: + _restore_history_state(target) + 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 StateChartState and child.is_ancestor_of(target): + # found it. + # change active state if necessary + if _active_state != child: + if is_instance_valid(_active_state): + _active_state._state_exit() + + _active_state = child + # 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 _restore_history_state(target:HistoryState): + # print("Target is history state, restoring saved state.") + var saved_state = target.history + if saved_state != null: + # restore the saved state + _state_restore(saved_state, -1 if target.deep else 1) + return + # print("No history saved so far, activating default state.") + # if we don't have history, we just activate the default state + var default_state = target.get_node_or_null(target.default_state) + if is_instance_valid(default_state): + _active_state = default_state + _active_state._state_enter() + return + else: + push_error("The default state '" + str(target.default_state) + "' of the history state '" + target.name + "' cannot be found.") + return + + + + +func _get_configuration_warnings() -> PackedStringArray: + var warnings = super._get_configuration_warnings() + + # count the amount of child states + var child_count = 0 + for child in get_children(): + if child is StateChartState: + child_count += 1 + + if child_count < 1: + warnings.append("Compound states should have at least one child state.") + + elif child_count < 2: + warnings.append("Compound states with only one child state are not very useful. Consider adding more child states or removing this compound state.") + + var the_initial_state = get_node_or_null(initial_state) + + if not is_instance_valid(the_initial_state): + warnings.append("Initial state could not be resolved, is the path correct?") + + elif the_initial_state.get_parent() != self: + warnings.append("Initial state must be a direct child of this compound state.") + + return warnings 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..fdef872 --- /dev/null +++ b/addons/godot_state_charts/csharp/CompoundState.cs @@ -0,0 +1,66 @@ + + +// ReSharper disable once CheckNamespace +namespace GodotStateCharts +{ + using System; + using Godot; + + /// + /// Wrapper around the compound state node. + /// + public class CompoundState : StateChartState + { + /// + /// Called when a child state is entered. + /// + public event Action ChildStateEntered + { + add => Wrapped.Connect(SignalName.ChildStateEntered, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.ChildStateEntered, Callable.From(value)); + } + + /// + /// Called when a child state is exited. + /// + public event Action ChildStateExited + { + add => Wrapped.Connect(SignalName.ChildStateExited, Callable.From(value)); + remove => Wrapped.Disconnect(SignalName.ChildStateExited, Callable.From(value)); + } + + private CompoundState(Node wrapped) : base(wrapped) + { + } + + /// + /// 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