【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になってるので、必要ない場合は、あえて実装する必要はないです。










ディスカッション
コメント一覧
まだ、コメントがありません