Phaserは、HTML5ゲーム開発のための強力な2Dフレームワークです。本記事では、Phaserで作成したゲームにGoogle Firebase Realtime Databaseを利用して、リアルタイムなランキングシステムを構築する方法を解説します。複雑なバックエンド開発なしに、プレイヤーのスコアを同期し、競争心を刺激する機能を追加できます。
開発環境の準備
まず、FirebaseプロジェクトとPhaser開発環境を準備します。
Firebaseプロジェクトの作成
- Firebaseコンソール (https://console.firebase.google.com/) にアクセスし、新しいプロジェクトを作成します。
- プロジェクト設定でWebアプリケーションを追加し、生成される初期設定オブジェクト(
apiKey、authDomainなど)を記録しておきます。これは後でFirebase SDKを初期化する際に必要になります。 - 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 など)が必要になります。上記ルールはあくまで例であり、実際のアプリケーションの要件に合わせて調整してください。