Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/brockmartin/roblox-game-skill/llms.txt

Use this file to discover all available pages before exploring further.

Obstacle courses are one of Roblox’s most proven formats. The core loop — attempt, fail, learn, succeed, move on — creates a tight mastery-based engagement cycle that works for all ages. The genre is deceptively simple to expand: adding more stages is purely additive. This template covers the entire backbone: server-authoritative checkpoint persistence with DataStore saving, a stage completion pipeline that advances players to the next stage, five distinct obstacle types with configurable attributes, a per-player speedrun timer synced to the client, and an OrderedDataStore-backed global leaderboard for fastest times.

Core Systems

Checkpoint System

Saves stage and checkpoint number to DataStore on every new checkpoint hit. On join, players respawn at their saved position. On death, CharacterAdded teleports to the last saved CFrame.

Stage Progression

StageClear trigger parts detect stage completion and teleport players to the first checkpoint of the next stage. Stage and checkpoint are always ahead-only — no regression.

Five Obstacle Types

Kill bricks, moving platforms, disappearing platforms, spinning obstacles, and conveyor belts — all driven by a single ObstacleManager that scans stage folders on startup.

Speedrun Timer

Per-player stage and overall timers tracked server-side with os.clock(). Synced to client every second via TimerSync remote. Stage time recorded on completion.

Global Leaderboard

OrderedDataStore per stage for fastest times. UpdateAsync only writes if the new time beats the player’s personal best. Top 10 fetched via GetSortedAsync.

DataStore Persistence

Progress saves on PlayerRemoving and loads on PlayerAdded. New players start at Stage 1, Checkpoint 0.

Key Code: Checkpoint System

The checkpoint server script handles loading saved progress, wiring up all checkpoint touch events across every stage folder, and detecting stage clears — all in one self-contained module.
-- ServerScriptService/CheckpointSystem.server.luau

local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")

local progressStore = DataStoreService:GetDataStore("ObbyProgress_v1")
local stagesFolder = workspace:WaitForChild("Stages")

-- In-memory state per player
local playerCheckpoints: { [Player]: { stage: number, checkpoint: number, spawnCFrame: CFrame } } = {}

local function getSpawnCFrame(checkpointPart: BasePart): CFrame
    return checkpointPart.CFrame + Vector3.new(0, 4, 0)
end

-- Load saved progress when a player joins
local function onPlayerAdded(player: Player)
    local savedStage = 1
    local savedCheckpoint = 0

    local success, data = pcall(function()
        return progressStore:GetAsync("player_" .. player.UserId)
    end)

    if success and data then
        savedStage = data.stage or 1
        savedCheckpoint = data.checkpoint or 0
    end

    -- Find the spawn position for the saved checkpoint
    local stageFolder = stagesFolder:FindFirstChild("Stage_" .. savedStage)
    local spawnCFrame = CFrame.new(0, 10, 0) -- fallback

    if stageFolder then
        local checkpointsFolder = stageFolder:FindFirstChild("Checkpoints")
        if checkpointsFolder and savedCheckpoint > 0 then
            local cp = checkpointsFolder:FindFirstChild("Checkpoint_" .. savedCheckpoint)
            if cp then
                spawnCFrame = getSpawnCFrame(cp)
            end
        else
            local firstCp = checkpointsFolder and checkpointsFolder:FindFirstChild("Checkpoint_1")
            if firstCp then
                spawnCFrame = getSpawnCFrame(firstCp)
            end
        end
    end

    playerCheckpoints[player] = {
        stage = savedStage,
        checkpoint = savedCheckpoint,
        spawnCFrame = spawnCFrame,
    }

    -- Teleport player to their saved checkpoint on first spawn
    player.CharacterAdded:Connect(function(character)
        local hrp = character:WaitForChild("HumanoidRootPart")
        task.wait(0.1) -- let physics settle
        local state = playerCheckpoints[player]
        if state then
            hrp.CFrame = state.spawnCFrame
        end
    end)
end

Key Code: Five Obstacle Types

All five obstacle types are initialised by ObstacleManager scanning stage folders. Parts are identified by name prefix (e.g. Kill_, Moving_, Disappear_, Spin_, Conveyor_) or a KillBrick attribute.
-- ServerScriptService/ObstacleManager.server.luau
-- OBSTACLE TYPE 3: Disappearing Platforms

local function setupDisappearingPlatforms()
    for _, stageFolder in stagesFolder:GetChildren() do
        local obstacles = stageFolder:FindFirstChild("Obstacles")
        if not obstacles then continue end

        for _, part: BasePart in obstacles:GetDescendants() do
            if not part:IsA("BasePart") or not part.Name:match("^Disappear_") then continue end

            part.Anchored = true
            local originalTransparency = part.Transparency
            local originalColor = part.Color

            -- Configurable timing via attributes
            local fadeDelay = part:GetAttribute("FadeDelay") or 0.5
            local disappearDuration = part:GetAttribute("DisappearDuration") or 3

            local isActive = true

            part.Touched:Connect(function(hit)
                if not isActive then return end

                local player = Players:GetPlayerFromCharacter(hit.Parent)
                if not player then return end

                isActive = false

                -- Warning flash: turn red briefly
                part.Color = Color3.fromRGB(255, 80, 80)
                task.wait(fadeDelay)

                -- Fade out
                local fadeTween = TweenService:Create(part, TweenInfo.new(0.3), {
                    Transparency = 1,
                })
                fadeTween:Play()
                fadeTween.Completed:Wait()

                part.CanCollide = false

                -- Wait then reappear
                task.wait(disappearDuration)

                part.CanCollide = true
                part.Color = originalColor
                local reappearTween = TweenService:Create(part, TweenInfo.new(0.3), {
                    Transparency = originalTransparency,
                })
                reappearTween:Play()
                reappearTween.Completed:Wait()

                isActive = true
            end)
        end
    end
end

Key Code: Global Leaderboard

Times are stored in milliseconds in an OrderedDataStore. UpdateAsync ensures only personal-best times are written, and GetSortedAsync fetches the top 10 in ascending order (lowest time = fastest).
-- ServerScriptService/LeaderboardManager.server.luau

-- Submit a time (only saves if it beats the player's previous best)
local function submitTime(player: Player, stageNumber: number, timeSeconds: number)
    local store = getStageLeaderboard(stageNumber)
    local key = "player_" .. player.UserId
    local timeMs = math.floor(timeSeconds * 1000)

    pcall(function()
        store:UpdateAsync(key, function(oldValue)
            if oldValue == nil or timeMs < oldValue then
                return timeMs
            end
            return oldValue
        end)
    end)
end

-- Fetch top times for a stage
local function getTopTimes(stageNumber: number): { { userId: number, timeMs: number } }
    local store = getStageLeaderboard(stageNumber)
    local results = {}

    local success, pages = pcall(function()
        return store:GetSortedAsync(true, LEADERBOARD_SIZE)
    end)

    if success and pages then
        local entries = pages:GetCurrentPage()
        for _, entry in entries do
            local odString = entry.key or ""
            local odUserId = tonumber(odString:match("player_(%d+)"))
            table.insert(results, {
                userId = odUserId,
                timeMs = entry.value,
            })
        end
    end

    return results
end

Architecture

ScriptPurpose
CheckpointSystem.server.luauCheckpoint touch detection, DataStore save/load, respawn positioning
ObstacleManager.server.luauAll five obstacle type behaviours — scans Workspace on start
TimerSystem.server.luauPer-player stage and overall timers, periodic TimerSync remote fires
LeaderboardManager.server.luauOrderedDataStore reads/writes for stage and overall fastest times
StageManager.server.luauStage completion detection, advancing players, tracking current stage

Stage Difficulty Curve

Stage RangeDifficultyMechanics Introduced
1–10BeginnerBasic jumps, simple gaps
11–25EasyKill bricks (stationary), longer jumps
26–50MediumMoving platforms, disappearing platforms
51–80HardSpinning obstacles, conveyors, combinations
81–100ExpertMulti-mechanic gauntlets, precision timing
100+Master/BonusSecret stages, extreme difficulty
Streaming tip: Enable Workspace.StreamingEnabled = true for obbys with many stages. Set ModelStreamingMode to Opportunistic on non-adjacent stage folders and Persistent for the current and next stage. Players only need to load the stage they are on — this dramatically reduces memory and load times on mobile.

How to Use This Template

Build me a 50-stage space-themed obby. Stages 1–20 are on a space station,
stages 21–40 are in zero-gravity asteroid fields, and stages 41–50 are inside
a black hole with neon obstacles. Add a speedrun timer and global leaderboard.
See the New Game workflow for the complete 8-step build process.

Build docs developers (and LLMs) love