Unityで60FPSを維持しつつ大量のメインスレッド処理を行いたい時の小技
要約
Unityで大量のメインスレッド処理を行いたいとき、1フレームに掛かった時間を計算して、1/60秒を超えないように非同期処理すると60FPSを維持しやすくなります。
When you want to perform a large amount of processing on the main thread in Unity, calculating the time taken per frame and performing asynchronous processing to avoid exceeding 1/60 second can make it easier to keep 60FPS.
Unityのメインスレッド処理について
UnityではBurst、DOTS、ECS、マルチスレッド処理、非同期処理、プーリングなど様々な高速化手法がありますが、InstantiateなどのUnityのAPIはメインスレッドでしか行えないため、高速化しようと思っても限度があります。
とはいえ、やむを得ない事情により大量のメインスレッド処理を行いたいときはあります。素直に全て同期処理してしまうと重すぎてカクついてしまうし、非同期で行うにしても、どのぐらいの量で60FPSを切ってしまうのかが分からず加減が大変です。
ということで、60FPSを維持できるギリギリまでコルーチンで非同期処理を行うコードを書きました。
コード
FixedUpdateとUpdateでフレーム開始時の時間を取得し、yield return new WaitForEndOfFrame();
によってフレーム終了時の時間を取得しています。
その差分が1フレームの表示にかかった時間なので、1フレームの16.66ms(1000ms/60)を超えないように残り時間で処理を行っています。
FixedUpdateとUpdateの時間を両方とも確認しているのは、フレームレートによってはFixedUpdateがUpdateより前に実行されることがあるためです。詳しくはUnity公式のイベント実行順をご覧ください。
実行結果
では、次のような様々なタイミングで行われる重い処理を実行しながら60FPS前後に収まるか試してみます。
using System.Collections;
using System.Threading;
using UnityEngine;
public class HeavyTasks : MonoBehaviour
{
void HeavyTask() => Thread.Sleep(Random.Range(1, 3));
void Update() => HeavyTask();
void LateUpdate() => HeavyTask();
void FixedUpdate() => HeavyTask();
IEnumerator Start()
{
while (true) {
HeavyTask();
yield return null;
}
}
}
重い処理を実行中のFPS
重い処理と60FPSを目指す処理を実行中のFPS
ベースとなる重い処理を実行している時は200〜100FPS付近になっていますが、60FPSを目指す処理を追加したあとは想定通り60FPS前後で処理を行うようになっています。ただし、Editorなどの制御できない処理が時々入るので、ある程度は時間にマージンが必要になります。使う場合、コード内のTimeMarginを変更して60FPSを切らないよう調整してみてください。
まとめ
この記事では、Unityで大量のメインスレッド処理を行いつつ、60FPSを維持しやすくするための非同期処理の方法を紹介しました。フレームごとの処理時間を計算し、1フレームの16.66msを超えないようにコルーチンを用いて非同期処理を実行することで、60FPSを維持しやすくなります。
しかし、この方法はあくまで実行タイミングをずらしているだけであり、真の意味での最適化ではありません。他に最適化手法がある場合は、それらを積極的に採用することが望ましいです。ですが、困った時の小技としては使えるかと思います。
変更履歴
2023/3/19 記事公開
2023/3/20 FixedUpdateを使用して計測精度を向上
Discussion