Loading...
Loading...
Compare original and translation side by side
Client (Browser)
↓ WebSocket connection
Room (Server)
↓ owns
State (Schema)
↓ syncs to
Client State (Mirror)Client (Browser)
↓ WebSocket connection
Room (Server)
↓ owns
State (Schema)
↓ syncs to
Client State (Mirror)modules/core/src/game-field.tsimport { 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''float64''int8''int16''int32''uint8''uint16''uint32''boolean''string'SchemaClass[SchemaClass]{map: SchemaClass}modules/core/src/game-field.tsimport { 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''float64''int8''int16''int32''uint8''uint16''uint32''boolean''string'SchemaClass[SchemaClass]{map: SchemaClass}// 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;// 正确顺序
@range([0, 1]) // 第1个
@tweakable('number') // 第2个
@gameField('float32') // 第3个 - 最后一个
power = 1.0;
// 错误顺序
@gameField('float32') // 不能放在第一个
@range([0, 1])
power = 1.0;// CORRECT
@gameField([Thruster])
thrusters = new ArraySchema<Thruster>();
@gameField({map: Spaceship})
ships = new MapSchema<Spaceship>();
// WRONG
@gameField([Thruster])
thrusters: ArraySchema<Thruster>; // Not initialized!// 正确写法
@gameField([Thruster])
thrusters = new ArraySchema<Thruster>();
@gameField({map: Spaceship})
ships = new MapSchema<Spaceship>();
// 错误写法
@gameField([Thruster])
thrusters: ArraySchema<Thruster>; // 未初始化!// 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)// 正确写法
@gameField('float32') speed = 0; // 32位浮点数
@gameField('int16') count = 0; // 16位整数
// 错误写法
@gameField('number') speed = 0; // 不存在'number'类型
@gameField('float') speed = 0; // 不存在'float'类型(请使用float32/float64)// 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;
}// 只同步客户端需要的状态
@gameField('float32') health = 100; // ✅ 客户端需要该数据
@gameField('float32') damage = 10; // ❌ 内部计算数据,无需同步
// 内部推导计算
get damage() {
return this.weapon.baseDamage * this.effectiveness;
}float32@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);float32@gameField('float32') speed = 123.456789;
console.log(speed); // 123.46(精度丢失!)toBeCloseTo()// 错误写法
expect(ship.speed).toBe(123.456789);
// 正确写法
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
}
}class ShipState extends Schema {
@gameField('float32') health = 100; // 已同步
@gameField(Reactor) reactor!: Reactor; // 已同步
}class ShipManager {
updateRate = 60; // 未同步
lastUpdate = Date.now(); // 未同步
update(dt: number) {
this.state.health -= 10 * dt; // 修改状态 → 会同步到客户端
}
}room.send({
type: '/Spaceship/ship-1/reactor/power',
value: 0.8
});// No code needed - JSON Pointer auto-applies to stateroom.send({
type: '/Spaceship/ship-1/reactor/power',
value: 0.8
});// 无需编写代码 - JSON Pointer 会自动应用到状态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!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); // 类型安全!AdminStatestartGamestopGameloadMapAdminStatestartGamestopGameloadMapSpaceStateGameManagerSpaceManagerSpaceStateGameManagerSpaceManagerShipStateshipIdship-0ship-1ShipStateshipIdship-0ship-1class 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);
});class ShieldManager {
update(dt: number) {
// 修改状态 → 自动同步
this.state.shield.strength += rechargeRate * dt;
}
}// 监听状态变化
ship.state.shield.onChange(() => {
updateUI(ship.state.shield.strength);
});// 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;
});// 用户调整功率滑块
powerSlider.on('change', (value) => {
room.send({type: '/Spaceship/ship-0/reactor/power', value});
});// 接收命令、验证、执行
this.onMessage((client, message) => {
const value = clamp(0, 1, message.value); // 验证
applyJsonPointer(this.state, message.type, value); // 应用
// 自动同步到所有客户端
});// UI 根据同步后的状态自动更新
ship.state.reactor.listen('power', (value) => {
powerSlider.value = value;
});ShipTestHarnessimport { 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();
});MultiClientDriverimport { 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();
});docs/testing/UTILITIES.mdShipTestHarnessimport { 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();
});MultiClientDriverimport { 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();
});docs/testing/UTILITIES.md// WRONG - doesn't sync
class Shield {
strength = 1000; // No decorator
}
// CORRECT - syncs
class Shield extends Schema {
@gameField('float32') strength = 1000;
}// 错误写法 - 不会同步
class Shield {
strength = 1000; // 没有装饰器
}
// 正确写法 - 会同步
class Shield extends Schema {
@gameField('float32') strength = 1000;
}// 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;// 错误写法 - 会破坏引用关系
state.velocity = {x: 10, y: 0};
// 正确写法 - 更新属性
state.velocity.setValue({x: 10, y: 0});
// 或者:
state.velocity.x = 10;
state.velocity.y = 0;// 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// 错误写法 - 客户端状态尚未更新
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); // 测试通过// 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
}// 错误写法 - 客户端不应包含业务逻辑
if (ship.state.health < 50) {
ship.state.broken = true; // 不要从客户端修改状态
}
// 正确写法 - 发送命令,由服务器决定
if (ship.state.health < 50) {
room.send({type: 'checkBroken'}); // 服务器验证并执行
}// WRONG - exact match fails
expect(ship.speed).toBe(123.456789);
// CORRECT - close enough
expect(ship.speed).toBeCloseTo(123.46, 1);// 错误写法 - 精确匹配会失败
expect(ship.speed).toBe(123.456789);
// 正确写法 - 允许一定误差
expect(ship.speed).toBeCloseTo(123.46, 1);// 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
});// 错误写法 - 连接未关闭
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// 测试代码
}); // 缺少cleanup()!
// 正确写法
test('something', async () => {
const harness = new ShipTestHarness();
await harness.connect();
// 测试代码
await harness.cleanup(); // 清理连接
});http://localhost:2567/colyseus-monitor
Login: admin / adminhttp://localhost:2567/colyseus-monitor
登录账号:admin / adminconsole.log('[SERVER] Shield strength:', this.state.shield.strength);console.log('[CLIENT] Shield strength:', ship.state.shield.strength);console.log('[SERVER] Shield strength:', this.state.shield.strength);console.log('[CLIENT] Shield strength:', ship.state.shield.strength);const path = '/Spaceship/ship-1/shield/power';
const obj = resolveJsonPointer(state, path);
console.log('Resolved:', obj); // Should not be undefinedconst path = '/Spaceship/ship-1/shield/power';
const obj = resolveJsonPointer(state, path);
console.log('Resolved:', obj); // 结果不应为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;
}
}// 错误写法 - 每秒同步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;
}
}// WRONG - wastes bandwidth
@gameField('float64') health = 100; // 8 bytes
// CORRECT - sufficient precision
@gameField('float32') health = 100; // 4 bytes// 错误写法 - 浪费带宽
@gameField('float64') health = 100; // 8字节
// 正确写法 - 精度足够
@gameField('float32') health = 100; // 4字节// 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
});// 错误写法 - 多次往返
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
});| 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) |