사운드매니저가 없으면 배경음악과 효과음을 전체적으로 끄거나 키는 것이 매우 어려워 질 수 있습니다. 또한 게임에서는 볼륨을 조절하거나 여러 사운드를 동시에 재생하는 경우가 빈번한데 이 상황에서 사운드를 관리해 주지 않으면 사운드 밸런스가 맞지 않아 게임의 퀄리티를 낮추기도 합니다. 그리고 페이드 인 아웃 효과를 적용하여 자연스럽게 사운드가 재생 되고 정지 되어야 하는데 이러한 기능을 넣기에도 어려움이 있습니다. 이번 시간에는 어떤 게임이든 바로 사용할 수 있는 사운드매니저 코드를 구현하고 코드를 설명하는 시간을 갖도록 하겠습니다.
목차
사운드매니저란?
오디오 리소스를 효과적으로 관리할 수 있도록 하는 클래스 입니다. 배경음과 효과음으로 나누어 재생 및 볼륨 조절, 일시 중지 및 정지 등을 쉽게 해줍니다.
SoundManager 구현
먼저, 코드를 보며 기능을 소개한 뒤 전체 소스코드를 제공해 드리겠습니다.
LoopingAudioSource
public class LoopingAudioSource
{
private AudioSource audioSource;
private float fadeInDuration;
private float fadeOutDuration;
private bool persist;
public LoopingAudioSource(AudioSource source, float fadeInDuration, float fadeOutDuration, bool persist)
{
this.audioSource = source;
this.fadeInDuration = fadeInDuration;
this.fadeOutDuration = fadeOutDuration;
this.persist = persist;
}
public void Play(float volumeScale, bool fadeIn)
{
if (fadeIn)
{
audioSource.volume = 0.0f;
CoroutineHelper.StartCoroutine(FadeInCoroutine(volumeScale));
}
else
{
audioSource.volume = volumeScale;
}
}
private IEnumerator FadeInCoroutine(float targetVolume)
{
float timer = 0.0f;
while (timer < fadeInDuration)
{
timer += Time.deltaTime;
audioSource.volume = Mathf.Lerp(0.0f, targetVolume, timer / fadeInDuration);
yield return null;
}
audioSource.volume = targetVolume;
}
public void Stop()
{
if (persist)
{
// MonoBehaviour 클래스가 아니기에 코루틴 헬퍼를 만들어서 사용
CoroutineHelper.StartCoroutine(FadeOutCoroutine());
}
else
{
GameObject.Destroy(audioSource.gameObject);
}
}
private IEnumerator FadeOutCoroutine()
{
float timer = 0.0f;
float startVolume = audioSource.volume;
while (timer < fadeOutDuration)
{
timer += Time.deltaTime;
audioSource.volume = Mathf.Lerp(startVolume, 0.0f, timer / fadeOutDuration);
yield return null;
}
GameObject.Destroy(audioSource.gameObject);
}
public void Pause()
{
audioSource.Pause();
}
public void Resume()
{
audioSource.UnPause();
}
public void SetVolume(float volume)
{
audioSource.volume = volume;
}
}
오디오를 반복 재생하기 위한 클래스 입니다. 해당 클래스를 사용하는 것은 배경음악입니다. (저는 Music이라고 정의 했습니다.) 배경음을 자연스럽게 재생하기 위해서 코루틴을 이용하여 페이드 인 아웃 효과를 주었습니다. 이 효과의 핵심은 Lerp를 이용하여 사운드의 크기를 자연스럽게 키우고 줄이는 것에 있습니다.
CoroutineHelper
public static class CoroutineHelper
{
private static MonoBehaviour coroutineContainer;
private static MonoBehaviour CoroutineContainer
{
get
{
if (coroutineContainer == null)
{
var go = new GameObject("CoroutineHelper");
GameObject.DontDestroyOnLoad(go);
coroutineContainer = go.AddComponent<CoroutineContainer>();
}
return coroutineContainer;
}
}
public static Coroutine StartCoroutine(IEnumerator routine)
{
return CoroutineContainer.StartCoroutine(routine);
}
private class CoroutineContainer : MonoBehaviour { }
}
MonoBehaviour를 상속 받지 않은 클래스도 코루틴을 사용할 수 있도록 도와주는 클래스 입니다.
SoundManager 초기화와 업데이트
public static class SoundManager
{
private static bool initialized = false;
public static void Initialize()
{
if (initialized)
return;
initialized = true;
}
public static void Update()
{
// 필요한 업데이트 로직
}
}
만약 초기화나 업데이트가 필요하다면 해당 부분에서 할 수 있도록 구성 하였습니다.
SoundManager의 배경음 관리
public static class SoundManager
{
private static float masterVolume = 1.0f;
private static List<LoopingAudioSource> musicList = new List<LoopingAudioSource>();
public static void PlayLoopingMusic(AudioClip musicClip, float volumeScale, float fadeSeconds, bool persist)
{
// 배경음 설정 초기화
AudioSource source = CreateAudioSource();
source.clip = musicClip;
source.loop = true;
source.volume = volumeScale * masterVolume;
// 재생
source.Play();
LoopingAudioSource loopingSource = new LoopingAudioSource(source, fadeSeconds, fadeSeconds, persist);
loopingSource.Play(volumeScale, true);
musicList.Add(loopingSource);
if (persist)
{
// 배경음이 지속되는 동안 필요한 추가 기능을 넣어 주시면 됩니다.
}
}
public static void StopLoopingMusic()
{
foreach (LoopingAudioSource source in musicList)
{
source.Stop();
}
}
}
LoopingAudioSource을 리스트로 만들어 재생되는 배경음들을 관리하는 부분 입니다. PlayLoopingMusic 부분에서는 오디오 클립을 초기화하고 재생하는 메서드 입니다. StopLoopingMusic은 재생 시 추가 했던 리스트에 있던 배경음들을 동시에 멈추는 기능을 합니다.
효과음 재생
public static class SoundManager
{
private static float masterVolume = 1.0f;
private static List<AudioSource> soundEffectList = new List<AudioSource>();
public static void PlayOneShotSound(AudioClip soundClip, float volumeScale)
{
AudioSource source = CreateAudioSource();
source.PlayOneShot(soundClip, volumeScale * masterVolume);
soundEffectList.Add(source);
}
private static AudioSource CreateAudioSource()
{
GameObject audioSourceObject = new GameObject("AudioSource");
audioSourceObject.transform.parent = AudioManager.Instance.transform;
AudioSource audioSource = audioSourceObject.AddComponent<AudioSource>();
return audioSource;
}
}
PlayOneShotSound메서드는 CreateAudioSource을 사용하여 오디오 소스를 생성하고 재생합니다. PlayOneShot 메서드를 이용하여 한번만 재생 되도록 합니다. 또한 volumeScale 파라미터를 이용하여 사운드 볼륨을 제어 할 수 있도록 하여 확장성을 높여줍니다. 예를 들어 효과음이 여러 개 실행 될 경우 AudioManager에서 soundEffectList의 개수를 파악하여 자동으로 볼륨을 줄이는 기능을 추가 등 유용한 기능을 추가 할 수 있도록 도와 줍니다.
사운드 전체 일시 정지, 다시 재생, 볼륨 조절
public static class SoundManager
{
private static float masterVolume = 1.0f;
private static List<LoopingAudioSource> musicList = new List<LoopingAudioSource>();
private static List<AudioSource> soundEffectList = new List<AudioSource>();
public static void PauseAll()
{
foreach (LoopingAudioSource source in musicList)
{
source.Pause();
}
foreach (AudioSource source in soundEffectList)
{
source.Pause();
}
}
public static void ResumeAll()
{
foreach (LoopingAudioSource source in musicList)
{
source.Resume();
}
foreach (AudioSource source in soundEffectList)
{
source.UnPause();
}
}
public static void SetMasterVolume(float volume)
{
// Set master volume for all sounds
masterVolume = volume;
foreach (LoopingAudioSource source in musicList)
{
source.SetVolume(volume);
}
foreach (AudioSource source in soundEffectList)
{
source.volume = volume;
}
}
}
musicList와 soundEffectList을 이용하여 손쉽게 해당 기능들을 구현 하였습니다.
AudioManager
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
private void Awake()
{
if (Instance != null)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
private void Start()
{
// 사운드매니저 초기화
SoundManager.Initialize();
}
private void Update()
{
SoundManager.Update();
}
public static void PlayBackgroundMusic(AudioClip musicClip)
{
// 배경음 재생
SoundManager.PlayLoopingMusic(musicClip, 1.0f, 2.0f, true);
}
public static void PlaySoundEffectOneShot(AudioClip soundEffectClip, float volumeScale)
{
// 효과음 재생
SoundManager.PlayOneShotSound(soundEffectClip, volumeScale);
}
public static void StopBackgroundMusic()
{
// 배경음 정지
SoundManager.StopLoopingMusic();
}
public static void PauseAllSounds()
{
// 사운드 모두 정지
SoundManager.PauseAll();
}
public static void ResumeAllSounds()
{
// 사운드 모두 재생
SoundManager.ResumeAll();
}
public static void MuteAllSounds()
{
SoundManager.SetMasterVolume(0.0f);
}
public static void UnmuteAllSounds()
{
SoundManager.SetMasterVolume(1.0f);
}
}
SoundManager를 한번 감싼 클래스 입니다. 감싸는 이유는 AudioManager와 SoundManager를 분리하여 다른 게임에 SoundManager를 이식할 때 시간을 최소화 하기 위함 입니다. 즉, SoundManager는 모든 게임이 사용 할 수 있는 공통 기능을 넣는 곳이고 AudioManager는 게임마다 필요한 기능들을 추가하는 곳 입니다. 이렇게 분리하면 코드의 가독성이 좋아질 뿐 아니라 이식성도 올라갑니다.
코드 설명이 끝났으니 SoundManager의 전체 소스 코드를 작성해 보겠습니다.(오디오매니저는 그대로 사용하시면 됩니다.)
public static class SoundManager
{
private static bool initialized = false;
private static float masterVolume = 1.0f;
private static List<LoopingAudioSource> musicList = new List<LoopingAudioSource>();
private static List<AudioSource> soundEffectList = new List<AudioSource>();
public static void Initialize()
{
if (initialized)
return;
initialized = true;
// Additional initialization code if needed
}
public static void Update()
{
// Update sound manager logic here
}
public static void PlayLoopingMusic(AudioClip musicClip, float volumeScale, float fadeSeconds, bool persist)
{
AudioSource source = CreateAudioSource();
source.clip = musicClip;
source.loop = true;
source.volume = volumeScale * masterVolume;
source.Play();
LoopingAudioSource loopingSource = new LoopingAudioSource(source, fadeSeconds, fadeSeconds, persist);
loopingSource.Play(volumeScale, true);
musicList.Add(loopingSource);
if (persist)
{
// Additional logic for persisting the looping music
}
}
public static void StopLoopingMusic()
{
foreach (LoopingAudioSource source in musicList)
{
source.Stop();
}
}
public static void PlayOneShotSound(AudioClip soundClip, float volumeScale)
{
AudioSource source = CreateAudioSource();
source.PlayOneShot(soundClip, volumeScale * masterVolume);
soundEffectList.Add(source);
}
public static void PauseAll()
{
foreach (LoopingAudioSource source in musicList)
{
source.Pause();
}
foreach (AudioSource source in soundEffectList)
{
source.Pause();
}
}
public static void ResumeAll()
{
foreach (LoopingAudioSource source in musicList)
{
source.Resume();
}
foreach (AudioSource source in soundEffectList)
{
source.UnPause();
}
}
public static void SetMasterVolume(float volume)
{
// Set master volume for all sounds
masterVolume = volume;
foreach (LoopingAudioSource source in musicList)
{
source.SetVolume(volume);
}
foreach (AudioSource source in soundEffectList)
{
source.volume = volume;
}
}
private static AudioSource CreateAudioSource()
{
GameObject audioSourceObject = new GameObject("AudioSource");
audioSourceObject.transform.parent = AudioManager.Instance.transform;
AudioSource audioSource = audioSourceObject.AddComponent<AudioSource>();
return audioSource;
}
}
수정한 사항은 없으며 위에서 설명한 코드를 합친 것일 뿐입니다.
마무리
게임마다 환경이 다르기 때문에 모든 사운드 기능을 고려하여 사운드매니저를 만드는 것은 무리가 있습니다. 그러므로 제가 한 것처럼 기본적인 기능을 제공하는 SoundManager클래스를 만들어 두신다면 프로젝트의 생산성이 크게 향상 될 것 입니다. 구현한 코드가 참고가 되셨 길 바랍니다.