공용 스크립트 제작기🎈 - 오디오 시스템 편

Unity 프로젝트의 공용으로 사용할 수 있는 스크립트를 제작하면서 어떻게 만들었는지, 어떤 점에 신경썼는지 설명하는 글입니다.
게임 만드는데 항상 필요한 것, 그것은 바로 오디오 시스템이다. 그러나 가장 만들기 귀찮은 것이기도 하다. BGM, SFX 등 각각 따로 관리하고, 볼륨 조절도 해야하고, 메모리 관리도 신경 써야 하고, 페이드 효과도 넣으면 좋다. 이를 통합 관리할 수 있는 Audio 시스템을 만들기로 했다.
채널 기반으로 오디오를 분류하고, Addressable과 풀링을 활용해 메모리를 최적화하며, 페이드/크로스페이드 같은 효과까지 내장된 시스템을 만들자. 그리고 사용은 AudioManager를 API처럼 사용하여 하나로 해결하자.
💡예시
간단하게 먼저 사용 예시부터 소개해보자면 다음과 같다.
// BGM 재생 (페이드 인)
await AudioManager.Instance.PlayBGMAsync("Audio/BGM/Title", fadeInDuration: 2f, ct);
// BGM 크로스페이드 전환
await AudioManager.Instance.CrossFadeBGMAsync("Audio/BGM/Battle", duration: 3f, ct);
// SFX 재생 (2D)
await AudioManager.Instance.PlaySFXAsync("Audio/SFX/ButtonClick", volume: 0.8f, ct);
// SFX 재생 (3D 위치)
await AudioManager.Instance.PlaySFXAtPositionAsync(
"Audio/SFX/Explosion",
enemyPosition,
volume: 1f,
ct
);
// Voice 재생 및 완료 대기
await AudioManager.Instance.PlayVoiceAsync("Audio/Voice/Intro", ct);
var reason = await AudioManager.Instance.WaitForVoiceCompleteAsync(ct);
if (reason == VoiceCompleteReason.Skipped)
{
Debug.Log("Voice was skipped");
}
// 볼륨 조절
AudioManager.Instance.MasterVolume = 0.8f;
AudioManager.Instance.BGMVolume = 0.6f;
AudioManager.Instance.SFXVolume = 1.0f;
// 음소거
AudioManager.Instance.IsBGMMuted = true;
// 플레이리스트 사용
var playlist = AudioManager.Instance.CreatePlaylist(
new List<string> { "BGM1", "BGM2", "BGM3" },
AudioPlaylist.PlayMode.Shuffle
);
await playlist.PlayNextBGMAsync(crossFadeDuration: 2f, ct);
📊 클래스 다이어그램
AudioManager (싱글톤)
├─ AudioChannel (BGM, Voice, SFX)
└─ AudioVolume (볼륨/음소거 관리)
AudioSoundBase (추상 클래스)
├─ BGMSound: 듀얼 AudioSource로 크로스페이드
├─ SFXSound: IPoolable, 자동 풀 반환
└─ VoiceSound: 스킵 기능, 완료 감지
Extensions (확장 기능)
├─ AudioPlaylist: 순차/랜덤/셔플 재생
├─ AudioFader: 페이드 인/아웃, 크로스페이드
└─ UIButtonSound: UI 버튼 클릭 사운드 자동 재생
📁 시스템 아키텍처
핵심 컴포넌트
AudioManager (싱글톤 매니저)
- 역할: 모든 오디오의 진입점이자 중앙 관리자
- 주요 기능:
- 3개 채널 관리 (BGM, SFX, Voice)
- Addressable 기반 AudioClip 로드
- 볼륨 및 음소거 통합 제어
- GameFlow에 등록되어 매 프레임 채널 업데이트
- 플레이리스트 생성
- 초기화:
Initialize()에서 비동기로 Config 로드 및 채널 생성- BGMSound, VoiceSound는 런타임에 GameObject로 생성
- SFXSound는 풀링으로 관리
AudioChannel / SFXChannel
- 역할: 채널별 볼륨, 음소거 관리
- AudioChannel 특징:
- BGM, Voice 채널에 사용
- 한 번에 하나만 재생
- 볼륨 계산:
MasterVolume * ChannelVolume * LocalVolume - volumeDirty 플래그로 변경 감지
- SFXChannel 특징 (추정):
- AudioChannel 상속
- 동시 재생 제한 (maxConcurrentSFX)
- 풀링 기반 SFXSound 관리
- 우선순위 시스템 지원
AudioSoundBase (추상 클래스)
- 역할: 모든 사운드의 기본 클래스
- 공통 기능:
- AudioSource 관리
- CurrentAddress 추적 (Addressable 해제용)
- IsPlaying 상태 관리
- ReleaseCurrentAddress() 자동 정리
BGMSound
- 역할: BGM 전용 사운드 (단일 인스턴스)
- 특징:
- PlayAsync: 페이드 인 지원
- StopAsync: 페이드 아웃 지원
- CrossFadeAsync: 두 AudioSource를 핑퐁하며 전환
- Pause/Resume 지원
- CancellationTokenSource로 페이드 중단 가능
SFXSound
- 역할: SFX 전용 사운드 (풀링)
- 특징:
- IPoolable 구현
- 재생 완료 시 자동으로 풀에 반환
- AudioSource는 Prefab에 미리 포함
- UniTaskCompletionSource로 재생 완료 감지
- OnReturnToPool에서 Addressable 해제
VoiceSound
- 역할: Voice/대사 전용 사운드 (단일 인스턴스)
- 특징:
- Skip 기능 (플레이어 입력)
- WaitForCompleteAsync로 완료 대기
- VoiceCompleteReason 반환 (Completed/Skipped/Cancelled)
- MonitorPlaybackAsync로 재생 상태 추적
AudioVolume
- 역할: 볼륨 및 음소거 관리 (단일 책임 원칙)
- 주요 기능:
- Master, BGM, SFX, Voice 볼륨 관리
- 각 채널 음소거 독립 제어
- AudioSettings로 PlayerPrefs에 저장/로드
- 볼륨 변경 시 채널에 MarkVolumeDirty() 호출
- ResetSettings로 기본값 복원
AudioFader
- 역할: 페이드 효과 전담
- 주요 기능:
- FadeInAsync: 0 → targetVolume
- FadeOutAsync: currentVolume → 0
- CrossFadeAsync: 두 AudioSource 동시 페이드 (UniTask.WhenAll)
- CancellationToken 지원
- deltaTime 기반 부드러운 보간
AudioPlaylist
- 역할: 플레이리스트 관리
- PlayMode:
- Sequential: 순차 재생
- Random: 완전 랜덤
- Shuffle: 전체 재생 후 다시 셔플 (중복 방지)
- 주요 기능:
- BGM 플레이리스트: PlayNextBGMAsync, PlayPreviousBGMAsync
- SFX 플레이리스트: PlaySFXAsync
- AddAddress, RemoveAddress로 동적 관리
- Fisher-Yates 알고리즘으로 셔플
🔄 데이터 흐름
초기화 단계
- AudioManager.Initialize() 호출 (게임 시작 시)
↓ - InitializeAsync 실행:
- AudioConfig 로드 (Addressable)
- 3개 채널 생성 (BGM, SFX, Voice)
- BGMSound, VoiceSound GameObject 생성
- AudioVolumeManager 초기화
- 설정 로드 (PlayerPrefs)
↓
- GameFlowManager에 IUpdatable 등록
↓ - [완료] AudioManager 사용 가능
BGM 재생 프로세스
- PlayBGMAsync 호출
↓ - [중복 체크] 같은 BGM이 재생 중이면 무시
- [페이드 아웃] 현재 BGM 정지 (fadeInDuration)
↓ - [Addressable] 새 AudioClip 로드
- [볼륨 계산]
bgmChannel.GetFinalVolume(BGMVolume)
↓ - [BGMSound] PlayAsync 호출:
- AudioSource.clip 설정
- volume = 0 (페이드용)
- AudioSource.Play()
- AudioFader.FadeInAsync (0 → targetVolume)
↓
- [완료] BGM 재생 중
SFX 재생 프로세스
- PlaySFXAsync 호출
↓ - [SFXChannel] Play2DAsync 실행:
- 동시 재생 수 체크 (maxConcurrentSFX)
- 우선순위 비교 (낮으면 거부)
↓
- [PoolManager] SFXSound 획득 (없으면 생성)
- [Addressable] AudioClip 로드
↓ - [SFXSound] Play 실행:
- AudioSource.clip 설정
- AudioSource.Play()
- completionSource 생성
↓
- [Update Loop] 재생 완료 감지
- [자동 반환] OnPlayComplete → 풀에 반환
↓ - [완료] SFX 재생 완료
Voice 재생 프로세스
- PlayVoiceAsync 호출
↓ - [Addressable] AudioClip 로드
- [VoiceSound] PlayAsync 호출:
- AudioSource.clip 설정
- AudioSource.Play()
- MonitorPlaybackAsync 시작
↓
- [대기] WaitForVoiceCompleteAsync로 완료 대기
- [모니터링] MonitorPlaybackAsync:
- AudioSource.isPlaying 체크
- 완료 시 completionSource.TrySetResult()
↓
- [스킵 감지] Skip() 호출 시 VoiceCompleteReason.Skipped 반환
- [정리] ReleaseCurrentAddress()
↓ - [완료] Voice 재생 완료
🎯 신경 쓴 부분
3채널 시스템
오디오를 BGM, SFX, Voice로 명확하게 분리
- BGM: 게임 배경음악, 크로스페이드 전환
- SFX: 효과음, 동시 재생, 풀링
- Voice: 음성/대사, 스킵 기능
이렇게 분리하면 볼륨 조절이 직관적이고, 각 채널에 특화된 기능을 제공할 수 있다.
Addressable 통합
모든 AudioClip을 Addressable로 로드
- 메모리 효율: 필요할 때만 로드, 자동 언로드
- ReleaseCurrentAddress()로 안전한 해제
- 에러 처리: finally 블록에서 clipLoaded 플래그 확인
bool clipLoaded = false; try { clip = await AddressableLoader.Instance.LoadAssetAsync<AudioClip>(address, ct); clipLoaded = true; await bgmSound.PlayAsync(address, clip, volume, fadeInDuration, ct); clipLoaded = false; // 성공 시 책임 이전 } finally { // 로드는 성공했지만 PlayAsync에서 실패한 경우에만 Release if (clipLoaded) { AddressableLoader.Instance.Release(address); } }
SFX 풀링 시스템
SFXSound를 오브젝트 풀로 관리하여 GC 방지
- IPoolable 구현: OnGetFromPool, OnReturnToPool
- 자동 반환: 재생 완료 시 풀에 반환
- 동시 재생 제한: maxConcurrentSFX
- 우선순위: priority가 낮으면 재생 거부
결론
생각보다 할 게 여러가지 떠올라서 기능을 많이 넣었다. 근데도 뭔가 덜 넣은 기분이 드는 건 왤까. 하지만 더이상 생각이 안난다.