mcpField
R
roblox-datastores

roblox-datastores

github.com/gamedev-skills数据与数据库5.4k6.3k 次安装1 天前更新
代码仓库

关于 roblox-datastores

面向 Roblox 服务端开发者的 DataStoreService 持久化指南,适用于保存玩家进度和维护排行榜。内容涵盖安全读写、重试及离开或关服时保存,以降低数据丢失和覆盖风险。

主要功能

  1. 01玩家加入时加载并离开时保存
  2. 02使用 UpdateAsync 安全更新数据
  3. 03用 pcall 处理失败并重试
  4. 04关服时保存在线玩家数据
  5. 05使用 OrderedDataStore 创建排行榜

适用场景

  1. 01保存玩家的金币、物品或等级
  2. 02避免多个服务器写入造成覆盖
  3. 03处理临时请求失败和节流
  4. 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。

核心工作流

  1. 在 Studio 中启用访问权限一次。 File → Game Settings → Security → Enable Studio Access to API Services(使用测试场景;Studio 会访问实时数据)。DataStores 只能通过服务器 Script 使用,不能通过 LocalScript 使用。
  2. 获取存储,然后按键读取/写入。 DataStoreService:GetDataStore("Name");每位玩家的键通常为 "Player_" .. player.UserId。
  3. 每次调用都用 pcall 包装。 GetAsync/SetAsync/UpdateAsync 是可能失败的网络调用;未处理的失败会导致线程报错,并有数据丢失风险。
  4. 在 PlayerAdded 时加载,在 PlayerRemoving 时保存,并且也要使用 BindToClose。 玩家离开和服务器关闭时都需要进行最后一次保存。
  5. 读取后修改再写入时优先使用 UpdateAsync(可安全应对多服务器),而不是 SetAsync(盲目覆盖)。加载失败时,不要用默认值覆盖——中止保存,避免清除有效数据。
  6. 使用 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