어떤 게임이든 플레이할 때 사운드가 필요하므로 해당 사운드를 관리하는 오디오매니저를 구현할 것이다.
1. 목적
2. 기능 명세
3. 구현
4. 후기
1. 목적
현재 프로젝트에서 사운드 관련해서 전체적으로 총괄하는 매니저를 구현할 예정이다. 사운드에 대한 볼륨이 그렇게 크지 않으므로 매니저 1개만 만들어서 구성하고 테스트할 예정이다.
2. 기능 명세
처음에 오디오믹서를 적용하려 했는데, 일단 사운드 관리하는 부분 먼저 관리하고 추후 마스터볼륨을 추가했을 때 오디오믹서를 적용하기로 했다.
현재 가장 필요한 기능은 다음과 같다.
* 자동 초기화 : 씬에 AudioSource가 없어도 자동으로 만든다.
* 캐싱 시스템 : 한 번 불러온 소리는 Dictionary에 저장되어 두 번째부터는 로딩 없이 즉시 재생한다.
* 채널 분리 : BGM과 SFX를 철저히 분리한다. (배경음악 재생/정지, 효과음 재생/정지)
* 음소거 기능
3. 구현
코드는 다음과 같다.
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : SingletonBehaviour<AudioManager>
{
private Dictionary<string, AudioClip> _audioClips = new Dictionary<string, AudioClip>();
private AudioSource _bgmPlayer;
private AudioSource _sfxPlayer;
protected override void Init()
{
base.Init();
GameObject bgmObject = new GameObject("BGM_Player");
bgmObject.transform.SetParent(transform);
_bgmPlayer = bgmObject.AddComponent<AudioSource>();
_bgmPlayer.loop = true;
_bgmPlayer.playOnAwake = false;
GameObject sfxObject = new GameObject("SFX_Player");
sfxObject.transform.SetParent(transform);
_sfxPlayer = sfxObject.AddComponent<AudioSource>();
_sfxPlayer.loop = false;
_sfxPlayer.playOnAwake = false;
Logger.Log("AudioManager : Initialized");
}
// 배경음악 재생
public void PlayBGM(string bgmName, float volume = 1.0f)
{
if (string.IsNullOrEmpty(bgmName))
{
Logger.Log("AudioManager.PlayBGM: bgmName is null or empty");
return;
}
if (_bgmPlayer == null)
{
Logger.Log("AudioManager.PlayBGM: _bgmPlayer is not initialized");
return;
}
if (_bgmPlayer.clip != null && _bgmPlayer.clip.name == bgmName)
return;
AudioClip clip = GetOrLoadClip(bgmName, "BGM");
if (clip != null)
{
_bgmPlayer.clip = clip;
_bgmPlayer.volume = volume;
_bgmPlayer.Play();
Logger.Log($"Play BGM : {bgmName}");
}
else
{
Logger.Log($"AudioManager.PlayBGM: Failed to load clip '{bgmName}'");
}
}
// 효과음 재생
public void PlaySFX(string sfxName, float volume = 1.0f)
{
if (string.IsNullOrEmpty(sfxName))
{
Logger.Log("AudioManager.PlaySFX: sfxName is null or empty");
return;
}
if (_sfxPlayer == null)
{
Logger.Log("AudioManager.PlaySFX: _sfxPlayer is not initialized");
return;
}
AudioClip clip = GetOrLoadClip(sfxName, "SFX");
if (clip != null)
{
_sfxPlayer.PlayOneShot(clip, volume);
}
else
{
Logger.Log($"AudioManager.PlaySFX: Failed to load clip '{sfxName}'");
}
}
// 소리 끄기
public void StopBGM()
{
if (_bgmPlayer == null)
{
Logger.Log("AudioManager.StopBGM: _bgmPlayer is not initialized");
return;
}
_bgmPlayer.Stop();
}
// 음소거 기능
public void Mute(bool isMute)
{
if (_bgmPlayer == null || _sfxPlayer == null)
{
Logger.Log("AudioManager.Mute: AudioSource is not initialized");
return;
}
_bgmPlayer.mute = isMute;
_sfxPlayer.mute = isMute;
}
// 리소스 로드
private AudioClip GetOrLoadClip(string name, string type)
{
if (_audioClips.TryGetValue(name, out AudioClip clip))
{
return clip;
}
string path = $"Sounds/{type}/{name}"; // 경로: Resources/Sounds/BGM/이름
clip = Resources.Load<AudioClip>(path);
if (clip == null)
{
Logger.Log($"Audio Clip Missing! Path: {path}");
return null;
}
_audioClips.Add(name, clip);
return clip;
}
// 씬 이동 시 사용하지 않는 소리 메모리 해제 (선택)
public void ClearCache()
{
_audioClips.Clear();
Resources.UnloadUnusedAssets();
}
}
각 음악 재생은 string 음악 이름을 매개변수로 받도록 설정했고, 같은 노래가 이미 나오고 있으면 무시하도록 설정했다. (배경음악의 경우만)
효과음은 여러 음과 겹쳐서 소리가 날 수 있으니 PlayOneShot을 적용해 1개 오디오 소스에서 여러 소리를 동시에 낼 수 있도록 설정했다.
캐싱하기 위해 우선 하드디스크에서 불러오고, 불러온 건 딕셔너리에 저장하고 사용하도록 설정한다.
그리고 씬 이동 시 사용하지 않는 소리의 메모리를 해제함으로써 메모리의 효율성을 생각해 보았다.
그리고 해당 코드를 테스트하는 글을 따로 작성했다.
4. 후기
음악을 직접 리소스에서 로드하는 과정만 갖고 캐싱해놓는 건 처음이었는데, 리소스에서 가져오는 데이터들을 씬 이동 전까지 특수한 경우 아니면 남겨놔 캐싱해서 쓰기 때문이다. 캐싱해서 쓸 때와 캐싱하지 않을 때의 시간이나 메모리 효율도(미세할 것 같지만) 비교해서 적어놔야겠다.