はかせのラボ

私の頭の中を書いていく雑記ブログです

プログラミング タイムベースで処理を進める

あいさつ

どうも、はかせです。
今回は前回のアドバイスにあった
タイムベース処理を実装してみました。

理屈

理屈は極めて単純です。
処理が1/60秒以内に終わった場合、
1/60秒まで処理を止める
ことでフレームレートを固定します。

この方法の利点は何といっても
環境を選ばないことです。

もちろん時間計測に使うAPIとかによって
多少の差はあれどもフレームベースでやることに比べたら微々たるものです。

実装

実装です。

//1フレームの時間
const float FRAME_TIME = 1.0f / 60.0f;
//フレームの経過時間
float frameTime = 0;
//計測開始時間
LARGE_INTEGER timeStart;
//計測終了時間
LARGE_INTEGER timeEnd;
//計測周波数
LARGE_INTEGER timeFreq;

//今回のメイン関数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hInstancePrev, LPSTR pCmdLine, int nCmdShow) 
{
	// ウィンドウクラスを登録する
	WNDCLASS windowClass = { 0 };
	windowClass.lpfnWndProc = WinProc;
	windowClass.hInstance = hInstance;
	windowClass.lpszClassName = mClassName.c_str();
	RegisterClass(&windowClass);
	// ウィンドウの作成
	mWindowHandle = CreateWindow(mClassName.c_str(), mWindowName.c_str(), WIN_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, WindowWidth, WindowHeight, NULL, NULL, hInstance, NULL);
	if (mWindowHandle == NULL) return 0;
	ShowWindow(mWindowHandle, nCmdShow);
	// メッセージループの実行
	MSG msg = { 0 };
	//周波数取得
	QueryPerformanceFrequency(&timeFreq);
	//計測開始時間の初期化
	QueryPerformanceCounter(&timeStart);
	//何フレーム目か
	int frameCount = 0;
	//ログを出力する回数
	int outputLogCount = 10;
	float fps = 0.0f;
	while (msg.message != WM_QUIT) 
	{
		
		if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) 
		{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else 
		{
			
			// 今の時間を取得
			QueryPerformanceCounter(&timeEnd);
			// (今の時間 - 前フレームの時間) / 周波数 = 経過時間(秒単位)
			frameTime = static_cast<float>(timeEnd.QuadPart - timeStart.QuadPart) / static_cast<float>(timeFreq.QuadPart);

			//経過時間が1/60秒未満(処理時間に余裕がある)
			if (frameTime < FRAME_TIME) 
			{ 
				//Sleepの時間を計算
				DWORD sleepTime = static_cast<DWORD>((FRAME_TIME - frameTime) * 1000);
				//分解能を上げる(こうしないとSleepの精度はガタガタ)
				timeBeginPeriod(1); 
				//寝る
				Sleep(sleepTime);  
				//戻す
				timeEndPeriod(1);   
				//処理を中断しループへ
				continue;
			}
			else
			{ 
				frameCount++;
				bool isOutputLog = frameCount % outputLogCount == 0;
				fps = mFPSCounter->GetFPS();
#ifdef _DEBUG
#ifdef UNICODE
				std::wstringstream stream;
#else
				std::stringstream stream;
#endif
				if(isOutputLog)
				{
					stream << fps << " FPS" << std::endl;
					OutputDebugString(stream.str().c_str());
				}
#endif
			}
			//計測終了時間を計測開始時間に
			timeStart = timeEnd; 
			// DirectXのループ処理                        
			mDXManager->Update();
		}
	}
	return 0;
}

QueryPerformanceFrequencyはカウントアップする周波数を取得します。
QueryPerformanceCounterは現在のカウント数を取得します。
カウント数を周波数で割ることで経過時間を求めることができます。

あとはこの経過時間をみて
時間内ならSleepして時間外なら処理をするって感じです。

では結果です。
今回はFPS固定の実装なのでログに出力したFPSの画像です。
f:id:hakase0274:20190615202728p:plain
まぁ最大FPSの固定なので、
58ぐらいに落ち着くのは妥当と言えば妥当でしょう。

あとがき

今回は時間を用いたFPSの固定でした。
正直動作環境が固定されているならば
私が今までやっていたフレームベースの方が実装も楽ですし、
何よりテアリングという画面のチラツキが絶対発生しないのでいいと思います。

ただPCで動作するものとかだと各PCの性能によってばらつきが出てしまうので
今回みたいな時間で固定する必要があります。

それでは今回はこの辺でノシ
今回作ったものはgithubに上げました
github.com

雑記 作品見てもらったまとめ

あいさつ

どうも、はかせです。
今回は業界内では有名な某提督に作品を見てもらい、
アドバイスをもらったのでまとめます。

パスとかは外部ファイルに

これは私が
「定数使うのに#defineとconstだったらどっち使ったほうがいいですかね」
という質問をした時に教えてもらったことです。

要は

#define PATH _T("nanntoka.png")
const int NANTOKA_INT = 10;

こんな感じでやってるとこをcsvなりxmlなりで締め出して
読み込む形がいいとのことです。

この形にすることで値の更新をした時に
逐一コンパイルが走らず、
ゲーム開発のイテレーションが早くなるとのことです。

フレームベースレンダリングではなくタイムベースレンダリング

これは某提督ではないですが、作品について教えてもらったので一緒にまとめます。
現在私の作品はフレームベースで動いており、
リフレッシュレートが60でないときは
リフレッシュレートを60で割った値でVSyncを行っています。

ただこのやり方だと75Hzみたいな60で割り切れない値の時
うまく値が算出されず、挙動がおかしくなってしまいます。

この問題を解決するためにはフレームベースではなくタイムベースで処理を進めるべきだというアドバイスをもらいました。

単位時間で処理することで環境のリフレッシュレートに依存せず安定した挙動ができるようになります。
リフレッシュレートに関しては過去にそれとなくまとめたものがあります
hakase0274.hatenablog.com
hakase0274.hatenablog.com

今後はSleepを用いたタイムベースの処理に切り替えていこうと思います。

あとがき

今回は作品を見てもらいアドバイスをもらった話です。

今回のアドバイスはゲーム開発の実践編の定石的なものだと思います。
それを知ることができた、その壁にぶつかったのはようやっと初心者の中の初心者を脱せたという風にとらえられるかなと思っておきます。
(まぁ私が無知すぎたんですけどねw)

今後も色々難しいことから簡単なとこまで、いろんなとこにぶつかりながら
学習を進めていきたいと思います。

それでは今回はこの辺でノシ