オブジェクトの管理

敵や弾のように、同時にいくつも存在してそれぞれが動くものを、同じ形式のデータの並びとして管理します。

オブジェクトとスロット

ゲームの画面には、プレイヤー・敵・弾・アイテムなど、個別に動くものがたくさん出てきます。種類は違っても、どれも「種類」「状態」「座標」「速度」のような、同じ項目のデータを持っています。

そこで、1つ分のデータの形式(構造体)を決め、それを決まった数だけ並べた領域を用意します。この1つ分の枠をスロット、スロットに入っている1つ1つをオブジェクトと呼ぶことにします。
ここでいうオブジェクトは、PPUのOBJ(スプライト)のことではありません。1つのオブジェクトが複数のスプライトで表示されることもあれば、スプライトを使わないオブジェクトもあります。

ここでは、1スロット16バイトのスロットを16個、WRAMの$1000~$10FFに置きます。

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

各スロットの先頭は「$1000 + スロット番号 × 16」です。各スロットの先頭アドレスをXレジスタに入れ、ダイレクトページレジスタを0に設定しておけば、LDA $06,X でそのスロットのX座標を読めます。

ここでは簡単にするため、座標はピクセル単位の整数にしています。1ピクセルより細かく動かす場合は、固定小数点で動かすのように小数部の項目を足してください。

種類が0のスロットを空きとみなすので、オブジェクトを消すときは種類に0を書き込むだけで済みます。

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

毎フレーム、スロットを先頭から順に調べ、空きでないスロットについて種類ごとの処理を呼び出します。処理の振り分けにはジャンプテーブルを使います。

; すべてのオブジェクトの処理を呼び出す
; $00 = 処理中のスロットの先頭アドレス
; Dレジスタは0に設定しておく
UpdateObjects:
    REP #$30
    LDX.w #$1000
.loop
    LDA $00,X       ; 種類
    BEQ .next       ; 0なら空きスロット
    STX $00
    ASL A
    TAX
    JSR (.handlers,X)
    LDX $00         ; 処理の中でXが変わっていてもよいように読み直す
.next
    TXA
    CLC
    ADC.w #$0010    ; 次のスロットへ
    TAX
    CPX.w #$1100    ; 最後のスロットの次のアドレス
    BNE .loop
    RTS

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

種類ごとの処理は、入口で$00からスロットの先頭アドレスを読み出して使います。ループは16ビットのまま続くので、種類ごとの処理はアキュムレータとインデクスレジスタを16ビットに戻してから返るものとします。

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

X座標が0より小さくなると$FFxxになるので、符号なしで$0100と比べるだけで、左右どちらの端から出たかを1回で判定できます。

オブジェクトを作る

新しいオブジェクトを作るときは、空きスロットを探し、項目を初期化してから種類を書き込みます。

; 空きスロットを探す
; 見つかればXにスロットの先頭アドレスを入れてキャリーをセット、
; 空きがなければキャリーをクリアして返る
FindFreeSlot:
    REP #$30
    LDX.w #$1000
.loop
    LDA $00,X
    BEQ .found
    TXA
    CLC
    ADC.w #$0010
    TAX
    CPX.w #$1100
    BNE .loop
    CLC             ; 空きスロットがない
    RTS
.found
    SEC
    RTS
; 弾を作る($30 = X座標、$32 = Y座標)
    JSR FindFreeSlot
    BCC .full       ; 空きがなければ作らない
    STZ $02,X       ; 状態
    STZ $04,X       ; タイマー
    LDA $30
    STA $06,X       ; X座標
    LDA $32
    STA $08,X       ; Y座標
    LDA.w #$0003
    STA $0A,X       ; X方向の速度: 1フレームに3ピクセル
    STZ $0C,X       ; Y方向の速度
    LDA.w #$0002
    STA $00,X       ; 種類 = 弾(最後に書き込む)
.full

種類を最後に書き込むのは、初期化の途中の値が残ったスロットを「使用中」に見せないためです。
ほかのオブジェクトの処理の中からオブジェクトを作った場合は、FindFreeSlotでXが書き換わるので、そのあと自分のスロットを扱う前にLDX $00で読み直してください。

空きスロットがないときにどうするかは、ゲームの作りによって決めます。作るのをあきらめる、古いオブジェクトを消して空ける、重要なオブジェクト用のスロットを別に取っておく、などの方法があります。

処理の順番

スロットは毎フレーム同じ順番で処理されます。このため、次のような性質があります。

  • オブジェクトの処理の中で作ったオブジェクトは、自分より後ろのスロットに入れば同じフレームのうちに処理され、前のスロットに入れば次のフレームから処理されます。
    作ったフレームに1回動くかどうかが、空きスロットの位置で変わります。
  • あるオブジェクトがほかのオブジェクトの座標を参照する場合、相手が前のスロットにあれば今のフレームで動いたあとの座標を、後ろのスロットにあれば動く前の座標を読むことになります。
  • スロットの順番でOAMの写しに書き込むと、スプライト同士が重なったときの前後関係もスロットの順番で決まります(OAMの番号の小さいほうが手前に表示されます)。

この違いが問題になる場合は、プレイヤーのように必ず先に処理したいものを先頭のスロットに固定する、種類ごとに使うスロットの範囲を分ける、といった方法で順番を決めておきます。

画面に表示する

表示も同じように、スロットを順に調べて、空きでないスロットごとにOAMの写しへスプライトを書き込みます。
オブジェクトの数はフレームごとに変わるので、毎フレームすべてのスプライトをいったん画面の外に置いてから書き込むか、書き込んだ数のあとに残ったスプライトを画面の外へ置きます。
OAMの写しの扱いはスプライトを表示するを参照してください。

2通りの並べ方

ここでは1つのオブジェクトの項目を連続して並べました(構造体の配列)。これとは別に、項目ごとに配列を作る並べ方もあります(項目ごとの配列)。

; 項目ごとの配列の例(16スロット、1項目2バイト)
;   $1000~$101F: 種類
;   $1020~$103F: 状態
;   $1040~$105F: タイマー
;   $1060~$107F: X座標
;   ...
; X = スロット番号×2
    LDA $1060,X     ; X座標
    CLC
    ADC $10A0,X     ; X方向の速度
    STA $1060,X
構造体の配列項目ごとの配列
Xに入れる値スロットの先頭アドレススロット番号×2
1つのオブジェクトをまとめてクリア・コピーするしやすい項目ごとに分かれる
ダイレクトページを向けて使うできるできない
あとから項目を増やす構造体の大きさが変わる配列を1つ足すだけ

構造体の配列を使うと、ダイレクトページでオブジェクトを扱う方法も使えます。

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

既存のプログラムを調べるときも、この形を知っていると、オブジェクトを管理している領域を見つけやすくなります。

  • 近いアドレスに LDA $xx,X や STA $xx,X あるいは LDA $xxxx,X や STA $xxxx,X が並び、Xを一定の値ずつ増やしながら(TXA CLC ADC #定数 TAX など)ループしていれば、構造体の配列です。
    増やす値が構造体の大きさ、ループを終える値と始める値の差がスロット数×構造体の大きさです。
  • Xを2ずつ増やしていて、離れたアドレスを同じXで読んでいれば、項目ごとの配列です。アドレスの間隔がスロット数×2です。
  • ループの最初で読む項目が0かどうかを調べていれば、それが種類(使用中かどうか)を表す項目です。その値でジャンプテーブルを引いていれば、テーブルの各アドレスが種類ごとの処理の入口です。

参照

ジャンプテーブルと状態遷移
ダイレクトページでオブジェクトを扱う
固定小数点で動かす
当たり判定
スプライトを表示する