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 を用意する
- プレイヤーの GameObject(例:
Player)を選択します。 Add ComponentからRigidbodyを追加します。Use Gravityは ON のままでOKです(WallClimberが必要な時だけ OFF にします)。Constraintsで回転を固定しておくと、壁登り中にひっくり返りにくくなります。
手順②:プレイヤーに WallClimber を追加する
- 同じくプレイヤーの GameObject に
WallClimberを追加します。 Climb Speedを 2〜5 くらいの値に調整します。- 新Input System を使っている場合は
Use New Input Systemを ON にします。- Input Actions の「Move」などのアクションに、この
WallClimber.OnMoveをイベントとして紐付けます。
- Input Actions の「Move」などのアクションに、この
- 旧Input Manager を使う場合は
Use New Input Systemを OFF にし、Legacy Vertical Axisの名前をVertical(デフォルト)にしておきます。
手順③:壁オブジェクトのレイヤー設定と検出
プレイヤーが「どれを壁とみなすか」を決めます。
- 壁として使いたいオブジェクト(例:
Wall)にBoxColliderなどのコライダーを付けます。 - インスペクター右上の
Layerから新規レイヤーWallを作成し、壁オブジェクトに設定します。 - プレイヤーの 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 に全部押し込めるのではなく、小さなコンポーネントに分割しながら設計していく習慣を、ぜひ身につけていきましょう。
