import the woofer
This commit is contained in:
parent
825fee53e2
commit
3e89acfea8
92 changed files with 5783 additions and 0 deletions
310
addons/godot_state_charts/state_chart.gd
Normal file
310
addons/godot_state_charts/state_chart.gd
Normal file
|
@ -0,0 +1,310 @@
|
|||
@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")
|
||||
|
||||
## The state chart utility class.
|
||||
const StateChartUtil = preload("utilities/state_chart_util.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)
|
||||
|
||||
@export_group("Debugging")
|
||||
## Flag indicating if this state chart should be tracked by the
|
||||
## state chart debugger in the editor.
|
||||
@export var track_in_editor:bool = false
|
||||
|
||||
## If set, the state chart will issue a warning when trying to
|
||||
## send an event that is not configured for any transition of
|
||||
## the state chart. It is usually a good idea to leave this
|
||||
## enabled, but in certain cases this may get in the way so
|
||||
## you can disable it here.
|
||||
@export var warn_on_sending_unknown_events:bool = true
|
||||
|
||||
@export_group("")
|
||||
## Initial values for the expression properties. These properties can be used in expressions, e.g
|
||||
## for guards or transition delays. It is recommended to set an initial value for each property
|
||||
## you use in an expression to ensure that this expression is always valid. If you don't set
|
||||
## an initial value, some expressions may fail to be evaluated if they use a property that has
|
||||
## not been set yet.
|
||||
@export var initial_expression_properties:Dictionary = {}
|
||||
|
||||
## The root state of the state chart.
|
||||
var _state:StateChartState = 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 pending events
|
||||
var _queued_events:Array[StringName] = []
|
||||
|
||||
## Whether or not a property change is pending.
|
||||
var _property_change_pending:bool = false
|
||||
|
||||
## Whether or not a state change occured during processing and we need to re-run
|
||||
## automatic transitions that may have been triggered by the state change.
|
||||
var _state_change_pending:bool = false
|
||||
|
||||
## Flag indicating if the state chart is currently processing.
|
||||
## Until a change is fully processed, no further changes can
|
||||
## be introduced from the outside.
|
||||
var _locked_down:bool = false
|
||||
|
||||
var _queued_transitions:Array[Dictionary] = []
|
||||
var _transitions_processing_active:bool = false
|
||||
|
||||
var _debugger_remote:DebuggerRemote = null
|
||||
var _valid_event_names:Array[StringName] = []
|
||||
|
||||
## A trigger type that defines events that can trigger a transition.
|
||||
enum TriggerType {
|
||||
## No trigger type. This usually should not happen and is used as a default value.
|
||||
NONE = 0,
|
||||
## The transition will be triggered by an event.
|
||||
EVENT = 1,
|
||||
## The transition is automatic and thus will be triggered when the state is entered.
|
||||
STATE_ENTER = 2,
|
||||
## The transition is automatic and will be triggered by a property change.
|
||||
PROPERTY_CHANGE = 4,
|
||||
## The transition is automatic and will be triggered by a state change.
|
||||
STATE_CHANGE = 8,
|
||||
}
|
||||
|
||||
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:Node = get_child(0)
|
||||
if not child is StateChartState:
|
||||
push_error("StateMachine's child must be a State")
|
||||
return
|
||||
|
||||
# in debug builds, collect a list of valid event names
|
||||
# to warn the developer when using an event that doesn't
|
||||
# exist.
|
||||
if OS.is_debug_build():
|
||||
_valid_event_names = StateChartUtil.events_of(self)
|
||||
|
||||
# set the initial expression properties
|
||||
if initial_expression_properties != null:
|
||||
for key in initial_expression_properties.keys():
|
||||
if not key is String and not key is StringName:
|
||||
push_error("Expression property names must be strings. Ignoring initial expression property with key ", key)
|
||||
continue
|
||||
_expression_properties[key] = initial_expression_properties[key]
|
||||
|
||||
# initialize the state machine
|
||||
_state = child as StateChartState
|
||||
_state._state_init()
|
||||
|
||||
# We wait one frame before entering initial state, so
|
||||
# parents of the state chart have a chance to run their
|
||||
# _ready methods first and not get events from the state
|
||||
# chart while they have not yet been initialized
|
||||
_enter_initial_state.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") and not Engine.is_editor_hint():
|
||||
_debugger_remote = DebuggerRemote.new(self)
|
||||
# add the remote as a child, so it gets cleaned up when the state
|
||||
# chart is deleted
|
||||
add_child(_debugger_remote)
|
||||
|
||||
|
||||
func _enter_initial_state():
|
||||
# https://github.com/derkork/godot-statecharts/issues/143
|
||||
# make sure that transitions resulting from state_enter handlers still
|
||||
# adhere our transactional processing
|
||||
_transitions_processing_active = true
|
||||
_locked_down = true
|
||||
|
||||
# enter the state
|
||||
_state._state_enter(null)
|
||||
|
||||
# run any queued transitions that may have come up during the enter
|
||||
_run_queued_transitions()
|
||||
|
||||
# run any queued external events that may have come up during the enter
|
||||
_run_changes()
|
||||
|
||||
|
||||
## 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_node_ready():
|
||||
push_error("State chart is not yet ready. If you call `send_event` in _ready, please call it deferred, e.g. `state_chart.send_event.call_deferred(\"my_event\").")
|
||||
return
|
||||
|
||||
if not is_instance_valid(_state):
|
||||
push_error("State chart has no root state. Ignoring call to `send_event`.")
|
||||
return
|
||||
|
||||
if warn_on_sending_unknown_events and event != "" and OS.is_debug_build() and not _valid_event_names.has(event):
|
||||
push_warning("State chart does not have an event '", event , "' defined. Sending this event will do nothing.")
|
||||
|
||||
_queued_events.append(event)
|
||||
if _locked_down:
|
||||
return
|
||||
|
||||
_run_changes()
|
||||
|
||||
|
||||
## 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:
|
||||
if not is_node_ready():
|
||||
push_error("State chart is not yet ready. If you call `set_expression_property` in `_ready`, please call it deferred, e.g. `state_chart.set_expression_property.call_deferred(\"my_property\", 5).")
|
||||
return
|
||||
|
||||
if not is_instance_valid(_state):
|
||||
push_error("State chart has no root state. Ignoring call to `set_expression_property`.")
|
||||
return
|
||||
|
||||
_expression_properties[name] = value
|
||||
_property_change_pending = true
|
||||
|
||||
if not _locked_down:
|
||||
_run_changes()
|
||||
|
||||
|
||||
## Returns the value of a previously set expression property. If the property does not exist, the default value
|
||||
## will be returned.
|
||||
func get_expression_property(name:StringName, default:Variant = null) -> Variant:
|
||||
return _expression_properties.get(name, default)
|
||||
|
||||
|
||||
func _run_changes() -> void:
|
||||
# enable the reentrance lock
|
||||
_locked_down = true
|
||||
|
||||
while (not _queued_events.is_empty()) or _property_change_pending or _state_change_pending:
|
||||
# We process stuff in this order:
|
||||
# 1. State changes
|
||||
if _state_change_pending:
|
||||
_state_change_pending = false
|
||||
_state._process_transitions(TriggerType.STATE_CHANGE)
|
||||
|
||||
# 2. Property changes
|
||||
if _property_change_pending:
|
||||
_property_change_pending = false
|
||||
_state._process_transitions(TriggerType.PROPERTY_CHANGE)
|
||||
|
||||
# 3. Events
|
||||
if not _queued_events.is_empty():
|
||||
# process the next event
|
||||
var next_event = _queued_events.pop_front()
|
||||
event_received.emit(next_event)
|
||||
_state._process_transitions(TriggerType.EVENT, next_event)
|
||||
|
||||
_locked_down = 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:StateChartState) -> void:
|
||||
|
||||
# Queue up the transition for running
|
||||
_queued_transitions.append({transition : source})
|
||||
|
||||
# if we are currently inside of a transition, finish processing the queue so we
|
||||
# get a predictable order. Queing can happen a state has an automatic transition on enter,
|
||||
# or when a transition is triggered as part of a signal handler. In these cases, we want to
|
||||
# finish the current transition before starting a new one because otherwise the transitions
|
||||
# see really unpredictable state changes. In a sense, every transition is also a
|
||||
# transaction that needs to be fully processed before the next one can start.
|
||||
if _transitions_processing_active:
|
||||
return
|
||||
|
||||
_run_queued_transitions()
|
||||
|
||||
## Runs all queued transitions until none are left. This also checks for infinite loops in transitions and
|
||||
## ensures triggering guards on state changes.
|
||||
func _run_queued_transitions() -> void:
|
||||
_transitions_processing_active = true
|
||||
|
||||
var execution_count := 1
|
||||
|
||||
# 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)
|
||||
execution_count += 1
|
||||
|
||||
if execution_count > 100:
|
||||
push_error("Infinite loop detected in transitions. Aborting. The state chart is now in an invalid state and no longer usable.")
|
||||
break
|
||||
|
||||
_transitions_processing_active = false
|
||||
|
||||
# transitions trigger a state change which can in turn activate
|
||||
# other transitions, so we need to handle these
|
||||
if not _locked_down:
|
||||
_run_changes()
|
||||
|
||||
## Runs the transition. Used internally by the state chart, do not call this directly.
|
||||
func _do_run_transition(transition:Transition, source:StateChartState):
|
||||
if source.active:
|
||||
# Notify interested parties that the transition is about to be taken
|
||||
transition.taken.emit()
|
||||
source._handle_transition(transition, source)
|
||||
_state_change_pending = true
|
||||
else:
|
||||
_warn_not_active(transition, source)
|
||||
|
||||
|
||||
func _warn_not_active(transition:Transition, source:StateChartState):
|
||||
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.")
|
||||
|
||||
|
||||
|
||||
## 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() -> void:
|
||||
if not is_node_ready():
|
||||
push_error("State chart is not yet ready. If you call `step` in `_ready`, please call it deferred, e.g. `state_chart.step.call_deferred()`.")
|
||||
return
|
||||
|
||||
if not is_instance_valid(_state):
|
||||
push_error("State chart has no root state. Ignoring call to `step`.")
|
||||
return
|
||||
_state._state_step()
|
||||
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings:PackedStringArray = []
|
||||
if get_child_count() != 1:
|
||||
warnings.append("StateChart must have exactly one child")
|
||||
else:
|
||||
var child:Node = get_child(0)
|
||||
if not child is StateChartState:
|
||||
warnings.append("StateChart's child must be a State")
|
||||
return warnings
|
Loading…
Add table
Add a link
Reference in a new issue