From 673c50ce8fa5ed4b557364dc1fbd895c2e510082 Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 19 Apr 2026 12:20:24 +0800 Subject: [PATCH] feat: add SceneManager with full lifecycle management --- src/scene/SceneManager.ts | 195 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/scene/SceneManager.ts diff --git a/src/scene/SceneManager.ts b/src/scene/SceneManager.ts new file mode 100644 index 0000000..1453b9f --- /dev/null +++ b/src/scene/SceneManager.ts @@ -0,0 +1,195 @@ +import { Container } from "pixi.js"; +import Game from "@/core/Game"; +import { logger } from "@/core/Logger"; +import { assetManager } from "@/core/AssetManager"; +import { SceneType } from "@/enums/SceneType"; +import { BaseScene } from "./BaseScene"; +import type { SceneConfig, IBaseScene } from "./types"; + +type StageChangeCallback = ( + current: IBaseScene, + previous: IBaseScene | undefined +) => Promise | void; + +class SceneManager { + private static instance: SceneManager; + private game: Game; + private scenes: Map = new Map(); + private _currentScene: IBaseScene | null = null; + private changeCallbacks: StageChangeCallback[] = []; + + private constructor() { + this.game = Game.getInstance(); + } + + static getInstance(): SceneManager { + if (!SceneManager.instance) { + SceneManager.instance = new SceneManager(); + } + return SceneManager.instance; + } + + get currentScene(): IBaseScene | null { + return this._currentScene; + } + + /** 监听场景变化 */ + onStageChange(cb: StageChangeCallback): () => void { + this.changeCallbacks.push(cb); + return () => { + const index = this.changeCallbacks.indexOf(cb); + if (index >= 0) { + this.changeCallbacks.splice(index, 1); + } + }; + } + + /** 触发场景变化回调 */ + private async emitStageChange( + current: IBaseScene, + previous: IBaseScene | undefined + ): Promise { + for (const cb of this.changeCallbacks) { + await cb(current, previous); + } + } + + /** 注册场景 */ + registerScene(scene: IBaseScene): void { + if (this.scenes.has(scene.name)) { + logger.warn(`SceneManager: scene "${scene.name}" already registered`); + } + this.scenes.set(scene.name, scene); + + // 注入 changeScene 方法 + scene.changeScene = (name: string, options) => { + this.changeScene(name, options); + }; + + if (scene.type === SceneType.Resident) { + this.game.stage.addChild(scene.stage); + logger.debug(`SceneManager: registered resident scene "${scene.name}"`); + } else { + logger.debug(`SceneManager: registered normal scene "${scene.name}"`); + } + } + + /** 初始化入口场景 */ + initScene(name: string): void { + const scene = this.getSceneOrThrow(name); + this._currentScene = scene; + + if (scene.type === SceneType.Normal) { + this.game.stage.addChild(scene.stage); + } + + this.emitStageChange(scene, undefined); + logger.debug(`SceneManager: initialized scene "${name}"`); + } + + /** 切换场景 */ + async changeScene(name: string, options?: { isHolderLast?: boolean }): Promise { + const previous = this._currentScene; + if (!previous) { + throw new Error(`SceneManager: no current scene, call initScene first`); + } + + const target = this.getSceneOrThrow(name); + + // 处理前一个场景 + if (previous.type === SceneType.Normal) { + if (options?.isHolderLast) { + previous.stage.visible = false; + // @ts-ignore 标记保留 + previous.stage._isHolderLast = true; + } else { + // 销毁场景 + previous.stage.destroy({ children: true }); + this.game.stage.removeChild(previous.stage); + // 卸载资源 + if (previous._assetsLoaded) { + await previous.unLoadBundle?.(); + previous._assetsLoaded = false; + } + await previous.onUnLoad?.(); + this.scenes.delete(previous.name); + } + } else if (previous.type === SceneType.Resident) { + previous.stage.visible = false; + await previous.onUnLoad?.(); + } + + this._currentScene = target; + + // 处理目标场景 + if (target.type === SceneType.Normal) { + // @ts-ignore + if (target.stage._isHolderLast) { + target.stage.visible = true; + } else { + this.game.stage.addChild(target.stage); + } + } else if (target.type === SceneType.Resident) { + target.stage.visible = true; + } + + // 生命周期 + if (!target._assetsLoaded) { + await target.loadBundle?.(); + target._assetsLoaded = true; + } + + if (!target._layoutDone || target.stage !== this.getSceneOrThrow(name).stage) { + await target.layout?.(); + target._layoutDone = true; + } + + await target.onLoad?.(); + + // 触发回调 + await this.emitStageChange(target, previous); + + logger.debug(`SceneManager: changed from "${previous?.name}" to "${name}"`); + } + + /** 获取已注册场景 */ + getScene(name: string): IBaseScene | undefined { + return this.scenes.get(name); + } + + getSceneOrThrow(name: string): IBaseScene { + const scene = this.scenes.get(name); + if (!scene) { + throw new Error(`SceneManager: scene "${name}" not registered`); + } + return scene; + } + + /** 确保场景存在,不存在则创建容器 */ + ensureSceneExists(name: string, type: SceneType = SceneType.Normal): Container { + if (this.scenes.has(name)) { + return this.getSceneOrThrow(name).stage; + } + + const container = new Container(); + container.label = name; + // 动态创建一个简单场景 + // 这个功能主要用于兼容旧的代码风格 + class DynamicScene extends BaseScene { + stage = container; + name = name; + type = type; + } + + const scene = new DynamicScene(); + this.registerScene(scene); + return container; + } + + hasScene(name: string): boolean { + return this.scenes.has(name); + } +} + +export default SceneManager; +export const sceneManager = SceneManager.getInstance(); \ No newline at end of file