考えてみれば当たり前な方法ですが、ぐぐっても見つからなかったので…。
もくじ
■
世の中に存在する大体のライブラリでは、時間をミリ秒単位で計測することができます。一秒間は1000ミリ秒です。
また、世の中、特にこの日本に存在するゲームは、大体60FPSで動いています。一秒間に60回画面上の絵が更新されるということです。
1/60秒は、16.666666666666667秒。1ミリ秒が精度のタイマーでは、正確に測定することはできません。でもゲームスピードを60FPSにしたいなら、なんとか測定しなければなりません。
そこで、多くのゲームではいろいろな方法でこの問題を解決してきました。
■方法1:1/60秒≒16ミリ秒と近似する。
いろいろと諦めてるのがこの方法です。この方法だと、1000ミリ秒/16ミリ秒=62.5となるので、60FPSより少し早くなってしまいますが、目をつぶります。
ソースコードはこんな感じ
void gameloop() { uint32_t nextFrame = getNow()+16;//ミリ秒のタイマー関数 while(ゲームが終わるまで){ move();//ゲーム内でのキャラクタの移動処理など uint32_t now = getNow(); if(now < nextFrame){ draw();//描画処理 now = getNow(); if(now < nextFrame){ //描画しても時間が余ってたら・・・ sleep(nextFrame-now);//ミリ秒精度で処理を中断する関数 } } nextFrame+=16; } }
■方法2:浮動小数点を使う
sleepも、現在の時間を取得する関数もミリ秒の精度しかありませんが、それらを管理する変数だけは浮動小数点を使う方法です。この方法を使うと、実際には16ミリ秒のフレームと17ミリ秒のフレームが、1:2の割合で現れます。
ソースコードを見ていただけたほうが早いでしょう。
const float frameInterval = 1000.0f/60; //60FPSの基準時間 void gameloop() { //floatでの管理に変えた float nextFrame = getNow()+frameInterval; while(ゲームが終わるまで){ move();//ゲーム内でのキャラクタの移動処理など uint32_t now = getNow(); if(now < nextFrame){ draw();//描画処理 now = getNow(); if(now < nextFrame){ //描画しても時間が余ってたら・・・ //型変換が必要。。。 sleep(static_cast<uint32_t>(nextFrame-now)); } } nextFrame+=frameInterval; } }
この方法ではかなり正確に60FPSを達成できますが、「計算の遅いfloat+型変換まで必要、よって遅そう」、「浮動小数点の精度は直感的に分かりづらい」(からなんか気持ち悪い)という精神衛生上の問題が発生します。
実際には一秒間に60回しか行わないので、パフォーマンス上の問題は無いでしょうし、そもそもミリ秒精度しか測れない以上キッチリ60FPSを測ることも不可能なので、floatの誤差を気にする必要は無いと思いますけど。でもなんか気持ち悪いんです…(ぇ
■方法3:vsyncに任せる
DirectXやらOpenGLやらのゲームライブラリは、システムがモニタに画面を描画するタイミングまで待ってくれたりします。これを垂直同期(vsync)を取るといい、この方法を使うとシステムが画面をディスプレイに描画している最中にゲーム画面を更新する事がなくなるので、画面がガクついたりしなくなります。
この方法は自分で時間を測る必要もないし†1、画面も綺麗になるしでいいこと尽くめなのですが、ひとつだけ問題があります。「すべてのモニタが60FPSとは限らない」、という点です。75FPSの画面もあれば、50FPSの画面もあります。垂直同期を取るとこういった場合にゲームが早くなったり遅くなったりしてしまいます。。
また、必ずしもvsyncがサポートされているとは限りません。グラフィックカードの設定でvsyncを無効にすることもできます(OpenGLの場合。DirectXは存じません)。
■今回の方法:浮動小数点でなく分数で計算する
長々と書いてきましたが、やっと今回の本題に入れます。vsyncが一番良いのはそのとおりなのですが、諸々の事情で使えないこともあるとわかりました。ということはやっぱりタイマを用いて制御しなければならないこともあるわけです。。。
方法2のfloatを用いる方法を継承しつつ、精神衛生上よろしくないfloatを取り除きます。一秒間に1000回回るカウンタを使って一秒間に60回を測る方法…。そう、最小公倍数の6000を基準にして、分数で測ってしまいましょう†2。
void gameloop() { //(1/60秒 = 100/6000秒) uint32_t nextFrame = getNow()*6 + 100; while(ゲームが終わるまで){ move();//ゲーム内でのキャラクタの移動処理など uint32_t now = getNow() * 6; if(now < nextFrame){ draw();//描画処理 now = getNow() * 6; if(now < nextFrame){ //描画しても時間が余ってたら・・・ sleep((nextFrame-now)/6); } } nextFrame+=100; } }
問題はタイマーの限界が6倍早く来てしまう点でしょうか…。SDLのSDL_GetTicksなどの32ビット整数を返すタイマでは49日間で0に戻ってしまいますが、6倍するのでだいたい8日でオーバーフローしてしまいます。
■応用
抽象化すると「”一定期間内にn回まわるカウンタ”で”一定期間内にm回まわるカウンタ”をつくる方法」ですので、フレームレート以外でも活用していただけます。
そもそも今回の内容は、現在開発中のファミコンエミュレータで、サウンドとCPUを同期する方法を考えていて思いつきました。とはいえその内容をそのまま書いても汎用性が低いので、フレームレートに置き換えて記述しています。