【C++】WindowsAPIで60fpsのゲームループを実装
本稿ではC++(WindowsAPI)でゲームループを作ってみます。
ゲームループはメッセージループのスレッドを利用する方法をやってみます。
ついでに、fps調整の処理もやっておこうと思います。
メッセージループを利用したゲームループ
PeekMessage関数はGetMessage関数と同じくウィンドウメッセージを取得できますが、メッセージ取得待ちが発生しません。
PeekMessage( メッセージ情報を受け取る構造体へのポインタ , NULL , 0, 0 , メッセージの処理方法 )
詳細は公式サイトへどうぞ。
取得したメッセージはMSG.messageで取得できます。messageがWM_QUITである場合はアプリケーション終了メッセージを表しますので、ループを抜けるようにしておきます。(WM_QUITはPostQuitMessageを実行した時発生)
これらを利用してメッセージループの部分を作ってみます。例えば以下のように組むとよさそうです。
//メッセージループ MSG msg = {}; while (true) { //PM_REMOVEはメッセージ処理後に削除することを表す。基本PM_REMOVEでOK if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { break; } TranslateMessage(&msg); DispatchMessage(&msg); } else { //ゲームの処理 //17ミリ秒スレッド待機 std::this_thread::sleep_for(std::chrono::milliseconds(17)); } }
せっかくなのでこのゲームループを使ってアニメーションを作ってみましょうか。
アニメーションはGDIを使用して実装します。最終的にはDirectXを使用するので、GDIに関しては紹介しません。
#include <windows.h> #include <thread> #include <chrono> #include <string> LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_CLOSE: return DefWindowProc(hwnd, message, wParam, lParam); case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, message, wParam, lParam); } return 0L; } int WINAPI WinMain( HINSTANCE hInstance, // 現在のインスタンスのハンドル HINSTANCE hPrevInstance, // 以前のインスタンスのハンドル LPSTR lpCmdLine, // コマンドライン int nCmdShow // 表示状態 ) { HWND hWnd; LPCTSTR szclassName = "WinAPITest"; WNDCLASSEX wcex; ZeroMemory((LPVOID)&wcex, sizeof(WNDCLASSEX)); //ウィンドウクラスを登録 wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = 0; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = szclassName; wcex.hIconSm = NULL; RegisterClassEx(&wcex); //ウィンドウ作成 hWnd = CreateWindowEx(0, szclassName, "Title", WS_OVERLAPPEDWINDOW, 0, 0, 300, 100, NULL, NULL, hInstance, NULL); //ウィンドウ表示 ShowWindow(hWnd, SW_SHOW); //メッセージループ MSG msg = {}; int cnt = 0; HDC hdc = GetDC(hWnd); while (true) { //メッセージを取得したら1(true)を返し取得しなかった場合は0(false)を返す if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { //終了メッセージが来たらゲームループから抜ける break; } TranslateMessage(&msg); DispatchMessage(&msg); } else { //ゲームの処理を記述 //DirectXの描画処理などもここに記述する //今回はGDIでカウントアップを描画する処理で動作テスト cnt++; std::string str = std::to_string(cnt); const char* result = str.c_str(); TextOut(hdc, 10, 10, result, str.size()); //.... //17ミリ秒スレッド待機 std::this_thread::sleep_for(std::chrono::milliseconds(17)); } } //解放 ReleaseDC(hWnd, hdc); }
ゲームループを任意のfps(描画数)に保つ
fpsとは1秒間に画面が何回更新されるかを表す単位です。(フレームレートのほうが一般的な言い方かもしれません。)例えば40fpsのゲームなら1秒間に40回画面を描画しなおしています。
ゲーム制作では60fpsや30fps等、処理の重さによって描画数を変更したい場合がある為、これの調整をできるようにしておくと便利です。
例えば60fpsなら1000ミリ秒/60で16.6666….ミリ秒スレッドスリープを挟めば良いです。
しかし1ミリ秒以下のスリープはまともに機能しない環境もあるので、60fpsにするには16、17ミリ秒スリープを適度に調整してあげる必要があります。(そもそもスレッドスリープ自体確実に指定時間待機できるわけではないです。)
また、ゲームの処理にかかった時間を待機時間から省いてあげるのもfpsを保つには重要です。これは現在時刻からゲーム処理前の時間を引き算するだけで求まります。
これらを実現するために、1フレームごとに次のフレームの処理開始時間を求め、処理後に時間が余った分を待機時間とします。
例えば50fpsの処理の流れはこんな感じですかね。
実際のコードを作成する前にfps計測する方法も確認しておきます。そのまま実行しても60fpsになっているのかわからないですからね。
fpsの計算は「(1秒)/(現在時刻-計測開始地点の時刻)*(開始地点から現在までのフレーム数)」で計測できます。
サンプルではFrameRateCalculatorクラスを作成し、この機能を持たせることにします。
それでは実際にコードを作成してみましょう。一応マイクロ秒単位での待機にしてますが、ミリ秒単位での待機に変えても対応できているはずです。
#include <windows.h> #include <thread> #include <chrono> #include <string> //フレームレート計算クラス class FrameRateCalculator { long long cnt = 0; const int limit = 60; std::string fpsStr = "0fps"; long long time = currentTime(); //現在時刻を取得する関数 long long currentTime() { std::chrono::system_clock::duration d = std::chrono::system_clock::now().time_since_epoch(); return std::chrono::duration_cast<std::chrono::milliseconds>(d).count(); } //フレームレートの計算と結果文字列を構築する void updateStr() { //fpsを計算し、文字列として保持する long long end = currentTime(); double fpsResult = (double)(1000) / (end - time) * cnt; time = end; fpsStr = std::to_string(fpsResult) + "fps"; cnt = 0; } public: //フレームレート更新メソッド std::string* update() { cnt++; //規定フレーム数になったらフレームレートの更新 if (limit <= cnt) { updateStr(); } return &fpsStr; } }; LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, message, wParam, lParam); } return 0L; } int WINAPI WinMain( HINSTANCE hInstance, // 現在のインスタンスのハンドル HINSTANCE hPrevInstance, // 以前のインスタンスのハンドル LPSTR lpCmdLine, // コマンドライン int nCmdShow // 表示状態 ) { HWND hWnd; LPCTSTR szclassName = "WinAPITest"; WNDCLASSEX wcex; ZeroMemory((LPVOID)&wcex, sizeof(WNDCLASSEX)); //ウィンドウクラスを登録 wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = 0; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = NULL; wcex.hCursor = LoadCursor(NULL, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_BACKGROUND + 1); wcex.lpszMenuName = NULL; wcex.lpszClassName = szclassName; wcex.hIconSm = NULL; RegisterClassEx(&wcex); //ウィンドウ作成 hWnd = CreateWindowEx(0, szclassName, "Title", WS_OVERLAPPEDWINDOW, 0, 0, 300, 100, NULL, NULL, hInstance, NULL); //ウィンドウ表示 ShowWindow(hWnd, SW_SHOW); //現在時刻をマイクロ秒で取得 std::function<long long(void)> currentTimeMicro = []() { std::chrono::system_clock::duration d = std::chrono::system_clock::now().time_since_epoch(); return std::chrono::duration_cast<std::chrono::microseconds>(d).count(); }; //fps計算用オブジェクト FrameRateCalculator fr; //メッセージループ MSG msg = {}; int cnt = 0; HDC hdc = GetDC(hWnd); //60fpsで動作させる int fps = 60; //現在時刻を取得(1秒=1000000) long long end = currentTimeMicro(); //次の更新時間を計算(1秒/フレームレート) long long next = end + (1000 * 1000 / fps); while (true) { //メッセージを取得したら1(true)を返し取得しなかった場合は0(false)を返す if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { //終了メッセージが来たらゲームループから抜ける break; } TranslateMessage(&msg); DispatchMessage(&msg); } else { //ゲームの処理を記述 //DirectXの描画処理などもここに記述する //今回はGDIでカウントアップを描画する処理で動作テスト cnt++; std::string str = std::to_string(cnt); const char* result = str.c_str(); TextOut(hdc, 10, 10, result, str.size()); //重い処理があったとする std::this_thread::sleep_for(std::chrono::milliseconds(5)); //fps描画 std::string* fpsStr = fr.update(); const char* fpsStrResult = fpsStr->c_str(); TextOut(hdc, 100, 30, fpsStrResult, fpsStr->size()); //できるだけ60fpsになるようにスレッド待機 end = currentTimeMicro(); if (end < next) { //更新時間まで待機 std::this_thread::sleep_for(std::chrono::microseconds(next - end)); //次の更新時間を計算(1秒/フレームレート加算) next += (1000 * 1000 / fps); } else { //更新時間を過ぎた場合は現在時刻から次の更新時間を計算 next = end + (1000 * 1000 / fps); } } } //解放 ReleaseDC(hWnd, hdc); }
大体60fpsになっていますかね。処理落ちしたら当然60fpsになりません。デバッグモードだと処理落ちしやすいので気を付けてください。
ちなみにDirectXを利用する場合は垂直同期が利用できるため、モニターの描画数に合わせたい場合に限り、スレッド待機の処理を作る必要がありません。だいたい60fpsになってるので、必要ない場合は、あえて実装する必要はないです。
ディスカッション
コメント一覧
まだ、コメントがありません