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 には実際のファイルシステムがないため、このファイルはコンテンツの開始前にダウンロードされ、コンテンツが実行されている間はずっと非圧縮状態のデータがブラウザーメモリの連続したブロックに保持され続けることになります。そのためダウンロード時間とメモリ使用量の削減を両立するには、このデータをできる限り少なくするよう心掛けるといいでしょう。アセットのビルドサイズを最適化する方法についての情報は ファイルサイズの削減 を参照してください。
読み込み時間とメモリ使用量を減らすために他にできることは、アセットデータを アセットバンドル にまとめることです。そうすることでアセット読み込みのタイミングを完全に管理することができ、必要なくなったタイミングで破棄して使用されていたメモリを解放することもできます。 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 で気を付けなければいけないのは、ガベージコレクション (GC) を実行するタイミングに注意が必要なことです。ガベージコレクションを実行するために、通常 GC はすべてのスレッドの実行を一時停止し、スタックを精査し読み込み済みオブジェクトの参照を登録します。これは現在の JavaScript では不可能なことです。このため WebGL で GC はスタックが空になった場合にのみ実行されます(現在これは毎フレーム後に1度行われます)。この仕様はほとんどのマネージドメモリを保守的に扱うコンテンツでは問題になりません。そして、相対的に多めの GC 割り当てがフレームごとに行われます(Unity プロファイラーを用いてデバッグすることができます)。
しかし、以下のようなコードを用いた場合は違います。
string hugeString = "";
for (int i = 0; i < 100000; i++)
{
hugeString += "foo";
}
このコードはループ中に、中間の文字列型オブジェクトで使用したメモリを解放するための GC を実行することができません。これが徐々に Unity ヒープのメモリ不足の原因になっていき WebGL での実行は失敗します。