Search icon
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Unity Game Optimization - Third Edition

You're reading from  Unity Game Optimization - Third Edition

Product type Book
Published in Nov 2019
Publisher Packt
ISBN-13 9781838556518
Pages 404 pages
Edition 3rd Edition
Languages
Authors (2):
Dr. Davide Aversa Dr. Davide Aversa
Profile icon Dr. Davide Aversa
Chris Dickinson Chris Dickinson
Profile icon Chris Dickinson
View More author details

Table of Contents (15) Chapters

Preface 1. Section 1: Base Scripting Optimization
2. Evaluating Performance Problems 3. Scripting Strategies 4. Section 2: Graphical Optimizations
5. The Benefits of Batching 6. Optimizing Your Art Assets 7. Faster Physics 8. Dynamic Graphics 9. Section 3: Advance Optimizations
10. Optimizations for Virtual and Augmented Reality 11. Masterful Memory Management 12. The Data-Oriented Technology Stack 13. Tactical Tips and Tricks 14. Other Books You May Enjoy

Scripting Strategies

Since scripting will consume a great deal of our development time, it will be enormously beneficial to learn some best practices. Scripting is a very broad term, so we will try to limit our exposure in this chapter to situations that are very Unity-specific, focusing on problems surrounding MonoBehaviours, GameObjects, and related functionality.

We will discuss the nuances and advanced topics of the C# language, .NET library, and Mono framework in Chapter 8, Masterful Memory Management.

In this chapter, we will explore ways of applying performance enhancements for the following:

  • Obtaining components in other game objects
  • Optimizing component callbacks (Update(), Awake(), and so on)
  • Using coroutines
  • Using GameObject and Transform efficiently
  • Exchanging messages between different objects
  • Optimizing mathematical calculations
  • Serializing/deserializing during...

Obtaining components using the fastest method

There are several variations of the GetComponent() method, and they each have a different performance cost, so it is prudent to call the fastest possible version of this method. The three overloads available are GetComponent(string), GetComponent<T>(), and GetComponent(typeof(T)). It turns out that the fastest version depends on which version of Unity we are running since several optimizations have been made to these methods through the years; however, if you are using any version of Unity (from Unity 2017 onward), it is best to use the GetComponent<T>() variant.

Let's prove this with some simple testing:

int numTests = 1000000;
TestComponent test;
using (new CustomTimer("GetComponent(string)", numTests)) {
for (var i = 0; i < numTests; ++i) {
test = (TestComponent)GetComponent("TestComponent"...

Removing empty callback definitions

The primary means of scripting in Unity is to write callback functions in classes derived from MonoBehaviour, which we know Unity will call when necessary. Perhaps the four most commonly used callbacks are Awake(), Start(), Update(), and FixedUpdate().

Awake() is called the moment MonoBehaviour is first created, whether this occurs during scene initialization or when a new GameObject instance containing the MonoBehaviour component is instantiated at runtime from a Prefab. Start() will be called shortly after Awake() but before its first Update(). During scene initialization, every MonoBehaviour component's Awake() callback will be called before any of their Start() callbacks are.

After this, Update() will be called repeatedly, each time the rendering pipeline presents a new image. Update() will continue to be called provided MonoBehaviour...

Caching component references

Repeatedly recalculating a value is a common mistake when scripting in Unity, and particularly when it comes to the GetComponent() method. For example, the following script code is trying to check a creature's health value, and if its health goes below 0, it will disable a series of components to prepare it for a death animation:

void TakeDamage() {

Rigidbody rigidbody = GetComponent<Rigidbody>();
Collider collider = GetComponent<Collider>();
AIControllerComponent ai = GetComponent<AIControllerComponent>();
Animator anim = GetComponent<Animator>();

if (GetComponent<HealthComponent>().health < 0) {
rigidbody.enabled = false;
collider.enabled = false;
ai.enabled = false;
anim.SetTrigger("death");
}
}

Each time this poorly optimized method executes, it will reacquire five different component...

Sharing calculation output

Performance can be saved by having multiple objects share the result of some calculation; of course, this only works if all of them generate the same result. Such situations are often easy to spot but can be tricky to refactor, and so exploiting this would be very implementation-dependent.

Some examples might include finding an object in a scene, reading data from a file, parsing data (such as XML or JSON), finding something in a big list or deep dictionary of information, calculating pathing for a group of Artificial Intelligence (AI) objects, complex mathematics-like trajectories, raycasting, and so on.

Think about each time an expensive operation is undertaken, and consider whether it is being called from multiple locations but always results in the same output. If this is the case, then it would be wise to restructure things so that the result is...

Update, coroutines, and InvokeRepeating

Another habit that's easy to fall into is to call something repeatedly in an Update() callback way more often than is needed. For example, we may start with a situation like this:

void Update() {
ProcessAI();
}

In this case, we're calling some custom ProcessAI() subroutine every single frame. This may be a complex task, requiring the AI system to check some grid system to figure out where it's meant to move or determine some fleet maneuvers for a group of spaceships or whatever our game needs for its AI.

If this activity is eating into our frame rate budget too much, and the task can be completed less frequently than every frame with no significant drawbacks, then a good trick to improve performance is to simply reduce the frequency at which that ProcessAI() gets called:

private float _aiProcessDelay = 0.2f;
private float _timer...

Faster GameObject null reference checks

It turns out that performing a null reference check against a GameObject will result in some unnecessary performance overhead. GameObjects and MonoBehaviours are special objects compared to a typical C# object, in that they have two representations in memory: one exists within the memory managed by the same system managing the C# code we write (managed code), whereas the other exists in a different memory space, which is handled separately (native code). Data can move between these two memory spaces, but each time this happens will result in some additional CPU overhead and possibly an extra memory allocation.

This effect is commonly referred to as crossing the Native-Managed Bridge. If this happens, it is likely to generate an additional memory allocation for an object's data to get copied across the bridge, which will require the...

Avoid retrieving string properties from GameObjects

Ordinarily, retrieving a string property from an object is the same as retrieving any other reference type property in C#; it should be acquired with no additional memory cost. However, retrieving string properties from GameObjects is another subtle way of accidentally crossing over the Native-Managed Bridge.

The two properties of GameObject affected by this behavior are tag and name. Therefore, it is unwise to use either property during gameplay, and you should only use them in performance-inconsequential areas, such as editor scripts. However, the tag system is commonly used for the runtime identification of objects, which can make this a significant problem for some teams.

For example, the following code would cause an additional memory allocation during every iteration of the loop:

for (int i = 0; i < listOfObjects.Count...

Using appropriate data structures

C# offers many different data structures in the System.Collections namespace and we shouldn't become too accustomed to using the same ones over and over again. A common performance problem in software development is making use of an inappropriate data structure for the problem we're trying to solve simply because it's convenient. The two most commonly used are perhaps lists (List<T>) and dictionaries (Dictionary<K,V>).

If we want to iterate through a set of objects, then a list is preferred, since it is effectively a dynamic array where the objects and/or references reside next to one another in memory, and therefore iteration causes minimal cache misses. Dictionaries are best used if two objects are associated with one another and we wish to acquire, insert, or remove these associations quickly. For example, we might...

Avoiding re-parenting transforms at runtime

In earlier versions of Unity (version 5.3 and older), the references to Transform components would be laid out in memory in a generally random order. This meant that iteration over multiple Transform components was fairly slow due to the likelihood of cache misses. The upside was that re-parenting GameObject to another one wouldn't really cause a significant performance hit since the Transforms operated a lot like a heap data structure, which tend to be relatively fast at insertion and deletion. This behavior wasn't something we could control, and so we simply lived with it.

However, since Unity 5.4 and beyond, the Transform component's memory layout has changed significantly. Since then, a Transform component's parent-child relationships have operated more like dynamic arrays, whereby Unity attempts to store all...

Considering caching transform changes

The Transform component stores data only relative to its own parent. This means that accessing and modifying a Transform component's position, rotation, and/or scale properties could potentially result in a lot of unanticipated matrix multiplication calculations to generate the correct Transform representation for the object through its parent Transforms. The deeper the object is in the Hierarchy window, the more calculations are needed to determine the final result.

However, this also means that using localPosition, localRotation, and localScale has a relatively trivial cost associated with it since these are the values stored directly in the given Transform component and they can be retrieved without any additional matrix multiplication. Therefore, these local property values should be used whenever possible.

Unfortunately, changing...

Avoiding Find() and SendMessage() at runtime

The SendMessage() method and family of GameObject.Find() methods are notoriously expensive and should be avoided at all costs. The SendMessage() method is about 2,000 times slower than a simple function call, and the cost of the Find() method scales very poorly with scene complexity since it must iterate through every GameObject in the scene. It is sometimes forgivable to call Find() during the initialization of a scene, such as during an Awake() or Start() callback. Even in this case, it should only be used to acquire objects that we know for certain already exist in the scene and for scenes that have only a handful of GameObjects in them. Regardless, using either of these methods for inter-object communication at runtime is likely to generate a very noticeable overhead and, potentially, dropped frames.

Relying on Find() and SendMessage...

Disabling unused scripts and objects

Scenes can get pretty busy sometimes, especially when we're building large, open worlds. The more objects there are invoking code in an Update() callback, the worse it will scale and the slower the game becomes. However, much of what is being processed may be completely unnecessary if it is outside of the player's view or simply too far away to matter. This may not be a possibility in large city-building simulation games, where the entire simulation must be processed at all times, but it is often possible in first-person and racing games since the player is wandering around a large expansive area, where non-visible objects can be temporarily disabled without having any noticeable effect on gameplay.

Disabling objects by visibility

...

Using distance-squared over distance

It is safe to say that CPUs are relatively good at multiplying floating-point numbers together, but relatively dreadful at calculating square roots from them. Every time we ask Vector3 to calculate a distance with the magnitude property or with the Distance() method, we're asking it to perform a square root calculation (as per Pythagorean theorem), which can cost a lot of CPU overhead compared to many other types of vector math calculations.

However, the Vector3 class also offers a sqrMagnitude property, which provides the same result as distance, only the value is squared. This means that if we also square the value we wish to compare distance against, then we can perform essentially the same comparison without the cost of an expensive square-root calculation.

For example, consider the following code:

float distance = (transform.position...

Minimizing deserialization behavior

Unity's serialization system is mainly used for scenes, Prefabs, ScriptableObjects, and various asset types (which tend to derive from ScriptableObject). When one of these object types is saved to disk, it is converted into a text file using the Yet Another Markup Language (YAML) format, which can be deserialized back into the original object type at a later time. All GameObjects and their properties get serialized when a Prefab or scene is serialized, including private and protected fields and all of their components, as well as child GameObjects and their components and so on.

When our application is built, this serialized data is bundled together in large binary data files internally called serialized files in Unity. Reading and deserializing this data from disk at runtime is an incredibly slow process (relatively speaking) and so all...

Loading scenes additively and asynchronously

Scenes can be loaded either to replace the current scene or can be loaded additively to add its contents to the current scene without unloading the preceding one. This can be toggled via the LoadSceneMode argument of the SceneManager.LoadScene() family of functions.

Another mode of scene loading is to complete it either synchronously or asynchronously, and there are good reasons to use both. Synchronous loading is the typical means of loading a scene by calling SceneManager.LoadScene(), where the main thread will block until the given scene completes loading. This normally results in poor user experience, as the game appears to freeze as the contents are loaded in (whether as a replacement or additively). This is best used if we want to get the player into the action as soon as possible, or we have no time to wait for scene objects...

Creating a custom Update() layer

Earlier in this chapter, in the Update, coroutines, and InvokeRepeating section, we discussed the relative pros and cons of using these Unity Engine features as a means of avoiding excessive CPU workload during most of our frames. Regardless of which of these approaches we might adopt, there is an additional risk of having lots of MonoBehaviours written to periodically call some function, which is having too many methods triggering in the same frame simultaneously.

Imagine thousands of MonoBehaviours that initialized together at the start of a scene, each starting a coroutine at the same time that will process their AI tasks every 500 milliseconds. It is highly likely that they would all trigger within the same frame, causing a huge spike in its CPU usage for a moment, which settles down temporarily and then spikes again a few moments later when...

Summary

This chapter introduced you to many methods that will improve your scripting practices in the Unity Engine and to improve performance if (and only if) you have already proven some scripts are the cause of a performance problem. Some of these techniques demand some forethought and profiling investigation before being implemented since they often come with introducing additional risks or obfuscating our code base. Workflow is often just as important as performance and design, so before you make any performance changes to the code, you should consider whether or not you're sacrificing too much on the altar of performance optimization.

We will investigate more advanced scripting improvement techniques later, in Chapter 8, Masterful Memory Management, but let's take a break from staring at code and explore some ways to improve graphics performance using a pair of...

lock icon The rest of the chapter is locked
You have been reading a chapter from
Unity Game Optimization - Third Edition
Published in: Nov 2019 Publisher: Packt ISBN-13: 9781838556518
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime}