オブジェクトの管理
敵や弾のように、同時にいくつも存在してそれぞれが動くものを、同じ形式のデータの並びとして管理します。
オブジェクトとスロット
ゲームの画面には、プレイヤー・敵・弾・アイテムなど、個別に動くものがたくさん出てきます。種類は違っても、どれも「種類」「状態」「座標」「速度」のような、同じ項目のデータを持っています。
そこで、1つ分のデータの形式(構造体)を決め、それを決まった数だけ並べた領域を用意します。この1つ分の枠をスロット、スロットに入っている1つ1つをオブジェクトと呼ぶことにします。
ここでいうオブジェクトは、PPUのOBJ(スプライト)のことではありません。1つのオブジェクトが複数のスプライトで表示されることもあれば、スプライトを使わないオブジェクトもあります。
ここでは、1スロット16バイトのスロットを16個、WRAMの$1000~$10FFに置きます。
| オフセット | 大きさ | 内容 |
|---|---|---|
| +$00 | 2バイト | 種類(0なら空きスロット) |
| +$02 | 2バイト | 状態 |
| +$04 | 2バイト | タイマー |
| +$06 | 2バイト | X座標 |
| +$08 | 2バイト | Y座標 |
| +$0A | 2バイト | X方向の速度(符号付き) |
| +$0C | 2バイト | Y方向の速度(符号付き) |
| +$0E | 2バイト | 種類ごとに自由に使う |
各スロットの先頭は「$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を一定の値ずつ増やしながら(TXACLCADC #定数TAXなど)ループしていれば、構造体の配列です。
増やす値が構造体の大きさ、ループを終える値と始める値の差がスロット数×構造体の大きさです。 - Xを2ずつ増やしていて、離れたアドレスを同じXで読んでいれば、項目ごとの配列です。アドレスの間隔がスロット数×2です。
- ループの最初で読む項目が0かどうかを調べていれば、それが種類(使用中かどうか)を表す項目です。その値でジャンプテーブルを引いていれば、テーブルの各アドレスが種類ごとの処理の入口です。