mcpField
S
starwards-colyseus

starwards-colyseus

github.com/starwards开发4447 次安装2 天前更新
代码仓库

关于 starwards-colyseus

这是面向 Starwards 开发者的 Colyseus 多人游戏同步实践 skill,适用于维护房间、状态和客户端命令的开发工作。它说明状态同步规则、常见陷阱及测试和调试方法。

主要功能

  1. 01使用 @gameField 定义同步状态
  2. 02组织房间与客户端命令
  3. 03处理嵌套集合和状态更新
  4. 04说明类型精度及同步注意事项
  5. 05提供同步测试与调试流程

适用场景

  1. 01新增或修改多人游戏同步状态
  2. 02排查客户端状态未更新的问题
  3. 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)

流程:

  1. 客户端发送命令 → 房间
  2. 房间修改状态
  3. Colyseus 推送补丁 → 客户端
  4. 客户端状态自动更新
  5. 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 消息:

  1. 打开 DevTools(F12)
  2. 选择 Network 面板 → WS 筛选器
  3. 点击连接
  4. 查看 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)

总结

请记住:

  1. @gameField 必须是最后一个装饰器
  2. 状态是事实来源(服务端具有权威性)
  3. 命令从客户端发送到服务端,补丁从服务端发送到客户端
  4. 使用测试工具进行多人游戏测试
  5. Float32 存在精度损失(使用 toBeCloseTo)
  6. 清理测试连接(await cleanup())
  7. 如有疑问,请查看 Colyseus 监视器

评论

支持 Markdown