ucg-3d/addons/godot_state_charts/state_chart_state.gd

319 lines
11 KiB
GDScript

@tool
## This class represents a state that can be either active or inactive.
class_name StateChartState
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:bool = 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_remaining_delay:float = 0
## The initial time of the pending transition.
var _pending_transition_initial_delay: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() -> void:
# 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) -> StateChart:
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, immediately:bool = false):
var initial_delay := transition.evaluate_delay()
if not immediately and initial_delay > 0:
_queue_transition(transition, initial_delay)
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 gives the target of the transition
## that caused the entering of the state. This can be used determine whether an
## initial child state should be activated. If the state entering was not caused by a transition
## this can be null.
func _state_enter(_transition_target:StateChartState):
# 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()
# process transitions that are triggered by entering the state
_process_transitions(StateChart.TriggerType.STATE_ENTER)
## Called when the state is exited.
func _state_exit():
# print("state_exit: " + name)
# cancel any pending transitions
_pending_transition = null
_pending_transition_remaining_delay = 0
_pending_transition_initial_delay = 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) -> void:
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_remaining_delay = _pending_transition_remaining_delay
our_saved_state.pending_transition_initial_delay = _pending_transition_initial_delay
# 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:int = -1 if child_levels == -1 else child_levels - 1
# save all children
for child in get_children():
if child is StateChartState 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) -> void:
# 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(null)
# and restore any pending transition
_pending_transition = get_node_or_null(our_saved_state.pending_transition_name) as Transition
_pending_transition_remaining_delay = our_saved_state.pending_transition_remaining_delay
_pending_transition_initial_delay = our_saved_state.pending_transition_initial_delay
# if _pending_transition != null:
# print("restored pending transition " + _pending_transition.name + " with time " + str(_pending_transition_remaining_delay))
# 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 StateChartState:
child._state_restore(our_saved_state, sub_child_levels)
## Called while the state is active.
func _process(delta:float) -> void:
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_remaining_delay -= delta
# Notify interested parties that currently a transition is pending.
transition_pending.emit(_pending_transition.delay_seconds, max(0, _pending_transition_remaining_delay))
# if the transition is ready, trigger it
# and clear it.
if _pending_transition_remaining_delay <= 0:
var transition_to_send := _pending_transition
_pending_transition = null
_pending_transition_remaining_delay = 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:StateChartState):
push_error("State " + name + " cannot handle transitions.")
func _physics_process(delta:float) -> void:
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 in this state.
func _process_transitions(trigger_type:StateChart.TriggerType, event:StringName = "") -> bool:
if not active:
return false
# emit an event received signal if this is an event trigger
if trigger_type == StateChart.TriggerType.EVENT:
event_received.emit(event)
# Walk over all transitions
for transition in _transitions:
# Check if the transition is triggered by the given trigger type
if transition.is_triggered_by(trigger_type) \
# if the event is given it needs to match the event of the transition
and (event == "" or transition.event == event) \
# and in every case the guard needs to match
and transition.evaluate_guard():
# print(name + ": consuming event " + event)
# first match wins
# if the winning transition is the currently pending transition, we do not replace it
if transition != _pending_transition:
_run_transition(transition)
# but in any case we return true, because we consumed the event
return true
return false
## Queues the transition to be triggered after the delay.
func _queue_transition(transition:Transition, initial_delay:float):
# 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_initial_delay = initial_delay
_pending_transition_remaining_delay = initial_delay
# 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