スペースインベーダーの謎

ふと、スペースインベーダー(元祖アーケード版)の仕様について興味が湧いたので、メモを兼ねてまとめてみる。

画面の解像度

256x224(註)を横倒し状態で使っている。8x8 の文字キャラクターに換算して、ピッタリ 32 列 28 行になる。

さらに、自機下部の画面を水平に区切る赤線を除いて、上下左右におそらくアナログ時代なので表示用のマージンとして各 1 文字分ずつが空けてある。その分を考慮に入れると 240x208(30 列 26 行)となる。

また、画面上部の 3 行分はスコア表示用の領域として使われ、画面下部の 1 行分は残機やクレジット表示用の領域として使われているので、これらの分も除いたゲームのメイン画面の表示領域は 208x208(26 列 26 行)の正方形となる(画面が横倒しになっている点に留意)。

註:Computer Archeology で Screen Geometry 情報として 2400 - 3FFF (1C00 bytes = 256 * 28) 28*8=224. Screen is 256x224 pixels. と記されているように、VRAM のアドレス割り当てから 256x224 であるという技術的根拠が明確である。一方、なぜか日本語の Wikipedia には 260x224 と記載されており、それを元にしたものと思われる日本語のブログ情報の多くが 260x224 としているが、間違っていることになる。MAME でキャプチャーすると 260x224 で画面キャプチャーされるため、それを元に日本語の Wikipedia に誰かが記載し、そこから流布したのだろう。Linux を「リナックス」と読んだり、Indie game(s) を「インディーズゲーム」と読んだりして、普及初期の誤ちが流布してそれが正しいものと思い込んでいたりするようなものか。

メイン画面の行構成

上記、メイン画面として残った 208x208(26 列 26 行)の部分について分析する。

行↑行↓用途
250外れた自弾が爆発
241UFO
22 ~ 232 ~ 3
18 ~ 21 4 ~ 7エイリアン
14 ~ 178 ~ 11水色エイリアン
10 ~ 1312 ~ 15エイリアン
6 ~ 916 ~ 19エイリアン
4 ~ 520 ~ 21トーチカ
322
223水色自機
124水色
025外れた敵弾が爆発
下部ボーダーライン

エイリアンや UFO の左右の移動範囲は、基本的にメイン画面の幅一杯を意図しているようだが、なぜか右端ではあと一歩(2 ピクセル)分余裕のある状態でエイリアンが折り返す。UFO は左右 0 ピクセルぴったりで、ちょうど左右対称になっている。

自機の移動範囲とトーチカの配置がちゃんと左右対称になっていないのも謎である。

自機は、左 8 ピクセル、右 15 ピクセルという歪な配置になっており、右側が約 1 文字分(7 ピクセル)ずらせば対称になるので、単なるミスの可能性がある。

トーチカにしても、左側が 3 文字分、右側が 3 文字分+ 3 ピクセル多いので、この余った 3 ピクセルを 3 つのトーチカの間に 1 ピクセルずつ分ければ、配置が完全に左右対称になるはずなので、やはりミスなのではないだろうか。そもそもトーチカのグラフィックス自体が左右非対称なのも気になる。

解析

機械語ソースコードによると、自機の移動範囲は 0x30 ~ 0xD9 となっている(MovePlayerLeft ~ MovePlayerRight)。つまり、48 ~ 217。これでは実際よりも右にずれ過ぎている(左右の画面マージン 8 ピクセルずつと配置の 8 + 15 ピクセルのパディングと自機の幅 16 ピクセルを考慮に入れると、幅的には 224 ピクセル幅に一致しているが)。C の移植版のコードを見ると、16 ~ 185 で左右の画面マージン 8 ピクセルずつと配置の 8 + 15 ピクセルのパディングと自機の幅 16 ピクセルを考慮に入れると、ピッタリ 0 ~ 223 の 224 ピクセル幅に収まり、上述の実際の画面の分析結果と一致する。16 ~ 185 は、十六進表記で 0x10 ~ 0xB9 であり、機械語ソースの値から 0x20 を引いた数である。逆に言えば、「なぜか」機械語ソースでは、座標に 0x20 の「ゲタ」があることになる。これが単なるプログラム内部での扱いに起因するものなのか、ハードウェア的なビデオの仕様に起因するものなのかは不明である。0x20 = 32 ピクセルであり、224 + 32 = 256 というちょうどキリの良い数字になり、またこれは画面の縦サイズと同じである。(→ プログラム内部の仕様だった:参考

シャーロック・ホームズ的な発想の飛躍的な推理を無謀にも試みると、実は、スペースインベーダーは元々画面を -90° 横倒しにせず、そのまま横長の 256x224 の画面で開発していたのではないだろうか。なので元々は x 座標は 0 ~ 255 を前提としてプログラムされていた。後に -90° 横倒しにした縦長の 224x256 の画面で使うことに変更された。そのために元々プログラム内の各箇所に入力してあった様々な x 座標の値を、いちいち修正するのではなく、すべからく -0x20 して扱うことにすれば、修正漏れによるバグの発生を防ぐことができる。ハンドアセンブルしていた時代のことなので、大いにありうる話である。もちろん、西角さん本人に確かめでもしない限り、真偽は確定不能なわけだが。

この推理のもう一つの強力な根拠は、機械語ソースで、走査線の中期割り込みを 224 / 2 = 112 本目ではなく、256 / 2 = 128 本目で行うようにプログラムされているという、ちょっとしたバグである。機械語ソースのサイトの解析者の人がコメントで首をかしげている(ScanLine96)が、この推理はそのようなバグを生むようになった経緯をも説明できるものである。

さらに、なぜ、後で -90° 横倒しにするようになったのかという理由までも推理できる。機械語ソースを理解できればわかることだが、スペースインベーダーでは、vblank(走査線が画面右下に達した直後で、これから左上に戻る空白期間の始まるタイミング)のみならず、middle screen という画面の半分(本当は 112 本目だが、バグで 128 本目)まで走査線が描き終ったタイミングの 2 回のタイミングで、ハードウェア側からの割り込み(現代のプログラミング用語で言うところのコールバック)を受け取り、ゲーム画面を 1 フレームあたり 2 回に分けて更新するような構造になっている。ダブルバッファリングなどなかった時代のことである。現代は、vsync に合わせたペースでフレームバッファを更新しておけば、勝手にダブルバッファなどで定期的に写し取ってくれるわけだから、このような苦労はない。しかし、西角さんは、このように、フレームバッファと抜きつ抜かれつのレースのような、アナログなハードウェアとの速度の限界を試すようなプログラミングをしていたわけである。こんな、「中期割り込み」みたいな曲芸技をするほどだから、当初は、vblank のタイミングのみによる処理速度の限界に達して、画面のチラツキが発生していたはずである。それで苦肉の策で「中期割り込み」という曲芸技に辿り着く。これは走査線を上半分描いた段階で、とっとと上半分のグラフィックスの次のフレームの更新を開始してしまおうという方策である(そして vblank に達したら、残り下半分の更新を開始する)。ところが、普通に横長の画面を上半分と下半分で分けた場合、処理の重さが偏る(おそらく、エイリアンは段々と下に降りてくるし、エイリアンの弾は下に向けて降ってくるし、プレイヤーの砲台は常に下だし、下半分の負荷の方が常に多い)。これをできるだけ均等になるようにするには、画面を横倒しに使って、ゲーム画面の左右が、走査線の上半分(ゲーム画面の左)と下半分(ゲーム画面の右)に配当するのが最適だろう、と。プレイヤーの砲台のみ、vblank 固定であり、その他のオブジェクトが、上半分にいれば中期割り込みで、下半分にいれば vblank でという風になっていることの説明にもなるのがこの推理である。

プレイヤーの砲台の移動範囲が、左右非対称になっていたり、トーチカの配置やグラフィックスが左右非対称になっていたりするのも、このような、画面の向きの変更経緯において発生したちょっとしたバグと考えれば、納得が行く。

最前列の配置

トーチカとの間の空間
17 文字分
24 文字分
32 文字分
41 文字分
51 文字分
61 文字分
70 文字分
80 文字分
90 文字分

自弾の移動範囲

自弾は自機の砲身の直上から出現し、外れた場合には、UFO の直上のマスに入ってから炸裂する。

ソースコード的には、自弾の Y 座標は RAM 0x2029 に保持されている。この RAM 0x2000~0x20BF の初期値は、ROM 0x1B00~0x1BBF からコピーされるが、0x2029 の初期値は 0x28 = 40 であり、自機の直上のマスであることがわかる。

外れた場合の上限は、ゲームのメインループで 2 番目に実行される PlyrShotAndBump から毎ループ実行される自弾の当り判定ルーチン PlayerShotHit において、Y 座標が 216 以上(UFO の直上のマスの下辺に相当する座標)かどうかを判断している。216 以上の場合は、当たり判定が発動する。

発射時の x 座標は、自機の(スプライトの)x 座標に +8 したものであり、ちょうど砲身の位置となるようになっているのがわかる(ソースコード)。

自弾の 1 フレームあたりの速度は 4 であり、RAM 0x202C に保持されているが、コピー元の ROM 0x1B2C の値は 4 であることで確かめられる。

注:スプライトを描画する座標は、左下角が基準になっている点に留意。ソースコード上は通常通り左上角だが、-90° 画面を回転しているので回転後の画面で上下左右を考えた場合には左下角になっている。

3 種類の敵弾

敵弾には 3 種類あるが、ネット上では、弾によって性能に違いがあるというまことしやかな噂を目にし、当初信じてしまったが、どうやら都市伝説のように思われる。ソースコードを見ても、そのような意図があるようには思われないのである。

都市伝説 1:自弾と敵弾の相打ち

外見の異なる敵弾は、それぞれ「ショットで相殺できる」「ショットで一方的に打ち消して貫通できる」「ショットで打ち消せずこちらのショットだけが遮られる」の3種類。

ゲームカタログ@Wiki ~名作からクソゲーまで~

YouTube のプレイ動画などを、コマ送りしたりして確かめてみたが、真っ赤な嘘だった。相殺が本来の正常な処理で、プログラムのバグによってタイミング的にすり抜けが時々起ってしまうだけのようで、決して敵弾の種類による性能などではないというのが真相のようである。その動画では、敵弾の種類によらず、互いの弾がすり抜けるのみならず、弾が当たったはずの UFO が 1 コマだけ一瞬エイリアンの爆発画像と重なったかと思うと、死なずにそのまま飛んでいくなどの現象も確かめられる。要するに、このゲームは、プログラムの構造上、処理タイミング的な問題が発生しやすいということである。

都市伝説 2:トーチカの破壊力に違い

この動画を観察すると、バリケード(シールド)への貫通度が、敵弾の種類によって異なるように思われます。

下図は左端のインベーダからT字型(Plunger)の敵弾が発射されたところです。敵弾が左端のバリケードに着弾しました。深く潜って(貫通度=7)爆発しています。

同じく左端のインベーダからジグザグ型(Squiggly)の敵弾が発射されたところです。敵弾が左端のバリケードに着弾しました。浅く潜って(貫通度=3)爆発しています。明らかに敵弾の種類により貫通度が異なっています。

FSマイクロ株式会社 ブログ

この件も、特にソースコードに「貫通度」を意図するような箇所は見つけられなかったので、たまたま、処理タイミング的な問題ではないかと思う。敵弾の種類による性能の違いではないはず。

3 種類の敵弾の違いの本当のところ

性能には違いはないものの、グラフィックス以外に、実は唯一の違いがある。それは、弾の発射されるタイミングである。Rolling Shot は自機を狙って発射され、Squigly Shot と Plunger Shot は、それぞれ一定周期で決まった列のエイリアンから発射される。さらに Squigly Shot は UFO と管理領域が共用されているため、UFO 出現中には発射されない。

逆アセンブルコード解析

行番号ブロック名備考
0000:0005Resetinit
0006:0007(n/a)
0008:000EScanLine96- 中期割り込み
- レジスターの状態を push して 008C に続く
000F(n/a)
0010:006E
ScanLine224- vblank 割り込み
1. レジスターの状態を push
2. vblankStatusCompYToBeam で利用する)を 128 にセット
3. ウェイト用のカウントダウンタイマー変数(isrDelay)を -1 する
4. 筐体傾きチェックルーチン(CheckHandleTilt
5. コイン投入チェック(002D:0041)
6. suspendPlay フラグが無効ならここで割込終了
7. gameMode フラグが有効な場合は、GameModeTask をして割込終了
8. それ以外の場合は、コインが投入されていなければスプラッシュ画面用の ISR 処理(ISRSplTasks)をして割込終了
9. コインが投入された場合、既にスタートボタン待ちループが有効な(waitStartLoop フラグが立っている)場合は何もしないで割込終了、未だスタートボタン待ちループが有効でない場合はループを開始する(WaitForStart
006F:0080GameModeTask1. 行軍音(TimeFleetSound
2. 敵弾のタイマーを同期
3. カーソルのエイリアンを描画(DrawAlien
4. ゲームオブジェクト処理ルーチン(RunGameObjs)を実行
5. UFO 出現のカウントダウンの更新(TimeToSaucer
0081(n/a)
0082:0087割込終了- レジスターの状態を pop して元の状態に戻し、割込受付(i8080 は割り込み発生時に都度、割込受付が無効化される)を有効化してから、今回の割込処理を終了する
- 中期割り込み、vblank 割り込み共用部分
0088:008B(n/a)
008C:00B0中期割り込み1. vblankStatusCompYToBeam で利用する)を 0 にセット
2. suspendPlay フラグが無効なら割込終了
3. gameMode フラグが無効でかつ isrSplashTask も無効(要するにデモプレイ以外のスプラッシュ画面)の場合は割込終了
4. ゲームオブジェクトのポインターを(1 番目の自機は飛ばして)2 番目の自弾にセットした状態で、ゲームオブジェクト処理ルーチン(RunGameObjs)を実行
5. エイリアンのカーソルを次に進める(CursorNextAlien
6. 割込終了
00B1:00D6InitRack- NewGame から呼ばれる
1. プレイヤー別のリファレンスエイリアンの座標(p1RefAlien or p2RefAlien)を GetAlRefPtr を使って得てリファレンスエイリアン(refAlien)およびカーソルエイリアンの座標(alienPos)としてセットする
2. 続けてプレイヤー別のリファレンスエイリアンの速度(p1RefAlienDX or p2RefAlienDX)を得てリファレンスエイリアンの速度(refAlienDXr)にセットする。ただし、速度が 3 の場合は 2 にする
3. さらにこの速度の ± に基いて rackDirection フラグをセットする
00D7:00E1InitAlienRacks- NewGame から呼ばれる
1. 1P(p1RefAlienDX)と 2P(p2RefAlienDX)両方のリファレンスエイリアンの Δx を 2 で初期化する
2. 08E4 に続く
00E2:00FF(n/a)
0100:0140DrawAlien- 0108:0112 ではプレイヤー(1 or 2)とエイリアンのカーソル(alienCurIndex: 0..54)に基いて該当エイリアンの生死フラグを得ている。
- エイリアンが死んでいた場合は、続く描画処理を丸々スキップして 0136 へ直行する。生きている場合は続く描画処理を行う。
- 0117:0135 + 013B:0140 は描画処理で、エイリアンの所属する行からエイリアンの種類を決定し、さらにアニメーションのフレームにより描画すべきスプライトを決定し、alienPosLSBalienPosMSB に格納された座標を使って DrawSprite で VRAM に書き込んでいる。
- 0136:013A は waitOnDraw フラグをクリアして、呼出元にリターンしている。このフラグがクリアされたことで、次に中期割り込みが発生した場合に CursorNextAlien の処理を進めることができるようになる。
0141:0179CursorNextAlien- 中期割り込み(mid-screen ISR)から実行される
- エイリアンの描画処理対象カーソル(alienCurIndex)を次に進める
1. 自機が爆発炎上中(playerOK フラグが 0)は処理をスルー
2. DrawAlienwaitOnDraw フラグがクリアされていない場合は(現在のカーソルのエイリアンに対する描画処理が未完了なので)中期割り込みから発動されても処理をスルー(カーソルを進めるわけにはいかない)
3. 現在のカーソルの次に生きているエイリアンのインデックスを探す。途中でインデックスが最後尾(= 54)に達していた場合は MoveRefAlien の処理を呼び出してインデックスを先頭に戻してから、検索を継続する(再度、最後尾に達した場合はエイリアンが全滅していることを意味するので、その場合は CursorNextAlien からリターンする)。
4. 新しい alienCurIndex に基き、そのエイリアンの座標と行位置を GetAlienCoords で算出する
5. 新しく得た座標を alienPosLSBalienPosMSB に格納し
6. Y 座標が 40 未満になった場合は占領を意味するので、ゲームオーバー(invaded へ)
7. 新しく得た行位置を alienRow に格納し
8. waitOnDraw フラグを立てて、呼出元にリターンする
017A:01A0GetAlienCoords- リファレンスエイリアンの座標を基準として、対象のエイリアンの隊列の中におけるインデックス値から、位置と行位置を算出している。
- 11 で割った商が行位置、余りが列位置である。座標は縦横とも 16 ピクセルずつの幅なので、行列が分かればリファレンスエイリアンの座標(2009:200A)から相対的に計算できる。
01A1:01BEMoveRefAlien- エイリアンの描画処理対象カーソル(alienCurIndex)が最後尾(= 54)に達していた場合に CursorNextAlien から呼び出される
- 先頭のエイリアンはリファレンスエイリアンとして特別な役割を持つので、生死によらず一定の処理が必要
1. alienCurIndex を 0 に戻す
2. 2007:200A のリファレンスエイリアンの座標 (x, y) と (dx, dy) に関する値を使って AddDelta によって (dx, dy) を適用する(要するに移動する)処理を行う。また dy は適用後 0 にリセットするので、隊列が端に達した場合は一段だけ下に降りることになる。
3. alienFrame をスイッチする
4. H レジスターにプレイヤー情報(1 or 2)をセットしてから、呼出元にリターンする
01BF(n/a)
01C0:01CCInitAliens2100:2136 にはエイリアン 55 匹分の生死フラグが格納されているが、ループでそれを全て 1(生)に初期化している。
01CD:01CEReturnTwo
01CF:01D8DrawBottomLine- 画面下部の水平線を描画する
- ClearSmallSprite を応用して、消去ではなく、ドットパターンで埋めることで結果的に線を引いている
01D9:01E3AddDeltaHL レジスターにセットされたアドレスから始まる連続する 4 つの変数値(呼出元によって 2007:200A または 20C3:20C6 の 2 通り)を使って、そこに格納されている現状の (x, y) に (dx, dy) を加算する処理を行う。この過程で dx を一旦 C レジスターに入れる処理を行うが、なぜかこのルーチン内部ではなく、呼出側でその作業を行う仕様になっている。
01E4:01EECopyRAMMirror- BlockCopy を使って RAM(2000:20BF)の各種設定値を ROM 初期値(1B00:1BBF)で初期化する
- 通常は前述の通りだが、init から呼ばれる時のみ ROM(1B00:1BFF)→ RAM(2000:20FF)の範囲がコピーされる
01EF:01F4DrawShieldPl11P 用のバッファー領域を使って DrawShield で初期状態のトーチカをバッファーにセットする
01F5:01F7DrawShieldPl22P 用のバッファー領域を使って DrawShield で初期状態のトーチカをバッファーにセットする
01F8:0208DrawShieldスプライトを定義している ROM 領域からプレイヤーのバッファー領域へ 4 個の初期状態のトーチカを BlockCopy でコピーしている
0209:020DRememberShields11P 用のバファー領域を使って CopyShields でトーチカをセーブする
020E:0212RememberShields22P 用のバファー領域を使って CopyShields でトーチカをセーブする
0213:0219RestoreShields22P 用のバファー領域を使って CopyShields でトーチカをリストアする
021A:021DRestoreShields11P 用のバファー領域を使って CopyShields でトーチカをリストアする
021E:0247CopyShields- A レジスター が 0 の場合はバッファーから VRAM へリストア、1 の場合は反対に VRAM からバッファーへセーブする
- リストアは RestoreShields を使って 4 つのトーチカをプレイヤー用のバッファーからコピーして描画する
- セーブは RememberShields を使って 4 つのトーチカをプレイヤー用のバッファへコピーして保存する
- (32, 48) に最初のトーチカを描画し、間を 23px 空けて 4 回繰り返している
0248:028DRunGameObjs- gameMode フラグが有効な場合に、vblank 割り込みと中期割り込みのどちらからも実行される
- HL レジスターにセットされたRAM 領域のアドレスを基準にして各オブジェクトのタイマーとハンドラーアドレスを読み込む
- 最初のタイマーの上位 1 バイトの値がマジックナンバー -2 の場合はスキップして次のオブジェクトの処理に、-1 の場合は全オブジェクトの RAM 領域を抜けた印なので RunGameObjs ルーチン自体からリターンする。
- タイマーは 2 種類ある。ステージ開始後に自機が登場するまで少し時間を置いたりするのに使われる。
- タイマーが 0 になっていれば、ハンドラーアドレスに飛んで、該当オブジェクトの処理ルーチンを実行する。
- 以下、次のオブジェクトの処理にループする
028E:0295GameObj0- まず冒頭で playerAlive を調べて、自機が爆発炎上中かどうかで処理が大きく 2 つに分岐する
- 平常状態では、PlayerNotBlowingUp へ飛ぶ
- 爆発炎上中の場合はそのまま引き続き HandleBlowingUpPlayer に進む
0296:02AD(GameObj0)
HandleBlowingUpPlayer
1. expAnimateTimer をカウントダウン(-1)し、0 に達していなければここで RET
2. playerOK フラグを false、enableAlienFire フラグも false、alienFireDelay は 48 にリセットし、expAnimateTimer を 5 にリセットする
3. さらに、expAnimateCnt をカウントダウン(-1)して 0 に達していなければ DrawPlayerDie へ、0 に達したらそのまま BlowUpFinished
02AE:0337(GameObj0)
BlowUpFinished
1. EraseSimpleSprite で爆発炎上スプライトを消去
2. 自機に関する各種 RAM 設定値(2010:201F)を ROM の初期値(1B10:1B1F)から BlockCopy してリセットする
3. 占領されている(invaded フラグが立っている)場合はここで処理を RET
4. gameMode ではない(スプラッシュ画面の)場合も ここで RET
5. スタックポインターを初期位置(0x2400)に戻す、すなわちスタックをクリアすることで、現在のプロセスを基底プロセスにしてから、割り込みを改めて有効にする
- 以下、同じではないが NewGame に似た一連の処理
6. Game タスクをサスペンド状態にする(DsableGameTasks
7. GetNumberOfShips により残機数を得て、0 なら GameOver
8. 残機がある場合に、以降の処理は主に 1P ↔ 2P の切り替えに伴う処理だが、さしあたって一人プレイモードだったり二人プレイでももう一人のプレイヤーが既にゲームオーバーだった場合は、プレイヤーの切り替え処理は関係がないのでそのまま残機を減らし(RemoveShip)て、NewGame 末尾の 0x0817 経由で GameLoop に流れ込む。
9. 二人プレイモードでもう一人のプレイヤーがまだ生きている場合には、プレイヤー(1P or 2P)に応じた領域にトーチカの状態を画面から保存し、リファレンスエイリアンの座標とデルタも保存する
10. RAM 設定領域をリセットする(CopyRAMMirror
11. playerDataMSB を、既存のプレイヤーが 1 なら 2(0x22)に、2 なら 1(0x21)に変更する
12. 2 秒待ってから、プレイヤーに応じてテーブル筐体用の表示の上下反転を行う(出力ポート 5 のビット 5 の On/Off による)
13. ClearPlayField で画面中段を消去
14. 残機を減らし(RemoveShip)て
15. NewGame 途中の 0x07F9 経由で GameLoop に流れ込む
0338:033A(n/a)
033B:0345(GameObj0)
PlayerNotBlowingUp
1. playerOK フラグを立てる
2. PlayerNotBlowingUp2 へ続く
0346:036E(GameObj0)
PlayerNotBlowingUp3
1. gameMode が false(デモモード)の場合はデモコマンドに基き、左右に移動する(MovePlayerRight or MovePlayerLeft
2. gameMode が true(ゲームモード)の場合はジョイスティックの入力に基き、左右に移動する(MovePlayerRight or MovePlayerLeft
3. DrawPlayer へ続く
036F:0380(GameObj0)
DrawPlayer
1. 自機のスプライト(3 種のうちポインターで示されているもの)と座標に基いて DrawSimpSprite で描画する
2. obj0TimerExtra = 0 をセットする(しかし実際には初期値 0 のまま変化することはないので有名無実化している)
0381:038DMovePlayerRight
038E:039AMovePlayerLeft
039B:03AF(GameObj0)
DrawPlayerDie
- 自機の 2 種の爆発スプライト #0 と #1 でトグルする
1. トグルした値を playerAlive にセーブ
2. さらに 0 か 1 かに基いてスプライトの格納されているアドレスを算出し、それを plyrSprPic として更新する
3. 更新された plyrSprPic に基いて自機を描画する(DrawPlayer
03B0:03BA(GameObj0)
PlayerNotBlowingUp2
- PlayerNotBlowingUp2 の処理の続き
1. enableAlienFire フラグが false の場合は、さらに alienFireDelay を -1 して 0 になった場合は enableAlienFire フラグを立てるために 0346:0349 経由で、
2. それ以外は 034A~ に直行する
03BB:03F9GameObj1- 自弾の状態(plyrShotStatus)に応じた分岐処理
1. CompYToBeam 的に今回の割り込みで処理すべきではない場合は RET
2. plyrShotStatus == 0 の場合、何もせずに RET
3. plyrShotStatus == 1 の場合、InitPlyShot へ(RET)
4. plyrShotStatus == 2 の場合、MovePlyShot へ(RET)
5. plyrShotStatus == 3 の場合、初回は blowUpTimer のカウントダウンを開始して RET、カウントダウンが途中の場合も単に RET。カウントダウンが終った場合のみ、ここを抜けて EndOfBlowup に突入する
6. plyrShotStatus == 4 の場合、EndOfBlowup に直行
7. plyrShotStatus == 5 の場合、何もせずに RET
03FA:0409(GameObj1)
InitPlyShot
1. plyrShotStatus を 2 に進めておいて
2. 自機の X 座標を基準に +8 した値(自機の砲身の座標)を自弾の座標(obj1CoorXr)としてセットし
3. 自弾を DrawShiftedSprite で描画する
040A:0429(GameObj1)
MovePlyShot
1. 自弾の既存の座標に対して EraseShifted で消去し
2. 自弾の Δy(shotDeltaX)を適用して自弾の Y 座標(obj1CoorYr)を更新
3. DrawSprCollision で衝突判定と描画を行って(衝突していたら、alienIsExploding フラグを立ててから)、GameObj1 のタスクを抜ける
042A:042F(GameObj1)
Other shot-status
- GameObj1 の自弾の状態(plyrShotStatus)に応じた分岐処理の続き
- ここの条件を抜けた(plyrShotStatus == 4 だった)場合は、EndOfBlowup に突入する
0430:0435(GameObj1)
ReadPlyShot
自弾のスプライト情報を RAM の該当領域(2027:202B)から読み込むメソッド
0436:0475(GameObj1)
EndOfBlowup
1. 自弾または自弾の爆発画像を EraseShifted で消去
2. 自弾の状態変数(2025:202B)を ROM 初期値に戻す
3. UFO の点数変動に関する処理
- UFO の点数は自弾のカウント(shotCountLSB)と連動して対応表(SaucerScrTab)の 16 個の値へのポインターをローテーションさせて使おうとしているが、バグにより一つ少ない 1D62 までの 15 個でローテーションさせてしまっている
- UFO の移動方向も自弾のカウントと連動している。shotCountMSB の値は ROM 初期値の 8 から変化しないが、自弾のカウンター shotCountLSB と結合して 0x0800 ~ 0x08FF のアドレスをローテーションで示すことになる。そのアドレスのプログラムコードの値の第 0 ビットを調べて、0 であれば右から左(←)に、1 であれば左から右(→)に UFO が出現・移動するという、奇想天外なアルゴリズムで実現されている。さらに、西角さんは 0x0800 ~ 0x08FF のアドレスを使った結果、左右の確率が均等になるようにするため、意図的に NOP = 0x00 を挿入することで、ちょうど 128:128 になるように調整しているらしい(参考)。
0476:04B5GameObj2- 回転弾
1. obj2TimerExtra に初期値(0x1B32 の 2)をセットし直す
2. rolShotCFir (2038:2039) が 0x0000 だった場合は 0xFFFF をセットして RET
3. 回転弾用に ToShotStruct をセットしてから、otherShot1pluShotStepCnt の値を、otherShot2squShotStepCnt の値をセットし、HandleAlienShot を実行する
4. aShotBlowCnt が 0 になっていない場合は FromShotStruct でデータを aShot(2073:207D)から rolShot(2035:203F)にセーブし、0 の場合は ROM の初期値(1B35:1B3F)を rolShot(2035:203F)に書き戻す
04B6:050DGameObj3- ピストン弾は特定の列から発射される。またエイリアンが最後の一匹になった場合は発射されない
1. skipPlunger フラグが立っていたら RET
2. shotSync が 1 でなかった場合も RET
3. ピストン弾用に ToShotStruct をセットしてから、otherShot1rolShotStepCnt の値を、otherShot2squShotStepCnt の値をセットし、HandleAlienShot を実行する
4. HandleAlienShot で発射列表のポインターが進められた結果、最後に達していたら ROM 初期値に戻しておく
5. aShotBlowCnt が 0 になっていない場合は FromShotStruct でデータを aShot(2073:207D)から pluShot(2045:204F)にセーブしてここで RET。0 の場合は ROM の初期値(1B45:1B4F)を pluShot(2045:204F)に書き戻すが、発射列表(のポインター)は初期化したくないため aShot から pluShot へセーブして次へ
6. numAliens を調べてエイリアンが最後の一匹になっている場合は skipPlunger フラグを立てておく
050E
050F:054FSquigglyShot- ウネウネ弾の処理本体
1. ウネウネ弾用に ToShotStruct をセットしてから、otherShot1pluShotStepCnt の値を、otherShot2rolShotStepCnt の値をセットし、HandleAlienShot を実行する
2. HandleAlienShot で発射列表のポインターが進められた結果、最後に達していたら ROM 初期値に戻しておく
3. aShotBlowCnt が 0 になっていない場合は FromShotStruct でデータを aShot(2073:207D)から squShot(2055:205F)にセーブしてここで RET。0 の場合は ROM の初期値(1B55:1B5F)を squShot(2055:205F)に書き戻すが、発射列表(のポインター)は初期化したくないため aShot から squShot へセーブし直す
0550:055AToShotStruct1. A レジスター経由で渡された値を shotPicEnd(敵弾のスプライトアニメーションの最後のバイトを示す下位アドレス)としてセット
2. RAM の敵弾のプロパティの領域(2073:207D)に DE レジスター経由で渡された敵弾(回転・ピストン・ウネウネのどれか)のプロパティの領域をブロックコピー(BlockCopy)する
055B:0562FromShotStructRAM の敵弾のプロパティの領域(2073:207D)から HL レジスター経由で渡された敵弾(回転・ピストン・ウネウネのどれか)のプロパティの領域にブロックコピー(BlockCopy)する
0563:05C0HandleAlienShot- 敵弾(GameObj2~4)共通の処理
1. 冒頭で敵弾の状態(aShotStatus)がアクティブなら MoveAlienShot
2. 非アクティブで isrSplashTask = 4(スプラッシュ画面で余分な C を射つ)の場合は途中の処理を飛ばして 9 の処理へ直行
3. enableAlienFire フラグが無効なら、ここで RET
4. aShotStepCnt を 0 にする
5. 0 < otherShot1aShotReloadRate の場合は、RET
6. 0 < otherShot2aShotReloadRate の場合も、RET
7. aShotTrack フラグにより、この弾が狙い撃ちタイプ(回転)の場合は TrackingShot へ(一旦分岐してから FindInColumn からの処理に合流する)。それ以外のタイプ(ピストン・ウネウネ)は発射列表(aShotCFir)を使う。その場合、発射列表から列番号を読み取り、ポインターを次のアドレスに進める。読み取った列番号を使って、FindInColumn で該当する生きたエイリアンを探し、見つからなければ RET
8. 見つかったエイリアンの座標を使い、x 座標は +7、y 座標は -10 したものを弾の座標(alienShot)としてセット
9. aShotStatus フラグを On にし、aShotStepCnt を +1 する
05C1:061AMoveAlienShot- HandleAlienShot で敵弾が既にアクティブだった場合にこちらに分岐する
1. CompYToBeam 的に今回の割り込みで処理すべきではない場合は RET
2. aShotStatus を調べて弾が爆発炎上中ならば ShotBlowingUp へ分岐(→ RET)
3. aShotStepCnt を +1
4. (0675:067B で)元の敵弾を消して
5. アニメーションスプライトをローテーションし
6. 弾の Δ を適用して座標を更新
7. (066C:0674 で)衝突判定付きで弾を描画する
8. 移動の結果、Y 座標が 21 未満の場合は床に当たったので末尾の爆発炎上処理(0612:061A)へ。何も衝突していなければ RET
9. 衝突していた場合、30 ≦ y < 39 であれば自機がやられたので playerAlive フラグを -1 にする
10. 爆発炎上処理(0612:061A
061B:062ETrackingShotHandleAlienShot の処理の分岐で、自機の頭上から発射する回転弾用の発射ルーチン
062F:0643FindInColumn- HandleAlienShot で使われるルーチン
- C レジスター経由で与えられた列上の 5 行を下から順に探して見つかったエイリアンのインデックスを返す(見つからなかった場合は Carry フラグが off で返る)
- 列番号は 1 開始なのでこのルーチンの中でも最初に -1 して使っている点に留意
0644:067DShotBlowingUp1. aShotBlowCnt を -1 する
2. aShotBlowCnt が 3 の場合は最初の段階であることを意味するので、まず元の弾を EraseShifted で消してから補正した位置に敵弾爆発スプライトを DrawSprCollision で描画する
3. aShotBlowCnt が 2~1 の間は何もせず RET(待ち時間)
4. aShotBlowCnt が 0 になったら、EraseShifted で敵弾爆発スプライトを消す。
067E:0681GameObj3GameObj3 の処理の 050D からの続き
0682:06F8GameObj4- ウネウネ弾か UFO 用のタスクで、どちらか一方だけ。両方同時には出現しない
1. shotSync が 2 でなかった場合は RET
2. saucerStart フラグが false ならウネウネ弾(SquigglyShot
3. squShotStepCnt が 0 以外の(処理が進行中)場合はウネウネ弾(SquigglyShot
4. ここまでの段階に来て、saucerActive フラグが無効な場合はエイリアンの残数(numAliens)が 8 未満の場合はウネウネ弾(SquigglyShot)、8 以上の場合は UFO を出現させる(saucerActive フラグを立てて DrawSaucer で描画)
5. CompYToBeam 的に今回の割り込みで処理すべきではない場合はここで RET
6. saucerHit フラグが立っていない場合、Δ を反映して UFO を進めて DrawSaucer で描画する。移動の結果、画面の左端(8 未満)または右端(193 以上)に達していたら RemoveSaucer。いずれにしても RET
8. saucerHit フラグが立っていた場合は爆発処理。UFO の飛翔音を消し、saucerHitTime を -1 する。
9. saucerHitTime が 31 の時は最初のフレーム用の処理(SaucerExplode)をして RET
10. saucerHitTime が 24 の時は UFO のスコア表示(SaucerScore)で RET
11. それ以外で saucerHitTime がまだ 0 に達していなければ何もすることがないのでここで RET
12. saucerHitTime が 0 に達していた場合は爆発音を Off にしてから、RemoveSaucer に進む
06F9:070BRemoveSaucer- UFO が飛び去って行く場合のルーチン
1. ClearSmallSprite で UFO 画像を消す
2. UFO の設定値 2083:208C を ROM(1B83:1B8C)の値で初期化
3. UFO 音を Off
070C:073BSaucerScore- UFO 撃墜時のスコア表示
1. adjustScore フラグを立てる
2. 自弾の EndOfBlowup で弾が爆発するたびにローテーションで更新される、UFO 撃墜時のスコア表(SaucerScrTab)に対するポインター(sauScore)から、今回の撃墜得点を得る
3. 4 種類の得点(5、10、15、30)とそれぞれに対応する表示用文字列のアドレスの対応表が 1D4C:1D4F と 1D50:1D53 にあるので、それを使って、SaucSoreStr から表示用文字列を得る
4. 得点を 0x10 倍したもの(0x50、0x100、0x150、0x300)を scoreDelta にセットする
5. 引数として 3 を与えて(PrintSaucerScore)から PrintMessage を使って 3 ケタの撃墜スコア文字列を表示する(RET)
073C:074ADrawSaucerDrawSimpSprite を使って UFO を描画するルーチン
074B:075ESaucerExplode- UFO 爆発時の最初のフレームでの処理ルーチン
1. 爆発音を On にし、行軍音は Off
2. DrawSimpSprite で爆発スプライトを描画
075F:0764ResetSaucerRemoveSaucer で使用する UFO の設定値を ROM 初期値に戻すルーチン
0765:0797WaitForStart- TestStartButton を繰り返し呼び出して 1P か 2P が押されるのを待つ
- 画面中央に "PRESS ONLY 1PLAYER BUTTON" または "PRESS 1 OR 2PLAYERS BUTTON" を表示する
- ループが開始されるに当たって、スタックがクリアされるので、このループが基底ループとなる(cf. GameLoopSplashLoop
0798:081ENewGame- ゲームループ用の一定の初期化処理を行ってから、GameLoop
1. プレイヤーモード(1P or 2P)に応じてクレジット(numCoins)を減らし、クレジット表示を更新(DrawNumCredits)する
2. 1P と 2P のスコア(P1ScorP2Scor)の両方を 0 点で初期化し、表示を更新する
3. (以降の一連の初期化処理が終わるまで)Game タスクをサスペンド状態にする(DsableGameTasks
4. gameMode を On にし
5. 1P と 2P 両方の playerAlive フラグと playerEx フラグを On にする
6. 画面上下のスコア表示、クレジット表示を描画(DrawStatus
7. 1P と 2P それぞれのトーチカを(VRAM ではなく各プレイヤーのデータ領域に)描画(DrawShieldPl1DrawShieldPl2
8. ディップスイッチの状態を調べてスタート時の残機数(3 ~ 6 機)を得て(GetShipsPerCred)、それを 1P(p1ShipsRem)と 2P(p2ShipsRem)それぞれの残機数としてセットする
9. リファレンスエイリアンの Δx を初期化(InitAlienRacks
10. 1P(p1RackCnt)と 2P(p2RackCnt)それぞれの面数を 0 に初期化する
11. 1P(InitAliens)と 2P(InitAliensP2)それぞれの 55 匹のエイリアンの生死状態を初期化する
12. 1P(p1RefAlienY)と 2P(p2RefAlienY)それぞれのリファレンスエイリアンの座標を (24, 120) で初期化する
13. RAM(2000:20BF)の各種設定値を ROM 初期値(1B00:1BBF)で初期化する(CopyRAMMirror
14. 自機の残機表示を更新(RemoveShip
15. PLAY PLAYER <1>(または <2>)の表示を 2 秒表示(PromptPlayer
16. 残機・クレジット表示部(画面下部 2 行分)とスコアラベル(画面上部 4 行分)を除いた部分(26 行分)を消去(ClearPlayField
17. isrSplashTask を 0 に設定
18. 画面下部の水平線を描画(DrawBottomLine
19. プレイヤー 1P(RestoreShields1)か 2P(RestoreShields2 + DrawBottomLine)に応じたルーチンでトーチカを VRAM に復元する
20. プレイヤー別にセーブされているエイリアン隊列の速度と方向をゲーム自体の状態としてセット(InitRack
21. サスペンド状態にしていた Game タスクを解除する(EnableGameTasks
22. サウンドのマスターボリュームを On にする(SoundBits3On でビット 5 を立てる)
23.(以下、GameLoop へ流れ込む)
081F:0853GameLoop- NewGame の処理が最終的に行き着くループ
1. 自弾発射またはデモコマンドの処理(PlrFireOrDemo
2. PlyrShotAndBump
3. エイリアンの残機の算定(CountAliens
4. スコアの更新(AdjustScore
5. エイリアンが全滅したかどうかの判定(全滅なら EndOfStage へ(面クリア処理してから NewGame の途中に戻る))
6. スコアによって敵弾の発射頻度を調整(AShotReloadRate
7. 自機 1-Up の判定(AwardExtraShip
8. エイリアン残数による敵弾速度の調整(SpeedShots
9. 自弾の On/Off(ShotSound
10. 自機が被弾している場合は爆発音を鳴らす
11. 行軍音のペースの調整(FleetDelayExShip
11. UFO 音の調整(CtrlSaucerSound
0854:0856(n/a)
0857:0871TestStartButton押された場合は NewGame(1P)または NewGame 途中の 079B(2P)へ
0872:0877RestoreShieldsForPlayer1NewGame の処理の中の Player 1 用の分岐処理
0878:0882
0883:0885(n/a)
0886:088CGetAlRefPtr
088D:08D0PromptPlayer
08D1:08D7GetShipsPerCredディップスイッチの状態により、スタート時の残機数として使う 3 ~ 6 の値を返す
08D8:08E3SpeedShots- GameLoop から呼ばれる
- エイリアンの残数(numAliens)が 9 未満になった時に、敵弾のスピード(alienShotDelta)をデフォルトの -4 から -5 に上げる
08E4:08F02P 得点表示消去InitAlienRacks の処理の続き
08F1:08F2PrintSaucerScoreSaucerScore の処理の続きで、3 ケタのスコアを PrintMessage を使って描画させる
08F3:08FEPrintMessageDrawChar を使って文字を描画する
08FF:0912DrawCharA レジスターで指定されたコード番号の文字を該当するスプライトを定義した ROM の領域(1E00:1F4F)から決定し、DrawSimpSprite で描画する
0913:092DTimeToSaucer- GameModeTask から呼ばれる
1. リファレンスエイリアンの Y 座標(refAlienYr)が 120 未満にならないとタイマー(tillSaucer)のカウントダウンが開始しない(120 以上は RET)
2. タイマーが 0 の場合は、ROM 初期値である 0x600 = 1536 にリセットし、さらに saucerStart フラグを立てる
2. タイマーをカウントダウンする
092E:0934GetNumberOfShips現在のプレイヤー(1P or 2P)の残機数(0x21FF or 0x22FF の値)を返す
0935:097BAwardExtraShip1. CurPlyAlive を利用して現在のプレイヤー(1P or 2P)に対応した player1Ex または player2Ex を取得し、1-Up 済であれば、RET
2. デフォルト 1500 だが、ディップスイッチの設定によっては 1000 点を基準値としてセットし、現在のプレイヤーの得点を取得(GetScoreDescriptor)して比較して、基準値未満であれば RET、基準値以上であれば先に進んで 1-Up の処理へ
3. GetNumberOfShips を利用し、現在のプレイヤーの残機数のポインターを得てそれを +1 する
4. 新たな残機数に基いて残機アイコンの表示を DrawSimpSprite を使ってする
5. 残機数の数字表示を RemoveShip の最後の処理(4)を利用して更新する
6. CurPlyAlive を利用して現在のプレイヤー(1P or 2P)に対応した player1Ex または player2Ex のフラグを立て、1-Up 済にする
7. サウンドの持続時間としてに extraHold に 255 をセットし、1-Up サウンド(bit-4)を On にする
- ちなみに、extraHold は GameLoop から定期的に呼ばれる FleetDelayExShip でカウントダウンされる
097C:0987AlienScoreValueAlienScores の得点表を使って、エイリアンの種類に応じた得点を返す
- エイリアンの種類は、隊列中の行番によって識別している
0988:09ACAdjustScore- GameLoop から呼ばれる
1. プレイヤー(1P or 2P)のスコアと表示座標(20F8:20FB or 20FC:20FF)を得る(GetScoreDescriptor
2. adjustScore フラグが立っている場合だけ処理(無効なら RET)
3. まず先に adjustScore フラグを処理済状態(false)にしておく
4. scoreDelta をスコアに加算して反映する
5. 更新されたスコアと座標に基き、Print4Digits で点数を表示
09AD:09B1Print4Digits2 バイトの数値を 1 バイトずつ 2 ケタ + 2 ケタ = 計 4 ケタの数字として DrawHexByte で表示する
09B2:09C9DrawHexByte 2 ケタの数値を 1 ケタずつ 2 回に分けて DrawChar を使って表示する
09CA:09D5GetScoreDescriptorプレイヤー(1P or 2P)別のスコアと表示座標(20F8:20FB or 20FC:20FF)を返す
09D6:09EEClearPlayField- 残機・クレジット表示部(画面下部 2 行分)とスコア表示部(画面上部 4 行分)を除いた部分(26 行分)を消去(該当する VRAM 領域を黒 = 0x00 で埋める)
- cf. ClearScreen
09EF:0A3BEndOfStage1. 面クリア間際での自機の爆発処理(EndOfStage2
2. suspendPlay フラグを無効にする
3. スクリーン中段を消去(ClearPlayField
4.(CopyRAMMirror にさしあたって)現在のプレイヤー(1P or 2P)に関する値はスタックに退避してから、RAM の設定値領域をリセット(CopyRAMMirror)し、プレイヤーに関する値をリストアする
5. 面数(p1RackCnt or p2RackCnt)を +1 する
6. AlienStartTable から面数に基くエイリアンの隊列の初期位置(Y 座標を)決定する
7. 決定した Y 座標をリファレンスエイリアンの Y 座標(p1RefAlienY or p2RefAlienY)にセットし、X 座標(p1RefAlienX or p2RefAlienX)は常に 24 にセットする
8. (2P の場合のみ、サウンドを行軍音の 0 番にすると共に、テーブル筐体の場合は画面を回転するビットをセットする)
9. トーチカを描画(DrawShieldPl1 or DrawShieldPl2)し、エイリアンの隊列を初期化(InitAliens or InitAliensP2)し、NewGame 途中の 18 へ
0A3C:0A58EndOfStage21. IsPlayerAlive により、既に自機が爆発炎上中だった場合は途中のウェイトは行わずに単に末尾の処理(3)で爆発炎上が終了するのを待つ
2. 現時点で自機が爆発炎上していない場合、面クリアまでのわずかな残り時間でエイリアンの流れ弾に当たる可能性が残されているので、isrDelay をカウンターとして使い、48 フレーム(0.8 秒)はループして基底プロセスの中で自機が爆発炎上しないかどうかを繰り返しチェックする。0.8 秒後も自機が無事ならば単に RET
3. 自機が爆発炎上している場合は、爆発炎上アニメーションが終わるのを待ってから RET
0A59:0A5EIsPlayerAliveplayerAlive 自体は自機の平常スプライト(-1)、爆発スプライト1(0)、爆発スプライト2(1)という 3 種類のスプライト状態を示す。これを元にして、自機が爆発中かどうか(-1 でないかどうか)をチェックするだけのルーチン
0A5F:0A7FScoreForAlien- CodeBug1 から呼ばれる
1. サウンドビットの 3(エイリアンの命中音)を On する
2. エイリアンの行番から AlienScoreValue により点数を決定し、RAM 領域のスコアの Δ(scoreDelta)に控え(加算処理は GameLoop 中の AdjustScore で行われる)、さらにスコア加算が必要であることを示すフラグ(adjustScore)を立てておく
5. エイリアンの爆発画像(へのポインター)をセットする(CodeBug1 の次の処理で使われる)
- gameMode フラグが無効な場合はエイリアンの爆発画像(へのポインター)をセットする最後の処理のみ実行
0A80:0A92Animate
0A93:0AAAPrintMessageDel- 遅延ウェイトをかけながらテキストを一文字ずつ表示する
1. まず 1 文字目を DrawChar で描画する
2. isrDelay をカウンターとして使い、初期値 7 をセットする
3. isrDelay - 1 = 0 になるまで(要するに isrDelay = 1 になるまで)この狭い 0A9E:0AA4 の範囲をループして isrDelay がカウントダウンされていくのを待つ。カウントダウンは ScanLine224 の割り込み発生時に行われる。このサブルーチンを利用する時は suspendPlay フラグが無効にされていたり、スプラッシュ画面でもデモプレイモードではなかったりする状況のため、後続の ScanLine224 や ScanLine96 の割り込みが発生しても処理が早期に終了して、制御がこちらに戻ってくるので、ScanLine224 によって isrDelay が 1 まで(6 フレーム分)カウントダウンされ次第、PrintMessageDel の処理が先に進むことになる
4. 文字数分が終ったら RET、終っていなければ文字列のポインターを次のバイトに進めて PrintMessageDel の冒頭に戻って繰り返す
0AAB:0AB0SplashSquiggly
0AB1:0AB5OneSecDelayWaitOnDelay を使い、64 フレーム待つ
0AB6:0ABATwoSecDelayWaitOnDelay を使い、128 フレーム待つ
0ABB:0ABESplashDemo元々 vblank 割り込み(ScanLine224)のタスクの一端として流れてきた処理だが、呼出元に戻らないようにするためにスタックを POP してから、GameModeTask の 2 へ(要するに行軍音をスキップした段階から)ジャンプする
0ABF:0ACEISRSplTasksisrSplashTask の値によってデモプレイ(SplashDemo)、逆さ Y(SplashSprite)、CCOIN(SplashSquiggly)のいずれかの動的なスプラッシュ画面を実行する。
0ACF:0AD6MessageToCenterOfScreen- 画面中央(56, 160)に PrintMessageDel で 15 文字のメッセージを表示する
- 実際にはスプラッシュ画面の「SPACE INVADERS」の表示のためだけに使われる
0AD7:0AE1WaitOnDelayA レジスターにセットされた値を isrDelay にセットし、その値が後続の割り込み(ScanLine224)によってフレーム毎にカウントダウンされて 0 になるまでは RET せずにループして待つ
0AE2:0AE9IniSplashAni
0AEA:0BF6
スプラッシュ画面ループ1. サウンド用のポートを両方とも off
2. スプラッシュ画面モードを 0(無効)にセット(SetISRSplashTask
3. 割り込みを有効化する
4. 64 フレーム待つ(OneSecDelay
5. splashAnimate フラグの On/Off によって PLAY または逆さ Y の PLAY を PrintMessageDel で表示する
6. その下に「SPACE INVADERS」を表示する(MessageToCenterOfScreen
7. 64 フレーム待つ(OneSecDelay
8. さらに下にスコア表を描画する(DrawAdvTable
9. 128 フレーム待つ(TwoSecDelay
10. splashAnimate フラグにより、逆さ Y のアニメーションを行う場合は 0B1E:0B49 を実行してから、通常の Y の場合は 11(0B4A~)に直行する
11. まず、画面中段を消去(ClearPlayField)してから、ゲームオーバー直後で残機が 0 になっている場合はデフォルトのスタート直後の残機数に戻し、そうでない場合は(残機を -1 せずに)そのまま次の処理に行く
12. デモプレイ用の初期化処理(CopyRAMMirrorInitAliensDrawShieldPl1RestoreShields1 → スプラッシュ画面モード(isrSplashTask)を 1(デモプレイ)にセット → DrawBottomLine
13. 自機が被弾するまで PlrFireOrDemoPlyrShotAndBumpCheckHiddenMes の処理をループし、被弾したら plyrShotStatus を 0 にしてから、playerAlive をチェックして爆発アニメーションが終わるのを待ってから次の処理へ進む
14. スプラッシュ画面モード(isrSplashTask)を 0(無効)にセット(GameOver 時はここから再開する
15. 64 フレーム待って(OneSecDelay)から、画面中段を消去(ClearPlayField
16. PrintMessage で「INSERT  COIN」を描画
17. さらに、splashAnimate フラグにより、「CCOIN」のアニメーションを行う場合は DrawChar で C を追加する
18.「<1 OR 2 PLAYERS>」を表示(座標や文字列は ReadPriStruct により、表示は PrintMessageDel による)
19. ディップスイッチの設定により、さらに「*1 PLAYER 1 COIN」「*2 PLAYER 2 COINS」を表示
20. 128 フレーム待つ(TwoSecDelay
21. splashAnimate フラグにより「CCOIN」のアニメーションを行う場合は、IniSplashAniAnimateAnimateShootingSplashAlien で C を破壊するアニメーション
22. splashAnimate フラグをトグルする
23. 画面中段を消去(ClearPlayField
24. init の 3(18DF~)に戻ってループ
0BF7:0BFFMessageCorp隠しメッセージの「TAITO COP」の文字列データ
0C00:13FF(検査用コード)
1400:1421DrawShiftedSprite- スキャンラインに沿った方向に 1 ピクセル単位で任意にずら(シフト)した位置にスプライト(8 ピクセル幅)を描画する。
- VRAM 側は(スキャンラインに沿った方向には)1 文字(1 バイト= 8 ピクセル)単位で読み書きされるので、1 回の処理で VRAM の 2 文字(バイト)分の該当アドレスを書き込む対象とする必要がある(書かれる側のスプライトのデータは 8 ピクセル幅でも、位置の「ずれ」により、2 文字分の領域にまたがるため)。
- 書かれるスプライトのデータを「ずらした」ものに加工するため、i8080 CPU には備わっていないハードウェアシフトレジスターを使っている
- シフト処理により、データを「ずらす」ためだけでなく、バイト単位でまとめて扱わなけれならなかった制限が解消されるので、ドット単位の処理が可能になり、ドット単位で論理和(OR)を行うことにより透過的なスプライト描画を実現している。
- EraseShifted はこれの消しゴム版
- DrawSprite とはほとんど共通しており、違いは、透過の有無
- 自弾とその爆発スプライトの描画に使われる
1422:1423(n/a)
1424:1438EraseSimpleSprite- 自機かエイリアンの爆発アニメーションの終了時の 2 箇所のみにおいて使われているメソッド。スキャンラインに沿って 2 バイト(16 ピクセル)ずつ消去している。上に 1 文字分大きい範囲を消去することになる。
- 爆発したエイリアンの真上に敵弾がさしかかっていた場合、意図せず消されてしまって一瞬ステルス化してしまうという可能性があるかもしれない
1439:1446DrawSimpSprite- レジスターで指定された横幅(スキャンラインの本数)分の回数をループして 1 バイト分(スキャンラインに沿った 8 ピクセル。画面の -90° 回転を考慮すると縦方向)ずつを描画している。
- 自機や UFO、スコア表示などで使われている
1447:1451(n/a)
1452:1473
EraseShifted- ピクセル単位の任意の位置に、透過を考慮してスプライトの形に描画ではなく消去をする。つまり、論理積(AND)を使い、元々白が描かれていた部分に、スプライトの白が重なった場合に、そこだけを消す。
- DrawShiftedSprite の消しゴム版に相当する
- ハードウェアシフトレジスターを使っている
- 自弾とその爆発か敵弾を消す場合に使われる(トーチカに弾痕を残すため)
1474:147BCnvtPixNumber
147C:1490RememberShields
1491:14CADrawSprCollision- スキャンラインに沿った方向に 1 ピクセル単位で任意にずら(シフト)した位置にスプライト(8 ピクセル幅)を描画(OR)するが、その前に一度、衝突判定を(AND によって)行う。もし衝突していたら(AND の結果 ≠ 0 だった場合)、衝突フラグ(collision)が立つ(ゲームループ側の処理に影響を与える)
- 衝突判定以外は DrawShiftedSprite と同じ
- ハードウェアシフトレジスターを使っている
- 自弾と敵弾によって使われる
- ピクセル単位の厳密な衝突判定であり、この点で、いくら OpenGL/DirectX など駆使したりして見た目は派手でも後世のほとんどのシューティングゲームは退化・劣化している
14CB:14D7ClearSmallSprite1 バイト(8 ドット)分の縦幅で B レジスターにセットされただけの横幅(スキャンラインの本数)だけA レジスターにセットされた二進数パターン(左右が逆になる?)を描画する。Clear という名前が(解析者によって)付けられているが、それは A レジスターにセットされた値が 0 の場合だけである。基本的には黒の塗り潰しによる Clear 同然の使われ方をしているが、唯一、画面下部の水平線を描画する場合に 0b00000001 をセットして使っている。
14D8:1503PlayerShotHit- GameLoop において実行される
-(割込処理の一環である)自弾の移動ルーチンにより衝突フラグがオンになった場合、何に命中したのかはこちら側で判定する。
- まず、天井に到達した場合は、MissShot に進む。
- UFO 領域で当った場合は、UFO に命中したものとして、SaucerHit を経て AExplodeTime の中に抜けてゆく。
- 後半の 14F7:1503 ではエイリアンの隊列全体に対するブロードフェイズの当たり判定を行っている。
- ブロードフェイズの当たり判定が true なら、さらにどのエイリアン個体に当たったのかを判別するナローフェイズのルーチン(CodeBug1)に入っていき、そこには若干のバグが孕んでいるようだ(CodeBug1 についての解析者のコメントを参照)。しかし、そもそもこのブロードフェイズの処理自体が、わかりにくい。
- ブロードフェイズで false になって除外される場合は、2 つの条件で判断され、2 つ目の条件から先に言うと「爆発した自弾がリファレンスエイリアン(隊列の左下隅のエイリアン)の Y 座標の -6 以下にいる」というもので、これはわかる。1 つ目の条件が不可解で、「リファレンスエイリアンの Y 座標が、0x90 = 144 以上」というものであり、1 面の一番高い場所からスタートする場合でも、Y 座標は 120 なわけで、そこから 2 段下がった位置である。常に false になるわけで、こんな条件をチェックしても意味がないのではないか──と。
- 実はこれ、リファレンスエイリアンの Y 座標がマイナスになった場合を想定している。マイナスになると、一周回って 255 から値が下りてくることになる。つまり、この「リファレンスエイリアンの Y 座標が 144 以上」というゲームスタート時の Y 座標としては有り得ない条件が発生するのは、まさしくそのタイミングなのである。
- 元々、2 つ目の条件が本命で、エイリアン以外の「敵弾またはトーチカに当った場合」を除外しようとしている。しかし、リファレンスエイリアンが 1 周回った高い Y 座標にあるために「爆発した自弾がリファレンスエイリアンと同じかそれよりも下にいる」という条件を満たしても意味がない。それで 1 つ目の条件でそのようなケースは除外しているわけである。
- つまり、ここでは、「確実にエイリアンの隊列に当たることは有り得ない場合」を除外するのが目的である。依然として、この先のルーチン(Code Bug)に進んだとしても、必ずしもエイリアンに当ったとは限らず、敵弾やトーチカに当っている可能性も残されている。
- ここで除かれた場合は、エイリアン以外のものに当ったということで、MissShot に進む。
1504:152FCodeBug1- エイリアンの隊列全体に対するブロードフェイズで true だった場合に突入するナローフェイズの当たり判定
- 最終的に命中したエイリアンのインデックスと座標を FindRow, FindColumn, GetAlienStatPtr により特定し、必要な処理を行うことを目的としている
- 命中したのがエイリアンであることが確定した場合、得点や命中音の処理(ScoreForAlien)をし、エイリアンの爆発スプライトを DrawSprite で描画し、表示タイマーを 16 にセットする
- 起りにくいケースではあるものの、自弾が命中した場所が隊列の内側であることを前提としているため、隊列の右外でトーチカに当った場合に誤判定が発生し得る。上下方向についてはブロードフェイズで絞られているために隊列の領域の外側になることはほとんど起り得ない。一方、左右方向にはブロードフェイズで絞られていないので、外側になることは有り得なくはない。ただし、上下方向で隊列の内側にあってかつ同時に左右方向に隊列の外側で自弾と当たる可能性があるのはトーチカくらいである。リファレンスエイリアン(死んでいてもよい)がトーチカよりも下に降りてきている段階までトーチカが残存していないと起こせない現象である。運良く引き起せると、トーチカに当たったのが誤判定でエイリアンに当たったことになって、ちょうどそのインデックスのエイリアンが生きていたらそれは死ぬことになる。
1530:1537MissShot- 自弾が命中した何かが、UFO でもエイリアンでもない場合に、辿り着く分岐処理
- 要するに敵弾かトーチカか天井に当った場合
1538:1552AExplodeTime- 場所的に CodeBug1 関連のコードの中に紛れ込んでいるが、これは DrawAlien で使われるサブルーチンである
- 後半の処理が MissShot と共有しているためにここに置かれているのだろう
1553(n/a)
1554:1561Cnt16s- FindRowFindColumn でエイリアンの隊列の左下に位置するリファレンスエイリアンの座標から標的(自弾)の座標までが、行列にして何行何列目なのかを 16(一匹のエイリアンが隊列の中で占めるサイズ)で割った数を算出して求めるために使っているメソッド
- i8080 には割り算命令などないので、割り算の商を加算の繰り返しによって求めるアルゴリズムを使っている。リファレンスエイリアンの座標に 16 ずつ加算することで、標的(自弾)の座標の値に到達するまでの回数を数えることで割り算の商と同じ結果を得ようとしている。
- 1556:1559 で CMP H して CALL NC, WrapRef しているが、CMP 命令は A レジスター(リファレンスエイリアンの座標)と H レジスター(標的の座標)の値の大小を比較している。その結果、NC(Carry フラグが false)であれば CALL WrapRef することになる。CMP 命令では、A と H が「同符号かつ A < H」か、「異符号かつ H ≦ A」の場合に、Carry フラグが true になる(Intel 8080 Assembly Language Programming Manual, p20~21)。逆に言えば、「同符号かつ H ≦ A」か、「異符号かつ A < H」の場合は NC に該当することになる。いずれにせよ、CMP H によって、標的がリファレンスエイリアンよりも右にある正常な状態を完全に選別できるので、そうでない場合はさっさと RET してしまうべきで、WrapRef するのは蛇足であり、むしろ蛇足が予測不能な結果を引き起す種となる(WrapRef の考察を参照)。
(要するに、CMP は値の符号は関係なしに、0~255 のバイトの範囲で H が A よりも右側にあれば C、同じか左側なら NC になる)
- つまり Cnt16s の本体はその次の 155A:1561 のループであり、このループは「標的がリファレンスエイリアンよりも右にある場合のみ」を対象とするアルゴリズムであることがわかる。
1562:156EFindRowCnt16s を使って得た値を最終調整して行番号とその Y 座標を得る
156F:1578FindColumn- Cnt16s を使って得た値を最終調整して列番号とその Y 座標を得る
- 役割は FindRow と同じなのだが、なぜか、こちらは列番号を 0 開始ではなく 1 開始のまま CodeBug1 に戻す仕様となっており、後にその列番号を利用する GetAlienStatPtr の中で結局 -1 して 0 開始に修正する羽目になっている
- ただし、このメソッドは CodeBug1 だけでなく、HandleAlienShot の処理の中でも使われているので、そちらでは列番号 1 開始の方が都合が良いのかもしれない(未確認)
1579:1580SaucerHitPlayerShotHit 中で UFO に命中したと判断された場合の処理で、AExplodeTime の後半の手続に抜けていく。
1581:158FGetAlienStatPtr- 行列番号(列番号は 1 開始として定義されている点に配慮する必要がある)からエイリアンの隊列インデックスを算出する(11 × 行数 + 列数)
- 1P か 2P によって格納されているアドレスが違うので、それに応じたアドレスを決定して戻り値として返す
1590:1596WrapRef- Cnt16s で使われている、というよりは CALL~RET を使っているものの実質 Cnt16s の一部
- Cnt16s では「標的がリファレンスエイリアンよりも右にある正常な状態」を拾ってカウントしようとしている。こちらには「標的がリファレンスエイリアンよりも左にある状態」の場合に飛ばされてくる。「同符号かつ H ≦ A」か、「異符号かつ A < H」の場合である。
- プラス同士で H ≦ A の場合、A が ~ 111 の場合 A に +16 しても依然プラスなので、カウントは 1 で終了して RET し、Cnt16s 側に戻っても CMP で即 RET する。つまり結果は 1。A が 112 ~ 127 の場合 A に +16 して A がマイナス(128~)になる。A が +16 を繰り返して一周回って 255 を超えてプラスになるまでカウントを続ける。Cnt16s 側に戻ると、A が H の左側にあるという正常な状態となっているので、さらにカウントを上積みしてから Cnt16s を RET することになる。
- マイナス同士で H ≦ A の場合、A が 255 を超えてプラスになるまでカウントを続ける。Cnt16s 側に戻ると、A が H の左側にあるという正常な状態となっているので、さらにカウントを上積みしてから Cnt16s を RET することになる。つまりプラス同士で A が 112 ~ 127 の場合と同じ状況になる。
- 異符号で A < H の場合も、「プラス同士で A が 112 ~ 127 の場合」や「マイナス同士で H ≦ A の場合」と同じである。
- もしここが、JM ではなく JP であった場合はどうだろうか? こちらの方がやや被害は小さい。A が +16 されていくことでマイナス(128~)になった場合にカウントがストップして RET するので、ほとんどのケースでカウント 1 で Cnt16s に RET し、Cnt16s から即 RET する。A が ~ 111 の場合のみ、いくつかカウントしてから Cnt16s に RET するが、Cnt16s から即 RET するのは同じである。リファレンスエイリアン(A)は右端にいても 223 - 16 = 207、一番高い位置でも 120 である。16 を一度加算しただけで 255 を超過することは有り得ないので、必ず 128~255 の中で JP の条件に適合せずに RET することになる(JM だと反対に 255 を通り抜けてプラスになるまで適合してループが続いてしまう)。
- なぜこのバグが大きな問題となっていないのだろうか? 通常トーチカに弾が当たった場合、ほとんどのケースでリファレンスエイリアンよりも下である。この場合はそもそも CodeBug1 のルーチンに入ってくること自体がない。そしてリファレンスエイリアンがマイナス位置まで降りている段階では、トーチカや敵弾に「リファレンスエイリアンよりも下の位置で」自弾と当たることは有り得ない。最後に、リファレンスエイリアンよりも左側でかつリファレンスエイリアンよりも上の位置で敵弾に当たるケースで、これは有り得なくはないがかなり稀である。そんなわけでこのバグはほとんど顕在化しなかったのだろうと思う。
1597:15D1RackBump- 衝突判定と同様にユニークで、エイリアンの隊列の左右の折り返しを、エイリアン自身によらず、画面のフレームバッファ側を調べる。画面の左右端から一定範囲内のエリアを調べ、何かが書き込まれていれば、それはエイリアンがそこまで横移動してきた結果書き込まれたものと判定して、強制的にエイリアンの dx を反転させ、y 座標を一列下げる処理をしている。エイリアン側が自ら折り返すというよりも、画面側がバンパーのようにして跳ね返している処理になっているのが面白い。
- 左側は (9, 23)-(9, 206)、右側は (213, 23)-(213, 206) の(スキャンラインに沿った方向の)1 ドット幅の線上を 8 ドット(一文字)単位の縦幅でチェックしている。
- ドット座標が 0 開始であることを考えると、左側は余白が 9 ピクセル、右側は余白が 10 ピクセルある状態でボーダーラインを設置しており、左右非対称になっているのはミスだろう。
- 縦方向には、自機と同じ高さの行から、UFO の移動領域の高さの行まで達しており、やや過剰に長い
15D2(n/a)
15D3:15F2DrawSprite- スキャンラインに沿った方向に 8 ピクセル= 1 文字分の幅でスプライトを上書き(透過なし)で描画し、さらに(スキャンラインに沿った方向、要するに上に)1 文字分の幅を黒く塗り潰して消去する。
- DrawShiftedSprite とはほとんど共通しており、違いは、透過の有無
- ハードウェアシフトレジスターを使っている
- エイリアン、エイリアンの爆発、スプラッシュ画面の逆さ Y 字を持つエイリアンの描画に使われる
15F3:1610CountAliens- GameLoop から呼ばれる
1. 55 匹分のエイリアンの生死を調べて、残数をカウントし、numAliens に反映する
2. 残数が 1 の時については特別に oneAlien フラグを立てる(エイリアンのスピードに影響する)
1611:1617GetPlayerDataPtr
1618:166APlrFireOrDemo
166B:166C
166D:16E5GameOver
GameOver2
- BlowUpFinished から分岐してくる
1. 残機の数字表示を 0 にする
2. CurPlyAlive でプレイヤー(1P or 2P)に応じた生死状態(player1Alive or player2Alive)を得て 0(爆発炎上スプライト #0)にする
3. GetScoreDescriptor で得たプレイヤーの得点がハイスコア(HiScor)を上回っていたら、ハイスコアを更新した上で表示も更新(PrintHiScore)する
4. twoPlayers フラグが立っている場合、プレイヤー(1P or 2P)に応じた「GAME OVER PLAYER<n>」の表示を PrintMessageDel でして、1 秒後に、もう一人のプレイヤーが生きている場合はプレイヤーを交代(BlowUpFinished の途中の 02ED~ へ復帰)。
5. twoPlayers モードではない場合や、もう一人のプレイヤーが死んでいた場合は、PrintMessageDel で「GAME OVER」の表示をして 2 秒後に、ClearPlayField で画面中段を消去
6. gameMode を false(スプラッシュ画面モード)にする
7. 出力ポート 5 の全ビットを 0 にして、行軍音と UFO 命中音を Off にする
8. サスペンド状態にしていた Game タスクを解除する(EnableGameTasks
9. SplashLoop の中の 14(0B89~)へ
16E6:170DInvadedGameOver1. スタックポインターを初期位置(0x2400)に戻す、すなわちスタックをクリアすることで、現在のプロセスを基底プロセスにしてから、割り込みを改めて有効にする
2. playerAlive を 0(爆発炎上中)に
3. 自機の爆発炎上状態が終了するまでの間、自弾の衝突判定(PlayerShotHit)と自機の爆発炎上音を発音し(SoundBits3On で bit2 を立て)続ける
4. Game タスクをサスペンド状態にする(DsableGameTasks
5. 画面下部の残機アイコン表示領域を消去(EraseSpareShips
6. 残機数表示を 0 に(RemoveShip の途中へ)
7. InvadedGameOver2
170E:172BAShotReloadRate- GameLoop から呼ばれる
- プレイヤーの点数によって敵弾の発射頻度を変えることで難易度を変化させる
-(1P か 2P かの)プレイヤーの点数の上位バイト(20F9 か 20FD)の値(要するに 100 点単位の値)を使う。
- スコア上限の表は 1CB8:1CBB、対応する発射頻度の表は 1AA1:1AA5 にある
- 下から探して行って、プレイヤーの上位バイトの値がその値以下であればそこの対応する頻度を aShotReloadRate にセット
- スコア上限の表の 4 番目を超過している場合は、5 番目の対応する頻度(最速の 7)が使われることにになる
172C:173DShotSound- 自弾の状態(plyrShotStatus が 0 かどうか)を見てサウンドビットの 1(自弾発射音)を On/Off する
- GameLoop で実行される
173E:173F(n/a)
1740:1774TimeFleetSound- GameModeTask のうちの一つ
1. 行軍音の発音持続時間(fleetSndHold)を -1 し、0 になったらサウンドポート 5 のビット 0~3(4 種の行軍音)を Off にする
2. 自機がやられている(playerOK フラグが false)なら、サウンドポート 5 のビット 0~3(4 種の行軍音)を Off にして RET(TimeFleetSound から抜ける)
3. 行軍音の交代カウンター(fleetSndCnt)を -1 し、未だ 0 にならなければ RET(TimeFleetSound から抜ける)。0 になっていたら soundPort5 の値を出力ポート 5 に書き込む。
4. エイリアンの残数(numAliens)を調べて 0 ならサウンドポート 5 のビット 0~3(4 種の行軍音)を Off にして RET
5. fleetSndCnt を fleetSndReload の値でリセットし、changeFleetSnd フラグを立てて行軍音の変更処理が必要な状態になっていることを示し、行軍音の発音持続時間(fleetSndHold)を 4 にセットする
1775:17B3FleetDelayExShip- GameLoop から呼ばれる
1. エイリアンの残数リスト(1A11:1A20)とそれに対応する行軍音の遅延時間リスト(1A21:1A30)を使い、現在のエイリアンの残数(numAliens)に基いて遅延時間を決定し、その値を fleetSndReload にセットする。
2. soundPort5 の値のうち、ビット 0~3 が 4 種類の行軍サウンドの選択を示すが、それをローテーションする。ビット 4、5 は別の機能があるのでそれを元のまま維持して、変更したビット 0~3 と結合したものが最終的に soundPort5 の値となる。
3. changeFleetSnd フラグを無効にする
4. 1-Up 音の持続時間(extraHold)を -1 し、0 になった場合はサウンドポート 3 のビット 4(1-Up 音)を Off にする
- changeFleetSnd フラグが無効な場合は 1-Up 音に関する最後の処理のみ実行
17B4:17BESndOffExtPly
17BF(n/a)
17C0:17CCReadInputs
17CD:1803CheckHandleTilt筐体の傾きを検知した場合にゲームオーバー処理にする。ピンボールゲーム全盛期の名残のような機能。
1804:1814CtrlSaucerSound- GameLoop で実行される
1. saucerActive フラグが false の場合は UFO 音を Off して RET
2. saucerHit フラグが立っていない場合は UFO 音を On にする
1815:1855DrawAdvTable
1856:1867ReadPriStruct
1868:1894SplashSprite
1895:1897(n/a)
1898:189D
189E:18D3
18D4:18E6init1. CopyRAMMirror により RAM 初期値を RAM 領域にコピーして設定値を初期化(通常の 0x20BF までとは違い、0x20FF まで初期化するので、ハイスコア等までも初期化される)
2. ステータス画面の描画(DrawStatus
3. 敵弾発射頻度(aShotReloadRate)を 8 にする(RAM 初期値の領域に混ったスプライトのデータにより aShotReloadRate が 3 となってしまうため)
4. スプラッシュ画面ループ
18E7:18F0
18F1:18F9
18FA:1903SoundBits3On
1904:1909InitAliensP22P 用の InitAliens
190A:190FPlyrShotAndBump1. PlayerShotHit を呼び出してから、
2. RackBump にジャンプしている
- RackBump は PlyrShotAndBump からしか使われていないので、実質、RackBump がPlyrShotAndBump の続きのようなものである。
- PlyrShotAndBump 自身は、Game Loop か、初期化後のループからしか呼ばれない。
1910:1919CurPlyAlive
191A:1930DrawScoreHead
1931:193BDrawScore
193C:1946
1947:194FDrawNumCredits
1950:1955PrintHiScore
1956:196ADrawStatus1. 全画面消去(ClearScreen
2. スコア表示の一行目の見出し部分を描画(DrawScoreHead
3. P1、P2 ぞれぞれのスコアを描画(PrintP1ScorePrintP2Score
4. ハイスコアを描画(PrintHiScore
5. 画面下部の「CREDIT」の文字列を描画(PrintCreditLabel
6. クレジット数を描画(DrawNumCredits
196B:1970InvadedGameOver2- InvadedGameOver の続き
1. 自弾音を off
2. GameOver 途中の 2 へ(1 の残機数表示を 0 にする処理は InvadedGameOver の 6 で済んでいるため。むしろ InvadedGameOver の 6 を削って GameOver の 1 に飛ぶ形にしても良かった気もするが……)
1971:1978invaded1. invaded フラグを立ててから
2. 占領時のゲームオーバー処理(InvadedGameOver)へ
1979:1981
1982:1995SetISRSplashTask1. A レジスターにセットした値でスプラッシュ画面のモード(isrSplashTask、1: デモプレイ; 2: 逆さ Y; 4: 余分な C)を切り替える(このようなサブルーチンを設けなくとも、直接 isrSplashTask を変更すればいいだけだし、場所によってはそうしている場合もあって不統一になっている)
2. 画面下部に「*TAITO CORPORATION*」の文字列を PrintMessage で表示する
1996:1999(n/a)
199A:19BDCheckHiddenMes
19BE:19D0MessageTaito
19D1:19D6EnableGameTasks- suspendPlay フラグを On にする
- 解析者によるグローバル変数の名付けが紛らわしいが、On の時 play が On であって、suspend が On になるのではない
19D7:19DADsableGameTasks- suspendPlay フラグを Off にする
- 解析者によるグローバル変数の名付けが紛らわしいが、Off の時 play が off であって、suspend が Off になるのではない
19DB(n/a)
19DC:19E5SoundBits3Off
19E6:19F9DrawNumShips
19FA:1A05EraseSpareShips画面下部の残機アイコン表示領域を DrawSimpSprite で消去する
1A06:1A10CompYToBeam- 中期割り込み(ScanLine96)または vblank 割り込み(ScanLine224)でセットされる vblankStatus を利用する唯一の処理
- オブジェクトの X 座標が 0 ~ 127(画面左半分の位置)か、128 ~ 255(画面右半分の位置)なのかで判別する。中期割り込み時には vblankStatus に 0 がセットされているので、0 ~ 127の場合に True、vblank 割り込み時には vblankStatus に 128 がセットされているので、128 ~ 255 の場合に True になる。
1A11:1A31FleetSoundDeleyTableエイリアンの残数による行軍音のペースの対応表
1A32:1A3ABlockCopy- 各所で ROM → RAM や、RAM → RAM のブロックコピーで利用されている
- B レジスターのサイズのブロックを、DE レジスターのアドレスから HL レジスターのアドレスへとコピーする
1A3B:1A46ReadDesc
1A47:1A5BConvToScrドット座標を 8x8 のキャラクター座標へと変換する。つまり、論理的には 8 で割った値を返すだけ。しかし実際のコードは、1) 割算をシフトレジスターで行うために右シフトを 3 回で実装。2) 座標が格納される RAM の実際のアドレス(0x2400)がオフセットとしてハードコードされている(0x2000 はこのメソッド中で加算。0x400 の分は元々入力されるドット座標がオフセット込みで使われているという前提)ので、一見複雑な感じになっている。
1A5C:1A68ClearScreen- VRAM 領域(2400:3FFF)の計 7168 バイトを 0x00(黒)で埋めることで全画面を消去している
- DrawStatus でのみ使用する
- cf. ClearPlayField
1A69:1A7ERestoreShields- トーチカ一つを描画する
- B レジスターには 22、C レジスターには 2 がセットされた状態で呼ばれる。これはトーチカのスキャンラインに沿った方向に縦幅が 2 バイト分(16px)、スキャンラインの本数 22 本分の横幅であることを意味する
- HL レジスターで示された VRAM のアドレスに、その座標に対応する DE レジスターで示されたトーチカのスプライトデータを二重ループ(2 バイトずつを 22 回)でコピーしている
1A7F:1A92RemoveShip1. GetNumberOfShips によりプレイヤーの残機数を得て、0 機の場合はスキップ(RET)
2. 残機を -1 する
3. 残機数分の自機アイコンを DrawNumShips で表示
4. 残機数 + 1 の数字を表示
1A93:1A94(n/a)
1A95:1AA0
1AA1:1AA5ShotReloadRate
1AA6:1AB9MessageGOver
1ABA:1ACEMessageB1or2
1ACF:1AE3Message1Only
1AE4:1AFFMessageScore
1B00:1BFFRAM 初期値CopyRAMMirror により、0x1BBF まで(init 時以外)または 0x1BFF まで(init 時のみ)の範囲が RAM の 0x2000 以降のアドレスにコピーされるが、途中にスプライトなどの無関係なデータの領域が混っている。
1BA0:1BAFAlienSprCYA
1BC0:1BCFShot descriptor for splash shooting the extra "C"
1BD0:1BDFAlienSprCYB
1BE0:1BFFMore RAM initialization copied by 18D9
1C00:1C2FAlienSprA
1C30:1C5FAlienSprB
1C60:1C6FPlayerSprite
1C70:1C8FPlrBlowupSprites
1C90PlayerShotSpr
1C91:1C98ShotExploding
1C99:1CA2Message10Pts
1CA3:1CB7MessageAdv
1CB8:1CBBAReloadScoreTab
1CBC:1CBFMessageTilt
1CC0:1CCFAlienExplode
1CD0:1CDBSquiglyShot
1CDC:1CE1AShotExplo
1CE2:1CEDPlungerShot
1CEE:1CF9RollShot
1CFA:1CFDMessagePlayUY
1CFE:1CFF(n/a)
1D00:1D1FColFireTable
1D20:1D4BShieldImage
1D4C:1D4FTable of possible saucer scores
1D50:1D53Table of corresponding string prints for each possible score
1D54:1D63SaucerScrTabUFO の点数表。本来 16 個あるが、バグによって 15 個でループしている。自弾を発射するごとにローテーションする。
1D64:1D7BSpriteSaucer
1D7C:1D93SpriteSaucerExp
1D94:1D9FSaucSoreStr
1DA0:1DA2AlienScoresエイリアンの種類別得点表(10、20、30)
1DA3:1DAAAlienStartTable面数による 2 面以降のエイリアンの隊列の初期位置(Y 座標)の一覧表
- ちなみに 1 面は NewGame での初期化時の 12 で 0x78 = 120 がセットされる
1DAB:1DAEMessagePlayY
1DAF:1DBDMessageInvaders
1DBE:1DCETables used to draw "SCORE ADVANCE TABLE" information
1DCF:1DDFAlienScoreTable
1DE0:1DE9MessageMyst
1DEA:1DF3Message30Pts
1DFE:1DFFMessage20Pts
1CFE:1CFF(n/a)
1E00:1F4FCharacters
1F50:1F61MessageP1or2
1F62:1F73Message1Coin
1F74:1F7EDemoCommandsデモプレイ画面での自機の左右移動のパターンデータ
1F7F(n/a)
1F80:1F8FAlienSprCA
1F90:1F9BMessageCoin
1F9C:1FA8CreditTable
1FA9:1FAFMessageCredit
1FB0:1FBFAlienSprCB
1FC0:1FC8"?" の文字キャラクター
1FC9:1FD4Splash screen animation structure 3
1FD5:1FE0Splash screen animation structure 4
1FE1:1FF2Message2Coins
1FF3:1FF7MessagePush
1FF8:1FFF"-" の文字キャラクター

プロセス構造

基本的には中期割込と vblank 割込が交互に発生して合わせて 1 フレームの処理として続いていく構図だが、これらは割込であるから、一定のタスクを処理し終ったら消えるワンショット(一回限り)のオペレーションである。

一方、ソースコード中には、2 つの無限ループが存在する。1. スプラッシュ画面ループと、2. ゲームループである。これらはどういう影響をもたらすのであろうか?

まず、スプラッシュ画面ループはブート後の init の流れで突入するループなので、決して終わることのない、一番基底にあるプロセスである。このプロセスに対して、中期割込と vblank 割込が継続的に割り込んでくる。つまり、ゲームループが存在しない限り、中期割込と vblank 割込の処理が存在しない合間を縫ってスプラッシュ画面ループが断続的に回り続ける構図となる。

──かと思ってしまったのだが、ダイクストラが構造化プログラミングを学会で唱えて未だ 10 年経つか経たないかの時代、それもプログラミングの情報自体がそもそもごく専門的な狭い世界の時代だった時のことである。スパゲッティプログラムがむしろデフォルト形態だった機械語主体の融通無碍なプログラム構造の時代を舐めてはいけない。

コード全体で 02D0:02D3(GameObject0 用のルーチン)、076A:076D(スタートボタン待ちルーチン)、16E6:16E9(エイリアンに侵略された場合の処理ルーチン)の 3 箇所にのみ、LD SP, $2400(スタックポインターを 0x2400 に設定)して EI(Enable Interrupt: 割込を有効化)する操作がある。このスタックをリセットする操作により、割り込んだ側のルーチンは割り込み前の状態に戻れなくなる。つまり、init からの流れで行き着いたスプラッシュ画面ループは、省みられることなく、忘れ去られる。また、割り込む側のルーチンも、割り込む前の処理に復帰することを前提として特にプログラムはされていないということでもある。

しかし、ゲームオーバーしたら再びスプラッシュ画面は表示される。これは init からの流れで発生したループのプロセスに「リターンする」わけではなく、割り込みで発生したプロセスの流れで、スプラッシュ画面ルーチンの途中にジャンプすることによってなされる。02DA:02DD でゲームオーバーになった場合、166D に飛ばされる。166D:16E5 のブロックは最終的に 16E3:16E5 の命令で 0B89 に飛ばされるが、この飛ばされた先が、スプラッシュ画面ループのクレジット表示処理の段階なのである。

ゲームループの方は、vblank 割込処理からコイン投入時に派生して、スタートボタン待ちループを経由して、NewGame ルーチンへとジャンプし、NewGame ルーチンの末尾のブロックがゲームループとしてループする形となる。

つまり、状況に応じて、スプラッシュ画面ループかゲームループかのいずれかの状態でループしているが、いずれにしてもどこかのタイミングでスタックをリセットしたものが基底のプロセスとなり、後続の割込プロセスがワンショットの処理を終えるとそのループにリターンする。以後、02D0:02D3(GameObject0 用のルーチン)、076A:076D(スタートボタン待ちルーチン)、16E6:16E9(エイリアンに侵略された場合の処理ルーチン)のどれか 3 箇所の処理が発生すると、その処理を発生させたプロセスに基底プロセスの座を交代する。ちなみに 02D0:02D3 は GameObject0 用のルーチンの一つで、(デモではなく)ゲームプレイ中に自機の爆発アニメーションが終った時に実行されるものである。

ともかく興味深いのが、インベーダーゲームの段階でマルチプロセス構造のプログラムになっていることである。メインループと、フレーム毎に発生するワンショットの割込処理と、2 つの並行する処理の流れがある。

中期割込(サスペンドされておらず、デモプレイモードの場合)

  1. 自機以外の画面の左半分にある各オブジェクトを更新
  2. エイリアンのカーソルを次に進めておく

ゲームループ

  1. 自弾発射判定
  2. 自弾の命中時処理とエイリアンの隊列の折り返し処理
  3. エイリアンの残機の更新
  4. スコアの更新
  5. エイリアンが全滅したかどうかの判定(全滅なら面クリア処理してから NewGame の途中に戻る)
  6. スコアによって敵弾の発射頻度を調整
  7. 自機 1-Up の判定
  8. 敵弾スピードの調整
  9. 自弾の発射音の on/off
  10. 自機が被弾している場合は爆発音を鳴らす
  11. 自機 1-Up 音、行軍音の発音ペース、行軍音の音階のローテーション

vblank 割込(サスペンドされていない場合)

  1. 敵弾のタイマーをローリング弾に同期
  2. カーソルのエイリアンを描画更新
  3. 自機と画面の右半分にある各オブジェクトを更新
  4. UFO 出現タイマーの更新処理

ゲームループ(以下繰り返し)

サスペンドフラグ(0x20E9 suspendPlay)

ゲームループ中でエイリアンを全滅させると、0x09EF に飛ぶ(0x082B ~ 0x0831)。0x09EF ~ 0x0A58 は 0x0804 に戻ってゲームループに再入するが、冒頭の 0x09F2 ~ 0x09F5 で 0x20E9 (suspendPlay) というフラグをクリア(0 に)する。プログラム全体でもこのフラグをクリアするのはここだけである。

一方、このフラグをオンにするのは、EnableGameTasks というルーチンだが、このルーチンを呼ぶのは、0x0817 ~ 0x0819、0x16E0 ~ 0x16E2 の 2 箇所だけである。前者はゲームループに後者は突入するほぼ直前の処理で、後者はゲームオーバーになった場合にスプラッシュ画面のクレジット表示に飛ぶ直前の段階で呼び出されている。

つまり、エイリアンを絶滅させた場合に、新しいステージの画面を再構成する場合に限って、画面の更新をサスペンドするのがこのフラグの目的であることがわかる。画面を再構成している途中でエイリアンが動き出しては困るからだ。

エイリアンの隊列全体の当たり判定

自弾の命中相手を判定する処理において 0x14F7 ~ 0x1503(Asm, C)ではエイリアンの隊列全体の当たり判定を行っている。

隊列全体の当たり判定が true なら、さらにどのエイリアン個体に当たったのかを判別するルーチン(Asm, C)に入っていき、そこには若干のバグが孕んでいるようだ(Code Bug)。しかし、そもそもこの全体の当たり判定の方も、不可解なのである。

全体の当たり判定が false になる場合には、2 つの条件で判断され、2 つ目の条件は「爆発した自弾がリファレンスエイリアン(隊列の左下隅のエイリアン)の Y 座標の -6 以下にいる」というもので、これはわかる。1 つ目の条件が不可解で、「リファレンスエイリアンの Y 座標が、0x90 = 144 以上というものであり、1 面の一番高い場所からスタートする場合でも、120 なわけで、そこから 2 段下がった位置である。常に false になるわけで、意味ないのではないか──。

実はこれ、リファレンスエイリアンの Y 座標がマイナスになった場合を想定している。マイナスになると、一周回って 255 から値が下りてくることになる。つまり、この「リファレンスエイリアンの Y 座標が 144 以上」というゲームスタート時には有り得ない条件が発生するのは、まさしくそのタイミングなのである。

元々、2 つ目の条件でエイリアンではなく、「敵弾またはトーチカに当った場合」を確定しようとしている。しかし、リファレンスエイリアンが 1 周回った高い Y 座標にあるために「爆発した自弾がリファレンスエイリアンと同じかそれよりも下にいる」という条件を満たしても意味がない。それで 1 つ目の条件でそのようなケースは除外しているわけである。

つまり、ここでは、「確実にエイリアンの隊列に当たることは有り得ない場合」を除外するのが目的である。依然として、この先のルーチン(Code Bug)に進んだとしても、必ずしもエイリアンに当ったとは限らず、敵弾やトーチカに当っている可能性も残されている。

WrapRef

WrapRef はリファレンスエイリアンの座標と自弾の座標の差について、16 で割った値を求めて、命中したエイリアンの行列中の場所を求めようとしている。i8080 には割り算命令などないので、16 の割り算の商を加算の繰り返しによって求めるアルゴリズムを使っている。

コメント

このブログの人気の投稿

Mac → Mac のメールアカウントの移行

LiveData と MutableLiveData の使い分け

WireGuard の OpenWrt での運用