ひらしょー

平山尚が技術のことを書く場所。Unityが多そう。

アセットバンドルの使い方で悩む

ダウンロード待ちは嫌なものだ。

 

極力少量のダウンロードでゲームを始められるようにしたいし、

まとめてダウンロードしてほしいタイミングでは

極力高速にダウンロードを済ませてほしい。

 

それには、元々のデータが小さいのが理想だ。

ダウンロードも速いし、管理の負担も小さく、

そもそも必要もないものを作らなかったということで開発も安く済む。

技術的には何の工夫もいらない。そうあるべきだ!

しかし、それで済むような小さなゲームでないのなら、

何かしら工夫する必要がある。

 

Unityで起動後ダウンロードする手段といえば、アセットバンドルだ。

サーバに置いておいて、初回はネットで送ってクライアントに保存し、

以降はローカルからロードする。

スマホの場合昔のゲーム機と違って光ディスクのように

遅いわけではないので、ローカルにあればそこそこ

高速にロードできるだろう。

光ディスクはファイルのロード開始までに100ミリ秒

とかかかる悪夢のような機械だったし、HDDですら

10ミリ秒とかかるのでファイルが多いと遅かった。

まあスマホが実際に速いかは測ってはいないのだが。

モノにもよるだろうし。

 

さて、ダウンロードを速くするには、ファイル数は少ない方がいい。

ローカルからのロードでもそうなのだが、ファイルが多いと「ファイルごとに

かかるオーバーヘッド」の影響を強く受ける。

ましてネットならなおさらだ。

「このファイルくれよ」「はいどうぞ」というやりとりをサーバとするだけで

0.5秒とかいう時間がかかってしまう。

キロバイトの小さなファイルごとにそれをやるのは馬鹿馬鹿しい。

そういうわけで、ダウンロードのことだけ考えれば、

巨大なアセットバンドルが一個あるのが一番速いわけだ。

 

ところが一方、更新の問題がある。100MBのアセットバンドルの中身の

テクスチャを1枚だけ更新したとする。お客はまた100MBダウンロード

せねばならない。これはマズい。更新が発生しそうな所に関しては、

バラでダウンロードできる方が良い。ただしダウンロードそのものは遅くなる。

 

結局のところ、それらのバランスを取ってまとめる単位を決めることになり、

それには「代表的なケースでの実際の数字」がないと話にならない。

例えばファイルごとに余計にかかる時間が1ファイル1秒だとしよう。

また、転送速度は1MB/sだとする。

総量が1GBであれば、1ファイルなら1000秒かかる。

10ファイルに増えても、1000秒+10秒で、あんまり変わらない。

それなら更新のことも考えて10ファイルの方が良さそうだ。

そして100ファイルになると、1000秒+100秒で、ちょっと遅くなってくるが、

管理上それくらいに割りたいのであれば、まだ許容できるかもしれない。

ただ、1000ファイルともなると2000秒になり、それはちょっと辛いので、

ファイル数はそんなには増やせないな、という話になる。

実際にはダウンロードを並列させることでファイルごとのオーバーヘッドは

軽減できるのでそのへんも見ないといけないし、

コンテンツ追加や修正の頻度や量も加味する必要があるだろう。

 

さて、落とした後のことも考えないといけない。

 

落とした後はローカルからロードするのだが、もし、

「全部読み込まないと中身が取れない」とすると、

あまり大きな単位でまとめるとマズいことになる。

 

例えば、1GBに1000個の画像が入っているアセットバンドルがあって、

そのうちの1枚の画像が欲しいとしよう。

この時に、1GB読まないといけない状態では困る。

最初の画像を出すための待ち時間がえらいことになるし、

メモリ消費も大変なことになる。

もし音を入れておいた場合、鳴らしたくなってからロードしたのでは

間に合わないだろう。ボタンを押した時の音があまり遅れると辛いことになる。

 

アセットバンドルの場合、LZMAで圧縮しているとこういう状況に陥るようだ。

ビルド時に何も指定しないとそうなる。総容量は減るが、展開が遅いし、

部分だけ取り出すことができない。

もしダウンロード速度を最優先してLZMAにするのであれば、

アセットバンドルはあまり大きくしない方がいいし、

明らかに同時に使うとわかっているもの以外はまとめない方がいいのだろう。

試しに1GBのテキストアセットを入れたアセットバンドルにLoadFromFileを

かけた所、60秒もかかった。どう考えてもファイル全体を処理している。

 

一方、LZ4、つまりChunkBasedCompression入りで

アセットバンドルを作っていれば、

部分だけ取り出すことができるようだ。LoadFromFile(Async)では

目次だけを読み込んでファイル全体は読まず、

LoadAsset~で初めて実際にファイルから読み込むように見える。

であれば、どんなにアセットバンドルが大きくても問題ない。

仮に何もかもが1つのアセットバンドルに入っていたとしても、

その時に使うものだけを読み込める。ロード時間もメモリも無駄にならない。

同じく1GBのテキストアセットを入れたアセットバンドルを作ってみたが、

LZ4をかけておけば1秒でLoadFromFileが終わる。

ファイル全体を見ていたらありえない速度だ。

 

ゲームのデザインや素材の物量によっては、

「とにかくたくさんあって、いつどれが使われるか事前に予測し難い」

という状況はありうる。

例えばエフェクト類とか、短い音声とかだ。

操作や展開次第で「何を鳴らすか」「何を出すか」が違ってくる場合、

使い得る物を全部メモリに展開しておきたくはない。

使わずに終わるデータがあるなら無駄なロードをしたことになるし、

そもそも同時に全部メモリに置くことが許容できないかもしれない。

遅延が許容できる範囲で済むなら、使うとわかってからロードしたいわけだ。

しかし、ファイルがバラだとダウンロード時間が長くなる。

キロバイト量のファイルが数千個、みたいな状態は嫌だ。

もしそういう時に、「1ファイルに数千個入れておけるが、

ロードの時にはその中の必要なものだけを取り出せる」のであれば、

全部まとめて1ファイルにしても問題が出ない。

LZ4のアセットバンドルはそれを可能にしてくれるものと思われる。

昔はそれを可能にするためにファイルを結合するものを自作していたし、

「サーバにはzipして配置、クライアントで展開してアセットバンドルをバラで配置」

といった手も考えられるが、その必要はなさそうだ。

 

さて、もう一つ考えておかねばならないことがある。

「どのアセットがどのファイルに入っているかを誰が知るか」だ。

 

コードを書いている時に考えることは、

「charaAという名前のSpriteが欲しい」

とか、

「bgm_battleという名前のAudioClipが欲しい」

というようなことで、それがどのファイルに入っているかなんて

知りたくもない。

 

ファイルにまとめる単位は上記のように諸々の事情で決まるため、

コードを書いた後でまとめることだってあるだろう。

更新のことを考えて、まとめ方が後から変わることだってある。

であれば、コード中にファイル名は一切出て来ない方がいい。

アセット名だけで済むように仕掛けを作っておく必要がある。

 

しかし、当然ロードするにはファイル名がわからないといけない。

「○○というファイルの中の、××というアセット」と指定しないと

ロードできないからだ。

そこで、「××というアセットは○○というファイルにあるよ」

ということを知っているクラスをどこかに用意して、

そのクラスに「○○くれよ」と言うと、それが入っているファイルを

開けて出してくれる、という感じにする必要がある。

 

AssetBundleの場合、作成時にmanifestなるものが吐かれ、

そこには「何というアセットが入っているか」が書かれている。

これを使うのが良さそうだ。

加工してjsonなりにしてサーバから送信してもいいし、

これをそのままクライアントにダウンロードしてしまって、

落としてから加工して目録を作ってもいいだろう。

そのへんはやったことがないので何とも言えない。

しかし何かしらそういうものが必要になる。

 

ただ、ファイル名を指定したいこともある、というのは注意がいる。

例えばキャラごとにファイルがあって、

それぞれにfaceとかbodyとかいうスプライトが入っている、みたいな時だ。

読むファイルだけ換えて、後のコードは一緒にしたい。

virtualな関数みたいなものである。

なので、ファイル名の指定をできなくしてはいけない。

そもそもアセット名がグローバルに唯一、なんてことは保証し難いわけで、

同じ名前のアセットが複数あるケースはあるものと考えないといけない。

アセットの名前だけでロード要求された場合、それが複数あったらエラーを返す、

というようなことになるだろう。「最初に見つかった奴を返す」

とかだと絶対バグるだろうな。

 

以上まとめると、

 

- アセットバンドルのファイル数が少ないほどダウンロードは速いが、更新や管理と妥協しつつ作る単位を決める感じになるんだろう。

- LZ4(ChunkBasedCompression)でアセットバンドルを作れば一部だけ取り出せるので、同時にロードする確証がないものでもまとめて入れておける(っぽい)。

- 何がどのファイルに入ってるかを気にしないでロードできるようにしたい。

 

という感じだろうか。「一部だけ取り出し」が本当にそのように動作するかは、

実のところまだ確認してない。

1万ファイルくらい入れた巨大アセットバンドルから1個だけ取り出して、

プロファイラで測定する、ということを後でやっておく。

さすがに確認しないと怖くて使えない。

後でここに追記するか、別記事で書くと思う。