✅ 기능
사운드를 여러 오브젝트에 붙이고, 특정 조건이 발생하면 실행할 수 있지만 씬 규모가 거대해지고 많은 오브젝트들이 생성되면 사운드 관리가 어려워질 수 있습니다. 그렇기에 하나의 사운드 매니저를 사용하고, 싱글톤 패턴으로 구현하여 다른 스크립트에서 쉽게 접근이 가능하고 관리도 용이한 형태로 사운드 매니저를 구현했습니다. 4개월 전 구현했던 사운드 매니저의 단점을 보완하여 새로운 시스템으로 업데이트한 모델입니다.
> 이전 버전의 사운드 매니저에서의 새롭게 추가된 기능 및 장점입니다.
1. 더 이상 순서에 의존하는 오브젝트 형태로 사운드를 담지 않아 사운드의 추가, 삭제가 용이합니다.
2. AudioClip 자체를 레퍼런스하기때문에 오브젝트가 미리 로드되지 않습니다.
3. 사운드 크기 조절에 대한 비용이 크게 감소합니다.
4. 3D 사운드를 쉽게 재생할 수 있습니다.
5. 구조가 매우 단순하며 사운드 재생이 매우 편리합니다.
6. 별도의 오디오소스 컴포넌트를 사용하기때문에 하나의 오디오 클립에 대하여 중복으로 재생이 가능합니다.
> 개선해야할 사항
1. 자주 사용하지 않을 오디오클립을 미리 로드시켜놓으면 메모리를 낭비하기때문에, 로컬에서 불러오는 방식을 추가로 구현해야합니다.
- 이전 버전의 사운드 매니저
✅ 흐름도
외부에서 사운드를 재생하고 싶을 때 사운드 매니저의 인스턴스에 접근하여 사운드를 실행합니다.
사운드가 재생되면 Temporary Sound Player에서 해당 사운드가 끝나면 스스로 파괴하여 사운드 재생이 완료됩니다.
✅ 사용 예시
총을 발사하거나 줍는 소리는 2D 사운드로 재생됩니다.
좀비 소리는 3D 사운드로 재생되어 좀비 오브젝트의 자식에서 생성되어 위치에따라 소리가 다르게 들리도록 재생됩니다.
✅ 구현 1
//SoundManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
//사운드의 타입이다. 사운드를 중단을 식별하기위해 사용한다.
public enum SoundType
{
BGM,
EFFECT,
}
public class SoundManager : Singleton<SoundManager>
{
/// <summary>
/// 오디오 믹서, 오디오의 타입별로 사운드를 조절할 수 있도록 한다.
/// </summary>
[SerializeField] private AudioMixer mAudioMixer;
//옵션에서 설정된 현재 배경음악과 효과 사운드의 불륨이다. 효과는 BGM을 제외한 모든 소리의 불륨을 담당한다.
private float mCurrentBGMVolume, mCurrentEffectVolume;
/// <summary>
/// 클립들을 담는 딕셔너리
/// </summary>
private Dictionary<string, AudioClip> mClipsDictionary;
/// <summary>
/// 사전에 미리 로드하여 사용할 클립들
/// </summary>
[SerializeField] private AudioClip[] mPreloadClips;
private List<TemporarySoundPlayer> mInstantiatedSounds;
private void Start()
{
mClipsDictionary = new Dictionary<string, AudioClip>();
foreach (AudioClip clip in mPreloadClips)
{
mClipsDictionary.Add(clip.name, clip);
}
mInstantiatedSounds = new List<TemporarySoundPlayer>();
}
/// <summary>
/// 오디오의 이름을 기반으로 찾는다.
/// </summary>
/// <param name="clipName">오디오의 이름(파일 이름 기준)</param>
/// <returns></returns>
private AudioClip GetClip(string clipName)
{
AudioClip clip = mClipsDictionary[clipName];
if (clip == null) { Debug.LogError(clipName + "이 존재하지 않습니다."); }
return clip;
}
/// <summary>
/// 사운드를 재생할 때, 루프 형태로 재생된경우에는 나중에 제거하기위해 리스트에 저장한다.
/// </summary>
/// <param name="soundPlayer"></param>
private void AddToList(TemporarySoundPlayer soundPlayer)
{
mInstantiatedSounds.Add(soundPlayer);
}
/// <summary>
/// 루프 사운드 중 리스트에 있는 오브젝트를 이름으로 찾아 제거한다.
/// </summary>
/// <param name="clipName"></param>
public void StopLoopSound(string clipName)
{
foreach(TemporarySoundPlayer audioPlayer in mInstantiatedSounds)
{
if(audioPlayer.ClipName == clipName)
{
mInstantiatedSounds.Remove(audioPlayer);
Destroy(audioPlayer.gameObject);
return;
}
}
Debug.LogWarning(clipName + "을 찾을 수 없습니다.");
}
/// <summary>
/// 2D 사운드로 재생한다. 거리에 상관 없이 같은 소리 크기로 들린다.
/// </summary>
/// <param name="clipName">오디오 클립 이름</param>
/// <param name="type">오디오 유형(BGM, EFFECT 등.)</param>
public void PlaySound2D(string clipName, float delay = 0f, bool isLoop = false, SoundType type = SoundType.EFFECT)
{
GameObject obj = new GameObject("TemporarySoundPlayer 2D");
TemporarySoundPlayer soundPlayer = obj.AddComponent<TemporarySoundPlayer>();
//루프를 사용하는경우 사운드를 저장한다.
if (isLoop) { AddToList(soundPlayer); }
soundPlayer.InitSound2D(GetClip(clipName));
soundPlayer.Play(mAudioMixer.FindMatchingGroups(type.ToString())[0], delay, isLoop);
}
/// <summary>
/// 3D 사운드로 재생한다.
/// </summary>
/// <param name="clipName"></param>
/// <param name="audioTarget"></param>
/// <param name="type"></param>
/// <param name="attachToTarget"></param>
/// <param name="minDistance"></param>
/// <param name="maxDistance"></param>
public void PlaySound3D(string clipName, Transform audioTarget, float delay = 0f, bool isLoop = false, SoundType type = SoundType.EFFECT, bool attachToTarget = true, float minDistance = 0.0f, float maxDistance = 50.0f)
{
GameObject obj = new GameObject("TemporarySoundPlayer 3D");
obj.transform.localPosition = audioTarget.transform.position;
if (attachToTarget) { obj.transform.parent = audioTarget; }
TemporarySoundPlayer soundPlayer = obj.AddComponent<TemporarySoundPlayer>();
//루프를 사용하는경우 사운드를 저장한다.
if (isLoop) { AddToList(soundPlayer); }
soundPlayer.InitSound3D(GetClip(clipName), minDistance, maxDistance);
soundPlayer.Play(mAudioMixer.FindMatchingGroups(type.ToString())[0], delay, isLoop);
}
//씬이 로드될 때 옵션 매니저에의해 모든 사운드 불륨을 저장된 옵션의 크기로 초기화시키는 함수.
public void InitVolumes(float bgm, float effect)
{
SetVolume(SoundType.BGM, bgm);
SetVolume(SoundType.EFFECT, effect);
}
//옵션을 변경할 때 소리의 불륨을 조절하는 함수
public void SetVolume(SoundType type, float value)
{
mAudioMixer.SetFloat(type.ToString(), value);
}
/// <summary>
/// 무작위 사운드를 실행하기위해 랜덤 값을 리턴 (included)
/// </summary>
/// <param name="from">시작하는 인덱스 번호</param>
/// <param name="includedTo">끝나는 인덱스 번호(포함)</param>
/// <param name="isStartZero">한자리일경우 0으로 시작하는가? 예)01</param>
/// <returns></returns>
public static string Range(int from, int includedTo, bool isStartZero = false)
{
if (includedTo > 100 && isStartZero) { Debug.LogWarning("0을 포함한 세자리는 지원하지 않습니다."); }
int value = UnityEngine.Random.Range(from, includedTo + 1);
return value < 10 && isStartZero ? '0' + value.ToString() : value.ToString();
}
}
[SerializeField] private AudioMixer mAudioMixer;
- 오디오 믹서입니다.
- BGM, EFFECT 등 여러 사운드 타입의 불륨을 조절하기 위해 사용합니다.
private Dictionary<string, AudioClip> mClipsDictionary;
[SerializeField] private AudioClip[] mPreloadClips;
- 외부에서 오디오 클립의 이름으로 접근하기위한 딕셔너리와 오디오 클립을 사전에 로드시킬 배열입니다.
- 인스펙터에서 mPreloadClips에 오디오 클립을 집어넣으면 딕셔너리에 이름으로 삽입됩니다.
private List<TemporarySoundPlayer> mInstantiatedSounds;
- TemporarySoundPlayer을 담는 리스트입니다.
- 이 리스트는 Loop로 재생되는 오디오를 추후에 접근하여 제거하기위해 사용합니다.
private void Start()
- 오디오 클립을 딕셔너리에 삽입합니다.
private AudioClip GetClip(string clipName)
- 딕셔너리에서 오디오 클립을 찾습니다
- 오디오 클립을 찾을 수 없는경우 스크립트에서 오디오의 이름을 잘못 입력한것이 가장 큰 이유이기에 LogError로 에러 표시를 나타내도록 합니다.
public void PlaySound2D(string clipName, SoundType type = SoundType.EFFECT)
- 2D 사운드로 오디오를 재생합니다.
- 새로운 오브젝트가 생성되어 Vector3.zero의 위치 (0, 0, 0)에 생성되며 2D사운드를 재생합니다.
- SoundType은 해당 타입의 소리 불륨을 따라가도록 설정합니다.
public void PlaySound3D(string clipName, Transform audioTarget, SoundType type = SoundType.EFFECT, bool attachToTarget = true, float minDistance = 0.0f, float maxDistance = 50.0f)
- 3D 사운드로 오디오를 재생합니다.
- 새로운 오브젝트가 생성되며 audioTarget의 위치에서 생성됩니다.
- attachToTarget이 활성화되면 해당 오브젝트의 자식으로 붙습니다. 아니면 해당 위치에만 생성됩니다.
- minDistance, maxDistance를 통해 사운드가 들리는 거리를 조절합니다.
public void SetVolume(SoundType type, float value)
- 사운드의 크기를 조절합니다.
- SoundType별로 해당 레이어(오디오믹서)의 불륨을 조절합니다.
public void StopLoopSound(string clipName)
- mInstantiatedSounds에 저장되어 있는 오디오를 찾아서 제거합니다.
- 호출시 이름으로 사용했던 것을 사용하여 찾으며, 제거합니다.
- 찾을 수 없다면 오류 포맷으로 디버그합니다.
public static string Range(int from, int includedTo, bool isStartZero = false)
- 같은 유형의 사운드가 여러개일경우 사운드를 랜덤으로 재생하기위해 제작한 함수입니다.
- 숫자 형태로 리턴이되며 to는 include되어 리턴됩니다.
- isStartZero가 true인경우에는 사용방법에서 나온 예시대로 사운드가 01, 02, 03 처럼 한자리일경우 0이 붙는경우에 사용할 수 있습니다.
✅ 구현 2
//TemporarySoundPlayer.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
[RequireComponent(typeof(AudioSource))]
public class TemporarySoundPlayer : MonoBehaviour
{
private AudioSource mAudioSource;
public string ClipName
{
get
{
return mAudioSource.clip.name;
}
}
public void Awake()
{
mAudioSource = GetComponent<AudioSource>();
}
public void Play(AudioMixerGroup audioMixer, float delay, bool isLoop)
{
mAudioSource.outputAudioMixerGroup = audioMixer;
mAudioSource.loop = isLoop;
mAudioSource.Play();
if (!isLoop) { StartCoroutine(COR_DestroyWhenFinish(mAudioSource.clip.length)); }
}
public void InitSound2D(AudioClip clip)
{
mAudioSource.clip = clip;
}
public void InitSound3D(AudioClip clip, float minDistance, float maxDistance)
{
mAudioSource.clip = clip;
mAudioSource.spatialBlend = 1.0f;
mAudioSource.rolloffMode = AudioRolloffMode.Linear;
mAudioSource.minDistance = minDistance;
mAudioSource.maxDistance = maxDistance;
}
private IEnumerator COR_DestroyWhenFinish(float clipLength)
{
yield return new WaitForSeconds(clipLength);
Destroy(gameObject);
}
}
public void Play(AudioMixerGroup audioMixer, float delay, bool isLoop)
- SoundManager.cs에서 호출되며 사운드를 재생합니다.
- 코루틴을 실행하여 오디오클립의 길이 후 스스로 파괴되도록 설정합니다.
- isLoop가 true인경우 코루틴을 실행하지 않고 스스로 파괴하지 않도록 설정합니다. (사운드 매니저에 저장되어 나중에 접근하여 파괴할 수 있습니다)
public void InitSound2D(AudioClip clip)
public void InitSound3D(AudioClip clip, float minDistance, float maxDistance)
- SoundManager.cs에서 호출되며 오디오 소스를 설정합니다.
private IEnumerator COR_DestroyWhenFinish(float clipLength)
- 코루틴으로 오디오 클립의 길이만큼 기다렸다가 스스로 파괴합니다.
✅ 사용 방법
- 씬에 SoundManager.cs를 넣은 후 Preloaded Clips에 사용할 오디오 클립을 넣습니다.
- 불륨을 조절하기 위해 오디오 믹서 에셋을 넣습니다.
- 아래 링크는 오디오 믹서를 이용하여 사운드 크기를 조절하는 방법을 나타낸 글 입니다.
- 특정 조건이 발생되면 사운드 매니저의 인스턴스에 접근하여 소리를 재생합니다.
- SoundManager.Instance.PlaySound2D(...)를 통하여 외부에서 쉽게 소리를 재생할 수 있습니다.
/// <summary>
/// 방패로 공격(방패 밀치기)
/// </summary>
public class SHIELD_SHOVE : State<PlayerController>
{
...
public override void Enter(PlayerController entity)
{
//ShieldState가 2이면, 방패 공격 활성화
entity.Animator.SetInteger("_ShieldState", 2);
//애니메이션 베이스 레이어는 IDLE로 설정(서 있도록 함);
entity.Animator.SetInteger("_AnimState", 0);
SoundManager.Instance.PlaySound2D("Male Grunt " + SoundManager.Range(1, 21, true));
//애니메이션 끝남상태가 아님
mIsAnimEnd = false;
}
...
}
'unity game modules' 카테고리의 다른 글
[유니티] 3D World, Sprite Ordering Layer (0) | 2022.12.23 |
---|---|
[유니티] 애니메이션 랜덤으로 재생하기(StateMachineBehaviour) (0) | 2022.11.23 |
[유니티] NavMesh를 응용하여 '내비게이션(경로 시각화)' 만들기 (0) | 2022.10.22 |
[유니티] 파일브라우저(FileExplorer)를 이용하여 로컬 파일 저장/읽기 (SimpleFileBrowser) (0) | 2022.10.22 |
[유니티] 지정한 위치에 직선 형태로 여러 발을 소환하여 공격하기 (0) | 2022.09.26 |