Verification: Static review complete; Studio/device tests unverifiedLast verified: 2026-10-03Prerequisites: Collect coins and track a score

08 UI and client scripts

Before you start and what you will build

Continue the obby/collectibles place from Collectibles and score. Keep its server scripts enabled. Your player must already have a leaderstats Folder containing an IntValue named Coins during Play. This lesson adds a small heads-up display, or HUD. Collecting a coin updates both the leaderboard and the HUD; the HUD survives character death.

For an experienced programmer, think of this as a view bound to replicated state. The view does not own the score. That separation becomes important when clients can request gameplay actions in Remotes and validation.

Where the code runs

Create a LocalScript, not a regular Script, under StarterPlayer > StarterPlayerScripts. Name it CoinHud. Studio copies this template into the joining player's PlayerScripts. Each client runs its own copy. Players.LocalPlayer identifies the player on that client. Do not place this LocalScript in ReplicatedStorage or ServerScriptService. Roblox documents these execution locations in Script types and locations.

The script creates its UI at runtime. You do not also create a ScreenGui in StarterGui for this example. Before Play, the only added object is:

StarterPlayer
  StarterPlayerScripts
    CoinHud                  LocalScript

During Play, look in the client Explorer for:

Players
  YourPlayer
    leaderstats
      Coins                  IntValue, created by the server
    PlayerGui
      CoinHud                ScreenGui, created by this client
        CoinsLabel           TextLabel
    PlayerScripts
      CoinHud                runtime copy of the LocalScript

Complete implementation

Replace the new LocalScript's default contents with this entire file. This is additive: it does not replace Collectibles or any checkpoint/hazard script.

Download 08-coin-hud.client.luau

-- LocalScript: StarterPlayer > StarterPlayerScripts > CoinHud
local Players = game:GetService("Players")
local player = Players.LocalPlayer
local playerGui = player:WaitForChild("PlayerGui")

local gui = Instance.new("ScreenGui")
gui.Name = "CoinHud"
gui.ResetOnSpawn = false
gui.Parent = playerGui

local label = Instance.new("TextLabel")
label.Name = "CoinsLabel"
label.Position = UDim2.fromOffset(16, 16)
label.Size = UDim2.fromOffset(220, 48)
label.BackgroundColor3 = Color3.fromRGB(25, 30, 40)
label.TextColor3 = Color3.fromRGB(255, 235, 120)
label.Font = Enum.Font.GothamBold
label.TextSize = 24
label.Text = "Coins: loading..."
label.Parent = gui

local leaderstats = player:WaitForChild("leaderstats", 10)
assert(leaderstats, "CoinHud needs lesson 07's Collectibles server script")
local coins = leaderstats:WaitForChild("Coins", 10)
assert(coins and coins:IsA("IntValue"), "Expected leaderstats.Coins IntValue")

local function render()
    label.Text = string.format("Coins: %d", coins.Value)
end
coins:GetPropertyChangedSignal("Value"):Connect(render)
render()

Understand the useful details

The ScreenGui is the root of this on-screen interface. ResetOnSpawn = false keeps it through ordinary character respawns. The script belongs in StarterPlayerScripts so it runs once per player session rather than making another HUD each time the character spawns. UI objects rendered on a player's screen live in PlayerGui; ScreenGui documents the container and its screen-layout controls.

UDim2.fromOffset(220, 48) gives the label a fixed width and height. Position places its top-left corner 16 pixels from the available UI area's top-left. Fixed offsets are easy to inspect while learning; later, Cross-device controls examines smaller screens and safe placement. The label is deliberately large enough to read at a glance. Avoid conveying success only with color, since some players cannot distinguish those colors.

The server and client do not initialize every object simultaneously. WaitForChild avoids reading leaderstats before it arrives. Here a ten-second timeout produces an actionable assertion if the prerequisite is missing. It is a diagnostic limit, not a network guarantee. If a deliberately slow loading design needs longer, change the timeout intentionally.

GetPropertyChangedSignal("Value") subscribes to future changes. Calling render() immediately afterward also handles the current count. Without that initial render, a player who already has coins might see “loading” until the next pickup. There is no frame loop and no polling timer: the interface changes only when its input changes.

The script never assigns coins.Value. Treat that as a code-review invariant. If you later animate the number, keep the real count separate from an interpolated display count. An attractive animation must not become the game's accounting system.

Verify it yourself

  1. Use Play, not a server-only Run test. The label should read Coins: 0 after startup.
  2. Touch an uncollected coin. The label and leaderboard should both increase by one.
  3. Touch the same coin again. The lesson 07 per-player pickup rule should prevent another award.
  4. Reset your character. There should still be exactly one HUD, showing the same session count.
  5. Stop and Play again. Without persistence, the new session starts at zero.

The examples have been statically reviewed, not run in Roblox Studio for this manual. These checks are the acceptance test, not a claim that execution was already verified.

Troubleshooting

  • Nothing appears: confirm the object's class is LocalScript, its name is CoinHud, and its parent is StarterPlayerScripts. Check client Output for an earlier error.
  • Loading remains: inspect Players > YourPlayer > leaderstats during Play. Exact capitalization matters. Fix the prerequisite server script instead of creating a second client-owned score.
  • Two labels overlap: remove the duplicate template/script; do not move one label to disguise a duplicate execution path.
  • Edits disappear after Stop: you edited the runtime copy. Stop first, then edit the StarterPlayerScripts source.

Exercises

  1. Add a second TextLabel explaining the three-coin boost requirement. Keep it display-only.
  2. Change the label's background and text while maintaining readable contrast.
  3. Predict what happens if render() is removed, then test your prediction and restore it.

Sources and next steps

API and placement references last checked 2026-10-03: script locations, PlayerScripts, ScreenGui.

Previous: 07 Collectibles and score · Next: 09 Client-server boundary

Related: 04 Events and debugging · 13 Cross-device controls · Glossary

Progress and help notes

Nothing is sent automatically. Copying happens only when you press the button.

Sources