【Cocos Creator 3.8】FootstepRandomizer の実装:アタッチするだけで足音のピッチを毎回ランダム化して「同じ音の連続感」を消す汎用スクリプト

足音 SE を何度も再生していると、「同じ音が連打されている」ような単調さが気になることがあります。この記事では、AudioSource を持つノードにアタッチするだけで、再生のたびにピッチを 0.9〜1.1 の範囲でランダム変更してくれる汎用コンポーネント「FootstepRandomizer」を実装します。

足音に限らず、銃声・ボタン音・ダメージ音など「同じ SE を連打する」シーンで簡単に使い回せる設計にしています。


コンポーネントの設計方針

基本コンセプト

  • このコンポーネントをアタッチしたノードに AudioSource があれば、その 再生前にピッチをランダムに設定 します。
  • 足音再生は、外部スクリプトから playFootstep() を呼ぶ、または Animation Event から呼ぶ 形を想定しています。
  • 外部の GameManager やシングルトンには一切依存しません。必要なのは「このノードに AudioSource を付けること」だけです。
  • ピッチの最小値・最大値・音量・自動再生間隔などは すべてインスペクタから調整可能 にします。

対応するユースケース

  • プレイヤーキャラの足音:移動処理側から playFootstep() を呼ぶだけで、毎回微妙に違う音になる。
  • 敵キャラの足音:アニメーションイベントや AI ロジックから同様に呼び出し。
  • テスト用に「一定間隔で自動再生するモード」も用意し、シーン上での確認をしやすくします。

インスペクタで設定可能なプロパティ

FootstepRandomizer コンポーネントのプロパティ設計は以下の通りです。

  • minPitch: number
    • ピッチの最小値。
    • デフォルト: 0.9
    • 足音の低さをどこまで許容するかを調整できます。
  • maxPitch: number
    • ピッチの最大値。
    • デフォルト: 1.1
    • 高くしすぎるとコミカルになるので、1.1〜1.2 程度が現実的です。
  • baseVolume: number
    • 足音再生時の基本音量(0.0〜1.0)。
    • デフォルト: 1.0
    • 音量もここで一括管理できるようにします。
  • randomizeVolume: boolean
    • 音量も微妙にランダム化するかどうか。
    • デフォルト: false
    • ON にすると、毎回 90〜110% 程度のランダム補正が入ります。
  • volumeJitter: number
    • 音量ランダム幅(0〜1)。
    • デフォルト: 0.1
    • たとえば 0.1 の場合、baseVolume × [0.9〜1.1] の範囲で音量が揺れます。
  • autoTestPlay: boolean
    • エディタやゲーム中に、一定間隔で自動的に足音を鳴らすテストモード。
    • デフォルト: false
    • ON にすると、testInterval 秒ごとに playFootstep() を自動呼び出しします。
  • testInterval: number
    • 自動再生モード時の再生間隔(秒)。
    • デフォルト: 0.5
    • 歩行アニメのテンポに近い値を入れると確認しやすいです。
  • logWarningIfMissingAudio: boolean
    • AudioSource が見つからないときに警告ログを出すか。
    • デフォルト: true
    • 制作段階では ON、ビルド時に OFF にしてログを抑える、などの使い分けができます。

コンポーネントは 自身のノードにある AudioSource のみを利用 し、他ノードや他スクリプトへの依存は一切ありません。


TypeScriptコードの実装


import { _decorator, Component, AudioSource, randomRange, clamp01, log, warn } from 'cc';
const { ccclass, property } = _decorator;

/**
 * FootstepRandomizer
 * - AudioSource を持つノードにアタッチして使用します。
 * - playFootstep() を呼び出すたびに、ピッチを [minPitch, maxPitch] の範囲でランダム設定してから再生します。
 * - オプションで音量も微妙にランダム化できます。
 */
@ccclass('FootstepRandomizer')
export class FootstepRandomizer extends Component {

    @property({
        tooltip: 'ピッチの最小値。足音の低さをどこまで許容するかを指定します。'
    })
    public minPitch: number = 0.9;

    @property({
        tooltip: 'ピッチの最大値。高くしすぎるとコミカルになるので 1.1〜1.2 程度が現実的です。'
    })
    public maxPitch: number = 1.1;

    @property({
        tooltip: '足音再生時の基本音量(0.0〜1.0)。AudioSource.volume に反映されます。'
    })
    public baseVolume: number = 1.0;

    @property({
        tooltip: '音量も微妙にランダム化するかどうか。ON にすると baseVolume を中心に揺らぎます。'
    })
    public randomizeVolume: boolean = false;

    @property({
        tooltip: '音量ランダム幅(0〜1)。0.1 の場合、baseVolume × [0.9〜1.1] の範囲で変動します。'
    })
    public volumeJitter: number = 0.1;

    @property({
        tooltip: 'テスト用:ON にすると、testInterval 秒ごとに自動で足音を再生します。'
    })
    public autoTestPlay: boolean = false;

    @property({
        tooltip: '自動テスト再生モード時の再生間隔(秒)。',
        min: 0.05
    })
    public testInterval: number = 0.5;

    @property({
        tooltip: 'このノードに AudioSource が見つからない場合に警告ログを出すかどうか。'
    })
    public logWarningIfMissingAudio: boolean = true;

    private _audioSource: AudioSource | null = null;
    private _testTimer: number = 0;

    onLoad() {
        // 必要な AudioSource コンポーネントを取得
        this._audioSource = this.getComponent(AudioSource);

        if (!this._audioSource) {
            if (this.logWarningIfMissingAudio) {
                warn('[FootstepRandomizer] このノードに AudioSource がアタッチされていません。足音は再生されません。');
            }
        }

        // minPitch と maxPitch の整合性を確保
        if (this.minPitch > this.maxPitch) {
            const tmp = this.minPitch;
            this.minPitch = this.maxPitch;
            this.maxPitch = tmp;
            warn('[FootstepRandomizer] minPitch が maxPitch より大きかったため、値を入れ替えました。');
        }

        // volumeJitter は 0〜1 の範囲にクランプ
        this.volumeJitter = clamp01(this.volumeJitter);
        // baseVolume も 0〜1 にクランプ
        this.baseVolume = clamp01(this.baseVolume);
    }

    start() {
        // AudioSource がある場合、初期音量を設定
        if (this._audioSource) {
            this._audioSource.volume = this.baseVolume;
        }
    }

    update(deltaTime: number) {
        // 自動テスト再生モード
        if (!this.autoTestPlay) {
            return;
        }

        if (!this._audioSource) {
            // AudioSource がない場合は何もしない
            return;
        }

        this._testTimer += deltaTime;
        if (this._testTimer >= this.testInterval) {
            this._testTimer = 0;
            this.playFootstep();
        }
    }

    /**
     * 外部から呼び出して足音を再生するためのメソッド。
     * - Animation Event からも直接呼び出せます。
     */
    public playFootstep(): void {
        if (!this._audioSource) {
            if (this.logWarningIfMissingAudio) {
                warn('[FootstepRandomizer] AudioSource が見つからないため、playFootstep() は何も行いません。');
            }
            return;
        }

        // ランダムなピッチを決定
        const pitch = randomRange(this.minPitch, this.maxPitch);
        this._audioSource.pitch = pitch;

        // 音量の設定
        let volume = this.baseVolume;
        if (this.randomizeVolume && this.volumeJitter > 0) {
            const minFactor = 1.0 - this.volumeJitter;
            const maxFactor = 1.0 + this.volumeJitter;
            const volumeFactor = randomRange(minFactor, maxFactor);
            volume = this.baseVolume * volumeFactor;
            volume = clamp01(volume);
        }
        this._audioSource.volume = volume;

        // デバッグ用にログを出したい場合はここで log() を使う
        // log(`[FootstepRandomizer] Play footstep: pitch=${pitch.toFixed(2)}, volume=${volume.toFixed(2)}`);

        // 再生
        this._audioSource.play();
    }
}

コードのポイント解説

  • onLoad()
    • this.getComponent(AudioSource) で同一ノードの AudioSource を取得します。
    • 見つからない場合は warn() で警告を出し、存在しなくてもエラーで落ちない防御的実装 にしています。
    • minPitch > maxPitch の場合は自動で入れ替えて安全な値に補正します。
    • volumeJitterbaseVolumeclamp01 で 0〜1 にクランプします。
  • start()
    • AudioSource が存在する場合、baseVolume を初期音量として反映します。
  • update(deltaTime)
    • autoTestPlay が ON のときだけ、testInterval 秒ごとに playFootstep() を自動呼び出しします。
    • 実運用では OFF にしておき、開発・調整中だけ ON にするのが便利です。
  • playFootstep()
    • 外部スクリプトや Animation Event から呼ぶための公開メソッドです。
    • AudioSource がなければ何もせず、必要に応じて警告を出します。
    • randomRange(minPitch, maxPitch) でピッチをランダムに決定し、this._audioSource.pitch に設定してから play() します。
    • 音量ランダム化が有効な場合は、baseVolume × [1 - volumeJitter 〜 1 + volumeJitter] の範囲で音量を揺らし、clamp01 で 0〜1 に収めます。

使用手順と動作確認

1. スクリプトファイルを作成する

  1. Editor の Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. 作成されたファイル名を FootstepRandomizer.ts に変更します。
  4. ダブルクリックして開き、先ほどの TypeScript コードを 丸ごと貼り付け て保存します。

2. テスト用ノードを用意する

  1. Hierarchy パネルで右クリックし、Create → Empty Node などで新しいノードを作成します。
    • 名前は分かりやすく PlayerFootstep などにしておきます。
  2. そのノードを選択し、InspectorAdd Component → Audio → AudioSource を追加します。
  3. AudioSource の Clip に、足音の AudioClip を設定します。
  4. 必要に応じて Loop のチェックを外す(足音は通常ループしないため)ようにしてください。

※ AudioSource を付け忘れると、FootstepRandomizer は警告ログを出しますが、ゲームは落ちません。

3. FootstepRandomizer をアタッチする

  1. 同じノード(PlayerFootstep)を選択したまま、Inspector 内で Add Component → Custom → FootstepRandomizer を選択します。
    • Custom カテゴリは、プロジェクト内のユーザースクリプトが表示される場所です。
  2. Inspector の FootstepRandomizer セクションで、プロパティを確認します。
    • Min Pitch: 0.9
    • Max Pitch: 1.1
    • Base Volume: 1.0
    • Randomize Volume: OFF(チェックなし)
    • Volume Jitter: 0.1
    • Auto Test Play: OFF(チェックなし)
    • Test Interval: 0.5

4. エディタ上で動作確認(自動テストモード)

まずは、外部スクリプトを用意せずに「足音が毎回微妙に違うか」を確認してみます。

  1. FootstepRandomizerAuto Test Play にチェックを入れます。
  2. Test Interval0.40.6 秒程度に設定します。
  3. 画面上部の Play ボタン(▶)を押してゲームを再生します。
  4. シーン内で、一定間隔で足音が鳴っていることを確認します。
    • ピッチが 0.9〜1.1 の範囲で毎回変化しているため、「同じ音の連打感」がかなり軽減されているはずです。
  5. さらに変化を確認したい場合は、Max Pitch を 1.2〜1.3 にしてみてください。
    • 高くしすぎると違和感が出るので、最終的には好みの値に戻します。

5. 実運用:移動スクリプトやアニメーションから呼び出す

実際のゲームでは、自動テストモードではなく「歩いたタイミングで鳴らす」必要があります。その場合は、以下のように 外部スクリプトから playFootstep() を呼び出すだけで済みます。

例: プレイヤー移動スクリプトから呼ぶ場合(参考コード)


// PlayerMove.ts など(FootstepRandomizer には一切変更不要)
import { _decorator, Component, Node } from 'cc';
import { FootstepRandomizer } from './FootstepRandomizer';
const { ccclass, property } = _decorator;

@ccclass('PlayerMove')
export class PlayerMove extends Component {

    @property({ tooltip: '足音を鳴らすノード(FootstepRandomizer + AudioSource が付いているノード)' })
    public footstepNode: Node | null = null;

    private _footstepRandomizer: FootstepRandomizer | null = null;

    start() {
        if (this.footstepNode) {
            this._footstepRandomizer = this.footstepNode.getComponent(FootstepRandomizer);
        }
    }

    // 例えば、一定距離歩くごとに呼ぶメソッド
    private _onStep() {
        if (this._footstepRandomizer) {
            this._footstepRandomizer.playFootstep();
        }
    }
}

また、アニメーションイベントから直接呼び出す場合は、以下の手順で行えます。

  1. 足音を鳴らしたいフレームに Animation Clip のイベントを追加します。
  2. 関数名に playFootstep と入力します。
  3. 対象ノードに FootstepRandomizer コンポーネントがアタッチされていれば、その playFootstep() が呼び出されます。

このように、FootstepRandomizer 自体は 「AudioSource を持つノードにアタッチして、必要なタイミングで playFootstep() を呼ぶ」だけで完結する独立コンポーネントになっています。


まとめ

  • FootstepRandomizer は、AudioSource を持つノードにアタッチするだけで
    • 再生のたびにピッチを 0.9〜1.1 の範囲でランダム化し、
    • 必要に応じて音量もランダム化できる、
    • 外部スクリプトやシングルトンに一切依存しない

    汎用コンポーネントです。

  • 足音だけでなく、銃声・打撃音・ボタン音など、「同じ SE を連打するすべての場面」でそのまま再利用できます。
  • インスペクタからピッチ・音量・自動テスト再生などを簡単に調整できるため、サウンド調整の試行錯誤がエディタ上で完結します。
  • 防御的に AudioSource の有無をチェックしており、付け忘れてもゲームが落ちない設計になっています。

このような「アタッチしてメソッドを呼ぶだけで完結する小さなコンポーネント」を積み重ねていくと、プロジェクト全体の再利用性と保守性が大きく向上します。まずは足音から導入し、気に入ったら他の効果音にも同じコンポーネントを使い回してみてください。