Menu
CoddyTech

Best Times

Lesson 19 of 21 in Coddy's Roblox Game: Endless Tower course.

Speedrunners make obbies last. Time every climb and keep each player's best on the leaderboard.

time() is the number of seconds the game has been running. Note it when the round starts; at the top, the difference is the climb. Round it to tenths for the board:

local lap = math.floor((time() - lapStart) * 10) / 10 -- 7.46 seconds becomes 7.4

A best time is the smallest, and 0 means none yet. A NumberValue holds it, since an IntValue only holds whole numbers.

challenge icon

Challenge

Hard

Time every climb and keep each player's best.

  1. Add a NumberValue BestTime to leaderstats; startRound sets a top-level roundStart = time().
  2. In reachTop: local seconds = math.floor((time() - roundStart) * 10) / 10; if BestTime is 0 or bigger, set it to seconds.
  3. Print player.Name .. " reached the top in " .. seconds .. " seconds" instead.

Press Play and win: your time shows on the leaderboard. Then Check.

Try it yourself

local Players = game:GetService("Players")
local ServerStorage = game:GetService("ServerStorage")
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Settings = require(game.ReplicatedStorage.Settings)

local stages = ServerStorage.Stages
local lobbySpawn = workspace.Lobby.SpawnLocation
local tower = nil
local towerStages = {}
local lava = nil
local finished = {}

print("Each tower has " .. Settings.StageCount .. " stages")

-- Every player gets Wins and Stage on the leaderboard
Players.PlayerAdded:Connect(function(player)
	local leaderstats = Instance.new("Folder")
	leaderstats.Name = "leaderstats"
	leaderstats.Parent = player

	local wins = Instance.new("IntValue")
	wins.Name = "Wins"
	wins.Parent = leaderstats

	local stage = Instance.new("IntValue")
	stage.Name = "Stage"
	stage.Parent = leaderstats

	-- a player who falls starts again on the last stage they reached,
	-- unless the lava has covered it: then they wait in the lobby
	player.CharacterAdded:Connect(function(character)
		task.wait()
		local checkpoint = towerStages[stage.Value]
		local covered = checkpoint and lava and checkpoint.Start.Position.Y < lava.Position.Y + lava.Size.Y / 2
		if checkpoint and not covered and not finished[player] then
			character:PivotTo(checkpoint.Start.CFrame + Vector3.new(0, 4, 0))
		end
	end)
end)

-- The board in the lobby
local gui = Instance.new("SurfaceGui")
gui.Parent = workspace.Lobby.Board

local boardLabel = Instance.new("TextLabel")
boardLabel.Name = "Status"
boardLabel.Size = UDim2.fromScale(1, 1)
boardLabel.BackgroundColor3 = Color3.fromRGB(25, 25, 35)
boardLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
boardLabel.TextScaled = true
boardLabel.Text = "Endless Tower"
boardLabel.Parent = gui

local function setBoard(text)
	boardLabel.Text = text
end

-- Anything tagged Kill ends a life
local function killOnTouch(hit)
	local humanoid = hit.Parent:FindFirstChild("Humanoid")
	if humanoid then
		humanoid.Health = 0
	end
end

-- A vanishing step fades soon after someone lands on it, then comes back
local function makeVanish(part)
	part.Touched:Connect(function(hit)
		if part:GetAttribute("Gone") or not hit.Parent:FindFirstChild("Humanoid") then
			return
		end
		part:SetAttribute("Gone", true)
		task.wait(Settings.VanishDelay)
		part.Transparency = 0.8
		part.CanCollide = false
		task.wait(Settings.VanishTime)
		part.Transparency = 0
		part.CanCollide = true
		part:SetAttribute("Gone", nil)
	end)
end

-- A laser slides to its Slide offset and back, forever
local function makeSlide(part)
	local info = TweenInfo.new(Settings.SlideTime, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, -1, true)
	local goal = { Position = part.Position + part:GetAttribute("Slide") }
	TweenService:Create(part, info, goal):Play()
end

-- A bounce pad throws whoever lands on it up into the air
local function bounce(hit)
	local root = hit.Parent:FindFirstChild("HumanoidRootPart")
	if root then
		root.AssemblyLinearVelocity = Vector3.new(0, Settings.BouncePower, 0)
	end
end

-- Spinners turn a little every frame
RunService.Heartbeat:Connect(function(dt)
	for _, spinner in CollectionService:GetTagged("Spinner") do
		if spinner:IsDescendantOf(workspace) then
			spinner:PivotTo(spinner:GetPivot() * CFrame.Angles(0, math.rad(Settings.SpinSpeed) * dt, 0))
		end
	end
end)

-- Turn on every obstacle in a stage, by its tags
local function setupStage(stage, index)
	for _, part in stage:GetDescendants() do
		if part:HasTag("Kill") then
			part.Touched:Connect(killOnTouch)
		end
		if part:HasTag("Vanish") then
			makeVanish(part)
		end
		if part:HasTag("Conveyor") then
			part.AssemblyLinearVelocity = part.CFrame.LookVector * Settings.BeltSpeed
		end
		if part:HasTag("Slider") then
			makeSlide(part)
		end
		if part:HasTag("Bounce") then
			part.Touched:Connect(bounce)
		end
	end
	-- reaching a stage's Start counts on the leaderboard
	stage.Start.Touched:Connect(function(hit)
		local player = Players:GetPlayerFromCharacter(hit.Parent)
		if player and player.leaderstats.Stage.Value < index then
			player.leaderstats.Stage.Value = index
		end
	end)
end

-- Harder stages higher up: the bottom third easy, the top third hard
local function pickStage(index)
	local difficulty = math.ceil(index / Settings.StageCount * 3)
	local choices = {}
	for _, stage in stages:GetChildren() do
		if stage:GetAttribute("Difficulty") == difficulty then
			table.insert(choices, stage)
		end
	end
	return choices[math.random(1, #choices)]
end

-- Put a copy of a model on top of the tower; returns the copy and the new top
local function stack(template, bottom)
	local copy = template:Clone()
	local box, size = copy:GetBoundingBox()
	local lift = bottom - (box.Position.Y - size.Y / 2)
	copy:PivotTo(copy:GetPivot() + Vector3.new(0, lift, 0))
	copy.Parent = tower
	return copy, bottom + size.Y
end

-- The top of the tower: a win, and back to the lobby
local function reachTop(hit)
	local player = Players:GetPlayerFromCharacter(hit.Parent)
	if not player or finished[player] then
		return
	end
	finished[player] = true
	player.leaderstats.Wins.Value += 1
	-- Lesson 19: Best Times goes here
	print(player.Name .. " reached the top")
	hit.Parent:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 4, 0))
end

-- Build a new random tower, bottom up
local function buildTower()
	tower = Instance.new("Model")
	tower.Name = "Tower"
	tower.Parent = workspace
	towerStages = {}
	local top = 0
	for i = 1, Settings.StageCount do
		local stage, newTop = stack(pickStage(i), top)
		top = newTop
		setupStage(stage, i)
		towerStages[i] = stage
	end
	local finish = stack(ServerStorage.Finish, top)
	finish.FinishPad.Touched:Connect(reachTop)
	return top
end

-- Lava rises from the ground to just under the top of the tower
local function startLava(top)
	lava = Instance.new("Part")
	lava.Name = "Lava"
	lava.Anchored = true
	lava.CanCollide = false
	lava.Size = Vector3.new(30, 2, 30)
	lava.Position = Vector3.new(0, -2, 0)
	lava.Color = Color3.fromRGB(255, 90, 20)
	lava.Material = Enum.Material.Neon
	lava.Parent = workspace
	lava.Touched:Connect(killOnTouch)
	local info = TweenInfo.new(top / Settings.LavaSpeed, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, 0, false, Settings.LavaDelay)
	TweenService:Create(lava, info, { Position = Vector3.new(0, top - 2, 0) }):Play()
end

local function startRound()
	finished = {}
	for _, player in Players:GetPlayers() do
		player.leaderstats.Stage.Value = 0
	end
	local top = buildTower()
	startLava(top)
	for _, player in Players:GetPlayers() do
		if player.Character then
			player.Character:PivotTo(towerStages[1].Start.CFrame + Vector3.new(0, 4, 0))
		end
	end
end

local function endRound()
	for _, player in Players:GetPlayers() do
		if player.Character and not finished[player] then
			player.Character:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 4, 0))
		end
		player.leaderstats.Stage.Value = 0
	end
	tower:Destroy()
	lava:Destroy()
	towerStages = {}
end

-- The round loop
while true do
	for i = Settings.Intermission, 1, -1 do
		setBoard("Next tower in " .. i)
		task.wait(1)
	end
	startRound()
	for i = Settings.RoundLength, 1, -1 do
		setBoard("Climb! " .. i .. " seconds left")
		task.wait(1)
	end
	endRound()
end

All lessons in Roblox Game: Endless Tower

Practice on your own: Roblox playground