徒手空拳で始めるD言語+SDL2.0入門ライフゲーム編
はじめに
D言語のプログラミングは、とても楽しいものです。
この記事では、D言語を使うとどんな感じなのか、どう楽しいのか、実際にライフゲームを作っていく過程を通して、紹介します。
C/C++やJavaの経験が多少ある人なら、この記事をなぞって簡単にD言語版ライフゲームを作れると思います。
できあがりイメージ
こんなものが出来上がる予定です。
ソースはこちらに置いてあります。
使用ツール
徒手空拳と謳った通り、使用ツールはこれだけです。
- 本家DMD ver2.0.64
- SDL2.0
- IMPLIB (Windows限定。DLLからのlib作成に使用)
- 好きなエディタ
- 好きなシェルやコマンドプロンプト
SDLとは
[SDL](http://www.libsdl.org SDL2.0)は、有名なオープンソースのゲーム用ライブラリです。
基本的にはごく薄いC言語のラッパーAPIで、Mac・Windows・Linux・iOS・Androidといったプラットフォームに対応しています。
イベント処理・画面描画・ディスプレイ管理・サウンドなど、ゲームに必要なAPIが一通り揃っており、商用ゲームでの採用実績もたくさんあります。
ただし、提供されているAPIがプリミティブなので、リッチなゲームを作る場合は自分で色々とコードを書く必要があります。
今回作ろうとしているライフゲームのように、描画しかないような単純で気軽な開発にはとても向いていると思います。
D言語からC言語を使う
D言語はC言語との親和性が非常に高く、C言語インターフェイスを持っているライブラリを簡単に使うことができます。
具体的には、下記手順を踏めばC言語のライブラリがそのまま使用できます。
- 使いたい関数・構造体の宣言をD言語ソースに書く。
- 使いたい関数が入っているライブラリをリンクする。
関数宣言・構造体やenum・マクロをD言語に書き換えるのは、多少配慮は必要ですが、基本的にはコピペレベルの作業です。
ただ、面倒くさいのは確かなので、deimosというインポートライブラリのプロジェクトがあったり、htodというツールがあったりします。
しかし、SDL2.0はまだ出来合いのものが見当たらないし、必要な関数もSDLのごく一部なので、この記事では手作業で必要最小限のインポートを行いました。自分でこの記事の内容を試してみる場合は、私のソースをコピーすることをオススメしますが、いかにそのままポーティングできるかは見ておいてください。
野生の男さんから、Delerict3に入っていることを教えて頂きました。SDL1の頃お世話になったDelerictをなぜ当たらなかったんだ!
というわけで、この記事を試してみる皆さんは普通にDerelict3を使ってください。使い方については、野生の男さんの記事がとても参考になります。
とはいえ、まだポーティングが存在しない別のC言語ライブラリとか使いたい場合でも、必要な分だけの手作業でどうにかできるよ、という点は頭の片隅に置いておいてくださいね〜
DMDのインストール
最初にやることはDMDのインストールです。
大体のプラットフォームでインストーラが用意されています。それらで普通にインストールすれば大丈夫です。
verは最新の2.064.2を選んでください。(2013/12現在)
インストールが完了したら、ターミナルなりコマンドプロンプトでdmd
とタイプしてみてください。バージョンとコマンドラインオプションが表示されればインストール成功です。
SDLのインストール
このページからダウンロードできます。
Macの場合
開発者版をインストーラでインストールしてください。
Windowsの場合
開発者版zipをダウンロードします。後でDLLをコピーし、libファイルを作成します。
ディレクトリ構成
次に、ソースコードを置くためのディレクトリを用意します。記事の中では下記のようにします。
読者の皆さんは好きなようにやってください。
- ac2013/
- src/
- dlife/
- main.d # メイン関数など
- sdl/
- bindings.d # SDL関数のインポート
- utils.d # SDL関連ユーティリティの置き場
- lib/
- SDL.lib # SDLのlibファイル(windows用)
- dlife/
- life(.exe) # 生成される実行ファイル
- SDL.dll # windows用DLLファイル
- build-win.bat # ビルド用バッチファイル(windows)
- build-mac.sh # ビルド用バッチファイル(mac)
- obj # 出力ファイル置き場
- src/
最初の一歩
さて、いよいよD言語のプログラミングを始めます。
dmdが実行できることを確かめたら、次のようなソースを書きます。
module dlife.main;
/**
* メイン関数
*
* Params:
* args = コマンドライン引数
*/
void main(string[][] args) {
}
そしてビルドします。
dmd src/dlife/main.d -od./obj -oflife
無言で./life
ができるはずです。そして、./life
を実行しても無言でエラーが出なければ成功です。
SDLとのリンク
SDLを使うには、コンパイラのオプションでライブラリを指定する必要があります。
毎回タイプするのは面倒なので、簡単なシェル(バッチファイル)を作ります。
Mac版
#!/bin/sh
ROOT=.
DMD=dmd
SD=${ROOT}/src
SRCS="${SD}/dlife/main.d"
OD=${ROOT}/obj
OF=${ROOT}/life
INC=${SD}
OPTS="-od${OD} -of${OF} -I${INC} -unittest -L-framework -LSDL2"
${DMD} ${SRCS} ${OPTS}
開発者版SDL2がインストールされていれば、上記シェルを叩くと何事もなくlifeができるはずです。
(TODO: Windows版について追記する。libを作る手順も必要)
SDLのインポートを行う
さて次は、SDLから最低限のインポートを行い、実際にSDLが動く事を確かめます。
最低限動く——つまり初期化と終了です。
SDLのヘッダーファイルを見ながら、初期化・終了関数とenumを下記のようにインポートします。
この作業は面倒なので、私のGithubリポジトリからコピペしても良いと思いますが、C言語のライブラリをインポートをするとどんな感じなのかちょっと見てみてください。
module sdl.bindings;
extern ( C ) : // C++のexternに該当
enum SDL_INIT_TIMER = 0x00000001;
enum SDL_INIT_AUDIO = 0x00000010;
enum SDL_INIT_VIDEO = 0x00000020;
enum SDL_INIT_JOYSTICK = 0x00000200;
enum SDL_INIT_HAPTIC = 0x00001000;
enum SDL_INIT_GAMECONTROLLER = 0x00002000;
enum SDL_INIT_EVENTS = 0x00004000;
enum SDL_INIT_NOPARACHUTE = 0x00100000;
enum SDL_INIT_EVERYTHING = SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_JOYSTICK | SDL_INIT_HAPTIC | SDL_INIT_GAMECONTROLLER;
alias uint Uint32; // typedefに該当
int SDL_Init(Uint32 flags);
void SDL_Quit();
const(char)* SDL_GetError(); // constなcharへのポインタを返す関数
これで、main.dとビルド用シェルを下記のようにすると動きます。
module dlife.main;
import sdl;
import std.stdio;
/**
* メイン関数
*
* Params:
* args = コマンドライン引数
*/
void main(string[] args) {
// SDL初期化。最後に終了。
SDL_Init(SDL_INIT_EVERYTHING);
scope(exit) SDL_Quit(); // スコープを抜けるときに実行される
}
#!/bin/sh
ROOT=.
DMD=dmd
SD=${ROOT}/src
SRCS="${SD}/dlife/main.d ${SD}/sdl/bindings.d" # ここを追加
OD=${ROOT}/obj
OF=${ROOT}/life
INC=${SD}
OPTS="-od${OD} -of${OF} -I${INC} -unittest -L-framework -LSDL2"
${DMD} ${SRCS} ${OPTS}
これで再ビルドすれば、多分ちゃんと動きます。相変わらず何も起こらなければ成功ですが……。
D言語からC言語のライブラリを使うには、基本的にこれだけの手順で済みます。
後は、src/bindings/sld.dに必要な関数・enum・構造体をどんどん足していくだけです。
SDLのドキュメントを見ながら、必要に応じて追加していくのがオススメです。
ここまでのソースはこちら。
ちょっとだけユーティリティ関数も追加しています。
ウィンドウを表示してみる
さて、何も起こらないというのは詰まらない。せっかくSDLを使っているのだから、ウィンドウを表示してGUIであることをアピールしてみましょう。
import sdl.all;
import std.string;
enum WINDOW_WIDTH = 640;
enum WINDOW_HEIGHT = 480;
/**
* メイン関数
*
* Params:
* args = コマンドライン引数
*/
void main(string[] args) {
// SDL初期化。最後に終了。
enforceSDL(SDL_Init(SDL_INIT_EVERYTHING));
scope(exit) SDL_Quit();
auto window = enforceSDL(SDL_CreateWindow(
toStringz(args[0]), // ウィンドウタイトルはとりあえず実行ファイル名
SDL_WINDOWPOS_CENTERED, // 真ん中表示
SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH, // サイズ指定
WINDOW_HEIGHT,
SDL_WINDOW_SHOWN)); // 最初から見えるよ
scope(exit) SDL_DestroyWindow(window); // 終了時に破棄
SDL_Delay(10000); // 10秒間だけ見せてあげる
}
SDL_CreateWindow
がウィンドウを作る関数です。SDL_Delay
は指定ミリ秒待つ関数です。新しい関数も定数も全てsrc/sdl/bindings.d
に追加しています。
ビルドしてlife
を実行するとウィンドウが出ます。
どうでも良いですがlife
を実行ってすごいですね。
プチCTFEで女子力アップ
さて、今回インポートしたSDLのマクロ部分を見てみてください。
/* 元はこんなマクロ */
#define SDL_WINDOWPOS_CENTERED_MASK 0x2FFF0000
#define SDL_WINDOWPOS_CENTERED_DISPLAY(X) (SDL_WINDOWPOS_CENTERED_MASK|(X))
#define SDL_WINDOWPOS_CENTERED SDL_WINDOWPOS_CENTERED_DISPLAY(0)
// D言語版
enum SDL_WINDOWPOS_CENTERED_MASK = 0x2FFF0000; // 普通の定数定義
/**
* マクロ関数はテンプレート関数に。
* xはどんな型でもOK。
* 戻り値autoは、returnの式から型推論して自動的に型が決まる。
*/
auto SDL_WINDOWPOS_CENTERED_DISPLAY(X)(X x) pure nothrow @safe {
return SDL_WINDOWPOS_CENTERED_MASK | x;
}
// 定数定義。定数なのに上の関数が使える!
enum SDL_WINDOWPOS_CENTERED = SDL_WINDOWPOS_CENTERED_DISPLAY(0);
こんな風に、関数をさりげなく定数定義に使うことができます。そう、D言語ならね。
(最近はC++でもconstexprとかでできるようですが)
もちろんC言語でもマクロでできているのですが、D言語では普通の関数なので型安全だし、余計なカッコとか不要です。
このようなコンパイル時に関数が実行できる事を、D言語界隈ではCTFE(Compile Time Function Execution)と呼んでいます。
本当はもっと、催眠術だとか超スピードだとかそんなチャチなもんじゃあ断じてねえ、もっと恐ろしいものの片鱗を味わわせることができるのですが、今回は女子力がアップしているのでこの程度にとどめます。
イベント処理
ウィンドウが10秒見えるだけでは物足りない。もっと長い時間君を眺めていたい。
そんな願望に応えるために、イベントループを実装しましょう。
/// 秒間フレーム数(希望)
enum FPS = 60;
/// 1フレーム当たりミリ秒数
enum MILLS_PER_FRAME = 1000 / FPS;
/**
* メイン関数
*
* Params:
* args = コマンドライン引数
*/
void main(string[] args) {
// SDL初期化。最後に終了。
enforceSDL(SDL_Init(SDL_INIT_EVERYTHING));
scope(exit) SDL_Quit();
// ウィンドウを生成する
auto window = enforceSDL(SDL_CreateWindow(
toStringz(args[0]), // とりあえずプロセス名
SDL_WINDOWPOS_CENTERED, // 中央表示
SDL_WINDOWPOS_CENTERED, // 中央表示
WINDOW_WIDTH,
WINDOW_HEIGHT,
SDL_WINDOW_SHOWN)); // 最初から表示
// スコープ終了時にウィンドウを破棄
scope(exit) SDL_DestroyWindow(window);
// メインループ
for(bool quit = false; !quit;) {
// フレーム開始時刻
immutable startTicks = SDL_GetTicks();
// キューに溜まったイベントを処理
for(SDL_Event e; SDL_PollEvent(&e);) {
if(!processEvent(e)) {
quit = true;
}
}
// 次のフレーム開始時刻まで待つ
immutable elapse = SDL_GetTicks() - startTicks;
SDL_Delay(elapse < MILLS_PER_FRAME ? MILLS_PER_FRAME - elapse : 0);
}
}
/**
* イベントを処理する。
*
* Params:
* e = 発生したイベント
* Returns:
* 処理を継続する場合はtrue。終了する場合はfalse。
*/
bool processEvent(const ref SDL_Event e) {
switch(e.type) {
// 終了イベント
case SDL_QUIT:
return false;
// マウスクリック。終了する。
case SDL_MOUSEBUTTONDOWN:
return false;
// 上記以外。無視して継続
default:
return true;
}
}
SDLの裏ではイベントキューが用意されていて、そこにマウスやキーボード入力のイベントがどんどん入ってきます。
SDL_PollEvent
という関数で、そのキューのイベントを順に取り出しています。
processEvent
の中では、その取り出したイベントを処理しています。今回は単純にマウスクリックで終了するだけで、他は何もしません。
1フレームの間にイベントキューに溜まったイベントを処理したら、それに合わせて画面描画を行います。
そして、次のフレーム開始時間が訪れるまで待機します。
1/60秒ごとに上記処理を繰り返します。
FPSを数える
先ほどのソースでいきなり60FPSくらいでイベントループを回しているのですが、本当にそんなにループが回っているのかイマイチ不安です。
そこで、現在のFPSがウィンドウタイトルに出るようにします。
// FPS計測用
size_t totalElapse = 0;
size_t frameCount = 0;
// 先ほどのメインループ
for(bool quit = false; !quit;) {
/* 中略 */
// 次のフレーム開始時刻まで待つ
immutable elapse = SDL_GetTicks() - startTicks;
SDL_Delay((elapse < MILLS_PER_FRAME) ? MILLS_PER_FRAME - elapse : 0);
// FPS計測用に描画時間を集計
totalElapse += (elapse < MILLS_PER_FRAME) ? MILLS_PER_FRAME : elapse;
++frameCount;
// 1秒分描画したら、FPSを表示する
if(frameCount >= FPS) {
SDL_SetWindowTitle(window, toStringz(format("FPS:%f", FPS * 1000.0 / totalElapse)));
// カウンタ初期化
frameCount = 0;
totalElapse = 0;
}
}
これで多分58FPSくらい出ていることが分かると思います。まだ何もしていないので当然ですね。
画面描画する
さて、現状ではウィンドウにはよくわからないゴミが表示されていると思います。
そのままの君が好きというのもカッコ良いですが、現状ではアプリケーションとして死骸も同然であり、世間体にも関わるので、ちゃんと画面描画を行わせましょう。
SDL2の画面描画では、SDL_Rendererという描画用のオブジェクトが使用できます。
SDL_Rendererには、点・線・矩形といった簡単な図形描画用の関数が備わっています。
SDL1にはこんな便利なものはありませんでした。
ウィンドウ生成の直後に、まず画面をクリアします。
// レンダラー生成。ウィンドウを対象に描画。オプションは何かすごい速い指定。
auto renderer = enforceSDL(SDL_CreateRenderer(
window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC));
scope(exit) SDL_DestroyRenderer(renderer); // これもスコープ終了時に消す。
// 色を指定して画面クリア。真っ白にする。
enforceSDL(SDL_SetRenderDrawColor(renderer, Uint8.max, Uint8.max, Uint8.max, Uint8.max));
SDL_RenderClear(renderer);
そしてイベントループ内で点を描画します。
// メインループ
for(bool quit = false; !quit;) {
/* 中略 */
// 画面クリア
enforceSDL(SDL_SetRenderDrawColor(renderer, Uint8.max, Uint8.max, Uint8.max, Uint8.max));
SDL_RenderClear(renderer);
// 描画処理
enforceSDL(SDL_SetRenderDrawColor(renderer, 0, 0, 0, Uint8.max));
enforceSDL(SDL_RenderDrawPoint(renderer, WINDOW_WIDTH / 2, WINDOW_HEIGHT / 2));
// 描画結果表示
SDL_RenderPresent(renderer);
/* 後略 */
}
これで真っ白な世界の真ん中に、見えるか見えないか微妙なゴミが描画されました。
今までのゴミは意図しないものでしたが、今回は意図されたゴミであり、それゆえ美しいのです。
FPS計測とか見苦しかったので、勝手にstructにまとめたりしました。
ライフゲイムの宇宙
さて、思い出してほしいのですが、この記事の目的はライフゲームの実装でした。
ライフゲームとは、まあWikipediaを見ればすぐ分かるわけですが、Excelじみた宇宙で、
- 最初、セルに適当に生き物を配置
- 周囲のセルに2匹いれば生き残り
- 周囲のセルに3匹いれば、空セルの場合に生き物誕生。既にいれば生き残り
- それ以外は死亡
というルールをひたすら繰り返し適用していくという、そんな殺伐とした世界をシミュレーションするゲームです。
たったこれだけのルールで、生き物どうしが相互作用して出会いと別れを繰り返し、その結果非常に複雑で面白い現象が見られたりして、一人の夜でも寂しくありません。
楽しいです。
ライフゲームの世界の定義
プログラム的には、ライフゲームのやることの全ては下記の通りです。
/// ライフゲームの世界
class World {
unittest {
// 10 * 10セルの世界を生成
auto world = new World(10, 10);
// 座標(1, 1)にライフを配置
world.addLife(1, 1);
// 当初は生きていることを確認
assert(world.isAlive(1, 1));
// 次の時刻へ
world.next();
// ぼっちは死んだ。
assert(!world.isAlive(1, 1));
}
}
さて、さりげなくunittest
というものを書いてみました。D言語ではなんと、unittest
と書くとそれがそのままユニットテストになって、毎回起動時に実行されるようになります。(コンパイラのオプションで制御可能)
ただ、上のコードだけだとまだコンパイラも通らないので、最小限コンパイルが通るようにしてやります。
/// ライフゲームの世界
class World {
/* 略 */
this(size_t width, size_t height) {}
void addLife(size_t x, size_t y) {}
void next() {}
bool isAlive(size_t x, size_t y) {return true;}
}
多分これでコンパイルは通ると思います。そして実行すると、変なエラーが出ます。
$ ./life
core.exception.AssertError@dlife.life(27): unittest failure
----------------
# なんかゴチャゴチャ
これは、27行目でユニットテストが失敗しているよ、というありがたいお告げなのです。
というか、まだ全然メンバ関数の中身を書いていないので、失敗して当然です。
これはユニットテストがちゃんと失敗することを確かめているのです。
嘘とか強がりじゃありません。いわゆるひとつのテストファーストです。
テストが通るまで実装
さて、書いたテストが通るまで実装してみます。
class World {
/* 略 */
/// 世界の2次元動的配列
private bool[][] world_;
/// 世界を指定サイズで初期化
this(size_t width, size_t height) {
// 2次元の動的配列を生成。D言語ではこれでメモリが確保される。
world_.length = height;
foreach(ref row; world_) { // いわゆる拡張for文。型は推論される。refと書くと参照になり、内容の変更が可能
row.length = width;
}
}
/// 生き物追加
void addLife(size_t x, size_t y) {
world_[y][x] = true;
}
/// 生きているか確認
bool isAlive(size_t x, size_t y) {
return world_[y][x];
}
/// 中身はどうするか……。
void next() {}
}
さて、上記だとまだテストは通りません。next
メンバ関数の中身を実装して、ぼっちを死なせる必要があります。
この辺りを実装していくのがライフゲームの醍醐味なので、この記事も長くなりすぎたし、自分で考えてやってみてください。
私がそれとなく書いたコードはこちらです。なお、描画用にforeachで全生き物を巡回できるようにもしてあります。
世界の描画
ライフゲームの世界が実装できて、ちゃんと書いたユニットテストがちゃんと通れば、いよいよその結果を画面に描画します。
ライフゲームのロジック部分がちゃんとしていれば、何も難しいことはありません。下記のようなコードをmainに追加します。
// これは冒頭に追加
import std.random;
import dlife.life;
// 以下はmain内部
// ライフゲームワールドの生成
auto world = new World(WINDOW_WIDTH, WINDOW_HEIGHT);
// ランダムにライフを配置
foreach(y; 0 .. WINDOW_HEIGHT) {
foreach(x; 0 .. WINDOW_WIDTH) {
if(uniform(0, 2) == 0) { // 0, 1を返す乱数。つまり1/2の確率でライフが発生
world.addLife(x, y);
}
}
}
// 描画部分
// 全ライフの描画
foreach(x, y; world) {
enforceSDL(SDL_RenderDrawPoint(renderer, cast(int) x, cast(int) y));
}
それぞれの追加位置は、詳しくはコードをみてください。
動かした
上記まで書いたところで、私はライフゲームが動くところまでたどり着きました。おめでとう、私!
これでぼっちのクリスマスも寂しくありません。
しかし3.6FPS? 遅え!
この辺りを最適化しつつ、もっと面白い見た目になるようなこともやりましたが、それはまた別の物語……。
おわり
時間に追われて段々適当になる本記事に、最後までお付き合い頂いてありがとうございます。
他のACに投稿されている皆さんの記事が全体的にハイレベルだったので、バランスを取ろうと思い、カラスでも分かる超入門的なものを書いてみました。
ここでやっているのは本当にD言語とSDLの1%ぐらいの部分なのですが、それだけでも結構手軽に遊べそう、と騙されて頂ければ幸いです。
何より、記事でやった通り、クソでかくて重いIDEとかライブラリとか不要で、全部で数MBのコンパイラさえダウンロードすれば何とかなる言語なので、気軽にどんどん触れてみて欲しいです。
余裕があれば、また連休に続編を上げるかもしれません。続く……。
参考文献
-
ライフゲイムの宇宙
- 絶版だけど、私はこの冬に入手したので、自慢として貼っておきます。昔は7000円くらいした。
-
プログラミング言語D
- AC参加者のrepeatedlyさんと9rnsrさんが監修されています。これを1冊読めばD言語は大丈夫。
- SDLの本とかあるんだろうか……。でも公式サイトで大体はどうにかなりそう。