第2回「プログラマの三大美徳 〜怠慢編〜」


皆様ごきげんよう
本講座は、無料で入手できるプログラミング環境「Cygwin」を用いて本格的なSTG(シューティング)ゲームを製作してみる講座である。
その実質的な初回である前回では、DirectXを用いた最も簡単なプログラムを紹介し、それをコンパイルして実行ファイルを作る方法について紹介した。
が、そのプログラムの意味などに関しては全く触れなかったし、コンパイルをする際に入力するコマンドも長く面倒なものであった。
ゆえに今回は、昨日紹介したプログラムの大まかな意味を説明し、その後に楽してコンパイルするための方法である「Makefile」の作り方を紹介したいと思う。
最後までお付き合い頂ければ幸い。



それでは、早速解説を始めよう。
全てを解説するのは大変なので、DirectXの最低限のプログラム dx_hello.cc に対し、説明を行いたい。
再掲:dx_hello.cc

#include <windows.h>
#include <d3d9.h>
#include <d3dx9.h>

#include <boost/intrusive_ptr.hpp>
void intrusive_ptr_add_ref( IUnknown* ptr )
{
	ptr->AddRef();
}
void intrusive_ptr_release( IUnknown* ptr )
{
	ptr->Release();
}
using boost::intrusive_ptr;

#include <stdexcept>

static const int width = 800, height = 600;
intrusive_ptr<IDirect3D9> pD3D;
intrusive_ptr<IDirect3DDevice9> pD3DDevice;
D3DPRESENT_PARAMETERS D3DPParams;

// ウィンドウプロシャージャ
LRESULT CALLBACK WndProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
{
	switch (msg)
	{
	 case WM_DESTROY:
	 {
		PostQuitMessage(0);
		return 0;
	 }
	 break;
	 
	 case WM_KEYDOWN:
	 {
		if( wp == VK_ESCAPE )
		{
			PostQuitMessage(0);
			return 0;
		}
	 }
	 break;
	 
	 default:
	 // ここは無くても良いが作者は入れる主義
	 break;
	}
	
	// デフォルト動作
	return DefWindowProc( hwnd, msg, wp, lp );
}

// ウィンドウを作る
HWND create_window()
{
	const char* wc_name = "dx_hello";
	
	// ウィンドウクラスの設定
	WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, &WndProc, 0L, 0L, 
			GetModuleHandle(NULL), NULL, LoadCursor( NULL, IDC_ARROW ), NULL,
			NULL, wc_name, NULL };

	// コケたらNULLを返す
	if( !RegisterClassEx( &wc ) )
	{
		return NULL;
	}
	
	// ウィンドウの生成
	DWORD window_style = ~( WS_MAXIMIZEBOX | WS_THICKFRAME ) & WS_OVERLAPPEDWINDOW;
	return CreateWindow( wc_name, "Hello, World!!", window_style,
		CW_USEDEFAULT, CW_USEDEFAULT,
		width + GetSystemMetrics(SM_CXDLGFRAME) * 2,
		height + GetSystemMetrics(SM_CYDLGFRAME) * 2 + GetSystemMetrics(SM_CYCAPTION),
		NULL, NULL, wc.hInstance, NULL );
}

void init_dx( HWND hWnd, bool windowed )
{
	// D3Dオブジェクトの生成
	pD3D = intrusive_ptr< IDirect3D9 >( Direct3DCreate9( D3D_SDK_VERSION ), false );
	if( !pD3D ){ throw std::runtime_error( "D3Dオブジェクトの生成に失敗しました" ); }
	
	// D3Dデバイスを生成するための構造体を設定
	ZeroMemory( &D3DPParams, sizeof(D3DPRESENT_PARAMETERS) );
	
	// プレゼンテーションパラメータ設定
	if( windowed )
	{
		D3DPParams.BackBufferFormat = D3DFMT_UNKNOWN;
		D3DPParams.Windowed = TRUE;
	}
	else
	{
		D3DPParams.BackBufferHeight = height;
		D3DPParams.BackBufferWidth = width;
		D3DPParams.BackBufferFormat = D3DFMT_A8R8G8B8;	// 32bit。16bitのときは D3DFMT_A1R5G5B5 。
		
		D3DPParams.Windowed = FALSE;
	}
	
	// 各パラメータ
	D3DPParams.BackBufferCount = 1;	// バックバッファの数
	D3DPParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
	D3DPParams.EnableAutoDepthStencil = TRUE;	// Direct3Dに深度バッファの管理を任せる
	D3DPParams.AutoDepthStencilFormat = D3DFMT_D16;	// 深度バッファのフォーマット
	D3DPParams.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT; // リフレッシュレート
	
	// D3Dデバイスの生成
	{
		IDirect3DDevice9 *pD3DDevice_ = NULL;
		// HAL、HELの組み合わせを変えながら、D3Dディバイスを生成する
		// まず、HALのいいやつ
		if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
				D3DCREATE_HARDWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
		{
			// 失敗。普通のHALで試す
			if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
				D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
			{
				// それでもダメならREFも試す
				if( FAILED( pD3D->CreateDevice( D3DADAPTER_DEFAULT, D3DDEVTYPE_REF, hWnd,
						D3DCREATE_SOFTWARE_VERTEXPROCESSING, &D3DPParams, &pD3DDevice_ ) ) )
				{
					// ダメでした
					throw std::runtime_error( "D3Dデバイスの生成に失敗しました" ); 
				}
			}
		}
		pD3DDevice = intrusive_ptr< IDirect3DDevice9 >( pD3DDevice_, false );
	}
	// レンダリングステートの設定
	pD3DDevice->SetRenderState( D3DRS_ZENABLE, TRUE );	// Zバッファを有効にする
	pD3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_NONE );	// ポリゴンの裏は表示しない
	pD3DDevice->SetRenderState( D3DRS_LIGHTING, TRUE );	// ライトを有効に

	// フィルタの設定
	pD3DDevice->SetSamplerState( 0, D3DSAMP_MIPFILTER, D3DTEXF_POINT );
	pD3DDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_POINT );
	pD3DDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_POINT );
	
	// デバイスをリセットして使用開始
	pD3DDevice->Reset( &D3DPParams );
}

int main( int argc, char* argv[] )
{
	// コマンドライン解析
	bool windowed = true;
	for( int i = 1; i < argc; ++i )
	{
		const char* p = argv[i];
		if( *p == '-' )
		{
			++p;
			if( *p == 'f' )
			{
				windowed = false;
			}
			else if( *p == 'w' )
			{
				windowed = true;
			}
		}
	}
	
	// ウィンドウ製作
	HWND hWnd = create_window();
	if( !hWnd )
	{
		return -1;
	}
	
	// DirectX 初期化
	try
	{
		init_dx( hWnd, windowed );
	}
	catch(...)
	{
		return -1;
	}
	
	// ウィンドウ表示
	ShowWindow( hWnd, SW_SHOWDEFAULT );
	UpdateWindow( hWnd );
	
	// メッセージループ
	MSG msg;
	do
	{
		if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) ) 
		{
			if( !GetMessage ( &msg, NULL, 0, 0 ) )
			{
				msg.message = WM_QUIT;	// 終了
			}
			else
			{
				TranslateMessage( &msg );
				DispatchMessage( &msg );
			}
		}
		else
		{
			Sleep(1);
			
			// 描画
			
			// まずクリア
			pD3DDevice->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_XRGB(0,0,0), 0, 0 );
			// シーンの開始
			if( SUCCEEDED( pD3DDevice->BeginScene() ) )
			{
				// 何もしないで終了
				pD3DDevice->EndScene();
			}

			// バックバッファを表画面に反映させる
			HRESULT hr = pD3DDevice->Present( NULL, NULL, NULL, NULL );
		}
	}
	while( msg.message != WM_QUIT );
	
	return msg.wParam;
}

早速、上のソースコードを見ていただきたい。
まず冒頭でインクルードしているヘッダは、Windows 上で DirectX プログラミングをするに当たって必要なヘッダである。このあたりは、特に問題は無いだろう。
次の

#include <boost/intrusive_ptr.hpp>
void intrusive_ptr_add_ref( IUnknown* ptr )
{
	ptr->AddRef();
}
void intrusive_ptr_release( IUnknown* ptr )
{
	ptr->Release();
}
using boost::intrusive_ptr;

この部分は、スマートポインタである boost::intrusive_ptr を使うために必要な部分だ。スマートポインタ、というものに対して詳しい説明は行わないが、要するに「面倒な管理が不要になる」ポインタと考えればよい(ポインタが分からない人は、各自で調べて欲しい)。細かい説明は割愛するが、C++ におけるプログラミングにおいて、スマートポインタを使うメリットの大きさは計り知れない。よって、本講座では出来る限りスマートポインタを用いたプログラミングを行う。
なお、今回使った boost::intrusive_ptr は、実は DirectX のプログラムと相性が良くない。よって、よりプログラミングしやすくするため、今後は com_ptr というスマートポインタを自作し、それを用いてプログラミングを行うことになるのでご了承を。

閑話休題。次の部分、

static const int width = 800, height = 600;
intrusive_ptr<IDirect3D9> pD3D;
intrusive_ptr<IDirect3DDevice9> pD3DDevice;
D3DPRESENT_PARAMETERS D3DPParams;

これは、グローバル変数の宣言を行っている部分である。
width および height は画面の幅および高さであり、これは常に一定なので static const を付けて宣言している(グローバル変数に static を付けて宣言すると、その変数は「このソースファイルの中でだけ」使われる変数として扱われる。それに const が付加されたことにより、コンパイラは変数 width や height の実体を最適化によって省略することが出来、効率がよい)。
次の pD3D、pD3DDevice、D3DPParams は DirectX プログラミングをするにあたって必要なものである。
なお、人によってはグローバル変数に g_ というプレフィックスを付けて区別するという人がいるが、本講座では別にそのようなことはしない。これはハンガリアン嫌いな筆者の趣味である(その割には p とか付けてるが)。
次の部分はウィンドウプロシャージャ。普通の Windows プログラミングでは最重要部分なのだが、DirectX プログラミングでは別に重要ではないので、詳しい説明は割愛する。やっていることは、Escキーが押されたら終了しているだけだ。

次の create_window 関数は、メインウィンドウを作る関数。
これは DirectX プログラミングでは殆ど固定された書き方であるので、「こういうものか」と思っていただければよい。
その次の init_dx 関数も、殆どお約束通りの書き方だ。というか、巷に溢れる DirectX の書籍から拾ってきた、殆どそのままである。
ただし、失敗したときは例外を投げるようにしてある点、およびデバイスの生成時に生のポインタでデバイスを受け取ってからスマートポインタにコピーする点などが、銀天流にアレンジされている。
最後、main 関数(WinMain だとコマンドライン引数が単語ごとに分割されないので、あえて main を使っている)も、まずコマンドライン引数を解析しウィンドウモードを決定することを除けば、ウィンドウを作り DirectX を初期化しメッセージループを回す、ごく一般的な構成だ(デバイスロストにこそ対応していないが)。


当初の予定よりだいぶ荒い説明になってしまったが、以上でソースコードの説明は終わりたい。
正直このあたりの部分は、意味が分かっていなくても大体こんな感じで書けば通用する、お約束の世界である。
よって、読んでいる側としては非常に分かりにくい説明になってしまってはないかと思う。
が、安心して欲しい。この辺は、ネットで調べれば幾らでも説明を得られる部分なのだ。ゆえに、分からないと思ったら検索して調べれば、いくらでも解説を得られるだろう。


それでは、今回の正念場、後半戦「Makefile の書き方」に移りたいと思う。
これは、Cygwin でプログラミングする手間を大幅に改善する手法である。
前回の説明を思い出してみれば分かると思うが、CygwinDirectX のゲームをコンパイルして起動しようと思ったら、

g++ -mno-cygwin -mwindows dx_hello.cc -o dx_hello -ld3d9
dx_hello

てな感じで、長いコマンドを入力しなければならない。が、いったん Makefile なるものを書いてしまえば、

make run

と入力しただけで勝手にコンパイルが進み、完成した実行ファイルを起動することができるようになる。この手間の差は大きいだろう。
というわけで、今回の後半では Makefile の書き方を学習する。なお、本来この Makefile は「分割コンパイル」をするためのものなのだが、今回はまだ製作するプログラムが分割コンパイルするほどでもないので、その辺りは後々解説する。そちらは請うご期待、ということで。


さて、それでは始めよう。今回の肝は、ずばり make コマンドである。
この make コマンドは、カレントディレクトリにある「Makefile」というファイル(拡張子なし)を調べ、その中に書いてある手順をもとにプログラムをコンパイルしてくれるコマンドだ。
長々と説明するのは好きでは無いので、さっそく試してみよう。
まず Cygwin を立ち上げて、講座用のディレクトリに移動する。前回、ホームディレクトリ直下に ginten_kouza ディレクトリを作った場合なら「cd ~/ginten_kouza」、C:\hoge ディレクトリ下に ginten_kouza ディレクトリを作った場合なら「cd /cygdrive/c/hoge/ginten_kouza」でカレントディレクトリを変更する。
そしたら、

touch Makefile

として空の Makefile を作成しよう。
次に、この Makefile を編集する・・・のだが、こいつには拡張子が無いので、cygstart を使ってエディタを立ち上げることが出来ない(わけではないが、面倒だ)。よって、Windowsの方からエディタで開くことにする。

cygstart ./

でカレントディレクトリを開き、その中にある拡張子なしの「Makefile」をエディタで開く(ドラッグ&ドロップを使うと良い)。
開けたら、とりあえず次の三行をコピペして保存する:

.PHONY: all
all : 
	g++ -mno-cygwin -mwindows dx_hello.cc -o dx_hello -ld3d9

この意味は、冒頭行が「『all』ってのはコマンドだ」という宣言、二行目が「これから『all』コマンドの中身を書きます」という宣言、そして三行目がコマンド『all』の中身、という感じだ(厳密には違うが、そういう意味だと思っておいて欲しい)。
それでは、実際に make コマンドを実行・・・する前に、結果が分かりやすいよう出力先の dx_hello.exe を消しておこう。Cygwin

rm dx_hello.exe

と入力することで、dx_hello.exe を消去する。ls コマンドで、実際に消去できているかを確かめよう。
確かめられたら、

make all

と入力する。すると、「g++ -mno-cygwin -mwindows dx_hello.cc -o dx_hello -ld3d9」と表示された後、(コンパイルが成功すれば)何も表示されずにコマンド入力可能状態に戻るはずだ。これで、コンパイルは完了したことになる。lsコマンドを入力すれば、さっき消したはずの dx_hello.exe が再び作成されているのが分かるはずだ。
このように、Makefile を使えば、予め Makefile に書いたコマンドを自動的に実行できる。ちなみに今回は「all」のみだったが、もちろん他のコマンドを作ることもでき、例えば Makefile

.PHONY: win
win : 
	g++ -mno-cygwin -mwindows win_hello.cc -o win_hello

と追加した後「make win」と入力すれば、win_hello.exe を作ることができる。
これにより、いちいちコマンドをその都度入力する手間が省けるわけだ。


さて、今のままでも十分便利なわけだが、Makefile にはさらに便利な機能があり、それがマクロである。
マクロとは簡単に言えば C++ で言うところの変数であり、何度も使う文字列に名前を与える機能、と考えて欲しい。
実を言うと、今の段階ではマクロは使っても使わなくても同じ程度の機能でしかないのだが、Makefile が複雑になってくるにつれ、その恩恵は大きくなっていく。ので、本講座では早い段階からマクロを最大活用していこうと思う。
それでは、マクロを使った Makefile をご覧頂きたい:

CC = g++
OUT = dx_hello
SRC = dx_hello.cc
OPT = -mno-cygwin -mwindows
LIB = -ld3d9

.PHONY: all
all : 
	$(CC) $(OPT) $(SRC) $(LIB) -o $(OUT)

.PHONY: run
run: all
	$(OUT)

この前半部、「CC = g++」とかの部分がマクロである。このような代入文の形で設定し、「$(マクロ名)」の形でその値を使用する、それだけの機能だ。
だが、これを使うことにより、Makefile の編集が一気に楽になる。
これは、変更しうる部分(ソースファイル名とかコンパイラオプションとか)が冒頭に一覧の形で表示されるためだ。
また、後半部に run というコマンドを追加してあるが、こいつの動作は「コンパイルした後、起動する」ものである。これにより、

make run

と入力するだけで、自動でコンパイルし実行することができるようになった。


以上で、だいぶ荒い説明になったが、Makefile の説明を終了する。
実を言うと、この説明では Makefile の本領について一切触れていないのだが、その辺りに関しては後々説明するのでご容赦いただきたい。
では、この辺りで今回の講座は終わりにしよう。次回の講座は、いよいよ真っ暗な画面に絵を描こうと思う。それまでの課題はこちら:

  1. DirectX の画像表示について検索し、分かる範囲で予習せよ。
  2. DirectX の画面上で表示したい画像を自作せよ。

それでは次回の講座まで、ごきげんよう