【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. スクリプトファイルの作成
- エディタ上部メニュー、または Assets パネルで操作します。
- Assets パネルで右クリック → Create → TypeScript を選択。
- ファイル名を AudioPool.ts にします。
- 作成された
AudioPool.tsをダブルクリックして開き、中身をすべて削除して、上記の TypeScript コードを貼り付けて保存します。
2. テスト用ノードの準備
効果音用のテストとして、2D シーン上で試してみます。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、「SfxTester」 など分かりやすい名前に変更します。
- この Sprite は見た目の目印用なので、画像は何でも構いません(空でも可)。
3. AudioPool コンポーネントをアタッチ
- Hierarchy で先ほど作成した SfxTester ノードを選択します。
- Inspector パネル下部の Add Component ボタンをクリックします。
- Custom カテゴリ内から AudioPool を選択して追加します。
4. プロパティの設定
Inspector 上で、AudioPool コンポーネントに対して次のように設定してみましょう。
- audioClip: 任意の効果音(例:
click.wavやshot.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 ボタンから呼ぶ例を紹介します。
- Hierarchy で右クリック → Create → 2D Object → Button を選択し、名前を PlaySfxButton にします。
- Canvas 配下に Button が作成されていることを確認し、位置やテキストは任意で調整します。
- Hierarchy で PlaySfxButton を選択し、Inspector の Button コンポーネントを確認します。
- Button コンポーネントの Click Events セクションで、「+」ボタンをクリックしてイベントを追加します。
- 追加されたイベントの Target に、先ほど AudioPool をアタッチした SfxTester ノードをドラッグ&ドロップします。
- Component ドロップダウンから AudioPool を選択します。
- Handler ドロップダウンから play を選択します。
これで、ゲーム実行中に PlaySfxButton をクリックすると、SfxTester ノード上の AudioPool.play() が呼ばれ、効果音が再生されます。
6. 同時再生制限の確認
- ゲームを実行(Play ボタン)します。
- PlaySfxButton を素早く連打して、効果音がどのように鳴るかを確認します。
- Console を開き、
[AudioPool]から始まるログを確認します。AudioSource プールを 3 個用意しました。などの初期化ログ。- 同時再生数上限に達した場合は
再生可能な AudioSource がありません。同時再生数の上限に達しています。の警告。
- stopOldestWhenFull = false の場合:
- 最大 3 つまで同時に鳴り、それ以上は新しい再生が無視されます。
- stopOldestWhenFull = true に変更して再テスト:
- 4 回目以降の再生要求では、最も古く鳴っている音が止まり、新しい音に入れ替わります。
- Console に
最も古いインスタンスを停止して再利用します。というログが出ます。
7. フェードアウト停止の確認
- Inspector で fadeOutOnStop = true、fadeOutDuration = 0.5 などに設定します。
- ゲーム実行中に、スクリプトから
stopAll()を呼び出して挙動を確認します。- 簡単な例として、もう 1 つ Button を作成し、同様に Click Events から
AudioPool.stopAll()を呼ぶように設定します。
- 簡単な例として、もう 1 つ Button を作成し、同様に Click Events から
- 複数の音を鳴らした状態で「Stop ボタン」を押すと、音が一気に小さくなりながら停止することを確認できます。
まとめ
今回実装した AudioPool コンポーネント は、以下の特徴を持つ 完全独立・再利用可能な音声管理スクリプト です。
- 任意のノードにアタッチするだけで、同じ AudioClip の同時再生数を制限 できる。
- AudioSource のプールを自動生成 するため、エディタでの事前設定がほぼ不要。
- maxInstances / stopOldestWhenFull / fadeOutOnStop などをインスペクタから調整でき、用途に応じた柔軟な挙動を実現。
- 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結 している。
応用例としては、
- 敵弾の発射音、ヒット音など、連打されがちな効果音の管理
- 複数の BGM 候補を別ノードの AudioPool で管理し、シーンごとに切り替え
- UI 操作音など、画面内の各パネルごとに独立したプール を持たせる
といった使い方が考えられます。
プロパティ設計と内部のプール管理ロジックさえ押さえておけば、「アタッチして play() を呼ぶだけ」 で音声管理の品質を一段引き上げることができます。
プロジェクト共通の「汎用効果音プレイヤー」として、そのまま流用してみてください。




