3D ダンジョン マシン語版(後編)

ここからは Z80 コードの解説です。 全てを解説すると長くなるので、特に注意が必要な部分に絞ります。

Z80 の命令を覚えていないと読むのが大変だと思いますが、 少しずつ覚えていってください。 マシン語プログラミングに一番必要なのは根気です。




文字表示

PC-8001 のテキスト RAM は 1 行あたり 120 バイトで、N-BASIC では $F300 から配置されています。


テキストRAM

文字は横 40 文字モードの場合は 1 バイトおきに使用されます(奇数アドレスは未使用)。 アトリビュートは常に 40 バイト使用されます。

今回はマシン語側ではアトリビュートの操作は行いません (BASIC 側で初期化したまま)。もし色を付けたりしたい場合、ROM 内ルーチンを利用すると楽です(詳しくは外部リンクを参照)。

さて、まずは XY 座標からテキスト RAM アドレスを求めるサブルーチン CALC_VRAM_ADDR を作ります。今回は横 40 文字モードなので、アドレスは $F300+Y*120+X*2 となります。

Z80 には乗算命令が無いので、乗算は毎回ちょっと苦労します。 「120 倍=128 倍-8 倍」と考えて、次のコードになりました。

VRAM	EQU $F300

; in : H=X, L=Y
; out: HL=VRAM addr
; brk: BC,AF
CALC_VRAM_ADDR:
	LD B,0
	LD C,H
	SLA C		; BC=X*2
	LD A,L
	LD H,L
	LD L,B		; HL=Y*256
	SRL H
	RR L		; HL=Y*128
	ADD HL,BC
	ADD A,A
	ADD A,A
	ADD A,A
	LD C,A		; BC=Y*8
	SBC HL,BC	; 最後の ADD A,A で CF=0
	LD BC,VRAM
	ADD HL,BC
	RET

ここで一つ注意があります。BASIC ではカラーモードにした場合に左端 1 文字が使えませんが、上記の計算では考慮していないので、X 座標が 1 ズレます

今回は全ての X 座標にその都度 1 足しましたが、このサブルーチンで 1 足した方が良かったかもしれません。

テキスト RAM アドレスを求めることができたので、次は 1 文字表示ルーチン PUT_CHAR と文字列表示ルーチン PUT_STR を作ります。

; in : H=X, L=Y, A=code
; out: HL=VRAM addr
; brk: E,BC,AF
PUT_CHAR:
	LD E,A
	CALL CALC_VRAM_ADDR
	LD (HL),E
	RET

; in : H=X, L=Y, DE=string addr
; out: HL=next VRAM addr, DE=next string addr
; brk: BC,AF
PUT_STR:
	CALL CALC_VRAM_ADDR
@@:
	LD A,(DE)
	INC DE
	OR A
	RET Z
	LD (HL),A
	INC L	; ※1
	INC HL
	JR @B

文字列は 0 を終端コードとしています。 改行などの特殊な動作はありません。

※1INC HL でもいいのですが、偶数アドレスに 1 足して奇数アドレスになるため、上位バイト(H) が変わらないことを利用した些細な高速化です。




枠の描画+α

では枠などを描くサブルーチン INIT_SCR を作ります。

INIT_SCR:
	LD HL,1*$100+0	; 座標(1,0)
	CALL CALC_VRAM_ADDR
	PUSH HL
	LD (HL),$98	; "┌"
	INC L
	INC HL
	LD A,$95	; "─"
	LD B,36
@@:
	LD (HL),A
	INC L
	INC HL
	DJNZ @B
	LD (HL),$99	; "┐"
	POP HL

ここまでで最上部 1 ラインを描画します。 グラフィック文字は打ち込めないので面倒ですが、文字コード一覧を見ながら打ち込むか、EQU でわかりやすい名前を定義するなど工夫してください。

コツコツと枠を描いた後、右下のステータス類を表示しています。ここで PUT_TRE_COUNT というサブルーチンを呼んでおり、さらにその中で PUT_DECIMAL1 というサブルーチンを呼んでいます。これは 1 バイトの値を 10 進数で表示するルーチンです。

PUT_TRE_COUNT:
	LD HL,32*$100+20
	LD A,(TRE_COUNT)
	CALL PUT_DECIMAL1
	RET

; in : H=X, L=Y, A=value
; brk: IX,IY,HL,DE,BC,AF
PUT_DECIMAL1:
	LD D,0
	LD E,A

; in : H=X, L=Y, DE=value
; brk: IX,IY,HL,DE,BC,AF
PUT_DECIMAL2:
	CALL CALC_VRAM_ADDR
	PUSH HL
	POP IX
	EX DE,HL
	LD IY,DEC_TBL
	LD B,0
@1:
	LD D,(IY+1)
	LD A,D
	ADD A,A
	JR C,@2F
	LD E,(IY)
	INC IY
	INC IY
	LD C,$30	; "0"
@@:
	OR A		; 念のため CF=0 に
	SBC HL,DE
	JR C,@F
	INC B
	INC C
	JR @B
@@:
	ADD HL,DE
	LD A,B
	OR A
	JR Z,@F
	LD (IX),C
	INC IX
	INC IX
@@:
	JR @1B
@2:
	LD A,B
	OR A
	RET NZ
	LD (IX),$30	; "0"
	RET

DEC_TBL:
	DW 10000,1000,100,10,1,$FFFF

2 バイトの値を 10 進数で表示する PUT_DECIMAL2 に続いています。上の桁から順に DEC_TBL の各値を引けるだけ引いて、引けた回数を表示しています。

上位桁の 0 を表示しないように B レジスタで管理しています(一度も引けていないうちは 0)。

なお、SBC HL,DE の前に OR A でキャリーフラグをクリアしていますが、 実はここに来た時点で必ずクリアされているので必要ありません。 ただ、そのことを慎重に確認しなければならないのと、 将来コードを書き替える可能性を考えて、無理せず確実にクリアしています。




マップ描画

BASIC 版で INSTR を使用しているところがあります。

2010 A$=MID$(M$(Z*16+Y),X+1,1):A=INSTR(" *@-+#?$",A$):IF A=0 THEN A=ASC(A$)-32

このあたりをマシン語に置き換えたのが以下の部分です。

OPEN_MAP:
	CALL CALC_MAP_ADDR
	LD A,(HL)
	OR A
	RET NZ
	PUSH HL
	LD HL,MAP_DATA
	ADD HL,DE
	LD A,(HL)
	LD HL,MAP_SRC_CHARS
	LD BC,8
	CPIR
	POP HL
	JR Z,@1F
	SUB 32
	JR @2F
@1:
	LD A,C
	INC A
@2:
(中略)

MAP_SRC_CHARS:
	DB "$?#+-@* "

あまり使われない CPIR 命令ですが、このような使い方ができます。ただし見つかるまで BC レジスタを減らしていくため、検索バイト列(ここでは MAP_SRC_CHARS)は逆順にする必要があります。

あと INSTR では最初に見つかると 1 が返りますが、CPIR では最後に見つかると 0 が返るという違いもあります。




3D 描画

大半の処理は BASIC からマシン語に置き換えただけですが、3D 描画に関しては一つ大きな違いがあります。

BASIC 版では直接 PRINT していましたが、マシン語版では 19x19 バイトの REND_WORK を用意し、そこに描画した後でテキスト RAM に丸ごと転送しています。 こうすることで中途半端な書き換えが見えなくなります。

描画ルーチンは REND_3D で、ソースファイルは 3dd_rend.z80s に分けてあります。

テキスト RAM への転送ルーチンは TRANS_SCR ですが、ここで初めに WAIT_VBLANK_ENDWAIT_VBLANK というサブルーチンを呼んでいます。

; brk: AF
WAIT_VBLANK:
	IN A,($40)
	AND $20
	JR Z,WAIT_VBLANK
	RET

; brk: AF
WAIT_VBLANK_END:
	IN A,($40)
	AND $20
	JR NZ,WAIT_VBLANK_END
	RET

I/O ポート $40 入力はビット 5 が V-Blank(垂直帰線期間)フラグで、 ここを監視することで画面の表示タイミングと合わせることができます。 また、マシン語で実行が速すぎる場合に、速度調整のためにも使用できます。

なお、PasocomMini PC-8001 の垂直周波数は(HDMI 出力の都合上)60Hz ですが、PC-8001 実機では 25 行モードで 62.42Hz と、多少の違いがあります。ご注意ください。




キー入力

BASIC 版では動作が遅かったので、キー入力を INP で読むことで押しっぱなしでも動けるようにしましたが、 マシン語だと速すぎて思うように動けなくなるので、 キーを離すのを待つことにします(動作としては、BASIC で INKEY$ を使うのと同様)。

WAIT_INPUT:
@@:
	IN A,(0)
	INC A
	JR NZ,@B
	IN A,(1)
	INC A
	JR NZ,@B

INPUT_LOOP:
	IN A,(9)
	BIT 0,A	; STOP キー
	JP Z,EXIT

I/O ポート $00 入力と $01 入力が両方 $FF になるまで待っています。

さらに、STOP キーが押されたら終了しています(BASIC に戻って Break がかかります)。




効果音

効果音は適当にウェイトを入れて、だいたい BASIC 版と同じ音を出しています。ちなみにマシン語なら高速に BEEP 音を ON/OFF できるので、もっと凝った音を出すことも可能です。

BEEP 音は I/O ポート $40 出力のビット 5 で ON/OFF します。ただしこのポートの他のビットは変えてはいけません (他の用途に使われているので)が、$40 入力で最後に出力した値を読み取れるわけではありません。

ではどうするかというと、N-BASIC では最後に出力した値を $EA67 に格納しているのを利用します。

OUT_40H	EQU $EA67

; brk: AF
BEEP1:
	LD A,(OUT_40H)
	OR $20
	OUT ($40),A
	RET

; brk: AF
BEEP0:
	LD A,(OUT_40H)
	AND $DF
	OUT ($40),A
	RET

こうすることで、他のビットを変えずに BEEP 音の ON/OFF ができます。ROM 内ルーチンを呼び出す方法もありますが、 処理が単純なのでこの方が良いでしょう。




おまけ機能

スタート直後、1 歩も進まずに A キーを押すと、オートプレイモードになります。 自力で解きたい方は押さないようにしましょう。

↓がその動画です。



ダウンロードはこちら


inserted by FC2 system