Three.js의 THREE.Audio 및 THREE.PositionalAudio는 내부적으로 Web Audio API의 AudioBufferSourceNode를 래핑하여 동작합니다. Web Audio API 설계상 하나의 소스 노드는 단 한 번만 재생할 수 있으며, Three.js의 play() 메서드는 이미 오디오가 재생 중일 때(isPlaying === true) 중복 호출하면 경고를 내며 실행되지 않습니다.
따라서 발사할 때마다 소리를 중첩해서 재생하려면 다음 3가지 방법 중 하나를 선택하여 구현할 수 있습니다.
방법 1. 오디오 객체 풀(Audio Pool) 사용하기 (가장 권장)
동시 재생할 수 있는 PositionalAudio 인스턴스를 미리 여러 개 생성하여 풀(Pool)로 관리하고, 발사할 때마다 사용 중이지 않은 인스턴스를 가져와 재생하는 방식입니다. 가비지 컬렉션(GC) 부담이 없고 성능이 가장 안정적입니다.
class AudioPool {
private pool: THREE.PositionalAudio[] = [];
private size: number;
constructor(listener: THREE.AudioListener, buffer: AudioBuffer, size: number = 10) {
this.size = size;
for (let i = 0; i < size; i++) {
const audio = new THREE.PositionalAudio(listener);
audio.setBuffer(buffer);
// 추가적인 panner 설정 (예: refDistance, rolloffFactor 등)
audio.setRefDistance(1);
audio.setRolloffFactor(1);
this.pool.push(audio);
}
}
// 발사 시점에 호출할 메서드
play(parent: THREE.Object3D) {
// 재생 중이지 않은 오디오 찾기
const idleAudio = this.pool.find(audio => !audio.isPlaying);
if (idleAudio) {
parent.add(idleAudio);
idleAudio.play();
} else {
// 모든 풀이 재생 중인 경우: 가장 오래전에 재생된 오디오를 강제 정지 후 재사용
const fallbackAudio = this.pool[0];
fallbackAudio.stop();
parent.add(fallbackAudio);
fallbackAudio.play();
// 순서를 맨 뒤로 보내서 다음 fallback 대상으로 교체
this.pool.push(this.pool.shift()!);
}
}
}방법 2. 발사 시점에 동적으로 생성하고 소멸시키기
발사 이벤트가 발생할 때마다 새로운 PositionalAudio 인스턴스를 생성하고, 재생이 끝나면 자동으로 부모 객체에서 제거 및 메모리를 해제하는 방식입니다.
function playShootSound(parent: THREE.Object3D, listener: THREE.AudioListener, buffer: AudioBuffer) {
const audio = new THREE.PositionalAudio(listener);
audio.setBuffer(buffer);
parent.add(audio);
audio.play();
// 재생 완료 시 정리 작업
audio.source.onended = () => {
audio.disconnect();
parent.remove(audio);
};
}[!WARNING]
총을 매우 빠르게 연사(초당 수십 발 등)하는 콘텐츠인 경우, 객체의 빈번한 생성과 소멸로 인해 가비지 컬렉션(GC)이 발생하여 프레임 드랍(렉)이 생길 수 있습니다.
방법 3. Web Audio API 레벨에서 소스 노드 직접 생성 및 연결
Three.js의 PositionalAudio가 가지고 있는 panner 노드에 Web Audio API를 사용하여 직접 AudioBufferSourceNode를 동적으로 생성해 연결하는 로우레벨 방식입니다. Three.js의 isPlaying 제약을 우회할 수 있습니다.
function playDirectBuffer(positionalAudio: THREE.PositionalAudio) {
const context = positionalAudio.listener.context;
const buffer = positionalAudio.buffer;
if (!buffer) return;
// Web Audio API 소스 노드 직접 생성
const sourceNode = context.createBufferSource();
sourceNode.buffer = buffer;
// Three.js 오디오의 panner(혹은 적용된 filter)에 노드 연결
const destination = positionalAudio.filters.length > 0
? positionalAudio.filters[0]
: positionalAudio.panner;
sourceNode.connect(destination);
sourceNode.start(0);
// 재생이 끝나면 커넥션 해제
sourceNode.onended = () => {
sourceNode.disconnect();
};
}[!NOTE]
이 방식은 Three.js의audio.isPlaying상태 관리 및audio.stop(),audio.pause()기능이 동작하지 않으므로, 재생 도중 일시정지나 소리 끄기 등의 기능을 수동으로 관리해야 합니다.
요약 및 추천
일반적인 슈팅 게임 환경에서는 방법 1(Audio Pool) 방식이 성능과 기능 관리 편의성 측면에서 가장 안정적이므로 적용하시는 것을 권장합니다.



