アプリケーションモデル その2 ~Update~
12/27/2008 XNA GS 2.0の以降の振る舞いに合わせて「固定更新、高リフレッシュレート環境」部分を更新
Gameクラスには、Updateの呼ばれ方の振る舞いを変更するIsFixedTimeStepというプロパティがあります。この値がtrue(既定値) の場合、Game.TargetElapsedTime(規定値は16.6ms) で指定された間隔でUpdateメソッドが呼ばれます。これを仮に固定更新と呼びます。
この値がfalseの場合は、GameクラスはUpdate、Drawを交互に呼び続けるだけです。ただし、GraphicsDeviceManager.SynchronizeWithVerticalRetraceの既定値がtrueなので、GameクラスがDrawを呼んだ後に、IGraphicsDeviceManager.EndDrawメソッド内でデバイスのPresentを呼んだ時にV-Sync(垂直同期)待ちがが発生するので、通常は使っているモニタのリフレッシュレートになります。これを可変更新と呼びます。
理想的Update
上の図はモニタのリフレッシュレートが60Hz(一秒間に60回)環境で、Update、Drawの処理時間が16.6ms以下の場合の様子です。
この場合は、固定、可変のどちらの場合も同じ動作をし、Update、Drawの引数であるGameTime.ElapsedGameTimeの値は常に16.6msを示します。常にこの状態でゲームが動作するのが好ましいのですが、画面に沢山のキャラクターを表示したりして処理が16.6msに間に合わない、つまり処理落ちの状態では固定、可変更新での振る舞いは違います。
可変更新、処理落ち状態
上の図は、IsFixedTimeStepがfalseの場合で、処理落ちした状態での様子です。この場合、フレーム1以降でUpdateとDrawに渡されるGameTime.ElapsedGameTimeの値は大体23~25msと処理状態によって変化します。また、処理落ちが発生した場合はGraphicsDevice.PresentはV-Syncと同期しません。
固定更新、処理落ち状態
上の図は、IsFixedTimeStepがtrueの場合で、処理落ちした状態での様子です。この状態でもUpdateとDrawに渡されるGameTime.ElapsedGameTimeの値はGame.TargetElapsedTimeと同じものです。
ここで違うのは、フレーム3の時にUpdateが2回呼ばれるということです。固定更新の場合、Gmaeクラスは各フレームで経過時間によってUpdateを呼ぶ回数を変化させます。上の場合は、フレーム3の時にフレーム1で更新するべき時間の16.6msから、大体34ms以上の時間が経過しているので、Gameクラスは実時間とゲーム内でのシミュレーション時間を合わせるために、34ms/16.6ms=2フレーム分のUpdateを呼ぶわけです。
結果的にDrawを呼ぶ回数が減るので、次のフレームでは処理落ちが回避できる可能性が高くなりますが、Updateの処理時間がDrawの処理時間より掛かる場合はいつまで経っても実時間に追いつかないケースが発生します。この状態に陥るのを防ぐ為にGameクラスは500msを経過時間の上限としていいます。また、Updateが複数回呼ばれる状況になったときにはUpdate、Drawに渡されるGameTime.IsRunningSlowlyの値がtrueになるので、この値によってゲーム側で余計な処理を省いたりすることで、処理落ちを防ぐことができます。
固定更新、高リフレッシュレート環境
PCのモニタ、特にCRTモニタの場合だとLCDモニタに比べて高いリフレッシュレートのモニタがあります。この場合、可変更新の場合は単にGame.TargetElapsedTimeがリフレッシュレートの間隔になるのに比べて、固定更新の場合は上の図のように、フレーム1、3、そして6のようにUpdateとDrawがフレームの途中で呼び出されるフレームが存在します。XNA GS 2.0以降ではUpdateが呼ばれない場合は、Drawも呼ばれず、単に次の更新時間になるまでループするようになっています。XNA GSE 1.0ではUpdateが呼ばれない場合でもDrawが呼ばれるので、通常はそこで垂直帰線期間待ちが発生するようになっていました。
これは、GameクラスはUpdateを呼ぶに足る経過時間が経った場合にのみUpdateを呼ぶという処理の仕方からくる現象です。この振る舞いを言葉で伝えると、大抵の人は変な振る舞いだと思ってしまいますが、上の図の50msの時点を見てみると60Hz、100Hzどちらの場合もUpdateが3回呼ばれているということが分かると思います。つまり表示される時間に多少の違いがあるものの、ゲームシミュレーションとしては常に一定(Deterministic)であるというわけです。
実はこれに近い仕組みは、皆さんの身の回りで良く目にすることができます。それはTVで映画を観る時です。秒間24フレームの映画を秒間30フレームのTVで見るために2-3プルダウンという手法が使われています。
なぜ固定更新なのか?
以上のことをまとめると以下のようになります。
- 固定更新(IsFixedTimeStepがtrueの場合)
- Updateに渡されるGameTime.ElapsedGameTimeの値は固定(Game.TargetElapsedTimeと同一)
- Updateが呼ばれる回数が変化
- Updateが複数回呼ばれた場合はGameTime.IsRunningSlowlyがtrueになる
- 可変更新(IsFixedTimeStepがfalse場合)
- Updateに渡されるGameTime.ElapsedGameTimeの値は変化する
- Update、Drawは常に交互に呼ばれる
- GameTime.IsRunningSlowlyは常にfalse
Xbox360などのコンシューマーゲーム機では、固定更新を使っているゲームが殆どです。PCのゲームでは以前は可変更新をするゲームが主流でしたが、最近は固定更新をするゲームが増えてきました。
固定更新をするゲームず増えてきた要因は、ゲーム内で物理シミュレーションが本格的に使われるようになったのと、ネットワーク対応のゲームが増えてきたことです。
物理シミュレーションでは物同士が衝突した瞬間を計算するわけですが、この衝突した瞬間というのが更新時間によって左右される場合が多く、更新時間の違いによってシミュレーション結果が変わってしまうという問題があります。
ネットワークゲームの場合は相手側に情報を伝える時間は人間にとっては一瞬でも、プログラムにとっては充分に長い時間なので、常に次の動きを予想しながら動作しています。このときに可変更新にしてしまうと、こちら側と向こう側のシミュレーション結果を一致させるのに多大な努力と、ネットワーク帯域の消費してしまうという問題があります。
これらの理由から、XNAの既定値は1/60秒固定更新になっています。多くのケースに対応するように設計されているわけですが、最大の利点としては、そんなことを一切気にせずともWindows/Xbox360上で同じように動作するゲームが作れてしまうということです。
Comments
- Anonymous
December 27, 2008
注:今回紹介するコンポーネントは デバッグサンプル に入っています。 正確な測定には注意が必要 FPSカウンターは、一定時間内に(数秒程度)何フレーム更新できたかを計測した結果から、1秒間のフレーム数、FPS(Frame