Learn Godot 4 by Making a 2D Platformer — Part 21: Instructions & Custom Cursor

christine - Aug 9 '23 - - Dev Community

*You can find the links to the previous parts at the bottom of this tutorial.

When a new player starts our game, they have no way of knowing how the game works. What does each pickup item do? How do they climb ladders? How long does an attack boost last? We need to fix our player’s problems by giving them an Instructions screen that will give them a general overview of the game when they first run the game in the first level. We’ll also hide our cursor when the game is running, and only when the game is paused will we show our custom cursor.


WHAT YOU WILL LEARN IN THIS PART:

  • How to work with Popup nodes.
  • How to show/hide and create custom cursors.

STEP 1: INSTRUCTIONS UI

In your Player scene, add a new Popup node. This will create a popup, and it isn’t necessarily more efficient than a CanvasLayer node, but I wanted to make our Instructions menu a bit different than the other menus!

Godot 2D Platformer

Rename this Popup to “Instructions”, and add a ColorRect node to it. Rename the ColorRect node to “Menu”.

Godot 2D Platformer

Add another ColorRect to your Menu node called “Container”.

Then add the following nodes to it:

  • Label

  • Button

  • ColorRect1

  • Sprite2D

  • — Label

  • ColorRect2

  • — Sprite2D

  • — Label

  • ColorRect3

  • Sprite2D

  • — Label

  • ColorRect4

  • Sprite2D

  • — Label

  • ColorRect5

  • — Sprite2D

  • — Label

  • ColorRect6

  • — Sprite2D

  • — Label

  • ColorRect7

  • — Sprite2D

  • — Label

Rename them as indicated in the image below:

Godot 2D Platformer

Connect your AcceptButton’s pressed() signal to your Player script.

Godot 2D Platformer

Also, change your Instructions node’s Processing Mode to “When Paused”. We will pause the game when this popup is visible, and thus we need to have it processed if the game is paused so that we can press the accept button.

Godot 2D Platformer

  • Instructions Node

Select your node and changed its size to (x: 1152, y: 648).

Godot 2D Platformer

  • Instructions/Menu Node

Select your node and change its color to #101a59. Change its anchor preset to full-rect to make it span over the entire window.

Godot 2D Platformer

  • Instructions/Menu/Container Node

Select your node and change its color to #153084. Change its size to (x: 1000, y: 500) and its position to (x: 80, y: 80).

Godot 2D Platformer

  • Instructions/Menu/Container/AcceptButton Node

Select your node and change its text to “x”. Its font should be “QuinqueFive”, and the size of the font should be 15. Change its size to (x: 50, y: 50) and its position to (x: 950, y: 0).

Godot 2D Platformer

  • Instructions/Menu/Container/Label Node

Select your node and change its text to “Game Instructions”. Its font should be “QuinqueFive”, and the size of the font should be 25. Change its size to (x: 540, y: 33) and its position to (x: 230, y: 20).

Godot 2D Platformer

  • Instructions/Menu/Container/Inputs/Label Node

Select your node and change its text to:

- Move the king left & right using your ← → keys
- Climb ladders by pressing the ↑ key
- Jump by pressing the “SPACE” key
- Pause by pressing the “ESC” key It

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 88). Also change its font color to #b5b5b5, and its line spacing (found under Fonts > Constants) to 20.

Godot 2D Platformer

  • Instructions/Menu/Container/Lives/Sprite2D Node

Select your node and change its texture to “res://Assets/heart/heart/sprite_4.png”. Change its position to (x: 50, y: 250).

Godot 2D Platformer

  • Instructions/Menu/Container/Lives/Label Node

Select your node and change its text to:

- Will restore 1 of your 3 lives

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 240). Also, change its font color to #b5b5b5.

Godot 2D Platformer

· Instructions/Menu/Container/Boosts/Sprite2D Node

Select your node and change its texture to “res://Assets/lightning bolt/lightning bolt/sprite_4.png”. Change its position to (x: 45, y: 290).

Godot 2D Platformer

  • Instructions/Menu/Container/Boosts/Label Node

Select your node and change its text to:

- Enables “SHIFT” to destroy obstacles

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 280). Also, change its font color to #b5b5b5.

Godot 2D Platformer

· Instructions/Menu/Container/Score/Sprite2D Node

Select your node and change its texture to “res://Assets/star/star/sprite_04.png”. Change its position to (x: 50, y: 325).

Godot 2D Platformer

· Instructions/Menu/Container/Score/Label Node

Select your node and change its text to:

- Will give you 1000 score points

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 320). Also, change its font color to #b5b5b5.

Godot 2D Platformer

  • Instructions/Menu/Container/Time/Sprite2D Node

Select your node and change its texture to “res://Assets/clock.png”. Change its position to (x: 45, y: 373). Scale it down to a value such as “0.067”.

Godot 2D Platformer

  • Instructions/Menu/Container/Time/Label Node

Select your node and change its text to:

- How long you’ve been in the current level

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 360). Also, change its font color to #b5b5b5.

Godot 2D Platformer

  • Instructions/Menu/Container/Box/Sprite2D Node

Select your node and change its texture to “res://Assets/Kings and Pigs/Sprites/08-Box/Idle.png”. Change its position to (x: 50, y: 410). Scale it up to a value such as “1.2”.

Godot 2D Platformer

  • Instructions/Menu/Container/Box/Label Node

Select your node and change its text to:

- Jump over obstacles to avoid losing 10 points

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 400). Also change its font color to #b5b5b5.

Godot 2D Platformer

  • Instructions/Menu/Container/Bomb/Sprite2D Node

Select your node and change its texture to “res://Assets/Kings and Pigs/Sprites/09-Bomb/Bomb Off.png”. Change its position to (x: 50, y: 445). Scale it up to a value such as “1.5”.

Godot 2D Platformer

  • Instructions/Menu/Container/Bomb/Label Node

Select your node and change its text to:

- Jump over obstacles to avoid losing 100 points

Its font should be “QuinqueFive”, and the size of the font should be 15. Change its position to (x: 90, y: 440). Also, change its font color to #b5b5b5.

Godot 2D Platformer

Your final Instructions UI should look like this:

Godot 2D Platformer

Now, change the visibility of your Instructions node to be hidden.

Godot 2D Platformer

STEP 2: INSTRUCTIONS VISIBILITY

In your Player script, let’s make our popup visible in the ready() function. We only want to show our instructions if our level number is “1”. We can access this value from our Global script’s get_current_level_number() function.

To make a popup node visible, we can simply use its show() method. We’ll also pause the game to stop our spawners from spawning objects. We also need to stop our game from running our process(delta) function when the game starts on the first level because otherwise, our timer will run in the background even if the game is paused. We can do this via the set_process() method.

    ### Player.gd
    #older code
    func _ready():
        #older code

        #show instructions
        if Global.get_current_level_number() == 1:
            #stop processing
            set_process(false)
            $Instructions.show()
Enter fullscreen mode Exit fullscreen mode

Please note that popup nodes close by default if you press the ESC key or if they click outside of the popup box. If you don’t like this, you can simply change the Instruction node’s type to be a CanvasLayer, and then instead of .show() use $Instructions.visible = true, and .hide() should change to $Instructions.visible = false.

Then, in our _on_accept_button_pressed() function created from our AcceptButton, we will unpause the game and hide our popup. To make a popup node invisible, we can simply use its hide() method. We also need to enable processing if our game is started.

    ### Player.gd
    #older code
    #hide popup
    func _on_accept_button_pressed():
        #hide popup
        $Instructions.hide()
        #unpause game
        get_tree().paused = false
        set_process(true)
Enter fullscreen mode Exit fullscreen mode

We need to go back to our MainMenu scene and change the MainMenu root node’s processing value to “Inherit”. We don’t want this node to process any other children (besides the Menu) if the game is not in an unpaused state explicitly.

Godot 2D Platformer

We want the Menu node inside of our MainMenu scene’s processing mode to be “When Paused”. This will ensure that we can press our buttons whilst keeping our game paused when it first loads.

Godot 2D Platformer

We also need to pause the game by default when the game runs.

    ### MainMenu.gd

    extends CanvasLayer

    func _ready():
        #pause it by default 
        get_tree().paused = true
Enter fullscreen mode Exit fullscreen mode

Since we want the game to be paused when we run our scene if it is the first level, we need to disable our load and new game functions from unpausing the game if our player is in the first level. We need the game to be paused when showing the Instructions popup — otherwise, it will continue processing even if the popup is visible because our buttons unpaused our game in the background.

    ### MainMenu.gd

    #older code

    #starts a new game in the Main scene, which is our 1st level
    func _on_button_new_pressed():
        # Get the current scene
        var current_scene = get_tree().current_scene
        # Free the current scene if it exists
        if current_scene:
            current_scene.queue_free()
        # Load the new scene
        var new_scene = load("res://Scenes/Main.tscn").instantiate()
        # Add the new scene as a child of the root node
        get_tree().root.add_child(new_scene)
        # Set the new scene as the current scene
        get_tree().set_current_scene(new_scene)
        # Update the global variable with the name of the new scene
        Global.current_scene_name = new_scene.name
        # Ensures scene isn't paused if it isn'the first level
        if Global.get_current_level_number() > 1:
            get_tree().paused = false 

    #load game
    func _on_button_load_pressed():
        # Get the current scene (MainMenu in this case)
        var current_scene = get_tree().current_scene
        # Free the current scene if it exists
        if current_scene:
            current_scene.queue_free()
        #load game
        Global.load_game()
        # Ensures scene isn't paused if it isn'the first level
        if Global.get_current_level_number() > 1:
            get_tree().paused = false
Enter fullscreen mode Exit fullscreen mode

Now if you run your scene and you start a new game, your Instructions menu should show, but if you progress to the next level, your menu should not be displayed again.

IMPORTANT NOTE:

If you test your game, you will notice that the timer starts even before you close the Instructions menu. We do fix this issue in Part 24.

STEP 3: CUSTOM CURSOR

Currently, our cursor shows when we run our game. I don’t want the cursor to show if our pause or menu screens aren’t showing — since it breaks the simulation!

Download this cursor pack and drag it into your Assets folder.

Godot 2D Platformer

Now, in your Project Settings underneath Display > Mouse Cursor, we can change our cursor icon to any of the icons from our asset pack!

Godot 2D Platformer

I’m going to assign the “res://Assets/Free Basic Cursor Pack/Win95Def.png” as my cursor image.

Godot 2D Platformer

Close your Project Settings window, and in your Player script, let’s set our cursor to be hidden when the game is not paused, and visible when our game is paused. This should show the cursor when we are in our menu screens, and hide the cursor when we are running around our level. To change our cursor’s visibility, we can change our mouse states. You can set the cursor state using Input.set_mouse_mode().

There are four possible mouse modes:

  • MOUSE_MODE_VISIBLE: The mouse is visible and can move freely into and out of the window. This is the default state.

  • MOUSE_MODE_HIDDEN: The mouse cursor is invisible, but the mouse can still move outside the window.

  • MOUSE_MODE_CAPTURED: The mouse cursor is hidden, and the mouse is unable to leave the game window.

  • MOUSE_MODE_CONFINED: The mouse is visible, but cannot leave the game window.

In our Player’s process() function, let’s show our cursor if our game is paused, and hide it if our game is unpaused. We’ll also move our update_time_label() function into our conditional that runs when the game is unpaused.

    ### Player.gd

    #older code

    func _process(delta):
        #older code

        if get_tree().paused == true:
            #show cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) 
        elif get_tree().paused == false:
            #hide cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)   
            # Update the time label
            update_time_label()
Enter fullscreen mode Exit fullscreen mode

We also need to make the mouse mode visible when the ui_pause input is captured, because sometimes it does not show!

    ### Player.gd

    #older code

    #singular input captures
    func _input(event):
        #pause game
        if event.is_action_pressed("ui_pause"):
            #show menu
            $PauseMenu.visible = true
            #pause scene
            get_tree().paused = true
            #show cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
Enter fullscreen mode Exit fullscreen mode

We also need to show the cursor when we start a new game in our MainMenu script. And for extra security, we will also show the cursor when our MainMenu screen loads.

    ### MainMenu.gd

    extends CanvasLayer

    func _ready():
        #pause it by default 
        get_tree().paused = true
        #show cursor
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) 

    #starts a new game in the Main scene, which is our 1st level
    func _on_button_new_pressed():
        # Get the current scene
        var current_scene = get_tree().current_scene
        # Free the current scene if it exists
        if current_scene:
            current_scene.queue_free()
        # Load the new scene
        var new_scene = load("res://Scenes/Main.tscn").instantiate()
        # Add the new scene as a child of the root node
        get_tree().root.add_child(new_scene)
        # Set the new scene as the current scene
        get_tree().set_current_scene(new_scene)
        # Update the global variable with the name of the new scene
        Global.current_scene_name = new_scene.name
        # Ensures scene isn't paused if it isn'the first level
        if Global.get_current_level_number() > 1:
            get_tree().paused = false
            #show cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
Enter fullscreen mode Exit fullscreen mode

We’ll also need to show our cursor in our LevelDoor scene when the player enters its collision body.

    ### LevelDoor.gd

    #older code

    func _on_body_entered(body):
        if body.name == "Player":
            # pause game
            get_tree().paused = true
            # show menu
            $UI/Menu.visible = true
            # make modular value visible
            $AnimationPlayer.play("ui_visibility")  
            #hide the player's UI 
            body.get_node("UI").visible = false 
            #get final values
            body.final_score_time_and_rating()
            # show player values
            $UI/Menu/Container/TimeCompleted/Value.text = str(Global.final_time)
            $UI/Menu/Container/Score/Value.text = str(Global.final_score)
            $UI/Menu/Container/Ranking/Value.text = str(Global.final_rating)
            #show cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
Enter fullscreen mode Exit fullscreen mode

We also need to fix its resume() button to only unpause the scene on load if the player is not in the first level.

    ### LevelDoor.gd

    #older code

    func _on_restart_button_pressed():
        if Global.get_current_level_number() > 1:
            #unpause scene
            get_tree().paused = false
        #hide menu
        $UI/Menu.visible = false
        # Restart current scene
        get_tree().reload_current_scene()
Enter fullscreen mode Exit fullscreen mode

Finally, we’ll need to show our cursor in our game over the screen. Since its processing mode was set to always, it won’t show the cursor. We’ll also have to update its restart button to only unpause the game if our player is not in the first level — since we want to show them the Instructions screen again.

    ### Player.gd

    #older code

    #reset our animation variables
    func _on_animated_sprite_2d_animation_finished():
        if attack_time_left <= 0:
            Global.is_attacking = false
        set_physics_process(true)
        is_hurt = false

        if $AnimatedSprite2D.animation == "death":
            # pause game
            get_tree().paused = true
            # show menu
            $GameOver/Menu.visible = true
            # make modular value visible
            $AnimationPlayer.play("ui_visibility")  
            #hide the player's UI 
            $UI.visible = false
            #get final values
            final_score_time_and_rating()
            # show player values
            $GameOver/Menu/Container/TimeCompleted/Value.text = str(Global.final_time)
            $GameOver/Menu/Container/Score/Value.text = str(Global.final_score)
            $GameOver/Menu/Container/Ranking/Value.text = str(Global.final_rating)
            #show cursor
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) 

    #older code         

    #restarts game
    func _on_restart_button_pressed():
        #hide menu
        $GameOver/Menu.visible = false
        # Restart current scene
        get_tree().reload_current_scene()
        if Global.get_current_level_number() > 1:
            #unpause scene
            get_tree().paused = false
Enter fullscreen mode Exit fullscreen mode

Your code should look like this.

Now if you run your scene, your menu should show and you should be able to see your cursor when the game is paused, and it should be hidden if the game is running.

Godot 2D Platformer

Godot 2D Platformer

Godot 2D Platformer

Congratulations on adding your instructions menu with your cursor! You finally made it through all of the UI creation parts for our game — which for me, is the most tedious thing in game dev! In the next part, we’ll be adding music and SFX to our game. Now would be a good time to save your project and make a backup of your project so that you can revert to this part if any game-breaking errors occur. Go back and revise what you’ve learned before you continue with the series, and once you’re ready, I’ll see you in the next part!


Next Part to the Tutorial Series

The tutorial series has 24 chapters. I’ll be posting all of the chapters in sectional daily parts over the next couple of weeks. You can find the updated list of the tutorial links for all 24 parts of this series on my GitBook. If you don’t see a link added to a part yet, then that means that it hasn’t been posted yet. Also, if there are any future updates to the series, my GitBook would be the place where you can keep up-to-date with everything!

Godot 2D Platformer


Support the Series & Gain Early Access!

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!

The booklet gives you lifelong access to the full, offline version of the “Learn Godot 4 by Making a 2D Platformer” PDF booklet. This is a 451-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!

Godot 2D Platformer

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .