开发 / ROBLOX
第一次保存 Roblox 进度:读取、修改与重新进入
用一个计数器学习持久化保存。我们将区分玩家数据和 Studio 项目文件,准备独立的测试体验,并避免把读取失败误当成没有记录。本教程是入门练习,不是可直接用于正式游戏经济系统的完整方案。
明确退出后需要保留什么 #
设想一个小工坊:玩家完成三个订单,关闭游戏,晚上再回来。工坊建筑属于项目,而完成订单的数量属于这名玩家。保存关卡文件不会自动保存每位访客不断变化的个人进度。
先使用一个整数,不要同时加入货币、物品、任务和交易。否则第一次出现异常时,很难判断究竟哪一步改变了数据。练习计数器可以叫 TestCompletedOrders。界面应显示服务器实际读取到的值,而不是预先填入的数字。在制作按钮之前,先写下再次进入时应看到什么,以及怎样判断结果正确。
准备独立的测试体验 #
为练习新建一个独立的 experience,发布后合理限制访问。仅在正式体验中增加一个 place,不能达到相同的隔离效果:同一体验的不同 place 可以访问它的数据存储。修改设置前,先在 Creator Hub 确认当前打开的是哪个体验。
Roblox 文档在 Experience Settings 的 Security 中介绍 Studio 的 API 访问设置。只为独立测试体验启用它。Studio 的菜单位置可能变化;界面不一致时,应查阅当前文档。不要为了更快开始试验,就允许 Studio 访问正式玩家数据。你不熟悉的线上记录不能成为这次练习的实验对象。
保存逻辑放在服务器 #
DataStore 操作由服务器 Script 执行。本练习可将脚本放在 ServerScriptService。LocalScript 可以显示加载状态和结果,但不应自行决定已保存的计数,也不应擅自宣布写入成功。
将工作分成三步:玩家进入时读取数据,执行服务器允许的修改,再保存修改。如果全部逻辑混在按钮点击处理里,就容易把每次点击都当成成功。在工坊场景中,“订单完成”意味着服务器验证了任务。界面上出现一次点击,并不能证明玩家获得了进度。
固定存储名称与玩家键 #
为测试存储选择固定名称,并根据 Player.UserId 构建键。本练习建议将存储命名为 GuidebookOrdersTest_v1,将键写成 User_ 加用户标识。这些只是我们的练习约定,并非 Roblox 强制要求的格式。
不要使用 Display Name 作为身份标识,因为不同玩家可以拥有相同的显示名称。第一次和第二次测试之间,也不要修改存储名称,否则可能读取另一份存储,从而误认为旧数据丢失。将体验、存储名称和键的生成规则记录在测试日志中。以后检查异常时,就能确认两次操作是否真的针对同一条记录。
数据不存在与读取失败不同 #
第一次读取成功时可能得到 nil,表示该键下尚无保存值。只有成功读取并确认记录不存在后,本练习才创建初始计数。如果请求失败,没有可用结果并不能说明已经保存的档案不存在。
设置明确状态,例如 Loading、Ready 和 LoadFailed。处于 Loading 时,禁止改变进度的操作。进入 LoadFailed 后显示清晰提示,不要用零覆盖内容未知的数据。第一次练习可以直接停止进度修改并结束测试。重试策略需要单独设计;不能借此悄悄把原档案替换成新档案。
| 状态 | 预期行为 |
|---|---|
| Loading | 禁止修改进度 |
| Ready | 允许服务器认可的操作 |
| LoadFailed | 不要写入初始档案 |
处理错误而不虚报成功 #
数据存储调用可能失败。Roblox 文档用 pcall 展示错误处理,但写上 pcall 并不会自动让保存变得可靠。关键是检查返回结果,并明确应用在失败后怎么处理。
“已发起尝试”和“保存已确认”是不同状态。在 Output 中分别记录读取、修改和写入。服务器获得成功结果之前,界面不能显示“已保存”。玩家可见的错误信息不应包含完整档案或个人信息。本练习只需简短解释与失败阶段。更详细的诊断应留在受控测试环境中,按照操作顺序与预期结果比较。
理解更新方式的区别 #
选择写入方法前,比较 SetAsync 与 UpdateAsync。SetAsync 写入你提供的值;UpdateAsync 根据当前保存值执行转换。多个服务器同时写入时,直接写回旧的本地值可能覆盖另一项修改。
UpdateAsync 的 callback 不能暂停执行,例如不能在其中调用 task.wait。不要在转换函数里发放外部奖励,并假定函数永远只执行一次。先描述数据的变化规则,再单独通知玩家已经确认的结果。完整的跨服务器档案方案还需要更多设计;本入门教程没有提供会话所有权或购买处理系统。
将下面的学习代码放入 ServerScriptService 中名为 StoreExperiment 的 ModuleScript。服务器 Script 用 DataStoreService:GetDataStore("GuidebookOrdersTest_v1") 获取测试存储,通过 require 加载模块,并调用 StoreExperiment.new(store) 创建适配器。调用 adapter:Load(player.UserId) 读取。服务器验证真实练习操作后,再调用 adapter:CompleteTestOrder(player.UserId)。它不是按钮处理函数,也不会自动发放进入奖励。成功返回 true 和数字;失败返回 false 与 LoadFailed 或 SaveFailed。请与表格对照。练习只接受 0 到 1,000,000 的整数,达到该自定上限后停止增加。另一次独立调用会再次增加,因此没有防重复奖励机制。代码已用模拟存储进行本地 Luau 检查,未运行 Studio 或真实 API。
-- ModuleScript: StoreExperiment (ServerScriptService).
-- Learning adapter only; no sessions, receipts, retries or production economy.
local StoreExperiment = {}
local function keyFor(userId)
assert(type(userId) == "number" and userId > 0
and userId < math.huge and userId == math.floor(userId), "Invalid UserId")
return "User_" .. tostring(userId)
end
local function counter(value)
if value == nil then return 0 end
assert(type(value) == "number" and value >= 0 and value <= 1000000
and value == math.floor(value), "Unexpected counter format")
return value
end
function StoreExperiment.new(store)
local adapter = {}
function adapter:Load(userId)
local key = keyFor(userId)
local ok, result = pcall(function()
return counter(store:GetAsync(key))
end)
if not ok then return false, "LoadFailed" end
return true, result
end
function adapter:CompleteTestOrder(userId)
-- Call only after the server verifies the exercise action.
-- A failed read never authorizes a write of initial data.
local loaded = self:Load(userId)
if not loaded then return false, "LoadFailed" end
local key = keyFor(userId)
local ok, result = pcall(function()
return store:UpdateAsync(key, function(current)
local value = counter(current)
assert(value < 1000000, "Exercise counter limit reached")
return value + 1
end)
end)
if not ok then return false, "SaveFailed" end
return true, result
end
return adapter
end
return StoreExperiment
清楚说明练习范围 #
学习用计数器不是正式钱包。用于真实经济系统之前,需要决定哪个服务器管理档案、如何识别重复操作、网络失败怎样处理,以及服务器关闭时怎样处理待写入的数据。
不要每帧保存,也不要每次界面文字改变就保存。写入频率取决于服务限制和可接受的进度损失。本练习观察一次明确写入,以及新会话中的读取。这能帮助理解机制,但不能证明一次写入就适合所有游戏,也不能保证成功请求后再无故障。
测试两个连续会话 #
启动之前先准备测试日志。第一次进入时,等待加载成功,完成一次测试操作,收到写入确认,并记下界面显示的计数。结束该会话。随后使用相同账号、存储与键,重新进入同一测试体验。
比较新加载值与预期值。在旧会话里重新打开一个界面,不能代替持久化测试。进一步调查时,也要考虑 GetAsync 的缓存:连续两次读取不一定证明分别访问了后端的最新状态。应准确描述看到的结果,而不是把每一次重复读取都称为全新的服务端验证。
检查失败与玩家隔离 #
两次正常进入只是部分检查。还要确认另一名用户使用独立键,修改第一个计数不会影响第二个。读取失败不能触发初始数据写入;写入失败不能出现保存成功提示。
为受控测试,可将持久化接口与其余游戏逻辑分开,暂时使用返回失败的练习适配器。不要损坏真实记录来制造错误。模拟测试检查程序分支,但随后仍需要真实 API 测试。二者必须分别标注,不能将模拟响应当成实际执行 Roblox 服务调用的证据。
| 检查 | 通过条件 |
|---|---|
| 再次进入 | 加载已确认的计数 |
| 另一玩家 | 独立键与数值 |
| 读取失败 | 不写入空数据 |
| 写入失败 | 不显示保存成功 |
再次进入后又显示零怎么办 #
按顺序检查:体验是否正确,存储名称是否一致,进入者的 UserId 是否符合预期,上次写入是否成功,以及应用是否在读取失败后建立了初始数据。按时间顺序阅读 Output,不要只盯着最后一条红色错误。
不要为了消除疑问就批量删除数据。先保存日志,用独立键复现原因。如果试验意外影响了正式游戏,应停止实验性写入,再了解可用的数据管理工具。不断重启相同的错误逻辑,可能扩大损失,而不是帮助找出原因。
第一阶段怎样算完成 #
只有满足以下条件,练习才通过:计数在已确认的会话之间保留,玩家数据彼此隔离,读取失败不会写入空档案,写入失败不会被显示为成功。记录实际账号、环境、操作与观察到的值,不要只写一句“能够工作”。
下一阶段处理跨服务器更新、有限重试和档案所有权。在这些检查完成前,不应把练习直接搬到已有购买和收藏系统的正式游戏。本草稿没有运行 Studio;本文提供的是解释和测试计划,并非已经完成的游戏内测试报告。
原始资料
Roblox Creator Hub — Data storesRoblox Creator Hub — Best practices for data stores