Edited at
UnityDay 9

Unity3Dで、体感ロード時間を減らす

More than 5 years have passed since last update.

Unityはロードが長くなりやすいので、それを何とかする方法について。

一応、1シーンで全てのゲームを管理しResource.Loadでリソース管理を行う手法もある。パフォーマンス的に言えばそっちのほうが良いが、今回は別のアプローチを提案する。

何故別のアプローチが必要かというと、単純にこの手法で組むのが大変だからだ。というか、よく事故ると聞く。なので、多少パフォーマンスが劣化する可能性があっても楽な方法も模索してみた。

持論だが、難しくてすごい事をするより、簡単に楽に確実に動くほうが重要かなと。まあ状況によるところではあるが。

ちなみにこの記事は、この記事の発展みたいなもの。

ロードの待ち時間を短くする(その1)

ロードの待ち時間を短くする(その2)

ロードの待ち時間を短くする(その3/Resources.Load)

ロードの待ち時間を短くする(その4/Application.LoadLevelAdditive)

ロードの待ち時間を短くする(その5)リソースのキャッシュ


今回の手法について

この手法は基本的にUnityのキャッシュ機能をそのまま利用する形で行う。

この手法は「ゲーム全体のロード短縮」とか、「シーン間の移動を短くする」のには向いてると思うが、カードゲームの「美しいカード1枚」をロードしたりするのには向いてない。


リソースのキャッシュを利用する

利用したのは「別シーンに移動する際に、以前のシーンと同じリソースを使用している場合は、リソースの読み込みを行わない」といったルール。

ロードの待ち時間を短くする(その5)リソースのキャッシュ

実はコレ、「前のシーン」にあるリソース全てに適応されるルールなので、例えばDontDestroyのオブジェクト以下にマテリアルの参照を残しておいた場合でも有効。

マテリアルがキャッシュされれば次回以降のロードがほぼ無くなる。

(シーンロード時のオーバーヘッドはある)

イメージ

シーン1からシーン3はロード時間があるが、シーン2からシーン3はほぼロード無し。

で、思いついたのが登録しておきたいリソースを登録したプレハブを作成しておいて、ResourceManagerオブジェクトの子に登録するといった手法。

そして、リソースを先行キャッシュしておいて、ロード時間を0にしてしまう方法。


具体的な手順

具体的な手順は、こんな感じ。


手順1:

GameObjectを作成し、シーンが利用する「テクスチャ」やら「モデル」の参照を登録しておく。NGUIならば、Atlasでも突っ込んでおく。コードはこんな感じのを利用。

C#:ResourceRef.cs

public class ResourceRef : MonoBehaviour {

  [SerializeField] private Texture[] textures = null;

  [SerializeField] private GameObject[] models = null;

}



イメージ

イメージではTextureを大量に登録しているが、本当は3DモデルとかMaterialの単位で登録したほうが楽だと思う。もしくは登録するプレハブを生成する時にリソースマネージャーに自分から登録する仕組みとか。


手順2:

手順1で作成したオブジェクトを含むシーンをロードした時、ResourceManagerオブジェクトの子になるように移動する(既にある場合は破棄)。ResourceManagerはDontDestroy設定で、どこのシーンにも存在するユニークなGameObject。

C#:ResourceRef.cs

void Start()

{

transform.parent = ResourceManager.Instance.transform;

}



イメージ


これで、各シーン遷移時にリソースのロードがある程度簡略化し、ロードの高速化が期待できる。管理も、GameObjectがシーンにあるだけで良いので、だいぶシンプルだと思う。

ちなみに、リソースが不要になった場合は、LoadLevelを呼ぶ直前に手順1で作ったGameObjectを破棄してやればいい。そうすれば、シーン読み込み時に綺麗にしてくれる。

どのシーンでオブジェクトを破棄するか辺りは、WBSみたいな一覧図で管理してやると良いと思う。コードは「どのオブジェクトを破棄」ではなく「この一覧以外は破棄」がわかりやすいと思う。この機能はResourceManagerオブジェクトにでも渡しておくとスッキリ。


非同期の読み込み

Pro専用の話になるが、実はこの手法は非同期でリソースを読み込むこともできる。

概念的には「先行でリソースをキャッシュしておき、ロードを短縮」といったイメージ。

具体的にはLoadLevelAdditiveAsyncを使って「GameObjectのみ登録したシーン」をロードしておく形で実装する。

当然、この手法はシーンで余計なメモリを食う形になる。なので、先読みしておきたい最低限のリソースをロードしておく形がベストかもしれない。

またAsync系は別スレッドの並行作業する形になるので、マルチコア以外の端末にはちと荷が重いかもしれない。

ロードの待ち時間を短くする(その4/Application.LoadLevelAdditive)

とは言え、ゲームのインターバルやタイトル画面やデータの通信中など、CPU負荷がそれほど高くないタイミングで行う機会があるなら、この手法は割と良い方法だと思う。

少し分かりにくいのでサンプルを用意した。非同期ロードのサンプル

下のボタンは非同期読み込みボタンで、上のボタンは画面遷移ボタン。

ボタンを押して黒くなる時間がロード時間といった感じ。

最初から上野ボタンを押すのと、下のボタンを押して「1」になった後に上野ボタンを押すのでは速度が大分違う事がわかると思う。


カードや細かいアイテムの管理

この手法だが、カードやらアイテムの管理には向いてない。理由は簡単で、シーン読み込み自体のオーバーヘッドがあるからだ。

そしてシーンに全てのカードやアイテムのリソースを含めるとロードが超長くなる上に(2年前のモバイル端末や一部Androidは)メモリ不足になる可能性があるので、必要最低限を読む形がベストだと思う。

なのでカードゲームみたいな膨大な量のリソースを管理する場合、お金がないならResource.LoadやStreamingAssets、お金があるならAssetBundleを使うと良いと思う。読んだら即破棄が簡単だし、ピンポイントで指定の画像を読むにはこっちが向いてる。

StreamingAssetsはAndroid下では非同期で動かせるし、ファイルサイズを少し減らせるので悪くない選択肢。少なくともRGBA32をバカスカ使いたがるデザイナーが居たり、RGBA32を使わざるえない状況の場合は、(ファイルをpng形式で保持できるので)アプリサイズやクォリティ的にも悪くないと思う。

ただAndroid/iOS共にファイルが丸見えだったり、iOSで動かす場合、ロード直後に凄まじいCPU負荷が入ったり、大量にファイルを投入すると最初のロード(Unityロゴが出るやつ)が異常に伸びたりするので注意。