TIL(Today I Learned)

[Unity] 코루틴(Coroutine)을 이용한 지연 실행, AudioManager 만들기 - TIL#35

Najdorf 2024. 2. 13. 21:51
728x90

1. AudioManager?

 

AudioManager를 쓰는 이유는, 플레이어 기준에서 뇌 속에서 바로 들리는 소리들을 한데 묶어 편하게 관리하기 위함이다.

예를 들면, BGM(배경음악), 플레이어 관련 SFX(효과음) 등이 있을 수 있다. (걷기, 뛰기, 도약, 착지 등)

 

그래서 어떤 AudioSource 컴포넌트를 만들어서 쓴다면,

무작정 AudioManager를 거치게 하는게 아니라,

해당 음원이 플레이어의 뇌 속에서 울리는 지 아닌 지를 생각해보자.

 

  • 플레이어 뇌 속에서 들림(BGM, 플레이어 SFX 등) => AudioManager 활용
  • 어떤 객체를 기준으로 적용되는 3D 음원(거리에 따라 멀어지는 장작 소리 등) => 해당 객체에 AudioSource 컴포넌트 추가

 

그래서 AudioManager를 쓰게 된다면,

나는 하단의 영상을 참고해서 스크립트 구조를 다음과 같이 정했다.

  • AudioManager(싱글톤) : BGM, SFX 등을 겹치지 않게 재생만 함
  • 객체의 Sound 스크립트 : 해당 객체와 관련된 사운드의 상세 조건과 로직을 구현, 소스를 직접 받아옴

 

다음은 내가 오늘 작업한 AudioManager와 PlayerSound 스크립트이다.

더보기

AudioManager.cs

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

public class SoundManager : MonoBehaviour
{
    // SoundManager에선 3D가 적용되지 않는 소리(BGM, 일부SFX)만 관리합니다.
    public static SoundManager instance;

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

    public void SFXPlay(string sfxName, AudioClip clip)
    {
        if (GameObject.FindWithTag(sfxName) == null)
        {
            GameObject go = new GameObject(sfxName + "Sound");
            go.tag = sfxName;
            AudioSource audioSource = go.AddComponent<AudioSource>();
            audioSource.clip = clip;
            audioSource.Play();

            Destroy(go, clip.length);
        }
    }


}

 

더보기

PlayerSound.cs

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

public class PlayerSound : MonoBehaviour
{
    public AudioClip[] walkMetalV1;
    public AudioClip[] runMetalV1;
    public AudioClip[] jumpStartMetalV1;
    public AudioClip[] jumpLandMetalV1;

    public bool isRun = false; // 플레이어가 뛰고 있는가?
    public bool isGrounded = false; // 플레이어가 바닥에 붙어 있는가?
    private bool isGroundedOnce = false;

    private void Update()
    {
        CheckGroundedOnce();
    }

    IEnumerator LoopMoveSFX() // 플레이어 움직임 관련 코루틴
    {
        while (true)
        {
            if (isGrounded) // 바닥에 붙어있고
            {
                if (isRun == false) // 걷고 있다면
                {
                    float wait = PickRandomSFX("WalkSFX", walkMetalV1);
                    yield return new WaitForSeconds(wait);
                }
                else // 뛰고 있다면
                {
                    float wait = PickRandomSFX("RunSFX", runMetalV1);
                    yield return new WaitForSeconds(wait);
                }
            }
            else
            {
                yield return null; 
            }
        }
    }

    public float PickRandomSFX(string tag, AudioClip[] inputs) // AudioClip[] 중에서 하나 뽑고 tag를 붙여 SoundManager에 전달. return값은 소리 길이.
    {
        int rand = Random.Range(0, inputs.Length); // 소스 랜덤 뽑기
        SoundManager.instance.SFXPlay(tag, inputs[rand]);
        return inputs[rand].length;
    }

    private void CheckGroundedOnce()
    {
        if (isGrounded && !isGroundedOnce)
        {
            isGroundedOnce = true;
            OnLand();
        }
        else if (!isGrounded)
        {
            isGroundedOnce = false;
        }
    }

    public void OnWalk()
    {
        StartCoroutine("LoopMoveSFX");
    }

    public void OnJump()
    {
        PickRandomSFX("JumpStartSFX", jumpStartMetalV1);
    }

    public void OnLand()
    {
        PickRandomSFX("JumpLandSFX", jumpLandMetalV1);
    }

    public void OffWalk()
    {
        StopCoroutine("LoopMoveSFX");
    }
}

보면 플레이어의 걷기, 뛰기, 도약, 착지에 관련된 로직이 PlayerSound.cs에 상세히 구현되어 있고,

AudioManager는 단순히 겹치지 않게 음원을 실행해주는 역할을 한다.

(일부 변수들은 PlayerControl.cs 와 통신한다.)

 

 

https://www.youtube.com/watch?v=KJfzT1VfOaM

(위 영상을 참고하니 AudioManager를 이해하고 다루는데 큰 도움이 됐다.)


2. 코루틴(Coroutine)을 활용한 지연 실행

 

항상 Invoke로 함수 지연 실행을 하다 골치가 아팠던 적이 많다.

아까도 그랬다. 파라미터를 못 넣는 것도 그렇고 예상한 순차대로 작동하는 일이 거의 없었다.

 

그래서 찾아봤더니 Unity는 코루틴이라는 것을 지원한다.

https://docs.unity3d.com/kr/current/Manual/Coroutines.html

 

코루틴 - Unity 매뉴얼

코루틴을 사용하면 작업을 다수의 프레임에 분산할 수 있습니다. Unity에서 코루틴은 실행을 일시 정지하고 제어를 Unity에 반환하지만 중단한 부분에서 다음 프레임을 계속할 수 있는 메서드입니

docs.unity3d.com

 

공식 문서 안에 뭔가 많긴 하지만, 내가 쓸 건 지연 실행과 일정 주기 반복이기 때문에

그것만 간단히 짚고 넘어가려고 한다.

 

 

코루틴의 사용방법이다.

'IEnumerator' 라는 인터페이스로 기존 함수를 선언하듯 하고,

실행과 중단은 StartCoroutine("코루틴명"), StopCoroutine("코루틴명") 하는 식이다.

 

가장 중요한 시간 지연은,

yield return new WaitForSeconds(시간) 을 하는 식이다!

null이면 1프레임을 뜻한다.

 

이 외에도 많은 세부 메서드와 기능이 있으나, 나는 단순히 지연과 반복 기능만 쓸 것이기에

상세히 다루지 않는다.

 

코루틴의 반복은 while문 안에다가 집어넣어버리고 종료 조건만 잘 갖추면 된다.

 

IEnumerator LoopMoveSFX() // 플레이어 움직임 관련 코루틴
{
    while (true)
    {
        if (isGrounded) // 바닥에 붙어있고
        {
            if (isRun == false) // 걷고 있다면
            {
                float wait = PickRandomSFX("WalkSFX", walkMetalV1);
                yield return new WaitForSeconds(wait);
            }
            else // 뛰고 있다면
            {
                float wait = PickRandomSFX("RunSFX", runMetalV1);
                yield return new WaitForSeconds(wait);
            }
        }
        else
        {
            yield return null; 
        }
    }
}

public void OnWalk()
{
    StartCoroutine("LoopMoveSFX");
}

public void OffWalk()
{
    StopCoroutine("LoopMoveSFX");
}

 

이런 식으로 활용한다.


3. (번외) FMOD와 적응형 배경음악

 

평소 게임 음악에 관심이 많아 적응형 배경음악을 사용한 연출에 깊은 감명을 받곤 했다.

지금은 게임 개발을 공부하는 입장으로서, 그게 뭐고 어떻게 하는 건지 알아보기로 한다.

 

적응형 배경음악이란 객체의 상황에 맞춰 배경음악 또한 해당 분위기에 맞게 변화하는 배경음악을 말한다.

플레이어가 마을에 있다가 사냥에 나서면 자연스럽게 웅장한 BGM으로 바뀌는 식이다.

 

이걸 편하게 개발할 수 있는 미들웨어가 대표적으로 두 가지가 있는데,

FMOD와 Wwise가 있다.

 

Unity 인디 개발에서는 라이센스 편의 등의 이유로 FMOD를 애용한다.

 

아래 영상이 이해하는데 매우 도움이 되었다.

https://www.youtube.com/watch?v=-kE4-e-ajUg

 

728x90