Bridge Academy.
SUMMER CAMP OPTION · LEVEL I AND LEVEL II

Roblox Game Design

Roblox Game Design is an optional summer camp program for Level I and Level II students. Students build small game features in Roblox Studio by following a clear workflow, then copying tested scripts into the correct place.

FEATURE LIBRARY

Organized by game feature

Each feature starts with the Studio setup steps, then gives students one complete script to copy. The goal is to help students build working behavior first, while gradually connecting the script to familiar programming ideas like variables, conditions, loops, and functions.

READY FEATURE

Respawn an Object

Use this when a player drives, rides, or moves an object away from its starting place. When no matching object is within 25 studs of home, Roblox waits 5 seconds and spawns a fresh copy at the original location. The object that was driven away is left alone.

WORKFLOW FIRST

  1. Put the object in Workspace exactly where it should start.
  2. Duplicate that object and move the duplicate into ServerStorage.
  3. Make sure both objects have the exact same name.
  4. Open ServerScriptService, add a new Script, and paste the code below.
  5. Change only OBJECT_NAME, then press Play to test.

WHY IT WORKS

The script first looks for common moving parts, like a DriveSeat, VehicleSeat, Seat, or HumanoidRootPart. It checks whether any copy with the same name is still near the start, then clones the backup copy from ServerStorage only when the home spot is empty.

To respawn three different cars, make one script for each car and change OBJECT_NAME in each script.

COMPLETE SCRIPT
Paste into ServerScriptService
-- Put this Script in ServerScriptService.
-- Put a backup copy of the object in ServerStorage.
-- Roblox uses -- for comments. Python uses # for comments.

local ServerStorage = game:GetService("ServerStorage")

-- PART 1: Student settings
-- STUDENT CHANGE: Replace MyObject with the exact name of your car, plane, horse, or model.
local OBJECT_NAME = "MyObject"

-- STUDENT CHANGE: Try 2 for a faster respawn, or 10 for a slower respawn.
local RESPAWN_SECONDS = 5

-- STUDENT CHANGE:
-- Try a smaller number like 10 to respawn sooner.
-- Try a bigger number like 50 if the object is large.
local HOME_RADIUS = 25

-- PART 2: Good parts to track on moving objects
-- You usually do not need to change this list.
local TRACKING_PART_NAMES = {
    "DriveSeat",
    "DriverSeat",
    "HumanoidRootPart",
    "RootPart",
    "Torso",
}

-- PART 3: Find the backup object and the starting object
local templateObject = ServerStorage:FindFirstChild(OBJECT_NAME)

if not templateObject then
    warn("Missing backup object in ServerStorage: " .. OBJECT_NAME)
    return
end

local startingObject = workspace:FindFirstChild(OBJECT_NAME, true)

if not startingObject then
    warn("Missing starting object in Workspace: " .. OBJECT_NAME)
    return
end

local startCFrame

if startingObject:IsA("Model") then
    startCFrame = startingObject:GetPivot()
elseif startingObject:IsA("BasePart") then
    startCFrame = startingObject.CFrame
else
    warn(OBJECT_NAME .. " must be a Model or a Part")
    return
end

-- PART 4: Pick one part that tells us where the object is
-- Cars usually use a seat. Characters and animals often use HumanoidRootPart or Torso.
local function findTrackingPart(object)
    if object:IsA("BasePart") then
        return object
    end

    if not object:IsA("Model") then
        return nil
    end

    for _, partName in ipairs(TRACKING_PART_NAMES) do
        local part = object:FindFirstChild(partName, true)

        if part and part:IsA("BasePart") then
            return part
        end
    end

    local vehicleSeat = object:FindFirstChildWhichIsA("VehicleSeat", true)
    if vehicleSeat then
        return vehicleSeat
    end

    local seat = object:FindFirstChildWhichIsA("Seat", true)
    if seat then
        return seat
    end

    return object:FindFirstChildWhichIsA("BasePart", true)
end

local startPart = findTrackingPart(startingObject)

if not startPart then
    warn("Could not find a moving part inside: " .. OBJECT_NAME)
    return
end

local startPosition = startPart.Position
local isRespawning = false

-- PART 5: Check whether any copy of this object is still near home
local function objectIsAtStart()
    for _, thing in ipairs(workspace:GetDescendants()) do
        if thing.Name == OBJECT_NAME then
            local trackingPart = findTrackingPart(thing)

            if trackingPart then
                local distance = (trackingPart.Position - startPosition).Magnitude

                if distance < HOME_RADIUS then
                    return true
                end
            end
        end
    end

    return false
end

-- PART 6: Spawn a fresh copy at the starting place
-- This does NOT delete the object that was driven away.
local function spawnObject()
    local newObject = templateObject:Clone()

    if newObject:IsA("Model") then
        newObject:PivotTo(startCFrame)
    elseif newObject:IsA("BasePart") then
        newObject.CFrame = startCFrame
    end

    newObject.Parent = workspace
    print("Respawned: " .. OBJECT_NAME)
end

print("Watching object for respawn: " .. OBJECT_NAME)

-- PART 7: Main loop
-- Every second, check whether the home spot is empty.
while true do
    task.wait(1)

    if not isRespawning and not objectIsAtStart() then
        isRespawning = true
        task.wait(RESPAWN_SECONDS)

        if not objectIsAtStart() then
            spawnObject()
        end

        isRespawning = false
    end
end

READY FEATURE

Race Checkpoints

Use this when students build a simple race path. Players must touch Checkpoint1 through Checkpoint10 in order. Correct checkpoints show a green message, play a sound, and flash. Wrong checkpoints show a red message and tell the player which checkpoint to find next.

WORKFLOW FIRST

  1. Create a Folder in Workspace named RaceCheckpoints.
  2. Add 10 Parts inside the folder named Checkpoint1 through Checkpoint10.
  3. Place the Parts around the map in the order racers should drive through them.
  4. Make each checkpoint large enough to touch, then keep it transparent and non-colliding.
  5. Open ServerScriptService, add a new Script, and paste the code below.

WHY IT WORKS

The script gives each player their own next checkpoint number, so multiple students can race at the same time. It only counts a checkpoint if it is the exact next one in sequence.

It also creates floating number labels, screen messages, sounds, and checkpoint flashes from the script, so students do not need to build those effects by hand.

COMPLETE SCRIPT
Paste into ServerScriptService
local Players = game:GetService("Players")

-- PART 1: Student settings
-- STUDENT CHANGE: This folder must exist in Workspace.
local CHECKPOINT_FOLDER_NAME = "RaceCheckpoints"

-- STUDENT CHANGE: Change this if you want 5 checkpoints, 20 checkpoints, etc.
local CHECKPOINT_COUNT = 10

-- STUDENT CHANGE: After someone wins, wait this many seconds before resetting.
local RESET_AFTER_WIN_SECONDS = 5

-- STUDENT CHANGE: Sound played when the correct checkpoint is touched.
local CORRECT_SOUND_ID = "rbxassetid://12221967"

-- STUDENT CHANGE: Sound played when the wrong checkpoint is touched.
local WRONG_SOUND_ID = "rbxassetid://138090596"

-- STUDENT CHANGE: Sound played when the player wins.
local WIN_SOUND_ID = "rbxassetid://12222253"

-- STUDENT CHANGE: Try 0.3 for quiet, 1 for normal, or 2 for loud.
local SOUND_VOLUME = 1

-- STUDENT CHANGE: Change these colors to change the flash effect.
local CORRECT_FLASH_COLOR = Color3.fromRGB(80, 255, 120)
local WRONG_FLASH_COLOR = Color3.fromRGB(255, 80, 80)
local WIN_FLASH_COLOR = Color3.fromRGB(255, 215, 0)

-- STUDENT CHANGE: Bigger number means more flashes.
local FLASH_COUNT = 3

-- STUDENT CHANGE: Bigger number means slower flashing.
local FLASH_SPEED = 0.12

-- STUDENT CHANGE: Bigger number makes the checkpoint number float higher.
local NUMBER_HEIGHT = 5

-- STUDENT CHANGE: Change the number color.
local NUMBER_COLOR = Color3.fromRGB(255, 255, 255)

local checkpointFolder = workspace:WaitForChild(CHECKPOINT_FOLDER_NAME)

local raceWinner = nil
local lastTouchTime = {}

local function showFeedback(player, text, color)
	local playerGui = player:FindFirstChild("PlayerGui") or player:WaitForChild("PlayerGui", 2)

	if not playerGui then
		return
	end

	local gui = playerGui:FindFirstChild("RaceFeedbackGui")

	if not gui then
		gui = Instance.new("ScreenGui")
		gui.Name = "RaceFeedbackGui"
		gui.ResetOnSpawn = false
		gui.Parent = playerGui
	end

	local label = gui:FindFirstChild("Message")

	if not label then
		label = Instance.new("TextLabel")
		label.Name = "Message"
		label.Size = UDim2.new(0.6, 0, 0, 60)
		label.Position = UDim2.new(0.2, 0, 0.12, 0)
		label.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
		label.BackgroundTransparency = 0.25
		label.TextScaled = true
		label.Font = Enum.Font.GothamBold
		label.Visible = false
		label.Parent = gui
	end

	label.Text = text
	label.TextColor3 = color
	label.Visible = true

	task.delay(2, function()
		if label and label.Parent and label.Text == text then
			label.Visible = false
		end
	end)
end

local function playSound(player, soundId)
	local playerGui = player:FindFirstChild("PlayerGui") or player:WaitForChild("PlayerGui", 2)

	if not playerGui then
		return
	end

	local sound = Instance.new("Sound")
	sound.SoundId = soundId
	sound.Volume = SOUND_VOLUME
	sound.Parent = playerGui
	sound:Play()

	sound.Ended:Connect(function()
		sound:Destroy()
	end)

	task.delay(5, function()
		if sound and sound.Parent then
			sound:Destroy()
		end
	end)
end

local function flashCheckpoint(checkpoint, flashColor)
	if checkpoint:GetAttribute("IsFlashing") then
		return
	end

	checkpoint:SetAttribute("IsFlashing", true)

	local originalColor = checkpoint.Color
	local originalTransparency = checkpoint.Transparency
	local originalMaterial = checkpoint.Material

	task.spawn(function()
		for _ = 1, FLASH_COUNT do
			checkpoint.Color = flashColor
			checkpoint.Transparency = 0.1
			checkpoint.Material = Enum.Material.Neon

			task.wait(FLASH_SPEED)

			checkpoint.Color = originalColor
			checkpoint.Transparency = originalTransparency
			checkpoint.Material = originalMaterial

			task.wait(FLASH_SPEED)
		end

		checkpoint.Color = originalColor
		checkpoint.Transparency = originalTransparency
		checkpoint.Material = originalMaterial
		checkpoint:SetAttribute("IsFlashing", false)
	end)
end

local function addCheckpointNumber(checkpoint, checkpointNumber)
	local oldLabel = checkpoint:FindFirstChild("CheckpointNumberLabel")

	if oldLabel then
		oldLabel:Destroy()
	end

	local billboard = Instance.new("BillboardGui")
	billboard.Name = "CheckpointNumberLabel"
	billboard.Size = UDim2.new(0, 100, 0, 100)
	billboard.StudsOffset = Vector3.new(0, NUMBER_HEIGHT, 0)
	billboard.AlwaysOnTop = true
	billboard.Parent = checkpoint

	local textLabel = Instance.new("TextLabel")
	textLabel.Size = UDim2.new(1, 0, 1, 0)
	textLabel.BackgroundTransparency = 1
	textLabel.Text = tostring(checkpointNumber)
	textLabel.TextScaled = true
	textLabel.Font = Enum.Font.GothamBlack
	textLabel.TextColor3 = NUMBER_COLOR
	textLabel.TextStrokeTransparency = 0
	textLabel.TextStrokeColor3 = Color3.fromRGB(0, 0, 0)
	textLabel.Parent = billboard
end

local function setupPlayer(player)
	player:SetAttribute("NextCheckpoint", 1)
	player:SetAttribute("FinishedRace", false)

	local leaderstats = player:FindFirstChild("leaderstats")

	if not leaderstats then
		leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player
	end

	local checkpointStat = leaderstats:FindFirstChild("Checkpoint")

	if not checkpointStat then
		checkpointStat = Instance.new("IntValue")
		checkpointStat.Name = "Checkpoint"
		checkpointStat.Parent = leaderstats
	end

	checkpointStat.Value = 0

	local wins = leaderstats:FindFirstChild("Wins")

	if not wins then
		wins = Instance.new("IntValue")
		wins.Name = "Wins"
		wins.Parent = leaderstats
	end
end

local function findPlayerFromHit(hit)
	local current = hit

	while current and current ~= workspace do
		if current:IsA("Model") then
			local player = Players:GetPlayerFromCharacter(current)

			if player then
				return player
			end

			local vehicleSeat = current:FindFirstChildWhichIsA("VehicleSeat", true)

			if vehicleSeat and vehicleSeat.Occupant then
				return Players:GetPlayerFromCharacter(vehicleSeat.Occupant.Parent)
			end

			local seat = current:FindFirstChildWhichIsA("Seat", true)

			if seat and seat.Occupant then
				return Players:GetPlayerFromCharacter(seat.Occupant.Parent)
			end
		end

		current = current.Parent
	end

	return nil
end

local function resetRace()
	raceWinner = nil

	for _, player in ipairs(Players:GetPlayers()) do
		player:SetAttribute("NextCheckpoint", 1)
		player:SetAttribute("FinishedRace", false)

		local leaderstats = player:FindFirstChild("leaderstats")
		local checkpointStat = leaderstats and leaderstats:FindFirstChild("Checkpoint")

		if checkpointStat then
			checkpointStat.Value = 0
		end
	end

	print("Race reset. Start at Checkpoint1 again.")
end

local function handleCheckpointTouch(checkpointNumber, checkpoint, hit)
	if raceWinner then
		return
	end

	local player = findPlayerFromHit(hit)

	if not player then
		return
	end

	local touchKey = player.UserId .. "-" .. checkpointNumber
	local now = os.clock()

	if lastTouchTime[touchKey] and now - lastTouchTime[touchKey] < 1 then
		return
	end

	lastTouchTime[touchKey] = now

	local nextCheckpoint = player:GetAttribute("NextCheckpoint") or 1

	if checkpointNumber < nextCheckpoint then
		return
	end

	if checkpointNumber > nextCheckpoint then
		showFeedback(
			player,
			"Wrong checkpoint! Go to Checkpoint" .. nextCheckpoint,
			Color3.fromRGB(255, 80, 80)
		)

		playSound(player, WRONG_SOUND_ID)
		flashCheckpoint(checkpoint, WRONG_FLASH_COLOR)

		print(player.Name .. " touched Checkpoint" .. checkpointNumber .. " but needs Checkpoint" .. nextCheckpoint)
		return
	end

	local leaderstats = player:FindFirstChild("leaderstats")
	local checkpointStat = leaderstats and leaderstats:FindFirstChild("Checkpoint")

	if checkpointStat then
		checkpointStat.Value = checkpointNumber
	end

	showFeedback(
		player,
		"Good! Checkpoint " .. checkpointNumber .. "/" .. CHECKPOINT_COUNT,
		Color3.fromRGB(80, 255, 120)
	)

	playSound(player, CORRECT_SOUND_ID)

	print(player.Name .. " completed Checkpoint" .. checkpointNumber)

	if checkpointNumber == CHECKPOINT_COUNT then
		raceWinner = player
		player:SetAttribute("FinishedRace", true)

		local wins = leaderstats and leaderstats:FindFirstChild("Wins")

		if wins then
			wins.Value += 1
		end

		showFeedback(
			player,
			"You win the race!",
			Color3.fromRGB(255, 215, 0)
		)

		playSound(player, WIN_SOUND_ID)
		flashCheckpoint(checkpoint, WIN_FLASH_COLOR)

		print(player.Name .. " WINS THE RACE!")

		task.wait(RESET_AFTER_WIN_SECONDS)
		resetRace()
	else
		flashCheckpoint(checkpoint, CORRECT_FLASH_COLOR)
		player:SetAttribute("NextCheckpoint", checkpointNumber + 1)
	end
end

Players.PlayerAdded:Connect(setupPlayer)

for _, player in ipairs(Players:GetPlayers()) do
	setupPlayer(player)
end

for checkpointNumber = 1, CHECKPOINT_COUNT do
	local checkpoint = checkpointFolder:WaitForChild("Checkpoint" .. checkpointNumber)

	checkpoint.Anchored = true
	checkpoint.CanCollide = false
	checkpoint.Transparency = math.max(checkpoint.Transparency, 0.5)

	addCheckpointNumber(checkpoint, checkpointNumber)

	checkpoint.Touched:Connect(function(hit)
		handleCheckpointTouch(checkpointNumber, checkpoint, hit)
	end)
end

print("Race checkpoint system ready.")

READY FEATURE

Score Pad

Use this when students want a simple scoring object on the track. A player can walk into it or drive into it to gain PadScore. The pad flashes, plays a sound, disappears for 10 seconds, and then returns for the next player.

WORKFLOW FIRST

  1. Create one Part in Workspace named exactly ScorePad.
  2. Put it anywhere students should score a point.
  3. Duplicate it around the map without changing the name.
  4. Open ServerScriptService, add a new Script, and paste the code below.
  5. Press Play and touch or drive into the pad to test the PadScore leaderboard.

WHY IT WORKS

The script finds every Part named ScorePad, so students can add more scoring objects by duplicating the same pad. The first valid touch scores, then the pad turns off until it respawns.

It uses PadScore instead of Score, so it can run beside checkpoint or target games without mixing those points.

COMPLETE SCRIPT
Paste into ServerScriptService
-- Put this Script in ServerScriptService.
-- Create one or more Parts in Workspace named ScorePad.
-- Roblox uses -- for comments. Python uses # for comments.

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

-- PART 1: Student settings
-- STUDENT CHANGE: Every score pad Part in Workspace should use this exact name.
local SCORE_PAD_NAME = "ScorePad"

-- STUDENT CHANGE: How many points the player gets when touching the object.
local POINTS_PER_TOUCH = 1

-- STUDENT CHANGE: How long the object disappears after someone scores.
local RESPAWN_SECONDS = 10

-- STUDENT CHANGE: Sound played when someone scores.
local SCORE_SOUND_ID = "rbxassetid://12221967"

-- STUDENT CHANGE: Try 0.3 for quiet, 1 for normal, or 2 for loud.
local SOUND_VOLUME = 1

-- STUDENT CHANGE: Change these colors to change the score effect.
local READY_COLOR = Color3.fromRGB(0, 200, 255)
local SCORE_FLASH_COLOR = Color3.fromRGB(255, 230, 80)

-- STUDENT CHANGE: Change this if you want the object more visible or more transparent.
local READY_TRANSPARENCY = 0.2

-- PART 2: Create the PadScore leaderboard for each player
local function setupPlayer(player)
	local leaderstats = player:FindFirstChild("leaderstats")

	if not leaderstats then
		leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player
	end

	local score = leaderstats:FindFirstChild("PadScore")

	if not score then
		score = Instance.new("IntValue")
		score.Name = "PadScore"
		score.Parent = leaderstats
	end
end

-- PART 3: Find the player who touched the score pad
-- This works if the player walks into it OR drives a car into it.
local function findPlayerFromHit(hit)
	local current = hit

	while current and current ~= workspace do
		if current:IsA("Model") then
			local player = Players:GetPlayerFromCharacter(current)

			if player then
				return player
			end

			local vehicleSeat = current:FindFirstChildWhichIsA("VehicleSeat", true)

			if vehicleSeat and vehicleSeat.Occupant then
				return Players:GetPlayerFromCharacter(vehicleSeat.Occupant.Parent)
			end

			local seat = current:FindFirstChildWhichIsA("Seat", true)

			if seat and seat.Occupant then
				return Players:GetPlayerFromCharacter(seat.Occupant.Parent)
			end
		end

		current = current.Parent
	end

	return nil
end

-- PART 4: Play a sound from the score pad
local function playScoreSound(scorePad)
	if SCORE_SOUND_ID == "" then
		return
	end

	local sound = Instance.new("Sound")
	sound.Name = "ScorePadSound"
	sound.SoundId = SCORE_SOUND_ID
	sound.Volume = SOUND_VOLUME
	sound.Parent = scorePad
	sound:Play()

	Debris:AddItem(sound, 5)
end

-- PART 5: Flash the score pad before it disappears
local function flashScorePad(scorePad)
	scorePad.Color = SCORE_FLASH_COLOR
	scorePad.Material = Enum.Material.Neon
	scorePad.Transparency = 0

	local light = Instance.new("PointLight")
	light.Name = "ScorePadLight"
	light.Color = SCORE_FLASH_COLOR
	light.Brightness = 5
	light.Range = 18
	light.Parent = scorePad

	Debris:AddItem(light, 0.4)
end

-- PART 6: Hide the score pad after someone scores
local function hideScorePad(scorePad)
	scorePad.Transparency = 1
	scorePad.CanTouch = false
	scorePad.CanCollide = false
	scorePad.CanQuery = false
end

-- PART 7: Show the score pad again
local function showScorePad(scorePad)
	scorePad.Color = READY_COLOR
	scorePad.Material = Enum.Material.Neon
	scorePad.Transparency = READY_TRANSPARENCY
	scorePad.CanTouch = true
	scorePad.CanCollide = false
	scorePad.CanQuery = true
	scorePad:SetAttribute("IsHidden", false)
end

-- PART 8: Give the player points
local function givePoint(player)
	local leaderstats = player:FindFirstChild("leaderstats")
	local score = leaderstats and leaderstats:FindFirstChild("PadScore")

	if score then
		score.Value += POINTS_PER_TOUCH
	end
end

-- PART 9: Handle one score pad touch
local function handleScorePadTouch(scorePad, hit)
	if scorePad:GetAttribute("IsHidden") then
		return
	end

	local player = findPlayerFromHit(hit)

	if not player then
		return
	end

	scorePad:SetAttribute("IsHidden", true)

	givePoint(player)
	flashScorePad(scorePad)
	playScoreSound(scorePad)

	print(player.Name .. " scored +" .. POINTS_PER_TOUCH)

	task.delay(0.25, function()
		if scorePad and scorePad.Parent then
			hideScorePad(scorePad)
		end
	end)

	task.delay(RESPAWN_SECONDS, function()
		if scorePad and scorePad.Parent then
			showScorePad(scorePad)
		end
	end)
end

-- PART 10: Connect one score pad
local function setupScorePad(scorePad)
	if not scorePad:IsA("BasePart") then
		return false
	end

	if scorePad:GetAttribute("ScorePadReady") then
		return false
	end

	scorePad:SetAttribute("ScorePadReady", true)
	scorePad.Anchored = true
	showScorePad(scorePad)

	scorePad.Touched:Connect(function(hit)
		handleScorePadTouch(scorePad, hit)
	end)

	return true
end

Players.PlayerAdded:Connect(setupPlayer)

for _, player in ipairs(Players:GetPlayers()) do
	setupPlayer(player)
end

-- PART 11: Find every score pad with the same name
local scorePadCount = 0

for _, thing in ipairs(workspace:GetDescendants()) do
	if thing.Name == SCORE_PAD_NAME and setupScorePad(thing) then
		scorePadCount += 1
	end
end

workspace.DescendantAdded:Connect(function(thing)
	if thing.Name == SCORE_PAD_NAME and setupScorePad(thing) then
		scorePadCount += 1
		print("Added ScorePad. Total score pads: " .. scorePadCount)
	end
end)

if scorePadCount == 0 then
	warn("No score pads found. Create Parts in Workspace named " .. SCORE_PAD_NAME)
else
	print("Score pads ready: " .. scorePadCount)
end

NEXT FEATURES

Planned additions

Finish Line Timer

Record when a player reaches the end of a race.