【Cocos Creator 3.8】AudioOcclusion の実装:アタッチするだけで「壁越しの音量減衰(遮蔽音)」を実現する汎用スクリプト
このガイドでは、音源とリスナー(プレイヤー)との間に壁などの障害物があるとき、RayCast で検知して自動的に音量を下げる「AudioOcclusion」コンポーネントを実装します。
任意の音源ノード(AudioSource を持つノード)にこのコンポーネントをアタッチし、インスペクタでリスナーや遮蔽時の音量などを設定するだけで、3D/2D問わず「壁越しにこもって聞こえる」効果を簡単に導入できます。
目次
コンポーネントの設計方針
機能要件の整理
- このコンポーネントは「音源側」にアタッチする。
- 音源ノードは
AudioSourceコンポーネントを持っていることが前提。 - リスナー(プレイヤー)ノードをインスペクタから指定できる。
- 毎フレーム(または一定間隔)で、音源 → リスナーへ RayCast を飛ばし、間にコライダーがあれば「遮蔽されている」と判定。
- 遮蔽されている場合は音量を 指定した遮蔽音量まで下げる。
- 遮蔽されていない場合は音量を 元の音量(基準音量)に戻す。
- 音量の変化は、急激に変わらないように スムージング(補間)できるようにする。
- 2D/3D両対応とするため、PhysicsSystem / PhysicsSystem2D のどちらを使うかを選択可能にする。
- 「どのレイヤーのコライダーを遮蔽として扱うか」を ビットマスクで設定可能にする。
- このスクリプト単体で完結し、他のカスタムスクリプトには依存しない。
使用する主な標準コンポーネント・クラス
AudioSource:音量制御の対象。Node,Vec3:位置取得と方向ベクトルの計算。PhysicsSystem(3D)、PhysicsSystem2D(2D)とgeometry.Ray(3D)またはPhysicsRayResult2Dなど。
防御的実装として、必要なコンポーネントが存在しない場合は onLoad で console.error を出力し、以降の処理は安全にスキップします。
インスペクタで設定可能なプロパティ設計
AudioOcclusion コンポーネントに定義する @property は以下の通りです。
listener(Node)- 説明: 音を聞く側(プレイヤーなど)のノード。
- 役割: このノードの位置と音源の位置を結ぶ線上に RayCast を飛ばします。
- 注意: 未設定の場合は遮蔽判定を行わず、エラーログを出して処理をスキップします。
use2DPhysics(boolean)- 説明: 2D 物理を使うかどうか。
- true:
PhysicsSystem2Dによる RayCast。 - false:
PhysicsSystem(3D)による RayCast。
occludedVolume(number)- 説明: 遮蔽されているときの目標音量(0.0〜1.0)。
- 例: 0.2 にすると、壁越しのときの音量が 20% になる。
normalVolume(number)- 説明: 遮蔽されていないときの目標音量(0.0〜1.0)。
- 通常は 1.0(フルボリューム)を指定。
- AudioSource の初期 volume と同期しておくと自然。
volumeLerpSpeed(number)- 説明: 現在の音量を目標音量に近づける速度(1秒あたりの補間係数のイメージ)。
- 例: 5〜10 くらいにすると、0.1〜0.2秒程度でスムーズに変化。
- 0 以下の場合は即時切り替え(補間なし)。
raycastInterval(number)- 説明: RayCast を実行する間隔(秒)。
- 例: 0.05〜0.2 くらいにするとパフォーマンスと精度のバランスが良い。
- 0 以下の場合は毎フレーム RayCast を実行。
maxRayDistance(number)- 説明: RayCast の最大距離。
- 0 以下の場合は「音源とリスナーの距離」を自動で使用。
occlusionLayerMask3D(number)- 説明: 3D 物理で遮蔽として扱うレイヤーのビットマスク。
- 例: デフォルトの 0xffffffff なら全レイヤーが遮蔽対象。
- 3D のときのみ使用。
occlusionLayerMask2D(number)- 説明: 2D 物理で遮蔽として扱うレイヤーのビットマスク。
- 2D のときのみ使用。
debugLog(boolean)- 説明: 遮蔽状態の変化時にログを出すかどうか。
- デバッグ用。リリースでは OFF 推奨。
TypeScriptコードの実装
import {
_decorator,
Component,
Node,
AudioSource,
Vec3,
geometry,
PhysicsSystem,
PhysicsSystem2D,
PhysicsRayResult2D,
} from 'cc';
const { ccclass, property } = _decorator;
/**
* AudioOcclusion
* 音源とリスナーの間にコライダーがある場合、RayCast で検知して音量を下げるコンポーネント。
* - 音源ノードに AudioSource を持たせ、このコンポーネントを追加して使用します。
* - リスナー(プレイヤー)ノードを inspector から指定してください。
*/
@ccclass('AudioOcclusion')
export class AudioOcclusion extends Component {
@property({
type: Node,
tooltip: '音を聞く側(プレイヤーなど)のノード。\nこのノードと音源の間にコライダーがあると遮蔽と判定します。',
})
public listener: Node | null = null;
@property({
tooltip: 'true: 2D 物理 (PhysicsSystem2D) を使用。\nfalse: 3D 物理 (PhysicsSystem) を使用。',
})
public use2DPhysics: boolean = false;
@property({
tooltip: '遮蔽されているときの目標音量 (0.0〜1.0)。\n壁越しにどれくらい音が聞こえるかを設定します。',
min: 0.0,
max: 1.0,
step: 0.01,
})
public occludedVolume: number = 0.2;
@property({
tooltip: '遮蔽されていないときの目標音量 (0.0〜1.0)。\n通常は 1.0 を指定します。',
min: 0.0,
max: 1.0,
step: 0.01,
})
public normalVolume: number = 1.0;
@property({
tooltip: '音量を目標値へ補間する速度。\n値が大きいほどすばやく変化します。\n0 以下の場合は即時切り替え。',
min: 0.0,
step: 0.1,
})
public volumeLerpSpeed: number = 8.0;
@property({
tooltip: 'RayCast を実行する間隔(秒)。\n0 以下の場合は毎フレーム RayCast を実行します。',
min: 0.0,
step: 0.01,
})
public raycastInterval: number = 0.05;
@property({
tooltip: 'RayCast の最大距離。\n0 以下の場合は音源とリスナーの距離を自動で使用します。',
min: 0.0,
step: 0.1,
})
public maxRayDistance: number = 0.0;
@property({
tooltip: '3D 物理で遮蔽として扱うレイヤーのビットマスク。\n0xffffffff なら全レイヤーが対象です。',
})
public occlusionLayerMask3D: number = 0xffffffff;
@property({
tooltip: '2D 物理で遮蔽として扱うレイヤーのビットマスク。\n0xffffffff なら全レイヤーが対象です。',
})
public occlusionLayerMask2D: number = 0xffffffff;
@property({
tooltip: '遮蔽状態が変化したときにログを出力するかどうか(デバッグ用)。',
})
public debugLog: boolean = false;
private _audioSource: AudioSource | null = null;
private _currentTargetVolume: number = 1.0;
private _isOccluded: boolean = false;
private _timeSinceLastRaycast: number = 0.0;
// 再利用用のベクトル・レイ(GC削減)
private static _tempPosA: Vec3 = new Vec3();
private static _tempPosB: Vec3 = new Vec3();
private static _tempDir: Vec3 = new Vec3();
private _ray3D: geometry.Ray = new geometry.Ray();
onLoad() {
// AudioSource の取得
this._audioSource = this.getComponent(AudioSource);
if (!this._audioSource) {
console.error('[AudioOcclusion] AudioSource コンポーネントが見つかりません。このノードに AudioSource を追加してください。', this.node.name);
} else {
// AudioSource の初期音量を normalVolume に反映(初期値が 1.0 以外のケースも考慮)
this.normalVolume = this._audioSource.volume;
}
// 初期状態では遮蔽されていないとみなす
this._isOccluded = false;
this._currentTargetVolume = this.normalVolume;
}
start() {
if (!this.listener) {
console.error('[AudioOcclusion] listener が設定されていません。リスナーとなるノードを Inspector から指定してください。', this.node.name);
}
// 初期ボリュームを反映
if (this._audioSource) {
this._audioSource.volume = this.normalVolume;
}
}
update(deltaTime: number) {
if (!this._audioSource || !this.listener) {
// 必須コンポーネントまたはリスナー未設定時は何もしない
return;
}
// RayCast の実行タイミング管理
let shouldRaycast = false;
if (this.raycastInterval = this.raycastInterval) {
this._timeSinceLastRaycast = 0;
shouldRaycast = true;
}
}
if (shouldRaycast) {
this._updateOcclusionState();
}
// 音量の補間
this._updateVolume(deltaTime);
}
/**
* 現在の遮蔽状態を RayCast によって更新する。
*/
private _updateOcclusionState() {
const from = AudioOcclusion._tempPosA;
const to = AudioOcclusion._tempPosB;
const dir = AudioOcclusion._tempDir;
this.node.worldPosition.clone().assign(from);
this.listener!.worldPosition.clone().assign(to);
// direction = to - from
Vec3.subtract(dir, to, from);
const distance = dir.length();
if (distance <= 0.0001) {
// リスナーがほぼ同じ位置にいる場合は遮蔽なしとみなす
this._setOccluded(false);
return;
}
Vec3.normalize(dir, dir);
const maxDistance = (this.maxRayDistance > 0) ? Math.min(this.maxRayDistance, distance) : distance;
if (this.use2DPhysics) {
this._raycast2D(from, dir, maxDistance);
} else {
this._raycast3D(from, dir, maxDistance);
}
}
/**
* 3D 物理で RayCast を行い、遮蔽状態を判定。
*/
private _raycast3D(origin: Vec3, direction: Vec3, distance: number) {
const physics3D = PhysicsSystem.instance;
if (!physics3D) {
console.error('[AudioOcclusion] 3D PhysicsSystem が有効ではありません。Project Settings で 3D Physics を有効にしてください。');
return;
}
// Ray の設定
this._ray3D.o.set(origin);
this._ray3D.d.set(direction);
// closest = true で最も近いヒットのみ取得
const hit = physics3D.raycastClosest(this._ray3D, this.occlusionLayerMask3D, distance);
if (!hit) {
// 何もヒットしなければ遮蔽なし
this._setOccluded(false);
return;
}
const result = physics3D.raycastClosestResult;
if (!result) {
this._setOccluded(false);
return;
}
// ヒットしたコライダーがリスナー自身であれば遮蔽なし、
// それ以外なら遮蔽ありとみなす。
const hitNode = result.collider?.node;
if (hitNode === this.listener) {
this._setOccluded(false);
} else {
this._setOccluded(true);
}
}
/**
* 2D 物理で RayCast を行い、遮蔽状態を判定。
*/
private _raycast2D(origin: Vec3, direction: Vec3, distance: number) {
const physics2D = PhysicsSystem2D.instance;
if (!physics2D) {
console.error('[AudioOcclusion] 2D PhysicsSystem が有効ではありません。Project Settings で 2D Physics を有効にしてください。');
return;
}
// 2D では x, y のみを使用(z は無視)
const fromX = origin.x;
const fromY = origin.y;
const toX = origin.x + direction.x * distance;
const toY = origin.y + direction.y * distance;
const results: PhysicsRayResult2D[] = [];
const hitCount = physics2D.raycast(
{ x: fromX, y: fromY },
{ x: toX, y: toY },
this.occlusionLayerMask2D,
true,
results
);
if (hitCount === 0) {
this._setOccluded(false);
return;
}
// 最も近いヒットのみを見る(results は距離順とは限らないので、自前で最小を探す)
let closest: PhysicsRayResult2D | null = null;
let minFraction = Number.MAX_VALUE;
for (let i = 0; i < hitCount; i++) {
const r = results[i];
if (r.fraction < minFraction) {
minFraction = r.fraction;
closest = r;
}
}
if (!closest) {
this._setOccluded(false);
return;
}
const hitNode = closest.collider.node;
if (hitNode === this.listener) {
this._setOccluded(false);
} else {
this._setOccluded(true);
}
}
/**
* 遮蔽状態フラグを更新し、目標音量を設定する。
*/
private _setOccluded(occluded: boolean) {
if (this._isOccluded === occluded) {
return;
}
this._isOccluded = occluded;
this._currentTargetVolume = occluded ? this.occludedVolume : this.normalVolume;
if (this.debugLog) {
console.log(`[AudioOcclusion] Occlusion changed: ${occluded ? 'OCCLUDED' : 'CLEAR'} on node "${this.node.name}"`);
}
}
/**
* 現在の音量を目標音量へ近づける。
*/
private _updateVolume(deltaTime: number) {
if (!this._audioSource) {
return;
}
const current = this._audioSource.volume;
const target = this._currentTargetVolume;
if (this.volumeLerpSpeed <= 0) {
// 補間なしで即時反映
if (current !== target) {
this._audioSource.volume = target;
}
return;
}
// 線形補間 (exponential に近づけたい場合は別の式も可)
const t = 1 - Math.exp(-this.volumeLerpSpeed * deltaTime);
const newVolume = current + (target - current) * t;
// 数値誤差を抑制
if (Math.abs(newVolume - target) < 0.001) {
this._audioSource.volume = target;
} else {
this._audioSource.volume = newVolume;
}
}
}
コードの主要部分の解説
onLoad- 同じノードから
AudioSourceを取得し、存在しない場合はconsole.errorを出力して警告。 - AudioSource が存在する場合、その
volumeをnormalVolumeの初期値として使用。 - 初期状態では「遮蔽なし」として
_isOccluded = false,_currentTargetVolume = normalVolumeを設定。
- 同じノードから
startlistenerが未設定の場合にエラーログを出力。- AudioSource の現在の
volumeをnormalVolumeに合わせて初期化。
update- AudioSource または listener がない場合は何もしない。
raycastIntervalに基づいて RayCast 実行タイミングを制御。- RayCast が必要なタイミングになったら
_updateOcclusionStateを呼び出す。 - 毎フレーム
_updateVolumeを呼び出して、現在の音量を目標音量へ補間。
_updateOcclusionState- 音源ノードとリスナーノードの
worldPositionを取得。 - 方向ベクトルと距離を計算し、距離が極端に短い場合は遮蔽なしと判定。
use2DPhysicsに応じて_raycast2Dまたは_raycast3Dを呼び出す。
- 音源ノードとリスナーノードの
_raycast3D/_raycast2D- それぞれ 3D / 2D の PhysicsSystem を使って RayCast を実行。
- 最も近いヒットが「リスナー自身」であれば遮蔽なし、それ以外なら遮蔽ありと判定。
- 「何もヒットしない」場合も遮蔽なしと判定。
_setOccluded- 内部フラグ
_isOccludedを更新し、_currentTargetVolumeをoccludedVolumeまたはnormalVolumeに設定。 debugLogが true の場合、遮蔽状態の変化をログ出力。
- 内部フラグ
_updateVolume- 現在の音量と目標音量の差を、
volumeLerpSpeedとdeltaTimeに基づいて補間。 volumeLerpSpeed <= 0の場合は補間せず即時反映。- 指数関数的に近づけることで、フレームレートに依存しにくいスムーズな変化を実現。
- 現在の音量と目標音量の差を、
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
AudioOcclusion.tsとします。 - 作成された
AudioOcclusion.tsをダブルクリックして開き、内容をすべて削除して、前章の TypeScript コード全体 を貼り付けて保存します。
2. テスト用シーンの準備(共通)
- Hierarchy パネルで右クリック → Create → 2D/3D の任意のノード(例:
NodeやSprite、3D Object)を作成し、名前をPlayerとします。- このノードが「リスナー(プレイヤー)」になります。
- 同様に、音源用のノードを作成します(例:
SoundSource)。 SoundSourceノードを選択し、Inspector で Add Component → Audio → AudioSource を追加します。- AudioSource の Clip に任意の音声クリップを設定し、Loop にチェックを入れてループ再生にします。
3. 2D での動作確認例
- 2D 物理を有効化していない場合は、Project → Project Settings → Physics 2D で有効にします。
- Hierarchy で
Canvas(または 2D 用のルートノード)配下に以下のノードを作成します。Player(既に作成している場合はそれを使用)SoundSource(AudioSource を持つノード)Wall(遮蔽用の壁)
Wallノードを選択し、Add Component → Physics 2D → BoxCollider2D を追加します。- サイズや位置を調整して、
PlayerとSoundSourceの間に壁が来るように配置します。 - 必要に応じて RigidBody2D も追加します(静的壁なら
Type = Static)。
- サイズや位置を調整して、
SoundSourceノードを選択し、Add Component → Custom → AudioOcclusion を追加します。- Inspector の AudioOcclusion セクションで以下のように設定します。
- Listener:
Playerノードをドラッグ&ドロップ。 - Use 2D Physics: チェックを ON。
- Occluded Volume:
0.2程度。 - Normal Volume:
1.0(AudioSource の volume と合わせる)。 - Volume Lerp Speed:
8〜10。 - Raycast Interval:
0.05。 - Max Ray Distance:
0(自動で音源〜リスナー距離を使用)。 - Occlusion Layer Mask 2D: デフォルトのままで問題ありません(壁のレイヤーを含んでいることを確認)。
- Debug Log: 動作確認時は ON にすると状態変化がコンソールに表示されます。
- Listener:
- 再生ボタン(▶)でシーンを実行し、以下を確認します。
PlayerとSoundSourceの間にWallがあるとき:音量が 徐々に occludedVolume まで下がる。- 壁を動かして線上から外す、または Player を動かして壁を避けると:音量が normalVolume に戻る。
Debug Logを ON にしている場合、コンソールにOCCLUDED/CLEARが表示される。
4. 3D での動作確認例
- 3D 物理を有効化していない場合は、Project → Project Settings → Physics 3D で有効にします。
- Hierarchy で 3D シーン用のノードを作成します。
Player(任意の 3D ノード、例:Empty Node)SoundSource(AudioSource を持つノード)Wall(3D の壁オブジェクト、例: Cube)
Wallノードを選択し、Add Component → Physics → BoxCollider を追加します。- 必要に応じて RigidBody を追加し、
Type = Staticに設定。
- 必要に応じて RigidBody を追加し、
SoundSourceノードに AudioSource と AudioOcclusion コンポーネントを追加します。- Inspector の AudioOcclusion セクションで以下のように設定します。
- Listener:
Playerノード。 - Use 2D Physics: チェックを OFF(3D を使う)。
- その他のパラメータ(Occluded Volume, Normal Volume, Volume Lerp Speed など)は 2D の例と同様。
- Occlusion Layer Mask 3D: 壁の Collider が属するレイヤーを含むマスクを設定。
- Listener:
- シーンを再生し、Player や Wall を動かして以下を確認します。
- 音源と Player の間に壁が入るときに音量が下がる。
- 壁が線上から外れたら音量が戻る。
5. よくあるハマりどころとチェックポイント
- AudioSource が同じノードに付いていない
- AudioOcclusion は 同じノード上の AudioSource を探します。別ノードにある場合は動作しません。
- listener が未設定
- Inspector の listener プロパティに、必ずプレイヤーノードを割り当ててください。
- 物理システムが無効
- 2D / 3D どちらを使うかに応じて、Project Settings で該当の Physics を有効にしてください。
- レイヤーマスクの設定ミス
- 壁の Collider が属するレイヤーが、
occlusionLayerMask2D/3Dに含まれているか確認してください。
- 壁の Collider が属するレイヤーが、
まとめ
この「AudioOcclusion」コンポーネントを使えば、
- 音源ノードに AudioSource + AudioOcclusion をアタッチするだけで
- プレイヤーとの間に壁があるかどうかを 自動で RayCast 判定し
- 遮蔽時 / 非遮蔽時の音量を スムーズに補間して切り替える
という、実用的な「遮蔽音」表現を簡単に導入できます。
このスクリプトは完全に独立しており、GameManager やシングルトンに依存しないため、
- 任意のシーン・任意のプロジェクトにそのままコピーして再利用できる
- 複数の音源(例: ドアの向こうのテレビ、別室の NPC など)に対して簡単に適用できる
といったメリットがあります。
応用例としては、
- 遮蔽時に音量だけでなく Low-pass フィルタを併用して「こもった音」を再現する
- 遮蔽レベルを複数段階にして、「薄い壁」「厚い壁」で減衰量を変える
- Ray を複数本(広がりを持たせて)飛ばし、「部分的に遮蔽されている」状況を表現する
なども考えられます。
まずは本記事の実装をベースに、プロジェクトに合わせた調整や拡張を加えつつ、より臨場感のあるサウンド演出に発展させてみてください。
