How to Build a Roblox Round System: Intermission, Timer, Winners
Last updated:
Quick answer: A Roblox round system is a server-side loop that cycles through three states — Intermission, InRound, and Results — waiting for enough players, teleporting them to an arena with PivotTo, broadcasting one authoritative countdown, detecting the winner, and resetting everything before the next cycle.
A Roblox round system is a server-side loop that cycles a match through three states: an Intermission where players gather and the game waits for enough people, the InRound phase where players are teleported to the arena and compete, and a Results phase that announces the winner and resets the map. The server owns this loop, publishes the current state and time left so every client sees the same countdown, and cleans up fully before starting the next round.
What states does a round system need?
Model the game as a small state machine with exactly three phases, and let one script on the server move between them. Everything else — GUIs, teleporting, rewards — hangs off the current state.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MIN_PLAYERS = 2
local INTERMISSION_TIME = 15
local ROUND_TIME = 120
local RESULTS_TIME = 8
local function setState(stateName)
ReplicatedStorage:SetAttribute("RoundState", stateName)
end
while true do
setState("Intermission")
task.wait(INTERMISSION_TIME)
setState("InRound")
task.wait(ROUND_TIME)
setState("Results")
task.wait(RESULTS_TIME)
end
Attributes on ReplicatedStorage are a lightweight way to publish state: clients read them with GetAttribute and react with GetAttributeChangedSignal, and only the server can write them, so no client can fake the phase. For the GUI you will want a countdown too, which the timer section below adds.
Keep the state values in one table or a ModuleScript config instead of scattering string literals; a typo like "Inround" in a client comparison silently breaks a whole GUI.
How do I wait for enough players without freezing the server?
Do not block startup waiting for players. Run the intermission loop as part of the cycle and simply stay in the Intermission state until the lobby has enough people:
local function waitForEnoughPlayers()
while #Players:GetPlayers() < MIN_PLAYERS do
task.wait(1)
end
end
-- inside the main loop, replacing task.wait(INTERMISSION_TIME) when the lobby is empty:
while #Players:GetPlayers() < MIN_PLAYERS do
ReplicatedStorage:SetAttribute("RoundState", "WaitingForPlayers")
task.wait(1)
end
A task.wait(1) poll is the right tool here — the loop needs to re-check player count every second anyway, and unlike an event-driven setup it also handles players leaving right before the round starts. Never use the legacy wait, spawn, or delay functions for this; they are deprecated in favor of task.wait and task.spawn and their scheduling behavior is unreliable.
If you want the intermission to run only when the lobby is actually filling up, tick the countdown only while at least one player is present, and reset the countdown when the lobby drops below MIN_PLAYERS. That avoids announcing "round starting" to an empty server.
How do I teleport players to the arena safely?
Never set CFrame on individual body parts. Use character:PivotTo to move the whole character in one call, and verify each character actually exists and is alive — players disconnecting mid-teleport, or respawning right as the round begins, are normal occurrences:
local function teleportToArena(spawnPoints)
local players = Players:GetPlayers()
for index, player in players do
local character = player.Character
local root = character and character:FindFirstChild("HumanoidRootPart")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if root and humanoid and humanoid.Health > 0 then
local spawnPoint = spawnPoints[(index - 1) % #spawnPoints + 1]
character:PivotTo(spawnPoint.CFrame + Vector3.new(0, 3, 0))
end
end
end
The + Vector3.new(0, 3, 0) lift keeps characters from spawning clipped inside the spawn pad and getting stuck. Gather the spawn points once per round from the arena model (for example arena:GetChildren() filtered to parts named "SpawnPoint") and pass them in, so teleporting does not depend on global workspace layout.
Handle respawns too: if a player dies and respawns during a round, Roblox places them at a SpawnLocation. Either disable respawning for the round (set the players' Humanoid behavior via Players.CharacterAutoLoads = false and load characters yourself) or teleport every new character to the lobby on respawn until the next round starts.
How do I build a countdown timer everyone sees?
The timer must be server-authoritative. If each client counts down on its own, players drift apart and an exploiter can freeze their own display. Have the server update one shared value every second, and let clients merely display it:
local function countdown(seconds)
for remaining = seconds, 0, -1 do
ReplicatedStorage:SetAttribute("TimeLeft", remaining)
task.wait(1)
end
ReplicatedStorage:SetAttribute("TimeLeft", 0)
end
-- usage inside the main loop:
countdown(INTERMISSION_TIME) -- shown in the lobby GUI
-- ... teleport players, then:
countdown(ROUND_TIME) -- shown in the round HUD
On the client, a LocalScript renders whatever the server published:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local function refreshTimer()
local timeLeft = ReplicatedStorage:GetAttribute("TimeLeft") or 0
local stateName = ReplicatedStorage:GetAttribute("RoundState") or "Intermission"
timerLabel.Text = stateName .. ": " .. tostring(timeLeft) .. "s"
end
ReplicatedStorage:GetAttributeChangedSignal("TimeLeft"):Connect(refreshTimer)
ReplicatedStorage:GetAttributeChangedSignal("RoundState"):Connect(refreshTimer)
refreshTimer()
Because task.wait(1) can drift slightly over a long round, deriving the display from one server counter (rather than having every client decrement its own copy) keeps every screen honest to the second.
How do I detect the winner and give rewards?
For a last-player-standing round, track who is still alive and end early when one remains. Connect to each Humanoid.Died at the start of the round:
local function runRound()
local alive = {}
for _, player in Players:GetPlayers() do
alive[player] = true
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Died:Connect(function()
alive[player] = nil
end)
end
end
local elapsed = 0
while elapsed < ROUND_TIME do
local survivors = 0
for _ in alive do
survivors += 1
end
if survivors <= 1 then
break
end
task.wait(1)
elapsed += 1
end
for player in alive do
return player -- winner (nil on timeout or mutual elimination)
end
return nil
end
Award the winner on the server — coins through your leaderstats values or a persistent win counter saved with the DataStore guide pattern. Never hand the reward through a RemoteEvent the client requests; the server decides who won and what they get.
For other win conditions the same skeleton applies: collect scores during the round, then pick the highest score at the end instead of tracking eliminations.
How do I reset everything between rounds?
Rounds corrupt each other through leftovers: dead characters, arena clones, stale alive-tables, and GUIs still showing the last winner. Reset deliberately in the Results phase:
- Move every surviving character back to the lobby (or respawn them by setting
player:LoadCharacter()). :Destroy()the cloned arena model so the next round builds a fresh one.- Clear per-round state tables and disconnect any connections you stored, so
Diedhandlers from the last round cannot touch the new one. - Reset scores or round-only stats, but never wipe persistent values like coins.
local function cleanupRound(arena, connections)
for _, connection in connections do
connection:Disconnect()
end
if arena then
arena:Destroy()
end
for _, player in Players:GetPlayers() do
local character = player.Character
if character then
character:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 3, 0))
end
end
end
Destroying the arena clone matters even if the next round reuses the same map: leftover dropped tools, welded debris, and lingering Touched connections all live inside that model.
How do I support multiple maps?
Put your map models in a folder (for example ReplicatedStorage.Maps), pick one at random per round, clone it into workspace, and teleport players to its spawn points. A ModuleScript keeps the tuning in one place and is the natural next step once the single-map version works — see the ModuleScript guide for how to require it from the round script.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local mapsFolder = ReplicatedStorage:WaitForChild("Maps")
local function startArena()
local templates = mapsFolder:GetChildren()
if #templates == 0 then
return nil
end
local choice = templates[math.random(1, #templates)]
local arena = choice:Clone()
arena.Parent = workspace
return arena
end
Random selection with math.random plus a full cleanup pass means players never see the same map twice in a row by accident of stale state, and adding a new map is just dropping a model into the folder.
Common mistakes that break round systems
- Counting down on the client. Each screen drifts and exploiters can freeze their display; keep the counter on the server and replicate it.
- Teleporting without checking the character. Players leave or respawn mid-teleport; guard with
characterandHumanoidRootPartchecks (andHumanoid.Health > 0) beforePivotTo. - Blocking the server at startup. A busy-wait loop without
task.wait, or legacywaitcalls, stalls the whole script and makes the lobby unresponsive — always yield withtask.wait. - Letting mid-round joins teleport in. A player who joins during InRound gets spawned mid-fight or counted as alive instantly; hold them in the lobby and only include players present when the round began.
- Skipping cleanup. Stale arena clones, undisconnected
Diedconnections, and old alive-tables leak into the next round and cause "two winners" or double-reward bugs. - Trusting the client for results. Rewards, winner names, and score changes must be decided server-side; anything else is trivially exploitable.
Learn Luau by doing
A round system is really one loop, three states, and disciplined cleanup — the kind of structure that only becomes natural by writing it. On Luablox you can practice the language pieces on /theory, drill the loops and tables this design leans on at /problems, and follow the Luablox lessons from first script to full gameplay systems.
Frequently asked questions
How do I make a countdown timer everyone sees?
Store the time left on the server — for example an attribute on ReplicatedStorage updated once per second by the round loop — and have LocalScripts display it via GetAttributeChangedSignal. Never let each client count on its own, because the displays drift apart and exploiters can freeze their own timer.
How do I teleport all players to the arena at round start?
Iterate over Players:GetPlayers(), verify each player has a character with a HumanoidRootPart and a living Humanoid, then call character:PivotTo on the chosen spawn point CFrame with a small upward offset. PivotTo moves the whole character at once and is the modern replacement for setting body-part CFrames.
How do I stop players who join mid-round from interfering?
Only include players who were present when the round started: snapshot the player list at teleport time and leave everyone else in the lobby (or in spectator mode) until the next Intermission. Also handle PlayerAdded so late joiners get teleported to the lobby spawn instead of the arena.
How many players do I need before a round starts?
That is your MIN_PLAYERS setting — two for most games. Stay in the Intermission state and re-check #Players:GetPlayers() about once per second instead of blocking, so the server keeps responding while it waits for the lobby to fill.
How do I end the round early when only one player is left?
Track alive players in a table and clear each entry from a Humanoid.Died connection. Poll the table every second (or check on each death) and break the round loop as soon as one or zero survivors remain, then run your normal Results and cleanup phases.
Learn core Roblox scripting and Luau programming concepts, including: roblox round system tutorial, roblox intermission timer, roblox teleport players to arena, last man standing roblox script, roblox game loop script.