Forked from firebelley/callable_state_machine.gd
Last active
December 22, 2025 16:51
-
-
Save alkemann/481252a7f8d3d84e606a049a3d6c6b9d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## A state machine that manages state transitions and executes callbacks. | |
| class_name StateMachine | |
| ## The currently active state. | |
| var current_state: State | |
| ## Sets the initial state and calls its enter callback. | |
| ## Should be called once during initialization (e.g., in _ready()). | |
| func set_initial_state(state: State): | |
| _set_state(state) | |
| ## Updates the current state by calling its update callback. | |
| ## Should be called every frame (e.g., in _process()). | |
| func update(): | |
| if not current_state.update.is_null(): | |
| current_state.update.call() | |
| ## Transitions to a new state. Calls leave callback on current state, then enter callback on new state. | |
| ## Uses call_deferred to ensure safe state transitions. | |
| func change_state(state: State): | |
| _set_state.call_deferred(state) | |
| ## Internal method that handles the actual state transition logic. | |
| func _set_state(state: State): | |
| if current_state: | |
| var leave_callable := current_state.leave as Callable | |
| if not leave_callable.is_null(): | |
| leave_callable.call() | |
| current_state = state | |
| var enter_callable = current_state.enter as Callable | |
| if not enter_callable.is_null(): | |
| enter_callable.call() | |
| ## Represents a single state with update, enter, and leave callbacks. | |
| class State extends RefCounted: | |
| ## Callback executed every frame while this state is active. | |
| var update: Callable | |
| ## Callback executed when entering this state. | |
| var enter: Callable | |
| ## Callback executed when leaving this state. | |
| var leave: Callable | |
| var _name: StringName | |
| ## Creates a new state with the provided callbacks. | |
| ## [param call_on_update] Required callback for state updates. | |
| ## [param call_on_enter] Optional callback called when entering the state. | |
| ## [param call_one_leave] Optional callback called when leaving the state. | |
| func _init(call_on_update: Callable, call_on_enter: Callable = Callable(), call_one_leave: Callable = Callable()) -> void: | |
| update = call_on_update | |
| enter = call_on_enter | |
| leave = call_one_leave | |
| _name = call_on_update.get_method() | |
| ## Returns the name of the state derived from the update callback method name. | |
| func name() -> StringName: | |
| return _name |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extends Node | |
| # Create a StateMachine instance to manage state transitions | |
| var state_machine := StateMachine.new() | |
| # Define states using StateMachine.State.new() with three callbacks: | |
| # 1. update: Called every frame while this state is active (required) | |
| # 2. enter: Called when transitioning into this state (optional) | |
| # 3. leave: Called when transitioning out of this state (optional) | |
| var state_idle := StateMachine.State.new( | |
| # Update callback: runs every frame while in idle state | |
| func _state_idle_tick(): | |
| label.text = "IDLE: %.1fs" % timer.time_left, | |
| # Enter callback: runs when entering idle state | |
| func _state_idle_enter(): | |
| counter += 1 | |
| if counter > 5: | |
| state_machine.change_state(state_end) | |
| # Connect signal to trigger state transition | |
| timer.timeout.connect(state_machine.change_state.bind(state_dragging)), | |
| # Leave callback: cleanup signal connections to prevent memory leaks | |
| func _state_idle_leave(): | |
| if timer.timeout.is_connected(state_machine.change_state): | |
| timer.timeout.disconnect(state_machine.change_state) | |
| ) | |
| var state_dragging := StateMachine.State.new( | |
| # Update callback: runs every frame while dragging | |
| func(): label.text = "DRAGGING: %.1fs" % timer.time_left, | |
| # Enter callback: setup signal for transition back to idle | |
| func(): timer.timeout.connect(state_machine.change_state.bind(state_idle)), | |
| # Leave callback: cleanup signal connection | |
| func(): | |
| if timer.timeout.is_connected(state_machine.change_state): | |
| timer.timeout.disconnect(state_machine.change_state) | |
| ) | |
| # Example of a state with no update callback (use Callable() to skip) | |
| var state_end := StateMachine.State.new( | |
| Callable(), # No update callback - state does nothing each frame | |
| # Enter callback: finalize when reaching end state | |
| func(): | |
| timer.stop() | |
| label.text = "Done" | |
| ) | |
| var counter: int = 0 | |
| @onready var timer: Timer = $Timer | |
| @onready var label: Label = $"../UI/Control/PanelContainer/Label" | |
| func _ready() -> void: | |
| # Set the initial state - this will call the enter callback | |
| state_machine.set_initial_state(state_idle) | |
| func _process(_delta: float) -> void: | |
| # Update the state machine each frame - this calls the current state's update callback | |
| state_machine.update() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| extends Node | |
| var sm := StateMachine.new() | |
| var idle := StateMachine.State.new( | |
| func(): label.text = "IDLE: %.1fs" % t.time_left, | |
| func(): | |
| counter += 1 | |
| if counter > 5: sm.change_state(done) | |
| t.timeout.connect(sm.change_state.bind(drag)), | |
| func(): | |
| if t.timeout.is_connected(sm.change_state): t.timeout.disconnect(sm.change_state) | |
| ) | |
| var drag := StateMachine.State.new( | |
| func(): label.text = "DRAGGING: %.1fs" % t.time_left, | |
| func(): t.timeout.connect(sm.change_state.bind(idle)), | |
| func(): | |
| if t.timeout.is_connected(sm.change_state): t.timeout.disconnect(sm.change_state) | |
| ) | |
| var done := StateMachine.State.new( | |
| Callable(), | |
| func(): | |
| t.stop() | |
| label.text = "Done" | |
| ) | |
| var counter = 0 | |
| @onready var t = %Timer | |
| @onready var label = %Label | |
| func _ready(): sm.set_initial_state(idle) | |
| func _process(_delta): sm.update() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment