【Cocos Creator 3.8】EarRinging コンポーネントの実装:アタッチするだけで「爆発直後のキーンという耳鳴り状態」を再現する汎用スクリプト
このコンポーネントは、手榴弾などの爆発で大ダメージを受けた直後に「環境音やBGMを一時的にミュートし、キーンという耳鳴り音だけを鳴らす」演出を簡単に実装するためのものです。
任意のノードにアタッチしておき、ゲーム中でメソッドを呼ぶだけで「耳鳴り状態」を開始できます。既存の BGM や SE 管理スクリプトに依存せず、このコンポーネント単体で完結するように設計します。
コンポーネントの設計方針
機能要件の整理
- 爆発などのイベント時に「耳鳴り状態」を開始できる。
- 耳鳴り状態中は:
- 指定した AudioSource(BGM や環境音など)を一時的にミュートする。
- 耳鳴り用の AudioSource から「キーン」という音を再生する。
- 時間経過とともに耳鳴り音のボリュームをフェードアウトし、同時に元の AudioSource のボリュームをフェードインする。
- 耳鳴り状態の長さやフェード時間、ボリュームなどをインスペクタから調整できる。
- 外部の GameManager やシングルトンに依存せず、このスクリプトだけで完結する。
外部依存をなくすためのアプローチ
- 「耳鳴り音」を鳴らすための
AudioSourceを、このコンポーネントがアタッチされたノードに追加して利用する。 - 「ミュート/フェードさせたい音」(BGM や環境音など)の
AudioSourceは、インスペクタで配列として指定する。 - 耳鳴り状態の開始は、外部から
triggerEarRinging()メソッドを呼び出すだけでよいようにする。 - 必要な
AudioSourceが存在しない場合はgetComponentでチェックし、エラーログを出して処理をスキップする防御的実装にする。
@property で設定可能なプロパティ設計
インスペクタで調整できるプロパティと役割は次の通りです。
- earRingingAudioSource: AudioSource
- 耳鳴り用の音を再生する AudioSource。
- このコンポーネントと同じノード、または任意のノードの AudioSource を指定。
- 事前に AudioClip(キーン音)を設定しておく。
- targetAudioSources: AudioSource[]
- 耳鳴り中に一時的にボリュームを下げたい(ほぼミュートしたい)AudioSource の配列。
- BGM、環境音、効果音など自由に指定可能。
- ringDuration: number
- 耳鳴り状態の総時間(秒)。
- 例: 3.0 にすると、3秒かけて耳鳴りが収束し、元の音量が戻る。
- fadeInDuration: number
- 耳鳴り開始直後に、耳鳴り音を「0 → 最大音量」にフェードインする時間(秒)。
- 小さくすると、いきなりキーンと鳴る。大きくすると、じわっと耳鳴りが立ち上がる。
- fadeOutDuration: number
- 耳鳴り終了に向けて、耳鳴り音を「最大音量 → 0」にフェードアウトする時間(秒)。
- 通常は
ringDurationと同じか、それより短めに設定。
- earMaxVolume: number
- 耳鳴り音の最大ボリューム。
- 0.0〜1.0 の範囲で指定。1.0 が AudioSource の本来の最大音量。
- targetMutedVolume: number
- 耳鳴り中に、ターゲット AudioSource をどの程度まで下げるか。
- 0.0 で完全ミュート。0.1〜0.2 などにすると、うっすら音が残る表現も可能。
- autoRestoreOnDisable: boolean
- コンポーネントが無効化(disable)されたときに、音量を強制的に元の状態へ戻すかどうか。
- シーン遷移やノード破棄時に音量が戻らない事故を防ぐ目的。
- logDebug: boolean
- デバッグ用ログを出すかどうか。
- 挙動確認時のみ
trueにしておくと便利。
TypeScriptコードの実装
import { _decorator, Component, AudioSource, clamp01, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* EarRinging
* 爆発などの直後に、指定した AudioSource 群を一時的にミュートし、
* 耳鳴り用 AudioSource をフェードイン/アウトさせるコンポーネント。
*
* 外部 GameManager などに依存せず、このスクリプト単体で完結する設計。
*/
@ccclass('EarRinging')
export class EarRinging extends Component {
@property({
type: AudioSource,
tooltip: '耳鳴り用の AudioSource。\nここにキーン音の AudioClip を設定しておきます。'
})
public earRingingAudioSource: AudioSource | null = null;
@property({
type: [AudioSource],
tooltip: '耳鳴り中にボリュームを下げる対象の AudioSource 配列。\nBGM や環境音などを指定します。'
})
public targetAudioSources: AudioSource[] = [];
@property({
tooltip: '耳鳴り状態の総時間(秒)。\nこの時間をかけて耳鳴りが収束し、元の音量に戻ります。'
})
public ringDuration: number = 3.0;
@property({
tooltip: '耳鳴り開始時のフェードイン時間(秒)。\n0 にするといきなり最大音量になります。'
})
public fadeInDuration: number = 0.2;
@property({
tooltip: '耳鳴り終了に向けたフェードアウト時間(秒)。\n通常は ringDuration と同じか、それより短くします。'
})
public fadeOutDuration: number = 2.5;
@property({
tooltip: '耳鳴り音の最大ボリューム(0〜1)。'
})
public earMaxVolume: number = 1.0;
@property({
tooltip: '耳鳴り中にターゲット AudioSource を下げるボリューム(0〜1)。\n0 で完全ミュート。'
})
public targetMutedVolume: number = 0.05;
@property({
tooltip: 'コンポーネントが無効化されたときに、音量を元に戻すかどうか。'
})
public autoRestoreOnDisable: boolean = true;
@property({
tooltip: 'デバッグ用ログを出力するかどうか。'
})
public logDebug: boolean = false;
// 内部状態管理用
private _isRinging: boolean = false;
private _elapsed: number = 0;
// 対象 AudioSource の元の音量を記録しておく
private _originalTargetVolumes: number[] = [];
// 耳鳴り AudioSource の元の音量
private _originalEarVolume: number = 1.0;
onLoad() {
// 必要なコンポーネントの存在チェック
if (!this.earRingingAudioSource) {
// 同じノードに AudioSource があれば自動取得を試みる
const found = this.getComponent(AudioSource);
if (found) {
this.earRingingAudioSource = found;
if (this.logDebug) {
console.warn('[EarRinging] earRingingAudioSource が未設定だったため、同一ノードの AudioSource を自動で利用します。');
}
} else {
console.error('[EarRinging] earRingingAudioSource が設定されておらず、同一ノードにも AudioSource が見つかりません。耳鳴り音を再生できません。');
}
}
if (this.earRingingAudioSource) {
this._originalEarVolume = this.earRingingAudioSource.volume;
}
// 対象 AudioSource の元のボリュームを記録
this._cacheOriginalTargetVolumes();
}
start() {
// 特に開始時に動作はしない(外部から triggerEarRinging() が呼ばれる想定)
}
update(deltaTime: number) {
if (!this._isRinging) {
return;
}
this._elapsed += deltaTime;
// 安全のため、負の値や NaN を防ぐ
const duration = Math.max(this.ringDuration, 0.0001);
// 0〜1 の正規化時間
const t = clamp01(this._elapsed / duration);
// 耳鳴り音のフェードイン/アウト
this._updateEarVolume(t);
// ターゲット AudioSource のボリュームを補間
this._updateTargetVolumes(t);
// 終了判定
if (this._elapsed >= duration) {
this._finishRinging();
}
}
/**
* 耳鳴り状態を開始する。
* 外部スクリプトから呼び出して使用する想定。
*/
public triggerEarRinging(): void {
if (!this.earRingingAudioSource) {
console.error('[EarRinging] triggerEarRinging() が呼ばれましたが、earRingingAudioSource が設定されていません。');
return;
}
if (this.logDebug) {
console.log('[EarRinging] triggerEarRinging() 呼び出し。耳鳴り状態を開始します。');
}
// 既に耳鳴り中であればリスタート
this._isRinging = true;
this._elapsed = 0;
// 元の音量を再キャッシュ(途中で変更されている可能性があるため)
this._cacheOriginalTargetVolumes();
this._originalEarVolume = this.earRingingAudioSource.volume;
// 耳鳴り音を再生
if (!this.earRingingAudioSource.playing) {
this.earRingingAudioSource.play();
}
// 開始時点では耳鳴り音を 0 にしておく(フェードイン用)
this.earRingingAudioSource.volume = 0;
// ターゲット AudioSource は開始時点で元の音量から徐々に下げるため、
// ここでは即時にはミュートせず、update() 内で補間する。
}
/**
* 耳鳴り状態が進行中かどうか。
*/
public get isRinging(): boolean {
return this._isRinging;
}
/**
* 元のボリュームをキャッシュ。
*/
private _cacheOriginalTargetVolumes(): void {
this._originalTargetVolumes.length = 0;
if (!this.targetAudioSources) {
return;
}
for (let i = 0; i < this.targetAudioSources.length; i++) {
const src = this.targetAudioSources[i];
if (!src) {
this._originalTargetVolumes.push(1.0);
if (this.logDebug) {
console.warn(`[EarRinging] targetAudioSources[${i}] が null です。`);
}
continue;
}
this._originalTargetVolumes.push(src.volume);
}
}
/**
* 正規化時間 t (0〜1) に応じて耳鳴り AudioSource のボリュームを更新。
*/
private _updateEarVolume(t: number): void {
if (!this.earRingingAudioSource) {
return;
}
// フェードインフェーズの割合
const fadeIn = Math.max(this.fadeInDuration, 0);
const fadeOut = Math.max(this.fadeOutDuration, 0);
let volume = 0;
// フェードイン
if (fadeIn > 0 && this._elapsed <= fadeIn) {
const ti = clamp01(this._elapsed / fadeIn);
volume = math.lerp(0, this.earMaxVolume, ti);
}
// フェードアウト
else if (fadeOut > 0 && this._elapsed >= (this.ringDuration - fadeOut)) {
const remaining = this.ringDuration - this._elapsed;
const to = clamp01(remaining / fadeOut); // 残り時間割合
volume = math.lerp(0, this.earMaxVolume, to);
}
// 安定フェーズ(最大音量維持)
else {
volume = this.earMaxVolume;
}
// 0〜1 にクランプ
volume = clamp01(volume);
this.earRingingAudioSource.volume = volume;
}
/**
* 正規化時間 t (0〜1) に応じてターゲット AudioSource のボリュームを更新。
* t=0 で元の音量、t=1 で targetMutedVolume に近づく → その後の時間で元に戻す、というより、
* 耳鳴り全体の間、基本的には targetMutedVolume 付近を維持し、
* 終了に向けて元の音量へ戻していくイメージ。
*/
private _updateTargetVolumes(t: number): void {
if (!this.targetAudioSources) {
return;
}
const fadeOut = Math.max(this.fadeOutDuration, 0);
const duration = Math.max(this.ringDuration, 0.0001);
for (let i = 0; i < this.targetAudioSources.length; i++) {
const src = this.targetAudioSources[i];
if (!src) {
continue;
}
const original = this._originalTargetVolumes[i] ?? src.volume;
let volume = original;
// 耳鳴り開始時〜中盤までは、元の音量 → targetMutedVolume へ素早く落とす
const fadeDownTime = Math.min(0.2, duration * 0.2); // 0.2秒 or 全体の20% くらいで落とす
if (this._elapsed <= fadeDownTime) {
const td = clamp01(this._elapsed / fadeDownTime);
volume = math.lerp(original, this.targetMutedVolume, td);
}
// 終了に向けて元の音量へ戻す
else if (fadeOut > 0 && this._elapsed >= (duration - fadeOut)) {
const passed = this._elapsed - (duration - fadeOut);
const tr = clamp01(passed / fadeOut);
volume = math.lerp(this.targetMutedVolume, original, tr);
}
// それ以外の時間帯は targetMutedVolume を維持
else {
volume = this.targetMutedVolume;
}
src.volume = clamp01(volume);
}
}
/**
* 耳鳴り状態の終了処理。
*/
private _finishRinging(): void {
this._isRinging = false;
// 耳鳴り音を停止し、元の音量に戻す
if (this.earRingingAudioSource) {
this.earRingingAudioSource.stop();
this.earRingingAudioSource.volume = this._originalEarVolume;
}
// ターゲット AudioSource の音量を元に戻す
this._restoreTargetVolumes();
if (this.logDebug) {
console.log('[EarRinging] 耳鳴り状態が終了しました。音量を元に戻しました。');
}
}
/**
* 対象 AudioSource の音量を元に戻す。
*/
private _restoreTargetVolumes(): void {
if (!this.targetAudioSources) {
return;
}
for (let i = 0; i < this.targetAudioSources.length; i++) {
const src = this.targetAudioSources[i];
if (!src) {
continue;
}
const original = this._originalTargetVolumes[i];
if (typeof original === 'number') {
src.volume = original;
}
}
}
/**
* コンポーネントが無効化されたときの処理。
* シーン遷移などで中途半端な状態にならないよう、必要なら音量を復元する。
*/
onDisable() {
if (!this.autoRestoreOnDisable) {
return;
}
if (this._isRinging) {
if (this.logDebug) {
console.warn('[EarRinging] コンポーネントが無効化されました。耳鳴り状態を強制終了し、音量を復元します。');
}
}
this._isRinging = false;
// 耳鳴り音の停止と復元
if (this.earRingingAudioSource) {
this.earRingingAudioSource.stop();
this.earRingingAudioSource.volume = this._originalEarVolume;
}
// ターゲットの音量復元
this._restoreTargetVolumes();
}
}
主要メソッドの動作解説
- onLoad()
earRingingAudioSourceが未設定の場合、同一ノードのAudioSourceを自動取得。- 見つからなければ
console.errorを出して終了(耳鳴り音は鳴らないが、ゲームは落ちない)。 - ターゲット AudioSource の元の音量をキャッシュ。
- update(deltaTime)
_isRingingがtrueの間だけ動作。- 経過時間
_elapsedを進め、0〜1 に正規化したtを使ってフェード処理。 _updateEarVolume(t)で耳鳴り音のフェードイン/アウト。_updateTargetVolumes(t)でターゲット AudioSource のボリュームをミュート寄りにしてから元に戻す。- 時間が
ringDurationを超えたら_finishRinging()を呼んで終了処理。
- triggerEarRinging()
- 外部から耳鳴り状態を開始するための公開メソッド。
- 内部状態をリセットし、元音量を再キャッシュ。
- 耳鳴り AudioSource を再生し、ボリューム 0 からスタート(フェードイン用)。
- onDisable()
autoRestoreOnDisableがtrueのとき、耳鳴り状態を強制終了し、すべての音量を元に戻す。- シーン遷移やノード破棄時に音量が中途半端になる事故を防ぐ。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - フォルダ上で右クリック → Create → TypeScript を選択します。
- ファイル名を
EarRinging.tsに変更します。 - 作成された
EarRinging.tsをダブルクリックで開き、上記の TypeScript コードをすべて貼り付けて保存します。
2. テスト用シーンとノードの用意
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、名前を
AudioRootなどに変更します。 AudioRootを選択し、Inspector の Add Component ボタンをクリックします。- Audio → AudioSource を追加し、これを BGM 用とします。
- この AudioSource の Clip に、任意の BGM 用 AudioClip(ループ再生しても違和感のない音)を設定します。
- Loop にチェックを入れ、Play On Load もオンにしておくと、シーン再生時に自動で鳴り始めます。
- 名前は分かりやすく
BGMSourceなどに変更しておくとよいです。
- 同様に、環境音などを試したい場合は、別ノードを作成して AudioSource を追加しておきます。
3. 耳鳴り用ノードと AudioSource の設定
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、名前を
EarRingingControllerに変更します。 EarRingingControllerを選択し、Inspector の Add Component → Audio → AudioSource を追加します。- この AudioSource の Clip に、「キーン」という耳鳴り用の AudioClip を設定します。
- Loop は通常オフで問題ありませんが、耳鳴り音が短い場合はループさせてもよいです。
- Play On Load はオフにしてください(スクリプトから再生を制御するため)。
4. EarRinging コンポーネントのアタッチとプロパティ設定
EarRingingControllerノードを選択した状態で、Inspector の Add Component → Custom → EarRinging を選択してアタッチします。- EarRinging コンポーネントのプロパティを設定します。
- Ear Ringing Audio Source:
- 自動で同一ノードの AudioSource がセットされていなければ、
EarRingingControllerの AudioSource をドラッグ&ドロップで指定します。
- 自動で同一ノードの AudioSource がセットされていなければ、
- Target Audio Sources:
- サイズ(Size)を
1に設定し、要素0にAudioRoot(または BGM 用ノード)の AudioSource をドラッグ&ドロップします。 - 環境音なども一緒にミュートしたい場合は、Size を増やしてそれぞれの AudioSource を追加します。
- サイズ(Size)を
- Ring Duration:
3.0〜4.0秒程度から試してみてください。 - Fade In Duration:
0.1〜0.3秒程度。0にすると即座に最大音量になります。 - Fade Out Duration:
2.0〜3.0秒程度。耳鳴りが徐々に引いていく感覚を出します。 - Ear Max Volume:
1.0(うるさければ0.6〜0.8程度に下げる)。 - Target Muted Volume:
0.0で完全ミュート。0.05〜0.1くらいにすると、かすかに音が残るリアルな表現になります。 - Auto Restore On Disable: 通常は
trueのままで OK。 - Log Debug: 動作確認中は
trueにしてログを見ながら調整すると便利です。
- Ear Ringing Audio Source:
5. 耳鳴り状態をトリガーする簡易テスト
シンプルなテストとして、キー入力で耳鳴りを発動させてみます。
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、名前を
TestInputに変更します。 TestInput.tsという TypeScript スクリプトを作成し、以下のような簡易コードを貼り付けます。
import { _decorator, Component, input, Input, KeyCode, EventKeyboard } from 'cc';
import { EarRinging } from './EarRinging';
const { ccclass, property } = _decorator;
@ccclass('TestInput')
export class TestInput extends Component {
@property({ type: EarRinging, tooltip: '耳鳴りを制御する EarRinging コンポーネント。' })
public earRinging: EarRinging | null = null;
onLoad() {
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
onDestroy() {
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
}
private onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.SPACE) {
if (this.earRinging) {
this.earRinging.triggerEarRinging();
}
}
}
}
TestInputノードに TestInput コンポーネントをアタッチします。TestInputの Ear Ringing プロパティに、EarRingingControllerノード上の EarRinging コンポーネントをドラッグ&ドロップで指定します。- シーンを再生し、BGM が鳴っている状態でキーボードの Space キーを押すと、耳鳴り状態が発動し、BGM がミュートされてキーンという音だけが鳴り、時間経過とともに元に戻ることを確認できます。
※ 実際のゲームでは、爆発や大ダメージを受けたタイミングで triggerEarRinging() を呼び出すように実装すれば、そのまま演出として利用できます。
まとめ
- EarRinging コンポーネントは、Cocos Creator 3.8.7 と TypeScript で実装した「耳鳴り演出」専用の汎用スクリプトです。
- 任意のノードにアタッチし、耳鳴り用の AudioSource と、ミュートしたい AudioSource 群をインスペクタで指定するだけで、外部の GameManager やサウンド管理スクリプトに依存せず動作します。
- 耳鳴りの長さ、フェードイン/アウト時間、耳鳴り音量、元の音のミュート量などをすべてプロパティで調整できるため、リアル寄りの表現から誇張した演出まで幅広く対応できます。
- このコンポーネントをプロジェクトの「共通演出」として用意しておけば、爆発武器やトラップ、巨大ボスの攻撃など、どのシーン・どのギミックからでも簡単に耳鳴り演出を呼び出せるようになり、ゲーム全体の演出クオリティと開発効率を大きく向上させられます。
あとは、実際のゲームに合わせて AudioClip やプロパティ値を微調整し、自分の作品に最適な「キーン…」を作り込んでみてください。




