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_⇒アクションを起こす弾を生成する
となります。


弾オブジェクトの定義

いよいよ実装です。

とりあえず、実装例のリンクを貼っておきます。
libBulletML実装例
ただ、
  • 1つのBulletMLしか読み込めない
  • TopActionを消滅させる仕組みがない
という制限があるため、完全な実装ではありません。
ただ、その部分を実装することはそれほど難しくないと思います。

それでは、コードの解説です。
10行目あたりで、弾オブジェクトを定義しています。
/**
 * 弾タスククラス
 */
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系の実装は簡単です。
実行中の弾オブジェクトの値などを返すだけです。(103行目あたり)
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 * VEL_SS_SDM_RATIO; // ゲーム上での速さへの補正を戻す
}
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 * VEL_SDM_SS_RATIO, g_task[g_id].ax, g_task[g_id].ay); // VEL_SDM_SS_RATIO -> ゲーム上での速さに補正
}
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 * VEL_SDM_SS_RATIO, g_task[g_id].ax, g_task[g_id].ay); // VEL_SDM_SS_RATIO -> ゲーム上での速さに補正
}
注意点としては、「生成される座標」と「加速度」は親の値を引き継ぐ、というところです。

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」で生成される弾タスクである、といえます。

全体の流れ

説明が前後しまくりですが、全体の流れを追ってみます。
297行目あたりからです。

まずはTopActionを生成

createTaskTopAction(320, 120);
でTopActionを生成しています。
本来なら、TopActionを持っている敵が死んだときに、その敵が持っているTopActionを消滅させる必要がありますが、
このコードでは未実装です。
(実装方法としては、TopAction生成時にその生成IDを返す、などの方法があると思います)
あと、ここでは未実装ですが、どのBulletMLを使うか? という情報も必要になります。

弾タスクを回す

307行目あたりの部分で、弾タスクを動かしています。
foreach(i, task; g_task)
{
	if(!task.exist) continue;
	// 更新
	g_id = i;           // ①実行中のタスクに登録
	Task_running(task); // runner実行
	if(!task.exist) continue;
	Task_move(task);    // 移動
	// 描画
	drawRect(screen, cast(int)task.x-2, cast(int)task.y-2, 4, 4, 0xff);
}
  1. 実行中の弾タスクのIDを登録する
  2. runnerを実行
  3. 移動量などに基づいて移動
という流れになります。

Task_running

Task_runningは、Runnerオブジェクトを実行する処理です。
75行目あたりになります。
void Task_running(Task task)
{
	if(!task.runner) return;
	if(!BulletMLRunner_isEnd(task.runner))
	{
		// runner実行
		BulletMLRunner_run(task.runner);
		task.turn++; // (※1)実行カウンタを進める
		if(!task.exist)
		{
			Task_vanish(task); // (※2)消滅
		}
		return;
	}
	// runner終了
	Task_deleteRunner(task);
	if(task.topAction)
	{
		// TopActionのみ最初に戻る(※3)
		task.turn = 0;
		task.runner = BulletMLRunner_new_parser(g_parser);
		registerFunctions(task.runner);
	}
}
まず、runnerをnullチェックしています。
runnerがnullであれば、SimpleBullet(Actionのない弾)なので、何もしません。

注意点は、※1のところで、runnerを実行したあとは、turnという実行カウンタを進めます。
これをしないと、「wait」命令や「term」命令が正常に動作しなくなります。
また、※2のところで、existという存在フラグをチェックして、消滅させています。
これをしないと、「vanish」命令が(場合によっては)正しく動作しなくなります。

最後に、※3ですが、TopActionに限り、BulletMLRunner_isEndがtrueを返した(Actionが終了した)
場合に、Parserを作り直して最初に戻しています。
これにより、弾を撃ち終えても、すぐにまた撃ち直すようにしています。
ただし、この撃ち直しを敵オブジェクトから制御したい場合には、この処理は必要ありません。

Task_move

Task_moveは、実際の弾の動きの制御をしているところです。
void Task_move(Task task)
{
	task.x += -sin(task.rad) * task.speed;
	task.y += -cos(task.rad) * task.speed;
	task.x += task.ax;
	task.y += task.ay;
	if(task.x < 0 || 640 < task.x || task.y < 0 || 480 < task.y)
	{
		Task_vanish(task); // 消滅
	}
}
まあ、そのまんまですね。
ただ、注意点としては、加速度(ax/ay)は、速度に足しこむのではなくて、
座標に足しこむものである、ということです。

その他注意点

これで実装についての説明は終わりです。

libBulletMLはちゃんと動かすまでに、それなりに時間がかかるので、
結構しんどかったりします。

kenmoは、issikiさんのページを参考にしたのですが、それでも2~3日ほどかかりました。

ここの情報を参考に、もう少し簡単に実装してもらえれば嬉しく思います。

最後にkenmoがハマッた点をピックアップしておきます。
  • 変数の初期化し忘れで「Nan」が入って動かなくなった⇒rank変数の初期化を忘れてました。。。
  • 速度を求めるところで、sin/cosが逆に入っていた⇒x=cos(rad), y=sin(rad)と思っていたのですが、BulletMLだと、x=-sin(rad), y=-cos(rad)なんですよねー。
  • TopActionとSubActionの違いを考えてなかった
  • SimpleBulletとActiveBulletの違いが分からなかった
といった感じです。

以上、D言語でのBulletML組み込み方法でしたー。

参考リンク

BulletML

解説

ツール

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

コメント

なにかあればコメントをどうぞ
名前:
コメント: