여러마리의 적 오브젝트, 여러 개의 UI 오브젝트처럼 프로젝트 내부에서 여러개를 인스턴싱해서 사용하는 클래스도 있지만, 특정한 시스템은 프로젝트 전반에 걸쳐 두개 이상으로 늘어나지 않는 것이 적합한 경우도 있다.
예를 들어 플레이어의 능력치라던가 게임의 스코어를 기록하는 시스템, 혹은 배경음악이나 효과음을 재생시켜주는 시스템, 게임의 진행상황을 저장해주는 역할의 시스템은 두 개 이상의 객체가 필요한 경우가 없지 않을까?
이런 시스템들은 다른 여러 시스템이나 모듈에서 공유될 필요가 있는 기능을 담당하는 클래스이기도 하며, 주로 정적 변수를 사용한 싱글톤 패턴으로 구현되어 하나의 인스턴스만 유지하도록 관리된다.
유니티의 내 대부분의 인스턴스는 Monobehaviour
를 상속받은 컴포넌트 단위로 이루어진다. 이 컴포넌트를 적절한 게임 오브젝트에 부착하여 내가 원하는 동작을 하도록 만들 수 있다.
Monobehaviour
가 부착된 유니티 컴포넌트는 유니티 엔진 이벤트 사이클에 영향을 받는다. 생성자나 초기화를 담당하는 Awake()
Start()
메서드나 소멸자인 OnDestory()
등 다양한 기능을 사용할 수 있다.
Monobehaviour
가 부착되어 있지 않은 플래인 클래스는 씬 내부 게임오브젝트에 부착할 수도 없고, 엔진의 특별한 기능과 이벤트들을 지원받을 수 없다.
이러한 특징이 있는 유니티에서 하나만 생성, 유지되는 싱글톤 클래스를 만드려면 어떤 방식이 좋을까.
using UnityEngine;
public class Singleton : MonoBehaviour
{
private static Singleton instance;
public static Singleton Instance
{
get
{
if (instance == null) // 인스턴스가 없으면
{
GameObject go = new GameObject("Singleton"); // 게임오브젝트 새로 만들어서
instance = go.AddComponent<Singleton>(); // 부착 해주세요
DontDestroyOnLoad(go);
}
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this; // 최초로 만들어진 게 인스턴스
DontDestroyOnLoad(gameObject);
}
else if (instance != this) // 최초 인스턴스가 아니면 다 부숴주세요
{
Destroy(gameObject);
}
}
}
필요 요건만 충족한다면 어떤 형식으로 만들어도 상관이 없겠다. 하나의 인스턴스만 유지를 보장하고 두개 이상이 될 경우에 대한 처리를 해주면 됨.
그러니까 프로젝트 규모에서 통일성과 코드 가독성을 위해 기본적인 기능을 제네릭 타입으로 만들어서 싱글톤이 필요한 스크립트엔 상속받아서 사용할 수 있게 하면 좋을 것 같다.
using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : Component
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
// 일단 찾아봐
instance = FindFirstObjectByType<T>();
if (instance == null)
{
// 없으면 새로 만들어서 넣어
GameObject 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)
{
Destroy(gameObject);
}
}
}
싱글톤이어도 씬 전환에서 파괴가 필요한 경우도 있을테니 멤버변수 isDestroyOnLoad
와 초기화 함수 Init()
을 통해 DontDestroyOnLoad()
의 호출 여부를 결정할 수 있도록 만들었다.
using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : Component
{
protected static T instance;
protected bool isDestroyOnLoad = false;
public static T Instance
{
get
{
if (instance == null)
{
// 일단 찾아봐
instance = FindFirstObjectByType<T>();
if (instance == null)
{
// 없으면 새로 만들어서 넣어
GameObject go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
(instance as Singleton<T>)?.Init(); // 초기화 메서드 호출
}
return instance;
}
}
protected virtual void Init()
{
if (!isDestroyOnLoad)
{
DontDestroyOnLoad(gameObject);
}
if (instance != this)
{
Destroy(gameObject);
}
}
}
사용할 땐 Init()
을 오버라이드 해주고 isDestroyOnLoad
를 먼저 설정해준 뒤 base.Init()
을 호출해주면 됨.
using UnityEngine;
public class Manager : Singleton<Manager>
{
protected override void Init()
{
isDestroyOnLoad = false;
base.Init();
}
}
미리 씬에 안만들어놓고 코드만 써도 잘 작동했다.
using UnityEngine;
public class Test : MonoBehaviour
{
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
Manager.Instance.Log();
Manager2.Instance.Log();
}
}
}
보통 싱글톤은 특정한 역할을 담당하는 매니저들이 사용하는데 매니저들이 유니티 이벤트 사이클에 얽메어있지 않아도 된다면 MonoBehaivour
를 상속받지 않고 플래인 클래스로 만들어서 클래스를 묶어줄 다른 클래스에서 정적 변수로 가지고 있어도 된다.
using UnityEngine;
public class Managers : MonoBehaviour
{
// 매니저 정적 변수
private static GameManager gameManager;
private static SoundManager soundManager;
public static GameManager Game
{
get
{
if (gameManager == null)
{
gameManager = new GameManager();
gameManager.Init();
}
return gameManager;
}
}
public static SoundManager Sound
{
get
{
if (soundManager == null)
{
soundManager = new SoundManager();
soundManager.Init();
}
return soundManager;
}
}
private void Awake()
{
DontDestroyOnLoad(gameObject);
}
}
이런식으로 Managers
에서 정적 변수로 전부 다 저장해놓으면 좀 더 가벼울듯.