D言語研究室
・ドキュメント
D言語はC++と構文がかなり似てるので
doxygen[仕様書生成ツール]が使えるのではないかと思います。
→
D言語のソースにdoxygenを使う
・インターフェース
外部(DLL側etc)からもEXE側のライブラリを呼び出したいことがある。例)
スクリプトとしてのD言語
そこで、インターフェースを用意したいです。
ただし、外部から呼び出しもしないのにインターフェースだけ用意するとすべてのメンバ呼び出しがvirtualになってしまうのでまずいのかなぁと思います。
これについては、
以下、
かきかけ。
簡単そうなところから作っていきます。
まずはタイマークラスの制作から始めようと思います。
ゲームで必要となるタイマー関連のモジュール
・[ms]単位の個別タイマー
・fpsを指定すればあとはそのfpsになるように調整してくれるクラス
・fpsの計測クラス
タイマー取得関数はstd.dateにあるのですが、
> d_time getUTCtime()
> 現在の時刻をUTCで取得します。
> int TicksPerSecond
> この実装で1秒が何Ticksであるかを示す定数。少なくとも1000。
精度がかなり悪いです。おまけに後者は249が返ってきました。期待はしてなかったですけど、かなりションボリです。
やはりここは、timeGetTimeを呼び出す必要がありそうです。
timeGetTimeはmmsystem.dllを呼び出します。ソースレベルで
リンクするlibの指定ができるとよさそうですが、その機能はD言語にはありません。
この時点で、SDLのほうにもtimerクラスぐらいあるという情報をいただいたので(そりゃまあゲーム用のライブラリなら持っていて当然でしょうけれど)Linuxのことも考えて、まずはSDLのほうのtimerクラスの出来を調べるところから始めましょう。(実は私、SDLはド素人です。)
SDLについては、
DedicateD[英語]のほうでD言語用のportingライブラリを公開されていますので、こちらからdownloadしてきます。あと、実装の確認のためSDLのソースも本家から落としてきます。
さっそくSDLのtimerの実装(Win32)を見たところ、原則、timeGetTimeで、_WIN32_WCEシンボルが定義されているときだけ(WindowsCE環境)、GetTickCountを用いてありました。
これは正しい実装です。timeGetTimeの前に初期化段階でtimeBeginPeriod(1)があることも確認しました。これで、Win32環境で用いるならば1msの精度のタイマーが得られることが保障されていることがわかりました。timeBeginPeriod〜timeGetTimeを行なっていないならば、SDLとは見切りをつけたほうがいいんじゃないかと思っていた私にとって、これは満足のいく実装でした。
さっそくタイマーを実装。yaneSDK3rdのソースを持ってきて1時間ほどいじくる。難なく完成。これでfps設定〜調整、測定がこれを使うだけで出来るようになりました。
サンプル書いておきます。
import y4D;
import SDL;
int main(){
// y4d_timerのサンプル
// timerは独立している
Timer t1 = new Timer;
Timer t2 = new Timer;
FpsTimer fps = new FpsTimer;
fps.setFps(30);
int i = 0;
while(1) {
printf("%d , %d , skipped frame = %d , FPS = %d \n",
t1.get(),t2.get(),fps.getSkipFrame(),fps.getRealFps());
/**
このプログラムでは
秒間フレーム数30に設定。
しかし一回の描画処理に50ms必要。
よって、実描画は秒間20回ほどしか行なえない。
getSkipFrame() は 10 付近
getRealFps() は (waitFrameを呼び出した回数なので) 30付近を
指すものと思われる
*/
i++;
// タイマーt1,t2はそれぞれ独立して動作する
if (i==5000) { t2.reset(); }
if (i==10000) { i=0; t1.reset();}
if (fps.toBeSkip()) {
// このフレームは(立ち遅れるので)描画処理を省略すべき
printf("skipped\n");
} else {
// ここで描画処理を行なう
// 1回の描画に50ms必要だと仮定する。
SDL_Delay(50);
}
fps.waitFrame(); // これで30fpsを維持する
}
return 0;
}
|
次にゲームで必要となる数学関係のものを制作。
ゲームで必要となる数学関係のモジュール
・高速なsin,cos,atan
・MT法によるrand
・ゲームで使うようなcounter
・2次元ベクトルライブラリ(y4d_vector2D)
・3次元ベクトルライブラリ(y4d_vector3D)
MT法によるrandは誰かが用意していたようですが、OO風でないのが嫌なので、yaneSDK3rdのものを持ってきて10分ほどいじって完成。
以下、MT法による乱数発生クラスの呼び出しサンプル。
import y4D;
int main(){
Rand rand = new Rand;
rand.randomize();
for(int i=0;i<10;++i){
printf("%d \n",rand.get(100));
}
return 0;
}
|
高速なsin,cos,atan、それから丸め処理用の関数を用意。
以下、呼び出しテスト。
import y4D;
int main(){
SinTable t = new SinTable;
// (Cosθ,Sinθ)のatanを求めてみる:
for(int i=0;i<512;i++){
int x = t.cos(i,10000);
int y = t.sin(i,10000);
int at = t.atan(x,y);
// そのatanが元の値と一致するか調べてみる
at = ( at + 0x40) >> 7; // 丸め処理
// roundマクロを用いても同じ
// 例)
// at = instance round(int).RShift(at,7);
printf("i:%d x:%d y:%d at:%d\n",i,x,y,at);
}
return 0;
}
|
y4d_inputは、ゲームで必要となる入力関連のモジュールです。
joystickは、軸もボタン扱いする。軸入力もボタンとして扱います。
↑:0 ↓:1 ←:2 →:3 1つ目のボタン:4 2つ目のボタン:5 ...(以下ボタンのついている限り)
もっと多機能な取り扱いがしたければ、直接SDLを呼び出せば良いでしょう。
ループ毎にupdateが必要。isPushは前回押されていなくて今回押されていればtrue。 isPressは前回の状態は関係なく、今回押されていればtrueが返ります。
サンプルコード。
import y4D;
int main(){
JoyStick joy = new JoyStick(0);
while(1) {
joy.update(); // joystickの状態の更新
for(int i=0;i<joy.getButtonNum();++i)
printf("%d ",joy.isPress(i));
printf("\n");
}
return 0;
}
|
ジョイスティックがパソコンに繋がっていない場合、JoyStickにはNullDeviceとattachするので、エラーチェックなしにisPress,isPushを呼び出して構いません。
キー入力クラスです。SDLそのまま呼び出しているだけなので、何なく完成。
ただ、SDLのキー入力は、ウィンドゥに付帯するものなので、SDLのウィンドゥを初期化してある必要があるようです。(なんだか使いにくいなぁ..)
まあ、Windows以外のプラットフォームでは、ウィンドゥがないと入力出来ないこともあるでしょうから、仕方ないですか..。
キー入力統合クラスです。
キーボードの↑キーと、テンキーの8キー、ジョイスティックの↑入力を、一つの仮想キーとして登録することによって、それらのどれか一つが入力されているかを、関数をひとつ呼び出すだけで判定できるようになります。
全体的な流れは、キーデバイスの登録→仮想キーの設定としておいて、InputしたのちisPress/isPushで判定します。
実際に Key1,Key2,Key3,Key4 は、このクラスの応用事例なので、そちらも参照すること。
yaneSDK2ndの時代から存在するクラスで、大変使いやすいので、非常に気に入っています。
SoundはSDLを呼び出しています。wav,ogg,mid,riff,..etc..いろいろ再生できます。
サウンドを再生するときはSDL_mixerを用いるとずいぶん楽です。
SDL_mixerではmusicチャンネルと、chunkと呼ばれる8つのチャンネル、計9個のチャンネルがあり、それぞれを同時に再生することが出来ます。ファイルを再生するときに、チャンネルを指定しないといけないのです。そのへん、管理がなんだか面倒です。
サウンドを停止させるときも、チャンネルに対して停止させることしか出来ないので、そのチャンネルを再生中なのが自分自身なのかどうかをサウンドクラスは把握している必要があります。
MIDI再生できるのはmusicチャンネルのみだとか、MUSICチャンネルではRWopsが使えないだとか、SDLを用いたライブラリの設計者としては、いろいろ苦労させられる部分もあります。
とは言っても、自前でサウンドクラスをがりがり書くよりは遙かに楽なので、ささっと作りました。
その結果、再生は、これだけで出来るようになりました。
Sound s = new Sound;
s.load("1.ogg",-1);
s.setLoop(-1); // endless
s.play();
|
非常に楽です。あとは、これをcacheしたりするクラスを用意して、実際はそちらから使うと良いです。
SoundLoaderは読み込みcacheを実現するためのクラスです。たくさん読み込みをしたときに、一番最後にアクセスしたものから自動的に解放したりする仕組みを提供します。
クロスフェードのサンプル:
int main(){
SoundLoader ca = new SoundLoader;
ca.loadDefFile("filelist.txt");
/**
この↑ファイルの中身は、
1.ogg
2.ogg
と、再生するファイル名が書いてある
*/
ca.get(0).playFade(3000);
// 0番目のファイルを読み込み、そいつをフェードインさせながら再生
Timer t = new Timer;
while(t.get()<5000) {}
ca.get(1).playFade(3000);
// 1番目のファイルをフェードイン再生
ca.get(0).stopFade(3000);
// 0番目のファイルをフェードアウト再生
t.reset();
while(t.get()<5000) {}
return 0;
}
|
※ サウンドにおけるクロスフェードとは、片方のBGMがフェードしながら、もう片方がフェードインしてくることを言います。
ファイルを検索するときに、pathとして指定されたフォルダも調べて欲しいことがあります。また、ファイルを読み込むときに、zipを読み込むためのアーカイバや、自作のアーカイバや、暗号化フィルタなどを通したいことがあります。
商用ゲームならば、bmp(png?)が丸見えだとさすがに体裁が悪いので、暗号化フィルタを用意するのは当然として、暗号化フィルタを用意した結果、ファイルがシームレスに扱えないのでは困りものです。
そこで、yaneSDK4Dで使用しているクラスは、すべてファイルを読み込むときは、FileSysを用いて、こいつがpathの処理や、アーカイバの処理を受け持ってもらうことにしました。
ファイル名を連結するのにしても、駆け上がりパスなどを考慮しなくてはなりません。
1. / は \ に置き換える
2.左辺の終端の \ は自動補間する
"a"と"b"をつっつければ、"a\b"が返ります。
3.駆け上りpathをサポートする
"dmd/bin/"と"../src/a.d"をつくっければ、"dmd\src\a.d"が返ります。
4.カレントpathをサポートする
"./bin/src"と"./a.d"をくっつければ、"bin\src\a.d"が返ります
"../src/a.d"と"../src.d"をつっつければ"..\src\src.d"が返ります
5.途中のかけあがりもサポート
"./src/.././bin/"と"a.c"をつっつければ、"bin\a.c"が返ります。
6.左辺が ".."で始まるならば、それは除去されない
"../src/bin"と"../test.c"をつっつければ、"../src/test.c"が返ります。
char name1[] = r"../../test2\./../test3/test3\";
char name2[] = ".././test.d";
=> r"..\..\test3\test.d"
|
FileSysクラスは、こういうファイル名の連結もサポートします。
FileSys.addPath("..");
FileSys.addPath("sdl\src\");
FileSys.addPath("yanesdk4d/");
char[] name = FileSys.makeFullName("y4d.d");
./y4d.d(これはディフォルト) と ../y4d.d と sdl/src/y4d.d と
yanesdk4d/y4d.d を検索して、存在するファイルの名前が返る。
|
また、アーカイバとして、 FileArchiverBase 派生クラスを addArchiver で指定できます。その指定した Archiver も FileSys.read/readRW のときに必要ならば自動的に呼び出されます。
FileSys.addArchiver(new FileArchiverZipFile);
void[] data = FileSys.read("src/some.txt");
|
phobosのstd.zlibは、zip書庫をサポートしていないので自前で読み込む必要があります。→zip書庫の読み込み実装しました。
FileSysを作っているときに気づいたことを書き残しておきます。
・RWops
SDL_RWopsについては、こちら。
http://risky-safety.org/~zinnia/sdl/sourcetour/no5/
簡単に言うと、メモリ上に読み込まれたファイルを、ファイルっぽく扱うための仕組みです。
なぜこれが必要になるかと言うと、FileSysではアーカイバ等も扱うのですが、展開後のものは、単なるメモリです。それを再度ファイルに書き出して、それを読み込んだのでは2度手間なので、そういうことはしないのです。メモリを直接指定して、それを使いたいのです。
そのため、SDLではメソッド名の最後にRWとついているものがあって、そちらに対してはRWopsを渡すことが出来るので、これを利用します。
ところが、SDL_mixerのmusicチャンネルとかは、RWopsを受け付けてくれないのです。おそらく、MIDI再生などはファイルを指定でないと再生してくれない環境があるからだと思うのですが、これはかなり迷惑です。
そこで、ファイルが実在すれば、そのファイル名を返し、ファイルが実在しなければ(ただし、アーカイブとしては存在する)、メモリ内にそのファイルを読み込み、テンポラリファイルに書き出し、それを返すようなクラスが必要になります。
FileSys.TmpFileがそのための機構なのです。
ただし、テンポラリファイルを生成するための方法が、PhobosやSDLには存在しないので、環境依存の方法で作成するしかなくなってしまいます。Windowsのほうは、何とかしておいたんですけど、他の環境は..適当な実装でお茶をにごしてあります。(´Д`)
このクラスを通じて呼び出した関数は、関数の途中でresumeし、次に呼び出されたときに、以前の状態を再現することが出来ます。
詳しくは、こちらもどうぞ。→
マイクロスレッドの実装
ゲームでは必須のクラスと言えるでしょう。switching(次のスレッドへの切り替え)は、非常に高速で、Dコンパイラの場合、小さな関数ならばマイクロスレッド用のスタックも数百バイト程度で済むので、シューティングにおいて、敵一匹とか弾ひとつに対してひとつのマイクロスレッドを割り当てても構いません。
yaneSDK4Dでも、ゲーム用のフレームワークとしては、タスクシステムを実装した上で、マイクロスレッドを併用してプログラムを組んでいくことを想定しています。
※ タスクシステムについては、
http://yaneurao.hp.infoseek.co.jp/yaneSDK3rd/
のサンプル10か、さらに詳しい解説については、やね本2などを参考にどうぞ。
タスクシステム自体をクラスとしてあらかじめyaneSDK4Dに含めたほうがいいのかは微妙なところですが、とりあえずプロトタイプだけでも用意しておこうと思っています。
スレッドクラス自体は、std.threadかSDLのthreadを何らかのwrapを行なって使用するべきだと考えています。(まだ作りかけです)
ただし、同期オブジェクトは、synchronized文/修飾子がサポートされているので、わざわざそれ相当のものを作る必要もなさそうです。
yaneSDK3rdのときはソフトウェアでαブレンドや加色合成etc..を行ないましたが、もうそういう時代でもないでしょうから、3Dのハードウェアアクセラレーションで行ないます。つまりは、2D描画にも3D描画を用います。
ただし、2D描画を行なうときは3Dの知識を一切知らなくても使えるようにしておきます。
サポートするのは
a)転送時ジオメトリ変換
・等倍・拡大
・上下左右反転
・拡大縮小回転
・転送先自由凸4角形
b)転送の種類
・転送元がαつきサーフェース(抜き色付きも含む)
・ブレンド転送
・加色/減色合成転送
a)×b)の組み合わせすべて出来ます。ハードを用いるので、ブレンドだろうが、何だろうが、そんなに遅くはないです。(3Dのアクセラレーションをハードで行なえることが大前提ですが。)
あと、文字フォントの表示は、SDL_ttfを用いてtrue type fontの描画を行ないます。SDL_ttfは22ポイント以下のフォントサイズだと正しく描画できないので、n倍に拡大して描画して、それを自前で縮小するコードを書きます。
また、Windows時はWindowsのフォントフォルダをディフォルトで検索しに行くようにしてあります。
それから、文字リポジトリを用意し、一度描画したフォントはテクスチャ状態でキャッシュしていくようにしてあります。これにより、非常に高速に描画されます。
vector2D,vector3D,matrix2D,matrix3D,quotanion etc..を実装して、Cg(シェーダー言語)を呼び出すようにするつもりです。
CgはOpenGL用のものはportingされていますが、Windows上をメインのプラットフォームと考えているので、OpenGLの速度では少し耐え難いものがあります。よって、DirectX9にも対応させようと考えています。
(このへんは考えちゅう)
かきかけ
Last Updated : 2004-4-19
written by yaneurao
http://bm98.yaneu.com/dlang/