246 lines
8.6 KiB
GDScript
246 lines
8.6 KiB
GDScript
@tool
|
|
@icon("compound_state.svg")
|
|
## A compound state is a state that has multiple sub-states of which exactly one can
|
|
## be active at any given time.
|
|
class_name CompoundState
|
|
extends State
|
|
|
|
## Called when a child state is entered.
|
|
signal child_state_entered()
|
|
|
|
## Called when a child state is exited.
|
|
signal child_state_exited()
|
|
|
|
## The initial state which should be activated when this state is activated.
|
|
@export_node_path("State") var initial_state:NodePath:
|
|
get:
|
|
return initial_state
|
|
set(value):
|
|
initial_state = value
|
|
update_configuration_warnings()
|
|
|
|
|
|
## The currently active substate.
|
|
var _active_state:State = null
|
|
|
|
## The initial state
|
|
@onready var _initial_state:State = get_node_or_null(initial_state)
|
|
|
|
## The history states of this compound state.
|
|
var _history_states:Array[HistoryState] = []
|
|
## Whether any of the history states needs a deep history.
|
|
var _needs_deep_history = false
|
|
|
|
func _state_init():
|
|
super._state_init()
|
|
|
|
# check if we have any history states
|
|
for child in get_children():
|
|
if child is HistoryState:
|
|
var child_as_history_state:HistoryState = child as HistoryState
|
|
_history_states.append(child_as_history_state)
|
|
# remember if any of the history states needs a deep history
|
|
_needs_deep_history = _needs_deep_history or child_as_history_state.deep
|
|
|
|
# initialize all substates. find all children of type State and call _state_init on them.
|
|
for child in get_children():
|
|
if child is State:
|
|
var child_as_state:State = child as State
|
|
child_as_state._state_init()
|
|
child_as_state.state_entered.connect(func(): child_state_entered.emit())
|
|
child_as_state.state_exited.connect(func(): child_state_exited.emit())
|
|
|
|
func _state_enter(expect_transition:bool = false):
|
|
super._state_enter()
|
|
# activate the initial state unless we expect a transition
|
|
if not expect_transition:
|
|
if _initial_state != null:
|
|
_active_state = _initial_state
|
|
_active_state._state_enter()
|
|
else:
|
|
push_error("No initial state set for state '" + name + "'.")
|
|
|
|
func _state_step():
|
|
super._state_step()
|
|
if _active_state != null:
|
|
_active_state._state_step()
|
|
|
|
func _state_save(saved_state:SavedState, child_levels:int = -1):
|
|
super._state_save(saved_state, child_levels)
|
|
|
|
# in addition save all history states, as they are never active and normally would not be saved
|
|
var parent = saved_state.get_substate_or_null(self)
|
|
if parent == null:
|
|
push_error("Probably a bug: The state of '" + name + "' was not saved.")
|
|
return
|
|
|
|
for history_state in _history_states:
|
|
history_state._state_save(parent, child_levels)
|
|
|
|
func _state_restore(saved_state:SavedState, child_levels:int = -1):
|
|
super._state_restore(saved_state, child_levels)
|
|
|
|
# in addition check if we are now active and if so determine the current active state
|
|
if active:
|
|
# find the currently active child
|
|
for child in get_children():
|
|
if child is State and child.active:
|
|
_active_state = child
|
|
break
|
|
|
|
func _state_exit():
|
|
# if we have any history states, we need to save the current active state
|
|
if _history_states.size() > 0:
|
|
var saved_state = SavedState.new()
|
|
# we save the entire hierarchy if any of the history states needs a deep history
|
|
# otherwise we only save this level. This way we can save memory and processing time
|
|
_state_save(saved_state, -1 if _needs_deep_history else 1)
|
|
|
|
# now save the saved state in all history states
|
|
for history_state in _history_states:
|
|
# when saving history it's ok when we save deep history in a history state that doesn't need it
|
|
# because at restore time we will use the state's deep flag to determine if we need to restore
|
|
# the entire hierarchy or just this level. This way we don't need multiple copies of the same
|
|
# state hierarchy.
|
|
history_state.history = saved_state
|
|
|
|
# deactivate the current state
|
|
if _active_state != null:
|
|
_active_state._state_exit()
|
|
_active_state = null
|
|
super._state_exit()
|
|
|
|
|
|
func _process_transitions(event:StringName, property_change:bool = false) -> bool:
|
|
if not active:
|
|
return false
|
|
|
|
# forward to the active state
|
|
if is_instance_valid(_active_state):
|
|
if _active_state._process_transitions(event, property_change):
|
|
# emit the event_received signal, unless this is a property change
|
|
if not property_change:
|
|
self.event_received.emit(event)
|
|
return true
|
|
|
|
# if the event was not handled by the active state, we handle it here
|
|
# base class will also emit the event_received signal
|
|
return super._process_transitions(event, property_change)
|
|
|
|
|
|
func _handle_transition(transition:Transition, source:State):
|
|
# print("CompoundState._handle_transition: " + name + " from " + source.name + " to " + str(transition.to))
|
|
# resolve the target state
|
|
var target = transition.resolve_target()
|
|
if not target is State:
|
|
push_error("The target state '" + str(transition.to) + "' of the transition from '" + source.name + "' is not a state.")
|
|
return
|
|
|
|
# the target state can be
|
|
# 0. this state. in this case exit this state and re-enter it. This can happen when
|
|
# a child state transfers to its parent state.
|
|
# 1. a direct child of this state. this is the easy case in which
|
|
# we will deactivate the current _active_state and activate the target
|
|
# 2. a descendant of this state. in this case we find the direct child which
|
|
# is the ancestor of the target state, activate it and then ask it to perform
|
|
# the transition.
|
|
# 3. no descendant of this state. in this case, we ask our parent state to
|
|
# perform the transition
|
|
|
|
if target == self:
|
|
# exit this state and re-enter it
|
|
_state_exit()
|
|
_state_enter(false)
|
|
return
|
|
|
|
if target in get_children():
|
|
# all good, now first deactivate the current state
|
|
if is_instance_valid(_active_state):
|
|
_active_state._state_exit()
|
|
|
|
# now check if the target is a history state, if this is the
|
|
# case, we need to restore the saved state
|
|
if target is HistoryState:
|
|
# print("Target is history state, restoring saved state.")
|
|
var saved_state = target.history
|
|
if saved_state != null:
|
|
# restore the saved state
|
|
_state_restore(saved_state, -1 if target.deep else 1)
|
|
return
|
|
# print("No history saved so far, activating default state.")
|
|
# if we don't have history, we just activate the default state
|
|
var default_state = target.get_node_or_null(target.default_state)
|
|
if is_instance_valid(default_state):
|
|
_active_state = default_state
|
|
_active_state._state_enter()
|
|
return
|
|
else:
|
|
push_error("The default state '" + target.default_state + "' of the history state '" + target.name + "' cannot be found.")
|
|
return
|
|
|
|
# else, just activate the target state
|
|
_active_state = target
|
|
_active_state._state_enter()
|
|
return
|
|
|
|
if self.is_ancestor_of(target):
|
|
# find the child which is the ancestor of the new target.
|
|
for child in get_children():
|
|
if child is State and child.is_ancestor_of(target):
|
|
# found it.
|
|
# change active state if necessary
|
|
if _active_state != child:
|
|
if is_instance_valid(_active_state):
|
|
_active_state._state_exit()
|
|
|
|
_active_state = child
|
|
# set the "expect_transition" flag to true because we will send
|
|
# the transition to the child state right after we activate it.
|
|
# this avoids the child needlessly entering the initial state
|
|
_active_state._state_enter(true)
|
|
|
|
# ask child to handle the transition
|
|
child._handle_transition(transition, source)
|
|
return
|
|
return
|
|
|
|
# ask the parent
|
|
get_parent()._handle_transition(transition, source)
|
|
|
|
|
|
func add_child(node:Node, force_readable_name:bool = false, internal:InternalMode = INTERNAL_MODE_DISABLED) -> void:
|
|
super.add_child(node, force_readable_name, internal)
|
|
# when a child is added in the editor and the child is a state
|
|
# and we don't have an initial state yet, set the initial state
|
|
# to the newly added child
|
|
if Engine.is_editor_hint() and node is State:
|
|
if initial_state.is_empty():
|
|
# the newly added node may have a random name now,
|
|
# so we need to defer the call to build a node path
|
|
# to the next frame, so the editor has time to rename
|
|
# the node to its final name
|
|
(func(): initial_state = get_path_to(node)).call_deferred()
|
|
|
|
|
|
func _get_configuration_warnings() -> PackedStringArray:
|
|
var warnings = super._get_configuration_warnings()
|
|
|
|
# count the amount of child states
|
|
var child_count = 0
|
|
for child in get_children():
|
|
if child is State:
|
|
child_count += 1
|
|
|
|
if child_count < 2:
|
|
warnings.append("Compound states should have at two child states.")
|
|
|
|
var the_initial_state = get_node_or_null(initial_state)
|
|
|
|
if not is_instance_valid(the_initial_state):
|
|
warnings.append("Initial state could not be resolved, is the path correct?")
|
|
|
|
elif the_initial_state.get_parent() != self:
|
|
warnings.append("Initial state must be a direct child of this compound state.")
|
|
|
|
return warnings
|