「D言語へBulletMLを組み込む方法」の編集履歴(バックアップ)一覧に戻る

D言語へBulletMLを組み込む方法 - (2006/11/01 (水) 15:52:59) の編集履歴(バックアップ)


D言語へBulletMLを組み込む方法

これは何?

D言語で弾幕記述言語「BulletML」を使う方法を解説するページです。(Written by kenmo)

D言語コンパイラのバージョン

0.172でビルドします。

libBulletML

kenmoは昔、PythonでBulletMLを使うために、自分でBulletMLを解析するコードを書いたりしたのですが、
1ヶ月ほどかかったわりに、ref系の実装がうまくいかなかったりとか、不十分な実装でした。

まー、色々と勉強にはなりましたが、普通は「libBulletML」という自動解析してくれるライブラリを使用します。

libBulletMLのダウンロード

弾幕360度 - 配布物の話
から、「bulletss.zip」をダウンロードします。

この中で必要なものは、
  • bulletml.d(libBulletMLのインポートヘッダ)
  • bulletml.dll(libBulletMLのDLL)
  • test_bulletml.d(libBulletMLのサンプルコード)
  • SDLフォルダ(SDLのインポートヘッダ)
です。
あと、SDL関係で「SDL.dll」と「SDL.lib」が足りないので、
D - porting
から、「SDL」のリンクをクリックして、ダウンロードします。

bulletml.libの生成

さらに「bulletml.lib」が足りません。
これは、「implib」を使って、「bulletml.dll」から生成します。
「implib」は、
Digital Mars Download C and C++ Compilers
の「Basic Utilities(bup.zip)」とか、
Borland C++Compiler 5.5無償ダウンロード
の「Borland C++Compiler 5.5」に含まれています。

ここから、「implib」を「bulletml.dll」があるフォルダにコピーして、
implib bulletml.lib bulletml.dll
と、バッチファイルなり、コマンドプロンプトから実行するなりすると、「bulletml.lib」が生成されます。
  • 参考

test_bulletml.dのビルド

で、
dmd -ISDL test_bulletml.d SDL.lib bulletml.lib
とビルドすれば、ビルド完了、、、
とはならず、コンパイラのバージョンの違いでサンプルコードがビルドできません。

なので、以下の点を修正します。
  • 先頭のインポート文「import xxxxx;」を「import std.xxxxx;」というように「std」を付ける
  • 「import stream;」を「import std.cstream;」にする
  • インポートしているものを使っているところの頭に全て「std」をつける(例:string.toString()→std.string.toString())
  • 「stream.stdout」を「std.cstream.dout」にする

test_bulletml.dの実行

例えば、このようなまっすぐ弾を撃つだけのBulletMLを実行してみます。
<bulletml type="vertical"
          xmlns="http://www.asahi-net.or.jp/~cs8k-cyu/bulletml">
<action label="top">
 <fire>
  <direction type="relative">0</direction>
  <bullet/>
 </fire>
</action>
</bulletml>
これを「bml.xml」で保存すると、
test_bulletml.exe bml.xml
で、
getTurn 0
getTurn 0
getTurn 0
getRank
getBulletDirection
getDefaultSpeed
createSimpleBullet 0.000000,0.000000
getTurn 1
getTurn 1
end
となり、実行を確認できます。

libBulletMLの仕組み

libBulletMLの仕組みを図で表すとこんな感じです。

点線から右側がlibBulletMLの処理範囲となります。

流れとしては、
  1. BulletML(.xml)を元に、BulletMLParserTinyXMLオブジェクトを生成
  2. BulletMLParserTinyXMLオブジェクト内のBulletMLを解析
  3. BulletMLParserTinyXMLオブジェクトを元に、BulletMLRunnerオブジェクトを生成
  4. BulletMLRunnerオブジェクトを実行
という4つのステップを踏むことになります。

ここで、
  • BulletMLParserTinyXMLオブジェクト(Parser)
  • BulletMLRunnerオブジェクト(Runner)
という2つのオブジェクトがでてきました。

が、
「なぜ、BulletMLから、直接Runnerを作らないのか?」
と思うかもしれません。

この理由は
「XMLの解析は時間がかかるから」
という理由です。

例えば、先ほどの「まっすぐ弾を撃つ」BulletMLを、
新しい敵が出るたびに毎回解析していると、速度的に厳しいものがあります。

そのため、解析済みのParserから、Runnerの複製を作る仕組みであれば、解析のコストを減らすことができます。

registerFunctions

元の「test_bulletml.d」のソースを見てみます。
83行目にregisterFunctions関数を見てみると、BulletMLRunner_set_xxxxxxxxxx()という関数が、
ずらーっと並んでいます。

これは、BulletMLRunner_run()を実行したときに「コールバックされる関数」を登録しています。

この「コールバックされる関数」が、20行目の「extern (C) {」からの部分です。
この「extern(C)」はD言語で関数ポインタを使うときのお約束となっています。

libBulletMLを使う場合には、これらのコールバック関数の中身を実装しなければなりません。

コールバック関数の役割

とりあえず、実装の前に、これらの関数の役割を見ていきます。
これらは、大きく分けて3種類あります。
頭に「get」が付くもの、「do」が付くもの、「create」が付くもの、の3つです。
それぞれの役割は、
  • get⇒実行中の弾の状態を返す
  • do⇒実行中の弾の状態を変化させる
  • create⇒新しい弾を作る
です。

さらに細かく見ていくと、、

get系

  • getBulletDirection_⇒方向(角度)を返す
  • getAimDirection_⇒プレイヤーへの方向(角度)を返す。(狙い撃ち弾などに使う)
  • getBulletSpeed_⇒速さを返す
  • getDefaultSpeed_⇒デフォルトの速さを返す
  • getRank_⇒ランク(難易度)を返す
  • getTurn_⇒実行カウンタを返す
  • getBulletSpeedX_⇒X方向への速さを返す
  • getBulletSpeedY_⇒Y方向への速さを返す
  • getRand_⇒乱数を返す(0~1.0)

do系

  • doVanish_⇒弾を消す
  • doChangeDirection_⇒弾の方向を変える
  • doChangeSpeed_⇒弾の速さを変える
  • doAccelX_⇒弾をX方向に加速させる
  • doAccelY_⇒弾をY方向に加速させる

create系

  • createSimpleBullet_⇒アクションのない(まっすぐに飛ぶ)弾を生成する
  • createBullet_⇒アクションを起こす弾を生成する
となります。

弾オブジェクトの定義

いよいよ実装です。

まずは弾オブジェクトを定義します。
/**
 * 弾タスククラス
 */
class Task
{
public:
	bool   exist;     // 生存フラグ
	float  x, y;      // 座標
	float  ax, ay;    // 加速度
	int    turn;      // 実行(running)回数
	double rank;      // ランク(難易度)
	double rad;       // ラジアン(-π<rad<π)
	double speed;     // 速さ
	bool   topAction; // TopActionかどうか
	BulletMLRunner* runner; // Runnerオブジェクト
public:
	/**
	 * コンストラクタ
	 */
	this()
	{
		exist  = false;
		runner = null;
	}
};
Taskとかいう名前がついていますが、タスクシステムとは関係ないです。
Bulletという名前にしてしまうと、BulletMLとの区別がなんとなくつけにくいということで、
この名前にしてみました。

BulletMLを動かすには、
  • 生存フラグ
  • 座標
  • 加速度
  • 実行回数(カウンタ)
  • ランク(難易度)
  • ラジアン(弾の向いている方向)
  • 速さ
  • TopActionかどうか
  • Runnerオブジェクト
これらのパラメータが必要となります。

コールバック関数の実装

先ほどの「registerFunctions」のところにでてきたコールバック関数を実装してみます。

ただ、ここで注意なのが、これらのコールバック関数で実装するものは、、
今Runningしている(BulletMLRunner_run()を実行したとき)弾オブジェクト
でなければなりません。

つまり、
実行中の弾オブジェクトを参照できるグローバル変数
を用意する必要があります。
(ちなみに、ABAさんなどの実装では、シングルトンで参照しています)

そこで、このようなグローバル変数を用意します。
Task[] g_task; // 弾オブジェクト配列
int g_id;      // 実行中の弾オブジェクト配列番号
これにより、
g_task[g_id]
といったように、どこからでも実行中の弾オブジェクトが参照できるようになります。

get系

get系の実装は簡単です。
実行中の弾オブジェクトの値などを返すだけです。
double getBulletDirection_(BulletMLRunner* r) {
	return rad2deg(g_task[g_id].rad);
}
double getAimDirection_(BulletMLRunner* r) {
	int x, y;
	SDL_GetMouseState(&x, &y); // マウス座標で
	float dx = x - g_task[g_id].x;
	float dy = y - g_task[g_id].y;
	float rad = atan2(-dx, -dy); // xとyが逆で正負反転
	return rad2deg(rad);
}
double getBulletSpeed_(BulletMLRunner* r) {
	return g_task[g_id].speed;
}
double getDefaultSpeed_(BulletMLRunner* r) {
	return DEFAULT_SPEED; // 1.0
}
double getRank_(BulletMLRunner* r) {
	return g_task[g_id].rank;
}
int getTurn_(BulletMLRunner* r) {
	return g_task[g_id].turn;
}
double getBulletSpeedX_(BulletMLRunner* r) {
	return g_task[g_id].ax; // スピードじゃなくて加速度だよ
}
double getBulletSpeedY_(BulletMLRunner* r) {
	return g_task[g_id].ay; // スピードじゃなくて加速度だよ
}
double getRand_(BulletMLRunner* r) {
	return (cast(double)(std.random.rand() & 32767)) / 32767.0;
}

do系

do系は、弾オブジェクトに対する値の代入を行います。
void doVanish_(BulletMLRunner* r) {
	g_task[g_id].exist = false;
}
void doChangeDirection_(BulletMLRunner* r, double d) {
	g_task[g_id].rad = deg2rad(d);
}
void doChangeSpeed_(BulletMLRunner* r, double s) {
	g_task[g_id].speed = s;
}
void doAccelX_(BulletMLRunner* r, double x) {
	g_task[g_id].ax = x;
}
void doAccelY_(BulletMLRunner* r, double y) {
	g_task[g_id].ay = y;
}

create系

やっかいなのが、create系です。
ただ、やっていることは、stateのあるなしだけで2つとも中身はほとんど同じです。

ちなみにstateがある、というのは、
ref系の呼び出しによって、さらに弾が動くよ(Actionがrunningする)ということです。
stateがない、というのは、
<bullet/>
というように、Actionがないような弾のことです(まっすぐ飛ぶだけ)

実装のフローとしてはこんな感じです。
  1. SubActionの弾タスクを生成
  2. 移動情報などを設定
void createSimpleBullet_(BulletMLRunner* r, double d, double s) {
	// 親(現在実行中の弾タスク)の座標を元にSubAction弾タスク生成
	Task task = createTaskSubAction(g_task[g_id].x, g_task[g_id].y);
	if(!task) return;
	Task_setMovingInfo(task, d, s, g_task[g_id].ax, g_task[g_id].ay);
}
void createBullet_(BulletMLRunner* r, BulletMLState* state, double d, double s) {
	Task task = createTaskSubAction(g_task[g_id].x, g_task[g_id].y, state);
	if(!task) return;
	Task_setMovingInfo(task, d, s, g_task[g_id].ax, g_task[g_id].ay);
}
注意点としては、「生成される座標」と「加速度」は親の値を引き継ぐ、というところです。

Actionについて

説明が前後しているのですが、「Action」について説明します。
Actionとは、
「弾の移動方法や弾発射(BulletMLでは弾が弾を生成することができる)」
を定義したものです。
そして、Actionには、「TopAction」と「SubAction」が存在します。
<bulletml type="vertical" xmlns="http://www.asahi-net.or.jp/~cs8k-cyu/bulletml">
<action label="top">
 <fire>
  <direction type="relative">0</direction>
  <bullet/>
 </fire>
</action>
</bulletml>
例えば、こんなまっすく弾を撃つだけのBulletMLを生成したときに最初に生成されるActionが「TopAction」です。
そして、
 <fire>
  <direction type="relative">0</direction>
  <bullet/>
 </fire>
のところで生成されるのが「SubAction」です。


「TopAction」は敵オブジェクトが持っている砲台のようなものです。
これは厳密には弾ではありません。
実際にプレイヤーに影響を与える弾は、「SubAction」で生成される弾タスクである、といえます。

とりあえず、今日はここまで。。。

参考リンク

BulletML

解説

ツール

  • BulletML Demo ver. 0.21BulletMLがアプレットで動きます。これで動きを確認。
  • Bullet¬ML---BulletMLをジェネレートする便利なツールです。

コメント

なにかあればコメントをどうぞ
名前:
コメント:
記事メニュー
目安箱バナー