PhaserとFirebaseを用いたゲームランキングの構築

Phaserは、HTML5ゲーム開発のための強力な2Dフレームワークです。本記事では、Phaserで作成したゲームにGoogle Firebase Realtime Databaseを利用して、リアルタイムなランキングシステムを構築する方法を解説します。複雑なバックエンド開発なしに、プレイヤーのスコアを同期し、競争心を刺激する機能を追加できます。

開発環境の準備

まず、FirebaseプロジェクトとPhaser開発環境を準備します。

Firebaseプロジェクトの作成

  1. Firebaseコンソール (https://console.firebase.google.com/) にアクセスし、新しいプロジェクトを作成します。
  2. プロジェクト設定でWebアプリケーションを追加し、生成される初期設定オブジェクト(apiKeyauthDomain など)を記録しておきます。これは後でFirebase SDKを初期化する際に必要になります。
  3. Realtime Databaseを有効にし、テスト用にセキュリティルールを一時的にtrueに設定しておくと開発がスムーズです。最終的には安全なルールを設定します。

Phaserプロジェクトの概要

基本的なPhaserプロジェクトがセットアップ済みであることを前提とします。通常、Node.jsとnpmがインストールされており、Phaserのゲームコード(JavaScriptまたはTypeScript)を含むHTMLファイルが存在する状態です。

Firebase SDKの組み込み

ゲームのHTMLファイルにFirebase SDKを追加し、プロジェクト設定で初期化します。ここでは、Firebase SDKの互換バージョンを使用します。

<!-- HTMLファイルの <body> タグの末尾、Phaserゲームスクリプトの前に配置 -->
<script src="https://www.gstatic.com/firebasejs/9.22.1/firebase-app-compat.js"></script>
<script src="https://www.gstatic.com/firebasejs/9.22.1/firebase-database-compat.js"></script>
<script>
  // Firebase設定オブジェクト (YOUR_...を実際の値に置き換える)
  const firebaseConfig = {
    apiKey: "YOUR_API_KEY",
    authDomain: "your-project.firebaseapp.com",
    databaseURL: "https://your-project-default-rtdb.firebaseio.com", // Realtime DatabaseのURL
    projectId: "your-project-id",
    storageBucket: "your-project.appspot.com",
    messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
    appId: "YOUR_APP_ID"
  };

  // Firebaseを初期化
  firebase.initializeApp(firebaseConfig);
  const gameDatabase = firebase.database(); // Realtime Databaseへの参照
</script>

リアルタイムランキング機能の実装

スコアの送信

プレイヤーのスコアをFirebase Realtime Databaseに保存する関数を作成します。

/**
 * プレイヤーのゲームスコアをFirebase Realtime Databaseに保存します。
 * @param {string} playerIdentifier プレイヤー名またはID
 * @param {number} scoreValue 獲得スコア
 */
function submitGameScore(playerIdentifier, scoreValue) {
  // 'gameScores' パスにスコアを保存
  const scoreEntriesRef = gameDatabase.ref('gameScores');
  scoreEntriesRef.push({
    player: playerIdentifier,
    score: scoreValue,
    recordedAt: Date.now() // スコアが記録されたタイムスタンプ
  }).then(() => {
    console.log("スコアが正常に記録されました。");
  }).catch(error => {
    console.error("スコアの記録中にエラーが発生しました:", error);
  });
}

ランキングデータの取得と表示

Firebaseからトップスコアを取得し、指定されたHTML要素に表示する関数です。on('value')リスナーを使用することで、データが変更されるたびにランキングがリアルタイムで更新されます。

/**
 * Firebaseから最新のトップスコアを取得し、指定されたHTML要素に表示します。
 * @param {string} targetElementId ランキングを表示するHTML要素のID
 */
function retrieveAndDisplayRanking(targetElementId = 'rankingBoard') {
  const scoreEntriesRef = gameDatabase.ref('gameScores').orderByChild('score').limitToLast(10); // スコアでソートし、上位10件を取得
  const rankingContainer = document.getElementById(targetElementId);

  if (!rankingContainer) {
    console.error(`指定されたID '${targetElementId}' の要素が見つかりません。`);
    return;
  }
  rankingContainer.innerHTML = '<li>ランキングデータを読み込み中...</li>'; // ロード中の表示

  scoreEntriesRef.on('value', (snapshot) => {
    const allScores = snapshot.val();
    if (!allScores) {
      rankingContainer.innerHTML = '<li>まだスコアがありません。最初のプレイヤーになりましょう!</li>';
      return;
    }

    // スコアを降順でソート
    const sortedScoreList = Object.entries(allScores)
                                  .map(([key, value]) => ({ ...value, id: key })) // Firebaseキーも保持
                                  .sort((a, b) => b.score - a.score); // 最高スコアが上に来るようにソート

    rankingContainer.innerHTML = ''; // コンテナの内容をクリア
    sortedScoreList.forEach((entry, index) => {
      const listItem = document.createElement('li');
      listItem.className = 'ranking-entry'; // ランキングのエントリ用クラス
      listItem.innerHTML = `
        <span class="entry-rank">${index + 1}.</span>
        <span class="entry-player">${entry.player}</span>
        <span class="entry-score">${entry.score}点</span>
      `;
      rankingContainer.appendChild(listItem);
    });
    console.log("ランキングが更新されました。");
  }, (error) => {
    console.error("ランキングデータの取得中にエラーが発生しました:", error);
    rankingContainer.innerHTML = '<li>ランキングの読み込みに失敗しました。</li>';
  });
}

Phaserゲームへの統合

Phaserゲームのシーン内に、ランキング表示をトリガーするUI要素(ボタンなど)と、テスト用のスコア送信ロジックを追加します。

ランキングの表示には、Phaserキャンバスの上にHTML要素を重ねる一般的な方法を推奨します。CSSでposition: absolute;などを使って<div><ul>要素をPhaserキャンバスの上に配置します。

// Phaserゲームシーンの定義
class MainGameScene extends Phaser.Scene {
  constructor() {
    super({ key: 'MainGameScene' });
  }

  create() {
    // 背景や他のゲーム要素の初期化...

    // ランキング表示ボタンの作成
    const rankingButton = this.add.text(50, 50, 'ランキングを見る', {
      fontSize: '28px',
      fill: '#ffdb58',
      backgroundColor: '#4a2c00',
      padding: { x: 15, y: 8 }
    }).setInteractive({ cursor: 'pointer' }); // ホバー時にカーソルをポインターに

    rankingButton.on('pointerdown', () => {
      this.displayRankingOverlay(); // ランキング表示オーバーレイをアクティブにする
    });

    // テスト用にランダムなスコアを一定時間後に送信
    this.time.addEvent({
      delay: 7000, // 7秒後に実行
      callback: () => {
        const testPlayerName = 'PhaserDev-' + Math.floor(Math.random() * 100);
        const testScore = Math.floor(Math.random() * 2000) + 500; // 500点から2500点の間のスコア
        submitGameScore(testPlayerName, testScore);
        console.log(`テストスコアを送信: ${testPlayerName}, ${testScore}`);
      },
      loop: false // 一度だけ実行
    });
  }

  /**
   * ランキング表示用のオーバーレイをPhaserシーン上に作成し、
   * 外部HTML要素にランキングデータを表示するよう指示します。
   */
  displayRankingOverlay() {
    // Phaserシーン上にランキング表示用の背景パネルを作成
    const overlayBg = this.add.rectangle(
      this.cameras.main.width / 2,
      this.cameras.main.height / 2,
      450, // 幅
      550, // 高さ
      0x1a1a1a, // 背景色
      0.9 // 透明度
    ).setScrollFactor(0); // カメラのスクロールに影響されないように固定

    this.add.text(
      this.cameras.main.width / 2,
      this.cameras.main.height / 2 - 220,
      'ゲームランキング',
      { fontSize: '38px', fill: '#00ccff' }
    ).setOrigin(0.5).setScrollFactor(0);

    // 外部HTML要素にランキングを表示することを促すテキスト
    this.add.text(
        this.cameras.main.width / 2,
        this.cameras.main.height / 2,
        'ランキングはHTML要素に表示されます。\n(ID: rankingBoard)',
        { fontSize: '20px', fill: '#ffffff', align: 'center' }
    ).setOrigin(0.5).setScrollFactor(0);

    // 閉じるボタン
    const closeBtn = this.add.text(
        this.cameras.main.width / 2 + 180,
        this.cameras.main.height / 2 - 250,
        'X',
        { fontSize: '28px', fill: '#ff0000', backgroundColor: '#333333', padding: { x: 10, y: 5 } }
    ).setInteractive({ cursor: 'pointer' }).setOrigin(0.5).setScrollFactor(0);

    closeBtn.on('pointerdown', () => {
        // オーバーレイのPhaser要素を全て破棄
        overlayBg.destroy();
        this.children.getChildren().filter(c => c.getData('isRankingOverlay')).forEach(c => c.destroy());
        // 外部HTMLランキングボードも非表示にする (CSSなどで制御)
        document.getElementById('rankingBoard').style.display = 'none';
    });

    // オーバーレイに関連するPhaser要素に識別子を付与し、一括で管理・破棄できるようにする
    overlayBg.setData('isRankingOverlay', true);
    this.children.getChildren().slice(-3).forEach(c => c.setData('isRankingOverlay', true)); // 最近追加されたテキスト要素にも適用

    // 外部のHTML要素にランキングを表示する関数を呼び出す
    // CSSで display: block; に設定してランキングを表示
    document.getElementById('rankingBoard').style.display = 'block';
    retrieveAndDisplayRanking('rankingBoard');
  }
}

// Phaserゲームの初期化設定
const config = {
  type: Phaser.AUTO, // CanvasまたはWebGLを自動選択
  width: 800,
  height: 600,
  scene: MainGameScene, // ゲームで使用するシーン
  parent: 'game-container', // ゲームキャンバスを埋め込むHTML要素のID
  dom: {
    createContainer: true // DOM要素をPhaser上に配置する場合に必要
  }
};

// Phaserゲームインスタンスの作成
const game = new Phaser.Game(config);

HTML側では、Phaserゲームキャンバスと同じページにランキング表示用のコンテナ(例: <ul>要素)を用意し、CSSで適切に配置する必要があります。

<style>
  body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #222; }
  #game-container { position: relative; width: 800px; height: 600px; overflow: hidden; } /* Phaserキャンバスのコンテナ */
  #rankingBoard {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%); /* 中央に配置 */
    width: 400px;
    height: 500px;
    background-color: rgba(0, 0, 0, 0.9);
    color: white;
    padding: 20px;
    box-sizing: border-box;
    border-radius: 10px;
    overflow-y: auto; /* スコアが多い場合にスクロール可能にする */
    z-index: 1000; /* Phaserキャンバスより手前に表示 */
    list-style: none; /* リストのマークを削除 */
    margin: 0;
    display: none; /* 初期は非表示 */
    font-family: Arial, sans-serif;
  }
  #rankingBoard li {
    padding: 10px 0;
    border-bottom: 1px solid #444;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  #rankingBoard li:last-child {
    border-bottom: none;
  }
  .entry-rank {
    width: 40px;
    text-align: center;
    font-weight: bold;
    color: #00ccff;
  }
  .entry-player {
    flex-grow: 1;
    padding-left: 10px;
    color: #ffdb58;
  }
  .entry-score {
    width: 80px;
    text-align: right;
    font-weight: bold;
    color: #ffffff;
  }
</style>

<div id="game-container"></div>
<ul id="rankingBoard"></ul>

Firebaseセキュリティルールの設定

不正なスコアの送信を防ぐために、Firebase Realtime Databaseのセキュリティルールを設定することが非常に重要です。以下のルールは、認証されたユーザーのみがスコアを書き込めるようにし、データの形式を検証します。

{
  "rules": {
    "gameScores": { // 'gameScores' パスに対するルール
      ".read": true, // 誰でもランキングを読み取り可能
      ".write": "auth != null", // 認証されたユーザーのみ書き込み可能
      "$recordId": { // 各スコアエントリのユニークID
        "player": {
          ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 15" // プレイヤー名の文字列と長さ検証
        },
        "score": {
          ".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 999999" // スコアの数値と範囲検証
        },
        "recordedAt": {
          ".validate": "newData.isNumber()" // タイムスタンプは数値であること
        },
        "$other": { ".validate": false } // 未定義のフィールドは許可しない
      }
    }
  }
}

".write": "auth != null" の設定は、ユーザー認証機能を実装している場合に有効です。認証を実装しない場合は、より厳密なルール(例: 特定のサーバーからのみ書き込み可能にする Cloud Functions など)が必要になります。上記ルールはあくまで例であり、実際のアプリケーションの要件に合わせて調整してください。

タグ: Phaser Firebase Realtime Database HTML5 Game Development javascript

7月27日 02:50 投稿