- 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.
Rocket Ball
Control a Rocket League style ball that cars can push, steer, pop into the air, and respawn.
Car Jump and Air Boost
Replace the car controller so Space jumps, a second Space boosts forward in the air, and the car stays stable.
Rocket Goal Scoring
Score Red and Blue team goals when RocketBall hits RedGoal or BlueGoal, then reset the ball.
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
Rocket Ball
Use this to start a Rocket League style game. Students create one large ball in Roblox Studio, then paste one server script that makes car hits feel more like a soccer kick: quick forward motion, limited spin, light bounce, and automatic respawn if the ball is deleted or falls off the map.
- Create one Part in Workspace.
- Change the Part shape to Ball and name it exactly RocketBall.
- Place the ball in the middle of the map where it should respawn.
- Open ServerScriptService, add a new Script, and paste the code below.
- Press Play, hit the ball with a car, then tune Attributes such as KickForce, SteerPower, and UpForce.
The script adds editable Attributes to the ball, so students can change the game feel from the Properties panel without rewriting the full script. KickForce controls the base hit, CarSpeedBonus rewards faster cars, and UpForce controls how much the ball hops upward.
The hit direction uses both the contact angle and the car's facing direction. SteerPower controls that mix, while SpinKeepAmount damps unwanted rotation so a large ball does not keep spinning after each car hit.
-- Put this Script in ServerScriptService.
-- Create one Sphere Part in Workspace named RocketBall.
-- This version uses the simple kickable-ball idea, adapted for cars.
-- Goal: light soccer feel, no wild spinning, no "hit a brick" feeling.
-- Roblox uses -- for comments. Python uses # for comments.
local Debris = game:GetService("Debris")
-- PART 1: Student settings
-- STUDENT CHANGE: The ball in Workspace must use this exact name.
local BALL_NAME = "RocketBall"
-- These default Attributes are added to the ball if students did not add them yet.
-- Students can click RocketBall in Explorer and change these Attributes in Properties.
local DEFAULT_ATTRIBUTES = {
-- STUDENT CHANGE: Base speed added when a car hits the ball.
KickForce = 142,
-- STUDENT CHANGE: Bigger number means fast cars kick the ball harder.
-- Try 0.4 for soft, 0.7 for soccer feel, or 1 for very strong.
CarSpeedBonus = 0.7,
-- STUDENT CHANGE: Bigger number means the car's facing direction controls the kick more.
-- Try 0 for contact-angle only, 0.5 for balanced, or 1 for car-facing only.
SteerPower = 0.9,
-- STUDENT CHANGE: Bigger number makes the ball hop upward more.
UpForce = 18,
-- STUDENT CHANGE: The ball will not be allowed to move faster than this.
MaxSpeed = 85,
-- STUDENT CHANGE: Smaller number makes the ball feel lighter.
BallDensity = 0.035,
-- STUDENT CHANGE: Smaller number makes the ball roll with less drag.
BallFriction = 0.03,
-- STUDENT CHANGE: 0 means no bounce. 1 means very bouncy.
BouncePower = 0.9,
-- STUDENT CHANGE: Smaller number stops unwanted spin faster.
-- Try 0 for almost no spin, 0.2 for a little spin, 0.5 for more spin.
SpinKeepAmount = 0.12,
-- STUDENT CHANGE: 0.9 means the car keeps 90% of its forward speed after contact.
VehicleKeepSpeedAfterHit = 0.9,
-- STUDENT CHANGE: How many seconds the BodyVelocity kick lasts.
KickDuration = 0.16,
-- STUDENT CHANGE: If the ball falls below this Y height, it respawns.
RespawnY = -50,
-- STUDENT CHANGE: How many seconds to wait before resetting or replacing the ball.
RespawnDelay = 3,
}
-- STUDENT CHANGE: Smaller number lets the same car push the ball more often.
local HIT_COOLDOWN_SECONDS = 0.2
local currentBall = nil
local ballTemplate = nil
local startCFrame = nil
local touchConnection = nil
local isRespawning = false
local lastVehicleHitTime = {}
-- PART 2: Make sure the ball has editable Attributes
local function addDefaultAttributes(ball)
for attributeName, defaultValue in pairs(DEFAULT_ATTRIBUTES) do
if ball:GetAttribute(attributeName) == nil then
ball:SetAttribute(attributeName, defaultValue)
end
end
end
local function getNumberAttribute(ball, attributeName)
local value = ball:GetAttribute(attributeName)
if typeof(value) == "number" then
return value
end
local defaultValue = DEFAULT_ATTRIBUTES[attributeName]
ball:SetAttribute(attributeName, defaultValue)
return defaultValue
end
-- PART 3: Prepare the ball physics
local function configureBall(ball)
addDefaultAttributes(ball)
if ball:IsA("Part") then
ball.Shape = Enum.PartType.Ball
end
ball.Anchored = false
ball.CanCollide = true
ball.CanTouch = true
ball.CanQuery = true
ball.Material = Enum.Material.SmoothPlastic
local ballDensity = math.max(getNumberAttribute(ball, "BallDensity"), 0.01)
local ballFriction = math.clamp(getNumberAttribute(ball, "BallFriction"), 0, 1)
local bouncePower = math.clamp(getNumberAttribute(ball, "BouncePower"), 0, 1)
ball.CustomPhysicalProperties = PhysicalProperties.new(ballDensity, ballFriction, bouncePower, 0.2, 1)
pcall(function()
ball:SetNetworkOwner(nil)
end)
end
-- PART 4: Find the car that hit the ball
local function findVehicleFromHit(hit)
local current = hit
while current and current ~= workspace do
if current:IsA("Model") then
local vehicleSeat = current:FindFirstChildWhichIsA("VehicleSeat", true)
local seat = current:FindFirstChildWhichIsA("Seat", true)
local drivingSeat = vehicleSeat or seat
if drivingSeat and drivingSeat.Occupant then
return current, drivingSeat
end
end
current = current.Parent
end
return nil, nil
end
-- PART 5: Keep the ball from moving too fast
local function limitBallSpeed(ball)
local maxSpeed = getNumberAttribute(ball, "MaxSpeed")
local velocity = ball.AssemblyLinearVelocity
if velocity.Magnitude > maxSpeed then
ball.AssemblyLinearVelocity = velocity.Unit * maxSpeed
end
end
-- PART 6: Choose the kick direction
local function getKickDirection(ball, drivingSeat)
local angleDirection = ball.Position - drivingSeat.Position
local flatAngleDirection = Vector3.new(angleDirection.X, 0, angleDirection.Z)
local aimDirection = drivingSeat.CFrame.LookVector
local flatAimDirection = Vector3.new(aimDirection.X, 0, aimDirection.Z)
local steerPower = math.clamp(getNumberAttribute(ball, "SteerPower"), 0, 1)
if flatAngleDirection.Magnitude < 0.1 then
flatAngleDirection = flatAimDirection
end
if flatAngleDirection.Magnitude < 0.1 or flatAimDirection.Magnitude < 0.1 then
return nil
end
local contactDirection = flatAngleDirection.Unit
local carAimDirection = flatAimDirection.Unit
local mixedDirection = (contactDirection * (1 - steerPower)) + (carAimDirection * steerPower)
if mixedDirection.Magnitude < 0.1 then
return contactDirection
end
return mixedDirection.Unit
end
-- PART 7: Kick the ball with a short BodyVelocity
local function kickBall(ball, kickDirection, kickSpeed)
local oldVelocity = ball:FindFirstChild("RocketBallKickVelocity")
if oldVelocity then
oldVelocity:Destroy()
end
local upForce = getNumberAttribute(ball, "UpForce")
local kickDuration = getNumberAttribute(ball, "KickDuration")
local maxSpeed = getNumberAttribute(ball, "MaxSpeed")
local finalSpeed = math.min(kickSpeed, maxSpeed)
local newVelocity = kickDirection * finalSpeed + Vector3.new(0, upForce, 0)
ball.AssemblyAngularVelocity = Vector3.zero
local velocity = Instance.new("BodyVelocity")
velocity.Name = "RocketBallKickVelocity"
velocity.MaxForce = Vector3.new(1, 1, 1) * math.huge
velocity.Velocity = newVelocity
velocity.Parent = ball
Debris:AddItem(velocity, kickDuration)
task.delay(kickDuration, function()
if ball and ball.Parent then
ball.AssemblyLinearVelocity = newVelocity
ball.AssemblyAngularVelocity = ball.AssemblyAngularVelocity * getNumberAttribute(ball, "SpinKeepAmount")
limitBallSpeed(ball)
end
end)
end
-- PART 8: Keep the car from feeling like it hit a brick
local function keepVehicleMoving(ball, drivingSeat)
local keepSpeedAmount = math.clamp(getNumberAttribute(ball, "VehicleKeepSpeedAfterHit"), 0, 1)
local forwardDirection = drivingSeat.CFrame.LookVector
local flatForward = Vector3.new(forwardDirection.X, 0, forwardDirection.Z)
if flatForward.Magnitude < 0.1 then
return
end
local currentVelocity = drivingSeat.AssemblyLinearVelocity
local currentFlatVelocity = Vector3.new(currentVelocity.X, 0, currentVelocity.Z)
local forwardSpeed = currentFlatVelocity:Dot(flatForward.Unit)
if forwardSpeed < 0 then
forwardSpeed = 0
end
local targetForwardSpeed = math.max(forwardSpeed, getNumberAttribute(ball, "KickForce") * keepSpeedAmount)
local targetVelocity = flatForward.Unit * targetForwardSpeed
drivingSeat.AssemblyLinearVelocity = Vector3.new(targetVelocity.X, currentVelocity.Y, targetVelocity.Z)
drivingSeat.AssemblyAngularVelocity = drivingSeat.AssemblyAngularVelocity * 0.5
end
-- PART 9: Push the ball when a driven car hits it
local function pushBallFromVehicle(ball, vehicle, drivingSeat)
local kickDirection = getKickDirection(ball, drivingSeat)
if not kickDirection then
return
end
local kickForce = getNumberAttribute(ball, "KickForce")
local carSpeedBonus = getNumberAttribute(ball, "CarSpeedBonus")
local carFlatVelocity = Vector3.new(
drivingSeat.AssemblyLinearVelocity.X,
0,
drivingSeat.AssemblyLinearVelocity.Z
)
local kickSpeed = kickForce + carFlatVelocity.Magnitude * carSpeedBonus
kickBall(ball, kickDirection, kickSpeed)
keepVehicleMoving(ball, drivingSeat)
print("RocketBall car kick: " .. vehicle.Name .. " speed=" .. math.floor(kickSpeed))
end
local function handleBallTouch(hit)
local ball = currentBall
if not ball or not ball.Parent then
return
end
local vehicle, drivingSeat = findVehicleFromHit(hit)
if not vehicle or not drivingSeat then
return
end
local now = os.clock()
if lastVehicleHitTime[vehicle] and now - lastVehicleHitTime[vehicle] < HIT_COOLDOWN_SECONDS then
return
end
lastVehicleHitTime[vehicle] = now
pushBallFromVehicle(ball, vehicle, drivingSeat)
end
-- PART 10: Connect the active ball
local function setupActiveBall(ball)
if touchConnection then
touchConnection:Disconnect()
end
currentBall = ball
configureBall(ball)
touchConnection = ball.Touched:Connect(handleBallTouch)
end
-- PART 11: Reset an existing ball or replace a deleted ball
local function spawnReplacementBall()
local newBall = ballTemplate:Clone()
newBall.Name = BALL_NAME
newBall.CFrame = startCFrame
newBall.AssemblyLinearVelocity = Vector3.zero
newBall.AssemblyAngularVelocity = Vector3.zero
newBall.Parent = workspace
setupActiveBall(newBall)
end
local function respawnBall(reason)
if isRespawning then
return
end
isRespawning = true
local ballForSettings = currentBall or ballTemplate
local respawnDelay = getNumberAttribute(ballForSettings, "RespawnDelay")
print("RocketBall will respawn in " .. respawnDelay .. " seconds: " .. reason)
task.delay(respawnDelay, function()
if currentBall and currentBall.Parent then
currentBall.CFrame = startCFrame
currentBall.AssemblyLinearVelocity = Vector3.zero
currentBall.AssemblyAngularVelocity = Vector3.zero
configureBall(currentBall)
print("RocketBall reset to the middle.")
else
spawnReplacementBall()
print("RocketBall was replaced at the middle.")
end
isRespawning = false
end)
end
-- PART 12: Start the ball controller
local startingBall = workspace:FindFirstChild(BALL_NAME)
if not startingBall or not startingBall:IsA("BasePart") then
warn("Create one Sphere Part in Workspace named " .. BALL_NAME)
return
end
configureBall(startingBall)
startCFrame = startingBall.CFrame
ballTemplate = startingBall:Clone()
setupActiveBall(startingBall)
print("RocketBall car-kick controller ready.")
-- PART 13: Watch the ball while the game runs
while true do
task.wait(0.25)
local ball = currentBall
if not ball or not ball.Parent then
respawnBall("ball was deleted")
elseif ball.Position.Y < getNumberAttribute(ball, "RespawnY") then
respawnBall("ball fell below RespawnY")
else
configureBall(ball)
ball.AssemblyAngularVelocity = ball.AssemblyAngularVelocity * getNumberAttribute(ball, "SpinKeepAmount")
limitBallSpeed(ball)
end
end
Car Jump and Air Boost
Use this with a Roblox car that already has the standard controller modules. It replaces the existing controller LocalScript so the driver can press Space once to jump and press Space again in the air to boost straight forward into the ball.
- In Explorer, open Workspace, then open the car model such as Carblue.
- Find the existing controller LocalScript that starts with local Players = game:GetService.
- Duplicate the original script or copy it somewhere safe before replacing it.
- Replace the whole controller script with the code below.
- Press Play, sit in the car, press Space once to jump, then press Space again in the air to boost.
The script keeps the normal camera, input, speedometer, nitro, and controller loop. The new section listens for Space only while the local player is driving this car.
Students can tune JUMP_UP_SPEED, AIR_BOOST_FORWARD_SPEED, AIR_SPIN_KEEP_AMOUNT, and FLOAT_ASSIST_SECONDS to change the feel. Cloning the car also clones this behavior.
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
-- ROCKET JUMP NEW: We use UserInputService to listen for the Space key.
local UserInputService = game:GetService("UserInputService")
local Constants = require(script.Parent.Parent.Constants)
local Units = require(script.Parent.Parent.Units)
local Controller = require(script.Parent.Parent.Controller)
local Camera = require(script.Parent.Camera)
local Input = require(script.Parent.Input)
local Speedometer = require(script.Parent.Speedometer)
local setJumpingEnabled = require(script.setJumpingEnabled)
local disconnectAndClear = require(script.Parent.disconnectAndClear)
local destructionHandlerTemplate = script.DestructionHandler
local player = Players.LocalPlayer
local car = script.Parent.Parent.Parent
local driverSeat = car.DriverSeat
local chassis = car.Chassis
local engine = car.Engine
local remotes = car.Remotes
local setNitroEnabledRemote = remotes.SetNitroEnabled
local animations = car.Animations
local driveAnimation = animations.DriveAnimation
local isControlling = false
local connections = {}
local driveAnimationTrack = nil
-- ROCKET JUMP NEW: These variables remember whether this car already jumped or boosted.
local jumpUsed = false
local airBoostUsed = false
local lastSpaceTime = 0
local jumpTime = 0
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Bigger number makes the first Space press jump higher.
local JUMP_UP_SPEED = 55
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Bigger number makes the second Space press push forward harder.
-- This is 4x stronger than the first version, which used 95.
local AIR_BOOST_FORWARD_SPEED = 380
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Adds a little upward lift during the second Space boost.
local AIR_BOOST_UP_SPEED = 16
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Smaller number allows faster repeated key presses.
local SPACE_COOLDOWN_SECONDS = 0.12
-- ROCKET JUMP NEW:
-- After the car jumps, ignore the ground for a tiny moment so the second Space can work.
local IGNORE_GROUND_AFTER_JUMP_SECONDS = 0.35
-- ROCKET JUMP NEW:
-- How far below the chassis to look for the ground.
local GROUND_CHECK_DISTANCE = 8
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Smaller number removes more spin and makes the car more stable in the air.
-- Try 0.1 for very stable or 0.5 for more flipping.
local AIR_SPIN_KEEP_AMOUNT = 0.2
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- Bigger number makes the car float longer after Space.
local FLOAT_ASSIST_SECONDS = 0.8
-- ROCKET JUMP NEW - STUDENT CHANGE:
-- 0 means no float help. 1 means almost cancels gravity while floating.
local FLOAT_GRAVITY_CANCEL = 0.65
-- ROCKET JUMP NEW: Check if the car is touching or near the ground.
local function isCarGrounded()
local raycastParams = RaycastParams.new()
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
raycastParams.FilterDescendantsInstances = { car }
local result = workspace:Raycast(
chassis.Position,
Vector3.new(0, -GROUND_CHECK_DISTANCE, 0),
raycastParams
)
return result ~= nil
end
-- ROCKET JUMP NEW: Push the main car assembly.
-- We do NOT set every wheel and body part separately, because that can make the car unstable.
local function setCarVelocity(newVelocity)
chassis.AssemblyLinearVelocity = newVelocity
if driverSeat ~= chassis then
driverSeat.AssemblyLinearVelocity = newVelocity
end
end
-- ROCKET JUMP NEW: Reduce spin so the car stays easier to control in the air.
local function stabilizeCarSpin()
chassis.AssemblyAngularVelocity = chassis.AssemblyAngularVelocity * AIR_SPIN_KEEP_AMOUNT
if driverSeat ~= chassis then
driverSeat.AssemblyAngularVelocity = driverSeat.AssemblyAngularVelocity * AIR_SPIN_KEEP_AMOUNT
end
end
-- ROCKET JUMP NEW: Add a short upward force so the car hangs in the air longer.
local function applyFloatAssist()
local oldForce = chassis:FindFirstChild("RocketJumpFloatForce")
if oldForce then
oldForce:Destroy()
end
local attachment = chassis:FindFirstChild("RocketJumpFloatAttachment")
if not attachment then
attachment = Instance.new("Attachment")
attachment.Name = "RocketJumpFloatAttachment"
attachment.Parent = chassis
end
local force = Instance.new("VectorForce")
force.Name = "RocketJumpFloatForce"
force.Attachment0 = attachment
force.ApplyAtCenterOfMass = true
force.RelativeTo = Enum.ActuatorRelativeTo.World
force.Force = Vector3.new(0, chassis.AssemblyMass * workspace.Gravity * FLOAT_GRAVITY_CANCEL, 0)
force.Parent = chassis
task.delay(FLOAT_ASSIST_SECONDS, function()
if force and force.Parent then
force:Destroy()
end
end)
end
-- ROCKET JUMP NEW: First Space press.
local function jumpCar()
local oldVelocity = chassis.AssemblyLinearVelocity
local newVelocity = Vector3.new(oldVelocity.X, JUMP_UP_SPEED, oldVelocity.Z)
jumpUsed = true
airBoostUsed = false
jumpTime = os.clock()
setCarVelocity(newVelocity)
stabilizeCarSpin()
applyFloatAssist()
end
-- ROCKET JUMP NEW: Second Space press while the car is in the air.
local function airBoostCar()
local forwardDirection = chassis.CFrame.LookVector
local oldVelocity = chassis.AssemblyLinearVelocity
local forwardVelocity = forwardDirection.Unit * AIR_BOOST_FORWARD_SPEED
local newVelocity = forwardVelocity + Vector3.new(0, math.max(oldVelocity.Y, AIR_BOOST_UP_SPEED), 0)
airBoostUsed = true
setCarVelocity(newVelocity)
stabilizeCarSpin()
applyFloatAssist()
end
-- ROCKET JUMP NEW: Decide whether Space should jump or air boost.
local function handleSpacePressed(input)
if input.KeyCode ~= Enum.KeyCode.Space then
return
end
if UserInputService:GetFocusedTextBox() then
return
end
local now = os.clock()
if now - lastSpaceTime < SPACE_COOLDOWN_SECONDS then
return
end
lastSpaceTime = now
local recentlyJumped = jumpUsed and now - jumpTime < IGNORE_GROUND_AFTER_JUMP_SECONDS
local grounded = isCarGrounded() and not recentlyJumped
if grounded then
jumpUsed = false
airBoostUsed = false
end
if not jumpUsed then
jumpCar()
elseif not airBoostUsed then
airBoostCar()
end
end
-- Enable the client modules and Controller update loop
local function startControlling()
if isControlling then
return
end
isControlling = true
-- Stop the player from jumping out the seat since a separate key is bound to exit the car.
local character = player.Character
if character then
setJumpingEnabled(character, false)
-- Play the driving animation
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
local animator = humanoid:FindFirstChildOfClass("Animator")
if animator then
driveAnimationTrack = animator:LoadAnimation(driveAnimation)
driveAnimationTrack:Play()
end
end
end
-- Connect the Controller's update function to RunService.Stepped.
-- Stepped is used here since it fires prior to the physics simulation, and the controller
-- updates the physics constraints on the car.
table.insert(
connections,
RunService.Stepped:Connect(function(_, deltaTime: number)
Controller:update(deltaTime)
end)
)
-- ROCKET JUMP NEW: Listen for Space only while this player is driving this car.
table.insert(
connections,
UserInputService.InputBegan:Connect(function(input)
handleSpacePressed(input)
end)
)
-- Enable replication for the nitro audio and VFX
table.insert(
connections,
engine:GetAttributeChangedSignal(Constants.NITRO_ENABLED_ATTRIBUTE):Connect(function()
local isNitroEnabled = engine:GetAttribute(Constants.NITRO_ENABLED_ATTRIBUTE)
setNitroEnabledRemote:FireServer(isNitroEnabled)
end)
)
-- Enable the client modules
Camera:enable()
Input:enable()
Speedometer:enable()
end
-- Disable the client modules and Controller update loop
local function stopControlling()
if not isControlling then
return
end
isControlling = false
-- ROCKET JUMP NEW: Reset jump state when the player leaves the car.
jumpUsed = false
airBoostUsed = false
lastSpaceTime = 0
jumpTime = 0
-- Disable the client modules
Camera:disable()
Input:disable()
Speedometer:disable()
-- Disconnect event connections
disconnectAndClear(connections)
-- Reset the car Controller. If the car has been destroyed (i.e. is not parented anywhere in the DataModel) this can be skipped.
if car:IsDescendantOf(game) then
Controller:reset()
end
-- Reenable jumping for the player
local character = player.Character
if character then
setJumpingEnabled(character, true)
end
-- Stop the driving animation
if driveAnimationTrack then
driveAnimationTrack:Stop()
driveAnimationTrack = nil
end
end
-- Call startControlling() or stopControlling() if the player enters or exits the seat
local function onOccupantChanged()
local isOccupiedByLocalPlayer = false
local humanoid = driverSeat.Occupant
if humanoid then
local character = humanoid.Parent
local playerInSeat = Players:GetPlayerFromCharacter(character)
if playerInSeat == player then
isOccupiedByLocalPlayer = true
end
end
if isOccupiedByLocalPlayer then
startControlling()
else
stopControlling()
end
end
-- When using Deferred signal mode: since this script is parented to the car, destroying the car will clean up
-- all connections (including .Destroying) before they can actually execute.
-- To avoid this, we'll use a slightly hacky solution and clone a listener script into PlayerScripts.
-- This script will continue running after the car is destroyed and allow us to properly clean up when the car is destroyed.
local function setupDestructionHandler()
local destructionHandler = destructionHandlerTemplate:Clone()
destructionHandler.Parent = player.PlayerScripts
destructionHandler.Enabled = true
-- Defer firing the BindableEvent so the script is able to initialize first
task.defer(function()
destructionHandler.BindToCar:Fire(car, stopControlling)
end)
end
setupDestructionHandler()
driverSeat:GetPropertyChangedSignal("Occupant"):Connect(onOccupantChanged)
Rocket Goal Scoring
Use this after the Rocket Ball script. Students add one red goal and one blue goal, then paste this script to track team points. When RocketBall hits RedGoal, Blue wins a point. When RocketBall hits BlueGoal, Red wins a point.
- Finish the Rocket Ball setup first, with one ball named exactly RocketBall.
- Create one large red Part in Workspace named exactly RedGoal.
- Create one large blue Part in Workspace named exactly BlueGoal.
- Set each goal to Anchored = true, CanCollide = false, and CanTouch = true.
- Open ServerScriptService, add a new Script, and paste the code below.
The script watches only the ball and the two goal Parts. It keeps RedScore and BlueScore in ReplicatedStorage, shows a top scoreboard, and displays a center-screen message when a team wins a point.
After a goal, the script plays a celebration sound, flashes the goal, and moves RocketBall back to its starting center position so the game can continue.
-- Put this Script in ServerScriptService.
-- Use this with the RocketBall script.
-- Create one ball named RocketBall, one red goal named RedGoal, and one blue goal named BlueGoal.
-- Roblox uses -- for comments. Python uses # for comments.
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")
-- PART 1: Student settings
-- STUDENT CHANGE: The ball in Workspace must use this exact name.
local BALL_NAME = "RocketBall"
-- STUDENT CHANGE: The red goal is defended by Red. If the ball hits it, Blue scores.
local RED_GOAL_NAME = "RedGoal"
-- STUDENT CHANGE: The blue goal is defended by Blue. If the ball hits it, Red scores.
local BLUE_GOAL_NAME = "BlueGoal"
-- STUDENT CHANGE: Try 2 if you want each goal to be worth more points.
local POINTS_PER_GOAL = 1
-- STUDENT CHANGE: Prevents one goal touch from counting many times.
local GOAL_COOLDOWN_SECONDS = 3
-- STUDENT CHANGE: How long the center-screen message stays visible.
local MESSAGE_SECONDS = 3
-- STUDENT CHANGE: How long to wait before putting the ball back in the middle.
local BALL_RESET_DELAY_SECONDS = 0.5
-- STUDENT CHANGE: Celebration sound. Change the number to test another Roblox sound.
local CELEBRATION_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 if your teams use different colors.
local RED_COLOR = Color3.fromRGB(255, 80, 80)
local BLUE_COLOR = Color3.fromRGB(80, 160, 255)
local FLASH_COLOR = Color3.fromRGB(255, 230, 80)
local redScoreValue = nil
local blueScoreValue = nil
local startCFrame = nil
local goalOnCooldown = false
-- PART 2: Find the ball and goals
local ball = workspace:FindFirstChild(BALL_NAME, true)
local redGoal = workspace:FindFirstChild(RED_GOAL_NAME, true)
local blueGoal = workspace:FindFirstChild(BLUE_GOAL_NAME, true)
if not ball or not ball:IsA("BasePart") then
warn("Create one Sphere Part in Workspace named " .. BALL_NAME)
return
end
if not redGoal or not redGoal:IsA("BasePart") then
warn("Create one Part in Workspace named " .. RED_GOAL_NAME)
return
end
if not blueGoal or not blueGoal:IsA("BasePart") then
warn("Create one Part in Workspace named " .. BLUE_GOAL_NAME)
return
end
startCFrame = ball.CFrame
-- PART 3: Create team score values students can see in ReplicatedStorage
local function getOrCreateScoreValue(scoreFolder, valueName)
local scoreValue = scoreFolder:FindFirstChild(valueName)
if not scoreValue then
scoreValue = Instance.new("IntValue")
scoreValue.Name = valueName
scoreValue.Value = 0
scoreValue.Parent = scoreFolder
end
return scoreValue
end
local function setupScoreValues()
local scoreFolder = ReplicatedStorage:FindFirstChild("RocketTeamScores")
if not scoreFolder then
scoreFolder = Instance.new("Folder")
scoreFolder.Name = "RocketTeamScores"
scoreFolder.Parent = ReplicatedStorage
end
redScoreValue = getOrCreateScoreValue(scoreFolder, "RedScore")
blueScoreValue = getOrCreateScoreValue(scoreFolder, "BlueScore")
end
-- PART 4: Create the screen message and scoreboard
local function createGui(player)
local playerGui = player:FindFirstChild("PlayerGui") or player:WaitForChild("PlayerGui", 5)
if not playerGui then
return nil
end
local gui = playerGui:FindFirstChild("RocketGoalGui")
if gui then
return gui
end
gui = Instance.new("ScreenGui")
gui.Name = "RocketGoalGui"
gui.ResetOnSpawn = false
gui.Parent = playerGui
local scoreLabel = Instance.new("TextLabel")
scoreLabel.Name = "ScoreLabel"
scoreLabel.AnchorPoint = Vector2.new(0.5, 0)
scoreLabel.Position = UDim2.fromScale(0.5, 0.04)
scoreLabel.Size = UDim2.fromOffset(360, 44)
scoreLabel.BackgroundColor3 = Color3.fromRGB(15, 23, 42)
scoreLabel.BackgroundTransparency = 0.2
scoreLabel.BorderSizePixel = 0
scoreLabel.Font = Enum.Font.GothamBold
scoreLabel.TextScaled = true
scoreLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
scoreLabel.Parent = gui
local messageLabel = Instance.new("TextLabel")
messageLabel.Name = "MessageLabel"
messageLabel.AnchorPoint = Vector2.new(0.5, 0.5)
messageLabel.Position = UDim2.fromScale(0.5, 0.38)
messageLabel.Size = UDim2.fromOffset(520, 80)
messageLabel.BackgroundColor3 = Color3.fromRGB(15, 23, 42)
messageLabel.BackgroundTransparency = 0.15
messageLabel.BorderSizePixel = 0
messageLabel.Font = Enum.Font.GothamBlack
messageLabel.TextScaled = true
messageLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
messageLabel.Visible = false
messageLabel.Parent = gui
return gui
end
local function updateScoreboard(player)
local gui = createGui(player)
if not gui then
return
end
local scoreLabel = gui:FindFirstChild("ScoreLabel")
if scoreLabel then
scoreLabel.Text = "Red " .. redScoreValue.Value .. " | Blue " .. blueScoreValue.Value
end
end
local function updateAllScoreboards()
for _, player in ipairs(Players:GetPlayers()) do
updateScoreboard(player)
end
end
local function showCenterMessage(text, color)
for _, player in ipairs(Players:GetPlayers()) do
local gui = createGui(player)
if gui then
local messageLabel = gui:FindFirstChild("MessageLabel")
if messageLabel then
messageLabel.Text = text
messageLabel.TextColor3 = color
messageLabel.Visible = true
task.delay(MESSAGE_SECONDS, function()
if messageLabel and messageLabel.Parent and messageLabel.Text == text then
messageLabel.Visible = false
end
end)
end
end
end
end
local function getGoalMessage(scoringTeamName)
local pointWord = "points"
if POINTS_PER_GOAL == 1 then
pointWord = "point"
end
return scoringTeamName .. " Team won " .. POINTS_PER_GOAL .. " " .. pointWord .. "!"
end
-- PART 5: Goal effects
local function playCelebrationSound(goal)
local sound = Instance.new("Sound")
sound.SoundId = CELEBRATION_SOUND_ID
sound.Volume = SOUND_VOLUME
sound.Parent = goal
sound:Play()
Debris:AddItem(sound, 4)
end
local function flashGoal(goal)
local originalColor = goal.Color
local originalMaterial = goal.Material
goal.Color = FLASH_COLOR
goal.Material = Enum.Material.Neon
local light = Instance.new("PointLight")
light.Color = FLASH_COLOR
light.Brightness = 3
light.Range = 18
light.Parent = goal
task.delay(0.5, function()
if goal and goal.Parent then
goal.Color = originalColor
goal.Material = originalMaterial
end
if light and light.Parent then
light:Destroy()
end
end)
end
-- PART 6: Reset the ball after a goal
local function resetBallToStart()
task.delay(BALL_RESET_DELAY_SECONDS, function()
local currentBall = workspace:FindFirstChild(BALL_NAME, true)
if currentBall and currentBall:IsA("BasePart") then
currentBall.CFrame = startCFrame
currentBall.AssemblyLinearVelocity = Vector3.zero
currentBall.AssemblyAngularVelocity = Vector3.zero
end
end)
end
-- PART 7: Score a goal
local function scoreGoal(scoringTeamName, scoringTeamColor, scoringValue, goal)
if goalOnCooldown then
return
end
goalOnCooldown = true
scoringValue.Value += POINTS_PER_GOAL
local goalMessage = getGoalMessage(scoringTeamName)
updateAllScoreboards()
showCenterMessage(goalMessage, scoringTeamColor)
playCelebrationSound(goal)
flashGoal(goal)
resetBallToStart()
print(goalMessage .. " Red " .. redScoreValue.Value .. " - Blue " .. blueScoreValue.Value)
task.delay(GOAL_COOLDOWN_SECONDS, function()
goalOnCooldown = false
end)
end
-- PART 8: Connect the two goals
local function prepareGoal(goal)
goal.Anchored = true
goal.CanCollide = false
goal.CanTouch = true
end
local function connectGoal(goal, scoringTeamName, scoringTeamColor, scoringValue)
prepareGoal(goal)
goal.Touched:Connect(function(hit)
if hit and hit:IsA("BasePart") and hit.Name == BALL_NAME then
scoreGoal(scoringTeamName, scoringTeamColor, scoringValue, goal)
end
end)
end
setupScoreValues()
Players.PlayerAdded:Connect(function(player)
task.wait(1)
updateScoreboard(player)
end)
for _, player in ipairs(Players:GetPlayers()) do
updateScoreboard(player)
end
connectGoal(redGoal, "Blue", BLUE_COLOR, blueScoreValue)
connectGoal(blueGoal, "Red", RED_COLOR, redScoreValue)
print("Rocket goal scoring ready. RedGoal gives Blue a point. BlueGoal gives Red a point.")
Planned additions
Finish Line Timer
Record when a player reaches the end of a race.