Unityを触り始めた頃って、つい何でもかんでも Update() の中に書いてしまいがちですよね。移動、ジャンプ、アニメーション、エフェクト、当たり判定のチェック……全部が1つの巨大スクリプトに詰め込まれていくと、だんだん「どこを直せばいいのか」「どこでバグっているのか」が分からなくなってきます。

とくにアクションゲームの「ジャンプ系」ロジックは、if (isGround) { ... }if (isWall) { ... } のような条件分岐が増えやすく、気づけば God クラス化してしまいがちな危険ゾーンです。

そこでこの記事では、「壁に張り付いている状態でジャンプすると、反対側に飛び上がる」挙動だけを担当する小さなコンポーネント 「WallJump」 を用意して、プレイヤー移動ロジックからキレイに分離してみましょう。

【Unity】壁からシュンッとキレる離脱ジャンプ!「WallJump」コンポーネント

ここでは次のような前提を置きます。

  • 2D横スクロール想定(Rigidbody2D を使用)
  • プレイヤーには「通常のジャンプ処理」は別コンポーネントで存在している想定
  • この WallJump は「壁に張り付いているときのジャンプ入力」をフックして、
    壁から反対側へ跳ね飛ばすベクトルを与える役割だけを持つ

もちろん 3D に応用することもできますが、まずは 2D でシンプルに実装していきます。

フルコード:WallJump.cs


using UnityEngine;
using UnityEngine.InputSystem; // 新Input System用

/// <summary>  
/// 壁張り付き状態からの壁ジャンプだけを担当するコンポーネント  
/// - Rigidbody2D に依存  
/// - 「壁に触れているか」「どちら側の壁か」を判定  
/// - 壁に張り付いている状態でジャンプ入力が来たら、反対方向+上向きに力を与える  
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class WallJump : MonoBehaviour
{
    // ====== 物理系参照 ======
    [SerializeField] private Rigidbody2D rb;

    // ====== 壁判定用 ======
    [Header("Wall Detect Settings")]
    [SerializeField] private Transform wallCheckPoint; // 壁チェック用の位置(プレイヤーの横)
    [SerializeField] private float wallCheckRadius = 0.1f; // 壁チェックの半径
    [SerializeField] private LayerMask wallLayer; // 壁レイヤー

    // ====== 壁ジャンプ設定 ======
    [Header("Wall Jump Settings")]
    [SerializeField] private float wallJumpForce = 10f;         // 壁ジャンプの強さ(ベクトルの大きさ)
    [SerializeField] private float wallJumpUpwardRatio = 0.8f; // 上方向成分の比率(0〜1)
    [SerializeField] private float wallStickGravityScale = 0.2f; // 壁に張り付いている間の重力スケール
    [SerializeField] private float wallDetachTime = 0.15f;      // 壁ジャンプ後、一時的に壁判定を無効にする時間

    // ====== 入力 ======
    [Header("Input Settings")]
    [SerializeField] private InputActionReference jumpAction; // 新Input SystemのJumpアクション

    // ====== 状態管理 ======
    private bool isOnWall;         // 現在、壁に接しているか
    private int wallDirection;     // どちら側の壁か(-1: 左側に壁, +1: 右側に壁, 0: なし)
    private bool canDetectWall = true; // 壁ジャンプ直後の一時的な無効化フラグ
    private float defaultGravityScale; // Rigidbody2D の元の重力スケール

    private void Reset()
    {
        // コンポーネントを追加したときに自動でセットされるようにする
        rb = GetComponent<Rigidbody2D>();
    }

    private void Awake()
    {
        if (rb == null)
        {
            rb = GetComponent<Rigidbody2D>();
        }

        defaultGravityScale = rb.gravityScale;

        // InputAction の購読設定
        if (jumpAction != null && jumpAction.action != null)
        {
            // performed はボタンが押された瞬間に呼ばれる
            jumpAction.action.performed += OnJumpPerformed;
        }
        else
        {
            Debug.LogWarning("WallJump: Jump Action が設定されていません。インスペクターで設定してください。", this);
        }
    }

    private void OnEnable()
    {
        // 有効化されたときに InputAction も有効化
        if (jumpAction != null && jumpAction.action != null)
        {
            jumpAction.action.Enable();
        }
    }

    private void OnDisable()
    {
        // 無効化されたときに購読解除+Action無効化
        if (jumpAction != null && jumpAction.action != null)
        {
            jumpAction.action.performed -= OnJumpPerformed;
            jumpAction.action.Disable();
        }
    }

    private void Update()
    {
        UpdateWallState();
        UpdateGravityWhileOnWall();
    }

    /// <summary>
    /// 壁に接しているかどうか、どちら側の壁かを判定する
    /// </summary>
    private void UpdateWallState()
    {
        if (!canDetectWall)
        {
            // 壁ジャンプ直後のクールタイム中は壁判定を行わない
            isOnWall = false;
            wallDirection = 0;
            return;
        }

        if (wallCheckPoint == null)
        {
            // wallCheckPoint が未設定の場合は Transform の位置を使う
            wallCheckPoint = transform;
        }

        // OverlapCircle を使って壁レイヤーに触れているかを判定
        Collider2D hit = Physics2D.OverlapCircle(
            wallCheckPoint.position,
            wallCheckRadius,
            wallLayer
        );

        if (hit != null)
        {
            isOnWall = true;

            // 壁の位置と自分の位置を比較して、どちら側にあるかを判断
            // 壁が自分より右側にあれば +1, 左側にあれば -1
            float diffX = hit.transform.position.x - transform.position.x;
            wallDirection = diffX >= 0f ? 1 : -1;
        }
        else
        {
            isOnWall = false;
            wallDirection = 0;
        }
    }

    /// <summary>
    /// 壁に張り付いている間は落下を遅くするため、重力スケールを変更する
    /// </summary>
    private void UpdateGravityWhileOnWall()
    {
        if (isOnWall)
        {
            rb.gravityScale = wallStickGravityScale;
        }
        else
        {
            rb.gravityScale = defaultGravityScale;
        }
    }

    /// <summary>
    /// ジャンプ入力が押された瞬間に呼ばれるコールバック
    /// </summary>
    /// <param name="context">入力コンテキスト</param>
    private void OnJumpPerformed(InputAction.CallbackContext context)
    {
        // 壁に張り付いていないなら、ここでは何もしない(通常ジャンプは別コンポーネントに任せる)
        if (!isOnWall || wallDirection == 0)
        {
            return;
        }

        // 壁ジャンプの方向ベクトルを計算
        // 壁が右側にあるとき wallDirection = +1 なので、左方向へ飛ばすには -1 を掛ける
        // x成分: -wallDirection(反対方向) / y成分: 上向き
        Vector2 jumpDir = new Vector2(-wallDirection, 1f).normalized;

        // 速度をリセットしてから壁ジャンプベクトルを与えると、挙動が安定しやすい
        rb.velocity = Vector2.zero;

        // 上方向成分の比率を調整
        // 壁ジャンプの方向ベクトルを「水平成分」と「垂直成分」に分けて、上方向比率を強める
        Vector2 horizontal = new Vector2(jumpDir.x, 0f);
        Vector2 vertical = new Vector2(0f, jumpDir.y);

        Vector2 finalDir = horizontal.normalized * (1f - wallJumpUpwardRatio)
                           + vertical.normalized * wallJumpUpwardRatio;

        rb.AddForce(finalDir.normalized * wallJumpForce, ForceMode2D.Impulse);

        // 壁ジャンプ直後は、しばらく壁検出を無効にして、すぐに同じ壁に張り付かないようにする
        if (gameObject.activeInHierarchy)
        {
            StartCoroutine(DisableWallDetectForSeconds(wallDetachTime));
        }
    }

    /// <summary>
    /// 一定時間だけ壁検出を無効にするコルーチン
    /// </summary>
    private System.Collections.IEnumerator DisableWallDetectForSeconds(float seconds)
    {
        canDetectWall = false;
        isOnWall = false;
        wallDirection = 0;

        yield return new WaitForSeconds(seconds);

        canDetectWall = true;
    }

    // デバッグ用にシーンビューで壁チェックの範囲を表示
    private void OnDrawGizmosSelected()
    {
        if (wallCheckPoint == null) return;

        Gizmos.color = Color.cyan;
        Gizmos.DrawWireSphere(wallCheckPoint.position, wallCheckRadius);
    }
}

使い方の手順

ここでは「2Dアクションのプレイヤーキャラクター」に壁ジャンプ機能を追加する例で説明します。

  1. プレイヤーオブジェクトに Rigidbody2D / Collider2D を設定する
    • プレイヤーの GameObject を選択
    • Rigidbody2D を追加(Body Type: Dynamic)
    • BoxCollider2D などのコライダーも付けておきます
  2. 壁用レイヤーを作成して、ステージの壁に設定する
    • Unity上部メニュー「Layer」から「Add Layer…」を選択
    • 例えば Wall というレイヤーを追加
    • ステージの壁(TilemapCollider2D などが付いた GameObject)を選択し、Layer を Wall に変更
  3. WallJump コンポーネントをプレイヤーに追加する
    • プレイヤーの GameObject に WallJump スクリプトをアタッチ
    • インスペクターで次を設定
      • Rb: 自動で Rigidbody2D が入っていなければ、ドラッグ&ドロップで設定
      • Wall Check Point: プレイヤーの子オブジェクトとして Empty を作成し、プレイヤーの横(少し右側)に配置して割り当てる
        ※ 左右両方に対応したい場合は、wallCheckRadius を少し大きめにすると簡単です
      • Wall Layer: さきほど作った Wall レイヤーを選択
      • Wall Jump Force: 8〜15 くらいからお好みで調整
      • Wall Jump Upward Ratio: 0.6〜0.9 くらい(数値が大きいほど上方向に強く飛ぶ)
      • Wall Stick Gravity Scale: 0.1〜0.3 くらい(小さいほど壁に「へばりつく」感じになります)
      • Wall Detach Time: 0.1〜0.2 秒(短すぎると同じ壁にすぐ張り付いてしまう)
  4. Input System の Jump アクションを紐づける
    • 新 Input System(Input System Package)を使っている前提です
    • プロジェクト内に InputActionAsset(例: PlayerInputActions.inputactions)を作成し、
      その中に Jump アクション(Space キーやゲームパッドのボタン)を定義しておきます
    • プロジェクトビューでそのアクションを選択し、インスペクター下部から Create Input Action Reference を作成
    • プレイヤーの WallJump コンポーネントの Jump Action に、作成した InputActionReference をドラッグ&ドロップ

これで、プレイヤーが壁に接している状態でジャンプボタンを押すと、
壁の反対方向+上方向に向かって「壁ジャンプ」が発動します。

例えば、

  • プレイヤーキャラ: 純粋な移動・通常ジャンプは別スクリプト(例: PlayerMovement)で管理
  • WallJump: 壁検出と壁ジャンプ処理だけを担当

という構成にしておくと、PlayerMovement 側は「地上・空中の移動と通常ジャンプ」だけに集中できて、
コードがかなりスッキリします。

メリットと応用

WallJump をコンポーネントとして分離することで、次のようなメリットがあります。

  • プレハブの差し替えが楽
    壁ジャンプが必要なキャラクターだけに WallJump を付ければよく、
    「この敵は壁ジャンプできる」「このプレイヤーは壁ジャンプ禁止」といったバリエーションを
    コンポーネントの有無だけで管理できます。
  • レベルデザイン時の調整がしやすい
    wallJumpForcewallStickGravityScale をインスペクターからいじるだけで、
    「高難度な壁キック」「ぬるっと張り付く壁」などの調整がすぐにできます。
    ステージのジャンプ距離に合わせてパラメータを変えるだけなので、プレハブ再利用性も高いです。
  • 責務が小さいのでテストしやすい
    壁検出と壁ジャンプだけに責務を絞っているため、挙動がおかしいときに
    どこを見ればいいかが明確です。Update() に全部詰め込んだ巨大スクリプトと比べて、
    デバッグコストがかなり下がります。

応用として、「壁に張り付いている間にスライドダウンさせる」「特定の壁だけ壁ジャンプ可能にする」なども簡単に追加できます。

例えば「壁張り付き中に、ゆっくりと下方向へスライドさせる」改造案はこんな感じです。


/// <summary>
/// 壁に張り付いている間、ゆっくりと下方向へ滑り落ちる処理の例
/// (WallJump クラス内に追加するイメージ)
/// </summary>
[SerializeField] private float wallSlideSpeed = -1.5f;

private void ApplyWallSlide()
{
    if (!isOnWall) return;

    // 上向きの速度がある場合はそのまま(ジャンプ直後など)
    if (rb.velocity.y > 0f) return;

    // 下方向の速度を一定値に制限する
    rb.velocity = new Vector2(rb.velocity.x, Mathf.Max(rb.velocity.y, wallSlideSpeed));
}

この関数を Update()FixedUpdate() から呼び出すだけで、
「壁に張り付いている間は、ゆっくりスルスルと落ちていく」ようなアクションゲームらしい挙動が追加できます。

こんなふうに、小さなコンポーネントに責務を分けておくと、後からの改造や差し替えがとても楽になります。
「壁ジャンプだけ」「ダッシュだけ」「二段ジャンプだけ」といったコンポーネントを積み重ねていくスタイルで、
Godクラスから卒業していきましょう。