「libBulletML」を組み込んでみる〜C言語編〜
モルド
「libBulletML」ってなに?
シューティングの敵弾をお手軽に定義できる弾幕記述言語BulletMLを扱うためのライブラリです。こちらで公開されております。
最近C++でゲーム作る方が増えてまいりまして、Cバリバリの私のようなプログラマは肩身が狭い毎日を送っております。
あと、意外と「BulletMLの組み込み方」を解説しているページは無いよな?無いですよね?あったらごめんなさい。
このページは「CでBulletMLを実装するぞ」というのが趣旨です。実装の都合上、どうしてもC++にしなきゃならん部分とかありますが、記述自体はCと同じ記述でいけますので、まあ、問題ないかと。
というわけで、主な対象言語は「C言語」です。そのため、このページを読むためには必然的にC言語の知識を必要とします。
ソースとか実行ファイルとかはWindows前提なんで、Linuxとかその他のプラットフォームの方は、それぞれの環境に置き換えて読んでくださいね。
(2004/02/09追記:d金魚さんにソースなどの誤りをご指摘いただきました。ありがとうございます。)
で?で?どんなの?
サンプルを作ってみました。まずはこれをダウンロードして、解凍してみてください。
解凍された諸々の中にある実行ファイル(sht_bulletml.exe)を実行してみてください。
敵が適当に弾をばら撒いていると思います。この弾の挙動は「BulletML」で定義されているのです。
で、その定義された弾を動かすためのライブラリが「libBulletML」というわけです。
以下、解凍されたソースファイル(srcフォルダ以下)とあわせて読むことをお勧めします。
実行ファイルと同じフォルダに、「.xml」という拡張子がついたファイルがあると思います。それが「弾幕定義ファイル」です。
っつーても、中身は単なる「XMLファイル」なわけですが。見たら解ると思いますが「HTML」に毛が生えた程度です。
どうやって組み込むの?
理屈が解ってしまえば、実装は結構簡単です。
ですが、その前に用意しなければならないものがあります。
「libBulletML」本体です。
公開元では、ソースコードしか公開されていません(たしか)。
なので、自分でライブラリを作成する必要があります。
ライブラリの作成方法などは、Webで調べてください。ググればすぐに見つかるかと思います。
っとその前に、もし「Windows環境化で無事にコンパイルできない!」という方がいらしたら、以下の事をチェックしてみてください。
「libBulletml.lib」作成の注意点。
1.「yggフォルダ」で「ygg.lib」を作成する。
「ygg_test.cpp」はプロジェクトに追加しないように。以下、注意点。
2.「ygg.h」の67行目
//#define __USING_SJIS__
のコメントを外す。
3.「ygg.h」の72行目
#if !(defined(__USING_ANSI__) || defined(__USING_SJIS__) || defined(__USING_EUC__) || defined(__USING_UTF8__) || defined(__USING_UNKNOWN__ ))
を、
#if !(defined(__USING_ANSI__) || !defined(__USING_SJIS__) || !defined(__USING_EUC__) || !defined(__USING_UTF8__) || !defined(__USING_UNKNOWN__))
とする。
4.「ygg.cpp」「ygg.h」の
#ifdef _DEBUG
・
・
・
#endif
をはずす。
5.「tynyxmlフォルダ」で「tynyxml.lib」を作成する。
6.その後、bulletmlライブラリを作成。上で作った「ygg.lib」と「tynyxml.lib」をプロジェクトに加える。
7.それでもだめなら、ソース自体が「EUC」なので「SHIFT-JIS」で保存しなおす。
これでもだめだったら、私に問い合わせてください。
(2004/02/07追記)
もしうまくコンパイルできない場合、SOX 読み込みを必要としていなければ「bulletmlparser-ygg.cpp」をコンパイルしなければ良いみたいです。
まだ試してないですが、上記のことを試してもうまくいか無い場合、試してみてください。
無事にライブラリを用意できたら、いよいよ組み込みです。
肝となるソースは、アーカイブに入っている以下の二つです。
src/bulletcommand.cpp
src/bulletml.cpp
「bulletcommand.cpp」は、libBulletMLからいろいろパラメータを受け取ったり設定したりする部分。
「bulletml.cpp」は、libBulletMLとアプリ(ゲーム)を結びつける部分、と思ってください。
以下、一つ一つ見ていきましょう。
「bulletcommand.cpp」を実装してみる
ココの実装で全てが決まってしまうわけですが。
正直、構造体のメンバ変数やら素の変数以外、変更点は無いかと思います。
変数の解説の前に、関数の解説をば。
BulletCommand::BulletCommand( BulletMLParser *parser,ACT *eactp ) : BulletMLRunner(parser)
凝った弾幕ではない、BulletML用語で「SimpleBullet」用の関数です。
通常は、弾本体のアクセスに必要な情報(主に構造体?)へのポインタを設定すします。
ここでは、弾の発生源(敵本体)のポインタを設定しています。
BulletCommand::BulletCommand( BulletMLState *state,ACT *eactp ) : BulletMLRunner(state)
凝った弾幕、BulletML用語で「ActiveBullet」用の関数です。「Ref系」の玉を定義すると呼ばれるっぽい。
それ以外は「SimpleBullet」と同様。
BulletCommand::~BulletCommand()
デストラクタだが、この例では使わない。
double BulletCommand::getBulletDirection()
現在の弾の方向を返します。
角度は、通常は「0〜359」で返されますが、コマンドによってはそれ以上になることがあります。
実際にこの角度が使われるときは、「0〜359」に丸められるので、気にしなくていいでしょう。
double BulletCommand::getAimDirection()
自機方向を返します。ここでは、自機方向を-179〜+180の間で返しています。
double BulletCommand::getBulletSpeed()
弾の現在の速度を返します。通常は、その弾の速度そのままを返せばいいでしょう。
double BulletCommand::getDefaultSpeed()
弾の標準の速度を返します。BulletMLで速度を指定しない場合は、この値が使われます。
double BulletCommand::getRank()
現在のランクを「0.0〜1.0」の間で返します。ここで返す値で、弾の難易度を決定します。
void BulletCommand::createSimpleBullet( double direction,double speed )
弾を発生させます。
ここでは「凝っていない弾」を生成します。例えば「一定方向にしか進まない弾」とかです。
void BulletCommand::createBullet( BulletMLState *state,double direction,double speed )
弾を発生させます。
ここでは「ある程度複雑な動作をする弾」を生成します。「Ref系」のコマンドを使うと呼ばれるようです。
int BulletCommand::getTurn()
現在のフレーム数を返します。「ゲームが1フレーム進むごとに加算する値」を返すのがいいでしょう。
ここで返される値は、<wait>命令や<term>命令で参照されます。
void BulletCommand::doVanish()
弾を自滅させます。この命令がきたら、弾そのものを消すようにしましょう。
void BulletCommand::doChangeDirection( double d )
<changeDirection>命令で呼ばれます。角度とXYそれぞれの速度を設定してください。
void BulletCommand::doChangeSpeed( double s )
<changeSpeed>命令で呼ばれます。基準速度とXYそれぞれの速度を設定してください。
void BulletCommand::doAccelX( double ax )
<accel>命令で呼ばれます。加速値Xを設定してください。
void BulletCommand::doAccelY( double ay )
<accel>命令で呼ばれます。加速値Yを設定してください。
double BulletCommand::getBulletSpeedX()
速度Xを返してください。
double BulletCommand::getBulletSpeedY()
速度Yを返してください。
サンプルで使われている代表的な変数は、以下の通り。
actp タスクへのポインタです。メンバは「custom.h」で定義されています。
bul_vel[] 弾の速度です。XYWが使われます。
bul_acc[] 弾の加速度です。XYが使われます。
bul_rot[] 弾の方向です。Wが使われます。
で、ここで使われている弾の移動方向と速度の設定方法ですが、
方向 〜 direction / ROTVAL
X速度 〜 sin( direction ) * (+vel)
Y速度 〜 cos( direction ) * (-vel)
こうなります。「direction」は、libBulletMLから「0〜359」で渡されるはずです。
「ROTVAL」は、ソースの上の方で宣言されています。解らない人は、おまじないみたいな物だと思ってください。
「bulletml.cpp」を実装してみる
「bulletcommand.cpp」の実装が終わったら、今度は「bulletml.cpp」の実装です。
ゲームのよっては、いくつか実装しなくてもいい関数があるかもしれませんが、念のため全部解説しておきます。
void BulletMLinit( void )
「BulletML」の初期化を行うのですが、ここでは自機タスクへのポインタ変数を初期化しているだけですね。
何か初期化する必要があれば、ここで初期化してください。
ゲーム中一度だけ呼ばれます
void BulletMLreadFiles( SINT bank,const char *fname )
「BulletML定義ファイル」を読み込み、初期化します。
実際には「libBulletML」の「BulletMLParserTinyXML()」を呼び出している(とは言わんか?)だけですが。
void BulletMLclose( void )
BulletMLの終了処理です。全定義弾幕に対して「delete」しています。
void BulletMLaddCtrl( ACT *actp,int num )
「BulletCommand()」を呼び出して、BulletML制御を追加します。
得られた戻り値を、タスクのメンバに保存しています。これは後に実行するための「bc->run()」の呼び出しに使用するためです。
void BulletMLexecCtrl( ACT *actp )
この関数で、実際にBulletMLを実行します。実行は追加した1制御ごと(例えば敵一体に一回)実行する必要があります。
また、ActiveBulletで生成された弾からも実行する必要があります。
BOOL BulletMLisEnd( ACT *actp )
BulletML制御が完了したかどうかを調べます。完了していたら「TRUE」を返します。
void BulletMLdelCtrl( ACT *actp )
BulletML制御を削除します。敵が消えたり、死んだりした場合は、必ず呼び出しましょう。
void BulletMLclrCtrl( ACT *actp )
弾本体を自滅させます。今回のサンプルでは「doVanish()」から呼ばれています。
ですが、このフラグ、使ってないんですよね。紛らわしくてごめんなさい。
void BulletMLaddNormal( ACT *actp,float *pos,double rank,double d,double spd )
「createSimpleBullet()」から呼ばれる、通常弾を生成する関数です。
基本は、発生したタスクに対して、必要な値を設定しているだけです。
void BulletMLaddActive( ACT *actp,float *pos,double rank,double d,double spd,BulletMLState *state )
「createBullet()」から呼ばれる、ActiveBulletを生成する関数です。
タスクに対して値を生成する以外に、新たにBulletML制御を生成します。
ここで生成された弾は、「BulletMLexecCtrl()」が呼ばれる必要があります。
float BulletMLgetPosX( ACT *actp )
BulletML制御を持つタスクの現在位置Xを取得します。
通常は敵本体ですが、弾から弾を生成するような場合、ActiveBulletの弾座標である場合もあります。
float BulletMLgetPosY( ACT *actp )
BulletML制御を持つタスクの現在位置Yを取得します。
通常は敵本体ですが、弾から弾を生成するような場合、ActiveBulletの弾座標である場合もあります。
void BulletMLsetPos( ACT *actp,float *pos )
BulletML制御を持つタスクの現在位置を設定します。
が、今見たら使ってませんでした。ごめんなさい。
void BulletMLsetShipAct( ACT *actp )
自機本体のタスクへのポインタを設定します。
BulletML制御は、このタスクを見て、弾の方向を決めたりします。
void BulletMLgetShipDist( ACT *actp,float *pos_x,float *pos_y )
自機本体から座標までの、各XYのオフセット座標を取得します。
当然、これを使用する前に「BulletMLsetShipAct()」が呼ばれている必要があります。
正直、ココの関数郡は何をやっているかがわかれば、実装は難しく無いと思います。
BulletMLを動かしてみよう
実装ができたら、動かしてみましょう。
今回のサンプルで唯一使っているのが「act_enemy.cpp」です。
おっとまたもやその前に。
動かすためには、前準備が必要ですね。
前準備は「start.cpp」でやってます。
「start.cpp」の「226〜230行目」を見てください。
/*
// BulletML初期化
*/
BulletMLinit();
BulletMLreadFiles( 0,"simple1.xml" );
BulletMLreadFiles( 1,"simple2.xml" );
BulletMLreadFiles( 2,"example1.xml" );
BulletMLreadFiles( 3,"example2.xml" );
これが前準備です。初期化して、必要なXMLをBulletMLに食わしてます。
「BulletMLreadFiles()」の第一引数は、BulletMLの「バンク番号」です。現状では128個定義できるようにしています。
違う弾を定義する場合は、ココの番号を変えてあげる必要があります。実際にBulletML制御を発生させる際に、このバンク番号を利用します。
それでは、動かしている方を見てみましょう。「act_enemy.cpp」の「116〜136行目」を見てください。
if( BulletMLisEnd(actp) )
{
BulletMLdelCtrl( actp );
switch((rand() % 4))
{
case 0:
BulletMLaddCtrl( actp,0 );
break;
case 1:
BulletMLaddCtrl( actp,1 );
break;
case 2:
BulletMLaddCtrl( actp,2 );
break;
case 3:
BulletMLaddCtrl( actp,3 );
break;
}
actp->wait = 60;
}
BulletMLexecCtrl( actp );
これは、「BulletML制御を追加し実行」している部分です。ランダムに4種類の定義から一つの制御を追加しています。
最初のif文は、「BulletML制御が終了していたら」という条件です。これがないと、終わっていないにもかかわらず無条件にBulletML制御を追加してしまいます。
とかなんとか書いておきながら、直後に「BulletMLdelCtrl()」なんて書いて、BulletML制御を強制削除しています。まあ、念には念を、ということで。
後の4つのcaseで、それぞれBulletML制御を追加しています。第二引数が、先程書いた「BulletMLのバンク番号」です。
最後の行が、「BulletML制御の実行」です。
次に、弾本体を見てみましょう。
同じソースの「156行目以降」が、弾本体のソースです。
移動の制御自体は、「176〜180行目」です。
BulletMLexecCtrl( actp );
actp->pos[X] += actp->bul_vel[X];
actp->pos[Y] += actp->bul_vel[Y];
actp->pos[X] += actp->bul_acc[X];
actp->pos[Y] += actp->bul_acc[Y];
最初の行は、「ActiveBullet」用のBulletML制御の実行。「SimpleBullet」の場合は、BulletML制御自体がないので、無視されます。
その後の2行は、速度値の加算。さらにその後の2行は、加速値の加算です。
実は、私はここで詰まってしまいまして。
私にとって「加速値」というのは、例えば以下のようなことだと思っていたわけです。
actp->pos[X] += actp->bul_vel[X];
actp->pos[Y] += actp->bul_vel[Y];
actp->vel[X] += actp->bul_acc[X];
actp->vel[Y] += actp->bul_acc[Y];
パッと見、違いが解りにくいですが、ようは「加速値は速度に足すもの」であり「現在座標に足すもの」と思っていなかったわけです。
そういう前提があるわけで、「なーんか妙に速度が速くなりすぎだよなー?」と思っていて、ちょっと考えれば「速度に足すから速くなりすぎるのは当たり前」という事実に、なかなか気がつかなかったわけです。アホですね。
しかも「実装実績のあるソースを見れば一目瞭然」だったのにもかかわらず、なかなかその事実に気が付かなかったってんだから救いようが無い。
みなさんは、そういうことが無いようにきをつけてください。アホは私1人で充分です。
与太話はともかく、弾を発生させる方も、動かす方も、実際はコレだけでできてしまいます。
どうですか?思ったより簡単に見えませんか?
最後に
以上、駆け足で書きましたが、どうでしょうか?少しはご理解の一助になったでしょうか?
BulletMLを使うと「多少凝った動きをする敵弾」が簡単に定義できます。正直、これをプログラムなりスクリプトなりでやろうとすると、結構大変なのではないでしょうか?
サンプルでは、故意に省いている部分があります。例えば「当たり判定」であるとか。そういうのは、実装の解説の邪魔になると判断したので、敢えて入れませんでした。
今回のサンプルは、前に公開した「SS2004のソース」から、実装の解りにくさソースの読みにくさなど、それなりに手を入れております。
そのため、思わぬ実行の不具合とかあるかもしれません。一応チェックはしていますが、当然チェックしきれていない部分は存在しますので、そのへんはメールでご連絡ください(できれば問題のあるXMLファイルを添付していただければ嬉しい)。
あと、実は「SS2004」の時点でうまく動かない、検証していない弾定義が存在します。そのへんの修正点などを指摘していただければ、さらに嬉しかったりします。
書いている内容に関してですが、「コレで万全だ」とは思っていません。所詮、「実装者=書き手」であるわけで、必ずしも読み手に解りやすい、理解しやすい、正確な内容だと、声を大にして言える訳もありません。
なので、ここに書いてある内容に関して、「ココが解らん」「もっと解りやすく書け」などの意見は、どしどし言ってください書いてください送ってください。そういう意見が伝わり次第、どんどん書き換えます。あと当然、「ここ間違ってます」などの意見も大歓迎です。
これを参考にしてくださった方々が、「BulletML」及び「libBulletML」を使って新しいシューティングを作ってくれれば、この上ない喜びであり、またこのページを書いた意義があったというものです。
権利とか
「BulletML」は「ABA Games」及び「長 健太」氏のものです
「libBulletML」は「Entangled Space」及び「shinichiro.h」氏のものです
今回のサンプルでは「DxLib」を使わせていただきました。「DxLib」は「DXライブラリ置き場」及び「山田 巧」氏のものです
このページへの意見、苦情、質問などはこちらまで。