168 lines
6.2 KiB
GDScript3
168 lines
6.2 KiB
GDScript3
|
@icon("state_chart.svg")
|
||
|
@tool
|
||
|
## This is statechart. It contains a root state (commonly a compound or parallel state) and is the entry point for
|
||
|
## the state machine.
|
||
|
class_name StateChart
|
||
|
extends Node
|
||
|
|
||
|
## The the remote debugger
|
||
|
const DebuggerRemote = preload("utilities/editor_debugger/editor_debugger_remote.gd")
|
||
|
|
||
|
## Emitted when the state chart receives an event. This will be
|
||
|
## emitted no matter which state is currently active and can be
|
||
|
## useful to trigger additional logic elsewhere in the game
|
||
|
## without having to create a custom event bus. It is also used
|
||
|
## by the state chart debugger. Note that this will emit the
|
||
|
## events in the order in which they are processed, which may
|
||
|
## be different from the order in which they were received. This is
|
||
|
## because the state chart will always finish processing one event
|
||
|
## fully before processing the next. If an event is received
|
||
|
## while another is still processing, it will be enqueued.
|
||
|
signal event_received(event:StringName)
|
||
|
|
||
|
## Flag indicating if this state chart should be tracked by the
|
||
|
## state chart debugger in the editor.
|
||
|
@export var track_in_editor:bool = false
|
||
|
|
||
|
## The root state of the state chart.
|
||
|
var _state:State = null
|
||
|
|
||
|
## This dictonary contains known properties used in expression guards. Use the
|
||
|
## [method set_expression_property] to add properties to this dictionary.
|
||
|
var _expression_properties:Dictionary = {
|
||
|
}
|
||
|
|
||
|
## A list of events which are still pending resolution.
|
||
|
var _queued_events:Array[StringName] = []
|
||
|
|
||
|
## Flag indicating if the state chart is currently processing an
|
||
|
## event. Until an event is fully processed, new events will be queued
|
||
|
## and then processed later.
|
||
|
var _event_processing_active:bool = false
|
||
|
|
||
|
|
||
|
var _queued_transitions:Array[Dictionary] = []
|
||
|
var _transitions_processing_active:bool = false
|
||
|
|
||
|
|
||
|
var _debugger_remote:DebuggerRemote = null
|
||
|
|
||
|
|
||
|
func _ready() -> void:
|
||
|
if Engine.is_editor_hint():
|
||
|
return
|
||
|
|
||
|
# check if we have exactly one child that is a state
|
||
|
if get_child_count() != 1:
|
||
|
push_error("StateChart must have exactly one child")
|
||
|
return
|
||
|
|
||
|
# check if the child is a state
|
||
|
var child = get_child(0)
|
||
|
if not child is State:
|
||
|
push_error("StateMachine's child must be a State")
|
||
|
return
|
||
|
|
||
|
# initialize the state machine
|
||
|
_state = child as State
|
||
|
_state._state_init()
|
||
|
|
||
|
# enter the state
|
||
|
_state._state_enter.call_deferred()
|
||
|
|
||
|
# if we are in an editor build and this chart should be tracked
|
||
|
# by the debugger, create a debugger remote
|
||
|
if track_in_editor and OS.has_feature("editor"):
|
||
|
_debugger_remote = DebuggerRemote.new(self)
|
||
|
|
||
|
|
||
|
## Sends an event to this state chart. The event will be passed to the innermost active state first and
|
||
|
## is then moving up in the tree until it is consumed. Events will trigger transitions and actions via emitted
|
||
|
## signals. There is no guarantee when the event will be processed. The state chart
|
||
|
## will process the event as soon as possible but there is no guarantee that the
|
||
|
## event will be fully processed when this method returns.
|
||
|
func send_event(event:StringName) -> void:
|
||
|
if not is_instance_valid(_state):
|
||
|
push_error("StateMachine is not initialized")
|
||
|
return
|
||
|
|
||
|
if _event_processing_active:
|
||
|
# the state chart is currently processing an event
|
||
|
# therefore queue the event and process it later.
|
||
|
_queued_events.append(event)
|
||
|
return
|
||
|
|
||
|
# enable the reentrance lock for event processing
|
||
|
_event_processing_active = true
|
||
|
|
||
|
# first process this event.
|
||
|
event_received.emit(event)
|
||
|
_state._process_transitions(event, false)
|
||
|
|
||
|
# if other events have accumulated while the event was processing
|
||
|
# process them in order now
|
||
|
while _queued_events.size() > 0:
|
||
|
var next_event = _queued_events.pop_front()
|
||
|
event_received.emit(next_event)
|
||
|
_state._process_transitions(next_event, false)
|
||
|
|
||
|
_event_processing_active = false
|
||
|
|
||
|
## Allows states to queue a transition for running. This will eventually run the transition
|
||
|
## once all currently running transitions have finished. States should call this method
|
||
|
## when they want to transition away from themselves.
|
||
|
func _run_transition(transition:Transition, source:State):
|
||
|
# if we are currently inside of a transition, queue it up
|
||
|
if _transitions_processing_active:
|
||
|
_queued_transitions.append({transition : source})
|
||
|
return
|
||
|
|
||
|
# we can only transition away from a currently active state
|
||
|
# if for some reason the state no longer is active, ignore the transition
|
||
|
_do_run_transition(transition, source)
|
||
|
|
||
|
# if we still have transitions
|
||
|
while _queued_transitions.size() > 0:
|
||
|
var next_transition_entry = _queued_transitions.pop_front()
|
||
|
var next_transition = next_transition_entry.keys()[0]
|
||
|
var next_transition_source = next_transition_entry[next_transition]
|
||
|
_do_run_transition(next_transition, next_transition_source)
|
||
|
|
||
|
|
||
|
## Runs the transition. Used internally by the state chart, do not call this directly.
|
||
|
func _do_run_transition(transition:Transition, source:State):
|
||
|
if source.active:
|
||
|
# Notify interested parties that the transition is about to be taken
|
||
|
transition.taken.emit()
|
||
|
source._handle_transition(transition, source)
|
||
|
else:
|
||
|
_warn_not_active(transition, source)
|
||
|
|
||
|
|
||
|
func _warn_not_active(transition:Transition, source:State):
|
||
|
push_warning("Ignoring request for transitioning from ", source.name, " to ", transition.to, " as the source state is no longer active. Check whether your trigger multiple state changes within a single frame.")
|
||
|
|
||
|
## Sets a property that can be used in expression guards. The property will be available as a global variable
|
||
|
## with the same name. E.g. if you set the property "foo" to 42, you can use the expression "foo == 42" in
|
||
|
## an expression guard.
|
||
|
func set_expression_property(name:StringName, value) -> void:
|
||
|
_expression_properties[name] = value
|
||
|
# run a property change event through the state chart to run automatic transitions
|
||
|
_state._process_transitions(&"", true)
|
||
|
|
||
|
|
||
|
## Calls the `step` function in all active states. Used for situations where `state_processing` and
|
||
|
## `state_physics_processing` don't make sense (e.g. turn-based games, or games with a fixed timestep).
|
||
|
func step():
|
||
|
_state._state_step()
|
||
|
|
||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||
|
var warnings = []
|
||
|
if get_child_count() != 1:
|
||
|
warnings.append("StateChart must have exactly one child")
|
||
|
else:
|
||
|
var child = get_child(0)
|
||
|
if not child is State:
|
||
|
warnings.append("StateChart's child must be a State")
|
||
|
return warnings
|