Unityを触り始めた頃は、プレイヤーの移動・ジャンプ・アニメ・当たり判定などを全部ひとつの Update() に書いてしまいがちですよね。動き始めるので一見うまくいきますが、機能が増えるたびに条件分岐だらけになって、バグ修正や仕様変更が地獄になっていきます。

そこでおすすめしたいのが、「ひとつのスクリプト=ひとつの責務」というコンポーネント指向の考え方です。プレイヤーの「移動」「攻撃」「カメラ追従」「特殊アクション」などを、それぞれ独立したコンポーネントに分解していくイメージですね。

この記事では、その中の「特殊アクション」の一例として、壁に触れている間だけ重力を無効化して、上下に移動できる「壁登り」専用コンポーネント WallClimber を実装してみます。既存の移動スクリプトにベタ書きするのではなく、「壁登り」という機能だけを切り出したコンポーネントにすることで、プレイヤーにも敵にも簡単に再利用できるようにしていきましょう。

【Unity】壁に張り付いてスイスイ移動!「WallClimber」コンポーネント

ここでは以下のような仕様の WallClimber を作ります。

  • 親オブジェクトが 壁に接している間だけ 有効になる
  • 壁に接している間は Rigidbody の重力を無効化 する
  • その状態で 上下入力に応じて移動 できる
  • 壁から離れたら 重力を元に戻す

「親が壁に接している時」という条件は、親に付いている別コンポーネント(例:プレイヤーの当たり判定)で検出して、その結果を WallClimber に伝える形にしています。こうすることで、WallClimber 自体は「壁にくっついているかどうか」と「上下移動」の責務だけを持つ、シンプルなコンポーネントになります。

フルコード:WallClimber.cs


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

/// <summary>  
/// 壁登り用のコンポーネント。  
/// ・親オブジェクトが「壁に接している」ときだけ重力を無効化  
/// ・上下入力で壁に沿って移動  
///  
/// 想定環境:  
/// ・Rigidbody を持つキャラクター(親側)  
/// ・親側で「壁に接しているか」を判定し、このコンポーネントに通知する  
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class WallClimber : MonoBehaviour
{
    // === 設定項目 ===

    [Header("移動設定")]
    [SerializeField] private float climbSpeed = 3f; // 壁を上下に移動する速度

    [Header("入力設定")]
    [SerializeField] private bool useNewInputSystem = true;
    [SerializeField] private string legacyVerticalAxis = "Vertical"; 
    // 旧Input Managerを使う場合の軸名

    [Header("状態デバッグ")]
    [SerializeField] private bool debugLogState = false;

    // === 内部状態 ===

    private Rigidbody rb;

    // 現在、壁に接していて「登りモード」かどうか
    private bool isClimbing = false;

    // 元々の重力設定を覚えておく
    private bool originalUseGravity = true;

    // 入力値(-1〜1)
    private float verticalInput = 0f;

    // New Input System 用: PlayerInput から呼ばれる
    // (Input Actions の "Move" などに紐付けて使う想定)
    public void OnMove(InputAction.CallbackContext context)
    {
        if (!useNewInputSystem) return;

        Vector2 input = context.ReadValue<Vector2>();
        verticalInput = input.y;
    }

    // 旧Input System用:Updateで読み取る
    private void ReadLegacyInput()
    {
        if (useNewInputSystem) return;

        verticalInput = Input.GetAxisRaw(legacyVerticalAxis);
    }

    private void Awake()
    {
        rb = GetComponent<Rigidbody>();
        originalUseGravity = rb.useGravity;
    }

    private void OnEnable()
    {
        // 念のため、状態を初期化
        isClimbing = false;
        rb.useGravity = originalUseGravity;
    }

    private void OnDisable()
    {
        // 無効化されたときに重力を元に戻す
        rb.useGravity = originalUseGravity;
    }

    private void Update()
    {
        // 旧Input Systemを使う場合はこちらで入力を更新
        ReadLegacyInput();
    }

    private void FixedUpdate()
    {
        // 壁に登っていないときは何もしない
        if (!isClimbing)
        {
            return;
        }

        // Rigidbody の現在速度を取得
        Vector3 velocity = rb.velocity;

        // 上下方向の速度だけを壁登り用に書き換える
        // ここでは「ワールドのY軸方向」に沿って移動させる実装
        velocity.y = verticalInput * climbSpeed;

        rb.velocity = velocity;
    }

    /// <summary>
    /// 外部(親オブジェクト側)から「壁に接しているかどうか」を通知するための関数。
    /// 例: プレイヤーの当たり判定スクリプトから呼び出す。
    /// </summary>
    /// <param name="isTouchingWall">壁に接しているなら true</param>
    public void SetWallContact(bool isTouchingWall)
    {
        // 接触状態が変わらないなら何もしない
        if (isClimbing == isTouchingWall) return;

        isClimbing = isTouchingWall;

        if (isClimbing)
        {
            // 壁に接した瞬間:登りモード開始
            StartClimbing();
        }
        else
        {
            // 壁から離れた瞬間:登りモード終了
            StopClimbing();
        }
    }

    /// <summary>
    /// 壁登りモード開始時の処理
    /// </summary>
    private void StartClimbing()
    {
        // 元の重力設定を保存(万一他スクリプトで動的に変えている場合に備える)
        originalUseGravity = rb.useGravity;

        // 重力を無効化
        rb.useGravity = false;

        // 垂直速度をリセット(急な落下を止めるため)
        Vector3 v = rb.velocity;
        v.y = 0f;
        rb.velocity = v;

        if (debugLogState)
        {
            Debug.Log("[WallClimber] Start climbing: gravity off");
        }
    }

    /// <summary>
    /// 壁登りモード終了時の処理
    /// </summary>
    private void StopClimbing()
    {
        // 重力設定を元に戻す
        rb.useGravity = originalUseGravity;

        // 壁から離れた瞬間に、上下入力をクリア
        verticalInput = 0f;

        if (debugLogState)
        {
            Debug.Log("[WallClimber] Stop climbing: gravity restored");
        }
    }
}

補助コンポーネント例:SimpleWallDetector.cs(任意)

「親が壁に接している時」という判定は、ゲームごとにやり方が変わる部分なので、本来は各自のキャラクター制御スクリプトから SetWallContact を呼び出してもらえばOKです。

ここでは具体例として、プレイヤーの横方向のレイキャストで「壁」を検出し、WallClimber に通知するだけのシンプルな検出コンポーネントも載せておきます。


using UnityEngine;

/// <summary>  
/// 非常にシンプルな「壁検出」コンポーネントの例。  
/// ・左右にレイを飛ばして「Wall」レイヤーを検出  
/// ・結果を子オブジェクトの WallClimber に通知  
///  
/// 実際のゲームでは、プレイヤーの当たり判定や移動処理に統合して  
/// 「壁に接しているか」を判定し、その結果だけを WallClimber に渡すのがおすすめ。  
/// </summary>
public class SimpleWallDetector : MonoBehaviour
{
    [Header("レイキャスト設定")]
    [SerializeField] private float detectDistance = 0.6f;
    [SerializeField] private LayerMask wallLayerMask;

    [Header("参照")]
    [SerializeField] private WallClimber wallClimber; 

    private void Reset()
    {
        // 同じオブジェクト、または子から自動取得してみる
        if (wallClimber == null)
        {
            wallClimber = GetComponentInChildren<WallClimber>();
        }
    }

    private void FixedUpdate()
    {
        if (wallClimber == null) return;

        // 左右方向にレイを飛ばす(横方向の壁を検出)
        bool isTouchingWall = false;

        Vector3 origin = transform.position;
        Vector3 left = Vector3.left;
        Vector3 right = Vector3.right;

        if (Physics.Raycast(origin, left, detectDistance, wallLayerMask))
        {
            isTouchingWall = true;
        }
        else if (Physics.Raycast(origin, right, detectDistance, wallLayerMask))
        {
            isTouchingWall = true;
        }

        // 結果を WallClimber に通知
        wallClimber.SetWallContact(isTouchingWall);
    }

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.cyan;
        Vector3 origin = transform.position;
        Gizmos.DrawLine(origin, origin + Vector3.left * detectDistance);
        Gizmos.DrawLine(origin, origin + Vector3.right * detectDistance);
    }
}

使い方の手順

ここでは「プレイヤーキャラクターが壁を登れるようにする」例で説明します。他にも、敵キャラや動く壁用のギミックにも同じコンポーネントを流用できます。

手順①:プレイヤーに Rigidbody を用意する

  1. プレイヤーの GameObject(例:Player)を選択します。
  2. Add Component から Rigidbody を追加します。
    • Use Gravity は ON のままでOKです(WallClimber が必要な時だけ OFF にします)。
    • Constraints で回転を固定しておくと、壁登り中にひっくり返りにくくなります。

手順②:プレイヤーに WallClimber を追加する

  1. 同じくプレイヤーの GameObject に WallClimber を追加します。
  2. Climb Speed を 2〜5 くらいの値に調整します。
  3. 新Input System を使っている場合は Use New Input System を ON にします。
    • Input Actions の「Move」などのアクションに、この WallClimber.OnMove をイベントとして紐付けます。
  4. 旧Input Manager を使う場合は Use New Input System を OFF にし、Legacy Vertical Axis の名前を Vertical(デフォルト)にしておきます。

手順③:壁オブジェクトのレイヤー設定と検出

プレイヤーが「どれを壁とみなすか」を決めます。

  1. 壁として使いたいオブジェクト(例:Wall)に BoxCollider などのコライダーを付けます。
  2. インスペクター右上の Layer から新規レイヤー Wall を作成し、壁オブジェクトに設定します。
  3. プレイヤーの GameObject に SimpleWallDetector を追加します。
    • Wall Layer Mask に先ほど作成した Wall レイヤーを指定します。
    • Wall Climber には、プレイヤーに付けた WallClimber をドラッグ&ドロップで割り当てます。
    • Detect Distance を 0.5〜1.0 くらいに調整します(プレイヤーのコライダーサイズに合わせてください)。

この状態でプレイすると、プレイヤーが左右の壁に近づいたときに SimpleWallDetector が壁を検出し、その結果を WallClimber に渡してくれます。

手順④:実際の挙動を確認する

  • ゲームを再生して、プレイヤーを壁の近くに移動させます。
  • 壁の距離が Detect Distance 以内に入ると、WallClimber が「登りモード」に入り、Rigidbody の重力がOFFになります。
  • この状態で上下キー(またはゲームパッドのスティックの上下)を入力すると、プレイヤーが壁に沿って上下移動します。
  • 壁から離れると自動的に重力が元に戻り、通常の落下挙動に戻ります。

プレイヤー以外にも、例えば

  • 天井や壁を移動する「蜘蛛型の敵キャラ」
  • 壁を登って上段に移動する「動く足場」
  • 時間制限で壁をよじ登る「レースゲームのチェックポイントギミック」

などにも同じ WallClimber を付けて、壁接触判定だけをそれぞれのロジックに差し替えることで、簡単に「壁登り」ギミックを量産できます。

メリットと応用

WallClimber を独立コンポーネントとして切り出すことで、次のようなメリットがあります。

  • プレハブの再利用性が高い
    「このキャラは壁登りできる」「このキャラはできない」を、WallClimber の有無だけで切り替えられます。プレイヤー、敵、ギミックなど、どのプレハブにも同じコンポーネントをポン付けするだけでOKです。
  • レベルデザインが楽になる
    壁側は「Wall レイヤーを付けたコライダー」を配置するだけで、どこでも壁登りポイントにできます。後から「やっぱりここも登れるようにしよう」と思ったときも、プレハブをいじらずにシーン上の壁を増やすだけで対応できます。
  • Godクラス化を防げる
    プレイヤーのメインスクリプトには「移動」と「壁登り」と「攻撃」と「UI更新」…とどんどん責務が増えていきがちですが、壁登りだけを WallClimber に分離しておけば、バグ調査や改造の影響範囲が限定されます。

応用として、例えば「壁登り中は徐々にスタミナが減って、スタミナがゼロになると落下する」といった要素も、WallClimber に小さな機能として足すだけで実現できます。

改造案:スタミナ制の壁登りにする例

以下のようなメソッドを WallClimber に追加し、FixedUpdate() の最後から呼び出すだけで、簡易的なスタミナ制にできます。


    // --- 追加するフィールド例 ---
    [Header("スタミナ設定(任意機能)")]
    [SerializeField] private bool useStamina = false;
    [SerializeField] private float maxStamina = 5f;
    [SerializeField] private float staminaDrainPerSecond = 1f;
    [SerializeField] private float staminaRecoverPerSecond = 0.5f;

    private float currentStamina;

    private void Start()
    {
        currentStamina = maxStamina;
    }

    /// <summary>
    /// スタミナ制の壁登り処理(任意機能)。
    /// FixedUpdate の最後で呼ぶと、  
    /// ・登っている間はスタミナが減少  
    /// ・登っていない間はスタミナが回復  
    /// ・スタミナが0になると強制的に壁登り終了  
    /// </summary>
    private void HandleStamina()
    {
        if (!useStamina) return;

        if (isClimbing && Mathf.Abs(verticalInput) > 0.01f)
        {
            // 移動中はスタミナ減少
            currentStamina -= staminaDrainPerSecond * Time.fixedDeltaTime;
        }
        else
        {
            // 非移動時はスタミナ回復
            currentStamina += staminaRecoverPerSecond * Time.fixedDeltaTime;
        }

        currentStamina = Mathf.Clamp(currentStamina, 0f, maxStamina);

        // スタミナが尽きたら壁登り終了
        if (currentStamina <= 0f && isClimbing)
        {
            SetWallContact(false);
        }
    }

このように、壁登りという「ひとつの責務」を持ったコンポーネントにしておくと、後からの改造やゲーム固有のルール追加も、局所的な変更でどんどん積み上げていけます。巨大な PlayerController に全部押し込めるのではなく、小さなコンポーネントに分割しながら設計していく習慣を、ぜひ身につけていきましょう。