このパートでは、マリオのスタイルでプレイ可能な最初のデモが表示されます。 これを行うには、スクロールとデバッグの方法を扱う必要があります。
スクロール
レジスタ$ 2005は、バックグラウンドスクロールを制御します。 最初のエントリはそこに水平スクロール位置を設定し、2番目のエントリは垂直スクロール位置を設定します。 設定されているスクロールが不明な場合は、レジスタ$ 2002から水平読み取りにリセットできます。
99%のゲームが使用するネームテーブルは2つだけです。実際、これは画面の背景です。 さらに2つの使用可能なテーブルが最初の2つをミラーリングします。 エミュレータは、カートリッジのiNESイメージのヘッダーからテーブル設定に関する情報を取得します。 バイト6および7は、マッパー(カートリッジ内のコプロセッサー)について説明しています。 バイト6の最下位ビットは、スクロール方向を示します。 0-垂直にスクロールします。名前テーブルは水平にミラーリングされます。 1-その逆。 その結果、画面の使用可能な1x2領域(選択したスクロールに応じて2x1)と、この領域上をスライドするウィンドウがテレビに表示されます。
Gauntletゲームでは、4方向のスクロールが使用されます。 これには、カートリッジごとに2kの追加RAMが必要です。 マッパーMMC3ゲームでは、ゲームの途中でスクロールモードを切り替えることができます。 ただし、ほとんどの場合、スクロールモードはゲーム全体で同じであり、2つの名前テーブルのみが使用されます。
最初の例では、水平スクロールを構成します。 これは、reset.sファイルで設定されます。 ジョイスティックの矢印は背景を移動します。 スプライトは、バックグラウンド位置インジケータを実装しました。水平シフトの場合はH、垂直シフトの場合はVです。 このイメージをFCEUXで実行し、運転中に名前テーブルのデバッガーを確認することを強くお勧めします。
$ FFを水平に横切った後、名前テーブルはレジスタPPU_CTRLにアクセスすることにより変更されます-それは$ 2000にあります。 ユーザーにとって、これは目に見えません。
次のツールを使用してデモを準備しました。文字はPhotoshopで描画され、4色の画像にインデックスが付けられ、YY-CHRにコピーされました。 次に、それらをchrファイルに保存し、NES Screen Toolで開き、背景を作成してから、RLE圧縮で.hファイルとしてエクスポートする必要があります。 起動時にダウンロードできるようになりました。 キャラクターの動きは背景のシフトによって実装され、スプライトの位置は変わりません。
void move_logic(void) { if ((joypad1 & RIGHT) != 0){ state = Going_Right; ++Horiz_scroll; if (Horiz_scroll == 0) ++Nametable; } if ((joypad1 & LEFT) != 0){ state = Going_Left; --Horiz_scroll; if (Horiz_scroll == 0xff) ++Nametable; } Nametable = Nametable & 1; // 0<->1 if ((joypad1 & DOWN) != 0){ state = Going_Down; ++Vert_scroll; if (Vert_scroll == 0xf0) Vert_scroll = 0; } if ((joypad1 & UP) != 0){ state = Going_Up; --Vert_scroll; if (Vert_scroll == 0xff) Vert_scroll = 0xef; } }
フレームを更新すると、スプライトが更新され、スクロール位置が設定されます。
void every_frame(void) { OAM_ADDRESS = 0; OAM_DMA = 2; // $200-$2FF RAM PPU_CTRL = (0x90 + Nametable); // NMI PPU_MASK = 0x1e; SCROLL = Horiz_scroll; SCROLL = Vert_scroll; // Get_Input(); }
2番目の例では、スクロールは垂直であり、名前テーブルは画面の左端と右端をループします。 すべて同じリセットで公開されます。 垂直スクロールの場合、名前テーブル0および2が使用されます。
画面の高さは240ピクセルなので、最大垂直スクロール位置は$ EFです。 これは、前の例と同様に処理されます。 もう1つの違いは、名前テーブルをゼロから2番目に、またはその逆に切り替えることです。
PPU_CTRL = (0x90 + (Nametable << 1));
最も単純なプラットフォーマー
そして、プラットフォームで水平スクロールとジャンプを使用したデモを行います。 2つのバックグラウンドページのコリジョンマップはメモリに保存され、そこに200バイトを占有します。
最初に重力をやってみましょう。 プラットフォーム上にない場合、各フレームスプライトは(++ Y)に落ちます。 メタスプライトの下部が背景に揃えられていると仮定します。 そのため、プラットフォームでメタスプライトの下隅が失敗したかどうかを確認できます。
// // ? NametableB = Nametable; Scroll_Adjusted_X = (X1 + Horiz_scroll + 3); // high_byte = Scroll_Adjusted_X >> 8; if (high_byte != 0){ // 255 , ++NametableB; NametableB &= 1; // 0<->1 } // ? collision_Index = (((char)Scroll_Adjusted_X>>4) + ((Y1+16) & 0xf0)); collision = 0; Collision_Down(); // , ++collision // ... , (X1 + Horiz_scroll + 12); void Collision_Down(void){ if (NametableB == 0){ // temp = C_MAP[collision_Index]; collision += PLATFORM[temp]; } else { // temp = C_MAP2[collision_Index]; collision += PLATFORM[temp]; } } // platform // , // if(collision == 0){ Y_speed += 2; } else { Y_speed = 0; Y1 &= 0xf0; // }
次に、動きとジャンプの滑らかさに取り組む必要があります。 スプライトと背景の位置の座標には多くの変数が必要で、速度、加速度、最大許容速度にはいくつかの定数が必要です。 しかし、私はそれを獲得しました。 その結果、スクロール速度は古いニブルX_speedに保存されます。
Horiz_scroll += (X_speed >> 4);
通常、バックグラウンドスクロールは、キャラクターが画面の端に近づくと開始されます。 そして、それが中央部にあるとき、それは静的な背景でそれ自身で動きます。 ここでも、簡単にするために、この手法は使用しません。 たぶんいつかリファクタリングをするでしょう。
ゼロスプライトを使用します。 デバッグ
スプライトゼロヒットは、水平スクロール位置の変更など、フレームの途中でイベントを追跡する1つの方法です。 これにより、ポイントカウンターなど、画面の静的な上部を作成し、画面の下部をスクロールできます。
実装するにはいくつかの方法があります。
- スプライトゼロヒット
- スプライトオーバーフロー(これを行う必要はありません)
- サウンドプロセッサの中断(など)
- 一部のマッパーは行数をサポートしています(MMC3を使用する場合に適しています)
最初の方法のみが私たちに適しています-それは最も単純でバグが多い方法です。
ゼロスプライトは、OAMのアドレス0〜3に保存されます。 不透明なピクセルが含まれ、このピクセルが不透明な背景ピクセルの上に描画される場合、ビット0x40がレジスタ$ 2002に設定されます。 スプライトが透明な背景の上に描画されると、ゲームは無限ループに入ります。 これを使用してスクロールをカスタマイズできます。 手順はアセンブラーで記述されています。
まず、必要なすべてをVブランクで実行します。 次に、水平スクロールをゼロに設定し、目的の名前テーブルをオンにします。 次に、SpriteZero()を呼び出し、イベントを待機します-必要なピクセルが重ねられた線を描画します。 その後、スクロールと名前テーブルを切り替えることができます-これは画面レンダリングの途中で発生します。
// NMI - Sprite_Zero(); // SCROLL = Horiz_scroll; SCROLL = 0; // PPU_CTRL = (0x94 + Nametable);
この例では、明確にするために、ゼロスプライトにはゼロシンボルが含まれています。 そして、[スタート]をクリックすると、彼はそれを非表示にしました。
if ((joypad1 & START) > 0){ SPRITE_ZERO[1] = 0xff; // SPRITE_ZERO[2] = 0x20; // }
チュートリアルの最初のバージョンでは、レッスンはここで終了しましたが、トピックを展開することにしました。 そこで、RLE圧縮なしで、4画面の背景幅とメタファイルでの動的な背景生成を使用してデモを作成します。
キャラクターを16ピクセル移動させると、デモは画面の境界を越えて2列のタイルを目的の名前テーブルに追加します。 Vブランクに収まります。 書き込み手順を高速化するには、PPUupdateをアセンブラーで記述してループする必要がありました。 バックグラウンド属性テーブルも作業に応じて変化します。
困難で扱いにくいことが判明し、デバッグには多くの時間がかかりました。 したがって、デバッグ手法を示す良い例があります。
第一に、スクロールの実装は遅く、時間内にネストされません。 これを理解するには、コマンドを挿入する必要がありました
PPU_MASK = 0x1F;
メイン()でVブランクを待機します。 これ以降、画面の線は白黒でレンダリングされます。 このハックは、すべてのエミュレーターと互換性があるわけではありません。たとえば、FCEUXでは、「古いPPU」オプションを有効にする必要があります。 次のようになりました。
利用可能なプロセッサ時間の半分はすでに費やされており、これには音楽や対戦相手がいません。 関数をプロファイルするために、関数を実行する前後に変数にレコードを作成し、デバッガーをオンにしてこの変数のアドレスのレコードを追跡しました。 また、FCEUXは停止間のプロセッササイクルをカウントできます。 どういうわけかこのようになった:
TEST = *((unsigned char*)0xFF) // ++TEST; Should_We_Buffer(); // 4422 ++TEST;
バッファの処理が遅くなることが判明しました。 短い2つの関数に分割し、フレーム全体で実行できます。 プロセッサの負荷がより良くなりました。
次に、左のスクロールを削除します。 これで、左に走って画面の端に寄りかかることができるように実装するとよいでしょう。 すぐにこれは機能せず、コードへの分析鈍化の方法によるデバッグ((c) DIHALT )は役に立ちませんでした。 アドレスマップを生成する必要がありました。 これを行うには、オプションを指定してリンカーを呼び出します。
ld65 ... -Ln “labels.txt”
-gオプション付きのトランスレーターを備えたコンパイラー。
これらのファイルから、疑わしい関数move_logic()が$ C5B2にあることがわかるので、そこにブレークポイントを配置します。 原則として、コードに直接マークを配置して最適化をオフにすることができますが、適切な場所で空の関数を呼び出し(文字を左に移動)、マークマップ上の正確な位置を追跡しました。 ただし、変数レコードのインターセプトはよりコンパクトで便利です。
それでも、アセンブリリストごとにデバッグする必要がありましたが、 'if(X_speed <0)'の誤った比較がすぐに見つかりました。 この時点で、左を押してもX_speedはゼロにリセットされます。 比較を<=に変更し、すべてが正常でした。
FCEUXでは、デバッガーをオンにしてジョイスティックを処理するには、「自動保留」オプションをキーボードボタンにマップし、最初に保留をオンにして左を押してから、デバッガーにブレークポイントを設定する必要があります。
NesdevのユーザーRainwarriorが行いました。ca65ラベルをFCEUXデバッガー用のファイルに変換するPythonスクリプトを少し修正しました 。 彼はlabel.txtを入力に取り込みます。 使用例は、レッスンのソースのmakefileおよびbatファイルにあります。
4画面にスクロールできるようになりましたが、想像以上に複雑です。 オプションBも似ていますが、すべてのデバッグがカットアウトされています。 デバッガで名前テーブルを見て、スクロールの仕組みを理解することをお勧めします。
Dropbox
Github
場合によっては、リンカーがマップ内のすべてのラベルの入力を拒否することがあります。その場合、各アセンブラーファイルに次の行を追加する必要があります。
.debuginfo
それ以外の場合は、cc65およびca65を呼び出すたびに-gを追加する必要があります。