ゲーム開発入門② 座標と当たり判定を理解しよう
左端・右端の計算から始め、四角形の当たり判定を4段階で読み解きます。数値例、端が触れる場合、一度だけ加点する実行例、練習の解答を掲載します。
コインの上を通ったのに拾えない。逆に、まだ離れているように見えるのに当たる。ゲームを作っていると、位置の扱いで「あれ?」となる場面が出てきます。
今回は、四角形のプレイヤーとコインを題材に、位置の表し方から当たり判定まで進みます。いきなり長い条件式を覚える必要はありません。まず、左右の端を足し算で求めるところからです。
XとYで位置を表す
2Dの位置には、横方向の X座標と縦方向の Y座標を使います。この記事では、ブラウザーのCanvasなどでよく使う「左上が (0, 0)、右と下が正」の座標を採用します。
(0, 0) ───────→ Xが増える
│
│
↓ Yが増える
数学のグラフでは上へ行くほどYが増えることが多いので、ここは少し戸惑いますよね。「ゲームだから必ず下が正」ではなく、使うエンジンや画面の座標の決まりを確認してください。
位置が (40, 20) なら、左端から右へ40、上端から下へ20です。右へ10進むならXだけを増やします。
let x = 40;
let y = 20;
x += 10;
console.log(x, y);
出力は 50 20。上下へ動いていないのでYは20のままです。
| 動き | 計算 |
|---|---|
| 右へ進む | x = x + 距離 |
| 左へ進む | x = x - 距離 |
| 下へ進む | y = y + 距離 |
| 上へ進む | y = y - 距離 |
1回の移動距離を速さと経過時間から求める方法は、ゲームループの記事で説明しています。
位置だけでなく大きさも持つ
点には大きさがありませんが、キャラクターには幅と高さがあります。ここでは x と y を四角形の左上とし、次の4つで表します。
const player = {
x: 40,
y: 20,
width: 32,
height: 24
};
これはJavaScriptのオブジェクトです。x や width のような名前と値をまとめています。player.x と書くと、この中のX座標を使えます。
端の位置を計算すると、次のようになります。
| 端 | 計算 | 値 |
|---|---|---|
| 左 | x |
40 |
| 右 | x + width |
72 |
| 上 | y |
20 |
| 下 | y + height |
44 |
大切なのは、「右端=幅」ではないことです。幅32は大きさ、右端72は場所です。位置と大きさを混ぜると、画面の左側では動くのに右側で判定がおかしい、といった不具合につながります。
エンジンによっては位置が画像の中心を表します。その場合の左端は x - width / 2 です。この記事の式を使うなら、左上基準へそろえてから比べます。
四角形の重なりを調べる
回転していない、軸に平行な四角形の判定を AABB判定と呼びます。Axis-Aligned Bounding Boxの略で、「座標の軸に沿った囲みの箱」という意味です。
1. まず横方向だけを見る
プレイヤーの横範囲は40〜72、コインの横範囲を60〜76とします。
- プレイヤーの左40は、コインの右76より左
- プレイヤーの右72は、コインの左60より右
この両方が成立すると、横方向に共通する部分があります。40〜72と60〜76なら、60〜72が重なっています。
2. 縦方向も同じように見る
プレイヤーの縦範囲は20〜44、コインは30〜46とします。
- プレイヤーの上20は、コインの下46より上
- プレイヤーの下44は、コインの上30より下
縦も重なっています。横だけ重なっていても、上下に離れていれば衝突しません。 横と縦の両方を見る必要があるんですね。
3. 4つの条件を関数にする
function isHit(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
関数は、処理を名前でまとめて再利用する仕組みです。a と b に2つの四角形を渡すと、return で結果を返します。
&& は「かつ」。4条件がすべて正しいときだけ true、どれか1つでも違えば false になります。true と false は、はい・いいえを表す真偽値です。
この方法では、幅と高さが0より大きく、同じ座標の基準を使う四角形を想定しています。
4. 端がぴったり触れたら?
プレイヤーの右端72と、コインの左端72が一致する場合を考えます。
式は 72 > 72 になるので false。今回の関数は、面積を持って重なる場合だけ当たりとし、端に触れただけでは当たりにしません。
端への接触も含めたいなら、< と > をそれぞれ <= と >= に変える選択肢があります。どちらが正しいかは、ゲームのルール次第です。式の意味を決めずに記号を変えると、壁際などの動作が不安定になるので、先に決めておきましょう。
当たった後の処理を書く
判定の式だけではゲームは変わりません。「当たったら何をするか」を別に書きます。
次は、コンソールまたはNode.jsでそのまま実行できる例です。前の関数も含めて、独立したコードにしています。
function isHit(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
);
}
const player = { x: 40, y: 20, width: 32, height: 24 };
const coin = { x: 60, y: 30, width: 16, height: 16, active: true };
let score = 0;
function collectCoin() {
if (coin.active && isHit(player, coin)) {
score += 10;
coin.active = false;
}
}
collectCoin();
console.log(score, coin.active);
collectCoin();
console.log(score, coin.active);
10 false
10 false
1回目はコインが有効で重なっているので、10点を加算して active を false にします。2回目は最初の条件が成立せず、10点のままです。
非表示にするだけでは足りない
「画像を消したから大丈夫」と思いやすいのですが、表示と判定は別です。visible = false にしても、独自に書いた判定コードはその値を見なければ動き続けます。
この例では、判定に参加するかを表す active を条件で確認することで二重取得を防いでいます。描画する側でも、active が真のコインだけを描くようにします。
ゲームループは何度も回るので、一度だけ起こす処理には「もう処理済みか」という情報が必要になるんですね。
判定と見た目を分ける
画像の透明な部分まで含めた四角形で判定すると、離れて見えるのに当たることがあります。そこで、見えない判定用の形を別に置きます。こうした形はコライダー、用途によってヒットボックスなどと呼ばれます。
例えば64×64の画像に、中央の40×48だけを判定範囲として使うなら、左上を画像の位置から右へ12、下へ8ずらします。
ただし、小さくすれば必ず遊びやすいわけではありません。地面に立つ足元、攻撃が届く範囲、コインの取りやすさでは目的が違います。開発中は判定枠も描いて、見た目と重なりを確認すると原因が見つけやすくなります。
この判定でできないこと
AABBは分かりやすい方法ですが、万能ではありません。
- 回転した形や円の輪郭を、ぴったり判定するものではない
- 重なった後、どの方向へ押し戻すかまでは決めない
- 1回の更新で薄い壁を飛び越えると、重なりを見逃すことがある
最後の問題をすり抜けと呼ぶことがあります。現在位置だけを見るため、前の位置では壁の左、次の位置では壁の右にいると、途中の接触が分かりません。
移動を細かく分ける、移動経路を判定する、エンジンの連続衝突判定を使うなどの対応があります。小さな学習用なら、まず速さを控えめにし、「重なり判定」と「通った経路の判定」は違うと覚えておけば十分です。
練習:右端の座標を求める
問題1:プレイヤーの x が40、width が32のとき、右端はいくつでしょうか。
問題2:今回の isHit で、コインを x = 72、y = 30 にした場合は当たりますか。コインの大きさは16×16のままとします。
模範解答
- 右端は
40 + 32 = 72 false。横方向は端が触れるだけだからです
解説
縦方向は重なっていますが、player.x + player.width > coin.x が 72 > 72 となり成立しません。4条件すべてが必要なので、全体も false になります。
もしコインのXを71へ変えると、横に1だけ重なるため true になります。このように境目の前後を試す確認を境界値のテストと呼びます。ちょうど触れる・1だけ離れる・1だけ重なる、を調べると記号の間違いを見つけやすくなります。
次に読む記事と出典
当たり判定ができたら、タイトル・プレイ・ゲームオーバーの切り替えへ進みましょう。得点や体力を「どの画面で更新するか」まで分けると、ゲームらしい形になります。