DEV Community

Cover image for ## Let’s Learn Godot 4 by Making a Procedurally Generated Maze Game — Part 2: Player Setup & Movement💣
christine
christine

Posted on

## Let’s Learn Godot 4 by Making a Procedurally Generated Maze Game — Part 2: Player Setup & Movement💣

The first entity scene that we will create for our game is our Player. Entity scenes in our game refer to all the scenes that interact with the game and respond to player input or other entities — such as our bombs, enemies, players, and boosts. All of our entity scenes will be instanced by our Level scene, which will handle the spawning of our entities and our map generation. All of our entity scenes, and our Level scene, will be instanced by our Main scene, which is the main container for our game.


What is a Scene and a Node?
A scene is a container that holds a collection of nodes arranged in a hierarchical structure — such as our Player or Enemy. A node is the basic unit within a scene that makes up the scene — such as a Sprite or Camera within our scene.


Figure 5: Overview of a Node & a Scene.

Overview of a Node & a Scene.

Figure 6: Overview of our game’s scene hierarchy and logic.

Overview of our game’s scene hierarchy and logic.

PLAYER OVERVIEW

We will create the Main and Level scenes in the next few parts when we create our procedurally generated map, but for now, we will make our player who will move around the map and perform activities related to destroying tiles and enemies. Our player will also spawn with a random color on each load, so each time you start a new game, your player might be blue, grey, or orange.

We also will spawn our AI player later on with a random color — and this will help us establish some individuality between our players. Our player entity will have three lives and they will also be able to take damage, deal damage, and pick up boosts.

Figure 7: Overview of our Player entity spawning logic.

Overview of our Player entity spawning logic.

PLAYER ANIMATION SETUP

Open up your project and create a new 2D Scene.

Learn Godot 4

You will see that this creates a Node2D node as its root or parent. Let’s rename this node to be “Player”.

Learn Godot 4


What is a Node2D node?
A Node2D is the base 2D node in Godot that has functionalities for positioning, transformation, and hierarchy. We can use this node to contain or organize other nodes.


We don’t want our Player to have a Node2D node as its root because we need a node that can handle movement and physics, such as our CharacterBody2D node. Right-click your root node, and change its type to CharacterBody2D.

Learn Godot 4

You will see it has a yellow warning icon next to it, and that is because it is missing a CollisionShape2D node, which will allow our player to be blocked or sensed by other node’s collisions. Add a CollisionShape2D node to your root node, and assign it a new CircleShape2D.

Learn Godot 4

Learn Godot 4

To see our player and give it a movement animation, we need to assign it an AnimatedSprite2D node. An AnimatedSprite2D node can contain multiple sprite frames and thus we can organize these frames to play an animation, whereas a Sprite2D node is a static object that contains one singular sprite frame.

Learn Godot 4

This node will also give us a yellow warning, and that’s because we need to assign it with a Sprite Frames resource which will allow us to load, create, edit, and delete animations. Select the AnimatedSprite2D node, and in the Inspector panel assign it with a new Sprite Frames resource.

Learn Godot 4

Now, if you look in your Assets directory underneath “players”, you will see that there are three “char_” folders with movement sprites (images) for three different colored characters. We will use these images to create an *idle, up, down, *and *side *animation for each color — so orange_idle, blue_idle, green_idle, etc.

Learn Godot 4

Let’s start with blue. In your SpriteFrames panel below, add a new animation called “blue_idle”. It is important to prefix the animation state with the color value since we will use this value to play the animations based on the color generated when the player spawns.

Drag the “base.png” image underneath char_01 into your frames panel. We can leave the FPS to 5 and looping value to on.

Learn Godot 4

Do the same for orange_idle and grey_idle.

Learn Godot 4

Learn Godot 4

Then, create a new animation called “blue_down”, and assign the “down_1” to “down_3” sprites underneath char_01 to your animation. Leave the FPS to 5 and looping value to on.

Learn Godot 4

Do the same for orange_down and grey_down.

Learn Godot 4

Learn Godot 4

Create a new animation called “blue_up”, and assign the “up_1” to “up_3” sprites underneath char_01 to your animation. Leave the FPS to 5 and looping value to on.

Learn Godot 4

Do the same for orange_up and grey_up.

Learn Godot 4

Learn Godot 4

Finally, create a new animation called “blue_side”, and assign the “side_1” to “side_3” sprites underneath char_01 to your animation. Leave the FPS to 5 and looping value to on.

Learn Godot 4

Do the same for orange_side and grey_side.

Learn Godot 4

Learn Godot 4

Your final animations list should look like this:

Learn Godot 4

Save your scene as “Player” underneath your Scenes folder.

Learn Godot 4

PLAYER COLOR GENERATION & MOVEMENT

Attach a new script to your Player scene’s root node and save it as “Player.gd” underneath your Scripts folder.

Learn Godot 4

We will also create a new Global Autoload Singleton script which will contain all the variables and methods that will be used throughout our game in multiple scenes. We will use this script throughout our game to store our level variables and signals.


What is a Singleton Script?
A singleton script is a script that is automatically loaded when the game starts and remains accessible throughout the entire lifetime of the game. This makes it ideal for managing game-wide data, functions, and systems that need to be accessed from multiple scenes or scripts.


Underneath your Scripts folder, create a new script called “Global.gd”.

Learn Godot 4

Then, in your Project Settings > Autoload menu, assign the Global script as a new singleton. This will make the variables and functions stored in this script accessible in every scene.

Learn Godot 4

Both our player and our AI use the same sprites, and they will both generate a random color on load, so it would be smart to save our Color array in our Global script. Our sprites have a blue, grey, and orange color, so let’s define an array in our Global script with these color values.

    ### Global.gd
    extends Node
    # Color generation array for player and ai_player
    var color: Array = ["blue", "grey", "orange"]
Enter fullscreen mode Exit fullscreen mode

Now in our Player script, we can use this Global variable to assign our player a color when they load into our Level or Main scene. We will do this in our built-in ready() function by generating a random index within the range of valid indexes for the Global.color array. To generate a random index from 0 to 2 (which is our Global.color array size), we will use the randi() method alongside the modulo operation (%) to ensure that the random integer stays within the array size, effectively randomizing the index.


When to use _ready()?
We use the _ready() function whenever we need to set or initialize code that needs to run right after a node and its children are fully added to the scene. This function will only execute once before any _process() or _physics_process() functions.


    ### Player.gd

    extends CharacterBody2D

    # Player states
    var color: String

    func _ready():
        # Randomly assign it a color on spawn
        color = Global.color[randi() % Global.color.size()]
Enter fullscreen mode Exit fullscreen mode

Now, we can use this color variable to play the appropriate animation for our player. So if the color’s index is 0, it will play the animations for blue_ — and for 1 = grey_ and 2 = _orange. We will create a new function called movement_input() which will handle the player’s animations based on the color and current input of the player.

To make our code more organized, we will use the @onready annotation to create an instance of a reference to our AnimatedSprite2D node. This way we can reuse the variable name instead of saying $AnimatedSprite2D.play() each time we want to change the animation. Take note that we also flip our sprite horizontally when playing the side_ animation. This is so that we can reuse the same animation for both left and right directions.

    ### Player.gd

    extends CharacterBody2D

    # Node References
    @onready var animated_sprite = $AnimatedSprite2D

    # Player states
    var color: String

    func _ready():
        # Randomly assign it a color on spawn
        color = Global.color[randi() % Global.color.size()]

    # Player movement
    func movement_input():
        # -------- Animations by color ------------
        #left anim
        if Input.is_action_pressed("ui_left"):
            animated_sprite.play(color + "_side")
            animated_sprite.flip_h = false
        #right anim
        elif Input.is_action_pressed("ui_right"):
            animated_sprite.flip_h = true
            animated_sprite.play(color + "_side")
        #up anim
        elif Input.is_action_pressed("ui_up"):
            animated_sprite.play(color + "_up")
        #down anim
        elif Input.is_action_pressed("ui_down"):
            animated_sprite.play(color + "_down")
        #idle anim
        else:
            animated_sprite.play(color + "_idle")
            animated_sprite.flip_h = false
Enter fullscreen mode Exit fullscreen mode

To play these animations, we will need to call our newly created movement_input() function in our physics_process() function, which is responsible for our game’s physics engine. This function allows our player to use direction, speed, and velocity inputs to move around the map.


When to use _processing() and _physics_processing()?
Use the _process() function for things that are graphical or need to respond quickly. Use the _physics_process() function for things that are physics-based or need to happen at a consistent, predictable rate.


    ### Player.gd

    #older code

    # Movement Physics
    func _physics_process(delta):
        movement_input()

    # Player movement
    func movement_input():
        # -------- Animations by color ------------
        #left anim
        if Input.is_action_pressed("ui_left"):
            animated_sprite.play(color + "_side")
            animated_sprite.flip_h = false
        #right anim
        elif Input.is_action_pressed("ui_right"):
            animated_sprite.flip_h = true
            animated_sprite.play(color + "_side")
        #up anim
        elif Input.is_action_pressed("ui_up"):
            animated_sprite.play(color + "_up")
        #down anim
        elif Input.is_action_pressed("ui_down"):
            animated_sprite.play(color + "_down")
        #idle anim
        else:
            animated_sprite.play(color + "_idle")
            animated_sprite.flip_h = false
Enter fullscreen mode Exit fullscreen mode

Now if you run your scene by pressing F5 (with your Player scene as your default scene), you will see that your player’s animations play when you press WASD, and the color should randomly generate when you run your scene again.

Learn Godot 4

Learn Godot 4

To get our player to move in different directions, we need to define a new speed variable for our player. I want our player to be quite quick, so I will give it a high value such as 100.

    ### Player.gd

    extends CharacterBody2D

    # Node References
    @onready var animated_sprite = $AnimatedSprite2D

    # Player states
    var color: String
    var speed = 100

    #older code
Enter fullscreen mode Exit fullscreen mode

Then in our movement_input() function, we will define a new variable that will use Godot’s built-in get_vector() method that returns a Vector2 based on the state of the specified input actions. The vector’s x-component will be -1, 0, or 1 depending on whether “ui_left” or “ui_right” is pressed. The vector’s y-component will be -1, 0, or 1 depending on whether “ui_up” or “ui_down” is pressed.


What is a Vector?
Vectors are objects that represent quantities like force, velocity, and position. In 2D games, we use Vectors to determine and calculate the position of entities on the X and Y axes.


Figure 8: Overview of a Vector2 for position determination.

Overview of a Vector2 for position determination.

    ### Player.gd

    #older code

    # Player movement
    func movement_input():
        var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")

        # -------- Animations by color ------------
        #left anim
        if Input.is_action_pressed("ui_left"):
            animated_sprite.play(color + "_side")
            animated_sprite.flip_h = false
        #right anim
        elif Input.is_action_pressed("ui_right"):
            animated_sprite.flip_h = true
            animated_sprite.play(color + "_side")
        #up anim
        elif Input.is_action_pressed("ui_up"):
            animated_sprite.play(color + "_up")
        #down anim
        elif Input.is_action_pressed("ui_down"):
            animated_sprite.play(color + "_down")
        #idle anim
        else:
            animated_sprite.play(color + "_idle")
            animated_sprite.flip_h = false
Enter fullscreen mode Exit fullscreen mode

We’ll then take this input_direction vector value and multiply it by our speed to get our player’s velocity. This will allow our player to move in a certain direction at our defined speed.

    ### Player.gd

    #older code

    # Player movement
    func movement_input():
        var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
        velocity = input_direction * speed

        # -------- Animations by color ------------
        #left anim
        if Input.is_action_pressed("ui_left"):
            animated_sprite.play(color + "_side")
            animated_sprite.flip_h = false
        #right anim
        elif Input.is_action_pressed("ui_right"):
            animated_sprite.flip_h = true
            animated_sprite.play(color + "_side")
        #up anim
        elif Input.is_action_pressed("ui_up"):
            animated_sprite.play(color + "_up")
        #down anim
        elif Input.is_action_pressed("ui_down"):
            animated_sprite.play(color + "_down")
        #idle anim
        else:
            animated_sprite.play(color + "_idle")
            animated_sprite.flip_h = false
Enter fullscreen mode Exit fullscreen mode

Now to move our player, we will simply call the move_and_slide() method, which will smoothly move our player across the screen when an input is pressed.


When to use move_and_slide and move_and_collide()?
Use move_and_slide() when you want the character to move in general directions, such as in platformer games. Use move_and_collide() when you want detailed information on your character’s collisions to perform custom actions, such as in puzzle games where characters need to move or dodge obstacles.


    ### Player.gd

    #older code

    # Movement Physics
    func _physics_process(delta):
        movement_input()
        move_and_slide()

    # Player movement
    func movement_input():
        var input_direction = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
        velocity = input_direction * speed

        # -------- Animations by color ------------
        #left anim
        if Input.is_action_pressed("ui_left"):
            animated_sprite.play(color + "_side")
            animated_sprite.flip_h = false
        #right anim
        elif Input.is_action_pressed("ui_right"):
            animated_sprite.flip_h = true
            animated_sprite.play(color + "_side")
        #up anim
        elif Input.is_action_pressed("ui_up"):
            animated_sprite.play(color + "_up")
        #down anim
        elif Input.is_action_pressed("ui_down"):
            animated_sprite.play(color + "_down")
        #idle anim
        else:
            animated_sprite.play(color + "_idle")
            animated_sprite.flip_h = false
Enter fullscreen mode Exit fullscreen mode

Before we wrap up this section and test our code, we need to add one more node to our Player scene tree: a Sprite2D node. This node should be above the player’s head to serve as their indicator. This will help us separate the player from enemies and AI Players.

Add a Sprite2D node to your scene and change its Texture to “Player_Indicator.png”, which can be found underneath Assets/players/char_indicators. We also want to change this node’s scale to 0.05, and its position on the y-axis to -15.

Learn Godot 4

Your final scene tree should look like this:

Learn Godot 4

Now if you run your scene, your player should play the appropriate animations and smoothly move across the screen!

Learn Godot 4

We will make amendments later on to our player, so we are not done yet, but it’s good that we have a Player character that can now spawn with a random color and play its respective animations in each input direction. Now would be a good time to save your project and make a small incremental backup of your code.

The final source code for this part can be found here.


Unlock the Series!

If you like this series and would like to support me, you could donate any amount to my KoFi shop or you could purchase the offline PDF that has the entire series in one on-the-go booklet!

This PDF gives you lifelong access to the full, offline version of the “Learn Godot 4 by Making a Procedurally Generated Maze Game” series. This is a 387-page document that contains all the tutorials of this series in a sequenced format, plus you get dedicated help from me if you ever get stuck or need advice. This means you don’t have to wait for me to release the next part of the tutorial series on Dev.to or Medium. You can just move on and continue the tutorial at your own pace — anytime and anywhere!

Learn Godot 4

This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.

Top comments (0)