縦画面シューティングをつくる


img
前回は P6 を酷使したつもりが意外と余裕を見せられて作った本人が驚くというグダグダなオチでしたが、
今回は趣向を変えて、P6 でゲームを作る際の暗黒面にスポットを当ててみたいと思います。

そう、PC-6001mk2 というマシンは実際の労働時間を大幅に誤魔化しているブラック PC なのです。
そんなマシンで「何故作ろうと思い至ったのか」「これを目の当たりにして何故大丈夫、いけると思ったのか」
数々の名移植に思いを馳せながら、作り手のメンタルの強さを検証してみたいと思います。

企画名をタテスカウォーズにしようかと思ったのですが、特にスクロールとかしないのでやめました。



■ た・て・つ・く


画面はイメージです。


PC-6001mk2 というマシンはと・て・も遅いです。
公称 4MHz の CPU は一見すると MSX の 3.5MHz よりも速く思えます。
実際 CPU は 4MHz で動いているのですが画面を表示している間は動作を停止するので、実質 1.6MHz 程度の速度になってしまいます。

6001 でゲーム制作を志した人は BASIC で GET@,PUT@命令(画面にキャラクターを描く)を使ってみて「あ、これはダメだな」と思います。
ちょっと諦めの悪い人はマシン語を覚えてアレコレしてみるも、他機種と比べてあんまりな速度に次々に脱落していきます。それほど遅い。

こうした性能にも関わらず、 P6mk2 にはアクションゲームの素晴らしい移植作品がいくつもあります。
正直「どうやって移植したのか」よりも「何故移植できると思ったのか」が知りたい!
これは実際に制作過程を追体験してみるしかないですね。


・成果物公開
nyaha60_20130217.zip (P6mk2用1Dディスクイメージ+ソース)
いろいろ改造しているうちにこうなりました。
ラーメンを作っていたらチャーハンになった、みたいな感じでしょうか。

時間内に全12面をクリアするゲームです。ステージが進むごとに敵が固くなったりします。
98 時代の有名なフリーソフトに敬意を表して NyaHacks60 と命名。Hax ではなく Hacks。ボスと音楽が無いのが残念なり。
ステージクリアで時間が少し加算されるほか、ある敵を倒してもボーナス時間が得られます。
ゲーム中 stop キーを押すとギブアップできます。

9 面が鬼門。クリア時に 60 秒残っていれば大丈夫かも。
バグ技を使えば…。

■ 6001 の掟

目標としては、40 以上のキャラクタが画面上で常に動き続け、エフェクトや効果音を交えながら
「そこそこ遊べる」レベルの速度で動作するアプリケーション、ということにします。具体名は略。

第一歩を踏み出す前にいくつか確認(覚悟?)しておかなければならないことがあります。

1)律速を諦める

普通のゲームマシンでは vsync などのタイミング信号を待ってキー入力や画面の更新、音楽の演奏などを行います。
フレームという概念とゲームの進行が密接に結びついています。

対する P6 は遅すぎて CPU が遊んでいる余裕などありません。
常に全力疾走で、キー入力を拾ったら画面の更新をしてゲーム処理の合間に音を鳴らしたら次のキー入力を拾いに行きます。
処理の負荷次第で速度が変わりそうな気がしますが、タイミングを合わせるために遅いシーンに合わせて空ループを挿入して調整します。
具体的には、敵が 1 匹死ぬごとに同等の負荷をかけて処理時間の平均を保ちます。

2)解像度と色数を諦める

160x200 の 16色というスペックではあるのですが、1 ドット単位で動かしていては指さしで追えるほどの速度になってしまいます。
色数もフルに使えるとは限りません。ある程度の妥協をしてでも速度を優先します。
動きはガタガタになりますが上位機種の 88 でもこうした妥協はあったので諦めが肝心です。

3)リッチな表示を諦める

上に関連しますが、スプライトなどという高級な機能はありませんので綺麗な重ね合わせは諦めます。
同様にパレット機能などもありません。背景の星ですら描いては消しを繰り替えさないと実現できません。

マスクパターンを取ってキャラクタを描画すれば重ね合わせ可能ですが、40 ものキャラクタに P6 の処理能力では
あまりに厳しすぎます。というわけで、漢らしく次々上書きして、欠けたら欠けっぱなしでいくことにします。

4)綺麗なプログラムを諦める

関数の入り口と出口が 1 つずつ、みたいな考え方を窓から投げ捨てます。
命令とワークエリアが入り混じってカオスな状態になっても怯んではいけません。
でもなるべくコメントは残しておきたい。

5)使えるものは使う

たとえば、音に関しては他機種と遜色ないので(out 命令にウェイトが余計にかかるとはいえ)速度面に目をつぶれば
結構いいものが出来そうです。その他、メモリの許す限り、入れない理由が無いものは全部入れてしまいましょう。


■ キャラクターを(執拗に)描いてみる

ゲームの高速化にはコードをカリカリ弄るよりも、ゲームのロジックやアルゴリズムを改良した方が手っ取り早く、
効果も高い事が多いですが、P6 レベルのマシンパワーでは、実行頻度の多いループの最深部の高速化も避けては通れません。
今回はキャラクターの描画に注目してみます。

40 以上のキャラクターがそれぞれ画像データ数十バイトを VRAM に毎フレーム転送するので全体ではかなりの負荷になりますが
ここが高速化できれば全体の速度がぐっと上がります。

P6 にはスプライトなどという高級なものはありませんので、描画と同じように消去も自前で行わなければなりません。
画面外にはみ出したらメモリの VRAM 範囲外にそのまま誤爆してしまいますし、キャラ同士が重なってもベタで上書きされます。

しかし今回は仕様としてキャラクタは常に動き回っており静止しているものはありません。動き回っているということは
キャラクタが重なっても常に更新され続けているので欠けて見えるのも一瞬のことで大して気にはなりません。
こうした仕様も味方にして高速化できるところを考えていきます。

まず、キャラクターの仕様から。
「キャラクターを高速に描画する」の前に「高速に描画できるキャラクターを考える」ことが肝心です。



P6 の Screen Mode 3 は 160x200ドット 16色です。
図のような 1 バイトにつき 4 ドットの横長ドットで、160 ドットは横に 40 バイトの並びになります。
VRAM(L)には 16 色の下位のみが 40*200 バイト並び、VRAM(H)は上位のみです。

1 バイトが 4 ドットなので、横 8 ドット(2バイト) * 縦 16 ドット(16バイト)あたりがキャラクタとして適当なサイズです。
しかし、横 8 ドットキャラを左右に動かすと、3 バイト書き込みになる場合があり描画処理が複雑になります。



そこで 8*16 キャラはあっさり諦めます。代わりに 6*12 キャラで考えてみます。


これならはみ出さずに済みます。上の状態からさらに右に 2 ドット動かしたとしても左側の状態に戻るのが分かると思います。
ただし、横 6 ドットでキャラクタを表現する難易度はかなり高いです。

先ほどから「右に 2ドット動かす」と書いていますが、なぜ 1 ドット単位ではないのか。
答えは「遅いから」です。かと言って、4 ドット(半キャラ)単位で動かすとカクカクしすぎてアクションゲームとしては致命的です。
6*12でも 1 ドットずつ動かすと 3 ドット動かした際にはみだす部分が出来る点もデメリットです。



ちなみにこちらが 1 ドットずつ動かした例。ヒゲは 8 体です。変換後の GIF が 10 fps なので全然なめらかではないですが。
案外実用的?と思われるかもしれませんが、全力でやってこの速度です。ただし描画最適化なし。

それから、当然ながらキャラクタは右にシフトした状態としていない状態の 2 種類作ってメモリに配置しておきます。
描画する直前になってシフトしていたら高速化の努力が台無しなので。描画するときに x 座標の偶数奇数で描き分けます。

さて、キャラクタのサイズも決まったところで VRAM に転送するわけですが、転送先の VRAM アドレスはどうやってきまるかというと
VRAM = 0x4000 + (y * 40) + (x / 4)
が VRAM(L) で、VRAM(H) は 0x4000 が 0x6000 になります。(x=0〜159 y=0〜199 の場合)

しかし、先ほど横に 2 ドットずつ動かす(=ドット縦横比から縦には 4 ドット)と決めたので、x と y の範囲は(x=0-79 y=0-49) となります。
実画面解像度(160*200)とゲーム画面解像度(80*50)の違いですね。これを適用すると VRAM アドレスは
VRAM = 0x4000 + (y * 40 * 4) + (x / 2)
となります。80*40 だとアイコンを縦横 2 つ並べたら隠れてしまう程度の解像度ですが、それでも半キャラずつ動かすよりはマシなのです。

x の方は 2 で割る部分は右シフト 1 回で代用できます。
これに対して面倒なのは y*40*4 の部分で、Z80 には掛け算はありませんので、シフトと足し算を組み合わせて 160 倍することになります。
どうせ y の範囲は 0〜49 しかないので、ここはテーブル化して y*40*4 の結果をあらかじめ持っておくことにします。
テーブルは全部で 2*50=100 バイト。アドレスを求める部分も出来るだけ速い方が良いでしょう。

GETADR:
    LD          L,E                 ;E=Y(0-49) D=X(0-55) とする 
    SLA         L                   ;2倍
    LD          H,XYTABLE >> 8
    LD          A,D
    RRA                             ;x>>1
    ADD         A,(HL)              ;桁上がり無し保障
    INC         L
    LD          H,(HL)
    LD          L,A
    RET

XYTABLE:
    DW  0*40*4, 1*40*4, 2*40*4,  (略)

テーブルはアドレス xx00 から始まる位置に配置してテーブル参照にかかるコストを最小化します。
このルーチン自体も RST 命令で呼び出せる配置にするのも手ですが、短いので描画ルーチンに組み込んでしまう方がいいかもしれません。

今回はフィールドの幅を 0〜55 に制限します。この範囲でなら同じ y 座標の中でなら桁上がりが起きないので
(x>>1) を足す部分を 16bit 加算でなく 8bit 加算に出来ます。以下、ゲームフィールド外は青い部分で示します。

さて、アドレスも求まったことですし実際に描画してみます。
ここでちょっと話を端折って、いきなり pop-ld 転送でいきます。ldi など 1 バイトずつ転送する方法は速度面で不利なので。

DRAW:                       ;HL=キャラデータ DE=VRAMアドレス 
    LD      BC,39
    DI
    LD      (.STACK+1),SP
    LD      SP,HL
    EX      DE,HL           ;SP=CHR HL=VRAM

    REPT    12
    POP     DE
    LD      (HL),E
    INC     HL
    LD      (HL),D
    ADD     HL,BC
    ENDM

    LD      DE,0x2000-40*12
    ADD     HL,DE

    REPT    12
    POP     DE
    LD      (HL),E
    INC     HL
    LD      (HL),D
    ADD     HL,BC
    ENDM
.STACK:
    LD      SP,0000         ;ここに直接書き込み
    EI
    RET



pop-ld 転送というのは、pop 命令が 1 命令で 2 バイトずつメモリから値を読み出せる(しかも読み出し位置は自動更新)ことを利用して
高速に転送を行う処理です。ldi 命令のように hl,de,bc をフルに使ったりしないのでレジスタにも余裕ができます。
反面、スタックポインタを使う関係上、その処理中には call 命令等は禁止、割り込みも入らないようにしなければなりません。

上の例では長くなるのでまとめてしまっていますが、ループ回数は 12 でなく 11 にして 1 回分を外に追い出し add hl,bc を省きます。

さて、ここまでは基本。ここから削れるかどうかがこの章の主旨です。

VRAM(L) から VRAM(H) へ書き込み先を変更する処理は ld de,0x2000-40*12 と add hl,de で 2 命令 10+11=21 ステートですが、
よく考えると VRAM(L) を左上から右下へ向かって書いたのであれば、そのアドレスから VRAM(H) は右下から左上に戻ればいいですよね。
というわけで set 5,h と ld bc,-39 に書き換えて 8+10=18 ステートに縮まりました。わずか 3 ステートでもやらないよりマシ。
ただし、VRAM(H) に書き込むデータは下から上になるようにフォーマットを変更しなければなりません。

ここで bc は 39 のままで sbc hl,bc にすれば…と思うかもしれませんが、add が 11 ステートなのに対し sbc は 15 なので相当遅くなるのです。

まだ縮まるところは無いでしょうか。
キャラクタは横に 2 バイトを縦に 12 回書き込むのですが、縦 12 回の中で横 2 バイトの inc hl が繰り上がらない回が無いか
全座標(0〜55,0〜49)について調査してみます。すると、キャラクタの line=0,4,8 の描画時は繰り上がりが絶対に発生しないことが分かりました。

つまり 24 回(12回*L/Hの2回)ある inc hl の中の 6 回は inc hl を inc l に変更できるということです。
これで 6 ステート * 6 を 4 ステート * 6 に減らせます。合計で 12 ステートの高速化。
さきほどのと合わせて 1 キャラにつき 15 ステートですが、40 キャラ描画すると 600 ステート分浮きます。結構デカい。

これで満足…していると音楽や演出を付け足していったときに残念なことになります。
キャラクタが少なければ大丈夫なのですが、どうしても速度が足りない場合はキャラクタの仕様の方を変えてしまう方法もアリです。



キャラクタ仕様を 6*6 ドットに変更します。
6*6 ドットといっても、横長ドットを 2 ライン続けて同じものを描くという意味なので、見かけ上のドット比は 1:1 になります。
ドット打ちがさらに職人芸を要求するようになります。文字ですら 8x8 ドットとかなのに…。

DRAW:                          
    LD          BC,39
    DI
    LD          (.STACK+1),SP
    LD          SP,HL
    EX          DE,HL

    REPT        3
    POP         DE
    LD          (HL),E
    INC         L
    LD          (HL),D
    ADD         HL,BC
    LD          (HL),E
    INC         HL
    LD          (HL),D
    ADD         HL,BC
    POP         DE
    LD          (HL),E
    INC         HL
    LD          (HL),D
    ADD         HL,BC
    LD          (HL),E
    INC         HL
    LD          (HL),D
    ADD         HL,BC
    ENDM

    SET         5,H
    LD          BC,-39
	VRAM(H) は(略)
.STACK:
    LD          SP,0000
    EI
    RET

6*6 キャラとこれまでの改良点を盛り込みました。ループ回数は 3 回ではなく(同上・以下略)
pop 1 回で実質 4 バイト分読み込んでいるようなものです。VRAM(H) は省略していますが、書き込む順序は d->e かつ dec hl になります。

何?まだ遅い? うーむ仕方ない。。。
縦 2 ラインに同じものを描くのであれば、どちらか欠けていてもちょっと暗くなる程度だよね?横長ドットに戻るだけだよね?と自分を納得させつつ
スキャンライン描画方式を導入してみます。描画処理 12 ラインのところを 1 ライン飛ばしで 6 ラインにしてしまいます。

DRAW:               
    LD          BC,40+39
    DI
    LD          (.STACK+1),SP
    LD          SP,HL
    EX          DE,HL
.PATCH:
    REPT    3
    POP         DE
    LD          (HL),E
    INC         L
    LD          (HL),D
    ADD         HL,BC
    POP         DE
    LD          (HL),E
    INC         HL
    LD          (HL),D
    ADD         HL,BC
    ENDM

    SET         5,H
    LD          BC,-40-39
            VRAM(H) は(略)
.STACK:
    LD          SP,0000
    EI
    RET


残骸がチラついているのは GIF 化が原因。

描画と同じくらい大事なのが消去です。描画と同じ回数だけ発生しますので、こちらも手を入れれば入れた分だけ速くなります。
こちらも、繰り上がりの無い VRAM アドレスは 8bit インクリメントに変えて高速化します。

CLEAR:
    XOR         A
    LD          DE,-0x2000+40*2 

    REPT    3
    LD          (HL),A
    INC         L
    LD          (HL),A
    SET         5,H
    LD          (HL),A
    DEC         L
    LD          (HL),A
    ADD         HL,DE

    LD          (HL),A
    INC         HL
    LD          (HL),A
    SET         5,H
    LD          (HL),A
    DEC         HL
    LD          (HL),A
    ADD         HL,DE
    ENDM
    RET
    ENDIF

以上、スキャンライン版。
消し方は描画の時と違い、VRAM(L) と (H) を交互に消していった方が速くなります。
pop-ld と同様に push を使って消す方法もあるのですが、2 バイト程度ではこちらのほうが速いようです。

その他、重要度の低いエフェクトなどのキャラクタは 16 色使って描かなくても良いものがあります。
そういったキャラクタは VRAM の (L) か (H)、どちらか片方のみを使用し、描画回数を半分に削減します。
色数が 16 色から 4 色に制限されますので、ここもドット打ち技術の見せ所(という名のしわ寄せ)。



他にもフォント専用の描画処理など、まだまだいろいろあります。
上のエフェクトにしても専用の描画処理を組むので、最終的に消したり描いたりする処理が沢山できあがります。
その中で、速くなくて良いものやサイズを気にしなければならないものなど、用途に合わせた作りにしないと
P6mk2 というマシンで満足に動かすのはなかなか難しいのです。

「ガントレット」が出来るほど大量に動かせるといいんですけどねぇ。


■ テープローダーを作る

テープの処理は基本的にサブ CPU 任せになります。
サブ CPU にロードまたはセーブを指示した後は、サブ CPU から割り込みがかかるのを待って
割り込み処理内でデータをメモリに取り込んだりテープに出力したりします。

1)割り込みベクタ(0x08)にテープ割り込みアドレスを登録。ここがロード時に呼び出される。
2)他にテープ中の STOP キー押下検出(0x0E,0x10)と CMT エラー時の割り込みベクタ(0x12)にも必要な処理アドレスを登録。
3)サブ CPU に 0x1E,0x19 を出力。1200 bps でのロードを指示する。
4)I/O ポート 0xB0 の bit3 に 1 を出力してモーターを ON にする。
5)割り込みが入るたびにサブ CPU から 1byte 取得してメモリに書き込む。ロード最終アドレスまで続ける。
6)サブ CPU に 0x1A を出力してテープ処理をクローズする

という流れです。Z80 はロード中は割り込み処理以外、何もしていないので他の処理に回すことが出来ます。

ただし、サブ CPU からの割り込みを取りこぼしてしまうのはまずいので、他の割り込み処理が入った際は
割り込み禁止時間をなるべく短く抑えなければなりません。
他の割り込み要因としては、タイマ割り込みなどがあります。キー入力は考えなくてよいです(サブ CPU は CMT 処理に専念)。
しかし音楽を演奏する場合などはタイマでテンポのカウントをしたいところ。

そのため、タイマ割り込みも「割り込みが入った」というフラグだけ記録しておいてすぐに割り込み禁止を解除します。
あとはメインループ中(ロード終了までの空ループ)に上記フラグを監視する処理を追加してフラグが立っていたら演奏処理をします。
普通にタイマ割り込み時に即割り込み許可して演奏、でも良いのですが、演奏終了までに重複して
タイマ割り込みが入ると面倒なのでフラグ管理の方が良いと思います。

もう一つ、メモリにそのままロードするのではなく、加工しつつ展開するというロード方法もあります。
VRAM にロードするときに左上から右下でなく、櫛状にロードされたりしたほうがカッコイイですね。

この場合、ロードされたデータを 1 バイトずつ扱っていると、そのデータを加工している間に次の 1 バイトが来てしまったりして
取りこぼしエラーの原因になります。これを解決するために、割り込み処理と加工処理で共通して使うバッファを設けて、
割り込み処理はロードしたデータをバッファに書き込み、加工処理はバッファから値を拾うような間接的なやりとりが有効です。

その他注意点
テープからのロード時には、ヘッダをつけておいた方が良いかもしれません。
たとえば、BASIC の場合は 0xD3 が 10個連続した後にファイル名 6 バイトが続きその後本体の読み込みとなります。
CLOAD 処理では 0xD3 が 10連続するのを検出してから本体をメモリに展開する処理になっているので、
誤ってテープの途中からロードしてもプログラムの開始位置の誤認を防止することができます。

マシン語の場合は、自前のヘッダ検出処理次第なので別に 0xD3 でなくても(むしろ BASIC が誤認しないよう 0xD3 でない方が良い)
10 連続でなくても好きに仕様を決めて構いません。

さらに、CLOAD は 0x00 が 10個連続していると、ロードすべきプログラムの終端とみなします。
BASIC のテープバイナリを直接アセンブラで出力する場合などは、終端に 0x00 を並べることとプログラム中に 0x00 が連続しない工夫が必要です。
また、この仕組みを逆用して本来の BASIC 終端の 0x00 を消してマシン語データなどをくっつけると
CLOAD で BASIC + マシン語を一括ロードできます。市販ゲームで BASIC が 1行しかないのに妙にロードが長かったりすると大抵コレです。



tapetest_mode5page1.zip

簡単なサンプルプログラム(ソース付)を置いておきます。
BASIC にマシン語をくっつけてあります。
テキトーな音を鳴らしながらヘッダ付バイナリを読み込つつ画像を表示していきますが、画像データは VRAM の下位と上位を交互に埋めていきます。
この程度なら割り込み処理の中で VRAM アドレスを交互にスイッチしても全然間に合うとは思いますが。。。

試していないので、実現できるかどうかは分かりませんが、テープをロードしながら音声合成 LSI にデータを流し込んで
喋らせるということもできるかもしれません。メモリマップとか割り込み禁止期間とか相当気を使いそうですが。


■ 当たり判定のはなし

よくある矩形判定ですが、大量の敵、大量の弾を扱うのであれば出来る限り速くしておきたいところ。
そのため、まず考えなければならないのは判定の必要があるかどうかです。
自機が横方向にしか動かないのであれば、先に Y の判定を行い、該当しない対象を弾いてしまった方が良いでしょう。
このあたりは横スクロールか縦か、とか画面の縦横比などの条件次第でも変わってきます。

判定は分かりやすくするために、まずは X 座標のみで考えます。


x3<=x2 かつ x1<=x4 を当たりと判定するとよさそうですね。
キャラクタは大抵左上の座標で管理しているでしょうから図の例では x2=x1+4, x4=x3+4(キャラクタの横サイズ-1)となります。
そうすると、x3<=x1+4 && x1<=x3+4 が当たり。
つまりx3-x1<5 && x1-x3<5ということになります。

HitCheck:               
    LD     A,(X1)
    LD     E,A
    SUB    A,(IX+X3)
    JR     C,.NEXT
    CP     5
    RET    NC
.NEXT:
    LD     A,(IX+X3)
    SUB    E
    JR     C,.HIT
    CP     5
    RET    NC
.HIT:

判定の対象によってキャラクタのサイズが違ったりしますが、基本的にはこんな感じ。
x3-x1 あるいは x1-x3 のいずれかで負数が発生しますが、負数が発生した時点で 5 未満は明らかなので次の判定に進んでOKです。
が、もうすこしなんとかしたい。

HitCheck:               
    LD     A,(X1)
    SUB    A,(IX+X3)
    JR     C,.MINUS
    CP     5
    JR     C,.HIT
    RET
.MINUS:
    NEG
    CP     5
    RET    NC
.HIT:

要するに絶対値で比較するということですね。abs(x1-x3)<5 で当たり、と。
同じ cp が 2回あるのが格好悪い?

HitCheck:               
    LD     A,(X1)
    SUB    A,(IX+X3)
    ADD    A,4
    CP     9
    RET    NC
.HIT:

両方の X を sub した結果が±4 以内であれば当たりなので、結果を +4 すると、絶対値を求めなくても 8 以下なら当たりとみなせます。
いわゆるゲタを履かせるという方法ですね。
こういうのは BASIC か何かで動かしながら実際に書いてみると分かりやすいです。

最初に書いた通り、先に判定が必要ないものを除外した方がよいので、当たりそうにないもの(動いていないオブジェクトなど)は
判定しない工夫が必要です。先入観があると条件分岐を多用するのはタブーみたいに思われがちですが、
論理演算やビットをこね回すよりも、案外分岐したほうが速かったりするときもあります。
今時の CPU ではないのでキャッシュが詰まるとか考えなくて良いですしね。


■ 疑似乱数

メガドラ姉さんの所でクロサワ式乱数というのが紹介されていたので
こちらも Z80 で使えるような使えないような、よくわからない乱数ルーチンを紹介したいと思います。
タネは Wikipedia の "LFSR"(Linear Feedback Shift Register)です。

詳しいことは上記 Wikipedia (フィボナッチLFSR)にあるのですが、要するに
1)初期値を決める(仮に 0xACE1 とする)
2)bit 位置 0(最下位ビット)と bit 位置 2 を xor する
3)2)の結果と bit 位置 3 を xor する
4)3)の結果と bit 位置 5 を xor する
5)初期値を右に 1bit シフトする
6)3)結果を5)の最上位ビットに入れた結果を次の乱数値とする

ソースも示されているので引用ー。
 uint16_t reg = 0xACE1;
       uint16_t bit;
       while(1)
       {
               bit = (reg & 0x0001) ^
                    ((reg & 0x0004) >> 2) ^
                    ((reg & 0x0008) >> 3) ^
                    ((reg & 0x0020) >> 5);
               reg = (reg >> 1) | (bit << 15);  
               printf("%04X\n",reg);
       }

うーん、何回演算すればいいんだって感じですね。Z80 で愚直に書いてみましょうか。

RAND:
    LD     HL,0xACE1
    LD     A,L
    ADD    A,A
    ADD    A,A
    XOR    L                ;bit0 XOR bit 2
    ADD    A,A
    XOR    L                ;(bit0 XOR bit2) XOR bit3
    ADD    A,A
    ADD    A,A
    XOR    L                ;((bit0 XOR bit2) XOR bit3) XOR bit5  
    AND    %00100000
    ADD    A,A
    ADD    A,A
    ADD    A,A              ;cfに持ってくる
    RR     H
    RR     L                ;16bit 右シフト
    LD     (RAND+1),HL
    RET

馬鹿正直に右シフト (SRL A) で書いてみたのですがあまりにもアレなので左シフト (ADD A,A) に変えました。
論理を目で追っていくと、結局 XOR で結果が 0 か 1 かコロコロ変わっているだけとわかると思います。それにしても冗長ですね。
もうすこし考えてみると、最終的な 0 か 1 かは演算に使うビット(タップ位置)の 1 が偶数個あるか奇数個あるかに掛かっていると気づくはずです。
0,1,0,1 の時は最終的に 0 に、1,0,1,1 の時は最終的に 1 が得られます。
ということで、偶数奇数のところでピンと来たら話は簡単です。

RAND:
    LD     HL,0xACE1
    LD     A,L
    SRL    H
    RR     L
    AND    %00101101
    JP     PE,.SKIP         ;Parity Even  
    SET    7,H
.SKIP:
    LD     (RAND+1),HL
    RET

Z80 にはそのものずばり、演算した結果 1 になった bit の数が奇数か偶数かを得られるパリティフラグがあるのでこれを利用します。
ちなみに上の例では元のネタにならって 16bit 右シフトしていますが、左シフトなら ADD HL,HL で短縮できます。
その際には結果を入れるビット位置は最下位になります。これを ADC HL,HL にできればさらに短く。

乱数のバラツキに関してはお察しなので、適当に 16bit の真ん中あたりから必要な bit 数だけ拾って使うのがよろしいかと。
ただ Z80 でそんなに高速に疑似乱数を得ないといけないのがどういうときなのか分からないので
フツーに LD A,R 使えばいいんじゃないの?という結論になるかも。

乱数はいまだにアルゴリズムが改良されていたりして面白いですね。


■ デバッグにプリンタを使う

といってもエミュレータの話です。
ある処理をテストしてみたい時、たとえば上に挙げた疑似乱数が一巡するまでの全数値を吐き出してみたい時など
64KB のメモリ空間やディスクでは足りない場合があります。こういう時、外部にファイルとして出力してくれるプリンタ出力機能が便利です。

プリンタは I/O ポート 0x91 に 1 バイトを出力した後、0x93 からストローブ信号でプリンタに通知します。
    CPL                ;反転 
    OUT     (0x91),A   ;出力
    LD      A,1
    OUT     (0x93),A
    XOR     A
    OUT     (0x93),A
プリンタに出力する 1 バイトは論理反転しておく必要があります。
ストローブ信号は立ち下がり(立ち上がりかも)をトリガーとするので 1 -> 0 と連続出力します。

エミュレータでは printer.txt というファイルが出来ます。追記なので一旦消しておいた方が良いかも。
プリンタの busy が 0xC0 の bit1 に出ていますが、これは無視して良いようです。


■ あとがき

ゲームも「遊ぶ・作る」から「見る」にシフトしていって、懐古趣味もその流れで段々変化しているように思いますね。
危機感というか問題意識はずっとあるのですが、この記事がそれに対する回答になっているかというと微妙なところです。


▲ TOP