【Unity】プレハブのインスタンス生成が遅い・画面が止まる問題の対処法まとめ【パフォーマンス改善】

この記事は、
プレハブをInstantiateしてゲームオブジェクトを生成する際に、
遅い・画面が止まるといった問題に対して自分が行っているアプローチを書きます。

何が遅いのか原因が分かってない場合

「なんか遅いけど原因がプレハブかどうかわからない」場合は、まずはどこの処理が遅いかを特定しましょう。
特定に一番簡単なのは、遅いときに走る処理に現在日時を表示するデバッグログを仕組むことです。

Debug.Log(DateTime.Now + ":" + DateTime.Now.Millisecond);
//処理1
Debug.Log(DateTime.Now + ":" + DateTime.Now.Millisecond);
//処理2
Debug.Log(DateTime.Now + ":" + DateTime.Now.Millisecond);

(DateTime.Nowはミリ秒を表示しないのでDateTime.Now.Millisecondを追加しています)

これでどの処理に時間がかかっているかが特定できます。

「プレハブのインスタンス生成に時間がかかってる!」と特定できた場合はこのまま読み進めてください^^

インスタンス生成時ではなくプレハブのロードが遅いのかも

プレハブをInstantiateしてゲームオブジェクトを生成する、をもう少し細かく書くと、

・プレハブをロード
・プレハブをInstantiateしてゲームオブジェクトを作る

という二つのステップを踏むと思います。
まずはこのどちらかが遅いかを確認しましょう。

プレハブをロードするのが遅い場合

ソースからロードするのではなくあらかじめアタッチしておく

ロードするタイミングを自分でコントロールしなくてもいい場合、
あらかじめプレハブをアタッチしておくことで改善できる場合があります。(エディタにロードをまかせる)
これで解決するならこれが一番簡単かも。

f:id:snoopopo:20211205210436p:plain

public class TestPrefabCreater : MonoBehaviour
{
    public GameObject prefab;
}

非同期でロードする

ここでいう非同期とは、

1フレーム:プレハブをロード(1秒)→表示更新 ⇒1秒画面が固まる
だったものを、
1フレーム:プレハブの一部をロード(0.1秒)→表示更新 ⇒0.1秒しか画面が止まらない これを10フレーム繰り返す

ようなイメージです。非同期にすると複数フレームかけてプレハブ読み込むということ。

プレハブをResourcesフォルダにいれている場合、Resources#LoadAsync で非同期ロードを実現できます。
アセットバンドルを使っている場合はAssetBundle#LoadAssetAsyncがあります。

kan-kikuchi.hatenablog.com

例(複数ある場合)

「Test1~5」という名前の5つのプレハブのロードに時間がかかっているとします。

private void Load()
{
    for (int i = 1; i <= 5; i++)
    {
        GameObject obj = Resources.Load("Test" + i) as GameObject;
    }
}

これを非同期でロードするには Resources.LoadResources#LoadAsyncに置き換えます。

private IEnumerator LoadAsync()
{
    for (int i = 1; i <= 5; i++)
    {
        ResourceRequest resourceRequest2 = Resources.LoadAsync<GameObject>("Test" + i);
        while (!resourceRequest2.isDone)
        {
            yield return null; //まだロード終わってない
        }
        Debug.Log("Test" + i + "のロード終わった");
    }
    Debug.Log("全部のロード終わった");
}

これで、

Test1のロード開始→Test1のロード完了→Test2のロード開始→Test2のロード完了・・・

という風に処理されます。

       while (!resourceRequest2.isDone)
        {
            yield return null; //まだロード終わってない
        }

ロードがまだ終わっていない時にyield return null;を呼ぶことで、
ロードが終わるまで次のプレハブをロードしないようになります。
これがないと1フレームの内に5つ分のロードのリクエストが走ってしまうため、忘れないように注意!

インスタンス生成が遅い場合

インスタンス化するオブジェクトの数を減らす

たくさんのオブジェクトをインスタンス生成していて遅い場合、
本当にそのオブジェクトは生成する必要があるのかを改めて考えます。

画面に見えてる範囲だけ順次生成する

例えば、画面に表示されないものもインスタンス生成している場合は、
表示されるものの分だけ生成するということで一度に作る量を減らすことができます。

生成したオブジェクトを使いまわす

一つのプレハブを100個複製するようなゲームの場合、
画面に見えていないゲームオブジェクトを使いまわすということも考えられます。

プレハブ内を分けて非同期で順次インスタンス化する

1つのプレハブのインスタンス生成に時間がかかっている場合、
プレハブを分割してインスタンス生成することが検討できます。

▼worldプレハブ内のインスタンス生成が遅い… →子オブジェクトのworld1~6を非同期で順次生成しよう!
f:id:snoopopo:20211205234308p:plain

スクリプトで

worldプレハブ全体をインスタンス化すると遅いので、
worldプレハブの直下のゲームオブジェクト(world1~6)を非同期で順次生成します。

1フレ:world1→2フレ:world2→3フレ:world3 ・・・

となります。

修正前はworld1~6すべてを生成し終わるまで画面が止まってしまっていますが、
修正後は、world1が生成し終える時間しか止まらなくなります。

   private void Start()
    {
        GameObject worldGobj = new GameObject("world");
        StartCoroutine(InstanceChildAsync(worldPrefab, worldGobj));
    }

    public IEnumerator InstanceChildAsync(GameObject prefab, GameObject parent)
    {
        foreach (Transform prefabChild in prefab.transform)
        {
            Instantiate(prefabChild, parent.transform);
            yield return null;
        }
    }

tsubakit1.hateblo.jp

プレハブ自体を分割

上はスクリプトで、子オブジェクトを順次生成していますが、
予めプレハブ自体を分割しておくことでも実現できます。

▼worldプレハブ内のworld1~6をそれぞれ別のプレハブにしておき、順次生成する
f:id:snoopopo:20211205233840p:plain

画面が止まっている間「Loading・・・」を出して待ってもらう

パフォーマンス改善を直接するのではなく、
画面が止まっている間「Loading…」を出してユーザーに待ってもらうというのも全然あります!

f:id:snoopopo:20211205234851p:plain