From 8cfba4a166d9898924520d92e1408b633933cd24 Mon Sep 17 00:00:00 2001
From: YHH <359807859@qq.com>
Date: Wed, 6 Aug 2025 17:04:02 +0800
Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9Eprotobuf=E4=BE=9D=E8=B5=96?=
=?UTF-8?q?=EF=BC=88=E4=B8=BA=E7=BD=91=E7=BB=9C=E5=92=8C=E5=BA=8F=E5=88=97?=
=?UTF-8?q?=E5=8C=96=E5=81=9A=E5=87=86=E5=A4=87=EF=BC=89=20=E6=9B=B4?=
=?UTF-8?q?=E6=96=B0readme?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitmodules | 3 +
README.md | 426 ++++-------------
assets/svg/ecs-architecture.svg | 382 +++++++++++++++
assets/svg/soa-vs-aos.svg | 273 +++++++++++
package-lock.json | 105 +++++
package.json | 10 +-
src/Utils/Serialization/ProtobufDecorators.ts | 284 +++++++++++
src/Utils/Serialization/ProtobufSerializer.ts | 371 +++++++++++++++
.../Serialization/StaticProtobufSerializer.ts | 371 +++++++++++++++
src/Utils/Serialization/index.ts | 7 +
src/Utils/Snapshot/SnapshotManager.ts | 110 ++++-
src/Utils/index.ts | 1 +
tests/Utils/Serialization/Performance.test.ts | 441 ++++++++++++++++++
.../Serialization/ProtobufDecorators.test.ts | 278 +++++++++++
.../Serialization/ProtobufSerializer.test.ts | 314 +++++++++++++
.../Serialization/RealPerformance.test.ts | 393 ++++++++++++++++
.../SnapshotManagerIntegration.test.ts | 370 +++++++++++++++
tests/Utils/Serialization/index.test.ts | 17 +
thirdparty/BehaviourTree-ai | 2 +-
thirdparty/ecs-astar | 1 +
thirdparty/mvvm-ui-framework | 1 -
21 files changed, 3816 insertions(+), 344 deletions(-)
create mode 100644 assets/svg/ecs-architecture.svg
create mode 100644 assets/svg/soa-vs-aos.svg
create mode 100644 src/Utils/Serialization/ProtobufDecorators.ts
create mode 100644 src/Utils/Serialization/ProtobufSerializer.ts
create mode 100644 src/Utils/Serialization/StaticProtobufSerializer.ts
create mode 100644 src/Utils/Serialization/index.ts
create mode 100644 tests/Utils/Serialization/Performance.test.ts
create mode 100644 tests/Utils/Serialization/ProtobufDecorators.test.ts
create mode 100644 tests/Utils/Serialization/ProtobufSerializer.test.ts
create mode 100644 tests/Utils/Serialization/RealPerformance.test.ts
create mode 100644 tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
create mode 100644 tests/Utils/Serialization/index.test.ts
create mode 160000 thirdparty/ecs-astar
delete mode 160000 thirdparty/mvvm-ui-framework
diff --git a/.gitmodules b/.gitmodules
index 72013bc6..493d4ec9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -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
diff --git a/README.md b/README.md
index 2dbe6161..4e127e7e 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,42 @@
# ECS Framework
+[](https://git.io/typing-svg)
+
[](https://github.com/esengine/ecs-framework/actions)
[](https://badge.fury.io/js/%40esengine%2Fecs-framework)
+[](https://www.typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
+[](https://github.com/esengine/ecs-framework/stargazers)
TypeScript ECS (Entity-Component-System) 框架,专为游戏开发设计。
-> 🤔 **什么是 ECS?** 不熟悉 ECS 架构?建议先阅读 [ECS 架构基础](docs/concepts-explained.md#ecs-架构基础) 了解核心概念
+## 💡 项目特色
+
+
+
+[](https://store.cocos.com/app/detail/7823)
+[](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
+
+
+
+## ECS 架构原理
+
+
+

+
+
+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#组件索引系统)
+
+

+
-通过建立索引避免线性搜索,将查询复杂度从 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)
\ No newline at end of file
+[MIT](LICENSE)
\ No newline at end of file
diff --git a/assets/svg/ecs-architecture.svg b/assets/svg/ecs-architecture.svg
new file mode 100644
index 00000000..f4653188
--- /dev/null
+++ b/assets/svg/ecs-architecture.svg
@@ -0,0 +1,382 @@
+
\ No newline at end of file
diff --git a/assets/svg/soa-vs-aos.svg b/assets/svg/soa-vs-aos.svg
new file mode 100644
index 00000000..9ffd2f57
--- /dev/null
+++ b/assets/svg/soa-vs-aos.svg
@@ -0,0 +1,273 @@
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index b2698615..ef6d0007 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index ff3b5840..38282734 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/Utils/Serialization/ProtobufDecorators.ts b/src/Utils/Serialization/ProtobufDecorators.ts
new file mode 100644
index 00000000..5121b251
--- /dev/null
+++ b/src/Utils/Serialization/ProtobufDecorators.ts
@@ -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;
+ /** 构造函数 */
+ constructor: any;
+}
+
+/**
+ * Protobuf注册表
+ */
+export class ProtobufRegistry {
+ private static instance: ProtobufRegistry;
+ private components = new Map();
+
+ 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 {
+ 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 (constructor: T) {
+ const componentName = protoName || constructor.name;
+ const registry = ProtobufRegistry.getInstance();
+
+ // 获取字段定义(由ProtoField装饰器设置)
+ const fields = (constructor.prototype._protoFields as Map)
+ || new Map();
+
+ // 注册组件定义
+ 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();
+ }
+
+ // 自动推断类型
+ 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;
+}
\ No newline at end of file
diff --git a/src/Utils/Serialization/ProtobufSerializer.ts b/src/Utils/Serialization/ProtobufSerializer.ts
new file mode 100644
index 00000000..2673e281
--- /dev/null
+++ b/src/Utils/Serialization/ProtobufSerializer.ts
@@ -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 {
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/Serialization/StaticProtobufSerializer.ts b/src/Utils/Serialization/StaticProtobufSerializer.ts
new file mode 100644
index 00000000..16c44dff
--- /dev/null
+++ b/src/Utils/Serialization/StaticProtobufSerializer.ts
@@ -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 {
+ 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 {
+ 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;
+ }
+}
\ No newline at end of file
diff --git a/src/Utils/Serialization/index.ts b/src/Utils/Serialization/index.ts
new file mode 100644
index 00000000..ef5a7a3e
--- /dev/null
+++ b/src/Utils/Serialization/index.ts
@@ -0,0 +1,7 @@
+/**
+ * 序列化模块导出
+ */
+
+export * from './ProtobufDecorators';
+export * from './ProtobufSerializer';
+export * from './StaticProtobufSerializer';
\ No newline at end of file
diff --git a/src/Utils/Snapshot/SnapshotManager.ts b/src/Utils/Snapshot/SnapshotManager.ts
index a7f1f5cc..e4837889 100644
--- a/src/Utils/Snapshot/SnapshotManager.ts
+++ b/src/Utils/Snapshot/SnapshotManager.ts
@@ -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);
}
}
diff --git a/src/Utils/index.ts b/src/Utils/index.ts
index a3eab8b2..ded6a080 100644
--- a/src/Utils/index.ts
+++ b/src/Utils/index.ts
@@ -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';
\ No newline at end of file
diff --git a/tests/Utils/Serialization/Performance.test.ts b/tests/Utils/Serialization/Performance.test.ts
new file mode 100644
index 00000000..45c20f6a
--- /dev/null
+++ b/tests/Utils/Serialization/Performance.test.ts
@@ -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
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Utils/Serialization/ProtobufDecorators.test.ts b/tests/Utils/Serialization/ProtobufDecorators.test.ts
new file mode 100644
index 00000000..f7e0742b
--- /dev/null
+++ b/tests/Utils/Serialization/ProtobufDecorators.test.ts
@@ -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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Utils/Serialization/ProtobufSerializer.test.ts b/tests/Utils/Serialization/ProtobufSerializer.test.ts
new file mode 100644
index 00000000..9a57550b
--- /dev/null
+++ b/tests/Utils/Serialization/ProtobufSerializer.test.ts
@@ -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();
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Utils/Serialization/RealPerformance.test.ts b/tests/Utils/Serialization/RealPerformance.test.ts
new file mode 100644
index 00000000..557e2a87
--- /dev/null
+++ b/tests/Utils/Serialization/RealPerformance.test.ts
@@ -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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Utils/Serialization/SnapshotManagerIntegration.test.ts b/tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
new file mode 100644
index 00000000..4a6bdb60
--- /dev/null
+++ b/tests/Utils/Serialization/SnapshotManagerIntegration.test.ts
@@ -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);
+ });
+ });
+});
\ No newline at end of file
diff --git a/tests/Utils/Serialization/index.test.ts b/tests/Utils/Serialization/index.test.ts
new file mode 100644
index 00000000..e580aff5
--- /dev/null
+++ b/tests/Utils/Serialization/index.test.ts
@@ -0,0 +1,17 @@
+/**
+ * 序列化模块集成测试
+ */
+
+// 导入所有测试
+import './ProtobufDecorators.test';
+import './ProtobufSerializer.test';
+import './SnapshotManagerIntegration.test';
+import './Performance.test';
+
+// 这个文件确保所有序列化相关的测试都被包含在测试套件中
+describe('序列化模块集成测试', () => {
+ it('应该包含所有序列化测试', () => {
+ // 这个测试确保模块正确加载
+ expect(true).toBe(true);
+ });
+});
\ No newline at end of file
diff --git a/thirdparty/BehaviourTree-ai b/thirdparty/BehaviourTree-ai
index 98aba30e..429961dd 160000
--- a/thirdparty/BehaviourTree-ai
+++ b/thirdparty/BehaviourTree-ai
@@ -1 +1 @@
-Subproject commit 98aba30ec1b2aaa2ac668718da4bec3a8aa9d41e
+Subproject commit 429961ddf754bedd5f9a68299577520488a903c8
diff --git a/thirdparty/ecs-astar b/thirdparty/ecs-astar
new file mode 160000
index 00000000..878bc297
--- /dev/null
+++ b/thirdparty/ecs-astar
@@ -0,0 +1 @@
+Subproject commit 878bc297acd0c0c8ab9684b4e7f5f1e05a00f885
diff --git a/thirdparty/mvvm-ui-framework b/thirdparty/mvvm-ui-framework
deleted file mode 160000
index e7044b9a..00000000
--- a/thirdparty/mvvm-ui-framework
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit e7044b9a7a8eaef76643d03459107eea88a3b5c1