mcpField
P
phaser-scene

phaser-scene

github.com/yakoub-ai開発29194 回インストール9 週間前に更新
リポジトリ

phaser-scene について

phaser-sceneは、Sceneクラスを中心にPhaser 4のゲームを構成するスキルです。代表的なシーンの種類、遷移、シーン間通信、安全な後処理やサイズ調整のパターンを解説します。

主な機能

  1. 01標準的なSceneクラスの構成
  2. 02起動、読み込み、メニュー、ゲームの各シーン
  3. 03HUDとポーズ用オーバーレイ
  4. 04データを伴うシーン遷移
  5. 05シーン間メッセージのパターン
  6. 06終了処理、タイマー、リサイズへの対応

活用シーン

  1. 01ゲームを役割ごとのシーンに分割
  2. 02並行HUDやポーズ用オーバーレイの構築
  3. 03シーン間で状態を受け渡す
  4. 04後処理や画面サイズの問題を防ぐ

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yakoub-ai/phaser4-gamedev --skill phaser-scene

インストール後、Claude Code で/phaser-sceneを入力して読み込まれたか確認します。

同梱ファイルreferences/scene-patterns.md

詳細

Phaser 4 シーンの作成

すべての Phaser 4 ゲームは Scene クラスで構成されています。各シーンは独自のライフサイクル、アセット、ゲームオブジェクトを持つ自己完結した単位です。

シーンクラスのパターン

すべてのシーンの標準的なパターン:

import Phaser from 'phaser';

export class MyScene extends Phaser.Scene {
  constructor() {
    super({ key: 'MyScene' });  // key MUST be unique across all scenes
  }

  // Called when scene starts, before preload. Receive data from previous scene.
  init(data?: Record<string, unknown>): void {
    // e.g., data.level, data.score from this.scene.start('MyScene', { level: 2 })
  }

  // Load assets used only by this scene (prefer PreloaderScene for shared assets)
  preload(): void { }

  // Build the scene: create game objects, physics, input, events
  create(): void { }

  // Called every frame. Keep lean — call helper methods
  update(time: number, delta: number): void { }

  // Called when scene is about to be stopped or replaced. Clean up here.
  shutdown(): void {
    // Remove event listeners; stop sounds; clear tracked timers
  }
}

すべてのシーンを main.ts の GameConfig に登録します:

scene: [BootScene, PreloaderScene, MainMenuScene, GameScene, GameOverScene]

シーンタイプ別のパターン

BootScene

最小限の構成です。ローディングバーに必要なアセットのみを読み込み、すぐに遷移します。

export class BootScene extends Phaser.Scene {
  constructor() { super({ key: 'BootScene' }); }

  preload(): void {
    this.load.image('loading-bg', 'assets/images/loading-bg.png');
    this.load.image('loading-bar', 'assets/images/loading-bar.png');
  }

  create(): void {
    this.scene.start('PreloaderScene');
  }
}
PreloaderScene

すべてのゲームアセットを読み込みながらローディングバーを表示します。ここでグローバルアニメーションも設定します。

export class PreloaderScene extends Phaser.Scene {
  constructor() { super({ key: 'PreloaderScene' }); }

  preload(): void {
    const { width, height } = this.scale;

    // Loading bar
    const bg = this.add.graphics();
    bg.fillStyle(0x111111, 0.8);
    bg.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);

    const bar = this.add.graphics();
    this.load.on('progress', (value: number) => {
      bar.clear();
      bar.fillStyle(0x00ff88, 1);
      bar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
    });

    // ── Load all game assets here ──
    this.load.atlas('characters', 'assets/atlases/characters.png', 'assets/atlases/characters.json');
    this.load.audio('bgm', ['assets/audio/bgm.mp3', 'assets/audio/bgm.ogg']);
  }

  create(): void {
    // Create all animations here — available globally after this
    this.anims.create({
      key: 'player-idle',
      frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
      frameRate: 8,
      repeat: -1,
    });

    this.scene.start('MainMenuScene');
  }
}
MainMenuScene

タイトル画面です。スタートボタンや設定へのナビゲーションを処理します。

export class MainMenuScene extends Phaser.Scene {
  constructor() { super({ key: 'MainMenuScene' }); }

  create(): void {
    const { width, height } = this.scale;

    this.add.image(width / 2, height / 2, 'menu-bg');
    this.add.text(width / 2, height * 0.3, 'MY GAME', {
      fontSize: '64px',
      color: '#ffffff',
      fontFamily: 'Arial Black',
    }).setOrigin(0.5);

    const startBtn = this.add.text(width / 2, height * 0.6, 'PLAY', {
      fontSize: '32px',
      color: '#00ff88',
      backgroundColor: '#333',
      padding: { x: 20, y: 10 },
    }).setOrigin(0.5).setInteractive({ useHandCursor: true });

    startBtn.on('pointerover', () => startBtn.setStyle({ color: '#ffff00' }));
    startBtn.on('pointerout', () => startBtn.setStyle({ color: '#00ff88' }));
    startBtn.on('pointerdown', () => {
      this.cameras.main.fadeOut(500, 0, 0, 0, () => {
        this.scene.start('GameScene', { level: 1 });
      });
    });
  }
}
GameOverScene

終了状態です。スコアを表示し、リスタートを提案します。

export class GameOverScene extends Phaser.Scene {
  constructor() { super({ key: 'GameOverScene' }); }

  init(data: { score: number }): void {
    this.registry.set('finalScore', data.score);
  }

  create(): void {
    const { width, height } = this.scale;
    const score = this.registry.get('finalScore') as number;

    this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7);
    this.add.text(width / 2, height * 0.35, 'GAME OVER', {
      fontSize: '56px', color: '#ff4444',
    }).setOrigin(0.5);
    this.add.text(width / 2, height * 0.5, `Score: ${score}`, {
      fontSize: '32px', color: '#ffffff',
    }).setOrigin(0.5);

    this.add.text(width / 2, height * 0.65, 'Play Again', {
      fontSize: '28px', color: '#00ff88',
    }).setOrigin(0.5).setInteractive({ useHandCursor: true })
      .on('pointerdown', () => this.scene.start('GameScene', { level: 1 }));
  }
}
HUDScene (並行オーバーレイ)

this.scene.launch('HUDScene') によって GameScene と同時に実行されます。体力バー、スコア、ミニマップに最適です。

// In GameScene.create():
this.scene.launch('HUDScene');
// Pass events to HUD
this.events.on('scoreChanged', (score: number) => {/* HUD listens */});

// HUDScene.ts:
export class HUDScene extends Phaser.Scene {
  private scoreText!: Phaser.GameObjects.Text;

  constructor() { super({ key: 'HUDScene' }); }

  create(): void {
    this.scoreText = this.add.text(16, 16, 'Score: 0', {
      fontSize: '20px', color: '#ffffff',
    }).setScrollFactor(0);  // Fixed to camera

    const gameScene = this.scene.get('GameScene');
    gameScene.events.on('scoreChanged', (score: number) => {
      this.scoreText.setText(`Score: ${score}`);
    }, this);

    // Clean up when GameScene stops
    gameScene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
      gameScene.events.off('scoreChanged');
    }, this);
  }
}
PauseScene (モーダルオーバーレイ)

一時停止された GameScene の上に起動されます。

// In GameScene — pause on Escape:
const esc = this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC);
esc.on('down', () => {
  this.scene.pause('GameScene');
  this.scene.launch('PauseScene');
});

// PauseScene.ts:
export class PauseScene extends Phaser.Scene {
  constructor() { super({ key: 'PauseScene' }); }

  create(): void {
    const { width, height } = this.scale;
    this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);
    this.add.text(width / 2, height / 2 - 50, 'PAUSED', {
      fontSize: '48px', color: '#ffffff',
    }).setOrigin(0.5);

    this.add.text(width / 2, height / 2 + 40, 'Resume', {
      fontSize: '28px', color: '#00ff88',
    }).setOrigin(0.5).setInteractive({ useHandCursor: true })
      .on('pointerdown', () => {
        this.scene.resume('GameScene');
        this.scene.stop('PauseScene');
      });
  }
}

シーンの遷移

// Basic transition
this.scene.start('TargetScene');

// With data
this.scene.start('GameScene', { level: 2, score: 1500 });

// With fade
this.cameras.main.fadeOut(500, 0, 0, 0, () => {
  this.scene.start('NextScene');
});

// Parallel launch (both scenes run simultaneously)
this.scene.launch('HUDScene');

// Pause/resume
this.scene.pause('GameScene');
this.scene.resume('GameScene');

// Stop a scene entirely
this.scene.stop('HUDScene');

// Restart current scene
this.scene.restart();
this.scene.restart({ level: 1, score: 0 }); // with fresh data

シーン間の通信

// 1. Registry (simple key-value, fires events on change)
this.registry.set('score', 0);
this.registry.get('score');
this.registry.events.on('changedata-score', (parent, value) => {});

// 2. Scene events (strongly typed messaging)
// Emitter scene:
this.events.emit('enemyKilled', { points: 100, x: enemy.x, y: enemy.y });
// Listener scene:
this.scene.get('GameScene').events.on('enemyKilled', (data: {points: number}) => {});

// 3. Direct scene reference (use sparingly — creates tight coupling)
const gameScene = this.scene.get('GameScene') as GameScene;
gameScene.addScore(100);

防御的なシーンシャットダウン

シーンの破棄処理は、Phaser 4 におけるランタイムクラッシュの最も一般的な原因です。physics、tweens、およびカメラが所有するオブジェクトは、シャットダウン中に null になる可能性があります。特に、トゥイーン実行中に停止されるオーバーレイシーンでは顕著です。

タイマー追跡パターン

匿名の time.addEvent() 呼び出しは蓄積され、シーンの再起動時にクリーンアップされません。すべてのタイマーを追跡してください:

export class GameScene extends Phaser.Scene {
  private activeTimers: Phaser.Time.TimerEvent[] = [];

  // Use this instead of this.time.addEvent() directly
  private addTimer(cfg: Phaser.Types.Time.TimerEventConfig): Phaser.Time.TimerEvent {
    const t = this.time.addEvent(cfg);
    this.activeTimers.push(t);
    return t;
  }

  shutdown(): void {
    for (const t of this.activeTimers) t.remove(false);
    this.activeTimers = [];
    this.bgMusic?.stop();
  }
}
オーバーレイシーンのサイズ指定

オーバーレイシーン (PauseScene、ダイアログパネル) は、全画面の背景を this.cameras.main.width/height からサイズ指定する必要があります。モジュールレベルの定数からではありません。定数は起動時に固定されますが、カメラの寸法はライブビューポートに合わせて更新されます。定数からサイズ指定された背景は、キャンバスのリサイズ後に正しくなくなります。

// BAD — constant freezes at boot-time dimensions:
const GAME_W = 800;
this.add.rectangle(GAME_W / 2, GAME_H / 2, GAME_W, GAME_H, 0x000000, 0.5);

// CORRECT — reads live dimensions:
const { width, height } = this.cameras.main;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);

また、ウィンドウのリサイズ中にオーバーレイが開いたままになる可能性がある場合は、リサイズリスナーも追加してください:

this.scale.on('resize', (size: Phaser.Structs.Size) => {
  this.backdrop.setPosition(size.width / 2, size.height / 2);
  this.backdrop.setSize(size.width, size.height);
});

追加リソース

参照ファイル
  • references/scene-patterns.md — すべてのシーンタイプの詳細なパターン、高度な遷移、シーンマネージャーのパターン

コメント