본문 바로가기

Unity

MonoBehaviour의 초기화는 어떻게 해야 하는가?

Awake로 충분하지 않나요?

MonoBehaviour의 초기화 메소드로는 유니티에서 Awake, Start 등을 지원하고 있다.

일반적으로 초기화는 Awake에서 하게 될 것이다.

using UnityEngine;

public class Something : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("awake.");
    }
}

 

잘 동작한다.

 

그런데 문서에는 강조되지 않는부분이지만, 생성 당시 GameObject가 비활성화 상태일때는 Awake가 호출되지 않는다. 나중에 활성화되는 시점에 Awake가 호출된다.

 

using UnityEngine;

public class SomethingParent : MonoBehaviour
{
    public Something something;
    void Start()
    {
        var newObject = Instantiate(something, transform);
    }
}
public class Something : MonoBehaviour
{
    private void Awake()
    {
        Debug.Log("awake.");	// Something의 초기상태가 비활성화시 호출되지 않음.
    }
}

 

 

이런 경우엔 Awake가 호출되지 않는다. 어떻게 해야 할까?

 

일단 Something이 항상 활성화상태면 좋겠지만, 그렇지 못 할 경우가 있다. 타 파트에서 작업된 프리팹의 경우에 항상 코드에 알맞게 올거라는 보장이 없을 수도 있다. 특히 UI작업 등에 이런 일을 많이 겪는다.

 

그래서 다른 여러가지 방법을 생각해 볼 수 있다.


생성자 사용

먼저 MonoBehaviour도 엄연히 C# class이므로 생성 시 생성자가 호출된다. MonoBehaviour에서 생성자를 사용하면 안 된다고 알고 있는 경우가 흔한데, 반드시 그렇지는 않다. 유니티와 관련 없는 순수 C# 메소드는 얼마든지 사용 가능하다. 

using UnityEngine;

public class Something : MonoBehaviour
{
    private Something()
    {
        Debug.Log("constructor.");
    }
    private void Awake()
    {
        Debug.Log("awake.");
    }
}

유니티의 오브젝트는 기본적으로 C++로 구현되어 있으며, C#은 C++ 객체를 랩핑하는 용도이다. 하지만 C#의 생성자가 호출되는 시점에서는 MonoBehaviour의 C++ 부분이 생성되어 있지 않은 상태이다. 따라서 생성자에서는 gameObject를 비롯한 모든 유니티 오브젝트를 사용할 수 없다. 초기화 시점에 gameObject와 관련 없는 순수한 C# 코드만 써도 괜찮은 경우는 많지 않을 것이다.


투박하지만 확실한 방법

다른 방법으로는 "무식"하게 해결하는 방법도 있다. 모든 상황을 커버할 수 있도록 초기화 코드를 매 메소드마다 집어넣는 것이다. 

 

using UnityEngine;

public class Something : MonoBehaviour
{
    private bool _initialized = false;
    public void Initialize()
    {
        if (_initialized) return;
        Debug.Log("initialize.");
    }
    private void Awake()
    {
        Initialize();
        Debug.Log("awake.");
    }

    private void MethodA()
    {
        Initialize();
        // 어떤 작업..
    }
    private void MethodB()
    {
        Initialize();
        // 다른 작업...
    }
}

 

우아하지는 않지만, 오브젝트가 초기에 활성화가 되어 있든 아니든, 초기화 메소드를 호출하든 호출하는것을 잊어버리든, 어떤 상황에서도 문제없이 작동하는 부분에 가치가 있는 코드이다. 단점으로는 중복된 코드가 많고 초기화 방식이 숨겨져 있어서 처음 보는 사람은 초기화가 어떤 경로로 이루어지는지 쉽게 알아보기 어렵다는 점이다. 


"초기화 해야 함" 인터페이스 정의

좀 더 명시적으로 하고 싶다면, 이런 식의 방법도 고려해 볼 수 있다.

 

using UnityEngine;

public class SomethingParent : MonoBehaviour
{
    public Something something;
    void Start()
    {
        var newObject = Instantiate(something, transform);
        foreach (var comp in newObject.GetComponentsInChildren<IInitializable>())
            comp.Initialize();
    }
}

public interface IInitializable
{
    void Initialize();

}
public class Something : MonoBehaviour, IInitializable
{
    void IInitializable.Initialize()
    {
        Debug.Log("initialize.");
    }
}

 

Awake등 유니티의 메시지에 의존하지 않고 순수하게 사용자 코드로 초기화 과정을 호출하는 방식이다. 이 방식의 장점은 언제 어떻게 초기화가 되는지 명시적으로 보이고, 초기화가 되는 시점도 완벽하게 컨트롤 할 수 있다는 것이다. 단점은 오브젝트를 생성할 때마다 올바른 초기화 코드를 호출해 줘야 한다는 점이다. 이 단점은 Instantiate나 Resources.Load등을 직접 호출하는 대신 초기화 코드까지 같이 들어있는 헬퍼 메소드를 호출하는 방법 등으로 어느정도 회피할 수 있다.


문제 회피

상황에 따라서는 아예 문제 자체를 회피하는 방법도 있다.

 

이 시나리오에서는 Something 컴포넌트가 직접 붙어있는 오브젝트는 항상 켜져 있다고 가정하고, Something 오브젝트 밑에는 Content 오브젝트 단 한개만이 있다고 가정한다. 프리팹 구조를 이런 식으로 강제하고 비활성화를 하고 싶으면 Content만 켰다껐다 하는 방식이다.

 

이 방식은 하이어라키가 쓸데없이 복잡해지고 구조가 강제된다는 점이 단점이지만, Something이 "꺼져" 있는 상태지만 코루틴을 실행할 수 있는 등 오브젝트가 활성화 된 것 자체에서 오는 이득이 있기 때문에, 편하고 쉽게 만들려면 고려해 볼 수 있는 선택지이다.

 

기타 초기화를 확실하게 하는 여러 방법이 있겠지만, 기본적으로는 위에 나열한 방식들에서 크게 벗어나지 않을 것이다. 프로젝트의 상황에 맞춰 적절한 방식으로 사용하면 된다.