From cac6aedf7810934cb36738844675cd665e1a6cb6 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 14 Oct 2025 22:12:35 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E8=AF=95=E6=8F=92=E4=BB=B6=20DebugPlu?= =?UTF-8?q?gin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/Plugins/DebugPlugin.ts | 359 ++++++++++++++++++ packages/core/src/Plugins/index.ts | 1 + packages/core/src/index.ts | 3 + .../core/tests/Plugins/DebugPlugin.test.ts | 352 +++++++++++++++++ 4 files changed, 715 insertions(+) create mode 100644 packages/core/src/Plugins/DebugPlugin.ts create mode 100644 packages/core/src/Plugins/index.ts create mode 100644 packages/core/tests/Plugins/DebugPlugin.test.ts diff --git a/packages/core/src/Plugins/DebugPlugin.ts b/packages/core/src/Plugins/DebugPlugin.ts new file mode 100644 index 00000000..f729d874 --- /dev/null +++ b/packages/core/src/Plugins/DebugPlugin.ts @@ -0,0 +1,359 @@ +import type { Core } from '../Core'; +import type { ServiceContainer } from '../Core/ServiceContainer'; +import { IPlugin } from '../Core/Plugin'; +import { createLogger } from '../Utils/Logger'; +import type { Scene } from '../ECS/Scene'; +import type { IScene } from '../ECS/IScene'; +import type { Entity } from '../ECS/Entity'; +import type { Component } from '../ECS/Component'; +import type { EntitySystem } from '../ECS/Systems/EntitySystem'; +import { WorldManager } from '../ECS/WorldManager'; +import { Injectable, Inject } from '../Core/DI/Decorators'; +import type { IService } from '../Core/ServiceContainer'; +import type { PerformanceData } from '../Utils/PerformanceMonitor'; + +const logger = createLogger('DebugPlugin'); + +/** + * ECS 调试插件统计信息 + */ +export interface ECSDebugStats { + scenes: SceneDebugInfo[]; + totalEntities: number; + totalSystems: number; + timestamp: number; +} + +/** + * 场景调试信息 + */ +export interface SceneDebugInfo { + name: string; + entityCount: number; + systems: SystemDebugInfo[]; + entities: EntityDebugInfo[]; +} + +/** + * 系统调试信息 + */ +export interface SystemDebugInfo { + name: string; + enabled: boolean; + updateOrder: number; + entityCount: number; + performance?: { + avgExecutionTime: number; + maxExecutionTime: number; + totalCalls: number; + }; +} + +/** + * 实体调试信息 + */ +export interface EntityDebugInfo { + id: number; + name: string; + enabled: boolean; + tag: number; + componentCount: number; + components: ComponentDebugInfo[]; +} + +/** + * 组件调试信息 + */ +export interface ComponentDebugInfo { + type: string; + data: any; +} + +/** + * ECS 调试插件 + * + * 提供运行时调试功能: + * - 实时查看实体和组件信息 + * - System 执行统计 + * - 性能监控 + * - 实体查询 + * + * @example + * ```typescript + * const core = Core.create(); + * const debugPlugin = new DebugPlugin({ autoStart: true, updateInterval: 1000 }); + * await core.pluginManager.install(debugPlugin); + * + * // 获取调试信息 + * const stats = debugPlugin.getStats(); + * console.log('Total entities:', stats.totalEntities); + * + * // 查询实体 + * const entities = debugPlugin.queryEntities({ tag: 1 }); + * ``` + */ +@Injectable() +export class DebugPlugin implements IPlugin, IService { + readonly name = '@esengine/debug-plugin'; + readonly version = '1.0.0'; + + private worldManager: WorldManager | null = null; + private updateInterval: number; + private updateTimer: any = null; + private autoStart: boolean; + + /** + * 创建调试插件实例 + * + * @param options - 配置选项 + */ + constructor(options?: { autoStart?: boolean; updateInterval?: number }) { + this.autoStart = options?.autoStart ?? false; + this.updateInterval = options?.updateInterval ?? 1000; + } + + /** + * 安装插件 + */ + async install(core: Core, services: ServiceContainer): Promise { + this.worldManager = services.resolve(WorldManager); + + logger.info('ECS Debug Plugin installed'); + + if (this.autoStart) { + this.start(); + } + } + + /** + * 卸载插件 + */ + async uninstall(): Promise { + this.stop(); + this.worldManager = null; + + logger.info('ECS Debug Plugin uninstalled'); + } + + /** + * 实现 IService 接口 + */ + public dispose(): void { + this.stop(); + this.worldManager = null; + } + + /** + * 启动调试监控 + */ + public start(): void { + if (this.updateTimer) { + logger.warn('Debug monitoring already started'); + return; + } + + logger.info('Starting debug monitoring'); + + this.updateTimer = setInterval(() => { + this.logStats(); + }, this.updateInterval); + } + + /** + * 停止调试监控 + */ + public stop(): void { + if (this.updateTimer) { + clearInterval(this.updateTimer); + this.updateTimer = null; + logger.info('Debug monitoring stopped'); + } + } + + /** + * 获取当前 ECS 统计信息 + */ + public getStats(): ECSDebugStats { + if (!this.worldManager) { + throw new Error('Plugin not installed'); + } + + const scenes: SceneDebugInfo[] = []; + let totalEntities = 0; + let totalSystems = 0; + + const worlds = this.worldManager.getAllWorlds(); + + for (const world of worlds) { + for (const scene of world.getAllScenes()) { + const sceneInfo = this.getSceneInfo(scene); + scenes.push(sceneInfo); + totalEntities += sceneInfo.entityCount; + totalSystems += sceneInfo.systems.length; + } + } + + return { + scenes, + totalEntities, + totalSystems, + timestamp: Date.now() + }; + } + + /** + * 获取场景调试信息 + */ + public getSceneInfo(scene: IScene): SceneDebugInfo { + const entities = scene.entities.buffer; + const systems = scene.systems; + + return { + name: scene.name, + entityCount: entities.length, + systems: systems.map(sys => this.getSystemInfo(sys)), + entities: entities.map(entity => this.getEntityInfo(entity)) + }; + } + + /** + * 获取系统调试信息 + */ + private getSystemInfo(system: EntitySystem): SystemDebugInfo { + const perfStats = system.getPerformanceStats(); + + return { + name: system.constructor.name, + enabled: system.enabled, + updateOrder: system.updateOrder, + entityCount: system.entities.length, + performance: perfStats ? { + avgExecutionTime: perfStats.averageTime, + maxExecutionTime: perfStats.maxTime, + totalCalls: perfStats.executionCount + } : undefined + }; + } + + /** + * 获取实体调试信息 + */ + public getEntityInfo(entity: Entity): EntityDebugInfo { + const components = entity.components; + + return { + id: entity.id, + name: entity.name, + enabled: entity.enabled, + tag: entity.tag, + componentCount: components.length, + components: components.map(comp => this.getComponentInfo(comp)) + }; + } + + /** + * 获取组件调试信息 + */ + private getComponentInfo(component: any): ComponentDebugInfo { + const type = component.constructor.name; + const data: any = {}; + + for (const key of Object.keys(component)) { + if (!key.startsWith('_')) { + const value = component[key]; + if (typeof value !== 'function') { + data[key] = value; + } + } + } + + return { type, data }; + } + + /** + * 查询实体 + * + * @param filter - 查询过滤器 + */ + public queryEntities(filter: { + sceneId?: string; + tag?: number; + name?: string; + hasComponent?: string; + }): EntityDebugInfo[] { + if (!this.worldManager) { + throw new Error('Plugin not installed'); + } + + const results: EntityDebugInfo[] = []; + const worlds = this.worldManager.getAllWorlds(); + + for (const world of worlds) { + for (const scene of world.getAllScenes()) { + if (filter.sceneId && scene.name !== filter.sceneId) { + continue; + } + + for (const entity of scene.entities.buffer) { + if (filter.tag !== undefined && entity.tag !== filter.tag) { + continue; + } + + if (filter.name && !entity.name.includes(filter.name)) { + continue; + } + + if (filter.hasComponent) { + const hasComp = entity.components.some( + c => c.constructor.name === filter.hasComponent + ); + if (!hasComp) { + continue; + } + } + + results.push(this.getEntityInfo(entity)); + } + } + } + + return results; + } + + /** + * 打印统计信息到日志 + */ + private logStats(): void { + const stats = this.getStats(); + + logger.info('=== ECS Debug Stats ==='); + logger.info(`Total Entities: ${stats.totalEntities}`); + logger.info(`Total Systems: ${stats.totalSystems}`); + logger.info(`Scenes: ${stats.scenes.length}`); + + for (const scene of stats.scenes) { + logger.info(`\n[Scene: ${scene.name}]`); + logger.info(` Entities: ${scene.entityCount}`); + logger.info(` Systems: ${scene.systems.length}`); + + for (const system of scene.systems) { + const perfStr = system.performance + ? ` | Avg: ${system.performance.avgExecutionTime.toFixed(2)}ms, Max: ${system.performance.maxExecutionTime.toFixed(2)}ms` + : ''; + logger.info( + ` - ${system.name} (${system.enabled ? 'enabled' : 'disabled'}) | Entities: ${system.entityCount}${perfStr}` + ); + } + } + + logger.info('========================\n'); + } + + /** + * 导出调试数据为 JSON + */ + public exportJSON(): string { + const stats = this.getStats(); + return JSON.stringify(stats, null, 2); + } +} diff --git a/packages/core/src/Plugins/index.ts b/packages/core/src/Plugins/index.ts new file mode 100644 index 00000000..90bac8a8 --- /dev/null +++ b/packages/core/src/Plugins/index.ts @@ -0,0 +1 @@ +export * from './DebugPlugin'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4d38f37b..2ef87b2d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -13,6 +13,9 @@ export { PluginManager } from './Core/PluginManager'; export { PluginState } from './Core/Plugin'; export type { IPlugin, IPluginMetadata } from './Core/Plugin'; +// 内置插件 +export * from './Plugins'; + // 依赖注入 export { Injectable, diff --git a/packages/core/tests/Plugins/DebugPlugin.test.ts b/packages/core/tests/Plugins/DebugPlugin.test.ts new file mode 100644 index 00000000..baf9ecc1 --- /dev/null +++ b/packages/core/tests/Plugins/DebugPlugin.test.ts @@ -0,0 +1,352 @@ +import { Core } from '../../src/Core'; +import { World } from '../../src/ECS/World'; +import { Scene } from '../../src/ECS/Scene'; +import { Component } from '../../src/ECS/Component'; +import { Matcher } from '../../src/ECS/Utils/Matcher'; +import { DebugPlugin } from '../../src/Plugins/DebugPlugin'; +import { Injectable } from '../../src/Core/DI'; +import { ECSSystem } from '../../src/ECS/Decorators'; +import { EntitySystem } from '../../src/ECS/Systems/EntitySystem'; + +class HealthComponent extends Component { + public health: number = 100; + public maxHealth: number = 100; +} + +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; +} + +@Injectable() +@ECSSystem('TestSystem', { updateOrder: 10 }) +class TestSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(PositionComponent)); + } + + protected override process(entities: readonly import('../../src/ECS/Entity').Entity[]): void { + // 模拟处理逻辑 + } +} + +describe('DebugPlugin', () => { + let core: Core; + let world: World; + let scene: Scene; + let debugPlugin: DebugPlugin; + + beforeEach(() => { + core = Core.create({ debug: false }); + world = Core.worldManager.createWorld('test-world', { name: 'test-world' }); + scene = world.createScene('test-scene'); + world.setSceneActive('test-scene', true); + world.start(); + + debugPlugin = new DebugPlugin({ autoStart: false, updateInterval: 1000 }); + }); + + afterEach(() => { + debugPlugin.stop(); + Core.destroy(); + }); + + describe('基本功能', () => { + it('应该能够安装插件', async () => { + await Core.installPlugin(debugPlugin); + expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(true); + }); + + it('应该能够卸载插件', async () => { + await Core.installPlugin(debugPlugin); + await Core.uninstallPlugin('@esengine/debug-plugin'); + expect(Core.isPluginInstalled('@esengine/debug-plugin')).toBe(false); + }); + + it('应该能够获取插件信息', async () => { + await Core.installPlugin(debugPlugin); + const plugin = Core.getPlugin('@esengine/debug-plugin'); + expect(plugin).toBeDefined(); + expect(plugin?.name).toBe('@esengine/debug-plugin'); + expect(plugin?.version).toBe('1.0.0'); + }); + }); + + describe('统计信息', () => { + beforeEach(async () => { + await Core.installPlugin(debugPlugin); + }); + + it('应该能够获取 ECS 统计信息', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new HealthComponent()); + + const stats = debugPlugin.getStats(); + + expect(stats).toBeDefined(); + expect(stats.totalEntities).toBe(2); + expect(stats.scenes.length).toBe(1); + expect(stats.scenes[0].name).toBe('test-scene'); + expect(stats.scenes[0].entityCount).toBe(2); + }); + + it('应该能够获取场景信息', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent()); + entity.addComponent(new HealthComponent()); + + scene.registerSystems([TestSystem]); + + const sceneInfo = debugPlugin.getSceneInfo(scene); + + expect(sceneInfo.name).toBe('test-scene'); + expect(sceneInfo.entityCount).toBe(1); + expect(sceneInfo.systems.length).toBeGreaterThan(0); + expect(sceneInfo.entities.length).toBe(1); + }); + + it('应该能够获取实体详细信息', () => { + const entity = scene.createEntity('PlayerEntity'); + entity.tag = 1; + entity.addComponent(new PositionComponent()); + entity.addComponent(new HealthComponent()); + + const entityInfo = debugPlugin.getEntityInfo(entity); + + expect(entityInfo.name).toBe('PlayerEntity'); + expect(entityInfo.tag).toBe(1); + expect(entityInfo.enabled).toBe(true); + expect(entityInfo.componentCount).toBe(2); + expect(entityInfo.components.length).toBe(2); + + const componentTypes = entityInfo.components.map(c => c.type); + expect(componentTypes).toContain('PositionComponent'); + expect(componentTypes).toContain('HealthComponent'); + }); + + it('应该能够获取组件数据', () => { + const entity = scene.createEntity('TestEntity'); + const position = new PositionComponent(); + position.x = 100; + position.y = 200; + entity.addComponent(position); + + const entityInfo = debugPlugin.getEntityInfo(entity); + const positionInfo = entityInfo.components.find(c => c.type === 'PositionComponent'); + + expect(positionInfo).toBeDefined(); + expect(positionInfo?.data.x).toBe(100); + expect(positionInfo?.data.y).toBe(200); + }); + }); + + describe('实体查询', () => { + beforeEach(async () => { + await Core.installPlugin(debugPlugin); + + const entity1 = scene.createEntity('Player'); + entity1.tag = 1; + entity1.addComponent(new PositionComponent()); + entity1.addComponent(new HealthComponent()); + + const entity2 = scene.createEntity('Enemy'); + entity2.tag = 2; + entity2.addComponent(new PositionComponent()); + + const entity3 = scene.createEntity('Item'); + entity3.tag = 3; + entity3.addComponent(new HealthComponent()); + }); + + it('应该能够按 tag 查询实体', () => { + const results = debugPlugin.queryEntities({ tag: 1 }); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Player'); + expect(results[0].tag).toBe(1); + }); + + it('应该能够按名称查询实体', () => { + const results = debugPlugin.queryEntities({ name: 'Player' }); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Player'); + }); + + it('应该能够按组件查询实体', () => { + const results = debugPlugin.queryEntities({ hasComponent: 'PositionComponent' }); + + expect(results.length).toBe(2); + expect(results.map(r => r.name)).toContain('Player'); + expect(results.map(r => r.name)).toContain('Enemy'); + }); + + it('应该能够组合多个过滤条件', () => { + const results = debugPlugin.queryEntities({ + tag: 1, + hasComponent: 'HealthComponent' + }); + + expect(results.length).toBe(1); + expect(results[0].name).toBe('Player'); + }); + + it('应该在没有匹配时返回空数组', () => { + const results = debugPlugin.queryEntities({ tag: 999 }); + expect(results.length).toBe(0); + }); + }); + + describe('监控功能', () => { + beforeEach(async () => { + await Core.installPlugin(debugPlugin); + }); + + it('应该能够启动监控', () => { + debugPlugin.start(); + expect(debugPlugin['updateTimer']).not.toBeNull(); + }); + + it('应该能够停止监控', () => { + debugPlugin.start(); + debugPlugin.stop(); + expect(debugPlugin['updateTimer']).toBeNull(); + }); + + it('应该防止重复启动', () => { + debugPlugin.start(); + const timer1 = debugPlugin['updateTimer']; + + debugPlugin.start(); + const timer2 = debugPlugin['updateTimer']; + + expect(timer1).toBe(timer2); + + debugPlugin.stop(); + }); + + it('应该支持自动启动', async () => { + await Core.uninstallPlugin('@esengine/debug-plugin'); + + const autoPlugin = new DebugPlugin({ autoStart: true, updateInterval: 100 }); + await Core.installPlugin(autoPlugin); + + expect(autoPlugin['updateTimer']).not.toBeNull(); + + autoPlugin.stop(); + }); + }); + + describe('数据导出', () => { + beforeEach(async () => { + await Core.installPlugin(debugPlugin); + }); + + it('应该能够导出 JSON 格式数据', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent()); + + const json = debugPlugin.exportJSON(); + + expect(json).toBeDefined(); + expect(typeof json).toBe('string'); + + const data = JSON.parse(json); + expect(data.totalEntities).toBe(1); + expect(data.scenes).toBeDefined(); + expect(data.timestamp).toBeDefined(); + }); + + it('导出的 JSON 应该包含完整的实体信息', () => { + const entity = scene.createEntity('ComplexEntity'); + const position = new PositionComponent(); + position.x = 50; + position.y = 75; + entity.addComponent(position); + + const json = debugPlugin.exportJSON(); + const data = JSON.parse(json); + + const entityData = data.scenes[0].entities[0]; + expect(entityData.name).toBe('ComplexEntity'); + expect(entityData.components[0].data.x).toBe(50); + expect(entityData.components[0].data.y).toBe(75); + }); + }); + + describe('性能监控', () => { + beforeEach(async () => { + await Core.installPlugin(debugPlugin); + scene.registerSystems([TestSystem]); + }); + + it('应该能够获取 System 性能数据', () => { + scene.createEntity('E1').addComponent(new PositionComponent()); + scene.createEntity('E2').addComponent(new PositionComponent()); + + scene.update(); + scene.update(); + scene.update(); + + const sceneInfo = debugPlugin.getSceneInfo(scene); + const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem'); + + expect(systemInfo).toBeDefined(); + if (systemInfo?.performance) { + expect(systemInfo.performance.totalCalls).toBeGreaterThan(0); + expect(systemInfo.performance.avgExecutionTime).toBeGreaterThanOrEqual(0); + } + }); + + it('应该记录 System 的实体数量', () => { + scene.createEntity('E1').addComponent(new PositionComponent()); + scene.createEntity('E2').addComponent(new PositionComponent()); + scene.createEntity('E3').addComponent(new HealthComponent()); + + const sceneInfo = debugPlugin.getSceneInfo(scene); + const systemInfo = sceneInfo.systems.find(s => s.name === 'TestSystem'); + + expect(systemInfo).toBeDefined(); + expect(systemInfo?.entityCount).toBe(2); + }); + }); + + describe('错误处理', () => { + it('应该在未安装时抛出错误', () => { + expect(() => { + debugPlugin.getStats(); + }).toThrow('Plugin not installed'); + }); + + it('应该在未安装时查询实体抛出错误', () => { + expect(() => { + debugPlugin.queryEntities({ tag: 1 }); + }).toThrow('Plugin not installed'); + }); + + it('应该处理空场景', async () => { + await Core.installPlugin(debugPlugin); + + const stats = debugPlugin.getStats(); + + expect(stats.totalEntities).toBe(0); + expect(stats.totalSystems).toBe(0); + }); + + it('应该处理没有 World 的情况', async () => { + Core.destroy(); + Core.create({ debug: false }); + const tempPlugin = new DebugPlugin(); + + await Core.installPlugin(tempPlugin); + + const stats = tempPlugin.getStats(); + + expect(stats.totalEntities).toBe(0); + expect(stats.scenes.length).toBe(0); + }); + }); +});