Unity/정보

[Unity] 사운드 매니저 구현 (Sound Manager)

달시_Dalsi 2025. 1. 13. 20:20
728x90

게임에서 사운드는 플레이어의 몰입감을 높이는 중요한 요소입니다. 효과음, 배경음악 등을 효율적으로 관리하려면 사운드 매니저가 필요합니다. 이 글에선 String과 Enum을 이용하여 원하는 소리클립을 찾아 출력시키는 사운드 매니저를 알아보겠습니다.

 

1. 사운드 매니저 소개

사운드 매니저는 게임 내 사운드를 관리하기 위해 사용하는 스크립트입니다.

  • 배경음악(BGM) 및 효과음(SFX) 관리
  • 성능 최적화를 위한 오디오 소스 재사용
  • 개발 편의성을 위한 인터페이스 제공

2. string을 사용하는 사운드 매니저

사운드를 이름으로 찾고 재생하는 방식은 직관적이며 간단합니다. 그러나 오타 가능성이 있고, 사운드 이름이 많아질수록 관리가 어려울 수 있습니다.

아래 코드에선 오디오클립 파일이름이 저장, 검색에 쓰입니다.

 

string 사용 사운드 매니저

using UnityEngine;
using System.Collections.Generic;

public class StringSoundManager : MonoBehaviour
{
    public static StringSoundManager instance;

    private Dictionary<string, AudioClip> soundDict;  // SFX와 BGM을 저장할 Dictionary
    private AudioSource sfxPlayer;                   // SFX 재생용 AudioSource
    private AudioSource bgmPlayer;                   // BGM 재생용 AudioSource

    [Header("Audio Clips")]
    [SerializeField] private AudioClip[] audioClips; // 오디오 클립 배열

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            Init();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Init()
    {
        soundDict = new Dictionary<string, AudioClip>();
        bgmPlayer.loop = true; // BGM은 기본적으로 반복 재생

        // Dictionary 초기화
        foreach (var clip in audioClips)
        {
            soundDict[clip.name] = clip;
        }
    }

    // SFX 재생
    public void PlaySFX(string soundName)
    {
        if (soundDict.TryGetValue(soundName, out var clip))
        {
            sfxPlayer.PlayOneShot(clip);
        }
        else
        {
            Debug.LogWarning("SFX not found.");
        }
    }

    // BGM 재생
    public void PlayBGM(string bgmName)
    {
        if (soundDict.TryGetValue(bgmName, out var clip))
        {
            if (bgmPlayer.clip != clip)
            {
                bgmPlayer.clip = clip;
                bgmPlayer.Play();
            }
        }
        else
        {
            Debug.LogWarning("BGM not found.");
        }
    }
}

3. enum을 사용하는 사운드 매니저

enum을 사용하면 사운드 이름을 코드에서 관리할 수 있어 오타를 방지하고, 자동완성을 제공받을 수 있습니다. 다만 사운드 추가 시 enum을 반드시 수정해야 합니다. 또한 Enum의 변수 선언과 클립 배열의 순서가 동일해야합니다.

 

enum 기반 사운드 매니저

using UnityEngine;
using System.Collections.Generic;

// 배경음악(BGM)
public enum EBgm
{
    TITLE,
    GAME,
    RESULT,
}

// 효과음(SFX)
public enum ESfx
{
    BUTTON_CLICK,
    ITEM_PICKUP,
    DOOR_OPEN,
    MISSION_CLEAR,
}

public class SoundManager : MonoBehaviour
{
    public static SoundManager instance;

    [Header("Audio Clips")]
    [SerializeField] private AudioClip[] bgmClips;  // BGM 클립 배열
    [SerializeField] private AudioClip[] sfxClips; // SFX 클립 배열

    [Header("Audio Sources")]
    [SerializeField] private AudioSource bgmSource; // BGM 재생 AudioSource
    [SerializeField] private AudioSource sfxSource; // SFX 재생 AudioSource

    private Dictionary<EBgm, AudioClip> bgmDict; // BGM Dictionary
    private Dictionary<ESfx, AudioClip> sfxDict; // SFX Dictionary

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
            return;
        }

        InitDictionaries();
    }

    // Dictionary 초기화
    private void InitDictionaries()
    {
        bgmDict = new Dictionary<EBgm, AudioClip>();
        for (int i = 0; i < bgmClips.Length; i++)
        {
            bgmDict[(EBgm)i] = bgmClips[i];
        }

        sfxDict = new Dictionary<ESfx, AudioClip>();
        for (int i = 0; i < sfxClips.Length; i++)
        {
            sfxDict[(ESfx)i] = sfxClips[i];
        }
    }

    // BGM 재생
    public void PlayBGM(EBgm bgmType)
    {
        if (bgmDict.TryGetValue(bgmType, out var clip))
        {
            bgmSource.clip = clip;
            bgmSource.loop = true; // 배경음악은 기본적으로 반복 재생
            bgmSource.Play();
        }
        else
        {
            Debug.LogWarning("BGM not found in Dictionary!");
        }
    }

    // SFX 재생
    public void PlaySFX(ESfx sfxType)
    {
        if (sfxDict.TryGetValue(sfxType, out var clip))
        {
            sfxSource.PlayOneShot(clip);
        }
        else
        {
            Debug.LogWarning("SFX not found in Dictionary!");
        }
    }
}

 

string vs enum 비교

오타 위험 높음 낮음
자동완성 지원 지원하지 않음 지원
확장성 사운드 추가가 간편 enum 수정 필요

4. 효과음 처리 방식: PlayOneShot vs 오브젝트 풀

PlayOneShot 방식

  • 장점: 단일 AudioSource로 여러 사운드를 간편하게 재생 가능
  • 단점: 매우 많은 사운드를 동시에 재생하면 성능 문제가 생길 수 있음

오브젝트 풀 방식 (다수의 AudioSource사용)

  • 장점: 다수의 AudioSource를 관리하여 성능 최적화
  • 단점: 구현 복잡도 증가

 

오브젝트 풀 사용한 효과음 처리 (string)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Unity.VisualScripting.Member;

public class StringSoundManager_Pooling : MonoBehaviour
{
    public static StringSoundManager_Pooling instance;

    [Header("Audio Clips")]
    [SerializeField] private AudioClip[] audioClips; // 오디오 클립 배열

    [Header("Object Pool Settings")]
    [SerializeField] private int poolSize = 10;           // 풀 크기

    private Dictionary<string, AudioClip> soundDict;      // SFX와 BGM을 저장할 Dictionary
    private Queue<AudioSource> audioSourcePool;           // 오브젝트 풀

    private AudioSource bgmPlayer;                        // BGM 재생용 AudioSource

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            Init();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Init()
    {
        // Dictionary 초기화
        soundDict = new Dictionary<string, AudioClip>();
        foreach (var clip in audioClips)
        {
            soundDict[clip.name] = clip;
        }

        // BGM 플레이어 초기화
        bgmPlayer = gameObject.AddComponent<AudioSource>();
        bgmPlayer.loop = true;

        // 오브젝트 풀 초기화
        InitPool();
    }

    private void InitPool()
    {
        audioSourcePool = new Queue<AudioSource>();
        for (int i = 0; i < poolSize; i++)
        {
            AudioSource source = gameObject.AddComponent<AudioSource>();
            source.playOnAwake = false;
            source.enabled = false;
            audioSourcePool.Enqueue(source);
        }
    }

    // SFX 재생 (Object Pooling 사용)
    public void PlaySFX(string soundName)
    {
        if (soundDict.TryGetValue(soundName, out var clip))
        {
            if (audioSourcePool.Count > 0)
            {
                AudioSource source = audioSourcePool.Dequeue();
                source.clip = clip;
                source.enabled = true;
                source.Play();

                StartCoroutine(ReturnToPool(source, clip.length));
            }
            else
            { 
                // 풀에 오디오 소스가 없을 경우, 새로 생성하여 사용
                AudioSource newSource = gameObject.AddComponent<AudioSource>();
                newSource.clip = clip;
                newSource.playOnAwake = false;
                newSource.enabled = true;
                newSource.Play();

                // 새로 생성한 소스는 재사용 후 풀에 다시 넣을 수 있도록 코루틴을 사용
                StartCoroutine(ReturnToPool(newSource, clip.length));
            }
        }
        else
        {
            Debug.LogWarning("SFX not found");
        }
    }

    // BGM 재생
    public void PlayBGM(string bgmName)
    {
        if (soundDict.TryGetValue(bgmName, out var clip))
        {
            if (bgmPlayer.clip != clip)
            {
                bgmPlayer.clip = clip;
                bgmPlayer.Play();
            }
        }
        else
        {
            Debug.LogWarning("BGM not found");
        }
    }

    private IEnumerator ReturnToPool(AudioSource source, float delay)
    {
        yield return new WaitForSeconds(delay);
        source.enabled = false;
        audioSourcePool.Enqueue(source);
    }
}

 

 

오브젝트 풀 사용한 효과음 처리 (enum)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 배경음악(BGM) 타입
public enum EBgm
{
    TITLE,
    GAME,
    RESULT,
}

// 효과음(SFX) 타입
public enum ESfx
{
    BUTTON_CLICK,
    ITEM_PICKUP,
    DOOR_OPEN,
    MISSION_CLEAR,
}

public class EnumSoundManager_Pooling : MonoBehaviour
{
    public static EnumSoundManager_Pooling instance;

    [Header("Audio Clips")]
    [SerializeField] private AudioClip[] bgmClips; // BGM 오디오 클립 배열
    [SerializeField] private AudioClip[] sfxClips; // SFX 오디오 클립 배열

    [Header("Object Pool Settings")]
    [SerializeField] private int poolSize = 10; // 풀 크기

    private Dictionary<EBgm, AudioClip> bgmDict; // BGM을 저장할 Dictionary
    private Dictionary<ESfx, AudioClip> sfxDict; // SFX를 저장할 Dictionary
    private Queue<AudioSource> audioSourcePool; // 오브젝트 풀

    private AudioSource bgmPlayer; // BGM 재생용 AudioSource

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
            Init();
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Init()
    {
        // BGM Dictionary 초기화
        bgmDict = new Dictionary<EBgm, AudioClip>();
        for (int i = 0; i < bgmClips.Length; i++)
        {
            bgmDict[(EBgm)i] = bgmClips[i]; // enum과 인덱스를 매핑
        }

        // SFX Dictionary 초기화
        sfxDict = new Dictionary<ESfx, AudioClip>();
        for (int i = 0; i < sfxClips.Length; i++)
        {
            sfxDict[(ESfx)i] = sfxClips[i]; // enum과 인덱스를 매핑
        }

        // BGM 플레이어 초기화
        bgmPlayer = gameObject.AddComponent<AudioSource>();
        bgmPlayer.loop = true;

        // 오브젝트 풀 초기화
        InitPool();
    }

    private void InitPool()
    {
        audioSourcePool = new Queue<AudioSource>();
        for (int i = 0; i < poolSize; i++)
        {
            AudioSource source = gameObject.AddComponent<AudioSource>();
            source.playOnAwake = false;
            source.enabled = false;
            audioSourcePool.Enqueue(source);
        }
    }

    // SFX 재생 (Object Pooling 사용)
    public void PlaySFX(ESfx sfxType)
    {
        if (sfxDict.TryGetValue(sfxType, out var clip))
        {
            if (audioSourcePool.Count > 0)
            {
                AudioSource source = audioSourcePool.Dequeue();
                source.clip = clip;
                source.enabled = true;
                source.Play();

                StartCoroutine(ReturnToPool(source, clip.length));
            }
            else
            {
                // 풀에 오디오 소스가 없을 경우, 새로 생성하여 사용
                AudioSource newSource = gameObject.AddComponent<AudioSource>();
                newSource.clip = clip;
                newSource.playOnAwake = false;
                newSource.enabled = true;
                newSource.Play();

                // 새로 생성한 소스는 재사용 후 풀에 다시 넣을 수 있도록 코루틴을 사용
                StartCoroutine(ReturnToPool(newSource, clip.length));
            }
        }
        else
        {
            Debug.LogWarning("SFX not found");
        }
    }

    // BGM 재생
    public void PlayBGM(EBgm bgmType)
    {
        if (bgmDict.TryGetValue(bgmType, out var clip))
        {
            if (bgmPlayer.clip != clip)
            {
                bgmPlayer.clip = clip;
                bgmPlayer.Play();
            }
        }
        else
        {
            Debug.LogWarning("BGM not found");
        }
    }

    private IEnumerator ReturnToPool(AudioSource source, float delay)
    {
        yield return new WaitForSeconds(delay);
        source.enabled = false;
        audioSourcePool.Enqueue(source);
    }
}

 

동작 원리는 PlaySFX로 호출하면 풀에서 오디오 소스를 하나 가져와서 클립을 할당 및 재생 시킵니다. 이후 코루틴을 이용하여 클립의 재생 시간에 따라 자동으로 풀에 다시 추가합니다.

 

사용법

AudioClip explosionSound = Resources.Load<AudioClip>("Explosion");
SFXPool.instance.PlaySFX(explosionSound);

5. 동적으로 AudioSource 생성 기능 추가

지금까지의 코드는 한곳에서만 소리가 재생되는 구조입니다. 하지만 게임에서는 특정 객체에서 소리가 나야 할 때가 많습니다. 아래 코드는 동적으로 AudioSource를 생성하여 객체별로 소리를 재생하는 방법입니다.

 

동적 AudioSource 추가 또는 사용 함수

public void PlaySoundOnObject(string soundName, GameObject obj)
{
    if (soundDict.TryGetValue(soundName, out var clip))
    {
        AudioSource source = obj.GetComponent<AudioSource>();
        if (source == null) source = obj.AddComponent<AudioSource>();

        source.clip = clip;
        source.Play();
    }
}

 

사용법

사운드 출력이 필요한 객체(ex.몬스터 스크립트)에서 호출하여 사용합니다.

SoundManager.instance.PlaySoundOnObject("MonsterRoar", gameObject);
 
728x90