Collect coins and track a score
Outcome and prerequisites
In the obby from lesson 06, each player can collect each coin once per server session. Their score appears in the built-in player list and becomes the data source for a custom HUD in the next lesson.
The design choice matters: collecting a coin does not remove it for everyone. Each player owns a separate set of coins they have collected. For now the coin stays visible even after you collect it. This makes the server model easy to inspect; a later polish exercise can hide consumed coins for that player only.
Build the coin objects
First add a Part named CoinPlatform under Workspace.Obby. Set Anchored=true, CanCollide=true, Size=(12,1,12) and Position=(0,1,-12). Its near edge meets Platform0 at Z=-6, forming a connected side pad away from the spawn. Then create Workspace.Coins as a Folder. Insert five Parts named Coin1 through Coin5. For each set Shape=Ball, Size=(2,2,2), Anchored=true, CanCollide=false and CanTouch=true. Give them a gold Color. Position them at:
| Coin | Position X Y Z |
|---|---|
| Coin1 | 0, 3, -12 |
| Coin2 | 12, 4, 0 |
| Coin3 | 24, 6, 0 |
| Coin4 | 36, 6, 0 |
| Coin5 | 48, 8, 0 |
Coin1 is on the connected side pad, well away from the spawn footprint; a fresh test with the normal avatar should start at zero. Walk onto that pad to collect it deliberately. The coins sit near avatar height so normal traversal can touch them. If your edited route differs, reposition coins above actual landings rather than copying unreachable coordinates.
Add the scoring owner
Insert one Script at ServerScriptService.Collectibles. Keep Checkpoints and Hazards from the previous lesson. Replace only the new script's default body:
-- Place: ServerScriptService.Collectibles (Script)
local Players = game:GetService("Players")
local coins = workspace:WaitForChild("Coins")
local collected: {[Player]: {[Instance]: boolean}} = {}
local function initialize(player: Player)
if collected[player] then return end
collected[player] = {}
local stats = Instance.new("Folder")
stats.Name = "leaderstats"
local score = Instance.new("IntValue")
score.Name = "Coins"
score.Value = 0
score.Parent = stats
stats.Parent = player
end
Players.PlayerAdded:Connect(initialize)
for _, player in Players:GetPlayers() do initialize(player) end
Players.PlayerRemoving:Connect(function(player)
collected[player] = nil
end)
for _, coin in coins:GetChildren() do
if not coin:IsA("BasePart") then continue end
coin.Touched:Connect(function(hit)
local character = hit:FindFirstAncestorOfClass("Model")
if not character then return end
local player = Players:GetPlayerFromCharacter(character)
local humanoid = character:FindFirstChildOfClass("Humanoid")
local root = character:FindFirstChild("HumanoidRootPart")
if not player or not humanoid or humanoid.Health <= 0 then return end
if not root or not root:IsA("BasePart") then return end
if not ((root.Position - coin.Position).Magnitude <= 8) then return end
local seen = collected[player]
if not seen or seen[coin] then return end
local stats = player:FindFirstChild("leaderstats")
local score = stats and stats:FindFirstChild("Coins")
if not score or not score:IsA("IntValue") then return end
-- No yielding between checking and marking this coin.
seen[coin] = true
score.Value += 1
print(player.Name, "collected", coin.Name, "total", score.Value)
end)
end
Download: collectibles.luau
leaderstats is a special lowercase name used by Roblox's built-in leaderboard display. Coins is an IntValue created by the server. The table of collected coin instances is server-only, so the client does not get to declare its own reward.
Notice the order: validate the contact, locate the score, mark the coin collected, then award exactly one point. There is no yield between the duplicate check and the mark. A body brushing the coin with multiple parts cannot repeatedly earn the same point through that path.
The distance check rejects obvious nonlocal contacts. It is defense in depth, not a guarantee against movement exploitation. For valuable rewards, design stronger server-side movement and progression checks. Keep this sample's coins as disposable learning points.
Verify
Run Test. Touch Coin1: Coins should become 1. Walk through it again: the value should stay 1. Collect a different coin: the value becomes 2. Die and respawn: the current score remains, because the Player and its leaderstats survive the Character. Stop and begin a new test: the score returns to 0.
Coins remain visible deliberately. Do not destroy them on the server to hide them for one player; that would remove the shared object from everyone. In a two-client test, both players should independently collect the same coin and each receive one point.
Inspect Players/<test player>/leaderstats/Coins in the server runtime tree. Compare it with the client view. You have now created server-owned state that can be observed by a client. The UI lesson presents it without becoming its authority.
Understand the limits
This script connects coins present at startup. Adding a new coin during runtime will not automatically connect its Touched event. Either author all coins in edit mode, or extend the design with an explicit registration function when spawning them.
The collected set uses Instance keys. That is convenient within one session but unsuitable as a persistent identifier. Recreating a coin creates a different Instance. Persistent collection would need stable IDs, a content/version policy and careful saving behavior. Do not serialize object references into a data store.
Troubleshooting
The built-in score is absent: check exact lowercase leaderstats, Script placement and Output errors. One touch gives two points: search ServerScriptService for duplicate Collectibles scripts. Nobody can touch a coin: check CanTouch and placement. Only one player gets it: check whether you added a global collected flag or destroyed the coin. A later lesson says Coins is missing: do not rename the IntValue without updating every dependency.
Exercise
Add Coin6 on your optional side route before pressing Play. Test that it awards once. Write a rule for whether coins should reset on death, on round start or only on rejoining; each is a different game design. Keep this course's once-per-session rule for the remaining obby lessons.
Save obby-workshop-07.rbxl.
Sources and navigation
Verified 2026-10-03: in-game leaderboards, BasePart.Touched, Players, client-server security.
Previous: Checkpoints · Next: UI and client scripts · Related: Client/server, Saving