Unityを触っていると、Rigidbodyを使うことがあると思います。そんな時に、よくFixedUpdateを使うといいよと言われます。
FixedUpdateは必ず一定間隔の固定フレームレートで呼ばれ、Updateのフレームレートとは違うから描画にズレが生じるんだーという話をよく見ます。
「必ず一定間隔で呼ばれる」「Updateとは別物」と、いろんなところで目にしました。
そして僕は勝手にこう考えていたんですよね。「物理演算はきっとUnity側が自動で別スレッドでしてくれているに違いない。何せ絶対一定間隔で呼ばれるんだろ。マルチコアってすげー!!!!」
・・・・ん?
お前メインスレッドやんけ!!!!
<FixedUpdateの疑問>
さて、ここで疑問が生まれます。メインスレッドである以上、普通のUpdate処理の影響も受けるし、レンダリングの影響も受けます。
本当に絶対一定間隔で呼ばれるの?
例えば、クッッッッソ重い物理演算をした時、Fixed Timestepよりも時間がかかってしまったら一定間隔で呼ぶことが果たして可能なのか疑問です。
一番引っかかる点は「一定間隔」と「現実時間」の関係です。
何故「一定間隔」と言う日本語を使うのか謎です。現実時間とか無理じゃね?
通常Fixed Timestepの間隔で呼ばれますが、それ以上に処理に時間がかかった場合どうなるのでしょう?
なんだかあげればキリがなさそうですが、いろんなパターンが考えられます。
それと、重くなるタイミングでも疑問点が出てきます。物理演算で重いとどうなるのか、通常の処理が重いとどうなるのか、レンダリングが重いとどうなるのか。
よく、重いとUpdateが1回呼ばれる間にFixedUpdateが3回以上呼ばれたりすることがあるけど処理的にどうなっているのでしょう?何故同じメインスレッドを占有しているのに、FixedUpdateは何回も呼べるのでしょうか?
例えばStartがすこぶる重かった場合、Updateが呼ばれる前に物理演算を間に入れるなどをするのだろうか?それとも全部終わってから物理演算の処理を開始するのだろうか?不明な事が多々あります。
なんとなくわかっているフリをしていたUpdateとFixedUpdateの挙動について徹底検証していきたいと思います。
<検証1 : 普通の挙動>
とりあえず、普通に処理が軽い時を検証してみます。
緑色の<ーーーー>がFixedUpdate始めを表します。赤色がUpdateです。
横にくっついている数字はTime.deltaTimeの加算値です。
fixedTimerはTime.fixedDeltaTimeの加算値です。
最初のフレーム近辺は最初のレンダリング処理で重くなっていたので、軽かった場所を抽出しています。
FixedUpdateが必ず一定間隔で呼ばれている事がわかります。
FixedUpdateの間隔の中に差し込める隙があれば複数回Updateが呼ばれている事があります。このため、物理関係の処理はFixedUpdateに書いた方がいいと言われるのですね。
<検証2:物理演算がすこぶる重い場合>
それでは実験です。Cubeをたくさん置いて、爆発させてみました。
ご覧のようにバチクソ重いです。
以下コンソールの出力結果です。
明らかに0.02秒以上たっていたのにTime.deltaTimeが0.02ずつ増えています。
そして、物理演算が重いのにUpdateが途中から呼ばれなくなりました。(だいぶ下の方で次が呼ばれてました)
↑は遥か下で呼ばれてたupdateです。Time.deltaTimeが現実の時間と大きく乖離しています。その代わり、Time.fixedDeltaTimeと非常に近い値になっています。どうやら、Time.deltaTimeというのは、Time.fixedDeltaTimeを基準にしているようです。
と、言うわけで↑の結果から言えるのはこう言う事のようです。
おそらく、FixedUpdateは「基準」なんだと思います。ゲーム内の時間を進めるための基準です。そのため、現実の時間とは分けて考えた方が良さそうです。現実の時間通り一定間隔で呼ばれるのは処理が軽い時のみのようです。
この「基準」の処理のタイミングで「ゲーム内の時間をFixed Timestepぶん進める」という作りになっているのだと思います。
この事からFixed Updateは現実時間で一定間隔で呼ばれるというものではなく、物理演算をするタイミングを「Unityにおけるゲーム内の時間」で一定にすると定義しているものである事がわかります。
<検証3:物理演算以外がすこぶる重い場合>
今度は物理演算以外の場所をすこぶる重くしてみました。Update内でめっちゃゲームオブジェクトを生成してみました。物理演算を稼働させないため、コライダーは外してあります。
さて、コンソールにはどのように表示されたでしょうか。
結果としては、物理演算を重くした時と対して変わりませんでした。
うーん何故だろう。物理演算以外を重くしているはずなのに・・・・
何が起こっているかプロファイラーをみて確認してみましょう。
青色がUpdate処理です。うーん確かにUpdateが重くなっている事がわかります。
ちょっと左端に注目してください。
なんだか、うすーい線がたくさん入っています。
これを拡大すると、細かい束がたくさんあります。
さらに拡大すると
この細かい奴は全部FixedUpdateでした。何故かUpdateの前にFixedUpdateがたくさん呼ばれています。
ここからわかることは次の結果でしょうか
要は、物理演算以外が重かった場合、ズレてしまった時間分何回もFixed Updateを呼んで帳尻を合わせているということみたいです。
とはいっても、どーにも限界があるみたいで、大きく現実時間と乖離したとしても、実際にその差分全てをループして回収するわけではなく、可能な限りループしたら諦めるみたいです。
で、大きく現実時間と乖離した場合、時間をよーくみて見ると、0.333333ずつ増えているっぽいですね(Debug.Logだと表示上は0.3ずつ増えていますが、実際は0.333333増えています)
これはMaximum Allowed Timestepの値ですね。
なので、Maximum Allowed Timestepを超えるぐらい現実時間が経過すると、Maximum Allowed Timestep分進めるみたいですね。
というか、Maximum Allowed Timestepの説明分見れば一発でわかる話でしたね。何故これをFixedUpdateの説明に書いていないのか
<まとめ>
Unityにおける時間軸は物理演算周りの処理(FixedUpdate含む)を基準にし、ズレが生じた場合、帳尻を合わせようとする。
と、いうのがUnityの処理のようです。FixedUpdateは必ずしも一定間隔で呼ばれるわけではなく、FixedUpdateを呼ばれるタイミングを「一定間隔と定義する」みたいです。
1フレームの流れは次のようです。
物理演算をn回行う
(nは現実時間に合わせた回数。1フレーム中0回の場合もある)
(nは現実時間とズレた時、ズレた時間を埋めるように可能な限りループできる回数。限界はMaximum Allowed Timestep)
↓
ゲーム内処理を行う(Updateなど)
↓
レンダリングを行う
↓
物理演算をn回行う
↓
以下ループ……..
さて、なんとなくボンヤリと理解していたUpdateとFixedUpdateの中身が線で繋がったでしょうか?
この部分がよくわかっていると、UpdateとFixedUpdateのどっちに処理を書くべきなのかという事がわかってくると思います。
以上、徹底検証FixedUpdateは本当に一定間隔なのか?でした。
<これからUnityを学習するのなら>
もし、これからUnityについて学ぼうと思っていらっしゃるなら、このサイトでUnity初心者の方に向けた解説を行なっています。
もしよかったら参考にしてみてください。