关于 roblox-datastores
面向 Roblox 服务端开发者的 DataStoreService 持久化指南,适用于保存玩家进度和维护排行榜。内容涵盖安全读写、重试及离开或关服时保存,以降低数据丢失和覆盖风险。
主要功能
- 01玩家加入时加载并离开时保存
- 02使用 UpdateAsync 安全更新数据
- 03用 pcall 处理失败并重试
- 04关服时保存在线玩家数据
- 05使用 OrderedDataStore 创建排行榜
适用场景
- 01保存玩家的金币、物品或等级
- 02避免多个服务器写入造成覆盖
- 03处理临时请求失败和节流
- 04读取并展示排名数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ gamedev-skills/ awesome-gamedev-agent-skills --skill roblox-datastores安装后在 Claude Code 中输入/roblox-datastores确认已加载。
附带文件references/sessions-and-limits.md
详细介绍
Roblox DataStores
使用 DataStoreService 在 Roblox 中跨会话持久化数据:加入时加载、离开和关闭时保存、安全更新、重试,以及用于排行榜的有序存储。仅限服务器端使用。
适用场景
- 用于保存/加载玩家进度(金币、物品栏、等级)、构建持久化排行榜,或修复数据丢失、覆盖和限流问题。
- 用于服务器代码调用
DataStoreService、GetDataStore、GetAsync、SetAsync、UpdateAsync或GetOrderedDataStore的场景。
不适用场景: 通用脚本编写、服务、远程对象、客户端/服务器划分 → roblox-luau。高频临时状态(匹配、每回合状态)→ 内存存储(另一种服务)。与引擎无关的持久化理论 → save-systems。
核心工作流
- 在 Studio 中启用访问权限一次。 File → Game Settings → Security → Enable Studio Access to API Services(使用测试场景;Studio 会访问实时数据)。DataStores 只能通过服务器
Script使用,不能通过LocalScript使用。 - 获取存储,然后按键读取/写入。
DataStoreService:GetDataStore("Name");每位玩家的键通常为"Player_" .. player.UserId。 - 每次调用都用
pcall包装。GetAsync/SetAsync/UpdateAsync是可能失败的网络调用;未处理的失败会导致线程报错,并有数据丢失风险。 - 在
PlayerAdded时加载,在PlayerRemoving时保存,并且也要使用BindToClose。 玩家离开和服务器关闭时都需要进行最后一次保存。 - 读取后修改再写入时优先使用
UpdateAsync(可安全应对多服务器),而不是SetAsync(盲目覆盖)。加载失败时,不要用默认值覆盖——中止保存,避免清除有效数据。 - 使用
OrderedDataStore存储排名数据(排行榜),并通过GetSortedAsync获取。通过加入、修改数据、重新加入并确认数据已持久化来进行测试。
常用模式
1. 加入时加载(使用 pcall 保护)
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("PlayerData")
local DEFAULT = { Coins = 0, Level = 1 }
Players.PlayerAdded:Connect(function(player)
local key = "Player_" .. player.UserId
local ok, data = pcall(function()
return store:GetAsync(key)
end)
if not ok then
-- Load FAILED (network). Do not treat as a new player; flag so we never save
-- over their real data with defaults.
warn("Load failed for", player.Name, data)
player:SetAttribute("DataLoaded", false)
return
end
player:SetAttribute("DataLoaded", true)
local profile = data or DEFAULT -- nil == genuinely new player
applyToLeaderstats(player, profile)
end)
2. 使用 UpdateAsync 保存(可安全应对多服务器)
-- UpdateAsync reads the latest value, then writes what the callback returns.
-- The callback MUST NOT yield (no task.wait, no further Async calls inside it).
local function savePlayer(player)
if player:GetAttribute("DataLoaded") == false then return end -- never overwrite on a bad load
local key = "Player_" .. player.UserId
local newData = gatherDataFor(player) -- a plain table of serializable values
local ok, err = pcall(function()
store:UpdateAsync(key, function(old)
-- merge/decide here; return nil to cancel the write
return newData
end)
end)
if not ok then warn("Save failed for", player.Name, err) end
end
3. 离开时和关闭时都保存
Players.PlayerRemoving:Connect(savePlayer)
-- BindToClose runs when the server shuts down; save everyone still in.
-- It has a limited time budget, so save in parallel and yield until done.
game:BindToClose(function()
local players = Players:GetPlayers()
local remaining = #players
if remaining == 0 then return end
for _, player in players do
task.spawn(function()
savePlayer(player)
remaining -= 1
end)
end
while remaining > 0 do task.wait() end
end)
4. 使用退避策略重试(暂时性故障)
local function withRetry(fn, attempts)
attempts = attempts or 3
for i = 1, attempts do
local ok, result = pcall(fn)
if ok then return true, result end
if i < attempts then task.wait(2 ^ i) end -- 2s, 4s, ... backoff
end
return false
end
local ok, data = withRetry(function() return store:GetAsync(key) end)
5. 增加计数器
-- IncrementAsync is a convenience for integer read-modify-write (still wrap it).
local ok, newTotal = pcall(function()
return store:IncrementAsync("Visits_" .. player.UserId, 1)
end)
6. 使用 OrderedDataStore 创建排行榜
local boards = DataStoreService:GetOrderedDataStore("Coins")
-- Write a player's score (call when it changes, not every frame).
pcall(function() boards:SetAsync("Player_" .. player.UserId, coins) end)
-- Read the top 10, descending.
local ok, pages = pcall(function()
return boards:GetSortedAsync(false, 10) -- ascending=false → highest first
end)
if ok then
for rank, entry in ipairs(pages:GetCurrentPage()) do
print(rank, entry.key, entry.value) -- entry.value is the number
end
end
常见问题
- 未处理的故障会清除进度 → 始终对 Async 调用使用
pcall;加载失败时,标记该会话并拒绝保存,以免默认值覆盖真实数据。 - 服务器之间的
SetAsync竞争 → 两个服务器写入同一个键时可能会互相覆盖。使用UpdateAsync进行读取后修改再写入,确保每次写入都能获取最新值。 - 在
UpdateAsync回调中让出执行权 → 回调不能调用task.wait或其他 Async 函数;应预先计算新值并返回。 - 未在
BindToClose中保存 → 服务器关闭时,玩家尚未保存的进度会丢失;添加game:BindToClose,并在其时间限制内等待保存完成。 - 限流 /“请求过多” → 遵守每个键和每分钟的限额;不要在每次数值变化时都保存。应批量保存,并使用定时器或在玩家离开时保存。
GetAsync会短暂缓存,因此立即重新读取可能得到过期数据。 - 存储不可序列化的值 → 只有可用 JSON 序列化的数据才能持久化:数字、字符串、布尔值,以及键为字符串/数字的表。
Instance、Vector3、CFrame和函数不支持持久化——请先将它们序列化为普通表。 - 未启用 API 访问权限就进行测试 → 在 Studio 中,启用 Enable Studio Access to API Services 之前,DataStores 会静默地无法使用(并且不能通过
LocalScript使用)。 - 有序存储的
DataStoreKeyInfo为 nil →OrderedDataStore不支持版本控制/元数据;需要这些功能时请使用常规DataStore。
参考资料
- 有关会话锁定(防止不同服务器间的数据重复)、使用
DataStoreSetOptions进行版本控制/元数据、有序存储分页(AdvanceToNextPageAsync)、键错误代码和请求限额,以及遵守被遗忘权的要求,请阅读references/sessions-and-limits.md。
相关技能
roblox-luau— 服务、实例、事件以及服务端/客户端模型。save-systems— 与引擎无关的序列化、存档槽位和迁移。
评论
支持 Markdown