사용자가 Application을 실행하는 동안 전역적으로 관리해야 하는 Context들이 있습니다. 예로 들어 Player의 Main Party에 포함되어 있는 Character들에 대한 정보나 현재 Application의 State가 대표적인 예시가 될 수 있을 것 같습니다.
Unity 내에서는 일반적으로 GameManager라는 Class를 구현하고, 해당 Class에 Context를 Attribute의 형태로 담거나, 혹은 GameManger가 관리하는 하위 Class들에 해당 Context들을 나누어 관리함으로서 해결합니다. 하지만 GameManager를 일반적인 Component를 구현하는 것 처럼 구현할 경우 몇 가지 문제를 유발할 수 있습니다.
GameManager로부터 유래한 Instance가 2개 이상일 경우 각각의 Instance들은 각자의 Context를 가질 수 있다는 문제가 발생합니다.GameManager는 Unity의 MonoBehaviour를 상속하기 때문에 GameManager가 생성되고 유지할 수 있는 환경을 만들어 주는 것이 필요합니다.이런 문제들을 해결하기 위해 Application의 Life Cycle 동안 단 하나만 존재하고 파괴되지 않는 특별한 환경을 GameManager, 필요한 경우 다른 Manager Instance들에게 제공하는 구조를 설계하려고 합니다.
앞서 언급한 바와 같이 GameManager로부터 유래한 Instance가 2개 이상일 경우 각각의 Instance들은 각자의 Context를 가질 수 있습니다.
GameManager Class로부터 유래한 Instance를 하나만 만들고, 두 번째 이후의 생성 시도는 생성하지 못하게 끔 조건을 나눔으로서 구현합니다.Instance를 통해 접근합니다.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를 상속할 수 없기 때문에 하술할 문제가 발생합니다.
Awake(), Start(), Update()를 사용할 수 없습니다.GameManager의 내부에서 Application의 Life Cycle Event Method를 구현해야 하는데 이를 구현하는 과정에서 오류가 발생할 가능성을 무시할 수 없습니다.MonoBehaviour Class를 상속할 수 없기 때문에 GameObject에 Component를 붙일 수 없습니다. 따라서 Static Class의 Attribute의 값을 Inspector 창에서 수정할 수 없습니다.따라서 GameManager를 Static Class로 구현하는 것 보다 Singleton Pattern을 이용해 한 번 GameManager가 Initialize된 후의 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가 게임 흐름을 시작합니다.
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);
}
}
}