ダイレクトページでオブジェクトを扱う

ダイレクトページレジスタを処理中のオブジェクトのスロットに向けて、構造体の項目をダイレクトページのアドレスで読み書きします。

考え方

オブジェクトの管理では、ダイレクトページを$0000にしたまま、スロットの先頭アドレスをXレジスタに入れ、LDA $06,X のように項目を読みました。
構造体は同じものを使います。

オフセット大きさ内容
+$002バイト種類(0なら空きスロット)
+$022バイト状態
+$042バイトタイマー
+$062バイトX座標
+$082バイトY座標
+$0A2バイトX方向の速度(符号付き)
+$0C2バイトY方向の速度(符号付き)
+$0E2バイト種類ごとに自由に使う

65816のダイレクトページは、バンク0の好きな位置から始まる256バイトの領域です(ダイレクトページレジスタ)。
ダイレクトページレジスタ(D)にスロットの先頭アドレスを入れておけば、そのスロットの項目は LDA $06 のように、構造体の中のオフセットだけで読み書きできます。
つまり、オブジェクトの管理でXレジスタに入れていたスロットの先頭アドレスを、代わりにダイレクトページレジスタに入れる方法です。どちらも似た方法ですが、それぞれに利点と欠点があります。

方法X座標を読む命令実際に読むアドレス
Xレジスタを使うLDA $06,X$06 + X(D = $0000)
ダイレクトページを使うLDA $06D + $06

2つの方法の比較

Xレジスタを使うDレジスタを使う
項目を読む命令の長さLDA dp,X
2バイト
LDA dp
2バイト
命令サイクル4~63~5
ダイレクトページ$0000の変数使いやすい使いにくい
レジスタ割り当ての自由度低い高い
項目に置いたポインタでの間接アドレッシング(dp,X) のみ(dp)・(dp),Y・[dp]・[dp],Y

ダイレクトページを各オブジェクトの先頭アドレスにする最大の利点は、Xレジスタを占有しないのでレジスタ割り当ての自由度が高まることと、オブジェクトが持っている固有のデータに間接アクセスしやすいことです。
たとえば、あるオブジェクトが状態の値に応じたテーブルを持っていて、そのテーブルのアドレスが+$0Eに書き込まれているとします。

Xレジスタを使って構造体にアクセスしている場合、LDA ($0E,X) でポインタの指す先を読むことはできますが、そこにインデクスを足す形はありません。そのため、ポインタをいったんダイレクトページの決まった場所に写してから使います。

    LDA $0E,X       ; テーブルのアドレス
    STA $30         ; ダイレクトページの作業用の場所に写す
    LDA $02,X       ; 状態
    ASL A           ; テーブル内のインデクス
    TAY
    LDA ($30),Y     ; テーブルを読む
    ; ... 読み取った値を使った処理

ダイレクトページがこのオブジェクトを指している場合は、ポインタを写す必要がありません。

    LDA $02         ; 状態
    ASL A           ; テーブル内のインデクス
    TAY
    LDA ($0E),Y     ; テーブルを読む
    ; ... 読み取った値を使った処理

オブジェクトごとのポインタを使うたびに写す手間と作業用の場所が要らなくなり、Xレジスタもほかの用途に使えます。

一方で、ダイレクトページでオブジェクトを指す方法には、ほかの変数へのアクセスが不便になるという弱点があります。
たとえば、ゲーム中ダイレクトページレジスタを0のまま変更しないと決めた場合、$7E:0000~$7E:00FFの変数にはいつも LDA dp の形でアクセスできます。
ダイレクトページをオブジェクトに向けている間は、これらの変数にも LDA abs の形でアクセスしなければなりません(ほかの変数を読む)。
また、ダイレクトページレジスタの下位8ビットが0でない(ページ境界にない)場合は、ダイレクトページを使う命令に1サイクル追加されるため、速さの利点はなくなります(注意を参照)。

全部のオブジェクトを動かす

スロットの配置はオブジェクトの管理と同じです(1スロット16バイト、16個、$1000~$10FF)。スロットを1つ進めるたびに、ダイレクトページレジスタを16ずつ進めます。

; すべてのオブジェクトの処理を呼び出す
UpdateObjects:
    REP #$30
    LDA.w #$1000    ; 最初のスロットの先頭
.loop
    TCD             ; ダイレクトページ = スロットの先頭
    LDA $00         ; 種類
    BEQ .next       ; 0なら空きスロット
    ASL A
    TAX
    JSR (.handlers,X)
.next
    TDC
    CLC
    ADC.w #$0010    ; 次のスロットへ
    CMP.w #$1100
    BNE .loop
    LDA.w #$0000
    TCD             ; ダイレクトページを元に戻す
    RTS

.handlers
    dw $0000        ; 0: 空きスロット(呼ばれない)
    dw ObjEnemy     ; 1: 敵
    dw ObjBullet    ; 2: 弾

種類ごとの処理は、ダイレクトページのアドレスで自分の項目を扱います。スロットの先頭アドレスを読み直す必要はありません。
ループは16ビットのまま続くので、種類ごとの処理はアキュムレータとインデクスレジスタを16ビットに戻してから返るものとします。

; 弾: 速度を座標に加え、画面の外に出たら消える
ObjBullet:
    LDA $06         ; X座標
    CLC
    ADC $0A         ; X方向の速度
    STA $06
    CMP.w #$0100
    BCC .alive      ; 0~255なら画面の中
    STZ $00         ; 種類 = 0: このオブジェクトを消す
.alive
    RTS

構造体の中のポインタを使う

次の例では、+$0E に置いたポインタが速度の表を指していて、毎フレーム表から1つずつ値を読み出します。

; +$0E のポインタが指す表から、X方向の速度を1つ読んで進める
    LDA ($0E)       ; ポインタが指す値
    STA $0A         ; X方向の速度にする
    LDA $0E
    CLC
    ADC.w #$0002
    STA $0E         ; ポインタを次の値へ進める

(dp) で読むデータのバンクは、データバンクレジスタの値になります。表はデータバンクレジスタが指すバンクに置くか、24ビットのポインタにして [dp] で読んでください。

ほかの変数を読む

ダイレクトページをスロットに向けている間は、LDA $30 は「スロットの先頭 + $30」を読みます。ふだんダイレクトページのアドレスで読んでいる変数(ダイレクトページが$0000のときの$0030など)を読むには、絶対アドレスを使います。

; プレイヤーのX座標($0030)と自分のX座標の差
    LDA.w $0030     ; 絶対アドレス: データバンクの$0030
    SEC
    SBC $06         ; ダイレクトページ: 自分のX座標

asarでは .w を付けると絶対アドレスで書かれます。データバンクレジスタが$00または$80なら、$0000~$1FFFはWRAMの先頭8KBに当たります。

ほかの変数をたくさん使う処理では、ダイレクトページをいったん$0000に戻し、終わったら元に戻します。

    PHD             ; 今のダイレクトページを保存する
    LDA.w #$0000
    TCD
    ; ... ダイレクトページ$0000の変数を使う処理 ...
    PLD             ; スロットの先頭に戻す

ほかのオブジェクトの項目は、そのスロットの先頭アドレスをXに入れ、LDA.w $0006,X のように絶対アドレスで読みます。
ダイレクトページがスロットを指している間に LDA $06,X と書くと、「処理中のスロットの先頭 + $06 + X」を読んでしまいます。

注意

  • ダイレクトページは常にバンク0にあります。WRAMをダイレクトページとして使えるのは、バンク0から見える$0000~$1FFF(WRAMの先頭8KB)だけなので、スロットはこの範囲に置いてください。
  • ダイレクトページのアドレスは8ビットなので、1つの構造体の大きさは256バイトまでです。
  • スロットの先頭が$xx00でないと、ダイレクトページを使う命令に1サイクルずつ追加されます。
    この例では$1000のスロット以外はすべて該当し、命令サイクルはオブジェクトの管理の dp,X を使う方法(ダイレクトページ$0000)と同じになります。
  • 種類ごとの処理の中でダイレクトページレジスタを書き換えた場合は、戻る前に元に戻してください。
    上の例のループは、TDC命令で今のダイレクトページから次のスロットの位置を計算しています。
  • オブジェクトの処理の途中でもNMIやIRQは発生します。割り込み処理ルーチンでは、入口でダイレクトページを自分で設定し、出口で元に戻してください。
    (NMIとフレームの処理の例では、PHD命令とPLD命令で保存と復帰をしています。)

コードから構造を読み取る

既存のプログラムでは、次のようなところからこの方法が使われていることが分かります。

  • ループの中でTCD命令が実行され、そのたびにダイレクトページが一定の値ずつ進められています。進める値が構造体の大きさです。
  • そのループから呼ばれるサブルーチンに、LDA $06 のような1バイトのアドレスの命令がたくさん出てきます。
    これらのアドレスは呼ばれたときのダイレクトページからのオフセットなので、同じ LDA $06 でも、どのスロットを処理しているかによって読む番地が変わります。
  • $0100より小さいアドレスを、あえて絶対アドレス(LDA $0030 が AD 30 00 の3バイト)で読んでいる命令があれば、そこではダイレクトページが$0000以外を指している可能性が高いと考えられます。

ダイレクトページのアドレスを読み解くときは、その命令が実行されるときのダイレクトページレジスタの値を確かめてください。

参照

オブジェクトの管理
ダイレクトページ(アドレッシングモード)
TCD命令
TDC命令
PHD命令
PLD命令
NMIとフレームの処理