roblox-data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
Roblox Data Persistence Reference
Roblox 数据持久化参考文档
1. Overview
1. 概述
Data persistence in Roblox means saving player progress so it survives across sessions. Every time a player joins, the server loads their data from the cloud; every time they leave (or periodically), it saves back.
When data flows:
Player Joins --> Server loads from DataStore --> Populate in-game objects
Player Plays --> Data lives in server memory --> Auto-save on interval
Player Leaves --> Server saves to DataStore --> Data persists for next sessionData architecture decisions:
| Approach | Best For | Complexity |
|---|---|---|
| Raw DataStoreService | Simple games, prototypes | Low |
| ProfileStore | Production games (USE THIS) | Medium |
| Custom wrapper | Specific advanced requirements | High |
Use ProfileStore for any game that will ship. Raw DataStore examples in sections 2-3 exist to explain the underlying system. Do NOT implement manual auto-save, session locking, BindToClose handlers, or retry logic - ProfileStore handles all of this automatically. Section 4 is the production pattern.
Prerequisite: Enable API Services in Roblox Studio under Game Settings > Security > Enable Studio Access to API Services. Without this, DataStore calls will fail in Studio testing.
Roblox中的数据持久化指的是保存玩家进度,使其在不同会话间得以保留。玩家每次加入游戏时,服务器会从云端加载他们的数据;每次离开(或定期)时,数据会重新保存回云端。
数据流转流程:
Player Joins --> Server loads from DataStore --> Populate in-game objects
Player Plays --> Data lives in server memory --> Auto-save on interval
Player Leaves --> Server saves to DataStore --> Data persists for next session数据架构方案选择:
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| 原生DataStoreService | 简单游戏、原型开发 | 低 |
| ProfileStore | 正式上线游戏(推荐使用) | 中 |
| 自定义封装 | 特定高级需求 | 高 |
所有要上线的游戏都请使用ProfileStore。第2-3节中的原生DataStore示例仅用于解释底层系统。请勿手动实现自动保存、会话锁定、BindToClose处理器或重试逻辑——ProfileStore会自动处理所有这些内容。第4节是生产环境的标准实现方案。
前置条件:在Roblox Studio的游戏设置 > 安全 > 启用Studio对API服务的访问中开启API服务。否则,在Studio测试时DataStore调用会失败。
Quick Reference
快速参考
Load Full Reference below only when you need specific implementation examples or migration patterns.
Key rules:
- ALWAYS use ProfileStore for player data. Never raw DataStoreService for mutable player state.
- Session locking prevents data corruption from multi-server joins. ProfileStore handles this.
- BindToClose is MANDATORY. Flush all pending saves on server shutdown.
- Schema: use a default template table. New fields get default values automatically.
- Access pattern: . Mutate directly, ProfileStore auto-saves.
profile.Data.fieldName - Release profile on PlayerRemoving:
profile:Release() - OrderedDataStore for leaderboards only (separate from player data).
- Data migration: version field in schema, migrate on load if version < current.
- Never store Instances or functions in DataStores. Serialize to primitives.
- Cross-server: MessagingService for real-time, GlobalDataStore for shared state.
仅当需要特定实现示例或迁移方案时,才查看下方完整参考内容。
核心规则:
- 始终使用ProfileStore处理玩家数据。绝不要使用原生DataStoreService处理可变玩家状态。
- 会话锁定可防止多服务器加入导致的数据损坏。ProfileStore会自动处理这一点。
- BindToClose是必须的。服务器关闭时需刷新所有待保存数据。
- 数据结构:使用默认模板表。新增字段会自动获取默认值。
- 访问方式:。直接修改数据,ProfileStore会自动保存。
profile.Data.fieldName - 玩家离开时释放档案:
profile:Release() - 仅将OrderedDataStore用于排行榜(与玩家数据分开)。
- 数据迁移:在数据结构中加入版本字段,加载时若版本低于当前版本则执行迁移。
- 不要在DataStores中存储实例或函数。需序列化为基本类型。
- 跨服务器通信:使用MessagingService实现实时通信,使用GlobalDataStore处理共享状态。
Full Reference
完整参考
2. Raw DataStore API (Reference Only)
2. 原生DataStore API(仅作参考)
For production games, skip to section 4 (ProfileStore). This section exists so you understand what's underneath. Do NOT implement manual auto-save, session locking, BindToClose handlers, or retry logic - ProfileStore handles all of this.
对于正式上线游戏,请直接跳至第4节(ProfileStore)。本节仅用于帮助你理解底层原理。请勿手动实现自动保存、会话锁定、BindToClose处理器或重试逻辑——ProfileStore会自动处理所有这些内容。
Core Methods
核心方法
| Method | Purpose | Notes |
|---|---|---|
| Get/create a named DataStore | Returns DataStore object |
| Read a value | Returns nil if key doesn't exist |
| Write a value (overwrites) | No conflict protection |
| Atomic read-modify-write | Preferred for saves |
| Delete a key | Returns the old value |
UpdateAsyncSetAsync| 方法 | 用途 | 说明 |
|---|---|---|
| 获取/创建指定名称的DataStore | 返回DataStore对象 |
| 读取值 | 若键不存在则返回nil |
| 写入值(会覆盖原有内容) | 无冲突保护 |
| 原子化的读取-修改-写入操作 | 推荐用于保存操作 |
| 删除键 | 返回旧值 |
UpdateAsyncSetAsyncLeaderstats (Display Pattern)
排行榜(显示方案)
Leaderstats are IntValue/StringValue children of a Folder named "leaderstats" parented to the Player. Roblox automatically displays these on the in-game leaderboard.
luau
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local cash = Instance.new("IntValue")
cash.Name = "Cash"
cash.Value = profile.Data.Cash -- populated from ProfileStore
cash.Parent = leaderstatsLeaderstats are display-only. The authoritative data lives in ProfileStore's . Sync leaderstats back to profile on save.
profile.DataLeaderstats是名为“leaderstats”的Folder的子对象,类型为IntValue/StringValue,父对象为Player。Roblox会自动在游戏内排行榜中显示这些数据。
luau
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local cash = Instance.new("IntValue")
cash.Name = "Cash"
cash.Value = profile.Data.Cash -- populated from ProfileStore
cash.Parent = leaderstatsLeaderstats仅用于显示。权威数据存储在ProfileStore的中。保存时需将Leaderstats的数据同步回档案。
profile.Data4. ProfileStore
4. ProfileStore
ProfileStore is the community-standard library for production-grade data persistence. It solves critical problems that raw DataStore usage does not handle.
ProfileStore是生产级数据持久化的社区标准库。它解决了原生DataStore使用时无法处理的关键问题。
Why Use It
为什么使用它
| Feature | Raw DataStore | ProfileStore |
|---|---|---|
| Session locking | Manual (hard) | Automatic |
| Auto-save | Manual | Built-in |
| Schema migration | Manual | Supported |
| Data corruption protection | None | Built-in |
| Retry logic | Manual | Built-in |
| BindToClose handling | Manual | Automatic |
| 特性 | 原生DataStore | ProfileStore |
|---|---|---|
| 会话锁定 | 需手动实现(难度大) | 自动处理 |
| 自动保存 | 需手动实现 | 内置功能 |
| 数据结构迁移 | 需手动实现 | 支持 |
| 数据损坏防护 | 无 | 内置功能 |
| 重试逻辑 | 需手动实现 | 内置功能 |
| BindToClose处理 | 需手动实现 | 自动处理 |
Installation
安装方式
With Wally (recommended):
toml
undefined推荐使用Wally:
toml
undefinedwally.toml
wally.toml
[dependencies]
ProfileStore = "madstudioroblox/profileservice@1.4.0"
Run `wally install`, then require from the Packages folder.
**Manual:** Download from GitHub and place the ProfileStore ModuleScript into ServerScriptService or ReplicatedStorage.[dependencies]
ProfileStore = "madstudioroblox/profileservice@1.4.0"
运行`wally install`,然后从Packages文件夹中引入。
**手动安装:**从GitHub下载ProfileStore ModuleScript,放入ServerScriptService或ReplicatedStorage中。Complete ProfileStore Setup
完整ProfileStore配置
luau
-- ServerScript in ServerScriptService
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.Packages.ProfileStore)
-- Adjust the require path based on where you installed it
-- Define the profile template (default data for new players)
local PROFILE_TEMPLATE = {
DataVersion = 1,
Cash = 0,
Level = 1,
Experience = 0,
Inventory = {},
Settings = {
MusicVolume = 0.5,
SFXVolume = 0.8,
},
Statistics = {
TotalPlayTime = 0,
GamesPlayed = 0,
},
}
-- Create the store (wraps a DataStore with session locking)
local PlayerStore = ProfileStore.New("PlayerProfiles_v1", PROFILE_TEMPLATE)
-- Active profiles cache
local Profiles: { [Player]: typeof(PlayerStore:LoadProfileAsync("")) } = {}
local function onProfileLoaded(player: Player, profile)
-- Session lock: if the profile was stolen by another server, release and kick
profile:AddUserId(player.UserId) -- GDPR compliance
profile:Reconcile() -- Fills in missing fields from PROFILE_TEMPLATE
profile:ListenToRelease(function()
Profiles[player] = nil
player:Kick("Your data was loaded on another server. Please rejoin.")
end)
-- Check if player is still in game (they may have left during async load)
if not player:IsDescendantOf(Players) then
profile:Release()
return
end
-- Store and set up the player
Profiles[player] = profile
-- Example: set up leaderstats from profile data
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local cash = Instance.new("IntValue")
cash.Name = "Cash"
cash.Value = profile.Data.Cash
cash.Parent = leaderstats
local level = Instance.new("IntValue")
level.Name = "Level"
level.Value = profile.Data.Level
level.Parent = leaderstats
end
Players.PlayerAdded:Connect(function(player: Player)
local profile = PlayerStore:LoadProfileAsync(
`Player_{player.UserId}`,
"ForceLoad" -- Wait until the session lock is acquired
)
if profile == nil then
player:Kick("Unable to load your data. Please rejoin.")
return
end
onProfileLoaded(player, profile)
end)
Players.PlayerRemoving:Connect(function(player: Player)
local profile = Profiles[player]
if profile then
-- Sync leaderstats back to profile before release
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats then
profile.Data.Cash = leaderstats.Cash.Value
profile.Data.Level = leaderstats.Level.Value
end
profile:Release()
end
end)
-- Helper to get a player's profile from other scripts
-- Export this via a ModuleScript in production
local function getProfile(player: Player)
return Profiles[player]
endluau
-- ServerScript in ServerScriptService
local Players = game:GetService("Players")
local ServerScriptService = game:GetService("ServerScriptService")
local ProfileStore = require(ServerScriptService.Packages.ProfileStore)
-- 根据安装路径调整引入路径
-- 定义档案模板(新玩家的默认数据)
local PROFILE_TEMPLATE = {
DataVersion = 1,
Cash = 0,
Level = 1,
Experience = 0,
Inventory = {},
Settings = {
MusicVolume = 0.5,
SFXVolume = 0.8,
},
Statistics = {
TotalPlayTime = 0,
GamesPlayed = 0,
},
}
-- 创建存储(包装DataStore并添加会话锁定功能)
local PlayerStore = ProfileStore.New("PlayerProfiles_v1", PROFILE_TEMPLATE)
-- 活跃档案缓存
local Profiles: { [Player]: typeof(PlayerStore:LoadProfileAsync("")) } = {}
local function onProfileLoaded(player: Player, profile)
-- 会话锁定:如果档案被另一服务器抢占,释放并踢出玩家
profile:AddUserId(player.UserId) -- 符合GDPR合规要求
profile:Reconcile() -- 从PROFILE_TEMPLATE填充缺失字段
profile:ListenToRelease(function()
Profiles[player] = nil
player:Kick("你的数据已在另一服务器加载,请重新加入。")
end)
-- 检查玩家是否仍在游戏中(加载异步操作期间玩家可能已离开)
if not player:IsDescendantOf(Players) then
profile:Release()
return
end
-- 存储档案并设置玩家数据
Profiles[player] = profile
-- 示例:从档案数据设置排行榜
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
local cash = Instance.new("IntValue")
cash.Name = "Cash"
cash.Value = profile.Data.Cash
cash.Parent = leaderstats
local level = Instance.new("IntValue")
level.Name = "Level"
level.Value = profile.Data.Level
level.Parent = leaderstats
end
Players.PlayerAdded:Connect(function(player: Player)
local profile = PlayerStore:LoadProfileAsync(
`Player_{player.UserId}`,
"ForceLoad" -- 等待获取会话锁定
)
if profile == nil then
player:Kick("无法加载你的数据,请重新加入。")
return
end
onProfileLoaded(player, profile)
end)
Players.PlayerRemoving:Connect(function(player: Player)
local profile = Profiles[player]
if profile then
-- 释放前将排行榜数据同步回档案
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats then
profile.Data.Cash = leaderstats.Cash.Value
profile.Data.Level = leaderstats.Level.Value
end
profile:Release()
end
end)
-- 辅助函数:从其他脚本获取玩家档案
-- 生产环境中可通过ModuleScript导出此函数
local function getProfile(player: Player)
return Profiles[player]
endAccessing Profile Data From Other Scripts
从其他脚本访问档案数据
luau
-- In another ServerScript or ModuleScript
local function addCash(player: Player, amount: number)
local profile = getProfile(player)
if not profile then
return
end
profile.Data.Cash += amount
-- Also update leaderstats if visible
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats and leaderstats:FindFirstChild("Cash") then
leaderstats.Cash.Value = profile.Data.Cash
end
endluau
-- 在其他ServerScript或ModuleScript中
local function addCash(player: Player, amount: number)
local profile = getProfile(player)
if not profile then
return
end
profile.Data.Cash += amount
-- 如果排行榜可见,同时更新其数据
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats and leaderstats:FindFirstChild("Cash") then
leaderstats.Cash.Value = profile.Data.Cash
end
end5. Session Locking Explained
5. 会话锁定详解
The Problem
问题背景
Without session locking, data corruption can occur during server hops:
Timeline:
t=0 Player is on Server A, data loaded
t=1 Player teleports to Server B
t=2 Server B starts loading player data from DataStore
t=3 Server A fires PlayerRemoving, starts saving data
t=4 Server B finishes loading (gets STALE data)
t=5 Server A finishes saving (writes LATEST data)
t=6 Server B eventually saves its stale copy, OVERWRITING the latest data
Result: Player loses progress from Server A session如果没有会话锁定,服务器切换期间可能会发生数据损坏:
时间线:
t=0 玩家在服务器A,数据已加载
t=1 玩家传送至服务器B
t=2 服务器B开始从DataStore加载玩家数据
t=3 服务器A触发PlayerRemoving事件,开始保存数据
t=4 服务器B完成加载(获取到过期数据)
t=5 服务器A完成保存(写入最新数据)
t=6 服务器B最终保存其过期数据,覆盖了最新数据
结果:玩家丢失了在服务器A会话中的进度The Solution
解决方案
Session locking ensures that only one server can own a player's data at a time:
Timeline with Session Locking:
t=0 Server A loads profile, acquires session lock
t=1 Player teleports to Server B
t=2 Server B tries to load -- sees lock owned by Server A, WAITS
t=3 Server A fires PlayerRemoving, saves data, RELEASES lock
t=4 Server B detects lock released, acquires lock, loads LATEST data
Result: No data loss会话锁定确保同一时间只有一台服务器能拥有玩家的数据:
启用会话锁定后的时间线:
t=0 服务器A加载档案,获取会话锁定
t=1 玩家传送至服务器B
t=2 服务器B尝试加载——发现锁定被服务器A持有,等待
t=3 服务器A触发PlayerRemoving事件,保存数据,释放锁定
t=4 服务器B检测到锁定已释放,获取锁定,加载最新数据
结果:无数据丢失How ProfileStore Implements It
ProfileStore的实现方式
- When is called, ProfileStore writes a session lock tag (server JobId) to the DataStore entry.
LoadProfileAsync - If another server already holds the lock, ProfileStore either waits ("ForceLoad") or gives up ("Steal").
- The locking server periodically refreshes the lock via auto-save.
- On , the lock is cleared and the data is saved.
profile:Release() - If a server crashes without releasing, the lock expires after a timeout (~30 minutes by default), and another server can then claim it.
You do NOT need to implement session locking manually. ProfileStore handles all of this. This is the primary reason to use it over raw DataStoreService.
- 调用时,ProfileStore会向DataStore条目写入会话锁定标签(服务器JobId)。
LoadProfileAsync - 如果另一服务器已持有锁定,ProfileStore会选择等待("ForceLoad"模式)或放弃("Steal"模式)。
- 持有锁定的服务器会通过自动保存定期刷新锁定。
- 调用时,锁定会被清除并保存数据。
profile:Release() - 如果服务器崩溃未释放锁定,锁定会在超时后过期(默认约30分钟),之后其他服务器可获取锁定。
你无需手动实现会话锁定。ProfileStore会自动处理所有相关逻辑。这是使用它而非原生DataStoreService的主要原因。
6. Data Schema Design
6. 数据结构设计
Flat vs. Nested Structure
扁平结构 vs 嵌套结构
Flat (simple games):
luau
local PROFILE_TEMPLATE = {
Cash = 0,
Level = 1,
Wins = 0,
Losses = 0,
}Nested (complex games):
luau
local PROFILE_TEMPLATE = {
DataVersion = 1,
Currency = {
Cash = 0,
Gems = 0,
Tickets = 0,
},
Progression = {
Level = 1,
Experience = 0,
Prestige = 0,
},
Inventory = {
Swords = {}, -- Array of item IDs or item tables
Armor = {},
Consumables = {},
},
Quests = {
Active = {},
Completed = {},
},
Settings = {
MusicVolume = 0.5,
SFXVolume = 0.8,
ShowTutorial = true,
},
}扁平结构(适用于简单游戏):
luau
local PROFILE_TEMPLATE = {
Cash = 0,
Level = 1,
Wins = 0,
Losses = 0,
}嵌套结构(适用于复杂游戏):
luau
local PROFILE_TEMPLATE = {
DataVersion = 1,
Currency = {
Cash = 0,
Gems = 0,
Tickets = 0,
},
Progression = {
Level = 1,
Experience = 0,
Prestige = 0,
},
Inventory = {
Swords = {}, -- 物品ID或物品表的数组
Armor = {},
Consumables = {},
},
Quests = {
Active = {},
Completed = {},
},
Settings = {
MusicVolume = 0.5,
SFXVolume = 0.8,
ShowTutorial = true,
},
}Versioning Your Schema
数据结构版本控制
Always include a field. This lets you detect and migrate old data formats.
DataVersionluau
local PROFILE_TEMPLATE = {
DataVersion = 3, -- Increment when schema changes
-- ... fields ...
}始终包含字段。这能让你检测并迁移旧数据格式。
DataVersionluau
local PROFILE_TEMPLATE = {
DataVersion = 3, -- 数据结构变更时递增版本号
-- ... 其他字段 ...
}Default Values for New Fields
新增字段的默认值
When you add new fields, existing players won't have them. ProfileStore's handles this automatically - it fills in any missing fields from your PROFILE_TEMPLATE. Call it after loading:
Reconcile()luau
profile:Reconcile() -- Fills missing fields from templateNo manual merge code needed when using ProfileStore.
当你添加新字段时,现有玩家的数据中不会有这些字段。ProfileStore的方法会自动处理这一点——它会从PROFILE_TEMPLATE中填充所有缺失字段。加载档案后调用此方法:
Reconcile()luau
profile:Reconcile() -- 从模板填充缺失字段使用ProfileStore时无需编写手动合并代码。
Type Safety Tips
类型安全建议
- Use consistent types per field. If is a number, never save it as a string.
Cash - Arrays should contain uniform types (, not mixed).
{ number } - Avoid storing explicitly -- DataStore omits nil keys, which can cause confusion. Use sentinel values (e.g.,
nil,0,"") instead.false - Remember: DataStore serializes to JSON internally. Only JSON-compatible types work: ,
number,string,boolean(arrays and dictionaries). No Instances, Vector3s, CFrames, or other Roblox types directly.table
- 每个字段使用一致的类型。如果是数字类型,绝不要将其保存为字符串。
Cash - 数组应包含统一类型(如,不要混合类型)。
{ number } - 避免显式存储——DataStore会忽略nil键,可能导致混淆。使用标记值(如
nil、0、"")替代。false - 注意:DataStore内部会序列化为JSON。仅支持JSON兼容类型:、
number、string、boolean(数组和字典)。不能直接存储Instances、Vector3s、CFrames或其他Roblox类型。table
7. Data Migration
7. 数据迁移
When your data schema changes, you need to migrate existing player data to the new format.
当数据结构变更时,你需要将现有玩家的数据迁移至新格式。
Migration Strategy
迁移策略
- Check when data is loaded.
DataVersion - Apply migration functions sequentially (v1 -> v2, v2 -> v3, etc.).
- Update to current.
DataVersion
- 加载数据时检查。
DataVersion - 按顺序应用迁移函数(v1 -> v2,v2 -> v3,依此类推)。
- 将更新为当前版本。
DataVersion
Complete Migration Example
完整迁移示例
luau
-- DataMigrations module
local DataMigrations = {}
-- Each migration transforms data from version N to version N+1
local migrations: { [number]: (data: { [string]: any }) -> { [string]: any } } = {}
-- v1 -> v2: Split "Money" into "Cash" and "Gems"
migrations[1] = function(data)
if data.Money then
data.Cash = data.Money
data.Gems = 0
data.Money = nil
end
return data
end
-- v2 -> v3: Move settings out of flat structure into nested table
migrations[2] = function(data)
data.Settings = {
MusicVolume = data.MusicVolume or 0.5,
SFXVolume = data.SFXVolume or 0.8,
}
data.MusicVolume = nil
data.SFXVolume = nil
return data
end
-- v3 -> v4: Add Quests system and rename "Wins" to "Statistics.Wins"
migrations[3] = function(data)
data.Quests = {
Active = {},
Completed = {},
}
data.Statistics = data.Statistics or {}
data.Statistics.Wins = data.Wins or 0
data.Wins = nil
return data
end
local CURRENT_VERSION = 4
function DataMigrations.migrate(data: { [string]: any }): { [string]: any }
local version = data.DataVersion or 1
if version > CURRENT_VERSION then
warn(`[Migration] Data version {version} is newer than code version {CURRENT_VERSION}`)
return data
end
while version < CURRENT_VERSION do
local migrator = migrations[version]
if migrator then
data = migrator(data)
print(`[Migration] Migrated data from v{version} to v{version + 1}`)
end
version += 1
end
data.DataVersion = CURRENT_VERSION
return data
end
return DataMigrationsluau
-- DataMigrations模块
local DataMigrations = {}
-- 每个迁移函数将数据从版本N转换为版本N+1
local migrations: { [number]: (data: { [string]: any }) -> { [string]: any } } = {}
-- v1 -> v2:将"Money"拆分为"Cash"和"Gems"
migrations[1] = function(data)
if data.Money then
data.Cash = data.Money
data.Gems = 0
data.Money = nil
end
return data
end
-- v2 -> v3:将设置从扁平结构移至嵌套表
migrations[2] = function(data)
data.Settings = {
MusicVolume = data.MusicVolume or 0.5,
SFXVolume = data.SFXVolume or 0.8,
}
data.MusicVolume = nil
data.SFXVolume = nil
return data
end
-- v3 -> v4:添加任务系统并将"Wins"重命名为"Statistics.Wins"
migrations[3] = function(data)
data.Quests = {
Active = {},
Completed = {},
}
data.Statistics = data.Statistics or {}
data.Statistics.Wins = data.Wins or 0
data.Wins = nil
return data
end
local CURRENT_VERSION = 4
function DataMigrations.migrate(data: { [string]: any }): { [string]: any }
local version = data.DataVersion or 1
if version > CURRENT_VERSION then
warn(`[Migration] 数据版本{version}高于代码版本{CURRENT_VERSION}`)
return data
end
while version < CURRENT_VERSION do
local migrator = migrations[version]
if migrator then
data = migrator(data)
print(`[Migration] 将数据从v{version}迁移至v{version + 1}`)
end
version += 1
end
data.DataVersion = CURRENT_VERSION
return data
end
return DataMigrationsUsing Migrations With ProfileStore
在ProfileStore中使用迁移
luau
-- After loading the profile, before using the data:
local profile = PlayerStore:LoadProfileAsync(`Player_{player.UserId}`, "ForceLoad")
if profile then
profile.Data = DataMigrations.migrate(profile.Data)
profile:Reconcile() -- Fill in any remaining missing defaults
endluau
-- 加载档案后,使用数据前:
local profile = PlayerStore:LoadProfileAsync(`Player_{player.UserId}`, "ForceLoad")
if profile then
profile.Data = DataMigrations.migrate(profile.Data)
profile:Reconcile() -- 填充所有剩余缺失的默认值
end8. OrderedDataStore
8. OrderedDataStore
OrderedDataStoreOrderedDataStoreComplete Leaderboard Implementation
完整排行榜实现
luau
-- ServerScript in ServerScriptService
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local cashLeaderboard = DataStoreService:GetOrderedDataStore("CashLeaderboard")
local LEADERBOARD_SIZE = 100
local UPDATE_INTERVAL = 120 -- seconds
-- Update a player's score in the leaderboard
local function updateLeaderboardScore(userId: number, score: number)
local success, err = pcall(function()
cashLeaderboard:SetAsync(tostring(userId), score)
end)
if not success then
warn(`[Leaderboard] Failed to update score for {userId}: {err}`)
end
end
-- Fetch the top N entries from the leaderboard
local function getTopPlayers(count: number): { { UserId: number, Score: number, Rank: number } }
local results = {}
local success, pages = pcall(function()
return cashLeaderboard:GetSortedAsync(
false, -- isAscending: false = highest first
count -- pageSize
)
end)
if not success then
warn(`[Leaderboard] Failed to fetch leaderboard: {pages}`)
return results
end
local currentPage = pages:GetCurrentPage()
local rank = 0
for _, entry in currentPage do
rank += 1
table.insert(results, {
UserId = tonumber(entry.key),
Score = entry.value,
Rank = rank,
})
end
return results
end
-- Populate a SurfaceGui or Billboard leaderboard (example with a Frame)
local function displayLeaderboard(surfaceGui: SurfaceGui, entries: { { UserId: number, Score: number, Rank: number } })
local container = surfaceGui:FindFirstChild("Container")
if not container then
return
end
-- Clear old entries
for _, child in container:GetChildren() do
if child:IsA("Frame") then
child:Destroy()
end
end
for _, entry in entries do
-- Get player name (works for offline players too)
local success, name = pcall(function()
return Players:GetNameFromUserIdAsync(entry.UserId)
end)
if success then
local row = Instance.new("Frame")
row.Name = `Rank_{entry.Rank}`
row.Size = UDim2.new(1, 0, 0, 30)
row.LayoutOrder = entry.Rank
row.Parent = container
local rankLabel = Instance.new("TextLabel")
rankLabel.Text = `#{entry.Rank}`
rankLabel.Size = UDim2.new(0.15, 0, 1, 0)
rankLabel.Parent = row
local nameLabel = Instance.new("TextLabel")
nameLabel.Text = name
nameLabel.Size = UDim2.new(0.55, 0, 1, 0)
nameLabel.Position = UDim2.new(0.15, 0, 0, 0)
nameLabel.Parent = row
local scoreLabel = Instance.new("TextLabel")
scoreLabel.Text = tostring(entry.Score)
scoreLabel.Size = UDim2.new(0.3, 0, 1, 0)
scoreLabel.Position = UDim2.new(0.7, 0, 0, 0)
scoreLabel.Parent = row
end
end
end
-- Periodic leaderboard update loop
task.spawn(function()
while true do
-- Update scores for all online players
for _, player in Players:GetPlayers() do
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats and leaderstats:FindFirstChild("Cash") then
task.spawn(updateLeaderboardScore, player.UserId, leaderstats.Cash.Value)
end
end
-- Fetch and display updated leaderboard
task.wait(5) -- Brief delay for scores to propagate
local topPlayers = getTopPlayers(LEADERBOARD_SIZE)
task.wait(UPDATE_INTERVAL)
end
end)Important: OrderedDataStore only supports integer values. If you need decimal scores, multiply by a factor (e.g., store ).
score * 100luau
-- ServerScript in ServerScriptService
local Players = game:GetService("Players")
local DataStoreService = game:GetService("DataStoreService")
local cashLeaderboard = DataStoreService:GetOrderedDataStore("CashLeaderboard")
local LEADERBOARD_SIZE = 100
local UPDATE_INTERVAL = 120 -- 秒
-- 更新玩家在排行榜中的分数
local function updateLeaderboardScore(userId: number, score: number)
local success, err = pcall(function()
cashLeaderboard:SetAsync(tostring(userId), score)
end)
if not success then
warn(`[Leaderboard] 更新玩家{userId}分数失败: {err}`)
end
end
-- 从排行榜获取前N名玩家
local function getTopPlayers(count: number): { { UserId: number, Score: number, Rank: number } }
local results = {}
local success, pages = pcall(function()
return cashLeaderboard:GetSortedAsync(
false, -- isAscending: false = 分数从高到低排序
count -- 每页数量
)
end)
if not success then
warn(`[Leaderboard] 获取排行榜失败: {pages}`)
return results
end
local currentPage = pages:GetCurrentPage()
local rank = 0
for _, entry in currentPage do
rank += 1
table.insert(results, {
UserId = tonumber(entry.key),
Score = entry.value,
Rank = rank,
})
end
return results
end
-- 在SurfaceGui或Billboard中显示排行榜(以Frame为例)
local function displayLeaderboard(surfaceGui: SurfaceGui, entries: { { UserId: number, Score: number, Rank: number } })
local container = surfaceGui:FindFirstChild("Container")
if not container then
return
end
-- 清除旧条目
for _, child in container:GetChildren() do
if child:IsA("Frame") then
child:Destroy()
end
end
for _, entry in entries do
-- 获取玩家名称(离线玩家也适用)
local success, name = pcall(function()
return Players:GetNameFromUserIdAsync(entry.UserId)
end)
if success then
local row = Instance.new("Frame")
row.Name = `Rank_{entry.Rank}`
row.Size = UDim2.new(1, 0, 0, 30)
row.LayoutOrder = entry.Rank
row.Parent = container
local rankLabel = Instance.new("TextLabel")
rankLabel.Text = `#{entry.Rank}`
rankLabel.Size = UDim2.new(0.15, 0, 1, 0)
rankLabel.Parent = row
local nameLabel = Instance.new("TextLabel")
nameLabel.Text = name
nameLabel.Size = UDim2.new(0.55, 0, 1, 0)
nameLabel.Position = UDim2.new(0.15, 0, 0, 0)
nameLabel.Parent = row
local scoreLabel = Instance.new("TextLabel")
scoreLabel.Text = tostring(entry.Score)
scoreLabel.Size = UDim2.new(0.3, 0, 1, 0)
scoreLabel.Position = UDim2.new(0.7, 0, 0, 0)
scoreLabel.Parent = row
end
end
end
-- 定期更新排行榜的循环
task.spawn(function()
while true do
-- 更新所有在线玩家的分数
for _, player in Players:GetPlayers() do
local leaderstats = player:FindFirstChild("leaderstats")
if leaderstats and leaderstats:FindFirstChild("Cash") then
task.spawn(updateLeaderboardScore, player.UserId, leaderstats.Cash.Value)
end
end
-- 短暂延迟等待分数同步
task.wait(5)
local topPlayers = getTopPlayers(LEADERBOARD_SIZE)
task.wait(UPDATE_INTERVAL)
end
end)重要提示:OrderedDataStore仅支持整数值。如果需要小数分数,可乘以一个系数(例如,存储)。
score * 1009. Cross-Server Data
9. 跨服务器数据
MessagingService (Real-Time Pub/Sub)
MessagingService(实时发布/订阅)
For real-time communication between servers (announcements, events, cross-server trading).
luau
local MessagingService = game:GetService("MessagingService")
-- Subscribe to a topic
local connection = MessagingService:SubscribeAsync("GlobalAnnouncement", function(message)
local data = message.Data -- The payload
local sent = message.Sent -- Timestamp when sent (Unix time)
-- Broadcast to all players on this server
for _, player in Players:GetPlayers() do
-- Show announcement UI, etc.
end
end)
-- Publish to a topic (reaches all servers)
local success, err = pcall(function()
MessagingService:PublishAsync("GlobalAnnouncement", {
Text = "Double XP weekend starts now!",
Duration = 3600,
})
end)MessagingService limits:
- Message size: 1 KB max
- Messages per server: 150 + 60 * playerCount per minute
- Subscriptions per server: 5 + 2 * playerCount
- Messages are NOT persisted -- only online servers receive them.
用于服务器间的实时通信(公告、事件、跨服务器交易等)。
luau
local MessagingService = game:GetService("MessagingService")
-- 订阅主题
local connection = MessagingService:SubscribeAsync("GlobalAnnouncement", function(message)
local data = message.Data -- 负载数据
local sent = message.Sent -- 发送时间戳(Unix时间)
-- 广播给当前服务器的所有玩家
for _, player in Players:GetPlayers() do
-- 显示公告UI等
end
end)
-- 发布到主题(所有服务器都会收到)
local success, err = pcall(function()
MessagingService:PublishAsync("GlobalAnnouncement", {
Text = "双倍经验周末现在开始!",
Duration = 3600,
})
end)MessagingService限制:
- 消息大小:最大1 KB
- 每服务器消息数量:每分钟150 + 60 * 玩家数量
- 每服务器订阅数量:5 + 2 * 玩家数量
- 消息不持久化——仅在线服务器会收到。
GlobalDataStore for Shared State
用于共享状态的GlobalDataStore
For persistent cross-server state (global counters, server-wide events):
luau
local globalStore = DataStoreService:GetDataStore("GlobalState")
-- Atomically increment a global counter
local function incrementGlobalCounter(key: string, amount: number): number?
local success, newValue = pcall(function()
return globalStore:UpdateAsync(key, function(old)
return (old or 0) + amount
end)
end)
if success then
return newValue
end
return nil
end
-- Example: Track total enemies defeated across all servers
local totalDefeated = incrementGlobalCounter("TotalEnemiesDefeated", 1)用于持久化跨服务器状态(全局计数器、服务器级事件等):
luau
local globalStore = DataStoreService:GetDataStore("GlobalState")
-- 原子化递增全局计数器
local function incrementGlobalCounter(key: string, amount: number): number?
local success, newValue = pcall(function()
return globalStore:UpdateAsync(key, function(old)
return (old or 0) + amount
end)
end)
if success then
return newValue
end
return nil
end
-- 示例:跟踪所有服务器击败的敌人总数
local totalDefeated = incrementGlobalCounter("TotalEnemiesDefeated", 1)10. Best Practices
10. 最佳实践
If using ProfileStore (recommended), sections 10.1 through 10.4 are handled automatically. You only need to worry about these if you're building on raw DataStoreService. The patterns below are shown for understanding and for the rare case where raw DataStore is appropriate.
如果使用ProfileStore(推荐),第10.1至10.4节的内容会自动处理。仅当基于原生DataStoreService开发时才需要关注这些内容。以下模式仅用于理解原理,以及极少数适合使用原生DataStore的场景。
10.1 Auto-Save Interval (ProfileStore: automatic)
10.1 自动保存间隔(ProfileStore:自动处理)
ProfileStore handles auto-save internally. If using raw DataStore, save every 5 minutes:
luau
local AUTO_SAVE_INTERVAL = 300
task.spawn(function()
while true do
task.wait(AUTO_SAVE_INTERVAL)
for player, _data in playerDataCache do
task.spawn(savePlayerData, player)
end
end
end)ProfileStore内部会处理自动保存。如果使用原生DataStore,建议每5分钟保存一次:
luau
local AUTO_SAVE_INTERVAL = 300
task.spawn(function()
while true do
task.wait(AUTO_SAVE_INTERVAL)
for player, _data in playerDataCache do
task.spawn(savePlayerData, player)
end
end
end)10.2 Save on PlayerRemoving (ProfileStore: automatic via Release)
10.2 玩家离开时保存(ProfileStore:通过Release自动处理)
ProfileStore saves and releases the session lock when is called. If using raw DataStore:
profile:Release()luau
Players.PlayerRemoving:Connect(function(player: Player)
savePlayerData(player)
playerDataCache[player] = nil
end)调用时,ProfileStore会保存数据并释放会话锁定。如果使用原生DataStore:
profile:Release()luau
Players.PlayerRemoving:Connect(function(player: Player)
savePlayerData(player)
playerDataCache[player] = nil
end)10.3 BindToClose Handler (ProfileStore: automatic)
10.3 BindToClose处理器(ProfileStore:自动处理)
ProfileStore handles shutdown saves automatically. If using raw DataStore, fires when the server shuts down. You have 30 seconds to save all data before the server terminates. Use for parallel saves.
game:BindToClosetask.spawnluau
-- Only needed with raw DataStore
game:BindToClose(function()
if game:GetService("RunService"):IsStudio() then
task.wait(1)
return
end
local finished = Instance.new("BindableEvent")
local allPlayers = Players:GetPlayers()
local remaining = #allPlayers
if remaining == 0 then return end
for _, player in allPlayers do
task.spawn(function()
savePlayerData(player)
remaining -= 1
if remaining <= 0 then finished:Fire() end
end)
end
task.delay(25, function() finished:Fire() end)
finished.Event:Wait()
finished:Destroy()
end)ProfileStore会自动处理服务器关闭时的保存操作。如果使用原生DataStore,会在服务器关闭时触发。你有30秒时间完成所有数据保存,之后服务器会强制终止。使用实现并行保存。
game:BindToClosetask.spawnluau
-- 仅原生DataStore需要
game:BindToClose(function()
if game:GetService("RunService"):IsStudio() then
task.wait(1)
return
end
local finished = Instance.new("BindableEvent")
local allPlayers = Players:GetPlayers()
local remaining = #allPlayers
if remaining == 0 then return end
for _, player in allPlayers do
task.spawn(function()
savePlayerData(player)
remaining -= 1
if remaining <= 0 then finished:Fire() end
end)
end
task.delay(25, function() finished:Fire() end)
finished.Event:Wait()
finished:Destroy()
end)10.4 Retry Failed Saves (ProfileStore: built-in)
10.4 失败保存的重试逻辑(ProfileStore:内置)
ProfileStore has built-in retry with exponential backoff. If using raw DataStore:
luau
local MAX_RETRIES = 3
local RETRY_DELAY = 2
local function saveWithRetry(player: Player): boolean
for attempt = 1, MAX_RETRIES do
local success = savePlayerData(player)
if success then return true end
if attempt < MAX_RETRIES then
warn(`[DataStore] Retry {attempt}/{MAX_RETRIES} for {player.Name}`)
task.wait(RETRY_DELAY * attempt)
end
end
warn(`[DataStore] All retries failed for {player.Name}`)
return false
endProfileStore内置了带指数退避的重试逻辑。如果使用原生DataStore:
luau
local MAX_RETRIES = 3
local RETRY_DELAY = 2
local function saveWithRetry(player: Player): boolean
for attempt = 1, MAX_RETRIES do
local success = savePlayerData(player)
if success then return true end
if attempt < MAX_RETRIES then
warn(`[DataStore] 为玩家{player.Name}进行第{attempt}/{MAX_RETRIES}次重试`)
task.wait(RETRY_DELAY * attempt)
end
end
warn(`[DataStore] 玩家{player.Name}的所有重试均失败`)
return false
end10.5 Validate Data Before Saving (always relevant)
10.5 保存前验证数据(始终适用)
This applies regardless of whether you use ProfileStore or raw DataStore. Validate before writing:
luau
local function validateData(data: { [string]: any }): boolean
if typeof(data) ~= "table" then return false end
if typeof(data.Cash) ~= "number" or data.Cash < 0 then return false end
if typeof(data.Level) ~= "number" or data.Level < 1 then return false end
return true
end无论使用ProfileStore还是原生DataStore,都需要在写入前验证数据:
luau
local function validateData(data: { [string]: any }): boolean
if typeof(data) ~= "table" then return false end
if typeof(data.Cash) ~= "number" or data.Cash < 0 then return false end
if typeof(data.Level) ~= "number" or data.Level < 1 then return false end
return true
end11. Anti-Patterns
11. 反模式
Saving Too Frequently
过于频繁地保存
Wrong:
luau
-- DO NOT DO THIS: saving on every coin pickup
coinTouched:Connect(function(player)
player.Data.Cash += 1
dataStore:SetAsync(`Player_{player.UserId}`, player.Data) -- Rate limit hit
end)Right: Modify in-memory data immediately, rely on periodic auto-save.
DataStore rate limits: requests per minute per DataStore. With 50 players, that is 560 requests/min total -- or about 11 per player per minute. Saving once per 5 minutes uses only 0.2 per player per minute.
60 + numPlayers * 10错误示例:
luau
-- 请勿这样做:每次拾取硬币都保存
coinTouched:Connect(function(player)
player.Data.Cash += 1
dataStore:SetAsync(`Player_{player.UserId}`, player.Data) -- 会触发速率限制
end)**正确做法:**立即修改内存中的数据,依赖定期自动保存。
**DataStore速率限制:**每个DataStore每分钟允许次请求。如果有50名玩家,总共有560次请求/分钟——约每位玩家每分钟11次请求。每5分钟保存一次仅占每位玩家每分钟0.2次请求。
60 + numPlayers * 10Not Handling DataStore Errors
未处理DataStore错误
Wrong:
luau
-- DO NOT DO THIS: unprotected call
local data = dataStore:GetAsync(key) -- Will error and break the scriptRight:
luau
local success, data = pcall(function()
return dataStore:GetAsync(key)
end)
if not success then
warn("DataStore error:", data)
-- Handle gracefully
end错误示例:
luau
-- 请勿这样做:未受保护的调用
local data = dataStore:GetAsync(key) -- 会报错并中断脚本正确做法:
luau
local success, data = pcall(function()
return dataStore:GetAsync(key)
end)
if not success then
warn("DataStore错误:", data)
-- 优雅处理错误
endStoring Instance References
存储实例引用
Wrong:
luau
-- DO NOT DO THIS: Instances are not serializable
data.Weapon = workspace.Sword -- Will fail or produce garbage
data.Character = player.Character -- Same problemRight: Store serializable identifiers.
luau
data.WeaponId = "IronSword"
data.EquippedSlots = { "Helmet_01", "Armor_03" }错误示例:
luau
-- 请勿这样做:实例无法序列化
data.Weapon = workspace.Sword -- 会失败或产生无效数据
data.Character = player.Character -- 同样问题**正确做法:**存储可序列化的标识符。
luau
data.WeaponId = "IronSword"
data.EquippedSlots = { "Helmet_01", "Armor_03" }Exceeding Key Size Limits
超过键大小限制
| Limit | Value |
|---|---|
| Key name length | 50 characters |
| Value size per key | 4,194,304 bytes (4 MB) |
| DataStore name length | 50 characters |
If you're approaching 4 MB, split data across multiple keys:
luau
-- Split by category
local coreStore = DataStoreService:GetDataStore("PlayerCore")
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory")
local questStore = DataStoreService:GetDataStore("PlayerQuests")| 限制 | 数值 |
|---|---|
| 键名称长度 | 50字符 |
| 每个键的值大小 | 4,194,304字节(4 MB) |
| DataStore名称长度 | 50字符 |
如果数据大小接近4 MB,可将数据拆分到多个键中:
luau
-- 按类别拆分
local coreStore = DataStoreService:GetDataStore("PlayerCore")
local inventoryStore = DataStoreService:GetDataStore("PlayerInventory")
local questStore = DataStoreService:GetDataStore("PlayerQuests")12. Sharp Edges
12. 注意事项
Rate Limits
速率限制
DataStore requests are throttled per-server, not per-player:
| Operation | Budget per Minute |
|---|---|
| GetAsync | 60 + numPlayers * 10 |
| SetAsync / UpdateAsync | 60 + numPlayers * 10 |
| GetSortedAsync | 5 + numPlayers * 2 |
| SetAsync on OrderedDataStore | 5 + numPlayers * 2 |
Exceeding these results in requests being queued or erroring. Plan save intervals accordingly.
DataStore请求是按服务器而非按玩家进行限流的:
| 操作 | 每分钟请求限额 |
|---|---|
| GetAsync | 60 + numPlayers * 10 |
| SetAsync / UpdateAsync | 60 + numPlayers * 10 |
| GetSortedAsync | 5 + numPlayers * 2 |
| OrderedDataStore上的SetAsync | 5 + numPlayers * 2 |
超过限额会导致请求排队或报错。请合理规划保存间隔。
Eventual Consistency
最终一致性
DataStore reads are eventually consistent. After a , a from another server may briefly return stale data. on the same key is atomic within a single call, but across keys or across servers there is no transaction guarantee.
SetAsyncGetAsyncUpdateAsyncDataStore读取是最终一致的。调用后,另一服务器的可能会短暂返回过期数据。同一键的在单次调用中是原子化的,但跨键或跨服务器没有事务保证。
SetAsyncGetAsyncUpdateAsyncBindToClose 30-Second Timeout
BindToClose的30秒超时
When a Roblox server shuts down, callbacks are given at most 30 seconds to finish. After that, the server process is killed regardless. If you have many players, you MUST save in parallel using , not sequentially.
BindToClosetask.spawnluau
-- BAD: Sequential saves with 50 players could take > 30 seconds
for _, player in Players:GetPlayers() do
savePlayerData(player) -- Each call might take 0.5-2 seconds
end
-- GOOD: Parallel saves complete in the time of the slowest single save
for _, player in Players:GetPlayers() do
task.spawn(savePlayerData, player)
end
task.wait(25) -- Wait with buffer当Roblox服务器关闭时,回调最多有30秒时间完成。超时后,服务器进程会被强制终止。如果玩家数量较多,必须使用并行保存,而非顺序保存。
BindToClosetask.spawnluau
-- 错误:顺序保存50名玩家可能耗时超过30秒
for _, player in Players:GetPlayers() do
savePlayerData(player) -- 每次调用可能耗时0.5-2秒
end
-- 正确:并行保存耗时等于最慢的单次保存时间
for _, player in Players:GetPlayers() do
task.spawn(savePlayerData, player)
end
task.wait(25) -- 留缓冲时间等待Data Loss from Race Conditions Without Session Locking
无会话锁定时竞态条件导致的数据丢失
Without session locking (i.e., using raw DataStore), the following scenario causes data loss:
- Player leaves Server A. fires, save begins.
PlayerRemoving - Player joins Server B before Server A's save completes.
- Server B loads stale data (Server A hasn't finished writing yet).
- Server A finishes saving. Server B later saves its stale copy, overwriting Server A's save.
This is why you use ProfileStore. It handles session locking automatically. If you must use raw DataStore, implement manual session locking with by writing a lock field containing the server's and checking it before loading.
UpdateAsyncgame.JobId如果没有会话锁定(即使用原生DataStore),以下场景会导致数据丢失:
- 玩家离开服务器A。触发事件,开始保存数据。
PlayerRemoving - 玩家在服务器A的保存完成前加入服务器B。
- 服务器B加载到过期数据(服务器A尚未完成写入)。
- 服务器A完成保存。服务器B之后保存其过期数据,覆盖了服务器A的保存结果。
**这就是为什么要使用ProfileStore。**它会自动处理会话锁定。如果必须使用原生DataStore,需通过手动实现会话锁定,写入包含服务器的锁定字段,并在加载前检查该字段。
UpdateAsyncgame.JobIdStudio Testing Gotchas
Studio测试注意事项
- does NOT fire when you press Stop in Studio. Data will not save on exit during testing unless you also test via
PlayerRemoving.BindToClose - DataStore calls fail in Studio unless Enable Studio Access to API Services is checked in Game Settings > Security.
- Studio and live game share the same DataStore if using the same place. Use different DataStore names or a prefix for testing:
luau
local RunService = game:GetService("RunService")
local PREFIX = RunService:IsStudio() and "Dev_" or ""
local dataStore = DataStoreService:GetDataStore(`{PREFIX}PlayerData_v1`)- 在Studio中按下停止按钮时,事件不会触发。测试期间退出游戏时数据不会保存,除非同时测试
PlayerRemoving逻辑。BindToClose - 除非在游戏设置 > 安全中勾选启用Studio对API服务的访问,否则Studio中的DataStore调用会失败。
- 如果使用同一游戏实例,Studio和线上游戏会共享同一DataStore。测试时请使用不同的DataStore名称或前缀:
luau
local RunService = game:GetService("RunService")
local PREFIX = RunService:IsStudio() and "Dev_" or ""
local dataStore = DataStoreService:GetDataStore(`{PREFIX}PlayerData_v1`)Other Pitfalls
其他陷阱
- NaN values: If a NaN (not a number) sneaks into your data (e.g., ),
0/0/SetAsyncwill error silently or corrupt the entry. Validate numeric fields.UpdateAsync - Empty tables: An empty table can deserialize as either an array or a dictionary depending on context. Be consistent.
{} - Key naming: Keys are case-sensitive. and
"Player_123"are different keys. Standardize your key format."player_123" - UpdateAsync callback: The callback passed to must be pure (no yields, no side effects). It may be called multiple times if there is contention. Return
UpdateAsyncto cancel the update.nil
- **NaN值:**如果NaN(非数字)混入数据中(例如),
0/0/SetAsync会静默报错或损坏数据条目。请验证数值字段。UpdateAsync - **空表:**空表在不同上下文下可能被反序列化为数组或字典。请保持一致性。
{} - **键命名:**键区分大小写。和
"Player_123"是不同的键。请标准化键的格式。"player_123" - **UpdateAsync回调:**传递给的回调必须是纯函数(无yield,无副作用)。如果存在竞争,它可能会被多次调用。返回
UpdateAsync可取消更新。",nil