状态同步
@NetworkEntity 装饰器
Section titled “@NetworkEntity 装饰器”@NetworkEntity 装饰器用于标记需要自动广播生成/销毁的组件。当包含此组件的实体被创建或销毁时,ECSRoom 会自动广播相应的消息给所有客户端。
import { Component, ECSComponent, sync, NetworkEntity } from '@esengine/ecs-framework';
@ECSComponent('Enemy')@NetworkEntity('Enemy')class EnemyComponent extends Component { @sync('float32') x: number = 0; @sync('float32') y: number = 0; @sync('uint16') health: number = 100;}当添加此组件到实体时,ECSRoom 会自动广播 spawn 消息:
// 服务端const entity = scene.createEntity('Enemy');entity.addComponent(new EnemyComponent()); // 自动广播 spawn
// 销毁时自动广播 despawnentity.destroy(); // 自动广播 despawn@NetworkEntity('Bullet', { autoSpawn: true, // 自动广播生成(默认 true) autoDespawn: false // 禁用自动广播销毁})class BulletComponent extends Component { }| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
autoSpawn | boolean | true | 添加组件时自动广播 spawn |
autoDespawn | boolean | true | 销毁实体时自动广播 despawn |
使用 @NetworkEntity 时,应在添加组件之前初始化数据:
// ✅ 正确:先初始化,再添加const comp = new PlayerComponent();comp.playerId = player.id;comp.x = 100;comp.y = 200;entity.addComponent(comp); // spawn 时数据已正确
// ❌ 错误:先添加,再初始化const comp = entity.addComponent(new PlayerComponent());comp.playerId = player.id; // spawn 时数据是默认值简化 GameRoom
Section titled “简化 GameRoom”使用 @NetworkEntity 后,GameRoom 变得更加简洁:
// 无需手动回调class GameRoom extends ECSRoom { private setupSystems(): void { // 敌人生成系统(自动广播 spawn) this.addSystem(new EnemySpawnSystem());
// 敌人 AI 系统 const enemyAI = new EnemyAISystem(); enemyAI.onDeath((enemy) => { enemy.destroy(); // 自动广播 despawn }); this.addSystem(enemyAI); }}ECSRoom 配置
Section titled “ECSRoom 配置”可以在 ECSRoom 中禁用自动网络实体功能:
class GameRoom extends ECSRoom { constructor() { super({ enableAutoNetworkEntity: false // 禁用自动广播 }); }}组件同步系统
Section titled “组件同步系统”基于 @sync 装饰器的 ECS 组件状态同步。
定义同步组件
Section titled “定义同步组件”import { Component, ECSComponent, sync } from '@esengine/ecs-framework';
@ECSComponent('Player')class PlayerComponent extends Component { @sync("string") name: string = ""; @sync("uint16") score: number = 0; @sync("float32") x: number = 0; @sync("float32") y: number = 0;
// 不带 @sync 的字段不会同步 localData: any;}import { ComponentSyncSystem } from '@esengine/network';
const syncSystem = new ComponentSyncSystem({}, true);scene.addSystem(syncSystem);
// 编码所有实体(首次连接)const fullData = syncSystem.encodeAllEntities(true);sendToClient(fullData);
// 编码增量(只发送变更)const deltaData = syncSystem.encodeDelta();if (deltaData) { broadcast(deltaData);}const syncSystem = new ComponentSyncSystem();scene.addSystem(syncSystem);
// 注册组件类型syncSystem.registerComponent(PlayerComponent);
// 监听同步事件syncSystem.addSyncListener((event) => { if (event.type === 'entitySpawned') { console.log('New entity:', event.entityId); }});
// 应用状态syncSystem.applySnapshot(data);| 类型 | 描述 | 字节数 |
|---|---|---|
"boolean" | 布尔值 | 1 |
"int8" / "uint8" | 8位整数 | 1 |
"int16" / "uint16" | 16位整数 | 2 |
"int32" / "uint32" | 32位整数 | 4 |
"float32" | 32位浮点 | 4 |
"float64" | 64位浮点 | 8 |
"string" | 字符串 | 变长 |
用于存储服务器状态快照并进行插值:
import { createSnapshotBuffer, type IStateSnapshot } from '@esengine/network';
const buffer = createSnapshotBuffer<IStateSnapshot>({ maxSnapshots: 30, // 最大快照数 interpolationDelay: 100 // 插值延迟 (ms)});
// 添加快照buffer.addSnapshot({ time: serverTime, entities: states});
// 获取插值状态const interpolated = buffer.getInterpolatedState(clientTime);| 属性 | 类型 | 描述 |
|---|---|---|
maxSnapshots | number | 缓冲区最大快照数 |
interpolationDelay | number | 插值延迟(毫秒) |
适用于简单的位置插值:
import { createTransformInterpolator } from '@esengine/network';
const interpolator = createTransformInterpolator();
// 添加状态interpolator.addState(time, { x: 0, y: 0, rotation: 0 });
// 获取插值结果const state = interpolator.getInterpolatedState(currentTime);Hermite 插值器
Section titled “Hermite 插值器”使用 Hermite 样条实现更平滑的插值,适合需要考虑速度的场景:
import { createHermiteTransformInterpolator } from '@esengine/network';
const interpolator = createHermiteTransformInterpolator({ bufferSize: 10});
// 添加带速度的状态interpolator.addState(time, { x: 100, y: 200, rotation: 0, vx: 5, vy: 0});
// 获取平滑的插值结果const state = interpolator.getInterpolatedState(currentTime);| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 线性插值 | 简单、计算快 | 可能不平滑 | 简单移动 |
| Hermite 插值 | 平滑、考虑速度 | 计算量较大 | 高速移动 |
实现客户端预测和服务器校正,减少输入延迟:
import { createClientPrediction } from '@esengine/network';
const prediction = createClientPrediction({ maxPredictedInputs: 60, reconciliationThreshold: 0.1});
// 预测输入const seq = prediction.predict(inputState, currentState, (state, input) => { // 应用输入到状态 return applyInput(state, input);});
// 服务器校正const corrected = prediction.reconcile( serverState, serverSeq, (state, input) => applyInput(state, input));| 属性 | 类型 | 描述 |
|---|---|---|
maxPredictedInputs | number | 最大预测输入数 |
reconciliationThreshold | number | 校正阈值 |
客户端 服务器 │ │ ├─ 1. 本地预测输入 ──────────────────► │ │ ├─ 2. 发送输入到服务器 │ │ │ │ ├─ 3. 处理输入 │ │ ◄──────────────────── 4. 返回权威状态 │ │ ├─ 5. 校正本地状态 │ │ │插值延迟设置
Section titled “插值延迟设置”- 低延迟网络(局域网):50-100ms
- 普通网络:100-150ms
- 高延迟网络:150-200ms
const buffer = createSnapshotBuffer({ interpolationDelay: 100 // 根据网络情况调整});对于本地玩家使用客户端预测:
// 本地玩家:预测 + 校正if (identity.bIsLocalPlayer) { const predicted = prediction.predict(input, state, applyInput); // 使用预测状态渲染}
// 远程玩家:纯插值if (!identity.bIsLocalPlayer) { const interpolated = interpolator.getInterpolatedState(time); // 使用插值状态渲染}-
合理设置插值延迟:太小会导致抖动,太大会增加延迟感
-
客户端预测仅用于本地玩家:远程玩家使用插值
-
校正阈值:根据游戏精度需求设置合适的阈值
-
快照数量:保持足够的快照以应对网络抖动