ゲーム実行のための軽い時間計測方法

Cocoaに関するヌマタメモ
2008年1月18日 22:05

PCゲームの実行中は、モデルの変更と描画を行ったあと、それも含めて1/60秒で1フレームが終了するのを待たなければならない。ここで、残り何ミリ秒なのかを計測し、その時間きっちりスリープさせるのは、結構思案どころである。

まず、現在時刻をどうやって計測するか、である。
Mac OS X で使える現在時刻の取得方法は、主に5つある。

  • +[NSDate date] (Cocoa)
  • Microseconds() 関数 (Carbon)
  • gettimeofday() 関数 (4.2BSD)
  • UpTime() 関数 (CoreServices - CarbonCore)
  • mach_absolute_time() 関数 (Mach)

まあ Cocoa のメソッド呼び出しが遅いのは間違いないところだろうから、これがいちばん遅いとして、「COCOA-STUDY.COM BLOG」の情報を信じると、

 +[NSDate date] >> Microseconds() > gettimeofday() >> UpTime() ≒ mach_absolute_time()

という感じになっているらしい。

そこで UpTime() と mach_absolute_time() について見てみると、どちらも CPU クロックか何かを単位とした64ビット値で時間を扱うようになっていて、秒単位で扱う場合には、専用の関数によって変換しなければならない。この変換自体に時間がかかるため、秒単位で扱う場合には、変換の必要がない gettimeofday() 関数の方が速いという結果になると上記のサイトでは結論付けている。

しかしゲームの場合には、時間計測をしたい訳ではなく、計測した時間を元に一定時間のスリープをさせたいだけなので、1/60秒の方を、これらの関数の単位に変換しておけば良い。

Technical Q&A QA1398:Mach Absolute Time Units」の情報を元に逆変換を考えると、次のようになる。

mach_timebase_info_data_t timebaseInfo;
mach_timebase_info(&timebaseInfo);
uint64_t duration = ((1000000000 * timebaseInfo.denom) / 60) / timebaseInfo.numer;

ここで、CoreServices フレームワークを追加するのがめんどくさいというのと、ちょっとでも Carbon への依存をなくしたいというのと、uint32_t が2個パックされた構造体を使っている UpTime() 関数に対して uint64_t 型をそのまま使っているという理由で、mach_absolute_time() 関数の方を使うことを推したい。

さらに、Mach の専用関数として、mach_time.h で mach_absolute_time() 関数のすぐ上に mach_wait_until() という関数が定義されている。これを使うと、時間を計測したときと同じ単位を使って、そこそこ正確にスリープしてくれるハズ。

ということで、次のようにするのが現在の Mac OS X ではいちばん軽いフレーム実現の方法なのでは、と、とりあえず結論した。

uint64_t    finishTime;     // 次の終了時刻
uint64_t    duration;       // 1フレームの時間
bool        doSkip = false// 次のフレームをスキップするか
int         skipCount = 0;  // 何フレーム連続でスキップしたか

mach_timebase_info_data_t timebaseInfo;
mach_timebase_info(&timebaseInfo);
duration = ((1000000000 * timebaseInfo.denom) / 60) / timebaseInfo.numer;

finishTime = mach_absolute_time();

while (isRunning) {
    finishTime += duration;
    
    UpdateModel();  // モデルの更新

    if (doSkip) {
        doSkip = false;
        skipCount++;
        if (skipCount == 3) {
            // 3回スキップしたらズレを諦めてリセット
            finishTime = mach_absolute_time();
            skipCount = 0;
        }
    } else {
        DrawView(); // 画面の描画
    }
    if (mach_absolute_time() < finishTime) {
        mach_wait_until(finishTime);
        skipCount = 0;
    } else {
        doSkip = true;
    }
}

フレームスキップのやり方がおざなりなので、もっとちゃんと考えなきゃいけない点は
まだ多いけど、何かしらこんな感じで。

ちょっと改良

動かしてはないけど、こんな感じかな?

/*!
    
@class      Game
    
@abstract   ゲームの処理を記述するためのクラス。
 */

class Game {
public:
    void    Initialize();   //!< 初期化処理(モデル&ビュー)
    void    UpdateModel();  //!< モデルの更新
    void    DrawView();     //!< ビューの描画
    void    CleanUp();      //!< クリーンアップ
};

/*!
    
@class      GameController
    
@abstract   ゲーム実行の管理を行うクラス。
 */

class GameController {
    
public:
    static const int    FramePerSecond;     //!< fps値
    static const int    MaxFrameSkipCount;  //!< スキップするフレームの最大数

private:
    Game    mGame;      //!< ゲームの処理記述
    bool    mIsRunning; //!< ゲーム実行中かどうか

public:
    void    MainLoop(); //!< ゲームのメイン実行部分
};

/*!
    
@function   ConvertNanoSecToMachTime
    
@abstract   ナノ秒単位を Mach 時間に変換します。
    
@param      nanoSec 変換する時間(ナノ秒単位)
    
@result     nanoSec を Mach 時間に変換した64ビット値
 */

static inline uint64_t ConvertNanoSecToMachTime(uint64_t nanoSec) {
    mach_timebase_info_data_t timebaseInfo;
    mach_timebase_info(&timebaseInfo);
    return nanoSec * timebaseInfo.denom / timebaseInfo.numer;
}

/*!
    
@const      FramePerSecond
    
@abstract   fps値
 */

const int GameController::FramePerSecond = 60;

/*!
    
@const      MaxFrameSkipCount
    
@abstract   スキップするフレームの最大数
 */

const int GameController::MaxFrameSkipCount = 5;

/*!
    
@function   MainLoop
    
@abstract   ゲームのメイン実行部分
 */

void GameController::MainLoop() {
    // 各種変数宣言
    uint64_t    frameInterval;      // 1フレームの時間(Mach 時間)
    uint64_t    currentTime;        // 現在時刻(Mach 時間)
    uint64_t    prevTime;           // 1ループ前の終了時刻(Mach 時間)
    uint64_t    modelUpdateCount;   // モデルの更新数
    int         i;

    // 1フレームの時間を計算
    frameInterval = ConvertNanoSecToMachTime(1000000000 / FramePerSecond);
    
    // 初期化処理
    mGame.Initialize();

    // メインループ
    mIsRunning = true;
    prevTime = mach_absolute_time();
    while (isRunning) {
        // 現在時刻の取得
        currentTime = mach_absolute_time();
        
        // モデル更新数の割り出しとスリープ
        modelUpdateCount = (currentTime - prevTime) / frameInterval;
        if (modelUpdateCount <= 0) {
            modelUpdateCount = 1;   // 最低1回は更新
            prevTime += frameInterval;
            mach_wait_until(prevTime);  // スリープ
        } else if (modelUpdateCount > MaxFrameSkipCount) {
            modelUpdateCount = MaxFrameSkipCount;   // 処理落ち
            prevTime = currentTime;
        } else {
            prevTime += modelUpdateCount * frameInterval;
        }
        
        // モデルの更新処理
        for (i = 0; i <  modelUpdateCount; i++) {
            mGame.UpdateModel();
        }
        
        // 描画処理
        mGame.DrawView();
    }
    
    // クリーンアップ
    mGame.CleanUp();
}

コメントを書く


トラックバックはありません。

トラックバックURL: http://numata.designed.jp/mt-tb.cgi/258