大きなマップのスクロール

タイルマップより大きなマップを、新しく見えてくる列だけを書き足しながら横にスクロールさせます。

考え方

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 + 00列目の0行目~31行目のタイル
MapData + 641列目の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 レジスタ