MSX 当たり判定 内部処理の解説
今回記事は前回GitHubにアップしたsample008.asmの解説です。
当たり判定もそうですが、基本的にはワークテーブルという考え方の応用編になります。
移動量をワークテーブルにしてみた
initialize.asmで以下の箇所があります。
WK_PLAYERMOVE_TBL:equ $D009 ; 8バイト(プレイヤーの移動量)
これはジョイスティックが押された方向によって、X座標、Y座標がそれぞれどのくらい変化するのかを格納しているワークテーブルになっています。
sample008.asmの以下のコードで、初期値(PLAYERMOVE_TBL)の値をWK_PLAYERMOVE_TBLにコピーしています。
ld bc, 8
ld de, WK_PLAYERMOVE_TBL
ld hl, PLAYERMOVE_TBL
ldir
初期値はPLAYERMOVE_TBL(data_sprite.asm)です。こんな感じになっています。
; スプライトの移動量を格納したテーブル
; 1バイト内の上位4ビットがX方向の移動量
; 1バイト内の下位4ビットがY方向の移動量
; 移動量が2の場合は-1として処理する
PLAYERMOVE_TBL:
defb $00 ; ジョイスティック方向0
defb $02 ; ジョイスティック方向1(X= 0, Y=-1)
defb $12 ; ジョイスティック方向2(X= 1, Y=-1)
defb $10 ; ジョイスティック方向3(X= 1, Y= 0)
defb $11 ; ジョイスティック方向4(X= 1, Y= 1)
defb $01 ; ジョイスティック方向5(X= 0, Y= 1)
defb $21 ; ジョイスティック方向6(X=-1, Y= 1)
defb $20 ; ジョイスティック方向7(X=-1, Y= 0)
defb $22 ; ジョイスティック方向8(X=-1, Y=-1)
PLAYERMOVE_TBLを図にすると次のようになっています。
上位4ビットと下位4ビットでそれぞれX座標の移動量、Y座標の移動量を表しています。2はマイナス1の意味としています。
使うときはWK_PLAYERMOVE_TBLというワークテーブルにコピーして使っています。ワークテーブルにしておくことでのちのち融通が効くようになるのでそうしています。
どうやって使ってるの?
MovePlayerサブルーチン(sprite.asm)の以下の箇所が使用箇所です。
;--------------------------------------------
; 移動処理を行う
;--------------------------------------------
; ジョイスティックの方向に移動させる
ld a, (WK_PLAYERDIST)
ld hl, WK_PLAYERMOVE_TBL
ld b, 0
ld c, a
add hl, bc ; テーブルのポインタをジョイスティック番号分進める
ld ix, hl ; ポインタアドレスをIXレジスタに格納する
; 下位4ビットはY座標
ld a, (ix)
and $0F ; 0FHでANDして下位4ビットの値だけにする
ld b, a ; BレジスタにY座標の値をセットする
ld a, (ix)
sra a ; 4ビット右にシフトしてX座標の値とする
sra a
sra a
sra a
ld c, a ; CレジスタにX座標の値をセットする
MoveY_Plus:
ld a, b
cp 1
jr c, MoveX_Plus ; 移動量が0の場合はX座標の処理にジャンプ
cp 2
jr z, MoveY_Minus ; 移動量が2の場合はY座標の減算にジャンプ
; Y座標を加算
ld a, (WK_PLAYERPOSY)
inc a ; 移動量をインクリメントする
ld (WK_PLAYERPOSY), a
jr MoveX_Plus
MoveY_Minus:
; Y座標を減算
ld a, (WK_PLAYERPOSY)
dec a ; 移動量をデクリメントする
ld (WK_PLAYERPOSY), a
MoveX_Plus:
ld a, c
cp 1
jr c, MovePlayerPutSprite ; 移動量が0の場合は何も処理しない
cp 2
jr z, MoveX_Minus
; X座標を加算
ld a, (WK_PLAYERPOSX)
inc a ; 移動量をインクリメントする
ld (WK_PLAYERPOSX), a
jr MovePlayerPutSprite
MoveX_Minus:
; X座標を減算
ld a, (WK_PLAYERPOSX)
dec a ; 移動量をデクリメントする
ld (WK_PLAYERPOSX), a
jr MovePlayerPutSprite
ジョイスティックの方向ぶんWK_PLAYERMOVE_TBLのポインタを進めるとその方向にあわせた移動量が決まります。
例えば、右方向(3)が押されたとするとWK_PLAYERMOVE_TBL+3のアドレスの値を取得します。上記PLAYERMOVE_TBLの図だと10Hですね。
これを上位4ビットと下位4ビットに分割します。
下位4ビットはY座標です。
下位4ビットを取得するためには値に対して、0FH(00001111B)でAND演算をします。
00001111B AND 00010000 は 0 になります。つまり右方向を押された場合のWK_PLAYERMOVE_TBL+3のY座標の移動量は0ということがわかります。
X座標の移動量は上位4ビットの値です。
上位4ビットの値を取得するために
SRA A ← Aレジスタの値を1ビット右にシフトする命令
を4回繰り返すことで 00010000B を 00000001B にしています。
00010000B を1回右シフトすると 00001000B
00001000B を1回右シフトすると 00000100B
00000100B を1回右シフトすると 00000010B
00000010B を1回右シフトすると 00000001B
ということです。前記事で紹介したビットシフトです。
ここ重要、ついてきてますか?
つまり、WK_PLAYERMOVE_TBL+3の上位4ビットの値は1。つまりX座標は+1ということになります。
これで、右方向を押されたときの移動量はYが0、Xが+1であると決定します。
ビットシフトとかAND演算とか、なんでこんな面倒なことを・・
かなり面倒なことをしているように見えますが、ちょっと考えてみてみましょう。
X座標の移動量とY座標の移動量はそれぞれ1か0か-1です。
X座標の移動量とY座標の移動量をそれぞれ別のアドレス(変数)にすると単純にバイト数が9 x 2(ジョイスティック番号の数x2)で18バイト必要です。1か0か-1なら1バイトを必要としません。4ビットもあれば十分です。4ビットにすることで9バイトで済みますよね。たかが1バイト、されど1バイトです。
また、ポインタで移動量を特定できるので条件分岐も少なくて済みます。
sample007.asmのMovePlayerサブルーチンのコードと比較してみてください。sample007.asmのMovePlayerサブルーチンのコードでは15回の条件分岐になっていますが、今回の修正で9回の条件分岐で済むようになっています。マシン語プログラミングで条件分岐を多用するとあとあと苦しむことになります。
♩飛んで飛んで飛んで飛んで飛んで、まわってまわってまわって暴走〜
というような事態になることも多いです。
ということで今回は移動量をワークテーブル化することで条件分岐を減らしてみました。
当たり判定ロジック:XY座標から2次元テーブルを作成する
今回の当たり判定ロジックではGetVRAM4x4(vram.asm)というサブルーチンを追加しています。このサブルーチンではVRAMの画面情報(パターンネームテーブル:VRAMの1800H-1AFFH)をもとにWK_VRAM4X4_TBL(initialize.asm)というワークテーブルを作成します。
前記事までのワークテーブルの考え方の応用です。
WK_VRAM4X4_TBLは指定された座標周辺の情報を4X4の情報(テーブル、表)として保持します。こういうタテ・ヨコの表のことを2次元テーブルといったりします。いままでの直列な形のテーブルは1次元テーブルです。
ワークテーブルの考え方がわからないと以降、何を言ってるのかわからないかもしれません。ご容赦ください。
;---------------------------------------------
; SUB-ROUTINE:GetVRAM4x4
; 指定したX座標、Y座標周辺の画面情報を
; 4x4のTBLに格納する
;
; WK_CHECKPOSX, WK_CHECKPOSYに座標値をセットして
; から呼び出すこと
;
; WK_VRAM4X4_TBL
;
; +--+--+--+--+
; |00|01|02|03|
; +--+--+--+--+
; |04|05|06|07| <- 05がX座標、Y座標に対応
; +--+--+--+--+
; |08|09|0A|0B|
; +--+--+--+--+
; |0C|0D|0E|0F|
; +--+--+--+--+
;
; 注意:
; WK_CHECKPOSX, WK_CHECKPOSYは1以上の値とすること
;
;---------------------------------------------
GetVRAM4x4:
push ix
push iy
push hl
push de
; IXレジスタにWK_VRAM4X4_TBLのアドレスをセット
ld ix, WK_VRAM4X4_TBL
(以下、略)
このサブルーチンを使うとWK_CHECKPOSX, WK_CHECKPOSYの座標周辺のVRAM情報を4x4のワークテーブル(WK_VRAM4X4_TBL)に格納して返却します。図解で説明するとわかりやすいと思いますので、以下に図で表現します。WK_VRAM4X4_TBL+05Hの部分がX座標(WK_CHECKPOSX)、Y座標(WK_CHECKPOSY)の位置と同じです。
WK_CHECKPOSX、WK_CHECKPOSYの変数に知りたい座標をセットしてGetVRAM4x4サブルーチンを呼び出すと次のようにWK_VRAM4X4_TBLのアドレスにVRAMの情報が格納されて戻ってきます。次の図は上記図2の状態でWK_VRAM4X4_TBLを作成した図です。
WK_VRAM4X4_TBL+05HがX座標Y座標に該当しているので、右に動かそうとした場合に、WK_VRAM4X4_TBL+07HとWK_VRAM4X4_TBL+0BHの内容を調べて、床(80H)だったら移動できる、床じゃなかったら移動できない。と判定できます。MovePlayerサブルーチン(sprite.asm)では以下のように「最初に動かす方向に障害物がないか(移動できるか)」をCheckVRAM4x4サブルーチン(sprite.asm)を使って判定しています。移動させたあとに判定して動けないなら元に戻す。という処理にすると無駄な処理が発生します。そのため一番最初で判定しています。無駄な処理は全体の処理速度に影響するなどパフォーマンス劣化の原因になります。
------------------------------------------------
; SUB-ROUTINE: MovePlayer
; Aレジスタに格納されている情報から
; 方向を特定してプレイヤーのスプライトを移動させる
;------------------------------------------------
MovePlayer:
;--------------------------------------------------------
; 移動処理の仕様
; ・前向いていた方向と異なる場合は移動はせず向きだけ変える
; ・前向いていた方向と同じ場合はその方向に移動する
;--------------------------------------------------------
ld a, (WK_PLAYERDIST) ; Bレジスタに今回の向きをセット
ld b, a
ld a, (WK_PLAYERDISTOLD) ; Aレジスタに前回の向きをセット
; Aレジスタの値からBレジスタの値を減算した結果が0でなければ
; 向きが変わったと判定する
cp b
jp nz, MovePlayerChangeDistOnly
; 向きが同じであれば移動させる
;--------------------------------------------
; 移動可能か判定する
; Aレジスタが1の場合は移動不可
;--------------------------------------------
call CheckVRAM4x4
cp 1
jp z, MovePlayerEnd
(中略)
;--------------------------------------------
; SUB-ROUTINE: CheckVRAM4x4
; (返却値)
; Aレジスタ=0 : 移動可能
; Aレジスタ=1 : 移動不可
;--------------------------------------------
CheckVRAM4x4:
;--------------------------------------------
; 移動先のVRAM情報判定
; 現在自分がいる場所の周囲4x4の情報を取得する
;--------------------------------------------
ld a, (WK_PLAYERPOSX)
ld (WK_CHECKPOSX), a
ld a, (WK_PLAYERPOSY)
ld (WK_CHECKPOSY), a
call GetVRAM4x4
;--------------------------------------------
; 進行方向に床(80H)以外の文字がないかチェックする
; 80H以外が見つかれば移動不可とする
;--------------------------------------------
ld a, (WK_PLAYERDIST)
(中略)
CheckVRAM4x4Right:
cp 3
jr nz, CheckVRAM4x4Down
; 進行方向は右
ld a, (WK_VRAM4X4_TBL+$07)
cp $80 ; 移動先は床か?
jr nz, CheckVRAM4x4DoNotMove
ld a, (WK_VRAM4X4_TBL+$0B)
cp $80 ; 移動先は床か?
jr nz, CheckVRAM4x4DoNotMove
jr CheckVRAM4x4MoveOkay
当たり判定ロジック:どうやって判定しているの?
CheckVRAM4x4では最初にGetVRAM4x4で座標周辺のVRAM情報をワークアドレスに格納してから判定しています。CheckVRAM4x4Rightの判定箇所を見てください。この箇所は右方向が床(80H)なのかどうかを調べています。上記図3を見てみましょう。上記図3の状態で(画面的には図2の状態で)WK_VRAM4X4_TBLの右方向の物体を取得します。プレイヤーのキャラクターは16ドットなので
WK_VRAM4X4_TBL+07H ←プレイヤーのスプライトの上半分の右側
WK_VRAM4X4_TBL+0BH ←プレイヤーのスプライトの下半分の右側
の値を調べて、どちらも床(80H)であればCheckVRAM4x4MoveOkayにジャンプして「移動可能」と判定しています。
ついてこれてますか?(汗)
ソースコードをじっくり読むとわかってくるかも・・?
このWK_VRAM4X4_TBLを作成するGetVRAM4x4サブルーチンは敵キャラにも応用できます。敵キャラであっても、「ブロックにぶつかるなら動けない」などといった判定で使えます。ほかにも宝箱とか落とし穴とかに当たったかどうか?といった判定でも使えます。
ただし、MSXではVRAMの操作は少し遅いようで多用する場合や最高速を求める場合は別のやりかたでやるみたいです。私はそのやりかたを知りませんので、それはまたの機会で。。。
マップ作成とかやってみる?
今回はここまでです。
当たり判定についてはいろんなやりかたがありますが、今回はいちばんわかりやすいやつ&他にも応用がきくやつで解説してみました。
・1バイトを節約するマシン語プログラミング
・ワークテーブルやポインタを使ったマシン語プログラミング
・無駄な処理、無駄な分岐を削減するマシン語プログラミング
が出来るようになれば、あとはひたすら応用です。
(そう信じたいワタクシです・・)
今回で白いブロック(障害物)との当たり判定が出来るようになったので次回はちょっと趣向を変えてマップ作成にチャレンジしてみたいと思います。
では、また!
マシン語講座:マシン語はかく語りき(圧倒的スピードを体感しよう)
マシン語は圧倒的に速いです。
そりゃもうびっくりするくらい速いです。
いままでのサンプルではわざと空ループ(なにもしないループ処理)をはさむことで人間が操作できるレベルまでの速度になるようにしています。
sample008.asmの jr DelayLoop を次のようにコメント文にしてアセンブルして動かしてみてください。処理をコメント文にすることを「コメントアウトする」と言ったりします。
MainLoop:
;call DelayLoop
残像が残るほど速いことが確認できると思います。