本日はその成果報告です。
成果といっても、今日は簡単のためブルースクリーンを表示するだけ。昨日よりひどい。
さて。
オブジェクト指向というからには、本来ならウィンドウもクラスにすべきところですが、OS からコールバックされる関数(ウィンドウプロシージャ)をメンバ関数にするにはちょっとしたトリックを仕込まねばなりません。
※ C++ において、非 static なメンバ関数は、暗黙のうちに自クラスへのポインタを引数として受け渡す性質があるので、コールバック関数をそのままクラスのメンバにしようとすると、『引数の数が合わないよ!』と叱られちゃうのです。
原因が判れば回避策を講じるのもそう面倒ではないのですが(たとえば追加のメッセージ処理関数を独自にフックさせるとか、あるいは思い切ってウィンドウプロシージャだけ static なメンバ関数にしちゃうとか)、それだけに極端な『オレ流ソース』になりやすく、あんまりよろしくないなぁと思ったので、ウィンドウに関してはそのまま放置にしました。
というわけで手始めに、ウィンドウ生成と Direct3D 関連のコードの分離を試みました。
これは単に、Renderer クラスを作り、コンストラクタに初期化処理、デストラクタに解放処理を書けば済みます。
メンバ変数には、デバイスやスワップチェーン等、Direct3D 関連の情報を private で持たせる事にしました。
……そこまではよかったのですが、作っているうちに大きな問題に気付いたのです。
たとえば、(ゲームなどで)複数のシーン(状態)を切り替えたい場合を想定しましょう。
状態数が増えれば描画処理を Renderer クラスが一括して管理するには無理が生じますので、共通の親から派生した『状態クラス』を各シーンごとに作り、それらを切り替えていくのが定石となっています(ぼくがよく使う State パターンです)。
ここでぼくは、各状態クラスのメンバに描画用のフック関数を作り、Renderer からそちらへ飛ばそうと考えました。
ですが、DirectX の描画処理は、デバイス(のポインタ)を経由して呼び出さねばなりません。そしてそれは、あろうことか Renderer クラスが private で保持している変数です。
そのほかにも、画面の消去等ではレンダリングパイプラインにくっついている各種“ビュー”にもアクセスする局面が出てきます。これらも Renderer の private なメンバ変数です。
つまり、このままでは描画処理を分散させる事ができず、どうにかして各状態クラスに対して情報の橋渡しをしてやらなければならないわけです。しかも、ただやみくもにデータをやり取りしようとすると、オブジェクト指向の利点であるカプセル化の恩恵が受けられなくなってしまいます。
※ ここまでのまとめ: 柔軟な状態遷移に耐えるようにするには描画処理の分散が不可欠だが、それは一筋縄ではいかない。
というわけで、今日は、この問題に対してぼくがどう考えたのかを晒してみようと思います。
まずはじめに考えたのは、Renderer のメンバ変数をすべて public にしてしまう方法でした。
状態クラスの描画関数(たとえば Render 関数)をコールするのは Renderer オブジェクトですから、関数に this ポインタを渡せばそれを経由してデバイス情報等にアクセスする事ができます(下の例)。
【個別の状態クラスに実装された描画処理の例】
void Scene1::Render(const Renderer* renderer) { // 背景塗りつぶし float bgColor[4] = {0, 0, 1, 1}; renderer->pDevice->ClearRenderTargetView( renderer->pRenderTargetView, bgColor); renderer->pDevice->ClearDepthStencilView( renderer->pDepthStencilView, D3D10_CLEAR_DEPTH, 1.0f, 0); }
しかしこれはどう考えてもヤバいので速攻でボツにしました。
大切なポインタ変数に対して、どこからでもアクセスを許す事がどれほど危険な事かは言うまでもないでしょう。
次に、ふたたび Renderer のメンバ変数を private に戻し、代わりに外部からのアクセスが必要な情報に関してはゲッタを設ける事を試みました。
これでだいぶマシになりましたが、結局、“どこからでも”ポインタを Get できるという事は、阿呆がデバイスを勝手に Release してしまう危険性を孕んでいるため、やはりリスキーな事だなと思います。
そもそも、参照カウンタを持つオブジェクトのゲッタを安易に実装すると、使い手は参照カウンタの振る舞いにまで注意を払わねばならないため、個人的に使いづらいなぁという印象です。
※ ちなみに、メンバ変数のポインタをただ返すように実装されたゲッタの場合、参照カウンタは変化しません。
結局、この方法も諦めました。
さらにその次には、状態クラスを Renderer の内部クラスにしてしまう発想が浮かびました。
これなら、 Renderer の private な変数にアクセスもでき、なおかつ Renderer のカプセル化も守られたままですので、それなりによい方法かな…と考えたのです。
が。
看過できない問題点もいくつかあります。
まず、ソースの構造上、各状態クラスの定義が Renderer クラスに内包されてしまうという事(なるべく外に出してやりたい)。
加えて、 Renderer のメンバ変数のうち、不必要なものまでが可視状態になるという鬱陶しさがあり、これも諦めました。
迷走の挙げ句、最終的に『フレンドクラス』という機構を使う事で(一応)落ち着きました。
State パターンでは、各状態クラスは共通の親クラスを継承するので、その親クラスに対してのみ Renderer の全てのメンバ変数を公開します(つまり、親クラスを Renderer のフレンドクラスにしてしまうのです)。
ですが、親を継承した各状態クラスにとって、それらは依然として不可視状態のままですので、親クラスは『派生クラスに対してのみアクセス可能なゲッタ』を実装します(以下参照)。
【各状態の親クラス IScene におけるゲッタの実装例】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // 状態用インタフェース(Stateパターン) // 継承して使ってね! // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___I_STATE_TERCEL___ #define ___I_STATE_TERCEL___ #include "Common.h" class Renderer; // レンダラの前方宣言 class IState { public: /* 中略(仮想関数の宣言等) */ protected: // Direct3D 関連のオブジェクトにアクセス // ---------------------------------------- static ID3D10Device* pDevice (const Renderer* renderer) { return renderer->m_pDevice; } static IDXGISwapChain* pSwapChain (const Renderer* renderer) { return renderer->m_pSwapChain; } static ID3D10RenderTargetView* pRenderTargetView (const Renderer* renderer) { return renderer->m_pRenderTargetView; } static ID3D10DepthStencilView* pDepthStencilView (const Renderer* renderer) { return renderer->m_pDepthStencilView; } static ID3D10Texture2D* pDepthStencil (const Renderer* renderer) { return renderer->m_pDepthStencil; } static ID3D10RasterizerState* pRasterizerState (const Renderer* renderer) { return renderer->m_pRasterizerState; } }; #endif
こうする事で、Renderer のすべてのメンバ変数にアクセスできるのは親クラス(IState)のみに制限でき、派生クラスは親の権限で公開した情報にしかアクセスできなくなっています(以下に利用例)。
【IScene から派生した状態クラス Scene1 の描画処理の例】
void Scene1::Render(const Renderer* renderer) { // 背景塗りつぶし float bgColor[4] = {0, 0, 1, 1}; pDevice(renderer)->ClearRenderTargetView( Get_pRenderTargetView(renderer), bgColor); pDevice(renderer)->ClearDepthStencilView( pDepthStencilView(renderer), D3D10_CLEAR_DEPTH, 1.0f, 0); }
で。
結局、ここまでで何ができたかというと、
- Direct3D の初期化 / 後始末の処理をクラスとして分離できた
- (今後増えるであろう)シーン遷移に対して柔軟に対応できた
これだけの事に数時間頭を悩ませた上、まだ改良の余地がありそうで依然頭がもやもやしています。
ここでお待ちかねのソースコード(ノーカット)。
【Common.h】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // DirectX (Direct3D) 関連の共通ヘッダ // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___COMMON_TERCEL___ #define ___COMMON_TERCEL___ #include <windows.h> #include <d3d10.h> #include <d3dx10.h> #pragma comment(lib, "d3d10.lib") #pragma comment(lib, "d3dx10.lib") #pragma comment(lib, "d3dCompiler.lib") // ポインタの二重解放を防ぐSAFE_RELEASE関数 template<class T> inline void SAFE_RELEASE(T x) { if(x) { x->Release(); x = 0; } } #endif
【Main.cpp】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // メイン: ウィンドウを出す担当 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #include "Common.h" #include "Renderer.h" // アプリケーションとウィンドウの初期化に必要な定数 // ---------------------------------------- const LPCTSTR APP_NAME = TEXT("APP_TERCEL_TECH"); const LPCTSTR MUTEX_NAME = TEXT("MUTEX_TERCEL_TECH"); // クライアント領域の幅と高さ // ---------------------------------------- const UINT WIDTH = 400; const UINT HEIGHT = 300; // タイマー関連 // ---------------------------------------- const UINT TIMER_ID = 100; // 作成するタイマーの識別ID const UINT TIMER_ELAPSE = 16; // WM_TIMERの発生間隔。60fps≒16ms LONG mouseX, mouseY; // マウスのXY座標 // コールバック関数(メッセージ処理) // ======================================== LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { Renderer* pRenderer = (Renderer*)GetWindowLongPtr(hWnd, 0); switch(uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_CREATE: return 0; case WM_TIMER: if(wParam != TIMER_ID) // 関係ないタイマーは無視 return DefWindowProc(hWnd, uMsg, wParam, lParam); // 定期更新処理 // ---------------------------------------- pRenderer->Update(); return 0; case WM_PAINT: // 描画処理 // ---------------------------------------- pRenderer->Render(); return 0; } return DefWindowProc(hWnd, uMsg, wParam, lParam); } // エントリポイント // ======================================== int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); if(GetLastError() == ERROR_ALREADY_EXISTS) { HWND existingWnd = FindWindow(APP_NAME, NULL); if(existingWnd != NULL) { if(IsIconic(existingWnd)) ShowWindowAsync(existingWnd, SW_RESTORE); SetForegroundWindow(existingWnd); } return -1; } WNDCLASSEX wc; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = WindowProc; wc.cbClsExtra = 0; wc.cbWndExtra = sizeof(LONG_PTR); wc.hInstance = hInstance; wc.hIcon = (HICON)LoadImage(NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED); wc.hCursor = (HICON)LoadImage(NULL, MAKEINTRESOURCE(IDC_ARROW), IMAGE_CURSOR, 0, 0, LR_DEFAULTSIZE | LR_SHARED); wc.hbrBackground = NULL; wc.lpszMenuName = NULL; wc.lpszClassName = APP_NAME; wc.hIconSm = (HICON)LoadImage(NULL, MAKEINTRESOURCE(IDI_APPLICATION), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED); if(!RegisterClassEx(&wc)) return -1; UINT windowWidth = WIDTH + GetSystemMetrics(SM_CXFIXEDFRAME) * 2; UINT windowHeight = HEIGHT + GetSystemMetrics(SM_CYFIXEDFRAME) * 2 + GetSystemMetrics(SM_CYCAPTION); HWND hWnd = CreateWindow(APP_NAME, TEXT("はじめてのクラス分け"), WS_OVERLAPPEDWINDOW^WS_THICKFRAME^WS_MAXIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, windowWidth, windowHeight, NULL, NULL, hInstance, NULL); if(!hWnd) return -1; // ---------------------------------------- // Direct3D の初期化 // ---------------------------------------- // まず、オブジェクトを new して… Renderer* pRenderer = new Renderer(hWnd); if(!pRenderer) return -1; // ウィンドウの追加メモリ領域にポインタを保存 SetWindowLongPtr(hWnd, 0, (LONG_PTR)pRenderer); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); SetTimer(hWnd, TIMER_ID, TIMER_ELAPSE, NULL); MSG msg; while(TRUE) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { if(msg.message == WM_QUIT) break; TranslateMessage(&msg); DispatchMessage(&msg); } } // ---------------------------------------- // Direct3D の後始末 // ---------------------------------------- delete(pRenderer); KillTimer(hWnd, TIMER_ID); CloseHandle(hMutex); return (int)msg.wParam; }
【Renderer.h】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // DirectX (Direct3D) 設定関連のクラス // インタフェース部 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___RENDERER_TERCEL___ #define ___RENDERER_TERCEL___ #include "Common.h" class IState; // IState の前方参照 // ここからRendererのインタフェース部 class Renderer { private: // メンバ変数 // ---------------------------------------- HWND m_hWnd; // 描画対象 UINT m_width; // 描画領域の幅 UINT m_height; // 〃 高さ ID3D10Device* m_pDevice; IDXGISwapChain* m_pSwapChain; ID3D10RenderTargetView* m_pRenderTargetView; ID3D10DepthStencilView* m_pDepthStencilView; ID3D10Texture2D* m_pDepthStencil; ID3D10RasterizerState* m_pRasterizerState; // IState からは、Rendererの非公開メンバ全てにアクセス可能 friend class IState; IState* m_pScene; // 現在のシーン // メンバ関数 // ---------------------------------------- HRESULT InitD3D(); public: // メンバ関数 // ---------------------------------------- Renderer(HWND); // コンストラクタ ~Renderer(); // デストラクタ void Update(); // 更新 void Render(); // 描画 // ゲッタ(描画領域の幅/高さを返す) inline UINT GetWidth() const { return m_width; } inline UINT GetHeight() const { return m_height; } }; #endif
【Renderer.cpp】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // DirectX (Direct3D) 設定関連のクラス // 実装部 // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #include "Renderer.h" #include "Scenes.h" // コンストラクタ // ---------------------------------------- // 引数: 描画対象となるウィンドウのハンドル // ======================================== Renderer::Renderer(HWND hWnd) : m_hWnd(hWnd) { // クライアント領域の幅と高さを取得 RECT rect; GetClientRect(m_hWnd, &rect); m_width = rect.right; m_height = rect.bottom; // Direct3Dの初期化 InitD3D(); // 初期状態の代入 m_pScene = (IState *)new SimpleScene1(); } // デストラクタ // ---------------------------------------- // Direct3D の後始末 // ======================================== Renderer::~Renderer() { SAFE_RELEASE(m_pRasterizerState); SAFE_RELEASE(m_pSwapChain); SAFE_RELEASE(m_pRenderTargetView); SAFE_RELEASE(m_pDepthStencilView); SAFE_RELEASE(m_pDepthStencil); SAFE_RELEASE(m_pDevice); if(m_pScene) delete m_pScene; } // Direct3D の初期化 // ======================================== HRESULT Renderer::InitD3D() { // デバイスとスワップチェーン作成 DXGI_SWAP_CHAIN_DESC sd; ZeroMemory(&sd, sizeof(sd)); sd.BufferCount = 1; sd.BufferDesc.Width = m_width; sd.BufferDesc.Height = m_height; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; sd.BufferDesc.RefreshRate.Numerator = 60; sd.BufferDesc.RefreshRate.Denominator = 1; sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = m_hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; if(FAILED(D3D10CreateDeviceAndSwapChain(NULL, D3D10_DRIVER_TYPE_HARDWARE, NULL, 0, D3D10_SDK_VERSION, &sd, &m_pSwapChain, &m_pDevice))) return FALSE; // レンダーターゲットビューの作成 ID3D10Texture2D* pBackBuffer; m_pSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), (LPVOID*)&pBackBuffer); m_pDevice->CreateRenderTargetView(pBackBuffer, NULL, &m_pRenderTargetView); SAFE_RELEASE(pBackBuffer); // 深度ステンシルビューの作成 D3D10_TEXTURE2D_DESC descDepth; descDepth.Width = m_width; descDepth.Height = m_height; descDepth.MipLevels = 1; descDepth.ArraySize = 1; descDepth.Format = DXGI_FORMAT_D32_FLOAT; descDepth.SampleDesc.Count = 1; descDepth.SampleDesc.Quality = 0; descDepth.Usage = D3D10_USAGE_DEFAULT; descDepth.BindFlags = D3D10_BIND_DEPTH_STENCIL; descDepth.CPUAccessFlags = 0; descDepth.MiscFlags = 0; m_pDevice->CreateTexture2D(&descDepth, NULL, &m_pDepthStencil); m_pDevice->CreateDepthStencilView(m_pDepthStencil, NULL, &m_pDepthStencilView); // レンダーターゲットビューと深度ステンシルビューを // パイプラインにバインド m_pDevice->OMSetRenderTargets(1, &m_pRenderTargetView, m_pDepthStencilView); // ビューポートの設定 D3D10_VIEWPORT vp; vp.Width = m_width; vp.Height = m_height; vp.MinDepth = 0.0f; vp.MaxDepth = 1.0f; vp.TopLeftX = 0; vp.TopLeftY = 0; m_pDevice->RSSetViewports(1, &vp); // ラスタライズ設定 D3D10_RASTERIZER_DESC rdc; ZeroMemory(&rdc, sizeof(rdc)); rdc.CullMode = D3D10_CULL_NONE; rdc.FillMode = D3D10_FILL_SOLID; m_pDevice->CreateRasterizerState(&rdc, &m_pRasterizerState); m_pDevice->RSSetState(m_pRasterizerState); return S_OK; } // 更新処理(ウィンドウプロシージャから呼ばれる) // ======================================== void Renderer::Update() { // ---------------------------------------- // // ここに、表示する各子コンポーネントの // 更新処理(関数呼び出し等)を書きます // // ---------------------------------------- IState *newScene = m_pScene->Update(); if(newScene != m_pScene) { delete m_pScene; m_pScene = newScene; } } // 描画処理(ウィンドウプロシージャから呼ばれる) // ======================================== void Renderer::Render() { // ---------------------------------------- // // ここに、表示する各子コンポーネントの // 描画処理(関数呼び出し等)を書いてください // // ---------------------------------------- m_pScene->Render(this); m_pSwapChain->Present(0, 0); }
【IState.h】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // 状態用インタフェース(Stateパターン) // 継承して使ってね! // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___I_STATE_TERCEL___ #define ___I_STATE_TERCEL___ #include "Common.h" class Renderer; // レンダラの前方宣言 class IState { public: virtual ~IState() { } // 更新処理 // ---------------------------------------- // 戻り値: 次状態のポインタ // (IState* 型にアップキャスト) virtual IState* Update() = 0; // 描画 // ---------------------------------------- virtual void Render(const Renderer*) = 0; protected: // Direct3D 関連のオブジェクトにアクセス // ---------------------------------------- static ID3D10Device* pDevice (const Renderer* renderer) { return renderer->m_pDevice; } static IDXGISwapChain* pSwapChain (const Renderer* renderer) { return renderer->m_pSwapChain; } static ID3D10RenderTargetView* pRenderTargetView (const Renderer* renderer) { return renderer->m_pRenderTargetView; } static ID3D10DepthStencilView* pDepthStencilView (const Renderer* renderer) { return renderer->m_pDepthStencilView; } static ID3D10Texture2D* pDepthStencil (const Renderer* renderer) { return renderer->m_pDepthStencil; } static ID3D10RasterizerState* pRasterizerState (const Renderer* renderer) { return renderer->m_pRasterizerState; } }; #endif
【Scenes.h】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // 状態管理用インタフェース // // 全状態クラスのヘッダファイルを // 一括インクルード // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___SCENES_TERCEL___ #define ___SCENES_TERCEL___ // ======================================= // // ここに、全状態クラスのヘッダファイルを書いてね // ※今回はひとつだけ // // ======================================= #include "SimpleScene1.h" #endif
【SimpleScene1.h】
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- // // 状態用クラス(その1) // 青い画面を出すだけで特に何もしない // // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- #ifndef ___SIMPLESCENE_1_TERCEL___ #define ___SIMPLESCENE_1_TERCEL___ #include "Common.h" #include "IState.h" class SimpleScene1 : IState { public: SimpleScene1() { }; ~SimpleScene1() { }; // 更新 IState* Update() { return (IState*)this; } // 描画 void Render(const Renderer* renderer) { // 背景塗りつぶし float bgColor[4] = {0, 0, 1, 1}; pDevice(renderer) -> ClearRenderTargetView( pRenderTargetView(renderer), bgColor); pDevice(renderer) -> ClearDepthStencilView( pDepthStencilView(renderer), D3D10_CLEAR_DEPTH, 1.0f, 0); } }; #endif
これによって、通常のレンダリングは IState を継承したクラス(上記サンプルでは SimpleScene1)をいじるだけでOKになりました(たぶん)。
IState 自体は、更新処理ののち、次状態のポインタを返す Update 関数と、自身を描画する Render 関数からなる非常にシンプルなクラスです。
この設計は、Processing をちょっと意識してみました。
今日はできるだけわかりやすくシンプルなコードを書こうと思っていましたが、やはり多少テクニカルでアレな感じになってしまいました。
C++むずかしい。
0 件のコメント:
コメントを投稿
ひとことどうぞφ(・ω・,,)