Kahibaro
Discord Login Register

3.2.4 Updating UI with scripts

Why Update UI with Scripts

User interfaces in Roblox only feel alive when they react to what the player does. A static text label that never changes is just decoration. As soon as your game shows scores, timers, health, coins, or messages that change during play, you need scripts to control the UI. Updating UI with scripts connects your game logic to what the player sees on screen.

In this chapter you focus on how scripts talk to UI elements, how to change their properties in real time, and how to respond to events in the game. You already know what ScreenGuis, buttons, and text labels are, so here you learn how to control them from code.

Where UI Objects Live and How to Find Them

To update UI with scripts you first need to understand where the UI objects are stored and how to get references to them in Lua.

In Roblox, interface elements such as ScreenGui, TextLabel, TextButton, and ImageLabel usually live under StarterGui. When the player joins the game, Roblox automatically copies the contents of StarterGui into PlayerGui, which is a child of each Player. The actual visible UI at runtime is in PlayerGui, not directly in StarterGui.

This leads to a very important pattern. When you want to change a UI element during the game, you usually script from the client side, using a LocalScript that runs for each player. The script will look up the player's PlayerGui and then find the specific GUI you want to modify.

For example, imagine that you have this structure in Roblox Studio:

StarterGui contains MainGui (a ScreenGui).

MainGui contains ScoreLabel (a TextLabel).

At runtime, for each player, the path to the live label is:

Players.LocalPlayer.PlayerGui.MainGui.ScoreLabel.

A typical way to get it in a LocalScript is:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local scoreLabel = mainGui:WaitForChild("ScoreLabel")

Using WaitForChild is important because it makes the script wait until the object has been created. If you try to access the UI too early, you can get errors. With this reference in hand, you can start updating the UI over time.

Always update on-screen UI through PlayerGui objects that belong to the local player. Use LocalScripts for client interfaces, not normal Script objects that run on the server.

Choosing Scripts vs LocalScripts for UI

Most interface updates should happen on the client, inside a LocalScript. Only LocalScripts can access Players.LocalPlayer, which is the usual starting point to find the current player's PlayerGui. LocalScripts run in specific places, such as inside StarterGui, PlayerGui, StarterPlayerScripts, or inside a Tool.

On the other hand, normal Script objects run on the server. Server scripts usually manage game rules, shared data, and communication between players. When a server script needs to change a player's UI, it does not edit PlayerGui directly. Instead, it sends information to the correct client, and a LocalScript on that client updates the interface.

In simple single player experiences you might feel tempted to do everything in server scripts. You should still keep UI updates on the client side. This keeps your game smoother and prepares you for multiplayer design.

Changing Text and Numbers on Screen

The most common UI update in games is changing text to show numbers, such as score, coins, or time left. To change the text of a TextLabel or TextButton, you simply change the Text property of that object.

Continuing the earlier example, suppose you have a variable that tracks score in a LocalScript. Each time the score changes, you want to display it. The code might look like this:

local score = 0
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local scoreLabel = mainGui:WaitForChild("ScoreLabel")
local function updateScoreDisplay()
    scoreLabel.Text = "Score: " .. score
end
local function addPoints(points)
    score = score + points
    updateScoreDisplay()
end
addPoints(10)
addPoints(5)

The .. operator joins text together. Inside updateScoreDisplay, the number score is converted to a string and combined with the label "Score: ".

If you want to format numbers with more control, you can do it in Lua before assigning them to the Text property. For example, to show time remaining in seconds using a variable timeLeft, you might write:

timeLabel.Text = "Time left: " .. timeLeft .. "s"

You can update UI text from any place in your code. The important habit is to keep the lookup for the label in one place, then call functions that use those references instead of searching the entire hierarchy each time.

Never try to update the text of StarterGui during the game. Roblox copies StarterGui into PlayerGui when the player joins. During play, you must update the live objects in PlayerGui, or the player will not see your changes.

Responding to Game Events with UI Updates

UI should react whenever something important happens in the game. You do this by connecting your UI update functions to events in your scripts. For example, imagine you are building an obby, and you want a label to show how many times the player has died.

Suppose you already have code that handles player death. When the character dies, your script fires a custom event or calls a function that increments a death counter. In the same place, you can update the UI.

One approach is to put a LocalScript in StarterPlayerScripts that listens to the local player's character and updates UI when the Humanoid dies. The code could be:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local deathsLabel = mainGui:WaitForChild("DeathsLabel")
local deaths = 0
local function updateDeathsDisplay()
    deathsLabel.Text = "Deaths: " .. deaths
end
local function onCharacterAdded(character)
    local humanoid = character:WaitForChild("Humanoid")
    humanoid.Died:Connect(function()
        deaths = deaths + 1
        updateDeathsDisplay()
    end)
end
player.CharacterAdded:Connect(onCharacterAdded)

Here, the Humanoid.Died event is a game event that triggers the UI update. This pattern is powerful. Any time you have an event, such as a player grabbing a coin, finishing a stage, or starting a timer, you can connect that event to a function that updates the UI.

In more complex games, the server will fire a RemoteEvent to the client when something changes, and the client LocalScript will use the data in that event to update text labels or other UI elements. The concept is the same. Game events drive UI changes.

Timers and Repeated UI Updates

Sometimes you need the interface to change every frame or every second, such as countdown timers or progress bars. In Roblox, you usually do this with loops that run inside a LocalScript, along with task.wait or RunService.

A simple countdown timer might look like this:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local timerLabel = mainGui:WaitForChild("TimerLabel")
local timeLeft = 30
while timeLeft >= 0 do
    timerLabel.Text = "Time left: " .. timeLeft
    task.wait(1)
    timeLeft = timeLeft - 1
end
timerLabel.Text = "Time's up!"

This loop runs on the client and updates the label once per second. For very short or accurate updates, you can use a connection to RunService.RenderStepped for smoother changes, especially if you animate bars or numbers each frame. For example, a smooth bar might update its Size property many times per second.

When you use loops for UI, always think about whether the loop should stop at some point. Infinite loops that never break can cause problems if they allocate resources or reference objects that later get removed. Make sure loops check a condition and exit when the UI is no longer needed.

Visual Feedback by Changing UI Properties

Text is not the only property you can change. Any writable property on a UI object can be controlled by scripts. This lets you give players visual feedback when something happens.

Some common property changes include:

Changing TextColor3 of a TextLabel to indicate good or bad outcomes.

Changing BackgroundColor3 of a frame to highlight it.

Changing the Visible property of a UI object to show or hide it.

Changing the Size or Position to move or resize elements.

For example, to show a "checkpoint reached" message briefly on screen, you might do:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local checkpointFrame = mainGui:WaitForChild("CheckpointMessage")
local function showCheckpointMessage()
    checkpointFrame.Visible = true
    task.wait(2)
    checkpointFrame.Visible = false
end

You would call showCheckpointMessage() when the player touches a checkpoint part. The script toggles visibility to create a simple popup. In more advanced designs, UI animations can be combined with these property changes, but even simple scripts like this improve feedback to the player.

For health displays, you might have a Frame that shrinks as health decreases. If the full bar width is 1 in scale units, and health is between 0 and 100, you can set:

$$
\text{HealthBarWidth} = \frac{\text{currentHealth}}{\text{maxHealth}}
$$

Then you can write:

local maxHealth = 100
local currentHealth = 75
local healthBar = mainGui:WaitForChild("HealthBar")
local function updateHealthBar()
    local ratio = currentHealth / maxHealth
    healthBar.Size = UDim2.new(ratio, 0, healthBar.Size.Y.Scale, healthBar.Size.Y.Offset)
end

When you convert game values into UI, always keep ratios in the range from 0 to 1 for size scales. This helps you avoid bars that grow outside their frames or disappear completely.

Connecting Buttons to UI Changes

You have already seen how to detect button clicks. Updating the interface in response to button events is a direct extension of that idea. For example, you may have a "Show Help" button that toggles the visibility of a help panel.

Assume the UI hierarchy contains MainGui, with a HelpButton (a TextButton) and a HelpFrame (a Frame). A LocalScript inside MainGui could manage both like this:

local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")
local mainGui = playerGui:WaitForChild("MainGui")
local helpButton = mainGui:WaitForChild("HelpButton")
local helpFrame = mainGui:WaitForChild("HelpFrame")
local helpVisible = false
local function updateHelpVisibility()
    helpFrame.Visible = helpVisible
end
helpButton.MouseButton1Click:Connect(function()
    helpVisible = not helpVisible
    updateHelpVisibility()
end)

Here the button click changes a variable and then calls a function to update the UI. Keeping the actual visibility logic in its own function makes it easier to call it from other parts of your code if needed.

You can use this pattern in menus, inventory systems, and settings screens. Button events change state in your game, and then your UI update functions read that state and adjust properties to match.

Keeping UI Updates Organized

As your game grows, it is easy to place small UI changes all over your scripts. This leads to complicated code that is hard to change. A better habit is to keep related UI changes in one script or module, and expose functions like updateScore, updateHealth, showMessage, or setMenuOpen.

For example, a LocalScript inside MainGui can handle all the labels and frames in that ScreenGui. Other scripts that run on the client can fire BindableEvents, or simply call functions inside that LocalScript if you organize them with ModuleScripts. Then, all the logic for formatting text, animating frames, and showing or hiding elements stays in one place.

You do not need a full architecture for simple beginner projects, but you can still start using small rules such as:

Keep a single LocalScript in each main ScreenGui to manage that GUI.

Write a separate function for each type of update, such as setStageNumber(stage), setCoins(coins), or setTimer(seconds).

Call these functions whenever relevant game events happen.

This organization helps you change the look of your UI without hunting through all of your game code.

Putting It All Together in a Simple Obby

To see how all this connects, imagine your obby has a StageLabel and a TimeLabel that update as the player progresses. A LocalScript in your main ScreenGui might define functions:

setStageNumber(stageNumber) which writes "Stage: " .. stageNumber into the StageLabel.

setTimeRemaining(seconds) which writes "Time left: " .. seconds into the TimeLabel.

Your game logic, whether on the server or client, tracks the current stage and remaining time. Each time those values change, the scripts that manage the game call these functions. The LocalScript finds the labels, converts numbers to strings, and updates the Text properties. With only a few lines of code, you turn raw gameplay variables into clear feedback on the screen.

Once you understand how to find UI objects, change their properties, and tie updates to game events, you can use the same methods for more advanced menus, shops, and inventories in later chapters.

Views: 24

Comments

Please login to add a comment.

Don't have an account? Register now!