D言語 Advent Calendar 2015の16日目の記事です。
#はじめに
ここ数年、メイン開発言語としてD言語を採用した自作ゲーム(Windows用)をいくつか製作・公開しています。
そこで、自分がD言語を活用してどんな雰囲気でゲームを作っているかを共有しようかと思います。
といっても自分は野良開発者で、特にゲームプログラミングを複数人でやったことは一切無いので、変なことを書いている可能性があります。ご了承ください。
#D言語で気に入っているところ
ゲーム開発に採用する言語として、気にいっているD言語の性質は以下のようなものです。
- コンパイルが高速
- 実行速度が高速
- 文法がそれなりに好き
- ぜひ欲しいものが標準で組み込まれている(Fiberとか軽い乱数とか)
- C言語系のライブラリと連携しやすい
以下、適当に触れていきます。
#コンパイルが高速
ビルドが速いです。
また、ゲーム部分は動的リンクライブラリ(DLL)にし、プロセスを再起動せずに差し替えられるような構成にしているので、開発中は少しコードを書き換えてビルドするとプロセスを再起動せずにゲームを変更できます。
開発中は少し変更して試して少し変更して試して、の繰り返しなので、D言語の高速コンパイルが嬉しいところです。
ちなみに、内部動作に近い部分はC++、ゲーム固有部分でガシガシ変更していく部分はスクリプト言語、といったものがゲーム制作ではオーソドックスな構成だったりするらしいですが、こっからここまではC++、ここからはスクリプト言語、といった感じに言語を使い分けるのが個人的に煩わしかったため、スクリプト言語は使用していません。エンジン的な部分もスクリプト的な部分もD言語にしたかったということです。
#実行速度が高速
D言語をビルドしてできあがるバイナリの実行速度は、高速と言って良い水準のものです。
(いい感じのベンチマーク結果があるサイト貼ろうと思ったけど見つかりませんでした、でもまあ速いはずです)
ゲーム開発では描画等がボトルネックになりやすいものの、言語自体の実行速度が速いに越したことはありません。
#Fiber
標準で組み込まれているFiberが利用できます。簡単にいうと、関数を少しずつ実行するみたいなことができます。
例:
import core.thread;
import std.stdio;
void func()
{
// ★1
int num = 0;
num += 1;
writefln("%d回目", num);
Fiber.yield();
// ★2
num += 1;
writefln("%d回目", num);
Fiber.yield();
// ★3
num += 1;
writefln("%d回目", num);
}
void main()
{
auto f = new Fiber(&func);
f.call(); // ★1から実行される
f.call(); // ★2から実行される
f.call(); // ★3から実行される
}
Fiber、coroutine、マイクロスレッドといったものは、ゲーム製作としては嬉しい機能です。ゲーム内オブジェクトの動きを上から下に自然に書けるようになるためです。
(参考:http://www.slideshare.net/amusementcreators/ss-16019639)
実際には、敵の動きなどを作るときはFiberを直接使用するのではなく、適当にラップして、後述のようなAPIにしています。
#APIの雰囲気
D言語の文法や、前述のFiberを活用しつつ、こんな雰囲気で組めるようなAPIにしています。
ここにあるAPIを活用できるようにするライブラリ等を公開しているわけでは別にないのですが、D言語でどのように動きを書いていくのかの雰囲気をお伝えするために掲載してみます。
actが動きを指定するための万能メソッドみたいな感じで、functionやdelegateを渡せるようにしています。
// 敵の生成
auto enemy = Actor.create();
// 敵を生成して180Fかけて横軸に+200、縦軸に+200移動する
auto enemy = Actor.create();
enemy.act(&moveby, 180, 200, 200);
// 敵を生成して180Fかけて横軸に+200、縦軸に+200移動する(減速イージング)
auto enemy = Actor.create();
enemy.act(&moveby, 180, 200, 200, &ease_out);
// 敵を生成して180Fかけて5倍にする
auto enemy = Actor.create();
enemy.act(&scaleto, 180, 5.0);
// 敵を生成して180Fかけて5倍にする(減速+戻りイージング)
auto enemy = Actor.create();
enemy.act(&scaleto, 180, 5.0, &ease_outback);
上記の、動きを規定するためのmovebyとかscaletoとか、イージング用のease_outとか、メソッド内に組み込まれているものではなく外から渡せるただの関数なので、あとでいろいろ拡張しやすくなっています。新たな動きを追加したくなったら新たな関数を定義すれば良いわけです。
また、actには無名functionやdelegateも渡せるため、D言語のシンプルなdelegate構文と合わせて、このように書くことができます。
actor.act({ // delegate
// 180Fかけて右に150移動
actor.act(&moveby, 180, 150, 0, &ease_outback);
actor.sleep(180);
// 180Fかけて3.0倍に
actor.act(&scaleto, 180, 3.0, &ease_outback);
actor.sleep(180);
// 180Fかけて左に移動しながら1.0倍に
actor.act(&moveby, 180, -150, 0, &ease_outback);
actor.act(&scaleto, 180, 1.0, &ease_outback);
actor.sleep(180);
});
内部的に前述のFiberを活用し、上から下への流れで動きを指定することができます。ここではシンプルな例のみですが、あとはこの辺の組み合わせで複雑な動きを実現していきます。
#GCとの戦い
GC実行中の時間停止はゲームとは大変に都合が悪いので、意図しないタイミングでGCが発生するのは避けたいものです。
といっても自分はそんな真面目にGC対策しているわけではなく、起動時に使用する分のインスタンスは生成しておいてずっとそれを使い回す(ゲーム進行中は自力newはしない)とか、GC発生してほしくない時は一時的にGC.disable()しておくとか、そんな程度でやっていて、特に問題なく実行できている感じです。
GCのスキャン時間を減らすために、自力でメモリアロケートしてごにょごにょやったりすることもできるようですが、面倒なのでそこまでやっていません。
どこまで真剣にGC対策する必要があるかは、ゲームの規模や性質にもよるでしょう。
#高速な乱数
高速な乱数(Xorshift)が標準ライブラリにあるので嬉しいです。
周期はメルセンヌツイスターの方が優れていますが、Xorshiftの方が高速なので、ゲーム用途のほとんどでは、Xorshiftの方が都合が良い気がしています。
といっても乱数の処理がボトルネックになって遅くなるとかそういうことはゲームではあんまないと思いますが、いたるところで使用している乱数生成が高速なアルゴリズムを採用していることは、なんとなく心の平穏につながります。
#C言語系のライブラリと連携しやすい
DXライブラリやBox2Dなどを活用しています。
#まとめ
「D言語でしか実現できないこと」というものはありません。
それは、他の汎用プログラミング言語にも言えることです。
自作ゲームに採用する言語は、書いていて楽しいとか、自分に合っているとか、そういうのが最終的な利用になるかと思います。自分にとってのD言語もそうです。
気分重要。