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の横スクロール風アクションを例にします。
プレイヤーや敵キャラが、足元の穴を自動で飛び越えるようにしてみましょう。
手順①:地面と穴のレイアウトを作る
- シーンに床となるオブジェクト(例: Plane や Cube を並べた足場)を配置します。
- 床オブジェクトには Collider(BoxCollider など) を付けておきます。
- 穴にしたい場所は、単純に「床を置かない」だけでOKです。
例: 足場A(Cube)、その先に少し空間を空けて、足場B(Cube)を配置する。 - 床オブジェクトの Layer を、共通のレイヤー(例:
Ground)に設定しておきます。
このレイヤーは後で JumpGap の groundLayer に指定します。
手順②:プレイヤー(または敵)にRigidbodyとColliderを設定
- キャラクター用の GameObject(例:
Player)を作成します。 - Rigidbody コンポーネントを追加します。
– Use Gravity: ON
– Constraints: Rotation X/Z を Freeze(転がらないように) - キャラクターに Collider(CapsuleCollider など) を付けて、足元が床に接するようにサイズを調整します。
- 横移動用のスクリプト(例: 左右入力で
Rigidbody.velocityを更新するだけの簡単なもの)を別コンポーネントとして付けておきます。
ここでは「移動は別のコンポーネントに任せる」ことがポイントです。
手順③:JumpGapコンポーネントをアタッチして設定
- 上のフルコードを
JumpGap.csという名前で保存します。 - Unityに戻り、キャラクターの GameObject を選択し、
JumpGapコンポーネントを追加します。
[RequireComponent(typeof(Rigidbody))]が付いているので、Rigidbodyがなければ自動で付与されます。 - インスペクタで以下を設定します:
- 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 を育ててみてください。
