DEV Community

Cover image for Let’s Learn Godot 4 by Making an RPG — Part 5: Setting Up The Game GUI #1🤠
christine
christine

Posted on • Updated on

Let’s Learn Godot 4 by Making an RPG — Part 5: Setting Up The Game GUI #1🤠

Before we get to the fun parts, such as setting up enemies and quests, I first want us to get the creation of the game’s GUI out of the way. The GUI will allow us to visually show the changes in our player’s stats, such as their ammo and pickup amounts, health and stamina values, and current XP and level. By creating the basic user interface first, we can eliminate the problem of having to constantly check if these values are being influenced or changing via the console output.

Figure 10: GUI Layout plan.

Figure 10: GUI Layout plan.

This part, like the TileMap’’s part before, might be a bit long, so I’m going to break our GUI creation into three manageable parts: Health & Stamina; Pickups; and XP and Leveling.


WHAT YOU WILL LEARN IN THIS PART:
· How to add UI elements to your scene.
· How to duplicate nodes.
· How to update UI elements via custom-signals.
· How to change the anchoring of nodes.
· How to create, initialize, and connect custom-signals.


In your Player Scene, add a new CanvasLayer node. This node will serve as the container for the graphics that we will draw onto the screen, such as our progress bars or labels. We are adding the UI elements to the Main scene because we want to connect these elements from our instantiated Player script via signals to update their values.

Godot Learning RPG

Let’s also go ahead and rename it as UI, just so that it stays nice and organized.

Godot Learning RPG

We want the UI elements to be contained within the borders of the blue frame on your screen. I hid my existing nodes so that we can solely focus on the UI for now, so don’t panic if you see that I don’t have a map or player anymore!

Godot Learning RPG

HEALTH & STAMINA BARS

Let’s add a ColorRect node as a child to the UI node. This draws a rectangle that we can fill with color. This node will serve as our progress bar for our health values.

Godot Learning RPG

Add another ColorRect node to your newly added node. Rename the first ColorRect as HealthBar, and the second ColorRect as Value. The outer rectangle (HealthBar) will serve as the box or border for our progress bar, and the inner rectangle (Value) will be the actual color that changes to show the progress value.

Godot Learning RPG

Now, in the Inspector panel, underneath Layout > Transform, change the HealthBar node size to x: 100, and y: 10. Do the same for the Value node, but change its x: 98, and y: 8.

Godot Learning RPG

Godot Learning RPG

We also want to center our Value rectangle inside of the HealthBar rectangle. To do this, change its anchor preset to center.

Godot Learning RPG

Change the color of the HealthBar to #3a39356a, and the color of Value to #a4de79. In my game, the healthbar will be green, but you can change this color to whatever your heart desires.

Godot Learning RPG

Godot Learning RPG

Let’s anchor our healthbar to the bottom-left of our screen. In your Inspector panel, underneath Layout, change the anchor preset from center to bottom-left, and its position values to x: 5, y: 155.

Godot Learning RPG

Duplicate the entire HealthBar node (with its child-node Value) and rename it to StaminaBar.

Godot Learning RPG

Godot Learning RPG

Change the color of the Value node within StaminaBar to #377c9e. This will make it a blue color, but once again, this is your game so make it any color you’d like.

Godot Learning RPG

Let’s also anchor our stamina bar to the bottom-left of our screen. In your Inspector panel, underneath Layout, change the anchor preset from center to bottom-left, and its position values to x: 5, y: 165.

Godot Learning RPG

Now we have our health and stamina progress bars set up! We can now go ahead and implement the functionality for these elements in our code.

In our Player script, we need to add variables to capture our health value, max health, and health regeneration value, as well as the same for our stamina. On top of your Player.gd script, underneath the is_attacking code, let’s add these variables.

    ### Player.gd
    # older code

    # UI variables
    var health = 100
    var max_health = 100
    var regen_health = 1
    var stamina = 100
    var max_stamina = 100
    var regen_stamina = 5
Enter fullscreen mode Exit fullscreen mode

With our variables created, we need to create custom signals for both health and stamina so that other objects in our game can listen and react to change events, without directly referencing one another. In other words, these signals will notify our game that a change in our health or stamina values has occurred, and then trigger other events to occur because of this.

If our health and stamina variable changes, we want to fire off the signal to it so that visually these changes are shown in our progress bar and our health value is updated. We worked with signals before when we connected the built-in on_animation_finished() signal to our player script, but this time, we will create our own custom signal.


Why use custom signals?
While built-in signals cover a lot of common use cases (like button clicks or mouse events), they may not handle all the specific interactions or events that are unique to your game or application. Custom signals provide a way to define your own set of events that are specific to your game’s logic, making your codebase more organized, reusable, and maintainable.


Now, before we create our custom signal, let’s try and understand when we want these signals to emit and notify our game that our health and stamina values have changed. We want the signals to emit when we press the sprint input, and when we get damaged from a bullet or drink a health potion later on.

Figure 12: Custom signal overview

Figure 12: Custom signal overview

We will add our custom signals to the top of our code because we will add more signals later on. This will keep all of our signals neat and organized. To define custom signals, we use the keyword signal followed by the name of the signal. Our signals will update our health and stamina values, so we will call them health_updated and stamina_updated.

    ### Player.gd
    # older code
    # Custom signals
    signal health_updated
    signal stamina_updated
Enter fullscreen mode Exit fullscreen mode

Now that we have our signals and variables created, we also need to constantly check them to see if our health or stamina values need to be updated or regenerated. We can do this check in the _process() function, which will be called every time a frame is drawn (60 times a second). The function has a parameter called delta, which represents the elapsed time that has passed since the previous frame.

For each health and stamina, we need to calculate the updated values for each. We will do this by using the min() method, which will ensure that it will never exceed the maximum values of our max_health and max_stamina variables. Inside this min() function, we will calculate the updated value of our health and stamina variables and add it to the value of these variables that are captured in the current frame.

    ### Player.gd

    # older code

    # ------------------------- UI --------------------------------------------------
    func _process(delta):
        #calculate health
        var updated_health = min(health + regen_health * delta, max_health)   
        #calculate stamina  
        var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)
Enter fullscreen mode Exit fullscreen mode

If these values are different than our original values (var health and stamina), we will then update our health to the new value and emit our signal to notify the game of this change.

    ### Player.gd

    # older code

    # ------------------------- UI --------------------------------------------------
    func _process(delta):
        #regenerates health
        var updated_health = min(health + regen_health * delta, max_health)
        if updated_health != health:
            health = updated_health
            health_updated.emit(health, max_health)
        #regenerates stamina    
        var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)
        if updated_stamina != stamina:
            stamina = updated_stamina
            stamina_updated.emit(stamina, max_stamina)
Enter fullscreen mode Exit fullscreen mode

While we’re at it, let’s update our sprinting input action to use up some of our stamina when pressed. You’ll notice that we emit the signal via the .emit() method. This emits this signal, and all the callables connected to this signal will be triggered. We’ll create functions in our UI script which will be our callables, and so if the signal is emiited, this callable will be notified to update our UI components.

    ### Player.gd

    # older code

    # --------------------------------- Movement & Animations -----------------------
    func _physics_process(delta):
        # Get player input (left, right, up/down)
        var direction: Vector2
        direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
        direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
        # Normalize movement
        if abs(direction.x) == 1 and abs(direction.y) == 1:
            direction = direction.normalized()
        # Sprinting             
        if Input.is_action_pressed("ui_sprint"):
            if stamina >= 25:
                speed = 100
                stamina = stamina - 5
                stamina_updated.emit(stamina, max_stamina)
        elif Input.is_action_just_released("ui_sprint"):
            speed = 50  
        # older code
Enter fullscreen mode Exit fullscreen mode

You’ll notice that we passed our stamina variables as parameters into our signal. Since we’ll be using these signals to update our progress bars in our UI, passing both health/stamina and max_health/max_stamina in the signals provides the necessary information for UI components to accurately and flexibly display the player’s stamina status. It also ensures consistency and efficiency in the game’s code.

Now we need to connect our signal to our UI components (callables), so in your Player scene add a new script to both HealthBar and StaminaBar and save these scripts under your GUI folder. Call the one Health and the other one Stamina.

Godot Learning RPG

Godot Learning RPG

In your newly created Health.gd script, let’s create an @onready variable for the value of our HealthBar/Value node.

    ### Health.gd
    extends ColorRect
    # Node refs
    @onready var value = $Value
Enter fullscreen mode Exit fullscreen mode

We then need to create a function that will update the color value of the Value node. We can do this by multiplying its width (98) by the value of the player’s health divided by max_health. This will return a percentage value that will reflect our Value node’s new width.

    ### Health.gd

    extends ColorRect

    # Node refs
    @onready var value = $Value

    # Updates UI
    func update_health_ui(health, max_health):
        value.size.x = 98 * health / max_health
Enter fullscreen mode Exit fullscreen mode

Do the same for your stamina value in Stamina.gd.

    ### Stamina.gd

    extends ColorRect

    # Node refs
    @onready var value = $Value

    # Updates UI
    func update_stamina_ui(stamina, max_stamina):
        value.size.x = 98 * stamina / max_stamina
Enter fullscreen mode Exit fullscreen mode

Now we can connect the functions from our UI components to our signals in our built-in _ready() function. This function will connect our callables to our signal when the Player node enters the Main scene — thus the UI will be able to update the progress bars upon game load.


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.


We will connect our Player’s health_updated signal via the connect keyword, and the callable that it will connect to is our HealthBar’s update_health_ui function. This means each time there is a change in our health value, the player script will emit the signal, and our healthbar will update its value. We’ll create a node reference to our HealthBar and StaminaBar nodes so that we can access the functions from their attached scripts.

    ### Player.gd

    extends CharacterBody2D

    # Node references
    @onready var animation_sprite = $AnimatedSprite2D
    @onready var health_bar = $UI/HealthBar
    @onready var stamina_bar = $UI/StaminaBar

    # older code

    func _ready():
        # Connect the signals to the UI components' functions
        health_updated.connect(health_bar.update_health_ui)
        stamina_updated.connect(stamina_bar.update_stamina_ui)
Enter fullscreen mode Exit fullscreen mode

If you run your scene now and you sprint, you will see that the stamina bar decreases, as well as regenerates!

Godot Learning RPG

Let’s move on to our next GUI part for our Pickups UI. Remember to save and to make a backup of your project, and I’ll see you in the next part.

Your final code for this part should look like this.

Buy Me a Coffee at ko-fi.com


FULL TUTORIAL

Godot RPG

The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.

If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊

You can find the updated list of the tutorial links for all 23 parts in this series here.

Top comments (9)

Collapse
 
swan-chris profile image
Christopher Swan

I think where you have the code.
while stamina >= 25:
It should instead be:
if stamina >= 25:
Otherwise it will just use up all the stamina immediately 🤔
Also I think the reduction in stamina should be effected by delta, otherwise frame rate is going to just use up the stamina faster on faster computers.
Something like:
stamina = stamina - 20 * delta
I increased the stamina drain, otherwise the regen rate is the same as the use rate.
Unless I'm missing something?
What do you think?

P.S. Also can't thank you enough for the awesome tutorial, thank you

Collapse
 
christinec_dev profile image
christine

That's a pretty good suggestion! Thank you, and I'll have a look at the project to see where I can amend it with your upgrade. You're not missing something, in fact, I think you making changes like this on your own shows that you are good at problem solving & you can think for yourself, which will make you a great game dev! 😊

Collapse
 
roofener profile image
Robert

Ok everything was working and then after coding it keeps telling me that the script inherits from native type "Node2D", so it can't be assigned to an object of type "ColorRect". I have replaced all my code with the code for this lesson and am still having this issue. I am using Godot V4.1 stable. Thanks in advance.

Collapse
 
christinec_dev profile image
christine

It seems that you are trying to do the UI code in a script that "Extends Node2D". Make sure that the scripts for your UI (Health, and Stamina) are attached to the UI components - so Health.gd should be attached to HealthBar and Stamina.gd should be attached to StaminaBar. At the top of each of these scripts it should say "Extends ColorRect".

If this doesn't help, please can you tell me exactly which script returns this error (Main, Player, etc.) and at which line so that I can assist you further.

Collapse
 
znp8b profile image
Nikita

I've read some comments about stamina fixes and did more fixes such as:

  • Change the stamina bar color to red if its < 25
  • Use stamina all the way to 0 and dont go below 0
  • If you use stamina at <25 - there is no speed but stamina still is being used
  • Use your stamina only if you use your movement with sprint (so your stamina is not going down if you just hold sprint button while standing still)

Code below changes the color

# -------------- UI -----------------------------
func _process(delta):
    #calculate health
    var updated_health = min(health + regen_health * delta, max_health)
    if updated_health != health:
        health = updated_health
        health_updated.emit(health, max_health)
    #calculate stamina
    var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)
    if updated_stamina != stamina:
        stamina = updated_stamina
        stamina_updated.emit(stamina, max_stamina)

    # Change stamina color if < 25 (return the color back if >=25)
    if updated_stamina >= 25:
        # stamina_bar = $UI/StaminaBar
        stamina_bar.value.set_color(Color("#377c9e"))
    elif updated_stamina < 25:
        stamina_bar.value.set_color(Color("#d92938"))
Enter fullscreen mode Exit fullscreen mode

Code below changes:

  • stamina drain speed
  • cant go below 0
  • cant drain stamina while standing still
func _physics_process(delta):

# ...Old code of physics proccess...

    # Sprinting
    if Input.is_action_pressed("ui_sprint"):
        # Inside using sprint i added all our movement inputs
        # so its using stamina only when sprint is combined with moves
        if (Input.is_action_pressed("ui_up") 
        or Input.is_action_pressed("ui_down") 
        or Input.is_action_pressed("ui_left") 
        or Input.is_action_pressed("ui_right")):
            if stamina >= 25:
                speed = 100
            elif (stamina < 25):
                speed = 50
                # Drain stamina only if stamina > 0
            if (stamina > 0):
                # Found that "* delta" from the other comment
                # Comment from Christopher Swan
                # Not sure how it works, but it works better
                stamina = stamina - 20 * delta
                stamina_updated.emit(stamina, max_stamina)
    elif Input.is_action_just_released("ui_sprint"):
        speed = 50

# ...Old code of physics proccess...
Enter fullscreen mode Exit fullscreen mode

Chrisopher's comment:
Also I think the reduction in stamina should be effected by delta, otherwise frame rate is going to just use up the stamina faster on faster computers.
Something like:
stamina = stamina - 20 * delta
I increased the stamina drain, otherwise the regen rate is the same as the use rate.

I guess it works the same like:
stamina + regen_stamina * delta
^^ we have this at stamina regen with delta

Collapse
 
dj_retort_vt profile image
LinkageAX|AffinityLX

Not sure why but if I leave delta in

var updated_health = min(health * delta, max_health)
Enter fullscreen mode Exit fullscreen mode

the bars just instantly reset leading to no visual change, but removing delta resets the bars. I'm using a universal timer to drain the bar for testing purposes instead of button presses to make life easier. (maybe this is why? also the timer works)

Collapse
 
alex2025 profile image
Alex

I think that there are a couple of additions that can be made to the script.

  1. The script loses stamina when you press the shift button to sprint, even if you are not moving.
  2. When your stamina runs low, you do not slow down.

I don't know if you implement these later, as I'm giving my feedback one tutorial at a time. :)

Collapse
 
saleewisdom profile image
SaleeWisdom

I have a question, does updating data with float values have any impact on performance? Is it more appropriate to use int?

Collapse
 
muphet profile image
Muphet

i would probably use progressbars instead of color rects, no need to calculate percentages manually or update node's size