CでのNESのゲーム開発。第24章付録2-メモリバンクの操作

サイクルの最後の部分。 この章では、例を使用してマッパーMMC3の操作について説明します。

<<<前へ



画像

出所



以前はメモリバンクの切り替えを使用しませんでしたが、MMC3マッパーについて学習します。 マッパーがなければ、32 KBのPRG ROMをコードに使用し、8 KBのCHR ROMをグラフィックに使用できます。 マッパーを使用すると、この障壁を回避できます。



実際のカートリッジでのゲームのリリースを念頭に置いてください。 [マニュアル](http://kevtris.org/mappers/mmc3/)には、次のオプションがあると記載されています。



-最大64K PRG、64K CHR

-最大512K PRG、64K CHR

-最大512K PRG、VRAM

-最大512K PRG、256K CHR

-最大128K PRG、64K CHR、8K CHR RAM



リストは不完全です。 最もコンパクトな形式である64 / 64kを選択してください。 エミュレータが認識できるように、カートリッジイメージのヘッダーでこれを指定する必要があります。 画像形式のドキュメントはwikiで入手できます



INESヘッダー
.byte $4e,$45,$53,$1a

.byte $04 ; = 4 x 04000 PRG ROM

.byte $08 ; = 8 x 02000 CHR ROM

.byte $40 ; = №4 - MMC3










次に、.cfgにメモリバンクを登録する必要があります。



フラグメントnes.cfg
# ROM:

# $8000,

PRG0: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;

PRG1: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;

PRG2: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;

PRG3: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;

PRG4: start = $8000, size = $2000, file = %O ,fill = yes, define = yes;

PRG5: start = $a000, size = $2000, file = %O ,fill = yes, define = yes;

PRG6: start = $c000, size = $2000, file = %O ,fill = yes, define = yes;

PRG7: start = $e000, size = $1ffa, file = %O ,fill = yes, define = yes;



# ROM

VECTORS: start = $fffa, size = $6, file = %O, fill = yes;












すべてのメモリバンクは、同じアドレス8000ドルでロードされます。 実行可能コードは最後の再読み込み不能バンクにあり、任意のアドレスに配置できます。 マッパーを使用する場合、メモリの割り当ては最も困難です。ここでは注意が必要です。



セグメントは構成に登録する必要があります。

nes.cfg
SEGMENTS {

HEADER: load = HEADER, type = ro;

CODE0: load = PRG0, type = ro, define = yes;

CODE1: load = PRG1, type = ro, define = yes;

CODE2: load = PRG2, type = ro, define = yes;

CODE3: load = PRG3, type = ro, define = yes;

CODE4: load = PRG4, type = ro, define = yes;

CODE5: load = PRG5, type = ro, define = yes;

CODE6: load = PRG6, type = ro, define = yes;

STARTUP: load = PRG7, type = ro, define = yes;

CODE: load = PRG7, type = ro, define = yes;

VECTORS: load = VECTORS, type = ro;

CHARS: load = CHR, type = rw;



BSS: load = RAM, type = bss, define = yes;

HEAP: load = RAM, type = bss, optional = yes;

ZEROPAGE: load = ZP, type = zp;

#OAM: load = OAM1, type = bss, define = yes;

}










この例では、OAMセグメントは使用されません。



そして、各バンクで注目すべき何かを書き、それがROMファイルにどのように収まるかを確認します。 たとえば、Bank0、Bank1などの単語を使用します。 これらの単語は画面に表示され、スタートボタンでバンクを切り替えます。



目的のバンクに変数を配置するには、PRAGMAディレクティブを使用します。

lesson19.c
#pragma rodata-name (“CODE0”)

#pragma code-name (“CODE0”)

const unsigned char TEXT1[]={

“Bank0”};



#pragma rodata-name (“CODE1”)

#pragma code-name (“CODE1”)

const unsigned char TEXT2[]={

“Bank1”};



#pragma rodata-name (“CODE2”)

#pragma code-name (“CODE2”)

const unsigned char TEXT3[]={

“Bank2”};












[スタート]をクリックすると、アドレス$ 8000- $ 9FFFのメモリバンクがオフになり、最初の5バイトが画面に表示されます

銀行からテキストを出力する
 void Draw_Bank_Num(void){ //     PPU_ADDRESS = 0x20; PPU_ADDRESS = 0xa6; for (index = 0;index < 5;++index){ PPU_DATA = TEXT1[index]; } PPU_ADDRESS = 0; PPU_ADDRESS = 0; }
      
      











TEXT1は、コンパイル段階で決定され、コンソールの開始時にゼロバンクをポイントします。 銀行を変更すると、この住所は変更されずに残り、いずれの場合でも、住所$ 8000-8004からのテキストが表示されます。 銀行は次のように切り替えます。

銀行の切り替え
 if (((joypad1old & START) == 0)&&((joypad1 & START) != 0)){ ++PRGbank; if (PRGbank > 7) PRGbank = 0; *((unsigned char*)0x8000) = 6; //   PRG   $8000 *((unsigned char*)0x8001) = PRGbank; Draw_Bank_Num(); //    
      
      









アドレス$ 8000はROMに属しますが、レコードはマッパーによってインターセプトされます。 次はロードする銀行番号です。 [wiki](http://wiki.nesdev.com/w/index.php/MMC3)の通常の詳細:



少しの混乱は、銀行の先頭の住所とマッパーの公式レジスタのランダムな平等によって引き起こされます。 銀行を$ A000- $ BFFFの住所に送金できます。



 *((unsigned char*)0x8000) = 7; //    PRG - $A000 *((unsigned char*)0x8001) = which_PRG_bank;
      
      







しかし、管理レジスタは、8000ドルと8001ドルのアドレスのままです。



また、main()の先頭に初期化コードを追加しました。 この瞬間は文書化されていませんが、明らかに、リセット後、アドレス$ E000- $ FFFFで最後のバンクのみの正しいロードが保証されます。 初期化コードはすべて、そこにのみ配置する必要があります。



メモリバンクを使用するこの方法(先頭が1つのアドレスに固定されている場合)は非常に不便です。 通常、データ構造と関数へのポインターを持つ配列は、各バンクの先頭に保存されます。 次に、それらの間接遷移、またはスタックでのより高速なフォーカスに進むことができます 。 アセンブラーがありますが、それだけの価値があります。



いずれにせよ、視差付きの背景スクロールを追加します。 これを行うには、4フレームごとに、CHR ROMバンクをPPUメモリ領域に切り替えます-タイルがそこから取得されます。 MMC3はCHR ROMを64タイルのバンクに分割します。これは0x400バイトです。 アニメーション化されたウォーターフォールを作成します。タイルの各セットで1ピクセルずつシフトします。バンクを変更すると、アニメーションが生成されます。



画像



ソースコードへのリンク、次のフレームは[スタート]ボタンで表示されます。

Dropbox

Github



MMC3は、テレビに表示される行をカウントする方法も知っています。 これは通常、ゼロスプライトを介して行われますが、フレームごとに1回動作します-時にはそれ以上が必要です。 背景の視差をシミュレートするために、20行ごとにスクロール位置を変更します。 MMC3は適切なタイミングで割り込みを発生させ、そのハンドラーでスクロールが目的の位置に設定されます。 ハンドラーはアセンブラーで記述されています。Cを使用している場合、関数http://www.cc65.org/faq.php#IntHandlersを呼び出すと誤ってスタックを損傷する可能性があるためです。



プレフィックスの先頭で、割り込みはオフになります。それらはmain()に含める必要があります。



 asm (“cli”); //  
      
      







reset.sファイルの最後にある割り込みベクトル内のポインターは、正しいハンドラーを指している必要があります。 行カウントを設定できるようになりました:



 *((unsigned char*)0xe000) = 1; //  MMC3 IRQ *((unsigned char*)0xc000) = 20; //    20  *((unsigned char*)0xc001) = 20; *((unsigned char*)0xe001) = 1; //   MMC3 IRQ
      
      







どうやら、21行後に割り込みがトリガーされるため、最初の行は考慮されません。



また、非常に短いHブランク期間(ビームが行の先頭まで移動する時間)の間、水平スクロールを引くことも非常に望ましいことです。 これを考慮しないと、画像にわずかな歪みが生じます。 どこを見るべきか知っていれば、多くのゲームで目立ちます。



MMC3割り込みはHブランクで正確に起動しますが、その期間はハンドラーに移動するには十分ではありません。 そこで、次のHブランクまで約100ビート待つ単純なループを配置しました。 この点は、一部のエミュレータでは正確に処理されない場合があります。 実際のゲームは、次の行を待たずに、塗りつぶされた領域でスクロールシフトを行います。 スクロールシフトの後、次の20行を待って、もう一度繰り返します。



自分の目で見たい場合は、ハンドラーのループ制限を修正してください。 文字通り1回の繰り返しのシフトが見られます-Hブランクは本当に短いです。



画像



スタートはまだ銀行を切り替えますが、ここでは目立ちません。



Dropbox

Github



あなたが再コンパイルを台無しにするのが面倒なら、ここにgifがあります:



画像



タイミングサイクルは1回転分短縮されます-スクロールは数ピクセルを行末まで変更します。 歪みは、各水平レイヤーの一番下の行の右端に表示されます。 フレームごとに変化するため、画面上ですべてが踊ります。 中断が行の途中で機能する場合、それは非常に悪いでしょう。



このスクロール操作により、視差効果を実現できます。 YouTubeのクエリ「NES parallax scrolling」は、明確な例を示しています。 繰り返しますが、ほとんどのゲームでは、背景レイヤーは塗りつぶしで区切られています。



All Articles