Unityを触り始めた頃は、つい Update() に「移動」「入力」「アニメ」「物理補正」などを全部まとめて書いてしまいがちですよね。最初は動くので満足してしまいますが、だんだんと

  • ちょっと挙動を変えたいだけなのに巨大なスクリプトを開く必要がある
  • プレイヤーと敵で同じような処理をコピペしてしまう
  • どこで速度が変えられているのか分からなくなる

といった「Godクラス地獄」になりがちです。

そこで今回は、「床の上にいる間だけ、横方向の速度を自然に減衰させる」役割に特化したコンポーネント SurfaceFriction を用意します。
Rigidbody の velocity.x を徐々に 0 に近づけることで、キャラクターやオブジェクトの滑りを止める専用コンポーネントとして切り出していきましょう。

【Unity】床の滑りをスッと止める!「SurfaceFriction」コンポーネント

このコンポーネントはざっくり言うと、

  • 「親オブジェクトが床の上にいる間だけ」
  • Rigidbody の velocity.x を徐々に 0 に近づける

という動きをします。
床判定は「親のスクリプトなどから SetOnGround(true/false) で教えてあげる」構成にして、責務を分離します。

SurfaceFriction.cs(フルコード)


using UnityEngine;

/// <summary>
/// 親が「床の上にいる間だけ」Rigidbody の横方向速度を徐々に 0 に近づける摩擦コンポーネント。
/// ・責務は「横方向の減速」のみ
/// ・床にいるかどうかの判定は外部(例: キャラクターの接地判定コンポーネント)から通知してもらう
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class SurfaceFriction : MonoBehaviour
{
    [Header("摩擦の強さ設定")]
    [SerializeField]
    [Tooltip("1秒あたりどれくらい速度を0に近づけるか。値が大きいほど素早く止まる。")]
    private float frictionStrength = 10f;

    [SerializeField]
    [Tooltip("この速度以下になったら完全に0にスナップするしきい値。")]
    private float stopThreshold = 0.05f;

    [Header("適用軸設定")]
    [SerializeField]
    [Tooltip("true の場合は X 軸方向の速度を減衰させる。")]
    private bool applyOnX = true;

    [SerializeField]
    [Tooltip("true の場合は Z 軸方向の速度も減衰させる(3D の場合など)。")]
    private bool applyOnZ = false;

    [Header("デバッグ")]
    [SerializeField]
    [Tooltip("現在、床の上にいるとみなすかどうか(外部から切り替える想定)。")]
    private bool isOnGround = false;

    private Rigidbody rb;

    private void Awake()
    {
        // 必須コンポーネントとして指定しているので、必ず取得できる
        rb = GetComponent<Rigidbody>();

        // 念のため、回転を固定したい場合はインスペクター側で FreezeRotation を設定しましょう。
    }

    private void FixedUpdate()
    {
        // 床の上にいないときは何もしない
        if (!isOnGround)
        {
            return;
        }

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

        // X軸の減衰
        if (applyOnX)
        {
            velocity.x = ApplyFrictionOnAxis(velocity.x);
        }

        // Z軸の減衰(3D キャラなどで使いたい場合)
        if (applyOnZ)
        {
            velocity.z = ApplyFrictionOnAxis(velocity.z);
        }

        rb.velocity = velocity;
    }

    /// <summary>
    /// 指定された軸の速度を「0に近づける」処理。
    /// </summary>
    /// <param name="axisSpeed">対象軸の速度(例: velocity.x)</param>
    /// <returns>減衰後の速度</returns>
    private float ApplyFrictionOnAxis(float axisSpeed)
    {
        // すでにほぼ停止している場合は、完全に0にスナップ
        if (Mathf.Abs(axisSpeed) < stopThreshold)
        {
            return 0f;
        }

        // 減衰量を計算(frictionStrength が大きいほど強くブレーキがかかる)
        // Mathf.MoveTowards を使うことで、符号を意識せず 0 に近づけられる
        float newSpeed = Mathf.MoveTowards(
            axisSpeed,
            0f,
            frictionStrength * Time.fixedDeltaTime
        );

        return newSpeed;
    }

    /// <summary>
    /// 外部から「床にいる / いない」を設定するための公開メソッド。
    /// 接地判定コンポーネントや、プレイヤーコントローラから呼び出す想定。
    /// </summary>
    public void SetOnGround(bool onGround)
    {
        isOnGround = onGround;
    }

    /// <summary>
    /// 現在の接地状態を取得したい場合用の Getter。
    /// デバッグや他コンポーネントとの連携に利用できます。
    /// </summary>
    public bool IsOnGround => isOnGround;
}

使い方の手順

  1. Rigidbody を持つオブジェクトを用意する
    例: プレイヤーキャラクター、敵キャラクター、物理で動く箱、動く床など。
    そのオブジェクトに Rigidbody コンポーネントを追加しておきます(3D想定。2Dなら Rigidbody2D 用に別スクリプトを作るとよいです)。
  2. SurfaceFriction コンポーネントをアタッチ
    上記の C# ファイルを SurfaceFriction.cs として保存し、Unity に戻るとコンポーネントとして使えるようになります。
    対象オブジェクトを選択し、Add Component から SurfaceFriction を追加しましょう。
    インスペクターで以下を調整します:
    • Friction Strength: 5〜20 あたりから試すと良いです。大きいほどキュッと止まります。
    • Stop Threshold: 0.01〜0.1 くらい。細かいガタガタを消すためのしきい値です。
    • Apply On X: 横移動を止めたいなら ON(2D横スクロールなど)。
    • Apply On Z: 3D で前後移動も止めたいなら ON。
  3. 接地判定から SetOnGround を呼び出す
    SurfaceFriction 自体は「床にいるかどうか」を判断しません。
    代わりに、プレイヤーや敵などの「接地判定コンポーネント」から SetOnGround(true/false) を呼び出します。
    例えば、キャラクターにこんなシンプルな接地判定を付けておきます:
    
    using UnityEngine;
    
    /// <summary>
    /// 非常にシンプルな接地判定の例。
    /// 足元に小さな SphereCast を飛ばして「床があるか」をチェックし、
    /// SurfaceFriction に通知するだけのコンポーネント。
    /// </summary>
    [RequireComponent(typeof(SurfaceFriction))]
    public class SimpleGroundChecker : MonoBehaviour
    {
        [SerializeField]
        private LayerMask groundLayer = ~0; // どのレイヤーを「床」とみなすか
    
        [SerializeField]
        private float checkRadius = 0.2f;
    
        [SerializeField]
        private float checkDistance = 0.1f;
    
        [SerializeField]
        private Vector3 checkOffset = Vector3.zero;
    
        private SurfaceFriction surfaceFriction;
    
        private void Awake()
        {
            surfaceFriction = GetComponent<SurfaceFriction>();
        }
    
        private void FixedUpdate()
        {
            Vector3 origin = transform.position + checkOffset;
    
            bool hit = Physics.SphereCast(
                origin,
                checkRadius,
                Vector3.down,
                out RaycastHit hitInfo,
                checkDistance,
                groundLayer,
                QueryTriggerInteraction.Ignore
            );
    
            surfaceFriction.SetOnGround(hit);
        }
    
        private void OnDrawGizmosSelected()
        {
            // エディタ上で接地判定の範囲を可視化
            Gizmos.color = Color.yellow;
            Vector3 origin = transform.position + checkOffset;
            Gizmos.DrawWireSphere(origin + Vector3.down * checkDistance, checkRadius);
        }
    }
    

    これで、キャラクターが床の上にいる間だけ SurfaceFriction が働き、自然に横方向の滑りを止めてくれます。

  4. 具体的な使用例
    • プレイヤーキャラクター
      プレイヤーの移動スクリプトでは「入力に応じて速度を加える」ことだけに集中させ、
      「止まるときの減速」は SurfaceFriction に任せる構成にすると、コードがかなりスッキリします。
      例えば、プレイヤー移動スクリプトからは Rigidbody.velocity.x を直接 0 にせず、入力がないときは何もしないようにしておき、
      代わりに SurfaceFriction が自動的にブレーキをかけてくれるイメージです。
    • 敵キャラクター
      パトロール移動が終わったときに急にピタッと止まるのではなく、
      SurfaceFriction を付けておけば、ちょっと滑ってから止まるような自然な感じを出せます。
      プレイヤーと同じコンポーネントを使い回せるので、挙動の統一もしやすいです。
    • 動く床
      動く床が停止したあと、慣性で少しだけ滑って止まるような表現にも使えます。
      動く床オブジェクトに Rigidbody と SurfaceFriction を付けて、
      「床がレールの上にいる間だけ SetOnGround(true)」といった形で制御しても良いですね。

メリットと応用

SurfaceFriction を導入することで、以下のようなメリットがあります。

  • 「止まり方」のロジックを1カ所に集約できる
    プレイヤー、敵、ギミックなど、複数のオブジェクトが同じ「摩擦感」を共有できます。
    後から「もう少しキビキビ止まるようにしたい」と思ったときも、
    各キャラの移動スクリプトをバラバラに修正する必要がなく、
    SurfaceFriction のパラメータを変えるだけで全体の調整が可能です。
  • プレハブ単位で調整しやすい
    プレイヤープレハブ・敵プレハブ・ギミックプレハブそれぞれに SurfaceFriction を付けておけば、
    「この敵はツルツルしていて滑りやすい」「この床はゴムっぽくてすぐ止まる」など、
    プレハブごとに摩擦の強さをインスペクターから変えるだけで表現できます。
    レベルデザイン中に「このステージだけ全体的に滑りやすくしたい」といった調整も、
    プレハブのパラメータをいじるだけで済むので、コードを書き換える必要がありません。
  • 責務を分離して God クラスを避けられる
    「入力」「移動」「接地判定」「摩擦」「アニメーション」などをコンポーネントに分けることで、
    それぞれのスクリプトがシンプルになり、テストや改造がしやすくなります。
    SurfaceFriction は「摩擦」のみを担当しているので、他のプロジェクトにも簡単に持ち運べます。

応用として、例えば「氷の床に乗っているときだけ摩擦を弱くする」「泥の中では強くする」なども簡単に実現できます。

最後に、そんな拡張のきっかけになる「改造案」の一例を載せておきます。


/// <summary>
/// 一時的に摩擦の強さを変更するサンプルメソッド。
/// 例えば「氷の床に乗ったときに呼ぶ」と、ツルツル滑る床を表現できます。
/// 一定時間後に元の値に戻す、という処理と組み合わせても良いですね。
/// </summary>
public void SetTemporaryFriction(float newFrictionStrength)
{
    frictionStrength = newFrictionStrength;
}

このように、小さなコンポーネントに責務を分けていくと、挙動の組み合わせや調整がとても楽になります。
「滑りを止める」という単機能だけでも、しっかりコンポーネントとして切り出しておくと、プロジェクト全体の見通しがかなり良くなりますね。