19 KiB
宇宙ゲームを作ろう パート4: レーザーの追加と衝突検知
講義前のクイズ
スター・ウォーズでルークのプロトン魚雷がデス・スターの排気口に命中した瞬間を思い出してください。その正確な衝突検知が銀河の運命を変えました!ゲームでも衝突検知は同じように機能します。オブジェクトが相互作用するタイミングとその結果を決定します。
このレッスンでは、宇宙ゲームにレーザー兵器を追加し、衝突検知を実装します。NASAのミッションプランナーが宇宙船の軌道を計算してデブリを回避するように、ゲームオブジェクトが交差するタイミングを検知する方法を学びます。これを段階的に分解して進めていきます。
最終的には、レーザーが敵を破壊し、衝突がゲームイベントを引き起こす戦闘システムが完成します。この衝突原理は、物理シミュレーションからインタラクティブなウェブインターフェースまで、さまざまな場面で使用されています。
✅ 最初に作られたコンピューターゲームについて少し調べてみましょう。その機能はどのようなものでしたか?
衝突検知
衝突検知は、アポロ月着陸船の近接センサーのように機能します。常に距離をチェックし、オブジェクトが近づきすぎたときにアラートを発します。ゲームでは、このシステムがオブジェクトの相互作用とその結果を決定します。
今回使用する方法では、すべてのゲームオブジェクトを矩形として扱います。これは航空管制システムが航空機を追跡する際に簡略化された幾何学的形状を使用するのと似ています。この矩形の方法は基本的に見えるかもしれませんが、計算効率が高く、ほとんどのゲームシナリオでうまく機能します。
矩形の表現
すべてのゲームオブジェクトには座標境界が必要です。これは、火星探査機パスファインダーが火星表面で位置をマッピングした方法に似ています。以下のように境界座標を定義します:
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width
}
}
これを分解してみましょう:
- 上端: オブジェクトが垂直方向に開始する位置(y座標)
- 左端: 水平方向に開始する位置(x座標)
- 下端: y座標に高さを加えることで終了位置を取得
- 右端: x座標に幅を加えることで完全な境界を取得
交差アルゴリズム
矩形の交差を検知するには、ハッブル宇宙望遠鏡が視野内で天体が重なっているかどうかを判断する方法と似たロジックを使用します。このアルゴリズムは分離をチェックします:
function intersectRect(r1, r2) {
return !(r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top);
}
分離テストはレーダーシステムのように機能します:
- 矩形2が完全に矩形1の右側にあるか?
- 矩形2が完全に矩形1の左側にあるか?
- 矩形2が完全に矩形1の下側にあるか?
- 矩形2が完全に矩形1の上側にあるか?
これらの条件がすべて真でない場合、矩形は重なっているはずです。このアプローチは、レーダーオペレーターが2つの航空機が安全な距離にあるかどうかを判断する方法に似ています。
オブジェクトのライフサイクル管理
レーザーが敵に命中した場合、両方のオブジェクトをゲームから削除する必要があります。しかし、ループの途中でオブジェクトを削除するとクラッシュを引き起こす可能性があります。これはアポロ誘導コンピュータのような初期のコンピュータシステムで学んだ教訓です。その代わりに、「削除マーク」アプローチを使用して、フレーム間で安全にオブジェクトを削除します。
以下は削除をマークする方法です:
// Mark object for removal
enemy.dead = true;
このアプローチが機能する理由:
- オブジェクトを「死んだ」とマークしますが、すぐには削除しません
- 現在のゲームフレームを安全に終了できます
- すでに削除されたものを使用しようとしてクラッシュすることを防ぎます!
次に、次のレンダーサイクルの前にマークされたオブジェクトをフィルタリングします:
gameObjects = gameObjects.filter(go => !go.dead);
このフィルタリングが行うこと:
- 「生きている」オブジェクトだけの新しいリストを作成
- 「死んだ」とマークされたものを捨てる
- ゲームをスムーズに動作させる
- 破壊されたオブジェクトが蓄積してメモリが膨張するのを防ぐ
レーザーのメカニクスを実装する
ゲーム内のレーザー弾は、スター・トレックのフォトン魚雷と同じ原理で動作します。それらは直線的に移動し、何かに当たるまで進みます。スペースバーを押すたびに、新しいレーザーオブジェクトが画面上を移動します。
これを実現するためには、いくつかの異なる要素を調整する必要があります:
実装する主要なコンポーネント:
- レーザーオブジェクトを作成し、ヒーローの位置から生成
- キーボード入力を処理してレーザーの生成をトリガー
- レーザーの移動とライフサイクルを管理
- レーザー弾の視覚的表現を実装
発射速度制御の実装
無制限の発射速度はゲームエンジンを圧倒し、ゲームプレイを簡単にしすぎてしまいます。実際の武器システムも同様の制約に直面します。USSエンタープライズのフェイザーでさえ、ショット間に充電時間が必要でした。
スペースバー連打を防ぎつつ、操作性を維持するクールダウンシステムを実装します:
class Cooldown {
constructor(time) {
this.cool = false;
setTimeout(() => {
this.cool = true;
}, time);
}
}
class Weapon {
constructor() {
this.cooldown = null;
}
fire() {
if (!this.cooldown || this.cooldown.cool) {
// Create laser projectile
this.cooldown = new Cooldown(500);
} else {
// Weapon is still cooling down
}
}
}
クールダウンの仕組み:
- 作成時、武器は「熱い」(まだ発射できない)状態になる
- タイムアウト期間後、「冷たい」(発射可能)状態になる
- 発射前に「武器は冷たいか?」を確認
- スペースバー連打を防ぎつつ、操作性を維持
✅ 宇宙ゲームシリーズのレッスン1を参照して、クールダウンについて思い出してください。
衝突システムの構築
既存の宇宙ゲームコードを拡張して衝突検知システムを作成します。国際宇宙ステーションの自動衝突回避システムのように、ゲームはオブジェクトの位置を継続的に監視し、交差に応じて反応します。
前回のレッスンのコードを基に、オブジェクトの相互作用を管理する具体的なルールを追加します。
💡 プロのヒント: レーザーのスプライトはすでにアセットフォルダに含まれており、コード内で参照されています。すぐに実装可能です。
実装する衝突ルール
追加するゲームメカニクス:
- レーザーが敵に命中: レーザー弾が敵オブジェクトに当たると敵が破壊される
- レーザーが画面境界に到達: レーザーが画面上端に到達すると削除される
- 敵とヒーローの衝突: 両方のオブジェクトが交差すると破壊される
- 敵が画面下端に到達: 敵が画面下端に到達するとゲームオーバー
開発環境のセットアップ
朗報です - ほとんどの基盤はすでに準備されています!すべてのゲームアセットと基本構造が your-work サブフォルダに用意されており、衝突機能を追加する準備が整っています。
プロジェクト構造
-| assets
-| enemyShip.png
-| player.png
-| laserRed.png
-| index.html
-| app.js
-| package.json
ファイル構造の理解:
- ゲームオブジェクトに必要なスプライト画像を含む
- メインHTMLドキュメントとJavaScriptアプリケーションファイルを含む
- ローカル開発サーバーのパッケージ構成を提供
開発サーバーの起動
プロジェクトフォルダに移動し、ローカルサーバーを起動します:
cd your-work
npm start
このコマンドシーケンス:
- 作業プロジェクトフォルダにディレクトリを変更
- ローカルHTTPサーバーを
http://localhost:5000で起動 - ゲームファイルをテストと開発のために提供
- 自動リロードでライブ開発を可能にする
ブラウザを開き、http://localhost:5000 にアクセスして、ヒーローと敵が画面に描画されている現在のゲーム状態を確認してください。
実装のステップバイステップ
NASAがボイジャー宇宙船をプログラムした体系的なアプローチのように、衝突検知を段階的に構築していきます。
1. 矩形衝突境界を追加
まず、ゲームオブジェクトに境界を記述する方法を教えます。GameObject クラスに以下のメソッドを追加してください:
rectFromGameObject() {
return {
top: this.y,
left: this.x,
bottom: this.y + this.height,
right: this.x + this.width,
};
}
このメソッドが達成すること:
- 正確な境界座標を持つ矩形オブジェクトを作成
- 位置と寸法を使用して下端と右端を計算
- 衝突検知アルゴリズムに準備されたオブジェクトを返す
- すべてのゲームオブジェクトに標準化されたインターフェースを提供
2. 交差検知を実装
次に、2つの矩形が重なっているかどうかを判断できる衝突検知関数を作成します:
function intersectRect(r1, r2) {
return !(
r2.left > r1.right ||
r2.right < r1.left ||
r2.top > r1.bottom ||
r2.bottom < r1.top
);
}
このアルゴリズムが機能する方法:
- 矩形間の4つの分離条件をテスト
- 分離条件が真の場合は
falseを返す - 分離が存在しない場合は衝突を示す
- 効率的な交差テストのために否定ロジックを使用
3. レーザー発射システムを実装
ここからが本番です!レーザー発射システムを設定しましょう。
メッセージ定数
まず、ゲーム内の異なる部分が互いに通信できるようにメッセージタイプを定義します:
KEY_EVENT_SPACE: "KEY_EVENT_SPACE",
COLLISION_ENEMY_LASER: "COLLISION_ENEMY_LASER",
COLLISION_ENEMY_HERO: "COLLISION_ENEMY_HERO",
これらの定数が提供するもの:
- アプリケーション全体でイベント名を標準化
- ゲームシステム間の一貫した通信を可能にする
- イベントハンドラ登録時のタイプミスを防ぐ
キーボード入力処理
キーイベントリスナーにスペースキー検知を追加します:
} else if(evt.keyCode === 32) {
eventEmitter.emit(Messages.KEY_EVENT_SPACE);
}
この入力ハンドラ:
- キーコード32を使用してスペースキー押下を検知
- 標準化されたイベントメッセージを送信
- 分離された発射ロジックを可能にする
イベントリスナーの設定
initGame() 関数に発射動作を登録します:
eventEmitter.on(Messages.KEY_EVENT_SPACE, () => {
if (hero.canFire()) {
hero.fire();
}
});
このイベントリスナー:
- スペースキーイベントに応答
- 発射クールダウン状態を確認
- 許可されている場合にレーザー生成をトリガー
レーザーと敵の相互作用の衝突処理を追加します:
eventEmitter.on(Messages.COLLISION_ENEMY_LASER, (_, { first, second }) => {
first.dead = true;
second.dead = true;
});
この衝突ハンドラ:
- 両方のオブジェクトを削除対象としてマーク
- 衝突後の適切なクリーンアップを保証
4. レーザークラスを作成
上方向に移動し、自身のライフサイクルを管理するレーザー弾を実装します:
class Laser extends GameObject {
constructor(x, y) {
super(x, y);
this.width = 9;
this.height = 33;
this.type = 'Laser';
this.img = laserImg;
let id = setInterval(() => {
if (this.y > 0) {
this.y -= 15;
} else {
this.dead = true;
clearInterval(id);
}
}, 100);
}
}
このクラスの実装:
- GameObjectを拡張して基本機能を継承
- レーザースプライトに適切な寸法を設定
setInterval()を使用して自動的に上方向に移動- 画面上端に到達した際に自己破壊を処理
- アニメーションタイミングとクリーンアップを管理
5. 衝突検知システムを実装
包括的な衝突検知関数を作成します:
function updateGameObjects() {
const enemies = gameObjects.filter(go => go.type === 'Enemy');
const lasers = gameObjects.filter(go => go.type === "Laser");
// Test laser-enemy collisions
lasers.forEach((laser) => {
enemies.forEach((enemy) => {
if (intersectRect(laser.rectFromGameObject(), enemy.rectFromGameObject())) {
eventEmitter.emit(Messages.COLLISION_ENEMY_LASER, {
first: laser,
second: enemy,
});
}
});
});
// Remove destroyed objects
gameObjects = gameObjects.filter(go => !go.dead);
}
この衝突システム:
- 効率的なテストのためにゲームオブジェクトをタイプ別にフィルタリング
- すべてのレーザーとすべての敵を交差テスト
- 交差が検出された場合に衝突イベントを送信
- 衝突処理後に破壊されたオブジェクトをクリーンアップ
⚠️ 重要: 衝突検知を有効にするために、
updateGameObjects()をwindow.onloadのメインゲームループに追加してください。
6. ヒーロークラスにクールダウンシステムを追加
ヒーロークラスを強化して発射メカニクスと発射速度制限を追加します:
class Hero extends GameObject {
constructor(x, y) {
super(x, y);
this.width = 99;
this.height = 75;
this.type = "Hero";
this.speed = { x: 0, y: 0 };
this.cooldown = 0;
}
fire() {
gameObjects.push(new Laser(this.x + 45, this.y - 10));
this.cooldown = 500;
let id = setInterval(() => {
if (this.cooldown > 0) {
this.cooldown -= 100;
} else {
clearInterval(id);
}
}, 200);
}
canFire() {
return this.cooldown === 0;
}
}
強化されたヒーロークラスの理解:
- クールダウンタイマーをゼロで初期化(発射可能状態)
- ヒーロー船の上に位置するレーザーオブジェクトを作成
- 連続発射を防ぐためにクールダウン期間を設定
- 間隔ベースの更新を使用してクールダウンタイマーを減少
canFire()メソッドを通じて発射状態を確認
実装のテスト
あなたの宇宙ゲームは、完全な衝突検知と戦闘メカニクスを備えています。🚀 新しい機能をテストしてください:
- 矢印キーで移動して操作性を確認
- スペースバーでレーザーを発射 - クールダウンが連打を防ぐことに注目
- レーザーが敵に命中する様子を観察 - 削除がトリガーされる
- 破壊されたオブジェクトがゲームから消える様子を確認
衝突検知システムを、宇宙船の航行やロボット工学を導く数学的原理を使用して成功裏に実装しました。
GitHub Copilot Agent チャレンジ 🚀
Agentモードを使用して以下のチャレンジを完了してください:
説明: 衝突検知システムを強化し、ランダムに生成されるパワーアップを実装してヒーロー船が収集した際に一時的な能力を提供するようにします。
プロンプト: GameObject を拡張する PowerUp クラスを作成し、ヒーローとパワーアップ間の衝突検知を実装します。少なくとも2種類のパワーアップを追加してください:発射速度を上げる(クールダウンを短縮する)ものと、一時的なシールドを作成するもの。ランダムな間隔と位置でパワーアップを生成するロジックを含めてください。
🚀 チャレンジ
爆発を追加しましょう!Space Art リポジトリ のゲームアセットを確認し、レーザーがエイリアンに命中した際に爆発を追加してみてください。
講義後のクイズ
復習と自己学習
これまでのゲームで使用している間隔を試してみましょう。間隔
免責事項:
この文書はAI翻訳サービスCo-op Translatorを使用して翻訳されています。正確性を追求しておりますが、自動翻訳には誤りや不正確な部分が含まれる可能性があります。元の言語で記載された文書を正式な情報源としてお考えください。重要な情報については、専門の人間による翻訳を推奨します。この翻訳の使用に起因する誤解や誤認について、当方は一切の責任を負いません。