godot_simple_state_machine/state_machine.gd

178 lines
5.1 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)
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)
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)