-
グラフィック関連では、パレットカラーが扱えるツールが必要です。
ここでは、当方がWindows上の開発で使用したツール類の簡単な紹介を行います。
詳細なインストール及び利用方法は、各ツールの配布先を参照してください。
Windows以外の方は導入が大変ですがWine上で動作させることができます。
- EDGE
- TAKABOSOFT様のEDGEシリーズは、ドット絵作成に特化した高機能なペイントソフトです。
256色パレットのbmp形式で出力したファイルを後述の「Pic to CHR」や「YY-CHR」でNES用のバイナリデータへ変換します。
ただし、NESは4色パレットしか扱えないため同時に使う色は4色に抑え、パレットととして選択する色も固定の52色から選択することとなります。
当方は、4色パレット画像を用意した後、後述の「YY-CHR」で色の調整を行っていました。 - Pic to CHR
- わいわい様作成のデータ変換ツールです。
一枚絵(256*240)から、キャラクタデータ(CHR)とネームテーブル(NAM)を作成します。
キャラクタデータは、縦横8ピクセルを1単位(キャラクタ)として一枚絵の中から繰り返しパターンがまとめられた状態で出力されます。マッパー0ではキャラクタROM領域に配置するデータとなります。
ネームテーブルは背景用にキャラクタの並び方を表すデータですが、キャラクタROMではなく通常のデータ領域に格納し、プログラムでPPUへ転送することになります。
- YY-CHR
- わいわい様作成の汎用グラフィックエディタです。パレットがNES向けのものが使えます。
キャラクタデータの最終的な編集を行うツールとなります。
パレットデータも出力可能ですので、ネームテーブル同様、通常のデータ領域に格納してプログラムでPPUへ転送します。
最低限これだけ準備できれば、NESのキャラクタデータは何とかなります。
-
大まかなグラフィックデータ作成の流れは以下の通りです。
- EDGEで基本となるビットマップファイルを作成
- Pic to CHRでビットマップファイルからキャラクタデータを生成(背景の場合は、ネームテーブルも生成する)
- YY-CHRでキャラクタデータの調整やパレットデータを生成
今回キャラクタデータはキャラクタROM上に配置し、PPUが最初から参照可能とします。
ネームテーブルやパレットデータはプログラムROM側の配置になります。アセンブラソース上でプログラムROMのデータ領域セグメントにバイナリファイルをインクルードするか、Cソース上でunsigned char型の配列に用意します。
注意点は以下の通りです。
- スプライトは1キャラクタ単位で4色1組のパレットを指定するが、0番目のパレットを透過色として使うため、1キャラクタでは3色+透過した背景色となる。
- BGは属性テーブルによって4キャラクタ単位で4色1組のパレットを指定するが、0番目のパレットはスプライトと同様に透過色として扱う。
- 背景色はBGパレットの先頭(PPUの0x3F00番地)の値で設定するが、スプライトパレットの先頭(PPUの0x3F10番地)がミラーアドレスのため、パレットデータの上書きに注意すること。
- マッパー0の場合は、キャラクタROMのメモリサイズがBGとスプライトで合わせて8KBです。BG用とスプライト用で別々に用意する場合は、splitコマンドなどを用いて1ファイルを4KBに切り詰めます。
以降の説明では、以下のサンプルデータを使用しています。
- BG用キャラクタデータ:sample-bgchr.zip (bg.chr)
英数フォントと背景用パターンを数種。
パレットアニメーション用のパターンもあります。 - 8*8スプライト用キャラクタデータ:sample-sprite8x8.zip (sprite.chr)
男女の横16、縦32ドットの歩行パターン。
ドットが荒いのは8*16ドットで作っていたのを縦横2倍に引き伸ばしたためです。
表示イメージに合わせて並べていますので、8*8スプライトを組み合わせて表示することを想定しています。 - 8*16スプライト用キャラクタデータ:sample-sprite8x16.zip (sprite.chr)
8*16サイズのスプライトで表示できるようキャラクタパターンの並びを変えたバージョン。
今回使用するのは上半分の8*16スプライト用に並び変えた部分。
1つのスプライトで8*8が縦並びに2つ分表示されますので、キャラクタデータの並べ方に注意が必要です。 - パレットデータ:sample-palette.zip (palette.dat)
上記キャラクタデータ用のパレットデータ。
最初の4組がBG用で次の4組がスプライトで利用することを想定しています。- BG
- スプライト
-
以下は、バイナリデータをアセンブラ上で配置した例です。
.setcpu "6502" .autoimport on ; C言語参照用に変数名をexport宣言 .export _palette ; プログラムROMのデータ領域セグメントを指定 .segment "RODATA" ; パレット _palette: .incbin "palette.dat" ; キャラクタROMのセグメントを指定 .segment "CHARS" ; パターンテーブル(BG、スプライトを個別のファイルで扱う場合:合わせて8KBまで) ; BG用とスプライト用データの順番は、PPUコントロールレジスタに設定するbit3、4のうち「0」を設定した方が先 ; BG用 .incbin "bg.chr" ; スプライト用 .incbin "sprite.chr" ; パターンテーブル(BG、スプライトを一つのファイルで扱う場合: 8KBまで) ;.incbin "pattern.chr"
- アセンブラソース上では「_」から始まる識別子が、C言語上では「_」なしで参照可能です。
- アドレス解決のみ行われますので型やサイズをC言語側で実データに合わせて宣言します。
- ネームテーブル、属性テーブルやパレットデータは、プログラムにてPPU管理のメモリへ転送します。
-
以下は、kihon.hに定義されている関数を使った背景とパレット設定サンプルです。
#include <kihon.h> // C言語では「_」なしで、データサイズに合わせたextern宣言を行う。 // 256バイトのパレットデータを16色1組で使いたいため2次元配列として宣言している。 extern const unsigned char palette[16][16]; // 文字列表示用のパターンネームデータ const unsigned char message[] = "BACKGROUND TEST!"; // 背景サンプルのパターンネームデータ #0 const unsigned char bgsa0[] = { 0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0x0A,0x0B,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6, 0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0x0A,0x0B,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6, }; // 背景サンプルのパターンネームデータ #1 const unsigned char bgsa1[] = { 0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0x0A,0x0B,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8, 0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0x0A,0x0B,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8, 0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0x0A,0x0B,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4, 0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0x0A,0x0B,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4, }; // 背景サンプルのパターンネームデータ #2 const unsigned char bgsa2[] = { 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x05,0x05,0x05,0x05,0x04,0x04,0x04,0x04,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x08,0x09,0x08,0x09,0x06,0x06,0x06,0x06,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, }; // パレットアニメーションのパターンネームデータ const unsigned char bgpam[] = { 0x00,0x00,0x00,0xA1,0xA2,0x00,0x00,0x00, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x15,0x15,0x0C,0x0D,0x0E,0x0C,0x12,0x12, 0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C, 0x18,0x19,0x1A,0xB1,0xB2,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x16,0x16,0x0C,0x0D,0x0E,0x0C,0x13,0x13, 0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C, 0x18,0x19,0x1A,0x18,0x19,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x17,0x17,0x0F,0x10,0x11,0x0F,0x14,0x14, 0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C, 0x18,0x19,0x1A,0x18,0x19,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x15,0x15,0x0F,0x10,0x11,0x0F,0x12,0x12, 0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C, }; // 画面奥の山脈表示用のパターンネームデータ const unsigned char bgmnt[] = { 0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64, 0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74, }; void NesMain(void) { // PPUの初期化 InitPPU(); // スプライトDMAの初期化 InitDMA((unsigned char *)0x700); // BGの初期化(最初はVRAMの状態が不定のため見えない領域の分も初期化) // 0 256 511 // 0 + 0x2000 --------+ 0x2400 --------+ // | | | // 240 + 0x2800 --------+ 0x2C00 --------+ // | | | // 279 +----------------+----------------+ // Horizontal mirroring: 0x2000は$2400、0x2800は0x2C00がミラーアドレス // Vertical mirroring : 0x2000は$2800、0x2400は0x2C00がミラーアドレス FillBackground(0x20, 0x00, 0x00, 0); FillBackground(0x21, 0x00, 0x00, 0); FillBackground(0x22, 0x00, 0x00, 0); FillBackground(0x23, 0x00, 0x00, 0xC0); FillAttribute(0x23, 0xC0, 0x00, 0x40); // Vertical mirroringの場合、画面右上 //FillBackground(0x24, 0x00, 0x00, 0); //FillBackground(0x25, 0x00, 0x00, 0); //FillBackground(0x26, 0x00, 0x00, 0); //FillBackground(0x27, 0x00, 0x00, 0xC0); //FillAttribute(0x27, 0xC0, 0x00, 0x40); // Horizontal mirroringの場合、画面左下 FillBackground(0x28, 0x00, 0x00, 0); FillBackground(0x29, 0x00, 0x00, 0); FillBackground(0x2A, 0x00, 0x00, 0); FillBackground(0x2B, 0x00, 0x00, 0xC0); FillAttribute(0x2B, 0xC0, 0x00, 0x40); //// 文字列の設定:ASCIIコードに合わせてキャラクタデータにフォントを配置しておくこと SetBackground(0x20, 0x20, message, sizeof(message)-1); //// 背景設定 SetBackground(0x20, 0x40, bgsa0, 0x40); SetBackground(0x20, 0x80, bgsa1, 0x80); SetBackground(0x21, 0x00, bgsa2, 0x80); //// パレットアニメーションのサンプル SetBackground(0x21, 0x80, bgpam, 0x80); //// 一番奥の山々 SetBackground(0x22, 0x40, bgmnt, 0x40); //// 奥から手前へのパレットアニメーションの床 FillBackground(0x22, 0x80, 0x60, 0x20); FillBackground(0x22, 0xA0, 0x70, 0x20); FillBackground(0x22, 0xC0, 0x80, 0x20); FillBackground(0x22, 0xE0, 0x90, 0x20); FillBackground(0x23, 0x00, 0xA0, 0x20); FillBackground(0x23, 0x20, 0xB0, 0x20); FillBackground(0x23, 0x40, 0xC0, 0x20); FillBackground(0x23, 0x60, 0xD0, 0x20); FillBackground(0x23, 0x80, 0xD0, 0x20); //// 属性設定 FillAttribute(0x23, 0xC0+ 0, 0xA0, 8); // 10 10 00 00 : 0xA0 FillAttribute(0x23, 0xC0+ 8, 0x5F, 8); // 01 01 11 11 : 0x5F FillAttribute(0x23, 0xC0+16, 0x55, 8); // 01 01 01 01 : 0x55 FillAttribute(0x23, 0xC0+32, 0xFA, 8); // 11 11 10 10 : 0xFA FillAttribute(0x23, 0xC0+48, 0x00, 8); // 00 00 00 00 : 0x00 //// 属性設定(ハシゴ) FillAttribute(0x23, 0xC0+ 5, 0xEC, 1); // 11 10 11 00 : 0xEC FillAttribute(0x23, 0xC0+13, 0xDF, 1); // 11 01 11 11 : 0xDF //// 属性設定(鉄板) FillAttribute(0x23, 0xC0+20, 0x5A, 1); // 01 01 10 10 : 0x5A //// パレット設定 SetPalette(palette[0], 0); SetPalette(palette[1], 1); //// スクロール値の設定 SetScroll(0, 0); // 描画開始(NMI割り込み無効) SetPPU(0x20, 0x1e); // メインループ while (1) { } } // NMI割り込み void NMIProc(void) { //// 未使用 }
実行結果:
- ミラーでない領域を全て初期化します。
- パターンネームテーブルと属性テーブル、パレットを設定し、PPUにBG表示を許可することで表示可能です。
- PPUの描画開始前であれば一気に書き込みできます。
- パターンネームは属性テーブルの指定で選択されるパレットが決まりますが、16x16キャラクタ単位の指定です。
- 上記のサンプルでは行っていませんが、描画開始後にキャラクタやパレット情報を書き換えることでアニメーションが可能です。ただしVBlank中に書き換え終わる必要があります。
以下は、VirtuaNESエミュレータ上で確認したPPUメモリ上の状態です。
- キャラクタパターン
BGとスプライトのキャラクタデータが連続して配置されています。
- ネームテーブル
startup.asmのiNESヘッダにて「vertical arrangement/horizontal mirroring」を指定しているため、横方向は同じパターンが繰り返ししているように見えています。
実際は左上の1画面分しか設定していません。
- パレット
BGとスプライトのパレットデータが連続して配置されています。
-
以下は、kihon.hに定義されている関数を使った8*8スプライト表示サンプルです。
キャラクタの移動とスプライトを組み合わせて表示する処理やアニメーション制御を含んでいるため、コード量が増えています。
#include <kihon.h> // C言語では「_」なしで、データサイズに合わせたextern宣言を行う。 // 256バイトのパレットデータを16色1組で使いたいため2次元配列として宣言している。 extern const unsigned char palette[16][16]; // アクション番号 enum { ACT_UP = 0, ACT_DOWN, ACT_LEFT, ACT_RIGHT, ACT_MAX }; // キャラクタ属性テーブル(4コマ×アクション番号) const unsigned char chrattr[] = { 0x00,0x00,0x40,0x00, // UP 0x00,0x00,0x40,0x00, // DOWN 0x40,0x40,0x40,0x40, // LEFT 0x00,0x00,0x00,0x00, // RIGHT }; // キャラクタパターンテーブル(8スプライト×4コマ×アクション番号) const unsigned char chrptrn[]= { 0x06,0x07,0x16,0x17,0x26,0x27,0x36,0x37, // UP 0 0x08,0x09,0x18,0x19,0x28,0x29,0x38,0x39, // UP 1 0x07,0x06,0x17,0x16,0x27,0x26,0x37,0x36, // UP 2 0x08,0x09,0x18,0x19,0x28,0x29,0x38,0x39, // UP 3 0x02,0x03,0x12,0x13,0x22,0x23,0x32,0x33, // DOWN 0 0x04,0x05,0x14,0x15,0x24,0x25,0x34,0x35, // DOWN 1 0x03,0x02,0x13,0x12,0x23,0x22,0x33,0x32, // DOWN 2 0x04,0x05,0x14,0x15,0x24,0x25,0x34,0x35, // DOWN 3 0x0B,0x0A,0x1B,0x1A,0x2B,0x2A,0x3B,0x3A, // LEFT 0 0x0D,0x0C,0x1D,0x1C,0x2D,0x2C,0x3D,0x3C, // LEFT 1 0x0F,0x0E,0x1F,0x1E,0x2F,0x2E,0x3F,0x3E, // LEFT 2 0x0D,0x0C,0x1D,0x1C,0x2D,0x2C,0x3D,0x3C, // LEFT 3 0x0A,0x0B,0x1A,0x1B,0x2A,0x2B,0x3A,0x3B, // RIGHT 0 0x0C,0x0D,0x1C,0x1D,0x2C,0x2D,0x3C,0x3D, // RIGHT 1 0x0E,0x0F,0x1E,0x1F,0x2E,0x2F,0x3E,0x3F, // RIGHT 2 0x0C,0x0D,0x1C,0x1D,0x2C,0x2D,0x3C,0x3D, // RIGHT 3 }; // 作業領域構造体 typedef struct Work { unsigned char frame; unsigned char cx; unsigned char cy; unsigned char flag; unsigned char top; unsigned char ch; unsigned char i; } Work; // プレイヤーキャラクタ情報構造体 typedef struct Player { unsigned char action; unsigned char type; unsigned char px; unsigned char py; } Player; extern Work zpw; #pragma zpsym ("zpw"); /* zpw is in the zeropage */ extern Player player[2]; #pragma zpsym ("player"); /* player is in the zeropage */ // メイン処理 void NesMain(void) { // 変数初期化 zpw.frame = zpw.cx = zpw.cy = zpw.flag = zpw.top = zpw.ch = zpw.i = 0; player[0].action = ACT_UP; player[0].type = 3; player[0].px = 120; player[0].py = 240 - 64; player[1].action = ACT_LEFT; player[1].type = 1; player[1].px = 255 - 32 + 1; player[1].py = 56; // PPUの初期化 InitPPU(); // スプライトDMAの初期化 InitDMA((unsigned char *)0x700); // BGの初期化 FillBackground(0x20, 0x00, 0x00, 0); FillBackground(0x21, 0x00, 0x00, 0); FillBackground(0x22, 0x00, 0x00, 0); FillBackground(0x23, 0x00, 0x00, 0xC0); FillAttribute(0x23, 0xC0, 0x00, 0x40); FillBackground(0x28, 0x00, 0x00, 0); FillBackground(0x29, 0x00, 0x00, 0); FillBackground(0x2A, 0x00, 0x00, 0); FillBackground(0x2B, 0x00, 0x00, 0xC0); FillAttribute(0x2B, 0xC0, 0x00, 0x40); //// パレット設定 SetPalette(palette[0], 0); SetPalette(palette[1], 1); //// スクロール値の設定 SetScroll(0, 0); // 描画開始(NMI割り込み無効、スプライトサイズ8x8、スプライト表示、BG非表示) SetPPU(0x08, 0x16); // メインループ while (1) { // VBlank処理開始待ち VBlank(); //// スプライト転送 SendDMA(0x07); //// コントローラ入力確認 CheckPad(); // フレーム更新 ++zpw.frame; //// コントローラ操作処理 if (ButtonDown(0, BTN_UP )) --player[0].py, player[0].action = ACT_UP; if (ButtonDown(0, BTN_DOWN )) ++player[0].py, player[0].action = ACT_DOWN; if (ButtonDown(0, BTN_LEFT )) --player[0].px, player[0].action = ACT_LEFT; if (ButtonDown(0, BTN_RIGHT)) ++player[0].px, player[0].action = ACT_RIGHT; if (ButtonPush(0, BTN_B)) player[0].type = (player[0].type + 1) & 0x03; // キャラクタ用パレット変更 if (ButtonDown(1, BTN_UP )) --player[1].py, player[1].action = ACT_UP; if (ButtonDown(1, BTN_DOWN )) ++player[1].py, player[1].action = ACT_DOWN; if (ButtonDown(1, BTN_LEFT )) --player[1].px, player[1].action = ACT_LEFT; if (ButtonDown(1, BTN_RIGHT)) ++player[1].px, player[1].action = ACT_RIGHT; if (ButtonPush(1, BTN_B)) player[1].type = (player[1].type + 1) & 0x03; // スプライトの設定 SetDMA((unsigned char *)0x700, 1, 0, 80, 0, 0); // ZERO BOMB //// player1 zpw.i = ((zpw.frame >> 3) & 3) + (player[0].action << 2); // キャラクタ属性の参照先 zpw.flag = chrattr[zpw.i] | player[0].type; // 反転の有無とパレット指定を合成 zpw.top = zpw.i << 3; // キャラクタパターンの参照先 for (zpw.i = 0; zpw.i < 16*2; zpw.i += 8) { // 横は2キャラなのでループせずに展開 zpw.cx = player[0].px; // 縦は4キャラ分のループ zpw.cy = player[0].py + zpw.i; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); } //// player2 zpw.i = ((zpw.frame >> 3) & 3) + (player[1].action << 2); zpw.flag = chrattr[zpw.i] | player[1].type; zpw.top = zpw.i << 3; for (zpw.i = 0; zpw.i < 16*2; zpw.i += 8) { zpw.cx = player[1].px; zpw.cy = player[1].py + zpw.i; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); } } } // NMI割り込み void NMIProc(void) { //// 未使用 }
実行結果:
- PPUにスプライトサイズと表示許可を設定後、CPUメモリのスプライト用バッファに書き出した内容をDMAを使ってPPUへ転送します。
- スプライトは、キャラクタパターンの表示数が64、横並びは8キャラクタまで表示されます。
- 8*8スプライトは小さめのキャラクタを多く表示する場合や、細かいパーツの入れ替えを行う場合に便利です。
- DMA転送は64キャラ分全部を転送するため、使わないキャラクタは背景色だけのパターンと画面の邪魔にならない位置を設定します。
サンプルでは0番目のパターンが背景色だけのため、位置も含めてゼロ初期化で済ませています。
次は、8*16スプライト表示サンプルです。
キャラクタパターンの定義数が変わるため、参照先の計算式が変わります。
#include <kihon.h> // C言語では「_」なしで、データサイズに合わせたextern宣言を行う。 // 256バイトのパレットデータを16色1組で使いたいため2次元配列として宣言している。 extern const unsigned char palette[16][16]; // アクション番号 enum { ACT_UP = 0, ACT_DOWN, ACT_LEFT, ACT_RIGHT, ACT_MAX }; // キャラクタ属性テーブル(4コマ×アクション番号) const unsigned char chrattr[] = { 0x00,0x00,0x40,0x00, // UP 0x00,0x00,0x40,0x00, // DOWN 0x40,0x40,0x40,0x40, // LEFT 0x00,0x00,0x00,0x00, // RIGHT }; // キャラクタパターンテーブル(4スプライト×4コマ×アクション番号) const unsigned char chrptrn[] = { 0x11,0x13,0x15,0x17, // UP 0 0x19,0x1B,0x1D,0x1F, // UP 1 0x13,0x11,0x17,0x15, // UP 2 0x19,0x1B,0x1D,0x1F, // UP 3 0x01,0x03,0x05,0x07, // DOWN 0 0x09,0x0B,0x0D,0x0F, // DOWN 1 0x03,0x01,0x07,0x05, // DOWN 2 0x09,0x0B,0x0D,0x0F, // DOWN 3 0x23,0x21,0x27,0x25, // LEFT 0 0x2B,0x29,0x2F,0x2D, // LEFT 1 0x33,0x31,0x37,0x35, // LEFT 2 0x2B,0x29,0x2F,0x2D, // LEFT 3 0x21,0x23,0x25,0x27, // RIGHT 0 0x29,0x2B,0x2D,0x2F, // RIGHT 1 0x31,0x33,0x35,0x37, // RIGHT 2 0x29,0x2B,0x2D,0x2F, // RIGHT 3 }; // 作業領域構造体 typedef struct Work { unsigned char frame; unsigned char cx; unsigned char cy; unsigned char flag; unsigned char top; unsigned char ch; unsigned char i; } Work; // プレイヤーキャラクタ情報構造体 typedef struct Player { unsigned char action; unsigned char type; unsigned char px; unsigned char py; } Player; extern Work zpw; #pragma zpsym ("zpw"); /* zpw is in the zeropage */ extern Player player[2]; #pragma zpsym ("player"); /* player is in the zeropage */ // メイン処理 void NesMain(void) { // 変数初期化 zpw.frame = zpw.cx = zpw.cy = zpw.flag = zpw.top = zpw.ch = zpw.i = 0; player[0].action = ACT_UP; player[0].type = 3; player[0].px = 120; player[0].py = 240 - 64; player[1].action = ACT_LEFT; player[1].type = 1; player[1].px = 255 - 32 + 1; player[1].py = 56; // PPUの初期化 InitPPU(); // スプライトの初期化 InitDMA((unsigned char *)0x700); // BGの初期化 FillBackground(0x20, 0x00, 0x00, 0); FillBackground(0x21, 0x00, 0x00, 0); FillBackground(0x22, 0x00, 0x00, 0); FillBackground(0x23, 0x00, 0x00, 0xC0); FillAttribute(0x23, 0xC0, 0x00, 0x40); FillBackground(0x28, 0x00, 0x00, 0); FillBackground(0x29, 0x00, 0x00, 0); FillBackground(0x2A, 0x00, 0x00, 0); FillBackground(0x2B, 0x00, 0x00, 0xC0); FillAttribute(0x2B, 0xC0, 0x00, 0x40); //// パレット設定 SetPalette(palette[0], 0); SetPalette(palette[1], 1); //// スクロール値の設定 SetScroll(0, 0); // 描画開始(NMI割り込み無効、スプライトサイズ8x16、スプライト表示、BG非表示) SetPPU(0x20, 0x16); // メインループ while (1) { // VBlank処理開始待ち VBlank(); //// スプライト転送 SendDMA(0x07); //// コントローラ入力確認 CheckPad(); // フレーム更新 ++zpw.frame; //// コントローラ操作処理 if (ButtonDown(0, BTN_UP )) --player[0].py, player[0].action = ACT_UP; if (ButtonDown(0, BTN_DOWN )) ++player[0].py, player[0].action = ACT_DOWN; if (ButtonDown(0, BTN_LEFT )) --player[0].px, player[0].action = ACT_LEFT; if (ButtonDown(0, BTN_RIGHT)) ++player[0].px, player[0].action = ACT_RIGHT; if (ButtonPush(0, BTN_B)) player[0].type = (player[0].type + 1) & 0x03; // キャラクタ用パレット変更 if (ButtonDown(1, BTN_UP )) --player[1].py, player[1].action = ACT_UP; if (ButtonDown(1, BTN_DOWN )) ++player[1].py, player[1].action = ACT_DOWN; if (ButtonDown(1, BTN_LEFT )) --player[1].px, player[1].action = ACT_LEFT; if (ButtonDown(1, BTN_RIGHT)) ++player[1].px, player[1].action = ACT_RIGHT; if (ButtonPush(1, BTN_B)) player[1].type = (player[1].type + 1) & 0x03; // スプライトの設定 SetDMA((unsigned char *)0x700, 1, 0, 80, 0, 0); // ZERO BOMB //// player1 zpw.i = ((zpw.frame >> 3) & 3) + (player[0].action << 2); // キャラクタ属性の参照先 zpw.flag = chrattr[zpw.i] | player[0].type; // 反転の有無とパレット指定を合成 zpw.top = zpw.i << 2; // キャラクタパターンの参照先 ////// 全部で4キャラなのでループせずに展開 zpw.cx = player[0].px; zpw.cy = player[0].py; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx -= 8; zpw.cy += 16; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top]; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); //// player2 zpw.i = ((zpw.frame >> 3) & 3) + (player[1].action << 2); zpw.flag = chrattr[zpw.i] | player[1].type; zpw.top = zpw.i << 2; zpw.cx = player[1].px; zpw.cy = player[1].py; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx -= 8; zpw.cy += 16; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top] + 0x40; SetDMA((unsigned char *)0x700, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); } } // NMI割り込み void NMIProc(void) { //// 未使用 }
- 大きめのキャラクタを表示したい場合に便利です。
- 最大表示数や横並びの制限は変わりません。
- BGのキャラクタデータも表示できるため、背景の一部を動かしたい場合にデータが節約できます。この場合はBGとスプライトを連結したファイルの方が扱いやすいです。
-
以降のサンプルでは処理落ちした場合でも定周期処理を行うため、NMI割り込みを使い、メインループと描画処理を切り離します。
また、TNS-HFCシリーズを使っての実機確認では垂直ミラー固定になるため、併せてiNESヘッダを変更します。
以下、NMI割り込みに関する箇所の抜粋です。
- startup.asm:垂直ミラーへ変更
; iNESヘッダ .segment "HEADER" .byte $4E, $45, $53, $1A ; "NES" Header .byte $02 ; PRG-BANKS 16K x 2 .byte $01 ; CHR-BANKS 8k x 1 .byte $01 ; 0:Horizontal/1:Vertical/3:Vertical & WRAM Mirror
- work.asm:ゼロページにメインループと割り込み間の同期用変数とスプライトバッファ切り替え用の変数を用意
; 構造体の定義 .struct Work sync .byte ; 同期状態値 dma .byte ; DMA転送を行う表示用スプライトバッファアドレスの上位バイト sprite_buf_adr .res 2 ; 書込用スプライトバッファのアドレス
- main.c:NMI割り込み処理及びメインスレッドとの同期処理を追加
#include <kihon.h> // スプライトのダブルバッファアドレスの上位バイト #define SPRITE_BUF_ADR 0x06 // 0x0600〜0x07FFの場合 // スプライトのバッファ切り替えマクロ // 表示用のバッファアドレスを書込用としてポインタ変数に設定 // DMA指定の値の最下位ビットのみ反転して表示用のバッファを設定 #define SPRITE_SWAP_BUF(dma, sprite_buf_adr) \ ((unsigned char *)&sprite_buf_adr)[0] = 0x00, \ ((unsigned char *)&sprite_buf_adr)[1] = dma, \ dma = (~dma & 1) + SPRITE_BUF_ADR; // 同期状態番号 enum { SYNC_NOTHING = 0, SYNC_READY, SYNC_BUSY }; // 256バイトのパレットデータを16色1組で使いたいため2次元配列として宣言 extern const unsigned char palette[16][16]; typedef struct { unsigned char sync; // 同期状態値 unsigned char dma; // DMA転送を行う表示用スプライトバッファアドレスの上位バイト unsigned char* sprite_buf_adr; // 書込用スプライトバッファのアドレス } Work; extern Work zpw; #pragma zpsym ("zpw"); /* zpw is in the zeropage */ // メイン処理 void NesMain(void) { // 変数初期化 zpw.sync = SYNC_NOTHING; zpw.dma = SPRITE_BUF_ADR; // PPUの初期化 InitPPU(); // スプライトの初期化 // 書込用のバッファを切り替えてダブルバッファ全体を初期化 SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); InitDMA(zpw.sprite_buf_adr); SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); InitDMA(zpw.sprite_buf_adr); // BGの初期化 // 0 256 511 // 0 + 0x2000 --------+ 0x2400 --------+ // | | | // 240 + 0x2800 --------+ 0x2C00 --------+ // | | | // 279 +----------------+----------------+ // Horizontal mirroring: 0x2000は$2400、0x2800は0x2C00がミラーアドレス // Vertical mirroring : 0x2000は$2800、0x2400は0x2C00がミラーアドレス FillBackground(0x20, 0x00, 0x00, 0); FillBackground(0x21, 0x00, 0x00, 0); FillBackground(0x22, 0x00, 0x00, 0); FillBackground(0x23, 0x00, 0x00, 0xC0); FillAttribute(0x23, 0xC0, 0x00, 0x40); // Vertical mirroringの場合、画面右上 FillBackground(0x24, 0x00, 0x00, 0); FillBackground(0x25, 0x00, 0x00, 0); FillBackground(0x26, 0x00, 0x00, 0); FillBackground(0x27, 0x00, 0x00, 0xC0); FillAttribute(0x27, 0xC0, 0x00, 0x40); // Horizontal mirroringの場合、画面左下 //FillBackground(0x28, 0x00, 0x00, 0); //FillBackground(0x29, 0x00, 0x00, 0); //FillBackground(0x2A, 0x00, 0x00, 0); //FillBackground(0x2B, 0x00, 0x00, 0xC0); //FillAttribute(0x2B, 0xC0, 0x00, 0x40); //// パレット設定 SetPalette(palette[0], 0); SetPalette(palette[1], 1); //// スクロール値の設定 SetScroll(0, 0); // 描画開始(NMI割り込み有効、スプライトサイズ8x16、スプライト表示、BG表示) SetPPU(0xA0, 0x1E); // スプライトの設定 SetDMA(zpw.sprite_buf_adr, 1, 8, 80, 0x1E, 0); // ZERO BOMB (x,y)=(8,80) // メインループ while (1) { //// VBlank処理同期開始設定 zpw.sync = SYNC_READY; //// VBlank処理完了待ち while (zpw.sync); ////// キャラクタ移動などのメイン処理 //// スプライトの設定 SetDMA(zpw.sprite_buf_adr, 1, 8, 80, 0x1E, 0); // ZERO BOMB (x,y)=(8,80) ////// キャラクタ表示向けスプライトの設定 SetDMA(zpw.sprite_buf_adr, 0, 8, 80, 0x03, 0); } } // NMI割り込み void NMIProc(void) { //// VBlank処理開始待ち if (zpw.sync != SYNC_READY) { //// 同期開始前のため、BGとスプライトの更新をしない。 //// 処理落ち発生中につき、必要最低限の処理を行って割り込み処理を終了 #ifdef USE_ZERO_BOMB //// スプライトの再転送 //// スプライトバッファの切り替え前のため、前回と同じデータを転送 //// 0番スプライト表示の検出を行う場合は必須 SendDMA(zpw.dma); //// スクロール位置の設定 //// 画面上部をスクロールさせる場合は、ここでスクロール値を変更 SetScroll(0, 0); //// 0番スプライト表示を検出 ZeroSprite(); //// スクロール位置の再設定 //// 画面下部をスクロールさせる場合は、ここでスクロール値を変更 SetScroll(0, 0); #endif return; } //// VBlank処理開始 zpw.sync = SYNC_BUSY; //// スプライトバッファの切り替え SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); //// スプライト転送 SendDMA(zpw.dma); //// BG書き換え *REGIST_ADR = 書き換え先アドレスの上位バイト; *REGIST_ADR = 書き換え先アドレスの下位バイト; *REGIST_GRA = 設定値; *REGIST_GRA = 設定値; : //// BG表示を一時停止 //// パレット書き換え時やBG書き換え箇所が多い場合は画面が乱れるため SetPPU(0xA0, 0x16); //// 背景色パレット書き換え *REGIST_ADR = 書き換え先アドレスの上位バイト; *REGIST_ADR = 書き換え先アドレスの下位バイト; *REGIST_GRA = 設定値; *REGIST_GRA = 設定値; : //// BG表示を再開 SetPPU(0xA0, 0x1E); //// スクロール位置の設定 //// BGやパレットを変更する場合は、必ず再設定すること //// 画面上部をスクロールさせる場合は、ここでスクロール値を変更 SetScroll(0, 0); #ifdef USE_ZERO_BOMB //// 0番スプライト表示を検出 //// PPU描画中にCPUで監視するため、描画以外の処理時間を圧迫しないよう注意 //// 表示を検出できない場合は無限ループになります! //// 画面の下の方でスクロール位置を変化させる場合は、処理落ちさせずにメインループ中で表示待ちすること ZeroSprite(); //// スクロール位置の再設定 //// 画面下部をスクロールさせる場合は、ここでスクロール値を変更 SetScroll(0, 0); #endif //// VBlank処理完了 zpw.sync = SYNC_NOTHING; }
- 1フレーム分の処理が完了する前にNMI割り込みが発生した場合を処理落ち中と判断し、前回表示と同じスプライトとBGスクロール値設定といった再表示処理を行います。
- グラフィック描画サンプルでは0番スプライト表示検出で部分スクロールを行うのですが、実機では処理落ち中でもスプライトのDMA転送を行わないと0番スプライトの検知ができず部分スクロールができません。
IRQ割り込みで任意の走査線が確認できるマッパーであれば楽ができるのですが…
-
NMI割り込みの実装例と、パレットアニメーションや部分スクロールを合わせたサンプルです。
#include <kihon.h> // スプライトのダブルバッファアドレスの上位バイト #define SPRITE_BUF_ADR 0x06 // スプライトのバッファ切り替えマクロ #define SPRITE_SWAP_BUF(dma, sprite_buf_adr) \ ((unsigned char *)&sprite_buf_adr)[0] = 0x00, \ ((unsigned char *)&sprite_buf_adr)[1] = dma, \ dma = (~dma & 1) + SPRITE_BUF_ADR; // 表示する木キャラクタの数 #define TREE_MAX 4 // C言語では「_」なしで、データサイズに合わせたextern宣言を行う。 // 256バイトのパレットデータを16色1組で使いたいため2次元配列として宣言している。 extern const unsigned char palette[16][16]; // アクション番号 enum { ACT_UP, ACT_DOWN, ACT_LEFT, ACT_RIGHT, ACT_MAX }; // 同期状態番号 enum { SYNC_NOTHING = 0, SYNC_READY, SYNC_BUSY }; // キャラクタ属性テーブル(4コマ×アクション番号) const unsigned char chrattr[] = { 0x00,0x00,0x40,0x00, // UP 0x00,0x00,0x40,0x00, // DOWN 0x40,0x40,0x40,0x40, // LEFT 0x00,0x00,0x00,0x00, // RIGHT }; // キャラクタパターンテーブル(4スプライト×4コマ×アクション番号) const unsigned char chrptrn[] = { 0x11,0x13,0x15,0x17, // UP 0 0x19,0x1B,0x1D,0x1F, // UP 1 0x13,0x11,0x17,0x15, // UP 2 0x19,0x1B,0x1D,0x1F, // UP 3 0x01,0x03,0x05,0x07, // DOWN 0 0x09,0x0B,0x0D,0x0F, // DOWN 1 0x03,0x01,0x07,0x05, // DOWN 2 0x09,0x0B,0x0D,0x0F, // DOWN 3 0x23,0x21,0x27,0x25, // LEFT 0 0x2B,0x29,0x2F,0x2D, // LEFT 1 0x33,0x31,0x37,0x35, // LEFT 2 0x2B,0x29,0x2F,0x2D, // LEFT 3 0x21,0x23,0x25,0x27, // RIGHT 0 0x29,0x2B,0x2D,0x2F, // RIGHT 1 0x31,0x33,0x35,0x37, // RIGHT 2 0x29,0x2B,0x2D,0x2F, // RIGHT 3 }; // 木キャラクターパターンテーブル const unsigned char treeptrn[] = { 0x3B,0x00,0x00,0x00, 0x39,0x00,0x00,0x00, 0x3F,0x3D,0x3B,0x00, 0x7D,0x7F,0x79,0x7B, }; // 木キャラクタ構成テーブル const unsigned char treepstn[] = { // dx0,dy0, dx1,dy1, dx2,dy2, dx3,dy3 -4, -8, 0, 0, 0, 0, 0, 0, -4, -8, 0, 0, 0, 0, 0, 0, -1, -8, -8, -8, -4,-18, 0, 0, -1, -8, -8, -8, -4,-20, 0, 0, 0, -8, -8, -8, -4,-22, 0, 0, 0, -8, -8, -8, -4,-24, 0, 0, -7, -8, -1, -8, -7,-18, -1,-18, -7, -8, -1, -8, -7,-19, -1,-19, -8, -8, -1, -8, -8,-20, -1,-20, -8, -8, -1, -8, -8,-21, -1,-21, -8, -8, 0, -8, -8,-22, 0,-22, -8, -8, 0, -8, -8,-23, 0,-23, -8, -8, 0, -8, -8,-24, 0,-24, }; // 木キャラクター配置テーブル const unsigned char treeargm[] = { // sx0, sx1, sx2, sx3, sx4, sx5, sx6, sx7, 127 -19, 127 -14, 127 -8, 127 -3, 127 +3, 127 +8, 127 +14, 127 +19, 127 -26, 127 -19, 127 -11, 127 -4, 127 +4, 127 +11, 127 +19, 127 +26, 127 -35, 127 -25, 127 -15, 127 -5, 127 +5, 127 +15, 127 +25, 127 +35, 127 -44, 127 -31, 127 -19, 127 -6, 127 +6, 127 +19, 127 +31, 127 +44, 127 -52, 127 -37, 127 -22, 127 -7, 127 +7, 127 +22, 127 +37, 127 +52, 127 -60, 127 -42, 127 -25, 127 -8, 127 +8, 127 +25, 127 +42, 127 +60, 127 -69, 127 -49, 127 -29, 127 -10, 127 +10, 127 +29, 127 +49, 127 +69, 127 -77, 127 -55, 127 -33, 127 -11, 127 +11, 127 +33, 127 +55, 127 +77, 127 -85, 127 -60, 127 -36, 127 -12, 127 +12, 127 +36, 127 +60, 127 +85, 127 -94, 127 -67, 127 -40, 127 -13, 127 +13, 127 +40, 127 +67, 127 +94, 127-102, 127 -73, 127 -44, 127 -15, 127 +15, 127 +44, 127 +73, 127+102, 127-110, 127 -78, 127 -47, 127 -16, 127 +16, 127 +47, 127 +78, 127+110, 127-119, 127 -85, 127 -51, 127 -17, 127 +17, 127 +51, 127 +85, 127+119, }; // 木キャラクター奥行き配置テーブル const unsigned char treedepth[] = { // chr, sz, 0, 0+152, 1, 1+152, 2, 3+152, 2, 6+152, 2, 10+152, 2, 15+152, 3, 21+152, 3, 28+152, 3, 36+152, 3, 45+152, 3, 55+152, 3, 66+152, 3, 78+152, }; // 文字列表示用のパターンネームデータ const unsigned char message[] = "GRAPHICS TEST"; // 背景書き換えマーキー文字列表示用のパターンネームデータ const unsigned char marquee[] = "RUN&RUN!SUN&RUN!"; // 背景サンプルのパターンネームデータ #0 const unsigned char bgsa0[] = { 0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0x0A,0x0B,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6,0xA5,0xA6, 0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0x0A,0x0B,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6,0xB5,0xB6, }; // 背景サンプルのパターンネームデータ #1 const unsigned char bgsa1[] = { 0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0x0A,0x0B,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8,0xA7,0xA8, 0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0x0A,0x0B,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8,0xB7,0xB8, 0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0x0A,0x0B,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4,0xA3,0xA4, 0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0x0A,0x0B,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4,0xB3,0xB4, }; // 背景サンプルのパターンネームデータ #2 const unsigned char bgsa2[] = { 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x05,0x05,0x05,0x05,0x04,0x04,0x04,0x04,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x08,0x09,0x08,0x09,0x06,0x06,0x06,0x06,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02,0x02, 0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06,0x06, }; // パレットアニメーションのパターンネームデータ const unsigned char bgpam[] = { 0x00,0x00,0x00,0xA1,0xA2,0x00,0x00,0x00, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x15,0x15,0x0C,0x0D,0x0E,0x0C,0x12,0x12, 0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C, 0x18,0x19,0x1A,0xB1,0xB2,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x16,0x16,0x0C,0x0D,0x0E,0x0C,0x13,0x13, 0x75,0x76,0x77,0x78,0x79,0x7A,0x7B,0x7C, 0x18,0x19,0x1A,0x18,0x19,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x17,0x17,0x0F,0x10,0x11,0x0F,0x14,0x14, 0x85,0x86,0x87,0x88,0x89,0x8A,0x8B,0x8C, 0x18,0x19,0x1A,0x18,0x19,0x1A,0x18,0x19, 0x1B,0x1C,0x1D,0x1B,0x1C,0x1D,0x1B,0x1C, 0x15,0x15,0x0F,0x10,0x11,0x0F,0x12,0x12, 0x95,0x96,0x97,0x98,0x99,0x9A,0x9B,0x9C, }; // 画面奥の山脈表示用のパターンネームデータ const unsigned char bgmnt[] = { 0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64,0x61,0x62,0x63,0x64, 0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74,0x71,0x72,0x73,0x74, }; // 背景単色パレットアニメーション用カラーテーブル const unsigned char backcolors[] = { 0x01,0x11,0x21,0x31,0x32,0x33,0x34,0x35, 0x25,0x15,0x05,0x04,0x03,0x02,0x01,0x1d, }; // 作業領域構造体 typedef struct Work { unsigned char sync; unsigned char dma; unsigned char frame; unsigned char bg_x; unsigned char bg_y; unsigned char backdrop_col; unsigned char palette_buf[4]; unsigned char* sprite_buf_adr; unsigned char cx; unsigned char cy; unsigned char flag; unsigned char top; unsigned char ch; unsigned char i; unsigned char j; } Work; // プレイヤーキャラクタ情報構造体 typedef struct Player { unsigned char action; unsigned char type; unsigned char px; unsigned char py; } Player; // 木キャラクタ情報構造体 typedef struct Tree { unsigned char pz; unsigned char px; } Tree; extern Work zpw; #pragma zpsym ("zpw"); /* zpw is in the zeropage */ extern Player player[2]; #pragma zpsym ("player"); /* player is in the zeropage */ extern Tree tree[4]; #pragma zpsym ("tree"); /* tree is in the zeropage */ // メイン処理 void NesMain(void) { // 変数初期化 zpw.frame = zpw.backdrop_col = zpw.bg_x = zpw.bg_y = zpw.cx = zpw.cy = zpw.flag = zpw.top = zpw.ch = zpw.i = zpw.j = 0; zpw.sync = SYNC_NOTHING; zpw.dma = SPRITE_BUF_ADR; //// パレットアニメーション用バッファの初期値設定 zpw.palette_buf[0] = backcolors[0]; zpw.palette_buf[1] = palette[0][1]; zpw.palette_buf[2] = palette[0][2]; zpw.palette_buf[3] = palette[0][3]; player[0].action = ACT_UP; player[0].type = 3; player[0].px = 120; player[0].py = 240 - 64; player[1].action = ACT_LEFT; player[1].type = 1; player[1].px = 255 - 32 + 1; player[1].py = 48; tree[0].pz = -4; tree[0].px = 7; tree[1].pz = -12; tree[1].px = 3; tree[2].pz = -6; tree[2].px = 5; tree[3].pz = -10; tree[3].px = 0; // PPUの初期化 InitPPU(); // スプライトの初期化 SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); InitDMA(zpw.sprite_buf_adr); SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); InitDMA(zpw.sprite_buf_adr); // BGの初期化 // 0 256 511 // 0 + 0x2000 --------+ 0x2400 --------+ // | | | // 240 + 0x2800 --------+ 0x2C00 --------+ // | | | // 279 +----------------+----------------+ // Horizontal mirroring: 0x2000は$2400、0x2800は0x2C00がミラーアドレス // Vertical mirroring : 0x2000は$2800、0x2400は0x2C00がミラーアドレス FillBackground(0x20, 0x00, 0x00, 0); FillBackground(0x21, 0x00, 0x00, 0); FillBackground(0x22, 0x00, 0x00, 0); FillBackground(0x23, 0x00, 0x00, 0xC0); FillAttribute(0x23, 0xC0, 0x00, 0x40); // Vertical mirroringの場合、画面右上 FillBackground(0x24, 0x00, 0x00, 0); FillBackground(0x25, 0x00, 0x00, 0); FillBackground(0x26, 0x00, 0x00, 0); FillBackground(0x27, 0x00, 0x00, 0xC0); FillAttribute(0x27, 0xC0, 0x00, 0x40); // Horizontal mirroringの場合、画面左下 //FillBackground(0x28, 0x00, 0x00, 0); //FillBackground(0x29, 0x00, 0x00, 0); //FillBackground(0x2A, 0x00, 0x00, 0); //FillBackground(0x2B, 0x00, 0x00, 0xC0); //FillAttribute(0x2B, 0xC0, 0x00, 0x40); //// 文字列の設定:ASCIIコードに合わせてキャラクタデータにフォントを配置しておくこと SetBackground(0x20, 0x29, message, sizeof(message)-1); SetBackground(0x24, 0x29, message, sizeof(message)-1); //// 背景設定のサンプル SetBackground(0x20, 0x40, bgsa0, 0x40); SetBackground(0x20, 0x80, bgsa1, 0x80); SetBackground(0x21, 0x00, bgsa2, 0x80); SetBackground(0x24, 0x40, bgsa0, 0x40); SetBackground(0x24, 0x80, bgsa1, 0x80); SetBackground(0x25, 0x00, bgsa2, 0x80); //// パレットアニメーションのサンプル SetBackground(0x21, 0x80, bgpam, 0x80); //// 一番奥の山々 SetBackground(0x22, 0x40, bgmnt, 0x40); //// 奥から手前へのパレットアニメーションの床 FillBackground(0x22, 0x80, 0x60, 0x20); FillBackground(0x22, 0xA0, 0x70, 0x20); FillBackground(0x22, 0xC0, 0x80, 0x20); FillBackground(0x22, 0xE0, 0x90, 0x20); FillBackground(0x23, 0x00, 0xA0, 0x20); FillBackground(0x23, 0x20, 0xB0, 0x20); FillBackground(0x23, 0x40, 0xC0, 0x20); FillBackground(0x23, 0x60, 0xD0, 0x20); FillBackground(0x23, 0x80, 0xD0, 0x20); //// 属性設定 FillAttribute(0x23, 0xC0+ 0, 0xA0, 8); // 10 10 00 00 : 0xA0 FillAttribute(0x23, 0xC0+ 8, 0x5F, 8); // 01 01 11 11 : 0x5F FillAttribute(0x23, 0xC0+16, 0x55, 8); // 01 01 01 01 : 0x55 FillAttribute(0x23, 0xC0+32, 0xFA, 8); // 11 11 10 10 : 0xFA FillAttribute(0x23, 0xC0+48, 0x00, 8); // 00 00 00 00 : 0x00 FillAttribute(0x27, 0xC0+ 0, 0xA0, 8); // 10 10 00 00 : 0xA0 FillAttribute(0x27, 0xC0+ 8, 0x5F, 8); // 01 01 11 11 : 0x5F FillAttribute(0x27, 0xC0+16, 0x55, 8); // 01 01 01 01 : 0x55 FillAttribute(0x27, 0xC0+32, 0xFA, 8); // 11 11 10 10 : 0xFA FillAttribute(0x27, 0xC0+48, 0x00, 8); // 00 00 00 00 : 0x00 //// 属性設定(ハシゴ) FillAttribute(0x23, 0xC0+ 5, 0xEC, 1); // 11 10 11 00 : 0xEC FillAttribute(0x23, 0xC0+13, 0xDF, 1); // 11 01 11 11 : 0xDF FillAttribute(0x27, 0xC0+ 5, 0xEC, 1); // 11 10 11 00 : 0xEC FillAttribute(0x27, 0xC0+13, 0xDF, 1); // 11 01 11 11 : 0xDF //// 属性設定(鉄板) FillAttribute(0x23, 0xC0+20, 0x5A, 1); // 01 01 10 10 : 0x5A FillAttribute(0x27, 0xC0+20, 0x5A, 1); // 01 01 10 10 : 0x5A //// パレット設定 SetPalette(palette[0], 0); SetPalette(palette[1], 1); //// スクロール値の設定 SetScroll(zpw.bg_x, zpw.bg_y); // 描画開始(NMI割り込み有効、スプライトサイズ8x16、スプライト表示、BG表示) SetPPU(0xA0, 0x1E); // スプライトの設定 SetDMA(zpw.sprite_buf_adr, 1, 8, 80, 0x1E, 0); // ZERO BOMB // メインループ while (1) { //// VBlank処理完了待ち zpw.sync = SYNC_READY; while (zpw.sync); //// フレーム更新 ++zpw.frame; //// 画面上部スクロール位置変更 --zpw.bg_x; //// コントローラ入力確認 CheckPad(); if ((zpw.frame & 0x7f) == 0) { //// パレットアニメーション(単色変化) zpw.backdrop_col = (zpw.backdrop_col + 1) & 0x0f; zpw.palette_buf[0] = backcolors[zpw.backdrop_col]; } if ((zpw.frame & 7) == 0) { //// パレットアニメーション(3色ローテーション) zpw.i = zpw.palette_buf[1]; zpw.palette_buf[1] = zpw.palette_buf[2]; zpw.palette_buf[2] = zpw.palette_buf[3]; zpw.palette_buf[3] = zpw.i; //// 木キャラクタの移動 for (zpw.i = 0; zpw.i < TREE_MAX; ++zpw.i) { ++tree[zpw.i].pz; if (tree[zpw.i].pz == 13) { //// 初期位置設定 tree[zpw.i].px = (tree[zpw.i].px + 3) & 7; tree[zpw.i].pz = 0; } } } //// コントローラ操作処理 if (ButtonDown(0, BTN_UP )) --player[0].py, player[0].action = ACT_UP; if (ButtonDown(0, BTN_DOWN )) ++player[0].py, player[0].action = ACT_DOWN; if (ButtonDown(0, BTN_LEFT )) --player[0].px, player[0].action = ACT_LEFT; if (ButtonDown(0, BTN_RIGHT)) ++player[0].px, player[0].action = ACT_RIGHT; if (ButtonPush(0, BTN_B)) player[0].type = (player[0].type + 1) & 0x03; if (ButtonDown(1, BTN_UP )) --player[1].py, player[1].action = ACT_UP; if (ButtonDown(1, BTN_DOWN )) ++player[1].py, player[1].action = ACT_DOWN; if (ButtonDown(1, BTN_LEFT )) --player[1].px, player[1].action = ACT_LEFT; if (ButtonDown(1, BTN_RIGHT)) ++player[1].px, player[1].action = ACT_RIGHT; if (ButtonPush(1, BTN_B)) player[1].type = (player[1].type + 1) & 0x03; // スプライトの設定 SetDMA(zpw.sprite_buf_adr, 1, 8, 80, 0x1E, 0); // ZERO BOMB //// player1 zpw.i = ((zpw.frame >> 3) & 3) + (player[0].action << 2); // キャラクタ属性の参照先 zpw.flag = chrattr[zpw.i] | player[0].type; // 反転の有無とパレット指定を合成 zpw.top = zpw.i << 2; // キャラクタパターンの参照先 ////// 全部で4キャラなのでループせずに展開 zpw.cx = player[0].px; zpw.cy = player[0].py; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx -= 8; zpw.cy += 16; zpw.ch = chrptrn[zpw.top]; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top]; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); //// player2 zpw.i = ((zpw.frame >> 3) & 3) + (player[1].action << 2); zpw.flag = chrattr[zpw.i] | player[1].type; zpw.top = zpw.i << 2; zpw.cx = player[1].px; zpw.cy = player[1].py; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx -= 8; zpw.cy += 16; zpw.ch = chrptrn[zpw.top] + 0x40; ++zpw.top; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); zpw.cx += 8; zpw.ch = chrptrn[zpw.top] + 0x40; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx, zpw.cy, zpw.ch, zpw.flag); //// 木キャラ for (zpw.top = 0; zpw.top < TREE_MAX; ++zpw.top) { if (tree[zpw.top].pz < 13) { //// 奥行き情報取得 zpw.i = tree[zpw.top].pz << 1; zpw.ch = treedepth[zpw.i] << 2; ++zpw.i; zpw.cy = treedepth[zpw.i]; //// 配置情報取得 zpw.i = tree[zpw.top].pz << 3; zpw.j = zpw.i + tree[zpw.top].px; zpw.cx = treeargm[zpw.j]; //// スプライト設定 zpw.j = zpw.i + 1; SetDMA(zpw.sprite_buf_adr, 0, zpw.cx + treepstn[zpw.i], zpw.cy + treepstn[zpw.j], treeptrn[zpw.ch], 2); zpw.i += 2, zpw.j += 2, ++zpw.ch; if (treeptrn[zpw.ch]) { SetDMA(zpw.sprite_buf_adr, 0, zpw.cx + treepstn[zpw.i], zpw.cy + treepstn[zpw.j], treeptrn[zpw.ch], 2); } else { SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); } zpw.i += 2, zpw.j += 2, ++zpw.ch; if (treeptrn[zpw.ch]) { SetDMA(zpw.sprite_buf_adr, 0, zpw.cx + treepstn[zpw.i], zpw.cy + treepstn[zpw.j], treeptrn[zpw.ch], 2); } else { SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); } zpw.i += 2, zpw.j += 2, ++zpw.ch; if (treeptrn[zpw.ch]) { SetDMA(zpw.sprite_buf_adr, 0, zpw.cx + treepstn[zpw.i], zpw.cy + treepstn[zpw.j], treeptrn[zpw.ch], 2); } else { SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); } } else { //// 表示範囲外は空キャラクタを設定 SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); SetDMA(zpw.sprite_buf_adr, 0, 0, 0, 0, 0); } } } } // NMI割り込み void NMIProc(void) { //// VBlank処理開始待ち if (zpw.sync != SYNC_READY) { //// 同期開始前のため、BGとスプライトの更新をしない。 //// 処理落ち発生中につき、必要最低限の処理を行って割り込み処理を終了する。 //// スプライト転送 SendDMA(zpw.dma); //// スクロール位置の設定 SetScroll(zpw.bg_x, zpw.bg_y); //// 0番スプライト表示を検出 ZeroSprite(); //// スクロール位置の再設定 SetScroll(0, 0); return; } //// VBlank処理開始 zpw.sync = SYNC_BUSY; //// スプライトバッファの切り替え SPRITE_SWAP_BUF(zpw.dma, zpw.sprite_buf_adr); //// スプライト転送 SendDMA(zpw.dma); //// マーキー文字列の設定(BG書き換え) zpw.i = (zpw.frame >> 4) & 0x0F; *REGIST_ADR = 0x22; *REGIST_ADR = 0x28; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; zpw.i = (zpw.i + 1) & 0x0F; *REGIST_GRA = marquee[zpw.i]; //// パレット書き換え時は画面が乱れるのでBG表示を一時停止 SetPPU(0xA0, 0x16); //// 背景色パレット書き換え *REGIST_ADR = 0x3f; *REGIST_ADR = 0x00; *REGIST_GRA = zpw.palette_buf[0]; *REGIST_GRA = zpw.palette_buf[1]; *REGIST_GRA = zpw.palette_buf[2]; *REGIST_GRA = zpw.palette_buf[3]; //// BG表示を再開 SetPPU(0xA0, 0x1E); //// スクロール位置の設定 SetScroll(zpw.bg_x, zpw.bg_y); //// 0番スプライト表示を検出 //// PPU描画中にCPUで監視するため、描画以外の処理時間を圧迫しないよう注意。 //// 表示を検出できない場合は無限ループになります! //// 画面の下の方でスクロール位置を変化させる場合は、 //// 処理落ちさせずにメインループのVBlank待ちの前で表示待ちします。 ZeroSprite(); //// スクロール位置の再設定 SetScroll(0, 0); //// VBlank処理完了 zpw.sync = SYNC_NOTHING; }
実行結果:
- 上段は部分スクロール、中段はパレットアニメーション、下段は疑似3Dのサンプルです。
- 部分スクロールは、0番スプライトの描画位置でスクロール値を変化させているだけです。
- パレットアニメーションは、BG用パレットの最初の1組で背景色の単色変化と3色パレットのローテートを行っています。
単色変化で昼夜の表現、3色パレットローテートで輝きや動きを表現しています。 - 疑似3Dは、パレットアニメーションによる地面の移動に合わせて、木のキャラクタの疑似拡大表示を行っています。
まともに3D演算を行うわけにはいかないため、全表示位置に対するキャラクタの組み合わせと表示位置の配列情報を用意しています。 - 今回のサンプルでは、木のキャラクタを3本表示した時点で処理落ちしました。描画は割り込みで行いますので秒間60フレームですが、処理は30フレームです。
-
上記を含めたサンプルコードをまとめたものはこちら。
- nesapp-background.zip「背景設定」
- nesapp-sprite8x8.zip「8x8スプライト設定」
- nesapp-sprite8x16.zip「8x16スプライト設定」
- nesapp-nmitest.zip「NMI割り込み」
- nesapp-graphics.zip「グラフィックス サンプル」
cc65@wiki様の「kihon.h」の定義を再び改造したバージョンを使用しています。
- nesapp-libkihon3.zip「ライブラリ本体」