最終更新:2020 年 3 月(読み終わるまでの時間:15 分)

Unity のパフォーマンスの最適化に関するベストプラクティス

このページで学ぶ内容:データ指向設計に対応するよう進化した Unity のアーキテクチャを受けて更新されたスクリプティングパフォーマンスと最適化に関するヒントを Ian Dundore が紹介します。 

進化しつづける Unity。これまでのやり方は、今やゲームエンジンのパフォーマンスをフルに発揮するには最適なものではなくなっているかもしれません。この記事では、Unity(Unity 5.x から Unity 2019.x まで)の変更点の概要と、それらをどう利用できるかという点について説明します。 

Script order diagram in Unity PlayerLoop

Get to know the PlayerLoop and the lifecycle of a script.

Generic リグと Humanoid リグの比較

デフォルトでは、Unity はアニメーション化されたモデルを Generic リグでインポートしますが、開発者がキャラクターをアニメーション化するときは Humanoid リグに切り替えることがよくあります。ただし、Humanoid リグの使用にはコストがかかります。

Humanoid リグは、アニメーターシステムにインバースキネマティクス(IK)とアニメーションリターゲティング(異なるアバター間でアニメーションを再利用できる)という、 2 つの機能をもたらしてくれます。

ただし、IK やアニメーションリターゲティングを使用していない場合でも、Humanoid リグが設定されたキャラクターのアニメーターは各フレームで IK とリターゲティングのデータを計算します。これは、こういった計算を行わない Generic リグと比較すると約 30% から 50% 多くの CPU 時間を消費します。

Humanoid リグ固有の機能を利用する必要がないのであれば、Generic リグを使用するようにしましょう。

アニメーターのリバインド

ゲームプレイ中のパフォーマンスのスパイクを防ぐため、オブジェクトプールの実行をしておくことは対策として欠かせません。とはいえ、これまでアニメーターはオブジェクトプールの実行が困難でした。アニメーターのゲームオブジェクトが有効になっている場合は、常にアニメーターがアニメーション化している、そのプロパティのメモリアドレスへのポインターの一覧を構築していなければなりません。これはつまり、あるコンポーネントの特定のフィールドに対してヒエラルキーを照会することを意味し、これによりコストが高くなる可能性があるのです。このプロセスはアニメーターのリバインドと呼ばれ、Unity プロファイラーに Animator.Rebind として表示されます。

アニメーターのリバインドは、あらゆるシーンで不可避であり一度は必ず行わなければならないものです。これには、そのアニメーターがアタッチされているすべての子を再帰的に走査すること、その対象の名前のハッシュを取得すること、さらにそのハッシュそれぞれを各アニメーションカーブのターゲットパスのハッシュと比較することが伴います。そのため、ヒエラルキーがアニメーション化していない子の存在により、バインディングプロセスに追加コストがかかります。アニメーション化をしていない莫大な数の子が存在しているゲームオブジェクト上のアニメーターを避けておけば、それがリバインドのパフォーマンス改善を後押ししてくれます。

MonoBehaviour のリバインドは、Transform などの組み込みのクラスのリバインドよりもコストがかかります。Animator コンポーネントは MonoBehaviour 上のフィールドをスキャンして、それらのフィールド名のハッシュによってインデックスが付けられたソート済みの一覧を作成します。その後、その MonoBehaviour のフィールドをアニメーション化している各アニメーションカーブについて、そのソート済みの一覧でバイナリ検索が実行されます。そのため、普段からアニメーション化している MonoBehaviour 内のフィールドを簡潔に保ち、大規模にネストされたシリアライズ可能な構造を回避しておけば、リバインドにかかる時間がさらに減ることになります。

不可避である最初のリバインド後に、ゲームオブジェクトをプールするようにもしておきましょう。ゲームオブジェクト全体を有効化/無効化する代わりに、これで Animator コンポーネントを有効化/無効化してリバインドを回避することができます。

Custom Update Manager diagram

Building a custom Update Manager reduces interop calls.

Build a custom Update Manager

If your project has demanding performance requirements (e.g., an open-world game), consider creating a custom Update Manager using Update, LateUpdate, or FixedUpdate.

A common usage pattern for Update or LateUpdate is to run logic only when some condition is met. This can lead to a number of per-frame callbacks that effectively run no code except for checking this condition.

Whenever Unity calls a message method like Update or LateUpdate, it makes an interop call – meaning, a call from the C/C++ side to the managed C# side. For a small number of objects, this is not an issue. When you have thousands of objects, this overhead starts becoming significant.

Subscribe active objects to this Update Manager when they need callbacks, and unsubscribe when they don’t. This pattern can reduce many of the interop calls to your Monobehaviour objects.

Refer to the Game engine-specific optimization techniques for examples of implementation.

Minimize code that runs every frame

Consider whether code must run every frame. You can move unnecessary logic out of Update, LateUpdate, and FixedUpdate. These Unity event functions are convenient places to put code that must update every frame, but you can extract any logic that does not need to update with that frequency.

Only execute logic when things change. Remember to leverage techniques such as the observer pattern in the form of events to trigger a specific function signature.

If you need to use Update, you might run the code every n frames. This is one way to apply Time Slicing, a common technique of distributing a heavy workload across multiple frames.

In this example, we run the ExampleExpensiveFunction once every three frames.

The trick is to interleave this with other work that runs on the other frames. In this example, you could “schedule” other expensive functions when Time.frameCount % interval == 1 or Time.frameCount % interval == 2.

Alternatively, use a custom Update Manager class to update the subscribed objects every n frames.

Cache the results of expensive functions

In Unity versions prior to 2020.2, GameObject.Find, GameObject.GetComponent, and Camera.main can be expensive, so it’s best to avoid calling them in Update methods. 

Additionally, try to avoid placing expensive methods in OnEnable and OnDisable if they are called often. Frequently calling these methods can contribute to CPU spikes. 

Wherever possible, run expensive functions, such as MonoBehaviour.Awake and MonoBehaviour.Start during the initialization phase. Cache the needed references and reuse them later. Check out our earlier section on the Unity PlayerLoop for the script order execution in more detail.

Here’s an example that demonstrates inefficient use of a repeated GetComponent call:

void Update()
{
    Renderer myRenderer = GetComponent<Renderer>();
    ExampleFunction(myRenderer);
}

Instead, invoke GetComponent only once as the result of the function is cached. The cached result can be reused in Update without any further calls to GetComponent.

Read more about the Order of execution for event functions.

Script compilation with custom preprocessor

Adding a custom preprocessor directive lets you partition your scripts.

Avoid empty Unity events and debug log statements

Log statements (especially in Update, LateUpdate, or FixedUpdate) can bog down performance, so disable your log statements before making a build. To do this quickly, consider making a Conditional Attribute along with a preprocessing directive.

For example, you might want to create a custom class as shown below.

Generate your log message with your custom class. If you disable the ENABLE_LOG preprocessor in the Player Settings > Scripting Define Symbols, all of your log statements disappear in one fell swoop.

Handling strings and text is a common source of performance problems in Unity projects. That’s why removing log statements and their expensive string formatting can potentially be a big performance win.

Similarly, empty MonoBehaviours require resources, so you should remove blank Update or LateUpdate methods. Use preprocessor directives if you are employing these methods for testing:

#if UNITY_EDITOR
void Update()
{
}
#endif

Here, you can use the Update in-Editor for testing without unnecessary overhead slipping into your build.

This blog post on 10,000 Update calls explains how Unity executes the Monobehaviour.Update.

Stack Trace options interface

Stack Trace options

Disable Stack Trace logging

Use the Stack Trace options in the Player Settings to control what type of log messages appear. If your application is logging errors or warning messages in your release build (e.g., to generate crash reports in the wild), disable Stack Traces to improve performance.

Learn more about Stack Trace logging.

Use hash values instead of string parameters

Unity does not use string names to address Animator, Material, or Shader properties internally. For speed, all property names are hashed into Property IDs, and these IDs are used to address the properties.

When using a Set or Get method on an Animator, Material, or Shader, leverage the integer-valued method instead of the string-valued methods. The string-valued methods perform string hashing and then forward the hashed ID to the integer-valued methods.

Use Animator.StringToHash for Animator property names and Shader.PropertyToID for Material and Shader property names.

Related is the choice of data structure, which impacts performance as you iterate thousands of times per frame. Follow the MSDN guide to data structures in C# as a general guide for choosing the right structure.

Object Pool script interface

In this example, the Object Pool creates 20 PlayerLaser instances for reuse.

Pool your objects

Instantiate and Destroy can generate garbage collection (GC) spikes. This is generally a slow process, so rather than regularly instantiating and destroying GameObjects (e.g., shooting bullets from a gun), use pools of preallocated objects that can be reused and recycled. 

Create the reusable instances at a point in the game, like during a menu screen or a loading screen, when a CPU spike is less noticeable. Track this “pool” of objects with a collection. During gameplay, simply enable the next available instance when needed, and disable objects instead of destroying them, before returning them to the pool. This reduces the number of managed allocations in your project and can prevent GC problems.

Similarly, avoid adding components at runtime; Invoking AddComponent comes with some cost. Unity must check for duplicates or other required components whenever adding components at runtime. Instantiating a Prefab with the desired components already set up is more performant, so use this in combination with your Object Pool.

Related, when moving Transforms, use Transform.SetPositionAndRotation to update both the position and rotation at once. This avoids the overhead of modifying a Transform twice.

If you need to instantiate a GameObject at runtime, parent and reposition it for optimization, see below.

For more on Object.Instantiate, see the Scripting API.

Learn how to create a simple Object Pooling system in Unity here.

Scriptable object pool

In this example, a ScriptableObject called Inventory holds settings for various GameObjects.

Harness the power of ScriptableObjects

Store unchanging values or settings in a ScriptableObject instead of a MonoBehaviour. The ScriptableObject is an asset that lives inside of the project. It only needs to be set up once, and cannot be directly attached to a GameObject.

Create fields in the ScriptableObject to store your values or settings, then reference the ScriptableObject in your MonoBehaviours. Using fields from the ScriptableObject can prevent the unnecessary duplication of data every time you instantiate an object with that MonoBehaviour.

Watch this Introduction to ScriptableObjects tutorial and find relevant documentation here.

Get the free e-book

One of our most comprehensive guides ever collects over 80 actionable tips on how to optimize your games for PC and console. Created by our expert Success and Accelerate Solutions engineers, these in-depth tips will help you get the most out of Unity and boost your game’s performance.

弊社のウェブサイトは最善のユーザー体験をお届けするためにクッキーを使用しています。詳細については、クッキーポリシーのページをご覧ください。

OK