In this tutorial we’ll learn how to create a Graphical User Interface for our RPG, using a few simple nodes. We’ll also see how to update it using Godot’s event system and signals.

Player stats

For now, we will mainly deal with two player stats:

  • Health: represents the player’s health status. It has a maximum value that cannot be exceeded and slowly regenerates over time. Health points are lost when the player is hit by an enemy attack.
  • Mana: it’s the player’s magic power. It has a maximum value that cannot be exceeded. It’s consumed when casting a fireball and it regenerates over time.

To start, let’s add these statistics to the player’s script:

# Player stats
var health = 100
var health_max = 100
var health_regeneration = 1
var mana = 100
var mana_max = 100
var mana_regeneration = 2

The variables health and mana are the current values, while health_max and mana_max are the maximum values that cannot be exceeded. The health_regeneration and mana_regeneration variables are the regeneration rates of the two stats, expressed in recovered points per second.

Health and mana bars

The GUI of a game is used to display information to the player in a quick and easy way. For example, it can show to the player what their health, character’s level, and other stats are using bars and other graphic elements. The way generally used to show health and mana in a RPG GUI is to use bars whose length can vary when points are lost or recovered.

We will create these bars using ColorRect nodes. As the name implies, this node has only one function: to show a colored rectangle on the screen.

Add a ColorRect node as a child of Root and rename it Health.

In the Inspector, click on Color and set the values of Red, Green, Blue and Alpha to 0, 0, 0 and 160 respectively.

Then, open the Rect section and set Position to (3, 158) and Size to (74, 9).

Now, add a child node to Health, also of ColorRect type, and rename it Bar. In the Inspector, set its color to (255, 50, 50, 255) and, in the Rect section, set Position to (1, 1) and Size on (72, 7).

In the workspace, you’ll now see a red bar surrounded by a semi-transparent black border.

Run the game and move to the right. You will see that the bar will remain anchored to the ground and exit the screen!

To solve this problem, you need to create a second canvas to draw the GUI. Add a CanvasLayer node to Root.

Now drag the Health node into CanvasLayer.

Run the game again: this time the bar will remain anchored to the lower left corner of the game window.

To create the mana bar, right-click on the Health node and choose Duplicate.

A duplicate of the Health node will be created, including all its children. Rename the duplicate node as Mana.

Select Mana node and, in the Inspector, change the value of Rect → Position → y to 169. Then, select its child node Bar and change its color to (33, 128, 255, 255). You will now have a second blue bar to view the player’s mana level.

Updating health and mana values

Now that we’ve created the health and mana bars, we need to update their length whenever Player‘s health and mana variables are changed.

We will do this by using a custom signal, emitted from the Player node script when we change one of the player’s stats. We will attach to each nodes a script, that will handle the signal and take care of changing the length of the bars.

Open the Player script and declare the custom player_stats_changed() signal using the keyword signal:

signal player_stats_changed

The first time we want to emit this signal is when the game starts. This way, even if we had changed the length of the bars in the editor to performs some tests, at the start of the game they will be set to the correct initial state.

We will emit the signal inside one of the functions we will use most often in Godot: the _ready() function. As the name says, the _ready() function is called when the node is “ready”, i.e. when both the node and its children have entered the scene tree. This function is usually used for node initialization, so it’s the right place to emit player_stats_changed() to set the bars to their initial length.

To emit a signal, we’ll use the emit_signal() function. Add this code to Player‘s script:

func _ready():
	emit_signal("player_stats_changed", self)

The emit_signal() function is called passing as the first argument the name of the signal we want to emit. In addition to the signal name, it accepts a variable number of parameters which are subsequently passed as parameters to the function that handles the signal.

In our case we set only one parameter: a reference to the Player node, obtained using the self keyword. Bar scripts will use this reference to read Player health and mana variables.

As we said earlier, health and mana regenerate over time. Then, we must add the code that increments these values very frame. Each time they are incremented, the player_stats_changed() signal must be emitted to update the bars.

To execute code continuously during the game, we will use another fundamental Godot function: the _process() function. This function is called at every frame, as fast as possible. The function has a parameter delta, which represents the elapsed time since the previous frame.

For health and mana regeneration, add this code to the script:

func _process(delta):
	# Regenerates mana
	var new_mana = min(mana + mana_regeneration * delta, mana_max)
	if new_mana != mana:
		mana = new_mana
		emit_signal("player_stats_changed", self)

	# Regenerates health
	var new_health = min(health + health_regeneration * delta, health_max)
	if new_health != health:
		health = new_health
		emit_signal("player_stats_changed", self)

At the beginning of the function, we calculate the updated mana value, adding to the current value the mana variation for the current frame (mana_regeneration * delta). The min() function ensures that the maximum health value is never exceeded. If the new value is different from the previous one, the value is updated and the signal to update the GUI is emitted. For the health variable, it works the same way.

The last circumstance in which we want to emit the player_stats_changed() signal is when we cast a fireball. Casting a fireball costs a certain amount of mana, which we’re going to subtract when the fireball input event is detected.

In the _input() method, change the fireball event handling code like this:

elif event.is_action_pressed("fireball"):
	if mana >= 25:
		mana = mana - 25
		emit_signal("player_stats_changed", self)
		attack_playing = true
		var animation = get_animation_direction(last_direction) + "_fireball"

The code checks if there is enough mana to cast a fireball (the cost is 25 points). If so, the points are subtracted and the signal is emitted to update the GUI.

Connecting the signal to scripts

At this point, we only need to create the scripts for the two bars and connect them to the signal emitted by Player.

To keep the project organized, in the FileSystem panel, create a folder named GUI, in which we will put all files related to the user interface. Attach a new script to the Health node and call it Save it in the GUI folder you just created.

If you want, you can delete all the lines of the script except the first one (extends ColorRect).

Now we must connect the player_stats_changed() signal to this script. Select the Player node and go to the Node → Signals panel. Select the player_stats_changed() signal and press Connect.

When the Connect Signal window appears, select the Health node and press Connect again.

Godot will open the Health node script and you’ll see the new function that handle the signal. Replace its code with this:

func _on_Player_player_stats_changed(var player):
	$Bar.rect_size.x = 72 * / player.health_max

As you can see, we added an argument to the function. This argument will contain the reference to Player that we have passed as a parameter when emitting the signal. The function does nothing but set the width of the Bar node by calculating it as the maximum width (72 pixels) multiplied by the player’s health percentage ( / player.health_max).

Repeat the same procedure for the Mana node, creating the script and connecting it to the signal. The code that handles the signal is also very similar:

func _on_Player_player_stats_changed(var player):
	$Bar.rect_size.x = 72 * player.mana / player.mana_max

Run the game and press the fireball button. You will see the mana value drop instantly and then slowly rise again.

If you want to test the health bar, you can temporarily change the health variable in Player’s script to a value lower than health_max. You will see the bar grow as health regenerates (the health bar moves more slowly than the mana bar because health regenerates by 1 point per second instead of 2).

Experience points and player’s level

The second block we must add to the GUI will show us the experience points (XPs) earned by the player and his current level. In this tutorial we will just draw the interface, without going into the details of its working. We will view XPs and levels progression in a future tutorial.

What we want to create is a simple box, where the XP and level values are shown. To show text, we need a font that reflects the pixel art style of the game, so I created one that you can download by clicking the button below.

Download “Schrödinger Font” Schrö – 6 KB

In the FileSystem panel, create a folder named Fonts and drag the font you just downloaded into it. We can’t directly use the font in a GUI node, but instead, we must first create a DynamicFont type resource. To do so, right-click on the Fonts folder and choose New Resource.

Choose DynamicFont and press Create.

Finally, save the new resource as Font.tres.

The properties of the newly created resource will appear in the Inspector (switch to it if you have the Node panel still selected). In the Font section, drag the Schrödinger.ttf file into the Font Data property. In the Settings section, set Size to 10 (at this size, the font is pixel perfect). Finally, save the resource by pressing the disk icon.

Create a rectangle by adding a node of type ColorRect as a child of CanvasLayer and call it XP. In the Inspector, set Color to (0, 0, 0, 160) then, in the Rect section, set Position to (124, 158) and Size to (72, 20).

Now, add four Label nodes as children of XP, and set their Custom Fonts → Font properties to use the Font.tres resource we created earlier. Then, set their parameters as shown in the table:

NameTextAlignRect → PositionRect → Size
LabelXPXP:Left(2, 1)(20, 10)
LabelLevelLVL:Left(2, 10)(20, 10)
ValueXP0/100Right(21, 1)(50, 10)
ValueLevel1Right(21, 10)(50, 10)

This is the final result:


The game we are creating is very simple and therefore there will be only two types of objects that the player can own: health potions and mana potions.

So, our inventory will simply be made up of two boxes, that show us how many potions we have. Next to the quantity, we will place two sprites to identify the type of potion. Even for inventory and potions, for now we’ll only draw the user interface. There will be a specific tutorial explaining how it works.

First of all, download the potions sprite by clicking the button below.

Download “SimpleRPG Potions Sprite” potions.png – 262 B

Import the image you just downloaded into the project, placing it in the GUI folder. Remember to re-import the image disabling the Filter property.

To create the box for health potions, add a ColorRect child node to CanvasLayer and call it HealthPotions. In the Inspector, set its Color property to (0, 0, 0, 160), then, in the Rect section, set Position to (243, 158) and Size to (36, 20).

Now, add a Sprite node to HealthPotions. In the Inspector, drag the potions.png file into the Texture property then, in the Region section, turn on Enabled and set the region Rect‘s w and h to 16. Finally, in the Transform section, set Position to (10, 10).

To show the amount of potions the player owns, add a Label node to HealthPotions. In the Inspector, in the Custom Fonts section, set Font to Font.tres. Then, set Text to 0, Align to Right and, in the Rect section, set Position to (19, 6) and Size to (14, 10).

For the mana potions box, right-click on HealthPotions and choose Duplicate. Rename the newly created copy in ManaPotions. With ManaPotions selected, in the Inspector, change Rect → Position to (281, 158). Then, select its child Sprite node and in Region → Rect set x to 16 to change the image of the potion.

This is the final result:


In this tutorial we learned how to create a simple Graphical User Interface and update it using Godot’s event system.

You can try the final result of the tutorial by clicking here:

In the next tutorial, we will introduce monsters into the game and provide them with basic artificial intelligence.



Anastasia · March 7, 2020 at 5:47 pm

Ok hello! As always, I did something wrong and the manna bar doesnt work
I get lots of the error:
E 0:00:02:0155 Error calling method from signal ‘player_stats_changed’: ‘ColorRect(’: Method not found.
core/object.cpp:1238 @ emit_signal() @ _ready()
and cant quite say why.

var health = 100
var health_max = 100
var health_regeneration = 1
var mana = 100
var mana_max = 100
var mana_regeneration = 2
var attack_playing = false

signal player_stats_changed

# Player movement speed
export var speed = 75
var last_direction = Vector2(0, 1)

func _ready():
emit_signal(“player_stats_changed”, self)

func _input(event):
if event.is_action_pressed(“attack”):
attack_playing = true
var animation = get_animation_direction(last_direction) + “_attack”
elif event.is_action_pressed(“fireball”):
if mana >= 25:
mana = mana – 25
emit_signal(“player_stats_changed”, self)
attack_playing = true
var animation = get_animation_direction(last_direction) + “_fireball”

Here’s the stats fragment of the code. I asure you the signal is well connected and everything. The bars do have proper code too
thanks for being patient with my stupidity


    Davide Pesce · March 7, 2020 at 7:43 pm

    According to the error, there is no _on_player_player_stats_changed function in Have you checked that the name is typed correctly?


      Anastasia · March 8, 2020 at 11:13 am

      Both the mana and health code looks like this:
      extends ColorRect

      func _ready():
      pass # Replace with function body.

      func _on_Player_player_stats_changed(var player):
      $Bar.rect_size.x = 72 * player.mana / player.mana_max


        Davide Pesce · March 8, 2020 at 11:22 am

        Godot is case sensitive, _on_Player_player_stats_changed is a different function than _on_player_player_stats_changed. Always check that lowercase and uppercase letters of functions, variables and nodes are correct!


Anastasia · March 8, 2020 at 12:02 pm

YES you’re right! Bun now i get another set of errors

W 0:00:02:0448 Font oversampling only works with the resize modes ‘Keep Width’, ‘Keep Height’, and ‘Expand’.
scene/main/scene_tree.cpp:1158 @ _update_root_rect()

Guess the rect cant shrink? But why?


    Davide Pesce · March 8, 2020 at 8:48 pm

    Open the Project Settings window and go to the Display → Window section. Check if Strech → Mode is 2d and Strech → Aspect is keep.


      Anastasia · March 11, 2020 at 12:29 pm

      Aspect wasnt, Thanks~!


Dzandaa · April 25, 2020 at 5:45 pm


I add a CanvasLayer in my own game (Isometric) to display the player status
With just a ColorRect (Gray) and some labels

The player have a linked camera

I see the CanvasLayer in the editor but not in the game…

Any Idea?


    Davide Pesce · April 26, 2020 at 7:04 pm

    Hi Dzandaa, it’s difficult to answer without knowing the structure of your project. Have you changed any properties of the CanvasLayer in the Inspector?


Dzandaa · April 27, 2020 at 8:57 am

Hi Davide,

No, I didn’t change anything except the “Layer” -> 1 (Normally above).
I also try 0 and -1 values.
I read on another forum that there are some bugs with the CanvasLayer in Godot 3.2.

Don’t know if it is related.

Thanks anyway.


    Davide Pesce · April 27, 2020 at 9:01 am

    If you want, upload the project to Dropbox, Drive or similar and send me the link, I can try to take a look (hoping it’s not just a Godot bug)


    Bayguls · April 27, 2020 at 5:48 pm

    Hi Dzandaa,

    While utilizing this tutorial for my own 2D platformer, I think ran into this issue as well and I figured out what was happening for me. In my case, I could get the CanvasLayer to appear, but only by enabling Follow Viewpoint setting and even then, the HUD stayed still. I ended up creating a CanvasLayer scene and instanced it to the main scene and that worked.

    Hi David,

    Thank you so much for these tutorials! It’s certainly been helping me navigate Godot because of your concise explanations of each setting that’s used and how it works. I’ve gone from being frustrated figuring out GDscript to programming my own first prototype.


      Davide Pesce · April 27, 2020 at 8:52 pm

      Thanks Bayguls! 🙂


        Pacobato · August 26, 2020 at 8:01 am

        Hi Davide,

        Thanks for your tutorials.
        I had the same issues that Dzandaa encountered. It worked fine in your tutorial and in Godot tutorial but in my project, no health bar when running the project…
        I found that it is a small “glitch” , not really a bug, in Godot editor.
        The issue:
        Canvas Layers and canvas items are in screen coordinates, 0,0 top left and y axis pointing down. The editor put the canvas layer at (0,0) wich is perfect in your project because your working viewport in the editor has (0,0) top left.
        If for some reason, you prefer to have (0,0) in the middle of the working viewport (or elsewhere). It dont work well. I did translate the canvas and/or the health bar to view it right in the editor and it is the problem… (-138, -100) are outside screen coordinates.
        Maybe there’s an other workaround for this problem but I used Bayguls solution, a scene for the HUD. When added to the root, it does’nt display well in the editor but it work at run time. You can do it directly in your root scene but I’d found it easier to edit the HUD separately.


          Davide Pesce · August 26, 2020 at 1:06 pm

          Thanks for sharing your experience!


Dzandaa · April 28, 2020 at 6:50 am

Hi Bayguls,
I will try.
I had the same issue with follow viewpoint.
Thank you.


Özgür · September 30, 2020 at 5:00 pm

Hi Davide,
i have a question about how we are able to use variables from Player script inside the Health and Mana script
I see the same thing in other tutorials too but i never understand how it can be happen

Is it because of the signal we created or is there a something else like me being a potato ?


    Davide Pesce · October 3, 2020 at 9:43 am

    Hi Özgür, in the GDScript language all script’s variables are public, so if you have a reference to a node, you can access all its variables. In the case of this tutorial, the reference to player is passed as an argument of the player_stat_changed signal.


      Özgür · October 3, 2020 at 2:36 pm

      thanks for explaining that to me man I was trying to figure that out for a long time with the little bit knowledge that i have about coding. You are awesome


Anurag · October 20, 2020 at 2:55 pm

My fireball and attack animation is not playing although mana bar is working fine . Here is the code:
signal stats_changed

# Player Script
var velocity=Vector2.ZERO
var speed=100
var direction = Vector2.ZERO
var last_direction = Vector2(0,1)
var attack_playing=false

var health =100
var max_health=100
var health_regeneration=1
var mana =100
var max_mana = 100
var mana_regeneration=2
onready var anim_sprite:=$Sprite

func _ready() -> void:

func _input(event: InputEvent) -> void:
if event.is_action_pressed(“attack”):
var animation = get_animation_direction(last_direction) +”_attack”
elif event.is_action_pressed(“Fireball”):
if mana >=25:
mana = mana-25
var animation = get_animation_direction(last_direction) +”_fireball”


    Anurag · October 20, 2020 at 2:56 pm

    Please help me on this …


    Davide Pesce · October 22, 2020 at 7:30 pm

    Have you checked if all letter cases are correct? For example, you wrote Fireball with a capital F, in the tutorial the action was called fireball with a lowercase f. Start by checking that!

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.