Chapter.12 フルアセンブラ化と自機弾発射処理と乱数発生ルーチン
[Chapter12 制作物]

今回は、BASICプログラムに若干残っている処理 "COLOR15,4,0:SCREEN4,2,0" をアセンブラに移動
してフルアセンブラ化する作業をメインに進めます。
さらに、前回、サボって自動連射機能にしてしまった部分をスペースバー/Aボタンによる発射に戻します。
ついでに、前回実験した乱数生成を、アセンブラプログラムにしていきます。


■COLOR 15,4,0:SCREEN4,2,0
COLOR 15,4 の部分は、実際のところ今回はあまり意味を持ちません。
BASIC の中で「色を省略したグラフィック描画の色(前景色)が 15 になりますよ」という意味と、
「初期の背景は 4 の色ですよ」という意味しかありません。
しかし、グラフィック初期化ルーチンで、すぐにグラフィックキャラクタに形状と色を付けてしまう
ので、背景色の指定は無意味です。それどころか、前景色に至っては、BASIC の命令で色番号が省略
された場合に BASIC が何色を自動的に設定するか、という BASIC の都合なので、効果を成しません。

COLOR,,0 の 0 は、グラフィックを表示出来ない外側の色なので、これは今回も意味があります。
しっかり設定しないと、外側の色が思惑と異なる色になってしまうことがあるからです。
確実にどの MSX で動作させても同じように見せるためには、この部分に相当する設定をしなければなりません。

MSX の BIOSワークエリアに下記のようなエントリがあります。

FORCLR = 0xF3E9
BAKCLR = 0xF3EA
BDRCLR = 0xF3EB

COLOR a,b,c とすると、FORCLR は a が格納されるアドレス、BAKCLR は b が格納されるアドレス、BDRCLR は c が格納されるアドレス
に対応しています。
ここは、BIOS が、「VRAM や VDP に何を設定したっけ?」と忘れないようにする備忘録のようなメモリなので、ここを書き換えただけ
では変化しません。
COLOR a,b,c と等価な結果を得るためには、CHGCLR か CLGMOD という BIOSエントリを呼び出す必要があります。
CHGCLR は、COLOR a,b,c の処理そのものです。FORCLR,BAKCLR,BDRCLR に記入してある値にあわせて VRAM や VDP を更新します。
しかし、今回はこれは使いません。CHGMOD の方を使います。
CHGMOD は、BASIC で言う SCREEN n に相当します。
a レジスタに画面モード番号をセットして call すれば、その画面モードに切り替えてくれます。
その切り替えの際に、CHGCLR の処理も実施してくれます。

さらに、SCREEN命令は スプライトサイズ・スプライト拡大の設定もできますが、CHGMOD の呼び出しだけでは対応できません。
スプライトサイズ・スプライト拡大に関する設定は、VDP の R#1 レジスタに設定します。
このとき、実際にレジスタをセットするのではなく、R#1 の内容を保存してある BIOSワークを書き換えて、CHGMOD に実際のレジスタ書き込みを
任せてしまうことにします。

RG1SAV = 0xF3E0

CHGMOD が全部の VDP レジスタへアクセスするわけではありませんが、画面モードに関する VDP 設定は R#0,R#1 などにあるため、
CHGMOD は、必ず R#0,R#1 の一部のビットを書き換えます。
しかし、VDP のレジスタはステータスレジスタ以外は読み出せない(書き込みオンリー)ため、BIOS は書き込んだ内容をメモリ上に
バックアップしているわけです。
一部のビットを書き換えるときには、そのバックアップに対して加工して、実際のレジスタへ書き込むのとバックアップを更新する処理を
実施しています。
したがって、R#1 の下位2bitがスプライトサイズ・拡大設定、その他が画面モードということは、そのバックアップを加工しておけば
自動的に CHGMOD がその部分を残して、画面モードに関する部分だけを書き換え、レジスタ設定してくれることがわかりますね。

もちろん、VDP にダイレクトにアクセスしても良いし、VDP(1) = xx に相当する BIOS エントリで書き換えてもかまいません。
やり方はいろいろあります。

これらを踏まえて、test_main.asm の game_init ルーチンに追加した初期化処理を プログラム1 に示します。

プログラム1 追加した初期化ルーチン
game_init:
		; COLOR 15,0,0
		ld		a, #15
		ld		(FORCLR), a
		xor		a
		ld		(BAKCLR), a
		ld		(BDRCLR), a
		; SCREEN 4
		ld		a, (RG1SAV)		; VDP(1) = (VDP(1) AND &HFC) OR 0x02		※スプライトサイズ 16x16, 2倍拡大無し
		and		#0xFC
		or		#0x02
		ld		(RG1SAV), a
		ld		a, #4			; SCREEN 4
		call	CHGMOD
		


■自機弾の発射処理
chapter.10 までは BASIC でトリガー判定して、自機弾の発射処理ルーチンを呼び出していました。
chapter.11 では、簡単にアセンブラ化するために、とりあえずトリガー判定ナシに、常に発射処理ルーチンを呼ぶようにしたため、
自動連射になりました。

今回は、自機弾発射ルーチンの頭に、トリガー判定処理を追加して、トリガーを押さないと発射されないように変更します。

MSX の BIOSエントリに GTTRIG (0x00D8) というエントリがあります。このエントリは、BASIC の STRIG(n) と等価なルーチンで、
Aレジスタに n に相当する値を入れて call すれば、Aレジスタに 0x00 か 0xFF が返ってきます。
0x00 の場合は "ボタンは押されていない" を示し、0xFF の場合は "ボタンが押されている" を示します。
n = 0 の場合がスペースバー、n = 1 の場合が Aボタン なので、それらの結果を得るために 2回呼び出すことになります。

GTTRIG エントリは、AF, BC あたりのレジスタを破壊しますので、1回目の呼び出しの結果は Dレジスタに退避することにします。
(スタックを使っても良いですが、スタック操作関連は遅い命令なので、使わなくてすむ場合は使わないようにします)

プログラム2 追加した初期化ルーチン
shot_fire::
		; 発射するタイミングか否か調べる
		xor	a							; STRIG(0) の 0 を Aレジスタに代入する
		call	GTTRIG						; STRIG(A) の状態を得る
		ld	d, a						; 結果を D レジスタに退避
		ld	a, #1						; STRIG(1) の 1 を Aレジスタに代入する
		call	GTTRIG						; STRIG(A) の状態を得る
		or	d							; A = A OR D (これは A = STRIG(1) OR STRIG(0) に相当する)
		ld	d, a						; 結果を Dレジスタ に退避
		ld	a, (shot_last_trigger)		; 前回のトリガ判定結果を Aレジスタ に読み込む
		or	a							; [*1] 前回のトリガ判定結果は 0x00 であるか? (前回ボタンが押されていたか?)
		ld	a, d						; 今回のトリガ判定結果を shot_last_trigger に上書きする	※フラグ不変
		ld	(shot_last_trigger), a		; ※フラグ不変
		ret	nz							; 上の [*1] の判定結果が、「前回ボタンが押されていた」なら何もせずに return
		or	a							; 今回のトリガ判定結果が 「ボタンは押されていない」か?
		ret	z							; 「ボタンは押されていない」なら何もしないで return

		(以下、これまでの自機弾発射処理)

shot_last_trigger:
		.db	#0							; 前回ボタンが押されていたかどうかを覚えておくための作業用変数
		


■乱数発生ルーチン
前回、乱数発生の演算を、PC上のプログラムで確認しました。
そのときの演算式は下記のようになっていました。

last_random = (((last_random >> 1) | (last_random << 15)) ^ (~last_random & 0x1529)) & 0xFFFF;
これを、そのまま Z80 のアセンブラっぽく書くとプログラム3 のようになります。

プログラム3 乱数発生ルーチン(幻想)
		ld	hl, (last_random)
		ld	de, hl
		rrc	de
		cpl	hl
		and	hl, #1529
		xor	hl, de
		ld	(last_random), hl
		


すごく短い!やった!・・・なんて、世の中甘くないです。
プログラム3 の中で、最初と最後以外の命令は、すべて存在しない命令です。

所詮は 8bit CPU なので、残念ながら 16bit そのままの演算命令は、あまり充実していないのです。
なので、8bit に分解して処理することになります。

で、実際に作ったプログラムは プログラム4 のようになってます。

プログラム4 乱数発生ルーチン
random::
		ld	hl, (rand_seed)
		ld	b, h
		ld	a, l
		; HL = HL >> 1
		srl	h
		rr	l
		; L = L xor ((not a) and 0x29)
		cpl
		and	#0x29
		xor	l
		ld	l, a
		; H = (H xor ((not b) and 0x15)) or ((b << 7) & 0x80)
		ld	a, b
		cpl
		and	#15
		xor	h
		ld	h, a
		ld	a, b
		rrca		; bit0 を bit7 へ移すために右1bit回転を使う
		and	#0x80
		or	h
		ld	h, a
		ld	(rand_seed), hl
		ret
		


さて、これは本当にあっているのでしょうか?
実は、まだ全く動かしていないので、正しいかどうかは不明です。
動作確認に関しては、次回以降に。


[▲トップページへ]