Compare commits

..

22 Commits

Author SHA1 Message Date
YHH
bf14b59a28 空查询应该返回所有实体 2025-10-10 11:49:06 +08:00
YHH
0a0f64510f 更新测试用例 2025-10-10 10:58:52 +08:00
YHH
9445c735c3 对象池内存管理优化 2025-10-10 10:16:44 +08:00
YHH
7339e7ecec 新增scenemanager,重构core类减少多世界造成的性能压力 2025-10-09 23:33:11 +08:00
YHH
79f7c89e23 修复再不同环境下buffer兼容性问题 2025-10-09 17:44:15 +08:00
YHH
e724e5a1ba 更新demo 2025-10-09 17:43:46 +08:00
YHH
fdaa94a61d v2.1.52 2025-10-09 17:26:07 +08:00
YHH
6af0074c36 导航增加序列化章节 2025-10-09 17:18:14 +08:00
YHH
97a69fed09 增量序列化支持二进制 2025-10-09 17:14:18 +08:00
YHH
959879440d 更新序列化文档 2025-10-09 14:18:43 +08:00
YHH
fd1bbb0e00 新增增量序列化 2025-10-09 12:30:04 +08:00
YHH
072e68cf43 修复序列化ci测试 2025-10-08 20:58:07 +08:00
YHH
610232e6b0 core库demo更新 2025-10-08 20:52:31 +08:00
YHH
69c46f32eb 支持二进制序列化 2025-10-08 20:42:55 +08:00
YHH
06b3f92007 场景自定义序列化支持 2025-10-08 18:34:15 +08:00
YHH
c631290049 对query/entity进行安全类型扩展 2025-10-08 13:13:23 +08:00
YHH
f41c1a3ca3 冗余测试合并 2025-10-08 12:04:13 +08:00
YHH
bd6ba84087 Merge pull request #72 from 0MirageTank0/master
优化掩码数据结构,新增BitMaskHashMap类型,支持无限数量原型
2025-10-05 09:13:30 +08:00
MirageTank
1512409eb3 优化位掩码工具的输出格式
- 十六进制不再输出无意义的前导0符号
- 修正部分测试单元检测逻辑
2025-10-04 13:16:51 +08:00
MirageTank
bcb5feeb1c 实现高性能 BitMaskHashMap 并优化ArchetypeSystem
- 引入 BitMaskHashMap 类,使用双层 MurmurHash3 哈希算法提升查找性能
- 替换 ArchetypeSystem 中原有的嵌套 Map 结构为 BitMaskHashMap,支持任意数量的原型
- 验证在十万级连续键值下无哈希冲突,确保生产环境可用性
2025-10-04 10:26:19 +08:00
MirageTank
da8b7cf601 重构位掩码数据结构,修复部分方法未考虑扩展位的问题
- 所有操作均考虑扩展位、扩展长度不一致的使用场景,无感扩容掩码位
- 使用定长数组存储高低位,遍历友好,为高效哈希计算提供结构支持
- 补充相应单元测试,覆盖所有方法及分支
2025-10-03 16:55:07 +08:00
YHH
316527c459 更新实体文档(components为只读属性) 2025-10-01 00:15:19 +08:00
83 changed files with 12328 additions and 5055 deletions

View File

@@ -74,6 +74,7 @@ export default defineConfig({
]
},
{ text: '场景管理 (Scene)', link: '/guide/scene' },
{ text: '序列化系统 (Serialization)', link: '/guide/serialization' },
{ text: '事件系统 (Event)', link: '/guide/event-system' },
{ text: '时间和定时器 (Time)', link: '/guide/time-and-timers' },
{ text: '日志系统 (Logger)', link: '/guide/logging' }

View File

@@ -83,8 +83,8 @@ if (player.hasComponent(Position)) {
console.log("玩家有位置组件");
}
// 获取所有组件实例(直接访问 components 属性)
const allComponents = player.components; // Component[]
// 获取所有组件实例(只读属性)
const allComponents = player.components; // readonly Component[]
// 获取指定类型的所有组件(支持同类型多组件)
const allHealthComponents = player.getComponents(Health); // Health[]

View File

@@ -264,22 +264,17 @@ player.addComponent(new Velocity(50, 30)); // 每秒移动 50 像素x方向
player.addComponent(new Sprite("player.png", 64, 64));
```
## World 概念
## 场景管理
World 是 Scene 的容器,用于管理多个独立的游戏世界。这种设计特别适用于
- 多人游戏房间(每个房间一个 World
- 不同的游戏模式
- 独立的模拟环境
### 基本用法
Core 内置了场景管理功能,使用非常简单
```typescript
import { World, Scene } from '@esengine/ecs-framework'
import { Core, Scene } from '@esengine/ecs-framework';
// 创建游戏房间的World
const roomWorld = new World({ name: 'Room_001' });
// 初始化Core
Core.create({ debug: true });
// 在World中创建多个Scene
// 创建并设置场景
class GameScene extends Scene {
initialize(): void {
this.name = "GamePlay";
@@ -288,78 +283,106 @@ class GameScene extends Scene {
}
}
class UIScene extends Scene {
initialize(): void {
this.name = "UI";
// UI相关系统
}
const gameScene = new GameScene();
Core.setScene(gameScene);
// 游戏循环(自动更新场景)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 自动更新全局服务和场景
}
// 添加Scene到World
const gameScene = roomWorld.createScene('game', new GameScene());
const uiScene = roomWorld.createScene('ui', new UIScene());
// 切换场景
Core.loadScene(new MenuScene()); // 延迟切换(下一帧)
Core.setScene(new GameScene()); // 立即切换
// 激活Scene
roomWorld.setSceneActive('game', true);
roomWorld.setSceneActive('ui', true);
// 访问当前场景
const currentScene = Core.scene;
// 启动World
roomWorld.start();
// 使用流式API
const player = Core.ecsAPI?.createEntity('Player')
.addComponent(Position, 100, 100)
.addComponent(Velocity, 50, 0);
```
### World 生命周期
### 高级:使用 WorldManager 管理多世界
World 提供了完整的生命周期管理
- `start()`: 启动 World 和所有全局系统
- `updateGlobalSystems()`: 更新全局系统(由 Core.update() 调用)
- `updateScenes()`: 更新所有激活的 Scene由 Core.update() 调用)
- `stop()`: 停止 World
- `destroy()`: 销毁 World 和所有资源
仅适用于复杂的服务器端应用MMO游戏服务器、游戏房间系统等
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// 初始化Core
Core.create({ debug: true });
// 创建世界管理器(手动管理)
const worldManager = new WorldManager();
// 创建多个独立的游戏世界
const room1 = worldManager.createWorld('room_001');
const room2 = worldManager.createWorld('room_002');
// 在每个世界中创建场景
const gameScene1 = room1.createScene('game', new GameScene());
const gameScene2 = room2.createScene('game', new GameScene());
// 激活场景
room1.setSceneActive('game', true);
room2.setSceneActive('game', true);
// 游戏循环(需要手动更新世界)
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 更新全局服务
worldManager.updateAll(); // 手动更新所有世界
}
```
## 与游戏引擎集成
### Laya 引擎集成
```typescript
import { Stage } from "laya/display/Stage"
import { Stat } from "laya/utils/Stat"
import { Laya } from "Laya"
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
// 初始化 Laya
Laya.init(800, 600).then(() => {
// 初始化 ECS
const core = Core.create(true)
// 设置场景...
Core.create(true);
Core.setScene(new GameScene());
// 启动游戏循环
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000 // 转换为秒
Core.update(deltaTime)
})
})
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // 自动更新全局服务和场景
});
});
```
### Cocos Creator 集成
```typescript
import { Component, _decorator } from 'cc'
import { Component, _decorator } from 'cc';
import { Core } from '@esengine/ecs-framework';
const { ccclass } = _decorator
const { ccclass } = _decorator;
@ccclass('ECSGameManager')
export class ECSGameManager extends Component {
onLoad() {
// 初始化 ECS
const core = Core.create(true)
// 设置场景...
Core.create(true);
Core.setScene(new GameScene());
}
update(deltaTime: number) {
// 更新 ECS
Core.update(deltaTime)
// 自动更新全局服务和场景
Core.update(deltaTime);
}
onDestroy() {
// 清理资源
Core.destroy();
}
}
```
@@ -378,7 +401,7 @@ export class ECSGameManager extends Component {
确保:
1. 系统已添加到场景:`this.addSystem(system)` (在 Scene 的 initialize 方法中)
2. 场景已设置为当前场景`Core.setScene(scene)`
2. 场景已设置:`Core.setScene(scene)`
3. 游戏循环在调用:`Core.update(deltaTime)`
### 如何调试 ECS 应用?

View File

@@ -19,6 +19,9 @@
### [事件系统 (Event)](./event-system.md)
掌握类型安全的事件系统,实现组件间通信和系统协作。
### [序列化系统 (Serialization)](./serialization.md)
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
### [时间和定时器 (Time)](./time-and-timers.md)
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。

View File

@@ -289,12 +289,20 @@ class StatsScene extends Scene {
## 场景集成到框架
场景可以通过两种方式运行
ECS Framework 提供了灵活的场景管理架构,适用于不同规模的应用
### 1. 简单的单场景应用
### 1. 使用 SceneManager推荐大多数应用
适用于 95% 的游戏应用(单人游戏、简单多人游戏、移动游戏等):
```typescript
import { Core } from '@esengine/ecs-framework';
import { Core, SceneManager } from '@esengine/ecs-framework';
// 初始化Core全局服务
Core.create({ debug: true });
// 创建场景管理器
const sceneManager = new SceneManager();
// 创建游戏场景
class GameScene extends Scene {
@@ -305,21 +313,52 @@ class GameScene extends Scene {
}
}
// 启动游戏
Core.create();
// 设置场景
const gameScene = new GameScene();
Core.setScene(gameScene);
sceneManager.setScene(gameScene);
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 更新全局服务
sceneManager.update(); // 更新当前场景
}
```
### 2. 复杂的多场景应用
### 2. 场景切换
SceneManager 支持流畅的场景切换:
```typescript
import { WorldManager } from '@esengine/ecs-framework';
// 立即切换场景
const menuScene = new MenuScene();
sceneManager.setScene(menuScene);
// 获取WorldManager实例
const worldManager = WorldManager.getInstance();
// 延迟切换场景(在下一帧切换)
const gameScene = new GameScene();
sceneManager.startSceneTransition(gameScene, false);
// 创建World
// 访问当前场景
const currentScene = sceneManager.currentScene;
// 访问 ECS API
const ecsAPI = sceneManager.ecsAPI;
const entity = ecsAPI?.createEntity('player');
```
### 3. 使用 WorldManager高级用例
适用于需要完全隔离的多世界应用MMO服务器、游戏房间系统等
```typescript
import { Core, WorldManager } from '@esengine/ecs-framework';
// 初始化Core全局服务
Core.create({ debug: true });
// 创建世界管理器
const worldManager = new WorldManager();
// 创建多个独立的游戏世界
const gameWorld = worldManager.createWorld('game', {
name: 'MainGame',
maxScenes: 5
@@ -331,6 +370,12 @@ const gameScene = gameWorld.createScene('game', new GameScene());
// 激活场景
gameWorld.setSceneActive('menu', true);
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 更新全局服务
worldManager.updateAll(); // 更新所有世界
}
```
## 多场景管理
@@ -376,21 +421,43 @@ class GameWorld extends World {
}
```
## 与 World 的关系
## 架构层次
Scene 的运行架构层次
ECS Framework 的架构层次清晰,职责分明
```typescript
// Core -> WorldManager -> World -> Scene -> EntitySystem -> Entity -> Component
// 架构层次:
// Core (全局服务) → SceneManager (场景管理) → Scene → EntitySystem → Entity → Component
// 或
// Core (全局服务) → WorldManager (世界管理) → World → Scene → EntitySystem → Entity → Component
// 1. 简单应用Core直接管理单个Scene
Core.setScene(new GameScene());
// 1. 推荐:使用 SceneManager 管理单场景/场景切换
import { Core, SceneManager } from '@esengine/ecs-framework';
// 2. 复杂应用WorldManager管理多个World每个World管理多个Scene
const worldManager = WorldManager.getInstance();
Core.create({ debug: true });
const sceneManager = new SceneManager();
sceneManager.setScene(new GameScene());
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 全局服务
sceneManager.update(); // 场景更新
}
// 2. 高级:使用 WorldManager 管理多世界
import { Core, WorldManager } from '@esengine/ecs-framework';
Core.create({ debug: true });
const worldManager = new WorldManager();
const world = worldManager.createWorld('gameWorld');
const scene = world.createScene('mainScene', new GameScene());
world.setSceneActive('mainScene', true);
// 游戏循环
function gameLoop(deltaTime: number) {
Core.update(deltaTime); // 全局服务
worldManager.updateAll(); // 所有世界更新
}
```
## 最佳实践

810
docs/guide/serialization.md Normal file
View File

@@ -0,0 +1,810 @@
# 序列化系统
序列化系统提供了完整的场景、实体和组件数据持久化方案,支持全量序列化和增量序列化两种模式,适用于游戏存档、网络同步、场景编辑器、时间回溯等场景。
## 基本概念
序列化系统分为两个层次:
- **全量序列化**:序列化完整的场景状态,包括所有实体、组件和场景数据
- **增量序列化**:只序列化相对于基础快照的变更部分,大幅减少数据量
### 支持的数据格式
- **JSON格式**:人类可读,便于调试和编辑
- **Binary格式**使用MessagePack体积更小性能更高
## 全量序列化
### 基础用法
#### 1. 标记可序列化组件
使用 `@Serializable``@Serialize` 装饰器标记需要序列化的组件和字段:
```typescript
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
public name: string = '';
@Serialize()
public level: number = 1;
@Serialize()
public experience: number = 0;
@Serialize()
public position: { x: number; y: number } = { x: 0, y: 0 };
// 不使用 @Serialize() 的字段不会被序列化
private tempData: any = null;
}
```
#### 2. 序列化场景
```typescript
// JSON格式序列化
const jsonData = scene.serialize({
format: 'json',
pretty: true // 美化输出
});
// 保存到本地存储
localStorage.setItem('gameSave', jsonData);
// Binary格式序列化更小的体积
const binaryData = scene.serialize({
format: 'binary'
});
// 保存为文件Node.js环境
fs.writeFileSync('save.bin', binaryData);
```
#### 3. 反序列化场景
```typescript
// 从JSON恢复
const saveData = localStorage.getItem('gameSave');
if (saveData) {
scene.deserialize(saveData, {
strategy: 'replace' // 替换当前场景内容
});
}
// 从Binary恢复
const binaryData = fs.readFileSync('save.bin');
scene.deserialize(binaryData, {
strategy: 'merge' // 合并到现有场景
});
```
### 序列化选项
#### SerializationOptions
```typescript
interface SceneSerializationOptions {
// 指定要序列化的组件类型(可选)
components?: ComponentType[];
// 序列化格式:'json' 或 'binary'
format?: 'json' | 'binary';
// JSON美化输出
pretty?: boolean;
// 包含元数据
includeMetadata?: boolean;
}
```
示例:
```typescript
// 只序列化特定组件类型
const saveData = scene.serialize({
format: 'json',
components: [PlayerComponent, InventoryComponent],
pretty: true,
includeMetadata: true
});
```
#### DeserializationOptions
```typescript
interface SceneDeserializationOptions {
// 反序列化策略
strategy?: 'merge' | 'replace';
// 组件类型注册表(可选,默认使用全局注册表)
componentRegistry?: Map<string, ComponentType>;
}
```
### 高级装饰器
#### 字段序列化选项
```typescript
@ECSComponent('Advanced')
@Serializable({ version: 1 })
class AdvancedComponent extends Component {
// 使用别名
@Serialize({ alias: 'playerName' })
public name: string = '';
// 自定义序列化器
@Serialize({
serializer: (value: Date) => value.toISOString(),
deserializer: (value: string) => new Date(value)
})
public createdAt: Date = new Date();
// 忽略序列化
@IgnoreSerialization()
public cachedData: any = null;
}
```
#### 集合类型序列化
```typescript
@ECSComponent('Collections')
@Serializable({ version: 1 })
class CollectionsComponent extends Component {
// Map序列化
@SerializeAsMap()
public inventory: Map<string, number> = new Map();
// Set序列化
@SerializeAsSet()
public acquiredSkills: Set<string> = new Set();
constructor() {
super();
this.inventory.set('gold', 100);
this.inventory.set('silver', 50);
this.acquiredSkills.add('attack');
this.acquiredSkills.add('defense');
}
}
```
### 场景自定义数据
除了实体和组件,还可以序列化场景级别的配置数据:
```typescript
// 设置场景数据
scene.sceneData.set('weather', 'rainy');
scene.sceneData.set('difficulty', 'hard');
scene.sceneData.set('checkpoint', { x: 100, y: 200 });
// 序列化时会自动包含场景数据
const saveData = scene.serialize({ format: 'json' });
// 反序列化后场景数据会恢复
scene.deserialize(saveData);
console.log(scene.sceneData.get('weather')); // 'rainy'
```
## 增量序列化
增量序列化只保存场景的变更部分,适用于网络同步、撤销/重做、时间回溯等需要频繁保存状态的场景。
### 基础用法
#### 1. 创建基础快照
```typescript
// 在需要开始记录变更前创建基础快照
scene.createIncrementalSnapshot();
```
#### 2. 修改场景
```typescript
// 添加实体
const enemy = scene.createEntity('Enemy');
enemy.addComponent(new PositionComponent(100, 200));
enemy.addComponent(new HealthComponent(50));
// 修改组件
const player = scene.findEntity('Player');
const pos = player.getComponent(PositionComponent);
pos.x = 300;
pos.y = 400;
// 删除组件
player.removeComponentByType(BuffComponent);
// 删除实体
const oldEntity = scene.findEntity('ToDelete');
oldEntity.destroy();
// 修改场景数据
scene.sceneData.set('score', 1000);
```
#### 3. 获取增量变更
```typescript
// 获取相对于基础快照的所有变更
const incremental = scene.serializeIncremental();
// 查看变更统计
const stats = IncrementalSerializer.getIncrementalStats(incremental);
console.log('总变更数:', stats.totalChanges);
console.log('新增实体:', stats.addedEntities);
console.log('删除实体:', stats.removedEntities);
console.log('新增组件:', stats.addedComponents);
console.log('更新组件:', stats.updatedComponents);
```
#### 4. 序列化增量数据
```typescript
// JSON格式默认
const jsonData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'json'
});
// 二进制格式(更小的体积,更高性能)
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'binary'
});
// 美化JSON输出便于调试
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, {
format: 'json',
pretty: true
});
// 发送或保存
socket.send(binaryData); // 网络传输使用二进制
localStorage.setItem('changes', jsonData); // 本地存储可用JSON
```
#### 5. 应用增量变更
```typescript
// 在另一个场景应用变更
const otherScene = new Scene();
// 直接应用增量对象
otherScene.applyIncremental(incremental);
// 从JSON字符串应用
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
otherScene.applyIncremental(jsonData);
// 从二进制Buffer应用
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
otherScene.applyIncremental(binaryData);
```
### 增量快照管理
#### 更新快照基准
在应用增量变更后,可以更新快照基准:
```typescript
// 创建初始快照
scene.createIncrementalSnapshot();
// 第一次修改
entity.addComponent(new VelocityComponent(5, 0));
const incremental1 = scene.serializeIncremental();
// 更新基准(将当前状态设为新的基准)
scene.updateIncrementalSnapshot();
// 第二次修改(增量将基于更新后的基准)
entity.getComponent(VelocityComponent).dx = 10;
const incremental2 = scene.serializeIncremental();
```
#### 清除快照
```typescript
// 释放快照占用的内存
scene.clearIncrementalSnapshot();
// 检查是否有快照
if (scene.hasIncrementalSnapshot()) {
console.log('存在增量快照');
}
```
### 增量序列化选项
```typescript
interface IncrementalSerializationOptions {
// 是否进行组件数据的深度对比
// 默认true设为false可提升性能但可能漏掉组件内部字段变更
deepComponentComparison?: boolean;
// 是否跟踪场景数据变更
// 默认true
trackSceneData?: boolean;
// 是否压缩快照使用JSON序列化
// 默认false
compressSnapshot?: boolean;
// 序列化格式
// 'json': JSON格式可读性好方便调试
// 'binary': MessagePack二进制格式体积小性能高
// 默认 'json'
format?: 'json' | 'binary';
// 是否美化JSON输出仅在format='json'时有效)
// 默认false
pretty?: boolean;
}
// 使用选项
scene.createIncrementalSnapshot({
deepComponentComparison: true,
trackSceneData: true
});
```
### 增量数据结构
增量快照包含以下变更类型:
```typescript
interface IncrementalSnapshot {
version: number; // 快照版本号
timestamp: number; // 时间戳
sceneName: string; // 场景名称
baseVersion: number; // 基础版本号
entityChanges: EntityChange[]; // 实体变更
componentChanges: ComponentChange[]; // 组件变更
sceneDataChanges: SceneDataChange[]; // 场景数据变更
}
// 变更操作类型
enum ChangeOperation {
EntityAdded = 'entity_added',
EntityRemoved = 'entity_removed',
EntityUpdated = 'entity_updated',
ComponentAdded = 'component_added',
ComponentRemoved = 'component_removed',
ComponentUpdated = 'component_updated',
SceneDataUpdated = 'scene_data_updated'
}
```
## 版本迁移
当组件结构发生变化时,版本迁移系统可以自动升级旧版本的存档数据。
### 注册迁移函数
```typescript
import { VersionMigrationManager } from '@esengine/ecs-framework';
// 假设 PlayerComponent v1 有 hp 字段
// v2 改为 health 和 maxHealth 字段
// 注册从版本1到版本2的迁移
VersionMigrationManager.registerComponentMigration(
'Player',
1, // 从版本
2, // 到版本
(data) => {
// 迁移逻辑
const newData = {
...data,
health: data.hp,
maxHealth: data.hp,
};
delete newData.hp;
return newData;
}
);
```
### 使用迁移构建器
```typescript
import { MigrationBuilder } from '@esengine/ecs-framework';
new MigrationBuilder()
.forComponent('Player')
.fromVersionToVersion(2, 3)
.migrate((data) => {
// 从版本2迁移到版本3
data.experience = data.exp || 0;
delete data.exp;
return data;
});
```
### 场景级迁移
```typescript
// 注册场景级迁移
VersionMigrationManager.registerSceneMigration(
1, // 从版本
2, // 到版本
(scene) => {
// 迁移场景结构
scene.metadata = {
...scene.metadata,
migratedFrom: 1
};
return scene;
}
);
```
### 检查迁移路径
```typescript
// 检查是否可以迁移
const canMigrate = VersionMigrationManager.canMigrateComponent(
'Player',
1, // 从版本
3 // 到版本
);
if (canMigrate) {
// 可以安全迁移
scene.deserialize(oldSaveData);
}
// 获取迁移路径
const path = VersionMigrationManager.getComponentMigrationPath('Player');
console.log('可用迁移版本:', path); // [1, 2, 3]
```
## 使用场景
### 游戏存档系统
```typescript
class SaveSystem {
private static SAVE_KEY = 'game_save';
// 保存游戏
public static saveGame(scene: Scene): void {
const saveData = scene.serialize({
format: 'json',
pretty: false
});
localStorage.setItem(this.SAVE_KEY, saveData);
console.log('游戏已保存');
}
// 加载游戏
public static loadGame(scene: Scene): boolean {
const saveData = localStorage.getItem(this.SAVE_KEY);
if (saveData) {
scene.deserialize(saveData, {
strategy: 'replace'
});
console.log('游戏已加载');
return true;
}
return false;
}
// 检查是否有存档
public static hasSave(): boolean {
return localStorage.getItem(this.SAVE_KEY) !== null;
}
}
```
### 网络同步
```typescript
class NetworkSync {
private baseSnapshot?: any;
private syncInterval: number = 100; // 100ms同步一次
constructor(private scene: Scene, private socket: WebSocket) {
this.setupSync();
}
private setupSync(): void {
// 创建基础快照
this.scene.createIncrementalSnapshot();
// 定期发送增量
setInterval(() => {
this.sendIncremental();
}, this.syncInterval);
// 接收远程增量
this.socket.onmessage = (event) => {
this.receiveIncremental(event.data);
};
}
private sendIncremental(): void {
const incremental = this.scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
// 只在有变更时发送
if (stats.totalChanges > 0) {
// 使用二进制格式减少网络传输量
const binaryData = IncrementalSerializer.serializeIncremental(incremental, {
format: 'binary'
});
this.socket.send(binaryData);
// 更新基准
this.scene.updateIncrementalSnapshot();
}
}
private receiveIncremental(data: ArrayBuffer): void {
// 直接应用二进制数据
const buffer = Buffer.from(data);
this.scene.applyIncremental(buffer);
}
}
```
### 撤销/重做系统
```typescript
class UndoRedoSystem {
private history: IncrementalSnapshot[] = [];
private currentIndex: number = -1;
private maxHistory: number = 50;
constructor(private scene: Scene) {
// 创建初始快照
this.scene.createIncrementalSnapshot();
this.saveState('Initial');
}
// 保存当前状态
public saveState(label: string): void {
const incremental = this.scene.serializeIncremental();
// 删除当前位置之后的历史
this.history = this.history.slice(0, this.currentIndex + 1);
// 添加新状态
this.history.push(incremental);
this.currentIndex++;
// 限制历史记录数量
if (this.history.length > this.maxHistory) {
this.history.shift();
this.currentIndex--;
}
// 更新快照基准
this.scene.updateIncrementalSnapshot();
}
// 撤销
public undo(): boolean {
if (this.currentIndex > 0) {
this.currentIndex--;
const incremental = this.history[this.currentIndex];
this.scene.applyIncremental(incremental);
return true;
}
return false;
}
// 重做
public redo(): boolean {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const incremental = this.history[this.currentIndex];
this.scene.applyIncremental(incremental);
return true;
}
return false;
}
public canUndo(): boolean {
return this.currentIndex > 0;
}
public canRedo(): boolean {
return this.currentIndex < this.history.length - 1;
}
}
```
### 关卡编辑器
```typescript
class LevelEditor {
// 导出关卡
public exportLevel(scene: Scene, filename: string): void {
const levelData = scene.serialize({
format: 'json',
pretty: true,
includeMetadata: true
});
// 浏览器环境
const blob = new Blob([levelData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// 导入关卡
public importLevel(scene: Scene, fileContent: string): void {
scene.deserialize(fileContent, {
strategy: 'replace'
});
}
// 验证关卡数据
public validateLevel(saveData: string): boolean {
const validation = SceneSerializer.validate(saveData);
if (!validation.valid) {
console.error('关卡数据无效:', validation.errors);
return false;
}
return true;
}
// 获取关卡信息(不完全反序列化)
public getLevelInfo(saveData: string): any {
const info = SceneSerializer.getInfo(saveData);
return info;
}
}
```
## 性能优化建议
### 1. 选择合适的格式
- **开发阶段**使用JSON格式便于调试和查看
- **生产环境**使用Binary格式减少30-50%的数据大小
### 2. 按需序列化
```typescript
// 只序列化需要持久化的组件
const saveData = scene.serialize({
format: 'binary',
components: [PlayerComponent, InventoryComponent, QuestComponent]
});
```
### 3. 增量序列化优化
```typescript
// 对于高频同步,关闭深度对比以提升性能
scene.createIncrementalSnapshot({
deepComponentComparison: false // 只检测组件的添加/删除
});
```
### 4. 批量操作
```typescript
// 批量修改后再序列化
scene.entities.buffer.forEach(entity => {
// 批量修改
});
// 一次性序列化所有变更
const incremental = scene.serializeIncremental();
```
## 最佳实践
### 1. 明确序列化字段
```typescript
// 明确标记需要序列化的字段
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
public name: string = '';
@Serialize()
public level: number = 1;
// 运行时数据不序列化
private _cachedSprite: any = null;
}
```
### 2. 使用版本控制
```typescript
// 为组件指定版本
@Serializable({ version: 2 })
class PlayerComponent extends Component {
// 版本2的字段
}
// 注册迁移函数确保兼容性
VersionMigrationManager.registerComponentMigration('Player', 1, 2, migrateV1ToV2);
```
### 3. 避免循环引用
```typescript
// 不要在组件中直接引用其他实体
@ECSComponent('Follower')
@Serializable({ version: 1 })
class FollowerComponent extends Component {
// 存储实体ID而不是实体引用
@Serialize()
public targetId: number = 0;
// 通过场景查找目标实体
public getTarget(scene: Scene): Entity | null {
return scene.entities.findEntityById(this.targetId);
}
}
```
### 4. 压缩大数据
```typescript
// 对于大型数据结构,使用自定义序列化
@ECSComponent('LargeData')
@Serializable({ version: 1 })
class LargeDataComponent extends Component {
@Serialize({
serializer: (data: LargeObject) => compressData(data),
deserializer: (data: CompressedData) => decompressData(data)
})
public data: LargeObject;
}
```
## API参考
### 全量序列化API
- [`Scene.serialize()`](/api/classes/Scene#serialize) - 序列化场景
- [`Scene.deserialize()`](/api/classes/Scene#deserialize) - 反序列化场景
- [`SceneSerializer`](/api/classes/SceneSerializer) - 场景序列化器
- [`ComponentSerializer`](/api/classes/ComponentSerializer) - 组件序列化器
### 增量序列化API
- [`Scene.createIncrementalSnapshot()`](/api/classes/Scene#createincrementalsnapshot) - 创建基础快照
- [`Scene.serializeIncremental()`](/api/classes/Scene#serializeincremental) - 获取增量变更
- [`Scene.applyIncremental()`](/api/classes/Scene#applyincremental) - 应用增量变更支持IncrementalSnapshot对象、JSON字符串或二进制Buffer
- [`Scene.updateIncrementalSnapshot()`](/api/classes/Scene#updateincrementalsnapshot) - 更新快照基准
- [`Scene.clearIncrementalSnapshot()`](/api/classes/Scene#clearincrementalsnapshot) - 清除快照
- [`Scene.hasIncrementalSnapshot()`](/api/classes/Scene#hasincrementalsnapshot) - 检查是否有快照
- [`IncrementalSerializer`](/api/classes/IncrementalSerializer) - 增量序列化器
- [`IncrementalSnapshot`](/api/interfaces/IncrementalSnapshot) - 增量快照接口
- [`IncrementalSerializationOptions`](/api/interfaces/IncrementalSerializationOptions) - 增量序列化选项
- [`IncrementalSerializationFormat`](/api/type-aliases/IncrementalSerializationFormat) - 序列化格式类型
### 版本迁移API
- [`VersionMigrationManager`](/api/classes/VersionMigrationManager) - 版本迁移管理器
- `VersionMigrationManager.registerComponentMigration()` - 注册组件迁移
- `VersionMigrationManager.registerSceneMigration()` - 注册场景迁移
- `VersionMigrationManager.canMigrateComponent()` - 检查是否可以迁移
- `VersionMigrationManager.getComponentMigrationPath()` - 获取迁移路径
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。

View File

@@ -0,0 +1,483 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Core Demos</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #0f0f23;
color: #e0e0e0;
overflow: hidden;
height: 100vh;
}
.app-container {
display: flex;
height: 100vh;
}
/* 侧边栏 */
.sidebar {
width: 280px;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
border-right: 2px solid #0a3d62;
display: flex;
flex-direction: column;
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
}
.sidebar-header {
padding: 30px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text-align: center;
}
.sidebar-header h1 {
font-size: 1.5em;
color: white;
margin-bottom: 8px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.sidebar-header p {
font-size: 0.85em;
color: rgba(255,255,255,0.9);
font-weight: 300;
}
.demo-list {
flex: 1;
overflow-y: auto;
padding: 15px 0;
}
.demo-category {
margin-bottom: 10px;
}
.category-title {
padding: 12px 20px;
color: #8892b0;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.demo-item {
padding: 14px 20px;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid transparent;
display: flex;
align-items: center;
gap: 12px;
}
.demo-item:hover {
background: rgba(102, 126, 234, 0.1);
border-left-color: #667eea;
}
.demo-item.active {
background: rgba(102, 126, 234, 0.2);
border-left-color: #667eea;
}
.demo-icon {
font-size: 20px;
width: 24px;
text-align: center;
}
.demo-info {
flex: 1;
}
.demo-name {
font-size: 14px;
font-weight: 500;
color: #ccd6f6;
margin-bottom: 2px;
}
.demo-item.active .demo-name {
color: #667eea;
font-weight: 600;
}
.demo-desc {
font-size: 11px;
color: #8892b0;
line-height: 1.3;
}
.sidebar-footer {
padding: 20px;
border-top: 1px solid rgba(255,255,255,0.1);
text-align: center;
}
.github-link {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: rgba(102, 126, 234, 0.2);
color: #667eea;
text-decoration: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.github-link:hover {
background: rgba(102, 126, 234, 0.3);
transform: translateY(-2px);
}
/* 主内容区 */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-header {
padding: 25px 40px;
background: #1a1a2e;
border-bottom: 2px solid #0a3d62;
}
.content-header h2 {
font-size: 2em;
color: #ccd6f6;
margin-bottom: 8px;
}
.content-header p {
color: #8892b0;
font-size: 14px;
line-height: 1.6;
}
.demo-canvas-container {
flex: 1;
position: relative;
overflow: hidden;
background: #0a0a15;
}
#demoCanvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* 控制面板 */
.control-panel {
position: absolute;
top: 20px;
right: 20px;
width: 320px;
background: rgba(26, 26, 46, 0.95);
border-radius: 12px;
border: 1px solid rgba(102, 126, 234, 0.3);
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
backdrop-filter: blur(10px);
max-height: calc(100% - 40px);
overflow-y: auto;
}
.control-panel-header {
padding: 15px 20px;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
border-bottom: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 12px 12px 0 0;
}
.control-panel-header h3 {
color: #667eea;
font-size: 16px;
font-weight: 600;
}
.control-panel-content {
padding: 20px;
}
.control-section {
margin-bottom: 20px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section h4 {
color: #8892b0;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 12px;
font-weight: 600;
}
.button-group {
display: flex;
flex-direction: column;
gap: 8px;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button.secondary {
background: rgba(102, 126, 234, 0.2);
color: #667eea;
}
button.secondary:hover {
background: rgba(102, 126, 234, 0.3);
}
button.danger {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
button.success {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.input-group {
margin-bottom: 12px;
}
.input-group label {
display: block;
color: #8892b0;
font-size: 12px;
margin-bottom: 6px;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 6px;
color: #ccd6f6;
font-size: 13px;
transition: all 0.2s;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
background: rgba(255,255,255,0.08);
}
/* 统计信息 */
.stats-panel {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-top: 15px;
}
.stat-item {
background: rgba(255,255,255,0.03);
padding: 12px;
border-radius: 6px;
border: 1px solid rgba(255,255,255,0.05);
}
.stat-label {
color: #8892b0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
}
.stat-value {
color: #667eea;
font-size: 20px;
font-weight: 700;
}
/* Toast通知 */
.toast {
position: fixed;
bottom: 30px;
right: 30px;
background: rgba(26, 26, 46, 0.98);
border: 1px solid #667eea;
border-radius: 8px;
padding: 15px 20px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
transform: translateY(150%);
transition: transform 0.3s ease;
z-index: 1000;
min-width: 280px;
}
.toast.show {
transform: translateY(0);
}
.toast-content {
display: flex;
align-items: center;
gap: 12px;
}
.toast-icon {
font-size: 20px;
}
.toast-message {
color: #ccd6f6;
font-size: 14px;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(255,255,255,0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(102, 126, 234, 0.5);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(102, 126, 234, 0.7);
}
/* 加载动画 */
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
.loading-spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-text {
margin-top: 15px;
color: #8892b0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 侧边栏 -->
<div class="sidebar">
<div class="sidebar-header">
<h1>🎮 ECS Core Demos</h1>
<p>交互式演示集合</p>
</div>
<div class="demo-list" id="demoList">
<!-- Demo列表将通过JS动态生成 -->
</div>
<div class="sidebar-footer">
<a href="https://github.com/esengine/ecs-framework" target="_blank" class="github-link">
<span></span>
<span>GitHub</span>
</a>
</div>
</div>
<!-- 主内容区 -->
<div class="main-content">
<div class="content-header">
<h2 id="demoTitle">选择一个演示开始</h2>
<p id="demoDescription">从左侧菜单选择一个演示查看效果</p>
</div>
<div class="demo-canvas-container">
<canvas id="demoCanvas"></canvas>
<!-- 控制面板 -->
<div class="control-panel" id="controlPanel" style="display: none;">
<div class="control-panel-header">
<h3>控制面板</h3>
</div>
<div class="control-panel-content" id="controlPanelContent">
<!-- 控制内容将由各个demo动态生成 -->
</div>
</div>
<!-- 加载动画 -->
<div class="loading" id="loading" style="display: none;">
<div class="loading-spinner"></div>
<div class="loading-text">加载中...</div>
</div>
</div>
</div>
</div>
<!-- Toast通知 -->
<div class="toast" id="toast">
<div class="toast-content">
<span class="toast-icon"></span>
<span class="toast-message" id="toastMessage"></span>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -1,11 +1,11 @@
{
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
@@ -17,8 +17,11 @@
},
"../../packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.49",
"version": "2.1.51",
"license": "MIT",
"dependencies": {
"msgpack-lite": "^0.1.26"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
@@ -29,6 +32,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -553,9 +557,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -1,7 +1,7 @@
{
"name": "ecs-worker-system-demo",
"name": "ecs-core-demos",
"version": "1.0.0",
"description": "ECS Framework Worker System Demo",
"description": "ECS Framework Core Demos - Multiple Interactive Examples",
"main": "src/main.ts",
"scripts": {
"dev": "vite",
@@ -15,4 +15,4 @@
"dependencies": {
"@esengine/ecs-framework": "file:../../packages/core"
}
}
}

View File

@@ -0,0 +1,99 @@
import { Scene, Core } from '@esengine/ecs-framework';
export interface DemoInfo {
id: string;
name: string;
description: string;
category: string;
icon: string;
}
export abstract class DemoBase {
protected scene: Scene;
protected canvas: HTMLCanvasElement;
protected ctx: CanvasRenderingContext2D;
protected controlPanel: HTMLElement;
protected isRunning: boolean = false;
protected animationFrameId: number | null = null;
protected lastTime: number = 0;
constructor(canvas: HTMLCanvasElement, controlPanel: HTMLElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.controlPanel = controlPanel;
this.scene = new Scene({ name: this.getInfo().name });
// 设置canvas大小
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
}
abstract getInfo(): DemoInfo;
abstract setup(): void;
abstract createControls(): void;
protected resizeCanvas() {
const rect = this.canvas.getBoundingClientRect();
this.canvas.width = rect.width;
this.canvas.height = rect.height;
}
public start() {
if (this.isRunning) return;
this.isRunning = true;
this.lastTime = performance.now();
// 设置当前场景到Core
Core.setScene(this.scene);
this.scene.begin();
this.loop();
}
public stop() {
this.isRunning = false;
if (this.animationFrameId !== null) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
public destroy() {
this.stop();
this.scene.end();
}
protected loop = () => {
if (!this.isRunning) return;
// 计算deltaTime
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
Core.update(deltaTime);
// 渲染
this.render();
// 继续循环
this.animationFrameId = requestAnimationFrame(this.loop);
}
protected abstract render(): void;
protected showToast(message: string, icon: string = '✅') {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = icon;
toastMessage.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
}

View File

@@ -0,0 +1,468 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Matcher,
Serializable,
Serialize,
IncrementalSerializer
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('IncDemo_Position')
@Serializable({ version: 1, typeId: 'IncDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('IncDemo_Velocity')
@Serializable({ version: 1, typeId: 'IncDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('IncDemo_Renderable')
@Serializable({ version: 1, typeId: 'IncDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(PositionComponent, VelocityComponent));
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
pos.x += vel.vx;
pos.y += vel.vy;
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(PositionComponent, RenderableComponent));
this.canvas = canvas;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get canvas context');
this.ctx = ctx;
}
protected override process(entities: readonly Entity[]): void {
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.fillStyle = 'white';
this.ctx.font = '10px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(entity.name, pos.x, pos.y - render.radius - 5);
}
}
}
export class IncrementalSerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private incrementalHistory: any[] = [];
private autoSnapshotInterval: number | null = null;
getInfo(): DemoInfo {
return {
id: 'incremental-serialization',
name: '增量序列化',
description: '演示增量序列化功能,只保存场景变更而非完整状态,适用于网络同步和回放系统',
category: '核心功能',
icon: '🔄'
};
}
setup() {
// 创建控制面板
this.createControls();
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建基础快照
this.scene.createIncrementalSnapshot();
this.addToHistory('Initial State');
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
// 设置场景数据
this.scene.sceneData.set('gameTime', 0);
this.scene.sceneData.set('score', 0);
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
private addToHistory(label: string) {
const incremental = this.scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
// 计算JSON和二进制格式的大小
const jsonSize = IncrementalSerializer.getIncrementalSize(incremental, 'json');
const binarySize = IncrementalSerializer.getIncrementalSize(incremental, 'binary');
this.incrementalHistory.push({
label,
incremental,
stats,
timestamp: Date.now(),
jsonSize,
binarySize
});
this.scene.updateIncrementalSnapshot();
this.updateHistoryPanel();
this.updateStats();
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="removeEntity" class="danger">删除最后一个实体</button>
<button id="modifyEntity" class="secondary">修改实体数据</button>
</div>
</div>
<div class="control-section">
<h4>增量快照</h4>
<div class="button-group">
<button id="captureSnapshot" class="success">捕获当前状态</button>
<button id="clearHistory" class="danger">清空历史</button>
</div>
<div style="margin-top: 10px;">
<label>
<input type="checkbox" id="autoSnapshot">
自动快照每2秒
</label>
</div>
</div>
<div class="control-section">
<h4>场景数据控制</h4>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="0" step="1">
</div>
<div class="input-group">
<label>分数</label>
<input type="number" id="score" value="0" step="10">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">历史记录</div>
<div class="stat-value" id="historyCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
<div class="stat-item">
<div class="stat-label">总变更数</div>
<div class="stat-value" id="totalChanges">0</div>
</div>
</div>
<div class="control-section">
<h4>增量历史 <small style="color: #8892b0;">(点击快照查看详情)</small></h4>
<div style="max-height: 300px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px;" id="historyPanel">
暂无历史记录
</div>
</div>
<div class="control-section">
<h4>快照详情</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0;" id="snapshotDetails">
点击历史记录查看详情...
</div>
</div>
`;
this.bindEvents();
this.updateStats();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.addToHistory('添加实体');
this.showToast('添加了一个随机实体');
});
document.getElementById('removeEntity')!.addEventListener('click', () => {
const entities = this.scene.entities.buffer;
if (entities.length > 1) {
const lastEntity = entities[entities.length - 1];
lastEntity.destroy();
this.addToHistory('删除实体');
this.showToast('删除了最后一个实体');
} else {
this.showToast('至少保留一个实体', '⚠️');
}
});
document.getElementById('modifyEntity')!.addEventListener('click', () => {
const entities = this.scene.entities.buffer;
if (entities.length > 0) {
const randomEntity = entities[Math.floor(Math.random() * entities.length)];
const pos = randomEntity.getComponent(PositionComponent);
if (pos) {
pos.x = Math.random() * this.canvas.width;
pos.y = Math.random() * this.canvas.height;
}
this.addToHistory('修改实体位置');
this.showToast(`修改了 ${randomEntity.name} 的位置`);
}
});
document.getElementById('captureSnapshot')!.addEventListener('click', () => {
this.addToHistory('手动快照');
this.showToast('已捕获当前状态', '📸');
});
document.getElementById('clearHistory')!.addEventListener('click', () => {
this.incrementalHistory = [];
this.scene.createIncrementalSnapshot();
this.addToHistory('清空后重新开始');
this.showToast('历史记录已清空');
});
document.getElementById('autoSnapshot')!.addEventListener('change', (e) => {
const checkbox = e.target as HTMLInputElement;
if (checkbox.checked) {
this.autoSnapshotInterval = window.setInterval(() => {
this.addToHistory('自动快照');
}, 2000);
this.showToast('自动快照已启用', '⏱️');
} else {
if (this.autoSnapshotInterval !== null) {
clearInterval(this.autoSnapshotInterval);
this.autoSnapshotInterval = null;
}
this.showToast('自动快照已禁用');
}
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const gameTime = parseInt((document.getElementById('gameTime') as HTMLInputElement).value);
const score = parseInt((document.getElementById('score') as HTMLInputElement).value);
this.scene.sceneData.set('gameTime', gameTime);
this.scene.sceneData.set('score', score);
this.addToHistory('更新场景数据');
this.showToast('场景数据已更新');
});
}
private updateHistoryPanel() {
const panel = document.getElementById('historyPanel')!;
if (this.incrementalHistory.length === 0) {
panel.innerHTML = '暂无历史记录';
return;
}
panel.innerHTML = this.incrementalHistory.map((item, index) => {
const isLatest = index === this.incrementalHistory.length - 1;
const time = new Date(item.timestamp).toLocaleTimeString();
return `
<div class="history-item" data-index="${index}" style="
padding: 8px;
margin: 4px 0;
background: ${isLatest ? 'rgba(74, 158, 255, 0.2)' : 'rgba(136, 146, 176, 0.1)'};
border-left: 3px solid ${isLatest ? '#4a9eff' : '#8892b0'};
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<strong>${item.label}</strong>
${isLatest ? '<span style="color: #4a9eff; margin-left: 8px;">●</span>' : ''}
</div>
<small style="color: #8892b0;">${time}</small>
</div>
<div style="font-size: 11px; color: #8892b0; margin-top: 4px;">
实体: +${item.stats.addedEntities} -${item.stats.removedEntities} ~${item.stats.updatedEntities} |
组件: +${item.stats.addedComponents} -${item.stats.removedComponents} ~${item.stats.updatedComponents}
</div>
<div style="font-size: 11px; color: #8892b0; margin-top: 2px;">
JSON: ${this.formatBytes(item.jsonSize)} |
Binary: ${this.formatBytes(item.binarySize)} |
<span style="color: #4ecdc4;">节省: ${((1 - item.binarySize / item.jsonSize) * 100).toFixed(1)}%</span>
</div>
</div>
`;
}).join('');
// 绑定点击事件
panel.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.getAttribute('data-index')!);
this.showSnapshotDetails(index);
});
});
// 自动滚动到底部
panel.scrollTop = panel.scrollHeight;
}
private showSnapshotDetails(index: number) {
const item = this.incrementalHistory[index];
const detailsPanel = document.getElementById('snapshotDetails')!;
const compressionRatio = ((1 - item.binarySize / item.jsonSize) * 100).toFixed(1);
const details = {
版本: item.incremental.version,
基础版本: item.incremental.baseVersion,
时间戳: new Date(item.incremental.timestamp).toLocaleString(),
场景名称: item.incremental.sceneName,
: {
JSON大小: this.formatBytes(item.jsonSize),
二进制大小: this.formatBytes(item.binarySize),
: `${compressionRatio}%`,
节省字节: this.formatBytes(item.jsonSize - item.binarySize)
},
统计: item.stats,
实体变更: item.incremental.entityChanges.map((c: any) => ({
操作: c.operation,
实体ID: c.entityId,
实体名称: c.entityName
})),
组件变更: item.incremental.componentChanges.map((c: any) => ({
操作: c.operation,
实体ID: c.entityId,
组件类型: c.componentType
})),
场景数据变更: item.incremental.sceneDataChanges.map((c: any) => ({
: c.key,
: c.value,
已删除: c.deleted
}))
};
detailsPanel.textContent = JSON.stringify(details, null, 2);
}
private updateStats() {
document.getElementById('entityCount')!.textContent = this.scene.entities.count.toString();
document.getElementById('historyCount')!.textContent = this.incrementalHistory.length.toString();
if (this.incrementalHistory.length > 0) {
const lastItem = this.incrementalHistory[this.incrementalHistory.length - 1];
document.getElementById('jsonSize')!.textContent = this.formatBytes(lastItem.jsonSize);
document.getElementById('binarySize')!.textContent = this.formatBytes(lastItem.binarySize);
const compressionRatio = ((1 - lastItem.binarySize / lastItem.jsonSize) * 100).toFixed(1);
const ratioElement = document.getElementById('compressionRatio')!;
ratioElement.textContent = `${compressionRatio}%`;
ratioElement.style.color = parseFloat(compressionRatio) > 30 ? '#4ecdc4' : '#ffe66d';
document.getElementById('totalChanges')!.textContent = lastItem.stats.totalChanges.toString();
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
public destroy() {
if (this.autoSnapshotInterval !== null) {
clearInterval(this.autoSnapshotInterval);
}
super.destroy();
}
}

View File

@@ -0,0 +1,386 @@
import { DemoBase, DemoInfo } from './DemoBase';
import {
Component,
ECSComponent,
Entity,
EntitySystem,
Matcher,
Serializable,
Serialize,
SerializeAsMap
} from '@esengine/ecs-framework';
// ===== 组件定义 =====
@ECSComponent('SerDemo_Position')
@Serializable({ version: 1, typeId: 'SerDemo_Position' })
class PositionComponent extends Component {
@Serialize() x: number = 0;
@Serialize() y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('SerDemo_Velocity')
@Serializable({ version: 1, typeId: 'SerDemo_Velocity' })
class VelocityComponent extends Component {
@Serialize() vx: number = 0;
@Serialize() vy: number = 0;
constructor(vx: number = 0, vy: number = 0) {
super();
this.vx = vx;
this.vy = vy;
}
}
@ECSComponent('SerDemo_Renderable')
@Serializable({ version: 1, typeId: 'SerDemo_Renderable' })
class RenderableComponent extends Component {
@Serialize() color: string = '#ffffff';
@Serialize() radius: number = 10;
constructor(color: string = '#ffffff', radius: number = 10) {
super();
this.color = color;
this.radius = radius;
}
}
@ECSComponent('SerDemo_Player')
@Serializable({ version: 1, typeId: 'SerDemo_Player' })
class PlayerComponent extends Component {
@Serialize() name: string = 'Player';
@Serialize() level: number = 1;
@Serialize() health: number = 100;
@SerializeAsMap() inventory: Map<string, number> = new Map();
constructor(name: string = 'Player') {
super();
this.name = name;
}
}
// ===== 系统定义 =====
class MovementSystem extends EntitySystem {
constructor() {
super(Matcher.all(PositionComponent, VelocityComponent));
}
protected override process(entities: readonly Entity[]): void {
for (const entity of entities) {
const [pos, vel] = this.getComponents(entity, PositionComponent, VelocityComponent);
pos.x += vel.vx;
pos.y += vel.vy;
// 边界反弹
if (pos.x < 0 || pos.x > 1200) vel.vx *= -1;
if (pos.y < 0 || pos.y > 600) vel.vy *= -1;
}
}
}
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(PositionComponent, RenderableComponent));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: readonly Entity[]): void {
// 清空画布
this.ctx.fillStyle = '#0a0a15';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// 渲染所有实体
for (const entity of entities) {
const [pos, render] = this.getComponents(entity, PositionComponent, RenderableComponent);
this.ctx.fillStyle = render.color;
this.ctx.beginPath();
this.ctx.arc(pos.x, pos.y, render.radius, 0, Math.PI * 2);
this.ctx.fill();
// 如果是玩家,显示名字
const player = entity.getComponent(PlayerComponent);
if (player) {
this.ctx.fillStyle = 'white';
this.ctx.font = '12px Arial';
this.ctx.textAlign = 'center';
this.ctx.fillText(player.name, pos.x, pos.y - render.radius - 5);
}
}
}
}
export class SerializationDemo extends DemoBase {
private renderSystem!: RenderSystem;
private jsonData: string = '';
private binaryData: Buffer | null = null;
getInfo(): DemoInfo {
return {
id: 'serialization',
name: '场景序列化',
description: '演示场景的序列化和反序列化功能支持JSON和二进制格式',
category: '核心功能',
icon: '💾'
};
}
setup() {
// @ECSComponent装饰器会自动注册组件到ComponentRegistry
// ComponentRegistry会被序列化系统自动使用无需手动注册
// 添加系统
this.renderSystem = new RenderSystem(this.canvas);
this.scene.addEntityProcessor(new MovementSystem());
this.scene.addEntityProcessor(this.renderSystem);
// 创建初始实体
this.createInitialEntities();
// 创建控制面板
this.createControls();
}
private createInitialEntities() {
// 创建玩家
const player = this.scene.createEntity('Player');
player.addComponent(new PositionComponent(600, 300));
player.addComponent(new VelocityComponent(2, 1.5));
player.addComponent(new RenderableComponent('#4a9eff', 15));
const playerComp = new PlayerComponent('Hero');
playerComp.level = 5;
playerComp.health = 100;
playerComp.inventory.set('sword', 1);
playerComp.inventory.set('potion', 5);
player.addComponent(playerComp);
// 创建一些随机实体
for (let i = 0; i < 5; i++) {
this.createRandomEntity();
}
// 设置场景数据
this.scene.sceneData.set('weather', 'sunny');
this.scene.sceneData.set('gameTime', 12.5);
this.scene.sceneData.set('difficulty', 'normal');
}
private createRandomEntity() {
const entity = this.scene.createEntity(`Entity_${Date.now()}`);
entity.addComponent(new PositionComponent(
Math.random() * this.canvas.width,
Math.random() * this.canvas.height
));
entity.addComponent(new VelocityComponent(
(Math.random() - 0.5) * 3,
(Math.random() - 0.5) * 3
));
const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d', '#a8dadc', '#f1faee'];
entity.addComponent(new RenderableComponent(
colors[Math.floor(Math.random() * colors.length)],
5 + Math.random() * 10
));
}
createControls() {
this.controlPanel.innerHTML = `
<div class="control-section">
<h4>实体控制</h4>
<div class="button-group">
<button id="addEntity" class="secondary">添加随机实体</button>
<button id="clearEntities" class="danger">清空所有实体</button>
</div>
</div>
<div class="control-section">
<h4>序列化操作</h4>
<div class="button-group">
<button id="serializeJSON">序列化为JSON</button>
<button id="serializeBinary" class="success">序列化为二进制</button>
<button id="deserialize" class="secondary">反序列化恢复</button>
</div>
</div>
<div class="control-section">
<h4>本地存储</h4>
<div class="button-group">
<button id="saveLocal" class="success">保存到LocalStorage</button>
<button id="loadLocal" class="secondary">从LocalStorage加载</button>
</div>
</div>
<div class="control-section">
<h4>场景数据</h4>
<div class="input-group">
<label>天气</label>
<input type="text" id="weather" value="sunny" placeholder="sunny/rainy/snowy">
</div>
<div class="input-group">
<label>游戏时间</label>
<input type="number" id="gameTime" value="12.5" step="0.1" min="0" max="24">
</div>
<button id="updateSceneData" class="secondary">更新场景数据</button>
</div>
<div class="stats-panel">
<div class="stat-item">
<div class="stat-label">实体数量</div>
<div class="stat-value" id="entityCount">0</div>
</div>
<div class="stat-item">
<div class="stat-label">JSON大小</div>
<div class="stat-value" id="jsonSize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">二进制大小</div>
<div class="stat-value" id="binarySize">0B</div>
</div>
<div class="stat-item">
<div class="stat-label">压缩率</div>
<div class="stat-value" id="compressionRatio">0%</div>
</div>
</div>
<div class="control-section">
<h4>序列化数据预览</h4>
<div style="max-height: 200px; overflow-y: auto; background: rgba(0,0,0,0.3); padding: 10px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #8892b0; word-break: break-all;" id="dataPreview">
点击序列化按钮查看数据...
</div>
</div>
`;
// 绑定事件
this.bindEvents();
}
private bindEvents() {
document.getElementById('addEntity')!.addEventListener('click', () => {
this.createRandomEntity();
this.updateStats();
this.showToast('添加了一个随机实体');
});
document.getElementById('clearEntities')!.addEventListener('click', () => {
this.scene.destroyAllEntities();
this.createInitialEntities();
this.updateStats();
this.showToast('场景已重置');
});
document.getElementById('serializeJSON')!.addEventListener('click', () => {
this.jsonData = this.scene.serialize({ format: 'json', pretty: true }) as string;
this.updateDataPreview(this.jsonData, 'json');
this.updateStats();
this.showToast('已序列化为JSON格式');
});
document.getElementById('serializeBinary')!.addEventListener('click', () => {
this.binaryData = this.scene.serialize({ format: 'binary' }) as Buffer;
const base64 = this.binaryData.toString('base64');
this.updateDataPreview(`Binary Data (Base64):\n${base64.substring(0, 500)}...`, 'binary');
this.updateStats();
this.showToast('已序列化为二进制格式', '🔐');
});
document.getElementById('deserialize')!.addEventListener('click', () => {
const data = this.binaryData || this.jsonData;
if (!data) {
this.showToast('请先执行序列化操作', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('场景已恢复');
});
document.getElementById('saveLocal')!.addEventListener('click', () => {
const jsonData = this.scene.serialize({ format: 'json' }) as string;
localStorage.setItem('ecs_demo_scene', jsonData);
this.showToast('已保存到LocalStorage', '💾');
});
document.getElementById('loadLocal')!.addEventListener('click', () => {
const data = localStorage.getItem('ecs_demo_scene');
if (!data) {
this.showToast('LocalStorage中没有保存的场景', '⚠️');
return;
}
this.scene.deserialize(data, {
strategy: 'replace'
// componentRegistry会自动从ComponentRegistry获取无需手动传入
});
this.updateStats();
this.showToast('已从LocalStorage加载', '📂');
});
document.getElementById('updateSceneData')!.addEventListener('click', () => {
const weather = (document.getElementById('weather') as HTMLInputElement).value;
const gameTime = parseFloat((document.getElementById('gameTime') as HTMLInputElement).value);
this.scene.sceneData.set('weather', weather);
this.scene.sceneData.set('gameTime', gameTime);
this.showToast('场景数据已更新');
});
// 初始更新统计
this.updateStats();
}
private updateDataPreview(data: string, format: string) {
const preview = document.getElementById('dataPreview')!;
if (format === 'json') {
const truncated = data.length > 1000 ? data.substring(0, 1000) + '\n...(truncated)' : data;
preview.textContent = truncated;
} else {
preview.textContent = data;
}
}
private updateStats() {
const entityCount = this.scene.entities.count;
document.getElementById('entityCount')!.textContent = entityCount.toString();
// 计算JSON大小
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
document.getElementById('jsonSize')!.textContent = this.formatBytes(jsonSize);
}
// 计算二进制大小
if (this.binaryData) {
const binarySize = this.binaryData.length;
document.getElementById('binarySize')!.textContent = this.formatBytes(binarySize);
// 计算压缩率
if (this.jsonData) {
const jsonSize = new Blob([this.jsonData]).size;
const ratio = ((1 - binarySize / jsonSize) * 100).toFixed(1);
document.getElementById('compressionRatio')!.textContent = `${ratio}%`;
}
}
}
private formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes}B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
}
protected render() {
// RenderSystem会处理渲染
}
}

View File

@@ -0,0 +1,832 @@
import { DemoBase, DemoInfo } from './DemoBase';
import { Component, ECSComponent, WorkerEntitySystem, EntitySystem, Matcher, Entity, ECSSystem, PlatformManager, Time } from '@esengine/ecs-framework';
import { BrowserAdapter } from '../platform/BrowserAdapter';
// ============ 组件定义 ============
@ECSComponent('WorkerDemo_Position')
class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
@ECSComponent('WorkerDemo_Velocity')
class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
}
@ECSComponent('WorkerDemo_Physics')
class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
@ECSComponent('WorkerDemo_Renderable')
class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
shape: 'circle' | 'square' = 'circle';
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
super();
this.color = color;
this.size = size;
this.shape = shape;
}
}
@ECSComponent('WorkerDemo_Lifetime')
class Lifetime extends Component {
maxAge: number = 5;
currentAge: number = 0;
constructor(maxAge: number = 5) {
super();
this.maxAge = maxAge;
this.currentAge = 0;
}
isDead(): boolean {
return this.currentAge >= this.maxAge;
}
}
// ============ 系统定义 ============
interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig;
constructor(enableWorker: boolean, canvasWidth: number, canvasHeight: number) {
const defaultConfig = {
gravity: 100,
canvasWidth,
canvasHeight,
groundFriction: 0.98
};
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true
}
);
this.physicsConfig = defaultConfig;
}
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable)!;
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable.size
};
}
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || this.physicsConfig;
const result = entities.map(e => ({ ...e }));
for (let i = 0; i < result.length; i++) {
const entity = result[i];
entity.dy += config.gravity * deltaTime;
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
entity.dx *= config.groundFriction;
}
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
if (distance < minDistance && distance > 0) {
const nx = dx / distance;
const ny = dy / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
const relativeVelocityX = ball2.dx - ball1.dx;
const relativeVelocityY = ball2.dy - ball1.dy;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
const energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
if (!entity || !entity.enabled) return;
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
if (!position || !velocity) return;
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
protected getDefaultEntityDataSize(): number {
return 9;
}
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量
const currentEntityCount = Math.floor(offset / 9) + 1;
sharedArray[0] = currentEntityCount;
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
sharedArray[dataOffset + 0] = entityData.id;
sharedArray[dataOffset + 1] = entityData.x;
sharedArray[dataOffset + 2] = entityData.y;
sharedArray[dataOffset + 3] = entityData.dx;
sharedArray[dataOffset + 4] = entityData.dy;
sharedArray[dataOffset + 5] = entityData.mass;
sharedArray[dataOffset + 6] = entityData.bounce;
sharedArray[dataOffset + 7] = entityData.friction;
sharedArray[dataOffset + 8] = entityData.radius;
}
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引9开始存储
const dataOffset = offset + 9;
return {
id: sharedArray[dataOffset + 0],
x: sharedArray[dataOffset + 1],
y: sharedArray[dataOffset + 2],
dx: sharedArray[dataOffset + 3],
dy: sharedArray[dataOffset + 4],
mass: sharedArray[dataOffset + 5],
bounce: sharedArray[dataOffset + 6],
friction: sharedArray[dataOffset + 7],
radius: sharedArray[dataOffset + 8]
};
}
protected getSharedArrayBufferProcessFunction(): any {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
const actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset = i * 9 + 9;
const id = sharedFloatArray[offset + 0];
if (id === 0) continue;
let x = sharedFloatArray[offset + 1];
let y = sharedFloatArray[offset + 2];
let dx = sharedFloatArray[offset + 3];
let dy = sharedFloatArray[offset + 4];
const bounce = sharedFloatArray[offset + 6];
const friction = sharedFloatArray[offset + 7];
const radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞
if (x <= radius) {
x = radius;
dx = -dx * bounce;
} else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
} else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 碰撞检测
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset1 = i * 9 + 9;
const id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0) continue;
let x1 = sharedFloatArray[offset1 + 1];
let y1 = sharedFloatArray[offset1 + 2];
let dx1 = sharedFloatArray[offset1 + 3];
let dy1 = sharedFloatArray[offset1 + 4];
const mass1 = sharedFloatArray[offset1 + 5];
const bounce1 = sharedFloatArray[offset1 + 6];
const radius1 = sharedFloatArray[offset1 + 8];
for (let j = 0; j < actualEntityCount; j++) {
if (i === j) continue;
const offset2 = j * 9 + 9;
const id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0) continue;
const x2 = sharedFloatArray[offset2 + 1];
const y2 = sharedFloatArray[offset2 + 2];
const dx2 = sharedFloatArray[offset2 + 3];
const dy2 = sharedFloatArray[offset2 + 4];
const mass2 = sharedFloatArray[offset2 + 5];
const bounce2 = sharedFloatArray[offset2 + 6];
const radius2 = sharedFloatArray[offset2 + 8];
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const minDistance = radius1 + radius2;
if (distance < minDistance && distance > 0) {
const nx = deltaX / distance;
const ny = deltaY / distance;
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
const relativeVelocityX = dx2 - dx1;
const relativeVelocityY = dy2 - dy1;
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
if (velocityAlongNormal > 0) continue;
const restitution = (bounce1 + bounce2) * 0.5;
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}
@ECSSystem('RenderSystem')
class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override process(entities: readonly Entity[]): void {
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
for (const entity of entities) {
const position = this.requireComponent(entity, Position);
const renderable = this.requireComponent(entity, Renderable);
this.ctx.fillStyle = renderable.color;
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
}
}
}
@ECSSystem('LifetimeSystem')
class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.all(Lifetime));
}
protected override process(entities: readonly Entity[]): void {
const deltaTime = Time.deltaTime;
for (const entity of entities) {
const lifetime = this.requireComponent(entity, Lifetime);
lifetime.currentAge += deltaTime;
if (lifetime.isDead()) {
entity.destroy();
}
}
}
}
// ============ Demo类 ============
export class WorkerSystemDemo extends DemoBase {
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
private currentFPS = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private elements: { [key: string]: HTMLElement } = {};
getInfo(): DemoInfo {
return {
id: 'worker-system',
name: 'Worker System',
description: '演示 ECS 框架中的多线程物理计算能力',
category: '核心功能',
icon: '⚙️'
};
}
setup(): void {
// 注册浏览器平台适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 初始化系统
this.physicsSystem = new PhysicsWorkerSystem(true, this.canvas.width, this.canvas.height);
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
this.scene.addSystem(this.physicsSystem);
this.scene.addSystem(this.lifetimeSystem);
this.scene.addSystem(this.renderSystem);
// 创建控制面板
this.createControls();
// 初始化UI元素引用
this.initializeUIElements();
this.bindEvents();
// 生成初始实体
this.spawnInitialEntities(1000);
}
createControls(): void {
this.controlPanel.innerHTML = `
<div style="background: #2a2a2a; padding: 20px; border-radius: 8px; height: 100%; overflow-y: auto;">
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100"
style="width: 100%; margin-bottom: 5px;">
<span id="entityCountValue" style="color: #fff;">1000</span>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">Worker 设置:</label>
<button id="toggleWorker" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
禁用 Worker
</button>
</div>
<div style="margin-bottom: 15px;">
<button id="spawnParticles" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
生成粒子爆炸
</button>
<button id="clearEntities" style="width: 100%; padding: 8px; margin-bottom: 5px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
清空所有实体
</button>
<button id="resetDemo" style="width: 100%; padding: 8px;
background: #4a9eff; color: white; border: none; border-radius: 4px; cursor: pointer;">
重置演示
</button>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; margin-bottom: 5px; color: #ccc;">物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10"
style="width: 100%; margin-bottom: 5px;">
<label style="color: #ccc;">重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5"
style="width: 100%; margin-top: 10px; margin-bottom: 5px;">
<label style="color: #ccc;">摩擦力: <span id="frictionValue">95%</span></label>
</div>
<div style="background: #1a1a1a; padding: 15px; border-radius: 8px; font-family: monospace; font-size: 12px;">
<h3 style="margin-top: 0; color: #4a9eff;">性能统计</h3>
<div style="margin: 5px 0; color: #ccc;">FPS: <span id="fps" style="color: #4eff4a;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">实体数量: <span id="entityCountStat" style="color: #fff;">0</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker状态: <span id="workerStatus" style="color: #ff4a4a;">未启用</span></div>
<div style="margin: 5px 0; color: #ccc;">Worker负载: <span id="workerLoad" style="color: #fff;">N/A</span></div>
</div>
</div>
`;
}
protected render(): void {
this.frameCount++;
const currentTime = performance.now();
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
this.updateUI();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
}
}
}
private bindEvents(): void {
if (this.elements.entityCount && this.elements.entityCountValue) {
const slider = this.elements.entityCount as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.entityCountValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const count = parseInt(slider.value);
this.spawnInitialEntities(count);
});
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
});
}
if (this.elements.gravity && this.elements.gravityValue) {
const slider = this.elements.gravity as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.gravityValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const gravity = parseInt(slider.value);
this.updateWorkerConfig({ gravity });
});
}
if (this.elements.friction && this.elements.frictionValue) {
const slider = this.elements.friction as HTMLInputElement;
slider.addEventListener('input', () => {
const value = parseInt(slider.value);
this.elements.frictionValue.textContent = `${value}%`;
});
slider.addEventListener('change', () => {
const friction = parseInt(slider.value) / 100;
this.updateWorkerConfig({ friction });
});
}
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.spawnParticleExplosion(centerX, centerY, 100);
});
}
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.clearAllEntities();
});
}
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
this.spawnInitialEntities(1000);
this.updateWorkerConfig({ gravity: 100, friction: 0.95 });
});
}
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.spawnParticleExplosion(x, y, 30);
});
}
private updateUI(): void {
const workerInfo = this.physicsSystem.getWorkerInfo();
if (this.elements.fps) {
this.elements.fps.textContent = this.currentFPS.toString();
}
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = this.scene.entities.count.toString();
}
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.style.color = '#4eff4a';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.style.color = '#ff4a4a';
}
}
if (this.elements.workerLoad) {
const entityCount = this.scene.entities.count;
if (workerInfo.enabled && entityCount > 0) {
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
} else {
this.elements.workerLoad.textContent = 'N/A';
}
}
}
private spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
private createParticle(): void {
const entity = this.scene.createEntity(`Particle_${Date.now()}_${Math.random()}`);
const x = Math.random() * (this.canvas.width - 20) + 10;
const y = Math.random() * (this.canvas.height - 20) + 10;
const dx = (Math.random() - 0.5) * 200;
const dy = (Math.random() - 0.5) * 200;
const mass = Math.random() * 3 + 2;
const bounce = 0.85 + Math.random() * 0.15;
const friction = 0.998 + Math.random() * 0.002;
const colors = [
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44'
];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 6 + 3;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, friction));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(5 + Math.random() * 10));
}
private spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.scene.createEntity(`Explosion_${Date.now()}_${i}`);
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const distance = Math.random() * 30;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
const speed = 100 + Math.random() * 150;
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const mass = 0.5 + Math.random() * 1;
const bounce = 0.8 + Math.random() * 0.2;
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 4 + 2;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, 0.999));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(2 + Math.random() * 3));
}
}
private clearAllEntities(): void {
const entities = [...this.scene.entities.buffer];
for (const entity of entities) {
entity.destroy();
}
}
private toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 保存当前物理配置
const currentConfig = this.physicsSystem.getPhysicsConfig();
this.scene.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled, this.canvas.width, this.canvas.height);
this.physicsSystem.updateOrder = 1;
// 恢复物理配置
this.physicsSystem.updatePhysicsConfig(currentConfig);
this.scene.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
private updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
if (config.gravity !== undefined || config.friction !== undefined) {
const physicsConfig = this.physicsSystem.getPhysicsConfig();
this.physicsSystem.updatePhysicsConfig({
gravity: config.gravity ?? physicsConfig.gravity,
groundFriction: config.friction ?? physicsConfig.groundFriction
});
}
}
}

View File

@@ -0,0 +1,13 @@
import { DemoBase } from './DemoBase';
import { SerializationDemo } from './SerializationDemo';
import { IncrementalSerializationDemo } from './IncrementalSerializationDemo';
import { WorkerSystemDemo } from './WorkerSystemDemo';
export { DemoBase, SerializationDemo, IncrementalSerializationDemo, WorkerSystemDemo };
// Demo注册表
export const DEMO_REGISTRY: typeof DemoBase[] = [
SerializationDemo,
IncrementalSerializationDemo,
WorkerSystemDemo
];

View File

@@ -0,0 +1,171 @@
import { DEMO_REGISTRY, DemoBase } from './demos';
import { Core } from '@esengine/ecs-framework';
class DemoManager {
private demos: Map<string, typeof DemoBase> = new Map();
private currentDemo: DemoBase | null = null;
private canvas: HTMLCanvasElement;
private controlPanel: HTMLElement;
constructor() {
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
this.canvas = document.getElementById('demoCanvas') as HTMLCanvasElement;
this.controlPanel = document.getElementById('controlPanel') as HTMLElement;
// 注册所有demos
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
this.demos.set(info.id, DemoClass);
tempInstance.destroy();
}
// 渲染demo列表
this.renderDemoList();
// 自动加载第一个demo
const firstDemo = DEMO_REGISTRY[0];
if (firstDemo) {
const tempInstance = new firstDemo(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
this.loadDemo(info.id);
}
}
private renderDemoList() {
const demoList = document.getElementById('demoList')!;
// 按分类组织demos
const categories = new Map<string, typeof DemoBase[]>();
for (const DemoClass of DEMO_REGISTRY) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
if (!categories.has(info.category)) {
categories.set(info.category, []);
}
categories.get(info.category)!.push(DemoClass);
}
// 渲染分类和demos
let html = '';
for (const [category, demoClasses] of categories) {
html += `<div class="demo-category">`;
html += `<div class="category-title">${category}</div>`;
for (const DemoClass of demoClasses) {
const tempInstance = new DemoClass(this.canvas, this.controlPanel);
const info = tempInstance.getInfo();
tempInstance.destroy();
html += `
<div class="demo-item" data-demo-id="${info.id}">
<div class="demo-icon">${info.icon}</div>
<div class="demo-info">
<div class="demo-name">${info.name}</div>
<div class="demo-desc">${info.description}</div>
</div>
</div>
`;
}
html += `</div>`;
}
demoList.innerHTML = html;
// 绑定点击事件
demoList.querySelectorAll('.demo-item').forEach(item => {
item.addEventListener('click', () => {
const demoId = item.getAttribute('data-demo-id')!;
this.loadDemo(demoId);
});
});
}
private loadDemo(demoId: string) {
// 停止并销毁当前demo
if (this.currentDemo) {
this.currentDemo.destroy();
this.currentDemo = null;
}
// 显示加载动画
const loading = document.getElementById('loading')!;
loading.style.display = 'block';
// 延迟加载,给用户反馈
setTimeout(() => {
const DemoClass = this.demos.get(demoId);
if (!DemoClass) {
console.error(`Demo ${demoId} not found`);
loading.style.display = 'none';
return;
}
try {
// 创建新demo
this.currentDemo = new DemoClass(this.canvas, this.controlPanel);
const info = this.currentDemo.getInfo();
// 更新页面标题和描述
document.getElementById('demoTitle')!.textContent = info.name;
document.getElementById('demoDescription')!.textContent = info.description;
// 设置demo
this.currentDemo.setup();
// 显示控制面板
this.controlPanel.style.display = 'block';
// 启动demo
this.currentDemo.start();
// 更新菜单选中状态
document.querySelectorAll('.demo-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-demo-id') === demoId) {
item.classList.add('active');
}
});
loading.style.display = 'none';
console.log(`✅ Demo "${info.name}" loaded successfully`);
} catch (error) {
console.error(`Failed to load demo ${demoId}:`, error);
loading.style.display = 'none';
this.showError('加载演示失败:' + (error as Error).message);
}
}, 300);
}
private showError(message: string) {
const toast = document.getElementById('toast')!;
const toastMessage = document.getElementById('toastMessage')!;
const toastIcon = toast.querySelector('.toast-icon')!;
toastIcon.textContent = '❌';
toastMessage.textContent = message;
toast.style.borderColor = '#f5576c';
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
toast.style.borderColor = '#667eea';
}, 3000);
}
}
// 初始化
window.addEventListener('DOMContentLoaded', () => {
new DemoManager();
});

View File

@@ -1,8 +1,9 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -14,12 +15,7 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"references": [{ "path": "./tsconfig.node.json" }]
}
"include": ["src"]
}

View File

@@ -0,0 +1,15 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3003,
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp'
}
},
build: {
target: 'es2020',
outDir: 'dist'
}
});

View File

@@ -1,177 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ECS Framework Worker System Demo</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #1a1a1a;
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
color: #4a9eff;
}
.demo-area {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.canvas-container {
flex: 1;
}
#gameCanvas {
border: 2px solid #4a9eff;
background: #000;
display: block;
}
.controls {
width: 300px;
background: #2a2a2a;
padding: 20px;
border-radius: 8px;
}
.control-group {
margin-bottom: 15px;
}
.control-group label {
display: block;
margin-bottom: 5px;
color: #ccc;
}
.control-group input, .control-group button {
width: 100%;
padding: 8px;
margin-bottom: 5px;
border: 1px solid #555;
background: #3a3a3a;
color: white;
border-radius: 4px;
}
.control-group button {
background: #4a9eff;
cursor: pointer;
transition: background 0.3s;
}
.control-group button:hover {
background: #3a8eef;
}
.control-group button:disabled {
background: #555;
cursor: not-allowed;
}
.stats {
background: #2a2a2a;
padding: 15px;
border-radius: 8px;
font-family: monospace;
font-size: 12px;
}
.stats h3 {
margin-top: 0;
color: #4a9eff;
}
.stat-line {
margin: 5px 0;
}
.worker-enabled {
color: #4eff4a;
}
.worker-disabled {
color: #ff4a4a;
}
.performance-high {
color: #4eff4a;
}
.performance-medium {
color: #ffff4a;
}
.performance-low {
color: #ff4a4a;
}
</style>
</head>
<body>
<div class="container">
<h1>ECS Framework Worker System 演示</h1>
<div class="demo-area">
<div class="canvas-container">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<div class="controls">
<div class="control-group">
<label>实体数量:</label>
<input type="range" id="entityCount" min="100" max="10000" value="1000" step="100">
<span id="entityCountValue">1000</span>
</div>
<div class="control-group">
<label>Worker 设置:</label>
<button id="toggleWorker">禁用 Worker</button>
<button id="toggleSAB">禁用 SharedArrayBuffer</button>
</div>
<div class="control-group">
<button id="spawnParticles">生成粒子系统</button>
<button id="clearEntities">清空所有实体</button>
<button id="resetDemo">重置演示</button>
</div>
<div class="control-group">
<label>物理参数:</label>
<input type="range" id="gravity" min="0" max="500" value="100" step="10">
<label>重力: <span id="gravityValue">100</span></label>
<input type="range" id="friction" min="0" max="100" value="95" step="5">
<label>摩擦力: <span id="frictionValue">95%</span></label>
</div>
</div>
</div>
<div class="stats">
<h3>性能统计</h3>
<div class="stat-line">FPS: <span id="fps">0</span></div>
<div class="stat-line">实体数量: <span id="entityCountStat">0</span></div>
<div class="stat-line">Worker状态: <span id="workerStatus" class="worker-disabled">未启用</span></div>
<div class="stat-line">Worker负载: <span id="workerLoad">N/A</span></div>
<div class="stat-line">运行模式: <span id="sabStatus" class="worker-disabled">同步模式</span></div>
<div class="stat-line">物理系统耗时: <span id="physicsTime">0</span>ms</div>
<div class="stat-line">渲染系统耗时: <span id="renderTime">0</span>ms</div>
<div class="stat-line">总帧时间: <span id="frameTime">0</span>ms</div>
<div class="stat-line">内存使用: <span id="memoryUsage">0</span>MB</div>
</div>
</div>
<script type="module" src="src/main.ts"></script>
</body>
</html>

View File

@@ -1,187 +0,0 @@
import { Scene } from '@esengine/ecs-framework';
import { PhysicsWorkerSystem, RenderSystem, LifetimeSystem } from './systems';
import { Position, Velocity, Physics, Renderable, Lifetime } from './components';
export class GameScene extends Scene {
private canvas: HTMLCanvasElement;
private physicsSystem!: PhysicsWorkerSystem;
private renderSystem!: RenderSystem;
private lifetimeSystem!: LifetimeSystem;
constructor(canvas: HTMLCanvasElement) {
super();
this.canvas = canvas;
}
override initialize(): void {
this.name = "WorkerDemoScene";
// 创建系统
this.physicsSystem = new PhysicsWorkerSystem(true); // 默认启用Worker
this.renderSystem = new RenderSystem(this.canvas);
this.lifetimeSystem = new LifetimeSystem();
// 设置系统执行顺序
this.physicsSystem.updateOrder = 1;
this.lifetimeSystem.updateOrder = 2;
this.renderSystem.updateOrder = 3;
// 添加系统到场景
this.addSystem(this.physicsSystem);
this.addSystem(this.lifetimeSystem);
this.addSystem(this.renderSystem);
}
override onStart(): void {
console.log("Worker演示场景已启动");
this.spawnInitialEntities();
}
override unload(): void {
console.log("Worker演示场景已卸载");
}
/**
* 生成初始实体
*/
public spawnInitialEntities(count: number = 1000): void {
this.clearAllEntities();
for (let i = 0; i < count; i++) {
this.createParticle();
}
}
/**
* 创建一个粒子实体
*/
public createParticle(): void {
const entity = this.createEntity(`Particle_${Date.now()}_${Math.random()}`);
// 随机位置
const x = Math.random() * (this.canvas.width - 20) + 10;
const y = Math.random() * (this.canvas.height - 20) + 10;
// 随机速度
const dx = (Math.random() - 0.5) * 200;
const dy = (Math.random() - 0.5) * 200;
const mass = Math.random() * 3 + 2;
const bounce = 0.85 + Math.random() * 0.15;
const friction = 0.998 + Math.random() * 0.002;
// 随机颜色和大小 - 增加更多颜色提高多样性
const colors = [
'#ff4444', '#44ff44', '#4444ff', '#ffff44', '#ff44ff', '#44ffff', '#ffffff',
'#ff8844', '#88ff44', '#4488ff', '#ff4488', '#88ff88', '#8888ff', '#ffaa44',
'#aaff44', '#44aaff', '#ff44aa', '#aa44ff', '#44ffaa', '#cccccc'
];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 6 + 3;
// 添加组件
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, friction));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(5 + Math.random() * 10)); // 5-15秒生命周期
}
/**
* 生成粒子爆发效果
*/
public spawnParticleExplosion(centerX: number, centerY: number, count: number = 50): void {
for (let i = 0; i < count; i++) {
const entity = this.createEntity(`Explosion_${Date.now()}_${i}`);
// 在中心点周围随机分布
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5) * 0.5;
const distance = Math.random() * 30;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
// 爆炸速度
const speed = 100 + Math.random() * 150;
const dx = Math.cos(angle) * speed;
const dy = Math.sin(angle) * speed;
const mass = 0.5 + Math.random() * 1;
const bounce = 0.8 + Math.random() * 0.2;
// 亮色
const colors = ['#ffaa00', '#ff6600', '#ff0066', '#ff3300', '#ffff00'];
const color = colors[Math.floor(Math.random() * colors.length)];
const size = Math.random() * 4 + 2;
entity.addComponent(new Position(x, y));
entity.addComponent(new Velocity(dx, dy));
entity.addComponent(new Physics(mass, bounce, 0.999));
entity.addComponent(new Renderable(color, size, 'circle'));
entity.addComponent(new Lifetime(2 + Math.random() * 3)); // 短生命周期
}
}
/**
* 清空所有实体
*/
public clearAllEntities(): void {
const entities = [...this.entities.buffer]; // 复制数组避免修改原数组
for (const entity of entities) {
entity.destroy();
}
}
/**
* 切换Worker启用状态
*/
public toggleWorker(): boolean {
const workerInfo = this.physicsSystem.getWorkerInfo();
const newWorkerEnabled = !workerInfo.enabled;
// 重新创建物理系统
this.removeSystem(this.physicsSystem);
this.physicsSystem = new PhysicsWorkerSystem(newWorkerEnabled);
this.physicsSystem.updateOrder = 1;
this.addSystem(this.physicsSystem);
return newWorkerEnabled;
}
/**
* 更新Worker配置
*/
public updateWorkerConfig(config: { gravity?: number; friction?: number }): void {
if (config.gravity !== undefined || config.friction !== undefined) {
const physicsConfig = this.physicsSystem.getPhysicsConfig();
this.physicsSystem.updatePhysicsConfig({
gravity: config.gravity ?? physicsConfig.gravity,
groundFriction: config.friction ?? physicsConfig.groundFriction
});
}
}
/**
* 切换 SharedArrayBuffer 状态
*/
public toggleSharedArrayBuffer(): void {
this.physicsSystem.disableSharedArrayBuffer();
}
/**
* 获取物理系统状态
*/
public getPhysicsSystemStatus() {
return this.physicsSystem.getCurrentStatus();
}
/**
* 获取系统信息
*/
public getSystemInfo() {
return {
physics: this.physicsSystem.getWorkerInfo(),
entityCount: this.entities.count,
physicsConfig: this.physicsSystem.getPhysicsConfig()
};
}
}

View File

@@ -1,89 +0,0 @@
import { Component, ECSComponent } from '@esengine/ecs-framework';
// 位置组件
@ECSComponent('Position')
export class Position extends Component {
x: number = 0;
y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
set(x: number, y: number): void {
this.x = x;
this.y = y;
}
}
// 速度组件
@ECSComponent('Velocity')
export class Velocity extends Component {
dx: number = 0;
dy: number = 0;
constructor(dx: number = 0, dy: number = 0) {
super();
this.dx = dx;
this.dy = dy;
}
set(dx: number, dy: number): void {
this.dx = dx;
this.dy = dy;
}
scale(factor: number): void {
this.dx *= factor;
this.dy *= factor;
}
}
// 物理组件
@ECSComponent('Physics')
export class Physics extends Component {
mass: number = 1;
bounce: number = 0.8;
friction: number = 0.95;
constructor(mass: number = 1, bounce: number = 0.8, friction: number = 0.95) {
super();
this.mass = mass;
this.bounce = bounce;
this.friction = friction;
}
}
// 渲染组件
@ECSComponent('Renderable')
export class Renderable extends Component {
color: string = '#ffffff';
size: number = 5;
shape: 'circle' | 'square' = 'circle';
constructor(color: string = '#ffffff', size: number = 5, shape: 'circle' | 'square' = 'circle') {
super();
this.color = color;
this.size = size;
this.shape = shape;
}
}
// 生命周期组件
@ECSComponent('Lifetime')
export class Lifetime extends Component {
maxAge: number = 5;
currentAge: number = 0;
constructor(maxAge: number = 5) {
super();
this.maxAge = maxAge;
this.currentAge = 0;
}
isDead(): boolean {
return this.currentAge >= this.maxAge;
}
}

View File

@@ -1,376 +0,0 @@
import { Core, PlatformManager } from '@esengine/ecs-framework';
import { GameScene } from './GameScene';
import { BrowserAdapter } from './platform/BrowserAdapter';
// 性能监控
interface PerformanceStats {
fps: number;
frameTime: number;
physicsTime: number;
renderTime: number;
memoryUsage: number;
}
class WorkerDemo {
private gameScene: GameScene;
private canvas: HTMLCanvasElement;
private isRunning = false;
private lastTime = 0;
private frameCount = 0;
private fpsUpdateTime = 0;
private currentFPS = 0;
private lastWorkerStatusUpdate = 0;
// UI元素
private elements: { [key: string]: HTMLElement } = {};
constructor() {
// 注册浏览器适配器
const browserAdapter = new BrowserAdapter();
PlatformManager.getInstance().registerAdapter(browserAdapter);
// 获取canvas
this.canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
if (!this.canvas) {
throw new Error('Canvas element not found');
}
// 初始化UI元素引用
this.initializeUIElements();
// 初始化ECS Core
Core.create({
debug: true,
enableEntitySystems: true
});
// 创建游戏场景
this.gameScene = new GameScene(this.canvas);
// 设置场景
Core.setScene(this.gameScene);
// 绑定事件
this.bindEvents();
// 启动演示
this.start();
}
private initializeUIElements(): void {
const elementIds = [
'entityCount', 'entityCountValue', 'toggleWorker', 'toggleSAB',
'gravity', 'gravityValue', 'friction', 'frictionValue', 'spawnParticles',
'clearEntities', 'resetDemo', 'fps', 'entityCountStat', 'workerStatus', 'workerLoad',
'physicsTime', 'renderTime', 'frameTime', 'memoryUsage', 'sabStatus'
];
for (const id of elementIds) {
const element = document.getElementById(id);
if (element) {
this.elements[id] = element;
} else {
console.warn(`Element with id '${id}' not found`);
}
}
}
private bindEvents(): void {
// 实体数量滑块
if (this.elements.entityCount && this.elements.entityCountValue) {
const slider = this.elements.entityCount as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.entityCountValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const count = parseInt(slider.value);
this.gameScene.spawnInitialEntities(count);
});
}
// Worker切换按钮
if (this.elements.toggleWorker) {
this.elements.toggleWorker.addEventListener('click', () => {
const workerEnabled = this.gameScene.toggleWorker();
this.elements.toggleWorker.textContent = workerEnabled ? '禁用 Worker' : '启用 Worker';
this.updateWorkerStatus();
});
}
// SharedArrayBuffer切换按钮
if (this.elements.toggleSAB) {
this.elements.toggleSAB.addEventListener('click', () => {
this.gameScene.toggleSharedArrayBuffer();
this.updateWorkerStatus();
});
}
// 重力滑块
if (this.elements.gravity && this.elements.gravityValue) {
const slider = this.elements.gravity as HTMLInputElement;
slider.addEventListener('input', () => {
this.elements.gravityValue.textContent = slider.value;
});
slider.addEventListener('change', () => {
const gravity = parseInt(slider.value);
this.gameScene.updateWorkerConfig({ gravity });
});
}
// 摩擦力滑块
if (this.elements.friction && this.elements.frictionValue) {
const slider = this.elements.friction as HTMLInputElement;
slider.addEventListener('input', () => {
const value = parseInt(slider.value);
this.elements.frictionValue.textContent = `${value}%`;
});
slider.addEventListener('change', () => {
const friction = parseInt(slider.value) / 100;
this.gameScene.updateWorkerConfig({ friction });
});
}
// 生成粒子按钮
if (this.elements.spawnParticles) {
this.elements.spawnParticles.addEventListener('click', () => {
const centerX = this.canvas.width / 2;
const centerY = this.canvas.height / 2;
this.gameScene.spawnParticleExplosion(centerX, centerY, 100);
});
}
// 清空实体按钮
if (this.elements.clearEntities) {
this.elements.clearEntities.addEventListener('click', () => {
this.gameScene.clearAllEntities();
});
}
// 重置演示按钮
if (this.elements.resetDemo) {
this.elements.resetDemo.addEventListener('click', () => {
this.resetDemo();
});
}
// Canvas点击事件 - 在点击位置生成粒子爆发
this.canvas.addEventListener('click', (event) => {
const rect = this.canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
this.gameScene.spawnParticleExplosion(x, y, 30);
});
}
private start(): void {
this.isRunning = true;
this.lastTime = performance.now();
this.gameLoop();
console.log('Worker演示已启动');
}
private gameLoop = (): void => {
if (!this.isRunning) return;
const currentTime = performance.now();
const deltaTime = (currentTime - this.lastTime) / 1000; // 转换为秒
this.lastTime = currentTime;
// 更新ECS框架
const frameStartTime = performance.now();
Core.update(deltaTime);
const frameEndTime = performance.now();
// 更新性能统计
this.updatePerformanceStats({
fps: this.currentFPS,
frameTime: frameEndTime - frameStartTime,
physicsTime: (window as any).physicsExecutionTime || 0,
renderTime: (window as any).renderExecutionTime || 0,
memoryUsage: this.getMemoryUsage()
});
// 更新FPS计算
this.frameCount++;
if (currentTime - this.fpsUpdateTime >= 1000) {
this.currentFPS = this.frameCount;
this.frameCount = 0;
this.fpsUpdateTime = currentTime;
}
// 更新UI
this.updateUI();
// 继续循环
requestAnimationFrame(this.gameLoop);
};
private updatePerformanceStats(stats: PerformanceStats): void {
if (this.elements.fps) {
this.elements.fps.textContent = stats.fps.toString();
this.elements.fps.className = stats.fps >= 55 ? 'performance-high' :
stats.fps >= 30 ? 'performance-medium' : 'performance-low';
}
if (this.elements.frameTime) {
this.elements.frameTime.textContent = stats.frameTime.toFixed(2);
this.elements.frameTime.className = stats.frameTime <= 16 ? 'performance-high' :
stats.frameTime <= 33 ? 'performance-medium' : 'performance-low';
}
if (this.elements.physicsTime) {
this.elements.physicsTime.textContent = stats.physicsTime.toFixed(2);
this.elements.physicsTime.className = stats.physicsTime <= 8 ? 'performance-high' :
stats.physicsTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.renderTime) {
this.elements.renderTime.textContent = stats.renderTime.toFixed(2);
this.elements.renderTime.className = stats.renderTime <= 8 ? 'performance-high' :
stats.renderTime <= 16 ? 'performance-medium' : 'performance-low';
}
if (this.elements.memoryUsage) {
this.elements.memoryUsage.textContent = stats.memoryUsage.toFixed(1);
}
}
private updateUI(): void {
const currentTime = performance.now();
const systemInfo = this.gameScene.getSystemInfo();
// 更新实体数量(每帧更新)
if (this.elements.entityCountStat) {
this.elements.entityCountStat.textContent = systemInfo.entityCount.toString();
}
// 更新Worker状态每500ms更新一次即可
if (currentTime - this.lastWorkerStatusUpdate >= 500) {
this.updateWorkerStatus();
this.lastWorkerStatusUpdate = currentTime;
}
// 更新全局Worker信息供其他系统使用
(window as any).workerInfo = systemInfo.physics;
}
private updateWorkerStatus(): void {
const systemInfo = this.gameScene.getSystemInfo();
const workerInfo = systemInfo.physics;
const entityCount = systemInfo.entityCount;
const status = this.gameScene.getPhysicsSystemStatus();
if (this.elements.workerStatus) {
if (workerInfo.enabled) {
this.elements.workerStatus.textContent = `启用 (${workerInfo.workerCount} Workers)`;
this.elements.workerStatus.className = 'worker-enabled';
} else {
this.elements.workerStatus.textContent = '禁用';
this.elements.workerStatus.className = 'worker-disabled';
}
}
if (this.elements.workerLoad) {
if (workerInfo.enabled && entityCount > 0) {
const entitiesPerWorker = Math.ceil(entityCount / workerInfo.workerCount);
this.elements.workerLoad.textContent = `${entitiesPerWorker}/Worker (共${workerInfo.workerCount}个)`;
} else {
this.elements.workerLoad.textContent = 'N/A';
}
}
// 更新 SharedArrayBuffer 状态
if (this.elements.sabStatus) {
const modeNames = {
'shared-buffer': 'SharedArrayBuffer模式',
'single-worker': '单Worker模式',
'multi-worker': '多Worker模式',
'sync': '同步模式'
};
this.elements.sabStatus.textContent = modeNames[status.mode] || status.mode;
this.elements.sabStatus.className = status.mode === 'shared-buffer' ? 'worker-enabled' : 'worker-disabled';
}
// 更新 SharedArrayBuffer 按钮文本
if (this.elements.toggleSAB) {
if (status.sharedArrayBufferEnabled) {
this.elements.toggleSAB.textContent = '禁用 SharedArrayBuffer';
} else {
this.elements.toggleSAB.textContent = '启用 SharedArrayBuffer';
this.elements.toggleSAB.setAttribute('disabled', 'true'); // SAB 一旦禁用就无法重新启用
}
}
}
private getMemoryUsage(): number {
if ('memory' in performance) {
const memory = (performance as any).memory;
return memory.usedJSHeapSize / (1024 * 1024); // MB
}
return 0;
}
private resetDemo(): void {
// 重置所有控件到默认值
if (this.elements.entityCount) {
(this.elements.entityCount as HTMLInputElement).value = '1000';
this.elements.entityCountValue.textContent = '1000';
}
if (this.elements.gravity) {
(this.elements.gravity as HTMLInputElement).value = '100';
this.elements.gravityValue.textContent = '100';
}
if (this.elements.friction) {
(this.elements.friction as HTMLInputElement).value = '95';
this.elements.frictionValue.textContent = '95%';
}
// 确保Worker被启用
const workerInfo = this.gameScene.getSystemInfo().physics;
if (!workerInfo.enabled) {
this.gameScene.toggleWorker(); // 只有在禁用时才切换
}
if (this.elements.toggleWorker) {
this.elements.toggleWorker.textContent = '禁用 Worker';
}
// 重新生成实体
this.gameScene.spawnInitialEntities(1000);
// 重置配置
this.gameScene.updateWorkerConfig({
gravity: 100,
friction: 0.95
});
console.log('演示已重置');
}
public stop(): void {
this.isRunning = false;
}
}
// 启动演示
document.addEventListener('DOMContentLoaded', () => {
try {
new WorkerDemo();
} catch (error) {
console.error('启动演示失败:', error);
document.body.innerHTML = `
<div style="padding: 20px; color: red;">
<h1>启动失败</h1>
<p>错误: ${error}</p>
<p>请确保浏览器支持Web Workers和Canvas API</p>
</div>
`;
}
});

View File

@@ -1,30 +0,0 @@
import { EntitySystem, Matcher, Entity, ECSSystem, Time } from '@esengine/ecs-framework';
import { Lifetime } from '../components';
@ECSSystem('LifetimeSystem')
export class LifetimeSystem extends EntitySystem {
constructor() {
super(Matcher.empty().all(Lifetime));
}
protected override process(entities: readonly Entity[]): void {
const entitiesToRemove: Entity[] = [];
for (const entity of entities) {
const lifetime = entity.getComponent(Lifetime)!;
// 更新年龄
lifetime.currentAge += Time.deltaTime;
// 检查是否需要销毁
if (lifetime.isDead()) {
entitiesToRemove.push(entity);
}
}
// 销毁过期的实体
for (const entity of entitiesToRemove) {
entity.destroy();
}
}
}

View File

@@ -1,513 +0,0 @@
import { WorkerEntitySystem, Matcher, Entity, ECSSystem, SharedArrayBufferProcessFunction } from '@esengine/ecs-framework';
import { Position, Velocity, Physics, Renderable } from '../components';
interface PhysicsEntityData {
id: number;
x: number;
y: number;
dx: number;
dy: number;
mass: number;
bounce: number;
friction: number;
radius: number;
}
interface PhysicsConfig {
gravity: number;
canvasWidth: number;
canvasHeight: number;
groundFriction: number;
}
@ECSSystem('PhysicsWorkerSystem')
export class PhysicsWorkerSystem extends WorkerEntitySystem<PhysicsEntityData> {
private physicsConfig: PhysicsConfig = {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98 // 减少地面摩擦
};
constructor(enableWorker: boolean = true) {
const defaultConfig = {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
// 检查 SharedArrayBuffer 是否可用
const isSharedArrayBufferAvailable = typeof SharedArrayBuffer !== 'undefined' && self.crossOriginIsolated;
super(
Matcher.empty().all(Position, Velocity, Physics),
{
enableWorker,
// 当 SharedArrayBuffer 可用时使用多 Worker否则使用单 Worker 保证碰撞检测完整性
workerCount: isSharedArrayBufferAvailable ? (navigator.hardwareConcurrency || 2) : 1,
systemConfig: defaultConfig,
useSharedArrayBuffer: true // 优先使用 SharedArrayBuffer
}
);
}
protected extractEntityData(entity: Entity): PhysicsEntityData {
const position = entity.getComponent(Position)!;
const velocity = entity.getComponent(Velocity)!;
const physics = entity.getComponent(Physics)!;
const renderable = entity.getComponent(Renderable)!;
return {
id: entity.id,
x: position.x,
y: position.y,
dx: velocity.dx,
dy: velocity.dy,
mass: physics.mass,
bounce: physics.bounce,
friction: physics.friction,
radius: renderable.size
};
}
/**
* Worker处理函数 - 纯函数会被序列化到Worker中执行
* 注意:这个函数内部不能访问外部变量,必须是纯函数
* 非SharedArrayBuffer模式每个Worker只能看到分配给它的实体批次
* 这会导致跨批次的碰撞检测缺失,但单批次内的碰撞是正确的
*/
protected workerProcess(
entities: PhysicsEntityData[],
deltaTime: number,
systemConfig?: PhysicsConfig
): PhysicsEntityData[] {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
// 创建实体副本以避免修改原始数据
const result = entities.map(e => ({ ...e }));
// 应用重力和基础物理
for (let i = 0; i < result.length; i++) {
const entity = result[i];
// 应用重力
entity.dy += config.gravity * deltaTime;
// 更新位置
entity.x += entity.dx * deltaTime;
entity.y += entity.dy * deltaTime;
// 边界碰撞检测和处理
if (entity.x <= entity.radius) {
entity.x = entity.radius;
entity.dx = -entity.dx * entity.bounce;
} else if (entity.x >= config.canvasWidth - entity.radius) {
entity.x = config.canvasWidth - entity.radius;
entity.dx = -entity.dx * entity.bounce;
}
if (entity.y <= entity.radius) {
entity.y = entity.radius;
entity.dy = -entity.dy * entity.bounce;
} else if (entity.y >= config.canvasHeight - entity.radius) {
entity.y = config.canvasHeight - entity.radius;
entity.dy = -entity.dy * entity.bounce;
// 地面摩擦力
entity.dx *= config.groundFriction;
}
// 空气阻力
entity.dx *= entity.friction;
entity.dy *= entity.friction;
}
// 小球间碰撞检测
for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const ball1 = result[i];
const ball2 = result[j];
// 计算距离
const dx = ball2.x - ball1.x;
const dy = ball2.y - ball1.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const minDistance = ball1.radius + ball2.radius;
// 检测碰撞
if (distance < minDistance && distance > 0) {
// 碰撞法线
const nx = dx / distance;
const ny = dy / distance;
// 分离小球以避免重叠
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
ball1.x -= separationX;
ball1.y -= separationY;
ball2.x += separationX;
ball2.y += separationY;
// 相对速度
const relativeVelocityX = ball2.dx - ball1.dx;
const relativeVelocityY = ball2.dy - ball1.dy;
// 沿碰撞法线的速度分量
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// 如果速度分量为正,小球正在分离,不需要处理
if (velocityAlongNormal > 0) continue;
// 计算弹性系数(两球弹性的平均值)
const restitution = (ball1.bounce + ball2.bounce) * 0.5;
// 计算冲量大小
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/ball1.mass + 1/ball2.mass);
// 应用冲量
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
ball1.dx -= impulseX / ball1.mass;
ball1.dy -= impulseY / ball1.mass;
ball2.dx += impulseX / ball2.mass;
ball2.dy += impulseY / ball2.mass;
// 轻微的能量损失,保持活力
const energyLoss = 0.98;
ball1.dx *= energyLoss;
ball1.dy *= energyLoss;
ball2.dx *= energyLoss;
ball2.dy *= energyLoss;
}
}
}
return result;
}
/**
* 应用处理结果
*/
protected applyResult(entity: Entity, result: PhysicsEntityData): void {
// 检查实体是否仍然存在且有效
if (!entity || !entity.enabled) {
return;
}
const position = entity.getComponent(Position);
const velocity = entity.getComponent(Velocity);
// 检查组件是否仍然存在实体可能在Worker处理期间被修改
if (!position || !velocity) {
return;
}
position.set(result.x, result.y);
velocity.set(result.dx, result.dy);
}
/**
* 更新物理配置
*/
public updatePhysicsConfig(newConfig: Partial<PhysicsConfig>): void {
Object.assign(this.physicsConfig, newConfig);
this.updateConfig({ systemConfig: this.physicsConfig });
}
/**
* 获取物理配置
*/
public getPhysicsConfig(): PhysicsConfig {
return { ...this.physicsConfig };
}
/**
* 禁用 SharedArrayBuffer用于测试降级行为
*/
public disableSharedArrayBuffer(): void {
console.log(`[${this.systemName}] Disabling SharedArrayBuffer for testing - falling back to single Worker mode`);
// 使用正式的配置更新 API
this.updateConfig({
useSharedArrayBuffer: false
});
}
/**
* 获取当前运行状态
*/
public getCurrentStatus(): {
mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync';
sharedArrayBufferEnabled: boolean;
workerCount: number;
workerEnabled: boolean;
} {
const workerInfo = this.getWorkerInfo();
let mode: 'shared-buffer' | 'single-worker' | 'multi-worker' | 'sync' = 'sync';
if (workerInfo.enabled) {
if (workerInfo.sharedArrayBufferEnabled && workerInfo.sharedArrayBufferSupported) {
mode = 'shared-buffer';
} else if (workerInfo.workerCount === 1) {
mode = 'single-worker';
} else {
mode = 'multi-worker';
}
}
return {
mode,
sharedArrayBufferEnabled: workerInfo.sharedArrayBufferEnabled,
workerCount: workerInfo.workerCount,
workerEnabled: workerInfo.enabled
};
}
private startTime: number = 0;
/**
* 性能监控
*/
protected override onEnd(): void {
super.onEnd();
const endTime = performance.now();
const executionTime = endTime - this.startTime;
// 发送性能数据到UI
(window as any).physicsExecutionTime = executionTime;
}
/**
* 获取实体数据大小
*/
protected getDefaultEntityDataSize(): number {
return 9; // id, x, y, dx, dy, mass, bounce, friction, radius
}
/**
* 将实体数据写入SharedArrayBuffer
*/
protected writeEntityToBuffer(entityData: PhysicsEntityData, offset: number): void {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return;
// 在第一个位置存储当前实体数量用于Worker函数判断实际有效数据范围
const currentEntityCount = Math.floor(offset / 9) + 1;
sharedArray[0] = currentEntityCount; // 元数据:实际实体数量
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
sharedArray[dataOffset + 0] = entityData.id;
sharedArray[dataOffset + 1] = entityData.x;
sharedArray[dataOffset + 2] = entityData.y;
sharedArray[dataOffset + 3] = entityData.dx;
sharedArray[dataOffset + 4] = entityData.dy;
sharedArray[dataOffset + 5] = entityData.mass;
sharedArray[dataOffset + 6] = entityData.bounce;
sharedArray[dataOffset + 7] = entityData.friction;
sharedArray[dataOffset + 8] = entityData.radius;
}
/**
* 性能监控开始
*/
protected override onBegin(): void {
super.onBegin();
this.startTime = performance.now();
}
/**
* 从SharedArrayBuffer读取实体数据
*/
protected readEntityFromBuffer(offset: number): PhysicsEntityData | null {
const sharedArray = (this as any).sharedFloatArray as Float32Array;
if (!sharedArray) return null;
// 数据从索引9开始存储第一个9个位置用作元数据区域
const dataOffset = offset + 9;
return {
id: sharedArray[dataOffset + 0],
x: sharedArray[dataOffset + 1],
y: sharedArray[dataOffset + 2],
dx: sharedArray[dataOffset + 3],
dy: sharedArray[dataOffset + 4],
mass: sharedArray[dataOffset + 5],
bounce: sharedArray[dataOffset + 6],
friction: sharedArray[dataOffset + 7],
radius: sharedArray[dataOffset + 8]
};
}
/**
* SharedArrayBuffer处理函数
*/
protected getSharedArrayBufferProcessFunction(): SharedArrayBufferProcessFunction {
return function(sharedFloatArray: Float32Array, startIndex: number, endIndex: number, deltaTime: number, systemConfig?: any) {
const config = systemConfig || {
gravity: 100,
canvasWidth: 800,
canvasHeight: 600,
groundFriction: 0.98
};
// 读取实际实体数量(存储在第一个位置)
const actualEntityCount = sharedFloatArray[0];
// 基础物理更新
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset = i * 9 + 9; // 数据从索引9开始加上元数据偏移
// 读取实体数据
const id = sharedFloatArray[offset + 0];
if (id === 0) continue; // 跳过无效实体
let x = sharedFloatArray[offset + 1];
let y = sharedFloatArray[offset + 2];
let dx = sharedFloatArray[offset + 3];
let dy = sharedFloatArray[offset + 4];
// const mass = sharedFloatArray[offset + 5]; // 未使用
const bounce = sharedFloatArray[offset + 6];
const friction = sharedFloatArray[offset + 7];
const radius = sharedFloatArray[offset + 8];
// 应用重力
dy += config.gravity * deltaTime;
// 更新位置
x += dx * deltaTime;
y += dy * deltaTime;
// 边界碰撞检测和处理
if (x <= radius) {
x = radius;
dx = -dx * bounce;
} else if (x >= config.canvasWidth - radius) {
x = config.canvasWidth - radius;
dx = -dx * bounce;
}
if (y <= radius) {
y = radius;
dy = -dy * bounce;
} else if (y >= config.canvasHeight - radius) {
y = config.canvasHeight - radius;
dy = -dy * bounce;
// 地面摩擦力
dx *= config.groundFriction;
}
// 空气阻力
dx *= friction;
dy *= friction;
// 写回数据
sharedFloatArray[offset + 1] = x;
sharedFloatArray[offset + 2] = y;
sharedFloatArray[offset + 3] = dx;
sharedFloatArray[offset + 4] = dy;
}
// 小球间碰撞检测
for (let i = startIndex; i < endIndex && i < actualEntityCount; i++) {
const offset1 = i * 9 + 9; // 数据从索引9开始加上元数据偏移
const id1 = sharedFloatArray[offset1 + 0];
if (id1 === 0) continue;
let x1 = sharedFloatArray[offset1 + 1];
let y1 = sharedFloatArray[offset1 + 2];
let dx1 = sharedFloatArray[offset1 + 3];
let dy1 = sharedFloatArray[offset1 + 4];
const mass1 = sharedFloatArray[offset1 + 5];
const bounce1 = sharedFloatArray[offset1 + 6];
const radius1 = sharedFloatArray[offset1 + 8];
// 检测与所有其他小球的碰撞(能看到所有实体,实现完整碰撞检测)
for (let j = 0; j < actualEntityCount; j++) {
if (i === j) continue;
const offset2 = j * 9 + 9; // 数据从索引9开始加上元数据偏移
const id2 = sharedFloatArray[offset2 + 0];
if (id2 === 0) continue;
const x2 = sharedFloatArray[offset2 + 1];
const y2 = sharedFloatArray[offset2 + 2];
const dx2 = sharedFloatArray[offset2 + 3];
const dy2 = sharedFloatArray[offset2 + 4];
const mass2 = sharedFloatArray[offset2 + 5];
const bounce2 = sharedFloatArray[offset2 + 6];
const radius2 = sharedFloatArray[offset2 + 8];
// 额外检查:确保位置和半径都是有效值
if (isNaN(x2) || isNaN(y2) || isNaN(radius2) || radius2 <= 0) continue;
// 计算距离
const deltaX = x2 - x1;
const deltaY = y2 - y1;
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const minDistance = radius1 + radius2;
// 检测碰撞
if (distance < minDistance && distance > 0) {
// 碰撞法线
const nx = deltaX / distance;
const ny = deltaY / distance;
// 分离小球 - 只调整当前Worker负责的球
const overlap = minDistance - distance;
const separationX = nx * overlap * 0.5;
const separationY = ny * overlap * 0.5;
x1 -= separationX;
y1 -= separationY;
// 相对速度
const relativeVelocityX = dx2 - dx1;
const relativeVelocityY = dy2 - dy1;
// 沿碰撞法线的速度分量
const velocityAlongNormal = relativeVelocityX * nx + relativeVelocityY * ny;
// 如果速度分量为正,小球正在分离
if (velocityAlongNormal > 0) continue;
// 弹性系数
const restitution = (bounce1 + bounce2) * 0.5;
// 冲量计算
const impulseScalar = -(1 + restitution) * velocityAlongNormal / (1/mass1 + 1/mass2);
// 应用冲量到当前小球只更新当前Worker负责的球
const impulseX = impulseScalar * nx;
const impulseY = impulseScalar * ny;
dx1 -= impulseX / mass1;
dy1 -= impulseY / mass1;
// 能量损失
const energyLoss = 0.98;
dx1 *= energyLoss;
dy1 *= energyLoss;
}
}
// 只更新当前Worker负责的实体
sharedFloatArray[offset1 + 1] = x1;
sharedFloatArray[offset1 + 2] = y1;
sharedFloatArray[offset1 + 3] = dx1;
sharedFloatArray[offset1 + 4] = dy1;
}
};
}
}

View File

@@ -1,107 +0,0 @@
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import { Position, Renderable } from '../components';
@ECSSystem('RenderSystem')
export class RenderSystem extends EntitySystem {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private startTime: number = 0;
private batchCount: number = 0;
private drawCallCount: number = 0;
constructor(canvas: HTMLCanvasElement) {
super(Matcher.empty().all(Position, Renderable));
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
protected override onBegin(): void {
super.onBegin();
this.startTime = performance.now();
// 清空画布
this.ctx.fillStyle = '#000000';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
protected override process(entities: readonly Entity[]): void {
// 保持原始绘制顺序,但优化连续相同颜色的绘制
let lastColor = '';
this.drawCallCount = 0;
for (const entity of entities) {
const position = entity.getComponent(Position)!;
const renderable = entity.getComponent(Renderable)!;
// 只在颜色变化时设置fillStyle减少状态切换
if (renderable.color !== lastColor) {
this.ctx.fillStyle = renderable.color;
lastColor = renderable.color;
}
if (renderable.shape === 'circle') {
this.ctx.beginPath();
this.ctx.arc(position.x, position.y, renderable.size, 0, Math.PI * 2);
this.ctx.fill();
this.drawCallCount++;
} else if (renderable.shape === 'square') {
this.ctx.fillRect(
position.x - renderable.size / 2,
position.y - renderable.size / 2,
renderable.size,
renderable.size
);
this.drawCallCount++;
}
}
// 计算颜色多样性用于显示
const uniqueColors = new Set(entities.map(e => e.getComponent(Renderable)!.color));
this.batchCount = uniqueColors.size;
}
protected override onEnd(): void {
super.onEnd();
const endTime = performance.now();
const executionTime = endTime - this.startTime;
// 发送性能数据到UI
(window as any).renderExecutionTime = executionTime;
// 绘制调试信息
this.drawDebugInfo();
}
private drawDebugInfo(): void {
const entities = this.entities;
this.ctx.fillStyle = '#00ff00';
this.ctx.font = '14px Arial';
this.ctx.fillText(`实体数量: ${entities.length}`, 10, 20);
this.ctx.fillText(`渲染批次: ${this.batchCount}`, 10, 140);
this.ctx.fillText(`绘制调用: ${this.drawCallCount}`, 10, 160);
const workerInfo = (window as any).workerInfo;
if (workerInfo) {
this.ctx.fillStyle = workerInfo.enabled ? '#00ff00' : '#ff0000';
this.ctx.fillText(`Worker: ${workerInfo.enabled ? '启用' : '禁用'}`, 10, 40);
if (workerInfo.enabled) {
this.ctx.fillStyle = '#ffff00';
const entitiesPerWorker = Math.ceil(entities.length / workerInfo.workerCount);
this.ctx.fillText(`每个Worker实体: ${entitiesPerWorker}`, 10, 60);
this.ctx.fillText(`Worker数量: ${workerInfo.workerCount}`, 10, 80);
}
}
// 显示性能信息
const physicsTime = (window as any).physicsExecutionTime || 0;
const renderTime = (window as any).renderExecutionTime || 0;
this.ctx.fillStyle = physicsTime > 16 ? '#ff0000' : physicsTime > 8 ? '#ffff00' : '#00ff00';
this.ctx.fillText(`物理: ${physicsTime.toFixed(2)}ms`, 10, 100);
this.ctx.fillStyle = renderTime > 16 ? '#ff0000' : renderTime > 8 ? '#ffff00' : '#00ff00';
this.ctx.fillText(`渲染: ${renderTime.toFixed(2)}ms`, 10, 120);
}
}

View File

@@ -1,3 +0,0 @@
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
export { RenderSystem } from './RenderSystem';
export { LifetimeSystem } from './LifetimeSystem';

View File

@@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,26 +0,0 @@
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
open: true,
headers: {
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'
}
},
build: {
target: 'es2020',
outDir: 'dist',
minify: false,
rollupOptions: {
output: {
format: 'es',
manualChunks: undefined
}
}
},
esbuild: {
target: 'es2020'
}
});

45
package-lock.json generated
View File

@@ -5220,6 +5220,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/msgpack-lite": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@types/msgpack-lite/-/msgpack-lite-0.1.11.tgz",
"integrity": "sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/multer": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz",
@@ -7705,6 +7715,12 @@
"node": ">=0.10.0"
}
},
"node_modules/event-lite": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
"license": "MIT"
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
@@ -8656,7 +8672,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -8846,6 +8861,12 @@
"node": ">=8"
}
},
"node_modules/int64-buffer": {
"version": "0.1.10",
"resolved": "https://registry.npmjs.org/int64-buffer/-/int64-buffer-0.1.10.tgz",
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
"license": "MIT"
},
"node_modules/ip-address": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
@@ -9117,7 +9138,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true,
"license": "MIT"
},
"node_modules/isexe": {
@@ -11523,6 +11543,21 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true
},
"node_modules/msgpack-lite": {
"version": "0.1.26",
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
"integrity": "sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==",
"license": "MIT",
"dependencies": {
"event-lite": "^0.1.1",
"ieee754": "^1.1.8",
"int64-buffer": "^0.1.9",
"isarray": "^1.0.0"
},
"bin": {
"msgpack": "bin/msgpack"
}
},
"node_modules/multimatch": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
@@ -15522,8 +15557,11 @@
},
"packages/core": {
"name": "@esengine/ecs-framework",
"version": "2.1.51",
"version": "2.1.52",
"license": "MIT",
"dependencies": {
"msgpack-lite": "^0.1.26"
},
"devDependencies": {
"@babel/core": "^7.28.3",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
@@ -15534,6 +15572,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/ecs-framework",
"version": "2.1.51",
"version": "2.1.52",
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
"main": "bin/index.js",
"types": "bin/index.d.ts",
@@ -58,6 +58,7 @@
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@types/jest": "^29.5.14",
"@types/msgpack-lite": "^0.1.11",
"@types/node": "^20.19.17",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
@@ -75,5 +76,8 @@
"type": "git",
"url": "https://github.com/esengine/ecs-framework.git",
"directory": "packages/core"
},
"dependencies": {
"msgpack-lite": "^0.1.26"
}
}

View File

@@ -5,61 +5,53 @@ import { Timer } from './Utils/Timers/Timer';
import { Time } from './Utils/Time';
import { PerformanceMonitor } from './Utils/PerformanceMonitor';
import { PoolManager } from './Utils/Pool/PoolManager';
import { ECSFluentAPI, createECSAPI } from './ECS/Core/FluentAPI';
import { IScene } from './ECS/IScene';
import { WorldManager, IWorldManagerConfig } from './ECS/WorldManager';
import { DebugManager } from './Utils/Debug';
import { ICoreConfig, IECSDebugConfig } from './Types';
import { createLogger } from './Utils/Logger';
import { SceneManager } from './ECS/SceneManager';
import { IScene } from './ECS/IScene';
/**
* 游戏引擎核心类
*
* 负责管理游戏的生命周期、场景切换、全局管理器和定时器系统。
* 提供统一的游戏循环管理。
*
*
* 职责:
* - 提供全局服务Timer、Performance、Pool等
* - 管理场景生命周期内置SceneManager
* - 管理全局管理器的生命周期
* - 提供统一的游戏循环更新入口
*
* @example
* ```typescript
* // 创建核心实例
* const core = Core.create(true);
*
* // 设置场景
* Core.scene = new MyScene();
*
* // 在游戏循环中更新Laya引擎示例
* Laya.timer.frameLoop(1, this, () => {
* const deltaTime = Laya.timer.delta / 1000;
* // 初始化并设置场景
* Core.create({ debug: true });
* Core.setScene(new GameScene());
*
* // 游戏循环(自动更新全局服务和场景)
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime);
* });
*
* // 调度定时器
* }
*
* // 使用定时器
* Core.schedule(1.0, false, null, (timer) => {
* Core._logger.info("1秒后执行");
* console.log("1秒后执行");
* });
*
* // 切换场景
* Core.loadScene(new MenuScene()); // 延迟切换
* Core.setScene(new GameScene()); // 立即切换
*
* // 获取当前场景
* const currentScene = Core.scene;
* ```
*/
export class Core {
/**
* 游戏暂停状态
*
*
* 当设置为true时游戏循环将暂停执行。
*/
public static paused = false;
/**
* 默认World ID
*
* 用于单Scene模式的默认World标识
*/
private static readonly DEFAULT_WORLD_ID = '__default__';
/**
* 默认Scene ID
*
* 用于单Scene模式的默认Scene标识
*/
private static readonly DEFAULT_SCENE_ID = '__main__';
/**
* 全局核心实例
*/
@@ -69,81 +61,71 @@ export class Core {
* Core专用日志器
*/
private static _logger = createLogger('Core');
/**
* 实体系统启用状态
*
*
* 控制是否启用ECS实体系统功能。
*/
public static entitySystemsEnabled: boolean;
/**
* 调试模式标志
*
*
* 在调试模式下会启用额外的性能监控和错误检查。
*/
public readonly debug: boolean;
/**
* 全局管理器集合
*
*
* 存储所有注册的全局管理器实例。
*/
public _globalManagers: GlobalManager[] = [];
/**
* 定时器管理器
*
*
* 负责管理所有的游戏定时器。
*/
public _timerManager: TimerManager;
/**
* 性能监控器
*
*
* 监控游戏性能并提供优化建议。
*/
public _performanceMonitor: PerformanceMonitor;
/**
* 对象池管理器
*
*
* 管理所有对象池的生命周期。
*/
public _poolManager: PoolManager;
/**
* ECS流式API
*
* 提供便捷的ECS操作接口。
*/
public _ecsAPI?: ECSFluentAPI;
/**
* 调试管理器
*
*
* 负责收集和发送调试数据。
*/
public _debugManager?: DebugManager;
/**
* World管理器
*
* 管理多个World实例支持多房间/多世界架构
* 场景管理器
*
* 管理当前场景的生命周期
*/
public _worldManager?: WorldManager;
private _sceneManager: SceneManager;
/**
* Core配置
*/
private _config: ICoreConfig;
/**
* 创建核心实例
*
*
* @param config - Core配置对象
*/
private constructor(config: ICoreConfig = {}) {
@@ -156,14 +138,13 @@ export class Core {
...config
};
// 初始化管理器
// 初始化定时器管理器
this._timerManager = new TimerManager();
Core.registerGlobalManager(this._timerManager);
// 初始化性能监控器
this._performanceMonitor = PerformanceMonitor.instance;
// 在调试模式下启用性能监控
if (this._config.debug) {
this._performanceMonitor.enable();
@@ -171,7 +152,10 @@ export class Core {
// 初始化对象池管理器
this._poolManager = PoolManager.getInstance();
// 初始化场景管理器
this._sceneManager = new SceneManager();
Core.entitySystemsEnabled = this._config.enableEntitySystems ?? true;
this.debug = this._config.debug ?? true;
@@ -180,118 +164,161 @@ export class Core {
this._debugManager = new DebugManager(this, this._config.debugConfig);
}
this.initialize();
}
/**
* 获取核心实例
*
*
* @returns 全局核心实例
*/
public static get Instance() {
return this._instance;
}
/**
* 获取当前活动的场景(属性访问器)
*
* @returns 当前场景实例如果没有则返回null
*/
public static get scene(): IScene | null {
return this.getScene();
}
/**
* 获取当前活动的场景(方法调用)
*
* @returns 当前场景实例如果没有则返回null
*/
public static getScene<T extends IScene>(): T | null {
if (!this._instance) {
return null;
}
// 确保默认World存在
this._instance.ensureDefaultWorld();
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID);
return defaultWorld?.getScene(this.DEFAULT_SCENE_ID) as T || null;
}
/**
* 设置当前场景
*
* @param scene - 要设置的场景实例
* @returns 设置的场景实例,便于链式调用
*/
public static setScene<T extends IScene>(scene: T): T {
if (!this._instance) {
throw new Error("Core实例未创建请先调用Core.create()");
}
// 确保默认World存在
this._instance.ensureDefaultWorld();
const defaultWorld = this._instance._worldManager!.getWorld(this.DEFAULT_WORLD_ID)!;
// 移除旧的主Scene如果存在
if (defaultWorld.getScene(this.DEFAULT_SCENE_ID)) {
defaultWorld.removeScene(this.DEFAULT_SCENE_ID);
}
// 添加新Scene到默认World
defaultWorld.createScene(this.DEFAULT_SCENE_ID, scene);
defaultWorld.setSceneActive(this.DEFAULT_SCENE_ID, true);
// 触发场景切换回调
this._instance.onSceneChanged();
return scene;
}
/**
* 创建Core实例
*
*
* 如果实例已存在,则返回现有实例。
*
*
* @param config - Core配置也可以直接传入boolean表示debug模式向后兼容
* @returns Core实例
*
* @example
* ```typescript
* // 方式1使用配置对象
* Core.create({
* debug: true,
* enableEntitySystems: true,
* debugConfig: {
* enabled: true,
* websocketUrl: 'ws://localhost:9229'
* }
* });
*
* // 方式2简单模式向后兼容
* Core.create(true); // debug = true
* ```
*/
public static create(config: ICoreConfig | boolean = true): Core {
if (this._instance == null) {
// 向后兼容如果传入boolean转换为配置对象
const coreConfig: ICoreConfig = typeof config === 'boolean'
const coreConfig: ICoreConfig = typeof config === 'boolean'
? { debug: config, enableEntitySystems: true }
: config;
this._instance = new Core(coreConfig);
} else {
this._logger.warn('Core实例已创建返回现有实例');
}
return this._instance;
}
/**
* 更新游戏逻辑
*
* 此方法应该在游戏引擎的更新循环中调用。
*
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
*
* 设置当前场景
*
* @param scene - 要设置的场景
* @returns 设置的场景实例
*
* @example
* ```typescript
* // Laya引擎
* Core.create({ debug: true });
*
* // 创建并设置场景
* const gameScene = new GameScene();
* Core.setScene(gameScene);
* ```
*/
public static setScene<T extends IScene>(scene: T): T {
if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()");
throw new Error("Core实例未创建");
}
return this._instance._sceneManager.setScene(scene);
}
/**
* 获取当前场景
*
* @returns 当前场景如果没有场景则返回null
*/
public static get scene(): IScene | null {
if (!this._instance) {
return null;
}
return this._instance._sceneManager.currentScene;
}
/**
* 获取ECS流式API
*
* @returns ECS API实例如果当前没有场景则返回null
*
* @example
* ```typescript
* // 使用流式API创建实体
* const player = Core.ecsAPI?.createEntity('Player')
* .addComponent(Position, 100, 100)
* .addComponent(Velocity, 50, 0);
*
* // 查询实体
* const enemies = Core.ecsAPI?.query(Enemy, Transform);
*
* // 发射事件
* Core.ecsAPI?.emit('game:start', { level: 1 });
* ```
*/
public static get ecsAPI() {
if (!this._instance) {
return null;
}
return this._instance._sceneManager.api;
}
/**
* 延迟加载场景(下一帧切换)
*
* @param scene - 要加载的场景
*
* @example
* ```typescript
* // 延迟切换场景(在下一帧生效)
* Core.loadScene(new MenuScene());
* ```
*/
public static loadScene<T extends IScene>(scene: T): void {
if (!this._instance) {
Core._logger.warn("Core实例未创建请先调用Core.create()");
return;
}
this._instance._sceneManager.loadScene(scene);
}
/**
* 更新游戏逻辑
*
* 此方法应该在游戏引擎的更新循环中调用。
* 会自动更新全局服务和当前场景。
*
* @param deltaTime - 外部引擎提供的帧时间间隔(秒)
*
* @example
* ```typescript
* // 初始化
* Core.create({ debug: true });
* Core.setScene(new GameScene());
*
* // Laya引擎集成
* Laya.timer.frameLoop(1, this, () => {
* const deltaTime = Laya.timer.delta / 1000;
* Core.update(deltaTime);
* Core.update(deltaTime); // 自动更新全局服务和场景
* });
*
* // Cocos Creator
*
* // Cocos Creator集成
* update(deltaTime: number) {
* Core.update(deltaTime);
* Core.update(deltaTime); // 自动更新全局服务和场景
* }
*
* ```
*/
public static update(deltaTime: number): void {
@@ -299,15 +326,15 @@ export class Core {
Core._logger.warn("Core实例未创建请先调用Core.create()");
return;
}
this._instance.updateInternal(deltaTime);
}
/**
* 注册全局管理器
*
*
* 将管理器添加到全局管理器列表中,并启用它。
*
*
* @param manager - 要注册的全局管理器
*/
public static registerGlobalManager(manager: GlobalManager) {
@@ -317,9 +344,9 @@ export class Core {
/**
* 注销全局管理器
*
*
* 从全局管理器列表中移除管理器,并禁用它。
*
*
* @param manager - 要注销的全局管理器
*/
public static unregisterGlobalManager(manager: GlobalManager) {
@@ -329,7 +356,7 @@ export class Core {
/**
* 获取指定类型的全局管理器
*
*
* @param type - 管理器类型构造函数
* @returns 管理器实例如果未找到则返回null
*/
@@ -343,14 +370,27 @@ export class Core {
/**
* 调度定时器
*
*
* 创建一个定时器,在指定时间后执行回调函数。
*
*
* @param timeInSeconds - 延迟时间(秒)
* @param repeats - 是否重复执行默认为false
* @param context - 回调函数的上下文默认为null
* @param onTime - 定时器触发时的回调函数
* @returns 创建的定时器实例
*
* @example
* ```typescript
* // 一次性定时器
* Core.schedule(1.0, false, null, (timer) => {
* console.log("1秒后执行一次");
* });
*
* // 重复定时器
* Core.schedule(0.5, true, null, (timer) => {
* console.log("每0.5秒执行一次");
* });
* ```
*/
public static schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean = false, context?: TContext, onTime?: (timer: ITimer<TContext>) => void): Timer<TContext> {
if (!onTime) {
@@ -359,18 +399,9 @@ export class Core {
return this._instance._timerManager.schedule(timeInSeconds, repeats, context as TContext, onTime);
}
/**
* 获取ECS流式API
*
* @returns ECS API实例如果未初始化则返回null
*/
public static get ecsAPI(): ECSFluentAPI | null {
return this._instance?._ecsAPI || null;
}
/**
* 启用调试功能
*
*
* @param config 调试配置
*/
public static enableDebug(config: IECSDebugConfig): void {
@@ -408,7 +439,7 @@ export class Core {
/**
* 获取调试数据
*
*
* @returns 当前调试数据如果调试未启用则返回null
*/
public static getDebugData(): unknown {
@@ -421,118 +452,30 @@ export class Core {
/**
* 检查调试是否启用
*
*
* @returns 调试状态
*/
public static get isDebugEnabled(): boolean {
return this._instance?._config.debugConfig?.enabled || false;
}
/**
* 获取WorldManager实例
*
* @param config 可选的WorldManager配置用于覆盖默认配置
* @returns WorldManager实例如果未初始化则自动创建
*/
public static getWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
if (!this._instance) {
throw new Error("Core实例未创建请先调用Core.create()");
}
if (!this._instance._worldManager) {
// 多World模式的配置用户主动获取WorldManager
const defaultConfig = {
maxWorlds: 50,
autoCleanup: true,
cleanupInterval: 60000,
debug: this._instance._config.debug
};
this._instance._worldManager = WorldManager.getInstance({
...defaultConfig,
...config // 用户传入的配置会覆盖默认配置
});
}
return this._instance._worldManager;
}
/**
* 启用World管理
*
* 显式启用World功能用于多房间/多世界架构
*
* @param config 可选的WorldManager配置用于覆盖默认配置
*/
public static enableWorldManager(config?: Partial<IWorldManagerConfig>): WorldManager {
return this.getWorldManager(config);
}
/**
* 确保默认World存在
*
* 内部方法用于懒初始化默认World
*/
private ensureDefaultWorld(): void {
if (!this._worldManager) {
this._worldManager = WorldManager.getInstance({
maxWorlds: 1, // 单场景用户只需要1个World
autoCleanup: false, // 单场景不需要自动清理
cleanupInterval: 0, // 禁用清理定时器
debug: this._config.debug
});
}
// 检查默认World是否存在
if (!this._worldManager.getWorld(Core.DEFAULT_WORLD_ID)) {
this._worldManager.createWorld(Core.DEFAULT_WORLD_ID, {
name: 'DefaultWorld',
maxScenes: 1,
autoCleanup: false
});
this._worldManager.setWorldActive(Core.DEFAULT_WORLD_ID, true);
}
}
/**
* 场景切换回调
*
* 在场景切换时调用,用于重置时间系统等。
*/
public onSceneChanged() {
Time.sceneChanged();
// 获取当前Scene从默认World
const currentScene = Core.getScene();
// 初始化ECS API如果场景支持
if (currentScene && currentScene.querySystem && currentScene.eventSystem) {
this._ecsAPI = createECSAPI(currentScene, currentScene.querySystem, currentScene.eventSystem);
}
// 延迟调试管理器通知,避免在场景初始化过程中干扰属性
if (this._debugManager) {
queueMicrotask(() => {
this._debugManager?.onSceneChanged();
});
}
}
/**
* 初始化核心系统
*
*
* 执行核心系统的初始化逻辑。
*/
protected initialize() {
// 核心系统初始化
Core._logger.info('Core initialized', {
debug: this.debug,
entitySystemsEnabled: Core.entitySystemsEnabled,
debugEnabled: this._config.debugConfig?.enabled || false
});
}
/**
* 内部更新方法
*
*
* @param deltaTime - 帧时间间隔(秒)
*/
private updateInternal(deltaTime: number): void {
@@ -560,26 +503,8 @@ export class Core {
// 更新对象池管理器
this._poolManager.update();
// 更新所有World
if (this._worldManager) {
const worldsStartTime = this._performanceMonitor.startMonitoring('Worlds.update');
const activeWorlds = this._worldManager.getActiveWorlds();
let totalWorldEntities = 0;
for (const world of activeWorlds) {
// 更新World的全局System
world.updateGlobalSystems();
// 更新World中的所有Scene
world.updateScenes();
// 统计实体数量(用于性能监控)
const worldStats = world.getStats();
totalWorldEntities += worldStats.totalEntities;
}
this._performanceMonitor.endMonitoring('Worlds.update', worldsStartTime, totalWorldEntities);
}
// 更新场景
this._sceneManager.update();
// 更新调试管理器基于FPS的数据发送
if (this._debugManager) {
@@ -589,4 +514,29 @@ export class Core {
// 结束性能监控
this._performanceMonitor.endMonitoring('Core.update', frameStartTime);
}
/**
* 销毁Core实例
*
* 清理所有资源,通常在应用程序关闭时调用。
*/
public static destroy(): void {
if (!this._instance) return;
// 停止调试管理器
if (this._instance._debugManager) {
this._instance._debugManager.stop();
}
// 清理全局管理器
for (const manager of this._instance._globalManagers) {
manager.enabled = false;
}
this._instance._globalManagers = [];
Core._logger.info('Core destroyed');
// @ts-ignore - 清空实例引用
this._instance = null;
}
}

View File

@@ -1,6 +1,7 @@
import {Entity} from '../Entity';
import {ComponentType} from './ComponentStorage';
import {BitMask64Data, BitMask64Utils, ComponentTypeManager} from "../Utils";
import { Entity } from '../Entity';
import { ComponentType } from './ComponentStorage';
import { BitMask64Data, BitMask64Utils, ComponentTypeManager } from "../Utils";
import { BitMaskHashMap } from "../Utils/BitMaskHashMap";
/**
* 原型标识符
@@ -36,7 +37,7 @@ export interface ArchetypeQueryResult {
*/
export class ArchetypeSystem {
/** 所有原型的映射表 */
private _archetypes = new Map<number, Map<number, Archetype>>();
private _archetypes = new BitMaskHashMap<Archetype>();
/** 实体到原型的映射 */
private _entityToArchetype = new Map<Entity, Archetype>();
@@ -57,15 +58,13 @@ export class ArchetypeSystem {
const componentTypes = this.getEntityComponentTypes(entity);
const archetypeId = this.generateArchetypeId(componentTypes);
let archetype = this.getArchetype(archetypeId);
let archetype = this._archetypes.get(archetypeId);
if (!archetype) {
archetype = this.createArchetype(componentTypes);
}
archetype.entities.add(entity);
this._entityToArchetype.set(entity, archetype);
this.updateComponentIndexes(archetype, componentTypes, true);
}
/**
@@ -109,7 +108,7 @@ export class ArchetypeSystem {
}
// 获取或创建新原型
let newArchetype = this.getArchetype(newArchetypeId);
let newArchetype = this._archetypes.get(newArchetypeId);
if (!newArchetype) {
newArchetype = this.createArchetype(newComponentTypes);
}
@@ -118,12 +117,6 @@ export class ArchetypeSystem {
newArchetype.entities.add(entity);
this._entityToArchetype.set(entity, newArchetype);
// 更新组件索引
if (currentArchetype) {
this.updateComponentIndexes(currentArchetype, currentArchetype.componentTypes, false);
}
this.updateComponentIndexes(newArchetype, newComponentTypes, true);
}
/**
@@ -139,19 +132,52 @@ export class ArchetypeSystem {
let totalEntities = 0;
if (operation === 'AND') {
// 生成查询的 BitMask
const queryMask = this.generateArchetypeId(componentTypes);
// 使用 BitMask 位运算快速判断原型是否包含所有指定组件
for (const archetype of this._allArchetypes) {
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
if (componentTypes.length === 0) {
for (const archetype of this._allArchetypes) {
matchingArchetypes.push(archetype);
totalEntities += archetype.entities.size;
}
return { archetypes: matchingArchetypes, totalEntities };
}
if (componentTypes.length === 1) {
const archetypes = this._componentToArchetypes.get(componentTypes[0]);
if (archetypes) {
for (const archetype of archetypes) {
matchingArchetypes.push(archetype);
totalEntities += archetype.entities.size;
}
}
return { archetypes: matchingArchetypes, totalEntities };
}
let smallestSet: Set<Archetype> | undefined;
let smallestSize = Infinity;
for (const componentType of componentTypes) {
const archetypes = this._componentToArchetypes.get(componentType);
if (!archetypes || archetypes.size === 0) {
return { archetypes: [], totalEntities: 0 };
}
if (archetypes.size < smallestSize) {
smallestSize = archetypes.size;
smallestSet = archetypes;
}
}
const queryMask = this.generateArchetypeId(componentTypes);
if (smallestSet) {
for (const archetype of smallestSet) {
if (BitMask64Utils.hasAll(archetype.id, queryMask)) {
matchingArchetypes.push(archetype);
totalEntities += archetype.entities.size;
}
}
}
} else {
const foundArchetypes = new Set<Archetype>();
for (const componentType of componentTypes) {
const archetypes = this._componentToArchetypes.get(componentType);
if (archetypes) {
@@ -160,13 +186,13 @@ export class ArchetypeSystem {
}
}
}
for (const archetype of foundArchetypes) {
matchingArchetypes.push(archetype);
totalEntities += archetype.entities.size;
}
}
return {
archetypes: matchingArchetypes,
totalEntities
@@ -215,25 +241,14 @@ export class ArchetypeSystem {
this._entityComponentTypesCache.clear();
this._allArchetypes = [];
}
/**
* 根据原型ID获取原型
* @param archetypeId
* @private
*/
private getArchetype(archetypeId: ArchetypeId): Archetype | undefined {
return this._archetypes.get(archetypeId.hi)?.get(archetypeId.lo);
}
/**
* 更新所有原型数组
*/
private updateAllArchetypeArrays(): void {
this._allArchetypes = [];
for (const [, innerMap] of this._archetypes) {
for (const [, archetype] of innerMap) {
this._allArchetypes.push(archetype);
}
for (let archetype of this._archetypes.values()) {
this._allArchetypes.push(archetype);
}
}
@@ -262,45 +277,26 @@ export class ArchetypeSystem {
*/
private createArchetype(componentTypes: ComponentType[]): Archetype {
const id = this.generateArchetypeId(componentTypes);
const archetype: Archetype = {
id,
componentTypes: [...componentTypes],
entities: new Set<Entity>()
};
// 存储原型ID - 原型
let archetypeGroup = this._archetypes.get(id.hi);
if (!archetypeGroup) {
archetypeGroup = new Map<number, Archetype>();
this._archetypes.set(id.hi, archetypeGroup);
}
archetypeGroup.set(id.lo, archetype);
// 更新数组
this._archetypes.set(id,archetype);
this.updateAllArchetypeArrays();
return archetype;
}
/**
* 更新组件索引
*/
private updateComponentIndexes(archetype: Archetype, componentTypes: ComponentType[], add: boolean): void {
for (const componentType of componentTypes) {
let archetypes = this._componentToArchetypes.get(componentType);
if (!archetypes) {
archetypes = new Set();
this._componentToArchetypes.set(componentType, archetypes);
}
if (add) {
archetypes.add(archetype);
} else {
archetypes.delete(archetype);
if (archetypes.size === 0) {
this._componentToArchetypes.delete(componentType);
}
}
archetypes.add(archetype);
}
return archetype;
}
}

View File

@@ -8,24 +8,41 @@ export class ComponentPool<T extends Component> {
private createFn: () => T;
private resetFn?: (component: T) => void;
private maxSize: number;
private minSize: number;
private growthFactor: number;
private stats = {
totalCreated: 0,
totalAcquired: 0,
totalReleased: 0
};
constructor(
createFn: () => T,
resetFn?: (component: T) => void,
maxSize: number = 1000
maxSize: number = 1000,
minSize: number = 10,
growthFactor: number = 1.5
) {
this.createFn = createFn;
this.resetFn = resetFn;
this.maxSize = maxSize;
this.minSize = Math.max(1, minSize);
this.growthFactor = Math.max(1.1, growthFactor);
}
/**
* 获取一个组件实例
*/
acquire(): T {
this.stats.totalAcquired++;
if (this.pool.length > 0) {
return this.pool.pop()!;
}
this.stats.totalCreated++;
return this.createFn();
}
@@ -33,20 +50,41 @@ export class ComponentPool<T extends Component> {
* 释放一个组件实例回池中
*/
release(component: T): void {
if (this.pool.length < this.maxSize) {
if (this.resetFn) {
this.resetFn(component);
}
this.pool.push(component);
this.stats.totalReleased++;
if (this.pool.length >= this.maxSize) {
return;
}
if (this.resetFn) {
this.resetFn(component);
}
this.pool.push(component);
}
/**
* 预填充对象池
*/
prewarm(count: number): void {
for (let i = 0; i < count && this.pool.length < this.maxSize; i++) {
this.pool.push(this.createFn());
const targetCount = Math.min(count, this.maxSize);
for (let i = this.pool.length; i < targetCount; i++) {
const component = this.createFn();
if (this.resetFn) {
this.resetFn(component);
}
this.pool.push(component);
this.stats.totalCreated++;
}
}
/**
* 自动收缩池大小
*/
shrink(): void {
while (this.pool.length > this.minSize) {
this.pool.pop();
}
}
@@ -70,6 +108,35 @@ export class ComponentPool<T extends Component> {
getMaxSize(): number {
return this.maxSize;
}
/**
* 获取统计信息
*/
getStats() {
const hitRate = this.stats.totalAcquired === 0
? 0
: (this.stats.totalAcquired - this.stats.totalCreated) / this.stats.totalAcquired;
return {
totalCreated: this.stats.totalCreated,
totalAcquired: this.stats.totalAcquired,
totalReleased: this.stats.totalReleased,
hitRate: hitRate,
currentSize: this.pool.length,
maxSize: this.maxSize,
minSize: this.minSize,
utilizationRate: this.pool.length / this.maxSize
};
}
}
/**
* 组件使用追踪
*/
interface ComponentUsageTracker {
createCount: number;
releaseCount: number;
lastAccessTime: number;
}
/**
@@ -78,6 +145,10 @@ export class ComponentPool<T extends Component> {
export class ComponentPoolManager {
private static instance: ComponentPoolManager;
private pools = new Map<string, ComponentPool<any>>();
private usageTracker = new Map<string, ComponentUsageTracker>();
private autoCleanupInterval = 60000;
private lastCleanupTime = 0;
private constructor() {}
@@ -95,9 +166,16 @@ export class ComponentPoolManager {
componentName: string,
createFn: () => T,
resetFn?: (component: T) => void,
maxSize?: number
maxSize?: number,
minSize?: number
): void {
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize));
this.pools.set(componentName, new ComponentPool(createFn, resetFn, maxSize, minSize));
this.usageTracker.set(componentName, {
createCount: 0,
releaseCount: 0,
lastAccessTime: Date.now()
});
}
/**
@@ -105,6 +183,9 @@ export class ComponentPoolManager {
*/
acquireComponent<T extends Component>(componentName: string): T | null {
const pool = this.pools.get(componentName);
this.trackUsage(componentName, 'create');
return pool ? pool.acquire() : null;
}
@@ -113,11 +194,71 @@ export class ComponentPoolManager {
*/
releaseComponent<T extends Component>(componentName: string, component: T): void {
const pool = this.pools.get(componentName);
this.trackUsage(componentName, 'release');
if (pool) {
pool.release(component);
}
}
/**
* 追踪使用情况
*/
private trackUsage(componentName: string, action: 'create' | 'release'): void {
let tracker = this.usageTracker.get(componentName);
if (!tracker) {
tracker = {
createCount: 0,
releaseCount: 0,
lastAccessTime: Date.now()
};
this.usageTracker.set(componentName, tracker);
}
if (action === 'create') {
tracker.createCount++;
} else {
tracker.releaseCount++;
}
tracker.lastAccessTime = Date.now();
}
/**
* 自动清理(定期调用)
*/
public update(): void {
const now = Date.now();
if (now - this.lastCleanupTime < this.autoCleanupInterval) {
return;
}
for (const [name, tracker] of this.usageTracker.entries()) {
const inactive = now - tracker.lastAccessTime > 120000;
if (inactive) {
const pool = this.pools.get(name);
if (pool) {
pool.shrink();
}
}
}
this.lastCleanupTime = now;
}
/**
* 获取热点组件列表
*/
public getHotComponents(threshold: number = 100): string[] {
return Array.from(this.usageTracker.entries())
.filter(([_, tracker]) => tracker.createCount > threshold)
.map(([name]) => name);
}
/**
* 预热所有池
*/
@@ -137,10 +278,28 @@ export class ComponentPoolManager {
}
/**
* 重置管理器,移除所有注册的池
* 重置管理器
*/
reset(): void {
this.pools.clear();
this.usageTracker.clear();
}
/**
* 获取全局统计信息
*/
getGlobalStats() {
const stats: any[] = [];
for (const [name, pool] of this.pools.entries()) {
stats.push({
componentName: name,
poolStats: pool.getStats(),
usage: this.usageTracker.get(name)
});
}
return stats;
}
/**
@@ -158,7 +317,7 @@ export class ComponentPoolManager {
}
/**
* 获取池利用率信息(用于调试)
* 获取池利用率信息
*/
getPoolUtilization(): Map<string, { used: number; total: number; utilization: number }> {
const utilization = new Map();
@@ -167,7 +326,7 @@ export class ComponentPoolManager {
const maxSize = pool.getMaxSize();
const used = maxSize - available;
const utilRate = maxSize > 0 ? (used / maxSize * 100) : 0;
utilization.set(name, {
used: used,
total: maxSize,
@@ -183,11 +342,11 @@ export class ComponentPoolManager {
getComponentUtilization(componentName: string): number {
const pool = this.pools.get(componentName);
if (!pool) return 0;
const available = pool.getAvailableCount();
const maxSize = pool.getMaxSize();
const used = maxSize - available;
return maxSize > 0 ? (used / maxSize * 100) : 0;
}
}

View File

@@ -54,10 +54,7 @@ export class ComponentRegistry {
const typeName = getComponentTypeName(componentType);
throw new Error(`Component type ${typeName} is not registered`);
}
const mask: BitMask64Data = { lo: 0, hi: 0 };
BitMask64Utils.setBitExtended(mask, bitIndex);
return mask;
return BitMask64Utils.create(bitIndex);
}
/**

View File

@@ -0,0 +1,404 @@
/**
* 类型安全的Query查询系统
*
* 提供完整的TypeScript类型推断在编译时确保类型安全
*/
import type { Entity } from '../../Entity';
import type { ComponentConstructor, ComponentInstance, ComponentTypeMap } from '../../../Types/TypeHelpers';
import { Matcher, type QueryCondition } from '../../Utils/Matcher';
/**
* 类型安全的查询结果
*
* 根据查询条件自动推断实体必定拥有的组件类型
*/
export class TypedQueryResult<TAll extends readonly ComponentConstructor[]> {
private _entities: readonly Entity[];
private _componentTypes: TAll;
constructor(entities: readonly Entity[], componentTypes: TAll) {
this._entities = entities;
this._componentTypes = componentTypes;
}
/**
* 获取实体列表
*/
get entities(): readonly Entity[] {
return this._entities;
}
/**
* 实体数量
*/
get length(): number {
return this._entities.length;
}
/**
* 遍历所有实体
*
* @example
* ```typescript
* query.forEach((entity) => {
* // entity.getComponent返回类型自动推断
* const pos = entity.getComponent(Position); // Position类型
* const vel = entity.getComponent(Velocity); // Velocity类型
* });
* ```
*/
forEach(callback: (entity: Entity, index: number) => void): void {
this._entities.forEach(callback);
}
/**
* 映射转换实体
*/
map<R>(callback: (entity: Entity, index: number) => R): R[] {
return this._entities.map(callback);
}
/**
* 过滤实体
*/
filter(predicate: (entity: Entity, index: number) => boolean): TypedQueryResult<TAll> {
return new TypedQueryResult(this._entities.filter(predicate), this._componentTypes);
}
/**
* 查找第一个匹配的实体
*/
find(predicate: (entity: Entity, index: number) => boolean): Entity | undefined {
return this._entities.find(predicate);
}
/**
* 检查是否存在匹配的实体
*/
some(predicate: (entity: Entity, index: number) => boolean): boolean {
return this._entities.some(predicate);
}
/**
* 检查是否所有实体都匹配
*/
every(predicate: (entity: Entity, index: number) => boolean): boolean {
return this._entities.every(predicate);
}
/**
* 获取指定索引的实体
*/
get(index: number): Entity | undefined {
return this._entities[index];
}
/**
* 获取第一个实体
*/
get first(): Entity | undefined {
return this._entities[0];
}
/**
* 获取最后一个实体
*/
get last(): Entity | undefined {
return this._entities[this._entities.length - 1];
}
/**
* 检查查询结果是否为空
*/
get isEmpty(): boolean {
return this._entities.length === 0;
}
/**
* 转换为数组
*/
toArray(): Entity[] {
return [...this._entities];
}
/**
* 获取组件类型信息(用于调试)
*/
getComponentTypes(): readonly ComponentConstructor[] {
return this._componentTypes;
}
/**
* 迭代器支持
*/
[Symbol.iterator](): Iterator<Entity> {
return this._entities[Symbol.iterator]();
}
}
/**
* 类型安全的查询构建器
*
* 支持链式调用,自动推断查询结果的类型
*
* @example
* ```typescript
* // 基础查询
* const query = new TypedQueryBuilder()
* .withAll(Position, Velocity)
* .build();
*
* // 复杂查询
* const complexQuery = new TypedQueryBuilder()
* .withAll(Transform, Renderer)
* .withAny(BoxCollider, CircleCollider)
* .withNone(Disabled)
* .withTag(EntityTags.Enemy)
* .build();
* ```
*/
export class TypedQueryBuilder<
TAll extends readonly ComponentConstructor[] = [],
TAny extends readonly ComponentConstructor[] = [],
TNone extends readonly ComponentConstructor[] = []
> {
private _all: TAll;
private _any: TAny;
private _none: TNone;
private _tag?: number;
private _name?: string;
constructor(
all?: TAll,
any?: TAny,
none?: TNone,
tag?: number,
name?: string
) {
this._all = (all || []) as TAll;
this._any = (any || []) as TAny;
this._none = (none || []) as TNone;
this._tag = tag;
this._name = name;
}
/**
* 要求实体拥有所有指定的组件
*
* @param types 组件类型
* @returns 新的查询构建器,类型参数更新
*/
withAll<TNewAll extends readonly ComponentConstructor[]>(
...types: TNewAll
): TypedQueryBuilder<
readonly [...TAll, ...TNewAll],
TAny,
TNone
> {
return new TypedQueryBuilder(
[...this._all, ...types] as readonly [...TAll, ...TNewAll],
this._any,
this._none,
this._tag,
this._name
);
}
/**
* 要求实体至少拥有一个指定的组件
*
* @param types 组件类型
* @returns 新的查询构建器
*/
withAny<TNewAny extends readonly ComponentConstructor[]>(
...types: TNewAny
): TypedQueryBuilder<
TAll,
readonly [...TAny, ...TNewAny],
TNone
> {
return new TypedQueryBuilder(
this._all,
[...this._any, ...types] as readonly [...TAny, ...TNewAny],
this._none,
this._tag,
this._name
);
}
/**
* 排除拥有指定组件的实体
*
* @param types 组件类型
* @returns 新的查询构建器
*/
withNone<TNewNone extends readonly ComponentConstructor[]>(
...types: TNewNone
): TypedQueryBuilder<
TAll,
TAny,
readonly [...TNone, ...TNewNone]
> {
return new TypedQueryBuilder(
this._all,
this._any,
[...this._none, ...types] as readonly [...TNone, ...TNewNone],
this._tag,
this._name
);
}
/**
* 按标签过滤实体
*
* @param tag 标签值
* @returns 新的查询构建器
*/
withTag(tag: number): TypedQueryBuilder<TAll, TAny, TNone> {
return new TypedQueryBuilder(
this._all,
this._any,
this._none,
tag,
this._name
);
}
/**
* 按名称过滤实体
*
* @param name 实体名称
* @returns 新的查询构建器
*/
withName(name: string): TypedQueryBuilder<TAll, TAny, TNone> {
return new TypedQueryBuilder(
this._all,
this._any,
this._none,
this._tag,
name
);
}
/**
* 构建Matcher对象
*
* @returns Matcher实例用于传统查询API
*/
buildMatcher(): Matcher {
let matcher = Matcher.complex();
if (this._all.length > 0) {
matcher = matcher.all(...(this._all as unknown as ComponentConstructor[]));
}
if (this._any.length > 0) {
matcher = matcher.any(...(this._any as unknown as ComponentConstructor[]));
}
if (this._none.length > 0) {
matcher = matcher.none(...(this._none as unknown as ComponentConstructor[]));
}
if (this._tag !== undefined) {
matcher = matcher.withTag(this._tag);
}
if (this._name !== undefined) {
matcher = matcher.withName(this._name);
}
return matcher;
}
/**
* 获取查询条件
*
* @returns 查询条件对象
*/
getCondition(): QueryCondition {
return {
all: [...this._all] as ComponentConstructor[],
any: [...this._any] as ComponentConstructor[],
none: [...this._none] as ComponentConstructor[],
tag: this._tag,
name: this._name
};
}
/**
* 获取required组件类型用于类型推断
*/
getRequiredTypes(): TAll {
return this._all;
}
/**
* 克隆查询构建器
*/
clone(): TypedQueryBuilder<TAll, TAny, TNone> {
return new TypedQueryBuilder(
[...this._all] as unknown as TAll,
[...this._any] as unknown as TAny,
[...this._none] as unknown as TNone,
this._tag,
this._name
);
}
}
/**
* 创建类型安全的查询构建器
*
* @example
* ```typescript
* const query = createQuery()
* .withAll(Position, Velocity)
* .withNone(Disabled);
*
* // 在System或Scene中使用
* const entities = scene.query(query);
* entities.forEach(entity => {
* const pos = entity.getComponent(Position); // 自动推断为Position
* const vel = entity.getComponent(Velocity); // 自动推断为Velocity
* });
* ```
*/
export function createQuery(): TypedQueryBuilder<[], [], []> {
return new TypedQueryBuilder();
}
/**
* 创建单组件查询的便捷方法
*
* @param componentType 组件类型
* @returns 查询构建器
*
* @example
* ```typescript
* const healthEntities = queryFor(HealthComponent);
* ```
*/
export function queryFor<T extends ComponentConstructor>(
componentType: T
): TypedQueryBuilder<readonly [T], [], []> {
return new TypedQueryBuilder([componentType] as readonly [T]);
}
/**
* 创建多组件查询的便捷方法
*
* @param types 组件类型数组
* @returns 查询构建器
*
* @example
* ```typescript
* const movableEntities = queryForAll(Position, Velocity);
* ```
*/
export function queryForAll<T extends readonly ComponentConstructor[]>(
...types: T
): TypedQueryBuilder<T, [], []> {
return new TypedQueryBuilder(types);
}

View File

@@ -4,9 +4,8 @@ import { ComponentRegistry, ComponentType } from './ComponentStorage';
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
import { createLogger } from '../../Utils/Logger';
import { getComponentTypeName } from '../Decorators';
import { ComponentPoolManager } from './ComponentPool';
import { ArchetypeSystem, Archetype, ArchetypeQueryResult } from './ArchetypeSystem';
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
import { ComponentTypeManager } from "../Utils";
/**
* 查询条件类型
@@ -78,19 +77,16 @@ export class QuerySystem {
private entities: Entity[] = [];
private entityIndex: EntityIndex;
// 版本号,用于缓存失效
private _version = 0;
// 查询缓存系统
private queryCache = new Map<string, QueryCacheEntry>();
private cacheMaxSize = 1000;
private cacheTimeout = 5000; // 5秒缓存过期
private cacheTimeout = 5000;
private componentMaskCache = new Map<string, BitMask64Data>();
private archetypeSystem: ArchetypeSystem;
// 性能统计
private queryStats = {
totalQueries: 0,
cacheHits: 0,
@@ -100,6 +96,9 @@ export class QuerySystem {
dirtyChecks: 0
};
private resultArrayPool: Entity[][] = [];
private poolMaxSize = 50;
constructor() {
this.entityIndex = {
byTag: new Map(),
@@ -109,7 +108,19 @@ export class QuerySystem {
this.archetypeSystem = new ArchetypeSystem();
}
private acquireResultArray(): Entity[] {
if (this.resultArrayPool.length > 0) {
return this.resultArrayPool.pop()!;
}
return [];
}
private releaseResultArray(array: Entity[]): void {
if (this.resultArrayPool.length < this.poolMaxSize) {
array.length = 0;
this.resultArrayPool.push(array);
}
}
/**
* 设置实体列表并重建索引
@@ -460,18 +471,21 @@ export class QuerySystem {
this.queryStats.archetypeHits++;
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
const entities: Entity[] = [];
const entities = this.acquireResultArray();
for (const archetype of archetypeResult.archetypes) {
for (const entity of archetype.entities) {
entities.push(entity);
}
}
this.addToCache(cacheKey, entities);
const frozenEntities = [...entities];
this.releaseResultArray(entities);
this.addToCache(cacheKey, frozenEntities);
return {
entities,
count: entities.length,
entities: frozenEntities,
count: frozenEntities.length,
executionTime: performance.now() - startTime,
fromCache: false
};
@@ -787,8 +801,7 @@ export class QuerySystem {
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
// 生成缓存键
const cacheKey = componentTypes.map(t => {
const name = getComponentTypeName(t);
return name;
return getComponentTypeName(t);
}).sort().join(',');
// 检查缓存
@@ -797,27 +810,10 @@ export class QuerySystem {
return cached;
}
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
let hasValidComponents = false;
for (const type of componentTypes) {
try {
const bitMask = ComponentRegistry.getBitMask(type);
BitMask64Utils.orInPlace(mask, bitMask);
hasValidComponents = true;
} catch (error) {
this._logger.warn(`组件类型 ${getComponentTypeName(type)} 未注册,跳过`);
}
}
// 如果没有有效的组件类型,返回一个不可能匹配的掩码
if (!hasValidComponents) {
mask = { lo: 0xFFFFFFFF, hi: 0xFFFFFFFF };
}
let mask = ComponentTypeManager.instance.getEntityBits(componentTypes);
// 缓存结果
this.componentMaskCache.set(cacheKey, mask);
return mask;
this.componentMaskCache.set(cacheKey, mask.getValue());
return mask.getValue();
}
/**

View File

@@ -191,7 +191,7 @@ export class Entity {
const maxBitIndex = ComponentRegistry.getRegisteredCount();
for (let bitIndex = 0; bitIndex < maxBitIndex; bitIndex++) {
if (BitMask64Utils.getBitExtended(mask, bitIndex)) {
if (BitMask64Utils.getBit(mask, bitIndex)) {
const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex);
if (componentType) {
let component: Component | null = null;
@@ -346,13 +346,19 @@ export class Entity {
/**
* 创建并添加组件
*
* @param componentType - 组件类型
*
* @param componentType - 组件类型构造函数
* @param args - 组件构造函数参数
* @returns 创建的组件实例
*
* @example
* ```typescript
* const position = entity.createComponent(Position, 100, 200);
* const health = entity.createComponent(Health, 100);
* ```
*/
public createComponent<T extends Component>(
componentType: ComponentType<T>,
componentType: ComponentType<T>,
...args: any[]
): T {
const component = new componentType(...args);
@@ -387,10 +393,16 @@ export class Entity {
/**
* 添加组件到实体
*
*
* @param component - 要添加的组件实例
* @returns 添加的组件实例
* @throws {Error} 如果组件类型已存在
* @throws {Error} 如果实体已存在该类型的组件
*
* @example
* ```typescript
* const position = new Position(100, 200);
* entity.addComponent(position);
* ```
*/
public addComponent<T extends Component>(component: T): T {
const componentType = component.constructor as ComponentType<T>;
@@ -429,8 +441,17 @@ export class Entity {
/**
* 获取指定类型的组件
*
* @param type - 组件类型
* @returns 组件实例null
* @param type - 组件类型构造函数
* @returns 组件实例,如果不存在则返回null
*
* @example
* ```typescript
* const position = entity.getComponent(Position);
* if (position) {
* position.x += 10;
* position.y += 20;
* }
* ```
*/
public getComponent<T extends Component>(type: ComponentType<T>): T | null {
// 快速检查:位掩码
@@ -442,7 +463,7 @@ export class Entity {
if (this.scene?.componentStorageManager) {
const component = this.scene.componentStorageManager.getComponent(this.id, type);
if (component) {
return component;
return component as T;
}
}
@@ -454,10 +475,18 @@ export class Entity {
/**
* 检查实体是否有指定类型的组件
*
* @param type - 组件类型
* @returns 如果有该组件返回true
* 检查实体是否有指定类型的组件
*
* @param type - 组件类型构造函数
* @returns 如果实体拥有该组件返回true否则返回false
*
* @example
* ```typescript
* if (entity.hasComponent(Position)) {
* const position = entity.getComponent(Position)!;
* position.x += 10;
* }
* ```
*/
public hasComponent<T extends Component>(type: ComponentType<T>): boolean {
if (!ComponentRegistry.isRegistered(type)) {
@@ -470,13 +499,22 @@ export class Entity {
/**
* 获取或创建指定类型的组件
*
* @param type - 组件类型
* @param args - 组件构造函数参数(仅在创建时使用)
*
* 如果组件已存在则返回现有组件,否则创建新组件并添加到实体
*
* @param type - 组件类型构造函数
* @param args - 组件构造函数参数(仅在创建新组件时使用)
* @returns 组件实例
*
* @example
* ```typescript
* // 确保实体拥有Position组件
* const position = entity.getOrCreateComponent(Position, 0, 0);
* position.x = 100;
* ```
*/
public getOrCreateComponent<T extends Component>(
type: ComponentType<T>,
type: ComponentType<T>,
...args: any[]
): T {
let component = this.getComponent(type);
@@ -504,7 +542,7 @@ export class Entity {
this._localComponents.delete(componentType);
// 更新位掩码
BitMask64Utils.clearBitExtended(this._componentMask, bitIndex);
BitMask64Utils.clearBit(this._componentMask, bitIndex);
// 使缓存失效
this._componentCache = null;
@@ -836,8 +874,8 @@ export class Entity {
/**
* 销毁实体
*
* 移除所有组件、子实体并标记为已销毁
*
* 移除所有组件、子实体并标记为已销毁
*/
public destroy(): void {
if (this._isDestroyed) {
@@ -845,29 +883,66 @@ export class Entity {
}
this._isDestroyed = true;
const childrenToDestroy = [...this._children];
for (const child of childrenToDestroy) {
child.destroy();
}
if (this._parent) {
this._parent.removeChild(this);
}
this.removeAllComponents();
if (this.scene) {
if (this.scene.querySystem) {
this.scene.querySystem.removeEntity(this);
}
if (this.scene.entities) {
this.scene.entities.remove(this);
}
}
}
/**
* 批量销毁所有子实体
*/
public destroyAllChildren(): void {
if (this._children.length === 0) return;
const scene = this.scene;
const toDestroy: Entity[] = [];
const collectChildren = (entity: Entity) => {
for (const child of entity._children) {
toDestroy.push(child);
collectChildren(child);
}
};
collectChildren(this);
for (const entity of toDestroy) {
entity._isDestroyed = true;
}
for (const entity of toDestroy) {
entity.removeAllComponents();
}
if (scene) {
for (const entity of toDestroy) {
scene.entities.remove(entity);
scene.querySystem.removeEntity(entity);
}
scene.clearSystemEntityCaches();
}
this._children.length = 0;
}
/**
* 比较实体
*

View File

@@ -9,7 +9,7 @@ import { TypeSafeEventSystem } from './Core/EventSystem';
/**
* 场景接口定义
*
*
* 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。
*/
export interface IScene {
@@ -18,6 +18,25 @@ export interface IScene {
*/
name: string;
/**
* 场景自定义数据
*
* 用于存储场景级别的配置和状态数据,例如:
* - 天气状态
* - 时间设置
* - 游戏难度
* - 音频配置
* - 关卡检查点
*
* @example
* ```typescript
* scene.sceneData.set('weather', 'rainy');
* scene.sceneData.set('timeOfDay', 14.5);
* scene.sceneData.set('checkpoint', { x: 100, y: 200 });
* ```
*/
readonly sceneData: Map<string, any>;
/**
* 场景中的实体集合
*/

View File

@@ -3,12 +3,16 @@ import { EntityList } from './Utils/EntityList';
import { EntityProcessorList } from './Utils/EntityProcessorList';
import { IdentifierPool } from './Utils/IdentifierPool';
import { EntitySystem } from './Systems/EntitySystem';
import { ComponentStorageManager } from './Core/ComponentStorage';
import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage';
import { QuerySystem } from './Core/QuerySystem';
import { TypeSafeEventSystem } from './Core/EventSystem';
import { EventBus } from './Core/EventBus';
import { IScene, ISceneConfig } from './IScene';
import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decorators';
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer';
import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer';
import { ComponentPoolManager } from './Core/ComponentPool';
/**
* 游戏场景默认实现类
@@ -19,14 +23,21 @@ import { getComponentInstanceTypeName, getSystemInstanceTypeName } from './Decor
export class Scene implements IScene {
/**
* 场景名称
*
*
* 用于标识和调试的友好名称。
*/
public name: string = "";
/**
* 场景自定义数据
*
* 用于存储场景级别的配置和状态数据。
*/
public readonly sceneData: Map<string, any> = new Map();
/**
* 场景中的实体集合
*
*
* 管理场景内所有实体的生命周期。
*/
public readonly entities: EntityList;
@@ -175,14 +186,13 @@ export class Scene implements IScene {
* 更新场景
*/
public update() {
// 更新实体列表(处理延迟操作)
ComponentPoolManager.getInstance().update();
this.entities.updateLists();
// 更新实体处理器
if (this.entityProcessors != null)
this.entityProcessors.update();
// 更新实体处理器后处理
if (this.entityProcessors != null)
this.entityProcessors.lateUpdate();
}
@@ -263,13 +273,35 @@ export class Scene implements IScene {
}
/**
* 批量销毁实体
*/
public destroyEntities(entities: Entity[]): void {
if (entities.length === 0) return;
for (const entity of entities) {
entity._isDestroyed = true;
}
for (const entity of entities) {
entity.removeAllComponents();
}
for (const entity of entities) {
this.entities.remove(entity);
this.querySystem.removeEntity(entity);
}
this.querySystem.clearCache();
this.clearSystemEntityCaches();
}
/**
* 从场景中删除所有实体
*/
public destroyAllEntities() {
this.entities.removeAllEntities();
// 清理查询系统中的实体引用和缓存
this.querySystem.setEntities([]);
}
@@ -319,6 +351,70 @@ export class Scene implements IScene {
return this.findEntitiesByTag(tag);
}
/**
* 查询拥有所有指定组件的实体
*
* @param componentTypes - 组件类型数组
* @returns 查询结果
*
* @example
* ```typescript
* const result = scene.queryAll(Position, Velocity);
* for (const entity of result.entities) {
* const pos = entity.getComponent(Position);
* const vel = entity.getComponent(Velocity);
* }
* ```
*/
public queryAll(...componentTypes: any[]): { entities: readonly Entity[] } {
return this.querySystem.queryAll(...componentTypes);
}
/**
* 查询拥有任意一个指定组件的实体
*
* @param componentTypes - 组件类型数组
* @returns 查询结果
*/
public queryAny(...componentTypes: any[]): { entities: readonly Entity[] } {
return this.querySystem.queryAny(...componentTypes);
}
/**
* 查询不包含指定组件的实体
*
* @param componentTypes - 组件类型数组
* @returns 查询结果
*/
public queryNone(...componentTypes: any[]): { entities: readonly Entity[] } {
return this.querySystem.queryNone(...componentTypes);
}
/**
* 创建类型安全的查询构建器
*
* @returns 查询构建器,支持链式调用
*
* @example
* ```typescript
* // 使用查询构建器
* const matcher = scene.query()
* .withAll(Position, Velocity)
* .withNone(Disabled)
* .buildMatcher();
*
* // 在System中使用
* class MovementSystem extends EntitySystem {
* constructor() {
* super(matcher);
* }
* }
* ```
*/
public query(): TypedQueryBuilder {
return new TypedQueryBuilder();
}
/**
* 在场景中添加一个EntitySystem处理器
* @param processor 处理器
@@ -424,4 +520,201 @@ export class Scene implements IScene {
componentStats: this.componentStorageManager.getAllStats()
};
}
/**
* 序列化场景
*
* 将场景及其所有实体、组件序列化为JSON字符串或二进制Buffer
*
* @param options 序列化选项
* @returns 序列化后的数据JSON字符串或二进制Buffer
*
* @example
* ```typescript
* // JSON格式
* const jsonData = scene.serialize({
* format: 'json',
* pretty: true
* });
*
* // 二进制格式(更小、更快)
* const binaryData = scene.serialize({
* format: 'binary'
* });
* ```
*/
public serialize(options?: SceneSerializationOptions): string | Buffer {
return SceneSerializer.serialize(this, options);
}
/**
* 反序列化场景
*
* 从序列化数据恢复场景状态
*
* @param saveData 序列化的数据JSON字符串或二进制Buffer
* @param options 反序列化选项
*
* @example
* ```typescript
* // 从JSON恢复自动从ComponentRegistry获取组件类型
* scene.deserialize(jsonData, {
* strategy: 'replace'
* });
*
* // 从二进制恢复
* scene.deserialize(binaryData, {
* strategy: 'replace'
* });
* ```
*/
public deserialize(saveData: string | Buffer, options?: SceneDeserializationOptions): void {
SceneSerializer.deserialize(this, saveData, options);
}
// ==================== 增量序列化 API ====================
/** 增量序列化的基础快照 */
private _incrementalBaseSnapshot?: any;
/**
* 创建增量序列化的基础快照
*
* 在需要进行增量序列化前,先调用此方法创建基础快照
*
* @param options 序列化选项
*
* @example
* ```typescript
* // 创建基础快照
* scene.createIncrementalSnapshot();
*
* // 进行一些修改...
* entity.addComponent(new PositionComponent(100, 200));
*
* // 计算增量变更
* const incremental = scene.serializeIncremental();
* ```
*/
public createIncrementalSnapshot(options?: IncrementalSerializationOptions): void {
this._incrementalBaseSnapshot = IncrementalSerializer.createSnapshot(this, options);
}
/**
* 增量序列化场景
*
* 只序列化相对于基础快照的变更部分
*
* @param options 序列化选项
* @returns 增量快照对象
*
* @example
* ```typescript
* // 创建基础快照
* scene.createIncrementalSnapshot();
*
* // 修改场景
* const entity = scene.createEntity('NewEntity');
* entity.addComponent(new PositionComponent(50, 100));
*
* // 获取增量变更
* const incremental = scene.serializeIncremental();
* console.log(`变更数量: ${incremental.entityChanges.length}`);
*
* // 序列化为JSON
* const json = IncrementalSerializer.serializeIncremental(incremental);
* ```
*/
public serializeIncremental(options?: IncrementalSerializationOptions): IncrementalSnapshot {
if (!this._incrementalBaseSnapshot) {
throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照');
}
return IncrementalSerializer.computeIncremental(
this,
this._incrementalBaseSnapshot,
options
);
}
/**
* 应用增量变更到场景
*
* @param incremental 增量快照数据IncrementalSnapshot对象、JSON字符串或二进制Buffer
* @param componentRegistry 组件类型注册表(可选,默认使用全局注册表)
*
* @example
* ```typescript
* // 应用增量变更对象
* scene.applyIncremental(incrementalSnapshot);
*
* // 从JSON字符串应用
* const jsonData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'json' });
* scene.applyIncremental(jsonData);
*
* // 从二进制Buffer应用
* const binaryData = IncrementalSerializer.serializeIncremental(snapshot, { format: 'binary' });
* scene.applyIncremental(binaryData);
* ```
*/
public applyIncremental(
incremental: IncrementalSnapshot | string | Buffer,
componentRegistry?: Map<string, any>
): void {
const isSerializedData = typeof incremental === 'string' ||
(typeof Buffer !== 'undefined' && Buffer.isBuffer(incremental)) ||
incremental instanceof Uint8Array;
const snapshot = isSerializedData
? IncrementalSerializer.deserializeIncremental(incremental as string | Buffer)
: incremental as IncrementalSnapshot;
const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map<string, any>;
IncrementalSerializer.applyIncremental(this, snapshot, registry);
}
/**
* 更新增量快照基准
*
* 将当前场景状态设为新的增量序列化基准
*
* @param options 序列化选项
*
* @example
* ```typescript
* // 创建初始快照
* scene.createIncrementalSnapshot();
*
* // 进行一些修改并序列化
* const incremental1 = scene.serializeIncremental();
*
* // 更新基准,之后的增量将基于当前状态
* scene.updateIncrementalSnapshot();
*
* // 继续修改
* const incremental2 = scene.serializeIncremental();
* ```
*/
public updateIncrementalSnapshot(options?: IncrementalSerializationOptions): void {
this.createIncrementalSnapshot(options);
}
/**
* 清除增量快照
*
* 释放快照占用的内存
*/
public clearIncrementalSnapshot(): void {
this._incrementalBaseSnapshot = undefined;
}
/**
* 检查是否有增量快照
*
* @returns 如果已创建增量快照返回true
*/
public hasIncrementalSnapshot(): boolean {
return this._incrementalBaseSnapshot !== undefined;
}
}

View File

@@ -0,0 +1,238 @@
import { IScene } from './IScene';
import { ECSFluentAPI, createECSAPI } from './Core/FluentAPI';
import { Time } from '../Utils/Time';
import { Core } from '../Core';
import { createLogger } from '../Utils/Logger';
/**
* 单场景管理器
*
* 适用场景:
* - 单人游戏
* - 简单场景切换
* - 不需要多World隔离的项目
*
* 特点:
* - 轻量级,零额外开销
* - 简单直观的API
* - 支持延迟场景切换
* - 自动管理ECS API
*
* @example
* ```typescript
* // 初始化Core
* Core.create({ debug: true });
*
* // 创建场景管理器
* const sceneManager = new SceneManager();
*
* // 设置场景
* class GameScene extends Scene {
* initialize() {
* const player = this.createEntity('Player');
* player.addComponent(new Transform(100, 100));
* }
* }
*
* sceneManager.setScene(new GameScene());
*
* // 游戏循环
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime); // 更新全局服务
* sceneManager.update(); // 更新场景
* }
*
* // 延迟切换场景(下一帧生效)
* sceneManager.loadScene(new MenuScene());
* ```
*/
export class SceneManager {
/**
* 当前活跃场景
*/
private _currentScene: IScene | null = null;
/**
* 待切换的下一个场景(延迟切换用)
*/
private _nextScene: IScene | null = null;
/**
* ECS流式API
*/
private _ecsAPI: ECSFluentAPI | null = null;
/**
* 日志器
*/
private _logger = createLogger('SceneManager');
/**
* 设置当前场景(立即切换)
*
* 会自动处理旧场景的结束和新场景的初始化。
*
* @param scene - 要设置的场景实例
* @returns 返回设置的场景实例,便于链式调用
*
* @example
* ```typescript
* const gameScene = sceneManager.setScene(new GameScene());
* console.log(gameScene.name); // 可以立即使用返回的场景
* ```
*/
public setScene<T extends IScene>(scene: T): T {
// 结束旧场景
if (this._currentScene) {
this._logger.info(`Ending scene: ${this._currentScene.name}`);
this._currentScene.end();
}
// 设置并初始化新场景
this._currentScene = scene;
this._currentScene.initialize();
this._currentScene.begin();
// 重建ECS API
if (scene.querySystem && scene.eventSystem) {
this._ecsAPI = createECSAPI(scene, scene.querySystem, scene.eventSystem);
} else {
this._ecsAPI = null;
}
// 触发场景切换回调
Time.sceneChanged();
// 通知调试管理器
const coreInstance = Core.Instance;
if (coreInstance && coreInstance._debugManager) {
coreInstance._debugManager.onSceneChanged();
}
this._logger.info(`Scene changed to: ${scene.name}`);
return scene;
}
/**
* 延迟加载场景(下一帧切换)
*
* 场景不会立即切换,而是在下一次调用 update() 时切换。
* 这对于避免在当前帧的中途切换场景很有用。
*
* @param scene - 要加载的场景实例
*
* @example
* ```typescript
* // 在某个System中触发场景切换
* class GameOverSystem extends EntitySystem {
* process(entities: readonly Entity[]) {
* if (playerHealth <= 0) {
* sceneManager.loadScene(new GameOverScene());
* // 当前帧继续执行,场景将在下一帧切换
* }
* }
* }
* ```
*/
public loadScene<T extends IScene>(scene: T): void {
this._nextScene = scene;
this._logger.info(`Scheduled scene load: ${scene.name}`);
}
/**
* 获取当前活跃的场景
*
* @returns 当前场景实例如果没有场景则返回null
*/
public get currentScene(): IScene | null {
return this._currentScene;
}
/**
* 获取ECS流式API
*
* 提供便捷的实体查询、事件发射等功能。
*
* @returns ECS API实例如果当前没有场景则返回null
*
* @example
* ```typescript
* const api = sceneManager.api;
* if (api) {
* // 查询所有敌人
* const enemies = api.find(Enemy, Transform);
*
* // 发射事件
* api.emit('game:start', { level: 1 });
* }
* ```
*/
public get api(): ECSFluentAPI | null {
return this._ecsAPI;
}
/**
* 更新场景
*
* 应该在每帧的游戏循环中调用。
* 会自动处理延迟场景切换。
*
* @example
* ```typescript
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime);
* sceneManager.update(); // 每帧调用
* }
* ```
*/
public update(): void {
// 处理延迟场景切换
if (this._nextScene) {
this.setScene(this._nextScene);
this._nextScene = null;
}
// 更新当前场景
if (this._currentScene) {
this._currentScene.update();
}
}
/**
* 销毁场景管理器
*
* 会自动结束当前场景并清理所有资源。
* 通常在应用程序关闭时调用。
*/
public destroy(): void {
if (this._currentScene) {
this._logger.info(`Destroying scene: ${this._currentScene.name}`);
this._currentScene.end();
this._currentScene = null;
}
this._nextScene = null;
this._ecsAPI = null;
this._logger.info('SceneManager destroyed');
}
/**
* 检查是否有活跃场景
*
* @returns 如果有活跃场景返回true否则返回false
*/
public get hasScene(): boolean {
return this._currentScene !== null;
}
/**
* 检查是否有待切换的场景
*
* @returns 如果有待切换场景返回true否则返回false
*/
public get hasPendingScene(): boolean {
return this._nextScene !== null;
}
}

View File

@@ -0,0 +1,336 @@
/**
* 组件序列化器
*
* 负责组件的序列化和反序列化操作
*/
import { Component } from '../Component';
import { ComponentType } from '../Core/ComponentStorage';
import { getComponentTypeName } from '../Decorators';
import {
getSerializationMetadata,
isSerializable,
SerializationMetadata
} from './SerializationDecorators';
/**
* 序列化后的组件数据
*/
export interface SerializedComponent {
/**
* 组件类型名称
*/
type: string;
/**
* 序列化版本
*/
version: number;
/**
* 组件数据
*/
data: Record<string, any>;
}
/**
* 组件序列化器类
*/
export class ComponentSerializer {
/**
* 序列化单个组件
*
* @param component 要序列化的组件实例
* @returns 序列化后的组件数据如果组件不可序列化则返回null
*/
public static serialize(component: Component): SerializedComponent | null {
const metadata = getSerializationMetadata(component);
if (!metadata) {
// 组件没有使用@Serializable装饰器不可序列化
return null;
}
const componentType = component.constructor as ComponentType;
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
const data: Record<string, any> = {};
// 序列化标记的字段
for (const [fieldName, options] of metadata.fields) {
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
const value = (component as any)[fieldName];
// 跳过忽略的字段
if (metadata.ignoredFields.has(fieldName)) {
continue;
}
// 使用自定义序列化器或默认序列化
const serializedValue = options.serializer
? options.serializer(value)
: this.serializeValue(value);
// 使用别名或原始字段名
const key = options.alias || fieldKey;
data[key] = serializedValue;
}
return {
type: typeName,
version: metadata.options.version,
data
};
}
/**
* 反序列化组件
*
* @param serializedData 序列化的组件数据
* @param componentRegistry 组件类型注册表 (类型名 -> 构造函数)
* @returns 反序列化后的组件实例如果失败则返回null
*/
public static deserialize(
serializedData: SerializedComponent,
componentRegistry: Map<string, ComponentType>
): Component | null {
const componentClass = componentRegistry.get(serializedData.type);
if (!componentClass) {
console.warn(`未找到组件类型: ${serializedData.type}`);
return null;
}
const metadata = getSerializationMetadata(componentClass);
if (!metadata) {
console.warn(`组件 ${serializedData.type} 不可序列化`);
return null;
}
// 创建组件实例
const component = new componentClass();
// 反序列化字段
for (const [fieldName, options] of metadata.fields) {
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
const key = options.alias || fieldKey;
const serializedValue = serializedData.data[key];
if (serializedValue === undefined) {
continue; // 字段不存在于序列化数据中
}
// 使用自定义反序列化器或默认反序列化
const value = options.deserializer
? options.deserializer(serializedValue)
: this.deserializeValue(serializedValue);
(component as any)[fieldName] = value;
}
return component;
}
/**
* 批量序列化组件
*
* @param components 组件数组
* @returns 序列化后的组件数据数组
*/
public static serializeComponents(components: Component[]): SerializedComponent[] {
const result: SerializedComponent[] = [];
for (const component of components) {
const serialized = this.serialize(component);
if (serialized) {
result.push(serialized);
}
}
return result;
}
/**
* 批量反序列化组件
*
* @param serializedComponents 序列化的组件数据数组
* @param componentRegistry 组件类型注册表
* @returns 反序列化后的组件数组
*/
public static deserializeComponents(
serializedComponents: SerializedComponent[],
componentRegistry: Map<string, ComponentType>
): Component[] {
const result: Component[] = [];
for (const serialized of serializedComponents) {
const component = this.deserialize(serialized, componentRegistry);
if (component) {
result.push(component);
}
}
return result;
}
/**
* 默认值序列化
*
* 处理基本类型、数组、对象等的序列化
*/
private static serializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 日期
if (value instanceof Date) {
return {
__type: 'Date',
value: value.toISOString()
};
}
// 数组
if (Array.isArray(value)) {
return value.map(item => this.serializeValue(item));
}
// Map (如果没有使用@SerializeMap装饰器)
if (value instanceof Map) {
return {
__type: 'Map',
value: Array.from(value.entries())
};
}
// Set
if (value instanceof Set) {
return {
__type: 'Set',
value: Array.from(value)
};
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.serializeValue(value[key]);
}
}
return result;
}
// 其他类型(函数等)不序列化
return undefined;
}
/**
* 默认值反序列化
*/
private static deserializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型直接返回
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 处理特殊类型标记
if (type === 'object' && value.__type) {
switch (value.__type) {
case 'Date':
return new Date(value.value);
case 'Map':
return new Map(value.value);
case 'Set':
return new Set(value.value);
}
}
// 数组
if (Array.isArray(value)) {
return value.map(item => this.deserializeValue(item));
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.deserializeValue(value[key]);
}
}
return result;
}
return value;
}
/**
* 验证序列化数据的版本
*
* @param serializedData 序列化数据
* @param expectedVersion 期望的版本号
* @returns 版本是否匹配
*/
public static validateVersion(
serializedData: SerializedComponent,
expectedVersion: number
): boolean {
return serializedData.version === expectedVersion;
}
/**
* 获取组件的序列化信息
*
* @param component 组件实例或组件类
* @returns 序列化信息对象,包含类型名、版本、可序列化字段列表
*/
public static getSerializationInfo(component: Component | ComponentType): {
type: string;
version: number;
fields: string[];
ignoredFields: string[];
isSerializable: boolean;
} | null {
const metadata = getSerializationMetadata(component);
if (!metadata) {
return {
type: 'unknown',
version: 0,
fields: [],
ignoredFields: [],
isSerializable: false
};
}
const componentType = typeof component === 'function'
? component
: (component.constructor as ComponentType);
return {
type: metadata.options.typeId || getComponentTypeName(componentType),
version: metadata.options.version,
fields: Array.from(metadata.fields.keys()).map(k =>
typeof k === 'symbol' ? k.toString() : k
),
ignoredFields: Array.from(metadata.ignoredFields).map(k =>
typeof k === 'symbol' ? k.toString() : k
),
isSerializable: true
};
}
}

View File

@@ -0,0 +1,223 @@
/**
* 实体序列化器
*
* 负责实体的序列化和反序列化操作
*/
import { Entity } from '../Entity';
import { Component } from '../Component';
import { ComponentType } from '../Core/ComponentStorage';
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
/**
* 序列化后的实体数据
*/
export interface SerializedEntity {
/**
* 实体ID
*/
id: number;
/**
* 实体名称
*/
name: string;
/**
* 实体标签
*/
tag: number;
/**
* 激活状态
*/
active: boolean;
/**
* 启用状态
*/
enabled: boolean;
/**
* 更新顺序
*/
updateOrder: number;
/**
* 组件列表
*/
components: SerializedComponent[];
/**
* 子实体列表
*/
children: SerializedEntity[];
/**
* 父实体ID如果有
*/
parentId?: number;
}
/**
* 实体序列化器类
*/
export class EntitySerializer {
/**
* 序列化单个实体
*
* @param entity 要序列化的实体
* @param includeChildren 是否包含子实体默认true
* @returns 序列化后的实体数据
*/
public static serialize(entity: Entity, includeChildren: boolean = true): SerializedEntity {
const serializedComponents = ComponentSerializer.serializeComponents(
Array.from(entity.components)
);
const serializedEntity: SerializedEntity = {
id: entity.id,
name: entity.name,
tag: entity.tag,
active: entity.active,
enabled: entity.enabled,
updateOrder: entity.updateOrder,
components: serializedComponents,
children: []
};
// 序列化父实体引用
if (entity.parent) {
serializedEntity.parentId = entity.parent.id;
}
// 序列化子实体
if (includeChildren) {
for (const child of entity.children) {
serializedEntity.children.push(this.serialize(child, true));
}
}
return serializedEntity;
}
/**
* 反序列化实体
*
* @param serializedEntity 序列化的实体数据
* @param componentRegistry 组件类型注册表
* @param idGenerator 实体ID生成器用于生成新ID或保持原ID
* @param preserveIds 是否保持原始ID默认false
* @returns 反序列化后的实体
*/
public static deserialize(
serializedEntity: SerializedEntity,
componentRegistry: Map<string, ComponentType>,
idGenerator: () => number,
preserveIds: boolean = false
): Entity {
// 创建实体使用原始ID或新生成的ID
const entityId = preserveIds ? serializedEntity.id : idGenerator();
const entity = new Entity(serializedEntity.name, entityId);
// 恢复实体属性
entity.tag = serializedEntity.tag;
entity.active = serializedEntity.active;
entity.enabled = serializedEntity.enabled;
entity.updateOrder = serializedEntity.updateOrder;
// 反序列化组件
const components = ComponentSerializer.deserializeComponents(
serializedEntity.components,
componentRegistry
);
for (const component of components) {
entity.addComponent(component);
}
// 反序列化子实体
for (const childData of serializedEntity.children) {
const childEntity = this.deserialize(
childData,
componentRegistry,
idGenerator,
preserveIds
);
entity.addChild(childEntity);
}
return entity;
}
/**
* 批量序列化实体
*
* @param entities 实体数组
* @param includeChildren 是否包含子实体
* @returns 序列化后的实体数据数组
*/
public static serializeEntities(
entities: Entity[],
includeChildren: boolean = true
): SerializedEntity[] {
const result: SerializedEntity[] = [];
for (const entity of entities) {
// 只序列化顶层实体(没有父实体的实体)
// 子实体会在父实体序列化时一并处理
if (!entity.parent || !includeChildren) {
result.push(this.serialize(entity, includeChildren));
}
}
return result;
}
/**
* 批量反序列化实体
*
* @param serializedEntities 序列化的实体数据数组
* @param componentRegistry 组件类型注册表
* @param idGenerator 实体ID生成器
* @param preserveIds 是否保持原始ID
* @returns 反序列化后的实体数组
*/
public static deserializeEntities(
serializedEntities: SerializedEntity[],
componentRegistry: Map<string, ComponentType>,
idGenerator: () => number,
preserveIds: boolean = false
): Entity[] {
const result: Entity[] = [];
for (const serialized of serializedEntities) {
const entity = this.deserialize(
serialized,
componentRegistry,
idGenerator,
preserveIds
);
result.push(entity);
}
return result;
}
/**
* 创建实体的深拷贝
*
* @param entity 要拷贝的实体
* @param componentRegistry 组件类型注册表
* @param idGenerator ID生成器
* @returns 拷贝后的新实体
*/
public static clone(
entity: Entity,
componentRegistry: Map<string, ComponentType>,
idGenerator: () => number
): Entity {
const serialized = this.serialize(entity, true);
return this.deserialize(serialized, componentRegistry, idGenerator, false);
}
}

View File

@@ -0,0 +1,758 @@
/**
* 增量序列化器
*
* 提供高性能的增量序列化支持,只序列化变更的数据
* 适用于网络同步、大场景存档、时间回溯等场景
*/
import type { IScene } from '../IScene';
import { Entity } from '../Entity';
import { Component } from '../Component';
import { ComponentSerializer, SerializedComponent } from './ComponentSerializer';
import { SerializedEntity } from './EntitySerializer';
import { ComponentType } from '../Core/ComponentStorage';
import * as msgpack from 'msgpack-lite';
/**
* 变更操作类型
*/
export enum ChangeOperation {
/** 添加新实体 */
EntityAdded = 'entity_added',
/** 删除实体 */
EntityRemoved = 'entity_removed',
/** 实体属性更新 */
EntityUpdated = 'entity_updated',
/** 添加组件 */
ComponentAdded = 'component_added',
/** 删除组件 */
ComponentRemoved = 'component_removed',
/** 组件数据更新 */
ComponentUpdated = 'component_updated',
/** 场景数据更新 */
SceneDataUpdated = 'scene_data_updated'
}
/**
* 实体变更记录
*/
export interface EntityChange {
/** 操作类型 */
operation: ChangeOperation;
/** 实体ID */
entityId: number;
/** 实体名称用于Added操作 */
entityName?: string;
/** 实体数据用于Added/Updated操作 */
entityData?: Partial<SerializedEntity>;
}
/**
* 组件变更记录
*/
export interface ComponentChange {
/** 操作类型 */
operation: ChangeOperation;
/** 实体ID */
entityId: number;
/** 组件类型名称 */
componentType: string;
/** 组件数据用于Added/Updated操作 */
componentData?: SerializedComponent;
}
/**
* 场景数据变更记录
*/
export interface SceneDataChange {
/** 操作类型 */
operation: ChangeOperation;
/** 变更的键 */
key: string;
/** 新值 */
value: any;
/** 是否删除 */
deleted?: boolean;
}
/**
* 增量序列化数据
*/
export interface IncrementalSnapshot {
/** 快照版本号 */
version: number;
/** 时间戳 */
timestamp: number;
/** 场景名称 */
sceneName: string;
/** 基础版本号(相对于哪个快照的增量) */
baseVersion: number;
/** 实体变更列表 */
entityChanges: EntityChange[];
/** 组件变更列表 */
componentChanges: ComponentChange[];
/** 场景数据变更列表 */
sceneDataChanges: SceneDataChange[];
}
/**
* 场景快照(用于对比)
*/
interface SceneSnapshot {
/** 快照版本号 */
version: number;
/** 实体ID集合 */
entityIds: Set<number>;
/** 实体数据映射 */
entities: Map<number, {
name: string;
tag: number;
active: boolean;
enabled: boolean;
updateOrder: number;
parentId?: number;
}>;
/** 组件数据映射 (entityId -> componentType -> serializedData) */
components: Map<number, Map<string, string>>; // 使用JSON字符串存储组件数据
/** 场景自定义数据 */
sceneData: Map<string, string>; // 使用JSON字符串存储场景数据
}
/**
* 增量序列化格式
*/
export type IncrementalSerializationFormat = 'json' | 'binary';
/**
* 增量序列化选项
*/
export interface IncrementalSerializationOptions {
/**
* 是否包含组件数据的深度对比
* 默认true设为false可提升性能但可能漏掉组件内部字段变更
*/
deepComponentComparison?: boolean;
/**
* 是否跟踪场景数据变更
* 默认true
*/
trackSceneData?: boolean;
/**
* 是否压缩快照使用JSON序列化
* 默认false设为true可减少内存占用但增加CPU开销
*/
compressSnapshot?: boolean;
/**
* 序列化格式
* - 'json': JSON格式可读性好方便调试
* - 'binary': MessagePack二进制格式体积小性能高
* 默认 'json'
*/
format?: IncrementalSerializationFormat;
/**
* 是否美化JSON输出仅在format='json'时有效)
* 默认false
*/
pretty?: boolean;
}
/**
* 增量序列化器类
*/
export class IncrementalSerializer {
/** 当前快照版本号 */
private static snapshotVersion = 0;
/**
* 创建场景快照
*
* @param scene 要快照的场景
* @param options 序列化选项
* @returns 场景快照对象
*/
public static createSnapshot(
scene: IScene,
options?: IncrementalSerializationOptions
): SceneSnapshot {
const opts = {
deepComponentComparison: true,
trackSceneData: true,
compressSnapshot: false,
...options
};
const snapshot: SceneSnapshot = {
version: ++this.snapshotVersion,
entityIds: new Set(),
entities: new Map(),
components: new Map(),
sceneData: new Map()
};
// 快照所有实体
for (const entity of scene.entities.buffer) {
snapshot.entityIds.add(entity.id);
// 存储实体基本信息
snapshot.entities.set(entity.id, {
name: entity.name,
tag: entity.tag,
active: entity.active,
enabled: entity.enabled,
updateOrder: entity.updateOrder,
parentId: entity.parent?.id
});
// 快照组件
if (opts.deepComponentComparison) {
const componentMap = new Map<string, string>();
for (const component of entity.components) {
const serialized = ComponentSerializer.serialize(component);
if (serialized) {
// 使用JSON字符串存储便于后续对比
componentMap.set(
serialized.type,
JSON.stringify(serialized.data)
);
}
}
if (componentMap.size > 0) {
snapshot.components.set(entity.id, componentMap);
}
}
}
// 快照场景数据
if (opts.trackSceneData) {
for (const [key, value] of scene.sceneData) {
snapshot.sceneData.set(key, JSON.stringify(value));
}
}
return snapshot;
}
/**
* 计算增量变更
*
* @param scene 当前场景
* @param baseSnapshot 基础快照
* @param options 序列化选项
* @returns 增量快照
*/
public static computeIncremental(
scene: IScene,
baseSnapshot: SceneSnapshot,
options?: IncrementalSerializationOptions
): IncrementalSnapshot {
const opts = {
deepComponentComparison: true,
trackSceneData: true,
...options
};
const incremental: IncrementalSnapshot = {
version: ++this.snapshotVersion,
timestamp: Date.now(),
sceneName: scene.name,
baseVersion: baseSnapshot.version,
entityChanges: [],
componentChanges: [],
sceneDataChanges: []
};
const currentEntityIds = new Set<number>();
// 检测实体变更
for (const entity of scene.entities.buffer) {
currentEntityIds.add(entity.id);
if (!baseSnapshot.entityIds.has(entity.id)) {
// 新增实体
incremental.entityChanges.push({
operation: ChangeOperation.EntityAdded,
entityId: entity.id,
entityName: entity.name,
entityData: {
id: entity.id,
name: entity.name,
tag: entity.tag,
active: entity.active,
enabled: entity.enabled,
updateOrder: entity.updateOrder,
parentId: entity.parent?.id,
components: [],
children: []
}
});
// 新增实体的所有组件都是新增
for (const component of entity.components) {
const serialized = ComponentSerializer.serialize(component);
if (serialized) {
incremental.componentChanges.push({
operation: ChangeOperation.ComponentAdded,
entityId: entity.id,
componentType: serialized.type,
componentData: serialized
});
}
}
} else {
// 检查实体属性变更
const oldData = baseSnapshot.entities.get(entity.id)!;
const entityChanged =
oldData.name !== entity.name ||
oldData.tag !== entity.tag ||
oldData.active !== entity.active ||
oldData.enabled !== entity.enabled ||
oldData.updateOrder !== entity.updateOrder ||
oldData.parentId !== entity.parent?.id;
if (entityChanged) {
incremental.entityChanges.push({
operation: ChangeOperation.EntityUpdated,
entityId: entity.id,
entityData: {
name: entity.name,
tag: entity.tag,
active: entity.active,
enabled: entity.enabled,
updateOrder: entity.updateOrder,
parentId: entity.parent?.id
}
});
}
// 检查组件变更
if (opts.deepComponentComparison) {
this.detectComponentChanges(
entity,
baseSnapshot,
incremental.componentChanges
);
}
}
}
// 检测删除的实体
for (const oldEntityId of baseSnapshot.entityIds) {
if (!currentEntityIds.has(oldEntityId)) {
incremental.entityChanges.push({
operation: ChangeOperation.EntityRemoved,
entityId: oldEntityId
});
}
}
// 检测场景数据变更
if (opts.trackSceneData) {
this.detectSceneDataChanges(
scene,
baseSnapshot,
incremental.sceneDataChanges
);
}
return incremental;
}
/**
* 检测组件变更
*/
private static detectComponentChanges(
entity: Entity,
baseSnapshot: SceneSnapshot,
componentChanges: ComponentChange[]
): void {
const oldComponents = baseSnapshot.components.get(entity.id);
const currentComponents = new Map<string, SerializedComponent>();
// 收集当前组件
for (const component of entity.components) {
const serialized = ComponentSerializer.serialize(component);
if (serialized) {
currentComponents.set(serialized.type, serialized);
}
}
// 检测新增和更新的组件
for (const [type, serialized] of currentComponents) {
const currentData = JSON.stringify(serialized.data);
if (!oldComponents || !oldComponents.has(type)) {
// 新增组件
componentChanges.push({
operation: ChangeOperation.ComponentAdded,
entityId: entity.id,
componentType: type,
componentData: serialized
});
} else if (oldComponents.get(type) !== currentData) {
// 组件数据变更
componentChanges.push({
operation: ChangeOperation.ComponentUpdated,
entityId: entity.id,
componentType: type,
componentData: serialized
});
}
}
// 检测删除的组件
if (oldComponents) {
for (const oldType of oldComponents.keys()) {
if (!currentComponents.has(oldType)) {
componentChanges.push({
operation: ChangeOperation.ComponentRemoved,
entityId: entity.id,
componentType: oldType
});
}
}
}
}
/**
* 检测场景数据变更
*/
private static detectSceneDataChanges(
scene: IScene,
baseSnapshot: SceneSnapshot,
sceneDataChanges: SceneDataChange[]
): void {
const currentKeys = new Set<string>();
// 检测新增和更新的场景数据
for (const [key, value] of scene.sceneData) {
currentKeys.add(key);
const currentValue = JSON.stringify(value);
const oldValue = baseSnapshot.sceneData.get(key);
if (!oldValue || oldValue !== currentValue) {
sceneDataChanges.push({
operation: ChangeOperation.SceneDataUpdated,
key,
value
});
}
}
// 检测删除的场景数据
for (const oldKey of baseSnapshot.sceneData.keys()) {
if (!currentKeys.has(oldKey)) {
sceneDataChanges.push({
operation: ChangeOperation.SceneDataUpdated,
key: oldKey,
value: undefined,
deleted: true
});
}
}
}
/**
* 应用增量变更到场景
*
* @param scene 目标场景
* @param incremental 增量快照
* @param componentRegistry 组件类型注册表
*/
public static applyIncremental(
scene: IScene,
incremental: IncrementalSnapshot,
componentRegistry: Map<string, ComponentType>
): void {
// 应用实体变更
for (const change of incremental.entityChanges) {
switch (change.operation) {
case ChangeOperation.EntityAdded:
this.applyEntityAdded(scene, change);
break;
case ChangeOperation.EntityRemoved:
this.applyEntityRemoved(scene, change);
break;
case ChangeOperation.EntityUpdated:
this.applyEntityUpdated(scene, change);
break;
}
}
// 应用组件变更
for (const change of incremental.componentChanges) {
switch (change.operation) {
case ChangeOperation.ComponentAdded:
this.applyComponentAdded(scene, change, componentRegistry);
break;
case ChangeOperation.ComponentRemoved:
this.applyComponentRemoved(scene, change, componentRegistry);
break;
case ChangeOperation.ComponentUpdated:
this.applyComponentUpdated(scene, change, componentRegistry);
break;
}
}
// 应用场景数据变更
for (const change of incremental.sceneDataChanges) {
if (change.deleted) {
scene.sceneData.delete(change.key);
} else {
scene.sceneData.set(change.key, change.value);
}
}
}
private static applyEntityAdded(scene: IScene, change: EntityChange): void {
if (!change.entityData) return;
const entity = new Entity(change.entityName || 'Entity', change.entityId);
entity.tag = change.entityData.tag || 0;
entity.active = change.entityData.active ?? true;
entity.enabled = change.entityData.enabled ?? true;
entity.updateOrder = change.entityData.updateOrder || 0;
scene.addEntity(entity);
}
private static applyEntityRemoved(scene: IScene, change: EntityChange): void {
const entity = scene.entities.findEntityById(change.entityId);
if (entity) {
entity.destroy();
}
}
private static applyEntityUpdated(scene: IScene, change: EntityChange): void {
if (!change.entityData) return;
const entity = scene.entities.findEntityById(change.entityId);
if (!entity) return;
if (change.entityData.name !== undefined) entity.name = change.entityData.name;
if (change.entityData.tag !== undefined) entity.tag = change.entityData.tag;
if (change.entityData.active !== undefined) entity.active = change.entityData.active;
if (change.entityData.enabled !== undefined) entity.enabled = change.entityData.enabled;
if (change.entityData.updateOrder !== undefined) entity.updateOrder = change.entityData.updateOrder;
if (change.entityData.parentId !== undefined) {
const newParent = scene.entities.findEntityById(change.entityData.parentId);
if (newParent && entity.parent !== newParent) {
if (entity.parent) {
entity.parent.removeChild(entity);
}
newParent.addChild(entity);
}
} else if (entity.parent) {
entity.parent.removeChild(entity);
}
}
private static applyComponentAdded(
scene: IScene,
change: ComponentChange,
componentRegistry: Map<string, ComponentType>
): void {
if (!change.componentData) return;
const entity = scene.entities.findEntityById(change.entityId);
if (!entity) return;
const component = ComponentSerializer.deserialize(change.componentData, componentRegistry);
if (component) {
entity.addComponent(component);
}
}
private static applyComponentRemoved(
scene: IScene,
change: ComponentChange,
componentRegistry: Map<string, ComponentType>
): void {
const entity = scene.entities.findEntityById(change.entityId);
if (!entity) return;
const componentClass = componentRegistry.get(change.componentType);
if (!componentClass) return;
entity.removeComponentByType(componentClass);
}
private static applyComponentUpdated(
scene: IScene,
change: ComponentChange,
componentRegistry: Map<string, ComponentType>
): void {
if (!change.componentData) return;
const entity = scene.entities.findEntityById(change.entityId);
if (!entity) return;
const componentClass = componentRegistry.get(change.componentType);
if (!componentClass) return;
entity.removeComponentByType(componentClass);
const component = ComponentSerializer.deserialize(change.componentData, componentRegistry);
if (component) {
entity.addComponent(component);
}
}
/**
* 序列化增量快照
*
* @param incremental 增量快照
* @param options 序列化选项
* @returns 序列化后的数据JSON字符串或二进制Buffer
*
* @example
* ```typescript
* // JSON格式默认
* const jsonData = IncrementalSerializer.serializeIncremental(snapshot);
*
* // 二进制格式
* const binaryData = IncrementalSerializer.serializeIncremental(snapshot, {
* format: 'binary'
* });
*
* // 美化JSON
* const prettyJson = IncrementalSerializer.serializeIncremental(snapshot, {
* format: 'json',
* pretty: true
* });
* ```
*/
public static serializeIncremental(
incremental: IncrementalSnapshot,
options?: { format?: IncrementalSerializationFormat; pretty?: boolean }
): string | Buffer {
const opts = {
format: 'json' as IncrementalSerializationFormat,
pretty: false,
...options
};
if (opts.format === 'binary') {
return msgpack.encode(incremental);
} else {
return opts.pretty
? JSON.stringify(incremental, null, 2)
: JSON.stringify(incremental);
}
}
/**
* 反序列化增量快照
*
* @param data 序列化的数据JSON字符串或二进制Buffer
* @returns 增量快照
*
* @example
* ```typescript
* // 从JSON反序列化
* const snapshot = IncrementalSerializer.deserializeIncremental(jsonString);
*
* // 从二进制反序列化
* const snapshot = IncrementalSerializer.deserializeIncremental(buffer);
* ```
*/
public static deserializeIncremental(data: string | Buffer): IncrementalSnapshot {
if (typeof data === 'string') {
// JSON格式
return JSON.parse(data);
} else {
// 二进制格式MessagePack
return msgpack.decode(data);
}
}
/**
* 计算增量快照的大小(字节)
*
* @param incremental 增量快照
* @param format 序列化格式,默认为 'json'
* @returns 字节数
*/
public static getIncrementalSize(
incremental: IncrementalSnapshot,
format: IncrementalSerializationFormat = 'json'
): number {
const data = this.serializeIncremental(incremental, { format });
if (typeof data === 'string') {
// JSON格式计算UTF-8编码后的字节数
// 使用 Blob 来计算浏览器和 Node.js 环境兼容的字节数
if (typeof Blob !== 'undefined') {
return new Blob([data]).size;
} else if (typeof Buffer !== 'undefined') {
return Buffer.byteLength(data, 'utf8');
} else {
// 回退方案:粗略估算(不精确,但可用)
return new TextEncoder().encode(data).length;
}
} else {
// 二进制格式直接返回Buffer长度
return data.length;
}
}
/**
* 获取增量快照的统计信息
*
* @param incremental 增量快照
* @returns 统计信息
*/
public static getIncrementalStats(incremental: IncrementalSnapshot): {
totalChanges: number;
entityChanges: number;
componentChanges: number;
sceneDataChanges: number;
addedEntities: number;
removedEntities: number;
updatedEntities: number;
addedComponents: number;
removedComponents: number;
updatedComponents: number;
} {
return {
totalChanges:
incremental.entityChanges.length +
incremental.componentChanges.length +
incremental.sceneDataChanges.length,
entityChanges: incremental.entityChanges.length,
componentChanges: incremental.componentChanges.length,
sceneDataChanges: incremental.sceneDataChanges.length,
addedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityAdded
).length,
removedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityRemoved
).length,
updatedEntities: incremental.entityChanges.filter(
c => c.operation === ChangeOperation.EntityUpdated
).length,
addedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentAdded
).length,
removedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentRemoved
).length,
updatedComponents: incremental.componentChanges.filter(
c => c.operation === ChangeOperation.ComponentUpdated
).length
};
}
/**
* 重置快照版本号(用于测试)
*/
public static resetVersion(): void {
this.snapshotVersion = 0;
}
}

View File

@@ -0,0 +1,535 @@
/**
* 场景序列化器
*
* 负责整个场景的序列化和反序列化,包括实体、组件等
*/
import type { IScene } from '../IScene';
import { Entity } from '../Entity';
import { Component } from '../Component';
import { ComponentType, ComponentRegistry } from '../Core/ComponentStorage';
import { EntitySerializer, SerializedEntity } from './EntitySerializer';
import { getComponentTypeName } from '../Decorators';
import { getSerializationMetadata } from './SerializationDecorators';
import * as msgpack from 'msgpack-lite';
/**
* 场景序列化格式
*/
export type SerializationFormat = 'json' | 'binary';
/**
* 场景序列化策略
*/
export type DeserializationStrategy = 'merge' | 'replace';
/**
* 版本迁移函数
*/
export type MigrationFunction = (
oldVersion: number,
newVersion: number,
data: any
) => any;
/**
* 场景序列化选项
*/
export interface SceneSerializationOptions {
/**
* 要序列化的组件类型列表
* 如果未指定,则序列化所有可序列化的组件
*/
components?: ComponentType[];
/**
* 是否序列化系统状态(当前不支持)
*/
systems?: boolean;
/**
* 序列化格式
*/
format?: SerializationFormat;
/**
* 是否美化JSON输出仅在format='json'时有效)
*/
pretty?: boolean;
/**
* 是否包含元数据(如序列化时间、版本等)
*/
includeMetadata?: boolean;
}
/**
* 场景反序列化选项
*/
export interface SceneDeserializationOptions {
/**
* 反序列化策略
* - 'merge': 合并到现有场景
* - 'replace': 替换现有场景内容
*/
strategy?: DeserializationStrategy;
/**
* 版本迁移函数
*/
migration?: MigrationFunction;
/**
* 是否保持原始实体ID
*/
preserveIds?: boolean;
/**
* 组件类型注册表
* 如果未提供,将尝试从全局注册表获取
*/
componentRegistry?: Map<string, ComponentType>;
}
/**
* 序列化后的场景数据
*/
export interface SerializedScene {
/**
* 场景名称
*/
name: string;
/**
* 序列化版本
*/
version: number;
/**
* 序列化时间戳
*/
timestamp?: number;
/**
* 场景自定义数据
*
* 存储场景级别的配置和状态
*/
sceneData?: Record<string, any>;
/**
* 实体列表
*/
entities: SerializedEntity[];
/**
* 元数据
*/
metadata?: {
entityCount: number;
componentTypeCount: number;
serializationOptions?: SceneSerializationOptions;
};
/**
* 组件类型注册信息
*/
componentTypeRegistry: Array<{
typeName: string;
version: number;
}>;
}
/**
* 场景序列化器类
*/
export class SceneSerializer {
/**
* 当前序列化版本
*/
private static readonly SERIALIZATION_VERSION = 1;
/**
* 序列化场景
*
* @param scene 要序列化的场景
* @param options 序列化选项
* @returns 序列化后的数据JSON字符串或二进制Buffer
*/
public static serialize(scene: IScene, options?: SceneSerializationOptions): string | Buffer {
const opts: SceneSerializationOptions = {
systems: false,
format: 'json',
pretty: true,
includeMetadata: true,
...options
};
// 过滤实体和组件
const entities = this.filterEntities(scene, opts);
// 序列化实体
const serializedEntities = EntitySerializer.serializeEntities(entities, true);
// 收集组件类型信息
const componentTypeRegistry = this.buildComponentTypeRegistry(entities);
// 序列化场景自定义数据
const sceneData = this.serializeSceneData(scene.sceneData);
// 构建序列化数据
const serializedScene: SerializedScene = {
name: scene.name,
version: this.SERIALIZATION_VERSION,
entities: serializedEntities,
componentTypeRegistry
};
// 添加场景数据(如果有)
if (sceneData && Object.keys(sceneData).length > 0) {
serializedScene.sceneData = sceneData;
}
// 添加元数据
if (opts.includeMetadata) {
serializedScene.timestamp = Date.now();
serializedScene.metadata = {
entityCount: serializedEntities.length,
componentTypeCount: componentTypeRegistry.length,
serializationOptions: opts
};
}
// 根据格式返回数据
if (opts.format === 'json') {
return opts.pretty
? JSON.stringify(serializedScene, null, 2)
: JSON.stringify(serializedScene);
} else {
// 二进制格式(使用 MessagePack
return msgpack.encode(serializedScene);
}
}
/**
* 反序列化场景
*
* @param scene 目标场景
* @param saveData 序列化的数据JSON字符串或二进制Buffer
* @param options 反序列化选项
*/
public static deserialize(
scene: IScene,
saveData: string | Buffer,
options?: SceneDeserializationOptions
): void {
const opts: SceneDeserializationOptions = {
strategy: 'replace',
preserveIds: false,
...options
};
// 解析数据
let serializedScene: SerializedScene;
try {
if (typeof saveData === 'string') {
// JSON格式
serializedScene = JSON.parse(saveData);
} else {
// 二进制格式MessagePack
serializedScene = msgpack.decode(saveData);
}
} catch (error) {
throw new Error(`Failed to parse save data: ${error}`);
}
// 版本迁移
if (opts.migration && serializedScene.version !== this.SERIALIZATION_VERSION) {
serializedScene = opts.migration(
serializedScene.version,
this.SERIALIZATION_VERSION,
serializedScene
);
}
// 构建组件注册表
const componentRegistry = opts.componentRegistry || this.getGlobalComponentRegistry();
// 根据策略处理场景
if (opts.strategy === 'replace') {
// 清空场景
scene.destroyAllEntities();
}
// ID生成器
const idGenerator = () => scene.identifierPool.checkOut();
// 反序列化实体
const entities = EntitySerializer.deserializeEntities(
serializedScene.entities,
componentRegistry,
idGenerator,
opts.preserveIds || false
);
// 将实体添加到场景
for (const entity of entities) {
scene.addEntity(entity);
}
// 反序列化场景自定义数据
if (serializedScene.sceneData) {
this.deserializeSceneData(serializedScene.sceneData, scene.sceneData);
}
}
/**
* 序列化场景自定义数据
*
* 将 Map<string, any> 转换为普通对象
*/
private static serializeSceneData(sceneData: Map<string, any>): Record<string, any> {
const result: Record<string, any> = {};
for (const [key, value] of sceneData) {
result[key] = this.serializeValue(value);
}
return result;
}
/**
* 反序列化场景自定义数据
*
* 将普通对象还原为 Map<string, any>
*/
private static deserializeSceneData(
data: Record<string, any>,
targetMap: Map<string, any>
): void {
targetMap.clear();
for (const [key, value] of Object.entries(data)) {
targetMap.set(key, this.deserializeValue(value));
}
}
/**
* 序列化单个值
*/
private static serializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// Date
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
// Map
if (value instanceof Map) {
return { __type: 'Map', value: Array.from(value.entries()) };
}
// Set
if (value instanceof Set) {
return { __type: 'Set', value: Array.from(value) };
}
// 数组
if (Array.isArray(value)) {
return value.map(item => this.serializeValue(item));
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.serializeValue(value[key]);
}
}
return result;
}
// 其他类型不序列化
return undefined;
}
/**
* 反序列化单个值
*/
private static deserializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 处理特殊类型标记
if (type === 'object' && value.__type) {
switch (value.__type) {
case 'Date':
return new Date(value.value);
case 'Map':
return new Map(value.value);
case 'Set':
return new Set(value.value);
}
}
// 数组
if (Array.isArray(value)) {
return value.map(item => this.deserializeValue(item));
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.deserializeValue(value[key]);
}
}
return result;
}
return value;
}
/**
* 过滤要序列化的实体和组件
*/
private static filterEntities(scene: IScene, options: SceneSerializationOptions): Entity[] {
const entities = Array.from(scene.entities.buffer);
// 如果指定了组件类型过滤
if (options.components && options.components.length > 0) {
const componentTypeSet = new Set(options.components);
// 只返回拥有指定组件的实体
return entities.filter(entity => {
return Array.from(entity.components).some(component =>
componentTypeSet.has(component.constructor as ComponentType)
);
});
}
return entities;
}
/**
* 构建组件类型注册表
*/
private static buildComponentTypeRegistry(
entities: Entity[]
): Array<{ typeName: string; version: number }> {
const registry = new Map<string, number>();
for (const entity of entities) {
for (const component of entity.components) {
const componentType = component.constructor as ComponentType;
const typeName = getComponentTypeName(componentType);
const metadata = getSerializationMetadata(component);
if (metadata && !registry.has(typeName)) {
registry.set(typeName, metadata.options.version);
}
}
}
return Array.from(registry.entries()).map(([typeName, version]) => ({
typeName,
version
}));
}
/**
* 获取全局组件注册表
*
* 从所有已注册的组件类型构建注册表
*/
private static getGlobalComponentRegistry(): Map<string, ComponentType> {
return ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>;
}
/**
* 验证保存数据的有效性
*
* @param saveData 序列化的数据
* @returns 验证结果
*/
public static validate(saveData: string): {
valid: boolean;
version?: number;
errors?: string[];
} {
const errors: string[] = [];
try {
const data = JSON.parse(saveData);
if (!data.version) {
errors.push('Missing version field');
}
if (!data.entities || !Array.isArray(data.entities)) {
errors.push('Missing or invalid entities field');
}
if (!data.componentTypeRegistry || !Array.isArray(data.componentTypeRegistry)) {
errors.push('Missing or invalid componentTypeRegistry field');
}
return {
valid: errors.length === 0,
version: data.version,
errors: errors.length > 0 ? errors : undefined
};
} catch (error) {
return {
valid: false,
errors: [`JSON parse error: ${error}`]
};
}
}
/**
* 获取保存数据的信息(不完全反序列化)
*
* @param saveData 序列化的数据
* @returns 保存数据的元信息
*/
public static getInfo(saveData: string): {
name: string;
version: number;
timestamp?: number;
entityCount: number;
componentTypeCount: number;
} | null {
try {
const data: SerializedScene = JSON.parse(saveData);
return {
name: data.name,
version: data.version,
timestamp: data.timestamp,
entityCount: data.metadata?.entityCount || data.entities.length,
componentTypeCount: data.componentTypeRegistry.length
};
} catch (error) {
return null;
}
}
}

View File

@@ -0,0 +1,254 @@
/**
* 序列化装饰器
*
* 提供组件级别的序列化支持,包括字段级装饰器和类级装饰器
*/
import { Component } from '../Component';
/**
* 序列化元数据的Symbol键
*/
export const SERIALIZABLE_METADATA = Symbol('SerializableMetadata');
export const SERIALIZE_FIELD = Symbol('SerializeField');
export const SERIALIZE_OPTIONS = Symbol('SerializeOptions');
/**
* 可序列化配置选项
*/
export interface SerializableOptions {
/**
* 序列化版本号,用于数据迁移
*/
version: number;
/**
* 组件类型标识符(可选,默认使用类名)
*/
typeId?: string;
}
/**
* 字段序列化配置
*/
export interface FieldSerializeOptions {
/**
* 自定义序列化器
*/
serializer?: (value: any) => any;
/**
* 自定义反序列化器
*/
deserializer?: (value: any) => any;
/**
* 字段别名(用于序列化后的键名)
*/
alias?: string;
}
/**
* 序列化元数据
*/
export interface SerializationMetadata {
options: SerializableOptions;
fields: Map<string | symbol, FieldSerializeOptions>;
ignoredFields: Set<string | symbol>;
}
/**
* 组件可序列化装饰器
*
* 标记组件类为可序列化,必须与字段装饰器配合使用
*
* @param options 序列化配置选项
*
* @example
* ```typescript
* @ECSComponent('Player')
* @Serializable({ version: 1 })
* class PlayerComponent extends Component {
* @Serialize() name: string = 'Player';
* @Serialize() level: number = 1;
* }
* ```
*/
export function Serializable(options: SerializableOptions) {
return function <T extends new (...args: any[]) => Component>(target: T): T {
if (!options || typeof options.version !== 'number') {
throw new Error('Serializable装饰器必须提供有效的版本号');
}
// 初始化或获取现有元数据
let metadata: SerializationMetadata = (target as any)[SERIALIZABLE_METADATA];
if (!metadata) {
metadata = {
options,
fields: new Map(),
ignoredFields: new Set()
};
(target as any)[SERIALIZABLE_METADATA] = metadata;
} else {
metadata.options = options;
}
return target;
};
}
/**
* 字段序列化装饰器
*
* 标记字段为可序列化
*
* @param options 字段序列化选项(可选)
*
* @example
* ```typescript
* @Serialize()
* name: string = 'Player';
*
* @Serialize({ alias: 'hp' })
* health: number = 100;
* ```
*/
export function Serialize(options?: FieldSerializeOptions) {
return function (target: any, propertyKey: string | symbol) {
const constructor = target.constructor;
// 获取或创建元数据
let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA];
if (!metadata) {
metadata = {
options: { version: 1 }, // 默认版本
fields: new Map(),
ignoredFields: new Set()
};
constructor[SERIALIZABLE_METADATA] = metadata;
}
// 记录字段
metadata.fields.set(propertyKey, options || {});
};
}
/**
* Map序列化装饰器
*
* 专门用于序列化Map类型字段
*
* @example
* ```typescript
* @SerializeAsMap()
* inventory: Map<string, number> = new Map();
* ```
*/
export function SerializeAsMap() {
return function (target: any, propertyKey: string | symbol) {
Serialize({
serializer: (value: Map<any, any>) => {
if (!(value instanceof Map)) {
return null;
}
return Array.from(value.entries());
},
deserializer: (value: any) => {
if (!Array.isArray(value)) {
return new Map();
}
return new Map(value);
}
})(target, propertyKey);
};
}
/**
* Set序列化装饰器
*
* 专门用于序列化Set类型字段
*
* @example
* ```typescript
* @SerializeAsSet()
* tags: Set<string> = new Set();
* ```
*/
export function SerializeAsSet() {
return function (target: any, propertyKey: string | symbol) {
Serialize({
serializer: (value: Set<any>) => {
if (!(value instanceof Set)) {
return null;
}
return Array.from(value);
},
deserializer: (value: any) => {
if (!Array.isArray(value)) {
return new Set();
}
return new Set(value);
}
})(target, propertyKey);
};
}
/**
* 忽略序列化装饰器
*
* 标记字段不参与序列化
*
* @example
* ```typescript
* @IgnoreSerialization()
* tempCache: any = null;
* ```
*/
export function IgnoreSerialization() {
return function (target: any, propertyKey: string | symbol) {
const constructor = target.constructor;
// 获取或创建元数据
let metadata: SerializationMetadata = constructor[SERIALIZABLE_METADATA];
if (!metadata) {
metadata = {
options: { version: 1 },
fields: new Map(),
ignoredFields: new Set()
};
constructor[SERIALIZABLE_METADATA] = metadata;
}
// 记录忽略字段
metadata.ignoredFields.add(propertyKey);
};
}
/**
* 获取组件的序列化元数据
*
* @param componentClass 组件类或组件实例
* @returns 序列化元数据如果组件不可序列化则返回null
*/
export function getSerializationMetadata(componentClass: any): SerializationMetadata | null {
if (!componentClass) {
return null;
}
// 如果是实例,获取其构造函数
const constructor = typeof componentClass === 'function'
? componentClass
: componentClass.constructor;
return constructor[SERIALIZABLE_METADATA] || null;
}
/**
* 检查组件是否可序列化
*
* @param component 组件类或组件实例
* @returns 如果组件可序列化返回true
*/
export function isSerializable(component: any): boolean {
return getSerializationMetadata(component) !== null;
}

View File

@@ -0,0 +1,371 @@
/**
* 版本迁移系统
*
* 提供组件和场景数据的版本迁移支持
*/
import { SerializedComponent } from './ComponentSerializer';
import { SerializedScene } from './SceneSerializer';
/**
* 组件迁移函数
*/
export type ComponentMigrationFunction = (data: any, fromVersion: number, toVersion: number) => any;
/**
* 场景迁移函数
*/
export type SceneMigrationFunction = (
scene: SerializedScene,
fromVersion: number,
toVersion: number
) => SerializedScene;
/**
* 版本迁移管理器
*/
export class VersionMigrationManager {
/**
* 组件迁移函数注册表
* Map<组件类型名, Map<版本号, 迁移函数>>
*/
private static componentMigrations = new Map<string, Map<number, ComponentMigrationFunction>>();
/**
* 场景迁移函数注册表
* Map<版本号, 迁移函数>
*/
private static sceneMigrations = new Map<number, SceneMigrationFunction>();
/**
* 注册组件迁移函数
*
* @param componentType 组件类型名称
* @param fromVersion 源版本号
* @param toVersion 目标版本号
* @param migration 迁移函数
*
* @example
* ```typescript
* // 从版本1迁移到版本2
* VersionMigrationManager.registerComponentMigration(
* 'PlayerComponent',
* 1,
* 2,
* (data) => {
* // 添加新字段
* data.experience = 0;
* return data;
* }
* );
* ```
*/
public static registerComponentMigration(
componentType: string,
fromVersion: number,
toVersion: number,
migration: ComponentMigrationFunction
): void {
if (!this.componentMigrations.has(componentType)) {
this.componentMigrations.set(componentType, new Map());
}
const versionMap = this.componentMigrations.get(componentType)!;
// 使用fromVersion作为key表示"从这个版本迁移"
versionMap.set(fromVersion, migration);
}
/**
* 注册场景迁移函数
*
* @param fromVersion 源版本号
* @param toVersion 目标版本号
* @param migration 迁移函数
*
* @example
* ```typescript
* VersionMigrationManager.registerSceneMigration(
* 1,
* 2,
* (scene) => {
* // 迁移场景结构
* scene.metadata = { ...scene.metadata, migratedFrom: 1 };
* return scene;
* }
* );
* ```
*/
public static registerSceneMigration(
fromVersion: number,
toVersion: number,
migration: SceneMigrationFunction
): void {
this.sceneMigrations.set(fromVersion, migration);
}
/**
* 迁移组件数据
*
* @param component 序列化的组件数据
* @param targetVersion 目标版本号
* @returns 迁移后的组件数据
*/
public static migrateComponent(
component: SerializedComponent,
targetVersion: number
): SerializedComponent {
const currentVersion = component.version;
if (currentVersion === targetVersion) {
return component; // 版本相同,无需迁移
}
const migrations = this.componentMigrations.get(component.type);
if (!migrations) {
console.warn(`No migration path found for component ${component.type}`);
return component;
}
let migratedData = { ...component };
let version = currentVersion;
// 执行迁移链
while (version < targetVersion) {
const migration = migrations.get(version);
if (!migration) {
console.warn(
`Missing migration from version ${version} to ${version + 1} for ${component.type}`
);
break;
}
migratedData.data = migration(migratedData.data, version, version + 1);
version++;
}
migratedData.version = version;
return migratedData;
}
/**
* 迁移场景数据
*
* @param scene 序列化的场景数据
* @param targetVersion 目标版本号
* @returns 迁移后的场景数据
*/
public static migrateScene(scene: SerializedScene, targetVersion: number): SerializedScene {
const currentVersion = scene.version;
if (currentVersion === targetVersion) {
return scene; // 版本相同,无需迁移
}
let migratedScene = { ...scene };
let version = currentVersion;
// 执行场景级迁移
while (version < targetVersion) {
const migration = this.sceneMigrations.get(version);
if (!migration) {
console.warn(`Missing scene migration from version ${version} to ${version + 1}`);
break;
}
migratedScene = migration(migratedScene, version, version + 1);
version++;
}
migratedScene.version = version;
// 迁移所有组件
migratedScene = this.migrateSceneComponents(migratedScene);
return migratedScene;
}
/**
* 迁移场景中所有组件的版本
*/
private static migrateSceneComponents(scene: SerializedScene): SerializedScene {
const migratedScene = { ...scene };
migratedScene.entities = scene.entities.map(entity => ({
...entity,
components: entity.components.map(component => {
// 查找组件的目标版本
const typeInfo = scene.componentTypeRegistry.find(
t => t.typeName === component.type
);
if (typeInfo && typeInfo.version !== component.version) {
return this.migrateComponent(component, typeInfo.version);
}
return component;
}),
children: this.migrateEntitiesComponents(entity.children, scene.componentTypeRegistry)
}));
return migratedScene;
}
/**
* 递归迁移实体的组件
*/
private static migrateEntitiesComponents(
entities: any[],
typeRegistry: Array<{ typeName: string; version: number }>
): any[] {
return entities.map(entity => ({
...entity,
components: entity.components.map((component: SerializedComponent) => {
const typeInfo = typeRegistry.find(t => t.typeName === component.type);
if (typeInfo && typeInfo.version !== component.version) {
return this.migrateComponent(component, typeInfo.version);
}
return component;
}),
children: this.migrateEntitiesComponents(entity.children, typeRegistry)
}));
}
/**
* 清除所有迁移函数
*/
public static clearMigrations(): void {
this.componentMigrations.clear();
this.sceneMigrations.clear();
}
/**
* 获取组件的迁移路径
*
* @param componentType 组件类型名称
* @returns 可用的迁移版本列表
*/
public static getComponentMigrationPath(componentType: string): number[] {
const migrations = this.componentMigrations.get(componentType);
if (!migrations) {
return [];
}
return Array.from(migrations.keys()).sort((a, b) => a - b);
}
/**
* 获取场景的迁移路径
*
* @returns 可用的场景迁移版本列表
*/
public static getSceneMigrationPath(): number[] {
return Array.from(this.sceneMigrations.keys()).sort((a, b) => a - b);
}
/**
* 检查是否可以迁移组件
*
* @param componentType 组件类型名称
* @param fromVersion 源版本
* @param toVersion 目标版本
* @returns 是否存在完整的迁移路径
*/
public static canMigrateComponent(
componentType: string,
fromVersion: number,
toVersion: number
): boolean {
if (fromVersion === toVersion) {
return true;
}
const migrations = this.componentMigrations.get(componentType);
if (!migrations) {
return false;
}
// 检查是否存在完整的迁移路径
for (let v = fromVersion; v < toVersion; v++) {
if (!migrations.has(v)) {
return false;
}
}
return true;
}
/**
* 检查是否可以迁移场景
*
* @param fromVersion 源版本
* @param toVersion 目标版本
* @returns 是否存在完整的迁移路径
*/
public static canMigrateScene(fromVersion: number, toVersion: number): boolean {
if (fromVersion === toVersion) {
return true;
}
// 检查是否存在完整的场景迁移路径
for (let v = fromVersion; v < toVersion; v++) {
if (!this.sceneMigrations.has(v)) {
return false;
}
}
return true;
}
}
/**
* 便捷的迁移构建器
*
* 提供链式API来定义迁移
*/
export class MigrationBuilder {
private componentType?: string;
private fromVersion: number = 1;
private toVersion: number = 2;
/**
* 设置组件类型
*/
public forComponent(componentType: string): this {
this.componentType = componentType;
return this;
}
/**
* 设置版本范围
*/
public fromVersionToVersion(from: number, to: number): this {
this.fromVersion = from;
this.toVersion = to;
return this;
}
/**
* 注册迁移函数
*/
public migrate(migration: ComponentMigrationFunction | SceneMigrationFunction): void {
if (this.componentType) {
VersionMigrationManager.registerComponentMigration(
this.componentType,
this.fromVersion,
this.toVersion,
migration as ComponentMigrationFunction
);
} else {
VersionMigrationManager.registerSceneMigration(
this.fromVersion,
this.toVersion,
migration as SceneMigrationFunction
);
}
}
}

View File

@@ -0,0 +1,62 @@
/**
* ECS序列化系统
*
* 提供完整的场景、实体和组件序列化支持
*/
// 装饰器
export {
Serializable,
Serialize,
SerializeAsMap,
SerializeAsSet,
IgnoreSerialization,
getSerializationMetadata,
isSerializable,
SERIALIZABLE_METADATA,
SERIALIZE_FIELD,
SERIALIZE_OPTIONS
} from './SerializationDecorators';
export type {
SerializableOptions,
FieldSerializeOptions,
SerializationMetadata
} from './SerializationDecorators';
// 组件序列化器
export { ComponentSerializer } from './ComponentSerializer';
export type { SerializedComponent } from './ComponentSerializer';
// 实体序列化器
export { EntitySerializer } from './EntitySerializer';
export type { SerializedEntity } from './EntitySerializer';
// 场景序列化器
export { SceneSerializer } from './SceneSerializer';
export type {
SerializedScene,
SerializationFormat,
DeserializationStrategy,
MigrationFunction,
SceneSerializationOptions,
SceneDeserializationOptions
} from './SceneSerializer';
// 版本迁移
export { VersionMigrationManager, MigrationBuilder } from './VersionMigration';
export type {
ComponentMigrationFunction,
SceneMigrationFunction
} from './VersionMigration';
// 增量序列化
export { IncrementalSerializer, ChangeOperation } from './IncrementalSerializer';
export type {
IncrementalSnapshot,
IncrementalSerializationOptions,
IncrementalSerializationFormat,
EntityChange,
ComponentChange,
SceneDataChange
} from './IncrementalSerializer';

View File

@@ -7,6 +7,7 @@ import type { QuerySystem } from '../Core/QuerySystem';
import { getSystemInstanceTypeName } from '../Decorators';
import { createLogger } from '../../Utils/Logger';
import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem';
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
/**
* 事件监听器记录
@@ -21,17 +22,22 @@ interface EventListenerRecord {
/**
* 实体系统的基类
*
*
* 用于处理一组符合特定条件的实体。系统是ECS架构中的逻辑处理单元
* 负责对拥有特定组件组合的实体执行业务逻辑。
*
*
* 支持泛型参数以提供类型安全的组件访问:
*
* @template TComponents - 系统需要的组件类型数组
*
* @example
* ```typescript
* // 传统方式
* class MovementSystem extends EntitySystem {
* constructor() {
* super(Transform, Velocity);
* super(Matcher.of(Transform, Velocity));
* }
*
*
* protected process(entities: readonly Entity[]): void {
* for (const entity of entities) {
* const transform = entity.getComponent(Transform);
@@ -40,9 +46,26 @@ interface EventListenerRecord {
* }
* }
* }
*
* // 类型安全方式
* class MovementSystem extends EntitySystem<[typeof Transform, typeof Velocity]> {
* constructor() {
* super(Matcher.of(Transform, Velocity));
* }
*
* protected process(entities: readonly Entity[]): void {
* for (const entity of entities) {
* // 类型安全的组件访问
* const [transform, velocity] = this.getComponents(entity);
* transform.position.add(velocity.value);
* }
* }
* }
* ```
*/
export abstract class EntitySystem implements ISystemBase {
export abstract class EntitySystem<
TComponents extends readonly ComponentConstructor[] = []
> implements ISystemBase {
private _updateOrder: number;
private _enabled: boolean;
private _performanceMonitor: PerformanceMonitor;
@@ -793,4 +816,235 @@ export abstract class EntitySystem implements ISystemBase {
protected onDestroy(): void {
// 子类可以重写此方法进行清理操作
}
// ============================================================
// 类型安全的辅助方法
// ============================================================
/**
* 类型安全地获取单个组件
*
* 相比Entity.getComponent此方法保证返回非空值
* 如果组件不存在会抛出错误而不是返回null
*
* @param entity 实体
* @param componentType 组件类型
* @returns 组件实例(保证非空)
* @throws 如果组件不存在则抛出错误
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* for (const entity of entities) {
* const transform = this.requireComponent(entity, Transform);
* // transform 保证非空,类型为 Transform
* }
* }
* ```
*/
protected requireComponent<T extends ComponentConstructor>(
entity: Entity,
componentType: T
): ComponentInstance<T> {
const component = entity.getComponent(componentType as any);
if (!component) {
throw new Error(
`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`
);
}
return component as ComponentInstance<T>;
}
/**
* 批量获取实体的所有必需组件
*
* 根据泛型参数TComponents推断返回类型
* 返回一个元组,包含所有组件实例
*
* @param entity 实体
* @param components 组件类型数组
* @returns 组件实例元组
*
* @example
* ```typescript
* class MySystem extends EntitySystem<[typeof Position, typeof Velocity]> {
* protected process(entities: readonly Entity[]): void {
* for (const entity of entities) {
* const [pos, vel] = this.getComponents(entity, Position, Velocity);
* // pos: Position, vel: Velocity (自动类型推断)
* pos.x += vel.x;
* }
* }
* }
* ```
*/
protected getComponents<T extends readonly ComponentConstructor[]>(
entity: Entity,
...components: T
): { [K in keyof T]: ComponentInstance<T[K]> } {
return components.map((type) =>
this.requireComponent(entity, type)
) as any;
}
/**
* 遍历实体并处理每个实体
*
* 提供更简洁的语法糖,避免手动遍历
*
* @param entities 实体列表
* @param processor 处理函数
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* this.forEach(entities, (entity) => {
* const transform = this.requireComponent(entity, Transform);
* transform.position.y -= 9.8 * Time.deltaTime;
* });
* }
* ```
*/
protected forEach(
entities: readonly Entity[],
processor: (entity: Entity, index: number) => void
): void {
for (let i = 0; i < entities.length; i++) {
processor(entities[i], i);
}
}
/**
* 过滤实体
*
* @param entities 实体列表
* @param predicate 过滤条件
* @returns 过滤后的实体数组
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* const activeEntities = this.filterEntities(entities, (entity) => {
* const health = this.requireComponent(entity, Health);
* return health.value > 0;
* });
* }
* ```
*/
protected filterEntities(
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): Entity[] {
return Array.from(entities).filter(predicate);
}
/**
* 映射实体到另一种类型
*
* @param entities 实体列表
* @param mapper 映射函数
* @returns 映射后的结果数组
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* const positions = this.mapEntities(entities, (entity) => {
* const transform = this.requireComponent(entity, Transform);
* return transform.position;
* });
* }
* ```
*/
protected mapEntities<R>(
entities: readonly Entity[],
mapper: (entity: Entity, index: number) => R
): R[] {
return Array.from(entities).map(mapper);
}
/**
* 查找第一个满足条件的实体
*
* @param entities 实体列表
* @param predicate 查找条件
* @returns 第一个满足条件的实体或undefined
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* const player = this.findEntity(entities, (entity) =>
* entity.hasComponent(PlayerTag)
* );
* }
* ```
*/
protected findEntity(
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): Entity | undefined {
for (let i = 0; i < entities.length; i++) {
if (predicate(entities[i], i)) {
return entities[i];
}
}
return undefined;
}
/**
* 检查是否存在满足条件的实体
*
* @param entities 实体列表
* @param predicate 检查条件
* @returns 是否存在满足条件的实体
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* const hasLowHealth = this.someEntity(entities, (entity) => {
* const health = this.requireComponent(entity, Health);
* return health.value < 20;
* });
* }
* ```
*/
protected someEntity(
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): boolean {
for (let i = 0; i < entities.length; i++) {
if (predicate(entities[i], i)) {
return true;
}
}
return false;
}
/**
* 检查是否所有实体都满足条件
*
* @param entities 实体列表
* @param predicate 检查条件
* @returns 是否所有实体都满足条件
*
* @example
* ```typescript
* protected process(entities: readonly Entity[]): void {
* const allHealthy = this.everyEntity(entities, (entity) => {
* const health = this.requireComponent(entity, Health);
* return health.value > 50;
* });
* }
* ```
*/
protected everyEntity(
entities: readonly Entity[],
predicate: (entity: Entity, index: number) => boolean
): boolean {
for (let i = 0; i < entities.length; i++) {
if (!predicate(entities[i], i)) {
return false;
}
}
return true;
}
}

View File

@@ -198,6 +198,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
protected sharedBuffer: SharedArrayBuffer | null = null;
protected sharedFloatArray: Float32Array | null = null;
private platformAdapter: IPlatformAdapter;
private hasLoggedSyncMode = false;
constructor(matcher?: Matcher, config: WorkerSystemConfig = {}) {
super(matcher);
@@ -449,7 +450,10 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
}
} else {
// 同步处理最后的fallback
this.logger.info(`${this.systemName}: Worker不可用使用同步处理`);
if (!this.hasLoggedSyncMode) {
this.logger.info(`${this.systemName}: Worker不可用使用同步处理`);
this.hasLoggedSyncMode = true;
}
this.processSynchronously(entities);
this.isProcessing = false;
}

View File

@@ -0,0 +1,307 @@
/**
* Entity类型安全工具函数
*
* 提供类型安全的组件操作工具函数无需修改Entity类
*/
import { Entity } from './Entity';
import type { Component } from './Component';
import type { ComponentType } from './Core/ComponentStorage';
import type { ComponentConstructor, ComponentInstance } from '../Types/TypeHelpers';
/**
* 获取组件,如果不存在则抛出错误
*
* @param entity - 实体实例
* @param componentType - 组件类型构造函数
* @returns 组件实例(保证非空)
* @throws {Error} 如果组件不存在
*
* @example
* ```typescript
* const position = requireComponent(entity, Position);
* position.x += 10;
* ```
*/
export function requireComponent<T extends ComponentConstructor>(
entity: Entity,
componentType: T
): ComponentInstance<T> {
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
if (!component) {
throw new Error(
`Component ${componentType.name} not found on entity ${entity.name} (id: ${entity.id})`
);
}
return component;
}
/**
* 尝试获取组件
*
* @param entity - 实体实例
* @param componentType - 组件类型构造函数
* @returns 组件实例或undefined
*
* @example
* ```typescript
* const health = tryGetComponent(entity, Health);
* if (health) {
* health.value -= 10;
* }
* ```
*/
export function tryGetComponent<T extends ComponentConstructor>(
entity: Entity,
componentType: T
): ComponentInstance<T> | undefined {
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
return component !== null ? component : undefined;
}
/**
* 批量获取组件
*
* @param entity - 实体实例
* @param types - 组件类型构造函数数组
* @returns 组件实例元组每个元素可能为null
*
* @example
* ```typescript
* const [pos, vel, health] = getComponents(entity, Position, Velocity, Health);
* if (pos && vel && health) {
* pos.x += vel.dx;
* }
* ```
*/
export function getComponents<T extends readonly ComponentConstructor[]>(
entity: Entity,
...types: T
): { [K in keyof T]: ComponentInstance<T[K]> | null } {
return types.map((type) =>
entity.getComponent(type as unknown as ComponentType<ComponentInstance<typeof type>>)
) as { [K in keyof T]: ComponentInstance<T[K]> | null };
}
/**
* 检查实体是否拥有所有指定的组件
*
* @param entity - 实体实例
* @param types - 组件类型构造函数数组
* @returns 如果拥有所有组件返回true否则返回false
*
* @example
* ```typescript
* if (hasComponents(entity, Position, Velocity)) {
* const pos = entity.getComponent(Position)!;
* const vel = entity.getComponent(Velocity)!;
* }
* ```
*/
export function hasComponents(entity: Entity, ...types: ComponentConstructor[]): boolean {
return types.every((type) => entity.hasComponent(type as unknown as ComponentType));
}
/**
* 检查实体是否拥有至少一个指定的组件
*
* @param entity - 实体实例
* @param types - 组件类型构造函数数组
* @returns 如果拥有任意一个组件返回true否则返回false
*/
export function hasAnyComponent(entity: Entity, ...types: ComponentConstructor[]): boolean {
return types.some((type) => entity.hasComponent(type as unknown as ComponentType));
}
/**
* 添加组件并立即配置
*
* @param entity - 实体实例
* @param component - 组件实例
* @param configure - 配置回调函数
* @returns 实体实例(支持链式调用)
*
* @example
* ```typescript
* addAndConfigure(entity, new Health(), health => {
* health.maxValue = 100;
* health.value = 50;
* });
* ```
*/
export function addAndConfigure<T extends Component>(
entity: Entity,
component: T,
configure: (component: T) => void
): Entity {
entity.addComponent(component);
configure(component);
return entity;
}
/**
* 获取或添加组件
*
* 如果组件已存在则返回现有组件,否则通过工厂函数创建并添加
*
* @param entity - 实体实例
* @param componentType - 组件类型构造函数
* @param factory - 组件工厂函数(仅在组件不存在时调用)
* @returns 组件实例
*
* @example
* ```typescript
* const health = getOrAddComponent(entity, Health, () => new Health(100));
* ```
*/
export function getOrAddComponent<T extends ComponentConstructor>(
entity: Entity,
componentType: T,
factory: () => ComponentInstance<T>
): ComponentInstance<T> {
let component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
if (!component) {
component = factory();
entity.addComponent(component);
}
return component;
}
/**
* 更新组件的部分字段
*
* @param entity - 实体实例
* @param componentType - 组件类型构造函数
* @param data - 要更新的部分数据
* @returns 如果更新成功返回true组件不存在返回false
*
* @example
* ```typescript
* updateComponent(entity, Position, { x: 100 });
* ```
*/
export function updateComponent<T extends ComponentConstructor>(
entity: Entity,
componentType: T,
data: Partial<ComponentInstance<T>>
): boolean {
const component = entity.getComponent(componentType as unknown as ComponentType<ComponentInstance<T>>);
if (!component) {
return false;
}
Object.assign(component, data);
return true;
}
/**
* 类型安全的实体构建器
*
* @example
* ```typescript
* const player = buildEntity(scene.createEntity("Player"))
* .with(new Position(100, 100))
* .with(new Velocity(0, 0))
* .withTag(1)
* .build();
* ```
*/
export class TypedEntityBuilder {
private _entity: Entity;
constructor(entity: Entity) {
this._entity = entity;
}
/**
* 添加组件
*/
with<T extends Component>(component: T): this {
this._entity.addComponent(component);
return this;
}
/**
* 添加并配置组件
*/
withConfigured<T extends Component>(
component: T,
configure: (component: T) => void
): this {
this._entity.addComponent(component);
configure(component);
return this;
}
/**
* 设置标签
*/
withTag(tag: number): this {
this._entity.tag = tag;
return this;
}
/**
* 设置名称
*/
withName(name: string): this {
this._entity.name = name;
return this;
}
/**
* 设置激活状态
*/
withActive(active: boolean): this {
this._entity.active = active;
return this;
}
/**
* 设置启用状态
*/
withEnabled(enabled: boolean): this {
this._entity.enabled = enabled;
return this;
}
/**
* 设置更新顺序
*/
withUpdateOrder(order: number): this {
this._entity.updateOrder = order;
return this;
}
/**
* 添加子实体
*/
withChild(child: Entity): this {
this._entity.addChild(child);
return this;
}
/**
* 完成构建
*/
build(): Entity {
return this._entity;
}
/**
* 获取正在构建的实体
*/
get entity(): Entity {
return this._entity;
}
}
/**
* 创建类型安全的实体构建器
*
* @param entity - 要包装的实体
* @returns 实体构建器
*/
export function buildEntity(entity: Entity): TypedEntityBuilder {
return new TypedEntityBuilder(entity);
}

View File

@@ -1,31 +1,31 @@
/**
* 位枚举
*/
export enum SegmentPart {
LOW = 0,
HIGH = 1,
}
/**
* 基础 64 位段结构
* [低32位高32位]
*/
export interface BitMask64Segment {
/** 低32位bit 0-31 */
lo: number;
/** 高32位bit 32-63 */
hi: number;
}
export type BitMask64Segment = [number,number]
/**
* 位掩码数据结构
* 基础模式64位使用 lo + hi 存储 64 位segments 为空
* 扩展模式128+位lo + hi 作为第一段segments 存储额外的 64 位段
* 基础模式64位使用 base[lo , hi] 存储 64 位segments 为空
* 扩展模式128+位):base[lo , hi] 作为第一段segments 存储额外的 64 位段
* segments[0] 对应 bit 64-127segments[1] 对应 bit 128-191以此类推
*/
export interface BitMask64Data {
/** 低32位bit 0-31 */
lo: number;
/** 高32位bit 32-63 */
hi: number;
base: BitMask64Segment;
/** 扩展段数组,每个元素是一个 64 位段,用于超过 64 位的场景 */
segments?: BitMask64Segment[];
}
export class BitMask64Utils {
/** 零掩码常量所有位都为0 */
public static readonly ZERO: BitMask64Data = { lo: 0, hi: 0 };
public static readonly ZERO: Readonly<BitMask64Data> = { base: [0, 0], segments: undefined };
/**
* 根据位索引创建64位掩码
@@ -34,15 +34,12 @@ export class BitMask64Utils {
* @throws 当位索引超出范围时抛出错误
*/
public static create(bitIndex: number): BitMask64Data {
if (bitIndex < 0 || bitIndex >= 64) {
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
}
if (bitIndex < 32) {
return { lo: 1 << bitIndex, hi: 0 };
} else {
return { lo: 0, hi: 1 << (bitIndex - 32) };
if (bitIndex < 0) {
throw new Error(`Bit index ${bitIndex} out of range [0, ∞)`);
}
const mask: BitMask64Data = { base: [0, 0], segments: undefined };
BitMask64Utils.setBit(mask, bitIndex);
return mask;
}
/**
@@ -51,7 +48,7 @@ export class BitMask64Utils {
* @returns 低32位为输入值、高32位为0的掩码
*/
public static fromNumber(value: number): BitMask64Data {
return { lo: value >>> 0, hi: 0 };
return { base: [value >>> 0, 0], segments: undefined};
}
/**
@@ -61,75 +58,50 @@ export class BitMask64Utils {
* @returns 如果掩码包含bits中的任意位则返回true
*/
public static hasAny(mask: BitMask64Data, bits: BitMask64Data): boolean {
// 检查第一个 64 位段
if ((mask.lo & bits.lo) !== 0 || (mask.hi & bits.hi) !== 0) {
return true;
}
// 如果 bits 没有扩展段,检查完成
if (!bits.segments || bits.segments.length === 0) {
return false;
}
// 如果 bits 有扩展段但 mask 没有,返回 false
if (!mask.segments || mask.segments.length === 0) {
return false;
}
// 检查每个扩展段
const minSegments = Math.min(mask.segments.length, bits.segments.length);
for (let i = 0; i < minSegments; i++) {
const maskSeg = mask.segments[i];
const bitsSeg = bits.segments[i];
if ((maskSeg.lo & bitsSeg.lo) !== 0 || (maskSeg.hi & bitsSeg.hi) !== 0) {
return true;
}
}
return false;
const bitsBase = bits.base;
const maskBase = mask.base;
const bitsSegments = bits.segments;
const maskSegments = mask.segments;
const baseHasAny = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) !== 0 || (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) !== 0;
// 基础区段就包含指定的位,或任意一个参数不含扩展段,直接短路
if(baseHasAny || !bitsSegments || !maskSegments) return baseHasAny;
// 额外检查扩展区域是否包含指定的位 - 如果bitsSegments[index]不存在会被转为NaNNaN的位运算始终返回0
return maskSegments.some((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) !== 0)
}
/**
* 检查掩码是否包含所有指定的位
* 支持扩展模式,自动处理超过 64 位的掩码
* @param mask 要检查的掩码
* @param bits 指定的位模式
* @returns 如果掩码包含bits中的所有位则返回true
*/
public static hasAll(mask: BitMask64Data, bits: BitMask64Data): boolean {
// 检查第一个 64 位段
if ((mask.lo & bits.lo) !== bits.lo || (mask.hi & bits.hi) !== bits.hi) {
return false;
}
const maskBase = mask.base;
const bitsBase = bits.base;
const maskSegments = mask.segments;
const bitsSegments = bits.segments;
const baseHasAll = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) === bitsBase[SegmentPart.LOW] && (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) === bitsBase[SegmentPart.HIGH];
// 基础区域就不包含指定的位或bits没有扩展区段直接短路。
if(!baseHasAll || !bitsSegments) return baseHasAll;
// 如果 bits 没有扩展段,检查完成
if (!bits.segments || bits.segments.length === 0) {
return true;
// 扩展区段的hasAll匹配逻辑
const maskSegmentsLength = maskSegments?.length ?? 0;
// 对mask/bits中都存在的区段进行hasAll判断
if(maskSegments){
for (let i = 0; i < Math.min(maskSegmentsLength,bitsSegments.length); i++) {
if((maskSegments[i][SegmentPart.LOW] & bitsSegments[i][SegmentPart.LOW]) !== bitsSegments[i][SegmentPart.LOW] || (maskSegments[i][SegmentPart.HIGH] & bitsSegments[i][SegmentPart.HIGH]) !== bitsSegments[i][SegmentPart.HIGH]){
// 存在不匹配的位,直接短路
return false;
}
}
}
// 如果 bits 有扩展段但 mask 没有,返回 false
if (!mask.segments || mask.segments.length === 0) {
// 检查 bits 的扩展段是否全为 0
return bits.segments.every(seg => BitMask64Utils.isZero(seg));
}
// 检查每个扩展段
const minSegments = Math.min(mask.segments.length, bits.segments.length);
for (let i = 0; i < minSegments; i++) {
const maskSeg = mask.segments[i];
const bitsSeg = bits.segments[i];
if ((maskSeg.lo & bitsSeg.lo) !== bitsSeg.lo || (maskSeg.hi & bitsSeg.hi) !== bitsSeg.hi) {
// 对mask中不存在但bits中存在的区段进行isZero判断
for (let i = maskSegmentsLength; i < bitsSegments.length; i++) {
if(bitsSegments[i][SegmentPart.LOW] !== 0 || bitsSegments[i][SegmentPart.HIGH] !== 0){
// 存在不为0的区段直接短路
return false;
}
}
// 如果 bits 有更多段,检查这些段是否为空
for (let i = minSegments; i < bits.segments.length; i++) {
if (!BitMask64Utils.isZero(bits.segments[i])) {
return false;
}
}
return true;
}
@@ -140,7 +112,15 @@ export class BitMask64Utils {
* @returns 如果掩码不包含bits中的任何位则返回true
*/
public static hasNone(mask: BitMask64Data, bits: BitMask64Data): boolean {
return (mask.lo & bits.lo) === 0 && (mask.hi & bits.hi) === 0;
const maskBase = mask.base;
const bitsBase = bits.base;
const maskSegments = mask.segments;
const bitsSegments = bits.segments;
const baseHasNone = (maskBase[SegmentPart.LOW] & bitsBase[SegmentPart.LOW]) === 0 && (maskBase[SegmentPart.HIGH] & bitsBase[SegmentPart.HIGH]) === 0;
//不含扩展区域或基础区域就包含指定的位或bits不含拓展区段直接短路。
if(!maskSegments || !baseHasNone || !bitsSegments) return baseHasNone;
// 额外检查扩展区域是否都包含指定的位 - 此时bitsSegments存在,如果bitsSegments[index]不存在会被转为NaNNaN的位运算始终返回0
return maskSegments.every((seg, index) => (seg[SegmentPart.LOW] & bitsSegments[index][SegmentPart.LOW]) === 0 && (seg[SegmentPart.HIGH] & bitsSegments[index][SegmentPart.HIGH]) === 0);
}
/**
@@ -148,8 +128,14 @@ export class BitMask64Utils {
* @param mask 要检查的掩码
* @returns 如果掩码所有位都为0则返回true
*/
public static isZero(mask: BitMask64Data | BitMask64Segment): boolean {
return mask.lo === 0 && mask.hi === 0;
public static isZero(mask: BitMask64Data): boolean {
const baseIsZero = mask.base[SegmentPart.LOW] === 0 && mask.base[SegmentPart.HIGH] === 0;
if(!mask.segments || !baseIsZero){
// 不含扩展区域或基础区域值就不为0直接短路
return baseIsZero;
}
// 额外检查扩展区域是否都为0
return mask.segments.every(seg => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0);
}
/**
@@ -159,42 +145,82 @@ export class BitMask64Utils {
* @returns 如果两个掩码完全相等则返回true
*/
public static equals(a: BitMask64Data, b: BitMask64Data): boolean {
return a.lo === b.lo && a.hi === b.hi;
let baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH];
// base不相等或ab都没有扩展区域位直接返回base比较结果
if(!baseEquals || (!a.segments && !b.segments)) return baseEquals;
// 不能假设ab的segments都存在或长度相同.
const aSegments = a.segments ?? [];
const bSegments = b.segments ?? [];
for (let i = 0; i < Math.max(aSegments.length, bSegments.length); i++) {
const aSeg: BitMask64Segment | undefined = aSegments[i]; // 可能为undefined
const bSeg: BitMask64Segment | undefined = bSegments[i]; // 可能为undefined
if(aSeg && !bSeg){
//bSeg不存在则必须要求aSeg全为0
if(aSeg[SegmentPart.LOW] !== 0 || aSeg[SegmentPart.HIGH] !== 0) return false;
}else if(!aSeg && bSeg){
//aSeg不存在则必须要求bSeg全为0
if(bSeg[SegmentPart.LOW] !== 0 || bSeg[SegmentPart.HIGH] !== 0) return false;
}else{
//理想状态aSeg/bSeg都存在
if(aSeg[SegmentPart.LOW] !== bSeg[SegmentPart.LOW] || aSeg[SegmentPart.HIGH] !== bSeg[SegmentPart.HIGH]) return false;
}
}
return true;
}
/**
* 设置掩码中指定位为1
* 设置掩码中指定位为1,必要时自动扩展
* @param mask 要修改的掩码(原地修改)
* @param bitIndex 位索引,范围 [0, 63]
* @param bitIndex 位索引,不小于零
* @throws 当位索引超出范围时抛出错误
*/
public static setBit(mask: BitMask64Data, bitIndex: number): void {
if (bitIndex < 0 || bitIndex >= 64) {
if (bitIndex < 0) {
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
}
if (bitIndex < 32) {
mask.lo |= (1 << bitIndex);
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex)!;
const mod = bitIndex & 63; // bitIndex % 64 优化方案
if(mod < 32){
targetSeg[SegmentPart.LOW] |= (1 << mod);
} else {
mask.hi |= (1 << (bitIndex - 32));
targetSeg[SegmentPart.HIGH] |= (1 << (mod - 32));
}
}
/**
* 清除掩码中指定位0
* 获取掩码中指定位,如果位超出当前掩码的区段长度,则直接返回0
* @param mask 掩码
* @param bitIndex 位索引,不小于零
*/
public static getBit(mask: BitMask64Data, bitIndex: number): boolean {
if (bitIndex < 0) {
return false;
}
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex, false);
if(!targetSeg) return false;
const mod = bitIndex & 63; // bitIndex % 64 优化方案
if(mod < 32){
return (targetSeg[SegmentPart.LOW] & (1 << mod)) !== 0;
} else {
return (targetSeg[SegmentPart.HIGH] & (1 << (mod - 32))) !== 0;
}
}
/**
* 清除掩码中指定位为0如果位超出当前掩码的区段长度则什么也不做
* @param mask 要修改的掩码(原地修改)
* @param bitIndex 位索引,范围 [0, 63]
* @throws 当位索引超出范围时抛出错误
* @param bitIndex 位索引,不小于0
*/
public static clearBit(mask: BitMask64Data, bitIndex: number): void {
if (bitIndex < 0 || bitIndex >= 64) {
if (bitIndex < 0) {
throw new Error(`Bit index ${bitIndex} out of range [0, 63]`);
}
if (bitIndex < 32) {
mask.lo &= ~(1 << bitIndex);
const targetSeg = BitMask64Utils.getSegmentByBitIndex(mask, bitIndex, false);
if(!targetSeg) return;
const mod = bitIndex & 63; // bitIndex % 64 优化方案
if(mod < 32){
targetSeg[SegmentPart.LOW] &= ~(1 << mod);
} else {
mask.hi &= ~(1 << (bitIndex - 32));
targetSeg[SegmentPart.HIGH] &= ~(1 << (mod - 32));
}
}
@@ -204,24 +230,26 @@ export class BitMask64Utils {
* @param other 用于按位或的掩码
*/
public static orInPlace(target: BitMask64Data, other: BitMask64Data): void {
target.lo |= other.lo;
target.hi |= other.hi;
target.base[SegmentPart.LOW] |= other.base[SegmentPart.LOW];
target.base[SegmentPart.HIGH] |= other.base[SegmentPart.HIGH];
// 处理扩展段
if (other.segments && other.segments.length > 0) {
const otherSegments = other.segments;
if (otherSegments && otherSegments.length > 0) {
if (!target.segments) {
target.segments = [];
}
const targetSegments = target.segments;
// 确保 target 有足够的段
while (target.segments.length < other.segments.length) {
target.segments.push({ lo: 0, hi: 0 });
while (targetSegments.length < otherSegments.length) {
targetSegments.push([0, 0]);
}
// 对每个段执行或操作
for (let i = 0; i < other.segments.length; i++) {
target.segments[i].lo |= other.segments[i].lo;
target.segments[i].hi |= other.segments[i].hi;
for (let i = 0; i < otherSegments.length; i++) {
targetSegments[i][SegmentPart.LOW] |= otherSegments[i][SegmentPart.LOW];
targetSegments[i][SegmentPart.HIGH] |= otherSegments[i][SegmentPart.HIGH];
}
}
}
@@ -232,8 +260,27 @@ export class BitMask64Utils {
* @param other 用于按位与的掩码
*/
public static andInPlace(target: BitMask64Data, other: BitMask64Data): void {
target.lo &= other.lo;
target.hi &= other.hi;
target.base[SegmentPart.LOW] &= other.base[SegmentPart.LOW];
target.base[SegmentPart.HIGH] &= other.base[SegmentPart.HIGH];
// 处理扩展段
const otherSegments = other.segments;
if (otherSegments && otherSegments.length > 0) {
if (!target.segments) {
target.segments = [];
}
const targetSegments = target.segments;
// 确保 target 有足够的段
while (targetSegments.length < otherSegments.length) {
targetSegments.push([0, 0]);
}
// 对每个段执行与操作
for (let i = 0; i < otherSegments.length; i++) {
targetSegments[i][SegmentPart.LOW] &= otherSegments[i][SegmentPart.LOW];
targetSegments[i][SegmentPart.HIGH] &= otherSegments[i][SegmentPart.HIGH];
}
}
}
/**
@@ -242,8 +289,25 @@ export class BitMask64Utils {
* @param other 用于按位异或的掩码
*/
public static xorInPlace(target: BitMask64Data, other: BitMask64Data): void {
target.lo ^= other.lo;
target.hi ^= other.hi;
target.base[SegmentPart.LOW] ^= other.base[SegmentPart.LOW];
target.base[SegmentPart.HIGH] ^= other.base[SegmentPart.HIGH];
// 处理扩展段
const otherSegments = other.segments;
if (!otherSegments || otherSegments.length == 0) return;
if (!target.segments) target.segments = [];
const targetSegments = target.segments;
// 确保 target 有足够的段
while (targetSegments.length < otherSegments.length) {
targetSegments.push([0, 0]);
}
// 对每个段执行异或操作
for (let i = 0; i < otherSegments.length; i++) {
targetSegments[i][SegmentPart.LOW] ^= otherSegments[i][SegmentPart.LOW];
targetSegments[i][SegmentPart.HIGH] ^= otherSegments[i][SegmentPart.HIGH];
}
}
/**
@@ -251,18 +315,43 @@ export class BitMask64Utils {
* @param mask 要清除的掩码(原地修改)
*/
public static clear(mask: BitMask64Data): void {
mask.lo = 0;
mask.hi = 0;
mask.base[SegmentPart.LOW] = 0;
mask.base[SegmentPart.HIGH] = 0;
for (let i = 0; i < (mask.segments?.length ?? 0); i++) {
const seg = mask.segments![i];
seg[SegmentPart.LOW] = 0;
seg[SegmentPart.HIGH] = 0;
}
}
/**
* 将源掩码的值复制到目标掩码
* 将源掩码的值复制到目标掩码如果source包含扩展段则target也会至少扩展到source扩展段的长度
* @param source 源掩码
* @param target 目标掩码(原地修改)
*/
public static copy(source: BitMask64Data, target: BitMask64Data): void {
target.lo = source.lo;
target.hi = source.hi;
BitMask64Utils.clear(target);
target.base[SegmentPart.LOW] = source.base[SegmentPart.LOW];
target.base[SegmentPart.HIGH] = source.base[SegmentPart.HIGH];
// source没有扩展段直接退出
if(!source.segments || source.segments.length == 0) return;
// 没有拓展段,则直接复制数组
if(!target.segments){
target.segments = source.segments.map(seg => [...seg]);
return;
}
// source有扩展段target扩展段不足则补充长度
const copyLength = source.segments.length - target.segments.length;
for (let i = 0; i < copyLength; i++) {
target.segments.push([0,0]);
}
// 逐个重写
for (let i = 0; i < length; i++) {
const targetSeg = target.segments![i];
const sourSeg = source.segments![i];
targetSeg[SegmentPart.LOW] = sourSeg[SegmentPart.LOW];
targetSeg[SegmentPart.HIGH] = sourSeg[SegmentPart.HIGH];
}
}
/**
@@ -271,36 +360,65 @@ export class BitMask64Utils {
* @returns 新的掩码对象,内容与源掩码相同
*/
public static clone(mask: BitMask64Data): BitMask64Data {
return { lo: mask.lo, hi: mask.hi };
return {
base: mask.base.slice() as BitMask64Segment,
segments: mask.segments ? mask.segments.map(seg => [...seg]) : undefined
};
}
/**
* 将掩码转换为字符串表示
* 将掩码转换为字符串表示,每个区段之间将使用空格分割。
* @param mask 要转换的掩码
* @param radix 进制支持2二进制或16十六进制默认为2
* @param radix 进制支持2二进制或16十六进制默认为2其他的值被视为2
* @param printHead 打印头
* @returns 掩码的字符串表示二进制不带前缀十六进制带0x前缀
* @throws 当进制不支持时抛出错误
*/
public static toString(mask: BitMask64Data, radix: number = 2): string {
if (radix === 2) {
if (mask.hi === 0) {
return mask.lo.toString(2);
} else {
const hiBits = mask.hi.toString(2);
const loBits = mask.lo.toString(2).padStart(32, '0');
return hiBits + loBits;
public static toString(mask: BitMask64Data, radix: 2 | 16 = 2, printHead: boolean = false): string {
if(radix != 2 && radix != 16) radix = 2;
const totalLength = mask.segments?.length ?? 0;
let result: string = '';
if(printHead){
let paddingLength = 0;
if(radix === 2){
paddingLength = 64 + 1 + 1;
}else{
paddingLength = 16 + 2 + 1;
}
} else if (radix === 16) {
if (mask.hi === 0) {
return '0x' + mask.lo.toString(16).toUpperCase();
} else {
const hiBits = mask.hi.toString(16).toUpperCase();
const loBits = mask.lo.toString(16).toUpperCase().padStart(8, '0');
return '0x' + hiBits + loBits;
for (let i = 0; i <= totalLength; i++) {
const title = i === 0 ? '0 (Base):' : `${i} (${64 * i}):`;
result += title.toString().padEnd(paddingLength);
}
} else {
throw new Error('Only radix 2 and 16 are supported');
result += '\n';
}
for (let i = -1; i < totalLength; i++) {
let segResult = '';
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
let hi = bitMaskData[SegmentPart.HIGH];
let lo = bitMaskData[SegmentPart.LOW];
if(radix == 2){
const hiBits = hi.toString(2).padStart(32, '0');
const loBits = lo.toString(2).padStart(32, '0');
segResult = hiBits + '_' + loBits; //高低位之间使用_隔离
}else{
let hiBits = hi ? hi.toString(16).toUpperCase() : '';
if(printHead){
// 存在标头,则输出高位之前需要补齐位数
hiBits = hiBits.padStart(8, '0');
}
let loBits = lo.toString(16).toUpperCase();
if(hiBits){
// 存在高位 则输出低位之前需要补齐位数
loBits = loBits.padStart(8, '0');
}
segResult = '0x' + hiBits + loBits;
}
if(i === -1)
result += segResult;
else
result += ' ' + segResult; // 不同段之间使用空格隔离
}
return result;
}
/**
@@ -310,126 +428,49 @@ export class BitMask64Utils {
*/
public static popCount(mask: BitMask64Data): number {
let count = 0;
let lo = mask.lo;
let hi = mask.hi;
while (lo) {
lo &= lo - 1;
count++;
for (let i = -1; i < (mask.segments?.length ?? 0); i++) {
const bitMaskData = i == -1 ? mask.base : mask.segments![i];
let lo = bitMaskData[SegmentPart.LOW];
let hi = bitMaskData[SegmentPart.HIGH];
while (lo) {
lo &= lo - 1;
count++;
}
while (hi) {
hi &= hi - 1;
count++;
}
}
while (hi) {
hi &= hi - 1;
count++;
}
return count;
}
/**
* 设置扩展位(支持超过 64 位的索引)
* @param mask 要修改的掩码(原地修改)
* @param bitIndex 位索引(可以超过 63
* 获取包含目标位的BitMask64Segment
* @param mask 要操作的掩码
* @param bitIndex 目标
* @param createNewSegment 如果bitIndex超过了当前范围是否自动补充扩展区域默认为真
* @private
*/
public static setBitExtended(mask: BitMask64Data, bitIndex: number): void {
if (bitIndex < 0) {
throw new Error('Bit index cannot be negative');
}
if (bitIndex < 64) {
BitMask64Utils.setBit(mask, bitIndex);
return;
}
// 计算段索引和段内位索引
const segmentIndex = Math.floor(bitIndex / 64) - 1;
const localBitIndex = bitIndex % 64;
// 确保 segments 数组存在
if (!mask.segments) {
mask.segments = [];
}
// 扩展 segments 数组
while (mask.segments.length <= segmentIndex) {
mask.segments.push({ lo: 0, hi: 0 });
}
// 设置对应段的位
const segment = mask.segments[segmentIndex];
if (localBitIndex < 32) {
segment.lo |= (1 << localBitIndex);
} else {
segment.hi |= (1 << (localBitIndex - 32));
}
}
/**
* 获取扩展位(支持超过 64 位的索引)
* @param mask 要检查的掩码
* @param bitIndex 位索引(可以超过 63
* @returns 如果位被设置则返回 true
*/
public static getBitExtended(mask: BitMask64Data, bitIndex: number): boolean {
if (bitIndex < 0) {
return false;
}
if (bitIndex < 64) {
const testMask = BitMask64Utils.create(bitIndex);
return BitMask64Utils.hasAny(mask, testMask);
}
if (!mask.segments) {
return false;
}
const segmentIndex = Math.floor(bitIndex / 64) - 1;
if (segmentIndex >= mask.segments.length) {
return false;
}
const localBitIndex = bitIndex % 64;
const segment = mask.segments[segmentIndex];
if (localBitIndex < 32) {
return (segment.lo & (1 << localBitIndex)) !== 0;
} else {
return (segment.hi & (1 << (localBitIndex - 32))) !== 0;
}
}
/**
* 清除扩展位(支持超过 64 位的索引)
* @param mask 要修改的掩码(原地修改)
* @param bitIndex 位索引(可以超过 63
*/
public static clearBitExtended(mask: BitMask64Data, bitIndex: number): void {
if (bitIndex < 0) {
return;
}
if (bitIndex < 64) {
BitMask64Utils.clearBit(mask, bitIndex);
return;
}
if (!mask.segments) {
return;
}
const segmentIndex = Math.floor(bitIndex / 64) - 1;
if (segmentIndex >= mask.segments.length) {
return;
}
const localBitIndex = bitIndex % 64;
const segment = mask.segments[segmentIndex];
if (localBitIndex < 32) {
segment.lo &= ~(1 << localBitIndex);
} else {
segment.hi &= ~(1 << (localBitIndex - 32));
private static getSegmentByBitIndex(mask: BitMask64Data,bitIndex: number, createNewSegment: boolean = true): BitMask64Segment | null{
if(bitIndex <= 63){
// 基础位
return mask.base;
}else{
// 扩展位
let segments = mask.segments;
if(!segments) {
if(!createNewSegment) return null;
segments = mask.segments = [];
}
const targetSegIndex = (bitIndex >> 6) - 1; // Math.floor(bitIndex / 64) - 1的位运算优化
if(segments.length <= targetSegIndex){
if(!createNewSegment) return null;
const diff = targetSegIndex - segments.length + 1;
for (let i = 0; i < diff; i++) {
segments.push([0, 0]);
}
}
return segments[targetSegIndex];
}
}
}

View File

@@ -0,0 +1,143 @@
import { BitMask64Data } from "./BigIntCompatibility";
// FlatHashMapFast.ts
/**
* 高性能 HashMap使用BitMask64Data作为Key。内部计算两层哈希
* - primaryHash: MurmurHash3(seed1) => 定位 bucket
* - secondaryHash: MurmurHash3(seed2) => 处理 bucket 内碰撞判定
*
* 理论上在1e5数量数据规模下碰撞概率在数学意义上的可忽略。
* 在本地测试中,一千万次连续/随机BitMask64Data生成未发生一级哈希冲突考虑到使用场景原型系统、组件系统等远达不到此数量级因此可安全用于生产环境。
*/
export class BitMaskHashMap<T> {
private buckets: Map<number, [number, T][]> = new Map();
private _size = 0;
constructor() {}
get size(): number {
return this._size;
}
get innerBuckets(): Map<number, [number, T][]> {
return this.buckets;
}
/** MurmurHash3 (32bit) 简化实现 */
private murmur32(key: BitMask64Data, seed: number): number {
let h = seed >>> 0;
const mix = (k: number) => {
k = Math.imul(k, 0xcc9e2d51) >>> 0; // 第一个 32 位魔术常数
k = (k << 15) | (k >>> 17);
k = Math.imul(k, 0x1b873593) >>> 0; // 第二个 32 位魔术常数
h ^= k;
h = (h << 13) | (h >>> 19);
h = (Math.imul(h, 5) + 0xe6546b64) >>> 0;
};
// base
mix(key.base[0] >>> 0);
mix(key.base[1] >>> 0);
// segments
if (key.segments) {
for (const seg of key.segments) {
mix(seg[0] >>> 0);
mix(seg[1] >>> 0);
}
}
h ^= (key.segments ? key.segments.length * 8 : 8);
h ^= h >>> 16;
h = Math.imul(h, 0x85ebca6b) >>> 0;
h ^= h >>> 13;
h = Math.imul(h, 0xc2b2ae35) >>> 0;
h ^= h >>> 16;
return h >>> 0;
}
/** primaryHash + secondaryHash 计算 */
private getHashes(key: BitMask64Data): [number, number] {
const primary = this.murmur32(key, 0x9747b28c); // seed1
const secondary = this.murmur32(key, 0x12345678); // seed2
return [primary, secondary];
}
set(key: BitMask64Data, value: T): this {
const [primary, secondary] = this.getHashes(key);
let bucket = this.buckets.get(primary);
if (!bucket) {
bucket = [];
this.buckets.set(primary, bucket);
}
// 查找是否存在 secondaryHash
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === secondary) {
bucket[i][1] = value;
return this;
}
}
// 新增
bucket.push([secondary, value]);
this._size++;
return this;
}
get(key: BitMask64Data): T | undefined {
const [primary, secondary] = this.getHashes(key);
const bucket = this.buckets.get(primary);
if (!bucket) return undefined;
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === secondary) {
return bucket[i][1];
}
}
return undefined;
}
has(key: BitMask64Data): boolean {
return this.get(key) !== undefined;
}
delete(key: BitMask64Data): boolean {
const [primary, secondary] = this.getHashes(key);
const bucket = this.buckets.get(primary);
if (!bucket) return false;
for (let i = 0; i < bucket.length; i++) {
if (bucket[i][0] === secondary) {
bucket.splice(i, 1);
this._size--;
if (bucket.length === 0) {
this.buckets.delete(primary);
}
return true;
}
}
return false;
}
clear(): void {
this.buckets.clear();
this._size = 0;
}
*entries(): IterableIterator<[BitMask64Data, T]> {
for (const [_, bucket] of this.buckets) {
for (const [secondary, value] of bucket) {
// 无法还原原始 key只存二级 hash所以 entries 返回不了 key
yield [undefined as any, value];
}
}
}
*values(): IterableIterator<T> {
for (const bucket of this.buckets.values()) {
for (const [_, value] of bucket) {
yield value;
}
}
}
}

View File

@@ -1,9 +1,8 @@
import { BitMask64Data, BitMask64Utils } from './BigIntCompatibility';
import { SegmentPart, BitMask64Data, BitMask64Utils } from './BigIntCompatibility';
/**
* 位集合类,用于高效的位操作
* 自动扩展支持:默认 64 位,超过时自动扩展到 128/256 位
* 扩展模式性能略有下降,但仍然比数组遍历快得多
* 支持任意位的位运算操作.
*/
export class Bits {
/** 存储位数据的掩码,支持扩展 */
@@ -37,7 +36,7 @@ export class Bits {
throw new Error('Bit index cannot be negative');
}
BitMask64Utils.setBitExtended(this._value, index);
BitMask64Utils.setBit(this._value, index);
}
/**
@@ -50,7 +49,7 @@ export class Bits {
throw new Error('Bit index cannot be negative');
}
BitMask64Utils.clearBitExtended(this._value, index);
BitMask64Utils.clearBit(this._value, index);
}
/**
@@ -59,7 +58,7 @@ export class Bits {
* @returns 如果位被设置为1则返回true否则返回false
*/
public get(index: number): boolean {
return BitMask64Utils.getBitExtended(this._value, index);
return BitMask64Utils.getBit(this._value, index);
}
/**
@@ -163,16 +162,16 @@ export class Bits {
if (maxBits <= 32) {
const mask = (1 << maxBits) - 1;
result._value.lo = (~result._value.lo) & mask;
result._value.hi = 0;
result._value.base[SegmentPart.LOW] = (~result._value.base[SegmentPart.LOW]) & mask;
result._value.base[SegmentPart.HIGH] = 0;
} else {
result._value.lo = ~result._value.lo;
result._value.base[SegmentPart.LOW] = ~result._value.base[SegmentPart.LOW];
if (maxBits < 64) {
const remainingBits = maxBits - 32;
const mask = (1 << remainingBits) - 1;
result._value.hi = (~result._value.hi) & mask;
result._value.base[SegmentPart.HIGH] = (~result._value.base[SegmentPart.HIGH]) & mask;
} else {
result._value.hi = ~result._value.hi;
result._value.base[SegmentPart.HIGH] = ~result._value.base[SegmentPart.HIGH];
}
}
@@ -237,9 +236,10 @@ export class Bits {
* @param maxBits 最大位数默认为64
* @returns 二进制字符串表示每8位用空格分隔
*/
public toBinaryString(maxBits: number = 64): string {
if (maxBits > 64) maxBits = 64;
public toBinaryString(maxBits: number = 0): string {
if(maxBits == 0){
maxBits = 64 + (this._value.segments ? this._value.segments.length * 64 : 0);
}
let result = '';
for (let i = maxBits - 1; i >= 0; i--) {
result += this.get(i) ? '1' : '0';
@@ -265,16 +265,16 @@ export class Bits {
*/
public static fromBinaryString(binaryString: string): Bits {
const cleanString = binaryString.replace(/\s/g, '');
let data: BitMask64Data;
let data: BitMask64Data = { base: undefined!, segments: undefined};
if (cleanString.length <= 32) {
const num = parseInt(cleanString, 2);
data = { lo: num >>> 0, hi: 0 };
data.base = [num >>> 0, 0];
} else {
const loBits = cleanString.substring(cleanString.length - 32);
const hiBits = cleanString.substring(0, cleanString.length - 32);
const lo = parseInt(loBits, 2);
const hi = parseInt(hiBits, 2);
data = { lo: lo >>> 0, hi: hi >>> 0 };
data.base = [lo >>> 0, hi >>> 0];
}
return new Bits(data);
}
@@ -286,16 +286,16 @@ export class Bits {
*/
public static fromHexString(hexString: string): Bits {
const cleanString = hexString.replace(/^0x/i, '');
let data: BitMask64Data;
let data: BitMask64Data = { base: undefined!, segments: undefined};
if (cleanString.length <= 8) {
const num = parseInt(cleanString, 16);
data = { lo: num >>> 0, hi: 0 };
data.base = [num >>> 0, 0];
} else {
const loBits = cleanString.substring(cleanString.length - 8);
const hiBits = cleanString.substring(0, cleanString.length - 8);
const lo = parseInt(loBits, 16);
const hi = parseInt(hiBits, 16);
data = { lo: lo >>> 0, hi: hi >>> 0 };
data.base = [lo >>> 0, hi >>> 0];
}
return new Bits(data);
}
@@ -318,16 +318,16 @@ export class Bits {
return -1;
}
if (this._value.hi !== 0) {
if (this._value.base[SegmentPart.HIGH] !== 0) {
for (let i = 31; i >= 0; i--) {
if ((this._value.hi & (1 << i)) !== 0) {
if ((this._value.base[SegmentPart.HIGH] & (1 << i)) !== 0) {
return i + 32;
}
}
}
for (let i = 31; i >= 0; i--) {
if ((this._value.lo & (1 << i)) !== 0) {
if ((this._value.base[SegmentPart.LOW] & (1 << i)) !== 0) {
return i;
}
}
@@ -345,13 +345,13 @@ export class Bits {
}
for (let i = 0; i < 32; i++) {
if ((this._value.lo & (1 << i)) !== 0) {
if ((this._value.base[SegmentPart.LOW] & (1 << i)) !== 0) {
return i;
}
}
for (let i = 0; i < 32; i++) {
if ((this._value.hi & (1 << i)) !== 0) {
if ((this._value.base[SegmentPart.HIGH] & (1 << i)) !== 0) {
return i + 32;
}
}

View File

@@ -30,40 +30,45 @@ export interface IWorldManagerConfig {
/**
* World管理器 - 管理所有World实例
*
* WorldManager是全局单例负责管理所有World的生命周期
*
* WorldManager负责管理多个独立的World实例
* 每个World都是独立的ECS环境可以包含多个Scene。
*
* 设计理念
* - Core负责单Scene的传统ECS管理
* - World负责多Scene的管理和协调
* - WorldManager负责多World的全局管理
*
*
* 适用场景
* - MMO游戏的多房间管理
* - 服务器端的多游戏实例
* - 需要完全隔离的多个游戏环境
*
* @example
* ```typescript
* // 获取全局WorldManager
* const worldManager = WorldManager.getInstance();
*
* // 创建WorldManager实例
* const worldManager = new WorldManager({
* maxWorlds: 100,
* autoCleanup: true
* });
*
* // 创建游戏房间World
* const roomWorld = worldManager.createWorld('room_001', {
* const room1 = worldManager.createWorld('room_001', {
* name: 'GameRoom_001',
* maxScenes: 5
* });
*
* // 在游戏循环中更新所有World
* worldManager.updateAll(deltaTime);
* room1.setActive(true);
*
* // 游戏循环
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime);
* worldManager.updateAll(); // 更新所有活跃World
* }
* ```
*/
export class WorldManager {
private static _instance: WorldManager | null = null;
private readonly _config: IWorldManagerConfig;
private readonly _worlds: Map<string, World> = new Map();
private readonly _activeWorlds: Set<string> = new Set();
private _cleanupTimer: NodeJS.Timeout | null = null;
private _cleanupTimer: ReturnType<typeof setInterval> | null = null;
private _isRunning: boolean = false;
private constructor(config: IWorldManagerConfig = {}) {
public constructor(config: IWorldManagerConfig = {}) {
this._config = {
maxWorlds: 50,
autoCleanup: true,
@@ -72,6 +77,9 @@ export class WorldManager {
...config
};
// 默认启动运行状态
this._isRunning = true;
logger.info('WorldManager已初始化', {
maxWorlds: this._config.maxWorlds,
autoCleanup: this._config.autoCleanup,
@@ -81,26 +89,6 @@ export class WorldManager {
this.startCleanupTimer();
}
/**
* 获取WorldManager单例实例
*/
public static getInstance(config?: IWorldManagerConfig): WorldManager {
if (!this._instance) {
this._instance = new WorldManager(config);
}
return this._instance;
}
/**
* 重置WorldManager实例主要用于测试
*/
public static reset(): void {
if (this._instance) {
this._instance.destroy();
this._instance = null;
}
}
// ===== World管理 =====
/**
@@ -205,9 +193,37 @@ export class WorldManager {
// ===== 批量操作 =====
/**
* 更新所有活跃的World
*
* 应该在每帧的游戏循环中调用。
* 会自动更新所有活跃World的全局系统和场景。
*
* @example
* ```typescript
* function gameLoop(deltaTime: number) {
* Core.update(deltaTime); // 更新全局服务
* worldManager.updateAll(); // 更新所有World
* }
* ```
*/
public updateAll(): void {
if (!this._isRunning) return;
for (const worldId of this._activeWorlds) {
const world = this._worlds.get(worldId);
if (world && world.isActive) {
// 更新World的全局System
world.updateGlobalSystems();
// 更新World中的所有Scene
world.updateScenes();
}
}
}
/**
* 获取所有激活的World
* 注意此方法供Core.update()使用
*/
public getActiveWorlds(): World[] {
const activeWorlds: World[] = [];

View File

@@ -6,9 +6,11 @@ export * from './Utils';
export * from './Decorators';
export { Scene } from './Scene';
export { IScene, ISceneFactory, ISceneConfig } from './IScene';
export { SceneManager } from './SceneManager';
export { World, IWorldConfig } from './World';
export { WorldManager, IWorldManagerConfig } from './WorldManager';
export * from './Core/Events';
export * from './Core/Query';
export * from './Core/Storage';
export * from './Core/StorageDecorators';
export * from './Core/StorageDecorators';
export * from './Serialization';

View File

@@ -0,0 +1,295 @@
/**
* TypeScript类型工具集
*
* 提供高级类型推断和类型安全的工具类型
*/
import type { IComponent } from './index';
import { Component } from '../ECS/Component';
/**
* 组件类型提取器
* 从组件构造函数中提取实例类型
*/
export type ComponentInstance<T> = T extends new (...args: any[]) => infer R ? R : never;
/**
* 组件构造函数类型
*
* 与 ComponentType 保持一致,避免类型转换
*/
export type ComponentConstructor<T extends IComponent = IComponent> = new (...args: any[]) => T;
/**
* 组件类型的通用约束
*
* 用于确保类型参数是有效的组件构造函数
*/
export type AnyComponentConstructor = ComponentConstructor<any>;
/**
* 多组件类型提取
* 从组件构造函数数组中提取所有实例类型的联合
*/
export type ExtractComponents<T extends readonly ComponentConstructor[]> = {
[K in keyof T]: ComponentInstance<T[K]>;
};
/**
* 组件类型映射
* 将组件构造函数数组转换为实例类型的元组
*/
export type ComponentTypeMap<T extends readonly ComponentConstructor[]> = {
[K in keyof T]: T[K] extends ComponentConstructor<infer C> ? C : never;
};
/**
* 实体with组件的类型
* 表示一个实体确定拥有某些组件
*/
export interface EntityWithComponents<T extends readonly ComponentConstructor[]> {
readonly id: number;
readonly name: string;
/**
* 类型安全的组件获取
* 确保返回非空的组件实例
*/
getComponent<C extends ComponentConstructor>(componentType: C): ComponentInstance<C>;
/**
* 检查是否拥有组件
*/
hasComponent<C extends ComponentConstructor>(componentType: C): boolean;
/**
* 获取所有组件
*/
readonly components: ComponentTypeMap<T>;
}
/**
* 查询结果类型
* 根据查询条件推断实体必定拥有的组件
*/
export type QueryResult<
All extends readonly ComponentConstructor[] = [],
Any extends readonly ComponentConstructor[] = [],
None extends readonly ComponentConstructor[] = []
> = {
/**
* 实体列表确保拥有All中的所有组件
*/
readonly entities: ReadonlyArray<EntityWithComponents<All>>;
/**
* 实体数量
*/
readonly length: number;
/**
* 遍历实体
*/
forEach(callback: (entity: EntityWithComponents<All>, index: number) => void): void;
/**
* 映射转换
*/
map<R>(callback: (entity: EntityWithComponents<All>, index: number) => R): R[];
/**
* 过滤实体
*/
filter(predicate: (entity: EntityWithComponents<All>, index: number) => boolean): QueryResult<All, Any, None>;
};
/**
* System处理的实体类型
* 根据Matcher推断System处理的实体类型
*/
export type SystemEntityType<M> = M extends {
getCondition(): {
all: infer All extends readonly ComponentConstructor[];
};
}
? EntityWithComponents<All>
: never;
/**
* 组件字段类型提取
* 提取组件中所有可序列化的字段
*/
export type SerializableFields<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
/**
* 只读组件类型
* 将组件的所有字段转为只读
*/
export type ReadonlyComponent<T extends IComponent> = {
readonly [K in keyof T]: T[K];
};
/**
* 部分组件类型
* 用于组件更新操作
*/
export type PartialComponent<T extends IComponent> = {
[K in SerializableFields<T>]?: T[K];
};
/**
* 组件类型约束
* 确保类型参数是有效的组件
*/
export type ValidComponent<T> = T extends Component ? T : never;
/**
* 组件数组约束
* 确保数组中的所有元素都是组件构造函数
*/
export type ValidComponentArray<T extends readonly any[]> = T extends readonly ComponentConstructor[]
? T
: never;
/**
* 事件处理器类型
* 提供类型安全的事件处理
*/
export type TypedEventHandler<T> = (data: T) => void | Promise<void>;
/**
* 系统生命周期钩子类型
*/
export interface SystemLifecycleHooks<T extends readonly ComponentConstructor[]> {
/**
* 实体添加到系统时调用
*/
onAdded?: (entity: EntityWithComponents<T>) => void;
/**
* 实体从系统移除时调用
*/
onRemoved?: (entity: EntityWithComponents<T>) => void;
/**
* 系统初始化时调用
*/
onInitialize?: () => void;
/**
* 系统销毁时调用
*/
onDestroy?: () => void;
}
/**
* Fluent API构建器类型
*/
export interface TypeSafeBuilder<T> {
/**
* 完成构建
*/
build(): T;
}
/**
* 组件池类型
*/
export interface ComponentPool<T extends IComponent> {
/**
* 从池中获取组件实例
*/
obtain(): T;
/**
* 归还组件到池中
*/
free(component: T): void;
/**
* 清空池
*/
clear(): void;
/**
* 池中可用对象数量
*/
readonly available: number;
}
/**
* 实体查询条件类型
*/
export interface TypedQueryCondition<
All extends readonly ComponentConstructor[] = [],
Any extends readonly ComponentConstructor[] = [],
None extends readonly ComponentConstructor[] = []
> {
all: All;
any: Any;
none: None;
tag?: number;
name?: string;
}
/**
* 组件类型守卫
*/
export function isComponentType<T extends IComponent>(
value: any
): value is ComponentConstructor<T> {
return typeof value === 'function' && value.prototype instanceof Component;
}
/**
* 类型安全的组件数组守卫
*/
export function isComponentArray(
value: any[]
): value is ComponentConstructor[] {
return value.every(isComponentType);
}
/**
* 提取组件类型名称(编译时)
*/
export type ComponentTypeName<T extends ComponentConstructor> = T extends {
prototype: { constructor: { name: infer N } };
}
? N
: string;
/**
* 多组件类型名称联合
*/
export type ComponentTypeNames<T extends readonly ComponentConstructor[]> = {
[K in keyof T]: ComponentTypeName<T[K]>;
}[number];
/**
* 深度只读类型
*/
export type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
/**
* 深度可选类型
*/
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
/**
* 排除方法的类型
*/
export type DataOnly<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
/**
* 可序列化的组件数据
*/
export type SerializableComponent<T extends IComponent> = DeepPartial<DataOnly<T>>;

View File

@@ -2,6 +2,9 @@
* 框架核心类型定义
*/
// 导出TypeScript类型增强工具
export * from './TypeHelpers';
/**
* 组件接口
*

View File

@@ -9,7 +9,7 @@ export class WebSocketManager {
private reconnectInterval: number = 2000;
private url: string;
private autoReconnect: boolean;
private reconnectTimer?: NodeJS.Timeout;
private reconnectTimer?: ReturnType<typeof setTimeout>;
private onOpen?: (event: Event) => void;
private onClose?: (event: CloseEvent) => void;
private onError?: (error: Event | any) => void;

View File

@@ -27,6 +27,10 @@ export type { ILogger, LoggerConfig } from './Utils/Logger';
// ECS核心组件
export * from './ECS';
// TypeScript类型增强API
export * from './ECS/TypedEntity';
export * from './ECS/Core/Query/TypedQuery';
// 事件系统
export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './ECS/CoreEvents';
@@ -34,5 +38,8 @@ export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './
export * from './Utils';
export * from './Types';
// 显式导出ComponentPool类解决与Types中ComponentPool接口的命名冲突
export { ComponentPool, ComponentPoolManager } from './ECS/Core/Storage';
// 平台适配
export * from './Platform';

View File

@@ -1,10 +1,10 @@
import { Core } from '../src/Core';
import { Scene } from '../src/ECS/Scene';
import { SceneManager } from '../src/ECS/SceneManager';
import { Entity } from '../src/ECS/Entity';
import { Component } from '../src/ECS/Component';
import { GlobalManager } from '../src/Utils/GlobalManager';
import { ITimer } from '../src/Utils/Timers/ITimer';
import { WorldManager } from '../src/ECS/WorldManager';
// 测试组件
class TestComponent extends Component {
@@ -65,10 +65,9 @@ describe('Core - 核心管理系统测试', () => {
beforeEach(() => {
// 清除之前的实例
(Core as any)._instance = null;
// 重置WorldManager全局状态
WorldManager.reset();
// 注意:WorldManager不再是单例无需reset
// 模拟console.warn以避免测试输出
originalConsoleWarn = console.warn;
console.warn = jest.fn();
@@ -77,12 +76,11 @@ describe('Core - 核心管理系统测试', () => {
afterEach(() => {
// 恢复console.warn
console.warn = originalConsoleWarn;
// 清理Core实例
(Core as any)._instance = null;
// 重置WorldManager全局状态
WorldManager.reset();
if (Core.Instance) {
Core.destroy();
}
});
describe('实例创建和管理', () => {
@@ -128,96 +126,36 @@ describe('Core - 核心管理系统测试', () => {
});
});
describe('场景管理', () => {
// 注意场景管理功能已移至SceneManager
// 相关测试请查看 SceneManager.test.ts
describe('更新循环 - 全局服务', () => {
let core: Core;
let testScene: TestScene;
beforeEach(() => {
core = Core.create(true);
testScene = new TestScene();
});
test('应该能够设置场景', () => {
Core.setScene(testScene);
expect(Core.scene).toBe(testScene);
expect(testScene.beginCalled).toBe(true);
});
test('应该能够使用推荐的setScene方法设置场景', () => {
const scene = Core.setScene(testScene);
expect(Core.scene).toBe(testScene);
expect(testScene.beginCalled).toBe(true);
expect(scene).toBe(testScene); // 应该返回场景实例
});
test('设置新场景应该触发场景切换', () => {
const firstScene = new TestScene();
const secondScene = new TestScene();
// 设置第一个场景
Core.setScene(firstScene);
expect(firstScene.beginCalled).toBe(true);
// 设置第二个场景(应该在下一帧切换)
Core.setScene(secondScene);
// 模拟更新循环触发场景切换
Core.update(0.016);
expect(firstScene.endCalled).toBe(true);
expect(secondScene.beginCalled).toBe(true);
expect(Core.scene).toBe(secondScene);
});
test('获取场景在未设置时应该返回null', () => {
// 创建全新的Core实例确保没有场景设置
const core = Core.create(false);
expect(Core.scene).toBeNull();
});
});
describe('更新循环', () => {
let core: Core;
let testScene: TestScene;
let globalManager: TestGlobalManager;
beforeEach(() => {
core = Core.create(true);
testScene = new TestScene();
globalManager = new TestGlobalManager();
Core.registerGlobalManager(globalManager);
Core.setScene(testScene);
});
test('应该能够执行更新循环', () => {
const deltaTime = 0.016;
Core.update(deltaTime);
expect(testScene.updateCallCount).toBe(1);
test('应该能够更新全局管理器', () => {
Core.update(0.016);
expect(globalManager.updateCallCount).toBe(1);
});
test('暂停状态下不应该执行更新', () => {
Core.paused = true;
Core.update(0.016);
expect(testScene.updateCallCount).toBe(0);
expect(globalManager.updateCallCount).toBe(0);
// 恢复状态
Core.paused = false;
});
test('禁用的全局管理器不应该被更新', () => {
globalManager.enabled = false;
Core.update(0.016);
expect(globalManager.updateCallCount).toBe(0);
});
@@ -225,8 +163,6 @@ describe('Core - 核心管理系统测试', () => {
Core.update(0.016);
Core.update(0.016);
Core.update(0.016);
expect(testScene.updateCallCount).toBe(3);
expect(globalManager.updateCallCount).toBe(3);
});
});
@@ -410,41 +346,8 @@ describe('Core - 核心管理系统测试', () => {
});
});
describe('ECS API集成', () => {
let core: Core;
let testScene: TestScene;
beforeEach(() => {
core = Core.create(true);
testScene = new TestScene();
});
test('设置支持ECS的场景应该初始化ECS API', () => {
// 模拟带有querySystem和eventSystem的场景
const ecsScene = Object.assign(testScene, {
querySystem: { query: jest.fn() },
eventSystem: { emit: jest.fn() }
});
Core.setScene(ecsScene);
expect(Core.ecsAPI).toBeDefined();
});
test('设置普通场景不应该初始化ECS API', () => {
// 创建一个普通场景对象不继承Scene
const plainScene = {
initialize: () => {},
begin: () => {},
end: () => {},
update: () => {}
};
Core.setScene(plainScene as any);
expect(Core.ecsAPI).toBeNull();
});
});
// ECS API 现在由 SceneManager 管理
// 相关测试请查看 SceneManager.test.ts
describe('性能监控集成', () => {
let core: Core;
@@ -455,52 +358,21 @@ describe('Core - 核心管理系统测试', () => {
test('调试模式下应该启用性能监控', () => {
const performanceMonitor = (core as any)._performanceMonitor;
expect(performanceMonitor).toBeDefined();
// 性能监控器应该在调试模式下被启用
expect(performanceMonitor.isEnabled).toBe(true);
});
test('更新循环应该包含性能监控', () => {
const scene = new TestScene();
Core.setScene(scene);
const performanceMonitor = (core as any)._performanceMonitor;
const startMonitoringSpy = jest.spyOn(performanceMonitor, 'startMonitoring');
const endMonitoringSpy = jest.spyOn(performanceMonitor, 'endMonitoring');
Core.update(0.016);
expect(startMonitoringSpy).toHaveBeenCalled();
expect(endMonitoringSpy).toHaveBeenCalled();
});
});
describe('错误处理', () => {
test('设置null场景应该被忽略', () => {
const core = Core.create(false);
// Core的新架构中场景不能直接设置为null
// 默认情况下Core.scene应该为null没有设置场景时
expect(Core.scene).toBeNull();
});
test('应该处理场景更新中的异常', () => {
const core = Core.create(true);
const errorScene = new TestScene();
// 模拟场景更新抛出异常
errorScene.update = () => {
throw new Error('Test error');
};
Core.setScene(errorScene);
// 由于Core目前不捕获场景异常我们预期它会抛出异常
// 这是一个已知的行为,可以在未来版本中改进
expect(() => {
Core.update(0.016);
}).toThrow('Test error');
});
});
});

View File

@@ -138,8 +138,11 @@ describe('ComponentPool - 组件对象池测试', () => {
expect(pool.getAvailableCount()).toBe(0);
});
it('多次预热应该正确累加', () => {
it('多次预热应该填充到最大值', () => {
pool.prewarm(3);
expect(pool.getAvailableCount()).toBe(3);
pool.prewarm(5);
expect(pool.getAvailableCount()).toBe(5);
pool.prewarm(2);
expect(pool.getAvailableCount()).toBe(5);
});

View File

@@ -197,40 +197,6 @@ describe('ComponentRegistry Extended - 64+ 组件支持', () => {
expect(duration).toBeLessThan(100);
});
it('大量组件操作应该高效', () => {
const scene = new Scene();
const entity = scene.createEntity('TestEntity');
// 注册 100 个组件
const componentTypes: any[] = [];
for (let i = 0; i < 100; i++) {
const ComponentClass = createTestComponent(i);
ComponentRegistry.register(ComponentClass);
componentTypes.push(ComponentClass);
}
const startAdd = performance.now();
// 添加 100 个组件
for (let i = 0; i < 100; i++) {
entity.addComponent(new componentTypes[i]());
}
const endAdd = performance.now();
const startGet = performance.now();
// 获取 100 个组件
for (let i = 0; i < 100; i++) {
entity.getComponent(componentTypes[i]);
}
const endGet = performance.now();
const addDuration = endAdd - startAdd;
const getDuration = endGet - startGet;
// 添加应该在 50ms 内
expect(addDuration).toBeLessThan(50);
// 获取应该在 20ms 内
expect(getDuration).toBeLessThan(20);
});
});
describe('边界情况', () => {

View File

@@ -1,9 +1,4 @@
import {
ComponentRegistry,
ComponentStorage,
ComponentStorageManager,
ComponentType
} from '../../../src/ECS/Core/ComponentStorage';
import { ComponentRegistry, ComponentStorage, ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { Component } from '../../../src/ECS/Component';
import { BitMask64Utils } from '../../../src/ECS/Utils/BigIntCompatibility';
@@ -104,8 +99,8 @@ describe('ComponentRegistry - 组件注册表测试', () => {
const mask1 = ComponentRegistry.getBitMask(TestComponent);
const mask2 = ComponentRegistry.getBitMask(PositionComponent);
expect(mask1.lo).toBe(1); // 2^0
expect(mask2.lo).toBe(2); // 2^1
expect(BitMask64Utils.getBit(mask1,0)).toBe(true); // 2^0
expect(BitMask64Utils.getBit(mask2,1)).toBe(true); // 2^1
});
test('应该能够获取组件的位索引', () => {
@@ -471,7 +466,7 @@ describe('ComponentStorageManager - 组件存储管理器测试', () => {
const mask = manager.getComponentMask(1);
// 应该包含TestComponent(位0)和PositionComponent(位1)的掩码
expect(mask.lo).toBe(3); // 1 | 2 = 3
expect(BitMask64Utils.getBit(mask,0) && BitMask64Utils.getBit(mask,1)).toBe(true);
});
test('没有组件的实体应该有零掩码', () => {
@@ -485,15 +480,15 @@ describe('ComponentStorageManager - 组件存储管理器测试', () => {
manager.addComponent(1, new TestComponent(100));
let mask = manager.getComponentMask(1);
expect(mask.lo).toBe(1);
expect(BitMask64Utils.getBit(mask,0)).toBe(true);
manager.addComponent(1, new PositionComponent(10, 20));
mask = manager.getComponentMask(1);
expect(mask.lo).toBe(3); // 0b11
expect(BitMask64Utils.getBit(mask,1)).toBe(true); // 0b11
manager.removeComponent(1, TestComponent);
mask = manager.getComponentMask(1);
expect(mask.lo).toBe(2); // 0b10
expect(BitMask64Utils.getBit(mask,0)).toBe(false); // 0b10
});
});

View File

@@ -1,253 +0,0 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { EnableSoA, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/SoAStorage';
// 测试组件:使用集合类型装饰器
@EnableSoA
class CollectionsComponent extends Component {
// 序列化Map存储
@SerializeMap
public playerStats: Map<string, number> = new Map();
// 序列化Set存储
@SerializeSet
public achievements: Set<string> = new Set();
// 序列化Array存储
@SerializeArray
public inventory: string[] = [];
// 深拷贝对象存储
@DeepCopy
public config: { settings: { volume: number } } = { settings: { volume: 0.5 } };
// 普通对象(引用存储)
public metadata: any = null;
constructor() {
super();
}
}
describe('SoA集合类型装饰器测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('验证Map序列化存储', () => {
console.log('\\n=== 测试Map序列化存储 ===');
const component = new CollectionsComponent();
// 设置Map数据
component.playerStats.set('health', 100);
component.playerStats.set('mana', 50);
component.playerStats.set('experience', 1250);
console.log('原始Map数据:', {
size: component.playerStats.size,
entries: Array.from(component.playerStats.entries())
});
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
console.log('取回Map数据:', {
size: retrieved?.playerStats.size,
entries: Array.from(retrieved?.playerStats.entries() || [])
});
// 验证Map数据完整性
expect(retrieved?.playerStats).toBeInstanceOf(Map);
expect(retrieved?.playerStats.size).toBe(3);
expect(retrieved?.playerStats.get('health')).toBe(100);
expect(retrieved?.playerStats.get('mana')).toBe(50);
expect(retrieved?.playerStats.get('experience')).toBe(1250);
console.log('✅ Map序列化存储验证通过');
});
test('验证Set序列化存储', () => {
console.log('\\n=== 测试Set序列化存储 ===');
const component = new CollectionsComponent();
// 设置Set数据
component.achievements.add('first_kill');
component.achievements.add('level_10');
component.achievements.add('boss_defeated');
console.log('原始Set数据:', {
size: component.achievements.size,
values: Array.from(component.achievements)
});
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
console.log('取回Set数据:', {
size: retrieved?.achievements.size,
values: Array.from(retrieved?.achievements || [])
});
// 验证Set数据完整性
expect(retrieved?.achievements).toBeInstanceOf(Set);
expect(retrieved?.achievements.size).toBe(3);
expect(retrieved?.achievements.has('first_kill')).toBe(true);
expect(retrieved?.achievements.has('level_10')).toBe(true);
expect(retrieved?.achievements.has('boss_defeated')).toBe(true);
console.log('✅ Set序列化存储验证通过');
});
test('验证Array序列化存储', () => {
console.log('\\n=== 测试Array序列化存储 ===');
const component = new CollectionsComponent();
// 设置Array数据
component.inventory.push('sword', 'shield', 'potion');
console.log('原始Array数据:', component.inventory);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
console.log('取回Array数据:', retrieved?.inventory);
// 验证Array数据完整性
expect(Array.isArray(retrieved?.inventory)).toBe(true);
expect(retrieved?.inventory.length).toBe(3);
expect(retrieved?.inventory).toEqual(['sword', 'shield', 'potion']);
console.log('✅ Array序列化存储验证通过');
});
test('验证深拷贝对象存储', () => {
console.log('\\n=== 测试深拷贝对象存储 ===');
const component = new CollectionsComponent();
const originalConfig = component.config;
// 修改配置
component.config.settings.volume = 0.8;
console.log('原始配置:', component.config);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
console.log('取回配置:', retrieved?.config);
// 验证深拷贝
expect(retrieved?.config).toEqual(component.config);
expect(retrieved?.config).not.toBe(originalConfig); // 不是同一个引用
expect(retrieved?.config.settings.volume).toBe(0.8);
// 修改原始对象不应该影响取回的对象
component.config.settings.volume = 0.3;
expect(retrieved?.config.settings.volume).toBe(0.8); // 保持不变
console.log('✅ 深拷贝对象存储验证通过');
});
test('对比普通对象存储(引用存储)', () => {
console.log('\\n=== 测试普通对象存储(引用存储)===');
const component = new CollectionsComponent();
const sharedObject = { data: 'shared' };
component.metadata = sharedObject;
console.log('原始metadata:', component.metadata);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
console.log('取回metadata:', retrieved?.metadata);
// 验证引用存储
expect(retrieved?.metadata).toBe(sharedObject); // 是同一个引用
expect(retrieved?.metadata.data).toBe('shared');
console.log('✅ 普通对象存储验证通过');
});
test('复杂场景:多种类型混合使用', () => {
console.log('\\n=== 测试复杂场景 ===');
const component = new CollectionsComponent();
// 设置复杂数据
component.playerStats.set('level', 25);
component.playerStats.set('gold', 5000);
component.achievements.add('explorer');
component.achievements.add('warrior');
component.inventory.push('legendary_sword', 'magic_potion');
component.config = {
settings: {
volume: 0.75
}
};
component.metadata = { timestamp: Date.now() };
console.log('复杂数据设置完成');
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionsComponent);
// 全面验证
expect(retrieved?.playerStats.get('level')).toBe(25);
expect(retrieved?.achievements.has('explorer')).toBe(true);
expect(retrieved?.inventory).toContain('legendary_sword');
expect(retrieved?.config.settings.volume).toBe(0.75);
expect(retrieved?.metadata).toBeDefined();
console.log('✅ 复杂场景验证通过');
});
test('性能测试:序列化 vs 深拷贝', () => {
console.log('\\n=== 性能对比测试 ===');
const entityCount = 100;
// 准备测试数据
const startTime = performance.now();
for (let i = 0; i < entityCount; i++) {
const component = new CollectionsComponent();
// 设置数据
component.playerStats.set('id', i);
component.playerStats.set('score', i * 100);
component.achievements.add(`achievement_${i}`);
component.inventory.push(`item_${i}`);
component.config = { settings: { volume: i / entityCount } };
manager.addComponent(i, component);
}
const createTime = performance.now() - startTime;
// 读取测试
const readStartTime = performance.now();
for (let i = 0; i < entityCount; i++) {
const component = manager.getComponent(i, CollectionsComponent);
expect(component?.playerStats.get('id')).toBe(i);
}
const readTime = performance.now() - readStartTime;
console.log(`创建${entityCount}个复杂组件: ${createTime.toFixed(2)}ms`);
console.log(`读取${entityCount}个复杂组件: ${readTime.toFixed(2)}ms`);
console.log(`平均每个组件: ${((createTime + readTime) / entityCount).toFixed(4)}ms`);
console.log('✅ 性能测试完成');
});
});

View File

@@ -0,0 +1,631 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import {
EnableSoA,
HighPrecision,
Float64,
Int32,
SerializeMap,
SerializeSet,
SerializeArray,
DeepCopy,
SoAStorage
} from '../../../src/ECS/Core/SoAStorage';
/**
* SoA存储完整测试套件
*/
// 测试组件定义
@EnableSoA
class BasicTypesComponent extends Component {
public intNumber: number;
public floatNumber: number;
public boolValue: boolean;
public stringValue: string;
public nullValue: null;
public undefinedValue: undefined;
constructor(...args: unknown[]) {
super();
const [
intNumber = 42,
floatNumber = 3.14,
boolValue = true,
stringValue = 'test',
nullValue = null,
undefinedValue = undefined
] = args as [number?, number?, boolean?, string?, null?, undefined?];
this.intNumber = intNumber;
this.floatNumber = floatNumber;
this.boolValue = boolValue;
this.stringValue = stringValue;
this.nullValue = nullValue;
this.undefinedValue = undefinedValue;
}
}
@EnableSoA
class DecoratedNumberComponent extends Component {
public normalFloat: number;
@HighPrecision
public highPrecisionNumber: number;
@Float64
public preciseFloat: number;
@Int32
public integerValue: number;
constructor(...args: unknown[]) {
super();
const [
normalFloat = 3.14,
highPrecisionNumber = Number.MAX_SAFE_INTEGER,
preciseFloat = Math.PI,
integerValue = 42
] = args as [number?, number?, number?, number?];
this.normalFloat = normalFloat;
this.highPrecisionNumber = highPrecisionNumber;
this.preciseFloat = preciseFloat;
this.integerValue = integerValue;
}
}
@EnableSoA
class CollectionComponent extends Component {
@SerializeMap
public mapData: Map<string, any>;
@SerializeSet
public setData: Set<any>;
@SerializeArray
public arrayData: any[];
@DeepCopy
public deepCopyData: any;
constructor(...args: unknown[]) {
super();
const [
mapData = new Map(),
setData = new Set(),
arrayData = [],
deepCopyData = null
] = args as [Map<string, any>?, Set<any>?, any[]?, any?];
this.mapData = mapData;
this.setData = setData;
this.arrayData = arrayData;
this.deepCopyData = deepCopyData;
}
}
class MockNode {
public name: string;
public active: boolean;
constructor(name: string) {
this.name = name;
this.active = true;
}
}
@EnableSoA
class ComplexObjectComponent extends Component {
public x: number;
public y: number;
public node: MockNode | null;
public callback: Function | null;
public data: any;
constructor(...args: unknown[]) {
super();
const [
x = 0,
y = 0,
node = null as MockNode | null,
callback = null as Function | null,
data = null as any
] = args as [number?, number?, (MockNode | null)?, (Function | null)?, any?];
this.x = x;
this.y = y;
this.node = node;
this.callback = callback;
this.data = data;
}
}
@EnableSoA
class MixedComponent extends Component {
@HighPrecision
public bigIntId: number;
@Float64
public preciseValue: number;
@Int32
public intValue: number;
@SerializeMap
public gameMap: Map<string, any>;
@SerializeSet
public flags: Set<number>;
@SerializeArray
public items: any[];
@DeepCopy
public config: any;
public normalFloat: number;
public boolFlag: boolean;
public text: string;
constructor(...args: unknown[]) {
super();
const [
bigIntId = 0,
preciseValue = 0,
intValue = 0,
normalFloat = 0,
boolFlag = false,
text = ''
] = args as [number?, number?, number?, number?, boolean?, string?];
this.bigIntId = bigIntId;
this.preciseValue = preciseValue;
this.intValue = intValue;
this.gameMap = new Map();
this.flags = new Set();
this.items = [];
this.config = null;
this.normalFloat = normalFloat;
this.boolFlag = boolFlag;
this.text = text;
}
}
describe('SoAStorage - SoA存储测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
describe('基础数据类型', () => {
test('应该正确存储和检索number类型', () => {
const component = new BasicTypesComponent(999, 2.718);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.intNumber).toBe(999);
expect(retrieved?.floatNumber).toBeCloseTo(2.718);
});
test('应该正确存储和检索boolean类型', () => {
const component = new BasicTypesComponent(0, 0, false);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.boolValue).toBe(false);
});
test('应该正确存储和检索string类型', () => {
const testString = '测试中文字符串 with emoji 🎉';
const component = new BasicTypesComponent(0, 0, true, testString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(testString);
});
test('应该正确处理null和undefined', () => {
const component = new BasicTypesComponent();
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.nullValue).toBe(null);
// undefined在SoA存储中保持为undefined不会序列化
expect(retrieved?.undefinedValue).toBeUndefined();
});
test('应该正确处理空字符串', () => {
const component = new BasicTypesComponent(0, 0, true, '');
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe('');
});
test('应该正确处理数值边界值', () => {
// Float32可精确表示的最大整数约为2^24 (16777216)
// Float32最小正值约为1.4e-45Number.MIN_VALUE (5e-324)会被截断为0
const maxFloat32Int = 16777216;
const minFloat32 = 1.401298464324817e-45;
const component = new BasicTypesComponent(
maxFloat32Int,
minFloat32
);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.intNumber).toBe(maxFloat32Int);
expect(retrieved?.floatNumber).toBeCloseTo(minFloat32, 45);
});
test('应该正确处理特殊字符串', () => {
const specialString = '\n\t\r"\'\\\\';
const component = new BasicTypesComponent(0, 0, true, specialString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(specialString);
});
test('应该正确处理长字符串', () => {
const longString = 'a'.repeat(1000);
const component = new BasicTypesComponent(0, 0, true, longString);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BasicTypesComponent);
expect(retrieved?.stringValue).toBe(longString);
});
});
describe('数值类型装饰器', () => {
test('@HighPrecision应该保持高精度数值', () => {
const component = new DecoratedNumberComponent(
0,
Number.MAX_SAFE_INTEGER
);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
});
test('@Float64应该使用双精度浮点存储', () => {
const component = new DecoratedNumberComponent(0, 0, Math.PI);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.preciseFloat).toBeCloseTo(Math.PI, 15);
});
test('@Int32应该使用32位整数存储', () => {
const component = new DecoratedNumberComponent(0, 0, 0, -2147483648);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.integerValue).toBe(-2147483648);
});
test('默认应该使用Float32存储', () => {
const component = new DecoratedNumberComponent(3.14159);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedNumberComponent);
expect(retrieved?.normalFloat).toBeCloseTo(3.14159, 5);
});
test('应该使用正确的TypedArray类型', () => {
const component = new DecoratedNumberComponent();
manager.addComponent(1, component);
const storage = manager.getStorage(DecoratedNumberComponent) as SoAStorage<DecoratedNumberComponent>;
expect(storage.getFieldArray('normalFloat')).toBeInstanceOf(Float32Array);
expect(storage.getFieldArray('preciseFloat')).toBeInstanceOf(Float64Array);
expect(storage.getFieldArray('integerValue')).toBeInstanceOf(Int32Array);
expect(storage.getFieldArray('highPrecisionNumber')).toBeNull();
});
});
describe('集合类型序列化', () => {
test('@SerializeMap应该正确序列化Map', () => {
const component = new CollectionComponent();
component.mapData.set('key1', 'value1');
component.mapData.set('key2', 123);
component.mapData.set('key3', { nested: 'object' });
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.mapData).toBeInstanceOf(Map);
expect(retrieved?.mapData.size).toBe(3);
expect(retrieved?.mapData.get('key1')).toBe('value1');
expect(retrieved?.mapData.get('key2')).toBe(123);
expect(retrieved?.mapData.get('key3')).toEqual({ nested: 'object' });
});
test('@SerializeSet应该正确序列化Set', () => {
const component = new CollectionComponent();
component.setData.add('item1');
component.setData.add('item2');
component.setData.add(123);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.setData).toBeInstanceOf(Set);
expect(retrieved?.setData.size).toBe(3);
expect(retrieved?.setData.has('item1')).toBe(true);
expect(retrieved?.setData.has('item2')).toBe(true);
expect(retrieved?.setData.has(123)).toBe(true);
});
test('@SerializeArray应该正确序列化Array', () => {
const component = new CollectionComponent();
component.arrayData = ['item1', 'item2', 123, { nested: 'object' }];
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(Array.isArray(retrieved?.arrayData)).toBe(true);
expect(retrieved?.arrayData).toEqual(['item1', 'item2', 123, { nested: 'object' }]);
});
test('@DeepCopy应该创建深拷贝', () => {
const component = new CollectionComponent();
component.deepCopyData = { level1: { level2: { value: 42 } } };
const originalRef = component.deepCopyData;
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.deepCopyData).toEqual(component.deepCopyData);
expect(retrieved?.deepCopyData).not.toBe(originalRef);
component.deepCopyData.level1.level2.value = 100;
expect(retrieved?.deepCopyData.level1.level2.value).toBe(42);
});
test('Map应该正确处理边界值', () => {
const component = new CollectionComponent();
component.mapData.set('null', null);
component.mapData.set('undefined', undefined);
component.mapData.set('empty', '');
component.mapData.set('zero', 0);
component.mapData.set('false', false);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.mapData.get('null')).toBe(null);
expect(retrieved?.mapData.get('undefined')).toBe(null);
expect(retrieved?.mapData.get('empty')).toBe('');
expect(retrieved?.mapData.get('zero')).toBe(0);
expect(retrieved?.mapData.get('false')).toBe(false);
});
test('Set应该支持数值0', () => {
const component = new CollectionComponent();
component.setData.add(0);
component.setData.add(1);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.setData.has(0)).toBe(true);
expect(retrieved?.setData.has(1)).toBe(true);
});
test('Array应该正确处理null和undefined', () => {
const component = new CollectionComponent();
component.arrayData = [null, undefined, '', 0, false];
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, CollectionComponent);
expect(retrieved?.arrayData).toEqual([null, null, '', 0, false]);
});
});
describe('复杂对象处理', () => {
test('应该正确保存复杂对象引用', () => {
const node = new MockNode('testNode');
const callback = () => console.log('test');
const data = { complex: 'object' };
const component = new ComplexObjectComponent(100, 200, node, callback, data);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComplexObjectComponent);
expect(retrieved?.x).toBe(100);
expect(retrieved?.y).toBe(200);
expect(retrieved?.node?.name).toBe('testNode');
expect(retrieved?.node?.active).toBe(true);
expect(retrieved?.callback).toBe(callback);
expect(retrieved?.data).toEqual(data);
});
test('应该正确处理null对象', () => {
const component = new ComplexObjectComponent(0, 0, null, null, null);
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComplexObjectComponent);
expect(retrieved?.node).toBe(null);
expect(retrieved?.callback).toBe(null);
expect(retrieved?.data).toBe(null);
});
});
describe('混合装饰器使用', () => {
test('应该支持多种装饰器混合使用', () => {
const component = new MixedComponent(
Number.MAX_SAFE_INTEGER,
Math.PI,
-2147483648,
1.23,
true,
'test'
);
component.gameMap.set('player1', { level: 10 });
component.flags.add(1);
component.flags.add(2);
component.items.push('item1');
component.config = { settings: { volume: 0.8 } };
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, MixedComponent);
expect(retrieved?.bigIntId).toBe(Number.MAX_SAFE_INTEGER);
expect(retrieved?.preciseValue).toBeCloseTo(Math.PI, 15);
expect(retrieved?.intValue).toBe(-2147483648);
expect(retrieved?.normalFloat).toBeCloseTo(1.23, 5);
expect(retrieved?.boolFlag).toBe(true);
expect(retrieved?.text).toBe('test');
expect(retrieved?.gameMap.get('player1')).toEqual({ level: 10 });
expect(retrieved?.flags.has(1)).toBe(true);
expect(retrieved?.flags.has(2)).toBe(true);
expect(retrieved?.items).toContain('item1');
expect(retrieved?.config.settings.volume).toBe(0.8);
});
});
describe('存储管理', () => {
test('应该正确统计存储信息', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 5; i++) {
const component = new MixedComponent(i, i * Math.PI, i * 10);
manager.addComponent(i, component);
}
const stats = storage.getStats();
expect(stats.size).toBe(5);
expect(stats.capacity).toBeGreaterThanOrEqual(5);
expect(stats.memoryUsage).toBeGreaterThan(0);
});
test('应该支持压缩操作', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 5; i++) {
const component = new MixedComponent();
manager.addComponent(i, component);
}
storage.removeComponent(2);
storage.removeComponent(4);
const statsBefore = storage.getStats();
storage.compact();
const statsAfter = storage.getStats();
expect(statsAfter.size).toBe(3);
expect(statsAfter.size).toBeLessThan(statsBefore.capacity);
});
test('应该正确处理循环引用', () => {
const component = new MixedComponent();
const cyclicObject: any = { name: 'test' };
cyclicObject.self = cyclicObject;
component.items.push(cyclicObject);
expect(() => {
manager.addComponent(1, component);
}).not.toThrow();
const retrieved = manager.getComponent(1, MixedComponent);
expect(retrieved).toBeDefined();
});
});
describe('性能测试', () => {
test('大容量创建性能应该可接受', () => {
const entityCount = 2000;
const startTime = performance.now();
for (let i = 1; i <= entityCount; i++) {
const component = new MixedComponent(i, i * 0.1, i * 10);
component.gameMap.set(`key${i}`, i);
manager.addComponent(i, component);
}
const createTime = performance.now() - startTime;
expect(createTime).toBeLessThan(1000);
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
const stats = storage.getStats();
expect(stats.size).toBe(entityCount);
});
test('随机访问性能应该可接受', () => {
const entityCount = 2000;
for (let i = 1; i <= entityCount; i++) {
const component = new MixedComponent(i);
manager.addComponent(i, component);
}
const startTime = performance.now();
for (let i = 0; i < 100; i++) {
const randomId = Math.floor(Math.random() * entityCount) + 1;
const component = manager.getComponent(randomId, MixedComponent);
expect(component?.bigIntId).toBe(randomId);
}
const readTime = performance.now() - startTime;
expect(readTime).toBeLessThan(100);
});
test('向量化批量操作应该正确执行', () => {
const storage = manager.getStorage(MixedComponent) as SoAStorage<MixedComponent>;
for (let i = 1; i <= 10; i++) {
const component = new MixedComponent(0, 0, i * 10, i);
manager.addComponent(i, component);
}
let operationExecuted = false;
storage.performVectorizedOperation((fieldArrays, activeIndices) => {
operationExecuted = true;
const normalFloatArray = fieldArrays.get('normalFloat') as Float32Array;
const intArray = fieldArrays.get('intValue') as Int32Array;
expect(normalFloatArray).toBeInstanceOf(Float32Array);
expect(intArray).toBeInstanceOf(Int32Array);
expect(activeIndices.length).toBe(10);
for (let i = 0; i < activeIndices.length; i++) {
const idx = activeIndices[i];
normalFloatArray[idx] *= 2;
intArray[idx] += 5;
}
});
expect(operationExecuted).toBe(true);
const component = manager.getComponent(5, MixedComponent);
expect(component?.normalFloat).toBe(10);
expect(component?.intValue).toBe(55);
});
});
});

View File

@@ -1,308 +0,0 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { EnableSoA, HighPrecision, Float64, Int32, SerializeMap, SerializeSet, SerializeArray, DeepCopy } from '../../../src/ECS/Core/SoAStorage';
import { SoAStorage } from '../../../src/ECS/Core/SoAStorage';
// 综合测试组件,覆盖所有装饰器
@EnableSoA
class ComprehensiveComponent extends Component {
@HighPrecision
public bigIntId: number = BigInt(Number.MAX_SAFE_INTEGER + 1) as any;
@Float64
public preciseValue: number = Math.PI;
@Int32
public intValue: number = -2147483648;
@SerializeMap
public gameMap: Map<string, any> = new Map();
@SerializeSet
public flags: Set<number> = new Set();
@SerializeArray
public items: any[] = [];
@DeepCopy
public nestedConfig: any = { deep: { nested: { value: 42 } } };
// 未装饰的字段
public normalFloat: number = 1.23;
public flag: boolean = true;
public text: string = 'default';
public complexObject: any = null;
constructor() {
super();
}
}
describe('SoA存储综合测试覆盖', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('验证所有装饰器类型的存储和检索', () => {
console.log('\\n=== 综合装饰器测试 ===');
const component = new ComprehensiveComponent();
// 设置复杂数据
component.gameMap.set('player1', { level: 10, gold: 500 });
component.gameMap.set('player2', { level: 15, gold: 1200 });
component.flags.add(1);
component.flags.add(2);
component.flags.add(4);
component.items.push({ type: 'weapon', name: 'sword' });
component.items.push({ type: 'armor', name: 'shield' });
component.nestedConfig.deep.nested.value = 999;
component.complexObject = { reference: 'shared' };
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComprehensiveComponent);
// 验证所有类型
expect(retrieved?.bigIntId).toBe(component.bigIntId);
expect(retrieved?.preciseValue).toBeCloseTo(Math.PI, 15);
expect(retrieved?.intValue).toBe(-2147483648);
expect(retrieved?.gameMap).toBeInstanceOf(Map);
expect(retrieved?.gameMap.get('player1')).toEqual({ level: 10, gold: 500 });
expect(retrieved?.flags).toBeInstanceOf(Set);
expect(retrieved?.flags.has(2)).toBe(true);
expect(retrieved?.items).toEqual(component.items);
expect(retrieved?.nestedConfig.deep.nested.value).toBe(999);
// 深拷贝验证
expect(retrieved?.nestedConfig).not.toBe(component.nestedConfig);
console.log('✅ 综合装饰器测试通过');
});
test('测试存储器内存统计和容量管理', () => {
console.log('\\n=== 存储器管理测试 ===');
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
// 添加多个组件
for (let i = 1; i <= 5; i++) {
const component = new ComprehensiveComponent();
component.intValue = i * 100;
component.preciseValue = i * Math.PI;
manager.addComponent(i, component);
}
// 检查统计信息
const stats = storage.getStats();
console.log('存储统计:', {
size: stats.size,
capacity: stats.capacity,
memoryUsage: stats.memoryUsage,
fieldCount: stats.fieldStats.size
});
expect(stats.size).toBe(5);
expect(stats.capacity).toBeGreaterThanOrEqual(5);
expect(stats.memoryUsage).toBeGreaterThan(0);
// 测试压缩
storage.removeComponent(2);
storage.removeComponent(4);
const statsBeforeCompact = storage.getStats();
storage.compact();
const statsAfterCompact = storage.getStats();
expect(statsAfterCompact.size).toBe(3);
console.log('压缩前后对比:', {
before: statsBeforeCompact.size,
after: statsAfterCompact.size
});
console.log('✅ 存储器管理测试通过');
});
test('测试序列化错误处理', () => {
console.log('\\n=== 序列化错误处理测试 ===');
// 创建包含循环引用的对象
const component = new ComprehensiveComponent();
const cyclicObject: any = { name: 'test' };
cyclicObject.self = cyclicObject; // 循环引用
// 这应该不会崩溃,而是优雅处理
component.items.push(cyclicObject);
expect(() => {
manager.addComponent(1, component);
}).not.toThrow();
const retrieved = manager.getComponent(1, ComprehensiveComponent);
expect(retrieved).toBeDefined();
console.log('✅ 序列化错误处理测试通过');
});
test('测试大容量扩展和性能', () => {
console.log('\\n=== 大容量性能测试 ===');
const startTime = performance.now();
const entityCount = 2000;
// 创建大量实体
for (let i = 1; i <= entityCount; i++) {
const component = new ComprehensiveComponent();
component.intValue = i;
component.preciseValue = i * 0.1;
component.gameMap.set(`key${i}`, i);
component.flags.add(i % 10);
component.items.push(`item${i}`);
manager.addComponent(i, component);
}
const createTime = performance.now() - startTime;
// 随机访问测试
const readStartTime = performance.now();
for (let i = 0; i < 100; i++) {
const randomId = Math.floor(Math.random() * entityCount) + 1;
const component = manager.getComponent(randomId, ComprehensiveComponent);
expect(component?.intValue).toBe(randomId);
}
const readTime = performance.now() - readStartTime;
console.log(`创建${entityCount}个组件: ${createTime.toFixed(2)}ms`);
console.log(`随机读取100次: ${readTime.toFixed(2)}ms`);
console.log(`平均创建时间: ${(createTime / entityCount).toFixed(4)}ms/组件`);
// 验证存储统计
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
const stats = storage.getStats();
expect(stats.size).toBe(entityCount);
expect(stats.capacity).toBeGreaterThanOrEqual(entityCount);
console.log('✅ 大容量性能测试通过');
});
test('测试空值和边界处理', () => {
console.log('\\n=== 空值边界测试 ===');
const component = new ComprehensiveComponent();
// 设置各种边界值
component.gameMap.set('null', null);
component.gameMap.set('undefined', undefined);
component.gameMap.set('empty', '');
component.gameMap.set('zero', 0);
component.gameMap.set('false', false);
component.flags.add(0);
component.items.push(null, undefined, '', 0, false);
component.nestedConfig = null;
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, ComprehensiveComponent);
// 验证边界值处理
expect(retrieved?.gameMap.get('null')).toBe(null);
expect(retrieved?.gameMap.get('undefined')).toBe(null); // JSON序列化会将undefined转为null
expect(retrieved?.gameMap.get('empty')).toBe('');
expect(retrieved?.gameMap.get('zero')).toBe(0);
expect(retrieved?.gameMap.get('false')).toBe(false);
expect(retrieved?.flags.has(0)).toBe(true);
expect(retrieved?.items).toEqual([null, null, '', 0, false]); // undefined序列化为null
expect(retrieved?.nestedConfig).toBe(null);
console.log('✅ 空值边界测试通过');
});
test('测试不同TypedArray类型的字段访问', () => {
console.log('\\n=== TypedArray字段测试 ===');
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
// 添加测试数据
const component = new ComprehensiveComponent();
manager.addComponent(1, component);
// 检查不同类型的TypedArray
const preciseArray = storage.getFieldArray('preciseValue');
const intArray = storage.getFieldArray('intValue');
const normalArray = storage.getFieldArray('normalFloat');
const flagArray = storage.getFieldArray('flag');
expect(preciseArray).toBeInstanceOf(Float64Array);
expect(intArray).toBeInstanceOf(Int32Array);
expect(normalArray).toBeInstanceOf(Float32Array);
expect(flagArray).toBeInstanceOf(Uint8Array);
// 高精度字段不应该在TypedArray中
const bigIntArray = storage.getFieldArray('bigIntId');
expect(bigIntArray).toBeNull();
console.log('TypedArray类型验证:', {
preciseValue: preciseArray?.constructor.name,
intValue: intArray?.constructor.name,
normalFloat: normalArray?.constructor.name,
flag: flagArray?.constructor.name,
bigIntId: bigIntArray ? 'Found' : 'null (正确)'
});
console.log('✅ TypedArray字段测试通过');
});
test('测试向量化批量操作', () => {
console.log('\\n=== 向量化操作测试 ===');
const storage = manager.getStorage(ComprehensiveComponent) as SoAStorage<ComprehensiveComponent>;
// 添加测试数据
for (let i = 1; i <= 10; i++) {
const component = new ComprehensiveComponent();
component.normalFloat = i;
component.intValue = i * 10;
manager.addComponent(i, component);
}
// 执行向量化操作
let operationExecuted = false;
storage.performVectorizedOperation((fieldArrays, activeIndices) => {
operationExecuted = true;
const normalFloatArray = fieldArrays.get('normalFloat') as Float32Array;
const intArray = fieldArrays.get('intValue') as Int32Array;
expect(normalFloatArray).toBeInstanceOf(Float32Array);
expect(intArray).toBeInstanceOf(Int32Array);
expect(activeIndices.length).toBe(10);
// 批量修改数据
for (let i = 0; i < activeIndices.length; i++) {
const idx = activeIndices[i];
normalFloatArray[idx] *= 2; // 乘以2
intArray[idx] += 5; // 加5
}
});
expect(operationExecuted).toBe(true);
// 验证批量操作结果
const component = manager.getComponent(5, ComprehensiveComponent);
expect(component?.normalFloat).toBe(10); // 5 * 2
expect(component?.intValue).toBe(55); // 50 + 5
console.log('✅ 向量化操作测试通过');
});
});

View File

@@ -1,171 +0,0 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { SoAStorage, EnableSoA, HighPrecision, Float64, Int32 } from '../../../src/ECS/Core/SoAStorage';
// 测试组件:使用不同的数值类型装饰器
@EnableSoA
class DecoratedComponent extends Component {
// 默认Float32Array存储
public normalFloat: number = 3.14;
// 高精度存储(作为复杂对象)
@HighPrecision
public highPrecisionNumber: number = Number.MAX_SAFE_INTEGER;
// Float64Array存储
@Float64
public preciseFloat: number = Math.PI;
// Int32Array存储
@Int32
public integerValue: number = 42;
// 布尔值默认Float32Array
public flag: boolean = true;
// 字符串(专门数组)
public text: string = 'hello';
constructor() {
super();
}
}
describe('SoA数值类型装饰器测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('验证不同装饰器的存储类型', () => {
console.log('\\n=== 测试装饰器存储类型 ===');
const component = new DecoratedComponent();
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
component.preciseFloat = Math.PI;
component.integerValue = 999999;
component.normalFloat = 2.718;
console.log('原始数据:', {
normalFloat: component.normalFloat,
highPrecisionNumber: component.highPrecisionNumber,
preciseFloat: component.preciseFloat,
integerValue: component.integerValue,
flag: component.flag,
text: component.text
});
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedComponent);
console.log('\\n取回数据:', {
normalFloat: retrieved?.normalFloat,
highPrecisionNumber: retrieved?.highPrecisionNumber,
preciseFloat: retrieved?.preciseFloat,
integerValue: retrieved?.integerValue,
flag: retrieved?.flag,
text: retrieved?.text
});
// 验证精度保持
expect(retrieved?.normalFloat).toBeCloseTo(2.718, 5); // Float32精度
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER); // 高精度保持
expect(retrieved?.preciseFloat).toBeCloseTo(Math.PI, 15); // Float64精度
expect(retrieved?.integerValue).toBe(999999); // 整数保持
expect(retrieved?.flag).toBe(true);
expect(retrieved?.text).toBe('hello');
console.log('✅ 所有装饰器类型验证通过');
});
test('验证存储器内部结构', () => {
console.log('\\n=== 测试存储器内部结构 ===');
const component = new DecoratedComponent();
manager.addComponent(1, component);
const storage = manager.getStorage(DecoratedComponent) as SoAStorage<DecoratedComponent>;
// 检查TypedArray字段
const normalFloatArray = storage.getFieldArray('normalFloat');
const preciseFloatArray = storage.getFieldArray('preciseFloat');
const integerArray = storage.getFieldArray('integerValue');
const flagArray = storage.getFieldArray('flag');
console.log('存储类型:', {
normalFloat: normalFloatArray?.constructor.name,
preciseFloat: preciseFloatArray?.constructor.name,
integerValue: integerArray?.constructor.name,
flag: flagArray?.constructor.name
});
// 验证存储类型
expect(normalFloatArray).toBeInstanceOf(Float32Array);
expect(preciseFloatArray).toBeInstanceOf(Float64Array);
expect(integerArray).toBeInstanceOf(Int32Array);
expect(flagArray).toBeInstanceOf(Uint8Array);
// 高精度字段不应该在TypedArray中
const highPrecisionArray = storage.getFieldArray('highPrecisionNumber');
expect(highPrecisionArray).toBeNull();
console.log('✅ 存储器内部结构验证通过');
});
test('测试边界值精度', () => {
console.log('\\n=== 测试边界值精度 ===');
const component = new DecoratedComponent();
// 测试极限值
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
component.preciseFloat = Number.MIN_VALUE;
component.normalFloat = 16777217; // 超出Float32精度
component.integerValue = -2147483648; // Int32最小值
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, DecoratedComponent);
console.log('边界值测试结果:', {
highPrecision: retrieved?.highPrecisionNumber === Number.MAX_SAFE_INTEGER,
preciseFloat: retrieved?.preciseFloat === Number.MIN_VALUE,
normalFloat: retrieved?.normalFloat, // 可能有精度损失
integerValue: retrieved?.integerValue === -2147483648
});
// 验证高精度保持
expect(retrieved?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
expect(retrieved?.preciseFloat).toBe(Number.MIN_VALUE);
expect(retrieved?.integerValue).toBe(-2147483648);
console.log('✅ 边界值精度测试通过');
});
test('性能对比:装饰器 vs 自动检测', () => {
console.log('\\n=== 性能对比测试 ===');
const entityCount = 1000;
// 使用装饰器的组件
const startTime = performance.now();
for (let i = 0; i < entityCount; i++) {
const component = new DecoratedComponent();
component.highPrecisionNumber = Number.MAX_SAFE_INTEGER;
component.preciseFloat = Math.PI * i;
component.integerValue = i;
manager.addComponent(i, component);
}
const decoratorTime = performance.now() - startTime;
console.log(`装饰器方式: ${decoratorTime.toFixed(2)}ms`);
console.log(`平均每个组件: ${(decoratorTime / entityCount).toFixed(4)}ms`);
// 验证数据完整性
const sample = manager.getComponent(500, DecoratedComponent);
expect(sample?.highPrecisionNumber).toBe(Number.MAX_SAFE_INTEGER);
expect(sample?.integerValue).toBe(500);
console.log('✅ 性能测试完成,数据完整性验证通过');
});
});

View File

@@ -1,129 +0,0 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { EnableSoA } from '../../../src/ECS/Core/SoAStorage';
// 模拟复杂对象如cocos的node节点
class MockNode {
public name: string;
public active: boolean;
constructor(name: string) {
this.name = name;
this.active = true;
}
public toString() {
return `Node(${this.name})`;
}
}
// 包含复杂属性的组件
@EnableSoA
class ProblematicComponent extends Component {
public x: number = 0;
public y: number = 0;
public node: MockNode | null = null;
public callback: Function | null = null;
public data: any = null;
constructor() {
super();
this.node = new MockNode('test');
this.callback = () => console.log('test');
this.data = { complex: 'object' };
}
}
// 安全的数值组件
@EnableSoA
class SafeComponent extends Component {
public x: number = 0;
public y: number = 0;
public active: boolean = true;
}
describe('SoA边界情况和复杂属性测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('包含复杂对象的组件会有什么问题', () => {
console.log('\\n=== 测试复杂对象处理 ===');
// 创建包含复杂属性的组件
const originalComponent = new ProblematicComponent();
console.log('原始组件:', {
x: originalComponent.x,
y: originalComponent.y,
node: originalComponent.node?.name,
callback: typeof originalComponent.callback,
data: originalComponent.data
});
// 添加到SoA存储
manager.addComponent(1, originalComponent);
// 获取组件看看发生了什么
const retrievedComponent = manager.getComponent(1, ProblematicComponent);
console.log('取回的组件:', {
x: retrievedComponent?.x,
y: retrievedComponent?.y,
node: retrievedComponent?.node,
callback: retrievedComponent?.callback,
data: retrievedComponent?.data
});
// 验证数据完整性
expect(retrievedComponent?.x).toBe(0);
expect(retrievedComponent?.y).toBe(0);
// 复杂对象的问题
console.log('\\n⚠ 问题发现:');
console.log('- node对象:', retrievedComponent?.node);
console.log('- callback函数:', retrievedComponent?.callback);
console.log('- data对象:', retrievedComponent?.data);
// 复杂属性现在应该正确保存
expect(retrievedComponent?.node?.name).toBe('test'); // 应该保持原始值
expect(retrievedComponent?.callback).toBe(originalComponent.callback); // 应该是同一个函数
expect(retrievedComponent?.data).toEqual({ complex: 'object' }); // 应该保持原始数据
console.log('✅ 修复成功:复杂对象现在能正确处理!');
});
test('纯数值组件工作正常', () => {
console.log('\\n=== 测试纯数值组件 ===');
const safeComponent = new SafeComponent();
safeComponent.x = 100;
safeComponent.y = 200;
safeComponent.active = false;
manager.addComponent(1, safeComponent);
const retrieved = manager.getComponent(1, SafeComponent);
console.log('纯数值组件正常工作:', {
x: retrieved?.x,
y: retrieved?.y,
active: retrieved?.active
});
expect(retrieved?.x).toBe(100);
expect(retrieved?.y).toBe(200);
expect(retrieved?.active).toBe(false);
});
test('SoA是否能检测到不适合的组件类型', () => {
console.log('\\n=== 测试类型检测 ===');
// 当前实现会静默忽略复杂字段
// 这是一个潜在的问题!
const storage = manager.getStorage(ProblematicComponent);
console.log('存储类型:', storage.constructor.name);
// SoA存储应该能警告或拒绝不适合的组件
expect(storage.constructor.name).toBe('SoAStorage');
});
});

View File

@@ -1,159 +0,0 @@
import { Component } from '../../../src/ECS/Component';
import { ComponentStorageManager } from '../../../src/ECS/Core/ComponentStorage';
import { EnableSoA, HighPrecision, Float64 } from '../../../src/ECS/Core/SoAStorage';
// 包含所有基础类型的组件
@EnableSoA
class AllTypesComponent extends Component {
// 数值类型
public intNumber: number = 42;
public floatNumber: number = 3.14;
public zeroNumber: number = 0;
// 布尔类型
public trueBoolean: boolean = true;
public falseBoolean: boolean = false;
// 字符串类型
public emptyString: string = '';
public normalString: string = 'hello';
public longString: string = 'this is a long string with spaces and 123 numbers!';
// 其他基础类型
public nullValue: null = null;
public undefinedValue: undefined = undefined;
// 复杂类型
public arrayValue: number[] = [1, 2, 3];
public objectValue: { name: string } = { name: 'test' };
constructor() {
super();
}
}
// 边界测试专用组件
@EnableSoA
class BoundaryTestComponent extends Component {
// 高精度大整数
@HighPrecision
public maxInt: number = 0;
// 高精度小浮点数
@Float64
public minFloat: number = 0;
// 普通数值
public normalNumber: number = 0;
// 字符串测试
public testString: string = '';
public longString: string = '';
constructor() {
super();
}
}
describe('SoA所有数据类型处理测试', () => {
let manager: ComponentStorageManager;
beforeEach(() => {
manager = new ComponentStorageManager();
});
test('验证所有基础类型的处理', () => {
console.log('\\n=== 测试所有数据类型 ===');
// 创建包含各种类型的组件
const originalComponent = new AllTypesComponent();
originalComponent.normalString = 'modified string';
originalComponent.longString = '测试中文字符串 with emoji 🎉';
originalComponent.intNumber = 999;
originalComponent.floatNumber = 2.718;
originalComponent.trueBoolean = false;
originalComponent.falseBoolean = true;
console.log('原始组件数据:', {
intNumber: originalComponent.intNumber,
floatNumber: originalComponent.floatNumber,
trueBoolean: originalComponent.trueBoolean,
falseBoolean: originalComponent.falseBoolean,
emptyString: `"${originalComponent.emptyString}"`,
normalString: `"${originalComponent.normalString}"`,
longString: `"${originalComponent.longString}"`,
arrayValue: originalComponent.arrayValue,
objectValue: originalComponent.objectValue
});
// 存储到SoA
manager.addComponent(1, originalComponent);
// 获取并验证
const retrievedComponent = manager.getComponent(1, AllTypesComponent);
console.log('\\n取回的组件数据:', {
intNumber: retrievedComponent?.intNumber,
floatNumber: retrievedComponent?.floatNumber,
trueBoolean: retrievedComponent?.trueBoolean,
falseBoolean: retrievedComponent?.falseBoolean,
emptyString: `"${retrievedComponent?.emptyString}"`,
normalString: `"${retrievedComponent?.normalString}"`,
longString: `"${retrievedComponent?.longString}"`,
arrayValue: retrievedComponent?.arrayValue,
objectValue: retrievedComponent?.objectValue
});
// 验证数值类型
expect(retrievedComponent?.intNumber).toBe(999);
expect(retrievedComponent?.floatNumber).toBeCloseTo(2.718);
// 验证布尔类型
expect(retrievedComponent?.trueBoolean).toBe(false);
expect(retrievedComponent?.falseBoolean).toBe(true);
// 验证字符串类型
expect(retrievedComponent?.emptyString).toBe('');
expect(retrievedComponent?.normalString).toBe('modified string');
expect(retrievedComponent?.longString).toBe('测试中文字符串 with emoji 🎉');
// 验证复杂类型
expect(retrievedComponent?.arrayValue).toEqual([1, 2, 3]);
expect(retrievedComponent?.objectValue).toEqual({ name: 'test' });
console.log('\\n✅ 所有类型验证完成');
});
test('边界情况测试', () => {
console.log('\\n=== 边界情况测试 ===');
const component = new BoundaryTestComponent();
// 特殊数值
component.maxInt = Number.MAX_SAFE_INTEGER;
component.minFloat = Number.MIN_VALUE;
component.normalNumber = -0;
// 特殊字符串
component.testString = '\\n\\t\\r"\'\\\\'; // 转义字符
component.longString = 'a'.repeat(1000); // 长字符串
manager.addComponent(1, component);
const retrieved = manager.getComponent(1, BoundaryTestComponent);
console.log('边界情况结果:', {
maxInt: retrieved?.maxInt,
minFloat: retrieved?.minFloat,
negativeZero: retrieved?.normalNumber,
escapeStr: retrieved?.testString,
longStr: retrieved?.longString?.length
});
expect(retrieved?.maxInt).toBe(Number.MAX_SAFE_INTEGER);
expect(retrieved?.minFloat).toBe(Number.MIN_VALUE);
expect(retrieved?.testString).toBe('\\n\\t\\r"\'\\\\');
expect(retrieved?.longString).toBe('a'.repeat(1000));
console.log('✅ 边界情况测试通过');
});
});

View File

@@ -0,0 +1,512 @@
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
/**
* System初始化测试套件
*
* 测试覆盖:
* - 系统初始化时序问题(先添加实体 vs 先添加系统)
* - 系统重复初始化防护
* - 动态组件修改响应
* - 系统生命周期管理
*/
// 测试组件
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(...args: unknown[]) {
super();
const [x = 0, y = 0] = args as [number?, number?];
this.x = x;
this.y = y;
}
}
class VelocityComponent extends Component {
public vx: number;
public vy: number;
constructor(...args: unknown[]) {
super();
const [vx = 0, vy = 0] = args as [number?, number?];
this.vx = vx;
this.vy = vy;
}
}
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
const [health = 100] = args as [number?];
this.health = health;
}
}
class TagComponent extends Component {
public tag: string;
constructor(...args: unknown[]) {
super();
const [tag = ''] = args as [string?];
this.tag = tag;
}
}
class TestComponent extends Component {
public value: number;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
// 测试系统
class MovementSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
public onAddedEntities: Entity[] = [];
public onRemovedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override onRemoved(entity: Entity): void {
this.onRemovedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
for (const entity of entities) {
const position = entity.getComponent(PositionComponent)!;
const velocity = entity.getComponent(VelocityComponent)!;
position.x += velocity.vx;
position.y += velocity.vy;
}
}
}
class HealthSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
public onAddedEntities: Entity[] = [];
public onRemovedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override onRemoved(entity: Entity): void {
this.onRemovedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
for (const entity of entities) {
const health = entity.getComponent(HealthComponent)!;
if (health.health <= 0) {
entity.enabled = false;
}
}
}
}
class MultiComponentSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
public onAddedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, HealthComponent, TagComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
}
}
class TrackingSystem extends EntitySystem {
public initializeCallCount = 0;
public onChangedCallCount = 0;
public trackedEntities: Entity[] = [];
public override initialize(): void {
const wasInitialized = (this as any)._initialized;
super.initialize();
if (!wasInitialized) {
this.initializeCallCount++;
if (this.scene) {
for (const entity of this.scene.entities.buffer) {
this.onChanged(entity);
}
}
}
}
public onChanged(entity: Entity): void {
this.onChangedCallCount++;
if (this.isInterestedEntity(entity)) {
if (!this.trackedEntities.includes(entity)) {
this.trackedEntities.push(entity);
}
} else {
const index = this.trackedEntities.indexOf(entity);
if (index !== -1) {
this.trackedEntities.splice(index, 1);
}
}
}
public isInterestedEntity(entity: Entity): boolean {
return entity.hasComponent(TestComponent);
}
}
describe('SystemInitialization - 系统初始化测试', () => {
let scene: Scene;
beforeEach(() => {
ComponentTypeManager.instance.reset();
scene = new Scene();
scene.name = 'InitializationTestScene';
});
describe('初始化时序', () => {
test('先添加实体再添加系统 - 系统应该正确初始化', () => {
const player = scene.createEntity('Player');
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(player);
});
test('先添加系统再添加实体 - 系统应该正确响应', () => {
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(0);
const player = scene.createEntity('Player');
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(player);
});
test('先添加部分实体,再添加系统,再添加更多实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(0, 0));
entity1.addComponent(new VelocityComponent(1, 0));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(1);
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new PositionComponent(0, 0));
entity2.addComponent(new VelocityComponent(0, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(2);
expect(system.onAddedEntities[1]).toBe(entity2);
});
test('批量实体创建后系统初始化应该正确', () => {
const entities: Entity[] = [];
for (let i = 0; i < 5; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i, i));
entity.addComponent(new VelocityComponent(1, 1));
entities.push(entity);
}
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(5);
for (let i = 0; i < 5; i++) {
expect(system.onAddedEntities).toContain(entities[i]);
}
});
});
describe('重复初始化防护', () => {
test('系统被多次添加到场景 - 应该防止重复初始化', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
});
test('手动多次调用initialize - 应该防止重复处理', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
expect(system.onChangedCallCount).toBe(1);
system.initialize();
expect(system.initializeCallCount).toBe(1);
expect(system.onChangedCallCount).toBe(1);
expect(system.trackedEntities).toHaveLength(1);
});
test('系统被移除后重新添加 - 应该重新初始化', () => {
const system = new TrackingSystem();
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
scene.removeEntityProcessor(system);
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(2);
expect(system.trackedEntities).toHaveLength(1);
});
});
describe('动态组件修改响应', () => {
test('运行时添加组件 - 系统应该自动响应', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new VelocityComponent(1, 1));
scene.update(); // 触发系统查询
expect(system.onAddedEntities).toHaveLength(1);
expect(system.onAddedEntities[0]).toBe(entity);
});
test('运行时移除组件 - 系统应该自动响应', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
const velocity = entity.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.onAddedEntities).toHaveLength(1);
entity.removeComponent(velocity);
scene.update(); // 触发系统查询
expect(system.onRemovedEntities).toHaveLength(1);
expect(system.onRemovedEntities[0]).toBe(entity);
});
test('复杂的组件添加移除序列', () => {
const entity = scene.createEntity('Entity');
const system = new MovementSystem();
scene.addEntityProcessor(system);
entity.addComponent(new PositionComponent(0, 0));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
const velocity1 = entity.addComponent(new VelocityComponent(1, 1));
scene.update();
expect(system.onAddedEntities).toHaveLength(1);
entity.removeComponent(velocity1);
scene.update();
expect(system.onRemovedEntities).toHaveLength(1);
entity.addComponent(new VelocityComponent(2, 2));
scene.update();
expect(system.onAddedEntities).toHaveLength(2);
});
test('多个组件同时满足条件', () => {
const entity = scene.createEntity('Entity');
const system = new MultiComponentSystem();
scene.addEntityProcessor(system);
entity.addComponent(new PositionComponent(0, 0));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new HealthComponent(100));
scene.update();
expect(system.onAddedEntities).toHaveLength(0);
entity.addComponent(new TagComponent('player'));
scene.update();
expect(system.onAddedEntities).toHaveLength(1);
});
});
describe('多系统协同', () => {
test('多个系统同时响应同一实体', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(entity);
expect(healthSystem.onAddedEntities[0]).toBe(entity);
});
test('不同系统匹配不同实体', () => {
const movingEntity = scene.createEntity('Moving');
movingEntity.addComponent(new PositionComponent(0, 0));
movingEntity.addComponent(new VelocityComponent(1, 1));
const healthEntity = scene.createEntity('Health');
healthEntity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
expect(movementSystem.onAddedEntities).toHaveLength(1);
expect(movementSystem.onAddedEntities[0]).toBe(movingEntity);
expect(healthSystem.onAddedEntities).toHaveLength(1);
expect(healthSystem.onAddedEntities[0]).toBe(healthEntity);
});
test('组件变化影响多个系统', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.addComponent(new HealthComponent(100));
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
const multiSystem = new MultiComponentSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
scene.addEntityProcessor(multiSystem);
entity.addComponent(new TagComponent('player'));
scene.update(); // 触发系统查询
expect(multiSystem.onAddedEntities).toHaveLength(1);
expect(multiSystem.onAddedEntities[0]).toBe(entity);
});
});
describe('边界情况', () => {
test('空场景添加系统', () => {
const system = new MovementSystem();
scene.addEntityProcessor(system);
expect(system.initializeCalled).toBe(true);
expect(system.onAddedEntities).toHaveLength(0);
});
test('实体禁用状态不影响系统初始化', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
entity.enabled = false;
const system = new MovementSystem();
scene.addEntityProcessor(system);
// 禁用的实体仍然被系统跟踪但在process时会被过滤
expect(system.onAddedEntities).toHaveLength(1);
});
test('系统初始化时实体被销毁', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
const system = new MovementSystem();
scene.addEntityProcessor(system);
entity.destroy();
scene.update(); // 触发系统查询检测移除
expect(system.onRemovedEntities).toHaveLength(1);
});
});
});

View File

@@ -1,500 +0,0 @@
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
// 测试组件
class PositionComponent extends Component {
public x: number;
public y: number;
constructor(...args: unknown[]) {
super();
this.x = (args[0] as number) ?? 0;
this.y = (args[1] as number) ?? 0;
}
}
class VelocityComponent extends Component {
public vx: number;
public vy: number;
constructor(...args: unknown[]) {
super();
this.vx = (args[0] as number) ?? 0;
this.vy = (args[1] as number) ?? 0;
}
}
class HealthComponent extends Component {
public health: number;
constructor(...args: unknown[]) {
super();
this.health = (args[0] as number) ?? 100;
}
}
class TagComponent extends Component {
public tag: string;
constructor(...args: unknown[]) {
super();
this.tag = (args[0] as string) ?? '';
}
}
// 测试系统
class MovementSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
public onAddedEntities: Entity[] = [];
public onRemovedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(PositionComponent, VelocityComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override onRemoved(entity: Entity): void {
this.onRemovedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
for (const entity of entities) {
const position = entity.getComponent(PositionComponent)!;
const velocity = entity.getComponent(VelocityComponent)!;
position.x += velocity.vx;
position.y += velocity.vy;
}
}
}
class HealthSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
public onAddedEntities: Entity[] = [];
public onRemovedEntities: Entity[] = [];
constructor() {
super(Matcher.empty().all(HealthComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override onAdded(entity: Entity): void {
this.onAddedEntities.push(entity);
}
protected override onRemoved(entity: Entity): void {
this.onRemovedEntities.push(entity);
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
for (const entity of entities) {
const health = entity.getComponent(HealthComponent)!;
if (health.health <= 0) {
entity.enabled = false;
}
}
}
}
class MultiComponentSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public initializeCalled = false;
constructor() {
super(Matcher.empty().all(PositionComponent, HealthComponent, TagComponent));
}
public override initialize(): void {
this.initializeCalled = true;
super.initialize();
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
}
}
describe('ECS系统初始化时序问题深度测试', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene();
scene.name = "InitializeTestScene";
});
describe('基础时序问题测试', () => {
test('先添加实体再添加系统 - 系统应该正确初始化', () => {
// 创建实体并添加组件
const player = scene.createEntity("Player");
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
player.addComponent(new HealthComponent(100));
const enemy = scene.createEntity("Enemy");
enemy.addComponent(new PositionComponent(50, 60));
enemy.addComponent(new VelocityComponent(-1, 0));
enemy.addComponent(new HealthComponent(80));
// 验证实体已创建
expect(scene.entities.count).toBe(2);
// 添加系统
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
// 验证initialize方法被调用
expect(movementSystem.initializeCalled).toBe(true);
expect(healthSystem.initializeCalled).toBe(true);
// 验证系统正确识别已存在的实体
expect(movementSystem.entities.length).toBe(2);
expect(healthSystem.entities.length).toBe(2);
// 验证onAdded回调被正确调用
expect(movementSystem.onAddedEntities.length).toBe(2);
expect(movementSystem.onAddedEntities).toContain(player);
expect(movementSystem.onAddedEntities).toContain(enemy);
// 运行更新确认处理
scene.update();
expect(movementSystem.processedEntities.length).toBe(2);
expect(healthSystem.processedEntities.length).toBe(2);
// 检查移动逻辑是否生效
const playerPos = player.getComponent(PositionComponent)!;
expect(playerPos.x).toBe(11);
expect(playerPos.y).toBe(21);
});
test('先添加系统再添加实体 - 正常工作', () => {
// 先添加系统
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
// 验证initialize被调用但没有发现实体
expect(movementSystem.initializeCalled).toBe(true);
expect(healthSystem.initializeCalled).toBe(true);
expect(movementSystem.entities.length).toBe(0);
expect(healthSystem.entities.length).toBe(0);
// 创建实体
const player = scene.createEntity("Player");
player.addComponent(new PositionComponent(10, 20));
player.addComponent(new VelocityComponent(1, 1));
player.addComponent(new HealthComponent(100));
// 系统应该自动识别新实体
expect(movementSystem.entities.length).toBe(1);
expect(healthSystem.entities.length).toBe(1);
expect(movementSystem.onAddedEntities.length).toBe(1);
});
});
describe('复杂场景的时序测试', () => {
test('部分匹配实体的初始化', () => {
// 创建不同类型的实体
const fullEntity = scene.createEntity("FullEntity");
fullEntity.addComponent(new PositionComponent(0, 0));
fullEntity.addComponent(new VelocityComponent(1, 1));
fullEntity.addComponent(new HealthComponent(100));
const partialEntity1 = scene.createEntity("PartialEntity1");
partialEntity1.addComponent(new PositionComponent(10, 10));
partialEntity1.addComponent(new HealthComponent(50));
// 缺少VelocityComponent
const partialEntity2 = scene.createEntity("PartialEntity2");
partialEntity2.addComponent(new PositionComponent(20, 20));
partialEntity2.addComponent(new VelocityComponent(2, 2));
// 缺少HealthComponent
// 添加系统
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
// 验证选择性匹配
expect(movementSystem.entities).toContain(fullEntity);
expect(movementSystem.entities).not.toContain(partialEntity1);
expect(movementSystem.entities).toContain(partialEntity2);
expect(movementSystem.entities.length).toBe(2);
expect(healthSystem.entities).toContain(fullEntity);
expect(healthSystem.entities).toContain(partialEntity1);
expect(healthSystem.entities).not.toContain(partialEntity2);
expect(healthSystem.entities.length).toBe(2);
});
test('多组件要求系统的初始化', () => {
// 创建具有不同组件组合的实体
const entity1 = scene.createEntity("Entity1");
entity1.addComponent(new PositionComponent(0, 0));
entity1.addComponent(new HealthComponent(100));
entity1.addComponent(new TagComponent("player"));
const entity2 = scene.createEntity("Entity2");
entity2.addComponent(new PositionComponent(10, 10));
entity2.addComponent(new HealthComponent(80));
// 缺少TagComponent
const entity3 = scene.createEntity("Entity3");
entity3.addComponent(new PositionComponent(20, 20));
entity3.addComponent(new TagComponent("enemy"));
// 缺少HealthComponent
// 添加要求三个组件的系统
const multiSystem = new MultiComponentSystem();
scene.addEntityProcessor(multiSystem);
// 只有entity1应该匹配
expect(multiSystem.entities.length).toBe(1);
expect(multiSystem.entities).toContain(entity1);
expect(multiSystem.entities).not.toContain(entity2);
expect(multiSystem.entities).not.toContain(entity3);
});
test('批量实体创建后的系统初始化', () => {
// 批量创建实体
const entities = scene.createEntities(10, "BatchEntity");
// 为所有实体添加组件
entities.forEach((entity, index) => {
entity.addComponent(new PositionComponent(index * 10, index * 10));
entity.addComponent(new VelocityComponent(index, index));
if (index % 2 === 0) {
entity.addComponent(new HealthComponent(100 - index * 10));
}
});
// 添加系统
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
// 验证系统正确处理批量实体
expect(movementSystem.entities.length).toBe(10); // 所有实体都有Position+Velocity
expect(healthSystem.entities.length).toBe(5); // 只有偶数索引的实体有Health
// 验证onAdded回调被正确调用
expect(movementSystem.onAddedEntities.length).toBe(10);
expect(healthSystem.onAddedEntities.length).toBe(5);
});
});
describe('动态组件修改后的系统响应', () => {
test('运行时添加组件 - 系统自动响应', () => {
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
// 创建只有位置组件的实体
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(0, 0));
// 系统不应该匹配
expect(movementSystem.entities.length).toBe(0);
// 添加速度组件
entity.addComponent(new VelocityComponent(5, 5));
// 系统应该立即识别
expect(movementSystem.entities.length).toBe(1);
expect(movementSystem.entities).toContain(entity);
expect(movementSystem.onAddedEntities).toContain(entity);
});
test('运行时移除组件 - 系统自动响应', () => {
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
// 创建完整的可移动实体
const entity = scene.createEntity("MovableEntity");
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(5, 5));
// 系统应该识别
expect(movementSystem.entities.length).toBe(1);
// 移除速度组件
const velocityComponent = entity.getComponent(VelocityComponent);
if (velocityComponent) {
entity.removeComponent(velocityComponent);
}
// 系统应该移除实体
expect(movementSystem.entities.length).toBe(0);
expect(movementSystem.onRemovedEntities).toContain(entity);
});
test('复杂的组件添加移除序列', () => {
const movementSystem = new MovementSystem();
const healthSystem = new HealthSystem();
scene.addEntityProcessor(movementSystem);
scene.addEntityProcessor(healthSystem);
const entity = scene.createEntity("ComplexEntity");
// 初始状态:无组件
expect(movementSystem.entities.length).toBe(0);
expect(healthSystem.entities.length).toBe(0);
// 添加位置组件
entity.addComponent(new PositionComponent(0, 0));
expect(movementSystem.entities.length).toBe(0);
expect(healthSystem.entities.length).toBe(0);
// 添加健康组件
entity.addComponent(new HealthComponent(100));
expect(movementSystem.entities.length).toBe(0);
expect(healthSystem.entities.length).toBe(1);
// 添加速度组件
entity.addComponent(new VelocityComponent(1, 1));
expect(movementSystem.entities.length).toBe(1);
expect(healthSystem.entities.length).toBe(1);
// 移除健康组件
const healthComponent = entity.getComponent(HealthComponent);
if (healthComponent) {
entity.removeComponent(healthComponent);
}
expect(movementSystem.entities.length).toBe(1);
expect(healthSystem.entities.length).toBe(0);
// 移除位置组件
const positionComponent = entity.getComponent(PositionComponent);
if (positionComponent) {
entity.removeComponent(positionComponent);
}
expect(movementSystem.entities.length).toBe(0);
expect(healthSystem.entities.length).toBe(0);
});
});
describe('系统重复添加和移除测试', () => {
test('重复添加同一个系统 - 应该忽略', () => {
const movementSystem = new MovementSystem();
// 第一次添加
scene.addEntityProcessor(movementSystem);
expect(scene.entityProcessors.count).toBe(1);
expect(movementSystem.initializeCalled).toBe(true);
// 重置标志
movementSystem.initializeCalled = false;
// 第二次添加同一个系统
scene.addEntityProcessor(movementSystem);
expect(scene.entityProcessors.count).toBe(1); // 没有增加
expect(movementSystem.initializeCalled).toBe(false); // initialize不应该再次调用
});
test('添加后移除再添加 - 应该重新初始化', () => {
const entity = scene.createEntity("TestEntity");
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
const movementSystem = new MovementSystem();
// 第一次添加
scene.addEntityProcessor(movementSystem);
expect(movementSystem.entities.length).toBe(1);
expect(movementSystem.initializeCalled).toBe(true);
// 移除系统
scene.removeEntityProcessor(movementSystem);
expect(scene.entityProcessors.count).toBe(0);
// 重置状态
movementSystem.initializeCalled = false;
movementSystem.onAddedEntities = [];
// 重新添加
scene.addEntityProcessor(movementSystem);
expect(movementSystem.entities.length).toBe(1);
expect(movementSystem.initializeCalled).toBe(true);
expect(movementSystem.onAddedEntities.length).toBe(1);
});
});
describe('空场景和空系统的边界情况', () => {
test('空场景添加系统 - 不应该出错', () => {
const movementSystem = new MovementSystem();
expect(() => {
scene.addEntityProcessor(movementSystem);
}).not.toThrow();
expect(movementSystem.initializeCalled).toBe(true);
expect(movementSystem.entities.length).toBe(0);
});
test('有实体但没有匹配组件 - 系统应该为空', () => {
// 创建只有健康组件的实体
const entity = scene.createEntity("HealthOnlyEntity");
entity.addComponent(new HealthComponent(100));
// 添加移动系统需要Position+Velocity
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
expect(movementSystem.entities.length).toBe(0);
expect(movementSystem.onAddedEntities.length).toBe(0);
});
test('实体被禁用 - 系统仍应包含但不处理', () => {
const entity = scene.createEntity("DisabledEntity");
entity.addComponent(new PositionComponent(0, 0));
entity.addComponent(new VelocityComponent(1, 1));
const movementSystem = new MovementSystem();
scene.addEntityProcessor(movementSystem);
expect(movementSystem.entities.length).toBe(1);
// 禁用实体
entity.enabled = false;
// 系统仍然包含实体,但处理时应该跳过
expect(movementSystem.entities.length).toBe(1);
scene.update();
// 处理逻辑中应该检查enabled状态
// 由于实体被禁用,位置不应该改变(这取决于系统实现)
});
});
afterEach(() => {
scene.destroyAllEntities();
const processors = [...scene.entityProcessors.processors];
processors.forEach(processor => scene.removeEntityProcessor(processor));
});
});

View File

@@ -1,140 +0,0 @@
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { ComponentTypeManager } from '../../../src/ECS/Utils/ComponentTypeManager';
class TestComponent extends Component {
public value: number = 0;
constructor(...args: unknown[]) {
super();
const [value = 0] = args as [number?];
this.value = value;
}
}
class TrackingSystem extends EntitySystem {
public initializeCallCount = 0;
public onChangedCallCount = 0;
public trackedEntities: Entity[] = [];
public override initialize(): void {
// 必须先调用父类的initialize来检查防重复逻辑
const wasInitialized = (this as any)._initialized;
super.initialize();
// 只有在真正执行初始化时才增加计数和处理实体
if (!wasInitialized) {
this.initializeCallCount++;
// 处理所有现有实体
if (this.scene) {
for (const entity of this.scene.entities.buffer) {
this.onChanged(entity);
}
}
}
}
public onChanged(entity: Entity): void {
this.onChangedCallCount++;
if (this.isInterestedEntity(entity)) {
if (!this.trackedEntities.includes(entity)) {
this.trackedEntities.push(entity);
}
} else {
const index = this.trackedEntities.indexOf(entity);
if (index !== -1) {
this.trackedEntities.splice(index, 1);
}
}
}
public isInterestedEntity(entity: Entity): boolean {
return entity.hasComponent(TestComponent);
}
}
describe('系统多次初始化问题测试', () => {
let scene: Scene;
let system: TrackingSystem;
beforeEach(() => {
ComponentTypeManager.instance.reset();
scene = new Scene();
system = new TrackingSystem();
});
test('系统被多次添加到场景 - 应该防止重复初始化', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
// 第一次添加系统
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities.length).toBe(1);
expect(system.onChangedCallCount).toBe(1);
// 再次添加同一个系统 - 应该被忽略
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1); // 不应该增加
expect(system.trackedEntities.length).toBe(1); // 实体不应该重复
expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用
});
test('手动多次调用initialize - 应该防止重复处理', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities.length).toBe(1);
expect(system.onChangedCallCount).toBe(1);
// 手动再次调用initialize - 应该被防止
system.initialize();
expect(system.initializeCallCount).toBe(1); // 不应该增加
expect(system.onChangedCallCount).toBe(1); // onChanged不应该重复调用
expect(system.trackedEntities.length).toBe(1);
});
test('系统被移除后重新添加 - 应该重新初始化', () => {
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(10));
// 添加系统
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities.length).toBe(1);
// 移除系统
scene.removeEntityProcessor(system);
// 重新添加系统 - 应该重新初始化
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(2); // 应该重新初始化
expect(system.trackedEntities.length).toBe(1);
});
test('多个实体的重复初始化应该被防止', () => {
// 创建多个实体
const entities = [];
for (let i = 0; i < 5; i++) {
const entity = scene.createEntity(`Entity${i}`);
entity.addComponent(new TestComponent(i));
entities.push(entity);
}
scene.addEntityProcessor(system);
expect(system.initializeCallCount).toBe(1);
expect(system.trackedEntities.length).toBe(5);
expect(system.onChangedCallCount).toBe(5);
// 手动再次初始化 - 应该被防止
system.initialize();
expect(system.initializeCallCount).toBe(1); // 不应该增加
expect(system.onChangedCallCount).toBe(5); // 不应该重复处理
expect(system.trackedEntities.length).toBe(5);
});
});

View File

@@ -2,6 +2,7 @@ import { Core } from '../../../src/Core';
import { Scene } from '../../../src/ECS/Scene';
import { World, IGlobalSystem } from '../../../src/ECS/World';
import { WorldManager } from '../../../src/ECS/WorldManager';
import { SceneManager } from '../../../src/ECS/SceneManager';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import { Component } from '../../../src/ECS/Component';
import { Matcher } from '../../../src/ECS/Utils/Matcher';
@@ -10,580 +11,185 @@ import { Entity } from '../../../src/ECS/Entity';
// 测试用组件
class TestComponent extends Component {
public value: number = 0;
constructor(value: number = 0) {
super();
this.value = value;
}
public reset(): void {
this.value = 0;
}
}
class NetworkComponent extends Component {
public playerId: string;
constructor(playerId: string) {
super();
this.playerId = playerId;
}
public reset(): void {
this.playerId = '';
}
}
// 测试用系统
class TestGlobalSystem extends EntitySystem {
public processedEntities: Entity[] = [];
public updateCount: number = 0;
constructor() {
super(Matcher.empty().all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processedEntities = [...entities];
this.updateCount++;
}
}
// 正确的全局系统实现
class NetworkSyncGlobalSystem implements IGlobalSystem {
public readonly name = 'NetworkSyncSystem';
public updateCount: number = 0;
public initialize(): void {
// 初始化网络连接等
}
public update(): void {
this.updateCount++;
// 同步网络数据等全局逻辑
}
public reset(): void {
this.updateCount = 0;
}
public destroy(): void {
// 清理网络连接等
}
}
// Scene级别的EntitySystem正确的用法
class NetworkSyncSystem extends EntitySystem {
public syncCount: number = 0;
constructor() {
super(Matcher.empty().all(NetworkComponent));
}
protected override process(entities: Entity[]): void {
this.syncCount++;
}
}
// World级别的网络同步全局系统
// 全局系统实现
class NetworkGlobalSystem implements IGlobalSystem {
public readonly name = 'NetworkGlobalSystem';
public syncCount: number = 0;
public initialize(): void {
// 初始化网络连接
}
public update(): void {
this.syncCount++;
// 全局网络同步逻辑
}
public reset(): void {
this.syncCount = 0;
}
public destroy(): void {
// 清理网络连接
}
}
// 测试用Scene
class TestScene extends Scene {
public updateCallCount: number = 0;
public override update(): void {
super.update();
this.updateCallCount++;
}
}
/**
* World与Core集成测试
*
* 注意v3.0重构后Core不再直接管理Scene/World
* - 场景管理由 SceneManager 负责
* - 多世界管理由 WorldManager 负责
* - Core 仅负责全局服务Timer、Performance等
*
* 大部分旧的集成测试已移至 SceneManager.test.ts 和 WorldManager.test.ts
*/
describe('World与Core集成测试', () => {
let worldManager: WorldManager;
let sceneManager: SceneManager;
beforeEach(() => {
// 重置Core和WorldManager
if ((Core as any)._instance) {
(Core as any)._instance = null;
// 重置Core
if (Core.Instance) {
Core.destroy();
}
WorldManager['_instance'] = null;
Core.create({ debug: false });
// WorldManager和SceneManager不再是单例
worldManager = new WorldManager();
sceneManager = new SceneManager();
});
afterEach(() => {
// 清理资源
if ((Core as any)._instance) {
const worldManager = Core.getWorldManager?.();
if (worldManager) {
const worldIds = worldManager.getWorldIds();
worldIds.forEach(id => {
worldManager.removeWorld(id);
});
}
(Core as any)._instance = null;
if (sceneManager) {
sceneManager.destroy();
}
if (worldManager) {
worldManager.destroy();
}
if (Core.Instance) {
Core.destroy();
}
WorldManager['_instance'] = null;
});
describe('融合设计基础功能', () => {
test('单Scene模式应该保持向后兼容', () => {
Core.create({ debug: false });
// 传统单Scene用法
describe('基础功能', () => {
test('Core应该能够独立运行', () => {
expect(Core.Instance).toBeDefined();
// Core.update 仅更新全局服务
Core.update(0.016);
});
test('SceneManager应该能够独立管理场景', () => {
const scene = new Scene();
scene.name = 'TestScene';
Core.setScene(scene);
const retrievedScene = Core.getScene();
expect(retrievedScene).toBe(scene);
expect(retrievedScene?.name).toBe('TestScene');
sceneManager.setScene(scene);
expect(sceneManager.currentScene).toBe(scene);
expect(sceneManager.hasScene).toBe(true);
// 场景更新独立于Core
sceneManager.update();
});
test('启用WorldManager应该支持多World功能', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
expect(worldManager).toBeDefined();
const world = worldManager.createWorld('TestWorld');
expect(world).toBeDefined();
expect(world.name).toBe('TestWorld');
});
test('getWorldManager应该自动创建WorldManager', () => {
Core.create({ debug: false });
// 获取WorldManager会自动创建实例
const worldManager = Core.getWorldManager();
expect(worldManager).toBeDefined();
// 多次获取应该返回同一个实例
const worldManager2 = Core.getWorldManager();
expect(worldManager2).toBe(worldManager);
});
test('单Scene模式下Core.update应该正常工作', () => {
Core.create({ debug: false });
const scene = new TestScene();
Core.setScene(scene);
// 模拟更新
Core.update(0.016);
expect(scene.updateCallCount).toBeGreaterThan(0);
test('WorldManager应该能够独立管理多个World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
expect(worldManager.worldCount).toBe(2);
expect(worldManager.getWorld('world1')).toBe(world1);
expect(worldManager.getWorld('world2')).toBe(world2);
});
});
describe('默认World机制', () => {
test('设置Scene应该自动创建默认World', () => {
Core.create({ debug: false });
describe('组合使用', () => {
test('Core + SceneManager 应该正确协作', () => {
const scene = new Scene();
Core.setScene(scene);
// 启用WorldManager后应该能看到默认World
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
expect(worldManager.getWorld('__default__')).toBeDefined();
const defaultWorld = worldManager.getWorld('__default__');
expect(defaultWorld).toBeDefined();
expect(defaultWorld?.getScene('__main__')).toBe(scene);
sceneManager.setScene(scene);
const entity = scene.createEntity('TestEntity');
entity.addComponent(new TestComponent(42));
// 游戏循环
Core.update(0.016); // 更新全局服务
sceneManager.update(); // 更新场景
expect(scene.entities.count).toBe(1);
});
test('默认World的Scene应该正确激活', () => {
Core.create({ debug: false });
const scene = new Scene();
Core.setScene(scene);
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const defaultWorld = worldManager.getWorld('__default__');
expect(defaultWorld?.isSceneActive('__main__')).toBe(true);
test('Core + WorldManager 应该正确协作', () => {
const world = worldManager.createWorld('test-world');
const scene = world.createScene('main', new Scene());
world.start();
// 游戏循环
Core.update(0.016); // 更新全局服务
worldManager.updateAll(); // 更新所有World
expect(world.isActive).toBe(true);
});
test('替换默认Scene应该正确处理', () => {
Core.create({ debug: false });
const scene1 = new Scene();
scene1.name = 'Scene1';
Core.setScene(scene1);
const scene2 = new Scene();
scene2.name = 'Scene2';
Core.setScene(scene2);
const currentScene = Core.getScene();
expect(currentScene).toBe(scene2);
expect(currentScene?.name).toBe('Scene2');
});
});
describe('多World更新机制', () => {
test('Core.update应该更新所有活跃World', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
// 创建多个World
const world1 = worldManager.createWorld('World1');
const world2 = worldManager.createWorld('World2');
const world3 = worldManager.createWorld('World3');
// 为每个World创建Scene和System
const scene1 = world1.createScene('scene1', new TestScene());
const scene2 = world2.createScene('scene2', new TestScene());
const scene3 = world3.createScene('scene3', new TestScene());
// 启动部分World
worldManager.setWorldActive('World1', true);
worldManager.setWorldActive('World2', true);
// world3保持未启动
world1.setSceneActive('scene1', true);
world2.setSceneActive('scene2', true);
// 执行更新
Core.update(0.016);
// 检查只有激活的World被更新
expect(scene1.updateCallCount).toBeGreaterThan(0);
expect(scene2.updateCallCount).toBeGreaterThan(0);
expect(scene3.updateCallCount).toBe(0);
});
test('全局系统应该在Scene更新前执行', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const world = worldManager.createWorld('TestWorld');
// 添加正确设计的全局系统业务逻辑系统不是EntitySystem
const globalSystem = new NetworkSyncGlobalSystem();
test('World的全局系统应该能够正常工作', () => {
const world = worldManager.createWorld('test-world');
const globalSystem = new NetworkGlobalSystem();
world.addGlobalSystem(globalSystem);
// 创建Scene
const scene = world.createScene('testScene');
worldManager.setWorldActive('TestWorld', true);
world.setSceneActive('testScene', true);
// 执行更新
Core.update(0.016);
// 验证全局System被正确更新
expect(globalSystem.updateCount).toBeGreaterThan(0);
worldManager.setWorldActive('test-world', true);
// 更新World
worldManager.updateAll();
expect(globalSystem.syncCount).toBeGreaterThan(0);
});
});
describe('多房间游戏服务器场景', () => {
test('多个游戏房间应该独立运行', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
// 创建两个游戏房间
const room1 = worldManager.createWorld('Room_001');
const room2 = worldManager.createWorld('Room_002');
// 为每个房间设置Scene
const gameScene1 = room1.createScene('game');
const gameScene2 = room2.createScene('game');
// 为每个房间添加全局网络系统
const netSystem1 = new NetworkGlobalSystem();
const netSystem2 = new NetworkGlobalSystem();
room1.addGlobalSystem(netSystem1);
room2.addGlobalSystem(netSystem2);
// 在每个房间创建玩家
const player1 = gameScene1.createEntity('Player1');
player1.addComponent(new NetworkComponent('player_123'));
const player2 = gameScene2.createEntity('Player2');
player2.addComponent(new NetworkComponent('player_456'));
// 启动房间
worldManager.setWorldActive('Room_001', true);
worldManager.setWorldActive('Room_002', true);
room1.setSceneActive('game', true);
room2.setSceneActive('game', true);
// 模拟游戏循环
for (let i = 0; i < 5; i++) {
Core.update(0.016);
}
// 验证每个房间独立运行
expect(netSystem1.syncCount).toBeGreaterThan(0);
expect(netSystem2.syncCount).toBeGreaterThan(0);
expect(room1.getActiveSceneCount()).toBe(1);
expect(room2.getActiveSceneCount()).toBe(1);
describe('隔离性测试', () => {
test('多个WorldManager实例应该完全隔离', () => {
const manager1 = new WorldManager();
const manager2 = new WorldManager();
manager1.createWorld('world1');
manager2.createWorld('world2');
expect(manager1.getWorld('world1')).toBeDefined();
expect(manager1.getWorld('world2')).toBeNull();
expect(manager2.getWorld('world2')).toBeDefined();
expect(manager2.getWorld('world1')).toBeNull();
// 清理
manager1.destroy();
manager2.destroy();
});
test('房间销毁应该完全清理资源', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
// 创建房间
const room = worldManager.createWorld('TempRoom');
const scene = room.createScene('game');
// 添加内容
for (let i = 0; i < 10; i++) {
const entity = scene.createEntity(`Entity${i}`);
entity.addComponent(new TestComponent(i));
}
room.addGlobalSystem(new NetworkSyncGlobalSystem());
worldManager.setWorldActive('TempRoom', true);
room.setSceneActive('game', true);
// 验证房间正常运行
Core.update(0.016);
const beforeDestroy = worldManager.getStats();
expect(beforeDestroy.totalWorlds).toBe(1);
expect(beforeDestroy.activeWorlds).toBe(1);
// 销毁房间
worldManager.removeWorld('TempRoom');
const afterDestroy = worldManager.getStats();
expect(afterDestroy.totalWorlds).toBe(0);
expect(afterDestroy.activeWorlds).toBe(0);
test('多个SceneManager实例应该完全隔离', () => {
const sm1 = new SceneManager();
const sm2 = new SceneManager();
const scene1 = new Scene();
const scene2 = new Scene();
sm1.setScene(scene1);
sm2.setScene(scene2);
expect(sm1.currentScene).toBe(scene1);
expect(sm2.currentScene).toBe(scene2);
// 清理
sm1.destroy();
sm2.destroy();
});
});
describe('客户端多层Scene架构', () => {
test('分层Scene应该同时运行', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const clientWorld = worldManager.createWorld('ClientWorld');
// 创建不同层的Scene
const gameplayScene = clientWorld.createScene('gameplay', new TestScene());
const uiScene = clientWorld.createScene('ui', new TestScene());
const effectsScene = clientWorld.createScene('effects', new TestScene());
// 启动世界并激活所有Scene
worldManager.setWorldActive('ClientWorld', true);
clientWorld.setSceneActive('gameplay', true);
clientWorld.setSceneActive('ui', true);
clientWorld.setSceneActive('effects', true);
// 执行更新
Core.update(0.016);
// 验证所有Scene都被更新
expect(gameplayScene.updateCallCount).toBeGreaterThan(0);
expect(uiScene.updateCallCount).toBeGreaterThan(0);
expect(effectsScene.updateCallCount).toBeGreaterThan(0);
});
test('Scene的动态激活和停用', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const world = worldManager.createWorld('DynamicWorld');
const gameScene = world.createScene('game', new TestScene());
const menuScene = world.createScene('menu', new TestScene());
worldManager.setWorldActive('DynamicWorld', true);
// 初始状态只有游戏Scene激活
world.setSceneActive('game', true);
world.setSceneActive('menu', false);
Core.update(0.016);
const gameCount1 = gameScene.updateCallCount;
const menuCount1 = menuScene.updateCallCount;
// 切换到菜单
world.setSceneActive('game', false);
world.setSceneActive('menu', true);
Core.update(0.016);
const gameCount2 = gameScene.updateCallCount;
const menuCount2 = menuScene.updateCallCount;
// 验证Scene状态切换
expect(gameCount2).toBe(gameCount1); // 游戏Scene停止更新
expect(menuCount2).toBeGreaterThan(menuCount1); // 菜单Scene开始更新
});
});
describe('性能和稳定性', () => {
test('大量World和Scene应该稳定运行', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const worldCount = 20;
const scenePerWorld = 3;
// 创建大量World和Scene
for (let i = 0; i < worldCount; i++) {
const world = worldManager.createWorld(`World${i}`);
for (let j = 0; j < scenePerWorld; j++) {
const scene = world.createScene(`Scene${j}`, new TestScene());
// 添加一些实体
for (let k = 0; k < 5; k++) {
const entity = scene.createEntity(`Entity${k}`);
entity.addComponent(new TestComponent(k));
}
world.setSceneActive(`Scene${j}`, true);
}
worldManager.setWorldActive(`World${i}`, true);
}
// 验证所有资源创建成功
expect(worldManager.getWorldIds()).toHaveLength(worldCount);
expect(worldManager.getActiveWorlds()).toHaveLength(worldCount);
// 执行多次更新测试稳定性
for (let i = 0; i < 10; i++) {
expect(() => {
Core.update(0.016);
}).not.toThrow();
}
// 验证更新正常工作
const activeWorlds = worldManager.getActiveWorlds();
activeWorlds.forEach(world => {
const scenes = world.getAllScenes();
scenes.forEach(scene => {
if (scene instanceof TestScene && world.isSceneActive(scene.name)) {
expect(scene.updateCallCount).toBeGreaterThan(0);
}
});
});
});
test('频繁的World创建和销毁应该不影响性能', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
// 频繁创建和销毁World
for (let cycle = 0; cycle < 10; cycle++) {
// 创建批次World
const worldIds: string[] = [];
for (let i = 0; i < 5; i++) {
const worldId = `Cycle${cycle}_World${i}`;
worldIds.push(worldId);
const world = worldManager.createWorld(worldId);
const scene = world.createScene('test');
scene.createEntity('entity');
worldManager.setWorldActive(worldId, true);
world.setSceneActive('test', true);
}
// 更新一次
Core.update(0.016);
// 销毁批次World
worldIds.forEach(id => {
worldManager.removeWorld(id);
});
// 验证清理完成
expect(worldManager.getWorldIds()).toHaveLength(0);
expect(worldManager.getActiveWorlds()).toHaveLength(0);
}
});
});
describe('错误处理和边界情况', () => {
test('Core未初始化时操作应该抛出合适错误', () => {
// getScene 会返回 null 而不是抛出错误
expect(Core.getScene()).toBeNull();
expect(() => {
Core.setScene(new Scene());
}).toThrow();
});
test('在World销毁后继续操作应该安全', () => {
Core.create({ debug: false });
Core.enableWorldManager();
const worldManager = Core.getWorldManager();
const world = worldManager.createWorld('DestroyTest');
worldManager.setWorldActive('DestroyTest', true);
worldManager.removeWorld('DestroyTest');
// 对已销毁的World进行操作应该不会崩溃
expect(() => {
world.updateGlobalSystems();
world.updateScenes();
}).not.toThrow();
});
test('混合使用单Scene和多World模式', () => {
Core.create({ debug: false });
// 直接启用WorldManager避免先使用单Scene创建限制性配置
const worldManager = Core.getWorldManager();
// 然后使用单Scene模式
const singleScene = new Scene();
Core.setScene(singleScene);
// 验证默认World被创建
expect(worldManager.getWorld('__default__')).toBeDefined();
// 创建额外的World
const extraWorld = worldManager.createWorld('ExtraWorld');
worldManager.setWorldActive('ExtraWorld', true);
// 两种模式应该能共存
expect(() => {
Core.update(0.016);
}).not.toThrow();
});
});
});
});

View File

@@ -518,21 +518,17 @@ describe('Scene - 场景管理系统测试', () => {
// 测试查询性能
const queryStartTime = performance.now();
const positionResult = scene.querySystem.queryAll(PositionComponent);
const velocityResult = scene.querySystem.queryAll(VelocityComponent);
const healthResult = scene.querySystem.queryAll(HealthComponent);
const queryTime = performance.now() - queryStartTime;
expect(positionResult.entities.length).toBe(entityCount);
expect(velocityResult.entities.length).toBe(entityCount / 2);
expect(healthResult.entities.length).toBe(Math.floor(entityCount / 3) + 1);
// 性能断言(这些值可能需要根据实际环境调整)
// 性能记录场景创建性能数据不设硬阈值避免CI不稳定
// 性能记录场景查询性能数据不设硬阈值避免CI不稳定
console.log(`创建${entityCount}个实体耗时: ${creationTime.toFixed(2)}ms`);
console.log(`查询操作耗时: ${queryTime.toFixed(2)}ms`);
});

View File

@@ -0,0 +1,762 @@
/**
* 增量序列化系统测试
*/
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import {
Serializable,
Serialize,
IncrementalSerializer,
ChangeOperation
} from '../../../src/ECS/Serialization';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件定义
@ECSComponent('IncTest_Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('IncTest_Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public dx: number = 0;
@Serialize()
public dy: number = 0;
}
@ECSComponent('IncTest_Health')
@Serializable({ version: 1 })
class HealthComponent extends Component {
@Serialize()
public current: number = 100;
@Serialize()
public max: number = 100;
}
describe('Incremental Serialization System', () => {
let scene: Scene;
beforeEach(() => {
IncrementalSerializer.resetVersion();
ComponentRegistry.reset();
// 重新注册测试组件
ComponentRegistry.register(PositionComponent);
ComponentRegistry.register(VelocityComponent);
ComponentRegistry.register(HealthComponent);
scene = new Scene({ name: 'IncrementalTestScene' });
});
afterEach(() => {
scene.end();
});
describe('Scene Snapshot', () => {
it('应该创建场景快照', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
});
it('应该在快照中包含所有实体', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
const snapshot = IncrementalSerializer.createSnapshot(scene);
expect(snapshot.entityIds.size).toBe(2);
expect(snapshot.entityIds.has(entity1.id)).toBe(true);
expect(snapshot.entityIds.has(entity2.id)).toBe(true);
});
it('应该在快照中包含实体基本信息', () => {
const entity = scene.createEntity('TestEntity');
entity.tag = 42;
entity.active = false;
entity.enabled = false;
entity.updateOrder = 5;
const snapshot = IncrementalSerializer.createSnapshot(scene);
const entityData = snapshot.entities.get(entity.id);
expect(entityData).toBeDefined();
expect(entityData!.name).toBe('TestEntity');
expect(entityData!.tag).toBe(42);
expect(entityData!.active).toBe(false);
expect(entityData!.enabled).toBe(false);
expect(entityData!.updateOrder).toBe(5);
});
it('应该在快照中包含组件数据', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(100, 200));
const snapshot = IncrementalSerializer.createSnapshot(scene, {
deepComponentComparison: true
});
const components = snapshot.components.get(entity.id);
expect(components).toBeDefined();
expect(components!.has('IncTest_Position')).toBe(true);
});
});
describe('Entity Changes Detection', () => {
it('应该检测新增的实体', () => {
scene.createIncrementalSnapshot();
const newEntity = scene.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityAdded);
expect(incremental.entityChanges[0].entityId).toBe(newEntity.id);
expect(incremental.entityChanges[0].entityName).toBe('NewEntity');
});
it('应该检测删除的实体', () => {
const entity = scene.createEntity('ToDelete');
scene.createIncrementalSnapshot();
entity.destroy();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityRemoved);
expect(incremental.entityChanges[0].entityId).toBe(entity.id);
});
it('应该检测实体属性变更', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.name = 'UpdatedName';
entity.tag = 99;
entity.active = false;
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(1);
expect(incremental.entityChanges[0].operation).toBe(ChangeOperation.EntityUpdated);
expect(incremental.entityChanges[0].entityData!.name).toBe('UpdatedName');
expect(incremental.entityChanges[0].entityData!.tag).toBe(99);
expect(incremental.entityChanges[0].entityData!.active).toBe(false);
});
});
describe('Component Changes Detection', () => {
it('应该检测新增的组件', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentAdded);
expect(incremental.componentChanges[0].entityId).toBe(entity.id);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测删除的组件', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentRemoved);
expect(incremental.componentChanges[0].componentType).toBe('IncTest_Position');
});
it('应该检测组件数据变更', () => {
const entity = scene.createEntity('Entity');
const pos = new PositionComponent(10, 20);
entity.addComponent(pos);
scene.createIncrementalSnapshot();
pos.x = 100;
pos.y = 200;
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(1);
expect(incremental.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
expect(incremental.componentChanges[0].componentData!.data.x).toBe(100);
expect(incremental.componentChanges[0].componentData!.data.y).toBe(200);
});
it('应该检测多个组件变更', () => {
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
entity.addComponent(new VelocityComponent());
entity.addComponent(new HealthComponent());
entity.removeComponentByType(PositionComponent);
const incremental = scene.serializeIncremental();
expect(incremental.componentChanges.length).toBe(3);
});
});
describe('Scene Data Changes Detection', () => {
it('应该检测新增的场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'sunny');
scene.sceneData.set('time', 12.5);
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(2);
});
it('应该检测更新的场景数据', () => {
scene.sceneData.set('weather', 'sunny');
scene.createIncrementalSnapshot();
scene.sceneData.set('weather', 'rainy');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].key).toBe('weather');
expect(incremental.sceneDataChanges[0].value).toBe('rainy');
});
it('应该检测删除的场景数据', () => {
scene.sceneData.set('temp', 'value');
scene.createIncrementalSnapshot();
scene.sceneData.delete('temp');
const incremental = scene.serializeIncremental();
expect(incremental.sceneDataChanges.length).toBe(1);
expect(incremental.sceneDataChanges[0].deleted).toBe(true);
});
});
describe('Apply Incremental Changes', () => {
it('应该应用实体添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const newEntity = scene1.createEntity('NewEntity');
newEntity.addComponent(new PositionComponent(50, 100));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(1);
const entity = scene2.findEntity('NewEntity');
expect(entity).not.toBeNull();
expect(entity!.hasComponent(PositionComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('应该应用实体删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity = scene1.createEntity('ToDelete');
scene1.createIncrementalSnapshot();
entity.destroy();
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('ToDelete');
Object.defineProperty(entity2, 'id', { value: entity.id, writable: true });
scene2.applyIncremental(incremental);
expect(scene2.entities.count).toBe(0);
scene1.end();
scene2.end();
});
it('应该应用实体属性更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.name = 'UpdatedName';
entity1.tag = 42;
entity1.active = false;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.name).toBe('UpdatedName');
expect(entity2.tag).toBe(42);
expect(entity2.active).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件添加变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
scene1.createIncrementalSnapshot();
entity1.addComponent(new PositionComponent(100, 200));
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(true);
const pos = entity2.getComponent(PositionComponent);
expect(pos!.x).toBe(100);
expect(pos!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用组件删除变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
entity1.addComponent(new PositionComponent(10, 20));
scene1.createIncrementalSnapshot();
entity1.removeComponentByType(PositionComponent);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
expect(entity2.hasComponent(PositionComponent)).toBe(false);
scene1.end();
scene2.end();
});
it('应该应用组件数据更新', () => {
const scene1 = new Scene({ name: 'Scene1' });
const entity1 = scene1.createEntity('Entity');
const pos1 = new PositionComponent(10, 20);
entity1.addComponent(pos1);
scene1.createIncrementalSnapshot();
pos1.x = 100;
pos1.y = 200;
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
const entity2 = scene2.createEntity('Entity');
Object.defineProperty(entity2, 'id', { value: entity1.id, writable: true });
entity2.addComponent(new PositionComponent(10, 20));
scene2.applyIncremental(incremental);
const pos2 = entity2.getComponent(PositionComponent);
expect(pos2!.x).toBe(100);
expect(pos2!.y).toBe(200);
scene1.end();
scene2.end();
});
it('应该应用场景数据变更', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
scene1.sceneData.set('weather', 'sunny');
scene1.sceneData.set('time', 12.5);
const incremental = scene1.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
expect(scene2.sceneData.get('weather')).toBe('sunny');
expect(scene2.sceneData.get('time')).toBe(12.5);
scene1.end();
scene2.end();
});
});
describe('Incremental Serialization', () => {
it('应该序列化和反序列化增量快照JSON格式', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
const incremental = scene.serializeIncremental();
const json = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
expect(typeof json).toBe('string');
const deserialized = IncrementalSerializer.deserializeIncremental(json);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
});
it('应该序列化和反序列化增量快照(二进制格式)', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 42;
scene.sceneData.set('weather', 'sunny');
const incremental = scene.serializeIncremental();
const binary = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
expect(Buffer.isBuffer(binary)).toBe(true);
const deserialized = IncrementalSerializer.deserializeIncremental(binary);
expect(deserialized.version).toBe(incremental.version);
expect(deserialized.sceneName).toBe(incremental.sceneName);
expect(deserialized.entityChanges.length).toBe(incremental.entityChanges.length);
expect(deserialized.componentChanges.length).toBe(incremental.componentChanges.length);
expect(deserialized.sceneDataChanges.length).toBe(incremental.sceneDataChanges.length);
});
it('应该美化JSON输出', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
const prettyJson = IncrementalSerializer.serializeIncremental(incremental, { format: 'json', pretty: true });
expect(typeof prettyJson).toBe('string');
expect(prettyJson).toContain('\n');
expect(prettyJson).toContain(' ');
});
it('二进制格式应该比JSON格式更小', () => {
const entities = [];
for (let i = 0; i < 50; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i * 10, i * 20));
entity.addComponent(new VelocityComponent());
entities.push(entity);
}
scene.createIncrementalSnapshot();
for (const entity of entities) {
const pos = entity.getComponent(PositionComponent)!;
pos.x += 100;
pos.y += 200;
}
const incremental = scene.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const jsonSize = Buffer.byteLength(jsonData as string);
const binarySize = (binaryData as Buffer).length;
expect(binarySize).toBeLessThan(jsonSize);
});
it('二进制和JSON格式应该包含相同的数据', () => {
scene.createIncrementalSnapshot();
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
entity1.tag = 99;
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
scene.sceneData.set('level', 5);
scene.sceneData.set('score', 1000);
const incremental = scene.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const fromJson = IncrementalSerializer.deserializeIncremental(jsonData);
const fromBinary = IncrementalSerializer.deserializeIncremental(binaryData);
expect(fromJson.version).toBe(fromBinary.version);
expect(fromJson.timestamp).toBe(fromBinary.timestamp);
expect(fromJson.sceneName).toBe(fromBinary.sceneName);
expect(fromJson.entityChanges.length).toBe(fromBinary.entityChanges.length);
expect(fromJson.componentChanges.length).toBe(fromBinary.componentChanges.length);
expect(fromJson.sceneDataChanges.length).toBe(fromBinary.sceneDataChanges.length);
expect(fromJson.entityChanges[0].entityName).toBe(fromBinary.entityChanges[0].entityName);
expect(fromJson.entityChanges[0].entityData?.tag).toBe(fromBinary.entityChanges[0].entityData?.tag);
});
it('应该正确应用二进制格式反序列化的增量快照', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(100, 200));
entity.tag = 77;
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const deserializedIncremental = IncrementalSerializer.deserializeIncremental(binaryData);
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(deserializedIncremental);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(77);
expect(restoredEntity!.hasComponent(PositionComponent)).toBe(true);
const pos = restoredEntity!.getComponent(PositionComponent)!;
expect(pos.x).toBe(100);
expect(pos.y).toBe(200);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持二进制Buffer', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity1 = scene1.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
entity1.addComponent(new VelocityComponent());
const entity2 = scene1.createEntity('Entity2');
entity2.addComponent(new HealthComponent());
const incremental = scene1.serializeIncremental();
const binaryData = IncrementalSerializer.serializeIncremental(incremental, { format: 'binary' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(binaryData);
expect(scene2.entities.count).toBe(2);
const e1 = scene2.findEntity('Entity1');
expect(e1).not.toBeNull();
expect(e1!.hasComponent(PositionComponent)).toBe(true);
expect(e1!.hasComponent(VelocityComponent)).toBe(true);
const e2 = scene2.findEntity('Entity2');
expect(e2).not.toBeNull();
expect(e2!.hasComponent(HealthComponent)).toBe(true);
scene1.end();
scene2.end();
});
it('Scene.applyIncremental应该直接支持JSON字符串', () => {
const scene1 = new Scene({ name: 'Scene1' });
scene1.createIncrementalSnapshot();
const entity = scene1.createEntity('TestEntity');
entity.addComponent(new PositionComponent(50, 100));
entity.tag = 99;
const incremental = scene1.serializeIncremental();
const jsonData = IncrementalSerializer.serializeIncremental(incremental, { format: 'json' });
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(jsonData);
expect(scene2.entities.count).toBe(1);
const restoredEntity = scene2.findEntity('TestEntity');
expect(restoredEntity).not.toBeNull();
expect(restoredEntity!.tag).toBe(99);
scene1.end();
scene2.end();
});
});
describe('Snapshot Management', () => {
it('应该更新增量快照基准', () => {
const entity = scene.createEntity('Entity');
scene.createIncrementalSnapshot();
entity.addComponent(new PositionComponent(10, 20));
const incremental1 = scene.serializeIncremental();
scene.updateIncrementalSnapshot();
const pos = entity.getComponent(PositionComponent)!;
pos.x = 100;
const incremental2 = scene.serializeIncremental();
// incremental2应该只包含Position的更新不包含添加
expect(incremental1.componentChanges.length).toBe(1);
expect(incremental2.componentChanges.length).toBe(1);
expect(incremental2.componentChanges[0].operation).toBe(ChangeOperation.ComponentUpdated);
});
it('应该清除增量快照', () => {
scene.createIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(true);
scene.clearIncrementalSnapshot();
expect(scene.hasIncrementalSnapshot()).toBe(false);
});
it('应该在没有快照时抛出错误', () => {
expect(() => {
scene.serializeIncremental();
}).toThrow('必须先调用 createIncrementalSnapshot() 创建基础快照');
});
});
describe('Statistics and Utilities', () => {
it('应该计算增量快照大小', () => {
scene.createIncrementalSnapshot();
const entity = scene.createEntity('Entity');
entity.addComponent(new PositionComponent(10, 20));
const incremental = scene.serializeIncremental();
const size = IncrementalSerializer.getIncrementalSize(incremental);
expect(size).toBeGreaterThan(0);
});
it('应该提供增量快照统计信息', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
scene.createIncrementalSnapshot();
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new VelocityComponent());
entity1.destroy();
const incremental = scene.serializeIncremental();
const stats = IncrementalSerializer.getIncrementalStats(incremental);
expect(stats.addedEntities).toBe(1);
expect(stats.removedEntities).toBe(1);
expect(stats.addedComponents).toBe(1);
expect(stats.totalChanges).toBeGreaterThan(0);
});
});
describe('Performance and Edge Cases', () => {
it('应该处理大量实体变更', () => {
scene.createIncrementalSnapshot();
for (let i = 0; i < 100; i++) {
const entity = scene.createEntity(`Entity_${i}`);
entity.addComponent(new PositionComponent(i, i * 2));
}
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(100);
expect(incremental.componentChanges.length).toBe(100);
});
it('应该处理空变更', () => {
scene.createIncrementalSnapshot();
const incremental = scene.serializeIncremental();
expect(incremental.entityChanges.length).toBe(0);
expect(incremental.componentChanges.length).toBe(0);
expect(incremental.sceneDataChanges.length).toBe(0);
});
it('应该处理复杂嵌套场景数据', () => {
scene.createIncrementalSnapshot();
scene.sceneData.set('config', {
nested: {
deep: {
value: 42
}
},
array: [1, 2, 3]
});
const incremental = scene.serializeIncremental();
const scene2 = new Scene({ name: 'Scene2' });
scene2.applyIncremental(incremental);
const config = scene2.sceneData.get('config');
expect(config.nested.deep.value).toBe(42);
expect(config.array).toEqual([1, 2, 3]);
scene2.end();
});
it('应该正确处理快照版本号', () => {
IncrementalSerializer.resetVersion();
const snapshot1 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot1.version).toBe(1);
const snapshot2 = IncrementalSerializer.createSnapshot(scene);
expect(snapshot2.version).toBe(2);
});
});
});

View File

@@ -0,0 +1,631 @@
/**
* 序列化系统测试
*/
import { Component } from '../../../src/ECS/Component';
import { Scene } from '../../../src/ECS/Scene';
import { Entity } from '../../../src/ECS/Entity';
import {
Serializable,
Serialize,
SerializeAsMap,
SerializeAsSet,
IgnoreSerialization,
ComponentSerializer,
EntitySerializer,
SceneSerializer,
VersionMigrationManager,
MigrationBuilder
} from '../../../src/ECS/Serialization';
import { ECSComponent } from '../../../src/ECS/Decorators';
import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage';
// 测试组件定义
@ECSComponent('Position')
@Serializable({ version: 1 })
class PositionComponent extends Component {
@Serialize()
public x: number = 0;
@Serialize()
public y: number = 0;
constructor(x: number = 0, y: number = 0) {
super();
this.x = x;
this.y = y;
}
}
@ECSComponent('Velocity')
@Serializable({ version: 1 })
class VelocityComponent extends Component {
@Serialize()
public dx: number = 0;
@Serialize()
public dy: number = 0;
}
@ECSComponent('Player')
@Serializable({ version: 1 })
class PlayerComponent extends Component {
@Serialize()
public name: string = '';
@Serialize()
public level: number = 1;
@SerializeAsMap()
public inventory: Map<string, number> = new Map();
@SerializeAsSet()
public tags: Set<string> = new Set();
@IgnoreSerialization()
public tempCache: any = null;
}
@ECSComponent('Health')
@Serializable({ version: 1 })
class HealthComponent extends Component {
@Serialize()
public current: number = 100;
@Serialize()
public max: number = 100;
}
// 非可序列化组件
class NonSerializableComponent extends Component {
public data: any = null;
}
describe('ECS Serialization System', () => {
beforeEach(() => {
// 清空测试环境
ComponentRegistry.reset();
// 重新注册测试组件因为reset会清空所有注册
ComponentRegistry.register(PositionComponent);
ComponentRegistry.register(VelocityComponent);
ComponentRegistry.register(PlayerComponent);
ComponentRegistry.register(HealthComponent);
});
describe('Component Serialization', () => {
it('should serialize a simple component', () => {
const position = new PositionComponent(100, 200);
const serialized = ComponentSerializer.serialize(position);
expect(serialized).not.toBeNull();
expect(serialized!.type).toBe('Position');
expect(serialized!.version).toBe(1);
expect(serialized!.data.x).toBe(100);
expect(serialized!.data.y).toBe(200);
});
it('should deserialize a simple component', () => {
const serializedData = {
type: 'Position',
version: 1,
data: { x: 150, y: 250 }
};
const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
const component = ComponentSerializer.deserialize(serializedData, registry);
expect(component).not.toBeNull();
expect(component).toBeInstanceOf(PositionComponent);
expect((component as PositionComponent).x).toBe(150);
expect((component as PositionComponent).y).toBe(250);
});
it('should serialize Map fields', () => {
const player = new PlayerComponent();
player.name = 'Hero';
player.level = 5;
player.inventory.set('sword', 1);
player.inventory.set('potion', 10);
const serialized = ComponentSerializer.serialize(player);
expect(serialized).not.toBeNull();
expect(serialized!.data.inventory).toEqual([
['sword', 1],
['potion', 10]
]);
});
it('should deserialize Map fields', () => {
const serializedData = {
type: 'Player',
version: 1,
data: {
name: 'Hero',
level: 5,
inventory: [
['sword', 1],
['potion', 10]
],
tags: ['warrior', 'hero']
}
};
const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
const component = ComponentSerializer.deserialize(
serializedData,
registry
) as PlayerComponent;
expect(component).not.toBeNull();
expect(component.inventory.get('sword')).toBe(1);
expect(component.inventory.get('potion')).toBe(10);
expect(component.tags.has('warrior')).toBe(true);
expect(component.tags.has('hero')).toBe(true);
});
it('should ignore fields marked with @IgnoreSerialization', () => {
const player = new PlayerComponent();
player.tempCache = { foo: 'bar' };
const serialized = ComponentSerializer.serialize(player);
expect(serialized).not.toBeNull();
expect(serialized!.data.tempCache).toBeUndefined();
});
it('should return null for non-serializable components', () => {
const nonSerializable = new NonSerializableComponent();
const serialized = ComponentSerializer.serialize(nonSerializable);
expect(serialized).toBeNull();
});
});
describe('Entity Serialization', () => {
it('should serialize an entity with components', () => {
const entity = new Entity('Player', 1);
entity.addComponent(new PositionComponent(50, 100));
entity.addComponent(new VelocityComponent());
entity.tag = 10;
const serialized = EntitySerializer.serialize(entity);
expect(serialized.id).toBe(1);
expect(serialized.name).toBe('Player');
expect(serialized.tag).toBe(10);
expect(serialized.components.length).toBe(2);
});
it('should serialize entity hierarchy', () => {
const parent = new Entity('Parent', 1);
const child = new Entity('Child', 2);
parent.addComponent(new PositionComponent(0, 0));
child.addComponent(new PositionComponent(10, 10));
parent.addChild(child);
const serialized = EntitySerializer.serialize(parent);
expect(serialized.children.length).toBe(1);
expect(serialized.children[0].id).toBe(2);
expect(serialized.children[0].name).toBe('Child');
});
it('should deserialize an entity', () => {
const serializedEntity = {
id: 1,
name: 'TestEntity',
tag: 5,
active: true,
enabled: true,
updateOrder: 0,
components: [
{
type: 'Position',
version: 1,
data: { x: 100, y: 200 }
}
],
children: []
};
const registry = ComponentRegistry.getAllComponentNames() as Map<string, any>;
let idCounter = 10;
const entity = EntitySerializer.deserialize(
serializedEntity,
registry,
() => idCounter++,
false
);
expect(entity.name).toBe('TestEntity');
expect(entity.tag).toBe(5);
expect(entity.components.length).toBe(1);
});
});
describe('Scene Serialization', () => {
let scene: Scene;
beforeEach(() => {
scene = new Scene({ name: 'TestScene' });
});
afterEach(() => {
scene.end();
});
it('should serialize a scene', () => {
const entity1 = scene.createEntity('Entity1');
entity1.addComponent(new PositionComponent(10, 20));
const entity2 = scene.createEntity('Entity2');
entity2.addComponent(new PlayerComponent());
const saveData = scene.serialize({ format: 'json', pretty: true });
expect(saveData).toBeTruthy();
expect(typeof saveData).toBe('string');
const parsed = JSON.parse(saveData as string);
expect(parsed.name).toBe('TestScene');
expect(parsed.version).toBe(1);
expect(parsed.entities.length).toBe(2);
});
it('should deserialize a scene with replace strategy', () => {
// 创建初始实体
const entity1 = scene.createEntity('Initial');
entity1.addComponent(new PositionComponent(0, 0));
// 序列化
const entity2 = scene.createEntity('ToSave');
entity2.addComponent(new PositionComponent(100, 100));
const saveData = scene.serialize();
// 清空并重新加载
scene.deserialize(saveData, {
strategy: 'replace',
// componentRegistry会自动从ComponentRegistry获取
});
expect(scene.entities.count).toBeGreaterThan(0);
});
it('should filter components during serialization', () => {
const entity = scene.createEntity('Mixed');
entity.addComponent(new PositionComponent(1, 2));
entity.addComponent(new PlayerComponent());
entity.addComponent(new HealthComponent());
const saveData = scene.serialize({
components: [PositionComponent, PlayerComponent],
format: 'json'
});
const parsed = JSON.parse(saveData as string);
expect(parsed.entities.length).toBeGreaterThan(0);
});
it('should preserve entity hierarchy', () => {
const parent = scene.createEntity('Parent');
const child = scene.createEntity('Child');
parent.addChild(child);
parent.addComponent(new PositionComponent(0, 0));
child.addComponent(new PositionComponent(10, 10));
const saveData = scene.serialize({ format: 'json' });
const parsed = JSON.parse(saveData as string);
// 只有父实体在顶层
expect(parsed.entities.length).toBe(1);
expect(parsed.entities[0].children.length).toBe(1);
});
it('should validate save data', () => {
const entity = scene.createEntity('Test');
entity.addComponent(new PositionComponent(5, 5));
const saveData = scene.serialize({ format: 'json' });
const validation = SceneSerializer.validate(saveData as string);
expect(validation.valid).toBe(true);
expect(validation.version).toBe(1);
});
it('should get save data info', () => {
const entity = scene.createEntity('InfoTest');
entity.addComponent(new PositionComponent(1, 1));
const saveData = scene.serialize({ format: 'json' });
const info = SceneSerializer.getInfo(saveData as string);
expect(info).not.toBeNull();
expect(info!.name).toBe('TestScene');
expect(info!.version).toBe(1);
});
});
describe('Version Migration', () => {
@ECSComponent('OldPlayer')
@Serializable({ version: 1 })
class OldPlayerV1 extends Component {
@Serialize()
public name: string = '';
@Serialize()
public hp: number = 100;
}
@ECSComponent('OldPlayer')
@Serializable({ version: 2 })
class OldPlayerV2 extends Component {
@Serialize()
public name: string = '';
@Serialize()
public health: number = 100; // 重命名了字段
@Serialize()
public maxHealth: number = 100; // 新增字段
}
beforeEach(() => {
VersionMigrationManager.clearMigrations();
});
it('should migrate component from v1 to v2', () => {
// 注册迁移
VersionMigrationManager.registerComponentMigration(
'OldPlayer',
1,
2,
(data) => {
return {
name: data.name,
health: data.hp,
maxHealth: data.hp
};
}
);
const v1Data = {
type: 'OldPlayer',
version: 1,
data: { name: 'Hero', hp: 80 }
};
const migrated = VersionMigrationManager.migrateComponent(v1Data, 2);
expect(migrated.version).toBe(2);
expect(migrated.data.health).toBe(80);
expect(migrated.data.maxHealth).toBe(80);
expect(migrated.data.hp).toBeUndefined();
});
it('should use MigrationBuilder for component migration', () => {
new MigrationBuilder()
.forComponent('Player')
.fromVersionToVersion(1, 2)
.migrate((data: any) => {
data.experience = 0;
return data;
});
expect(VersionMigrationManager.canMigrateComponent('Player', 1, 2)).toBe(true);
});
it('should check migration path availability', () => {
VersionMigrationManager.registerComponentMigration('Test', 1, 2, (d) => d);
VersionMigrationManager.registerComponentMigration('Test', 2, 3, (d) => d);
expect(VersionMigrationManager.canMigrateComponent('Test', 1, 3)).toBe(true);
expect(VersionMigrationManager.canMigrateComponent('Test', 1, 4)).toBe(false);
});
it('should get migration path', () => {
VersionMigrationManager.registerComponentMigration('PathTest', 1, 2, (d) => d);
VersionMigrationManager.registerComponentMigration('PathTest', 2, 3, (d) => d);
const path = VersionMigrationManager.getComponentMigrationPath('PathTest');
expect(path).toEqual([1, 2]);
});
});
// ComponentTypeRegistry已被移除现在使用ComponentRegistry自动管理组件类型
describe('Integration Tests', () => {
it('should perform full save/load cycle', () => {
const scene1 = new Scene({ name: 'SaveTest' });
// 创建复杂实体
const player = scene1.createEntity('Player');
const playerComp = new PlayerComponent();
playerComp.name = 'TestHero';
playerComp.level = 10;
playerComp.inventory.set('sword', 1);
playerComp.inventory.set('shield', 1);
playerComp.tags.add('warrior');
player.addComponent(playerComp);
player.addComponent(new PositionComponent(100, 200));
player.addComponent(new HealthComponent());
// 创建子实体
const weapon = scene1.createEntity('Weapon');
weapon.addComponent(new PositionComponent(5, 0));
player.addChild(weapon);
// 序列化
const saveData = scene1.serialize();
// 新场景
const scene2 = new Scene({ name: 'LoadTest' });
// 反序列化
scene2.deserialize(saveData, {
strategy: 'replace',
// componentRegistry会自动从ComponentRegistry获取
});
// 验证
const loadedPlayer = scene2.findEntity('Player');
expect(loadedPlayer).not.toBeNull();
const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent;
expect(loadedPlayerComp).not.toBeNull();
expect(loadedPlayerComp.name).toBe('TestHero');
expect(loadedPlayerComp.level).toBe(10);
expect(loadedPlayerComp.inventory.get('sword')).toBe(1);
expect(loadedPlayerComp.tags.has('warrior')).toBe(true);
// 验证层级结构
expect(loadedPlayer!.childCount).toBe(1);
scene1.end();
scene2.end();
});
it('should serialize and deserialize scene custom data', () => {
const scene1 = new Scene({ name: 'SceneDataTest' });
// 设置场景自定义数据
scene1.sceneData.set('weather', 'rainy');
scene1.sceneData.set('timeOfDay', 14.5);
scene1.sceneData.set('difficulty', 'hard');
scene1.sceneData.set('checkpoint', { x: 100, y: 200 });
scene1.sceneData.set('tags', new Set(['action', 'adventure']));
scene1.sceneData.set('metadata', new Map([['author', 'test'], ['version', '1.0']]));
// 序列化
const saveData = scene1.serialize();
// 新场景
const scene2 = new Scene({ name: 'LoadTest' });
// 反序列化
scene2.deserialize(saveData, {
strategy: 'replace',
// componentRegistry会自动从ComponentRegistry获取
});
// 验证场景数据
expect(scene2.sceneData.get('weather')).toBe('rainy');
expect(scene2.sceneData.get('timeOfDay')).toBe(14.5);
expect(scene2.sceneData.get('difficulty')).toBe('hard');
expect(scene2.sceneData.get('checkpoint')).toEqual({ x: 100, y: 200 });
const tags = scene2.sceneData.get('tags');
expect(tags).toBeInstanceOf(Set);
expect(tags.has('action')).toBe(true);
expect(tags.has('adventure')).toBe(true);
const metadata = scene2.sceneData.get('metadata');
expect(metadata).toBeInstanceOf(Map);
expect(metadata.get('author')).toBe('test');
expect(metadata.get('version')).toBe('1.0');
scene1.end();
scene2.end();
});
it('should serialize and deserialize using binary format', () => {
const scene1 = new Scene({ name: 'BinaryTest' });
// 创建测试数据
const player = scene1.createEntity('Player');
const playerComp = new PlayerComponent();
playerComp.name = 'BinaryHero';
playerComp.level = 5;
playerComp.inventory.set('sword', 1);
player.addComponent(playerComp);
player.addComponent(new PositionComponent(100, 200));
scene1.sceneData.set('weather', 'sunny');
scene1.sceneData.set('score', 9999);
// 二进制序列化
const binaryData = scene1.serialize({ format: 'binary' });
// 验证是Buffer类型
expect(Buffer.isBuffer(binaryData)).toBe(true);
// JSON序列化对比
const jsonData = scene1.serialize({ format: 'json', pretty: false });
// 二进制应该更小
const binarySize = (binaryData as Buffer).length;
const jsonSize = (jsonData as string).length;
console.log(`Binary size: ${binarySize} bytes, JSON size: ${jsonSize} bytes`);
expect(binarySize).toBeLessThan(jsonSize);
// 新场景反序列化二进制数据
const scene2 = new Scene({ name: 'LoadTest' });
scene2.deserialize(binaryData, {
strategy: 'replace',
// componentRegistry会自动从ComponentRegistry获取
});
// 验证数据完整性
const loadedPlayer = scene2.findEntity('Player');
expect(loadedPlayer).not.toBeNull();
const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent;
expect(loadedPlayerComp.name).toBe('BinaryHero');
expect(loadedPlayerComp.level).toBe(5);
expect(loadedPlayerComp.inventory.get('sword')).toBe(1);
const loadedPos = loadedPlayer!.getComponent(PositionComponent as any) as PositionComponent;
expect(loadedPos.x).toBe(100);
expect(loadedPos.y).toBe(200);
expect(scene2.sceneData.get('weather')).toBe('sunny');
expect(scene2.sceneData.get('score')).toBe(9999);
scene1.end();
scene2.end();
});
it('should handle complex nested data in binary format', () => {
const scene1 = new Scene({ name: 'NestedBinaryTest' });
// 复杂嵌套数据
scene1.sceneData.set('config', {
graphics: {
quality: 'high',
resolution: { width: 1920, height: 1080 }
},
audio: {
masterVolume: 0.8,
effects: new Map([['music', 0.7], ['sfx', 0.9]])
},
tags: new Set(['multiplayer', 'ranked']),
timestamp: new Date('2024-01-01')
});
// 二进制序列化
const binaryData = scene1.serialize({ format: 'binary' });
// 反序列化
const scene2 = new Scene({ name: 'LoadTest' });
scene2.deserialize(binaryData, {
// componentRegistry会自动从ComponentRegistry获取
});
const config = scene2.sceneData.get('config');
expect(config.graphics.quality).toBe('high');
expect(config.graphics.resolution.width).toBe(1920);
expect(config.audio.masterVolume).toBe(0.8);
expect(config.audio.effects.get('music')).toBe(0.7);
expect(config.tags.has('multiplayer')).toBe(true);
expect(config.timestamp).toBeInstanceOf(Date);
scene1.end();
scene2.end();
});
});
});

View File

@@ -1,247 +1,183 @@
import {
BitMask64Data,
BitMask64Utils
} from '../../../src/ECS/Utils/BigIntCompatibility';
import { BitMask64Data, BitMask64Utils } from "../../../src";
describe('64位掩码兼容性测试', () => {
describe('基本功能', () => {
it('应该能够创建和检查掩码', () => {
const zero = BitMask64Utils.ZERO;
const mask1 = BitMask64Utils.create(0);
const mask2 = BitMask64Utils.create(5);
expect(BitMask64Utils.isZero(zero)).toBe(true);
expect(BitMask64Utils.isZero(mask1)).toBe(false);
expect(BitMask64Utils.isZero(mask2)).toBe(false);
});
describe("BitMask64Utils 位掩码工具测试", () => {
test("create() 应该在指定索引位置设置位", () => {
const mask = BitMask64Utils.create(0);
expect(mask.base[0]).toBe(1);
expect(mask.base[1]).toBe(0);
it('应该支持数字创建', () => {
const mask = BitMask64Utils.fromNumber(42);
expect(mask.lo).toBe(42);
expect(mask.hi).toBe(0);
});
const mask2 = BitMask64Utils.create(33);
expect(mask2.base[0]).toBe(0);
expect(mask2.base[1]).toBe(0b10);
});
describe('位运算', () => {
let mask1: BitMask64Data;
let mask2: BitMask64Data;
beforeEach(() => {
mask1 = BitMask64Utils.create(2); // 位2
mask2 = BitMask64Utils.create(3); // 位3
});
it('hasAny运算', () => {
const combined = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.orInPlace(combined, mask1);
BitMask64Utils.orInPlace(combined, mask2);
expect(BitMask64Utils.hasAny(combined, mask1)).toBe(true);
expect(BitMask64Utils.hasAny(combined, mask2)).toBe(true);
const mask4 = BitMask64Utils.create(4);
expect(BitMask64Utils.hasAny(combined, mask4)).toBe(false);
});
it('hasAll运算', () => {
const combined = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.orInPlace(combined, mask1);
BitMask64Utils.orInPlace(combined, mask2);
expect(BitMask64Utils.hasAll(combined, mask1)).toBe(true);
expect(BitMask64Utils.hasAll(combined, mask2)).toBe(true);
const both = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.orInPlace(both, mask1);
BitMask64Utils.orInPlace(both, mask2);
expect(BitMask64Utils.hasAll(combined, both)).toBe(true);
});
it('hasNone运算', () => {
const mask4 = BitMask64Utils.create(4);
const mask5 = BitMask64Utils.create(5);
expect(BitMask64Utils.hasNone(mask1, mask2)).toBe(true);
expect(BitMask64Utils.hasNone(mask1, mask4)).toBe(true);
expect(BitMask64Utils.hasNone(mask1, mask1)).toBe(false);
});
it('原地位运算', () => {
const target = BitMask64Utils.clone(mask1);
// OR操作
BitMask64Utils.orInPlace(target, mask2);
expect(BitMask64Utils.hasAll(target, mask1)).toBe(true);
expect(BitMask64Utils.hasAll(target, mask2)).toBe(true);
// AND操作
const andTarget = BitMask64Utils.clone(target);
BitMask64Utils.andInPlace(andTarget, mask1);
expect(BitMask64Utils.hasAll(andTarget, mask1)).toBe(true);
expect(BitMask64Utils.hasAny(andTarget, mask2)).toBe(false);
// XOR操作
const xorTarget = BitMask64Utils.clone(target);
BitMask64Utils.xorInPlace(xorTarget, mask1);
expect(BitMask64Utils.hasAny(xorTarget, mask1)).toBe(false);
expect(BitMask64Utils.hasAll(xorTarget, mask2)).toBe(true);
});
it('设置和清除位', () => {
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.setBit(mask, 5);
expect(BitMask64Utils.hasAny(mask, BitMask64Utils.create(5))).toBe(true);
BitMask64Utils.clearBit(mask, 5);
expect(BitMask64Utils.isZero(mask)).toBe(true);
});
test("fromNumber() 应该把数值放入低32位", () => {
const mask = BitMask64Utils.fromNumber(123456);
expect(mask.base[0]).toBe(123456);
expect(mask.base[1]).toBe(0);
});
describe('字符串表示', () => {
it('二进制字符串', () => {
const mask = BitMask64Utils.create(5); // 位5设置为1
const binaryStr = BitMask64Utils.toString(mask, 2);
expect(binaryStr).toBe('100000'); // 位5为1
});
test("setBit/getBit/clearBit 应该正确设置、读取和清除位", () => {
const mask: BitMask64Data = { base: [0, 0] };
it('十六进制字符串', () => {
const mask = BitMask64Utils.fromNumber(255);
const hexStr = BitMask64Utils.toString(mask, 16);
expect(hexStr).toBe('0xFF');
});
BitMask64Utils.setBit(mask, 5);
expect(BitMask64Utils.getBit(mask, 5)).toBe(true);
it('大数字的十六进制表示', () => {
const mask: BitMask64Data = { lo: 0xFFFFFFFF, hi: 0x12345678 };
const hexStr = BitMask64Utils.toString(mask, 16);
expect(hexStr).toBe('0x12345678FFFFFFFF');
});
BitMask64Utils.clearBit(mask, 5);
expect(BitMask64Utils.getBit(mask, 5)).toBe(false);
// 测试扩展段
BitMask64Utils.setBit(mask, 70);
expect(mask.segments).toBeDefined();
expect(BitMask64Utils.getBit(mask, 70)).toBe(true);
});
describe('位计数', () => {
it('popCount应该正确计算设置位的数量', () => {
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
expect(BitMask64Utils.popCount(mask)).toBe(0);
BitMask64Utils.setBit(mask, 0);
BitMask64Utils.setBit(mask, 2);
BitMask64Utils.setBit(mask, 4);
expect(BitMask64Utils.popCount(mask)).toBe(3);
});
test("hasAny/hasAll/hasNone 判断应正确", () => {
const maskA = BitMask64Utils.create(1);
const maskB = BitMask64Utils.create(1);
const maskC = BitMask64Utils.create(2);
it('大数的popCount', () => {
const mask = BitMask64Utils.fromNumber(0xFF); // 8个1
expect(BitMask64Utils.popCount(mask)).toBe(8);
});
expect(BitMask64Utils.hasAny(maskA, maskB)).toBe(true);
expect(BitMask64Utils.hasAll(maskA, maskB)).toBe(true);
expect(BitMask64Utils.hasNone(maskA, maskC)).toBe(true);
});
describe('ECS组件掩码操作', () => {
it('多组件掩码组合', () => {
const componentMasks: BitMask64Data[] = [];
for (let i = 0; i < 10; i++) {
componentMasks.push(BitMask64Utils.create(i));
}
let combinedMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
for (const mask of componentMasks) {
BitMask64Utils.orInPlace(combinedMask, mask);
}
expect(BitMask64Utils.popCount(combinedMask)).toBe(10);
// 检查所有位都设置了
for (let i = 0; i < 10; i++) {
expect(BitMask64Utils.hasAny(combinedMask, BitMask64Utils.create(i))).toBe(true);
}
});
test("isZero 应正确判断", () => {
const mask = BitMask64Utils.create(3);
expect(BitMask64Utils.isZero(mask)).toBe(false);
it('实体匹配模拟', () => {
// 模拟实体具有组件0, 2, 4
const entityMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.setBit(entityMask, 0);
BitMask64Utils.setBit(entityMask, 2);
BitMask64Utils.setBit(entityMask, 4);
// 查询需要组件0和2
const queryMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.setBit(queryMask, 0);
BitMask64Utils.setBit(queryMask, 2);
expect(BitMask64Utils.hasAll(entityMask, queryMask)).toBe(true);
// 查询需要组件1和3
const queryMask2 = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.setBit(queryMask2, 1);
BitMask64Utils.setBit(queryMask2, 3);
expect(BitMask64Utils.hasAll(entityMask, queryMask2)).toBe(false);
});
BitMask64Utils.clear(mask);
expect(BitMask64Utils.isZero(mask)).toBe(true);
});
describe('边界测试', () => {
it('应该处理64位边界', () => {
expect(() => BitMask64Utils.create(63)).not.toThrow();
expect(() => BitMask64Utils.create(64)).toThrow();
expect(() => BitMask64Utils.create(-1)).toThrow();
});
test("equals 应正确判断两个掩码是否相等", () => {
const mask1 = BitMask64Utils.create(10);
const mask2 = BitMask64Utils.create(10);
const mask3 = BitMask64Utils.create(11);
it('设置和清除边界位', () => {
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.setBit(mask, 63);
expect(BitMask64Utils.hasAny(mask, BitMask64Utils.create(63))).toBe(true);
expect(mask.hi).not.toBe(0);
expect(mask.lo).toBe(0);
BitMask64Utils.clearBit(mask, 63);
expect(BitMask64Utils.isZero(mask)).toBe(true);
});
it('高32位和低32位操作', () => {
const lowMask = BitMask64Utils.create(15); // 低32位
const highMask = BitMask64Utils.create(47); // 高32位
expect(lowMask.hi).toBe(0);
expect(lowMask.lo).not.toBe(0);
expect(highMask.hi).not.toBe(0);
expect(highMask.lo).toBe(0);
const combined = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.orInPlace(combined, lowMask);
BitMask64Utils.orInPlace(combined, highMask);
expect(combined.hi).not.toBe(0);
expect(combined.lo).not.toBe(0);
expect(BitMask64Utils.popCount(combined)).toBe(2);
});
expect(BitMask64Utils.equals(mask1, mask2)).toBe(true);
expect(BitMask64Utils.equals(mask1, mask3)).toBe(false);
});
describe('复制和相等性', () => {
it('clone应该创建独立副本', () => {
const original = BitMask64Utils.create(5);
const cloned = BitMask64Utils.clone(original);
expect(BitMask64Utils.equals(original, cloned)).toBe(true);
BitMask64Utils.setBit(cloned, 6);
expect(BitMask64Utils.equals(original, cloned)).toBe(false);
});
test("orInPlace/andInPlace/xorInPlace 运算应正确", () => {
const mask1 = BitMask64Utils.create(1);
const mask2 = BitMask64Utils.create(2);
it('copy应该正确复制', () => {
const source = BitMask64Utils.create(10);
const target = BitMask64Utils.clone(BitMask64Utils.ZERO);
BitMask64Utils.copy(source, target);
expect(BitMask64Utils.equals(source, target)).toBe(true);
});
BitMask64Utils.orInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 1)).toBe(true);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(true);
it('clear应该清除所有位', () => {
const mask = BitMask64Utils.create(20);
expect(BitMask64Utils.isZero(mask)).toBe(false);
BitMask64Utils.clear(mask);
expect(BitMask64Utils.isZero(mask)).toBe(true);
});
BitMask64Utils.andInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 1)).toBe(false);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(true);
BitMask64Utils.xorInPlace(mask1, mask2);
expect(BitMask64Utils.getBit(mask1, 2)).toBe(false);
});
});
test("copy/clone 应正确复制数据", () => {
const source = BitMask64Utils.create(15);
const target: BitMask64Data = { base: [0, 0] };
BitMask64Utils.copy(source, target);
expect(BitMask64Utils.equals(source, target)).toBe(true);
const clone = BitMask64Utils.clone(source);
expect(BitMask64Utils.equals(source, clone)).toBe(true);
expect(clone).not.toBe(source); // 深拷贝
});
test("越界与非法输入处理", () => {
expect(() => BitMask64Utils.create(-1)).toThrow();
expect(BitMask64Utils.getBit({ base: [0,0] }, -5)).toBe(false);
expect(() => BitMask64Utils.clearBit({ base: [0,0] }, -2)).toThrow();
});
test("大于64位的扩展段逻辑 - hasAny/hasAll/hasNone/equals", () => {
// 掩码 A: 只在 bit 150 位置为 1
const maskA = BitMask64Utils.create(150);
// 掩码 B: 只在 bit 200 位置为 1
const maskB = BitMask64Utils.create(200);
// A 与 B 在不同扩展段,不存在重叠位
expect(BitMask64Utils.hasAny(maskA, maskB)).toBe(false);
expect(BitMask64Utils.hasNone(maskA, maskB)).toBe(true);
// C: 在 150 与 200 都置位
const maskC = BitMask64Utils.clone(maskA);
BitMask64Utils.setBit(maskC, 200);
// A 是 C 的子集
expect(BitMask64Utils.hasAll(maskC, maskA)).toBe(true);
// B 是 C 的子集
expect(BitMask64Utils.hasAll(maskC, maskB)).toBe(true);
// A 和 C 不相等
expect(BitMask64Utils.equals(maskA, maskC)).toBe(false);
// C 与自身相等
expect(BitMask64Utils.equals(maskC, maskC)).toBe(true);
//copy
const copyMask = BitMask64Utils.create(0);
BitMask64Utils.copy(maskA,copyMask);
expect(BitMask64Utils.equals(copyMask,maskA)).toBe(true);
// hasAll短路测试对第一个if的测试覆盖
BitMask64Utils.setBit(copyMask,64);
expect(BitMask64Utils.hasAll(maskA, copyMask)).toBe(false);
BitMask64Utils.clearBit(copyMask, 64);
// 扩展到350位对最后一个短路if的测试覆盖
BitMask64Utils.setBit(copyMask,350);
expect(BitMask64Utils.hasAll(maskA, copyMask)).toBe(false);
});
test("大于64位的逻辑运算 - or/and/xor 跨段处理", () => {
const maskA = BitMask64Utils.create(128); // 第一扩展段
const maskB = BitMask64Utils.create(190); // 同一扩展段但不同位置
const maskC = BitMask64Utils.create(300); // 不同扩展段
// OR: 合并不同扩展段
const orMask = BitMask64Utils.clone(maskA);
BitMask64Utils.orInPlace(orMask, maskC);
expect(BitMask64Utils.getBit(orMask, 128)).toBe(true);
expect(BitMask64Utils.getBit(orMask, 300)).toBe(true);
// AND: 交集为空
const andMask = BitMask64Utils.clone(maskA);
BitMask64Utils.andInPlace(andMask, maskB);
expect(BitMask64Utils.isZero(andMask)).toBe(true);
// XOR: 不同扩展段应该都保留
const xorMask = BitMask64Utils.clone(maskA);
BitMask64Utils.xorInPlace(xorMask, maskC);
expect(BitMask64Utils.getBit(xorMask, 128)).toBe(true);
expect(BitMask64Utils.getBit(xorMask, 300)).toBe(true);
});
test("toString 与 popCount 应该在扩展段正常工作", () => {
const mask = BitMask64Utils.create(0);
BitMask64Utils.setBit(mask, 130); // 扩展段,此时扩展段长度延长到2
BitMask64Utils.setBit(mask, 260); // 再设置另一个超出当前最高段范围更高位,此时扩展段长度延长到3
// 现在应该有三个置位
expect(BitMask64Utils.popCount(mask)).toBe(3);
const strBin = BitMask64Utils.toString(mask, 2);
const strHex = BitMask64Utils.toString(mask, 16);
// 第三个区段应该以100结尾130位为1
expect(strBin.split(' ')[2].endsWith('100')).toBe(true);
// 不存在高位的第三个区段字符串应为0x4
expect(strHex.split(' ')[2]).toBe('0x4');
// 设置第244位为1 这是第四个区段的第(256 - 244 =)12位
BitMask64Utils.setBit(mask, 244);
// 四个区段的在二进制下第12位的字符串应为'1'
expect(BitMask64Utils.toString(mask, 2).split(' ')[3][11]).toBe('1');
// 第四个区段的十六进制下所有字符串应为'0x10000000000000',即二进制的'10000 00000000 00000000 00000000 00000000 00000000 00000000'
expect(BitMask64Utils.toString(mask, 16).split(' ')[3]).toBe('0x10000000000000');
});
});

View File

@@ -0,0 +1,75 @@
// FlatHashMap.test.ts
import { BitMaskHashMap } from "../../../src/ECS/Utils/BitMaskHashMap";
import { BitMask64Data, BitMask64Utils } from "../../../src";
describe("FlatHashMap 基础功能", () => {
test("set/get/has/delete 基本操作", () => {
const map = new BitMaskHashMap<number>();
const keyA = BitMask64Utils.create(5);
const keyB = BitMask64Utils.create(63);
map.set(keyA, 100);
map.set(keyB, 200);
expect(map.size).toBe(2);
expect(map.get(keyA)).toBe(100);
expect(map.get(keyB)).toBe(200);
expect(map.has(keyA)).toBe(true);
map.delete(keyA);
expect(map.has(keyA)).toBe(false);
expect(map.size).toBe(1);
map.clear();
expect(map.size).toBe(0);
});
test("覆盖 set 应该更新 value 而不是新增", () => {
const map = new BitMaskHashMap<string>();
const key = BitMask64Utils.create(10);
map.set(key, "foo");
map.set(key, "bar");
expect(map.size).toBe(1);
expect(map.get(key)).toBe("bar");
});
test("不同 key 产生相同 primaryHash 时应正确区分", () => {
const map = new BitMaskHashMap<number>();
// 伪造两个不同 key理论上可能 hash 冲突
// 为了测试,我们直接用两个高位 bit分段不同
const keyA = BitMask64Utils.create(150);
const keyB = BitMask64Utils.create(300);
map.set(keyA, 111);
map.set(keyB, 222);
expect(map.get(keyA)).toBe(111);
expect(map.get(keyB)).toBe(222);
expect(map.size).toBe(2);
});
test("100000 个掩码连续的 key 不应存在冲突", () => {
const map = new BitMaskHashMap<number>();
const count = 100000;
const mask: BitMask64Data = { base: [0,0] };
for (let i = 0; i < count; i++) {
let temp = i;
// 遍历 i 的二进制表示的每一位
let bitIndex = 0;
while (temp > 0) {
if (temp & 1) {
BitMask64Utils.setBit(mask, bitIndex);
}
temp = temp >>> 1; // 无符号右移一位,检查下一位
bitIndex++;
}
map.set(mask,1);
}
// 预计没有任何冲突,每一个元素都在单独的桶中。
expect(map.innerBuckets.size).toBe(map.size);
});
});

View File

@@ -39,13 +39,12 @@ class TestGlobalSystem {
describe('WorldManager', () => {
let worldManager: WorldManager;
beforeEach(() => {
// 重置单
WorldManager['_instance'] = null;
worldManager = WorldManager.getInstance();
// WorldManager不再是单例直接创建新实
worldManager = new WorldManager();
});
afterEach(() => {
// 清理所有World
if (worldManager) {
@@ -56,33 +55,40 @@ describe('WorldManager', () => {
// 清理定时器
worldManager.destroy();
}
WorldManager['_instance'] = null;
});
describe('单例模式', () => {
test('获取实例应该返回相同的实例', () => {
const instance1 = WorldManager.getInstance();
const instance2 = WorldManager.getInstance();
expect(instance1).toBe(instance2);
describe('实例化', () => {
test('可以创建多个独立的WorldManager实例', () => {
const manager1 = new WorldManager();
const manager2 = new WorldManager();
expect(manager1).not.toBe(manager2);
manager1.createWorld('world1');
expect(manager2.getWorld('world1')).toBeNull();
// 清理
manager1.destroy();
manager2.destroy();
});
test('使用配置创建实例应该正确', () => {
WorldManager['_instance'] = null;
const config: IWorldManagerConfig = {
maxWorlds: 10,
autoCleanup: true,
debug: false
};
const instance = WorldManager.getInstance(config);
const instance = new WorldManager(config);
expect(instance).toBeDefined();
expect(instance).toBe(WorldManager.getInstance());
expect(instance.worldCount).toBe(0);
// 清理
instance.destroy();
});
});
describe('World管理', () => {
test('创建World应该成功', () => {
const world = worldManager.createWorld('test-world');
@@ -115,19 +121,17 @@ describe('WorldManager', () => {
});
test('超出最大World数量应该抛出错误', () => {
WorldManager['_instance'] = null;
const limitedManager = WorldManager.getInstance({ maxWorlds: 2 });
const limitedManager = new WorldManager({ maxWorlds: 2 });
limitedManager.createWorld('world1');
limitedManager.createWorld('world2');
expect(() => {
limitedManager.createWorld('world3');
}).toThrow("已达到最大World数量限制: 2");
// 清理
limitedManager.removeWorld('world1');
limitedManager.removeWorld('world2');
limitedManager.destroy();
});
test('获取World应该正确', () => {
@@ -420,45 +424,41 @@ describe('WorldManager', () => {
});
describe('配置验证', () => {
test('无效的maxWorlds配置应该使用默认值', () => {
WorldManager['_instance'] = null;
test('无效的maxWorlds配置应该按传入值使用', () => {
const invalidConfig: IWorldManagerConfig = {
maxWorlds: -1,
autoCleanup: true,
debug: true
};
expect(() => {
WorldManager.getInstance(invalidConfig);
}).not.toThrow();
const manager = new WorldManager(invalidConfig);
expect(manager.getStats().config.maxWorlds).toBe(-1);
manager.destroy();
});
test('配置更新应该影响后续操作', () => {
WorldManager['_instance'] = null;
test('配置应该正确应用于新实例', () => {
const config: IWorldManagerConfig = {
maxWorlds: 3,
autoCleanup: true,
debug: true
};
const manager = WorldManager.getInstance(config);
const manager = new WorldManager(config);
// 创建到限制数量的World
manager.createWorld('world1');
manager.createWorld('world2');
manager.createWorld('world3');
// 第四个应该失败
expect(() => {
manager.createWorld('world4');
}).toThrow();
// 清理
manager.removeWorld('world1');
manager.removeWorld('world2');
manager.removeWorld('world3');
manager.destroy();
});
});
});

View File

@@ -0,0 +1,145 @@
/**
* Scene查询方法测试
*/
import { Component } from '../src/ECS/Component';
import { Entity } from '../src/ECS/Entity';
import { Scene } from '../src/ECS/Scene';
import { Core } from '../src/Core';
import { ECSComponent } from '../src/ECS/Decorators';
import { EntitySystem } from '../src/ECS/Systems/EntitySystem';
@ECSComponent('Position')
class Position extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx: number = 0, public dy: number = 0) {
super();
}
}
@ECSComponent('Disabled')
class Disabled extends Component {}
describe('Scene查询方法', () => {
let scene: Scene;
beforeEach(() => {
Core.create({ debug: false, enableEntitySystems: true });
scene = new Scene();
scene.initialize();
});
afterEach(() => {
scene.end();
});
describe('基础查询方法', () => {
test('queryAll 查询拥有所有组件的实体', () => {
const e1 = scene.createEntity('E1');
e1.addComponent(new Position(10, 20));
e1.addComponent(new Velocity(1, 2));
const e2 = scene.createEntity('E2');
e2.addComponent(new Position(30, 40));
const result = scene.queryAll(Position, Velocity);
expect(result.entities).toHaveLength(1);
expect(result.entities[0]).toBe(e1);
});
test('queryAny 查询拥有任意组件的实体', () => {
const e1 = scene.createEntity('E1');
e1.addComponent(new Position(10, 20));
const e2 = scene.createEntity('E2');
e2.addComponent(new Velocity(1, 2));
const e3 = scene.createEntity('E3');
e3.addComponent(new Disabled());
const result = scene.queryAny(Position, Velocity);
expect(result.entities).toHaveLength(2);
});
test('queryNone 查询不包含指定组件的实体', () => {
const e1 = scene.createEntity('E1');
e1.addComponent(new Position(10, 20));
const e2 = scene.createEntity('E2');
e2.addComponent(new Position(30, 40));
e2.addComponent(new Disabled());
const result = scene.queryNone(Disabled);
expect(result.entities).toHaveLength(1);
expect(result.entities[0]).toBe(e1);
});
});
describe('TypedQueryBuilder', () => {
test('scene.query() 创建类型安全的查询构建器', () => {
const e1 = scene.createEntity('E1');
e1.addComponent(new Position(10, 20));
e1.addComponent(new Velocity(1, 2));
const e2 = scene.createEntity('E2');
e2.addComponent(new Position(30, 40));
e2.addComponent(new Velocity(3, 4));
e2.addComponent(new Disabled());
// 构建查询
const query = scene.query()
.withAll(Position, Velocity)
.withNone(Disabled);
const matcher = query.buildMatcher();
// 创建System使用这个matcher
class TestSystem extends EntitySystem {
public processedCount = 0;
constructor() {
super(matcher);
}
protected override process(entities: readonly Entity[]): void {
this.processedCount = entities.length;
}
}
const system = new TestSystem();
scene.addSystem(system);
scene.update();
// 应该只处理e1e2被Disabled排除
expect(system.processedCount).toBe(1);
});
test('TypedQueryBuilder 支持复杂查询', () => {
const e1 = scene.createEntity('E1');
e1.addComponent(new Position(10, 20));
e1.tag = 100;
const e2 = scene.createEntity('E2');
e2.addComponent(new Position(30, 40));
e2.tag = 200;
const query = scene.query()
.withAll(Position)
.withTag(100);
const condition = query.getCondition();
expect(condition.all).toContain(Position as any);
expect(condition.tag).toBe(100);
});
});
});

View File

@@ -0,0 +1,205 @@
/**
* TypeScript类型推断测试
*
* 验证组件类型自动推断功能
*/
import { Component } from '../src/ECS/Component';
import { Entity } from '../src/ECS/Entity';
import { Scene } from '../src/ECS/Scene';
import { Core } from '../src/Core';
import { ECSComponent } from '../src/ECS/Decorators';
import { requireComponent, tryGetComponent, getComponents } from '../src/ECS/TypedEntity';
// 测试组件
@ECSComponent('Position')
class Position extends Component {
constructor(public x: number = 0, public y: number = 0) {
super();
}
}
@ECSComponent('Velocity')
class Velocity extends Component {
constructor(public dx: number = 0, public dy: number = 0) {
super();
}
}
@ECSComponent('Health')
class Health extends Component {
constructor(public value: number = 100, public maxValue: number = 100) {
super();
}
}
describe('TypeScript类型推断', () => {
let scene: Scene;
let entity: Entity;
beforeEach(() => {
Core.create({ debug: false, enableEntitySystems: true });
scene = new Scene();
scene.initialize();
entity = scene.createEntity('TestEntity');
});
afterEach(() => {
scene.end();
});
describe('Entity.getComponent 类型推断', () => {
test('getComponent 应该自动推断正确的返回类型', () => {
entity.addComponent(new Position(100, 200));
// 类型推断为 Position | null
const position = entity.getComponent(Position);
// TypeScript应该知道position可能为null
expect(position).not.toBeNull();
// 在null检查后TypeScript应该知道position是Position类型
if (position) {
expect(position.x).toBe(100);
expect(position.y).toBe(200);
// 这些操作应该有完整的类型提示
position.x += 10;
position.y += 20;
expect(position.x).toBe(110);
expect(position.y).toBe(220);
}
});
test('getComponent 返回null时类型安全', () => {
// 实体没有Velocity组件
const velocity = entity.getComponent(Velocity);
// 应该返回null
expect(velocity).toBeNull();
});
test('多个不同类型组件的类型推断', () => {
entity.addComponent(new Position(10, 20));
entity.addComponent(new Velocity(1, 2));
entity.addComponent(new Health(100));
const pos = entity.getComponent(Position);
const vel = entity.getComponent(Velocity);
const health = entity.getComponent(Health);
// 所有组件都应该被正确推断
if (pos && vel && health) {
// Position类型的字段
pos.x = 50;
pos.y = 60;
// Velocity类型的字段
vel.dx = 5;
vel.dy = 10;
// Health类型的字段
health.value = 80;
health.maxValue = 150;
expect(pos.x).toBe(50);
expect(vel.dx).toBe(5);
expect(health.value).toBe(80);
}
});
});
describe('Entity.createComponent 类型推断', () => {
test('createComponent 应该自动推断返回类型', () => {
// 应该推断为Position类型非null
const position = entity.createComponent(Position, 100, 200);
expect(position).toBeInstanceOf(Position);
expect(position.x).toBe(100);
expect(position.y).toBe(200);
// 应该有完整的类型提示
position.x = 300;
expect(position.x).toBe(300);
});
});
describe('Entity.hasComponent 类型守卫', () => {
test('hasComponent 可以用作类型守卫', () => {
entity.addComponent(new Position(10, 20));
if (entity.hasComponent(Position)) {
// 在这个作用域内,我们知道组件存在
const pos = entity.getComponent(Position)!;
pos.x = 100;
expect(pos.x).toBe(100);
}
});
});
describe('Entity.getOrCreateComponent 类型推断', () => {
test('getOrCreateComponent 应该自动推断返回类型', () => {
// 第一次调用:创建新组件
const position1 = entity.getOrCreateComponent(Position, 50, 60);
expect(position1.x).toBe(50);
expect(position1.y).toBe(60);
// 第二次调用:返回已存在的组件
const position2 = entity.getOrCreateComponent(Position, 100, 200);
// 应该是同一个组件
expect(position2).toBe(position1);
expect(position2.x).toBe(50); // 值未改变
});
});
describe('TypedEntity工具函数类型推断', () => {
test('requireComponent 返回非空类型', () => {
entity.addComponent(new Position(100, 200));
// requireComponent 返回非null类型
const position = requireComponent(entity, Position);
// 不需要null检查
expect(position.x).toBe(100);
position.x = 300;
expect(position.x).toBe(300);
});
test('tryGetComponent 返回可选类型', () => {
entity.addComponent(new Position(50, 50));
const position = tryGetComponent(entity, Position);
// 应该返回组件
expect(position).toBeDefined();
if (position) {
expect(position.x).toBe(50);
}
// 不存在的组件返回undefined
const velocity = tryGetComponent(entity, Velocity);
expect(velocity).toBeUndefined();
});
test('getComponents 批量获取组件', () => {
entity.addComponent(new Position(10, 20));
entity.addComponent(new Velocity(1, 2));
entity.addComponent(new Health(100));
const [pos, vel, health] = getComponents(entity, Position, Velocity, Health);
// 应该推断为数组类型
expect(pos).not.toBeNull();
expect(vel).not.toBeNull();
expect(health).not.toBeNull();
if (pos && vel && health) {
expect(pos.x).toBe(10);
expect(vel.dx).toBe(1);
expect(health.value).toBe(100);
}
});
});
});

View File

@@ -137,19 +137,6 @@ describe('NumberExtension - 数字扩展工具类测试', () => {
expect(NumberExtension.toNumber('010')).toBe(10); // 被当作十进制处理
});
it('性能测试 - 大量转换应该高效', () => {
const testValues = [42, '123', true, null, undefined, '3.14'];
const startTime = performance.now();
for (let i = 0; i < 10000; i++) {
testValues.forEach(value => {
NumberExtension.toNumber(value);
});
}
const endTime = performance.now();
expect(endTime - startTime).toBeLessThan(100); // 应该在100ms内完成
});
});
describe('类型兼容性测试', () => {

View File

@@ -120,29 +120,6 @@ describe('IdentifierPool 集成测试', () => {
expect(stats.memoryUsage).toBeLessThan(1000 * 100); // 每个实体少于100字节
});
test('ID回收不应该影响性能', () => {
const entities: Entity[] = [];
const count = 500;
// 创建实体
for (let i = 0; i < count; i++) {
entities.push(scene.createEntity(`RecycleTest_${i}`));
}
// 测试回收性能
const startTime = performance.now();
entities.forEach(entity => entity.destroy());
const endTime = performance.now();
const duration = endTime - startTime;
// 回收500个实体应该在50ms内完成
expect(duration).toBeLessThan(50);
const stats = scene.identifierPool.getStats();
expect(stats.pendingRecycle).toBe(count);
});
});
describe('向后兼容性', () => {