クラスまみれのゲームプログラミング入門

15:タスク処理を実装する(5)

 Direct3Dの描画処理タスクを作成し、リストに追加します。描画開始処理の実行優先度は0、描画終了処理の実行優先度はINT_MAXにし、敵キャラなど描画に必要なデータの優先度は1~(INT_MAX - 1)の範囲とします。関数の中でCGameObject派生クラスを直接newで作っても全く問題ありません。autodelete引数をtrueにしておけば、削除はプログラムが勝手にやってくれます。

 では、「GameMain.h」「GameMain.cpp」の2つのファイルを新規にプロジェクトに追加し、描画開始処理と終了処理を行うためのクラスを作成しましょう。

ウェイト処理を用意する

 家庭用テレビが1秒間あたり60回の表示更新を行うこともあってか、たいていのゲームは1秒間に付き60回、つまり約16ms(=0.016秒)につき1回描画を行うような設計になっています。しかし、単純に指定時間だけ待機するウィンドウズ命令「Sleep(16);」を記入しただけでは不都合が起こります。例えば、ゲーム処理の計算に10msかかったとしたら、実際に画面が更新されるのは「10ms + Sleep(16) = 26ms」後になり、コンピュータにそれほど負担がかかっているわけでもないのに、処理落ちしたかのような挙動になってしまいます。

 これを解決するには、処理計算にかかった時間を取得して、ゲーム標準の待機時間から差し引けばよいことになります。

wait.cpp
	const DWORD WAIT_TIME = 16;	// 約60FPS

	//最後に行った処理からの時間を調べる
	DWORD ntime = timeGetTime();	// システムが起動してからの時間を取得
	DWORD rtime = ntime - lasttime;
	lasttime = ntime;
	if(rtime < WAIT_TIME){
		//ウェイト処理を行う
		Sleep(WAIT_TIME - rtime);
	}

 しかしながら、Sleepで指定した待ち時間はあいまいで、時と場合によっては、指定した時間の倍以上の待機が行われることもあります。この待ち時間を厳密にするには、WinMain関数のはじめに次の命令を追加してください。

timeBeginPeriod(1);

 これにより、時間計測の厳密さが1ミリ秒単位に変更されるので、正確なウェイト処理が行われるようになります。


描画開始・終了処理を用意する

 おおまかにすると、Direct3Dによる描画に必要な一連の命令は以下のようになります。
  • Clear()で描画済みのデータを消去します。
  • BeginScene()で、バックグラウンドにてシーンの描画を始めます。
  • ID3DXSprite::Draw()などをつかって、描画を行います。
  • EndScene()でシーンの描画を終わります。
  • Present()でウィンドウに描画結果を転送し、実際に表示されます。

 これをタスク処理に割り当てると、描画開始時に「Clear()」と「BeginScene()」、描画終了時には「EndScene()」と「Present()」を行えばよいことになります。これらをふまえてクラスを作成した結果がこちらです。

GameMain.h
#pragma once

#include "GameObject.h"

class CTaskHead : public CGameObject
{
private:
	DWORD lasttime;
protected:
	void Init();
	void Exec();
};

class CTaskTail : public CGameObject
{
protected:
	void Exec();
};

GameMain.cpp
#include "GameMain.h"

void CTaskHead::Init()
{
	lasttime = 0;	// 値の初期化
}

void CTaskHead::Exec()
{
	const DWORD WAIT_TIME = 16;	//約60FPS

	//最後に行った処理からの時間を調べる
	DWORD ntime = timeGetTime();
	DWORD rtime = ntime - lasttime;
	lasttime = ntime;
	if(rtime < WAIT_TIME){
		//ウェイト処理を行う
		Sleep(WAIT_TIME - rtime);
	}

	//画像のクリア
	pD3Ddevice->Clear(
		0,						// クリアする領域の配列個数
		NULL,					// クリアする領域の配列
		D3DCLEAR_TARGET,		// 対象を指定の色でクリアする
		D3DCOLOR_XRGB(0,0,64),	// クリアする色を紺色に指定
		1.0f,					// z方向のクリア(1.0fですべてをクリア)。
		0						// ステンシルバッファのクリア(使用していないので0を指定)
	);

	//開始宣言
	pD3Ddevice->BeginScene();
}

void CTaskTail::Exec()
{
	// 表示
	pD3Ddevice->EndScene();
	pD3Ddevice->Present(
		NULL,	// 転送元の領域(NULLで全域指定)
		NULL,	// 転送先の領域(NULLで全域指定)
		NULL,	// 転送先のウィンドウを示すハンドル(NULLで標準のウィンドウ)
		NULL	// 現行バージョンでは常にNULLを指定
	);
}

 この2つのクラスをWinMain()内において、初期化処理の次の行あたりでタスクリストに追加します。

winmain.cpp
	HWND hWnd = CreateWindow(
		wc.lpszClassName,
		_T("クラスまみれのゲームプログラミング入門"),
		WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,	
		NULL, NULL, hInstance, NULL );

	CGameObject game;
	game.Initialize(hWnd, hInstance);

	game.AppendObject(new CTaskHead(), 0, true);
	game.AppendObject(new CTaskTail(), INT_MAX, true);

	ShowWindow(hWnd, nCmdShow);

 こうしてできたプログラムをコンパイル(timeGetTime()を有効にするため「winmm.lib」のリンクを忘れずに!)して実行してみましょう。紺色の画面が延々と表示され続けていたら成功です。一見静止しているように見えますが、裏では1秒間に60回ものスピードで画面が更新されているんですねぇ。