*You can find the links to the previous parts at the bottom of this tutorial.
Now that we have our abilities set up for our players to lose health, we need to give them the ability to regain a health point if they run through a life pickup.
Our pickup system will consist of three types:
health (lives) — which will give our player 1+ lives.
score — which will give our player 1000 extra points.
attack boost — which will temporarily allow our player to destroy boxes and bombs.
In this part, we’ll be adding the functionality for us to regain a health point via a health pickup. We’ll also create a pickup system that can dynamically change according to our editor value selected, and we’ll set up our HUD to display our Health values.
WHAT YOU WILL LEARN IN THIS PART:
- How to connect signals to callables.
- How to work with CanvasLayer children.
STEP 1: PICKUP SCENE CREATION
In your project, let’s create a new scene with an Area2D node as its root. We’re using this because we want to access our scene’s body_entered() and body_exited() signals to notify our player body that their values are changing.
Add a CollisionShape2D node to your scene and make it of type CircleShape2D.
Rename your Area2D node to Pickup, and save your scene underneath your Scenes folder.
Our Pickup scene will be static, meaning it will not move or be animated. Therefore we can use a simple Sprite2D node to represent our Pickup texture. If our pickup type (which we’ll add later) is of type health, the Sprite2D node will be a heart, and if it’s of type attack boost, the Sprite2D node will be a lightning bolt, etc.
You’ll see how this works in a minute. For now, let’s assign our “res://Assets/heart/heart/sprite_4.png” sprite to our Textures property in our Sprite2D node’s Inspector panel. Move your Sprite2D node to be centered inside of your CollisionShape.
Attach a new script to your scene and save it underneath your Scripts folder.
Connect your root node’s (Area2D) body_entered() signal to your script. We’ll use this to delete our scene from our level scene tree if the Player’s body enters our pickup’s collision.
### Pickup.gd
extends Area2D
#removes the pickup from the game scene tree
func _on_body_entered(body):
if body.name == "Player":
get_tree().queue_delete(self)
In our script, let’s create a Pickups enum that will hold all the pickup types for our game. We’ll export this enum so that we can change our pickup type from the Inspector panel. This allows us to instance our Pickups scene multiple times in our level scene, whilst dynamically changing our type of pickup in the editor. By doing this we can use one scene for all of our pickups, instead of creating a new pickups scene for each Pickup type (Health.tscn, Score.tscn, Attack.tcsn).
### Pickup.gd
#pickups enum
enum Pickups {HEALTH, SCORE, ATTACK}
@export var pickup : Pickups
Now if you look in your Inspector panel, you can change your Pickup type to the values defined in your enum. This won’t do anything yet.
Let’s create some changes to our scene by changing the icon or texture of our Pickup if we change its type in the Inspector panel because not all of our pickup items can look like hearts! To do this we will make use of something new so that we can see real-time changes to these textures in our editor. We will use @tool, which is a powerful line of code that, when added at the top of your script, makes it execute in the editor. You can also decide which parts of the script execute in the editor, which in-game, and which in both.
You need to add @tool to the top of your script before your node class extends.
### Pickup.gd
#When you add at the top of a script the tool keyword, it will be executed not only during the game, but also in the editor.
@tool
extends Area2D
#pickups enum
enum Pickups {HEALTH, SCORE, ATTACK}
@export var pickup : Pickups
Let’s create three new variables which will hold the textures for our Pickup item type.
The textures paths are as follows:
health — res://Assets/heart/heart/sprite_4.png
score — res://Assets/star/star/sprite_04.png
attack boost — res://Assets/lightning bolt/lightning bolt/sprite_4.png
### Pickup.gd
#When you add at the top of a script the tool keyword, it will be executed not only during the game, but also in the editor.
@tool
#older code
#texture assets for our pickup
var health_texture = preload("res://Assets/heart/heart/sprite_0.png")
var score_texture = preload("res://Assets/star/star/sprite_04.png")
var attack_boost_texture = preload("res://Assets/lightning bolt/lightning bolt/sprite_0.png")
We also need to create a reference to our Sprite2D node so that we can change its texture property. We’ll annotate this variable with the @onready object which allows us to initialize the node’s path in the variable definition directly, instead of doing it in our ready() function.
#with @onready:
@onready var my_label = get_node("MyLabel")
#without @onready:
var my_label
### Pickup.gd
#older code
#texture assets for our pickup
var health_texture = preload("res://Assets/heart/heart/sprite_0.png")
var score_texture = preload("res://Assets/star/star/sprite_04.png")
var attack_boost_texture = preload("res://Assets/lightning bolt/lightning bolt/sprite_0.png")
#reference to our Sprite2D texture
@onready var pickup_texture = get_node("Sprite2D")
Since we want to constantly check whether or not our sprite textures are changing when we choose a new Pickup type from our Inspector panel, we want to add the conditional check to change these textures in our process() function. We want to execute our conditional in the editor to see our texture changes in the editor without having to run the game. To do this we will use our @tool functionality, which requires the Engine.is_editor_hint() function to run the code in the editor.
### Pickup.gd
#older code
#allow us to change the sprite texture in the editor
func _process(_delta):
if Engine.is_editor_hint():
if pickup == Pickups.HEALTH:
pickup_texture.set_texture(health_texture)
elif pickup == Pickups.SCORE:
pickup_texture.set_texture(score_texture)
elif pickup == Pickups.ATTACK:
pickup_texture.set_texture(attack_boost_texture)
The above code will allow us to see the texture changes of our pickups in the editor when we instance our Pickup scene in our level scenes (Main & Main_2).
If we run our scene, however, our texture will reset back to our default heart texture! This is because we need to also initialize this texture change in our game itself since we previously only initialized this texture change to show in our editor. We’ll do this in our ready() function so that our texture can change when the Pickup scene enters the scene.
### Pickup.gd
#older code
#changes pickup texture in game screen
func _ready():
if pickup == Pickups.HEALTH:
pickup_texture.set_texture(health_texture)
elif pickup == Pickups.SCORE:
pickup_texture.set_texture(score_texture)
elif pickup == Pickups.ATTACK:
pickup_texture.set_texture(attack_boost_texture)
Now if you run your scene your pickups texture should change according to the value you assigned to the instanced scene in the Inspector panel.
STEP 2: HEALTH PICKUPS FUNCTIONALITY
We want our player to be able to run through our pickups and depending on the Pickup type selected from the enum in our Inspector panel, we want to do something. When our player runs through our health pickup type, we want to increase our lives value if our lives aren’t exceeding our max lives (3 lives) value already. To do this we’ll need to create a new function in our Player scene which we can then call in our Pickup scene’s body_entered() function.
Since we’ll need to redefine our Pickups enum in our Player script, we can simplify our process by moving our Pickups enum from our Pickup script to our Global script. This way we can change/access our Pickups enum from a centralized space, instead of re-defining our variables whenever we need to access our enum.
### Global.gd
#older variables
#pickups
enum Pickups {HEALTH, SCORE, ATTACK}
Now change your Pickups values in your Pickup.gd script as follows:
### Pickup.gd
#When you add at the top of a script the tool keyword, it will be executed not only during the game, but also in the editor.
@tool
extends Area2D
#pickups enum
@export var pickup : Global.Pickups
#texture assets for our pickup
var health_texture = preload("res://Assets/heart/heart/sprite_0.png")
var score_texture = preload("res://Assets/star/star/sprite_04.png")
var attack_boost_texture = preload("res://Assets/lightning bolt/lightning bolt/sprite_0.png")
#reference to our Sprite2D texture
@onready var pickup_texture = get_node("Sprite2D")
#allow us to change the sprite texture in the editor
func _process(_delta):
if Engine.is_editor_hint():
if pickup == Global.Pickups.HEALTH:
pickup_texture.set_texture(health_texture)
elif pickup == Global.Pickups.SCORE:
pickup_texture.set_texture(score_texture)
elif pickup == Global.Pickups.ATTACK:
pickup_texture.set_texture(attack_boost_texture)
#changes pickup texture in game screen
func _ready():
if pickup == Global.Pickups.HEALTH:
pickup_texture.set_texture(health_texture)
elif pickup == Global.Pickups.SCORE:
pickup_texture.set_texture(score_texture)
elif pickup == Global.Pickups.ATTACK:
pickup_texture.set_texture(attack_boost_texture)
#removes the pickup from the game scene tree
func _on_body_entered(body):
if body.name == "Player":
get_tree().queue_delete(self)
In your Player.gd script, we can now go ahead and create our new function that will add the pickups to our player’s “inventory”. We’ll add to this function throughout the next few parts to come.
### Player.gd
#older code
#adds pickups to our player and updates our lives/attack boosts
func add_pickup(pickup):
#increases life count if we don't have 3 lives already
if pickup == Global.Pickups.HEALTH:
if lives < max_lives:
lives += 1
update_lives.emit(lives, max_lives)
print(lives)
#temporarily allows us to destroy boxes/bombs
if pickup == Global.Pickups.ATTACK:
pass
#increases our player's score
if pickup == Global.Pickups.SCORE:
pass
We need to call this function in our Pickup script so that it can execute when our Player runs through our Pickup item.
### Pickup.gd
#older code
#removes the pickup from the game scene tree
func _on_body_entered(body):
if body.name == "Player":
get_tree().queue_delete(self)
#adds pickup to player to change player stats
body.add_pickup(pickup)
If we temporarily changed our player’s life value to equal “2” (var lives = 2), and we run through our Health Pickup, the add_pickup() function should execute and our life value should update back to “3”. The pickup item should also be removed from the scene.
STEP 3: HEALTH PICKUP UI
Currently, we are printing the value of our players’ lives in the Debug console. We want to move this result out of our debug console and into a HUD value. In Godot, you can create your game UI inside of a CanvasLayer node. This allows you to draw canvas items such as labels, shapes, or controls directly onto your screen.
In your Player scene, add a new CanvasLayer node.
Rename this node to “UI” and add a ColorRect node to it. This will allow us to draw a rectangle filled with a solid color.
Rename this node to “Health” and add a Sprite2D node and a Label node to it. The Health rectangle will contain the UI elements for our Health stats. The Sprite2D node inside it will display our Health icon — our heart — and the Label node will display our “lives” value from our Player script in plain text.
Select the Health ColorRect node, and in the Inspector panel, change its Color value to #2c2c2c68. This will draw a dark grey background for our node which has some transparency to it.
Then, underneath Layout > Transform, let’s change its size and position properties. Its size should be (x: 102, y: 40) and its position should be (x: 20, y: 20).
Change the Sprite2D node’s texture to “res://Assets/heart/heart/sprite_4.png”, and change its Transform properties to be (x: 30, y: 17).
Select your Label node, and change its anchor-preset value to “center-right”. This will anchor your node to the right side of your ColorRect node.
Change its text value to “0”.
We also want to give it a custom font.
Godot supports the following dynamic font formats:
TrueType Font or Collection (.ttf, .ttc)
OpenType Font or Collection (.otf, .otc)
Web Open Font Format 1 (.woff)
Navigate to Theme Overrides > Fonts. Select the option to “Quick Load”. This will load the font resource.
In our Assets folder, we have one Font called “QuinqueFive” in both .otf and .ttf format. Select either one, as both formats are a dynamic font type. Dynamic fonts are the most commonly used option, as they can be resized and still look crisp at higher sizes.
We also want to change our font to be a bit smaller. Underneath Fonts > Font Sizes, change the font-size property to a smaller value such as “15”.
Finally, we want our Label to be centered vertically. In the Inspector panel underneath Vertical Alignment, change the value from “Top” to “Center”.
The Health UI should now look like this:
Now to get our Health value to reflect the value of our lives from our code. We could do this in our process() function, which will update our health UI value continuously. In general, it is not recommended to update UI values directly in the process() function. The process() function is called every frame and is typically used for low-level updates and calculations related to physics and rendering. Modifying UI elements in this function can lead to inefficient updates and performance issues.
Instead, it is better to update UI values in a custom function, and then we can connect that custom function to our update_lives signal. Each time our signal is emitted, it will update our UI functions accordingly. At the beginning of this tutorial series, we created a folder called “GUI”. We created this because this folder will contain all the scripts that we have connected to our UI elements. Connect a new script to your Health node in your Player scene, and save it underneath your GUI folder.
In this script, we will create a custom function that will update our Label’s text values each time our signal is emitted.
### Health.gd
extends ColorRect
#ref to our label node
@onready var label = $Label
# updates label text when signal is emitted
func update_lives(lives, max_lives):
label.text = str(lives)
Then, in our Player scripts ready() function we will connect our signal to our Health scripts’ update_lives() function. We also need to set our Label’s value to be the value of our lives variables so that our lives can show properly when we load our game. If we do not do this, it will show “0” when we load the game.
### Player.gd
#older code
func _ready():
current_direction = -1
#updates our UI labels when signals are emitted
update_lives.connect($UI/Health.update_lives)
#show our correct lives value on load
$UI/Health/Label.text = str(lives)
Your code should look like this.
Now if you run your scene, your lives value should show correctly on load, and it should update if you run through a Health pickup. The “lives” value should also decrease if your player gets injured by a box or a bomb. Make sure you set your “lives” value to be lower than “3” when testing this since our Health pickup will only update if our lives are bigger than our max lives. You can change this if you want to add a life even if it is over the max lives.
Congratulations on setting up the base for your Health System. We’ll add a death screen later on if our player’s health falls below 1, but first, we need to complete our pickups functionality. In the next part, we’ll be setting up our attack system. This attack system will be enabled for a few seconds if we run through an attack boost Pickup. This will allow us to destroy boxes and bombs that are in our path without being damaged.
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.