BulletGBA の歩み
■ BulletGBA の歩み
GAMEBOY ADVANCE 上で動作する弾幕シミュレータ BulletGBA の開発の歩みを読み物風に。
組み込みの開発をしたいとか、BulletML を貧弱な環境に移植しようという人へ。
■ 開発の歩み
BulletGBA は短かい開発期間のわりには 2 回の大幅な作り直しを行っていて、以下のように開発言語がかわっています。
| 段階 | エンジン部分 | コンバータ | 歩み |
|---|---|---|---|
| 序盤(1) | C 言語 | エンジン部分の目処が立つ | |
| 序盤(2) | C 言語 | Ruby | コンバータの開発を経て C 言語での開発の問題点が発覚 |
| 中盤 | C++ 言語 | Ruby | コンバータロジックの目処が立つ |
| 終盤 | C++ 言語 | PHP | 完成 |
■ 序盤(1)
Dreamcast では g++ 自体を -fno-exceptions で作らないと腐ったコードを吐くという苦すぎる経験から、 当初は C 言語でエンジン部分のコードを開発しています。
そもそもは「GAMEBOY micro を縦向きに使ったら面白いのではないか」というアイディアから始まった BulletGBA ですが、 開発全体の難易度から見て一番の障壁になりそうな部分は GBA で弾幕描写が可能かどうかでした。
GBA では 128 個のスプライト表示能力しかなく、ぱっと見、とても弾幕が張れるだけのスペックはありません。 そこで、どれだけ同時スプライト数を増やすかが出来を左右すると考えて、 せめて 512 個の弾を表示できることを目標にエンジンの開発を始めました。
もともと Dreamcast 上で動く NES エミュレータ の開発でファミコン世代の巨匠たちから 数々のプログラミングテクニックを盗んでいたので、弾を 512 個表示するまでにそう時間はかかりませんでした。 エミュレータの開発時には、最適化の障害になるので敵だった HBlank を用いたスプライトダブラも使う側に立てば 便利な技術です。
この頃はとりあえず表示部分のみを開発していたので、 直進する弾幕しか対応していませんでしたし、動作スピードも随分と遅いものでした。 また、不可思議な現象が発生しても深追いをせずに出来るだけ簡単な手段で回避をしています。
例えば当初は -mthumb 付きでコンパイルすると HBlank が有効にならないという症状がありましたので、 -mthumb を潰してコンパイルするなどしています。
■ 序盤(2)
BulletML で記述できる弾幕には、もちろん直進以外の動作をするものも多くあります。
将来的に BulletML からコードを生成するにはコンバータを記述して自動的にさせるにしろ、 最初はどのような実装をすれば十分かわかりませんから、 コンバータに期待される動作を人間がシミュレートして、手動コンバートをしていきます。
最初は 2、3 弾を撃つだけの簡単なものから始めました。 その後、エンジンに加速度設定や角速度設定などの必要な機能を加えながら、 比較的単純な「エスプレイド アリスクローン」や「ぐわんげ 4 ボス」などを表現することが可能になりました。
その頃になると GBA のメモリが枯渇してきて、 ちょっと弾幕記述用の struct にデータを追加するとメモリから溢れるようになります。 そのころは GBA には IWRAM (32KB) と EWRAM (256KB) が存在するとは思いもよらず、 ひたすら IWRAM にデータ全体が収まるように苦慮しながら開発を進めていました。 (EWRAM の存在を知らなかったわけです)。
メモリ容量が限界ということは、コンバータが生成するコードはメモリ消費を限界まで抑えないといけないということです。 この条件から、コンバータのロジックは確定します。 (今では EWRAM を使えば、もしくは……と考えられますが、この時点では IWRAM が全てでした)。
基本的には、この時点でエンジンのロジックが固まって、 コンバータがどのような動作をすれば良いのかもわかりました。

■ 中盤
BulletML からコードを生成するコンバータの生成に移ります。
XML の処理が楽で使い慣れている Ruby を開発言語として選択して、コンバータの開発を始めました。 ところが、この時点でエンジンを C 言語で記述することについての問題が発覚します。
BulletML では弾のスピードや方向などの数値データに数式を使用することが出来ます。
<speed> 125 - ((30 * $rand)) </speed>
GBA のハードウェアスペック上、浮動小数点はもちろん使えるわけもなく、 弾幕の位置やスピード、方向などは固定小数点 (signed 16bit) で持っていたわけですが、 固定小数点の場合、乗除演算が特殊な処理となります。
typedef signed short int fixednum;
typedef signed int _fixednum_double; // Use in fixed_multiply
#define fixed_decimalbit 7
#define num2fixed(num) ((num) << fixed_decimalbit)
#define fixed2num(fixed) ((fixed) >> fixed_decimalbit)
fixednum
fixed_multiply(fixednum fixed1, fixednum fixed2)
{
_fixednum_double m = fixed1 * fixed2;
return m >> fixed_decimalbit;
}
上のように固定小数点を表現していた場合、 2 * 3 は fixed_multiply(num2fixed(2), num2fixed(3)) としなければいけません。
先の
<speed> 125 - ((30 * $rand)) </speed>
という数式は
num2fixed(125) - (fixed_multiply(num2fixed(30), fixed_rand()))
と変換することになります。 このような変換は数式をマジメに解釈しないことには不可能です。
もちろん、数式を解釈することは不可能ではないですし、実際に libBulletML では bison を使って解釈をしています。
とはいえ、数式解析は鬼門なので、出来れば避けたいところです。 ですが operator の再定義が出来ない C 言語を使っている限りは不可避でした。
ところが、 operator の再定義が可能な C++ で固定小数点を記述すると、 先程の数式は以下のように変換すれば十分です。
FixedPointNum(125) - (FixedPointNum(30) * FixedPointNum::random());
これならば、数式を解析することなく単純な文字列置換で変換が可能です。 複雑な演算子の優先度の解釈などもコンパイラにおまかせです。
そこで C++ で固定小数点ライブラリを書き直しました。 また、 C 言語よりも C++ 言語のほうがコンパイラの最適化を受けやすいので、 どうせ C++ を使うなら全体を書き直してしまったほうが有利です。 過去の苦い思い出から C++ には懐疑的でしたが gcc のバージョンがあがったからか、 サポートが手厚い arm アーキテクチャだからか幸い C++ でも問題は出ませんでした。
数式の部分の問題が解決できれば、 あとは BulletML で記述されたフローを 1 ステップ毎に区切って、それらの動作を関数コードに落とすだけです。 こういった処理は 2 パスで行うと簡便になることが多いですので、 BulletML を解釈しやすい形に変換するプリプロセッサと、 その結果からコードを生成するジェネレータに機能を分割してコンバータを開発しました。
この時点で大抵の BulletML は C++ のコードに変換することが出来るようになり、 コンバータによる細かいフィードバックを C++ のエンジンに反映させることを 繰り返すことで精度が良くなっていきました。
■ 終盤
実は中盤でプリプロセッサを記述している際に、 Ruby の REXML による XML パーサでは速度の問題が出ていました。 とにかく遅いのです。 500KB 程度の XML を読み込むだけでも 2 秒ほどかかり、 更に変換処理で時間がかかりと悲惨なものでした。
ともかく、コンバータのロジックが確定するまでは Ruby を使い続けて、 ロジックが完成したら捨てようと心に決めて開発を続けました。 Ruby を捨てるにして、代替言語は何にするかという問題がありましたので、 慣れや開発効率、実行速度を考慮しながら Python、PHP、JavaScript、C++ のそれぞれの評価を行いました。
個人の慣れが大きいと思いますが、上記の言語の中では圧倒的に PHP が有利でした。 もともと Web 用の言語ですので、コンバータの言語として使うのはどうかという思いもありましたが、 他に代替手段もありませんので Ruby 版コンバータの移植を始めます。
移植にあたってはデバッグを容易にするために、 プリプロセッサを複数段階に分割して、それぞれの結果を一時ファイルに残すようにするといった改良を加えましたが、 基本的には Ruby 版をそのまま移植する形となりました。
実用的な速度で動くコンバータが出来たことで、 大量の BulletML を一括して変換することが可能となりましたので、 C++ のエンジンに更に多くのフィードバックを与えることが出来ました。 この際に見えた点としては以下のようなものがあります。- 生成された C++ のコードが肥大化するためコンパイル後のバイナリのサイズが極端に大きくなってしまった。
- 素朴な数式変換では速度低下や誤差が無視できないレベルになるので、数式変換の際に定数畳み込みなどの処理が必須。
コンバータが生成するコードにおけるメモリ消費量を抑えるために、 コンバータでは <*Ref> による関数呼び出しや <repeat> による繰り返しを事前に展開する処理をしていました。 (このため、実行時にはこれらの処理でメモリを使う必要がなくなるわけです)。
当時はこれらの展開された部分について、それぞれコードを生成するという処理をしていたために コードの肥大化がおこっていました。 もちろん、それらをまとめることでコードがコンパクトになることは自明ですので、 変換対象となる XML 断片に対して md5 を計算して、同一の XML 断片については一つだけコードを生成するように 改良しました。
また素朴な数式変換についても、演算子の優先度を考えないで済むレベルの定数畳み込みについては実装を行い、 それらの改良でも十分な性能向上が図れるということ、またそれ以上の改良をしても徒労に終わりそうという感触を得られました。 (数式のうち、$rand に関係しない部分については完全に畳み込みが出来るので、大抵の場合は大丈夫なのです)。
この時点で、公開しても良いかなというレベルになってきましたので、 衝突判定の追加などの細かい調整を行って一区切りという形になりました。
Comments for This Page. Date: 2006-03-04 00:00 (JST)
