テキキャラを動かす その1
前回記事からだいぶ時間があいてしまいました。
前回まででスクロールを実現させましたが、今回はそのマップ上にテキキャラを配置してみました。
今回記事から、テキキャラについての解説になります。
解説したい箇所がいくつかあるので複数回の記事にわけて説明します。
テキをどう作るか?スプライト or PCG?
テキキャラをどう作るか?という点についてはいろんな意見がありますが、私は「自分のキャラ以外はすべてPCG」という宗派(笑)に属していますので、当記事ではテキキャラはPCGで動かします。
ということで、まずはサンプルコード(sample011)をゲットだぜ!!
https://github.com/sailorman-msx/games/tree/main/src/sample011
テキの移動処理のキホン
テキキャラについては、MAP上の論理座標で動かすようにして、今回次のような情報テーブルをテキキャラ1体ごと作成しています。
; テキキャラ管理用1体分(11byte x 100体ぶん)
;
; + 0 : テキキャラの種類(0:なし 1:ENEMY-TYPE1, 2:ENEMY-TYPE2, 3:GHOST)
; ENEMY-TYPE1とENEMY-TYPE2はテキキャラ
; GHOSTは味方キャラ
; + 1 : MAP論理X座標
; + 2 : MAP論理Y座標
; + 3 : 進行方向(1:上 3:右 5:下 7:左)
; + 4 : 進行距離(タイル数)移動ごとに1減らす(1-16)
; + 5 : 進行カウンタ
; + 6 : 当たり判定フラグ(0:床 1:壁ブロック 2:炎 3:緑ブロック)
; + 7 : 初期スポーン位置MAP論理X座標
; + 8 : 初期スポーン位置MAP論理Y座標
; + 9 : 上書き前のタイル番号
; + A : 倒した時のスコア
;
今回のサンプルではテキキャラは2種類。TYPE-1とTYPE-2だけ作成してます。青鬼と赤鬼を作ってみました。コメントにGHOSTっていう種類も書いていますがこれはまた後続記事で追加するキャラの予定です。
うーん・・・。
ドット絵師さんのスキルの1%でもいいから欲しい今日この頃です。。。
さて、テキキャラの仕様は次のようになっていて75体ぶん動かします。
1タイルぶん広いエリアで動かしてみる
テキキャラはMAP上の論理座標上で動かして、最大12x12タイルのビューポート内で移動処理を行います。(enemy.asmを参照してください)
画面に表示(VRAMに転送)するときだけそのビューポート内のさらに10x10タイルぶんだけを切り取って表示します。このことによって「見えないところからテキキャラが動いてきた!」というような視覚効果が表現できます。
12x12という範囲を広げればよりたくさんのテキの座標をワラワラと動かすことが可能ですが処理する対象のテキの数に比例して、処理速度が遅くなってしまう原因となるため調整が必要です。また、この「画面上で見える範囲外のテキを動かす処理」は、後続記事で説明する「テキの半タイル移動」の重要な要素となります。
テキの移動ロジック詳細
テキキャラは生成時にランダムに移動方向と移動距離を決めていて、1タイル動くたびに移動距離をデクリメントしています。移動距離が0になると、移動距離を再構築しています。再構築後の進行方向が再構築前の方向と同じであれば異なる方向になるまで進行方向決めをやり直しています。
テキの表示
テキキャラはMAP上にテキキャラのタイル番号を埋めることで表示を表現しています。enemy.asm、map.asm、data_map.asmを参照してください。
TYPE1ならタイル番号は#11-#14
TYPE2ならタイル番号は#15-#18
1タイル移動はハードモード。だ。
動かしてみるとわかるのですが、今回のサンプルでは移動単位は1タイル(16ドット)になっていますので「めちゃくちゃ速い」です。昔、スペランカーの3周目?とかはこんな感じだったような・・。
こんなゲームバランス無理やん。
クソゲーやん。テキキャラの動き半端ないやん。。。
ということで、半タイル(8ドット)で動かすことを考えてみましょう。
コード詳細は次回
どうやって実現しているの?どんなコードになっているの?
という点については次回記事から説明しますね。
では、また!!
マシン語講座:ジャンプ先をテーブル化する
ひさしぶりのマシン語講座です。
マシン語でよく書いてしまうのが次のようなコードです。
; CPの連続で分岐を繰り返す
; 分岐処理
; Aレジスタの値によって処理を分岐する
;
; A = 0 ?
ProcA:
cp 1
jr c, ProcB
ld b, 0
jr ProcEnd
; A = 1 ?
ProcB:
cp 1
jr nz, ProcC
ld b, 1
jr ProcEnd
; A = 2 ?
ProcC:
cp 2
jr nz, ProcD
ld b, 2
jr ProcEnd
....たくさん続く
ProcEnd:
ret
こういう分岐は、ムダコードの典型です。
上記のようなコードを書くと、Aレジスタの値が200だったら200回の「ムダな比較命令(CP)とジャンプ」が発生します。Z80は貧弱なCPUなのでムダなことで働かせてはいけません。Z80くんがかわいそうです。
最近のコンピュータであれば処理速度がかなり速いので、こういうコードを書いてしまいがち。プログラマーの新人くん「あるある」。
では、どうやって分岐させるのか?っていうと「処理ラベル(アドレス)をテーブルにしてしまう」という方法があります。以下の図のようにラベルのテーブルを定義して、値によってそのラベルにジャンプさせる。という形です。アセンブラによってはたぶんこういう書き方ができないやつもあるかもですが、z88dkでは書けます。
これだと分岐処理は次のような形になります。
; 修正後(直接アドレッシング指定で分岐をなくす)
; テーブルの作成
ld hl, WK_BUNKI_PROC
ld de, ProcA
ld (hl), e
inc hl
ld (hl), d
inc hl
ld de, ProcB
ld (hl), e
inc hl
ld (hl), d
inc hl
ld de, ProcC
ld (hl), e
inc hl
ld (hl), d
inc hl
ld de, ProcD
ld (hl), e
inc hl
ld (hl), d
inc hl
; ここから先が分岐処理
; HLレジスタに処理のラベル(アドレス)テーブルの先頭アドレスをセット
ld hl, WK_BUNKI_PROC
ld b, 0
ld c, a ; Aレジスタの値をHLレジスタに加算すると処理すべきコードのアドレスになる
add hl, bc
jp (hl) ; HLレジスタのアドレスに強制ジャンプ
; A = 0
ProcA:
ld b, 0
jr ProcEnd
; A = 1
ProcB:
ld b, 1
jr ProcEnd
; A = 2
ProcC:
ld b, 2
jr ProcEnd
....たくさん続く
ProcEnd:
ret
CPの連続のほうが読みやすいコードなのはたしかですが、CPUにムダな負荷をかけてはいけません。なので値によって直接アドレッシングで強制ジャンプさせるやりかたです。JPは1回こっきり。CPの結果でJP。。になっていないことがわかりますか?今回のサンプルでも多用しています。探してみてください。
ちなみにこのやりかたは、ちょっとでも間違うと暴走します。(汗)