Unity を検索

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

2020年10月1日 カテゴリ: テクノロジー | 18 分 で読めます
Blog header image
Blog header image
取り上げているトピック
シェア

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

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

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

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

6.854 ms
7.423 ms
6.691 ms
6.707 ms
7.045 ms
7.346 ms
6.513 ms

これは 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 ms
tdeltaTime(6) = 1.45 + 2.81 + 1.48 + 0.5 + 0.4 = 6.64 ms
tdeltaTime(7) = 1.43 + 3.13 + 1.61 + 0.51 + 0.35 = 7.03 ms

この場合のデルタタイムの平均値((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 ms
tissueGPUCommands(5) + tpumpOSMessages(6) + tprocessInput(6) + tUpdate(6) + twait(6) = 1.48 + 0.5 + 0.4 + 1.43 + 3.13 = 6.94 ms
tissueGPUCommands(6) + tpumpOSMessages(7) + tprocessInput(7) + tUpdate(7) + twait(7) = 1.61 + 0.51 + 0.35 + 1.28 + 3.19 = 6.94 ms

ただし、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 が無効になっていたら、時間を手動で計算しなければならないケースが出てきます。

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

自分で時間をサンプリングする必要がなくなったため、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 を使用した場合に達成できる最低値です。

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

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

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

latency = tdisplay - tinput

この式を考えると、入力遅延を短縮するには 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 年のロードマップ計画についても記事を公開しました。こちらにもぜひ目をお通しください。

2020年10月1日 カテゴリ: テクノロジー | 18 分 で読めます
取り上げているトピック