第1回「Cygwinに慣れよう」


皆様ごきげんよう
本講座、銀天流「無料で出来る本格派ゲームプログラミング - STG編 -」は、無料で入手できるプログラミング環境「Cygwin」を用いて本格的なSTG(シューティング)ゲームを製作してみよう、というものである。
その最初の回である前回、第0回では、本講座を受けるに当たって必要な資質、およびツールなどを紹介した。
今回はそれらを受けて、実際にどのようにプログラミングをするか、その大まかな流れを紹介していきたい。
それでは始めよう。最後までお付き合い頂ければ幸い。



前回の講座では、必要なツールを紹介し、自力で何とかインストールして欲しい、といったところで終了した。
今回はそれを踏まえ、インストールしたツール「Cygwin」の使い方を学ぼう。
それでは早速、Cygwinを起動してみよう。
まずすることは、プログラミング用ディレクトリの作成である。ここでは Cygwin のホームディレクトリ直下に ginten_kouza というディレクトリを作り、その中で作業をすることにしよう。
この「ディレクトリを作る」という作業は、もちろん Windowsエクスプローラ上で行うことも出来る。が、せっかく UNIX とほぼ同じ機能を持つ Cygwin を使っているので、今回は Cygwin の機能を用いて作ることする。
まず、ホームディレクトリ(~/)に移動してみよう。そのためには、

cd ~

と入力すればよい。自分で作った他のディレクトリ(例えば C:\hoge とか)の下で作業をしたい場合には、

cd /cygdrive/c/hoge

と入力すれば、そのディレクトリの中で作業が出来る。どちらでも、好きなほうを選ぼう。講座内ではホームディレクトリの下で作業を行うが、それ以外のディレクトリで作業する場合でも操作は全く変わらない。
では、この下に作業用フォルダ ginten_kouza を作ろう。

mkdir ginten_kouza

と入力することで、現在のカレントディレクトリの下に「ginten_kouza」ディレクトリを作ることができる。きちんと作れたかを確認するために、

ls

とタイプしてフォルダの中身を見てみよう。表示された文字列の中に「ginten_kouza/」とあれば、ディレクトリは製作できている。それでは、

cd ginten_kouza

と入力して、今作った ginten_kouza ディレクトリに移動しよう。今後は、このフォルダの中で全ての作業を行うことになる。


それでは、実際のプログラム製作を始めよう。まず、前回の課題にあった「ハローワールド」プログラムを製作する。このプログラムは、実行すると

Hello, World!!

と表示するだけの、ごくごく簡単なプログラムだ。
まず、プログラムのソースコードを作ろう。ソースコード、といっても要は単なるテキストファイルである。ここでは、まず空のファイルを作ってから、それをエディタで編集することでソースコードを作ることにしよう。
空のファイルを製作するコマンドは「touch」である。

touch hello.cc

と入力すれば、現在のカレントディレクトリ下に「hello.cc」というファイルを製作できる。

ls

と入力して、「hello.cc」と表示されていれば OK だ。
次に、製作した hello.cc をエディタで編集する。そのためには、

cygstart ./

と入力すれば、現在のカレントディレクトリを Windows で開くことができるので、その中の hello.cc をエディタで開いて編集すればよい。とりあえず、エディタに

#include <iostream>
using namespace std;

int main()
{
	cout << "Hello, World!!" << endl;
}

と打ち込んで、保存してみよう。これが「ハローワールド」と呼ばれるプログラムの全容である。
こうして入力したプログラムは、そのままではただのテキストファイルである。これを実際に実行するためには、「コンパイル」をして実行可能ファイル(所謂 .exe である)に変換してやらねばならない。
コンパイル」するには、Cygwin の g++ コマンドを使う。すなわち、Cygwin

g++ hello.cc

と打ち込んでやればよい。打ち込んだ後、何かエラーメッセージが表示されればコンパイルは失敗、何も表示されなければコンパイルは成功である。コンパイルが成功した場合、「a.exe」というファイルが作成される。

ls

と打ち込んで、「a.exe」が実際に出来ているかどうかを確かめよう。出来ていることを確認できたら、次はこの a.exe を実行してみる。Cygwin

a

と入力してみよう。これは「a.exeを実行せよ」という命令だ。ここまでの手順が全て上手く行けば、「Hello, World!!」と Cygwin 上に表示されるはずだ。もし表示されない場合は、もう一度試すか、プログラミングの初歩のページを検索して調べてみると良い。


さて、このようにして、我々はついに最初のプログラムを作り上げた。
しかし、これがゲーム製作に繋がるかと言えば、別にそんなことはない。
現に、今作ったプログラムは、単に「Hello, World!!」と表示する機能しかないものであり、しかも Cygwin 上でしか実行できないという粗末なものだ。
試しに、「cygstart ./」と入力して Windows 上でフォルダを開き、実行ファイル「a」を起動してみても、「なんたら.dllが見つからない」と表示され、起動できない。
ゲームを作るからには、せめてウィンドウが開いてなんたら、というものを作りたいものである。そこで、次はそういうプログラムを作ってみるとしよう。
まず、さっきと同様、ソースファイルを製作する。今回のファイル名は、Windows版ハローワールドということで、「win_hello.cc」というものにしよう。こいつを作るには、

touch win_hello.cc

と入力すればよい。さっきと全く同様である。このプログラムを編集し、最初のWindows版プログラムを製作する。こいつを編集するには、さっきのように「cygstart ./」とやって開いたフォルダからエディタを使って編集すればよい。あるいは、「.cc」拡張子が目当てのエディタに関連付けされている場合には、cygwin から直接

cygstart win_hello.cc

とやってもエディタを起動できる。好きな方にすればよい。
こうして開いたエディタに、以下のソースコードを打ち込もう:

#include <windows.h>

// ウィンドウプロシャージャ
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 )
		{
			// Esc が押されたら終了
			PostQuitMessage(0);
			return 0;
		}
	 }
	 break;
	 
	 default:
	 // ここは無くても良いが作者は入れる主義
	 break;
	}
	
	// デフォルト動作
	return DefWindowProc( hWnd, msg, wp, lp );
}

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

	// コケたらNULLを返す
	if( !RegisterClassEx( &wc ) )
	{
		return NULL;
	}
	
	// ウィンドウの生成
	return CreateWindow( wc_name, "Hello, World!!", WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
		NULL, NULL, wc.hInstance, NULL );
}

int main()
{
	// ウィンドウ製作
	HWND hWnd = create_window();
	if( !hWnd )
	{
		// 失敗したら終了
		return -1;
	}
	
	// ウィンドウ表示
	ShowWindow( hWnd, SW_SHOWDEFAULT );
	UpdateWindow( hWnd );
	
	// メッセージループ
	MSG msg;
	do
	{
		if( GetMessage( &msg, NULL, 0, 0 ) )
		{
			TranslateMessage( &msg );
			DispatchMessage( &msg );
		}
		else
		{
			msg.message = WM_QUIT;	// 終了
		}
	}
	while( msg.message != WM_QUIT );
	
	return msg.wParam;
}

少し長いプログラムだが、頑張って打ち込んで欲しい。それが嫌なら、コピー&ペーストをするのも一つの手だ。
打ち込み終わったら、こいつをコンパイルする。先ほどと同様、Cygwin

g++ win_hello.cc

と入力すれば、それでOKだ。コンパイルが上手く行けば、何も表示されずに「a.exe」が結果として出力される(ちなみに hello.cc をコンパイルして出来た a.exe は上書きされて消えてしまうので注意)。
それでは早速、「a」と入力して a.exe を起動してみよう。真っ白なウィンドウが開かれて、そのタイトルバーに「Hello, World!!」と表示されれば成功だ。
これで、れっきとした Windows プログラムを作ることに成功した・・・ように見えるが、実はまだ足りない。最初の例でやったように、Cygwin を使わず 直接 a.exe を起動した場合、同じように「DLLが無いぜ」と文句を言われるのだ。これを回避するには、「-mno-cygwinコンパイラオプションを使う。すなわち、

g++ -mno-cygwin win_hello.cc

と入力してコンパイルしてみよう。コンパイルが終了したら、また直接 a.exe を起動してみる。すると今度は、特にエラーも起きずに白いウィンドウが表示されるのが分かると思う。
ただ、この方法だと、ウィンドウが開くと同時に、なんか黒い画面も同時に開いてしまう。それを回避するには、

g++ -mno-cygwin -mwindows win_hello.cc

と、さらに -mwindows オプションを付ければ解決する。再び、直接 a.exe を起動してみよう。すると、黒いウィンドウが表示されることもなく、他の Windows 実行ファイルと同じように実行できているのが分かると思う。
さて、これで全てうまく行くわけだが、ここでもう一つだけ教えたいことがある。それが「-o」コンパイラオプションだ。先ほどまでのコンパイルでは、全て出力される実行ファイルが「a.exe」という名前になった。だが、ゲームを作るに当たって、ゲーム本体の実行ファイルの名前が「a.exe」というのは、なんともお粗末な感じがする。無論、出来た a.exe を別のファイル名に変更すれば問題は解決するが、わざわざ手動で名前を変えるよりは、元からファイル名を指定できたほうが楽なのは言うまでも無い。そうするためには、コンパイルのコマンドに「-o 出力ファイル名」と付け加えればよい。例えば今回の例で、実行ファイル名を「win_hello.exe」にしたければ、

g++ -mno-cygwin -mwindows win_hello.cc -o win_hello

このようにタイプすれば、全て上手く行く。

今まで説明したことをまとめると、CygwinWindows プログラムを作るには、g++ -mno-cygwin -mwindows ソースファイル名 -o 実行ファイル名」と入力すればよい、ということだ。 これが、今回の講座で最も大事なことである。


さて最後に、折角なので DirectX を用いた最小限のプログラムを製作し、この場を閉めようと思う。
なおこのプログラムは、前回の講座でインストールしてもらうよう お願いした「DirectX 9.0c」および「Boost C++ Library」が正常にインストールされているかどうかをチェックするプログラムでもある。それでは、いってみようか。
とりあえずファイル名は、DirectX版 ハローワールドということで、dx_hello.cc という名前にしよう。
早速、

touch dx_hello.cc
cygstart 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;
}

かなり長いので、例によってコピー&ペーストで何とかすること。
入力が終わったら、例によってコンパイル&実行する。
とりあえず今回の実行ファイル名は「dx_hello.exe」として、さっきまでと同様

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

と入力・・・しても、「undefined reference to なんたら」とか言われて上手くコンパイルできない。
これは、コンパイルは上手くいったが、リンクが上手くいっていない、というメッセージだ。
「リンク」という耳慣れない言葉が出てきたが、これは各自で学習して欲しい。
本講座では、このエラーへの対処法のみ教えよう。これは状況によって様々であるが、今回のものに関しては「-ld3d9」というコンパイラオプションを付けることで解決する。このオプションは、簡単に言うと「これはDirectXのプログラムです」とコンパイラに教えるためのものだ(正確には違うので、正確な意味を知りたければ検索してみよう)。
では、以上のことに注意し、もう一度コンパイルする。

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

コンパイルが成功すれば、何も表示されず終了するので、実際に起動してみよう。
今回、実行ファイル名は「dx_hello.exe」なので、

dx_hello

Cygwin に入力すれば実行できる。真っ黒なウィンドウが開けば、無事成功だ。
無論、これだけだと、さっきの白いウィンドウと何ら変わらないが、今度は

dx_hello -f

と入力してみよう。すると、画面が真っ暗になる・・・そう、ゲームのフルスクリーンモードが可能なのである。
なお、この真っ暗なウィンドウを終了する場合は、Alt+F4 か Esc で終了できる。


以上で、今回の講座は終了である。今回のおさらいとしては、

  1. touch ファイル名」でファイルを製作
  2. cygstart ファイル名」でファイルを編集
  3. g++ -mno-cygwin -mwindows ソースファイル名 -o 実行ファイル名」でファイルをコンパイル
  4. 実行ファイル名」で起動

この流れでプログラムを製作する、ということである。
といっても、実は、慣れてくるともっとスマートな方法があるのだが・・・それは、次回以降のお話ということで。


今回の課題。

  1. C++の勉強をして、今回使ったソースコードの意味を調べておくこと。
  2. できるならば、様々なプログラムを製作し、コンパイル&実行して、これらの手順に慣れること。

なお今回は初回ということで、かなり丁寧めに説明したが、次回以降、どんどん説明は少なくしていこうと思う。
これは、説明に文字数を使い過ぎ、大切なことが分かりにくくなってしまうような事態を防ぐためである。
よって、分からないことがあれば、すぐに自力で検索する癖をつけるように。
次回の講座では、今回のソースコードの意味をざっと説明した後、「Makefile」について説明しようと思う。
それでは次回の講座まで、ごきげんよう