*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!
Rename this Popup to “Instructions”, and add a ColorRect node to it. Rename the ColorRect node to “Menu”.
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:
Connect your AcceptButton’s pressed() signal to your Player script.
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.
- Instructions Node
Select your node and changed its size to (x: 1152, y: 648).
- 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.
- 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).
- 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).
- 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).
- 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.
- 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).
- 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.
· 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).
- 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.
· 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).
· 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.
- 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”.
- 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.
- 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”.
- 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.
- 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”.
- 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.
Your final Instructions UI should look like this:
Now, change the visibility of your Instructions node to be hidden.
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()
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)
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.
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.
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
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
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.
Now, in your Project Settings underneath Display > Mouse Cursor, we can change our cursor icon to any of the icons from our asset pack!
I’m going to assign the “res://Assets/Free Basic Cursor Pack/Win95Def.png” as my cursor image.
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()
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)
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)
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)
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()
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
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.
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!
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!
This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.