基礎知識
戦闘関係
ラスタースクロールは、カラーマスやマスク処理などと同じく、HDMA転送を使ったウィンドウ処理のひとつである。
名前の通りスクロール処理も絡むため、スクロールを先にお読みいただきたい。
ラスタースクロールは画面が波打つように左右に揺れる処理のことで(縦方向にも揺らせることは可能)、近未来編の序盤、物質転送装置に乗った時や、SF編のラストバトル前で使われている。
仕組みとしては、画面を描画する時、1スキャンライン毎にHDMA転送で描画開始位置(スクロール位置)を変化させる、というものである。
スーパーファミコンは横方向に1スキャンラインずつ描画していく都合で、左右に揺らす処理は縦方向に揺らす処理よりも簡単に実装できる、らしい。
うねうねしている形はいわゆる「サイン (sine) 波形」とか「正弦波形」と呼ばれるものである。
高校数学で習う、三角関数のy = sin xのグラフの図とか、周波数の正弦波の形である。
正弦波 - Wikipedia
※このページを読んでいる方は多分、ある程度の数学の知識があると思うので、三角関数とは何かという根本的な説明は省く。
三角関数 - Wikipedia
筆者が表計算ソフトで適当に作ってみたy = sin xのグラフが以下で、
ラスタースクロールは元画像(背景グラフィック)がグラフのように開始ピクセルが変わるので全体が歪む感じである。
本作の処理を見る限り、実際には、y = a sin (bx) のように定数を設定して、うねうね(振れ幅)の高さa、うねうねの周期bを徐々に変化させる処理を行っている。
画像の横幅自体は変化しないが、書き始める位置がわずかずつ変わるからうねっているように見える。
左右に揺れる都合で、本来表示する横幅256ピクセル分の外側(右または左)が映ってしまうが、本作の場合、外側をマスクし真っ黒にして外側を表示しないようにしている。
戦闘開始時の三角形がぐるぐる回って暗くなる時やアイリスイン・アイリスアウト時の、ウィンドウ処理で外側をマスクして真っ暗にする処理と似たようなものである。
本作でのラスタースクロールはいずれもフィールド上で、背景のBG1, BG2に対してのみ行っている。
セリフを表示するBG3には使用しない。
また、OBJ(スプライト)にも使用しない。背景のスクロール量を変化させるという処理の都合で、スプライトにはラスタースクロールがかからない。
大まかな流れを見てみると、ラスタースクロール中の画面1枚分の表示(1フレーム分)は以下のように処理している。
$7F:4000~$7F:41BFにコピー[$00:210D](BG1水平スクロール)と[$00:210F]BG2水平スクロール量224スキャンライン分$7F:4000~$7F:41BFをHDMA転送以下、SF編ラストバトル前のラスタースクロールのサブルーチンを紹介する。仕組み自体は近未来編でのラスタースクロールと変わらないはず。
HDMA転送の設定は以下になる。
$C0/D721 LDA $20 [$00:0020] ;Aに[$00:0020]をロード $C0/D723 BIT #$F3 ;Aと$F3で論理積(ステータスフラグ変更のみ) $C0/D725 BEQ $6B [$D792] ;ゼロフラグが立っているとき[$D792]分岐 $C0/D727 LDA $0114 [$00:0114] ;Aに[$00:0114]をロード $C0/D72A BNE $1A [$D746] ;ゼロフラグが立っていないとき[$D746]分岐 $C0/D72C LDA #$41 ;Aに$41をロード $C0/D72E STA $4330 [$00:4330] ;Aを[$00:4330]に書き込み $C0/D731 LDA #$28 ;Aに$28をロード $C0/D733 STA $4331 [$00:4331] ;Aを[$00:4331]に書き込み $C0/D736 LDA #$7F ;Aに$7Fをロード $C0/D738 STA $4334 [$00:4334] ;Aを[$00:4334]に書き込み $C0/D73B STA $4337 [$00:4337] ;Aを[$00:4337]に書き込み ; ;[$00:4330] GDMA / HDMA設定レジスタ $41 間接テーブルモード 2レジスタ1書き込み ;[$00:4331] 転送先指定 $28=WH2 ウィンドウ2左座標設定 ;[$00:4333][$00:4332] データ元アドレス(開始位置) →$C0/DA2Dで、$3C30か$3C20セット ;[$00:4334] データ元バンク $7F ;[$00:4337] HDMA間接アドレスバンクバイト $7F ; $C0/D73E LDA $014F [$00:014F] ;Aに[$00:014F]をロード $C0/D741 ORA #$08 ;Aと$08で論理和 $C0/D743 STA $014F [$00:014F] ;Aを[$00:014F]に書き込み $C0/D746 LDA $20 [$00:0020] ;Aに[$00:0020]をロード $C0/D748 BIT #$40 ;Aと$40で論理積(ステータスフラグ変更のみ) $C0/D74A BEQ $20 [$D76C] ;ゼロフラグが立っているとき[$D76C]分岐 $C0/D74C LDA #$42 ;Aに$42をロード $C0/D74E STA $4340 [$00:4340] ;Aを[$00:4340]に書き込み $C0/D751 LDA #$0D ;Aに$0Dをロード $C0/D753 STA $4341 [$00:4341] ;Aを[$00:4341]に書き込み $C0/D756 LDX #$3C60 ;Xに$3C60をロード $C0/D759 STX $4342 [$00:4342] ;Xを[$00:4343][$00:4342]に書き込み $C0/D75C LDA #$7F ;Aに$7Fをロード $C0/D75E STA $4344 [$00:4344] ;Aを[$00:4344]に書き込み $C0/D761 STA $4347 [$00:4347] ;Aを[$00:4347]に書き込み ; ;[$00:4340] GDMA / HDMA設定レジスタ $42 間接テーブルモード 1レジスタ2書き込み ;[$00:4341] 転送先指定 $0D=BG1HOFS BG1水平スクロール量 ;[$00:4343][$00:4342] データ元アドレス(開始位置) $3C60 ;[$00:4344] データ元バンク $7F ;[$00:4347] HDMA間接アドレスバンクバイト $7F ; $C0/D764 LDA $014F [$00:014F] ;Aに[$00:014F]をロード $C0/D767 ORA #$10 ;Aと$10で論理和 $C0/D769 STA $014F [$00:014F] ;Aを[$00:014F]に書き込み $C0/D76C LDA $20 [$00:0020] ;Aに[$00:0020]をロード $C0/D76E BIT #$80 ;Aと$80で論理積(ステータスフラグ変更のみ) $C0/D770 BEQ $20 [$D792] ;ゼロフラグが立っているとき[$D792]分岐 $C0/D772 LDA #$42 ;Aに$42をロード $C0/D774 STA $4350 [$00:4350] ;Aを[$00:4350]に書き込み $C0/D777 LDA #$0F ;Aに$0Fをロード $C0/D779 STA $4351 [$00:4351] ;Aを[$00:4351]に書き込み $C0/D77C LDX #$3C60 ;Xに$3C60をロード $C0/D77F STX $4352 [$00:4352] ;Xを[$00:4352]に書き込み $C0/D782 LDA #$7F ;Aに$7Fをロード $C0/D784 STA $4354 [$00:4354] ;Aを[$00:4354]に書き込み $C0/D787 STA $4357 [$00:4357] ;Aを[$00:4357]に書き込み ; ;[$00:4350] GDMA / HDMA設定レジスタ $42 間接テーブルモード 1レジスタ2書き込み ;[$00:4351] 転送先指定 $0F=BG2HOFS BG2水平スクロール量 ;[$00:4353][$00:4352] データ元アドレス(開始位置) $3C60 ;[$00:4354] データ元バンク $7F ;[$00:4357] HDMA間接アドレスバンクバイト $7F ; $C0/D78A LDA $014F [$00:014F] ;Aに[$00:014F]をロード $C0/D78D ORA #$20 ;Aと$20で論理和 $C0/D78F STA $014F [$00:014F] ;Aを[$00:014F]に書き込み $C0/D792 LDA $0156 [$00:0156] ;Aに[$00:0156]をロード $C0/D795 BIT #$F3 ;Aと$F3で論理積(ステータスフラグ変更のみ) $C0/D797 BEQ $2B [$D7C4] ;ゼロフラグが立っているとき[$D7C4]分岐 $C0/D7C4 STZ $30 [$00:0030] ;[$00:0030]に$00を書き込み $C0/D7C6 STZ $31 [$00:0031] ;[$00:0031]に$00を書き込み $C0/D7C8 LDA $0114 [$00:0114] ;Aに[$00:0114]をロード $C0/D7CB BNE $28 [$D7F5] ;ゼロフラグが立っていないとき[$D7F5]分岐 $C0/D7CD LDA $0156 [$00:0156] ;Aに[$00:0156]をロード $C0/D7D0 BEQ $11 [$D7E3] ;ゼロフラグが立っているとき[$D7E3]分岐 $C0/D7E3 LDA $20 [$00:0020] ;Aに[$00:0020]をロード $C0/D7E5 BEQ $0E [$D7F5] ;ゼロフラグが立っているとき[$D7F5]分岐 $C0/D7E7 LDA $0145 [$00:0145] ;Aに[$00:0145]をロード $C0/D7EA AND #$33 ;Aと$33で論理積 $C0/D7EC ORA #$CC ;Aと$CCで論理和 $C0/D7EE STA $0145 [$00:0145] ;Aを[$00:0145]に書き込み $C0/D7F1 LDA #$03 ;Aに$03をロード $C0/D7F3 STA $31 [$00:0031] ;Aを[$00:0031]に書き込み $C0/D7F5 LDA $0140 [$00:0140] ;Aに[$00:0140]をロード $C0/D7F8 ORA #$04 ;Aと$04で論理和 $C0/D7FA AND $30 [$00:0030] ;Aと[$00:0030]で論理積 $C0/D7FC EOR #$FF ;Aと$FFで排他的論理和XOR $C0/D7FE AND $0142 [$00:0142] ;Aと[$00:0142]で論理積 $C0/D801 STA $0142 [$00:0142] ;Aを[$00:0142]に書き込み $C0/D804 LDA $0141 [$00:0141] ;Aに[$00:0141]をロード $C0/D807 AND $30 [$00:0030] ;Aと[$00:0030]で論理積 $C0/D809 EOR #$FF ;Aと$FFで排他的論理和XOR $C0/D80B AND $0143 [$00:0143] ;Aと[$00:0143]で論理積 $C0/D80E STA $0143 [$00:0143] ;Aを[$00:0143]に書き込み $C0/D811 LDA $0140 [$00:0140] ;Aに[$00:0140]をロード $C0/D814 ORA #$04 ;Aと$04で論理和 $C0/D816 AND $31 [$00:0031] ;Aと[$00:0031]で論理積 $C0/D818 ORA $0142 [$00:0142] ;Aと[$00:0142]で論理和 $C0/D81B STA $0142 [$00:0142] ;Aを[$00:0142]に書き込み $C0/D81E LDA $0141 [$00:0141] ;Aに[$00:0141]をロード $C0/D821 AND $31 [$00:0031] ;Aと[$00:0031]で論理積 $C0/D823 ORA $0143 [$00:0143] ;Aと[$00:0143]で論理和 $C0/D826 STA $0143 [$00:0143] ;Aを[$00:0143]に書き込み $C0/D829 RTS ;サブルーチン戻り
HDMAチャネル3, 4, 5が設定されており、どれもラスタースクロールで使われる。
順に見ていく。
| アドレス | 内容 | 値 | 詳細 |
|---|---|---|---|
[$00:4330] | GDMA / HDMA設定レジスタ | $41 | 間接テーブルモード 2レジスタ1書き込み |
[$00:4331] | 転送先指定 | $28=WH2 | ウィンドウ2左座標[$00:2128]設定 |
[$00:4333][$00:4332] | データ元アドレス(開始位置) | - | ※$C0/DA2Dで、$3C30か$3C20セット |
[$00:4334] | データ元バンク | $7F | |
[$00:4337] | HDMA間接アドレスバンクバイト | $7F |
[$00:4330]に$41が書き込まれているので、HDMA設定は間接テーブルモード、2レジスタ1書き込みである。
先に述べたが、ラスタースクロールするBG1とBG2は、ウィンドウ2に表示する。
本作では必要がない限り、各BGとスプライトはウィンドウ1に表示して重ねているようだが、ラスタースクロールではBG1とBG2だけウィンドウ2に表示させて処理をしている。
[$00:4331]の転送先は[$00:2128](ウィンドウ2左座標)だが、2レジスタ1書き込みであることから、実際には[$00:2128][$00:2129]の値の転送である。
つまり、ウィンドウ2左座標[$00:2128]・ウィンドウ2右座標[$00:2129]に同時に値を送っている。
ウィンドウ2の内側・外側位置を決定するためのHDMA転送であることがわかる。
なお、通常のフィールドではウィンドウ2を使っていない。マップ切り替え時に、ウィンドウ2左座標[$00:2128]もウィンドウ2右座標[$00:2129]も$00が入っており、ウィンドウ2を使うとしたら、ウィンドウ2全体が「ウィンドウの外側」扱いである。
HDMA設定は間接テーブルモードだが、間接テーブルモードでのアドレスの読み方はウィンドウ効果にあるので参照していただきたい。
HDMAテーブルが書き込まれているアドレスなのだが、[$00:4337]からバンクは$7Fとわかるが、上処理にはアドレスの[$00:4333][$00:4332]が指定されていない。
というのは、後の処理でわかる通り、[$00:4333][$00:4332]には状況により$3C30か$3C20が入るのである。
HDMAテーブルは$7F:3C20からか、$7F:3C30からのどちらかである。
| アドレス | 内容 | 値 | 詳細 |
|---|---|---|---|
[$00:4340] | GDMA / HDMA設定レジスタ | $42 | 間接テーブルモード 1レジスタ2書き込み |
[$00:4341] | 転送先指定 | $0D=BG1HOFS | BG1水平スクロール量 |
[$00:4343][$00:4342] | データ元アドレス(開始位置) | $3C60 | |
[$00:4344] | データ元バンク | $7F | |
[$00:4347] | HDMA間接アドレスバンクバイト | $7F |
HDMAチャネル4はBG1水平スクロール量[$00:210D]へのHDMA転送であるが、スクロール量は2度書き込みして10ビットの値を設定しなければならない。
このため、[$00:4340]で1レジスタ2書き込みの設定がされている。
間接テーブルモードで、バンクは$7F、アドレスが$7F:3C60と指定されているので、$7F:3C60~をみてみる。
$7F:3C60~3バイトで区切ると、
$7F:4000から$F0(240)ピクセル分
$7F:40E0から$F0(240)ピクセル分
の転送処理だということがわかる。
| アドレス | 内容 | 値 | 詳細 |
|---|---|---|---|
[$00:4350] | GDMA / HDMA設定レジスタ | $42 | 間接テーブルモード 1レジスタ2書き込み |
[$00:4351] | 転送先指定 | $0F=BG2HOFS | BG2水平スクロール量 |
[$00:4353][$00:4352] | データ元アドレス(開始位置) | $3C60 | |
[$00:4354] | データ元バンク | $7F | |
[$00:4357] | HDMA間接アドレスバンクバイト | $7F |
HDMAチャネル4と設定が異なるのは、[$00:4351]の転送先がBG2水平スクロール量[$00:210F]に変わることだけ。
転送するデータも、
$7F:4000から$F0(240)ピクセル分
$7F:40E0から$F0(240)ピクセル分
である。
これは当然といえば当然で、BG1とBG2はいずれも同じ位置に表示される背景であり、多重スクロールのマップでない限り、スクロール量は同一でなければならない。
サブルーチンを見る限り、「多重スクロールのマップではない」という前提でのラスタースクロール設定のようである。
本作でラスタースクロールが使われる、近未来編の壽商会の地下1階や、SF編のリフレッシュルームは多重スクロールのマップではないので、このサブルーチンで特に問題は起こらない。
ゲーム制作時点で「多重スクロールのマップではラスタースクロールはなし」と決めてから演出を考えていったのかもしれない、ということがサブルーチンから読み取れる。
以上から、ラスタースクロール時、
$7F:4000から$F0(240)ピクセル分
$7F:40E0から$F0(240)ピクセル分
は、$7F:4000~$7F:41BFにあたり、2バイトずつ、BG1水平スクロール量[$00:210D]・BG2水平スクロール量[$00:210F]に書き込まれていくことがわかる。
$7F:4000~$7F:41BFは1フレーム毎に更新されているが、では$7F:4000~$7F:41BFはどのような値になるのか。
辿ってみると、
$C0/D6B3 PHX ;Xレジスタをスタックにプッシュ $C0/D6B4 PHY ;Yレジスタをスタックへプッシュ $C0/D6B5 PHB ;DBレジスタをスタックにプッシュ $C0/D6B6 LDX #$BC96 ;Xに$BC96をロード $C0/D6B9 LDY #$5402 ;Yに$5402をロード $C0/D6BC LDA #$0023 ;Aに$0023をロード $C0/D6BF MVN C0 7F ;データ転送 ;A:転送データサイズ-1 $0023 ;X:転送元アドレス $BC96 ;Y:転送先アドレス $5402 ;オペランド:転送元と転送先バンク C0 -> 7F ;$C0:BC96 -> $7F:5402 +$0023バイト $C0/D6BF MVN C0 7F ;データ転送 $C0/D6C2 STZ $5400 [$7F:5400] ;[$7F:5401][$7F:5400]に$00を書き込み $C0/D6C5 STZ $5448 [$7F:5448] ;[$7F:5449][$7F:5448]に$00を書き込み $C0/D6C8 LDX #$5402 ;Xに$5402をロード $C0/D6CB LDY #$5446 ;Yに$5446をロード $C0/D6CE LDA #$0011 ;Aに$0011をロード $C0/D6D1 STA $20 [$00:0020] ;Aを[$00:0021][$00:0020]に書き込み $C0/D6D3 LDA $0000,x[$7F:5402] ;Aに[$7F:5403][$7F:5402]をロード $C0/D6D6 STA $32 [$00:0032] ;Aを[$00:0033][$00:0032]に書き込み $C0/D6D8 SEP #$20 ;MフラグON Aレジスタは8bit幅 $C0/D6DA PHB ;DBレジスタをスタックにプッシュ $C0/D6DB LDA #$00 ;Aに$00をロード $C0/D6DD PHA ;Aレジスタをスタックにプッシュ $C0/D6DE PLB ;DBレジスタにスタックからプル $C0/D6DF JSR $BA16 [$C0:BA16] ;[$C0:BA16]へジャンプ
まず、この処理で、$C0:BC96~$C0:BCB9が$7F:5402~$7F:5425に書き込まれる($24バイト)。
$C0:BC96~$C0:BCB9一見、何だかよくわからない文字列であるが、これは正接(sin)の三角関数表である。
2バイト値で、$0016, $002C, $0042,……$00FF, $0100となる。
[$7F:5401][$7F:5400]が$0000だから、初期値に$0000を加えてグラフを書いてみると以下のようになる。
サイン波形の半分っぽい形になった。
このままだと最大値が$0100(256)なので、画面の横幅のピクセル数と同一である。
いきなりこの量だけ画面を揺らしたりはせず、上の値に一定の数値を掛け算することで画面の揺れ量を調整する。
例えば0.1倍にすれば最大部分で25ピクセルだけ画面をズラすことができる、という理屈である。
以上から、$C0:BC96~$C0:BCB9は三角関数表(sin x)であるということがわかった。
ちなみに、実際には$C0:BC94, $C0:BC95に$00が入っているため、$C0:BC94~$C0:BCB9が0~90度までの三角関数表(sin x)である。
また、$C0:BCBA~$C0:BCDCには$C0:BC96~$C0:BCB9を逆転した値が入っているから、$C0:BC96~$C0:BCDCは0~180度(ラジアンで0~π)、5度区切りで180度分の三角関数表(sin x)ということになる。
ということで$7F:5400~$7F:5425はサイン波形の半分の値が入ることになる。残りの半分は形を左右反転すれば良いし、波形が幾つも続くなら$7F:5400~$7F:5425を元にコピーしていけば良い。
結果、$7F:5400~$7F:548Fにサイン波形の元となる90バイトのデータが書き込まれ(処理省略)、
$C0/D65E LDA $00,x [$00:0250] ;Aに[$00:0250]をロード $C0/D660 STA $20 [$00:0020] ;Aを[$00:0020]に書き込み $C0/D662 LDA $08,x [$00:0258] ;Aに[$00:0258]をロード $C0/D664 STA $22 [$00:0022] ;Aを[$00:0022]に書き込み $C0/D666 LDA $3B [$00:003B] ;Aに[$00:003B]をロード ;ループ処理 $C0/D668 AND #$00FF ;Aと$00FFで論理積 $C0/D66B CMP #$0002 ;Aと$0002を減算比較(ステータスレジスタ変更のみ) $C0/D66E BNE $05 [$D675] ;ゼロフラグが立っていないとき[$D675]分岐 $C0/D675 LDA $0A3A [$00:0A3A] ;Aに[$00:0A3A]をロード $C0/D678 LSR A ;Aを論理右シフト (/2) $C0/D679 LSR A ;Aを論理右シフト (/2) $C0/D67A LSR A ;Aを論理右シフト (/2) $C0/D67B LSR A ;Aを論理右シフト (/2) $C0/D67C STA $24 [$00:0024] ;Aを[$00:0024]に書き込み $C0/D67E SEP #$20 ;MフラグON Aレジスタは8bit幅 $C0/D680 PHB ;DBレジスタをスタックにプッシュ $C0/D681 LDA #$7F ;Aに$7Fをロード $C0/D683 PHA ;Aレジスタをスタックにプッシュ $C0/D684 PLB ;DBレジスタにスタックからプル $C0/D685 REP #$20 ;Aを16bit幅に変更、Mフラグをクリア $C0/D687 LDA #$00E0 ;Aに$00E0をロード $C0/D68A STA $26 [$00:0026] ;Aを[$00:0026]に書き込み $C0/D68C LDA $21 [$00:0021] ;Aに[$00:0021]をロード $C0/D68E AND #$00FF ;Aと$00FFで論理積 $C0/D691 ASL A ;Aを算術左シフト *2 $C0/D692 TAX ;Aの値をXレジスタに転送 $C0/D693 LDA $5400,x[$7F:5400] ;Aに[$7F:5401][$7F:5400]をロード $C0/D696 CLC ;キャリーフラグクリア $C0/D697 ADC $24 [$00:0024] ;A + [$00:0024] $C0/D699 STA $0000,y[$7F:4000] ;Aを[$7F:4001][$7F:4000]~[$7F:41BF][$7F:41BE]に書き込み $C0/D69C LDA $20 [$00:0020] ;Aに[$00:0020]をロード $C0/D69E CLC ;キャリーフラグクリア $C0/D69F ADC $22 [$00:0022] ;A + [$00:0022] $C0/D6A1 CMP #$4800 ;Aと$4800を減算比較(ステータスレジスタ変更のみ) $C0/D6A4 BCC $03 [$D6A9] ;キャリーフラグが立っていないとき[$D6A9]分岐 $C0/D6A6 SBC #$4800 ;A - $4800 $C0/D6A9 STA $20 [$00:0020] ;Aを[$00:0020]に書き込み $C0/D6AB INY ;Yをインクリメント +1 $C0/D6AC INY ;Yをインクリメント +1 $C0/D6AD DEC $26 [$00:0026] ;[$00:0026]をデクリメント -1 $C0/D6AF BNE $DB [$D68C] ;ゼロフラグが立っていないとき[$D68C]分岐 ;ループ処理ここまで $C0/D6B1 PLB ;DBレジスタにスタックからプル $C0/D6B2 RTS ;サブルーチン戻り
$7F:5400~$7F:548Fからさらに$7F:4000~$7F:41BEにコピーされ、$7F:4000~$7F:41BEがHDMA転送に使われることになるようである。
$C0/D6B3~$C0/D6DFの処理だけでは毎回三角関数表をコピーするだけだから、同じ形状のサイン波形にしかならないが、$C0/D404~$C0/D485、$C0/D4F4~$C0/D54E、$C0/D54F~$C0/D5ADあたりの処理で係数を毎フレーム計算し直しているようである。詳細省略。
$C0/D9DC LDA $0154 [$00:0154] ;Aに[$00:0154]をロード $C0/D9DF ORA $0156 [$00:0156] ;Aと[$00:0156]で論理和 $C0/D9E2 BNE $03 [$D9E7] ;ゼロフラグが立っていないとき[$D9E7]分岐 $C0/D9E7 STA $30 [$00:0030] ;Aを[$00:0030]に書き込み $C0/D9E9 LDA $0142 [$00:0142] ;Aに[$00:0142](03 0000 0011)をロード $C0/D9EC STA $212E [$00:212E] ;Aを[$00:212E]に書き込み メインスクリーンウインドウマスク BG1/BG2ウインドウマスク $C0/D9EF LDA $0143 [$00:0143] ;Aに[$00:0143](00)をロード $C0/D9F2 STA $212F [$00:212F] ;Aを[$00:212F]に書き込み サブスクリーンウインドウマスク $C0/D9F5 LDA $0145 [$00:0145] ;Aに[$00:0145](CC 1100 1100)をロード $C0/D9F8 STA $2123 [$00:2123] ;Aを[$00:2123]に書き込み BG1,BG2ウィンドウマスク設定 $C0/D9FB LDA $0146 [$00:0146] ;Aに[$00:0146](00)をロード $C0/D9FE STA $2124 [$00:2124] ;Aを[$00:2124]に書き込み BG3,BG4ウィンドウマスク設定 $C0/DA01 LDA $0147 [$00:0147] ;Aに[$00:0147](00)をロード $C0/DA04 STA $2125 [$00:2125] ;Aを[$00:2125]に書き込み スプライトウィンドウマスク設定 $C0/DA07 LDX $01FE [$00:01FE] ;Xに[$00:01FF][$00:01FE](FF00)をロード $C0/DA0A STX $2128 [$00:2128] ;Xを[$00:2129][$00:2128]に書き込み ウィンドウ2右・左座標設定 $C0/DA0D LDA $0148 [$00:0148] ;Aに[$00:0148](00)をロード $C0/DA10 STA $212A [$00:212A] ;Aを[$00:212A]に書き込み BGウインドウマスクロジック設定 $C0/DA13 LDA $0149 [$00:0149] ;Aに[$00:0149](00)をロード $C0/DA16 STA $212B [$00:212B] ;Aを[$00:212B]に書き込み スプライトウインドウマスクロジック設定 $C0/DA19 LDA $014F [$00:014F] ;Aに[$00:014F]をロード $C0/DA1C BIT #$08 ;Aと$08で論理積(ステータスフラグ変更のみ) $C0/DA1E BEQ $10 [$DA30] ;ゼロフラグが立っているとき[$DA30]分岐 $C0/DA20 LDA $01FB [$00:01FB] ;Aに[$00:01FB]をロード $C0/DA23 BNE $05 [$DA2A] ;ゼロフラグが立っていないとき[$DA2A]分岐 ;ゼロフラグON $C0/DA25 LDX #$3C20 ;Xに$3C20をロード $C0/DA28 BRA $03 [$DA2D] ;フラグにかかわりなく常に分岐[$DA2D] ;ゼロフラグOFF $C0/DA2A LDX #$3C30 ;Xに$3C30をロード $C0/DA2D STX $4332 [$00:4332] ;Xを[$00:4333][$00:4332]に書き込み $C0/DA30 LDA $014F [$00:014F] ;Aに[$00:014F]をロード $C0/DA33 BIT #$10 ;Aと$10で論理積(ステータスフラグ変更のみ) $C0/DA35 BEQ $25 [$DA5C] ;ゼロフラグが立っているとき[$DA5C]分岐 $C0/DA37 LDA $01FB [$00:01FB] ;Aに[$00:01FB]をロード $C0/DA3A BNE $05 [$DA41] ;ゼロフラグが立っていないとき[$DA41]分岐 ;ゼロフラグON $C0/DA3C LDX #$4000 ;Xに$4000をロード $C0/DA3F BRA $03 [$DA44] ;フラグにかかわりなく常に分岐[$DA44] ;ゼロフラグOFF $C0/DA41 LDX #$4380 ;Xに$4380をロード $C0/DA44 PHB ;DBレジスタをスタックにプッシュ $C0/DA45 LDA #$7F ;Aに$7Fをロード $C0/DA47 PHA ;Aレジスタをスタックにプッシュ $C0/DA48 PLB ;DBレジスタにスタックからプル $C0/DA49 REP #$20 ;Aを16bit幅に変更、Mフラグをクリア $C0/DA4B TXA ;Xレジスタの値をAレジスタに転送 $C0/DA4C LDX #$3C60 ;Xに$3C60をロード $C0/DA4F STA $0001,x[$7F:3C61] ;Aを[$7F:3C62][$7F:3C61]に書き込み $C0/DA52 CLC ;キャリーフラグクリア $C0/DA53 ADC #$00E0 ;A + $00E0 $C0/DA56 STA $0004,x[$7F:3C64] ;Aを[$7F:3C65][$7F:3C64]に書き込み $C0/DA59 SEP #$20 ;MフラグON Aレジスタは8bit幅 $C0/DA5B PLB ;DBレジスタにスタックからプル $C0/DA5C LDA $014F [$00:014F] ;Aに[$00:014F](38 = 0011 1000)をロード $C0/DA5F STA $420C [$00:420C] ;Aを[$00:420C]に書き込み HDMAチャネル3,4,5を実行 $C0/DA62 REP #$20 ;Aを16bit幅に変更、Mフラグをクリア $C0/DA64 LDA $0A38 [$00:0A38] ;Aに[$00:0A39][$00:0A38]をロード $C0/DA67 LSR A ;Aを論理右シフト (/2) $C0/DA68 LSR A ;Aを論理右シフト (/2) $C0/DA69 LSR A ;Aを論理右シフト (/2) $C0/DA6A LSR A ;Aを論理右シフト (/2) $C0/DA6B CLC ;キャリーフラグクリア $C0/DA6C ADC $0158 [$00:0158] ;A + [$00:0159][$00:0158] $C0/DA6F STA $30 [$00:0030] ;Aを[$00:0031][$00:0030]に書き込み $C0/DA71 LDA $0A3C [$00:0A3C] ;Aに[$00:0A3D][$00:0A3C]をロード $C0/DA74 LSR A ;Aを論理右シフト (/2) $C0/DA75 LSR A ;Aを論理右シフト (/2) $C0/DA76 LSR A ;Aを論理右シフト (/2) $C0/DA77 LSR A ;Aを論理右シフト (/2) $C0/DA78 CLC ;キャリーフラグクリア $C0/DA79 ADC $015C [$00:015C] ;A + [$00:015D][$00:015C] $C0/DA7C DEC A ;Aをデクリメント -1 $C0/DA7D STA $32 [$00:0032] ;Aを[$00:0033][$00:0032]に書き込み $C0/DA7F LDA $0A3A [$00:0A3A] ;Aに[$00:0A3B][$00:0A3A]をロード $C0/DA82 LSR A ;Aを論理右シフト (/2) $C0/DA83 LSR A ;Aを論理右シフト (/2) $C0/DA84 LSR A ;Aを論理右シフト (/2) $C0/DA85 LSR A ;Aを論理右シフト (/2) $C0/DA86 CLC ;キャリーフラグクリア $C0/DA87 ADC $015A [$00:015A] ;A + [$00:015B][$00:015A] $C0/DA8A STA $34 [$00:0034] ;Aを[$00:0035][$00:0034]に書き込み $C0/DA8C LDA $0A3E [$00:0A3E] ;Aに[$00:0A3F][$00:0A3E]をロード $C0/DA8F LSR A ;Aを論理右シフト (/2) $C0/DA90 LSR A ;Aを論理右シフト (/2) $C0/DA91 LSR A ;Aを論理右シフト (/2) $C0/DA92 LSR A ;Aを論理右シフト (/2) $C0/DA93 CLC ;キャリーフラグクリア $C0/DA94 ADC $015E [$00:015E] ;A + [$00:015F][$00:015E] $C0/DA97 DEC A ;Aをデクリメント -1 $C0/DA98 STA $36 [$00:0036] ;Aを[$00:0037][$00:0036]に書き込み $C0/DA9A SEP #$20 ;MフラグON Aレジスタは8bit幅 $C0/DA9C LDA $30 [$00:0030] ;Aに[$00:0030]をロード $C0/DA9E STA $210D [$00:210D] ;Aを[$00:210D]に書き込み $C0/DAA1 LDA $31 [$00:0031] ;Aに[$00:0031]をロード $C0/DAA3 STA $210D [$00:210D] ;Aを[$00:210D]に書き込み $C0/DAA6 LDA $32 [$00:0032] ;Aに[$00:0032]をロード $C0/DAA8 STA $210E [$00:210E] ;Aを[$00:210E]に書き込み $C0/DAAB LDA $33 [$00:0033] ;Aに[$00:0033]をロード $C0/DAAD STA $210E [$00:210E] ;Aを[$00:210E]に書き込み $C0/DAB0 LDA $34 [$00:0034] ;Aに[$00:0034]をロード $C0/DAB2 STA $210F [$00:210F] ;Aを[$00:210F]に書き込み $C0/DAB5 LDA $35 [$00:0035] ;Aに[$00:0035]をロード $C0/DAB7 STA $210F [$00:210F] ;Aを[$00:210F]に書き込み $C0/DABA LDA $36 [$00:0036] ;Aに[$00:0036]をロード $C0/DABC STA $2110 [$00:2110] ;Aを[$00:2110]に書き込み $C0/DABF LDA $37 [$00:0037] ;Aに[$00:0037]をロード $C0/DAC1 STA $2110 [$00:2110] ;Aを[$00:2110]に書き込み $C0/DAC4 LDA #$01 ;Aに$01をロード $C0/DAC6 STA $0166 [$00:0166] ;Aを[$00:0166]に書き込み $C0/DAC9 RTS ;サブルーチン戻り
ここがHDMA転送の前準備+HDMA転送開始処理である。
最初はHDMA転送前のI/Oレジスタへの書き込みである。まとめると以下のとおり。
| I/Oレジスタ | 内容 | 値 | |
|---|---|---|---|
[$00:212E] | メインスクリーンウインドウマスク | [$00:0142]($03) | |
[$00:212F] | サブスクリーンウインドウマスク | [$00:0143]($00) | |
[$00:2123] | BG1,BG2ウィンドウマスク設定 | [$00:0145]($CC) | |
[$00:2124] | BG3,BG4ウィンドウマスク設定 | [$00:0146]($00) | |
[$00:2125] | スプライトウィンドウマスク設定 | [$00:0147]($00) | |
[$00:2129][$00:2128] | ウィンドウ2右・左座標設定 | [$00:01FF][$00:01FE]($FF00) | |
[$00:212A] | BGウインドウマスクロジック設定 | [$00:0148]($00) | |
[$00:212B] | スプライトウインドウマスクロジック設定 | [$00:0149]($00) |
メインスクリーンのBG1・BG2のみウインドウマスクがON。
BG1,BG2ウィンドウマスクはどちらも「ウィンドウ2有効・外側をマスク」「ウィンドウ1無効」である。
[$00:2128](ウィンドウ2右座標): $00[$00:2129](ウィンドウ2左座標): $FFだから、ウィンドウ2の$00~$FF、つまりすべてが内側扱いであり、このタイミングだとマスクされている部分は画面外である。
[$00:212A][$00:212B]は、ウィンドウ1とウィンドウ2をどう合成するかという設定になるのだが、たぶん、例えば「BG1のウィンドウ1とウィンドウ2をどう合成するか」の意味なので、ウィンドウ1とウィンドウ2のどちらかしか使っていない場合、たぶん意味はない(あくまでもたぶん)
$C0/DA20~$C0/DA2Dで、HDMA転送チャネル3の、決定していなかった[$00:4333][$00:4332]の値が書き込まれる。
処理を見ると[$00:01FB]が$00なら$3C30、[$00:01FB]が$01なら$3C20が書き込まれている。
$C0/DA5FでHDMA転送チャネル3~5の転送が開始になっている。
$C0/DA5C LDA $014F [$00:014F] ;Aに[$00:014F](38 = 0011 1000)をロード $C0/DA5F STA $420C [$00:420C] ;Aを[$00:420C]に書き込み HDMAチャネル3,4,5を実行
HDMA転送をまとめると、
と、いうような順序のようである。
その後から$C0/DA64~$C0/DAC9の処理が入るが、スクロールにおける$C0/53C0~$C0/541Cの処理に似ている。
ただし[$00:0158]~[$00:015F]の加算がある。
[$00:0158]~[$00:015F]が実際のスクロール量で、そこに計算値を加算している、と見て良いようである。
[$00:0A39][$00:0A38]/$10 + [$00:0159][$00:0158] = [$00:0031][$00:0030] → [$00:210D]2度書き[$00:0A3D][$00:0A3C]/$10 + [$00:015D][$00:015C] - 1 = [$00:0033][$00:0032] → [$00:210E]2度書き[$00:0A3B][$00:0A3A]/$10 + [$00:015B][$00:015A] = [$00:0035][$00:0034] → [$00:210F]2度書き[$00:0A3F][$00:0A3E]/$10 + [$00:015F][$00:015E] - 1 = [$00:0037][$00:0036] → [$00:2110]2度書きBG1水平スクロール量・BG1垂直スクロール量・BG2水平スクロール量・BG2垂直スクロール量の更新なのだが、ここは実質、スクロール量のリセットである。
というのは、毎フレーム、必ず以下の値(2バイト)が2度書きで入るからである。
| アドレス | 値 |
|---|---|
[$00:210D] | $0000 |
[$00:210E] | $FFFF |
[$00:210F] | $0000 |
[$00:2110] | $FFFF |
この後に画面が更新され、HDMA転送で毎スキャンライン水平スクロール量がずれる。
以上が大まかな仕組みである。
1フレーム毎に更新される$7F:4000~$7F:41BFだが、揺れだした初期あたりの値を取り出してみると以下のような感じである(16バイト区切り)。
2バイト区切りで、符号付き16ビット整数から10進数に変換すると、スクロール量がわかりやすくなる。
プラスの値なら右にずれるし、マイナスの値なら左にずれる。
| ライン | 符号付き 16ビット整数 | 10進数 |
|---|---|---|
| 000 | $0001 | 1 |
| 001 | $0001 | 1 |
| 002 | $0000 | 0 |
| 003 | $0000 | 0 |
| 004 | $0000 | 0 |
| 005 | $FFFF | -1 |
| 006 | $FFFE | -2 |
| 007 | $FFFE | -2 |
| 008 | $FFFD | -3 |
| 009 | $FFFD | -3 |
| 010 | $FFFC | -4 |
| 011 | $FFFC | -4 |
| 012 | $FFFB | -5 |
| 013 | $FFFB | -5 |
| 014 | $FFFB | -5 |
| 015 | $FFFA | -6 |
| 016 | $FFFA | -6 |
| 017 | $FFFA | -6 |
| 018 | $FFFA | -6 |
| 019 | $FFFA | -6 |
| 020 | $FFFA | -6 |
| 021 | $FFFA | -6 |
| 022 | $FFFB | -5 |
| 023 | $FFFB | -5 |
| 024 | $FFFB | -5 |
| 025 | $FFFC | -4 |
| 026 | $FFFC | -4 |
| 027 | $FFFD | -3 |
| 028 | $FFFD | -3 |
| 029 | $FFFE | -2 |
| 030 | $FFFE | -2 |
| 031 | $FFFF | -1 |
| 032 | $0000 | 0 |
| 033 | $0000 | 0 |
| 034 | $0000 | 0 |
| 035 | $0001 | 1 |
| 036 | $0001 | 1 |
| 037 | $0002 | 2 |
| 038 | $0003 | 3 |
| 039 | $0003 | 3 |
| 040 | $0004 | 4 |
| 041 | $0004 | 4 |
| 042 | $0005 | 5 |
| 043 | $0005 | 5 |
| 044 | $0005 | 5 |
| 045 | $0006 | 6 |
| 046 | $0006 | 6 |
| 047 | $0006 | 6 |
| 048 | $0006 | 6 |
| 049 | $0006 | 6 |
| 050 | $0006 | 6 |
| 051 | $0006 | 6 |
| 052 | $0006 | 6 |
| 053 | $0005 | 5 |
| 054 | $0005 | 5 |
| 055 | $0004 | 4 |
| 056 | $0004 | 4 |
| 057 | $0004 | 4 |
| 058 | $0003 | 3 |
| 059 | $0002 | 2 |
| 060 | $0002 | 2 |
| 061 | $0001 | 1 |
| 062 | $0001 | 1 |
| 063 | $0000 | 0 |
| 064 | $0000 | 0 |
| 065 | $FFFF | -1 |
| 066 | $FFFF | -1 |
| 067 | $FFFE | -2 |
| 068 | $FFFE | -2 |
| 069 | $FFFD | -3 |
| 070 | $FFFC | -4 |
| 071 | $FFFC | -4 |
| 072 | $FFFC | -4 |
| 073 | $FFFB | -5 |
| 074 | $FFFB | -5 |
| 075 | $FFFA | -6 |
| 076 | $FFFA | -6 |
| 077 | $FFFA | -6 |
| 078 | $FFFA | -6 |
| 079 | $FFFA | -6 |
| 080 | $FFFA | -6 |
| 081 | $FFFA | -6 |
| 082 | $FFFA | -6 |
| 083 | $FFFB | -5 |
| 084 | $FFFB | -5 |
| 085 | $FFFC | -4 |
| 086 | $FFFC | -4 |
| 087 | $FFFC | -4 |
| 088 | $FFFD | -3 |
| 089 | $FFFD | -3 |
| 090 | $FFFE | -2 |
| 091 | $FFFF | -1 |
| 092 | $FFFF | -1 |
| 093 | $0000 | 0 |
| 094 | $0000 | 0 |
| 095 | $0001 | 1 |
| 096 | $0001 | 1 |
| 097 | $0002 | 2 |
| 098 | $0002 | 2 |
| 099 | $0003 | 3 |
| 100 | $0003 | 3 |
| 101 | $0004 | 4 |
| 102 | $0004 | 4 |
| 103 | $0005 | 5 |
| 104 | $0005 | 5 |
| 105 | $0005 | 5 |
| 106 | $0006 | 6 |
| 107 | $0006 | 6 |
| 108 | $0006 | 6 |
| 109 | $0006 | 6 |
| 110 | $0006 | 6 |
| 111 | $0006 | 6 |
| 112 | $0006 | 6 |
| 113 | $0005 | 5 |
| 114 | $0005 | 5 |
| 115 | $0005 | 5 |
| 116 | $0004 | 4 |
| 117 | $0004 | 4 |
| 118 | $0003 | 3 |
| 119 | $0003 | 3 |
| 120 | $0002 | 2 |
| 121 | $0002 | 2 |
| 122 | $0001 | 1 |
| 123 | $0000 | 0 |
| 124 | $0000 | 0 |
| 125 | $0000 | 0 |
| 126 | $FFFF | -1 |
| 127 | $FFFE | -2 |
| 128 | $FFFE | -2 |
| 129 | $FFFD | -3 |
| 130 | $FFFD | -3 |
| 131 | $FFFC | -4 |
| 132 | $FFFC | -4 |
| 133 | $FFFB | -5 |
| 134 | $FFFB | -5 |
| 135 | $FFFB | -5 |
| 136 | $FFFA | -6 |
| 137 | $FFFA | -6 |
| 138 | $FFFA | -6 |
| 139 | $FFFA | -6 |
| 140 | $FFFA | -6 |
| 141 | $FFFA | -6 |
| 142 | $FFFA | -6 |
| 143 | $FFFB | -5 |
| 144 | $FFFB | -5 |
| 145 | $FFFB | -5 |
| 146 | $FFFC | -4 |
| 147 | $FFFC | -4 |
| 148 | $FFFC | -4 |
| 149 | $FFFD | -3 |
| 150 | $FFFE | -2 |
| 151 | $FFFE | -2 |
| 152 | $FFFF | -1 |
| 153 | $FFFF | -1 |
| 154 | $0000 | 0 |
| 155 | $0000 | 0 |
| 156 | $0001 | 1 |
| 157 | $0001 | 1 |
| 158 | $0002 | 2 |
| 159 | $0003 | 3 |
| 160 | $0003 | 3 |
| 161 | $0004 | 4 |
| 162 | $0004 | 4 |
| 163 | $0004 | 4 |
| 164 | $0005 | 5 |
| 165 | $0005 | 5 |
| 166 | $0006 | 6 |
| 167 | $0006 | 6 |
| 168 | $0006 | 6 |
| 169 | $0006 | 6 |
| 170 | $0006 | 6 |
| 171 | $0006 | 6 |
| 172 | $0006 | 6 |
| 173 | $0006 | 6 |
| 174 | $0005 | 5 |
| 175 | $0005 | 5 |
| 176 | $0004 | 4 |
| 177 | $0004 | 4 |
| 178 | $0004 | 4 |
| 179 | $0003 | 3 |
| 180 | $0003 | 3 |
| 181 | $0002 | 2 |
| 182 | $0001 | 1 |
| 183 | $0001 | 1 |
| 184 | $0000 | 0 |
| 185 | $0000 | 0 |
| 186 | $FFFF | -1 |
| 187 | $FFFF | -1 |
| 188 | $FFFE | -2 |
| 189 | $FFFE | -2 |
| 190 | $FFFD | -3 |
| 191 | $FFFC | -4 |
| 192 | $FFFC | -4 |
| 193 | $FFFC | -4 |
| 194 | $FFFB | -5 |
| 195 | $FFFB | -5 |
| 196 | $FFFB | -5 |
| 197 | $FFFA | -6 |
| 198 | $FFFA | -6 |
| 199 | $FFFA | -6 |
| 200 | $FFFA | -6 |
| 201 | $FFFA | -6 |
| 202 | $FFFA | -6 |
| 203 | $FFFA | -6 |
| 204 | $FFFB | -5 |
| 205 | $FFFB | -5 |
| 206 | $FFFB | -5 |
| 207 | $FFFC | -4 |
| 208 | $FFFC | -4 |
| 209 | $FFFD | -3 |
| 210 | $FFFD | -3 |
| 211 | $FFFE | -2 |
| 212 | $FFFE | -2 |
| 213 | $FFFF | -1 |
| 214 | $0000 | 0 |
| 215 | $0000 | 0 |
| 216 | $0000 | 0 |
| 217 | $0001 | 1 |
| 218 | $0002 | 2 |
| 219 | $0002 | 2 |
| 220 | $0003 | 3 |
| 221 | $0003 | 3 |
| 222 | $0004 | 4 |
| 223 | $0004 | 4 |
試しにエクセル方眼紙でそれっぽい図にしてみたが、うねうねの再現こそできたものの数値は縮小されて読めなくなってしまった。ご了承いただきたい。
$C0:BC96~$C0:BCB9が正接(sin)の三角関数表なのだが、実際の三角関数の答えときちんと一致するものなのだろうか。という話を記しておく。
$C0:BC96~$C0:BCB9は、0~90度(ラジアンなら0~π/2)分の正接(sin)の三角関数表になるのだが、1度ずつの値ではなく、18等分している。つまり5度ずつの値である。sin00°, sin05°, ……という値は関数電卓や表計算ソフトなどで求められるが、ここでは古風に、実際の三角関数表で見てみよう。検索すれば出てくる。
三角関数表(PDFファイル)
また、最小値を$00、最大値を$FF(10進数256)としている。三角関数の値は直角三角形の角度から「対辺/斜辺」のように割り算して割合を求めるから、0~1の値であり、
$C0:BC96~$C0:BCB9と合わせるのなら256倍しなければならない。5度刻みで三角関数表のsinの値を転載させていただくと以下のようになる。
これに256を掛けたのが3列目の値であり(小数点以下第2位四捨五入)、4列目は
$C0:BC96~$C0:BCB9を10進数にした値。$C0:BC96~この通り、三角関数表の値を小数点以下で四捨五入すると
$C0:BC96~$C0:BCB9の値とぴったり一致しており、きちんと三角関数で計算された値だったことがはっきりした。なお、現代のプログラミング言語であれば、標準ライブラリで数学関数を呼び出したり、組み込み関数(例えばJavaScriptならMath.sin(x))で簡単に三角関数が計算できるから、わざわざ三角関数表をプログラムの中に書き込んでおく必要はない。
だが、スーパーファミコンのアセンブリ言語(65C816)は、三角関数どころか掛け算や割り算の命令もない(乗算・除算のやり方はこれまでにも説明しているので省く)。
よって、三角関数で滑らかな曲線の計算をするといった場合は、三角関数表を埋め込んでおく必要があったのである。
(何かしらの物体を投げて落下する、という場合は放物線、y = a(x - p)2 + qで座標計算できるから、三角関数のように表は必要ない……と思う)