その15 プラットフォーム非依存なコードを作る
DirectXは現在(2011.6)DirectX11がリリースされています。ちょっと前まではDirectX9が主流でした。10は・・・(^-^;。同じDirectXでもバージョンによって実装がガラリと変わります。また各バージョン間のインターフェイスには互換性はありません。よって、DirectX11の環境でDirectX9のコードを入れるのはちょっと難しい物があります。DirectXを使えない環境(Windows以外など)での3D描画はOpenGLが有力候補ですが、もちろんそういう環境ではDirectXのコード自体が微塵も走りません。
ゲームを作るには膨大なソースコードを要します。もしそれがDirectX9専用だとしたら、DirectX10やOpenGL環境に移植する際には、ほぼ全コードを書き直すことになってしまいます。それはとっても心苦しいのです。
とは言え、一つのコードで複数のプラットフォームに対応するのはやや無理があります。それはデバイスに直結しているゲーム制作では、どうしてもプラットフォームに依存する部分があるためです。例えばDirectX9だとIDirect3DDevice9インターフェイスが無いと描画が一切できません。つまり、デバイスを利用するコード内には何らかのプラットフォーム依存なコードが必ず含まれます。
プラットフォーム依存なコードが広範囲に広がる程、他のプラットフォームへの移植度は低下していきます。できれば、依存性のある部分は局所的にしたい。それが理想です。ではどうするか?依存部分を下層に、ロジック部分を上層に据えます。例えばこういう図です:
最下層にあるPhysXやDirectXなどがプラットフォームにがっちり依存する部分です。上層部分(Game Engine以上)はプラットフォームを気にしない部分です。これが徹底している場合、下層部分は取り替えることができます。それを可能にするには、「下層専用のクラス等を上層が持ってはいけません」。例えば、Game Engine部分でIDirect3DDevice9インターフェイスを持った瞬間、Game EngineはDirectX9専用になってしまいます。それだとDirectX10やOpenGLなどに差し替える事ができないわけです。
では、このような階層をしっかり分けて、ゲームエンジンからプラットフォーム依存な匂いを消すにはどうしたら良いのでしょうか?
@ .cppに依存性を集約させる
C++は基本的にヘッダーファイルで使用するクラスの型やメソッドを宣言し、.cppにその実装を入れます。例えばこんな感じです:
DebugPrintDX9.h #include <d3dx9.h>
class DebugPrintDX9 {
ID3DXFont *font;
public:
virtual void print(int x, int y, const char* text) {
RECT r = {x, y, x + 640, y + 480};
font->DrawText(NULL, text, -1, &r, DT_LEFT | DT_NOCLIP, 0xffffffff);
}
}
これはDirectX9上で文字列を描画するクラスの例です。このクラスはDirectX9では動きますが、DirectX10はもちろんOpenGLなどでは全く動きません。ここから依存性を少しずつ除いていきましょう。
まず、簡単なところから。ヘッダー内の実装コードを.cppへ移行します。するとヘッダーはこうなります:
DebugPrintDX9.h #include <d3dx9.h>
class DebugPrintDX9 {
ID3DXFont *font;
public:
virtual void print(int x, int y, const char* text);
}
printメソッドだけを見れば、ここに依存性は無くなりました。でも、インクルード部分とメンバはこれ以上どうしようもありません。このインクルードがあるのはメンバにID3DXFontインターフェイスがあるせいです。つまり、「ヘッダー内クラス宣言にメンバがあると、依存性が発生する事がある」わけです。
では、クラスからメンバを無くしたらどうでしょう。ん?それって「抽象クラス」です。そう、手っ取り早く依存性を無くすには依存があるクラスを抽象クラス化すれば良いんです。上のクラスはつまりこうなります:
DebugPrint.h class DebugPrint {
public:
DebugPrint(){}
virtual ~DebugPrint() {}
virtual void print(int x, int y, const char* text) = 0;
}
これで依存性は0になりました。ゲームエンジンはDebugPrintDX9ではなくて、DebugPrintクラス(のポインタ)のインターフェイスを通して文字列を描画する事になります。
ただ、抽象クラスは実体化できません。必ず派生クラスでの実装が必要になるわけで、結局DebugPrintDX9クラスを宣言しどこかで作る必要が出てきます。でも、エンジン側にDirectX9な匂いが無くなるのは確かです。依存性のあるクラスをどう端に追いやり、そのオブジェクトをどうやって作るのか?それが残った課題です。
ヒントはDirectXの仕様にあります。DirectXでは、インターフェイスを直接newできません。その代わりにインターフェイスを作成する専用関数が必ず用意されています。例えばテクスチャを作成するにはID3DXCreateTexture関数とかIDirect3DDevice9::CreateTextureメソッドを使います。そういう「生成メソッド」を通せば、オブジェクトを作成できます。いわゆる「ファクトリクラス」が役に立つわけです。
先のDebugPrintオブジェクトを作成するファクトリクラスはこんな感じになります:
Factory.h class Factory {
public:
static DebugPrint* createDebugPrint();
}
FactoryDX9.cpp #include "debugprintdx9.h"
DebugPrint* Factory::createDebugPrint() {return new DebugPrintDX9;}
ここに最大の集約ポイントがあります。Factory.hには相変わらずDirectX9な匂いはありません。でも、Factory.cppはしっかりとDirectX9しています。唯一DirectX9を全力で醸し出すのはここなんです。でも、.cppはヘッダーではありませんから、ゲームエンジンを侵食しません。
A デバイスアクセスクラスはどうしても必要かもしれません
@のようにすると、ゲームエンジン側で依存性が無くなります。しかし、一つ大きな問題が残っているんです。それは「デバイスインターフェイス」です。
WindowsならばHWNDやHINSTANCE、DirectXであればIDirect3DDevice9など、デバイスにがっちり依存した値(クラス)があります。これらは最終的な描画で「絶対に」使われます。そればかりではありません。例えば@のDebugPrintDX9クラスにあるID3DXFontインターフェイスを生成するのにもIDirect3DDevice9は必要になってしまいます。HWNDだってIDirect3DDevice9の生成には必須です。
これらのデバイス自体の生成は依存性が極限に高い部分です。ただ、それは大抵main.cpp内で作り終えてしまいます。@でも説明したように、.cpp内での依存性は他のクラスに波及しないため許容されます。でも、ID3DXFontインターフェイスを作るためにFactoryクラスにIDirect3DDevice9を直接渡すのは本末転倒。依存性が復活してしまいます。
これを回避する一つの方法は「グローバルアクセスが可能なクラス(シングルトンクラス)を作る」です。例えば次のような簡単なクラスを作ったとしましょう:
RenderDeviceDX9.h #include "debugprintdx9.h"
class RenderDeviceDX9 {
static IDirect3DDevice9 *pDev;
RenderDeviceDX9() {} // プライベートコンストラクタ
public:
static void set(IDirect3DDevice9* dev) {
pDev = dev;
}
static IDirect3DDevice9* get() {
return pDev;
}
}
ゲームが始まってmain.cpp内でIDirect3DDevice9インターフェイスを作ったら、このクラスにそれを登録してしまいます。するとDebugPrintDX9クラスのコンストラクタで次のようにID3DXFontインターフェイスを作成するのに必要な描画デバイスを直接取得できます:
DebugPrintDX9.cpp #include "renderdevicedx9.h"
DebugPrintDX9::DebugPrintDX9() {
// 描画デバイスを取得
IDirect3DDevice9* pDev = RenderDeviceDX9::get();
// ID3DXFontインターフェイスを作成
D3DXCreateFont(pDev, ..., &font);
}
ここでもDirectX9に依存しているヘッダーは.cpp内にあるので、ゲームエンジンを侵食してはいません。
もしこのようなデバイスアクセスクラスを使わないとしたら、デバイスを保持している「ゲーム環境クラス」をたらい回しにします。そういうクラスをGameEnvクラスとしまして、例えばGameEnv::getRenderDeviceメソッドなどで描画デバイスを取得します。ただ、この時に取得できる値はvoid*です。IDirect3DDevice9*にはできません。これが嫌ならば、GameEnv::QueryIntefaceメソッドを次のように作るしか無いかなぁと思います:
GameEnv.h class GameEnv {
public:
virtual bool QueryInterface(const char* interfaceName, void** ppInterface);
};
GameEnvDX9.h #include <d3d9.h>
#include "gameenv.h"
class GameEnvDX9 : public GameEnv {
IDirect3DDevice9 *pDev;
public:
virtual bool QueryInterface(const char* interfaceName, void** ppInterface);
};
GameEnvDX9.cpp bool GameEnvDX9::QueryInterface(const char* interfaceName, void** ppInterface) {
if (interfaceName == "DX9Dev") {
*ppInterface = pDev;
return true;
}
reutrn false;
};
ゲームエンジン側はGameEnvクラスのオブジェクトポインタしか扱いません。一方その実体はGameEnvDX9で、main.cppなど依存性を許容する所で作ります。シングルトンなアクセスクラスにせよ、GameEnvクラスのような環境クラスにせよ、依存性を無くせるのは同じです。私個人的にはシングルトンが好きだったりします(^-^)
B フォルダ構成とプロジェクトの設定
@、Aとゲームエンジン側で認識すべきプラットフォーム非依存な部分と、DirectX9などにバリバリ依存する部分とが出てきました。これらのファイルはどのように管理すべきでしょうか?やってはいけないのは次のようなフォルダ管理です:
一つのフォルダにゲームエンジンが利用するヘッダーや.cppとプラットフォーム依存のそれらがごちゃまぜになっています。色々考え方もあると思いますが、例えば次のようにフォルダを分けてみます:
ゲームエンジン側で参照するプラットフォーム非依存なヘッダーと.cppを左側(srcフォルダ)にまとめています。一方プラットフォーム依存がある部分はその下位フォルダdx9にすべて突っ込みます。もしOpenGL用の環境を作りたい場合は、srcフォルダ下にopenGLフォルダを作成し、dx9フォルダ内のヘッダーや.cppと同じ物のOpenGL版を作成します。
Visual Studio等でのプロジェクトでは、まずプラットフォーム非依存のファイルを全部プロジェクトに追加します(ヘッダーは必ずしも追加する必要はありません)。次に、もしDirectX9版のゲームを作りたい場合はdx9フォルダ下の.cppをプロジェクトに追加します。dx9フォルダへパスを通す必要はありません。dx9内のヘッダーファイルはこのフォルダ内になる.cppしか参照しないためです。
C テストしてみましょう
では、非常に簡単ですがテストプログラムを作ってみます。作るプログラムは画面にテスト文字列を表示する簡単なものです。プラットフォームとしてはWin32APIを用いた描画とDirectX9を用いた描画を試みてみます。
まずVisual Studioで新規のソルーションOXGameを作成します。Win32 Applicationで空のプロジェクトを立ち上げましょう。この段階でとあるフォルダに次のようなソルーションが出来ているはずです:
このフォルダにsrcフォルダ、mainWin32.cpp、mainDX9.cppを追加し、さらにsrcフォルダ下にwinフォルダとdx9フォルダを追加します:
次に最初に出来たプロジェクトを一旦削除します。ソルーションエクスプローラにあるOXGameプロジェクトアイコンで右クリックし[削除]でさっくりと消えます。これでソルーションのみですっきりしました。続いて[OXGameWin32プロジェクト]及び[OXGameDX9プロジェクト]をソルーションにそれぞれ追加します。追加先をソルーションがあるフォルダにし、からのプロジェクトを作成するよう注意して下さい。最後にsrcフォルダにあったmainWin32.cppとmainDX9.cppをそれぞれOXGameWin32及びOXGameDX9フォルダに移動します。これでフォルダおよびVS内のソルーションエクスプローラは次のようになったはずです:
ではWin32版のアプリから作成していきます。OXGameWin32プロジェクトにmainWin32.cppを追加し、次のようなメイン関数のコードを作成します:
mainWin32.cpp #include <windows.h>
#include <tchar.h>
#include "Application.h"
#include "win32/renderdevice_win32.h"
TCHAR gName[100] = _T("OXGame for Win32");
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){
if(mes == WM_DESTROY) {PostQuitMessage(0); return 0;}
return DefWindowProc(hWnd, mes, wParam, lParam);
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
MSG msg;
HWND hWnd;
WNDCLASSEX wcex ={sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
if(!RegisterClassEx(&wcex))
return 0;
if(!(hWnd = CreateWindow(gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 400, 300, NULL, NULL, hInstance, NULL)))
return 0;
// HWNDを登録
RenderDeviceWin32::set(hWnd);
// アプリケーションを作成
Application app;
// メイン メッセージ ループ
ShowWindow(hWnd, nCmdShow);
do{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ) {
TranslateMessage(&msg);
DispatchMessage(&msg);
} else
app.gameLoop();
} while(msg.message != WM_QUIT);
return 0;
}
メイン関数はプラとフォームにどっぷり浸かって良い場所の一つです。ここではWin32アプリケーション専用のオブジェクトをいくつか作っています。一番大切なのはメインウィンドウのハンドル(hWnd)です。これがあれば各種描画ができます。作成したhWndを、グローバルアクセスが可能なRenderDeviceWin32クラスに登録しています。このクラスはWin32専用なので、src/win32フォルダ下にヘッダーや.cppを作ります。
アプリケーション(Application)はWin32とDirectX9の両方で共通して使用するエンジン部分です。Application.hとapplication.cppはそのためsrcフォルダ下に置きます。アプリケーションの実装を見てみましょう:
application.cpp #include "application.h"
#include "factory.h"
Application::Application() : debugPrint(Factory::createDebugPrint()) {
}
Application::~Application() {
delete debugPrint;
}
// ゲームループ
void Application::gameLoop() {
debugPrint->print(0, 0, "OX Game !");
}
コンストラクタに注目です。ここではdebugPrintオブジェクトをファクトリを用いて作成しています。factory.hは共通なのでsrcフォルダ下、しかしその実装factor_win32.cppはWin32用なのでsrc/win32フォルダ下に置きます。プロジェクトにはこのfactory_win32.cppを追加します。プラットフォームの依存性は.cppに集約するんです。Factory::createDebugPrintメソッドが返すのはWin32専用のオブジェクトであるDebugPrintWin32オブジェクトです。
ゲームループではDebugPrint::printメソッドを通して画面に文字列を描画するようにしています。Win32版の場合はメインウィンドウのデバイスコンテキストを取得し、TextOut関数で描画します:
debugprint_win32.cpp #include "debugprint_win32.h"
#include "renderdevice_win32.h"
// 文字列描画
void DebugPrintWin32::print(int x, int y, const char* text) {
// ウィンドウハンドルを取得
HWND hWnd = RenderDeviceWin32::get();
HDC hDC = ::GetDC(hWnd);
::TextOutA(hDC, 0, 0, text, (int)strlen(text));
ReleaseDC(hWnd, hDC);
}
これでWin32 APIを用いて次のような描画が実現できます:
さて、ではこのエンジンに当たるApplication.hやApplication.cppの実装を全く変えずに、同じような描画をDirectXで実現してみましょう。
ソルーション内のOXGameDX9プロジェクトにsrc/dx9/mainDX9.cppを追加し、以下のようなメイン関数を実装します:
mainDX9.cpp #pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "d3d9.lib")
#pragma comment(lib, "d3dx9.lib")
#include <windows.h>
#include <tchar.h>
#include "Application.h"
#include "dx9/renderdevice_dx9.h"
TCHAR gName[100] = _T("OXGame for DirectX9");
LRESULT CALLBACK WndProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){
if(mes == WM_DESTROY) {PostQuitMessage(0); return 0;}
return DefWindowProc(hWnd, mes, wParam, lParam);
}
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
MSG msg; HWND hWnd;
WNDCLASSEX wcex ={sizeof(WNDCLASSEX), CS_HREDRAW | CS_VREDRAW, WndProc, 0, 0, hInstance, NULL, NULL, (HBRUSH)(COLOR_WINDOW+1), NULL, (TCHAR*)gName, NULL};
if(!RegisterClassEx(&wcex))
return 0;
if(!(hWnd = CreateWindow(gName, gName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, 400, 300, NULL, NULL, hInstance, NULL)))
return 0;
// Direct3Dの初期化
LPDIRECT3D9 g_pD3D;
LPDIRECT3DDEVICE9 g_pD3DDev;
if( !(g_pD3D = Direct3DCreate9( D3D_SDK_VERSION )) )
return 0;
D3DPRESENT_PARAMETERS d3dpp = {0,0,D3DFMT_UNKNOWN,0,D3DMULTISAMPLE_NONE,0,D3DSWAPEFFECT_DISCARD,NULL,TRUE,0,D3DFMT_UNKNOWN,0,0};
if( FAILED( g_pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &g_pD3DDev ) ) ) {
g_pD3D->Release();
return 0;
}
// IDirect3DDevice9を登録
RenderDeviceDX9::set(g_pD3DDev);
// アプリケーションを作成
Application app;
// メイン メッセージ ループ
ShowWindow(hWnd, nCmdShow);
do{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE) ) {
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// Direct3Dの処理
g_pD3DDev->Clear( 0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,255), 1.0f, 0 );
g_pD3DDev->BeginScene();
app.gameLoop();
g_pD3DDev->EndScene();
g_pD3DDev->Present( NULL, NULL, NULL, NULL );
}
} while(msg.message != WM_QUIT);
return 0;
}
やっている事はWin32版とそれほど変わりません。IDirect3DDevice9を作成してグローバルアクセスができるRenderDeviceDX9オブジェクトに登録しています。次にアプリケーションをWin32版と全く同様に作成し、app.gameLoopメソッドでゲームループを回します。
アプリケーションクラス内にはDebugPrintオブジェクトがありますが、今度はこれをDirectX9版のDebugPrintDX9オブジェクトに入れ替えます。それを担うのはもちろんファクトリクラスです:
factory_dx9.cpp #include "../factory.h"
#include "dx9/debugprint_dx9.h"
DebugPrint* Factory::createDebugPrint() {
return new DebugPrintDX9;
}
このファクトリの実装はDirectX9専用なのでsrc/dx9フォルダ内に置きます。DebugPrintDX9クラスはDirectXの機能を使って文字列を描画します。実装はこんな感じです:
debugprint_dx9.cpp #include "debugprint_dx9.h"
#include "renderdevice_dx9.h"
DebugPrintDX9::DebugPrintDX9() {
IDirect3DDevice9 *pDev = RenderDeviceDX9::get();
D3DXFONT_DESCA lf = {24, 0, 0, 1, 0, SHIFTJIS_CHARSET, OUT_TT_ONLY_PRECIS, PROOF_QUALITY, FIXED_PITCH | FF_MODERN, "MS ゴシック"};
D3DXCreateFontIndirectA(pDev, &lf, &font);
}
DebugPrintDX9::~DebugPrintDX9() {
}
// 文字列描画
void DebugPrintDX9::print(int x, int y, const char* text) {
RECT r = {x, y, x + 640, y + 480};
font->DrawTextA(NULL, text, -1, &r, DT_LEFT | DT_NOCLIP, 0xffffffff);
}
コンストラクタでID3DXFontオブジェクトを作ります。printメソッドが呼ばれたらそのフォントオブジェクトを通して文字列を描画します。結果として次のような描画を得られます:
ゲームエンジン部を変えること無く、Win32版をDirectX9版に移植出来たことになります!
このサンプルの完全なコードや設定はこちらからダウンロードできます。
プラットフォーム非依存にするにはこのような感じで慎重なプロジェクトの設定と抽象化が必要です。もちろんプラットフォームによってはそもそもの表現が難しい物もあります(Win32
APIで3D表示はきっと相当に困難です)。ただ、ゲームのロジックからプラットフォーム依存を排除すればするほど移植性はどんどん高くなりますし、ゲームエンジンは磨かれていきます。大変ですが魅力的なのがプラットフォーム非依存コードにはあるかなと思います。