게임 로고 이미지

사운드매니저가 없으면 배경음악과 효과음을 전체적으로 끄거나 키는 것이 매우 어려워 질 수 있습니다. 또한 게임에서는 볼륨을 조절하거나 여러 사운드를 동시에 재생하는 경우가 빈번한데 이 상황에서 사운드를 관리해 주지 않으면 사운드 밸런스가 맞지 않아 게임의 퀄리티를 낮추기도 합니다. 그리고 페이드 인 아웃 효과를 적용하여 자연스럽게 사운드가 재생 되고 정지 되어야 하는데 이러한 기능을 넣기에도 어려움이 있습니다. 이번 시간에는 어떤 게임이든 바로 사용할 수 있는 사운드매니저 코드를 구현하고 코드를 설명하는 시간을 갖도록 하겠습니다.


사운드매니저란?

오디오 리소스를 효과적으로 관리할 수 있도록 하는 클래스 입니다. 배경음과 효과음으로 나누어 재생 및 볼륨 조절, 일시 중지 및 정지 등을 쉽게 해줍니다.

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클래스를 만들어 두신다면 프로젝트의 생산성이 크게 향상 될 것 입니다. 구현한 코드가 참고가 되셨 길 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다