はじめに
現在、z88dk の z80asm を用いてゲームギアのゲームを開発しているのですが、z80asm の defvars
がグローバル変数やOAM(スプライト)の定数管理に便利だったので紹介します。
本書は基本的にゲームギア・プログラミング向けに書いていますが、MSX や VGS-Zero のフルアセンブリ言語でのプログラミングでも応用可能かと思われます。
なお、ゲームギア・プログラミングの基礎については以下の記事をお読み頂ければだいたい分かると思われます。
defvars
詳細は以下のドキュメントに書かれています。
defvars は 起点となるアドレス値 を指定して、サイズ情報付きのラベル を列挙する形で記述します。
(凡例)
defvars 起点アドレス
{
ラベル1 {ds.b | ds.w | ds.p | ds.q} 個数
ラベル2 {ds.b | ds.w | ds.p | ds.q} 個数
ラベル3 {ds.b | ds.w | ds.p | ds.q} 個数
:
}
- ds.b = 8 bits (1 byte)
- ds.w = 16 bits (2 bytes)
- ds.p = 24 bits (3 bytes)
- ds.q = 32 bits (4 bytes)
(具体例)
defvars $1000
{
hoge ds.b 1 ; hoge = $1000 (1 byte x 1)
hige ds.b 3 ; hige = $1001 (1 byte x 3)
fuga ds.w 1 ; fuga = $1004 (2 bytes x 1)
}
ゲームギアでの用例
ゲームギアで defvars
を用いたグローバル変数と OAM 定数管理の用例を記します。
(1) グローバル変数
ゲームギアの RAM の先頭アドレスは 0xC000
です。
VGS-Zero の RAM の先頭アドレスも同じく
0xC000
です。MSX はスロット構成によりケースバイケースですが、ROM ゲームを開発する場合の初期状態はゲームギアと同様に
0xC000
(slot3) が RAM という前提でコードを書いても問題ないものと思われます。
そこで、起点アドレス 0xC000
の defvars
でグローバル変数を管理できます。
; グローバル変数
defvars $c000
{
player_x ds.w 1 ; プレイヤーの X 座標(実数)
player_y ds.w 1 ; プレイヤーの Y 座標(実数)
joypad ds.b 1 ; ジョイパッドの入力保持
}
上記のラベル joypad
にゲームギアのジョイパッド入力状態を保持するコードの例を示します。
.io_joypad
; ===== start ボタン以外の入力を拾う =====
in a, ($dc)
and $3F ; 上位2ビット(外部入力)をマスク
ld b, a
; ===== start ボタンの入力を拾う =====
in a, ($00)
and $80 ; スタートボタン(最上位ビット)以外をマスク
or b ; ビットレイアウト: `s-abrldu`
; ===== グローバル変数 joypad に格納 =====
ld (joypad), a
ret
(2) OAM 定数管理
ゲームギアでは 64 枚のスプライトを利用することができますが、「何番目のスプライトを何につかうか」 という情報管理に defvars
を用いると便利です。
; OAM アドレス管理
defvars $6800 ; OAM アドレス ($2800) + アドレス設定フラグ ($4000)
{
oam_y ds.b 64 ; Y座標, Y座標, Y座標...
oam_unused ds.b 64 ; 未使用領域
oam_x ds.b 128 ; X座標, パターン, X座標, パターン...
}
; スプライトインデクスの管理
defvars $0000
{
oi_jiki ds.b 6 ; 自機のスプライト (6枚)
oi_shot ds.b 4 ; 自機ショットのスプライト (4枚)
oi_spray ds.b 8 ; 水しぶきのスプライト (8枚)
}
OAM のデータ構造は次のような形になっています。
offset | size | description |
---|---|---|
+0x00 | 64 bytes | Y 座標 |
+0x40 | 64 bytes | 未使用領域 |
+0x80 | 128 bytes | X 座標 + パターン番号 |
オブジェクト単位の連番構造になっていない構造のため、defvars
では OAM のインデクスのみ管理しておき、VRAM アドレスを設定する時に OAM アドレスとインデクス値を加算する形で実装するのがスマートだと考えられます。
以下、6 枚のスプライトを使ったキャラクタ(自機)の Y 座標、X 座標、パターン番号を設定するサンプルコードを示します。
なお、以下のコードは VRAM の OAM アドレスが 0x2800 の前提で記述しています。(こちらの記事の
Hello, World
を表示するサンプルと同じ構成です)
; VRAM アドレスやレジスタ設定用のサブルーチン
.vdp_setreg
ld a, l
out ($bf), a
ld a, h
out ($bf), a
ret
; 次の並びで 6 枚のスプライトを使った自機の OAM を設定するサブルーチン
; +-------+-------+-------+
; | ptn00 | ptn01 | ptn02 |
; +-------+-------+-------+
; | ptn10 | ptn11 | ptn12 |
; +-------+-------+-------+
.render_jiki
; jiki の Y 座標を OAM に設定
di
ld hl, oam_y + oi_jiki
call vdp_setreg
ld a, (player_y + 1) ; NOTE: 座標は固定小数点数で上位 8 bits (アドレス上はリトルエンディアン) が整数部分
out ($be), a ; ptn00 の Y 座標を設定
out ($be), a ; ptn01 の Y 座標を設定
out ($be), a ; ptn02 の Y 座標を設定
add $08
out ($be), a ; ptn10 の Y 座標を設定
out ($be), a ; ptn11 の Y 座標を設定
out ($be), a ; ptn12 の Y 座標を設定
; jiki の X 座標 と パターン番号 を OAM に設定
ld hl, oam_x + oi_jiki * 2
call vdp_setreg
ld a, (player_x + 1)
out ($be), a
ld a, player_ptn00
out ($be), a
ld a, (player_x + 1)
add $08
out ($be), a
ld a, player_ptn01
out ($be), a
ld a, (player_x + 1)
add $10
out ($be), a
ld a, player_ptn02
out ($be), a
ld a, (player_x + 1)
out ($be), a
ld a, player_ptn10
out ($be), a
ld a, (player_x + 1)
add $08
out ($be), a
ld a, player_ptn11
out ($be), a
ld a, (player_x + 1)
add $10
out ($be), a
ld a, player_ptn12
out ($be), a
ei
ret
上記コード中のポイントは以下の箇所です。
ld hl, oam_y + oi_jiki
ld hl, oam_x + oi_jiki * 2
アセンブリソース上では定数の加算や乗算ができ、演算結果が求まった状態でアセンブルされるので動作上の速度への影響がありません。
スプライトの優先順位を入れ替えたい場合、スプライトインデクスの defvars
の並び順を変えて再アセンブルするだけで簡単に調整ができます。
例えば、以下のようにコード変更すれば水しぶきのスプライト(oi_spray
)の描画有線順位が最優先(若いスプライト番号)になります。
; スプライトインデクスの管理
defvars $0000
{
oi_spray ds.b 8 ; 水しぶきのスプライト (8枚)
oi_jiki ds.b 6 ; 自機のスプライト (6枚)
oi_shot ds.b 4 ; 自機ショットのスプライト (4枚)
}