Unity5でLoadLevelAsyncでシーンロードしてisDoneがtrueにならないケースにはまった

タイトルではUnity5って書いたけど、別にUnity5からの問題ではなくUnity4でも起こってた様子。

Unity5になって、Proでしか使えなかったApplication.LoadLevelAsyncもせっかく使えるし、試しに使ってみたらはまりました。
Application.LoadLevelAsyncの戻り値はAsyncOperationクラスのインスタンスで、AsyncOperation.allowSceneActivation = falseしておくと、シーンロードが終わっても遷移はしないという事ができます。
なのでロードだけしておいて、任意のタイミングでAsyncOperation.allowSceneActivation = trueして遷移させるという、シーンの先読みみたいな事が出来るらしい。
というのを見かけて試してみたら、なぜか遷移しない。あれこれ試してみるとAsyncOperation.allowSceneActivation = falseしなければ遷移するという事に気がつきました。

ユニティちゃんファイティングモーションを使う縛りのゲームジャムに参加してきました | kanonjiのブログ

ちなみに、先日参加したゲームジャムで、いつもはゲームジャム自体のと技術的な話のとで2つ書いてたけど、今回はあまり技術的な面で書く事ない感じだったので、1個のエントリーに詰め気味に書いてました。
と思ったら、そういえばこれで割とはまったのを思い出したので書いてます。

回避策

AsyncOperation.isDone will never be true if AsyncOperation.allowSceneActivation is false. Progress will stop at around 0.9f. You can work around this to do whatever needs to be done before the final load takes place by waiting until progress has reached 0.9f, and end with allowing the scene activation again after you've done what you need to prior to that. Example:
http://answers.unity3d.com/questions/934354/loadlevelasync-stops-at-90.html

AsyncOperation.allowSceneActivation = falseしてるとAsyncOperation.isDoneはtrueにならず、AsyncOperation.progressも0.9fで止まってしまうらしい。

using UnityEngine;
using System.Collections;

public class LoadLevel : MonoBehaviour {
    public string level;
    private string progress = "";    // Name of scene to load.
    private bool isLoading = false;
    private bool doneLoading = false;
    private bool allowLoading = false;
    private void OnGUI() {
        GUILayout.BeginVertical("box");
        if (!isLoading) {
            if (GUILayout.Button ("Begin Load")) {
                isLoading = true;
                StartCoroutine(LoadRoutine());
            }
        } else {
            if (doneLoading) {
                if (GUILayout.Button ("Actually Load")) {
                    allowLoading = true;
                    StartCoroutine(LoadRoutine());
                }
            }
            GUILayout.Label(progress);
        }

        GUILayout.EndVertical();
    }
    private IEnumerator LoadRoutine() {
        AsyncOperation op = Application.LoadLevelAsync(level);
        op.allowSceneActivation = false;
        while (op.progress < 0.9f) {
            // Report progress etc.
            progress = "Progress: " + op.progress.ToString();
            yield return null;
        }
        // Show the UI button to actually start loaded level
        doneLoading = true;
        while(!allowLoading) {
            // Wait for allow button to be pushed.
            progress = "Progress: " + op.progress.ToString();
            yield return null;
        }
        // Allow the activation of the scene again.
        op.allowSceneActivation = true;
    }
}

http://answers.unity3d.com/questions/934354/loadlevelasync-stops-at-90.html

理由はわからないけど、0.9fまでは進む挙動をするらしい。
という事で、サンプルコードはちょっと長めだけど、要するにprogress < 0.9fの間はロード中と判断するというworkaroundです。
必ず0.9fまでは進んでくれるのかわからないけど、ゲームジャム中に書いたコードでは、確かに遷移出来るようになりました。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[DisallowMultipleComponent]
public class SceneManager : SingletonMonoBehaviour<SceneManager> {
    Dictionary<string, AsyncOperation> asyncs = new Dictionary<string, AsyncOperation> { };

    protected void Awake() {
        if (this != Instance) {
            Destroy(gameObject);
            return;
        }
        DontDestroyOnLoad(this);
    }

    public void MoveToTitle(float wait = 0) {
        string sceneName = "Title";
        StartCoroutine(MoveTo(sceneName, wait));
        InitWorld();
    }

    public void MoveToResult(float wait = 0) {
        string sceneName = "Result";
        StartCoroutine(MoveTo(sceneName, wait));
    }

    public void MoveToGame(float wait = 0) {
        string sceneName = "stage1";
        StartCoroutine(MoveTo(sceneName, wait));
    }

    public void LoadAsync(string sceneName) {
        StartCoroutine(LoadingAsync(sceneName));
    }

    private IEnumerator LoadingAsync(string sceneName) {
        AsyncOperation async = Application.LoadLevelAsync(sceneName);
        async.allowSceneActivation = false;
        asyncs[sceneName] = async;
        while (async.progress < 0.9f) {
            yield return new WaitForEndOfFrame();
        }
    }

    private IEnumerator MoveTo(string sceneName, float wait = 0) {
        if(! asyncs.ContainsKey(sceneName) || asyncs[sceneName].isDone){
            yield return StartCoroutine(LoadingAsync(sceneName));
        }
        yield return new WaitForSeconds(wait);
        asyncs[sceneName].allowSceneActivation = true;
    }
}

一部省略してるけど、ゲームジャムで書いたコードでは、シーンマネージャーにこんなメソッドを作りました。
LoadAsync()で先読みしておいてMoveToで遷移。先読みしてなかったらロードして遷移。という動きを想定してます。
ただ、ゲームジャムでは実際先読みできてるかどうか検証してないので、もしかしたら毎回ロードしてるかも?

読み込み終わって即遷移なら

using UnityEngine;
using System.Collections;

public class ExampleClass : MonoBehaviour {
    IEnumerator Start() {
        AsyncOperation async = Application.LoadLevelAsync("MyBigLevel");
        yield return async;
        Debug.Log("Loading complete");
    }
}

http://docs.unity3d.com/ScriptReference/Application.LoadLevelAsync.html

最初は、公式ドキュメントのコードのコピペから書き始めて、なんかネットにはwhileしてる記事ちらほらあるけど、上記のようにyield return async;だけで良いっぽいし、Unity5でシンプルになったのかなとか思ってました。
実際、シーンロード終わったら即遷移で良いなら、確かにこれで動くんだけど。
せっかく非同期なんだし、先読みしておいて必要になったら即遷移とか、やってみたかったので手を出してはまった形です。

Unity4でも

Unity3DのLoadLevelAsyncとAsyncOperationで非同期ロードを待ってみる – Qiita

例えばこのエントリーだとUnity 4.3.7p3を使ってて、やっぱりao.progress < 0.9fで判別してます。
他にも、問題を認識してから改めて検索してみると、Unity5に限らずこの挙動について書いたエントリーがちらほらありました。

環境

環境 バージョン
Unity 5.1.2f1

書いた日

2015年7月28日頃

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>