はかせのラボ

私の頭の中を書いていく雑記ブログです

Unity シーン切替をプレハブを用いて約40倍速にする

あいさつ

どうも、はかせです。

あなたはシーン管理どうしていますか?
おそらくUnityユーザーならばSceneManagerクラスを使っていると思います。

ただUnityのシーン遷移って遅くないですか?
3Dオブジェクトとかゴリゴリに置いたシーンを読み込もうとすれば
数秒待つことも多いでしょう。

今回はそのシーン遷移時の待機時間を減らせる
Unityのプレハブを使ったテクニックをご紹介します。

シーン遷移が遅いのか?

あいさつで遅いですよねなんていいましたが、
今のUnityもシーン遷移は遅いんでしょうか?

とりあえず18000個ほど
キューブを置いたシーンの読み込みで
試してみます。
f:id:hakase0274:20190909231427p:plain

約3.5秒・・・・
速くはないですね。

なんでこんな遅いのかって言うと
理由は単純でシーン遷移のタイミングで
オブジェクトの破棄と読込が行われるからです。

どうやって速くする?

結論は単純で破棄と読込をしなければいいんです。
つまりキャッシュですね。
シーンに置くGameObjectなんかを全部プレハブにしてしまいます。

そしてそのプレハブのアクティブ操作で
シーンの切替を行います。

実装

やりたいことが決まったところで
さっそくコードに落としていきます。

using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

namespace HAKASE
{
    interface IScene
    {
        //シーンが始まった時の処理
        Task SceneEnter();
        //シーンが終わった時の処理
        Task SceneExit();
    }

    static class HakaseSceneManagercs
    {
        //シーンのリスト
        private static List<IScene> sSceneList = new List<IScene>();
        //現在アクティブなシーンインデックス
        private static int sSceneIndex = 0;

        //シーンを追加する
        public static void AddScene(GameObject scene)
        {
            var s = scene.GetComponent<IScene>();
            scene.SetActive(false);
            if (s != null) sSceneList.Add(s);
        }

        //インデックス指定でシーンを読み込む
        public static async Task<bool> LoadSceneByIndex(int index)
        {
            if (index >= sSceneList.Count) return false;
            await sSceneList[sSceneIndex].SceneExit();
            sSceneIndex = index;
            await sSceneList[sSceneIndex].SceneEnter();
            return true;
        }

        //インデックスが次のシーンを読み込む
        public static async Task LoadNextScene()
        {
            await sSceneList[sSceneIndex].SceneExit();
            sSceneIndex = (sSceneIndex + 1) % sSceneList.Count;
            await sSceneList[sSceneIndex].SceneEnter();
        }

        //インデックスが一個前のシーンを読み込む
        public static async Task LoadPrevScene()
        {
            await sSceneList[sSceneIndex].SceneExit();
            sSceneIndex = (sSceneIndex + sSceneList.Count - 1) % sSceneList.Count;
            await sSceneList[sSceneIndex].SceneEnter();
        }
    }
}

ISceneインターフェイスにシーンの開始と終了のメソッドを定義しています。
これはシーン切替なのでそれぞれ待ちたいなーということで定義しています。

基本的にISceneつけたコンポーネントが付いてる親の下に
必要なオブジェクトを置いていってプレハブ化。
その後プレハブを生成しAddSceneに放り込む。
後は勝手にぐるぐる切り替えてくれます。

このループでの切替は過去にC++でシーンを作った時に
説明していますので詳しくはそちらをどうぞ
hakase0274.hatenablog.com

さてシーンをプレハブに切り替えたら
一体何秒短縮されたんでしょうか?
f:id:hakase0274:20190909231417p:plain

0.09秒・・・!
約40分の1になりましたね。

シーンとプレハブどっち使う?

この記事でシーン切替ならば
プレハブにすることで爆速になることが証明できたと思います。

シーン単位での分担作業も
プレハブ単位に切り替えればいいだけなので
作業工程的にも大きく変わりません。

シーンと違って複数同時置きも楽ですし、
情報の共有も楽です。

そして何よりオブジェクトの寿命を完全に管理できます
知らんとこで消えてGC呼ばれてぷぎゃーとかいう事態は起こりません。

じゃあプレハブ切替一強かと言われるとそうではないと思います。

なぜならプレハブを使った方法は所詮やってることはキャッシュです。
つまり初期ロードが長くなります。

あと必要なオブジェクトを常にメモリに置いてる形になるので
メモリ量的にも決して優しくはありません

あとは純粋にチームメンバーがプレハブ式に馴染めるかという問題もあります。

あとがき

今回はプレハブを使った疑似的なシーンシステムの紹介でした。
私はこの方法を学校の同期から教わりました。

C++をやるまでは、
ちょっとめんどくさいなぁなんて思ってたんですが、
やってからはむしろこっちのが性に合うようになりました。
(メモリとか意識するようになったからかな)

先述しましたが、
この方法はあくまでキャッシュの応用みたいな仕組みであり、
切替は爆速になりますが、初回は遅いですし、メモリも結構食います。

さらに言えば3Dオブジェクトを同時に何万も置くなんて言う
割とありえにくい状況下での比較でもあります。
(ただ弾幕とか無双とか作ってると起こるのよ)

なのでそういった大量のオブジェクトを扱い、
シーン切替がボトルネックになりそうな場合にプレハブ式を使うのがいいかと思います。
(やっぱり銀の弾丸はないんだね)

それでは今回はこの辺でノシ