【Cocos Creator】アタッチするだけ!FootstepAudio (足音再生)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】FootstepAudio の実装:アタッチするだけで「移動速度やアニメーションに応じた足音再生」を実現する汎用スクリプト

このコンポーネントは、キャラクターなどのノードにアタッチするだけで、

  • ノードの移動速度に応じて自動で足音を鳴らす
  • (任意で)アニメーションイベントから足音を鳴らす

といった処理を行う「汎用足音コンポーネント」です。
移動処理やアニメーション制御がどのように実装されていても、このスクリプト単体で完結し、Inspector から足音の間隔や音量、ランダムピッチなどを調整できます。


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

要件整理

  • 親ノード(アタッチ先ノード)の「移動速度」を監視し、一定以上動いているときだけ足音を鳴らす。
  • 足音の再生間隔は、移動速度に応じて変化できるようにする(速度が速いほど間隔が短くなる)。
  • アニメーションを使う場合は、アニメーションイベントから直接「足音を鳴らす」メソッドを呼び出せるようにする。
  • 外部の GameManager やシングルトンには一切依存しない。
  • 必要なサウンド再生は、アタッチ先ノードに AudioSource を追加して行う(なければ警告ログ)。
  • エディタ上で足音クリップや音量、ランダム性などを柔軟に設定できるようにする。

設計アプローチ

このコンポーネントは、以下の2つの方法で足音を鳴らせるようにします。

  1. 移動監視モード(自動)
    • 毎フレーム、前フレームの位置との差分から「移動速度」を計算。
    • 一定以上の速度(しきい値)を超えた場合のみ「歩行中」とみなし、一定間隔で足音を再生。
    • しきい値や間隔は Inspector から調整可能。
  2. アニメーションイベントモード(手動トリガ)
    • AnimationClip のイベントから public メソッド playFootstepFromAnimation() を呼び出す。
    • このメソッドは、移動速度に関係なく「そのフレームで足音を鳴らしたい」ときに使用。
    • アニメーションに合わせて左右の足のタイミングで鳴らしたい場合に便利。

どちらのモードも同時に使えるようにし、必要に応じて Inspector から ON/OFF 切り替えできるようにします。

Inspector で設定可能なプロパティ

  • enableMovementBased (移動ベース足音を有効化)
    – 型: boolean
    – 説明: ON の場合、ノードの移動速度を監視して自動的に足音を鳴らします。
  • enableAnimationEvent (アニメーションイベント足音を有効化)
    – 型: boolean
    – 説明: ON の場合、AnimationClip のイベントから playFootstepFromAnimation() を呼び出した時に足音が鳴ります。
  • minSpeedForSteps (足音再生の最小移動速度)
    – 型: number
    – 単位: 単位時間あたりの距離(world座標ベース)
    – 説明: この値以上の速度で動いているときだけ足音を鳴らします。小さすぎると微妙な揺れでも鳴ってしまうので注意。
  • baseInterval (基準足音間隔)
    – 型: number
    – 単位: 秒
    – 説明: 通常の歩行速度(後述の referenceSpeed)で移動しているときの足音の間隔。
  • referenceSpeed (基準速度)
    – 型: number
    – 説明: この速度で移動しているときに baseInterval で足音を鳴らします。速く動けば間隔が短くなり、遅ければ間隔が長くなります。
  • randomIntervalOffset (間隔のランダム幅)
    – 型: number
    – 単位: 秒
    – 説明: 足音間隔に ±randomIntervalOffset のランダム値を加算して、単調にならないようにします。0 にすると完全に一定間隔になります。
  • audioClips (足音 AudioClip の配列)
    – 型: AudioClip[]
    – 説明: 足音として再生する AudioClip を複数登録できます。再生時にランダムで1つ選ばれます。最低1つは設定してください。
  • volume (音量)
    – 型: number
    – 範囲: 0.0 ~ 1.0
    – 説明: 足音再生時の音量。AudioSource の volume とは別に「足音用の倍率」として扱います。
  • randomPitchEnabled (ランダムピッチを有効化)
    – 型: boolean
    – 説明: ON の場合、再生ごとに pitch をランダムに変化させて、同じ音でもバリエーションを出します。
  • pitchMin / pitchMax (ピッチ範囲)
    – 型: number
    – 説明: ランダムピッチ有効時に、この範囲内で pitch をランダム設定します。1.0 が通常の高さです。
  • use3DPosition (3D位置を使用)
    – 型: boolean
    – 説明: ON の場合、worldPosition を使って速度を計算します。2Dゲームでも 3D ノード構造を使っている場合はこちらを使うと安定します。OFF の場合は 2D の position を利用します。
  • debugLog (デバッグログ出力)
    – 型: boolean
    – 説明: ON にすると、足音再生やエラー・警告のログをコンソールに出力します。開発中のみ ON 推奨。

また、内部的には以下の標準コンポーネントを利用します。

  • AudioSource(必須)
    – アタッチ先ノードに AudioSource が存在しない場合、自動で追加しつつ警告ログを出します。

TypeScriptコードの実装


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

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

    @property({
        tooltip: '移動速度に応じて自動で足音を鳴らすかどうか。ON の場合、ノードの移動を監視して一定間隔で足音を再生します。'
    })
    public enableMovementBased: boolean = true;

    @property({
        tooltip: 'アニメーションイベントから足音を鳴らす機能を有効にします。AnimationClip のイベントから playFootstepFromAnimation() を呼び出してください。'
    })
    public enableAnimationEvent: boolean = true;

    @property({
        tooltip: '足音を鳴らし始める最小移動速度。これ未満の速度では足音を鳴らしません。'
    })
    public minSpeedForSteps: number = 0.1;

    @property({
        tooltip: '基準の足音間隔(秒)。referenceSpeed で移動しているとき、この間隔で足音を鳴らします。'
    })
    public baseInterval: number = 0.4;

    @property({
        tooltip: '基準速度。baseInterval と組み合わせて、実際の移動速度に応じて足音間隔をスケーリングします。'
    })
    public referenceSpeed: number = 2.0;

    @property({
        tooltip: '足音間隔に加えるランダム幅(秒)。0 の場合は完全に一定間隔になります。'
    })
    public randomIntervalOffset: number = 0.05;

    @property({
        type: [AudioClip],
        tooltip: '足音として再生する AudioClip のリストです。再生時にランダムで 1 つ選ばれます。最低 1 つは設定してください。'
    })
    public audioClips: AudioClip[] = [];

    @property({
        tooltip: '足音の音量(0.0 ~ 1.0)。AudioSource の volume と掛け合わされます。'
    })
    public volume: number = 1.0;

    @property({
        tooltip: 'ランダムピッチを有効にするかどうか。ON の場合、再生ごとに pitch をランダムに変更します。'
    })
    public randomPitchEnabled: boolean = true;

    @property({
        tooltip: 'ランダムピッチの最小値。1.0 が通常の高さです。',
        visible() {
            return this.randomPitchEnabled;
        }
    })
    public pitchMin: number = 0.95;

    @property({
        tooltip: 'ランダムピッチの最大値。1.0 が通常の高さです。',
        visible() {
            return this.randomPitchEnabled;
        }
    })
    public pitchMax: number = 1.05;

    @property({
        tooltip: '3D の worldPosition を使用して速度を計算するかどうか。2D でも 3D ノード構造を使っている場合はこちらを ON にします。'
    })
    public use3DPosition: boolean = true;

    @property({
        tooltip: 'デバッグログをコンソールに出力するかどうか。開発中のみ ON を推奨します。'
    })
    public debugLog: boolean = false;

    private _audioSource: AudioSource | null = null;
    private _lastPosition: Vec3 = new Vec3();
    private _timer: number = 0;
    private _currentInterval: number = 0.4;
    private _isMoving: boolean = false;

    onLoad() {
        // AudioSource の取得または自動追加
        this._audioSource = this.getComponent(AudioSource);
        if (!this._audioSource) {
            // 自動で追加するが、開発者に気づいてもらうために警告を出す
            this._audioSource = this.addComponent(AudioSource);
            warn('[FootstepAudio] AudioSource が見つからなかったため、自動で追加しました。必要に応じて設定を見直してください。');
        }

        // 初期位置を記録
        if (this.use3DPosition) {
            this.node.getWorldPosition(this._lastPosition);
        } else {
            const p = this.node.position;
            this._lastPosition.set(p.x, p.y, p.z);
        }

        // 初期の足音間隔を設定
        this._currentInterval = this.baseInterval;

        if (this.debugLog) {
            log('[FootstepAudio] onLoad 完了');
        }
    }

    start() {
        // 特に初期化は onLoad で済ませているが、
        // 将来的に初期再生タイミングなどを制御したい場合はここで行う。
        this._timer = 0;
    }

    update(deltaTime: number) {
        if (!this.enableMovementBased) {
            return;
        }

        // 現在位置の取得
        const currentPos = new Vec3();
        if (this.use3DPosition) {
            this.node.getWorldPosition(currentPos);
        } else {
            const p = this.node.position;
            currentPos.set(p.x, p.y, p.z);
        }

        // フレーム間の移動距離から速度を計算
        const dx = currentPos.x - this._lastPosition.x;
        const dy = currentPos.y - this._lastPosition.y;
        const dz = currentPos.z - this._lastPosition.z;
        const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
        const speed = distance / deltaTime;

        // 次フレームのために位置を更新
        this._lastPosition.set(currentPos.x, currentPos.y, currentPos.z);

        // 移動しているかどうかを判定
        this._isMoving = speed >= this.minSpeedForSteps;

        if (!this._isMoving) {
            // 動いていないときはタイマーをリセット(好みによってはリセットしない実装もあり)
            this._timer = 0;
            return;
        }

        // 実際の速度に応じて足音間隔を計算
        // 速度が速いほど間隔が短くなるようにする(下限は baseInterval * 0.3 に制限)
        if (this.referenceSpeed > 0) {
            const ratio = speed / this.referenceSpeed;
            const clampedRatio = Math.max(0.3, Math.min(3.0, 1 / ratio)); // ざっくりとしたスケーリング
            this._currentInterval = this.baseInterval * clampedRatio;
        } else {
            this._currentInterval = this.baseInterval;
        }

        // ランダム幅を加味した実際の間隔
        let targetInterval = this._currentInterval;
        if (this.randomIntervalOffset > 0) {
            const offset = randomRange(-this.randomIntervalOffset, this.randomIntervalOffset);
            targetInterval += offset;
            // 負の間隔にならないように最低値を保証
            targetInterval = Math.max(0.05, targetInterval);
        }

        // タイマーを進めて、一定時間ごとに足音を再生
        this._timer += deltaTime;
        if (this._timer >= targetInterval) {
            this._timer = 0;
            this.playFootstepInternal('movement');
        }
    }

    /**
     * アニメーションイベントから呼び出す用のメソッド。
     * AnimationClip のイベントにこのメソッド名を設定することで、
     * 指定フレームで足音を鳴らすことができます。
     */
    public playFootstepFromAnimation() {
        if (!this.enableAnimationEvent) {
            if (this.debugLog) {
                log('[FootstepAudio] アニメーションイベントは無効化されています。');
            }
            return;
        }
        this.playFootstepInternal('animation');
    }

    /**
     * 足音再生の共通処理。
     * triggerSource: 'movement' | 'animation' など、どこから呼ばれたかの識別用。
     */
    private playFootstepInternal(triggerSource: 'movement' | 'animation') {
        if (!this._audioSource) {
            warn('[FootstepAudio] AudioSource が見つかりません。足音を再生できません。');
            return;
        }

        if (!this.audioClips || this.audioClips.length === 0) {
            warn('[FootstepAudio] audioClips が設定されていません。足音用の AudioClip を Inspector で設定してください。');
            return;
        }

        // ランダムに AudioClip を選択
        const index = Math.floor(Math.random() * this.audioClips.length);
        const clip = this.audioClips[index];
        if (!clip) {
            warn('[FootstepAudio] audioClips[' + index + '] が null です。設定を確認してください。');
            return;
        }

        // ランダムピッチ設定
        if (this.randomPitchEnabled) {
            const min = Math.min(this.pitchMin, this.pitchMax);
            const max = Math.max(this.pitchMin, this.pitchMax);
            const pitch = randomRange(min, max);
            this._audioSource.pitch = pitch;
        } else {
            this._audioSource.pitch = 1.0;
        }

        // 音量設定
        this._audioSource.volume = this.volume;

        // 再生
        this._audioSource.playOneShot(clip, 1.0);

        if (this.debugLog) {
            log(`[FootstepAudio] 足音再生: source=${triggerSource}, clipIndex=${index}, volume=${this.volume.toFixed(2)}, pitch=${this._audioSource.pitch.toFixed(2)}`);
        }
    }
}

コードのポイント解説

  • onLoad()
    • AudioSourcegetComponent で取得し、なければ addComponent(AudioSource) で自動追加。
    • 初期位置を _lastPosition に記録し、速度計算の基準にします。
    • デバッグログが有効な場合、初期化完了をコンソールに表示します。
  • update(deltaTime)
    • enableMovementBased が OFF のときは何もしません。
    • 現在位置と前フレームの位置の差から移動距離を求め、distance / deltaTime で速度を算出します。
    • 速度が minSpeedForSteps 未満なら足音は鳴らさず、タイマーもリセット。
    • 速度と referenceSpeed から足音間隔をスケーリングし、randomIntervalOffset で少しバラつきを出します。
    • 累積タイマー _timer が目標間隔を超えたら playFootstepInternal('movement') を呼び出して足音を鳴らします。
  • playFootstepFromAnimation()
    • AnimationClip のイベントから直接呼び出すための public メソッドです。
    • enableAnimationEvent が OFF の場合は何もせず、デバッグログのみ出力します。
    • ON の場合は playFootstepInternal('animation') を呼び出します。
  • playFootstepInternal()
    • 足音再生の実処理を共通化したメソッドです。
    • AudioSourceaudioClips の存在をチェックし、足りない場合は warn で警告を出して早期 return。
    • ランダムに AudioClip を選択し、必要なら pitch をランダム設定。
    • volume を AudioSource に設定した上で playOneShot() で再生します。
    • debugLog が ON の場合、どのクリップをどのピッチ・音量で再生したかをコンソールに出力します。

使用手順と動作確認

1. スクリプトの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を FootstepAudio.ts として作成します。
  3. 自動生成されたコードをすべて削除し、本記事の FootstepAudio コードをそのまま貼り付けて保存します。

2. テスト用ノード(キャラクター)の準備

ここでは 2D のキャラクターを例に説明しますが、3D でも手順はほぼ同じです。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、テスト用のスプライトノードを作成します。
    • 名前を Player など分かりやすいものに変更しておきましょう。
  2. 作成した Player ノードを選択し、Inspector を確認します。

3. FootstepAudio コンポーネントのアタッチ

  1. Inspector で Add Component ボタンをクリックします。
  2. 検索欄に FootstepAudio と入力し、Custom → FootstepAudio を選択して追加します。
  3. Inspector に FootstepAudio のプロパティが表示されることを確認します。

4. AudioSource の確認

  • FootstepAudio は onLoad 時に自動で AudioSource を追加しますが、手動で設定を確認したい場合は以下を行います。
    1. Player ノードの Inspector をスクロールし、AudioSource コンポーネントが追加されていることを確認します。
    2. 必要であれば AudioSourceLoop を OFF、PlayOnAwake を OFF にしておきます(足音専用なので自動再生は不要です)。

5. 足音用 AudioClip の設定

  1. Assets に足音用の AudioClip(例: footstep_grass_01.mp3 など)をインポートしておきます。
  2. Player ノードを選択し、Inspector の FootstepAudio コンポーネントを確認します。
  3. Audio Clips の右側にある「+」ボタンをクリックして要素数を増やし、インポートした足音 AudioClip をドラッグ&ドロップで登録します。
    • 1つだけでも動作しますが、2~4個程度登録しておくとランダム再生で自然になります。

6. 基本プロパティの設定例

2D の横スクロールアクションで、Player がだいたい 2~3 の速度で歩く想定の設定例です。

  • Enable Movement Based: ON
  • Enable Animation Event: ON(アニメーションからも鳴らしたい場合)
  • Min Speed For Steps: 0.1
  • Base Interval: 0.4
  • Reference Speed: 2.0
  • Random Interval Offset: 0.05
  • Volume: 0.8
  • Random Pitch Enabled: ON
  • Pitch Min: 0.95
  • Pitch Max: 1.05
  • Use 3D Position: 2D プロジェクトなら OFF / 3D プロジェクトなら ON
  • Debug Log: 動作確認中は ON、本番ビルド前に OFF 推奨

7. 移動スクリプトを仮実装して動作確認

FootstepAudio 自体は他のスクリプトに依存しませんが、実際にノードが動かないと足音は鳴りません
簡単なテスト用の移動スクリプトを用意して、足音が鳴くことを確認します。

  1. Assets パネルで右クリック → Create → TypeScriptTestMover.ts を作成します。
  2. 以下のような簡易移動コードを貼り付けます。(あくまで検証用)

import { _decorator, Component, Vec3, input, Input, EventKeyboard, KeyCode } from 'cc';
const { ccclass, property } = _decorator;

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

    @property
    public speed: number = 2.0;

    private _direction: number = 0;

    onLoad() {
        input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
        input.on(Input.EventType.KEY_UP, this.onKeyUp, this);
    }

    onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
        input.off(Input.EventType.KEY_UP, this.onKeyUp, this);
    }

    onKeyDown(event: EventKeyboard) {
        if (event.keyCode === KeyCode.ARROW_LEFT) {
            this._direction = -1;
        } else if (event.keyCode === KeyCode.ARROW_RIGHT) {
            this._direction = 1;
        }
    }

    onKeyUp(event: EventKeyboard) {
        if (event.keyCode === KeyCode.ARROW_LEFT || event.keyCode === KeyCode.ARROW_RIGHT) {
            this._direction = 0;
        }
    }

    update(deltaTime: number) {
        if (this._direction !== 0) {
            const pos = this.node.position;
            const move = this._direction * this.speed * deltaTime;
            this.node.setPosition(pos.x + move, pos.y, pos.z);
        }
    }
}
  1. Hierarchy で Player ノードを選択し、Add Component → Custom → TestMover を追加します。
  2. TestMover の Speed2.0 などに設定します。
  3. エディタ右上の Play ボタンでゲームを再生し、左右キー を押して Player を動かします。
  4. Player が一定以上の速度で移動している間、足音が一定間隔で鳴ることを確認します。

8. アニメーションイベントから足音を鳴らす(任意)

歩行アニメーションに合わせて、左右の足が地面に着地するタイミングで足音を鳴らしたい場合は、AnimationClip のイベント機能を使います。

  1. 歩行用の AnimationClip(例: walk.anim)を用意しておきます。
  2. Animation パネルで該当の AnimationClip を開きます。
  3. タイムライン上で、足が地面に着地するフレームを選択します。
  4. そのフレームに イベント を追加し、以下のように設定します。
    • Function: playFootstepFromAnimation
    • 引数は不要です。
  5. Player ノードには、先ほどの FootstepAudio がアタッチされていることを確認します。
  6. 再生してみると、アニメーションの足の着地タイミング で足音が鳴るようになります。

このとき、移動ベースの足音も同時に有効にしている場合は、二重に鳴ってしまわないように、どちらか片方を OFF にすることをおすすめします。

  • アニメーションに合わせて正確に鳴らしたい → Enable Animation Event: ON / Enable Movement Based: OFF
  • 単純な移動ベースで良い → Enable Movement Based: ON / Enable Animation Event: OFF

まとめ

今回作成した FootstepAudio コンポーネントは、

  • アタッチ先ノードの移動速度を自動監視し、一定以上動いているときだけ足音を鳴らす。
  • AnimationClip のイベントから直接足音を鳴らすこともできる。
  • AudioSource を自動追加し、Inspector から足音クリップや音量、ピッチ、間隔などを柔軟に調整できる。
  • 外部の GameManager やシングルトンに一切依存しない、完全に独立した汎用コンポーネント。

プロジェクト内のどのキャラクターにも、FootstepAudio.ts をアタッチして AudioClip を設定するだけで、統一された足音システムを簡単に導入できます。
歩行だけでなく、走り・ダッシュ・ステルス移動など、移動速度に応じて足音の間隔や音量を変えることで、プレイヤーへのフィードバックが大きく向上します。

このコンポーネントをベースに、

  • 地面の種類(草・石・水たまりなど)によって AudioClip を差し替える
  • 接地判定(Raycast など)と組み合わせてジャンプ着地音を鳴らす
  • キャラクターごとに足音の音量やピッチを変える

といった拡張も、すべてこのスクリプト単体の範囲内で実現しやすくなります。
まずは本記事のコードをそのまま導入し、プロジェクトに合わせてパラメータを調整してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!