Introduction to GDScript in Godot 4 Part 2

In this second part of the GDScript introduction, you’ll learn about state machines, adding and removing nodes and how to make a camera follow a node. By Eric Van de Kerckhove.

Leave a rating/review
Download materials
Save for later
Share

Before starting with this part of the tutorial, make sure you finished the first part as this part will build upon the fundamentals laid down there.
In this second part of the tutorial, you’re going to learn about the following:

  • The basics of state machines.
  • Adding and removing nodes with code.
  • Creating a camera that follows a node.
  • Using UI elements to inform the player.

Without further ado, it’s time to get back to scripting!

Getting Started

Download the projects by clicking Download Materials at the top or bottom of the tutorial. Next, unzip the file and import the starter folder into Godot. Alternatively, you can continue with the project you used in the previous part.

At the end of the previous part of this tutorial, gravity was constantly being applied on the player avatar, allowing it to jump and fall back down. This had the side effect of the avatar never being able to stand on the ground below, so you have to continuously jump now.

Robot constantly jumping and falling

Ideally, the avatar should stay on the ground and should only fall down once he’s in the air. One way of keeping track of these states is using a finite state machine.

Finite State Machines

A finite state machine (FSM) is a way to break down a game or application into different states. Take a simple adventure game for instance: you might have a game state for walking around, a state for fighting and a state for when the game menu is open. While the player is exploring, you might hide the UI for full immersion, while in battle you want to show health bars, skills and so on. Meanwhile, you want to pause the game in the background when the menu is open so the player can manage his inventory and change settings.

Programming what should happen in each scenario without a FSM leads to messy code with a lot of if-statements strewn about. It’s also prone to mistakes and harder to debug. With a FSM, you create three states:

  • Walking
  • Fighting
  • Menu

The current state is stored in a variable and depending on the active state, a different code path is followed. You can even add transitions between states where UI elements fade in or out for example.

For this game, there are two possible states: on the ground and in the air. The game starts with the avatar on the ground, where it can do a manual jump and isn’t effected by gravity. Once the avatar has jumped up, it’s in the air and gravity gets applied to it. To add this to the script, start by adding the following line at the top of the script, below var velocity : Vector2 = Vector2.ZERO:

enum AvatarState {ON_GROUND, IN_AIR}

AvatarState isn’t a variable, but an enum, which is a set of constants. Enums are useful as you can access their value by their key, which are ON_GROUND and IN_AIR in this case. Behind the scenes, the values of these keys are integers starting from 0. You can use an enum as a type as you’ll see below.
To keep track of the active state, add this variable declaration right above the enum you added:

var state : AvatarState = AvatarState.ON_GROUND

The state variable uses AvatarState as its type and sets its default value to AvatarState.ON_GROUND. This variable will come in use to keep track of the avatar’s state and react accordingly. To apply the finite state machine concept to the rest of the script, you’ll need to make several changes to its structure. To start with, add these two functions below the _process function:

func _process_on_ground() -> void:
    pass

func _process_in_air(delta) -> void:
    pass

Depending on the state, the corresponding function will be called every frame. This splits up the logic according to the state, which is the basis of every FSM.
Now add this block of code to _process, right below var cursor_right:

match state: # 1
    AvatarState.ON_GROUND: # 2
        _process_on_ground()
    AvatarState.IN_AIR: # 3
        _process_in_air(delta)  

Here’s what this code does:

  1. This match statement reads the value of state and branches the further execution flow depending on its value. This can replace if-statements where the only difference is a single value and results in a cleaner, more readable result. If you’re familiar with other programming languages, the match statement is similar to a switch statement, albeit with some extra features.
  2. In case the value of state is AvatarState.ON_GROUND, call _process_on_ground.
  3. In case the value of state is AvatarState.IN_AIR, call _process_in_air.

Note that the match statement, its branches and the logic for each branch needs its own indentation.
With the finite state machine in place, it’s time to move the process code to their appropriate functions. To start off, move the call to _process_input from _process to _process_on_ground, replacing the pass keyword. This ensures the avatar can’t manually jump if it’s not on the ground. The _process_on_ground function should look like this now:

func _process_on_ground() -> void:
    _process_input()

Next, the gravity should only be applied when the avatar is in the air, so move the line velocity.y += delta * gravity from _process to _process_in_air, replacing the pass keyword. The _process_in_air function now looks like this:

func _process_in_air(delta) -> void:
    velocity.y += delta * gravity

If you run the project now and make the avatar jump, you’ll notice the avatar is back to his space rocket ways as gravity isn’t being applied. This makes sense, as gravity is now only applied in the IN_AIR state, while the avatar never switches to that state. To fix that, add the following line to _jump function:

state = AvatarState.IN_AIR

This changes the state to IN_AIR after a jump, so gravity will start getting applied. Run the project again and try jumping, the avatar will now jump and fall down and… keeps falling down. Hey, it’s progress!

Robot jumps and falls down through ground

As with all the other issues you’ve faced throughout the tutorial, this too can be fixed with some code. The current problem is the avatar has no idea where the ground is, and as a result, has no way to react to falling on the ground. Luckily, it’s easy to figure out where the ground is, as that’s the avatar’s starting position. You just need to save that position somewhere, if only there was some sort of container to store values into. :]

Yes, you need another variable! Add this line to the top of the script, right below var state:

var start_height : float

This will store the Y position of the avatar at the start of its lifetime, its starting height. You don’t have to give it a default value in this case, as its value will be set in the _ready function. Speaking of which, add this line to _ready, replacing the print statement that’s in there:

start_height = global_position.y

This sets start_height to the initial Y position of the avatar. To use this starting height to detect the ground when falling, add the following to the _process_in_air function:

if velocity.y > 0: # 1 (falling)
    if global_position.y >= start_height: # 2
        var _result = get_tree().reload_current_scene() # 3
else:  # 4 (going up)
    pass

For simplicity’s sake, the scene gets reloaded if the avatar hits the ground after falling. This resets the game state without having to reset any variables. Here’s a breakdown of the different parts:

  1. If the avatar’s vertical velocity is positive, that means it’s falling down.
  2. If the Y position of the avatar is equal to the start height or moves past it while falling…
  3. Restart the current scene using get_tree().reload_current_scene(). The get_tree() call returns an instance of SceneTree, which is a node manager class that contains useful methods for working with scenes and nodes. One of those methods is reload_current_scene(), which reloads the active scene and returns a result code. The result code is ignored for this example.
  4. If the avatar is moving up, do nothing for now.

Run the project again, this time the scene will reset once the avatar hits the ground, allowing you to keep “playing” indefinitely. There’s now a game loop, even if it’s not the most exciting one.

Robot jumping up and down

Now is a good time to make the avatar jump up when hitting jumpers, after all, that’s what they’re for!
To do so, you just need to connect the Area2D‘s area_entered signal to the player_avatar script like you did with the jumpers. To recap, select Area2D in the Scene dock, open the Node tab on the right side of the editor and double click the area_entered signal. Finally, click the Connect button to create the new _on_area_2d_area_entered function.
Add this line to the newly created _on_area_2d_area_entered function, replacing its pass keyword:

_jump()

This makes the avatar automatically jump when hitting jumpers. Guess what? It’s time for another test run! Run the project and see if you can hit all three jumpers before hitting the ground again.

Robot jumping on orbs

I hope you agree that this starts to feel like a game now. It might even already fit in with WarioWare‘s microgames. :]