initial commit
This commit is contained in:
commit
483161d330
1
README
Normal file
1
README
Normal file
@ -0,0 +1 @@
|
||||
icon: made by Freepik from https://www.flaticon.com, license: Creative Commons BY 3.0
|
7
plugin.cfg
Normal file
7
plugin.cfg
Normal 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
178
state_machine.gd
Normal 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
BIN
state_machine.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 614 B |
8
state_machine_init.gd
Normal file
8
state_machine_init.gd
Normal 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")
|
Loading…
x
Reference in New Issue
Block a user