Unity WebGLで、ゲームを更新(サーバにデータを上書き)した時に発生する事があるMaximum call stack size exceededエラー。
プログラムのミスで再起関数を大量呼び出しした時に発生する事もありますが、そうでない場合はプレイヤーにキャッシュクリアをお願いする必要がある厄介なエラーです。
この『Maximum call stack size exceeded』ですが、プレイヤーにキャッシュクリアを要求しなくても解決できる方法があるので、解説していきます。
なお、記事で使用している環境はUnity2021.2.19になります。
目次
結論
・Cache Bustingでunityの実行ファイルを確実に更新させる
・ビルド後に作成されるindex.htmlを編集して『Build.loader.js』『Build.data』『Build.framework.js』『Build.wasm』の後ろに『?hoge』を追加する(hoge=更新前の文字列と違う文字列)
・IPostprocessBuildWithReportを実装してindex.htmlを自動で編集するようにすると楽
・Unityのバージョンによって、index.htmlの内容が結構異なるのでバージョンに合わせて対応する
・unityroomの場合、こちらからindex.htmlを指定できないため効果なし
Maximum call stack size exceededとは何か
先に断っておきますが、私はWeb関連の知識には乏しいため、間違った情報を提供している可能性があります。
ですので間違っていたら、コメントにご指摘いただけると助かります。
Maximum call stack size exceededについて
JavaScriptのRangeErrorです。Maximum call stack size exceeded は、関数呼び出しが多すぎる場合や、関数にベースケースがない場合に発生するエラーです
https://rollbar.com/blog/javascript-rangeerror-maximum-call-stack-size-exceeded/
と書かれている通り『関数を大量に呼び出した場合』と『呼び出される関数が存在しない場合』の2パターンがあります。
『Maximum call stack size exceeded』で検索をかけた時に表示される対応は大体『関数を大量に呼び出した場合』のパターンです。
一方で『Maximum call stack size exceeded unity』等で検索を賭けた時に出てくる、キャッシュクリアに関連した記事やこの記事で記載する内容は『呼び出される関数が存在しない場合』のパターンです。
何故キャッシュがMaximum call stack size exceededエラーを引き起こすのか
そもそもキャッシュとは、同じURLを参照した時に何度もサーバへデータを取りに行かなくても済むよう、データを一時的に記憶しているデータです。
このキャッシュがある事で、同じURLに何度アクセスしてもサーバを経由する時間をかけずに画面を表示してくれるのですが、サーバを更新した後でも更新前のキャッシュを使用してしまう問題があります。
で、ここから先は調べても情報が少なすぎるため、殆ど推測になるのですが、Unity WebGLは『index.html』から『○○.loader.js』『○○.data』『○○.framework.js』『○○.wasm』を呼び出しています。
この『○○.loader.js』『○○.data』『○○.framework.js』『○○.wasm』が、更新前のキャッシュデータとサーバから取得した更新後データが混在した時に、更新前データには存在せず更新後に存在する関数が呼ばれMaximum call stack size exceededが発生してしまうのではないかと推測しています。
キャッシュクリアで直る理由
キャッシュクリアは一時的に保存しているデータを全て削除する事です。
キャッシュクリアを行うと、次にURLへアクセスした時にサーバから必要なデータを全て取りに行くため『index.html』や『○○.loader.js』『○○.data』『○○.framework.js』『○○.wasm』を全て更新後のデータにすることが出来ます。
その結果、Maximum call stack size exceededのエラーは出現しなくなります。
が、更新するたびに毎回「Maximum call stack size exceededエラーが出現した場合は、キャッシュクリアをして再度URLにアクセスしてください」とプレイヤーに要求するのは、ちょっとアレかなって思います。
プレイヤーにキャッシュクリアさせずに対策する方法
Cache Bustingで更新時のみサーバから取得する
エラー原因はプレイヤーが保持しているキャッシュです。
ですので、更新した時は確実にサーバから全データを取得させるよう指示したいところです。
それを可能にする方法が『Cache Busting』です。
Cache Bustingですが、Web系専門外の私には詳しく説明できる自信がないので、各自で検索お願いします。
(Cache Bustingについて詳しく説明されているサイト:https://webliker.info/html/cache-busting/)
要はjsファイルやgzファイルの後ろに『?更新前とは違う任意の文字列』を追加してからサーバにアップロードすればいいわけです。
そうすれば、更新した時にサーバへファイルを取りに行ってくれるため、プレイヤー側がキャッシュクリアしなくてもMaximum call stack size exceededを出さずにプレイできるようになります。
Unity2021.2でCache Bustingを行う
例としてUnity2021.2.19を用いて、Cache Bustingが出来るように改造してみます。
まずは通常通りWebGLをビルドします。
ビルド完了後、index.htmlをコードエディター(vsCodeとか)で開き、該当箇所を下記のように変更します。
//変更前
var buildUrl = "Build";
var loaderUrl = buildUrl + "/○○.loader.js";
var config = {
dataUrl: buildUrl + "/○○.data",
frameworkUrl: buildUrl + "/○○.framework.js",
codeUrl: buildUrl + "/○○.wasm",
streamingAssetsUrl: "StreamingAssets",
companyName: "YourCompany",
productName: "YourProduct",
productVersion: "0.1.0",
showBanner: unityShowBanner,
};
//変更後
var buildUrl = "Build";
var loaderUrl = buildUrl + "/○○.loader.js?hoge";
var config = {
dataUrl: buildUrl + "/○○.data?hoge",
frameworkUrl: buildUrl + "/○○.framework.js?hoge",
codeUrl: buildUrl + "/○○.wasm?hoge",
streamingAssetsUrl: "StreamingAssets",
companyName: "YourCompany",
productName: "YourProduct",
productVersion: "0.1.0",
showBanner: unityShowBanner,
};
『○○.loader.js』『○○.data』『○○.framework.js』『○○.wasm』それぞれの後ろに『?hoge』を追加しました。
もし『?hoge』を付けて更新した後さらに更新する場合は『?huga』『?piyo』の様に更新前とは異なる文字列を追加してください。
この状態で更新すれば、キャッシュを使用せずにサーバからデータを取得してくれます
注. 無圧縮での処理となります。Gzip等の圧縮形式を使用する場合『○○.data』『○○.framework.js』『○○.wasm』の拡張子が変化します。こちらのサイトに圧縮形式による拡張子の違いが書かれているのでご確認ください
IPostprocessBuildWithReportで自動化する
毎回ビルドした後に自力で入力するのは面倒なので、ビルド後に自動で行ってくれるようにしてみます。
IPostprocessBuildWithReportはビルド後に処理を行うインターフェースなので、これを実装して以下の様に書いてみました。
/// <summary>
/// <para>更新した時にMaximum call stack size exceededエラーを出さない様にする処理</para>
/// <para>Unity2021.2.19で動作確認済み</para>
/// <para>Create by Whimsical</para>
/// <para>Mit License</para>
/// </summary>
public class PostprocessBuildWebGL : IPostprocessBuildWithReport {
public int callbackOrder => 0;
readonly string loader = "loaderUrl = ";
readonly string framework = "dataUrl:";
readonly string wasm = "frameworkUrl:";
readonly string data = "codeUrl:";
public void OnPostprocessBuild(BuildReport report) {
//WebGL以外は実行しない
if (report.summary.platform != UnityEditor.BuildTarget.WebGL) return;
//index.htmlを探す
foreach(var file in report.files) {
if(file.path.Contains("index.html")) {
ReplaceHtml(file.path);
Debug.Log("html変換完了");
return;
}
}
Debug.LogError("index.htmlが見つかりませんでした");
}
/// <summary>
/// htmlを変更する処理
/// </summary>
private void ReplaceHtml(string path) {
List<string> html = new List<string>();
//html読み取り
using (StreamReader sr = new StreamReader(path)) {
while (!sr.EndOfStream) {
string text = sr.ReadLine();
//CacheBustingで、更新後のみキャッシュを利用しないようにする
text = AddCacheBusting(text);
html.Add(text);
}
}
//htmlを上書きする
using (StreamWriter sw = new StreamWriter(path, false)) {
foreach(var set in html) {
sw.WriteLine(set);
}
}
}
/// <summary>
/// キャッシュさせたくないファイルにgetパラメータを付与する
/// </summary>
private string AddCacheBusting(string text) {
text = AddCacheBusting(loader, text, "\";");
text = AddCacheBusting(framework, text, "\",");
text = AddCacheBusting(wasm, text, "\",");
text = AddCacheBusting(data, text, "\",");
return text;
}
/// <summary>
/// 差し換え対象の時のみ、ビルドの完了時刻を追加する
/// </summary>
private string AddCacheBusting(string replaceTarget, string text, string lastCode) {
if (text.Contains(replaceTarget)) {
//ここに?を追加すると、正規表現チェックでErrorになる
string para = "time=";
string add = DateTime.Now.Ticks.ToString();
//前回追加したデータが残っている場合
if (text.Contains(para)) {
Debug.Log($"{replaceTarget}のパラメータを変更しました");
return System.Text.RegularExpressions.Regex.Replace(text, @$"{para}[0-9]+", $"{para}{add}");
}
//新規作成されている場合
else {
Debug.Log($"{replaceTarget}にパラメータを追加しました");
return text.Replace(lastCode, $"?{para}{add}{lastCode}");
}
}
else {
return text;
}
}
}
このスクリプトをEditorディレクトリに入れてビルドすると、index.html内にある『○○.loader.js』『○○.data』『○○.framework.js』『○○.wasm』に自動でビルド完了時刻を追加します。
無圧縮、Gzip、Brotli、Decompression Fallbackによる拡張子の変更に対応していますが、何度も書いている通り、Unity2021.2で動くscriptなので他バージョンでの動作は保証できません。
unityroomに投稿する場合
この対応はindex.htmlを書き換えるものであり、自前のサーバへ配置する時などに上記対応を行う事でMaximum call stack size exceededを防ぐことが出来ます
しかし、unityroomの場合はカスタムしたindex.htmlを使用することは出来ません。(2022/12/26現在)
そのため上記の対応をしても効果はなく、Maximum call stack size exceededが発生する場合があります。
(私が最後にMaximum call stack size exceededエラーを確認したのは去年頃なので、もしかしたら今は対応されているかもしれないですが一応)
注意事項
繰り返しますが、この記事で紹介した方法はUnity2021.2での方法です。
それ以外のバージョンでは変更箇所が異なったり、上手くいかない可能性があります。
実際Unity2019.4のindex.htmlとUnity2021.2のindex.htmlでは画像のように書き方が全く違うため、2021.2以外のバージョンはそれぞれ各自で対応をお願いします。