본 게시글은 정답이 아닌 필자의 개인적인 의견입니다. 참고 바랍니다.
싱글톤은 보통 Awake
이벤트에서 인스턴스를 할당하게 되는데 다른 스크립트의 OnEnable
이벤트에서 싱글톤 레퍼런스를 참조하여 어떠한 동작을 하게 되는 경우 null
오류가 발생할 수 있다.
Unity에서는 모든 스크립트들의 Awake
이벤트가 호출된 다음 OnEnable
이벤트가 호출되는 방식이 아니라, 각각의 스크립트들에서 Awake
> OnEnable
이벤트가 순서대로 호출되는 구조이기 때문이다.
물론 OnEnable
대신 Start
이벤트에서 참조하도록 하면 당장 문제는 없겠지만, 오브젝트 풀링에 사용되는 경우나 이벤트 바인딩의 경우에는 OnEnable
에서 초기화를 하는 경우가 많으므로 본 게시글에서는 OnEnable
이벤트에서 다른 레퍼런스를 참조하는 경우를 가정하도록 하겠습니다.
또한, 다양한 스크립트나 게임 오브젝트들에서도 사용이 가능한 방법이지만 설명의 편의성을 위해 레퍼런스 대상은 싱글톤 매니저
로 두겠습니다.
위 문제의 핵심은 결국 참조 대상의 스크립트가 먼저 호출되어야한다는 점입니다.
여러 방법이 있겠지만 먼저 가장 좋은 방법이라고 생각하는 것부터 적어보겠습니다.
static 함수 위에 이 Attribute를 작성하게 되면 스크립트가 씬에 존재하지 않아도 무조건 한 번만 호출됩니다. 대신 씬 전환이 이루어져도 다시 호출되지 않습니다.
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
여기서 데이터 로딩
과 DontDestroyOnLoad를 적용시킬 싱글톤 매니저
들을 생성하면 됩니다.
해당 씬에서만 사용할 싱글톤 매니저
들의 경우 GameFlow
나 GameState
같은 이름의 클래스에서 수동 제어합니다. 여기서는 GameState
라고 하겠습니다.
방법은 간단합니다.
- 씬에 미리 배치된
싱글톤 매니저
나 게임 오브젝트들 중OnEnable
이벤트를 사용하는 것들은 전부 비활성화GameState
는 씬에 미리 배치 후 활성화 상태로 그대로 둡니다.GameState
의OnEnable
과OnDisable
이벤트에서 원하는 순서대로싱글톤 매니저
와 게임 오브젝트들을 활성화시킵니다. 즉, 레퍼런스 참조 대상들을 먼저 활성화시키고 그 다음에 레퍼런스를 사용하는 대상들을 활성화시키면 됩니다.
아래는 예시 코드입니다.
using System;
using UnityEditor;
using UnityEngine;
public abstract class GameState<T> : GenericMonoSingleton<T> where T : GameState<T>
{
// 씬 로드 전에 생성될 매니저 리스트
[SerializeField] GameObject m_ManagerGroup;
[SerializeField] EManagerClass[] m_ManagerClasses;
GameObject[] m_CreatedManagers;
// 추가된 순서대로 매니저 게임 오브젝트를 SetActive(true)
[SerializeField] GameObject[] m_GameObjectsToActivate;
protected abstract void CopyPlayerState();
protected abstract void UpdatePlayerState();
void CreateManagers()
{
m_CreatedManagers = new GameObject[m_ManagerClasses.Length];
var managerClasses = ManagerClassDictionary.Get(m_ManagerClasses);
for (int i = 0; i < m_ManagerClasses.Length; i++)
{
// 매니저 생성
var managerObject = new GameObject(managerClasses[i].Name);
managerObject.AddComponent(managerClasses[i]);
// 매니저 그룹이 설정되어 있다면 하위 계층으로 이동
if (m_ManagerGroup)
managerObject.transform.parent = m_ManagerGroup.transform;
// 레퍼런스 저장
m_CreatedManagers[i] = managerObject;
}
Debug.Log(GetType().Name + " > Create Managers");
}
void DestroyManagers()
{
var managerClasses = ManagerClassDictionary.Get(m_ManagerClasses);
// 생성된 순서의 반대로 파괴
for (int i = m_ManagerClasses.Length - 1; i >= 0; i--)
{
// 매니저 생성
var managerObject = new GameObject(managerClasses[i].Name);
managerObject.AddComponent(managerClasses[i]);
// 매니저 그룹이 설정되어 있다면 하위 계층으로 이동
if (m_ManagerGroup)
managerObject.transform.parent = m_ManagerGroup.transform;
}
Debug.Log(GetType().Name + " > Destroy Managers");
}
void ActivateGameObjects()
{
for (int i = 0; i < m_GameObjectsToActivate.Length; i++)
{
m_GameObjectsToActivate[i].SetActive(true);
}
Debug.Log(GetType().Name + " > Activate Game Objects");
}
void DeactivateGameObjects()
{
for (int i = m_GameObjectsToActivate.Length - 1; i >= 0; i--)
{
m_GameObjectsToActivate[i].SetActive(false);
}
Debug.Log(GetType().Name + " > Deactivate Game Objects");
}
protected virtual void OnEnable()
{
CopyPlayerState();
CreateManagers();
ActivateGameObjects();
}
protected virtual void OnDisable()
{
DeactivateGameObjects();
DestroyManagers();
UpdatePlayerState();
}
}
public enum EManagerClass
{
DataManager,
PlayerState,
PoolManager,
TimeManager
}
public class ManagerClassDictionary
{
static Dictionary<EManagerClass, Type> ManagerDict = new Dictionary<EManagerClass, Type>()
{
{ EManagerClass.DataManager, typeof(DataManager) },
{ EManagerClass.PlayerState, typeof(PlayerState) },
{ EManagerClass.PoolManager, typeof(PoolManager) },
{ EManagerClass.TimeManager, typeof(TimeManager) }
};
public static Type[] Get(EManagerClass[] _managerClasses)
{
Type[] managerClassList = new Type[_managerClasses.Length];
for (int i = 0; i < _managerClasses.Length; i++)
{
managerClassList[i] = ManagerDict[_managerClasses[i]];
}
return managerClassList;
}
}
공감하며 읽었습니다. 좋은 글 감사드립니다.