第10章: ボス戦AI・ウェーブ完了とゲームループ
いよいよ最終章です。 第9章までで、数千体のモンスターの大群、自動攻撃兵器、XPBDPlugin による群衆物理、ジェム回収、HUD、そして効果音と画面振動を備えた本格ゲームの基礎がすべて完成しました。
第10章では、この大群サバイバーのクライマックスを飾る**巨大ボス「スウォーム・タイタン(Swarm Titan)」**を出現させ、**Utility AI によるマルチフェーズ行動パターン(突進・全方位弾幕・護衛召喚)**と、完全な勝敗判定(ゲームループ)を実装してゲームを完成させます。
1. ボス降臨の演出 (The Boss Arrival)
サバイバル時間が一定に達した時(または一定レベル到達時)、周囲の雑魚敵をすべて吹き飛ばす衝撃波とともに、画面外から巨大ボスが降臨します。 ここでも、InstanceBufferArena による SoA (Structure of Arrays) 操作を活用し、オブジェクトの生成・破棄によるガベージコレクション(GC)を回避します。
typescript
export class SwarmSurvivorScene extends Scene {
// ... 前章までのプロパティ ...
// ボス管理
private bossActive = false;
private bossId = -1;
private bossHp = 0;
private readonly bossMaxHp = 1500;
private bossAiTimer = 0;
private bossPhase = 0; // 0: 追跡, 1: 突進チャージ, 2: 弾幕ノヴァ
// ボスHPゲージ用テキスト
private bossHpText!: Text;
/**
* ボスを降臨させる
*/
private spawnBoss(): void {
this.bossActive = true;
this.bossHp = this.bossMaxHp;
// 1. 周囲の雑魚敵を一掃する衝撃波 (カメラトラウマ最大)
this.addTrauma(0.8);
for (let i = this.enemyCount - 1; i >= 0; i--) {
// 物理制約と空間ハッシュの登録解除
this.physics.removeBody(this.enemyIds[i]);
this.morton.remove(this.enemyIds[i]);
this.arena.free(this.enemyIds[i]);
}
this.enemyCount = 0;
// 2. 画面上部から巨大ボスを生成 (サイズ 72px)
this.bossId = this.arena.allocate();
this.arena.posX[this.bossId] = 960 / 2;
this.arena.posY[this.bossId] = -100;
this.arena.scale[this.bossId] = 72;
this.arena.tint[this.bossId] = 0xa855f7; // 妖しい魔界の紫
// ボス用の物理ボディ登録 (XPBDPlugin)
this.physics.addBody(this.bossId, { mass: 1000, radius: 36 });
// ボス用の空間ハッシュ登録 (MortonPlugin)
this.morton.insert(this.bossId, 960 / 2, -100, 36);
// 3. ボスHPバーを表示
this.bossHpText = this.add.text(960 / 2 - 120, 50, '--- SWARM TITAN: 1500 / 1500 ---', {
fontSize: 16,
color: 0xc084fc,
});
// 画面中央上部へ入場するTweenアニメーション (SoAベースのTween)
this.tweens.add({
targets: { id: this.bossId },
props: { y: 120 },
duration: 1200
});
console.log('警告: 巨大ボス [SWARM TITAN] が出現しました!');
}2. ボスの Utility AI パターン
ボスは単調に追尾するだけでなく、AIタイマーによって3つの異なる攻撃行動(フェーズ)を交互に繰り出します:
- フェーズ 0: 重力追尾 (Tracking): プレイヤーの周囲を巨大な体でじわじわと追い詰める。
- フェーズ 1: 超音速突進 (Dash Charge): プレイヤーの現在位置をロックオンし、画面端まで高速突進。
- フェーズ 2: 全方位弾幕ノヴァ (Radial Barrage): 立ち止まり、360度全方位へ16発の弾幕を発射。
update() ループ内で new キーワードを使用しないよう、算術演算はすべて基本型のまま処理します。
typescript
private updateBossAI(dt: number): void {
if (!this.bossActive) return;
this.bossAiTimer += dt;
const bx = this.arena.posX[this.bossId];
const by = this.arena.posY[this.bossId];
const px = this.player.x;
const py = this.player.y;
// 3秒ごとにフェーズを切り替え
if (this.bossAiTimer >= 3.0) {
this.bossAiTimer = 0;
this.bossPhase = (this.bossPhase + 1) % 3;
if (this.bossPhase === 2) {
// フェーズ 2: 16方向放射弾幕を発射!
this.fireBossRadialBarrage(bx, by);
}
}
// フェーズごとの行動
if (this.bossPhase === 0) {
// 通常追跡
const dx = px - bx;
const dy = py - by;
const dist = Math.hypot(dx, dy);
if (dist > 1.0) {
const speed = 70 * dt;
this.arena.posX[this.bossId] += (dx / dist) * speed;
this.arena.posY[this.bossId] += (dy / dist) * speed;
}
} else if (this.bossPhase === 1) {
// 突進 (プレイヤー方向へ高速移動)
const dx = px - bx;
const dy = py - by;
const dist = Math.hypot(dx, dy);
if (dist > 1.0) {
const dashSpeed = 220 * dt;
this.arena.posX[this.bossId] += (dx / dist) * dashSpeed;
this.arena.posY[this.bossId] += (dy / dist) * dashSpeed;
}
}
// 移動後の空間ハッシュ(MortonPlugin)更新
this.morton.update(this.bossId, this.arena.posX[this.bossId], this.arena.posY[this.bossId]);
// ボスHP表示の更新
this.bossHpText.text = `--- SWARM TITAN: ${Math.max(0, Math.ceil(this.bossHp))} / ${this.bossMaxHp} ---`;
}
/**
* ボスの全方位弾幕 (16方向)
*/
private fireBossRadialBarrage(bx: number, by: number): void {
this.addTrauma(0.3);
const numBullets = 16;
for (let i = 0; i < numBullets; i++) {
const angle = (i * Math.PI * 2) / numBullets;
// 敵弾を生成 (SoA)
const bulletId = this.arena.allocate();
this.arena.posX[bulletId] = bx;
this.arena.posY[bulletId] = by;
this.arena.scale[bulletId] = 14;
this.arena.tint[bulletId] = 0xf43f5e; // 危険なローズレッド
// 速度ベクトルを設定して弾プール等に登録
const vx = Math.cos(angle) * 300;
const vy = Math.sin(angle) * 300;
// ... 弾の移動処理用配列に登録 ...
}
}3. 勝敗判定と完全なゲームループ
ボスを撃破した際の「VICTORY(完全勝利)」と、プレイヤーのHPが尽きた際の「GAME OVER」を実装します。
typescript
// ゲームオーバー/クリア判定
private isGameOver = false;
private checkGameConditions(): void {
if (this.isGameOver) return;
// 1. プレイヤー死亡
if (this.playerHp <= 0) {
this.isGameOver = true;
this.add.text(960 / 2 - 150, 540 / 2 - 40, 'GAME OVER', {
fontSize: 48,
color: 0xef4444,
});
this.add.text(960 / 2 - 130, 540 / 2 + 30, 'Press SPACE to Restart', {
fontSize: 20,
color: 0xffffff,
});
return;
}
// 2. ボス撃破 (勝利!)
if (this.bossActive && this.bossHp <= 0) {
this.isGameOver = true;
// 物理・ハッシュから除外してボスを消滅
this.physics.removeBody(this.bossId);
this.morton.remove(this.bossId);
this.arena.free(this.bossId);
this.bossActive = false;
this.addTrauma(1.0); // 最大の画面揺れ!
this.sfx.playLevelUp(); // 勝利ファンファーレ
this.add.text(960 / 2 - 140, 540 / 2 - 40, 'VICTORY!', {
fontSize: 48,
color: 0xfacc15, // 輝く黄金色
});
this.add.text(960 / 2 - 160, 540 / 2 + 30, 'You survived the Swarm Titan!', {
fontSize: 20,
color: 0x38bdf8,
});
}
// スペースキーでリスタート
if (this.isGameOver && this.input.isKeyJustPressed('Space')) {
// シーンを再起動
this.scene.start('SwarmSurvivorScene');
}
}4. チュートリアル完結!学んだことの振り返り
おめでとうございます。 数万体のモンスターが押し寄せる大群サバイバーゲームを、PlutoEngine v1.2.1 上で完全にゼロから構築しました。
このチュートリアルを通じて、現代のWebGPUゲームエンジンの技術を実践的に習得しました:
- ゼロアロケーション(Zero-Allocation):
newの排除により実行時GCを完全に無くし、滑らかな描画を永続化。 - データ指向設計(SoA / Structure of Arrays):
InstanceBufferArenaによる Float32Array バッファでのCPUキャッシュ局所性の最大化。 - WebGPU レンダリングと RenderGraph: 高速なバッチ処理と最新のグラフィックスAPI(
createGraphicsDevice)を活用した描画パイプライン。 - モートン空間ハッシュ(MortonPlugin): 空間分割とZオーダー曲線を用い、$O(N^2)$ の当たり判定を $O(1)$ スケールに短縮。
- XPBD(Extended Position Based Dynamics):
XPBDPluginによる、数千体が重なり合わない安定したリアル群衆物理。 - Web Audio プロシージャル合成: 外部ファイル不要のゼロ遅延サウンド。
- フライウェイト・パターン: 開発者フレンドリーなAPIと高速内部処理の両立。
5. 次のステップ: あなたのゲームを世界へ
PlutoEngine をさらに発展させるためのアイデアをご紹介します:
- カスタムスプライトシートのロード:
this.load.imageでスプライトシートを読み込み、this.texturesで管理してキャラクターに適用。 - 新しい武器の追加: 稲妻チェーンライトニング、炎の結界、巨大鎌など。
- ポアソンディスクサンプリング:
@pluto-engine/poissonで障害物(木や岩)を自然に配置。 - Verlet IK 触手:
@pluto-engine/verlet-ikでボスにうねる多関節の触手を実装。