refactor: ReactiveScene
This commit is contained in:
parent
21a7afa276
commit
59fa0e6122
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue