はかせのラボ

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

DirectX リフレッシュレートとFPS ~実装編~

あいさつ

12月に入りなぜか気温が上がりましたね。
ただなにやらちょっとしたら一気に冬が来るらしいので
タツが欲しいはかせです。
今回はFPS固定の実装です。

FPS固定方法

大きく分けて3つあります
①Sleepを使う
②Timer割り込みを使う
③Vsyncを使う

今回私は③を使って固定しました。
1つずつ簡単に説明していきます。

Sleepを使う

経過時間(今時間-前フレーム時間)を計算し、
その値が1フレームの処理時間内なら余ってる分処理を止める方法です。

実装難度とパフォーマンス、環境を選ばないという点で
すごく良い方法だと思っています。
基本困ったらこの方法でいいと思います。

Timer割り込みを使う

WindowsにはSetTimerという一定時間ごとにメッセージを発行してくれる関数があります。
この関数を使って1 / 60秒ごとにメッセージを発行し、
それを受けてから処理行う方法です。

この方法はとにかく実装が簡単です。つまり開発が早いです。
ただ楽ということは精度がお察しということでもあります。
SetTimer関数はミリ秒指定しかできません。

つまり
1 / 60≒16ms
1000 / 16=62.5fps
となってしまい、今回の私の作った弾幕シューティングのような
リアルタイム性が重要なゲームでは使えません。

逆にカードゲームのようなそこまでリアルタイム性がいらないゲームであれば
十分採用できる気がします。

Vsyncを使う

一番理想的なものです。
実装も楽ですし①②ではティアリングと呼ばれる画像のチラつきが起こる可能性がありますが、この方法では起こりえません。
ただ前回の理屈偏で話したようにディスプレイのリフレッシュレート次第で困ることがあります。

今回はリフレッシュレート問題を解決しこの方法でFPSを固定する方法の一例を紹介します。

解決方法

前回は1秒で1m進むプログラム
60FPSで1m進むプログラムであると言えるという話。
そしてFPSが120になったら1秒で2m進むに変わる、
という例を使ってFPS固定が必要な理由を説明しました。

この問題の解決策は2つあります。
①ディスプレイのリフレッシュレートを変更する
②リフレッシュレートに応じて画面同期間隔を変更する

できるのならば①が楽です。間違いもありません。
ただ配布等のことを考えた場合、適切な解決策ではありません。
①の場合リフレッシュレートが変更できるディスプレイならばいいですが、
できないものの場合リフレッシュレートは変更されません。

なので②です。
Vsyncは画面更新とフレーム更新を同期させる手法と言いました。
ただこの同期間隔は実はプログラム側で指定できます。これを弄ります。

WindowsにはGetDeviceCapsというデバイス情報取得の関数があります。
これを使ってリフレッシュレートを取得し
60以下なら1フレごとに、120以上なら2フレごとに同期するようにします。

//デバイス取得
auto hdc = GetDC(hwnd);
auto rate = GetDeviceCaps(hdc, VREFRESH);
//リフレッシュレートに応じて同期間隔を設定
int intarval = 0;
if (rate <= 60) intarval = 1;
if (rate >= 120) intarval = 2;
SetVsyncIntarval(intarval);

最初はリフレッシュレートを60で割った値を直接同期間隔にしていたのですが
うまくいく時といかないときがあり不安定だったので、if文で値を直接設定しています。

(同期間隔の設定はUINT型で設定するのですが、
小数点以下が発生した場合キャストかなんかで誤差が発生して
うまく意図した値になってないのかなーといった予想です。)

長々と話しましたが、
私は今回この方法を使いリフレッシュレートが60Hzと120Hzの
2つの環境でFPSを60に固定しました。

あとがき

なぜこんなことをしたかというと今回作ったものを別環境に持って行ったとき
ゲームが倍速になるという問題が発生したからです。

弾幕シューティングで弾が倍速になるってなかなかやばいですよねw
ということで直すために悪戦苦闘したお話でした。

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

今回作ったものはgithubに上げました
github.com