Chapter 02(発展編)

PhaserJS入門
最初のゲーム(発展編) 🎮

配列を使った複数のモグラを管理する、より高度なモグラたたきゲームを作成します

📚 この章で学ぶこと(発展編)

高度なPhaserJS概念

  • • 配列を使ったオブジェクト管理
  • • 複数のスプライトの制御
  • • 動的なオブジェクト生成・削除
  • • DeltaTimeを使った時間管理

作成するゲーム

  • • 複数の穴からランダムに出現
  • • 複数のモグラの同時管理
  • • より本格的なゲームシステム
  • • 実践的なプログラミング手法

⚠️ この章の前提条件

この発展編では配列複数オブジェクトの管理など、やや高度な概念を扱います。 まずは基本編で1匹のモグラを使ったシンプルなゲームを完成させてから挑戦することをおすすめします。

Step 1: ゲーム用画像の準備

まず、ゲームで使用する画像ファイルを準備します。src/assetsフォルダに以下の画像を配置してください。

🖼️ 必要な画像ファイル:

  • hole.png - モグラの穴(64x64px推奨)
  • mole.png - 通常のモグラ(64x64px推奨)
  • mole_hit.png - やられたモグラ(64x64px推奨)

💡 画像について:

画像は自分で用意するか、フリー素材サイトからダウンロードしてください。 シンプルな図形でも構いません。重要なのはPhaserJSの使い方を学ぶことです!

Step 2: メインシーンファイルの作成

src/scenesフォルダにGameScene.tsを作成します。

// src/scenes/GameScene.ts
import * as Phaser from 'phaser';

export class GameScene extends Phaser.Scene {
    // モグラの穴の位置を定義
    private holes: { x: number, y: number }[] = [
        { x: 200, y: 200 },
        { x: 400, y: 200 },
        { x: 600, y: 200 },
        { x: 200, y: 350 },
        { x: 400, y: 350 },
        { x: 600, y: 350 },
    ];

    // 穴のスプライトを格納する配列
    private holeSprites: Phaser.GameObjects.Image[] = [];
    
    // 現在表示中のモグラを格納する配列
    private moles: Phaser.GameObjects.Image[] = [];

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

    // ゲーム開始前にアセットを読み込む
    preload(): void {
        // 画像ファイルを読み込み
        this.load.image('hole', 'assets/hole.png');
        this.load.image('mole', 'assets/mole.png');
        this.load.image('mole_hit', 'assets/mole_hit.png');
    }

    // シーンが開始されたときに実行
    create(): void {
        // 背景色を設定
        this.cameras.main.setBackgroundColor('#4ade80');

        // 穴を配置
        this.holes.forEach(holePos => {
            const hole = this.add.image(holePos.x, holePos.y, 'hole');
            this.holeSprites.push(hole);
        });

        // 2秒ごとにモグラを出現させる
        this.time.addEvent({
            delay: 2000, // 2000ミリ秒 = 2秒
            callback: this.spawnMole,
            callbackScope: this,
            loop: true // 繰り返し実行
        });
    }
}

🔍 コードの解説:

  • holes配列 - モグラの穴の位置を格納
  • preload() - ゲーム開始前に画像を読み込み
  • create() - シーン開始時に実行される初期化処理
  • time.addEvent() - 定期的にイベントを実行

📝 変数について(初心者向け):

プログラミングでは変数を使ってデータを保存します。箱に名前を付けて、その中に値を入れるイメージです。

private holes: クラス内でのみ使える変数(外部からアクセス不可)
{ x: number, y: number }[] 型定義(配列の中身がx,yの数値ペア)
holeSprites: 画面に表示される穴の画像を入れる箱
moles: 画面に表示されるモグラの画像を入れる箱

💡 変数名は分かりやすい名前にすることが重要です!holesなら「穴」、molesなら「モグラ」とすぐ分かりますね。

📚 配列について詳しく解説:

配列は、複数のデータをまとめて管理できる便利な入れ物です。ロッカーのように番号付きの箱がたくさん並んでいるイメージです。

🎮 ゲーム開発で配列が必要不可欠な理由:
✅ 複数のキャラクターを効率的に管理
→ 敵キャラクター100体を一つずつ変数で管理するのは不可能
✅ 一括操作で処理が簡単
→ 全ての敵に同じ処理(移動、攻撃判定など)を適用
✅ 動的な追加・削除が容易
→ ゲーム中に敵やアイテムを自由に増減
🔢 配列の基本例:
fruits = ["りんご", "バナナ", "みかん"]
→ 果物を3つまとめて管理
fruits[0] で「りんご」を取得
fruits[1] で「バナナ」を取得
⚠️ 重要:配列は「0番目」から始まる!
配列[0] = 1番目の要素
配列[1] = 2番目の要素
配列[2] = 3番目の要素
💡 なぜ0から?
コンピュータは0から数える習慣があります。慣れるまで混乱しやすいので注意!
🎮 ゲーム内での配列活用:
holes配列: 6つの穴の位置(x, y座標)をまとめて管理
holeSprites配列: 6つの穴の画像をまとめて管理
moles配列: 出現中のモグラをまとめて管理
💫 実例: holes[0] = 左上の穴、holes[5] = 右下の穴
⚡ 配列の便利な操作:
push() - 配列の最後に新しい要素を追加
length - 配列の要素数を取得
forEach() - 配列の全要素に対して処理を実行
find() - 条件に合う要素を検索

Step 3: モグラ出現機能の実装

GameScene.tsに以下のメソッドを追加します。

// GameScene.tsのcreate()メソッドの後に追加

// ランダムな穴からモグラを出現させる
private spawnMole(): void {
    // ランダムな穴を選択
    const randomIndex = Phaser.Math.Between(0, this.holes.length - 1);
    const holePos = this.holes[randomIndex];

    // すでにその穴にモグラがいるかチェック
    const existingMole = this.moles.find(mole => 
        mole.x === holePos.x && mole.y === holePos.y
    );

    // すでにモグラがいる場合は何もしない
    if (existingMole) {
        return;
    }

    // モグラを作成
    const mole = this.add.image(holePos.x, holePos.y, 'mole');
    
    // モグラをクリック可能にする
    mole.setInteractive();
    
    // クリックされたときの処理
    mole.on('pointerdown', () => {
        this.hitMole(mole);
    });

    // モグラを配列に追加
    this.moles.push(mole);

    // 3秒後にモグラを自動で消す
    this.time.delayedCall(3000, () => {
        this.removeMole(mole);
    });
}

// モグラがクリックされたときの処理
private hitMole(mole: Phaser.GameObjects.Image): void {
    // 画像をやられた状態に変更
    mole.setTexture('mole_hit');
    
    // クリックを無効にする
    mole.disableInteractive();
    
    // 0.5秒後にモグラを消す
    this.time.delayedCall(500, () => {
        this.removeMole(mole);
    });
}

// モグラを画面から削除
private removeMole(mole: Phaser.GameObjects.Image): void {
    // 配列からモグラを削除
    const index = this.moles.indexOf(mole);
    if (index > -1) {
        this.moles.splice(index, 1);
    }
    
    // 画面からモグラを削除
    mole.destroy();
}

🎯 重要なポイント:

  • setInteractive() - オブジェクトをクリック可能にする
  • on('pointerdown') - クリックイベントを監視
  • setTexture() - 表示する画像を変更
  • destroy() - オブジェクトを完全に削除

Step 4: ゲーム設定ファイルの更新

src/main.tsを以下のように更新します。

// src/main.ts
import './style.css';
import * as Phaser from 'phaser';
import { GameScene } from './scenes/GameScene';

// ゲーム設定
const config: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'app',
    backgroundColor: '#4ade80',
    scene: [GameScene]
};

// ゲームを開始
new Phaser.Game(config);

⚙️ 設定の説明:

  • type: Phaser.AUTO - 最適なレンダリング方法を自動選択
  • width/height - ゲーム画面のサイズ
  • parent: 'app' - HTMLの要素IDを指定
  • scene: [GameScene] - 使用するシーンを指定

Step 5: ゲームの実行

すべてのコードを保存したら、ターミナルで開発サーバーを起動します。

npm run dev

🎮 ゲームの動作確認:

  • ブラウザでゲームが表示される
  • 緑色の背景に6つの穴が配置される
  • 2秒ごとにランダムな穴からモグラが出現
  • モグラをクリックするとやられた画像に変わる
  • 3秒経つか、クリックされると自動で消える

🐛 うまく動かない場合:

ブラウザのデベロッパーツール(F12)でエラーメッセージを確認してください。 よくある問題は画像ファイルのパスが間違っていることです。

⚡ より柔軟な実装:DeltaTimeを使った時間管理

タイマーイベントの代わりに、変数とdeltaTimeを使ってモグラの表示時間を管理する方法を学びましょう。 この方法はより細かい制御が可能で、フレームレートに依存しない安定した動作を実現できます。

// より柔軟なGameScene.tsの実装
export class GameScene extends Phaser.Scene {
    // モグラの穴の位置を定義
    private holes: { x: number, y: number }[] = [
        { x: 200, y: 200 },
        { x: 400, y: 200 },
        { x: 600, y: 200 },
        { x: 200, y: 350 },
        { x: 400, y: 350 },
        { x: 600, y: 350 },
    ];

    // 穴のスプライトを格納する配列
    private holeSprites: Phaser.GameObjects.Image[] = [];
    
    // モグラの情報を格納するオブジェクト
    private moles: {
        sprite: Phaser.GameObjects.Image;
        timeAlive: number;        // 生存時間(ミリ秒)
        maxLifeTime: number;     // 最大生存時間
        isHit: boolean;          // クリックされたかどうか
        hitTime: number;         // クリックされた時間
    }[] = [];

    // 時間管理用の変数
    private lastSpawnTime: number = 0;      // 最後にモグラを出現させた時間
    private spawnInterval: number = 2000;    // モグラ出現間隔(ミリ秒)
    private moleLifeTime: number = 3000;     // モグラの生存時間(ミリ秒)
    private hitDisplayTime: number = 500;   // やられた状態の表示時間

    create(): void {
        // 背景色を設定
        this.cameras.main.setBackgroundColor('#4ade80');

        // 穴を配置
        this.holes.forEach(holePos => {
            const hole = this.add.image(holePos.x, holePos.y, 'hole');
            this.holeSprites.push(hole);
        });

        // 初回のモグラ出現時間を設定
        this.lastSpawnTime = this.time.now;
    }

    // 毎フレーム実行される更新処理
    update(time: number, delta: number): void {
        // モグラの出現チェック
        if (time - this.lastSpawnTime >= this.spawnInterval) {
            this.spawnMole();
            this.lastSpawnTime = time;
        }

        // 各モグラの生存時間を更新
        this.moles.forEach((moleData, index) => {
            moleData.timeAlive += delta;

            // クリックされていない場合
            if (!moleData.isHit) {
                // 生存時間が超過した場合、モグラを削除
                if (moleData.timeAlive >= moleData.maxLifeTime) {
                    this.removeMoleAt(index);
                    return; // 配列が変更されるので処理を中断
                }
            } else {
                // クリックされた場合、一定時間後に削除
                if (time - moleData.hitTime >= this.hitDisplayTime) {
                    this.removeMoleAt(index);
                    return;
                }
            }
        });
    }

    // ランダムな穴からモグラを出現させる
    private spawnMole(): void {
        // ランダムな穴を選択
        const randomIndex = Phaser.Math.Between(0, this.holes.length - 1);
        const holePos = this.holes[randomIndex];

        // すでにその穴にモグラがいるかチェック
        const existingMole = this.moles.find(moleData => 
            moleData.sprite.x === holePos.x && moleData.sprite.y === holePos.y
        );

        if (existingMole) {
            return;
        }

        // モグラを作成
        const mole = this.add.image(holePos.x, holePos.y, 'mole');
        mole.setInteractive();
        
        // モグラデータを作成
        const moleData = {
            sprite: mole,
            timeAlive: 0,
            maxLifeTime: this.moleLifeTime,
            isHit: false,
            hitTime: 0
        };

        // クリックイベント
        mole.on('pointerdown', () => {
            this.hitMole(moleData);
        });

        this.moles.push(moleData);
    }

    // モグラがクリックされたときの処理
    private hitMole(moleData: any): void {
        moleData.sprite.setTexture('mole_hit');
        moleData.sprite.disableInteractive();
        moleData.isHit = true;
        moleData.hitTime = this.time.now;
    }

    // 指定したインデックスのモグラを削除
    private removeMoleAt(index: number): void {
        const moleData = this.moles[index];
        moleData.sprite.destroy();
        this.moles.splice(index, 1);
    }
}

🔧 DeltaTime実装の利点:

  • フレームレート独立 - 60FPSでも30FPSでも同じ速度で動作
  • 精密な制御 - ミリ秒単位での細かい時間管理
  • パフォーマンス向上 - 毎フレーム実行で無駄なコールバックを削減
  • 拡張性 - 複雑な時間ベースの処理に対応しやすい

📊 重要な概念の解説:

  • time - ゲーム開始からの経過時間(ミリ秒)
  • delta - 前フレームからの経過時間(ミリ秒)
  • update() - 毎フレーム自動実行されるメソッド
  • timeAlive += delta - オブジェクトの生存時間を蓄積

🚀 さらなる改良アイデア

ゲーム要素の追加

  • • スコアの表示
  • • 効果音の追加
  • • 難易度の調整(出現間隔)
  • • 時間制限の追加

ビジュアルの改善

  • • アニメーションの追加
  • • パーティクルエフェクト
  • • 背景画像の設定
  • • レスポンシブ対応

🎉 お疲れ様でした!

この発展編では、配列を使った複数オブジェクト管理やDeltaTimeによる高度な時間制御など、 より実践的なゲーム開発テクニックを学習しました。基本編で学んだ概念を発展させ、 本格的なゲーム開発に必要なスキルを身につけることができたはずです。

今回学んだ発展的な概念:

  • 配列を使った複数オブジェクトの効率的な管理
  • 動的なスプライトの生成と削除
  • DeltaTimeによるフレームレート独立な時間管理
  • 複雑なゲーム状態の追跡と更新
  • 実践的なTypeScriptプログラミング手法