[Unity] 오브젝트 및 스크립트 로딩 순서 제어

Eu4ng·2023년 8월 16일
0

Unity

목록 보기
1/1

본 게시글은 정답이 아닌 필자의 개인적인 의견입니다. 참고 바랍니다.

문제 상황

싱글톤은 보통 Awake 이벤트에서 인스턴스를 할당하게 되는데 다른 스크립트의 OnEnable 이벤트에서 싱글톤 레퍼런스를 참조하여 어떠한 동작을 하게 되는 경우 null 오류가 발생할 수 있다.

Unity에서는 모든 스크립트들의 Awake 이벤트가 호출된 다음 OnEnable 이벤트가 호출되는 방식이 아니라, 각각의 스크립트들에서 Awake > OnEnable 이벤트가 순서대로 호출되는 구조이기 때문이다.

물론 OnEnable 대신 Start 이벤트에서 참조하도록 하면 당장 문제는 없겠지만, 오브젝트 풀링에 사용되는 경우나 이벤트 바인딩의 경우에는 OnEnable에서 초기화를 하는 경우가 많으므로 본 게시글에서는 OnEnable 이벤트에서 다른 레퍼런스를 참조하는 경우를 가정하도록 하겠습니다.

또한, 다양한 스크립트나 게임 오브젝트들에서도 사용이 가능한 방법이지만 설명의 편의성을 위해 레퍼런스 대상은 싱글톤 매니저로 두겠습니다.

해결 방안

위 문제의 핵심은 결국 참조 대상의 스크립트가 먼저 호출되어야한다는 점입니다.
여러 방법이 있겠지만 먼저 가장 좋은 방법이라고 생각하는 것부터 적어보겠습니다.

1.RuntimeInitializeOnLoadMethod

static 함수 위에 이 Attribute를 작성하게 되면 스크립트가 씬에 존재하지 않아도 무조건 한 번만 호출됩니다. 대신 씬 전환이 이루어져도 다시 호출되지 않습니다.

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]

여기서 데이터 로딩과 DontDestroyOnLoad를 적용시킬 싱글톤 매니저들을 생성하면 됩니다.

2. GameState

해당 씬에서만 사용할 싱글톤 매니저들의 경우 GameFlowGameState같은 이름의 클래스에서 수동 제어합니다. 여기서는 GameState라고 하겠습니다.

방법은 간단합니다.

  1. 씬에 미리 배치된 싱글톤 매니저나 게임 오브젝트들 중 OnEnable이벤트를 사용하는 것들은 전부 비활성화
  2. GameState는 씬에 미리 배치 후 활성화 상태로 그대로 둡니다.
  3. GameStateOnEnableOnDisable 이벤트에서 원하는 순서대로 싱글톤 매니저와 게임 오브젝트들을 활성화시킵니다. 즉, 레퍼런스 참조 대상들을 먼저 활성화시키고 그 다음에 레퍼런스를 사용하는 대상들을 활성화시키면 됩니다.

아래는 예시 코드입니다.

GameState.cs

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();
    }
}

ManagerClassDictionary.cs

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;
    }
}

참고 링크

profile
초보 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 16일

공감하며 읽었습니다. 좋은 글 감사드립니다.

답글 달기