CのNES向けのゲーム開発。第7〜10章。 ジョイスティックを操作します。 スプライトの衝突

ゲームの記述にスムーズに移動します。 このパートでは、ジョイスティックとスプライトの競合の処理について説明します。







<<<前 次>>>







画像

出所







ユーザー入力



ジョイスティックの操作は非常に簡単です。 最初のジョイスティックのボタンを押すと4016ドル、2番目のボタンを押すと4017ドルです。 PPUを更新してスクロールを設定した直後に、フレームごとに1回読み取るだけで十分です。







ジョイスティックごとに2つの変数を常に取得します。現在押されているボタンと、最後のフレームを押すためです。 押されたボタンを取得するには、最初に4016ドルで書き込み、次に0で書き込む必要があります。そこから8つの値を読み取ります。これらは、ジョイスティックでボタンを押すことに対応する値になります。 これらは、A、B、選択、開始、上、下、左、右の順に終了します。 それらはビットシフトと論理演算で便利に保存されます。







ボタンのビットマスクを定義することも便利です。 これにより、高速で明確なビット単位の操作でイベントを受信できます。







ボタン定義
#define RIGHT 0x01 #define LEFT 0x02 #define DOWN 0x04 #define UP 0x08 #define START 0x10 #define SELECT 0x20 #define B_BUTTON 0x40 #define A_BUTTON 0x80
      
      





最後のレッスンのメタスプライトを使用して、画面上を移動します。 この作品は、アセンブラーで書くのにはるかに便利でした。掘り下げる必要はありません。







Cコードからアセンブラー関数を呼び出すには、そのプロトタイプが必要です。 リンカーは、コンパイル段階でそれらをまとめます。







void Get_Input(void);









関数をvoidとして宣言することはオプションです;これは、コードの均一性のためです。 __fastcall__を使用することを強くお勧めします。この場合、最後の(または唯一の)引数がレジスタAおよびXを介して渡されるためです。これはスタックを介するよりも高速です。 8ビット引数はレジスタAを介して渡され、16ビット引数はA / Xのペアにあり、32ビット引数はA / X / sregです。sregはメモリのゼロページの16ビット変数です。 詳細については、コンパイラのドキュメントを参照してください







しかし、Get_Input()に戻ります。 各フレームの後にこの関数を1回呼び出すと、すべてのボタンの押下が収集され、便利な形式になります。







これで、ジョイスティックを使用して画面上の小さな男を動かすことができます。 すべてのアセンブラコードがasm4c.sファイルに移動されます。 ビルドスクリプトも調整されています。 そして、ジョイスティックイベントハンドラーは別の関数に移動されます。







ハンドラーコードをクリックします
 void move_logic(void) { if ((joypad1 & RIGHT) != 0){ state = Going_Right; ++X1; } if ((joypad1 & LEFT) != 0){ state = Going_Left; --X1; } if ((joypad1 & DOWN) != 0){ state = Going_Down; ++Y1; } if ((joypad1 & UP) != 0){ state = Going_Up; --Y1; } }
      
      





ソースコード:

Dropbox

Github







スプライトの衝突



2つのスプライトの衝突を検出する最も簡単な方法。 ここでは、16x16ピクセルのサイズのメタスプライトを検討します。 これは、原則として、ほとんどのNESゲームの標準と見なすことができます。







衝突検出は、オブジェクトのエッジの座標を比較することにより実装されます。 かなり明らかな比較の束が判明しました。 左上隅の座標を介してスプライトの位置を決定し、境界を計算すると便利です。 次のようになります。







スプライト衝突検出
 A_left_side_X = A_X + 3; //   -      A_right_side_X = A_X + 12; //   A_top_Y = A_Y; //  A_bottom_Y = A_Y + 15; //  //    if (A_left_side_X <= B_right_side_X && A_right_side_X >= B_left_side_X && A_top_Y <= B_bottom_Y && A_bottom_Y >= B_top_Y){ //    }
      
      





空のエッジを正しく処理するには左マージンが必要です







画像







しかし、整数オーバーフローは不愉快な驚きになります。 スプライトが画面の右端、A_X = 250の領域、A_X + 12 = 6の場合、これは明らかに間違っています。 エッジをチェックし、オーバーフロー時に値を255に設定する必要があります。これは完全ではありませんが、うまく機能します。 座標の下で16ビット変数を取得することは可能ですが、非効率的です-フレームごとに多くのスプライトに対して衝突チェックコードが実行され、6502プロセッサはそのような多数では強力ではありません。 または、スプライトのアプローチを強制的にエッジに制限できます。







  A_left_side_X = A_X + 3; if (A_left_side_X < A_X) A_left_side_X = 255; //       
      
      





次の例では、オブジェクトBは前の章のコードとともに独自に移動し、Aはジョイスティックで制御されます。 それらがタッチされるたびに、カウンターは1ずつ増加します。検証はフレームごとに1回行われます。 カウンターは、カウンターの各桁の整数変数として保存され、手動で転送されます。







衝突カウンター
 if (score5 > 9){ ++score4; score5 = 0; } if (score4 > 9){ ++score3; score4 = 0; } if (score3 > 9){ ++score2; score3 = 0; } if (score2 > 9){ ++score1; score2 = 0; } if (score1 > 9){ //     score1 = 0; score2 = 0; score3 = 0; score4 = 0; score5 = 0; }
      
      





カウンタが更新されるたびに、次のフレームでカウンタが再描画されるフラグに従ってフラグが設定されます。 Vブランク期間中にのみスプライトを変更できます。 このイベントは、NMIハンドラーを介してキャッチされます。







カウンターレンダリング
 void PPU_Update (void) { PPU_ADDRESS = 0x20; PPU_ADDRESS = 0x8c; PPU_DATA = score1+1; //   ,  - "0",  - "1",    PPU_DATA = score2+1; //          PPU_DATA = score3+1; PPU_DATA = score4+1; PPU_DATA = score5+1; }
      
      





画像







Dropbox

Github







全背景レンダリング



これで、スプライトを表示できます-V空白期間中または画面を強制的にオフにします。 V-blankは2行のタイルにのみ十分であり、2番目のケースでは、完全な再描画のためにいくつかのフレームをスキップする必要があります-画面はデフォルトの背景にあふれます。







2番目のオプションはより単純で、必要なコードが少なくなります。 背景は、NES Screen Toolでの描画に非常に便利であり、RLE圧縮による名前テーブルの保存をサポートしています。 これらは、アセンブラーの単純なデコーダーによって解凍されます。 詳細は説明しませんが、完成したコードを取得します。







ジョイスティックの[開始]をクリックすると、背景が変更されます。 また、ボタンが長時間押されたときにレンダリングが1回だけ実行されるようにしてください。そうしないと、複数のレンダリングが重複して開始される可能性があります。







ロジックは次のようなものです。







  1. フレームごとにジョイスティックの登録を読み取ります
  2. Startがこのフレームに押し込まれ、それより前のフレームが押されなかった場合...
  3. 画面の空白フラグを設定します
  4. 最も近いVブランクでは、画面を消します
  5. 必要なすべてをレンダリングする
  6. 次のVブランクで、画面をオンにします


全画面背景レンダリング
 void Draw_Background(void) { All_Off(); PPU_ADDRESS = 0x20; //  $2000 =    #0 PPU_ADDRESS = 0x00; UnRLE(All_Backgrounds[which_BGD]); //   Wait_Vblank(); //   ,    V-blank All_On(); ++which_BGD; if (which_BGD == 4) //    which_BGD = 0; } const unsigned char * const All_Backgrounds[]={n1,n2,n3,n4}; //    
      
      





画像







コード:

Dropbox

Github







背景との衝突



背景とのスプライトの衝突を追跡するのは少し難しくなります。 デフォルトでは、16x16の正方形のメタファイルと同じサイズのスプライトを使用します。 ほとんどのゲームはこのスキームを使用します。 スプライトはフレームごとに1ピクセルで4つの方向のいずれかに移動し、各フレームは衝突をチェックします。







通常、スプライトは最初に移動し、接触があるかどうかを確認し、このタッチが処理されます。 これを2つの座標に分けます-最初にシフトし、水平にチェックしてから、垂直にチェックします。







PPUからの読み取りは苦痛です。 ゲームロジックを動作させてスプライトを更新するのに十分な時間があるように、現在の名前テーブルのアドレスを計算し、Vブランク中にPPUに要求する必要があります。 それはしません。







バックグラウンドメタタイルマップを1つのRAMページに保存する必要があります。 同じマップを使用して衝突をすばやく計算できます。タイルのタイプが2種類しかない場合は、文字0に対しては通過可能にし、最初のタイルには通過させません。 さらに多くのタイプのタイルが必要な場合は、開通性テーブルを個別に保存する必要があります。 原則として、カードはカートリッジROMに保存することもできます。







コリジョンマップは、タイルエディタで簡単に作成できます。 メタファイル(2つすべて)はNES Screen Toolで描画され、256x256にカットされたスクリーンショットを介してTiledに転送されます。 バックグラウンドごとに1つ、.csvにエクスポートできます。 このファイルは少し修正する必要がありました-定数のヘッダーを追加します







インポートの準備ができたカード
 const unsigned char c2[]={ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0, 0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0, 0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0, 0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0, 0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
      
      





これで、Cコードにインポートし、配列へのポインターを参照できます。







[スタート]ボタンハンドラーは次の背景を描画し、衝突マップをRAMのアドレス300〜3FFにロードします。 これを行うには、設定を微調整する必要がありました-サイズが300ドルと100ドルのMAPセグメントを追加します。 コードは、このセグメントに空の配列を登録するだけです。







 #pragma bss-name(push, “MAP”) unsigned char C_MAP[256];
      
      





正確なアドレスは、FCEUXでのデバッグにも便利です-実行中のゲームでデバッガーとして使用して、何をどのように確認できます。







したがって、衝突マップはROMからRAMにロードされます。







 p_C_MAP = All_Collision_Maps[which_BGD]; //      ROM for (index = 0;index < 240; ++index){ C_MAP[index] = p_C_MAP[index]; //   RAM }
      
      





しかし、しばらくしてからmemcpyでこれを書き直しました。バイト単位のコピーには42688プロセッサーサイクルがかかり、memcpyの9倍です。







 void __fastcall__ memcpy (void* dest, const void* src, int count); p_C_MAP = All_Collision_Maps[which_BGD]; //     memcpy (C_MAP, p_C_MAP, 240);
      
      





しかし、それだけではありません。 発射体への3番目のアプローチはアセンブラーを使用したもので、4%高速でした。 今のところ価値がないと思います 大きなゲームでは可能ですが、これらのプロセッササイクルでは十分ではなく、コンソールから可能なすべてを絞り出さなければなりません。







背景との衝突をチェックするロジックは次のようなものです。







  1. スプライトを水平に移動します
  2. 左右のエッジを考慮します-エッジに透明なストライプがあります。それらを考慮する必要があります
  3. 右に移動した場合、スプライトの右上隅または下隅が禁止タイルに落ちたかどうかを確認します
  4. 残っている場合、その逆


右端と背景の衝突を確認する
 if ((joypad1 & RIGHT) != 0){ //   corner = ((X1_Right_Side & 0xf0) >> 4) + (Y1_Top & 0xf0); //        if (C_MAP[corner] > 0) X1 = (X1 & 0xf0) + 3; //   -   //   corner = ((X1_Right_Side & 0xf0) >> 4) + (Y1_Bottom & 0xf0); if (C_MAP[corner] > 0) X1 = (X1 & 0xf0) + 3; //   -   }
      
      





スプライトの3ピクセルの透明なエッジを補正するには、+ 3が必要です。







垂直衝突チェックも同様に行われます。 どうやら、スプライトが1ポイント/フレームより速く移動する場合、コードは機能しません。







スプライトは常に予想より1ポイント低く描画されることに注意してください。 OAMの垂直座標を更新する前に修正を行うことができます。 プラットフォーマーでは、通常これで十分です。 トップビューのゲームは奇妙に見えるかもしれません-キャラクターはテクスチャに少し落ちます。







ソースコード。 カードをコピーする3つの実装は、異なるプロジェクトに広がっています。

lesson8.zip-ループ

lesson8B.​​zip-memcpy

lesson8C.zip-アセンブラー

Github








All Articles