回復ボタンを押した直後、仲間の配列を読んでいた処理が敵の配列へ切り替わっていました。 ホラーRPG「ヴェイルモート」(検証当時の題名は「葬約の国」)の長い遠征で起きた停止は、音や敵の枚数を減らして直す問題ではありませんでした。
これは、UnityのIMGUIで作った戦闘画面を直した制作記録です。ゲーム一式の作り方ではなく、実際の失敗、原因を絞った順序、修正した範囲を残します。
遠征の途中で、WebGLの実行が止まった
最初は水路の主との戦闘中でした。PCでは15ターン目、模擬タッチでは5ターン目に、次の表示が出ました。保存を観測する処理にも応答しなくなります。
Uncaught exception from main loop: RuntimeError: memory access out of bounds Halting program.
同じブラウザーの中断保存から再開すると、主を倒せました。帰還して宿泊もできます。再開すれば動くことと、長い遠征を止まらず遊べることは別でした。
私はまず、音の有無と保存の反復を切り分けました。どちらも停止を再現できず、例外が起きた場所を調べる方へ進みました。
| 実施した切り分け | 結果と、分からないこと |
|---|---|
| 音ありの合成戦闘を70ターン | 停止を再現しなかった。音に問題がないとまでは言えない |
| 音なしの合成戦闘を70ターン | こちらも停止しなかった。音を切れば解決するという根拠にならない |
| 通常UIで保存取込200回、その後70ターン | 例外0。保存回数だけでも再現しなかった |
| 別出力の開発版で、同じ実保存から水路遠征 | 画面描画のIndexOutOfRangeExceptionを取得できた |
連続戦闘の検査では通常攻撃を選び続けていました。後で分かった原因の操作が、その中にありません。
開発版では、戦闘画面の配列範囲外と分かった
公開候補を上書きせず、Development BuildとFullWithStacktraceを使った診断版を別フォルダーへ出力しました。同じ実冒険の保存ファイルを標準の取込操作で読み、水路へ向かいました。
診断版では帰還・宿泊まで操作できましたが、コンソールには次の例外が残りました。クリアできたことだけを成功にすると、このエラーを見落とします。
IndexOutOfRangeException: Index was outside the bounds of the array. at Hollow.Runtime.GameView.BattleScreen () at Hollow.Runtime.GameView.OnGUI ()
PC用のBattleScreenを読むと、対象ボタンの件数はループの前に決めている一方、対象が仲間か敵かは毎回判定していました。タッチ用のCompactBattleにも同じ処理がありました。
ボタンを押した、その描画の途中で行動が変わっていた
戦闘では3人分の指示を順に選びます。傷薬の対象を割り当てるAssignは、次の人物へ進む時に行動をattackへ戻していました。
| 同じループの中で起きた順序 | 状態 |
|---|---|
| 傷薬の対象ボタンを用意する | 仲間3人なので、反復回数は3 |
| 仲間0のボタンを押す | Assignが次の人物へ進め、行動をattackに変更 |
| 続きのボタンを描く | 行動が攻撃なので、敵の配列を参照 |
| 敵1を読む | 敵は1体。存在する添字は0だけなので範囲外 |
最初に決めた件数は3のままです。後から読む配列だけが敵側へ変わる。この組み合わせが問題でした。

以前の回復表示の検査は、最後の人物が仲間0へ回復する条件でした。最後の人物の指示が揃うと保存処理へ進み、その場で次の人物の攻撃へは切り替わりません。回復できるかを確かめていても、今回の境界は覆えていませんでした。
対象の種類を固定し、選択したら描画関数から戻す
PCとタッチの両方で、仲間向けか、蘇生向けかをループの最初に固定しました。また、対象ボタンを押したらその画面の描画関数から戻し、残りのボタンを変更後の状態で読み進めないようにしました。
次は実装の構造を短くした抜粋です。ゲーム本体の対象名・座標・Assignは省略しているため、単独で動く完成コードではありません。
bool allies = AllyAction;
bool reviving = action == "revive";
int count = allies ? State.party.Length : battle.enemies.Length;
for (int i = 0; i < count; i++)
{
// 対象名や有効状態も、固定した allies / reviving で決める。
// ボタン操作で Assign(i) が行動を変えても、残りへ進まない。
if (DrawTargetButton(i, allies, reviving))
return;
}
単に添字を配列の末尾へ丸める修正にはしませんでした。そうすると、存在しない敵の代わりに別の敵を表示し、選択の不整合を隠してしまいます。
一操作で再現する検査に変えた
長い遠征を繰り返す代わりに、敵1体・仲間3人・傷薬2個の合成入口を用意しました。保存ファイルを通常UIで読み、最初の人物から仲間0へ傷薬を予約します。
修正前の開発版では、PCのBattleScreen、模擬タッチのCompactBattleで同じ例外を再現しました。修正後の通常版では、最初の人物と途中の人物の2条件を、両方の操作方式で確認しました。
| 修正後に確認したもの | 結果 |
|---|---|
| 最初の人物からの傷薬対象選択 | PC・模擬タッチとも例外0 |
| 途中の人物からの傷薬対象選択 | PC・模擬タッチとも例外0 |
| 指示を全員分選んだ後の保存 | 2ターン目、傷薬1個消費、体力回復を確認 |
| 通常ビルド | 194.57秒、14,893,041バイト、ビルドエラー0 |
合成入口は、この不具合の再現専用です。ゲームを正規の資源で攻略できる証明には使っていません。難易度、乱数、メモリ容量も、この修正では変えていません。
今回残した再発防止は、最初・途中・最後の対象で、次の入力状態へ移る操作を確かめることです。検査のターン数を増やす前に、どの入力で状態が切り替わるかを見る必要がありました。
文字欠けを実ファイルから調べた例はUnity WebGLの日本語フォントを絞った記録、操作やカメラの作り直しは横スクロールアクションの制作記録へまとめています。
同じゲームの画像取込と配信対象を見直した結果は、WebGLの配信を40.6%削減した記録へまとめました。