generated from mstar/godot-template
230 lines
7.8 KiB
GDScript
230 lines
7.8 KiB
GDScript
@tool
|
|
@icon("../../assets/icons/state_machine_icon.png")
|
|
extends State
|
|
class_name StateMachine
|
|
|
|
signal transition_added(transition) # Transition added
|
|
signal transition_removed(to_state) # Transition removed
|
|
|
|
@export var states: Dictionary: # States within this StateMachine, keyed by State.name
|
|
get = get_states,
|
|
set = set_states
|
|
@export var transitions: Dictionary: # Transitions from this state, keyed by Transition.to
|
|
get = get_transitions,
|
|
set = set_transitions
|
|
|
|
var _states
|
|
var _transitions
|
|
|
|
|
|
func _init(p_name="", p_transitions={}, p_states={}):
|
|
super._init(p_name)
|
|
_transitions = p_transitions
|
|
_states = p_states
|
|
|
|
# Attempt to transit with global/local parameters, where local_params override params
|
|
func transit(current_state, params={}, local_params={}):
|
|
var nested_states = current_state.split("/")
|
|
var is_nested = nested_states.size() > 1
|
|
var end_state_machine = self
|
|
var base_path = ""
|
|
for i in nested_states.size() - 1: # Ignore last one, to get its parent StateMachine
|
|
var state = nested_states[i]
|
|
# Construct absolute base path
|
|
base_path = join_path(base_path, [state])
|
|
if end_state_machine != self:
|
|
end_state_machine = end_state_machine.states[state]
|
|
else:
|
|
end_state_machine = _states[state] # First level state
|
|
|
|
# Nested StateMachine in Exit state
|
|
if is_nested:
|
|
var is_nested_exit = nested_states[nested_states.size()-1] == State.EXIT_STATE
|
|
if is_nested_exit:
|
|
# Normalize path to transit again with parent of end_state_machine
|
|
var end_state_machine_parent_path = ""
|
|
for i in nested_states.size() - 2: # Ignore last two state(which is end_state_machine/end_state)
|
|
end_state_machine_parent_path = join_path(end_state_machine_parent_path, [nested_states[i]])
|
|
var end_state_machine_parent = get_state(end_state_machine_parent_path)
|
|
var normalized_current_state = end_state_machine.name
|
|
var next_state = end_state_machine_parent.transit(normalized_current_state, params)
|
|
if next_state:
|
|
# Construct next state into absolute path
|
|
next_state = join_path(end_state_machine_parent_path, [next_state])
|
|
return next_state
|
|
|
|
# Transit with current running nested state machine
|
|
var from_transitions = end_state_machine.transitions.get(nested_states[nested_states.size()-1])
|
|
if from_transitions:
|
|
var from_transitions_array = from_transitions.values()
|
|
from_transitions_array.sort_custom(func(a, b): Transition.sort(a, b))
|
|
|
|
for transition in from_transitions_array:
|
|
var next_state = transition.transit(params, local_params)
|
|
if next_state:
|
|
if "states" in end_state_machine.states[next_state]:
|
|
# Next state is a StateMachine, return entry state of the state machine in absolute path
|
|
next_state = join_path(base_path, [next_state, State.ENTRY_STATE])
|
|
else:
|
|
# Construct next state into absolute path
|
|
next_state = join_path(base_path, [next_state])
|
|
return next_state
|
|
return null
|
|
|
|
# Get state from absolute path, for exmaple, "path/to/state" (root == empty string)
|
|
# *It is impossible to get parent state machine with path like "../sibling", as StateMachine is not structed as a Tree
|
|
func get_state(path):
|
|
var state
|
|
if path.is_empty():
|
|
state = self
|
|
else:
|
|
var nested_states = path.split("/")
|
|
for i in nested_states.size():
|
|
var dir = nested_states[i]
|
|
if state:
|
|
state = state.states[dir]
|
|
else:
|
|
state = _states[dir] # First level state
|
|
return state
|
|
|
|
# Add state, state name must be unique within this StateMachine, return state added if succeed else return null
|
|
func add_state(state):
|
|
if not state:
|
|
return null
|
|
if state.name in _states:
|
|
return null
|
|
|
|
_states[state.name] = state
|
|
return state
|
|
|
|
# Remove state by its name
|
|
func remove_state(state):
|
|
return _states.erase(state)
|
|
|
|
# Change existing state key in states(Dictionary), return true if success
|
|
func change_state_name(from, to):
|
|
if not (from in _states) or to in _states:
|
|
return false
|
|
|
|
for state_key in _states.keys():
|
|
var state = _states[state_key]
|
|
var is_name_changing_state = state_key == from
|
|
if is_name_changing_state:
|
|
state.name = to
|
|
_states[to] = state
|
|
_states.erase(from)
|
|
for from_key in _transitions.keys():
|
|
var from_transitions = _transitions[from_key]
|
|
if from_key == from:
|
|
_transitions.erase(from)
|
|
_transitions[to] = from_transitions
|
|
for to_key in from_transitions.keys():
|
|
var transition = from_transitions[to_key]
|
|
if transition.from == from:
|
|
transition.from = to
|
|
elif transition.to == from:
|
|
transition.to = to
|
|
if not is_name_changing_state:
|
|
# Transitions to name changed state needs to be updated
|
|
from_transitions.erase(from)
|
|
from_transitions[to] = transition
|
|
return true
|
|
|
|
# Add transition, Transition.from must be equal to this state's name and Transition.to not added yet
|
|
func add_transition(transition):
|
|
if transition.from == "" or transition.to == "":
|
|
push_warning("Transition missing from/to (%s/%s)" % [transition.from, transition.to])
|
|
return
|
|
|
|
var from_transitions
|
|
if transition.from in _transitions:
|
|
from_transitions = _transitions[transition.from]
|
|
else:
|
|
from_transitions = {}
|
|
_transitions[transition.from] = from_transitions
|
|
|
|
from_transitions[transition.to] = transition
|
|
emit_signal("transition_added", transition)
|
|
|
|
# Remove transition with Transition.to(name of state transiting to)
|
|
func remove_transition(from_state, to_state):
|
|
var from_transitions = _transitions.get(from_state)
|
|
if from_transitions:
|
|
if to_state in from_transitions:
|
|
from_transitions.erase(to_state)
|
|
if from_transitions.is_empty():
|
|
_transitions.erase(from_state)
|
|
emit_signal("transition_removed", from_state, to_state)
|
|
|
|
func get_entries():
|
|
return _transitions[State.ENTRY_STATE].values()
|
|
|
|
func get_exits():
|
|
return _transitions[State.EXIT_STATE].values()
|
|
|
|
func has_entry():
|
|
return State.ENTRY_STATE in _states
|
|
|
|
func has_exit():
|
|
return State.EXIT_STATE in _states
|
|
|
|
# Get duplicate of states dictionary
|
|
func get_states():
|
|
return _states.duplicate()
|
|
|
|
func set_states(val):
|
|
_states = val
|
|
|
|
# Get duplicate of transitions dictionary
|
|
func get_transitions():
|
|
return _transitions.duplicate()
|
|
|
|
func set_transitions(val):
|
|
_transitions = val
|
|
|
|
static func join_path(base, dirs):
|
|
var path = base
|
|
for dir in dirs:
|
|
if path.is_empty():
|
|
path = dir
|
|
else:
|
|
path = str(path, "/", dir)
|
|
return path
|
|
|
|
# Validate state machine resource to identify and fix error
|
|
static func validate(state_machine):
|
|
var validated = false
|
|
for from_key in state_machine.transitions.keys():
|
|
# Non-existing state found in StateMachine.transitions
|
|
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
|
if not (from_key in state_machine.states):
|
|
validated = true
|
|
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition" % from_key)
|
|
state_machine.transitions.erase(from_key)
|
|
continue
|
|
|
|
var from_transition = state_machine.transitions[from_key]
|
|
for to_key in from_transition.keys():
|
|
# Non-existing state found in StateMachine.transitions
|
|
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
|
if not (to_key in state_machine.states):
|
|
validated = true
|
|
push_warning("gd-YAFSM ValidationError: Non-existing state(%s) found in transition(%s -> %s)" % [to_key, from_key, to_key])
|
|
from_transition.erase(to_key)
|
|
continue
|
|
|
|
# Mismatch of StateMachine.transitions with Transition.to
|
|
# See https://github.com/imjp94/gd-YAFSM/issues/6
|
|
var to_transition = from_transition[to_key]
|
|
if to_key != to_transition.to:
|
|
validated = true
|
|
push_warning("gd-YAFSM ValidationError: Mismatch of StateMachine.transitions key(%s) with Transition.to(%s)" % [to_key, to_transition.to])
|
|
to_transition.to = to_key
|
|
|
|
# Self connecting transition
|
|
# See https://github.com/imjp94/gd-YAFSM/issues/5
|
|
if to_transition.from == to_transition.to:
|
|
validated = true
|
|
push_warning("gd-YAFSM ValidationError: Self connecting transition(%s -> %s)" % [to_transition.from, to_transition.to])
|
|
from_transition.erase(to_key)
|
|
return validated
|