├── plugin.cfg ├── Scripts └── StateBase.gd ├── Editor ├── GraphEditorEntryNode.gd ├── Views │ ├── GraphEditorNodeSlotView.gd │ ├── ScrollContainerView.gd │ ├── GraphEditorView.gd │ ├── EditorView.gd │ └── GraphEditorNodeView.gd ├── GraphEditor_ScrollContainer.gd ├── GraphEditorSelectionBox.gd ├── GraphEditorNodeSlot.gd ├── GraphEditorStateNode.gd ├── GraphEditorRerouter.gd ├── GraphEditorConnection.gd ├── GraphEditor_OverlayLayer.gd ├── EditorTheme.gd ├── GraphEditorConnectionBase.gd ├── GraphEditor_ConnectionsLayer.gd ├── Editor.gd ├── GraphEditorNode.gd ├── GraphEditor_NodesLayer.gd └── GraphEditor.gd ├── plugin.gd ├── LICENSE ├── Resources ├── SuperState.gd ├── Transition.gd ├── State.gd └── StateMachineGraph.gd └── StateMachine.gd /plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="StateMachine" 4 | description="A custom node and editor which allows to create state machine graphs." 5 | author="Ugis Brekis" 6 | version="0.1.4" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /Scripts/StateBase.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | class_name StateBase 3 | 4 | # Signals 5 | signal transition_requested(p_output, p_args) 6 | 7 | func on_start(p_args = []): 8 | pass 9 | 10 | func on_stop(): 11 | pass 12 | 13 | func invoke_transition(p_output, p_args = []): 14 | emit_signal("transition_requested", p_output, p_args) 15 | -------------------------------------------------------------------------------- /Editor/GraphEditorEntryNode.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "GraphEditorNode.gd" 3 | 4 | const StateMachineGraph = preload("../Resources/StateMachineGraph.gd") 5 | 6 | var graph : StateMachineGraph = null 7 | 8 | func _init(p_theme : Theme): 9 | theme = p_theme 10 | initialize_view() 11 | 12 | func initialize(p_graph : StateMachineGraph): 13 | graph = p_graph 14 | 15 | # Apply properties 16 | display_scale = theme.get_constant("scale", "Editor") 17 | self.offset = graph.entry_node_offset 18 | self.title = "Entry" 19 | 20 | warning_button.hide() 21 | 22 | # Create output slot 23 | add_output_slot(0, Color.yellowgreen, "OnStart") 24 | 25 | # Connect signals 26 | connect("offset_changed", self, "on_offset_changed") 27 | 28 | func on_offset_changed(): 29 | graph.entry_node_offset = offset 30 | 31 | graph.property_list_changed_notify() -------------------------------------------------------------------------------- /plugin.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends EditorPlugin 3 | 4 | const Editor = preload("Editor/Editor.gd") 5 | 6 | var editor : Editor = null 7 | 8 | func _enter_tree(): 9 | editor = Editor.new(get_editor_interface(), get_undo_redo()) 10 | 11 | add_control_to_bottom_panel(editor, "State Machine") 12 | 13 | editor.connect("attention_request", self, "on_editor_attention_request") 14 | 15 | func _exit_tree(): 16 | editor.disconnect("attention_request", self, "on_editor_attention_request") 17 | 18 | remove_control_from_bottom_panel(editor) 19 | 20 | editor.free() 21 | 22 | func save_external_data(): 23 | if editor == null: 24 | return 25 | 26 | if editor.is_queued_for_deletion(): 27 | return 28 | 29 | editor.apply_changes() 30 | 31 | func on_editor_attention_request(): 32 | if editor == null: 33 | return 34 | 35 | if editor.is_queued_for_deletion(): 36 | return 37 | 38 | make_bottom_panel_item_visible(editor) -------------------------------------------------------------------------------- /Editor/Views/GraphEditorNodeSlotView.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends PanelContainer 3 | 4 | var container : HBoxContainer = null 5 | var socket : TextureRect = null 6 | var label : Label = null 7 | 8 | func initialize_view(): 9 | container = HBoxContainer.new() 10 | container.mouse_filter = Control.MOUSE_FILTER_IGNORE 11 | 12 | # Socket 13 | socket = TextureRect.new() 14 | socket.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED 15 | socket.size_flags_vertical = Control.SIZE_EXPAND_FILL 16 | 17 | socket.texture = theme.get_icon("state_machine_editor_socket", "EditorIcons") 18 | 19 | container.add_child(socket) 20 | 21 | # Label 22 | label = Label.new() 23 | label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 24 | label.size_flags_vertical = Control.SIZE_SHRINK_CENTER 25 | label.mouse_filter = Control.MOUSE_FILTER_IGNORE 26 | 27 | container.add_child(label) 28 | 29 | add_child(container) 30 | 31 | mouse_filter = Control.MOUSE_FILTER_IGNORE 32 | add_stylebox_override("panel", theme.get_stylebox("state_node_slot", "Editor")) -------------------------------------------------------------------------------- /Editor/GraphEditor_ScrollContainer.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "Views/ScrollContainerView.gd" 3 | 4 | signal left_click_down 5 | signal right_click_down 6 | signal state_scripts_dropped(p_state_scripts) 7 | 8 | func _init(p_theme : Theme): 9 | initialize_view(p_theme) 10 | 11 | func can_drop_data(position, data): 12 | if typeof(data) == TYPE_DICTIONARY && data.has("files"): 13 | return true 14 | 15 | return false 16 | 17 | func drop_data(position, data): 18 | var state_scripts = [] 19 | 20 | for path in data.files: 21 | var file = load(path) 22 | 23 | if !(file is GDScript): 24 | continue 25 | 26 | if file.get_base_script() != StateBase: 27 | continue 28 | 29 | state_scripts.push_back(file) 30 | 31 | if state_scripts.size() == 0: 32 | return 33 | 34 | emit_signal("state_scripts_dropped", state_scripts) 35 | 36 | func _gui_input(event): 37 | if event is InputEventMouseButton: 38 | if event.pressed: 39 | match event.button_index: 40 | BUTTON_LEFT: 41 | emit_signal("left_click_down") 42 | 43 | BUTTON_RIGHT: 44 | emit_signal("right_click_down") 45 | 46 | 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | -------------------------------------------------------------------------------- /Editor/GraphEditorSelectionBox.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | var from_position : Vector2 = Vector2() setget set_from_position 5 | var to_position : Vector2 = Vector2() setget set_to_position 6 | 7 | var bounds : PoolVector2Array = PoolVector2Array() 8 | 9 | func _init(): 10 | mouse_filter = Control.MOUSE_FILTER_IGNORE 11 | 12 | hide() 13 | 14 | func set_from_position(p_position): 15 | from_position = p_position 16 | 17 | update_rect() 18 | 19 | func set_to_position(p_position): 20 | to_position = p_position 21 | 22 | update_rect() 23 | 24 | func update_rect(): 25 | rect_position = Vector2(min(from_position.x, to_position.x), min(from_position.y, to_position.y)) 26 | rect_size = Vector2(max(from_position.x, to_position.x), max(from_position.y, to_position.y)) - rect_position 27 | 28 | bounds.resize(0) 29 | 30 | bounds.push_back(Vector2()) 31 | bounds.push_back(Vector2(rect_size.x, 0)) 32 | bounds.push_back(rect_size) 33 | bounds.push_back(Vector2(0, rect_size.y)) 34 | 35 | func _draw(): 36 | draw_rect(Rect2(Vector2(), rect_size), Color(1, 1, 1, 0.1)) 37 | draw_rect(Rect2(Vector2(), rect_size), Color(1, 1, 1, 0.3), false) 38 | draw_rect(Rect2(Vector2(-1, -1), rect_size + Vector2(1, 1)), Color(0, 0, 0, 0.3), false) -------------------------------------------------------------------------------- /Editor/Views/ScrollContainerView.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends ScrollContainer 3 | 4 | var grid_cell_size = 40 5 | var grid_big_cell_color : Color = Color.black 6 | var grid_line_thickness = 1 7 | 8 | func initialize_view(p_theme : Theme): 9 | theme = p_theme 10 | 11 | grid_cell_size = theme.get_constant("graph_editor_grid_cell_size", "Editor") 12 | grid_big_cell_color = theme.get_color("dark_color_3", "Editor") 13 | grid_line_thickness = theme.get_constant("graph_editor_grid_line_thickness", "Editor") 14 | 15 | func _draw(): 16 | var h_lines : int = rect_size.y / grid_cell_size 17 | var v_lines : int = rect_size.x / grid_cell_size 18 | 19 | var offset : Vector2 = Vector2() 20 | 21 | offset.x = scroll_horizontal % grid_cell_size 22 | offset.y = scroll_vertical % grid_cell_size 23 | 24 | for i in (h_lines + 1): 25 | var line_offset = grid_cell_size * (i + 1) - offset.y 26 | 27 | var from = Vector2(0, line_offset) 28 | var to = Vector2(rect_size.x, line_offset) 29 | 30 | draw_line(from, to, grid_big_cell_color, grid_line_thickness) 31 | 32 | for i in (v_lines + 1): 33 | var line_offset = grid_cell_size * (i + 1) - offset.x 34 | 35 | var from = Vector2(line_offset, 0) 36 | var to = Vector2(line_offset, rect_size.y) 37 | 38 | draw_line(from, to, grid_big_cell_color, grid_line_thickness) 39 | 40 | 41 | -------------------------------------------------------------------------------- /Editor/GraphEditorNodeSlot.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "Views/GraphEditorNodeSlotView.gd" 3 | 4 | # Properties 5 | var is_input : bool = true setget set_is_input 6 | 7 | var socket_type : int = -1 8 | var socket_color : Color = Color.white setget set_socket_color 9 | 10 | var text : String = "" setget set_text 11 | 12 | # Signals 13 | signal socket_pressed(p_is_input, p_socket_id) 14 | 15 | func _init(p_theme : Theme): 16 | theme = p_theme 17 | initialize_view() 18 | 19 | func _enter_tree(): 20 | if !socket.is_connected("gui_input", self, "on_socket_gui_input"): 21 | socket.connect("gui_input", self, "on_socket_gui_input") 22 | 23 | func on_socket_gui_input(event : InputEvent): 24 | if event is InputEventMouseButton: 25 | if event.button_index != BUTTON_LEFT: 26 | return 27 | 28 | if event.pressed: 29 | emit_signal("socket_pressed", is_input, get_position_in_parent()) 30 | 31 | func set_is_input(p_is_input : bool): 32 | is_input = p_is_input 33 | 34 | if is_input: 35 | container.move_child(socket, 0) 36 | label.align = Label.ALIGN_LEFT 37 | 38 | else: 39 | container.move_child(socket, 1) 40 | label.align = Label.ALIGN_RIGHT 41 | 42 | func set_socket_color(p_color : Color): 43 | socket.modulate = p_color 44 | 45 | func set_text(p_text : String): 46 | text = p_text 47 | 48 | label.text = text 49 | 50 | func initialize(p_is_input : bool, p_type : int, p_color : Color, p_text : String): 51 | self.is_input = p_is_input 52 | self.socket_type = p_type 53 | self.socket_color = p_color 54 | self.text = p_text 55 | -------------------------------------------------------------------------------- /Resources/SuperState.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | 4 | export(String) var name = "New State" setget set_name 5 | export(GDScript) var state_script = null setget set_state_script 6 | export(PoolStringArray) var outputs = PoolStringArray() setget set_outputs 7 | 8 | var property_cache = [] 9 | 10 | var _graph_state_scripts : Array 11 | 12 | # Signals 13 | signal renamed 14 | signal state_script_changed 15 | signal outputs_changed 16 | signal property_cache_changed 17 | 18 | func set_name(p_name : String): 19 | name = p_name 20 | 21 | emit_signal("renamed") 22 | 23 | func set_state_script(p_script : GDScript): 24 | if p_script != null: 25 | for graph_state_script in _graph_state_scripts: 26 | if graph_state_script == p_script: 27 | return 28 | 29 | state_script = p_script 30 | 31 | update_property_cache() 32 | 33 | emit_signal("state_script_changed") 34 | 35 | func set_outputs(p_outputs : PoolStringArray): 36 | outputs = p_outputs 37 | 38 | emit_signal("outputs_changed") 39 | 40 | func set_graph_state_scripts_list(p_array : Array): 41 | _graph_state_scripts = p_array 42 | 43 | func update_property_cache(): 44 | property_cache.clear() 45 | 46 | if state_script == null: 47 | emit_signal("property_cache_changed") 48 | return 49 | 50 | var instance = state_script.new() 51 | 52 | for property in instance.get_property_list(): 53 | if property.usage & PROPERTY_USAGE_DEFAULT && property.usage & PROPERTY_USAGE_SCRIPT_VARIABLE: 54 | property_cache.push_back({ 55 | "name" : property.name, 56 | "type" : property.type, 57 | "hint" : property.hint, 58 | "hint_string" : property.hint_string, 59 | "default_value" : instance.get(property.name) 60 | }) 61 | 62 | # Clean up 63 | instance.queue_free() 64 | 65 | emit_signal("property_cache_changed") 66 | -------------------------------------------------------------------------------- /Editor/GraphEditorStateNode.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "GraphEditorNode.gd" 3 | 4 | const State = preload("../Resources/State.gd") 5 | const StateBase = preload("../Scripts/StateBase.gd") 6 | 7 | var state : State = null 8 | 9 | # Signals 10 | signal outputs_updated 11 | 12 | func _init(p_theme : Theme): 13 | theme = p_theme 14 | initialize_view() 15 | 16 | func initialize(p_state : State): 17 | state = p_state 18 | 19 | # Apply properties 20 | display_scale = theme.get_constant("scale", "Editor") 21 | self.offset = state.offset 22 | self.title = state.superstate.name 23 | 24 | if state.superstate.state_script == null: 25 | warning_button.show() 26 | else: 27 | warning_button.hide() 28 | 29 | # Create input slot 30 | add_input_slot(1, Color.white, "Start") 31 | 32 | # Add output slots 33 | update_outputs() 34 | 35 | # Connect signals 36 | connect("offset_changed", self, "on_offset_changed") 37 | 38 | state.superstate.connect("renamed", self, "on_state_renamed") 39 | state.superstate.connect("state_script_changed", self, "on_state_script_changed") 40 | state.superstate.connect("outputs_changed", self, "on_state_outputs_changed") 41 | 42 | func dispose(): 43 | print("Remove this graph editor node") 44 | 45 | func on_offset_changed(): 46 | if state == null: 47 | return 48 | 49 | state.offset = offset 50 | 51 | func on_state_renamed(): 52 | if state == null: 53 | return 54 | 55 | self.title = state.superstate.name 56 | 57 | func on_state_script_changed(): 58 | if state == null: 59 | return 60 | 61 | if state.superstate.state_script == null: 62 | warning_button.show() 63 | else: 64 | warning_button.hide() 65 | 66 | func on_state_outputs_changed(): 67 | if state == null: 68 | return 69 | 70 | update_outputs() 71 | 72 | func update_outputs(): 73 | remove_all_output_slots() 74 | 75 | for output in state.superstate.outputs: 76 | add_output_slot(2, Color.coral, output) 77 | 78 | emit_signal("outputs_updated") 79 | -------------------------------------------------------------------------------- /Editor/Views/GraphEditorView.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Panel 3 | 4 | const GRAPH_SHEET_SIZE = 8192 5 | 6 | const GraphEditorScrollContainer = preload("../GraphEditor_ScrollContainer.gd") 7 | 8 | const ConnectionsLayer = preload("../GraphEditor_ConnectionsLayer.gd") 9 | const NodesLayer = preload("../GraphEditor_NodesLayer.gd") 10 | const OverlayLayer = preload("../GraphEditor_OverlayLayer.gd") 11 | 12 | var scroll_container : GraphEditorScrollContainer = null 13 | 14 | var layers_container : Control = null 15 | 16 | var connections_layer : ConnectionsLayer = null 17 | var nodes_layer : NodesLayer = null 18 | var overlay_layer : OverlayLayer = null 19 | 20 | var popup_menu : PopupMenu = null 21 | 22 | func initialize_view(): 23 | # Scroll container 24 | scroll_container = GraphEditorScrollContainer.new(theme) 25 | 26 | # Layers container 27 | layers_container = Control.new() 28 | 29 | # Layers 30 | connections_layer = ConnectionsLayer.new(theme) 31 | nodes_layer = NodesLayer.new(theme) 32 | overlay_layer = OverlayLayer.new(theme) 33 | 34 | connections_layer.mouse_filter = Control.MOUSE_FILTER_PASS 35 | nodes_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE 36 | overlay_layer.mouse_filter = Control.MOUSE_FILTER_IGNORE 37 | 38 | connections_layer.set_anchors_preset(Control.PRESET_WIDE) 39 | nodes_layer.set_anchors_preset(Control.PRESET_WIDE) 40 | overlay_layer.set_anchors_preset(Control.PRESET_WIDE) 41 | 42 | layers_container.add_child(connections_layer) 43 | layers_container.add_child(nodes_layer) 44 | layers_container.add_child(overlay_layer) 45 | 46 | layers_container.rect_min_size = Vector2(GRAPH_SHEET_SIZE, GRAPH_SHEET_SIZE) 47 | layers_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 48 | 49 | scroll_container.add_child(layers_container) 50 | 51 | add_child(scroll_container) 52 | 53 | scroll_container.set_anchors_preset(Control.PRESET_WIDE) 54 | 55 | # Popup menu 56 | popup_menu = PopupMenu.new() 57 | 58 | add_child(popup_menu) 59 | -------------------------------------------------------------------------------- /Resources/Transition.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | 4 | const State = preload("State.gd") 5 | 6 | var from_state : State = null 7 | var to_state : State = null 8 | 9 | var from_slot_index : int = -1 10 | var to_slot_index : int = -1 11 | 12 | var reroute_points : PoolVector2Array = PoolVector2Array() 13 | 14 | func _get(property): 15 | if property == "from_state": 16 | return from_state 17 | 18 | elif property == "to_state": 19 | return to_state 20 | 21 | elif property == "from_slot_index": 22 | return from_slot_index 23 | 24 | elif property == "to_slot_index": 25 | return to_slot_index 26 | 27 | elif property == "reroute_points": 28 | return reroute_points 29 | 30 | func _set(property, value): 31 | if property == "from_state": 32 | from_state = value 33 | return true 34 | 35 | elif property == "to_state": 36 | to_state = value 37 | return true 38 | 39 | elif property == "from_slot_index": 40 | from_slot_index = value 41 | return true 42 | 43 | elif property == "to_slot_index": 44 | to_slot_index = value 45 | return true 46 | 47 | elif property == "reroute_points": 48 | reroute_points = value 49 | return true 50 | 51 | return false 52 | 53 | func _get_property_list(): 54 | var property_list = [] 55 | 56 | property_list.push_back({ 57 | "name" : "from_state", 58 | "type" : TYPE_OBJECT, 59 | "usage" : PROPERTY_USAGE_STORAGE 60 | }) 61 | 62 | property_list.push_back({ 63 | "name" : "from_slot_index", 64 | "type" : TYPE_INT, 65 | "usage" : PROPERTY_USAGE_STORAGE 66 | }) 67 | 68 | property_list.push_back({ 69 | "name" : "to_state", 70 | "type" : TYPE_OBJECT, 71 | "usage" : PROPERTY_USAGE_STORAGE 72 | }) 73 | 74 | property_list.push_back({ 75 | "name" : "to_slot_index", 76 | "type" : TYPE_INT, 77 | "usage" : PROPERTY_USAGE_STORAGE 78 | }) 79 | 80 | property_list.push_back({ 81 | "name" : "reroute_points", 82 | "type" : TYPE_VECTOR2_ARRAY, 83 | "usage" : PROPERTY_USAGE_STORAGE 84 | }) 85 | 86 | return property_list -------------------------------------------------------------------------------- /Editor/GraphEditorRerouter.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends TextureRect 3 | 4 | var default_texture : Texture 5 | var highlight_texture : Texture 6 | 7 | var offset : Vector2 = Vector2() setget set_offset 8 | var display_scale : float = 1.0 9 | var snap_distance : int = -1 setget set_snap_distance 10 | 11 | var pressed : bool = false 12 | 13 | signal offset_changed(p_id, p_offset) 14 | signal remove_requested(p_id) 15 | 16 | func _init(p_width : float, p_default_texture : Texture, p_highlight_texture : Texture, p_scale : float, p_offset : Vector2): 17 | mouse_filter = Control.MOUSE_FILTER_STOP 18 | stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED 19 | expand = true 20 | 21 | default_texture = p_default_texture 22 | highlight_texture = p_highlight_texture 23 | 24 | rect_size = Vector2(4, 4) * p_width 25 | texture = default_texture 26 | display_scale = p_scale 27 | 28 | self.offset = p_offset 29 | 30 | func set_offset(p_offset : Vector2): 31 | offset = p_offset 32 | 33 | var target_position : Vector2 = offset * display_scale 34 | 35 | if snap_distance > -1: 36 | target_position = target_position.snapped(Vector2(snap_distance, snap_distance)) 37 | 38 | rect_position = target_position - rect_size/2 39 | 40 | emit_signal("offset_changed", get_position_in_parent(), offset) 41 | 42 | func set_snap_distance(p_snap_distance : int): 43 | snap_distance = p_snap_distance 44 | 45 | self.offset = offset.snapped(Vector2(snap_distance, snap_distance) / display_scale) 46 | 47 | func _gui_input(event): 48 | if event is InputEventMouseButton: 49 | if event.button_index == BUTTON_LEFT && event.pressed: 50 | pressed = true 51 | 52 | if Input.is_key_pressed(KEY_ALT): 53 | emit_signal("remove_requested", get_position_in_parent()) 54 | 55 | elif event is InputEventMouseMotion: 56 | if texture != highlight_texture: 57 | texture = highlight_texture 58 | 59 | func _input(event): 60 | if event is InputEventMouseButton: 61 | if event.pressed: 62 | pass 63 | 64 | else: 65 | match event.button_index: 66 | BUTTON_LEFT: 67 | if pressed: 68 | pressed = false 69 | 70 | if event is InputEventMouseMotion: 71 | if pressed: 72 | self.offset += event.relative / display_scale 73 | 74 | if !get_rect().has_point(get_local_mouse_position()): 75 | texture = default_texture -------------------------------------------------------------------------------- /Resources/State.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | 4 | const SuperState = preload("SuperState.gd") 5 | 6 | var superstate : SuperState setget set_superstate, get_superstate 7 | var offset : Vector2 setget set_offset, get_offset 8 | var properties : Dictionary = {} setget set_properties, get_properties 9 | 10 | func set_superstate(p_superstate : SuperState): 11 | superstate = p_superstate 12 | 13 | if superstate == null: 14 | return 15 | 16 | superstate.connect("property_cache_changed", self, "on_property_cache_changed") 17 | 18 | func get_superstate() -> SuperState: 19 | return superstate 20 | 21 | func set_offset(p_offset : Vector2): 22 | offset = p_offset 23 | 24 | func get_offset() -> Vector2: 25 | return offset 26 | 27 | func set_properties(p_properties : Dictionary): 28 | properties = p_properties 29 | 30 | func get_properties() -> Dictionary: 31 | return properties 32 | 33 | func on_property_cache_changed(): 34 | _update_properties_from_cache(superstate.property_cache) 35 | 36 | func _update_properties_from_cache(p_property_cache : Array): 37 | var redundant_keys : PoolStringArray = PoolStringArray() 38 | 39 | # Collect redundant keys and erase them 40 | for key in properties.keys(): 41 | var is_redundant : bool = true 42 | 43 | for cached_item in p_property_cache: 44 | if key == cached_item.name: 45 | is_redundant = false 46 | 47 | break 48 | 49 | if is_redundant: 50 | redundant_keys.push_back(key) 51 | 52 | for key in redundant_keys: 53 | properties.erase(key) 54 | 55 | # Apply default values 56 | for cached_item in p_property_cache: 57 | if !(properties.has(cached_item.name)): 58 | properties[cached_item.name] = cached_item.default_value 59 | 60 | else: 61 | if typeof(properties[cached_item.name]) != cached_item.type: 62 | properties[cached_item.name] = cached_item.default_value 63 | 64 | func _get_property_list(): 65 | var property_list = [] 66 | 67 | property_list.push_back({ 68 | "name" : "superstate", 69 | "type" : TYPE_OBJECT, 70 | "usage" : PROPERTY_USAGE_STORAGE 71 | }) 72 | 73 | property_list.push_back({ 74 | "name" : "offset", 75 | "type" : TYPE_VECTOR2, 76 | "usage" : PROPERTY_USAGE_STORAGE 77 | }) 78 | 79 | property_list.push_back({ 80 | "name" : "properties", 81 | "type" : TYPE_DICTIONARY, 82 | "usage" : PROPERTY_USAGE_STORAGE 83 | }) 84 | 85 | return property_list 86 | -------------------------------------------------------------------------------- /Editor/GraphEditorConnection.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "GraphEditorConnectionBase.gd" 3 | 4 | const GraphEditorNode = preload("GraphEditorNode.gd") 5 | 6 | var from_node : GraphEditorNode = null 7 | var to_node : GraphEditorNode = null 8 | 9 | var from_slot_index : int = -1 10 | var to_slot_index : int = -1 11 | 12 | var from_slot_text : String 13 | var to_slot_text : String 14 | 15 | func initialize(p_width : float, p_display_scale : float, p_curvature : float, p_from : GraphEditorNode, p_from_slot : int, p_to : GraphEditorNode, p_to_slot : int, p_reroute_points : PoolVector2Array): 16 | create_texture() 17 | width = p_width 18 | display_scale = p_display_scale 19 | curvature = p_curvature 20 | 21 | from_node = p_from 22 | to_node = p_to 23 | 24 | from_slot_index = p_from_slot 25 | to_slot_index = p_to_slot 26 | 27 | from_slot_text = from_node.get_output_slot(from_slot_index).text 28 | to_slot_text = to_node.get_input_slot(to_slot_index).text 29 | 30 | reroute_points = p_reroute_points 31 | 32 | for i in reroute_points.size(): 33 | var rerouter = Rerouter.new(p_width, reroute_default_texture, reroute_highlight_texture, p_display_scale, reroute_points[i]) 34 | add_child(rerouter) 35 | 36 | rerouter.connect("offset_changed", self, "on_rerouter_offset_changed") 37 | rerouter.connect("remove_requested", self, "on_rerouter_remove_requested") 38 | 39 | rerouter.snap_distance = snap_distance 40 | 41 | call_deferred("update_positions") 42 | 43 | from_node.connect("offset_changed", self, "on_from_node_offset_changed") 44 | from_node.connect("resized", self, "on_from_node_resized") 45 | 46 | to_node.connect("offset_changed", self, "on_to_node_offset_changed") 47 | to_node.connect("resized", self, "on_to_node_resized") 48 | 49 | func on_from_node_resized(): 50 | call_deferred("update_from_position") 51 | 52 | func on_to_node_resized(): 53 | call_deferred("update_to_position") 54 | 55 | func on_from_node_offset_changed(): 56 | update_from_position() 57 | 58 | func on_to_node_offset_changed(): 59 | update_to_position() 60 | 61 | func update_positions(): 62 | update_from_position() 63 | update_to_position() 64 | 65 | func update_from_position(): 66 | if from_node == null || from_slot_index == -1: 67 | queue_free() 68 | return 69 | 70 | self.from_position = from_node.get_output_slot_socket_position(from_slot_index) 71 | 72 | func update_to_position(): 73 | if to_node == null || to_slot_index == -1: 74 | queue_free() 75 | return 76 | 77 | self.to_position = to_node.get_input_slot_socket_position(to_slot_index) 78 | 79 | -------------------------------------------------------------------------------- /Editor/Views/EditorView.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | const GraphEditor = preload("../GraphEditor.gd") 5 | 6 | enum PopupMenuItems { 7 | CREATE_NEW, 8 | OPEN, 9 | SAVE_AS 10 | } 11 | 12 | # Nodes 13 | var header_button_graph : MenuButton = null 14 | var snap_toggle : Button = null 15 | var graph_editor : GraphEditor = null 16 | 17 | var file_dialog : FileDialog = null 18 | 19 | # Properties 20 | var disabled : bool = true setget set_disabled 21 | 22 | func initialize_view(): 23 | rect_min_size = Vector2(256, 256) 24 | 25 | # Main VBox container 26 | var vbox_container = VBoxContainer.new() 27 | 28 | # Header 29 | var hbox_container = HBoxContainer.new() 30 | 31 | # Create State machine graph menu button 32 | header_button_graph = MenuButton.new() 33 | header_button_graph.text = "Graph" 34 | header_button_graph.disabled = disabled 35 | 36 | var popup = header_button_graph.get_popup() 37 | popup.add_icon_item(theme.get_icon("New", "EditorIcons"), "Create new", PopupMenuItems.CREATE_NEW) 38 | popup.add_separator() 39 | popup.add_icon_item(theme.get_icon("Load", "EditorIcons"), "Open", PopupMenuItems.OPEN) 40 | popup.add_icon_item(theme.get_icon("Save", "EditorIcons"), "Save As", PopupMenuItems.SAVE_AS) 41 | 42 | popup.connect("id_pressed", self, "on_header_button_graph_id_pressed") 43 | 44 | hbox_container.add_child(header_button_graph) 45 | 46 | # Spacer 47 | hbox_container.add_spacer(false) 48 | 49 | # Snapping controls 50 | snap_toggle = Button.new() 51 | snap_toggle.flat = true 52 | snap_toggle.add_stylebox_override("focus", StyleBoxEmpty.new()) 53 | snap_toggle.icon = theme.get_icon("SnapGrid", "EditorIcons") 54 | snap_toggle.toggle_mode = true 55 | snap_toggle.disabled = disabled 56 | 57 | snap_toggle.connect("toggled", self, "on_snaping_toggled") 58 | 59 | hbox_container.add_child(snap_toggle) 60 | 61 | # End of header 62 | vbox_container.add_child(hbox_container) 63 | 64 | # Graph Editor 65 | graph_editor = GraphEditor.new(theme) 66 | 67 | graph_editor.size_flags_vertical = Control.SIZE_EXPAND_FILL 68 | 69 | # End of graph editor 70 | vbox_container.add_child(graph_editor) 71 | 72 | add_child(vbox_container) 73 | 74 | vbox_container.set_anchors_preset(Control.PRESET_WIDE) 75 | 76 | # File dialog 77 | file_dialog = FileDialog.new() 78 | 79 | file_dialog.connect("file_selected", self, "on_file_dialog_file_selected") 80 | 81 | add_child(file_dialog) 82 | 83 | func set_disabled(p_disabled : bool): 84 | if p_disabled == disabled: 85 | return 86 | 87 | disabled = p_disabled 88 | 89 | header_button_graph.disabled = disabled 90 | snap_toggle.disabled = disabled 91 | graph_editor.disabled = disabled 92 | 93 | func on_header_button_graph_id_pressed(p_id : int): 94 | pass 95 | 96 | func on_file_dialog_file_selected(p_path): 97 | pass -------------------------------------------------------------------------------- /Editor/Views/GraphEditorNodeView.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends PanelContainer 3 | 4 | var content_container : VBoxContainer = null 5 | 6 | var header_panel : PanelContainer = null 7 | var title_label : Label = null 8 | var warning_button : Button = null 9 | 10 | var inputs_container : VBoxContainer = null 11 | var outputs_container : VBoxContainer = null 12 | 13 | var focus_panel : Panel = null 14 | 15 | func initialize_view(): 16 | add_stylebox_override("panel", theme.get_stylebox("state_node", "Editor")) 17 | 18 | content_container = VBoxContainer.new() 19 | content_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 20 | 21 | # Header panel 22 | header_panel = PanelContainer.new() 23 | header_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE 24 | 25 | header_panel.add_stylebox_override("panel", theme.get_stylebox("state_node_header", "Editor")) 26 | 27 | var header_hbox = HBoxContainer.new() 28 | 29 | # Title label 30 | title_label = Label.new() 31 | title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL 32 | title_label.mouse_filter = Control.MOUSE_FILTER_IGNORE 33 | 34 | # Warning icon 35 | warning_button = Button.new() 36 | warning_button.flat = true 37 | warning_button.add_stylebox_override("focus", StyleBoxEmpty.new()) 38 | warning_button.icon = theme.get_icon("NodeWarning", "EditorIcons") 39 | 40 | header_hbox.add_child(title_label) 41 | header_hbox.add_child(warning_button) 42 | 43 | header_panel.add_child(header_hbox) 44 | 45 | content_container.add_child(header_panel) 46 | 47 | # Body 48 | var margin_container = MarginContainer.new() 49 | margin_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 50 | 51 | margin_container.add_constant_override("margin_left", theme.get_constant("state_node_content_margin_left", "Editor")) 52 | margin_container.add_constant_override("margin_top", theme.get_constant("state_node_content_margin_top", "Editor")) 53 | margin_container.add_constant_override("margin_right", theme.get_constant("state_node_content_margin_right", "Editor")) 54 | margin_container.add_constant_override("margin_bottom", theme.get_constant("state_node_content_margin_bottom", "Editor")) 55 | 56 | var slots_container = HBoxContainer.new() 57 | slots_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 58 | 59 | slots_container.add_constant_override("separation", theme.get_constant("state_node_slot_separation", "Editor")) 60 | 61 | # Inputs 62 | inputs_container = VBoxContainer.new() 63 | inputs_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 64 | inputs_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL 65 | 66 | slots_container.add_child(inputs_container) 67 | 68 | # Ouputs 69 | outputs_container = VBoxContainer.new() 70 | outputs_container.mouse_filter = Control.MOUSE_FILTER_IGNORE 71 | outputs_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL 72 | 73 | slots_container.add_child(outputs_container) 74 | 75 | margin_container.add_child(slots_container) 76 | 77 | content_container.add_child(margin_container) 78 | 79 | add_child(content_container) 80 | 81 | # Focus panel 82 | focus_panel = Panel.new() 83 | focus_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE 84 | focus_panel.hide() 85 | 86 | focus_panel.add_stylebox_override("panel", theme.get_stylebox("state_node_focus", "Editor")) 87 | 88 | add_child(focus_panel) 89 | -------------------------------------------------------------------------------- /Editor/GraphEditor_OverlayLayer.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | const Connection = preload("GraphEditorConnectionBase.gd") 5 | const SelectionBox = preload("GraphEditorSelectionBox.gd") 6 | 7 | var connection : Connection = null 8 | var selection_box : SelectionBox = null 9 | 10 | # Properties 11 | var snap_distance = 20 12 | 13 | var snap_positions = [] 14 | 15 | # Flags 16 | var is_input = true 17 | var is_dragging = false 18 | var is_empty_space = true 19 | 20 | # Signals 21 | signal connection_drag_completed(p_from, p_to, p_empty_space) 22 | signal selection_box_drag_completed(p_rect) 23 | 24 | func _init(p_theme : Theme): 25 | theme = p_theme 26 | 27 | func _enter_tree(): 28 | connection = Connection.new() 29 | selection_box = SelectionBox.new() 30 | 31 | connection.width = theme.get_constant("graph_editor_connection_width", "Editor") 32 | connection.curvature = theme.get_constant("graph_editor_connection_curvature", "Editor") 33 | snap_distance = theme.get_constant("graph_editor_socket_snap_distance", "Editor") 34 | 35 | add_child(connection) 36 | add_child(selection_box) 37 | 38 | connection.hide() 39 | 40 | func begin_connection_drag(p_is_input : bool, p_position : Vector2, p_snap_positions : PoolVector2Array): 41 | is_input = p_is_input 42 | is_dragging = true 43 | 44 | snap_positions = p_snap_positions 45 | 46 | connection.from_position = p_position 47 | connection.to_position = p_position 48 | 49 | connection.show() 50 | selection_box.hide() 51 | 52 | func begin_selection_box_drag(): 53 | is_dragging = true 54 | 55 | selection_box.from_position = get_local_mouse_position() 56 | selection_box.to_position = selection_box.from_position 57 | 58 | connection.hide() 59 | selection_box.show() 60 | 61 | func stop_drag(): 62 | if !is_dragging: 63 | return 64 | 65 | is_dragging = false 66 | 67 | var from_position : Vector2 68 | var to_position : Vector2 69 | 70 | if connection.visible: 71 | from_position = connection.from_position 72 | to_position = connection.to_position 73 | 74 | connection.hide() 75 | 76 | emit_signal("connection_drag_completed", from_position, to_position, is_empty_space) 77 | 78 | elif selection_box.visible: 79 | from_position = selection_box.from_position 80 | to_position = selection_box.to_position 81 | 82 | selection_box.hide() 83 | 84 | emit_signal("selection_box_drag_completed", selection_box.get_rect()) 85 | 86 | func _input(event): 87 | if event is InputEventMouseButton: 88 | if event.pressed: 89 | pass 90 | 91 | else: 92 | match event.button_index: 93 | BUTTON_LEFT: 94 | if is_dragging: 95 | stop_drag() 96 | 97 | elif event is InputEventMouseMotion: 98 | if !is_dragging: 99 | return 100 | 101 | var local_mouse_position = get_local_mouse_position() 102 | var target_position = local_mouse_position 103 | var distance = snap_distance 104 | 105 | # Connection is being dragged 106 | if connection.visible: 107 | # Check if mouse position is within snap distance 108 | for pos in snap_positions: 109 | var d = pos.distance_to(local_mouse_position) 110 | 111 | if d < distance: 112 | target_position = pos 113 | distance = d 114 | 115 | # Is the target position snapped? 116 | if target_position in snap_positions: 117 | is_empty_space = false 118 | 119 | else: 120 | is_empty_space = true 121 | 122 | if is_input: 123 | connection.from_position = target_position 124 | 125 | else: 126 | connection.to_position = target_position 127 | 128 | # Box selection is being dragged 129 | else: 130 | selection_box.to_position = target_position 131 | 132 | accept_event() 133 | -------------------------------------------------------------------------------- /Editor/EditorTheme.gd: -------------------------------------------------------------------------------- 1 | extends Reference 2 | 3 | static func create_editor_theme(p_theme : Theme): 4 | var scale = p_theme.get_constant("scale", "Editor") 5 | 6 | var dark_theme = p_theme.get_constant("dark_theme", "Editor") 7 | 8 | var accent_color = p_theme.get_color("accent_color", "Editor") 9 | var highlight_color = p_theme.get_color("highlight_color", "Editor") 10 | var base_color = p_theme.get_color("base_color", "Editor") 11 | var dark_color_1 = p_theme.get_color("dark_color_1", "Editor") 12 | var dark_color_2 = p_theme.get_color("dark_color_2", "Editor") 13 | var dark_color_3 = p_theme.get_color("dark_color_3", "Editor") 14 | var contrast_color_1 = p_theme.get_color("contrast_color_1", "Editor") 15 | var contrast_color_2 = p_theme.get_color("contrast_color_2", "Editor") 16 | 17 | # Graph editor 18 | # Grid constants 19 | p_theme.set_constant("graph_editor_grid_cell_size", "Editor", 20 * scale) 20 | p_theme.set_constant("graph_editor_grid_line_thickness", "Editor", 1 * scale) 21 | 22 | # Snap to node sockets within this distance 23 | p_theme.set_constant("graph_editor_socket_snap_distance", "Editor", 10 * scale) 24 | 25 | # Graph Editor Node 26 | # Body 27 | p_theme.set_constant("state_node_content_margin_left", "Editor", 4 * scale) 28 | p_theme.set_constant("state_node_content_margin_top", "Editor", 8 * scale) 29 | p_theme.set_constant("state_node_content_margin_bottom", "Editor", 16 * scale) 30 | p_theme.set_constant("state_node_content_margin_right", "Editor", 4 * scale) 31 | 32 | p_theme.set_constant("state_node_slot_separation", "Editor", 8 * scale) 33 | 34 | var sb = StyleBoxFlat.new() 35 | 36 | sb.bg_color = base_color 37 | 38 | sb.border_color = dark_color_3 39 | 40 | sb.border_width_left = 1 * scale 41 | sb.border_width_top = 1 * scale 42 | sb.border_width_right = 1 * scale 43 | sb.border_width_bottom = 1 * scale 44 | 45 | p_theme.set_stylebox("state_node", "Editor", sb) 46 | 47 | # Header 48 | sb = StyleBoxFlat.new() 49 | sb.bg_color = dark_color_3 50 | 51 | sb.content_margin_bottom = 2 * scale 52 | sb.content_margin_left = 4 * scale 53 | sb.content_margin_right = 4 * scale 54 | sb.content_margin_top = 2 * scale 55 | 56 | p_theme.set_stylebox("state_node_header", "Editor", sb) 57 | 58 | # Slot 59 | sb = StyleBoxEmpty.new() 60 | 61 | sb.content_margin_bottom = 2 * scale 62 | sb.content_margin_top = 2 * scale 63 | 64 | p_theme.set_stylebox("state_node_slot", "Editor", sb) 65 | 66 | # Slot socket 67 | p_theme.set_icon("state_machine_editor_socket", "EditorIcons", create_socket_texture(p_theme)) 68 | 69 | # Focus 70 | sb = StyleBoxFlat.new() 71 | sb.draw_center = false 72 | 73 | sb.border_color = accent_color 74 | 75 | sb.border_width_left = 1 * scale 76 | sb.border_width_top = 1 * scale 77 | sb.border_width_right = 1 * scale 78 | sb.border_width_bottom = 1 * scale 79 | 80 | sb.expand_margin_bottom = 2 * scale 81 | sb.expand_margin_left = 2 * scale 82 | sb.expand_margin_right = 2 * scale 83 | sb.expand_margin_top = 2 * scale 84 | 85 | p_theme.set_stylebox("state_node_focus", "Editor", sb) 86 | 87 | # Connection 88 | var connection_width = 4.0 * scale 89 | p_theme.set_constant("graph_editor_connection_width", "Editor", 4 * scale) 90 | p_theme.set_constant("graph_editor_connection_curvature", "Editor", 100 * scale) 91 | 92 | static func create_socket_texture(p_theme : Theme) -> ImageTexture: 93 | var image : Image = p_theme.get_icon("VisualShaderPort", "EditorIcons").get_data() 94 | var size : Vector2 = image.get_size() 95 | 96 | image.lock() 97 | 98 | for x in size.x: 99 | for y in size.y: 100 | var alpha = image.get_pixel(x, y).a 101 | 102 | image.set_pixel(x, y, Color(1, 1, 1, alpha)) 103 | 104 | image.unlock() 105 | 106 | var socket_texture = ImageTexture.new() 107 | 108 | socket_texture.create_from_image(image) 109 | 110 | return socket_texture 111 | -------------------------------------------------------------------------------- /StateMachine.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Node 3 | class_name StateMachine 4 | 5 | const Graph = preload("Resources/StateMachineGraph.gd") 6 | 7 | export(bool) var autostart = true 8 | 9 | var graph : Graph = null 10 | 11 | var active_state : Graph.State = null 12 | var active_state_instance : StateBase = null 13 | 14 | # Signals 15 | signal started 16 | signal stopped 17 | signal state_changed 18 | 19 | func _set(property, value): 20 | if property == "graph": 21 | graph = value 22 | return true 23 | 24 | if Engine.editor_hint: 25 | if active_state != null: 26 | for cached_item in active_state.superstate.property_cache: 27 | if property == "Selected state/%s" % [cached_item.name]: 28 | active_state.properties[cached_item.name] = value 29 | 30 | graph.property_list_changed_notify() 31 | return true 32 | 33 | func _get(property): 34 | if property == "graph": 35 | return graph 36 | 37 | if Engine.editor_hint: 38 | if active_state != null: 39 | for cached_item in active_state.superstate.property_cache: 40 | if property == "Selected state/%s" % [cached_item.name]: 41 | return active_state.properties[cached_item.name] 42 | 43 | func _get_property_list(): 44 | var property_list = [] 45 | 46 | if !Engine.editor_hint: 47 | return property_list 48 | 49 | if graph == null: 50 | return property_list 51 | 52 | property_list = [{ 53 | "name" : "graph", 54 | "type" : TYPE_OBJECT, 55 | "usage" : PROPERTY_USAGE_STORAGE 56 | }] 57 | 58 | if active_state == null: 59 | return property_list 60 | 61 | for cached_item in active_state.superstate.property_cache: 62 | var property = { 63 | "name" : "Selected state/%s" % [cached_item.name], 64 | "type" : cached_item.type, 65 | "hint" : cached_item.hint, 66 | "hint_string" : cached_item.hint_string, 67 | "usage" : PROPERTY_USAGE_EDITOR 68 | } 69 | 70 | property_list.push_back(property) 71 | 72 | return property_list 73 | 74 | func _ready(): 75 | if Engine.editor_hint: 76 | return 77 | 78 | if autostart: 79 | start() 80 | 81 | func start(p_args = []): 82 | if graph == null: 83 | return 84 | 85 | var default_state : Graph.State = graph.get_default_state() 86 | 87 | if default_state == null: 88 | return 89 | 90 | instantiate_next_state(default_state, p_args) 91 | 92 | emit_signal("started") 93 | 94 | func stop(): 95 | if active_state_instance != null: 96 | active_state_instance.disconnect("transition_requested", self, "on_transition_requested") 97 | active_state_instance.on_stop() 98 | active_state_instance.queue_free() 99 | 100 | active_state_instance = null 101 | 102 | active_state = null 103 | 104 | emit_signal("stopped") 105 | 106 | func on_transition_requested(p_output, p_args : Array = []): 107 | # Can only transition if there is active state 108 | if active_state == null: 109 | stop() 110 | return 111 | 112 | # Find output index 113 | var output_index = -1 114 | 115 | match typeof(p_output): 116 | TYPE_INT: 117 | if p_output in range(active_state.superstate.outputs.size()): 118 | output_index = p_output 119 | 120 | TYPE_STRING: 121 | for i in active_state.superstate.outputs.size(): 122 | if p_output == active_state.superstate.outputs[i]: 123 | output_index = i 124 | 125 | break 126 | 127 | if output_index == -1: 128 | print("State machine :: Output[ %s ] does not exist" % [p_output]) 129 | stop() 130 | return 131 | 132 | # Stop current active state 133 | active_state_instance.disconnect("transition_requested", self, "on_transition_requested") 134 | active_state_instance.on_stop() 135 | active_state_instance.queue_free() 136 | 137 | # Find next state index 138 | for i in graph.transitions.size(): 139 | var transition = graph.transitions[i] as Graph.Transition 140 | 141 | if transition.from_state == active_state && transition.from_slot_index == output_index: 142 | # Transition found, try to instantiate next state 143 | instantiate_next_state(transition.to_state, p_args) 144 | return 145 | 146 | # Transition does not exist 147 | print("State machine couldn't perform a transition") 148 | stop() 149 | 150 | func instantiate_next_state(p_state : Graph.State, p_args : Array = []): 151 | # Check if the script is attached to the next state 152 | if p_state.superstate.state_script == null: 153 | stop() 154 | return 155 | 156 | active_state = p_state 157 | 158 | active_state_instance = active_state.superstate.state_script.new() 159 | 160 | add_child(active_state_instance) 161 | 162 | # Apply properties 163 | active_state_instance.owner = get_parent() 164 | 165 | for key in active_state.properties.keys(): 166 | var value = active_state.properties[key] 167 | 168 | # Resolve NodePaths 169 | if typeof(value) == TYPE_NODE_PATH: 170 | var path : String = str(value) 171 | 172 | path = path.insert(0, "../") 173 | 174 | value = NodePath(path) 175 | 176 | active_state_instance.set(key, value) 177 | 178 | active_state_instance.connect("transition_requested", self, "on_transition_requested") 179 | 180 | active_state_instance.on_start(p_args) 181 | 182 | emit_signal("state_changed") 183 | 184 | -------------------------------------------------------------------------------- /Editor/GraphEditorConnectionBase.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Line2D 3 | 4 | const Rerouter = preload("GraphEditorRerouter.gd") 5 | 6 | const LineTextureMode = Line2D.LINE_TEXTURE_TILE 7 | const LineColor = Color.whitesmoke 8 | 9 | var curve : Curve2D = null 10 | 11 | # Properties 12 | var reroute_default_texture : Texture = null 13 | var reroute_highlight_texture : Texture = null 14 | 15 | var curvature : float = 20 setget set_curvature 16 | var display_scale : float = 1.0 17 | var snap_distance : int = -1 setget set_snap_distance 18 | 19 | var from_position : Vector2 = Vector2() setget set_from_position 20 | var to_position : Vector2 = Vector2() setget set_to_position 21 | 22 | var reroute_points : PoolVector2Array = PoolVector2Array() 23 | 24 | signal reroute_points_changed(p_connection) 25 | 26 | func _init(): 27 | create_texture() 28 | texture_mode = LineTextureMode 29 | default_color = LineColor 30 | 31 | curve = Curve2D.new() 32 | curve.bake_interval = 8 33 | 34 | func create_texture(): 35 | var image = Image.new() 36 | image.create(6, 6, false, Image.FORMAT_LA8) 37 | 38 | image.lock() 39 | 40 | for x in 6: 41 | image.set_pixel(x, 1, Color.darkgray) 42 | image.set_pixel(x, 2, Color.white) 43 | image.set_pixel(x, 3, Color.white) 44 | image.set_pixel(x, 4, Color.darkgray) 45 | 46 | image.unlock() 47 | 48 | var image_texture = ImageTexture.new() 49 | image_texture.create_from_image(image) 50 | 51 | texture = image_texture 52 | 53 | func set_curvature(p_curvature : float): 54 | curvature = p_curvature 55 | 56 | update_shape() 57 | 58 | func set_snap_distance(p_snap_distance : int): 59 | snap_distance = p_snap_distance 60 | 61 | for rerouter in get_children(): 62 | rerouter.snap_distance = snap_distance 63 | 64 | func set_from_position(p_position : Vector2): 65 | from_position = p_position 66 | 67 | update_shape() 68 | 69 | func set_to_position(p_position : Vector2): 70 | to_position = p_position 71 | 72 | update_shape() 73 | 74 | func update_shape(): 75 | if curve == null: 76 | return 77 | 78 | curve.clear_points() 79 | 80 | #var handle_length = min(from_position.distance_to(to_position) * 0.5, curvature) 81 | 82 | curve.add_point(from_position) 83 | #curve.add_point(from_position + Vector2(20, 0), Vector2(), Vector2(handle_length, 0)) 84 | 85 | for point in reroute_points: 86 | curve.add_point(point * display_scale) 87 | 88 | #curve.add_point(to_position + Vector2(-20, 0), Vector2(-handle_length, 0)) 89 | curve.add_point(to_position) 90 | 91 | var first : Vector2 = curve.get_point_position(0) 92 | var last : Vector2 = curve.get_point_position(curve.get_point_count() - 1) 93 | var previous : Vector2 = curve.get_point_position(curve.get_point_count() - 2) 94 | var next : Vector2 = curve.get_point_position(1) 95 | 96 | var handle_out : float = min(first.distance_to(next) * 0.5, curvature) 97 | var handle_in : float = min(last.distance_to(previous) * 0.5, curvature) 98 | 99 | curve.set_point_out(0, Vector2(handle_out, 0)) 100 | curve.set_point_in(curve.get_point_count() - 1, Vector2(-handle_in, 0)) 101 | 102 | if curve.get_point_count() > 2: 103 | for i in range(1, curve.get_point_count() - 1): 104 | var current : Vector2 = curve.get_point_position(i) 105 | previous = curve.get_point_position(i - 1) 106 | next = curve.get_point_position(i + 1) 107 | 108 | var handle : Vector2 = (next - previous).normalized() 109 | handle.y = 0 110 | 111 | curve.set_point_in(i, -handle * min(current.distance_to(previous) * 0.5, curvature)) 112 | curve.set_point_out(i, handle * min(current.distance_to(next) * 0.5, curvature)) 113 | 114 | points = curve.get_baked_points() 115 | 116 | func add_reroute_point(p_position : Vector2): 117 | var index = 0 118 | 119 | for i in reroute_points.size(): 120 | if curve.get_closest_offset(p_position) < curve.get_closest_offset(reroute_points[i] * display_scale): 121 | break 122 | 123 | index = i + 1 124 | 125 | reroute_points.insert(index, p_position / display_scale) 126 | 127 | var rerouter = Rerouter.new(width, reroute_default_texture, reroute_highlight_texture, display_scale, reroute_points[index]) 128 | add_child(rerouter) 129 | move_child(rerouter, index) 130 | 131 | rerouter.connect("offset_changed", self, "on_rerouter_offset_changed") 132 | rerouter.connect("remove_requested", self, "on_rerouter_remove_requested") 133 | 134 | rerouter.snap_distance = snap_distance 135 | 136 | update_shape() 137 | 138 | func remove_reroute_point(p_index : int): 139 | var rerouter : Rerouter = get_child(p_index) 140 | rerouter.queue_free() 141 | 142 | reroute_points.remove(p_index) 143 | 144 | update_shape() 145 | 146 | emit_signal("reroute_points_changed", self) 147 | 148 | func on_rerouter_offset_changed(p_id : int, p_offset : Vector2): 149 | var rerouter = get_child(p_id) as Rerouter 150 | 151 | reroute_points[p_id] = (rerouter.rect_position + rerouter.rect_size / 2) / display_scale 152 | 153 | emit_signal("reroute_points_changed", self) 154 | 155 | update_shape() 156 | 157 | func on_rerouter_remove_requested(p_id): 158 | get_child(p_id).queue_free() 159 | 160 | reroute_points.remove(p_id) 161 | 162 | emit_signal("reroute_points_changed", self) 163 | 164 | update_shape() 165 | -------------------------------------------------------------------------------- /Editor/GraphEditor_ConnectionsLayer.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | const GraphEditorNode = preload("GraphEditorNode.gd") 5 | const Connection = preload("GraphEditorConnection.gd") 6 | 7 | var connection_width : float = 1 8 | var connection_curvature : float = 20 9 | var display_scale : float = 1.0 10 | 11 | var reroute = { 12 | "connection" : null, 13 | "position" : null 14 | } 15 | 16 | var snapping_enabled = false setget set_snapping_enabled 17 | var grid_cell_size = 10 18 | 19 | # Signals 20 | signal reroute_points_changed(p_connection) 21 | 22 | func _init(p_theme : Theme): 23 | theme = p_theme 24 | display_scale = theme.get_constant("scale", "Editor") 25 | connection_width = theme.get_constant("graph_editor_connection_width", "Editor") 26 | connection_curvature = theme.get_constant("graph_editor_connection_curvature", "Editor") 27 | grid_cell_size = theme.get_constant("graph_editor_grid_cell_size", "Editor") 28 | 29 | func _gui_input(event): 30 | if event is InputEventMouseButton: 31 | if event.button_index != BUTTON_LEFT: 32 | return 33 | 34 | if !event.pressed: 35 | return 36 | 37 | var key = KEY_CONTROL 38 | 39 | if OS.get_name() == "OSX": 40 | key = KEY_META 41 | 42 | if Input.is_key_pressed(key): 43 | reroute.connection = null 44 | reroute.position = null 45 | 46 | for child in get_children(): 47 | var connection = child as Connection 48 | 49 | if connection == null: 50 | continue 51 | 52 | var point = connection.curve.get_closest_point(get_local_mouse_position()) 53 | 54 | if point.distance_to(get_local_mouse_position()) > 40: 55 | continue 56 | 57 | if reroute.position == null: 58 | reroute.connection = connection 59 | reroute.position = point 60 | 61 | continue 62 | 63 | if point.distance_to(get_local_mouse_position()) < reroute.position.distance_to(get_local_mouse_position()): 64 | reroute.connection = connection 65 | reroute.position = point 66 | 67 | if reroute.connection != null: 68 | var connection = reroute.connection as Connection 69 | 70 | connection.add_reroute_point(reroute.position) 71 | 72 | on_reroute_points_changed(connection) 73 | 74 | func set_snapping_enabled(p_snapping_enabled : bool): 75 | snapping_enabled = p_snapping_enabled 76 | 77 | var snap_distance = -1 78 | 79 | if snapping_enabled: 80 | snap_distance = grid_cell_size 81 | 82 | for child in get_children(): 83 | var connection = child as Connection 84 | 85 | connection.snap_distance = snap_distance 86 | 87 | func on_reroute_points_changed(p_connection : Connection): 88 | emit_signal("reroute_points_changed", p_connection) 89 | 90 | func add_new_connection(p_from : GraphEditorNode, p_from_index: int, p_to : GraphEditorNode, p_to_index : int, p_reroute_points : PoolVector2Array): 91 | var connection : Connection = Connection.new() 92 | 93 | connection.reroute_default_texture = theme.get_icon("grabber", "HSlider") 94 | connection.reroute_highlight_texture = theme.get_icon("grabber_highlight", "HSlider") 95 | 96 | add_child(connection) 97 | 98 | connection.initialize(connection_width, display_scale, connection_curvature, p_from, p_from_index, p_to, p_to_index, p_reroute_points) 99 | 100 | var snap_distance = -1 101 | 102 | if snapping_enabled: 103 | snap_distance = grid_cell_size 104 | 105 | connection.snap_distance = snap_distance 106 | 107 | connection.connect("reroute_points_changed", self, "on_reroute_points_changed") 108 | 109 | return OK 110 | 111 | func remove_connection(p_from : GraphEditorNode, p_from_index: int, p_to : GraphEditorNode, p_to_index : int): 112 | var connection = get_connection(p_from, p_from_index, p_to, p_to_index) 113 | 114 | if connection == null: 115 | return ERR_DOES_NOT_EXIST 116 | 117 | connection.queue_free() 118 | 119 | return OK 120 | 121 | func clear(): 122 | for child in get_children(): 123 | child.free() 124 | 125 | func get_connection(p_from : GraphEditorNode, p_from_index: int, p_to : GraphEditorNode, p_to_index : int): 126 | for child in get_children(): 127 | var connection = child as Connection 128 | 129 | if connection == null: 130 | continue 131 | 132 | if connection.from_node == p_from && connection.to_node == p_to: 133 | if connection.from_slot_index == p_from_index && connection.to_slot_index == p_to_index: 134 | return connection 135 | 136 | return null 137 | 138 | func get_incomming_connections(p_node : GraphEditorNode, p_slot_index : int): 139 | var connections = [] 140 | 141 | for child in get_children(): 142 | var connection = child as Connection 143 | 144 | if connection == null: 145 | continue 146 | 147 | if p_node == connection.to_node && p_slot_index == connection.to_slot_index: 148 | connections.push_back(connection) 149 | 150 | return connections 151 | 152 | func get_outgoing_connections(p_node : GraphEditorNode, p_slot_index : int): 153 | var connections = [] 154 | 155 | for child in get_children(): 156 | var connection = child as Connection 157 | 158 | if connection == null: 159 | continue 160 | 161 | if p_node == connection.from_node && p_slot_index == connection.from_slot_index: 162 | connections.push_back(connection) 163 | 164 | return connections 165 | 166 | 167 | 168 | 169 | -------------------------------------------------------------------------------- /Editor/Editor.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "Views/EditorView.gd" 3 | 4 | const EditorTheme = preload("EditorTheme.gd") 5 | 6 | const GraphEditorNode = preload("GraphEditorNode.gd") 7 | const GraphEditorEntryNode = preload("GraphEditorEntryNode.gd") 8 | const GraphEditorStateNode = preload("GraphEditorStateNode.gd") 9 | 10 | # Currently selected state machine 11 | var active_state_machine : StateMachine = null setget set_active_state_machine 12 | 13 | # Dependencies 14 | var editor_interface : EditorInterface = null 15 | var editor_selection : EditorSelection = null 16 | var undo_redo : UndoRedo = null 17 | 18 | # Signals 19 | signal attention_request 20 | 21 | func _init(p_editor_interface : EditorInterface, p_undo_redo : UndoRedo): 22 | editor_interface = p_editor_interface as EditorInterface 23 | editor_selection = editor_interface.get_selection() as EditorSelection 24 | undo_redo = p_undo_redo as UndoRedo 25 | 26 | theme = editor_interface.get_base_control().theme 27 | EditorTheme.create_editor_theme(theme) 28 | 29 | initialize_view() 30 | 31 | func _enter_tree(): 32 | connect_signals() 33 | 34 | graph_editor.undo_redo = undo_redo 35 | 36 | func _exit_tree(): 37 | disconnect_signals() 38 | 39 | func connect_signals(): 40 | editor_selection.connect("selection_changed", self, "on_editor_interface_selection_changed") 41 | 42 | graph_editor.connect("selection_changed", self, "on_graph_editor_selection_changed") 43 | graph_editor.connect("inspect_state_request", self, "on_inspect_state_request") 44 | graph_editor.connect("graph_edited", self, "on_graph_edited") 45 | 46 | func disconnect_signals(): 47 | editor_selection.disconnect("selection_changed", self, "on_editor_interface_selection_changed") 48 | 49 | graph_editor.disconnect("selection_changed", self, "on_graph_editor_selection_changed") 50 | graph_editor.disconnect("inspect_state_request", self, "on_inspect_state_request") 51 | graph_editor.disconnect("graph_edited", self, "on_graph_edited") 52 | 53 | func on_editor_interface_selection_changed(): 54 | var selected_nodes = editor_selection.get_selected_nodes() 55 | 56 | if selected_nodes.size() != 1: 57 | self.active_state_machine = null 58 | return 59 | 60 | if !(selected_nodes[0] is StateMachine): 61 | self.active_state_machine = null 62 | return 63 | 64 | self.active_state_machine = selected_nodes[0] 65 | 66 | func apply_changes(): 67 | if active_state_machine == null: 68 | return 69 | 70 | if active_state_machine.graph == null: 71 | return 72 | 73 | for superstate in active_state_machine.graph.superstates: 74 | superstate.update_property_cache() 75 | 76 | func on_header_button_graph_id_pressed(p_id : int): 77 | match p_id: 78 | PopupMenuItems.CREATE_NEW: 79 | create_new_state_machine_graph() 80 | 81 | PopupMenuItems.OPEN: 82 | show_open_file_dialog() 83 | 84 | PopupMenuItems.SAVE_AS: 85 | show_save_file_dialog() 86 | 87 | func show_open_file_dialog(): 88 | file_dialog.mode = FileDialog.MODE_OPEN_FILE 89 | file_dialog.access = FileDialog.ACCESS_RESOURCES 90 | file_dialog.clear_filters() 91 | file_dialog.add_filter("*.tres") 92 | 93 | file_dialog.popup_centered_ratio() 94 | 95 | func show_save_file_dialog(): 96 | file_dialog.mode = FileDialog.MODE_SAVE_FILE 97 | file_dialog.access = FileDialog.ACCESS_RESOURCES 98 | file_dialog.clear_filters() 99 | file_dialog.add_filter("*.tres") 100 | 101 | file_dialog.popup_centered_ratio() 102 | 103 | func on_file_dialog_file_selected(p_path): 104 | match file_dialog.mode: 105 | FileDialog.MODE_OPEN_FILE: 106 | var resource = ResourceLoader.load(p_path) 107 | 108 | if !(resource is StateMachine.Graph): 109 | print("Selected could not be loaded") 110 | return 111 | 112 | active_state_machine.graph = resource 113 | active_state_machine.property_list_changed_notify() 114 | 115 | graph_editor.graph = active_state_machine.graph 116 | 117 | editor_interface.inspect_object(active_state_machine) 118 | 119 | FileDialog.MODE_SAVE_FILE: 120 | var err = ResourceSaver.save(p_path, active_state_machine.graph) 121 | 122 | if err != OK: 123 | print("Error saving state machine graph") 124 | return 125 | 126 | active_state_machine.graph.take_over_path(p_path) 127 | active_state_machine.property_list_changed_notify() 128 | 129 | graph_editor.graph = active_state_machine.graph 130 | 131 | editor_interface.inspect_object(active_state_machine) 132 | 133 | func on_snaping_toggled(p_toggled): 134 | graph_editor.snapping_enabled = p_toggled 135 | 136 | func on_graph_editor_selection_changed(p_state : StateMachine.Graph.State): 137 | if active_state_machine == null: 138 | return 139 | 140 | if active_state_machine.graph == null: 141 | return 142 | 143 | active_state_machine.active_state = p_state 144 | active_state_machine.property_list_changed_notify() 145 | 146 | editor_interface.inspect_object(active_state_machine) 147 | 148 | func on_inspect_state_request(p_state : StateMachine.Graph.State): 149 | if active_state_machine == null: 150 | return 151 | 152 | if p_state == null: 153 | active_state_machine.active_state = null 154 | active_state_machine.property_list_changed_notify() 155 | return 156 | 157 | # Show state script 158 | if p_state.superstate.state_script != null: 159 | editor_interface.inspect_object(p_state.superstate.state_script) 160 | 161 | # Show state properties in inspector 162 | editor_interface.inspect_object(p_state.superstate) 163 | 164 | func on_graph_edited(): 165 | if active_state_machine == null: 166 | return 167 | 168 | active_state_machine.property_list_changed_notify() 169 | 170 | func create_new_state_machine_graph(): 171 | if active_state_machine == null: 172 | return 173 | 174 | var graph_resource = StateMachine.Graph.new() 175 | active_state_machine.graph = graph_resource.duplicate() as StateMachine.Graph 176 | 177 | graph_editor.graph = active_state_machine.graph 178 | 179 | active_state_machine.property_list_changed_notify() 180 | 181 | editor_interface.inspect_object(active_state_machine) 182 | 183 | func set_active_state_machine(p_state_machine : StateMachine): 184 | active_state_machine = p_state_machine 185 | 186 | graph_editor.graph = null 187 | 188 | if active_state_machine == null: 189 | self.disabled = true 190 | return 191 | 192 | self.disabled = false 193 | emit_signal("attention_request") 194 | 195 | if active_state_machine.graph == null: 196 | return 197 | 198 | graph_editor.graph = active_state_machine.graph 199 | 200 | graph_editor.snapping_enabled = snap_toggle.pressed 201 | 202 | 203 | -------------------------------------------------------------------------------- /Resources/StateMachineGraph.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Resource 3 | 4 | const Superstate = preload("SuperState.gd") 5 | const State = preload("State.gd") 6 | const Transition = preload("Transition.gd") 7 | 8 | var entry_node_offset : Vector2 9 | 10 | var _default_state : State 11 | var _default_transition_reroute_points : PoolVector2Array 12 | 13 | export(Array) var superstates = [] 14 | export(Array) var states = [] 15 | export(Array) var transitions = [] 16 | 17 | func get_default_state() -> State: 18 | return _default_state 19 | 20 | func set_default_state(p_state : State): 21 | if p_state == null: 22 | _default_state = null 23 | _default_transition_reroute_points.resize(0) 24 | 25 | property_list_changed_notify() 26 | return 27 | 28 | if !states.has(p_state): 29 | return 30 | 31 | _default_state = p_state 32 | _default_transition_reroute_points.resize(0) 33 | 34 | property_list_changed_notify() 35 | 36 | func add_superstate(p_state_script : GDScript, p_outputs : PoolStringArray = PoolStringArray()) -> Superstate: 37 | var superstate_resource = Superstate.new() 38 | var superstate = superstate_resource.duplicate() as Superstate 39 | 40 | superstate.state_script = p_state_script 41 | superstate.outputs = p_outputs 42 | 43 | superstates.push_back(superstate) 44 | 45 | update_superstates() 46 | 47 | property_list_changed_notify() 48 | 49 | return superstate 50 | 51 | func add_state(p_superstate : Superstate, p_offset : Vector2, p_properties : Dictionary) -> State: 52 | var state_resource = State.new() 53 | var state = state_resource.duplicate() as State 54 | 55 | state.superstate = p_superstate 56 | state.offset = p_offset 57 | state.properties = p_properties 58 | 59 | state.on_property_cache_changed() 60 | 61 | states.push_back(state) 62 | 63 | property_list_changed_notify() 64 | 65 | return state 66 | 67 | func duplicate_state(p_state : State, p_offset : Vector2) -> State: 68 | var state : State = add_state(p_state.superstate, p_offset, p_state.properties.duplicate()) 69 | 70 | property_list_changed_notify() 71 | 72 | return state 73 | 74 | func remove_state(p_state : State): 75 | if !states.has(p_state): 76 | return ERR_DOES_NOT_EXIST 77 | 78 | if _default_state == p_state: 79 | set_default_state(null) 80 | 81 | states.erase(p_state) 82 | 83 | if _is_superstate_redundant(p_state.superstate): 84 | superstates.erase(p_state.superstate) 85 | 86 | update_superstates() 87 | 88 | property_list_changed_notify() 89 | 90 | func get_state(p_index : int): 91 | if states.size() == 0: 92 | return null 93 | 94 | if p_index < 0 || p_index > states.size() - 1: 95 | return null 96 | 97 | return states[p_index] 98 | 99 | func add_transition(p_from_state : State, p_from_slot_index : int, p_to_state : State, p_to_slot_index : int): 100 | if p_from_state == p_to_state: 101 | return null 102 | 103 | if !states.has(p_from_state) || !states.has(p_to_state) : 104 | return null 105 | 106 | var transition_resource = Transition.new() 107 | var transition = transition_resource.duplicate() as Transition 108 | 109 | transition.from_state = p_from_state 110 | transition.from_slot_index = p_from_slot_index 111 | transition.to_state = p_to_state 112 | transition.to_slot_index = p_to_slot_index 113 | 114 | transitions.push_back(transition) 115 | 116 | property_list_changed_notify() 117 | 118 | return transition 119 | 120 | func remove_transition(p_from_state : State, p_from_slot_index : int, p_to_state : State, p_to_slot_index : int): 121 | var transition = get_transition(p_from_state, p_from_slot_index, p_to_state, p_to_slot_index) 122 | 123 | if transition == null: 124 | return ERR_DOES_NOT_EXIST 125 | 126 | transitions.erase(transition) 127 | 128 | property_list_changed_notify() 129 | 130 | return OK 131 | 132 | func get_transition(p_from_state : State, p_from_slot_index : int, p_to_state : State, p_to_slot_index : int): 133 | for i in transitions.size(): 134 | var transition = transitions[i] as Transition 135 | if transition.from_state != p_from_state || transition.to_state != p_to_state: 136 | continue 137 | 138 | if transition.from_slot_index == p_from_slot_index && transition.to_slot_index == p_to_slot_index: 139 | return transition 140 | 141 | return null 142 | 143 | func get_attached_connections(p_state : State): 144 | var connections = [] 145 | 146 | for i in transitions.size(): 147 | var transition = transitions[i] as Transition 148 | 149 | if transition.from_state == p_state || transition.to_state == p_state: 150 | connections.push_back(transition) 151 | 152 | return connections 153 | 154 | func get_outgoing_connections(p_from_state : State, p_from_slot_index : int): 155 | var connections = [] 156 | 157 | for i in transitions.size(): 158 | var transition = transitions[i] as Transition 159 | 160 | if transition.from_state == p_from_state && transition.from_slot_index == p_from_slot_index: 161 | connections.push_back(transition) 162 | 163 | return connections 164 | 165 | func get_incomming_connections(p_to_state : State, p_to_slot_index : int): 166 | var connections = [] 167 | 168 | for i in transitions.size(): 169 | var transition = transitions[i] as Transition 170 | 171 | if transition.to_state == p_to_state && transition.to_slot_index == p_to_slot_index: 172 | connections.push_back(transition) 173 | 174 | return connections 175 | 176 | func get_state_script_list() -> Array: 177 | var state_script_list : Array = [] 178 | 179 | for i in superstates.size(): 180 | var superstate = superstates[i] as Superstate 181 | 182 | if superstate.state_script == null: 183 | continue 184 | 185 | state_script_list.push_back(superstate.state_script) 186 | 187 | return state_script_list 188 | 189 | func update_reroute_points(p_transition : Transition, p_reroute_points : PoolVector2Array): 190 | p_transition.reroute_points = p_reroute_points 191 | 192 | property_list_changed_notify() 193 | 194 | func update_superstates(): 195 | var state_script_list : Array = get_state_script_list() 196 | 197 | for i in superstates.size(): 198 | var superstate = superstates[i] as Superstate 199 | 200 | superstate.set_graph_state_scripts_list(state_script_list) 201 | 202 | func _is_superstate_redundant(p_superstate : Superstate) -> bool: 203 | for i in states.size(): 204 | var state = states[i] as State 205 | 206 | if state.superstate == p_superstate: 207 | return false 208 | 209 | return true 210 | 211 | func _get_property_list(): 212 | var property_list = [] 213 | 214 | property_list.push_back({ 215 | "name" : "entry_node_offset", 216 | "type" : TYPE_VECTOR2, 217 | "usage" : PROPERTY_USAGE_STORAGE 218 | }) 219 | 220 | property_list.push_back({ 221 | "name" : "_default_state", 222 | "type" : TYPE_OBJECT, 223 | "usage" : PROPERTY_USAGE_STORAGE 224 | }) 225 | 226 | property_list.push_back({ 227 | "name" : "_default_transition_reroute_points", 228 | "type" : TYPE_VECTOR2_ARRAY, 229 | "usage" : PROPERTY_USAGE_STORAGE 230 | }) 231 | 232 | return property_list -------------------------------------------------------------------------------- /Editor/GraphEditorNode.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "Views/GraphEditorNodeView.gd" 3 | 4 | # Slot constants 5 | const Slot = preload("GraphEditorNodeSlot.gd") 6 | 7 | # Properties 8 | var title : String = "Node" setget set_title 9 | var is_selected : bool = false setget set_is_selected 10 | var offset : Vector2 = Vector2() setget set_offset 11 | var display_scale : float = 1 12 | var snap_distance : int = -1 setget set_snap_distance 13 | 14 | # Flags 15 | var pressed = false 16 | 17 | # Signals 18 | signal selected 19 | signal deselected 20 | 21 | signal left_clicked 22 | signal right_clicked 23 | signal doubleclicked 24 | 25 | signal offset_changed 26 | signal drag_request(p_relative) 27 | 28 | signal socket_drag_started(p_node, p_slot_index) 29 | 30 | func _gui_input(event): 31 | if event is InputEventMouseButton: 32 | if event.pressed: 33 | match event.button_index: 34 | BUTTON_LEFT: 35 | pressed = true 36 | 37 | emit_signal("left_clicked") 38 | 39 | BUTTON_RIGHT: 40 | emit_signal("right_clicked") 41 | 42 | if event.doubleclick: 43 | match event.button_index: 44 | BUTTON_LEFT: 45 | emit_signal("doubleclicked") 46 | 47 | func _input(event): 48 | if event is InputEventMouseButton: 49 | if event.pressed: 50 | pass 51 | 52 | else: 53 | match event.button_index: 54 | BUTTON_LEFT: 55 | if pressed: 56 | pressed = false 57 | 58 | if snap_distance > -1: 59 | apply_snapped_offset() 60 | 61 | elif event is InputEventMouseMotion: 62 | if pressed && is_selected: 63 | emit_signal("drag_request", event.relative / display_scale) 64 | 65 | func update_size(): 66 | rect_size = Vector2() 67 | 68 | func set_title(p_title : String): 69 | title = p_title 70 | 71 | if title_label == null: 72 | return 73 | 74 | title_label.text = title 75 | 76 | func set_is_selected(p_is_selected : bool): 77 | is_selected = p_is_selected 78 | 79 | if is_selected: 80 | raise() 81 | focus_panel.show() 82 | emit_signal("selected") 83 | 84 | else: 85 | focus_panel.hide() 86 | emit_signal("deselected") 87 | 88 | func set_offset(p_offset : Vector2): 89 | if offset == p_offset: 90 | return 91 | 92 | offset = p_offset 93 | 94 | var target_position = offset * display_scale 95 | 96 | # Apply snapping if needed 97 | if snap_distance > -1: 98 | target_position = target_position.snapped(Vector2(snap_distance, snap_distance)) 99 | 100 | rect_position = target_position 101 | 102 | emit_signal("offset_changed") 103 | 104 | func set_snap_distance(p_snap_distance): 105 | snap_distance = p_snap_distance 106 | 107 | if snap_distance > -1: 108 | apply_snapped_offset() 109 | 110 | func apply_snapped_offset(): 111 | self.offset = offset.snapped(Vector2(snap_distance, snap_distance) / display_scale) 112 | 113 | func add_slot(p_is_input : bool, p_type : int, p_color : Color, p_text : String): 114 | var container : Container = null 115 | 116 | if p_is_input: 117 | container = inputs_container 118 | 119 | else: 120 | container = outputs_container 121 | 122 | if container == null: 123 | return 124 | 125 | var slot : Slot = Slot.new(theme) 126 | container.add_child(slot) 127 | 128 | slot.initialize(p_is_input, p_type, p_color, p_text) 129 | 130 | slot.connect("socket_pressed", self, "on_socket_pressed") 131 | 132 | update_size() 133 | 134 | func remove_slot(p_is_input : bool, p_index : int): 135 | var container : Container = null 136 | 137 | if p_is_input: 138 | container = inputs_container 139 | 140 | else: 141 | container = outputs_container 142 | 143 | if container == null: 144 | return 145 | 146 | var slot = container.get_child(p_index) 147 | 148 | if slot == null: 149 | return 150 | 151 | slot.free() 152 | 153 | update_size() 154 | 155 | func remove_all_slots(): 156 | remove_all_input_slots() 157 | remove_all_output_slots() 158 | 159 | func get_slot_count(p_is_input : bool): 160 | var container : Container = null 161 | 162 | if p_is_input: 163 | container = inputs_container 164 | 165 | else: 166 | container = outputs_container 167 | 168 | if container == null: 169 | return 0 170 | 171 | return container.get_child_count() 172 | 173 | func get_input_slot_count(): 174 | return get_slot_count(true) 175 | 176 | func get_output_slot_count(): 177 | return get_slot_count(false) 178 | 179 | func get_slot_index_from_position(p_is_input : bool, p_position : Vector2): 180 | var slots = [] 181 | 182 | if p_is_input: 183 | slots = get_input_slots() 184 | 185 | else: 186 | slots = get_output_slots() 187 | 188 | for i in slots.size(): 189 | var socket_position = get_socket_position(p_is_input, i) 190 | 191 | if socket_position == p_position: 192 | return i 193 | 194 | return -1 195 | 196 | func get_slot(p_is_input : bool, p_index : int): 197 | var container : Container = null 198 | 199 | if p_is_input: 200 | container = inputs_container 201 | 202 | else: 203 | container = outputs_container 204 | 205 | if container == null: 206 | return null 207 | 208 | return container.get_child(p_index) 209 | 210 | func get_slot_type(p_is_input : bool, p_index : int): 211 | var container : Container = null 212 | 213 | if p_is_input: 214 | container = inputs_container 215 | 216 | else: 217 | container = outputs_container 218 | 219 | if container == null: 220 | return -1 221 | 222 | var slot : Slot = get_slot(p_is_input, p_index) 223 | 224 | if slot == null: 225 | return -1 226 | 227 | return slot.socket_type 228 | 229 | func get_input_slot(p_index): 230 | return get_slot(true, p_index) 231 | 232 | func get_input_slots(): 233 | return inputs_container.get_children() 234 | 235 | func get_output_slot(p_index): 236 | return get_slot(false, p_index) 237 | 238 | func get_output_slots(): 239 | return outputs_container.get_children() 240 | 241 | func get_socket_position(p_is_input : bool, p_index : int): 242 | var slot : Slot = get_slot(p_is_input, p_index) 243 | 244 | if slot == null: 245 | return null 246 | 247 | var socket = slot.socket 248 | 249 | return rect_position + socket.rect_global_position - rect_global_position + socket.rect_size/2 250 | 251 | # Input slots 252 | func add_input_slot(p_type : int, p_color : Color, p_text : String): 253 | add_slot(true, p_type, p_color, p_text) 254 | 255 | func remove_input_slot(p_index : int): 256 | remove_slot(true, p_index) 257 | 258 | func remove_all_input_slots(): 259 | if inputs_container == null: 260 | return 261 | 262 | for slot in inputs_container.get_children(): 263 | slot.free() 264 | 265 | update_size() 266 | 267 | func get_input_slot_socket_position(p_index : int): 268 | return get_socket_position(true, p_index) 269 | 270 | func get_input_slot_type(p_index : int): 271 | return get_slot_type(true, p_index) 272 | 273 | # Output slots 274 | func add_output_slot(p_type : int, p_color : Color, p_text : String): 275 | add_slot(false, p_type, p_color, p_text) 276 | 277 | func remove_output_slot(p_index : int): 278 | remove_slot(false, p_index) 279 | 280 | func remove_all_output_slots(): 281 | if outputs_container == null: 282 | return 283 | 284 | for slot in outputs_container.get_children(): 285 | slot.free() 286 | 287 | update_size() 288 | 289 | func get_output_slot_socket_position(p_index : int): 290 | return get_socket_position(false, p_index) 291 | 292 | func get_output_slot_type(p_index : int): 293 | return get_slot_type(false, p_index) 294 | 295 | func on_socket_pressed(p_is_input, p_slot_index): 296 | var socket_position = get_socket_position(p_is_input, p_slot_index) 297 | var slot_type = get_slot(p_is_input, p_slot_index).socket_type 298 | 299 | if socket_position == null: 300 | return 301 | 302 | emit_signal("socket_drag_started", self, p_is_input, p_slot_index) 303 | -------------------------------------------------------------------------------- /Editor/GraphEditor_NodesLayer.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends Control 3 | 4 | const GraphEditorNode = preload("GraphEditorNode.gd") 5 | const GraphEditorNodeSlot = preload("GraphEditorNodeSlot.gd") 6 | 7 | const GraphEditorEntryNode = preload("GraphEditorEntryNode.gd") 8 | const GraphEditorStateNode = preload("GraphEditorStateNode.gd") 9 | 10 | var snapping_enabled = false setget set_snapping_enabled 11 | var grid_cell_size = 10 12 | 13 | var valid_connection_pairs = [] 14 | 15 | var selection = [] 16 | 17 | signal selection_changed 18 | signal inspect_state_request(p_state) 19 | signal state_node_context_menu_request(p_state_node) 20 | signal begin_connection_drag_request(p_node, p_input, p_slot_index, snap_positions) 21 | signal dragged(p_relative) 22 | 23 | func _init(p_theme : Theme): 24 | theme = p_theme 25 | grid_cell_size = theme.get_constant("graph_editor_grid_cell_size", "Editor") 26 | 27 | func set_snapping_enabled(p_snapping_enabled : bool): 28 | snapping_enabled = p_snapping_enabled 29 | 30 | var snap_distance = -1 31 | 32 | if snapping_enabled: 33 | snap_distance = grid_cell_size 34 | 35 | for child in get_children(): 36 | var node = child as GraphEditorNode 37 | 38 | node.snap_distance = snap_distance 39 | 40 | func add_entry_node(p_graph : StateMachine.Graph): 41 | var entry_node : GraphEditorEntryNode = GraphEditorEntryNode.new(theme) 42 | 43 | add_child(entry_node) 44 | 45 | entry_node.initialize(p_graph) 46 | 47 | # Apply snapping if activated 48 | var snap_distance = -1 49 | 50 | if snapping_enabled: 51 | snap_distance = grid_cell_size 52 | 53 | entry_node.snap_distance = snap_distance 54 | 55 | entry_node.connect("left_clicked", self, "on_node_left_clicked", [entry_node]) 56 | entry_node.connect("drag_request", self, "on_graph_node_drag_request") 57 | entry_node.connect("socket_drag_started", self, "on_node_socket_drag_started") 58 | 59 | func get_entry_node(): 60 | for child in get_children(): 61 | if child is GraphEditorEntryNode: 62 | return child 63 | 64 | return null 65 | 66 | func add_state_node(p_state): 67 | var state_node : GraphEditorStateNode = GraphEditorStateNode.new(theme) 68 | 69 | add_child(state_node) 70 | 71 | state_node.initialize(p_state) 72 | 73 | # Apply snapping if activated 74 | var snap_distance = -1 75 | 76 | if snapping_enabled: 77 | snap_distance = grid_cell_size 78 | 79 | state_node.snap_distance = snap_distance 80 | 81 | state_node.connect("left_clicked", self, "on_node_left_clicked", [state_node]) 82 | state_node.connect("right_clicked", self, "on_state_node_right_clicked", [state_node]) 83 | state_node.connect("doubleclicked", self, "on_state_node_doubleclicked", [state_node]) 84 | state_node.connect("drag_request", self, "on_graph_node_drag_request") 85 | state_node.connect("socket_drag_started", self, "on_node_socket_drag_started") 86 | 87 | func get_state_node(p_state : StateMachine.Graph.State): 88 | for child in get_children(): 89 | if !(child is GraphEditorStateNode): 90 | continue 91 | 92 | if child.state == p_state: 93 | return child 94 | 95 | return null 96 | 97 | func remove_state_node(p_state_node : GraphEditorStateNode): 98 | if selection.size() > 0: 99 | clear_selection() 100 | 101 | p_state_node.queue_free() 102 | 103 | func clear(): 104 | for child in get_children(): 105 | child.free() 106 | 107 | func box_select(p_rect : Rect2): 108 | for child in get_children(): 109 | var node = child as GraphEditorNode 110 | 111 | if p_rect.intersects(node.get_rect()): 112 | add_to_selection(node) 113 | 114 | func box_deselect(p_rect : Rect2): 115 | for child in get_children(): 116 | var node = child as GraphEditorNode 117 | 118 | if p_rect.intersects(node.get_rect()): 119 | remove_from_selection(node) 120 | 121 | func select_node(p_node : GraphEditorNode): 122 | for i in selection.size(): 123 | var node = selection[i] as GraphEditorNode 124 | 125 | node.is_selected = false 126 | 127 | selection.clear() 128 | 129 | selection.push_back(p_node) 130 | p_node.is_selected = true 131 | 132 | emit_signal("selection_changed") 133 | 134 | func add_to_selection(p_node : GraphEditorNode): 135 | if selection.has(p_node): 136 | return 137 | 138 | selection.push_back(p_node) 139 | p_node.is_selected = true 140 | 141 | emit_signal("selection_changed") 142 | 143 | func remove_from_selection(p_node : GraphEditorNode): 144 | if !selection.has(p_node): 145 | return 146 | 147 | selection.erase(p_node) 148 | p_node.is_selected = false 149 | 150 | emit_signal("selection_changed") 151 | 152 | func clear_selection(): 153 | for i in selection.size(): 154 | var node = selection[i] as GraphEditorNode 155 | node.is_selected = false 156 | 157 | selection.clear() 158 | 159 | emit_signal("selection_changed") 160 | 161 | func on_node_left_clicked(p_node : GraphEditorNode): 162 | if Input.is_key_pressed(KEY_SHIFT): 163 | add_to_selection(p_node) 164 | return 165 | 166 | elif Input.is_key_pressed(KEY_META): 167 | remove_from_selection(p_node) 168 | return 169 | 170 | if selection.size() > 1: 171 | if selection.has(p_node): 172 | return 173 | 174 | select_node(p_node) 175 | 176 | func on_state_node_right_clicked(p_node : GraphEditorStateNode): 177 | if selection.size() > 1: 178 | pass 179 | else: 180 | select_node(p_node) 181 | 182 | emit_signal("state_node_context_menu_request", p_node) 183 | 184 | func on_state_node_doubleclicked(p_node : GraphEditorStateNode): 185 | emit_signal("inspect_state_request", p_node.state) 186 | 187 | func on_graph_node_drag_request(p_relative_position : Vector2): 188 | for i in selection.size(): 189 | var node = selection[i] as GraphEditorNode 190 | 191 | node.offset += p_relative_position 192 | 193 | emit_signal("dragged", p_relative_position) 194 | 195 | func on_node_socket_drag_started(p_node : GraphEditorNode, p_input : bool, p_slot_index : int): 196 | var slot_type = p_node.get_slot_type(p_input, p_slot_index) 197 | 198 | var valid_types = get_valid_connection_types(p_input, slot_type) 199 | var snap_positions : PoolVector2Array = get_socket_positions(valid_types) 200 | 201 | emit_signal("begin_connection_drag_request", p_node, p_input, p_slot_index, snap_positions) 202 | 203 | func get_valid_connection_types(p_is_input : bool, p_type : int): 204 | var valid_types : PoolIntArray = PoolIntArray() 205 | 206 | for pair in valid_connection_pairs: 207 | if pair.from_type == p_type: 208 | valid_types.push_back(pair.to_type) 209 | 210 | elif pair.to_type == p_type: 211 | valid_types.push_back(pair.from_type) 212 | 213 | return valid_types 214 | 215 | func get_socket_positions(p_valid_types : PoolIntArray = PoolIntArray()): 216 | var socket_positions : PoolVector2Array = PoolVector2Array() 217 | 218 | for child in get_children(): 219 | var node = child as GraphEditorNode 220 | 221 | var input_slot_count = node.get_input_slot_count() 222 | var output_slot_count = node.get_output_slot_count() 223 | 224 | # Input sockets 225 | for i in input_slot_count: 226 | var slot = node.get_input_slot(i) 227 | 228 | # Check slot type 229 | if p_valid_types.size() > 0 && !(slot.socket_type in p_valid_types): 230 | continue 231 | 232 | var socket_position = node.get_socket_position(true, i) 233 | socket_positions.push_back(socket_position) 234 | 235 | # Output sockets 236 | for i in output_slot_count: 237 | var slot = node.get_output_slot(i) 238 | 239 | # Check slot type 240 | if p_valid_types.size() > 0 && !(slot.socket_type in p_valid_types): 241 | continue 242 | 243 | var socket_position = node.get_socket_position(false, i) 244 | socket_positions.push_back(socket_position) 245 | 246 | return socket_positions 247 | 248 | func get_nodes_from_position(p_position : Vector2): 249 | var nodes = [] 250 | 251 | for child in get_children(): 252 | if !(child is GraphEditorNode): 253 | continue 254 | 255 | if child.get_rect().has_point(p_position): 256 | nodes.push_back(child) 257 | 258 | return nodes 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /Editor/GraphEditor.gd: -------------------------------------------------------------------------------- 1 | tool 2 | extends "Views/GraphEditorView.gd" 3 | 4 | enum PopupMenuIDs { 5 | CREATE_NEW_STATE, 6 | SET_AS_START_STATE, 7 | DUPLICATE_STATE, 8 | DELETE_STATE 9 | } 10 | 11 | const GraphEditorNode = preload("GraphEditorNode.gd") 12 | 13 | const GraphEditorEntryNode = preload("GraphEditorEntryNode.gd") 14 | const GraphEditorStateNode = preload("GraphEditorStateNode.gd") 15 | 16 | # Properties 17 | var graph : StateMachine.Graph = null setget set_graph 18 | 19 | var disabled : bool = true setget set_disabled 20 | var snapping_enabled : bool = false setget set_snapping_enabled 21 | 22 | var valid_connection_pairs = [] 23 | 24 | var display_scale : float = 1.0 25 | 26 | var undo_redo : UndoRedo 27 | 28 | # Signals 29 | signal selection_changed(p_state) 30 | 31 | signal set_start_state_request(p_state_node) 32 | signal inspect_state_request(p_state) 33 | 34 | signal reroute_points_changed(p_connection) 35 | 36 | signal graph_edited 37 | 38 | func _init(p_theme : Theme): 39 | theme = p_theme 40 | initialize_view() 41 | 42 | display_scale = theme.get_constant("scale", "Editor") 43 | 44 | func _enter_tree(): 45 | # Set up valid connection pairs 46 | add_valid_connection_pair(0, 1) 47 | add_valid_connection_pair(1, 2) 48 | 49 | # Scroll Container 50 | scroll_container.connect("left_click_down", self, "on_scroll_container_left_click_down") 51 | scroll_container.connect("right_click_down", self, "on_scroll_container_context_menu_request") 52 | scroll_container.connect("state_scripts_dropped", self, "on_state_scripts_dropped") 53 | 54 | # Connections layer 55 | connections_layer.connect("reroute_points_changed", self, "on_reroute_points_changed") 56 | 57 | # Nodes layer 58 | nodes_layer.connect("selection_changed", self, "on_nodes_layer_selection_changed") 59 | nodes_layer.connect("inspect_state_request", self, "on_inspect_state_request") 60 | nodes_layer.connect("state_node_context_menu_request", self, "on_state_node_context_menu_request") 61 | nodes_layer.connect("begin_connection_drag_request", self, "on_begin_connection_drag_request") 62 | nodes_layer.connect("dragged", self, "on_nodes_layer_dragged") 63 | 64 | # Overlay Layer 65 | overlay_layer.connect("connection_drag_completed", self, "on_overlay_layer_connection_drag_completed") 66 | overlay_layer.connect("selection_box_drag_completed", self, "on_overlay_layer_selection_box_drag_completed") 67 | 68 | # Popup Menu 69 | popup_menu.connect("id_pressed", self, "on_popup_menu_id_pressed") 70 | 71 | func set_graph(p_graph : StateMachine.Graph): 72 | clear_graph() 73 | 74 | if !(p_graph is StateMachine.Graph): 75 | graph = null 76 | return 77 | 78 | graph = p_graph 79 | 80 | populate_graph(graph) 81 | 82 | graph.update_superstates() 83 | 84 | func set_disabled(p_disabled : bool): 85 | disabled = p_disabled 86 | 87 | func set_snapping_enabled(p_snapping_enabled : bool): 88 | snapping_enabled = p_snapping_enabled 89 | 90 | connections_layer.snapping_enabled = snapping_enabled 91 | nodes_layer.snapping_enabled = snapping_enabled 92 | 93 | func on_scroll_container_left_click_down(): 94 | if !Input.is_key_pressed(KEY_SHIFT) && !Input.is_key_pressed(KEY_META): 95 | nodes_layer.clear_selection() 96 | 97 | overlay_layer.begin_selection_box_drag() 98 | 99 | func on_scroll_container_context_menu_request(): 100 | show_popup_menu(null) 101 | 102 | func on_reroute_points_changed(p_connection): 103 | update_reroute_points(p_connection) 104 | 105 | func on_nodes_layer_selection_changed(): 106 | var state = null 107 | 108 | if nodes_layer.selection.size() == 1: 109 | if nodes_layer.selection[0] is GraphEditorStateNode: 110 | state = nodes_layer.selection[0].state 111 | 112 | emit_signal("selection_changed", state) 113 | 114 | func on_inspect_state_request(p_state): 115 | emit_signal("inspect_state_request", p_state) 116 | 117 | func on_state_node_context_menu_request(p_node): 118 | show_popup_menu(p_node) 119 | 120 | func on_begin_connection_drag_request(p_node : GraphEditorNode, p_input : bool, p_slot_index : int, snap_positions : PoolVector2Array): 121 | var socket_position = p_node.get_socket_position(p_input, p_slot_index) 122 | 123 | # If it's output slot, remove already existing connection 124 | if !p_input: 125 | var output_connections = connections_layer.get_outgoing_connections(p_node, p_slot_index) 126 | 127 | for connection in output_connections: 128 | remove_transition(connection.from_node, connection.from_slot_index, connection.to_node, connection.to_slot_index) 129 | 130 | overlay_layer.begin_connection_drag(p_input, socket_position, snap_positions) 131 | 132 | func on_nodes_layer_dragged(p_relative : Vector2): 133 | emit_signal("graph_edited") 134 | 135 | func on_overlay_layer_connection_drag_completed(p_from_position : Vector2, p_to_position : Vector2, p_is_empty_space : bool): 136 | if p_is_empty_space: 137 | return 138 | 139 | # Find nodes in positions 140 | var from_node : GraphEditorNode = null 141 | var to_node : GraphEditorNode = null 142 | 143 | # From node 144 | var nodes = nodes_layer.get_nodes_from_position(p_from_position) 145 | 146 | if nodes.size() > 0: 147 | from_node = nodes[0] 148 | 149 | # Get the node that's on top of all others 150 | for node in nodes: 151 | if node.get_position_in_parent() > from_node.get_position_in_parent(): 152 | from_node = node 153 | 154 | # To node 155 | nodes = nodes_layer.get_nodes_from_position(p_to_position) 156 | 157 | if nodes.size() > 0: 158 | to_node = nodes[0] 159 | 160 | # Get the node that's on top of all others 161 | for node in nodes: 162 | if node.get_position_in_parent() > to_node.get_position_in_parent(): 163 | to_node = node 164 | 165 | if from_node == null || to_node == null: 166 | return 167 | 168 | if from_node == to_node: 169 | return 170 | 171 | # Find slots in nodes 172 | var from_slot = null 173 | var to_slot = null 174 | 175 | from_slot = from_node.get_slot_index_from_position(false, p_from_position) 176 | to_slot = to_node.get_slot_index_from_position(true, p_to_position) 177 | 178 | if from_slot == null || to_slot == null: 179 | return 180 | 181 | create_new_transition(from_node, from_slot, to_node, to_slot) 182 | 183 | func on_overlay_layer_selection_box_drag_completed(p_rect : Rect2): 184 | if Input.is_key_pressed(KEY_META): 185 | nodes_layer.box_deselect(p_rect) 186 | return 187 | 188 | nodes_layer.box_select(p_rect) 189 | 190 | func on_state_scripts_dropped(p_state_scripts): 191 | nodes_layer.clear_selection() 192 | 193 | var root_position = layers_container.get_local_mouse_position() / display_scale 194 | var position_step = Vector2(20, 20) * display_scale 195 | 196 | for i in p_state_scripts.size(): 197 | create_new_state(root_position + position_step * i, p_state_scripts[i]) 198 | 199 | func on_popup_menu_id_pressed(p_id): 200 | match p_id: 201 | PopupMenuIDs.CREATE_NEW_STATE: 202 | create_new_state(layers_container.get_local_mouse_position() / display_scale, null) 203 | 204 | PopupMenuIDs.SET_AS_START_STATE: 205 | if nodes_layer.selection.size() != 1: 206 | return 207 | 208 | if !(nodes_layer.selection[0] is GraphEditorStateNode): 209 | return 210 | 211 | undo_redo.create_action("Set default state") 212 | 213 | undo_redo.add_do_method(self, "set_default_state", nodes_layer.selection[0]) 214 | 215 | var default_state = graph.get_default_state() 216 | var entry_node = nodes_layer.get_entry_node() 217 | 218 | if default_state == null: 219 | undo_redo.add_undo_method(self, "remove_transition", entry_node, 0, nodes_layer.selection[0], 0) 220 | 221 | else: 222 | var state_node = nodes_layer.get_state_node(default_state) 223 | 224 | undo_redo.add_undo_method(self, "set_default_state", state_node) 225 | 226 | undo_redo.commit_action() 227 | 228 | PopupMenuIDs.DUPLICATE_STATE: 229 | var position_offset = Vector2(20, 20) * display_scale 230 | 231 | for i in nodes_layer.selection.size(): 232 | if !(nodes_layer.selection[i] is GraphEditorStateNode): 233 | continue 234 | 235 | duplicate_state(nodes_layer.selection[i].state, layers_container.get_local_mouse_position() / display_scale + position_offset * i) 236 | 237 | PopupMenuIDs.DELETE_STATE: 238 | var states_to_remove = [] 239 | 240 | for node in nodes_layer.selection: 241 | if !(node is GraphEditorStateNode): 242 | continue 243 | 244 | states_to_remove.push_back(node.state) 245 | 246 | for state in states_to_remove: 247 | remove_state(state) 248 | 249 | states_to_remove.clear() 250 | 251 | func set_default_state(p_state_node : GraphEditorStateNode): 252 | if graph == null: 253 | return 254 | 255 | var entry_node = nodes_layer.get_entry_node() 256 | 257 | # Disconnect present default node if it's set 258 | var default_state = graph.get_default_state() 259 | 260 | if default_state != null: 261 | var state_node = nodes_layer.get_state_node(default_state) 262 | 263 | remove_transition(entry_node, 0, state_node, 0) 264 | 265 | create_new_transition(entry_node, 0, p_state_node, 0) 266 | 267 | func create_new_state(p_offset : Vector2, p_state_script : GDScript): 268 | if graph == null: 269 | return 270 | 271 | var superstate : StateMachine.Graph.Superstate 272 | 273 | if p_state_script != null: 274 | for i in graph.superstates.size(): 275 | var item = graph.superstates[i] as StateMachine.Graph.Superstate 276 | 277 | if item.state_script == p_state_script: 278 | superstate = item 279 | 280 | break 281 | 282 | if superstate == null: 283 | superstate = graph.add_superstate(p_state_script) as StateMachine.Graph.Superstate 284 | 285 | var state = graph.add_state(superstate, p_offset, {}) as StateMachine.Graph.State 286 | 287 | nodes_layer.add_state_node(state) 288 | 289 | func duplicate_state(p_state : StateMachine.Graph.State, p_offset : Vector2): 290 | if graph == null: 291 | return 292 | 293 | nodes_layer.add_state_node(graph.duplicate_state(p_state, p_offset)) 294 | 295 | func remove_state(p_state : StateMachine.Graph.State): 296 | if graph == null: 297 | return 298 | 299 | # Make sure selection is cleared 300 | nodes_layer.clear_selection() 301 | 302 | # Remove all transitions connected to this state 303 | var redundant_transitions = [] 304 | 305 | for i in graph.transitions.size(): 306 | var transition = graph.transitions[i] as StateMachine.Graph.Transition 307 | 308 | if transition.from_state == p_state || transition.to_state == p_state: 309 | redundant_transitions.push_back(transition) 310 | 311 | for i in redundant_transitions.size(): 312 | var transition = redundant_transitions[i] as StateMachine.Graph.Transition 313 | 314 | var from_state = transition.from_state 315 | var from_slot_index = transition.from_slot_index 316 | var to_state = transition.to_state 317 | var to_slot_index = transition.to_slot_index 318 | 319 | if graph.remove_transition(from_state, from_slot_index, to_state, to_slot_index) != OK: 320 | return 321 | 322 | redundant_transitions.clear() 323 | 324 | # If state is set as start state, reset it to none 325 | if graph.get_default_state() == p_state: 326 | graph.set_default_state(null) 327 | 328 | # Get graph editor state node and remove connections 329 | var state_node : GraphEditorStateNode = nodes_layer.get_state_node(p_state) 330 | 331 | if remove_all_connections_from_node(state_node) != OK: 332 | print("remove_state :: Failed to remove all connections from state node") 333 | return 334 | 335 | nodes_layer.remove_state_node(state_node) 336 | 337 | if graph.remove_state(p_state) != OK: 338 | print("remove_state :: Failed to remove state") 339 | return 340 | 341 | print("remove_state :: State removed") 342 | 343 | func create_new_transition(p_from : GraphEditorNode, p_from_index : int, p_to : GraphEditorNode, p_to_index : int): 344 | if graph == null: 345 | return 346 | 347 | if p_from is GraphEditorEntryNode && p_to is GraphEditorStateNode: 348 | graph.set_default_state(p_to.state) 349 | 350 | elif p_from is GraphEditorStateNode && p_to is GraphEditorStateNode: 351 | graph.add_transition(p_from.state, p_from_index, p_to.state, p_to_index) 352 | 353 | else: 354 | return 355 | 356 | connections_layer.add_new_connection(p_from, p_from_index, p_to, p_to_index, PoolVector2Array()) 357 | 358 | func remove_transition(p_from : GraphEditorNode, p_from_index : int, p_to : GraphEditorNode, p_to_index : int): 359 | if graph == null: 360 | return 361 | 362 | # If start node is being disconnected from entry 363 | if p_from is GraphEditorEntryNode && p_to is GraphEditorStateNode: 364 | graph.set_default_state(null) 365 | 366 | elif p_from is GraphEditorStateNode && p_to is GraphEditorStateNode: 367 | graph.remove_transition(p_from.state, p_from_index, p_to.state, p_to_index) 368 | 369 | else: 370 | return 371 | 372 | connections_layer.remove_connection(p_from, p_from_index, p_to, p_to_index) 373 | 374 | func update_reroute_points(p_connection): 375 | if graph == null: 376 | return 377 | 378 | var from_node : GraphEditorNode = p_connection.from_node 379 | var to_node : GraphEditorNode = p_connection.to_node 380 | var from_slot_index : int = p_connection.from_slot_index 381 | var to_slot_index : int = p_connection.to_slot_index 382 | 383 | # If state node if being disconnected from another state node 384 | if !(from_node is GraphEditorStateNode) || !(to_node is GraphEditorStateNode): 385 | return 386 | 387 | var transition = graph.get_transition(from_node.state, from_slot_index, to_node.state, to_slot_index) 388 | 389 | if transition == null: 390 | return 391 | 392 | graph.update_reroute_points(transition, p_connection.reroute_points) 393 | 394 | func show_popup_menu(p_node : GraphEditorNode): 395 | if popup_menu == null: 396 | return 397 | 398 | popup_menu.clear() 399 | popup_menu.rect_size = Vector2() 400 | 401 | if p_node == null: 402 | nodes_layer.clear_selection() 403 | 404 | if nodes_layer.selection.size() == 0: 405 | popup_menu.add_item("New state", PopupMenuIDs.CREATE_NEW_STATE) 406 | 407 | elif nodes_layer.selection.size() == 1: 408 | popup_menu.add_item("Set as default", PopupMenuIDs.SET_AS_START_STATE) 409 | popup_menu.add_separator() 410 | popup_menu.add_item("Duplicate", PopupMenuIDs.DUPLICATE_STATE) 411 | popup_menu.add_separator() 412 | popup_menu.add_item("Remove", PopupMenuIDs.DELETE_STATE) 413 | 414 | else: 415 | popup_menu.add_item("Duplicate", PopupMenuIDs.DUPLICATE_STATE) 416 | popup_menu.add_separator() 417 | popup_menu.add_item("Remove", PopupMenuIDs.DELETE_STATE) 418 | 419 | popup_menu.rect_position = get_global_mouse_position() 420 | popup_menu.popup() 421 | 422 | func add_valid_connection_pair(p_from_type : int, p_to_type : int): 423 | for pair in valid_connection_pairs: 424 | if pair.from_type == p_from_type: 425 | if pair.to_type == p_to_type: 426 | return 427 | 428 | var pair = { "from_type" : p_from_type, "to_type" : p_to_type } 429 | 430 | valid_connection_pairs.push_back(pair) 431 | 432 | nodes_layer.valid_connection_pairs = valid_connection_pairs 433 | 434 | func populate_graph(p_graph : StateMachine.Graph): 435 | clear_graph() 436 | 437 | # Entry node 438 | nodes_layer.add_entry_node(p_graph) 439 | 440 | # State nodes 441 | for state in p_graph.states: 442 | nodes_layer.add_state_node(state) 443 | 444 | # Entry->Start transition 445 | var default_state = p_graph.get_default_state() 446 | 447 | if default_state != null: 448 | var entry_node : GraphEditorEntryNode = nodes_layer.get_entry_node() 449 | var start_node : GraphEditorStateNode = null 450 | 451 | for child in nodes_layer.get_children(): 452 | if !(child is GraphEditorStateNode): 453 | continue 454 | 455 | var node = child as GraphEditorStateNode 456 | 457 | if node.state == default_state: 458 | start_node = node 459 | break 460 | 461 | if entry_node != null && start_node != null: 462 | connections_layer.add_new_connection(entry_node, 0, start_node, 0, PoolVector2Array()) 463 | 464 | # State transitions 465 | for i in p_graph.transitions.size(): 466 | var transition = p_graph.transitions[i] as StateMachine.Graph.Transition 467 | 468 | var from_state = transition.from_state 469 | var to_state = transition.to_state 470 | 471 | if from_state == null || to_state == null: 472 | continue 473 | 474 | var from_node : GraphEditorStateNode = null 475 | var to_node : GraphEditorStateNode = null 476 | 477 | for state_node in nodes_layer.get_children(): 478 | if !(state_node is GraphEditorStateNode): 479 | continue 480 | 481 | if state_node.state == from_state: 482 | from_node = state_node 483 | 484 | elif state_node.state == to_state: 485 | to_node = state_node 486 | 487 | if from_node != null && to_node != null: 488 | break 489 | 490 | if from_node == null || to_node == null: 491 | return 492 | 493 | connections_layer.add_new_connection(from_node, transition.from_slot_index, to_node, transition.to_slot_index, transition.reroute_points) 494 | 495 | func clear_graph(): 496 | nodes_layer.clear_selection() 497 | 498 | connections_layer.clear() 499 | nodes_layer.clear() 500 | 501 | return OK 502 | 503 | func remove_all_connections_from_node(p_node : GraphEditorNode): 504 | var err = OK 505 | 506 | if p_node is GraphEditorStateNode: 507 | for child in connections_layer.get_children(): 508 | if p_node == child.from_node || p_node == child.to_node: 509 | err = connections_layer.remove_connection(child.from_node, child.from_slot_index, child.to_node, child.to_slot_index) 510 | 511 | if err != OK: 512 | return err 513 | 514 | return err 515 | 516 | 517 | 518 | 519 | 520 | --------------------------------------------------------------------------------