Unity / C#
Beginner
Unity 2021.3+ / Unity 6
씬 전환에도 파괴되지 않는 제네릭 영속 싱글톤(Persistent Singleton) 패턴
DontDestroyOnLoad와 지연 초기화, 씬 종료 시 고스트 오브젝트 생성을 원천 차단하는 스레드 세이프 유니티 C# 제네릭 싱글톤 템플릿.
#Unity #C# #Design Pattern #Architecture #Optimization
핵심 개념유니티에서 싱글톤을 순진하게 구현하면 게임 종료 시 파괴 순서 문제로 인해 씬에 유령(Ghost) 인스턴스가 생성되는 문제와 씬 전환 시 중복 인스턴스가 살아남아 데이터가 꼬이는 문제가 발생합니다. 종료 감지 플래그와
DontDestroyOnLoad를 상속 단계에서 캡슐화한 제네릭 기반 싱글톤으로 이를 해결합니다.1. 문제 정의: 일반적인 구현의 치명적 결함
- 고스트 인스턴스 버그: 에디터 플레이를 중단하거나 씬을 나갈 때, 다른 객체의
OnDestroy()에서 싱글톤을 참조하면 이미 파괴된 싱글톤이 재호출되면서new GameObject가 씬에 영구 잔류합니다. - 중복 오브젝트 누적: 씬 A에서 DontDestroyOnLoad 된 매니저가 존재하는 상태에서 씬 A로 다시 로드될 때 새로 스폰된 매니저가 파괴되지 않고 중복 공존하여 이벤트 중복 호출이 발생합니다.
2. 검증된 제네릭 싱글톤 구현
using UnityEngine;
/// <summary>
/// 씬이 바뀌어도 유지되며, 종료 시 고스트 오브젝트를 생성하지 않는 범용 싱글톤 베이스 클래스입니다.
/// </summary>
public abstract class PersistentSingleton<T> : MonoBehaviour where T : Component
{
private static T _instance;
private static bool _isApplicationQuitting = false;
private static readonly object _lock = new object();
public static T Instance
{
get
{
if (_isApplicationQuitting)
{
Debug.LogWarning($"[PersistentSingleton] 애플리케이션 종료 중 '{typeof(T).Name}'에 접근하여 null을 반환합니다.");
return null;
}
lock (_lock)
{
if (_instance == null)
{
_instance = FindAnyObjectByType<T>();
if (_instance == null)
{
var go = new GameObject($"[{typeof(T).Name}]");
_instance = go.AddComponent<T>();
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
}
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Debug.LogWarning($"[PersistentSingleton] 중복 생성된 '{typeof(T).Name}' 인스턴스를 파괴합니다.");
Destroy(gameObject);
}
}
protected virtual void OnApplicationQuit()
{
_isApplicationQuitting = true;
}
protected virtual void OnDestroy()
{
if (_instance == this)
{
_instance = null;
}
}
}
3. 실무 사용 예제
using UnityEngine;
public class SoundManager : PersistentSingleton<SoundManager>
{
[SerializeField] private AudioSource bgmSource;
public void PlayBGM(AudioClip clip)
{
if (bgmSource == null) return;
bgmSource.clip = clip;
bgmSource.Play();
}
}
// 호출처:
// SoundManager.Instance.PlayBGM(introMusic);
4. 실무 주의점 및 가이드
- DontDestroyOnLoad 루트 제약:
DontDestroyOnLoad는 반드시 최상위(Root) GameObject에만 적용됩니다. 부모 Transform이 있는 자식 오브젝트에 이 스크립트를 붙이면 유니티 엔진 경고가 발생하므로transform.SetParent(null);을 사전에 호출해야 합니다. - 도메인 리로드 비활성화 대응: Unity 에디터의 'Enter Play Mode Settings'에서 Domain Reload를 껐다면
static _instance = null;,_isApplicationQuitting = false;를[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]에서 리셋해주어야 합니다.