UnityのTime.deltaTime 詳細解説

本資料では、UnityにおけるTime.deltaTimeの仕組みを、基本的な概念から内部処理、応用例、最適化、デバッグ、そして高度な活用法まで、あらゆる角度から徹底的に解説します。Unity初心者から中・上級者まで、あらゆるレベルの開発者が参考にできるよう、実例や注意点も交えて詳細に説明します。


目次

1. はじめに

  • 背景:
    ゲーム開発では、描画フレーム(FPS)は使用環境や負荷により大きく変動します。フレーム数だけに依存した処理では、異なる環境で動作速度が大きく変わってしまいます。
  • 目的:
    Time.deltaTimeを用いて、実際の経過時間に基づいた処理を行い、どの環境でも一貫したゲーム体験を実現する方法を学びます。

2. 基本概念

2.1 Time.deltaTimeの定義

  • 概要:
    前のフレームから現在のフレームまでの経過時間(秒)を返すプロパティ。
  • 例:
    • 60FPS → 約0.016秒
    • 30FPS → 約0.033秒

2.2 役割と重要性

  • フレームレート補正:
    FPSが異なる環境でも、「1秒間に○○ユニット動く」といった一貫した動作を実現できる。
  • 時間依存の処理:
    移動、回転、アニメーション、タイマーなど、時間経過に依存する処理に不可欠。

3. フレームレートとTime.deltaTimeの数学的関係

3.1 FPSの変動と経過時間

  • 高FPSの場合:
    • 経過時間が短く、1フレームごとのTime.deltaTimeは小さい。
    • 例: 60FPS → 1/60 ≒ 0.016秒
  • 低FPSの場合:
    • 経過時間が長く、1フレームごとのTime.deltaTimeは大きい。
    • 例: 30FPS → 1/30 ≒ 0.033秒

3.2 数学的補正の考え方

  • 基本式:
    • 移動量(フレーム毎)=速度(ユニット/秒)×Time.deltaTime(秒)
  • 具体例:
    • 1秒間に5ユニット移動する場合:
      • 60FPS: 5 × 0.016 = 0.08ユニット/フレーム
      • 30FPS: 5 × 0.033 = 0.165ユニット/フレーム

4. UpdateとFixedUpdateの使い分け

4.1 Update関数

  • 用途:
    • 入力処理、アニメーション、非物理演算の更新に使用。
    • 毎フレーム呼ばれるため、Time.deltaTimeを使用して補正。
  • 例:
void Update() 
{
    float speed = 5.0f;
    transform.Translate(Vector3.forward * speed * Time.deltaTime);
}

4.2 FixedUpdate関数

  • 用途:
    • 物理演算(Rigidbody操作、衝突判定など)の更新に使用。
    • 固定時間間隔(通常は0.02秒)で呼ばれ、Time.fixedDeltaTimeを利用する。
  • 例:
void FixedUpdate() 
{
    float force = 10.0f;
    rigidbody.AddForce(Vector3.forward * force * Time.fixedDeltaTime);
}

基本的には、何を「力」として定義するかによって意味が変わります。

  • 1秒あたりの力(加速度)の概念で考える場合:
    たとえば「毎秒10ニュートンの力をかけたい」と定義するなら、FixedUpdateは固定時間間隔(例:0.02秒)で呼ばれるため、1フレームあたりの適用量は
    10×0.02=0.2 ニュートンとなり、1秒間に合計10ニュートンが加わる計算になります。
    → この場合、Time.fixedDeltaTimeで乗算する意味があります。
  • 毎フレーム一定の力を加えたい場合:
    すでに各FixedUpdateごとに決まった力(たとえば、毎フレーム10ニュートン)を与えたいのであれば、乗算は不要です。

つまり、1秒間の累積力を一定に保ちたいという意図であれば、Time.fixedDeltaTimeでスケールするのは有効な方法です。

乗算が不要なケースは、力を「各FixedUpdateごとに一定の大きさで加える」ことを意図している場合です。以下、具体的なポイントを解説します。


1. 力の定義の違い

  • 「1秒間あたり」の力として定義する場合:
    たとえば「1秒間に10ニュートンの力を加えたい」とするなら、FixedUpdateは固定間隔(たとえば0.02秒)で呼ばれるため、1フレームあたりの力は
    10ニュートン×0.02秒=0.2ニュートン
    と計算されます。この場合は、Time.fixedDeltaTimeを乗算する意味があり、実際の1秒あたりの合計が10ニュートンになるよう補正します。
  • 「各FixedUpdateごとに固定の力を加える」場合:
    もし力をすでに「このFixedUpdateで常に10ニュートンを加える」というように定義している場合、すでに各呼び出しで一定の力が作用しているので、Time.fixedDeltaTimeでさらに乗算すると、かえって力が意図よりも小さくなってしまいます。

2. 力の適用方法と物理エンジンの性質

  • ForceMode.Forceの場合:
    デフォルトのForceMode(連続的な力として作用)では、物理エンジンはこの力をフレーム間の時間で積分して物体の加速度を計算します。
    • 1秒間あたりの力の値で設計するなら、各FixedUpdateでTime.fixedDeltaTimeを乗算して、時間に応じた小さな力を加えるのが一般的です。
  • ForceMode.Impulseの場合:
    インパルスは「瞬間的に与えられる力の変化(運動量の変化)」として扱われ、すでに時間の積分が考慮されています。
    • この場合、各FixedUpdateごとに一度だけ大きな力(衝撃)を与える設計なら、Time.fixedDeltaTimeで乗算する必要はありません。

3. まとめ

  • 乗算する場合:
    • 「1秒あたりの力」を定義し、連続的に適用する設計の場合。
    • 物理シミュレーションの中で、時間に依存した変化(滑らかな加速度)を実現するために有効です。
  • 乗算が不要な場合:
    • 各FixedUpdateごとに、既に固定の力(または衝撃としてのインパルス)を与えたい場合。
    • 固定の値をそのまま使って、一定の変化量(または一定のインパルス)を適用する設計の場合、余分な乗算は不要となります。

つまり、各FixedUpdateで「固定の10ニュートンの力」を加えるという意図なら、Time.fixedDeltaTimeを乗算する必要はありません。設計意図に合わせて、どちらの手法が適しているかを判断することが大切です。

4.3 適切な使い分け

  • 非物理: Update + Time.deltaTime
  • 物理: FixedUpdate + Time.fixedDeltaTime

5. Time.deltaTimeと関連プロパティの連携

5.1 Time.timeScale

  • 定義:
    ゲーム全体の時間の流れを制御するプロパティ。
    • 1が通常速度、0.5で半分、0で停止。
  • 連動:
    Time.deltaTimeはTime.timeScaleの影響を受けるため、スローモーションや一時停止時も自然な動作となる。

5.2 Time.unscaledDeltaTime

  • 用途:
    timeScaleの影響を受けずに経過時間を取得したい場合に使用。
  • 例:
    UIアニメーションや特定のタイマー処理に利用。

5.3 Time.smoothDeltaTime

  • 概要:
    複数フレームに渡って平滑化されたdeltaTimeの値を返す。
  • 利点:
    急激なフレーム遅延があった場合でも、滑らかな値を提供し、ビジュアルやアニメーションの一貫性を維持する。

6. 内部処理とTime.deltaTimeの取得方法

6.1 Unityエンジン内部の計算

  • 内部処理:
    Unityは各フレームの開始時に、前フレームとの時間差を計算し、deltaTimeとして格納します。
  • 最大値の制限:
    突発的なフレーム遅延(ラグスパイク)に対しては、Time.maximumDeltaTimeが設定され、異常に大きなdeltaTime値の影響を抑えることが可能です。

6.2 平均化とスムージング

  • Time.smoothDeltaTimeの利用:
    複数フレームの値を平均化することで、急激な変動を緩和し、アニメーションや物理シミュレーションでの不連続性を回避します。

7. 応用例:高度な実装テクニック

7.1 複数タイマーの同時管理

  • シナリオ:
    複数のイベントが異なるタイミングで発生する場合、各イベントで独立したタイマーを管理。
  • コード例:
float timerA = 0f;
float timerB = 0f;
float intervalA = 2.0f; // 2秒ごとにイベントA
float intervalB = 5.0f; // 5秒ごとにイベントB

void Update() 
{
    timerA += Time.deltaTime;
    timerB += Time.deltaTime;

    if (timerA >= intervalA) 
    {
        Debug.Log("イベントA発生");
        timerA -= intervalA;
    }
    if (timerB >= intervalB) 
    {
        Debug.Log("イベントB発生");
        timerB -= intervalB;
    }
}

条件が整った際にタイマーを0にリセットする方法も動作しますが、注意点があります。

0に代入する場合の問題点

  • 余剰時間の切り捨て
    例えば、タイマーが2.1秒になっている状態でイベントが発生すると、0にリセットすると0.1秒分の余剰時間が失われます。これが繰り返されると、時間のずれが蓄積され、インターバルの正確性が低下する可能性があります。

減算する方法のメリット

  • 正確なタイミング維持
    timerA -= intervalA; のように減算する方法では、余剰時間(上記の例では0.1秒)が次のサイクルに持ち越されるため、インターバルがより正確に維持されます。

結論

  • シンプルな用途の場合
    微妙な誤差が問題にならない場合は、タイマーを0にリセットしても大きな問題にはならないでしょう。
  • 正確性が求められる場合
    長期間の動作や厳密なタイミングが必要な場合は、減算する方法を推奨します。

このように、用途や求められる精度によって使い分けると良いでしょう。

7.2 非同期処理との連携(コルーチン)

  • 概要:
    コルーチン内で時間経過を管理する際、Time.deltaTimeを利用しながらyield return null;で1フレーム待機し、自然な時間経過を実現。
  • コード例:
IEnumerator FadeOut(CanvasGroup canvasGroup, float duration) 
{
    float elapsed = 0f;
    while (elapsed < duration) 
    {
        elapsed += Time.deltaTime;
        canvasGroup.alpha = 1 - (elapsed / duration);
        yield return null;
    }
    canvasGroup.alpha = 0;
}

7.3 補間と滑らかな変化(LerpとSmoothDamp)

  • Lerp:
    線形補間にTime.deltaTimeを掛け合わせ、フレームレートに依存しない滑らかな変化を実現。
  • SmoothDamp:
    指定した減衰時間で目標値に近づける方法。Time.deltaTimeは変化量の計算に利用され、振動や急激な変化を抑えます。
  • コード例:
float velocity = 0f;
float smoothTime = 0.3f;
float currentValue = 0f;
float targetValue = 100f;

void Update() 
{
    currentValue = Mathf.SmoothDamp(currentValue, targetValue, ref velocity, smoothTime, Mathf.Infinity, Time.deltaTime);
}

8. パフォーマンスと最適化の観点

8.1 計算負荷の最小化

  • 必要な箇所だけで使用:
    毎フレーム大量の計算が発生する場合、Time.deltaTimeの値はキャッシュして使うなど、無駄な計算を避ける工夫が必要です。
  • 固定更新:
    物理演算の更新はFixedUpdateで行うことで、一定間隔の計算により予測可能なパフォーマンスを実現。

8.2 異常値への対策

  • ラグスパイク対策:
    一時的にTime.deltaTimeが大きくなる場合、上限値を設定して急激な変化を防ぐ。
float safeDeltaTime = Mathf.Min(Time.deltaTime, 0.05f);
  • Time.maximumDeltaTimeの利用:
    プロジェクト設定やスクリプト内で、最大deltaTimeを設定して不安定な動作を抑制する。

9. デバッグとトラブルシュート

9.1 ログ出力による監視

  • デバッグ方法:
    実行中にTime.deltaTimeやTime.smoothDeltaTime、Time.fixedDeltaTimeの値をログ出力して、フレームごとの変動をモニタリングする。
  • コード例:
void Update() 
{
    Debug.Log($"deltaTime: {Time.deltaTime}, smoothDeltaTime: {Time.smoothDeltaTime}");
}

9.2 ビジュアルデバッグ

  • エディタ拡張:
    カスタムインスペクターやデバッグウィンドウを利用して、リアルタイムにdeltaTimeの変動を可視化する方法も有効です。

9.3 問題の切り分け

  • FPS低下の原因分析:
    deltaTimeの急激な増加が、重い処理やガベージコレクションによるものかを特定し、最適化の対象を明確にする。

10. よくある落とし穴とベストプラクティス

10.1 落とし穴

  • deltaTime未使用:
    フレーム数に依存した処理になり、環境ごとに動作が大きく変わる。
  • FixedUpdate内でのTime.deltaTime使用:
    固定更新にはTime.fixedDeltaTimeを使うべき。これにより、物理シミュレーションの一貫性が損なわれる可能性がある。
  • timeScaleの影響見落とし:
    スローモーションや一時停止実装時、timeScaleがdeltaTimeに与える影響を無視すると、意図しない挙動が発生する。

10.2 ベストプラクティス

  • 必ず時間依存の処理にdeltaTimeを使用する。
  • 物理処理はFixedUpdate内でTime.fixedDeltaTimeを利用。
  • スローモーション等の特殊な時間操作が必要な場合、Time.unscaledDeltaTimeを活用する。
  • 大きすぎるdeltaTime値への対策として、上限値を設定する。
  • デバッグ中は、deltaTimeの変動を監視して問題箇所を特定する。

11. 高度な活用法と拡張テクニック

11.1 マルチスレッド・非同期処理との連携

  • Unityのメインスレッドと非同期処理:
    基本的にUnityの更新処理はメインスレッドで実行されますが、非同期処理(TaskやAsync/Await)でバックグラウンド処理を行う場合、UIやアニメーションに影響を与えないよう、メインスレッドへの戻し方に注意が必要です。deltaTimeの値はメインスレッドのUpdate内でのみ意味を持つため、非同期処理では設計パターンを工夫しましょう。

11.2 カスタムタイムマネージャの実装

  • シナリオ:
    ゲーム内で特定のサブシステムやミニゲームなど、メインの時間管理とは異なる時間スケールで動作させたい場合、独自のタイムマネージャを実装する手法もあります。
  • 実装例:
    メインのTime.timeScaleとは別に、サブシステム用の「仮想時間」を管理し、内部でdeltaTimeの補正を行う方法です。

11.3 高精度タイミング

  • リアルタイムシミュレーション:
    高精度が求められるシミュレーションや物理計算では、Time.deltaTimeの精度や安定性を意識する必要があります。必要に応じて、固定更新(FixedUpdate)の設定や、カスタムループによる高精度計測の導入を検討します。

12. まとめ

  • Time.deltaTimeの本質:
    前フレームから現在フレームまでの実時間(秒)を提供し、FPSの変動に対する補正を行うことで、一貫したゲーム体験を実現する基本要素。
  • 関連プロパティとの連携:
    Time.fixedDeltaTime、Time.unscaledDeltaTime、Time.smoothDeltaTime、Time.timeScaleなどと組み合わせることで、さまざまな時間依存処理に柔軟に対応可能。
  • 実装のポイント:
    • UpdateとFixedUpdateの適切な使い分けで、入力処理と物理計算の整合性を保つ。
    • ラグスパイク対策最大値の制限を導入し、急激な変動の影響を防止。
    • デバッグツールの活用により、実行時のdeltaTimeの挙動を監視し、最適化に役立てる。
  • 応用と拡張:
    高度なシナリオやマルチスレッド環境、カスタムタイムマネージャの実装など、基本を押さえた上でさらに発展的な利用法を模索できる。

この超詳細な資料を通して、Unity開発におけるTime.deltaTimeの内部動作、実践的な使い方、そして最適な実装方法を深く理解し、より堅牢で一貫性のあるゲームシステムの構築にお役立てください。

Unity

Posted by hidepon