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();
}
コメントを書く