■ 新・ゲーム開発講座




■ へっぽこプログラミング入門♪


■第3夜:フルスクラッチスケルトンA

前動作のイメージをつかんでいただいたところで、最少構成のスケルトン test1.cpp をもう少し詳しく見ていきましょう。ここでは、コンパイラを立ち上げ、ソースとこのページをにらめっこしている状態を想定しています(^^)

■グローバル変数

グローバル変数は、このスケルトンでは以下の3つを記述してあります。

変数の型

変数名

意味
HWND hwnd Windowのハンドル
HDC win_hdc デバイスコンテキストのハンドル
HINSTANCE hinst インスタンス値


なんだか見なれない型の変数ばかりかも知れませんが、Windowsではとにかく変数の型が大量に定義されています。私も最初は「なんだこれはー!?」とビビったものですが、こればかりはAPIやライブラリを呼び出すときに嫌でも必要になりますので、
「そーゆーものだ」と割り切って使うしかありません。ここでは、手っ取り早く「動くソフト」を組み上げるのが目的ですので、解説はしますが理解までは求めないスタンスで爽やかに?進行したいと思います(^^)。

さて順を追って説明しましょう。HWND hwndWindowのハンドルです。ハンドルというのはデスクトップ上にいくつも開くウィンドウを識別する背番号みたいなもので、ウィンドウを開くときにWindowsのシステムから「君はこの番号ね」と割り当てられます。APIやライブラリ関数を呼び出すときには必ずといっていいほど必要になりますので、グローバル変数としてどこからでも参照できるようにしておきます。HWND はハンドル番号を格納するための型です(←気にしないで使いましょう♪)

HDC win_hdcデバイスコンテキストへのハンドルです。デバイスコンテキストとはウィンドウに絵や文字を表示するときに使うデータ構造のことで、画面の解像度や色数など(+その他いろいろ)を意識しないで描画を行うために規定されています。画面に何かを表示するときには必ず使うものです。HDC はデバイスコンテキストへのハンドルを格納するための型です。
なお、test1.cpp ではまだウィンドウに何も表示していませんので、win_hdcは宣言しただけで全く使っていません(^0^;)蛇足ですが、プログラム上で表画面、裏画面を用意した場合、それぞれ別個にデバイスコンテキストへのハンドルを取得する必要があります。

HINSTANCE hinstインスタンスへのハンドルです。インスタンスとは、メモリの中で実行中のプログラム本体のことだと思って下さい。マルチタスクの環境下では、同じプログラムが2重、3重に起動している場合もありますので、これを識別するために背番号(ハンドル)がプログラム起動時にWindowsのシステムから与えられます。これも、APIやライブラリ関数を呼び出す際に必要になる場合がありますので、アクセスしやすいようにグローバル変数で保持しておきます。HINSTANCE はインスタンスへのハンドルを格納するための型です。

・・・・なんだか、ハンドルを格納するなら HANDLE という型をひとつ作ればそれでいいじゃないか、という気がしないでもありませんが、どうもマイクロソフト社は煩雑ではあっても厳密に区別をさせたいようです。賢い選択なのかただの阿呆なのか、私には良くわかりません♪(^_-)

■void Mainloop(void)

さて、ここからは関数の説明です。test1.cpp では Mainloop() はまったくの空っぽですが、ゲーム開発の主戦場はこの Mainloop() の中になります。気をつけるべきことはただひとつ。「タスク型の処理になるよう上手く考えて組め」です。前回タッチ&ゴー、というお話をしましたがまさにそのとおりで、「スッ」と入って「パッ」と仕事をし「サッ」と去っていく・・・という形の記述が要求されます。このへんは後日「遅延処理」と絡めてお話しようと思います(^^)。いまは、空っぽで充分です。

■LRESULT WndProc(HWND hwnd,UINT msg,WPARAM wprm,LPARAM lprm)

次に WndProc()についてです。この関数はプログラム起動時に WinMain()内部で登録されるもので、Windowsシステムの中で自分のプログラムに関連するイベントが起こったときに、そのメッセージをさばく関数です。引数の hwnd、msg、wprm、lprm はいずれもWindowsのシステム経由で流れてくるデータで、この引数がイベントの内容を表現しています(多少わかりにくい?引数ですが汎用性を追求した結果、このような形式に落ち着いたみたいですね)。関数の内容は、次の通りです。

【引数】
HWND hwnd
:ウィンドウのハンドルです。Windowsアプリケーションは複数の子ウィンドウを持っていることもあるので、どの子ウィンドウに関するイベントなのかをここで把握します。単純なゲームではウィンドウは1つで間に合うので、あまり気にしなくても良いように思います(^^)。

UINT msg:メッセージのコード番号です。ちなみにUINTは unsigned int の短縮表記です。

WPARAM wprm , LPARAM lprm:汎用データ型?の引数です(実際には32bitのint)。特に用途が決まっている訳ではなく、必要に応じて(メッセージコードによって)いろいろな値が入ってきます。それ以外は無視していて結構です。

【内容解説】
ソースを見ていただければ分かると思います。特に説明の必要もないほど単純ですよね(笑)。発生したイベントのメッセージコードによって switch 文で処理を振り分けているだけです。test1.cpp では話を単純にするために WM_CREATE(ウィンドウが生成された)、WM_DESTROY(ウィンドウが消去された)の2種類のイベントしかチェックしていません。

test1.cppでは WM_CREATE は単に break しているだけですが、実際のゲームではこの部分に各種初期化プロセスを記述します(WinMainの最初の方でもOKですけどね)。WM_DESTOROY では
PostQuitMessage(0) を実行していますが、これは Windowsアプリを終了するときの常套句だと思ってください(Windowsシステムに終了コード0で終了メッセージを投げています)。
なお default 処理の部分にある return DefWindowProc(hwnd,msg,wprm,lprm) ですが、これは switch 文の中で明示的に処理しなかった「その他大勢のイベントメッセージ」を適当にさばいてもらうための処理です。プログラマは自分の処理したい部分だけを記述し、あとはデフォルト処理に投げて良い訳ですね。実にらくちんです♪(^^) 戻り値としては、自前の処理が無事完了したときは 0 を戻し、デフォルト処理に任せたときはその戻り値を投げてやればOKです。(このあたりは定型処理です)

イベントメッセージにはたくさんの種類があって、とてもすべてを載せることは出来ません。代表的なものとしては以下のようなものがあります。皆さんも調べてみてください。

WM_ACTIVATEAPP Windowがアクティブになった
WM_PAINT 描画命令が出た
WM_CHAR 文字入力があった
WM_KEYDOWN キーが押された
WM_LBUTTONDOWN マウス左ボタンが押された
WM_LBUTTONUP マウス左ボタンが離された
WM_RBUTTONDOWN マウス右ボタンが押された
WM_RBUTTONUP マウス右ボタンが離された
WM_MOUSEMOVE マウスが動いた
MM_MCINOTIFY MCI系の再生が終了した



■APIENTRY WinMain(HINSTANCE hInst,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow)

Windowsアプリケーションのエントリポイント=実行開始位置となる関数です。DOSソフトでいえば main() に相当します。ここでウィンドウの初期設定を行い、またイベント監視ループを回します。まずは引数から見ていきましょう。

【引数】
WinMainの引数はプログラムが起動したときにWindowsのシステムから降ってくるものです。

HINSTANCE hinst メモリ内にある実行ファイルの実体(インスタンス)を識別する番号
HINSTANCE hPrevInst Win32環境では常に0なので無視♪
LPSTR lpCmdLine コマンドライン文字列(今となってはほとんど意味無しですね ^^;)
int nCmdShow ウィンドウをどう表示するかの指定

実際に意味のある引数は、hinstnCmdShow でしょう。hinst は多重起動時に自分自身を区別するために、また nCmdShow は(あまりきちんとは調べていないのですが ^^)おそらく子プロセスとしてプログラムが起動したときに、親プロセスから「最少化して起動せよ」「非表示で起動せよ」などの指令を受け取るために用意されているようです。

【内容】
WinMain()関数のポイントは、ウィンドウの初期化とメッセージループです。まずはウィンドウの初期化からです。

最初に WNDCLASS wc を宣言します。ウィンドウクラスです。とりあえず普通の構造体だと思ってメンバを設定しましょう。窓を開くための準備その1です。



wc.hInstance=hIns; //インスタンス値
wc.lpszClassName="窓1号"; //クラス名(適当でかまわない)
wc.lpfnWndProc=(WNDPROC)WndProc; //イベントハンドラを登録
wc.style=0; //スタイルはとりあえず0でいいや
wc.hIcon=LoadIcon((HINSTANCE)NULL,IDI_APPLICATION); //タイトルバーに表示するアイコン
wc.hCursor=LoadCursor((HINSTANCE)NULL,IDC_ARROW); //マウスカーソル
wc.lpszMenuName=0; //メニューなんて無視、無視
wc.cbClsExtra=0; //このメンバは今のところ未使用
wc.cbWndExtra=0; //このメンバは今のところ未使用
wc.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH); //背景は白


wc.hInstance=hIns は、Windowsのシステムから与えられた背番号(インスタンス値)を格納しています。
wc.lpszClassName="窓1号" はクラスに"窓1号"という適当な名前を付けています(ウィンドウのタイトルでは無いので注意)。そして wc.lpfnWndProc=(WNDPROC)WndProc ですが、ここで test1.exe(このスケルトンをコンパイルして出来上がる実行ファイル)のウィンドウ上におけるイベント処理関数を登録しています。またwc.style=0 はウィンドウのスタイルをデフォルトにしています。

wc.hIcon=LoadIcon((HINSTANCE)NULL,IDI_APPLICATION) は、ウィンドウを開いたときにキャプションバーの左肩に表示するアイコンを設定しています。IDI_APPLICATIONというのはデフォルトのアイコンで、何もしなくてもコンパイル時にリソースとして実行ファイルの中に埋め込まれるものです。
wc.hCursor=LoadCursor((HINSTANCE)NULL,IDC_ARROW) はマウスカーソルをデフォルトの IDC_ARROW に設定しています。ここを変更するとマウスカーソルがウィンドウに重なったとき、自由なデザインのカーソルに切替えられますが、ここではお遊びはしないで置きます(^^)。
wc.lpszMenuName=0 はメニュー設定ですが今のところ0にしておきます。wc.cbClsExtrawc.cbWndExtraも今のところ未使用なのでひとまず0をほうり込んでおきましょう。wc.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH) は初期画面を白で塗りつぶすための設定です。WHITE_BRUSH を BLACK_BRUSH にすると、真っ黒画面で起動できます。

さて、メンバを設定したところで、登録です。RegisterClass()関数に、先ほどのウィンドウクラスのアドレスを渡してやります。

if(RegisterClass(&wc)==0)return 0;

RegisterClass()関数は成功するとアトム値(クラスの識別番号だと思って下さい)を返し、失敗すると0を返します。失敗したら、0を返して終了しましょう(TーT)。

さて、クラスを登録したら、次はウィンドウの生成です。ウィンドウの生成には CreateWindowEx()関数を使います。引数のメンバーが多いので大変ですが、特に難しい内容はないと思います。




hwnd=CreateWindowEx (
0,
wc.lpszClassName,
"たいとる",
WS_OVERLAPPEDWINDOW,
20,20,640,480,
(HWND)NULL,
(HMENU)NULL,
hIns,
(LPVOID)NULL
//拡張スタイルは特に考えない
//クラス名(↑で決めたものを与えておけばOK)
//Windowの左肩に表示される窓の名前
//ウィンドウスタイル(←ここでは縁取り指定のみ)
//窓の配置ポジション、大きさ(左肩のx,y及び幅、高さ)
//親Windowのハンドル(ここでは無視)
//メニューも子ウィンドウも無いので気にしない
//インスタンス値
//他に参照すべきデータは無いので気にしない

);

if(!hwnd)return 0; //失敗したらシステムに0を返して終了(T0T)

※ここでは関数の引数が多いので縦に並べてしまっていますが、()で囲まれていさえすればコンパイラは正しく認識してくれますので大丈夫です(いわゆるフリーフォーマットって奴ですね)


よく見ると、引数の中にインスタンス値が入っています。このように、ライブラリやAPIを呼び出す際に「俺は実行ファイルの○○だぁ!濃厚なサービスを要求するぅ〜」と名乗る必要があるので、インスタンス値はアクセスしやすい形で保持しておく必要があります(べつにグローバル変数でなければいけない訳ではありません)。
なお CreateWindowEx()の返している値は、ウィンドウのハンドルです。ソースのはじめの方で宣言していたグローバル変数 hwnd は、ここで値をセットされるのですね。

さて、実はこれだけではウィンドウはまだ開きません(笑)。生成しただけで表示をしていないからです。描画は、外枠と内容の2段構えで行います。これで、ウィンドウが画面上に出現します。

ShowWindow(hwnd,nCmdShow); //外枠の描画
UpdateWindow(hwnd); //クライアント領域の描画


なお、test1.cpp ではここでDC(デバイスコンテキスト)の取得も行っています。将来のお絵描き操作のための布石です。(まだ何もしていないんですけど ^^;)

win_hdc=GetDC(hwnd);


メッセージループ:
さて、ウィンドウが開いたら、いよいよメッセージループです。Windowsのプログラムはイベントドリブン型と言って、ユーザーがなにか操作をするまでひたすらぐるぐるとループ待ちをし、イベント(キー操作など)が発生したときに、その処理ルーチンに制御を飛ばす・・・という動作をします。以下の部分です。



while(1){
//メッセージキューになにか入って来たか?
if(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
//メッセージがあれば処理する
if(!GetMessage(&msg,(HWND)NULL,0,0))break; //終了メッセージならループ抜け

TranslateMessage(&msg); //キーメッセージを文字メッセージに変換
DispatchMessage(&msg); //ウィンドウプロシージャにメッセージを送付

}else{
//メッセージが無ければ Mainloop()を実行
Mainloop();

//一瞬だけOSに制御を戻しておく
Sleep(1);
}
}


まず while(1)というところに漢を感じてください(爆)。無限ループです。終了メッセージが来るまで、ここは本当に無限ループでぐるぐる回り続けます。

ループの中身を見てみましょう。まず PeekMessage(&msg,NULL,0,0,PM_NOREMOVE) でWindowsシステムのメッセージキュー(メッセージの溜まり場と思って下さい)をのぞきます。そして自分のウィンドウに関係のあるメッセージがあるかどうかで処理を分けています。メッセージが無ければ Mainloop()が呼び出され、タッチ&ゴー式に処理を終えて戻ってきます。もしメッセージがあった場合は、それを TranslateMessage()関数でイベントハンドラが解釈しやすい文字メッセージに変換して、「はいどうぞ」と送ってやります。たったこれだけですが、実はこの部分が Windows ソフトの心臓部と言えます。いますべてを理解する必要はないと思いますが(私も知識が怪しいですし:笑)、動作内容のイメージを描くことができるだけで、今後のプログラミングはとても理解しやすくなるのではないかと思います。

ここのループ部分ですが、実はどんなに重量級のソフトでも、このスケルトンとほとんど変わらないくらい「あっさり風味」で記述してあるのだそうです(^^;)。ゲームを作る際には、もしこの test1.cpp から出発するのであれば、Mainloop() と WndProc() を充実させる方向で努力すれば良い訳ですね。

<2006.06.06追記>
ところで、Mainloop() を呼び出した後に
Sleep(1) という記述があります。これはスレッドの実行を一瞬だけ(ここでは1ミリ秒)停止してOSに制御を戻すというものです。一瞬といってもメインループはほとんどカラ回りに近い状態で抜けてくるので、実際には基本的にOS側がCPU時間を食べていて、必要に応じてプログラム側の占有度が上がるといったイメージで捕らえればよいと思います。この Sleep(1) が無いと、プログラムが意味もなくCPU時間をほとんど100%独占し続けてしまいます。この講座を最初に書いたときはこの Sleep(1) を入れないまま連載を続けていて、ちょっとみっともない動作状況(・・・まあ、動くには動くのですけどね ^^;) だったりしました。時間があれば順次ソースを直していきたいですけど、なかなか一気に書き直すのは難しいかも…(大汗 ^^;)

そうそう、最後に WinMain() の戻り値について。while(1)に入る前に終了するときは 0 を、while(1)以降に終了する際には msg.wParam を返すようにして下さい。これはプログラミング規定として決まっていることです(^^)。