弾を打てるようにしてみた
これまでのサンプルで
・PCGで文字をキャラクターに変える
・PCGのキャラで描かれたマップをスクロールさせる
・スプライトを動かす
・テキキャラを動かす
・当たり判定をつける
・割り込み処理を使う
・割り込み処理を使って音を鳴らす
といったことができるようになりました。
今回のサンプルではプレイヤーが弾を打てるようにしてみました。
ピコピコ、パキューンなやつです。
ということで、さっそくサンプルコードをゲットだぜ!
https://github.com/sailorman-msx/games/tree/main/src/sample014
さっさと動きが見てみたいというのであればこちらをどうぞ。。。
弾を管理する変数たち
弾を管理する変数は initialize.asm に以下のとおり定義しています。
; FIREBALL移動制御用(同時に発射できるのは2個まで)
; 弾は8ドット単位で移動する
;
; +0 : 発射していれば1、そうでなければ0
; +1 : 弾のX座標(8-152)
; +2 : 弾のY座標(8-152)
; +3 : 弾のX座標X(WK_PLAYERPOSXと同じ単位)
; +4 : 弾のY座標Y(WK_PLAYERPOSYと同じ単位)
; +5 : 移動量X(0:移動量なし 1:プラス移動 2:マイナス移動)
; +6 : 移動量Y(0:移動量なし 1:プラス移動 2:マイナス移動)
; +7 : 未使用
;
WK_FIREBALL_TRIG:equ $C0C3 ; スペースキー、もしくはジョイスティックAボタンの状態
WK_FIREBALL_DATA_TBL:equ $C0C4 ; 8バイト x 2 = 16バイト
WK_FIREBALLSPRATTR:equ $C0D4 ; 8バイト(スプライト2枚分)
WK_FIREBALL_INTTIME:equ $C0DC ; 1バイト(1/60秒ごとにデクリメントされゼロになると弾を発射できる)
WK_FIREBALL_TIMER:equ 10 ; 定数(連続弾発射のためのインターバル値)
WK_FIREBALL_MAPX1:equ $C0DD ; 1バイト(弾のMAP座標X)
WK_FIREBALL_MAPY1:equ $C0DE ; 1バイト(弾のMAP座標Y)
WK_FIREBALL_MAPX2:equ $C0DF ; 1バイト(弾のMAP座標X)
WK_FIREBALL_MAPY2:equ $C0E0 ; 1バイト(弾のMAP座標Y)
弾を発射するとWK_FIREBALL_TRIGの値がインクリメントされます。
WK_FIREBALL_DATA_TBLは8バイトずつになっていて、弾の座標や移動量を管理しています。弾を発射するとWK_FIREBALL_INTTIMEに値がセットされ、その値は1/60秒ごとにデクリメントされます。また、この値が0になっていないと弾を発射できないような仕組みにしています。こうすることで弾が連射されないようにしています。それらの制御は主として sample014.asm で制御しています。
(sample014.asm)
;--------------------------------------------
; 割り込み処理開始
;--------------------------------------------
GameProc:
; 弾発射のインターバル値が0でなければデクリメントする
ld a, (WK_FIREBALL_INTTIME)
or 0
jp z, GameProc_Init2
GameProc_Init1:
dec a
ld (WK_FIREBALL_INTTIME), a
GameProc_Init2:
; 当たり判定情報の変数に値が入っていなければ
; 当たり判定をチェックする
; 当たっている場合はAレジスタに1がセットされる
(中略)
; スペースキーが押されているか?
GameProc_IsSPACE:
ld a, 0
call GTTRIG
cp $FF
jr nz, GameProc_IsAbutton
; ToDo: 弾は2発までとしたいが
; 2発打つと変になるのでとりあえず1発までとしておく
ld a, (WK_FIREBALL_TRIG)
cp 1
jr z, GameProc_IsCURSOR
; ファイアボール処理を呼び出す
call Fireball
jp GameProc_IsCURSOR
; ジョイスティック1のAボタンが押されているか?
GameProc_IsAbutton:
ld a, 1
call GTTRIG
cp $FF
jr nz, GameProc_IsCURSOR
; ToDo: 弾は2発までとしたいが
; 2発打つと変になるのでとりあえず1発までとしておく
ld a, (WK_FIREBALL_TRIG)
cp 1
jr z, GameProc_IsCURSOR
; ファイアボール処理を呼び出す
call Fireball
; ジョイスティックまたはカーソルキーの方向を取得
; GTSTCK呼び出し後、Aレジスタに方向がセットされる
GameProc_IsCURSOR:
(中略)
SpriteDisplayEnd:
; WK_FIREBALL_TRIGの値が0でなければ
; ファイアボール処理を呼び出す
ld a, (WK_FIREBALL_TRIG)
or 0
jr z, GameProcEnd
; 衝突判定中は弾の動作は止める
ld a, (WK_PLAYERCOLLISION)
or 0
jr nz, GameProcEnd
call MoveFireball
GameProcEnd:
ret
弾発射サブルーチン:Fireball
弾発射時に最初に呼び出されるサブルーチンはFireball(sprite.asm)です。今回はタイル単位の位置にプレイヤーがいないと弾を発射できなくしています。これ後で出てくるテキとの当たり判定でしんどいからそうしています。テキキャラはタイル単位で動いていますので・・。
弾が発射できる位置にプレイヤーがいると画面右上に「弾発射OK」のステータスアイコンが表示されるようにしています。
弾発射サブルーチンでは、initialize.asm で定義している弾の管理テーブル(WK_FIREBALL_DATA_TBL)を作成して、プレイヤーの向きにあわせて進行方向や弾の移動量(X方向にいくつ、Y方向にいくつ)といった値をセットしています。
(sprite.asm)
(略)
;------------------------------------------------
; SUB-ROUTINE: Fireball
; 弾の処理を行う
; 同時に発射できる弾は画面上で2発までとする
;------------------------------------------------
Fireball:
push ix
push iy
; 弾発射のインターバル中は弾を発射できない
ld a, (WK_FIREBALL_INTTIME)
or 0
jp nz, FireballInfoSetEnd
;----- 重要 --------------------------
; PLAYER座標から1を引いた値が2で割り切れない場合は
; 弾は発射できないこととする
;-------------------------------------
ld a, (WK_PLAYERPOSX)
dec a
ld d, a
call CalcDivideBy2
ld a, l
or a
jp nz, FireballInfoSetEnd
ld a, (WK_PLAYERPOSY)
dec a
ld d, a
call CalcDivideBy2
ld a, l
or a
jp nz, FireballInfoSetEnd
; ここから下は弾の座標の設定処理
ld hl, WK_FIREBALL_DATA_TBL
ld ix, hl
ld d, 1
; 弾情報の空いている場所を特定する
ld a, (ix + 0)
or 0
jr z, FireballInfoSet ; 空いている場所を特定
(以降、略)
弾を移動させる処理:MoveFireball
弾はMoveFireballサブルーチンで移動させます。このサブルーチンでは弾の管理テーブルの内容によって弾を移動させ、弾の当たり判定をおこなっています。ここでの当たり判定は壁にあたったか?という当たり判定だけです。当たり判定は、ずいぶん前の記事で紹介したCheckVRAM4x4サブルーチンを流用しています。
(略)
;--------------------------------------------
; SUB-ROUTINE: MoveFireball
; 弾の表示処理を行う
;--------------------------------------------
MoveFireball:
ld hl, WK_FIREBALL_DATA_TBL
ld ix, hl
ld b, 1
MoveFireballLoop:
ld a, (ix + 0) ; 発射フラグがたっていなければ次の弾の処理を行う
or 0
jp z, MoveFireballLoopEnd
;
; 弾が壁にぶつかっていたら弾を消す
;
ld a, (ix + 3)
ld (WK_CHECKPOSX), a ; 弾のX座標
ld a, (ix + 4)
ld (WK_CHECKPOSY), a ; 弾のY座標
call GetVRAM4x4
ld hl, WK_VRAM4X4_TBL
ld iy, hl
ld a, (iy+5)
cp $98 ; テキキャラはぶつかり対象にしない
jp c, MoveFireballCheckWall
jp MoveFireballMoveY_Plus
MoveFireballCheckWall:
cp '$' ; 床はぶつかり対象にしない
(以降、略)
テキキャラとの当たり判定の仕組み
今回のサンプルでは自キャラ以外はタイルで処理しているため
弾の当たり判定にずいぶん悩みました。
弾の視点でテキキャラと当たっているか判定する処理を考えると
・弾をMAP座標に変換する
・そのMAP座標のタイルをMAP上で走査する(調べる)
・そのタイルがテキキャラだったら当たったと判定する
という処理が必要になりますが、ものすごくCPUに負担をかけることになってしまいます。MAPを構成している全タイルを判定対象にすると45x45で2025回調べる必要がありますし、いやいやさすがにそんなことはしないでしょ。とテキキャラのテーブルだけを対象にしても75回調べる必要があります。(テキキャラは全部で75体いますからね)
うーん・・・と悩んだ結果
「テキキャラが移動するときにテキキャラ自身が弾に当たったか判定すればいいじゃん」
という結論になりました。
・ビューポート座標から10タイル以上離れていない座標にテキキャラが存在するのであれば(=弾と同時に見える範囲にテキキャラが存在していたら)弾との衝突判定処理を行う
という内容です。これだとCPUの負担はかなり軽くすることができます。
弾が衝突判断をするのではなく、テキが衝突判断をする。という処理仕様です。こういう関係性を弾目線では「パッシブ(passive)」といいます。弾の目線では受身で衝突判定をしてもらうという感じです。
(enemy.asm)
(略)
MoveEnemies:
ld hl, WK_ENEMY_DATA_TBL ; テキキャラ管理用テーブルの先頭アドレスをHLレジスタにセット
ld (WK_ENEMY_DATA_IDX), hl ; HLレジスタの値をテキキャラインデックス変数にセット
call GetFireballMapPos
MoveEnemiesLoop1:
ld ix, (WK_ENEMY_DATA_IDX)
ld a, (ix + 0)
or a; テキキャラポインタテーブルの+0番目の値がゼロの場合は処理を抜ける
jp z, MoveEnemiesEnd
ld a, (ix + 1)
ld (WK_ENEMY_POSX), a
ld a, (ix + 2)
ld (WK_ENEMY_POSY), a
; 弾のMAP座標と合致していたら衝突処理を行う
; ただし、ビューポート座標から10タイル以上離れていたら
; 衝突処理は行わない
ld a, (WK_VIEWPORTPOSX)
ld b, a
ld a, (WK_ENEMY_POSX)
sub b ; A = A - B
cp 10
jp nc, MoveEnemiesMoveTileInit
ld a, (WK_VIEWPORTPOSY)
ld b, a
ld a, (WK_ENEMY_POSY)
sub b ; A = A - B
cp 10
jp nc, MoveEnemiesMoveTileInit
; 弾との衝突判定処理呼び出し
ld a, (WK_FIREBALL_TRIG)
or 0
jp z, MoveEnemiesMoveTileInit
call CheckFireballCollision
CheckFireballCollisionというサブルーチンでは弾の座標をMAP座標に変換してテキキャラとの衝突判定を実施しています。また、今回のサンプルではテキキャラに弾が当たったらそのタイルを壁タイルにしています。
次回予告:第一部完まで残り1記事
今回の記事までで、かなりゲームっぽいものが作れるようになったかと思います。
筆者としても行き当たりばったりでここまで記事を連載してきましたが、書き進めながら少年時代を思い返しつつ、新しいことを知ったりと有意義なものになったと思っていますし、MSXの仕組みやZ80マシン語でのプログラミングについてより深く知れたのではないかと思います。
また、ぼちぼちこのnote記事を読んでくださるかたたちも増えてきてるようで、本当にありがたいかぎりです。感謝感謝でございます。
あわせて、今回記事までに参考にさせていただいたおのおの諸先輩方にも感謝感謝でございます。
さて、次回は第一部の完了として
「完全にゲームにする」ということを目標にしたい
と思っています。
今年中に出来るかな〜。完成させたいな〜。というかんじ(汗)
まだまだ無駄コードも散見されていたり、もはや使ってない変数があったりという箇所をダイエットさせて、ギミック(仕掛け)やオープニング画面、ゲームオーバー画面などもきちんと整備して昔のMSX-FANやベーマガの投稿記事に見劣りしないものを作って
「第一部完。セーラーマン先生の新しい連載をお待ちください!」
という黄金期ジャンプ的な終わりにしたいと思っています。
では、また!ノシ