MSX マシン語でスプライトを動かす その1
今回記事では前回GitHubにアップロードしたソースについて解説します。
まずはsample007.asmの説明です。
https://github.com/sailorman-msx/games/tree/main/src/sample007
;--------------------------------------------
; メイン処理
;--------------------------------------------
Main:
;--------------------------------------------
; キャラクタパターンとカラーテーブルを
; 作成する
;--------------------------------------------
call CreateCharacterPattern
;--------------------------------------------
; スプライトパターンを作成する
;--------------------------------------------
call CreateSpritePattern
ld a, 14
ld (WK_PLAYERPOSX), a ; プレイヤーのX座標の初期化
ld a, 10
ld (WK_PLAYERPOSY), a ; プレイヤーのY座標の初期化
ld a, 5
ld (WK_PLAYERDIST), a ; プレイヤーの向きの初期化(下向き)
ld a, $0D
ld (WK_PLAYERSPRCLR1), a ; スプライトの表示色
ld a, $0F
ld (WK_PLAYERSPRCLR2), a ; スプライトの表示色
; スプライトを初期表示する
ld ix, SPRDISTPTN_TBL + 2
call CreateWorkSpriteAttr
ld de, $1B00
ld bc, 8
call PutSprite
call DebugPrint
MainLoop:
call DelayLoop
;--------------------------------------------
; 入力を受け付ける
;--------------------------------------------
; キーボードバッファをクリアする
; これを呼び出さないとカーソルキーを正常に判定できない
call KILBUF
; またはカーソルキーの方向を取得
; GTSTCK呼び出し後、Aレジスタに方向がセットされる
ld a, 0
call GTSTCK
;--------------------------------------------
; ジョイスティックが押されたら移動処理を呼ぶ
;--------------------------------------------
; Aレジスタに0をOR演算する
; ジョイスティックが押されるとAレジスタの
; 値には0より大きい値が入るためOR演算の結果はゼロにならない
or 0
jr z, MainEnd
;--------------------------------------------
; プレイヤーの移動
; 移動は8ドット単位で移動する
; X座標は1-29まで
; Y座標は1-21まで
; の範囲だけ移動可能とする
;--------------------------------------------
call MovePlayer
call DebugPrint
MainEnd:
jr MainLoop
CreateSpritePattern(sprite.asm)でスプライトパターンを作成しています。
その後、変数としてプレイヤー(スプライト)のX座標、Y座標、向き、スプライト1枚目の表示色、スプライト2枚目の表示色を初期化しています。
sample006.asmでは1B00Hを直接WRTVRMで操作することでスプライトを表示していましたが、今回は、スプライトアトリビュートテーブルを自前で作成して、表示するときにだけ1B00Hにブロック転送するようにしました。
PutSprite(sprite.asm)ではDEレジスタにVRAMの転送先アドレス、BCレジスタに転送量(バイト)をセットして呼び出すとWK_PLAYERSPRATTRのアドレスの値を転送量分だけ、転送するようになっています。
このようにスプライトを作成する場合は自前でスプライトアトリビュートテーブルを作っておくのが効率が良いです。そうしないと1B00HのVRAMの値を取得して変数に入れて、加工して、再度VRAMに書き直す・・といったことを繰り返すことになりとても面倒(バギー:トラブルのもと)なのです。
論理演算(OR、AND、NOT)
sample007.asmで以下のコード箇所があります。
;--------------------------------------------
; ジョイスティックが押されたら移動処理を呼ぶ
;--------------------------------------------
; Aレジスタに0をOR演算する
; ジョイスティックが押されるとAレジスタの
; 値には0より大きい値が入るためOR演算の結果はゼロにならない
or 0
jr z, MainEnd
このORという命令は論理演算の命令です。
まず、コンピューターでは論理演算というものがあります。
論理演算には代表的に次のようなものがあります。
OR(論理和)
どちらかが1であれば1になる
1 OR 1 = 1
1 OR 0 = 1
0 OR 1 = 1
0 OR 0 = 0
さて、14 OR 3 は 何になりますか?
答えは 15 です。2進数で考えてくださいね。
「???」となっていると思いますが、ついてきてください!
AND(論理積)
どちらも1であれば1になる
1 AND 1 = 1
1 AND 0 = 0
0 AND 1 = 0
0 AND 0 = 0
さて、14 AND 3 は何になりますか?
答えは2です。ついてきてください!!
2進数でものごとを考える。
これがコンピューターの演算での必須事項です。
NOT(否定)
ビットが反転する
NOT 1 = 0
NOT 0 = 1
値の各ビットを反転させるときだけ必要です。
筆者はゲームプログラミングでの用途が思いつきません。
これらの論理演算を使うと条件分岐が作れます。上記、ジョイスティック判定のコードは以下のようなフローチャートで表せます。演算結果がゼロになるとフラグレジスタのZフラグが1になる。という特性を活かした条件分岐です。
Z80の論理演算では必ずAレジスタと演算対象のペアで構成され、演算結果はAレジスタに格納されます。演算対象は値だったりレジスタだったりです。
OR n
と書くと、「Aレジスタ OR n の結果をAレジスタに格納する(あわせて演算結果によってフラグレジスタが変化する)」という意味になります。
上記コーディング例ではジョイスティックの値が0以外か?という判定のためにわざと 0 でOR演算をしています。こんな感じですね。
上(1) OR 0 = 1
斜め右上(2) OR 0 = 2
右(3) OR 0 = 3
右下(4) OR 0 = 4
下(5) OR 0 = 5
左下(6) OR 0 = 6
左(7) OR 0 = 7
左上(8) OR 0 = 8
押されていない(0) OR 0 = 0
演算をしないとZフラグは成立しません。なのであえてOR 0と演算をしてフラグレジスタを変化させています。
これらの理由をもとに「ジョイスティックの方向が押されていたらMovePlayerサブルーチンをCALLする。それ以外は無視。」という構成になっています。
CP命令
sprite.asmのMovePlayerサブルーチンの中でいくつかCP命令が登場しています。これも条件分岐のための命令です。Z80では比較命令と呼ばれます。
;------------------------------------------------
; SUB-ROUTINE: MovePlayer
; Aレジスタに格納されている情報から
; 方向を特定してプレイヤーのスプライトを移動させる
;------------------------------------------------
MovePlayer:
;--------------------------------------------
; スプライトの移動量TBLのアドレスを取得して
; HLレジスタに格納する
;--------------------------------------------
ld b, a ; Bレジスタにジョイスティックの方向の値を退避
MovePlayerSectionUp:
;--------------------------------------------
; スプライトの方向によってHLレジスタの値(ポインタ)
; を変更する
;--------------------------------------------
cp 1 ; ジョイスティックの方向は上か?
jr nz, MovePlayerSectionRight
ld ix, SPRDISTPTN_TBL
; 方向は上
; 移動処理にジャンプ
jr MovePlayerMoveProc
MovePlayerSectionRight:
cp 3 ; ジョイスティックの方向は右か?
jr nz, MovePlayerSectionDown
; 方向は右
ld ix, SPRDISTPTN_TBL + 1
; 移動処理にジャンプ
jr MovePlayerMoveProc
(以下、略)
CP命令はAレジスタと値(数値であってもレジスタであっても良い)を比較してフラグレジスタを変化させる命令です。ORやANDとは異なりAレジスタの値は変化しません。あくまでもフラグレジスタだけが変化します。
比較といってもAレジスタから値を引き算してフラグレジスタを変化させるだけです。
cp 1 ; ジョイスティックの方向は上か?
jr nz, MovePlayerSectionRight
この箇所をフローチャートで表すと次のようになります。
上記コーディングについては
CP 2
JR NC, MovePlayerSectionRight
と書いても同様になります。ジョイスティックの上方向が押されているとき(Aレジスタに1がセットされているとき)に
CP 2と書くとAレジスタの引き算の結果はマイナスになります。
演算結果がマイナスになるとCYフラグ(キャリーフラグ)が立ちます。
JR NC,・・・はキャリーフラグが立っていなければ・・という意味になります。
イメージできますか?ついてきてください(汗)
条件(フラグレジスタ)はRETやCALLでも使える
JPやJRで使う、フラグレジスタですが実はRETやCALLでも使えます。
これらは条件付きRETとか条件付きCALLと呼ばれます。
CALL NZ, nn はゼロフラグが立っていなければnnをCALLします。
RET Z はゼロフラグが立っていたら呼び出し元にリターンします。
こちらのほうがアセンブルした結果のバイト数が少なくて済みますね。
JPやJRでフラグレジスタの扱いに慣れてからにすればいいと思います。
フラグレジスタの種類については過去記事を参照してください。
おさらい
今回はここまでです。以下、当記事のおさらいです。
・演算結果でフラグレジスタが変化する。
・OR、ANDといった論理演算がある。
・論理演算は2進数で考える必要がある。
・論理演算はAレジスタをもとにして演算結果はAレジスタに格納される。
・CPという比較命令がある。
・比較命令はAレジスタから値を引いてフラグレジスタを変化させるだけでAレジスタの値は変化しない。
・フラグレジスタを駆使することでいろんな条件分岐が可能となる。
・RETやCALLでも条件は使える。
これでだいぶ「プログラミングっぽく」マシン語でコードが書けるようになったのではないかと思います。
次回は「ワークテーブルという考え方」について説明しますね。
では、また!