【Cocos Creator】アタッチするだけ!AudioPool (同時発音制限)の実装方法【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】AudioPool の実装:アタッチするだけで「同じ効果音の鳴りすぎ(多重再生)」を自動制限する汎用スクリプト

アクションゲームやパーティクル的な演出では、同じ効果音を短時間に何度も再生することがよくあります。しかし無制限に再生すると、「音がうるさくて聞き取れない」「CPU負荷・メモリ負荷が増える」 といった問題が発生します。
本記事では、任意のノードにアタッチするだけで「同じ AudioClip の同時再生数」を自動管理する汎用コンポーネント AudioPool を実装します。

このコンポーネントを使うと、「この効果音は最大 5 発まで」「BGM は常に 1 つだけ」 といった制限をインスペクタから直感的に設定でき、外部の GameManager やシングルトンに一切依存せずに運用できます。


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

機能要件の整理

  • このコンポーネントをアタッチしたノード上で、AudioSource コンポーネントをプール(複数保持) し、同時再生数を制限する。
  • play() を呼ぶたびに、利用可能な AudioSource を探し、同時再生数上限を超える場合は再生をスキップ する。
  • 同じ AudioClip を連打しても、同時に鳴る数が指定上限を超えない ようにする。
  • 外部スクリプトやシングルトンには依存しない。インスペクタで設定した AudioClip を使って自前で完結 する。
  • 必要な標準コンポーネント(AudioSource)は、コード内で自動生成 し、存在しないことによるエラーを防ぐ。
  • 開発中のデバッグ・調整をしやすくするため、ログ出力のオン/オフ などもプロパティで制御可能にする。

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

以下のようなプロパティを用意します。

  • audioClip: AudioClip | null
    再生する音源。
    – インスペクタで任意の AudioClip をアサイン。
    – 未設定の場合は再生要求が来てもエラーを出して再生しない。
  • maxInstances: number
    同時に再生できる最大数。
    – 1 以上の整数。
    – 例: 3 にすると、同じ音が最大 3 つまで同時に鳴る。4 回目以降は無視される。
  • volume: number
    各 AudioSource に設定する音量(0.0 ~ 1.0)。
    – 例: 0.5 で半分の音量。
  • loop: boolean
    再生する音をループさせるかどうか。
    – BGM 的な利用では true、効果音では通常 false。
  • playOnLoad: boolean
    コンポーネントの onLoad / start 時に自動で 1 回再生するか。
    – BGM 用としてノードに置くだけで自動再生したい場合に便利。
  • stopOldestWhenFull: boolean
    同時再生数が上限に達したときの挙動。
    false: これ以上再生しない(新しいリクエストを無視)。
    true: 一番古く再生されたインスタンスを止めて、新しい再生に差し替える。
  • fadeOutOnStop: boolean
    stopAll() 実行時に、即停止ではなくフェードアウトさせるか。
    – 効果音のブツ切れ感を減らしたいときに有効。
  • fadeOutDuration: number
    フェードアウトにかける時間(秒)。
    fadeOutOnStop が true のときのみ有効。
  • debugLog: boolean
    デバッグ用ログを出力するか。
    – true のとき、プール生成や再生・スキップの情報を console.log / console.warn で出力。

AudioSource コンポーネントは、この AudioPool が自動で複数生成 します。そのため、エディタで事前に AudioSource を追加する必要はありません。


TypeScriptコードの実装


import { _decorator, Component, AudioSource, AudioClip, Node, clamp01 } from 'cc';
const { ccclass, property } = _decorator;

/**
 * AudioPool
 * - 同じ AudioClip の同時再生数を制限する汎用コンポーネント
 * - 外部の GameManager などには一切依存せず、このコンポーネント単体で完結します
 */
@ccclass('AudioPool')
export class AudioPool extends Component {

    @property({
        type: AudioClip,
        tooltip: '再生する AudioClip。未設定の場合、play() はエラーを出して何もしません。'
    })
    public audioClip: AudioClip | null = null;

    @property({
        tooltip: '同時に再生できる最大数。1 以上の整数にしてください。'
    })
    public maxInstances: number = 5;

    @property({
        tooltip: '各 AudioSource に設定する音量 (0.0 ~ 1.0)。'
    })
    public volume: number = 1.0;

    @property({
        tooltip: 'ループ再生するかどうか。BGM 的な用途では true、効果音では通常 false。'
    })
    public loop: boolean = false;

    @property({
        tooltip: 'コンポーネント開始時に自動で 1 回再生するかどうか。'
    })
    public playOnLoad: boolean = false;

    @property({
        tooltip: '同時再生数が上限に達したとき、最も古い再生を止めて新しい再生に差し替えるか。'
    })
    public stopOldestWhenFull: boolean = false;

    @property({
        tooltip: 'stopAll() 実行時にフェードアウトさせるかどうか。'
    })
    public fadeOutOnStop: boolean = false;

    @property({
        tooltip: 'フェードアウトにかける時間(秒)。fadeOutOnStop が true のときのみ有効。'
    })
    public fadeOutDuration: number = 0.2;

    @property({
        tooltip: 'デバッグ用ログを有効にするかどうか。'
    })
    public debugLog: boolean = false;

    /** プールしている AudioSource の配列 */
    private _sources: AudioSource[] = [];

    /** フェードアウト中の情報 */
    private _fadeOutInfos: {
        source: AudioSource;
        startVolume: number;
        timeLeft: number;
    }[] = [];

    onLoad() {
        // maxInstances の防御的補正
        if (this.maxInstances <= 0) {
            console.warn('[AudioPool] maxInstances が 0 以下だったため、1 に補正しました。');
            this.maxInstances = 1;
        }

        // volume のクランプ
        this.volume = clamp01(this.volume);

        // AudioSource プールを生成
        this._createAudioSourcePool();

        if (this.debugLog) {
            console.log(`[AudioPool] onLoad 完了。maxInstances=${this.maxInstances}`);
        }
    }

    start() {
        if (this.playOnLoad) {
            this.play();
        }
    }

    update(dt: number) {
        // フェードアウト処理
        if (this._fadeOutInfos.length > 0) {
            for (let i = this._fadeOutInfos.length - 1; i >= 0; i--) {
                const info = this._fadeOutInfos[i];
                info.timeLeft -= dt;
                const t = info.timeLeft / Math.max(this.fadeOutDuration, 0.0001);
                const newVolume = clamp01(info.startVolume * Math.max(t, 0));
                info.source.volume = newVolume;

                if (info.timeLeft <= 0) {
                    info.source.stop();
                    info.source.volume = this.volume; // 元のボリュームに戻す
                    this._fadeOutInfos.splice(i, 1);
                }
            }
        }
    }

    /**
     * AudioSource プールを生成する
     * - このノードに maxInstances 個の AudioSource をアタッチします。
     */
    private _createAudioSourcePool() {
        const node = this.node;

        // 既存の AudioSource があればそれも利用する
        const existing = node.getComponents(AudioSource);
        for (const src of existing) {
            this._setupSource(src);
            this._sources.push(src);
        }

        // 足りない分を追加生成
        for (let i = this._sources.length; i < this.maxInstances; i++) {
            const src = node.addComponent(AudioSource);
            this._setupSource(src);
            this._sources.push(src);
        }

        if (this.debugLog) {
            console.log(`[AudioPool] AudioSource プールを ${this._sources.length} 個用意しました。`);
        }
    }

    /**
     * AudioSource の基本設定を行う
     */
    private _setupSource(source: AudioSource) {
        source.clip = this.audioClip;
        source.loop = this.loop;
        source.volume = this.volume;
        // playOnAwake は AudioPool 側で制御するため false にしておく
        // Cocos Creator 3.8 の AudioSource には playOnAwake はありませんが、
        // 将来のバージョン互換などを考える場合はここで制御するとよいです。
    }

    /**
     * 利用可能な AudioSource を取得する
     * - 再生していないものを優先的に返す
     * - 全て再生中で stopOldestWhenFull が true のとき、最も古い再生中のものを返す
     * - それ以外の場合は null を返す
     */
    private _getAvailableSource(): AudioSource | null {
        // 1. 再生していないソースを探す
        for (const src of this._sources) {
            if (!src.playing) {
                return src;
            }
        }

        // 2. 全て再生中で、かつ stopOldestWhenFull が true の場合、最も古いものを返す
        if (this.stopOldestWhenFull) {
            // 再生中のものの中で、一番長く再生している(=time が最大)のものを探す
            let oldest: AudioSource | null = null;
            let maxTime = -1;
            for (const src of this._sources) {
                const t = src.currentTime;
                if (t > maxTime) {
                    maxTime = t;
                    oldest = src;
                }
            }
            if (oldest) {
                if (this.debugLog) {
                    console.log('[AudioPool] 上限に達したため、最も古いインスタンスを停止して再利用します。');
                }
                oldest.stop();
                return oldest;
            }
        }

        // 3. 利用可能なソースなし
        return null;
    }

    /**
     * 音を 1 回再生する
     * - AudioClip 未設定の場合は警告を出して終了
     * - 同時再生数上限に達している場合の挙動は stopOldestWhenFull に従う
     */
    public play() {
        if (!this.audioClip) {
            console.error('[AudioPool] audioClip が設定されていません。インスペクタで AudioClip を指定してください。');
            return;
        }

        const src = this._getAvailableSource();
        if (!src) {
            if (this.debugLog) {
                console.warn('[AudioPool] 再生可能な AudioSource がありません。同時再生数の上限に達しています。');
            }
            return;
        }

        // 再生前に設定を同期(インスペクタで値を変えた場合に対応)
        src.clip = this.audioClip;
        src.loop = this.loop;
        src.volume = this.volume;

        src.play();

        if (this.debugLog) {
            console.log('[AudioPool] 再生開始。');
        }
    }

    /**
     * 再生中の全ての音を停止する
     * - fadeOutOnStop が true の場合はフェードアウトさせる
     */
    public stopAll() {
        if (!this.fadeOutOnStop || this.fadeOutDuration <= 0) {
            // 即停止
            for (const src of this._sources) {
                if (src.playing) {
                    src.stop();
                    src.volume = this.volume;
                }
            }
            this._fadeOutInfos.length = 0;
            if (this.debugLog) {
                console.log('[AudioPool] すべての再生を即時停止しました。');
            }
        } else {
            // フェードアウト停止
            this._fadeOutInfos.length = 0;
            for (const src of this._sources) {
                if (src.playing) {
                    this._fadeOutInfos.push({
                        source: src,
                        startVolume: src.volume,
                        timeLeft: this.fadeOutDuration,
                    });
                }
            }
            if (this.debugLog) {
                console.log(`[AudioPool] すべての再生をフェードアウトで停止します。duration=${this.fadeOutDuration}`);
            }
        }
    }

    /**
     * 現在再生中のインスタンス数を取得する
     * - デバッグや UI 表示用に利用できます。
     */
    public getPlayingCount(): number {
        let count = 0;
        for (const src of this._sources) {
            if (src.playing) {
                count++;
            }
        }
        return count;
    }
}

ライフサイクルメソッドのポイント解説

  • onLoad()
    maxInstances の値を防御的に補正(0 以下の場合は 1 に)。
    volume を 0.0~1.0 にクランプ。
    _createAudioSourcePool() を呼び出して AudioSource をまとめて生成。
    – デバッグログが有効なら初期化状況を出力。
  • start()
    playOnLoad が true の場合、自動で play() を 1 回呼び出す。
    – BGM 的な用途でノードを置くだけで自動再生したい場合に便利。
  • update(dt)
    fadeOutOnStop が true のときに stopAll() から登録されたフェードアウト情報を毎フレーム更新。
    timeLeft を減らし、残り時間に応じて音量を線形に下げ、0 以下になったら停止・クリーンアップ。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ上部メニュー、または Assets パネルで操作します。
    • Assets パネルで右クリック → Create → TypeScript を選択。
    • ファイル名を AudioPool.ts にします。
  2. 作成された AudioPool.ts をダブルクリックして開き、中身をすべて削除して、上記の TypeScript コードを貼り付けて保存します。

2. テスト用ノードの準備

効果音用のテストとして、2D シーン上で試してみます。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、「SfxTester」 など分かりやすい名前に変更します。
  2. この Sprite は見た目の目印用なので、画像は何でも構いません(空でも可)。

3. AudioPool コンポーネントをアタッチ

  1. Hierarchy で先ほど作成した SfxTester ノードを選択します。
  2. Inspector パネル下部の Add Component ボタンをクリックします。
  3. Custom カテゴリ内から AudioPool を選択して追加します。

4. プロパティの設定

Inspector 上で、AudioPool コンポーネントに対して次のように設定してみましょう。

  • audioClip: 任意の効果音(例: click.wavshot.wav など)をドラッグ&ドロップで設定。
  • maxInstances: 3
    • 同じ音が最大 3 つまで同時に鳴るようにします。
  • volume: 0.8 などお好みで。
  • loop: false(効果音なのでループしない)。
  • playOnLoad: false(手動で鳴らしてテストするため)。
  • stopOldestWhenFull: 最初は false で試し、後で true に変えて違いを確認しても良いです。
  • fadeOutOnStop: false(まずは即停止で挙動確認)。
  • fadeOutDuration: 0.2(フェードアウトを試したい場合用)。
  • debugLog: true にしておくと、Console にプールの状況や再生スキップの情報が出て分かりやすいです。

5. 再生テスト(エディタ上から手動で呼び出す)

Cocos Creator の Inspector から直接メソッドを叩く機能はないため、簡単なテスト用スクリプト か、ボタンイベント から play() を呼ぶのが現実的です。ここでは UI ボタンから呼ぶ例を紹介します。

  1. Hierarchy で右クリック → Create → 2D Object → Button を選択し、名前を PlaySfxButton にします。
  2. Canvas 配下に Button が作成されていることを確認し、位置やテキストは任意で調整します。
  3. Hierarchy で PlaySfxButton を選択し、Inspector の Button コンポーネントを確認します。
  4. Button コンポーネントの Click Events セクションで、「+」ボタンをクリックしてイベントを追加します。
  5. 追加されたイベントの Target に、先ほど AudioPool をアタッチした SfxTester ノードをドラッグ&ドロップします。
  6. Component ドロップダウンから AudioPool を選択します。
  7. Handler ドロップダウンから play を選択します。

これで、ゲーム実行中に PlaySfxButton をクリックすると、SfxTester ノード上の AudioPool.play() が呼ばれ、効果音が再生されます。

6. 同時再生制限の確認

  1. ゲームを実行(Play ボタン)します。
  2. PlaySfxButton を素早く連打して、効果音がどのように鳴るかを確認します。
  3. Console を開き、[AudioPool] から始まるログを確認します。
    • AudioSource プールを 3 個用意しました。 などの初期化ログ。
    • 同時再生数上限に達した場合は 再生可能な AudioSource がありません。同時再生数の上限に達しています。 の警告。
  4. stopOldestWhenFull = false の場合:
    • 最大 3 つまで同時に鳴り、それ以上は新しい再生が無視されます。
  5. stopOldestWhenFull = true に変更して再テスト:
    • 4 回目以降の再生要求では、最も古く鳴っている音が止まり、新しい音に入れ替わります。
    • Console に 最も古いインスタンスを停止して再利用します。 というログが出ます。

7. フェードアウト停止の確認

  1. Inspector で fadeOutOnStop = truefadeOutDuration = 0.5 などに設定します。
  2. ゲーム実行中に、スクリプトから stopAll() を呼び出して挙動を確認します。
    • 簡単な例として、もう 1 つ Button を作成し、同様に Click Events から AudioPool.stopAll() を呼ぶように設定します。
  3. 複数の音を鳴らした状態で「Stop ボタン」を押すと、音が一気に小さくなりながら停止することを確認できます。

まとめ

今回実装した AudioPool コンポーネント は、以下の特徴を持つ 完全独立・再利用可能な音声管理スクリプト です。

  • 任意のノードにアタッチするだけで、同じ AudioClip の同時再生数を制限 できる。
  • AudioSource のプールを自動生成 するため、エディタでの事前設定がほぼ不要。
  • maxInstances / stopOldestWhenFull / fadeOutOnStop などをインスペクタから調整でき、用途に応じた柔軟な挙動を実現。
  • 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結 している。

応用例としては、

  • 敵弾の発射音、ヒット音など、連打されがちな効果音の管理
  • 複数の BGM 候補を別ノードの AudioPool で管理し、シーンごとに切り替え
  • UI 操作音など、画面内の各パネルごとに独立したプール を持たせる

といった使い方が考えられます。
プロパティ設計と内部のプール管理ロジックさえ押さえておけば、「アタッチして play() を呼ぶだけ」 で音声管理の品質を一段引き上げることができます。
プロジェクト共通の「汎用効果音プレイヤー」として、そのまま流用してみてください。

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をコピーしました!