From b82891caeea7cc0e74b3fa9a53a5f64ea33b49db Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Sun, 28 Sep 2025 23:58:43 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A3=85=E9=A5=B0=E5=99=A8=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/ECS/Core/EventBus.ts | 91 ++++- packages/core/src/ECS/Systems/EntitySystem.ts | 56 ++- .../tests/ECS/Systems/EntitySystem.test.ts | 326 ++++++++++++++++++ 3 files changed, 456 insertions(+), 17 deletions(-) create mode 100644 packages/core/tests/ECS/Systems/EntitySystem.test.ts diff --git a/packages/core/src/ECS/Core/EventBus.ts b/packages/core/src/ECS/Core/EventBus.ts index 3382678b..1eaf1ec5 100644 --- a/packages/core/src/ECS/Core/EventBus.ts +++ b/packages/core/src/ECS/Core/EventBus.ts @@ -469,39 +469,114 @@ export class GlobalEventBus { /** * 事件装饰器工厂 - * 用于自动注册事件监听器 + * 用于自动注册事件监听器,支持自动清理 */ export function EventHandler(eventType: string, config: IEventListenerConfig = {}) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; - + + // 存储装饰器信息 + if (!target.constructor._eventHandlers) { + target.constructor._eventHandlers = []; + } + target.constructor._eventHandlers.push({ + eventType, + methodName: propertyKey, + config + }); + // 在类实例化时自动注册监听器 const initMethod = target.constructor.prototype.initEventListeners || function() {}; target.constructor.prototype.initEventListeners = function() { initMethod.call(this); + + // 初始化监听器追踪数组 + if (!this._decoratorEventListeners) { + this._decoratorEventListeners = []; + } + const eventBus = GlobalEventBus.getInstance(); - eventBus.on(eventType, originalMethod.bind(this), config); + const listenerId = eventBus.on(eventType, originalMethod.bind(this), config); + + // 保存监听器ID用于后续清理 + this._decoratorEventListeners.push({ + eventType, + methodName: propertyKey, + listenerId + }); }; - + + // 添加清理方法 + const cleanupMethod = target.constructor.prototype.cleanupEventListeners || function() {}; + target.constructor.prototype.cleanupEventListeners = function() { + cleanupMethod.call(this); + + if (this._decoratorEventListeners) { + const eventBus = GlobalEventBus.getInstance(); + for (const listener of this._decoratorEventListeners) { + eventBus.off(listener.eventType, listener.listenerId); + } + this._decoratorEventListeners.length = 0; + } + }; + return descriptor; }; } /** * 异步事件装饰器工厂 - * 用于自动注册异步事件监听器 + * 用于自动注册异步事件监听器,支持自动清理 */ export function AsyncEventHandler(eventType: string, config: IEventListenerConfig = {}) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; - + + // 存储装饰器信息 + if (!target.constructor._eventHandlers) { + target.constructor._eventHandlers = []; + } + target.constructor._eventHandlers.push({ + eventType, + methodName: propertyKey, + config, + async: true + }); + const initMethod = target.constructor.prototype.initEventListeners || function() {}; target.constructor.prototype.initEventListeners = function() { initMethod.call(this); + + // 初始化监听器追踪数组 + if (!this._decoratorEventListeners) { + this._decoratorEventListeners = []; + } + const eventBus = GlobalEventBus.getInstance(); - eventBus.onAsync(eventType, originalMethod.bind(this), config); + const listenerId = eventBus.onAsync(eventType, originalMethod.bind(this), config); + + // 保存监听器ID用于后续清理 + this._decoratorEventListeners.push({ + eventType, + methodName: propertyKey, + listenerId + }); }; - + + // 添加清理方法 + const cleanupMethod = target.constructor.prototype.cleanupEventListeners || function() {}; + target.constructor.prototype.cleanupEventListeners = function() { + cleanupMethod.call(this); + + if (this._decoratorEventListeners) { + const eventBus = GlobalEventBus.getInstance(); + for (const listener of this._decoratorEventListeners) { + eventBus.off(listener.eventType, listener.listenerId); + } + this._decoratorEventListeners.length = 0; + } + }; + return descriptor; }; } \ No newline at end of file diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index a789f0d8..8354882f 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -18,6 +18,7 @@ interface EventListenerRecord { listenerRef: string; } + /** * 实体系统的基类 * @@ -52,6 +53,10 @@ export abstract class EntitySystem implements ISystemBase { private _scene: Scene | null; protected logger = createLogger('EntitySystem'); + // 装饰器动态添加的方法(可选) + protected initEventListeners?: () => void; + protected cleanupEventListeners?: () => void; + /** * 实体ID映射缓存 */ @@ -134,6 +139,9 @@ export abstract class EntitySystem implements ISystemBase { this._entityIdMapVersion = -1; this._entityIdMapSize = 0; + // 初始化装饰器事件监听器 + this.initDecoratorEventListeners(); + this._entityCache = { frame: null, persistent: null, @@ -238,10 +246,8 @@ export abstract class EntitySystem implements ISystemBase { this._entityIdMapSize = 0; // 清理所有事件监听器 - this.cleanupEventListeners(); - - // 调用用户可重写的销毁方法 - this.onDestroy(); + // 调用框架销毁方法 + this.destroy(); } /** @@ -746,12 +752,29 @@ export abstract class EntitySystem implements ISystemBase { } } + /** - * 清理所有事件监听器 - * - * 系统移除时自动调用,清理所有通过addEventListener添加的监听器。 + * 初始化装饰器事件监听器 */ - private cleanupEventListeners(): void { + protected initDecoratorEventListeners(): void { + if (this.initEventListeners) { + this.initEventListeners(); + } + } + + /** + * 清理装饰器事件监听器 + */ + protected cleanupDecoratorEventListeners(): void { + if (this.cleanupEventListeners) { + this.cleanupEventListeners(); + } + } + + /** + * 清理手动添加的事件监听器 + */ + private cleanupManualEventListeners(): void { for (const listener of this._eventListeners) { try { listener.eventSystem.off(listener.eventType, listener.listenerRef); @@ -765,7 +788,22 @@ export abstract class EntitySystem implements ISystemBase { } /** - * 系统销毁时的回调 + * 框架内部销毁方法 + * 由框架调用,处理系统的完整销毁流程 + */ + public destroy(): void { + // 1. 清理手动添加的事件监听器 + this.cleanupManualEventListeners(); + + // 2. 清理装饰器事件监听器 + this.cleanupDecoratorEventListeners(); + + // 3. 调用用户的销毁回调 + this.onDestroy(); + } + + /** + * 用户销毁回调 * * 当系统从场景中移除时调用,子类可以重写此方法进行清理操作。 * 注意:事件监听器会被框架自动清理,无需手动处理。 diff --git a/packages/core/tests/ECS/Systems/EntitySystem.test.ts b/packages/core/tests/ECS/Systems/EntitySystem.test.ts new file mode 100644 index 00000000..5efdb93c --- /dev/null +++ b/packages/core/tests/ECS/Systems/EntitySystem.test.ts @@ -0,0 +1,326 @@ +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { Scene } from '../../../src/ECS/Scene'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { EventHandler, AsyncEventHandler, GlobalEventBus } from '../../../src/ECS/Core/EventBus'; +import { TypeSafeEventSystem } from '../../../src/ECS/Core/EventSystem'; + +// 测试组件 +class TestComponent extends Component { + public value: number = 0; + + constructor(...args: unknown[]) { + super(); + const [value = 0] = args as [number?]; + this.value = value; + } +} + +// 测试事件 +interface TestEvent { + id: number; + message: string; +} + +interface AsyncTestEvent { + data: string; + timestamp: number; +} + +// 具体的实体系统实现 +class ConcreteEntitySystem extends EntitySystem { + public processCallCount = 0; + public eventHandlerCallCount = 0; + public asyncEventHandlerCallCount = 0; + public lastEvent: TestEvent | null = null; + public lastAsyncEvent: AsyncTestEvent | null = null; + + constructor() { + super(Matcher.all(TestComponent)); + } + + protected override process(entities: readonly Entity[]): void { + this.processCallCount++; + } + + @EventHandler('test_event') + onTestEvent(event: TestEvent): void { + this.eventHandlerCallCount++; + this.lastEvent = event; + } + + @AsyncEventHandler('async_test_event') + async onAsyncTestEvent(event: AsyncTestEvent): Promise { + this.asyncEventHandlerCallCount++; + this.lastAsyncEvent = event; + // 模拟异步操作 + await new Promise(resolve => setTimeout(resolve, 10)); + } + + // 测试用的手动事件监听器 + public addManualEventListener(): void { + this.addEventListener('manual_event', (event: any) => { + this.eventHandlerCallCount++; + }); + } + + // 测试用的移除事件监听器 + public removeManualEventListener(): void { + const handler = (event: any) => { + this.eventHandlerCallCount++; + }; + this.addEventListener('manual_event', handler); + this.removeEventListener('manual_event', handler); + } + + // 公开测试方法 + public testAddEventListener(eventType: string, handler: (event: any) => void): void { + this.addEventListener(eventType, handler); + } + + public testRemoveEventListener(eventType: string, handler: (event: any) => void): void { + this.removeEventListener(eventType, handler); + } +} + +// 没有装饰器的系统(用于对比测试) +class PlainEntitySystem extends EntitySystem { + public processCallCount = 0; + public onDestroyCallCount = 0; + + constructor() { + super(Matcher.all(TestComponent)); + } + + protected override process(entities: readonly Entity[]): void { + this.processCallCount++; + } + + protected override onDestroy(): void { + this.onDestroyCallCount++; + } +} + +describe('EntitySystem', () => { + let scene: Scene; + let system: ConcreteEntitySystem; + let entity: Entity; + + beforeEach(() => { + scene = new Scene(); + system = new ConcreteEntitySystem(); + entity = new Entity('test_entity', 1); + entity.addComponent(new TestComponent(10)); + + scene.addEntity(entity); + scene.addSystem(system); + }); + + afterEach(() => { + // 清理场景 + scene.removeSystem(system); + }); + + describe('装饰器事件处理', () => { + it('应该自动注册 @EventHandler 装饰器标记的方法', () => { + const testEvent: TestEvent = { id: 1, message: 'test' }; + + // 发射事件(使用全局事件总线,因为装饰器注册在那里) + GlobalEventBus.getInstance().emit('test_event', testEvent); + + // 验证事件处理器被调用 + expect(system.eventHandlerCallCount).toBe(1); + expect(system.lastEvent).toEqual(testEvent); + }); + + it('应该自动注册 @AsyncEventHandler 装饰器标记的方法', async () => { + const asyncEvent: AsyncTestEvent = { + data: 'async test', + timestamp: Date.now() + }; + + // 发射异步事件(使用全局事件总线) + await GlobalEventBus.getInstance().emitAsync('async_test_event', asyncEvent); + + // 等待异步处理完成 + await new Promise(resolve => setTimeout(resolve, 20)); + + // 验证异步事件处理器被调用 + expect(system.asyncEventHandlerCallCount).toBe(1); + expect(system.lastAsyncEvent).toEqual(asyncEvent); + }); + + it('当系统被销毁时,应该自动清理装饰器事件监听器', () => { + const testEvent: TestEvent = { id: 2, message: 'test after destroy' }; + + // 验证事件监听器工作正常 + GlobalEventBus.getInstance().emit('test_event', testEvent); + expect(system.eventHandlerCallCount).toBe(1); + + // 销毁系统 + scene.removeSystem(system); + + // 重置计数器并再次发射事件 + system.eventHandlerCallCount = 0; + GlobalEventBus.getInstance().emit('test_event', testEvent); + + // 验证事件监听器已被清理,不再响应事件 + expect(system.eventHandlerCallCount).toBe(0); + }); + }); + + describe('手动事件监听器管理', () => { + it('应该能够手动添加事件监听器', () => { + system.addManualEventListener(); + + // 发射手动事件 + scene.eventSystem.emitSync('manual_event', { test: true }); + + // 验证手动事件监听器被调用 + expect(system.eventHandlerCallCount).toBe(1); + }); + + it('应该能够手动移除事件监听器', () => { + const handler = jest.fn(); + + // 添加监听器 + system.testAddEventListener('manual_remove_event', handler); + + // 发射事件验证监听器工作 + scene.eventSystem.emitSync('manual_remove_event', {}); + expect(handler).toHaveBeenCalledTimes(1); + + // 移除监听器 + system.testRemoveEventListener('manual_remove_event', handler); + + // 再次发射事件 + scene.eventSystem.emitSync('manual_remove_event', {}); + + // 验证监听器已被移除 + expect(handler).toHaveBeenCalledTimes(1); + }); + + it('当系统被销毁时,应该清理手动添加的事件监听器', () => { + const handler = jest.fn(); + + // 添加手动监听器 + system.testAddEventListener('destroy_test_event', handler); + + // 验证监听器工作 + scene.eventSystem.emitSync('destroy_test_event', {}); + expect(handler).toHaveBeenCalledTimes(1); + + // 销毁系统 + scene.removeSystem(system); + + // 再次发射事件 + scene.eventSystem.emitSync('destroy_test_event', {}); + + // 验证监听器已被清理 + expect(handler).toHaveBeenCalledTimes(1); + }); + }); + + describe('生命周期管理', () => { + it('应该按正确的顺序执行销毁流程', () => { + const plainSystem = new PlainEntitySystem(); + scene.addSystem(plainSystem); + + // 销毁系统 + scene.removeSystem(plainSystem); + + // 验证用户的 onDestroy 被调用 + expect(plainSystem.onDestroyCallCount).toBe(1); + }); + + it('框架的 destroy 方法应该清理所有资源并调用用户回调', () => { + const testEvent: TestEvent = { id: 3, message: 'destroy test' }; + const handler = jest.fn(); + + // 添加手动监听器 + system.testAddEventListener('destroy_order_test', handler); + + // 验证装饰器和手动监听器都工作 + GlobalEventBus.getInstance().emit('test_event', testEvent); + scene.eventSystem.emitSync('destroy_order_test', {}); + expect(system.eventHandlerCallCount).toBe(1); + expect(handler).toHaveBeenCalledTimes(1); + + // 直接调用框架的销毁方法 + system.destroy(); + + // 重置计数器并验证所有监听器都被清理 + system.eventHandlerCallCount = 0; + handler.mockClear(); + + GlobalEventBus.getInstance().emit('test_event', testEvent); + scene.eventSystem.emitSync('destroy_order_test', {}); + + expect(system.eventHandlerCallCount).toBe(0); + expect(handler).not.toHaveBeenCalled(); + }); + }); + + describe('错误处理', () => { + it('当事件系统不可用时,应该安全地处理监听器注册', () => { + // 创建没有事件系统的场景 + const sceneWithoutEvents = new Scene(); + (sceneWithoutEvents as any).eventSystem = null; + + const systemWithoutEvents = new ConcreteEntitySystem(); + + // 应该不会抛出错误 + expect(() => { + sceneWithoutEvents.addSystem(systemWithoutEvents); + }).not.toThrow(); + }); + + it('当移除不存在的监听器时,应该安全处理', () => { + const nonExistentHandler = () => {}; + + // 应该不会抛出错误 + expect(() => { + system.testRemoveEventListener('non_existent_event', nonExistentHandler); + }).not.toThrow(); + }); + }); + + describe('多个事件监听器', () => { + it('应该支持同一个系统监听多个不同的事件', () => { + const event1: TestEvent = { id: 1, message: 'first' }; + const event2: AsyncTestEvent = { data: 'second', timestamp: Date.now() }; + + // 发射两个不同的事件 + GlobalEventBus.getInstance().emit('test_event', event1); + GlobalEventBus.getInstance().emitAsync('async_test_event', event2); + + // 等待异步处理 + return new Promise(resolve => { + setTimeout(() => { + expect(system.eventHandlerCallCount).toBe(1); + expect(system.asyncEventHandlerCallCount).toBe(1); + expect(system.lastEvent).toEqual(event1); + expect(system.lastAsyncEvent).toEqual(event2); + resolve(); + }, 50); + }); + }); + + it('应该支持多个系统监听同一个事件', () => { + const system2 = new ConcreteEntitySystem(); + scene.addSystem(system2); + + const testEvent: TestEvent = { id: 4, message: 'shared event' }; + + // 发射事件 + GlobalEventBus.getInstance().emit('test_event', testEvent); + + // 验证两个系统都接收到了事件 + expect(system.eventHandlerCallCount).toBe(1); + expect(system2.eventHandlerCallCount).toBe(1); + expect(system.lastEvent).toEqual(testEvent); + expect(system2.lastEvent).toEqual(testEvent); + }); + }); +}); \ No newline at end of file