├── 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 | 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 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 | ![](addons/visual_fsm/resources/screenshots/demo.gif) 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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Layer 1 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | F 33 | S 34 | M 35 | 36 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------