feat: scene manager and transitions

This commit is contained in:
hyper
2026-04-12 16:52:53 +08:00
parent f8a19653ba
commit 50531446c2
9 changed files with 318 additions and 39 deletions
+42 -1
View File
@@ -13,6 +13,9 @@ export class GameScene extends GameHostScene<TicTacToeState> {
private gridGraphics!: Phaser.GameObjects.Graphics;
private turnText!: Phaser.GameObjects.Text;
private winnerOverlay?: Phaser.GameObjects.Container;
private menuButton!: Phaser.GameObjects.Container;
private menuButtonText!: Phaser.GameObjects.Text;
private menuButtonBg!: Phaser.GameObjects.Rectangle;
constructor() {
super('GameScene');
@@ -24,7 +27,8 @@ export class GameScene extends GameHostScene<TicTacToeState> {
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawGrid();
this.createMenuButton();
this.disposables.add(spawnEffect(new TicTacToePartSpawner(this)));
this.addEffect(() => {
@@ -49,6 +53,43 @@ export class GameScene extends GameHostScene<TicTacToeState> {
return !!this.state.board.partMap[`${row},${col}`];
}
private createMenuButton(): void {
const buttonX = this.game.scale.width - 80;
const buttonY = 30;
this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, 0x6b7280)
.setInteractive({ useHandCursor: true });
this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', {
fontSize: '18px',
fontFamily: 'Arial',
color: '#ffffff',
}).setOrigin(0.5);
this.menuButton = this.add.container(buttonX, buttonY, [
this.menuButtonBg,
this.menuButtonText,
]);
// 按钮交互
this.menuButtonBg.on('pointerover', () => {
this.menuButtonBg.setFillStyle(0x4b5563);
});
this.menuButtonBg.on('pointerout', () => {
this.menuButtonBg.setFillStyle(0x6b7280);
});
this.menuButtonBg.on('pointerdown', () => {
this.goToMenu();
});
}
private async goToMenu(): Promise<void> {
const data = this.initData as unknown as Record<string, unknown>;
await this.sceneController.launch('MenuScene', data);
}
private setupInput(): void {
for (let row = 0; row < BOARD_SIZE; row++) {
for (let col = 0; col < BOARD_SIZE; col++) {
@@ -0,0 +1,85 @@
import { ReactiveScene } from 'boardgame-phaser';
export class MenuScene extends ReactiveScene {
private titleText!: Phaser.GameObjects.Text;
private startButton!: Phaser.GameObjects.Container;
private startButtonText!: Phaser.GameObjects.Text;
private startButtonBg!: Phaser.GameObjects.Rectangle;
constructor() {
super('MenuScene');
}
create(): void {
super.create();
const centerX = this.game.scale.width / 2;
const centerY = this.game.scale.height / 2;
// 标题
this.titleText = this.add.text(centerX, centerY - 100, 'Tic-Tac-Toe', {
fontSize: '48px',
fontFamily: 'Arial',
color: '#1f2937',
}).setOrigin(0.5);
// 添加标题动画
this.titleText.setScale(0);
this.tweens.add({
targets: this.titleText,
scale: 1,
duration: 600,
ease: 'Back.easeOut',
});
// 开始按钮
this.startButtonBg = this.add.rectangle(centerX, centerY + 40, 200, 60, 0x3b82f6)
.setInteractive({ useHandCursor: true });
this.startButtonText = this.add.text(centerX, centerY + 40, 'Start Game', {
fontSize: '24px',
fontFamily: 'Arial',
color: '#ffffff',
}).setOrigin(0.5);
this.startButton = this.add.container(centerX, centerY + 40, [
this.startButtonBg,
this.startButtonText,
]);
// 按钮交互
this.startButtonBg.on('pointerover', () => {
this.startButtonBg.setFillStyle(0x2563eb);
this.tweens.add({
targets: this.startButton,
scale: 1.05,
duration: 100,
});
});
this.startButtonBg.on('pointerout', () => {
this.startButtonBg.setFillStyle(0x3b82f6);
this.tweens.add({
targets: this.startButton,
scale: 1,
duration: 100,
});
});
this.startButtonBg.on('pointerdown', () => {
this.startGame();
});
// 副标题
this.add.text(centerX, centerY + 140, 'Click to start playing', {
fontSize: '16px',
fontFamily: 'Arial',
color: '#6b7280',
}).setOrigin(0.5);
}
private async startGame(): Promise<void> {
const data = this.initData as unknown as Record<string, unknown>;
await this.sceneController.launch('GameScene', data);
}
}
+16 -16
View File
@@ -1,7 +1,9 @@
import {useComputed} from '@preact/signals';
import {useComputed, useSignalEffect} from '@preact/signals';
import { createGameHost, type GameModule } from 'boardgame-core';
import { h } from 'preact';
import {PhaserGame, PhaserScene, ReactiveScene} from 'boardgame-phaser';
import {PhaserGame, PhaserScene, ReactiveScene, phaserContext} from 'boardgame-phaser';
import {useContext} from 'preact/hooks';
import {MenuScene} from "@/scenes/MenuScene";
export default function App<TState extends Record<string, unknown>>(props: { gameModule: GameModule<TState>, gameScene: { new(): ReactiveScene } }) {
@@ -10,26 +12,24 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
return { gameHost };
});
const scene = useComputed(() => new props.gameScene());
const gameScene = useComputed(() => new props.gameScene());
const menuScene = useComputed(() => new MenuScene());
const handleReset = () => {
gameHost.value.gameHost.start();
};
const label = useComputed(() => gameHost.value.gameHost.status.value === 'running' ? 'Restart' : 'Start');
// 自动启动菜单场景
const phaserSignal = useContext(phaserContext);
useSignalEffect(() => {
const ctx = phaserSignal?.value;
if (ctx?.sceneController) {
ctx.sceneController.launch('MenuScene', { gameHost: gameHost.value });
}
});
return (
<div className="flex flex-col h-screen">
<div className="p-4 bg-gray-100 border-t border-gray-200">
<button
onClick={handleReset}
className="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors font-medium"
>
{label}
</button>
</div>
<div className="flex-1 flex relative justify-center items-center">
<PhaserGame>
<PhaserScene sceneKey="GameScene" scene={scene.value} autoStart data={gameHost.value} />
<PhaserScene sceneKey="MenuScene" scene={menuScene.value} />
<PhaserScene sceneKey="GameScene" scene={gameScene.value} />
</PhaserGame>
</div>
</div>