본문 바로가기

Anything

Unity C# Singleton 구현 4가지.

 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()
    {

    }
}