見出し画像

テキキャラを動かす その2:処理説明

テキキャラを動作させるための主な仕組み

今回は前回のサンプル(sample011)の処理説明です。
https://github.com/sailorman-msx/games/tree/main/src/sample011
ちょっと長くなっちゃったけど、おつきあいください。
テキキャラはenemy.asmにおおよそすべての処理が入ってます。
enemy.asmに入っているサブルーチンはざっと以下のようなものです。

投稿したあとでバグが存在していることを確認しました。
バグの内容は「ビューポート座標が大きくなるにつれテキキャラの進行速度が速くなる」というわけわからんバグでした。
とりあえず、記事の内容としてはあまり変わらないので、GitHubのソースコードはそのまま(バグ含む版のまま)にしています。
次回、記事ではそのバグは解消されます。失礼しました。
(追記:2023/02/04)
アセンブルが通らないコードがある。
というとてつもない欠陥を見つけたので近日中にソースコードを更新します。
その際にこの記事も修正します。
ほんと申し訳ないです。。。
(追記:2023/02/05)
以下のとおり、サンプルコードを修正しました。失礼しました・・。
・ADD HL, A というとんでもないコードを修正
・サブルーチンコメントを修正
・全テキキャラを同時に移動させるよう修正
・テキキャラの進行方向の変更時にはランダムではなく規則性を持たせた
・テキキャラの進行距離は0タイルから1タイルまでにした。(処理速度の都合上)



enemy.asm
 
;--------------------------------------------
; SUB-ROUTINE: InitializeEnemyDatas
; テキキャラポインタテーブルと
; テキキャラ1体ぶんのデータを初期化する
;--------------------------------------------
InitializeEnemyDatas:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: GetEnemyType
; テキキャラの種類を取得する(1 or 2)
; Aレジスタに種類がセットされて返却される
;--------------------------------------------
GetEnemyType:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: GetEnemyDist    
; テキキャラの進行方向を取得する(1 or 3 or 5 or 7)
; テキキャラの種類によって、進行方向を変える 
; TYPE1:
;   1 -> 3 -> 5 -> 7 -> 1 ... (時計回り)
; TYPE2:
;   1 -> 7 -> 5 -> 3 -> 1 ... (反時計回り)
; Aレジスタに種類がセットされて返却される
;--------------------------------------------
GetEnemyDist:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: GetEnemyRange
; テキキャラの進行距離を取得する(0 - 1)
; Aレジスタに種類がセットされて返却される
; 処理速度の都合上、最大1タイルに修正
;--------------------------------------------
GetEnemyRange:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: MoveEnemies
; テキキャラの移動情報を変更して移動させる
;
; (仕様)
; テキを表示させるためには12x12タイルのビューポートをベースに考える。
; テキの論理座標がその範囲内に存在していれば、描画処理の対象となる。
; ※この処理はmap.asmで実装
;
; 範囲内に存在していない場合は論理座標のタイル情報だけで判定する。
;
; テキの進行カウンタが15以上の場合は進行方向に半タイルぶん移動させる。
; この場合は論理座標は移動させない。
; *この処理はmap.asmで実装(現在、未実装)
;
; テキの論理座標が12x12ビューポート範囲外の場合:
; 移動方向先の1タイルが床や炎ではない場合は移動せずに、移動情報を初期化する。
;
; テキの論理座標が12x12ビューポート範囲内の場合:
; 半タイルの移動先が床や炎ではない場合は移動させずに、移動情報を初期化する。
; 移動できる場合は半タイル移動させた位置にテキを表示する。
; ※この処理はmap.asmで実装
;
; テキの移動フラグが1の場合は進行方向に1タイル分移動させて移動フラグを0にする。
; この際に論理座標も移動させる。
;
; 12x12タイルぶんの表示キャラクターのメモリ展開が完了したら
; そのうちの10×10タイルぶんだけのキャラクター情報をVRAM(画面)に転送する。
;
; テキの移動後の論理座標が炎のタイルである場合は
; タイル情報をテキのキャラには書き換えずテキの当たり判定に進める。
;--------------------------------------------
MoveEnemies:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: MoveEnemyTileMove
; テキキャラをMAP座標で移動させる
; 指定されてるMAP座標のタイルが床でなければ移動しない
; (WK_MAPAREAを更新する)
;
; Aレジスタには移動後の座標のタイル番号がセットされて
; 返却される
;--------------------------------------------
MoveEnemyTileMove:
 
(中略)

;--------------------------------------------
; SUB-ROUTINE: ResetEnemyMoveSrc
; テキキャラの移動元MAP座標のタイル番号を床(0)にする
;--------------------------------------------
ResetEnemyMoveSrc:
 
(中略)
 
;--------------------------------------------
; SUB-ROUTINE: RestructEnemyMoveData
; テキキャラの移動情報を再構築する
;
; テキキャラ管理用アドレスをセットして呼び出すこと
;--------------------------------------------
RestructEnemyMoveData:

(中略)

ちょっとコードの実態とサブルーチンの処理説明コメントに未実装の「願望処理」が記載されてる箇所などが散見されていますが、まあ、だいたいこんな感じです。ご容赦ください(汗)
InitializeEnemyDatasサブルーチンはsample011.asmの最初のほうで呼び出され、75体ぶんのテキキャラ情報のテーブルを生成します。
そしてメインループ(MainLoop)のなかで、MoveEnemiesサブルーチンが呼び出されて、テキをMAPデータ内部でタイル移動させて、その結果をCreateViewPort、DisplayViewPortの連続呼び出しで画面に表示させています。

(sample011.asm抜粋)    

        ;--------------------------------------------
    ; テキキャラデータを生成する
    ;--------------------------------------------
    ld b, 100
    ld hl, WK_ENEMY_PTR_TBL
    ld de, 0x0000
EnemyPtrTblInitLoop:
    ld (hl), de ; アドレスの値を初期化する
    inc hl      ; アドレスを2バイト進める
    inc hl
    djnz EnemyPtrTblInitLoop

    ld a, 127
    ld (WK_RANDOM_VALUE), a
    call InitializeEnemyDatas ; テキキャラデータ生成メイン
 
(中略)
 
MainLoop:

    call MoveEnemies

    call CreateViewPort
    call DisplayViewPort

45x45タイルの情報の中でテキキャラを移動させながら、その結果をビューポート情報として表示させている。ただそれだけの仕組みです。
enemy.asm内のMoveEnemyTileMoveサブルーチンのなかでは、テキキャラの進行方向にあわせてテキキャラのタイル番号を変化させています。テキキャラのタイル番号は以下のようにdata_map.asmで定義してあります。

ENEMY_TILE_NUMBER_TYPE1:

; ENEMY-TYPE1のタイル番号

defb 0   ; 方向=0
defb 11  ; 方向=1
defb 0   ; 方向=2
defb 12  ; 方向=3
defb 0   ; 方向=4
defb 13  ; 方向=5                     
defb 0   ; 方向=6                     
defb 14  ; 方向=7
defb 0   ; 方向=8

ENEMY_TILE_NUMBER_TYPE2:

; ENEMY-TYPE2のタイル番号 

defb 0   ; 方向=0   
defb 15  ; 方向=1
defb 0   ; 方向=2
defb 16  ; 方向=3
defb 0   ; 方向=4
defb 17  ; 方向=5
defb 0   ; 方向=6
defb 18  ; 方向=7
defb 0   ; 方向=8

方向1=上、方向3=右、方向5=下、方向7=左です。
data_enemy.asmにはテキキャラの初期出現座標を定義していて、テキキャラ5体ごとに同じ出現位置が割り当てられています。これは後続の記事で「テキキャラをやっつけたとき、再度出現させるために必要な座標データ」になります。75体ぶん全部定義したほうが良かったかも・・なんて思ったりしています。

;--------------------------------------------
; data_enemy.asm 
; テキキャラ関連データ集
;--------------------------------------------

; テキキャラのスポーン位置
ENEMY_SPAWN_POS:
    defb 43,  1 ; テキキャラ# 0 - # 4の出現位置
    defb 30,  3 ; テキキャラ# 5 - # 9の出現位置
    defb 17,  4 ; テキキャラ#10 - #14の出現位置
    defb  5,  5 ; テキキャラ#15 - #19の出現位置
    defb 41,  5 ; テキキャラ#20 - #24の出現位置
    defb 19,  7 ; テキキャラ#25 - #29の出現位置
    defb 29, 12 ; テキキャラ#30 - #34の出現位置
    defb 26, 14 ; テキキャラ#35 - #39の出現位置
    defb  6, 16 ; テキキャラ#35 - #39の出現位置
    defb 43, 16 ; テキキャラ#40 - #44の出現位置
    defb 27, 18 ; テキキャラ#45 - #49の出現位置
    defb 38, 21 ; テキキャラ#50 - #54の出現位置
    defb  8, 26 ; テキキャラ#55 - #59の出現位置
    defb 22, 26 ; テキキャラ#60 - #64の出現位置
    defb 41, 28 ; テキキャラ#65 - #69の出現位置
    defb  1, 32 ; テキキャラ#70 - #74の出現位置

いま、思いましたがdata_enemy.asmにテキキャラのタイル番号もコードしたほうが整理されてみやすくなるような気がしました(汗)

タイル情報はきちんと書き換える

enemy.asmのMoveEnemyTileMoveサブルーチンで移動方向のタイル番号を書き換えたあとに、ResetEnemyMoveSrcサブルーチンを呼び出し移動前のタイル情報を0x00(タイル番号0:床)に書き換えています。これをやらないと移動前の座標にテキのタイル番号が残ってしまうことになり、結果としてズラーっとテキキャラが表示されてしまうことになります。タイル情報はきちんと書き換えたり戻したりする必要があるということです。もちろん、タイル情報としてテキキャラを配置するのではなく、スプライトでテキキャラを表示させる場合はこんなことは考えなくてよくなります。まあ、プログラミングとコードメンテナンスと表示処理とエフェクト処理のトレードオフで考えましょう。「自分のキャラ以外は基本PCG」という今回の作り方の制約ですね。。。

進行カウンタの意味

テキキャラ管理テーブル(WK_ENEMY_DATA_TBL)は1体あたり11バイトの情報をもっていて、6バイト目(先頭から+5)に進行カウンタという情報を持っています。この進行カウンタはMoveEnemiesサブルーチンが呼び出されるたびにインクリメントされ、15になると1タイルぶん移動させて、進行カウンタはふたたび0に初期化されます。また、テキキャラの移動先が床以外のタイルであっても0に初期化されます。このカウンタはちょっと重要で、2つの意味をもっています。
ひとつめは「テキキャラの進行速度の調整のため」です。
この数値を「15になったら移動」ではなく「60になったら移動」にするとテキキャラの動く速度は遅くなります。よくゲームではハードモードとかあると思います。めちゃくちゃテキキャラの動きが速いアレです。そういった調整のために進行カウンタを使っています。なお、手元のコードを修正して30にしたらちょうどいいくらいの速度になりました。
ふたつめは「半タイル移動の表示のため」です。
たとえば最大15で1タイル動かす。という仕様であればカウンタが8になったら半タイル動かす。ということが表現できるようになります。
後続の記事ではこの進行カウンタを使って半タイル移動を実装しようと思っています。

乱数生成器

テキキャラの種類、進行方向、進行距離はすべてランダムに設定しています。MSX BIOSにはなぜかランダムな値を取得するような処理が存在しません(MSX-BASICなら存在するのに不思議に思っています)。そのため自力で乱数生成の処理を作る必要があります。このランダムな値を生成するアルゴリズムは乱数生成器と呼ばれたりします。乱数生成にはXorShiftとか線形合同法(LCG)とかM系列とかRレジスタと呼ばれる特殊なレジスタを使ったりとか、いろんなやり方があるのですがネットで拾った方法を採用しました。@fujitanozomuさん、@aburi6800さんの情報を参考にさせていただきました。ありがとうございます。

さて、common.asmに乱数生成のサブルーチンがあります。

initialize.asm 抜粋

INTCNT:equ $FCA2 ; MSX BIOSにて1/60秒ごとにインクリメントされる値が格納されているアドレス

common.asm 抜粋

;--------------------------------------------
; SUB-ROUTINE: RandomValue
; 乱数を取得する
; WK_RANDOM_VALUEに16ビットの乱数をセットして返却する
;--------------------------------------------
InitRandom:

    ld a,(INTCNT)
    ld (WK_RANDOM_VALUE),a          ; 乱数のシード値を設定

    ret

RandomValue:

    push bc

    ld a, (WK_RANDOM_VALUE)         ; 乱数のシード値を乱数ワークエリアから取得
    ld b, a
    ld a, b

    add a, a                        ; Aを5倍する
    add a, a                        ;
    add a, b                        ;

    add a,123                       ; 123を加える
    ld (WK_RANDOM_VALUE), a         ; 乱数ワークエリアに保存

    pop bc
    ret

InitRandomを最初に呼び出してWK_RANDOM_VALUE変数にSEED値(乱数生成のタネ)をセットし、以降、RandomValueを呼び出すとWK_RANDOM_VALUE変数にランダムな0から255までの値がセットされて戻ってきます。ランダムといっても完全なランダムではないため、今回の乱数は疑似乱数と呼ばれたりします。また、WK_RANDOM_VALUEの値は次の乱数生成のSEED値になります。ワークエリアのINTCNT(FCA2H)には割り込み処理時にカウントアップされるカウンタの値が入っているので事実上のSEED値とすることができる。という形ですね。
ただし、今回のサンプルでは常に同じ値になるためsample011.asmでは127という値をあえてWK_RANDOM_VALUEにセットしています。(InitRandomは呼び出していない)
例えば、タイトル画面などを表示させてSTARTボタンとかでゲームのメイン処理が呼び出される。というような仕組みであれば、ゲーム開始までにタイムラグが発生することになり、INTCNTは有効に作用すると思います。

次回は「半タイル移動」の予定です

今回はここまで。
次回は前回の記事からの宿題となっているテキキャラの半タイル移動について解説します。余裕があればテキキャラとの当たり判定も入れて、テキキャラと当たったらライフポイントを下げる仕組みとかも入れたいなと思っています。そこまで作れるようになればゲーム作りの基本的な要素はほぼまかなえるようになるかな〜??
ちなみに、先日、容量を見積もってみたら現在のプログラムではまだまだ余裕があるのでマップももっと広くできるなあ〜、2ビット1タイルデータではなく4ビット1タイルデータにすればもっといろんなタイルの情報も表現出来るよな〜、なんて思いました。
あとはサウンド・・、鬼門です・・。
でもピコピコサウンド出したいですよねえ・・
サウンドと割り込み処理については一連托生なので同じ記事になるかなー・・。

では、また!ノシ

マシン語講座:前回記事の該当箇所

前回記事では「ムダな比較が繰り返されてしまうようなCP、JRの組み合わせではなく直接アドレッシングによるジャンプ方式にしてみた」とお伝えしました。その実装箇所はいくつかありますが、ジャンプ用のテーブルを作成している箇所はinitialize.asmに書いてます。おわかりいただけますか?

initilize.asm 抜粋

(略)
 
; VRAMチェック処理のJUMP先アドレス
WK_VRAM_CHECK_PROC:equ $C044        ; 2*9 = 18バイト

; テキキャラ移動処理のJUMP先アドレス
WK_ENEMY_MOVE_PROC:equ $C056        ; 2*9 = 18バイト

; スクロール処理のJUMP先アドレス
WK_SCROLL_PROC:equ $C068            ; 2*9 = 18バイト

(略)

    ;---------------------------------------
    ; テキキャラ処理ルーチンのアドレスを
    ; 定義する
    ; ※当テーブルの利用箇所はsprite.asm
    ;---------------------------------------
    ld ix, WK_ENEMY_MOVE_PROC
    ld de, MoveEnemiesLoop1End

    ; 方向=0
    ld (ix + 0), e
    ld (ix + 1), d

    ; 方向=1(上)
    ld hl, MoveEnemiesMoveUp
    ld (ix + 2), l
    ld (ix + 3), h

    ; 方向=2
    ld (ix + 4), e
    ld (ix + 5), d
 
(以下、略)

マシン語講座:値を2倍、4倍、5倍にする簡単な方法

当サンプルではずっと前からcommon.asmにCalcMultiという掛け算用のサブルーチンを用意しています。
CalcMultiでは8回(8bitぶん)必ずループをまわしているため処理に負荷がかかります。
ですが、値を2倍、4倍、5倍にすることが目的であり、なおかつ1バイトの数値しか期待しないのであればいちばん簡単(初心者的にわかりやすい)のはADD命令を使うことです。
こんな感じですね。

; Aレジスタの値を4倍にするやりかた

ld a, 1 ; Aレジスタに1をセット
 
add a, a ; Aレジスタの値にAレジスタの値を足してAレジスタに格納(これでAレジスタの値は2倍になる)
add a, a ; Aレジスタの値にAレジスタの値を足してAレジスタに格納(これでAレジスタの値は4倍になる)

; Aレジスタの値を5倍にするやりかた

ld a, 1 ; Aレジスタに1をセット
ld b, a ; BレジスタにAレジスタの値をセット
 
add a, a ; Aレジスタの値にAレジスタの値を足してAレジスタに格納(これでAレジスタの値は2倍になる)
add a, a ; Aレジスタの値にAレジスタの値を足してAレジスタに格納(これでAレジスタの値は4倍になる)
add a, b ; Aレジスタの値にBレジスタの値を足してAレジスタに格納(これでAレジスタの値は5倍になる)

「ビットシフトも覚えたよ〜!」なら、左シフト(SLA)でも良いですよ!
こんな感じ。

; Aレジスタの値を4倍にするやりかた

ld a, 1 ; Aレジスタに1をセット
 
sla a ; Aレジスタの値を1ビット左にシフト(これでAレジスタの値は2倍になる)
sla a ; さらにAレジスタの値を1ビット左にシフト(これでAレジスタの値は4倍になる)

; Aレジスタの値を5倍にするやりかた

ld a, 1 ; Aレジスタに1をセット
ld b, a ; BレジスタにAレジスタの値をセット
 
sla a ; Aレジスタの値を1ビット左にシフト(これでAレジスタの値は2倍になる)
sla a ; さらにAレジスタの値を1ビット左にシフト(これでAレジスタの値は4倍になる)
add a, b ; Aレジスタの値にBレジスタの値を足してAレジスタに格納(これでAレジスタの値は5倍になる)

セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。