Turbo Pascalを初めて見た20分後に、初めてコンソール用のゲームを開発する方法を考えました。 キーボードの付いたSuborが時々目を引き、「おそらくプログラムを入力して再生することができる」という考えが浮かびました。 しかし、このトピックに関する情報はまったく入手できなかったため、関心はすぐに薄れました。 次回、古いコンソールの非常にプレイ可能なエミュレーターを見たときに同じ考えが浮かびました。 次に、リストをコンソール自体に入れる必要はないことが明らかになりました。 かなり後のどこかで、Habrはそのようなことに対して慈悲深い聴衆とともに現れました。 ある時点で、私はマニュアルを書くために異種の情報を収集し始めましたが、今日、既製の教科書に出会いました。これは明らかに翻訳する必要があります。
古いコンソールの開発は広く文書化されていますが、情報の99%がアセンブラーの開発に関連しているのはNESによるとです。 何らかの理由で、Sで作業をマスターする必要があるとハッキングしました。
みなさんこんにちは。
私の名前はダグです。 私はもう1年NES向けのゲームを書いていますが、このブログを始めることにしました。 NESゲーム開発チュートリアルを書いて、他の人に自分のゲームを作るように促します。
ブログの特徴は純粋なCの使用であるため、他のプログラマーは、プロセッサー6502のアセンブラーを深く掘り下げることなく、すぐに書き始めることができます。
また、私は開発やブログのどちらの専門家でもないことを忘れないでください。 NESについて質問がある場合は、おそらくWikiで回答を見つけることができます。
トレーニングを可能な限り簡素化し、最も単純な例を使用するようにします。 また、ゲームの最も単純なアイデアから始めることをお勧めします。 読者は明らかに新しいゼルダを作りたいと思うでしょうが、それはうまくいきません。 最も単純なゲームの開発には2〜3か月、ゼルダ-2〜3年かかります。 そのようなプロジェクトは放棄される可能性があります。 少なくとも初めて、Pakmanに注目してください。
コンソールメモリ
メモリの構造について話しましょう。 NESには2つの独立したアドレス空間があります-範囲が$ 0〜$ FFFFのプロセッサメモリとPPUビデオチップのメモリ。
プロセッサのメモリから始めましょう。
- 最初の800ドルはRAMです。
- 6000ドルから7FFFの範囲は、一部のゲームでSRAM(バッテリー付きカートリッジに保存)を使用したり、追加のワークRAMとして使用したりするために使用されます。
- スペースに$ 8000- $ FFFF ROMが表示されます。 一部のマッパー(カートリッジ内のオプションのプロセッサー)は32k以上のROMを使用できますが、通常は$ 8000- $ FFFFで動作します。
- アドレス$ FFFC- $ FFFDは、プログラムの開始を示すリセットベクトルです。
詳細な情報はこちらです 。
PPUには独自の独立したアドレス空間があります。 サイズは3FFFですが、一部の場所でミラー化されています。 それへのアクセスは、プロセッサメモリ内のレジスタを通過します。 4つの画面バッファーに十分なビデオメモリがありますが、ゲームの大部分ではスクロールを実装するために2つしか使用されていません。
- $ 0- $ 1FFF =スプライトはここに保存されます
- $ 2000- $ 23FF =名前テーブル0
- $ 2400- $ 27FF =表1の名前
- $ 2800- $ 2BFF =名前表2
- $ 2C00- $ 2FFF =名前表3
この場合、テーブル2と3はテーブル0と1のミラーです。 - $ 3F00- $ 3F1F =パレット
名前テーブル、名前テーブルは、背景タイルと画面上の位置をリンクします。
ミラーリングを使用すると、水平スクロールまたは垂直スクロールを制御できますが、すべてに独自の時間があります。
PPUには、サイズが256バイトの個別のOAMメモリ領域であるオブジェクト属性メモリもあります。 それへのアクセスは、プロセッサのアドレス空間のレジスタを介して実装され、スプライトの表示を制御できます。
詳細なPPUメモリ情報は次のとおりです。
http://wiki.nesdev.com/w/index.php/PPU_memory_map
別のポイント。 カートリッジには2つのタイプがあります。 いくつかは2つのROMチップを持っています-実行可能コードを備えたPRG-ROMとグラフィックを備えたCHR-ROM。 この場合、スケジュールは自動的にアドレス$ 0-1FFF PPUにマッピングされます。 これにより、非常に簡単に描画できます-テーブルにタイル番号を書き込むだけです。 この形式を使用します。
別のタイプのカートリッジは、CHR-ROMの代わりにCHR-RAMを使用します。 これにより、この追加RAMにグラフィックの一部をロードできます。 これは複雑な手法であり、このチュートリアルでは取り上げません。
これで、開発に使用されるソフトウェアを確認できます。
- コンパイラ
- タイルエディター
- グラフィックエディター
- メモ帳++
- 良いエミュレータ
- タイルパッカー
このチュートリアルではcc65のみを扱います。 これは、6502、NESプロセッサ用の最高のコンパイラの1つです。
バージョン2.15を使用しています(確認のため、コンソールに「cc65 --version」と入力します)。 異なるバージョンのファイルには互換性がないため、必要に応じてコンパイラキットのnes.libを使用してください。
次に、グラフィックを作成する必要があります。 YY-CHRを使用します
グラフィックの前処理には、PhotoshopまたはGIMPのグラフィックエディターが必要です。
このコードは、 メモ帳++で書くのに便利です 。 構文の強調表示と行番号付けがあります-これによりデバッグが容易になります。
そして今、エミュレーター。 私はFCEUXを90%の時間使用しています。なぜなら、それはメモリ、スプライトビューアーなどを操作するためのクールなデバッガーとツールを備えているからです。 しかし、エミュレーションでは最も正確ではありません。 ゲームは別の場所でテストする必要があります。 レビューから判断すると、最も正確なエミュレーターは、Nintendulator、Nestopia、およびpuNESです。 ここで、より正確なパレットをロードすることが依然として望まれます。
FCEUXには、SDLとWin32の2つのバージョンがあります。 1つ目はほぼすべての場所で機能し、2つ目はWindowsでのみ機能します。 そのため、デバッガは2番目にのみ存在します。 そのため、代替OSの場合、仮想マシンまたはWineを使用する必要があります。
そして最後に、タイルアライナー。 それなしでもゲームを作成できますが、間違いなく役立ちます。 NES Screen Toolをお勧めします。 コンソールの色の制限を完全に示しており、単一画面のゲームに最適です。 スクロールゲームには、 タイルマップエディターが最適です。
これをすべて使用する方法は?
画像を適切なサイズ、たとえば128ピクセル幅に圧縮する必要があります。 次に、4色に変換し、必要に応じて傷を修正します。 これで、YY-CHRでコピーアンドペーストできます。
YY-CHRでは、色が2ビットであることを確認する必要があります。
パレットは、他の場所に設定されているため、今は重要ではありません。
ss65はどのように
NESのすべてのコンパイラは、グラフィカルインターフェイスなしでコンソールを介して動作します。 つまり、メモ帳でプログラムを作成し、必要なパラメーターを使用してコンパイラーを呼び出します。
作業を簡素化するために、.batスクリプトとMakefileを使用します。 これにより、プロセスが自動化され、ワンタッチでカートリッジの画像が収集されます。
プロセスはこのようなものです。 cc65は、Cコードファイルをアセンブリコードにコンパイルします。 ca65はオブジェクトファイルを収集します。 ld65は、エミュレータで実行できる.nesカートリッジイメージにリンクします。 設定は.cfgファイルに保存されます。
プレフィックスは8ビットプロセッサMOS 6502を使用します。8ビットを超える変数に単純にアクセスする方法はわかりません。 アドレス指定は16ビットで、数学的には加算、減算、ビットシフトのみです。 そのため、これらの要因を考慮してコードを作成する必要があります。
- ほとんどの変数は、unsigned char型-8ビット、値0〜255である必要があります
- 関数に値を渡さないか、3つのレジスタ(A、X、Y)に引数を渡すfastcallディレクティブを介して値を渡すことをお勧めします
- 配列は256バイトより長くすることはできません
- printfがありません
- ++ gはg ++よりも著しく速い
- cc65は、構造体を値で渡すことも、関数から返すこともできません
- グローバル変数はローカル変数、構造体よりもはるかに高速です
-Oオプションを使用して最適化します。 オプションi、r、sもあります。これらは、-Oirsで組み合わされることもありますが、たとえば、値が使用されないプロセッサレジスタから読み取りを削除できます。 そしてこれは致命的です。
コンパイラを使用するための推奨事項を次に示します。
8ビットプロセッサのリソースは非常に少ないため、最適化は不可欠です。場合によっては、コードの実行時間を監視する必要があります。 通常のCコードはこれらの要件を満たしていません。
他のファイルからの変数のインポートがサポートされています。 cc65は、コマンドを使用してアセンブラーモジュールから変数と配列をインポートできます。
extern unsigned char foo;
そして、それがメモリのゼロページからの文字である場合、ディレクティブを追加します
#pragma zpsym (“foo”);
将来のコースでは、これらのデザインはほとんど使用されません。 唯一の例外は、大きなバイナリファイルのインポートです。 この場合、アセンブラーファイルにラップするのが最適です。
.export _foo _foo: .incbin "foo.bin"
そして、次のようにCにインポートします
extern unsigned char foo[];
_65記号はここで重要です。アセンブリコードにコンパイルされると、cc65は各変数名の前に_を追加するからです。 これに合わせる必要があります。
__fastcall__を使用して、アセンブラーで記述された関数を呼び出すことができます。 この場合、引数はスタックではなくレジスタを介して関数に渡されます。時間を節約できます。 場合によっては、たとえばプレフィックスを初期化するときなど、アセンブラコードを省くことができません。 いずれにせよ、関数に渡される引数が少ないほど良いです。 2つの関数、テスト変数とグローバル変数Aを比較します。
void Test (char A) { test = A; } // 19
jsr pusha ldy #$00 lda (sp),y sta _test ; test = A; jmp incsp1 pusha: ldy sp beq @L1 dec sp ldy #0 sta (sp),y rts @L1: dec sp+1 dec sp sta (sp),y rts incsp1: inc sp bne @L1 inc sp+1 @L1: rts
void Test (void) { test = A; } // , 3
lda _A sta _test rts
アセンブラコードをコードに直接挿入することもできます。 私はほとんどそれをしませんが、時にはそれが時々必要です。 次のようになります。
asm ("Z: bit $2002") ; asm ("bpl Z") ;
さらに、かさばるcrt0.s初期化コードをコンパクトなreset.sに置き換え、これらすべての設定を微調整しました。 これらのファイルは時々変更されます。 nes.libは、コンパイラの標準のものを使用します。 プロジェクトは、中間アセンブラファイルを削除しない-add-sourceオプションを使用してビルドされます。生成されたコードを検討できます。
ソースコードで変数を定義してから、アセンブラにインポートする方が便利です。
.import _Foo
しかし、これは好みの問題であり、私の意見では、そのようなコードはより視覚的です。
ハローワールド
このプログラムは、画面にテキストを印刷するだけです。 プレフィックスは、ASCIIエンコーディングと、あらゆる形式のテキストの操作について知らないことに注意する必要があります。 ただし、背景に8x8のサイズの写真を表示する機会があります。
そこで、スプライト文字の配列を作成し、その文字のアドレスがASCIIコードに対応するようにします。 その後、Cのコードからそれらを取得できます。
プレフィックス初期化コードをそのまま使用しますが、実行後、main()への遷移が発生します。
次の操作を行う必要があります。
- 画面をオフにする
- パレットをカスタマイズ
- 大切な言葉を引き出す
- スクロールを無効にする
- 画面をオンにする
- 繰り返す
ビデオメモリを操作すると画面にゴミが発生するため、画面をオフにする必要があります。 画面をオフにするか、フレームブランキングパルス(V-Blank)を待つ必要があります。 次回はこの問題について詳しく検討します。
初期化コードはメモリをゼロで埋め、画面全体がゼロのタイルでいっぱいになるようにします。この例では空です。 そして、パレット全体が灰色で塗りつぶされています。
画面に表示するには、アドレス$ 2006の上位バイトから開始して、塗りつぶしの開始の座標を記録し、次に$ 2007にタイル番号を書き込む必要があります。 PPUは対応する番号のタイルを1行ずつ出力し、新しい行に移行します。 PPUを32の出力ステップに再構成できます-タイルは上下に表示されます。 レジスタ$ 2000を使用して、ステップ1を設定する必要があります。 NES画面ツールを使用して、画面座標をアドレスに再計算できます。
また、パレットの最初の4色を塗りつぶす必要があります-それらは背景を担当します。 それらは3F00ドルで記録されます。
PPUレジスタに書き込むとスクロール位置が壊れるため、リセットする必要があります。 そうしないと、画像が画面から消える場合があります。 これは、2006年と2005年のレジスタを通じて行われます。
#define PPU_CTRL *((unsigned char*)0x2000) #define PPU_MASK *((unsigned char*)0x2001) #define PPU_STATUS *((unsigned char*)0x2002) #define SCROLL *((unsigned char*)0x2005) #define PPU_ADDRESS *((unsigned char*)0x2006) #define PPU_DATA *((unsigned char*)0x2007) unsigned char index; const unsigned char TEXT[]={ "Hello World!"}; const unsigned char PALETTE[]={ 0x1f, 0x00, 0x10, 0x20 }; //black, gray, lt gray, white void main (void) { // turn off the screen PPU_CTRL = 0; PPU_MASK = 0; // load the palette PPU_ADDRESS = 0x3f; // set an address in the PPU of 0x3f00 PPU_ADDRESS = 0x00; for(index = 0; index < sizeof(PALETTE); ++index){ PPU_DATA = PALETTE[index]; } // load the text PPU_ADDRESS = 0x21; // set an address in the PPU of 0x21ca PPU_ADDRESS = 0xca; // about the middle of the screen for( index = 0; index < sizeof(TEXT); ++index ){ PPU_DATA = TEXT[index]; } // reset the scroll position PPU_ADDRESS = 0; PPU_ADDRESS = 0; SCROLL = 0; SCROLL = 0; // turn on screen PPU_CTRL = 0x90; // NMI on PPU_MASK = 0x1e; // screen on // infinite loop while (1); }
コードへのリンク:
Dropbox
Github
Githubでは、Windowsで正しく機能するようにMakefileをわずかに修正しました。
ひも
ONCE: load = PRG, type = ro, optional = yes;
.cfgファイルのセグメント{}セクション内は、cc65の最新バージョンとの互換性のために必要です。
「PPUMASK = 0x1e」で画面をオンにする方法は、 wikiで説明されています 。
ここのすべてのファイルのサイズは0x4000です。 これは、可能な限り最小のPRG ROMサイズです。 ゲームの90%はここに収まらず、$ 8000- $ FFFFのアドレスにマッピングされます。 わが国では、ゲームは$ C000- $ FFFFのアドレスにロードされ、$ 8000- $ BFFFでミラーリングされます。 より大きなゲームを開発するには、ROMの開始アドレスを$ 8000に再構成し、サイズも$ 8000に設定する必要があります。 また、ヘッダーセクションに2番目のバンクPRG ROMも含めます。