boardgame-phaser/packages/framework/src/ui/PhaserBridge.tsx

247 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { signal, useSignal, useSignalEffect } from "@preact/signals";
import Phaser, { AUTO } from "phaser";
import { createContext } from "preact";
import { useContext, useEffect, useRef } from "preact/hooks";
import {
FadeScene as FadeSceneClass,
FADE_SCENE_KEY,
} from "../scenes/FadeScene";
import type { ReactiveScene } from "../scenes";
import type { ReadonlySignal } from "@preact/signals-core";
export interface SceneController {
/** 启动场景(带淡入淡出过渡) */
launch(sceneKey: string): Promise<void>;
/** 重新启动当前场景(带淡入淡出过渡) */
restart(): Promise<void>;
/** 当前活跃场景 key */
currentScene: ReadonlySignal<string | null>;
/** 是否正在过渡 */
isTransitioning: ReadonlySignal<boolean>;
}
export interface PhaserGameContext {
game: Phaser.Game;
sceneController: SceneController;
}
export const phaserContext =
createContext<ReadonlySignal<PhaserGameContext> | null>(null);
export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = {
type: AUTO,
width: 560,
height: 560,
parent: "phaser-container",
backgroundColor: "#f9fafb",
scene: [],
disableContextMenu: true,
};
export interface PhaserGameProps {
config?: Partial<Phaser.Types.Core.GameConfig>;
/** 初始启动的场景 key */
initialScene?: string;
children?: preact.ComponentChildren;
}
export function PhaserGame(props: PhaserGameProps) {
const gameSignal = useSignal<PhaserGameContext>({
game: undefined!,
sceneController: undefined!,
});
const initialSceneLaunched = useRef(false);
useSignalEffect(() => {
const config: Phaser.Types.Core.GameConfig = {
...defaultPhaserConfig,
...props.config,
};
const phaserGame = new Phaser.Game(config);
// 添加 FadeScene 并启动它来初始化 overlay
const fadeScene = new FadeSceneClass();
phaserGame.scene.add(FADE_SCENE_KEY, fadeScene, true); // 改为 true 以触发 create
// 创建 SceneController
const currentScene = signal<string | null>(null);
const isTransitioning = signal(false);
const sceneController: SceneController = {
async launch(sceneKey: string) {
if (isTransitioning.value) {
console.warn("SceneController: 正在进行场景切换");
return;
}
// 等待场景注册完成(最多等待 100ms
let retries = 0;
while (!phaserGame.scene.getScene(sceneKey) && retries < 10) {
await new Promise((resolve) => setTimeout(resolve, 10));
retries++;
}
// 验证场景是否已注册
if (!phaserGame.scene.getScene(sceneKey)) {
console.error(`SceneController: 场景 "${sceneKey}" 未注册`);
return;
}
isTransitioning.value = true;
const fade = phaserGame.scene.getScene(
FADE_SCENE_KEY,
) as FadeSceneClass;
// 淡出到黑色
phaserGame.scene.bringToTop(FADE_SCENE_KEY);
await fade.fadeOut(300);
// 停止当前场景
if (currentScene.value) {
phaserGame.scene.stop(currentScene.value);
}
// 确保场景已注册后再启动
// (场景应该已经在 PhaserScene 组件中注册)
if (!phaserGame.scene.getScene(sceneKey)) {
console.error(`SceneController: 场景 "${sceneKey}" 在切换时仍未注册`);
isTransitioning.value = false;
return;
}
// 启动新场景
phaserGame.scene.start(sceneKey);
currentScene.value = sceneKey;
// 淡入
await fade.fadeIn(300);
isTransitioning.value = false;
},
async restart() {
if (isTransitioning.value) {
console.warn("SceneController: 正在进行场景切换");
return;
}
if (!currentScene.value) {
console.warn("SceneController: 没有当前场景,无法 restart");
return;
}
const sceneKey = currentScene.value;
const scene = phaserGame.scene.getScene(sceneKey);
if (!scene) {
console.error(`SceneController: 场景 "${sceneKey}" 不存在`);
return;
}
isTransitioning.value = true;
const fade = phaserGame.scene.getScene(
FADE_SCENE_KEY,
) as FadeSceneClass;
// 淡出到黑色
phaserGame.scene.bringToTop(FADE_SCENE_KEY);
await fade.fadeOut(300);
// 重启当前场景
scene.scene.restart();
// 淡入
await fade.fadeIn(300);
isTransitioning.value = false;
},
currentScene,
isTransitioning,
};
gameSignal.value = { game: phaserGame, sceneController };
return () => {
gameSignal.value = { game: undefined!, sceneController: undefined! };
initialSceneLaunched.current = false;
phaserGame.destroy(true);
};
});
// 启动初始场景(仅一次)
useEffect(() => {
const ctx = gameSignal.value;
if (
!initialSceneLaunched.current &&
props.initialScene &&
ctx?.sceneController
) {
initialSceneLaunched.current = true;
// 使用 microtask 确保所有子组件的场景注册已完成
Promise.resolve().then(() => {
ctx.sceneController.launch(props.initialScene!);
});
}
}, [gameSignal.value, props.initialScene]);
return (
<div id="phaser-container" className="w-full h-full">
<phaserContext.Provider value={gameSignal}>
{props.children}
</phaserContext.Provider>
</div>
);
}
export interface PhaserSceneProps<TData = {}> {
sceneKey?: string;
scene: ReactiveScene<TData> | { new (): ReactiveScene<TData> };
data?: TData;
children?: any;
}
export const phaserSceneContext =
createContext<ReadonlySignal<ReactiveScene> | null>(null);
export function PhaserScene<TData = {}>(props: PhaserSceneProps<TData>) {
const phaserGameSignal = useContext(phaserContext);
const sceneSignal = useSignal<ReactiveScene<TData>>();
const registered = useRef(false);
useSignalEffect(() => {
if (!phaserGameSignal) return;
const ctx = phaserGameSignal.value;
if (!ctx?.game) return;
const game = ctx.game;
// 注册场景到 Phaser但不启动
const scene = "scene" in props.scene ? props.scene : new props.scene();
const sceneKey = props.sceneKey ?? scene.sys.settings.key;
if (!game.scene.getScene(sceneKey)) {
const initData = {
...props.data,
phaserGame: phaserGameSignal,
sceneController: ctx.sceneController,
};
game.scene.add(sceneKey, props.scene, false, initData);
}
sceneSignal.value = scene;
registered.current = true;
return () => {
sceneSignal.value = undefined;
registered.current = false;
// 不在这里移除场景,让 SceneController 管理生命周期
};
});
return (
<phaserSceneContext.Provider
value={sceneSignal as ReadonlySignal<ReactiveScene>}
>
{props.children}
</phaserSceneContext.Provider>
);
}