Unityを触り始めた頃って、つい「とりあえず全部Updateに書けば動くしOK!」となりがちですよね。
移動処理、ジャンプ処理、入力処理、アニメーション、当たり判定…全部を1つの巨大スクリプトに押し込んでしまうと、少し仕様を変えたいだけでもコードの海を泳ぐハメになります。

特に「穴を見つけたら自動でジャンプする」みたいな処理をプレイヤーに足すとき、
既存の移動コードに if 文を足しまくって、どんどん God クラス化していく…というのはよくあるパターンです。

そこでこの記事では、

  • 「穴を検知してジャンプする」機能だけに責務を絞った
  • 他の移動コンポーネントと組み合わせて使える

そんなコンポーネント指向な実装として、「JumpGap」コンポーネントを紹介します。
プレイヤーや敵、パトロールするAIなどにポン付けできる、穴飛び越え専用コンポーネントです。

【Unity】穴を見たら自動ジャンプ!「JumpGap」コンポーネント

「JumpGap」は、キャラクターの進行方向の足元をRaycastでチェックし、
そこが「床ではなく穴」だと判断されたら、自動でジャンプを実行するコンポーネントです。

ジャンプそのものの物理挙動(Rigidbodyに上向きの力を加えるなど)も内部に持っていますが、
「穴検知」と「ジャンプのトリガー」に責務を絞っているため、他の移動処理と組み合わせやすい構造になっています。


フルソースコード


using UnityEngine;

/// <summary>
//  進行方向の足元をRaycastでチェックして、
//  先が「床なし(穴)」なら自動でジャンプするコンポーネント。
//  - Rigidbodyを使ったシンプルなジャンプ実装付き
//  - 横移動は別コンポーネントに任せる前提
//  - 2D/3Dどちらでも使えるが、ここでは3D想定(X-Z平面で移動)
//</summary>
[RequireComponent(typeof(Rigidbody))]
public class JumpGap : MonoBehaviour
{
    // ====== インスペクタで調整するパラメータ群 ======

    [Header("ジャンプ設定")]
    [Tooltip("ジャンプの初速度(上向きの速度)。値を大きくすると高く跳びます。")]
    [SerializeField] private float jumpVelocity = 7f;

    [Tooltip("地面と判定するLayer。穴かどうかの判定にも使用します。")]
    [SerializeField] private LayerMask groundLayer;

    [Header("穴検知設定")]
    [Tooltip("足元からどれくらい前方をチェックするか(メートル)。")]
    [SerializeField] private float forwardCheckDistance = 0.6f;

    [Tooltip("Rayを飛ばす高さのオフセット。キャラの足元付近に合わせると精度が上がります。")]
    [SerializeField] private float rayOriginHeight = 0.2f;

    [Tooltip("どれくらい下方向まで床を探すか(メートル)。この距離内に床がなければ「穴」と判定。")]
    [SerializeField] private float groundCheckDistance = 1.0f;

    [Header("移動方向設定")]
    [Tooltip("true: Transformのforward方向を進行方向とみなす / false: Rigidbodyの速度から進行方向を推定")]
    [SerializeField] private bool useTransformForward = true;

    [Tooltip("速度ベースで方向を決める場合、この値より遅いと「止まっている」とみなしてチェックをスキップします。")]
    [SerializeField] private float minSpeedForCheck = 0.1f;

    [Header("デバッグ表示")]
    [Tooltip("SceneビューにRayの可視化を行うかどうか。")]
    [SerializeField] private bool drawDebugRay = true;


    // ====== 内部キャッシュ ======
    private Rigidbody rb;

    // 地面判定用の小さなオフセット(浮動小数誤差対策)
    private const float GroundedCheckOffset = 0.05f;

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();

        // Rigidbodyの基本設定(任意ですが、よくある設定をデフォルトで)
        rb.interpolation = RigidbodyInterpolation.Interpolate;
        rb.constraints = RigidbodyConstraints.FreezeRotationX |
                         RigidbodyConstraints.FreezeRotationZ;
    }

    private void FixedUpdate()
    {
        // 地面にいないときはジャンプ入力を受け付けない
        if (!IsGrounded())
        {
            return;
        }

        // 今の進行方向を取得
        Vector3 moveDir = GetMoveDirection();
        if (moveDir.sqrMagnitude < 0.0001f)
        {
            // ほぼ停止しているときは穴チェック不要
            return;
        }

        // 正規化しておく
        moveDir.Normalize();

        // 穴があるかどうかをチェック
        if (IsGapAhead(moveDir))
        {
            // 穴があるならジャンプ実行
            PerformJump();
        }
    }

    /// <summary>
    /// 地面に接地しているかどうかをRaycastで判定。
    /// キャラクターの足元から下に向けて短いRayを飛ばす。
    /// </summary>
    private bool IsGrounded()
    {
        Vector3 origin = transform.position + Vector3.up * GroundedCheckOffset;
        float distance = GroundedCheckOffset * 2f;

        // 下方向にRaycastして地面を探す
        bool hit = Physics.Raycast(origin, Vector3.down, out RaycastHit hitInfo, distance, groundLayer);

        if (drawDebugRay)
        {
            Color c = hit ? Color.green : Color.red;
            Debug.DrawLine(origin, origin + Vector3.down * distance, c);
        }

        return hit;
    }

    /// <summary>
    /// 進行方向を取得。
    /// - useTransformForward が true の場合: transform.forward
    /// - false の場合: Rigidbodyの速度ベクトル
    /// </summary>
    private Vector3 GetMoveDirection()
    {
        if (useTransformForward)
        {
            Vector3 forward = transform.forward;
            forward.y = 0f; // 水平成分だけを使う
            return forward;
        }
        else
        {
            Vector3 velocity = rb.velocity;
            velocity.y = 0f; // 水平成分だけを使う

            if (velocity.magnitude < minSpeedForCheck)
            {
                return Vector3.zero;
            }
            return velocity.normalized;
        }
    }

    /// <summary>
    /// 進行方向の足元をチェックして「穴」があるかどうかを判定。
    /// - 前方に forwardCheckDistance だけ進んだ位置から
    /// - 下方向に groundCheckDistance だけRayを飛ばす
    /// - その範囲に groundLayer のコライダーがなければ「穴」とみなす
    /// </summary>
    private bool IsGapAhead(Vector3 moveDir)
    {
        // Rayの発射位置(足元より少し上)
        Vector3 origin = transform.position;
        origin.y += rayOriginHeight;

        // 前方にオフセットした位置(足が踏み出すあたり)
        Vector3 forwardPoint = origin + moveDir * forwardCheckDistance;

        // 下方向にRaycast
        bool hasGround = Physics.Raycast(
            forwardPoint,
            Vector3.down,
            out RaycastHit hitInfo,
            groundCheckDistance,
            groundLayer
        );

        if (drawDebugRay)
        {
            Color c = hasGround ? Color.cyan : Color.yellow;
            Debug.DrawLine(
                forwardPoint,
                forwardPoint + Vector3.down * groundCheckDistance,
                c
            );
        }

        // 床がない = 穴
        return !hasGround;
    }

    /// <summary>
    /// 実際にジャンプを実行する。
    /// - 現在の垂直速度をリセットしてから
    /// - 上向きの初速度を与える
    /// </summary>
    private void PerformJump()
    {
        Vector3 v = rb.velocity;
        // 既存のY速度をリセット(連続ジャンプで速度が積み上がらないように)
        v.y = 0f;
        rb.velocity = v;

        // 上向きにジャンプ速度を付与
        rb.AddForce(Vector3.up * jumpVelocity, ForceMode.VelocityChange);
    }

    // ====== おまけ: Sceneビューでの可視化(ギズモ) ======
    private void OnDrawGizmosSelected()
    {
        if (!drawDebugRay) return;

        // エディタ上で進行方向が分かるように簡易表示
        Vector3 origin = transform.position + Vector3.up * rayOriginHeight;

        Vector3 dir;
        if (useTransformForward)
        {
            dir = transform.forward;
            dir.y = 0f;
        }
        else
        {
            dir = Vector3.forward; // 仮の方向(エディタ上の目安)
        }

        dir = dir.sqrMagnitude > 0.0001f ? dir.normalized : Vector3.forward;

        Vector3 forwardPoint = origin + dir * forwardCheckDistance;

        Gizmos.color = Color.blue;
        Gizmos.DrawLine(origin, forwardPoint);

        Gizmos.color = Color.magenta;
        Gizmos.DrawLine(forwardPoint, forwardPoint + Vector3.down * groundCheckDistance);
    }
}

使い方の手順

ここでは、3Dの横スクロール風アクションを例にします。
プレイヤーや敵キャラが、足元の穴を自動で飛び越えるようにしてみましょう。

手順①:地面と穴のレイアウトを作る

  1. シーンに床となるオブジェクト(例: Plane や Cube を並べた足場)を配置します。
  2. 床オブジェクトには Collider(BoxCollider など) を付けておきます。
  3. 穴にしたい場所は、単純に「床を置かない」だけでOKです。
    例: 足場A(Cube)、その先に少し空間を空けて、足場B(Cube)を配置する。
  4. 床オブジェクトの Layer を、共通のレイヤー(例: Ground)に設定しておきます。

このレイヤーは後で JumpGapgroundLayer に指定します。

手順②:プレイヤー(または敵)にRigidbodyとColliderを設定

  1. キャラクター用の GameObject(例: Player)を作成します。
  2. Rigidbody コンポーネントを追加します。
    – Use Gravity: ON
    – Constraints: Rotation X/Z を Freeze(転がらないように)
  3. キャラクターに Collider(CapsuleCollider など) を付けて、足元が床に接するようにサイズを調整します。
  4. 横移動用のスクリプト(例: 左右入力で Rigidbody.velocity を更新するだけの簡単なもの)を別コンポーネントとして付けておきます。
    ここでは「移動は別のコンポーネントに任せる」ことがポイントです。

手順③:JumpGapコンポーネントをアタッチして設定

  1. 上のフルコードを JumpGap.cs という名前で保存します。
  2. Unityに戻り、キャラクターの GameObject を選択し、JumpGap コンポーネントを追加します。
    [RequireComponent(typeof(Rigidbody))] が付いているので、Rigidbodyがなければ自動で付与されます。
  3. インスペクタで以下を設定します:
    • Jump Velocity: 5~8 くらいから試して、ちょうどいい高さに調整。
    • Ground Layer: 手順①で床に設定したレイヤー(例: Ground)。
    • Forward Check Distance: キャラの幅の半分~1倍くらい(例: 0.5~0.8)。
    • Ray Origin Height: 足元少し上(例: 0.2)。
    • Ground Check Distance: 足場の高さ + 余裕分(例: 1.0)。
    • Use Transform Forward:
      • 横スクロールで常に +X 方向に進むなら、キャラの forward を +X に向けて ON。
      • 自由移動で速度ベースにしたいなら OFF にして、minSpeedForCheck を0.1くらいに。
    • Draw Debug Ray: 開発中は ON にして SceneビューでRayを確認すると調整しやすいです。

手順④:動作確認(プレイヤー/敵/動く床への応用)

  • プレイヤー
    横移動スクリプトでプレイヤーを前に進ませて、足場の終端に近づいたときに自動でジャンプするか確認します。
    飛び越えられない場合は jumpVelocity を上げるか、forwardCheckDistance を調整しましょう。
  • 敵キャラ(パトロールAI)
    敵に「一定速度で前進するだけ」のシンプルな移動コンポーネントを付け、その上に JumpGap を追加します。
    これだけで「穴を見つけたら自動でジャンプするパトロール敵」ができます。
  • 動く床
    動く床の先に穴がある場合、プレイヤーの JumpGap は「床の端」を検知してジャンプします。
    動く床側は単に Transform を動かしているだけでOKです。
    重要なのは、床オブジェクトが groundLayer に含まれていることです。

メリットと応用

JumpGap を使うことで、

  • 「穴を見たらジャンプ」というロジックをプレイヤーや敵から切り離して再利用できる
  • 横移動スクリプトは「移動」だけ、JumpGap は「穴検知とジャンプトリガー」だけと責務を分離できる
  • プレハブに仕込んでおけば、シーンに配置するだけで自動ジャンプキャラを量産できる

といったメリットがあります。
レベルデザイン的にも、「この敵は穴を飛び越えて追ってくる」「この敵は落ちる」といった違いを、
コンポーネントの有無だけで切り替えられるのはかなり便利です。

また、穴の大きさやジャンプ力を変えたバリエーションをプレハブ化しておけば、
「このステージはジャンプ力弱めの敵だけ」「ここは高く跳ぶ敵」など、
シーンにドラッグ&ドロップするだけでゲーム性のバリエーションを作れます。

改造案:ジャンプ前に「ため時間」を入れる

もう少しキャラクター性を出したい場合、
穴を検知してからすぐにジャンプするのではなく、少しだけため時間を入れるのも面白いです。

以下は、PerformJump をコルーチン版に差し替える改造例です。


private bool isPreparingJump = false;

private void PerformJumpWithDelay(float delay)
{
    if (isPreparingJump) return;
    StartCoroutine(JumpRoutine(delay));
}

private System.Collections.IEnumerator JumpRoutine(float delay)
{
    isPreparingJump = true;

    // ため時間中に移動速度を少し落としたり、
    // アニメーション用のフラグを立てるなどもここで行える
    yield return new WaitForSeconds(delay);

    // 実際のジャンプ(元のPerformJumpを呼ぶ)
    PerformJump();

    isPreparingJump = false;
}

この関数を使う場合は、FixedUpdate 内の PerformJump();
PerformJumpWithDelay(0.2f); などに置き換えるだけでOKです。

こういった小さな改造も、コンポーネントが単機能で分かれているほどやりやすくなります。
巨大な God クラスにしてしまう前に、ぜひ「穴飛び越え」専用コンポーネントとして JumpGap を育ててみてください。