refactor: ReactiveScene

This commit is contained in:
hyper 2026-04-12 16:26:52 +08:00
parent 21a7afa276
commit 59fa0e6122
6 changed files with 105 additions and 47 deletions

View File

@ -7,9 +7,9 @@ export { spawnEffect } from './spawner';
export type { Spawner } from './spawner'; export type { Spawner } from './spawner';
// Scene base classes // Scene base classes
export { GameHostScene } from './scenes'; export { ReactiveScene, GameHostScene } from './scenes';
export type { GameHostSceneOptions } from './scenes'; export type { ReactiveSceneOptions, ReactiveScenePhaserData, GameHostSceneOptions } from './scenes';
// React ↔ Phaser bridge // 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'; export type { PhaserGameProps, PhaserSceneProps, GameUIOptions } from './ui';

View File

@ -1,22 +1,16 @@
import Phaser from 'phaser';
import { effect } from '@preact/signals-core';
import type { GameHost } from 'boardgame-core'; import type { GameHost } from 'boardgame-core';
import { DisposableBag, type IDisposable } from '../utils'; import { ReactiveScene, type ReactiveScenePhaserData } from './ReactiveScene';
type CleanupFn = void | (() => void);
export interface GameHostSceneOptions<TState extends Record<string, unknown>> { export interface GameHostSceneOptions<TState extends Record<string, unknown>> {
gameHost: GameHost<TState>; gameHost: GameHost<TState>;
[key: string]: unknown;
} }
export abstract class GameHostScene<TState extends Record<string, unknown>> export abstract class GameHostScene<TState extends Record<string, unknown>>
extends Phaser.Scene extends ReactiveScene<GameHostSceneOptions<TState>>
implements IDisposable
{ {
protected disposables = new DisposableBag();
private _gameHost!: GameHost<TState>;
public get gameHost(): GameHost<TState> { public get gameHost(): GameHost<TState> {
return this._gameHost; return this.initData.gameHost as GameHost<TState>;
} }
public get state(): TState { public get state(): TState {
@ -32,24 +26,4 @@ export abstract class GameHostScene<TState extends Record<string, unknown>>
resolve => tween.once('complete', resolve) resolve => tween.once('complete', resolve)
)); ));
} }
init(data: GameHostSceneOptions<TState>): 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));
}
} }

View File

@ -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<TData extends Record<string, unknown> = {}> {
key?: string;
}
/**
* Scene
* @typeparam TData - init(data) phaserGame
*/
export abstract class ReactiveScene<TData extends Record<string, unknown> = {}>
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));
}
}

View File

@ -1,2 +1,5 @@
export { ReactiveScene } from './ReactiveScene';
export type { ReactiveSceneOptions, ReactiveScenePhaserData } from './ReactiveScene';
export { GameHostScene } from './GameHostScene'; export { GameHostScene } from './GameHostScene';
export type { GameHostSceneOptions } from './GameHostScene'; export type { GameHostSceneOptions } from './GameHostScene';

View File

@ -3,8 +3,13 @@ import { signal, useSignal, useSignalEffect } from '@preact/signals';
import { createContext, h } from 'preact'; import { createContext, h } from 'preact';
import { useContext } from 'preact/hooks'; import { useContext } from 'preact/hooks';
import {ReadonlySignal} from "@preact/signals-core"; import {ReadonlySignal} from "@preact/signals-core";
import type { ReactiveScene, ReactiveScenePhaserData } from '../scenes';
export const phaserContext = createContext<ReadonlySignal<Phaser.Game | undefined>>(signal(undefined)); export interface PhaserGameContext {
game: Phaser.Game;
}
export const phaserContext = createContext<ReadonlySignal<PhaserGameContext> | null>(null);
export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = { export const defaultPhaserConfig: Phaser.Types.Core.GameConfig = {
type: Phaser.AUTO, type: Phaser.AUTO,
@ -21,7 +26,7 @@ export interface PhaserGameProps {
} }
export function PhaserGame(props: PhaserGameProps) { export function PhaserGame(props: PhaserGameProps) {
const gameSignal = useSignal<Phaser.Game>(); const gameSignal = useSignal<PhaserGameContext>({ game: undefined! });
useSignalEffect(() => { useSignalEffect(() => {
const config: Phaser.Types.Core.GameConfig = { const config: Phaser.Types.Core.GameConfig = {
@ -29,10 +34,10 @@ export function PhaserGame(props: PhaserGameProps) {
...props.config, ...props.config,
}; };
const phaserGame = new Phaser.Game(config); const phaserGame = new Phaser.Game(config);
gameSignal.value = phaserGame; gameSignal.value = { game: phaserGame };
return () => { return () => {
gameSignal.value = undefined; gameSignal.value = { game: undefined! };
phaserGame.destroy(true); phaserGame.destroy(true);
}; };
}); });
@ -46,24 +51,31 @@ export function PhaserGame(props: PhaserGameProps) {
); );
} }
export interface PhaserSceneProps { export interface PhaserSceneProps<TData extends Record<string, unknown> = {}> {
sceneKey: string; sceneKey: string;
scene: Phaser.Scene; scene: ReactiveScene<TData>;
autoStart: boolean; autoStart: boolean;
data?: object; data?: TData;
children?: any; children?: any;
} }
export const phaserSceneContext = createContext<ReadonlySignal<Phaser.Scene | undefined>>(signal(undefined)); export const phaserSceneContext = createContext<ReadonlySignal<Phaser.Scene | undefined>>(signal(undefined));
export function PhaserScene(props: PhaserSceneProps) { export function PhaserScene<TData extends Record<string, unknown> = {}>(props: PhaserSceneProps<TData>) {
const context = useContext(phaserContext); const phaserGameSignal = useContext(phaserContext);
const sceneSignal = useSignal<Phaser.Scene>(); const sceneSignal = useSignal<Phaser.Scene>();
useSignalEffect(() => { useSignalEffect(() => {
const game = context.value; if (!phaserGameSignal) return;
if (!game) 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); sceneSignal.value = game.scene.getScene(props.sceneKey);
return () => { return () => {
sceneSignal.value = undefined; sceneSignal.value = undefined;

View File

@ -1,5 +1,5 @@
export { GameUI } from './GameUI'; export { GameUI } from './GameUI';
export type { GameUIOptions } 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'; export type { PhaserGameProps, PhaserSceneProps } from './PhaserBridge';