3-3 CTimerクラスを作ろう

ここでは前回紹介した高解像度タイマー関数を使ったクラスを作ります。
CTimerクラスは音ゲーに限らず他のどのゲームでも利用可能なようにします。

ちなみにこのクラスは音ゲーのタイミングを取るためのものではありません。
例えば光るエフェクトなどフレーム単位でアニメーションが必要なものを、
これを使うことで処理落ちするPCや逆に処理の早いPCなどで統一することが出来ます。

もしこれが無いとあるマシンではエフェクトがとても早くなってしまったり、
またあるマシンでは逆に遅すぎてエフェクトが間に合わないと、
スローのようなアニメーションとなってしまいます。

なお、アニメーションを全て実時間で計算するような設計であればこのようなクラスは必要ありません。
このクラスはあくまでも、フレーム単位でのアニメーション処理に使用するものと考えてください。


■仕様

プログラム全般に言えることですが、プログラムをする際にまずはそのプログラムが何をするものなのかを整理しておきます。
何も考えずにプログラムを始めると、あとで機能の追加がしたいといった時に最悪作り直しとなってしまいます。

まずここで作成するCTimerは以下の機能を実装することとします。

1.高解像度タイマーが使用可能かチェック
2.マシンの周波数を取得
3.指定のフレームレートで実行開始
4.飛ばされたフレーム数を返す



■ヘッダファイル

クラスの定義として必要なメソッドを先に決めます。
また使用する変数なども定義しておきます。

以下がタイマー処理クラスのヘッダになります。

#ifndef _CTIMER_H
#define _CTIMER_H
////////////////////////////////////////////////////////////////////////////
// CTimer : タイマー処理クラス v2.00
////////////////////////////////////////////////////////////////////////////
#include <windows.h>

class CTimer {
protected:
    BOOL            bPerf;          // 高解像度タイマーがサポートされているか
    LARGE_INTEGER   mFreq;          // システム周波数
    LARGE_INTEGER   mStart;         // 開始時間
    int             iFps;           // 割り込みタイミング
    int             iCount;         // 割り込みカウント数
public:
    CTimer();
    virtual ~CTimer();
    BOOL Start( int fps=60 );       // 指定のFPSで割り込みスタート
    int Run( void );                // 割り込みがあれば1以上を返す(割り込み回数)
};

#endif



■コンストラクタの処理

仕様の中の1と2はコンストラクタで行うことが出来ます。
以下がコンストラクタになります。

CTimer::CTimer()
{
    if( QueryPerformanceFrequency(&mFreq) ) {
        // 高解像度カウンターに対応
        bPerf   = TRUE;
    } else {
        // 高解像度カウンターに非対応
        mFreq.QuadPart  = 1000LL;       // timeGetTime()で代用
        bPerf   = FALSE;
    }
    iFps = 60;
}

ここではシステム周波数を取得し、それが失敗した場合は低解像度の
タイマーを使用するように初期化をします。


■デストラクタ

このクラスのデストラクタは特に何もしません。
ヘッダでの定義自体無くても問題ありませんが、
ここではコンストラクタとデストラクタをセットとして考えているので、
何もしないデストラクタを定義します。

CTimer::~CTimer()
{

}



■タイマー開始

Start()メソッドは指定のフレームレートで動作するように初期化をし、
そのタイマーカウンタをスタートさせます。
スタートといっても現在のタイマーを取得して、それが開始時の時間として保存するだけです。

高解像度タイマーが使用できなければ低解像度タイマーの値を取得します。

BOOL CTimer::Start( int fps )
{
    iFps    = fps;
    iCount  = 0;

    if( bPerf ) {
        QueryPerformanceCounter( &mStart );
    } else {
        mStart.QuadPart = (LONGLONG)timeGetTime();
    }
    return TRUE;
}



■経過したフレーム数の取得

メインループ内でRun()を呼び出すと、前回Run()を呼び出したときから何フレーム経過したかを返します。

このフレーム数が0だった場合はまだ1フレームも経過していないことになり、この場合は何も処理をさせないようにします。

もし1以上ならその数だけフレーム計算を行うことで、処理落ちした分も補間して同期を取ることができます。

int CTimer::Run( void )
{
    LARGE_INTEGER now;
    if( bPerf ) {
        QueryPerformanceCounter( &now );
    } else {
        now.QuadPart = (LONGLONG)timeGetTime();
    }

    int count = (int)((now.QuadPart - mStart.QuadPart) / (mFreq.QuadPart/iFps) );
    if( count!=iCount ) {
        int ret = count - iCount;
        iCount = count;
        return ret;
    }

    return 0;
}

この関数内では以下のことを行っています。

まず現在のタイマーからスタート時のタイマー値を差し引くことで、スタート時から経過したカウント数を算出します。
それを1秒間のカウント数からFPS値で割った値で割ることで、スタートから今まで何フレームが経過したかが算出出来ます。

次に今回のフレーム数が前回のフレーム数と異なる場合、その差分が前回から進んだフレーム数となるのでこれを戻り値とし、
最後に今回のフレーム数を次に使うために保存しておきます。

このクラスの利点は必ずスタート時点の時間から計算しているため、途中で処理落ちなどが発生しても、
完璧なフレーム処理を行うことが出来るようになっています。


■サンプル

このクラスを使った場合のプログラムを見てみましょう。

#include "CTimer.h"

void Sample( void )
{
    CTimer tm;
    tm.Start( 60 );   // 60フレームで動くようにする

    // メイン
    while(1) {
        int loop = tm.Run();
        int i;
        for( i=0;i<loop;i++ ) {
            // ここで60フレームで処理する内容を記述
                :
        }

        Sleep(3);
    }

}

ここでは60フレームで動作するようにタイマーをスタートさせます。
※クラスヘッダでデフォルト値として60が設定されているため、引数無しで呼び出すことも可能です

メインループではforを使いRun()が返す値の分だけループを行います。
60fpsのゲームであればメインループ内は1フレーム単位で呼ばれるのでRun()からは1が返るはずですが、
たまたま処理が遅くなり2フレーム以上経過してしまった場合は、2回ループすることで遅延が無かったかのように処理出来ます。

ちなみに0が返ったときはforの中には入らないため、逆に処理の早いマシンではフレーム処理がスキップされることで、
どちらも確実に60フレームで計算を行わせることが出来ます。

注意
 ループ処理を「for( i=0;i<tm.Run():i++ )」というようにするのはダメです。
 なぜかというとfor文は2番目の比較がtrueであればループするという仕様ですが、この評価は毎ループ行われることになるため、
 1回目のforを処理したあとに次の評価でもう一度tm.Run()が呼び出されてしまい、次の瞬間のカウント値はリセットされているためです。


■ダウンロード

ここでは上記の内容を踏まえたクラスファイルをダウンロード出来ます。
CWindowsクラス同様、自由に改造して使ってもOKです。

CTimerライブラリ v2.00 CTimer_v2.00.zip