starwards-colyseus

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Colyseus 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:
  1. Client sends command → Room
  2. Room modifies State
  3. Colyseus patches → Client
  4. Client state updates automatically
  5. UI reacts to state changes
Client (Browser)
  ↓ WebSocket connection
Room (Server)
  ↓ owns
State (Schema)
  ↓ syncs to
Client State (Mirror)
流程:
  1. 客户端发送命令 → 房间(Room)
  2. 房间修改状态(State)
  3. Colyseus 生成补丁 → 客户端
  4. 客户端状态自动更新
  5. UI 响应状态变化

The @gameField Decorator

@gameField 装饰器

Purpose: Marks properties for automatic Colyseus synchronization
Location:
modules/core/src/game-field.ts
Usage:
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:
  • 'float32'
    - 32-bit float (precision loss vs 64-bit number!)
  • 'float64'
    - 64-bit float
  • 'int8'
    ,
    'int16'
    ,
    'int32'
    - Signed integers
  • 'uint8'
    ,
    'uint16'
    ,
    'uint32'
    - Unsigned integers
  • 'boolean'
    - Boolean
  • 'string'
    - String
  • SchemaClass
    - Nested Schema
  • [SchemaClass]
    - Array of Schema
  • {map: SchemaClass}
    - Map of Schema
用途: 标记需要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>();
}
支持的类型:
  • 'float32'
    - 32位浮点数(与64位数字相比存在精度损失!)
  • 'float64'
    - 64位浮点数
  • 'int8'
    ,
    'int16'
    ,
    'int32'
    - 有符号整数
  • 'uint8'
    ,
    'uint16'
    ,
    'uint32'
    - 无符号整数
  • 'boolean'
    - 布尔值
  • 'string'
    - 字符串
  • SchemaClass
    - 嵌套Schema类
  • [SchemaClass]
    - Schema类数组
  • {map: SchemaClass}
    - Schema类映射

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
float32
is 32-bit
typescript
@gameField('float32') speed = 123.456789;
console.log(speed);  // 123.46 (precision lost!)
Solution: Use
toBeCloseTo()
in tests
typescript
// WRONG
expect(ship.speed).toBe(123.456789);

// CORRECT
expect(ship.speed).toBeCloseTo(123.46, 1);
问题: JavaScript 数字为64位,而Colyseus的
float32
是32位
typescript
@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 state
Use 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:
AdminState
(has SpaceState)
Clients: GM interface, admin panel
Commands:
startGame
,
stopGame
,
loadMap
用途: 游戏管理、地图选择、开始/停止游戏
状态:
AdminState
(包含SpaceState)
连接客户端: GM界面、管理面板
支持命令:
startGame
,
stopGame
,
loadMap

SpaceRoom

SpaceRoom(太空场景房间)

Purpose: Space-level gameplay, physics simulation
State:
SpaceState
(all space objects)
Clients: Tactical displays, overview screens
Managed by:
GameManager
,
SpaceManager
用途: 太空层级游戏玩法、物理模拟
状态:
SpaceState
(包含所有太空对象)
连接客户端: 战术显示器、概览屏幕
管理模块:
GameManager
,
SpaceManager

ShipRoom (per ship)

ShipRoom(单船房间,每个飞船对应一个)

Purpose: Individual ship control
State:
ShipState
(ship systems)
Clients: Ship stations (weapons, engineering, etc.)
roomId: Equals
shipId
(
ship-0
,
ship-1
, etc.)
Commands: JSON Pointer only (for flexibility)
用途: 单个飞船的控制
状态:
ShipState
(飞船系统数据)
连接客户端: 飞船操作台(武器、工程等)
roomId:
shipId
一致(如
ship-0
,
ship-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
ShipTestHarness
:
typescript
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
MultiClientDriver
:
typescript
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
docs/testing/UTILITIES.md
for full API.
使用
ShipTestHarness
typescript
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();
});
使用
MultiClientDriver
typescript
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.md

Common 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);  // PASSES
typescript
// 错误写法 - 客户端状态尚未更新
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 / admin
See:
  • Active rooms
  • Connected clients
  • State tree (live values)
http://localhost:2567/colyseus-monitor
登录账号:admin / admin
可查看内容:
  • 活跃房间
  • 已连接客户端
  • 状态树(实时数值)

2. Chrome DevTools Network Tab

2. Chrome 开发者工具 网络面板

WebSocket messages:
  1. Open DevTools (F12)
  2. Network tab → WS filter
  3. Click connection
  4. See Messages: commands sent, patches received
查看WebSocket消息:
  1. 打开开发者工具(F12)
  2. 网络面板 → 筛选WS
  3. 点击对应连接
  4. 查看消息:发送的命令、接收的补丁

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);  // 结果不应为undefined

Performance 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 bytes
typescript
// 错误写法 - 浪费带宽
@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

速查表

TaskPattern
Add synced property
@gameField('type') prop = value
Nested Schema
@gameField(Class) obj = new Class()
Array
@gameField([Class]) arr = new ArraySchema()
Map
@gameField({map: Class}) map = new MapSchema()
Send command (client)
room.send({type: '/path', value})
Listen to changes (client)
state.onChange(() => {})
Test sync
await harness.waitForSync()
Debug stateColyseus Monitor (port 2567)
任务实现方式
添加同步属性
@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)

The Bottom Line

核心要点总结

Remember:
  1. @gameField must be last decorator
  2. State is source of truth (server authoritative)
  3. Commands go client → server, patches go server → client
  4. Use harnesses for multiplayer tests
  5. Float32 has precision loss (use toBeCloseTo)
  6. Clean up test connections (await cleanup())
  7. When in doubt, check Colyseus Monitor
请牢记:
  1. @gameField 必须是最后一个装饰器
  2. 状态为唯一可信源(服务器拥有权威控制权)
  3. 命令从客户端发往服务器,补丁从服务器发往客户端
  4. 使用测试工具包进行多人游戏测试
  5. Float32 存在精度损失(测试时使用toBeCloseTo)
  6. 测试后清理连接(调用await cleanup())
  7. 遇到问题时,先查看Colyseus监控面板