Chapter.1B 自機の移動処理

■自機の移動処理
自機は、カーソルキーまたはジョイパッド1の十字キーで操作できるようにします。
スピードアップアイテムを登場させることにすると、移動量がドット単位では以下のような問題が出てきます。

(1) 動きの粒度が荒すぎる

 速度1の移動量は、1ドット
 速度2の移動量は、2ドット
 速度3の移動量は、3ドット

 速度1→速度2の変化は倍速になってますが、速度2→速度3は 1.5倍速にしかなっていません。
 速度3を4ドットにすれば良いかもしれませんが、動きが荒すぎになってしまいます。

(2) 斜め移動が速すぎる

 右に動くときは (1,0) を加算、右下に動くときは (1,1) を加算とすると、右下に動くときの移動量が大きすぎます。


以上の問題を解決するために、座標に小数部を設けることにします。

 右に動くときは (1,0) を加算、右下に動くときは (0.7,0.7) を加算とすると、だいたい移動量は同じになります。

座標はあくまで整数なので、座標として使うときは整数部だけを切り出して使いますが、切り出すのに演算が必要では
貴重な CPU リソースを無駄に浪費してしまいます。

Z80 には、16bitレジスタとして使える HLレジスタと、それを上下8bitずつに分解した H, Lレジスタというペアレジスタの
概念があります。
この上位8bit が整数部、下位8bit が小数部であると、勝手に決めつけてしまうことで、小数表現を実現することにします。

つまり、HLレジスタ = 256 のときに 1.0 と解釈する。HL = 128 のときに 0.5 と解釈する、HL = 64 のときに 0.25 と解釈する
といった使い方にするわけです。

とりあえず、自機の移動ベクトルの長さは 0.25 → 64 にしておきます。
斜め方向は、cos45°= 1/√2 倍すれば良いので、64 * 1 / √2 ≒ 45 となります。

その辺のイメージを図1 に示します。

図1. 斜めと水平/垂直のドット数の違い

これを、STRIG(n) の返値にあわせて表1にまとめます。

表1. STRIG(n)の返値と移動ベクトルの関係
STRIG(n)の返値 方向 X方向の移動量 Y方向の移動量
0 ニュートラル 0 0
1 0 -64
2 右上 45 -45
3 64 0
4 右下 45 45
5 0 64
6 左下 -45 45
7 -64 0
8 左上 -45 -45

この表をプログラム内に持っておくことにします。
表を具体的にプログラムにしたモノを、プログラム1に示します。

プログラム1 移動ベクトル表
player_move_vector0:
		.dw	#0, #0			; ・
		.dw	#0, #-64		; ↑
		.dw	#45, #-45		; /
		.dw	#64, #0			; →
		.dw	#45, #45		; \
		.dw	#0, #64			; ↓
		.dw	#-45, #45		; /
		.dw	#-64, #0		; ←
		.dw	#-45, #-45		; \
		

1行が1方向に対応しており、最初の値が Xの移動量、次の値が Yの移動量になっています。
.dw は 16bitデータを示すので、1行は2データ分で 32bit = 4byte あることになります。

■カーソルキーやジョイパッドの十字ボタン入力処理
MSX には、よく使う処理のルーチンを BIOS として提供しています。
実際に、MSX-BASIC が内部で使用していたり、ROMカートリッジのゲームソフトが利用したりしてます。
OUT/IN 命令を使えば、ハードウェアを直接操作することも出来ますが、いろいろな手続きがあったりして
複雑な処理を記述しないと制御出来なかったりします。
でも、BIOS を使えば、ある程度大きな単位で用意されているので、比較的簡単に周辺機器を制御するようなプログラムを
組めるわけです。ここでいう周辺機器とは、CPU の外側という意味で、画面表示の VDP や、音関連の PSG、ジョイパッドなど
をひっくるめたモノです。

BIOS の中に、GTSTICK というエントリがあります。
これは、BASIC の STICK(n) と同様の効果を成すルーチンで、n を Aレジスタにセットして call すると、Aレジスタに STICK(n) と
同じ結果が得られます。

作成したプログラム sca_player.asm の中の下記の部分が、カーソルキーとジョイパッド1の十字キーを読み出す処理になってます。

player_move::
		push	ix
		; カーソルキーの状態を得る
		xor	a
		call	GTSTCK
		push	af
		; ジョイスティック1の状態を得る
		ld	a, #1
		call	GTSTCK
		; カーソルキー状態、ジョイスティック1状態をミックス
		pop	bc
		or	b
		; アドレスオフセットに変換 ( iy = p_vector + a * 4 )
		pop	ix
		

BIOSルーチンも機械語プログラムであるため、内部でレジスタを使用します。
BIOSルーチンを call することによって、いくつかのレジスタの値が書き換えられてしまうことがあります。
どのようなレジスタが書き換えられるのかは、BIOSエントリアドレスと共に公開されていますので、BIOSエントリを調べるときは
破壊されるレジスタもいっしょに調べておいてください。
GTSTCK の場合、"全てのレジスタが破壊される" ということになっているので、壊されては困るレジスタをどこかに待避することで
以降の動作に支障の内容にします。
ここでは、ix レジスタを push してスタックへ待避。
更に1回目の GTSTCK の処理結果を push af してスタックへ待避しています。
2回目の GTSTCK の処理の後に、1回目の処理結果を pop bc して b レジスタに読み取り、
or b することで、A ← STICK(1) OR STICK(0) のような効果を得ています。
最後に待避していた ix を復帰させて、STICK の値を得る処理は終わりです。

覚えておいて欲しいのは、STICK(n) は 0〜8 の範囲を返すと言うことです。
STICK(1) OR STICK(0) のように OR をとっているため、"STICK(1) OR STICK(0)" のとりうる値は 0〜15 になることです。
(たとえば、STICK(1) が 7 で、STICK(1) が 8 の場合など)

■移動ベクトルの取得
方向入力が Aレジスタに得られました。
ここから、移動ベクトルの対応する行を読み出せるように、値を加工していきます。

移動ベクトル表が格納されている先頭アドレスは、プログラム1を見て貰えれば分かるように player_move_vector0 という名前を付けてあります。
そして、1行が1方向に対応していて 4byte あります。
移動ベクトル表の中の方向並びと、STICK の返値は同じになるように並べてあります。

以上の情報から、アドレス計算は下記のように求まることが分かります。

player_move_vector0 + Aレジスタ * 4
ただ、後々 player_move_vector0 の部分は変えられるようにしておいた方が、「スピードアップアイテム」を実現するのに役立ちそうです。
なぜならば、スピードアップ後の移動ベクトル表をあらかじめ用意しておけば、それをすり替えるだけでスピード変化に対応できるからです。
その辺も踏まえて、プログラムを書いてみます。

Aレジスタは、0〜15 なので、これを 4倍した値は 0〜60 の範囲になります。なので、8bit のまま演算しても溢れません。
残念ながら、かけ算命令は無いので、A = A + A を2回繰り返して対処します。
add a の代わりに左シフト命令や左回転命令を使っても良いです。

		add		a			; A ← A + A
		add		a			; A ← A + A
		ld		l, a		; HL ← A
		ld		h, #0
		ld		c, 4(ix)	; BC ← (ix + 4)   ※(ix + 4) には、player_move_vector0 が格納されている
		ld		b, 5(ix)
		add		hl, bc		; HL ← HL + BC
		push	hl			; IY ← HL
		pop		iy
		

ここまでの演算により、iy = (STICK(1) OR STICK(0)) * 4 + player_move_vector0 を得ることが出来ました。
つまり、iy には、移動すべき方向の移動ベクトルが格納されている先頭アドレスが入っていることになります。
なので、実際に自機の座標に移動ベクトルを加算してみましょう。
		ld		l, 0(ix)	; HL ← 自機 X座標
		ld		h, 1(ix)
		ld		c, 0(iy)	; BC ← 自機 X移動量
		ld		b, 1(iy)
		add		hl, bc		; HL ← HL + BC
		

これで X 方向に移動させました。
L は小数部扱いなので、H が実際の X座標になります。
次に画面外判定をします。

		ld		a, h
		cp		a, #(192-16)
		jr		c, player_move_x_success1
		

192-16 というのは、右端の座標です。
X=192 より右側は、スコアの表示枠にするつもりなので、192 という数値が出てきます。
自機スプライトの幅は 16ドットで、スプライトは左上座標を指定するので、スプライト幅分を差し引いてます。

192-16 は、定数同士の演算=アセンブル時に値が確定する演算 なので、アセンブラがアセンブル時に自動的に計算してくれます。
加算命令等は生成しないので安心してください。
定数なので # がつきます。

cp a, #(192-16) で、X座標と 192-16 を比較しています。
CPU 動作としては、a - (192-16) の演算結果によってフラグを変化させています。

画面右側にはみ出していなければ、a < (192-16) が成立するため、a - (192-16) < 0 になります。
つまり、a - (192-16) の演算によって、桁借りが発生するため キャリーフラグが立ちます。

次に画面左側にはみ出した場合はどうなるでしょうか。
X座標が 0 で、移動ベクトルが -1 だった場合。移動後は -1 になりますが、「符号無し8bit整数」と見なせば、
2の補数なので -1 は 255 になります。
255 - (192-16) は、桁借りが生じないので、キャリーフラグは立たないのです。

キャリーフラグは、減算では符号無し数を対象として桁借りを示すフラグなので、例え使ってる人が「符号有りのつもり」でも、
sub/sbc/cp 等の演算では、符号無しと見なして考える必要があるわけです。
(符号有りとして、範囲外にはみ出したことを確認したいなら P/Vフラグを読むことになります)

ということで、cp a, #(192-16) の1命令を実行した結果のキャリーフラグを見るだけで、「画面外にはみ出したかどうか」を
判断出来ることになります。

従って、jr c, player_move_x_success1 は、「画面外に出ていなければ、player_move_x_success1 へジャンプ」という
意味になります。

慣れるまではヤヤコシイと感じる人も居るかも知れませんが、ビットの桁上がり・桁借りのイメージを思い浮かべながら
cp 命令を使うことで、自然と思いつくようになります。慣れてください (^_^;


かならず1ドットずつしか動かないのであれば、「はみ出した場合は座標保持メモリを更新しない」という対応で
済みますが、自機のスピードアップ処理も考慮すると、2ドット以上動くケースも想定しておく必要があります。
はみ出してしまった場合は、はみ出した方の端に張り付くという対応にしようと思います。

たとえば、左へ 2ドット移動したときに、X座標が 1 から -1 に変化したとします。
これを X座標 1 のままにするのではなく、X座標を 0 にすることで、左端に張り付くようになります。

player_move_x_fail1:
		cp		a, #224
		; 左なら左端張り付き
		ld		hl, #0			; ※フラグ変化無し
		jr		nc, player_move_x_success1
		; 右なら右端張り付き
		ld		hl, #((191-16)*256+0)
		

画面の左にはみ出しても、右にはみ出しても player_move_x_fail1 に到達します。
左にはみ出した場合は 0、右にはみ出した場合は 191-16 にしたいので、どちらにはみ出したのか判断する必要があります。

移動量を複数ドットまで考慮するとは言っても、せいぜい数ドットです。仮に 4ドットまでとすると、
左にはみ出した場合は最大でも -4、右にはみ出した場合は最大でも (192-16)+4 までしか到達しません。
-4 は、符号無しと見なすと 252 です。
(192-16)+4は、180 です。
192と256 の中間にあたる 224 より大きい場合は 左にはみ出した、小さい場合は 右にはみ出した とすれば判別できますね。

ちょうどスプライトの X座標も、符号無し8bit なので、スプライトの表示位置で考えるとわかりやすいですね。
その辺のイメージを図にまとめたのが図2になります。


図2. 画面外判定イメージ


実際に移動した X座標は、HLレジスタに入ってます。
はみ出した場合も、HLレジスタの値を加工しているので、HLレジスタに入ってます。
player_move_x_success1 では、HL レジスタの内容を、X座標保持メモリへ書き戻します。
player_move_x_success1:
		; X座標を更新
		ld		0(ix), l
		ld		1(ix), h
		

Y座標も同じですね。

		; Y座標に移動ベクトルを加算して垂直移動
		ld		l, 2(ix)
		ld		h, 3(ix)
		ld		c, 2(iy)
		ld		b, 3(iy)
		add		hl, bc
		; 画面外にはみ出したかチェック
		ld		a, h
		cp		a, #(192-16)
		jr		c, player_move_y_success1
		; はみ出した方向を判別
player_move_y_fail1:
		cp		a, #224
		; 上なら上端張り付き
		ld		hl, #0			; ※フラグ変化無し
		jr		nc, player_move_y_success1
		; 下なら下端張り付き
		ld		hl, #((191-16)*256+0)
player_move_y_success1:
		; Y座標を更新
		ld		2(ix), l
		ld		3(ix), h
		ret
		

最後は、ret で呼び出し元に戻ってます。

■補足 移動ベクトルテーブルの中身を見て、.dw #0,#0 のような行がたくさんくっついていることを不思議に思った人もいるかも
しれないので、補足しておきます。

移動ベクトルのアドレスは、iy = (STICK(1) OR STICK(0)) * 4 + player_move_vector0 という演算によって求めています。
STICK(n) 自体は、0〜8 の範囲しかとりえませんが、(STICK(1) OR STICK(0)) の値は 0〜15 の値をとり得ます。
これは、カーソルキーを押しながら、ジョイパッドの十字ボタンを押すという、意地悪な操作をした場合です。
そのような意地悪な操作をした場合に、挙動不審にならないように、「動かない」という動作を明確に定義しておきます。
そのために 方向 9〜15 に対応する移動ベクトルを、(0,0) にしているわけです。

これを付けないと、この後にプログラムをリンクした場合に、プログラムである機械語コードを移動ベクトルとして読み取り、
それを加算してしまうという暴挙をやってしまいます。
そうすると、意地悪な操作をしたときに、自機がおかしなところにぶっ飛んで行ってしまう「バグ」が発生します。

■test_main
ということで、自機の移動計算ルーチンを作成したわけですが、既に 144行もあるプログラムですから、少し動かして
おかしな動作をするケースがないか確認しておきたいところです。これを単体テストと呼ぶことにします。

単体テストは、やはり BASIC から呼び出せた方が楽なので、BASIC と 機械語によるゲームプログラム の仲介役になるプログラムを
作成します。それが test_main.asm です。

		.globl	player_init
		.globl	player_move
		

.globl で test_main.asm 以外の場所で定義されている名前を、test_main.asm の中で利用できるようにします。
ここでは、単体テストしたい処理ルーチンの名前を一覧しておけばいいです。

.globl で使える名前は、「::」(コロン2個)を付けた名前だけです。「:」(コロン1個)の名前は指定できません。
なので、*.asm を記述するときは、外から呼び出したいルーチンの名前には「::」(コロン2個)を使うようにしてください。
よく分からない人は、全部「::」でもいいです(本当は使い分けて欲しいですけどね (^_^; )。

		.area	CODE (ABS)
		.org	#0xC000		; BASIC と共存できるように C000h から始まるようにする
		

次に、.area CODE(ABS) は、決まり文句のおまじないのように思ってください、説明がめんどくさいので省きます (^_^;
.org #0xC000 は、「次の命令語以降は、0xC000番地以降にマッピングする」という意味の命令です。

BASIC から、機械語ルーチンを呼び出すには、下記のようにします。

100 DEFUSR=呼び出すアドレス
110 A=USR(引数)
		

このとき、呼び出すアドレスが分からないとダメですが、.org #0xC000 が指定されて作られたモノなら、0xC000 番地を呼び出しアドレス
とすれば良いことがわかります。

「0xC000 はどうやって決めたのか?」という疑問を持つ方も居るかも知れませんが、その辺は MSX-BASIC 動作時のメモリマップに大きく
関わってきます。
空いているメモリなら、どこを指定しても良いのですが、だいたい 0xC000 あたりに割り当てるのが普通です。
BASIC ほとんど使用しないなら、もう少し若いアドレス(0xA000 等)を指定しても問題ありません。

		; USR(n) の n の下位8bitを得る
		inc		hl
		inc		hl
		ld		a, (hl)
		

これは、B=USR(n) の n に整数が指定された場合に、n の下位8bit を得るための処理です。
たとえば、B=USR(&H1234) という指定をした場合、上の機械語を実行することで、Aレジスタには 0x34 が代入されます。

		ld		ix, #player_info

		; 0 なら player_init のテスト
		or		a
		jp		z, player_init
		; 1 なら player_move のテスト
		dec		a
		jp		z, player_move
		; それ以外は何もしない
		ret
		

あとは、Aレジスタが何であるかによって、呼び出すサブルーチンを選択する処理を記述しています。
0 かどうかを調べるときだけ、or a を使ってます。
それ以降は、dec a するたびに0になったかどうかを見ていけば、0,1,2 ... とそれぞれの値毎の処理ルーチン呼び出しを実現できるわけです。


■BASICからの呼び出し
mk.bat をダブルクリックしてビルドしてください。
chapter1.bin が出来上がりますね。

こいつを読み出して実行する BASICプログラムを作ります。

test_main の中の player_info に自機の X座標/Y座標 が格納されています。
ビルド時に作られた chapter1.map を開いてください。
player_info は、0xC010 にマップされているのが確認出来ます。
0xC010 に X座標の小数部、0xC011 に X座標の整数部、0xC012 に Y座標の小数部、0xC013 に Y座標の整数部が格納されているのは、
これまでの解説から分かると思います。

従って、0xC011 と 0xC013 を表示してやれば、実際に座標がどのように変化していくのか確認出来るわけです。

100 CLEAR 200,&HC000:DEFINTA-Z:CLS
110 BLOAD "CHAPTER1.BIN"
120 DEFUSR=&HC000
130 A=USR(0)
140 '
150 LOCATE 0,0
160 PRINT PEEK(&HC011);" "
170 PRINT PEEK(&HC013);" "
180 A=USR(1)
190 GOTO 150
		

CLEAR 200, &HC000 は、BASIC の変数格納用メモリとして 200byte の領域を確保し、かつ &HC000 以降を BASIC が侵略しないことを
BASIC に対して要求します。
&H8000〜&HBFFF までで、200byte を除いた分は、BASIC のプログラムメモリに割り当てられます。
(&H0000〜&H7FFF は MAIN-ROM (BIOS) が見えているので ROM です)

今回のプログラムは、0xC000 (&HC000) からにマッピングしているので、CLEAR 200, &HC000 になっています。

DEFINT A-Z:CLS は、BASIC でおなじみの命令ですので、説明は省きます。

BLOAD "CHAPTER1.BIN" は、CHAPTER1.BIN という機械語プログラムのファイルをメモリ上に読み込む命令です。
どこに読み込まれるのかは、ファイル上に書かれていて、今回は 0xC000番地からに読み込まれます。

DEFUSR=&HC000 は、USR(n) の呼び出しが 0xC000 番地の呼び出しになるように宣言しています。

A=USR(0) は、player_init を実行する処理です。

150〜170行は、0xC011番地 と 0xC013番地に入っている値を表示する部分です。

A=USR(1) は、player_move を実行する処理です。

GOTO 150 は、表示と player_move 実行を繰り返すループです。

これを実行して、カーソルキーやジョイパッドの十字ボタンを押してみて、思惑通りに値が変化しないケースがないか検証してみてください。
もちろん、カーソルキーとジョイパッド十字ボタンの同時押しも確認しておきます。

これで、一通り動きが確認出来たら、「数字だけじゃ面白くないから、スプライトを表示させてみよう」と思います。

作成した機械語ルーチンは、あくまで座標計算しかしませんので、スプライトの表示部分は BASIC で記述することになります。
上のテストプログラムをスプライト使用に切り替えただけなので、BASIC の使い方を知っている方なら、理解できますね。

100 CLEAR 200,&HC000:DEFINTA-Z:COLOR15,4,7:SCREEN4,2,0
110 BLOAD "CHAPTER1.BIN"
120 DEFUSR=&HC000:GOSUB 520
130 A=USR(0):PUTSPRITE0,(0,0),1,0:PUTSPRITE1,(0,0),&H4E,1
140 '
150 A=USR(1):X=PEEK(&HC011):Y=PEEK(&HC013)
160 PUT SPRITE0,(X,Y):GOTO 150
200 '
500 DATA "0001000303030307677F6F6F633DC4030080804040C0C0E06A727A7A427A4680"
510 DATA "010303060604044CCECFDFDFDFC30300000000808000000484ACBCBCBC848000"
520 FORI=0TO1:READA$:S$="":FORJ=0TO31:S$=S$+CHR$(VAL("&H"+MID$(A$,J*2+1,2))):NEXT
530 SPRITE$(I)=S$:NEXT:RETURN
		

動かしてみてください。
実際に自機が表示されるのを確認出来ると思います。




以上、駆け足でしたが、自機が動くところまで出来ました。
今回説明が長いのは、test_main の説明や、アセンブラ命令の説明など、中身に触れる前の説明が多かったのが原因です。
次回以降は、もう少し気楽に進められる長さにおさまっていますので、ご安心を。


[▲トップページへ]