Hatena::ブログ(Diary)

mirichiの日記 このページをアンテナに追加 RSSフィード

2016-12-12

Rubyの標準添付ライブラリFiddleでゲームプログラミングする

この記事はRuby Game Developing Advent Calendar 2016 - Adventar の12日目です。11日目は土屋つかささんのrubyゲーム開発にユニットテスト/テスティングフレームワークを導入する(2)【実践編】 - 土屋つかさのテクノロジーは今か無しかでした。Ruby...ゲーム...ユニットテスト...うっ、頭が...

まあ、ゲーム関連でのユニットテストは難しい問題で、スピーカから出てくる音をマイクで拾って判定するわけにもいかないので、とりあえず全ての機能をテストすることができないのは明らかである。っていうかそもそも外部機器を扱わない部分を考えても、例えばDXRubyのSpriteで言うところのSprite#x=で設定した値がSprite#xで取得できることはテストできても、それがSprite#yで取得する値に影響を与えていないこと、などをテストし始めると現実的には不可能な量になってしまうので、つまりは全てのユニットテストにおいて取れる戦略はたった一つ、「できるテストをやる」。ああ、耳が痛い。

ちなみに高尾さんのSDLを使ったDXRuby互換ライブラリdxruby_sdlではRSpecを使ってテストを書いていて参考になる。ここではSoundは音声ファイルを読み込んで再生して例外が出ないこと、をテストしている。なるほど。

さて、今回はRubyの標準添付ライブラリFiddleでゲームプログラミングということで、WindowsのDirectXを叩く方法を紹介する。DirectXはCOMインターフェイスなのでRubyからは普通の方法では呼べないが、Cインターフェイスが存在するのでそれに沿った扱い方で呼ぶことが可能である。

前提

基本的なWindowsAPIはわかるものとする。Fiddleの使い方はわかるものとする。今回はDirectX9をテーマにするので、DirectX9の扱い方はわかるものとする。COMインターフェイスも基本的なところはわかるものとする。Cもわかるものとする。当然ながらRubyもわかるものとする。(無茶なテーマだコレ)

COMインターフェイスのCからの呼び出し方法

世の中にはCで書かれたゲームライブラリは地味に多い。COMインターフェイスの第一言語はC++なのでDirectXの解説記事はほぼC++だが、COMはCからも呼べるので、例えばDXRubyやSDLはCで書かれているし、GLFWはジョイスティック入力にDirectInputを使っていてこれもCで書かれている。

Cから呼ぶ前に、その構造を理解しておいたほうがよい。mingwに含まれるヘッダファイルunknwn.hにCOMインターフェイスの基底クラスIUnknownの定義が書かれている。CとC++で変わるが、Cではこうだ。

  typedef struct IUnknownVtbl {
      HRESULT (WINAPI *QueryInterface)(IUnknown *This,REFIID riid,void **ppvObject);
      ULONG (WINAPI *AddRef)(IUnknown *This);
      ULONG (WINAPI *Release)(IUnknown *This);
  } IUnknownVtbl;
  struct IUnknown {
    CONST_VTBL struct IUnknownVtbl *lpVtbl;
  };

つまりCOMオブジェクトというのは関数ポインタテーブル(仮想関数テーブル)へのポインタ1個だけを持つ構造体である。このlpVtblと関数ポインタを経由してIUnknownオブジェクトの3つの関数を呼び出すことができる。C使いはC++が内部でやってることを自前でやりな、ということだ。

なので必然的に、

  lpiunknown->lpVtbl->Release(lpiunknown); // lpiunknownはstruct IUnknown*型変数

のような形式で呼び出す。第1引数はthisポインタで、C++では暗黙の引数として渡されるのでDirectXのリファレンスには書かれていないが、Cで呼ぶときは全ての関数の第1引数に自身が必要となる。また、この呼び出し方が冗長なのでC用にマクロが提供されている。

#define IUnknown_QueryInterface(This,riid,ppvObject) (This)->lpVtbl->QueryInterface(This,riid,ppvObject)
#define IUnknown_AddRef(This) (This)->lpVtbl->AddRef(This)
#define IUnknown_Release(This) (This)->lpVtbl->Release(This)

ちょっとだけラク?かな?

FiddleによるCOMインターフェイスの扱い方

COMオブジェクトはライブラリ側で生成されて返されるため、Cでは構造体のポインタとして扱う。DirectX9の関数Direct3DCreate9を呼ぶ場合はFiddleでこのように関数を定義する。

module D3D9
  # 省略
  extern "void* Direct3DCreate9(unsigned int)"
  # 省略

これで関数を呼び出すとFiddle::PointerインスタンスとしてCOMオブジェクトが得られる。

d3d9 = D3D9::Direct3DCreate9(D3D9::D3D_SDK_VERSION)

このポインタが参照している先のメモリに入ったアドレスから更に先を辿ることになるので、まずFiddle::Pointer#ptrを呼んで、Cで言うところの*d3d9を読み出す。このメソッドでは読み出した結果はFiddle::Pointerインスタンスになる。

lpvtbl = d3d9.ptr

次にこのアドレスから呼び出したい関数のアドレスを算出する。

func_adrs = (lpvtbl + i * Fiddle::SIZEOF_VOIDP).ptr

Fiddle::SIZEOF_VOIDPはアドレス情報のサイズ、iは呼びたい関数の位置である。Releaseなら3番めなので2、みたいな。Fiddle::Pointerに加算すると加算後のアドレスを差すFiddle::Pointerが返ってくるので、もう一度Fiddle::Pointer#ptrを使って関数のアドレスを取得する。最後にFiddle::Function.newでこのアドレスを呼び出す関数定義を生成する。

func = Fiddle::Function.new(func_adrs, ...#うんたらかんたら

あとはこのfuncのcallを呼べばCOMインターフェイス経由で関数の呼び出しができる。とりあえず三角形のポリゴンを描画するところまで作ったコードがこちら

問題点

非常に面倒。定数、列挙、マクロ、構造体、関数をCのヘッダファイル見て追っかけながら定義していくのは正直しんどい。一度作ればずっと使えるのかもしれないが。とはいえ膨大なWindowsの定数などを全部定義するとそれだけで起動時の定義処理で時間もメモリも食うし、必要なものだけってすると何かが必要になるたびにヘッダ見ないといけない。

あとFiddleが構造体内に構造体を書けないのが痛い。それから構造体の値渡しができないのも痛い。Direct2Dのインターフェイスには構造体の値渡しがあるのでFiddleでは扱えない。この点は標準ではなくなるがruby-ffiでは解決できるのでそっちを使ったほうがよいかもしれない。Windowsではgemでインストールするとバイナリが入るのでコンパイルする必要も無い。ちなみにffiによる三角形描画はこちら

感想

Rubyから呼べるならRubyだけで書いたほうがラクかなあーと思って試してみたのだが、やってみたら辛かった、という話。まあ、定義ができたとしても次はオブジェクトの生存期間の問題でFinalizerと格闘することになるだろうから、たぶん最後まで辛いことうけあい。将来的にRubyがもっと速くなるのならこのレベルからRubyで書いてしまうのが何かと便利なんだろうな、とは思うのだが。いやはや。

おしまい

次はhoshi_sanoさんの「道場で学んだ「ゲームとは何か」を踏まえて3〜4時間でゲーム作った話と反省」です。お楽しみに〜