Compare commits
22 Commits
v2.1.51
...
issue-78-组
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf14b59a28 | ||
|
|
0a0f64510f | ||
|
|
9445c735c3 | ||
|
|
7339e7ecec | ||
|
|
79f7c89e23 | ||
|
|
e724e5a1ba | ||
|
|
fdaa94a61d | ||
|
|
6af0074c36 | ||
|
|
97a69fed09 | ||
|
|
959879440d | ||
|
|
fd1bbb0e00 | ||
|
|
072e68cf43 | ||
|
|
610232e6b0 | ||
|
|
69c46f32eb | ||
|
|
06b3f92007 | ||
|
|
c631290049 | ||
|
|
f41c1a3ca3 | ||
|
|
bd6ba84087 | ||
|
|
1512409eb3 | ||
|
|
bcb5feeb1c | ||
|
|
da8b7cf601 | ||
|
|
316527c459 |
@@ -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' }
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -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 应用?
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
### [事件系统 (Event)](./event-system.md)
|
||||
掌握类型安全的事件系统,实现组件间通信和系统协作。
|
||||
|
||||
### [序列化系统 (Serialization)](./serialization.md)
|
||||
掌握场景、实体和组件的序列化方案,支持全量序列化和增量序列化,实现游戏存档、网络同步等功能。
|
||||
|
||||
### [时间和定时器 (Time)](./time-and-timers.md)
|
||||
学习时间管理和定时器系统,实现游戏逻辑的精确时间控制。
|
||||
|
||||
|
||||
@@ -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
810
docs/guide/serialization.md
Normal 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()` - 获取迁移路径
|
||||
|
||||
序列化系统是构建完整游戏的重要基础设施,合理使用可以实现强大的功能,如存档系统、网络同步、关卡编辑器等。
|
||||
483
examples/core-demos/index.html
Normal file
483
examples/core-demos/index.html
Normal 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>
|
||||
@@ -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": {
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
99
examples/core-demos/src/demos/DemoBase.ts
Normal file
99
examples/core-demos/src/demos/DemoBase.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal file
468
examples/core-demos/src/demos/IncrementalSerializationDemo.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal file
386
examples/core-demos/src/demos/SerializationDemo.ts
Normal 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会处理渲染
|
||||
}
|
||||
}
|
||||
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal file
832
examples/core-demos/src/demos/WorkerSystemDemo.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
examples/core-demos/src/demos/index.ts
Normal file
13
examples/core-demos/src/demos/index.ts
Normal 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
|
||||
];
|
||||
171
examples/core-demos/src/main.ts
Normal file
171
examples/core-demos/src/main.ts
Normal 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();
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
15
examples/core-demos/vite.config.ts
Normal file
15
examples/core-demos/vite.config.ts
Normal 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'
|
||||
}
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export { PhysicsWorkerSystem } from './PhysicsWorkerSystem';
|
||||
export { RenderSystem } from './RenderSystem';
|
||||
export { LifetimeSystem } from './LifetimeSystem';
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -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
45
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
404
packages/core/src/ECS/Core/Query/TypedQuery.ts
Normal file
404
packages/core/src/ECS/Core/Query/TypedQuery.ts
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较实体
|
||||
*
|
||||
|
||||
@@ -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>;
|
||||
|
||||
/**
|
||||
* 场景中的实体集合
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
238
packages/core/src/ECS/SceneManager.ts
Normal file
238
packages/core/src/ECS/SceneManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
336
packages/core/src/ECS/Serialization/ComponentSerializer.ts
Normal file
336
packages/core/src/ECS/Serialization/ComponentSerializer.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
223
packages/core/src/ECS/Serialization/EntitySerializer.ts
Normal file
223
packages/core/src/ECS/Serialization/EntitySerializer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
758
packages/core/src/ECS/Serialization/IncrementalSerializer.ts
Normal file
758
packages/core/src/ECS/Serialization/IncrementalSerializer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
535
packages/core/src/ECS/Serialization/SceneSerializer.ts
Normal file
535
packages/core/src/ECS/Serialization/SceneSerializer.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
254
packages/core/src/ECS/Serialization/SerializationDecorators.ts
Normal file
254
packages/core/src/ECS/Serialization/SerializationDecorators.ts
Normal 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;
|
||||
}
|
||||
371
packages/core/src/ECS/Serialization/VersionMigration.ts
Normal file
371
packages/core/src/ECS/Serialization/VersionMigration.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
62
packages/core/src/ECS/Serialization/index.ts
Normal file
62
packages/core/src/ECS/Serialization/index.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
307
packages/core/src/ECS/TypedEntity.ts
Normal file
307
packages/core/src/ECS/TypedEntity.ts
Normal 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);
|
||||
}
|
||||
@@ -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-127,segments[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]不存在,会被转为NaN,NaN的位运算始终返回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]不存在,会被转为NaN,NaN的位运算始终返回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;
|
||||
// 不能假设a,b的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];
|
||||
}
|
||||
}
|
||||
}
|
||||
143
packages/core/src/ECS/Utils/BitMaskHashMap.ts
Normal file
143
packages/core/src/ECS/Utils/BitMaskHashMap.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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';
|
||||
295
packages/core/src/Types/TypeHelpers.ts
Normal file
295
packages/core/src/Types/TypeHelpers.ts
Normal 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>>;
|
||||
@@ -2,6 +2,9 @@
|
||||
* 框架核心类型定义
|
||||
*/
|
||||
|
||||
// 导出TypeScript类型增强工具
|
||||
export * from './TypeHelpers';
|
||||
|
||||
/**
|
||||
* 组件接口
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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('边界情况', () => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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('✅ 性能测试完成');
|
||||
});
|
||||
});
|
||||
631
packages/core/tests/ECS/Core/SoAStorage.complete.test.ts
Normal file
631
packages/core/tests/ECS/Core/SoAStorage.complete.test.ts
Normal 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-45,Number.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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('✅ 向量化操作测试通过');
|
||||
});
|
||||
});
|
||||
@@ -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('✅ 性能测试完成,数据完整性验证通过');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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('✅ 边界情况测试通过');
|
||||
});
|
||||
});
|
||||
512
packages/core/tests/ECS/Core/SystemInitialization.test.ts
Normal file
512
packages/core/tests/ECS/Core/SystemInitialization.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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`);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
631
packages/core/tests/ECS/Serialization/Serialization.test.ts
Normal file
631
packages/core/tests/ECS/Serialization/Serialization.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
75
packages/core/tests/ECS/Utils/BitMaskHashMap.test.ts
Normal file
75
packages/core/tests/ECS/Utils/BitMaskHashMap.test.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
145
packages/core/tests/SceneQuery.test.ts
Normal file
145
packages/core/tests/SceneQuery.test.ts
Normal 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();
|
||||
|
||||
// 应该只处理e1(e2被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);
|
||||
});
|
||||
});
|
||||
});
|
||||
205
packages/core/tests/TypeInference.test.ts
Normal file
205
packages/core/tests/TypeInference.test.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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('类型兼容性测试', () => {
|
||||
|
||||
@@ -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('向后兼容性', () => {
|
||||
|
||||
Reference in New Issue
Block a user