关于 starwards-colyseus
这是面向 Starwards 开发者的 Colyseus 多人游戏同步实践 skill,适用于维护房间、状态和客户端命令的开发工作。它说明状态同步规则、常见陷阱及测试和调试方法。
主要功能
- 01使用 @gameField 定义同步状态
- 02组织房间与客户端命令
- 03处理嵌套集合和状态更新
- 04说明类型精度及同步注意事项
- 05提供同步测试与调试流程
适用场景
- 01新增或修改多人游戏同步状态
- 02排查客户端状态未更新的问题
- 03测试多个客户端的状态一致性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ starwards/ starwards --skill starwards-colyseus安装后在 Claude Code 中输入/starwards-colyseus确认已加载。
详细介绍
Starwards 的 Colyseus 多人游戏模式
概述
Starwards 使用 Colyseus v0.15 实时同步多人游戏状态。了解装饰器、房间和状态同步可避免错误。
核心原则: 状态是真实数据源。命令会修改状态。客户端会自动接收更新。
架构
Client (Browser)
↓ WebSocket connection
Room (Server)
↓ owns
State (Schema)
↓ syncs to
Client State (Mirror)
流程:
- 客户端发送命令 → 房间
- 房间修改状态
- Colyseus 推送补丁 → 客户端
- 客户端状态自动更新
- UI 响应状态变化
@gameField 装饰器
用途: 标记属性以便 Colyseus 自动同步
位置: modules/core/src/game-field.ts
用法:
import { gameField } from '../game-field';
class Shield extends SystemState {
// Primitive types
@gameField('float32') strength = 1000;
@gameField('float32') power = 1.0;
@gameField('boolean') broken = false;
// Nested Schema
@gameField(ShieldDesign) design = new ShieldDesign();
// Arrays
@gameField([Emitter]) emitters = new ArraySchema<Emitter>();
// Maps
@gameField({ map: Target }) targets = new MapSchema<Target>();
}
类型:
'float32'- 32 位浮点数(精度低于 64 位数字!)'float64'- 64 位浮点数'int8'、'int16'、'int32'- 有符号整数'uint8'、'uint16'、'uint32'- 无符号整数'boolean'- 布尔值'string'- 字符串SchemaClass- 嵌套 Schema[SchemaClass]- Schema 数组{map: SchemaClass}- Schema 映射
@gameField 的重要规则
规则 1:@gameField 必须是最后一个装饰器
// CORRECT order
@range([0, 1]) // 1st
@tweakable('number') // 2nd
@gameField('float32') // 3rd - LAST
power = 1.0;
// WRONG order
@gameField('float32') // Can't be first
@range([0, 1])
power = 1.0;
原因: 装饰器从下往上执行。@gameField 必须包装最终属性。
规则 2:初始化集合
// CORRECT
@gameField([Thruster])
thrusters = new ArraySchema<Thruster>();
@gameField({map: Spaceship})
ships = new MapSchema<Spaceship>();
// WRONG
@gameField([Thruster])
thrusters: ArraySchema<Thruster>; // Not initialized!
原因: Colyseus 需要实例,不能是 undefined。
规则 3:使用正确的类型
// CORRECT
@gameField('float32') speed = 0; // 32-bit float
@gameField('int16') count = 0; // 16-bit int
// WRONG
@gameField('number') speed = 0; // No 'number' type
@gameField('float') speed = 0; // No 'float' type (use float32/float64)
原因: Colyseus Schema 类型必须显式指定。
规则 4:不要同步所有内容
// Only sync state that clients need
@gameField('float32') health = 100; // ✅ Clients need this
@gameField('float32') damage = 10; // ❌ Internal calculation, don't sync
// Derive internally
get damage() {
return this.weapon.baseDamage * this.effectiveness;
}
原因: 同步越少,性能越好。
嵌套 ArraySchema(深度为两层)
一个 ArraySchema<Child> 嵌套在另一个 ArraySchema<Parent> 的项目中时,可以正确同步——第二层深度无需特殊处理,只需在中间层 Schema 上使用普通的 @gameField([Child])。实际示例:modules/core/src/ship/armor.ts 中的 Armor.armorPlates[i].layers[j].health。
- colyseus-events 会发送包含两个索引的叶节点事件(例如
/armor/armorPlates/3/layers/0/health),而 EventEmitter2 通配符监听器(/armor/armorPlates/*/layers/*/health,分隔符为'/')可以匹配这些事件。 - 在
wireEvents之后推入新的嵌套子项时,会自动为其接入事件。
由 modules/core/test/armor-layers-sync.spec.ts 固定验证。
Float32 精度注意事项
问题: JavaScript 数字是 64 位,而 Colyseus 的 float32 是 32 位
@gameField('float32') speed = 123.456789;
console.log(speed); // 123.46 (precision lost!)
解决方案: 在测试中使用 toBeCloseTo()
// WRONG
expect(ship.speed).toBe(123.456789);
// CORRECT
expect(ship.speed).toBeCloseTo(123.46, 1);
状态与非状态
状态(已同步):
class ShipState extends Schema {
@gameField('float32') health = 100; // Synced
@gameField(Reactor) reactor!: Reactor; // Synced
}
非状态(仅服务器):
class ShipManager {
updateRate = 60; // Not synced
lastUpdate = Date.now(); // Not synced
update(dt: number) {
this.state.health -= 10 * dt; // Modify state → syncs
}
}
规则: 只有带有 @gameField 的 Schema 类才会同步。普通属性不会同步。
命令:客户端 → 服务器
有两种模式:
1. JSON Pointer(动态)
客户端:
room.send({
type: '/Spaceship/ship-1/reactor/power',
value: 0.8
});
服务器(在 ShipRoom 中自动处理):
// No code needed - JSON Pointer auto-applies to state
适用场景: 简单的属性更新、GM 界面、调试
2. 类型化命令(优化)
定义(在 core 中):
export const setShieldPower: StateCommand<number, ShipState, void> = {
cmdName: 'setShieldPower',
setValue: (state, value) => {
state.shield.power = value;
}
};
服务器(在房间中注册):
this.onMessage(setShieldPower.cmdName, cmdReceiver(this.manager, setShieldPower));
客户端(发送):
const send = cmdSender(room, setShieldPower, undefined);
send(0.5); // Type-safe!
适用场景: 高频命令、复杂验证、类型安全
房间架构
AdminRoom
用途: 游戏管理、地图选择、开始/停止
状态: AdminState(包含 SpaceState)
客户端: GM 界面、管理面板
命令: startGame、stopGame、loadMap
SpaceRoom
用途: 太空级游戏玩法、物理模拟
状态: SpaceState(所有太空对象)
客户端: 战术显示屏、总览屏幕
管理者: GameManager、SpaceManager
ShipRoom(每艘船一个)
用途: 单艘船的控制
状态: ShipState(船只系统)
客户端: 船只操作站(武器、工程等)
roomId: 等于 shipId(ship-0、ship-1 等)
命令: 仅使用 JSON Pointer(以提高灵活性)
状态同步模式
模式 1:服务器修改,客户端响应
服务器:
class ShieldManager {
update(dt: number) {
// Modify state → auto-syncs
this.state.shield.strength += rechargeRate * dt;
}
}
客户端:
// Listen for changes
ship.state.shield.onChange(() => {
updateUI(ship.state.shield.strength);
});
模式 2:客户端发送命令,服务器验证
客户端:
// User adjusts power slider
powerSlider.on('change', (value) => {
room.send({type: '/Spaceship/ship-0/reactor/power', value});
});
服务器:
// Receives command, validates, applies
this.onMessage((client, message) => {
const value = clamp(0, 1, message.value); // Validate
applyJsonPointer(this.state, message.type, value); // Apply
// Auto-syncs to all clients
});
客户端:
// UI updates automatically from synced state
ship.state.reactor.listen('power', (value) => {
powerSlider.value = value;
});
模式 3:多人游戏测试
使用 ShipTestHarness:
import { ShipTestHarness } from './ship-test-harness';
test('client receives server state updates', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// Server modifies
harness.shipManager.state.shield.strength = 750;
// Wait for sync
await harness.waitForSync();
// Client receives
expect(harness.shipDriver.state.shield.strength).toBe(750);
await harness.cleanup();
});
使用 MultiClientDriver:
import { MultiClientDriver } from '@starwards/server/test/multi-client-driver';
test('multiple clients see same state', async () => {
const driver = new MultiClientDriver();
await driver.start();
const [c1, c2] = await Promise.all([
driver.joinShip('ship-1'),
driver.joinShip('ship-1')
]);
// Modify server state
driver.getShipManager('ship-1').state.shield.strength = 800;
await driver.waitForSync();
// Both clients updated
expect(c1.state.shield.strength).toBe(800);
expect(c2.state.shield.strength).toBe(800);
await driver.cleanup();
});
完整 API 参见 docs/testing/UTILITIES.md。
常见的 Colyseus 陷阱
陷阱 1:修改状态时未添加 @gameField
// WRONG - doesn't sync
class Shield {
strength = 1000; // No decorator
}
// CORRECT - syncs
class Shield extends Schema {
@gameField('float32') strength = 1000;
}
陷阱 2:设置对象而不是属性
// WRONG - breaks references
state.velocity = {x: 10, y: 0};
// CORRECT - update properties
state.velocity.setValue({x: 10, y: 0});
// Or:
state.velocity.x = 10;
state.velocity.y = 0;
原因: Colyseus 跟踪的是属性变更,而不是对象替换。
陷阱 3:忘记 await harness.waitForSync()
// WRONG - client not updated yet
harness.shipManager.state.health = 50;
expect(harness.shipDriver.state.health).toBe(50); // FAILS
// CORRECT - wait for replication
harness.shipManager.state.health = 50;
await harness.waitForSync();
expect(harness.shipDriver.state.health).toBe(50); // PASSES
陷阱 4:在客户端逻辑中使用状态
// WRONG - client shouldn't have business logic
if (ship.state.health < 50) {
ship.state.broken = true; // Don't modify from client
}
// CORRECT - send command, server decides
if (ship.state.health < 50) {
room.send({type: 'checkBroken'}); // Server validates & applies
}
原因: 服务端具有权威性。客户端仅用于显示。
陷阱 5:测试中的 Float32 精度
// WRONG - exact match fails
expect(ship.speed).toBe(123.456789);
// CORRECT - close enough
expect(ship.speed).toBeCloseTo(123.46, 1);
陷阱 6:未清理测试工具
// WRONG - leaves connections open
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// Test code
}); // MISSING cleanup()!
// CORRECT
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// Test code
await harness.cleanup(); // Clean up
});
调试 Colyseus 问题
1. Colyseus 监视器
http://localhost:2567/colyseus-monitor
Login: admin / admin
查看:
- 活动房间
- 已连接的客户端
- 状态树(实时值)
2. Chrome DevTools 网络面板
WebSocket 消息:
- 打开 DevTools(F12)
- 选择 Network 面板 → WS 筛选器
- 点击连接
- 查看 Messages:已发送的命令、已接收的补丁
3. 状态日志
服务端:
console.log('[SERVER] Shield strength:', this.state.shield.strength);
客户端:
console.log('[CLIENT] Shield strength:', ship.state.shield.strength);
比较值以查找同步问题。
4. JSON Pointer 路径验证
测试路径:
const path = '/Spaceship/ship-1/shield/power';
const obj = resolveJsonPointer(state, path);
console.log('Resolved:', obj); // Should not be undefined
性能注意事项
尽量减少同步频率
// WRONG - syncs 60 times/sec
update(dt: number) {
this.state.position.x += velocity.x * dt;
this.state.position.y += velocity.y * dt;
}
// BETTER - sync only when significant change
update(dt: number) {
const newX = this.state.position.x + velocity.x * dt;
const newY = this.state.position.y + velocity.y * dt;
if (Math.abs(newX - this.state.position.x) > 0.1) {
this.state.position.x = newX;
}
if (Math.abs(newY - this.state.position.y) > 0.1) {
this.state.position.y = newY;
}
}
但是: Starwards 的更新已经过优化。不要过早优化。
使用合适的类型
// WRONG - wastes bandwidth
@gameField('float64') health = 100; // 8 bytes
// CORRECT - sufficient precision
@gameField('float32') health = 100; // 4 bytes
批量发送命令
// WRONG - multiple round trips
room.send({type: '/Spaceship/ship-0/reactor/power', value: 0.8});
room.send({type: '/Spaceship/ship-0/thrusters/0/enabled', value: true});
room.send({type: '/Spaceship/ship-0/thrusters/1/enabled', value: true});
// BETTER - single batch command
room.send('batchUpdate', {
'/reactor/power': 0.8,
'/thrusters/0/enabled': true,
'/thrusters/1/enabled': true
});
但是: 仅在确实存在性能瓶颈时才这样做。常规使用时,JSON Pointer 已足够。
与其他技能集成
- starwards-tdd - 使用测试工具测试状态同步
- starwards-debugging - 使用工具调试同步问题
- starwards-verification - 验证多人游戏场景
快速参考
| 任务 | 模式 |
|---|---|
| 添加同步属性 | @gameField('type') prop = value |
| 嵌套 Schema | @gameField(Class) obj = new Class() |
| 数组 | @gameField([Class]) arr = new ArraySchema() |
| 映射 | @gameField({map: Class}) map = new MapSchema() |
| 发送命令(客户端) | room.send({type: '/path', value}) |
| 监听变更(客户端) | state.onChange(() => {}) |
| 测试同步 | await harness.waitForSync() |
| 调试状态 | Colyseus 监视器(端口 2567) |
总结
请记住:
- @gameField 必须是最后一个装饰器
- 状态是事实来源(服务端具有权威性)
- 命令从客户端发送到服务端,补丁从服务端发送到客户端
- 使用测试工具进行多人游戏测试
- Float32 存在精度损失(使用 toBeCloseTo)
- 清理测试连接(await cleanup())
- 如有疑问,请查看 Colyseus 监视器