#author("2023-05-08T19:43:11+09:00","","")
> トップページ > GBA > ドキュメント
#author("2023-05-09T07:07:57+09:00","","")
* スプライトダブラ [#e3a083e3]
ラスタ割り込みでスプライトの情報を書き換えることで、 スプライトを限界を越えて表示する技術です。昔のハードウェア、例えばX68000やMSXなどが現役の時代に限られたスプライトを有効利用するために使われていました。この技術をGBAで用いると、例えば128個以上のスプライトを一度に表示することが可能になります。

始めに
■ GBA とは?
任天堂から発売されている携帯ゲームマシン GAMEBOY ADVANCE。 簡単なスペックは以下の通りです。
CPU 	ARM7 CPU (32bit RISC)
メモリ 	外部メモリ 256KB
内部メモリ 32KB
VRAM 96KB
画面解像度 	240x160
スプライト 	最大 128 個
サイズ: 8x8 〜 64x64

ファミコンからの流れを汲むガチガチの 2D マシンです。 浮動小数点演算ユニットもありませんし、ポリゴン用のハードウェアも載っていませんが、 手頃な CPU パワーとスプライトがあるので 2D のプログラムはサクサク動きます。
■ スプライトダブラとは?
ラスタ割り込みでスプライトの情報を書き換えることで、 スプライトを限界を越えて表示する技術です。 昔のハードウェア、例えば X68000 や MSX などが現役の時代に限られたスプライトを有効利用するために使われていました。

この技術を GBA で用いると、例えば 128 個以上のスプライトを一度に表示することが可能になります。

単純な例ですと、以下のようにスプライト 0 を画面の左上に表示していたとします。
単純な例ですと、以下のようにスプライト0を画面の左上に表示していたとします。
sprite1.png

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

これをスプライト 0 〜 スプライト 127 までに対して行えば、256 個のスプライトを表示することが出来ます。
スプライトダブラの実践
■ 始めに
GBA ではスプライトの情報を以下のような形で持っています。
これをスプライト0〜スプライト127までに対して行えば、256個のスプライトを表示することが出来ます。

** 始めに [#w8cb0b00]
GBAではスプライトの情報を以下のような形で持っています。

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

この OBJATTR の配列 (128 個分) が OAM と呼ばれるメモリ空間に格納されていますので、 スプライトダブラではラスタ割り込みの処理で OAM を書き換えることになります。
■ 方針
スプライトダブラを実装するにあたっては、ラスタ割り込みの処理を工夫する必要があります。 というのも、割り込み処理はあまり時間のかかる処理を行うことが出来ません。 ループをまわしてスプライト情報を書き換えることは現実的とは言い難いものがあります。
このOBJATTRの配列(128個分)がOAMと呼ばれるメモリ空間に格納されていますので、スプライトダブラではラスタ割り込みの処理でOAMを書き換えることになります。

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

■ 基本的な構造
スプライトダブラで最大 512 個のスプライトを表示しようとする場合、 512 個分の OBJATTR データをあらかじめ用意しておき、 ラスタ割り込みの際に少しづつデータをずらして上書きコピーしていけば 512 個のスプライトを表示することが出来ます。
** 基本的な構造 [#x16dece7]
スプライトダブラで最大512個のスプライトを表示しようとする場合、512個分のOBJATTRデータをあらかじめ用意しておき、ラスタ割り込みの際に少しづつデータをずらして上書きコピーしていけば512個のスプライトを表示することが出来ます。

※疑似コードを書く
フレーム開始時 	oam01.png
ラスタ割り込み 	oam02.png
このようにコピーをしていくためには、 あらかじめ用意する 512 個の OBJATTR は y 軸に沿ってソートされている必要があります。 そのため、スプライトダブラの主な処理として、以下の 2 つを用意します。

    ソートされた 512 個の OBJATTR を用意する。
    ラスタ割り込みで、それらを上書きコピーする。
このようにコピーをしていくためには、あらかじめ用意する 512個のOBJATTRはy軸に沿ってソートされている必要があります。そのため、スプライトダブラの主な処理として、以下の2つを用意します。

オマケ: 単純な構造のダブラ
BulletGBA から見る実装方式
■ BulletGBA のソースコードから見るスプライトダブラの実装
BulletGBA のソースコードのからスプライトダブラの実装を見てみます。 前提として、 BulletGBA では y 軸を 4 ライン毎に区切って、それらを一塊のブロックとして扱います。
- ソートされた 512 個の OBJATTR を用意する。
- ラスタ割り込みで、それらを上書きコピーする。

これは、 OAM への DMA 転送が最大 2 ライン分の時間がかかると考え、 最高でも 4 ライン毎でしか DMA 転送をスタートしないことに起因しています。 ラスタ割り込みが最大限発生する場合には VCOUNT が 0, 4, 8, 12, ..., 152 の場合のラスタ割り込みで DMA 転送をスタートさせます。
** 実装方式 [#hc944e96]
サンプルのソースコードのからスプライトダブラの実装を見てみます。前提として、y軸を4ライン毎に区切って、それらを一塊のブロックとして扱います。

その場合、一度の DMA 転送で扱うスプライトは最大 4 ライン分、つまり一塊のブロック分になります。
これは、OAMへのDMA転送が最大2ライン分の時間がかかると考え、 最高でも4ライン毎でしかDMA転送をスタートしないことに起因しています。ラスタ割り込みが最大限発生する場合にはVCOUNTが0,4,8,12, ..., 152の場合のラスタ割り込みでDMA転送をスタートさせます。

■ ソートされた 512 個の OBJATTR の用意
その場合、一度のDMA転送で扱うスプライトは最大4ライン分、つまり一塊のブロック分になります。

** ソートされた512個のOBJATTRの用意 [#u7a70178]

    class SpriteDoubler {
      static const u32 LINEBLOCK = 4;
      static const u32 MAXITEM = 512;
      static const u32 NUM_BLOCK = 160 / LINEBLOCK;

      class CompiledObjattr {
        OBJATTR sortedOBJATTR[MAXITEM]; // 512 個分のソート済み OBJATTR

        u32 itemNumInBlock[NUM_BLOCK]; // 各ブロックに含まれるスプライトの数

        OBJATTR *objattrStartPosInBlock[NUM_BLOCK]; // sortedOBJATTR に対するポインタ。 各ブロックに対応するコピー先エリア。
      };
    };

上で見たように、 BulletGBA では 4 ラインを一塊として見るので、 OBJATTR のソートも正確に y 軸に沿っていなくても 4 ライン分は同一視してかまわないことになります。
上で見たように、4ラインを一塊として見るので、OBJATTRのソートも正確にy軸に沿っていなくても4ライン分は同一視してかまわないことになります。

そこで、まずは 4 ライン毎に区切られた各ブロックにそれぞれ幾つのスプライトが属するかをカウントします。 これらの個数が itemNumInBlock に格納されます。 例えば itemNumInBlock[0] は 0 〜 3 ラインに含まれるスプライトの数、 itemNumInBlock[1] は 4 〜 7 ラインに含まれるスプライトの数になります。
そこで、まずは4ライン毎に区切られた各ブロックにそれぞれ幾つのスプライトが属するかをカウントします。これらの個数がitemNumInBlockに格納されます。 例えばitemNumInBlock[0]は0〜3ラインに含まれるスプライトの数、itemNumInBlock[1]は4〜7ラインに含まれるスプライトの数になります。

次に、 512 個分の OBJATTR に対して、各ブロックに対応するコピー先エリアを設定します。 下記の makeObjAttrStartPosInBlock 関数で設定を行います。 各ブロックに所属するスプライトについて *objattrStartPosInBlock に attribute を設定すれば良いことになります。
次に、512個分のOBJATTRに対して、各ブロックに対応するコピー先エリアを設定します。 下記のmakeObjAttrStartPosInBlock関数で設定を行います。各ブロックに所属するスプライトについて*objattrStartPosInBlockにattributeを設定すれば良いことになります。

        void makeObjAttrStartPosInBlock() {
          objattrStartPosInBlock[0] = sortedOBJATTR;
          for (u32 i = 0; i < NUM_BLOCK - 1; ++i) {
            objattrStartPosInBlock[i + 1] = objattrStartPosInBlock[i] + itemNumInBlock[i];
          }
        }

このように事前準備を行った上でソート済み OBJATTR を作成します。 大雑把に下記のような処理で sortedOBJATTR を作成していきます。
このように事前準備を行った上でソート済みOBJATTRを作成します。大雑把に下記のような処理でsortedOBJATTR を作成していきます。

        void registObjAttr(u32 attr0, u32 attr1, u32 attr2) {
          u32 idx = attr0 / LINEBLOCK;

          OBJATTR *p = objattrStartPosInBlock[idx];

          p->attr0 = attr0;
          p->attr1 = attr1;
          p->attr2 = attr2;

          ++(objattrStartPosInBlock[idx]);
        }

こうすることで、ソート済み OBJATTR が sortedOBJATTR 上に構築されました。 実際にこれらの処理を呼び出す処理は、 GameEngine::compileBullet などで行われています。
こうすることで、ソート済みOBJATTRがsortedOBJATTR 上に構築されました。実際にこれらの処理を呼び出す処理は、GameEngine::compileBulletなどで行われています。

    void
    GameEngine::compileBullet()
    {
      SpriteDoubler::CompiledObjattr *p = SpriteDoubler::getIncurrentCompiledObjattr();
      p->initialize();

      BulletInfo *bi;
      // ------------------------------------------------------------
      bi = ListBullets::getFirstItem();
      for (;;) {
        if (bi == NULL) {
          break;
        }
        p->registItemNumInBlock(bi->posy);

        bi = ListBullets::iterator(bi);
      }

      // ------------------------------------------------------------
      p->normalizeItemNumInBlock();
      p->makeObjAttrStartPosInBlock();

      // ------------------------------------------------------------
      bi = ListBullets::getFirstItem();
      for (;;) {
        if (bi == NULL) {
          break;
        }

        p->registObjAttr(bi->posy, bi->posx, OBJ_PALETTE(0) | bi->type);

        bi = ListBullets::iterator(bi);
      }
    }

■ ラスタ割り込みによる OAM の DMA 転送
まずは VBlank 割り込みで OAM の初期設定を行います。 その後に、ラスタ割り込みを行うべき VCOUNT の値を設定します。
** ラスタ割り込みによるOAMのDMA転送 [#a76c368e]
まずはVBlank割り込みでOAMの初期設定を行います。その後に、ラスタ割り込みを行うべきVCOUNTの値を設定します。

注意点としては、 n 番目のブロックを転送するには、 4 ライン前の VCOUNT、つまり (n - 1) * 4 ライン目で割り込みを発生させる必要があります。 このタイミングで転送をスタートしておけば、実際に n ブロック目が描写されるタイミング (n * 4 ライン目) には OAM への DMA 転送が終了している計算になります。
注意点としては、n番目のブロックを転送するには、4ライン前のVCOUNT、つまり(n-1)*4ライン目で割り込みを発生させる必要があります。このタイミングで転送をスタートしておけば、実際にnブロック目が描写されるタイミング(n*4 ライン目) にはOAMへのDMA転送が終了している計算になります。

    void
    SpriteDoubler::irq_vblank()
    {
      p->srcStart = p->sortedOBJATTR;
      p->dstStart = OAM + DOUBLER_SPRITE_START;

      u32 size = p->firstCopySize;
      DMA1COPY(p->srcStart, p->dstStart, DMA32 | DMA_IMMEDIATE | (size * sizeof(OBJATTR) / 4));

      p->nextIdx = p->itemContainIdx;
      u32 vcount = (*(p->nextIdx) - 1) * LINEBLOCK;
      REG_DISPSTAT = (REG_DISPSTAT & 0xff) | VCOUNT(vcount);
    }

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

    void
    SpriteDoubler::irq_vcount()
    {
      u32 num = p->itemNumInBlock[*(p->nextIdx)];
      DMA1COPY(p->srcStart, p->dstStart, DMA32 | DMA_IMMEDIATE | (num * sizeof(OBJATTR) / 4));
      p->srcStart += num;
      p->dstStart += num;

      ++(p->nextIdx);
      u32 vcount = (*(p->nextIdx) - 1) * LINEBLOCK;
      REG_DISPSTAT = (REG_DISPSTAT & 0xff) | VCOUNT(vcount);
    }

実際にはコピー先が OAM の最後に達した際には、 wrap 処理を行うなど細かい制御が必要になりますが、 基本的には上記のようなロジックでラスタ割り込み処理を行います。
オマケ
■ 単純な構造のダブラ
上記の構造 だとコピー先の OAM アドレスが毎回違っていたり、 あらかじめ用意する 512 個分のスプライトメモリは隙間なく並べておかないといけないなど、 ロジックが複雑になりがちです。 そこで、下記のような構造を取ると簡単にダブラを実装できます。
実際にはコピー先が OAM の最後に達した際には、wrap処理を行うなど細かい制御が必要になりますが、基本的には上記のようなロジックでラスタ割り込み処理を行います。

この場合、 40 個の OBJATTR からなるブロックをラスタ割り込みの毎にコピーしていく形になるので、 コピー先の OAM アドレスは 3 種類で固定されていますし、 あらかじめ用意するスプライトメモリの構造も単純です。
制約の話書く

ただし、この構造の場合、一度に扱えるスプライト数が 40 個に制限されてしまうので、 スプライトの表示数を限界まで高めたい場合には 上記の構造 を取る必要があります。
** オマケ単純な構造のダブラ [#n80470da]
上記の構造だとコピー先のOAMアドレスが毎回違っていたり、あらかじめ用意する512個分のスプライトメモリは隙間なく並べておかないといけないなど、ロジックが複雑になりがちです。 そこで、下記のような構造を取ると簡単にダブラを実装できます。

この場合、40個のOBJATTRからなるブロックをラスタ割り込みの毎にコピーしていく形になるので、コピー先のOAM アドレスは3種類で固定されていますし、あらかじめ用意するスプライトメモリの構造も単純です。

ただし、この構造の場合、一度に扱えるスプライト数が40 個に制限されてしまうので、スプライトの表示数を限界まで高めたい場合には上記の構造を取る必要があります。
フレーム開始時 	oam-another01.png
ラスタ割り込み(1) 	oam-another02.png
ラスタ割り込み(2) 	oam-another03.png
■ 単純な構造のダブラとの比較

左が単純な構造のダブラ、右が複雑のダブラです。
(下のバーにマウスカーソルをあわせると再生できます)。
Comments for This Page.
Date: 2007-03-21 00:00 (JST)


GBA で学ぶ古典的プログラミング (スプライトダブラ)


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