Chapter 3-2. 파괴 가능한 환경들 : Sound Manager, 싱글톤

Date:     Updated:

카테고리:

태그:

인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀
🌜 강의 들으러 가기 Click

Chapter 3. 파괴 가능한 환경들

Sound Manager

사운드를 각각의 오브젝트 내에서 관리하는게 아니라 📜SoundManager.cs 한 곳에서 효율적으로 총괄 관리하게끔 한다!


🚖 SoundManager

  • Sound Manager 이름의 빈 오브젝트 생성

싱글톤

싱글톤은 씬이 전환되어도 유지된다.

현재 Sound Manager 오브젝트에 붙어있는 📜SoundManager.cs 를 게임 월드 내에 단 한 개만 유지하고 이를 접근하여 공유하며 사용하려 할 땐 📜SoundManager.cs 을 싱글톤으로 만들면 된다.

  • 만약 📜SoundManager.cs가 싱글톤이 아니였다면
    • A,B,C 오브젝트에서 📜SoundManager.cs를 사용한다면 A, B, C 오프젝트들에게 📜SoundManager.cs를 각각 직접 붙여주어야 한다.
    • 또한 각각 공유하지 않고 독립적인 부품으로서 움직인다.
      • A, B, C 만의 부품이 됨
  • 📜SoundManager.cs를 싱글톤으로 만들면
    • A, B, C 오프젝트들에게 📜SoundManager.cs를 컴포넌트로서 붙여줄 필요가 없다.
    • 📜SoundManager.cs가 붙어있는 오브젝트 인스턴스를 static으로 선언하여 여러 곳에서 공유할 수 있게 하고 메모리를 유지하게 하고 단 하나만 유지할 수 있도록 보장해주면 된다.
static public SoundManager instance; // 자기 자신을 공유 자원으로. static은 씬이 바뀌어도 유지된다.

private void Awake()  // 객체 생성시 최초 실행 (그래서 싱글톤을 여기서 생성)
{
    if (instance == null)   // 최초 생성
    {
        instance = this;  // 현재의 자기 자신(인스턴스)를 할당
        DontDestroyOnLoad(gameObject);  // 씬 전환되도 자기 자신이 파괴되지 않고 유지되도록
    }
    else  // 단 하나만 존재하게끔 새로 생긴 Sound Manager 오브젝트 인스턴스일 경우엔 파괴
        Destroy(this.gameObject); 
}
  • instance 👉 자기 자신을 할당할 멤버. 자기 자신을 공유 자원으로
    • 유니티에서의 static 특징
      • 게임 내내 메모리가 유지된다.
        • 따라서 씬이 전환되어도 살아서 그 값이 유지된다.
          • 유니티에선 다른 씬으로 전환되면 기존 씬의 오브젝트들을 모두 파괴한다. 그리고 다시 원래 씬으로 돌아오면 기존 씬의 초기화 상태로 오브젝트들을 새롭게 다시 생성한다.
          • 그러나 씬이 바뀌어 오브젝트가 파괴되더라도 static 멤버 값 메모리는 그대로 유지가 된다. 나중에 원래 씬으로 돌아와 오브젝트를 새롭게 만들더라도 static 멤버 메모리는 유지되고 있는 것을 확인할 수 있다.
            • 씬이 바뀌어도 오브젝트를 파괴하지 않고 유지시키고 싶으면 DontDestroyOnLoad(gameObject) 함수를 실행하여 씬이 바뀌어도 이 오브젝트는 파괴되지 않고 모든 멤버 값을 유지한다는것을 보장해주면 된다.
  • Awake() 👉 오브젝트가 생성될 때 호출된다.
    • 오브젝트 생성시에만 최초로 실행 된다.
      • 따라서 싱글톤은 Awake() 안에서 구현하는 것이 좋다.
    • 보통 초기화 작업들을 넣는 Start()보다 먼저 호출되기 때문이다.
    • 씬이 전환되면 기존 씬의 오브젝트들은 삭제되고 다시 원래 씬으로 돌아오면 새롭게 오브젝트들을 생성하므로, 오브젝트가 생성될 때 마다, 씬이 바뀔 때마다 Awake()가 호출된다.
  • instance에 현재 아무 것도 할당 되어 있지 않을 때 👉 instance에 자기 자신인 📜SoundManager.cs 최초 할당
    if (instance == null)
    
    • instance에 자기 자신인 Sound Manager 오브젝트를 할당한 후
      instance = this;
      
    • 씬이 바뀔 때 자기 자신(Sound Manager)이 파괴되는 것을 방지한다.
      DontDestroyOnLoad(gameObject);
      
      • 📜SoundManager.cs 에서 static 멤버인 instance에 자기 자신(Sound Manager)을 담고 있기 때문에 Sound Manager은 씬이 전환되도 메모리에 보존 되고 파괴되지는 않는다. 다만 static 멤버를 제외한 📜SoundManager.cs의 다른 멤버들은 씬이 바뀌면 값이 모조리 날라가 버린다.
  • instance가 null이 아니라면 👉 기존에 instanceSound Manager 인스턴스를 할당을 해놔서 그 메모리를 보존하고 있다는 뜻
    • Sound Manager 인스턴스는 단 하나만 존재해야 하므로 새롭게 생성된 Sound Manager는 파괴해주어야 한다.
      • 다른 씬 됐다가 원래의 씬으로 다시 전환 되면 유니티가 오브젝트들을 새롭게 다시 생성하므로 Sound Manager 오브젝트도 생성한다.
      • 그러나 씬전환 전에 최초로 Sound Manager 오브젝트를 생성했었을 때 instance에 그때의 Sound Manager 오브젝트를 할당했고 그게 메모리에 계속 유지가 되왔으므로 다른 씬 됐다가 원래의 씬으로 다시 전환 되서 또 새롭게 만들어진 Sound Manager의 📜SoundManager.cs 에선 Awake()를 호출하고 이어서 else 에 걸리게 된다.
        • 씬이 재로드 되면서 새롭게 만들어진 Sound Manager 인스턴스를 파괴해준다. (새롭게 만들어진 Sound Manager 입장에서의 자기 자신을 파괴)
          else
              Destroy(this.gameObject); 
          
        • 이 과정을 통해 Sound Manager 오브젝트 인스턴스는 동일한 그 인스턴스가 게임 내내 단 하나만 유지된다. 씬 전환으로 새롭게 생긴 Sound Manager 인스턴스는 파괴되므로!
        • 단 하나만 존재하게끔 다른 씬 갔다가 다시 돌아오면 오브젝트가 다시 생성되서 사운드 매니저 또 생기는 것을 파괴해 줌
      • 기존 instance에 보관되어 있는 기존의 그 Sound Manager 인스턴스는 다른 씬 갔다가 돌아와도 Awake()가 실행되지 않는다. 여태 계속 살아 있었고 새로 다시 태어나는게 아니니까 !
  • DontDestroyOnLoad(gameObject)
    • 인수로 넘긴 오브젝트가 씬이 전환되더라도 파괴하지 않고 유지할 수 있게 해주는 함수다.
    • 이 함수를 해주지 않으면 Sound Manager 오브젝트는 사라진다.
      • 물론 static인 instance에 기존 인스턴스를 담아 두어 메모리에 계속 보관되긴 하지만 그래도 게임 씬 상에 존재하는 유니티 오브젝트로서는 사라지니까 📜SoundManager.cs 또한 실행시킬 수 없음. 따라서 오브젝트가 씬 전환 되어도 파괴안되게 막아 주어야 함.


📜SoundManager.cs

Sound Manager 오브젝트에 붙이기

  • 여러 오브젝트들의 사운드를 총괄 담당하는 스크립트인 만큼 여러 곳에서 접근 및 공유가 되야 하므로 싱글톤으로 구현하기
    • 단 하나로 게임 내내 살아서 유지가 되야 함. (Sound Manager 오브젝트도 내내 활성화 되있어야 한다는 소리)
      • 여러 곳에서 접근해야 하니까 파괴되지 않도록.
        • 단 하나를 유지하도록 새로 생성된건 파괴해준다.
      • 공유가 되도록 static 인스턴스로.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class Sound  // 컴포넌트 추가 불가능.  MonoBehaviour 상속 안 받아서. 그냥 C# 클래스.
{
    public string name;  // 곡 이름
    public AudioClip clip;  // 곡
}

public class SoundManager : MonoBehaviour
{
    #region singleton
    static public SoundManager instance;  // 자기 자신을 공유 자원으로. static은 씬이 바뀌어도 유지된다.

    private void Awake()  // 객체 생성시 최초 실행 (그래서 싱글톤을 여기서 생성)
    {
        if (instance == null)  // 단 하나만 존재하게끔
        {
            instance = this;  // 객체 생성시 instance에 자기 자신을 넣어줌
            DontDestroyOnLoad(gameObject);  // 씬 바뀔 때 자기 자신 파괴 방지
        }
        else
            Destroy(this.gameObject);  
    }
    #endregion singleton


    public Sound[] effectSounds;  // 효과음 오디오 클립들
    public Sound[] bgmSounds;  // BGM 오디오 클립들


    public AudioSource audioSourceBFM;  // BGM 재생기. BGM 재생은 한 군데서만 이루어지면 되므로 배열 X 
    public AudioSource[] audioSourceEffects;  // 효과음들은 동시에 여러개가 재생될 수 있으므로 'mp3 재생기'를 배열로 선언

    public string[] playSoundName;  // 재생 중인 효과음 사운드 이름 배열


    void Start()
    {
        playSoundName = new string[audioSourceEffects.Length];
    }

    public void PlaySE(string _name)
    {
        for (int i = 0; i < effectSounds.Length; i++)
        {
            if(_name == effectSounds[i].name)
            {
                for (int j = 0; j < audioSourceEffects.Length; j++)
                {
                    if(!audioSourceEffects[j].isPlaying)
                    {
                        audioSourceEffects[j].clip = effectSounds[i].clip;
                        audioSourceEffects[j].Play();
                        playSoundName[j] = effectSounds[i].name;
                        return;
                    }
                }
                Debug.Log("모든 가용 AudioSource가 사용 중입니다.");
                return;
            }
        }
        Debug.Log(_name + "사운드가 SoundManager에 등록되지 않았습니다.");
    }

    public void PlayBGM(string _name)
    {
        for (int i = 0; i < bgmSounds.Length; i++)
        {
            if (_name == bgmSounds[i].name)
            {
                audioSourceBFM.clip = bgmSounds[i].clip;
                audioSourceBFM.Play();
                return;
            }
        }
        Debug.Log(_name + "사운드가 SoundManager에 등록되지 않았습니다.");
    }

    public void StopAllSE()
    {
        for (int i = 0; i < audioSourceEffects.Length; i++)
        {
            audioSourceEffects[i].Stop();
        }
    }

    public void StopSE(string _name)
    {
        for (int i = 0; i < audioSourceEffects.Length; i++)
        {
            if(playSoundName[i] == _name)
            {
                audioSourceEffects[i].Stop();
                break;
            }
        }
        Debug.Log("재생 중인" + _name + "사운드가 없습니다. ");
    }
}

효과음 vs BGM

  • Sound 클래스 👉 일반 C# 클래스. MonoBehaviour 상속 안 받아서 컴포넌트 추가 불가능
    • [System.Serializable] 을 붙여주면 클래스의 모든 멤버 변수에게 [SerializeField] 을 한꺼번에 적용시키는 것과 같다.
    • name 곡 이름
    • clip 오디오 클립
    • 위 2 개를 묶어서 관리
    • 이 Sound 타입의 배열 멤버
      • effectSounds 👉 효과음 배열. Sound 타입이므로 원소 하나당 곡 이름과 클립 정보 有
      • bgmSounds 👉 BGM 배열. Sound 타입이므로 원소 하나당 곡 이름과 클립 정보 有
        • BGM도 여러개 있을 수 있음. 단 효과음처럼 재생될 땐 동시에 여러 BGM 이 재생되지는 않다.
  • AudioSource 👉 재생기!! MP3 플레이어 같은 것!
    • audioSourceBFM
      • BGM을 재생시킬 Audio Source 컴포넌트
    • audioSourceEffects
      • 효과음들을 재생시킬 Audio Source 컴포넌트
      • 효과음은 동시에 여러개 재생될 수 있으므로 재생기가 여러개여야 한다. 따라서 배열로 선언.
  • playSoundName 👉 문자열 배열. 재생 중인 ‘효과음’들의 이름이 게임 중 실시간으로 원소로 들어간다.
  • Start()
    • Sound Manager 오브젝트가 활성화 되자마자, playSoundName 배열의 크기를 audioSourceEffects 재생기 개수와 일치시킨다.
    • 사용할 효과음 개수만큼 효과음을 재생기들을 똑같은 개수로 생성해주었었기 때문에
  • PlaySE(string _name)
    • 효과음 재생 함수
      • 효과음 배열에서 똑같은 이름의 곡이 있는지를 찾아 검사하고
        // for문
        if(_name == effectSounds[i].name)
        
      • 해당 곡이 있다면 현재 재생중이지 않고 놀고 있는 재생기를 찾는다. 이 효과음을 재생시킬 수 있는 AudioSource 를 찾는 것이다. 재생 중이지 않은 재생기를 찾으면 해당 효과음을 재생 시키고 playSoundName에 효과음 이름을 원소로 추가해주고 바로 함수를 종료시킨다.
                  for (int j = 0; j < audioSourceEffects.Length; j++)
                  {
                      if(!audioSourceEffects[j].isPlaying)  // 재생중이지 않고 놀고 있는 재생기 발견
                      {
                          audioSourceEffects[j].clip = effectSounds[i].clip;
                          audioSourceEffects[j].Play();   // 재생시키고
                          playSoundName[j] = effectSounds[i].name;  // `playSoundName` 원소 추가
                          return;
                      }
                  }
        
        
  • PlayBGM(string _name)
    • BGM 재생 함수
      • BGM 배열에서 똑같은 이름의 곡이 있는지를 찾아 검사한다.
      • 어차피 BGM 재생기는 하나이므로 해당 곡을 찾으면 바로 재생시키고 함수 종료시키면 된다.
  • StopAllSE()
    • 모든 효과음 재생을 멈춤
  • StopSE(string _name)
    • 특정 효과음의 재생을 멈춤
      • playSoundName 배열을 검사하여 인수로 들어온 그 특정 효과음 원소(이름)을 찾는다. 있다면 그 효과음을 재생하고 있는 AudioSource를 멈춘다.
      • 이게 가능한 이유 + 그리고 playSoundName 배열을 만든 이유.
        • playSoundName의 인덱스와 audioSourceEffects 의 인덱스는 서로 대응한다. 따라서 현재 재생중인 효과음을 이름(string)으로 playSoundName에서 찾아, 동일한 인덱스의 audioSourceEffects 원소(AudioSource)에서 재생 중이므로 해당 효과음을 현재 재생중인 재생기를 쉽게 알 수 있다.

image

  • Audio Source 컴포넌트를 7 개 추가해주었다. 그리고 각각의 멤버에 할당해주었다.
    • audioSourceBFM 하나는 BGM 재생기
    • audioSourceEffects 6 개는 효과음 재생기 (효과음을 6 개 쓸거라서)
  • PlaySoundName 배열은 게임이 시작되면 Start() 에 의해서 배열의 크기가 6 으로 설정 될 것이다.
  • 효과음 2 개만 effectSounds 원소로 추가해보았다.
    • 이름과 함께 오디오 클립 할당


📜Rock.cs

📜SoundManager.cs 사용하여 재생하도록 스크립트 수정

    // 필요한 사운드 이름
    [SerializeField]
    private string strike_Sound;
    [SerializeField]
    private string destroy_Sound;

    public void Mining()
    {
        SoundManager.instance.PlaySE(strike_Sound);

        GameObject clone = Instantiate(go_effect_prefabs, col.bounds.center, Quaternion.identity);
        Destroy(clone, destroyTime);

        hp--;
        if (hp <= 0)
            Destruction();
    }

    private void Destruction()
    {
        SoundManager.instance.PlaySE(destroy_Sound);

        col.enabled = false;
        Destroy(go_rock);

        go_debris.SetActive(true);
        Destroy(go_debris, destroyTime);
    }
SoundManager.instance.PlaySE(strike_Sound);  // 곡괭이로 바위 때릴시 효과음 재생
SoundManager.instance.PlaySE(destroy_Sound); // 바위가 파괴될 때 효과음 재생

직접 📜Rock.cs 에서 Audio Source 컴포넌트를 붙이고 멤버로 가져오고 했던 과정을 지우고 📜SoundManager.cs 싱글톤을 사용해 재생해주는 방식으로 코드를 수정했다. 📜SoundManager.cs의 PlaySE 함수의 인수로 넘겨줄 곡 이름 (string)만 있으면 된다.

image



🌜 개인 공부 기록용 블로그입니다. 오류나 틀린 부분이 있을 경우 
언제든지 댓글 혹은 메일로 지적해주시면 감사하겠습니다! 😄

맨 위로 이동하기

댓글남기기