ホーム < ゲームつくろー! < クラス構築編 < FPS計測クラス
FPS計測クラス
本番のゲームで必要になるかはわかりませんが、何かと欲しくなるのが「FPS計測」です。FPS(Flips par Second)は、1秒間の画面の書き換え数を表し、画面のリフレッシュレートと深く関連します。動作テストにおいてはこの数値を見て、アルゴリズムの取替えやポリゴン数の増減を判断します。FPSを計測する実装はかなり簡単なのでついつい直接書き込んでしまうのですが、一次変数等が散りばめられてしまい、結構本番のソースを汚してしまいます。そこで、FPSを測定するクラスを作成して、よりシンプルにエレガントにFPSを測定してみたいと思います。尚、クラスのソースは完全公開いたしますので、ご自由にお使い下さい。
@ FPS計測の手順
1秒間に画面が書き換わる回数をNとし、1回の描画間隔時間(ミリ秒)をtとすると、両者には次の関係が成り立ちます。
N = 1000 / t
これはある瞬間におけるFPSを表すことになります。よって、毎回再描画される度にtを計算すればよいと言うことになります。tは間隔ですから、2つの時刻が必要になります。
しかしながら、1回ごとのFPSを計測していますと、数字がめまぐるしく変わり過ぎて、何がなんだか分からなくなります。そこで、それを安定させるために「移動平均」を取っていくことにします。移動平均とは、時間順に並んだデータを決まった数(サンプル数)だけ取って、時間を徐々にずらしながら連続的に平均を計算する方法です。例えば、
58, 59, 58, 60, 57, 58, 59, 60, 58, 59
というFPSのデータがあって、5つずつ移動平均を取っていくのならば、最初の移動平均は先頭の「58」から5つ分のデータから算出され58.4となります。次の移動平均は、先頭から2つ目の「59」から5つ分の平均を取ります。同様にずらしながら平均を取っていくと、元のデータよりもなだらかで変動の少ない値が算出されてきます。一般には平均を計算するためのデータの個数を増やせば増やすほど安定度が増しますが、その分小さな変動を反映しなくなります。
今回は、過去n個のFPSの移動平均を求めていく実装にしましょう。
A クラスの公開インターフェイス
今回作成するFPS計測クラスはCFPSCounterという名前にします。このクラスはツールクラスですから、扱いは極々簡単にしたいものです。そこで、GetFPSというインターフェイスを公開し、この関数を呼ぶたびにFPSが算出されるようにします。他に、移動平均を取る時のサンプル数も決められるようにします。これはSetSampleNumという公開インターフェイスで実装しましょう。
重要なのは時間計測の方法です。最初に申しましたが、FPSを算出するには「時間t」が必要です。時間は2つの時刻の間の事ですから、1つの時間を算出するには、2つの時刻を測定する必要があります。よってこのクラスでは、前のGetFPS関数が呼ばれてから、次のGetFPS関数が呼ばれるまでの時間を「描画間隔時間」として扱うことにします。
「最初の時刻はどうするの?」と思った方は鋭いです。これは、生成した時に取得することにします。ですから、最初の1回目の計測は描画間隔時間を正しく表しません。しかし、その値はあっという間に消えてしまうでしょうから、無視してしまいましょう。
B 移動平均アルゴリズム
FPSを計算する時に移動平均を取ります。平均を取るためには、その分のデータを保持しておかなければなりません。よって、メンバ変数にFPSを格納するn個数分の配列を用意します。これは問題ありません。しかし、平均を取ると言う作業で少しやな部分が出てきます。例えば、極端な例ですが、10000個分のFPSから移動平均を取っていくとしましょう。単純に考えると、10000回の足し算をして、10000で割る作業をすることになります。しかし、10000回のループは考えただけでぞっとします。そこで、移動平均のアルゴリズムを最適化してみます。
まず、次のようにFPSの測定データがn個並んでいるとしましょう。
時系列に測定データが並んでいて、左が一番古いデータ、右端が最新のデータだとします。ここに新しいデータが入ってきました。
左端の古いデータは捨てられ、新しいデータ(赤)を加えて改めて平均を求めることになります。ここで注目は緑色のデータ。上の図から明らかなのですが、1つ前の状態と、最新の状態で、この緑色の部分の合計は全く一緒になりますね。と言うことは、この計算は1つ前の状態のときにすでに終わっているわけです。ですから、前に計算した値を使えば、ここの部分を改めて計算する必要はなくなってしまいます。つまり新しい移動平均値は、
という2回の計算だけで終わってしまいます。
この計算の後、緑色の部分を更新するのですが、これはとても簡単で、緑の中で一番古いデータを引いて、新しいデータ(赤)を加えるだけです。これを毎回繰り返せば、移動平均はサンプル数に関係なく超高速に計算されます。
古いデータを破棄するというのは、簡単そうで結構面倒です。データは配列に蓄積されるわけですが、一番古いデータは配列の先頭に位置することになります。しかし、それを破棄すると、今度は2番目の位置に古いデータが来ることになります。ですから単純に考えてしまうと、古いデータを削除後、配列の全てのデータを1つずつ前にずらして、空いた配列の最後尾に最新のデータを格納する必要が出てくるわけです。しかし、それでは上で移動平均のアルゴリズムを最適化した意味がありません。
これを解決するのが「リスト構造」です。リストであれば、データの追加や切り離しを高速に行うことが出来ます。先頭のデータを切り離し、新しいデータを最後尾にくっつけるだけで、望む配列が出来てしまうわけです。
リストを実装することは簡単ですが、今回は面倒なので素直にSTLのlistを使うことにします。
C 計測する時計は何が良いか?
アルゴリズムは問題なさそうです。もう1つ厄介なのが「時刻を計測する時計」です。WindowsというOSには様々な時刻計測関数があります。その中でまともに使えるのがtimeGetTime API関数とQueryPerformanceCounter API関数です。timeGetTime関数はミリ秒単位で現在の時刻を取得します。一方のQueryPerformanceCounter関数は、CPUの1クロックタイムレベルで時刻の取得ができます。もちろんQueryPerformanceCounter関数の方が精度が数千倍高くなります。ただ、オーバーヘッドという観点からすると、timeGetTime関数の方がオーバーヘッドが少なく、瞬間時刻を取ってくれることになります。
どちらを使うのが良いかというと、これははっきりとQueryPerformanceCounter関数の方が良いはずです。オーバーヘッドの時間は極僅かで、正確さの面からしてこちらがダントツです。ただ、この関数は環境によっては使えないことがあります。ですから、もしQueryPerformanceCounter関数が使えるのならばこちらを、そうで無い場合はtimeGetTime関数で代用するという方式を取る事にしましょう。
QueryPerformanceCounter関数が使えない場合、戻り値は0になります。ですから、クラスのコンストラクタにおいて次のような判定文を書き、フラグで切り替えを行うようにします。
if(QueryPerformanceCounter( &m_Counter ) != 0)
{
// QueryPerformanceCounter関数を使うフラグ
m_CounterFlag = FPSCOUNTER_QUERYPER_COUNTER;
}
else
{
// timeGetTime関数を使うフラグ
m_CounterFlag = FPSCOUNTER_TIMEGETTIME;
}
// 計測
GetFPS();
GetFPS関数の内部では、前回の呼び出しからの差分時刻を計測するGetCurDefTime関数を呼び出すことにします。これら関数については公開コードをご覧頂けばすぐにわかります。
D QueryPerformanceCounter関数の扱い
まずはQueryPerformanceCounter関数の定義です。
BOOL QueryPerformanceCounter(
LARGE_INTEGER *lpPerformanceCount
);
この関数は引数にLARGE_INTEGER構造体を取ります。この構造体の中に現在までの経過クロックカウント数が格納されます。この構造体は次のような定義になっています。
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
LONGLONG QuadPart;
} LARGE_INTEGER;
LowPartと言うのが下位32ビットの値、HightPartと言うのが上位32ビットの値を示します。LONGLONG型であるQuadPartには64bitの数字が直に入ります。扱いやすいのは後者の方です。
この関数で取られるのは上記の整数なのですが、その大きさは64bitです。電源を入れてから何クロック経過したかというクロック数が返されます。どうしてそのような巨大な整数値が必要かといいますと、例えば3GHzのCPUの場合、1秒間で3GHz、つまり約3,000,000,000(30億)回もカウントが進んでしまいます。これは高々40億くらいしかカウントできない32bit符号無し整数だと2秒持ちません。そのため64bitという凄まじく大きい整数の箱に入れる必要があるわけで、上のような構造体が必要になったわけです。
QueryPerformanceCounter関数で得られる値は電源を入れてから現在までのクロックカウント数ですが、1秒間のカウント数が分からなければカウント差を時間に変換できません。1秒間当たりのカウント数はQueryPerformanceFrequency関数で取得できます。
BOOL QueryPerformanceFrequency(
LARGE_INTEGER *lpFrequency
);
引数のlpFrequencyには正確なクロック周波数(1秒間辺りのクロックカウント数)が格納されます。
2回のQueryPerformanceCounter関数から算出される差分時間は、やはりLONGLONG型の整数に格納するのが良いのですが、これをクロック周波数で割ると、整数同士の割り算なので整数値しか出てきません。そこで、いったん倍精度浮動小数点(double)に変換してから計算を行います。
QueryPerformanceCounter( &m_Counter ); // 現在の時刻を取得
QueryPerformanceFrequency( &m_dFreq ); // クロック周波数
LONGLONG LongDef = m_Counter.QuadPart - m_OldLongCount; // 差分カウント数を算出
double dDef = (double)LongDef; // 差分クロック数を倍精度浮動小数点に変換
dDef = dDef * 1000 / m_dFreq; // 差分時間(ミリ秒)を算出
生成時のdDefには差分カウント数が入ります。最後の行でこれを1000倍してクロック周波数で割っています。これにより差分時間をミリ秒単位で得ることが出来ます。小さなことですが、最後の行を、
dDef = dDef / m_dFreq * 1000;
という計算はしない方が良いでしょう。こうすると精度が落ちる場合があります。
E timeGetTime関数の扱い
QueryPerformanceCounter関数が使えない場合の代替法としてtimeGetTime関数があります。この関数は現在の時刻をミリ秒単位で取得できます。この関数を使用するためには、winmm.libライブラリをライブラリパスに通し、windows.hとmmsystem.hをインクルードする必要があります。使い方はとても簡単で、以下のようにします。
DWORD CurTime = timeGetTime();
これで現在の時刻がミリ秒単位で格納されます。現在の時刻というのは起動してからの経過時間の事です。
このタイマーは「精度」を指定することが出来ます。
// 精度を上げる
timeBeginPeriod(1);
// 変更を戻す
timeEndPeriod(1);
timeBeginPeriod関数はtimeGetTime関数の精度をミリ秒単位でしています。この関数はWindows NT系のプラットフォームの場合に特に有効です。これは、有無を言わず上記のように最高精度にしておけば問題ないでしょう。timeBeginPeriod関数を呼び出した後、timeGetTime関数が必要なくなったら、timeEndPeriod関数を呼び出す必要があります。
ミリ秒単位の差分時間を得る方法は、言わずもがななのですが、一応示しておきます。
DWORD Def = timeGetTime() - OldTGTTime;
F クラス図
なんだかタイマーの使い方で終わってしまった気がしますが、以上でFPSの算出は簡単に出来ます。具体的な実装はソースを見て頂いた方が早いと思いますので、公開プログラムをじっくりとご参照下さい。やっていることは非常に単純で簡単なことですが、扱いやすさ抜群のクラスです。ちなみに、クラス図はこうなりました。
扱いがとても簡単で使えるクラスです。是非ご利用下さい。