배경

사용자가 Application을 실행하는 동안 전역적으로 관리해야 하는 Context들이 있습니다. 예로 들어 Player의 Main Party에 포함되어 있는 Character들에 대한 정보나 현재 Application의 State가 대표적인 예시가 될 수 있을 것 같습니다.

Unity 내에서는 일반적으로 GameManager라는 Class를 구현하고, 해당 Class에 Context를 Attribute의 형태로 담거나, 혹은 GameManger가 관리하는 하위 Class들에 해당 Context들을 나누어 관리함으로서 해결합니다. 하지만 GameManager를 일반적인 Component를 구현하는 것 처럼 구현할 경우 몇 가지 문제를 유발할 수 있습니다.

  1. GameManager로부터 유래한 Instance가 2개 이상일 경우 각각의 Instance들은 각자의 Context를 가질 수 있다는 문제가 발생합니다.
  2. 설계할 때 GameManager는 Unity의 MonoBehaviour를 상속하기 때문에 GameManager가 생성되고 유지할 수 있는 환경을 만들어 주는 것이 필요합니다.

이런 문제들을 해결하기 위해 Application의 Life Cycle 동안 단 하나만 존재하고 파괴되지 않는 특별한 환경을 GameManager, 필요한 경우 다른 Manager Instance들에게 제공하는 구조를 설계하려고 합니다.

구현

Singleton Pattern을 통한 유일성 보장

앞서 언급한 바와 같이 GameManager로부터 유래한 Instance가 2개 이상일 경우 각각의 Instance들은 각자의 Context를 가질 수 있습니다.

public class GameManager: MonoBehaviour {
	public static GameManager Instance {
		get => instance;
	}
	
	private static GameManager instance = null;
	
	private void Awake() {
		if (instance == null) {
			instance = this;
			DontDestroyOnLoad(this.gameObject);
		}
		else {
			Destroy(gameObject);
		}
	}
}

GameManager를 Static Class로 선언하지 않은 이유

GameManager의 역할을 생각하면 GameManager를 Static Class로 선언하더라도 큰 문제가 없어 보입니다. Static Class일 경우 굳이 GameManager 내부의 Static Field인 GameManager#Instance를 통해 Attribute를 접근할 필요가 없어 오히려 더 나은 것 처럼 느껴집니다. 하지만 GameManager가 Static Class일 경우 Static Class가 아닌 MonoBehaviour를 상속할 수 없기 때문에 하술할 문제가 발생합니다.

  1. Unity Lifecycle을 활용할 수 없습니다.
  2. Inspector 연동이 불가능합니다.

따라서 GameManager를 Static Class로 구현하는 것 보다 Singleton Pattern을 이용해 한 번 GameManager가 Initialize된 후의 Context를 잃어버리지 않도록 구현하였습니다.

Application의 초기 Context 설정

GameManager를 Singleton Pattern을 이용해 구현하였지만 이 GameManager의 Context는 게임이 시작되지마자 자동적으로 Initailize가 일어나지 않습니다. Static Class가 아니기 때문에 최초의 Instance를 생성해주는 작업이 수반되어야 합니다.

따라서 최초의 GameManager로부터 유래한 Instance의 생성을 안정적으로 보장하기 위해 최초 환경을 설정하는 역할만 담당하는 @init Scene을 선언합니다. 그 후 해당 Scene에 핵심 System Manager를 Component로 가지는 GameObject를 배치하고 초기 설정이 되어 있지 않을 경우 해당 Scene을 Load함으로서 Application의 관점에서 초기 Context를 마련할 수 있습니다.

Load된 후 @init Scene 내에 존재하는 Component들을 DontDestroyOnLoad Scene으로 옮겨 Application이 실행되는 동안 파괴되지 않도록 합니다.

이 Logic은 GameManager과 별개인 GameInitializer라는 Application의 초기 Context를 설정해주는 Class에 구현합니다.

sequenceDiagram
    participant Application
    participant GameInitializer
    participant GameManager

    Application->>GameInitializer: SceneLoadComplete()

    alt GameManager == null (매니저 없음)
        GameInitializer->>Application: LoadSceneAsync("@Init", Additive)
        note right of Application: @Init Scene이 추가로 로드됩니다.
        
        Application-->>GameManager: Create & Initialize
        activate GameManager
        
        GameInitializer->>Application: UnloadSceneAsync("@Init")
        note right of Application: @Init Scene이 제거됩니다.        
    end
        GameInitializer->>GameManager: BeginGameFlow()
        note left of GameManager: GameManager가 게임 흐름을 시작합니다.

Property를 이용한 Method의 실행 타이밍의 보장

RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad) Property를 이용해 Scene Load가 완료된 직후 자동으로 실행하도록 보장하고자 합니다.

using UnityEngine;
using UnityEngine.SceneManagement;

public class GameInitializer
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    public static void Initialize()
    {
        if (GameManager.Instance == null)
        {
            Debug.LogWarning("GameManager가 존재하지 않습니다. @Init Scene을 로드합니다.");

            SceneManager.LoadScene("@Init", LoadSceneMode.Additive);
        }
    }
}