ファミコン開発:スクロールとPPUレジスタ
ファミコンのスクロール機能とその応用で必要になるPPUレジスタについてまとめたいと思います。ファミコンのスクロール機能はシンプルな使い方をしている限りは簡単ですが、ちょっと工夫すると途端に?がたくさん出てきます。初めのうちはプログラムではよくあるおまじないのように、型として覚えておけば十分なのですが、複雑なことをしようとするとやはり深い理解が必要となるでしょう。つまり、今の私がそんな状態なわけです…。記事のほとんどは既存のファミコン開発関連ページ(主にNESdev)で得た知識です。詳しい情報はそちらを参照ください。ここはあくまで自分の理解のために整理しているメモです。
簡単なスクロール設定方法
ファミコンの基本的なスクロール設定方法は以下の方法です。
lda #00 ; X座標
sta $2005 ; スクロールレジスタに設定
lda #00 ; Y座標
sta $2005 ; スクロールレジスタに設定
X,Yそれぞれの座標を$2005にセットすれば画面が値の分だけズレてくれます。ポイントとしては、必ず【2回セットで実行】すること。またVBlank中に実行することです。$2005に値をセットしてもすぐには画面に反映されず、VBlank終了後にPPUによって自動的に行われます。
$2006でVRAMアドレスをセットするとスクロールが崩れる
ある程度ゲーム開発が進むとBGへの書き込み頻度も上がっていきます。初めのうちはゲーム初期化時に画面全体を描くだけで充分かもしれませんが、そのうちスコアの書き換えや、パレット変更などVRAMを動的に書き変える必要がでてくるわけです。すると(例えスクロールしない固定画面のゲームでも)$2006でVRAMのアドレスをセットしたことで、スクロール座標が勝手に変わってしまう問題が発生します。これはPPU内のレジスタが、【$2005で設定するスクロール値】と、【$2006で設定する書き込み先のVRAMアドレス】で共有しているためです。先に書いた通り『スクロール座標はVBlank終了時にPPUによって自動的にセットされます』。しかし、$2006でセットしたVRAMアドレスが誤ってスクロール座標としてセットされてしまうことで、意図しない結果となるわけです。そのため$2006でVRAMアドレスをセットした後は(たとえスクロールしないゲームでも)スクロール値を再セットする必要があります。
分割スクロール:横スクロール
デビルワールドあたりから、ゲーム画面を上下に分割して、スコアなどのUIを上部に配置して中央にメイン画面を表示するゲームが増えていきます。分割するためには、PPUが現在どの位置を描画しているかをプログラム側で知る必要があります。方法はいくつかあり、有名なのはスプライト0番を使用した通称0爆弾や、IRQを使った割り込みがあります。これらの使い方についてはここでは解説しませんが、0爆弾については以前少し触れましたのでリンクを貼っておきます。
もっとも簡単な分割スクロールは、分割したいラインをPPUが描画中にスクロール座標を$2005にセットしてしまう方法です。ただし、この方法が使えるのは水平方向へのスクロールだけです。具体例としてはスーパーマリオブラザーズのような構成ですね。上部UIは固定されていて、メイン部分ではキャラは右方向に進行し、画面は左方向へ流れていきます。このタイプのゲームであれば以下のようなコードで簡単に分割できます。
; 配置した0爆弾にヒットするまで待ち
WAIT_0BOMB:
lda $2002
and #%01000000
beq WAIT_0BOMB
; スクロール座標をセット
lda #10 ; X座標
sta $2005
lda #0 ; Y座標
sta $2005
前半部は、あらかじめ分割位置に配置した0爆弾を判定して分割位置まで描画処理がくるを待っています。ループを抜けるとちょうど分割位置ですので、ここでスクロール値を$2005に流します。Y座標は無視されますが、X座標はHBlank(画面右隅まで描き終わったあと描画位置を1ピクセル下の画面左隅へ移動するまでの期間)が終了するときに反映されます。
分割スクロール:縦横スクロール
次に、分割したうえで上下左右自由にスクロールさせる方法について説明していきます。これを実現するためには、$2005と$2006をうまく使いPPUのレジスタをコントロールする必要があります。具体的な方法を説明する前にPPUレジスタについて説明していきます。
PPUレジスタについて
PPU内のレジスタはCPUから直接操作や参照はできませんが、$2005や$2006への書き込みによって間接的に操作できます。より詳しい解説はNESdevの以下のページを参照ください
PPU内のレジスタは以下の種類があります。
現在のVRAMアドレス(15bit)【v レジスタ】
テンポラリVRAMアドレス(15bit)【t レジスタ】
X座標(3bit)【x レジスタ】
書き込み回数識別用フラグ(1bit)【w レジスタ】
上2つは同じ15bitでbit内の用途も同じです。CPUから$2005や$2006によってスクロール座標やVRAMアドレスをセットすると、まず【tレジスタ】が更新され、後に【vレジスタ】にコピーされます。詳細は後述します。
【xレジスタ】の3bitのレジスタは、$2005への1回目のセット(スクロールX座標)と同時に更新されます。セットされるのは、X座標値の下位3bit(0~7の値)です。NESdevでは、この下位3bitを【Fine X scroll】、上位5bitを【coarse X scroll】と表記しています。
【wレジスタ】は、$2005と$2006が常に2回セットで書き込む仕様と関係しています。つまりこのビットによって1回目か2回目かを識別しており、書き込むたびに0と1に交互に変化します。また$2002を読み込むと0セットされるようです(BIT $2002でもOK)。
※レジスタの名称については正式名かどうかはわかりません。NESdevを参考にさせていただきました。
PPUレジスタの変化:$2005への書き込み【1回目】
上の図は、$2005に対して1回目のスクロール値(X座標)をセットしたときに、PPUレジスタがどのように変化するかを表しています。テンポラリ内のbit0-4にX座標の上位5bitが、そしてxレジスタに下位3bitがセットされています。wレジスタは1回目の書き込み後には常に1がセットされます。
PPUレジスタの変化:$2005への書き込み【2回目】
2回目の$2005の書き込みではスクロールのY座標をセットします。Y座標もX座標と同様に上位5bitと下位3bitで分けて配置されます。wレジスタは0に戻ります。vレジスタとxレジスタは変化しません。この状態でVBlankが終われば自動的にtレジスタの内容がvレジスタにコピーされます(HBlank後にもコピーされますが、Y座標に関する値はコピーされないようです)。変化していないbit10-11には$2000のbit0-1で指定する画面番号が入ります。詳しくは後述します。
ちなみに、t/vレジスタのbit0-11までの値に$2000をorするとネームテーブルのアドレスになります。うまいことできていますね。
PPUレジスタの変化:$2006への書き込み【1回目】
$2006はCPUからVRAMへアクセスするために、VRAMアドレスを指定するときに利用されます。スクロール値と同様に2回セットで実行され、1回目ではVRAM16bitアドレスのうち上位8bitをセットします。VRAMアドレスは$2000~$2FFFですので上位アドレスのbit6-7は常に0です。そのためbit6-7は破棄され、bit0-5がtレジスタのbit8-13にコピーされます。またwレジスタは0から1に変わります。
PPUレジスタの変化:$2006への書き込み【2回目】
$2006への2回目の書き込みでは、すべてをtレジスタのbit0-7へセットし、即座にtレジスタ全体をvレジスタへコピーします。またwレジスタも0に戻ります。このvレジスタが変更できる仕様を利用して分割スクロールを実現します。
PPUレジスタの変化:$2000への書き込み
$2000への書き込みではファミコンの描画設定を行います。このうちbit0-1では画面に描画する画面番号(ネームテーブル番号)を指定します。この値はtレジスタのbit10-11へセットされます。ここまで読んだ方ならわかると思いますが、$2006へ書き込むとこの情報も上書きされてしまいますので注意が必要です。
そして分割スクロールへ…
PPUレジスタの変化がわかったので情報を整理します。IRQや0爆弾を使い任意のラインが描画されることを判定したら、X座標およびY座標をなんとかしてPPUのvレジスタにセットできれば画面が分割できます。ですが$2005でtレジスタへY座標をセットしても、vレジスタへコピーされません。そこで$2006を併用してPPUレジスタをコントロールします。
おおまかな流れ
$2006[1回目]に、画面番号を書きこむ
$2005[2回目]に、Y座標を書き込む
$2005[1回目]に、X座標を書き込む→xレジスタが変わる
$2006[2回目]に、合成したY座標とX座標を書き込む→vレジスタが変わる
アドレスの後ろの[]内の数字は、1回目か2回目を表す実行番号で、本来$2005なら$2005を2回連続で実行するのが普通ですが、ここでは$2006と$2005を異なった順番で実行しています。それぞれの書き込み後の変化を逆手に取り、うまい具合に必要な情報がレジスタにセットされます。以降、順番に説明していきます。
1:$2006[1回目]に、画面番号を書きこむ
1回目の書き込みでは画面番号を書きこみます。これは次のY座標の書き込みではtレジスタのbit10-11を変更できないからです。画面番号は$2000のbit0-1にセットする値と同じように0~3の値を2bit左シフトさせて、$2006へセットします。(1回目の$2006はbit0-5を、tレジスタbit13-8に展開します)
2:$2005[2回目]に、Y座標を書き込む
これは単純にY座標を$2005に書き込めばOKです。これによりtレジスタのbit8-15は確定します。
3:$2005[1回目]に、X座標を書き込む
次にX座標を$2005へ書き込みます。これでxレジスタに必要な情報がセットされました。
4:$2006[2回目]に、合成したY座標とX座標を書き込む
実は先の$2005へのX座標書き込みによって、tレジスタのbit0-7にも必要な情報はすでに書き込まれています。ですが、このままではvレジスタに情報が渡らないので、tレジスタのbit0-7と同じ内容のものを$2006に書き込みます。そして、これが$2006の2回目の書き込みという扱いになりますので、vレジスタへ必要な情報が書き込まれることになります。具体的な値は以下の例のように加工してください。
分割スクロールについての解説は以上となります。UIのようにスクロール値が変更しないものに関してはあらかじめ計算しておいた値を$2006にセットしてもOKです(ただしX座標には注意)。