Unity* Optimization Guide for Intel x86 Platforms: Part 2

Contents

Go Back to Part 1 of the Tutorial:
Unity* Optimization Guide for Intel x86 Platforms: Part 1


Optimizations

We will explore two main areas of optimizations in this guide: Scripting and Editor-based. You will see a number of specifics for each optimization based on the roots in which it functions. 

Scripting Optimizations

Script Frustum Culling and Co-routines

When you are profiling your application and see that a script’s Update() function does not need to be called every frame, you have a few great methods to reduce the amount of updates:

  • Script Frustum Culling
    • Use the following Monobehavior callbacks to cull scripts outside of the camera frustum that do not need to update when not in focus.
      Monobehavior callbacks which trigger when object with script leaves / enters the camera frustum
      Figure 12. Monobehavior callbacks which trigger when object with script leaves / enters the camera frustum
  • Co-routines
    • Co-routines are essentially functions with the ability to pause and resume execution. The power of co-routines can be leveraged by removing the original Update() function in your script and replacing it with a co-routine. You can then set how often you would like your co-routine to execute using the yield command. This snippet shows how to create a custom smart update that is called every 2 seconds, rather than the default setting of once per frame:
      More efficient update using co-routines
      Figure 13. More efficient update using co-routines

Smart Memory Management

When looking for ways to optimize your memory usage, it is helpful to check the Unity profiler first. A great way to get an overview of how you are managing memory is to check the ‘GC Alloc’ section of the Overview window (Figure 14) and step through your frames until you see a significant allocation.

By inspecting many frames in a row, you can determine when GC will occur and adjustFigure 14. By inspecting many frames in a row, you can determine when GC will occur and adjust

It is also helpful to check how frequently garbage collection is being invoked. To see this, isolate the GarbageCollector field in the ‘CPU Usage’ sub-profiler (Figure 15):

Point identified in CPU usage when GC occurs
Figure 15. Point identified in CPU usage when GC occurs

When collects are displayed, you can then click on a peak in the graph and look for the call to GC.Collect (Figure 16). By doing this you can see how much time each collect takes:

Statistics on GC
Figure 16. Statistics on GC

To avoid frequent allocations, it is advantageous to use structs instead of classes to have allocations be done on the stack, instead of in the heap. Multiple allocations to the heap can lead to significant memory fragmentation and frequent garbage collections.

Caching Frequent Objects and Components

As a rule of thumb, you should analyze your apps to find the most frequently used GameObjects and Components and make sure that these values are being cached. Any time you see an object being fetched every scene is an opportunity for caching and saving unnecessary computation.

The same rule applies to GameObject instantiation. In general, instantiation is a relatively slow call that should be avoided. If creating and destroying the same object types repeatedly in every scene, it is advantageous to maintain a list of those objects to be reused in an object manager script.

Unity recommends a central game manager to maintain lists of all of your cached game objects. After implementing this technique, you can include the following code snippet to compare the performance delta between the two methods by toggling a state button, or other control mechanism while viewing the CPU Usage profiler in real time. Here is the snippet (Figure 14) showing the difference in usage:

Using STATE to toggle objects
Figure 17. Using STATE to toggle objects

Best Practices for Working with the Unity Physics System

When working with dynamic objects in Unity, there are a few well-known optimizations and pitfalls to avoid. Whether you plan to move the object yourself or allow Unity to take control of the physics of an object, add a Rigidbody component to your object. This tells the Unity physics system that the object is moveable. When you wish to move the object manually, simply check the isKinematic flag (Figure 18). You also want to make sure that the static checkbox at the top right corner of the inspector is unchecked for that object (Figure 19).

isKinematic checked to take control over objects movement
Figure 18. isKinematic checked to take control over objects movement

Static property unchecked to keep dynamic object out of static set
Figure 19. Static property unchecked to keep dynamic object out of static set

To make sure that you are handling dynamic objects properly in your app, open up the profiler, isolate the physics subsection of the CPU Profiler, highlight a frame that lands on the physics time step (24 updates per second by default), and verify that you do not see any “Static Collider.Move (Expensive delayed cost)” entries (Figure 20) in the overview window under the object’s FixedUpdate() call. The lack of a Static Collider.Move message indicates the physics in this section is working appropriately.

Static Collider.Move (Expensive delayed cost) appears when you are not managing dynamic objects properly
Figure 20. Static Collider.Move (Expensive delayed cost) appears when you are not managing dynamic objects properly

Disable Fully Transparent Objects

For objects using a material with a ‘Fade Out’ rendering mode, or really anything that ever becomes fully transparent, it is important to make sure that you set the MeshRenderer component for that object to disabled once the object is fully transparent.  These objects will always be dispatched in a draw call regardless of the alpha value.  For example, sometimes developers use full screen quads to paint on damage indicators or vignette effects that pulse in and out when an event triggers.  It’s important to know that the engine itself does not track when objects are fully transparent and resources will be wasted if care isn’t taken.  The following screenshots were taken in the same scene, with the only difference being the alpha value of the semi-transparent objects in the foreground.

Five semi-transparent planes with the corresponding GPA frame capture. Semi-transparent objects are visible and taking 37.4% of the scene at 2,657.5 μs.
Figure 21. Five semi-transparent planes with the corresponding GPA frame capture. Semi-transparent objects are visible and taking 37.4% of the scene at 2,657.5 μs.

The five previously mentioned planes have their material’s alpha value set to 0. The corresponding GPA frame capture shows that there are still draw commands occurring on the GPU.  These draws are taking 32.1% of the scene at 2,316.5 μs.
Figure 22. The five previously mentioned planes have their material’s alpha value set to 0. The corresponding GPA frame capture shows that there are still draw commands occurring on the GPU.  These draws are taking 32.1% of the scene at 2,316.5 μs.

To make sure that you aren’t dispatching unnecessary draw calls, always check the alpha value associated with your potentially transparent objects if possible.  For a simple material that renders based on color, just use a mechanism along the lines of the following code snippet.

When editing an objects transparency, always check that it is visible and disable it if necessary to save resources.
Figure 23. When editing an objects transparency, always check that it is visible and disable it if necessary to save resources.


Continue to Part 3 of the Tutorial:
Unity* Optimization Guide for Intel x86 Platforms: Part 3

For more complete information about compiler optimizations, see our Optimization Notice.