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アクションのプレイヤーキャラクター」に壁ジャンプ機能を追加する例で説明します。
-
プレイヤーオブジェクトに Rigidbody2D / Collider2D を設定する
- プレイヤーの GameObject を選択
Rigidbody2Dを追加(Body Type: Dynamic)BoxCollider2Dなどのコライダーも付けておきます
-
壁用レイヤーを作成して、ステージの壁に設定する
- Unity上部メニュー「Layer」から「Add Layer…」を選択
- 例えば
Wallというレイヤーを追加 - ステージの壁(TilemapCollider2D などが付いた GameObject)を選択し、Layer を
Wallに変更
-
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 秒(短すぎると同じ壁にすぐ張り付いてしまう)
- プレイヤーの GameObject に
-
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を付ければよく、
「この敵は壁ジャンプできる」「このプレイヤーは壁ジャンプ禁止」といったバリエーションを
コンポーネントの有無だけで管理できます。 - レベルデザイン時の調整がしやすい
wallJumpForceやwallStickGravityScaleをインスペクターからいじるだけで、
「高難度な壁キック」「ぬるっと張り付く壁」などの調整がすぐにできます。
ステージのジャンプ距離に合わせてパラメータを変えるだけなので、プレハブ再利用性も高いです。 - 責務が小さいのでテストしやすい
壁検出と壁ジャンプだけに責務を絞っているため、挙動がおかしいときに
どこを見ればいいかが明確です。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クラスから卒業していきましょう。
