チュートリアルの最初の方で少し触れたVBlankとHBlankについて再度解説していこうと思います。HBlank期間を利用することで特殊な画面効果が可能です。
GBAの液晶画面は1秒間に約60回(59.727... hz)更新されます。1回の画面更新では、上のラインから1ラインずつ更新していき、画面を更新する160ライン分の期間と、直接描画は行わない68ライン分のブランク期間(VBlank)があります。通常のフレームごとの処理は画面のチラつき防止のためVBlank期間に行います。
画面を更新する1ラインごとの期間のうち、横1ラインを描画する期間が960サイクル、描画を行わない期間が272サイクル(HBlank)あります。960+272=1232サイクル。縦は160とVBlank68で160+68=228ライン、1フレームあたりのサイクル数は1232×228=280896サイクルとなり、GBAのCPUは16.78MHz=16780000サイクルなので1678000/280896=59.737... hz、ほぼ計算が合うことになります。このことは1秒間のCPUサイクル=1フレーム分と考えることができます。単純なことをいうと60fpsでゲームを作る場合、以下の表に書いてあるとおりVBlank中のCPU使用率30%以内でなければなりません。
The drawing time for each dot is 4 CPU cycles. Visible 240 dots, 57.221 us, 960 cycles - 78% of h-time H-Blanking 68 dots, 16.212 us, 272 cycles - 22% of h-time Total 308 dots, 73.433 us, 1232 cycles - ca. 13.620 kHz
Visible (*) 160 lines, 11.749 ms, 197120 cycles - 70% of v-time V-Blanking 68 lines, 4.994 ms, 83776 cycles - 30% of v-time Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz
横1ラインの描画は、横1ライン分のドットの色をBGやスプライト、拡大縮小回転、モザイクなどの特殊効果を考慮して描画されます。1ラインの描画が終わって次のラインを描画するまでのブランク期間(HBlank)に、BGの表示位置などを変更することによって通常の描画では行えないような特殊効果を出すことができます。具体的にはHBlank中に横方向のスクロールのレジスタ値を変更します。このことをラスタースクロールと言います。HBlank期間を判定する方法としては、割り込みのHBlank割り込みを使うことができます。また、現在どのラインを描画中なのかREG_VCOUNT(0~227)を取得することも必要です。
4000006h - VCOUNT - Vertical Counter (Read only) Indicates the currently drawn scanline, values in range from 160..227 indicate 'hidden' scanlines within VBlank area. Bit Expl. 0-7 Current Scanline (LY) (0..227) (R) 8 Not used (0) / NDS: MSB of Current Scanline (LY.Bit8) (0..262) (R) 9-15 Not Used (0)
まず割り込みできるようにIrqInit関数内のREG_IE、REG_DISPSTATにHBlankフラグを登録をします。IrqHandler関数は実際の割り込み中の処理となります。
#include "irq.arm.h" #include "math.h" //--------------------------------------------------------------------------- // main.c extern vs32 shakeAngle; // 横揺れの1周期内の位置 extern vs32 shakeCycle; // 横揺れの周期 extern vs32 shakeCx; // 横振れの幅 //--------------------------------------------------------------------------- EWRAM_CODE void IrqInit(void) { REG_IME = 0; INT_VECTOR = (u32)IrqHandler; REG_IE = IRQ_VBLANK | IRQ_HBLANK; REG_DISPSTAT = LCDC_VBL | LCDC_HBL; REG_IME = 1; } //--------------------------------------------------------------------------- IWRAM_CODE void IrqHandler(void) { REG_IME = 0; u16 flag = REG_IF; if(flag & IRQ_HBLANK) { if(REG_VCOUNT < 160) { // 横方向を揺らす REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + REG_VCOUNT) * shakeCx / 256; } else if(REG_VCOUNT == 227) { // 次のフレームのVCOUNT 0用 REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + 0) * shakeCx / 256; } } if(flag & IRQ_VBLANK) { REG_IRQ_WAITFLAGS |= IRQ_VBLANK; } REG_IF = flag; REG_IME = 1; } // 横方向を揺らす REG_BG1HOFS = MathSin(shakeAngle * shakeCycle + REG_VCOUNT) * shakeCx / 256;
そのラインの左右のスクロール値を決定している部分です。MathSin(shakeAngle * shakeCycle + REG_VCOUNT)の部分は、現在のフレーム数を元にsinの値を取得します。サインテーブルは360で1周期となっているので、angleにSHAKE_CYCLEをかけて360で1周期になるようにします。縦の1ラインごとにスクロール値を変更する必要がある(これをしないとラスタースクロールにならない)ので、REG_VCOUNTで現在のライン番号を取得してラインごとにスクロール値を変更しています。* shakeCx / 256はゆれ幅の調整部分です。GetSin()で取得する値は-256~256の範囲なので、-SHAKE_CX~SHAKE_CXの範囲になるように調節します。
#include "lib/gba.h" #include "irq.arm.h" #include "bg.h" #include "key.h" #include "math.h" //--------------------------------------------------------------------------- vs32 shakeAngle; // 横揺れの1周期内の位置 vs32 shakeCycle; // 横揺れの周期 vs32 shakeCx; // 横振れの幅 //--------------------------------------------------------------------------- int main(void) { REG_WSCNT = 0x4317; shakeAngle = 0; shakeCycle = 4; shakeCx = 32; IrqInit(); KeyInit(); BgInit(); u16 cnt, trg; for(;;) { VBlankIntrWait(); BgAsciiDrawPrintf(1, 1, "cycle = [%3d]", shakeCycle); BgAsciiDrawPrintf(1, 2, "cx = [%3d]", shakeCx); KeyExec(); cnt = KeyGetCnt(); trg = KeyGetTrg(); if(trg & KEY_UP) { shakeAngle=0; shakeCycle--; } if(trg & KEY_DOWN) { shakeAngle=0; shakeCycle++; } if(cnt & KEY_LEFT) { shakeAngle=0; shakeCx--; } if(cnt & KEY_RIGHT) { shakeAngle=0; shakeCx++; } shakeCycle = MathClamp(shakeCycle, 1, 32); shakeCx = MathClamp(shakeCx, 1, 64); if(shakeAngle == Div(360, shakeCycle)) { shakeAngle = 0; } shakeAngle++; } }
ボタン操作できるので動作を見つつトレースして頂ければ幸いです。