Chapter 9-1. Sound Manager
카테고리: Unity Lesson 2
태그: Unity Game Engine
인프런에 있는 Rookiss님의 [C#과 유니티로 만드는 MMORPG 게임 개발 시리즈] Part3: 유니티 엔진 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click
Chapter 9. Sound Manager
🚀 Sound Manager
- MP3 Player, 소리 발생 근원지 👉
Audio Source
컴포넌트 - MP3 음원, 어떤 소리를 재생할지, 음반 👉
Audio Clip
- 관객, 듣는 사람, 귀 👉
Audio Listner
컴포넌트- Main Camera에 기본적으로 달려있는 컴포넌트
- 한 Scene 안에 하나만 있으면 된다. 기본적으로 Main Camera 에 붙어있다.
소리를 발생시킬 오브젝트들에게 Audio Source
컴포넌트를 붙여준다. 이 컴포넌트의 Audio Clip
에 원하는 음원을 할당하면 된다.
- Volume : 소리 크기
- Pitch : 재생 속도 (느리고 빠르게)
🚀 사운드 매니저
✈ 사운드 매니저를 사용하는 이유
AudioClip audioClip1;
AudioClip audioClip2;
AudioSource audio = GetComponent<AudioSource>();
audio.PlayOneShot(audioClip1);
audio.PlayOneShot(audioClip2);
float lifeTime = Mathf.Max(audioClip1.length, audioClip2.length);
GameObject.Destroy(gameObject, lifeTime);
게임오브젝트가 비활성화 혹은 파괴되면 오브젝트에 붙어있는 Audio Source 가 재생하던 소리들까지 전부 중단된다. 재생기가 사라졌기 때문이다! 위와같이 클립이 끝까지 재생될 때까지 재생시간 동안 대기한 후에 파괴하게할 수도 있지만 이는 좋은 방법이 아니다. 그래서 경우에 따라 오브젝트에 종속되지 않게끔 오디오 클립을 재생해야 한다. 그래서 사운드 매니저가 필요하다!
📜 Define
사운드 종류 정의
public enum Sound
{
Bgm,
Effect,
MaxCount, // 아무것도 아님. 그냥 Sound enum의 개수 세기 위해 추가. (0, 1, '2' 이렇게 2개)
}
Bgm
: 반복재생 해야 됨. 배경음악Effect
: 짧게 1번만 재생됨.
📜 SoundManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SoundManager
{
AudioSource[] _audioSources = new AudioSource[(int)Define.Sound.MaxCount];
Dictionary<string, AudioClip> _audioClips = new Dictionary<string, AudioClip>();
- 재생기 Audio Source 가 여러개일 수 있으므로 이를
_audioSources
배열로 관리한다.- 사운드 종류는 📜Define의
Sound
에 의해 2 개(BGM, Effect)이며 각각의 Audio Source 재생기를 가진다. 즉, Audio Source 2개.- 재생기당 하나의 클립만 재생할 수 있으므로 이는 곧 2 개의 음을 중첩하여 재생할 수 있다는 의미가 된다.
- 배경음악 재생기
_audioSources[(int)Define.Sound.Bgm]
- 효과음 재생기
_audioSources[(int)Define.Sound.Effect]
- 배경음악 재생기
- 재생기당 하나의 클립만 재생할 수 있으므로 이는 곧 2 개의 음을 중첩하여 재생할 수 있다는 의미가 된다.
- 원소에 재생기 할당은 밑에 Init() 에서
- 사운드 종류는 📜Define의
- 효과음 사운드는 자주 재생된다. 따라서 계속된 로드는 성능을 떨어뜨릴 수 있도록 미리 로드한다.
- 따라서
_audioClips
Dictionary에 효과음 클립들을 미리 로드하여 보관하고 여기서 갖다 쓸 것이다.- Key : 클립의
path
- Value : 해당 효과음 클립
- Key : 클립의
- 👉 Object Pooling
- 따라서
다만 위와 같이 게임 내내 유지되는 빈 오브젝트에 Audio Source 를 붙이면, 거리에 따라 소리 크기가 달라지는 3D 사운드 효과는 낼 수가 없다. 배경음악처럼 100% 크게 다 들려야 하는 소리라던가 그런 사운드만 사운드 매니저에서 관리하는 것이 좋다. 3 D 사운드 관련해선 다음 포스팅 참고.
public void Init()
{
GameObject root = GameObject.Find("@Sound");
if (root == null)
{
root = new GameObject { name = "@Sound" };
Object.DontDestroyOnLoad(root);
string[] soundNames = System.Enum.GetNames(typeof(Define.Sound)); // "Bgm", "Effect"
for (int i = 0; i < soundNames.Length - 1; i++)
{
GameObject go = new GameObject { name = soundNames[i] };
_audioSources[i] = go.AddComponent<AudioSource>();
go.transform.parent = root.transform;
}
_audioSources[(int)Define.Sound.Bgm].loop = true; // bgm 재생기는 무한 반복 재생
}
}
- Init
- Audio Source 를 붙일 오브젝트들을 만들고 컴포넌트를 붙여준다.
@Sound
라는 이름의 오브젝트가 없다면 만들고 이를 부모로 하는 자식 오브젝트Bgm
,Effect
라는 이름의 오브젝트들을 만들고 각각 이에 Audio Source를 붙일 것이다.- 이 Audio Source들은 게임 내내 파괴되지 않고 살아있도록 DontDestroyOnLoad 함수로 인해 보호. 파괴 방지!
- BGM 재생기의 경우 무한 반복 재생하도록
- Audio Source 를 붙일 오브젝트들을 만들고 컴포넌트를 붙여준다.
public void Clear()
{
// 재생기 전부 재생 스탑, 음반 빼기
foreach (AudioSource audioSource in _audioSources)
{
audioSource.clip = null;
audioSource.Stop();
}
// 효과음 Dictionary 비우기
_audioClips.Clear();
}
Audio Source 가 붙어있는 두 오브젝트 Bgm
, Effect
는 DontDestroyOnLoad를 통해 파괴 안되도록 관리되므로 게임이 아주 오랫동안 지속이 되고 새로운 효과음들을 많이 재생시킨다면 _audioClip
Dictionary 가 계~~속 추가되어 그럴린 없겠지만 메모리가 부족해질 수 있는 문제도 생각해야 한다. 그래서 Clear 함수를 만들어줌. _audioClip
Dictionary 를 비워주는 함수.
- 📜Manager의 Clear 에서 호출된다. (아래 참고)
- 씬이 바뀔 때 호출될 것.
public void LoadScene(Define.Scene type) { Managers.Clear(); SceneManager.LoadScene(GetSceneName(type)); } public void Clear() { CurrentScene.Clear(); }
- 씬이 바뀔 때 호출될 것.
public void Play(AudioClip audioClip, Define.Sound type = Define.Sound.Effect, float pitch = 1.0f)
{
if (audioClip == null)
return;
if (type == Define.Sound.Bgm) // BGM 배경음악 재생
{
AudioSource audioSource = _audioSources[(int)Define.Sound.Bgm];
if (audioSource.isPlaying)
audioSource.Stop();
audioSource.pitch = pitch;
audioSource.clip = audioClip;
audioSource.Play();
}
else // Effect 효과음 재생
{
AudioSource audioSource = _audioSources[(int)Define.Sound.Effect];
audioSource.pitch = pitch;
audioSource.PlayOneShot(audioClip);
}
}
public void Play(string path, Define.Sound type = Define.Sound.Effect, float pitch = 1.0f)
{
AudioClip audioClip = GetOrAddAudioClip(path, type);
Play(audioClip, type, pitch);
}
- 음원의
AudioClip
를 받아 재생하는 Play- BGM 배경음악 재생시
- BGM 재생기인
_audioSources[(int)Define.Sound.Bgm]
을 통해 재생 - BGM 배경음악은 중첩되지 않게 딱 하나만 재생되어야 한다.
- 그러므로 BGM 재생기 AudioSource가
isPlaying
이미 재생중인게 있다면 정지해준다. - Play() 함수를 통해 재생한다.
- 그러므로 BGM 재생기 AudioSource가
- BGM 재생기인
- Effect 효과음 재생시
- 효과음 재생기인
_audioSources[(int)Define.Sound.Effect]
을 통해 재생 - 효과음은 중첩되도 된다.
- 원하는 클립을 중첩해서 재생 가능하게끔 PlayOneShot 함수를 통해 재생한다.
- 던져지는 개념이라 Play() 와 달리 중간에 멈출 수가 없다. 한번 던져지면 재생기 Audio Source가 꺼지는게 아닌 이상 끝까지 재생된다.
- 효과음 재생기인
- BGM 배경음악 재생시
- 음원의
path
를 받아 재생하는 Play- 해당
path
에서 GetOrAddAudioClip 함수를 통해(밑에 후술) 해당 클립을 📂Sound로부터 로드옴 - 그리고 이를 바탕으로 첫번째 Play 호출
- 해당
- 나중에 다른 곳에서 아래와 같은 방식으로 재생하게 될 것이다.
Managers.Sound.Play("UnityChan/univ0001", Define.Sound.Effect); Managers.Sound.Play("UnityChan/univ0002"); // Effect 가 디폴트
AudioClip GetOrAddAudioClip(string path, Define.Sound type = Define.Sound.Effect)
{
if (path.Contains("Sounds/") == false)
path = $"Sounds/{path}"; // 📂Sound 폴더 안에 저장될 수 있도록
AudioClip audioClip = null;
if (type == Define.Sound.Bgm) // BGM 배경음악 클립 붙이기
{
audioClip = Managers.Resource.Load<AudioClip>(path);
}
else // Effect 효과음 클립 붙이기
{
if (_audioClips.TryGetValue(path, out audioClip) == false)
{
audioClip = Managers.Resource.Load<AudioClip>(path);
_audioClips.Add(path, audioClip);
}
}
if (audioClip == null)
Debug.Log($"AudioClip Missing ! {path}");
return audioClip;
}
}
- GetOrAddAudioClip
path
를 통해 해당 클립을 AudioClip으로서 로드하고 리턴한다.- 효과음의 경우 굉장히 자주 사용되기 때문에 딱 한번만 로드해서
_audioClip
Dictionary에 보관해두고 여기서 가져올 것이다._audioClip
Dictionary에 해당path
Key 가 존재하는지 검사한다.- TryValue를 통해
path
Key 가 존재한다면 한번 로드 된적이 있어 Dictionary에 보관되어 있는 클립인 것이므로 그냥 그것을 리턴하면 되고, 없다면path
를 통해 오디오 클립을 로드하여 이를_audioClip
Dictionary에 추가하여 보관한다. - 최초 로도 제외하고는 Dictionary 에서 효과음 클립을 로컬 폴더로부터의 로드 없이 가져오게 되므로 성능상 효율적
📜 Manager
static Managers Instance { get { Init(); return s_instance; } }
SoundManager _sound = new SoundManager();
public static SoundManager Sound { get { return Instance._sound; } }
static void Init()
{
// Instance 프로퍼티 get 시 호출되니까 또 여기서 Instance 쓰면 무한 루프 빠짐 주의!
if (s_instance == null)
{
GameObject go = GameObject.Find("@Managers");
if (go == null)
{
go = new GameObject { name = "@Managers" };
go.AddComponent<Managers>();
}
DontDestroyOnLoad(go);
s_instance = go.GetComponent<Managers>();
s_instance._sound.Init(); // ⭐ 📜SoundManager의 Init() 호출
}
}
public static void Clear()
{
Input.Clear(); //
Sound.Clear();
Scene.Clear();
UI.Clear();
}
- Input.Clear()
- 논외지만 Input을 중지시키는건 간단하다. Input 관련 action 들을 null로 초기화하면 땡이다.
// 📜InputManager public void Clear() { KeyAction = null; MouseAction = null; }
- 논외지만 Input을 중지시키는건 간단하다. Input 관련 action 들을 null로 초기화하면 땡이다.
- Sound.Clear()
- 위 참고
- Scene.Clear()
public void Clear() { CurrentScene.Clear(); }
- UI.Clear();
public void Clear() { CloseAllPopupUI(); _sceneUI = null; }
🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄
댓글 남기기