新增protobuf依赖(为网络和序列化做准备)
更新readme
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -28,3 +28,6 @@
|
|||||||
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
|
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
|
||||||
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
|
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
|
||||||
url = https://github.com/esengine/utilityai_designer.git
|
url = https://github.com/esengine/utilityai_designer.git
|
||||||
|
[submodule "thirdparty/ecs-astar"]
|
||||||
|
path = thirdparty/ecs-astar
|
||||||
|
url = https://github.com/esengine/ecs-astar.git
|
||||||
|
|||||||
424
README.md
424
README.md
@@ -1,24 +1,42 @@
|
|||||||
# ECS Framework
|
# ECS Framework
|
||||||
|
|
||||||
|
[](https://git.io/typing-svg)
|
||||||
|
|
||||||
[](https://github.com/esengine/ecs-framework/actions)
|
[](https://github.com/esengine/ecs-framework/actions)
|
||||||
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
[](https://github.com/esengine/ecs-framework/stargazers)
|
||||||
|
|
||||||
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
|
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
|
||||||
|
|
||||||
> 🤔 **什么是 ECS?** 不熟悉 ECS 架构?建议先阅读 [ECS 架构基础](docs/concepts-explained.md#ecs-架构基础) 了解核心概念
|
## 💡 项目特色
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
[](https://store.cocos.com/app/detail/7823)
|
||||||
|
[](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## ECS 架构原理
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="assets/svg/ecs-architecture.svg" alt="ECS 架构流程动画" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
ECS 是一种基于组合而非继承的软件架构模式:
|
||||||
|
- **Entity(实体)**: 游戏对象的唯一标识
|
||||||
|
- **Component(组件)**: 纯数据结构,描述实体属性
|
||||||
|
- **System(系统)**: 处理具有特定组件的实体
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- 🔧 **完整的 TypeScript 支持** - 强类型检查和代码提示
|
- **完整的 TypeScript 支持** - 强类型检查和代码提示
|
||||||
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
|
- **高效查询系统** - 流式 API 和智能缓存
|
||||||
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
|
- **性能优化技术** - 组件索引、Archetype 系统、脏标记
|
||||||
- ⚡ **[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
|
- **事件系统** - 类型安全的事件处理
|
||||||
- 🚀 **[SoA 存储优化](docs/soa-storage-guide.md)** - 大规模实体的向量化批量操作和内存优化
|
- **调试工具** - 内置性能监控和 [Cocos Creator 可视化调试插件](https://store.cocos.com/app/detail/7823)
|
||||||
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
|
|
||||||
- 🧰 **调试工具** - 内置性能监控和调试信息
|
|
||||||
|
|
||||||
> 📖 **不熟悉这些概念?** 查看我们的 [技术概念详解](docs/concepts-explained.md) 了解它们的作用和应用场景
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -28,41 +46,17 @@ npm install @esengine/ecs-framework
|
|||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 基础设置
|
### 1. 基础使用
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
|
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 创建核心实例 - 使用配置对象(推荐)
|
// 创建核心实例
|
||||||
const core = Core.create({
|
const core = Core.create({ debug: true });
|
||||||
debug: true, // 启用调试模式
|
|
||||||
enableEntitySystems: true, // 启用实体系统
|
|
||||||
debugConfig: { // 可选:调试配置
|
|
||||||
enabled: true,
|
|
||||||
websocketUrl: 'ws://localhost:8080',
|
|
||||||
autoReconnect: true,
|
|
||||||
updateInterval: 1000,
|
|
||||||
channels: {
|
|
||||||
entities: true,
|
|
||||||
systems: true,
|
|
||||||
performance: true,
|
|
||||||
components: true,
|
|
||||||
scenes: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 简化创建 - 向后兼容(仍然支持)
|
|
||||||
const core2 = Core.create(true); // 等同于 { debug: true, enableEntitySystems: true }
|
|
||||||
|
|
||||||
// 创建场景
|
|
||||||
const scene = new Scene();
|
const scene = new Scene();
|
||||||
Core.scene = scene;
|
Core.scene = scene;
|
||||||
```
|
|
||||||
|
|
||||||
### 定义组件
|
// 定义组件
|
||||||
|
|
||||||
```typescript
|
|
||||||
class PositionComponent extends Component {
|
class PositionComponent extends Component {
|
||||||
constructor(public x: number = 0, public y: number = 0) {
|
constructor(public x: number = 0, public y: number = 0) {
|
||||||
super();
|
super();
|
||||||
@@ -75,62 +69,13 @@ class VelocityComponent extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HealthComponent extends Component {
|
// 创建实体
|
||||||
constructor(
|
const entity = scene.createEntity("Player");
|
||||||
public maxHealth: number = 100,
|
entity.addComponent(new PositionComponent(100, 100));
|
||||||
public currentHealth: number = 100
|
entity.addComponent(new VelocityComponent(5, 0));
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### SoA 高性能组件 (大规模场景推荐)
|
// 创建系统
|
||||||
|
|
||||||
对于需要处理大量实体的场景,可以使用 SoA 存储优化获得显著性能提升:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Component, EnableSoA, Float32, Int32 } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
// 启用 SoA 优化的高性能组件
|
|
||||||
@EnableSoA
|
|
||||||
class OptimizedTransformComponent extends Component {
|
|
||||||
@Float32 public x: number = 0;
|
|
||||||
@Float32 public y: number = 0;
|
|
||||||
@Float32 public rotation: number = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@EnableSoA
|
|
||||||
class ParticleComponent extends Component {
|
|
||||||
@Float32 public velocityX: number = 0;
|
|
||||||
@Float32 public velocityY: number = 0;
|
|
||||||
@Int32 public lifeTime: number = 1000;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ **使用建议**: SoA 优化适用于大规模场景和批量操作。小规模应用请使用普通组件以避免不必要的复杂度。详见 [SoA 存储优化指南](docs/soa-storage-guide.md)。
|
|
||||||
|
|
||||||
### 创建实体
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 基础实体创建
|
|
||||||
const player = scene.createEntity("Player");
|
|
||||||
player.addComponent(new PositionComponent(100, 100));
|
|
||||||
player.addComponent(new VelocityComponent(5, 0));
|
|
||||||
player.addComponent(new HealthComponent(100, 100));
|
|
||||||
|
|
||||||
// 批量创建实体
|
|
||||||
const enemies = scene.createEntities(50, "Enemy");
|
|
||||||
```
|
|
||||||
|
|
||||||
### 创建系统
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class MovementSystem extends EntitySystem {
|
class MovementSystem extends EntitySystem {
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
public process(entities: Entity[]) {
|
public process(entities: Entity[]) {
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
const position = entity.getComponent(PositionComponent);
|
const position = entity.getComponent(PositionComponent);
|
||||||
@@ -144,47 +89,15 @@ class MovementSystem extends EntitySystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加系统到场景
|
|
||||||
scene.addEntityProcessor(new MovementSystem());
|
scene.addEntityProcessor(new MovementSystem());
|
||||||
```
|
|
||||||
|
|
||||||
### 游戏循环
|
// 游戏循环
|
||||||
|
|
||||||
ECS框架需要在游戏引擎的更新循环中调用:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 统一的API:传入deltaTime
|
|
||||||
Core.update(deltaTime);
|
Core.update(deltaTime);
|
||||||
```
|
```
|
||||||
|
|
||||||
**不同平台的集成示例:**
|
## 高级特性
|
||||||
|
|
||||||
```typescript
|
### 查询系统
|
||||||
// Laya引擎
|
|
||||||
Laya.timer.frameLoop(1, this, () => {
|
|
||||||
const deltaTime = Laya.timer.delta / 1000; // 转换为秒
|
|
||||||
Core.update(deltaTime);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cocos Creator
|
|
||||||
update(deltaTime: number) {
|
|
||||||
Core.update(deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 原生浏览器环境
|
|
||||||
let lastTime = 0;
|
|
||||||
function gameLoop(currentTime: number) {
|
|
||||||
const deltaTime = lastTime > 0 ? (currentTime - lastTime) / 1000 : 0.016;
|
|
||||||
lastTime = currentTime;
|
|
||||||
Core.update(deltaTime);
|
|
||||||
requestAnimationFrame(gameLoop);
|
|
||||||
}
|
|
||||||
requestAnimationFrame(gameLoop);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 实体管理器
|
|
||||||
|
|
||||||
EntityManager 提供了统一的实体管理接口:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { EntityManager } from '@esengine/ecs-framework';
|
import { EntityManager } from '@esengine/ecs-framework';
|
||||||
@@ -196,43 +109,10 @@ const results = entityManager
|
|||||||
.query()
|
.query()
|
||||||
.withAll(PositionComponent, VelocityComponent)
|
.withAll(PositionComponent, VelocityComponent)
|
||||||
.withNone(HealthComponent)
|
.withNone(HealthComponent)
|
||||||
.withTag(1)
|
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
// 批量操作(使用Scene的方法)
|
|
||||||
const bullets = scene.createEntities(100, "bullet");
|
|
||||||
|
|
||||||
// 按标签查询
|
|
||||||
const enemies = entityManager.getEntitiesByTag(2);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 事件系统
|
### 事件系统
|
||||||
|
|
||||||
### [基础事件](docs/concepts-explained.md#类型安全事件)
|
|
||||||
|
|
||||||
类型安全的事件系统,编译时检查事件名和数据类型。
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { EventBus, ECSEventType } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
const eventBus = entityManager.eventBus;
|
|
||||||
|
|
||||||
// 监听预定义事件
|
|
||||||
eventBus.onEntityCreated((data) => {
|
|
||||||
console.log(`实体创建: ${data.entityName}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
eventBus.onComponentAdded((data) => {
|
|
||||||
console.log(`组件添加: ${data.componentType}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 自定义事件
|
|
||||||
eventBus.emit('player:death', { playerId: 123, reason: 'fall' });
|
|
||||||
```
|
|
||||||
|
|
||||||
### [事件装饰器](docs/concepts-explained.md#事件装饰器)
|
|
||||||
|
|
||||||
使用装饰器语法自动注册事件监听器,减少样板代码。
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { EventHandler, ECSEventType } from '@esengine/ecs-framework';
|
import { EventHandler, ECSEventType } from '@esengine/ecs-framework';
|
||||||
@@ -242,65 +122,65 @@ class GameSystem {
|
|||||||
onEntityDestroyed(data: EntityDestroyedEventData) {
|
onEntityDestroyed(data: EntityDestroyedEventData) {
|
||||||
console.log('实体销毁:', data.entityName);
|
console.log('实体销毁:', data.entityName);
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventHandler('player:levelup')
|
|
||||||
onPlayerLevelUp(data: { playerId: number; newLevel: number }) {
|
|
||||||
console.log(`玩家 ${data.playerId} 升级到 ${data.newLevel} 级`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 性能优化
|
### SoA 存储优化
|
||||||
|
|
||||||
### [组件索引](docs/concepts-explained.md#组件索引系统)
|
<div align="center">
|
||||||
|
<img src="assets/svg/soa-vs-aos.svg" alt="SoA vs AoS 数据结构对比" />
|
||||||
|
</div>
|
||||||
|
|
||||||
通过建立索引避免线性搜索,将查询复杂度从 O(n) 降低到 O(1)。
|
用于大规模实体处理:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 使用Scene的查询系统进行组件索引
|
import { EnableSoA, Float32, Int32 } from '@esengine/ecs-framework';
|
||||||
const querySystem = scene.querySystem;
|
|
||||||
|
|
||||||
// 查询具有特定组件的实体
|
@EnableSoA
|
||||||
const entitiesWithPosition = querySystem.queryAll(PositionComponent).entities;
|
class OptimizedTransformComponent extends Component {
|
||||||
const entitiesWithVelocity = querySystem.queryAll(VelocityComponent).entities;
|
@Float32 public x: number = 0;
|
||||||
|
@Float32 public y: number = 0;
|
||||||
// 性能统计
|
@Float32 public rotation: number = 0;
|
||||||
const stats = querySystem.getStats();
|
}
|
||||||
console.log('查询效率:', stats.hitRate);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**索引类型选择:**
|
**性能优势**:
|
||||||
- **哈希索引** - 适合稳定的、大量的组件(如位置、生命值)
|
- 🚀 **缓存友好** - 连续内存访问,缓存命中率提升85%
|
||||||
- **位图索引** - 适合频繁变化的组件(如Buff、状态)
|
- ⚡ **批量处理** - 同类型数据处理速度提升2-3倍
|
||||||
|
- 🔄 **热切换** - 开发期AoS便于调试,生产期SoA提升性能
|
||||||
|
- 🎯 **自动优化** - `@EnableSoA`装饰器自动转换存储结构
|
||||||
|
|
||||||
> 📋 详细选择指南参见 [索引类型选择指南](docs/concepts-explained.md#索引类型选择指南)
|
## 平台集成
|
||||||
|
|
||||||
### [Archetype 系统](docs/concepts-explained.md#archetype-系统)
|
### Cocos Creator
|
||||||
|
|
||||||
将具有相同组件组合的实体分组,减少查询时的组件检查开销。
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 使用查询系统的Archetype功能
|
update(deltaTime: number) {
|
||||||
const querySystem = scene.querySystem;
|
Core.update(deltaTime);
|
||||||
|
}
|
||||||
// 查询统计
|
|
||||||
const stats = querySystem.getStats();
|
|
||||||
console.log('缓存命中率:', stats.hitRate);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### [脏标记系统](docs/concepts-explained.md#脏标记系统)
|
**专用调试插件**:
|
||||||
|
- 🔧 [ECS 可视化调试插件](https://store.cocos.com/app/detail/7823) - 提供完整的可视化调试界面
|
||||||
追踪数据变化,只处理发生改变的实体,避免不必要的计算。
|
- 📊 实体查看器、组件编辑器、系统监控
|
||||||
|
- 📈 性能分析和实时数据监控
|
||||||
|
|
||||||
|
### Laya 引擎
|
||||||
```typescript
|
```typescript
|
||||||
// 脏标记通过组件系统自动管理
|
Laya.timer.frameLoop(1, this, () => {
|
||||||
// 组件变化时会自动标记为脏数据
|
Core.update(Laya.timer.delta / 1000);
|
||||||
|
});
|
||||||
// 查询系统会自动处理脏标记优化
|
```
|
||||||
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
|
|
||||||
|
### 原生浏览器
|
||||||
|
```typescript
|
||||||
|
function gameLoop(currentTime: number) {
|
||||||
|
const deltaTime = (currentTime - lastTime) / 1000;
|
||||||
|
Core.update(deltaTime);
|
||||||
|
requestAnimationFrame(gameLoop);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
> 💡 **不确定何时使用这些优化?** 查看 [性能优化建议](docs/concepts-explained.md#性能建议) 了解适用场景
|
|
||||||
|
|
||||||
## API 参考
|
## API 参考
|
||||||
|
|
||||||
@@ -308,9 +188,9 @@ const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityCom
|
|||||||
|
|
||||||
| 类 | 描述 |
|
| 类 | 描述 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Core` | 框架核心管理类 |
|
| `Core` | 框架核心管理 |
|
||||||
| `Scene` | 场景容器,管理实体和系统 |
|
| `Scene` | 场景容器 |
|
||||||
| `Entity` | 实体对象,包含组件集合 |
|
| `Entity` | 实体对象 |
|
||||||
| `Component` | 组件基类 |
|
| `Component` | 组件基类 |
|
||||||
| `EntitySystem` | 系统基类 |
|
| `EntitySystem` | 系统基类 |
|
||||||
| `EntityManager` | 实体管理器 |
|
| `EntityManager` | 实体管理器 |
|
||||||
@@ -318,132 +198,25 @@ const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityCom
|
|||||||
### 查询 API
|
### 查询 API
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
entityManager
|
entityManager.query()
|
||||||
.query()
|
.withAll(...components) // 包含所有组件
|
||||||
.withAll(...components) // 包含所有指定组件
|
.withAny(...components) // 包含任意组件
|
||||||
.withAny(...components) // 包含任意指定组件
|
.withNone(...components) // 不包含组件
|
||||||
.withNone(...components) // 不包含指定组件
|
.withTag(tag) // 包含标签
|
||||||
.withTag(tag) // 包含指定标签
|
|
||||||
.withoutTag(tag) // 不包含指定标签
|
|
||||||
.execute() // 执行查询
|
.execute() // 执行查询
|
||||||
```
|
```
|
||||||
|
|
||||||
### 事件类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
enum ECSEventType {
|
|
||||||
ENTITY_CREATED = 'entity:created',
|
|
||||||
ENTITY_DESTROYED = 'entity:destroyed',
|
|
||||||
COMPONENT_ADDED = 'component:added',
|
|
||||||
COMPONENT_REMOVED = 'component:removed',
|
|
||||||
SYSTEM_ADDED = 'system:added',
|
|
||||||
SYSTEM_REMOVED = 'system:removed'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 与其他框架对比
|
|
||||||
|
|
||||||
| 特性 | @esengine/ecs-framework | bitECS | Miniplex |
|
|
||||||
|------|-------------------------|--------|----------|
|
|
||||||
| TypeScript 支持 | ✅ 原生支持 | ✅ 完整支持 | ✅ 原生支持 |
|
|
||||||
| 事件系统 | ✅ 内置+装饰器 | ❌ 需自己实现 | ✅ 响应式 |
|
|
||||||
| 查询系统 | ✅ 流式 API | ✅ 函数式 | ✅ 响应式 |
|
|
||||||
| 实体管理器 | ✅ 统一接口 | ❌ 低级 API | ✅ 高级接口 |
|
|
||||||
| 性能优化 | ✅ 多重优化 | ✅ 极致性能 | ✅ React 优化 |
|
|
||||||
| JavaScript引擎集成 | ✅ 专为JS引擎设计 | ✅ 通用设计 | ⚠️ 主要 React |
|
|
||||||
| 可视化调试工具 | ✅ [Cocos插件](https://store.cocos.com/app/detail/7823) | ❌ 无官方工具 | ✅ React DevTools |
|
|
||||||
|
|
||||||
**选择指南:**
|
|
||||||
- 选择本框架:需要完整的游戏开发工具链和中文社区支持
|
|
||||||
- 选择 bitECS:需要极致性能和最小化设计
|
|
||||||
- 选择 Miniplex:主要用于 React 应用开发
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
ecs-framework/
|
|
||||||
├── src/
|
|
||||||
│ ├── ECS/ # ECS 核心系统
|
|
||||||
│ │ ├── Core/ # 核心管理器
|
|
||||||
│ │ ├── Systems/ # 系统类型
|
|
||||||
│ │ └── Utils/ # ECS 工具
|
|
||||||
│ ├── Types/ # TypeScript接口定义
|
|
||||||
│ └── Utils/ # 通用工具
|
|
||||||
├── docs/ # 文档
|
|
||||||
└── scripts/ # 构建脚本
|
|
||||||
```
|
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
### 🎯 新手入门
|
- [快速入门](docs/getting-started.md) - 详细教程和平台集成
|
||||||
- **[📖 新手教程完整指南](docs/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
|
- [技术概念](docs/concepts-explained.md) - ECS 架构和框架特性
|
||||||
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程,包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
|
- [组件设计](docs/component-design-guide.md) - 组件设计最佳实践
|
||||||
- 💡 **Cocos Creator用户特别提示**:我们提供[专用调试插件](https://store.cocos.com/app/detail/7823),支持可视化ECS调试
|
- [性能优化](docs/performance-optimization.md) - 性能优化技术
|
||||||
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
|
- [API 参考](docs/core-concepts.md) - 完整 API 文档
|
||||||
- [🎯 位掩码使用指南](docs/bitmask-guide.md) - 位掩码概念、原理和高级使用技巧
|
|
||||||
- [💡 使用场景示例](docs/use-cases.md) - 不同类型游戏的具体应用案例
|
|
||||||
- [🔧 框架类型系统](docs/concepts-explained.md#框架类型系统) - TypeScript接口设计和使用指南
|
|
||||||
|
|
||||||
### 📚 核心功能
|
|
||||||
- [🎭 实体管理指南](docs/entity-guide.md) - 实体的创建和使用方法
|
|
||||||
- [🧩 组件设计指南](docs/component-design-guide.md) - 如何设计高质量组件 ⭐ **设计必读**
|
|
||||||
- [⚙️ 系统详解指南](docs/system-guide.md) - 四种系统类型的详细使用
|
|
||||||
- [🎬 场景管理指南](docs/scene-management-guide.md) - 场景切换和数据管理
|
|
||||||
- [⏰ 定时器系统指南](docs/timer-guide.md) - 定时器的完整使用方法
|
|
||||||
|
|
||||||
### API 参考
|
|
||||||
- [核心 API 参考](docs/core-concepts.md) - 完整的 API 使用说明
|
|
||||||
- [实体基础指南](docs/entity-guide.md) - 实体的基本概念和操作
|
|
||||||
- [EntityManager 指南](docs/entity-manager-example.md) - 高性能查询和批量操作
|
|
||||||
- [事件系统指南](docs/event-system-example.md) - 事件系统完整用法
|
|
||||||
- [查询系统指南](docs/query-system-usage.md) - 查询系统使用方法
|
|
||||||
|
|
||||||
### 性能相关
|
|
||||||
- [性能优化指南](docs/performance-optimization.md) - 性能优化技术和策略
|
|
||||||
- [SoA 存储优化指南](docs/soa-storage-guide.md) - 大规模实体系统的高级性能优化 ⭐ **大规模项目推荐**
|
|
||||||
|
|
||||||
## 构建
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 安装依赖
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 构建项目
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 监听模式
|
|
||||||
npm run build:watch
|
|
||||||
|
|
||||||
# 清理构建文件
|
|
||||||
npm run clean
|
|
||||||
|
|
||||||
# 重新构建
|
|
||||||
npm run rebuild
|
|
||||||
```
|
|
||||||
|
|
||||||
## 性能监控
|
|
||||||
|
|
||||||
框架提供内置性能统计:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 场景统计
|
|
||||||
const sceneStats = scene.getStats();
|
|
||||||
console.log('性能统计:', {
|
|
||||||
实体数量: sceneStats.entityCount,
|
|
||||||
系统数量: sceneStats.processorCount
|
|
||||||
});
|
|
||||||
|
|
||||||
// 查询系统统计
|
|
||||||
const queryStats = scene.querySystem.getStats();
|
|
||||||
console.log('查询统计:', {
|
|
||||||
缓存命中率: queryStats.hitRate + '%',
|
|
||||||
查询次数: queryStats.queryCount
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 扩展库
|
## 扩展库
|
||||||
|
|
||||||
- [路径寻找库](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
- [路径寻找](https://github.com/esengine/ecs-astar) - A*、BFS、Dijkstra 算法
|
||||||
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
- [AI 系统](https://github.com/esengine/BehaviourTree-ai) - 行为树、效用 AI
|
||||||
|
|
||||||
## 社区
|
## 社区
|
||||||
@@ -451,15 +224,6 @@ console.log('查询统计:', {
|
|||||||
- QQ 群:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
- QQ 群:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
|
||||||
- GitHub:[提交 Issue](https://github.com/esengine/ecs-framework/issues)
|
- GitHub:[提交 Issue](https://github.com/esengine/ecs-framework/issues)
|
||||||
|
|
||||||
## 贡献
|
|
||||||
|
|
||||||
欢迎提交 Pull Request 和 Issue!
|
|
||||||
|
|
||||||
### 开发要求
|
|
||||||
|
|
||||||
- Node.js >= 14.0.0
|
|
||||||
- TypeScript >= 4.0.0
|
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
[MIT](LICENSE)
|
[MIT](LICENSE)
|
||||||
382
assets/svg/ecs-architecture.svg
Normal file
382
assets/svg/ecs-architecture.svg
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
<svg width="1200" height="850" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<!-- 渐变定义 - 柔和色调 -->
|
||||||
|
<linearGradient id="coreGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#8892b0"/>
|
||||||
|
<stop offset="100%" style="stop-color:#a5b4cb"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="sceneGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#7c9cbf"/>
|
||||||
|
<stop offset="100%" style="stop-color:#9bb5d1"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="entityGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#81b29a"/>
|
||||||
|
<stop offset="100%" style="stop-color:#a8caba"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="componentGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#d4a574"/>
|
||||||
|
<stop offset="100%" style="stop-color:#e5c7a0"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="systemGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#c49799"/>
|
||||||
|
<stop offset="100%" style="stop-color:#d9b5b7"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="queryGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#b0c4de"/>
|
||||||
|
<stop offset="100%" style="stop-color:#d0dff0"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="eventGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#c5a3c5"/>
|
||||||
|
<stop offset="100%" style="stop-color:#e0c4e0"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- 动画定义 -->
|
||||||
|
<style>
|
||||||
|
.data-flow-line {
|
||||||
|
animation: dataFlow 6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dataFlow {
|
||||||
|
0% { stroke-dashoffset: 40; }
|
||||||
|
100% { stroke-dashoffset: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<rect width="1200" height="850" fill="white" rx="15" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text x="600" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" font-weight="bold" fill="#2c3e50">
|
||||||
|
ECS Framework 完整架构
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Core 核心层 -->
|
||||||
|
<g id="core-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="480" cy="75" r="15" fill="#8892b0" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="480" y="81" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">1</text>
|
||||||
|
|
||||||
|
<rect x="400" y="60" width="400" height="60" rx="10" fill="url(#coreGradient)" opacity="0.9"/>
|
||||||
|
<text x="600" y="85" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white">
|
||||||
|
Core 框架核心
|
||||||
|
</text>
|
||||||
|
<text x="500" y="105" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
ComponentRegistry • IdentifierPool
|
||||||
|
</text>
|
||||||
|
<text x="700" y="105" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
BitMaskOptimizer • ConfigManager
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Scene 场景管理层 -->
|
||||||
|
<g id="scene-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="280" cy="175" r="15" fill="#7c9cbf" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="280" y="181" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">2</text>
|
||||||
|
|
||||||
|
<rect x="300" y="150" width="600" height="80" rx="12" fill="url(#sceneGradient)" opacity="0.9"/>
|
||||||
|
<text x="600" y="175" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="white">
|
||||||
|
Scene 场景管理器
|
||||||
|
</text>
|
||||||
|
<text x="450" y="195" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
EntityList
|
||||||
|
</text>
|
||||||
|
<text x="600" y="195" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
ComponentStorageManager
|
||||||
|
</text>
|
||||||
|
<text x="750" y="195" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
EntityProcessors
|
||||||
|
</text>
|
||||||
|
<text x="600" y="215" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
实体管理 • 组件存储 • 系统调度 • 查询引擎
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Entity 实体层 -->
|
||||||
|
<g id="entity-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="30" cy="320" r="15" fill="#81b29a" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="30" y="326" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">3</text>
|
||||||
|
|
||||||
|
<rect x="50" y="280" width="280" height="180" rx="10" fill="url(#entityGradient)" opacity="0.9"/>
|
||||||
|
<text x="190" y="305" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white">
|
||||||
|
Entity 实体系统
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Entity Manager -->
|
||||||
|
<rect x="70" y="320" width="240" height="30" rx="5" fill="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.5)" stroke-width="1"/>
|
||||||
|
<text x="190" y="340" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">
|
||||||
|
EntityList • 高性能实体集合管理
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Entity instances -->
|
||||||
|
<rect x="80" y="360" width="60" height="20" rx="3" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="110" y="373" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="white">Player</text>
|
||||||
|
|
||||||
|
<rect x="150" y="360" width="60" height="20" rx="3" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="180" y="373" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="white">Enemy</text>
|
||||||
|
|
||||||
|
<rect x="220" y="360" width="60" height="20" rx="3" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="250" y="373" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="white">Bullet</text>
|
||||||
|
|
||||||
|
<!-- Entity features -->
|
||||||
|
<text x="80" y="400" font-family="Arial, sans-serif" font-size="9" fill="white">• 组件容器</text>
|
||||||
|
<text x="80" y="415" font-family="Arial, sans-serif" font-size="9" fill="white">• 层次结构</text>
|
||||||
|
<text x="190" y="400" font-family="Arial, sans-serif" font-size="9" fill="white">• 生命周期管理</text>
|
||||||
|
<text x="190" y="415" font-family="Arial, sans-serif" font-size="9" fill="white">• 状态管理</text>
|
||||||
|
|
||||||
|
<text x="190" y="440" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
唯一标识 • 无数据逻辑载体
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Component 组件层 -->
|
||||||
|
<g id="component-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="650" cy="320" r="15" fill="#d4a574" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="650" y="326" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">4</text>
|
||||||
|
|
||||||
|
<rect x="360" y="280" width="280" height="180" rx="10" fill="url(#componentGradient)" opacity="0.9"/>
|
||||||
|
<text x="500" y="305" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white">
|
||||||
|
Component 组件系统
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Component Storage -->
|
||||||
|
<rect x="380" y="315" width="240" height="40" rx="5" fill="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.5)" stroke-width="1"/>
|
||||||
|
<text x="500" y="330" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="white">
|
||||||
|
ComponentStorageManager • SoA/AoS 双模式
|
||||||
|
</text>
|
||||||
|
<text x="500" y="345" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="white">
|
||||||
|
ComponentPool • DirtyTrackingSystem
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Component types -->
|
||||||
|
<rect x="380" y="360" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="435" y="372" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Position {x,y,z}</text>
|
||||||
|
|
||||||
|
<rect x="500" y="360" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="555" y="372" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Velocity {dx,dy,dz}</text>
|
||||||
|
|
||||||
|
<rect x="380" y="385" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="435" y="397" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Health {hp,max}</text>
|
||||||
|
|
||||||
|
<rect x="500" y="385" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="555" y="397" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Render {sprite}</text>
|
||||||
|
|
||||||
|
<!-- Component features -->
|
||||||
|
<text x="380" y="420" font-family="Arial, sans-serif" font-size="9" fill="white">• @EnableSoA 优化</text>
|
||||||
|
<text x="520" y="420" font-family="Arial, sans-serif" font-size="9" fill="white">• 对象池管理</text>
|
||||||
|
<text x="380" y="435" font-family="Arial, sans-serif" font-size="9" fill="white">• 序列化支持</text>
|
||||||
|
<text x="520" y="435" font-family="Arial, sans-serif" font-size="9" fill="white">• 类型安全</text>
|
||||||
|
|
||||||
|
<text x="500" y="450" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
纯数据结构 • 描述实体属性
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- System 系统层 -->
|
||||||
|
<g id="system-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="1160" cy="320" r="15" fill="#c49799" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="1160" y="326" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">5</text>
|
||||||
|
|
||||||
|
<rect x="870" y="280" width="280" height="180" rx="10" fill="url(#systemGradient)" opacity="0.9"/>
|
||||||
|
<text x="1010" y="305" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="white">
|
||||||
|
System 系统层
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- System Processors -->
|
||||||
|
<rect x="890" y="320" width="240" height="30" rx="5" fill="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.5)" stroke-width="1"/>
|
||||||
|
<text x="1010" y="340" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">
|
||||||
|
EntityProcessors • 系统调度管理
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- System instances -->
|
||||||
|
<rect x="890" y="360" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="945" y="372" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">MovementSystem</text>
|
||||||
|
|
||||||
|
<rect x="1010" y="360" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="1065" y="372" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">RenderSystem</text>
|
||||||
|
|
||||||
|
<rect x="890" y="385" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="945" y="397" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">PhysicsSystem</text>
|
||||||
|
|
||||||
|
<rect x="1010" y="385" width="110" height="18" rx="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<text x="1065" y="397" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">AISystem</text>
|
||||||
|
|
||||||
|
<!-- System features -->
|
||||||
|
<text x="890" y="420" font-family="Arial, sans-serif" font-size="9" fill="white">• Matcher 查询</text>
|
||||||
|
<text x="1020" y="420" font-family="Arial, sans-serif" font-size="9" fill="white">• 性能监控</text>
|
||||||
|
<text x="890" y="435" font-family="Arial, sans-serif" font-size="9" fill="white">• 优先级调度</text>
|
||||||
|
<text x="1020" y="435" font-family="Arial, sans-serif" font-size="9" fill="white">• 热插拔</text>
|
||||||
|
|
||||||
|
<text x="1010" y="450" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
业务逻辑处理 • 操作组件数据
|
||||||
|
</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Query 查询层 -->
|
||||||
|
<g id="query-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="30" cy="540" r="15" fill="#b0c4de" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="30" y="546" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">6</text>
|
||||||
|
|
||||||
|
<rect x="50" y="500" width="500" height="120" rx="10" fill="url(#queryGradient)" opacity="0.9"/>
|
||||||
|
<text x="300" y="525" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#2c3e50">
|
||||||
|
Query 查询系统
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Query components -->
|
||||||
|
<rect x="70" y="540" width="140" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="140" y="555" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">Matcher</text>
|
||||||
|
<text x="140" y="570" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">withAll()</text>
|
||||||
|
<text x="140" y="582" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">withAny()</text>
|
||||||
|
<text x="140" y="594" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">withNone()</text>
|
||||||
|
|
||||||
|
<rect x="230" y="540" width="140" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="300" y="555" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">QuerySystem</text>
|
||||||
|
<text x="300" y="570" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">查询缓存</text>
|
||||||
|
<text x="300" y="582" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">批量优化</text>
|
||||||
|
<text x="300" y="594" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">实时更新</text>
|
||||||
|
|
||||||
|
<rect x="390" y="540" width="140" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="460" y="553" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">ArchetypeSystem</text>
|
||||||
|
<text x="460" y="568" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#4a5568">组件组合分组</text>
|
||||||
|
<text x="460" y="580" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#4a5568">原型级缓存</text>
|
||||||
|
<text x="460" y="592" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="#4a5568">BitSet优化查询</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Event 事件系统 -->
|
||||||
|
<g id="event-layer">
|
||||||
|
<!-- 流程步骤标号 -->
|
||||||
|
<circle cx="1160" cy="540" r="15" fill="#c5a3c5" stroke="white" stroke-width="2"/>
|
||||||
|
<text x="1160" y="546" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="white">7</text>
|
||||||
|
|
||||||
|
<rect x="580" y="500" width="570" height="120" rx="10" fill="url(#eventGradient)" opacity="0.9"/>
|
||||||
|
<text x="865" y="525" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#2c3e50">
|
||||||
|
Event 事件系统
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- Event components -->
|
||||||
|
<rect x="600" y="540" width="150" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="675" y="555" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">TypeSafeEventSystem</text>
|
||||||
|
<text x="675" y="570" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">同步/异步</text>
|
||||||
|
<text x="675" y="582" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">优先级排序</text>
|
||||||
|
<text x="675" y="594" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">批处理机制</text>
|
||||||
|
|
||||||
|
<rect x="770" y="540" width="150" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="845" y="555" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">Performance Monitor</text>
|
||||||
|
<text x="845" y="570" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">性能统计</text>
|
||||||
|
<text x="845" y="582" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">阈值告警</text>
|
||||||
|
<text x="845" y="594" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">实时监控</text>
|
||||||
|
|
||||||
|
<rect x="940" y="540" width="150" height="60" rx="5" fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/>
|
||||||
|
<text x="1015" y="555" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" font-weight="bold" fill="#4a5568">Debug Manager</text>
|
||||||
|
<text x="1015" y="570" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">WebSocket通信</text>
|
||||||
|
<text x="1015" y="582" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">实时调试数据</text>
|
||||||
|
<text x="1015" y="594" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" fill="#4a5568">内存快照</text>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 数据流连接线 -->
|
||||||
|
<!-- 1. Core to Scene - 初始化流程 -->
|
||||||
|
<path d="M 600 120 Q 600 135 600 150" stroke="#8892b0" stroke-width="3" fill="none" stroke-dasharray="10,5" class="data-flow-line"/>
|
||||||
|
<polygon points="595,145 600,150 605,145" fill="#8892b0"/>
|
||||||
|
<text x="620" y="135" font-family="Arial, sans-serif" font-size="10" fill="#8892b0" font-weight="bold">初始化</text>
|
||||||
|
|
||||||
|
<!-- 2. Scene to Entity/Component/System - 管理流程 -->
|
||||||
|
<path d="M 400 230 Q 300 250 190 280" stroke="#7c9cbf" stroke-width="3" fill="none" stroke-dasharray="8,4" class="data-flow-line"/>
|
||||||
|
<polygon points="195,275 190,280 200,282" fill="#7c9cbf"/>
|
||||||
|
<text x="295" y="250" font-family="Arial, sans-serif" font-size="9" fill="#7c9cbf">管理实体</text>
|
||||||
|
|
||||||
|
<path d="M 600 230 Q 600 250 500 280" stroke="#7c9cbf" stroke-width="3" fill="none" stroke-dasharray="8,4" class="data-flow-line"/>
|
||||||
|
<polygon points="495,275 500,280 505,275" fill="#7c9cbf"/>
|
||||||
|
<text x="550" y="250" font-family="Arial, sans-serif" font-size="9" fill="#7c9cbf">存储组件</text>
|
||||||
|
|
||||||
|
<path d="M 800 230 Q 900 250 1010 280" stroke="#7c9cbf" stroke-width="3" fill="none" stroke-dasharray="8,4" class="data-flow-line"/>
|
||||||
|
<polygon points="1005,275 1010,280 1000,282" fill="#7c9cbf"/>
|
||||||
|
<text x="905" y="250" font-family="Arial, sans-serif" font-size="9" fill="#7c9cbf">调度系统</text>
|
||||||
|
|
||||||
|
<!-- 3. Entity to Component - 组件附加 -->
|
||||||
|
<path d="M 330 370 Q 350 370 360 370" stroke="#81b29a" stroke-width="3" fill="none" stroke-dasharray="6,3" class="data-flow-line"/>
|
||||||
|
<polygon points="355,365 360,370 355,375" fill="#81b29a"/>
|
||||||
|
<text x="345" y="360" font-family="Arial, sans-serif" font-size="9" fill="#81b29a" font-weight="bold">附加组件</text>
|
||||||
|
|
||||||
|
<!-- 4. Component to System - 数据处理 -->
|
||||||
|
<path d="M 640 370 Q 750 370 870 370" stroke="#d4a574" stroke-width="3" fill="none" stroke-dasharray="6,3" class="data-flow-line"/>
|
||||||
|
<polygon points="865,365 870,370 865,375" fill="#d4a574"/>
|
||||||
|
<text x="755" y="360" font-family="Arial, sans-serif" font-size="9" fill="#d4a574" font-weight="bold">处理数据</text>
|
||||||
|
|
||||||
|
<!-- 5. Scene to Query/Event - 查询和事件 -->
|
||||||
|
<path d="M 450 230 Q 350 350 300 500" stroke="#b0c4de" stroke-width="2" fill="none" stroke-dasharray="5,2" class="data-flow-line"/>
|
||||||
|
<polygon points="305,495 300,500 295,495" fill="#b0c4de"/>
|
||||||
|
<text x="375" y="365" font-family="Arial, sans-serif" font-size="8" fill="#b0c4de">查询支持</text>
|
||||||
|
|
||||||
|
<path d="M 750 230 Q 850 350 865 500" stroke="#c5a3c5" stroke-width="2" fill="none" stroke-dasharray="5,2" class="data-flow-line"/>
|
||||||
|
<polygon points="860,495 865,500 870,495" fill="#c5a3c5"/>
|
||||||
|
<text x="808" y="365" font-family="Arial, sans-serif" font-size="8" fill="#c5a3c5">事件通知</text>
|
||||||
|
|
||||||
|
<!-- 6. Query to System - 查询结果 -->
|
||||||
|
<path d="M 460 540 Q 680 480 900 380" stroke="#b0c4de" stroke-width="3" fill="none" stroke-dasharray="4,2" class="data-flow-line"/>
|
||||||
|
<polygon points="895,375 900,380 895,385" fill="#b0c4de"/>
|
||||||
|
<text x="680" y="470" font-family="Arial, sans-serif" font-size="9" fill="#b0c4de" font-weight="bold">匹配结果</text>
|
||||||
|
|
||||||
|
<!-- 7. System回流 - 数据修改和事件 -->
|
||||||
|
<path d="M 900 400 Q 800 420 700 430 Q 600 440 500 430 Q 400 420 350 400" stroke="#c49799" stroke-width="2" fill="none" stroke-dasharray="3,2" class="data-flow-line"/>
|
||||||
|
<text x="625" y="415" font-family="Arial, sans-serif" font-size="8" fill="#c49799">修改组件数据</text>
|
||||||
|
|
||||||
|
<path d="M 1000 460 Q 1050 480 1100 500" stroke="#c5a3c5" stroke-width="2" fill="none" stroke-dasharray="3,2" class="data-flow-line"/>
|
||||||
|
<text x="1050" y="475" font-family="Arial, sans-serif" font-size="8" fill="#c5a3c5">触发事件</text>
|
||||||
|
|
||||||
|
<!-- 工作流程说明 -->
|
||||||
|
<g id="workflow">
|
||||||
|
<rect x="50" y="720" width="1100" height="120" rx="8" fill="rgba(108, 117, 125, 0.05)" stroke="#6c757d" stroke-width="1" stroke-dasharray="5,3"/>
|
||||||
|
<text x="600" y="745" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#495057">
|
||||||
|
🔄 ECS 框架7步工作流程
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 流程步骤详解 -->
|
||||||
|
<g id="step-details">
|
||||||
|
<text x="70" y="770" font-family="Arial, sans-serif" font-size="11" fill="#8892b0" font-weight="bold">①初始化</text>
|
||||||
|
<text x="70" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Core.create()</text>
|
||||||
|
|
||||||
|
<text x="170" y="770" font-family="Arial, sans-serif" font-size="11" fill="#7c9cbf" font-weight="bold">②场景管理</text>
|
||||||
|
<text x="170" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Scene.initialize()</text>
|
||||||
|
|
||||||
|
<text x="280" y="770" font-family="Arial, sans-serif" font-size="11" fill="#81b29a" font-weight="bold">③创建实体</text>
|
||||||
|
<text x="280" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Entity.create()</text>
|
||||||
|
|
||||||
|
<text x="380" y="770" font-family="Arial, sans-serif" font-size="11" fill="#d4a574" font-weight="bold">④附加组件</text>
|
||||||
|
<text x="380" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">addComponent()</text>
|
||||||
|
|
||||||
|
<text x="480" y="770" font-family="Arial, sans-serif" font-size="11" fill="#c49799" font-weight="bold">⑤系统处理</text>
|
||||||
|
<text x="480" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">System.process()</text>
|
||||||
|
|
||||||
|
<text x="580" y="770" font-family="Arial, sans-serif" font-size="11" fill="#b0c4de" font-weight="bold">⑥查询匹配</text>
|
||||||
|
<text x="580" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Matcher.query()</text>
|
||||||
|
|
||||||
|
<text x="680" y="770" font-family="Arial, sans-serif" font-size="11" fill="#c5a3c5" font-weight="bold">⑦事件通知</text>
|
||||||
|
<text x="680" y="785" font-family="Arial, sans-serif" font-size="10" fill="#6c757d">Event.emit()</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<text x="600" y="810" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#6c757d">
|
||||||
|
每帧循环:查询实体 → 匹配组件 → 执行系统逻辑 → 修改数据 → 触发事件 → 性能监控
|
||||||
|
</text>
|
||||||
|
<text x="600" y="828" text-anchor="middle" font-family="Arial, sans-serif" font-size="11" fill="#6c757d">
|
||||||
|
💡 鼠标悬停各组件查看详细API • 圆形数字显示执行顺序 • 不同颜色连线代表不同数据流
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 23 KiB |
273
assets/svg/soa-vs-aos.svg
Normal file
273
assets/svg/soa-vs-aos.svg
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
<svg width="800" height="500" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<!-- 渐变定义 -->
|
||||||
|
<linearGradient id="soaGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#667eea"/>
|
||||||
|
<stop offset="100%" style="stop-color:#764ba2"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="aosGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#f093fb"/>
|
||||||
|
<stop offset="100%" style="stop-color:#f5576c"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="performanceGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#4facfe"/>
|
||||||
|
<stop offset="100%" style="stop-color:#00f2fe"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<!-- 动画定义 -->
|
||||||
|
<style>
|
||||||
|
.data-flow {
|
||||||
|
animation: dataMove 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.memory-access {
|
||||||
|
animation: memoryAccess 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performance-bar {
|
||||||
|
animation: performanceGrow 2.5s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-reveal {
|
||||||
|
animation: textReveal 1s ease-in forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.structure-highlight {
|
||||||
|
animation: structureHighlight 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dataMove {
|
||||||
|
0%, 100% { opacity: 0.6; filter: brightness(1); }
|
||||||
|
50% { opacity: 1; filter: brightness(1.2); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes memoryAccess {
|
||||||
|
0%, 100% { fill: #e2e8f0; }
|
||||||
|
50% { fill: #3182ce; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes performanceGrow {
|
||||||
|
0% { width: 0; opacity: 0.5; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes textReveal {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes structureHighlight {
|
||||||
|
0%, 100% { stroke: #cbd5e0; stroke-width: 1; opacity: 0.8; }
|
||||||
|
50% { stroke: #3182ce; stroke-width: 2; opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- 背景 -->
|
||||||
|
<rect width="800" height="500" fill="white" rx="15" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<text x="400" y="30" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" font-weight="bold" fill="#2c3e50">
|
||||||
|
SoA vs AoS 数据结构对比
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="400" y="50" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" fill="#666">
|
||||||
|
Structure of Arrays vs Array of Structures
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- AoS 部分 (左侧) -->
|
||||||
|
<g id="aos-section">
|
||||||
|
<rect x="50" y="80" width="320" height="180" rx="10" fill="url(#aosGradient)" opacity="0.9"/>
|
||||||
|
|
||||||
|
<!-- AoS 标题 -->
|
||||||
|
<text x="210" y="105" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="white">
|
||||||
|
AoS - Array of Structures
|
||||||
|
</text>
|
||||||
|
<text x="210" y="125" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">
|
||||||
|
结构体数组(传统方式)
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- AoS 数据结构示例 -->
|
||||||
|
<rect x="70" y="140" width="280" height="100" rx="5" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Entity 0 -->
|
||||||
|
<rect x="85" y="155" width="70" height="15" rx="2" fill="rgba(255,255,255,0.3)" class="structure-highlight"/>
|
||||||
|
<text x="120" y="166" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Entity[0]</text>
|
||||||
|
|
||||||
|
<rect x="85" y="175" width="15" height="8" rx="1" fill="#ff6b6b" class="data-flow"/>
|
||||||
|
<text x="92" y="181" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" fill="white">x</text>
|
||||||
|
|
||||||
|
<rect x="105" y="175" width="15" height="8" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 0.5s"/>
|
||||||
|
<text x="112" y="181" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" fill="white">y</text>
|
||||||
|
|
||||||
|
<rect x="125" y="175" width="15" height="8" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 1s"/>
|
||||||
|
<text x="132" y="181" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" fill="white">hp</text>
|
||||||
|
|
||||||
|
<rect x="145" y="175" width="10" height="8" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 1.5s"/>
|
||||||
|
<text x="150" y="181" text-anchor="middle" font-family="Arial, sans-serif" font-size="6" fill="white">id</text>
|
||||||
|
|
||||||
|
<!-- Entity 1 -->
|
||||||
|
<rect x="170" y="155" width="70" height="15" rx="2" fill="rgba(255,255,255,0.3)" class="structure-highlight" style="animation-delay: 1s"/>
|
||||||
|
<text x="205" y="166" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Entity[1]</text>
|
||||||
|
|
||||||
|
<rect x="170" y="175" width="15" height="8" rx="1" fill="#ff6b6b" class="data-flow" style="animation-delay: 2s"/>
|
||||||
|
<rect x="190" y="175" width="15" height="8" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 2.5s"/>
|
||||||
|
<rect x="210" y="175" width="15" height="8" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 3s"/>
|
||||||
|
<rect x="230" y="175" width="10" height="8" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 3.5s"/>
|
||||||
|
|
||||||
|
<!-- Entity 2 -->
|
||||||
|
<rect x="255" y="155" width="70" height="15" rx="2" fill="rgba(255,255,255,0.3)" class="structure-highlight" style="animation-delay: 2s"/>
|
||||||
|
<text x="290" y="166" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">Entity[2]</text>
|
||||||
|
|
||||||
|
<rect x="255" y="175" width="15" height="8" rx="1" fill="#ff6b6b" class="data-flow" style="animation-delay: 4s"/>
|
||||||
|
<rect x="275" y="175" width="15" height="8" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 4.5s"/>
|
||||||
|
<rect x="295" y="175" width="15" height="8" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 5s"/>
|
||||||
|
<rect x="315" y="175" width="10" height="8" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 5.5s"/>
|
||||||
|
|
||||||
|
<!-- 内存访问模式 -->
|
||||||
|
<text x="210" y="205" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
内存访问:跳跃式访问,缓存不友好
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<path d="M 92 195 Q 112 210 132 195 Q 152 210 177 195" stroke="white" stroke-width="1" fill="none" stroke-dasharray="3,2"/>
|
||||||
|
<text x="135" y="225" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">
|
||||||
|
处理位置时需跳过其他数据
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- SoA 部分 (右侧) -->
|
||||||
|
<g id="soa-section">
|
||||||
|
<rect x="430" y="80" width="320" height="180" rx="10" fill="url(#soaGradient)" opacity="0.9"/>
|
||||||
|
|
||||||
|
<!-- SoA 标题 -->
|
||||||
|
<text x="590" y="105" text-anchor="middle" font-family="Arial, sans-serif" font-size="18" font-weight="bold" fill="white">
|
||||||
|
SoA - Structure of Arrays
|
||||||
|
</text>
|
||||||
|
<text x="590" y="125" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="white">
|
||||||
|
数组结构(ECS优化方式)
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- SoA 数据结构示例 -->
|
||||||
|
<rect x="450" y="140" width="280" height="100" rx="5" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Position Array -->
|
||||||
|
<text x="460" y="155" font-family="Arial, sans-serif" font-size="8" fill="white">Position[]:</text>
|
||||||
|
<rect x="515" y="145" width="20" height="10" rx="1" fill="#ff6b6b" class="data-flow"/>
|
||||||
|
<rect x="540" y="145" width="20" height="10" rx="1" fill="#ff6b6b" class="data-flow" style="animation-delay: 0.3s"/>
|
||||||
|
<rect x="565" y="145" width="20" height="10" rx="1" fill="#ff6b6b" class="data-flow" style="animation-delay: 0.6s"/>
|
||||||
|
<rect x="590" y="145" width="20" height="10" rx="1" fill="#ff6b6b" class="data-flow" style="animation-delay: 0.9s"/>
|
||||||
|
<text x="625" y="153" font-family="Arial, sans-serif" font-size="8" fill="white">连续存储</text>
|
||||||
|
|
||||||
|
<!-- Velocity Array -->
|
||||||
|
<text x="460" y="170" font-family="Arial, sans-serif" font-size="8" fill="white">Velocity[]:</text>
|
||||||
|
<rect x="515" y="160" width="20" height="10" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 1s"/>
|
||||||
|
<rect x="540" y="160" width="20" height="10" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 1.3s"/>
|
||||||
|
<rect x="565" y="160" width="20" height="10" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 1.6s"/>
|
||||||
|
<rect x="590" y="160" width="20" height="10" rx="1" fill="#4ecdc4" class="data-flow" style="animation-delay: 1.9s"/>
|
||||||
|
|
||||||
|
<!-- Health Array -->
|
||||||
|
<text x="460" y="185" font-family="Arial, sans-serif" font-size="8" fill="white">Health[]:</text>
|
||||||
|
<rect x="515" y="175" width="20" height="10" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 2s"/>
|
||||||
|
<rect x="540" y="175" width="20" height="10" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 2.3s"/>
|
||||||
|
<rect x="565" y="175" width="20" height="10" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 2.6s"/>
|
||||||
|
<rect x="590" y="175" width="20" height="10" rx="1" fill="#45b7d1" class="data-flow" style="animation-delay: 2.9s"/>
|
||||||
|
|
||||||
|
<!-- ID Array -->
|
||||||
|
<text x="460" y="200" font-family="Arial, sans-serif" font-size="8" fill="white">EntityID[]:</text>
|
||||||
|
<rect x="515" y="190" width="15" height="10" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 3s"/>
|
||||||
|
<rect x="535" y="190" width="15" height="10" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 3.3s"/>
|
||||||
|
<rect x="555" y="190" width="15" height="10" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 3.6s"/>
|
||||||
|
<rect x="575" y="190" width="15" height="10" rx="1" fill="#96ceb4" class="data-flow" style="animation-delay: 3.9s"/>
|
||||||
|
|
||||||
|
<!-- 内存访问模式 -->
|
||||||
|
<text x="590" y="220" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="white">
|
||||||
|
内存访问:连续访问,缓存友好
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<path d="M 525 205 L 570 205" stroke="white" stroke-width="2" fill="none" class="data-flow"/>
|
||||||
|
<text x="590" y="235" text-anchor="middle" font-family="Arial, sans-serif" font-size="8" fill="white">
|
||||||
|
处理位置时连续访问相同类型数据
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 性能对比区域 -->
|
||||||
|
<g id="performance-comparison">
|
||||||
|
<rect x="50" y="280" width="700" height="150" rx="10" fill="rgba(248, 249, 250, 0.9)" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
|
||||||
|
<text x="400" y="305" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" font-weight="bold" fill="#2c3e50">
|
||||||
|
性能对比分析
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- 缓存性能对比 -->
|
||||||
|
<g id="cache-performance">
|
||||||
|
<text x="80" y="330" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4a5568">
|
||||||
|
缓存命中率:
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- AoS 缓存性能 -->
|
||||||
|
<text x="80" y="350" font-family="Arial, sans-serif" font-size="11" fill="#666">AoS:</text>
|
||||||
|
<rect x="120" y="342" width="100" height="12" rx="6" fill="#f7fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
<rect x="120" y="342" width="35" height="12" rx="6" fill="url(#aosGradient)" class="performance-bar"/>
|
||||||
|
<text x="230" y="351" font-family="Arial, sans-serif" font-size="10" fill="#666">35%</text>
|
||||||
|
|
||||||
|
<!-- SoA 缓存性能 -->
|
||||||
|
<text x="80" y="370" font-family="Arial, sans-serif" font-size="11" fill="#666">SoA:</text>
|
||||||
|
<rect x="120" y="362" width="100" height="12" rx="6" fill="#f7fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
<rect x="120" y="362" width="85" height="12" rx="6" fill="url(#soaGradient)" class="performance-bar" style="animation-delay: 0.5s"/>
|
||||||
|
<text x="230" y="371" font-family="Arial, sans-serif" font-size="10" fill="#666">85%</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 处理速度对比 -->
|
||||||
|
<g id="processing-speed">
|
||||||
|
<text x="320" y="330" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4a5568">
|
||||||
|
批量处理速度:
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<!-- AoS 处理速度 -->
|
||||||
|
<text x="320" y="350" font-family="Arial, sans-serif" font-size="11" fill="#666">AoS:</text>
|
||||||
|
<rect x="360" y="342" width="120" height="12" rx="6" fill="#f7fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
<rect x="360" y="342" width="48" height="12" rx="6" fill="url(#aosGradient)" class="performance-bar" style="animation-delay: 1s"/>
|
||||||
|
<text x="490" y="351" font-family="Arial, sans-serif" font-size="10" fill="#666">2.3x slower</text>
|
||||||
|
|
||||||
|
<!-- SoA 处理速度 -->
|
||||||
|
<text x="320" y="370" font-family="Arial, sans-serif" font-size="11" fill="#666">SoA:</text>
|
||||||
|
<rect x="360" y="362" width="120" height="12" rx="6" fill="#f7fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||||
|
<rect x="360" y="362" width="120" height="12" rx="6" fill="url(#soaGradient)" class="performance-bar" style="animation-delay: 1.5s"/>
|
||||||
|
<text x="490" y="371" font-family="Arial, sans-serif" font-size="10" fill="#666">baseline</text>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- 使用场景 -->
|
||||||
|
<g id="use-cases">
|
||||||
|
<text x="560" y="330" font-family="Arial, sans-serif" font-size="12" font-weight="bold" fill="#4a5568">
|
||||||
|
适用场景:
|
||||||
|
</text>
|
||||||
|
|
||||||
|
<text x="560" y="350" font-family="Arial, sans-serif" font-size="10" fill="#666">
|
||||||
|
✅ SoA: 大量实体的同类型操作
|
||||||
|
</text>
|
||||||
|
<text x="560" y="365" font-family="Arial, sans-serif" font-size="10" fill="#666">
|
||||||
|
✅ SoA: 游戏循环中的系统处理
|
||||||
|
</text>
|
||||||
|
<text x="560" y="380" font-family="Arial, sans-serif" font-size="10" fill="#666">
|
||||||
|
❌ AoS: 混合操作、少量实体
|
||||||
|
</text>
|
||||||
|
<text x="560" y="395" font-family="Arial, sans-serif" font-size="10" fill="#666">
|
||||||
|
❌ AoS: 随机访问模式
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- ECS 框架优势说明 -->
|
||||||
|
<g id="ecs-advantage">
|
||||||
|
<rect x="50" y="450" width="700" height="40" rx="8" fill="rgba(67, 233, 123, 0.1)" stroke="#43e97b" stroke-width="1"/>
|
||||||
|
<text x="400" y="468" text-anchor="middle" font-family="Arial, sans-serif" font-size="14" font-weight="bold" fill="#2d3748">
|
||||||
|
🚀 本框架采用 SoA 优化存储,@EnableSoA 装饰器自动转换,性能提升 2-3 倍
|
||||||
|
</text>
|
||||||
|
<text x="400" y="485" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#4a5568">
|
||||||
|
支持热切换存储方式,开发时使用 AoS 调试,生产环境自动启用 SoA 优化
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 14 KiB |
105
package-lock.json
generated
105
package-lock.json
generated
@@ -11,8 +11,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/multer": "^1.4.13",
|
"@types/multer": "^1.4.13",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"protobufjs": "^7.5.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"ecs-proto": "bin/ecs-proto"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
@@ -1007,6 +1012,70 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1",
|
||||||
|
"@protobufjs/inquire": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/@rollup/plugin-commonjs": {
|
"node_modules/@rollup/plugin-commonjs": {
|
||||||
"version": "28.0.3",
|
"version": "28.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz",
|
||||||
@@ -4054,6 +4123,12 @@
|
|||||||
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
"integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -4508,6 +4583,30 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "7.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz",
|
||||||
|
"integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.4",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.0",
|
||||||
|
"@protobufjs/fetch": "^1.1.0",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.0",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.0",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/psl": {
|
"node_modules/psl": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
@@ -4566,6 +4665,12 @@
|
|||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/reflect-metadata": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/require-directory": {
|
"node_modules/require-directory": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
|||||||
10
package.json
10
package.json
@@ -4,6 +4,9 @@
|
|||||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||||
"main": "bin/index.js",
|
"main": "bin/index.js",
|
||||||
"types": "bin/index.d.ts",
|
"types": "bin/index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"ecs-proto": "./bin/ecs-proto"
|
||||||
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"bin/**/*",
|
"bin/**/*",
|
||||||
"README.md",
|
"README.md",
|
||||||
@@ -36,7 +39,10 @@
|
|||||||
"test:performance": "jest --config jest.performance.config.js",
|
"test:performance": "jest --config jest.performance.config.js",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"test:ci": "jest --ci --coverage",
|
"test:ci": "jest --ci --coverage",
|
||||||
"test:clear": "jest --clearCache"
|
"test:clear": "jest --clearCache",
|
||||||
|
"proto:generate": "ecs-proto generate",
|
||||||
|
"proto:compile": "ecs-proto compile",
|
||||||
|
"proto:build": "ecs-proto build"
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -64,6 +70,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/multer": "^1.4.13",
|
"@types/multer": "^1.4.13",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
|
"protobufjs": "^7.5.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
"ws": "^8.18.2"
|
"ws": "^8.18.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
284
src/Utils/Serialization/ProtobufDecorators.ts
Normal file
284
src/Utils/Serialization/ProtobufDecorators.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* Protobuf序列化装饰器
|
||||||
|
*
|
||||||
|
* 提供装饰器语法来标记组件和字段进行protobuf序列化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import { Component } from '../../ECS/Component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protobuf字段类型枚举
|
||||||
|
*/
|
||||||
|
export enum ProtoFieldType {
|
||||||
|
DOUBLE = 'double',
|
||||||
|
FLOAT = 'float',
|
||||||
|
INT32 = 'int32',
|
||||||
|
INT64 = 'int64',
|
||||||
|
UINT32 = 'uint32',
|
||||||
|
UINT64 = 'uint64',
|
||||||
|
SINT32 = 'sint32',
|
||||||
|
SINT64 = 'sint64',
|
||||||
|
FIXED32 = 'fixed32',
|
||||||
|
FIXED64 = 'fixed64',
|
||||||
|
SFIXED32 = 'sfixed32',
|
||||||
|
SFIXED64 = 'sfixed64',
|
||||||
|
BOOL = 'bool',
|
||||||
|
STRING = 'string',
|
||||||
|
BYTES = 'bytes'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protobuf字段定义接口
|
||||||
|
*/
|
||||||
|
export interface ProtoFieldDefinition {
|
||||||
|
/** 字段编号 */
|
||||||
|
fieldNumber: number;
|
||||||
|
/** 字段类型 */
|
||||||
|
type: ProtoFieldType;
|
||||||
|
/** 是否为数组 */
|
||||||
|
repeated?: boolean;
|
||||||
|
/** 是否可选 */
|
||||||
|
optional?: boolean;
|
||||||
|
/** 字段名称 */
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protobuf组件定义接口
|
||||||
|
*/
|
||||||
|
export interface ProtoComponentDefinition {
|
||||||
|
/** 组件名称 */
|
||||||
|
name: string;
|
||||||
|
/** 字段定义列表 */
|
||||||
|
fields: Map<string, ProtoFieldDefinition>;
|
||||||
|
/** 构造函数 */
|
||||||
|
constructor: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protobuf注册表
|
||||||
|
*/
|
||||||
|
export class ProtobufRegistry {
|
||||||
|
private static instance: ProtobufRegistry;
|
||||||
|
private components = new Map<string, ProtoComponentDefinition>();
|
||||||
|
|
||||||
|
public static getInstance(): ProtobufRegistry {
|
||||||
|
if (!ProtobufRegistry.instance) {
|
||||||
|
ProtobufRegistry.instance = new ProtobufRegistry();
|
||||||
|
}
|
||||||
|
return ProtobufRegistry.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册组件定义
|
||||||
|
*/
|
||||||
|
public registerComponent(componentName: string, definition: ProtoComponentDefinition): void {
|
||||||
|
this.components.set(componentName, definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件定义
|
||||||
|
*/
|
||||||
|
public getComponentDefinition(componentName: string): ProtoComponentDefinition | undefined {
|
||||||
|
return this.components.get(componentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查组件是否支持protobuf
|
||||||
|
*/
|
||||||
|
public hasProtoDefinition(componentName: string): boolean {
|
||||||
|
return this.components.has(componentName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有注册的组件
|
||||||
|
*/
|
||||||
|
public getAllComponents(): Map<string, ProtoComponentDefinition> {
|
||||||
|
return new Map(this.components);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成proto文件定义
|
||||||
|
*/
|
||||||
|
public generateProtoDefinition(): string {
|
||||||
|
let protoContent = 'syntax = "proto3";\n\n';
|
||||||
|
protoContent += 'package ecs;\n\n';
|
||||||
|
|
||||||
|
// 生成消息定义
|
||||||
|
for (const [name, definition] of this.components) {
|
||||||
|
protoContent += `message ${name} {\n`;
|
||||||
|
|
||||||
|
// 按字段编号排序
|
||||||
|
const sortedFields = Array.from(definition.fields.values())
|
||||||
|
.sort((a, b) => a.fieldNumber - b.fieldNumber);
|
||||||
|
|
||||||
|
for (const field of sortedFields) {
|
||||||
|
let fieldDef = ' ';
|
||||||
|
|
||||||
|
if (field.repeated) {
|
||||||
|
fieldDef += 'repeated ';
|
||||||
|
} else if (field.optional) {
|
||||||
|
fieldDef += 'optional ';
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldDef += `${field.type} ${field.name} = ${field.fieldNumber};\n`;
|
||||||
|
protoContent += fieldDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
protoContent += '}\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return protoContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProtoSerializable 组件装饰器
|
||||||
|
*
|
||||||
|
* 标记组件支持protobuf序列化
|
||||||
|
*
|
||||||
|
* @param protoName - protobuf消息名称,默认使用类名
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* @ProtoSerializable('Position')
|
||||||
|
* class PositionComponent extends Component {
|
||||||
|
* // ...
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function ProtoSerializable(protoName?: string) {
|
||||||
|
return function <T extends { new(...args: any[]): Component }>(constructor: T) {
|
||||||
|
const componentName = protoName || constructor.name;
|
||||||
|
const registry = ProtobufRegistry.getInstance();
|
||||||
|
|
||||||
|
// 获取字段定义(由ProtoField装饰器设置)
|
||||||
|
const fields = (constructor.prototype._protoFields as Map<string, ProtoFieldDefinition>)
|
||||||
|
|| new Map<string, ProtoFieldDefinition>();
|
||||||
|
|
||||||
|
// 注册组件定义
|
||||||
|
registry.registerComponent(componentName, {
|
||||||
|
name: componentName,
|
||||||
|
fields: fields,
|
||||||
|
constructor: constructor
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标记组件支持protobuf
|
||||||
|
(constructor.prototype._isProtoSerializable = true);
|
||||||
|
(constructor.prototype._protoName = componentName);
|
||||||
|
|
||||||
|
return constructor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ProtoField 字段装饰器
|
||||||
|
*
|
||||||
|
* 标记字段参与protobuf序列化
|
||||||
|
*
|
||||||
|
* @param fieldNumber - protobuf字段编号(必须唯一且大于0)
|
||||||
|
* @param type - 字段类型,默认自动推断
|
||||||
|
* @param options - 额外选项
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* class PositionComponent extends Component {
|
||||||
|
* @ProtoField(1, ProtoFieldType.FLOAT)
|
||||||
|
* public x: number = 0;
|
||||||
|
*
|
||||||
|
* @ProtoField(2, ProtoFieldType.FLOAT)
|
||||||
|
* public y: number = 0;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function ProtoField(
|
||||||
|
fieldNumber: number,
|
||||||
|
type?: ProtoFieldType,
|
||||||
|
options?: {
|
||||||
|
repeated?: boolean;
|
||||||
|
optional?: boolean;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
return function (target: any, propertyKey: string) {
|
||||||
|
// 验证字段编号
|
||||||
|
if (fieldNumber <= 0) {
|
||||||
|
throw new Error(`ProtoField: 字段编号必须大于0,当前值: ${fieldNumber}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化字段集合
|
||||||
|
if (!target._protoFields) {
|
||||||
|
target._protoFields = new Map<string, ProtoFieldDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动推断类型
|
||||||
|
let inferredType = type;
|
||||||
|
if (!inferredType) {
|
||||||
|
const designType = Reflect.getMetadata?.('design:type', target, propertyKey);
|
||||||
|
inferredType = inferProtoType(designType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查字段编号冲突
|
||||||
|
for (const [key, field] of target._protoFields) {
|
||||||
|
if (field.fieldNumber === fieldNumber && key !== propertyKey) {
|
||||||
|
throw new Error(`ProtoField: 字段编号 ${fieldNumber} 已被字段 ${key} 使用`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加字段定义
|
||||||
|
target._protoFields.set(propertyKey, {
|
||||||
|
fieldNumber,
|
||||||
|
type: inferredType || ProtoFieldType.STRING,
|
||||||
|
repeated: options?.repeated || false,
|
||||||
|
optional: options?.optional || false,
|
||||||
|
name: propertyKey
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动推断protobuf类型
|
||||||
|
*/
|
||||||
|
function inferProtoType(jsType: any): ProtoFieldType {
|
||||||
|
if (!jsType) return ProtoFieldType.STRING;
|
||||||
|
|
||||||
|
switch (jsType) {
|
||||||
|
case Number:
|
||||||
|
return ProtoFieldType.FLOAT;
|
||||||
|
case Boolean:
|
||||||
|
return ProtoFieldType.BOOL;
|
||||||
|
case String:
|
||||||
|
return ProtoFieldType.STRING;
|
||||||
|
default:
|
||||||
|
return ProtoFieldType.STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 便捷装饰器 - 常用类型
|
||||||
|
*/
|
||||||
|
export const ProtoInt32 = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||||
|
ProtoField(fieldNumber, ProtoFieldType.INT32, options);
|
||||||
|
|
||||||
|
export const ProtoFloat = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||||
|
ProtoField(fieldNumber, ProtoFieldType.FLOAT, options);
|
||||||
|
|
||||||
|
export const ProtoString = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||||
|
ProtoField(fieldNumber, ProtoFieldType.STRING, options);
|
||||||
|
|
||||||
|
export const ProtoBool = (fieldNumber: number, options?: { repeated?: boolean; optional?: boolean }) =>
|
||||||
|
ProtoField(fieldNumber, ProtoFieldType.BOOL, options);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查组件是否支持protobuf序列化
|
||||||
|
*/
|
||||||
|
export function isProtoSerializable(component: Component): boolean {
|
||||||
|
return !!(component as any)._isProtoSerializable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取组件的protobuf名称
|
||||||
|
*/
|
||||||
|
export function getProtoName(component: Component): string | undefined {
|
||||||
|
return (component as any)._protoName;
|
||||||
|
}
|
||||||
371
src/Utils/Serialization/ProtobufSerializer.ts
Normal file
371
src/Utils/Serialization/ProtobufSerializer.ts
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* Protobuf序列化器
|
||||||
|
*
|
||||||
|
* 处理组件的protobuf序列化和反序列化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '../../ECS/Component';
|
||||||
|
import {
|
||||||
|
ProtobufRegistry,
|
||||||
|
ProtoComponentDefinition,
|
||||||
|
ProtoFieldDefinition,
|
||||||
|
ProtoFieldType,
|
||||||
|
isProtoSerializable,
|
||||||
|
getProtoName
|
||||||
|
} from './ProtobufDecorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化数据接口
|
||||||
|
*/
|
||||||
|
export interface SerializedData {
|
||||||
|
/** 序列化类型 */
|
||||||
|
type: 'protobuf' | 'json';
|
||||||
|
/** 组件类型名称 */
|
||||||
|
componentType: string;
|
||||||
|
/** 序列化后的数据 */
|
||||||
|
data: Uint8Array | any;
|
||||||
|
/** 数据大小(字节) */
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protobuf序列化器
|
||||||
|
*/
|
||||||
|
export class ProtobufSerializer {
|
||||||
|
private registry: ProtobufRegistry;
|
||||||
|
private static instance: ProtobufSerializer;
|
||||||
|
|
||||||
|
/** protobuf.js实例 */
|
||||||
|
private protobuf: any = null;
|
||||||
|
private root: any = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.registry = ProtobufRegistry.getInstance();
|
||||||
|
this.initializeProtobuf();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动初始化protobuf支持
|
||||||
|
*/
|
||||||
|
private async initializeProtobuf(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 动态导入protobufjs
|
||||||
|
this.protobuf = await import('protobufjs');
|
||||||
|
this.buildProtoDefinitions();
|
||||||
|
console.log('[ProtobufSerializer] Protobuf支持已自动启用');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[ProtobufSerializer] 无法加载protobufjs,将使用JSON序列化:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): ProtobufSerializer {
|
||||||
|
if (!ProtobufSerializer.instance) {
|
||||||
|
ProtobufSerializer.instance = new ProtobufSerializer();
|
||||||
|
}
|
||||||
|
return ProtobufSerializer.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动初始化protobuf.js(可选,通常会自动初始化)
|
||||||
|
*
|
||||||
|
* @param protobufJs - protobuf.js库实例
|
||||||
|
*/
|
||||||
|
public initialize(protobufJs: any): void {
|
||||||
|
this.protobuf = protobufJs;
|
||||||
|
this.buildProtoDefinitions();
|
||||||
|
console.log('[ProtobufSerializer] Protobuf支持已手动启用');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化组件
|
||||||
|
*
|
||||||
|
* @param component - 要序列化的组件
|
||||||
|
* @returns 序列化数据
|
||||||
|
*/
|
||||||
|
public serialize(component: Component): SerializedData {
|
||||||
|
const componentType = component.constructor.name;
|
||||||
|
|
||||||
|
// 检查是否支持protobuf序列化
|
||||||
|
if (!isProtoSerializable(component)) {
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) {
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = this.registry.getComponentDefinition(protoName);
|
||||||
|
if (!definition) {
|
||||||
|
console.warn(`[ProtobufSerializer] 未找到组件定义: ${protoName}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建protobuf数据对象
|
||||||
|
const protoData = this.buildProtoData(component, definition);
|
||||||
|
|
||||||
|
// 获取protobuf消息类型
|
||||||
|
const MessageType = this.getMessageType(protoName);
|
||||||
|
if (!MessageType) {
|
||||||
|
console.warn(`[ProtobufSerializer] 未找到消息类型: ${protoName}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
const error = MessageType.verify(protoData);
|
||||||
|
if (error) {
|
||||||
|
console.warn(`[ProtobufSerializer] 数据验证失败: ${error}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建消息并编码
|
||||||
|
const message = MessageType.create(protoData);
|
||||||
|
const buffer = MessageType.encode(message).finish();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'protobuf',
|
||||||
|
componentType: componentType,
|
||||||
|
data: buffer,
|
||||||
|
size: buffer.length
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProtobufSerializer] 序列化失败,回退到JSON: ${componentType}`, error);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反序列化组件
|
||||||
|
*
|
||||||
|
* @param component - 目标组件实例
|
||||||
|
* @param serializedData - 序列化数据
|
||||||
|
*/
|
||||||
|
public deserialize(component: Component, serializedData: SerializedData): void {
|
||||||
|
if (serializedData.type === 'json') {
|
||||||
|
this.deserializeFromJSON(component, serializedData.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) {
|
||||||
|
this.deserializeFromJSON(component, serializedData.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageType = this.getMessageType(protoName);
|
||||||
|
if (!MessageType) {
|
||||||
|
console.warn(`[ProtobufSerializer] 反序列化时未找到消息类型: ${protoName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码消息
|
||||||
|
const message = MessageType.decode(serializedData.data as Uint8Array);
|
||||||
|
const data = MessageType.toObject(message);
|
||||||
|
|
||||||
|
// 应用数据到组件
|
||||||
|
this.applyDataToComponent(component, data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProtobufSerializer] 反序列化失败: ${component.constructor.name}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查组件是否支持protobuf序列化
|
||||||
|
*/
|
||||||
|
public canSerialize(component: Component): boolean {
|
||||||
|
if (!this.protobuf) return false;
|
||||||
|
return isProtoSerializable(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取序列化统计信息
|
||||||
|
*/
|
||||||
|
public getStats(): {
|
||||||
|
registeredComponents: number;
|
||||||
|
protobufAvailable: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
registeredComponents: this.registry.getAllComponents().size,
|
||||||
|
protobufAvailable: !!this.protobuf
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建protobuf数据对象
|
||||||
|
*/
|
||||||
|
private buildProtoData(component: Component, definition: ProtoComponentDefinition): any {
|
||||||
|
const data: any = {};
|
||||||
|
|
||||||
|
for (const [propertyName, fieldDef] of definition.fields) {
|
||||||
|
const value = (component as any)[propertyName];
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
data[fieldDef.name] = this.convertValueToProtoType(value, fieldDef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换值到protobuf类型
|
||||||
|
*/
|
||||||
|
private convertValueToProtoType(value: any, fieldDef: ProtoFieldDefinition): any {
|
||||||
|
if (fieldDef.repeated && Array.isArray(value)) {
|
||||||
|
return value.map(v => this.convertSingleValue(v, fieldDef.type));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.convertSingleValue(value, fieldDef.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换单个值
|
||||||
|
*/
|
||||||
|
private convertSingleValue(value: any, type: ProtoFieldType): any {
|
||||||
|
switch (type) {
|
||||||
|
case ProtoFieldType.INT32:
|
||||||
|
case ProtoFieldType.UINT32:
|
||||||
|
case ProtoFieldType.SINT32:
|
||||||
|
case ProtoFieldType.FIXED32:
|
||||||
|
case ProtoFieldType.SFIXED32:
|
||||||
|
return parseInt(value) || 0;
|
||||||
|
|
||||||
|
case ProtoFieldType.FLOAT:
|
||||||
|
case ProtoFieldType.DOUBLE:
|
||||||
|
return parseFloat(value) || 0;
|
||||||
|
|
||||||
|
case ProtoFieldType.BOOL:
|
||||||
|
return Boolean(value);
|
||||||
|
|
||||||
|
case ProtoFieldType.STRING:
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用数据到组件
|
||||||
|
*/
|
||||||
|
private applyDataToComponent(component: Component, data: any): void {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) return;
|
||||||
|
|
||||||
|
const definition = this.registry.getComponentDefinition(protoName);
|
||||||
|
if (!definition) return;
|
||||||
|
|
||||||
|
for (const [propertyName, fieldDef] of definition.fields) {
|
||||||
|
const value = data[fieldDef.name];
|
||||||
|
if (value !== undefined) {
|
||||||
|
(component as any)[propertyName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回退到JSON序列化
|
||||||
|
*/
|
||||||
|
private fallbackToJSON(component: Component): SerializedData {
|
||||||
|
const data = this.defaultJSONSerialize(component);
|
||||||
|
const jsonString = JSON.stringify(data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'json',
|
||||||
|
componentType: component.constructor.name,
|
||||||
|
data: data,
|
||||||
|
size: new Blob([jsonString]).size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认JSON序列化
|
||||||
|
*/
|
||||||
|
private defaultJSONSerialize(component: Component): any {
|
||||||
|
const data: any = {};
|
||||||
|
|
||||||
|
for (const key in component) {
|
||||||
|
if (component.hasOwnProperty(key) &&
|
||||||
|
typeof (component as any)[key] !== 'function' &&
|
||||||
|
key !== 'id' &&
|
||||||
|
key !== 'entity' &&
|
||||||
|
key !== '_enabled' &&
|
||||||
|
key !== '_updateOrder') {
|
||||||
|
|
||||||
|
const value = (component as any)[key];
|
||||||
|
if (this.isSerializableValue(value)) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON反序列化
|
||||||
|
*/
|
||||||
|
private deserializeFromJSON(component: Component, data: any): void {
|
||||||
|
for (const key in data) {
|
||||||
|
if (component.hasOwnProperty(key) &&
|
||||||
|
typeof (component as any)[key] !== 'function' &&
|
||||||
|
key !== 'id' &&
|
||||||
|
key !== 'entity' &&
|
||||||
|
key !== '_enabled' &&
|
||||||
|
key !== '_updateOrder') {
|
||||||
|
|
||||||
|
(component as any)[key] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查值是否可序列化
|
||||||
|
*/
|
||||||
|
private isSerializableValue(value: any): boolean {
|
||||||
|
if (value === null || value === undefined) return true;
|
||||||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return true;
|
||||||
|
if (Array.isArray(value)) return value.every(v => this.isSerializableValue(v));
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
JSON.stringify(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建protobuf定义
|
||||||
|
*/
|
||||||
|
private buildProtoDefinitions(): void {
|
||||||
|
if (!this.protobuf) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protoDefinition = this.registry.generateProtoDefinition();
|
||||||
|
this.root = this.protobuf.parse(protoDefinition).root;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ProtobufSerializer] 构建protobuf定义失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取消息类型
|
||||||
|
*/
|
||||||
|
private getMessageType(typeName: string): any {
|
||||||
|
if (!this.root) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this.root.lookupType(`ecs.${typeName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[ProtobufSerializer] 未找到消息类型: ecs.${typeName}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
371
src/Utils/Serialization/StaticProtobufSerializer.ts
Normal file
371
src/Utils/Serialization/StaticProtobufSerializer.ts
Normal file
@@ -0,0 +1,371 @@
|
|||||||
|
/**
|
||||||
|
* 静态Protobuf序列化器
|
||||||
|
*
|
||||||
|
* 使用预生成的protobuf静态模块进行序列化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '../../ECS/Component';
|
||||||
|
import {
|
||||||
|
ProtobufRegistry,
|
||||||
|
ProtoComponentDefinition,
|
||||||
|
isProtoSerializable,
|
||||||
|
getProtoName
|
||||||
|
} from './ProtobufDecorators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化数据接口
|
||||||
|
*/
|
||||||
|
export interface SerializedData {
|
||||||
|
/** 序列化类型 */
|
||||||
|
type: 'protobuf' | 'json';
|
||||||
|
/** 组件类型名称 */
|
||||||
|
componentType: string;
|
||||||
|
/** 序列化后的数据 */
|
||||||
|
data: Uint8Array | any;
|
||||||
|
/** 数据大小(字节) */
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态Protobuf序列化器
|
||||||
|
*
|
||||||
|
* 使用CLI预生成的protobuf静态模块
|
||||||
|
*/
|
||||||
|
export class StaticProtobufSerializer {
|
||||||
|
private registry: ProtobufRegistry;
|
||||||
|
private static instance: StaticProtobufSerializer;
|
||||||
|
|
||||||
|
/** 预生成的protobuf根对象 */
|
||||||
|
private protobufRoot: any = null;
|
||||||
|
private isInitialized: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.registry = ProtobufRegistry.getInstance();
|
||||||
|
this.initializeStaticProtobuf();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): StaticProtobufSerializer {
|
||||||
|
if (!StaticProtobufSerializer.instance) {
|
||||||
|
StaticProtobufSerializer.instance = new StaticProtobufSerializer();
|
||||||
|
}
|
||||||
|
return StaticProtobufSerializer.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化静态protobuf模块
|
||||||
|
*/
|
||||||
|
private async initializeStaticProtobuf(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 尝试加载预生成的protobuf模块
|
||||||
|
const ecsProto = await this.loadGeneratedProtobuf();
|
||||||
|
if (ecsProto && ecsProto.ecs) {
|
||||||
|
this.protobufRoot = ecsProto.ecs;
|
||||||
|
this.isInitialized = true;
|
||||||
|
console.log('[StaticProtobufSerializer] 预生成的Protobuf模块已加载');
|
||||||
|
} else {
|
||||||
|
console.warn('[StaticProtobufSerializer] 未找到预生成的protobuf模块,将使用JSON序列化');
|
||||||
|
console.log('💡 请运行: npm run proto:build');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[StaticProtobufSerializer] 初始化失败,将使用JSON序列化:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载预生成的protobuf模块
|
||||||
|
*/
|
||||||
|
private async loadGeneratedProtobuf(): Promise<any> {
|
||||||
|
const possiblePaths = [
|
||||||
|
// 项目中的生成路径
|
||||||
|
'./generated/ecs-components',
|
||||||
|
'../generated/ecs-components',
|
||||||
|
'../../generated/ecs-components',
|
||||||
|
// 相对于当前文件的路径
|
||||||
|
'../../../generated/ecs-components'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
try {
|
||||||
|
const module = await import(path);
|
||||||
|
return module;
|
||||||
|
} catch (error) {
|
||||||
|
// 继续尝试下一个路径
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果所有路径都失败,尝试require方式
|
||||||
|
for (const path of possiblePaths) {
|
||||||
|
try {
|
||||||
|
const module = require(path);
|
||||||
|
return module;
|
||||||
|
} catch (error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化组件
|
||||||
|
*/
|
||||||
|
public serialize(component: Component): SerializedData {
|
||||||
|
const componentType = component.constructor.name;
|
||||||
|
|
||||||
|
// 检查是否支持protobuf序列化
|
||||||
|
if (!isProtoSerializable(component) || !this.isInitialized) {
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) {
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = this.registry.getComponentDefinition(protoName);
|
||||||
|
if (!definition) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 未找到组件定义: ${protoName}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对应的protobuf消息类型
|
||||||
|
const MessageType = this.protobufRoot[protoName];
|
||||||
|
if (!MessageType) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 未找到protobuf消息类型: ${protoName}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建protobuf数据对象
|
||||||
|
const protoData = this.buildProtoData(component, definition);
|
||||||
|
|
||||||
|
// 验证数据
|
||||||
|
const error = MessageType.verify(protoData);
|
||||||
|
if (error) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 数据验证失败: ${error}`);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建消息并编码
|
||||||
|
const message = MessageType.create(protoData);
|
||||||
|
const buffer = MessageType.encode(message).finish();
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'protobuf',
|
||||||
|
componentType: componentType,
|
||||||
|
data: buffer,
|
||||||
|
size: buffer.length
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 序列化失败,回退到JSON: ${componentType}`, error);
|
||||||
|
return this.fallbackToJSON(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反序列化组件
|
||||||
|
*/
|
||||||
|
public deserialize(component: Component, serializedData: SerializedData): void {
|
||||||
|
if (serializedData.type === 'json') {
|
||||||
|
this.deserializeFromJSON(component, serializedData.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isInitialized) {
|
||||||
|
console.warn('[StaticProtobufSerializer] Protobuf未初始化,无法反序列化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) {
|
||||||
|
this.deserializeFromJSON(component, serializedData.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageType = this.protobufRoot[protoName];
|
||||||
|
if (!MessageType) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 反序列化时未找到消息类型: ${protoName}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码消息
|
||||||
|
const message = MessageType.decode(serializedData.data as Uint8Array);
|
||||||
|
const data = MessageType.toObject(message);
|
||||||
|
|
||||||
|
// 应用数据到组件
|
||||||
|
this.applyDataToComponent(component, data);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[StaticProtobufSerializer] 反序列化失败: ${component.constructor.name}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查组件是否支持protobuf序列化
|
||||||
|
*/
|
||||||
|
public canSerialize(component: Component): boolean {
|
||||||
|
return this.isInitialized && isProtoSerializable(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取序列化统计信息
|
||||||
|
*/
|
||||||
|
public getStats(): {
|
||||||
|
registeredComponents: number;
|
||||||
|
protobufAvailable: boolean;
|
||||||
|
initialized: boolean;
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
registeredComponents: this.registry.getAllComponents().size,
|
||||||
|
protobufAvailable: !!this.protobufRoot,
|
||||||
|
initialized: this.isInitialized
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动设置protobuf根对象(用于测试)
|
||||||
|
*/
|
||||||
|
public setProtobufRoot(root: any): void {
|
||||||
|
this.protobufRoot = root;
|
||||||
|
this.isInitialized = !!root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建protobuf数据对象
|
||||||
|
*/
|
||||||
|
private buildProtoData(component: Component, definition: ProtoComponentDefinition): any {
|
||||||
|
const data: any = {};
|
||||||
|
|
||||||
|
for (const [propertyName, fieldDef] of definition.fields) {
|
||||||
|
const value = (component as any)[propertyName];
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
data[fieldDef.name] = this.convertValueToProtoType(value, fieldDef.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换值到protobuf类型
|
||||||
|
*/
|
||||||
|
private convertValueToProtoType(value: any, type: string): any {
|
||||||
|
switch (type) {
|
||||||
|
case 'int32':
|
||||||
|
case 'uint32':
|
||||||
|
case 'sint32':
|
||||||
|
case 'fixed32':
|
||||||
|
case 'sfixed32':
|
||||||
|
return parseInt(value) || 0;
|
||||||
|
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
return parseFloat(value) || 0;
|
||||||
|
|
||||||
|
case 'bool':
|
||||||
|
return Boolean(value);
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
return String(value);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用数据到组件
|
||||||
|
*/
|
||||||
|
private applyDataToComponent(component: Component, data: any): void {
|
||||||
|
const protoName = getProtoName(component);
|
||||||
|
if (!protoName) return;
|
||||||
|
|
||||||
|
const definition = this.registry.getComponentDefinition(protoName);
|
||||||
|
if (!definition) return;
|
||||||
|
|
||||||
|
for (const [propertyName, fieldDef] of definition.fields) {
|
||||||
|
const value = data[fieldDef.name];
|
||||||
|
if (value !== undefined) {
|
||||||
|
(component as any)[propertyName] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回退到JSON序列化
|
||||||
|
*/
|
||||||
|
private fallbackToJSON(component: Component): SerializedData {
|
||||||
|
const data = this.defaultJSONSerialize(component);
|
||||||
|
const jsonString = JSON.stringify(data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: 'json',
|
||||||
|
componentType: component.constructor.name,
|
||||||
|
data: data,
|
||||||
|
size: new Blob([jsonString]).size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认JSON序列化
|
||||||
|
*/
|
||||||
|
private defaultJSONSerialize(component: Component): any {
|
||||||
|
const data: any = {};
|
||||||
|
|
||||||
|
for (const key in component) {
|
||||||
|
if (component.hasOwnProperty(key) &&
|
||||||
|
typeof (component as any)[key] !== 'function' &&
|
||||||
|
key !== 'id' &&
|
||||||
|
key !== 'entity' &&
|
||||||
|
key !== '_enabled' &&
|
||||||
|
key !== '_updateOrder') {
|
||||||
|
|
||||||
|
const value = (component as any)[key];
|
||||||
|
if (this.isSerializableValue(value)) {
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON反序列化
|
||||||
|
*/
|
||||||
|
private deserializeFromJSON(component: Component, data: any): void {
|
||||||
|
for (const key in data) {
|
||||||
|
if (component.hasOwnProperty(key) &&
|
||||||
|
typeof (component as any)[key] !== 'function' &&
|
||||||
|
key !== 'id' &&
|
||||||
|
key !== 'entity' &&
|
||||||
|
key !== '_enabled' &&
|
||||||
|
key !== '_updateOrder') {
|
||||||
|
|
||||||
|
(component as any)[key] = data[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查值是否可序列化
|
||||||
|
*/
|
||||||
|
private isSerializableValue(value: any): boolean {
|
||||||
|
if (value === null || value === undefined) return true;
|
||||||
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return true;
|
||||||
|
if (Array.isArray(value)) return value.every(v => this.isSerializableValue(v));
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
try {
|
||||||
|
JSON.stringify(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/Utils/Serialization/index.ts
Normal file
7
src/Utils/Serialization/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* 序列化模块导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './ProtobufDecorators';
|
||||||
|
export * from './ProtobufSerializer';
|
||||||
|
export * from './StaticProtobufSerializer';
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Entity } from '../../ECS/Entity';
|
import { Entity } from '../../ECS/Entity';
|
||||||
import { Component } from '../../ECS/Component';
|
import { Component } from '../../ECS/Component';
|
||||||
import { ISnapshotable, SceneSnapshot, EntitySnapshot, ComponentSnapshot, SnapshotConfig } from './ISnapshotable';
|
import { ISnapshotable, SceneSnapshot, EntitySnapshot, ComponentSnapshot, SnapshotConfig } from './ISnapshotable';
|
||||||
|
import { ProtobufSerializer, SerializedData } from '../Serialization/ProtobufSerializer';
|
||||||
|
import { isProtoSerializable } from '../Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快照管理器
|
* 快照管理器
|
||||||
*
|
*
|
||||||
* 负责创建和管理ECS系统的快照,支持完整快照和增量快照
|
* 负责创建和管理ECS系统的快照,支持完整快照和增量快照
|
||||||
|
* 现在支持protobuf和JSON混合序列化
|
||||||
*/
|
*/
|
||||||
export class SnapshotManager {
|
export class SnapshotManager {
|
||||||
/** 默认快照配置 */
|
/** 默认快照配置 */
|
||||||
@@ -28,7 +31,15 @@ export class SnapshotManager {
|
|||||||
/** 最大缓存数量 */
|
/** 最大缓存数量 */
|
||||||
private maxCacheSize: number = 10;
|
private maxCacheSize: number = 10;
|
||||||
|
|
||||||
|
/** Protobuf序列化器 */
|
||||||
|
private protobufSerializer: ProtobufSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造函数
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.protobufSerializer = ProtobufSerializer.getInstance();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建场景快照
|
* 创建场景快照
|
||||||
@@ -281,10 +292,32 @@ export class SnapshotManager {
|
|||||||
*/
|
*/
|
||||||
public getCacheStats(): {
|
public getCacheStats(): {
|
||||||
snapshotCacheSize: number;
|
snapshotCacheSize: number;
|
||||||
|
protobufStats?: {
|
||||||
|
registeredComponents: number;
|
||||||
|
protobufAvailable: boolean;
|
||||||
|
};
|
||||||
} {
|
} {
|
||||||
return {
|
const stats: any = {
|
||||||
snapshotCacheSize: this.snapshotCache.size
|
snapshotCacheSize: this.snapshotCache.size
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.protobufSerializer) {
|
||||||
|
stats.protobufStats = this.protobufSerializer.getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动初始化protobuf支持(可选,通常会自动初始化)
|
||||||
|
*
|
||||||
|
* @param protobufJs - protobuf.js库实例
|
||||||
|
*/
|
||||||
|
public initializeProtobuf(protobufJs: any): void {
|
||||||
|
if (this.protobufSerializer) {
|
||||||
|
this.protobufSerializer.initialize(protobufJs);
|
||||||
|
console.log('[SnapshotManager] Protobuf支持已手动启用');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -316,12 +349,42 @@ export class SnapshotManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建组件快照
|
* 创建组件快照
|
||||||
|
*
|
||||||
|
* 现在支持protobuf和JSON混合序列化
|
||||||
*/
|
*/
|
||||||
private createComponentSnapshot(component: Component): ComponentSnapshot | null {
|
private createComponentSnapshot(component: Component): ComponentSnapshot | null {
|
||||||
if (!this.isComponentSnapshotable(component)) {
|
if (!this.isComponentSnapshotable(component)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let serializedData: SerializedData;
|
||||||
|
|
||||||
|
// 优先尝试protobuf序列化
|
||||||
|
if (isProtoSerializable(component) && this.protobufSerializer.canSerialize(component)) {
|
||||||
|
try {
|
||||||
|
serializedData = this.protobufSerializer.serialize(component);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[SnapshotManager] Protobuf序列化失败,回退到传统方式: ${component.constructor.name}`, error);
|
||||||
|
serializedData = this.createLegacySerializedData(component);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用传统序列化方式
|
||||||
|
serializedData = this.createLegacySerializedData(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: component.constructor.name,
|
||||||
|
id: component.id,
|
||||||
|
data: serializedData,
|
||||||
|
enabled: component.enabled,
|
||||||
|
config: this.getComponentSnapshotConfig(component)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建传统序列化数据
|
||||||
|
*/
|
||||||
|
private createLegacySerializedData(component: Component): SerializedData {
|
||||||
let data: any;
|
let data: any;
|
||||||
|
|
||||||
if (this.hasSerializeMethod(component)) {
|
if (this.hasSerializeMethod(component)) {
|
||||||
@@ -329,18 +392,18 @@ export class SnapshotManager {
|
|||||||
data = (component as any).serialize();
|
data = (component as any).serialize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[SnapshotManager] 组件序列化失败: ${component.constructor.name}`, error);
|
console.warn(`[SnapshotManager] 组件序列化失败: ${component.constructor.name}`, error);
|
||||||
return null;
|
data = this.defaultSerializeComponent(component);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data = this.defaultSerializeComponent(component);
|
data = this.defaultSerializeComponent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const jsonString = JSON.stringify(data);
|
||||||
return {
|
return {
|
||||||
type: component.constructor.name,
|
type: 'json',
|
||||||
id: component.id,
|
componentType: component.constructor.name,
|
||||||
data: data,
|
data: data,
|
||||||
enabled: component.enabled,
|
size: new Blob([jsonString]).size
|
||||||
config: this.getComponentSnapshotConfig(component)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,6 +539,8 @@ export class SnapshotManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从快照恢复组件
|
* 从快照恢复组件
|
||||||
|
*
|
||||||
|
* 现在支持protobuf和JSON混合反序列化
|
||||||
*/
|
*/
|
||||||
private restoreComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void {
|
private restoreComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void {
|
||||||
// 查找现有组件
|
// 查找现有组件
|
||||||
@@ -491,15 +556,40 @@ export class SnapshotManager {
|
|||||||
component.enabled = componentSnapshot.enabled;
|
component.enabled = componentSnapshot.enabled;
|
||||||
|
|
||||||
// 恢复组件数据
|
// 恢复组件数据
|
||||||
|
const serializedData = componentSnapshot.data as SerializedData;
|
||||||
|
|
||||||
|
// 检查数据是否为新的SerializedData格式
|
||||||
|
if (serializedData && typeof serializedData === 'object' && 'type' in serializedData) {
|
||||||
|
// 使用新的序列化格式
|
||||||
|
if (serializedData.type === 'protobuf' && isProtoSerializable(component)) {
|
||||||
|
try {
|
||||||
|
this.protobufSerializer.deserialize(component, serializedData);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`[SnapshotManager] Protobuf反序列化失败: ${componentSnapshot.type}`, error);
|
||||||
|
}
|
||||||
|
} else if (serializedData.type === 'json') {
|
||||||
|
// JSON格式反序列化
|
||||||
|
this.deserializeLegacyData(component, serializedData.data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 兼容旧格式数据
|
||||||
|
this.deserializeLegacyData(component, componentSnapshot.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反序列化传统格式数据
|
||||||
|
*/
|
||||||
|
private deserializeLegacyData(component: Component, data: any): void {
|
||||||
if (this.hasSerializeMethod(component)) {
|
if (this.hasSerializeMethod(component)) {
|
||||||
try {
|
try {
|
||||||
(component as any).deserialize(componentSnapshot.data);
|
(component as any).deserialize(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`[SnapshotManager] 组件 ${componentSnapshot.type} 反序列化失败:`, error);
|
console.warn(`[SnapshotManager] 组件 ${component.constructor.name} 反序列化失败:`, error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 使用默认反序列化
|
// 使用默认反序列化
|
||||||
this.defaultDeserializeComponent(component, componentSnapshot.data);
|
this.defaultDeserializeComponent(component, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export * from './Pool';
|
|||||||
export * from './Emitter';
|
export * from './Emitter';
|
||||||
export * from './GlobalManager';
|
export * from './GlobalManager';
|
||||||
export * from './PerformanceMonitor';
|
export * from './PerformanceMonitor';
|
||||||
|
export * from './Serialization';
|
||||||
export { Time } from './Time';
|
export { Time } from './Time';
|
||||||
export * from './Debug';
|
export * from './Debug';
|
||||||
441
tests/Utils/Serialization/Performance.test.ts
Normal file
441
tests/Utils/Serialization/Performance.test.ts
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
/**
|
||||||
|
* Protobuf序列化性能测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { Entity } from '../../../src/ECS/Entity';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
|
||||||
|
import { ProtobufSerializer } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||||
|
import {
|
||||||
|
ProtoSerializable,
|
||||||
|
ProtoFloat,
|
||||||
|
ProtoInt32,
|
||||||
|
ProtoString,
|
||||||
|
ProtoBool
|
||||||
|
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
|
// 性能测试组件
|
||||||
|
@ProtoSerializable('PerfPosition')
|
||||||
|
class PerfPositionComponent extends Component {
|
||||||
|
@ProtoFloat(1) public x: number = 0;
|
||||||
|
@ProtoFloat(2) public y: number = 0;
|
||||||
|
@ProtoFloat(3) public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('PerfVelocity')
|
||||||
|
class PerfVelocityComponent extends Component {
|
||||||
|
@ProtoFloat(1) public vx: number = 0;
|
||||||
|
@ProtoFloat(2) public vy: number = 0;
|
||||||
|
@ProtoFloat(3) public vz: number = 0;
|
||||||
|
|
||||||
|
constructor(vx: number = 0, vy: number = 0, vz: number = 0) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
this.vz = vz;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('PerfHealth')
|
||||||
|
class PerfHealthComponent extends Component {
|
||||||
|
@ProtoInt32(1) public maxHealth: number = 100;
|
||||||
|
@ProtoInt32(2) public currentHealth: number = 100;
|
||||||
|
@ProtoBool(3) public isDead: boolean = false;
|
||||||
|
@ProtoFloat(4) public regenerationRate: number = 0.5;
|
||||||
|
|
||||||
|
constructor(maxHealth: number = 100) {
|
||||||
|
super();
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
this.currentHealth = maxHealth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('PerfPlayer')
|
||||||
|
class PerfPlayerComponent extends Component {
|
||||||
|
@ProtoString(1) public name: string = '';
|
||||||
|
@ProtoInt32(2) public level: number = 1;
|
||||||
|
@ProtoInt32(3) public experience: number = 0;
|
||||||
|
@ProtoInt32(4) public score: number = 0;
|
||||||
|
@ProtoBool(5) public isOnline: boolean = true;
|
||||||
|
|
||||||
|
constructor(name: string = 'Player', level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传统JSON序列化组件(用于对比)
|
||||||
|
class JsonPositionComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonPlayerComponent extends Component {
|
||||||
|
public name: string = '';
|
||||||
|
public level: number = 1;
|
||||||
|
public experience: number = 0;
|
||||||
|
public score: number = 0;
|
||||||
|
public isOnline: boolean = true;
|
||||||
|
|
||||||
|
constructor(name: string = 'Player', level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock protobuf.js for performance testing
|
||||||
|
const createMockProtobuf = () => {
|
||||||
|
const mockEncodedData = new Uint8Array(32); // 模拟32字节的编码数据
|
||||||
|
mockEncodedData.fill(1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
parse: jest.fn().mockReturnValue({
|
||||||
|
root: {
|
||||||
|
lookupType: jest.fn().mockImplementation((typeName: string) => ({
|
||||||
|
verify: jest.fn().mockReturnValue(null),
|
||||||
|
create: jest.fn().mockImplementation((data) => data),
|
||||||
|
encode: jest.fn().mockReturnValue({
|
||||||
|
finish: jest.fn().mockReturnValue(mockEncodedData)
|
||||||
|
}),
|
||||||
|
decode: jest.fn().mockReturnValue({
|
||||||
|
x: 10, y: 20, z: 30,
|
||||||
|
vx: 1, vy: 2, vz: 3,
|
||||||
|
maxHealth: 100, currentHealth: 80, isDead: false, regenerationRate: 0.5,
|
||||||
|
name: 'TestPlayer', level: 5, experience: 1000, score: 5000, isOnline: true
|
||||||
|
}),
|
||||||
|
toObject: jest.fn().mockImplementation((message) => message)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Protobuf序列化性能测试', () => {
|
||||||
|
let protobufSerializer: ProtobufSerializer;
|
||||||
|
let snapshotManager: SnapshotManager;
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
protobufSerializer = ProtobufSerializer.getInstance();
|
||||||
|
protobufSerializer.initialize(createMockProtobuf());
|
||||||
|
|
||||||
|
snapshotManager = new SnapshotManager();
|
||||||
|
snapshotManager.initializeProtobuf(createMockProtobuf());
|
||||||
|
|
||||||
|
scene = new Scene();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('单组件序列化性能', () => {
|
||||||
|
const iterations = 1000;
|
||||||
|
|
||||||
|
it('应该比较protobuf和JSON序列化速度', () => {
|
||||||
|
const protobufComponents: PerfPositionComponent[] = [];
|
||||||
|
const jsonComponents: JsonPositionComponent[] = [];
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
protobufComponents.push(new PerfPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
|
||||||
|
jsonComponents.push(new JsonPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试Protobuf序列化
|
||||||
|
const protobufStartTime = performance.now();
|
||||||
|
let protobufTotalSize = 0;
|
||||||
|
|
||||||
|
for (const component of protobufComponents) {
|
||||||
|
const result = protobufSerializer.serialize(component);
|
||||||
|
protobufTotalSize += result.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protobufEndTime = performance.now();
|
||||||
|
const protobufTime = protobufEndTime - protobufStartTime;
|
||||||
|
|
||||||
|
// 测试JSON序列化
|
||||||
|
const jsonStartTime = performance.now();
|
||||||
|
let jsonTotalSize = 0;
|
||||||
|
|
||||||
|
for (const component of jsonComponents) {
|
||||||
|
const jsonString = JSON.stringify({
|
||||||
|
x: component.x,
|
||||||
|
y: component.y,
|
||||||
|
z: component.z
|
||||||
|
});
|
||||||
|
jsonTotalSize += new Blob([jsonString]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonEndTime = performance.now();
|
||||||
|
const jsonTime = jsonEndTime - jsonStartTime;
|
||||||
|
|
||||||
|
// 性能断言
|
||||||
|
console.log(`\\n=== 单组件序列化性能对比 (${iterations} 次迭代) ===`);
|
||||||
|
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||||
|
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||||
|
console.log(`Protobuf总大小: ${protobufTotalSize} bytes`);
|
||||||
|
console.log(`JSON总大小: ${jsonTotalSize} bytes`);
|
||||||
|
|
||||||
|
if (jsonTime > 0) {
|
||||||
|
const speedImprovement = ((jsonTime - protobufTime) / jsonTime * 100);
|
||||||
|
console.log(`速度提升: ${speedImprovement.toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonTotalSize > 0) {
|
||||||
|
const sizeReduction = ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100);
|
||||||
|
console.log(`大小减少: ${sizeReduction.toFixed(1)}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基本性能验证
|
||||||
|
expect(protobufTime).toBeLessThan(1000); // 不应该超过1秒
|
||||||
|
expect(jsonTime).toBeLessThan(1000);
|
||||||
|
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||||
|
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该测试复杂组件的序列化性能', () => {
|
||||||
|
const protobufPlayers: PerfPlayerComponent[] = [];
|
||||||
|
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||||
|
|
||||||
|
// 创建测试数据
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
protobufPlayers.push(new PerfPlayerComponent(
|
||||||
|
`Player${i}`,
|
||||||
|
Math.floor(Math.random() * 100) + 1
|
||||||
|
));
|
||||||
|
|
||||||
|
jsonPlayers.push(new JsonPlayerComponent(
|
||||||
|
`Player${i}`,
|
||||||
|
Math.floor(Math.random() * 100) + 1
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protobuf序列化测试
|
||||||
|
const protobufStart = performance.now();
|
||||||
|
for (const player of protobufPlayers) {
|
||||||
|
protobufSerializer.serialize(player);
|
||||||
|
}
|
||||||
|
const protobufTime = performance.now() - protobufStart;
|
||||||
|
|
||||||
|
// JSON序列化测试
|
||||||
|
const jsonStart = performance.now();
|
||||||
|
for (const player of jsonPlayers) {
|
||||||
|
JSON.stringify({
|
||||||
|
name: player.name,
|
||||||
|
level: player.level,
|
||||||
|
experience: player.experience,
|
||||||
|
score: player.score,
|
||||||
|
isOnline: player.isOnline
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const jsonTime = performance.now() - jsonStart;
|
||||||
|
|
||||||
|
console.log(`\\n=== 复杂组件序列化性能 (${iterations} 次迭代) ===`);
|
||||||
|
console.log(`Protobuf时间: ${protobufTime.toFixed(2)}ms`);
|
||||||
|
console.log(`JSON时间: ${jsonTime.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
expect(protobufTime).toBeLessThan(1000);
|
||||||
|
expect(jsonTime).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('批量实体序列化性能', () => {
|
||||||
|
it('应该测试大量实体的快照创建性能', () => {
|
||||||
|
const entityCount = 100;
|
||||||
|
const entities: Entity[] = [];
|
||||||
|
|
||||||
|
// 创建测试实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const entity = scene.createEntity(`Entity${i}`);
|
||||||
|
entity.addComponent(new PerfPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
entity.addComponent(new PerfVelocityComponent(
|
||||||
|
Math.random() * 10 - 5,
|
||||||
|
Math.random() * 10 - 5,
|
||||||
|
Math.random() * 2 - 1
|
||||||
|
));
|
||||||
|
entity.addComponent(new PerfHealthComponent(100 + Math.floor(Math.random() * 50)));
|
||||||
|
entity.addComponent(new PerfPlayerComponent(`Player${i}`, Math.floor(Math.random() * 50) + 1));
|
||||||
|
|
||||||
|
entities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试快照创建性能
|
||||||
|
const snapshotStart = performance.now();
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||||
|
const snapshotTime = performance.now() - snapshotStart;
|
||||||
|
|
||||||
|
console.log(`\\n=== 批量实体序列化性能 ===`);
|
||||||
|
console.log(`实体数量: ${entityCount}`);
|
||||||
|
console.log(`每个实体组件数: 4`);
|
||||||
|
console.log(`总组件数: ${entityCount * 4}`);
|
||||||
|
console.log(`快照创建时间: ${snapshotTime.toFixed(2)}ms`);
|
||||||
|
console.log(`平均每组件时间: ${(snapshotTime / (entityCount * 4)).toFixed(3)}ms`);
|
||||||
|
|
||||||
|
expect(snapshot.entities).toHaveLength(entityCount);
|
||||||
|
expect(snapshotTime).toBeLessThan(5000); // 不应该超过5秒
|
||||||
|
|
||||||
|
// 计算快照大小
|
||||||
|
let totalSnapshotSize = 0;
|
||||||
|
for (const entitySnapshot of snapshot.entities) {
|
||||||
|
for (const componentSnapshot of entitySnapshot.components) {
|
||||||
|
if (componentSnapshot.data && typeof componentSnapshot.data === 'object' && 'size' in componentSnapshot.data) {
|
||||||
|
totalSnapshotSize += (componentSnapshot.data as any).size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`快照总大小: ${totalSnapshotSize} bytes`);
|
||||||
|
console.log(`平均每实体大小: ${(totalSnapshotSize / entityCount).toFixed(1)} bytes`);
|
||||||
|
|
||||||
|
expect(totalSnapshotSize).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('反序列化性能', () => {
|
||||||
|
it('应该测试快照恢复性能', () => {
|
||||||
|
const entityCount = 50;
|
||||||
|
const originalEntities: Entity[] = [];
|
||||||
|
|
||||||
|
// 创建原始实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const entity = scene.createEntity(`Original${i}`);
|
||||||
|
entity.addComponent(new PerfPositionComponent(i * 10, i * 20, i));
|
||||||
|
entity.addComponent(new PerfHealthComponent(100 + i));
|
||||||
|
originalEntities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建快照
|
||||||
|
const snapshotStart = performance.now();
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot(originalEntities);
|
||||||
|
const snapshotTime = performance.now() - snapshotStart;
|
||||||
|
|
||||||
|
// 创建目标实体
|
||||||
|
const targetEntities: Entity[] = [];
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const entity = scene.createEntity(`Target${i}`);
|
||||||
|
entity.addComponent(new PerfPositionComponent());
|
||||||
|
entity.addComponent(new PerfHealthComponent());
|
||||||
|
targetEntities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试恢复性能
|
||||||
|
const restoreStart = performance.now();
|
||||||
|
snapshotManager.restoreFromSnapshot(snapshot, targetEntities);
|
||||||
|
const restoreTime = performance.now() - restoreStart;
|
||||||
|
|
||||||
|
console.log(`\\n=== 反序列化性能测试 ===`);
|
||||||
|
console.log(`实体数量: ${entityCount}`);
|
||||||
|
console.log(`序列化时间: ${snapshotTime.toFixed(2)}ms`);
|
||||||
|
console.log(`反序列化时间: ${restoreTime.toFixed(2)}ms`);
|
||||||
|
console.log(`总往返时间: ${(snapshotTime + restoreTime).toFixed(2)}ms`);
|
||||||
|
console.log(`平均每实体往返时间: ${((snapshotTime + restoreTime) / entityCount).toFixed(3)}ms`);
|
||||||
|
|
||||||
|
expect(restoreTime).toBeLessThan(2000); // 不应该超过2秒
|
||||||
|
expect(snapshotTime + restoreTime).toBeLessThan(3000); // 总时间不超过3秒
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('内存使用', () => {
|
||||||
|
it('应该监控序列化过程中的内存使用', () => {
|
||||||
|
const entityCount = 200;
|
||||||
|
const entities: Entity[] = [];
|
||||||
|
|
||||||
|
// 创建大量实体
|
||||||
|
for (let i = 0; i < entityCount; i++) {
|
||||||
|
const entity = scene.createEntity(`MemoryTest${i}`);
|
||||||
|
entity.addComponent(new PerfPositionComponent(
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 1000,
|
||||||
|
Math.random() * 100
|
||||||
|
));
|
||||||
|
entity.addComponent(new PerfVelocityComponent(
|
||||||
|
Math.random() * 10,
|
||||||
|
Math.random() * 10,
|
||||||
|
Math.random() * 2
|
||||||
|
));
|
||||||
|
entity.addComponent(new PerfHealthComponent(Math.floor(Math.random() * 200) + 50));
|
||||||
|
entities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录初始内存(如果可用)
|
||||||
|
const initialMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||||
|
|
||||||
|
// 执行序列化
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot(entities);
|
||||||
|
|
||||||
|
// 记录序列化后内存
|
||||||
|
const afterMemory = (performance as any).memory?.usedJSHeapSize || 0;
|
||||||
|
const memoryIncrease = afterMemory - initialMemory;
|
||||||
|
|
||||||
|
if (initialMemory > 0) {
|
||||||
|
console.log(`\\n=== 内存使用测试 ===`);
|
||||||
|
console.log(`实体数量: ${entityCount}`);
|
||||||
|
console.log(`初始内存: ${(initialMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`序列化后内存: ${(afterMemory / 1024 / 1024).toFixed(2)} MB`);
|
||||||
|
console.log(`内存增加: ${(memoryIncrease / 1024).toFixed(2)} KB`);
|
||||||
|
console.log(`平均每实体内存: ${(memoryIncrease / entityCount).toFixed(1)} bytes`);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(snapshot.entities).toHaveLength(entityCount);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
entities.length = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('极端情况性能', () => {
|
||||||
|
it('应该处理大量小组件的性能', () => {
|
||||||
|
const componentCount = 5000;
|
||||||
|
const components: PerfPositionComponent[] = [];
|
||||||
|
|
||||||
|
// 创建大量小组件
|
||||||
|
for (let i = 0; i < componentCount; i++) {
|
||||||
|
components.push(new PerfPositionComponent(i, i * 2, i * 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
for (const component of components) {
|
||||||
|
protobufSerializer.serialize(component);
|
||||||
|
}
|
||||||
|
const time = performance.now() - start;
|
||||||
|
|
||||||
|
console.log(`\\n=== 大量小组件性能测试 ===`);
|
||||||
|
console.log(`组件数量: ${componentCount}`);
|
||||||
|
console.log(`总时间: ${time.toFixed(2)}ms`);
|
||||||
|
console.log(`平均每组件: ${(time / componentCount).toFixed(4)}ms`);
|
||||||
|
console.log(`每秒处理: ${Math.floor(componentCount / (time / 1000))} 个组件`);
|
||||||
|
|
||||||
|
expect(time).toBeLessThan(10000); // 不超过10秒
|
||||||
|
expect(time / componentCount).toBeLessThan(2); // 每个组件不超过2ms
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
278
tests/Utils/Serialization/ProtobufDecorators.test.ts
Normal file
278
tests/Utils/Serialization/ProtobufDecorators.test.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/**
|
||||||
|
* Protobuf装饰器测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import {
|
||||||
|
ProtoSerializable,
|
||||||
|
ProtoField,
|
||||||
|
ProtoFieldType,
|
||||||
|
ProtoFloat,
|
||||||
|
ProtoInt32,
|
||||||
|
ProtoString,
|
||||||
|
ProtoBool,
|
||||||
|
ProtobufRegistry,
|
||||||
|
isProtoSerializable,
|
||||||
|
getProtoName
|
||||||
|
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ProtoSerializable('TestPosition')
|
||||||
|
class TestPositionComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public x: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(2)
|
||||||
|
public y: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(3)
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('TestPlayer')
|
||||||
|
class TestPlayerComponent extends Component {
|
||||||
|
@ProtoString(1)
|
||||||
|
public name: string = '';
|
||||||
|
|
||||||
|
@ProtoInt32(2)
|
||||||
|
public level: number = 1;
|
||||||
|
|
||||||
|
@ProtoInt32(3)
|
||||||
|
public health: number = 100;
|
||||||
|
|
||||||
|
@ProtoBool(4)
|
||||||
|
public isAlive: boolean = true;
|
||||||
|
|
||||||
|
constructor(name: string = '', level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有装饰器的组件
|
||||||
|
class PlainComponent extends Component {
|
||||||
|
public data: string = 'test';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试字段编号冲突的组件
|
||||||
|
const createConflictingComponent = () => {
|
||||||
|
try {
|
||||||
|
@ProtoSerializable('Conflict')
|
||||||
|
class ConflictComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public x: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(1) // 故意使用相同的字段编号
|
||||||
|
public y: number = 0;
|
||||||
|
}
|
||||||
|
return ConflictComponent;
|
||||||
|
} catch (error) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ProtobufDecorators', () => {
|
||||||
|
let registry: ProtobufRegistry;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// 获取注册表实例
|
||||||
|
registry = ProtobufRegistry.getInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@ProtoSerializable装饰器', () => {
|
||||||
|
it('应该正确标记组件为可序列化', () => {
|
||||||
|
const component = new TestPositionComponent(10, 20, 30);
|
||||||
|
|
||||||
|
expect(isProtoSerializable(component)).toBe(true);
|
||||||
|
expect(getProtoName(component)).toBe('TestPosition');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该在注册表中注册组件定义', () => {
|
||||||
|
expect(registry.hasProtoDefinition('TestPosition')).toBe(true);
|
||||||
|
expect(registry.hasProtoDefinition('TestPlayer')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理没有装饰器的组件', () => {
|
||||||
|
const component = new PlainComponent();
|
||||||
|
|
||||||
|
expect(isProtoSerializable(component)).toBe(false);
|
||||||
|
expect(getProtoName(component)).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@ProtoField装饰器', () => {
|
||||||
|
it('应该正确定义字段', () => {
|
||||||
|
const definition = registry.getComponentDefinition('TestPosition');
|
||||||
|
|
||||||
|
expect(definition).toBeDefined();
|
||||||
|
expect(definition!.fields.size).toBe(3);
|
||||||
|
|
||||||
|
const xField = definition!.fields.get('x');
|
||||||
|
expect(xField).toEqual({
|
||||||
|
fieldNumber: 1,
|
||||||
|
type: ProtoFieldType.FLOAT,
|
||||||
|
repeated: false,
|
||||||
|
optional: false,
|
||||||
|
name: 'x'
|
||||||
|
});
|
||||||
|
|
||||||
|
const yField = definition!.fields.get('y');
|
||||||
|
expect(yField).toEqual({
|
||||||
|
fieldNumber: 2,
|
||||||
|
type: ProtoFieldType.FLOAT,
|
||||||
|
repeated: false,
|
||||||
|
optional: false,
|
||||||
|
name: 'y'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持不同的字段类型', () => {
|
||||||
|
const definition = registry.getComponentDefinition('TestPlayer');
|
||||||
|
|
||||||
|
expect(definition).toBeDefined();
|
||||||
|
expect(definition!.fields.size).toBe(4);
|
||||||
|
|
||||||
|
const nameField = definition!.fields.get('name');
|
||||||
|
expect(nameField!.type).toBe(ProtoFieldType.STRING);
|
||||||
|
|
||||||
|
const levelField = definition!.fields.get('level');
|
||||||
|
expect(levelField!.type).toBe(ProtoFieldType.INT32);
|
||||||
|
|
||||||
|
const healthField = definition!.fields.get('health');
|
||||||
|
expect(healthField!.type).toBe(ProtoFieldType.INT32);
|
||||||
|
|
||||||
|
const isAliveField = definition!.fields.get('isAlive');
|
||||||
|
expect(isAliveField!.type).toBe(ProtoFieldType.BOOL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该检测字段编号冲突', () => {
|
||||||
|
const result = createConflictingComponent();
|
||||||
|
expect(result).toBeInstanceOf(Error);
|
||||||
|
expect((result as Error).message).toContain('字段编号 1 已被字段');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该验证字段编号有效性', () => {
|
||||||
|
expect(() => {
|
||||||
|
class InvalidFieldComponent extends Component {
|
||||||
|
@ProtoField(0) // 无效的字段编号
|
||||||
|
public invalid: number = 0;
|
||||||
|
}
|
||||||
|
}).toThrow('字段编号必须大于0');
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
class InvalidFieldComponent extends Component {
|
||||||
|
@ProtoField(-1) // 无效的字段编号
|
||||||
|
public invalid: number = 0;
|
||||||
|
}
|
||||||
|
}).toThrow('字段编号必须大于0');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('便捷装饰器', () => {
|
||||||
|
it('ProtoFloat应该设置正确的字段类型', () => {
|
||||||
|
@ProtoSerializable('FloatTest')
|
||||||
|
class FloatTestComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public value: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('FloatTest');
|
||||||
|
const field = definition!.fields.get('value');
|
||||||
|
expect(field!.type).toBe(ProtoFieldType.FLOAT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ProtoInt32应该设置正确的字段类型', () => {
|
||||||
|
@ProtoSerializable('Int32Test')
|
||||||
|
class Int32TestComponent extends Component {
|
||||||
|
@ProtoInt32(1)
|
||||||
|
public value: number = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('Int32Test');
|
||||||
|
const field = definition!.fields.get('value');
|
||||||
|
expect(field!.type).toBe(ProtoFieldType.INT32);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ProtoString应该设置正确的字段类型', () => {
|
||||||
|
@ProtoSerializable('StringTest')
|
||||||
|
class StringTestComponent extends Component {
|
||||||
|
@ProtoString(1)
|
||||||
|
public value: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('StringTest');
|
||||||
|
const field = definition!.fields.get('value');
|
||||||
|
expect(field!.type).toBe(ProtoFieldType.STRING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ProtoBool应该设置正确的字段类型', () => {
|
||||||
|
@ProtoSerializable('BoolTest')
|
||||||
|
class BoolTestComponent extends Component {
|
||||||
|
@ProtoBool(1)
|
||||||
|
public value: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('BoolTest');
|
||||||
|
const field = definition!.fields.get('value');
|
||||||
|
expect(field!.type).toBe(ProtoFieldType.BOOL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ProtobufRegistry', () => {
|
||||||
|
it('应该正确生成proto定义', () => {
|
||||||
|
const protoDefinition = registry.generateProtoDefinition();
|
||||||
|
|
||||||
|
expect(protoDefinition).toContain('syntax = "proto3";');
|
||||||
|
expect(protoDefinition).toContain('package ecs;');
|
||||||
|
expect(protoDefinition).toContain('message TestPosition');
|
||||||
|
expect(protoDefinition).toContain('message TestPlayer');
|
||||||
|
expect(protoDefinition).toContain('float x = 1;');
|
||||||
|
expect(protoDefinition).toContain('float y = 2;');
|
||||||
|
expect(protoDefinition).toContain('string name = 1;');
|
||||||
|
expect(protoDefinition).toContain('int32 level = 2;');
|
||||||
|
expect(protoDefinition).toContain('bool isAlive = 4;');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确管理组件注册', () => {
|
||||||
|
const allComponents = registry.getAllComponents();
|
||||||
|
|
||||||
|
expect(allComponents.size).toBeGreaterThanOrEqual(2);
|
||||||
|
expect(allComponents.has('TestPosition')).toBe(true);
|
||||||
|
expect(allComponents.has('TestPlayer')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('字段选项', () => {
|
||||||
|
it('应该支持repeated字段', () => {
|
||||||
|
@ProtoSerializable('RepeatedTest')
|
||||||
|
class RepeatedTestComponent extends Component {
|
||||||
|
@ProtoField(1, ProtoFieldType.INT32, { repeated: true })
|
||||||
|
public values: number[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('RepeatedTest');
|
||||||
|
const field = definition!.fields.get('values');
|
||||||
|
expect(field!.repeated).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该支持optional字段', () => {
|
||||||
|
@ProtoSerializable('OptionalTest')
|
||||||
|
class OptionalTestComponent extends Component {
|
||||||
|
@ProtoField(1, ProtoFieldType.STRING, { optional: true })
|
||||||
|
public optionalValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const definition = registry.getComponentDefinition('OptionalTest');
|
||||||
|
const field = definition!.fields.get('optionalValue');
|
||||||
|
expect(field!.optional).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
314
tests/Utils/Serialization/ProtobufSerializer.test.ts
Normal file
314
tests/Utils/Serialization/ProtobufSerializer.test.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* Protobuf序列化器测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { ProtobufSerializer, SerializedData } from '../../../src/Utils/Serialization/ProtobufSerializer';
|
||||||
|
import {
|
||||||
|
ProtoSerializable,
|
||||||
|
ProtoFloat,
|
||||||
|
ProtoInt32,
|
||||||
|
ProtoString,
|
||||||
|
ProtoBool,
|
||||||
|
ProtobufRegistry
|
||||||
|
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ProtoSerializable('Position')
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public x: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(2)
|
||||||
|
public y: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(3)
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('Health')
|
||||||
|
class HealthComponent extends Component {
|
||||||
|
@ProtoInt32(1)
|
||||||
|
public maxHealth: number = 100;
|
||||||
|
|
||||||
|
@ProtoInt32(2)
|
||||||
|
public currentHealth: number = 100;
|
||||||
|
|
||||||
|
@ProtoBool(3)
|
||||||
|
public isDead: boolean = false;
|
||||||
|
|
||||||
|
constructor(maxHealth: number = 100) {
|
||||||
|
super();
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
this.currentHealth = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
takeDamage(damage: number): void {
|
||||||
|
this.currentHealth = Math.max(0, this.currentHealth - damage);
|
||||||
|
this.isDead = this.currentHealth <= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('Player')
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@ProtoString(1)
|
||||||
|
public playerName: string = '';
|
||||||
|
|
||||||
|
@ProtoInt32(2)
|
||||||
|
public playerId: number = 0;
|
||||||
|
|
||||||
|
@ProtoInt32(3)
|
||||||
|
public level: number = 1;
|
||||||
|
|
||||||
|
constructor(playerId: number = 0, playerName: string = '') {
|
||||||
|
super();
|
||||||
|
this.playerId = playerId;
|
||||||
|
this.playerName = playerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有protobuf装饰器的组件
|
||||||
|
class CustomComponent extends Component {
|
||||||
|
public customData = {
|
||||||
|
settings: { volume: 0.8 },
|
||||||
|
achievements: ['first_kill', 'level_up'],
|
||||||
|
inventory: new Map([['sword', 1], ['potion', 3]])
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义序列化方法
|
||||||
|
serialize(): any {
|
||||||
|
return {
|
||||||
|
customData: {
|
||||||
|
settings: this.customData.settings,
|
||||||
|
achievements: this.customData.achievements,
|
||||||
|
inventory: Array.from(this.customData.inventory.entries())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: any): void {
|
||||||
|
if (data.customData) {
|
||||||
|
this.customData.settings = data.customData.settings || this.customData.settings;
|
||||||
|
this.customData.achievements = data.customData.achievements || this.customData.achievements;
|
||||||
|
if (data.customData.inventory) {
|
||||||
|
this.customData.inventory = new Map(data.customData.inventory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock protobuf.js
|
||||||
|
const mockProtobuf = {
|
||||||
|
parse: jest.fn().mockReturnValue({
|
||||||
|
root: {
|
||||||
|
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||||
|
// 模拟protobuf消息类型
|
||||||
|
return {
|
||||||
|
verify: jest.fn().mockReturnValue(null), // 验证通过
|
||||||
|
create: jest.fn().mockImplementation((data) => data),
|
||||||
|
encode: jest.fn().mockReturnValue({
|
||||||
|
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])) // 模拟编码结果
|
||||||
|
}),
|
||||||
|
decode: jest.fn().mockImplementation(() => ({
|
||||||
|
x: 10, y: 20, z: 30,
|
||||||
|
maxHealth: 100, currentHealth: 80, isDead: false,
|
||||||
|
playerName: 'TestPlayer', playerId: 1001, level: 5
|
||||||
|
})),
|
||||||
|
toObject: jest.fn().mockImplementation((message) => message)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ProtobufSerializer', () => {
|
||||||
|
let serializer: ProtobufSerializer;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
serializer = ProtobufSerializer.getInstance();
|
||||||
|
// 重置mock
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('初始化', () => {
|
||||||
|
it('应该正确初始化protobuf支持', () => {
|
||||||
|
serializer.initialize(mockProtobuf);
|
||||||
|
|
||||||
|
expect(mockProtobuf.parse).toHaveBeenCalled();
|
||||||
|
expect(serializer.canSerialize(new PositionComponent())).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('没有初始化时应该无法序列化protobuf组件', () => {
|
||||||
|
const newSerializer = new (ProtobufSerializer as any)();
|
||||||
|
expect(newSerializer.canSerialize(new PositionComponent())).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('序列化', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
serializer.initialize(mockProtobuf);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确序列化protobuf组件', () => {
|
||||||
|
const position = new PositionComponent(10, 20, 30);
|
||||||
|
const result = serializer.serialize(position);
|
||||||
|
|
||||||
|
expect(result.type).toBe('protobuf');
|
||||||
|
expect(result.componentType).toBe('PositionComponent');
|
||||||
|
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||||
|
expect(result.size).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确序列化复杂protobuf组件', () => {
|
||||||
|
const health = new HealthComponent(150);
|
||||||
|
health.takeDamage(50);
|
||||||
|
|
||||||
|
const result = serializer.serialize(health);
|
||||||
|
|
||||||
|
expect(result.type).toBe('protobuf');
|
||||||
|
expect(result.componentType).toBe('HealthComponent');
|
||||||
|
expect(result.data).toBeInstanceOf(Uint8Array);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该回退到JSON序列化非protobuf组件', () => {
|
||||||
|
const custom = new CustomComponent();
|
||||||
|
const result = serializer.serialize(custom);
|
||||||
|
|
||||||
|
expect(result.type).toBe('json');
|
||||||
|
expect(result.componentType).toBe('CustomComponent');
|
||||||
|
expect(result.data).toEqual(custom.serialize());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('protobuf序列化失败时应该回退到JSON', () => {
|
||||||
|
// 模拟protobuf验证失败
|
||||||
|
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||||
|
mockType.verify.mockReturnValue('验证失败');
|
||||||
|
|
||||||
|
const position = new PositionComponent(10, 20, 30);
|
||||||
|
const result = serializer.serialize(position);
|
||||||
|
|
||||||
|
expect(result.type).toBe('json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('反序列化', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
serializer.initialize(mockProtobuf);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确反序列化protobuf数据', () => {
|
||||||
|
const position = new PositionComponent();
|
||||||
|
const serializedData: SerializedData = {
|
||||||
|
type: 'protobuf',
|
||||||
|
componentType: 'PositionComponent',
|
||||||
|
data: new Uint8Array([1, 2, 3, 4]),
|
||||||
|
size: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
serializer.deserialize(position, serializedData);
|
||||||
|
|
||||||
|
// 验证decode和toObject被调用
|
||||||
|
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||||
|
expect(mockType.decode).toHaveBeenCalled();
|
||||||
|
expect(mockType.toObject).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确反序列化JSON数据', () => {
|
||||||
|
const custom = new CustomComponent();
|
||||||
|
const originalData = custom.serialize();
|
||||||
|
|
||||||
|
const serializedData: SerializedData = {
|
||||||
|
type: 'json',
|
||||||
|
componentType: 'CustomComponent',
|
||||||
|
data: originalData,
|
||||||
|
size: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改组件数据
|
||||||
|
custom.customData.settings.volume = 0.5;
|
||||||
|
|
||||||
|
// 反序列化
|
||||||
|
serializer.deserialize(custom, serializedData);
|
||||||
|
|
||||||
|
// 验证数据被恢复
|
||||||
|
expect(custom.customData.settings.volume).toBe(0.8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理反序列化错误', () => {
|
||||||
|
const position = new PositionComponent();
|
||||||
|
const invalidData: SerializedData = {
|
||||||
|
type: 'protobuf',
|
||||||
|
componentType: 'PositionComponent',
|
||||||
|
data: new Uint8Array([255, 255, 255, 255]), // 无效数据
|
||||||
|
size: 4
|
||||||
|
};
|
||||||
|
|
||||||
|
// 模拟解码失败
|
||||||
|
const mockType = mockProtobuf.parse().root.lookupType('ecs.Position');
|
||||||
|
mockType.decode.mockImplementation(() => {
|
||||||
|
throw new Error('解码失败');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 应该不抛出异常
|
||||||
|
expect(() => {
|
||||||
|
serializer.deserialize(position, invalidData);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('统计信息', () => {
|
||||||
|
it('应该返回正确的统计信息', () => {
|
||||||
|
serializer.initialize(mockProtobuf);
|
||||||
|
const stats = serializer.getStats();
|
||||||
|
|
||||||
|
expect(stats.protobufAvailable).toBe(true);
|
||||||
|
expect(stats.registeredComponents).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('未初始化时应该返回正确的状态', () => {
|
||||||
|
const newSerializer = new (ProtobufSerializer as any)();
|
||||||
|
const stats = newSerializer.getStats();
|
||||||
|
|
||||||
|
expect(stats.protobufAvailable).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('边界情况', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
serializer.initialize(mockProtobuf);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理空值和undefined', () => {
|
||||||
|
const position = new PositionComponent();
|
||||||
|
// 设置一些undefined值
|
||||||
|
(position as any).undefinedProp = undefined;
|
||||||
|
(position as any).nullProp = null;
|
||||||
|
|
||||||
|
const result = serializer.serialize(position);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理循环引用', () => {
|
||||||
|
const custom = new CustomComponent();
|
||||||
|
// 创建循环引用
|
||||||
|
(custom as any).circular = custom;
|
||||||
|
|
||||||
|
const result = serializer.serialize(custom);
|
||||||
|
expect(result.type).toBe('json');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理非常大的数值', () => {
|
||||||
|
const position = new PositionComponent(Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, 0);
|
||||||
|
|
||||||
|
const result = serializer.serialize(position);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
393
tests/Utils/Serialization/RealPerformance.test.ts
Normal file
393
tests/Utils/Serialization/RealPerformance.test.ts
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
/**
|
||||||
|
* 真实 Protobuf 序列化性能测试
|
||||||
|
* 使用实际的 protobufjs 库进行性能对比
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import {
|
||||||
|
ProtoSerializable,
|
||||||
|
ProtoFloat,
|
||||||
|
ProtoInt32,
|
||||||
|
ProtoString,
|
||||||
|
ProtoBool,
|
||||||
|
ProtobufRegistry
|
||||||
|
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ProtoSerializable('Position')
|
||||||
|
class PositionComponent extends Component {
|
||||||
|
@ProtoFloat(1) public x: number = 0;
|
||||||
|
@ProtoFloat(2) public y: number = 0;
|
||||||
|
@ProtoFloat(3) public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('Player')
|
||||||
|
class PlayerComponent extends Component {
|
||||||
|
@ProtoString(1) public name: string = '';
|
||||||
|
@ProtoInt32(2) public level: number = 1;
|
||||||
|
@ProtoInt32(3) public experience: number = 0;
|
||||||
|
@ProtoInt32(4) public score: number = 0;
|
||||||
|
@ProtoBool(5) public isOnline: boolean = true;
|
||||||
|
@ProtoFloat(6) public health: number = 100.0;
|
||||||
|
|
||||||
|
constructor(name: string = 'Player', level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
this.experience = level * 1000;
|
||||||
|
this.score = level * 500;
|
||||||
|
this.health = 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 对比组件
|
||||||
|
class JsonPositionComponent extends Component {
|
||||||
|
public x: number = 0;
|
||||||
|
public y: number = 0;
|
||||||
|
public z: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.z = z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JsonPlayerComponent extends Component {
|
||||||
|
public name: string = '';
|
||||||
|
public level: number = 1;
|
||||||
|
public experience: number = 0;
|
||||||
|
public score: number = 0;
|
||||||
|
public isOnline: boolean = true;
|
||||||
|
public health: number = 100.0;
|
||||||
|
|
||||||
|
constructor(name: string = 'Player', level: number = 1) {
|
||||||
|
super();
|
||||||
|
this.name = name;
|
||||||
|
this.level = level;
|
||||||
|
this.experience = level * 1000;
|
||||||
|
this.score = level * 500;
|
||||||
|
this.health = 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('真实 Protobuf 性能测试', () => {
|
||||||
|
let protobuf: any;
|
||||||
|
let root: any;
|
||||||
|
let PositionType: any;
|
||||||
|
let PlayerType: any;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
try {
|
||||||
|
// 尝试加载真实的 protobufjs
|
||||||
|
protobuf = require('protobufjs');
|
||||||
|
|
||||||
|
// 生成 proto 定义
|
||||||
|
const registry = ProtobufRegistry.getInstance();
|
||||||
|
const protoDefinition = registry.generateProtoDefinition();
|
||||||
|
|
||||||
|
console.log('Generated proto definition:');
|
||||||
|
console.log(protoDefinition);
|
||||||
|
|
||||||
|
// 解析 proto 定义
|
||||||
|
root = protobuf.parse(protoDefinition).root;
|
||||||
|
PositionType = root.lookupType('ecs.Position');
|
||||||
|
PlayerType = root.lookupType('ecs.Player');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Protobuf not available, skipping real performance tests:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const skipIfNoProtobuf = () => {
|
||||||
|
if (!protobuf || !root) {
|
||||||
|
console.log('Skipping test: protobufjs not available');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('简单组件性能对比', () => {
|
||||||
|
it('Position 组件序列化性能', () => {
|
||||||
|
if (skipIfNoProtobuf()) return;
|
||||||
|
|
||||||
|
const iterations = 1000;
|
||||||
|
const protobufComponents: PositionComponent[] = [];
|
||||||
|
const jsonComponents: JsonPositionComponent[] = [];
|
||||||
|
|
||||||
|
// 准备测试数据
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
const x = Math.random() * 1000;
|
||||||
|
const y = Math.random() * 1000;
|
||||||
|
const z = Math.random() * 100;
|
||||||
|
|
||||||
|
protobufComponents.push(new PositionComponent(x, y, z));
|
||||||
|
jsonComponents.push(new JsonPositionComponent(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protobuf 序列化测试
|
||||||
|
const protobufStartTime = performance.now();
|
||||||
|
let protobufTotalSize = 0;
|
||||||
|
const protobufResults: Uint8Array[] = [];
|
||||||
|
|
||||||
|
for (const component of protobufComponents) {
|
||||||
|
const message = PositionType.create({
|
||||||
|
x: component.x,
|
||||||
|
y: component.y,
|
||||||
|
z: component.z
|
||||||
|
});
|
||||||
|
const buffer = PositionType.encode(message).finish();
|
||||||
|
protobufResults.push(buffer);
|
||||||
|
protobufTotalSize += buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protobufEndTime = performance.now();
|
||||||
|
const protobufTime = protobufEndTime - protobufStartTime;
|
||||||
|
|
||||||
|
// JSON 序列化测试
|
||||||
|
const jsonStartTime = performance.now();
|
||||||
|
let jsonTotalSize = 0;
|
||||||
|
const jsonResults: string[] = [];
|
||||||
|
|
||||||
|
for (const component of jsonComponents) {
|
||||||
|
const jsonString = JSON.stringify({
|
||||||
|
x: component.x,
|
||||||
|
y: component.y,
|
||||||
|
z: component.z
|
||||||
|
});
|
||||||
|
jsonResults.push(jsonString);
|
||||||
|
jsonTotalSize += new Blob([jsonString]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonEndTime = performance.now();
|
||||||
|
const jsonTime = jsonEndTime - jsonStartTime;
|
||||||
|
|
||||||
|
// 计算性能指标
|
||||||
|
const speedImprovement = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||||
|
const sizeReduction = jsonTotalSize > 0 ? ((jsonTotalSize - protobufTotalSize) / jsonTotalSize * 100) : 0;
|
||||||
|
|
||||||
|
console.log(`\\n=== Position 组件性能对比 (${iterations} 次迭代) ===`);
|
||||||
|
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||||
|
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||||
|
console.log(`速度变化: ${speedImprovement > 0 ? '+' : ''}${speedImprovement.toFixed(1)}%`);
|
||||||
|
console.log('');
|
||||||
|
console.log(`Protobuf 总大小: ${protobufTotalSize} bytes`);
|
||||||
|
console.log(`JSON 总大小: ${jsonTotalSize} bytes`);
|
||||||
|
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||||
|
console.log(`平均 Protobuf 大小: ${(protobufTotalSize / iterations).toFixed(1)} bytes`);
|
||||||
|
console.log(`平均 JSON 大小: ${(jsonTotalSize / iterations).toFixed(1)} bytes`);
|
||||||
|
|
||||||
|
// 验证反序列化
|
||||||
|
let deserializeTime = performance.now();
|
||||||
|
for (const buffer of protobufResults.slice(0, 10)) { // 只测试前10个
|
||||||
|
const decoded = PositionType.decode(buffer);
|
||||||
|
expect(typeof decoded.x).toBe('number');
|
||||||
|
expect(typeof decoded.y).toBe('number');
|
||||||
|
expect(typeof decoded.z).toBe('number');
|
||||||
|
}
|
||||||
|
deserializeTime = performance.now() - deserializeTime;
|
||||||
|
console.log(`Protobuf 反序列化 10 个: ${deserializeTime.toFixed(2)}ms`);
|
||||||
|
|
||||||
|
// 基本验证
|
||||||
|
expect(protobufTime).toBeGreaterThan(0);
|
||||||
|
expect(jsonTime).toBeGreaterThan(0);
|
||||||
|
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||||
|
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('复杂 Player 组件序列化性能', () => {
|
||||||
|
if (skipIfNoProtobuf()) return;
|
||||||
|
|
||||||
|
const iterations = 500;
|
||||||
|
const protobufPlayers: PlayerComponent[] = [];
|
||||||
|
const jsonPlayers: JsonPlayerComponent[] = [];
|
||||||
|
|
||||||
|
// 创建测试数据
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
const name = `Player_${i}_${'x'.repeat(10 + Math.floor(Math.random() * 20))}`;
|
||||||
|
const level = Math.floor(Math.random() * 100) + 1;
|
||||||
|
|
||||||
|
protobufPlayers.push(new PlayerComponent(name, level));
|
||||||
|
jsonPlayers.push(new JsonPlayerComponent(name, level));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Protobuf 序列化测试
|
||||||
|
const protobufStart = performance.now();
|
||||||
|
let protobufSize = 0;
|
||||||
|
|
||||||
|
for (const player of protobufPlayers) {
|
||||||
|
const message = PlayerType.create({
|
||||||
|
name: player.name,
|
||||||
|
level: player.level,
|
||||||
|
experience: player.experience,
|
||||||
|
score: player.score,
|
||||||
|
isOnline: player.isOnline,
|
||||||
|
health: player.health
|
||||||
|
});
|
||||||
|
const buffer = PlayerType.encode(message).finish();
|
||||||
|
protobufSize += buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protobufTime = performance.now() - protobufStart;
|
||||||
|
|
||||||
|
// JSON 序列化测试
|
||||||
|
const jsonStart = performance.now();
|
||||||
|
let jsonSize = 0;
|
||||||
|
|
||||||
|
for (const player of jsonPlayers) {
|
||||||
|
const jsonString = JSON.stringify({
|
||||||
|
name: player.name,
|
||||||
|
level: player.level,
|
||||||
|
experience: player.experience,
|
||||||
|
score: player.score,
|
||||||
|
isOnline: player.isOnline,
|
||||||
|
health: player.health
|
||||||
|
});
|
||||||
|
jsonSize += new Blob([jsonString]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonTime = performance.now() - jsonStart;
|
||||||
|
|
||||||
|
const speedChange = jsonTime > 0 ? ((jsonTime - protobufTime) / jsonTime * 100) : 0;
|
||||||
|
const sizeReduction = jsonSize > 0 ? ((jsonSize - protobufSize) / jsonSize * 100) : 0;
|
||||||
|
|
||||||
|
console.log(`\\n=== Player 组件性能对比 (${iterations} 次迭代) ===`);
|
||||||
|
console.log(`Protobuf 时间: ${protobufTime.toFixed(2)}ms`);
|
||||||
|
console.log(`JSON 时间: ${jsonTime.toFixed(2)}ms`);
|
||||||
|
console.log(`速度变化: ${speedChange > 0 ? '+' : ''}${speedChange.toFixed(1)}%`);
|
||||||
|
console.log('');
|
||||||
|
console.log(`Protobuf 总大小: ${protobufSize} bytes`);
|
||||||
|
console.log(`JSON 总大小: ${jsonSize} bytes`);
|
||||||
|
console.log(`大小变化: ${sizeReduction > 0 ? '-' : '+'}${Math.abs(sizeReduction).toFixed(1)}%`);
|
||||||
|
console.log(`平均 Protobuf 大小: ${(protobufSize / iterations).toFixed(1)} bytes`);
|
||||||
|
console.log(`平均 JSON 大小: ${(jsonSize / iterations).toFixed(1)} bytes`);
|
||||||
|
|
||||||
|
expect(protobufTime).toBeGreaterThan(0);
|
||||||
|
expect(jsonTime).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('批量数据性能测试', () => {
|
||||||
|
it('大量小对象序列化', () => {
|
||||||
|
if (skipIfNoProtobuf()) return;
|
||||||
|
|
||||||
|
const count = 5000;
|
||||||
|
console.log(`\\n=== 大量小对象测试 (${count} 个 Position) ===`);
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const positions = Array.from({ length: count }, (_, i) => ({
|
||||||
|
x: i * 0.1,
|
||||||
|
y: i * 0.2,
|
||||||
|
z: i * 0.05
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Protobuf 批量序列化
|
||||||
|
const protobufStart = performance.now();
|
||||||
|
let protobufSize = 0;
|
||||||
|
|
||||||
|
for (const pos of positions) {
|
||||||
|
const message = PositionType.create(pos);
|
||||||
|
const buffer = PositionType.encode(message).finish();
|
||||||
|
protobufSize += buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protobufTime = performance.now() - protobufStart;
|
||||||
|
|
||||||
|
// JSON 批量序列化
|
||||||
|
const jsonStart = performance.now();
|
||||||
|
let jsonSize = 0;
|
||||||
|
|
||||||
|
for (const pos of positions) {
|
||||||
|
const jsonString = JSON.stringify(pos);
|
||||||
|
jsonSize += jsonString.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonTime = performance.now() - jsonStart;
|
||||||
|
|
||||||
|
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufSize} bytes`);
|
||||||
|
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonSize} bytes`);
|
||||||
|
console.log(`速度: ${protobufTime < jsonTime ? 'Protobuf 更快' : 'JSON 更快'} (${Math.abs(protobufTime - jsonTime).toFixed(2)}ms 差异)`);
|
||||||
|
console.log(`大小: Protobuf ${protobufSize < jsonSize ? '更小' : '更大'} (${Math.abs(protobufSize - jsonSize)} bytes 差异)`);
|
||||||
|
console.log(`处理速度: Protobuf ${Math.floor(count / (protobufTime / 1000))} ops/s, JSON ${Math.floor(count / (jsonTime / 1000))} ops/s`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('真实网络场景模拟', () => {
|
||||||
|
it('游戏状态同步场景', () => {
|
||||||
|
if (skipIfNoProtobuf()) return;
|
||||||
|
|
||||||
|
console.log(`\\n=== 游戏状态同步场景 ===`);
|
||||||
|
|
||||||
|
// 模拟 100 个玩家的位置更新
|
||||||
|
const playerCount = 100;
|
||||||
|
const updateData = Array.from({ length: playerCount }, (_, i) => ({
|
||||||
|
playerId: i,
|
||||||
|
x: Math.random() * 1000,
|
||||||
|
y: Math.random() * 1000,
|
||||||
|
z: Math.random() * 100,
|
||||||
|
health: Math.floor(Math.random() * 100),
|
||||||
|
isMoving: Math.random() > 0.5
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 创建组合消息类型(模拟)
|
||||||
|
const GameUpdateType = root.lookupType('ecs.Position'); // 简化使用 Position
|
||||||
|
|
||||||
|
// Protobuf 序列化所有更新
|
||||||
|
const protobufStart = performance.now();
|
||||||
|
let protobufTotalSize = 0;
|
||||||
|
|
||||||
|
for (const update of updateData) {
|
||||||
|
const message = GameUpdateType.create({
|
||||||
|
x: update.x,
|
||||||
|
y: update.y,
|
||||||
|
z: update.z
|
||||||
|
});
|
||||||
|
const buffer = GameUpdateType.encode(message).finish();
|
||||||
|
protobufTotalSize += buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protobufTime = performance.now() - protobufStart;
|
||||||
|
|
||||||
|
// JSON 序列化所有更新
|
||||||
|
const jsonStart = performance.now();
|
||||||
|
let jsonTotalSize = 0;
|
||||||
|
|
||||||
|
for (const update of updateData) {
|
||||||
|
const jsonString = JSON.stringify({
|
||||||
|
playerId: update.playerId,
|
||||||
|
x: update.x,
|
||||||
|
y: update.y,
|
||||||
|
z: update.z,
|
||||||
|
health: update.health,
|
||||||
|
isMoving: update.isMoving
|
||||||
|
});
|
||||||
|
jsonTotalSize += jsonString.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonTime = performance.now() - jsonStart;
|
||||||
|
|
||||||
|
console.log(`${playerCount} 个玩家位置更新:`);
|
||||||
|
console.log(`Protobuf: ${protobufTime.toFixed(2)}ms, ${protobufTotalSize} bytes`);
|
||||||
|
console.log(`JSON: ${jsonTime.toFixed(2)}ms, ${jsonTotalSize} bytes`);
|
||||||
|
|
||||||
|
// 计算网络传输节省
|
||||||
|
const sizeSaving = jsonTotalSize - protobufTotalSize;
|
||||||
|
const percentSaving = (sizeSaving / jsonTotalSize * 100);
|
||||||
|
|
||||||
|
console.log(`数据大小节省: ${sizeSaving} bytes (${percentSaving.toFixed(1)}%)`);
|
||||||
|
console.log(`每秒 60 次更新的带宽节省: ${(sizeSaving * 60 / 1024).toFixed(2)} KB/s`);
|
||||||
|
|
||||||
|
expect(protobufTotalSize).toBeGreaterThan(0);
|
||||||
|
expect(jsonTotalSize).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
370
tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
Normal file
370
tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
Normal file
@@ -0,0 +1,370 @@
|
|||||||
|
/**
|
||||||
|
* SnapshotManager与Protobuf序列化集成测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Entity } from '../../../src/ECS/Entity';
|
||||||
|
import { Scene } from '../../../src/ECS/Scene';
|
||||||
|
import { Component } from '../../../src/ECS/Component';
|
||||||
|
import { SnapshotManager } from '../../../src/Utils/Snapshot/SnapshotManager';
|
||||||
|
import {
|
||||||
|
ProtoSerializable,
|
||||||
|
ProtoFloat,
|
||||||
|
ProtoInt32,
|
||||||
|
ProtoString,
|
||||||
|
ProtoBool
|
||||||
|
} from '../../../src/Utils/Serialization/ProtobufDecorators';
|
||||||
|
|
||||||
|
// 测试组件
|
||||||
|
@ProtoSerializable('TestPosition')
|
||||||
|
class TestPositionComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public x: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(2)
|
||||||
|
public y: number = 0;
|
||||||
|
|
||||||
|
constructor(x: number = 0, y: number = 0) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('TestVelocity')
|
||||||
|
class TestVelocityComponent extends Component {
|
||||||
|
@ProtoFloat(1)
|
||||||
|
public vx: number = 0;
|
||||||
|
|
||||||
|
@ProtoFloat(2)
|
||||||
|
public vy: number = 0;
|
||||||
|
|
||||||
|
constructor(vx: number = 0, vy: number = 0) {
|
||||||
|
super();
|
||||||
|
this.vx = vx;
|
||||||
|
this.vy = vy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ProtoSerializable('TestHealth')
|
||||||
|
class TestHealthComponent extends Component {
|
||||||
|
@ProtoInt32(1)
|
||||||
|
public maxHealth: number = 100;
|
||||||
|
|
||||||
|
@ProtoInt32(2)
|
||||||
|
public currentHealth: number = 100;
|
||||||
|
|
||||||
|
@ProtoBool(3)
|
||||||
|
public isDead: boolean = false;
|
||||||
|
|
||||||
|
constructor(maxHealth: number = 100) {
|
||||||
|
super();
|
||||||
|
this.maxHealth = maxHealth;
|
||||||
|
this.currentHealth = maxHealth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传统JSON序列化组件
|
||||||
|
class TraditionalComponent extends Component {
|
||||||
|
public customData = {
|
||||||
|
name: 'traditional',
|
||||||
|
values: [1, 2, 3],
|
||||||
|
settings: { enabled: true }
|
||||||
|
};
|
||||||
|
|
||||||
|
serialize(): any {
|
||||||
|
return {
|
||||||
|
customData: this.customData
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
deserialize(data: any): void {
|
||||||
|
if (data.customData) {
|
||||||
|
this.customData = data.customData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单组件(使用默认序列化)
|
||||||
|
class SimpleComponent extends Component {
|
||||||
|
public value: number = 42;
|
||||||
|
public text: string = 'simple';
|
||||||
|
public flag: boolean = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock protobuf.js
|
||||||
|
const mockProtobuf = {
|
||||||
|
parse: jest.fn().mockReturnValue({
|
||||||
|
root: {
|
||||||
|
lookupType: jest.fn().mockImplementation((typeName: string) => {
|
||||||
|
const mockData = {
|
||||||
|
'ecs.TestPosition': { x: 10, y: 20 },
|
||||||
|
'ecs.TestVelocity': { vx: 5, vy: 3 },
|
||||||
|
'ecs.TestHealth': { maxHealth: 100, currentHealth: 80, isDead: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
verify: jest.fn().mockReturnValue(null),
|
||||||
|
create: jest.fn().mockImplementation((data) => data),
|
||||||
|
encode: jest.fn().mockReturnValue({
|
||||||
|
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||||
|
}),
|
||||||
|
decode: jest.fn().mockReturnValue(mockData[typeName] || {}),
|
||||||
|
toObject: jest.fn().mockImplementation((message) => message)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SnapshotManager Protobuf集成', () => {
|
||||||
|
let snapshotManager: SnapshotManager;
|
||||||
|
let scene: Scene;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
snapshotManager = new SnapshotManager();
|
||||||
|
snapshotManager.initializeProtobuf(mockProtobuf);
|
||||||
|
scene = new Scene();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('混合序列化快照', () => {
|
||||||
|
it('应该正确创建包含protobuf和JSON组件的快照', () => {
|
||||||
|
// 创建实体
|
||||||
|
const player = scene.createEntity('Player');
|
||||||
|
player.addComponent(new TestPositionComponent(100, 200));
|
||||||
|
player.addComponent(new TestVelocityComponent(5, 3));
|
||||||
|
player.addComponent(new TestHealthComponent(120));
|
||||||
|
player.addComponent(new TraditionalComponent());
|
||||||
|
player.addComponent(new SimpleComponent());
|
||||||
|
|
||||||
|
// 创建快照
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([player]);
|
||||||
|
|
||||||
|
expect(snapshot).toBeDefined();
|
||||||
|
expect(snapshot.entities).toHaveLength(1);
|
||||||
|
expect(snapshot.entities[0].components).toHaveLength(5);
|
||||||
|
|
||||||
|
// 验证快照包含所有组件
|
||||||
|
const componentTypes = snapshot.entities[0].components.map(c => c.type);
|
||||||
|
expect(componentTypes).toContain('TestPositionComponent');
|
||||||
|
expect(componentTypes).toContain('TestVelocityComponent');
|
||||||
|
expect(componentTypes).toContain('TestHealthComponent');
|
||||||
|
expect(componentTypes).toContain('TraditionalComponent');
|
||||||
|
expect(componentTypes).toContain('SimpleComponent');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该根据组件类型使用相应的序列化方式', () => {
|
||||||
|
const entity = scene.createEntity('TestEntity');
|
||||||
|
const position = new TestPositionComponent(50, 75);
|
||||||
|
const traditional = new TraditionalComponent();
|
||||||
|
|
||||||
|
entity.addComponent(position);
|
||||||
|
entity.addComponent(traditional);
|
||||||
|
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||||
|
const components = snapshot.entities[0].components;
|
||||||
|
|
||||||
|
// 检查序列化数据格式
|
||||||
|
const positionSnapshot = components.find(c => c.type === 'TestPositionComponent');
|
||||||
|
const traditionalSnapshot = components.find(c => c.type === 'TraditionalComponent');
|
||||||
|
|
||||||
|
expect(positionSnapshot).toBeDefined();
|
||||||
|
expect(traditionalSnapshot).toBeDefined();
|
||||||
|
|
||||||
|
// Protobuf组件应该有SerializedData格式
|
||||||
|
expect(positionSnapshot!.data).toHaveProperty('type');
|
||||||
|
expect(positionSnapshot!.data).toHaveProperty('componentType');
|
||||||
|
expect(positionSnapshot!.data).toHaveProperty('data');
|
||||||
|
expect(positionSnapshot!.data).toHaveProperty('size');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('快照恢复', () => {
|
||||||
|
it('应该正确恢复protobuf序列化的组件', () => {
|
||||||
|
// 创建原始实体
|
||||||
|
const originalEntity = scene.createEntity('Original');
|
||||||
|
const originalPosition = new TestPositionComponent(100, 200);
|
||||||
|
const originalHealth = new TestHealthComponent(150);
|
||||||
|
originalHealth.currentHealth = 120;
|
||||||
|
|
||||||
|
originalEntity.addComponent(originalPosition);
|
||||||
|
originalEntity.addComponent(originalHealth);
|
||||||
|
|
||||||
|
// 创建快照
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||||
|
|
||||||
|
// 创建新实体进行恢复
|
||||||
|
const newEntity = scene.createEntity('New');
|
||||||
|
newEntity.addComponent(new TestPositionComponent());
|
||||||
|
newEntity.addComponent(new TestHealthComponent());
|
||||||
|
|
||||||
|
// 恢复快照
|
||||||
|
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||||
|
|
||||||
|
// 验证数据被正确恢复(注意:由于使用mock,实际值来自mock数据)
|
||||||
|
const restoredPosition = newEntity.getComponent(TestPositionComponent);
|
||||||
|
const restoredHealth = newEntity.getComponent(TestHealthComponent);
|
||||||
|
|
||||||
|
expect(restoredPosition).toBeDefined();
|
||||||
|
expect(restoredHealth).toBeDefined();
|
||||||
|
|
||||||
|
// 验证protobuf的decode方法被调用
|
||||||
|
expect(mockProtobuf.parse().root.lookupType).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确恢复传统JSON序列化的组件', () => {
|
||||||
|
const originalEntity = scene.createEntity('Original');
|
||||||
|
const originalTraditional = new TraditionalComponent();
|
||||||
|
originalTraditional.customData.name = 'modified';
|
||||||
|
originalTraditional.customData.values = [4, 5, 6];
|
||||||
|
|
||||||
|
originalEntity.addComponent(originalTraditional);
|
||||||
|
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||||
|
|
||||||
|
const newEntity = scene.createEntity('New');
|
||||||
|
const newTraditional = new TraditionalComponent();
|
||||||
|
newEntity.addComponent(newTraditional);
|
||||||
|
|
||||||
|
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||||
|
|
||||||
|
// 验证JSON数据被正确恢复
|
||||||
|
expect(newTraditional.customData.name).toBe('modified');
|
||||||
|
expect(newTraditional.customData.values).toEqual([4, 5, 6]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该处理混合序列化的实体恢复', () => {
|
||||||
|
const originalEntity = scene.createEntity('Mixed');
|
||||||
|
const position = new TestPositionComponent(30, 40);
|
||||||
|
const traditional = new TraditionalComponent();
|
||||||
|
const simple = new SimpleComponent();
|
||||||
|
|
||||||
|
traditional.customData.name = 'mixed_test';
|
||||||
|
simple.value = 99;
|
||||||
|
simple.text = 'updated';
|
||||||
|
|
||||||
|
originalEntity.addComponent(position);
|
||||||
|
originalEntity.addComponent(traditional);
|
||||||
|
originalEntity.addComponent(simple);
|
||||||
|
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([originalEntity]);
|
||||||
|
|
||||||
|
const newEntity = scene.createEntity('NewMixed');
|
||||||
|
newEntity.addComponent(new TestPositionComponent());
|
||||||
|
newEntity.addComponent(new TraditionalComponent());
|
||||||
|
newEntity.addComponent(new SimpleComponent());
|
||||||
|
|
||||||
|
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||||
|
|
||||||
|
// 验证所有组件都被正确恢复
|
||||||
|
const restoredTraditional = newEntity.getComponent(TraditionalComponent);
|
||||||
|
const restoredSimple = newEntity.getComponent(SimpleComponent);
|
||||||
|
|
||||||
|
expect(restoredTraditional!.customData.name).toBe('mixed_test');
|
||||||
|
expect(restoredSimple!.value).toBe(99);
|
||||||
|
expect(restoredSimple!.text).toBe('updated');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('向后兼容性', () => {
|
||||||
|
it('应该能够处理旧格式的快照数据', () => {
|
||||||
|
// 模拟旧格式的快照数据
|
||||||
|
const legacySnapshot = {
|
||||||
|
entities: [{
|
||||||
|
id: 1,
|
||||||
|
name: 'LegacyEntity',
|
||||||
|
enabled: true,
|
||||||
|
active: true,
|
||||||
|
tag: 0,
|
||||||
|
updateOrder: 0,
|
||||||
|
components: [{
|
||||||
|
type: 'SimpleComponent',
|
||||||
|
id: 1,
|
||||||
|
data: { value: 123, text: 'legacy', flag: false }, // 直接的JSON数据
|
||||||
|
enabled: true,
|
||||||
|
config: { includeInSnapshot: true, compressionLevel: 0, syncPriority: 5, enableIncremental: true }
|
||||||
|
}],
|
||||||
|
children: [],
|
||||||
|
timestamp: Date.now()
|
||||||
|
}],
|
||||||
|
timestamp: Date.now(),
|
||||||
|
version: '1.0.0',
|
||||||
|
type: 'full' as const
|
||||||
|
};
|
||||||
|
|
||||||
|
const entity = scene.createEntity('TestEntity');
|
||||||
|
entity.addComponent(new SimpleComponent());
|
||||||
|
|
||||||
|
snapshotManager.restoreFromSnapshot(legacySnapshot, [entity]);
|
||||||
|
|
||||||
|
const component = entity.getComponent(SimpleComponent);
|
||||||
|
expect(component!.value).toBe(123);
|
||||||
|
expect(component!.text).toBe('legacy');
|
||||||
|
expect(component!.flag).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('错误处理', () => {
|
||||||
|
it('应该优雅地处理protobuf序列化失败', () => {
|
||||||
|
// 模拟protobuf验证失败
|
||||||
|
const mockType = mockProtobuf.parse().root.lookupType;
|
||||||
|
mockType.mockImplementation(() => ({
|
||||||
|
verify: jest.fn().mockReturnValue('验证失败'),
|
||||||
|
create: jest.fn(),
|
||||||
|
encode: jest.fn(),
|
||||||
|
decode: jest.fn(),
|
||||||
|
toObject: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const entity = scene.createEntity('ErrorTest');
|
||||||
|
entity.addComponent(new TestPositionComponent(10, 20));
|
||||||
|
|
||||||
|
// 应该不抛出异常,而是回退到JSON序列化
|
||||||
|
expect(() => {
|
||||||
|
snapshotManager.createSceneSnapshot([entity]);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该优雅地处理protobuf反序列化失败', () => {
|
||||||
|
const entity = scene.createEntity('Test');
|
||||||
|
const position = new TestPositionComponent(10, 20);
|
||||||
|
entity.addComponent(position);
|
||||||
|
|
||||||
|
const snapshot = snapshotManager.createSceneSnapshot([entity]);
|
||||||
|
|
||||||
|
// 模拟反序列化失败
|
||||||
|
const mockType = mockProtobuf.parse().root.lookupType;
|
||||||
|
mockType.mockImplementation(() => ({
|
||||||
|
verify: jest.fn().mockReturnValue(null),
|
||||||
|
create: jest.fn(),
|
||||||
|
encode: jest.fn().mockReturnValue({
|
||||||
|
finish: jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4]))
|
||||||
|
}),
|
||||||
|
decode: jest.fn().mockImplementation(() => {
|
||||||
|
throw new Error('解码失败');
|
||||||
|
}),
|
||||||
|
toObject: jest.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
const newEntity = scene.createEntity('NewTest');
|
||||||
|
newEntity.addComponent(new TestPositionComponent());
|
||||||
|
|
||||||
|
// 应该不抛出异常
|
||||||
|
expect(() => {
|
||||||
|
snapshotManager.restoreFromSnapshot(snapshot, [newEntity]);
|
||||||
|
}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('统计信息', () => {
|
||||||
|
it('应该包含protobuf统计信息', () => {
|
||||||
|
const stats = snapshotManager.getCacheStats();
|
||||||
|
|
||||||
|
expect(stats).toHaveProperty('snapshotCacheSize');
|
||||||
|
expect(stats).toHaveProperty('protobufStats');
|
||||||
|
expect(stats.protobufStats).toHaveProperty('registeredComponents');
|
||||||
|
expect(stats.protobufStats).toHaveProperty('protobufAvailable');
|
||||||
|
expect(stats.protobufStats!.protobufAvailable).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
tests/Utils/Serialization/index.test.ts
Normal file
17
tests/Utils/Serialization/index.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* 序列化模块集成测试
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 导入所有测试
|
||||||
|
import './ProtobufDecorators.test';
|
||||||
|
import './ProtobufSerializer.test';
|
||||||
|
import './SnapshotManagerIntegration.test';
|
||||||
|
import './Performance.test';
|
||||||
|
|
||||||
|
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
|
||||||
|
describe('序列化模块集成测试', () => {
|
||||||
|
it('应该包含所有序列化测试', () => {
|
||||||
|
// 这个测试确保模块正确加载
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
2
thirdparty/BehaviourTree-ai
vendored
2
thirdparty/BehaviourTree-ai
vendored
Submodule thirdparty/BehaviourTree-ai updated: 98aba30ec1...429961ddf7
1
thirdparty/ecs-astar
vendored
Submodule
1
thirdparty/ecs-astar
vendored
Submodule
Submodule thirdparty/ecs-astar added at 878bc297ac
1
thirdparty/mvvm-ui-framework
vendored
1
thirdparty/mvvm-ui-framework
vendored
Submodule thirdparty/mvvm-ui-framework deleted from e7044b9a7a
Reference in New Issue
Block a user