[Unity] Awake, OnEnable의 호출 순서는 보장되지 않는다.

Running boy·2023년 8월 18일
0

유니티

목록 보기
2/9

내 프로젝트의 코드는 GameManager가 Awake 메서드에서 싱글톤 인스턴스를 생성하고, Player가 OnEnable에서 GameManager의 인스턴스에 접근하여 이벤트를 구독하도록 되어 있다.

// Singleton.cs
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    public static T instance { get; private set; }

    protected virtual void Awake()
    {
        if (instance != null)
        {
            Destroy(gameObject);
            Debug.LogError("Instance already created.");

            return;
        }

        if (this is T ins)
        {
            instance = ins;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Debug.LogError("Formal parameter error.");
        }
    }
}
// GameManager.cs
public class GameManager : Singleton<GameManager>
{
	// ...
    public event EventHandler onBeginDrag;
    public event EventHandler onDuringDrag;
    public event EventHandler onEndDrag;
    // ...
}
// Player.cs
public class Player : Entity, IPlayable
{
	// ...
	private void OnEnable()
    {
    	GameManager instance = GameManager.instance;
        instance.onBeginDrag += OnBeginDrag;
        instance.onDuringDrag += OnDuringDrag;
        instance.onEndDrag += OnEndDrag;
    }
    // ...
}

그런데 어느순간 잘 되던 코드가 갑자기 NullReferenceException을 뱉어대며 애를 먹어기 시작했다.
싱글톤 인스턴스가 초기화되지 않는 것이었다.
분명 유니티 메뉴얼에 따르면 MonoBehaviour의 라이프 사이클은 Awake - OnEnable - Start의 순서로 초기화되며 시작한다고 하고 나도 그렇게 알고 있었다.

흔히 하는 착각

여러 오브젝트들 사이에서도 Awake - OnEnable - Start의 순서가 지켜질거라고 생각한다.

예를 들어 Object1, Object2가 씬에 배치되어 있고 GameObject, Script의 active가 모두 활성화 상태라면 아래의 순서로 실행될 것이다.

Object1.Awake();
Object2.Awake();

Object1.OnEnable();
Object2.OnEnable();

Object1.Start();
Object2.Start();

하지만 실제로는 위 순서대로 호출되지 않는다.


진실

유니티 런타임은 이벤트 메서드를 프레임 단위로 호출한다.
문제는 한 프레임 내에서 오브젝트 호출 순서가 보장되지 않고, 이벤트를 묶어서 처리한다는 것이다.

실제로 Awake, OnEnable, Start는 아래의 순서로 호출된다.

Object1.Awake();
Object1.OnEnable();
Object2.Awake();
Object2.OnEnable();

Object1.Start();
Object2.Start();

Start 메서드는 Awake, OnEnable보다 늦게 호출되는 것이 무조건 보장된다.
하지만 Awake와 OnEnable의 경우 오브젝트별로 묶어서 처리하기 때문에 해당 메서드 내에서 다른 오브젝트에 접근할 경우 아직 초기화가 안되어 에러가 발생할 수 있다.

결과적으로 싱글톤 추상 클래스를 아래와 같이 수정하여 문제를 해결했다.

// Singleton.cs
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    public static T instance
    { 
        get
        {
            if (_instance == null)
            {
                GetInstance();
            }

            return _instance;
        }
    }
    protected static T _instance = null;
    protected static bool instantiated = false;

    protected bool destroyed = false;

    protected virtual void Awake()
    {
        if (destroyed)
            return;

        if (_instance == null)
        {
            SetInstance(this as T);
        }
        else if (_instance != this)
        {
            Debug.LogError("Instance already created.");
            destroyed = true;
            Destroy(gameObject);

            return;
        }
    }

    private static void SetInstance(T ins)
    {
        _instance = ins;
        instantiated = true;
        (ins as Singleton<T>).destroyed = false;
        DontDestroyOnLoad(ins);
    }

    private static void GetInstance()
    {
        var objs = FindObjectsOfType<T>();

        if (objs.Length == 0)
        {
            Debug.LogError($"Place the {typeof(T).Name} in the scene.");

            return;
        }
        else if (objs.Length > 1)
        {
            Debug.LogError($"The scene contains more than one {typeof(T).Name}. Unintended behavior can be detected.");
            for (int i = 1; i < objs.Length; i++)
            {
                (objs[i] as Singleton<T>).destroyed = true;
                Destroy(objs[i]);
            }
        }

        SetInstance(objs[0]);
    }
}

인스턴스 호출이 먼저인 경우 씬 내에서 해당 타입의 인스턴스를 가져오는 기능을 추가하였다.
또한 인스턴스를 탐색하는 과정에서 중복 검사를 수행하고 중복되는 오브젝트를 제거한다.
유일하게 씬에 오브젝트가 없는 경우에만 위 코드가 오작동을 하는데, 오브젝트가 없는 경우 리소스 폴더에서 프리팹을 불러오는 기능을 추가하면 될 것 같다.


추가

여러 오브젝트 사이에서 OnDisable, OnDestroy의 호출 순서 역시 보장되지 않는다고 한다.
두 메서드 역시 오브젝트당 한 번에 묶여서 호출된다.


참고 자료
Unity Documentation - 이벤트 함수의 실행 순서
Unity Forum - OnEnable before Awake??

profile
Runner's high를 목표로

0개의 댓글