中级教程

创建自定义排行榜与排序数据存储

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

数据存储是一个可以用来保存和加载持久玩家数据的服务,跨越不同的玩家会话。它们存储重要信息,比如玩家的进度或库存,并允许你在玩家下次加入游戏时检索这些信息。如果没有数据存储,玩家每次离开游戏时将失去所有进度。

数据存储有两种类型:标准和排序。本教程使用排序数据存储,它存储可以按照数字进行排名和排序的数据,并根据存储的数值以升序或降序方式检索。

使用 Gold Rush 创建排行榜教程 - 开始 .rbxl 文件作为起始位置,和 Gold Rush 创建排行榜教程 - 完整 .rbxl 文件作为参考,本教程为你提供创建自定义玩家排行榜所需的基础,包括以下指导:

  • 在排序数据存储中存储金币分数。
  • 获取和排序所有服务器全球的高分。
  • 在用户界面中显示自定义排行榜的高分。
  • 每隔几秒自动刷新自定义排行榜。

到本教程结束时,你应该具备以下内容:

  1. ServerScriptService 中:
    1. 一个更新后的 GoldManager 脚本,该脚本将玩家的分数保存到排行榜的排序数据存储中。
    2. 一个新的 LeaderboardRemoteHandler 模块脚本,该脚本从排行榜获取并返回已排序的高分列表。
    3. 一个新的 LeaderboardManager 脚本,该脚本响应来自客户端的排行榜数据请求,充当 LeaderboardGuiScript 中客户端与 LeaderboardManager 中服务器之间的桥梁。
  2. StarterGui 下,一个新的 LeaderboardGuiScript 本地脚本,该脚本为玩家渲染排行榜用户界面。

启用工作室访问 API 服务

数据存储并不在你的设备上本地存储,因此你的游戏依赖于与 Roblox 后端的服务器间通信来使用它们。默认情况下,工作室会限制这种通信,以防止滥用或意外使用。在你显式启用之前,工作室将禁用对 API 服务的访问,以确保只有受信任的游戏能够读取和写入 Roblox 的后端服务器。

要启用工作室对 API 服务的访问,以便你可以使用数据存储:

  1. 在工作室中打开 Gold Rush 创建排行榜教程 - 开始 文件。这个起始文件已经包含了 保存玩家数据 教程中的代码和玩家排行榜的用户界面骨架。
  2. 返回工作室,转到 文件体验设置安全性
  3. 打开 启用工作室访问 API 服务
  4. 保存你的更改。

创建排序数据存储

创建数据存储时,你应该始终从服务器端 Script 调用 DataStoreService。这样做很重要,因为:

  • Roblox 阻止客户端对所有数据存储的访问,因此只有服务器有权限访问和修改持久玩家数据。
  • 服务器端脚本在一个集中环境中运行,确保读取和写入你的数据存储的数据是准确和有效的。
  • 由于客户端脚本在玩家的设备上运行,任何玩家都可能利用游戏并窃取或覆盖其他玩家的数据,如果客户端能够访问你的数据存储。

你还应该给你的数据存储一个唯一的名称,以保持你的数据有序,并防止在不同的数据存储中发生重叠或冲突。在本教程中,排行榜的数据存储被称为 GlobalLeaderboard

要创建 GlobalLeaderboard 排序数据存储:

  1. 打开 ServerScriptService 下现有的 GoldManager 脚本。

  2. 在现有的 PlayerGold 数据存储下,调用 GetOrderedDataStore 来创建字符串 "GlobalLeaderboard"。这会创建一个排序数据存储,在玩家获得金币时保存玩家的分数。


    local leaderboardStore = DataStoreService:GetOrderedDataStore("GlobalLeaderboard")

创建数据存储后,你可以在不同的脚本中引用这个相同的数据存储。例如,你可以在 GoldManager 脚本和稍后在本教程中创建的 LeaderboardManager 模块脚本中调用 GetOrderedDataStore。在两个地方引用相同的数据存储并不是说你在复制数据;这只是意味着两个脚本正在访问相同的集中存储,就像两个队友使用一个文件夹共享信息一样。

将玩家数据发送到排行榜

数据存储由键组成,键标识数据,以及值,值存储数据。与标准数据存储不同,排序数据存储允许你按 排序数据,这使它们非常适合创建排行榜等事物。

每个入口在排序数据存储中都存储在一个唯一的键下,你可以按数字对其进行排名和排序,然后按照存储的数值以升序或降序检索。这意味着你可以使用排序数据存储,例如,展示一个实时更新的前 3 名列表,随着玩家在你的游戏中收集更多金币而更新。

描述键是一个唯一的字符串标识符,你可以用来访问特定的数据,例如玩家的用户 ID。

键让你组织数据,以便可以轻松管理和调试。
值是你希望保存或加载的实际数字数据,例如玩家的分数。

值让你在会话之间存储进度,以及在玩家下次加入游戏时检索玩家数据。
允许的格式只能是字符串。如果你使用的是数字,如用户 ID,请使用 tostring() 将其转换为字符串。只能是数字。
示例"Player_A""TopScore""123456"(用户 ID 作为字符串)1003.140-25

现在你已经创建了 GlobalLeaderboard 排序数据存储,请更新 GoldManager 脚本中现有的 saveGold 函数,通过 SetAsync 将玩家的金币分数发送到你的自定义排行榜:


local function saveGold(player, value)
local userId = player.UserId
local success, err = pcall(function()
goldStore:SetAsync(userId, value)
leaderboardStore:SetAsync(tostring(userId), value)
end)
if not success then
warn("无法保存", player.Name, "的金币:", err)
end
end

你可以将排序数据存储 GlobalLeaderboard 想象成一个共享文件夹:SetAsync 将内容添加到文件夹,而 GetSortedAsync(稍后你将在 LeaderboardManager 模块脚本中创建)读取文件夹中的内容并按分数排序。

获取排行榜的分数

要获取排行榜的分数,你应该使用 ModuleScript。模块脚本让你可以在多个脚本中组织和重用代码。你可以在模块脚本中编写一次逻辑,然后在任何需要它的地方调用它,而不是在不同的地方复制相同的逻辑。

当你将排行榜逻辑存储在一个模块中时,UI 处理程序和服务器脚本都可以使用相同的函数来获取顶级玩家分数。如果你需要,例如,更新如何获取用户名或如何对分数进行排序,你只需在一个地方更改代码即可。更新模块后,所有使用它的脚本也会自动获得更新的行为。

你的自定义排行榜使用以下排序数据存储方法在不同服务器之间共享数据:

  • SetAsync(来自 GoldManager 脚本)将玩家分数发送到 GlobalLeaderboard 排序数据存储。
  • GetSortedAsync(在模块脚本中)从所有服务器获取该数据存储中的高分。GetSortedAsync 方法是排序数据存储专有的,能够根据特定的排序顺序、页面大小、最小和最大值检索多个排序键。

要获取排行榜的分数:

  1. ServerScriptService 下,创建一个新的 ModuleScript,命名为 LeaderboardManager

  2. 在模块脚本的顶部,调用 GetOrderedDataStore 创建字符串 "GlobalLeaderboard"。引用你在 GoldManager 脚本中创建的相同排序数据存储,将允许排行榜 UI 后面获取和排序顶级玩家分数。


    local DataStoreService = game:GetService("DataStoreService")
    local Players = game:GetService("Players")
    local leaderboardStore = DataStoreService:GetOrderedDataStore("GlobalLeaderboard")
  3. 准备一个模块表来保存你想要暴露给其他脚本的所有函数和数据。


    local leaderboardManager = {}
  4. 创建一个名为 GetTopScores 的函数,以获取顶级玩家分数。此函数:

    • 调用 GetSortedAsync 从数据存储中获取 limit 数量的高分,按降序排列。如果调用失败,将返回一个空列表,以防止其余代码崩溃。
    • 遍历排行榜以获取带有 GetCurrentPage 的条目的表格,并使用 GetNameFromUserIdAsync 获取玩家用户名。
    • 将每个结果添加到最终列表,并用玩家用户名和分数组成排行榜表。
    • 返回最终的玩家列表,并将排行榜数据发送回任何调用此 leaderboardManager.GetTopScores 函数的脚本。

    function leaderboardManager.GetTopScores(limit)
    local success, pages = pcall(function()
    return leaderboardStore:GetSortedAsync(false, limit)
    end)
    if not success or not pages then
    return {}
    end
    local results = {}
    for _, entry in ipairs(pages:GetCurrentPage()) do
    local userId = tonumber(entry.key)
    local score = entry.value
    local username = "未知"
    local ok, name = pcall(function()
    return Players:GetNameFromUserIdAsync(userId)
    end)
    if ok then
    username = name
    end
    table.insert(results, {
    Username = username,
    Score = score
    })
    end
    return results
    end
  5. 通过返回你之前创建的模块表来使该模块可用。每当你创建一个模块脚本时,必须返回模块表,以便其他脚本可以访问它并调用其中的函数。


    return leaderboardManager

从服务器请求排行榜数据

为了使客户端 UI 显示排行榜分数,服务器必须在客户端请求数据时做出响应。RemoteFunction 允许客户端请求来自服务器的信息并等待回复。

要从服务器请求排行榜数据:

  1. ReplicatedStorage 下,插入一个名为 LeaderboardRemoteRemoteFunction。这个远程函数充当客户端上的 UI 脚本与服务器上的排行榜逻辑之间的桥梁。
  2. ServerScriptService 下,创建一个名为 LeaderboardRemoteHandler 的脚本。在此脚本中:
    1. 调用 ReplicatedStorage(客户端和服务器之间的共享空间)、ServerScriptService(持有排行榜模块)和 leaderboardRemote(客户端将调用以请求排行榜数据的远程函数)。


      local ReplicatedStorage = game:GetService("ReplicatedStorage")
      local ServerScriptService = game:GetService("ServerScriptService")
      local leaderboardRemote = ReplicatedStorage:WaitForChild("LeaderboardRemote")
    2. 加载你之前创建的 LeaderboardManager 模块,其中包含从排序数据存储中获取高分的函数。


      local leaderboardManager = require(ServerScriptService:WaitForChild("LeaderboardManager"))
    3. 设置客户端调用 InvokeServer 时的行为。服务器通过获取前三个高分并将结果发送回客户端来响应。


      leaderboardRemote.OnServerInvoke = function(player)
      return leaderboardManager.GetTopScores(3)
      end

在自定义排行榜 UI 中显示数据

设置好服务器端脚本后,创建一个本地脚本,每隔 3 秒获取前 3 名玩家并在游戏内的全局 UI 排行榜中显示结果。

要在你的自定义排行榜中显示数据:

  1. StarterGuiLeaderboardGui 中,创建一个名为 LeaderboardGuiScript 的本地脚本。

  2. 在脚本顶部,与远程函数连接。调用 ReplicatedStorage 以获取服务器和客户端之间的共享对象,然后调用 leaderboardRemote 以允许客户端请求服务器的排行榜数据。


    local ReplicatedStorage = game:GetService("ReplicatedStorage")
    local leaderboardRemote = ReplicatedStorage:WaitForChild("LeaderboardRemote")
  3. 引用 LeaderboardGui 下现有的排行榜 UI 元素。


    local gui = script.Parent
    local leaderboardFrame = gui:WaitForChild("LeaderboardFrame")
    local entriesFrame = leaderboardFrame:WaitForChild("EntriesFrame")
    local template = entriesFrame:WaitForChild("LeaderboardEntry")
  4. 创建一个名为 clearEntries 的函数以清除旧的排行榜条目。此函数遍历 entriesFrame 的所有子项,该框架包含排行榜行,并确保排行榜在显示新数据之前完全刷新。


    local function clearEntries()
    for _, child in ipairs(entriesFrame:GetChildren()) do
    if child:IsA("TextLabel") and child.Name:match("^Row%d+$") then
    child:Destroy()
    end
    end
    end
  5. 创建一个名为 renderLeaderboard 的函数以在排行榜行中插入和显示玩家数据。data 对象是服务器返回的一个表,其中包含带有玩家用户名和分数的条目。


    local function renderLeaderboard(data)
    clearEntries()
    for i, entry in ipairs(data) do
    local row = template:Clone()
    row.Name = "Row" .. i
    row.Visible = true
    row.LayoutOrder = i
    row.Text = string.format("%d%s 名: %s - %d",
    i, i == 1 and "位" or i == 2 and "位" or i == 3 and "位" or "位",
    entry.Username, entry.Score)
    row.Parent = entriesFrame
    end
    end
  6. 创建一个循环以定期获取排行榜数据。每 3 秒,循环运行并使用你在 LeaderboardRemoteHandler 脚本中创建的 InvokeServer 函数调用服务器。如果调用成功,UI 将使用更新的分数刷新。


    while true do
    local success, data = pcall(function()
    return leaderboardRemote:InvokeServer()
    end)
    if success and data then
    renderLeaderboard(data)
    end
    task.wait(3)
    end

最终代码

请参见以下代码片段,获取完整的 GoldManagerLeaderboardManagerLeaderboardRemoteHandlerLeaderboardGuiScript 脚本。你可以直接在 Gold Rush 创建排行榜教程 - 开始 .rbxl 文件中粘贴并运行它们。

GoldManager

GoldManager 脚本创建一个排序数据存储以管理排行榜更新。


-- 获取服务
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CollectionService = game:GetService("CollectionService")
local DataStoreService = game:GetService("DataStoreService")
-- 用于更新 UI 的远程事件
local uiEvent = ReplicatedStorage:WaitForChild("UIEvent")
-- 用于保存玩家金币块的数据存储
local goldStore = DataStoreService:GetDataStore("PlayerGold")
-- 用于保存排行榜金币的排序数据存储
local leaderboardStore = DataStoreService:GetOrderedDataStore("GlobalLeaderboard")
-- 为金币块定位创建一个随机数生成器
local random = Random.new()
-- 用于存储单个会话中玩家金币块的表
local playerGold = {}
-- 常量
local GOLD_VALUE = 10 -- 每个金币块的价值
local GOLD_REGENERATION_BOUNDS = { -- 用于随机重新定位金币块的区域边界
minX = -72,
maxX = 72,
minZ = -72,
maxZ = 72
}
local GOLD_REGEN_DELAY = 2 -- 块重新生成的延迟(以秒为单位)
local AUTOSAVE_INTERVAL = 30 -- 玩家块的保存频率(以秒为单位)
-- 处理玩家收集金币块的函数
local function processGoldTouch(chunk, player)
-- 玩家收集金币块时隐藏块
chunk.Parent = nil
-- 将金币块添加到玩家的会话金币分数中
local userId = player.UserId
playerGold[userId] = (playerGold[userId] or 0) + GOLD_VALUE
print(player.Name, "现在有", playerGold[userId], "金币")
-- 更新 UI 分数条,以反映此会话的最新金币分数
uiEvent:FireClient(player, {
gold = playerGold[userId],
doTween = true,
showAlert = false
})
-- 等待一会儿,然后在随机位置重新生成刚刚收集的金币块
task.delay(GOLD_REGEN_DELAY, function()
-- 在新随机位置重新生成金币块
local nextPositionX = random:NextInteger(GOLD_REGENERATION_BOUNDS.minX, GOLD_REGENERATION_BOUNDS.maxX)
local nextPositionZ = random:NextInteger(GOLD_REGENERATION_BOUNDS.minZ, GOLD_REGENERATION_BOUNDS.maxZ)
chunk.CFrame = CFrame.new(nextPositionX, 50, nextPositionZ) -- 将块移动到新位置
chunk:SetAttribute("DropCollided", false) -- 重置任何碰撞标志
chunk.Parent = workspace.GoldChunks
end)
end
-- 遍历所有标记为 "Gold" 的部件并将其连接至触摸检测
for _, chunk in ipairs(CollectionService:GetTagged("Gold")) do
chunk.Touched:Connect(function(otherPart)
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
if player then
processGoldTouch(chunk, player)
end
end)
end
-- 函数以将玩家金币保存到标准和排序数据存储
local function saveGold(player, value)
local userId = player.UserId
local success, err = pcall(function()
goldStore:SetAsync(userId, value)
leaderboardStore:SetAsync(tostring(userId), value)
end)
if not success then
warn("无法保存", player.Name, "的金币:", err)
end
end
-- 处理玩家加入游戏的函数
local function onPlayerAdded(player)
local userId = player.UserId
print(player.Name, "已加入游戏")
-- 尝试从数据存储加载玩家的金币
local success, storedGold = pcall(function()
return goldStore:GetAsync(userId)
end)
-- 使用数据存储值设置当前会话金币分数
local currentGold = if success and storedGold then storedGold else 0
playerGold[userId] = currentGold
-- 使用数据存储值更新 UI
uiEvent:FireClient(player, {
gold = currentGold,
doTween = false,
showAlert = not success
})
if success then
print("加载了", currentGold, "金币对于", player.Name)
else
-- 通知玩家他们的金币无法被加载
uiEvent:FireClient(player, { showAlert = true })
warn("无法加载", player.Name, "的金币")
end
-- 启动自动保存协程以每 30 秒保存玩家的金币
coroutine.wrap(function()
while player.Parent do
task.wait(AUTOSAVE_INTERVAL)
if playerGold[userId] then
saveGold(player, playerGold[userId])
print("自动保存", playerGold[userId], "金币对于", player.Name)
end
end
end)()
end
-- 处理玩家离开游戏的函数
local function onPlayerRemoving(player)
local userId = player.UserId
print(player.Name, "已离开游戏")
-- 检查是否跟踪了玩家的金币
if playerGold[userId] then
-- 在他们离开之前将玩家的金币保存到数据存储中
saveGold(player, playerGold[userId])
end
end
-- 连接玩家加入和离开事件
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)

LeaderboardManager

LeaderboardManager 模块脚本:

  • 在 GlobalLeaderboard 排序数据存储上调用 GetSortedAsync()
  • 使用 GetNameFromUserIdAsync 将用户 ID 转换为用户名。
  • 返回一个 { Username, Score } 表的列表,用于排名前的玩家。
  • 在客户端请求排行榜数据时由 LeaderboardRemoteHandler 调用。

-- 获取服务
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
-- 排序数据存储以保存排行榜分数
local leaderboardStore = DataStoreService:GetOrderedDataStore("GlobalLeaderboard")
-- 存储排行榜分数的表
local leaderboardManager = {}
-- 函数以返回前高分
function leaderboardManager.GetTopScores(limit)
-- 尝试从数据存储中检索一个排序的排行榜条目页面
local success, pages = pcall(function()
-- false = 降序(最高分优先)
return leaderboardStore:GetSortedAsync(false, limit)
end)
-- 如果调用失败或返回无内容,记录警告并返回空表
if not success or not pages then
warn("获取排行榜数据失败")
return {}
end
-- 创建一个表以存储顶级玩家数据
local topPlayers = {}
-- 遍历当前页面的每个排行榜条目结果
for _, entry in ipairs(pages:GetCurrentPage()) do
local userId = tonumber(entry.key) -- 将条目键转换为数字(userId 存储为字符串)
local score = entry.value -- 该值是玩家的得分(金币数量)
local username = "未知" -- 默认名称以防查找失败
-- 尝试从 userId 获取玩家的用户名
local ok, name = pcall(function()
return Players:GetNameFromUserIdAsync(userId)
end)
-- 如果查找成功,存储真实的用户名
if ok then
username = name
end
-- 将该玩家及其分数添加到列表中
table.insert(topPlayers, {
Username = username,
Score = score
})
end
-- 返回包含用户名和分数的完整列表
return topPlayers
end
-- 返回 LeaderboardManager 模块,以便其他脚本可以使用它
return leaderboardManager

LeaderboardRemoteHandler

LeaderboardRemoteHandler 脚本从服务器获取并返回前三个得分给调用客户端。


-- 获取服务
local ServerScriptService = game:GetService("ServerScriptService") -- 访问服务器端模块
local ReplicatedStorage = game:GetService("ReplicatedStorage") -- 共享客户端-服务器通信对象
-- 等待客户端用来请求排行榜数据的 RemoteFunction
local leaderboardRemote = ReplicatedStorage:WaitForChild("LeaderboardRemote")
-- 加载包含 GetTopScores 函数的 LeaderboardManager 模块
local leaderboardManager = require(ServerScriptService:WaitForChild("LeaderboardManager"))
-- 设置客户端调用排行榜远程函数时的处理程序
leaderboardRemote.OnServerInvoke = function(player)
-- 调用排行榜模块以获取前 3 名分数并将其返回给客户端
return leaderboardManager.GetTopScores(3)
end

LeaderboardGuiScript

LeaderboardGuiScript 本地脚本:

  • 每 3 秒请求一次服务器的排行榜数据,使用 Class.RemoteHandler.InvokeServer|InvokeServer
  • 使用隐藏的 LeaderboardEntry 模板显示每个排行榜行。
  • 将每行格式化为 "第 1 名: 用户名 - 分数"。
  • 使用 UIListLayout 管理排行榜行的创建、显示和布局。

-- 获取用于访问远程函数的共享存储
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- 获取用于请求服务器排行榜数据的远程函数
local leaderboardRemote = ReplicatedStorage:WaitForChild("LeaderboardRemote")
-- UI 引用
local gui = script.Parent -- 屏幕 GUI 下的 LocalScript
local leaderboardFrame = gui:WaitForChild("LeaderboardFrame") -- 排行榜的主要容器
local entriesFrame = leaderboardFrame:WaitForChild("EntriesFrame") -- 排行榜条目出现的框架
local template = entriesFrame:WaitForChild("LeaderboardEntry") -- 用于创建行的隐藏模板 TextLabel
-- 函数以清除旧的排行榜行(但不清除模板)
local function clearEntries()
for _, child in ipairs(entriesFrame:GetChildren()) do
-- 只销毁名为 "Row1"、"Row2" 等的标签(保持模板不变)
if child:IsA("TextLabel") and child.Name:match("^Row%d+$") then
child:Destroy()
end
end
end
-- 函数以在屏幕上显示排行榜数据
local function renderLeaderboard(data)
-- 首先清除旧行
clearEntries()
for i, entry in ipairs(data) do
local row = template:Clone() -- 克隆隐藏模板
row.Name = "Row" .. i -- 给它一个唯一名称
row.Visible = true -- 使其可见
row.LayoutOrder = i -- 让 UIListLayout 正确排序它
-- 排行榜格式: "第 1 名: 用户名 - 123"
row.Text = string.format("%d%s 名: %s - %d",
i,
i == 1 and "位" or i == 2 and "位" or i == 3 and "位" or "位",
entry.Username,
entry.Score
)
-- 将数据添加到 UI
row.Parent = entriesFrame
end
end
-- 主循环: 每 3 秒更新一次排行榜
while true do
-- 请求服务器的排行榜数据
local success, data = pcall(function()
return leaderboardRemote:InvokeServer()
end)
if success and data then
-- 将结果显示在排行榜上
renderLeaderboard(data)
else
warn("获取排行榜数据失败")
end
-- 等待 3 秒后再更新
task.wait(3)
end
©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。