
이번에 공유할 코드는 오디오 기능 전반을 담당하는 AudioManager class이다.
AudioManager 클래스의 필드, 초기화 메서드와 이벤트 함수이다. 환경설정에서 볼륨을 조절하고 이를 반영시키기 위해 Setting Manager에 선언된 BGMSound 와 SFXSound proprety event에 구독하여 세팅된 볼륨으로 바뀌도록 연동시켰다.
SFX(효과음)는 오브젝트 풀 패턴을 적용시켜 따로 AudioSource를 관리하였는데, 다수의 효과음이 동시에 짧은 시간 동안만 출력되기 때문에 풀링 기반이 효과적이라고 생각했다. 이 경우 AudioSource 생성, 파괴보다 메모리 누수 관련 최적화를 기대할 수 있다.
public class AudioManager : Singleton<AudioManager>
{
// 풀링에 사용할 프리팹
[SerializeField] private GameObject _sfxPrefab;
[SerializeField] private GameObject _uiSfxPrefab;
// 실제 재생될 AudioClip 파일들
[SerializeField] private List<AudioClip> _sfxList;
[SerializeField] private List<AudioClip> _uiSfxList;
[SerializeField] private List<AudioClip> _bgmList;
// 이름 기반으로 Clip을 검색하기 위한 Dictionary
private Dictionary<string, AudioClip> _bgmDic;
public Dictionary<string, AudioClip> _sfxDic;
public Dictionary<string, AudioClip> _uiSfxDic;
// 오브젝트 풀 Queue, 크기
private Queue<AudioSource> _sfxPool;
private Queue<AudioSource> _uiSfxPool;
private int _amount;
// BGM 전용 AudioSource → AudioManager에 추가
private AudioSource _bgmSource;
private bool _isPlayed;
private float _curBGMVolume;
private float _curSFXVolume;
// 순차 재생을 위한 BGM List
private Queue<AudioClip> _bgmQueue = new Queue<AudioClip>();
// 초기화 메서드
protected override void Awake()
{
base.Awake();
Init();
}
// SettingManager에 선언된 프로퍼티 이벤트 등록 및 볼륨 초기화
private void Start()
{
_curBGMVolume = SettingManager.Instance.BGMSound.Value;
_curSFXVolume = SettingManager.Instance.SFXSound.Value;
SettingManager.Instance.BGMSound.OnChanged += BGMSoundUpdate;
SettingManager.Instance.SFXSound.OnChanged += SFXSoundUpdate;
}
// BGM은 풀링 기반이 아니기에 바로 AudioSource에 적용
private void BGMSoundUpdate(float value)
{
_curBGMVolume = value;
_bgmSource.volume = _curBGMVolume;
}
// 오브젝트 풀에서 가져올 때 적용하기 위한 캐싱
private void SFXSoundUpdate(float value)
{
_curSFXVolume = value;
}
private void OnDestroy()
{
if (Application.isPlaying)
{
SettingManager.Instance.BGMSound.OnChanged -= BGMSoundUpdate;
SettingManager.Instance.SFXSound.OnChanged -= SFXSoundUpdate;
}
}
private void Init()
{
_amount = 10;
_sfxPool = new Queue<AudioSource>();
_uiSfxPool = new Queue<AudioSource>();
// 오브젝트 풀 초기화
for (int i = 0; i < _amount; i++)
{
AudioSource sfx = Instantiate(_sfxPrefab).GetComponent<AudioSource>();
AudioSource uiSfx = Instantiate(_uiSfxPrefab).GetComponent<AudioSource>();
_sfxPool.Enqueue(sfx);
_uiSfxPool.Enqueue(uiSfx);
}
// AudioClip Dictionary 초기화
_sfxDic = new Dictionary<string, AudioClip>();
_uiSfxDic = new Dictionary<string, AudioClip>();
for (int i = 0; i < _sfxList.Count; i++)
{
_sfxDic.Add(_sfxList[i].name, _sfxList[i]);
}
for (int i = 0; i < _uiSfxList.Count; i++)
{
_uiSfxDic.Add(_uiSfxList[i].name, _uiSfxList[i]);
}
// BGM 재생
EnqueueBGM();
}
}
// = GetPool
private AudioSource GetUISfxSource()
{
if (_uiSfxPool == null) _uiSfxPool = new Queue<AudioSource>();
if (_uiSfxPool.Count > 0) return _uiSfxPool.Dequeue();
GameObject temp = Instantiate(_uiSfxPrefab);
return temp.GetComponent<AudioSource>();
}
// = GetPool
private AudioSource GetSfxSource()
{
if (_sfxPool == null) _sfxPool = new Queue<AudioSource>();
if (_sfxPool.Count > 0) return _sfxPool.Dequeue();
GameObject temp = Instantiate(_sfxPrefab);
return temp.GetComponent<AudioSource>();
}
// 효과음 재생 후 풀에 반납하는 메서드
// 효과음 시간까지 플레이가 되어야하기에 코루틴으로 AudioSource가 지속되도록 구현
private IEnumerator WaitSFX(float delay, AudioSource source, bool isUI = false)
{
yield return new WaitForSeconds(delay);
source.Stop();
source.clip = null;
if (isUI)
{
_uiSfxPool.Enqueue(source);
}
else
{
_sfxPool.Enqueue(source);
}
}
// BGM 전환 과정에서 페이드 인/아웃 기능 추가 메서드
private IEnumerator BGMFadeIn(AudioSource source, float fadeTime)
{
float timer = 0f;
while (fadeTime > timer)
{
source.volume = Mathf.Lerp(0f, _curBGMVolume, timer / fadeTime);
timer += Time.deltaTime;
yield return null;
}
source.volume = _curBGMVolume;
}
private IEnumerator BGMFadeOut(AudioSource source, float fadeTime)
{
float timer = 0f;
while (fadeTime > timer)
{
source.volume = Mathf.Lerp(_curBGMVolume, 0f, timer / fadeTime);
timer += Time.deltaTime;
yield return null;
}
source.Stop();
source.volume = _curBGMVolume;
}
private void EnqueueBGM()
{
foreach (var bgmName in _bgmList)
{
_bgmQueue.Enqueue(bgmName);
}
}
// 다음 BGM 재생
private void PlayNextBGMInQueue()
{
if (_bgmQueue.Count == 0) return;
AudioClip nextClip = _bgmQueue.Dequeue();
PlayBGM(nextClip, false);
// 꺼낸 곡을 다시 큐 뒤에 추가
_bgmQueue.Enqueue(nextClip);
}
외부에서 호출하여 원하는 효과음을 원하는 장소에 플레이할 수 있도록 구현한 기능들이다. AudioClip은 string, AudioClip 둘 다 접근이 가능하도록 오버로드하여 구현했다.
public void PlayBGM(AudioClip clipName, bool loop)
{
//if (_bgmDic.TryGetValue(clipName, out AudioClip audioClip))
{
if (_isPlayed && _bgmSource.clip == clipName) return;
_bgmSource.spatialBlend = 0;
_bgmSource.clip = clipName;
_bgmSource.loop = loop;
_bgmSource.Play();
StartCoroutine(BGMFadeIn(_bgmSource, 3f));
_isPlayed = true;
}
}
public void StopBGM()
{
StartCoroutine(BGMFadeOut(_bgmSource, 1f));
_isPlayed = false;
}
public void PlaySFX(AudioClip audioClip, Vector3 position)
{
if (audioClip != null)
{
AudioSource audioSource = GetSfxSource();
audioSource.transform.position = position;
audioSource.volume = _curSFXVolume;
audioSource.spatialBlend = 1f;
audioSource.PlayOneShot(audioClip);
StartCoroutine(WaitSFX(audioClip.length, audioSource));
}
}
public void PlaySFX(string clipName, Vector3 position)
{
if (_sfxDic.ContainsKey(clipName))
{
AudioSource audioSource = GetSfxSource();
audioSource.transform.position = position;
audioSource.volume = _curSFXVolume;
audioSource.spatialBlend = 1f;
audioSource.PlayOneShot(_sfxDic[clipName]);
StartCoroutine(WaitSFX(_sfxDic[clipName].length, audioSource));
}
}
public void PlayUISFX(AudioClip audioClip)
{
if (audioClip != null)
{
AudioSource audioSource = GetUISfxSource();
audioSource.volume = _curSFXVolume;
audioSource.spatialBlend = 0f;
audioSource.PlayOneShot(audioClip);
StartCoroutine(WaitSFX(audioClip.length, audioSource, true));
}
}
// BGM 자동 재생
public void PlayBgms(bool bgmPlay)
{
if (_bgmSource == null) return;
if (_bgmSource.isPlaying == false && _bgmQueue.Count > 0)
{
PlayNextBGMInQueue();
}
}