スプライトダブラとは、VCOUNT割り込み(ラスタ割り込み)でスプライト情報を書き換える方法です。この技術をGBAで用いると128個以上のスプライトを一度に表示することが可能になります。昔のハードウェア、例えばX68000やMSXなどの時代に限られたスプライトを有効利用するために使われていました。単純な例ですと以下のようにスプライト0を画面の左上に表示していたとします。赤線は現在の描画済みVCOUNTラインです。
その際にスプライト0が表示された後のVCOUNT割り込みでスプライト0の位置を変更すると、スプライト0を画面に2つ表示することが出来ます。
スプライト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ライン毎に区切って、それらを一塊のブロックとして扱います。
//--------------------------------------------------------------------------- #define BULLET_MAX_CHR_CNT 512 #define BULLET_MAX_IDX_CNT 42 // (縦画面160+非表示8) / 4 #define INT2FIX(A) ((A)<<7) #define FIX2INT(A) ((A)>>7) //--------------------------------------------------------------------------- typedef struct { bool is; s16 x; s16 y; s16 mx; s16 my; } ST_BULLET_CHR; typedef struct { s16 maxCnt; // 最大数 s16 idxCnt[BULLET_MAX_IDX_CNT]; // yラインを41分割したカウント数 ST_BULLET_CHR chr[BULLET_MAX_CHR_CNT]; // 弾情報 } ST_BULLET;
//--------------------------------------------------------------------------- IWRAM_CODE void BulletExec(void) { _Memset(&Bullet.idxCnt, 0x00, sizeof(Bullet.idxCnt)); s32 i; for(i=0; i<BULLET_MAX_CHR_CNT; i++) { if(Bullet.chr[i].is == FALSE) { continue; } Bullet.chr[i].x += Bullet.chr[i].mx; Bullet.chr[i].y += Bullet.chr[i].my; s32 xx = FIX2INT(Bullet.chr[i].x); s32 yy = FIX2INT(Bullet.chr[i].y); if(xx <= -8 || xx >= 240 || yy <= -8 || yy >= 160) { Bullet.chr[i].is = FALSE; Bullet.maxCnt--; continue; } s32 idx = (yy + 8) / 4; Bullet.idxCnt[idx]++; } }
まずは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ライン毎に区切られた各ブロック数は決まっているので、ブロック先頭の開始ワークポインタを作ります。関数の後半ではソートされていない弾情報を頭から順番に見ていって、各ワークポインタを通して弾情報とスプライトを作成しています。
まずは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個に制限されてしまうので、表示数を限界まで高めたい場合には上記の構造を取る必要があります。