commit 483161d3302714b2f0c32f7fdadfdd6a3e79179a Author: groug Date: Tue Feb 14 16:07:11 2023 +0100 initial commit diff --git a/README b/README new file mode 100644 index 0000000..15a974e --- /dev/null +++ b/README @@ -0,0 +1 @@ +icon: made by Freepik from https://www.flaticon.com, license: Creative Commons BY 3.0 diff --git a/plugin.cfg b/plugin.cfg new file mode 100644 index 0000000..7c48dad --- /dev/null +++ b/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Simple State Machine" +description="A simple state machine with a node 'StateMachine' and simple code to use it" +author="groug" +version="0.1" +script="state_machine_init.gd" diff --git a/state_machine.gd b/state_machine.gd new file mode 100644 index 0000000..8f7543e --- /dev/null +++ b/state_machine.gd @@ -0,0 +1,178 @@ +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) diff --git a/state_machine.png b/state_machine.png new file mode 100644 index 0000000..2270eaf Binary files /dev/null and b/state_machine.png differ diff --git a/state_machine_init.gd b/state_machine_init.gd new file mode 100644 index 0000000..090f8e4 --- /dev/null +++ b/state_machine_init.gd @@ -0,0 +1,8 @@ +@tool +extends EditorPlugin + +func _enter_tree(): + add_custom_type("StateMachine", "Node2D", preload("state_machine.gd"), preload("state_machine.png")) + +func _exit_tree(): + remove_custom_type("StateMachine")