Sound
Sound Manager
1. MP3 Player, 소리 발생 근원지 👉 Audio Source 컴포넌트
2. MP3 음원, 어떤 소리를 재생할지, 음반 👉 Audio Clip
3. 관객, 듣는 사람, 귀 👉 Audio Listner 컴포넌트
- Audio Listner 컴포넌트는 Main Camera에 기본적으로 달려있는 컴포넌트이다.
- 한 Scene 안에 하나만 있으면 된다.
소리를 발생시킬 오브젝트들에게 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 배열로 관리한다.
각 원소에 재생기 할당은 밑에 Init() 에서 진행한다.
사운드 종류는 📜Define의 Sound에 의해 2 개(BGM, Effect)이며 각각의 Audio Source 재생기를 가진다. 즉, Audio Source 는 2개이다.
재생기당 하나의 클립만 재생할 수 있으므로 이는 곧 2 개의 음을 중첩하여 재생할 수 있다는 의미가 된다.
- 배경음악 재생기 _audioSources[(int)Define.Sound.Bgm]
- 효과음 재생기 _audioSources[(int)Define.Sound.Effect]
효과음 사운드는 자주 재생된다. 따라서 계속된 로드는 성능을 떨어뜨릴 수 있으므로 미리 캐싱을 진행한다. 따라서 _audioClips Dictionary에 효과음 클립들을 미리 로드하여 보관하고 여기서 갖다 쓸 것이다.
Key : 클립의 path
Value : 해당 효과음 클립
👉 Object Pooling
다만 위와 같이 게임 내내 유지되는 빈 오브젝트에 Audio Source 를 붙이면, 거리에 따라 소리 크기가 달라지는 3D 사운드 효과는 낼 수가 없다. 배경음악처럼 100% 크게 다 들려야 하는 소리라던가 그런 사운드만 사운드 매니저에서 관리하는 것이 좋다.
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 를 붙일 오브젝트들을 만들고 컴포넌트를 붙여준다. BGM 재생기의 경우 무한 반복 재생하도록 설정한다.
- @Sound라는 이름의 오브젝트가 없다면 만들고 이를 부모로 하는 자식 오브젝트 Bgm, Effect라는 이름의 오브젝트들을 만들고 각각 이에 Audio Source를 붙일 것이다.
- 이 Audio Source들은 게임 내내 파괴되지 않고 살아있도록 DontDestroyOnLoad 함수로 인해 보호. 파괴 방지!
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
1. BGM 배경음악 재생시
BGM 재생기인 _audioSources[(int)Define.Sound.Bgm]을 통해 재생
BGM 배경음악은 중첩되지 않게 딱 하나만 재생되어야 한다.
- 그러므로 BGM 재생기 AudioSource가 isPlaying 이미 재생중인게 있다면 정지해준다.
- Play() 함수를 통해 재생한다.
2. Effect 효과음 재생시
효과음 재생기인 _audioSources[(int)Define.Sound.Effect]을 통해 재생
효과음은 중첩되도 된다.
원하는 클립을 중첩해서 재생 가능하게끔 PlayOneShot 함수를 통해 재생한다. 이는 함수를 통해 재생만 하고 사후 처리를 하는 개념이 아니라 Play() 와 달리 중간에 멈출 수가 없다. 한번 던져지면 재생기 Audio Source가 꺼지는게 아닌 이상 끝까지 재생된다.
음원의 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;
}
Sound.Clear()
위 참고
Scene.Clear()
public void Clear()
{
CurrentScene.Clear();
}
UI.Clear()
public void Clear()
{
CloseAllPopupUI();
_sceneUI = null;
}
3D 사운드
거리에 따라 사운드 크기가 달라야 경우에는 오브젝트에 Audio Source 를 붙여야한다. 그 오브젝트 자체가 소리의 진원지가 될 수 있도록!
오브젝트에 Audio Source 를 붙인다.
근데 반드시 꼭 Audio Source 달고 있는 오브젝트에서만 소리가 나야하는건 아니다! 다른 좌표에서도 소리를 재생하
게 할 수 있다. 그 함수가 바로 PlayClipAtPoint(AudioClip, Vector3)
audioSource.PlayClipAtPoint(clip, new Vector3(5, 1, 2));
Audio Source 속성
Spatial Blend
최대값에 가까울 수록 3 D 사운드 에 가깝고 최소값에 가까울 수록 2 D 사운드에 가깝다.
3D Sound Settings
거리에 따라 어떻게 소리가 들릴지에 대한 그래프 설정이다. Volume RollOff로 그래프 모양 설정. 예를 들어 일직선으로 감소하는 모양의 그래프라면 거리에 따라 1:1로 비례하면서 소리가 작아지게 될 것이다.
minDistance, maxDistance 소리를 들을 수 있는 범위
Audio Listener 컴포넌트가 (디폴트로 메인카메라에 붙어있음) 이 범위 안에 있어야지 소리를 들을 수 있다.
Audio Listner
좀 더 3D 현실감을 주고 싶다면 플레이어의 귀 부분에 Audio Listener 컴포넌트를 달 수도 있겠다!
단, 활성화 되어 있는 Audio Listener 컴포넌트 월드에서 하나만 존재해야 하기 때문에 이럴땐 메인 카메라의 Audio Listener 컴포넌트를 비활성화 해주어야 한다.
'공부 > 인프런 - Rookiss' 카테고리의 다른 글
Part 3-11-1. Corutine : 코루틴 최적화, 커스텀 (1) | 2023.08.29 |
---|---|
Part 3-10-1. Object Pooling : Pool Manager (0) | 2023.08.28 |
Part 3-8-1. Scene : Scene Manager (0) | 2023.08.26 |
Part 3-7-5. UI : 인벤토리 실습 (0) | 2023.08.25 |
Part 3-7-4. UI : UI Manager, Blocker (0) | 2023.08.25 |