スプライトエンジンを使ってみる

SGDKでスプライトを扱うライブラリには、vdp_spr.hとsprite_eng.hの2種類があります。ここでは、sprite_eng.hで定義されているスプライトエンジンを使ってみます。

■ 単純なスプライトの表示

まずはシンプルに1枚のスプライトを表示する例です。

スプライトとして表示する画像はこちらです

128×128のPNG画像上に、16×16サイズのドット絵を配置してあります。(こちらの画像をお借りしました / http://www.geocities.jp/tkool_yuta/

この画像をリソースとして利用するための定義ファイルの内容はこちらです。resフォルダにresource.resというファイル名で置いてあります。

SPRITE mychar "mychar.png" 2 2 NONE 0

ファイル名の後の2 2ですが、どうやら、8ドットを1タイルとした上下左右のタイル数のようです。16×16ドットのキャラ絵なので、8×8ドットが2個分ということのようですね。

プログラム本体はこちら。

#include 
#include "resource.h"

int main()
{
	SYS_disableInts();

	// スプライトエンジンの初期化
	SPR_init(80, 0, 0);

	// パレット設定
	VDP_setPalette(PAL1, mychar.palette->data);

	// スプライト登録
	Sprite* spr = SPR_addSprite(&mychar, 0, 0, TILE_ATTR(PAL1, FALSE, FALSE, FALSE));	

	// 表示座標を設定
	SPR_setPosition(spr, 160, 100);
	// 実際のスプライトに反映させる
	SPR_update();

	SYS_enableInts();

	while(1)
	{
		VDP_waitVSync();
	}

	return 0;
}

スプライトエンジンの初期化にはSPR_init()を呼び出します。

void SPR_init(u16 maxSprite, u16 vramSize, u16 unpackBufferSize);

maxSpriteは、使用するスプライトの個数です。スプライトエンジンが管理するメモリ領域と関係しているので、数を増やすとRAMの使用量が増えることになります。 最大値は127で、0を指定するとデフォルトの40個となります。

vramSizeは、VRAM(キャラクタ定義領域)に確保するサイズ指定です。0を指定すると384タイル分の領域が確保されるそうです・・・が、正直、どこに影響するのかよくわかりません。

unpackBufferSizeは、もっとよくわかりません。unpack状態のタイル(キャラクタ定義データ)を保持しておくためのメモリ領域のサイズ指定のようです。これもよくわかってません。

あとは、SPR_addSprite()でスプライトの定義をして、SPR_setPosition()で表示位置を指定です。 SPR_setPosition()を使って表示位置を指定しただけでは画面に表示されません。 SPR_update()を呼ぶことで、スプライトエンジンが管理している値を、次の画面更新処理のタイミングで実際のスプライトの位置に反映するように指示します。 つまり、スプライトエンジンが管理しているメモリ⇒更新用バッファ⇒スプライトレジスタという流れでスプライトの更新が行われるということになるようです。

これを実行した結果がこちら。

resouce.resで指定している画像の左上のキャラが表示されましたね。この時、メガドラ内ではどのようなタイルマップとスプライト定義になっているのかをエミュレータのデバッガでのぞいてみると・・・

タイルマップ内の0x420から4タイル分に割り当てられているようです(パレット番号の都合で白黒です)。また、スプライト設定は1番に16×16サイズで設定されていますね。0番は使われないのでしょうか?謎です。

ところで、元のPNGファイルでは、4方向それぞれに3パターンの絵が描いてあるのですが、SGDKのスプライトエンジンを使うと、メガドラのタイルマップには、現在表示している状態のタイルのみが定義されるようです。

これを3パターンアニメーションさせるには、ソースコードのwhile()箇所を次のように書き換えます。

	while(1)
	{
		SPR_update();
		VDP_waitVSync();
	}
	    
また、リソース定義ファイル(resource.res)も少しだけ書き換えます。
SPRITE mychar "mychar.png" 2 2 NONE 20

最後の数字の20は、20フレーム毎に、次の絵に変えるという設定のようです。3パターンの絵を、20フレーム毎に入れ替えて繰り返します。1だと1フレーム毎なのですが、これは毎フレームではなさそうです。かといって、0にするとアニメーションしなくなるようです、うーむ。

では、キャラクタの向きはというと、SPR_setAnim(spr, n)を使うようです。第二引数がPNG画像の上から数えた番号になります。

方向キーの入力に合わせて、SPR_setAnim(spr, n)のnを変えれば、向きを変える動きの出来上がりというわけですね。

■ アニメーションを切り替えについて

タイルの定義場所

タイルの定義は、先ほどエミュレータのVDPデバッガで確認した時は、タイルマップ内の0x420から4タイル分に割り当てられていました。これは変更可能なようで、

SPR_setVRAMTileIndex(spr, TILE_USERINDEX);

このようにすると、0x10からに割り当てられます。

TILE_USERINDEXは、vdp.hに定義されています。

#define TILE_SYSTEMINDEX        0x0000
#define TILE_SYSTEMLENGTH       16
#define TILE_SYSTEMLENGHT       TILE_SYSTEMLENGTH
#define TILE_USERINDEX          (TILE_SYSTEMINDEX + TILE_SYSTEMLENGTH)	

この他にも、定義可能なタイルの個数や、デフォルトで組み込まれるフォントの定義場所なども書かれているので、vdp.hに目を通しておくとよさそうです。

スプライトアニメーションの定義場所

resource.resでアニメーション間隔を設定してあれば、SPR_update()毎に自動的に表示を更新してくれます。この様子を、エミュレータのVDPデバッガで見てみると、使用しているタイルの個数は変わりません。つまり、スプライトエンジンのアニメーションの更新は、タイルの定義をごっそり書き換えるようです。このやり方は、タイル定義の領域を最小限に抑える代わりに、書き換えの時間が必要になるので、動作速度に影響が出てきます。

SGDKには動作速度を測るベンチマークソフトが入っていて、タイルを書き換える方式と、あらかじめタイルを全て書き込んでおいてスプライトが参照するタイル番号だけを書き換える方式では、目に見えてわかるくらいに後者の方が速く動作しています。

あらかじめタイルを展開しておくやり方ですが、自前でコードを書く必要があります。

まず、スプライトが参照するタイルの位置は自前で指定するようにします。タイルの指定は、SPR_setVRAMTileIndex()を使います。

タイル定義は、VDP_loadTileSet()を使って書き込みます。書き込む際に必要になるアニメーションの個数や、パターン数はPNG画像を変換した時にリソースデータ内に保存されているようです。これは、SPR_addSprite()の第一引数で指定しているデータに含まれています。これまで、SPR_addSprite()の第一引数は、PNG画像から変換したタイルデータやパレットデータとして扱ってきましたが、実際にはアニメーションに関する情報も含まれている、SpriteDefinition構造体になっています。

typedef struct
{
	Palette *palette;
	u16 numAnimation;
	Animation **animations;
	u16 maxNumTile;
	u16 maxNumSprite;
} SpriteDefinition;	

この定義内では更にAnimation構造体へのポインタ×2となっています。さあ複雑になってきたところで更にAnimation構造体をみると、次はAnimationFrame構造体が出てきたりします。もうこのあたりの定義はsprite_eng.hを眺めてみてくださいということで、実際に動くコードを示します。これもサンプルから引っ張ってきたものなので、自分でも漠然とした理解です。

#include 
#include "resource.h"

int main()
{
	SYS_disableInts();

	SPR_init(80, 0, 0);

	Sprite* spr = SPR_addSprite(&mychar, 0, 0, TILE_ATTR(PAL1, FALSE, FALSE, FALSE));
	SPR_setPosition(spr, 160, 100);
	SPR_setVRAMTileIndex(spr, TILE_USERINDEX);
	SPR_setAutoTileUpload(spr, FALSE);
	SPR_setAnim(spr, 0);

	int ind = TILE_USERINDEX;
	for(int i = 0; i < mychar.animations[0]->numFrame; i++)
	{
		TileSet* tileset = mychar.animations[0]->frames[i]->tileset;
		VDP_loadTileSet(tileset, ind, TRUE);
		ind += tileset->numTile;
	}

	VDP_setPalette(PAL1, mychar.palette->data);
	SPR_update();

	SYS_enableInts();

	while(1)
	{
		SPR_update();
		VDP_waitVSync();
	}

	return 0;
}

while文の中で、SPR_update()を実行すると、タイルの更新がされますが、ここでは自動更新して欲しくないので、SPR_setAutoTileUpload()を使って自動更新をしないようにしています。この状態でエミュレータのVDPデバッガをみると、3パターン×4タイルの合計12タイルが書き込まれていることが確認できます。

これだけではアニメーションしないので、while()のループ内で、SPR_setVRAMTileIndex()を使ってスプライトが参照するタイルのインデックスを更新するようにします。

#include 
#include "resource.h"

// 3で十分ですが大雑把に16
static u16 tileIndexes[16];

int main()
{
	SYS_disableInts();

	//- 略 - 

	int ind = TILE_USERINDEX;
	for(int i = 0; i < mychar.animations[0]->numFrame; i++)
	{
		TileSet* tileset = mychar.animations[0]->frames[i]->tileset;
		VDP_loadTileSet(tileset, ind, TRUE);
		// タイル番号を保存
		tileIndexes[i] = ind;
		ind += tileset->numTile;
	}

	//- 略 - 

	SYS_enableInts();

	int index = 0;
	while(1)
	{
		// スプライトが使用するタイル番号を設定
		SPR_setVRAMTileIndex(spr, tileIndexes[index]);
		// 次に備える
		index++;
		if (index == mychar.animations[0]->numFrame) index = 0;
		// スプライトレジスタへの反映を指示
		SPR_update();
		VDP_waitVSync();
	}

	return 0;
}				

このまま実行すると、秒間60フレームで全力アニメーションになってしまうので、SPR_setVRAMTileIndex()を呼び出す間隔を自前で処理する必要があるようです。ちょっと面倒ですね。必要な数値は、AnimationFrame構造体に入っているようなので、一度この辺でデータの中身を確認してみます。

#include 
#include "resource.h"

static u16 tileIndexes[16];

// va_startがうまく動かないので固定で...
void DebugMsg(const char* msg, int d)
{
	static int debugLine = 0;
	char msgBuf[64];

	sprintf(msgBuf, msg, d);
	VDP_drawText(msgBuf, 0, debugLine);
	debugLine++;
	if (debugLine == 24) debugLine = 0;
}

int main()
{
	SYS_disableInts();

	// - 略 -

	SPR_setAnim(spr, 0);

	DebugMsg("numAnimation:%d", mychar.numAnimation);
	DebugMsg("numFrame:%d", mychar.animations[0]->numFrame);

	int ind = TILE_USERINDEX;
	for(int i = 0; i < mychar.animations[0]->numFrame; i++)
	{
		DebugMsg("timer:%d", mychar.animations[0]->frames[i]->timer);

		TileSet* tileset = mychar.animations[0]->frames[i]->tileset;
		VDP_loadTileSet(tileset, ind, TRUE);
		tileIndexes[i] = ind;
		ind += tileset->numTile;
	}				

結果

3パターンが定義されていて、各パターンの表示フレーム数は20という設定になっているのがわかります。この数字を使ってSPR_setVRAMTileIndex()で更新すればよさそうですし、きっとどこかの構造体に現在のフレームカウントも入っていそうな気がします。(未調査)

numAnimation構造体のnumAnimationが8というのが気になりますが、どうやらこれは、

キャラクタを描いてあるPNGファイルの画像サイズが128×128になっているからのようです。ですので、PNG画像ファイルを128x64にするとnumAnimationは4になります。画像の横方向も無駄があるのですが、そこは自動的に判別してくれるようですね。

■ デカキャラ

ここまで、16×16のキャラクタで試してきましたが、より大きなキャラでも同様です。画像を用意してから、resource.resに定義します。(画像はこちらからお借りしました / http://park2.wakwak.com/~kuribo/dot/dot1.htm

SPRITE ikachan "char_giantsquid.png" 12 12 NONE 1

あとは、これまでのプログラムのmycharのところをikachanに変えるだけです。静止画だとわからないですが、動作させるとちゃんとアニメーションしてます。96×96ドットのキャラなので、32×32のスプライトを9個使っていますね。

■ 複数キャラクタ

キャラクタを複数定義・表示するには、resource.resに定義したい数だけ並べて、あとはSPR_addSprite()でスプライトを割り当てればOKです。使い方がわかるとすごく簡単ですね。

SPRITE mychar "mychar.png" 2 2 NONE 10
SPRITE ikachan "char_giantsquid.png" 12 12 NONE 8		
#include 
#include "resource.h"

int main()
{
	SYS_disableInts();

	SPR_init(80, 0, 0);

	Sprite* spr;
	spr = SPR_addSprite(&mychar, 0, 0, TILE_ATTR(PAL1, FALSE, FALSE, FALSE));
	SPR_setPosition(spr, 160, 80);
	SPR_setAnim(spr, 0);

	spr = SPR_addSprite(&ikachan, 0, 0, TILE_ATTR(PAL2, FALSE, FALSE, FALSE));
	SPR_setPosition(spr, 160, 100);
	SPR_setAnim(spr, 0);

	VDP_setPalette(PAL1, mychar.palette->data);
	VDP_setPalette(PAL2, ikachan.palette->data);

	SPR_update();
	SYS_enableInts();

	while(1)
	{
		SPR_update();
		VDP_waitVSync();
	}

	return 0;
}		

ついでなので、4体を同時に表示してみました。60fpsを割ってしまいますね。SPR_update()任せずに自前でタイル参照の更新をすれば速くなるはずです。

#include 
#include "resource.h"

int main()
{
	SYS_disableInts();

	SPR_init(80, 0, 0);

	Sprite* spr;
	int idx = 0;
	for (int i = 0; i < 4; i++)
	{
		spr = SPR_addSprite(&ikachan, 0, 0, TILE_ATTR(PAL2, FALSE, FALSE, FALSE));
		SPR_setVRAMTileIndex(spr, TILE_USERINDEX +idx);
		idx += ikachan.animations[0]->frames[i]->tileset->numTile;
		SPR_setPosition(spr, i * 32, i*32);
		SPR_setAnim(spr, i);
	}
	VDP_setPalette(PAL2, ikachan.palette->data);
	SPR_update();
	SYS_enableInts();

	while(1)
	{
		SPR_update();
		VDP_showFPS(0);
		VDP_waitVSync();
	}

	return 0;
}

スプライトエンジンを使うと楽ですよーと思えるのですが、ちょっと使い方を間違えるとあっさり暴走しますので、その辺はやっぱりゲーム機ならではのプログラミングですね。


上のページに戻る