🚪

【Unity】エントリーポイントを用意しよう

2023/06/10に公開

はじめに

通常アプリケーション開発において、エントリーポイント(最初に呼び出される場所)は必要不可欠ですが、
Unityでは明確にエントリーポイントと呼ばれるものは存在していません。

各オブジェクトのAwakeで各々が初期化を行ったりするなど、何も気にせずに開発を進めていくとオブジェクト間の依存関係によってはAwakeの呼び出し順や、非同期処理によってうまく制御ができず、対処療法な処理が増えていくことがよくあります。

そのためアプリを開発する初期段階でエントリーポイントを準備しておくことは重要な要素となります。

エントリーポイント関数を準備

先程「明確にエントリーポイントと呼ばれるものが存在していません」と記載しましたが、幸い実装するための機能は存在しています。

それはRuntimeInitializeOnLoadMethodで、シーンにオブジェクトが存在していなくともアプリ起動時に呼び出される関数を作成できます。

AppManager.cs
using UnityEngine;

public class AppManager
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void OnBeforeSceneLoad()
    {
        // ここが最初に呼び出される
        Debug.Log("こんにちは");
    }
}

なにやら長ったらしい呪文が関数の上に付いていますが、とりあえずこれで最初に呼び出される関数を準備できます。
※staticをつけ忘れると呼び出されないので注意

引数にRuntimeInitializeLoadTypeがありますがこれは「どのタイミングで呼び出してほしいか?」を設定できるもので、いくつか種類が存在していますが、少しややこしいものもあるので深く知りたい方は別途検索してもらえればと思います。

今回はBeforeSceneLoadなのでその名の通り、シーンの起動前に呼び出されます。

シーンの起動を停止させる

さて、エントリーポイントが作成できましたが、ここから非同期処理で初期化をガシガシ書いていったとしても、後ろでは勝手にシーンが起動してAwakeが呼び出されたり、画面に中途半端な表示物が出てしまったりしてしまいます。

そのため、それらを対策する仕組みを作っていきましょう。

オブジェクトのAwake中にオブジェクトを非アクティブにすると、自身の子オブジェクトのAwakeが呼び出されなくなるという挙動をとるので、それを活用します。

SceneRoot.cs
using UnityEngine;
using UnityEngine.SceneManagement;

[DefaultExecutionOrder(-2000)]
public class SceneRoot : MonoBehaviour
{
    private void Awake()
    {
        gameObject.SetActive(false);
    }

    public void Activate()
    {
        gameObject.SetActive(true);
    }

    public static SceneRoot GetFromActiveScene()
    {
        return GetFromScene(SceneManager.GetActiveScene());
    }

    public static SceneRoot GetFromScene(Scene scene)
    {
        foreach (var go in scene.GetRootGameObjects())
        {
            if (go.TryGetComponent<SceneRoot>(out var sceneRoot))
                return sceneRoot;
        }
        return null;
    }
}

※DefaultExecutionOrderでスクリプトの呼び出し順序を早めているのがミソです

シーンのトップ階層にSceneRootというオブジェクトを作って上記コンポーネントをアタッチしておくと、SceneRootオブジェクトの子オブジェクトはAwakeが呼び出されず非表示で停止した状態になります。

そして、任意のタイミングでActivateを呼び出すことでシーンを起動することができるようになります。

初期化処理をつなげる

これで必要な役者は揃いました。初期化処理を完成させましょう。

※非同期処理を想定しているためUniTaskを使用します、PackageManager等でインポートしてください。

AppManager.cs
using Cysharp.Threading.Tasks;
using UnityEngine;

public class AppManager
{
    private static UniTaskCompletionSource afterSceneLoadCompletionSource = new UniTaskCompletionSource();

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void OnBeforeSceneLoad()
    {
        InitializeAsync().Forget();
    }
    
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    private static void OnAfterSceneLoad()
    {
        afterSceneLoadCompletionSource.TrySetResult();
    }

    private static async UniTask InitializeAsync()
    {
        // ここに色々初期化処理を書いていく
        // 例えば通信だったり、SDKの初期化だったり
        // 色々終わったらシーンを起動する
        // ※AfterSceneLoad後でないとオブジェクトが存在していないので待機する
        await afterSceneLoadCompletionSource.Task;
        SceneRoot.GetFromActiveScene()?.Activate();
    }
}

以上で最低限のエントリーポイントを作成することができました。

※AfterSceneLoad以降でないとSceneオブジェクトが存在せず、正常にアクセスできないため待機処理を追加しています。

シリアライズできるようにする

今のAppManagerはUnityのオブジェクトではなく、単なるクラスです。
そのため初期化に必要なプレハブなどのアセットをシリアライズできず、効果的な初期化ができないかもしれません。

実際にはクラスを分けたりしたほうが扱いやすいかもしれませんが、ここではAppManagerを直接編集します。

AppManager.cs
using Cysharp.Threading.Tasks;
using UnityEngine;

public class AppManager: MonoBehaviour
{
    // 必要な何かしらのプレハブやオブジェクト / アセットをシリアライズできる
    [SerializeField] private GameObject somePrefab;

    private static UniTaskCompletionSource afterSceneLoadCompletionSource = new UniTaskCompletionSource();

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void OnBeforeSceneLoad()
    {
        // Resourcesからプレハブをロードして直接シーンに生成する
        Instantiate(Resources.Load<AppManager>(nameof(AppManager)));
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    private static void OnAfterSceneLoad()
    {
        afterSceneLoadCompletionSource.TrySetResult();
    }

    private void Awake()
    {
        DontDestroyOnLoad(this); // シーン遷移しても消えないオブジェクトにする
        InitializeAsync().Forget();
    }

    private async UniTask InitializeAsync()
    {
        // ここに色々初期化処理を書いていく
        // 例えば通信だったり、SDKの初期化だったり
        // 色々終わったらシーンを起動する
        // ※AfterSceneLoad後でないとオブジェクトが存在していないので待機する
        await afterSceneLoadCompletionSource.Task;
        SceneRoot.GetFromActiveScene()?.Activate();
    }
}

AppManagerにMonoBehaviourを継承させることで、SerializeFieldを扱えるようになりました。

AppManagerをAddComponentしたオブジェクトをプレハブ化し、Resourcesフォルダに配置すると完成です。

これで必要なアセットやオブジェクト、設定データをシリアライズ変数を介して参照・設定できます。

例えばAppManagerの子階層にManagerクラスのオブジェクトを配置してシリアライズさせることで、管理クラスの初期化を手動で行ったりできるようになります。

Discussion