184 lines
5.3 KiB
GDScript
184 lines
5.3 KiB
GDScript
extends Node
|
|
|
|
@onready var parent_node = get_parent()
|
|
var _transitions = []
|
|
var _transitions_by_source = {}
|
|
var _states = ['default']
|
|
var _current_state = 0
|
|
var _last_state = -1
|
|
var _current_state_duration = 0
|
|
var _triggers = {}
|
|
var _current_triggers = []
|
|
|
|
var _same_state_transitions_enabled = true
|
|
var _debug_enabled = false
|
|
|
|
func _ready():
|
|
add_to_group("state_machine")
|
|
|
|
func _physics_process(delta):
|
|
_current_state_duration += delta
|
|
var state_changed = false
|
|
var state_name = get_current_state()
|
|
if _transitions_by_source.has(state_name):
|
|
for transition_index in _transitions_by_source[state_name]:
|
|
var transition = _transitions[transition_index]
|
|
var trans_allowed = _same_state_transitions_enabled == true or state_name != transition['dest']
|
|
if transition['trigger_name'] == '' or _current_triggers.has(transition['trigger_name']):
|
|
if _are_conditions_valid(transition['conditions']) and trans_allowed:
|
|
_change_state(transition['dest'])
|
|
state_changed = true
|
|
break
|
|
|
|
if not state_changed:
|
|
var update_name = _get_on_state_update_method_name(state_name)
|
|
if parent_node.has_method(update_name):
|
|
Callable(parent_node, update_name).call(delta)
|
|
|
|
_current_triggers = []
|
|
|
|
func _are_conditions_valid(conditions):
|
|
var condition_valid = true
|
|
for condition in conditions:
|
|
var not_condition_wanted = condition.begins_with('!')
|
|
var condition_funcname = condition.substr(1, condition.length() - 1) if not_condition_wanted else condition
|
|
var condition_func = Callable(parent_node, condition_funcname)
|
|
var condition_met = condition_func.call()
|
|
if not_condition_wanted:
|
|
condition_met = not condition_met
|
|
if not condition_met:
|
|
condition_valid = false
|
|
break
|
|
return condition_valid
|
|
|
|
func set_same_state_transitions_enabled(val):
|
|
_same_state_transitions_enabled = val
|
|
|
|
func set_debug_enabled(val):
|
|
_debug_enabled = val
|
|
|
|
func get_current_state():
|
|
return _states[_current_state]
|
|
|
|
func get_last_state():
|
|
if _last_state == -1:
|
|
return null
|
|
return _states[_last_state]
|
|
|
|
func get_current_state_duration():
|
|
return _current_state_duration
|
|
|
|
func add_state(name):
|
|
if _states.count(name) == 0:
|
|
_states.append(name)
|
|
|
|
func add_states(states):
|
|
for state in states:
|
|
if _states.count(state) == 0:
|
|
_states.append(state)
|
|
|
|
func set_init_state(name):
|
|
_change_state(name)
|
|
|
|
func _get_on_state_enter_method_name(state):
|
|
return "on_" + state + "_state_enter"
|
|
|
|
func _get_on_state_update_method_name(state):
|
|
return "on_" + state + "_state_update"
|
|
|
|
func _get_on_state_exit_method_name(state):
|
|
return "on_" + state + "_state_exit"
|
|
|
|
func _change_state(name):
|
|
var index = _states.find(name)
|
|
if index != -1:
|
|
var old_state = get_current_state()
|
|
var exit_name = _get_on_state_exit_method_name(old_state)
|
|
# TODO: do not always call triggers (costly?)
|
|
trigger_all(exit_name)
|
|
if parent_node.has_method(exit_name):
|
|
Callable(parent_node, exit_name).call()
|
|
|
|
_last_state = _current_state
|
|
_current_state = index
|
|
_current_state_duration = 0
|
|
if _debug_enabled:
|
|
print(get_parent().name, "'s new state: ", name)
|
|
|
|
var enter_name = _get_on_state_enter_method_name(name)
|
|
trigger_all(enter_name)
|
|
if parent_node.has_method(enter_name):
|
|
Callable(parent_node, enter_name).call()
|
|
|
|
# source: can be a string or an array of string
|
|
# values:
|
|
# - STATE: any known state
|
|
# - *: all the states (including dest)
|
|
# - ^STATE: all the states except STATE
|
|
# - -STATE: remove_at STATE from the previous states found in source
|
|
# e.g. ['*', '-state1', '-state2', 'state1']
|
|
# => will populate everything but state1 and state2 and then add state1
|
|
# e.g. ['^state1']
|
|
# => will populate everything but state1
|
|
func add_transition(source, dest, conditions, trigger_name=''):
|
|
if not source:
|
|
return
|
|
|
|
var states = []
|
|
if typeof(source) == TYPE_ARRAY:
|
|
for state in source:
|
|
states.append(state)
|
|
else:
|
|
states.append(source)
|
|
|
|
var wanted_states = []
|
|
for state in states:
|
|
if state == "*":
|
|
for state2 in _states:
|
|
wanted_states.append(state2)
|
|
elif state.begins_with("^"):
|
|
var unwanted_state = state.lstrip("^")
|
|
for state2 in _states:
|
|
if state2 != unwanted_state:
|
|
wanted_states.append(state2)
|
|
elif state.begins_with("-"):
|
|
var unwanted_state = state.lstrip("-")
|
|
wanted_states.erase(unwanted_state)
|
|
else:
|
|
wanted_states.append(state)
|
|
|
|
if wanted_states.size() > 1:
|
|
for state in wanted_states:
|
|
add_transition(state, dest, conditions, trigger_name)
|
|
return
|
|
|
|
source = wanted_states[0]
|
|
|
|
var transition = {'source': source, 'dest': dest, 'conditions': conditions, 'trigger_name': trigger_name}
|
|
_transitions.append(transition)
|
|
var transition_index = _transitions.size() - 1
|
|
|
|
if !_transitions_by_source.has(source):
|
|
_transitions_by_source[source] = []
|
|
_transitions_by_source[source].append(transition_index)
|
|
|
|
if trigger_name:
|
|
if !_triggers.has(trigger_name):
|
|
_triggers[trigger_name] = []
|
|
_triggers[trigger_name].append(transition_index)
|
|
# dest: state
|
|
# conditions: list of functions
|
|
# trigger_signal: signal that can trigger the transition
|
|
# transitions["idle"] = {
|
|
# "dest": "hurt",
|
|
# "conditions": [],
|
|
# "trigger_signal": "receive_attack"
|
|
# }
|
|
|
|
func trigger(trigger_name):
|
|
if not _current_triggers.has(trigger_name):
|
|
_current_triggers.append(trigger_name)
|
|
|
|
func trigger_all(trigger_name):
|
|
get_tree().call_group("state_machine", "trigger", String(get_parent().name) + "_" + trigger_name)
|