以下はShoulder Blade OVERDRIVEの開発中に遭遇した……まぁ、そもそもは自分自身が無自覚なまま仕込むものだから遭遇もへったくれもないのだが……いわゆるバグ、についての備忘録である。
これらは既に解決済だが、きっと今後も似たようなことをしでかすに違いないので、こうして書き遺しておけば何かの役に立つだろう。
1. ランダムに発生する(ように見える)ウォームリスタート
現象としては、ゲーム動作中に何の前触れもなく突然MSXがリブートし、起動ロゴが表示されて何事もなかったようにオープニングデモが動き出す、というもの。アセンブラプログラミングをある程度知っている人なら、これだけで概ね原因は想定できると思うが、スタックを扱うループからの離脱に際しての考慮漏れが真因。
上に示したのは、この問題を単純化したサンプルコードになる。この構造は、特にシューティングゲームに頻出するもので、一般化すれば「Bレジスタの回数だけ内部でループするサブルーチン」ということになるが、例えば本作では弾との当たり判定がこれに該当する。
自弾にせよ敵弾にせよ、これは画面内に複数個存在している。それらとの当たり判定をおこなうオブジェクト主観から見ると、画面内に存在し得る弾の数=ループ回数の当たり判定となり、上に示したサンプルコードの形になる。そして、その途中で実際に当りが発生した場合、大抵それ以上当たり判定のループを続けることに意味はないから、その時点でループを途中で抜ける処理が発生する。ここに落とし穴がある。
Z80にかぎらず現代的なそれも含め、アセンブラレベルにおけるサブルーチンコールとは、呼び出し元のプログラムアドレスを一旦スタックに記録し、サブルーチン終了時にプログラムカウンタをスタックに記録していた呼び出し元アドレスへ戻す、という動作になる。問題になるのは、Z80を含む多くのCPUにおいては、このときメモリ上のアドレス記録に使われるスタックと、レジスタ等数値の退避に使われるスタックは同一のものである点。CPUは自分がスタックから引き出した値がメモリ上のアドレスなのか、データとしての値なのかを知る術を持っていないので、少なくともアセンブラレベルにおいてはプログラマがこれを意識して扱わないと、ここで触れているようなバグの温床となる。
上掲<駄目な例>のサンプルコードでは、サブルーチン内ループ処理の先頭でBCレジスタの値をスタックに積んでおきながら、これを引き出すことなくループから抜けようとしてretをおこなっている。すると、retによるプログラムカウンタの遷移は、サブルーチン呼び出し元のアドレスではなく直前にスタックに積まれたBCレジスタの値になる。どちらも16ビットの数値であることに違いはないが、一方は1丁目23番地、といった意味合いであるのに対し、他方は456という数値だ。後者を4丁目56番地と読んで実際にその住所を尋ねても無意味であるのと同様に、これは事実上ランダムなアドレスにいきなりジャンプするのと同じことになり、多くの場合CPUは暴走し、その結果としてしばしばウォームリスタートがかかる。
原理は概ね以上の通りだが、もちろんそんなことは百も承知で<良い例>の原則……こういう構造のループから抜けるときは、一見無駄だが別のアドレス(上例ではラベルEXIT)へジャンプし、スタックを一段抜いてからretするコーディングをやっているのだ。では、どういうときにこのバグを仕込んでしまうかと言うと、言い訳がましい話ではあるが、一旦できあがって動作テストも済んだこのサブルーチンに、3日後くらいに何らかの機能追加を思いついたときが一番ヤバい。
なにせ、実際のコードは(処理・条件判定)の部分が上に示したサンプルコードよりももっと長く複雑で、テキストエディタ上で全体のロジック構造が目視しにくくなっている。ましてや本作のソースコードは既に1.7万行に達しようとしているのだから何をか言わんや。いや、ボクが野放図なプログラマだ、ってだけの話ではあるんだけども。