上記の書籍を参考に、GBA上でミニゲームを作ってみました。ただ、ゲーム作りは手段であり、目的は低位層プログラミングに対する理解を深めることです。
まだ書き途中であり、適宜補足していく予定です。
バイナリと、一部ソースコードを置きます。参考書籍からほとんど手を加えていないようなソースコードは、載せていません(ライセンスが不明のため)。著者のサイトより、ダウンロードしてください(2007/07/29現在、履歴部分からダウンロードできます)。
前述の事情により、ソースファイルは歯抜けになっています。そのため、あくまで参考程度としてください。このソースファイルからバイナリをビルドすることは、あまり考慮していません。実行してみたい場合は、バイナリ版を落としてください。←ソースもバイナリもまとめて下記アーカイブ内にあります。
&attachref(mirmo_dance_ss.jpg,nolink);
0x0000000 | BIOS ROM |
0x2000000 | 外部RAM |
0x4000000 | I/Oレジスタ |
0x5000000 | パレット |
0x6000000 | VRAM |
0x7000000 | OBJ属性 |
メモリマップがないと、始まりません。とりあえず、ざっくりと開始アドレスだけ書きましたが、より詳しくはTOBY Soft Wikiをご覧ください。
作成したコードは、外部RAMに置くことになります。このさい、実行時に各オブジェクトを、メモリのどの位置に配置するか設定するのが、リンカスクリプトです。オブジェクトファイルのセクションごとに、配置場所を指定します。
実行ファイルはELF形式となりますが、とりあえず覚えておくべきセクション名は、次のとおりです。
.text | メインとなる命令コード |
.data | 初期化済変数 |
.bss | 未初期化変数 |
.rodata* | 定数 |
ELF形式(参考:ELFSection)の実行ファイルに対して、下記のようにコマンドを実行すると、利用されているセクションの一覧が見られます。
$ objdump -h a.out
gcc.ls
OUTPUT_ARCH(arm) SECTIONS { .text 0x2000000 : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } .rodata : { *(.rodata*) } }
簡単なリンカスクリプトの例です。開始アドレスを0x2000000と指定すると、あらゆるファイル(ワイルドカード*による)の.textセクションを、順番に配置していきます。その後、.data、.bss、.rodata*セクションと続きます。
main関数を備えたCソースをコンパイル、アセンブルしたオブジェクトだけでは、実行するのに問題があります。それは、CPUがmain関数のアドレスがわからないためです(先頭にある機械語を順次実行するとうまくいかないのは、何故だろう。何となく想像は付くけど、厳密に答えられない)。ソースコードでは、main関数を先頭に書いたつもりでも、アセンブルした後では、様々な宣言などにより、main関数のアドレスは後ろにずれます。そこで、スタートアップルーチンと呼ばれるアセンブラファイルを用意し、main関数アドレスへのjumpを指定します。そして、そのスタートアップルーチンのオブジェクトファイルを、先頭に配置します。アセンブラで書かれたコードなので、余計なコードが先に入り込むことがありません。では、このスタートアップルーチンが、先頭に配置されることはどうやって保証されるかというと、ビルド時の入力順です。
$ ld-arm -o mirmo_dance.out -T gcc.ls crt0.o mirmo_dance.o \ `gcc-arm -print-libgcc-file-name`
試しに、crt0.o(スタートアップルーチン)とmirmo_dance.o(main関数を含むオブジェクトファイル)の順序を入替えると、実行開始できない実行ファイルが出来上がるはずです。
以下は、crt.sの例です。
.text bl main
"bl main" で、メイン関数アドレスへ飛びます。
個人的にすっきりしないのは、なぜmainはextern指定なしで使えるの?
main関数の前処理や後処理もできます。具体的には、割込み処理先の指定があります。以下は、crt0.S(拡張子Sが大文字なのは、cpp処理を有効にするため)です。
#define INT_VECTOR 0x03007FFC // 割り込み処理アドレスの格納先 .extern int_handler .text ldr r0, =int_handler // レジスタにユーザー割り込み処理アドレス ldr r1, =INT_VECTOR // 割り込み処理アドレスの格納先 str r0, [ r1 ] // r1 に r0 の値を格納 bl main loop: b loop // 無限ループ
GBAゲーム開発における、基本的な要素を列挙します。
GBAの画像表示モードには、いくつか種類があります。色数が多いモードは、レイヤー化ができないなど、いろいろ制限があります。そこで、タイルモードと呼ばれる、8x8の画像配置による方法を使います。タイル表示というと、昔懐かしいRPGの2D移動フィールドがわかりやすいと思います。もちろん、普通の一枚絵でもタイルモードは使えます。その絵を8x8になるように分割し、それぞれの画像を個別のタイルとして定義すれば良いのです。手間がかかりますが、タイルモードでないと、受ける他の制約が大きいので、タイルモードを利用します。
ここのモジュールは、そのまま使っています。
背景画像ではなく、アクションゲームのキャラクターのように、ドット単位で動かしたい画像があります。8ドット単位の動きでは、あまりに粗雑すぎます。そこで、スプライトという仕組みがあります。8x8のタイルを組み合わせて、8x16、8x32、64x64などのスプライトと呼ばれる画像オブジェクトを作成できます。このスプライトについて、座標などの属性を指定できます。
ここのモジュールは、そのまま使っています。
画像は、1ドットのRGBを2バイトで保存する形式になっています。そのため、通常の画像形式から、GBA用に変換する必要があります。まず、画像編集ソフトで作成した画像を、256色以下に減色して、RAW形式で保存します。そして、パレットファイルを別途保存します。このとき、パレットは最初の1色が透明色(黒)、2色目が白になるようにしてください。そうしないと、透過部分がおかしくなったり、画像自体が大きく乱れたりします。
RAW形式の画像ファイルは、参考書籍に付属したmaketileプログラムで、Cソースのcharの配列に変換します。また、パレットファイルは、同じく付属のmakepalプログラムで、変換します。
そもそもデフォルトでは文字がありません。そのため、フリーのフォントを使い、GBA用の画像データに変換します。各文字は、タイルとして扱います。描画開始位置と、書きたい文字列を指定することで、文字を描画することができるモジュールが、参考書籍にて用意されています。
ここのモジュールは、そのまま使っています。
スタートアップルーチンで設定した割り込み処理です。後述するタイマのオーバフローや、キー入力、DMA完了通知など、ハードウェアからの要求によるものがほとんどです。割り込み処理は、メインの処理より優先度の高い別の流れで処理されます。ユーザープログラムでは、専用のハンドラ関数を用意し、そのなかに処理を記述します。割り込み通知を受けるには、あらかじめ割り込みを有効にする設定をする必要があります。マスタと各割り込み個別の設定があります。
GBAでは、合計4つのタイマを使うことができます。これらは、連結してより長い時間を計測することもできます。
タイマは、カウンタを指定された初期値から順次加算し、65535もしくは0に達したとき(つまりオーバーフローになったとき)、割り込みが発生します。このオーバーフロー割り込みが発生するまでの、カウンタアップは、1,64,256,1024サイクル(いずれかを設定できる)と決まっており、さらに初期値を設定することで、オーバーフロー割り込みが発生するサイクル数を調整できます。
なお、タイマはサウンド関連のところでも利用するので、予め考慮する必要があります。
キー入力は、割り込みで通知されます。キーの状態は、レジスタの値を見て判断します。どのキーが押されたら、割込み通知するか、を設定できます(or条件、and条件も)。
サウンドは、画像のときと同じように、WAVEファイルを、GBA用に変換します。変換プログラムは、参考書籍付属のmakegbaを使います。WAVEデータを、タイマ0もしくはタイマ1(いずれかを選択)のオーバーフローごとに、FIFOバッファに送ります。同時に、FIFOバッファに入っていたデータは、PVM回路という音を出す回路に渡されます。つまり、このタイマの間隔が音の標本周波数(サンプリングレート)に合わさることで、滑らかに音を再生できるのです。
とはいえ、FIFOバッファへのデータ転送をずっと続けるのは、CPUパワーをかなり消費します。そこで、DMA転送を使います。DMA転送を使えば、CPUはデータの転送元と転送先をDMAコントローラーに教えるだけで、良いのです。FIFOバッファの転送を終えるたびに、割り込み通知がくるので、どれくらいデータが転送されたか把握することもできます。必要な量を転送し終えたら、タイマを止めましょう。バッファ中のゴミが流れ込むと、耳障りな音が流れます。