ゲームボーイアドバンスのゲームを書こう

子どもの頃は、ゲームボーイのゲーム(たいがい、かなりひどいもの)で遊ぶのにたっぷり時間を費やしました。これまでは「標準の」一般的な目的に使われるコンピュータ以外のためにコードを書いたことはなかったのですが、最近考えるようになりました。「ゲームボーイ(アドバンス)のゲームは簡単に書けるだろうか?」

ゲーム機には詳しくない方のために説明すると、ゲームボーイアドバンス(GBA)は、任天堂が発売した人気携帯型ゲーム機でした(写真下)。240 x 160 (3:2) 15ビット カラーLCDディスプレイ、6種類のボタンと十字キーが付いています。


ゲームボーイアドバンス

内部を見ると、GBAのCPUは32-bit ARM7TDMI RISCコア(16.78MHz)です。標準の32-bit ARMのインストラクションのほかに、このチップは16ビット Thumbのインストラクションも実行できます。Thumbのインストラクションセットは、非常に一般的な32-bit ARMのインストラクションのいくつかに対してたった16ビットのエンコーディングを行うため、スペースを節約できます。

メモリについては、このデバイスはCPUの中に130KBの内蔵メモリ(うち96KBはVRAM、32KBは一般利用、2KBはその他に使われている)、CPUの外部に256KBのRAMを擁しています。またシステムは、BIOSを格納する16KBのシステムROMを持っています。他にも先行機のゲームボーイカラーとの上位互換に関わる機能がいろいろあるのですが、ここでは触れません。

以上全ての本体メモリに加え、GBAは一般的にいくつかの種類のゲームカートリッジを搭載できました。これらはたいていROM(インストラクション、リードオンリーデータなどを格納する)といくつかの形態のミュータブルなストレージ(SRAM、フラッシュメモリまたはEEPROMが多い)で構成されています。Game Pak ROMが16ビット幅のバス経由で接続されるので、ゲームコードの多くの時間、32ビット ARMのインストラクションよりも、当然16ビットのThumbインストラクションを使います。


GBA Game Pakの例(reinerziegier.de提供)

これまで見てきたメモリセクション、I/Oハードウェアレジスタ(グラフィック、音、DMAなどを制御する)は、メモリにマッピングされており、下記のようなレイアウトになっています。

  • 0x00000000 - 0x00003FFF – 16KBシステムROM(実行可能だが読み出し不可)
  • 0x02000000 - 0x02030000 – 256 KB EWRAM (標準目的の外部RAM)
  • 0x03000000 - 0x03007FFF – 32 KB IWRAM(標準目的の内部RAM)
  • 0x04000000 - 0x040003FF – I/Oレジスタ
  • 0x05000000 - 0x050003FF – 1KB カラーパレット RAM
  • 0x06000000 - 0x06017FFF – 96KB VRAM (ビデオRAM)
  • 0x07000000 - 0x070003FF – 1KB OAM RAM (オブジェクト属性メモリ – 後で説明します)
  • 0x08000000 - 0x???????? – Game Pak ROM (0 から 32MB)
  • 0x0E000000 - 0x???????? – Game Pak RAM

これらのセクションは異なるバス幅、読み出し/書き込み権限(たとえば、VRAMには個別のバイト数書き込みは不可)を持っており、いくつかのセクションはメモリに複数の点に渡るミラーデータも持っています。実際にはもう少し複雑な状況がありますが、これが基本的なGBAゲームを作るのに必要とする主な構造です。

デバイスのメモリ構造が分かったところで、GBA ROMで”Hello, World!”を表示させるプランは次のとおりです。Game Pak ROMにThumbコードを書きます。これはI/Oレジスタに、特定のディスプレイモードに適したディスプレイ変数を設定するものです。そして表示させたいグラフィックデータをVRAMに書きます。デバイスの理論は置いておいて、まずは実際に何かを作ってみましょう。

開発環境のセットアップ

GBA ROM構築プランを実行するにあたり、ROMフォーマットについて知る必要があります。細かいところに入り込む前に見ておきたいのは、GBA ROMは標準ヘッダで始まるということです。プログラムの開始アドレスの枝分かれを指示する4バイトARMインストラクションで始まり、次にいくつかの「magic」バイトが任天堂ロゴを表示させます。

加えて、このヘッダにはゲームに関するデータ(ゲームタイトルなど)とこのデータの’check’値が含まれます。ROMが最適に実行されるために、ヘッダは完璧に正しくなければならないことを知っておきましょう(特に、エミュレータでなくある実在のデバイスで動かすことを目的にしている場合)。

ありがたいことに、ROM作成過程のほぼ全てに使える良いツールチェーンがあります。私が使っているdevkitARMツールチェーン(GCCツールチェーンをベースにしたdevkitProのツールチェーンの1つ)は、プロセスを非常に簡単にしてくれます。ツールチェーンをセットアップしたら、実質4つの手順で、CコードをGBA ROMに入れることができるのです。

  1. CコードをGBAのCPUのThumbインストラクションにクロスコンパイルします。ROMコード製のThumbオブジェクトファイルが生成されます。
  2. オブジェクトファイルを、特定の「specs」ファイルを用いて実行可能ファイルにリンクし、リンクのふるまいを制御できるようにします。通常specsファイルは1つのリンクスクリプト(セグメントの位置—多くのミュータブルデータはIWRAMに、’const’データはROMに格納されます—、列などを特定し、GBAのコンパイルを正しい状態にします)と、その他複数のオブジェクトファイル(多くは標準ROMヘッダ、スタートアップルーティン、プログラム初期化・終了コード)を持っています。
  3. 完全に近いROMファイルを得るため、必要のない情報の実行可能ファイル(実行可能ヘッダ、シンボル、移動情報など)を取り除きます
  4. 前段階でできたROMファイル上でユーティリティを走らせ、ヘッダを修正します(ヘッダの任天堂ロゴデータが正しいか、「check」値は全て適切か、など)。

私のマシンOS Xで使用しているツールチェーンのバージョンでは、上述したようにCファイルをGBA ROMにコンパイルする際、次のコマンドを実行することができます(PATH環境変数を/opt/devkitpro/devkitARM/binだとして)。

  1. arm-none-eabi-gcc -c main.c -mthumb-interwork -mthumb -O2 -o main.o
  2. arm-none-eabi-gcc main.o -mthumb-interwork -mthumb -specs=gba.specs -o main.elf
  3. arm-none-eabi-objcopy -v -O binary main.elf main.gba
  4. gbafix main.gba

最初のステップ(コンパイル)で、追加で立てておきたいフラグが他にもあるかもしれません。その場合、たとえば、-fno-strict-aliasingを使う事をおすすめします。ローメモリとポインタを多数扱いますし、C言語の厳密な言い換え規則に煩わされたくないためです。あるいは、Makefileかシェルスクリプトをこれらのコマンドと共に書くのも、ROMコンパイルを容易にするのに有益かもしれません。このような詳細はこの投稿ではやや不必要と思われるかもしれませんが。

C言語からROMコンパイルのプロセスが確立したら、早速実行、テストをしてみましょう。理論には興味がない読者のため、今回作成しているGBA ROM “Hello, World” のコードを提示し、話は後にしましょう。

int main(void) {
    // Write into the I/O registers, setting video display parameters.
    volatile unsigned char *ioram = (unsigned char *)0x04000000;
    ioram[0] = 0x03; // Set the 'video mode' to 3 (in which BG2 is a 16 bpp bitmap in VRAM)
    ioram[1] = 0x04; // Enable BG2 (BG0 = 1, BG1 = 2, BG2 = 4, ...)

    // Write pixel colours into VRAM
    volatile unsigned short *vram = (unsigned short *)0x06000000;
    vram[80*240 + 115] = 0x001F; // X = 115, Y = 80, C = 000000000011111 = R
    vram[80*240 + 120] = 0x03E0; // X = 120, Y = 80, C = 000001111100000 = G
    vram[80*240 + 125] = 0x7C00; // X = 125, Y = 80, C = 111110000000000 = B

    // Wait forever
    while(1);

    return 0;
}

上のコードはかなりシンプルで、GBAスクリーンの中央に赤色、緑色、青色それぞれ1つずつ、3つのピクセルが水平に並ぶというものです。

では、改めてコードについて説明します。最初に、いくつかのディスプレイ変数をI/Oレジスタをマッピングしたメモリに書きます。特に、このメモリの初めの16ビットは、ディスプレイ制御レジスタ(DISPCNTとも呼ばれます)です。このレジスタの冒頭の3ビットはビデオモードを示し、11番目のビットはバックグラウンド#2(BG2)が有効か否かを示します。こうして値を書くことによって、ビデオモードをモード3に設定し、BG2を有効にします。

なぜこのような作業が必要なのでしょうか? まあ、まずはビデオモードからいきましょうか。ビデオモード3はVRAMにビットマップデータを書くことができるモードであり、BG2はこのビットマップを表示します(そのために、BG2も有効にしたいのです)。さらに、なぜ私があえて他のビデオモードでなくビデオモード3を選んだのか疑問に思われるかもしれません。その理由は、ビデオモード0から2は説明するのが非常に難しいからです(とはいえ、後でそれらも使います)。

先に述べたように、GBA上のLCDは15ビットカラーの表示が可能です。よって、GBAのカラーを1つの15ビットカラーフォーマットを使って表現できるということです。しかし、データ配列の理由から、GBAは16ビットのカラーフォーマットを使います。具体的には、フォーマットは下記のとおりです。?BBBBBGGGGGRRRRR。つまり、1ビットの不使用ビットの後に、5ビットの青色、5ビットの緑色、そして5ビットの赤色が並ぶということです。

このフォーマットと、ビデオモード3がVRAMを240 x 160ビットマップとしてどのように扱うかの知識をもとに、私たちの”Hello, World” ROMはシンプルに一定のピクセルオフセットでいくつかのカラー値を書き出します(例:unsigned shortが16ビットサイズだと仮定すると、vram[80*240 + 120]は240ピクセルの80行をスキップし、水平ライン上中央のピクセルにアクセスします)。憶えておきたいのは、コード内でハードウェアにインターフェース接続する全てのメモリアクセスはvolatileポインタを通じて起こるということです。このことは、コンパイラが不必要に思われるメモリオペレーションを最適化するのを妨げます。

ポンの類似ゲームを作ろう

基本は脇に置いて、もう少し面白い物を作ってみましょう。一連のプロジェクトのインフラストラクチャやヘルパー関数を構築するのではなく、ここでも一緒にソリューションのプログラミングに取り組んでいきます。ただし、更に進んだGBAの画像のレンダリング機能を使用する予定です。特に、ビットマップビデオモードを使わずに描画する方法を用いましょう。

GBAのビットマップビデオモード(モード3、4、5)で描画するのはとても簡単です。しかし、実際にゲーム制作に利用されることは多くありません。240×160のビットマップは、一度画面を埋めるだけでVRAMの大部分を使ってしまいます。そして各フレームで非常に多くのピクセルを使うので、コンピューターへの負荷がとても増えてしまうのです(適度なフレームレートでゲームをレンダリングしようとする余裕はなくなってしまうでしょう)。ですから、ビデオモード0、1、2を使います。

これらのモードはかなり複雑なので、ここでは最も重要な部分だけをざっと説明します。GBAのビデオモード0、1、2では、個々のピクセル上で動作するのではなく、タイル上で動作します。タイルとは8×8のビットマップのことです。4及び8ビット毎ピクセル(bpp)バリアント型データの中に存在します。今回は4bppタイプを使いましょう。つまり、使用するタイルのサイズは32バイトとなります(8×8×4=256ビット)

4(または8)ビットのピクセルに、どうやって15ビットの色値を適用させるのかと疑問に思われるかもしれませんが、そうはしません。タイル内のピクセル値は、色を直接参照するのではなく、あるカラーパレット内の色を参照します。先ほど言及したカラーパレットのメモリ(0x05000000)に、色値を書き込むことでカラーパレットを定義することが可能です。ここには16ビットを512セット(つまり512色)保存でき、実質的には256色を2パレット、または16色を32パレット保存できるということです。


パレットを適用したタイルを視覚化した例

ここで使っている4bppで8×8のビットマップのタイルでは、16色の32パレットとしてカラーパレットメモリを扱おうと思います。この方法では、各ピクセルに4ビットを用いてカラーインデックス(16色のパレット内)を指定することができます。タイルを基準としたビデオモードを使うと、タイルはVRAM内で「タイルブロック」または「charブロック」にセクション分けされます。各タイルブロックのサイズは16KBなので、1つのタイルブロックには512の4bppタイル、VRAMには6つのタイルブロックを適用させることができます。

理論上、VRAM内の6つのタイルブロックは2つのグループに分かれます。最初の4つ(0 – 3)は背景に使うことができ、残りの2つ(4と5)はスプライトに使うことができます。同様に、パレットメモリ内にある16色32パレットは、16パレットは背景用に、別の16パレットはスプライト用に分けられています。私たちが作るゲームでは背景は扱わないので、VRAM内のタイルブロック4、5(つまり0x60100000x6014000から始まるアドレス)とカラーパレットのブロック1(アドレス0x5000200)だけに注目していきましょう。例えば、いくつかのタイルをタイルブロック4へとロードしたとします。これをどうしたらいいのでしょうか? このような場合に、タイルの扱い方で肝心なのは、これらのタイルを使うスプライトを作るということです。

コンピュータグラフィックスにおいて、スプライトは、より大きなシーンに適用するための2次元のイメージです。GBAには「オブジェクト」(つまりスプライト)をレンダリングできるハードウェアが備わっていて、オブジェクトがピクセル修正の流れから外れることなく動き回れるように、オブジェクトはレンダリングされます。オブジェクトを使用可能なように設定されると(ディスプレイ制御のI/Oレジスタ内のビット13が設定されると)、オブジェクトの属性をGBAのObject Attribute Memory (OAM)に書き込むことで、特定のタイルセットからオブジェクトを生成できるようになります。私たちは「ポン」の類似ゲームを作ろうとしていますが、この場合、少なくともラケットとボールの2つのスプライトが必要になるでしょう。また、どんな「オブジェクト」であっても、それぞれ16ビットの属性を3セット持っています。

  • 属性0:特に、オブジェクトのY座標、オブジェクトのシェイプ、オブジェクトのタイルのカラーモード(4bppまたは8bpp)を含みます。
  • 属性1:特に、オブジェクトのX座標とオブジェクトのサイズを含みます。
  • 属性2:特に、オブジェクトのベースタイルインデックスとオブジェクトが使うべきカラーパレット(4bppモードの場合)を含みます。

これらの値の詳細はこちらのページで確認できます。しかし根本的に、Y座標は属性0の中で最も低い8ビット、X座標は属性1の中で最も低い9ビットとなり、カラーモードは4bppに初期化されます(つまり、0=4bpp)。

オブジェクトの「シェイプ」と「サイズ」のビットは、その形状を定義し、この4ビットを異なる組み合わせにすることで、最終的なシェイプを変えることができます(このシステムよりも複雑なエンティティは、より小さい複数のオブジェクトで構成されています)。1タイルのサイズよりオブジェクトを大きくしなければならない場合は、設定したマッピングモードに左右されるアピアランスのために、別のタイルを使うこととなります(ディスプレイ制御I/Oレジスタの7番目のビット)。オブジェクトが1タイルよりも大きくなってしまう場合は、メモリ内の「ベースタイル」に続くタイルを使って内部を埋める、1Dマッピングモードを使うのが一番簡単な方法でしょう。

先ほど説明したスプライトについてですが、構築する準備は大体できました。今回のプログラムではビデオモード0を使いたいと思います。このモード内ではBG0~BG3が「標準」モードで作動しています(アフィン変換はできません)。あとは、GBAの十字キーからプリミティブ物理コードを入力し、ゲームループの内部全体に置くだけで、ゲームができるのです!

最後に必要な2つの作業は、両方ともI/Oレジスタで行います。デバイスの入力状態は、単純にKEYINPUT I/Oレジスタ(0x04000130)から読み取ることができます。また、ビットの割り当ての詳細を使い、あるキーが押されたかどうか分かるよう、この状態にマスクを作っておきます。ゲームループに関しては、残念ですが、最後にもう一つ理論が必要となります。

典型的なゲームループは、描画期間と更新期間で構成されています。今回の場合は、これが発生するタイミングを選ぶことはできません。ゲームボーイがオブジェクトを途中まで描画している最中に、表示したいものを変える場合、スクリーンを分けなくてはいけません(オブジェクトの半分はあるデータセットで描画されたもので、もう半分は別のデータセットで描画されたもの)。結果として、GBAの表示リフレッシュサイクルを使って、描画と更新を同期しなければいけません。

デバイスには、描画した水平ライン(もしくはスキャンライン)が終わるたびに更新する時間が少し設けられていますが、スクリーン全体を描画した後は少し長めの時間が与えられます(およそ5ミリ秒)。この場合、画面全体を描画した後は、更新のためだけに時間を使えるようになります。この期間は「V-Blank」と呼ばれています(まだスクリーンが垂直方向に描画されている「V-Draw」の逆です)。

現在、デバイスがどの程度描画されたかを確認するためにVCOUNT I/Oレジスタ(0x04000006)で8ビットの値をチェックできます。この値はV-Blankの期間に、スキャンラインが描画され続けているかのように増加し続けます(そのため0から227までの幅があります)。もし160以上の値がカウントされたら、V-Blankの期間だということが分かるのです。そのため、ゲームループの「更新」ステージ前にV-Drawの期間が終わるのを待っているのであれば、同期プリミティブフォームがあります。

この同期によって、ようやくゲームを構築するのに充分な情報を得ることができます。今回は、1人用のポンの類似ゲームを作ることにしましょう(極めてプリミティブな物理学を用います)。コメント入りのソースコードを以下に示します。

typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef uint16 rgb15;
typedef struct object_attributes {
    uint16 attribute_zero;
    uint16 attribute_one;
    uint16 attribute_two;
    uint16 pad;
} __attribute__((aligned(4))) object_attributes;
typedef uint32 tile4bpp[8];
typedef tile4bpp tile_block[512];

#define SCREEN_WIDTH  240
#define SCREEN_HEIGHT 160

#define MEM_IO   0x04000000
#define MEM_PAL  0x05000000
#define MEM_VRAM 0x06000000
#define MEM_OAM  0x07000000

#define REG_DISPLAY        (*((volatile uint32 *)(MEM_IO)))
#define REG_DISPLAY_VCOUNT (*((volatile uint32 *)(MEM_IO + 0x0006)))
#define REG_KEY_INPUT      (*((volatile uint32 *)(MEM_IO + 0x0130)))

#define KEY_UP     0x0040
#define KEY_DOWN   0x0080
#define KEY_ANY    0x03FF

#define OBJECT_ATTRIBUTE_ZERO_Y_MASK  0xFF
#define OBJECT_ATTRIBUTE_ONE_X_MASK  0x1FF

#define oam_memory ((volatile object_attributes *)MEM_OAM)
#define tile_memory ((volatile tile_block *)MEM_VRAM)
#define object_palette_memory ((volatile rgb15 *)(MEM_PAL + 0x200))

// 各色の数値(範囲内に収まっていると信用することにします)から、16bitのGBA向けBGRカラーを生成します
static inline rgb15 RGB15(int r, int g, int b) { return r | (g << 5) | (b << 10); }

// オブジェクトの位置を、設定したx,yの値に設定します
static inline void set_object_position(volatile object_attributes *object, int x, int y) {
    object->attribute_zero = (object->attribute_zero & ~OBJECT_ATTRIBUTE_ZERO_Y_MASK) | (y & OBJECT_ATTRIBUTE_ZERO_Y_MASK);
    object->attribute_one = (object->attribute_one & ~OBJECT_ATTRIBUTE_ONE_X_MASK) | (x & OBJECT_ATTRIBUTE_ONE_X_MASK);
}

// 'value'として入力された値を、'min'と'max'の間に収めます(上下端を含む)
static inline int clamp(int value, int min, int max) { return (value < min ? min : (value > max ? max : value)); }

int main(void) {
    // スプライトのためのタイルをVRAMの4番目のタイルブロックに書き込みます
    // 具体的には、4つのタイルに8x32のラケットのスプライトを、1つのタイルに8x8のボールのスプライトを書き込みます
    // 0x1111 = 0001000100010001 [4bpp = colour index 1, colour index 1, colour index 1, colour index 1]
    // 0x2222 = 0002000200020002 [4bpp = colour index 2, colour index 2, colour index 2, colour index 2]
    // 注:ここでは、独自の書き込みコードを使うことで、'memset'がするようなバイト単位の書き込みを防いでいます
    // (GBAはバイト単位の書き込みをサポートしていないからです)
    volatile uint16 *paddle_tile_memory = (uint16 *)tile_memory[4][1];
    for (int i = 0; i < 4 * (sizeof(tile4bpp) / 2); ++i) { paddle_tile_memory[i] = 0x1111; }
    volatile uint16 *ball_tile_memory = (uint16 *)tile_memory[4][5];
    for (int i = 0; i < (sizeof(tile4bpp) / 2); ++i) { ball_tile_memory[i] = 0x2222; }

    // カラーパレットメモリーの最初の16色パレット(インデックスは0)に、
    // スプライトで使うカラーパレットを書き込みます
    object_palette_memory[1] = RGB15(0x1F, 0x1F, 0x1F); // 白
    object_palette_memory[2] = RGB15(0x1F, 0x00, 0x1F); // マゼンタ

    // オブジェクト属性をOAMメモリに書き込むことで、スプライトを生成します
    volatile object_attributes *paddle_attributes = &oam_memory[0];
    paddle_attributes->attribute_zero = 0x8000; // このスプライトは4bppのタイルからなり、TALL形状を持つ
    paddle_attributes->attribute_one = 0x4000; // このスプライトは、TALL形状が設定されていると8x32のサイズになる
    paddle_attributes->attribute_two = 1; // このスプライトのベースのタイルはタイルブロック4の1つめのタイルで、カラーパレット0を使います
    volatile object_attributes *ball_attributes = &oam_memory[1];
    ball_attributes->attribute_zero = 0; // このスプライトは4bppのタイルからなり、SQUARE形状を持つ
    ball_attributes->attribute_one = 0; // このスプライトは、SQUARE形状が設定されていると8x8のサイズになる
    ball_attributes->attribute_two = 5; // このスプライトのベースのタイルはタイルブロック4の5つめのタイルで、カラーパレット0を使います

    // ラケットとボールの状態を追跡するための変数を初期化し、
    // 初期位置にセットします(OAM内の属性を変更することによって)
    const int player_width = 8, player_height = 32, ball_width = 8, ball_height = 8;
    int player_velocity = 2, ball_velocity_x = 2, ball_velocity_y = 1;
    int player_x = 5, player_y = 96;
    int ball_x = 22, ball_y = 96;
    set_object_position(paddle_attributes, player_x, player_y);
    set_object_position(ball_attributes, ball_x, ball_y);

    // オブジェクトを使用可能にするためにディスプレイ・パラメータを設定し、オブジェクト→タイルの1Dマッピングを使います
    REG_DISPLAY = 0x1000 | 0x0040;

    // ゲームのメインループになります
    uint32 key_states = 0;
    while (1) {
        // 現在のV-Blank・V-DrawSkipが終わるまではスキップします
        while(REG_DISPLAY_VCOUNT >= 160);
        while(REG_DISPLAY_VCOUNT < 160);

        // 現在のキー入力状態の確認(REG_KEY_INPUTは反転された値を格納しています)
        key_states = ~REG_KEY_INPUT & KEY_ANY;

        // ここでは、物理法則は固定のタイムスタンプではなくフレームレートに依存することに注意
        int player_max_clamp_y = SCREEN_HEIGHT - player_height;
        if (key_states & KEY_UP) { player_y = clamp(player_y - player_velocity, 0, player_max_clamp_y); }
        if (key_states & KEY_DOWN) { player_y = clamp(player_y + player_velocity, 0, player_max_clamp_y); }
        if (key_states & KEY_UP || key_states & KEY_DOWN) { set_object_position(paddle_attributes, player_x, player_y); }

        int ball_max_clamp_x = SCREEN_WIDTH - ball_width, ball_max_clamp_y = SCREEN_HEIGHT - ball_height;
        if ((ball_x >= player_x && ball_x <= player_x + player_width) && (ball_y >= player_y && ball_y <= player_y + player_height)) {
            // 物理上はあまり正しいコードではないですが、衝突判定コード
            ball_x = player_x + player_width;
            ball_velocity_x = -ball_velocity_x;
        } else {
            if (ball_x == 0 || ball_x == ball_max_clamp_x) { ball_velocity_x = -ball_velocity_x; }
            if (ball_y == 0 || ball_y == ball_max_clamp_y) { ball_velocity_y = -ball_velocity_y; }
        }
        ball_x = clamp(ball_x + ball_velocity_x, 0, ball_max_clamp_x);
        ball_y = clamp(ball_y + ball_velocity_y, 0, ball_max_clamp_y);
        set_object_position(ball_attributes, ball_x, ball_y);
    }

    return 0;
}

これで、基本的なゲームの完成です! これは本当のゲームボーイアドバンスでも動くでしょうか? 多分…、動きます。もし、私が間違えていなければ正しく動くはずですが、どこかでコードを間違えている可能性だって充分あるでしょう。

エミュレータで実行したゲームボーイアドバンス用ゲーム

結論

この投稿は、予想よりもずっと長くなってしまいました。この投稿で説明した詳細よりも、GBA開発にははるかに多くの事象があり、他のプラットフォームと同じ様に、面白い機能や癖があります。もっとGBA開発や、この投稿で紹介したデバイスの仕様を知りたい場合は、以下に記載するリソースがとても役に立つはずです。Nintendo’s AGB Programming ManualGBATEKCowBiteSpecTonc