学習記事一覧 · Unity

【学習】Collider と Rigidbody で部位別当たり判定 ― 矢から逃げる猫(アフター編)

これまでの学習では、

  • 第1回(ビフォー編) では、円同士の距離d < r1 + r2)だけで当たり判定を書き、最小構成で遊べるようにしました。

しかし、円の近似しかできない右腕に当たったか左腕かのような 部位が表現しにくい・対象が増えると 矢のスクリプトが太る といった限界もありました。

Unity 2D の物理(Collider2D / Rigidbody2D)子オブジェクト に当たり判定を分けると、形を Inspector で調整しつつ、イベント駆動で部位ごとに反応を書けます。

今回は第1回の前提を書き換えOnTriggerEnter2D ベースの構成に作り替えます。

シリーズ


今日のゴール

  1. 矢の落下と当たり判定を 物理エンジンとコライダ に寄せる(座標手計算の主役を降ろす)
  2. 猫の 右腕・左腕 にそれぞれ Collider2D を持たせ、部位ごとに衝突イベントを受け取る
  3. コードを 毎フレームの距離判定 から、衝突時のコールバック中心に切り替える

座標計算版の限界(おさらい)

前回の当たり判定は、概ね次の 7 行でした。

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) { /* 当たり */ }
  • 猫を 1 つの円でしか表現しにくい
  • 右手 / 左手の区別がそのままではできない
  • 敵・障害物が増えると ArrowController 側の列挙が増える
  • 落下を transform.Translate 前提にした場合、物理挙動は自前

これらの「置き場所の問題」と「形の問題」を、2D 物理+子オブジェクトで整理します。


2D 物理の用語(最低限)

用語 役割
Collider2D 当たり判定のBox, Circle, Polygon など)。形だけ。
Rigidbody2D このオブジェクトを物理シミュレーションに乗せる。重力・力・衝突応答の入口。
Is Trigger OFF … 衝突して押し合う/ON … 通り抜けつつ、当たったイベントを取る。
OnCollisionEnter2D / OnTriggerEnter2D 衝突が起きたとき Unity が呼ぶコールバック。Trigger が絡めば主に OnTriggerEnter2D

中級者向けメモ
OnTriggerEnter2D を使う一般的な条件のひとつとして、ペアのどちらかに Rigidbody2D がある必要があります。今回はに Rigidbody2D を付けて満たします。


全体像(ビフォーからの差分)

player_0               ← PlayerController.cs(変更なし)
├─ RightArm           ★新規: BoxCollider2D + RightArmCollisionDetector.cs
└─ LeftArm            ★新規: BoxCollider2D + LeftArmCollisionDetector.cs
arrowPrefab
├─ SpriteRenderer     (既存)
├─ ArrowController.cs ★変更: 当たり判定と DecreaseHp・Destroy(衝突時)を削る
├─ BoxCollider2D      ★新規: IsTrigger = true
└─ Rigidbody2D        ★新規: Kinematic または Dynamic

猫の本体ではなく、という子 GameObject に役割を分けるのが、この回の設計の要です。腕側は 非 Trigger、矢側 Trigger + Rigidbody2D という定番の組み合わせで、OnTriggerEnter2Dのスクリプトで受け取ります。


ステップ 1: 矢 Prefab に Collider2D と Rigidbody2D

arrowPrefab に次を Add Component します。

  • BoxCollider2D
    • Size: スプライトに合わせる(例: 0.65 × 1.03
    • Is Trigger: ON
  • Rigidbody2D
    • 後述のとおり DynamicKinematic

Is Trigger を ON にする理由

矢は「当たったら消える」用途で、猫を物理的に押し退けたいわけではない、という前提なら、Trigger+OnTriggerEnter2D が扱いやすいです。

Rigidbody2D の Body Type

Body Type 重力 他から押される 用途のイメージ
Dynamic ある される 重力で落とす・物理で動かす
Kinematic なし 基本されない スクリプトで主に動かしつつ、Collider イベントを取る
Static なし されない 動かない地形など

学習のしやすさのため、本稿の流れは Kinematic + ビフォー同様の transform.Translate 落下を推奨します(移行が最小)。Dynamic にして gravityScale だけで落とす方法も併記します。


ステップ 2: 右腕・左腕の子オブジェクト

player_0 の子として 空の GameObject を2つ作り、名前を RightArm / LeftArm にします。それぞれ BoxCollider2D を付け、Offset / Size を調整します(例)。

オブジェクト Offset (X, Y) Size (W, H) Is Trigger
RightArm (3, 0.5) (4, 1) OFF
LeftArm (-3, 0.5) (4, 1) OFF

Offsetplayer_0 のローカル座標です。親が動けば子も一緒に動きます。

プロジェクト例 CatEscapeMultiCollider: Hierarchy で player_0 の子に RightArm / LeftArm を置き、RightArm 選択時の BoxCollider2D(Offset・Size)と RightArmCollisionDetector

なぜ「腕」に分けるのか

1 つの GameObjectBoxCollider2D を複数足すことはできますが、どの Collider かOnTriggerEnter2D の引数で見分けて分岐させる手間が出がちです。子に分けておけば、腕に付けたスクリプトOnTriggerEnter2D が呼ばれた=その部位に当たった、と 1:1 に対応づけられます。これが「複数の Collider を扱う」本稿の実体です。


ステップ 3: 腕ごとの検出スクリプト(まずはログ)

RightArm / LeftArmそれぞれ次を付け、再生で矢が当たると Console に出るか確認します。

// Assets/Scripts/RightArmCollisionDetector.cs
using UnityEngine;
public class RightArmCollisionDetector : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("右手に当たった");
    }
}
// Assets/Scripts/LeftArmCollisionDetector.cs
using UnityEngine;
public class LeftArmCollisionDetector : MonoBehaviour
{
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("左手に当たった");
    }
}

OnTriggerEnter2D が呼ばれる条件(おさらい)

今回の「矢 vs 右腕」では次のとおりです。

  • 2 つの Collider2D が重なった
  • そのペアのどちらかに Rigidbody2D がある(が Kinematic の Rigidbody2D)
  • そのペアのどちらかの Collider2DIsTrigger = trueが ON。腕は OFF)

よくある詰まり: 片方が Trigger なのに OnCollisionEnter2D と書く/OnTriggerEnter2D を落とすRigidbody2D がない など。

補足: 腕に Rigidbody2D を付けなくてよいのは、矢側で上記条件を満たしているからです(元稿の中級者向けメモと同趣旨です)。


ステップ 4: ArrowController から座標計算を抜く

ArrowController から 円判定・DecreaseHp・衝突時の Destroy を外し、落下と画面外だけ残します。

// Assets/Scripts/ArrowController.cs(アフター例)
using UnityEngine;
public class ArrowController : MonoBehaviour
{
    void Update()
    {
        // フレームごとに等速で落下させる
        transform.Translate(0, -0.1f, 0);
        // 画面外に出たらオブジェクトを破棄する
        if (transform.position.y < -5.0f)
        {
            Destroy(gameObject);
        }
    }
}
  • playerGameObject.Find は不要になれば削除します。
  • 当たり判定の責務は、矢から 腕側へ移ります(イベント駆動の分岐)。

Dynamic + 重力にした場合は、Update 内の Translate をやめ、Inspector の Gravity Scale などに任せられます。


ステップ 5: HP 減少と矢の破棄を腕側から

GameDirectorDecreaseHp()そのまま使い、呼び出し元を腕に移します。

// Assets/Scripts/RightArmCollisionDetector.cs(完成形の例)
using UnityEngine;
public class RightArmCollisionDetector : MonoBehaviour
{
    GameDirector director;
    void Start()
    {
        director = GameObject.Find("GameDirector").GetComponent<GameDirector>();
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("右手に当たった");
        director.DecreaseHp();
        Destroy(collision.gameObject);
    }
}
// Assets/Scripts/LeftArmCollisionDetector.cs(完成形の例)
using UnityEngine;
public class LeftArmCollisionDetector : MonoBehaviour
{
    GameDirector director;
    void Start()
    {
        director = GameObject.Find("GameDirector").GetComponent<GameDirector>();
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log("左手に当たった");
        director.DecreaseHp();
        Destroy(collision.gameObject);
    }
}
  • collision … ぶつかってきた相手の Collider2Dcollision.gameObjectその GameObject(矢)を Destroy できます。
  • GameDirector の取得は Start一度(毎フレーム Find しないのが目安です)。

中級者向け: 共通化の例

public class ArmCollisionDetector : MonoBehaviour
{
    [SerializeField] string armLabel = "腕";
    GameDirector director;
    void Start()
    {
        director = GameObject.Find("GameDirector").GetComponent<GameDirector>();
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        Debug.Log($"{armLabel}に当たった");
        director.DecreaseHp();
        Destroy(collision.gameObject);
    }
}

SerializeField でラベルやダメージ倍率を Inspector から与えると、同じスクリプトを複数部位に貼る展開に向きます。


ビフォー / アフター比較

観点 ビフォー(座標) アフター(Collider 等)
円固定の近似 Box 等、Inspector で形を詰められる
部位 1 式では区別しにくい 子オブジェクト+スクリプトの分離
主なコード 毎フレームの距離判定 衝突イベント中心
拡張 列挙が Arrow 側に寄りがち 部位を増やす=子と Collider を足すのが素直
物理 自前 Rigidbody2D に委ねられる余地

トラブルシューティング

  • Console に何も出ない … 矢の Rigidbody2DTrigger 設定コールバック名(2D 付き)を確認する。
  • 貫通する/反発が強いIsTrigger の意図(通過+イベントか、衝突応答か)を整理する。
  • 矢の着弾位置がズレる … 生成位置のワールド座標と、カメラ・背景のオフセットの関係を確認する。
  • 子の Collider 位置がおかしい … 親の Scale が 1 以外なら、子のオフセット感覚が変わります。

重要ポイント

  • 座標と距離の1行(d < r1 + r2)が、Unity 上ではCollider2D + Rigidbody2D + イベントの組に置き換わる、と捉えると移行の意味がはっきりします。
  • 子オブジェクトは、当たり判定の単位=責務の単位に揃えやすい、という点で有効です。
  • Rigidbody2D / Trigger / 2D コールバックの三つセットは、同様の 2D ミニゲーム全般の土台になります。

(参考)Animator でモーションの終端を揃える

Collider だけの話の先では、攻撃アニメーション当たりが有効なフレームを揃えたい場面が出てきます。Animator では、Attack などのステートから Exit へ抜ける遷移に Has Exit Time を付け、Exit Time(例: 0.75 = 再生のおおよそ 75% 付近)で いつ次の遷移に入れるか を制御できます。攻撃の手が振り切れるまで ステートを抜かせない といった モーションの終端合わせに使います。遷移先が Exit の場合、プレビューに「遷移先のステートがない」旨の表示が出ることはあり得ます(設定上の例としては問題ありません)。

Animator: Attack から Exit への遷移。Has Exit Time と Exit Time 0.75、Transition Duration 0.1 秒の例


発展アイデア

  • 部位ごとに 減少 HP を変える(右腕 5%・左腕 15% など)
  • ArmCollisionDetector 一つに寄せ、Inspector だけ差し替え
  • LayerLayer Collision Matrix で、矢と反応する相手を限定する
  • 矢の Rigidbody2D を Dynamic にし、gravityScale回転を試す

第1回からここまでで、「座標の一行」から「コンポーネントの組み合わせ」への橋を一度通したことになります。同じシーン上で、デバッグ表示(Gizmo)で Collider を可視化しながら触ると理解が深まります。