学習記事一覧 · Unity

【学習】Unity 2D 座標計算で作る当たり判定 ― 矢から逃げる猫(ビフォー編)

これまでの学習では、

  • Unity の画面操作や GameObject / コンポーネント の基本
  • スクリプトで位置を動かす こと

に慣れてきた段階だと想定します。

しかし、当たり判定を学ぶと「とりあえず Collider を付ける」に頼りがちで、何をしているのかが曖昧なまま進んでしまいがちです。

まず座標と距離の計算だけで当たり判定を書いてみると、あとで Collider に置き換えたときに「何が楽になったか」を言葉にしやすくなります。

今回は Collider も Rigidbody も使わず、円同士の距離で「矢と猫の衝突」を表現し、最小構成で遊べるところまで作ります。第2回で Collider ベースの設計へ作り替えます。

シリーズ


今日作るもの

画面上端から矢が落ち、猫が左右の矢印キーで避けるミニゲームです。

  • 矢が猫に当たると 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.cs

Prefab は 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物理を介さない移動のため、今回は壁すり抜けでも問題ありません。

中級者向けメモ

押しっぱなしで滑らかに動かすなら isPressedTime.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 … 次の生成まで待つ秒数。deltaTime.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 点です。

  1. 毎フレーム下方向へ移動(自由落下ではなく自前の Translate
  2. Y が -5 未満なら Destroy で消す
  3. 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回(アフター編) へ。