TOP > プログラミング関係解説&調査 > ウィンドウ効果

ウィンドウ効果

当ページの内容は、ある程度のプログラミングの知識を必要とする。
まず、プログラミング関係解説&調査をひととおり読んでいただきたい。

ウィンドウ効果は、前ページのカラーマスの説明とも関係するので先にお読みいただきたい。
前ページで説明した、メニュー画面で選択していないコマンドウィンドウが半透明になっているのは、ウィンドウ効果+カラーマス効果ということだからである。
つまり、

上図で黄色をかけた範囲は「ウィンドウの外側」、黄色ではない範囲は「ウィンドウの内側」と設定し、「ウィンドウの外側」のBG1(コマンドウィンドウ)・BG3(コマンド文字列)にだけハーフカラーマス効果を適用して、半透明にする、という方法を使っていた。

ウィンドウの外側・内側については、前ページでも説明したとおり、ウィンドウ座標を決定するレジスタ[$00:2126][$00:2129]に値($00$FF)を書き込んで設定する。

レジスタ名前内容
[$00:2126]WH0ウィンドウ1左座標設定$00$FF
[$00:2127]WH1ウィンドウ1右座標設定$00$FF
[$00:2128]WH2ウィンドウ2左座標設定$00$FF
[$00:2129]WH3ウィンドウ2右座標設定$00$FF

左座標に設定した数値から、右座標に設定した数値の間が、「ウィンドウの内側」である。それ以外は「ウィンドウの外側」になる。
左座標と右座標がどちらも$00なら、全領域を「ウィンドウの外側」として扱う。

ウィンドウ1とウィンドウ2は最終的に統合して表示する。
(カラーマスで使う、メインスクリーンとサブスクリーンとは別の話である。サブスクリーンは主にカラーマス表示のために使う)
ウィンドウ1とウィンドウ2の統合方法は[$00:212A][$00:212B]の値で決まるが、とりあえずこの話は横に置いておく。

ゲーム画面は横256ピクセル×縦224ピクセルだが、一番左の位置をX座標の原点0として、画面一番右のドットが$FF = 255となる。
このため、ウィンドウの内側の幅は、単純に「ウィンドウ1右座標」-「ウィンドウ1左座標」ではなく、この値に+1した値がウィンドウの内側の幅になる。

BG1~4やスプライト(OBJ)、カラーマス用サブスクリーン(下表のMATH)にウィンドウ1・ウィンドウ2を使うかどうかは、[$00:2123][$00:2125](Windowマスク設定)の値で決まる。

Bit[$00:2123][$00:2124][$00:2125]内容
0-1BG1BG3OBJウィンドウ1領域
(%00=無効, %10=内側, %11=外側)
2-3BG1BG3OBJウィンドウ2領域
(%00=無効, %10=内側, %11=外側)
4-5BG2BG4MATHウィンドウ1領域
(%00=無効, %10=内側, %11=外側)
6-7BG2BG4MATHウィンドウ2領域
(%00=無効, %10=内側, %11=外側)

メニュー画面では、初期設定で、[$00:2123][$00:2124]$00[$00:2125]$30 (%0011 0000) が指定されていた。
つまり、カラーマス用サブスクリーンのウィンドウ1領域だけ「外側」の指示がある。外側にカラーマス効果を適用しろという意味で、これにより先の図のようにウィンドウ座標で外側に指定した領域だけハーフカラーマスの半透明効果がかかる仕組みである。

さらに、[$00:212E][$00:212F](ウィンドウ領域無効化)に値を設定することで、ウィンドウ領域内の表示無効化といったことまでできる。

メニュー画面の場合、メニュー画面を開いた時点では、[$00:2126][$00:2129]すべてに$00を設定していた。
つまりゲーム画面全領域がウィンドウの外側という扱いである。
これだけだと、ゲーム画面の一部分だけウィンドウ幅を変えることができないが、前ページのカラーマス処理の時のように、HDMA転送で1行スキャンラインを描いた直後にウィンドウ座標を書き換えることで、1列ずつウィンドウ幅を変えることができる。
ウィンドウ幅の変更と、領域内の可視状態などの変更を組み合わせて、戦闘突入時の三角形が回転しながら画面が暗くなる効果などが作り出せる。

戦闘突入時の三角形ウィンドウマスク処理

では、実際に、戦闘突入時の画面処理について見てみよう。

$C0/D190 STA $30    [$00:0030]   ;Aを[$00:0030]に書き込み
$C0/D192 LDA #$FF                ;Aに$FFをロード
$C0/D194 STZ $2126  [$00:2126]   ;[$00:2126]に$00を書き込み
$C0/D197 STA $2127  [$00:2127]   ;Aを[$00:2127]に書き込み
$C0/D19A LDA $0145  [$00:0145]   ;Aに[$00:0145]をロード
$C0/D19D ORA #$33                ;Aと$33で論理和
$C0/D19F STA $0145  [$00:0145]   ;Aを[$00:0145]に書き込み
$C0/D1A2 STA $2123  [$00:2123]   ;Aを[$00:2123]に書き込み
$C0/D1A5 STA $0146  [$00:0146]   ;Aを[$00:0146]に書き込み
$C0/D1A8 STA $2124  [$00:2124]   ;Aを[$00:2124]に書き込み
$C0/D1AB STA $0147  [$00:0147]   ;Aを[$00:0147]に書き込み
$C0/D1AE STA $2125  [$00:2125]   ;Aを[$00:2125]に書き込み
$C0/D1B1 LDA $0148  [$00:0148]   ;Aに[$00:0148]をロード
$C0/D1B4 AND #$F0                ;Aと$F0で論理積
$C0/D1B6 STA $0148  [$00:0148]   ;Aを[$00:0148]に書き込み
$C0/D1B9 STA $212A  [$00:212A]   ;Aを[$00:212A]に書き込み
$C0/D1BC LDA $0149  [$00:0149]   ;Aに[$00:0149]をロード
$C0/D1BF STA $212B  [$00:212B]   ;Aを[$00:212B]に書き込み
$C0/D1C2 LDA $0140  [$00:0140]   ;Aに[$00:0140]をロード
$C0/D1C5 ORA #$04                ;Aと$04で論理和
$C0/D1C7 AND $30    [$00:0030]   ;Aと[$00:0030]で論理積
$C0/D1C9 ORA $0142  [$00:0142]   ;Aと[$00:0142]で論理和
$C0/D1CC STA $0142  [$00:0142]   ;Aを[$00:0142]に書き込み
$C0/D1CF STA $212E  [$00:212E]   ;Aを[$00:212E]に書き込み
$C0/D1D2 LDA $0141  [$00:0141]   ;Aに[$00:0141]をロード
$C0/D1D5 AND $30    [$00:0030]   ;Aと[$00:0030]で論理積
$C0/D1D7 ORA $0143  [$00:0143]   ;Aと[$00:0143]で論理和
$C0/D1DA STA $0143  [$00:0143]   ;Aを[$00:0143]に書き込み
$C0/D1DD STA $212F  [$00:212F]   ;Aを[$00:212F]に書き込み
$C0/D1E0 JSL $CEDAC1[$CE:DAC1]   ;[$CE:DAC1]へジャンプ

$CE/DAC1 JSR $DAC5  [$CE:DAC5]   ;[$CE:DAC5]へジャンプ

$CE/DAC5 LDA #$41                ;Aに$41をロード
$CE/DAC7 STA $4370  [$00:4370]   ;Aを[$00:4370]に書き込み
$CE/DACA LDA #$26                ;Aに$26をロード
$CE/DACC STA $4371  [$00:4371]   ;Aを[$00:4371]に書き込み
$CE/DACF LDX #$3C00              ;Xに$3C00をロード
$CE/DAD2 STX $4372  [$00:4372]   ;Xを[$00:4373][$00:4372]に書き込み
$CE/DAD5 LDA #$7F                ;Aに$7Fをロード
$CE/DAD7 STA $4374  [$00:4374]   ;Aを[$00:4374]に書き込み
$CE/DADA STA $4377  [$00:4377]   ;Aを[$00:4377]に書き込み
$CE/DADD PHB                     ;DBレジスタをスタックにプッシュ
$CE/DADE LDA #$7F                ;Aに$7Fをロード
$CE/DAE0 PHA                     ;Aレジスタをスタックにプッシュ
$CE/DAE1 PLB                     ;DBレジスタにスタックからプル
$CE/DAE2 LDX #$3C00              ;Xに$3C00をロード
$CE/DAE5 LDA #$F0                ;Aに$F0をロード
$CE/DAE7 STA $0000,x[$7F:3C00]   ;Aを[$7F:3C00]に書き込み
$CE/DAEA LDA #$F0                ;Aに$F0をロード
$CE/DAEC STA $0003,x[$7F:3C03]   ;Aを[$7F:3C03]に書き込み
$CE/DAEF STZ $0006,x[$7F:3C06]   ;[$7F:3C06]に$00を書き込み
$CE/DAF2 PLB                     ;DBレジスタにスタックからプル
$CE/DAF3 LDA $014F  [$00:014F]   ;Aに[$00:014F]をロード
$CE/DAF6 ORA #$80                ;Aと$80で論理和
$CE/DAF8 STA $014F  [$00:014F]   ;Aを[$00:014F]に書き込み
$CE/DAFB STZ $01FA  [$00:01FA]   ;[$00:01FA]に$00を書き込み
$CE/DAFE RTS                     ;サブルーチン戻り

上がウィンドウ関連および、HDMA転送(チャネル7)の初期設定部分にあたる。
整理すると以下のようになる。

I/Oアドレス名前内容
[$00:2126]WH0 ウィンドウ1左座標設定$00
[$00:2127]WH1 ウィンドウ1右座標設定$FF
[$00:2123]W12SELBG1,BG2ウィンドウマスク設定$33 (%0011 0011)
[$00:2124]W34SELBG3,BG4ウィンドウマスク設定$33 (%0011 0011)
[$00:2125]WOBJSELスプライトウィンドウマスク設定$33 (%0011 0011)
[$00:212A]WBGLOGBGウインドウマスクロジック設定$00
[$00:212B]WOBJLOGスプライトウインドウマスクロジック設定$00
[$00:212E]TMWメインスクリーンウインドウマスク指定$17 (%0001 0111)
[$00:212F]TSWサブスクリーンウインドウマスク指定$00

ウィンドウ座標の設定[$00:2126][$00:2127]から、X座標$00$FF、つまり画面全体が内側扱いである。
ウィンドウマスク設定で、ウィンドウ1領域のBG1~BG4、スプライト、サブスクリーンすべてに「外側」の指定があるので、ウィンドウの外側にこの後指定する何かしらの処理を行うことになる。初期設定では画面全体が内側扱いだから、何の変化もない。
[$00:212E]のメインスクリーンウインドウマスク指定は、BG1・BG2・BG3・OBJが指定されており、実質、背景・スプライトすべてにマスク指定がかかっている(BG4はBGmode1だと非表示なので)。
すべてにマスクをかけるということは、「ウィンドウの外側には何も表示しない」つまり「黒一色になる」ということである。
(メニュー画面のハーフカラーマスの時は、背景のBG2は表示、BG1とBG3は半透明だから、真っ暗にはならなかったのである)
これが、戦闘開始時、三角形に切り取った外側だけが黒一色の画面になる仕組みである。

HDMA チャネル7
I/Oアドレス
内容
[$00:4370]GDMA / HDMA設定レジスタ$41
[$00:4371]転送先指定$26=ウィンドウ1左座標設定
[$00:4373][$00:4372]データ元アドレス(開始位置)$3C00
[$00:4374]データ元バンク$7F
[$00:4377]HDMA間接アドレスバンクバイト$7F
アドレス
[$7F:3C00]$F0
[$7F:3C03]$F0
[$7F:3C06]$00

HDMA転送はチャネル7を指定している。
GDMA / HDMA設定レジスタに$41(%0100 0001)を指定しているため、

  • 指定レジスタアドレスと、指定レジスタアドレス+1に対し、転送1回につき1度書き込みを行う
  • HDMAテーブルは、間接テーブルモード

ということになる。
また、[$00:4371]の転送先が$26だから、メニュー画面の時と同じように、[$00:2126](ウィンドウ1左座標)と[$00:2127](ウィンドウ1右座標)をスキャンライン毎に書き換えていくHDMA転送だということもわかる。
[$00:2126][$00:2127]に転送するHDMAテーブルは、[$00:4372]以降の設定から、$7F:3C00から開始になっている。
基本的には、メニュー画面の時のように、HDMAテーブルの値を3バイトずつ区切って見ていけば良いのであるが、間接テーブルモードというモードが選択されているので、HDMAテーブルの読み方がメニュー画面の時とは変わる。
$7F:3C00以降に、最初にテーブルとして読み込む位置のアドレスが書かれている」のである。バンクは[$00:4377]で指定していて$7Fである。
また、上の処理で、[$7F:3C00][$7F:3C03][$7F:3C06]には値がそれぞれはいっている。
3バイト区切りで整理してみると、a~dは任意の16進数の数値として、

  • [$7F:3C00]: $F0
  • [$7F:3C01]: aa(まだ書き込まれていないが、テーブル読み込みアドレスの下1バイト)
  • [$7F:3C02]: bb(まだ書き込まれていないが、テーブル読み込みアドレスの上1バイト)
  • [$7F:3C03]: $F0
  • [$7F:3C04]: cc(まだ書き込まれていないが、テーブル読み込みアドレスの下1バイト)
  • [$7F:3C05]: dd(まだ書き込まれていないが、テーブル読み込みアドレスの上1バイト)
  • [$7F:3C06]: $00(ここでHDMA転送を終了)

HDMAテーブルは、

  1. $7F:bbaaから$F0 (240)ピクセル分
  2. $7F:ddccから$F0 (240)ピクセル分

を処理して終了、と読み取れば良い。
間接テーブルモードの場合は[$00:2126][$00:2127]の2バイトに入れる値が$7F:bbaa以降に並ぶことになる。

つまり、HDMA転送前に、[$7F:3C01][$7F:3C02][$7F:3C04][$7F:3C05]、さらに、HDMAテーブル指定先に[$00:2126][$00:2127]の値を書き込む処理が入るはずだ。
実際の処理を見ると、まずHDMAテーブル指定先の値の書き込みを行い、[$7F:3C01][$7F:3C05]に値を入れた直後にHDMA転送をしている。
先に、[$7F:3C01][$7F:3C05]に値を入れてHDMA転送の部分を見てみる。

$C0/D33C LDX #$00E0              ;Xに$00E0をロード
$C0/D33F STX $22    [$00:0022]   ;Xを[$00:0023][$00:0022]に書き込み
$C0/D341 LDA $01FA  [$00:01FA]   ;Aに[$00:01FA]をロード
$C0/D344 BEQ $08    [$D34E]      ;ゼロフラグが立っているとき[$D34E]分岐
$C0/D34E LDA #$01                ;Aに$01をロード
$C0/D350 STA $01FA  [$00:01FA]   ;Aを[$00:01FA]に書き込み
$C0/D353 LDX #$3E40              ;Xに$3E40をロード
$C0/D356 PHB                     ;DBレジスタをスタックにプッシュ
$C0/D357 LDA #$7F                ;Aに$7Fをロード
$C0/D359 PHA                     ;Aレジスタをスタックにプッシュ
$C0/D35A PLB                     ;DBレジスタにスタックからプル
$C0/D35B REP #$20                ;Aを16bit幅に変更、Mフラグをクリア
$C0/D35D LDY #$3C00              ;Yに$3C00をロード
$C0/D360 TXA                     ;Xレジスタの値($3E40)をAレジスタに転送
$C0/D361 STA $0001,y[$7F:3C01]   ;Aを[$7F:3C02][$7F:3C01]に書き込み
$C0/D364 CLC                     ;キャリーフラグクリア
$C0/D365 ADC $22    [$00:0022]   ;A($3E40) + [$00:0023][$00:0022]($00E0)
$C0/D367 STA $0004,y[$7F:3C04]   ;Aを[$7F:3C05][$7F:3C04]に書き込み
$C0/D36A SEP #$20                ;MフラグON Aレジスタは8bit幅
$C0/D36C PLB                     ;DBレジスタにスタックからプル
$C0/D36D RTS                     ;サブルーチン戻り

$C0/BFE4 LDA $014F  [$00:014F]   ;Aに[$00:014F]($80)をロード
$C0/BFE7 STA $420C  [$00:420C]   ;Aを[$00:420C]に書き込み
;HDMA チャネル7転送開始
$C0/BFEA LDX $01F2  [$00:01F2]   ;Xに[$00:01F2]をロード
$C0/BFED CPX #$0001              ;Xと$0001を減算比較(ステータスレジスタ変更のみ)
$C0/BFF0 BEQ $33    [$C025]      ;ゼロフラグが立っているとき[$C025]分岐
$C0/BFF2 LDA $01F0  [$00:01F0]   ;Aに[$00:01F0]をロード
$C0/BFF5 INC A                   ;Aをインクリメント +1
$C0/BFF6 INC A                   ;Aをインクリメント +1
$C0/BFF7 INC A                   ;Aをインクリメント +1
$C0/BFF8 CMP #$48                ;Aと$48を減算比較(ステータスレジスタ変更のみ)
$C0/BFFA BCC $03    [$BFFF]      ;キャリーフラグが立っていないとき[$BFFF]分岐
$C0/BFFF STA $01F0  [$00:01F0]   ;Aを[$00:01F0]に書き込み
$C0/C002 JSR $D297  [$C0:D297]   ;[$C0:D297]へジャンプ

[$7F:3C02][$7F:3C01] = $3E40
[$7F:3C05][$7F:3C04] = $3F20

と書き込んでからHDMA転送を行っている。
つまり、

HDMAテーブルは、

  1. $7F:3E40から$F0 (240)バイト
  2. $7F:3F20から$F0 (240)バイト

を処理して終了

と、いうことになる。
1回目の指示で120ピクセル分、2回目の指示で120ピクセル分、合計で240ピクセル分のHDMA転送処理になる。
ただ、ゲーム画面のY軸(縦幅)は224ピクセルだから、上の指示では指定がオーバーしている。
実際には224ピクセル、一番下のラインまでしか処理しない。
また、実質、$7F:3E40$7F:3F1F$7F:3F20$7F:3FFFと連続したアドレスの値の転送になる。

では、$7F:3E40~に値を書き込む処理はどうなっているのか。
実は、先のHDMA転送設定の最後$CE/DAFEの続きから、処理が始まっている。

$CE/DAC4 RTL                     ;サブルーチン戻り

$C0/D1E4 RTS                     ;サブルーチン戻り

$C0/BF6F JSL $CED906[$CE:D906]   ;[$CE:D906]へジャンプ

$CE/D906 JSR $DB04  [$CE:DB04]   ;[$CE:DB04]へジャンプ

$CE/DB04 LDY #$000E              ;Yに$000Eをロード
$CE/DB07 LDA $01FA  [$00:01FA]   ;Aに[$00:01FA]をロード
$CE/DB0A BEQ $05    [$DB11]      ;ゼロフラグが立っているとき[$DB11]分岐
$CE/DB11 LDX #$3E40              ;Xに$3E40をロード
$CE/DB14 STX $20    [$00:0020]   ;Xを[$00:0020]に書き込み
$CE/DB16 PHB                     ;DBレジスタをスタックにプッシュ
$CE/DB17 LDA #$7F                ;Aに$7Fをロード
$CE/DB19 PHA                     ;Aレジスタをスタックにプッシュ
$CE/DB1A PLB                     ;DBレジスタにスタックからプル
$CE/DB1B REP #$20                ;Aを16bit幅に変更、Mフラグをクリア

;ループ処理
$CE/DB1D LDA #$00FF              ;Aに$00FFをロード
$CE/DB20 STA $0000,x[$7F:3E40]   ;Aを[$7F:3E41][$7F:3E40](+$20)に書き込み
$CE/DB23 STA $0002,x[$7F:3E42]   ;Aを[$7F:3E43][$7F:3E42](+$20)に書き込み
$CE/DB26 STA $0004,x[$7F:3E44]   ;Aを[$7F:3E45][$7F:3E44](+$20)に書き込み
$CE/DB29 STA $0006,x[$7F:3E46]   ;Aを[$7F:3E47][$7F:3E46](+$20)に書き込み
$CE/DB2C STA $0008,x[$7F:3E48]   ;Aを[$7F:3E49][$7F:3E48](+$20)に書き込み
$CE/DB2F STA $000A,x[$7F:3E4A]   ;Aを[$7F:3E4B][$7F:3E4A](+$20)に書き込み
$CE/DB32 STA $000C,x[$7F:3E4C]   ;Aを[$7F:3E4D][$7F:3E4C](+$20)に書き込み
$CE/DB35 STA $000E,x[$7F:3E4E]   ;Aを[$7F:3E4F][$7F:3E4E](+$20)に書き込み
$CE/DB38 STA $0010,x[$7F:3E50]   ;Aを[$7F:3E51][$7F:3E50](+$20)に書き込み
$CE/DB3B STA $0012,x[$7F:3E52]   ;Aを[$7F:3E53][$7F:3E52](+$20)に書き込み
$CE/DB3E STA $0014,x[$7F:3E54]   ;Aを[$7F:3E55][$7F:3E54](+$20)に書き込み
$CE/DB41 STA $0016,x[$7F:3E56]   ;Aを[$7F:3E57][$7F:3E56](+$20)に書き込み
$CE/DB44 STA $0018,x[$7F:3E58]   ;Aを[$7F:3E59][$7F:3E58](+$20)に書き込み
$CE/DB47 STA $001A,x[$7F:3E5A]   ;Aを[$7F:3E5B][$7F:3E5A](+$20)に書き込み
$CE/DB4A STA $001C,x[$7F:3E5C]   ;Aを[$7F:3E5D][$7F:3E5C](+$20)に書き込み
$CE/DB4D STA $001E,x[$7F:3E5E]   ;Aを[$7F:3E5F][$7F:3E5E](+$20)に書き込み
$CE/DB50 TXA                     ;Xレジスタの値をAレジスタに転送
$CE/DB51 CLC                     ;キャリーフラグクリア
$CE/DB52 ADC #$0020              ;A + $0020
$CE/DB55 TAX                     ;Aの値をXレジスタに転送
$CE/DB56 DEY                     ;Yをデクリメント -1
$CE/DB57 BNE $C4    [$DB1D]      ;ゼロフラグが立っていないとき[$DB1D]分岐
;ループ処理ここまで

$CE/DB59 SEP #$20                ;MフラグON Aレジスタは8bit幅
$CE/DB5B PLB                     ;DBレジスタにスタックからプル
$CE/DB5C RTS                     ;サブルーチン戻り

上のループ処理で、[$7F:3E40][$7F:3E41]から順に、2バイトずつ$00FFを書き込んでいる。
1回のループで$20バイト分を書き込み、ループ回数はYに入った$000Eだから、最終的に[$7F:3FFF][$7F:3FFE]まで$00FFで埋まる。
つまり$200バイト分書き込んだことになる。
ちょうどHDMA転送する224ピクセル分にあたる。
ただ、これでは[$00:2127][$00:2126]に送るデータがすべて$00FFであり、ウィンドウ右座標が$00で左座標が$FFのままであるから、何の変化はない。
要するにこれは初期値の設定になる。

この後に計算値が入っていくのだが、その計算が少々ややこしい。
要するに、ウィンドウ外と中の境界で正三角形を描くように、座標を計算して上書きしている。
$CE/DBA9のLDA $C0BC94,xで呼び出す2バイト区切りの値が座標計算用の係数であり、sinの三角関数表になっている。
サブルーチンについては省くが、最終的に[$7F:3E40][$7F:3FFE]を16バイト($10バイト)区切りで記すと以下のようになる。
これがHDMA転送前の値である。

03 C6 03 C8 03 C9 03 CB 03 CD 03 CE 03 D0 03 D2
03 D4 03 D5 03 D7 03 D9 03 DB 03 DC 03 DE 03 E0
03 E2 03 E3 03 E5 03 E7 03 E9 03 EA 03 EC 03 EE
03 EF 03 F1 03 F3 03 F5 03 F6 03 F8 03 FA 03 FC
03 FD 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF 03 FF
03 FD 03 FC 03 FA 03 F8 03 F6 03 F5 03 F3 03 F1
03 EF 03 EE 03 EC 03 EA 03 E9 03 E7 03 E5 03 E3
03 E2 03 E0 03 DE 03 DC 03 DB 03 D9 03 D7 03 D5
03 D4 03 D2 03 D0 03 CE 03 CD 03 CB 03 C9 03 C8
03 C6 03 C4 03 C2 03 C1 03 BF 03 BD 03 BB 03 BA
03 B8 03 B6 03 B4 03 B3 03 B1 03 AF 03 AE 03 AC

HDMAで1スキャンライン書き込むと2バイトのデータを[$00:2127][$00:2126]に書き込み、それをY軸一番下まで処理するから、

1スキャンライン目:[$7F:3E40]($03)[$7F:3E41]($C6)→[$00:2126][$00:2127]
2スキャンライン目:[$7F:3E42]($03)[$7F:3E43]($C8)→[$00:2126][$00:2127]
3スキャンライン目:[$7F:3E44]($03)[$7F:3E45]($C9)→[$00:2126][$00:2127]
……
224スキャンライン目:[$7F:3FFE]($03)[$7F:3FFF]($AC)→[$00:2126][$00:2127]

と、いうことになる。
1スキャンライン目だったら、X座標$03$C6の間がウィンドウ内部扱いだから、戦闘直前の画像がそのまま表示されるが、X座標$00$02及びX座標$C7$FFはウィンドウの外扱いで、非表示、つまり真っ黒に塗りつぶし状態になる、ということである。
実際に上の指示どおりに横256×縦224マスのエクセル方眼紙を塗りつぶしてみたら、以下のようになる。

目が痛くなりそうで申し訳ない。筆者も塗りつぶしながら目が痛くなったが、左隅の3マス分と、右上側、右下側が黒で塗りつぶされた形状になる。
正三角形の右側が尖った状態の「▷」の形状の一部であることがおわかりいただけるだろうか。
これが戦闘開始直後に、最初にHDMA転送で送られるウィンドウマスク処理である。
ここから、$7F:3D??~にHDMAテーブルを転送してHDMA転送でウィンドウマスク処理、を繰り返すことで、三角形の形状を少しずつ回しながら小さくしていき、最後に画面全体を黒くして、フィールド画面から戦闘画面へと切り替えるのである。

なお、三角形マスキング用の1画面分の描画は50回行われている。
つまり50フレーム、1秒に満たない程度である。
その後暗転して、次はアイリスイン(主人公を起点に円状に画面が広がる)で戦闘画面が表示される。
このアイリスイン処理も、HDMA転送でのウィンドウマスク処理になる。
アイリスイン処理は、1画面分の描画24回分で、三角形の回転の半分程度の時間でしかなく、プレイヤーもあまりアイリスイン処理を気にしてはいないのではなかろうか。

イージング(easing)について

三角形の回転だが、見ていてなんとなく、三角形が小さくなるにつれて回転や三角形の大きさの減少が速くなっている(加速している)ような気がしないだろうか。
気がするだけでなく、実際にそういうように見えるよう計算されている。
加速と減速を自然に見せる「イージング」という手段があるが(徐々に加速し、徐々に減速するなど)、三角形ウィンドウマスクやアイリスインなどでも使われている。
この加速手段も三角関数が使われている。y = sin xの曲線はx = -1から1までの範囲の値を与えると、ちょうどyの値が「徐々に加速し、徐々に減速」を描くように見えるからである。
本作のようにスーパーファミコンの時代のゲームはもちろん、ファミコン以前のコンピュータゲームから、現代のゲームでも用いられている手法であり、イージング処理を実装するための「イージング関数」なども探せば簡単に見つかるだろう。

イージングについては三角関数以外でのいろいろな実装方法があるが(二次関数曲線や反比例曲線など)、詳しくは検索していただきたい。

戦闘画面表示前のアイリスイン処理

戦闘開始直後、$C1/F2D1$C1/F319にて、以下のようにGDMA関係のI/Oアドレスに値が書き込まれる、とこれまでにも紹介した。
アイリスイン処理は、以下におけるチャネル4のHDMA設定を使う。

アドレスn0123457
$00:43n0GDMA / HDMA
設定レジスタ
$01$00-$00$41$01$09
$00:43n1$18$22$80$04$26$26$18
転送先VRAMCGRAMWRAMOAMWindow
座標1L
Window
座標1L
VRAM
$00:43n2~3データ元アドレス----$A700$E9FD-
$00:43n4データ元バンク----$7E$C1-
$00:43n7間接HDMA
アドレス
----$7E$C1-

GDMA / HDMA設定レジスタに$41(%0100 0001)を指定しているため、

  • 指定レジスタアドレスと、指定レジスタアドレス+1に対し、転送1回につき1度書き込みを行う
  • HDMAテーブルは、間接テーブルモード

と、先ほどの戦闘突入時の三角形処理と同じである。
ただ、[$00:4343][$00:4342]の値はこの後更新されるため、HDMAテーブルのアドレス位置は上の値ではない。

また、ウィンドウ関連のI/Oアドレスの値も確認しなければならない。
背景グラフィックと効果 > 戦闘でのサブルーチンで、初期設定を紹介している。
$C1/F338$C1/F3AAが該当部分で、この時点ではウインドウ関係の値は以下のようになっている。

アドレス名称内容
[$00:212E]TMWメインスクリーンウインドウマスク指定$00
[$00:212F]TSWサブスクリーンウインドウマスク指定$00

初期設定の時点では、BGやスプライトなどに一切マスクがかかっていない。
アイリスイン処理の前に、各種設定が行われる。

$C1/8771 LDX #$9EC8              ;Xに$9EC8をロード
$C1/8774 STX $4342  [$00:4342]   ;Xを[$00:4343][$00:4342]に書き込み
$C1/8777 LDA $F2    [$00:03F2]   ;Aに[$00:03F2]をロード
$C1/8779 ORA #$10                ;Aと$10で論理和
$C1/877B STA $F2    [$00:03F2]   ;Aを[$00:03F2]に書き込み
$C1/877D STA $420C  [$00:420C]   ;Aを[$00:420C]に書き込み
$C1/8780 LDA #$33                ;Aに$33をロード
$C1/8782 STA $2123  [$00:2123]   ;Aを[$00:2123]に書き込み
$C1/8785 STA $2124  [$00:2124]   ;Aを[$00:2124]に書き込み
$C1/8788 LDA #$03                ;Aに$03をロード
$C1/878A STA $2125  [$00:2125]   ;Aを[$00:2125]に書き込み
$C1/878D LDA #$1F                ;Aに$1Fをロード
$C1/878F STA $212E  [$00:212E]   ;Aを[$00:212E]に書き込み
$C1/8792 RTS                     ;サブルーチン戻り

$C1/835B JSR $6145  [$C1:6145]   ;[$C1:6145]へジャンプ

上の時点で、以下のように値が入る。

I/Oアドレス名前内容
[$00:4343]A1T4HDMA転送元A1テーブルアドレス上位$C8
[$00:4342]A1T4LDMA転送元A1テーブルアドレス下位$9E
[$00:2123]W12SELBG1,BG2ウィンドウマスク設定$33 (%0011 0011)
[$00:2124]W34SELBG3,BG4ウィンドウマスク設定$33 (%0011 0011)
[$00:2125]WOBJSELスプライトウィンドウマスク設定$03 (%0000 0011)
[$00:212A]WBGLOGBGウインドウマスクロジック設定$00
[$00:212B]WOBJLOGスプライトウインドウマスクロジック設定$00
[$00:212E]TMWメインスクリーンウインドウマスク指定$1F (%0001 1111)
[$00:212F]TSWサブスクリーンウインドウマスク指定$00

[$00:4343][$00:4342]がここで更新されているため、$7E:9EC8以降に、HDMAテーブルのアドレス位置が書かれているはずだ。

なお、$C1/877D[$00:420C]$10が書き込まれており、ここで一度HDMA送信をしている。
ただ、この送信は通常戦闘だと、スキャンラインの222~224番目、つまり画面下の2ピクセル分のラインにHDMA転送しているだけで、実質画面に何かの変化はない(はず)。

ウィンドウマスク関係の設定は、細かい数値の違いがあるものの、戦闘前の三角形マスキングとだいたい同じ。
BG1~BG4、スプライトすべてウィンドウの外側マスクであるし、メインスクリーンも、BG1~BG4、スプライトすべてマスクする設定である。
このため、ウィンドウの外側はBGもスプライトもすべてマスクされ、真っ黒になる。

HDMAテーブルが書かれる$7E:9EC8~なのだが、実は上のサブルーチンの少し前に処理がある。

$C1/82FA LDA #$F0                ;Aに$F0をロード
$C1/82FC STA $7E9EC8[$7E:9EC8]   ;Aを[$7E:9EC8]に書き込み
$C1/8300 STA $7E9ECB[$7E:9ECB]   ;Aを[$7E:9ECB]に書き込み
$C1/8304 LDA #$00                ;Aに$00をロード
$C1/8306 STA $7E9ECE[$7E:9ECE]   ;Aを[$7E:9ECE]に書き込み
$C1/830A LDA #$04                ;Aに$04をロード
$C1/830C STA $7E9EC9[$7E:9EC9]   ;Aを[$7E:9EC9]に書き込み
$C1/8310 LDA #$E4                ;Aに$E4をロード
$C1/8312 STA $7E9ECC[$7E:9ECC]   ;Aを[$7E:9ECC]に書き込み
$C1/8316 LDA #$9D                ;Aに$9Dをロード
$C1/8318 STA $7E9ECA[$7E:9ECA]   ;Aを[$7E:9ECA]に書き込み
$C1/831C STA $7E9ECD[$7E:9ECD]   ;Aを[$7E:9ECD]に書き込み
$C1/8320 RTS                     ;サブルーチン戻り

以上から、以下のように値が入った。

アドレス
[$7E:9EC8]$F0
[$7E:9EC9]$04
[$7E:9ECA]$9D
[$7E:9ECB]$F0
[$7E:9ECC]$E4
[$7E:9ECD]$9D
[$7E:9ECE]$00

バンクは$7Eと指定されたままなので、HDMAテーブルは、

  • $7E:9D04から$F0 (240)ピクセル分
  • $7E:9DE4から$F0 (240)ピクセル分

で終了、となる。
ただ、224スキャンライン目までHDMA転送する都合で、実際には後半は$7E:9DE4$7E:9EC3までの転送で終了になる。

この後から、$7E:9D04~に値を入れる処理が入る。

$C1/834B JSR $60E8  [$C1:60E8]   ;[$C1:60E8]へジャンプ

$C1/60E8 LDA #$01                ;Aに$01をロード
$C1/60EA STA $92    [$00:0392]   ;Aを[$00:0392]に書き込み
$C1/60EC LDA #$0F                ;Aに$0Fをロード
$C1/60EE STA $91    [$00:0391]   ;Aを[$00:0391]に書き込み
$C1/60F0 LDA #$01                ;Aに$01をロード
$C1/60F2 STA $93    [$00:0393]   ;Aを[$00:0393]に書き込み
$C1/60F4 STA $94    [$00:0394]   ;Aを[$00:0394]に書き込み
$C1/60F6 RTS                     ;サブルーチン戻り

$C1/834E LDA #$0F                ;Aに$0Fをロード
$C1/8350 STA $92    [$00:0392]   ;Aを[$00:0392]に書き込み
$C1/8352 JSR $845E  [$C1:845E]   ;[$C1:845E]へジャンプ

$C1/845E LDX #$00FF              ;Xに$00FFをロード
$C1/8461 REP #$20                ;Aを16bit幅に変更、Mフラグをクリア
$C1/8463 TXA                     ;Xレジスタの値をAレジスタに転送
$C1/8464 LDX #$9B00              ;Xに$9B00をロード
;ループ処理
$C1/8467 STA $7E0000,x           ;Aを[$7E:9B01][$7E:9B00]~[$7E:9CC3][$7E:9CC2]に書き込み
$C1/846B INX                     ;Xをインクリメント +1
$C1/846C INX                     ;Xをインクリメント +1
$C1/846D CPX #$9CC4              ;Xと$9CC4を減算比較(ステータスレジスタ変更のみ)
$C1/8470 BNE $F5    [$8467]      ;ゼロフラグが立っていないとき[$8467]分岐
;ループ処理ここまで
$C1/8472 SEP #$20                ;MフラグON Aレジスタは8bit幅
$C1/8474 RTS                     ;サブルーチン戻り

$C1/8355 JSR $8475  [$C1:8475]   ;[$C1:8475]へジャンプ

$C1/8475 REP #$20                ;Aを16bit幅に変更、Mフラグをクリア
$C1/8477 LDX #$9B00              ;Xに$9B00をロード
;ループ処理
$C1/847A LDA $7E0000,x           ;Aに[$7E:9B01][$7E:9B00]~[$7E:9CC3][$7E:9CC2]をロード
$C1/847E STA $7E0200,x           ;Aを[$7E:9D01][$7E:9D00]~[$7E:9EC3][$7E:9EC2]に書き込み
$C1/8482 INX                     ;Xをインクリメント +1
$C1/8483 INX                     ;Xをインクリメント +1
$C1/8484 CPX #$9CC4              ;Xと$9CC4を減算比較(ステータスレジスタ変更のみ)
$C1/8487 BNE $F1    [$847A]      ;ゼロフラグが立っていないとき[$847A]分岐
;ループ処理ここまで
$C1/8489 SEP #$20                ;MフラグON Aレジスタは8bit幅
$C1/848B RTS                     ;サブルーチン戻り

$C1/8358 JSR $8771  [$C1:8771]   ;[$C1:8771]へジャンプ
;$C1/8771~$C1/8792のI/Oアドレス設定へ

これが初回設定で、[$7E:9B01][$7E:9B00][$7E:9CC3][$7E:9CC2]$00FFで埋めた後、[$7E:9D01][$7E:9D00][$7E:9EC3][$7E:9EC2]にコピーしている。
HDMA転送開始アドレスの$7E:9D04$FF$7E:9D05$00、……と続くから、実質、[$00:2126][$00:2127]$FF00ですべて揃えるだけである。
ウィンドウ左隅の座標を$FF、右隅を$00とするのは幅が負の値となり明らかにおかしいが、このように幅が負または0に設定された時は、すべてウィンドウ外とする扱いである。
これにより、画面内はすべてウィンドウ外、つまり全体がマスクされた状態で隠されるため、戦闘開始直後、つまり三角形のウィンドウ効果後は画面が真っ暗になった状態である。

ここから、[$7E:9D01][$7E:9D00][$7E:9EC3][$7E:9EC2]に計算値を入れていくことで、アイリスイン効果を出して、主人公キャラの位置を中心に円状に画面を広げていく。
上の初期値設定のサブルーチンのジャンプ処理をざっとまとめると、

$C1/8352
┣$C1/845E~$C1/8474
$C1/8355
┣$C1/8475~$C1/848B
$C1/8358

こんな手順である。
だが、HDMA転送準備2回目からはサブルーチンが変わる。

┣$C1/845E~$C1/8474
$C1/8378
┃┣$C1/83A5~$C1/83AF
┃┃┣$C1/46C9(乗算時間稼ぎ)
┃┣$C1/83B2~$C1/83BC
┃┃┣$C1/46C9(乗算時間稼ぎ)
┃┣$C1/83BF~$C1/83CF
┃┃┣$C1/46C8~$C1/46C9(乗算時間稼ぎ)
┃┗$C1/83D2~$C1/8458
$C1/837B
(省略)
$C1/837E~$C1/838C
┣$C1/8475~$C1/848B

だいたい上のようになっている。
$C1/83D2$C1/8458は、アイリスイン部分のみ[$7E:9B01][$7E:9B00][$7E:9CC3][$7E:9CC2]を上書き、という処理にあたる。

つまり$C1/83A5$C1/8458で、アイリスインする部分のウィンドウ幅を計算+[$7E:9B01][$7E:9B00][$7E:9CC3][$7E:9CC2]に上書きという作業を行っているが、詳細は省く。
円の形状にアイリスインするよう計算するための係数が、$C1/83F5の命令LDA $CAFF00,xで、$CA:FF00$CA:FFFFのいずれかになる。
戦闘中、技によっては円の形状にカラーマスでエフェクトをかけることがあるが、おそらくそのあたりも$CA:FF00$CA:FFFFの係数で座標計算をしている(ようである)。

以降はマスク部分がなくなるまでアイリスインして、戦闘画面表示となり、HDMA 4チャネルの転送が終了する。



このページをシェアする

上へ