196 lines
6.9 KiB
GDScript
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)
|