initial commit

This commit is contained in:
groug 2023-02-14 16:07:11 +01:00
commit 483161d330
5 changed files with 194 additions and 0 deletions

1
README Normal file
View File

@ -0,0 +1 @@
icon: made by Freepik from https://www.flaticon.com, license: Creative Commons BY 3.0

7
plugin.cfg Normal file
View File

@ -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"

178
state_machine.gd Normal file
View File

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

BIN
state_machine.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

8
state_machine_init.gd Normal file
View File

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