Files
esengine/packages/core/tests/ECS/WorldManager.test.ts
YHH a716d8006c fix(build): 修复 Web 构建组件注册和用户脚本打包问题 (#302)
* refactor(build): 重构 Web 构建管线,支持配置驱动的 Import Maps

- 重构 WebBuildPipeline 支持 split-bundles 和 single-bundle 两种构建模式
- 使用 module.json 的 isCore 字段识别核心模块,消除硬编码列表
- 动态生成 Import Map,从模块清单的 name 字段获取包名映射
- 动态扫描 module.json 文件,不再依赖固定模块列表
- 添加 HTTP 服务器启动脚本 (start-server.bat/sh) 支持 ESM 模块
- 更新 BuildSettingsPanel UI 支持新的构建模式选项
- 添加多语言支持 (zh/en/es)

* fix(build): 修复 Web 构建组件注册和用户脚本打包问题

主要修复:
- 修复组件反序列化时找不到类型的问题
- @ECSComponent 装饰器现在自动注册到 ComponentRegistry
- 添加未使用装饰器的组件警告
- 构建管线自动扫描用户脚本(无需入口文件)

架构改进:
- 解决 Decorators ↔ ComponentRegistry 循环依赖
- 新建 ComponentTypeUtils.ts 作为底层无依赖模块
- 移除冗余的防御性 register 调用
- 统一 ComponentType 定义位置

* refactor(build): 统一 WASM 配置架构,移除硬编码

- 新增 wasmConfig 统一配置替代 wasmPaths/wasmBindings
- wasmConfig.files 支持多候选源路径和明确目标路径
- wasmConfig.runtimePath 指定运行时加载路径
- 重构 _copyWasmFiles 使用统一配置
- HTML 生成使用配置中的 runtimePath
- 移除 physics-rapier2d 的冗余 WASM 配置(由 rapier2d 负责)
- IBuildFileSystem 新增 deleteFile 方法

* feat(build): 单文件构建模式完善和场景配置驱动

## 主要改动

### 单文件构建(single-file mode)
- 修复 WASM 初始化问题,支持 initSync 同步初始化
- 配置驱动的 WASM 识别,通过 wasmConfig.isEngineCore 标识核心引擎模块
- 从 wasmConfig.files 动态获取 JS 绑定路径,消除硬编码

### 场景配置
- 构建验证:必须选择至少一个场景才能构建
- 自动扫描:项目加载时扫描 scenes 目录
- 抽取 _filterScenesByWhitelist 公共方法统一过滤逻辑

### 构建面板优化
- availableScenes prop 传递场景列表
- 场景复选框可点击切换启用状态
- 移除动态 import,使用 prop 传入数据

* chore(build): 补充构建相关的辅助改动

- 添加 BuildFileSystemService 的 listFilesByExtension 优化
- 更新 module.json 添加 externalDependencies 配置
- BrowserRuntime 支持 wasmModule 参数传递
- GameRuntime 添加 loadSceneFromData 方法
- Rust 构建命令更新
- 国际化文案更新

* feat(build): 持久化构建设置到项目配置

## 设计架构

### ProjectService 扩展
- 新增 BuildSettingsConfig 接口定义构建配置字段
- ProjectConfig 添加 buildSettings 字段
- 新增 getBuildSettings / updateBuildSettings 方法

### BuildSettingsPanel
- 组件挂载时从 projectService 加载已保存配置
- 设置变化时自动保存(500ms 防抖)
- 场景选择状态与项目配置同步

### 配置保存位置
保存在项目的 ecs-editor.config.json 中:
- scenes: 选中的场景列表
- buildMode: 构建模式
- companyName/productName/version: 产品信息
- developmentBuild/sourceMap: 构建选项

* fix(editor): Ctrl+S 仅在主编辑区域触发保存场景

- 模态窗口打开时跳过(构建设置、设置、关于等)
- 焦点在 input/textarea/contenteditable 时跳过

* fix(tests): 修复 ECS 测试中 Component 注册问题

- 为所有测试 Component 类添加 @ECSComponent 装饰器
- 移除 beforeEach 中的 ComponentRegistry.reset() 调用
- 将内联 Component 类移到文件顶层以支持装饰器
- 更新测试预期值匹配新的组件类型名称
- 添加缺失的 HierarchyComponent 导入

所有 1388 个测试现已通过。
2025-12-10 18:23:29 +08:00

718 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { WorldManager, IWorldManagerConfig } from '../../src/ECS/WorldManager';
import { IWorldConfig, World } from '../../src/ECS/World';
import { Component } from '../../src/ECS/Component';
import { ECSComponent } from '../../src/ECS/Decorators';
// 测试用组件
@ECSComponent('WorldMgr_TestComponent')
class TestComponent extends Component {
public value: number = 0;
constructor(value: number = 0) {
super();
this.value = value;
}
}
// 测试用全局系统
class TestGlobalSystem {
public readonly name = 'TestGlobalSystem';
public updateCount: number = 0;
public initialize(): void {
// 初始化
}
public update(): void {
this.updateCount++;
}
public reset(): void {
this.updateCount = 0;
}
public destroy(): void {
// 销毁
}
}
describe('WorldManager', () => {
let worldManager: WorldManager;
beforeEach(() => {
// WorldManager不再是单例直接创建新实例
worldManager = new WorldManager();
});
afterEach(() => {
// 清理所有World
if (worldManager) {
const worldIds = worldManager.getWorldIds();
worldIds.forEach((id) => {
worldManager.removeWorld(id);
});
// 清理定时器
worldManager.destroy();
}
});
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('使用配置创建实例应该正确', () => {
const config: IWorldManagerConfig = {
maxWorlds: 10,
autoCleanup: true,
debug: false
};
const instance = new WorldManager(config);
expect(instance).toBeDefined();
expect(instance.worldCount).toBe(0);
// 清理
instance.destroy();
});
});
describe('World管理', () => {
test('创建World应该成功', () => {
const world = worldManager.createWorld('test-world');
expect(world).toBeDefined();
expect(world.name).toBe('test-world');
expect(worldManager.getWorld('test-world')).toBeDefined();
expect(worldManager.getWorldIds()).toContain('test-world');
});
test('创建World时传入配置应该正确', () => {
const worldConfig: IWorldConfig = {
name: 'ConfiguredWorld',
debug: true,
maxScenes: 5,
autoCleanup: false
};
const world = worldManager.createWorld('configured-world', worldConfig);
expect(world.name).toBe('configured-world');
});
test('空的World name应该抛出错误', () => {
expect(() => {
worldManager.createWorld('');
}).toThrow('World name不能为空');
expect(() => {
worldManager.createWorld(' ');
}).toThrow('World name不能为空');
});
test('重复的World ID应该抛出错误', () => {
worldManager.createWorld('duplicate-world');
expect(() => {
worldManager.createWorld('duplicate-world');
}).toThrow("World name 'duplicate-world' 已存在");
});
test('超出最大World数量应该抛出错误', () => {
const limitedManager = new WorldManager({ maxWorlds: 2 });
limitedManager.createWorld('world1');
limitedManager.createWorld('world2');
expect(() => {
limitedManager.createWorld('world3');
}).toThrow('已达到最大World数量限制: 2');
// 清理
limitedManager.destroy();
});
test('获取World应该正确', () => {
const world = worldManager.createWorld('get-world');
const retrievedWorld = worldManager.getWorld('get-world');
expect(retrievedWorld).toBe(world);
});
test('获取不存在的World应该返回null', () => {
const world = worldManager.getWorld('non-existent');
expect(world).toBeNull();
});
test('检查World存在性应该正确', () => {
expect(worldManager.getWorld('non-existent')).toBeNull();
worldManager.createWorld('exists');
expect(worldManager.getWorld('exists')).toBeDefined();
});
test('销毁World应该正确清理', () => {
const world = worldManager.createWorld('destroy-world');
world.start();
const destroyed = worldManager.removeWorld('destroy-world');
expect(destroyed).toBe(true);
expect(worldManager.getWorld('destroy-world')).toBeNull();
});
test('销毁不存在的World应该返回false', () => {
const destroyed = worldManager.removeWorld('non-existent');
expect(destroyed).toBe(false);
});
test('获取所有World ID应该正确', () => {
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.createWorld('world3');
const worldIds = worldManager.getWorldIds();
expect(worldIds).toHaveLength(3);
expect(worldIds).toContain('world1');
expect(worldIds).toContain('world2');
expect(worldIds).toContain('world3');
});
});
describe('活跃World管理', () => {
test('启动World应该加入活跃列表', () => {
const world = worldManager.createWorld('active-world');
worldManager.setWorldActive('active-world', true);
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(1);
expect(activeWorlds[0]).toBe(world);
});
test('停止World应该从活跃列表移除', () => {
worldManager.createWorld('inactive-world');
worldManager.setWorldActive('inactive-world', true);
worldManager.setWorldActive('inactive-world', false);
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(0);
});
test('销毁激活的World应该从活跃列表移除', () => {
worldManager.createWorld('destroy-active');
worldManager.setWorldActive('destroy-active', true);
worldManager.removeWorld('destroy-active');
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(0);
});
test('多个World的激活状态应该独立管理', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const world3 = worldManager.createWorld('world3');
worldManager.setWorldActive('world1', true);
worldManager.setWorldActive('world3', true);
// world2 保持未启动
const activeWorlds = worldManager.getActiveWorlds();
expect(activeWorlds).toHaveLength(2);
expect(activeWorlds).toContain(world1);
expect(activeWorlds).toContain(world3);
expect(activeWorlds).not.toContain(world2);
});
});
describe('统计和监控', () => {
test('获取WorldManager状态应该正确', () => {
worldManager.createWorld('status-world1');
worldManager.createWorld('status-world2');
worldManager.setWorldActive('status-world2', true);
const status = worldManager.getStats();
expect(status.totalWorlds).toBe(2);
expect(status.activeWorlds).toBe(1);
expect(status.config.maxWorlds).toBeGreaterThan(0);
expect(status.memoryUsage).toBeGreaterThanOrEqual(0);
expect(status.isRunning).toBeDefined();
});
test('获取所有World统计应该包含详细信息', () => {
const world1 = worldManager.createWorld('stats-world1');
worldManager.createWorld('stats-world2');
// 为world1添加一些内容
const scene1 = world1.createScene('scene1');
scene1.createEntity('entity1');
worldManager.setWorldActive('stats-world1', true);
// world2保持空
const allStats = worldManager.getDetailedStatus().worlds;
expect(allStats).toHaveLength(2);
const world1Stats = allStats.find((stat) => stat.id === 'stats-world1');
const world2Stats = allStats.find((stat) => stat.id === 'stats-world2');
expect(world1Stats).toBeDefined();
expect(world2Stats).toBeDefined();
expect(world1Stats?.isActive).toBe(true);
expect(world2Stats?.isActive).toBe(false);
});
test('空WorldManager的统计应该正确', () => {
const status = worldManager.getStats();
const allStats = worldManager.getDetailedStatus().worlds;
expect(status.totalWorlds).toBe(0);
expect(status.activeWorlds).toBe(0);
expect(allStats).toHaveLength(0);
});
});
describe('清理功能', () => {
test('清理空闲World应该移除符合条件的World', () => {
// 创建一个空的World
worldManager.createWorld('empty-world');
// 创建一个有内容的World
const fullWorld = worldManager.createWorld('full-world');
const scene = fullWorld.createScene('scene');
scene.createEntity('entity');
fullWorld.start();
// 执行清理
const cleanedCount = worldManager.cleanup();
// 由于清理逻辑可能基于时间或其他条件,这里主要测试不会出错
expect(cleanedCount).toBeGreaterThanOrEqual(0);
expect(() => worldManager.cleanup()).not.toThrow();
});
test('应该在指定帧数后自动执行清理', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 10 // 10 帧后清理
});
manager.createWorld('test');
// 模拟一个空的老 World (超过 10 分钟)
jest.advanceTimersByTime(11 * 60 * 1000);
// 执行 9 帧,不应该清理
for (let i = 0; i < 9; i++) {
manager.updateAll();
}
expect(manager.getWorld('test')).not.toBeNull();
// 第 10 帧,应该清理
manager.updateAll();
expect(manager.getWorld('test')).toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('autoCleanup=false 时不应该自动清理', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: false
});
manager.createWorld('test');
jest.advanceTimersByTime(20 * 60 * 1000);
// 执行多帧
for (let i = 0; i < 2000; i++) {
manager.updateAll();
}
// 不应该清理
expect(manager.getWorld('test')).not.toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('清理后计数器应该重置', () => {
jest.useFakeTimers();
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 10
});
manager.createWorld('world1');
jest.advanceTimersByTime(11 * 60 * 1000);
// 第 10 帧触发清理
for (let i = 0; i < 10; i++) {
manager.updateAll();
}
// world1 被清理
expect(manager.getWorld('world1')).toBeNull();
// 创建新 World
manager.createWorld('world2');
jest.advanceTimersByTime(11 * 60 * 1000);
// 又要等 10 帧才能清理 (计数器已重置)
for (let i = 0; i < 9; i++) {
manager.updateAll();
}
expect(manager.getWorld('world2')).not.toBeNull();
manager.updateAll();
expect(manager.getWorld('world2')).toBeNull();
jest.useRealTimers();
manager.destroy();
});
test('应该使用配置的 cleanupFrameInterval', () => {
const manager = new WorldManager({
autoCleanup: true,
cleanupFrameInterval: 5
});
expect(manager.config.cleanupFrameInterval).toBe(5);
manager.destroy();
});
});
describe('World更新协调', () => {
test('更新所有活跃World应该正确', () => {
const world1 = worldManager.createWorld('update-world1');
const world2 = worldManager.createWorld('update-world2');
worldManager.createWorld('update-world3');
// 添加一些内容到World中
const scene1 = world1.createScene('scene1');
const scene2 = world2.createScene('scene2');
scene1.createEntity('entity1');
scene2.createEntity('entity2');
// 启动部分World
worldManager.setWorldActive('update-world1', true);
worldManager.setWorldActive('update-world2', true);
// world3保持未启动
// 手动调用更新通常由Core.update()调用)
const activeWorlds = worldManager.getActiveWorlds();
expect(() => {
activeWorlds.forEach((world) => {
world.updateGlobalSystems();
world.updateScenes();
});
}).not.toThrow();
expect(activeWorlds).toHaveLength(2);
});
test('updateAll 应该只更新活跃的 World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const system1 = new TestGlobalSystem();
const system2 = new TestGlobalSystem();
world1.addGlobalSystem(system1);
world2.addGlobalSystem(system2);
worldManager.setWorldActive('world1', true);
// world2 保持未激活
worldManager.updateAll();
expect(system1.updateCount).toBe(1); // world1 被更新
expect(system2.updateCount).toBe(0); // world2 未被更新
});
test('isRunning=false 时 updateAll 不应该更新任何 World', () => {
const world = worldManager.createWorld('world');
const system = new TestGlobalSystem();
world.addGlobalSystem(system);
worldManager.setWorldActive('world', true);
worldManager.stopAll(); // isRunning = false
worldManager.updateAll();
expect(system.updateCount).toBe(0);
});
test('updateAll 应该正确处理多个活跃和非活跃 World', () => {
const worlds: World[] = [];
const systems: TestGlobalSystem[] = [];
// 创建 5 个 World
for (let i = 0; i < 5; i++) {
const world = worldManager.createWorld(`world${i}`);
const system = new TestGlobalSystem();
world.addGlobalSystem(system);
worlds.push(world);
systems.push(system);
}
// 激活第 0, 2, 4 个
worldManager.setWorldActive('world0', true);
worldManager.setWorldActive('world2', true);
worldManager.setWorldActive('world4', true);
worldManager.updateAll();
expect(systems[0].updateCount).toBe(1); // 活跃
expect(systems[1].updateCount).toBe(0); // 非活跃
expect(systems[2].updateCount).toBe(1); // 活跃
expect(systems[3].updateCount).toBe(0); // 非活跃
expect(systems[4].updateCount).toBe(1); // 活跃
});
});
describe('边界情况和错误处理', () => {
test('World ID为空字符串应该抛出错误', () => {
expect(() => {
worldManager.createWorld('');
}).toThrow();
});
test('World ID为null或undefined应该抛出错误', () => {
expect(() => {
worldManager.createWorld(null as unknown as string);
}).toThrow();
expect(() => {
worldManager.createWorld(undefined as unknown as string);
}).toThrow();
});
test('极限情况下的大量World管理', () => {
const worldCount = 50;
const worldIds: string[] = [];
// 创建大量World
for (let i = 0; i < worldCount; i++) {
const worldId = `mass-world-${i}`;
worldIds.push(worldId);
expect(() => {
worldManager.createWorld(worldId);
}).not.toThrow();
}
expect(worldManager.getWorldIds()).toHaveLength(worldCount);
// 启动一半的World
for (let i = 0; i < worldCount / 2; i++) {
worldManager.setWorldActive(worldIds[i], true);
}
expect(worldManager.getActiveWorlds()).toHaveLength(worldCount / 2);
// 批量清理
worldIds.forEach((id) => {
expect(() => {
worldManager.removeWorld(id);
}).not.toThrow();
});
expect(worldManager.getWorldIds()).toHaveLength(0);
});
test('销毁后获取World应该返回null', () => {
worldManager.createWorld('temp-world');
worldManager.removeWorld('temp-world');
expect(worldManager.getWorld('temp-world')).toBeNull();
});
});
describe('内存管理', () => {
test('销毁所有World后内存应该被释放', () => {
// 创建多个World并添加内容
for (let i = 0; i < 10; i++) {
const world = worldManager.createWorld(`memory-world-${i}`);
const scene = world.createScene('scene');
// 添加一些实体和系统
for (let j = 0; j < 5; j++) {
const entity = scene.createEntity(`entity-${j}`);
entity.addComponent(new TestComponent(j));
}
world.addGlobalSystem(new TestGlobalSystem());
worldManager.setWorldActive(`memory-world-${i}`, true);
}
const beforeCleanup = worldManager.getStats();
expect(beforeCleanup.totalWorlds).toBe(10);
expect(beforeCleanup.activeWorlds).toBe(10);
// 清理所有World
const worldIds = worldManager.getWorldIds();
worldIds.forEach((id) => {
worldManager.removeWorld(id);
});
const afterCleanup = worldManager.getStats();
expect(afterCleanup.totalWorlds).toBe(0);
expect(afterCleanup.activeWorlds).toBe(0);
});
});
describe('配置验证', () => {
test('无效的maxWorlds配置应该按传入值使用', () => {
const invalidConfig: IWorldManagerConfig = {
maxWorlds: -1,
autoCleanup: true,
debug: true
};
const manager = new WorldManager(invalidConfig);
expect(manager.getStats().config.maxWorlds).toBe(-1);
manager.destroy();
});
test('配置应该正确应用于新实例', () => {
const config: IWorldManagerConfig = {
maxWorlds: 3,
autoCleanup: true,
debug: true
};
const manager = new WorldManager(config);
// 创建到限制数量的World
manager.createWorld('world1');
manager.createWorld('world2');
manager.createWorld('world3');
// 第四个应该失败
expect(() => {
manager.createWorld('world4');
}).toThrow();
// 清理
manager.destroy();
});
});
describe('isWorldActive()', () => {
test('应该正确返回 World 激活状态', () => {
worldManager.createWorld('test');
// 初始未激活
expect(worldManager.isWorldActive('test')).toBe(false);
// 激活后
worldManager.setWorldActive('test', true);
expect(worldManager.isWorldActive('test')).toBe(true);
// 停用后
worldManager.setWorldActive('test', false);
expect(worldManager.isWorldActive('test')).toBe(false);
});
test('不存在的 World 应该返回 false', () => {
expect(worldManager.isWorldActive('non-existent')).toBe(false);
});
test('应该与 world.isActive 保持一致', () => {
const world = worldManager.createWorld('test');
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
worldManager.setWorldActive('test', true);
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
worldManager.setWorldActive('test', false);
expect(worldManager.isWorldActive('test')).toBe(world.isActive);
});
});
describe('activeWorldCount', () => {
test('应该正确返回激活的 World 数量', () => {
expect(worldManager.activeWorldCount).toBe(0);
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.createWorld('world3');
expect(worldManager.activeWorldCount).toBe(0);
worldManager.setWorldActive('world1', true);
expect(worldManager.activeWorldCount).toBe(1);
worldManager.setWorldActive('world2', true);
expect(worldManager.activeWorldCount).toBe(2);
worldManager.setWorldActive('world1', false);
expect(worldManager.activeWorldCount).toBe(1);
});
test('应该与 getActiveWorlds().length 一致', () => {
worldManager.createWorld('world1');
worldManager.createWorld('world2');
worldManager.setWorldActive('world1', true);
expect(worldManager.activeWorldCount).toBe(worldManager.getActiveWorlds().length);
});
});
describe('getAllWorlds()', () => {
test('应该返回所有 World', () => {
const world1 = worldManager.createWorld('world1');
const world2 = worldManager.createWorld('world2');
const world3 = worldManager.createWorld('world3');
const allWorlds = worldManager.getAllWorlds();
expect(allWorlds).toHaveLength(3);
expect(allWorlds).toContain(world1);
expect(allWorlds).toContain(world2);
expect(allWorlds).toContain(world3);
});
test('空 WorldManager 应该返回空数组', () => {
const allWorlds = worldManager.getAllWorlds();
expect(allWorlds).toHaveLength(0);
});
test('返回的数组修改不应该影响内部状态', () => {
worldManager.createWorld('world1');
const allWorlds = worldManager.getAllWorlds();
allWorlds.push({} as unknown as World);
expect(worldManager.getAllWorlds()).toHaveLength(1);
});
});
});