게임에서 사운드는 플레이어의 몰입감을 높이는 중요한 요소입니다. 효과음, 배경음악 등을 효율적으로 관리하려면 사운드 매니저가 필요합니다. 이 글에선 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);
'Unity > 정보' 카테고리의 다른 글
[Unity] Image와 RawImage 비교 (0) | 2025.01.16 |
---|---|
[Unity] 2D 무한 배경 구현하기 (0) | 2025.01.12 |
[Unity] unity6의 웹 배포 빌드 방식 (1) | 2025.01.05 |
[Unity] Unity6 픽셀 아트 2D 라이팅 튜토리얼 (2) (0) | 2025.01.05 |
[Unity] Unity6 픽셀 아트 2D 라이팅 튜토리얼 (1) (1) | 2025.01.05 |