エフアンダーバー

個人開発の記録

【Unity】 Timeクラス詳説

Timeクラス関連で調べものをしていたのですが、 詳しいことがまとまっているサイトが見当たらなかったので自分でまとめてみました。

Timeクラスのプロパティ一覧

リファレンスに記載されているTimeクラスのプロパティ一覧。

日本語リファレンスは何を言っているのかさっぱりなことがあるので、説明は自分で翻訳しています (あまり英語力に自信はないので信用しすぎないように)。

プロパティ 説明
captureFramerate フレーム間にスクリーンショットを保存できるようにゲームの再生速度を遅くする。
deltaTime 最後のフレームの完了にかかった秒単位の時間。<読み込み専用>
fixedDeltaTime 物理演算やその他の固定フレームレートの更新処理(MonoBehaviourのFixedUpdate等)が実行される秒単位の間隔。
fixedTime 最後にFixedUpdateを開始した時刻。ゲーム開始時からの秒単位の時刻。<読み込み専用>
frameCount 経過したフレーム総数。<読み込み専用>
maximumDeltaTime フレームにかかり得る最大時間。物理演算やその他の固定フレームレートの更新処理(MonoBehaviourのFixedUpdate等)はフレーム毎にこの時間の間だけ実行され得る。*1
realtimeSinceStartup ゲーム開始時からの秒単位の現実の時刻。<読み込み専用>
smoothDeltaTime 滑らかにしたTime.deltaTime。<読み込み専用>
time このフレームの開始時の時刻。ゲーム開始時からの秒単位の時刻。<読み込み専用>
timeScale 時間経過の倍率。スローモーションのエフェクトに使用可能。
timeSinceLevelLoad このフレームの開始時の時刻。最後のレベルロード時からの秒単位の時刻。<読み込み専用>
unscaledDeltaTime 最後のフレームの完了にかかったtimeScale非依存の時間。<読み込み専用>
unscaledTime このフレームの開始時のtimeScale非依存の時刻。ゲーム開始時からの秒単位の時刻。<読み込み専用>

いくつかの説明について"time"を「時刻」と訳していますが、 これは何時何分という類のものではなく、ある特定の時点からの経過時間という意味です。 そのまま「経過時間」と置き換えたほうが意味がわかりやすいかもしれません (「時間」と訳すと文脈によっては意味が通りづらく、「経過時間」と訳すとdeltaTime系と紛らわしいためやめました)。

Timeクラスのプロパティ分類

  • 時刻取得系
    • time
    • fixedTime
    • unscaledTime
    • realtimeSinceStartup
    • timeSinceLevelLoad
  • フレーム間時間取得系
    • deltaTime
    • fixedDeltaTime
    • unscaledDeltaTime
    • smoothDeltaTime
  • フレーム数取得系
    • frameCount
    • (renderedFrameCount)
  • 設定系
    • fixedDeltaTime
    • timeScale
    • maximumDeltaTime
    • captureFramerate

時刻取得系

時刻取得系は『特定の時点からの経過時間』を取得するものです。 便宜上、この『特定の時点からの経過時間』を「時刻」と呼ぶことにします。 日常で使う時刻の表現としての『何時何分』も0時0分からの経過時間だと捉えれば、これはさほどおかしな表現ではないと思います。

時刻取得系というと、ゲーム内で時刻を扱うような場面、 例えばセーブした時刻を記録するような場面に用いそうな感じがしますが、そのような用途では使えません。 というのも、これらのプロパティの基準となる『特定の時点』というのはゲーム開始時などの、現実の時刻とは関係のないものであるためです。 このような用途の場合には.NET FrameworkのDateTimeクラスなどを用います。

では、時刻取得系はどのような場面で利用するかというと、基本的には経過時間の計測に用います。 ある時点での時刻を取得しておき、その後、さらに別のある時点での時刻を取得、そしてそれらの差をとると経過時間がわかります。 現実でもよくありますよね、10時30分に電車に乗ったら11時15分についたから所要時間は45分だな、というあれです。 この場合、10時30分や11時15分といった時刻にはあまり意味はなく、電車で45分という所要時間にこそ意味がありますよね。 時刻取得系のプロパティも同様で、取得する時刻自体にはあまり意味はありません。

多くの場合、時刻取得系のプロパティを利用するシーンでは以下のようなコードが含まれます。

...
this.startTime = Time.time;   // ある時点での時刻を取得
...

...
float elapsedTime = Time.time - this.startTime;   // 現時刻との差から経過時間を取得
...

time

時刻取得系のプロパティで最もよく使用されるのはtimeプロパティです。 特殊な用途以外では、すべてtimeプロパティで問題ありません。 ゲーム開始時からの経過時間を表します。

値の更新タイミングはフレーム開始時です。 つまり、様々なスクリプトのUpdateから何度値を取得したとしても、 呼び出したフレームが同じならば、すべて同じ値となります。

特殊な挙動として、FixedUpdate内で値を取得した場合には、自動的にfixedTimeの値を返します。

fixedTime

fixedTimeプロパティはtimeプロパティ同様、ゲーム開始時からの経過時間を表します。 ただし、FixedUpdateなどの固定フレームレートで実行される更新処理において時刻を取得する場合に使用します。 しかし、timeプロパティはFixedUpdate内で値が取得された場合に自動的にfixedTimeの値を返すため、明示的に呼び出す必要はありません。

値の更新タイミングはFixedUpdateの開始時です。 つまり、様々なスクリプトのFixedUpdateから何度値を取得したとしても、 一度のFixedUpdate中ならば、すべて同じ値となります。

unscaledTime

unscaledTimeプロパティはtimeプロパティ同様、ゲーム開始時からの経過時間を表します。 ただし、timeScaleによる影響を受けません。

timeプロパティはtimeScaleプロパティを設定することで時間が流れる速度を遅くしたり、速くしたり、止めてしまうことすらできます。 これはゲームの演出に役立つのですが、UIなどのゲームの世界の時間とは無関係な部分のアニメーションまで影響を受けてしまいます。 そのような場合にはunscaledTimeで時刻を取得すると、通常の時間の流れで動かすことができます。

値の更新タイミングはtimeプロパティ同様、フレーム開始時です。 つまり、同フレームのUpdate内ではすべて同じ値となります。

realtimeSinceStartup

realtimeSinceStartupプロパティはtimeプロパティ同様、ゲーム開始時からの経過時間を表します。 ただし、時間の流れる速度は常に現実の時間と同じです。

他の時刻取得系プロパティ(特にunscaledTime)と何が違うかというと、 特定のタイミングで更新された時刻を返すのではなく、 つねに値を取得したときの時刻を返すことです。 つまり、たとえ同じUpdate内であったとしてもrealtimeSinceStartupは値を取得するたびに違う時刻を返し得ます。 (プログラムコードの実行時間だけ時刻が進むため)。

ゲームの更新処理においては基本的に、同じフレーム内の処理は同じタイミングで起こっているとした方が都合がいいです。 そのため、他の時刻取得系プロパティは特定のタイミングで値を更新し、次のタイミングまで同じ値を返します。 しかし、まれに同じフレーム内の二つの時点の経過時間が知りたい場合があり、 realtimeSinceStartupはそのような場合に使用します。 代表的なケースとしてコードのパフォーマンス計測が挙げられます。

realtimeSinceStartupの取得にはシステムのタイマーを利用しています。 プラットフォームやハードウェアによっては、期待した精度が得られない場合があるようです。

timeSinceLevelLoad

timeSinceLevelLoadは最後のレベルロード時からの経過時間を表します。

Unityでは使用頻度が高いと思われる値についてショートカット的に値を取得できるようにAPIを設計する傾向があります。 このプロパティに関しては必要性がよくわからないため、この類のものではないかと思います。

フレーム間時間取得系

フレーム間時間取得系は直前フレームにかかった時間を取得するものです。 時刻取得系でフレーム間の時刻の差をとれば、同様の値を取得できますが、 フレーム間の時間は非常によく使用するため別個のプロパティとして用意されています。 時間に応じて滑らかに値を変化させる場合にはこれらのプロパティが必要となります。

例えば、キャラクターの位置を一定の速度で右に動かすという処理を考えます。 Update内に次のようなコードを記述すると、アタッチしたキャラクターが少しずつ右に移動します。

transform.localPosition += new Vector3(0.1f, 0.0f, 0.0f);

これは『各フレームごとに座標を右に0.1ずらす』という処理です。 1秒間にこの処理が60回呼び出されれば、キャラクターは1秒ごとに6ずつ右に移動することになります。 一見すると、これで秒速6で移動しているように思えるかもしれません。 しかし、UnityのUpdateにおいて、フレームの時間は常に一定というわけではありません(可変フレームレート)。 そのため、実はキャラクターの速度(0.1 / フレームの時間)がフレーム毎に変化しています (ついでに、呼び出し回数が秒間60回とも限りません)。

このようなことを回避するため、通常、位置の更新処理は次のように記述します。

transform.localPosition += new Vector3(6.0f, 0.0f, 0.0f) * Time.deltaTime;

これで『各フレームごとに座標を秒速6で右にずらす』という処理になり、キャラクターは一定の速度で移動します。 Unity標準のコンポーネントであるAnimatorやRigidbodyなども同様の処理で記述されています。

基本的にフレーム間時間取得系のプロパティは各時刻取得系のプロパティと対応しています。

deltaTime

フレーム間時間取得系のプロパティで最もよく使用されるのはdeltaTimeプロパティです。 特殊な用途以外では、すべてdeltaTimeプロパティで問題ありません。 直前フレームにかかった時間を表します。

特殊な挙動として、FixedUpdate内で値を取得した場合には、自動的にfixedDeltaTimeの値を返します。

fixedDeltaTime

fixedDeltaTimeプロパティはdeltaTimeプロパティ同様、直前フレームにかかった時間を表します。 ただし、FixedUpdateなどの固定フレームレートで実行される更新処理において時刻を取得する場合に使用します。 しかし、deltaTimeプロパティはFixedUpdate内で値が取得された場合に自動的にfixedDeltaTimeの値を返すため、明示的に呼び出す必要はありません。

fixedDeltaTimeプロパティは値の取得よりも、主に値の設定に使用します(後述)。

unscaledDeltaTime

unscaledDeltaTimeプロパティはdeltaTimeプロパティ同様、直前フレームにかかった時間を表します。 ただし、timeScaleによる影響を受けません。

smoothDeltaTime

deltaTimeプロパティの値の変化が急激にならないように滑らかに補正した値かと思われます。

フレーム数取得系

フレーム数取得系はその名の通り、フレーム数を取得するものです。 時刻取得系と同様に基本的には二つの時点間のフレームを比較することで経過フレームを算出して用います。

ゲームの処理としてフレーム数を用いることはまれなので、主にデバッグ用になります。

frameCount

frameCountプロパティは経過したフレーム総数を表します。

すべてのAwake呼び出しが終わるまでframeCountの値は未定義です。 そのため、Awake内でframeCountの値を取得してはいけません。

(renderedFrameCount)

renderedFrameCountプロパティは経過した描画フレーム総数を表します。 エディタでのポーズ中など、Updateは呼び出されず、描画関係のイベント関数のみが呼び出される場合があります。 その場合にはframeCountの値は変化せず、renderedFrameCountの値のみが増加します。

リファレンスに記載されていないプロパティなので使用には注意が必要です。

設定系

設定系のプロパティは値を設定することで、Unityのフレーム管理や時間の流れを調整できます。

詳細はそれぞれのプロパティを参照してください。

fixedDeltaTime

fixedDeltaTimeプロパティは物理演算に関連する更新処理を実行する間隔を設定します。

UpdateなどUnityにおける更新処理の多くは各フレームの実行される間隔が一定ではありません。 しかし、物理演算に関しては精度と一貫性のため一定の間隔で実行する必要があります。 fixedDeltaTimeはその実行する間隔を秒単位で設定できます。

fixedDeltaTimeの値を小さくすると物理演算の精度は上がりますが、実行回数が増えるため負荷が大きくなります。 fixedDeltaTimeの値を大きくすると物理演算の精度は下がりますが、実行回数が減るため負荷が小さくなります。

fixedDeltaTimeはデフォルトで適切な値に設定されているため、 特別な理由がない限りは変更しないことをおすすめします。

エディタからは "Edit > Project Settings > Time" の "Fixed Timestep" にて変更できます。

timeScale

timeScaleプロパティは時間の流れる速度の倍率を設定します。 2.0と設定すると二倍の速度で時間が流れ、0.5と設定すると半分の速度で時間が流れます。 また値を0に設定すると、時が止まります。

仕組みとしては、各Timeクラスのプロパティの更新にこの値の乗算値を用いているだけです。 Updateなどの処理は通常通り呼び出されます。 Unity標準のコンポーネント(AnimatorやRigidbodyなど)は値の更新にTimeクラスのプロパティを考慮しているため期待通りの動作をしますが、 自身で記述したコードに関しては、適切にTimeクラスのプロパティを考慮していなければ設定が反映されません。

unscaledTime及びunscaledDeltaTime、またrealtimeSinceStartupはtimeScaleの影響を受けません。 ゲームの一部分だけ通常の速度で動かしたい場合にはこれらの値を用います。

timeScaleの値を変更する際には、物理演算に影響がでないように同時にfixedDeltaTimeの値も更新することが推奨されています (リファレンス参照)。

timeScaleの値を0に設定した場合には、例外的にFixedUpdateが呼び出されなくなります。

エディタからは "Edit > Project Settings > Time" の "Time Scale" にて変更できます。

maximumDeltaTime

maximumDeltaTimeプロパティは処理落ちを許容して物理演算を正確に実行し続ける最大のフレーム時間を設定します。

物理演算は精度と一貫性のために一定の間隔で更新処理を行う必要があります。 しかし、ガベージコレクションと物理演算の負荷が高い状態が重なった場合などに、 一時的に物理演算の更新処理が間に合わなくなり、滞ってしまうことがあります。 このような場合に更新処理を続けると、次々と更新要求がたまってしまい、数フレームにわたって大きな処理落ちが発生してしまいます。

この状況を正常化するため、Unityは処理落ちが大きくなった場合には物理演算をあきらめて、次のフレームの処理へと進みます。 これにより、更新処理がスキップされるためその間のオブジェクトの移動はすべて無視されますが、比較的速やかに処理落ち状態は解消されます (逆にこれをしないと、「処理落ち」という言葉から想像されるようなスローモーション状態が長く続きます)。 maximumDeltaTimeはこの『処理落ちが大きくなった』状態をどの程度とするかを秒単位で設定します。

1/10秒から1/3秒程度に設定することが推奨されています。

エディタからは "Edit > Project Settings > Time" の "Maximum Allowed Timestep" にて変更できます。

captureFramerate

captureFramerateプロパティはスクリーンショットのためにゲームの更新処理の間隔を設定します。

非常に特殊なケースとして、ゲーム画面のキャプチャをしたい場合にゲームの更新処理の間隔を変更したくなることがあります。 このような場合にcaptureFramerateに値を設定すると、1.0 / captureFramerateの間隔でゲームの更新処理を行うことができます。 値を0に設定した場合にはこの機能は無効になります。

経過時間の計測

Timeクラスのプロパティを利用して、経過時間を計測するには次の二つの方法があります。

  • 現時刻と開始時刻の差を計算
  • フレームにかかった時間の総和を計算

現時刻と開始時刻の差を計算

経過時間の計測法として素直なのは現時刻と開始時刻の差を計算する方法です。

計測開始のタイミングで時刻を取得して値を保存、経過時間の計測時には現時刻を取得し、その値から開始時刻の値を引きます。

...
this.startTime = Time.time;   // 開始時刻を取得
...

...
float elapsedTime = Time.time - this.startTime;   // 現時刻との差から経過時間を取得
...

この方法の利点は値の更新処理がいらないことと誤差が発生しづらいことです。 保存すべき値は開始時刻のみで、開始時刻は変化しないため更新処理が要りません。 また演算は減算一回のみであるため誤差はほぼありません。

一方で、欠点は特殊な処理を挟みづらいことです。 例えば、ゲームのクリア時間を計るとき、特殊な演出中に時を止めたり、失敗に対してペナルティを与えたりすることがあると思います。 このような場合に処理が少し面倒なことは想像に難くないでしょう。

フレームにかかった時間の総和を計算

ふたつめの計測法はフレームにかかった時間の合計を求める方法です。

計測開始のタイミングで経過時間を0で初期化し、毎フレームかかった時間を足し合わせていくことで経過時間を計算します。

...
this.elapsedTime = 0.0f;   // 経過時間を初期化
...

...
this.elapsedTime += Time.deltaTime   // 毎フレームかかった時間を足して、経過時間を更新
...

この方法の利点は値が操作しやすいことです。 値の更新をやめれば時が止まったようになり、また加減算の処理も楽々です。

一方で、欠点は計算誤差が発生しやすいことです。 毎フレーム演算が発生するため、時間が経つほどに誤差が蓄積していくことになります。 とはいえ、正の値同士の加算のみなので、問題となるほどの誤差が発生する状況は限定的です。

この方法で誤差が発生する最も大きな要因は情報落ちです。 つまり、経過時間が極めて大きくなった場合にフレーム間の時間が相対的に小さくなりすぎて無視されてしまう状態です。 ゲームの総プレイ時間のような非常に長くなる可能性のある値を計測するような場合には注意が必要です。

ちなみに、この現象を確認しようと1時間ほど初期値0から計測してみた結果、誤差は0.7秒程度でした。 一方で、同様の計算を60FPSの固定フレームレートと仮定して行った結果、誤差は2.8秒程度となりました。 たまたまうまく誤差が相殺した可能性もありますが、もしかすると内部で補正をしているのかもしれません。 どちらにしても、初期値が0でない場合(セーブデータから読み込んだ時など)には大きな誤差が発生しうるので注意が必要なことには変わりありません。

おわりに

他にもいくつか書こうと思っていたことがあったのですが、文字数が10000文字に達しそうなのでこのあたりで。



執筆時のUnityのバージョン:5.4.0f3

*1:何故か原文の説明が途切れていたため、プロパティ詳細ページの内容で補完しています。