大きなマップのスクロール
タイルマップより大きなマップを、新しく見えてくる列だけを書き足しながら横にスクロールさせます。
考え方
BGのタイルマップは、いちばん大きくしても64×64タイル(8×8のタイルなら512×512ピクセル)です(BGnSC)。それより大きなステージを表示するには、タイルマップを何度も使い回します。
スクロールの値(BGnHOFS)はタイルマップの大きさで折り返すので、タイルマップの右端の先には左端がつながって見えます。そこで、タイルマップを輪のように使い、カメラが右へ進んで画面の右端に新しい列が入ってくるたびに、その列のタイルを、もう画面に映っていない古い列の位置に上書きします。
ここでは、次のように決めます。
| 項目 | 値 |
|---|---|
| BG1のタイルマップ | VRAM $7000~$77FF、64×32タイル(BG1SC = $71) |
| ステージの大きさ | 横256列 × 縦32行(2048×256ピクセル) |
| カメラのX座標 | $30~$31(ピクセル) |
タイルマップの幅は画面の幅(32タイル)の2倍なので、画面に映っている33列(スクロールが8の倍数でないときは端の列が半分ずつ見える)のほかに、書き足すための余裕が十分にあります。
ステージのデータを列ごとに並べる
横にスクロールするステージでは、書き足すのはいつも縦1列です。そこで、ステージのデータを列ごとに並べておきます(1列 = 32タイル × 2バイト = 64バイト)。こうすると、1列分のデータがROMの中で連続するので、そのままDMAで転送できます。
| ROMの中の位置 | 内容 |
|---|---|
| MapData + 0 | 0列目の0行目~31行目のタイル |
| MapData + 64 | 1列目の0行目~31行目のタイル |
| … | … |
| MapData + 列 × 64 | その列の0行目~31行目のタイル |
VRAMのタイルマップは行ごとに並んでいます(タイルマップフォーマット)。縦1列を書き込むには、VMAINCでアドレスの増分を32にします。そうすると、1ワード書き込むたびに書込先が1行下に進みます。
このページのコードで使うMapData(mapdata.bin)と、そのタイルデータ(maptiles.bin)は、サンプルの素材に入っています。
1列を書き込む
64×32のタイルマップは、32×32のタイルマップが左右に2枚並んだものです。列の番号を64で割った余りのうち、32の位が左右どちらの1枚かを、下位5ビットがその中の列を表します。
; 1列分のタイルの転送を予約する
; 入力: A = 列の番号(ステージの左端から、8ピクセル単位)
; 列がステージの外なら何もしないでキャリーをセットして返る
; 予約できなければキャリーをクリアして返る
; 作業用: $00~$08
; A・X・Yは16ビットで呼ぶこと
LoadColumn:
CMP.w #256
BCS .outside ; ステージの外(負の列も符号なしで256以上になる)
PHA
AND.w #$001F
STA $08 ; 32×32の中の列
PLA
PHA
AND.w #$0020
ASL A
ASL A
ASL A
ASL A
ASL A ; 左の1枚なら$0000、右の1枚なら$0400
ORA.w #$7000
ORA $08
STA $03 ; 転送先
PLA
ASL A
ASL A
ASL A
ASL A
ASL A
ASL A ; 列の番号 × 64
CLC
ADC.w #MapData&$FFFF
STA $00 ; 転送元
LDA.w #$0040
STA $05 ; 64バイト
SEP #$20
LDA.b #MapData>>16
STA $02
LDA.b #$81
STA $07 ; VMAINC: $2119への書込後にアドレスを32増やす
JSR QueueVram
REP #$20
RTS
.outside
SEC
RTS
転送はVRAM転送のキューのQueueVramで予約し、NMI処理ルーチンでまとめて実行します。
カメラに合わせて書き足す
前のフレームまでに処理したカメラの列(カメラのX座標 ÷ 8)を$34に覚えておき、今のカメラの列と比べます。
- 右へ1列進んだら、画面の右端に入ってくる列(カメラの列 + 32)を書き込む。
- 左へ1列戻ったら、画面の左端の1つ左の列(カメラの列 − 1)を書き込む。
1フレームに8ピクセル以上動いた場合は、1列ずつ進めながら繰り返します。
; カメラの移動に合わせて、新しく見えてくる列を書き込む
; $30~$31 = カメラのX座標
; $34~$35 = 前回までに処理したカメラの列
; A・X・Yは16ビットで呼ぶこと
UpdateColumns:
.loop
LDA $30
LSR A
LSR A
LSR A ; 今のカメラの列
CMP $34
BEQ .done
BCC .left
LDA $34
INC A ; 右へ1列進む
PHA
CLC
ADC.w #$0020 ; 画面の右端に入ってくる列
JSR LoadColumn
PLA
BCC .done ; 予約できなければ次のフレームでやり直す
STA $34
BRA .loop
.left
LDA $34
DEC A ; 左へ1列戻る
PHA
DEC A ; 画面の左端の1つ左の列
JSR LoadColumn
PLA
BCC .done
STA $34
BRA .loop
.done
RTS
PLA命令はキャリーフラグを変えないので、LoadColumnの結果をPLA命令のあとで判定できます。
NMI処理ルーチンでは、予約した転送を実行し、カメラのX座標をそのままBG1HOFSに書き込みます。タイルマップの幅が512ピクセルなので、スクロールの値も512で折り返し、書き込んだ列の位置と一致します。
; NMI処理ルーチンの中で(Aは8ビット、X・Yは16ビット)
JSR RunVramQueue
LDA $30
STA $210D ; BG1HOFS 下位バイト
LDA $31
STA $210D ; BG1HOFS 上位バイト
最初の画面を作る
ステージの開始時は、強制ブランク中に、画面に映る範囲と左右の1列ずつ(カメラの列 − 1 からカメラの列 + 32 までの34列)を書き込みます。予約の表は16件分しかないので、ここでは1列予約するごとにすぐ実行します。
; 最初の画面を作る(強制ブランク中に呼ぶ)
; A・X・Yは16ビットで呼ぶこと
InitColumns:
LDA $30
LSR A
LSR A
LSR A
STA $34 ; カメラの列
DEC A
STA $36 ; 書き込む列
.loop
LDA $36
JSR LoadColumn
SEP #$20
JSR RunVramQueue
REP #$20
INC $36
LDA $34
CLC
ADC.w #33
CMP $36
BNE .loop ; カメラの列 + 32 まで書き込んだら終わり
RTS
応用
- 縦にもスクロールする場合は、同じ考え方で行も書き足します。行を書き込むときはVMAINCの増分を1にし、1行分のタイルを転送します。縦と横の両方を書き足すステージでは、ステージのデータをどちらかの向きに並べると、もう一方の向きは連続しなくなります。その場合は、16×16ピクセルなどのブロックの番号でステージを持ち、書き足す1列(1行)分のタイルをWRAMの作業用の領域に組み立ててから転送する方法がよく使われます。
- 64×32のタイルマップの片側(32×32)だけを使い、画面の幅ぎりぎりで書き足すこともできます。この場合は、新しい列を書き込む前にその位置が画面に映らないよう、書き足すタイミングと位置をより厳密に決める必要があります。
コードから構造を読み取る
既存のプログラムでは、次のような処理からこの方式を見つけられます。
- NMI処理ルーチンなどで、VMAINCに$81(増分32)を書き込んでから、タイルマップの範囲のVRAMへ転送している。1回の転送のバイト数が1列の大きさ(行数 × 2)です。
- カメラの座標を右に3回(8ピクセルのタイル)または4回(16ピクセルのブロック)シフトし、前のフレームの値と比べている。値が増えた方向によって、書き込む列の位置を変えています。
- 列の番号から転送先を求めるときに、
AND #$001FやAND #$0020(64×32のタイルマップの左右)のような計算をしている。
転送元を計算している箇所をたどると、ステージのデータの置き場所と並び方(1列の大きさ)が分かります。
参照
BGを表示する
VRAM転送のキュー
タイルマップフォーマット
BGnSC レジスタ
BGnOFS レジスタ
VMAINC レジスタ