Menu
CoddyTech

Lưu khi thoát

Bài học 20 trên 21 trong khóa học Trò chơi Roblox: Tháp bất tận của Coddy.

Số trận thắng và thành tích tốt nhất vẫn nên còn ở đó vào lần chơi tiếp theo. Roblox lưu dữ liệu giữa các lần truy cập trong DataStore, một bảng trên máy chủ của Roblox vẫn tồn tại sau khi trò chơi đóng. Hãy lưu dữ liệu khi người chơi rời đi và lưu cho tất cả người chơi vẫn đang chơi khi máy chủ tắt:

local coinStore = DataStoreService:GetDataStore("Coins")

local function save(player)
	pcall(function()
		coinStore:SetAsync(player.UserId, { Coins = player.leaderstats.Coins.Value })
	end)
end

Players.PlayerRemoving:Connect(save)
game:BindToClose(function()
	for _, player in Players:GetPlayers() do
		save(player)
	end
end)

Việc lưu dữ liệu có thể thất bại ở phía Roblox, và lỗi đó sẽ khiến script dừng lại, nên thao tác này nằm bên trong pcall. Khóa là UserId của người chơi, không bao giờ thay đổi, khác với tên của họ.

challenge icon

Thử thách

Khó

Lưu Wins và BestTime của mỗi người chơi khi họ rời đi.

  1. Ở gần đầu: local DataStoreService = game:GetService("DataStoreService") và local towerStore = DataStoreService:GetDataStore("Tower").
  2. Viết saveProgress(player): trong một pcall, gọi towerStore:SetAsync(player.UserId, { Wins = ..., BestTime = ... }) với dữ liệu từ leaderstats của họ.
  3. Gọi hàm này khi có Players.PlayerRemoving, và với mỗi người chơi trong game:BindToClose.

Nhấn Chơi, giành chiến thắng rồi nhấn Dừng: Wins của bạn sẽ được lưu để bài học tiếp theo tải lên. Sau đó nhấn Kiểm tra.

Tự làm thử

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 roundStart = 0
local finished = {}

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

-- Mỗi người chơi có Wins, Stage và BestTime trên bảng xếp hạng
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

	local best = Instance.new("NumberValue")
	best.Name = "BestTime"
	best.Parent = leaderstats

	-- Người chơi bị rơi sẽ bắt đầu lại ở chặng cuối cùng họ đã đến,
	-- trừ khi dung nham đã phủ kín chặng đó: lúc này họ chờ ở sảnh
	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)

-- Bảng thông báo trong sảnh
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

-- Bất cứ thứ gì gắn nhãn Kill đều khiến người chơi mất một mạng
local function killOnTouch(hit)
	local humanoid = hit.Parent:FindFirstChild("Humanoid")
	if humanoid then
		humanoid.Health = 0
	end
end

-- Bậc thang biến mất mờ dần ngay sau khi có người đáp xuống, rồi xuất hiện trở lại
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

-- Tia laser trượt đến vị trí lệch Slide rồi quay lại, lặp mãi
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

-- Tấm bật nhảy hất người đáp xuống lên không trung
local function bounce(hit)
	local root = hit.Parent:FindFirstChild("HumanoidRootPart")
	if root then
		root.AssemblyLinearVelocity = Vector3.new(0, Settings.BouncePower, 0)
	end
end

-- Các vật xoay quay một chút ở mỗi khung hình
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)

-- Kích hoạt mọi chướng ngại vật trong chặng theo nhãn của chúng
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
	-- Chạm đến Start của một chặng sẽ cập nhật bảng xếp hạng
	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

-- Chặng càng cao càng khó: một phần ba dưới cùng dễ, một phần ba trên cùng khó
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

-- Đặt bản sao của một mô hình lên đỉnh tháp; trả về bản sao và độ cao đỉnh mới
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

-- Đỉnh tháp: giành chiến thắng rồi trở lại sảnh chờ
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
	local seconds = math.floor((time() - roundStart) * 10) / 10
	local best = player.leaderstats.BestTime
	if best.Value == 0 or seconds < best.Value then
		best.Value = seconds
	end
	print(player.Name .. " reached the top in " .. seconds .. " seconds")
	hit.Parent:PivotTo(lobbySpawn.CFrame + Vector3.new(0, 4, 0))
end

-- Tạo một tháp ngẫu nhiên mới từ dưới lên
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

-- Dung nham dâng từ mặt đất lên sát dưới đỉnh tháp
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)
	roundStart = time()
	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

-- Bài học 20: Phần Save on Leave đặt ở đây

-- Vòng lặp của các lượt chơi
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

Tất cả bài học trong Trò chơi Roblox: Tháp bất tận

Tự luyện tập: Playground Roblox