Roblox Studio · 第 25 课

保存纪录:第一次认识 DataStore

先用离线练习模式理解读取与保存,再学习用 pcall 和 UpdateAsync 保护最高纪录。

10–15 岁 90 分钟 DataStore / pcall 动手实践 测试与保存

Course overview

让最高纪录留下来,同时学会认真对待保存失败。

前面的金币和任务只活在当前服务器里。DataStore 可以让数据在离开后仍然存在,但网络请求会失败,因此“点了保存”并不等于“已经保存成功”。

这一课分两轮。第一轮默认 PRACTICE=true,只使用当前服务器的内存练习;第二轮由你和家长或老师准备独立测试体验后,才可切换到真实 DataStore。练习模式不会跨 Stop 保存。

01 · BEFORE

开始之前

理解服务器、Attributes、ProximityPrompt 和 pcall。建议分两次学习,先完成练习模式。

02 · MAKE

你的作品

按一次得分站增加 10 分,按保存站保存最高纪录;Output 明确显示练习或云端结果。

03 · CHECK

完成标志

最高纪录不会降低;读取失败时禁止覆盖;失败不能输出“保存成功”。

Creator Hub 的 Data Stores Manager 官方截图,非本课保存成功凭证
图 25-1:官方 Data Stores Manager 界面参考:它用于查看云端数据,不代表本课已经保存成功。练习模式只在当前服务器内模拟保存。Roblox Creator Documentation · CC BY 4.0 · 原图未修改 · 许可说明

Learning route

先准备,再动手;看到结果后,记得保存。

每完成一小步,就测试一次。先说出你预计会看到什么,再用 Play 和 Output 检查,不用急着一次写完所有代码。

1

01 · 先做无需联网的保存练习

在新 Baseplate 中放两个小型 Part:ScoreStation 与 SaveStation,分别加入 ProximityPrompt。一个给本轮分数,一个尝试保存。

2

02 · 为什么用最高值合并

每名玩家用 UserId 组成数据键,比玩家昵称稳定。保存时用 UpdateAsync 读取当前值并返回更大的纪录,避免旧服务器把较高分数写低。

3

03 · 让每一次读写都有结果

代码稍长,可以先读 setup,再读两个 Prompt 的回调。不要一口气背下来;先找到“加载成功才能保存”和“最高纪录取较大值”这两条规则。

4

04 · 准备好测试环境,才切换云端

真实 DataStore 只能由服务器访问。请与家长或老师使用一个独立的测试体验,按当前 Studio 的设置启用所需 API 访问;不要把练习连接到正式游戏的数据。

5

05 · 成功、失败和未保存都要说清

练习模式 Stop 后重置是预期行为,不能拿它作为跨会话保存成功的证明。云端模式要在重新进入后读到相同或更高纪录,才完成这项验证。

Step by step

01 · 先做无需联网的保存练习

在新 Baseplate 中放两个小型 Part:ScoreStation 与 SaveStation,分别加入 ProximityPrompt。一个给本轮分数,一个尝试保存。

这不是完整线上玩家数据系统。它只演示手动保存“只升不降的最高纪录”,没有退出自动保存、货币交易或复杂会话锁。

  1. 两个站台 Size=6,2,6,Position 分别为 -8,1,-10 与 8,1,-10,Anchored=true。
  2. 两个 Prompt 的 ActionText 分别为“练习得分”和“保存纪录”,MaxActivationDistance=10。
  3. 创建 ServerScriptService > BestScore,保持 PRACTICE=true。

Step by step

02 · 为什么用最高值合并

每名玩家用 UserId 组成数据键,比玩家昵称稳定。保存时用 UpdateAsync 读取当前值并返回更大的纪录,避免旧服务器把较高分数写低。

UpdateAsync 的回调可能重试,所以里面只计算返回值,不播放音效、不发奖励,也不能 task.wait。读取失败时不把 0 当作真实旧数据继续覆盖。

  1. 记住 RunScore 是本轮分数,BestScore 是已知最高纪录。
  2. 下面的 readValue 会拒绝不符合预期的数据,不悄悄把坏数据清零。

Step by step

03 · 让每一次读写都有结果

代码稍长,可以先读 setup,再读两个 Prompt 的回调。不要一口气背下来;先找到“加载成功才能保存”和“最高纪录取较大值”这两条规则。

两个站台都有服务器距离检查和冷却。练习模式与云端模式走同样的流程,只是底层存储不同,Output 前缀会明确区分。

  1. 粘贴完整代码,Play 后等待 loaded 输出。
  2. 按得分站三次,再按保存站,应得到 30 分最高纪录。
  3. 保存后立刻再按一次,应被十秒冷却拦住。
ServerScriptService > BestScore · 手动保存最高纪录原型Luau
local Players = game:GetService("Players")
local PRACTICE = true -- 先保持 true;内存练习不跨 Stop 保存
local store = nil
if not PRACTICE then
    store = game:GetService("DataStoreService"):GetDataStore("Lesson25_Best_v1")
end
local memory = {}
local ready, saving, lastSave, lastScore = {}, {}, {}, {}
local scoreStation = workspace:WaitForChild("ScoreStation")
local saveStation = workspace:WaitForChild("SaveStation")
local mode = PRACTICE and "[PRACTICE]" or "[DATASTORE]"

local function readValue(value)
    if value == nil then return 0 end
    assert(typeof(value) == "number" and value == value, "Invalid stored data")
    assert(value >= 0 and value < math.huge and value % 1 == 0, "Invalid score")
    return value
end
local function near(player, station)
    local character = player.Character
    local root = character and character:FindFirstChild("HumanoidRootPart")
    local hum = character and character:FindFirstChildOfClass("Humanoid")
    return root and hum and hum.Health > 0 and (root.Position - station.Position).Magnitude <= 12
end
local function setup(player)
    player:SetAttribute("RunScore", 0)
    local key = "u_" .. player.UserId
    local ok, value = pcall(function()
        if PRACTICE then return readValue(memory[key]) end
        return readValue(store:GetAsync(key))
    end)
    if player.Parent ~= Players then return end
    if not ok then warn(mode, "Load failed; saving disabled", value); return end
    ready[player] = true
    player:SetAttribute("BestScore", value)
    print(mode, player.Name, "loaded", value)
end
Players.PlayerAdded:Connect(setup)
for _, player in ipairs(Players:GetPlayers()) do task.spawn(setup, player) end

scoreStation.ProximityPrompt.Triggered:Connect(function(player)
    if not ready[player] or not near(player, scoreStation) then return end
    local now = os.clock()
    if now - (lastScore[player] or -math.huge) < 0.5 then return end
    lastScore[player] = now
    local score = math.min(9990, (player:GetAttribute("RunScore") or 0) + 10)
    player:SetAttribute("RunScore", score)
    print(mode, "RunScore", score)
end)

saveStation.ProximityPrompt.Triggered:Connect(function(player)
    if not ready[player] or saving[player] or not near(player, saveStation) then return end
    local now = os.clock()
    if now - (lastSave[player] or -math.huge) < 10 then return end
    lastSave[player] = now
    saving[player] = true
    local key = "u_" .. player.UserId
    local score = player:GetAttribute("RunScore") or 0
    local ok, result = pcall(function()
        if PRACTICE then
            memory[key] = math.max(readValue(memory[key]), score)
            return memory[key]
        end
        return store:UpdateAsync(key, function(old)
            return math.max(readValue(old), score)
        end)
    end)
    saving[player] = nil
    if not ok then warn(mode, "Save failed; try again later", result); return end
    if player.Parent == Players then player:SetAttribute("BestScore", result) end
    print(mode, "Saved best:", result)
end)
Players.PlayerRemoving:Connect(function(player)
    ready[player], lastSave[player], lastScore[player] = nil, nil, nil
end)

Step by step

04 · 准备好测试环境,才切换云端

真实 DataStore 只能由服务器访问。请与家长或老师使用一个独立的测试体验,按当前 Studio 的设置启用所需 API 访问;不要把练习连接到正式游戏的数据。

这是权限与云端数据操作,不是这节课必须完成的门槛。没有合适的测试环境,就保留 PRACTICE=true 完成流程学习,不要使用他人的账号或绕过限制。

  1. 先保存本地副本,再确认目标是独立测试体验。
  2. 阅读下方官方 Data Stores 文档,核对当前的 Studio API 访问设置。
  3. 环境准备好后才把 PRACTICE 改为 false,重新运行。
  4. 只有看到 [DATASTORE] Saved best 后,才重新进入验证持久记录。

Step by step

05 · 成功、失败和未保存都要说清

练习模式 Stop 后重置是预期行为,不能拿它作为跨会话保存成功的证明。云端模式要在重新进入后读到相同或更高纪录,才完成这项验证。

这份最高值合并适合纪录,不适合可增可减的金币。把所有货币写成 math.max 会让花掉的钱回来;不要把一个例子用到不相同的数据规则。

  1. 练习模式:确认输出带 PRACTICE,Stop 后不会保留。
  2. 云端模式:先保存高分,再重新进入验证;低分保存不能降低最高纪录。
  3. 加载或保存报错:保留错误信息,暂停下一步,不能显示成功。
  4. 退出前手动保存并等待成功;本课没有自动退出保存。
本课用 Output 观察模拟读写或实际请求结果
图 25-2:官方 Output 界面参考:区分 PRACTICE 模拟反馈、实际请求成功与错误。只有真正重新读取成功,才能验证云端存档。Roblox Creator Documentation · CC BY 4.0 · 原图未修改 · 许可说明

Homework

轮到你来改一改,做出自己的版本。

1 画读写流程

画出加载成功、加载失败、保存成功、保存失败四条路线。

2 区分两种模式

写明哪一种不跨 Stop 保存,哪一种必须有独立云端测试环境。

3 选做:存档状态文字

用 Attribute 显示“正在保存/已保存/保存失败”,但仍由服务器决定状态。

完成标准:先在 Playtest 中验证作品,再点击 Stop,保存为“姓名缩写_Lesson25”或对应 Homework 副本;不要覆盖上一课的可运行版本。

Exit questions

先自己回答,再展开看看。

为什么 key 使用 UserId?

它适合稳定地对应玩家,昵称可能改变。

为什么不能读失败后直接写 0?

读取失败不代表旧数据不存在,写 0 可能覆盖真实纪录。

UpdateAsync 回调能发奖励吗?

不应该,它可能重试,应只计算并返回新值。

最高纪录的 math.max 能直接用于金币余额吗?

不能,余额会减少,需要不同的数据设计。

Explore further

需要查资料时,从这里出发。

正文代码可以直接复制。先确认脚本类型、放置位置,以及这段代码是完整版本还是小实验;不要把同一脚本的多个版本叠加运行。界面参考图已标注来源,图中的对象名可能与本课不同,请以操作步骤为准。