KoreanFoodie's Study

유니티 Monobehaviour 내장 함수 호출 순서 및 주의점 본문

Game Dev/Unity : Study

유니티 Monobehaviour 내장 함수 호출 순서 및 주의점

GoldGiver 2022. 7. 24. 21:39

Udemy 관련 개념 정리 및 Dev Log 를 기록하고 있습니다!

Monobehaviour 

 

유니티에는 Monobehaviour 라는 클래스가 있다. 씬에서 GameObject 로 사용하는 클래스는 Monobehaviour 를 상속하게 되는데, Monobehaviour 에는 다양한 내장 메소드들이 있다.

Awake, Start 처럼 초기화에 쓰이는 녀석도 있고, OnEnable 이나 OnDisable 처럼 해당 게임 오브젝트를 껐다 킬때 ( SetActive( ) 함수 호출 혹은 .enabled 값 변경 ) 호출되는 함수들도 있다. 

중요한 것은, 어떤 함수가 어떤 순서로 호출되는지를 알아야 한다는 것이다!

이 링크에 정리된 사진을 가져온 것이다.

 

특히 주의해야 할 사항으로는, Spawner 클래스에서 생성하는 오브젝트들의 내장 함수 호출 순서가 있다. 예를 들어, Enemy 들을 생성하는 EnemySpawner 클래스가 있다고 가정하겠다.

말만으로는 전달이 어려우니 코드를 보자.

while (!NalyuGameManager.instance.isGameOver)
{
    yield return new WaitForSeconds(_spawnInterval);

    if (_poolingObjectQueue.Count > 0)
    {
        NalyuEnemy enemy = _poolingObjectQueue.Dequeue();
        enemy.gameObject.SetActive(false);
        enemy.gameObject.SetActive(true);

        // 이게 있어야 enemy 에서 Start() 가 제대로 호출된 값이
        // 아래 enemy.shouldFlip 값으로 제대로 들어옴 (한 틱 차이)
        // 왜냐하면 아까 Instantiation 을 할 때, 바로 SetActive(false) 를 호출해서
        // enemy 의 Start() 함수가 호출되지 않았었기 때문
        yield return null;
        enemy.currentState = NalyuEnemy.State.Walking;

        if (enemy.shouldFlip)
        {
            enemy.transform.position = new Vector2(_leftSpawnLocation, _fixedPlatformHeight);
        }
        else
        {
            enemy.transform.position = new Vector2(_rightSpawnLocation, _fixedPlatformHeight);
        }

        enemy.OnEnemySpawned();
        StartCoroutine(ReturnEnemy(enemy));
    }
}

위 코드는 while 문에서 게임 오버가 아닐 경우 _spawnInterval 만큼을 대기하면서 계속 큐에서 Enemy 를 꺼내는 함수이다.

그런데, enemy.gameObject.SetActive(true) 를 함으로써 enemy 오브젝트는 Start( ) 가 호출되지만, 문제는 해당 코드가 실행되는 프레임이 아닌, '다음 틱에' 호출되게 된다. 따라서, enemy.shouldFlip 이라는 값을 체크하여 분류를 하고 싶은데, shouldFlip 을 설정하는 코드가 enemy 클래스의 Start( ) 에 들어있다면, yield return null 을 써서 한 틱을 기다려주지 않으면 enemy 의 Start( ) 가 호출되지 않은 값 (즉, 쓰레기 값이다) 을 체크하게 된다. 이렇게 되면 우리가 원하는 대로 동작하지 않을 것이다.

이를 방지하기 위해, Quick Fix 로 yield return null 을 주어 한 프레임을 기다리게 만듦으로써, enemy 내의 Start( ) 함수가 기다릴때까지 한 틱을 기다리고, enemy 클래스에서 Start( ) 가 호출되어 shouldFlip 값이 제대로 들어왔으므로 이제 비로소 enemy.shouldFlip 값을 체크 할 수 있게 만들었다.

사실 이는 위에 올린 링크의 다음 부분을 참조하면 그 이유를 파악할 수 있다.

게임플레이 도중 Instantiate 한 오브젝트의 경우, Start 를 생성시 호출되게 Enforce 할 수 없다는 것. 따라서 임시 방편으로 일단 한 프레임을 대기하도록 만든 것이다.

그리고 이는 Awake 와 OnEnable 함수도 동일하다.

(마지막 .. cannot be enforced when you instantiate an object during gameplay 줄을 보면 된다)

 

이 외에도, Awake 호출 후에 Start 가 호출된다는 것, 특히 Awake 나 Start 의 경우 어떤 클래스의 Awake 나 Start 가 먼저 호출되는지 알 수 없다는 점이다.

따라서, A 클래스에서 초기화한 값을 B 클래스에서 받아와야 한다면, A 클래스에서는 Awake 에서 초기화를 진행하고, B 클래스에서는 Start 를 통해 A 클래스의 값들을 받아와야 한다. 만약 둘 다 Awake 에 있다면... 미정의 동작이 발생할 것이다(보통은 각 자료형의 기본값 내지는 null 값이 들어갈 테니, 제대로 된 동작을 안한다고 보면 된다).

A 와 B 가 동시에 생성된다면, A 의 Awake 에서 설정한 값을 B 의 Start 함수에서 받아오는 것은 안전하다. (물론 A 가 B 보다 늦게 만들어진다거나 하면 안된다. 당연히!)

 

더 끔찍한 일은, 이런 버그는 '논리적으로는' 이상이 없어 보이므로, 함수 호출 순서를 생각하지 않는 경우 시간을 엄청 날릴 수 있다는 것이다. 

그러니 MonoBehaviour 의 내장 함수를 쓰기 전에는, 함수 호출 순서와 조건들을 생각하면서 코딩하도록 하자.

 

Comments