Unityを触り始めた頃って、プレイヤーの移動処理を全部 Update の中に書いてしまいがちですよね。

ジャンプ、落下、カメラ追従、攻撃、入力処理…とにかく全部が1つのスクリプトに詰め込まれていくと、

  • ちょっと挙動を変えたいだけで他の処理が壊れる
  • どこを触ればいいのか分からなくなる
  • プレイヤーのバリエーションを作るのが地獄になる

といった問題が出てきます。

そこで今回は「滑空(Glide)」の挙動だけを小さなコンポーネントとして切り出して、

  • 空中でボタンを押しっぱなしにすると落下速度を制限
  • 滑空中は横移動速度をブースト

という機能を持った 「Glider」コンポーネント を作っていきます。
プレイヤーの移動コンポーネントとは分離しておくことで、

  • 「このキャラだけ滑空できる」
  • 「この敵は滑空しながら近づいてくる」

といったバリエーションも、コンポーネントの付け外しだけで簡単に実現できます。

【Unity】ふわっと落ちてスイスイ進む!「Glider」コンポーネント

フルコード(Unity6 / C#)


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

/// <summary> 
/// 空中でボタン押しっぱなしで滑空するコンポーネント。
/// ・垂直速度を制限してふわっと落下
/// ・横移動速度をブースト
/// ・「空中でのみ」発動
/// 
/// 想定:
/// - Rigidbody2D を使った横スクロール系のプレイヤーや敵
/// - 新Input System(PlayerInput + InputAction)
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class Glider : MonoBehaviour
{
    // ==== 設定パラメータ ====

    [Header("滑空ボタン設定")]
    [Tooltip("PlayerInput に設定したアクション名(例: \"Glide\")。" +
             "空中でこのアクションが押されている間、滑空状態になります。")]
    [SerializeField] private string glideActionName = "Glide";

    [Header("滑空条件")]
    [Tooltip("この高さ以上の落下速度になっている時だけ滑空を有効にするかどうか。" +
             "true にすると、上昇中は滑空がかからないようにできます。")]
    [SerializeField] private bool requireFalling = true;

    [Tooltip("「今落下している」とみなすための閾値。負の値にします。")]
    [SerializeField] private float fallingVelocityThreshold = -0.1f;

    [Header("滑空中の落下制御")]
    [Tooltip("滑空中の最大落下速度(負の値)。" +
             "例: -2.0 にすると、それ以上は落下速度が増えません。")]
    [SerializeField] private float maxFallSpeedWhileGliding = -2.0f;

    [Header("滑空中の横移動ブースト")]
    [Tooltip("滑空していない時の横移動速度(ベース)。" +
             "プレイヤーの通常移動速度と合わせておきましょう。")]
    [SerializeField] private float baseHorizontalSpeed = 5.0f;

    [Tooltip("滑空中の横移動速度。baseHorizontalSpeed より少し高くすると滑空感が出ます。")]
    [SerializeField] private float glideHorizontalSpeed = 8.0f;

    [Tooltip("空中での左右入力のアクション名(例: \"Move\")。" +
             "Vector2 もしくは float のアクションを想定しています。")]
    [SerializeField] private string horizontalActionName = "Move";

    [Header("接地判定(簡易版)")]
    [Tooltip("どのレイヤーを「地面」とみなすか。")]
    [SerializeField] private LayerMask groundLayer;

    [Tooltip("足元から下方向に飛ばすレイの長さ。キャラの高さに合わせて調整してください。")]
    [SerializeField] private float groundCheckDistance = 0.1f;

    [Tooltip("レイの原点を少し下げたい場合にオフセットを指定します。")]
    [SerializeField] private Vector2 groundCheckOffset = new Vector2(0f, -0.5f);

    [Header("デバッグ表示")]
    [SerializeField] private bool showDebugRay = false;

    // ==== 内部参照 ====
    private Rigidbody2D _rb;
    private PlayerInput _playerInput;

    // ==== 状態 ====
    private bool _isGrounded;
    private bool _isGliding;

    // 横入力のキャッシュ
    private float _horizontalInput;

    private void Awake()
    {
        // 必須コンポーネントの取得
        _rb = GetComponent<Rigidbody2D>();
        _playerInput = GetComponent<PlayerInput>();

        if (_playerInput == null)
        {
            Debug.LogWarning(
                $"Glider: GameObject \"{name}\" に PlayerInput が見つかりませんでした。" +
                "新Input Systemでの入力取得を行う場合は PlayerInput を追加してください。"
            );
        }
    }

    private void Update()
    {
        // 接地判定は毎フレーム更新しておく
        UpdateGroundedState();

        // 入力状態の更新
        UpdateInput();
    }

    private void FixedUpdate()
    {
        // 物理挙動の更新は FixedUpdate で行う
        UpdateGlideState();
        ApplyGlideMovement();
    }

    /// <summary>
    /// 接地判定を更新する(簡易版:Raycast 1本)。
    /// 実際のプロジェクトでは、専用の GroundChecker コンポーネントに切り出すとさらに綺麗です。
    /// </summary>
    private void UpdateGroundedState()
    {
        Vector2 origin = (Vector2)transform.position + groundCheckOffset;
        Vector2 direction = Vector2.down;

        RaycastHit2D hit = Physics2D.Raycast(origin, direction, groundCheckDistance, groundLayer);
        _isGrounded = hit.collider != null;

        if (showDebugRay)
        {
            Color color = _isGrounded ? Color.green : Color.red;
            Debug.DrawRay(origin, direction * groundCheckDistance, color);
        }
    }

    /// <summary>
    /// 新Input Systemから入力値を読み取る。
    /// PlayerInput が無ければ何もしない(キーボード操作を自前で書いてもOK)。
    /// </summary>
    private void UpdateInput()
    {
        _horizontalInput = 0f;

        if (_playerInput == null) return;

        // 水平入力(Vector2 / float 両対応)
        InputAction moveAction = _playerInput.actions[horizontalActionName];
        if (moveAction != null)
        {
            // Vector2 の場合(2D横スクロールなら X 成分だけ使う)
            if (moveAction.valueType == typeof(Vector2))
            {
                Vector2 v = moveAction.ReadValue<Vector2>();
                _horizontalInput = v.x;
            }
            // float の場合
            else if (moveAction.valueType == typeof(float))
            {
                _horizontalInput = moveAction.ReadValue<float>();
            }
        }
    }

    /// <summary>
    /// 現在の状態から「滑空しているか」を判定・更新する。
    /// </summary>
    private void UpdateGlideState()
    {
        // 地上にいるなら滑空はリセット
        if (_isGrounded)
        {
            _isGliding = false;
            return;
        }

        // 空中にいる場合のみ、滑空判定を行う
        bool glideButtonHeld = IsGlideButtonHeld();

        if (!glideButtonHeld)
        {
            // ボタンを離したら滑空終了
            _isGliding = false;
            return;
        }

        // 「落下中だけ滑空を許可する」設定の場合
        if (requireFalling)
        {
            // velocity.y が fallingVelocityThreshold より小さい(より負の値)なら落下中とみなす
            if (_rb.velocity.y < fallingVelocityThreshold)
            {
                _isGliding = true;
            }
            else
            {
                _isGliding = false;
            }
        }
        else
        {
            // 上昇中でも滑空ONにしたい場合はこちら
            _isGliding = true;
        }
    }

    /// <summary>
    /// 実際に Rigidbody2D に対して「滑空中の挙動」を適用する。
    /// </summary>
    private void ApplyGlideMovement()
    {
        Vector2 velocity = _rb.velocity;

        if (_isGliding)
        {
            // 垂直速度の制限(落下速度を抑える)
            if (velocity.y < maxFallSpeedWhileGliding)
            {
                velocity.y = maxFallSpeedWhileGliding;
            }

            // 横移動速度をブースト
            float targetSpeed = Mathf.Abs(_horizontalInput) > 0.01f
                ? glideHorizontalSpeed
                : 0f; // 入力が無いなら止める

            velocity.x = _horizontalInput * targetSpeed;
        }
        else
        {
            // 通常空中挙動:横速度はベーススピードに合わせる
            float targetSpeed = Mathf.Abs(_horizontalInput) > 0.01f
                ? baseHorizontalSpeed
                : 0f;

            velocity.x = _horizontalInput * targetSpeed;
            // 縦方向は他のコンポーネント(ジャンプや重力設定)に任せる
        }

        _rb.velocity = velocity;
    }

    /// <summary>
    /// 滑空ボタンが押されているかを判定。
    /// PlayerInput が無い場合は、暫定でキーボードの Space キーを使う例も示しています。
    /// </summary>
    private bool IsGlideButtonHeld()
    {
        // 新Input System優先
        if (_playerInput != null)
        {
            InputAction glideAction = _playerInput.actions[glideActionName];
            if (glideAction != null)
            {
                return glideAction.IsPressed();
            }
        }

        // 予備:PlayerInput が無い場合はスペースキーで滑空
        // (最終的にはプロジェクトの入力設定に合わせて削除・変更してください)
        return Keyboard.current != null && Keyboard.current.spaceKey.isPressed;
    }

    /// <summary>
    /// 外部から「今滑空中か?」を知りたい時用のプロパティ。
    /// アニメーション切り替えなどに便利です。
    /// </summary>
    public bool IsGliding => _isGliding;
}

使い方の手順

ここでは 2D横スクロールのプレイヤーを例に、Glider コンポーネントの使い方を手順で見ていきます。

  1. プレイヤーオブジェクトの準備
    • ヒエラルキー上でプレイヤー用の GameObject を用意します(例: Player)。
    • Rigidbody2D をアタッチし、Gravity Scale を適度な値(例: 3〜5)に設定します。
    • 地面用の Tilemap や BoxCollider2D を用意し、Layer を「Ground」などに設定しておきます。
  2. Input System の設定
    • プロジェクト設定で Input System を有効にしておきます。
    • PlayerInput コンポーネントを Player に追加します。
    • Input Actions アセットを作成し、以下のようなアクションを用意します:
      • Move(Action Type: Value / Control Type: Vector2 または Axis)
      • Glide(Action Type: Button)
    • Glide にはキーボードの Space やゲームパッドの B ボタンなどを割り当てます。
    • PlayerInputActions にそのアセットを指定します。
  3. Glider コンポーネントを追加して設定
    • PlayerGlider コンポーネントをアタッチします。
    • インスペクターで以下を設定します:
      • Glide Action Name:Input Actions 内で設定した Glide の名前
      • Horizontal Action NameMove の名前
      • Ground Layer:地面に使っているレイヤー(例: Ground)
      • Max Fall Speed While Gliding:例: -2.0(ふわっと落下)
      • Base Horizontal Speed:例: 5.0
      • Glide Horizontal Speed:例: 8.0
    • プレイヤーの通常移動スクリプトは、「地上・空中のベース移動」を担当させ、滑空の特殊挙動は Glider に任せる構成にすると綺麗です。
  4. 動作確認と具体的な使用例

    プレイモードで動かしてみて、

    • ジャンプして空中にいる状態で Glide ボタンを押しっぱなしにする
    • 落下速度が抑えられ、横移動が少し速くなっているか

    を確認しましょう。

    具体的な利用シーンとしては:

    • プレイヤー:崖から飛び出して滑空しながら遠くの足場に届くアクションゲーム。
    • 敵キャラ:空中からプレイヤーに向かってふわっと滑空しながら近づいてくるフライングエネミー。
    • 動く床:プレイヤーがこの床に乗っている間だけ、床側に Glider を付けて「横移動ブーストだけ」使う、といった変わり種もアリです。

メリットと応用

Glider を「滑空だけを担当するコンポーネント」として分離しておくことで、いくつか嬉しいポイントがあります。

  • プレハブの組み合わせでキャラの個性を作れる
    プレイヤーのベース移動コンポーネントは共通にしておき、
    「このキャラには Glider を付ける」「このキャラにはダッシュコンポーネントを付ける」といった形で、
    プレハブの組み合わせだけで挙動のバリエーションを作れます。
  • レベルデザインがやりやすくなる
    滑空距離を変えたい場合も、maxFallSpeedWhileGlidingglideHorizontalSpeed を調整するだけでOKです。
    特定のステージだけ「強風でよく滑空できる」ようにしたい時は、そのステージ用のプレイヤープレハブだけ Glider のパラメータを変えれば済みます。
  • 責務の分離でバグが追いやすい
    「滑空の挙動がおかしい」と思ったら Glider コンポーネントだけを見ればよく、
    ジャンプやダッシュ、攻撃処理とは独立しているので原因を切り分けやすいです。

さらに、Glider を少し改造すると、例えば「滑空開始時に一瞬だけ上昇させる」ような、アクション性の高い挙動も簡単に追加できます。

例えばこんな感じの改造案です:


/// <summary>
/// 滑空を開始した瞬間に、少しだけ上向きの力を与える例。
/// (Glider 内に追加するなら、前フレームの _isGliding を覚えておいて、
///  切り替わった瞬間にこのメソッドを呼ぶイメージです)
/// </summary>
private void ApplyGlideStartBoost(float boostPower = 3.0f)
{
    // 現在の速度を取得
    Vector2 v = _rb.velocity;

    // 縦方向にだけ上向きの力を足す
    v.y = Mathf.Max(v.y, 0f);   // すでに上昇中ならそのまま、落下中なら0にリセット
    v.y += boostPower;          // さらに上向きに加速

    _rb.velocity = v;
}

このように、小さなコンポーネント単位で機能を分けておくと、
「滑空開始時だけの演出」「滑空中だけのエフェクト再生」「滑空中だけスタミナ消費」など、
後からの拡張もしやすくなります。ぜひ、自分のプロジェクト用に Glider を育ててみてください。