【学習】Unity 2D 座標計算で作る当たり判定 ― 矢から逃げる猫(ビフォー編)
これまでの学習では、
- Unity の画面操作や GameObject / コンポーネント の基本
- スクリプトで位置を動かす こと
に慣れてきた段階だと想定します。
しかし、当たり判定を学ぶと「とりあえず Collider を付ける」に頼りがちで、何をしているのかが曖昧なまま進んでしまいがちです。
まず座標と距離の計算だけで当たり判定を書いてみると、あとで Collider に置き換えたときに「何が楽になったか」を言葉にしやすくなります。
今回は Collider も Rigidbody も使わず、円同士の距離で「矢と猫の衝突」を表現し、最小構成で遊べるところまで作ります。第2回で Collider ベースの設計へ作り替えます。
シリーズ
- 第1回(本記事): 座標計算による当たり判定 ― 最小構成で動かす
- 第2回: Collider / Rigidbody / 子オブジェクトで部位別の当たり判定(アフター編)
今日作るもの
画面上端から矢が落ち、猫が左右の矢印キーで避けるミニゲームです。
- 矢が猫に当たると HP ゲージが 10% ずつ 減る
- 矢は 画面外に出るか、猫に当たると消える
- 当たり判定は 2 点間の距離と半径の和 だけ(物理コンポーネントは使わない)
完成形の仕様
- プレイヤーは猫。左右矢印キーで 左右に 3 ユニット 移動
- 画面上端から矢が ランダムな X 座標 で落ちてくる
- 矢が猫に当たると HP ゲージが 10% ずつ 減る
- 矢は 画面外に出るか、猫に当たると消える
登場するオブジェクトとスクリプト
GameScene
├─ Main Camera
├─ background_0
├─ Canvas
│ └─ hpGauge(Image / Filled)
├─ EventSystem
├─ GameDirector ← GameDirector.cs
├─ ArrowGenerator ← ArrowGenerator.cs
└─ player_0 ← PlayerController.csPrefab は 1 つだけです。
| 配置 | 内容 |
|---|---|
Assets/Prefabs/arrowPrefab.prefab |
ArrowController.cs を付与。Collider / Rigidbody は付けない(SpriteRenderer のみ可) |
1. プレイヤーの移動
player_0 にスプライトを貼り、次のスクリプトを付けます。
// Assets/Scripts/PlayerController.cs
using UnityEngine;
using UnityEngine.InputSystem; // 入力を検知するために必要!!
public class PlayerController : MonoBehaviour
{
void Start()
{
Application.targetFrameRate = 60;
}
void Update()
{
// 左矢印が押された時
if (Keyboard.current.leftArrowKey.wasPressedThisFrame)
{
transform.Translate(-3, 0, 0); // 左に「3」動かす
}
// 右矢印が押された時
if (Keyboard.current.rightArrowKey.wasPressedThisFrame)
{
transform.Translate(3, 0, 0); // 右に「3」動かす
}
}
}ポイント
Keyboard.current.xxx.wasPressedThisFrameは 新 Input System です。Package Manager で Input System を有効化している前提です。transform.Translateは 物理を介さない移動のため、今回は壁すり抜けでも問題ありません。
中級者向けメモ
押しっぱなしで滑らかに動かすなら isPressed と Time.deltaTime の組み合わせが一般的です。本稿では 一歩ずつ の動きに合わせ wasPressedThisFrame のままにします。
2. 矢を降らせる
ArrowGenerator という空の GameObject を作り、次のスクリプトを付けます。Inspector の arrowPrefab に Prefab をアサインします。
// Assets/Scripts/ArrowGenerator.cs
using UnityEngine;
public class ArrowGenerator : MonoBehaviour
{
public GameObject arrowPrefab;
float span = 1.0f;
float delta = 0;
void Update()
{
this.delta += Time.deltaTime;
if (this.delta > this.span)
{
this.delta = 0;
GameObject go = Instantiate(arrowPrefab);
int px = Random.Range(-6, 7);
go.transform.position = new Vector3(px, 7, 0);
}
}
}ポイント
span… 次の生成まで待つ秒数。deltaにTime.deltaTimeを足し、超えたら生成してリセット、という定番パターンです。Random.Range(-6, 7)(int 版)は 上限 7 は含みません(-6 ~ 6)。float 版は挙動が違うので注意します。Instantiate(arrowPrefab)… Prefab から新しいGameObjectを生成します。
3. 矢本体と当たり判定(この回の主役)
arrowPrefab に次の ArrowController を付けます。
// Assets/Scripts/ArrowController.cs
using UnityEngine;
public class ArrowController : MonoBehaviour
{
GameObject player;
void Start()
{
this.player = GameObject.Find("player_0");
}
void Update()
{
// フレームごとに等速で落下させる
transform.Translate(0, -0.1f, 0);
// 画面外に出たらオブジェクトを破棄する
if (transform.position.y < -5.0f)
{
Destroy(gameObject);
}
// 当たり判定
Vector2 p1 = transform.position; // 矢の中心座標
Vector2 p2 = this.player.transform.position; // プレイヤの中心座標
Vector2 dir = p1 - p2;
float d = dir.magnitude;
float r1 = 0.5f; // 矢の半径
float r2 = 1.0f; // プレイヤの半径
if (d < r1 + r2)
{
// 監督スクリプトにプレイヤと衝突したことを伝える
GameObject director = GameObject.Find("GameDirector");
director.GetComponent<GameDirector>().DecreaseHp();
// 衝突した場合は矢を消す
Destroy(gameObject);
}
}
}このスクリプトの役割は次の 3 点です。
- 毎フレーム下方向へ移動(自由落下ではなく自前の
Translate) - Y が -5 未満なら
Destroyで消す - 2 円として、中心距離が 半径の和未満なら衝突とみなす
当たり判定の中身
Vector2 p1 = transform.position; // 矢の中心座標
Vector2 p2 = this.player.transform.position; // プレイヤの中心座標
Vector2 dir = p1 - p2;
float d = dir.magnitude;
float r1 = 0.5f; // 矢の半径
float r2 = 1.0f; // プレイヤの半径
if (d < r1 + r2)
{
// 当たり
}2 点間の距離は、ベクトル p1 - p2 の 長さ(magnitude)です。2 円が重なる条件は 中心距離 d が r1 + r2 未満のとき、つまり d < r1 + r2 です。
┌──── 矢(半径 r1)
●
╱
╱ ← 距離 d
●
└──── 猫(半径 r2)
衝突 ⇔ d < r1 + r2中級者向けメモ
magnitudeの代わりにsqrMagnitudeと(r1+r2)*(r1+r2)の比較にすると sqrt を省略でき、微最適化に向きます(本稿は可読性優先)。GameObject.Findを毎フレーム呼ぶのは重く、名前変更にも弱いので、本番では参照をStartで一度取る等が望ましいです(GameDirectorも同様の改善候補)。
4. HP ゲージと GameDirector
// Assets/Scripts/GameDirector.cs
using UnityEngine;
using UnityEngine.UI; // UIを使うので忘れずに追加
public class GameDirector : MonoBehaviour
{
GameObject hpGauge;
void Start()
{
this.hpGauge = GameObject.Find("hpGauge");
}
public void DecreaseHp()
{
this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
}
}hpGaugeは Canvas 下の Image。Image Type を Filled にしておき、fillAmount(0~1)で減少を表します。DecreaseHp()はArrowControllerから呼ばれる公開メソッドです。
プログラムの流れ(衝突まで)
ArrowGenerator: 一定間隔で arrowPrefab を生成
↓
ArrowController: 毎フレーム落下・画面外なら Destroy
↓
2 円の距離判定 → 衝突なら GameDirector.DecreaseHp() と矢の Destroy
↓
GameDirector: fillAmount を減らすここまででできること・限界
できること: この時点で 遊べる状態になります。矢を避けきれなければ HP が減ります。
座標計算ならではの制約
| 制約 | 内容 |
|---|---|
| 形の表現 | 猫を 半径 1 の円としてしか扱えない。横長のスプライトも円近似になる |
| 部位 | どこに当たったかを区別しにくい。部位ごとに式を足す形になる |
| 拡張性 | 判定対象が増えると 矢側のコードに座標・半径の列挙が溜まる |
| 物理 | 跳ね返り・重力などは 自作が前提(今回は等速 Translate) |
次回(アフター編)では、BoxCollider2D + Rigidbody2D と 子オブジェクト(右腕・左腕) に置き換え、OnTriggerEnter2D で部位ごとに衝突を受け取る流れに作り直します。
重要ポイント
- 当たり判定の心臓部は 2 点間距離と 2 円(半径の和) の比較である、という図式です。
- 物理コンポーネントなしでも、距離さえわかれば衝突の「代用」は書けます。
- 代わりに 形・部位・拡張・物理の面では限界がはっきりするので、第2回との対比の土台になります。
発展アイデア
- 押しっぱなし移動に変え、移動量を
Time.deltaTimeで調整する sqrMagnitudeによる判定に書き換え、コスト意識を比較するGameObject.Findをやめ、SerializeField や FindObjectsOfType のキャッシュに置き換える
次は 第2回(アフター編) へ。