Search Unity

より安定したゲームプレイを実現する Unity 2020.2 の Time.deltaTime 修正 ― それはどのように成されたのか

, 10月 1, 2020

Unity 2020.2 ベータ版では、多くの開発プラットフォームで問題になっていた、Time.deltaTime の値が一定していないため、ぎくしゃくした、詰まった動きになってしまうという現象が修正されています。このブログ記事では、何が起こっていたのか、そして新しいバージョンの Unity ではどのようにしてよりスムーズなゲームプレイを実現することができるのかを説明します。ぜひご一読ください。

ゲームの黎明期から、ビデオゲームでフレームレートに依存しない動きを実現するためには、フレームのデルタ時間を考慮に入れる必要がありました。

void Update()
{
transform.position += m_Velocity * Time.deltaTime;
}

上記のコードで、ゲームが実行されているフレームレートに関係なく、オブジェクトが一定の平均速度で移動するという望ましい結果が得られます。理論的には、フレームレートが安定していれば、オブジェクトを安定したペースで移動させることができるはずです。しかし、実際には全く違っています。実際に返ってくる Time.deltaTime の値を見てみると、以下のようになっていることがあります。

6.854 ミリ秒
7.423 ミリ秒
6.691 ミリ秒
6.707 ミリ秒
7.045 ミリ秒
7.346 ミリ秒
6.513 ミリ秒

これは Unity を含む多くのゲームエンジンに影響を与える問題です。この問題に目を向けさせてくれたユーザーの皆様に感謝いたします。幸いなことに、Unity 2020.2 ベータ版ではこの問題への対応が開始されています。

では、なぜこのようなことが起こるのでしょうか?フレームレートを一定の 144 fps に固定していても、Time.deltaTime が返す値は毎回 1/144 秒(約 6.94 ミリ秒)にならないのはなぜでしょうか。このブログ記事では、この現象を調査し、ついに修正を実現するまでの過程をご紹介します。

デルタ時間とは何か、なぜ重要なのか

平たく言えば、デルタ時間とは、最新のフレームを描き終わるまでにかかった時間のことです。単純なことに聞こえますが、思ったほど直感的ではありません。ほとんどのゲーム開発関連の書籍には、ゲームループの定型的な定義が載っています。

while (true)
{
ProcessInput();
Update();
Render();
}

このようなゲームループを作ると、デルタ時間の計算を簡単に行えます。

var time = GetTime();
while (true)
{
var lastTime = time;
time = GetTime();
var deltaTime = time – lastTime;
ProcessInput();
Update(deltaTime);
Render(deltaTime);
}

このモデルはシンプルで理解しやすいのですが、最近のゲームエンジンを理解するには非常に不十分です。高いパフォーマンスを実現するために、最近のエンジンは「パイプライン化」と呼ばれる技術を使用しており、これによりエンジンは常に複数のフレームを処理することができます。

この図を

こちらの図と比較してみてください。

どちらの場合も、ゲームループの個別の部分は同じ時間を要しますが、2 つ目のケースでは並列に実行されるため、同じ時間で 2 倍以上の枚数のフレームを出力することができます。エンジンをパイプライン化することで、フレーム時間がすべてのパイプラインのステージにかかる時間の総和から、最も長いパイプラインのステージにかかる時間に変わります。

しかし、こうした説明もエンジンで毎フレーム実際に起こることを単純化した説明にすぎません。

  • パイプラインの各ステージにかかる時間は、フレームごとに異なります。これから処理しようとしているフレームは最後のフレームよりも画面上のオブジェクトが多く、レンダリングにもっと時間がかかるかもしれません。あるいは、プレイヤーがキーボードに顔をあててゴロゴロしたので、大量のキー入力が発生して、入力処理に時間がかかることがあるかもしれません。
  • パイプラインステージごとにかかる時間が異なるので、処理が速く済んだものをわざと停止させて、先に進みすぎないようにする必要があります。最も一般的には、前のフレームがフロントバッファ(スクリーンバッファとも呼ばれる)にフリップされるまで待つという実装になっています。VSync が有効になっている場合、さらにディスプレイの VBLANK 時間の開始への同期が加わります。これについては後ほど詳しく説明します。

その知識を念頭に置いて、Unity 2020.1 での典型的なフレームのタイムラインを見てみましょう。プラットフォームの選択と様々な設定が大きく影響するため、この記事では、マルチスレッドレンダリングを有効にし、グラフィックスジョブを無効にし、VSync を有効にし、QualitySettings.maxQueuedFrames を 2 に設定した Windows スタンドアロンプレーヤーを 144 Hz のモニター上でフレームを落とさずに実行していると仮定します。下の画像はタイムラインの模式図です。クリックするとフルサイズで表示されます。

現在の Unity のフレームパイプラインは、ゼロから実装されたわけではありません。過去 10 年の間に進化し、現在のようなものになりました。過去のバージョンの Unity を振り返れば、数回のリリースごとに変更されていることがわかります。

フレームパイプラインについて、以下のようなことにすぐ気づくかもしれません。

  • すべての作業が GPU に送信されると、Unity はそのフレームが画面に反映されるのを待たずに、前のフレームを待ちます。これは QualitySettings.maxQueuedFrames API によって制御されます。ここの設定では、現在表示されているフレームが、現在レンダリング中のフレームの後ろにどれだけ行けるかを指定します。framen が画面に表示されているときに framen+1 をレンダリングするのが最速なので、最小値は 1 です。この場合は 2 に設定されているので(これがデフォルトです)、Unity は framen+2 のレンダリングを開始する前に framen が画面に表示されるようにします(例えば、Unity が frame5 のレンダリングを開始する前に、画面に frame3 が表示されるのを待ちます)。
  • frame5 を GPU でレンダリングする時間は、モニタのリフレッシュの間隔より長い(7.22 ミリ秒と 6.94 ミリ秒)ですが、フレームはドロップしません。これは、QualitySettings.maxQueuedFrames の値が 2 になっていることで、実際のフレームが画面に表示されたときに遅延が起き、「スパイク」が常態化しない限りはフレームのドロップを防ぐためのバッファが時間内に生成されるためです。もしこの値が 1 なら、Unity で作業の重なりが無くなるので、フレームは確実にドロップしていたでしょう。

6.94 ミリ秒ごとに画面がリフレッシュされるにもかかわらず、Unity 側でデルタ時間をサンプリングすると、実際の数値は異なってきます。

tdeltaTime(5) = 1.4 + 3.19 + 1.51 + 0.5 + 0.67 = 7.27 ミリ秒
tdeltaTime(6) = 1.45 + 2.81 + 1.48 + 0.5 + 0.4 = 6.64 ミリ秒
tdeltaTime(7) = 1.43 + 3.13 + 1.61 + 0.51 + 0.35 = 7.03 ミリ秒

この場合のデルタタイムの平均値((7.27 + 6.64 + 7.03)/3 = 6.98 ミリ秒)は、実際のモニターのリフレッシュレート(6.94 ms)に非常に近く、これをより長い期間測定した場合、最終的には平均値はほぼ 6.94 ミリ秒に収束していきます。しかし残念ながら、このデルタ時間を可視オブジェクトの動きを計算するためにそのまま使用すると、非常に微妙なジッターが発生します。これを説明するために、簡単な Unity プロジェクトを作成しました。このプロジェクトでは、3 つの緑の正方形がワールド空間を移動します。

カメラは一番上の正方形にアタッチされているので、画面上では完全に静止しているように見えます。Time.deltaTime が正確ならば、真ん中と下の正方形も静止しているように見えるはずです。正方形は 1 秒ごとにディスプレイの幅の 2 倍の速度で移動します。速度が大きいほど、ぶれも見えやすくなります。動きをわかりやすくするために、紫とピンクの動かないキューブを背景のある位置に固定して、赤い線の間の正方形がどのくらいの速度で動いているかをわかりやすくしました。

Unity 2020.1 では、真ん中の正方形と下の正方形の動きが一番上の正方形の動きと完全に一致しません。ともに少しぶれているのがわかります。下の動画は、スローモーションカメラで撮影したものです(1/20 倍速に減速しています)。

 

デルタ時間変動の発生源の特定

こうしたデルタ時間が安定しないという現象の原因は何なのでしょうか。ディスプレイは、6.94 ミリ秒ごとに画像を変化させ、各フレームが一定時間表示されるように動作しています。この 6.94 ミリ秒こそ、フレームが画面に表示されるまでにかかる時間であり、ゲームのプレイヤーが各フレームを観察する時間、すなわち真のデルタ時間です。

6.94 ミリ秒という時間の中身は、処理とスリープの 2 つの部分で構成されています。例に挙げたフレームのタイムラインを見ると、デルタ時間はメインスレッドで計算されていることがわかりますので、ここではメインスレッドでの処理を中心に説明します。メインスレッドの処理部分は、OS メッセージのポンピング、入力処理、Update の呼び出し、レンダリングコマンドの発行で構成されています。「Wait for render thread」はスリープ部分です。この 2 つの時間の合計が実フレーム時間に相当します。

tprocessing + twaiting = 6.94 ミリ秒

これらのタイミングはいずれもフレームごとに様々な理由で変動しますが、その合計は一定です。処理時間が増加すれば待ち時間は減少し、逆もしかりです。結果、2 つの時間の合計は常に正確に 6.94 ms になります。実際、待ち時間までのすべての部分の合計は常に 6.94 ミリ秒に等しくなります。

tissueGPUCommands(4) + tpumpOSMessages(5) + tprocessInput(5) + tUpdate(5) + twait(5) = 1.51 + 0.5 + 0.67 + 1.45 + 2.81 = 6.94 ミリ秒
tissueGPUCommands(5) + tpumpOSMessages(6) + tprocessInput(6) + tUpdate(6) + twait(6) = 1.48 + 0.5 + 0.4 + 1.43 + 3.13 = 6.94 ミリ秒
tissueGPUCommands(6) + tpumpOSMessages(7) + tprocessInput(7) + tUpdate(7) + twait(7) = 1.61 + 0.51 + 0.35 + 1.28 + 3.19 = 6.94 ミリ秒

ただし、Unity は Update の開始時に時間を照会します。そのため、レンダリングコマンドの発行、OS メッセージのポンピング、入力イベントの処理などにかかる時間にばらつきがあると、結果に影響が出てしまいます。

Unity のメインスレッドループの簡略化した定義は次のようになります。

while (!ShouldQuit())
{
PumpOSMessages();
UpdateInput();
SampleTime(); // ここで時間を計測
Update();
WaitForRenderThread();
IssueRenderingCommands();
}

この問題の解決方法は簡単そうに見えます。時間計測を待ち時間の後に移動させればよさそうです。これを行うと、ゲームループはこのようになります。

while (!ShouldQuit())
{
PumpOSMessages();
UpdateInput();
Update();
WaitForRenderThread();
SampleTime();
IssueRenderingCommands();
}

しかし、こう変更しても期待通り動作しません。レンダリングは Update() とは異なる時間の読み方をしており、これはあらゆる部分に悪影響を及ぼします。1 つのオプションとして、この時点でサンプリングされた時間を保存し、次のフレームの開始時にのみエンジンの時間を更新するという方法があります。しかしこうすると、エンジンが最新のフレームをレンダリングする前の時間を使用するようになってしまいます。

SampleTime()Update() の後に移動させても効果がないことがわかりました。次に、待ち時間をフレームの先頭に移動させた方が効果的かもしれないとあたりをつけて、次のようにしてみます。

while (!ShouldQuit())
{
PumpOSMessages();
UpdateInput();
WaitForRenderThread();
SampleTime();
Update();
IssueRenderingCommands();
}

残念ながら、これは別の問題を引き起こします。レンダリングスレッドが、要求されてからほぼすぐにレンダリングを終了しなければならなくなり、レンダリングスレッドが並列で作業を行ったときの恩恵が最小限にしまいます。

フレームのタイムラインをもう一度見てみましょう。

Unity では、毎フレームレンダリングスレッドを待機させることで、パイプラインの同期を強制しています。これは、メインスレッドが画面に表示されているものから先行しすぎて、表示内容と乖離した処理を実行しないようにするために必要な処理です。レンダリングスレッドは、レンダリングが終了して画面にフレームが表示されるのを待つようになると、「作業が終了した」とみなされます。言い換えれば、バックバッファが反転してフロントバッファになるのを待ちます。しかし、レンダリングスレッドは前のフレームがいつ画面に表示されたかは関知しません。メインスレッドだけが、適宜スロットルを行うためにこのタイミングを監視します。そのため、レンダリングスレッドにフレームが画面に表示されるのを待たせる代わりに、この待ち時間をメインスレッドに移すことができます。これを WaitForLastPresentation() と呼びましょう。メインスレッドのループは次のようになります。

while (!ShouldQuit())
{
PumpOSMessages();
UpdateInput();
WaitForLastPresentation();
SampleTime();
Update();
WaitForRenderThread();
IssueRenderingCommands();
}

ループの待機部分の直後に時間が計測されるようになり、そのタイミングはモニターのリフレッシュレートに合致するようになります。時間はフレームの先頭でもサンプリングされるので、Update()Render() が同じ時間を共有します。

ここで非常に重要なのは、WaitForLastPresention() は framen – 1 が画面に表示されるのを待たないということです。もし表示を待つようであれば、パイプライン化は全く行われないことになります。その代わり、画面上に framen – QualitySettings.maxQueuedFrames が表示されるのを待ちます。これにより、最後のフレームが完了するのを待たずにメインスレッドを続行することができます(maxQueuedFrames が 1 に設定されている場合は、新しいフレームが開始される前にすべてのフレームが完了しなければならないので、ここに書いたようにはなりません)。

安定化の達成には深いところまで行く必要がある

このソリューションを実装した後、デルタ時間は以前よりもはるかに安定しましたが、タイミングの若干の揺らぎや、突然の変動は依然として発生していました。この原因は、オペレーティングシステムが定刻にスリープからエンジンを起動する仕組みに依存していることでした。起動には数マイクロ秒かかることがあり、特に複数のプログラムが同時に実行されるデスクトッププラットフォームで、デルタ時間に揺らぎを生じさせる原因になっていました。

では、どうすればいいのでしょうか。ほとんどのグラフィックス API やプラットフォームでは、画面(またはオフスクリーンバッファ)に表示されているフレームの正確なタイムスタンプを抽出することができます。例えば、Direct3D 11 と 12 には IDXGISwapChain::GetFrameStatistics があり、macOS には CVDisplayLink があります。しかし、この方法にはいくつかの欠点があります。

  • サポートされているグラフィックス API ごとに別個の抽出コードを書く必要があり、時間計測コードはプラットフォーム固有のものとなり、各プラットフォームについて独自の実装がされる状態になります。プラットフォームによって動作が異なるため、このような変更は壊滅的な結果を招く危険性があります。
  • いくつかのグラフィックス API では、このタイムスタンプを取得するために、VSync を有効にしなければなりません。つまり、VSync が無効になっていたら、時間を手動で計算しなければならないケースが出てきます。

しかし、私はこの方法はリスクと労力を払う価値があると考えています。この方法で得られた結果は、非常に信頼性が高く、ディスプレイに表示されているものに直接対応するタイミングが得られるのです。

グラフィックス API から計測した時間を抽出するようになったので、WaitForLastPresention() ステップと SampleTime() ステップは新しいステップに統合されました。

while (!ShouldQuit())
{
PumpOSMessages();
UpdateInput();
WaitForLastPresentationAndGetTimestamp();
Update();
WaitForRenderThread();
IssueRenderingCommands();
}

これで、揺らぎのある動きの問題は解決しました。

入力遅延の考慮

入力遅延はトリッキーなテーマです。正確に測定するのは簡単ではなく、入力ハードウェア、オペレーティングシステム、ドライバー、ゲームエンジン、ゲームロジック、ディスプレイなど、さまざまな要因によってもたらされます。ここでは、入力遅延のうち、Unity が唯一影響を与えうるゲームエンジンの要素に焦点を当ててみました。

エンジンの入力遅延は、入力される OS メッセージが利用可能になってから画像がディスプレイにディスパッチされるまでの時間です。メインスレッドループを考えると、コードの一部として入力はコードの一部として可視化できます(QualitySettings.maxQueuedFrames が 2 に設定されていると仮定)。

PumpOSMessages(); // フレーム 0 で入力される OS メッセージをポンプ
UpdateInput(); // フレーム 0 の入力を処理
——————— // フレーム 0 の一部にならなかった OS からの入力イベントのうち、最も早いものがここで到達
WaitForLastPresentationAndGetTimestamp(); // フレーム -2 が画面に表示されるのを待つ
Update(); // ゲームの状態をフレーム 0 のものに更新
WaitForRenderThread(); // フレーム -1 からのすべてのコマンドが GPU に送信されるまで待つ
IssueRenderingCommands(); // フレーム 0 のレンダリングコマンドをレンダリングスレッドに送信
PumpOSMessages(); // フレーム 1 で入力される OS メッセージをポンプ
UpdateInput(); // フレーム 1 の入力を処理
WaitForLastPresentationAndGetTimestamp(); // フレーム -1 が画面に表示されるのを待つ
Update(); // ゲームの状態をフレーム 1 に更新。ここで到着した入力イベントが見える
WaitForRenderThread(); // フレーム 0 からのすべてのコマンドが GPU に送信されるまで待つ
IssueRenderingCommands(); // フレーム 1 のレンダリングコマンドをレンダリングスレッドに送信
PumpOSMessages(); // フレーム 2 で入力される OS メッセージをポンプ
UpdateInput(); // フレーム 2 の入力を処理
WaitForLastPresentationAndGetTimestamp(); // フレーム 0 が画面に表示されるのを待つ
Update(); // ゲームの状態をフレーム 2 に更新
WaitForRenderThread(); // フレーム 1 からのすべてのコマンドが GPU に送信されるまで待つ
IssueRenderingCommands(); // フレーム 2 のレンダリングコマンドをレンダリングスレッドに送信
PumpOSMessages(); // フレーム 3 で入力される OS メッセージをポンプ
UpdateInput(); // フレーム 3 の入力を処理
WaitForLastPresentationAndGetTimestamp(); // フレーム 1 が画面に表示されるのを待つ。ここで入力イベントによる変更が目に見えるようになる

ずいぶんと長くなりましたが、以上です。入力が OS メッセージとして利用可能になってから、その結果が画面に表示されるまでの間には、かなり多くのことが起こります。Unity がフレームをドロップしておらず、ゲームループの中で処理時間に比べるとほとんどの時間が待ち時間とすると、リフレッシュレート 144hz の場合のエンジンからの入力遅延は、最悪の場合で、4 * 6.94 = 27.76 ミリ秒となります。4 が掛かっているのは、前のフレームが画面に表示されるのを 4 回(つまり、リフレッシュレートから計算される時間の 4 倍)待っているからです。

OS のイベントをポンプして、前のフレームが表示されるのを待ってから入力を更新することで、遅延を改善することができます。

while (!ShouldQuit())
{
WaitForLastPresentationAndGetTimestamp();
PumpOSMessages();
UpdateInput();
Update();
WaitForRenderThread();
IssueRenderingCommands();
}

これにより、先ほどの式から待ち時間が 1 回分消えて、最悪の場合の入力遅延は 3 * 6.94 = 20.82 ミリ秒となります。

QualitySettings.maxQueuedFrames を 1 に減らすことをサポートしているプラットフォームでは、そう設定することで入力遅延をさらに短縮することができます。この場合、入力処理のチェーンは次のようになります。

——————— // OS から入力イベントが到着
WaitForLastPresentationAndGetTimestamp(); // フレーム -2 が画面に表示されるのを待つ
PumpOSMessages(); // フレーム 0 で入力される OS メッセージをポンプ
UpdateInput(); // フレーム 0 の入力を処理
Update(); // 計測している入力イベントで、ゲームの状態をフレーム 0 に更新
WaitForRenderThread(); // フレーム -1 からのすべてのコマンドが GPU に送信されるまで待つ
IssueRenderingCommands(); // フレーム 0 のレンダリングコマンドをレンダリングスレッドに送信
WaitForLastPresentationAndGetTimestamp(); // フレーム 0 が画面に表示されるのを待つ。ここで入力イベントによる変更が目に見えるようになる

これで、最悪の場合の入力遅延は 2 * 6.94 = 13.88 ミリ秒となります。これは、VSync を使用した場合に達成できる最低値です。

警告:Setting QualitySettings.maxQueuedFrames を 1 に設定すると、エンジンのパイプライン化が実質的に無効になり、目標のフレームレートを達成するのが非常に難しくなります。フレームレートが低下した場合、入力遅延は QualitySettings.maxQueuedFrames を 2 のままにした場合よりも悪化する可能性が高いことは覚えておいてください。例えば、この設定により FPS が 72 まで低下した場合、入力遅延は 2 * 1/72 = 27.8 ミリ秒となり、設定を変更する前の 20.82 ミリ秒よりも悪化してしまいます。この設定を利用したい場合は、ゲーム設定メニューにオプションとして追加することをお勧めします。こうすれば、高速なハードウェアを使用するゲーマーは QualitySettings.maxQueuedFrames を減らし、低速なハードウェアを使用するゲーマーはデフォルト設定を維持することで、両者とも快適なゲームプレイを維持できます。

入力遅延に対する VSync の効果

VSync を無効にすることは、特定の状況では入力遅延を減らすのに役立ちます。入力遅延とは、OS からの入力が利用可能になってから、入力を処理したフレームが画面に表示されるまでの時間であることを思い出してください。数式で表すと、次のようになります。

latency = tdisplaytinput

この式を考えると、入力遅延を短縮するには 2 つの方法があることがわかります。tdisplay を小さくする(画像を画面により早く表示する)か、tinput を大きくする(入力イベントの問い合わせを後回しにする)かです。

GPU からディスプレイへの画像データの送信は、非常に多くのデータを扱う手続きです。2560×1440 の非 HDR 画像を毎秒 144 回ディスプレイに送信するには、毎秒 12.7 ギガビット(24 ビット/ピクセル* 2560*1440*144)を送信する必要があります。GPU は常にピクセルをディスプレイに送信しているため、このデータを瞬時に送信することはできません。各フレームが送信された後、短い休止時間があり、次のフレームの送信が始まります。この休止時間は VBLANK と呼ばれています。VSync が有効になっている場合、基本的には VBLANK の間にだけフレームバッファを反転させるよう OS に指示することになります。

VSync をオフにすると、レンダリングが終了した瞬間にバックバッファがフロントバッファに反転します。つまり、ディスプレイはリフレッシュサイクルの途中で突然新しい画像からデータを取り始め、この結果フレームの上部が古いフレームの画像に、下部が新しいフレームの画像になってしまいます。

この現象は「ティアリング」と呼ばれています。ティアリングを許容すれば、tdisplay が小さくなり、ビジュアルの品質とアニメーションの滑らかさは犠牲になるものの、入力遅延を小さくすることができます。これは、ゲームのフレームレートが VSync 間隔よりも低い場合に特に効果的で、VSync に遅れたことによる遅延を部分的に回復させることができます。また、画面上部が UI やスカイボックスで占められているゲームでは、ティアリングに気付きにくくなるため、より効果的です。

VSync を無効にすることで入力遅延を減らすことができるもう 1 つの方法は、tinput を大きくすることです。ゲームがリフレッシュレートよりもはるかに高いフレームレートでレンダリングできる場合(例えば、60 Hz のディスプレイで 150 fps)、VSync を無効にすることで、ゲームは各リフレッシュ間隔の間に OS イベントを何度かポンプするようになり、エンジンが処理するのを待つ OS の入力キューにかけられる平均時間が短縮されます。

VSync を無効にするかどうかは、最終的にはゲームのプレイヤーに委ねるべきであることは覚えておいてください。ビジュアル品質に悪影響を及ぼしたり、ティアリングが目に付いて、プレイヤーに不快感を与えることもあるためです。プラットフォームでサポートされている場合は、ゲーム内で有効化/無効化の設定オプションを提供することが最善の方法です。

結論

ここまで述べてきた修正が実装された結果、Unity のフレームのタイムラインは以下のようになりました。

しかし、実際にオブジェクトの動きの滑らかさは向上するのでしょうか。はい、向上したのです!

この記事の冒頭で紹介した Unity 2020.1 のデモを、Unity 2020.2.0b1 で実行してみました。その結果のスローモーション動画がこちらです。

 

この修正は、Unity 2020.2 ベータ版において、以下のプラットフォームとグラフィックス API に対して適用されます。

  • Windows、Xbox One、ユニバーサル Windows プラットフォーム(D3D11 および D3D12)
  • macOS、iOS、tvOS(Metal)
  • Playstation 4
  • Switch

Unity でサポートされているプラットフォームで、ここに挙げられていないものについても、近いうちにこの修正を実装する予定です。

フォーラムのこちらのスレッドをフォローして最新情報を入手し、これまでの作業についてのご意見をお聞かせください。

フレームのタイミングについてさらに知りたい方へ

Unity 2020.2 ベータ版の先へ

Unity 2020.2 でどんな技術が使えるようになるのかにご興味のある方は、ベータ版の内容を紹介するブログ記事をチェックして、Unity 2020.2 ベータ版のウェビナーにご登録ください。また先日、2021 年のロードマップ計画についても記事を公開しました。こちらにもぜひ目をお通しください。

35 replies on “より安定したゲームプレイを実現する Unity 2020.2 の Time.deltaTime 修正 ― それはどのように成されたのか”

What Are the Great things about Collagen for Your Body?
The protein features a big job in the body. “Collagen gives body tissues structure, toughness, rigidity, and texture.
In your skin, it’s comparable to a layer of leather. And when it intermingles with elastic fibers,
it provides skin strength and resilience,” says Yag-Howard.

When collagen begins to degrade in skin and degrees of it drop in the body, you may notice wrinkles, stiffer
tendons and ligaments, weaker muscles, pain, and even GI problems,
in line with the Cleveland Clinic. It’s clear that collagen is essential for the
fitness of every system in your body. Within the
skin, taking as much as 10 grams (g) of collagen peptides
per day might help improve skin elasticity, hydration,
and collagen density. (1) What’s more, a review in the journal Antioxidants in February 2020 notes
that taking hydrolyzed collagen can also protect against
UV-induced melasma, a skin problem marked by patches of discoloration on the
face, potentially because of its antioxidant effects.

That was a really good write up of the problem. I got here because I was trying to write a timer script that simulated a time bomb beeping using Time.deltaTime and the results were inconsistent. I found recommendations to use Time.time to compare Time differences but the documentation says to avoid doing that. It might just be something wrong with my implementation but the article is still very informative.

The Timer beeps also increase in frequency from 1 sec > 0.5 sec > 0.25 sec so that might have something to do with it.

Why did you start with this code:

void Update()
{
transform.position += m_Velocity * Time.deltaTime;
}

While **everywhere** – including Unity official documentation, you can read: you should **NOT** change / calculate physics in Update, but in FixedUpdate, using Time.fixedDeltaTime.

Why is that? How can we trust an article beginning with that? What opinion/best practice should we trust in Unity?

Thank you for addressing this issue! It’s remarkable how intricate deltaTime calculation can be. Great work by the team!

Speaking of deltaTime, when you have a game with VSync on, Application.targetFramerate is 60 and playing on a 144 Hz monitor, the game will run as if it was in fast-forward. Is there any chance there might be a “cappedDeltaTime” of sorts exposed to Animator, Timeline, ParticleSystem, Shaders etc. or a more global setting to set the FPS limit to Application.targetFramerate with VSync on?

Thanks for all the hard work. Thanks for taking the time to detail the issue and solution. Looking forward to the fix.

Good that it’s fixed, but with Unity 2020b5 I’ve run into bug – Time.deltaTime cannot be larger than 0.1f for some reason. So if my framerate lower than 10 fps (and I’m trying to create benchmark were such low framerates a common thing on low-end hardware) it’s not working (.

This improvement is really welcoming.
Unfortunately, I am one of people who spent good amount of time trying to figure it out why my time delta is so not consistent.
I am not angry that it took so long, what is weird is the fact that the issue was just recently acknowledged as problem with the engine.
I have seen many topis talking about it and the most common outcome was: “Your code is wrong, it’s not Unity’s fault”.
The main issue I see it took 10 years to acknowledge there is an issue in the first place.
How many things like that cost developers time because the only answer on internet they can find is: “It’s developers problem, not Unity issue”.
I hope we won’t see anything else like this going that path.

Nice work, sometimes it’s amazing how you can still find & fix issues like these that have been around for 10 years (and using ‘standard’ methods that have been around for ages). In my engine I found the same issue and tried fixed timesteps (assuming you’d have no dropframes, which is ofcourse not really realistic). Unfortunately my whole timing structure was based on integer milliseconds and doing 60Hz meant alternatively jumping 16 and 17 ms at times. :-) Similar issues will come up anyway by the 1ms difference, although subtle.

The depth of this article!! Thanks so much for breaking this down and for doing the work to make delta time smoother. Please more blog posts like this :)

This is very very nice!

The following 2 questions come to my mind:

1) At the beginning of the post its mentioned that several assumptions are made (Graphics jobs disabled, etc.),
does this means the fix only works under those settings or was it mentioned just to keep the same game loop structure across the examples?

2) Does the fix works regardless if we are using URP or Built-in Renderer?

What a nice, in-depth article. I for one really appreciate the time that went into both implementing these improvements and writing such a detailed report.

Nice work, now if you could just figure out how to break the rendering buffer up into layers so user input could jump to the next rendering frame set we could almost eradicate input latency or at least get it down to nearly next frame timings. e.g. Player shoots and a muzzle flash is displayed in the next frame set to render.

This is actually what the VR headset drivers do. They implement a technique called late latch. The VR headset orientation is updated to GPU memory after the GPU command buffer has been recorded. Just before the GPU renders that queued frame. This is similar to the technique used by the hardware mouse cursor.

This oculus blog post describes the technique in more detail:
https://developer.oculus.com/blog/optimizing-vr-graphics-with-late-latching/

This technique works well with hardware devices that you can poll at precise time intervals (modern mouse can be polled 1000 times per second). However the GPU can’t poll the CPU to give it an exact game state at this exact moment. It takes significant time for the CPU to calculate one frame. Thus we must start calculating it in advance. But there’s no guarantee that a full CPU frame is ready when the GPU would need to late update the data. It might be ready every other time and miss the deadline every other frame. This would cause massive juddering to the animation. The CPU and the GPU are running asynchronously for a reason (both can run at the same time without waiting). If we add wait for late latch data, we lose this asynchronous execution. It’s not an easy problem to solve in generic case. High frequency hardware sensors such as mouse and VR headset can be made to work acceptably well, but even in these cases, if you do sudden large movements, you might turn the camera too much and see missing objects on screen edges. Visibility culling algorithm generated draw calls 1-2 frames ago. Just updating the camera matrix isn’t enough.

Sad that it took you 10+ years to finally look into it. Imagine now many users suffered due to this over those years.
But hey! At least you have something to write about in the blog!

“What is delta time and why is it important?”
A great headline for a blog post from an engine team.
Quite well reflects how unity perceives it’s users.

Is this response really necessary?
Have you addressed every possible issue you are aware of in your own projects?
Did Unity work on any other features other than this issue?
Is it possible there were other priorities that superseded this work?
Have any successful games been shipped with Unity with the delta time issue?
Do you evaluate your own work with the same level of negativity?

When commenting ask yourself what your intended outcome is. If the answer is to ultimately to hurt someone, just don’t. Please.

Yes, this new approach hasn’t been in the engine for past 10 years till now. But fun fact: it hasn’t been in most competing game engines out there either, at least not in the ones you can get access to. What Unity has done in past has been pretty standard way of doing it. So you are essentially blaming Unity here for doing something in past that most engines have done and still do for the delta time measurement.

Now that they made it better, Unity actually has edge over competing engines in this regard, yet you manage only to leave a negative comment? I guess some people are never happy. I know I’m thrilled about this change :)

I’d be thrilled about this change if I hadn’t been using hacks to get around the deltatime issues for seven years now. The simple reality is that not addressing known issues in the engine for years, up to a decade in time, is the standard for Unity and it’s what’s caused a lot of people, myself included, to give up on the engine entirely.

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です