본문 바로가기
Unity/Undead Survivor

싱글톤 패턴(Singleton Pattern) / 게임 매니저(Game Manager) / 오디오 매니저(Audio Manager)

by hwan91 2025. 3. 19.

Singleton Pattern

싱글톤 패턴은 게임에서 "하나만 존재해야 하는 객체"를 만들 때 사용하는 디자인 패턴이다.
예를 들어, GameManager(게임 전체 관리), AudioManager(배경음, 효과음 관리), UIManager(UI 관리) 이런 것들은 굳이 여러 개 만들 필요가 없고 어디서든 쉽게 접근할 수 있어야 한다.
그래서 싱글톤 패턴을 사용하면 딱 하나만 존재하도록 강제할 수 있다.

 

기본 패턴 코드

public class GameManager : MonoBehaviour
{
    public static GameManager instance; // 자기 자신을 저장할 변수 (정적 변수)

    void Awake()
    {
        if (instance == null)  // 처음 실행되었을 때만 설정
        {
            instance = this;   // 현재 객체를 저장
            DontDestroyOnLoad(gameObject); // 씬이 바뀌어도 삭제되지 않음
        }
        else
        {
            Destroy(gameObject);  // 이미 존재하면 새로 만든 객체를 삭제
        }
    }
}

- instance를 이용해 딱 하나만 존재하도록 설정

 

사용하는 이유

GameManager.instance.gameTime = 100;  // GameManager의 변수 변경
GameManager.instance.GameOver();  // GameManager의 함수 호출

- 언제 어디서든 쉽게 접근 가능

 

- new GameManager()처럼 새로운 객체를 만들 필요 없이 바로 사용 가능
- 전역 변수처럼 사용하지만, 하나의 인스턴스만 유지


 

Game Manager

게임의 전반적인 상태를 관리하는 핵심 스크립트로, 게임의 시작, 종료, 레벨업, 승리 및 패배 로직 등의 역할을 담당한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement; // 장면 관리를 사용하기 위해 네임 스페이스 추가

public class GameManager : MonoBehaviour
{
    public static GameManager instance;
    [Header("# Game Control")]
    public bool isLive; // 게임 진행 중 여부
    public float gameTime; // 현재 게임이 진행된 시간
    public float maxGameTime; // 최대 게임 시간
    [Header("# Player Info")]
    public int playerId; // 캐릭터 ID
    public float health; // 현재 체력
    public float maxHealth = 100; // 최대 체력
    public int level; // 현재 레벨
    public int kill; // 적 처치 수
    public int exp; // 현재 경험치
    public int[] nextExp = { 3, 5, 10, 100, 150, 210, 280, 360, 450, 600 }; // 각 레벨업에 필요한 경험치를 미리 저장한 배열
    [Header("# Game Object")]
    public PoolManager pool; // Object Pooling 시스템
    public Player player; // 플레이어 객체
    public LevelUp uiLevelUp; // 레벨업 UI
    public Result uiResult; // 게임 결과 UI
    public Transform uiJoy; // 조이스틱 UI
    public GameObject enemyCleaner; // 게임 승리 시 적 전부 제거

    void Awake()
    {
        instance = this; // 자기자신(this)을 instance로 할당하여 전역적으로 접근 가능하게 설정
        Application.targetFrameRate = 60; // 60 프레임으로 고정(기본 30)
    }

    // 게임 시작
    public void GameStart(int id)
    {
        playerId = id;
        health = maxHealth; // 체력을 최대치로 설정

        player.gameObject.SetActive(true);
        uiLevelUp.Select(playerId % 2);
        Resume();

        AudioManager.instance.PlayBgm(true);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Select);
    }

    // 게임 오버
    public void GameOver()
    {
        StartCoroutine(GameOverRoutine());
    }

    // 게임 오버 코루틴
    IEnumerator GameOverRoutine()
    {
        isLive = false; // 게임 종료

        yield return new WaitForSeconds(0.5f); // 0.5초 기다림

        // 패배 화면 표시 후 게임 멈춤
        uiResult.gameObject.SetActive(true);
        uiResult.Lose();
        Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Lose);
    }

    // 게임 승리
    public void GameVictory()
    {
        StartCoroutine(GameVictoryRoutine());
    }

    // 게임 승리 코루틴
    IEnumerator GameVictoryRoutine()
    {
        isLive = false; // 게임 종료
        enemyCleaner.SetActive(true); // 적 제거

        yield return new WaitForSeconds(0.5f);

        // 승리 화면 표시 후 게임 멈춤
        uiResult.gameObject.SetActive(true);
        uiResult.Win();
        Stop();

        AudioManager.instance.PlayBgm(false);
        AudioManager.instance.PlaySfx(AudioManager.Sfx.Win);
    }

    // 게임 다시 시작
    public void GameRetry()
    {
        /* SceneManager.LoadScene(0);이 게임 재시작이 되는 이유?
         * 씬의 index는 따로 설정하지 않아도 유니티가 자동으로 "Build Settings"에
         * 추가된 순서대로 정해지는데, 자동으로 0번 씬을 첫 번째 씬으로 지정
         
         * 씬 인덱스 확인 방법
         * 1) 유니티에서 "File" -> "Build Settings" 클릭
         * 2) Scenes In Build 목록에서 씬들이 표시됨
         * 3) 씬이 추가된 순서대로 0, 1, 2, ... index가 매겨짐 */
        SceneManager.LoadScene(0);
    }

    // 게임 종료
    public void GameQuit()
    {
        Application.Quit(); // 게임 종료
    }

    // 게임 시간 체크
    void Update()
    {
        if (!isLive)
            return;

        gameTime += Time.deltaTime; // gameTime을 증가시켜서

        if (gameTime > maxGameTime) // 최대 시간이 되면
        {
            gameTime = maxGameTime;
            GameVictory(); // 게임 승리 처리
        }
    }

    // 경험치 획득, 레벨업
    public void GetExp()
    {
        if (!isLive)
            return;

        exp++;

        // Mathf.Min() = 두 개의 숫자 중 더 작은 값을 반환하는 함수
        // 이 함수를 사용하여 10레벨 이후 최고 필요 경험치를 계속 사용하도록 변경
        if (exp == nextExp[Mathf.Min(level, nextExp.Length-1)]) { // 필요 경험치에 도달하면
            level++; // 레벨업
            exp = 0; // 초기화
            uiLevelUp.Show();
        }
    }

    // 게임 정지
    public void Stop()
    {
        isLive = false;
        Time.timeScale = 0; // 유니티의 시간 속도(배율), 기존값 1
        uiJoy.localScale = Vector3.zero; // 조이스틱 UI 비활성화
    }

    // 게임 재개
    public void Resume()
    {
        isLive = true;
        Time.timeScale = 1; // 게임 속도 정상화
        uiJoy.localScale = Vector3.one; // 조이스틱 UI 활성화
    }
}

 

Select.cs

public class Result : MonoBehaviour
{
    public GameObject[] titles;

    public void Lose()
    {
        titles[0].SetActive(true);
    }

    public void Win()
    {
        titles[1].SetActive(true);
    }
}

Audio Manager

게임 내에서 배경음과 효과음을 재생하거나 조작할 수 있도록 도와주는 오디오 매니저 스크립트로, 배경음(BGM)과 효과음(SFX)을 관리하는 역할을 한다.

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

public class AudioManager : MonoBehaviour
{
    public static AudioManager instance;

    // 배경음 관련
    [Header("# BGM")] // 인스펙터에서 구분선 추가
    public AudioClip bgmClip; // 배경음 오디오 파일
    public float bgmVolume; // 배경음 볼륨 크기
    AudioSource bgmPlayer; // 배경음 재생 오디오 소스
    AudioHighPassFilter bgmEffect; // 배경음의 특정 주파수를 걸러내는 효과

    // 효과음 관련
    [Header("# SFX")]
    public AudioClip[] sfxClips; // 효과음 오디오 파일 배열
    public float sfxVolume; // 효과음 볼륨 크기
    public int channels; // 다량의 효과음을 재생할 수 있도록 채널 수 설정(예: channels = 5면, 효과음 최대 5개까지 동시 재생 가능)
    AudioSource[] sfxPlayers; // 효과음 재생 오디오 소스 배열
    int channelIndex; // 효과음을 재생할 채널을 지정하는 변수

    /* enum(열거형)은 여러 효과음의 이름을 숫자로 지정해 관리할 수 있음
     * Dead = 0, Hit = 1, LevelUp = 3. Lose = 5 ...
     * 나중에 효과음 재생 시 (int)sfx를 사용해 숫자로 변환 후 배열 인덱스처럼 사용 가능 */
    public enum Sfx { Dead, Hit, LevelUp=3, Lose, Melee, Range=7, Select, Win }

    void Awake()
    {
        instance = this; // 싱글톤 패턴 적용
        Init();
    }

    // 오디오 시스템 초기화
    void Init()
    {
        // 배경음 플레이어 초기화
        GameObject bgmObject = new GameObject("BgmPlayer"); // 하이어라키에 빈 게임 오브젝트 새로 생성
        bgmObject.transform.parent = transform; // 부모를 AudioManager로 설정
        bgmPlayer = bgmObject.AddComponent<AudioSource>(); // 새 오디오 소스 컴포넌트 생성 후 bgmPlayer 오브젝트에 저장
        bgmPlayer.playOnAwake = false; // 게임 시작 시 자동 재생되지 않도록 설정
        bgmPlayer.loop = true; // 배경음 반복 재생 설정
        bgmPlayer.volume = bgmVolume;
        bgmPlayer.clip = bgmClip;
        // Camera.main = 씬에서 MainCamera 태그가 붙은 카메라를 가져오는 기능
        // GetComponent<AudioHighPassFilter>() = 오디오의 저음(낮은 주파수)을 걸러내는 필터
        bgmEffect = Camera.main.GetComponent<AudioHighPassFilter>(); // 메인 카메라에 있는 AudioHighPassFilter 컴포넌트를 가져와서 bgmEffect 변수에 저장

        // 효과음 플레이어 초기화
        GameObject sfxObject = new GameObject("SfxPlayer");
        sfxObject.transform.parent = transform;
        sfxPlayers = new AudioSource[channels];
        // channels 개수만큼 오디오 소스를 만들어 배열에 저장
        for (int index = 0; index < sfxPlayers.Length; index++) {
            sfxPlayers[index] = sfxObject.AddComponent<AudioSource>();
            sfxPlayers[index].playOnAwake = false;
            sfxPlayers[index].bypassListenerEffects = true;// 3D 효과를 무시하고 원래 음원 그대로 재생
            sfxPlayers[index].volume = sfxVolume;
        }
    }

    // 배경음 재생/중지
    public void PlayBgm(bool isPlay)
    {
        if (isPlay) {
            bgmPlayer.Play();
        }
        else {
            bgmPlayer.Stop();
        }
    }

    // 배경음 필터 효과 적용
    public void EffectBgm(bool isPlay)
    {
        bgmEffect.enabled = isPlay;
    }

    // 효과음 재생
    public void PlaySfx(Sfx sfx)
    {
        // 여러 효과음 중 사용하지 않는 채널을 찾아 재생
        for (int index = 0; index < sfxPlayers.Length; index++) { // 재생 가능한 총 채널 수만큼
            /* 채널을 순환하며 비어있는 채널을 찾도록 유도하는 계산식
             * channelIndex는 마지막으로 사용한 채널 index이므로 그 이후 채널부터 검사
             * % sfxPlayers.Length를 하면 배열 크기를 넘어서지 않고 순환할 수 있음
             * channelIndex = 3, sfxPlayers.Length = 5일 때
             * index = 0일 경우 : (0 + 3) % 5 = 3
             * index = 1일 경우 : (1 + 3) % 5 = 4
             * index = 2일 경우 : (2 + 3) % 5 = 0 */
            int loopIndex = (index + channelIndex) % sfxPlayers.Length; // 현재 검사할 오디오 채널의 index

            if (sfxPlayers[loopIndex].isPlaying) // 해당 채널에서 이미 효과음이 재생 중이라면
                continue; // 현재 반복을 건너뛰고 다음 루프로

            // 효과음이 2개 이상인 것은 랜덤 index를 더하기
            int ranIndex = 0;
            if (sfx == Sfx.Hit || sfx == Sfx.Melee) { // Hit 또는 Melee 효과음일 경우
                ranIndex = Random.Range(0, 2); // 랜덤값 반환(0 ~ 1)
            }

            channelIndex = loopIndex;
            // (int)sfx == 1(Hit) 또는 5(Melee)
            sfxPlayers[channelIndex].clip = sfxClips[(int)sfx + ranIndex]; // 랜덤 효과음 선택
            sfxPlayers[channelIndex].Play(); // 해당 랜덤 효과음 재생

            break;
        }
    }
}