Unityを触り始めた頃は、つい1つのスクリプトのUpdateに全部の処理を書いてしまうことが多いですよね。
「移動処理」「アニメーション切り替え」「SE再生」「UI更新」などを1クラスに詰め込むと、だんだん手が付けられないGodクラスになっていきます。

特にサウンド周りは後から「足音だけちょっと調整したい」「ピッチを変えてランダムにしたい」といった要望が出やすいのに、
すべてをPlayerControllerのような巨大クラスに押し込んでしまうと、他の処理との依存が増えて修正が怖くなるんですよね。

そこでこの記事では、「足音のピッチだけをよしなにランダム化する」という単一の責務に絞ったコンポーネント
「FootstepRandomizer」を作ってみましょう。
歩行・走行の判定は別コンポーネントに任せて、「足音を鳴らす瞬間にだけ呼び出す小さなSE専用コンポーネント」として設計します。

【Unity】足音に生っぽさを!「FootstepRandomizer」コンポーネント

このコンポーネントは、足音を再生するたびにAudioSourceのPitchを0.9〜1.1の範囲でランダムに変更し、
同じ音源でも「毎回同じに聞こえない」ようにしてくれます。

フルコード


using UnityEngine;

/// <summary>
/// 足音SEのピッチをランダムに変えて再生するコンポーネント。
/// - 足音を鳴らしたいタイミングで PlayFootstep() を呼び出すだけでOK。
/// - Pitchのランダム範囲はインスペクターから調整可能。
/// </summary>
[RequireComponent(typeof(AudioSource))]
public class FootstepRandomizer : MonoBehaviour
{
    // ピッチの最小値(例:0.9)
    [SerializeField] private float minPitch = 0.9f;

    // ピッチの最大値(例:1.1)
    [SerializeField] private float maxPitch = 1.1f;

    // 足音に使うオーディオクリップ
    // 単一でも複数でもOK。複数あればランダム選択。
    [SerializeField] private AudioClip[] footstepClips;

    // 連続再生を防ぐための最小間隔(秒)
    [SerializeField] private float minInterval = 0.1f;

    // 同じクリップが連続で鳴るのを避けるかどうか
    [SerializeField] private bool avoidImmediateRepeat = true;

    // 足音を鳴らすAudioSource
    private AudioSource audioSource;

    // 最後に足音を鳴らした時刻
    private float lastPlayTime = -999f;

    // 直前に再生したクリップのインデックス
    private int lastClipIndex = -1;

    private void Awake()
    {
        // 必ず同じGameObjectにAudioSourceがついている前提
        audioSource = GetComponent<AudioSource>();

        // 足音用のAudioSourceとしてよくあるデフォルト設定を軽く整える
        // (必要に応じてインスペクターで上書きしてください)
        audioSource.playOnAwake = false;
        audioSource.loop = false;
    }

    /// <summary>
    /// 外部から呼び出して足音を鳴らすメイン関数。
    /// 例:アニメーションイベントや移動スクリプトから呼び出す。
    /// </summary>
    public void PlayFootstep()
    {
        // クリップが設定されていない場合は何もしない
        if (footstepClips == null || footstepClips.Length == 0)
        {
            Debug.LogWarning($"[{nameof(FootstepRandomizer)}] 足音用のAudioClipが設定されていません。", this);
            return;
        }

        // 最小間隔よりも短い場合はスキップ(カチカチ鳴りすぎ防止)
        if (Time.time - lastPlayTime < minInterval)
        {
            return;
        }

        // ランダムなピッチを設定
        float randomPitch = Random.Range(minPitch, maxPitch);
        audioSource.pitch = randomPitch;

        // 再生するクリップを決定
        AudioClip clipToPlay = GetRandomClip();

        // クリップをセットして再生
        audioSource.clip = clipToPlay;
        audioSource.Play();

        lastPlayTime = Time.time;
    }

    /// <summary>
    /// ランダムな足音クリップを取得する。
    /// avoidImmediateRepeat が true の場合は、
    /// 可能な限り直前と異なるクリップを選ぶ。
    /// </summary>
    private AudioClip GetRandomClip()
    {
        if (footstepClips.Length == 1)
        {
            // 1つしかない場合はそれを返す
            lastClipIndex = 0;
            return footstepClips[0];
        }

        int index = Random.Range(0, footstepClips.Length);

        if (avoidImmediateRepeat && footstepClips.Length > 1)
        {
            // 直前と同じインデックスを引いたら引き直す
            // (無限ループしないよう、最大数回だけ試行)
            const int maxTry = 5;
            int tryCount = 0;

            while (index == lastClipIndex && tryCount < maxTry)
            {
                index = Random.Range(0, footstepClips.Length);
                tryCount++;
            }
        }

        lastClipIndex = index;
        return footstepClips[index];
    }

#if UNITY_EDITOR
    // インスペクター上で値が変更されたときに呼ばれる(エディタ限定)
    private void OnValidate()
    {
        // minPitch > maxPitch にならないように自動で補正
        if (minPitch > maxPitch)
        {
            maxPitch = minPitch;
        }

        // 負の間隔にならないように補正
        if (minInterval < 0f)
        {
            minInterval = 0f;
        }
    }
#endif
}

使い方の手順

  1. コンポーネントを追加する
    プレイヤーキャラクターや敵キャラなど、足音を鳴らしたいGameObjectを選択し、
    FootstepRandomizer コンポーネントを追加します。
    同じオブジェクトに AudioSource も追加しておきましょう(自動でRequireComponentにより取得されます)。
  2. AudioSourceと足音クリップを設定する
    AudioSource の Output を適切なオーディオミキサーグループ(SFXなど)に割り当て、
    playOnAwakeloop はオフにしておきます(スクリプト側でもオフにしています)。
    その上で、FootstepRandomizerFootstep Clips に足音用のAudioClipを1つ以上登録します。
    ・1つだけでもOK
    ・複数登録すると、ピッチだけでなくクリップ自体もランダム再生されます。
  3. 足音を鳴らすタイミングで PlayFootstep() を呼ぶ
    ここがポイントです。
    移動判定やアニメーション制御は別コンポーネントに任せて、
    「足が地面に着地した瞬間」だけ PlayFootstep() を呼び出すようにします。

    例1:アニメーションイベントから呼ぶ場合
    歩き/走りアニメーションの足が地面につくフレームに Animation Event を仕込み、
    イベントから FootstepRandomizer.PlayFootstep() を呼び出します。

    例2:シンプルなプレイヤー移動スクリプトから呼ぶ場合
    「一定距離歩くごとに足音を鳴らす」ような小さなコンポーネントを用意して、
    そこで PlayFootstep() を呼ぶのもアリです。

  4. 動く床や敵キャラにも使い回す
    動く床:ガコンという機械音を「足音」と見なして同じ仕組みでランダム再生。
    敵キャラ:プレイヤーとは別の足音クリップを設定した FootstepRandomizer をアタッチ。
    こうすることで、「足音をランダム再生する役割」だけを共通コンポーネントに切り出せるので、
    プレイヤー・敵・ギミックなど、どこでも同じ仕組みを再利用できます。

メリットと応用

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

  • 責務が明確:足音のランダム化という1つの機能だけを担当する小さなコンポーネントになる。
  • プレハブ化が簡単:プレイヤー用、敵用、動く床用など、
    足音のバリエーションごとにプレハブを作っておけば、レベルデザイン時は置くだけでOK
  • チューニングが楽:ピッチの範囲や最小再生間隔をインスペクターから変えるだけで、
    「足音がうるさい」「テンポが早すぎる」といった調整にすぐ対応できる。
  • スクリプトの分離:移動処理やアニメーション制御とサウンド制御が分離されるので、
    それぞれを別々にテストしやすく、保守性が上がる。

また、同じ考え方で「足音」以外のSEにも応用できます。
銃声、UIクリック音、ドアの開閉音など、「同じSEが連続で鳴くと安っぽく聞こえるもの」には、
ピッチランダム化コンポーネントを挟んでおくと一気にリッチになります。

改造案:移動速度に応じてピッチを少しだけ変える

例えば、「走っているときは足音のピッチを少し高くしたい」といった要望が出た場合、
以下のようなメソッドを追加して、移動速度を渡せるAPIにしても良いですね。


    /// <summary>
    /// 移動速度に応じてピッチを少しだけ上げ下げしながら足音を鳴らす例。
    /// baseSpeed を基準に、速ければピッチを上げ、遅ければ下げる。
    /// </summary>
    public void PlayFootstepWithSpeed(float currentSpeed, float baseSpeed)
    {
        if (footstepClips == null || footstepClips.Length == 0)
        {
            Debug.LogWarning($"[{nameof(FootstepRandomizer)}] 足音用のAudioClipが設定されていません。", this);
            return;
        }

        if (Time.time - lastPlayTime < minInterval)
        {
            return;
        }

        // 0.8〜1.2倍の範囲で速度に応じて補正値を計算
        float speedRatio = baseSpeed > 0f ? currentSpeed / baseSpeed : 1f;
        float speedFactor = Mathf.Clamp(speedRatio, 0.8f, 1.2f);

        // もともとのランダムピッチに速度補正を掛ける
        float randomPitch = Random.Range(minPitch, maxPitch);
        audioSource.pitch = randomPitch * speedFactor;

        AudioClip clipToPlay = GetRandomClip();
        audioSource.clip = clipToPlay;
        audioSource.Play();

        lastPlayTime = Time.time;
    }

このように、小さなコンポーネントとして設計しておくと、
「ピッチをランダムにする」「速度に応じて変える」「マテリアルごとに音を変える」などの拡張を、
メインのプレイヤースクリプトを汚さずに積み重ねていけます。
ぜひ、自分のプロジェクト向けに少しずつカスタマイズしてみてください。