新增protobuf依赖(为网络和序列化做准备)

更新readme
This commit is contained in:
YHH
2025-08-06 17:04:02 +08:00
parent 51e6bba2a7
commit 8cfba4a166
21 changed files with 3816 additions and 344 deletions

3
.gitmodules vendored
View File

@@ -28,3 +28,6 @@
[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"]
path = extensions/cocos/cocos-ecs/extensions/utilityai_designer
url = https://github.com/esengine/utilityai_designer.git
[submodule "thirdparty/ecs-astar"]
path = thirdparty/ecs-astar
url = https://github.com/esengine/ecs-astar.git

426
README.md
View File

@@ -1,24 +1,42 @@
# ECS Framework
[![Typing SVG](https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=22&pause=1000&color=F75C7E&center=true&vCenter=true&width=435&lines=TypeScript+ECS+Framework;高性能游戏开发框架;支持+Cocos+Creator+%26+Laya)](https://git.io/typing-svg)
[![CI](https://github.com/esengine/ecs-framework/workflows/CI/badge.svg)](https://github.com/esengine/ecs-framework/actions)
[![npm version](https://badge.fury.io/js/%40esengine%2Fecs-framework.svg)](https://badge.fury.io/js/%40esengine%2Fecs-framework)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![GitHub stars](https://img.shields.io/github/stars/esengine/ecs-framework?style=social)](https://github.com/esengine/ecs-framework/stargazers)
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
> 🤔 **什么是 ECS** 不熟悉 ECS 架构?建议先阅读 [ECS 架构基础](docs/concepts-explained.md#ecs-架构基础) 了解核心概念
## 💡 项目特色
<div align="center">
[![Cocos Store](https://img.shields.io/badge/Cocos_Store-专业插件-FF6B35?style=flat&logo=cocos&logoColor=white)](https://store.cocos.com/app/detail/7823)
[![QQ群](https://img.shields.io/badge/QQ群-框架交流-1EAEDB?style=flat&logo=tencentqq&logoColor=white)](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 支持** - 强类型检查和代码提示
- 📡 **[类型安全事件系统](docs/concepts-explained.md#事件系统)** - 事件装饰器和异步事件处理
- 🔍 **[查询系统](docs/concepts-explained.md#实体管理)** - 流式 API 和智能缓存
- **[性能优化](docs/concepts-explained.md#性能优化技术)** - 组件索引、Archetype 系统、脏标记
- 🚀 **[SoA 存储优化](docs/soa-storage-guide.md)** - 大规模实体的向量化批量操作和内存优化
- 🎯 **[实体管理器](docs/concepts-explained.md#实体管理)** - 统一的实体生命周期管理
- 🧰 **调试工具** - 内置性能监控和调试信息
> 📖 **不熟悉这些概念?** 查看我们的 [技术概念详解](docs/concepts-explained.md) 了解它们的作用和应用场景
- **完整的 TypeScript 支持** - 强类型检查和代码提示
- **高效查询系统** - 流式 API 和智能缓存
- **性能优化技术** - 组件索引、Archetype 系统、脏标记
- **事件系统** - 类型安全的事件处理
- **调试工具** - 内置性能监控和 [Cocos Creator 可视化调试插件](https://store.cocos.com/app/detail/7823)
## 安装
@@ -28,41 +46,17 @@ npm install @esengine/ecs-framework
## 快速开始
### 基础设置
### 1. 基础使用
```typescript
import { Core, Scene, Entity, Component, EntitySystem } from '@esengine/ecs-framework';
// 创建核心实例 - 使用配置对象(推荐)
const core = Core.create({
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 core = Core.create({ debug: true });
const scene = new Scene();
Core.scene = scene;
```
### 定义组件
```typescript
// 定义组件
class PositionComponent extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
@@ -75,62 +69,13 @@ class VelocityComponent extends Component {
}
}
class HealthComponent extends Component {
constructor(
public maxHealth: number = 100,
public currentHealth: number = 100
) {
super();
}
}
```
// 创建实体
const entity = scene.createEntity("Player");
entity.addComponent(new PositionComponent(100, 100));
entity.addComponent(new VelocityComponent(5, 0));
### 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 {
constructor() {
super();
}
public process(entities: Entity[]) {
for (const entity of entities) {
const position = entity.getComponent(PositionComponent);
@@ -144,47 +89,15 @@ class MovementSystem extends EntitySystem {
}
}
// 添加系统到场景
scene.addEntityProcessor(new MovementSystem());
```
### 游戏循环
ECS框架需要在游戏引擎的更新循环中调用
```typescript
// 统一的API传入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
import { EntityManager } from '@esengine/ecs-framework';
@@ -196,43 +109,10 @@ const results = entityManager
.query()
.withAll(PositionComponent, VelocityComponent)
.withNone(HealthComponent)
.withTag(1)
.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
import { EventHandler, ECSEventType } from '@esengine/ecs-framework';
@@ -242,65 +122,65 @@ class GameSystem {
onEntityDestroyed(data: EntityDestroyedEventData) {
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
// 使用Scene的查询系统进行组件索引
const querySystem = scene.querySystem;
import { EnableSoA, Float32, Int32 } from '@esengine/ecs-framework';
// 查询具有特定组件的实体
const entitiesWithPosition = querySystem.queryAll(PositionComponent).entities;
const entitiesWithVelocity = querySystem.queryAll(VelocityComponent).entities;
// 性能统计
const stats = querySystem.getStats();
console.log('查询效率:', stats.hitRate);
@EnableSoA
class OptimizedTransformComponent extends Component {
@Float32 public x: number = 0;
@Float32 public y: number = 0;
@Float32 public rotation: number = 0;
}
```
**索引类型选择:**
- **哈希索引** - 适合稳定的、大量的组件(如位置、生命值)
- **位图索引** - 适合频繁变化的组件如Buff、状态
**性能优势**
- 🚀 **缓存友好** - 连续内存访问缓存命中率提升85%
- **批量处理** - 同类型数据处理速度提升2-3倍
- 🔄 **热切换** - 开发期AoS便于调试生产期SoA提升性能
- 🎯 **自动优化** - `@EnableSoA`装饰器自动转换存储结构
> 📋 详细选择指南参见 [索引类型选择指南](docs/concepts-explained.md#索引类型选择指南)
## 平台集成
### [Archetype 系统](docs/concepts-explained.md#archetype-系统)
将具有相同组件组合的实体分组,减少查询时的组件检查开销。
### Cocos Creator
```typescript
// 使用查询系统的Archetype功能
const querySystem = scene.querySystem;
// 查询统计
const stats = querySystem.getStats();
console.log('缓存命中率:', stats.hitRate);
update(deltaTime: number) {
Core.update(deltaTime);
}
```
### [脏标记系统](docs/concepts-explained.md#脏标记系统)
追踪数据变化,只处理发生改变的实体,避免不必要的计算。
**专用调试插件**
- 🔧 [ECS 可视化调试插件](https://store.cocos.com/app/detail/7823) - 提供完整的可视化调试界面
- 📊 实体查看器、组件编辑器、系统监控
- 📈 性能分析和实时数据监控
### Laya 引擎
```typescript
// 脏标记通过组件系统自动管理
// 组件变化时会自动标记为脏数据
// 查询系统会自动处理脏标记优化
const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityComponent);
Laya.timer.frameLoop(1, this, () => {
Core.update(Laya.timer.delta / 1000);
});
```
### 原生浏览器
```typescript
function gameLoop(currentTime: number) {
const deltaTime = (currentTime - lastTime) / 1000;
Core.update(deltaTime);
requestAnimationFrame(gameLoop);
}
```
> 💡 **不确定何时使用这些优化?** 查看 [性能优化建议](docs/concepts-explained.md#性能建议) 了解适用场景
## API 参考
@@ -308,9 +188,9 @@ const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityCom
| 类 | 描述 |
|---|---|
| `Core` | 框架核心管理 |
| `Scene` | 场景容器,管理实体和系统 |
| `Entity` | 实体对象,包含组件集合 |
| `Core` | 框架核心管理 |
| `Scene` | 场景容器 |
| `Entity` | 实体对象 |
| `Component` | 组件基类 |
| `EntitySystem` | 系统基类 |
| `EntityManager` | 实体管理器 |
@@ -318,132 +198,25 @@ const movingEntities = scene.querySystem.queryAll(PositionComponent, VelocityCom
### 查询 API
```typescript
entityManager
.query()
.withAll(...components) // 包含所有指定组件
.withAny(...components) // 包含任意指定组件
.withNone(...components) // 包含指定组件
.withTag(tag) // 包含指定标签
.withoutTag(tag) // 不包含指定标签
entityManager.query()
.withAll(...components) // 包含所有组件
.withAny(...components) // 包含任意组件
.withNone(...components) // 包含组件
.withTag(tag) // 包含标签
.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/beginner-tutorials.md)** - 完整学习路径,从零开始 ⭐ **强烈推荐**
- **[🚀 快速入门](docs/getting-started.md)** - 详细的入门教程包含Laya/Cocos/Node.js集成指南 ⭐ **平台集成必读**
- 💡 **Cocos Creator用户特别提示**:我们提供[专用调试插件](https://store.cocos.com/app/detail/7823)支持可视化ECS调试
- [🧠 技术概念详解](docs/concepts-explained.md) - 通俗易懂的技术概念解释 ⭐ **推荐新手阅读**
- [🎯 位掩码使用指南](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
});
```
- [快速入门](docs/getting-started.md) - 详细教程和平台集成
- [技术概念](docs/concepts-explained.md) - ECS 架构和框架特性
- [组件设计](docs/component-design-guide.md) - 组件设计最佳实践
- [性能优化](docs/performance-optimization.md) - 性能优化技术
- [API 参考](docs/core-concepts.md) - 完整 API 文档
## 扩展库
- [路径寻找](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
## 社区
@@ -451,15 +224,6 @@ console.log('查询统计:', {
- QQ 群:[ecs游戏框架交流](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
- 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)

View 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
View 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
View File

@@ -11,8 +11,13 @@
"dependencies": {
"@types/multer": "^1.4.13",
"@types/ws": "^8.18.1",
"protobufjs": "^7.5.3",
"reflect-metadata": "^0.2.2",
"ws": "^8.18.2"
},
"bin": {
"ecs-proto": "bin/ecs-proto"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
@@ -1007,6 +1012,70 @@
"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": {
"version": "28.0.3",
"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==",
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -4508,6 +4583,30 @@
"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": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -4566,6 +4665,12 @@
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",

View File

@@ -4,6 +4,9 @@
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
"main": "bin/index.js",
"types": "bin/index.d.ts",
"bin": {
"ecs-proto": "./bin/ecs-proto"
},
"files": [
"bin/**/*",
"README.md",
@@ -36,7 +39,10 @@
"test:performance": "jest --config jest.performance.config.js",
"test:coverage": "jest --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",
"license": "MIT",
@@ -64,6 +70,8 @@
"dependencies": {
"@types/multer": "^1.4.13",
"@types/ws": "^8.18.1",
"protobufjs": "^7.5.3",
"reflect-metadata": "^0.2.2",
"ws": "^8.18.2"
}
}

View 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;
}

View 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;
}
}
}

View 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;
}
}

View File

@@ -0,0 +1,7 @@
/**
* 序列化模块导出
*/
export * from './ProtobufDecorators';
export * from './ProtobufSerializer';
export * from './StaticProtobufSerializer';

View File

@@ -1,11 +1,14 @@
import { Entity } from '../../ECS/Entity';
import { Component } from '../../ECS/Component';
import { ISnapshotable, SceneSnapshot, EntitySnapshot, ComponentSnapshot, SnapshotConfig } from './ISnapshotable';
import { ProtobufSerializer, SerializedData } from '../Serialization/ProtobufSerializer';
import { isProtoSerializable } from '../Serialization/ProtobufDecorators';
/**
* 快照管理器
*
* 负责创建和管理ECS系统的快照支持完整快照和增量快照
* 现在支持protobuf和JSON混合序列化
*/
export class SnapshotManager {
/** 默认快照配置 */
@@ -28,7 +31,15 @@ export class SnapshotManager {
/** 最大缓存数量 */
private maxCacheSize: number = 10;
/** Protobuf序列化器 */
private protobufSerializer: ProtobufSerializer;
/**
* 构造函数
*/
constructor() {
this.protobufSerializer = ProtobufSerializer.getInstance();
}
/**
* 创建场景快照
@@ -281,10 +292,32 @@ export class SnapshotManager {
*/
public getCacheStats(): {
snapshotCacheSize: number;
protobufStats?: {
registeredComponents: number;
protobufAvailable: boolean;
};
} {
return {
const stats: any = {
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 {
if (!this.isComponentSnapshotable(component)) {
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;
if (this.hasSerializeMethod(component)) {
@@ -329,18 +392,18 @@ export class SnapshotManager {
data = (component as any).serialize();
} catch (error) {
console.warn(`[SnapshotManager] 组件序列化失败: ${component.constructor.name}`, error);
return null;
data = this.defaultSerializeComponent(component);
}
} else {
data = this.defaultSerializeComponent(component);
}
const jsonString = JSON.stringify(data);
return {
type: component.constructor.name,
id: component.id,
type: 'json',
componentType: component.constructor.name,
data: data,
enabled: component.enabled,
config: this.getComponentSnapshotConfig(component)
size: new Blob([jsonString]).size
};
}
@@ -476,6 +539,8 @@ export class SnapshotManager {
/**
* 从快照恢复组件
*
* 现在支持protobuf和JSON混合反序列化
*/
private restoreComponentFromSnapshot(entity: Entity, componentSnapshot: ComponentSnapshot): void {
// 查找现有组件
@@ -491,15 +556,40 @@ export class SnapshotManager {
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)) {
try {
(component as any).deserialize(componentSnapshot.data);
(component as any).deserialize(data);
} catch (error) {
console.warn(`[SnapshotManager] 组件 ${componentSnapshot.type} 反序列化失败:`, error);
console.warn(`[SnapshotManager] 组件 ${component.constructor.name} 反序列化失败:`, error);
}
} else {
// 使用默认反序列化
this.defaultDeserializeComponent(component, componentSnapshot.data);
this.defaultDeserializeComponent(component, data);
}
}

View File

@@ -3,5 +3,6 @@ export * from './Pool';
export * from './Emitter';
export * from './GlobalManager';
export * from './PerformanceMonitor';
export * from './Serialization';
export { Time } from './Time';
export * from './Debug';

View 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
});
});
});

View 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);
});
});
});

View 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();
});
});
});

View 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);
});
});
});

View 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);
});
});
});

View File

@@ -0,0 +1,17 @@
/**
* 序列化模块集成测试
*/
// 导入所有测试
import './ProtobufDecorators.test';
import './ProtobufSerializer.test';
import './SnapshotManagerIntegration.test';
import './Performance.test';
// 这个文件确保所有序列化相关的测试都被包含在测试套件中
describe('序列化模块集成测试', () => {
it('应该包含所有序列化测试', () => {
// 这个测试确保模块正确加载
expect(true).toBe(true);
});
});

1
thirdparty/ecs-astar vendored Submodule

Submodule thirdparty/ecs-astar added at 878bc297ac