How to Make UI in Godot 4
As a beginner, making UI can be very confusing. Godot handles the UI processes well for me, but for beginners it's a bit confusing, not gonna lie. I know this from myself and my friends who have tried to do UI in Godot. So in this blog I will guide you on how to handle the UI in Godot 4. In this blog post you will learn how to do this:
Recommendation
I would recommend watching a tutorial about Godot nodes or at least having a bit of experience on using them because I will not talk in depth about every node.
And! I will give you a homework to experiment with and learn different things. In this demo, I made it so that it'll be easier for you to learn the topics when you do your homework.
Now lets start!
Making the UI Framework
Let's start with our first issue... You know how you try to make a proper UI in the Godot and end up saying "WHY CAN'T I MOVE IT?!"
And our second issue... Setting every UI to Full-Rect doesn't seem to be a good idea. For this reason we will make a fancy framework. For this demo we are going to make a shop, and it will be a simple thing. So let's note our key elements:
- Title at the top
- Shop is scrollable
- Pre-made item slots will be added to a container that we can scroll.
- This slot will have a texture rect, a name label, lore text, price and a buy button.
And let's make it! We will start with a good old classic HBoxContainer and give our Shop area most of the space with Stretch Ratio. We are also making a debug panel ready to debug stuff. And let's give a background to them with Panels. And use Margin Container for good padding.
Now that we have made our general structure, we can style the Shop UI and lay the groundwork for a working UI.
My UI Design Principle in Godot
For me, laying the UI in just one scene, making the look correct is one of the most important part for it to work. If you got your nodes, give some good padding, deciding what nodes will clip UI so it doesn't get outside. Giving their custom minimum size is important. This is the framework part. You have to make your UI first drawn and UI logic must be explained. It's like game design but you are making this for the UI. If you got your idea drawn, know what elements do what. You make it visually in Godot then you can save stuff as a scene or add codes.
If you code, design later and code and try to make your design work then design to make things work... It looks like you are building the bicycle and trying the ride it at the same time.
We organized the UI nodes and now we have the framework ready. For us, the important part is the Scroll Container. With that we can scroll. What we are going to scroll and how it will behave is defined by a single node under that node. In this demo we want to scroll up and down, so we will choose VBoxContainer. Also we created our first Item Container; I named it slot.
Making the Item Container
Instead of saving every item as different Item Container scenes, we are going to use a database. Not to make stuff sound complicated, we are going to use Custom Resources. And it's easy stuff. For this purpose, the script is just this:
@icon("res://item/item.svg")
class_name Item extends Resource
@export var item_texture : CompressedTexture2D
@export var item_price : float
@export var item_name : String
@export var item_lore : String
This will hold every value we need. For our demo, I made some example items and you can create your own items in the demo!
Now that this is ready, let's prepare the Item Container. We are designing the container, but what's most important for us are the:
- TextureRect
- Label
- RichTextLabel
- Label (for price)
- TextureButton (Button is okey to) to buy
And what we got is here!
If you get curious what are these % icons. It makes easier to get a reference for these nodes in script. Instead of saying var box = $VBox/Margin/Hell/ScrollContainer/Box
we can just say var box = %Box
so even if we change node's position in the scene tree, it would not throw us an error.
How that node stays like that
Important part is the Slot (Control) has custom minimum_size 130px on the Y axis and 'Vertical Expand' property is off because we don't want it to be depended on the size of other elements and don't get thinner than 130px.
And with that, We have almost everything finished. We have our item database ready and our Item Container ready, and all that is left is instancing the scene with the GUI Manager and deleting it when we click the BUY button.
But you know, just before that, we need to prepare the money and the inventory. We are going to create an Autoload for this.
extends Node
var player_money : float = 100.0 :
//debt preventor
set(value):
if value < 0: player_money = 0
else: player_money = value
var player_inventory : Array[String]
const items = {
"face": preload("res://item/item_resources/face.tres"),
"stick": preload("res://item/item_resources/stick.tres"),
"sun": preload("res://item/item_resources/sun.tres"),
"sword": preload("res://item/item_resources/sword.tres"),
"tree": preload("res://item/item_resources/tree.tres")
}
func add_item_to_player(item_name: String)->void:
player_inventory.append(item_name)
We have our money, placed our setter getter so we don't go in debt (- money), made the inventory, the item adding function, and our items are defined in a dictionary so we can just say Global.add_item_to_player("tree")
.
And with that, we can just get into the last part. We are placing the script but do not worry. As I said, it's the easiest part.
The GUI Manager
With everything we need to just say to the GUI Manager:
"Bro, you know Scroll Container's VBoxContainer? Can you instance one Item Container for me into it? I want 'stick'. You can reach it in the Global.items, and the values are in it. Just say Global.items["stick"] and you have everything .name? .texture? Everything. Thanks bro."
So let's put that into the language of Godot:
GUI Manager gd
extends Control
const SLOT = preload("res://GUI/slot.tscn")
@onready var _item_content_box: VBoxContainer = %ItemContentVBox
func add_item_slot(item_name: String) -> void:
if not Global.items.keys().has(item_name): return
_item_content_box.add_child(new_item_slot)
//item_name is like a variable, we store item_name in the node
//as item_name
new_item_slot.set_meta("item_name", item_name)
new_item_slot.buy_button.pressed.connect(_item_bought.bind(new_item_slot))
new_item_slot.insert_values(item_name)
As you can see, it has very little code. If you caught it, you can ask "What is insert_values?"
Its handling the last part of our analogy sentence. The item container's script is just placing values from the Item Custom Resource's we made.
Slot gd
@icon("res://GUI/shop_slot.svg")
class_name ItemUISlot extends Control
@onready var item_texture_rect: TextureRect = %ItemTextureRect
@onready var item_name_label: Label = %ItemName
@onready var lore_rich_text: RichTextLabel = %Lore
@onready var price_label: Label = %PriceLabel
@onready var buy_button: TextureButton = %BuyButton
func insert_values(item_name: String)->void:
item_texture_rect.texture = Global.items[item_name].item_texture
item_name_label.text = Global.items[item_name].item_name
lore_rich_text.add_text(Global.items[item_name].item_lore)
price_label.text = str(Global.items[item_name].item_price)
Last thing is that, as you can see we connected this slot's buy_button's pressed signal in the GUI Manager to _item_bought
function. Also there is something called .bind()
and set_meta
? Shortly, when instancing the Item Container, for example a tree, we said to that container: "Yo tree, I need you to remember you are a 'tree' so hold on that information. When your BUY button is pressed I want you to come to my _item_bought
function. I'm gonna need you to tell me you have 'tree' value ready for me."
So that is that, it can be done in other ways but I left comments in the source code so you can check it. We can store anything with set_meta()
in a node, and for this demo it's the item's name is a string. And when we press buy, we get that node's meta value with get_meta()
.
And that function will look like this:
GUI Manager gd
func _item_bought(item_slot: ItemUISlot)->void:
item_slot.buy_button.disabled = true
if not item_slot.has_meta("item_name"):
print_rich("[color=red]BRO[/color]")
return
audio_stream_player.play()
//we take item_name from that node back and adding to the player
Global.add_item_to_player(item_slot.get_meta("item_name"))
item_slot.get_lost_lol()
And by getting the value, we added the item to our player's inventory as a string. We could just pass the String instead of setting it as a meta but I wanted to teach you about Metadata too :D Also if this UI is in a scene and you stopped the game and UI stopped working set it's process mode to Always
THE END
So that's it! We can set SFX, and place a function called get_lost_lol()
into our Item Container so it can get deleted in a fancy way when bought. Add some buttons and functions to the DEBUG section like adding money to the player, or adding an item to the shop with a button and such. I made those debug utils for you, but you can look at the source code and experiment yourself and add new things. Speaking of experimenting and new things, as I mentioned in the start, I'm giving you some homework. It's optional but you would be Certified Cool if you do it.
So... you'll have the source code as well as the excalidraw presentation for more stuff.
HOMEWORK OMG
Homework comments goes under this - Bsky Post Link
SPECIAL THANKS
Amazing People That Helped Me For this Blog
Sprained Tootsie
Composition Kat
Amelia Lazouras
Kloscur (CopperOwl Games)
Rafaela Overflow
Niki's Quest Dev
Cesilla
Descless
Abor