From e2b316b3cc7c684f43e545ddb0b25df73e5a9aff Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Fri, 14 Nov 2025 12:10:59 +0800 Subject: [PATCH] Fix/entity system dispose ondestroy (#223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(core): 修复 EntitySystem dispose 未调用 onDestroy 导致资源泄漏 * fix(core): 修复 Scene.end() 中 unload 调用时机导致用户无法清理资源 --- packages/core/src/ECS/Scene.ts | 17 ++++++-- packages/core/src/ECS/Systems/EntitySystem.ts | 31 ++++++++++++-- .../tests/ECS/Systems/EntitySystem.test.ts | 41 +++++++++++++++++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index a927656c..b85b1fe9 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -294,11 +294,24 @@ export class Scene implements IScene { * 结束场景,清除实体、实体处理器等 * * 这个方法会结束场景。它将移除所有实体,结束实体处理器等,并调用unload方法。 + * + * 执行顺序: + * 1. 调用 unload() - 用户可以在此访问实体和系统进行清理 + * 2. 清理所有实体 + * 3. 清空服务容器,触发所有系统的 onDestroy() + * + * 注意: + * - onRemoved 回调不会在 Scene.end() 时触发,因为这是批量销毁场景 + * - 用户清理:在 Scene.unload() 中处理(可访问实体和系统) + * - 系统清理:在 System.onDestroy() 中处理(实体已被清理) */ public end() { // 标记场景已结束运行 this._didSceneBegin = false; + // 先调用用户的卸载方法,此时用户可以访问实体和系统进行清理 + this.unload(); + // 移除所有实体 this.entities.removeAllEntities(); @@ -309,14 +322,12 @@ export class Scene implements IScene { this.componentStorageManager.clear(); // 清空服务容器(会调用所有服务的dispose方法,包括所有EntitySystem) + // 系统的 onDestroy 回调会在这里被触发 this._services.clear(); // 清空系统缓存 this._cachedSystems = null; this._systemsOrderDirty = true; - - // 调用卸载方法 - this.unload(); } /** diff --git a/packages/core/src/ECS/Systems/EntitySystem.ts b/packages/core/src/ECS/Systems/EntitySystem.ts index baa4faf2..a1946428 100644 --- a/packages/core/src/ECS/Systems/EntitySystem.ts +++ b/packages/core/src/ECS/Systems/EntitySystem.ts @@ -73,6 +73,7 @@ export abstract class EntitySystem implements ISystemBase, IService { private _matcher: Matcher; private _eventListeners: EventListenerRecord[]; private _scene: Scene | null; + private _destroyed: boolean; protected logger: ReturnType; /** @@ -145,6 +146,7 @@ export abstract class EntitySystem implements ISystemBase, IService { this._matcher = matcher || Matcher.empty(); this._eventListeners = []; this._scene = null; + this._destroyed = false; this._entityIdMap = null; this._entityIdMapVersion = -1; @@ -247,8 +249,17 @@ export abstract class EntitySystem implements ISystemBase, IService { * 重置系统状态 * * 当系统从场景中移除时调用,重置初始化状态以便重新添加时能正确初始化。 + * + * 注意:此方法由 Scene.removeEntityProcessor 调用,在 unregister(触发dispose)之后调用。 + * dispose 已经调用了 onDestroy 并设置了 _destroyed 标志,所以这里不需要重置该标志。 + * 重置 _destroyed 会违反服务容器的语义(dispose 后不应重用)。 */ public reset(): void { + // 如果系统已经被销毁,不需要再次调用destroy + if (this._destroyed) { + return; + } + this.scene = null; this._initialized = false; this._entityCache.clearAll(); @@ -257,8 +268,7 @@ export abstract class EntitySystem implements ISystemBase, IService { this._entityIdMap = null; this._entityIdMapVersion = -1; - // 清理所有事件监听器 - // 调用框架销毁方法 + // 清理所有事件监听器并调用销毁回调 this.destroy(); } @@ -728,15 +738,24 @@ export abstract class EntitySystem implements ISystemBase, IService { * * 默认行为: * - 移除所有事件监听器 + * - 调用 onDestroy 回调(仅首次) * - 清空所有缓存 * - 重置初始化状态 * * 子类可以重写此方法来清理自定义资源,但应该调用super.dispose()。 */ public dispose(): void { + // 防止重复销毁 + if (this._destroyed) { + return; + } + // 移除所有事件监听器 this.cleanupManualEventListeners(); + // 调用用户销毁回调 + this.onDestroy(); + // 清空所有缓存 this._entityCache.clearAll(); this._entityIdMap = null; @@ -744,6 +763,7 @@ export abstract class EntitySystem implements ISystemBase, IService { // 重置状态 this._initialized = false; this._scene = null; + this._destroyed = true; this.logger.debug(`System ${this._systemName} disposed`); } @@ -827,8 +847,13 @@ export abstract class EntitySystem implements ISystemBase, IService { * 由框架调用,处理系统的完整销毁流程 */ public destroy(): void { - this.cleanupManualEventListeners(); + // 防止重复销毁 + if (this._destroyed) { + return; + } + this.cleanupManualEventListeners(); + this._destroyed = true; this.onDestroy(); } diff --git a/packages/core/tests/ECS/Systems/EntitySystem.test.ts b/packages/core/tests/ECS/Systems/EntitySystem.test.ts index 862c38a8..a26e6673 100644 --- a/packages/core/tests/ECS/Systems/EntitySystem.test.ts +++ b/packages/core/tests/ECS/Systems/EntitySystem.test.ts @@ -192,6 +192,47 @@ describe('EntitySystem', () => { expect(handler).not.toHaveBeenCalled(); }); + + it('dispose 方法应该调用 onDestroy 回调', () => { + const plainSystem = new PlainEntitySystem(); + scene.addSystem(plainSystem); + + // 直接调用 dispose + plainSystem.dispose(); + + // 验证 onDestroy 被调用 + expect(plainSystem.onDestroyCallCount).toBe(1); + }); + + it('多次调用 dispose 或 destroy 不应该重复调用 onDestroy', () => { + const plainSystem = new PlainEntitySystem(); + scene.addSystem(plainSystem); + + // 调用 dispose + plainSystem.dispose(); + expect(plainSystem.onDestroyCallCount).toBe(1); + + // 再次调用 dispose + plainSystem.dispose(); + expect(plainSystem.onDestroyCallCount).toBe(1); + + // 调用 destroy + plainSystem.destroy(); + expect(plainSystem.onDestroyCallCount).toBe(1); + }); + + it('先调用 destroy 再调用 dispose 不应该重复调用 onDestroy', () => { + const plainSystem = new PlainEntitySystem(); + scene.addSystem(plainSystem); + + // 先调用 destroy + plainSystem.destroy(); + expect(plainSystem.onDestroyCallCount).toBe(1); + + // 再调用 dispose + plainSystem.dispose(); + expect(plainSystem.onDestroyCallCount).toBe(1); + }); }); describe('错误处理', () => {