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 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)