stick-the-quick/util/state_machine.gd

196 lines
6.9 KiB
GDScript

class_name StateMachine extends Node
## Can be in one of a set of states. Dispatches signals on tick or transition.
## Stores signals for subscribers interested in specific states.
class State:
## Emitted when entering this state.
signal started
## Emitted when the StateMachine is ticked while in this state.
signal ticked(delta: float)
## Emitted when exiting this state.
signal stopped
## Stores subscriber connection information for all signals of one state.
class Handler:
## Subscribes to State.started.
var on_state_started: Callable = func () -> void: pass
## Subscribes to State.ticked.
var on_state_ticked: Callable = func (_delta: float) -> void: pass
## Subscribes to State.stopped.
var on_state_stopped: Callable = func () -> void: pass
## How to connect to State.started.
var state_started_flags: int
## How to connect to State.ticked.
var state_ticked_flags: int
## How to connect to State.stopped.
var state_stopped_flags: int
func linkup(
param: Variant,
param2: Variant = null,
param3: Variant = null
) -> void:
if param is Callable:
param3 = param2
param2 = param
param = &'ticked'
if param is StringName:
if param == &'started':
on_state_started = param2
if param3 is int:
state_started_flags = param3
elif param == &'ticked':
on_state_ticked = param2
if param3 is int:
state_ticked_flags = param3
elif param == &'stopped':
on_state_stopped = param2
if param3 is int:
state_stopped_flags = param3
elif param is Array:
callv(&'linkup', param)
elif param is Dictionary:
for sig in param:
var rest = param[sig]
if rest is Callable:
linkup(sig, rest)
elif rest is Array:
rest = rest.duplicate()
rest.push_front(sig)
callv(&'linkup', rest)
else:
push_error("StateMachine.Handler invalid param %s" % param)
elif param is Handler:
on_state_started = param.on_state_started
on_state_ticked = param.on_state_ticked
on_state_stopped = param.on_state_stopped
state_started_flags = param.state_started_flags
state_ticked_flags = param.state_ticked_flags
state_stopped_flags = param.state_stopped_flags
else:
push_error("StateMachine.Handler invalid param %s" % param)
func _init(
param: Variant,
param2: Variant = null,
param3: Variant = null
) -> void:
linkup(param, param2, param3)
static func _make_handler_dict(
descriptors: Dictionary[StringName, Variant]
) -> Dictionary[StringName, Handler]:
var result: Dictionary[StringName, Handler] = {}
for state_name in descriptors:
result[state_name] = Handler.new(descriptors[state_name])
return result
## Emitted when starting any state (in addition to State.started).
## [br][br]
## The state argument is the name of the state that is starting.
signal state_changed(state: StringName)
var _states: Dictionary[StringName, State] = {}
var _state: StringName = &''
## Current state. Setting will emit all relevant signals.
var state: StringName:
get():
return _state
set(state):
if _state != state:
if _state:
_ensure_state_exists(_state)
_states[_state].stopped.emit()
_state = state
state_changed.emit(_state)
if _state:
_ensure_state_exists(_state)
_states[_state].started.emit()
func _ensure_state_exists(state_name: StringName) -> void:
if !(state_name in _states):
_states[state_name] = State.new()
## Creates described handlers and subscribes them to corresponding states.
## [br][br]
## A handler descriptor is a key-value pair in the passed Dictionary.
## The key part is a StringName referring to any state,
## and the value part is either a Callable, an Array, or a further Dictionary.
## The value specifies what Callable(s) to connect to the state's signals,
## what signal(s) to connect them to, and what connection flags to use.
## [br][br]
## If the value part of the key-value pair is a Callable, it is connected
## to the signal State.ticked, and no connection flags are used.
## [br][br]
## If the value part of the key-value pair is an Array, it must contain
## optionally a StringName which defaults to &'ticked' if not provided,
## mandatorily a Callable, and optionally an int which defaults to 0
## if not provided, in that order; these, respectively, are the signal name
## to connect to, the Callable to connect to it, and the connection flags
## to use.
## [br][br]
## If the value part of the key-value pair is an additional Dictionary,
## then for each key-value pair of that subordinate Dictionary,
## the key part is a StringName referring to a signal member of State
## (either &'started', &'ticked', or &'stopped') and specifies which signal
## to connect the value part to, and the value part is either a Callable
## or an array containing a Callable and an int indicating connection flags
## to use.
## [br][br]
## Any Callable which is ultimately to be connected to Signal.started
## or Signal.stopped should take no arguments and return nothing.
## Any Callable which is ultimately to be connected to Signal.ticked
## should take one float and return nothing. The float will be the time delta
## of the physics step.
## [br][br]
## If you are interested in performing some uniform action
## whenever any state starts, no matter which it is,
## prefer to subscribe to StateMachine.state_changed.
func connect_handlers(
handler_descriptors: Dictionary[StringName, Variant]
) -> void:
var handlers := _make_handler_dict(handler_descriptors)
for state_name in handlers:
_ensure_state_exists(state_name)
var state_obj := _states[state_name]
var handler := handlers[state_name]
state_obj.started.connect(
handler.on_state_started,
handler.state_started_flags
)
state_obj.ticked.connect(
handler.on_state_ticked,
handler.state_ticked_flags
)
state_obj.stopped.connect(
handler.on_state_stopped,
handler.state_stopped_flags
)
## Unsubscribes described handlers from corresponding states.
## [br][br]
## For information on the expected format of the handler_descriptors parameter,
## see documentation for connect_handlers.
## [br][br]
## It is recommended you build the handler_descriptors parameter only once,
## and use the same Dictionary instance to disconnect as you did to connect,
## in order to ensure the contained Callable instances being disconnected
## are the same instances that were initially connected --
## thus ensuring they are actually disconnected, and not left dangling
## due to failure to find them among the current connections.
func disconnect_handlers(
handler_descriptors: Dictionary[StringName, Variant]
) -> void:
var handlers := _make_handler_dict(handler_descriptors)
for state_name in handlers:
_ensure_state_exists(state_name)
var state_obj := _states[state_name]
var handler := handlers[state_name]
state_obj.started.disconnect(handler.on_state_started)
state_obj.ticked.disconnect(handler.on_state_ticked)
state_obj.stopped.disconnect(handler.on_state_stopped)
func _physics_process(delta: float) -> void:
if _state:
_ensure_state_exists(_state)
_states[_state].ticked.emit(delta)