Prompt Details
Model
Chat - (gpt-6-astra)
Token size
1,738
Example input
[GAME IDEA, e.g. "an obby with moving platforms and a prize at the end"]: an obby with moving platforms, spinning bars and a golden trophy at the end
[obby / tycoon / simulator / round-based minigame / other]: obby
[never used Roblox Studio / made a few things / comfortable with Lua]: never used Roblox Studio
[MUST-HAVES, e.g. "checkpoints, coin counter, leaderboard", or "none"]: heckpoints, a timer that shows how long the run took
[SIZE: small, 5 minutes of play / medium, 15-30 minutes]: small, 5 minutes of play
Example output
Skybound Sprint: build plan and full scripts
1. The game in 3 lines
- Play: Run, jump across floating platforms, dodge spinning bars, and survive tricky gaps.
- Goal: Reach the golden trophy at the end of a short obstacle course. Checkpoints save your progress when you fall.
- Core loop: Start the timer, clear obstacles, respawn at checkpoints when needed, finish the course, and try to beat your best time.
This first version is designed for a beginner and takes about five minutes to play. The scripts build the obstacle course automatically, so you won't need to create dozens of platforms by hand.
2. Build this in Studio first
You only need to create four objects by hand. The server script will build the platforms, checkpoints, spinning bars, moving platforms, finish area, and trophy.
Step 1: Create the project
1. Open Roblox Studio and sign in.
2. On the start screen, click New and choose Baseplate.
3. If Explorer isn't visible, click View at the top, then click Explorer. Open Properties from the same menu if needed.
4. In Explorer, expand Workspace. Select the default SpawnLocation if there is one, and delete it using the Delete key. Keep the Baseplate for now; the script will remove it.
Step 2: Create the objects
In Explorer, right-click the indicated parent and choose Insert Object, or hover over the parent and click the + button. Search for the object type, then select it.
1. Workspace β StartSpawn
Type: SpawnLocation
- Anchored: true
- CanCollide: true
- Transparency: 0
- Size: 12, 1, 12
- Position: 0, 4, 0
- Neutral: true
2. ServerScriptService β ObbyServer
Type: Script
Leave the default script settings. This script builds the course and handles checkpoints, the timer, and wins.
3. StarterGui β RunTimer
Type: ScreenGui
Set ResetOnSpawn to false. This keeps the timer visible when your character respawns.
4. RunTimer β TimerClient
Type: LocalScript
The LocalScript will create the timer display automatically.
What the finished Explorer tree looks like
Workspace
βββ Baseplate (Part) [removed by script]
βββ StartSpawn (SpawnLocation)
ServerScriptService
βββ ObbyServer (Script)
StarterGui
βββ RunTimer (ScreenGui)
βββ TimerClient (LocalScript)
You do not need to make any other objects manually. The server script creates everything else during Play, including the leaderstats folder and its Wins value.
3. Scripts
Create the scripts in the order below. For each one, double-click its name in Explorer, select all existing text in the editor, and replace it with the complete code.
1. ObbyServer
Type: Script
Put it in: ServerScriptService
What it does: Builds the whole course and handles moving platforms, spinning hazards, checkpoints, run timing, deaths, and wins. The server controls all important game state.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local startSpawn = workspace:WaitForChild("StartSpawn")
local course = Instance.new("Folder")
course.Name = "ObbyCourse"
course.Parent = workspace
local checkpoints = {}
local touchDebounce = {}
local finishDebounce = {}
local movingPlatforms = {}
local spinningBars = {}
local PLATFORM_Y = 4
local PLATFORM_SIZE = Vector3.new(10, 1, 10)
local COURSE_X = {0, 18, 36, 54, 72, 90, 108, 126, 144}
-- Remove the default floor and any extra spawn locations.
local baseplate = workspace:FindFirstChild("Baseplate")
if baseplate then
baseplate:Destroy()
end
for _, object in ipairs(workspace:GetDescendants()) do
if object:IsA("SpawnLocation") and object ~= startSpawn then
object:Destroy()
end
end
startSpawn.Anchored = true
startSpawn.CanCollide = true
startSpawn.Size = PLATFORM_SIZE
startSpawn.Position = Vector3.new(0, PLATFORM_Y, 0)
startSpawn.Color = Color3.fromRGB(70, 200, 120)
startSpawn.Neutral = true
startSpawn.Duration = 0
local function makePart(name, size, position, color, parent)
local part = Instance.new("Part")
part.Name = name
part.Size = size
part.Position = position
part.Anchored = true
part.CanCollide = true
part.Color = color
part.Material = Enum.Material.SmoothPlastic
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Parent = parent
return part
end
local function makePlatform(x, color)
return makePart(
"Platform",
PLATFORM_SIZE,
Vector3.new(x, PLATFORM_Y, 0),
color,
course
)
end
local function getPlayerFromHit(hit)
local character = hit:FindFirstAncestorOfClass("Model")
if not character then
return nil
end
return Players:GetPlayerFromCharacter(character)
end
local function setCheckpoint(player, position)
checkpoints[player] = position
player:SetAttribute("CheckpointPosition", position)
end
local function makeCheckpoint(x, number)
local pad = makePart(
"Checkpoint" .. number,
Vector3.new(7, 0.35, 7),
Vector3.new(x, PLATFORM_Y + 0.7, 0),
Color3.fromRGB(0, 220, 255),
course
)
pad.Material = Enum.Material.Neon
pad.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player or touchDebounce[player] then
return
end
touchDebounce[player] = true
setCheckpoint(player, Vector3.new(x, PLATFORM_Y + 4, 0))
player:SetAttribute("CheckpointMessage", "Checkpoint " .. number .. " saved!")
task.spawn(function()
task.wait(1)
touchDebounce[player] = nil
end)
end)
end
local function makeMovingPlatform(x, number)
local part = makePart(
"MovingPlatform" .. number,
Vector3.new(7, 1, 7),
Vector3.new(x, PLATFORM_Y + 0.5, 0),
Color3.fromRGB(255, 170, 0),
course
)
part.Material = Enum.Material.Metal
table.insert(movingPlatforms, {
part = part,
basePosition = part.Position,
phase = number * 1.7,
distance = 5,
speed = 1.2
})
end
local function makeSpinningBar(x, number)
local bar = makePart(
"SpinningBar" .. number,
Vector3.new(12, 0.8, 1),
Vector3.new(x, PLATFORM_Y + 2, 0),
Color3.fromRGB(255, 65, 65),
course
)
bar.Material = Enum.Material.Neon
table.insert(spinningBars, {
part = bar,
x = x,
angle = 0,
speed = 1.8
})
end
-- Build the fixed platforms.
for _, x in ipairs(COURSE_X) do
if x ~= 0 then
makePlatform(x, Color3.fromRGB(95, 120, 190))
end
end
-- Moving platforms bridge the gaps between fixed platforms.
makeMovingPlatform(27, 1)
makeMovingPlatform(63, 2)
makeMovingPlatform(99, 3)
-- Checkpoints are normal Parts, not SpawnLocations.
makeCheckpoint(54, 1)
makeCheckpoint(108, 2)
-- Spinning bars make three platforms more challenging.
makeSpinningBar(36, 1)
makeSpinningBar(90, 2)
makeSpinningBar(126, 3)
-- The finish platform and trophy.
local trophy = makePart(
"GoldenTrophy",
Vector3.new(3, 4, 3),
Vector3.new(144, PLATFORM_Y + 3, 0),
Color3.fromRGB(255, 210, 0),
course
)
trophy.Shape = Enum.PartType.Ball
trophy.Material = Enum.Material.Neon
trophy.CanCollide = false
local trophyTop = makePart(
"TrophyTop",
Vector3.new(4, 0.6, 4),
Vector3.new(144, PLATFORM_Y + 5.2, 0),
Color3.fromRGB(255, 230, 60),
course
)
trophyTop.Shape = Enum.PartType.Cylinder
trophyTop.Material = Enum.Material.Metal
trophyTop.CanCollide = false
-- Falling below the course resets the character.
local killZone = makePart(
"KillZone",
Vector3.new(400, 2, 100),
Vector3.new(72, -20, 0),
Color3.fromRGB(255, 0, 0),
course
)
killZone.Transparency = 1
killZone.CanCollide = false
killZone.Touched:Connect(function(hit)
local character = hit:FindFirstAncestorOfClass("Model")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
local function makeLeaderstats(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local wins = Instance.new("IntValue")
wins.Name = "Wins"
wins.Value = 0
wins.Parent = leaderstats
end
local function resetRun(player)
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", false)
player:SetAttribute("RunStartedAt", 0)
player:SetAttribute("RunElapsed", 0)
setCheckpoint(player, startSpawn.Position + Vector3.new(0, 4, 0))
if player.Character then
player.Character:PivotTo(CFrame.new(checkpoints[player]))
end
end
local function setupPlayer(player)
makeLeaderstats(player)
setCheckpoint(player, startSpawn.Position + Vector3.new(0, 4, 0))
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", false)
player:SetAttribute("RunStartedAt", 0)
player:SetAttribute("RunElapsed", 0)
player:SetAttribute("CheckpointMessage", "")
player.CharacterAdded:Connect(function(character)
local root = character:WaitForChild("HumanoidRootPart", 10)
if not root or player.Parent ~= Players then
return
end
local destination = checkpoints[player]
or (startSpawn.Position + Vector3.new(0, 4, 0))
character:PivotTo(CFrame.new(destination))
local humanoid = character:WaitForChild("Humanoid", 10)
if humanoid then
humanoid.Died:Connect(function()
-- The timer keeps running when the player dies.
-- Respawning returns the player to their last checkpoint.
end)
end
end)
if player.Character then
task.spawn(function()
local root = player.Character:WaitForChild("HumanoidRootPart", 10)
if root and player.Parent == Players then
player.Character:PivotTo(CFrame.new(checkpoints[player]))
end
end)
end
end
-- Start the timer when a player touches the start platform.
startSpawn.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player or player:GetAttribute("RunActive") then
return
end
if player:GetAttribute("RunFinished") then
return
end
player:SetAttribute("RunStartedAt", workspace:GetServerTimeNow())
player:SetAttribute("RunElapsed", 0)
player:SetAttribute("RunActive", true)
end)
trophy.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player or finishDebounce[player] then
return
end
if not player:GetAttribute("RunActive") then
return
end
finishDebounce[player] = true
local startedAt = player:GetAttribute("RunStartedAt") or 0
local elapsed = math.max(0, workspace:GetServerTimeNow() - startedAt)
player:SetAttribute("RunElapsed", elapsed)
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", true)
local leaderstats = player:FindFirstChild("leaderstats")
local wins = leaderstats and leaderstats:FindFirstChild("Wins")
if wins then
wins.Value += 1
end
-- Give the player time to see their result, then allow another run.
task.spawn(function()
task.wait(5)
if player.Parent == Players then
resetRun(player)
finishDebounce[player] = nil
end
end)
end)
Players.PlayerAdded:Connect(setupPlayer)
Players.PlayerRemoving:Connect(function(player)
checkpoints[player] = nil
touchDebounce[player] = nil
finishDebounce[player] = nil
end)
-- Handle players who joined before this script connected.
for _, player in ipairs(Players:GetPlayers()) do
if not player:FindFirstChild("leaderstats") then
setupPlayer(player)
end
end
-- Move platforms with matching assembly velocity so they can carry players.
RunService.Heartbeat:Connect(function()
local now = workspace:GetServerTimeNow()
for _, data in ipairs(movingPlatforms) do
local part = data.part
local oldPosition = part.Position
local offset = math.sin(now * data.speed + data.phase) * data.distance
local newPosition = data.basePosition + Vector3.new(0, 0, offset)
part.CFrame = CFrame.new(newPosition)
part.AssemblyLinearVelocity = (newPosition - oldPosition) / math.max(1 / 240, 1 / 60)
end
for _, data in ipairs(spinningBars) do
data.angle += data.speed / 60
data.part.CFrame = CFrame.new(data.x, PLATFORM_Y + 2, 0)
* CFrame.Angles(0, data.angle, 0)
end
end)
2. TimerClient
Type: LocalScript
Put it in: StarterGui β RunTimer
What it does: Creates a timer at the top of the screen, shows checkpoint messages, and displays your finish time before the next attempt.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local screenGui = script.Parent
screenGui.ResetOnSpawn = false
local label = Instance.new("TextLabel")
label.Name = "TimerLabel"
label.Size = UDim2.new(0, 340, 0, 58)
label.Position = UDim2.new(0.5, -170, 0, 20)
label.BackgroundColor3 = Color3.fromRGB(25, 30, 45)
label.BackgroundTransparency = 0.12
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.Text = "Touch START to begin!"
label.Parent = screenGui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = label
local message = Instance.new("TextLabel")
message.Name = "CheckpointMessage"
message.Size = UDim2.new(0, 380, 0, 42)
message.Position = UDim2.new(0.5, -190, 0, 88)
message.BackgroundTransparency = 1
message.TextColor3 = Color3.fromRGB(80, 240, 255)
message.TextScaled = true
message.Font = Enum.Font.GothamBold
message.Text = ""
message.Parent = screenGui
local lastCheckpointMessage = ""
local function formatTime(seconds)
local minutes = math.floor(seconds / 60)
local remainder = seconds - minutes * 60
return string.format("%02d:%05.2f", minutes, remainder)
end
RunService.RenderStepped:Connect(function()
local active = player:GetAttribute("RunActive")
local finished = player:GetAttribute("RunFinished")
local startedAt = player:GetAttribute("RunStartedAt") or 0
local elapsed = player:GetAttribute("RunElapsed") or 0
if active and startedAt > 0 then
elapsed = math.max(0, workspace:GetServerTimeNow() - startedAt)
label.Text = "TIME " .. formatTime(elapsed)
elseif finished then
label.Text = "FINISHED! " .. formatTime(elapsed)
else
label.Text = "Touch START to begin!"
end
local checkpointMessage = player:GetAttribute("CheckpointMessage") or ""
if checkpointMessage ~= lastCheckpointMessage then
lastCheckpointMessage = checkpointMessage
message.Text = checkpointMessage
if checkpointMessage ~= "" then
task.spawn(function()
task.wait(2)
if message.Text == checkpointMessage then
message.Text = ""
end
end)
end
end
end)
1. ObbyServer (final version)
Use this complete version in ServerScriptService β ObbyServer. It uses the actual frame duration when updating moving-platform velocity, making the movement less dependent on frame rate.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local startSpawn = workspace:WaitForChild("StartSpawn")
local oldCourse = workspace:FindFirstChild("ObbyCourse")
if oldCourse then
oldCourse:Destroy()
end
local course = Instance.new("Folder")
course.Name = "ObbyCourse"
course.Parent = workspace
local checkpoints = {}
local touchDebounce = {}
local finishDebounce = {}
local movingPlatforms = {}
local spinningBars = {}
local PLATFORM_Y = 4
local PLATFORM_SIZE = Vector3.new(10, 1, 10)
local COURSE_X = {0, 18, 36, 54, 72, 90, 108, 126, 144}
local baseplate = workspace:FindFirstChild("Baseplate")
if baseplate then
baseplate:Destroy()
end
for _, object in ipairs(workspace:GetDescendants()) do
if object:IsA("SpawnLocation") and object ~= startSpawn then
object:Destroy()
end
end
startSpawn.Anchored = true
startSpawn.CanCollide = true
startSpawn.Size = PLATFORM_SIZE
startSpawn.Position = Vector3.new(0, PLATFORM_Y, 0)
startSpawn.Color = Color3.fromRGB(70, 200, 120)
startSpawn.Neutral = true
startSpawn.Duration = 0
local function makePart(name, size, position, color)
local part = Instance.new("Part")
part.Name = name
part.Size = size
part.Position = position
part.Anchored = true
part.CanCollide = true
part.Color = color
part.Material = Enum.Material.SmoothPlastic
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.Parent = course
return part
end
local function getPlayerFromHit(hit)
local character = hit:FindFirstAncestorOfClass("Model")
if not character then
return nil
end
return Players:GetPlayerFromCharacter(character)
end
local function setCheckpoint(player, position)
checkpoints[player] = position
player:SetAttribute("CheckpointPosition", position)
end
local function makeCheckpoint(x, number)
local pad = makePart(
"Checkpoint" .. number,
Vector3.new(7, 0.35, 7),
Vector3.new(x, PLATFORM_Y + 0.7, 0),
Color3.fromRGB(0, 220, 255)
)
pad.Material = Enum.Material.Neon
pad.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player or touchDebounce[player] then
return
end
touchDebounce[player] = true
setCheckpoint(player, Vector3.new(x, PLATFORM_Y + 4, 0))
player:SetAttribute("CheckpointMessage", "Checkpoint " .. number .. " saved!")
task.spawn(function()
task.wait(1)
touchDebounce[player] = nil
end)
end)
end
local function makeMovingPlatform(x, number)
local part = makePart(
"MovingPlatform" .. number,
Vector3.new(7, 1, 7),
Vector3.new(x, PLATFORM_Y + 0.5, 0),
Color3.fromRGB(255, 170, 0)
)
part.Material = Enum.Material.Metal
table.insert(movingPlatforms, {
part = part,
basePosition = part.Position,
phase = number * 1.7,
distance = 5,
speed = 1.2
})
end
local function makeSpinningBar(x, number)
local bar = makePart(
"SpinningBar" .. number,
Vector3.new(12, 0.8, 1),
Vector3.new(x, PLATFORM_Y + 2, 0),
Color3.fromRGB(255, 65, 65)
)
bar.Material = Enum.Material.Neon
table.insert(spinningBars, {
part = bar,
x = x,
angle = 0,
speed = 1.8
})
end
-- Build the course.
for _, x in ipairs(COURSE_X) do
if x ~= 0 then
makePart(
"Platform",
PLATFORM_SIZE,
Vector3.new(x, PLATFORM_Y, 0),
Color3.fromRGB(95, 120, 190)
)
end
end
makeMovingPlatform(27, 1)
makeMovingPlatform(63, 2)
makeMovingPlatform(99, 3)
makeCheckpoint(54, 1)
makeCheckpoint(108, 2)
makeSpinningBar(36, 1)
makeSpinningBar(90, 2)
makeSpinningBar(126, 3)
local trophy = makePart(
"GoldenTrophy",
Vector3.new(3, 4, 3),
Vector3.new(144, PLATFORM_Y + 3, 0),
Color3.fromRGB(255, 210, 0)
)
trophy.Shape = Enum.PartType.Ball
trophy.Material = Enum.Material.Neon
trophy.CanCollide = false
local trophyTop = makePart(
"TrophyTop",
Vector3.new(4, 0.6, 4),
Vector3.new(144, PLATFORM_Y + 5.2, 0),
Color3.fromRGB(255, 230, 60)
)
trophyTop.Shape = Enum.PartType.Cylinder
trophyTop.Material = Enum.Material.Metal
trophyTop.CanCollide = false
local killZone = makePart(
"KillZone",
Vector3.new(400, 2, 100),
Vector3.new(72, -20, 0),
Color3.fromRGB(255, 0, 0)
)
killZone.Transparency = 1
killZone.CanCollide = false
killZone.Touched:Connect(function(hit)
local character = hit:FindFirstAncestorOfClass("Model")
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
local function makeLeaderstats(player)
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local wins = Instance.new("IntValue")
wins.Name = "Wins"
wins.Value = 0
wins.Parent = leaderstats
end
local function resetRun(player)
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", false)
player:SetAttribute("RunStartedAt", 0)
player:SetAttribute("RunElapsed", 0)
local startPosition = startSpawn.Position + Vector3.new(0, 4, 0)
setCheckpoint(player, startPosition)
if player.Character then
player.Character:PivotTo(CFrame.new(startPosition))
end
end
local function setupPlayer(player)
if player:FindFirstChild("leaderstats") then
return
end
makeLeaderstats(player)
setCheckpoint(player, startSpawn.Position + Vector3.new(0, 4, 0))
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", false)
player:SetAttribute("RunStartedAt", 0)
player:SetAttribute("RunElapsed", 0)
player:SetAttribute("CheckpointMessage", "")
player.CharacterAdded:Connect(function(character)
local root = character:WaitForChild("HumanoidRootPart", 10)
if not root or player.Parent ~= Players then
return
end
local destination = checkpoints[player]
or (startSpawn.Position + Vector3.new(0, 4, 0))
character:PivotTo(CFrame.new(destination))
end)
if player.Character then
task.spawn(function()
local character = player.Character
if not character then
return
end
local root = character:WaitForChild("HumanoidRootPart", 10)
if root and player.Parent == Players then
character:PivotTo(CFrame.new(checkpoints[player]))
end
end)
end
end
startSpawn.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player then
return
end
if player:GetAttribute("RunActive") or player:GetAttribute("RunFinished") then
return
end
player:SetAttribute("RunStartedAt", workspace:GetServerTimeNow())
player:SetAttribute("RunElapsed", 0)
player:SetAttribute("RunActive", true)
end)
trophy.Touched:Connect(function(hit)
local player = getPlayerFromHit(hit)
if not player or finishDebounce[player] then
return
end
if not player:GetAttribute("RunActive") then
return
end
finishDebounce[player] = true
local startedAt = player:GetAttribute("RunStartedAt") or 0
local elapsed = math.max(0, workspace:GetServerTimeNow() - startedAt)
player:SetAttribute("RunElapsed", elapsed)
player:SetAttribute("RunActive", false)
player:SetAttribute("RunFinished", true)
local leaderstats = player:FindFirstChild("leaderstats")
local wins = leaderstats and leaderstats:FindFirstChild("Wins")
if wins then
wins.Value += 1
end
task.spawn(function()
task.wait(5)
if player.Parent == Players then
resetRun(player)
finishDebounce[player] = nil
end
end)
end)
Players.PlayerAdded:Connect(setupPlayer)
Players.PlayerRemoving:Connect(function(player)
checkpoints[player] = nil
touchDebounce[player] = nil
finishDebounce[player] = nil
end)
for _, player in ipairs(Players:GetPlayers()) do
setupPlayer(player)
end
RunService.Heartbeat:Connect(function(dt)
local now = workspace:GetServerTimeNow()
for _, data in ipairs(movingPlatforms) do
local part = data.part
local oldPosition = part.Position
local offset = math.sin(now * data.speed + data.phase) * data.distance
local newPosition = data.basePosition + Vector3.new(0, 0, offset)
part.CFrame = CFrame.new(newPosition)
part.AssemblyLinearVelocity = (newPosition - oldPosition) / math.max(dt, 1 / 240)
end
for _, data in ipairs(spinningBars) do
data.angle += data.speed * dt
data.part.CFrame = CFrame.new(data.x, PLATFORM_Y + 2, 0)
* CFrame.Angles(0, data.angle, 0)
end
end)
2. TimerClient
Type: LocalScript
Put it in: StarterGui β RunTimer β TimerClient
Paste the following complete code:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local screenGui = script.Parent
screenGui.ResetOnSpawn = false
local label = Instance.new("TextLabel")
label.Name = "TimerLabel"
label.Size = UDim2.new(0, 340, 0, 58)
label.Position = UDim2.new(0.5, -170, 0, 20)
label.BackgroundColor3 = Color3.fromRGB(25, 30, 45)
label.BackgroundTransparency = 0.12
label.TextColor3 = Color3.fromRGB(255, 255, 255)
label.TextScaled = true
label.Font = Enum.Font.GothamBold
label.Text = "Touch START to begin!"
label.Parent = screenGui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = label
local message = Instance.new("TextLabel")
message.Name = "CheckpointMessage"
message.Size = UDim2.new(0, 380, 0, 42)
message.Position = UDim2.new(0.5, -190, 0, 88)
message.BackgroundTransparency = 1
message.TextColor3 = Color3.fromRGB(80, 240, 255)
message.TextScaled = true
message.Font = Enum.Font.GothamBold
message.Text = ""
message.Parent = screenGui
local lastCheckpointMessage = ""
local function formatTime(seconds)
local minutes = math.floor(seconds / 60)
local remainder = seconds - minutes * 60
return string.format("%02d:%05.2f", minutes, remainder)
end
RunService.RenderStepped:Connect(function()
local active = player:GetAttribute("RunActive")
local finished = player:GetAttribute("RunFinished")
local startedAt = player:GetAttribute("RunStartedAt") or 0
local elapsed = player:GetAttribute("RunElapsed") or 0
if active and startedAt > 0 then
elapsed = math.max(0, workspace:GetServerTimeNow() - startedAt)
label.Text = "TIME " .. formatTime(elapsed)
elseif finished then
label.Text = "FINISHED! " .. formatTime(elapsed)
else
label.Text = "Touch START to begin!"
end
local checkpointMessage = player:GetAttribute("CheckpointMessage") or ""
if checkpointMessage ~= lastCheckpointMessage then
lastCheckpointMessage = checkpointMessage
message.Text = checkpointMessage
if checkpointMessage ~= "" then
task.spawn(function()
task.wait(2)
if message.Text == checkpointMessage then
message.Text = ""
end
end)
end
end
end)
4. Test it
Step 1: Start your game
1. Click the View tab and make sure Explorer and Output are visible.
2. Click the Test tab at the top.
3. Click Play.
4. Wait for your character to spawn.
Expected result: the course appears, the timer is visible, and your character stands at the green starting platform. The timer may start immediately because your character touches the start platform on spawn.
Step 2: Test the obstacles
- Jump across the blue platforms.
- Try standing on an orange moving platform. It should move back and forth.
- Cross the red spinning bars carefully.
- Step onto the cyan checkpoint pads. You should see a checkpoint message.
- Fall off the course. Your character should respawn at the last checkpoint you touched, and the timer should continue.
Step 3: Test the finish
Reach the golden trophy.
Expected result:
- The timer stops.
- The display shows FINISHED! and your time.
- Your Wins value increases by one in the player list.
- After five seconds, your character returns to the start for another attempt.
Step 4: Fix common errors
Problem | What to check
Infinite yield possible on 'Workspace:WaitForChild("StartSpawn")' | Confirm the SpawnLocation is named exactly StartSpawn and is directly inside Workspace.
attempt to index nil with 'Position' | Confirm StartSpawn exists and that you pasted the complete server script into ServerScriptService.
Timer or checkpoint messages never appear | Confirm TimerClient is a LocalScript inside StarterGui β RunTimer, and RunTimer is a ScreenGui.
If something still doesn't work, click View β Output, copy the red error message, and use it to identify the failing script and line number.
5. Make it feel finished
Each of these touches takes less than 10 minutes.
1. Make checkpoints glow
About 2 minutes
In ObbyServer, inside makeCheckpoint, find pad.Material = Enum.Material.Neon. Add this line directly underneath:
pad.Transparency = 0.1
The cyan checkpoints become brighter and easier to see.
2. Add a finish celebration message
About 3 minutes
In TimerClient, find the line:
label.Text = "FINISHED! " .. formatTime(elapsed)
Replace that line with:
label.Text = "π COURSE COMPLETE! " .. formatTime(elapsed)
Players get a more exciting finish message alongside their time.
3. Make the timer stand out
About 2 minutes
In TimerClient, add this line immediately after label.TextScaled = true:
label.BackgroundColor3 = Color3.fromRGB(15, 100, 65)
The timer gets a green background that stands out from the course.
6. Next upgrades
1. Personal best timer β Save each player's fastest completion time.
2. More obstacles β Add disappearing platforms and narrow bridges.
3. Coins and rewards β Place collectible coins around difficult jumps.
4. A proper trophy room β Show completed runs and wins on a podium.
5. Persistent progress β Save wins and personal best times using DataStoreService, with safe server-side saving and loading.
Get this prompt + 9 more with Select$14 your first month, then $19/mo$14 first month, then $19/mo
View SelectBy purchasing this prompt, you agree to our terms of service
GPT-6-ASTRA
Have a Roblox game idea but don't know how to build it?
Most Roblox prompts stop at game ideas. This one gives you a small first version with a build plan and complete scripts to try in Roblox Studio.
- Exactly what to create in Studio, with names and places
- Complete Lua scripts, no "add your code here"
- Money and wins handled on the server, a good base for fair play
- Step-by-step test, common errors and fixes
- For obby, tycoon, simulator and round games
Beginner friendly.
...more
Added 1 day ago
