Unityを触り始めると、つい何でもかんでも1つの Update() に書いてしまいがちですよね。
「移動処理」「入力チェック」「アニメーション制御」「エフェクト再生」…全部が1つのスクリプトに詰め込まれていくと、だんだん怖くて触れないGodクラスが生まれてしまいます。

とくに「歩いたら土煙を出したい」「走ったら砂埃を濃くしたい」といった演出を、プレイヤー制御スクリプトの中に直接書き始めると、あっという間にカオスになります。

そこでこの記事では、「足元の土煙」だけに責務を絞った小さなコンポーネント
「FootDust」コンポーネント を作って、アニメーションのフレームに合わせて土煙を出す仕組みをきれいに分離してみましょう。

【Unity】足元の土煙をアニメーションに同期!「FootDust」コンポーネント

ここで作る FootDust は、以下のような役割を持ちます。

  • 歩行・走行アニメーションの「足が地面に着く瞬間」で土煙を出す
  • 土煙の位置・向き・サイズを簡単に調整できる
  • アニメーション側から Animation Event で呼び出されるだけの、シンプルな責務

プレイヤーの移動やアニメーション制御ロジックとは分離しておくことで、
「土煙の見た目だけ変えたい」「敵キャラにも同じ土煙を使いたい」といった時に、とても扱いやすくなります。


FootDust コンポーネントのフルコード


using UnityEngine;

namespace Example.Effects
{
    /// <summary>
    /// 足元から土煙(パーティクル)を出すためのコンポーネント。
    /// 
    /// - アニメーションイベントから TriggerDust() を呼び出す想定
    /// - 足の位置に合わせたオフセットを Inspector から調整可能
    /// - 移動方向に合わせて土煙の向きを揃えるオプション付き
    /// 
    /// プレイヤーの移動ロジックとは分離し、「土煙の制御」だけに責務を絞っています。
    /// </summary>
    [DisallowMultipleComponent]
    public class FootDust : MonoBehaviour
    {
        [Header("土煙プレハブ設定")]
        [Tooltip("足元に生成する土煙のプレハブ(ParticleSystem など)。")]
        [SerializeField] private GameObject dustPrefab;

        [Header("生成位置設定")]
        [Tooltip("足元の基準となる Transform(足ボーンや足元のダミー)。未指定ならこのオブジェクトの Transform を使用。")]
        [SerializeField] private Transform footRoot;

        [Tooltip("基準 Transform からのローカルオフセット(Y を少し下げて地面にめり込ませると自然です)。")]
        [SerializeField] private Vector3 localOffset = new Vector3(0f, -0.05f, 0f);

        [Header("向きの設定")]
        [Tooltip("土煙をキャラクターの前方に向けるかどうか。false の場合はプレハブの回転そのまま。")]
        [SerializeField] private bool alignToCharacterForward = true;

        [Tooltip("移動速度に応じて向きを補正したい場合に使用(0 のままでも OK)。")]
        [SerializeField] private Rigidbody characterRigidbody;

        [Tooltip("移動ベクトルがこの値以下のときは移動方向での向き補正を行わない。")]
        [SerializeField] private float minVelocityForDirection = 0.1f;

        [Header("スケール・寿命設定")]
        [Tooltip("生成する土煙の全体スケール。1 でプレハブそのまま。")]
        [SerializeField] private float dustScale = 1.0f;

        [Tooltip("生成した土煙を自動的に破棄するまでの時間(秒)。0 以下なら自動破棄しない。")]
        [SerializeField] private float autoDestroyTime = 3.0f;

        [Header("デバッグ")]
        [Tooltip("Scene ビューで生成位置の目安を表示するかどうか。")]
        [SerializeField] private bool drawGizmos = true;

        [Tooltip("Gizmos の色。")]
        [SerializeField] private Color gizmoColor = new Color(0.8f, 0.6f, 0.2f, 0.8f);

        /// <summary>
        /// アニメーションイベントから呼び出すメインの関数。
        /// 歩行アニメーションの「足が地面に着地するフレーム」に設定しておき、
        /// そのタイミングで土煙を 1 つ生成します。
        /// 
        /// AnimationClip のイベントからは引数なしで呼び出せるようにしてあります。
        /// </summary>
        public void TriggerDust()
        {
            // プレハブが設定されていない場合は何もしない(安全に無視)
            if (dustPrefab == null)
            {
                // 開発中に気付きやすいよう、エディタ上では警告を出す
#if UNITY_EDITOR
                Debug.LogWarning($"[FootDust] dustPrefab が設定されていません: {name}", this);
#endif
                return;
            }

            // 足元の基準 Transform が未設定なら、自分自身の Transform を使う
            Transform baseTransform = footRoot != null ? footRoot : transform;

            // ワールド座標の生成位置を計算
            Vector3 spawnPosition = baseTransform.TransformPoint(localOffset);

            // 生成する回転を決定
            Quaternion spawnRotation = CalculateSpawnRotation(baseTransform);

            // 土煙プレハブをインスタンス化
            GameObject dustInstance = Instantiate(dustPrefab, spawnPosition, spawnRotation);

            // スケールを調整(ローカルスケールに乗算)
            if (!Mathf.Approximately(dustScale, 1f))
            {
                dustInstance.transform.localScale *= dustScale;
            }

            // 一定時間後に自動破棄する場合
            if (autoDestroyTime > 0f)
            {
                Destroy(dustInstance, autoDestroyTime);
            }
        }

        /// <summary>
        /// 土煙の向きを決定するための補助関数。
        /// - alignToCharacterForward が false の場合は、プレハブのデフォルト回転を使用。
        /// - characterRigidbody が設定されている場合は、移動方向ベースで前方を決定。
        /// - それ以外は baseTransform.forward を使用。
        /// </summary>
        private Quaternion CalculateSpawnRotation(Transform baseTransform)
        {
            if (!alignToCharacterForward)
            {
                // プレハブの回転をそのまま使いたい場合は、ここで終了
                return dustPrefab.transform.rotation;
            }

            Vector3 forward = baseTransform.forward;

            // Rigidbody があり、ある程度以上の速度で動いているなら、その方向を前方として使用
            if (characterRigidbody != null)
            {
                Vector3 velocity = characterRigidbody.velocity;
                velocity.y = 0f; // Y 成分は無視して水平面上の向きだけを見る

                if (velocity.sqrMagnitude >= minVelocityForDirection * minVelocityForDirection)
                {
                    forward = velocity.normalized;
                }
            }

            // 上方向はワールドの Up(地面が水平な前提)
            if (forward.sqrMagnitude < 0.0001f)
            {
                // ほぼゼロベクトルなら、基準 Transform の forward を使用
                forward = baseTransform.forward;
            }

            return Quaternion.LookRotation(forward, Vector3.up);
        }

#if UNITY_EDITOR
        // シーンビュー上で生成位置の目安を表示する
        private void OnDrawGizmosSelected()
        {
            if (!drawGizmos) return;

            Transform baseTransform = footRoot != null ? footRoot : transform;
            Vector3 worldPos = baseTransform.TransformPoint(localOffset);

            Gizmos.color = gizmoColor;
            Gizmos.DrawSphere(worldPos, 0.05f);

            // 向きの目安(キャラクターの forward)
            if (alignToCharacterForward)
            {
                Vector3 forward = baseTransform.forward;
                Gizmos.DrawLine(worldPos, worldPos + forward * 0.25f);
            }
        }
#endif
    }
}

使い方の手順

ここでは、プレイヤーキャラクターの歩行アニメーションに土煙をつける例で説明します。
敵キャラや動く床などにも、ほぼ同じ手順で使い回せます。

① 土煙のプレハブを用意する

  • Project ウィンドウで右クリック → Create > Particle System などから、土煙用の GameObject を作成
  • 名前を DustParticle などに変更し、パーティクルの見た目を調整
  • できたら Hierarchy から Project へドラッグしてプレハブ化し、元のオブジェクトは削除しておきます

② プレイヤーに FootDust コンポーネントをアタッチ

  • プレイヤー(または足ボーンの GameObject)を選択
  • Add Component から FootDust を追加
  • Inspector で以下を設定
    • Dust Prefab に、さきほど作った DustParticle プレハブをドラッグ&ドロップ
    • Foot Root に、足元の Transform(右足ボーン、足元ダミーなど)を指定
      • もし左右の足で別々に出したいなら、右足用 GameObject に FootDust、左足用 GameObject に FootDust と、2つつけるのが分かりやすいです
    • Local Offset で、少しだけ Y をマイナスにして地面にめり込ませると自然な見た目になります
    • Align To Character Forward を ON にすると、移動方向やキャラの向きに合わせて土煙が前後を向きます
    • Rigidbody ベースで向きを取りたい場合は Character Rigidbody にプレイヤーの Rigidbody を指定

③ アニメーションイベントを設定する

ここがポイントです。足が地面に着く「フレーム」に合わせて、TriggerDust() を呼び出します。

  1. 歩行アニメーションの Animation Clip を選択し、Animation ウィンドウを開く
  2. タイムライン上で「足が地面に接地したフレーム」を選ぶ
  3. そのフレーム上で右クリック → Add Animation Event
  4. Inspector の Function ドロップダウンから TriggerDust を選択

これで、そのフレームが再生されるたびに FootDust.TriggerDust() が呼ばれ、足元に土煙が1つ生成されます。

④ 他のキャラやオブジェクトにも流用する

  • 敵キャラ:敵の歩行・走行アニメーションにも同様に AnimationEvent を仕込み、足元に FootDust をアタッチ
  • 動く床:床が上下するときに、床の四隅に FootDust をつけておき、アニメーションの開始・停止フレームで TriggerDust() を呼ぶと「ドスン」とした演出ができます
  • ジャンプ着地:ジャンプ着地用のアニメーションにイベントを入れれば、「着地時だけ濃い土煙」を出すこともできます(プレハブを差し替える or スケールを変える)

メリットと応用

FootDust を使うことで、次のようなメリットがあります。

  • プレイヤー制御スクリプトからエフェクト処理を分離
    「歩いたら土煙を出す」という演出ロジックをコンポーネントに切り出すことで、移動スクリプトは移動に専念できます。
  • プレハブ単位で演出を差し替えやすい
    足元の土煙を変えたいときは、dustPrefab を別のプレハブに差し替えるだけ。
    砂地・雪・水たまりなど、ステージごとに演出を切り替えるのも簡単です。
  • レベルデザイン時の調整が楽
    足元のオフセットやスケール、寿命などを Inspector から変えるだけで、
    「もう少しだけ足の後ろに出したい」「エフェクトが大きすぎる」などを即座に調整できます。
  • 再利用性が高い
    プレイヤー、敵、NPC、動くギミック…どれにでも同じコンポーネントをポン付けできます。
    それぞれのオブジェクトで dustPrefablocalOffset だけ変えて使い回しましょう。

改造案:地面のマテリアルによって土煙プレハブを切り替える

もう一歩踏み込むと、足元の地面に応じてエフェクトを変えることもできます。
たとえば「砂の上では砂埃」「水たまりでは水しぶき」を出したい場合、TriggerDust() の中で地面を判定してプレハブを差し替えれば OK です。

以下は、レイキャストで地面のタグを見て、プレハブを切り替える簡易版の例です。


        /// <summary>
        /// 地面のタグに応じて土煙プレハブを差し替えるサンプル。
        /// - "Sand" タグ: 砂埃用プレハブ
        /// - "Water" タグ: 水しぶき用プレハブ
        /// それ以外: デフォルトの dustPrefab
        /// 
        /// 実運用では、専用の ScriptableObject でマッピングするなど拡張すると便利です。
        /// </summary>
        private GameObject GetDustPrefabByGroundTag(Vector3 spawnPosition)
        {
            // 下方向にレイを飛ばして、足元のコライダーを取得
            if (Physics.Raycast(spawnPosition + Vector3.up * 0.2f, Vector3.down, out RaycastHit hit, 1.0f))
            {
                string groundTag = hit.collider.tag;

                // ここにタグごとの分岐を追加していく
                switch (groundTag)
                {
                    case "Sand":
                        // 砂埃用プレハブ(別途 SerializeField で用意しておく)
                        // return sandDustPrefab;
                        break;
                    case "Water":
                        // 水しぶき用プレハブ
                        // return waterSplashPrefab;
                        break;
                }
            }

            // デフォルトは通常の dustPrefab
            return dustPrefab;
        }

このように、FootDust を小さな責務に絞っておけば、
「地面判定」「天候」「キャラクターステータス」など、別のコンポーネントと組み合わせて演出をどんどん拡張していけます。
土煙ひとつでも、コンポーネント指向で設計しておくと後々かなり楽になりますね。