z88dk-z80asm+ゲームギアでグローバル変数とOAM定数の便利な管理方法（defvars）

# はじめに

現在、z88dk の z80asm を用いてゲームギアのゲームを開発しているのですが、z80asm の `defvars` がグローバル変数やOAM（スプライト）の定数管理に便利だったので紹介します。

本書は基本的にゲームギア・プログラミング向けに書いていますが、MSX や VGS-Zero のフルアセンブリ言語でのプログラミングでも応用可能かと思われます。

なお、ゲームギア・プログラミングの基礎については以下の記事をお読み頂ければだいたい分かると思われます。

https://qiita.com/suzukiplan/items/4388874cacef18873db2

# defvars

詳細は以下のドキュメントに書かれています。

https://github.com/z88dk/z88dk/wiki/Tool---z80asm---directives#defvars-origin--name-size_id-count--

defvars は __起点となるアドレス値__ を指定して、__サイズ情報付きのラベル__ を列挙する形で記述します。

（凡例）

```asm
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)

（具体例）

```vars.asm
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` でグローバル変数を管理できます。

```vars.asm
; グローバル変数
defvars $c000
{
    player_x ds.w 1   ; プレイヤーの X 座標（実数）
    player_y ds.w 1   ; プレイヤーの Y 座標（実数）
    joypad   ds.b 1   ; ジョイパッドの入力保持
}
```

上記のラベル `joypad` にゲームギアのジョイパッド入力状態を保持するコードの例を示します。

```io.asm
.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` を用いると便利です。

```vars.asm
; 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 の前提で記述しています。（[こちらの記事](https://qiita.com/suzukiplan/items/4388874cacef18873db2)の `Hello, World` を表示するサンプルと同じ構成です）

```asm
; 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
```

上記コード中のポイントは以下の箇所です。

```asm
    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枚)
}
```