Compare commits
12 Commits
@esengine/
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc7d1f726 | ||
|
|
04b08f3f07 | ||
|
|
d9969d0b08 | ||
|
|
bdbbf8a80a | ||
|
|
1368473c71 | ||
|
|
b28169b186 | ||
|
|
e2598b2292 | ||
|
|
2e3889abed | ||
|
|
d21caa974e | ||
|
|
a08a84b7db | ||
|
|
449bd420a6 | ||
|
|
1f297ac769 |
@@ -228,6 +228,7 @@ If you want a complete engine solution with rendering:
|
|||||||
A visual editor built with Tauri for scene management:
|
A visual editor built with Tauri for scene management:
|
||||||
|
|
||||||
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
- Download from [Releases](https://github.com/esengine/esengine/releases)
|
||||||
|
- [Build from source](./packages/editor/editor-app/README.md)
|
||||||
- Supports behavior tree editing, tilemap painting, visual scripting
|
- Supports behavior tree editing, tilemap painting, visual scripting
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -281,6 +282,7 @@ pnpm test
|
|||||||
|
|
||||||
- [ECS Framework Guide](./packages/framework/core/README.md)
|
- [ECS Framework Guide](./packages/framework/core/README.md)
|
||||||
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
|
||||||
|
- [Editor Setup Guide](./packages/editor/editor-app/README.md) ([中文](./packages/editor/editor-app/README_CN.md))
|
||||||
- [API Reference](https://esengine.cn/api/README)
|
- [API Reference](https://esengine.cn/api/README)
|
||||||
|
|
||||||
## Community
|
## Community
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ npm install @esengine/world-streaming # 世界流送
|
|||||||
基于 Tauri 构建的可视化编辑器:
|
基于 Tauri 构建的可视化编辑器:
|
||||||
|
|
||||||
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
|
||||||
|
- [从源码构建](./packages/editor/editor-app/README.md)
|
||||||
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
- 支持行为树编辑、Tilemap 绘制、可视化脚本
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
@@ -281,6 +282,7 @@ pnpm test
|
|||||||
|
|
||||||
- [ECS 框架指南](./packages/framework/core/README.md)
|
- [ECS 框架指南](./packages/framework/core/README.md)
|
||||||
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
- [行为树指南](./packages/framework/behavior-tree/README.md)
|
||||||
|
- [编辑器启动指南](./packages/editor/editor-app/README_CN.md) ([English](./packages/editor/editor-app/README.md))
|
||||||
- [API 参考](https://esengine.cn/api/README)
|
- [API 参考](https://esengine.cn/api/README)
|
||||||
|
|
||||||
## 社区
|
## 社区
|
||||||
|
|||||||
@@ -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
|
## Best Practices
|
||||||
|
|
||||||
1. **Set Appropriate Tick Rate**
|
1. **Set Appropriate Tick Rate**
|
||||||
|
|||||||
@@ -1,8 +1,176 @@
|
|||||||
---
|
---
|
||||||
title: "State Sync"
|
title: "State Sync"
|
||||||
description: "Interpolation, prediction and snapshot buffers"
|
description: "Component sync, interpolation, prediction and snapshot buffers"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## @NetworkEntity Decorator
|
||||||
|
|
||||||
|
The `@NetworkEntity` decorator marks components for automatic spawn/despawn broadcasting. When an entity containing this component is created or destroyed, ECSRoom automatically broadcasts the corresponding message to all clients.
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When adding this component to an entity, ECSRoom automatically broadcasts the spawn message:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server-side
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
entity.addComponent(new EnemyComponent()); // Auto-broadcasts spawn
|
||||||
|
|
||||||
|
// Destroying auto-broadcasts despawn
|
||||||
|
entity.destroy(); // Auto-broadcasts despawn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@NetworkEntity('Bullet', {
|
||||||
|
autoSpawn: true, // Auto-broadcast spawn (default true)
|
||||||
|
autoDespawn: false // Disable auto-broadcast despawn
|
||||||
|
})
|
||||||
|
class BulletComponent extends Component { }
|
||||||
|
```
|
||||||
|
|
||||||
|
| Option | Type | Default | Description |
|
||||||
|
|--------|------|---------|-------------|
|
||||||
|
| `autoSpawn` | `boolean` | `true` | Auto-broadcast spawn when component is added |
|
||||||
|
| `autoDespawn` | `boolean` | `true` | Auto-broadcast despawn when entity is destroyed |
|
||||||
|
|
||||||
|
### Initialization Order
|
||||||
|
|
||||||
|
When using `@NetworkEntity`, initialize data **before** adding the component:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ Correct: Initialize first, then add
|
||||||
|
const comp = new PlayerComponent();
|
||||||
|
comp.playerId = player.id;
|
||||||
|
comp.x = 100;
|
||||||
|
comp.y = 200;
|
||||||
|
entity.addComponent(comp); // Data is correct at spawn
|
||||||
|
|
||||||
|
// ❌ Wrong: Add first, then initialize
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
comp.playerId = player.id; // Data has default values at spawn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Simplified GameRoom
|
||||||
|
|
||||||
|
With `@NetworkEntity`, GameRoom becomes much cleaner:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// No manual callbacks needed
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
private setupSystems(): void {
|
||||||
|
// Enemy spawn system (auto-broadcasts spawn)
|
||||||
|
this.addSystem(new EnemySpawnSystem());
|
||||||
|
|
||||||
|
// Enemy AI system
|
||||||
|
const enemyAI = new EnemyAISystem();
|
||||||
|
enemyAI.onDeath((enemy) => {
|
||||||
|
enemy.destroy(); // Auto-broadcasts despawn
|
||||||
|
});
|
||||||
|
this.addSystem(enemyAI);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ECSRoom Configuration
|
||||||
|
|
||||||
|
You can disable the auto network entity feature in ECSRoom:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
enableAutoNetworkEntity: false // Disable auto-broadcasting
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
## Snapshot Buffer
|
||||||
|
|
||||||
Stores server state snapshots for interpolation:
|
Stores server state snapshots for interpolation:
|
||||||
|
|||||||
@@ -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 频率**
|
1. **合理设置 Tick 频率**
|
||||||
|
|||||||
@@ -1,8 +1,176 @@
|
|||||||
---
|
---
|
||||||
title: "状态同步"
|
title: "状态同步"
|
||||||
description: "插值、预测和快照缓冲区"
|
description: "组件同步、插值、预测和快照缓冲区"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## @NetworkEntity 装饰器
|
||||||
|
|
||||||
|
`@NetworkEntity` 装饰器用于标记需要自动广播生成/销毁的组件。当包含此组件的实体被创建或销毁时,ECSRoom 会自动广播相应的消息给所有客户端。
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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 消息:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 服务端
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
entity.addComponent(new EnemyComponent()); // 自动广播 spawn
|
||||||
|
|
||||||
|
// 销毁时自动广播 despawn
|
||||||
|
entity.destroy(); // 自动广播 despawn
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置选项
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@NetworkEntity('Bullet', {
|
||||||
|
autoSpawn: true, // 自动广播生成(默认 true)
|
||||||
|
autoDespawn: false // 禁用自动广播销毁
|
||||||
|
})
|
||||||
|
class BulletComponent extends Component { }
|
||||||
|
```
|
||||||
|
|
||||||
|
| 选项 | 类型 | 默认值 | 描述 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `autoSpawn` | `boolean` | `true` | 添加组件时自动广播 spawn |
|
||||||
|
| `autoDespawn` | `boolean` | `true` | 销毁实体时自动广播 despawn |
|
||||||
|
|
||||||
|
### 初始化顺序
|
||||||
|
|
||||||
|
使用 `@NetworkEntity` 时,应在添加组件**之前**初始化数据:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ 正确:先初始化,再添加
|
||||||
|
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
|
||||||
|
|
||||||
|
使用 `@NetworkEntity` 后,GameRoom 变得更加简洁:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 无需手动回调
|
||||||
|
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 配置
|
||||||
|
|
||||||
|
可以在 ECSRoom 中禁用自动网络实体功能:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
enableAutoNetworkEntity: false // 禁用自动广播
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 组件同步系统
|
||||||
|
|
||||||
|
基于 `@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"` | 字符串 | 变长 |
|
||||||
|
|
||||||
## 快照缓冲区
|
## 快照缓冲区
|
||||||
|
|
||||||
用于存储服务器状态快照并进行插值:
|
用于存储服务器状态快照并进行插值:
|
||||||
|
|||||||
86
packages/editor/editor-app/README.md
Normal file
86
packages/editor/editor-app/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# ESEngine Editor
|
||||||
|
|
||||||
|
A cross-platform desktop visual editor built with Tauri 2.x + React 18.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before running the editor, ensure you have the following installed:
|
||||||
|
|
||||||
|
- **Node.js** >= 18.x
|
||||||
|
- **pnpm** >= 10.x
|
||||||
|
- **Rust** >= 1.70 (for Tauri)
|
||||||
|
- **Platform-specific dependencies**:
|
||||||
|
- **Windows**: Microsoft Visual Studio C++ Build Tools
|
||||||
|
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
- **Linux**: See [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Clone and Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/esengine/esengine.git
|
||||||
|
cd esengine
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build Dependencies
|
||||||
|
|
||||||
|
From the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run Editor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor/editor-app
|
||||||
|
pnpm tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Scripts
|
||||||
|
|
||||||
|
| Script | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `pnpm tauri:dev` | Run editor in development mode with hot-reload |
|
||||||
|
| `pnpm tauri:build` | Build production application |
|
||||||
|
| `pnpm build:sdk` | Build editor-runtime SDK |
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
editor-app/
|
||||||
|
├── src/ # React application source
|
||||||
|
│ ├── components/ # UI components
|
||||||
|
│ ├── panels/ # Editor panels
|
||||||
|
│ └── services/ # Core services
|
||||||
|
├── src-tauri/ # Tauri (Rust) backend
|
||||||
|
├── public/ # Static assets
|
||||||
|
└── scripts/ # Build scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm clean
|
||||||
|
pnpm install
|
||||||
|
pnpm build:editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust/Tauri Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup update
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [ESEngine Documentation](https://esengine.cn/)
|
||||||
|
- [Tauri Documentation](https://tauri.app/)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
86
packages/editor/editor-app/README_CN.md
Normal file
86
packages/editor/editor-app/README_CN.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# ESEngine 编辑器
|
||||||
|
|
||||||
|
基于 Tauri 2.x + React 18 构建的跨平台桌面可视化编辑器。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
运行编辑器前,请确保已安装以下环境:
|
||||||
|
|
||||||
|
- **Node.js** >= 18.x
|
||||||
|
- **pnpm** >= 10.x
|
||||||
|
- **Rust** >= 1.70 (Tauri 需要)
|
||||||
|
- **平台相关依赖**:
|
||||||
|
- **Windows**: Microsoft Visual Studio C++ Build Tools
|
||||||
|
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
- **Linux**: 参考 [Tauri 环境配置](https://tauri.app/v1/guides/getting-started/prerequisites)
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 克隆并安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/esengine/esengine.git
|
||||||
|
cd esengine
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 构建依赖
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build:editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 启动编辑器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/editor/editor-app
|
||||||
|
pnpm tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 可用脚本
|
||||||
|
|
||||||
|
| 脚本 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `pnpm tauri:dev` | 开发模式运行编辑器(支持热重载)|
|
||||||
|
| `pnpm tauri:build` | 构建生产版本应用 |
|
||||||
|
| `pnpm build:sdk` | 构建 editor-runtime SDK |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
editor-app/
|
||||||
|
├── src/ # React 应用源码
|
||||||
|
│ ├── components/ # UI 组件
|
||||||
|
│ ├── panels/ # 编辑器面板
|
||||||
|
│ └── services/ # 核心服务
|
||||||
|
├── src-tauri/ # Tauri (Rust) 后端
|
||||||
|
├── public/ # 静态资源
|
||||||
|
└── scripts/ # 构建脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 构建错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm clean
|
||||||
|
pnpm install
|
||||||
|
pnpm build:editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust/Tauri 错误
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rustup update
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [ESEngine 文档](https://esengine.cn/)
|
||||||
|
- [Tauri 文档](https://tauri.app/)
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT License
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"build": "npm run build:sdk && tsc && vite build",
|
"build": "npm run build:sdk && tsc && vite build",
|
||||||
"build:watch": "vite build --watch",
|
"build:watch": "vite build --watch",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"copy-modules": "node ../../scripts/copy-engine-modules.mjs",
|
"copy-modules": "node ../../../scripts/copy-engine-modules.mjs",
|
||||||
"tauri:dev": "npm run build:sdk && npm run copy-modules && tauri dev",
|
"tauri:dev": "npm run build:sdk && npm run copy-modules && tauri dev",
|
||||||
"bundle:runtime": "node scripts/bundle-runtime.mjs",
|
"bundle:runtime": "node scripts/bundle-runtime.mjs",
|
||||||
"tauri:build": "npm run build:sdk && npm run copy-modules && npm run bundle:runtime && tauri build",
|
"tauri:build": "npm run build:sdk && npm run copy-modules && npm run bundle:runtime && tauri build",
|
||||||
|
|||||||
1010
packages/editor/editor-app/src-tauri/Cargo.lock
generated
1010
packages/editor/editor-app/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -10,16 +10,16 @@ name = "ecs_editor_lib"
|
|||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "2.0", features = [] }
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.0", features = ["protocol-asset"] }
|
tauri = { version = "2", features = ["protocol-asset"] }
|
||||||
tauri-plugin-shell = "2.0"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-dialog = "2.0"
|
tauri-plugin-dialog = "2"
|
||||||
tauri-plugin-fs = "2.0"
|
tauri-plugin-fs = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-http = "2.0"
|
tauri-plugin-http = "2"
|
||||||
tauri-plugin-cli = "2.0"
|
tauri-plugin-cli = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
glob = "0.3"
|
glob = "0.3"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@esengine/engine-core": "workspace:*",
|
"@esengine/engine-core": "workspace:*",
|
||||||
|
"@esengine/asset-system": "workspace:*",
|
||||||
"@esengine/editor-core": "workspace:*",
|
"@esengine/editor-core": "workspace:*",
|
||||||
"@esengine/editor-runtime": "workspace:*",
|
"@esengine/editor-runtime": "workspace:*",
|
||||||
"@esengine/node-editor": "workspace:*",
|
"@esengine/node-editor": "workspace:*",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* @zh ESEngine 行为树运行时模块
|
||||||
|
* @en ESEngine Behavior Tree Runtime Module
|
||||||
|
*
|
||||||
|
* @zh 纯运行时模块,不依赖 asset-system。资产加载由编辑器在 install 时注册。
|
||||||
|
* @en Pure runtime module, no asset-system dependency. Asset loading is registered by editor during install.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
|
||||||
|
import type { IRuntimeModule, SystemContext } from '@esengine/engine-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BehaviorTreeRuntimeComponent,
|
||||||
|
BehaviorTreeExecutionSystem,
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
GlobalBlackboardService,
|
||||||
|
BehaviorTreeSystemToken
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
export class BehaviorTreeRuntimeModule implements IRuntimeModule {
|
||||||
|
registerComponents(registry: IComponentRegistry): void {
|
||||||
|
registry.register(BehaviorTreeRuntimeComponent);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerServices(services: ServiceContainer): void {
|
||||||
|
if (!services.isRegistered(GlobalBlackboardService)) {
|
||||||
|
services.registerSingleton(GlobalBlackboardService);
|
||||||
|
}
|
||||||
|
if (!services.isRegistered(BehaviorTreeAssetManager)) {
|
||||||
|
services.registerSingleton(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSystems(scene: IScene, context: SystemContext): void {
|
||||||
|
const ecsServices = (context as { ecsServices?: ServiceContainer }).ecsServices;
|
||||||
|
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(ecsServices);
|
||||||
|
|
||||||
|
if (context.isEditor) {
|
||||||
|
behaviorTreeSystem.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.addSystem(behaviorTreeSystem);
|
||||||
|
context.services.register(BehaviorTreeSystemToken, behaviorTreeSystem);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,8 +30,11 @@ import {
|
|||||||
LocaleService,
|
LocaleService,
|
||||||
} from '@esengine/editor-runtime';
|
} from '@esengine/editor-runtime';
|
||||||
|
|
||||||
// Runtime imports from @esengine/behavior-tree package
|
// Runtime imports
|
||||||
import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengine/behavior-tree';
|
import { BehaviorTreeRuntimeComponent, BehaviorTreeAssetType } from '@esengine/behavior-tree';
|
||||||
|
import { AssetManagerToken } from '@esengine/asset-system';
|
||||||
|
import { BehaviorTreeRuntimeModule } from './BehaviorTreeRuntimeModule';
|
||||||
|
import { BehaviorTreeLoader } from './runtime/BehaviorTreeLoader';
|
||||||
|
|
||||||
// Editor components and services
|
// Editor components and services
|
||||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
||||||
@@ -71,6 +74,10 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
|||||||
// 设置插件上下文
|
// 设置插件上下文
|
||||||
PluginContext.setServices(services);
|
PluginContext.setServices(services);
|
||||||
|
|
||||||
|
// 注册行为树资产加载器到 AssetManager
|
||||||
|
// Register behavior tree asset loader to AssetManager
|
||||||
|
this.registerAssetLoader();
|
||||||
|
|
||||||
// 注册服务
|
// 注册服务
|
||||||
this.registerServices(services);
|
this.registerServices(services);
|
||||||
|
|
||||||
@@ -92,6 +99,22 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
|||||||
logger.info('BehaviorTree editor module installed');
|
logger.info('BehaviorTree editor module installed');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册行为树资产加载器
|
||||||
|
* Register behavior tree asset loader
|
||||||
|
*/
|
||||||
|
private registerAssetLoader(): void {
|
||||||
|
try {
|
||||||
|
const assetManager = PluginAPI.resolve(AssetManagerToken);
|
||||||
|
if (assetManager) {
|
||||||
|
assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader());
|
||||||
|
logger.info('BehaviorTree asset loader registered');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Failed to register asset loader:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private registerAssetCreationMappings(services: ServiceContainer): void {
|
private registerAssetCreationMappings(services: ServiceContainer): void {
|
||||||
try {
|
try {
|
||||||
const fileActionRegistry = services.resolve<FileActionRegistry>(IFileActionRegistry);
|
const fileActionRegistry = services.resolve<FileActionRegistry>(IFileActionRegistry);
|
||||||
@@ -376,7 +399,7 @@ export const BehaviorTreePlugin: IEditorPlugin = {
|
|||||||
editorModule: new BehaviorTreeEditorModule(),
|
editorModule: new BehaviorTreeEditorModule(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export { BehaviorTreeRuntimeModule };
|
// BehaviorTreeRuntimeModule is internal, not re-exported
|
||||||
|
|
||||||
// Re-exports for editor functionality
|
// Re-exports for editor functionality
|
||||||
export { PluginContext } from './PluginContext';
|
export { PluginContext } from './PluginContext';
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @zh ESEngine 资产加载器
|
||||||
|
* @en ESEngine asset loader
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Core } from '@esengine/ecs-framework';
|
||||||
|
import {
|
||||||
|
BehaviorTreeAssetManager,
|
||||||
|
EditorToBehaviorTreeDataConverter,
|
||||||
|
BehaviorTreeAssetType,
|
||||||
|
type BehaviorTreeData
|
||||||
|
} from '@esengine/behavior-tree';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 行为树资产接口
|
||||||
|
* @en Behavior tree asset interface
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export interface IBehaviorTreeAsset {
|
||||||
|
data: BehaviorTreeData;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 行为树加载器
|
||||||
|
* @en Behavior tree loader implementing IAssetLoader interface
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
export class BehaviorTreeLoader {
|
||||||
|
readonly supportedType = BehaviorTreeAssetType;
|
||||||
|
readonly supportedExtensions = ['.btree'];
|
||||||
|
readonly contentType = 'text' as const;
|
||||||
|
|
||||||
|
async parse(content: { text?: string }, context: { metadata: { path: string } }): Promise<IBehaviorTreeAsset> {
|
||||||
|
if (!content.text) {
|
||||||
|
throw new Error('Behavior tree content is empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content.text);
|
||||||
|
const assetPath = context.metadata.path;
|
||||||
|
treeData.id = assetPath;
|
||||||
|
|
||||||
|
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||||
|
if (btAssetManager) {
|
||||||
|
btAssetManager.loadAsset(treeData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: treeData,
|
||||||
|
path: assetPath
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(asset: IBehaviorTreeAsset): void {
|
||||||
|
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
|
||||||
|
if (btAssetManager && asset.data) {
|
||||||
|
btAssetManager.unloadAsset(asset.data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"composite": false,
|
||||||
"module": "ES2020",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"lib": ["ES2020", "DOM"],
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"resolveJsonModule": true,
|
"skipLibCheck": true,
|
||||||
"experimentalDecorators": true,
|
"moduleResolution": "bundler"
|
||||||
"emitDecoratorMetadata": true
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { defineConfig } from 'tsup';
|
|||||||
import { editorOnlyPreset } from '../../../tools/build-config/src/presets/plugin-tsup';
|
import { editorOnlyPreset } from '../../../tools/build-config/src/presets/plugin-tsup';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
...editorOnlyPreset(),
|
...editorOnlyPreset({
|
||||||
|
external: ['@esengine/asset-system']
|
||||||
|
}),
|
||||||
tsconfig: 'tsconfig.build.json'
|
tsconfig: 'tsconfig.build.json'
|
||||||
});
|
});
|
||||||
|
|||||||
13
packages/editor/plugins/fairygui-editor/tsconfig.build.json
Normal file
13
packages/editor/plugins/fairygui-editor/tsconfig.build.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../build-config/tsconfig.json",
|
"extends": "../../../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export default defineConfig({
|
|||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
dts: true,
|
dts: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
|
tsconfig: 'tsconfig.build.json',
|
||||||
external: [
|
external: [
|
||||||
'react',
|
'react',
|
||||||
'react-dom',
|
'react-dom',
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
# @esengine/behavior-tree
|
# @esengine/behavior-tree
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/behavior-tree",
|
"name": "@esengine/behavior-tree",
|
||||||
"version": "1.0.3",
|
"version": "3.0.1",
|
||||||
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
# @esengine/blueprint
|
# @esengine/blueprint
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
|
||||||
## 1.0.2
|
## 1.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/blueprint",
|
"name": "@esengine/blueprint",
|
||||||
"version": "1.0.2",
|
"version": "3.0.1",
|
||||||
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.js",
|
"module": "dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,169 @@
|
|||||||
# @esengine/ecs-framework
|
# @esengine/ecs-framework
|
||||||
|
|
||||||
|
## 2.6.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#396](https://github.com/esengine/esengine/pull/396) [`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e) Thanks [@esengine](https://github.com/esengine)! - fix(ecs): COMPONENT_ADDED 事件添加 entity 字段
|
||||||
|
|
||||||
|
修复 `ECSEventType.COMPONENT_ADDED` 事件缺少 `entity` 字段的问题,导致 ECSRoom 的 `@NetworkEntity` 自动广播功能报错。
|
||||||
|
|
||||||
|
## 2.6.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- feat(ecs): 添加 @NetworkEntity 装饰器,支持自动广播实体生成/销毁
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
**@NetworkEntity 装饰器**
|
||||||
|
- 标记组件为网络实体,自动广播 spawn/despawn 消息
|
||||||
|
- 支持 `autoSpawn` 和 `autoDespawn` 配置选项
|
||||||
|
- 通过事件系统(`ECSEventType.COMPONENT_ADDED` / `ECSEventType.ENTITY_DESTROYED`)实现
|
||||||
|
|
||||||
|
**ECSRoom 增强**
|
||||||
|
- 新增 `enableAutoNetworkEntity` 配置选项(默认启用)
|
||||||
|
- 自动监听组件添加和实体销毁事件
|
||||||
|
- 简化 GameRoom 实现,无需手动回调
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
**Entity 事件**
|
||||||
|
- `Entity.destroy()` 现在发出 `entity:destroyed` 事件
|
||||||
|
- `Entity.active` 变化时发出 `entity:enabled` / `entity:disabled` 事件
|
||||||
|
- 使用 `ECSEventType` 常量替代硬编码字符串
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务端
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
entity.addComponent(new EnemyComponent()); // 自动广播 spawn
|
||||||
|
entity.destroy(); // 自动广播 despawn
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.5.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- [#392](https://github.com/esengine/esengine/pull/392) [`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76) Thanks [@esengine](https://github.com/esengine)! - fix(sync): Decoder 现在使用 GlobalComponentRegistry 查找组件 | Decoder now uses GlobalComponentRegistry for component lookup
|
||||||
|
|
||||||
|
**问题 | Problem:**
|
||||||
|
1. `Decoder.ts` 有自己独立的 `componentRegistry` Map,与 `GlobalComponentRegistry` 完全分离。这导致通过 `@ECSComponent` 装饰器注册的组件在网络反序列化时找不到,产生 "Unknown component type" 错误。
|
||||||
|
2. `@sync` 装饰器使用 `constructor.name` 作为 `typeId`,而不是 `@ECSComponent` 装饰器指定的名称,导致编码和解码使用不同的类型 ID。
|
||||||
|
3. `Decoder.ts` had its own local `componentRegistry` Map that was completely separate from `GlobalComponentRegistry`. This caused components registered via `@ECSComponent` decorator to not be found during network deserialization, resulting in "Unknown component type" errors.
|
||||||
|
4. `@sync` decorator used `constructor.name` as `typeId` instead of the name specified by `@ECSComponent` decorator, causing encoding and decoding to use different type IDs.
|
||||||
|
|
||||||
|
**修改 | Changes:**
|
||||||
|
- 从 Decoder.ts 中移除本地 `componentRegistry`
|
||||||
|
- 更新 `decodeEntity` 和 `decodeSpawn` 使用 `GlobalComponentRegistry.getComponentType()`
|
||||||
|
- 移除已废弃的 `registerSyncComponent` 和 `autoRegisterSyncComponent` 函数
|
||||||
|
- 更新 `@sync` 装饰器使用 `getComponentTypeName()` 获取组件类型名称
|
||||||
|
- 更新 `@ECSComponent` 装饰器同步更新 `SYNC_METADATA.typeId`
|
||||||
|
- Removed local `componentRegistry` from Decoder.ts
|
||||||
|
- Updated `decodeEntity` and `decodeSpawn` to use `GlobalComponentRegistry.getComponentType()`
|
||||||
|
- Removed deprecated `registerSyncComponent` and `autoRegisterSyncComponent` functions
|
||||||
|
- Updated `@sync` decorator to use `getComponentTypeName()` for component type name
|
||||||
|
- Updated `@ECSComponent` decorator to sync update `SYNC_METADATA.typeId`
|
||||||
|
|
||||||
|
现在使用 `@ECSComponent` 装饰器的组件会自动可用于网络同步解码,无需手动注册。
|
||||||
|
|
||||||
|
Now `@ECSComponent` decorated components are automatically available for network sync decoding without any manual registration.
|
||||||
|
|
||||||
|
## 2.5.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#390](https://github.com/esengine/esengine/pull/390) [`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256) Thanks [@esengine](https://github.com/esengine)! - feat: ECS 网络状态同步系统
|
||||||
|
|
||||||
|
## @esengine/ecs-framework
|
||||||
|
|
||||||
|
新增 `@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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新增导出
|
||||||
|
- `sync` - 标记需要同步的字段装饰器
|
||||||
|
- `SyncType` - 支持的同步类型
|
||||||
|
- `SyncOperation` - 同步操作类型(FULL/DELTA/SPAWN/DESPAWN)
|
||||||
|
- `encodeSnapshot` / `decodeSnapshot` - 批量编解码
|
||||||
|
- `encodeSpawn` / `decodeSpawn` - 实体生成编解码
|
||||||
|
- `encodeDespawn` / `processDespawn` - 实体销毁编解码
|
||||||
|
- `ChangeTracker` - 字段级变更追踪
|
||||||
|
- `initChangeTracker` / `clearChanges` / `hasChanges` - 变更追踪工具函数
|
||||||
|
|
||||||
|
### 内部方法标记
|
||||||
|
|
||||||
|
将以下方法标记为 `@internal`,用户应通过 `Core.update()` 驱动更新:
|
||||||
|
- `Scene.update()`
|
||||||
|
- `SceneManager.update()`
|
||||||
|
- `WorldManager.updateAll()`
|
||||||
|
|
||||||
|
## @esengine/network
|
||||||
|
|
||||||
|
新增 `ComponentSyncSystem`,基于 `@sync` 装饰器自动同步组件状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ComponentSyncSystem } from '@esengine/network';
|
||||||
|
|
||||||
|
// 服务端:编码状态
|
||||||
|
const data = syncSystem.encodeAllEntities(false);
|
||||||
|
|
||||||
|
// 客户端:解码状态
|
||||||
|
syncSystem.applySnapshot(data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 将 `@esengine/ecs-framework` 从 devDependencies 移到 peerDependencies
|
||||||
|
|
||||||
|
## @esengine/server
|
||||||
|
|
||||||
|
新增 `ECSRoom`,带有 ECS World 支持的房间基类:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ECSRoom } from '@esengine/server/ecs';
|
||||||
|
|
||||||
|
// 服务端启动
|
||||||
|
Core.create();
|
||||||
|
setInterval(() => Core.update(1 / 60), 16);
|
||||||
|
|
||||||
|
// 定义房间
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
onCreate() {
|
||||||
|
this.addSystem(new PhysicsSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
onJoin(player: Player) {
|
||||||
|
const entity = this.createPlayerEntity(player.id);
|
||||||
|
entity.addComponent(new PlayerComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 设计
|
||||||
|
- 每个 `ECSRoom` 在 `Core.worldManager` 中创建独立的 World
|
||||||
|
- `Core.update()` 统一更新 Time 和所有 World
|
||||||
|
- `onTick()` 只处理状态同步逻辑
|
||||||
|
|
||||||
## 2.4.4
|
## 2.4.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/ecs-framework",
|
"name": "@esengine/ecs-framework",
|
||||||
"version": "2.4.4",
|
"version": "2.6.1",
|
||||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
"module": "dist/index.mjs",
|
"module": "dist/index.mjs",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"unpkg": "dist/index.umd.js",
|
"unpkg": "dist/index.umd.js",
|
||||||
|
"sideEffects": false,
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
"import": "./dist/index.mjs",
|
"import": "./dist/index.mjs",
|
||||||
"require": "./dist/index.cjs"
|
"require": "./dist/index.cjs",
|
||||||
|
"source": "./src/index.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -50,23 +52,24 @@
|
|||||||
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||||
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||||
"@babel/preset-env": "^7.28.3",
|
"@babel/preset-env": "^7.28.3",
|
||||||
|
"@eslint/js": "^9.37.0",
|
||||||
|
"@jest/globals": "^29.7.0",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
"@rollup/plugin-terser": "^0.4.4",
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@jest/globals": "^29.7.0",
|
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.17",
|
"@types/node": "^20.19.17",
|
||||||
"@eslint/js": "^9.37.0",
|
|
||||||
"eslint": "^9.37.0",
|
"eslint": "^9.37.0",
|
||||||
"typescript-eslint": "^8.46.1",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"rollup": "^4.42.0",
|
"rollup": "^4.42.0",
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
"rollup-plugin-dts": "^6.2.1",
|
||||||
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"typescript-eslint": "^8.46.1"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
|
|||||||
@@ -10,10 +10,16 @@ import { Int32 } from './Core/SoAStorage';
|
|||||||
* @en Components in ECS architecture should be pure data containers.
|
* @en Components in ECS architecture should be pure data containers.
|
||||||
* All game logic should be implemented in EntitySystem, not inside components.
|
* All game logic should be implemented in EntitySystem, not inside components.
|
||||||
*
|
*
|
||||||
|
* @zh **重要:所有 Component 子类都必须使用 @ECSComponent 装饰器!**
|
||||||
|
* @zh 该装饰器用于注册组件类型名称,是序列化、网络同步等功能正常工作的前提。
|
||||||
|
* @en **IMPORTANT: All Component subclasses MUST use the @ECSComponent decorator!**
|
||||||
|
* @en This decorator registers the component type name, which is required for serialization, network sync, etc.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* @zh 推荐做法:纯数据组件
|
* @zh 正确做法:使用 @ECSComponent 装饰器
|
||||||
* @en Recommended: Pure data component
|
* @en Correct: Use @ECSComponent decorator
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
* @ECSComponent('HealthComponent')
|
||||||
* class HealthComponent extends Component {
|
* class HealthComponent extends Component {
|
||||||
* public health: number = 100;
|
* public health: number = 100;
|
||||||
* public maxHealth: number = 100;
|
* public maxHealth: number = 100;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
type ComponentEditorOptions,
|
type ComponentEditorOptions,
|
||||||
type ComponentType
|
type ComponentType
|
||||||
} from '../Core/ComponentStorage/ComponentTypeUtils';
|
} from '../Core/ComponentStorage/ComponentTypeUtils';
|
||||||
|
import { SYNC_METADATA, type SyncMetadata } from '../Sync/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储系统类型名称的Symbol键
|
* 存储系统类型名称的Symbol键
|
||||||
@@ -138,6 +139,14 @@ export function ECSComponent(typeName: string, options?: ComponentOptions) {
|
|||||||
metadata[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
metadata[COMPONENT_EDITOR_OPTIONS] = options.editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新 @sync 装饰器创建的 SYNC_METADATA.typeId(如果存在)
|
||||||
|
// Update SYNC_METADATA.typeId created by @sync decorator (if exists)
|
||||||
|
// Property decorators execute before class decorators, so @sync may have used constructor.name
|
||||||
|
const syncMeta = (target as any)[SYNC_METADATA] as SyncMetadata | undefined;
|
||||||
|
if (syncMeta) {
|
||||||
|
syncMeta.typeId = typeName;
|
||||||
|
}
|
||||||
|
|
||||||
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
|
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
|
||||||
// Auto-register to GlobalComponentRegistry, enabling lookup by name
|
// Auto-register to GlobalComponentRegistry, enabling lookup by name
|
||||||
GlobalComponentRegistry.register(target);
|
GlobalComponentRegistry.register(target);
|
||||||
|
|||||||
@@ -7,14 +7,7 @@ import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators
|
|||||||
import { generateGUID } from '../Utils/GUID';
|
import { generateGUID } from '../Utils/GUID';
|
||||||
import type { IScene } from './IScene';
|
import type { IScene } from './IScene';
|
||||||
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
import { EntityHandle, NULL_HANDLE } from './Core/EntityHandle';
|
||||||
|
import { ECSEventType } from './CoreEvents';
|
||||||
/**
|
|
||||||
* @zh 组件活跃状态变化接口
|
|
||||||
* @en Interface for component active state change
|
|
||||||
*/
|
|
||||||
interface IActiveChangeable {
|
|
||||||
onActiveChanged(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @zh 比较两个实体的优先级
|
* @zh 比较两个实体的优先级
|
||||||
@@ -482,9 +475,10 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.scene.eventSystem) {
|
if (this.scene.eventSystem) {
|
||||||
this.scene.eventSystem.emitSync('component:added', {
|
this.scene.eventSystem.emitSync(ECSEventType.COMPONENT_ADDED, {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: 'Entity',
|
source: 'Entity',
|
||||||
|
entity: this,
|
||||||
entityId: this.id,
|
entityId: this.id,
|
||||||
entityName: this.name,
|
entityName: this.name,
|
||||||
entityTag: this.tag?.toString(),
|
entityTag: this.tag?.toString(),
|
||||||
@@ -639,7 +633,7 @@ export class Entity {
|
|||||||
component.entityId = null;
|
component.entityId = null;
|
||||||
|
|
||||||
if (this.scene?.eventSystem) {
|
if (this.scene?.eventSystem) {
|
||||||
this.scene.eventSystem.emitSync('component:removed', {
|
this.scene.eventSystem.emitSync(ECSEventType.COMPONENT_REMOVED, {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
source: 'Entity',
|
source: 'Entity',
|
||||||
entityId: this.id,
|
entityId: this.id,
|
||||||
@@ -770,19 +764,23 @@ export class Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 活跃状态改变时的回调
|
* @zh 活跃状态改变时的回调
|
||||||
|
* @en Callback when active state changes
|
||||||
|
*
|
||||||
|
* @zh 通过事件系统发出 ENTITY_ENABLED 或 ENTITY_DISABLED 事件,
|
||||||
|
* 组件可以通过监听这些事件来响应实体状态变化。
|
||||||
|
* @en Emits ENTITY_ENABLED or ENTITY_DISABLED event through the event system.
|
||||||
|
* Components can listen to these events to respond to entity state changes.
|
||||||
*/
|
*/
|
||||||
private onActiveChanged(): void {
|
private onActiveChanged(): void {
|
||||||
for (const component of this.components) {
|
if (this.scene?.eventSystem) {
|
||||||
if ('onActiveChanged' in component && typeof component.onActiveChanged === 'function') {
|
const eventType = this._active
|
||||||
(component as IActiveChangeable).onActiveChanged();
|
? ECSEventType.ENTITY_ENABLED
|
||||||
}
|
: ECSEventType.ENTITY_DISABLED;
|
||||||
}
|
|
||||||
|
|
||||||
if (this.scene && this.scene.eventSystem) {
|
this.scene.eventSystem.emitSync(eventType, {
|
||||||
this.scene.eventSystem.emitSync('entity:activeChanged', {
|
|
||||||
entity: this,
|
entity: this,
|
||||||
active: this._active
|
scene: this.scene,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -801,6 +799,15 @@ export class Entity {
|
|||||||
|
|
||||||
this._isDestroyed = true;
|
this._isDestroyed = true;
|
||||||
|
|
||||||
|
// 在清理之前发出销毁事件(组件仍然可访问)
|
||||||
|
if (this.scene?.eventSystem) {
|
||||||
|
this.scene.eventSystem.emitSync(ECSEventType.ENTITY_DESTROYED, {
|
||||||
|
entity: this,
|
||||||
|
entityId: this.id,
|
||||||
|
scene: this.scene,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.scene && this.scene.referenceTracker) {
|
if (this.scene && this.scene.referenceTracker) {
|
||||||
this.scene.referenceTracker.clearReferencesTo(this.id);
|
this.scene.referenceTracker.clearReferencesTo(this.id);
|
||||||
this.scene.referenceTracker.unregisterEntityScene(this.id);
|
this.scene.referenceTracker.unregisterEntityScene(this.id);
|
||||||
|
|||||||
@@ -508,7 +508,9 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新场景
|
* @zh 更新场景
|
||||||
|
* @en Update scene
|
||||||
|
* @internal 由 SceneManager 或 World 调用,用户不应直接调用
|
||||||
*/
|
*/
|
||||||
public update() {
|
public update() {
|
||||||
this.epochManager.increment();
|
this.epochManager.increment();
|
||||||
|
|||||||
@@ -240,18 +240,9 @@ export class SceneManager implements IService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新场景
|
* @zh 更新场景
|
||||||
*
|
* @en Update scene
|
||||||
* 应该在每帧的游戏循环中调用。
|
* @internal 由 Core.update() 调用,用户不应直接调用
|
||||||
* 会自动处理延迟场景切换。
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* function gameLoop(deltaTime: number) {
|
|
||||||
* Core.update(deltaTime);
|
|
||||||
* sceneManager.update(); // 每帧调用
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
public update(): void {
|
public update(): void {
|
||||||
// 处理延迟场景切换
|
// 处理延迟场景切换
|
||||||
|
|||||||
125
packages/framework/core/src/ECS/Sync/ChangeTracker.ts
Normal file
125
packages/framework/core/src/ECS/Sync/ChangeTracker.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* @zh 组件变更追踪器
|
||||||
|
* @en Component change tracker
|
||||||
|
*
|
||||||
|
* @zh 用于追踪 @sync 标记字段的变更,支持增量同步
|
||||||
|
* @en Tracks changes to @sync marked fields for delta synchronization
|
||||||
|
*/
|
||||||
|
export class ChangeTracker {
|
||||||
|
/**
|
||||||
|
* @zh 脏字段索引集合
|
||||||
|
* @en Set of dirty field indices
|
||||||
|
*/
|
||||||
|
private _dirtyFields: Set<number> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否有任何变更
|
||||||
|
* @en Whether there are any changes
|
||||||
|
*/
|
||||||
|
private _hasChanges: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 上次同步的时间戳
|
||||||
|
* @en Last sync timestamp
|
||||||
|
*/
|
||||||
|
private _lastSyncTime: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 标记字段为脏
|
||||||
|
* @en Mark field as dirty
|
||||||
|
*
|
||||||
|
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||||
|
*/
|
||||||
|
public setDirty(fieldIndex: number): void {
|
||||||
|
this._dirtyFields.add(fieldIndex);
|
||||||
|
this._hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查是否有变更
|
||||||
|
* @en Check if there are any changes
|
||||||
|
*/
|
||||||
|
public hasChanges(): boolean {
|
||||||
|
return this._hasChanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查特定字段是否脏
|
||||||
|
* @en Check if a specific field is dirty
|
||||||
|
*
|
||||||
|
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||||
|
*/
|
||||||
|
public isDirty(fieldIndex: number): boolean {
|
||||||
|
return this._dirtyFields.has(fieldIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取所有脏字段索引
|
||||||
|
* @en Get all dirty field indices
|
||||||
|
*/
|
||||||
|
public getDirtyFields(): number[] {
|
||||||
|
return Array.from(this._dirtyFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取脏字段数量
|
||||||
|
* @en Get number of dirty fields
|
||||||
|
*/
|
||||||
|
public getDirtyCount(): number {
|
||||||
|
return this._dirtyFields.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清除所有变更标记
|
||||||
|
* @en Clear all change marks
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this._dirtyFields.clear();
|
||||||
|
this._hasChanges = false;
|
||||||
|
this._lastSyncTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清除特定字段的变更标记
|
||||||
|
* @en Clear change mark for a specific field
|
||||||
|
*
|
||||||
|
* @param fieldIndex - @zh 字段索引 @en Field index
|
||||||
|
*/
|
||||||
|
public clearField(fieldIndex: number): void {
|
||||||
|
this._dirtyFields.delete(fieldIndex);
|
||||||
|
if (this._dirtyFields.size === 0) {
|
||||||
|
this._hasChanges = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取上次同步时间
|
||||||
|
* @en Get last sync time
|
||||||
|
*/
|
||||||
|
public get lastSyncTime(): number {
|
||||||
|
return this._lastSyncTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 标记所有字段为脏(用于首次同步)
|
||||||
|
* @en Mark all fields as dirty (for initial sync)
|
||||||
|
*
|
||||||
|
* @param fieldCount - @zh 字段数量 @en Field count
|
||||||
|
*/
|
||||||
|
public markAllDirty(fieldCount: number): void {
|
||||||
|
for (let i = 0; i < fieldCount; i++) {
|
||||||
|
this._dirtyFields.add(i);
|
||||||
|
}
|
||||||
|
this._hasChanges = fieldCount > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 重置追踪器
|
||||||
|
* @en Reset tracker
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._dirtyFields.clear();
|
||||||
|
this._hasChanges = false;
|
||||||
|
this._lastSyncTime = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
packages/framework/core/src/ECS/Sync/NetworkEntityDecorator.ts
Normal file
147
packages/framework/core/src/ECS/Sync/NetworkEntityDecorator.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* @zh 网络实体装饰器
|
||||||
|
* @en Network entity decorator
|
||||||
|
*
|
||||||
|
* @zh 提供 @NetworkEntity 装饰器,用于标记需要自动广播生成/销毁的组件
|
||||||
|
* @en Provides @NetworkEntity decorator to mark components for automatic spawn/despawn broadcasting
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体元数据的 Symbol 键
|
||||||
|
* @en Symbol key for network entity metadata
|
||||||
|
*/
|
||||||
|
export const NETWORK_ENTITY_METADATA = Symbol('NetworkEntityMetadata');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体元数据
|
||||||
|
* @en Network entity metadata
|
||||||
|
*/
|
||||||
|
export interface NetworkEntityMetadata {
|
||||||
|
/**
|
||||||
|
* @zh 预制体类型名称(用于客户端重建实体)
|
||||||
|
* @en Prefab type name (used by client to reconstruct entity)
|
||||||
|
*/
|
||||||
|
prefabType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否自动广播生成
|
||||||
|
* @en Whether to auto-broadcast spawn
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoSpawn: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否自动广播销毁
|
||||||
|
* @en Whether to auto-broadcast despawn
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoDespawn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体装饰器配置选项
|
||||||
|
* @en Network entity decorator options
|
||||||
|
*/
|
||||||
|
export interface NetworkEntityOptions {
|
||||||
|
/**
|
||||||
|
* @zh 是否自动广播生成
|
||||||
|
* @en Whether to auto-broadcast spawn
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoSpawn?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否自动广播销毁
|
||||||
|
* @en Whether to auto-broadcast despawn
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
autoDespawn?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体装饰器
|
||||||
|
* @en Network entity decorator
|
||||||
|
*
|
||||||
|
* @zh 标记组件类为网络实体。当包含此组件的实体被创建或销毁时,
|
||||||
|
* ECSRoom 会自动广播相应的 spawn/despawn 消息给所有客户端。
|
||||||
|
* @en Marks a component class as a network entity. When an entity containing
|
||||||
|
* this component is created or destroyed, ECSRoom will automatically broadcast
|
||||||
|
* the corresponding spawn/despawn messages to all clients.
|
||||||
|
*
|
||||||
|
* @param prefabType - @zh 预制体类型名称 @en Prefab type name
|
||||||
|
* @param options - @zh 可选配置 @en Optional configuration
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { Component, ECSComponent, NetworkEntity, sync } 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 enemy = scene.createEntity('Enemy');
|
||||||
|
* enemy.addComponent(new EnemyComponent()); // 自动广播给所有客户端
|
||||||
|
*
|
||||||
|
* // 当实体销毁时,自动广播 despawn
|
||||||
|
* enemy.destroy(); // 自动广播给所有客户端
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 只自动广播生成,销毁由手动控制
|
||||||
|
* @ECSComponent('Bullet')
|
||||||
|
* @NetworkEntity('Bullet', { autoDespawn: false })
|
||||||
|
* class BulletComponent extends Component {
|
||||||
|
* @sync('float32') x: number = 0;
|
||||||
|
* @sync('float32') y: number = 0;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function NetworkEntity(prefabType: string, options?: NetworkEntityOptions) {
|
||||||
|
return function <T extends new (...args: any[]) => any>(target: T): T {
|
||||||
|
const metadata: NetworkEntityMetadata = {
|
||||||
|
prefabType,
|
||||||
|
autoSpawn: options?.autoSpawn ?? true,
|
||||||
|
autoDespawn: options?.autoDespawn ?? true,
|
||||||
|
};
|
||||||
|
|
||||||
|
(target as any)[NETWORK_ENTITY_METADATA] = metadata;
|
||||||
|
|
||||||
|
return target;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取组件类的网络实体元数据
|
||||||
|
* @en Get network entity metadata for a component class
|
||||||
|
*
|
||||||
|
* @param componentClass - @zh 组件类或组件实例 @en Component class or instance
|
||||||
|
* @returns @zh 网络实体元数据,如果不存在则返回 null @en Network entity metadata, or null if not exists
|
||||||
|
*/
|
||||||
|
export function getNetworkEntityMetadata(componentClass: any): NetworkEntityMetadata | null {
|
||||||
|
if (!componentClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructor = typeof componentClass === 'function'
|
||||||
|
? componentClass
|
||||||
|
: componentClass.constructor;
|
||||||
|
|
||||||
|
return constructor[NETWORK_ENTITY_METADATA] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查组件是否标记为网络实体
|
||||||
|
* @en Check if a component is marked as a network entity
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件类或组件实例 @en Component class or instance
|
||||||
|
* @returns @zh 如果是网络实体返回 true @en Returns true if is a network entity
|
||||||
|
*/
|
||||||
|
export function isNetworkEntity(component: any): boolean {
|
||||||
|
return getNetworkEntityMetadata(component) !== null;
|
||||||
|
}
|
||||||
219
packages/framework/core/src/ECS/Sync/decorators.ts
Normal file
219
packages/framework/core/src/ECS/Sync/decorators.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* @zh 网络同步装饰器
|
||||||
|
* @en Network synchronization decorators
|
||||||
|
*
|
||||||
|
* @zh 提供 @sync 装饰器,用于标记需要网络同步的 Component 字段
|
||||||
|
* @en Provides @sync decorator to mark Component fields for network synchronization
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { SyncType, SyncFieldMetadata, SyncMetadata } from './types';
|
||||||
|
import { SYNC_METADATA, CHANGE_TRACKER } from './types';
|
||||||
|
import { ChangeTracker } from './ChangeTracker';
|
||||||
|
import { getComponentTypeName } from '../Core/ComponentStorage/ComponentTypeUtils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取或创建组件的同步元数据
|
||||||
|
* @en Get or create sync metadata for a component class
|
||||||
|
*
|
||||||
|
* @param target - @zh 组件类的原型 @en Component class prototype
|
||||||
|
* @returns @zh 同步元数据 @en Sync metadata
|
||||||
|
*/
|
||||||
|
function getOrCreateSyncMetadata(target: any): SyncMetadata {
|
||||||
|
const constructor = target.constructor;
|
||||||
|
|
||||||
|
// Check if has own metadata (not inherited)
|
||||||
|
const hasOwnMetadata = Object.prototype.hasOwnProperty.call(constructor, SYNC_METADATA);
|
||||||
|
|
||||||
|
if (hasOwnMetadata) {
|
||||||
|
return constructor[SYNC_METADATA];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for inherited metadata
|
||||||
|
const inheritedMetadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||||
|
|
||||||
|
// Create new metadata (copy from inherited if exists)
|
||||||
|
// Use getComponentTypeName to get @ECSComponent decorator name, or fall back to constructor.name
|
||||||
|
const metadata: SyncMetadata = {
|
||||||
|
typeId: getComponentTypeName(constructor),
|
||||||
|
fields: inheritedMetadata ? [...inheritedMetadata.fields] : [],
|
||||||
|
fieldIndexMap: inheritedMetadata ? new Map(inheritedMetadata.fieldIndexMap) : new Map()
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor[SYNC_METADATA] = metadata;
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步字段装饰器
|
||||||
|
* @en Sync field decorator
|
||||||
|
*
|
||||||
|
* @zh 标记 Component 字段为可网络同步。被标记的字段会自动追踪变更,
|
||||||
|
* 并在值修改时触发变更追踪器。
|
||||||
|
* @en Marks a Component field for network synchronization. Marked fields
|
||||||
|
* automatically track changes and trigger the change tracker on modification.
|
||||||
|
*
|
||||||
|
* @param type - @zh 字段的同步类型 @en Sync type of the field
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
|
* import { 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 的字段不会同步
|
||||||
|
* // Fields without @sync will not be synchronized
|
||||||
|
* localData: any;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function sync(type: SyncType) {
|
||||||
|
return function (target: any, propertyKey: string) {
|
||||||
|
const metadata = getOrCreateSyncMetadata(target);
|
||||||
|
|
||||||
|
// Assign field index (auto-increment based on field count)
|
||||||
|
const fieldIndex = metadata.fields.length;
|
||||||
|
|
||||||
|
// Create field metadata
|
||||||
|
const fieldMeta: SyncFieldMetadata = {
|
||||||
|
index: fieldIndex,
|
||||||
|
name: propertyKey,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register field
|
||||||
|
metadata.fields.push(fieldMeta);
|
||||||
|
metadata.fieldIndexMap.set(propertyKey, fieldIndex);
|
||||||
|
|
||||||
|
// Store original property key for getter/setter
|
||||||
|
const privateKey = `_sync_${propertyKey}`;
|
||||||
|
|
||||||
|
// Define getter/setter to intercept value changes
|
||||||
|
Object.defineProperty(target, propertyKey, {
|
||||||
|
get() {
|
||||||
|
return this[privateKey];
|
||||||
|
},
|
||||||
|
set(value: any) {
|
||||||
|
const oldValue = this[privateKey];
|
||||||
|
if (oldValue !== value) {
|
||||||
|
this[privateKey] = value;
|
||||||
|
// Trigger change tracker if exists
|
||||||
|
const tracker = this[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (tracker) {
|
||||||
|
tracker.setDirty(fieldIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取组件类的同步元数据
|
||||||
|
* @en Get sync metadata for a component class
|
||||||
|
*
|
||||||
|
* @param componentClass - @zh 组件类或组件实例 @en Component class or instance
|
||||||
|
* @returns @zh 同步元数据,如果不存在则返回 null @en Sync metadata, or null if not exists
|
||||||
|
*/
|
||||||
|
export function getSyncMetadata(componentClass: any): SyncMetadata | null {
|
||||||
|
if (!componentClass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const constructor = typeof componentClass === 'function'
|
||||||
|
? componentClass
|
||||||
|
: componentClass.constructor;
|
||||||
|
|
||||||
|
return constructor[SYNC_METADATA] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查组件是否有同步字段
|
||||||
|
* @en Check if a component has sync fields
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件类或组件实例 @en Component class or instance
|
||||||
|
* @returns @zh 如果有同步字段返回 true @en Returns true if has sync fields
|
||||||
|
*/
|
||||||
|
export function hasSyncFields(component: any): boolean {
|
||||||
|
const metadata = getSyncMetadata(component);
|
||||||
|
return metadata !== null && metadata.fields.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取组件实例的变更追踪器
|
||||||
|
* @en Get change tracker of a component instance
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @returns @zh 变更追踪器,如果不存在则返回 null @en Change tracker, or null if not exists
|
||||||
|
*/
|
||||||
|
export function getChangeTracker(component: any): ChangeTracker | null {
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return component[CHANGE_TRACKER] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 为组件实例初始化变更追踪器
|
||||||
|
* @en Initialize change tracker for a component instance
|
||||||
|
*
|
||||||
|
* @zh 这个函数应该在组件首次添加到实体时调用。
|
||||||
|
* 它会创建变更追踪器并标记所有字段为脏(用于首次同步)。
|
||||||
|
* @en This function should be called when a component is first added to an entity.
|
||||||
|
* It creates the change tracker and marks all fields as dirty (for initial sync).
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @returns @zh 变更追踪器 @en Change tracker
|
||||||
|
*/
|
||||||
|
export function initChangeTracker(component: any): ChangeTracker {
|
||||||
|
const metadata = getSyncMetadata(component);
|
||||||
|
if (!metadata) {
|
||||||
|
throw new Error('Component does not have sync metadata. Use @sync decorator on fields.');
|
||||||
|
}
|
||||||
|
|
||||||
|
let tracker = component[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (!tracker) {
|
||||||
|
tracker = new ChangeTracker();
|
||||||
|
component[CHANGE_TRACKER] = tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark all fields as dirty for initial sync
|
||||||
|
tracker.markAllDirty(metadata.fields.length);
|
||||||
|
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 清除组件实例的变更标记
|
||||||
|
* @en Clear change marks for a component instance
|
||||||
|
*
|
||||||
|
* @zh 通常在同步完成后调用,清除所有脏标记
|
||||||
|
* @en Usually called after sync is complete, clears all dirty marks
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
*/
|
||||||
|
export function clearChanges(component: any): void {
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
if (tracker) {
|
||||||
|
tracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查组件是否有变更
|
||||||
|
* @en Check if a component has changes
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @returns @zh 如果有变更返回 true @en Returns true if has changes
|
||||||
|
*/
|
||||||
|
export function hasChanges(component: any): boolean {
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
return tracker ? tracker.hasChanges() : false;
|
||||||
|
}
|
||||||
285
packages/framework/core/src/ECS/Sync/encoding/BinaryReader.ts
Normal file
285
packages/framework/core/src/ECS/Sync/encoding/BinaryReader.ts
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
/**
|
||||||
|
* @zh 二进制读取器
|
||||||
|
* @en Binary Reader
|
||||||
|
*
|
||||||
|
* @zh 提供高效的二进制数据读取功能
|
||||||
|
* @en Provides efficient binary data reading
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { decodeVarint } from './varint';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 文本解码器(使用浏览器原生 API)
|
||||||
|
* @en Text decoder (using browser native API)
|
||||||
|
*/
|
||||||
|
const textDecoder = typeof TextDecoder !== 'undefined' ? new TextDecoder() : null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 二进制读取器
|
||||||
|
* @en Binary reader for decoding data
|
||||||
|
*/
|
||||||
|
export class BinaryReader {
|
||||||
|
/**
|
||||||
|
* @zh 数据缓冲区
|
||||||
|
* @en Data buffer
|
||||||
|
*/
|
||||||
|
private _buffer: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh DataView 用于读取数值
|
||||||
|
* @en DataView for reading numbers
|
||||||
|
*/
|
||||||
|
private _view: DataView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 当前读取位置
|
||||||
|
* @en Current read position
|
||||||
|
*/
|
||||||
|
private _offset: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建二进制读取器
|
||||||
|
* @en Create binary reader
|
||||||
|
*
|
||||||
|
* @param buffer - @zh 要读取的数据 @en Data to read
|
||||||
|
*/
|
||||||
|
constructor(buffer: Uint8Array) {
|
||||||
|
this._buffer = buffer;
|
||||||
|
this._view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取当前读取位置
|
||||||
|
* @en Get current read position
|
||||||
|
*/
|
||||||
|
public get offset(): number {
|
||||||
|
return this._offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 设置读取位置
|
||||||
|
* @en Set read position
|
||||||
|
*/
|
||||||
|
public set offset(value: number) {
|
||||||
|
this._offset = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取剩余可读字节数
|
||||||
|
* @en Get remaining readable bytes
|
||||||
|
*/
|
||||||
|
public get remaining(): number {
|
||||||
|
return this._buffer.length - this._offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查是否有更多数据可读
|
||||||
|
* @en Check if there's more data to read
|
||||||
|
*/
|
||||||
|
public hasMore(): boolean {
|
||||||
|
return this._offset < this._buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取单个字节
|
||||||
|
* @en Read single byte
|
||||||
|
*/
|
||||||
|
public readUint8(): number {
|
||||||
|
this.checkBounds(1);
|
||||||
|
return this._buffer[this._offset++]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取有符号字节
|
||||||
|
* @en Read signed byte
|
||||||
|
*/
|
||||||
|
public readInt8(): number {
|
||||||
|
this.checkBounds(1);
|
||||||
|
return this._view.getInt8(this._offset++);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取布尔值
|
||||||
|
* @en Read boolean
|
||||||
|
*/
|
||||||
|
public readBoolean(): boolean {
|
||||||
|
return this.readUint8() !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 16 位无符号整数(小端序)
|
||||||
|
* @en Read 16-bit unsigned integer (little-endian)
|
||||||
|
*/
|
||||||
|
public readUint16(): number {
|
||||||
|
this.checkBounds(2);
|
||||||
|
const value = this._view.getUint16(this._offset, true);
|
||||||
|
this._offset += 2;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 16 位有符号整数(小端序)
|
||||||
|
* @en Read 16-bit signed integer (little-endian)
|
||||||
|
*/
|
||||||
|
public readInt16(): number {
|
||||||
|
this.checkBounds(2);
|
||||||
|
const value = this._view.getInt16(this._offset, true);
|
||||||
|
this._offset += 2;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 32 位无符号整数(小端序)
|
||||||
|
* @en Read 32-bit unsigned integer (little-endian)
|
||||||
|
*/
|
||||||
|
public readUint32(): number {
|
||||||
|
this.checkBounds(4);
|
||||||
|
const value = this._view.getUint32(this._offset, true);
|
||||||
|
this._offset += 4;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 32 位有符号整数(小端序)
|
||||||
|
* @en Read 32-bit signed integer (little-endian)
|
||||||
|
*/
|
||||||
|
public readInt32(): number {
|
||||||
|
this.checkBounds(4);
|
||||||
|
const value = this._view.getInt32(this._offset, true);
|
||||||
|
this._offset += 4;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 32 位浮点数(小端序)
|
||||||
|
* @en Read 32-bit float (little-endian)
|
||||||
|
*/
|
||||||
|
public readFloat32(): number {
|
||||||
|
this.checkBounds(4);
|
||||||
|
const value = this._view.getFloat32(this._offset, true);
|
||||||
|
this._offset += 4;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取 64 位浮点数(小端序)
|
||||||
|
* @en Read 64-bit float (little-endian)
|
||||||
|
*/
|
||||||
|
public readFloat64(): number {
|
||||||
|
this.checkBounds(8);
|
||||||
|
const value = this._view.getFloat64(this._offset, true);
|
||||||
|
this._offset += 8;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取变长整数
|
||||||
|
* @en Read variable-length integer
|
||||||
|
*/
|
||||||
|
public readVarint(): number {
|
||||||
|
const [value, newOffset] = decodeVarint(this._buffer, this._offset);
|
||||||
|
this._offset = newOffset;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取字符串(UTF-8 编码,带长度前缀)
|
||||||
|
* @en Read string (UTF-8 encoded with length prefix)
|
||||||
|
*/
|
||||||
|
public readString(): string {
|
||||||
|
const length = this.readVarint();
|
||||||
|
this.checkBounds(length);
|
||||||
|
|
||||||
|
const bytes = this._buffer.subarray(this._offset, this._offset + length);
|
||||||
|
this._offset += length;
|
||||||
|
|
||||||
|
if (textDecoder) {
|
||||||
|
return textDecoder.decode(bytes);
|
||||||
|
} else {
|
||||||
|
return this.utf8BytesToString(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 读取原始字节
|
||||||
|
* @en Read raw bytes
|
||||||
|
*
|
||||||
|
* @param length - @zh 要读取的字节数 @en Number of bytes to read
|
||||||
|
*/
|
||||||
|
public readBytes(length: number): Uint8Array {
|
||||||
|
this.checkBounds(length);
|
||||||
|
const bytes = this._buffer.slice(this._offset, this._offset + length);
|
||||||
|
this._offset += length;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 查看下一个字节但不移动读取位置
|
||||||
|
* @en Peek next byte without advancing read position
|
||||||
|
*/
|
||||||
|
public peekUint8(): number {
|
||||||
|
this.checkBounds(1);
|
||||||
|
return this._buffer[this._offset]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 跳过指定字节数
|
||||||
|
* @en Skip specified number of bytes
|
||||||
|
*/
|
||||||
|
public skip(count: number): void {
|
||||||
|
this.checkBounds(count);
|
||||||
|
this._offset += count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 检查边界
|
||||||
|
* @en Check bounds
|
||||||
|
*/
|
||||||
|
private checkBounds(size: number): void {
|
||||||
|
if (this._offset + size > this._buffer.length) {
|
||||||
|
throw new Error(`BinaryReader: buffer overflow (offset=${this._offset}, size=${size}, bufferLength=${this._buffer.length})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh UTF-8 字节转字符串(后备方案)
|
||||||
|
* @en UTF-8 bytes to string (fallback)
|
||||||
|
*/
|
||||||
|
private utf8BytesToString(bytes: Uint8Array): string {
|
||||||
|
let result = '';
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < bytes.length) {
|
||||||
|
let charCode: number;
|
||||||
|
const byte1 = bytes[i++]!;
|
||||||
|
|
||||||
|
if (byte1 < 0x80) {
|
||||||
|
charCode = byte1;
|
||||||
|
} else if (byte1 < 0xE0) {
|
||||||
|
const byte2 = bytes[i++]!;
|
||||||
|
charCode = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
|
||||||
|
} else if (byte1 < 0xF0) {
|
||||||
|
const byte2 = bytes[i++]!;
|
||||||
|
const byte3 = bytes[i++]!;
|
||||||
|
charCode = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
|
||||||
|
} else {
|
||||||
|
const byte2 = bytes[i++]!;
|
||||||
|
const byte3 = bytes[i++]!;
|
||||||
|
const byte4 = bytes[i++]!;
|
||||||
|
charCode = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) |
|
||||||
|
((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
|
||||||
|
|
||||||
|
// Convert to surrogate pair
|
||||||
|
if (charCode > 0xFFFF) {
|
||||||
|
charCode -= 0x10000;
|
||||||
|
result += String.fromCharCode(0xD800 + (charCode >> 10));
|
||||||
|
charCode = 0xDC00 + (charCode & 0x3FF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result += String.fromCharCode(charCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
257
packages/framework/core/src/ECS/Sync/encoding/BinaryWriter.ts
Normal file
257
packages/framework/core/src/ECS/Sync/encoding/BinaryWriter.ts
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
/**
|
||||||
|
* @zh 二进制写入器
|
||||||
|
* @en Binary Writer
|
||||||
|
*
|
||||||
|
* @zh 提供高效的二进制数据写入功能,支持自动扩容
|
||||||
|
* @en Provides efficient binary data writing with auto-expansion
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { encodeVarint, varintSize } from './varint';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 文本编码器(使用浏览器原生 API)
|
||||||
|
* @en Text encoder (using browser native API)
|
||||||
|
*/
|
||||||
|
const textEncoder = typeof TextEncoder !== 'undefined' ? new TextEncoder() : null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 二进制写入器
|
||||||
|
* @en Binary writer for encoding data
|
||||||
|
*/
|
||||||
|
export class BinaryWriter {
|
||||||
|
/**
|
||||||
|
* @zh 内部缓冲区
|
||||||
|
* @en Internal buffer
|
||||||
|
*/
|
||||||
|
private _buffer: Uint8Array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh DataView 用于写入数值
|
||||||
|
* @en DataView for writing numbers
|
||||||
|
*/
|
||||||
|
private _view: DataView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 当前写入位置
|
||||||
|
* @en Current write position
|
||||||
|
*/
|
||||||
|
private _offset: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建二进制写入器
|
||||||
|
* @en Create binary writer
|
||||||
|
*
|
||||||
|
* @param initialCapacity - @zh 初始容量 @en Initial capacity
|
||||||
|
*/
|
||||||
|
constructor(initialCapacity: number = 256) {
|
||||||
|
this._buffer = new Uint8Array(initialCapacity);
|
||||||
|
this._view = new DataView(this._buffer.buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取当前写入位置
|
||||||
|
* @en Get current write position
|
||||||
|
*/
|
||||||
|
public get offset(): number {
|
||||||
|
return this._offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取写入的数据
|
||||||
|
* @en Get written data
|
||||||
|
*
|
||||||
|
* @returns @zh 包含写入数据的 Uint8Array @en Uint8Array containing written data
|
||||||
|
*/
|
||||||
|
public toUint8Array(): Uint8Array {
|
||||||
|
return this._buffer.slice(0, this._offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 重置写入器(清空数据但保留缓冲区)
|
||||||
|
* @en Reset writer (clear data but keep buffer)
|
||||||
|
*/
|
||||||
|
public reset(): void {
|
||||||
|
this._offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 确保有足够空间
|
||||||
|
* @en Ensure enough space
|
||||||
|
*
|
||||||
|
* @param size - @zh 需要的额外字节数 @en Extra bytes needed
|
||||||
|
*/
|
||||||
|
private ensureCapacity(size: number): void {
|
||||||
|
const required = this._offset + size;
|
||||||
|
if (required > this._buffer.length) {
|
||||||
|
// Double the buffer size or use required size, whichever is larger
|
||||||
|
const newSize = Math.max(this._buffer.length * 2, required);
|
||||||
|
const newBuffer = new Uint8Array(newSize);
|
||||||
|
newBuffer.set(this._buffer);
|
||||||
|
this._buffer = newBuffer;
|
||||||
|
this._view = new DataView(this._buffer.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入单个字节
|
||||||
|
* @en Write single byte
|
||||||
|
*/
|
||||||
|
public writeUint8(value: number): void {
|
||||||
|
this.ensureCapacity(1);
|
||||||
|
this._buffer[this._offset++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入有符号字节
|
||||||
|
* @en Write signed byte
|
||||||
|
*/
|
||||||
|
public writeInt8(value: number): void {
|
||||||
|
this.ensureCapacity(1);
|
||||||
|
this._view.setInt8(this._offset++, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入布尔值
|
||||||
|
* @en Write boolean
|
||||||
|
*/
|
||||||
|
public writeBoolean(value: boolean): void {
|
||||||
|
this.writeUint8(value ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 16 位无符号整数(小端序)
|
||||||
|
* @en Write 16-bit unsigned integer (little-endian)
|
||||||
|
*/
|
||||||
|
public writeUint16(value: number): void {
|
||||||
|
this.ensureCapacity(2);
|
||||||
|
this._view.setUint16(this._offset, value, true);
|
||||||
|
this._offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 16 位有符号整数(小端序)
|
||||||
|
* @en Write 16-bit signed integer (little-endian)
|
||||||
|
*/
|
||||||
|
public writeInt16(value: number): void {
|
||||||
|
this.ensureCapacity(2);
|
||||||
|
this._view.setInt16(this._offset, value, true);
|
||||||
|
this._offset += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 32 位无符号整数(小端序)
|
||||||
|
* @en Write 32-bit unsigned integer (little-endian)
|
||||||
|
*/
|
||||||
|
public writeUint32(value: number): void {
|
||||||
|
this.ensureCapacity(4);
|
||||||
|
this._view.setUint32(this._offset, value, true);
|
||||||
|
this._offset += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 32 位有符号整数(小端序)
|
||||||
|
* @en Write 32-bit signed integer (little-endian)
|
||||||
|
*/
|
||||||
|
public writeInt32(value: number): void {
|
||||||
|
this.ensureCapacity(4);
|
||||||
|
this._view.setInt32(this._offset, value, true);
|
||||||
|
this._offset += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 32 位浮点数(小端序)
|
||||||
|
* @en Write 32-bit float (little-endian)
|
||||||
|
*/
|
||||||
|
public writeFloat32(value: number): void {
|
||||||
|
this.ensureCapacity(4);
|
||||||
|
this._view.setFloat32(this._offset, value, true);
|
||||||
|
this._offset += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入 64 位浮点数(小端序)
|
||||||
|
* @en Write 64-bit float (little-endian)
|
||||||
|
*/
|
||||||
|
public writeFloat64(value: number): void {
|
||||||
|
this.ensureCapacity(8);
|
||||||
|
this._view.setFloat64(this._offset, value, true);
|
||||||
|
this._offset += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入变长整数
|
||||||
|
* @en Write variable-length integer
|
||||||
|
*/
|
||||||
|
public writeVarint(value: number): void {
|
||||||
|
this.ensureCapacity(varintSize(value));
|
||||||
|
this._offset = encodeVarint(value, this._buffer, this._offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入字符串(UTF-8 编码,带长度前缀)
|
||||||
|
* @en Write string (UTF-8 encoded with length prefix)
|
||||||
|
*/
|
||||||
|
public writeString(value: string): void {
|
||||||
|
if (textEncoder) {
|
||||||
|
const encoded = textEncoder.encode(value);
|
||||||
|
this.writeVarint(encoded.length);
|
||||||
|
this.ensureCapacity(encoded.length);
|
||||||
|
this._buffer.set(encoded, this._offset);
|
||||||
|
this._offset += encoded.length;
|
||||||
|
} else {
|
||||||
|
// Fallback for environments without TextEncoder
|
||||||
|
const bytes = this.stringToUtf8Bytes(value);
|
||||||
|
this.writeVarint(bytes.length);
|
||||||
|
this.ensureCapacity(bytes.length);
|
||||||
|
this._buffer.set(bytes, this._offset);
|
||||||
|
this._offset += bytes.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 写入原始字节
|
||||||
|
* @en Write raw bytes
|
||||||
|
*/
|
||||||
|
public writeBytes(data: Uint8Array): void {
|
||||||
|
this.ensureCapacity(data.length);
|
||||||
|
this._buffer.set(data, this._offset);
|
||||||
|
this._offset += data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 字符串转 UTF-8 字节(后备方案)
|
||||||
|
* @en String to UTF-8 bytes (fallback)
|
||||||
|
*/
|
||||||
|
private stringToUtf8Bytes(str: string): Uint8Array {
|
||||||
|
const bytes: number[] = [];
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
let charCode = str.charCodeAt(i);
|
||||||
|
|
||||||
|
// Handle surrogate pairs
|
||||||
|
if (charCode >= 0xD800 && charCode <= 0xDBFF && i + 1 < str.length) {
|
||||||
|
const next = str.charCodeAt(i + 1);
|
||||||
|
if (next >= 0xDC00 && next <= 0xDFFF) {
|
||||||
|
charCode = 0x10000 + ((charCode - 0xD800) << 10) + (next - 0xDC00);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (charCode < 0x80) {
|
||||||
|
bytes.push(charCode);
|
||||||
|
} else if (charCode < 0x800) {
|
||||||
|
bytes.push(0xC0 | (charCode >> 6));
|
||||||
|
bytes.push(0x80 | (charCode & 0x3F));
|
||||||
|
} else if (charCode < 0x10000) {
|
||||||
|
bytes.push(0xE0 | (charCode >> 12));
|
||||||
|
bytes.push(0x80 | ((charCode >> 6) & 0x3F));
|
||||||
|
bytes.push(0x80 | (charCode & 0x3F));
|
||||||
|
} else {
|
||||||
|
bytes.push(0xF0 | (charCode >> 18));
|
||||||
|
bytes.push(0x80 | ((charCode >> 12) & 0x3F));
|
||||||
|
bytes.push(0x80 | ((charCode >> 6) & 0x3F));
|
||||||
|
bytes.push(0x80 | (charCode & 0x3F));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Uint8Array(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
372
packages/framework/core/src/ECS/Sync/encoding/Decoder.ts
Normal file
372
packages/framework/core/src/ECS/Sync/encoding/Decoder.ts
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
/**
|
||||||
|
* @zh 组件状态解码器
|
||||||
|
* @en Component state decoder
|
||||||
|
*
|
||||||
|
* @zh 从二进制格式解码并应用到 ECS Component
|
||||||
|
* @en Decodes binary format and applies to ECS Components
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Entity } from '../../Entity';
|
||||||
|
import type { Component } from '../../Component';
|
||||||
|
import type { Scene } from '../../Scene';
|
||||||
|
import type { SyncType, SyncMetadata } from '../types';
|
||||||
|
import { SyncOperation, SYNC_METADATA } from '../types';
|
||||||
|
import { BinaryReader } from './BinaryReader';
|
||||||
|
import { GlobalComponentRegistry } from '../../Core/ComponentStorage/ComponentRegistry';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码字段值
|
||||||
|
* @en Decode field value
|
||||||
|
*/
|
||||||
|
function decodeFieldValue(reader: BinaryReader, type: SyncType): any {
|
||||||
|
switch (type) {
|
||||||
|
case 'boolean':
|
||||||
|
return reader.readBoolean();
|
||||||
|
case 'int8':
|
||||||
|
return reader.readInt8();
|
||||||
|
case 'uint8':
|
||||||
|
return reader.readUint8();
|
||||||
|
case 'int16':
|
||||||
|
return reader.readInt16();
|
||||||
|
case 'uint16':
|
||||||
|
return reader.readUint16();
|
||||||
|
case 'int32':
|
||||||
|
return reader.readInt32();
|
||||||
|
case 'uint32':
|
||||||
|
return reader.readUint32();
|
||||||
|
case 'float32':
|
||||||
|
return reader.readFloat32();
|
||||||
|
case 'float64':
|
||||||
|
return reader.readFloat64();
|
||||||
|
case 'string':
|
||||||
|
return reader.readString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码并应用组件数据
|
||||||
|
* @en Decode and apply component data
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||||
|
* @param reader - @zh 二进制读取器 @en Binary reader
|
||||||
|
*/
|
||||||
|
export function decodeComponent(
|
||||||
|
component: Component,
|
||||||
|
metadata: SyncMetadata,
|
||||||
|
reader: BinaryReader
|
||||||
|
): void {
|
||||||
|
const fieldCount = reader.readVarint();
|
||||||
|
|
||||||
|
for (let i = 0; i < fieldCount; i++) {
|
||||||
|
const fieldIndex = reader.readUint8();
|
||||||
|
const field = metadata.fields[fieldIndex];
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
const value = decodeFieldValue(reader, field.type);
|
||||||
|
// Directly set the private backing field to avoid triggering change tracking
|
||||||
|
(component as any)[`_sync_${field.name}`] = value;
|
||||||
|
} else {
|
||||||
|
// Unknown field, skip based on type info in metadata
|
||||||
|
console.warn(`Unknown sync field index: ${fieldIndex}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码实体快照结果
|
||||||
|
* @en Decode entity snapshot result
|
||||||
|
*/
|
||||||
|
export interface DecodeEntityResult {
|
||||||
|
/**
|
||||||
|
* @zh 实体 ID
|
||||||
|
* @en Entity ID
|
||||||
|
*/
|
||||||
|
entityId: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否为新实体
|
||||||
|
* @en Whether it's a new entity
|
||||||
|
*/
|
||||||
|
isNew: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码的组件类型列表
|
||||||
|
* @en List of decoded component types
|
||||||
|
*/
|
||||||
|
componentTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码并应用实体数据
|
||||||
|
* @en Decode and apply entity data
|
||||||
|
*
|
||||||
|
* @param scene - @zh 场景 @en Scene
|
||||||
|
* @param reader - @zh 二进制读取器 @en Binary reader
|
||||||
|
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||||
|
* @returns @zh 解码结果 @en Decode result
|
||||||
|
*/
|
||||||
|
export function decodeEntity(
|
||||||
|
scene: Scene,
|
||||||
|
reader: BinaryReader,
|
||||||
|
entityMap?: Map<number, Entity>
|
||||||
|
): DecodeEntityResult {
|
||||||
|
const entityId = reader.readUint32();
|
||||||
|
const componentCount = reader.readVarint();
|
||||||
|
const componentTypes: string[] = [];
|
||||||
|
|
||||||
|
// Find or create entity
|
||||||
|
let entity: Entity | null | undefined = entityMap?.get(entityId);
|
||||||
|
let isNew = false;
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
entity = scene.findEntityById(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entity) {
|
||||||
|
// Entity doesn't exist, create it
|
||||||
|
entity = scene.createEntity(`entity_${entityId}`);
|
||||||
|
isNew = true;
|
||||||
|
entityMap?.set(entityId, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < componentCount; i++) {
|
||||||
|
const typeId = reader.readString();
|
||||||
|
componentTypes.push(typeId);
|
||||||
|
|
||||||
|
// Find component class from GlobalComponentRegistry
|
||||||
|
const componentClass = GlobalComponentRegistry.getComponentType(typeId) as (new () => Component) | null;
|
||||||
|
if (!componentClass) {
|
||||||
|
console.warn(`Unknown component type: ${typeId}`);
|
||||||
|
// Skip component data - we need to read it to advance the reader
|
||||||
|
const fieldCount = reader.readVarint();
|
||||||
|
for (let j = 0; j < fieldCount; j++) {
|
||||||
|
reader.readUint8(); // fieldIndex
|
||||||
|
// We can't skip properly without knowing the type, so this is a problem
|
||||||
|
// For now, log error and break
|
||||||
|
console.error(`Cannot skip unknown component type: ${typeId}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata: SyncMetadata | undefined = (componentClass as any)[SYNC_METADATA];
|
||||||
|
if (!metadata) {
|
||||||
|
console.warn(`Component ${typeId} has no sync metadata`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find or add component
|
||||||
|
let component = entity.getComponent(componentClass);
|
||||||
|
if (!component) {
|
||||||
|
component = entity.addComponent(new componentClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode component data
|
||||||
|
decodeComponent(component, metadata, reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityId, isNew, componentTypes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码快照结果
|
||||||
|
* @en Decode snapshot result
|
||||||
|
*/
|
||||||
|
export interface DecodeSnapshotResult {
|
||||||
|
/**
|
||||||
|
* @zh 操作类型
|
||||||
|
* @en Operation type
|
||||||
|
*/
|
||||||
|
operation: SyncOperation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码的实体列表
|
||||||
|
* @en List of decoded entities
|
||||||
|
*/
|
||||||
|
entities: DecodeEntityResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码状态快照
|
||||||
|
* @en Decode state snapshot
|
||||||
|
*
|
||||||
|
* @param scene - @zh 场景 @en Scene
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||||
|
* @returns @zh 解码结果 @en Decode result
|
||||||
|
*/
|
||||||
|
export function decodeSnapshot(
|
||||||
|
scene: Scene,
|
||||||
|
data: Uint8Array,
|
||||||
|
entityMap?: Map<number, Entity>
|
||||||
|
): DecodeSnapshotResult {
|
||||||
|
const reader = new BinaryReader(data);
|
||||||
|
const operation = reader.readUint8() as SyncOperation;
|
||||||
|
const entityCount = reader.readVarint();
|
||||||
|
const entities: DecodeEntityResult[] = [];
|
||||||
|
|
||||||
|
const map = entityMap || new Map<number, Entity>();
|
||||||
|
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const result = decodeEntity(scene, reader, map);
|
||||||
|
entities.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { operation, entities };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码生成消息结果
|
||||||
|
* @en Decode spawn message result
|
||||||
|
*/
|
||||||
|
export interface DecodeSpawnResult {
|
||||||
|
/**
|
||||||
|
* @zh 实体
|
||||||
|
* @en Entity
|
||||||
|
*/
|
||||||
|
entity: Entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 预制体类型
|
||||||
|
* @en Prefab type
|
||||||
|
*/
|
||||||
|
prefabType: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码的组件类型列表
|
||||||
|
* @en List of decoded component types
|
||||||
|
*/
|
||||||
|
componentTypes: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码实体生成消息
|
||||||
|
* @en Decode entity spawn message
|
||||||
|
*
|
||||||
|
* @param scene - @zh 场景 @en Scene
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||||
|
* @returns @zh 解码结果,如果不是 SPAWN 消息则返回 null @en Decode result, or null if not a SPAWN message
|
||||||
|
*/
|
||||||
|
export function decodeSpawn(
|
||||||
|
scene: Scene,
|
||||||
|
data: Uint8Array,
|
||||||
|
entityMap?: Map<number, Entity>
|
||||||
|
): DecodeSpawnResult | null {
|
||||||
|
const reader = new BinaryReader(data);
|
||||||
|
const operation = reader.readUint8();
|
||||||
|
|
||||||
|
if (operation !== SyncOperation.SPAWN) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityId = reader.readUint32();
|
||||||
|
const prefabType = reader.readString();
|
||||||
|
const componentCount = reader.readVarint();
|
||||||
|
const componentTypes: string[] = [];
|
||||||
|
|
||||||
|
// Create entity
|
||||||
|
const entity = scene.createEntity(`entity_${entityId}`);
|
||||||
|
entityMap?.set(entityId, entity);
|
||||||
|
|
||||||
|
for (let i = 0; i < componentCount; i++) {
|
||||||
|
const typeId = reader.readString();
|
||||||
|
componentTypes.push(typeId);
|
||||||
|
|
||||||
|
const componentClass = GlobalComponentRegistry.getComponentType(typeId) as (new () => Component) | null;
|
||||||
|
if (!componentClass) {
|
||||||
|
console.warn(`Unknown component type: ${typeId}`);
|
||||||
|
// Try to skip
|
||||||
|
const fieldCount = reader.readVarint();
|
||||||
|
for (let j = 0; j < fieldCount; j++) {
|
||||||
|
reader.readUint8();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata: SyncMetadata | undefined = (componentClass as any)[SYNC_METADATA];
|
||||||
|
if (!metadata) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = entity.addComponent(new (componentClass as new () => Component)());
|
||||||
|
decodeComponent(component, metadata, reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entity, prefabType, componentTypes };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码销毁消息结果
|
||||||
|
* @en Decode despawn message result
|
||||||
|
*/
|
||||||
|
export interface DecodeDespawnResult {
|
||||||
|
/**
|
||||||
|
* @zh 销毁的实体 ID 列表
|
||||||
|
* @en List of despawned entity IDs
|
||||||
|
*/
|
||||||
|
entityIds: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码实体销毁消息
|
||||||
|
* @en Decode entity despawn message
|
||||||
|
*
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @returns @zh 解码结果,如果不是 DESPAWN 消息则返回 null @en Decode result, or null if not a DESPAWN message
|
||||||
|
*/
|
||||||
|
export function decodeDespawn(data: Uint8Array): DecodeDespawnResult | null {
|
||||||
|
const reader = new BinaryReader(data);
|
||||||
|
const operation = reader.readUint8();
|
||||||
|
|
||||||
|
if (operation !== SyncOperation.DESPAWN) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityIds: number[] = [];
|
||||||
|
|
||||||
|
// Check if it's a single entity or batch
|
||||||
|
if (reader.remaining === 4) {
|
||||||
|
// Single entity
|
||||||
|
entityIds.push(reader.readUint32());
|
||||||
|
} else {
|
||||||
|
// Batch
|
||||||
|
const count = reader.readVarint();
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
entityIds.push(reader.readUint32());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { entityIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 处理销毁消息(从场景中移除实体)
|
||||||
|
* @en Process despawn message (remove entities from scene)
|
||||||
|
*
|
||||||
|
* @param scene - @zh 场景 @en Scene
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @param entityMap - @zh 实体 ID 映射(可选)@en Entity ID mapping (optional)
|
||||||
|
* @returns @zh 移除的实体 ID 列表 @en List of removed entity IDs
|
||||||
|
*/
|
||||||
|
export function processDespawn(
|
||||||
|
scene: Scene,
|
||||||
|
data: Uint8Array,
|
||||||
|
entityMap?: Map<number, Entity>
|
||||||
|
): number[] {
|
||||||
|
const result = decodeDespawn(data);
|
||||||
|
if (!result) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entityId of result.entityIds) {
|
||||||
|
const entity = entityMap?.get(entityId) || scene.findEntityById(entityId);
|
||||||
|
if (entity) {
|
||||||
|
entity.destroy();
|
||||||
|
entityMap?.delete(entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.entityIds;
|
||||||
|
}
|
||||||
291
packages/framework/core/src/ECS/Sync/encoding/Encoder.ts
Normal file
291
packages/framework/core/src/ECS/Sync/encoding/Encoder.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* @zh 组件状态编码器
|
||||||
|
* @en Component state encoder
|
||||||
|
*
|
||||||
|
* @zh 将 ECS Component 的 @sync 字段编码为二进制格式
|
||||||
|
* @en Encodes @sync fields of ECS Components to binary format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Entity } from '../../Entity';
|
||||||
|
import type { Component } from '../../Component';
|
||||||
|
import type { SyncType, SyncMetadata } from '../types';
|
||||||
|
import { SyncOperation, SYNC_METADATA, CHANGE_TRACKER } from '../types';
|
||||||
|
import type { ChangeTracker } from '../ChangeTracker';
|
||||||
|
import { BinaryWriter } from './BinaryWriter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码单个字段值
|
||||||
|
* @en Encode a single field value
|
||||||
|
*/
|
||||||
|
function encodeFieldValue(writer: BinaryWriter, value: any, type: SyncType): void {
|
||||||
|
switch (type) {
|
||||||
|
case 'boolean':
|
||||||
|
writer.writeBoolean(value);
|
||||||
|
break;
|
||||||
|
case 'int8':
|
||||||
|
writer.writeInt8(value);
|
||||||
|
break;
|
||||||
|
case 'uint8':
|
||||||
|
writer.writeUint8(value);
|
||||||
|
break;
|
||||||
|
case 'int16':
|
||||||
|
writer.writeInt16(value);
|
||||||
|
break;
|
||||||
|
case 'uint16':
|
||||||
|
writer.writeUint16(value);
|
||||||
|
break;
|
||||||
|
case 'int32':
|
||||||
|
writer.writeInt32(value);
|
||||||
|
break;
|
||||||
|
case 'uint32':
|
||||||
|
writer.writeUint32(value);
|
||||||
|
break;
|
||||||
|
case 'float32':
|
||||||
|
writer.writeFloat32(value);
|
||||||
|
break;
|
||||||
|
case 'float64':
|
||||||
|
writer.writeFloat64(value);
|
||||||
|
break;
|
||||||
|
case 'string':
|
||||||
|
writer.writeString(value ?? '');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码组件的完整状态
|
||||||
|
* @en Encode full state of a component
|
||||||
|
*
|
||||||
|
* @zh 格式: [fieldCount: varint] ([fieldIndex: uint8] [value])...
|
||||||
|
* @en Format: [fieldCount: varint] ([fieldIndex: uint8] [value])...
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||||
|
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||||
|
*/
|
||||||
|
export function encodeComponentFull(
|
||||||
|
component: Component,
|
||||||
|
metadata: SyncMetadata,
|
||||||
|
writer: BinaryWriter
|
||||||
|
): void {
|
||||||
|
const fields = metadata.fields;
|
||||||
|
writer.writeVarint(fields.length);
|
||||||
|
|
||||||
|
for (const field of fields) {
|
||||||
|
writer.writeUint8(field.index);
|
||||||
|
const value = (component as any)[field.name];
|
||||||
|
encodeFieldValue(writer, value, field.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码组件的增量状态(只编码脏字段)
|
||||||
|
* @en Encode delta state of a component (only dirty fields)
|
||||||
|
*
|
||||||
|
* @zh 格式: [dirtyCount: varint] ([fieldIndex: uint8] [value])...
|
||||||
|
* @en Format: [dirtyCount: varint] ([fieldIndex: uint8] [value])...
|
||||||
|
*
|
||||||
|
* @param component - @zh 组件实例 @en Component instance
|
||||||
|
* @param metadata - @zh 组件同步元数据 @en Component sync metadata
|
||||||
|
* @param tracker - @zh 变更追踪器 @en Change tracker
|
||||||
|
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||||
|
* @returns @zh 是否有数据编码 @en Whether any data was encoded
|
||||||
|
*/
|
||||||
|
export function encodeComponentDelta(
|
||||||
|
component: Component,
|
||||||
|
metadata: SyncMetadata,
|
||||||
|
tracker: ChangeTracker,
|
||||||
|
writer: BinaryWriter
|
||||||
|
): boolean {
|
||||||
|
if (!tracker.hasChanges()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirtyFields = tracker.getDirtyFields();
|
||||||
|
writer.writeVarint(dirtyFields.length);
|
||||||
|
|
||||||
|
for (const fieldIndex of dirtyFields) {
|
||||||
|
const field = metadata.fields[fieldIndex];
|
||||||
|
if (field) {
|
||||||
|
writer.writeUint8(field.index);
|
||||||
|
const value = (component as any)[field.name];
|
||||||
|
encodeFieldValue(writer, value, field.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirtyFields.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码实体的所有同步组件
|
||||||
|
* @en Encode all sync components of an entity
|
||||||
|
*
|
||||||
|
* @zh 格式:
|
||||||
|
* [entityId: uint32]
|
||||||
|
* [componentCount: varint]
|
||||||
|
* ([typeIdLength: varint] [typeId: string] [componentData])...
|
||||||
|
*
|
||||||
|
* @en Format:
|
||||||
|
* [entityId: uint32]
|
||||||
|
* [componentCount: varint]
|
||||||
|
* ([typeIdLength: varint] [typeId: string] [componentData])...
|
||||||
|
*
|
||||||
|
* @param entity - @zh 实体 @en Entity
|
||||||
|
* @param writer - @zh 二进制写入器 @en Binary writer
|
||||||
|
* @param deltaOnly - @zh 只编码增量 @en Only encode delta
|
||||||
|
* @returns @zh 编码的组件数量 @en Number of components encoded
|
||||||
|
*/
|
||||||
|
export function encodeEntity(
|
||||||
|
entity: Entity,
|
||||||
|
writer: BinaryWriter,
|
||||||
|
deltaOnly: boolean = false
|
||||||
|
): number {
|
||||||
|
writer.writeUint32(entity.id);
|
||||||
|
|
||||||
|
const components = entity.components;
|
||||||
|
const syncComponents: Array<{
|
||||||
|
component: Component;
|
||||||
|
metadata: SyncMetadata;
|
||||||
|
tracker: ChangeTracker | undefined;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Collect components with sync metadata
|
||||||
|
for (const component of components) {
|
||||||
|
const constructor = component.constructor as any;
|
||||||
|
const metadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||||
|
if (metadata && metadata.fields.length > 0) {
|
||||||
|
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
|
||||||
|
// For delta encoding, only include components with changes
|
||||||
|
if (deltaOnly && tracker && !tracker.hasChanges()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
syncComponents.push({ component, metadata, tracker });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeVarint(syncComponents.length);
|
||||||
|
|
||||||
|
for (const { component, metadata, tracker } of syncComponents) {
|
||||||
|
// Write component type ID
|
||||||
|
writer.writeString(metadata.typeId);
|
||||||
|
|
||||||
|
if (deltaOnly && tracker) {
|
||||||
|
encodeComponentDelta(component, metadata, tracker, writer);
|
||||||
|
} else {
|
||||||
|
encodeComponentFull(component, metadata, writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncComponents.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码状态快照(多个实体)
|
||||||
|
* @en Encode state snapshot (multiple entities)
|
||||||
|
*
|
||||||
|
* @zh 格式:
|
||||||
|
* [operation: uint8] (FULL=0, DELTA=1, SPAWN=2, DESPAWN=3)
|
||||||
|
* [entityCount: varint]
|
||||||
|
* (entityData)...
|
||||||
|
*
|
||||||
|
* @en Format:
|
||||||
|
* [operation: uint8] (FULL=0, DELTA=1, SPAWN=2, DESPAWN=3)
|
||||||
|
* [entityCount: varint]
|
||||||
|
* (entityData)...
|
||||||
|
*
|
||||||
|
* @param entities - @zh 要编码的实体数组 @en Entities to encode
|
||||||
|
* @param operation - @zh 同步操作类型 @en Sync operation type
|
||||||
|
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||||
|
*/
|
||||||
|
export function encodeSnapshot(
|
||||||
|
entities: Entity[],
|
||||||
|
operation: SyncOperation = SyncOperation.FULL
|
||||||
|
): Uint8Array {
|
||||||
|
const writer = new BinaryWriter(1024);
|
||||||
|
|
||||||
|
writer.writeUint8(operation);
|
||||||
|
writer.writeVarint(entities.length);
|
||||||
|
|
||||||
|
const deltaOnly = operation === SyncOperation.DELTA;
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
encodeEntity(entity, writer, deltaOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.toUint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码实体生成消息
|
||||||
|
* @en Encode entity spawn message
|
||||||
|
*
|
||||||
|
* @param entity - @zh 生成的实体 @en Spawned entity
|
||||||
|
* @param prefabType - @zh 预制体类型(可选)@en Prefab type (optional)
|
||||||
|
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||||
|
*/
|
||||||
|
export function encodeSpawn(entity: Entity, prefabType?: string): Uint8Array {
|
||||||
|
const writer = new BinaryWriter(256);
|
||||||
|
|
||||||
|
writer.writeUint8(SyncOperation.SPAWN);
|
||||||
|
writer.writeUint32(entity.id);
|
||||||
|
writer.writeString(prefabType || '');
|
||||||
|
|
||||||
|
// Encode all sync components for initial state
|
||||||
|
const components = entity.components;
|
||||||
|
const syncComponents: Array<{ component: Component; metadata: SyncMetadata }> = [];
|
||||||
|
|
||||||
|
for (const component of components) {
|
||||||
|
const constructor = component.constructor as any;
|
||||||
|
const metadata: SyncMetadata | undefined = constructor[SYNC_METADATA];
|
||||||
|
if (metadata && metadata.fields.length > 0) {
|
||||||
|
syncComponents.push({ component, metadata });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.writeVarint(syncComponents.length);
|
||||||
|
|
||||||
|
for (const { component, metadata } of syncComponents) {
|
||||||
|
writer.writeString(metadata.typeId);
|
||||||
|
encodeComponentFull(component, metadata, writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.toUint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码实体销毁消息
|
||||||
|
* @en Encode entity despawn message
|
||||||
|
*
|
||||||
|
* @param entityId - @zh 销毁的实体 ID @en Despawned entity ID
|
||||||
|
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||||
|
*/
|
||||||
|
export function encodeDespawn(entityId: number): Uint8Array {
|
||||||
|
const writer = new BinaryWriter(8);
|
||||||
|
|
||||||
|
writer.writeUint8(SyncOperation.DESPAWN);
|
||||||
|
writer.writeUint32(entityId);
|
||||||
|
|
||||||
|
return writer.toUint8Array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码批量实体销毁消息
|
||||||
|
* @en Encode batch entity despawn message
|
||||||
|
*
|
||||||
|
* @param entityIds - @zh 销毁的实体 ID 数组 @en Despawned entity IDs
|
||||||
|
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||||
|
*/
|
||||||
|
export function encodeDespawnBatch(entityIds: number[]): Uint8Array {
|
||||||
|
const writer = new BinaryWriter(8 + entityIds.length * 4);
|
||||||
|
|
||||||
|
writer.writeUint8(SyncOperation.DESPAWN);
|
||||||
|
writer.writeVarint(entityIds.length);
|
||||||
|
|
||||||
|
for (const id of entityIds) {
|
||||||
|
writer.writeUint32(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.toUint8Array();
|
||||||
|
}
|
||||||
50
packages/framework/core/src/ECS/Sync/encoding/index.ts
Normal file
50
packages/framework/core/src/ECS/Sync/encoding/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* @zh 二进制编解码模块
|
||||||
|
* @en Binary encoding/decoding module
|
||||||
|
*
|
||||||
|
* @zh 提供 ECS Component 状态的二进制序列化和反序列化功能
|
||||||
|
* @en Provides binary serialization and deserialization for ECS Component state
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Variable-length integer encoding
|
||||||
|
export {
|
||||||
|
varintSize,
|
||||||
|
encodeVarint,
|
||||||
|
decodeVarint,
|
||||||
|
zigzagEncode,
|
||||||
|
zigzagDecode,
|
||||||
|
encodeSignedVarint,
|
||||||
|
decodeSignedVarint
|
||||||
|
} from './varint';
|
||||||
|
|
||||||
|
// Binary writer/reader
|
||||||
|
export { BinaryWriter } from './BinaryWriter';
|
||||||
|
export { BinaryReader } from './BinaryReader';
|
||||||
|
|
||||||
|
// Encoder
|
||||||
|
export {
|
||||||
|
encodeComponentFull,
|
||||||
|
encodeComponentDelta,
|
||||||
|
encodeEntity,
|
||||||
|
encodeSnapshot,
|
||||||
|
encodeSpawn,
|
||||||
|
encodeDespawn,
|
||||||
|
encodeDespawnBatch
|
||||||
|
} from './Encoder';
|
||||||
|
|
||||||
|
// Decoder
|
||||||
|
export {
|
||||||
|
decodeComponent,
|
||||||
|
decodeEntity,
|
||||||
|
decodeSnapshot,
|
||||||
|
decodeSpawn,
|
||||||
|
decodeDespawn,
|
||||||
|
processDespawn
|
||||||
|
} from './Decoder';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
DecodeEntityResult,
|
||||||
|
DecodeSnapshotResult,
|
||||||
|
DecodeSpawnResult,
|
||||||
|
DecodeDespawnResult
|
||||||
|
} from './Decoder';
|
||||||
137
packages/framework/core/src/ECS/Sync/encoding/varint.ts
Normal file
137
packages/framework/core/src/ECS/Sync/encoding/varint.ts
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* @zh 变长整数编解码
|
||||||
|
* @en Variable-length integer encoding/decoding
|
||||||
|
*
|
||||||
|
* @zh 使用 LEB128 编码方式,可变长度编码正整数。
|
||||||
|
* 小数值使用更少字节,大数值使用更多字节。
|
||||||
|
* @en Uses LEB128 encoding for variable-length integer encoding.
|
||||||
|
* Small values use fewer bytes, large values use more bytes.
|
||||||
|
*
|
||||||
|
* | 值范围 | 字节数 |
|
||||||
|
* |--------|--------|
|
||||||
|
* | 0-127 | 1 |
|
||||||
|
* | 128-16383 | 2 |
|
||||||
|
* | 16384-2097151 | 3 |
|
||||||
|
* | 2097152-268435455 | 4 |
|
||||||
|
* | 268435456+ | 5 |
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 计算变长整数所需的字节数
|
||||||
|
* @en Calculate bytes needed for a varint
|
||||||
|
*
|
||||||
|
* @param value - @zh 整数值 @en Integer value
|
||||||
|
* @returns @zh 所需字节数 @en Bytes needed
|
||||||
|
*/
|
||||||
|
export function varintSize(value: number): number {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new Error('Varint only supports non-negative integers');
|
||||||
|
}
|
||||||
|
if (value < 128) return 1;
|
||||||
|
if (value < 16384) return 2;
|
||||||
|
if (value < 2097152) return 3;
|
||||||
|
if (value < 268435456) return 4;
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码变长整数到字节数组
|
||||||
|
* @en Encode varint to byte array
|
||||||
|
*
|
||||||
|
* @param value - @zh 要编码的整数 @en Integer to encode
|
||||||
|
* @param buffer - @zh 目标缓冲区 @en Target buffer
|
||||||
|
* @param offset - @zh 写入偏移 @en Write offset
|
||||||
|
* @returns @zh 写入后的新偏移 @en New offset after writing
|
||||||
|
*/
|
||||||
|
export function encodeVarint(value: number, buffer: Uint8Array, offset: number): number {
|
||||||
|
if (value < 0) {
|
||||||
|
throw new Error('Varint only supports non-negative integers');
|
||||||
|
}
|
||||||
|
|
||||||
|
while (value >= 0x80) {
|
||||||
|
buffer[offset++] = (value & 0x7F) | 0x80;
|
||||||
|
value >>>= 7;
|
||||||
|
}
|
||||||
|
buffer[offset++] = value;
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 从字节数组解码变长整数
|
||||||
|
* @en Decode varint from byte array
|
||||||
|
*
|
||||||
|
* @param buffer - @zh 源缓冲区 @en Source buffer
|
||||||
|
* @param offset - @zh 读取偏移 @en Read offset
|
||||||
|
* @returns @zh [解码值, 新偏移] @en [decoded value, new offset]
|
||||||
|
*/
|
||||||
|
export function decodeVarint(buffer: Uint8Array, offset: number): [number, number] {
|
||||||
|
let result = 0;
|
||||||
|
let shift = 0;
|
||||||
|
let byte: number;
|
||||||
|
|
||||||
|
do {
|
||||||
|
if (offset >= buffer.length) {
|
||||||
|
throw new Error('Varint decode: buffer overflow');
|
||||||
|
}
|
||||||
|
byte = buffer[offset++]!;
|
||||||
|
result |= (byte & 0x7F) << shift;
|
||||||
|
shift += 7;
|
||||||
|
} while (byte >= 0x80);
|
||||||
|
|
||||||
|
return [result, offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码有符号整数(ZigZag 编码)
|
||||||
|
* @en Encode signed integer (ZigZag encoding)
|
||||||
|
*
|
||||||
|
* @zh ZigZag 编码将有符号整数映射到无符号整数:
|
||||||
|
* 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
|
||||||
|
* 这样小的负数也能用较少字节表示。
|
||||||
|
* @en ZigZag encoding maps signed integers to unsigned:
|
||||||
|
* 0 → 0, -1 → 1, 1 → 2, -2 → 3, 2 → 4, ...
|
||||||
|
* This allows small negative numbers to use fewer bytes.
|
||||||
|
*
|
||||||
|
* @param value - @zh 有符号整数 @en Signed integer
|
||||||
|
* @returns @zh ZigZag 编码后的值 @en ZigZag encoded value
|
||||||
|
*/
|
||||||
|
export function zigzagEncode(value: number): number {
|
||||||
|
return (value << 1) ^ (value >> 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码有符号整数(ZigZag 解码)
|
||||||
|
* @en Decode signed integer (ZigZag decoding)
|
||||||
|
*
|
||||||
|
* @param value - @zh ZigZag 编码的值 @en ZigZag encoded value
|
||||||
|
* @returns @zh 原始有符号整数 @en Original signed integer
|
||||||
|
*/
|
||||||
|
export function zigzagDecode(value: number): number {
|
||||||
|
return (value >>> 1) ^ -(value & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码有符号变长整数
|
||||||
|
* @en Encode signed varint
|
||||||
|
*
|
||||||
|
* @param value - @zh 有符号整数 @en Signed integer
|
||||||
|
* @param buffer - @zh 目标缓冲区 @en Target buffer
|
||||||
|
* @param offset - @zh 写入偏移 @en Write offset
|
||||||
|
* @returns @zh 写入后的新偏移 @en New offset after writing
|
||||||
|
*/
|
||||||
|
export function encodeSignedVarint(value: number, buffer: Uint8Array, offset: number): number {
|
||||||
|
return encodeVarint(zigzagEncode(value), buffer, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 解码有符号变长整数
|
||||||
|
* @en Decode signed varint
|
||||||
|
*
|
||||||
|
* @param buffer - @zh 源缓冲区 @en Source buffer
|
||||||
|
* @param offset - @zh 读取偏移 @en Read offset
|
||||||
|
* @returns @zh [解码值, 新偏移] @en [decoded value, new offset]
|
||||||
|
*/
|
||||||
|
export function decodeSignedVarint(buffer: Uint8Array, offset: number): [number, number] {
|
||||||
|
const [encoded, newOffset] = decodeVarint(buffer, offset);
|
||||||
|
return [zigzagDecode(encoded), newOffset];
|
||||||
|
}
|
||||||
65
packages/framework/core/src/ECS/Sync/index.ts
Normal file
65
packages/framework/core/src/ECS/Sync/index.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @zh ECS 网络同步模块
|
||||||
|
* @en ECS Network Synchronization Module
|
||||||
|
*
|
||||||
|
* @zh 提供基于 ECS Component 的网络状态同步功能:
|
||||||
|
* - @sync 装饰器:标记需要同步的字段
|
||||||
|
* - ChangeTracker:追踪字段级变更
|
||||||
|
* - 二进制编解码器:高效的网络序列化
|
||||||
|
*
|
||||||
|
* @en Provides network state synchronization based on ECS Components:
|
||||||
|
* - @sync decorator: Mark fields for synchronization
|
||||||
|
* - ChangeTracker: Track field-level changes
|
||||||
|
* - Binary encoder/decoder: Efficient network serialization
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```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;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export {
|
||||||
|
SyncType,
|
||||||
|
SyncFieldMetadata,
|
||||||
|
SyncMetadata,
|
||||||
|
SyncOperation,
|
||||||
|
TYPE_SIZES,
|
||||||
|
SYNC_METADATA,
|
||||||
|
CHANGE_TRACKER
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
// Change Tracker
|
||||||
|
export { ChangeTracker } from './ChangeTracker';
|
||||||
|
|
||||||
|
// Decorators
|
||||||
|
export {
|
||||||
|
sync,
|
||||||
|
getSyncMetadata,
|
||||||
|
hasSyncFields,
|
||||||
|
getChangeTracker,
|
||||||
|
initChangeTracker,
|
||||||
|
clearChanges,
|
||||||
|
hasChanges
|
||||||
|
} from './decorators';
|
||||||
|
|
||||||
|
// Network Entity Decorator
|
||||||
|
export {
|
||||||
|
NetworkEntity,
|
||||||
|
getNetworkEntityMetadata,
|
||||||
|
isNetworkEntity,
|
||||||
|
NETWORK_ENTITY_METADATA,
|
||||||
|
type NetworkEntityMetadata,
|
||||||
|
type NetworkEntityOptions
|
||||||
|
} from './NetworkEntityDecorator';
|
||||||
|
|
||||||
|
// Encoding
|
||||||
|
export * from './encoding';
|
||||||
127
packages/framework/core/src/ECS/Sync/types.ts
Normal file
127
packages/framework/core/src/ECS/Sync/types.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
/**
|
||||||
|
* @zh 网络同步类型定义
|
||||||
|
* @en Network synchronization type definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 支持的同步数据类型
|
||||||
|
* @en Supported sync data types
|
||||||
|
*/
|
||||||
|
export type SyncType =
|
||||||
|
| 'boolean'
|
||||||
|
| 'int8'
|
||||||
|
| 'uint8'
|
||||||
|
| 'int16'
|
||||||
|
| 'uint16'
|
||||||
|
| 'int32'
|
||||||
|
| 'uint32'
|
||||||
|
| 'float32'
|
||||||
|
| 'float64'
|
||||||
|
| 'string';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步字段元数据
|
||||||
|
* @en Sync field metadata
|
||||||
|
*/
|
||||||
|
export interface SyncFieldMetadata {
|
||||||
|
/**
|
||||||
|
* @zh 字段索引(用于二进制编码)
|
||||||
|
* @en Field index (for binary encoding)
|
||||||
|
*/
|
||||||
|
index: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 字段名称
|
||||||
|
* @en Field name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 字段类型
|
||||||
|
* @en Field type
|
||||||
|
*/
|
||||||
|
type: SyncType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步元数据
|
||||||
|
* @en Component sync metadata
|
||||||
|
*/
|
||||||
|
export interface SyncMetadata {
|
||||||
|
/**
|
||||||
|
* @zh 组件类型 ID
|
||||||
|
* @en Component type ID
|
||||||
|
*/
|
||||||
|
typeId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步字段列表(按索引排序)
|
||||||
|
* @en Sync fields list (sorted by index)
|
||||||
|
*/
|
||||||
|
fields: SyncFieldMetadata[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 字段名到索引的映射
|
||||||
|
* @en Field name to index mapping
|
||||||
|
*/
|
||||||
|
fieldIndexMap: Map<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步操作类型
|
||||||
|
* @en Sync operation type
|
||||||
|
*/
|
||||||
|
export enum SyncOperation {
|
||||||
|
/**
|
||||||
|
* @zh 完整快照
|
||||||
|
* @en Full snapshot
|
||||||
|
*/
|
||||||
|
FULL = 0,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 增量更新
|
||||||
|
* @en Delta update
|
||||||
|
*/
|
||||||
|
DELTA = 1,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 实体生成
|
||||||
|
* @en Entity spawn
|
||||||
|
*/
|
||||||
|
SPAWN = 2,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 实体销毁
|
||||||
|
* @en Entity despawn
|
||||||
|
*/
|
||||||
|
DESPAWN = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 各类型的字节大小
|
||||||
|
* @en Byte size for each type
|
||||||
|
*/
|
||||||
|
export const TYPE_SIZES: Record<SyncType, number> = {
|
||||||
|
boolean: 1,
|
||||||
|
int8: 1,
|
||||||
|
uint8: 1,
|
||||||
|
int16: 2,
|
||||||
|
uint16: 2,
|
||||||
|
int32: 4,
|
||||||
|
uint32: 4,
|
||||||
|
float32: 4,
|
||||||
|
float64: 8,
|
||||||
|
string: -1, // 动态长度 | dynamic length
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步元数据的 Symbol 键
|
||||||
|
* @en Symbol key for sync metadata
|
||||||
|
*/
|
||||||
|
export const SYNC_METADATA = Symbol('SyncMetadata');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 变更追踪器的 Symbol 键
|
||||||
|
* @en Symbol key for change tracker
|
||||||
|
*/
|
||||||
|
export const CHANGE_TRACKER = Symbol('ChangeTracker');
|
||||||
@@ -317,9 +317,7 @@ export class WorldManager implements IService {
|
|||||||
/**
|
/**
|
||||||
* @zh 更新所有活跃的World
|
* @zh 更新所有活跃的World
|
||||||
* @en Update all active Worlds
|
* @en Update all active Worlds
|
||||||
*
|
* @internal 由 Core.update() 调用,用户不应直接调用
|
||||||
* @zh 应该在每帧的游戏循环中调用
|
|
||||||
* @en Should be called in each frame of game loop
|
|
||||||
*/
|
*/
|
||||||
public updateAll(): void {
|
public updateAll(): void {
|
||||||
if (!this._isRunning) return;
|
if (!this._isRunning) return;
|
||||||
|
|||||||
@@ -57,3 +57,6 @@ export { EpochManager } from './Core/EpochManager';
|
|||||||
// Compiled Query
|
// Compiled Query
|
||||||
export { CompiledQuery } from './Core/Query/CompiledQuery';
|
export { CompiledQuery } from './Core/Query/CompiledQuery';
|
||||||
export type { InstanceTypes } from './Core/Query/CompiledQuery';
|
export type { InstanceTypes } from './Core/Query/CompiledQuery';
|
||||||
|
|
||||||
|
// Network Synchronization
|
||||||
|
export * from './Sync';
|
||||||
|
|||||||
172
packages/framework/core/tests/ECS/Sync/ChangeTracker.test.ts
Normal file
172
packages/framework/core/tests/ECS/Sync/ChangeTracker.test.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { ChangeTracker } from '../../../src/ECS/Sync/ChangeTracker';
|
||||||
|
|
||||||
|
describe('ChangeTracker - 变更追踪器测试', () => {
|
||||||
|
let tracker: ChangeTracker;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tracker = new ChangeTracker();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('基本功能', () => {
|
||||||
|
test('初始状态应该没有变更', () => {
|
||||||
|
expect(tracker.hasChanges()).toBe(false);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(0);
|
||||||
|
expect(tracker.getDirtyFields()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setDirty 应该标记字段为脏', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(true);
|
||||||
|
expect(tracker.isDirty(0)).toBe(true);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(1);
|
||||||
|
expect(tracker.getDirtyFields()).toEqual([0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('多次 setDirty 同一字段应该只记录一次', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(0);
|
||||||
|
|
||||||
|
expect(tracker.getDirtyCount()).toBe(1);
|
||||||
|
expect(tracker.getDirtyFields()).toEqual([0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('setDirty 不同字段应该都被记录', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(1);
|
||||||
|
tracker.setDirty(2);
|
||||||
|
|
||||||
|
expect(tracker.getDirtyCount()).toBe(3);
|
||||||
|
expect(tracker.getDirtyFields().sort()).toEqual([0, 1, 2]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDirty 方法', () => {
|
||||||
|
test('未标记的字段应该返回 false', () => {
|
||||||
|
expect(tracker.isDirty(0)).toBe(false);
|
||||||
|
expect(tracker.isDirty(5)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('已标记的字段应该返回 true', () => {
|
||||||
|
tracker.setDirty(3);
|
||||||
|
|
||||||
|
expect(tracker.isDirty(3)).toBe(true);
|
||||||
|
expect(tracker.isDirty(0)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clear 方法', () => {
|
||||||
|
test('clear 应该清除所有变更', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(1);
|
||||||
|
tracker.setDirty(2);
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(true);
|
||||||
|
|
||||||
|
tracker.clear();
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(false);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(0);
|
||||||
|
expect(tracker.getDirtyFields()).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clear 应该更新 lastSyncTime', () => {
|
||||||
|
const before = tracker.lastSyncTime;
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.clear();
|
||||||
|
|
||||||
|
expect(tracker.lastSyncTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearField 方法', () => {
|
||||||
|
test('clearField 应该只清除指定字段', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(1);
|
||||||
|
tracker.setDirty(2);
|
||||||
|
|
||||||
|
tracker.clearField(1);
|
||||||
|
|
||||||
|
expect(tracker.isDirty(0)).toBe(true);
|
||||||
|
expect(tracker.isDirty(1)).toBe(false);
|
||||||
|
expect(tracker.isDirty(2)).toBe(true);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('清除最后一个字段应该使 hasChanges 返回 false', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
expect(tracker.hasChanges()).toBe(true);
|
||||||
|
|
||||||
|
tracker.clearField(0);
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('markAllDirty 方法', () => {
|
||||||
|
test('markAllDirty 应该标记所有字段', () => {
|
||||||
|
tracker.markAllDirty(5);
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(true);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(5);
|
||||||
|
expect(tracker.getDirtyFields().sort()).toEqual([0, 1, 2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markAllDirty(0) 应该没有变更', () => {
|
||||||
|
tracker.markAllDirty(0);
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(false);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markAllDirty 用于首次同步', () => {
|
||||||
|
tracker.markAllDirty(3);
|
||||||
|
|
||||||
|
expect(tracker.isDirty(0)).toBe(true);
|
||||||
|
expect(tracker.isDirty(1)).toBe(true);
|
||||||
|
expect(tracker.isDirty(2)).toBe(true);
|
||||||
|
expect(tracker.isDirty(3)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('reset 方法', () => {
|
||||||
|
test('reset 应该重置所有状态', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(1);
|
||||||
|
tracker.clear();
|
||||||
|
|
||||||
|
tracker.reset();
|
||||||
|
|
||||||
|
expect(tracker.hasChanges()).toBe(false);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(0);
|
||||||
|
expect(tracker.lastSyncTime).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况', () => {
|
||||||
|
test('大量字段标记应该正常工作', () => {
|
||||||
|
const fieldCount = 1000;
|
||||||
|
|
||||||
|
for (let i = 0; i < fieldCount; i++) {
|
||||||
|
tracker.setDirty(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(tracker.getDirtyCount()).toBe(fieldCount);
|
||||||
|
expect(tracker.hasChanges()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('交替设置和清除应该正常工作', () => {
|
||||||
|
tracker.setDirty(0);
|
||||||
|
tracker.setDirty(1);
|
||||||
|
tracker.clearField(0);
|
||||||
|
tracker.setDirty(2);
|
||||||
|
tracker.clearField(1);
|
||||||
|
|
||||||
|
expect(tracker.isDirty(0)).toBe(false);
|
||||||
|
expect(tracker.isDirty(1)).toBe(false);
|
||||||
|
expect(tracker.isDirty(2)).toBe(true);
|
||||||
|
expect(tracker.getDirtyCount()).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
327
packages/framework/core/tests/ECS/Sync/decorators.test.ts
Normal file
327
packages/framework/core/tests/ECS/Sync/decorators.test.ts
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ECSComponent } from '../../../src/ECS/Decorators';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import {
|
||||||
|
sync,
|
||||||
|
getSyncMetadata,
|
||||||
|
hasSyncFields,
|
||||||
|
getChangeTracker,
|
||||||
|
initChangeTracker,
|
||||||
|
clearChanges,
|
||||||
|
hasChanges
|
||||||
|
} from '../../../src/ECS/Sync/decorators';
|
||||||
|
import { SYNC_METADATA, CHANGE_TRACKER } from '../../../src/ECS/Sync/types';
|
||||||
|
|
||||||
|
@ECSComponent('SyncTest_PlayerComponent')
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@sync("string") name: string = "";
|
||||||
|
@sync("uint16") score: number = 0;
|
||||||
|
@sync("float32") x: number = 0;
|
||||||
|
@sync("float32") y: number = 0;
|
||||||
|
|
||||||
|
localData: string = "not synced";
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('SyncTest_SimpleComponent')
|
||||||
|
class SimpleComponent extends Component {
|
||||||
|
@sync("boolean") active: boolean = true;
|
||||||
|
@sync("int32") value: number = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('SyncTest_NoSyncComponent')
|
||||||
|
class NoSyncComponent extends Component {
|
||||||
|
localValue: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('SyncTest_AllTypesComponent')
|
||||||
|
class AllTypesComponent extends Component {
|
||||||
|
@sync("boolean") boolField: boolean = false;
|
||||||
|
@sync("int8") int8Field: number = 0;
|
||||||
|
@sync("uint8") uint8Field: number = 0;
|
||||||
|
@sync("int16") int16Field: number = 0;
|
||||||
|
@sync("uint16") uint16Field: number = 0;
|
||||||
|
@sync("int32") int32Field: number = 0;
|
||||||
|
@sync("uint32") uint32Field: number = 0;
|
||||||
|
@sync("float32") float32Field: number = 0;
|
||||||
|
@sync("float64") float64Field: number = 0;
|
||||||
|
@sync("string") stringField: string = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('@sync 装饰器测试', () => {
|
||||||
|
describe('getSyncMetadata', () => {
|
||||||
|
test('应该返回带 @sync 字段的组件元数据', () => {
|
||||||
|
const metadata = getSyncMetadata(PlayerComponent);
|
||||||
|
|
||||||
|
expect(metadata).not.toBeNull();
|
||||||
|
expect(metadata!.typeId).toBe('SyncTest_PlayerComponent');
|
||||||
|
expect(metadata!.fields.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该正确记录字段信息', () => {
|
||||||
|
const metadata = getSyncMetadata(PlayerComponent);
|
||||||
|
|
||||||
|
const nameField = metadata!.fields.find(f => f.name === 'name');
|
||||||
|
expect(nameField).toBeDefined();
|
||||||
|
expect(nameField!.type).toBe('string');
|
||||||
|
expect(nameField!.index).toBe(0);
|
||||||
|
|
||||||
|
const scoreField = metadata!.fields.find(f => f.name === 'score');
|
||||||
|
expect(scoreField).toBeDefined();
|
||||||
|
expect(scoreField!.type).toBe('uint16');
|
||||||
|
|
||||||
|
const xField = metadata!.fields.find(f => f.name === 'x');
|
||||||
|
expect(xField).toBeDefined();
|
||||||
|
expect(xField!.type).toBe('float32');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('没有 @sync 字段的组件应该返回 null', () => {
|
||||||
|
const metadata = getSyncMetadata(NoSyncComponent);
|
||||||
|
|
||||||
|
expect(metadata).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('可以从实例获取元数据', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
const metadata = getSyncMetadata(component);
|
||||||
|
|
||||||
|
expect(metadata).not.toBeNull();
|
||||||
|
expect(metadata!.fields.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fieldIndexMap 应该正确映射字段名到索引', () => {
|
||||||
|
const metadata = getSyncMetadata(PlayerComponent);
|
||||||
|
|
||||||
|
expect(metadata!.fieldIndexMap.get('name')).toBe(0);
|
||||||
|
expect(metadata!.fieldIndexMap.get('score')).toBe(1);
|
||||||
|
expect(metadata!.fieldIndexMap.get('x')).toBe(2);
|
||||||
|
expect(metadata!.fieldIndexMap.get('y')).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasSyncFields', () => {
|
||||||
|
test('有 @sync 字段应该返回 true', () => {
|
||||||
|
expect(hasSyncFields(PlayerComponent)).toBe(true);
|
||||||
|
expect(hasSyncFields(new PlayerComponent())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('没有 @sync 字段应该返回 false', () => {
|
||||||
|
expect(hasSyncFields(NoSyncComponent)).toBe(false);
|
||||||
|
expect(hasSyncFields(new NoSyncComponent())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('支持所有同步类型', () => {
|
||||||
|
test('AllTypesComponent 应该有所有类型的字段', () => {
|
||||||
|
const metadata = getSyncMetadata(AllTypesComponent);
|
||||||
|
|
||||||
|
expect(metadata).not.toBeNull();
|
||||||
|
expect(metadata!.fields.length).toBe(10);
|
||||||
|
|
||||||
|
const types = metadata!.fields.map(f => f.type);
|
||||||
|
expect(types).toContain('boolean');
|
||||||
|
expect(types).toContain('int8');
|
||||||
|
expect(types).toContain('uint8');
|
||||||
|
expect(types).toContain('int16');
|
||||||
|
expect(types).toContain('uint16');
|
||||||
|
expect(types).toContain('int32');
|
||||||
|
expect(types).toContain('uint32');
|
||||||
|
expect(types).toContain('float32');
|
||||||
|
expect(types).toContain('float64');
|
||||||
|
expect(types).toContain('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('字段值拦截', () => {
|
||||||
|
test('修改 @sync 字段应该触发变更追踪', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
expect(tracker).not.toBeNull();
|
||||||
|
tracker!.clear();
|
||||||
|
|
||||||
|
component.name = "TestPlayer";
|
||||||
|
|
||||||
|
expect(tracker!.hasChanges()).toBe(true);
|
||||||
|
expect(tracker!.isDirty(0)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('设置相同值不应该触发变更', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
component.name = "Test";
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
tracker!.clear();
|
||||||
|
|
||||||
|
component.name = "Test";
|
||||||
|
|
||||||
|
expect(tracker!.hasChanges()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('修改非 @sync 字段不应该触发变更', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
tracker!.clear();
|
||||||
|
|
||||||
|
component.localData = "new value";
|
||||||
|
|
||||||
|
expect(tracker!.hasChanges()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('多个字段变更应该都被追踪', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
tracker!.clear();
|
||||||
|
|
||||||
|
component.name = "NewName";
|
||||||
|
component.score = 100;
|
||||||
|
component.x = 1.5;
|
||||||
|
|
||||||
|
expect(tracker!.getDirtyCount()).toBe(3);
|
||||||
|
expect(tracker!.isDirty(0)).toBe(true);
|
||||||
|
expect(tracker!.isDirty(1)).toBe(true);
|
||||||
|
expect(tracker!.isDirty(2)).toBe(true);
|
||||||
|
expect(tracker!.isDirty(3)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initChangeTracker', () => {
|
||||||
|
test('应该创建变更追踪器', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
|
||||||
|
expect(getChangeTracker(component)).toBeNull();
|
||||||
|
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
expect(getChangeTracker(component)).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该标记所有字段为脏(用于首次同步)', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
expect(tracker!.hasChanges()).toBe(true);
|
||||||
|
expect(tracker!.getDirtyCount()).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('对没有 @sync 字段的组件应该抛出错误', () => {
|
||||||
|
const component = new NoSyncComponent();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
initChangeTracker(component);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('重复初始化应该重新标记所有字段', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
tracker!.clear();
|
||||||
|
|
||||||
|
expect(tracker!.hasChanges()).toBe(false);
|
||||||
|
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
expect(tracker!.hasChanges()).toBe(true);
|
||||||
|
expect(tracker!.getDirtyCount()).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clearChanges', () => {
|
||||||
|
test('应该清除所有变更标记', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(true);
|
||||||
|
|
||||||
|
clearChanges(component);
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('对没有追踪器的组件应该安全执行', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
clearChanges(component);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasChanges', () => {
|
||||||
|
test('初始化后应该有变更', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('清除后应该没有变更', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
clearChanges(component);
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('修改字段后应该有变更', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
initChangeTracker(component);
|
||||||
|
clearChanges(component);
|
||||||
|
|
||||||
|
component.score = 999;
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('没有追踪器应该返回 false', () => {
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('与实体集成', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('添加到实体的组件应该能正常工作', () => {
|
||||||
|
const entity = scene.createEntity('TestEntity');
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
|
||||||
|
entity.addComponent(component);
|
||||||
|
initChangeTracker(component);
|
||||||
|
|
||||||
|
component.name = "EntityPlayer";
|
||||||
|
component.x = 100;
|
||||||
|
|
||||||
|
const tracker = getChangeTracker(component);
|
||||||
|
expect(tracker!.hasChanges()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('从实体获取的组件应该保持追踪状态', () => {
|
||||||
|
const entity = scene.createEntity('TestEntity');
|
||||||
|
const component = new PlayerComponent();
|
||||||
|
|
||||||
|
entity.addComponent(component);
|
||||||
|
initChangeTracker(component);
|
||||||
|
clearChanges(component);
|
||||||
|
|
||||||
|
const retrieved = entity.getComponent(PlayerComponent);
|
||||||
|
retrieved!.score = 50;
|
||||||
|
|
||||||
|
expect(hasChanges(component)).toBe(true);
|
||||||
|
expect(hasChanges(retrieved!)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
530
packages/framework/core/tests/ECS/Sync/encoding.test.ts
Normal file
530
packages/framework/core/tests/ECS/Sync/encoding.test.ts
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
import { BinaryWriter } from '../../../src/ECS/Sync/encoding/BinaryWriter';
|
||||||
|
import { BinaryReader } from '../../../src/ECS/Sync/encoding/BinaryReader';
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ECSComponent } from '../../../src/ECS/Decorators';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { sync, initChangeTracker, clearChanges } from '../../../src/ECS/Sync/decorators';
|
||||||
|
import {
|
||||||
|
encodeSnapshot,
|
||||||
|
encodeSpawn,
|
||||||
|
encodeDespawn,
|
||||||
|
encodeDespawnBatch
|
||||||
|
} from '../../../src/ECS/Sync/encoding/Encoder';
|
||||||
|
import {
|
||||||
|
decodeSnapshot,
|
||||||
|
decodeSpawn,
|
||||||
|
processDespawn
|
||||||
|
} from '../../../src/ECS/Sync/encoding/Decoder';
|
||||||
|
import { SyncOperation } from '../../../src/ECS/Sync/types';
|
||||||
|
|
||||||
|
@ECSComponent('EncodingTest_PlayerComponent')
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@sync("string") name: string = "";
|
||||||
|
@sync("uint16") score: number = 0;
|
||||||
|
@sync("float32") x: number = 0;
|
||||||
|
@sync("float32") y: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('EncodingTest_AllTypesComponent')
|
||||||
|
class AllTypesComponent extends Component {
|
||||||
|
@sync("boolean") boolField: boolean = false;
|
||||||
|
@sync("int8") int8Field: number = 0;
|
||||||
|
@sync("uint8") uint8Field: number = 0;
|
||||||
|
@sync("int16") int16Field: number = 0;
|
||||||
|
@sync("uint16") uint16Field: number = 0;
|
||||||
|
@sync("int32") int32Field: number = 0;
|
||||||
|
@sync("uint32") uint32Field: number = 0;
|
||||||
|
@sync("float32") float32Field: number = 0;
|
||||||
|
@sync("float64") float64Field: number = 0;
|
||||||
|
@sync("string") stringField: string = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('BinaryWriter/BinaryReader - 二进制读写器测试', () => {
|
||||||
|
describe('基本数值类型', () => {
|
||||||
|
test('writeUint8/readUint8', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint8(0);
|
||||||
|
writer.writeUint8(127);
|
||||||
|
writer.writeUint8(255);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readUint8()).toBe(0);
|
||||||
|
expect(reader.readUint8()).toBe(127);
|
||||||
|
expect(reader.readUint8()).toBe(255);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeInt8/readInt8', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeInt8(-128);
|
||||||
|
writer.writeInt8(0);
|
||||||
|
writer.writeInt8(127);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readInt8()).toBe(-128);
|
||||||
|
expect(reader.readInt8()).toBe(0);
|
||||||
|
expect(reader.readInt8()).toBe(127);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeBoolean/readBoolean', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeBoolean(true);
|
||||||
|
writer.writeBoolean(false);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readBoolean()).toBe(true);
|
||||||
|
expect(reader.readBoolean()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeUint16/readUint16', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint16(0);
|
||||||
|
writer.writeUint16(32767);
|
||||||
|
writer.writeUint16(65535);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readUint16()).toBe(0);
|
||||||
|
expect(reader.readUint16()).toBe(32767);
|
||||||
|
expect(reader.readUint16()).toBe(65535);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeInt16/readInt16', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeInt16(-32768);
|
||||||
|
writer.writeInt16(0);
|
||||||
|
writer.writeInt16(32767);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readInt16()).toBe(-32768);
|
||||||
|
expect(reader.readInt16()).toBe(0);
|
||||||
|
expect(reader.readInt16()).toBe(32767);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeUint32/readUint32', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint32(0);
|
||||||
|
writer.writeUint32(2147483647);
|
||||||
|
writer.writeUint32(4294967295);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readUint32()).toBe(0);
|
||||||
|
expect(reader.readUint32()).toBe(2147483647);
|
||||||
|
expect(reader.readUint32()).toBe(4294967295);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeInt32/readInt32', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeInt32(-2147483648);
|
||||||
|
writer.writeInt32(0);
|
||||||
|
writer.writeInt32(2147483647);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readInt32()).toBe(-2147483648);
|
||||||
|
expect(reader.readInt32()).toBe(0);
|
||||||
|
expect(reader.readInt32()).toBe(2147483647);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeFloat32/readFloat32', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeFloat32(0);
|
||||||
|
writer.writeFloat32(3.14);
|
||||||
|
writer.writeFloat32(-100.5);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readFloat32()).toBe(0);
|
||||||
|
expect(reader.readFloat32()).toBeCloseTo(3.14, 5);
|
||||||
|
expect(reader.readFloat32()).toBeCloseTo(-100.5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writeFloat64/readFloat64', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeFloat64(0);
|
||||||
|
writer.writeFloat64(Math.PI);
|
||||||
|
writer.writeFloat64(-1e100);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readFloat64()).toBe(0);
|
||||||
|
expect(reader.readFloat64()).toBe(Math.PI);
|
||||||
|
expect(reader.readFloat64()).toBe(-1e100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('变长整数 (Varint)', () => {
|
||||||
|
test('小值 (1字节)', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeVarint(0);
|
||||||
|
writer.writeVarint(127);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readVarint()).toBe(0);
|
||||||
|
expect(reader.readVarint()).toBe(127);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('中等值 (2字节)', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeVarint(128);
|
||||||
|
writer.writeVarint(16383);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readVarint()).toBe(128);
|
||||||
|
expect(reader.readVarint()).toBe(16383);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('大值 (多字节)', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeVarint(16384);
|
||||||
|
writer.writeVarint(1000000);
|
||||||
|
writer.writeVarint(2147483647);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readVarint()).toBe(16384);
|
||||||
|
expect(reader.readVarint()).toBe(1000000);
|
||||||
|
expect(reader.readVarint()).toBe(2147483647);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('字符串', () => {
|
||||||
|
test('空字符串', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeString("");
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readString()).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ASCII 字符串', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeString("Hello, World!");
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readString()).toBe("Hello, World!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unicode 字符串', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeString("你好世界");
|
||||||
|
writer.writeString("日本語テスト");
|
||||||
|
writer.writeString("emoji: 🎮🎯");
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readString()).toBe("你好世界");
|
||||||
|
expect(reader.readString()).toBe("日本語テスト");
|
||||||
|
expect(reader.readString()).toBe("emoji: 🎮🎯");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('混合字符串', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeString("Player_玩家_プレイヤー");
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.readString()).toBe("Player_玩家_プレイヤー");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('字节数组', () => {
|
||||||
|
test('writeBytes/readBytes', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
||||||
|
writer.writeBytes(data);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
const result = reader.readBytes(5);
|
||||||
|
expect(Array.from(result)).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BinaryReader 辅助方法', () => {
|
||||||
|
test('remaining 应该返回剩余字节数', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint32(100);
|
||||||
|
writer.writeUint32(200);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.remaining).toBe(8);
|
||||||
|
|
||||||
|
reader.readUint32();
|
||||||
|
expect(reader.remaining).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasMore 应该正确判断', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint8(1);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.hasMore()).toBe(true);
|
||||||
|
|
||||||
|
reader.readUint8();
|
||||||
|
expect(reader.hasMore()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('peekUint8 不应该移动读取位置', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint8(42);
|
||||||
|
writer.writeUint8(99);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
expect(reader.peekUint8()).toBe(42);
|
||||||
|
expect(reader.peekUint8()).toBe(42);
|
||||||
|
expect(reader.offset).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skip 应该跳过指定字节', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint8(1);
|
||||||
|
writer.writeUint8(2);
|
||||||
|
writer.writeUint8(3);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
reader.skip(2);
|
||||||
|
expect(reader.readUint8()).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('读取超出范围应该抛出错误', () => {
|
||||||
|
const reader = new BinaryReader(new Uint8Array([1, 2]));
|
||||||
|
|
||||||
|
expect(() => reader.readUint32()).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('BinaryWriter 自动扩容', () => {
|
||||||
|
test('应该自动扩容', () => {
|
||||||
|
const writer = new BinaryWriter(4);
|
||||||
|
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
writer.writeUint32(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(writer.offset).toBe(400);
|
||||||
|
|
||||||
|
const reader = new BinaryReader(writer.toUint8Array());
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
expect(reader.readUint32()).toBe(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('reset 应该清空数据但保留缓冲区', () => {
|
||||||
|
const writer = new BinaryWriter();
|
||||||
|
writer.writeUint32(100);
|
||||||
|
writer.writeUint32(200);
|
||||||
|
|
||||||
|
expect(writer.offset).toBe(8);
|
||||||
|
|
||||||
|
writer.reset();
|
||||||
|
|
||||||
|
expect(writer.offset).toBe(0);
|
||||||
|
expect(writer.toUint8Array().length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Encoder/Decoder - 实体编解码测试', () => {
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
// Components are auto-registered via @ECSComponent decorator
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scene = new Scene();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodeSnapshot/decodeSnapshot', () => {
|
||||||
|
test('应该编码和解码单个实体', () => {
|
||||||
|
const entity = scene.createEntity('Player1');
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
comp.name = "TestPlayer";
|
||||||
|
comp.score = 100;
|
||||||
|
comp.x = 10.5;
|
||||||
|
comp.y = 20.5;
|
||||||
|
initChangeTracker(comp);
|
||||||
|
|
||||||
|
const data = encodeSnapshot([entity], SyncOperation.FULL);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const result = decodeSnapshot(targetScene, data);
|
||||||
|
|
||||||
|
expect(result.operation).toBe(SyncOperation.FULL);
|
||||||
|
expect(result.entities.length).toBe(1);
|
||||||
|
expect(result.entities[0].isNew).toBe(true);
|
||||||
|
|
||||||
|
const decodedEntity = targetScene.entities.buffer[0];
|
||||||
|
expect(decodedEntity).toBeDefined();
|
||||||
|
|
||||||
|
const decodedComp = decodedEntity!.getComponent(PlayerComponent);
|
||||||
|
expect(decodedComp).not.toBeNull();
|
||||||
|
expect(decodedComp!.name).toBe("TestPlayer");
|
||||||
|
expect(decodedComp!.score).toBe(100);
|
||||||
|
expect(decodedComp!.x).toBeCloseTo(10.5, 5);
|
||||||
|
expect(decodedComp!.y).toBeCloseTo(20.5, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该编码和解码多个实体', () => {
|
||||||
|
const entity1 = scene.createEntity('Player1');
|
||||||
|
const comp1 = entity1.addComponent(new PlayerComponent());
|
||||||
|
comp1.name = "Player1";
|
||||||
|
comp1.score = 50;
|
||||||
|
initChangeTracker(comp1);
|
||||||
|
|
||||||
|
const entity2 = scene.createEntity('Player2');
|
||||||
|
const comp2 = entity2.addComponent(new PlayerComponent());
|
||||||
|
comp2.name = "Player2";
|
||||||
|
comp2.score = 100;
|
||||||
|
initChangeTracker(comp2);
|
||||||
|
|
||||||
|
const data = encodeSnapshot([entity1, entity2], SyncOperation.FULL);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const result = decodeSnapshot(targetScene, data);
|
||||||
|
|
||||||
|
expect(result.entities.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELTA 操作应该只编码变更的字段', () => {
|
||||||
|
const entity = scene.createEntity('Player1');
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
comp.name = "TestPlayer";
|
||||||
|
comp.score = 0;
|
||||||
|
initChangeTracker(comp);
|
||||||
|
clearChanges(comp);
|
||||||
|
|
||||||
|
comp.score = 200;
|
||||||
|
|
||||||
|
const deltaData = encodeSnapshot([entity], SyncOperation.DELTA);
|
||||||
|
|
||||||
|
expect(deltaData[0]).toBe(SyncOperation.DELTA);
|
||||||
|
expect(deltaData.length).toBeLessThan(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodeSpawn/decodeSpawn', () => {
|
||||||
|
test('应该编码和解码实体生成', () => {
|
||||||
|
const entity = scene.createEntity('SpawnedEntity');
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
comp.name = "SpawnedPlayer";
|
||||||
|
comp.score = 50;
|
||||||
|
comp.x = 100;
|
||||||
|
comp.y = 200;
|
||||||
|
initChangeTracker(comp);
|
||||||
|
|
||||||
|
const data = encodeSpawn(entity, 'Player');
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const result = decodeSpawn(targetScene, data);
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result!.prefabType).toBe('Player');
|
||||||
|
expect(result!.componentTypes).toContain('EncodingTest_PlayerComponent');
|
||||||
|
|
||||||
|
const decodedComp = result!.entity.getComponent(PlayerComponent);
|
||||||
|
expect(decodedComp!.name).toBe("SpawnedPlayer");
|
||||||
|
expect(decodedComp!.score).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('没有 prefabType 应该也能工作', () => {
|
||||||
|
const entity = scene.createEntity('Entity');
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
initChangeTracker(comp);
|
||||||
|
|
||||||
|
const data = encodeSpawn(entity);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const result = decodeSpawn(targetScene, data);
|
||||||
|
|
||||||
|
expect(result!.prefabType).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encodeDespawn/processDespawn', () => {
|
||||||
|
test('应该编码和处理单个实体销毁', () => {
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const entity = targetScene.createEntity('ToBeDestroyed');
|
||||||
|
const entityId = entity.id;
|
||||||
|
|
||||||
|
const data = encodeDespawn(entityId);
|
||||||
|
|
||||||
|
expect(data[0]).toBe(SyncOperation.DESPAWN);
|
||||||
|
|
||||||
|
const removedIds = processDespawn(targetScene, data);
|
||||||
|
|
||||||
|
expect(removedIds).toContain(entityId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('应该编码和处理批量实体销毁', () => {
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const entity1 = targetScene.createEntity('Entity1');
|
||||||
|
const entity2 = targetScene.createEntity('Entity2');
|
||||||
|
const entity3 = targetScene.createEntity('Entity3');
|
||||||
|
|
||||||
|
const data = encodeDespawnBatch([entity1.id, entity2.id, entity3.id]);
|
||||||
|
|
||||||
|
expect(data[0]).toBe(SyncOperation.DESPAWN);
|
||||||
|
|
||||||
|
const removedIds = processDespawn(targetScene, data);
|
||||||
|
|
||||||
|
expect(removedIds.length).toBe(3);
|
||||||
|
expect(removedIds).toContain(entity1.id);
|
||||||
|
expect(removedIds).toContain(entity2.id);
|
||||||
|
expect(removedIds).toContain(entity3.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('所有同步类型编解码', () => {
|
||||||
|
test('应该正确编解码所有类型', () => {
|
||||||
|
const entity = scene.createEntity('AllTypes');
|
||||||
|
const comp = entity.addComponent(new AllTypesComponent());
|
||||||
|
comp.boolField = true;
|
||||||
|
comp.int8Field = -100;
|
||||||
|
comp.uint8Field = 200;
|
||||||
|
comp.int16Field = -30000;
|
||||||
|
comp.uint16Field = 60000;
|
||||||
|
comp.int32Field = -2000000000;
|
||||||
|
comp.uint32Field = 4000000000;
|
||||||
|
comp.float32Field = 3.14159;
|
||||||
|
comp.float64Field = Math.PI;
|
||||||
|
comp.stringField = "测试字符串";
|
||||||
|
initChangeTracker(comp);
|
||||||
|
|
||||||
|
const data = encodeSnapshot([entity], SyncOperation.FULL);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
decodeSnapshot(targetScene, data);
|
||||||
|
|
||||||
|
const decodedEntity = targetScene.entities.buffer[0];
|
||||||
|
const decodedComp = decodedEntity!.getComponent(AllTypesComponent);
|
||||||
|
|
||||||
|
expect(decodedComp!.boolField).toBe(true);
|
||||||
|
expect(decodedComp!.int8Field).toBe(-100);
|
||||||
|
expect(decodedComp!.uint8Field).toBe(200);
|
||||||
|
expect(decodedComp!.int16Field).toBe(-30000);
|
||||||
|
expect(decodedComp!.uint16Field).toBe(60000);
|
||||||
|
expect(decodedComp!.int32Field).toBe(-2000000000);
|
||||||
|
expect(decodedComp!.uint32Field).toBe(4000000000);
|
||||||
|
expect(decodedComp!.float32Field).toBeCloseTo(3.14159, 4);
|
||||||
|
expect(decodedComp!.float64Field).toBe(Math.PI);
|
||||||
|
expect(decodedComp!.stringField).toBe("测试字符串");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况', () => {
|
||||||
|
test('空实体列表应该能编码', () => {
|
||||||
|
const data = encodeSnapshot([], SyncOperation.FULL);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const result = decodeSnapshot(targetScene, data);
|
||||||
|
|
||||||
|
expect(result.entities.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('entityMap 应该正确跟踪实体', () => {
|
||||||
|
const entity = scene.createEntity('Tracked');
|
||||||
|
const comp = entity.addComponent(new PlayerComponent());
|
||||||
|
comp.name = "TrackedPlayer";
|
||||||
|
initChangeTracker(comp);
|
||||||
|
|
||||||
|
const data = encodeSnapshot([entity], SyncOperation.FULL);
|
||||||
|
|
||||||
|
const targetScene = new Scene();
|
||||||
|
const entityMap = new Map();
|
||||||
|
decodeSnapshot(targetScene, data, entityMap);
|
||||||
|
|
||||||
|
expect(entityMap.size).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,37 @@
|
|||||||
# @esengine/fsm
|
# @esengine/fsm
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/fsm",
|
"name": "@esengine/fsm",
|
||||||
"version": "1.0.3",
|
"version": "3.0.1",
|
||||||
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
|
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,120 @@
|
|||||||
# @esengine/network
|
# @esengine/network
|
||||||
|
|
||||||
|
## 4.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 4.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#390](https://github.com/esengine/esengine/pull/390) [`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256) Thanks [@esengine](https://github.com/esengine)! - feat: ECS 网络状态同步系统
|
||||||
|
|
||||||
|
## @esengine/ecs-framework
|
||||||
|
|
||||||
|
新增 `@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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新增导出
|
||||||
|
- `sync` - 标记需要同步的字段装饰器
|
||||||
|
- `SyncType` - 支持的同步类型
|
||||||
|
- `SyncOperation` - 同步操作类型(FULL/DELTA/SPAWN/DESPAWN)
|
||||||
|
- `encodeSnapshot` / `decodeSnapshot` - 批量编解码
|
||||||
|
- `encodeSpawn` / `decodeSpawn` - 实体生成编解码
|
||||||
|
- `encodeDespawn` / `processDespawn` - 实体销毁编解码
|
||||||
|
- `ChangeTracker` - 字段级变更追踪
|
||||||
|
- `initChangeTracker` / `clearChanges` / `hasChanges` - 变更追踪工具函数
|
||||||
|
|
||||||
|
### 内部方法标记
|
||||||
|
|
||||||
|
将以下方法标记为 `@internal`,用户应通过 `Core.update()` 驱动更新:
|
||||||
|
- `Scene.update()`
|
||||||
|
- `SceneManager.update()`
|
||||||
|
- `WorldManager.updateAll()`
|
||||||
|
|
||||||
|
## @esengine/network
|
||||||
|
|
||||||
|
新增 `ComponentSyncSystem`,基于 `@sync` 装饰器自动同步组件状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ComponentSyncSystem } from '@esengine/network';
|
||||||
|
|
||||||
|
// 服务端:编码状态
|
||||||
|
const data = syncSystem.encodeAllEntities(false);
|
||||||
|
|
||||||
|
// 客户端:解码状态
|
||||||
|
syncSystem.applySnapshot(data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 将 `@esengine/ecs-framework` 从 devDependencies 移到 peerDependencies
|
||||||
|
|
||||||
|
## @esengine/server
|
||||||
|
|
||||||
|
新增 `ECSRoom`,带有 ECS World 支持的房间基类:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ECSRoom } from '@esengine/server/ecs';
|
||||||
|
|
||||||
|
// 服务端启动
|
||||||
|
Core.create();
|
||||||
|
setInterval(() => Core.update(1 / 60), 16);
|
||||||
|
|
||||||
|
// 定义房间
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
onCreate() {
|
||||||
|
this.addSystem(new PhysicsSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
onJoin(player: Player) {
|
||||||
|
const entity = this.createPlayerEntity(player.id);
|
||||||
|
entity.addComponent(new PlayerComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 设计
|
||||||
|
- 每个 `ECSRoom` 在 `Core.worldManager` 中创建独立的 World
|
||||||
|
- `Core.update()` 统一更新 Time 和所有 World
|
||||||
|
- `onTick()` 只处理状态同步逻辑
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/network",
|
"name": "@esengine/network",
|
||||||
"version": "2.2.0",
|
"version": "4.0.1",
|
||||||
"description": "Network synchronization for multiplayer games",
|
"description": "Network synchronization for multiplayer games",
|
||||||
"esengine": {
|
"esengine": {
|
||||||
"plugin": true,
|
"plugin": true,
|
||||||
@@ -30,6 +30,15 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@esengine/rpc": "workspace:*"
|
"@esengine/rpc": "workspace:*"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
|
"@esengine/blueprint": "workspace:*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@esengine/blueprint": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esengine/blueprint": "workspace:*",
|
"@esengine/blueprint": "workspace:*",
|
||||||
"@esengine/ecs-framework": "workspace:*",
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
|
|||||||
@@ -133,6 +133,11 @@ export type {
|
|||||||
EntityDeltaState,
|
EntityDeltaState,
|
||||||
DeltaSyncData,
|
DeltaSyncData,
|
||||||
DeltaCompressionConfig,
|
DeltaCompressionConfig,
|
||||||
|
// Component sync types
|
||||||
|
ComponentSyncEventType,
|
||||||
|
ComponentSyncEvent,
|
||||||
|
ComponentSyncEventListener,
|
||||||
|
ComponentSyncConfig,
|
||||||
} from './sync'
|
} from './sync'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@@ -150,6 +155,9 @@ export {
|
|||||||
DeltaFlags,
|
DeltaFlags,
|
||||||
StateDeltaCompressor,
|
StateDeltaCompressor,
|
||||||
createStateDeltaCompressor,
|
createStateDeltaCompressor,
|
||||||
|
// Component sync
|
||||||
|
ComponentSyncSystem,
|
||||||
|
createComponentSyncSystem,
|
||||||
} from './sync'
|
} from './sync'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
408
packages/framework/network/src/sync/ComponentSync.ts
Normal file
408
packages/framework/network/src/sync/ComponentSync.ts
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
/**
|
||||||
|
* @zh 组件同步系统
|
||||||
|
* @en Component Sync System
|
||||||
|
*
|
||||||
|
* @zh 基于 @sync 装饰器的组件状态同步,与 ecs-framework 的 Sync 模块集成
|
||||||
|
* @en Component state synchronization based on @sync decorator, integrated with ecs-framework Sync module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EntitySystem,
|
||||||
|
Matcher,
|
||||||
|
type Entity,
|
||||||
|
// Sync types
|
||||||
|
SyncOperation,
|
||||||
|
SYNC_METADATA,
|
||||||
|
CHANGE_TRACKER,
|
||||||
|
type SyncMetadata,
|
||||||
|
type ChangeTracker,
|
||||||
|
// Encoding
|
||||||
|
encodeSnapshot,
|
||||||
|
encodeSpawn,
|
||||||
|
encodeDespawn,
|
||||||
|
decodeSnapshot,
|
||||||
|
decodeSpawn,
|
||||||
|
processDespawn,
|
||||||
|
GlobalComponentRegistry,
|
||||||
|
type DecodeSnapshotResult,
|
||||||
|
type DecodeSpawnResult,
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
import { NetworkIdentity } from '../components/NetworkIdentity';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types | 类型定义
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步事件类型
|
||||||
|
* @en Component sync event type
|
||||||
|
*/
|
||||||
|
export type ComponentSyncEventType =
|
||||||
|
| 'entitySpawned'
|
||||||
|
| 'entityDespawned'
|
||||||
|
| 'stateUpdated';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步事件
|
||||||
|
* @en Component sync event
|
||||||
|
*/
|
||||||
|
export interface ComponentSyncEvent {
|
||||||
|
type: ComponentSyncEventType;
|
||||||
|
entityId: number;
|
||||||
|
prefabType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步事件监听器
|
||||||
|
* @en Component sync event listener
|
||||||
|
*/
|
||||||
|
export type ComponentSyncEventListener = (event: ComponentSyncEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步配置
|
||||||
|
* @en Component sync configuration
|
||||||
|
*/
|
||||||
|
export interface ComponentSyncConfig {
|
||||||
|
/**
|
||||||
|
* @zh 是否启用增量同步
|
||||||
|
* @en Whether to enable delta sync
|
||||||
|
*/
|
||||||
|
enableDeltaSync: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 同步间隔(毫秒)
|
||||||
|
* @en Sync interval in milliseconds
|
||||||
|
*/
|
||||||
|
syncInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: ComponentSyncConfig = {
|
||||||
|
enableDeltaSync: true,
|
||||||
|
syncInterval: 50, // 20 Hz
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ComponentSyncSystem | 组件同步系统
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 组件同步系统
|
||||||
|
* @en Component sync system
|
||||||
|
*
|
||||||
|
* @zh 基于 @sync 装饰器自动同步组件状态
|
||||||
|
* @en Automatically syncs component state based on @sync decorator
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // Server-side: broadcast state
|
||||||
|
* const syncSystem = scene.getSystem(ComponentSyncSystem);
|
||||||
|
* const data = syncSystem.encodeAllEntities(false); // delta
|
||||||
|
* broadcast(data);
|
||||||
|
*
|
||||||
|
* // Client-side: receive state
|
||||||
|
* const syncSystem = scene.getSystem(ComponentSyncSystem);
|
||||||
|
* syncSystem.applySnapshot(data);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ComponentSyncSystem extends EntitySystem {
|
||||||
|
private readonly _config: ComponentSyncConfig;
|
||||||
|
private readonly _syncEntityMap: Map<number, Entity> = new Map();
|
||||||
|
private readonly _syncListeners: Set<ComponentSyncEventListener> = new Set();
|
||||||
|
private _lastSyncTime: number = 0;
|
||||||
|
private _isServer: boolean = false;
|
||||||
|
|
||||||
|
constructor(config?: Partial<ComponentSyncConfig>, isServer: boolean = false) {
|
||||||
|
super(Matcher.all(NetworkIdentity));
|
||||||
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
this._isServer = isServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 设置是否为服务端模式
|
||||||
|
* @en Set whether in server mode
|
||||||
|
*/
|
||||||
|
public set isServer(value: boolean) {
|
||||||
|
this._isServer = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取是否为服务端模式
|
||||||
|
* @en Get whether in server mode
|
||||||
|
*/
|
||||||
|
public get isServer(): boolean {
|
||||||
|
return this._isServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取配置
|
||||||
|
* @en Get configuration
|
||||||
|
*/
|
||||||
|
public get config(): Readonly<ComponentSyncConfig> {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 添加同步事件监听器
|
||||||
|
* @en Add sync event listener
|
||||||
|
*/
|
||||||
|
public addSyncListener(listener: ComponentSyncEventListener): void {
|
||||||
|
this._syncListeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 移除同步事件监听器
|
||||||
|
* @en Remove sync event listener
|
||||||
|
*/
|
||||||
|
public removeSyncListener(listener: ComponentSyncEventListener): void {
|
||||||
|
this._syncListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 注册同步组件类型
|
||||||
|
* @en Register sync component type
|
||||||
|
*
|
||||||
|
* @zh 客户端需要调用此方法注册所有需要同步的组件类型
|
||||||
|
* @en Client needs to call this to register all component types to be synced
|
||||||
|
*/
|
||||||
|
public registerComponent<T extends new () => any>(componentClass: T): void {
|
||||||
|
GlobalComponentRegistry.register(componentClass as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Server-side: Encoding | 服务端:编码
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码所有实体状态
|
||||||
|
* @en Encode all entities state
|
||||||
|
*
|
||||||
|
* @param fullSync - @zh 是否完整同步(首次连接时使用)@en Whether to do full sync (for initial connection)
|
||||||
|
* @returns @zh 编码后的二进制数据 @en Encoded binary data
|
||||||
|
*/
|
||||||
|
public encodeAllEntities(fullSync: boolean = false): Uint8Array {
|
||||||
|
const entities = this.getMatchingEntities();
|
||||||
|
const operation = fullSync ? SyncOperation.FULL : SyncOperation.DELTA;
|
||||||
|
|
||||||
|
const data = encodeSnapshot(entities, operation);
|
||||||
|
|
||||||
|
// Clear change trackers after encoding delta
|
||||||
|
if (!fullSync) {
|
||||||
|
this._clearChangeTrackers(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码有变更的实体
|
||||||
|
* @en Encode entities with changes
|
||||||
|
*
|
||||||
|
* @returns @zh 编码后的二进制数据,如果没有变更返回 null @en Encoded binary data, or null if no changes
|
||||||
|
*/
|
||||||
|
public encodeDelta(): Uint8Array | null {
|
||||||
|
const entities = this.getMatchingEntities();
|
||||||
|
const changedEntities = entities.filter(entity => this._hasChanges(entity));
|
||||||
|
|
||||||
|
if (changedEntities.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = encodeSnapshot(changedEntities, SyncOperation.DELTA);
|
||||||
|
this._clearChangeTrackers(changedEntities);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码实体生成消息
|
||||||
|
* @en Encode entity spawn message
|
||||||
|
*/
|
||||||
|
public encodeSpawn(entity: Entity, prefabType?: string): Uint8Array {
|
||||||
|
return encodeSpawn(entity, prefabType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 编码实体销毁消息
|
||||||
|
* @en Encode entity despawn message
|
||||||
|
*/
|
||||||
|
public encodeDespawn(entityId: number): Uint8Array {
|
||||||
|
return encodeDespawn(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Client-side: Decoding | 客户端:解码
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 应用状态快照
|
||||||
|
* @en Apply state snapshot
|
||||||
|
*
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @returns @zh 解码结果 @en Decode result
|
||||||
|
*/
|
||||||
|
public applySnapshot(data: Uint8Array): DecodeSnapshotResult {
|
||||||
|
if (!this.scene) {
|
||||||
|
throw new Error('ComponentSyncSystem not attached to a scene');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = decodeSnapshot(this.scene, data, this._syncEntityMap);
|
||||||
|
|
||||||
|
// Emit events
|
||||||
|
for (const entityResult of result.entities) {
|
||||||
|
if (entityResult.isNew) {
|
||||||
|
this._emitEvent({
|
||||||
|
type: 'entitySpawned',
|
||||||
|
entityId: entityResult.entityId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._emitEvent({
|
||||||
|
type: 'stateUpdated',
|
||||||
|
entityId: entityResult.entityId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 应用实体生成消息
|
||||||
|
* @en Apply entity spawn message
|
||||||
|
*
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @returns @zh 解码结果,如果不是 SPAWN 消息返回 null @en Decode result, or null if not a SPAWN message
|
||||||
|
*/
|
||||||
|
public applySpawn(data: Uint8Array): DecodeSpawnResult | null {
|
||||||
|
if (!this.scene) {
|
||||||
|
throw new Error('ComponentSyncSystem not attached to a scene');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = decodeSpawn(this.scene, data, this._syncEntityMap);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
this._emitEvent({
|
||||||
|
type: 'entitySpawned',
|
||||||
|
entityId: result.entity.id,
|
||||||
|
prefabType: result.prefabType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 应用实体销毁消息
|
||||||
|
* @en Apply entity despawn message
|
||||||
|
*
|
||||||
|
* @param data - @zh 二进制数据 @en Binary data
|
||||||
|
* @returns @zh 销毁的实体 ID 列表 @en List of despawned entity IDs
|
||||||
|
*/
|
||||||
|
public applyDespawn(data: Uint8Array): number[] {
|
||||||
|
if (!this.scene) {
|
||||||
|
throw new Error('ComponentSyncSystem not attached to a scene');
|
||||||
|
}
|
||||||
|
|
||||||
|
const entityIds = processDespawn(this.scene, data, this._syncEntityMap);
|
||||||
|
|
||||||
|
for (const entityId of entityIds) {
|
||||||
|
this._emitEvent({
|
||||||
|
type: 'entityDespawned',
|
||||||
|
entityId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Entity Management | 实体管理
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 通过网络 ID 获取实体
|
||||||
|
* @en Get entity by network ID
|
||||||
|
*/
|
||||||
|
public getEntityById(entityId: number): Entity | undefined {
|
||||||
|
return this._syncEntityMap.get(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取所有匹配的实体
|
||||||
|
* @en Get all matching entities
|
||||||
|
*/
|
||||||
|
public getMatchingEntities(): Entity[] {
|
||||||
|
return this.entities.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Internal | 内部方法
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
// Server mode: auto-sync at interval
|
||||||
|
if (this._isServer && this._config.enableDeltaSync) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this._lastSyncTime >= this._config.syncInterval) {
|
||||||
|
// Note: actual broadcast should be done by the user
|
||||||
|
// This just updates the sync time
|
||||||
|
this._lastSyncTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update entity ID map
|
||||||
|
for (const entity of entities) {
|
||||||
|
const identity = entity.getComponent(NetworkIdentity);
|
||||||
|
if (identity) {
|
||||||
|
this._syncEntityMap.set(entity.id, entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasChanges(entity: Entity): boolean {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (tracker?.hasChanges()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearChangeTrackers(entities: Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (tracker) {
|
||||||
|
tracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _emitEvent(event: ComponentSyncEvent): void {
|
||||||
|
for (const listener of this._syncListeners) {
|
||||||
|
try {
|
||||||
|
listener(event);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('ComponentSyncSystem: event listener error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onDestroy(): void {
|
||||||
|
this._syncEntityMap.clear();
|
||||||
|
this._syncListeners.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建组件同步系统
|
||||||
|
* @en Create component sync system
|
||||||
|
*/
|
||||||
|
export function createComponentSyncSystem(
|
||||||
|
config?: Partial<ComponentSyncConfig>,
|
||||||
|
isServer: boolean = false
|
||||||
|
): ComponentSyncSystem {
|
||||||
|
return new ComponentSyncSystem(config, isServer);
|
||||||
|
}
|
||||||
@@ -62,3 +62,19 @@ export {
|
|||||||
StateDeltaCompressor,
|
StateDeltaCompressor,
|
||||||
createStateDeltaCompressor
|
createStateDeltaCompressor
|
||||||
} from './StateDelta';
|
} from './StateDelta';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 组件同步 | Component Sync (@sync decorator based)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ComponentSyncEventType,
|
||||||
|
ComponentSyncEvent,
|
||||||
|
ComponentSyncEventListener,
|
||||||
|
ComponentSyncConfig
|
||||||
|
} from './ComponentSync';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ComponentSyncSystem,
|
||||||
|
createComponentSyncSystem
|
||||||
|
} from './ComponentSync';
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# @esengine/pathfinding
|
# @esengine/pathfinding
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 1.0.4
|
## 1.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/pathfinding",
|
"name": "@esengine/pathfinding",
|
||||||
"version": "1.1.0",
|
"version": "3.0.1",
|
||||||
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
|
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# @esengine/procgen
|
# @esengine/procgen
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/procgen",
|
"name": "@esengine/procgen",
|
||||||
"version": "1.0.3",
|
"version": "3.0.1",
|
||||||
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
|
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,143 @@
|
|||||||
# @esengine/server
|
# @esengine/server
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- feat(ecs): 添加 @NetworkEntity 装饰器,支持自动广播实体生成/销毁
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
|
||||||
|
**@NetworkEntity 装饰器**
|
||||||
|
- 标记组件为网络实体,自动广播 spawn/despawn 消息
|
||||||
|
- 支持 `autoSpawn` 和 `autoDespawn` 配置选项
|
||||||
|
- 通过事件系统(`ECSEventType.COMPONENT_ADDED` / `ECSEventType.ENTITY_DESTROYED`)实现
|
||||||
|
|
||||||
|
**ECSRoom 增强**
|
||||||
|
- 新增 `enableAutoNetworkEntity` 配置选项(默认启用)
|
||||||
|
- 自动监听组件添加和实体销毁事件
|
||||||
|
- 简化 GameRoom 实现,无需手动回调
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
|
||||||
|
**Entity 事件**
|
||||||
|
- `Entity.destroy()` 现在发出 `entity:destroyed` 事件
|
||||||
|
- `Entity.active` 变化时发出 `entity:enabled` / `entity:disabled` 事件
|
||||||
|
- 使用 `ECSEventType` 常量替代硬编码字符串
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务端
|
||||||
|
const entity = scene.createEntity('Enemy');
|
||||||
|
entity.addComponent(new EnemyComponent()); // 自动广播 spawn
|
||||||
|
entity.destroy(); // 自动广播 despawn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Minor Changes
|
||||||
|
|
||||||
|
- [#390](https://github.com/esengine/esengine/pull/390) [`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256) Thanks [@esengine](https://github.com/esengine)! - feat: ECS 网络状态同步系统
|
||||||
|
|
||||||
|
## @esengine/ecs-framework
|
||||||
|
|
||||||
|
新增 `@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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新增导出
|
||||||
|
- `sync` - 标记需要同步的字段装饰器
|
||||||
|
- `SyncType` - 支持的同步类型
|
||||||
|
- `SyncOperation` - 同步操作类型(FULL/DELTA/SPAWN/DESPAWN)
|
||||||
|
- `encodeSnapshot` / `decodeSnapshot` - 批量编解码
|
||||||
|
- `encodeSpawn` / `decodeSpawn` - 实体生成编解码
|
||||||
|
- `encodeDespawn` / `processDespawn` - 实体销毁编解码
|
||||||
|
- `ChangeTracker` - 字段级变更追踪
|
||||||
|
- `initChangeTracker` / `clearChanges` / `hasChanges` - 变更追踪工具函数
|
||||||
|
|
||||||
|
### 内部方法标记
|
||||||
|
|
||||||
|
将以下方法标记为 `@internal`,用户应通过 `Core.update()` 驱动更新:
|
||||||
|
- `Scene.update()`
|
||||||
|
- `SceneManager.update()`
|
||||||
|
- `WorldManager.updateAll()`
|
||||||
|
|
||||||
|
## @esengine/network
|
||||||
|
|
||||||
|
新增 `ComponentSyncSystem`,基于 `@sync` 装饰器自动同步组件状态:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ComponentSyncSystem } from '@esengine/network';
|
||||||
|
|
||||||
|
// 服务端:编码状态
|
||||||
|
const data = syncSystem.encodeAllEntities(false);
|
||||||
|
|
||||||
|
// 客户端:解码状态
|
||||||
|
syncSystem.applySnapshot(data);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 将 `@esengine/ecs-framework` 从 devDependencies 移到 peerDependencies
|
||||||
|
|
||||||
|
## @esengine/server
|
||||||
|
|
||||||
|
新增 `ECSRoom`,带有 ECS World 支持的房间基类:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ECSRoom } from '@esengine/server/ecs';
|
||||||
|
|
||||||
|
// 服务端启动
|
||||||
|
Core.create();
|
||||||
|
setInterval(() => Core.update(1 / 60), 16);
|
||||||
|
|
||||||
|
// 定义房间
|
||||||
|
class GameRoom extends ECSRoom {
|
||||||
|
onCreate() {
|
||||||
|
this.addSystem(new PhysicsSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
onJoin(player: Player) {
|
||||||
|
const entity = this.createPlayerEntity(player.id);
|
||||||
|
entity.addComponent(new PlayerComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 设计
|
||||||
|
- 每个 `ECSRoom` 在 `Core.worldManager` 中创建独立的 World
|
||||||
|
- `Core.update()` 统一更新 Time 和所有 World
|
||||||
|
- `onTick()` 只处理状态同步逻辑
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
|
||||||
## 1.3.0
|
## 1.3.0
|
||||||
|
|
||||||
### Minor Changes
|
### Minor Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/server",
|
"name": "@esengine/server",
|
||||||
"version": "1.3.0",
|
"version": "3.0.0",
|
||||||
"description": "Game server framework for ESEngine with file-based routing",
|
"description": "Game server framework for ESEngine with file-based routing",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
@@ -26,6 +26,10 @@
|
|||||||
"./testing": {
|
"./testing": {
|
||||||
"import": "./dist/testing/index.js",
|
"import": "./dist/testing/index.js",
|
||||||
"types": "./dist/testing/index.d.ts"
|
"types": "./dist/testing/index.d.ts"
|
||||||
|
},
|
||||||
|
"./ecs": {
|
||||||
|
"import": "./dist/ecs/index.js",
|
||||||
|
"types": "./dist/ecs/index.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -46,14 +50,19 @@
|
|||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"ws": ">=8.0.0",
|
"ws": ">=8.0.0",
|
||||||
"jsonwebtoken": ">=9.0.0"
|
"jsonwebtoken": ">=9.0.0",
|
||||||
|
"@esengine/ecs-framework": ">=2.6.1"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"jsonwebtoken": {
|
"jsonwebtoken": {
|
||||||
"optional": true
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esengine/ecs-framework": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esengine/ecs-framework": "workspace:*",
|
||||||
"@types/jsonwebtoken": "^9.0.0",
|
"@types/jsonwebtoken": "^9.0.0",
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/ws": "^8.5.13",
|
"@types/ws": "^8.5.13",
|
||||||
|
|||||||
348
packages/framework/server/src/ecs/ECSRoom.test.ts
Normal file
348
packages/framework/server/src/ecs/ECSRoom.test.ts
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
/**
|
||||||
|
* @zh ECSRoom 集成测试
|
||||||
|
* @en ECSRoom integration tests
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'
|
||||||
|
import {
|
||||||
|
Core,
|
||||||
|
Component,
|
||||||
|
ECSComponent,
|
||||||
|
sync,
|
||||||
|
initChangeTracker,
|
||||||
|
getSyncMetadata,
|
||||||
|
registerSyncComponent,
|
||||||
|
} from '@esengine/ecs-framework'
|
||||||
|
import { createTestEnv, type TestEnvironment, wait } from '../testing/TestServer.js'
|
||||||
|
import { ECSRoom } from './ECSRoom.js'
|
||||||
|
import type { Player } from '../room/Player.js'
|
||||||
|
import { onMessage } from '../room/decorators.js'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Components | 测试组件
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
@ECSComponent('ECSRoomTest_PlayerComponent')
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@sync('string') name: string = ''
|
||||||
|
@sync('uint16') score: number = 0
|
||||||
|
@sync('float32') x: number = 0
|
||||||
|
@sync('float32') y: number = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@ECSComponent('ECSRoomTest_HealthComponent')
|
||||||
|
class HealthComponent extends Component {
|
||||||
|
@sync('int32') current: number = 100
|
||||||
|
@sync('int32') max: number = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Room | 测试房间
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface TestRoomState {
|
||||||
|
gameStarted: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TestPlayerData {
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestECSRoom extends ECSRoom<TestRoomState, TestPlayerData> {
|
||||||
|
state: TestRoomState = {
|
||||||
|
gameStarted: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreate(): void {
|
||||||
|
// 可以在这里添加系统
|
||||||
|
}
|
||||||
|
|
||||||
|
onJoin(player: Player<TestPlayerData>): void {
|
||||||
|
const entity = this.createPlayerEntity(player.id)
|
||||||
|
const comp = entity.addComponent(new PlayerComponent())
|
||||||
|
comp.name = player.data.nickname || `Player_${player.id.slice(-4)}`
|
||||||
|
comp.x = Math.random() * 100
|
||||||
|
comp.y = Math.random() * 100
|
||||||
|
|
||||||
|
this.broadcast('PlayerJoined', {
|
||||||
|
playerId: player.id,
|
||||||
|
name: comp.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async onLeave(player: Player<TestPlayerData>, reason?: string): Promise<void> {
|
||||||
|
await super.onLeave(player, reason)
|
||||||
|
this.broadcast('PlayerLeft', { playerId: player.id })
|
||||||
|
}
|
||||||
|
|
||||||
|
@onMessage('Move')
|
||||||
|
handleMove(data: { x: number; y: number }, player: Player<TestPlayerData>): void {
|
||||||
|
const entity = this.getPlayerEntity(player.id)
|
||||||
|
if (entity) {
|
||||||
|
const comp = entity.getComponent(PlayerComponent)
|
||||||
|
if (comp) {
|
||||||
|
comp.x = data.x
|
||||||
|
comp.y = data.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@onMessage('AddScore')
|
||||||
|
handleAddScore(data: { amount: number }, player: Player<TestPlayerData>): void {
|
||||||
|
const entity = this.getPlayerEntity(player.id)
|
||||||
|
if (entity) {
|
||||||
|
const comp = entity.getComponent(PlayerComponent)
|
||||||
|
if (comp) {
|
||||||
|
comp.score += data.amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@onMessage('Ping')
|
||||||
|
handlePing(_data: unknown, player: Player<TestPlayerData>): void {
|
||||||
|
player.send('Pong', { timestamp: Date.now() })
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorld() {
|
||||||
|
return this.world
|
||||||
|
}
|
||||||
|
|
||||||
|
getScene() {
|
||||||
|
return this.scene
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerEntityCount(): number {
|
||||||
|
return this.scene.entities.buffer.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Test Suites | 测试套件
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
describe('ECSRoom Integration Tests', () => {
|
||||||
|
let env: TestEnvironment
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
Core.create()
|
||||||
|
registerSyncComponent('ECSRoomTest_PlayerComponent', PlayerComponent)
|
||||||
|
registerSyncComponent('ECSRoomTest_HealthComponent', HealthComponent)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
Core.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
env = await createTestEnv({ tickRate: 20 })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await env.cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Room Creation | 房间创建
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('Room Creation', () => {
|
||||||
|
it('should create ECSRoom with World and Scene', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
expect(client.roomId).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should have World managed by Core.worldManager', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
// 验证 World 正常创建(通过消息通信验证)
|
||||||
|
const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong')
|
||||||
|
client.sendToRoom('Ping', {})
|
||||||
|
const pong = await pongPromise
|
||||||
|
|
||||||
|
expect(pong.timestamp).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Player Entity Management | 玩家实体管理
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('Player Entity Management', () => {
|
||||||
|
it('should create player entity on join', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client1 = await env.createClient()
|
||||||
|
const { roomId } = await client1.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
// 等待第二个玩家加入时收到广播
|
||||||
|
const joinPromise = client1.waitForRoomMessage<{ playerId: string; name: string }>(
|
||||||
|
'PlayerJoined'
|
||||||
|
)
|
||||||
|
|
||||||
|
const client2 = await env.createClient()
|
||||||
|
await client2.joinRoomById(roomId)
|
||||||
|
|
||||||
|
const joinMsg = await joinPromise
|
||||||
|
expect(joinMsg.playerId).toBe(client2.playerId)
|
||||||
|
expect(joinMsg.name).toContain('Player_')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should destroy player entity on leave', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client1 = await env.createClient()
|
||||||
|
const { roomId } = await client1.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
const client2 = await env.createClient()
|
||||||
|
await client2.joinRoomById(roomId)
|
||||||
|
|
||||||
|
const leavePromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerLeft')
|
||||||
|
|
||||||
|
await client2.leaveRoom()
|
||||||
|
|
||||||
|
const leaveMsg = await leavePromise
|
||||||
|
expect(leaveMsg.playerId).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Component Sync | 组件同步
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('Component State Updates', () => {
|
||||||
|
it('should update component via message handler', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
client.sendToRoom('Move', { x: 100, y: 200 })
|
||||||
|
|
||||||
|
// 等待处理
|
||||||
|
await wait(50)
|
||||||
|
|
||||||
|
// 验证 Ping/Pong 仍能工作(房间仍活跃)
|
||||||
|
const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong')
|
||||||
|
client.sendToRoom('Ping', {})
|
||||||
|
const pong = await pongPromise
|
||||||
|
|
||||||
|
expect(pong.timestamp).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle AddScore message', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
client.sendToRoom('AddScore', { amount: 50 })
|
||||||
|
client.sendToRoom('AddScore', { amount: 25 })
|
||||||
|
|
||||||
|
await wait(50)
|
||||||
|
|
||||||
|
// 确认房间仍然活跃
|
||||||
|
const pongPromise = client.waitForRoomMessage<{ timestamp: number }>('Pong')
|
||||||
|
client.sendToRoom('Ping', {})
|
||||||
|
await pongPromise
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Sync Broadcast | 同步广播
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('State Sync Broadcast', () => {
|
||||||
|
it('should receive $sync messages when enabled', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
// 触发状态变更
|
||||||
|
client.sendToRoom('Move', { x: 50, y: 75 })
|
||||||
|
|
||||||
|
// 等待 tick 处理
|
||||||
|
await wait(200)
|
||||||
|
|
||||||
|
// 检查是否收到 $sync 消息
|
||||||
|
const hasSync = client.hasReceivedMessage('RoomMessage')
|
||||||
|
expect(hasSync).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Multi-player Sync | 多玩家同步
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('Multi-player Scenarios', () => {
|
||||||
|
it('should handle multiple players in same room', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client1 = await env.createClient()
|
||||||
|
const { roomId } = await client1.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
const client2 = await env.createClient()
|
||||||
|
const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined')
|
||||||
|
await client2.joinRoomById(roomId)
|
||||||
|
|
||||||
|
const joinMsg = await joinPromise
|
||||||
|
expect(joinMsg.playerId).toBe(client2.playerId)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should broadcast to all players on state change', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client1 = await env.createClient()
|
||||||
|
const { roomId } = await client1.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
const client2 = await env.createClient()
|
||||||
|
|
||||||
|
// client1 等待收到 client2 加入的广播
|
||||||
|
const joinPromise = client1.waitForRoomMessage<{ playerId: string }>('PlayerJoined')
|
||||||
|
|
||||||
|
await client2.joinRoomById(roomId)
|
||||||
|
|
||||||
|
const joinMsg = await joinPromise
|
||||||
|
expect(joinMsg.playerId).toBe(client2.playerId)
|
||||||
|
|
||||||
|
// 验证每个客户端都能独立通信
|
||||||
|
const pong1Promise = client1.waitForRoomMessage<{ timestamp: number }>('Pong')
|
||||||
|
client1.sendToRoom('Ping', {})
|
||||||
|
const pong1 = await pong1Promise
|
||||||
|
expect(pong1.timestamp).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const pong2Promise = client2.waitForRoomMessage<{ timestamp: number }>('Pong')
|
||||||
|
client2.sendToRoom('Ping', {})
|
||||||
|
const pong2 = await pong2Promise
|
||||||
|
expect(pong2.timestamp).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Cleanup | 清理
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
describe('Room Cleanup', () => {
|
||||||
|
it('should cleanup World on dispose', async () => {
|
||||||
|
env.server.define('ecs-test', TestECSRoom)
|
||||||
|
|
||||||
|
const client = await env.createClient()
|
||||||
|
await client.joinRoom('ecs-test')
|
||||||
|
|
||||||
|
await client.leaveRoom()
|
||||||
|
|
||||||
|
// 等待自动销毁
|
||||||
|
await wait(100)
|
||||||
|
|
||||||
|
// 房间应该已销毁
|
||||||
|
expect(client.roomId).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
405
packages/framework/server/src/ecs/ECSRoom.ts
Normal file
405
packages/framework/server/src/ecs/ECSRoom.ts
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
/**
|
||||||
|
* @zh ECS 房间基类
|
||||||
|
* @en ECS Room base class
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Core,
|
||||||
|
Scene,
|
||||||
|
World,
|
||||||
|
Entity,
|
||||||
|
EntitySystem,
|
||||||
|
type Component,
|
||||||
|
// Sync
|
||||||
|
SyncOperation,
|
||||||
|
SYNC_METADATA,
|
||||||
|
CHANGE_TRACKER,
|
||||||
|
type SyncMetadata,
|
||||||
|
type ChangeTracker,
|
||||||
|
encodeSnapshot,
|
||||||
|
encodeSpawn,
|
||||||
|
encodeDespawn,
|
||||||
|
initChangeTracker,
|
||||||
|
// Network Entity
|
||||||
|
NETWORK_ENTITY_METADATA,
|
||||||
|
type NetworkEntityMetadata,
|
||||||
|
// Events
|
||||||
|
ECSEventType,
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
import { Room, type RoomOptions } from '../room/Room.js';
|
||||||
|
import type { Player } from '../room/Player.js';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Types | 类型定义
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh ECS 房间配置
|
||||||
|
* @en ECS room configuration
|
||||||
|
*/
|
||||||
|
export interface ECSRoomConfig {
|
||||||
|
/**
|
||||||
|
* @zh 状态同步间隔(毫秒)
|
||||||
|
* @en State sync interval in milliseconds
|
||||||
|
*/
|
||||||
|
syncInterval: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否启用增量同步
|
||||||
|
* @en Whether to enable delta sync
|
||||||
|
*/
|
||||||
|
enableDeltaSync: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 是否启用自动网络实体广播(基于 @NetworkEntity 装饰器)
|
||||||
|
* @en Whether to enable automatic network entity broadcasting (based on @NetworkEntity decorator)
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enableAutoNetworkEntity: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ECS_CONFIG: ECSRoomConfig = {
|
||||||
|
syncInterval: 50, // 20 Hz
|
||||||
|
enableDeltaSync: true,
|
||||||
|
enableAutoNetworkEntity: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体标识组件
|
||||||
|
* @en Network entity identity component
|
||||||
|
*/
|
||||||
|
const NETWORK_ENTITY_OWNER = Symbol('NetworkEntityOwner');
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// ECSRoom | ECS 房间
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh ECS 房间基类,带有 ECS World 支持和自动状态同步
|
||||||
|
* @en ECS Room base class with ECS World support and automatic state synchronization
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 服务端启动
|
||||||
|
* Core.create();
|
||||||
|
* setInterval(() => Core.update(1/60), 16);
|
||||||
|
*
|
||||||
|
* // 定义房间
|
||||||
|
* class GameRoom extends ECSRoom {
|
||||||
|
* onCreate() {
|
||||||
|
* this.addSystem(new PhysicsSystem());
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* onJoin(player: Player) {
|
||||||
|
* const entity = this.createPlayerEntity(player.id);
|
||||||
|
* entity.addComponent(new PlayerComponent());
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export abstract class ECSRoom<TState = any, TPlayerData = Record<string, unknown>> extends Room<TState, TPlayerData> {
|
||||||
|
/**
|
||||||
|
* @zh ECS World(由 Core.worldManager 管理)
|
||||||
|
* @en ECS World (managed by Core.worldManager)
|
||||||
|
*/
|
||||||
|
protected readonly world: World;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh World 在 WorldManager 中的 ID
|
||||||
|
* @en World ID in WorldManager
|
||||||
|
*/
|
||||||
|
protected readonly worldId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 房间的主场景
|
||||||
|
* @en Room's main scene
|
||||||
|
*/
|
||||||
|
protected readonly scene: Scene;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh ECS 配置
|
||||||
|
* @en ECS configuration
|
||||||
|
*/
|
||||||
|
protected readonly ecsConfig: ECSRoomConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 玩家 ID 到实体的映射
|
||||||
|
* @en Player ID to Entity mapping
|
||||||
|
*/
|
||||||
|
private readonly _playerEntities: Map<string, Entity> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 网络实体映射(实体 ID -> prefabType)
|
||||||
|
* @en Network entity mapping (entity ID -> prefabType)
|
||||||
|
*/
|
||||||
|
private readonly _networkEntities: Map<number, string> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 上次同步时间
|
||||||
|
* @en Last sync time
|
||||||
|
*/
|
||||||
|
private _lastSyncTime: number = 0;
|
||||||
|
|
||||||
|
constructor(ecsConfig?: Partial<ECSRoomConfig>) {
|
||||||
|
super();
|
||||||
|
this.ecsConfig = { ...DEFAULT_ECS_CONFIG, ...ecsConfig };
|
||||||
|
|
||||||
|
this.worldId = `room_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
this.world = Core.worldManager.createWorld(this.worldId);
|
||||||
|
this.scene = this.world.createScene('game');
|
||||||
|
this.world.setSceneActive('game', true);
|
||||||
|
this.world.start();
|
||||||
|
|
||||||
|
// 设置自动网络实体广播
|
||||||
|
if (this.ecsConfig.enableAutoNetworkEntity) {
|
||||||
|
this._setupAutoNetworkEntity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 设置自动网络实体广播
|
||||||
|
* @en Setup automatic network entity broadcasting
|
||||||
|
*/
|
||||||
|
private _setupAutoNetworkEntity(): void {
|
||||||
|
// 监听组件添加事件,自动广播 spawn
|
||||||
|
this.scene.eventSystem.on(ECSEventType.COMPONENT_ADDED, (event: any) => {
|
||||||
|
const { entity, component } = event;
|
||||||
|
const metadata: NetworkEntityMetadata | undefined =
|
||||||
|
(component.constructor as any)[NETWORK_ENTITY_METADATA];
|
||||||
|
|
||||||
|
if (metadata?.autoSpawn) {
|
||||||
|
// 避免重复广播同一实体
|
||||||
|
if (!this._networkEntities.has(entity.id)) {
|
||||||
|
this._networkEntities.set(entity.id, metadata.prefabType);
|
||||||
|
this.broadcastSpawn(entity, metadata.prefabType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录需要自动 despawn 的实体
|
||||||
|
if (metadata?.autoDespawn && !this._networkEntities.has(entity.id)) {
|
||||||
|
this._networkEntities.set(entity.id, metadata.prefabType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听实体销毁事件,自动广播 despawn
|
||||||
|
this.scene.eventSystem.on(ECSEventType.ENTITY_DESTROYED, (event: any) => {
|
||||||
|
const { entityId } = event;
|
||||||
|
if (this._networkEntities.has(entityId)) {
|
||||||
|
const despawnData = encodeDespawn(entityId);
|
||||||
|
this.broadcastBinary(despawnData);
|
||||||
|
this._networkEntities.delete(entityId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Scene Management | 场景管理
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 添加系统到场景
|
||||||
|
* @en Add system to scene
|
||||||
|
*/
|
||||||
|
protected addSystem(system: EntitySystem): void {
|
||||||
|
this.scene.addSystem(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 创建实体
|
||||||
|
* @en Create entity
|
||||||
|
*/
|
||||||
|
protected createEntity(name?: string): Entity {
|
||||||
|
return this.scene.createEntity(name ?? `entity_${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 为玩家创建实体
|
||||||
|
* @en Create entity for player
|
||||||
|
*
|
||||||
|
* @param playerId - @zh 玩家 ID @en Player ID
|
||||||
|
* @param name - @zh 实体名称 @en Entity name
|
||||||
|
* @returns @zh 创建的实体 @en Created entity
|
||||||
|
*/
|
||||||
|
protected createPlayerEntity(playerId: string, name?: string): Entity {
|
||||||
|
const entityName = name ?? `player_${playerId}`;
|
||||||
|
const entity = this.scene.createEntity(entityName);
|
||||||
|
(entity as any)[NETWORK_ENTITY_OWNER] = playerId;
|
||||||
|
this._playerEntities.set(playerId, entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 获取玩家的实体
|
||||||
|
* @en Get player's entity
|
||||||
|
*/
|
||||||
|
protected getPlayerEntity(playerId: string): Entity | undefined {
|
||||||
|
return this._playerEntities.get(playerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 销毁玩家的实体
|
||||||
|
* @en Destroy player's entity
|
||||||
|
*/
|
||||||
|
protected destroyPlayerEntity(playerId: string): void {
|
||||||
|
const entity = this._playerEntities.get(playerId);
|
||||||
|
if (entity) {
|
||||||
|
const despawnData = encodeDespawn(entity.id);
|
||||||
|
this.broadcastBinary(despawnData);
|
||||||
|
entity.destroy();
|
||||||
|
this._playerEntities.delete(playerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// State Sync | 状态同步
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 广播二进制数据
|
||||||
|
* @en Broadcast binary data
|
||||||
|
*/
|
||||||
|
protected broadcastBinary(data: Uint8Array): void {
|
||||||
|
for (const player of this.players) {
|
||||||
|
this.sendBinary(player, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 发送二进制数据给指定玩家
|
||||||
|
* @en Send binary data to specific player
|
||||||
|
*/
|
||||||
|
protected sendBinary(player: Player<TPlayerData>, data: Uint8Array): void {
|
||||||
|
player.send('$sync', { data: Array.from(data) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 发送完整状态给玩家(用于玩家刚加入时)
|
||||||
|
* @en Send full state to player (for when player just joined)
|
||||||
|
*/
|
||||||
|
protected sendFullState(player: Player<TPlayerData>): void {
|
||||||
|
const entities = this._getSyncEntities();
|
||||||
|
if (entities.length === 0) return;
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
this._initComponentTrackers(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = encodeSnapshot(entities, SyncOperation.FULL);
|
||||||
|
this.sendBinary(player, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 广播实体生成
|
||||||
|
* @en Broadcast entity spawn
|
||||||
|
*/
|
||||||
|
protected broadcastSpawn(entity: Entity, prefabType?: string): void {
|
||||||
|
this._initComponentTrackers(entity);
|
||||||
|
const data = encodeSpawn(entity, prefabType);
|
||||||
|
this.broadcastBinary(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 广播增量状态更新
|
||||||
|
* @en Broadcast delta state update
|
||||||
|
*/
|
||||||
|
protected broadcastDelta(): void {
|
||||||
|
const entities = this._getSyncEntities();
|
||||||
|
const changedEntities = entities.filter(entity => this._hasChanges(entity));
|
||||||
|
|
||||||
|
if (changedEntities.length === 0) return;
|
||||||
|
|
||||||
|
const data = encodeSnapshot(changedEntities, SyncOperation.DELTA);
|
||||||
|
this.broadcastBinary(data);
|
||||||
|
this._clearChangeTrackers(changedEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Lifecycle Overrides | 生命周期重载
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 游戏循环,处理状态同步
|
||||||
|
* @en Game tick, handles state sync
|
||||||
|
*/
|
||||||
|
override onTick(_dt: number): void {
|
||||||
|
if (this.ecsConfig.enableDeltaSync) {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - this._lastSyncTime >= this.ecsConfig.syncInterval) {
|
||||||
|
this._lastSyncTime = now;
|
||||||
|
this.broadcastDelta();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 玩家离开时自动销毁其实体
|
||||||
|
* @en Auto destroy player entity when leaving
|
||||||
|
*/
|
||||||
|
override async onLeave(player: Player<TPlayerData>, reason?: string): Promise<void> {
|
||||||
|
this.destroyPlayerEntity(player.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @zh 房间销毁时从 WorldManager 移除 World
|
||||||
|
* @en Remove World from WorldManager when room is disposed
|
||||||
|
*/
|
||||||
|
override onDispose(): void {
|
||||||
|
this._playerEntities.clear();
|
||||||
|
Core.worldManager.removeWorld(this.worldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Internal | 内部方法
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
private _getSyncEntities(): Entity[] {
|
||||||
|
const entities: Entity[] = [];
|
||||||
|
for (const entity of this.scene.entities.buffer) {
|
||||||
|
if (this._hasSyncComponents(entity)) {
|
||||||
|
entities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasSyncComponents(entity: Entity): boolean {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const metadata: SyncMetadata | undefined = (component.constructor as any)[SYNC_METADATA];
|
||||||
|
if (metadata && metadata.fields.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hasChanges(entity: Entity): boolean {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (tracker?.hasChanges()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _initComponentTrackers(entity: Entity): void {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const metadata: SyncMetadata | undefined = (component.constructor as any)[SYNC_METADATA];
|
||||||
|
if (metadata && metadata.fields.length > 0) {
|
||||||
|
initChangeTracker(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearChangeTrackers(entities: Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
for (const component of entity.components) {
|
||||||
|
const tracker = (component as any)[CHANGE_TRACKER] as ChangeTracker | undefined;
|
||||||
|
if (tracker) {
|
||||||
|
tracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
packages/framework/server/src/ecs/index.ts
Normal file
62
packages/framework/server/src/ecs/index.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* @zh @esengine/server ECS 集成模块
|
||||||
|
* @en @esengine/server ECS integration module
|
||||||
|
*
|
||||||
|
* @zh 提供带 ECS World 的房间类,支持基于 @sync 装饰器的自动状态同步
|
||||||
|
* @en Provides Room class with ECS World, supports automatic state sync based on @sync decorator
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { ECSRoom } 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;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* class GameRoom extends ECSRoom {
|
||||||
|
* onCreate() {
|
||||||
|
* this.addSystem(new MovementSystem());
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* onJoin(player: Player) {
|
||||||
|
* const entity = this.createPlayerEntity(player.id);
|
||||||
|
* entity.addComponent(new PlayerComponent());
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { ECSRoom } from './ECSRoom.js';
|
||||||
|
export type { ECSRoomConfig } from './ECSRoom.js';
|
||||||
|
|
||||||
|
// Re-export Player for convenience
|
||||||
|
export { Player, type IPlayer } from '../room/Player.js';
|
||||||
|
|
||||||
|
// Re-export commonly used ECS types for convenience
|
||||||
|
export type {
|
||||||
|
Entity,
|
||||||
|
Component,
|
||||||
|
EntitySystem,
|
||||||
|
Scene,
|
||||||
|
World,
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Re-export sync types
|
||||||
|
export {
|
||||||
|
sync,
|
||||||
|
getSyncMetadata,
|
||||||
|
hasSyncFields,
|
||||||
|
initChangeTracker,
|
||||||
|
clearChanges,
|
||||||
|
hasChanges,
|
||||||
|
SyncOperation,
|
||||||
|
type SyncType,
|
||||||
|
type SyncFieldMetadata,
|
||||||
|
type SyncMetadata,
|
||||||
|
} from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
// Re-export room decorators
|
||||||
|
export { onMessage } from '../room/decorators.js';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from 'tsup'
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: [
|
entry: [
|
||||||
@@ -6,12 +6,13 @@ export default defineConfig({
|
|||||||
'src/auth/index.ts',
|
'src/auth/index.ts',
|
||||||
'src/auth/testing/index.ts',
|
'src/auth/testing/index.ts',
|
||||||
'src/ratelimit/index.ts',
|
'src/ratelimit/index.ts',
|
||||||
'src/testing/index.ts'
|
'src/testing/index.ts',
|
||||||
|
'src/ecs/index.ts',
|
||||||
],
|
],
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
dts: true,
|
dts: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
external: ['ws', 'jsonwebtoken', '@esengine/rpc', '@esengine/rpc/codec'],
|
external: ['ws', 'jsonwebtoken', '@esengine/rpc', '@esengine/rpc/codec', '@esengine/ecs-framework'],
|
||||||
treeshake: true,
|
treeshake: true,
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# @esengine/spatial
|
# @esengine/spatial
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 1.0.4
|
## 1.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/spatial",
|
"name": "@esengine/spatial",
|
||||||
"version": "1.0.4",
|
"version": "3.0.1",
|
||||||
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
|
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,37 @@
|
|||||||
# @esengine/timer
|
# @esengine/timer
|
||||||
|
|
||||||
|
## 3.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`04b08f3`](https://github.com/esengine/esengine/commit/04b08f3f073d69beb8f4be399c774bea0acb612e)]:
|
||||||
|
- @esengine/ecs-framework@2.6.1
|
||||||
|
- @esengine/blueprint@3.0.1
|
||||||
|
|
||||||
|
## 3.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/ecs-framework@2.6.0
|
||||||
|
- @esengine/blueprint@3.0.0
|
||||||
|
|
||||||
|
## 2.0.1
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`a08a84b`](https://github.com/esengine/esengine/commit/a08a84b7db28e1140cbc637d442552747ad81c76)]:
|
||||||
|
- @esengine/ecs-framework@2.5.1
|
||||||
|
- @esengine/blueprint@2.0.1
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/ecs-framework@2.5.0
|
||||||
|
- @esengine/blueprint@2.0.0
|
||||||
|
|
||||||
## 1.0.3
|
## 1.0.3
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/timer",
|
"name": "@esengine/timer",
|
||||||
"version": "1.0.3",
|
"version": "3.0.1",
|
||||||
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
|
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
# @esengine/transaction
|
# @esengine/transaction
|
||||||
|
|
||||||
|
## 2.0.4
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/server@3.0.0
|
||||||
|
|
||||||
|
## 2.0.3
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies [[`1f297ac`](https://github.com/esengine/esengine/commit/1f297ac769e37700f72fb4425639af7090898256)]:
|
||||||
|
- @esengine/server@2.0.0
|
||||||
|
|
||||||
## 2.0.2
|
## 2.0.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/transaction",
|
"name": "@esengine/transaction",
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"description": "Game transaction system with distributed support | 游戏事务系统,支持分布式事务",
|
"description": "Game transaction system with distributed support | 游戏事务系统,支持分布式事务",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ export class WeChatRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
|
|||||||
// 导入 Rapier2D 标准版
|
// 导入 Rapier2D 标准版
|
||||||
const RAPIER = await import('@esengine/rapier2d');
|
const RAPIER = await import('@esengine/rapier2d');
|
||||||
|
|
||||||
// 初始化 WASM - 标准版需要提供 WASM 路径
|
// 初始化 WASM - WASM 已经作为 base64 嵌入到包中
|
||||||
const wasmPath = this._config.minigame?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
|
// Initialize WASM - WASM is embedded as base64 in the package
|
||||||
await RAPIER.init(wasmPath);
|
await RAPIER.init();
|
||||||
|
|
||||||
return RAPIER;
|
return RAPIER;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -53,10 +53,9 @@ export class WebRapier2DLoader implements IWasmLibraryLoader<RapierModule> {
|
|||||||
// 动态导入标准版
|
// 动态导入标准版
|
||||||
const RAPIER = await import('@esengine/rapier2d');
|
const RAPIER = await import('@esengine/rapier2d');
|
||||||
|
|
||||||
// 初始化 WASM - 标准版需要提供 WASM 路径
|
// 初始化 WASM - WASM 已经作为 base64 嵌入到包中
|
||||||
// 构建时 WASM 文件会被复制到 wasm/ 目录
|
// Initialize WASM - WASM is embedded as base64 in the package
|
||||||
const wasmPath = this._config.web?.wasmPath || 'wasm/rapier_wasm2d_bg.wasm';
|
await RAPIER.init();
|
||||||
await RAPIER.init(wasmPath);
|
|
||||||
|
|
||||||
console.log(`[${this._config.name}] 加载完成`);
|
console.log(`[${this._config.name}] 加载完成`);
|
||||||
return RAPIER;
|
return RAPIER;
|
||||||
|
|||||||
@@ -19,11 +19,13 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"gen:src": "node scripts/gen-src.mjs",
|
"gen:src": "node scripts/gen-src.mjs",
|
||||||
"build": "pnpm gen:src && tsup",
|
"build": "tsup",
|
||||||
"clean": "rimraf dist src"
|
"build:regen": "pnpm gen:src && tsup",
|
||||||
|
"clean": "rimraf dist"
|
||||||
},
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
"tsup": "^8.0.0",
|
"tsup": "^8.0.0",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
|
|||||||
@@ -91,9 +91,8 @@ export class KinematicCharacterController {
|
|||||||
*/
|
*/
|
||||||
public setUp(vector: Vector) {
|
public setUp(vector: Vector) {
|
||||||
let rawVect = VectorOps.intoRaw(vector);
|
let rawVect = VectorOps.intoRaw(vector);
|
||||||
const result = this.raw.setUp(rawVect);
|
return this.raw.setUp(rawVect);
|
||||||
rawVect.free();
|
rawVect.free();
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public applyImpulsesToDynamicBodies(): boolean {
|
public applyImpulsesToDynamicBodies(): boolean {
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ export class DynamicRayCastVehicleController {
|
|||||||
bodies: RigidBodySet,
|
bodies: RigidBodySet,
|
||||||
colliders: ColliderSet,
|
colliders: ColliderSet,
|
||||||
) {
|
) {
|
||||||
if (typeof RawDynamicRayCastVehicleController === 'undefined') {
|
|
||||||
throw new Error('DynamicRayCastVehicleController is not available in 2D mode');
|
|
||||||
}
|
|
||||||
this.raw = new RawDynamicRayCastVehicleController(chassis.handle);
|
this.raw = new RawDynamicRayCastVehicleController(chassis.handle);
|
||||||
this.broadPhase = broadPhase;
|
this.broadPhase = broadPhase;
|
||||||
this.narrowPhase = narrowPhase;
|
this.narrowPhase = narrowPhase;
|
||||||
|
|||||||
@@ -1,60 +1,12 @@
|
|||||||
/**
|
// @ts-ignore
|
||||||
* RAPIER initialization module with dynamic WASM loading support.
|
import wasmBase64 from "../pkg/rapier_wasm2d_bg.wasm";
|
||||||
* RAPIER 初始化模块,支持动态 WASM 加载。
|
|
||||||
*/
|
|
||||||
|
|
||||||
import wasmInit from "../pkg/rapier_wasm2d";
|
import wasmInit from "../pkg/rapier_wasm2d";
|
||||||
|
import base64 from "base64-js";
|
||||||
/**
|
|
||||||
* Input types for WASM initialization.
|
|
||||||
* WASM 初始化的输入类型。
|
|
||||||
*/
|
|
||||||
export type InitInput =
|
|
||||||
| RequestInfo // URL string or Request object
|
|
||||||
| URL // URL object
|
|
||||||
| Response // Fetch Response object
|
|
||||||
| BufferSource // ArrayBuffer or TypedArray
|
|
||||||
| WebAssembly.Module; // Pre-compiled module
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes RAPIER.
|
* Initializes RAPIER.
|
||||||
* Has to be called and awaited before using any library methods.
|
* Has to be called and awaited before using any library methods.
|
||||||
*
|
|
||||||
* 初始化 RAPIER。
|
|
||||||
* 必须在使用任何库方法之前调用并等待。
|
|
||||||
*
|
|
||||||
* @param input - WASM source (required). Can be URL, Response, ArrayBuffer, etc.
|
|
||||||
* WASM 源(必需)。可以是 URL、Response、ArrayBuffer 等。
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Load from URL | 从 URL 加载
|
|
||||||
* await RAPIER.init('wasm/rapier_wasm2d_bg.wasm');
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Load from fetch response | 从 fetch 响应加载
|
|
||||||
* const response = await fetch('wasm/rapier_wasm2d_bg.wasm');
|
|
||||||
* await RAPIER.init(response);
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* // Load from ArrayBuffer | 从 ArrayBuffer 加载
|
|
||||||
* const buffer = await fetch('wasm/rapier_wasm2d_bg.wasm').then(r => r.arrayBuffer());
|
|
||||||
* await RAPIER.init(buffer);
|
|
||||||
*/
|
*/
|
||||||
export async function init(input?: InitInput): Promise<void> {
|
export async function init() {
|
||||||
if (initialized) {
|
await wasmInit(base64.toByteArray(wasmBase64 as unknown as string).buffer);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await wasmInit(input);
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if RAPIER is already initialized.
|
|
||||||
* 检查 RAPIER 是否已初始化。
|
|
||||||
*/
|
|
||||||
export function isInitialized(): boolean {
|
|
||||||
return initialized;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class VectorOps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: type ram: RawVector?
|
// FIXME: type ram: RawVector?
|
||||||
public static fromRaw(raw: RawVector): Vector | null {
|
public static fromRaw(raw: RawVector): Vector {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
let res = VectorOps.new(raw.x, raw.y);
|
let res = VectorOps.new(raw.x, raw.y);
|
||||||
@@ -56,7 +56,7 @@ export class RotationOps {
|
|||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static fromRaw(raw: RawRotation): Rotation | null {
|
public static fromRaw(raw: RawRotation): Rotation {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
|
|
||||||
let res = raw.angle;
|
let res = raw.angle;
|
||||||
|
|||||||
@@ -7,4 +7,7 @@ export default defineConfig({
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
clean: true,
|
clean: true,
|
||||||
external: ["../pkg/rapier_wasm2d.js"],
|
external: ["../pkg/rapier_wasm2d.js"],
|
||||||
|
loader: {
|
||||||
|
".wasm": "base64",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,49 @@
|
|||||||
# @esengine/demos
|
# @esengine/demos
|
||||||
|
|
||||||
|
## 1.0.8
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@3.0.1
|
||||||
|
- @esengine/pathfinding@3.0.1
|
||||||
|
- @esengine/procgen@3.0.1
|
||||||
|
- @esengine/spatial@3.0.1
|
||||||
|
- @esengine/timer@3.0.1
|
||||||
|
|
||||||
|
## 1.0.7
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@3.0.0
|
||||||
|
- @esengine/pathfinding@3.0.0
|
||||||
|
- @esengine/procgen@3.0.0
|
||||||
|
- @esengine/spatial@3.0.0
|
||||||
|
- @esengine/timer@3.0.0
|
||||||
|
|
||||||
|
## 1.0.6
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@2.0.1
|
||||||
|
- @esengine/pathfinding@2.0.1
|
||||||
|
- @esengine/procgen@2.0.1
|
||||||
|
- @esengine/spatial@2.0.1
|
||||||
|
- @esengine/timer@2.0.1
|
||||||
|
|
||||||
|
## 1.0.5
|
||||||
|
|
||||||
|
### Patch Changes
|
||||||
|
|
||||||
|
- Updated dependencies []:
|
||||||
|
- @esengine/fsm@2.0.0
|
||||||
|
- @esengine/pathfinding@2.0.0
|
||||||
|
- @esengine/procgen@2.0.0
|
||||||
|
- @esengine/spatial@2.0.0
|
||||||
|
- @esengine/timer@2.0.0
|
||||||
|
|
||||||
## 1.0.4
|
## 1.0.4
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/demos",
|
"name": "@esengine/demos",
|
||||||
"version": "1.0.4",
|
"version": "1.0.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Demo tests for ESEngine modules documentation",
|
"description": "Demo tests for ESEngine modules documentation",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
75
pnpm-lock.yaml
generated
75
pnpm-lock.yaml
generated
@@ -569,6 +569,9 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../framework/behavior-tree
|
version: link:../../../framework/behavior-tree
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@esengine/asset-system':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../../engine/asset-system
|
||||||
'@esengine/build-config':
|
'@esengine/build-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../tools/build-config
|
version: link:../../../tools/build-config
|
||||||
@@ -1489,6 +1492,9 @@ importers:
|
|||||||
rollup-plugin-dts:
|
rollup-plugin-dts:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.3.0(rollup@4.54.0)(typescript@5.9.3)
|
version: 6.3.0(rollup@4.54.0)(typescript@5.9.3)
|
||||||
|
rollup-plugin-sourcemaps:
|
||||||
|
specifier: ^0.6.3
|
||||||
|
version: 0.6.3(@types/node@20.19.27)(rollup@4.54.0)
|
||||||
ts-jest:
|
ts-jest:
|
||||||
specifier: ^29.4.0
|
specifier: ^29.4.0
|
||||||
version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.27)(ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@5.9.3)))(typescript@5.9.3)
|
version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.27)(ts-node@10.9.2(@swc/core@1.15.7(@swc/helpers@0.5.18))(@swc/wasm@1.15.7)(@types/node@20.19.27)(typescript@5.9.3)))(typescript@5.9.3)
|
||||||
@@ -1688,6 +1694,9 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../rpc
|
version: link:../rpc
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@esengine/ecs-framework':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../core/dist
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.10
|
version: 9.0.10
|
||||||
@@ -1858,6 +1867,9 @@ importers:
|
|||||||
|
|
||||||
packages/physics/rapier2d:
|
packages/physics/rapier2d:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
base64-js:
|
||||||
|
specifier: ^1.5.1
|
||||||
|
version: 1.5.1
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.10
|
version: 5.0.10
|
||||||
@@ -4481,6 +4493,12 @@ packages:
|
|||||||
rollup:
|
rollup:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@rollup/pluginutils@3.1.0':
|
||||||
|
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
||||||
|
engines: {node: '>= 8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
rollup: ^1.20.0||^2.0.0
|
||||||
|
|
||||||
'@rollup/pluginutils@5.3.0':
|
'@rollup/pluginutils@5.3.0':
|
||||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -5147,6 +5165,9 @@ packages:
|
|||||||
'@types/estree-jsx@1.0.5':
|
'@types/estree-jsx@1.0.5':
|
||||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||||
|
|
||||||
|
'@types/estree@0.0.39':
|
||||||
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
|
|
||||||
'@types/estree@1.0.8':
|
'@types/estree@1.0.8':
|
||||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||||
|
|
||||||
@@ -5788,6 +5809,11 @@ packages:
|
|||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
|
atob@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==}
|
||||||
|
engines: {node: '>= 4.5.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
axios@1.13.2:
|
axios@1.13.2:
|
||||||
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
|
||||||
|
|
||||||
@@ -6431,6 +6457,10 @@ packages:
|
|||||||
decode-named-character-reference@1.2.0:
|
decode-named-character-reference@1.2.0:
|
||||||
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
resolution: {integrity: sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==}
|
||||||
|
|
||||||
|
decode-uri-component@0.2.2:
|
||||||
|
resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
|
||||||
dedent@1.5.3:
|
dedent@1.5.3:
|
||||||
resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==}
|
resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -6835,6 +6865,9 @@ packages:
|
|||||||
estree-util-visit@2.0.0:
|
estree-util-visit@2.0.0:
|
||||||
resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
|
resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==}
|
||||||
|
|
||||||
|
estree-walker@1.0.1:
|
||||||
|
resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
|
||||||
|
|
||||||
estree-walker@2.0.2:
|
estree-walker@2.0.2:
|
||||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||||
|
|
||||||
@@ -9841,6 +9874,16 @@ packages:
|
|||||||
rollup: ^3.29.4 || ^4
|
rollup: ^3.29.4 || ^4
|
||||||
typescript: ^4.5 || ^5.0
|
typescript: ^4.5 || ^5.0
|
||||||
|
|
||||||
|
rollup-plugin-sourcemaps@0.6.3:
|
||||||
|
resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/node': '>=10.0.0'
|
||||||
|
rollup: '>=0.31.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/node':
|
||||||
|
optional: true
|
||||||
|
|
||||||
rollup@4.54.0:
|
rollup@4.54.0:
|
||||||
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
@@ -10040,6 +10083,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
source-map-resolve@0.6.0:
|
||||||
|
resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==}
|
||||||
|
deprecated: See https://github.com/lydell/source-map-resolve#deprecated
|
||||||
|
|
||||||
source-map-support@0.5.13:
|
source-map-support@0.5.13:
|
||||||
resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==}
|
resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==}
|
||||||
|
|
||||||
@@ -14125,6 +14172,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
rollup: 4.54.0
|
rollup: 4.54.0
|
||||||
|
|
||||||
|
'@rollup/pluginutils@3.1.0(rollup@4.54.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 0.0.39
|
||||||
|
estree-walker: 1.0.1
|
||||||
|
picomatch: 2.3.1
|
||||||
|
rollup: 4.54.0
|
||||||
|
|
||||||
'@rollup/pluginutils@5.3.0(rollup@4.54.0)':
|
'@rollup/pluginutils@5.3.0(rollup@4.54.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -14875,6 +14929,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
|
|
||||||
|
'@types/estree@0.0.39': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
||||||
'@types/express-serve-static-core@5.1.0':
|
'@types/express-serve-static-core@5.1.0':
|
||||||
@@ -15742,6 +15798,8 @@ snapshots:
|
|||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
|
atob@2.1.2: {}
|
||||||
|
|
||||||
axios@1.13.2:
|
axios@1.13.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.11
|
follow-redirects: 1.15.11
|
||||||
@@ -16421,6 +16479,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
character-entities: 2.0.2
|
character-entities: 2.0.2
|
||||||
|
|
||||||
|
decode-uri-component@0.2.2: {}
|
||||||
|
|
||||||
dedent@1.5.3: {}
|
dedent@1.5.3: {}
|
||||||
|
|
||||||
dedent@1.7.1: {}
|
dedent@1.7.1: {}
|
||||||
@@ -16871,6 +16931,8 @@ snapshots:
|
|||||||
'@types/estree-jsx': 1.0.5
|
'@types/estree-jsx': 1.0.5
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
|
estree-walker@1.0.1: {}
|
||||||
|
|
||||||
estree-walker@2.0.2: {}
|
estree-walker@2.0.2: {}
|
||||||
|
|
||||||
estree-walker@3.0.3:
|
estree-walker@3.0.3:
|
||||||
@@ -20693,6 +20755,14 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
|
|
||||||
|
rollup-plugin-sourcemaps@0.6.3(@types/node@20.19.27)(rollup@4.54.0):
|
||||||
|
dependencies:
|
||||||
|
'@rollup/pluginutils': 3.1.0(rollup@4.54.0)
|
||||||
|
rollup: 4.54.0
|
||||||
|
source-map-resolve: 0.6.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/node': 20.19.27
|
||||||
|
|
||||||
rollup@4.54.0:
|
rollup@4.54.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.8
|
'@types/estree': 1.0.8
|
||||||
@@ -20987,6 +21057,11 @@ snapshots:
|
|||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
|
source-map-resolve@0.6.0:
|
||||||
|
dependencies:
|
||||||
|
atob: 2.1.2
|
||||||
|
decode-uri-component: 0.2.2
|
||||||
|
|
||||||
source-map-support@0.5.13:
|
source-map-support@0.5.13:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-from: 1.1.2
|
buffer-from: 1.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user