スプライトダブラ

スプライトダブラとは、VCOUNT割り込み(ラスタ割り込み)でスプライト情報を書き換える方法です。この技術をGBAで用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代に限られたスプライトを有効利用するために使われていました。単純な例ですと以下のようにスプライト0を画面の左上に表示していたとします。赤線は現在の描画済みVCOUNTラインです。

1.png

その際にスプライト0が表示された後のVCOUNT割り込みでスプライト0の位置を変更すると、スプライト0を画面に2つ表示することが出来ます。

2.png

スプライト0〜スプライト127までに対して行えば256個のスプライトを表示することが出来ます。このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではVCOUNT割り込みの処理でOAMを書き換えることになります。

始めに

GBAではスプライトの情報を以下のような形で持っています。

   typedef struct {
           u16 attr0; // Y 座標などのデータ
           u16 attr1; // X 座標などのデータ
           u16 attr2; // タイル情報、パレット情報などのデータ
           u16 dummy;
   } OBJATTR;

方針

実装するにあたっては、割り込みの処理を工夫する必要があります。というのも割り込み処理はあまり時間のかかる処理を行うことが出来ません。ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。そこで事前に書き換え後のスプライト情報を用意しておいて、割り込みでそれらのデータをDMA転送させる方法を取ります。

基本的な構造

スプライトダブラで最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータをあらかじめ用意しておき、割り込みの際に少しづつデータをずらして上書きコピーしていけば表示することが出来ます。あらかじめ用意するOBJATTRはy軸に沿ってソートされている必要があります。

実装方式

前提として、y軸を4ライン毎に区切って、それらを一塊のブロックとして扱います。

まずは4ライン毎に区切られた各ブロックにそれぞれ幾つあるかカウントします。例えばBullet.idxCnt[0]は-7〜-5ラインに含まれる数。Bullet.idxCnt[1]は-4〜-1ラインに含まれる数といった形です。次に、512個分のOBJATTRに対してソートを行います。

//---------------------------------------------------------------------------
IWRAM_CODE void SprExec(void)
{
	SprInitItem();

	s32 i, cnt = 0; 
	ST_SPR_ITEM* pW[SPR_MAX_IDX_CNT];

	for(i=0; i<SPR_MAX_IDX_CNT; i++)
	{
		// 各VCOUNTでDMA転送するバッファ位置を格納
		Spr.idx[i] = cnt;

		// ソートで使うワークポインタを格納
		pW[i] = &Spr.item[cnt];

		cnt += Bullet.idxCnt[i];
	}

	// 弾情報ポインタを取得
	ST_BULLET_CHR* pS = (ST_BULLET_CHR*)&Bullet.chr;

	// 最初の弾情報を取得
	while(pS->is == FALSE)
	{
		pS++;
	}

	// ソート開始
	s32 max = Bullet.maxCnt;

	for(i=0; i<max; i++)
	{
		s32 x  = FIX2INT(pS->x);
		s32 y  = FIX2INT(pS->y);
		u32 y4 = (y + 8) / 4;

		pW[y4]->attr0 = OBJ_16_COLOR | OBJ_SQUARE | (y & 0x00ff);
		pW[y4]->attr1 = OBJ_SIZE(0)  | (x & 0x01ff);
		pW[y4]->attr2 = (1+Spr.chrNo);
		pW[y4]++;

		// 次の弾情報を取得
		do {
			pS++;

		} while(pS->is == FALSE);
	}

	// スプライトアニメーション
	Spr.chrNo++;
	Spr.chrNo &= 0x03;
}

4ライン毎に区切られた各ブロック数は決まっているので、ブロック先頭の開始ワークポインタを作ります。関数の後半ではソートされていない弾情報を頭から順番に見ていって、各ワークポインタを通して弾情報とスプライトを作成しています。

割り込みによるOAMのDMA転送

まずはVBlank割り込みでOAMの初期設定を行います。その後に割り込みを行うべきVCOUNTの値を設定します。

//---------------------------------------------------------------------------
IWRAM_CODE void SprVBlank(void)
{
	// 0クリア OAM 
	REG_DMA0SAD = (u32)&Spr.zero;
	REG_DMA0DAD = (u32)OAM;
	REG_DMA0CNT = (u32)128*2 | (DMA_SRC_FIXED | DMA_DST_INC | DMA32 | DMA_ENABLE);


	Spr.oamCnt = 0;
	Spr.vCnt = 0;

	REG_DISPSTAT = (REG_DISPSTAT & STAT_MASK) | LCDC_VCNT | VCOUNT(SprVmap[Spr.vCnt]);
}

注意点としては、n番目のブロックを転送するには、8ライン前のVCOUNT、つまり(n-1)*8ライン目で割り込みを発生させる必要があります。このタイミングで転送をスタートしておけば、実際にnブロック目が描写されるタイミング(n*8ライン目) にはOAMへのDMA転送が終了している計算になります。こちらでは-2分の安全マージンを取っています。実機では動作が異なるかもしれません。

// VCOUNT割り込みライン
s32 SprVmap[SPR_MAX_IDX_CNT+1] = 
{
	218,	// 0		218 = 228 - 8 - 4
	222,	// 1
	226,	// 2
	0,	// 3
	4,	// 4
	8,	// 5
	12,	// 6
	16,	// 7
	20,	// 8
	24,	// 9
	28,	// 10
	32,	// 11
	36,	// 12
	40,	// 13
	44,	// 14
	48,	// 15
	52,	// 16
	56,	// 17
	60,	// 18
	64,	// 19
	68,	// 20
	72,	// 21
	76,	// 22
	80,	// 23
	84,	// 24
	88,	// 25
	92,	// 26
	96,	// 27
	100,	// 28
	104,	// 29
	108,	// 30
	112,	// 31
	116,	// 32
	120,	// 33
	124,	// 34
	128,	// 35
	132,	// 36
	136,	// 37
	140,	// 38
	144,	// 39
	148,	// 40
	152,	// 41
	0,	// vblank待ち用
};

あとはVCOUNTでの割り込みで対応しているブロックのOBJATTRを転送していくだけです。割り込みの最後で次の割り込みの設定を行うことで次々とVCOUNT割り込みを発生させています。

//---------------------------------------------------------------------------
IWRAM_CODE void SprVCount(void)
{
	s32 idx = Spr.idx[Spr.vCnt];		// 転送するSpr.itemのインデックス
	s32 cnt = Spr.idxCnt[Spr.vCnt];		// 転送する数
	s32 all = Spr.oamCnt + cnt;		// OAMインデックス + 転送する数

	s32 sad0 = idx;
	s32 dad0 = Spr.oamCnt;
	s32 cnt0 = cnt;

	s32 sad1 = 0;
	s32 dad1 = 0;
	s32 cnt1 = 0;

	if(all >= SPR_MAX_OAM_CNT)
	{
		cnt0  = all - SPR_MAX_OAM_CNT;
		cnt1  = cnt - cnt0;
		sad1  = idx + cnt0;
	}


	OBJATTR* pS = (OBJATTR*)&Spr.item;
	OBJATTR* pD = (OBJATTR*)OAM;

	if(cnt0 != 0)
	{
		REG_DMA0SAD = (u32)&pS[sad0];
		REG_DMA0DAD = (u32)&pD[dad0];
		REG_DMA0CNT = (u32)cnt0*2 | (DMA_SRC_INC | DMA_DST_INC | DMA32 | DMA_ENABLE);
	}

	if(cnt1 != 0)
	{
		REG_DMA1SAD = (u32)&pS[sad1];
		REG_DMA1DAD = (u32)&pD[dad1];
		REG_DMA1CNT = (u32)cnt1*2 | (DMA_SRC_INC | DMA_DST_INC | DMA32 | DMA_ENABLE);
	}


	Spr.oamCnt += cnt;
	Spr.oamCnt &= (SPR_MAX_OAM_CNT - 1);

	Spr.vCnt++;
	REG_DISPSTAT = (REG_DISPSTAT & STAT_MASK) | LCDC_VCNT | VCOUNT(SprVmap[Spr.vCnt]);
}

実際にはコピー先が OAM の最後に達した際には、wrap処理を行うなど細かい制御が必要になりますが、基本的には上記のようなロジックでラスタ割り込み処理を行います。

オマケ単純な構造のダブラ

上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。 そこで、40個のOBJATTRからなるブロックをラスタ割り込みの毎にコピーしていく形にします。コピー先のOAMアドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。ただし、この構造の場合、一度に扱えるスプライト数が40個に制限されてしまうので、表示数を限界まで高めたい場合には上記の構造を取る必要があります。

512まで表示したスプライト

出典元

履歴


トップ   一覧 検索 最終更新   ヘルプ   最終更新のRSS