GDScript State Machine Tutorial – Complete Guide

If you have ever thought about developing a game or putting your ideas into an interactive experience, then understanding the concept of a State Machine, and in particular, how it is implemented in GDScript for the Godot game engine, is crucial. This tutorial will walk you through the fundamentals of GDScript State Machine, allowing you to enhance your game’s functionality and make it behave in pre-determined ways based on specific conditions.

GDScript: An Introduction

GDScript is the built-in scripting language of the Godot game engine. Designed with simplicity in mind, GDScript allows developers, experienced or beginners, to implement game mechanics or behaviors in a very intuitive manner.

Understanding State Machines

A State Machine is a model of computation or a mathematical model. It can be in one state from a set of states at a time. Depending on previous states or current inputs, it transitions from one state to another.

Why State Machine in GDScript?

GDScript provides an intuitive way of implementing State Machines. They are beneficial in game development scenarios where an object’s behavior changes depending upon its state.

Implementing a State Machine in GDScript can simplify complex game logic, reduce potential errors and bugs, and improve game AI, making it more predictable.</p

CTA Small Image
FREE COURSES AT ZENVA
LEARN GAME DEVELOPMENT, PYTHON AND MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Setting Up a Basic State Machine in GDScript

Let’s start by creating a basic State Machine in GDScript for a game character. We will define three states – Idle, Move and Jump.

enum State {
  IDLE,
  MOVE,
  JUMP
}

var current_state : State = State.IDLE

In this first example, we define an enumeration, or enum, which will hold our different states. We then define a variable, current_state, which will hold the current state of our game character. Right now, the character starts in the IDLE state.

Defining the State Behaviours

Now that we have our states set up, let’s define how our game character behaves in each state. We can do this using the _physics_process() function and a match statement to check the current state.

func _physics_process(delta):
  match current_state:
    State.IDLE:
      idle(delta)
    State.MOVE:
      move(delta)
    State.JUMP:
      jump(delta)

We haven’t written the idle(), move() and jump() functions yet, but when we do, they will contain the logic for how the game character should behave in each state.

Changing States

States aren’t useful unless we can transition between them. For that, we need a way to change the current_state variable. Here’s a simple function that does just that:

func change_state(new_state : State):
  current_state = new_state

This function allows us to easily change the current state, which will change the character’s behavior based on the logic we will define in idle(), move() and jump() functions.

Handling Inputs

Most games interact with the user through inputs. Let’s add an input handler to change states based on user inputs:

func _input(event):
  if event.is_action_pressed("ui_right"):
    change_state(State.MOVE)
  elif event.is_action_released("ui_right"):
    change_state(State.IDLE)
  elif event.is_action_pressed("ui_up"):
    change_state(State.JUMP)

In this code, pressing the right key makes the character enter the MOVE state, releasing it returns it to IDLE, and pressing up makes it enter the JUMP state.

Finalizing the State Behaviours

Now, it’s time to finalize the behaviors for each state when it gets activated. We’ll need to implement the idle(), move(), and jump() functions. Note that these are skeleton functions, and you’ll likely add more logic here in an actual game:

func idle(delta):
  # Code for idle state (waiting, standing still, etc.)

func move(delta):
  # Code for move state (horizontally moving, animating character, etc.)

func jump(delta):
  # Code for jump state (applying vertical velocity, etc.)

Understandably, these functions will depend upon your particular game logic. You might move the character with a certain velocity in the “move” state, display an animation in the “idle” state, or change the vertical position in the “jump” state.

Enhancing Our States

Right now, our State Machine is pretty basic. In a sophisticated game, you might want more complex behavior in each state and/or additional states such as “attack”, “defend”, “run”, and so on.

To make this, you could simply add more states to the enum, and add corresponding match cases in the _physics_process() function.

enum State {
  IDLE,
  MOVE,
  JUMP,
  ATTACK,
  DEFEND
}

func _physics_process(delta):
  match current_state:
    State.IDLE:
      idle(delta)
    State.MOVE:
      move(delta)
    State.JUMP:
      jump(delta)
    State.ATTACK:
      attack(delta)
    State.DEFEND:
      defend(delta)

You would then define your attack() and defend() functions in a similar way to how we defined our previous three functions.

Multi-State Objects

Apart from the basic states, complex games often have multi-state game objects. These objects can be in multiple states at the same time. Perhaps a character is moving and attacking at the same time.

To create multi-state objects, you can use an array to store all active states instead of a single variable. This can lead to more complex logic, but it can provide the flexibility required for intricate game mechanics.

var current_states = []

func add_state(state : State):
  if state not in current_states:
    current_states.append(state)

func remove_state(state : State):
  if state in current_states:
    current_states.erase(state)

In this example, the add_state method allows you to add a new state to the current_states array, and the remove_state allows you to remove a state.

Checking for Active States in Multi-state Objects

With our new array system for handling states, checking for active states requires iterating over the current_states array. Here’s the new _physics_process() function that accomplishes that:

func _physics_process(delta):
  for state in current_states:
    match state:
      State.IDLE:
        idle(delta)
      State.MOVE:
        move(delta)
      State.JUMP:
        jump(delta)
      State.ATTACK:
        attack(delta)
      State.DEFEND:
        defend(delta)

This will run the logic for every active state every physics tick. For instance, if IDLE and ATTACK states are active, the character will perform both idle and attack behaviors.

Handling Inputs with Multi-state Objects

Input handling needs to change too. Instead of transitioning from one state to another, in multi-state objects, we simultaneously add and remove states based on user inputs:

func _input(event):
  if event.is_action_pressed("ui_right"):
    add_state(State.MOVE)
  elif event.is_action_released("ui_right"):
    remove_state(State.MOVE)
  if event.is_action_pressed("ui_up"):
    add_state(State.JUMP)
  elif event.is_action_released("ui_up"):
    remove_state(State.JUMP)

With this, the MOVE state will be active as long as the right key is pressed. Similarly, the JUMP state will be active as long as the up key is pressed.

Fine-tuning State Behaviors

As you can see, the behavior of states can overlap. If the character is in both the MOVE and ATTACK states, both the move() and attack() functions can be run in the same physics tick. This might not be the desired behavior. Perhaps you want the character to stop moving when attacking.

To control this, you could add some conditions to your state functions. Here is an example:

func move(delta):
  if State.ATTACK in current_states:
    return
  # Rest of the move code

func attack(delta):
  # Attack code

With the addition of “if State.ATTACK in current_states:” in the move function, the character will stop moving when ATTACK state is active, even if MOVE state is also active. You can add similar checks for other states and functions, as per your game logic.

Combining Single and Multi-state Behavior

If a game object mostly stays in a single state but occasionally goes into multiple states, you can combine the single state and multi-state behaviors. In this case, the current_state variable and current_states array could coexist.

Here’s how you could define this:

enum State {
  IDLE,
  MOVE,
  JUMP,
  ATTACK,
  DEFEND
}

var current_state : State = State.IDLE
var current_states = [current_state]

func _physics_process(delta):
  match current_state:
    State.IDLE:
      idle(delta)
    State.MOVE:
      move(delta)
    State.JUMP:
      jump(delta)
    State.ATTACK:
      attack(delta)
    State.DEFEND:
      defend(delta)
  for state in current_states:
    additional_behavior(delta, state)

In this code, the _physics_process() function still operates based on the current_state, but after that, it also goes through any additional states in the current_states array, where each state triggers additional behavior.
The additional_behavior() function could be defined as per your game logic requirements.

Keeping the Learning Journey Onward

We hope this guide has ignited your interest in game development, particularly using GDScript and the Godot game engine. The world of game development is vast and rich with endless possibilities, and your journey of exploration and learning has only just begun.

For those who want to delve deeper and enhance your skills further, we highly recommend our Godot Game Development Mini-Degree. This comprehensive collection of courses covers building cross-platform games with Godot. From using assets, GDScript, and control flows to creating various game mechanics, these courses offer a well-rounded curriculum. Learn at your own pace and build a portfolio to jumpstart your career in the gaming industry.

Consider also exploring our broader collection of Godot courses, found here. With Zenva, you can navigate the path from beginner to professional with ease, enhancing your career and opening doors to incredible opportunities. Let us be your guide in your journey of mastering game development!

Conclusion

Understanding state machines and how to implement them in GDScript can open a new door in your game development journey. It is an important tool that allows you to bring your game ideas to life, creating interactive experiences that behave the way you want them to.

While articles like this one are a great starting point, as with all skills, practising is key. And there’s no better way to learn than by doing. Get started on your GDScript game development journey with us – Zenva, and our comprehensive Godot Game Development Mini-Degree. Together, let’s bring your game to life!

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.