starwards-colyseus
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseColyseus Multiplayer Patterns for Starwards
Starwards 的 Colyseus 多人游戏模式指南
Overview
概述
Starwards uses Colyseus v0.15 for real-time multiplayer state synchronization. Understanding decorators, rooms, and state sync prevents bugs.
Core principle: State is the source of truth. Commands modify state. Clients receive automatic updates.
Starwards 使用 Colyseus v0.15 实现实时多人游戏状态同步。理解装饰器、房间和状态同步机制可避免出现Bug。
核心原则: 状态为唯一可信源。通过命令修改状态,客户端将自动接收更新。
Architecture
架构
Client (Browser)
↓ WebSocket connection
Room (Server)
↓ owns
State (Schema)
↓ syncs to
Client State (Mirror)Flow:
- Client sends command → Room
- Room modifies State
- Colyseus patches → Client
- Client state updates automatically
- UI reacts to state changes
Client (Browser)
↓ WebSocket connection
Room (Server)
↓ owns
State (Schema)
↓ syncs to
Client State (Mirror)流程:
- 客户端发送命令 → 房间(Room)
- 房间修改状态(State)
- Colyseus 生成补丁 → 客户端
- 客户端状态自动更新
- UI 响应状态变化
The @gameField Decorator
@gameField 装饰器
Purpose: Marks properties for automatic Colyseus synchronization
Location:
modules/core/src/game-field.tsUsage:
typescript
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>();
}Types:
- - 32-bit float (precision loss vs 64-bit number!)
'float32' - - 64-bit float
'float64' - ,
'int8','int16'- Signed integers'int32' - ,
'uint8','uint16'- Unsigned integers'uint32' - - Boolean
'boolean' - - String
'string' - - Nested Schema
SchemaClass - - Array of Schema
[SchemaClass] - - Map of Schema
{map: SchemaClass}
用途: 标记需要Colyseus自动同步的属性
位置:
modules/core/src/game-field.ts用法:
typescript
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>();
}支持的类型:
- - 32位浮点数(与64位数字相比存在精度损失!)
'float32' - - 64位浮点数
'float64' - ,
'int8','int16'- 有符号整数'int32' - ,
'uint8','uint16'- 无符号整数'uint32' - - 布尔值
'boolean' - - 字符串
'string' - - 嵌套Schema类
SchemaClass - - Schema类数组
[SchemaClass] - - Schema类映射
{map: SchemaClass}
Critical @gameField Rules
@gameField 核心规则
Rule 1: @gameField Must Be Last Decorator
规则1:@gameField 必须是最后一个装饰器
typescript
// 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;Why: Decorators execute bottom-to-top. @gameField must wrap the final property.
typescript
// 正确顺序
@range([0, 1]) // 第1个
@tweakable('number') // 第2个
@gameField('float32') // 第3个 - 最后一个
power = 1.0;
// 错误顺序
@gameField('float32') // 不能放在第一个
@range([0, 1])
power = 1.0;原因: 装饰器的执行顺序是从下到上。@gameField 必须包裹最终的属性。
Rule 2: Initialize Collections
规则2:初始化集合类型
typescript
// CORRECT
@gameField([Thruster])
thrusters = new ArraySchema<Thruster>();
@gameField({map: Spaceship})
ships = new MapSchema<Spaceship>();
// WRONG
@gameField([Thruster])
thrusters: ArraySchema<Thruster>; // Not initialized!Why: Colyseus needs instances, not undefined.
typescript
// 正确写法
@gameField([Thruster])
thrusters = new ArraySchema<Thruster>();
@gameField({map: Spaceship})
ships = new MapSchema<Spaceship>();
// 错误写法
@gameField([Thruster])
thrusters: ArraySchema<Thruster>; // 未初始化!原因: Colyseus 需要具体的实例,而不是undefined。
Rule 3: Use Correct Types
规则3:使用正确的类型
typescript
// 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)Why: Colyseus schema types are explicit.
typescript
// 正确写法
@gameField('float32') speed = 0; // 32位浮点数
@gameField('int16') count = 0; // 16位整数
// 错误写法
@gameField('number') speed = 0; // 不存在'number'类型
@gameField('float') speed = 0; // 不存在'float'类型(请使用float32/float64)原因: Colyseus 的Schema类型是显式定义的。
Rule 4: Don't Sync Everything
规则4:不要同步所有内容
typescript
// 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;
}Why: Less sync = better performance.
typescript
// 只同步客户端需要的状态
@gameField('float32') health = 100; // ✅ 客户端需要该数据
@gameField('float32') damage = 10; // ❌ 内部计算数据,无需同步
// 内部推导计算
get damage() {
return this.weapon.baseDamage * this.effectiveness;
}原因: 同步的数据越少,性能越好。
Float32 Precision Gotcha
Float32 精度陷阱
Problem: JavaScript numbers are 64-bit, Colyseus is 32-bit
float32typescript
@gameField('float32') speed = 123.456789;
console.log(speed); // 123.46 (precision lost!)Solution: Use in tests
toBeCloseTo()typescript
// WRONG
expect(ship.speed).toBe(123.456789);
// CORRECT
expect(ship.speed).toBeCloseTo(123.46, 1);问题: JavaScript 数字为64位,而Colyseus的是32位
float32typescript
@gameField('float32') speed = 123.456789;
console.log(speed); // 123.46(精度丢失!)解决方案: 在测试中使用
toBeCloseTo()typescript
// 错误写法
expect(ship.speed).toBe(123.456789);
// 正确写法
expect(ship.speed).toBeCloseTo(123.46, 1);State vs Non-State
同步状态与非同步状态
State (synced):
typescript
class ShipState extends Schema {
@gameField('float32') health = 100; // Synced
@gameField(Reactor) reactor!: Reactor; // Synced
}Non-State (server only):
typescript
class ShipManager {
updateRate = 60; // Not synced
lastUpdate = Date.now(); // Not synced
update(dt: number) {
this.state.health -= 10 * dt; // Modify state → syncs
}
}Rule: Only Schema classes with @gameField sync. Plain properties don't sync.
同步状态(会同步到客户端):
typescript
class ShipState extends Schema {
@gameField('float32') health = 100; // 已同步
@gameField(Reactor) reactor!: Reactor; // 已同步
}非同步状态(仅服务器端使用):
typescript
class ShipManager {
updateRate = 60; // 未同步
lastUpdate = Date.now(); // 未同步
update(dt: number) {
this.state.health -= 10 * dt; // 修改状态 → 会同步到客户端
}
}规则: 只有带有@gameField装饰器的Schema类才会同步。普通属性不会同步。
Commands: Client → Server
命令:客户端 → 服务器
Two patterns:
两种实现模式:
1. JSON Pointer (Dynamic)
1. JSON Pointer(动态模式)
Client:
typescript
room.send({
type: '/Spaceship/ship-1/reactor/power',
value: 0.8
});Server (auto-handled in ShipRoom):
typescript
// No code needed - JSON Pointer auto-applies to stateUse when: Simple property updates, GM interface, debugging
客户端代码:
typescript
room.send({
type: '/Spaceship/ship-1/reactor/power',
value: 0.8
});服务器端(ShipRoom中自动处理):
typescript
// 无需编写代码 - JSON Pointer 会自动应用到状态适用场景: 简单属性更新、GM界面、调试
2. Typed Commands (Optimized)
2. 类型化命令(优化模式)
Define (in core):
typescript
export const setShieldPower: StateCommand<number, ShipState, void> = {
cmdName: 'setShieldPower',
setValue: (state, value) => {
state.shield.power = value;
}
};Server (register in room):
typescript
this.onMessage(setShieldPower.cmdName, cmdReceiver(this.manager, setShieldPower));Client (send):
typescript
const send = cmdSender(room, setShieldPower, undefined);
send(0.5); // Type-safe!Use when: High-frequency commands, complex validation, type safety
定义命令(核心模块中):
typescript
export const setShieldPower: StateCommand<number, ShipState, void> = {
cmdName: 'setShieldPower',
setValue: (state, value) => {
state.shield.power = value;
}
};服务器端(在房间中注册):
typescript
this.onMessage(setShieldPower.cmdName, cmdReceiver(this.manager, setShieldPower));客户端(发送命令):
typescript
const send = cmdSender(room, setShieldPower, undefined);
send(0.5); // 类型安全!适用场景: 高频命令、复杂验证、类型安全需求
Room Architecture
房间架构
AdminRoom
AdminRoom(管理房间)
Purpose: Game management, map selection, start/stop
State: (has SpaceState)
AdminStateClients: GM interface, admin panel
Commands: , ,
startGamestopGameloadMap用途: 游戏管理、地图选择、开始/停止游戏
状态: (包含SpaceState)
AdminState连接客户端: GM界面、管理面板
支持命令: , ,
startGamestopGameloadMapSpaceRoom
SpaceRoom(太空场景房间)
Purpose: Space-level gameplay, physics simulation
State: (all space objects)
SpaceStateClients: Tactical displays, overview screens
Managed by: ,
GameManagerSpaceManager用途: 太空层级游戏玩法、物理模拟
状态: (包含所有太空对象)
SpaceState连接客户端: 战术显示器、概览屏幕
管理模块: ,
GameManagerSpaceManagerShipRoom (per ship)
ShipRoom(单船房间,每个飞船对应一个)
Purpose: Individual ship control
State: (ship systems)
ShipStateClients: Ship stations (weapons, engineering, etc.)
roomId: Equals (, , etc.)
shipIdship-0ship-1Commands: JSON Pointer only (for flexibility)
用途: 单个飞船的控制
状态: (飞船系统数据)
ShipState连接客户端: 飞船操作台(武器、工程等)
roomId: 与一致(如, 等)
shipIdship-0ship-1支持命令: 仅支持JSON Pointer(保证灵活性)
State Sync Patterns
状态同步模式
Pattern 1: Server Modifies, Clients React
模式1:服务器修改状态,客户端响应
Server:
typescript
class ShieldManager {
update(dt: number) {
// Modify state → auto-syncs
this.state.shield.strength += rechargeRate * dt;
}
}Client:
typescript
// Listen for changes
ship.state.shield.onChange(() => {
updateUI(ship.state.shield.strength);
});服务器端:
typescript
class ShieldManager {
update(dt: number) {
// 修改状态 → 自动同步
this.state.shield.strength += rechargeRate * dt;
}
}客户端:
typescript
// 监听状态变化
ship.state.shield.onChange(() => {
updateUI(ship.state.shield.strength);
});Pattern 2: Client Commands, Server Validates
模式2:客户端发送命令,服务器验证并执行
Client:
typescript
// User adjusts power slider
powerSlider.on('change', (value) => {
room.send({type: '/Spaceship/ship-0/reactor/power', value});
});Server:
typescript
// 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
});Client:
typescript
// UI updates automatically from synced state
ship.state.reactor.listen('power', (value) => {
powerSlider.value = value;
});客户端:
typescript
// 用户调整功率滑块
powerSlider.on('change', (value) => {
room.send({type: '/Spaceship/ship-0/reactor/power', value});
});服务器端:
typescript
// 接收命令、验证、执行
this.onMessage((client, message) => {
const value = clamp(0, 1, message.value); // 验证
applyJsonPointer(this.state, message.type, value); // 应用
// 自动同步到所有客户端
});客户端:
typescript
// UI 根据同步后的状态自动更新
ship.state.reactor.listen('power', (value) => {
powerSlider.value = value;
});Pattern 3: Multiplayer Testing
模式3:多人游戏测试
Use :
ShipTestHarnesstypescript
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();
});Use :
MultiClientDrivertypescript
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();
});See for full API.
docs/testing/UTILITIES.md使用:
ShipTestHarnesstypescript
import { ShipTestHarness } from './ship-test-harness';
test('client receives server state updates', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// 服务器修改状态
harness.shipManager.state.shield.strength = 750;
// 等待同步完成
await harness.waitForSync();
// 客户端接收到更新
expect(harness.shipDriver.state.shield.strength).toBe(750);
await harness.cleanup();
});使用:
MultiClientDrivertypescript
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')
]);
// 修改服务器状态
driver.getShipManager('ship-1').state.shield.strength = 800;
await driver.waitForSync();
// 两个客户端都收到更新
expect(c1.state.shield.strength).toBe(800);
expect(c2.state.shield.strength).toBe(800);
await driver.cleanup();
});完整API请参考 。
docs/testing/UTILITIES.mdCommon Colyseus Pitfalls
Colyseus 常见陷阱
Pitfall 1: Modifying State Without @gameField
陷阱1:修改未标记@gameField的状态
typescript
// WRONG - doesn't sync
class Shield {
strength = 1000; // No decorator
}
// CORRECT - syncs
class Shield extends Schema {
@gameField('float32') strength = 1000;
}typescript
// 错误写法 - 不会同步
class Shield {
strength = 1000; // 没有装饰器
}
// 正确写法 - 会同步
class Shield extends Schema {
@gameField('float32') strength = 1000;
}Pitfall 2: Setting Objects Instead of Properties
陷阱2:直接替换对象而非修改属性
typescript
// 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;Why: Colyseus tracks property changes, not object replacement.
typescript
// 错误写法 - 会破坏引用关系
state.velocity = {x: 10, y: 0};
// 正确写法 - 更新属性
state.velocity.setValue({x: 10, y: 0});
// 或者:
state.velocity.x = 10;
state.velocity.y = 0;原因: Colyseus 跟踪的是属性变化,而非对象替换。
Pitfall 3: Forgetting await harness.waitForSync()
陷阱3:忘记使用await harness.waitForSync()
typescript
// 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); // PASSEStypescript
// 错误写法 - 客户端状态尚未更新
harness.shipManager.state.health = 50;
expect(harness.shipDriver.state.health).toBe(50); // 测试失败
// 正确写法 - 等待状态复制完成
harness.shipManager.state.health = 50;
await harness.waitForSync();
expect(harness.shipDriver.state.health).toBe(50); // 测试通过Pitfall 4: Using State in Client-Side Logic
陷阱4:在客户端逻辑中修改状态
typescript
// 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
}Why: Server is authoritative. Client is display only.
typescript
// 错误写法 - 客户端不应包含业务逻辑
if (ship.state.health < 50) {
ship.state.broken = true; // 不要从客户端修改状态
}
// 正确写法 - 发送命令,由服务器决定
if (ship.state.health < 50) {
room.send({type: 'checkBroken'}); // 服务器验证并执行
}原因: 服务器拥有权威控制权。客户端仅负责显示。
Pitfall 5: Float32 Precision in Tests
陷阱5:测试中忽略Float32精度问题
typescript
// WRONG - exact match fails
expect(ship.speed).toBe(123.456789);
// CORRECT - close enough
expect(ship.speed).toBeCloseTo(123.46, 1);typescript
// 错误写法 - 精确匹配会失败
expect(ship.speed).toBe(123.456789);
// 正确写法 - 允许一定误差
expect(ship.speed).toBeCloseTo(123.46, 1);Pitfall 6: Not Cleaning Up Test Harness
陷阱6:测试后未清理测试工具
typescript
// 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
});typescript
// 错误写法 - 连接未关闭
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// 测试代码
}); // 缺少cleanup()!
// 正确写法
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// 测试代码
await harness.cleanup(); // 清理连接
});Debugging Colyseus Issues
Colyseus 问题调试
1. Colyseus Monitor
1. Colyseus 监控面板
http://localhost:2567/colyseus-monitor
Login: admin / adminSee:
- Active rooms
- Connected clients
- State tree (live values)
http://localhost:2567/colyseus-monitor
登录账号:admin / admin可查看内容:
- 活跃房间
- 已连接客户端
- 状态树(实时数值)
2. Chrome DevTools Network Tab
2. Chrome 开发者工具 网络面板
WebSocket messages:
- Open DevTools (F12)
- Network tab → WS filter
- Click connection
- See Messages: commands sent, patches received
查看WebSocket消息:
- 打开开发者工具(F12)
- 网络面板 → 筛选WS
- 点击对应连接
- 查看消息:发送的命令、接收的补丁
3. State Logging
3. 状态日志
Server:
typescript
console.log('[SERVER] Shield strength:', this.state.shield.strength);Client:
typescript
console.log('[CLIENT] Shield strength:', ship.state.shield.strength);Compare values to find sync issues.
服务器端:
typescript
console.log('[SERVER] Shield strength:', this.state.shield.strength);客户端:
typescript
console.log('[CLIENT] Shield strength:', ship.state.shield.strength);对比两端数值,排查同步问题。
4. JSON Pointer Path Validation
4. JSON Pointer 路径验证
Test paths:
typescript
const path = '/Spaceship/ship-1/shield/power';
const obj = resolveJsonPointer(state, path);
console.log('Resolved:', obj); // Should not be undefined测试路径:
typescript
const path = '/Spaceship/ship-1/shield/power';
const obj = resolveJsonPointer(state, path);
console.log('Resolved:', obj); // 结果不应为undefinedPerformance Considerations
性能优化建议
Minimize Sync Frequency
减少同步频率
typescript
// 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;
}
}But: Starwards updates are already optimized. Don't prematurely optimize.
typescript
// 错误写法 - 每秒同步60次
update(dt: number) {
this.state.position.x += velocity.x * dt;
this.state.position.y += velocity.y * dt;
}
// 优化写法 - 仅当变化显著时同步
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 已经做了更新优化,无需过早优化。
Use Appropriate Types
使用合适的类型
typescript
// WRONG - wastes bandwidth
@gameField('float64') health = 100; // 8 bytes
// CORRECT - sufficient precision
@gameField('float32') health = 100; // 4 bytestypescript
// 错误写法 - 浪费带宽
@gameField('float64') health = 100; // 8字节
// 正确写法 - 精度足够
@gameField('float32') health = 100; // 4字节Batch Commands
批量发送命令
typescript
// 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
});But: Only if actually a bottleneck. JSON Pointer is fine for normal use.
typescript
// 错误写法 - 多次往返
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});
// 优化写法 - 单个批量命令
room.send('batchUpdate', {
'/reactor/power': 0.8,
'/thrusters/0/enabled': true,
'/thrusters/1/enabled': true
});注意: 仅当出现性能瓶颈时才需要这样做。普通场景下JSON Pointer足够好用。
Integration with Other Skills
与其他技能模块的集成
- starwards-tdd - Test state sync with harnesses
- starwards-debugging - Debug sync issues with tools
- starwards-verification - Verify multiplayer scenarios
- starwards-tdd - 使用测试工具包测试状态同步
- starwards-debugging - 使用调试工具排查同步问题
- starwards-verification - 验证多人游戏场景
Quick Reference
速查表
| Task | Pattern |
|---|---|
| Add synced property | |
| Nested Schema | |
| Array | |
| Map | |
| Send command (client) | |
| Listen to changes (client) | |
| Test sync | |
| Debug state | Colyseus Monitor (port 2567) |
| 任务 | 实现方式 |
|---|---|
| 添加同步属性 | |
| 嵌套Schema | |
| 数组 | |
| 映射 | |
| 客户端发送命令 | |
| 客户端监听状态变化 | |
| 测试状态同步 | |
| 调试状态 | Colyseus 监控面板(端口2567) |
The Bottom Line
核心要点总结
Remember:
- @gameField must be last decorator
- State is source of truth (server authoritative)
- Commands go client → server, patches go server → client
- Use harnesses for multiplayer tests
- Float32 has precision loss (use toBeCloseTo)
- Clean up test connections (await cleanup())
- When in doubt, check Colyseus Monitor
请牢记:
- @gameField 必须是最后一个装饰器
- 状态为唯一可信源(服务器拥有权威控制权)
- 命令从客户端发往服务器,补丁从服务器发往客户端
- 使用测试工具包进行多人游戏测试
- Float32 存在精度损失(测试时使用toBeCloseTo)
- 测试后清理连接(调用await cleanup())
- 遇到问题时,先查看Colyseus监控面板