DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 968,547 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
SebiTCR
SebiTCR

Posted on

A simplistic aproach to the Finite State Machines in Godot

Table of contents


What is a Finite State Machine?

When coding how an entity will behave in your game, you would typically write all the behaviours in one big script file.

func _process(_delta):
  if !in_air:
    if(Input.is_action_pressed("move_left")):
      move(Vector2(5,0))
    if !moving:
       pass # Do even more cool stuff
# Etc...

Enter fullscreen mode Exit fullscreen mode

The biggest issue with this script is that, when time time comes to implement another feature into your entity's behaviour, it wil become much harder for you to do it and the code will look even more messier than before. But worry not because the Finite State Machine came to the rescue!

To simply put, the FSM is a type of "system" that controls the states of an entity and keeps tracks of all the states available to it. We'll see in the Practical Example how do we implement one

Now, To clean up the code, we'll break up the code from above into multiple states, coresponding to their specific actions like: idle, moving, jumping.

The Structure

The State Class

At its core, a state has three main functions

func on_state_enter():
  pass


func on_state_update():
  pass

func on_state_exit():
  pass
Enter fullscreen mode Exit fullscreen mode

Both the enter and the exit methods are called when the states are changed. We can now implement the behaviour of our state as well as checking when a certain condition is met in order to switch to another state.

func _on_state_update():
  if moving:
    move(Position)
  else:
    change_state("Idle")
Enter fullscreen mode Exit fullscreen mode

The FSM

In our system, the state machine will only take care of the states. At it's core, it will only contain one important function that we will frequently use across our states.

func change_state(state):
  _current_state.on_state_exit()
  _current_state = state
  _current_state.on_state_enter()
Enter fullscreen mode Exit fullscreen mode

In the next part of this tutorial, we will see how to implement this system so that we can make it functional.

Practical Example

Enough with the theory. It's time to put everything into practice. We've discuss earlier about some principles of the FSM, but how do we exacly implement it? Firstly, let's implement our State Class.

# State.gd
class_name State
extends Node

var fsm = get_parent()
var parent = fsm.get_parent()

func on_state_enter():
  pass


func on_state_update():
  pass


func on_state_exit():
  pass
Enter fullscreen mode Exit fullscreen mode

This will be our base class for our states. Later on, we will create other classes for our states and we will derive all the features. Now, let's have a look at our Finite State Machine Class.

Like I've mentioned before, our state class will only have one essential function in order to be functional (That being the change_state() function). In a way, this is partially true, but it's not enough to be functional. Let's have a look at the code.

# FiniteStateMachine.gd
class_name FiniteStateMachine
extends Node

var _current_state = null

func change_state(state):
  _current_state.on_state_exit()
  _current_state = state
  _current_state.on_state_enter()

Enter fullscreen mode Exit fullscreen mode

But wait! Something doesn't really seem quite right, does it? Our game knows how to change the states, but how does it know what kind of stated the FSM has? Let's fix this!

FSM Node Hierarchy

What needs to be done now is to append the states to the FSM as children, and when calling the change_state function, we will use the name of the State's node as a parameter. The method will get the child node with the parameter's value and it will store it in the _current_state variable. As simple as that...

# FiniteStateMachine.gd
class_name FiniteStateMachine
extends Node

var _current_state = null

func change_state(state: String):
  _current_state.on_state_exit()
  _current_state = get_node(state)
  _current_state.on_state_enter()

Enter fullscreen mode Exit fullscreen mode

One more thing that we forgot to implement is the update function. In our states, we want to check the rules and change the states. The only piece that's missing here is the infinite loop that calls our on_state_update() function. In addition, our _current_state is null by default. A good practice would be to initialize when the entity is ready.

# FiniteStateMachine.gd
class_name FiniteStateMachine
extends Node

var _current_state = null

func _ready():
  _current_state = get_child(0)
  _current_state.on_state_enter()


func change_state(state: String):
  _current_state.on_state_exit()
  _current_state = get_node(state)
  _current_state.on_state_enter()


func _process(_delta):
  _current_state.on_state_update()

Enter fullscreen mode Exit fullscreen mode

Aaand that's it!
In the following example, we will try to control our player using the state machine.


A simple player controller

Let's consider the following scenario: We have created our player but it does nothing. Neither does it move, neither does any other cool stuff. What are we going to do to define it's behaviour?

The player

Before we move on, let's consider the fact that we already have a Finite State Machine implemented in the first place.

In this example, our player will only be able to move, but feel free to add as many states as you need. Let's implement the following states, shall we?

State Graph

Let's start with our player script. We will want to check when the player moves.

# player.gd
extends KinematicBody2D

var is_moving = false

func _process(_delta):
    if Input.is_action_pressed("move_right") or Input.is_action_pressed("move_left"):
        is_moving = true
    else:
        is_moving = false
Enter fullscreen mode Exit fullscreen mode

Now that we can check if the player is moving or not, we can proceed to the creation of the states. Firsly, let's implement the idle state. When a state will become active, we will want to print the current state in the console and then check the rules for the transition.

# idle.gd
extends State


func on_state_enter():
  print("Entering state: ", self.name)


func on_state_update():
    if parent.is_moving:
        fsm.change_state("Moving")


func on_state_exit():
  print("Exiting state: ", self.name)
  pass
Enter fullscreen mode Exit fullscreen mode

While in the idle state we are not doing anything special, in the moving state we'll have to define the movement of the player.

# moving.gd
extends State


func on_state_enter():
  print("Entering state: ", self.name)


func on_state_update():
    if !parent.is_moving:
        fsm.change_state("Idle")

    if Input.is_action_pressed("move_left"):
        parent.move_and_collide(Vector2(5,0))
    elif Input.is_action_pressed("move_right"):
        parent.move_and_collide(Vector2(-5,0))



func on_state_exit():
  print("Exiting state: ", self.name)
  pass

Enter fullscreen mode Exit fullscreen mode

Final Player Node Hierarchy

Now, the only thing left to do is to attach your FMS and states to your player node and it should work like new!

FSM Demo

Congrats for making it so far! Now your player is able to move like never before, with a FSM on the backend! πŸŽ‰

You can find the final project here

Top comments (0)

🌚 Life is too short to browse without dark mode