How to Make an Obby in Roblox Studio: Step by Step, With Scripts (2026)

Build a Roblox obby from an empty Baseplate to a published game: stages, kill bricks, checkpoints that save, moving platforms that carry players, a speed-run timer and a finish line, with every script included.

Updated By Alex · Founder, NPCMotion15 min read
Example output · R15
Text to motion

“Look scared then turn and run away with big dramatic strides”

Try this prompt
TL;DR
  • An obby is a Roblox obstacle course: jump from stage to stage, respawn at your last checkpoint when you fall, and race to the end.
  • You need four scripts: kill bricks (one script for all of them, using tags), checkpoints with saving, a moving platform, and a finish. All are below, ready to paste.
  • Use SpawnLocations as checkpoints and set Player.RespawnLocation, the approach Roblox’s own tutorials use. Save the stage with UpdateAsync.
  • A tweened, anchored platform will not carry players. Drive an unanchored platform with a PrismaticConstraint instead.
  • End on something worth reaching: a victory animation and a cheering NPC make a finish line feel like a finish.

To make an obby in Roblox, build a row of stages out of parts, add checkpoints so players respawn at the last one they reached, add hazards that reset them, and publish. The building is the fun part. The scripts are where most first obbies break, so every one you need is here in full, with the reasoning behind it.

This guide starts from the Baseplate template so you see how every piece works. Roblox’s Classic Obby template ships with checkpoints, jump pads and hazards already made, and is a good next step once you know what is inside them. Note that it turns on Roblox’s new Character Controller Library by default; the scripts here are written for the standard Humanoid character.

How do you plan an obby before you build?

  • Start with ten stages. Ten finished stages beat fifty half-built ones, and you can add more after people play it.
  • Ramp the difficulty. Stages 1 to 3 should be impossible to fail. Introduce one new obstacle type every couple of stages, then combine them.
  • Pick a theme with a twist. Not “space obby” but “an obby where you are mouse-sized in a giant’s kitchen”. Our list of Roblox game ideas has more.
  • Keep gaps fair. A default character walks at 16 studs per second and clears a gap of about eight studs comfortably. Measure your early gaps and playtest every jump.

How do you build the stages?

  1. 1Open Studio and create a Baseplate place. Save it straight away with File › Save to Roblox so your work is in the cloud.
  2. 2Make folders. In the Explorer, add two Folders to the Workspace: Stages and Checkpoints. Put each stage’s parts in a Model inside Stages.
  3. 3Build with parts. Insert parts from the Home tab and shape them with Move, Scale and Rotate. Turn on snapping in the Model tab so platforms line up.
  4. 4Anchor everything that should not move. Select all your platforms and tick Anchored, or they will fall when the game starts.
  5. 5Colour by meaning. Players learn fast if red always kills, green is always safe and blue always moves. Keep the rule for the whole obby.
  6. 6Playtest constantly. Press F5 (Play) after every stage and run it yourself. If you fail a jump twice, players will fail it ten times.

How do you make a kill brick in Roblox?

Do not put a script in every lava part. Give each one a tag instead (select the part, then in Properties › Tags add KillBrick) and let one script handle them all:

Script in ServerScriptService
-- Script in ServerScriptService. Kills on touch for every part
-- tagged "KillBrick", including ones added while the game runs.
local CollectionService = game:GetService("CollectionService")

local function onTouched(otherPart)
	local character = otherPart:FindFirstAncestorOfClass("Model")
	local humanoid = character and character:FindFirstChildOfClass("Humanoid")
	if humanoid and humanoid.Health > 0 then -- the Health check is the debounce
		humanoid.Health = 0
	end
end

local function setUp(part)
	if part:IsA("BasePart") then
		part.Touched:Connect(onTouched)
	end
end

CollectionService:GetInstanceAddedSignal("KillBrick"):Connect(setUp)
for _, part in CollectionService:GetTagged("KillBrick") do
	setUp(part)
end

Setting Health to 0 kills even a player with a spawn ForceField. If you want spawn protection to save players, use humanoid:TakeDamage(humanoid.MaxHealth) instead, which respects ForceFields.

How do you make checkpoints that save progress?

Use a SpawnLocation for every checkpoint and set the player’s RespawnLocation when they reach it. That is the approach in Roblox’s own spawn tutorial, and it avoids filling the player list with a team per stage.

  1. 1Place a SpawnLocation at the start of each stage and move it into the Checkpoints folder.
  2. 2Name them in order: 1, 2, 3 and so on. The script reads the number from the name.
  3. 3Set their properties: Neutral on, AllowTeamChangeOnTouch off, Anchored on, and Duration to 0 so players do not get a ForceField at every checkpoint.
  4. 4Turn on data stores for testing: File › Experience Settings › Security › Enable Studio Access to API Services. Do this on a test copy, not your live game.
  5. 5Add the script below to ServerScriptService.
Script in ServerScriptService
-- Script in ServerScriptService. Needs a folder Workspace.Checkpoints
-- holding SpawnLocations named 1, 2, 3 ... in order.
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local store = DataStoreService:GetDataStore("ObbyProgress_v1")
local checkpoints = workspace:WaitForChild("Checkpoints")

local function checkpointFor(stage)
	local checkpoint = checkpoints:FindFirstChild(tostring(stage))
	if checkpoint and checkpoint:IsA("SpawnLocation") then
		return checkpoint
	end
	return nil
end

local function onPlayerAdded(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats" -- must be exactly this name
	local stage = Instance.new("IntValue")
	stage.Name = "Stage"
	stage.Value = 1
	stage.Parent = leaderstats
	leaderstats.Parent = player
	player.RespawnLocation = checkpointFor(1) -- set before the first spawn

	local ok, saved = pcall(function()
		return store:GetAsync("User_" .. player.UserId)
	end)
	if not ok then
		warn("Could not load progress for", player.Name, saved)
		player:SetAttribute("LoadFailed", true) -- never overwrite good data
		return
	end
	if typeof(saved) == "number" and checkpointFor(saved) then
		stage.Value = saved
		player.RespawnLocation = checkpointFor(saved)
		if player.Character then -- spawned before the data arrived
			player.Character:PivotTo(checkpointFor(saved).CFrame + Vector3.new(0, 4, 0))
		end
	end
end

for _, checkpoint in checkpoints:GetChildren() do
	if not checkpoint:IsA("SpawnLocation") then
		continue
	end
	checkpoint.Touched:Connect(function(hit)
		local player = Players:GetPlayerFromCharacter(hit.Parent)
		local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid")
		if not player or not humanoid or humanoid.Health <= 0 then
			return
		end
		local stage = player.leaderstats.Stage
		if tonumber(checkpoint.Name) == stage.Value + 1 then -- no skipping
			stage.Value += 1
			player.RespawnLocation = checkpoint
		end
	end)
end

local function save(player)
	if player:GetAttribute("LoadFailed") then
		return
	end
	local leaderstats = player:FindFirstChild("leaderstats")
	local stage = leaderstats and leaderstats:FindFirstChild("Stage")
	if not stage then
		return
	end
	local ok, err = pcall(function()
		store:UpdateAsync("User_" .. player.UserId, function(old)
			if typeof(old) == "number" and old >= stage.Value then
				return nil -- nil cancels the write: never save less progress
			end
			return stage.Value
		end)
	end)
	if not ok then
		warn("Could not save progress for", player.Name, err)
	end
end

Players.PlayerAdded:Connect(onPlayerAdded)
for _, player in Players:GetPlayers() do
	task.spawn(onPlayerAdded, player)
end
Players.PlayerRemoving:Connect(save)

game:BindToClose(function() -- the server waits up to 30 seconds for this
	local pending = 0
	for _, player in Players:GetPlayers() do
		pending += 1
		task.spawn(function()
			save(player)
			pending -= 1
		end)
	end
	while pending > 0 do
		task.wait()
	end
end)
  • leaderstats must be spelled exactly like that for the Stage column to appear on the leaderboard.
  • Checkpoints only count in order, so a player cannot skip from stage 2 to stage 9 by falling onto a lower platform.
  • UpdateAsync refuses to save a lower stage than the one already stored, so a failed load can never wipe someone’s progress.
  • BindToClose saves everyone when the server shuts down, which PlayerRemoving alone can miss.

How do you make a moving platform that carries players?

The obvious approach, tweening an anchored part, moves the platform out from under the player, because a CFrame change is not physics and the character is not carried along. Drive an unanchored platform with a constraint instead:

  1. 1Make an invisible base. An anchored part named Base, with Transparency 1 and CanCollide off, where the platform should start.
  2. 2Make the platform. An unanchored part, in the same position as the base.
  3. 3Add attachments. One Attachment in the base and one in the platform. Point both so their X axis runs along the direction of travel.
  4. 4Add a PrismaticConstraint to the base, with Attachment0 set to the base’s attachment and Attachment1 to the platform’s.
  5. 5Set it to Servo: ActuatorType Servo, Speed 10, ServoMaxForce large (10,000 or more), and LimitsEnabled on with limits that cover the travel distance.
  6. 6Add the script below inside the base.
Script inside the Base part
-- Script inside the anchored Base part, next to the PrismaticConstraint.
local prismatic = script.Parent:WaitForChild("PrismaticConstraint")
local platform = prismatic.Attachment1.Parent
platform:SetNetworkOwner(nil) -- simulated by the server: no jitter

while true do
	prismatic.TargetPosition = 20
	task.wait(4)
	prismatic.TargetPosition = 0
	task.wait(4)
end

What other obstacles can you add?

A conveyor belt. An anchored part with a velocity pushes anything standing on it without moving itself. Set it from a server Script:

Script inside the conveyor part
-- Script inside an anchored part. Pushes anything standing on it
-- along the part's front face at 12 studs per second.
local belt = script.Parent
belt.AssemblyLinearVelocity = belt.CFrame.LookVector * 12

A disappearing platform. It fades when touched, stops being solid, and comes back after three seconds, based on Roblox’s own fading trap tutorial:

Script inside the platform part
-- Script inside the platform part.
local TweenService = game:GetService("TweenService")
local platform = script.Parent
local busy = false

platform.Touched:Connect(function(hit)
	if busy or not hit.Parent:FindFirstChildOfClass("Humanoid") then
		return
	end
	busy = true
	local fade = TweenService:Create(platform, TweenInfo.new(0.6), { Transparency = 1 })
	fade:Play()
	fade.Completed:Wait()
	platform.CanCollide = false
	task.wait(3)
	platform.CanCollide = true
	platform.Transparency = 0
	busy = false
end)
  • Spinning bars use a HingeConstraint set to Motor on an unanchored bar.
  • Truss ladders need no script: insert a TrussPart and players climb it automatically.
  • Jump pads set an upward velocity on whoever touches them; the Classic Obby template includes one to study.
  • Wall jumps and wraps are pure building. Some of the best stages have no scripts at all.

How do you add a speed-run timer?

Keep the time on the server so nobody can fake a record, and only display it on the client. Add parts named StartPad and FinishPad, then:

Script in ServerScriptService
-- Script in ServerScriptService. Needs parts named StartPad and
-- FinishPad in the Workspace.
local Players = game:GetService("Players")

local function playerFrom(hit)
	return Players:GetPlayerFromCharacter(hit.Parent)
end

workspace.StartPad.Touched:Connect(function(hit)
	local player = playerFrom(hit)
	if player then
		player:SetAttribute("RunStart", workspace:GetServerTimeNow())
	end
end)

workspace.FinishPad.Touched:Connect(function(hit)
	local player = playerFrom(hit)
	local started = player and player:GetAttribute("RunStart")
	if not started then
		return
	end
	player:SetAttribute("RunStart", nil)
	local seconds = workspace:GetServerTimeNow() - started
	print(player.Name, string.format("finished in %.2f seconds", seconds))
end)
LocalScript inside a TextLabel
-- LocalScript inside a TextLabel in a ScreenGui in StarterGui.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local player = Players.LocalPlayer
local label = script.Parent

RunService.Heartbeat:Connect(function()
	local started = player:GetAttribute("RunStart")
	if started then
		label.Text = string.format("%.2f", workspace:GetServerTimeNow() - started)
	else
		label.Text = ""
	end
end)

From here a leaderboard is one step away: save each player’s best time with the same data store pattern as the checkpoints, or use an OrderedDataStore for a global top ten.

How do you make a finish line worth reaching?

Most obbies end on a flat pad and a chat message. The end is the moment players decide whether to tell their friends, so make it a moment: confetti particles, a badge, a winners’ room, and above all movement. A player who reaches the top should celebrate, and the place should celebrate back.

Script inside the FinishPad part
-- Script inside the FinishPad part. Plays a victory animation on
-- whoever reaches it. Put your own animation ID below.
local Players = game:GetService("Players")
local pad = script.Parent

local victory = Instance.new("Animation")
victory.AnimationId = "rbxassetid://YOUR_VICTORY_ID"

local celebrating = {}

pad.Touched:Connect(function(hit)
	local player = Players:GetPlayerFromCharacter(hit.Parent)
	local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid")
	local animator = humanoid and humanoid:FindFirstChildOfClass("Animator")
	if not player or not animator or celebrating[player] then
		return
	end
	celebrating[player] = true
	local track = animator:LoadAnimation(victory)
	track.Priority = Enum.AnimationPriority.Action
	track.Looped = false -- a looping track would never fire Stopped
	track:Play()
	track.Stopped:Wait()
	track:Destroy()
	celebrating[player] = nil
end)

Then add a cheering NPC beside the pad and a guide NPC waving at the start. Our guide to animating an NPC in Roblox has the script; the animations themselves you can generate.

How do you publish your obby?

  1. 1File › Publish to Roblox. Give it a name, a description and a creator (a group is recommended if anyone else will help), choose devices, and click Create.
  2. 2It starts private. In the Creator Dashboard, open the experience, then Configure › Settings › Audience, and choose Public, or Limited for friends and testers first.
  3. 3Complete the requirements. Making a game public needs an account in good standing, an age check and the content maturity questionnaire.
  4. 4Add a thumbnail and icon. These decide whether anyone clicks. Show a player mid-jump on your best-looking stage.
  5. 5Test on a phone. Many obby players are on mobile, where thumbstick movement can be slower than on a keyboard. If a jump is hard on a phone, widen the platform.

Why isn’t my obby working?

ProblemLikely causeFix
“Stage” never appears on the leaderboardThe folder is not named exactly leaderstatsRename it, all lower case
Stuck on Stage 1 in StudioData stores are off for StudioEnable Studio Access to API Services on a test copy
“API Services rejected request”The same setting, or an unpublished placePublish the place, then enable the setting
Respawning at the start after rejoiningThe stage loaded but RespawnLocation was not set, or the load failedUse the checkpoint script above and check the Output for load warnings
Kill brick does nothingThe tag is misspelled, or CanTouch is offTag it KillBrick exactly and tick CanTouch
Platforms fall when the game startsThey are not anchoredAnchor every static part
Moving platform slides out from under playersAn anchored part moved by a tweenUse the PrismaticConstraint setup
Checkpoint skippedCheckpoints named out of orderName them 1, 2, 3 in the order players reach them

Sources and further reading (checked Oct 6, 2026)

Frequently asked questions

Written by Alex, Founder, NPCMotion. Builds NPCMotion, the text and video to R15 animation tool for Roblox creators, and writes up the parts of the pipeline that are hardest to get right: retargeting, foot contacts and GPU inference.

Published

Keep reading

All guides