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;
}
}
}
'Unity > Undead Survivor' 카테고리의 다른 글
로컬 저장소(PlayerPrefs) / 업적 시스템(Achievement System) (1) | 2025.03.19 |
---|---|
레벨업 시스템(LevelUp System) / 손과 무기 장착 표현(Hands) / 캐릭터 선택(Character Select) (0) | 2025.03.19 |
스크립터블 오브젝트(Scriptable Object) / 능력 업그레이드(Ability Upgrade) (0) | 2025.03.18 |
열거형(enum) / 월드 좌표(World Position)와 스크린 좌표(Screen Position) / HUD(Head-Up Display) (0) | 2025.03.18 |
범위 감지(Scanner) / 무기(Weapon)와 총알(Bullet) (0) | 2025.03.18 |