-
Notifications
You must be signed in to change notification settings - Fork 0
State machines
State machines are the foundation of many game features including menus, player controllers, basic enemy AI, and more. Most implementations differ only by minor details so ProtoJam provides a standard node-based implementation to help get your games running without the boilerplate code.
NodeStateMachine is the root of your state machine and handles executing and transitioning between the states under it. By default, the state machine will tick the active state every process frame but it can be configured to tick during the physics frame instead or manually with a call to tick(delta) (ideal for logic that doesn't need to run every frame).
To use the state machine, simply add an instance to your scene, add any states you want it to run as children, and set the initial active state with a call to change_state(state).
An AbstractNodeState is the base class which all states must inherit. It provides the enter, execute, and exit functions used by NodeStateMachine to transition and execute state logic. Additionally, it provides an optional get_state_name function which may be used for debugging purposes.
The enter function contains the logic that should be run every time the state is transition to. The execute function contains the logic which will be run each time the state machine ticks. Finally, the exit function contains the logic to be run before the state is transitioned out. Typically, enter and exit will contain setup and cleanup code like connecting and disconnecting signals used by the state.
When a state change is needed, the active state may queue the new state with a call to change_state from the state_machine instance passed to enter, execute, and exit.
Note
A state's _ready, _process, _physics_process, and other built-in functions will continue to run even while the state is not currently active. Users should be careful if they choose to leverage these functions.
Related, users should be careful to disconnect any signals connected during enter or execute to ensure the signal does not active logic within the state after it is no longer active (unless this is desired behavior).
This implementation is optimized for the state-design pattern which simply dictates that states decide when to transition and what state to transition to instead of the state machine. Consequently, this pattern requires states to have a reference to all the state they want to transition to.
Rather than hard-coding the refences to each state, users are strongly advised to make these references export variables so they may be assigned through the editor. This helps make the states more reusable and easier to test during debugging.
This example demonstrates a basic enemy with idle and attack states created through code.
Note
This example showcases adding and configuring states through code for brevity; however, it's recommended to add and configure states through the scene editor for simplicity.
enemy.gd
class_name Enemy
extends Node3D
var _state_machine: NodeStateMachine = NodeStateMachine.new()
var _idle_state: EnemyIdleState = EnemyIdleState.new()
var _attack_state: EnemyAttackState = EnemyAttackState.new()
@onready var _player_detector: PlayerDetector = %PlayerDetector
@onready var _blaster: Blaster = %Blaster
func _ready() -> void:
# Adding/configuring states is better done through the editor but can be done through code
add_child(_state_machine)
_idle_state.player_seen_state = _attack_state
_idle_state.player_detector = _player_detector
_state_machine.add_child(_idle_state)
_attack_state.player_lost_state = _idle_state
_attack_state.player_detector = _player_detector
_attack_state.blaster = _blaster
_state_machine.add_child(_attack_state)
# Set the initial state
_state_machine.change_state(_idle_state)enemy_idle_state.gd
class_name EnemyIdleState
extends AbstractNodeState
@export var player_seen_state: AbstractNodeState = null
@export var player_detector: PlayerDetector = null
func _execute(state_machine: NodeStateMachine) -> void:
if player_detector.is_player_detected():
state_machine.change_state(player_seen_state)enemy_attack_state.gd
class_name EnemyAttackState
extends AbstractNodeState
@export var player_lost_state: AbstractNodeState = null
@export var player_detector: PlayerDetector = null
@export var blaster: Blaster = null
func _execute(state_machine: NodeStateMachine) -> void:
if player_detector.is_player_detected():
blaster.shoot_at(player_detector.get_player_position())
else:
state_machine.change_state(player_lost_state)Contributions are always welcome! Check out the contributing guide to get started.
Made with ❤️ for humans by humans.