feat(ecs): ECS 网络状态同步系统 | add ECS network state synchronization (#390)

## @esengine/ecs-framework

新增 @sync 装饰器和二进制编解码器,支持基于 Component 的网络状态同步:

- `sync` 装饰器标记需要同步的字段
- `ChangeTracker` 组件变更追踪
- 二进制编解码器 (BinaryWriter/BinaryReader)
- `encodeSnapshot`/`decodeSnapshot` 批量编解码
- `encodeSpawn`/`decodeSpawn` 实体生成编解码
- `encodeDespawn`/`processDespawn` 实体销毁编解码

将以下方法标记为 @internal,用户应通过 Core.update() 驱动更新:
- Scene.update()
- SceneManager.update()
- WorldManager.updateAll()

## @esengine/network

- 新增 ComponentSyncSystem 基于 @sync 自动同步组件状态
- 将 ecs-framework 从 devDependencies 移到 peerDependencies

## @esengine/server

新增 ECSRoom,带有 ECS World 支持的房间基类:

- 每个 ECSRoom 在 Core.worldManager 中创建独立的 World
- Core.update() 统一更新 Time 和所有 World
- onTick() 只处理状态同步逻辑
- 自动创建/销毁玩家实体
- 增量状态广播
This commit is contained in:
YHH
2025-12-29 21:08:34 +08:00
committed by GitHub
parent 4cf868a769
commit 1f297ac769
32 changed files with 4620 additions and 23 deletions

View File

@@ -311,6 +311,93 @@ client.send('RoomMessage', {
})
```
## ECSRoom
`ECSRoom` is a room base class with ECS World support, suitable for games that need ECS architecture.
### Server Startup
```typescript
import { Core } from '@esengine/ecs-framework';
import { createServer } from '@esengine/server';
import { GameRoom } from './rooms/GameRoom.js';
// Initialize Core
Core.create();
// Global game loop
setInterval(() => Core.update(1/60), 16);
// Create server
const server = await createServer({ port: 3000 });
server.define('game', GameRoom);
await server.start();
```
### Define ECSRoom
```typescript
import { ECSRoom, Player } from '@esengine/server/ecs';
import { Component, ECSComponent, sync } from '@esengine/ecs-framework';
// Define sync component
@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;
}
// Define room
class GameRoom extends ECSRoom {
onCreate() {
this.addSystem(new MovementSystem());
}
onJoin(player: Player) {
const entity = this.createPlayerEntity(player.id);
const comp = entity.addComponent(new PlayerComponent());
comp.name = player.id;
}
}
```
### ECSRoom API
```typescript
abstract class ECSRoom<TState, TPlayerData> extends Room<TState, TPlayerData> {
protected readonly world: World; // ECS World
protected readonly scene: Scene; // Main scene
// Scene management
protected addSystem(system: EntitySystem): void;
protected createEntity(name?: string): Entity;
protected createPlayerEntity(playerId: string, name?: string): Entity;
protected getPlayerEntity(playerId: string): Entity | undefined;
protected destroyPlayerEntity(playerId: string): void;
// State sync
protected sendFullState(player: Player): void;
protected broadcastSpawn(entity: Entity, prefabType?: string): void;
protected broadcastDelta(): void;
}
```
### @sync Decorator
Mark component fields that need network synchronization:
| Type | Description | Bytes |
|------|-------------|-------|
| `"boolean"` | Boolean | 1 |
| `"int8"` / `"uint8"` | 8-bit integer | 1 |
| `"int16"` / `"uint16"` | 16-bit integer | 2 |
| `"int32"` / `"uint32"` | 32-bit integer | 4 |
| `"float32"` | 32-bit float | 4 |
| `"float64"` | 64-bit float | 8 |
| `"string"` | String | Variable |
## Best Practices
1. **Set Appropriate Tick Rate**

View File

@@ -1,8 +1,80 @@
---
title: "State Sync"
description: "Interpolation, prediction and snapshot buffers"
description: "Component sync, interpolation, prediction and snapshot buffers"
---
## Component Sync System
ECS component state synchronization based on `@sync` decorator.
### Define Sync Component
```typescript
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;
// Fields without @sync won't be synced
localData: any;
}
```
### Server-side Encoding
```typescript
import { ComponentSyncSystem } from '@esengine/network';
const syncSystem = new ComponentSyncSystem({}, true);
scene.addSystem(syncSystem);
// Encode all entities (initial connection)
const fullData = syncSystem.encodeAllEntities(true);
sendToClient(fullData);
// Encode delta (only send changes)
const deltaData = syncSystem.encodeDelta();
if (deltaData) {
broadcast(deltaData);
}
```
### Client-side Decoding
```typescript
const syncSystem = new ComponentSyncSystem();
scene.addSystem(syncSystem);
// Register component types
syncSystem.registerComponent(PlayerComponent);
// Listen for sync events
syncSystem.addSyncListener((event) => {
if (event.type === 'entitySpawned') {
console.log('New entity:', event.entityId);
}
});
// Apply state
syncSystem.applySnapshot(data);
```
### Sync Types
| Type | Description | Bytes |
|------|-------------|-------|
| `"boolean"` | Boolean | 1 |
| `"int8"` / `"uint8"` | 8-bit integer | 1 |
| `"int16"` / `"uint16"` | 16-bit integer | 2 |
| `"int32"` / `"uint32"` | 32-bit integer | 4 |
| `"float32"` | 32-bit float | 4 |
| `"float64"` | 64-bit float | 8 |
| `"string"` | String | Variable |
## Snapshot Buffer
Stores server state snapshots for interpolation:

View File

@@ -311,6 +311,93 @@ client.send('RoomMessage', {
})
```
## ECSRoom
`ECSRoom` 是带有 ECS World 支持的房间基类,适用于需要 ECS 架构的游戏。
### 服务端启动
```typescript
import { Core } from '@esengine/ecs-framework';
import { createServer } from '@esengine/server';
import { GameRoom } from './rooms/GameRoom.js';
// 初始化 Core
Core.create();
// 全局游戏循环
setInterval(() => Core.update(1/60), 16);
// 创建服务器
const server = await createServer({ port: 3000 });
server.define('game', GameRoom);
await server.start();
```
### 定义 ECSRoom
```typescript
import { ECSRoom, Player } from '@esengine/server/ecs';
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;
}
// 定义房间
class GameRoom extends ECSRoom {
onCreate() {
this.addSystem(new MovementSystem());
}
onJoin(player: Player) {
const entity = this.createPlayerEntity(player.id);
const comp = entity.addComponent(new PlayerComponent());
comp.name = player.id;
}
}
```
### ECSRoom API
```typescript
abstract class ECSRoom<TState, TPlayerData> extends Room<TState, TPlayerData> {
protected readonly world: World; // ECS World
protected readonly scene: Scene; // 主场景
// 场景管理
protected addSystem(system: EntitySystem): void;
protected createEntity(name?: string): Entity;
protected createPlayerEntity(playerId: string, name?: string): Entity;
protected getPlayerEntity(playerId: string): Entity | undefined;
protected destroyPlayerEntity(playerId: string): void;
// 状态同步
protected sendFullState(player: Player): void;
protected broadcastSpawn(entity: Entity, prefabType?: string): void;
protected broadcastDelta(): void;
}
```
### @sync 装饰器
标记需要网络同步的组件字段:
| 类型 | 描述 | 字节数 |
|------|------|--------|
| `"boolean"` | 布尔值 | 1 |
| `"int8"` / `"uint8"` | 8位整数 | 1 |
| `"int16"` / `"uint16"` | 16位整数 | 2 |
| `"int32"` / `"uint32"` | 32位整数 | 4 |
| `"float32"` | 32位浮点 | 4 |
| `"float64"` | 64位浮点 | 8 |
| `"string"` | 字符串 | 变长 |
## 最佳实践
1. **合理设置 Tick 频率**

View File

@@ -1,8 +1,80 @@
---
title: "状态同步"
description: "插值、预测和快照缓冲区"
description: "组件同步、插值、预测和快照缓冲区"
---
## 组件同步系统
基于 `@sync` 装饰器的 ECS 组件状态同步。
### 定义同步组件
```typescript
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;
}
```
### 服务端编码
```typescript
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);
}
```
### 客户端解码
```typescript
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"` | 字符串 | 变长 |
## 快照缓冲区
用于存储服务器状态快照并进行插值: