A player standing in the code-built arena facing the test dummy target, with the live ROUND status banner visible

Roblox · Medium

Round loop — intermission → arena → win condition → back to lobby

What it is

A server-driven round state machine for a 1-vs-many game: players wait in a lobby, get moved to an arena built entirely by code, win by killing a target, and are returned to intermission. Late joiners and respawns are routed into the live round instead of being stranded. Built in a day as a studio trial.

Under the hood

My role: sole author. Built as a quick trial for a small studio making a juggernaut-style round game; the arena and dummy are placeholders for their real map and enemies.

The loop is four states — Intermission → Round → Won → Intermission — owned by one server script, with the client only ever told which state it is in through a single RemoteEvent. The “different plane” in the brief is not a place teleport: the arena is a separate region of the same place, built by ArenaFactory at round start and destroyed at round end, so there is never a stale arena from a previous round to clean up and nothing for a player to walk into between rounds.

The part that separates a demo from something a studio can build on is the edge cases around the round boundary. A player who joins or respawns mid-round gets a CharacterAdded hook that pivots them into the arena instead of leaving them in the lobby watching a round they cannot reach. Those hooks are tracked per player and disconnected when the round ends or the player leaves — a round loop that leaks a connection per join is fine in a 5-minute test and a memory problem on a 12-hour server. The win condition is exactly one thing (dummyHumanoid.Died) and nothing else ends a round, so there is no ambiguity about which code path is allowed to transition state.

local function runRound()
	local arenaModel, dummyHumanoid = ArenaFactory.Build()
	local spawnPart = arenaModel:FindFirstChild("ArenaSpawn")
	local arenaCFrame = spawnPart and spawnPart.CFrame or arenaModel:GetPivot()

	teleportAllTo(arenaCFrame)
	Net.FireAll("RoundStateChanged", { state = "Round" })

	-- Anyone who spawns mid-round (late join / respawn) is sent
	-- straight into the arena instead of stranded in the lobby.
	local charConns = {}
	local function hookPlayer(player)
		charConns[player] = player.CharacterAdded:Connect(function(character)
			task.wait(0.5)
			character:PivotTo(arenaCFrame)
		end)
	end
	for _, player in ipairs(Players:GetPlayers()) do hookPlayer(player) end
	local addedConn = Players.PlayerAdded:Connect(hookPlayer)

	-- Win condition: the dummy dies. Nothing else ends the round.
	dummyHumanoid.Died:Wait()

	addedConn:Disconnect()
	for _, conn in pairs(charConns) do conn:Disconnect() end

	Net.FireAll("RoundStateChanged", { state = "Won" })
	task.wait(WIN_HOLD_SECONDS)
	teleportAllTo(getLobbyCFrame())
	ArenaFactory.Destroy(arenaModel)
end

What it deliberately does not do: no matchmaking across servers, no round timer (the brief’s win condition was kill-based), no scoring. Each of those is a state or a field away, which is the point of keeping the loop this small.

Round loop — a round in progress
Source

← Back to work