Skip to content

Godot UI Tutorial


Building a UI

Published: Mon May 19 2025 17:33:30 GMT+0000 (Coordinated Universal Time)

Building a UI

Make sure setup from

https://www.unseen-godot.com/content/tutorials/getting-started/first-time-setup

Is complete.

WARNING: This is a converted article that uses the new Github build of the addons, check out discord on using them.

This is a short tutorial to demo how to use basic GODOT UI elements, called Control Nodes.We will make a main menu, then a memory matching game using only button nodes. So it will also demonstrate how you could make a simple card or word games as the matching tiles are pulled from a deck and then hidden values are checked for if they are a match. So any card game can use the deck, and many types of word games such as word association games or language learning games can work the same way.As usual, you can skip the explanation part and go right to the steps if needed.

Main menu setup

Explanation

The first thing we will do is create a main menu the player will land on first thing.This will be a Control node that holds a container, and the container will hold our buttons.It is very important to pick the right nodes, as control nodes work differently based on what their parent is.Control nodes are also where the heart of Godot Accessibility lies. Accesskit builds an overlay for screen readers based on the configuration of the control nodes.But you have a lot of control over control nodes. You can pick when and how they are focused, if they have a "Text" property like a label or button it will be read out by default, but all control nodes also have Accessibility properties for what the screen reader will pick up such as giving it a better name/description, or connecting objects to the same parent describer.They also have an "Accessibility Live" property, which allows you to set them to Aria live regions, so will alert active screen readers whenever they are updated.I personally do not use them a lot as during a game I expect any text that the player does not expect will be interrupted on accident and the other limitations they have. But there are games or situations where it's still very helpful.

Control Nodes in Godot

When working with Control nodes, their focus settings are crucial. Some control nodes you do not want to be focusable, others you of course do, then Godot has logic to automatically find where focus should go when pressing tab/arrow keys, but it's not always perfect.So you can modify these properties both in the Editor and in the code. Allowing you to dynamically change where focus goes and what is and isn't focusable as the game is played. To get a better idea of focus, the linked documentation goes more in depth.

Keyboard/Controller Navigation and Focus

Instructions part 1:

Create a new scene. CTRL+N, CTRL+A, then select: Control

Set focus to Control in scene tab (CTRL+1).

Press enter on Control and rename it to MainMenu.

Add node GmVBoxContainer as child of MainMenu(Control Node) CTRL+A to open node create menu.

Set focus to MainMenu in scene tab (CTRL+1).

Add node VBoxContainer as child of MainMenu(Control Node) CTRL+A to open node create menu.

Press enter on VBoxContainer and rename it to MenuBox.

Add node Label as child of MenuBox(VBoxContainer Node) CTRL+A to open node create menu.

Press enter on Label and rename it to Title.

Set focus to MenuBox in scene tab (CTRL+1).

Add node Button as child of MenuBox(VBoxContainer Node) CTRL+A to open node create menu.

Press enter on Button and rename it to Play.

Set focus to Title in scene tab (CTRL+1).

Set focus to MenuBox in scene tab (CTRL+1).

Set focus to MainMenu in scene tab (CTRL+1).

Set focus to MenuBox in scene tab (CTRL+1).

Add node Button as child of MenuBox(VBoxContainer Node) CTRL+A to open node create menu.

Set focus to Play in scene tab (CTRL+1).

Set focus to Title in scene tab (CTRL+1).

Go to inspector, ctrl+2 to get to inspector tab, ctrl+U to inspector categories.

Property focus_mode set to 2

Property text set to Title

Set focus to Play in scene tab (CTRL+1).

Go to inspector, ctrl+2 to get to inspector tab, ctrl+U to inspector categories.

Property text set to Play

Set focus to Button in scene tab (CTRL+1).

Press enter on Button and rename it to Quit.

Go to inspector, ctrl+2 to get to inspector tab, ctrl+U to inspector categories.

Property text set to Quit

Set focus to MainMenu in scene tab (CTRL+1).

Attach a script to MainMenu(CTRL+Equal Sign)

Copy the below script into the code.

Code Snippet 1

extends Control
@onready var title: Label = $MenuBox/Title


func _ready():
	title.focus_mode =Control.FOCUS_ALL
	title.grab_focus()

func _on_play_pressed() -> void:
	get_tree().change_scene_to_file("res://game.tscn")

func _on_quit_pressed() -> void:
	get_tree().quit()

Instructions part 2:

Next we need to tell the buttons that when they are pressed, they should run the code in game.

Set focus to MainMenu in scene tab (CTRL+1).

Attach a script to MainMenu(CTRL+Equal Sign)

Set focus to Quit in scene tab (CTRL+1).

Go to signals tab (CTRL+5), then select "pressed", press enter until it connects the "signal" to the already written code.

Set focus to Play in scene tab (CTRL+1).

Go to signals tab (CTRL+5), then select "pressed", press enter until it connects the "signal" to the already written code.

Card Setup:

Right now if we press the button, the game will crash, because we have no game! But before we build our table, let’s make our cards that will go on our table.

“Scenes” can be things like the main menu, a room. Or they can be a template for a game object. We are going to make a scene of a “card”, then we will have our game room duplicate that as “instances” and deal them out.

Instructions part 3

Create a new scene. CTRL+N, CTRL+A, then select: Button

Set focus to Button in scene tab (CTRL+1).

Press enter on Button and rename it to Card.

Attach a script to Card(CTRL+Equal Sign)

copy the below script to the now open script window (Or in VS code)

Code Snippet 2

extends Button

#our timer will be used so that way after a card is flipped over wrong, it flips back over after.
@onready var timer: Timer = $Timer

#place holder so if 0 is ever displayed it won't error but we know it didn't get one assigned
#The game scene will give this and hidden text value.
var numb = '0'
#Place holder text for same reason
var hidden_text =""
#if it's flipped over and secret is readable or not
var picked = false
#If this card has been matched or not
var found = false 


func _process(_delta):
	#If found, don't do anything
	if found == false:
		#If it's not a picked card, have number readable
		if picked == false:
			text = numb 
		#If it's been selected, show hidden text.
		else:
			text = hidden_text

#when it is picked
func _on_pressed() -> void:
	if picked == false:
		picked = true
		#Tell game room it has been picked.
		get_parent().get_parent().card_picked(self)

#Game room will call this if two cards have been picked taht are not matches
func wrong():
	#If we set picked to false right away, we wouldn't be able to read what was on the other side.
	timer.start()

func _on_timer_timeout() -> void:
	#once timer is done, flip card back over.
	picked = false

Instructions part 4

Set focus to Card in scene tab (CTRL+1).

Add node Timer as child of Card(Button Node) CTRL+A to open node create menu.

Go to inspector, ctrl+2 to get to inspector tab, ctrl+U to inspector categories.

Property one_shot set to true

Set focus to Card in scene tab (CTRL+1).

For card, attach "pressed" to the code by selecting it, and pressing enter until attached

Repeat for timer and "timeout". Use CTRL+5 to get to scene tab.

Our Game Scene

We now need to create our game room that will deal out our cards.

Create a new scene. CTRL+N, CTRL+A, then select: Control

Set focus to Control in scene tab (CTRL+1).

Press enter on Control and rename it to Game.

Add node GridContainer as child of Game(Control Node) CTRL+A to open node create menu.

Press enter on GridContainer and rename it to Table.

Go to inspector, ctrl+2 to get to inspector tab, ctrl+U to inspector categories.

Property layout_mode set to 1

Property anchors_preset set to 8

Property columns set to 4

Set focus to Game in scene tab (CTRL+1).

Add node Label as child of Game(Control Node) CTRL+A to open node create menu.

Set focus to Game in scene tab (CTRL+1)

Attach a script to Game(CTRL+Equal Sign)

Code Snippet

extends Control

#the table all our cards will be in. As a grid table, it will be 2d.
@onready var table: GridContainer = $Table
#win message and score
@onready var label: Label = $Label 

#Storing where the picked was
var picked = []
#how many pairs we have
var correct = 0
#Storing how many pairs have successfully matched
var tries = 0
#how many pairs are needed for game to end.
var goal = 8
#we are going to be creating instances of the card it, so we will store path here.
const CARD = preload("res://card.tscn")
func _ready():
	#making label that will hold win message invisible/unfocusable until game is over.
	label.visible = false
	#getting a random seed so game is different
	randomize()
	#card variable we will instance
	var card
	#our list of words for our memory matching game. 
	var array = ["cat","dog","frog","cow","chicken","turtle","snake","pig"]
	#copying each word of our array
	array = array + array
	#mixing the array
	array.shuffle()
	#continue until array is empty
	while array.size() > 0:
		#make a card instance in the void
		card = CARD.instantiate()
		#giving them a number that will be readable when card is face down
		card.numb = str(array.size())
		#Giving the card its hidden word and removing that item from the array
		card.hidden_text = array.pop_back()
		#Making the buttons a little bigger so visually they don't resize as game is played.
		card.custom_minimum_size = Vector2(64,64)
		#card is now removed from the void and added as child of Table.
		table.add_child(card)
	#making card #1 be focused on game start
	card.grab_focus()

#This function will be called by the button when it is picked
func card_picked(card):
	#tracks number of flips
	tries += 1
	#adding the newly picked card to our picked array
	picked.append(card)
	
	#if two cards are flipped over
	if picked.size() == 2:
		#if the two cards match
		if picked[0].hidden_text == picked[1].hidden_text:
			#marking them as found will make them stop updating
			picked[0].found = true
			picked[1].found = true
			picked[0].text = "Correct!"
			picked[1].text = "Correct!"
			#removing from array
			picked.pop_front()
			picked.pop_front()
			#moving towards our goal
			correct += 1
		else:
			#activiating the wrong function, which will make the card flip back over in 1 second
			picked[0].wrong()
			picked[1].wrong()
			#emptying the picked array
			picked.pop_front()
			picked.pop_front()
	#If we have the correct number of pairs
	if correct == goal:
		#make table no longer visible/focusable
		table.visible = false
		#Unhide our lable
		label.visible = true
		#Giving label our text and score
		label.text = 'Winner! You took ' + str(tries) + ' flips! Press escape to go to main menu!'
		#Making it so label can be focused, and setting focus to it so message will be read out.
		#Can also play music here if you wanted!
		label.focus_mode =Control.FOCUS_ALL
		label.grab_focus()
		
#check every frame if player wants to leave
func _process(_delta):
	#If player pressed ESC key, go to main menu.
	if Input.is_action_just_pressed("ui_cancel"):
		get_tree().change_scene_to_file("res://main_menu.tscn")

Final instructions:

Press CTRL+S to save (Just accept defaults)

Ctrl+shift+o to open a list of your scenes, find your main menu

Press F5 to run your game, press “current scene” if it prompts you

Play your game!