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

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

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 알고리즘으로 셔플

🔄 데이터 흐름

초기화 단계
  1. AudioManager.Initialize() 호출 (게임 시작 시)
  2. InitializeAsync 실행:
    • AudioConfig 로드 (Addressable)
    • 3개 채널 생성 (BGM, SFX, Voice)
    • BGMSound, VoiceSound GameObject 생성
    • AudioVolumeManager 초기화
    • 설정 로드 (PlayerPrefs)
  3. GameFlowManager에 IUpdatable 등록
  4. [완료] AudioManager 사용 가능
BGM 재생 프로세스
  1. PlayBGMAsync 호출
  2. [중복 체크] 같은 BGM이 재생 중이면 무시
  3. [페이드 아웃] 현재 BGM 정지 (fadeInDuration)
  4. [Addressable] 새 AudioClip 로드
  5. [볼륨 계산] bgmChannel.GetFinalVolume(BGMVolume)
  6. [BGMSound] PlayAsync 호출:
    • AudioSource.clip 설정
    • volume = 0 (페이드용)
    • AudioSource.Play()
    • AudioFader.FadeInAsync (0 → targetVolume)
  7. [완료] BGM 재생 중
SFX 재생 프로세스
  1. PlaySFXAsync 호출
  2. [SFXChannel] Play2DAsync 실행:
    • 동시 재생 수 체크 (maxConcurrentSFX)
    • 우선순위 비교 (낮으면 거부)
  3. [PoolManager] SFXSound 획득 (없으면 생성)
  4. [Addressable] AudioClip 로드
  5. [SFXSound] Play 실행:
    • AudioSource.clip 설정
    • AudioSource.Play()
    • completionSource 생성
  6. [Update Loop] 재생 완료 감지
  7. [자동 반환] OnPlayComplete → 풀에 반환
  8. [완료] SFX 재생 완료
Voice 재생 프로세스
  1. PlayVoiceAsync 호출
  2. [Addressable] AudioClip 로드
  3. [VoiceSound] PlayAsync 호출:
    • AudioSource.clip 설정
    • AudioSource.Play()
    • MonitorPlaybackAsync 시작
  4. [대기] WaitForVoiceCompleteAsync로 완료 대기
  5. [모니터링] MonitorPlaybackAsync:
    • AudioSource.isPlaying 체크
    • 완료 시 completionSource.TrySetResult()
  6. [스킵 감지] Skip() 호출 시 VoiceCompleteReason.Skipped 반환
  7. [정리] ReleaseCurrentAddress()
  8. [완료] 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가 낮으면 재생 거부

결론

생각보다 할 게 여러가지 떠올라서 기능을 많이 넣었다. 근데도 뭔가 덜 넣은 기분이 드는 건 왤까. 하지만 더이상 생각이 안난다.


© 2022. All rights reserved.