godot-parkour/addons/godot_state_charts/state.gd

316 lines
10 KiB
GDScript

@tool
## This class represents a state that can be either active or inactive.
class_name State
extends Node
## Called when the state is entered.
signal state_entered()
## Called when the state is exited.
signal state_exited()
## Called when the state receives an event. Only called if the state is active.
signal event_received(event:StringName)
## Called when the state is processing.
signal state_processing(delta:float)
## Called when the state is physics processing.
signal state_physics_processing(delta:float)
## Called when the state chart step function is called.
signal state_stepped()
## Called when the state is receiving input.
signal state_input(event:InputEvent)
## Called when the state is receiving unhandled input.
signal state_unhandled_input(event:InputEvent)
## Called every frame while a delayed transition is pending for this state.
## Returns the initial delay and the remaining delay of the transition.
signal transition_pending(initial_delay:float, remaining_delay:float)
## Whether the state is currently active (internal flag, use active).
var _state_active = false
## Whether the current state is active.
var active:bool:
get: return _state_active
## The currently active pending transition.
var _pending_transition:Transition = null
## Remaining time in seconds until the pending transition is triggered.
var _pending_transition_time:float = 0
## Transitions in this state that react on events.
var _transitions:Array[Transition] = []
## The state chart that owns this state.
var _chart:StateChart
func _ready():
# don't run in the editor
if Engine.is_editor_hint():
return
_chart = _find_chart(get_parent())
## Finds the owning state chart by moving upwards.
func _find_chart(parent:Node):
if parent is StateChart:
return parent
return _find_chart(parent.get_parent())
## Runs a transition either immediately or delayed depending on the
## transition settings.
func _run_transition(transition:Transition):
if transition.delay_seconds > 0:
_queue_transition(transition)
else:
_chart._run_transition(transition, self)
## Called when the state chart is built.
func _state_init():
# disable state by default
process_mode = Node.PROCESS_MODE_DISABLED
_state_active = false
_toggle_processing(false)
# load transitions
_transitions.clear()
for child in get_children():
if child is Transition:
_transitions.append(child)
## Called when the state is entered. The parameter indicates whether the state
## is expected to immediately handle a transition after it has been entered.
## In this case the state should not automatically activate a default child state.
## This is to avoid a situation where a state is entered, activates a child then immediately
## exits and activates another child due to a transition.
func _state_enter(expect_transition:bool = false):
# print("state_enter: " + name)
_state_active = true
process_mode = Node.PROCESS_MODE_INHERIT
# enable processing if someone listens to our signal
_toggle_processing(true)
# emit the signal
state_entered.emit()
# run all automatic transitions
for transition in _transitions:
if not transition.has_event and transition.evaluate_guard():
# first match wins
_run_transition(transition)
## Called when the state is exited.
func _state_exit():
# print("state_exit: " + name)
# cancel any pending transitions
_pending_transition = null
_pending_transition_time = 0
_state_active = false
# stop processing
process_mode = Node.PROCESS_MODE_DISABLED
_toggle_processing(false)
# emit the signal
state_exited.emit()
## Called when the state should be saved. The parameter is is the SavedState object
## of the parent state. The state is expected to add a child to the SavedState object
## under its own name.
##
## The child_levels parameter indicates how many levels of children should be saved.
## If set to -1 (default), all children should be saved. If set to 0, no children should be saved.
##
## This method will only be called if the state is active and should only be called on
## active children if children should be saved.
func _state_save(saved_state:SavedState, child_levels:int = -1):
if not active:
push_error("_state_save should only be called if the state is active.")
return
# create a new SavedState object for this state
var our_saved_state := SavedState.new()
our_saved_state.pending_transition_name = _pending_transition.name if _pending_transition != null else ""
our_saved_state.pending_transition_time = _pending_transition_time
# add it to the parent
saved_state.add_substate(self, our_saved_state)
if child_levels == 0:
return
# calculate the child levels for the children, -1 means all children
var sub_child_levels = -1 if child_levels == -1 else child_levels - 1
# save all children
for child in get_children():
if child is State and child.active:
child._state_save(our_saved_state, sub_child_levels)
## Called when the state should be restored. The parameter is the SavedState object
## of the parent state. The state is expected to retrieve the SavedState object
## for itself from the parent and restore its state from it.
##
## The child_levels parameter indicates how many levels of children should be restored.
## If set to -1 (default), all children should be restored. If set to 0, no children should be restored.
##
## If the state was not active when it was saved, this method still will be called
## but the given SavedState object will not contain any data for this state.
func _state_restore(saved_state:SavedState, child_levels:int = -1):
# print("restoring state " + name)
var our_saved_state = saved_state.get_substate_or_null(self)
if our_saved_state == null:
# if we are currently active, deactivate the state
if active:
_state_exit()
# otherwise we are already inactive, so we don't need to do anything
return
# otherwise if we are currently inactive, activate the state
if not active:
_state_enter()
# and restore any pending transition
_pending_transition = get_node_or_null(our_saved_state.pending_transition_name) as Transition
_pending_transition_time = our_saved_state.pending_transition_time
# if _pending_transition != null:
# print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_time))
# else:
# print("no pending transition restored")
if child_levels == 0:
return
# calculate the child levels for the children, -1 means all children
var sub_child_levels = -1 if child_levels == -1 else child_levels - 1
# restore all children
for child in get_children():
if child is State:
child._state_restore(our_saved_state, sub_child_levels)
## Called while the state is active.
func _process(delta:float):
if Engine.is_editor_hint():
return
# emit the processing signal
state_processing.emit(delta)
# check if there is a pending transition
if _pending_transition != null:
_pending_transition_time -= delta
# Notify interested parties that currently a transition is pending.
transition_pending.emit(_pending_transition.delay_seconds, max(0, _pending_transition_time))
# if the transition is ready, trigger it
# and clear it.
if _pending_transition_time <= 0:
var transition_to_send = _pending_transition
_pending_transition = null
_pending_transition_time = 0
# print("requesting transition from " + name + " to " + transition_to_send.to.get_concatenated_names() + " now")
_chart._run_transition(transition_to_send, self)
func _handle_transition(transition:Transition, source:State):
push_error("State " + name + " cannot handle transitions.")
func _physics_process(delta:float):
if Engine.is_editor_hint():
return
state_physics_processing.emit(delta)
## Called when the state chart step function is called.
func _state_step():
state_stepped.emit()
func _input(event:InputEvent):
state_input.emit(event)
func _unhandled_input(event:InputEvent):
state_unhandled_input.emit(event)
## Processes all transitions. If the property_change parameter is true
## then only transitions which have no event are processed (eventless transitions/automatic transitions)
func _process_transitions(event:StringName, property_change:bool = false) -> bool:
if not active:
return false
# emit an event received signal if this is not a property change
if not property_change:
event_received.emit(event)
# Walk over all transitions
for transition in _transitions:
# the currently pending transition is not replaced by itself
if transition != _pending_transition \
# automatic transitions are always evaluated
# non-automatic only if this evaluation was not triggered
# by property change AND their event matches their current event
and (not transition.has_event or (not property_change and transition.event == event)) \
# and in every case the guard needs to match
and transition.evaluate_guard():
# print(name + ": consuming event " + event)
# first match wins
_run_transition(transition)
return true
return false
## Queues the transition to be triggered after the delay.
## Executes the transition immediately if the delay is 0.
func _queue_transition(transition:Transition):
# print("transitioning from " + name + " to " + transition.to.get_concatenated_names() + " in " + str(transition.delay_seconds) + " seconds" )
# queue the transition for the delay time (0 means next frame)
_pending_transition = transition
_pending_transition_time = transition.delay_seconds
# enable processing when we have a transition
set_process(true)
func _get_configuration_warnings() -> PackedStringArray:
var result = []
# if not at least one of our ancestors is a StateChart add a warning
var parent = get_parent()
var found = false
while is_instance_valid(parent):
if parent is StateChart:
found = true
break
parent = parent.get_parent()
if not found:
result.append("State is not a child of a StateChart. This will not work.")
return result
func _toggle_processing(active:bool):
set_process(active and _has_connections(state_processing))
set_physics_process(active and _has_connections(state_physics_processing))
set_process_input(active and _has_connections(state_input))
set_process_unhandled_input(active and _has_connections(state_unhandled_input))
## Checks whether the given signal has connections.
func _has_connections(sgnl:Signal) -> bool:
return sgnl.get_connections().size() > 0