How to make a Stand, Crouch, and Prone system with a Node-Based Approach FSM in Godot 4
Christopher Jiovanni Orpilla
IT Student Specializing in Web Development | Enthusiast in Game and App Development
This article will teach us how to make a Stand - Crouch - Prone System with a Node Based Approach Finite State Machine and how to implement this technique in our game development journey
This work heavily references GDQuest's Finite State Machine Guide.
(source and link will be provided at the very bottom, for now, let's learn together)
Quick Vent
I struggled to find good resources on how other developers implement their Character states on standing, crouching, and proning. Most available materials focused only on enemy states, and other character states such as sliding and jumping --which was different from what I was trying to find.
the worst part was, the overwhelming amount of information that I was processing and I failed where to start. I was devastated many times, but I kept banging my head and eventually succeeded. This is how I created the Standing, Crouching, and Proning System for my game.
I hope this article guides you and helps you in your game development journey.
Things we need to know:
Finite State Machine Visualization
At first glance, it looks like a yin-yang drawing! but this is how generally FSM works with their states.
Finite State Machine (FSM) needs to know the current state before moving on to the next state.
for instance, Imagine if:
- we are currently standing. we then decided to crouch.
so we pressed "c" to crouch.
- FSM exits the current state: "standing", then enters the next state: "standCrouching". As soon as FSM enters this state: "standCrouching", it becomes its current state.
Definitions
1 ) Finite State Machines (FSM) - it acts as a manager, for managing the transitioning of States
2 ) States - refer to the conditions of your character. whether standing, crouching, prone, jumping, running, resting, or in other positions.
From GDQuest, You use finite state machines to:
FSM structure
What. Stand Crouching? Prone Crouching?
I basically divided the "crouch" state into two. But why are there two types of crouches?
I will explain it later in the explanation section. Let's focus on the Structure first.
Structure Explanation
1. Nodes
- The CharacterBody3D node is the player node. it has StateMachine as its child node.
- StateMachine is a child node of CharacterBody3D, but it also has children of its own, which are the States: Standing, StandCrouching, ProneCrouching, and Proning.
Friendly Reminder: " State Machine acts as a manager, for managing the transitioning of states. "
2. Scripts
- These are what set the behaviors of the nodes.
With a Solid Black line, The CharacterBody3D node is attached to a script called character. gd.
the same with The StateMachine node, which is attached to a script called stateMachine. gd.
and so on and so forth...
- these are the scripts attached for those specific nodes to set the behavior.
Digging Deeper...
1. CharacterBody3D
- has an attached script named character. gd that defines the behavior of the playable character, a CharacterBody3D node.
2. StateMachine
- has an attached script called state_machine. gd, this script acts as a manager, for managing the transitioning of States.
The state machine node itself represents the finite state machine (FSM) in this architecture. It manages the current state, handles transitions, and ensures that only one state is active at a time.
3. Standing
- has an attached script called standing. gd, this script defines the player's behavior while in the standing state.
and so on and so forth...
State. gd Script - is a separate script, and will also be a base script for the states: Standing, Crouching, and Proning.
base script? This means it provides common properties, methods, and functionality that all states share.
This approach ensures consistency, reduces code duplication, and allows each state to have a defined set of methods and properties (like enter(), exit(), handle_input(), etc.) that can be overridden or extended as needed.
also from GDQuest :
" In this implementation, we will use nodes for the different states. We must be careful because if we define the physicsprocess() or _process() functions in the state script and put code in there, multiple states will process simultaneously. We want only one state’s code to run at a time.
So, we define functions that will allow the state machine to control which state’s code runs and when. "
State will be inherited by the player_state. gd
player_state. gd Script- Similar to the State script, this is a separate script that serves to reference and provide access to the player character's properties and functionality.
-player_state. gd inherits all the properties and methods from the State. In addition to inheriting from State, PlayerState can add player-specific properties and methods (e.g., speed, gravity, etc.).
PlayerState is the class name for the script player_state. gd.
It acts as a bridge, allowing the individual state scripts to access and manipulate the player's specific attributes (like speed, gravity, crouching status, proning status, etc.) and interact with the player node. This organization ensures that state scripts can easily and consistently modify the character's behavior according to the current state.
player_state.gd will be inherited by the states (Standing, Proning , etc.)
3. Lines and Arrows
I believe you already know what those lines and arrows indicate by. but to clarify, I'll tell about them a little:
- a solid black line indicates a direct connection or relationship between objects.
- dashed arrows indicate inheritance.
used by a green dashed arrow, this means, the player_state. gd has inherited the attributes of State.
- hollow circle head denote reference.
used with a hollow circle head with a solid line that matches the player_state. gd's orange color, this denotes that the player_state. gd is referencing the character. gd. This way, we can use the properties defined in the character. gd script and add it to the states controlled by the stateMachine.
The player_state. gd script acts as a bridge that connects the character node with its properties and behaviors to the StateMachine and individual states.
okay, following the structure, Let's build it! --but. I will assume that you have already created your animations for crouching and proning and the player.tscn.
FSM Construction
Back to our Godot, to construction!
1) Follow the structure mentioned above
2) Making the Scripts
State Script
领英推荐
Place the script named State. gd in the "states" folder for organization.
State. gd script:
## Virtual base class for all states.
## Extend this class and override its methods to implement a state.
class_name State extends Node
## Emitted when the state finishes and wants to transition to another state.
signal finished(next_state_path: String, data: Dictionary)
## Called by the state machine when receiving unhandled input events.
func handle_input(_event: InputEvent) -> void:
pass
## Called by the state machine on the engine's main loop tick.
func update(_delta: float) -> void:
pass
## Called by the state machine on the engine's physics update tick.
func physics_update(_delta: float) -> void:
pass
## Called by the state machine upon changing the active state. The `data` parameter
## is a dictionary with arbitrary data the state can use to initialize itself.
func enter(previous_state_path: String, data := {}) -> void:
pass
## Called by the state machine before changing the active state. Use this function
## to clean up the state.
func exit() -> void:
pass
StateMachine Script
Unlike State. gd, which is a separate script, the StateMachine script will be attached to the node.
and then put it also in the states folder to keep it tidy.
state_machine. gd script:
class_name StateMachine extends Node
## picking a starting state
#Put it to inspector as a property
@export var initial_state: State = null
#getting the initial state with an iief
@onready var state: State = (func get_initial_state() -> State:
return initial_state if initial_state != null else get_child(0)
).call()
## referencing the state for transitioning
func _ready() -> void:
#reference the states to the State Machine
for state_node: State in find_children("*", "State"):
state_node.finished.connect(_transitioning_to_next_state)
# State machines usually access data from the root node of the scene they're part of: the owner.
# We wait for the owner to be ready to guarantee all the data and nodes the states may need are available.
await owner.ready
state.enter("")
func _unhandled_input(event: InputEvent) -> void:
state.handle_input(event)
func _process(delta: float) -> void:
state.update(delta)
func _physics_process(delta: float) -> void:
state.physics_update(delta)
##transitioning to next state
func _transitioning_to_next_state(target_state_path: String, data: Dictionary = {}) -> void:
if not has_node(target_state_path):
printerr(owner.name + ": Trying to transition to state " + target_state_path + " but it does not exist.")
return
var previous_state_path := state.name
state.exit()
state = get_node(target_state_path)
state.enter(previous_state_path, data)
Player State script
player_state. gd is a separate script, similar to the State. gd script. We will create this script and place it in the states folder, but it will not be attached to any node.
double check the path and the script file name.
player_state. gd script:
class_name PlayerState extends State
const Standing = "Standing"
const StandCrouching = "StandCrouching"
const ProneCrouching = "ProneCrouching"
const Proning = "Proning"
#static var crouching : bool = false
static var crouchingFromStand : bool = false
static var crouchingFromProne : bool = false
static var proning : bool = false
#for debugging
#var display_output = "crouching: %s, proning: %s" % [str(crouching), str(proning)]
var CharacterPlayer : CharacterBody3D
func _ready() -> void:
await owner.ready
CharacterPlayer = owner as CharacterBody3D
assert(CharacterPlayer != null, "The PlayerState state type must be used only in the player scene. It needs the owner to be a Player node.")
Take note of this part of the script:
var CharacterPlayer : CharacterBody3D
The CharacterPlayer variable in this code refers to the class_name of your Character script, which you can modify as needed.
My Character node's script:
The player_state. gd script acts as a bridge that connects the character node with its properties and behaviors to the StateMachine and individual states. Therefore, it is imperative to reference your character to the player_state.
states Script
Now, we make the scripts for the individual states:
1) Standing state
Attach the script for Standing then put it in the states folder.
standing. gd script:
extends PlayerState
func enter(previous_state_path: String, data := {}) -> void:
if crouchingFromStand:
CharacterPlayer.animation_player.play("StandCrouching", -1, -CharacterPlayer.animation_speed, true)
print("Standing from crouch - ", "crouching: %-8s proning: %s" % [str(crouchingFromStand), str(proning)])
elif crouchingFromProne and not proning:
CharacterPlayer.animation_player.play("StandCrouching", -1, -CharacterPlayer.animation_speed, true)
print("crouchingFromProne - ", "crouching: %-8s proning: %s" % [str(crouchingFromStand), str(proning)])
elif proning:
CharacterPlayer.animation_player.play("StandProne", -1, -CharacterPlayer.animation_speed, true)
print("Standing from prone - ", "crouching: %-8s proning: %s" % [str(crouchingFromStand), str(proning)])
else:
print("Already standing - ", "crouching: %-8s proning: %s" % [str(crouchingFromStand), str(proning)])
# Reset state flags
crouchingFromStand = false
crouchingFromProne = false
proning = false
func physics_update(_delta: float) -> void:
if Input.is_action_just_pressed("crouch_or_uncrouch"):
crouchingFromStand = true
finished.emit(StandCrouching)
if Input.is_action_just_pressed("prone_or_unprone"):
proning = true
finished.emit(Proning)
Now do the same with the other states.
2) StandCrouching
stand_crouching. gd script:
extends PlayerState
func enter(previous_state_path: String, data = {}) -> void:
print("Crouch State: ", "crouching: %s, proning: %s" % [str(crouchingFromStand), str(proning)])
if crouchingFromStand:
CharacterPlayer.animation_player.play("StandCrouching", -1, CharacterPlayer.animation_speed)
print("Entering Crouching State - ", "crouching: %s, proning: %s" % [str(crouchingFromStand), str(proning)])
elif crouchingFromProne and not proning:
CharacterPlayer.animation_player.play("ProneCrouching", -1, -CharacterPlayer.animation_speed, true)
print("Transitioning from Prone to Crouch - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
crouchingFromProne = true
crouchingFromStand = true
proning = false
func physics_update(_delta: float) -> void:
if Input.is_action_just_pressed("crouch_or_uncrouch"):
crouchingFromStand = true
finished.emit(Standing) # Transition back to standing
elif Input.is_action_just_pressed("prone_or_unprone"):
crouchingFromStand = false
proning = true
finished.emit(Proning) # Transition to proning
3) ProneCrouching
prone_crouching. gd script:
extends PlayerState
func enter(previous_state_path: String, data = {}) -> void:
CharacterPlayer.animation_player.play("ProneCrouching", -1, -CharacterPlayer.animation_speed, true)
print("Transitioning from Prone to Crouch - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
crouchingFromProne = true
proning = false
func physics_update(_delta: float) -> void:
if Input.is_action_just_pressed("crouch_or_uncrouch") and crouchingFromProne and not proning:
finished.emit(Standing) # Transition back to standing
if Input.is_action_just_pressed("prone_or_unprone") and crouchingFromProne and not proning:
finished.emit(Proning)
4) Proning
proning. gd script:
extends PlayerState
func enter(previous_state_path: String, data = {}) -> void:
if not crouchingFromProne and proning:
CharacterPlayer.animation_player.play("ProneCrouching", -1, CharacterPlayer.animation_speed)
print("Transitioning from Crouch to Prone - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
else:
CharacterPlayer.animation_player.play("StandProne", -1, CharacterPlayer.animation_speed)
print("Entering Proning State - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
crouchingFromProne = false
proning = true
func physics_update(_delta: float) -> void:
if Input.is_action_just_pressed("prone_or_unprone"):
proning = true
print("Physics_update - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
finished.emit(Standing) # Transition back to standing
elif Input.is_action_just_pressed("crouch_or_uncrouch") and not crouchingFromProne and proning:
crouchingFromProne = true
proning = false
print("Physics_update - ", "crouching: %s, proning: %s" % [str(crouchingFromProne), str(proning)])
finished.emit(ProneCrouching)
Also, Don't forget to set these:
Inputs will not register if these are not configured.
Further Explanation
Two types of crouches?
The problem meets solution.
The Problem
Originally, my state machine only had one crouching state:
But how will information be passed in and out if we simulate a StateMachine?
with a crouching state, along with proning state and standing state. this is what the character can do at most:
the character we play, can only stand to crouch vice versa and stand to prone vice versa. but what about:
Sure, we can modify it into this:
but how about if we move our character consecutively like:
Let's perform the transition of these moves as if we are the StateMachine.
We are the StateMachine now:
current state: Standing
move: Standing state to Proning State.
Transition: exits Standing state, enters Proning state:
current state: Proning
next move: Proning state to Crouching state.
Transition: exits Pronings state, enters Crouching state:
current state: ??
Conflict?
Now, we exit the proning state, and enter the crouching state.
however, we do not know what kind of crouch will we perform? are we to perform;
crouching from Stand?
or perform crouching from Prone?
The flow charts seems feasible, yes. but the coding part requires additional complexity. And that's the problem. It get's complicated, leading to conflicts and unintended results.
Even if we approach our problem with a node based approach FSM, there will be lots of if-else blocks and Boolean values. This will make our FSM struggle, essentially leading to unexpected behaviors and inconsistent state changes.
The Solution
separate the crouching state into two distinct states:
By dividing the crouch state into two, we can manage the transitions and in a more organized way. This separation allows for more precise control over the crouch actions.
FSM Visualization
I hope this helps you through your game development journey, Happy Coding!
Source Code
Documentation