2011/06/26

いまさら DirectX10(なんちゃってオブジェクト指向篇)

昨日のソースコードが、あまりにもアレだったので、せめて DirectX の初期化関連だけでも別クラスに分けようと思いました。

本日はその成果報告です。

成果といっても、今日は簡単のためブルースクリーンを表示するだけ。昨日よりひどい
さて。

オブジェクト指向というからには、本来ならウィンドウもクラスにすべきところですが、OS からコールバックされる関数(ウィンドウプロシージャ)をメンバ関数にするにはちょっとしたトリックを仕込まねばなりません。

※ C++ において、static なメンバ関数は、暗黙のうちに自クラスへのポインタを引数として受け渡す性質があるので、コールバック関数をそのままクラスのメンバにしようとすると、『引数の数が合わないよ!』と叱られちゃうのです。

原因が判れば回避策を講じるのもそう面倒ではないのですが(たとえば追加のメッセージ処理関数を独自にフックさせるとか、あるいは思い切ってウィンドウプロシージャだけ static なメンバ関数にしちゃうとか)、それだけに極端な『オレ流ソース』になりやすく、あんまりよろしくないなぁと思ったので、ウィンドウに関してはそのまま放置にしました。



というわけで手始めに、ウィンドウ生成と Direct3D 関連のコードの分離を試みました。

これは単に、Renderer クラスを作り、コンストラクタに初期化処理、デストラクタに解放処理を書けば済みます。

メンバ変数には、デバイスやスワップチェーン等、Direct3D 関連の情報を private で持たせる事にしました。

……そこまではよかったのですが、作っているうちに大きな問題に気付いたのです。



たとえば、(ゲームなどで)複数のシーン(状態)を切り替えたい場合を想定しましょう。

状態数が増えれば描画処理を Renderer クラスが一括して管理するには無理が生じますので、共通の親から派生した『状態クラス』を各シーンごとに作り、それらを切り替えていくのが定石となっています(ぼくがよく使う State パターンです)。

ここでぼくは、各状態クラスのメンバに描画用のフック関数を作り、Renderer からそちらへ飛ばそうと考えました。

ですが、DirectX の描画処理は、デバイス(のポインタ)を経由して呼び出さねばなりません。そしてそれは、あろうことか Renderer クラスが private で保持している変数です。

そのほかにも、画面の消去等ではレンダリングパイプラインにくっついている各種“ビュー”にもアクセスする局面が出てきます。これらも Rendererprivate なメンバ変数です。

つまり、このままでは描画処理を分散させる事ができず、どうにかして各状態クラスに対して情報の橋渡しをしてやらなければならないわけです。しかも、ただやみくもにデータをやり取りしようとすると、オブジェクト指向の利点であるカプセル化の恩恵が受けられなくなってしまいます。

※ ここまでのまとめ: 柔軟な状態遷移に耐えるようにするには描画処理の分散が不可欠だが、それは一筋縄ではいかない。

というわけで、今日は、この問題に対してぼくがどう考えたのかを晒してみようと思います。



まずはじめに考えたのは、Renderer のメンバ変数をすべて public にしてしまう方法でした。

状態クラスの描画関数(たとえば Render 関数)をコールするのは Renderer オブジェクトですから、関数に this ポインタを渡せばそれを経由してデバイス情報等にアクセスする事ができます(下の例)。

【個別の状態クラスに実装された描画処理の例】
  1. void Scene1::Render(const Renderer* renderer)  
  2. {  
  3.     // 背景塗りつぶし  
  4.     float bgColor[4] = {0, 0, 1, 1};  
  5.       
  6.     renderer->pDevice->ClearRenderTargetView(  
  7.         renderer->pRenderTargetView, bgColor);  
  8.   
  9.     renderer->pDevice->ClearDepthStencilView(  
  10.         renderer->pDepthStencilView,     
  11.         D3D10_CLEAR_DEPTH,     
  12.         1.0f,     
  13.         0);   
  14. }  

しかしこれはどう考えてもヤバいので速攻でボツにしました。

大切なポインタ変数に対して、どこからでもアクセスを許す事がどれほど危険な事かは言うまでもないでしょう。



次に、ふたたび Renderer のメンバ変数を private に戻し、代わりに外部からのアクセスが必要な情報に関してはゲッタを設ける事を試みました。

これでだいぶマシになりましたが、結局、“どこからでも”ポインタを Get できるという事は、阿呆がデバイスを勝手に Release してしまう危険性を孕んでいるため、やはりリスキーな事だなと思います。

そもそも、参照カウンタを持つオブジェクトのゲッタを安易に実装すると、使い手は参照カウンタの振る舞いにまで注意を払わねばならないため、個人的に使いづらいなぁという印象です。

※ ちなみに、メンバ変数のポインタをただ返すように実装されたゲッタの場合、参照カウンタは変化しません。

結局、この方法も諦めました。



さらにその次には、状態クラスを Renderer の内部クラスにしてしまう発想が浮かびました。

これなら、 Rendererprivate な変数にアクセスもでき、なおかつ  Renderer のカプセル化も守られたままですので、それなりによい方法かな…と考えたのです。

が。

看過できない問題点もいくつかあります。

まず、ソースの構造上、各状態クラスの定義が Renderer クラスに内包されてしまうという事(なるべく外に出してやりたい)。

加えて、 Renderer のメンバ変数のうち、不必要なものまでが可視状態になるという鬱陶しさがあり、これも諦めました。



迷走の挙げ句、最終的に『フレンドクラス』という機構を使う事で(一応)落ち着きました。

State パターンでは、各状態クラスは共通の親クラスを継承するので、その親クラスに対してのみ Renderer の全てのメンバ変数を公開します(つまり、親クラスを Renderer のフレンドクラスにしてしまうのです)。

ですが、親を継承した各状態クラスにとって、それらは依然として不可視状態のままですので、親クラスは『派生クラスに対してのみアクセス可能なゲッタ』を実装します(以下参照)。

【各状態の親クラス IScene におけるゲッタの実装例】
  1. // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-  
  2. //  
  3. // 状態用インタフェース(Stateパターン)  
  4. //                       継承して使ってね!  
  5. //  
  6. // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-  
  7. #ifndef ___I_STATE_TERCEL___  
  8. #define ___I_STATE_TERCEL___  
  9.   
  10. #include "Common.h"  
  11. class Renderer; // レンダラの前方宣言  
  12.   
  13. class IState  
  14. {  
  15. public:  
  16.     /* 中略(仮想関数の宣言等) */  
  17.   
  18. protected:  
  19.     // Direct3D 関連のオブジェクトにアクセス  
  20.     // ----------------------------------------  
  21.   
  22.     static ID3D10Device* pDevice  
  23.         (const Renderer* renderer)  
  24.     {  
  25.         return renderer->m_pDevice;  
  26.     }  
  27.   
  28.     static IDXGISwapChain* pSwapChain  
  29.         (const Renderer* renderer)  
  30.     {  
  31.         return renderer->m_pSwapChain;  
  32.     }  
  33.   
  34.     static ID3D10RenderTargetView* pRenderTargetView  
  35.         (const Renderer* renderer)  
  36.     {  
  37.         return renderer->m_pRenderTargetView;  
  38.     }  
  39.   
  40.     static ID3D10DepthStencilView* pDepthStencilView  
  41.         (const Renderer* renderer)  
  42.     {  
  43.         return renderer->m_pDepthStencilView;  
  44.     }  
  45.   
  46.     static ID3D10Texture2D* pDepthStencil   
  47.         (const Renderer* renderer)  
  48.     {  
  49.         return renderer->m_pDepthStencil;  
  50.     }  
  51.   
  52.     static ID3D10RasterizerState* pRasterizerState  
  53.         (const Renderer* renderer)  
  54.     {  
  55.         return renderer->m_pRasterizerState;  
  56.     }  
  57. };  
  58. #endif  

こうする事で、Renderer のすべてのメンバ変数にアクセスできるのは親クラス(IState)のみに制限でき、派生クラスは親の権限で公開した情報にしかアクセスできなくなっています(以下に利用例)。

IScene から派生した状態クラス Scene1 の描画処理の例】

  1. void Scene1::Render(const Renderer* renderer)  
  2. {  
  3.     // 背景塗りつぶし  
  4.     float bgColor[4] = {0, 0, 1, 1};  
  5.       
  6.     pDevice(renderer)->ClearRenderTargetView(  
  7.         Get_pRenderTargetView(renderer), bgColor);  
  8.   
  9.     pDevice(renderer)->ClearDepthStencilView(  
  10.         pDepthStencilView(renderer),     
  11.         D3D10_CLEAR_DEPTH,     
  12.         1.0f,     
  13.         0);   
  14. }  



で。

結局、ここまでで何ができたかというと、
  • Direct3D の初期化 / 後始末の処理をクラスとして分離できた
  • (今後増えるであろう)シーン遷移に対して柔軟に対応できた
といったところでしょうか。

これだけの事に数時間頭を悩ませた上、まだ改良の余地がありそうで依然頭がもやもやしています。



ここでお待ちかねのソースコード(ノーカット)。

Common.h

Main.cpp

Renderer.h

Renderer.cpp

IState.h

Scenes.h

SimpleScene1.h

これによって、通常のレンダリングは IState を継承したクラス(上記サンプルでは SimpleScene1)をいじるだけでOKになりました(たぶん)。

IState 自体は、更新処理ののち、次状態のポインタを返す Update 関数と、自身を描画する Render 関数からなる非常にシンプルなクラスです。

この設計は、Processing をちょっと意識してみました



今日はできるだけわかりやすくシンプルなコードを書こうと思っていましたが、やはり多少テクニカルでアレな感じになってしまいました。

C++むずかしい。

0 件のコメント:

コメントを投稿

ひとことどうぞφ(・ω・,,)