diff --git a/src/component/callable_state_machine/README.md b/src/component/callable_state_machine/README.md new file mode 100644 index 0000000..9ee8552 --- /dev/null +++ b/src/component/callable_state_machine/README.md @@ -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 diff --git a/src/component/nox_callable_state_machine/nox_callable_state_machine.gd b/src/component/callable_state_machine/callable_state_machine.gd similarity index 92% rename from src/component/nox_callable_state_machine/nox_callable_state_machine.gd rename to src/component/callable_state_machine/callable_state_machine.gd index ac7a18e..be4a881 100644 --- a/src/component/nox_callable_state_machine/nox_callable_state_machine.gd +++ b/src/component/callable_state_machine/callable_state_machine.gd @@ -1,4 +1,4 @@ -class_name NoxCallableStateMachine extends RefCounted +class_name CallableStateMachine extends RefCounted signal state_changed(from: Callable, to: Callable) @@ -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 diff --git a/src/component/nox_callable_state_machine/nox_callable_state_machine.gd.uid b/src/component/callable_state_machine/callable_state_machine.gd.uid similarity index 100% rename from src/component/nox_callable_state_machine/nox_callable_state_machine.gd.uid rename to src/component/callable_state_machine/callable_state_machine.gd.uid diff --git a/src/component/node_state_machine/README.md b/src/component/node_state_machine/README.md new file mode 100644 index 0000000..75c472a --- /dev/null +++ b/src/component/node_state_machine/README.md @@ -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 diff --git a/src/component/codimons_state_machine/finite_state_machine.gd b/src/component/node_state_machine/node_state_machine.gd similarity index 80% rename from src/component/codimons_state_machine/finite_state_machine.gd rename to src/component/node_state_machine/node_state_machine.gd index 886d272..d7b686f 100644 --- a/src/component/codimons_state_machine/finite_state_machine.gd +++ b/src/component/node_state_machine/node_state_machine.gd @@ -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 @@ -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(): @@ -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 @@ -62,13 +62,13 @@ 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() @@ -76,7 +76,7 @@ func change_state(next_state: CodimonsStateMachineState): 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 diff --git a/src/component/codimons_state_machine/finite_state_machine.gd.uid b/src/component/node_state_machine/node_state_machine.gd.uid similarity index 100% rename from src/component/codimons_state_machine/finite_state_machine.gd.uid rename to src/component/node_state_machine/node_state_machine.gd.uid diff --git a/src/component/codimons_state_machine/state_machine_state.gd b/src/component/node_state_machine/state_machine_state.gd similarity index 91% rename from src/component/codimons_state_machine/state_machine_state.gd rename to src/component/node_state_machine/state_machine_state.gd index 658953f..0359f37 100644 --- a/src/component/codimons_state_machine/state_machine_state.gd +++ b/src/component/node_state_machine/state_machine_state.gd @@ -1,4 +1,4 @@ -class_name CodimonsStateMachineState +class_name NodeStateMachineState extends Node signal state_entered @@ -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() diff --git a/src/component/codimons_state_machine/state_machine_state.gd.uid b/src/component/node_state_machine/state_machine_state.gd.uid similarity index 100% rename from src/component/codimons_state_machine/state_machine_state.gd.uid rename to src/component/node_state_machine/state_machine_state.gd.uid diff --git a/src/tests/unit/test_callable_state_machine.gd b/src/tests/unit/test_callable_state_machine.gd new file mode 100644 index 0000000..48a2312 --- /dev/null +++ b/src/tests/unit/test_callable_state_machine.gd @@ -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") diff --git a/src/tests/unit/test_callable_state_machine.gd.uid b/src/tests/unit/test_callable_state_machine.gd.uid new file mode 100644 index 0000000..28ecab5 --- /dev/null +++ b/src/tests/unit/test_callable_state_machine.gd.uid @@ -0,0 +1 @@ +uid://bru1qgw7taovu diff --git a/src/tests/unit/test_debug_draw.gd b/src/tests/unit/test_debug_draw.gd index b239e8a..10e4aae 100644 --- a/src/tests/unit/test_debug_draw.gd +++ b/src/tests/unit/test_debug_draw.gd @@ -1,4 +1,4 @@ -extends "res://addons/gut/test.gd" +extends GutTest func test_debug_draw_valid() -> void: diff --git a/src/tests/unit/test_debug_popup.gd b/src/tests/unit/test_debug_popup.gd index 8eb0cd6..4a54263 100644 --- a/src/tests/unit/test_debug_popup.gd +++ b/src/tests/unit/test_debug_popup.gd @@ -1,4 +1,4 @@ -extends "res://addons/gut/test.gd" +extends GutTest var called_times: int diff --git a/src/tests/unit/test_node_state_machine.gd b/src/tests/unit/test_node_state_machine.gd new file mode 100644 index 0000000..88def04 --- /dev/null +++ b/src/tests/unit/test_node_state_machine.gd @@ -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) diff --git a/src/tests/unit/test_node_state_machine.gd.uid b/src/tests/unit/test_node_state_machine.gd.uid new file mode 100644 index 0000000..28e7ec4 --- /dev/null +++ b/src/tests/unit/test_node_state_machine.gd.uid @@ -0,0 +1 @@ +uid://c0tdhusdns0ls diff --git a/src/tests/unit/test_rng_utils.gd b/src/tests/unit/test_rng_utils.gd index 31544c9..783cb96 100644 --- a/src/tests/unit/test_rng_utils.gd +++ b/src/tests/unit/test_rng_utils.gd @@ -1,4 +1,4 @@ -extends "res://addons/gut/test.gd" +extends GutTest var test_rng: RandomNumberGenerator