Articles › Game development

Roblox EP4 A complete obby: checkpoints, stage board, lava, finish line

Put it all together into a playable obby: SpawnLocation checkpoints, a leaderstats board, all lava run by one Script via Tags, and a finish line that counts wins.

Beginner 8 min read October 16, 2026 Your first Roblox game · lesson 4/5

What you will learn
  • Obby checkpoints are several SpawnLocations, with the server setting each player's RespawnLocation to the latest one passed
  • Many parts that behave the same get a Tag and one Script; change one place and the whole game follows
  • leaderstats is a specially named folder; put values in it and Roblox shows them as a leaderboard

Over three episodes we gathered every piece: a scene you can walk and jump through (EP1), lava and fading floors (EP2), and an understanding of where code runs (EP3). This episode assembles them into a real, playable obby from start to finish line.

The game you will have

  • 5 stages, each harder than the last
  • Checkpoints on every stage: fall in lava and you respawn at your latest stage, not the very start
  • A leaderboard top right, showing your stage and how many times you have won
  • Lava and fading floors, as many as you like, run by one piece of code
  • A finish line that gives +1 win and sends you back to the start to play again

Preparing the scene

1. Clear out the old. Delete the Scripts you put inside each floor in EP2, or the code will run twice.

2. Checkpoints. In Workspace, create a Folder called Checkpoints (hover Workspace → + → Folder) and put 5 SpawnLocations in it.

  • The Baseplate template already has one SpawnLocation. Drag it into Checkpoints and rename it 1. That is the start
  • Add four more from the Model tab → Spawn, named 2, 3, 4, 5, each at the start of the next stage
  • Set Duration to 0 on all of them (no protective bubble on spawn)
  • No SpawnLocation may sit outside this folder, or players may randomly spawn there

The names must be numbers, because the code uses the name as the stage number.

3. Lava. Place as many lava floors as you like, all anchored, then add a Tag: select the part, scroll to the Tags section in Properties, press + and type Lava. You can select many parts and tag them at once.

4. Fading floors. Same again, with the Tag FadePad.

5. Finish line. Create a Part in Workspace named Finish, anchored, and make it stand out, for example gold with the Neon material.

6. Settings. Use the Config ModuleScript in ReplicatedStorage from EP3; it needs FADE_SECONDS and GONE_SECONDS.

Designing stages that are fun

StageObstacleTip
1plain stairslet players get used to jumping
2slabs apart over lava5–6 studs apart, still easy
3fading floorsforce players to hurry
4a 1–2 stud bridge with lava on both sidestests precision
5everything combined, jumps near the limitthe last stage should be the hardest

The golden rule of obbies: every stage must be beatable. Test every one yourself; if it takes more than ten tries, most players will quit first.

touch a checkpoint → Stage = its number12345start★Finishlavarespawn at latest checkpointWins +1 · back to checkpoint 1
Stage only moves forward; falling in lava never sends you all the way back

The whole game: one Script in ServerScriptService

Hover ServerScriptService → + → Script, name it ObbyServer, and paste in all of this. Each part is explained below.

local Players = game:GetService("Players")
local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Config = require(ReplicatedStorage:WaitForChild("Config"))
local checkpoints = workspace:WaitForChild("Checkpoints")
local finish = workspace:WaitForChild("Finish")
local firstSpawn = checkpoints:WaitForChild("1")

-- Who touched? Returns the player (or nil if not a player) and the Humanoid.
local function playerFromHit(hit)
	local character = hit.Parent
	local humanoid = character and character:FindFirstChildWhichIsA("Humanoid")
	if not humanoid or humanoid.Health <= 0 then
		return nil, nil
	end
	return Players:GetPlayerFromCharacter(character), humanoid
end

-- 1) Leaderboard: Stage and Wins
Players.PlayerAdded:Connect(function(player)
	local stats = Instance.new("Folder")
	stats.Name = "leaderstats"

	local stage = Instance.new("IntValue")
	stage.Name = "Stage"
	stage.Value = 1
	stage.Parent = stats

	local wins = Instance.new("IntValue")
	wins.Name = "Wins"
	wins.Parent = stats

	stats.Parent = player
	player.RespawnLocation = firstSpawn
end)

-- 2) Checkpoints: SpawnLocations named 1, 2, 3 ... in the Checkpoints folder
for _, spawn in checkpoints:GetChildren() do
	local number = tonumber(spawn.Name)
	if spawn:IsA("SpawnLocation") and number then
		spawn.Touched:Connect(function(hit)
			local player = playerFromHit(hit)
			if not player then
				return
			end
			local stage = player.leaderstats.Stage
			if number > stage.Value then  -- forward only
				stage.Value = number
				player.RespawnLocation = spawn
			end
		end)
	end
end

-- 3) Lava: every part tagged "Lava"
local function setupLava(part)
	part.Touched:Connect(function(hit)
		local _, humanoid = playerFromHit(hit)
		if humanoid then
			humanoid.Health = 0
		end
	end)
end
for _, part in CollectionService:GetTagged("Lava") do
	setupLava(part)
end
CollectionService:GetInstanceAddedSignal("Lava"):Connect(setupLava)

-- 4) Fading floors: every part tagged "FadePad"
local function setupFade(pad)
	local busy = false  -- each pad has its own busy
	pad.Touched:Connect(function(hit)
		if busy or not playerFromHit(hit) then
			return
		end
		busy = true
		for i = 1, 10 do
			pad.Transparency = i / 10
			task.wait(Config.FADE_SECONDS / 10)
		end
		pad.CanCollide = false
		task.wait(Config.GONE_SECONDS)
		pad.CanCollide = true
		pad.Transparency = 0
		busy = false
	end)
end
for _, pad in CollectionService:GetTagged("FadePad") do
	setupFade(pad)
end
CollectionService:GetInstanceAddedSignal("FadePad"):Connect(setupFade)

-- 5) Finish line
local finishing = {}  -- stops double counting, per player

finish.Touched:Connect(function(hit)
	local player = playerFromHit(hit)
	if not player or finishing[player] then
		return
	end
	finishing[player] = true

	player.leaderstats.Wins.Value += 1
	player.leaderstats.Stage.Value = 1
	player.RespawnLocation = firstSpawn

	task.wait(1)
	local character = player.Character
	if character then
		character:PivotTo(firstSpawn.CFrame + Vector3.new(0, 4, 0))
	end
	finishing[player] = nil
end)

Players.PlayerRemoving:Connect(function(player)
	finishing[player] = nil
end)

Part by part

playerFromHit. Every part of the game asks the same question, “who touched?”, so it is one function used everywhere. It checks three things: is there a Humanoid, is it still alive, and is it a real player or just an NPC character (GetPlayerFromCharacter returns nil for non-players). Luau functions can return several values, so it hands back both the player and the Humanoid.

The leaderboard. A folder named leaderstats (all lowercase, spelled exactly) under a player makes Roblox show its values as the top-right board. The values must be value objects such as IntValue (whole numbers), and each one’s name becomes a column heading. Notice we fill everything in first and set stats.Parent = player last, so the board appears complete in one go.

Checkpoints. player.RespawnLocation is where the player respawns after dying. Set it, and Roblox handles respawning. The condition number > stage.Value makes progress forward-only: walking back over an old checkpoint does not move you back. tonumber(spawn.Name) turns the name "3" into the number 3; any other name gives nil and that spawn is skipped.

Tags and CollectionService. Instead of a Script inside every lava part, each part carries the label Lava, and CollectionService:GetTagged("Lava") returns every part with that label. GetInstanceAddedSignal catches parts added later during the game. Place another hundred lava parts and the code never changes; just tag them.

A developer’s habit worth having from day one: things that behave the same get one piece of code.

Each pad’s own busy. In setupFade, busy is created fresh each time the function is called, so every pad has its own. One pad fading does not freeze the others. Move local busy = false outside the function and they all share one, so stepping on one pad makes all the others ignore you.

The finish line. The finishing table is a debounce per player. With a single variable as in EP2, when one person crosses the line, someone arriving at the same moment would not be counted. character:PivotTo(...) moves the whole character; we put it 4 studs above the start so it does not sink into the floor. PlayerRemoving clears entries for people who leave, so the table does not keep growing.

Testing

  1. Press Play and look top right: a board with Stage = 1 and Wins = 0
  2. Touch checkpoint 2: Stage becomes 2
  3. Jump into lava: you respawn at checkpoint 2, not the start
  4. Reach Finish: Wins becomes 1 and you are sent back to the start
  5. Test with 2 players (Test → Clients and Servers) and have both cross the finish together: both get +1

If you spawn in the wrong place, check for a SpawnLocation left outside the folder. If lava does nothing, check the Tag is spelled Lava with the same capitals, and look for red text in Output.

Try it in 10 minutes

  1. Add stage 6 by placing a SpawnLocation named 6 in the folder; no code changes at all
  2. Add 10 more lava parts just by tagging them
  3. Set GONE_SECONDS in Config to 1 and replay stage 3; the whole game’s difficulty changes from one line

Common mistakes

  • Forgetting to delete the old Scripts inside each lava part, so things run twice
  • A SpawnLocation outside the folder, so players spawn in the wrong place at random
  • Spelling leaderstats with capitals, so the board never appears
  • One debounce for all players at the finish, so people arriving together are not counted
  • Naming checkpoints Stage2 instead of 2, so tonumber returns nil and that checkpoint does nothing

Next episode

The game is playable, but leave it and your stage and wins vanish. EP5 builds a second game, Lava Rising, with the whole level made in code and the rules tested outside Studio. Then EP6 saves player data between sessions with DataStore.

FAQ

If I leave and come back, is my progress gone?

For now, yes. Nothing is saved yet; everything lives in the server's memory. Saving between sessions with DataStore is EP5.

Why is there a bubble around me after respawning?

A SpawnLocation has a Duration, the time you get a ForceField after spawning, 10 seconds by default. An obby does not need it; set it to 0.

Commands and settings were tested on sample systems. Try them on a test machine before production.