diff --git a/packages/framework/src/index.ts b/packages/framework/src/index.ts index aff1aac..5a3360f 100644 --- a/packages/framework/src/index.ts +++ b/packages/framework/src/index.ts @@ -7,9 +7,9 @@ export { spawnEffect } from './spawner'; export type { Spawner } from './spawner'; // Scene base classes -export { GameHostScene } from './scenes'; -export type { GameHostSceneOptions } from './scenes'; +export { ReactiveScene, GameHostScene } from './scenes'; +export type { ReactiveSceneOptions, ReactiveScenePhaserData, GameHostSceneOptions } from './scenes'; // React ↔ Phaser bridge -export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, GameUI } from './ui'; +export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, GameUI, type PhaserGameContext } from './ui'; export type { PhaserGameProps, PhaserSceneProps, GameUIOptions } from './ui'; diff --git a/packages/framework/src/scenes/GameHostScene.ts b/packages/framework/src/scenes/GameHostScene.ts index add7342..746d49f 100644 --- a/packages/framework/src/scenes/GameHostScene.ts +++ b/packages/framework/src/scenes/GameHostScene.ts @@ -1,24 +1,18 @@ -import Phaser from 'phaser'; -import { effect } from '@preact/signals-core'; import type { GameHost } from 'boardgame-core'; -import { DisposableBag, type IDisposable } from '../utils'; - -type CleanupFn = void | (() => void); +import { ReactiveScene, type ReactiveScenePhaserData } from './ReactiveScene'; export interface GameHostSceneOptions> { gameHost: GameHost; + [key: string]: unknown; } export abstract class GameHostScene> - extends Phaser.Scene - implements IDisposable + extends ReactiveScene> { - protected disposables = new DisposableBag(); - private _gameHost!: GameHost; public get gameHost(): GameHost { - return this._gameHost; + return this.initData.gameHost as GameHost; } - + public get state(): TState { return this.gameHost?.state.value; } @@ -32,24 +26,4 @@ export abstract class GameHostScene> resolve => tween.once('complete', resolve) )); } - - init(data: GameHostSceneOptions): void { - this._gameHost = data.gameHost; - } - - create(): void { - this.events.on('shutdown', this.dispose, this); - } - - dispose(): void { - this.disposables.dispose(); - } - - public addDisposable(disposable: IDisposable){ - this.disposables.add(disposable); - } - /** 注册响应式监听(场景关闭时自动清理) */ - public addEffect(fn: () => CleanupFn): void { - this.disposables.add(effect(fn)); - } } diff --git a/packages/framework/src/scenes/ReactiveScene.ts b/packages/framework/src/scenes/ReactiveScene.ts new file mode 100644 index 0000000..03d34b1 --- /dev/null +++ b/packages/framework/src/scenes/ReactiveScene.ts @@ -0,0 +1,69 @@ +import Phaser from 'phaser'; +import { effect } from '@preact/signals-core'; +import { DisposableBag, type IDisposable } from '../utils'; + +type CleanupFn = void | (() => void); + +export interface PhaserGameContext { + game: Phaser.Game; +} + +export interface ReactiveScenePhaserData { + phaserGame: PhaserGameContext; +} + +export interface ReactiveSceneOptions = {}> { + key?: string; +} + +/** + * 通用的响应式 Scene 基类 + * @typeparam TData - 通过 init(data) 接收的数据类型(必须包含 phaserGame) + */ +export abstract class ReactiveScene = {}> + extends Phaser.Scene + implements IDisposable +{ + protected disposables = new DisposableBag(); + private _initData!: TData & ReactiveScenePhaserData; + + /** + * 获取通过 init() 注入的数据 + * 在 create() 阶段保证可用 + */ + public get initData(): TData & ReactiveScenePhaserData { + return this._initData; + } + + /** + * 获取 Phaser game 实例的响应式信号 + */ + public get phaserGame(): PhaserGameContext { + return this._initData.phaserGame; + } + + constructor(key?: string) { + super(key); + } + + init(data: TData & ReactiveScenePhaserData): void { + this._initData = data; + } + + create(): void { + this.events.on('shutdown', this.dispose, this); + } + + dispose(): void { + this.disposables.dispose(); + } + + public addDisposable(disposable: IDisposable): void { + this.disposables.add(disposable); + } + + /** 注册响应式监听(场景关闭时自动清理) */ + public addEffect(fn: () => CleanupFn): void { + this.disposables.add(effect(fn)); + } +} diff --git a/packages/framework/src/scenes/index.ts b/packages/framework/src/scenes/index.ts index 98fb1d7..25e229d 100644 --- a/packages/framework/src/scenes/index.ts +++ b/packages/framework/src/scenes/index.ts @@ -1,2 +1,5 @@ +export { ReactiveScene } from './ReactiveScene'; +export type { ReactiveSceneOptions, ReactiveScenePhaserData } from './ReactiveScene'; + export { GameHostScene } from './GameHostScene'; export type { GameHostSceneOptions } from './GameHostScene'; diff --git a/packages/framework/src/ui/PhaserBridge.tsx b/packages/framework/src/ui/PhaserBridge.tsx index 64e80cb..fc6b81f 100644 --- a/packages/framework/src/ui/PhaserBridge.tsx +++ b/packages/framework/src/ui/PhaserBridge.tsx @@ -3,8 +3,13 @@ import { signal, useSignal, useSignalEffect } from '@preact/signals'; import { createContext, h } from 'preact'; import { useContext } from 'preact/hooks'; import {ReadonlySignal} from "@preact/signals-core"; +import type { ReactiveScene, ReactiveScenePhaserData } from '../scenes'; -export const phaserContext = createContext>(signal(undefined)); +export interface PhaserGameContext { + game: Phaser.Game; +} + +export const phaserContext = createContext | null>(null); export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = { type: Phaser.AUTO, @@ -21,7 +26,7 @@ export interface PhaserGameProps { } export function PhaserGame(props: PhaserGameProps) { - const gameSignal = useSignal(); + const gameSignal = useSignal({ game: undefined! }); useSignalEffect(() => { const config: Phaser.Types.Core.GameConfig = { @@ -29,10 +34,10 @@ export function PhaserGame(props: PhaserGameProps) { ...props.config, }; const phaserGame = new Phaser.Game(config); - gameSignal.value = phaserGame; + gameSignal.value = { game: phaserGame }; return () => { - gameSignal.value = undefined; + gameSignal.value = { game: undefined! }; phaserGame.destroy(true); }; }); @@ -46,24 +51,31 @@ export function PhaserGame(props: PhaserGameProps) { ); } -export interface PhaserSceneProps { +export interface PhaserSceneProps = {}> { sceneKey: string; - scene: Phaser.Scene; + scene: ReactiveScene; autoStart: boolean; - data?: object; + data?: TData; children?: any; } export const phaserSceneContext = createContext>(signal(undefined)); -export function PhaserScene(props: PhaserSceneProps) { - const context = useContext(phaserContext); +export function PhaserScene = {}>(props: PhaserSceneProps) { + const phaserGameSignal = useContext(phaserContext); const sceneSignal = useSignal(); useSignalEffect(() => { - const game = context.value; - if (!game) return; + if (!phaserGameSignal) return; + const ctx = phaserGameSignal.value; + if (!ctx?.game) return; - game.scene.add(props.sceneKey, props.scene, props.autoStart, props.data); + const game = ctx.game; + const initData = { + ...props.data, + phaserGame: phaserGameSignal.value, + } as TData & ReactiveScenePhaserData; + + game.scene.add(props.sceneKey, props.scene, props.autoStart, initData); sceneSignal.value = game.scene.getScene(props.sceneKey); return () => { sceneSignal.value = undefined; diff --git a/packages/framework/src/ui/index.ts b/packages/framework/src/ui/index.ts index 029b0fe..8329404 100644 --- a/packages/framework/src/ui/index.ts +++ b/packages/framework/src/ui/index.ts @@ -1,5 +1,5 @@ export { GameUI } from './GameUI'; export type { GameUIOptions } from './GameUI'; -export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig } from './PhaserBridge'; +export { PhaserGame, PhaserScene, phaserContext, defaultPhaserConfig, type PhaserGameContext } from './PhaserBridge'; export type { PhaserGameProps, PhaserSceneProps } from './PhaserBridge';