关于 authoritative-server
用于开发 Decentraland 多人场景的技能,由无头服务器掌控游戏状态并验证变更。内容涵盖服务端与客户端分支、同步状态、消息通信、持久化和环境配置。
主要功能
- 01使用 isServer() 区分共享代码
- 02验证服务端权威组件变更
- 03向客户端同步实体和状态
- 04在客户端与服务器间交换消息
- 05持久化世界和玩家数据
适用场景
- 01构建由服务器控制状态的多人场景
- 02验证玩家操作以防客户端篡改
- 03跨重启保存排行榜或玩家分数
- 04通过环境变量配置场景行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dcl-regenesislabs/ opendcl --skill authoritative-server安装后在 Claude Code 中输入/authoritative-server确认已加载。
附带文件references/server-patterns.md
详细介绍
权威服务器模式
构建多人 Decentraland 场景,由无头服务器控制游戏状态、验证更改并防止作弊。服务器和客户端运行同一套代码,且服务器拥有完全控制权。
如需了解基础 CRDT 多人游戏(无服务器),请改用 multiplayer-sync 技能。
设置
1. 安装 auth-server SDK 分支(必需)
必须使用 auth-server 标签——标准的 @dcl/sdk 不包含权威服务器 API(isServer、registerMessages、Storage、EnvVar 等):
npm install @dcl/sdk@auth-server
2. 配置 scene.json
你的 scene.json 必须包含以下属性:
authoritativeMultiplayer: true— 启用权威服务器运行时。worldConfiguration.name— 标识用于部署、Storage 和 EnvVar 的世界。logsPermissions— 允许在控制台中查看服务器日志的钱包地址数组。若缺少此项,服务器的console.log()输出会被隐藏。
{
"authoritativeMultiplayer": true,
"worldConfiguration": {
"name": "my-world-name.dcl.eth"
},
"logsPermissions": ["0xYourWalletAddress"]
}
3. 运行场景
直接使用常规预览即可——当 scene.json 中设置了 authoritativeMultiplayer: true 时,预览会自动在后台启动权威服务器。
调试说明(不要告诉用户运行此命令): 在底层,预览会运行
npx @dcl/hammurabi-server@next。如果 auth server 未启动,请检查 hammurabi 进程是否正在运行,并查看其输出中的错误。
服务器/客户端分支
使用 isServer() 在同一套代码中分支处理逻辑:
import { isServer } from '@dcl/sdk/network'
export async function main() {
if (isServer()) {
// Server-only: game logic, validation, state management
const { server } = await import('./server/server')
server()
return
}
// Client-only: UI, input, message sending
setupClient()
setupUi()
}
服务器会以无头模式运行你的场景代码(不进行渲染)。它可以通过 PlayerIdentityData 访问所有玩家的位置,并管理所有权威游戏状态。
带验证的同步组件
定义从服务器同步到所有客户端的自定义组件。始终使用 validateBeforeChange(),以防止客户端修改服务器权威状态。
自定义组件(全局验证)
import { engine, Schemas } from '@dcl/sdk/ecs'
import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'
export const GameState = engine.defineComponent('game:State', {
phase: Schemas.String,
score: Schemas.Number,
timeRemaining: Schemas.Number
})
// Restrict ALL modifications to server only
GameState.validateBeforeChange((value) => {
return value.senderAddress === AUTH_SERVER_PEER_ID
})
内置组件(按实体验证)
对于 Transform 和 GltfContainer 等内置组件,请使用按实体验证,以免阻止玩家自身实体上的客户端变换:
import { Entity, Transform, GltfContainer } from '@dcl/sdk/ecs'
import { AUTH_SERVER_PEER_ID } from '@dcl/sdk/network/message-bus-sync'
type ComponentWithValidation = {
validateBeforeChange: (entity: Entity, cb: (value: { senderAddress: string }) => boolean) => void
}
function protectServerEntity(entity: Entity, components: ComponentWithValidation[]) {
for (const component of components) {
component.validateBeforeChange(entity, (value) => {
return value.senderAddress === AUTH_SERVER_PEER_ID
})
}
}
// Usage: after creating a server-managed entity
const entity = engine.addEntity()
Transform.create(entity, { position: Vector3.create(10, 5, 10) })
GltfContainer.create(entity, { src: 'assets/model.glb' })
protectServerEntity(entity, [Transform, GltfContainer])
同步实体
创建实体并设置保护后,将其同步到所有客户端:
import { syncEntity } from '@dcl/sdk/network'
syncEntity(entity, [Transform.componentId, GameState.componentId])
消息
使用 registerMessages() 进行客户端到服务器以及服务器到客户端的通信:
定义消息
import { Schemas } from '@dcl/sdk/ecs'
import { registerMessages } from '@dcl/sdk/network'
export const Messages = {
// Client -> Server
playerJoin: Schemas.Map({ displayName: Schemas.String }),
playerAction: Schemas.Map({ actionType: Schemas.String, data: Schemas.Number }),
// Server -> Client
gameEvent: Schemas.Map({ eventType: Schemas.String, playerName: Schemas.String })
}
export const room = registerMessages(Messages)
发送消息
// Client sends to server
room.send('playerJoin', { displayName: 'Alice' })
// Server sends to ALL clients
room.send('gameEvent', { eventType: 'ROUND_START', playerName: '' })
// Server sends to ONE client
room.send('gameEvent', { eventType: 'YOU_WIN', playerName: 'Alice' }, { to: [playerAddress] })
接收消息
// Server receives from client
room.onMessage('playerJoin', (data, context) => {
if (!context) return
const playerAddress = context.from // Wallet address of sender
console.log(`[Server] Player joined: ${data.displayName} (${playerAddress})`)
})
// Client receives from server
room.onMessage('gameEvent', (data) => {
console.log(`Event: ${data.eventType}`)
})
等待状态同步
从客户端发送消息前,请等待状态同步完成:
import { isStateSyncronized } from '@dcl/sdk/network'
engine.addSystem(() => {
if (!isStateSyncronized()) return
// Safe to send messages now
room.send('playerJoin', { displayName: 'Player' })
})
服务器读取玩家位置
服务器可以读取玩家的实际位置——这对防作弊至关重要:
import { engine, PlayerIdentityData, Transform } from '@dcl/sdk/ecs'
engine.addSystem(() => {
for (const [entity, identity] of engine.getEntitiesWith(PlayerIdentityData)) {
const transform = Transform.getOrNull(entity)
if (!transform) continue
const address = identity.address
const position = transform.position
// Use actual server-verified position, not client-reported data
}
})
切勿信任客户端报告的位置。始终在服务器上读取 PlayerIdentityData 和 Transform。
存储
在服务器重启后保留数据。仅限服务器使用——请用 isServer() 进行保护。
import { Storage } from '@dcl/sdk/server'
世界存储(全局)
在所有玩家之间共享:
// Store
await Storage.world.set('leaderboard', JSON.stringify(leaderboardData))
// Retrieve
const data = await Storage.world.get<string>('leaderboard')
if (data) {
const leaderboard = JSON.parse(data)
}
// Delete
await Storage.world.delete('oldKey')
玩家存储(按玩家)
以玩家钱包地址为键:
// Store
await Storage.player.set(playerAddress, 'highScore', String(score))
// Retrieve
const saved = await Storage.player.get<string>(playerAddress, 'highScore')
const highScore = saved ? parseInt(saved) : 0
// Delete
await Storage.player.delete(playerAddress, 'highScore')
Storage 仅接受字符串。对象请使用 JSON.stringify()/JSON.parse(),数字请使用 String()/parseInt()。
本地开发存储位于 node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json。
环境变量
无需硬编码值即可配置场景。仅限服务器使用——请用 isServer() 进行保护。
import { EnvVar } from '@dcl/sdk/server'
// Read a variable with default
const maxPlayers = parseInt((await EnvVar.get('MAX_PLAYERS')) || '4')
const debugMode = ((await EnvVar.get('DEBUG')) || 'false') === 'true'
本地开发
在项目根目录中创建 .env 文件:
MAX_PLAYERS=8
GAME_DURATION=300
DEBUG=true
将 .env 添加到 .gitignore。
部署到生产环境
# Set a variable
npx sdk-commands deploy-env MAX_PLAYERS --value 8
# Delete a variable
npx sdk-commands deploy-env OLD_VAR --delete
已部署的环境变量优先于 .env 文件中的值。
推荐的项目结构
src/
├── index.ts # Entry point — isServer() branching
├── client/
│ ├── setup.ts # Client initialization, message handlers
│ └── ui.tsx # React ECS UI reading synced state
├── server/
│ ├── server.ts # Server init, systems, message handlers
│ └── gameState.ts # Server state management class
└── shared/
├── schemas.ts # Synced component definitions + validateBeforeChange
└── messages.ts # Message definitions via registerMessages()
将同步组件和消息放在 shared/ 中,以便服务器和客户端导入相同的定义。将服务器逻辑(Storage、EnvVar、游戏系统)放在 server/ 中。将 UI 和客户端输入处理放在 client/ 中。
测试与调试
- 日志前缀:在
console.log()中使用[Server]和[Client]前缀,以便在终端中区分服务端和客户端输出。 - 过期的 CRDT 文件:如果看到 "Outside of the bounds of written data" 错误,请删除
main.crdt和main1.crdt文件,然后重新启动。 - 存储检查:在本地开发期间,检查
node_modules/@dcl/sdk-commands/.runtime-data/server-storage.json,查看已持久化的数据。 - 计时器:运行时 polyfill 提供了
setTimeout/setInterval。对于游戏逻辑,优先使用带有 delta-time 累加器的engine.addSystem(),以便与帧循环保持同步。 - 实体同步问题:确认使用正确的组件 ID 调用
syncEntity(entity, [componentIds])(MyComponent.componentId)。
重要说明
- 时间戳使用
Schemas.Int64:Schemas.Number会损坏较大的数字(13 位或更多)。对于Date.now()等值,请始终使用Schemas.Int64。 - 状态同步就绪:客户端在发送消息前,必须等待
isStateSyncronized()(来自@dcl/sdk/network)返回true。请注意 SDK 中有意保留的拼写错误:"Syncronized" 而非 "Synchronized"。 - 自定义验证与内置验证:自定义组件使用全局的
validateBeforeChange((value) => ...)。内置组件(Transform、GltfContainer)使用针对每个实体的validateBeforeChange(entity, (value) => ...)。 - 单一代码库:服务端和客户端运行相同的
index.ts入口点。使用isServer()进行分支判断。 - 不支持 Node.js API:DCL 运行时使用沙箱化的 QuickJS,不支持
fs、http等。支持setTimeout/setInterval。服务端操作请使用 SDK 提供的 API(Storage、EnvVar、engine systems)。 - SDK 分支(必须):auth-server 模式要求使用
npm install @dcl/sdk@auth-server,而非标准的@dcl/sdk。否则,isServer()、registerMessages()、Storage和EnvVar将不可用。 scene.json必填字段:必须设置authoritativeMultiplayer: true,并在logsPermissions: ["0xWalletAddress"]中列出应能查看服务端日志的钱包地址。- 如需不使用服务端的基础 CRDT 多人游戏,请参阅
multiplayer-sync技能。
如需完整的服务端设置示例、身份验证流程、状态协调、Storage 模式和 EnvVar 用法,请参阅 {baseDir}/references/server-patterns.md。