おもちゃラボ

Unityで遊びを作ってます

【Unity】プログレスバーを作るときにハマりがちな3つの誤り

Unityでプログレスバー(progressbar)を作るときは、uGUIのImageを使うことが多いと思います。Imageの設定をFillに設定することで簡単にプログレスバーのようなものを作ることができます。

f:id:nn_hokuson:20170405192003p:plain

スクリプトからプログレスバーを更新する場合は、次のようにImageオブジェクトのfillAmountの項目を0.0〜1.0の間で設定することで表示の割合を変えることができます。

GetComponent<Image>().fillAmount = 0.5;

やることはこれだけなのですが、重い処理に合わせてプログレスバーを進めようとするとなかなかうまく行きません。次の3つが失敗例。最後に成功例を載せています。

誤り実装① コルーチンを使う

IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        yield return null;
    }
}

void HeavyProcess()
{
    StartCoroutine(Progress());
    
    //以下重たい処理
    // ・・・・
}

重たい処理をはじめる前にコルーチンを生成し、そちらでなんちゃってアニメーションしようという案です。

Unityのコルーチンはスレッドっぽくてスレッドではない(並列動作しない)ため、この実装は正常に動きません。重たい処理に引きずられて、Progressコルーチンは重たい処理が終わったあとにようやく実行されます。

誤り実装② 処理の間に挟み込む

void HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

コルーチンがだめなら、直接重たい処理の中にプログレスバーの更新プログラムを挟み込めばいいじゃん案です。

なかなか良さそうですが、残念ながらこれも正常には動作せず、いきなりプログレスバーの進捗が100%になってしまいます。

これは、fillAmountに値を代入したあとも重たい処理が続くため、uGUIの画面更新が行われないのが原因です。当たり前といえば当たり前か・・・・

誤り実装③ Threadを使う

private Thread _thread;
IEnumerator Progress()
{
    for(int i = 0; i <= 100; i++){
        GetComponent<Image>().fillAmount = i / 100.0f;
        Thread.Sleep(10);
    }
}

void HeavyProcess()
{
    _thread = new Thread(Progress());
    _thread.Start();
    
    //以下重たい処理
    // ・・・・
}

コルーチンがだめなら非同期スレッドだ、案です。そもそもUnityの設計思想がシングルスレッドなので、Threadを使う時点でイケてないのですが・・・Threadの中ではuGUIの更新ができずにエラーが出ます。

ちゃんとプログレスバーが伸びる実装

IEnumerator HeavyProcess()
{
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.2f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.4f;
    yield return null;
    //超重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 0.9f;
    yield return null;
    //重たい処理
    //・・・・
    GetComponent<Image>().fillAmount = 1.0f;
}

void Hoge()
{
    StartCoroutine(HeavyProcess());
}

さて、いよいよ正しく動く実装です。結局何をしたかというと、コルーチンと挟み込みの折衷案です。重たい関数自体をコルーチンにして、プログレスバーをすすめるたびに、一旦処理をメインスレッドに返します。これにより描画処理が実行され、プログレスバーも進む、という仕組みです。

これにて一件落着!?

f:id:nn_hokuson:20170405192018g:plain