You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

297 lines
8.6 KiB

import { Container } from "pixi.js";
import Game from "@/core/Game";
import { logger } from "@/core/Logger";
import { SceneType } from "@/enums/SceneType";
import { BaseScene } from "./BaseScene";
import { TransitionTransaction } from "./TransitionTransaction";
import type { IBaseScene } from "./types";
// Extend the Container interface to allow setting and accessing _isHolderLast
declare module 'pixi.js' {
interface Container {
_isHolderLast?: boolean;
}
}
declare global {
interface Container {
_isHolderLast?: boolean;
}
}
type StageChangeCallback = (
current: IBaseScene,
previous: IBaseScene | undefined
) => Promise<void> | void;
class SceneManager {
private static instance: SceneManager;
private game: Game;
private scenes: Map<string, IBaseScene> = 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<void> {
for (const cb of this.changeCallbacks) {
await cb(current, previous);
}
}
/** 注册场景 */
registerScene(scene: IBaseScene): void {
if (!scene.name) {
logger.warn("SceneManager: scene name cannot be empty");
return;
}
if (this.scenes.has(scene.name)) {
logger.warn(`SceneManager: scene "${scene.name}" already registered`);
}
this.scenes.set(scene.name, scene);
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}"`);
}
}
/** 初始化入口场景(与 changeScene 一致:加载资源 → 布局 → onLoad) */
async initScene(name: string): Promise<void> {
if (!name) {
logger.warn("SceneManager: scene name cannot be empty");
return;
}
const scene = this.getSceneOrThrow(name);
this._currentScene = scene;
if (scene.type === SceneType.Normal) {
this.game.stage.addChild(scene.stage);
} else if (scene.type === SceneType.Resident) {
scene.stage.visible = true;
}
if (!scene._assetsLoaded) {
await scene.loadBundle?.();
scene._assetsLoaded = true;
}
if (!scene._layoutDone) {
await scene.layout?.();
scene._layoutDone = true;
}
await scene.onLoad?.();
await this.emitStageChange(scene, undefined);
logger.debug(`SceneManager: initialized scene "${name}"`);
}
/** 切换场景 */
async changeScene(name: string, options?: { isHolderLast?: boolean }): Promise<void> {
if (!name) {
logger.warn("SceneManager: scene name cannot be empty");
return;
}
const previous = this._currentScene;
if (!previous) {
throw new Error(`SceneManager: no current scene, call initScene first`);
}
const target = this.getSceneOrThrow(name);
let previousVisibleBefore = true;
let previousMountedBefore = false;
let targetVisibleBefore = true;
let targetMountedBefore = false;
const transaction = new TransitionTransaction({
prepare: () => {
previousVisibleBefore = previous.stage.visible;
previousMountedBefore = this.game.stage.children.includes(previous.stage);
targetVisibleBefore = target.stage.visible;
targetMountedBefore = this.game.stage.children.includes(target.stage);
},
commit: async () => {
// 处理前一个场景
if (previous.type === SceneType.Normal) {
if (options?.isHolderLast) {
previous.stage.visible = false;
previous.stage._isHolderLast = true;
} else {
// 销毁显示节点并卸载,但保留场景注册,便于之后再次 changeScene("welcome") 等
const oldStage = previous.stage;
oldStage.destroy({ children: true });
this.game.stage.removeChild(oldStage);
if (previous._assetsLoaded) {
await previous.unLoadBundle?.();
previous._assetsLoaded = false;
}
await previous.onUnLoad?.();
const nextStage = new Container();
nextStage.label = previous.name;
(previous as { stage: Container }).stage = nextStage;
previous._layoutDone = false;
}
} else if (previous.type === SceneType.Resident) {
previous.stage.visible = false;
await previous.onUnLoad?.();
}
this._currentScene = target;
// 处理目标场景
if (target.type === SceneType.Normal) {
if (target.stage._isHolderLast) {
target.stage.visible = true;
} else {
if (!this.game.stage.children.includes(target.stage)) {
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) {
await target.layout?.();
target._layoutDone = true;
}
await target.onLoad?.();
// 触发回调
await this.emitStageChange(target, previous);
logger.debug(`SceneManager: changed from "${previous.name}" to "${name}"`);
},
rollback: async () => {
this._currentScene = previous;
previous.stage.visible = previousVisibleBefore;
target.stage.visible = targetVisibleBefore;
const previousMountedNow = this.game.stage.children.includes(previous.stage);
if (previousMountedBefore && !previousMountedNow) {
this.game.stage.addChild(previous.stage);
} else if (!previousMountedBefore && previousMountedNow) {
this.game.stage.removeChild(previous.stage);
}
const targetMountedNow = this.game.stage.children.includes(target.stage);
if (targetMountedBefore && !targetMountedNow) {
this.game.stage.addChild(target.stage);
} else if (!targetMountedBefore && targetMountedNow) {
this.game.stage.removeChild(target.stage);
}
},
});
await transaction.execute();
}
/** 获取已注册场景 */
getScene(name: string): IBaseScene | undefined {
if (!name) {
logger.warn("SceneManager: scene name cannot be empty");
return undefined;
}
return this.scenes.get(name);
}
getSceneOrThrow(name: string): IBaseScene {
if (!name) {
logger.warn("SceneManager: scene name cannot be empty");
throw new Error("SceneManager: scene name cannot be empty");
}
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 (!name) {
logger.warn("SceneManager: scene name cannot be empty");
return new Container();
}
if (this.scenes.has(name)) {
return this.getSceneOrThrow(name).stage;
}
const container = new Container();
container.label = name;
class DynamicScene extends BaseScene {
stage: Container;
constructor(sceneName: string, sceneType: SceneType, root: Container) {
super(sceneName, sceneType);
this.stage = root;
}
}
const scene = new DynamicScene(name, type, container);
this.registerScene(scene);
return container;
}
/** 获取所有已注册场景 */
getAllScenes(): IBaseScene[] {
return Array.from(this.scenes.values());
}
hasScene(name: string): boolean {
if (!name) {
logger.warn("SceneManager: scene name cannot be empty");
return false;
}
return this.scenes.has(name);
}
}
export default SceneManager;
export const sceneManager = SceneManager.getInstance();