Unity で WebGL を対象にビルドする場合、メモリは制作するコンテンツでできることを制限する原因になる可能性があります。そのため、このページでは WebGL でメモリがどのように使われるかについて説明していきます。
WebGL のコンテンツはブラウザ内で実行されます。そのため、メモリはすべてブラウザによってブラウザ内部のメモリ空間に割り当てられることになります。利用可能なメモリの総量は使用しているブラウザ, OS, デバイスによって大きく変動する可能性があります。決定要因は他にもブラウザが32ビット64ビットのどちらか、ブラウザがタブごとに分離したプロセスを使用するのか、それとも開かれているタブがすべて同一のメモリ空間を共有するのか、ブラウザの JavaScript エンジンがゲームのコードを変換するのにどれだけのメモリを要するか、というものがあります。
Unity WebGL コンテンツがブラウザに割り当てを求める重要なメモリ領域は以下のとおり複数あります。
これは Unity がすべての状況、管理されたオブジェクトやネイティブオブジェクト、現在読み込まれているアセットやシーンを保存するためのメモリです。他のプラットフォームで Unity プレイヤーが使用するメモリと似たようなものです。使用するメモリ量は Unity WebGL player settings から設定することができます(何度も書き出すのが嫌であれば、生成された HTML ファイルに記載されている TOTAL_MEMORY の値を編集することで同じ内容の操作をすることもできます)。 Unity Profiler を用いてこのメモリ量をサンプリングすることもできます。このメモリは JavaScript コードでバイト配列の TypedArray として生成され、このサイズのメモリが一続きの集まりとして割り当てできることを要求します。(メモリが断片化していてもブラウザが割り当てできるように)この空間はできるだけ小さくしたいかと思われますが、ゲームに含まれるすべてのシーンに属するデータを再生できるだけの大きさが必要です。
Unity WebGL のビルドを作成した際、 Unity はコンテンツに必要なすべてのシーンとアセットを持った .data ファイルを書き出します。WebGL には実際のファイルシステムがないため、このファイルはコンテンツの開始前にダウンロードされ、コンテンツが実行されている間はずっと非圧縮状態のデータがブラウザメモリの連続したブロックに保持され続けることになります。そのためダウンロード時間とメモリ使用量の削減を両立するには、このデータをできる限り少なくするよう心掛けるといいでしょう。アセットのビルドサイズを最適化する方法についての情報は Reducing File size にあるドキュメントページを参照してください。
読み込み時間とメモリ使用量を減らすために他にできることは、アセットデータを AssetBundle にまとめることです。そうすることでアセット読み込みのタイミングを完全に管理することができ、必要なくなったタイミングで破棄して使用されていたメモリを解放することもできます。 AssetBundle は直接 Unity ヒープに読み込まれ、ブラウザによる追加の割り当てが発生することはありません。( WWW.LoadFromCacheOrDownload を使用して AssetBundle をキャッシングする場合はこの限りではありません。ブラウザの IndexedDB にベイクされるメモリにマッピングされた仮想ファイルシステムを使用するためです。)
メモリに関係するもう1つの問題はブラウザの JavaScript エンジンに求められるメモリです。 Unity は何百万行という膨大な JavaScript コードファイルを発行しますが、これは通常ブラウザで扱われる JavaScript コード量よりも多いのです。 JavaScript エンジンによってはこのコードをパース、最適化するためにより大きなデータ構造体を割り当てる可能性もあり、それによってコンテンツ読み込み時にメモリスパイクが数ギガバイトに及ぶ、というケースもありえるのです。 WebAssembly などの将来的な技術が行く行くはこの問題を解決してくれることを期待していますが、そのときまでにできる最善のアドバイスは発行するコードサイズを少なくすること、です。サイズの削減についてのより詳しい情報と実践方法については こちらを参照してください。
Unity WebGL ビルドでメモリ関係のエラーが出た場合、ブラウザがメモリの割り当てに失敗しているのか、 Unity WebGL ランタイムが Unity ヒープの空いているブロックへ割り当てるのに失敗しているのかを理解することが重要です。ブラウザがメモリの割り当てに失敗している場合、(例えば Unity ヒープのサイズを削減するなど)上記のメモリエリアのサイズ削減を試みるといいかもしれません。一方で、 Unity ランタイムが Unity ヒープ内のブロックに割り当てるのに失敗している場合、逆にそのサイズを増やすといいかもしれません。
Unity はどちらに類するものかエラーメッセージの説明表示を試みます(そしてどうしたらいいかを提案します)。ブラウザごとにメッセージの表示方法が違うかもしれないため、表示が常に簡単というわけではなくすべてを説明できないかもしれません。 “Out of memory” エラーがブラウザで表示された場合、実行中のブラウザがメモリ不足の問題を抱えている可能性があります(この場合 Unity ヒープのサイズを少なくするといいかもしれません)。また、 Unity コンテンツを読み込んでいるときに可読エラーメッセージの表示などもなくブラウザがクラッシュすることがあるかもしれません。これには多くの理由が考えられますが、よくありうるものとしては JavaScript エンジンが生成されたコードをパース・最適化するために求められるメモリが多すぎることです。
サーバーはコンテンツの Large-Allocation http ヘッダーを生成することがあります。これはサポートされているブラウザー (現在 Firefox のみ) にメモリが必要としているものを伝え、分割されていないメモリ空間で新しい処理を発生させたり、大きなメモリ割り当てが成功するように雑多な作業を行ったりします。こうすることにより、特に 32 ビットブラウザーで Unity ヒープを割り当てようとするときに、ブラウザーがメモリ不足になる問題を解決できます。
Unity にマネージドオブジェクトを割り当てる場合、使われなくなったものはガベージコレクトされる必要があります。詳しい情報はドキュメントの automatic memory management を参照してください。 WebGL でも同じことが言えます。マネージドオブジェクト、ガベージコレクトされたメモリともに Unity ヒープ内に割り当てられます。
しかし、WebGL で1つ区別することがあります。ガベージコレクション(GC)が実行されるタイミングへの注意が必要なことです。ガベージコレクションを実行するために通常 GC はすべてのスレッドの実行を一時停止し、スタックを精査し読み込み済みオブジェクトの参照を登録します。これは現在の JavaScript では不可能なことです。このため WebGL で GC はスタックが空になった場合にのみ実行されます(現在これは毎フレーム後に1度行われます)。この仕様はほとんどのマネージドメモリを保守的に扱うコンテンツでは問題になりません。そして、相対的に多めの GC 割り当てがフレームごとに行われます(Unity プロファイラーを用いてデバッグすることができます)。
しかし、以下のようなコードを用いた場合は違います。
string hugeString = "";
for (int i = 0; i < 100000; i++)
{
hugeString += "foo";
}
このコードはループ中に String型オブジェクトで使用したメモリを解放するための GC を実行することができません。これが徐々に Unity ヒープのメモリ不足の原因になっていき WebGL での実行は失敗します。