- Put the object in Workspace exactly where it should start.
- Duplicate that object and move the duplicate into ServerStorage.
- Make sure both objects have the exact same name.
- Open ServerScriptService, add a new Script, and paste the code below.
- Change only OBJECT_NAME, then press Play to test.
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.
Respawn an Object
Spawn a fresh home copy when a car, plane, horse, or other moving object leaves its starting place.
Race Checkpoints
Guide racers through 10 checkpoints in order with messages, sounds, flashes, and floating numbers.
Score Pad
Give players PadScore points when they touch or drive into a pad, then hide it for 10 seconds.
Finish Line Timer
Record when a player reaches the end of a race.
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.
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.
-- 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
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.
- Create a Folder in Workspace named RaceCheckpoints.
- Add 10 Parts inside the folder named Checkpoint1 through Checkpoint10.
- Place the Parts around the map in the order racers should drive through them.
- Make each checkpoint large enough to touch, then keep it transparent and non-colliding.
- Open ServerScriptService, add a new Script, and paste the code below.
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.
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.")
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.
- Create one Part in Workspace named exactly ScorePad.
- Put it anywhere students should score a point.
- Duplicate it around the map without changing the name.
- Open ServerScriptService, add a new Script, and paste the code below.
- Press Play and touch or drive into the pad to test the PadScore leaderboard.
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.
-- 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
Planned additions
Finish Line Timer
Record when a player reaches the end of a race.