【Cocos Creator 3.8】AudioPool の実装:アタッチするだけで「同時発音数を制限して爆音を防ぐ」汎用スクリプト
爆発や攻撃 SE が一度に大量に再生されると、「バリバリ」「ドゴォォ」と耳障りな爆音になりがちです。
この記事では、任意のノードにアタッチして使える AudioPool(発音数制限)コンポーネント を TypeScript で実装し、同時に鳴らす音の数を 3 つまでに制限する仕組みを作ります。
このコンポーネントは、他のカスタムスクリプトに一切依存せず、インスペクタで AudioClip を指定しておくだけで、どこからでも簡単に安全な SE 再生ができるようにすることを目指します。
コンポーネントの設計方針
実現したい機能
- 指定した SE(AudioClip)を再生するが、同時再生数に上限(例: 3)を設ける。
- 上限に達しているときに再生要求が来た場合は、その再生要求を無視する(キューイングはしないシンプル仕様)。
- 1 つのノードにアタッチするだけで完結し、外部の GameManager やシングルトンに依存しない。
- コード側からも簡単に呼び出せる API(メソッド)を用意する。
play(): 事前に設定したクリップを再生playClip(clip: AudioClip): 引数で渡したクリップを再生
- 防御的な実装:
- 必須コンポーネントである
AudioSourceをonLoadで取得し、存在しない場合はエラーログを出す。 - Inspector で設定されていない値があれば、実行時に警告を出す。
- 必須コンポーネントである
インスペクタで設定可能なプロパティ設計
このコンポーネントは、インスペクタからすべての設定を行えるようにします。
- maxConcurrent(number)
- ラベル: 「最大同時発音数」
- 役割: 同時に再生を許可する SE の最大数。
- 例: 3 に設定すると、4 つ目以降の再生要求は無視される。
- 最小値 1、最大値 16 程度に制限。
- defaultClip(AudioClip)
- ラベル: 「デフォルト再生クリップ」
- 役割:
play()メソッドで再生する AudioClip。 - 空の場合は、
play()呼び出し時に警告を出して何もしない。
- volume(number)
- ラベル: 「音量」
- 役割: 0.0〜1.0 の範囲で音量を指定。
AudioSource.volumeに反映する。 - 例: 0.5 で半分の音量。
- useRandomPitch(boolean)
- ラベル: 「ピッチをランダム化」
- 役割: ON のとき、再生のたびにピッチをランダムに変えて耳障り感を軽減する。
- pitchMin(number)
- ラベル: 「ピッチ最小値」
- 役割: ランダムピッチ使用時の下限値。例: 0.9。
- pitchMax(number)
- ラベル: 「ピッチ最大値」
- 役割: ランダムピッチ使用時の上限値。例: 1.1。
- logWhenLimited(boolean)
- ラベル: 「制限時にログ出力」
- 役割: 同時発音数の制限により再生が拒否されたとき、警告ログを出すかどうか。
- デバッグ時のみ ON にすると便利。
内部的には、AudioSource を複数インスタンス化してプールするのではなく、1 つの AudioSource を何度も再生するのでは足りません。複数同時に鳴らすため、同じノード上に複数の AudioSource を生成してプールし、それぞれを順番に使っていきます。
- AudioSource プール(配列)を作成し、サイズは
maxConcurrent。 - 再生要求が来たら、再生中でない AudioSource を探してそこから再生する。
- 全ての AudioSource が再生中なら、再生をスキップし、必要に応じてログを出す。
TypeScriptコードの実装
以下が完成した AudioPool.ts の実装です。
import { _decorator, Component, AudioSource, AudioClip, Node, clamp, warn, error, log } from 'cc';
const { ccclass, property } = _decorator;
/**
* AudioPool
* - 同時発音数を制限する汎用 SE 再生コンポーネント
* - このコンポーネントをアタッチしたノード上に AudioSource を自動的に複数生成し、
* その中から空いているものを使って再生します。
*/
@ccclass('AudioPool')
export class AudioPool extends Component {
@property({
tooltip: '同時に再生できる最大数。\nこの数を超える再生要求は無視されます。',
min: 1,
max: 16,
step: 1,
})
public maxConcurrent: number = 3;
@property({
type: AudioClip,
tooltip: 'play() メソッドで再生するデフォルトの AudioClip。\n未設定の場合、play() は警告を出して何もしません。',
})
public defaultClip: AudioClip | null = null;
@property({
tooltip: '音量 (0.0〜1.0)。\n生成される全ての AudioSource に適用されます。',
min: 0,
max: 1,
step: 0.05,
})
public volume: number = 1.0;
@property({
tooltip: 'ON にすると、再生のたびにピッチをランダムに変化させます。\n同じ音が連続する耳障り感を軽減できます。',
})
public useRandomPitch: boolean = false;
@property({
tooltip: 'ランダムピッチ使用時の最小ピッチ値。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public pitchMin: number = 0.9;
@property({
tooltip: 'ランダムピッチ使用時の最大ピッチ値。',
min: 0.1,
max: 3.0,
step: 0.05,
})
public pitchMax: number = 1.1;
@property({
tooltip: '同時発音数の上限により再生が拒否されたとき、警告ログを出力します。\nデバッグ時のみ ON にすると便利です。',
})
public logWhenLimited: boolean = false;
/** プールされた AudioSource の配列 */
private _sources: AudioSource[] = [];
/** 一時的に使用するバッファ(GC削減用) */
private _tmpStopped: AudioSource[] = [];
onLoad() {
// 既にアタッチされている AudioSource があれば流用し、足りない分だけ追加生成します。
const existingSources = this.getComponents(AudioSource);
if (existingSources.length === 0) {
// 1つも AudioSource がない場合は少なくとも1つ追加
const src = this.node.addComponent(AudioSource);
if (!src) {
error('[AudioPool] AudioSource の追加に失敗しました。ノードにコンポーネントを追加できません。');
return;
}
this._sources.push(src);
} else {
this._sources = existingSources;
}
// maxConcurrent を安全な範囲にクランプ
this.maxConcurrent = Math.floor(clamp(this.maxConcurrent, 1, 16));
// プールサイズを maxConcurrent に合わせる
if (this._sources.length < this.maxConcurrent) {
const need = this.maxConcurrent - this._sources.length;
for (let i = 0; i < need; i++) {
const src = this.node.addComponent(AudioSource);
if (!src) {
error('[AudioPool] AudioSource の追加に失敗しました。');
continue;
}
this._sources.push(src);
}
} else if (this._sources.length > this.maxConcurrent) {
// 余分な AudioSource は無効化しておく(削除はしない)
for (let i = this.maxConcurrent; i < this._sources.length; i++) {
this._sources[i].enabled = false;
}
this._sources.length = this.maxConcurrent;
}
if (this._sources.length === 0) {
error('[AudioPool] AudioSource の初期化に失敗しました。コンポーネントは動作しません。');
return;
}
// 音量などの初期設定を反映
this._applySettingsToAllSources();
// ピッチ範囲の整合性チェック
if (this.pitchMin > this.pitchMax) {
warn('[AudioPool] pitchMin が pitchMax より大きいため、値を入れ替えます。');
const tmp = this.pitchMin;
this.pitchMin = this.pitchMax;
this.pitchMax = tmp;
}
}
start() {
// defaultClip が設定されていない場合は警告
if (!this.defaultClip) {
warn('[AudioPool] defaultClip が設定されていません。play() を呼び出しても何も再生されません。');
}
}
/**
* インスペクタで変更された値をエディタ上で即時反映したい場合は、
* エディタの「Execute In Edit Mode」を有効にし、ここで再適用しても良いです。
* (本サンプルではランタイム時のみ反映)
*/
/**
* デフォルトクリップを再生する簡易 API。
* - defaultClip が未設定の場合は警告を出して何もしません。
*/
public play(): void {
if (!this.defaultClip) {
warn('[AudioPool] defaultClip が設定されていないため、play() は何も再生しません。');
return;
}
this.playClip(this.defaultClip);
}
/**
* 指定した AudioClip をプールから再生する。
* 同時発音数の上限に達している場合は再生をスキップする。
* @param clip 再生したい AudioClip
*/
public playClip(clip: AudioClip | null): void {
if (!clip) {
warn('[AudioPool] playClip() に null の AudioClip が渡されました。何も再生しません。');
return;
}
if (this._sources.length === 0) {
error('[AudioPool] AudioSource が初期化されていません。コンポーネントの onLoad が正常に実行されたか確認してください。');
return;
}
// 空いている AudioSource を探す
const source = this._getAvailableSource();
if (!source) {
if (this.logWhenLimited) {
warn('[AudioPool] 同時発音数の上限 (' + this.maxConcurrent + ') に達しているため、再生要求をスキップしました。');
}
return;
}
// ランダムピッチの適用
if (this.useRandomPitch) {
const min = this.pitchMin;
const max = this.pitchMax;
const rand = min + Math.random() * (max - min);
source.pitch = rand;
} else {
source.pitch = 1.0;
}
// 音量を適用(念のため毎回反映)
source.volume = this.volume;
// クリップをセットして再生
source.clip = clip;
source.play();
}
/**
* 現在再生中の数を取得する。
* デバッグや UI 表示用。
*/
public getPlayingCount(): number {
let count = 0;
for (let i = 0; i < this._sources.length; i++) {
const s = this._sources[i];
if (s.playing) {
count++;
}
}
return count;
}
/**
* すべての再生を停止する。
*/
public stopAll(): void {
for (let i = 0; i < this._sources.length; i++) {
this._sources[i].stop();
}
}
/**
* 現在の設定値(volume など)を全 AudioSource に反映する。
*/
private _applySettingsToAllSources(): void {
for (let i = 0; i < this._sources.length; i++) {
const s = this._sources[i];
s.volume = this.volume;
// その他、loop や spatialBlend などをここで統一設定してもよい
s.loop = false;
}
}
/**
* 再生可能な AudioSource を一つ返す。
* - playing === false なものを優先的に返す。
* - 全て再生中なら null を返す。
*/
private _getAvailableSource(): AudioSource | null {
for (let i = 0; i < this._sources.length; i++) {
const s = this._sources[i];
if (!s.playing) {
return s;
}
}
return null;
}
}
主要な処理の解説
- onLoad()
this.getComponents(AudioSource)で、既にノードに付いている AudioSource をすべて取得。- 1 つも無ければ
this.node.addComponent(AudioSource)で新規追加。 maxConcurrentを 1〜16 にクランプし、その数になるまで AudioSource を追加してプールを作成。- 余分な AudioSource がある場合は
enabled = falseにして無効化。 - 音量などの設定を全 AudioSource に反映。
pitchMin > pitchMaxの場合は入れ替え、警告ログを出す。
- start()
defaultClipが未設定なら警告を出す(ランタイムで気づけるように)。
- play()
- Inspector で設定した
defaultClipを使って SE を再生する簡易メソッド。 defaultClipが設定されていなければ警告を出して終了。
- Inspector で設定した
- playClip(clip: AudioClip)
- 引数で渡された AudioClip を再生するメイン API。
_getAvailableSource()で「再生中でない AudioSource」を探す。- 見つからなければ、
logWhenLimitedが true のときに警告ログを出して再生をスキップ。 - ランダムピッチが有効なら
pitchMin〜pitchMaxの範囲でピッチを乱数設定。 - 音量を適用し、
source.clip = clip; source.play();で再生。
- getPlayingCount()
- 現在再生中の AudioSource の数をカウントして返す。
- デバッグや UI 表示に利用可能。
- stopAll()
- 全 AudioSource の
stop()を呼び出し、すべての再生を即時停止。
- 全 AudioSource の
使用手順と動作確認
1. スクリプトファイルの作成
- Editor の Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
AudioPool.tsにします。 - 自動生成された中身をすべて削除し、前述の TypeScript コードをそのまま貼り付けて保存します。
2. テスト用ノードの作成
どのノードにアタッチしても動作しますが、ここでは分かりやすく「SE_Manager」という空ノードを作成します。
- Hierarchy パネルで右クリック → Create → Empty Node を選択。
- 作成されたノードの名前を SE_Manager に変更します。
3. AudioPool コンポーネントのアタッチ
- Hierarchy で先ほど作成した SE_Manager ノードを選択します。
- Inspector パネル下部の Add Component ボタンをクリックします。
- Custom カテゴリから AudioPool を選択して追加します。
この時点で、AudioPool は自動的に AudioSource コンポーネントを複数追加します。Inspector の「AudioSource」が複数並んでいれば正常です。
4. 再生する AudioClip の用意
- 爆発 SE などの音声ファイル(
.mp3,.wavなど)を Assets パネルにドラッグ&ドロップしてインポートします。 - インポートされたオーディオアセットを選択し、Inspector で AudioClip として認識されていることを確認します。
5. AudioPool のプロパティ設定
- SE_Manager ノードを選択し、Inspector の AudioPool コンポーネントを確認します。
- プロパティを以下のように設定してみます(例):
- 最大同時発音数 (maxConcurrent):
3 - デフォルト再生クリップ (defaultClip): インポートした爆発 SE の AudioClip をドラッグ&ドロップで設定
- 音量 (volume):
0.8 - ピッチをランダム化 (useRandomPitch):
ON - ピッチ最小値 (pitchMin):
0.95 - ピッチ最大値 (pitchMax):
1.05 - 制限時にログ出力 (logWhenLimited): 開発中は
ONにしておくと挙動が分かりやすいです。
- 最大同時発音数 (maxConcurrent):
6. コードからの再生テスト
次に、別の簡単なスクリプトから AudioPool を呼び出して、一度に大量に再生要求を送るテストをします。
- Assets パネルで右クリック → Create → TypeScript を選択し、
AudioPoolTester.tsを作成します。 - 以下のようなテスト用コードを貼り付けます。
import { _decorator, Component, Node } from 'cc';
import { AudioPool } from './AudioPool';
const { ccclass, property } = _decorator;
@ccclass('AudioPoolTester')
export class AudioPoolTester extends Component {
@property({
type: AudioPool,
tooltip: '同時発音数制限付きの AudioPool コンポーネントを指定します。',
})
public audioPool: AudioPool | null = null;
start() {
if (!this.audioPool) {
console.warn('[AudioPoolTester] audioPool が設定されていません。Hierarchy 上の SE_Manager ノードをドラッグ&ドロップで割り当ててください。');
return;
}
// テスト: 一度に 10 回再生要求を送る
for (let i = 0; i < 10; i++) {
this.audioPool.play();
}
}
}
- Hierarchy で新しく空ノードを作成し、名前を AudioPoolTesterNode にします。
- AudioPoolTesterNode に AudioPoolTester コンポーネントをアタッチします。
- Hierarchy で AudioPoolTesterNode を選択。
- Inspector の Add Component → Custom → AudioPoolTester を選択。
- AudioPoolTester の audioPool プロパティに、Hierarchy の SE_Manager ノードをドラッグ&ドロップして割り当てます。
7. 実行して挙動を確認
- エディタ上部の Play ボタンを押してゲームを再生します。
- ゲーム開始時に
AudioPoolTester.start()が呼ばれ、10 回連続でaudioPool.play()が実行されます。 - しかし、AudioPool の 最大同時発音数は 3 に設定されているため、
- 最初の 3 回分だけが実際に再生される。
- 4 回目以降の再生要求は無視される。
- logWhenLimited = true にしている場合、Console に「上限に達したためスキップ」といった警告が表示される。
- 耳で聞いても、3 つ程度の SE が重なって鳴っているだけで、10 個全てが同時に鳴るような爆音にはなっていないことが確認できるはずです。
また、ピッチランダム化を ON にしている場合は、毎回少しずつ音程が変わるため、同じ爆発音を連打しても耳障りになりにくくなります。
8. 実運用での使い方の例
- 爆発エフェクトのスクリプトから呼び出す:
import { _decorator, Component } from 'cc'; import { AudioPool } from './AudioPool'; const { ccclass, property } = _decorator; @ccclass('Explosion') export class Explosion extends Component { @property(AudioPool) public audioPool: AudioPool | null = null; // 爆発演出を開始するときに呼ぶ public playExplosion() { if (this.audioPool) { this.audioPool.play(); } } } - 複数種類の SE を使いたい場合:
- AudioPool は
playClip(clip: AudioClip)も提供しているので、攻撃・被弾・爆発などを 1 つのプールでまとめて制限することも可能です。
- AudioPool は
まとめ
- AudioPool コンポーネントは、同時発音数を制限して爆音を防ぐための、シンプルかつ汎用的な SE 再生コンポーネントです。
- 任意のノードにアタッチするだけで完結し、外部の GameManager やシングルトンに一切依存しません。
- Inspector から
- 最大同時発音数(maxConcurrent)
- デフォルトの AudioClip
- 音量、ランダムピッチ、ログ出力の有無
を調整できるため、ゲームごと・シーンごとに柔軟にチューニングできます。
- 内部ではノード上に複数の AudioSource を自動生成し、空いているものだけを使って再生することで、同時発音数の制限を実現しています。
- このスクリプトをプロジェクトの標準 SE 再生コンポーネントとして採用すれば、
- 爆発・銃声・ヒット音などが重なっても耳障りな爆音になりにくくなる。
- どのシーンでも同じ API(
play(),playClip())で統一的に SE を扱える。 - 調整はすべて Inspector から行えるため、サウンド担当やレベルデザイナーでも簡単に調整できる。
このように、単体で完結する汎用コンポーネントとして設計しておくことで、プロジェクトをまたいで再利用しやすくなり、ゲーム開発の効率が大きく向上します。
ぜひ自分のプロジェクトに組み込んで、SE 周りのクオリティと制御性を高めてみてください。




