Unity나 UE4 같은 게임 `엔진`에서, 혹은 대규모 프레임워크를 도입, 활용할 때 Singleton 패턴은 유용하긴 하지만, Life-Cycle과 어긋나 골을 울리는 주범으로 꼽힐 때가 제법 있습니다. 그 상황에서 활용가능한 4가지 Singleton 패턴의 변형을 알아봅니다.
1. Game Instance를 구성하고, Main Scene에 사전에 배치하여, 스크립트 컴포넌트를 Static Member에 올린 후 그 객체에 필요한 Singleton 클래스들을 MonoBehaviour 컴포넌트로 줄줄이 매달아 놓는 방법.
* 이 구현은 여러개의 Scene을 Additive로 로드하여 게임을 구성할 때 유효한 구현입니다.
* 단, Editor 상에서 애셋, 데이터들을 셋팅해줘야 하는 경우엔 이 방법은 적절치 못할 수 있습니다.
GameInstance.cs
using UnityEngine;
public class GameInstance : MonoBehaviour
{
private static object m_PadLock = new object();
private static bool m_Exiting = false;
private static GameInstance m_Instance = null;
/// <summary>
/// 게임 인스턴스에 접근합니다.
/// </summary>
public static GameInstance instance
{
get
{
lock(m_PadLock)
{
if (m_Exiting || m_Instance != null)
return m_Instance;
GameObject gameObject = new GameObject();
gameObject.name = "FWK_GameInstance";
return (m_Instance = gameObject.AddComponent<GameInstance>());
}
}
}
/// <summary>
/// 게임 싱글턴 객체를 획득합니다.
/// </summary>
/// <typeparam name="SingletonType"></typeparam>
/// <returns></returns>
public static SingletonType GetGameSingleton<SingletonType>()
where SingletonType : GameSingleton<SingletonType>
{
lock (m_PadLock)
{
SingletonType singleton = instance != null ?
instance.GetComponent<SingletonType>() : null;
if (singleton != null || instance == null)
return singleton;
return instance.gameObject.AddComponent<SingletonType>();
}
}
/// <summary>
/// 초기화 시에, quitting 이벤트를 등록합니다.
/// </summary>
[RuntimeInitializeOnLoadMethod]
static void Init()
{
Application.quitting += () =>
{
lock (m_PadLock)
m_Exiting = true;
};
}
private void Awake()
{
lock(m_PadLock)
{
if (m_Instance == null)
m_Instance = this;
else if (m_Instance != this &&
m_Instance.GetType() == typeof(GameInstance))
{
if (m_Instance.gameObject != gameObject)
Destroy(m_Instance.gameObject);
else Destroy(m_Instance);
m_Instance = this;
}
else
{
Debug.LogWarning("GameInstance: valid instance already registered.");
Destroy(this);
}
}
}
private void OnDestroy()
{
lock (m_PadLock)
{
if (m_Instance != this)
return;
m_Instance = null;
}
}
}
GameSingleton.cs
using UnityEngine;
public class GameSingleton<SelfType> : MonoBehaviour
where SelfType : GameSingleton<SelfType>
{
/// <summary>
/// 인스턴스에 접근합니다.
/// </summary>
public static SelfType instance => GameInstance.GetGameSingleton<SelfType>();
}
2. 개별 Singleton 객체를 사전에 Scene에 배치, Static Member에 올려두고 사용하는 방법.
* 이 구현은 여러개의 Scene을 Additive로 로드하여 게임을 구성할 때 유효한 구현입니다.
* Editor상에서 애셋, 데이터들을 셋팅해줘야 하는 경우엔 이 방법이 적절합니다.
PrelocatedSingleton.cs
using UnityEngine;
public class PrelocatedSingleton<SelfType> : MonoBehaviour
where SelfType : PrelocatedSingleton<SelfType>
{
private static object m_PadLock = new object();
public static SelfType instance { get; private set; }
private void Awake()
{
lock (m_PadLock)
{
if (instance != null)
{
Debug.LogError("PrelocatedSingleton: multiple instances are prelocated!");
Destroy(this);
return;
}
instance = this as SelfType;
}
}
private void OnDestroy()
{
lock (m_PadLock)
{
if (instance != this)
return;
instance = null;
}
}
}
3. Unity 라이프 사이클 중 OnEnable/OnDisable/OnDestroy를 이용, 인스턴스 스위칭이 가능한 Singleton (정확히는 Singleton이 아니고 Semi-Singleton? 쯤 됨). 즉, 정규 Singleton 패턴과는 거리가 있음.
* Additive로 Game-Mode가 서로 다른 Scene간의 Async Switching이 필요할 때 적절한 구현입니다.
SwitchableSingleton.cs
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 다수의 인스턴스가 Prelocated 될 수 있지만,
/// 가장 마지막에 활성화된 인스턴스만 Publish 되는 싱글턴입니다.
/// </summary>
/// <typeparam name="SelfType"></typeparam>
public class SwitchableSingleton<SelfType> : MonoBehaviour
where SelfType: SwitchableSingleton<SelfType>
{
private static List<SelfType> m_Instances = new List<SelfType>();
/// <summary>
/// 인스턴스를 획득합니다.
/// </summary>
public static SelfType instance
{
get
{
lock (m_Instances)
{
return m_Instances.Count > 0 ?
m_Instances[m_Instances.Count - 1]: null;
}
}
}
private void OnEnable()
{
lock (m_Instances)
{
SelfType oldInstance = instance;
m_Instances.Remove(this as SelfType);
m_Instances.Add(this as SelfType);
if (oldInstance != this &&
oldInstance != null)
{
if (oldInstance.enabled)
oldInstance.enabled = false;
}
}
}
private void OnDisable()
{
lock (m_Instances)
{
if (instance == this)
{
m_Instances.Remove(this as SelfType);
if (m_Instances.Count > 0)
{
m_Instances.Insert(m_Instances.Count - 1, this as SelfType);
if (!m_Instances[m_Instances.Count - 1].enabled)
m_Instances[m_Instances.Count - 1].enabled = true;
}
}
}
}
private void OnDestroy()
{
lock (m_Instances)
{
bool wasBackground = instance != this;
m_Instances.Remove(this as SelfType);
if (wasBackground && m_Instances.Count > 0 &&
!m_Instances[m_Instances.Count - 1].enabled)
{
m_Instances[m_Instances.Count - 1].enabled = true;
}
}
}
}
4. 순수 Singleton. (라이프 사이클 이벤트를 추적할 수 없음)
* Editor Intergration이 불필요한 순수한 Singleton 구현이 필요할 때 적절합니다.
Singleton.cs
using System;
public class Singleton<T>
where T : Singleton<T>
{
private static T m_Instance;
private static object m_PadLock = new object();
public static T instance
{
get
{
lock (m_PadLock)
{
if (m_Instance != null)
return m_Instance;
m_Instance = (T)typeof(T)
.GetConstructor(Type.EmptyTypes)
.Invoke(new object[0]);
return m_Instance;
}
}
}
protected Singleton()
{
}
}
'Anything' 카테고리의 다른 글
오늘따라 UEFI가 말썽을... (0) | 2018.01.20 |
---|---|
이더넷 디버그쉘 만들기 (0) | 2017.11.04 |
Raspberry Pi 3 최초 설정하기! (0) | 2017.11.02 |
리눅스 민트에 카카오톡 설치하기 + 자동화 스크립트 (8) | 2017.08.06 |
구글 크롬 설치 후 PGP 키 에러가 발생하는 경우 (0) | 2017.08.05 |