野生のはてなブログ

twitterに書くには長すぎQiitaやサークルサイトに書くには雑多過ぎる話題を書いていきます

Unityでゲームを作った際に「カクカクしている」と言われないためのTimeSettings.FixedTimestep講座

はじめに

 土日は久しぶりに一般参加者としてOcufesに参加してきました。そのため出展側で参加するいつものOcuFesよりも多く他の方のアプリを体験することができました。盛況だったので残念ながら全てのコンテンツを体験することはできませんでしたが、酔い対策もフレームレートも十分なコンテンツ、スマートフォンなので時々フレームレートが落ちるが許容範囲なコンテンツ、このご時世にOculus ReadyどころかGPUを搭載していないノートで展示しているコンテンツなど、色々体験しましたが、一つ気になったのがフレームレートは足りているのにカクカクするコンテンツです。「フレームレートが足りているのにカクカクするってどういうこっちゃ」と思われる方が多いと思いますので、以下で解説していきます。

描画のフレームレートと物理のフレームレート

 UnityのMonoBehaviourにはUpdate(LateUpdate)FixedUpdateという2つのUpdateメソッドがあります。Update(LateUpdate)は状況に応じて可変周期、FixedUpdateは固定周期で実行されるということまでは数多くの方が知っていると思われますが、そもそもこの2つのメソッドが呼ばれる「周期」がデフォルト設定ではズレていることをご存じの方は少ないかと思います。

 Updateは描画フレーム処理のタイミングで実行されるメソッドです。Project Settings->Quality->V Sync CountDon't Syncの場合は性能が許す限り短い周期で、Every V Blankの場合はディスプレイの垂直同期周波数と同じ周期で、Every Second V Blankの場合はディスプレイの垂直同期周波数の半分の周期で実行されます。

f:id:yaseino:20160224001652p:plain

一般的なPCモニタやスマートフォンが持つディスプレイの垂直同期周波数は60Hzなので、滑らかさが大事なアクションゲームやVRゲームで用いられるEvery V Blankでは基本60 frame per secondで実行されますが、ゲーミングモニタの場合は120/144Hz、Oculus Rift DK2の場合は75Hz、HTC ViveOculus Rift製品版は90Hzと、製品によって様々です。そのため、UnityUpdate内にTransformなどを変更する処理を書く際はTime.deltaTimeを用いてどのような周期のモニタでも前フレームからの経過時間ベースで数値を変化させてやることが重要になります。

 

 それに対し、FixedUpdateProject Settings->Time->Fixed Timestepで設定されたUpdateとは独立した固定周期で実行されます。

f:id:yaseino:20160224001818p:plain

Fixed Timestepのデフォルト値は0.02(秒)となっており、60Hz以上のディスプレイでUpdateが実行される周期とは異なる上に長いです。「もしFixedUpdateでオブジェクトの移動を行っていた場合」にそのままプログラムを実行してしまうと何が起きるかを表したのが下の図です。

f:id:yaseino:20160223230310p:plain

 赤丸Updateの1フレーム目が実行される時、FixedUpdateはまだ実行されていません。そのため、UpdateはFixedUpdate実行前で初期位置にあるオブジェクトを描画します。そのため、1フレーム目のグラフィックは「動いてないオブジェクト」が表示されることになってしまいます。

 次に、緑丸で囲んだUpdateの3・4フレーム、6・7フレーム、8・9フレームを見てみましょう。Updateに比べてFixedUpdateが遅いため、オブジェクトは前のフレームと同じ位置にある状態になってしまいます。図で示したように、UpdateFixedUpdateの更新周期に差があると一定間隔でも不定期にこの現象が発生し得ることになります。この現象は、実際のゲームではjudder(振動)となって現れます。

解決案1. Rigidbody.Interpolate

 FixedUpdateで実行されている処理がUnity標準の物理処理(Rigidbodyコンポーネント)のみの場合は、Rigidbody.interpolation(エディタではInterpolateと表示)NoneからInterpolateに変更すると、Update時にオブジェクトの挙動が補間され滑らかに動いているように見えます。

f:id:yaseino:20160223234849p:plain

 「滑らかに動いているように見える」というのは、これがあくまで「補間された位置」であるということです。Rigidbody.interpolationではUnity物理エンジンを介さず単純な計算でそれらしい位置に補間しています。これはごく単純なアプリケーションでは良いですが、ゲームデザインによってはすり抜け・めり込みなどの視覚的なバグが起こり得る元となります。また、GameObjectごとにRigidbodyコンポーネントを設定する必要があり管理が大変です。Update毎に補間処理が呼ばれるのでFixedUpdateとは別にオブジェクトの処理コストも累積していきます。すり抜け・めり込み問題に対してはRigidbody.velocityを元により確実な補間を行うExtrapolateがありますが、より処理コストが増えます。

 これらのデメリットがあることから、UnityのマニュアルでもRigidbody.interpolationは操作キャラクターのみに適用して他のオブジェクトには使用しないことが推奨されており、あまりおすすめ出来ません。

解決案2. Fixed Timestepを短くする

 今回の場合、FixedUpdateの実行周期が一般的なディスプレイの描画に比べても遅すぎることが最大の原因なので、これをディスプレイの更新周期と同じにすると以下のようになります。

f:id:yaseino:20160224001853p:plain

 これなら最高ですね!「過去の位置を参照してしまう」問題も「前の描画から位置が更新されない」問題も解決したように見えます。しかしここで最初の方に書いたことを思いだしてください。FixedUpdateUpdateはそれぞれ独立した時間軸で実行されています。そのため、FixedUpdateUpdateが完全に揃う保証はありません。位相ズレが起きてしまうと「過去の位置を参照してしまう」問題は引き続き発生してしまいます。最悪のケースでは描画処理のほぼ1フレーム(13~16ミリ秒)前の位置を参照してしまうこともあるでしょう。

f:id:yaseino:20160224003525p:plain

 これを防ぐ場合には2種類の方法があり、まず1つがやはりRigidbody.interpolationを使う方法です。FixedUpdateの回数が増えたため、先ほどに比べると補間される値の誤差も減っており適用するデメリットが減っています。

 もう1つは、FixedUpdateの実行回数を更に増やす方法です。これはレースゲームなど、物理エンジンの誤差の少なさが極めて重要となるジャンルのAAAタイトルで採用されている手法で、例えば60fpsの描画周期に対して物理エンジンが120fpsで実行するなどです。この場合、位相ズレの問題が起きても最悪のケースで描画処理の0.5フレーム程度(6~8ミリ秒)しか誤差が出ません。

f:id:yaseino:20160224004203p:plain

 これは、通常のアクションゲームなどでは明らかに過剰な処理ですが、より違和感の少なさが要求されるVRゲームなどでは非常に有効な手段でしょう。昨今のPCゲームは膨大な数のオブジェクトを処理する場合や外部カメラを取り込んだリアルタイム画像処理をする場合などを除いてCPUバウンドの処理は減っており、大抵はCPU使用率に余裕があるので、試す価値があります。

まとめ

 FixedUpdateにオブジェクトの移動処理を実装するのはこのように描画時において様々な不具合が発生する元なので、「本当に物理演算が必要な場合」を除いてUpdate(LateUpdate)に移動処理を実装しましょう。