Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/component/callable_state_machine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Callable State machine

State machine where each state provides three callable methods through CallableStareMachine.add_state:
- normal: intended to be called in _process or _physics_process
- enter: called when the state is entered
- leave: called when leaving the state(before enter of the new state)

A state_changed signal is emitted on completion of the state change

See res://tests/unit/test_callable_state_machine.gd for sample usage
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class_name NoxCallableStateMachine extends RefCounted
class_name CallableStateMachine extends RefCounted

signal state_changed(from: Callable, to: Callable)

Expand Down Expand Up @@ -51,7 +51,7 @@ func change_state(to_state: Callable, immediate: bool = false) -> void:
if states.has(to_state_name):
if not current_state_name.is_empty() and states[current_state_name].has("leave"):
states[current_state_name].leave.call()
if not current_state_name.is_empty() and states[to_state_name].has("enter"):
if not to_state_name.is_empty() and states[to_state_name].has("enter"):
states[to_state_name].enter.call()

current_state = to_state
Expand Down
15 changes: 15 additions & 0 deletions src/component/node_state_machine/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Node Finite State Machine

State machine where each state is added as a child that is a subclass of NodeStateMachineState

Each state can then implement various lifecycle methods based on their needs, eg.:
- on_init called in a deferred call
- on_process and on_physics_process called each _process and _physics_process tick unles paused
- on_enter and on_leave called on state change
- many more...

state_entered, state_exited and finshed signals are emitted on various state changes

NodeStateMachineState implements empty methods for all of these

See res://tests/unit/test_node_state_machine.gd for sample usage
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
class_name CodimonsFiniteStateMachine
class_name NodeFiniteStateMachine
extends Node

signal state_changed(new_state: CodimonsStateMachineState)
signal state_changed(new_state: NodeStateMachineState)

@export var current_state: CodimonsStateMachineState = null:
@export var current_state: NodeStateMachineState = null:
set = set_current_state
@export var previous_state: CodimonsStateMachineState = null
@export var previous_state: NodeStateMachineState = null

# will throw an error if there's no current_state
@export var allow_no_state: bool = false
Expand All @@ -16,7 +16,7 @@ signal state_changed(new_state: CodimonsStateMachineState)
# disable temporarily
@export var paused: bool = false

var queued_state: CodimonsStateMachineState
var queued_state: NodeStateMachineState


func _ready():
Expand All @@ -27,7 +27,7 @@ func late_ready():
assert(
current_state or queued_state or allow_no_state, "No FSM initial state " + get_parent().name
)
for state: CodimonsStateMachineState in get_children():
for state: NodeStateMachineState in get_children():
state.on_init()
if queued_state:
current_state = queued_state
Expand Down Expand Up @@ -62,21 +62,21 @@ func _unhandled_input(event: InputEvent):
current_state.on_unhandled_input(event)


func change_state(next_state: CodimonsStateMachineState):
func change_state(next_state: NodeStateMachineState):
if next_state:
previous_state = current_state
current_state = next_state
else:
if not allow_no_state:
push_error("Not a valid CodimonsStateMachineState")
push_error("Not a valid NodeStateMachineState")
else:
if current_state:
current_state.on_exit()
current_state.state_exited.emit()
current_state = null


func set_current_state(next_state: CodimonsStateMachineState):
func set_current_state(next_state: NodeStateMachineState):
if not get_parent().is_inside_tree():
queued_state = next_state
return
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class_name CodimonsStateMachineState
class_name NodeStateMachineState
extends Node

signal state_entered
Expand Down Expand Up @@ -61,5 +61,5 @@ func is_current_state() -> bool:
return get_state_machine().current_state == self


func get_state_machine() -> CodimonsFiniteStateMachine:
func get_state_machine() -> NodeFiniteStateMachine:
return get_parent()
64 changes: 64 additions & 0 deletions src/tests/unit/test_callable_state_machine.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
extends GutTest

var idle_normal_calls: int
var idle_enter_calls: int
var idle_leave_calls: int
var walking_normal_calls: int
var walking_enter_calls: int
var walking_leave_calls: int


func _idle_normal():
idle_normal_calls += 1


func _idle_enter():
idle_enter_calls += 1


func _idle_leave():
idle_leave_calls += 1


func _walking_normal():
walking_normal_calls += 1


func _walking_enter():
walking_enter_calls += 1


func _walking_leave():
walking_leave_calls += 1


func test_callable_state_machine() -> void:
var csm = CallableStateMachine.new()

csm.add_state(_idle_normal, _idle_enter, _idle_leave)
csm.add_state(_walking_normal, _walking_enter, _walking_leave)
csm.set_initial_state(_idle_normal)

assert_eq(idle_enter_calls, 1, "Enter not called on set initial state")
assert_eq(walking_enter_calls, 0, "Enter called on wrong state")
assert_eq(idle_leave_calls, 0, "Leave called on set inital state")
assert_eq(walking_enter_calls, 0, "Leave called on set inital state")

for i in range(5):
csm.update()

assert_eq(idle_normal_calls, 5, "Update callable not called")
assert_eq(walking_normal_calls, 0, "Update callable called on wrong state")

csm.change_state(_walking_normal, true)

assert_eq(idle_enter_calls, 1, "Enter called on wrong state")
assert_eq(walking_enter_calls, 1, "Enter not called on change state")
assert_eq(idle_leave_calls, 1, "Leave not called on change state")
assert_eq(walking_leave_calls, 0, "Leave called on wrong state")

for i in range(3):
csm.update()

assert_eq(idle_normal_calls, 5, "Update callable called on wrong state")
assert_eq(walking_normal_calls, 3, "Update callable not called")
1 change: 1 addition & 0 deletions src/tests/unit/test_callable_state_machine.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://bru1qgw7taovu
2 changes: 1 addition & 1 deletion src/tests/unit/test_debug_draw.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends "res://addons/gut/test.gd"
extends GutTest


func test_debug_draw_valid() -> void:
Expand Down
2 changes: 1 addition & 1 deletion src/tests/unit/test_debug_popup.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends "res://addons/gut/test.gd"
extends GutTest

var called_times: int

Expand Down
34 changes: 34 additions & 0 deletions src/tests/unit/test_node_state_machine.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
extends GutTest


func test_node_state_machine() -> void:
var nsm = NodeFiniteStateMachine.new()
var idle_double = double(NodeStateMachineState).new()
nsm.add_child(idle_double)
var walking_double = double(NodeStateMachineState).new()
nsm.add_child(walking_double)

add_child_autofree(nsm)
nsm.current_state = idle_double

gut.simulate(nsm, 5, 0.1)

assert_called_count(idle_double.on_enter, 1)
assert_called_count(walking_double.on_enter, 0)
assert_called_count(idle_double.on_exit, 0)
assert_called_count(walking_double.on_exit, 0)

assert_called_count(idle_double.on_process, 5)
assert_called_count(walking_double.on_process, 0)

nsm.current_state = walking_double

assert_called_count(idle_double.on_enter, 1)
assert_called_count(walking_double.on_enter, 1)
assert_called_count(idle_double.on_exit, 1)
assert_called_count(walking_double.on_exit, 0)

gut.simulate(nsm, 8, 0.1)

assert_called_count(idle_double.on_process, 5)
assert_called_count(walking_double.on_process, 8)
1 change: 1 addition & 0 deletions src/tests/unit/test_node_state_machine.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://c0tdhusdns0ls
2 changes: 1 addition & 1 deletion src/tests/unit/test_rng_utils.gd
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extends "res://addons/gut/test.gd"
extends GutTest

var test_rng: RandomNumberGenerator

Expand Down