316 lines
10 KiB
GDScript
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
|