├── addons
└── visual_fsm
│ ├── resources
│ ├── screenshots
│ │ ├── .gdignore
│ │ └── demo.gif
│ ├── icons
│ │ ├── visual_fsm.png
│ │ ├── icon_add.svg
│ │ ├── icon_collision_shape_2d.svg
│ │ ├── icon_joypad.svg
│ │ ├── icon_script.svg
│ │ ├── icon_add.svg.import
│ │ ├── flow-chart.png.import
│ │ ├── icon_timer.svg.import
│ │ ├── visual_fsm.png.import
│ │ ├── visual_fsm.svg.import
│ │ ├── icon_joypad.svg.import
│ │ ├── icon_script.svg.import
│ │ ├── icon_collision_shape_2d.svg.import
│ │ ├── close-cross-symbol-in-a-circle.svg.import
│ │ ├── close-cross-symbol-in-a-circle.svg
│ │ ├── icon_timer.svg
│ │ └── visual_fsm.svg
│ ├── trigger_template.txt
│ ├── state_template.txt
│ ├── vfsm_state_base.gd
│ └── vfsm_trigger_base.gd
│ ├── demos
│ ├── icon.png
│ ├── simple_character_controller
│ │ └── simple_character_controller.tscn
│ ├── icon.png.import
│ ├── simple_traffic_lights
│ │ ├── traffic_lights.gd
│ │ └── simple_traffic_lights.tscn
│ └── simple_ai_character
│ │ ├── vfsm_simple_ai_jumpter.gd
│ │ └── simple_ai_character.tscn
│ ├── attributions.txt
│ ├── vfsm_singleton.gd
│ ├── plugin.cfg
│ ├── fsm
│ ├── vfsm_trigger.gd
│ ├── vfsm_trigger_timer.gd
│ ├── vfsm_trigger_action.gd
│ ├── vfsm_trigger_script.gd
│ ├── vfsm_state.gd
│ └── vfsm.gd
│ ├── editor
│ ├── vfsm_editor.gd
│ ├── dialogs
│ │ ├── vfsm_set_timer_dialog.gd
│ │ ├── vfsm_new_state_dialog.gd
│ │ ├── vfsm_set_input_trigger_dialog.gd
│ │ └── vfsm_new_scripted_trigger_dialog.gd
│ ├── vfsm_start_graph_node.tscn
│ ├── vfsm_trigger_graph_slot.gd
│ ├── vfsm_state_graph_node.tscn
│ ├── vfsm_state_graph_node.gd
│ ├── vfsm_trigger_graph_slot.tscn
│ ├── vfsm_graph_edit.gd
│ └── vfsm_editor.tscn
│ ├── visual_fsm_plugin.gd
│ └── visual_fsm.gd
├── .gitignore
├── default_env.tres
├── LICENSE
├── README.md
└── project.godot
/addons/visual_fsm/resources/screenshots/.gdignore:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imberny/VisualFSM/HEAD/addons/visual_fsm/demos/icon.png
--------------------------------------------------------------------------------
/addons/visual_fsm/attributions.txt:
--------------------------------------------------------------------------------
1 | TODO: add these to plugin description
2 | Icons made by Freepik from www.flaticon.com
3 |
--------------------------------------------------------------------------------
/addons/visual_fsm/vfsm_singleton.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMSingleton
3 | extends Node
4 |
5 |
6 | signal edit_custom_script(custom_script)
7 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/visual_fsm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imberny/VisualFSM/HEAD/addons/visual_fsm/resources/icons/visual_fsm.png
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/screenshots/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/imberny/VisualFSM/HEAD/addons/visual_fsm/resources/screenshots/demo.gif
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Godot-specific ignores
3 | .import/
4 | export.cfg
5 | export_presets.cfg
6 |
7 | # Mono-specific ignores
8 | .mono/
9 | data_*/
10 |
11 | build/
12 | .vscode/
13 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_add.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/default_env.tres:
--------------------------------------------------------------------------------
1 | [gd_resource type="Environment" load_steps=2 format=2]
2 |
3 | [sub_resource type="ProceduralSky" id=1]
4 |
5 | [resource]
6 | background_mode = 2
7 | background_sky = SubResource( 1 )
8 |
--------------------------------------------------------------------------------
/addons/visual_fsm/plugin.cfg:
--------------------------------------------------------------------------------
1 | [plugin]
2 |
3 | name="visual_fsm"
4 | description="A finite state machine plugin with a GUI editor."
5 | author="Bernard Cloutier"
6 | version="0.1"
7 | script="visual_fsm_plugin.gd"
8 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm_trigger.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMTrigger
3 | extends Resource
4 |
5 | export(int) var vfsm_id: int
6 | export(String) var name: String
7 |
8 |
9 | func enter() -> void:
10 | pass
11 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/trigger_template.txt:
--------------------------------------------------------------------------------
1 | # Trigger: %s
2 | extends VFSMTriggerBase
3 |
4 |
5 | func enter() -> void:
6 | pass
7 |
8 |
9 | func is_triggered(_object, _delta: float) -> bool:
10 | return false
11 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_editor.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends MarginContainer
3 |
4 |
5 | func edit(visual_fsm) -> void:
6 | if not is_inside_tree():
7 | yield(self, "tree_entered")
8 | $VFSMGraphEdit.edit(visual_fsm.fsm)
9 |
10 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/state_template.txt:
--------------------------------------------------------------------------------
1 | extends VFSMStateBase
2 |
3 |
4 | func enter() -> void:
5 | pass
6 |
7 |
8 | func update(_object, _delta: float) -> void:
9 | pass
10 |
11 |
12 | func exit() -> void:
13 | pass
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/simple_character_controller/simple_character_controller.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene format=2]
2 |
3 | [node name="World" type="Node2D"]
4 |
5 | [node name="Character" type="Node2D" parent="."]
6 |
7 | [node name="KinematicBody2D" type="KinematicBody2D" parent="Character"]
8 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_collision_shape_2d.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/vfsm_state_base.gd:
--------------------------------------------------------------------------------
1 | class_name VFSMStateBase
2 | extends Object
3 |
4 | #signal internal_event(params)
5 |
6 | var name: String
7 |
8 |
9 | func enter() -> void:
10 | pass
11 |
12 | func update(_object, _delta: float) -> void:
13 | pass
14 |
15 | func exit() -> void:
16 | pass
17 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/vfsm_trigger_base.gd:
--------------------------------------------------------------------------------
1 | class_name VFSMTriggerBase
2 | extends Object
3 |
4 | var name: String
5 |
6 |
7 | func enter() -> void:
8 | pass
9 |
10 |
11 | func is_triggered(_object, _delta: float) -> bool:
12 | assert(false, "VisualFSM: Method \"is_triggered\" is unimplemented.")
13 | return false
14 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm_trigger_timer.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMTriggerTimer
3 | extends VFSMTrigger
4 |
5 | export(float) var duration
6 |
7 | var _timer: float
8 |
9 |
10 | func enter() -> void:
11 | _timer = 0
12 |
13 |
14 | func is_over(delta: float) -> bool:
15 | _timer += delta
16 | if duration < _timer:
17 | return true
18 | return false
19 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm_trigger_action.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMTriggerAction
3 | extends VFSMTrigger
4 |
5 | export(Array) var action_list: Array
6 |
7 |
8 | func is_trigger_action(input: InputEvent) -> bool:
9 | for action in action_list:
10 | if input.is_action_pressed(action):
11 | return true
12 | return false
13 |
14 |
15 | # add down, released, duration held...
16 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_joypad.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_script.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/icon.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon.png-767b769eb113a798bd6f964752bee20e.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/demos/icon.png"
13 | dest_files=[ "res://.import/icon.png-767b769eb113a798bd6f964752bee20e.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_add.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon_add.svg-097d5e2a219b8a7aa5f1a16e1f8d79b9.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/icon_add.svg"
13 | dest_files=[ "res://.import/icon_add.svg-097d5e2a219b8a7aa5f1a16e1f8d79b9.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/flow-chart.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/flow-chart.png-609953648d983ac2dd51bc1b8a601f36.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/flow-chart.png"
13 | dest_files=[ "res://.import/flow-chart.png-609953648d983ac2dd51bc1b8a601f36.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_timer.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon_timer.svg-48f817c7999cf61b9132289c60f5f560.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/icon_timer.svg"
13 | dest_files=[ "res://.import/icon_timer.svg-48f817c7999cf61b9132289c60f5f560.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/visual_fsm.png.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/visual_fsm.png-8a2d4eb993c736f1323285a78044b909.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/visual_fsm.png"
13 | dest_files=[ "res://.import/visual_fsm.png-8a2d4eb993c736f1323285a78044b909.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/visual_fsm.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/visual_fsm.svg-44282a1c79609a165da9099145615f7f.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/visual_fsm.svg"
13 | dest_files=[ "res://.import/visual_fsm.svg-44282a1c79609a165da9099145615f7f.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_joypad.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon_joypad.svg-dadb895e7b460ea458fe0cf038c6b5ee.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/icon_joypad.svg"
13 | dest_files=[ "res://.import/icon_joypad.svg-dadb895e7b460ea458fe0cf038c6b5ee.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_script.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon_script.svg-b3dc3f4675f8b149f5b130acd4dc6687.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/icon_script.svg"
13 | dest_files=[ "res://.import/icon_script.svg-b3dc3f4675f8b149f5b130acd4dc6687.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm_trigger_script.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMTriggerScript
3 | extends VFSMTrigger
4 |
5 | export(GDScript) var custom_script: GDScript setget _set_custom_script
6 |
7 | var custom_script_instance: VFSMTriggerBase
8 |
9 | func enter() -> void:
10 | custom_script_instance.enter()
11 |
12 |
13 | func is_triggered(object: Node, delta: float) -> bool:
14 | return custom_script_instance.is_triggered(object, delta)
15 |
16 |
17 | func _set_custom_script(value: GDScript) -> void:
18 | custom_script = value
19 | custom_script.reload(true)
20 | custom_script_instance = custom_script.new() as VFSMTriggerBase
21 | assert(custom_script_instance, "VisualFSM: Script in event \"%s\" must extend VFSMTriggerBase" % self.name)
22 | custom_script_instance.name = self.name
23 |
24 |
25 | func _notification(what):
26 | if what == NOTIFICATION_PREDELETE:
27 | custom_script_instance.free()
28 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_collision_shape_2d.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/icon_collision_shape_2d.svg-fe7cdf1aa309db8c659ca3ea76a883c8.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/icon_collision_shape_2d.svg"
13 | dest_files=[ "res://.import/icon_collision_shape_2d.svg-fe7cdf1aa309db8c659ca3ea76a883c8.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/close-cross-symbol-in-a-circle.svg.import:
--------------------------------------------------------------------------------
1 | [remap]
2 |
3 | importer="texture"
4 | type="StreamTexture"
5 | path="res://.import/close-cross-symbol-in-a-circle.svg-1a459415114d2f8d6e74f5a67a109f24.stex"
6 | metadata={
7 | "vram_texture": false
8 | }
9 |
10 | [deps]
11 |
12 | source_file="res://addons/visual_fsm/resources/icons/close-cross-symbol-in-a-circle.svg"
13 | dest_files=[ "res://.import/close-cross-symbol-in-a-circle.svg-1a459415114d2f8d6e74f5a67a109f24.stex" ]
14 |
15 | [params]
16 |
17 | compress/mode=0
18 | compress/lossy_quality=0.7
19 | compress/hdr_mode=0
20 | compress/bptc_ldr=0
21 | compress/normal_map=0
22 | flags/repeat=0
23 | flags/filter=true
24 | flags/mipmaps=false
25 | flags/anisotropic=false
26 | flags/srgb=2
27 | process/fix_alpha_border=true
28 | process/premult_alpha=false
29 | process/HDR_as_SRGB=false
30 | process/invert_color=false
31 | stream=false
32 | size_limit=0
33 | detect_3d=true
34 | svg/scale=1.0
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Bernard Cloutier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/simple_traffic_lights/traffic_lights.gd:
--------------------------------------------------------------------------------
1 | class_name VFSMDemoTrafficLightsController
2 | extends Node2D
3 |
4 |
5 | var current_state_name: String setget _set_current_state_name
6 | var available_actions: Array setget _set_available_actions
7 |
8 |
9 | func green() -> void:
10 | $TrafficLights/Green/Cover.visible = false
11 | $TrafficLights/Yellow/Cover.visible = true
12 | $TrafficLights/Red/Cover.visible = true
13 |
14 |
15 | func yellow() -> void:
16 | $TrafficLights/Green/Cover.visible = true
17 | $TrafficLights/Yellow/Cover.visible = false
18 | $TrafficLights/Red/Cover.visible = true
19 |
20 |
21 | func red() -> void:
22 | $TrafficLights/Green/Cover.visible = true
23 | $TrafficLights/Yellow/Cover.visible = true
24 | $TrafficLights/Red/Cover.visible = false
25 |
26 |
27 | func _set_current_state_name(value) -> void:
28 | if has_node("StateContainer/State"):
29 | $StateContainer/State.text = value
30 |
31 |
32 | func _set_available_actions(value) -> void:
33 | available_actions = value.duplicate()
34 | $Actions/Actions.text = ""
35 | if available_actions.empty():
36 | return
37 |
38 | for action in available_actions:
39 | $Actions/Actions.text = action + ", "
40 | $Actions/Actions.text = $Actions/Actions.text.substr(0, len($Actions/Actions.text) - 2)
41 |
42 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/close-cross-symbol-in-a-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
45 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/dialogs/vfsm_set_timer_dialog.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends AcceptDialog
3 |
4 | var duration: float
5 |
6 | onready var _duration_field := $Margins/Content/Duration
7 |
8 | var _context: GDScriptFunctionState
9 |
10 |
11 | func open(timer_duration: float, context: GDScriptFunctionState) -> void:
12 | if _context: # opened from another slot
13 | _context.resume(false)
14 | _context = context
15 |
16 | show()
17 | duration = timer_duration
18 | _duration_field.text = str(timer_duration)
19 | _duration_field.caret_position = len(_duration_field.text)
20 | _duration_field.grab_focus()
21 |
22 |
23 | func close() -> void:
24 | if _context:
25 | _context.resume(false)
26 | _context = null
27 | hide()
28 |
29 |
30 | func _unhandled_input(event) -> void:
31 | if not visible:
32 | return
33 | if event.is_action("ui_accept"):
34 | emit_signal("confirmed")
35 | hide()
36 |
37 |
38 | func _on_Duration_text_changed(new_text: String) -> void:
39 | if new_text.ends_with('.'):
40 | return
41 | duration = max(0, float(new_text))
42 | var caret_position = _duration_field.caret_position
43 | if 0 == duration:
44 | _duration_field.text = ""
45 | else:
46 | _duration_field.text = str(duration)
47 | _duration_field.caret_position = caret_position
48 |
49 |
50 | func _on_confirmed() -> void:
51 | duration = float(_duration_field.text)
52 | _context.resume(true)
53 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_start_graph_node.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=2 format=2]
2 |
3 | [sub_resource type="StyleBoxFlat" id=1]
4 | bg_color = Color( 0.0941176, 0.0941176, 0.0941176, 1 )
5 |
6 | [node name="VFSMStartGraphNode" type="GraphNode"]
7 | margin_right = 100.0
8 | margin_bottom = 44.0
9 | rect_min_size = Vector2( 100, 0 )
10 | size_flags_horizontal = 9
11 | size_flags_vertical = 9
12 | custom_constants/port_offset = 0
13 | custom_constants/separation = 8
14 | slot/0/left_enabled = false
15 | slot/0/left_type = 0
16 | slot/0/left_color = Color( 1, 0.5, 0.31, 1 )
17 | slot/0/right_enabled = true
18 | slot/0/right_type = 0
19 | slot/0/right_color = Color( 1, 1, 1, 1 )
20 | __meta__ = {
21 | "_edit_use_anchors_": false
22 | }
23 |
24 | [node name="BottomPanel" type="PanelContainer" parent="."]
25 | margin_left = 16.0
26 | margin_top = 24.0
27 | margin_right = 84.0
28 | margin_bottom = 48.0
29 | size_flags_horizontal = 3
30 | size_flags_vertical = 3
31 | custom_styles/panel = SubResource( 1 )
32 |
33 | [node name="MarginContainer" type="MarginContainer" parent="BottomPanel"]
34 | margin_right = 68.0
35 | margin_bottom = 24.0
36 | custom_constants/margin_right = 5
37 | custom_constants/margin_top = 5
38 | custom_constants/margin_left = 5
39 | custom_constants/margin_bottom = 5
40 |
41 | [node name="Label" type="Label" parent="BottomPanel/MarginContainer"]
42 | margin_left = 5.0
43 | margin_top = 5.0
44 | margin_right = 63.0
45 | margin_bottom = 19.0
46 | text = "Start"
47 | align = 1
48 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/icon_timer.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/simple_ai_character/vfsm_simple_ai_jumpter.gd:
--------------------------------------------------------------------------------
1 | class_name VFSMDemoSimpleAIJumper
2 | extends KinematicBody2D
3 |
4 | export(float) var speed = 10
5 | export(float) var jump_speed = 50
6 | export(float) var gravity = 9.8
7 |
8 | onready var right_gap_detector: RayCast2D= $RightGapDetector
9 | onready var left_gap_detector: RayCast2D= $LeftGapDetector
10 | onready var left_bottom_raycast: RayCast2D = $LeftBottom
11 | onready var right_bottom_raycast: RayCast2D = $RightBottom
12 | onready var left_middle_raycast: RayCast2D = $LeftMiddle
13 | onready var right_middle_raycast: RayCast2D = $RightMiddle
14 |
15 | var _velocity: Vector2
16 |
17 |
18 | func move_x(dir: float) -> void:
19 | _velocity.x = clamp(dir, -1, 1) * speed
20 |
21 |
22 | func jump() -> void:
23 | if is_on_floor():
24 | _velocity += jump_speed * Vector2.UP
25 |
26 |
27 | func is_gap_in_front() -> bool:
28 | if _velocity.x > 0:
29 | return not right_gap_detector.is_colliding()
30 | else:
31 | return not left_gap_detector.is_colliding()
32 |
33 |
34 | func is_impassable_in_front() -> bool:
35 | if _velocity.x > 0:
36 | return right_bottom_raycast.is_colliding() and right_middle_raycast.is_colliding()
37 | else:
38 | return left_bottom_raycast.is_colliding() and left_middle_raycast.is_colliding()
39 |
40 |
41 | func is_jumpable_in_front() -> bool:
42 | if _velocity.x > 0:
43 | return right_bottom_raycast.is_colliding() and not right_middle_raycast.is_colliding()
44 | else:
45 | return left_bottom_raycast.is_colliding() and not left_middle_raycast.is_colliding()
46 |
47 |
48 | func _ready() -> void:
49 | _velocity = Vector2()
50 |
51 |
52 | func _physics_process(delta) -> void:
53 | _velocity += gravity * delta * Vector2.DOWN
54 | _velocity = move_and_slide(_velocity, Vector2.UP)
55 |
--------------------------------------------------------------------------------
/addons/visual_fsm/visual_fsm_plugin.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends EditorPlugin
3 |
4 | const CONTROL_LABEL := "Finite State Machine"
5 | const FSM_TYPE_NAME := "VisualFSM"
6 |
7 | var _fsm_editor: Control
8 | var _tool_button: ToolButton
9 | var _fsm_script := preload("visual_fsm.gd")
10 | var _fsm_singleton: VFSMSingleton = preload("vfsm_singleton.gd").new()
11 | var _current_fsm_node
12 |
13 |
14 | func _enter_tree() -> void:
15 | add_custom_type(FSM_TYPE_NAME, "Node", _fsm_script, preload("resources/icons/visual_fsm.png"))
16 |
17 | yield(get_tree(), "idle_frame")
18 | _fsm_singleton.name = "VFSMSingleton"
19 | _fsm_singleton.connect("edit_custom_script", self, "_on_edit_custom_script")
20 | get_tree().root.add_child(_fsm_singleton)
21 |
22 | _fsm_editor = preload("editor/vfsm_editor.tscn").instance()
23 | _tool_button = add_control_to_bottom_panel(_fsm_editor, CONTROL_LABEL)
24 | _tool_button.hide()
25 | var selected_nodes := get_editor_interface().get_selection().get_selected_nodes()
26 | if selected_nodes.size() > 0:
27 | make_visible(handles(selected_nodes[0]))
28 |
29 |
30 | func _exit_tree() -> void:
31 | remove_custom_type(FSM_TYPE_NAME)
32 | _fsm_singleton.disconnect("edit_custom_script", self, "_on_edit_custom_script")
33 | remove_control_from_bottom_panel(_fsm_editor)
34 |
35 | get_tree().root.call_deferred("remove_child", _fsm_singleton)
36 | # using queue_free causes memory leaks. Bug?
37 | _fsm_editor.free()
38 |
39 |
40 | func make_visible(visible) -> void:
41 | if visible:
42 | _tool_button.show()
43 | make_bottom_panel_item_visible(_fsm_editor)
44 | _fsm_editor.set_process(true)
45 | else:
46 | if _fsm_editor.visible:
47 | hide_bottom_panel()
48 | _tool_button.hide()
49 | _fsm_editor.set_process(false)
50 |
51 |
52 | func handles(object) -> bool:
53 | return object is _fsm_script
54 |
55 |
56 | func edit(object) -> void:
57 | _fsm_editor.edit(object)
58 | _current_fsm_node = object
59 |
60 |
61 | func _on_edit_custom_script(custom_script: GDScript) -> void:
62 | assert(_current_fsm_node, "VisualFSM: Current VisualFSM node is null.")
63 | get_editor_interface().edit_resource(custom_script)
64 | # inspect current fsm node, otherwise we lose the fsm panel
65 | get_editor_interface().inspect_object(_current_fsm_node)
66 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/dialogs/vfsm_new_state_dialog.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends ConfirmationDialog
3 |
4 | signal state_name_request(name)
5 |
6 | onready var _state_name_field := $Margins/Content/StateName
7 | onready var _name_status := $Margins/Content/Prompt/Margin/Name
8 |
9 | var state_name: String setget _set_state_name
10 | var _context: GDScriptFunctionState = null
11 |
12 |
13 | func _ready() -> void:
14 | get_cancel().connect("pressed", self, "_on_canceled")
15 |
16 |
17 | func open(context: GDScriptFunctionState) -> void:
18 | if _context:
19 | _context.resume(false)
20 | _context = context
21 |
22 | show()
23 | self.state_name = ""
24 | _state_name_field.grab_focus()
25 |
26 |
27 | func close() -> void:
28 | if _context:
29 | _context.resume(false)
30 | _context = null
31 | hide()
32 |
33 |
34 | func deny_name_request(name: String) -> void:
35 | _name_status.text = "A state with this name already exists."
36 | _name_status.add_color_override("font_color", Color.red)
37 | get_ok().disabled = true
38 |
39 |
40 | func approve_name_request(name: String) -> void:
41 | self.state_name = name
42 |
43 |
44 | func _unhandled_input(event) -> void:
45 | if not _context:
46 | return
47 |
48 | if event is InputEventKey and event.scancode == KEY_ENTER and not get_ok().disabled:
49 | emit_signal("confirmed")
50 | hide()
51 |
52 |
53 | func _set_state_name(value: String) -> void:
54 | state_name = value
55 | var caret_position = _state_name_field.caret_position
56 | _state_name_field.text = value
57 | _state_name_field.caret_position = caret_position
58 | _validate()
59 |
60 |
61 | func _validate() -> void:
62 | var ok_button = get_ok()
63 | var invalid_state_name: bool = self.state_name.empty()
64 | if invalid_state_name:
65 | _name_status.text = "State must have a name."
66 | _name_status.add_color_override("font_color", Color.red)
67 | else:
68 | _name_status.text = "State name is available."
69 | _name_status.add_color_override("font_color", Color.green)
70 |
71 | ok_button.disabled = invalid_state_name
72 |
73 |
74 | func _on_confirmed() -> void:
75 | _context.resume(true)
76 | _context = null
77 |
78 |
79 | func _on_canceled() -> void:
80 | _context.resume(false)
81 | _context = null
82 |
83 |
84 | func _on_StateName_text_changed(text: String) -> void:
85 | emit_signal("state_name_request", text)
86 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/dialogs/vfsm_set_input_trigger_dialog.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends AcceptDialog
3 |
4 | onready var actions := $Margins/Content/ActionContainer/Margins/Actions
5 | onready var invalid_panel := $Margins/Content/ValidationPanel
6 | onready var _filter_field := $Margins/Content/Header/FilterMargins/Filter
7 | onready var _action_list := []
8 |
9 | var _context: GDScriptFunctionState
10 |
11 |
12 | func open(trigger_actions: Array, context: GDScriptFunctionState) -> void:
13 | if _context:
14 | _context.resume(false)
15 | _context = context
16 |
17 | show()
18 | _filter_field.clear()
19 | _filter_field.grab_focus()
20 |
21 | for action in actions.get_children():
22 | actions.remove_child(action)
23 |
24 | for input_action in InputMap.get_actions():
25 | var action := CheckBox.new()
26 | action.connect("toggled", self, "_on_Action_toggled")
27 | action.text = input_action
28 | action.pressed = input_action in trigger_actions
29 | actions.add_child(action)
30 |
31 | _validate()
32 |
33 |
34 | func close() -> void:
35 | if _context:
36 | _context.resume(false)
37 | _context = null
38 | hide()
39 |
40 |
41 | func get_selected_actions() -> Array:
42 | return self._action_list.duplicate()
43 |
44 |
45 | func _unhandled_input(trigger) -> void:
46 | if not visible:
47 | return
48 | if trigger is InputEventKey and trigger.scancode == KEY_ENTER:
49 | if not get_ok().disabled:
50 | emit_signal("confirmed")
51 | hide()
52 |
53 |
54 | func _validate() -> void:
55 | invalid_panel.visible = _action_list.empty()
56 | get_ok().disabled = _action_list.empty()
57 |
58 |
59 | func _on_Action_toggled(_pressed) -> void:
60 | # no way to know which item, so rebuild list
61 | _action_list.clear()
62 | for action in actions.get_children():
63 | var checkbox := action as CheckBox
64 | if checkbox.pressed:
65 | _action_list.push_back(checkbox.text)
66 | _validate()
67 |
68 |
69 | func _on_Filter_text_changed(new_text: String) -> void:
70 | for action in actions.get_children():
71 | var checkbox := action as CheckBox
72 | checkbox.visible = new_text.empty() or -1 < checkbox.text.find(new_text)
73 |
74 |
75 | func _on_ClearButton_pressed():
76 | _action_list.clear()
77 | for action in actions.get_children():
78 | var checkbox := action as CheckBox
79 | checkbox.pressed = false
80 |
81 |
82 | func _on_confirmed():
83 | _context.resume(true)
84 | _context = null
85 |
--------------------------------------------------------------------------------
/addons/visual_fsm/visual_fsm.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends Node
3 |
4 | onready var _parent_node = get_parent()
5 | var fsm: VFSM
6 | var _current_state: VFSMState
7 |
8 | func _ready():
9 | if Engine.editor_hint:
10 | set_process(false)
11 | set_physics_process(false)
12 | set_process_input(false)
13 | if not self.fsm:
14 | self.fsm = VFSM.new()
15 | else:
16 | _current_state = fsm.get_start_state()
17 | assert(_current_state, "VisualFSM: %s's finite state machine doesn't point to a starting state." % _parent_node.name)
18 | if _current_state:
19 | _current_state.enter()
20 |
21 |
22 | func _unhandled_input(event: InputEvent) -> void:
23 | var next_state: VFSMState
24 | for trigger_id in _current_state.trigger_ids:
25 | var trigger := fsm.get_trigger(trigger_id)
26 | var go_to_next_trigger := false
27 | if trigger is VFSMTriggerAction:
28 | go_to_next_trigger = trigger.is_trigger_action(event)
29 |
30 | if go_to_next_trigger:
31 | next_state = fsm.get_next_state(_current_state, trigger)
32 | break
33 |
34 | if next_state:
35 | _current_state.exit()
36 |
37 | _current_state = next_state
38 | _current_state.enter()
39 | for trigger_id in _current_state.trigger_ids:
40 | fsm.get_trigger(trigger_id).enter()
41 |
42 |
43 | func _process(delta) -> void:
44 | _current_state.update(_parent_node, delta)
45 |
46 | var next_state: VFSMState
47 | for trigger_id in _current_state.trigger_ids:
48 | var trigger := fsm.get_trigger(trigger_id)
49 | var go_to_next_trigger := false
50 | if trigger is VFSMTriggerTimer:
51 | go_to_next_trigger = trigger.is_over(delta)
52 | elif trigger is VFSMTriggerScript:
53 | go_to_next_trigger = trigger.is_triggered(_parent_node, delta)
54 |
55 | if go_to_next_trigger:
56 | next_state = fsm.get_next_state(_current_state, trigger)
57 | break
58 |
59 | if next_state:
60 | _current_state.exit()
61 |
62 | _current_state = next_state
63 | _current_state.enter()
64 | for trigger_id in _current_state.trigger_ids:
65 | fsm.get_trigger(trigger_id).enter()
66 |
67 |
68 | func _set(property, value):
69 | match property:
70 | "finite_state_machine":
71 | fsm = value
72 | return false
73 |
74 |
75 | func _get(property):
76 | match property:
77 | "finite_state_machine":
78 | return fsm
79 | return null
80 |
81 |
82 | func _get_property_list() -> Array:
83 | return [
84 | {
85 | "name": "finite_state_machine",
86 | "type": TYPE_OBJECT,
87 | "hint": PROPERTY_HINT_RESOURCE_TYPE,
88 | "hint_string": "VFSM",
89 | "usage": PROPERTY_USAGE_NOEDITOR
90 | }
91 | ]
92 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/dialogs/vfsm_new_scripted_trigger_dialog.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends ConfirmationDialog
3 |
4 | signal new_trigger_created(trigger)
5 | signal trigger_name_request(name)
6 |
7 | export(Texture) var script_icon
8 |
9 | var trigger_name: String setget _set_trigger_name, _get_trigger_name
10 |
11 | onready var _trigger_name := $Margins/Content/TriggerName
12 | onready var _name_status := $Margins/Content/Prompt/Margin/VBox/Name
13 |
14 | var _context: GDScriptFunctionState
15 |
16 |
17 | func _ready() -> void:
18 | get_ok().text = "Create trigger"
19 | get_cancel().connect("pressed", self, "_on_canceled")
20 | _validate()
21 |
22 |
23 | func open(context: GDScriptFunctionState) -> void:
24 | if _context:
25 | _context.resume(false)
26 | _context = context
27 |
28 | show()
29 | self.trigger_name = ""
30 | _trigger_name.grab_focus()
31 |
32 |
33 | func close() -> void:
34 | _context = null
35 | hide()
36 |
37 |
38 | func deny_name_request(name: String) -> void:
39 | _name_status.text = "An trigger with this name already exists."
40 | _name_status.add_color_override("font_color", Color.red)
41 |
42 |
43 | func approve_name_request(name: String) -> void:
44 | self.state_name = name
45 |
46 |
47 | func _unhandled_input(trigger: InputEvent) -> void:
48 | if not visible:
49 | return
50 |
51 | if trigger is InputEventKey and trigger.scancode == KEY_ENTER and not get_ok().disabled:
52 | emit_signal("confirmed")
53 | hide()
54 |
55 |
56 | func _set_trigger_name(value: String) -> void:
57 | var caret_pos = _trigger_name.caret_position
58 | _trigger_name.text = value
59 | _trigger_name.caret_position = caret_pos
60 | _validate()
61 |
62 |
63 | func _get_trigger_name() -> String:
64 | return _trigger_name.text
65 |
66 |
67 | func _validate() -> void:
68 | var ok_button = get_ok()
69 | var invalid_trigger_name: bool = self.trigger_name.empty()
70 | if invalid_trigger_name:
71 | _name_status.text = "Trigger must have a name."
72 | _name_status.add_color_override("font_color", Color.red)
73 | else:
74 | _name_status.text = "Trigger name is available."
75 | _name_status.add_color_override("font_color", Color.green)
76 |
77 | ok_button.disabled = invalid_trigger_name
78 |
79 |
80 | func _on_about_to_show() -> void:
81 | _trigger_name.grab_focus()
82 |
83 |
84 | func _on_confirmed() -> void:
85 | _context.resume(true)
86 | close()
87 |
88 |
89 | func _on_canceled() -> void:
90 | _context.resume(false)
91 | close()
92 |
93 |
94 | func _on_TriggerName_text_changed(text: String) -> void:
95 | emit_signal("trigger_name_request", text)
96 |
97 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Warning - I'm not working on this anymore!
2 |
3 | I'm not using Godot anymore and further work on this plugin is pretty unlikely. Might get back on it if Godot 4.0 manages to pull me back, but don't count on it! Feel free to clone/fork/modify.
4 |
5 | ---
6 |
7 | # VisualFSM
8 |
9 | A visual finite state machine editor plugin for Godot.
10 | 
11 |
12 | ## Features
13 |
14 | - Visualize and edit your finite state machines with this intuitive editor.
15 | - Trigger based transitions to simplify your states' logic.
16 | - Use prebuilt triggers, or script your own.
17 | - Minimal setup required. Just add a VisualFSM node below the node you want controlled and start building your graph!
18 |
19 | ## Tutorial
20 | * Add a VisualFSM node as a child to the node you want to control.
21 | * Click on the VisualFSM node: a panel opens in the bottom dock.
22 | * Drag a connection from the start node to create your first state.
23 | * Give it a meaningful name and press enter.
24 | * Click the script icon to edit this state's script. The "object" parameter corresponds to the parent node.
25 | * Click on the "Add trigger" dropdown and select the trigger type.
26 | * A new connection is added to the right of the trigger: drag it to connect to a new state.
27 | * If you added a timer trigger, click on the timer icon to adjust the duration.
28 | * If you added an input action trigger, click on the icon to select the actions to react to.
29 | * If you added a scripted trigger, click on the script icon to edit it. Return true in `is_triggered` when the transition should occur.
30 |
31 |
32 | ## Why another FSM plugin for Godot?
33 |
34 | As your finite state machine (FSM) grows, managing transitions between states becomes a pain. This is especially true when states are responsible for triggering their own transitions. This approach violates the [single responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle), since on top of their own control logic they must correctly select the next state. You can easily end up with a messy structure that makes bugs harder to find than they should.
35 |
36 | This plugin's main goal is to visually edit your FSM and identify potential problems at a glance. States and their transitions are easily editable in a GraphEdit based editor. Furthermore, your states and transitions are decoupled to allow for a cleaner structure. Each tick, the current state's triggers are visited and the first to fulfill its condition causes the FSM to switch to the corresponding next state.
37 |
38 | ## Links
39 |
40 | Primer on finite state machines and their uses in games: https://gameprogrammingpatterns.com/state.html
41 |
42 | The finite state machine implementation was inspired by [this article](https://www.codeproject.com/Articles/1087619/State-Machine-Design-in-Cplusplus-2).
43 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_trigger_graph_slot.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMTriggerGraphSlot
3 | extends PanelContainer
4 |
5 | signal close_request(trigger_slot)
6 |
7 | onready var _timer_duration_field := $Timer/DurationMargins/Duration
8 | onready var _action_title_field := $Action/ActionMargins/ActionLabel
9 | onready var _script_title_field := $Script/TitleMargins/Title
10 |
11 | var timer_duration_dialog: AcceptDialog
12 | var input_action_dialog: AcceptDialog
13 |
14 | var trigger: VFSMTrigger setget _set_trigger
15 |
16 |
17 | func _set_trigger(value: VFSMTrigger) -> void:
18 | trigger = value
19 |
20 | if trigger is VFSMTriggerAction:
21 | $Action.visible = true
22 | _update_action_label()
23 | elif trigger is VFSMTriggerTimer:
24 | $Timer.visible = true
25 | _timer_duration_field.text = str(trigger.duration)
26 | elif trigger is VFSMTriggerScript:
27 | $Script.visible = true;
28 | _script_title_field.text = trigger.name
29 |
30 |
31 | func _on_CloseButton_pressed() -> void:
32 | emit_signal("close_request", self)
33 |
34 |
35 | func _on_Script_pressed() -> void:
36 | assert(self.trigger is VFSMTriggerScript,
37 | "VisualFSM: Trigger \"%s\" should be of type VFSMTriggerScript" % self.trigger.name)
38 | $"/root/VFSMSingleton".emit_signal("edit_custom_script", self.trigger.custom_script)
39 |
40 |
41 | func try_set_timer_duration() -> void:
42 | if yield():
43 | self.trigger.duration = timer_duration_dialog.duration
44 | _timer_duration_field.text = str(timer_duration_dialog.duration)
45 |
46 |
47 | func _on_Timer_pressed() -> void:
48 | assert(self.trigger is VFSMTriggerTimer,
49 | "VisualFSM: Trigger \"%s\" should be of type VFSMTriggerTimer" % self.trigger.name)
50 | var mouse_pos = get_global_mouse_position()
51 | timer_duration_dialog.rect_position = mouse_pos - timer_duration_dialog.rect_size / 2
52 | timer_duration_dialog.open(trigger.duration, try_set_timer_duration())
53 |
54 |
55 | func _update_action_label() -> void:
56 | var action_list = self.trigger.action_list
57 | if action_list.empty():
58 | _action_title_field.text = "No action"
59 | else:
60 | _action_title_field.text = action_list[0]
61 | if action_list.size() > 1:
62 | _action_title_field.text += " +%s" % str(action_list.size() - 1)
63 |
64 |
65 | func try_set_action_list() -> void:
66 | if yield():
67 | self.trigger.action_list = input_action_dialog.get_selected_actions()
68 | else:
69 | self.trigger.action_list = []
70 | _update_action_label()
71 |
72 |
73 | func _on_Action_pressed():
74 | assert(self.trigger is VFSMTriggerAction,
75 | "VisualFSM: Trigger \"%s\" should be of type VFSMTriggerAction" % self.trigger.name)
76 | var mouse_pos = get_global_mouse_position()
77 | input_action_dialog.rect_position = mouse_pos - input_action_dialog.rect_size + Vector2(80, 50)
78 | input_action_dialog.open(trigger.action_list, try_set_action_list())
79 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm_state.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMState
3 | extends Resource
4 |
5 | export(int) var vfsm_id: int
6 | export(String) var name: String
7 | export(Vector2) var position: Vector2
8 | export(Array) var trigger_ids: Array
9 | export(GDScript) var custom_script: GDScript setget _set_custom_script
10 |
11 | const STATE_TEMPLATE_PATH := "res://addons/visual_fsm/resources/state_template.txt"
12 | const SCRIPT_FIRST_LINE_TEMPLATE = "# State: %s\n"
13 |
14 | var _state_custom_script_template: String
15 | var custom_script_instance: VFSMStateBase
16 |
17 |
18 | func _read_from_file(path: String) -> String:
19 | var f = File.new()
20 | var err = f.open(path, File.READ)
21 | if err != OK:
22 | push_warning("Could not open file \"%s\", error code: %s" % [path, err])
23 | return ""
24 | var content = f.get_as_text()
25 | f.close()
26 | return content
27 |
28 |
29 | func _init():
30 | _state_custom_script_template = _read_from_file(STATE_TEMPLATE_PATH)
31 |
32 | func has_trigger(vfsm_id: int) -> bool:
33 | return trigger_ids.find(vfsm_id) > -1
34 |
35 |
36 | func add_trigger(trigger: VFSMTrigger) -> void:
37 | trigger_ids.push_back(trigger.vfsm_id)
38 | _changed()
39 |
40 |
41 | func remove_trigger(trigger: VFSMTrigger) -> void:
42 | trigger_ids.erase(trigger.vfsm_id)
43 | _changed()
44 |
45 |
46 | func get_trigger_id_from_index(index: int) -> int:
47 | return trigger_ids[index]
48 |
49 |
50 | func get_trigger_index(trigger: VFSMTrigger) -> int:
51 | for i in range(len(trigger_ids)):
52 | if trigger.vfsm_id == trigger_ids[i]:
53 | return i
54 | return -1
55 |
56 |
57 | func enter() -> void:
58 | custom_script_instance.enter()
59 |
60 |
61 | func update(object, delta: float) -> void:
62 | custom_script_instance.update(object, delta)
63 |
64 |
65 | func exit() -> void:
66 | custom_script_instance.exit()
67 |
68 |
69 | func rename(new_name: String) -> void:
70 | var old_name = self.name
71 | self.name = new_name
72 |
73 |
74 | func new_script() -> void:
75 | assert(not self.name.empty())
76 | var custom_script := GDScript.new()
77 | custom_script.source_code = (SCRIPT_FIRST_LINE_TEMPLATE % self.name) + _state_custom_script_template
78 | self.custom_script = custom_script
79 |
80 |
81 | func _set_custom_script(value: GDScript) -> void:
82 | custom_script = value
83 | custom_script.reload(true)
84 | custom_script.connect("script_changed", self, "_init_script")
85 | _init_script()
86 |
87 |
88 | func _init_script() -> void:
89 | custom_script_instance = self.custom_script.new() as VFSMStateBase
90 | assert(custom_script_instance, "VisualFSM: Script in state \"%s\" must extend VFSMStateBase" % self.name)
91 | custom_script_instance.name = self.name
92 |
93 |
94 | func _changed() -> void:
95 | call_deferred("emit_signal", "changed")
96 |
97 |
98 | func _notification(what):
99 | if what == NOTIFICATION_PREDELETE:
100 | if custom_script_instance:
101 | custom_script_instance.free()
102 |
--------------------------------------------------------------------------------
/addons/visual_fsm/resources/icons/visual_fsm.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/project.godot:
--------------------------------------------------------------------------------
1 | ; Engine configuration file.
2 | ; It's best edited using the editor UI and not directly,
3 | ; since the parameters that go here are not all obvious.
4 | ;
5 | ; Format:
6 | ; [section] ; section goes between []
7 | ; param=value ; assign values to parameters
8 |
9 | config_version=4
10 |
11 | _global_script_classes=[ {
12 | "base": "Resource",
13 | "class": "VFSM",
14 | "language": "GDScript",
15 | "path": "res://addons/visual_fsm/fsm/vfsm.gd"
16 | }, {
17 | "base": "KinematicBody2D",
18 | "class": "VFSMDemoSimpleAIJumper",
19 | "language": "GDScript",
20 | "path": "res://addons/visual_fsm/demos/simple_ai_character/vfsm_simple_ai_jumpter.gd"
21 | }, {
22 | "base": "Node2D",
23 | "class": "VFSMDemoTrafficLightsController",
24 | "language": "GDScript",
25 | "path": "res://addons/visual_fsm/demos/simple_traffic_lights/traffic_lights.gd"
26 | }, {
27 | "base": "Node",
28 | "class": "VFSMSingleton",
29 | "language": "GDScript",
30 | "path": "res://addons/visual_fsm/vfsm_singleton.gd"
31 | }, {
32 | "base": "Resource",
33 | "class": "VFSMState",
34 | "language": "GDScript",
35 | "path": "res://addons/visual_fsm/fsm/vfsm_state.gd"
36 | }, {
37 | "base": "Object",
38 | "class": "VFSMStateBase",
39 | "language": "GDScript",
40 | "path": "res://addons/visual_fsm/resources/vfsm_state_base.gd"
41 | }, {
42 | "base": "GraphNode",
43 | "class": "VFSMStateNode",
44 | "language": "GDScript",
45 | "path": "res://addons/visual_fsm/editor/vfsm_state_graph_node.gd"
46 | }, {
47 | "base": "Resource",
48 | "class": "VFSMTrigger",
49 | "language": "GDScript",
50 | "path": "res://addons/visual_fsm/fsm/vfsm_trigger.gd"
51 | }, {
52 | "base": "VFSMTrigger",
53 | "class": "VFSMTriggerAction",
54 | "language": "GDScript",
55 | "path": "res://addons/visual_fsm/fsm/vfsm_trigger_action.gd"
56 | }, {
57 | "base": "Object",
58 | "class": "VFSMTriggerBase",
59 | "language": "GDScript",
60 | "path": "res://addons/visual_fsm/resources/vfsm_trigger_base.gd"
61 | }, {
62 | "base": "PanelContainer",
63 | "class": "VFSMTriggerGraphSlot",
64 | "language": "GDScript",
65 | "path": "res://addons/visual_fsm/editor/vfsm_trigger_graph_slot.gd"
66 | }, {
67 | "base": "VFSMTrigger",
68 | "class": "VFSMTriggerScript",
69 | "language": "GDScript",
70 | "path": "res://addons/visual_fsm/fsm/vfsm_trigger_script.gd"
71 | }, {
72 | "base": "VFSMTrigger",
73 | "class": "VFSMTriggerTimer",
74 | "language": "GDScript",
75 | "path": "res://addons/visual_fsm/fsm/vfsm_trigger_timer.gd"
76 | } ]
77 | _global_script_class_icons={
78 | "VFSM": "",
79 | "VFSMDemoSimpleAIJumper": "",
80 | "VFSMDemoTrafficLightsController": "",
81 | "VFSMSingleton": "",
82 | "VFSMState": "",
83 | "VFSMStateBase": "",
84 | "VFSMStateNode": "",
85 | "VFSMTrigger": "",
86 | "VFSMTriggerAction": "",
87 | "VFSMTriggerBase": "",
88 | "VFSMTriggerGraphSlot": "",
89 | "VFSMTriggerScript": "",
90 | "VFSMTriggerTimer": ""
91 | }
92 |
93 | [application]
94 |
95 | config/name="VisualFSM"
96 | run/main_scene="res://addons/visual_fsm/demos/simple_traffic_lights/simple_traffic_lights.tscn"
97 | config/icon="res://addons/visual_fsm/demos/icon.png"
98 |
99 | [editor_plugins]
100 |
101 | enabled=PoolStringArray( "visual_fsm" )
102 |
103 | [rendering]
104 |
105 | quality/driver/driver_name="GLES2"
106 | vram_compression/import_etc=true
107 | vram_compression/import_etc2=false
108 | environment/default_environment="res://default_env.tres"
109 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_state_graph_node.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=7 format=2]
2 |
3 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_timer.svg" type="Texture" id=1]
4 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_joypad.svg" type="Texture" id=2]
5 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_script.svg" type="Texture" id=3]
6 | [ext_resource path="res://addons/visual_fsm/editor/vfsm_state_graph_node.gd" type="Script" id=4]
7 |
8 | [sub_resource type="StyleBoxFlat" id=1]
9 | bg_color = Color( 0.0941176, 0.0941176, 0.0941176, 1 )
10 |
11 | [sub_resource type="StyleBoxFlat" id=2]
12 | bg_color = Color( 0.0156863, 0.113725, 0.235294, 1 )
13 |
14 | [node name="VFSMStateGraphNode" type="GraphNode"]
15 | margin_right = 200.0
16 | margin_bottom = 91.0
17 | rect_min_size = Vector2( 200, 0 )
18 | size_flags_horizontal = 9
19 | size_flags_vertical = 9
20 | custom_constants/port_offset = 0
21 | custom_constants/separation = 8
22 | show_close = true
23 | resizable = true
24 | slot/0/left_enabled = true
25 | slot/0/left_type = 0
26 | slot/0/left_color = Color( 1, 0.5, 0.31, 1 )
27 | slot/0/right_enabled = false
28 | slot/0/right_type = 0
29 | slot/0/right_color = Color( 1, 1, 1, 1 )
30 | slot/1/left_enabled = false
31 | slot/1/left_type = 0
32 | slot/1/left_color = Color( 1, 1, 1, 1 )
33 | slot/1/right_enabled = false
34 | slot/1/right_type = 0
35 | slot/1/right_color = Color( 1, 1, 1, 1 )
36 | script = ExtResource( 4 )
37 | __meta__ = {
38 | "_edit_use_anchors_": false
39 | }
40 | timer_icon = ExtResource( 1 )
41 | action_icon = ExtResource( 2 )
42 | script_icon = ExtResource( 3 )
43 |
44 | [node name="TitlePanel" type="PanelContainer" parent="."]
45 | margin_left = 16.0
46 | margin_top = 24.0
47 | margin_right = 184.0
48 | margin_bottom = 48.0
49 | size_flags_horizontal = 3
50 | custom_styles/panel = SubResource( 1 )
51 |
52 | [node name="HBox" type="HBoxContainer" parent="TitlePanel"]
53 | margin_right = 168.0
54 | margin_bottom = 24.0
55 |
56 | [node name="Margins" type="MarginContainer" parent="TitlePanel/HBox"]
57 | margin_right = 136.0
58 | margin_bottom = 24.0
59 | size_flags_horizontal = 3
60 | size_flags_vertical = 3
61 | custom_constants/margin_right = 8
62 | custom_constants/margin_left = 10
63 |
64 | [node name="Name" type="LineEdit" parent="TitlePanel/HBox/Margins"]
65 | margin_left = 10.0
66 | margin_right = 128.0
67 | margin_bottom = 24.0
68 | size_flags_horizontal = 3
69 | size_flags_vertical = 3
70 | size_flags_stretch_ratio = 5.0
71 | text = "State name"
72 | editable = false
73 |
74 | [node name="Script" type="Button" parent="TitlePanel/HBox"]
75 | margin_left = 140.0
76 | margin_right = 168.0
77 | margin_bottom = 24.0
78 | rect_min_size = Vector2( 24, 24 )
79 | icon = ExtResource( 3 )
80 |
81 | [node name="BottomPanel" type="PanelContainer" parent="."]
82 | margin_left = 16.0
83 | margin_top = 56.0
84 | margin_right = 184.0
85 | margin_bottom = 76.0
86 | size_flags_horizontal = 3
87 | custom_styles/panel = SubResource( 2 )
88 |
89 | [node name="AddTriggerDropdown" type="MenuButton" parent="BottomPanel"]
90 | margin_right = 168.0
91 | margin_bottom = 20.0
92 | text = "Add trigger"
93 | flat = false
94 | expand_icon = true
95 | switch_on_hover = true
96 |
97 | [connection signal="close_request" from="." to="." method="_on_StateGraphNode_close_request"]
98 | [connection signal="resize_request" from="." to="." method="_on_StateGraphNode_resize_request"]
99 | [connection signal="focus_exited" from="TitlePanel/HBox/Margins/Name" to="." method="_on_Name_focus_exited"]
100 | [connection signal="gui_input" from="TitlePanel/HBox/Margins/Name" to="." method="_on_Name_gui_input"]
101 | [connection signal="text_entered" from="TitlePanel/HBox/Margins/Name" to="." method="_on_Name_text_entered"]
102 | [connection signal="pressed" from="TitlePanel/HBox/Script" to="." method="_on_Script_pressed"]
103 | [connection signal="about_to_show" from="BottomPanel/AddTriggerDropdown" to="." method="_on_AddTriggerDropdown_about_to_show"]
104 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_state_graph_node.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSMStateNode
3 | extends GraphNode
4 |
5 | signal state_removed(state_node)
6 | signal new_script_request(state_node)
7 |
8 | const COLORS := [
9 | Color.coral,
10 | Color.lightgreen,
11 | Color.aquamarine,
12 | Color.beige,
13 | Color.orchid,
14 | Color.brown,
15 | Color.magenta,
16 | Color.gold,
17 | Color.pink,
18 | Color.limegreen
19 | ]
20 |
21 | export(Texture) var timer_icon
22 | export(Texture) var action_icon
23 | export(Texture) var script_icon
24 |
25 | onready var _state_label := $TitlePanel/HBox/Margins/Name
26 | onready var _add_trigger_dropdown := $BottomPanel/AddTriggerDropdown
27 |
28 | var timer_duration_dialog: AcceptDialog
29 | var input_action_dialog: AcceptDialog
30 | var state: VFSMState setget _set_state
31 | var fsm: VFSM
32 | var _trigger_slot_scene: PackedScene = preload("vfsm_trigger_graph_slot.tscn")
33 |
34 |
35 | func _ready() -> void:
36 | set_slot(0, true, 0, COLORS[0], false, 0, Color.white)
37 | var add_trigger_menu: PopupMenu = _add_trigger_dropdown.get_popup()
38 | add_trigger_menu.connect(
39 | "index_pressed", self, "_on_AddTrigger_index_pressed")
40 | add_trigger_menu.connect("focus_exited", add_trigger_menu, "hide")
41 |
42 |
43 | func add_trigger(trigger: VFSMTrigger) -> void:
44 | if get_child_count() == COLORS.size() - 2:
45 | push_warning("VisualFSM: Maximum number of triggers in state %s reached!"
46 | % self.state.name)
47 | return
48 |
49 | var slot_idx = get_child_count() - 1
50 | var next_to_last = get_child(slot_idx - 1)
51 | var trigger_slot: VFSMTriggerGraphSlot = _trigger_slot_scene.instance()
52 | add_child_below_node(next_to_last, trigger_slot)
53 | trigger_slot.timer_duration_dialog = timer_duration_dialog
54 | trigger_slot.input_action_dialog = input_action_dialog
55 | trigger_slot.connect("close_request", self, "_on_TriggerSlot_close_request")
56 | trigger_slot.trigger = trigger
57 | set_slot(slot_idx, false, 0, Color.white, true, 0, COLORS[slot_idx])
58 |
59 |
60 | func _set_state(value: VFSMState) -> void:
61 | offset = value.position
62 | _state_label.text = value.name
63 | state = value
64 |
65 |
66 | func _has_timer_trigger(state: VFSMState) -> bool:
67 | for trigger in fsm.get_triggers_in_state(state):
68 | if trigger is VFSMTriggerTimer:
69 | return true
70 | return false
71 |
72 |
73 | func _on_AddTriggerDropdown_about_to_show() -> void:
74 | var popup: PopupMenu = _add_trigger_dropdown.get_popup()
75 | if not popup.is_inside_tree():
76 | yield(popup, "tree_entered")
77 | popup.clear()
78 | # important: this steals focus from state name and triggers validation
79 | popup.grab_focus()
80 | var options := []
81 | # TODO: potential issue with ordering
82 | for script_trigger in fsm.get_script_triggers():
83 | if not self.state.has_trigger(script_trigger.vfsm_id):
84 | options.push_back(script_trigger)
85 | for trigger in options:
86 | popup.add_icon_item(script_icon, trigger.name)
87 | if 0 < popup.get_item_count():
88 | popup.add_separator()
89 | if not _has_timer_trigger(self.state):
90 | popup.add_icon_item(timer_icon, "New timer trigger")
91 | popup.add_icon_item(action_icon, "New input action trigger")
92 | popup.add_icon_item(script_icon, "New script trigger")
93 |
94 |
95 | func _on_AddTrigger_index_pressed(index: int) -> void:
96 | var popup: PopupMenu = _add_trigger_dropdown.get_popup()
97 | var num_items = popup.get_item_count()
98 | if num_items - 3 == index: # new timer
99 | fsm.create_timer_trigger(self.state)
100 | elif num_items - 2 == index: # new input action
101 | fsm.create_action_trigger(self.state)
102 | elif num_items - 1 == index: # new script
103 | emit_signal("new_script_request", self)
104 | else: # reuse existing script trigger
105 | var options := []
106 | for script_trigger in fsm.get_script_triggers():
107 | if not self.state.has_trigger(script_trigger.vfsm_id):
108 | options.push_back(script_trigger)
109 | var selected_trigger = options[index]
110 | self.state.add_trigger(selected_trigger)
111 |
112 |
113 | func _on_StateGraphNode_close_request() -> void:
114 | emit_signal("state_removed", self)
115 | queue_free()
116 |
117 |
118 | func _on_StateGraphNode_resize_request(new_minsize) -> void:
119 | rect_size = new_minsize
120 |
121 |
122 | func _on_TriggerSlot_close_request(trigger_slot: VFSMTriggerGraphSlot) -> void:
123 | # TODO: Confirm
124 | fsm.remove_trigger_from_state(self.state, trigger_slot.trigger)
125 |
126 |
127 | func _on_Script_pressed() -> void:
128 | $"/root/VFSMSingleton".emit_signal(
129 | "edit_custom_script", self.state.custom_script
130 | )
131 |
132 |
133 | func _on_Name_gui_input(event: InputEvent) -> void:
134 | if not _state_label.editable and event is InputEventMouseButton:
135 | var mouse_event = event as InputEventMouseButton
136 | if mouse_event.button_index == BUTTON_LEFT and mouse_event.pressed:
137 | _state_label.editable = true
138 |
139 |
140 | func _validate_name(new_name: String) -> bool:
141 | if self.fsm.has_state(new_name):
142 | push_error("VisualFSM: A state named \"%s\" already exists." % new_name)
143 | return false
144 | if new_name.empty():
145 | push_error("VisualFSM: The state name cannot be empty.")
146 | return false
147 | return true
148 |
149 | func _on_Name_text_entered(new_text: String) -> void:
150 | if _validate_name(new_text):
151 | _state_label.editable = false
152 |
153 |
154 | func _on_Name_focus_exited():
155 | var new_name = _state_label.text
156 | if _validate_name(new_name):
157 | self.state.rename(new_name)
158 | else:
159 | _state_label.text = self.state.name
160 | _state_label.editable = false
161 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_trigger_graph_slot.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=7 format=2]
2 |
3 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_timer.svg" type="Texture" id=1]
4 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_joypad.svg" type="Texture" id=2]
5 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_script.svg" type="Texture" id=3]
6 | [ext_resource path="res://addons/visual_fsm/editor/vfsm_trigger_graph_slot.gd" type="Script" id=4]
7 | [ext_resource path="res://addons/visual_fsm/resources/icons/close-cross-symbol-in-a-circle.svg" type="Texture" id=5]
8 |
9 | [sub_resource type="StyleBoxFlat" id=1]
10 | bg_color = Color( 0.0941176, 0.0941176, 0.0941176, 1 )
11 |
12 | [node name="TriggerSlot" type="PanelContainer"]
13 | margin_right = 172.0
14 | margin_bottom = 32.0
15 | size_flags_horizontal = 3
16 | custom_styles/panel = SubResource( 1 )
17 | script = ExtResource( 4 )
18 | __meta__ = {
19 | "_edit_use_anchors_": false
20 | }
21 |
22 | [node name="Action" type="HBoxContainer" parent="."]
23 | visible = false
24 | margin_right = 184.0
25 | margin_bottom = 32.0
26 |
27 | [node name="ActionMargins" type="MarginContainer" parent="Action"]
28 | margin_right = 28.0
29 | margin_bottom = 32.0
30 | size_flags_horizontal = 3
31 | custom_constants/margin_right = 10
32 | custom_constants/margin_left = 10
33 |
34 | [node name="ActionLabel" type="Label" parent="Action/ActionMargins"]
35 | margin_left = 10.0
36 | margin_right = 18.0
37 | margin_bottom = 32.0
38 | focus_mode = 2
39 | size_flags_horizontal = 3
40 | size_flags_vertical = 3
41 | size_flags_stretch_ratio = 4.0
42 | valign = 1
43 |
44 | [node name="Button" type="CenterContainer" parent="Action"]
45 | margin_left = 124.0
46 | margin_right = 152.0
47 | margin_bottom = 32.0
48 | rect_min_size = Vector2( 28, 32 )
49 |
50 | [node name="Action" type="Button" parent="Action/Button"]
51 | margin_right = 28.0
52 | margin_bottom = 32.0
53 | rect_min_size = Vector2( 0, 32 )
54 | size_flags_horizontal = 3
55 | size_flags_vertical = 3
56 | icon = ExtResource( 2 )
57 |
58 | [node name="Close" type="CenterContainer" parent="Action"]
59 | margin_left = 156.0
60 | margin_right = 184.0
61 | margin_bottom = 32.0
62 | size_flags_horizontal = 0
63 | size_flags_vertical = 0
64 |
65 | [node name="Button" type="Button" parent="Action/Close"]
66 | margin_right = 28.0
67 | margin_bottom = 32.0
68 | rect_min_size = Vector2( 28, 32 )
69 | size_flags_horizontal = 3
70 | size_flags_vertical = 3
71 | icon = ExtResource( 5 )
72 | expand_icon = true
73 |
74 | [node name="Timer" type="HBoxContainer" parent="."]
75 | visible = false
76 | margin_right = 172.0
77 | margin_bottom = 32.0
78 |
79 | [node name="DurationMargins" type="MarginContainer" parent="Timer"]
80 | margin_right = 28.0
81 | margin_bottom = 32.0
82 | custom_constants/margin_right = 10
83 | custom_constants/margin_left = 10
84 |
85 | [node name="Duration" type="Label" parent="Timer/DurationMargins"]
86 | margin_left = 10.0
87 | margin_right = 18.0
88 | margin_bottom = 32.0
89 | focus_mode = 2
90 | size_flags_vertical = 3
91 | size_flags_stretch_ratio = 4.0
92 | text = "0"
93 | valign = 1
94 |
95 | [node name="Seconds" type="Label" parent="Timer"]
96 | margin_left = 32.0
97 | margin_right = 108.0
98 | margin_bottom = 32.0
99 | focus_mode = 2
100 | size_flags_horizontal = 3
101 | size_flags_vertical = 3
102 | text = "sec"
103 | valign = 1
104 |
105 | [node name="Button" type="CenterContainer" parent="Timer"]
106 | margin_left = 112.0
107 | margin_right = 140.0
108 | margin_bottom = 32.0
109 | rect_min_size = Vector2( 28, 32 )
110 |
111 | [node name="Timer" type="Button" parent="Timer/Button"]
112 | margin_right = 28.0
113 | margin_bottom = 32.0
114 | rect_min_size = Vector2( 0, 32 )
115 | size_flags_horizontal = 3
116 | size_flags_vertical = 3
117 | icon = ExtResource( 1 )
118 |
119 | [node name="Close" type="CenterContainer" parent="Timer"]
120 | margin_left = 144.0
121 | margin_right = 172.0
122 | margin_bottom = 32.0
123 | size_flags_horizontal = 0
124 | size_flags_vertical = 0
125 |
126 | [node name="Button" type="Button" parent="Timer/Close"]
127 | margin_right = 28.0
128 | margin_bottom = 32.0
129 | rect_min_size = Vector2( 28, 32 )
130 | size_flags_horizontal = 3
131 | size_flags_vertical = 3
132 | icon = ExtResource( 5 )
133 | expand_icon = true
134 |
135 | [node name="Script" type="HBoxContainer" parent="."]
136 | visible = false
137 | margin_right = 172.0
138 | margin_bottom = 32.0
139 |
140 | [node name="TitleMargins" type="MarginContainer" parent="Script"]
141 | margin_right = 108.0
142 | margin_bottom = 32.0
143 | size_flags_horizontal = 3
144 | custom_constants/margin_right = 10
145 | custom_constants/margin_left = 10
146 |
147 | [node name="Title" type="Label" parent="Script/TitleMargins"]
148 | margin_left = 10.0
149 | margin_right = 98.0
150 | margin_bottom = 32.0
151 | focus_mode = 2
152 | size_flags_horizontal = 3
153 | size_flags_vertical = 3
154 | size_flags_stretch_ratio = 4.0
155 | text = "ScriptName"
156 | valign = 1
157 |
158 | [node name="Button" type="CenterContainer" parent="Script"]
159 | margin_left = 112.0
160 | margin_right = 140.0
161 | margin_bottom = 32.0
162 | rect_min_size = Vector2( 28, 32 )
163 |
164 | [node name="Script" type="Button" parent="Script/Button"]
165 | margin_right = 28.0
166 | margin_bottom = 32.0
167 | rect_min_size = Vector2( 0, 32 )
168 | size_flags_horizontal = 3
169 | size_flags_vertical = 3
170 | icon = ExtResource( 3 )
171 |
172 | [node name="Close" type="CenterContainer" parent="Script"]
173 | margin_left = 144.0
174 | margin_right = 172.0
175 | margin_bottom = 32.0
176 | size_flags_horizontal = 0
177 | size_flags_vertical = 0
178 |
179 | [node name="Button" type="Button" parent="Script/Close"]
180 | margin_right = 28.0
181 | margin_bottom = 32.0
182 | rect_min_size = Vector2( 28, 32 )
183 | size_flags_horizontal = 3
184 | size_flags_vertical = 3
185 | icon = ExtResource( 5 )
186 | expand_icon = true
187 | [connection signal="pressed" from="Action/Button/Action" to="." method="_on_Action_pressed"]
188 | [connection signal="pressed" from="Action/Close/Button" to="." method="_on_CloseButton_pressed"]
189 | [connection signal="pressed" from="Timer/Button/Timer" to="." method="_on_Timer_pressed"]
190 | [connection signal="pressed" from="Timer/Close/Button" to="." method="_on_CloseButton_pressed"]
191 | [connection signal="pressed" from="Script/Button/Script" to="." method="_on_Script_pressed"]
192 | [connection signal="pressed" from="Script/Close/Button" to="." method="_on_CloseButton_pressed"]
193 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_graph_edit.gd:
--------------------------------------------------------------------------------
1 | tool
2 | extends GraphEdit
3 |
4 | onready var _new_trigger_dialog := $"../DialogLayer/NewScriptTriggerDialog"
5 | onready var _new_state_dialog := $"../DialogLayer/NewStateDialog"
6 | onready var _timer_duration_dialog := $"../DialogLayer/TimerDurationDialog"
7 | onready var _input_action_dialog := $"../DialogLayer/InputActionDialog"
8 |
9 | var _vfsm_start_scene: PackedScene = preload("vfsm_start_graph_node.tscn")
10 | var _vfsm_state_scene: PackedScene = preload("vfsm_state_graph_node.tscn")
11 | var _vfsm: VFSM
12 | var _triggers := {}
13 | var _state_node_creating_trigger: VFSMStateNode
14 |
15 |
16 | func _ready() -> void:
17 | # var hbox := get_zoom_hbox()
18 | # var trigger_dropdown := MenuButton.new()
19 | # trigger_dropdown.text = "ScriptTriggers"
20 | # trigger_dropdown.flat = false
21 | # hbox.add_child(trigger_dropdown)
22 | # hbox.move_child(trigger_dropdown, 0)
23 |
24 | add_valid_left_disconnect_type(0)
25 |
26 | _new_trigger_dialog.rect_position = get_viewport_rect().size / 2 - _new_trigger_dialog.rect_size / 2
27 |
28 | edit(VFSM.new())
29 |
30 |
31 | func edit(fsm: VFSM) -> void:
32 | if _vfsm:
33 | _vfsm.disconnect("changed", self, "_on_vfsm_changed")
34 | _vfsm = fsm
35 | if _vfsm:
36 | _vfsm.connect("changed", self, "_on_vfsm_changed")
37 | _redraw_graph()
38 |
39 |
40 | func _on_vfsm_changed():
41 | _redraw_graph()
42 |
43 |
44 | func _find_state_node(state: VFSMState) -> VFSMStateNode:
45 | for child in get_children():
46 | if child is VFSMStateNode and child.state == state:
47 | return child
48 | return null
49 |
50 |
51 | func _redraw_graph():
52 | # print_debug("Redrawing fsm graph.............")
53 |
54 | # clear dialogs
55 | _new_state_dialog.close()
56 | _timer_duration_dialog.close()
57 | _input_action_dialog.close()
58 |
59 | clear_connections()
60 | # clear graph elements
61 | for child in get_children():
62 | if child is GraphNode:
63 | remove_child(child)
64 | child.queue_free()
65 |
66 | if not _vfsm:
67 | return
68 |
69 | # add state nodes
70 | for state in _vfsm.get_states():
71 | var node: VFSMStateNode = _vfsm_state_scene.instance()
72 | node.timer_duration_dialog = _timer_duration_dialog
73 | node.input_action_dialog = _input_action_dialog
74 | add_child(node)
75 | node.connect("state_removed", self, "_on_StateNode_state_removed")
76 | node.connect(
77 | "new_script_request", self, "_on_StateNode_new_script_request")
78 | node.fsm = _vfsm
79 | node.state = state
80 | node.offset = state.position
81 | # add trigger slots
82 | for trigger in _vfsm.get_triggers_in_state(state):
83 | node.add_trigger(trigger)
84 |
85 | # add connections
86 | for from_state in _vfsm.get_states():
87 | var from_node := _find_state_node(from_state)
88 | for trigger in _vfsm.get_triggers_in_state(from_state):
89 | var to_state := _vfsm.get_to_state(from_state, trigger)
90 | if to_state:
91 | var to_node := _find_state_node(to_state)
92 | var from_port = from_state.get_trigger_index(trigger)
93 | assert(-1 < from_port,
94 | "VisualFSM: Missing trigger \"%s\" in state \"%s\""
95 | % [trigger.name, from_state.name])
96 | connect_node(from_node.name, from_port, to_node.name, 0)
97 |
98 | # add start node
99 | var start_node = _vfsm_start_scene.instance()
100 | start_node.name = "VFSMStartNode"
101 | start_node.offset = _vfsm.start_position
102 | add_child(start_node)
103 |
104 | # add start connection
105 | var start_state := _vfsm.get_start_state()
106 | if start_state:
107 | var start_state_node := _find_state_node(start_state)
108 | connect_node(start_node.name, 0, start_state_node.name, 0)
109 |
110 |
111 | func _try_create_new_state(from: String, from_slot: int, position: Vector2) -> void:
112 | if not yield():
113 | return
114 |
115 | var state_name: String = _new_state_dialog.state_name
116 | var state_position := position - Vector2(0, 40)
117 | var from_node = get_node(from)
118 | assert(from_node, "Missing node in create new state")
119 | if from_node is VFSMStateNode:
120 | var from_state: VFSMState = from_node.state
121 | var from_trigger_id := from_state.get_trigger_id_from_index(from_slot)
122 | var from_trigger := _vfsm.get_trigger(from_trigger_id)
123 | _vfsm.create_state(state_name, state_position, from_state, from_trigger)
124 | else: # from start node
125 | _vfsm.create_state(state_name, state_position)
126 |
127 |
128 | func _on_connection_to_empty(from: String, from_slot: int, release_position: Vector2):
129 | var mouse_pos := get_global_mouse_position()
130 | _new_state_dialog.rect_position = mouse_pos - _new_state_dialog.rect_size / 2
131 | _new_state_dialog.open(_try_create_new_state(from, from_slot, release_position))
132 |
133 |
134 | func _on_connection_request(
135 | from: String, from_slot: int, to: String, to_slot: int
136 | ) -> void:
137 | if from.empty() or to.empty():
138 | push_warning("VisualFSM States must have names.")
139 | return
140 |
141 | var from_node: GraphNode = get_node(from)
142 | assert(from_node, "Missing from node in connection request")
143 | var to_node: VFSMStateNode = get_node(to)
144 | assert(to_node, "Missing tonode in connection request")
145 | if from_node is VFSMStateNode:
146 | var trigger_id: int = from_node.state.get_trigger_id_from_index(from_slot)
147 | var trigger := _vfsm.get_trigger(trigger_id)
148 | _vfsm.add_transition(from_node.state, trigger, to_node.state)
149 | else: # start node connection
150 | _vfsm.set_start_state(to_node.state)
151 |
152 |
153 | func _on_disconnection_request(from, from_slot, to, to_slot):
154 | # hacky way to prtrigger weird connection lines when button held
155 | while Input.is_mouse_button_pressed(BUTTON_LEFT):
156 | yield(get_tree(), "idle_frame")
157 |
158 | yield(get_tree(), "idle_frame")
159 | # may have been removed during redraw. If so do nothing
160 | if not has_node(from) or not has_node(to):
161 | return
162 |
163 | var from_node: GraphNode = get_node(from)
164 | var to_node: VFSMStateNode = get_node(to)
165 | if from_node is VFSMStateNode:
166 | var trigger_id: int = from_node.state.get_trigger_id_from_index(from_slot)
167 | var trigger := _vfsm.get_trigger(trigger_id)
168 | _vfsm.remove_transition(from_node.state, trigger)
169 | else: # start node connection
170 | # start_target may have been reconnected during yield
171 | if _vfsm.get_start_state().vfsm_id == to_node.state.vfsm_id:
172 | _vfsm.set_start_state(null)
173 |
174 |
175 | func _on_StateNode_state_removed(state_node: VFSMStateNode) -> void:
176 | _vfsm.remove_state(state_node.state)
177 | _vfsm.remove_state(state_node.state)
178 |
179 |
180 | func _on_StateNode_rename_request(
181 | state_node: VFSMStateNode, new_name: String) -> void:
182 | var old_name := state_node.state.name
183 | var request_denied = false
184 | if new_name.empty():
185 | push_warning("VisualFSM: States must have names.")
186 | request_denied = true
187 | if _vfsm.has_state(new_name):
188 | push_warning("VisualFSM: A state named \"%s\" already exists." % new_name)
189 | request_denied = true
190 | if "Start" == new_name:
191 | push_warning("VisualFSM: The name \"Start\" is reserved." % new_name)
192 | request_denied = true
193 |
194 | if request_denied:
195 | return
196 |
197 | # print_debug("Renaming from %s to %s" % [old_name, new_name])
198 | _vfsm.rename_state(old_name, new_name)
199 |
200 |
201 | func _on_end_node_move():
202 | for child in get_children():
203 | if child is VFSMStateNode:
204 | var state := _vfsm.get_state(child.state.vfsm_id)
205 | state.position = child.offset
206 | elif child is GraphNode and child.name == "VFSMStartNode":
207 | _vfsm.start_position = child.offset
208 |
209 |
210 | func _try_create_new_script_trigger(state: VFSMState) -> void:
211 | if not yield():
212 | return
213 |
214 | var trigger_name: String = _new_trigger_dialog.trigger_name
215 | _vfsm.create_script_trigger(state, trigger_name)
216 |
217 |
218 | func _on_StateNode_new_script_request(node: VFSMStateNode) -> void:
219 | var mouse_pos := get_global_mouse_position()
220 | _new_trigger_dialog.rect_position = mouse_pos - _new_trigger_dialog.rect_size / 2
221 | _new_trigger_dialog.open(_try_create_new_script_trigger(node.state))
222 |
223 |
224 | func _on_Dialog_new_trigger_created(trigger: VFSMTrigger) -> void:
225 | _vfsm.add_trigger(trigger)
226 | _vfsm.add_script_transition(_state_node_creating_trigger.state.name, trigger.name)
227 | _state_node_creating_trigger = null
228 | _new_trigger_dialog.hide()
229 |
230 |
231 | func _on_Dialog_trigger_name_request(trigger_name: String) -> void:
232 | if not _vfsm.has_script_trigger(trigger_name):
233 | _new_trigger_dialog.trigger_name = trigger_name
234 | else:
235 | _new_trigger_dialog.trigger_name = ""
236 | _new_trigger_dialog.name_request_denied(trigger_name)
237 |
238 | func _on_StateCreateDialog_state_name_request(name: String) -> void:
239 | if not _vfsm.has_state(name):
240 | _new_state_dialog.approve_name_request(name)
241 | else:
242 | _new_state_dialog.deny_name_request(name)
243 |
--------------------------------------------------------------------------------
/addons/visual_fsm/fsm/vfsm.gd:
--------------------------------------------------------------------------------
1 | tool
2 | class_name VFSM
3 | extends Resource
4 |
5 | const STATE_TEMPLATE_PATH := "res://addons/visual_fsm/resources/state_template.txt"
6 | const TRIGGER_TEMPLATE_PATH := "res://addons/visual_fsm/resources/trigger_template.txt"
7 |
8 | export(int) var start_state_vfsm_id: int
9 | export(Vector2) var start_position: Vector2
10 |
11 | var _next_state_vfsm_id := 0
12 | var _next_trigger_vfsm_id := 0
13 | var _states := {} # vfsm_id to VFSMState
14 | var _trigger_vfsm_id_map := {} # vfsm_id to VFSMTrigger
15 | var _transitions := {
16 | # from_state_vfsm_id_1: {
17 | # trigger_vfsm_id_1: to_state_vfsm_id_1,
18 | # trigger_vfsm_id_2: to_state_vfsm_id_2
19 | # etc...
20 | # },
21 | # from_state_vfsm_id_1: {
22 | # etc...
23 | # },
24 | # etc...
25 | }
26 | var _state_custom_script_template: String
27 | var _trigger_custom_script_template: String
28 |
29 |
30 | func _read_from_file(path: String) -> String:
31 | var f = File.new()
32 | var err = f.open(path, File.READ)
33 | if err != OK:
34 | push_warning("Could not open file \"%s\", error code: %s" % [path, err])
35 | return ""
36 | var content = f.get_as_text()
37 | f.close()
38 | return content
39 |
40 |
41 | func _init():
42 | if not start_position:
43 | start_position = Vector2(100, 100)
44 | _trigger_custom_script_template = _read_from_file(TRIGGER_TEMPLATE_PATH)
45 |
46 |
47 | func has_state(name: String) -> bool:
48 | for state in _states.values():
49 | if name == state.name:
50 | return true
51 | return false
52 |
53 |
54 | func get_start_state() -> VFSMState:
55 | if 0 > self.start_state_vfsm_id:
56 | return null
57 | return _states.get(self.start_state_vfsm_id)
58 |
59 |
60 | func set_start_state(state: VFSMState) -> void:
61 | if state:
62 | self.start_state_vfsm_id = state.vfsm_id
63 | else:
64 | self.start_state_vfsm_id = -1
65 | _changed()
66 |
67 |
68 | func get_state(vfsm_id: int) -> VFSMState:
69 | return _states.get(vfsm_id)
70 |
71 |
72 | func get_next_state(state: VFSMState, trigger: VFSMTrigger) -> VFSMState:
73 | var next_state_id = _transitions.get(state.vfsm_id).get(trigger.vfsm_id)
74 | return _states.get(next_state_id)
75 |
76 |
77 | func get_triggers_in_state(state: VFSMState) -> Array:
78 | var triggers := []
79 | for trigger_vfsm_id in state.trigger_ids:
80 | triggers.push_back(_trigger_vfsm_id_map[trigger_vfsm_id])
81 | return triggers
82 |
83 |
84 | func get_to_state(
85 | from_state: VFSMState,
86 | trigger: VFSMTrigger
87 | ) -> VFSMState:
88 | var triggers = _transitions.get(from_state.vfsm_id)
89 | if not triggers.has(trigger.vfsm_id):
90 | return null
91 |
92 | var to_state_vfsm_id: int = triggers.get(trigger.vfsm_id)
93 | return _states.get(to_state_vfsm_id)
94 |
95 |
96 | func get_states() -> Array:
97 | return _states.values()
98 |
99 |
100 | func get_trigger(vfsm_id: int) -> VFSMTrigger:
101 | return _trigger_vfsm_id_map.get(vfsm_id)
102 |
103 |
104 | func has_script_trigger(name: String) -> bool:
105 | for trigger in _trigger_vfsm_id_map.values():
106 | if trigger is VFSMTriggerScript:
107 | return name == trigger.name
108 | return false
109 |
110 |
111 | func get_script_triggers() -> Array:
112 | var script_triggers := []
113 | for trigger in _trigger_vfsm_id_map.values():
114 | if trigger is VFSMTriggerScript:
115 | script_triggers.push_back(trigger)
116 | return script_triggers
117 |
118 |
119 | func create_state(name: String, position: Vector2,
120 | from_state: VFSMState = null,
121 | from_trigger: VFSMTrigger = null) -> void:
122 | var state := VFSMState.new()
123 | state.connect("changed", self, "_changed")
124 | state.vfsm_id = _next_state_vfsm_id
125 | _next_state_vfsm_id += 1
126 | state.name = name
127 | state.position = position
128 | state.new_script()
129 | _states[state.vfsm_id] = state
130 | _transitions[state.vfsm_id] = {}
131 | if from_state and from_trigger:
132 | _transitions[from_state.vfsm_id][from_trigger.vfsm_id] = state.vfsm_id
133 | _changed()
134 | else:
135 | self.set_start_state(state)
136 |
137 |
138 | func remove_state(state: VFSMState) -> void:
139 | _states.erase(state.vfsm_id)
140 | _transitions.erase(state.vfsm_id)
141 | for from_vfsm_id in _transitions:
142 | var triggers_to_erase := []
143 | for trigger_vfsm_id in _transitions.get(from_vfsm_id):
144 | if state.vfsm_id == _transitions.get(from_vfsm_id).get(trigger_vfsm_id):
145 | triggers_to_erase.push_back(trigger_vfsm_id)
146 | for trigger_vfsm_id in triggers_to_erase:
147 | _transitions[from_vfsm_id].erase(trigger_vfsm_id)
148 | _changed()
149 |
150 |
151 | func create_timer_trigger(state: VFSMState) -> void:
152 | var timer_trigger := VFSMTriggerTimer.new()
153 | timer_trigger.vfsm_id = _get_next_transition_id()
154 | timer_trigger.duration = 1
155 | _trigger_vfsm_id_map[timer_trigger.vfsm_id] = timer_trigger
156 | state.add_trigger(timer_trigger)
157 |
158 |
159 | func create_action_trigger(state: VFSMState) -> void:
160 | var action_trigger := VFSMTriggerAction.new()
161 | action_trigger.vfsm_id = _get_next_transition_id()
162 | _trigger_vfsm_id_map[action_trigger.vfsm_id] = action_trigger
163 | state.add_trigger(action_trigger)
164 |
165 |
166 | func create_script_trigger(state: VFSMState, trigger_name: String) -> void:
167 | assert(not has_script_trigger(trigger_name))
168 | var script_trigger := VFSMTriggerScript.new()
169 | script_trigger.vfsm_id = _get_next_transition_id()
170 | script_trigger.name = trigger_name
171 | var custom_script := GDScript.new()
172 | custom_script.source_code = _trigger_custom_script_template % trigger_name
173 | script_trigger.custom_script = custom_script
174 | _trigger_vfsm_id_map[script_trigger.vfsm_id] = script_trigger
175 | state.add_trigger(script_trigger)
176 |
177 |
178 | func remove_trigger_from_state(state: VFSMState, trigger: VFSMTrigger) -> void:
179 | _transitions[state.vfsm_id].erase(trigger.vfsm_id)
180 | state.remove_trigger(trigger)
181 |
182 |
183 | func remove_trigger(trigger: VFSMTrigger) -> void:
184 | _trigger_vfsm_id_map.erase(trigger.vfsm_id)
185 | for state_vfsm_id in _states:
186 | _states.get(state_vfsm_id).remove_trigger(trigger)
187 | _changed()
188 |
189 |
190 | func add_transition(
191 | from_state: VFSMState,
192 | from_trigger: VFSMTrigger,
193 | to_state: VFSMState
194 | ) -> void:
195 | _transitions[from_state.vfsm_id][from_trigger.vfsm_id] = to_state.vfsm_id
196 | _changed()
197 |
198 |
199 | func remove_transition(
200 | from_state: VFSMState,
201 | from_trigger: VFSMTrigger
202 | ) -> void:
203 | _transitions[from_state.vfsm_id].erase(from_trigger.vfsm_id)
204 | _changed()
205 |
206 |
207 | func _changed() -> void:
208 | call_deferred("emit_signal", "changed")
209 |
210 |
211 | func _get_next_state_id() -> int:
212 | var id = _next_state_vfsm_id
213 | _next_state_vfsm_id += 1
214 | return id
215 |
216 |
217 | func _get_next_transition_id() -> int:
218 | var id = _next_trigger_vfsm_id
219 | _next_trigger_vfsm_id += 1
220 | return id
221 |
222 |
223 | func _get(property: String):
224 | # print_debug("FSM: Getting property: %s" % property)
225 | match property:
226 | "states":
227 | return _states.values()
228 | "triggers":
229 | return _trigger_vfsm_id_map.values()
230 | "transitions":
231 | var transitions := []
232 | for from_vfsm_id in _transitions:
233 | for trigger_vfsm_id in _transitions[from_vfsm_id]:
234 | var to_vfsm_id = _transitions[from_vfsm_id][trigger_vfsm_id]
235 | transitions += [
236 | from_vfsm_id,
237 | trigger_vfsm_id,
238 | to_vfsm_id
239 | ]
240 | return transitions
241 | return null
242 |
243 |
244 | func _set(property: String, value) -> bool:
245 | match property:
246 | "states":
247 | for state in value:
248 | state.connect("changed", self, "_changed")
249 | _states[state.vfsm_id] = state
250 | _transitions[state.vfsm_id] = {}
251 | if _next_state_vfsm_id <= state.vfsm_id:
252 | _next_state_vfsm_id = state.vfsm_id + 1
253 | return true
254 | "triggers":
255 | for trigger in value:
256 | _trigger_vfsm_id_map[trigger.vfsm_id] = trigger
257 | if _next_trigger_vfsm_id <= trigger.vfsm_id:
258 | _next_trigger_vfsm_id = trigger.vfsm_id + 1
259 | return true
260 | "transitions":
261 | var num_transitions = value.size() / 3
262 | for vfsm_idx in range(num_transitions):
263 | var from_vfsm_id = value[3 * vfsm_idx]
264 | var trigger_vfsm_id = value[3 * vfsm_idx + 1]
265 | var to_vfsm_id = value[3 * vfsm_idx + 2]
266 | if not _transitions.has(from_vfsm_id):
267 | _transitions[from_vfsm_id] = {}
268 | _transitions[from_vfsm_id][trigger_vfsm_id] = to_vfsm_id
269 | return true
270 | return false
271 |
272 |
273 | func _get_property_list() -> Array:
274 | var property_list := []
275 | property_list += [
276 | {
277 | "name": "states",
278 | "type": TYPE_ARRAY,
279 | "hint": PROPERTY_HINT_NONE,
280 | "hint_string": "",
281 | "usage": PROPERTY_USAGE_NOEDITOR
282 | },
283 | {
284 | "name": "triggers",
285 | "type": TYPE_ARRAY,
286 | "hint": PROPERTY_HINT_NONE,
287 | "hint_string": "",
288 | "usage": PROPERTY_USAGE_NOEDITOR
289 | },
290 | {
291 | "name": "transitions",
292 | "type": TYPE_ARRAY,
293 | "hint": PROPERTY_HINT_NONE,
294 | "hint_string": "",
295 | "usage": PROPERTY_USAGE_NOEDITOR
296 | }
297 | ]
298 |
299 | return property_list
300 |
--------------------------------------------------------------------------------
/addons/visual_fsm/editor/vfsm_editor.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=10 format=2]
2 |
3 | [ext_resource path="res://addons/visual_fsm/editor/dialogs/vfsm_new_scripted_trigger_dialog.gd" type="Script" id=1]
4 | [ext_resource path="res://addons/visual_fsm/editor/vfsm_graph_edit.gd" type="Script" id=2]
5 | [ext_resource path="res://addons/visual_fsm/editor/dialogs/vfsm_new_state_dialog.gd" type="Script" id=3]
6 | [ext_resource path="res://addons/visual_fsm/editor/dialogs/vfsm_set_timer_dialog.gd" type="Script" id=4]
7 | [ext_resource path="res://addons/visual_fsm/resources/icons/icon_script.svg" type="Texture" id=5]
8 | [ext_resource path="res://addons/visual_fsm/editor/dialogs/vfsm_set_input_trigger_dialog.gd" type="Script" id=6]
9 | [ext_resource path="res://addons/visual_fsm/editor/vfsm_editor.gd" type="Script" id=7]
10 |
11 | [sub_resource type="StyleBoxFlat" id=1]
12 | bg_color = Color( 0.113725, 0.113725, 0.117647, 1 )
13 |
14 | [sub_resource type="StyleBoxFlat" id=2]
15 | bg_color = Color( 0.113725, 0.113725, 0.117647, 1 )
16 |
17 | [node name="VisualFSMEditor" type="MarginContainer"]
18 | anchor_right = 1.0
19 | anchor_bottom = 1.0
20 | rect_min_size = Vector2( 0, 384 )
21 | script = ExtResource( 7 )
22 | __meta__ = {
23 | "_edit_use_anchors_": false
24 | }
25 |
26 | [node name="VFSMGraphEdit" type="GraphEdit" parent="."]
27 | margin_right = 1024.0
28 | margin_bottom = 600.0
29 | custom_constants/port_grab_distance_vertical = 24
30 | custom_constants/port_grab_distance_horizontal = 32
31 | custom_constants/bezier_len_neg = 600
32 | right_disconnects = true
33 | script = ExtResource( 2 )
34 |
35 | [node name="DialogLayer" type="CanvasLayer" parent="."]
36 |
37 | [node name="NewScriptTriggerDialog" type="ConfirmationDialog" parent="DialogLayer"]
38 | anchor_left = 0.000976562
39 | anchor_right = 0.000976562
40 | anchor_bottom = 0.00333333
41 | margin_left = 329.0
42 | margin_top = 230.0
43 | margin_right = 693.0
44 | margin_bottom = 368.0
45 | rect_min_size = Vector2( 350, 70 )
46 | window_title = "New event"
47 | script = ExtResource( 1 )
48 | __meta__ = {
49 | "_edit_use_anchors_": false
50 | }
51 | script_icon = ExtResource( 5 )
52 |
53 | [node name="Margins" type="MarginContainer" parent="DialogLayer/NewScriptTriggerDialog"]
54 | margin_left = 8.0
55 | margin_top = 8.0
56 | margin_right = 356.0
57 | margin_bottom = 104.0
58 | custom_constants/margin_right = 16
59 | custom_constants/margin_top = 16
60 | custom_constants/margin_left = 16
61 | custom_constants/margin_bottom = 16
62 | __meta__ = {
63 | "_edit_use_anchors_": false
64 | }
65 |
66 | [node name="Content" type="VBoxContainer" parent="DialogLayer/NewScriptTriggerDialog/Margins"]
67 | margin_left = 16.0
68 | margin_top = 16.0
69 | margin_right = 332.0
70 | margin_bottom = 80.0
71 | size_flags_horizontal = 3
72 | size_flags_vertical = 3
73 | custom_constants/separation = 10
74 | __meta__ = {
75 | "_edit_use_anchors_": false
76 | }
77 |
78 | [node name="TriggerName" type="LineEdit" parent="DialogLayer/NewScriptTriggerDialog/Margins/Content"]
79 | margin_right = 316.0
80 | margin_bottom = 24.0
81 | placeholder_text = "Event name"
82 | caret_blink = true
83 | caret_blink_speed = 0.5
84 |
85 | [node name="Prompt" type="PanelContainer" parent="DialogLayer/NewScriptTriggerDialog/Margins/Content"]
86 | margin_top = 34.0
87 | margin_right = 316.0
88 | margin_bottom = 68.0
89 | custom_styles/panel = SubResource( 1 )
90 |
91 | [node name="Margin" type="MarginContainer" parent="DialogLayer/NewScriptTriggerDialog/Margins/Content/Prompt"]
92 | margin_right = 316.0
93 | margin_bottom = 34.0
94 | custom_constants/margin_right = 10
95 | custom_constants/margin_top = 10
96 | custom_constants/margin_left = 10
97 | custom_constants/margin_bottom = 10
98 |
99 | [node name="VBox" type="VBoxContainer" parent="DialogLayer/NewScriptTriggerDialog/Margins/Content/Prompt/Margin"]
100 | margin_left = 10.0
101 | margin_top = 10.0
102 | margin_right = 306.0
103 | margin_bottom = 24.0
104 | custom_constants/separation = 5
105 |
106 | [node name="Name" type="Label" parent="DialogLayer/NewScriptTriggerDialog/Margins/Content/Prompt/Margin/VBox"]
107 | margin_right = 296.0
108 | margin_bottom = 14.0
109 | size_flags_horizontal = 3
110 | size_flags_vertical = 3
111 | custom_colors/font_color = Color( 1, 0, 0, 1 )
112 | text = "Trigger must have a name."
113 | valign = 1
114 |
115 | [node name="NewStateDialog" type="ConfirmationDialog" parent="DialogLayer"]
116 | anchor_left = 0.000976562
117 | anchor_right = 0.000976562
118 | anchor_bottom = 0.00333333
119 | margin_left = 329.0
120 | margin_top = 229.0
121 | margin_right = 679.0
122 | margin_bottom = 371.0
123 | rect_min_size = Vector2( 350, 160 )
124 | window_title = "New state"
125 | script = ExtResource( 3 )
126 | __meta__ = {
127 | "_edit_group_": true,
128 | "_edit_use_anchors_": false
129 | }
130 |
131 | [node name="Margins" type="MarginContainer" parent="DialogLayer/NewStateDialog"]
132 | margin_left = 8.0
133 | margin_top = 8.0
134 | margin_right = 342.0
135 | margin_bottom = 124.0
136 | custom_constants/margin_right = 16
137 | custom_constants/margin_top = 16
138 | custom_constants/margin_left = 16
139 | custom_constants/margin_bottom = 16
140 | __meta__ = {
141 | "_edit_use_anchors_": false
142 | }
143 |
144 | [node name="Content" type="VBoxContainer" parent="DialogLayer/NewStateDialog/Margins"]
145 | margin_left = 16.0
146 | margin_top = 16.0
147 | margin_right = 318.0
148 | margin_bottom = 100.0
149 | size_flags_horizontal = 3
150 | size_flags_vertical = 3
151 | custom_constants/separation = 10
152 | __meta__ = {
153 | "_edit_use_anchors_": false
154 | }
155 |
156 | [node name="StateName" type="LineEdit" parent="DialogLayer/NewStateDialog/Margins/Content"]
157 | margin_right = 302.0
158 | margin_bottom = 24.0
159 | placeholder_text = "State name"
160 | caret_blink = true
161 | caret_blink_speed = 0.5
162 |
163 | [node name="Prompt" type="PanelContainer" parent="DialogLayer/NewStateDialog/Margins/Content"]
164 | margin_top = 34.0
165 | margin_right = 302.0
166 | margin_bottom = 68.0
167 | custom_styles/panel = SubResource( 1 )
168 |
169 | [node name="Margin" type="MarginContainer" parent="DialogLayer/NewStateDialog/Margins/Content/Prompt"]
170 | margin_right = 302.0
171 | margin_bottom = 34.0
172 | custom_constants/margin_right = 10
173 | custom_constants/margin_top = 10
174 | custom_constants/margin_left = 10
175 | custom_constants/margin_bottom = 10
176 |
177 | [node name="Name" type="Label" parent="DialogLayer/NewStateDialog/Margins/Content/Prompt/Margin"]
178 | margin_left = 10.0
179 | margin_top = 10.0
180 | margin_right = 292.0
181 | margin_bottom = 24.0
182 | size_flags_horizontal = 3
183 | size_flags_vertical = 3
184 | custom_colors/font_color = Color( 1, 0, 0, 1 )
185 | text = "State must have a name."
186 | valign = 1
187 | autowrap = true
188 |
189 | [node name="TimerDurationDialog" type="AcceptDialog" parent="DialogLayer"]
190 | margin_right = 181.0
191 | margin_bottom = 100.0
192 | window_title = "Set timer duration"
193 | script = ExtResource( 4 )
194 | __meta__ = {
195 | "_edit_group_": true,
196 | "_edit_use_anchors_": false
197 | }
198 |
199 | [node name="Margins" type="MarginContainer" parent="DialogLayer/TimerDurationDialog"]
200 | margin_left = 8.0
201 | margin_top = 8.0
202 | margin_right = 173.0
203 | margin_bottom = 64.0
204 | custom_constants/margin_right = 16
205 | custom_constants/margin_top = 16
206 | custom_constants/margin_left = 16
207 | custom_constants/margin_bottom = 16
208 | __meta__ = {
209 | "_edit_use_anchors_": false
210 | }
211 |
212 | [node name="Content" type="HBoxContainer" parent="DialogLayer/TimerDurationDialog/Margins"]
213 | margin_left = 16.0
214 | margin_top = 16.0
215 | margin_right = 149.0
216 | margin_bottom = 40.0
217 | size_flags_horizontal = 3
218 | size_flags_vertical = 3
219 | custom_constants/separation = 10
220 |
221 | [node name="Duration" type="LineEdit" parent="DialogLayer/TimerDurationDialog/Margins/Content"]
222 | margin_right = 101.0
223 | margin_bottom = 24.0
224 | size_flags_horizontal = 3
225 | size_flags_stretch_ratio = 3.0
226 | caret_blink = true
227 | caret_blink_speed = 0.5
228 |
229 | [node name="Seconds" type="Label" parent="DialogLayer/TimerDurationDialog/Margins/Content"]
230 | margin_left = 111.0
231 | margin_top = 5.0
232 | margin_right = 133.0
233 | margin_bottom = 19.0
234 | text = "sec"
235 |
236 | [node name="InputActionDialog" type="AcceptDialog" parent="DialogLayer"]
237 | margin_right = 306.0
238 | margin_bottom = 368.0
239 | window_title = "Select input actions"
240 | script = ExtResource( 6 )
241 | __meta__ = {
242 | "_edit_group_": true,
243 | "_edit_use_anchors_": false
244 | }
245 |
246 | [node name="Margins" type="MarginContainer" parent="DialogLayer/InputActionDialog"]
247 | margin_left = 8.0
248 | margin_top = 8.0
249 | margin_right = 298.0
250 | margin_bottom = 332.0
251 | custom_constants/margin_right = 16
252 | custom_constants/margin_top = 16
253 | custom_constants/margin_left = 16
254 | custom_constants/margin_bottom = 16
255 | __meta__ = {
256 | "_edit_use_anchors_": false
257 | }
258 |
259 | [node name="Content" type="VBoxContainer" parent="DialogLayer/InputActionDialog/Margins"]
260 | margin_left = 16.0
261 | margin_top = 16.0
262 | margin_right = 274.0
263 | margin_bottom = 308.0
264 | custom_constants/separation = 10
265 |
266 | [node name="Header" type="HBoxContainer" parent="DialogLayer/InputActionDialog/Margins/Content"]
267 | margin_right = 258.0
268 | margin_bottom = 24.0
269 | custom_constants/separation = 10
270 |
271 | [node name="FilterMargins" type="MarginContainer" parent="DialogLayer/InputActionDialog/Margins/Content/Header"]
272 | margin_right = 141.0
273 | margin_bottom = 24.0
274 | size_flags_horizontal = 3
275 |
276 | [node name="Filter" type="LineEdit" parent="DialogLayer/InputActionDialog/Margins/Content/Header/FilterMargins"]
277 | margin_right = 141.0
278 | margin_bottom = 24.0
279 | placeholder_text = "Filter actions"
280 |
281 | [node name="ClearButton" type="Button" parent="DialogLayer/InputActionDialog/Margins/Content/Header"]
282 | margin_left = 151.0
283 | margin_right = 258.0
284 | margin_bottom = 24.0
285 | text = "Clear selection"
286 |
287 | [node name="ActionContainer" type="ScrollContainer" parent="DialogLayer/InputActionDialog/Margins/Content"]
288 | margin_top = 34.0
289 | margin_right = 258.0
290 | margin_bottom = 248.0
291 | size_flags_vertical = 3
292 |
293 | [node name="Margins" type="MarginContainer" parent="DialogLayer/InputActionDialog/Margins/Content/ActionContainer"]
294 | margin_right = 258.0
295 | margin_bottom = 20.0
296 | size_flags_horizontal = 3
297 | custom_constants/margin_right = 10
298 | custom_constants/margin_top = 10
299 | custom_constants/margin_left = 10
300 | custom_constants/margin_bottom = 10
301 |
302 | [node name="Actions" type="VBoxContainer" parent="DialogLayer/InputActionDialog/Margins/Content/ActionContainer/Margins"]
303 | margin_left = 10.0
304 | margin_top = 10.0
305 | margin_right = 248.0
306 | margin_bottom = 10.0
307 | size_flags_horizontal = 3
308 |
309 | [node name="ValidationPanel" type="PanelContainer" parent="DialogLayer/InputActionDialog/Margins/Content"]
310 | margin_top = 258.0
311 | margin_right = 258.0
312 | margin_bottom = 292.0
313 | custom_styles/panel = SubResource( 2 )
314 |
315 | [node name="MarginContainer" type="MarginContainer" parent="DialogLayer/InputActionDialog/Margins/Content/ValidationPanel"]
316 | margin_right = 258.0
317 | margin_bottom = 34.0
318 | custom_constants/margin_right = 10
319 | custom_constants/margin_top = 10
320 | custom_constants/margin_left = 10
321 | custom_constants/margin_bottom = 10
322 |
323 | [node name="Label" type="Label" parent="DialogLayer/InputActionDialog/Margins/Content/ValidationPanel/MarginContainer"]
324 | margin_left = 10.0
325 | margin_top = 10.0
326 | margin_right = 248.0
327 | margin_bottom = 24.0
328 | custom_colors/font_color = Color( 1, 0, 0, 1 )
329 | text = "Select at least one action"
330 | [connection signal="_end_node_move" from="VFSMGraphEdit" to="VFSMGraphEdit" method="_on_end_node_move"]
331 | [connection signal="connection_request" from="VFSMGraphEdit" to="VFSMGraphEdit" method="_on_connection_request"]
332 | [connection signal="connection_to_empty" from="VFSMGraphEdit" to="VFSMGraphEdit" method="_on_connection_to_empty"]
333 | [connection signal="disconnection_request" from="VFSMGraphEdit" to="VFSMGraphEdit" method="_on_disconnection_request"]
334 | [connection signal="confirmed" from="DialogLayer/NewScriptTriggerDialog" to="DialogLayer/NewScriptTriggerDialog" method="_on_confirmed"]
335 | [connection signal="new_trigger_created" from="DialogLayer/NewScriptTriggerDialog" to="VFSMGraphEdit" method="_on_Dialog_new_trigger_created"]
336 | [connection signal="trigger_name_request" from="DialogLayer/NewScriptTriggerDialog" to="VFSMGraphEdit" method="_on_Dialog_trigger_name_request"]
337 | [connection signal="text_changed" from="DialogLayer/NewScriptTriggerDialog/Margins/Content/TriggerName" to="DialogLayer/NewScriptTriggerDialog" method="_on_TriggerName_text_changed"]
338 | [connection signal="confirmed" from="DialogLayer/NewStateDialog" to="DialogLayer/NewStateDialog" method="_on_confirmed"]
339 | [connection signal="state_name_request" from="DialogLayer/NewStateDialog" to="VFSMGraphEdit" method="_on_StateCreateDialog_state_name_request"]
340 | [connection signal="text_changed" from="DialogLayer/NewStateDialog/Margins/Content/StateName" to="DialogLayer/NewStateDialog" method="_on_StateName_text_changed"]
341 | [connection signal="confirmed" from="DialogLayer/TimerDurationDialog" to="DialogLayer/TimerDurationDialog" method="_on_confirmed"]
342 | [connection signal="text_changed" from="DialogLayer/TimerDurationDialog/Margins/Content/Duration" to="DialogLayer/TimerDurationDialog" method="_on_Duration_text_changed"]
343 | [connection signal="confirmed" from="DialogLayer/InputActionDialog" to="DialogLayer/InputActionDialog" method="_on_confirmed"]
344 | [connection signal="text_changed" from="DialogLayer/InputActionDialog/Margins/Content/Header/FilterMargins/Filter" to="DialogLayer/InputActionDialog" method="_on_Filter_text_changed"]
345 | [connection signal="pressed" from="DialogLayer/InputActionDialog/Margins/Content/Header/ClearButton" to="DialogLayer/InputActionDialog" method="_on_ClearButton_pressed"]
346 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/simple_traffic_lights/simple_traffic_lights.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=32 format=2]
2 |
3 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm.gd" type="Script" id=1]
4 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_state.gd" type="Script" id=2]
5 | [ext_resource path="res://addons/visual_fsm/visual_fsm.gd" type="Script" id=3]
6 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_trigger_action.gd" type="Script" id=4]
7 | [ext_resource path="res://addons/visual_fsm/demos/simple_traffic_lights/traffic_lights.gd" type="Script" id=5]
8 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_trigger_timer.gd" type="Script" id=6]
9 |
10 | [sub_resource type="GDScript" id=1]
11 | script/source = "# State: Green
12 | extends VFSMStateBase
13 |
14 |
15 | func enter() -> void:
16 | pass
17 |
18 |
19 | func update(_object, _delta: float) -> void:
20 | var traffic_lights := _object as VFSMDemoTrafficLightsController
21 | traffic_lights.green()
22 | traffic_lights.current_state_name = name
23 |
24 |
25 | func exit() -> void:
26 | pass
27 | "
28 |
29 | [sub_resource type="Resource" id=2]
30 | script = ExtResource( 2 )
31 | vfsm_id = 0
32 | name = "Green"
33 | position = Vector2( 380, 20 )
34 | trigger_ids = [ 0 ]
35 | custom_script = SubResource( 1 )
36 |
37 | [sub_resource type="GDScript" id=3]
38 | script/source = "# State: Yellow
39 | extends VFSMStateBase
40 |
41 |
42 | func enter() -> void:
43 | pass
44 |
45 |
46 | func update(_object, _delta: float) -> void:
47 | var traffic_lights := _object as VFSMDemoTrafficLightsController
48 | traffic_lights.yellow()
49 | traffic_lights.current_state_name = name
50 |
51 |
52 | func exit() -> void:
53 | pass
54 | "
55 |
56 | [sub_resource type="Resource" id=4]
57 | script = ExtResource( 2 )
58 | vfsm_id = 1
59 | name = "Yellow"
60 | position = Vector2( 766, 128 )
61 | trigger_ids = [ 1 ]
62 | custom_script = SubResource( 3 )
63 |
64 | [sub_resource type="GDScript" id=5]
65 | script/source = "# State: Red
66 | extends VFSMStateBase
67 |
68 |
69 | func enter() -> void:
70 | pass
71 |
72 |
73 | func update(_object, _delta: float) -> void:
74 | var traffic_lights := _object as VFSMDemoTrafficLightsController
75 | traffic_lights.red()
76 | traffic_lights.current_state_name = name
77 |
78 |
79 | func exit() -> void:
80 | pass
81 | "
82 |
83 | [sub_resource type="Resource" id=6]
84 | script = ExtResource( 2 )
85 | vfsm_id = 2
86 | name = "Red"
87 | position = Vector2( 439, 248 )
88 | trigger_ids = [ 2 ]
89 | custom_script = SubResource( 5 )
90 |
91 | [sub_resource type="Resource" id=7]
92 | script = ExtResource( 6 )
93 | vfsm_id = 0
94 | name = ""
95 | duration = 1
96 |
97 | [sub_resource type="Resource" id=8]
98 | script = ExtResource( 6 )
99 | vfsm_id = 1
100 | name = ""
101 | duration = 0.5
102 |
103 | [sub_resource type="Resource" id=9]
104 | script = ExtResource( 6 )
105 | vfsm_id = 2
106 | name = ""
107 | duration = 1.5
108 |
109 | [sub_resource type="Resource" id=10]
110 | script = ExtResource( 1 )
111 | start_state_vfsm_id = 0
112 | start_position = Vector2( 100, 100 )
113 | states = [ SubResource( 2 ), SubResource( 4 ), SubResource( 6 ) ]
114 | triggers = [ SubResource( 7 ), SubResource( 8 ), SubResource( 9 ) ]
115 | transitions = [ 0, 0, 1, 1, 1, 2, 2, 2, 0 ]
116 |
117 | [sub_resource type="StyleBoxFlat" id=11]
118 | bg_color = Color( 0.00784314, 1, 0.184314, 1 )
119 |
120 | [sub_resource type="StyleBoxFlat" id=12]
121 | bg_color = Color( 0, 0, 0, 1 )
122 |
123 | [sub_resource type="StyleBoxFlat" id=13]
124 | bg_color = Color( 0.921569, 0.909804, 0.137255, 1 )
125 |
126 | [sub_resource type="StyleBoxFlat" id=14]
127 | bg_color = Color( 1, 0, 0, 1 )
128 |
129 | [sub_resource type="GDScript" id=15]
130 | script/source = "# State: Green
131 | extends VFSMStateBase
132 |
133 |
134 | func enter() -> void:
135 | pass
136 |
137 |
138 | func update(_object, _delta: float) -> void:
139 | var traffic_lights := _object as VFSMDemoTrafficLightsController
140 | traffic_lights.green()
141 |
142 |
143 | func exit() -> void:
144 | pass
145 | "
146 |
147 | [sub_resource type="Resource" id=16]
148 | script = ExtResource( 2 )
149 | vfsm_id = 0
150 | name = "Green"
151 | position = Vector2( 420, -20 )
152 | trigger_ids = [ 0 ]
153 | custom_script = SubResource( 15 )
154 |
155 | [sub_resource type="GDScript" id=17]
156 | script/source = "# State: Yellow
157 | extends VFSMStateBase
158 |
159 |
160 | func enter() -> void:
161 | pass
162 |
163 |
164 | func update(_object, _delta: float) -> void:
165 | var traffic_lights := _object as VFSMDemoTrafficLightsController
166 | traffic_lights.yellow()
167 |
168 |
169 | func exit() -> void:
170 | pass
171 | "
172 |
173 | [sub_resource type="Resource" id=18]
174 | script = ExtResource( 2 )
175 | vfsm_id = 1
176 | name = "Yellow"
177 | position = Vector2( 420, 120 )
178 | trigger_ids = [ 1, 2 ]
179 | custom_script = SubResource( 17 )
180 |
181 | [sub_resource type="GDScript" id=19]
182 | script/source = "# State: Red
183 | extends VFSMStateBase
184 |
185 |
186 | func enter() -> void:
187 | pass
188 |
189 |
190 | func update(_object, _delta: float) -> void:
191 | var traffic_lights := _object as VFSMDemoTrafficLightsController
192 | traffic_lights.red()
193 |
194 |
195 | func exit() -> void:
196 | pass
197 | "
198 |
199 | [sub_resource type="Resource" id=20]
200 | script = ExtResource( 2 )
201 | vfsm_id = 2
202 | name = "Red"
203 | position = Vector2( 421, 302 )
204 | trigger_ids = [ 3 ]
205 | custom_script = SubResource( 19 )
206 |
207 | [sub_resource type="Resource" id=21]
208 | script = ExtResource( 4 )
209 | vfsm_id = 0
210 | name = ""
211 | action_list = [ "ui_down" ]
212 |
213 | [sub_resource type="Resource" id=22]
214 | script = ExtResource( 4 )
215 | vfsm_id = 1
216 | name = ""
217 | action_list = [ "ui_up" ]
218 |
219 | [sub_resource type="Resource" id=23]
220 | script = ExtResource( 4 )
221 | vfsm_id = 2
222 | name = ""
223 | action_list = [ "ui_down" ]
224 |
225 | [sub_resource type="Resource" id=24]
226 | script = ExtResource( 4 )
227 | vfsm_id = 3
228 | name = ""
229 | action_list = [ "ui_up" ]
230 |
231 | [sub_resource type="Resource" id=25]
232 | script = ExtResource( 1 )
233 | start_state_vfsm_id = 0
234 | start_position = Vector2( 100, 100 )
235 | states = [ SubResource( 16 ), SubResource( 18 ), SubResource( 20 ) ]
236 | triggers = [ SubResource( 21 ), SubResource( 22 ), SubResource( 23 ), SubResource( 24 ) ]
237 | transitions = [ 0, 0, 1, 1, 1, 0, 1, 2, 2, 2, 3, 1 ]
238 |
239 | [node name="TrafficLightsDemo" type="Node2D"]
240 |
241 | [node name="TimedTrafficLights" type="Node2D" parent="."]
242 | position = Vector2( -63.908, 78.2032 )
243 | script = ExtResource( 5 )
244 |
245 | [node name="VisualFSM" type="Node" parent="TimedTrafficLights"]
246 | script = ExtResource( 3 )
247 | finite_state_machine = SubResource( 10 )
248 |
249 | [node name="Label2" type="Label" parent="TimedTrafficLights"]
250 | margin_left = 257.217
251 | margin_top = 16.9993
252 | margin_right = 465.217
253 | margin_bottom = 47.9993
254 | text = "States switch based on a timer
255 | "
256 | __meta__ = {
257 | "_edit_use_anchors_": false
258 | }
259 |
260 | [node name="StateContainer" type="HBoxContainer" parent="TimedTrafficLights"]
261 | margin_left = 271.609
262 | margin_top = 32.7949
263 | margin_right = 362.609
264 | margin_bottom = 72.7949
265 | __meta__ = {
266 | "_edit_use_anchors_": false
267 | }
268 |
269 | [node name="Label" type="Label" parent="TimedTrafficLights/StateContainer"]
270 | margin_top = 13.0
271 | margin_right = 87.0
272 | margin_bottom = 27.0
273 | text = "Current state:"
274 |
275 | [node name="State" type="Label" parent="TimedTrafficLights/StateContainer"]
276 | margin_left = 91.0
277 | margin_top = 13.0
278 | margin_right = 91.0
279 | margin_bottom = 27.0
280 | __meta__ = {
281 | "_edit_use_anchors_": false
282 | }
283 |
284 | [node name="TrafficLights" type="Panel" parent="TimedTrafficLights"]
285 | margin_left = 320.0
286 | margin_top = 86.0
287 | margin_right = 360.0
288 | margin_bottom = 234.0
289 | __meta__ = {
290 | "_edit_use_anchors_": false
291 | }
292 |
293 | [node name="Green" type="Node2D" parent="TimedTrafficLights/TrafficLights"]
294 | position = Vector2( 0, 0.840912 )
295 |
296 | [node name="Lit" type="Panel" parent="TimedTrafficLights/TrafficLights/Green"]
297 | margin_left = 6.72717
298 | margin_top = 13.4543
299 | margin_right = 46.7272
300 | margin_bottom = 53.4543
301 | rect_scale = Vector2( 0.68, 0.68 )
302 | custom_styles/panel = SubResource( 11 )
303 | __meta__ = {
304 | "_edit_use_anchors_": false
305 | }
306 |
307 | [node name="Cover" type="Panel" parent="TimedTrafficLights/TrafficLights/Green"]
308 | margin_left = 6.72717
309 | margin_top = 13.4543
310 | margin_right = 46.7272
311 | margin_bottom = 53.4543
312 | rect_scale = Vector2( 0.68, 0.68 )
313 | custom_styles/panel = SubResource( 12 )
314 | __meta__ = {
315 | "_edit_use_anchors_": false
316 | }
317 |
318 | [node name="Yellow" type="Node2D" parent="TimedTrafficLights/TrafficLights"]
319 | position = Vector2( 0, 46.2492 )
320 |
321 | [node name="Lit" type="Panel" parent="TimedTrafficLights/TrafficLights/Yellow"]
322 | margin_left = 6.72717
323 | margin_top = 13.4543
324 | margin_right = 46.7272
325 | margin_bottom = 53.4543
326 | rect_scale = Vector2( 0.68, 0.68 )
327 | custom_styles/panel = SubResource( 13 )
328 | __meta__ = {
329 | "_edit_use_anchors_": false
330 | }
331 |
332 | [node name="Cover" type="Panel" parent="TimedTrafficLights/TrafficLights/Yellow"]
333 | margin_left = 6.72717
334 | margin_top = 13.4543
335 | margin_right = 46.7272
336 | margin_bottom = 53.4543
337 | rect_scale = Vector2( 0.68, 0.68 )
338 | custom_styles/panel = SubResource( 12 )
339 |
340 | [node name="Red" type="Node2D" parent="TimedTrafficLights/TrafficLights"]
341 | position = Vector2( 0, 90.8166 )
342 |
343 | [node name="Lit" type="Panel" parent="TimedTrafficLights/TrafficLights/Red"]
344 | margin_left = 6.72717
345 | margin_top = 13.4543
346 | margin_right = 46.7272
347 | margin_bottom = 53.4543
348 | rect_scale = Vector2( 0.68, 0.68 )
349 | custom_styles/panel = SubResource( 14 )
350 | __meta__ = {
351 | "_edit_use_anchors_": false
352 | }
353 |
354 | [node name="Cover" type="Panel" parent="TimedTrafficLights/TrafficLights/Red"]
355 | margin_left = 6.72717
356 | margin_top = 13.4543
357 | margin_right = 46.7272
358 | margin_bottom = 53.4543
359 | rect_scale = Vector2( 0.68, 0.68 )
360 | custom_styles/panel = SubResource( 12 )
361 |
362 | [node name="ActionTrafficLights" type="Node2D" parent="."]
363 | position = Vector2( 244.7, 82.4076 )
364 | script = ExtResource( 5 )
365 |
366 | [node name="VisualFSM" type="Node" parent="ActionTrafficLights"]
367 | script = ExtResource( 3 )
368 | finite_state_machine = SubResource( 25 )
369 |
370 | [node name="Label" type="Label" parent="ActionTrafficLights"]
371 | margin_left = 266.609
372 | margin_top = 21.7949
373 | margin_right = 499.609
374 | margin_bottom = 69.7949
375 | text = "States switch based on input actions
376 |
377 | Press up or down to switch state."
378 | __meta__ = {
379 | "_edit_use_anchors_": false
380 | }
381 |
382 | [node name="StateContainer" type="HBoxContainer" parent="ActionTrafficLights"]
383 | visible = false
384 | margin_left = -36.999
385 | margin_top = 28.5905
386 | margin_right = 54.001
387 | margin_bottom = 68.5905
388 | __meta__ = {
389 | "_edit_use_anchors_": false
390 | }
391 |
392 | [node name="Label" type="Label" parent="ActionTrafficLights/StateContainer"]
393 | margin_top = 13.0
394 | margin_right = 87.0
395 | margin_bottom = 27.0
396 | text = "Current state:"
397 |
398 | [node name="State" type="Label" parent="ActionTrafficLights/StateContainer"]
399 | margin_left = 91.0
400 | margin_top = 13.0
401 | margin_right = 91.0
402 | margin_bottom = 27.0
403 | __meta__ = {
404 | "_edit_use_anchors_": false
405 | }
406 |
407 | [node name="TrafficLights" type="Panel" parent="ActionTrafficLights"]
408 | margin_left = 320.0
409 | margin_top = 86.0
410 | margin_right = 360.0
411 | margin_bottom = 234.0
412 | __meta__ = {
413 | "_edit_use_anchors_": false
414 | }
415 |
416 | [node name="Green" type="Node2D" parent="ActionTrafficLights/TrafficLights"]
417 | position = Vector2( 0, 0.840912 )
418 |
419 | [node name="Lit" type="Panel" parent="ActionTrafficLights/TrafficLights/Green"]
420 | margin_left = 6.72717
421 | margin_top = 13.4543
422 | margin_right = 46.7272
423 | margin_bottom = 53.4543
424 | rect_scale = Vector2( 0.68, 0.68 )
425 | custom_styles/panel = SubResource( 11 )
426 | __meta__ = {
427 | "_edit_use_anchors_": false
428 | }
429 |
430 | [node name="Cover" type="Panel" parent="ActionTrafficLights/TrafficLights/Green"]
431 | margin_left = 6.72717
432 | margin_top = 13.4543
433 | margin_right = 46.7272
434 | margin_bottom = 53.4543
435 | rect_scale = Vector2( 0.68, 0.68 )
436 | custom_styles/panel = SubResource( 12 )
437 |
438 | [node name="Yellow" type="Node2D" parent="ActionTrafficLights/TrafficLights"]
439 | position = Vector2( 0, 46.2492 )
440 |
441 | [node name="Lit" type="Panel" parent="ActionTrafficLights/TrafficLights/Yellow"]
442 | margin_left = 6.72717
443 | margin_top = 13.4543
444 | margin_right = 46.7272
445 | margin_bottom = 53.4543
446 | rect_scale = Vector2( 0.68, 0.68 )
447 | custom_styles/panel = SubResource( 13 )
448 | __meta__ = {
449 | "_edit_use_anchors_": false
450 | }
451 |
452 | [node name="Cover" type="Panel" parent="ActionTrafficLights/TrafficLights/Yellow"]
453 | margin_left = 6.72717
454 | margin_top = 13.4543
455 | margin_right = 46.7272
456 | margin_bottom = 53.4543
457 | rect_scale = Vector2( 0.68, 0.68 )
458 | custom_styles/panel = SubResource( 12 )
459 |
460 | [node name="Red" type="Node2D" parent="ActionTrafficLights/TrafficLights"]
461 | position = Vector2( 0, 90.8166 )
462 |
463 | [node name="Lit" type="Panel" parent="ActionTrafficLights/TrafficLights/Red"]
464 | margin_left = 6.72717
465 | margin_top = 13.4543
466 | margin_right = 46.7272
467 | margin_bottom = 53.4543
468 | rect_scale = Vector2( 0.68, 0.68 )
469 | custom_styles/panel = SubResource( 14 )
470 | __meta__ = {
471 | "_edit_use_anchors_": false
472 | }
473 |
474 | [node name="Cover" type="Panel" parent="ActionTrafficLights/TrafficLights/Red"]
475 | margin_left = 6.72717
476 | margin_top = 13.4543
477 | margin_right = 46.7272
478 | margin_bottom = 53.4543
479 | rect_scale = Vector2( 0.68, 0.68 )
480 | custom_styles/panel = SubResource( 12 )
481 |
482 | [node name="Node" type="Node" parent="."]
483 |
--------------------------------------------------------------------------------
/addons/visual_fsm/demos/simple_ai_character/simple_ai_character.tscn:
--------------------------------------------------------------------------------
1 | [gd_scene load_steps=44 format=2]
2 |
3 | [ext_resource path="res://addons/visual_fsm/demos/simple_ai_character/vfsm_simple_ai_jumpter.gd" type="Script" id=1]
4 | [ext_resource path="res://addons/visual_fsm/demos/icon.png" type="Texture" id=2]
5 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_state.gd" type="Script" id=3]
6 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm.gd" type="Script" id=4]
7 | [ext_resource path="res://addons/visual_fsm/visual_fsm.gd" type="Script" id=5]
8 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_trigger_script.gd" type="Script" id=6]
9 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_trigger_timer.gd" type="Script" id=7]
10 | [ext_resource path="res://addons/visual_fsm/fsm/vfsm_trigger_action.gd" type="Script" id=8]
11 |
12 | [sub_resource type="GDScript" id=1]
13 | script/source = "# State: WalkingRight
14 | extends VFSMStateBase
15 |
16 |
17 | func enter() -> void:
18 | pass
19 |
20 |
21 | func update(_object, _delta: float) -> void:
22 | var jumper := _object as VFSMDemoSimpleAIJumper
23 | jumper.get_node(\"CurrentMovingState\").text = name
24 | jumper.move_x(1) # simulates x axis input
25 |
26 |
27 | func exit() -> void:
28 | pass
29 | "
30 |
31 | [sub_resource type="Resource" id=2]
32 | script = ExtResource( 3 )
33 | vfsm_id = 1
34 | name = "WalkingRight"
35 | position = Vector2( 380, 60 )
36 | trigger_ids = [ 1, 5 ]
37 | custom_script = SubResource( 1 )
38 |
39 | [sub_resource type="GDScript" id=3]
40 | script/source = "# State: WalkingLeft
41 | extends VFSMStateBase
42 |
43 |
44 | func enter() -> void:
45 | pass
46 |
47 |
48 | func update(_object, _delta: float) -> void:
49 | var jumper := _object as VFSMDemoSimpleAIJumper
50 | jumper.get_node(\"CurrentMovingState\").text = name
51 | jumper.move_x(-1) # simulates x axis input
52 |
53 |
54 | func exit() -> void:
55 | pass
56 | "
57 |
58 | [sub_resource type="Resource" id=4]
59 | script = ExtResource( 3 )
60 | vfsm_id = 2
61 | name = "WalkingLeft"
62 | position = Vector2( 400, 260 )
63 | trigger_ids = [ 2, 5 ]
64 | custom_script = SubResource( 3 )
65 |
66 | [sub_resource type="GDScript" id=5]
67 | script/source = "# Trigger: Gap detected
68 | extends VFSMTriggerBase
69 |
70 |
71 | func enter() -> void:
72 | pass
73 |
74 |
75 | func is_triggered(_object, _delta: float) -> bool:
76 | return false
77 | "
78 |
79 | [sub_resource type="Resource" id=6]
80 | script = ExtResource( 6 )
81 | vfsm_id = 0
82 | name = "Gap detected"
83 | custom_script = SubResource( 5 )
84 |
85 | [sub_resource type="Resource" id=7]
86 | script = ExtResource( 8 )
87 | vfsm_id = 1
88 | name = ""
89 | action_list = [ "ui_left" ]
90 |
91 | [sub_resource type="Resource" id=8]
92 | script = ExtResource( 8 )
93 | vfsm_id = 2
94 | name = ""
95 | action_list = [ "ui_right" ]
96 |
97 | [sub_resource type="GDScript" id=9]
98 | script/source = "# Trigger: Right obstacle detected
99 | extends VFSMTriggerBase
100 |
101 |
102 | func enter() -> void:
103 | pass
104 |
105 |
106 | func is_triggered(_object, _delta: float) -> bool:
107 | var jumper := _object as VFSMDemoSimpleAIJumper
108 | return jumper.is_impassable_in_front()
109 | "
110 |
111 | [sub_resource type="Resource" id=10]
112 | script = ExtResource( 6 )
113 | vfsm_id = 3
114 | name = "Right obstacle detected"
115 | custom_script = SubResource( 9 )
116 |
117 | [sub_resource type="GDScript" id=28]
118 | script/source = "# Trigger: Left obstacle detected
119 | extends VFSMTriggerBase
120 |
121 |
122 | func enter() -> void:
123 | pass
124 |
125 |
126 | func is_triggered(_object, _delta: float) -> bool:
127 | var jumper := _object as VFSMDemoSimpleAIJumper
128 | return jumper.left_step_detector.is_colliding()
129 | "
130 |
131 | [sub_resource type="Resource" id=29]
132 | script = ExtResource( 6 )
133 | vfsm_id = 4
134 | name = "Left obstacle detected"
135 | custom_script = SubResource( 28 )
136 |
137 | [sub_resource type="GDScript" id=30]
138 | script/source = "# Trigger: Impassable detected
139 | extends VFSMTriggerBase
140 |
141 |
142 | func enter() -> void:
143 | pass
144 |
145 |
146 | func is_triggered(_object, _delta: float) -> bool:
147 | var jumper := _object as VFSMDemoSimpleAIJumper
148 | return jumper.is_impassable_in_front()
149 | "
150 |
151 | [sub_resource type="Resource" id=31]
152 | script = ExtResource( 6 )
153 | vfsm_id = 5
154 | name = "Impassable detected"
155 | custom_script = SubResource( 30 )
156 |
157 | [sub_resource type="Resource" id=11]
158 | script = ExtResource( 4 )
159 | start_state_vfsm_id = 1
160 | start_position = Vector2( 100, 100 )
161 | states = [ SubResource( 2 ), SubResource( 4 ) ]
162 | triggers = [ SubResource( 6 ), SubResource( 7 ), SubResource( 8 ), SubResource( 10 ), SubResource( 29 ), SubResource( 31 ) ]
163 | transitions = [ 1, 1, 2, 1, 5, 2, 2, 2, 1, 2, 5, 1 ]
164 |
165 | [sub_resource type="GDScript" id=12]
166 | script/source = "# State: Grounded
167 | extends VFSMStateBase
168 |
169 |
170 | func enter() -> void:
171 | pass
172 |
173 |
174 | func update(_object, _delta: float) -> void:
175 | var jumper := _object as VFSMDemoSimpleAIJumper
176 | jumper.get_node(\"CurrentAirState\").text = name
177 |
178 |
179 | func exit() -> void:
180 | pass
181 | "
182 |
183 | [sub_resource type="Resource" id=13]
184 | script = ExtResource( 3 )
185 | vfsm_id = 0
186 | name = "Grounded"
187 | position = Vector2( 380, 100 )
188 | trigger_ids = [ 1, 4, 2 ]
189 | custom_script = SubResource( 12 )
190 |
191 | [sub_resource type="GDScript" id=14]
192 | script/source = "# State: Jumping
193 | extends VFSMStateBase
194 |
195 |
196 | func enter() -> void:
197 | pass
198 |
199 |
200 | func update(_object, _delta: float) -> void:
201 | var jumper := _object as VFSMDemoSimpleAIJumper
202 | jumper.get_node(\"CurrentAirState\").text = name
203 | jumper.jump()
204 |
205 |
206 | func exit() -> void:
207 | pass
208 | "
209 |
210 | [sub_resource type="Resource" id=15]
211 | script = ExtResource( 3 )
212 | vfsm_id = 1
213 | name = "Jumping"
214 | position = Vector2( 840, 100 )
215 | trigger_ids = [ 2 ]
216 | custom_script = SubResource( 14 )
217 |
218 | [sub_resource type="GDScript" id=16]
219 | script/source = "# State: Airborne
220 | extends VFSMStateBase
221 |
222 |
223 | func enter() -> void:
224 | pass
225 |
226 |
227 | func update(_object, _delta: float) -> void:
228 | var jumper := _object as VFSMDemoSimpleAIJumper
229 | jumper.get_node(\"CurrentAirState\").text = name
230 |
231 |
232 | func exit() -> void:
233 | pass
234 | "
235 |
236 | [sub_resource type="Resource" id=17]
237 | script = ExtResource( 3 )
238 | vfsm_id = 2
239 | name = "Airborne"
240 | position = Vector2( 560, -100 )
241 | trigger_ids = [ 3 ]
242 | custom_script = SubResource( 16 )
243 |
244 | [sub_resource type="GDScript" id=34]
245 | script/source = "# State: Falling
246 | extends VFSMStateBase
247 |
248 |
249 | func enter() -> void:
250 | pass
251 |
252 |
253 | func update(_object, _delta: float) -> void:
254 | var jumper := _object as VFSMDemoSimpleAIJumper
255 | jumper.get_node(\"CurrentAirState\").text = name
256 |
257 |
258 | func exit() -> void:
259 | pass
260 | "
261 |
262 | [sub_resource type="Resource" id=35]
263 | script = ExtResource( 3 )
264 | vfsm_id = 3
265 | name = "Falling"
266 | position = Vector2( 380, 380 )
267 | trigger_ids = [ 3 ]
268 | custom_script = SubResource( 34 )
269 |
270 | [sub_resource type="Resource" id=18]
271 | script = ExtResource( 7 )
272 | vfsm_id = 0
273 | name = ""
274 | duration = 1
275 |
276 | [sub_resource type="GDScript" id=19]
277 | script/source = "# Trigger: Gap detected
278 | extends VFSMTriggerBase
279 |
280 |
281 | func enter() -> void:
282 | pass
283 |
284 |
285 | func is_triggered(_object, _delta: float) -> bool:
286 | var jumper := _object as VFSMDemoSimpleAIJumper
287 | return jumper.is_gap_in_front()
288 | "
289 |
290 | [sub_resource type="Resource" id=20]
291 | script = ExtResource( 6 )
292 | vfsm_id = 1
293 | name = "Gap detected"
294 | custom_script = SubResource( 19 )
295 |
296 | [sub_resource type="GDScript" id=21]
297 | script/source = "# Trigger: Left ground
298 | extends VFSMTriggerBase
299 |
300 |
301 | func enter() -> void:
302 | pass
303 |
304 |
305 | func is_triggered(_object, _delta: float) -> bool:
306 | var jumper := _object as VFSMDemoSimpleAIJumper
307 | return not jumper.is_on_floor()
308 | "
309 |
310 | [sub_resource type="Resource" id=22]
311 | script = ExtResource( 6 )
312 | vfsm_id = 2
313 | name = "Left ground"
314 | custom_script = SubResource( 21 )
315 |
316 | [sub_resource type="GDScript" id=23]
317 | script/source = "# Trigger: Landed
318 | extends VFSMTriggerBase
319 |
320 |
321 | func enter() -> void:
322 | pass
323 |
324 |
325 | func is_triggered(_object, _delta: float) -> bool:
326 | var jumper := _object as VFSMDemoSimpleAIJumper
327 | return jumper.is_on_floor()
328 | "
329 |
330 | [sub_resource type="Resource" id=24]
331 | script = ExtResource( 6 )
332 | vfsm_id = 3
333 | name = "Landed"
334 | custom_script = SubResource( 23 )
335 |
336 | [sub_resource type="GDScript" id=32]
337 | script/source = "# Trigger: Jumpable obstacle
338 | extends VFSMTriggerBase
339 |
340 |
341 | func enter() -> void:
342 | pass
343 |
344 |
345 | func is_triggered(_object, _delta: float) -> bool:
346 | var jumper := _object as VFSMDemoSimpleAIJumper
347 | return jumper.is_jumpable_in_front()
348 | "
349 |
350 | [sub_resource type="Resource" id=33]
351 | script = ExtResource( 6 )
352 | vfsm_id = 4
353 | name = "Jumpable obstacle"
354 | custom_script = SubResource( 32 )
355 |
356 | [sub_resource type="Resource" id=25]
357 | script = ExtResource( 4 )
358 | start_state_vfsm_id = 0
359 | start_position = Vector2( 100, 100 )
360 | states = [ SubResource( 13 ), SubResource( 15 ), SubResource( 17 ), SubResource( 35 ) ]
361 | triggers = [ SubResource( 18 ), SubResource( 20 ), SubResource( 22 ), SubResource( 24 ), SubResource( 33 ) ]
362 | transitions = [ 0, 1, 1, 0, 4, 1, 0, 2, 3, 1, 2, 2, 2, 3, 0, 3, 3, 0 ]
363 |
364 | [sub_resource type="CapsuleShape2D" id=26]
365 | radius = 29.1352
366 | height = 3.62146
367 |
368 | [sub_resource type="RectangleShape2D" id=27]
369 | extents = Vector2( 30.7244, 31.4757 )
370 |
371 | [node name="World" type="Node2D"]
372 |
373 | [node name="Instructions" type="Label" parent="."]
374 | margin_left = -31.2875
375 | margin_top = -152.816
376 | margin_right = 228.712
377 | margin_bottom = -104.816
378 | text = "Press left/right to change direction.
379 | Automatically changes direction in front of impassable obstacles
380 | Will jump automatically over small obstacles and gaps"
381 | __meta__ = {
382 | "_edit_use_anchors_": false
383 | }
384 |
385 | [node name="SimpleAIJumper" type="KinematicBody2D" parent="."]
386 | position = Vector2( 0, 6.35678 )
387 | collision_mask = 2
388 | script = ExtResource( 1 )
389 | speed = 50.0
390 | jump_speed = 80.0
391 | gravity = 98.0
392 |
393 | [node name="MovementFSM" type="Node" parent="SimpleAIJumper"]
394 | script = ExtResource( 5 )
395 | finite_state_machine = SubResource( 11 )
396 |
397 | [node name="JumpingFSM" type="Node" parent="SimpleAIJumper"]
398 | script = ExtResource( 5 )
399 | finite_state_machine = SubResource( 25 )
400 |
401 | [node name="Sprite" type="Sprite" parent="SimpleAIJumper"]
402 | texture = ExtResource( 2 )
403 |
404 | [node name="CollisionShape2D" type="CollisionShape2D" parent="SimpleAIJumper"]
405 | shape = SubResource( 26 )
406 |
407 | [node name="Camera2D" type="Camera2D" parent="SimpleAIJumper"]
408 | current = true
409 |
410 | [node name="RightGapDetector" type="RayCast2D" parent="SimpleAIJumper"]
411 | position = Vector2( 10, 0 )
412 | enabled = true
413 | cast_to = Vector2( 0, 75 )
414 | collision_mask = 2
415 |
416 | [node name="LeftGapDetector" type="RayCast2D" parent="SimpleAIJumper"]
417 | position = Vector2( -10, 0 )
418 | enabled = true
419 | cast_to = Vector2( 0, 75 )
420 | collision_mask = 2
421 |
422 | [node name="LeftBottom" type="RayCast2D" parent="SimpleAIJumper"]
423 | position = Vector2( 0, 15 )
424 | enabled = true
425 | cast_to = Vector2( -40, 0 )
426 | collision_mask = 2
427 |
428 | [node name="RightBottom" type="RayCast2D" parent="SimpleAIJumper"]
429 | position = Vector2( 0, 15 )
430 | enabled = true
431 | cast_to = Vector2( 40, 0 )
432 | collision_mask = 2
433 |
434 | [node name="LeftMiddle" type="RayCast2D" parent="SimpleAIJumper"]
435 | enabled = true
436 | cast_to = Vector2( -40, 0 )
437 | collision_mask = 2
438 |
439 | [node name="RightMiddle" type="RayCast2D" parent="SimpleAIJumper"]
440 | enabled = true
441 | cast_to = Vector2( 40, 0 )
442 | collision_mask = 2
443 |
444 | [node name="CurrentMovingState" type="Label" parent="SimpleAIJumper"]
445 | margin_left = -31.1127
446 | margin_top = -69.3762
447 | margin_right = 8.88728
448 | margin_bottom = -55.3762
449 | __meta__ = {
450 | "_edit_use_anchors_": false
451 | }
452 |
453 | [node name="CurrentAirState" type="Label" parent="SimpleAIJumper"]
454 | margin_left = -31.1127
455 | margin_top = -49.3762
456 | margin_right = 8.88728
457 | margin_bottom = -35.3762
458 | __meta__ = {
459 | "_edit_use_anchors_": false
460 | }
461 |
462 | [node name="Platforms" type="Node2D" parent="."]
463 |
464 | [node name="StaticBody2D" type="StaticBody2D" parent="Platforms"]
465 | position = Vector2( 0, 62.2965 )
466 | collision_layer = 2
467 |
468 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D"]
469 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
470 | texture = ExtResource( 2 )
471 |
472 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D"]
473 | shape = SubResource( 27 )
474 |
475 | [node name="StaticBody2D14" type="StaticBody2D" parent="Platforms"]
476 | position = Vector2( -59.4603, 62.2965 )
477 | collision_layer = 2
478 |
479 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D14"]
480 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
481 | texture = ExtResource( 2 )
482 |
483 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D14"]
484 | shape = SubResource( 27 )
485 |
486 | [node name="StaticBody2D15" type="StaticBody2D" parent="Platforms"]
487 | position = Vector2( -178.381, 62.2965 )
488 | collision_layer = 2
489 |
490 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D15"]
491 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
492 | texture = ExtResource( 2 )
493 |
494 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D15"]
495 | shape = SubResource( 27 )
496 |
497 | [node name="StaticBody2D16" type="StaticBody2D" parent="Platforms"]
498 | position = Vector2( -235.463, 62.2965 )
499 | collision_layer = 2
500 |
501 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D16"]
502 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
503 | texture = ExtResource( 2 )
504 |
505 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D16"]
506 | shape = SubResource( 27 )
507 |
508 | [node name="StaticBody2D17" type="StaticBody2D" parent="Platforms"]
509 | position = Vector2( -287.788, 62.2965 )
510 | collision_layer = 2
511 |
512 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D17"]
513 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
514 | texture = ExtResource( 2 )
515 |
516 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D17"]
517 | shape = SubResource( 27 )
518 |
519 | [node name="StaticBody2D18" type="StaticBody2D" parent="Platforms"]
520 | position = Vector2( -287.788, 5.21459 )
521 | collision_layer = 2
522 |
523 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D18"]
524 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
525 | texture = ExtResource( 2 )
526 |
527 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D18"]
528 | shape = SubResource( 27 )
529 |
530 | [node name="StaticBody2D2" type="StaticBody2D" parent="Platforms"]
531 | position = Vector2( 61.3316, 62.2965 )
532 | collision_layer = 2
533 |
534 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D2"]
535 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
536 | texture = ExtResource( 2 )
537 |
538 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D2"]
539 | shape = SubResource( 27 )
540 |
541 | [node name="StaticBody2D3" type="StaticBody2D" parent="Platforms"]
542 | position = Vector2( 119.983, 62.2965 )
543 | collision_layer = 2
544 |
545 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D3"]
546 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
547 | texture = ExtResource( 2 )
548 |
549 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D3"]
550 | shape = SubResource( 27 )
551 |
552 | [node name="StaticBody2D4" type="StaticBody2D" parent="Platforms"]
553 | position = Vector2( 247.93, 62.2965 )
554 | collision_layer = 2
555 |
556 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D4"]
557 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
558 | texture = ExtResource( 2 )
559 |
560 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D4"]
561 | shape = SubResource( 27 )
562 |
563 | [node name="StaticBody2D20" type="StaticBody2D" parent="Platforms"]
564 | position = Vector2( 1048.46, 166.568 )
565 | collision_layer = 2
566 |
567 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D20"]
568 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
569 | texture = ExtResource( 2 )
570 |
571 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D20"]
572 | shape = SubResource( 27 )
573 |
574 | [node name="StaticBody2D21" type="StaticBody2D" parent="Platforms"]
575 | position = Vector2( 1048.46, 107.705 )
576 | collision_layer = 2
577 |
578 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D21"]
579 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
580 | texture = ExtResource( 2 )
581 |
582 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D21"]
583 | shape = SubResource( 27 )
584 |
585 | [node name="StaticBody2D5" type="StaticBody2D" parent="Platforms"]
586 | position = Vector2( 308.511, 62.2965 )
587 | collision_layer = 2
588 |
589 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D5"]
590 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
591 | texture = ExtResource( 2 )
592 |
593 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D5"]
594 | shape = SubResource( 27 )
595 |
596 | [node name="StaticBody2D6" type="StaticBody2D" parent="Platforms"]
597 | position = Vector2( 365.529, 81.4378 )
598 | collision_layer = 2
599 |
600 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D6"]
601 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
602 | texture = ExtResource( 2 )
603 |
604 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D6"]
605 | shape = SubResource( 27 )
606 |
607 | [node name="StaticBody2D7" type="StaticBody2D" parent="Platforms"]
608 | position = Vector2( 427.065, 82.3549 )
609 | collision_layer = 2
610 |
611 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D7"]
612 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
613 | texture = ExtResource( 2 )
614 |
615 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D7"]
616 | shape = SubResource( 27 )
617 |
618 | [node name="StaticBody2D8" type="StaticBody2D" parent="Platforms"]
619 | position = Vector2( 477.846, 102.846 )
620 | collision_layer = 2
621 |
622 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D8"]
623 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
624 | texture = ExtResource( 2 )
625 |
626 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D8"]
627 | shape = SubResource( 27 )
628 |
629 | [node name="StaticBody2D9" type="StaticBody2D" parent="Platforms"]
630 | position = Vector2( 649.664, 145.705 )
631 | collision_layer = 2
632 |
633 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D9"]
634 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
635 | texture = ExtResource( 2 )
636 |
637 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D9"]
638 | shape = SubResource( 27 )
639 |
640 | [node name="StaticBody2D10" type="StaticBody2D" parent="Platforms"]
641 | position = Vector2( 711.082, 145.705 )
642 | collision_layer = 2
643 |
644 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D10"]
645 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
646 | texture = ExtResource( 2 )
647 |
648 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D10"]
649 | shape = SubResource( 27 )
650 |
651 | [node name="StaticBody2D11" type="StaticBody2D" parent="Platforms"]
652 | position = Vector2( 834.917, 145.705 )
653 | collision_layer = 2
654 |
655 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D11"]
656 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
657 | texture = ExtResource( 2 )
658 |
659 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D11"]
660 | shape = SubResource( 27 )
661 |
662 | [node name="StaticBody2D19" type="StaticBody2D" parent="Platforms"]
663 | position = Vector2( 536.485, 121.894 )
664 | collision_layer = 2
665 |
666 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D19"]
667 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
668 | texture = ExtResource( 2 )
669 |
670 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D19"]
671 | shape = SubResource( 27 )
672 |
673 | [node name="StaticBody2D12" type="StaticBody2D" parent="Platforms"]
674 | position = Vector2( 933.032, 165.803 )
675 | collision_layer = 2
676 |
677 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D12"]
678 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
679 | texture = ExtResource( 2 )
680 |
681 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D12"]
682 | shape = SubResource( 27 )
683 |
684 | [node name="StaticBody2D13" type="StaticBody2D" parent="Platforms"]
685 | position = Vector2( 993.613, 165.803 )
686 | collision_layer = 2
687 |
688 | [node name="Sprite" type="Sprite" parent="Platforms/StaticBody2D13"]
689 | modulate = Color( 0.129412, 0.933333, 0.0156863, 1 )
690 | texture = ExtResource( 2 )
691 |
692 | [node name="CollisionShape2D" type="CollisionShape2D" parent="Platforms/StaticBody2D13"]
693 | shape = SubResource( 27 )
694 |
--------------------------------------------------------------------------------