Optimize Your Mobile Game Performance: Unity For Games Unity 2020 Lts Edition - E-Book
Optimize Your Mobile Game Performance: Unity For Games Unity 2020 Lts Edition - E-Book
Optimize Your
Mobile Game
Performance
Contents
Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Memory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Adaptive Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
Project configuration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Assets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
User interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Audio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Animation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Physics. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
Conclusion. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
Introduction
Optimizing your iOS and Android applications is an essential
process that underpins the entire development cycle. Mobile
hardware continues to evolve, and a mobile game’s optimization –
along with its art, game design, audio, and monetization strategy
– plays a key role in shaping the player experience.
Both iOS and Android have active user bases in the billions.
If your mobile game is highly optimized, it has a better chance
at passing certification from platform-specific stores. To maximize
your opportunity for success at launch and beyond, your aim is
always twofold: building the slickest, most immersive experience
and making it performant on the widest range of handhelds.
Profiling
The Unity Profiler can help you detect the causes of
any lags or freezes during runtime or understand what’s
happening at a specific frame (point in time). Enable the
CPU and Memory tracks by default. You can monitor
additional Profiler Modules (such as Renderer, Audio,
Physics, etc.) if you have specific needs for your game
(e.g., physics-heavy or music-based gameplay).
When profiling your game, we recommend that you cover both spikes and the cost of
an average frame in your game. Understanding and optimizing expensive operations
that occur each frame can be more useful for applications running below the target
framerate. When looking for spikes, explore expensive operations first (e.g., physics,
AI, animation) and garbage collection.
Click in the window to analyze a specific frame. Next, use either Timeline or the
Hierarchy view:
— Timeline shows a visual breakdown of the specific frame’s timings. This allows
you to visualize how the activities relate to one another and across different
threads. Use this to determine if you are CPU-bound or GPU-bound.
Read a complete overview of the Unity Profiler here. Those new to profiling can
also watch this Introduction to Unity Profiling.
Before optimizing anything in your project, save the Profiler .data file. Implement
your changes and compare the saved .data before and after the modification.
Rely on this cycle to improve performance: profile, optimize, and compare.
Then, rinse and repeat.
Don’t guess or make assumptions about what is slowing your game’s performance.
Use the Unity Profiler and platform-specific tools to locate what, specifically, is
causing the lag.
While profiling in the Editor can give you a very rough idea of the relative
performance of different systems in your game, there’s no substitute for the
real thing. Profile a development build on target devices whenever possible.
Remember to profile and optimize for the lowest-spec device you plan to support.
The Unity Profiler alone cannot see into every part of the engine. Fortunately, iOS
and Android both include native tools to help you test performance:
This tool lets you aggregate multiple frames of Profiler data, then locate frames
of interest. Want to see what happens to the Profiler after you make a change to
your project? The Compare view allows you to load and diff two data sets, which
is vital for testing changes and showing improvements. The Profile Analyzer is
available via Unity’s Package Manager.
Each frame will have a time budget based on your target frames per second (fps).
Ideally, an application running at 30 fps will allow for approximately 33.33 ms per
frame (1000 ms / 30 fps). Likewise, a target of 60 fps leaves 16.66 ms per frame.
For mobile, however, you cannot use this time consistently because the device
will overheat and the OS will thermal throttle the CPU and GPU. We recommend
that you only use around 65% of the available time to allow cooldown between
frames. A typical frame budget will be approximately 22 ms per frame at 30 fps
and 11 ms per frame at 60 fps.
Devices can exceed this for short periods of time (e.g., for cutscenes or loading
sequences) but not for a prolonged duration.
The Profiler can tell you if your CPU is taking longer than your allotted frame
budget or if the culprit is your GPU.
Most mobile devices do not have active cooling like their desktop counterparts.
Physical heat levels can directly impact performance.
If the device is running hot, the Profiler may report poor performance, even
if it may not be cause for concern. Combat profiling overhead by profiling in
short bursts to keep the device cool and simulate real-world conditions.
There is a wide range of iOS and Android devices. Test your project on the
minimum device specifications that you want your application to support.
The garbage collector (GC) periodically identifies and deallocates unused heap
memory. While this runs automatically, the process of examining all the objects
in the heap can cause the game to stutter or run slowly.
Optimizing your memory usage means being conscious of when you allocate and
deallocate heap memory and minimizing the effect of garbage collection.
Click in the Tree Map view to trace a variable to the native object holding
on to memory. Here, you can identify common memory consumption issues,
like excessively large textures or duplicate assets.
Watch how you can use the Memory Profiler in Unity to improve memory usage.
You can also check out the official Memory Profiler documentation.
— Strings: In C#, strings are reference types, not value types. Reduce unnecessary
string creation or manipulation. Avoid parsing string-based data files such
as JSON and XML; store data in ScriptableObjects or formats such as
MessagePack or Protobuf instead. Use the StringBuilder class if you need
to build strings at runtime.
— Unity function calls: Be aware that some functions create heap allocations.
Cache references to arrays rather than allocating them in the middle of a loop.
Also, take advantage of certain functions that avoid generating garbage;
for example, use GameObject.CompareTag instead of manually comparing
a string with GameObject.tag (returning a new string creates garbage).
If you are certain that a garbage collection freeze won’t affect a specific point
in your game, you can trigger garbage collection with System.GC.Collect.
While you can use Adaptive Performance APIs to fine-tune your application,
this package also offers automatic modes. In these modes, Adaptive Performance
determines the game settings along several key metrics:
These four metrics dictate the state of the device, and Adaptive Performance
tweaks the adjusted settings to reduce the bottleneck. This is done by providing
an integer value, known as an Indexer, to describe the state of the device.
When profiling, you’ll see all of your project’s user code under the PlayerLoop
(with Editor components under the EditorLoop).
Your custom scripts, settings, and graphics can significantly impact how long each frame takes to calculate and render onscreen.
You can optimize your scripts with these tips and tricks.
When your first scene loads, these functions get called for each object:
— Awake
— OnEnable
— Start
Avoid expensive logic in these functions until your application renders its first
frame. Otherwise, you may encounter longer loading times than necessary.
Refer to the order of execution for event functions for details about the first
scene load.
Use preprocessor directives if you are using these methods for testing:
#if UNITY_EDITOR
void Update()
{
}
#endif
Here, you can freely use the Update in the Editor for testing without
unnecessary overhead slipping into your build.
Generate your log message with your custom class. If you disable the
ENABLE_LOG preprocessor in the Player Settings, all of your Log statements
disappear in one fell swoop.
Unity does not use string names to address Animator, Material, and Shader
properties internally. For speed, all property names are hashed into property
IDs, and these IDs are actually used to address the properties.
Invoking AddComponent at runtime comes with some cost. Unity must check for
duplicate or other required components whenever adding components at runtime.
void Update()
{
Renderer myRenderer = GetComponent<Renderer>();
ExampleFunction(myRenderer);
}
Instead, you can 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.
void Update()
{
ExampleFunction(myRenderer);
}
Instantiate and Destroy can generate garbage and garbage collection (GC)
spikes and is generally a slow process. Instead of instantiating and destroying
GameObjects frequently (e.g., shooting bullets from a gun), use pools of
preallocated objects which can be reused and recycled.
In this example, a ScriptableObject called Inventory holds settings for various GameObjects.
Using those fields from the ScriptableObject can prevent unnecessary duplication
of data every time you instantiate an object with that Monobehaviour.
Ensure your Accelerometer Frequency is disabled if you are not making use of it in your mobile game.
In the Player settings, disable Auto Graphics API for unsupported platforms
to prevent generating excessive shader variants. Disable Target Architectures
for older CPUs if your application is not supporting them.
If your game is not using physics, uncheck Auto Simulation and Auto Sync
Transforms. These will just slow down your application with no discernible benefit.
Mobile projects must balance frame rates against battery life and thermal
throttling. Instead of pushing the limits of your device at 60 fps, consider
running at 30 fps as a compromise. Unity defaults to 30 fps for mobile.
You can also adjust the frame rate dynamically during runtime with
Application.targetFrameRate. For example, you could even drop below
30 fps for slow or relatively static scenes and reserve higher fps settings
for gameplay.
See Optimizing the Hierarchy and this Unite talk for best practices
with Transforms.
GameObject.Instantiate(prefab, parent);
GameObject.Instantiate(prefab, parent, position, rotation);
For more details about Object.Instantiate, please see the Scripting API.
Mobile platforms won’t render half-frames. Even if you disable Vsync in the
Editor (Project Settings > Quality), Vsync is enabled at the hardware level.
If the GPU cannot refresh fast enough, the current frame will be held,
effectively reducing your fps.
Assets
The asset pipeline can dramatically impact your application’s performance.
An experienced technical artist can help your team define and enforce asset
formats, specifications, and import settings.
Don’t rely on default settings. Use the platform-specific override tab to optimize
assets such as textures and mesh geometry. Incorrect settings may yield larger
build sizes, longer build times, and poor memory usage. Consider using the
Presets feature to help customize baseline settings for a specific project to
ensure optimal settings.
See this guide to best practices for art assets for more detail or check
out this course about 3D Art Optimization for Mobile Applications on
Unity Learn.
Most of your memory will likely go to textures, so the import settings here are
critical. In general, follow these guidelines:
Proper texture import settings will help optimize your build size.
Consider these two examples using the same model and texture. The settings
on the left consume almost eight times the memory as those on the right,
without much benefit in visual quality.
Use Adaptive Scalable Texture Compression (ATSC) for both iOS and Android.
The vast majority of games in development target min-spec devices that
support ATSC compression.
— iOS games targeting A7 devices or lower (e.g., iPhone 5, 5S, etc.) – use
PVRTC
If the quality of compressed formats such as PVRTC and ETC isn’t sufficiently
high, and if ASTC is not fully supported on your target platform, try using 16-bit
textures instead of 32-bit textures.
See the manual for more information about recommended texture compression
format by platform.
Much like textures, meshes can consume excess memory if not imported
carefully. To minimize meshes’ memory consumption:
If you split your non-code assets (Models, Textures, Prefabs, Audio, and even
entire Scenes) into an AssetBundle, you can separate them as downloadable
content (DLC).
Then, use Addressables to create a smaller initial build for your mobile
application. Cloud Content Delivery lets you host and deliver your game
content to players as they progress through the game.
Click here to see how the Addressable Asset System can take the pain out of
asset management.
Graphics and .
GPU optimization
Each frame, Unity determines which objects must be rendered and then
creates draw calls. A draw call is a call to the graphics API to draw objects
(e.g., a triangle), while a batch is a group of draw calls to be executed together.
As your projects become more complex, you’ll need a pipeline that optimizes
the workload on your GPU. The Universal Render Pipeline (URP) currently
uses a single-pass forward renderer to bring high-quality graphics to your
mobile platform (deferred rendering will be available in future releases).
The same physically based Lighting and Materials from consoles and PCs
can also scale to your phone or tablet.
— Dynamic batching: For small meshes, Unity can group and transform
vertices on the CPU, then draw them all in one go. Note: Only use this if
you have enough low-poly meshes (less than 900 vertex attributes and
no more than 300 vertices). The dynamic batcher won’t batch larger
meshes than this, so enabling it will waste CPU time every frame looking
for small meshes to batch.
— SRP Batching: Enable the SRP Batcher in your Universal Render Pipeline
Asset under Advanced. This can speed up your CPU rendering times
significantly, depending on the Scene.
Disable shadows
Shadow casting can be disabled per MeshRenderer and light. Disable shadows
whenever possible to reduce draw calls.
You can also create fake shadows using a blurred texture applied to a simple
mesh or quad underneath your characters. Alternately, create blob shadows
with custom shaders.
Adjust the Lightmapping Settings (Windows > Rendering > Lighting Settings) and Lightmap size to limit memory usage.
Follow the manual guide and this article about optimizing lighting for help
getting started with Lightmapping in Unity.
For complex scenes with multiple lights, separate your objects using layers,
then confine each light’s influence to a specific culling mask.
Light Probes store baked lighting information about the empty space in your
Scene and provide high-quality lighting (both direct and indirect). They use
Spherical Harmonics, which calculate very quickly compared to dynamic lights.
Objects hidden behind other objects can potentially still render and cost
resources. Use Occlusion Culling to discard them.
While frustum culling outside the camera view is automatic, occlusion culling is
a baked process. Simply mark your objects as Static Occluders or Occludees,
then bake through the Window > Rendering > Occlusion Culling dialog.
Though not appropriate for every scene, culling can improve performance
in many cases.
Check out the Working with Occlusion Culling tutorial for more information.
Phones and tablets have become increasingly advanced, with newer devices
sporting very high resolutions.
Each camera incurs some overhead, whether it’s doing meaningful work or not.
Only use camera components necessary for rendering. On lower-end mobile
platforms, each camera can use up to 1 ms of CPU time.
Optimize SkinnedMeshRenderers
Rendering skinned meshes is expensive. Make sure that every object using
a SkinnedMeshRenderer requires it. If a GameObject only needs animation
some of the time, use the BakeMesh function to freeze the skinned mesh in
a static pose, and swap to a simpler MeshRenderer at runtime.
A Reflection Probe can create realistic reflections, but this can be very costly
in terms of batches. Use low-resolution cubemaps, culling masks, and texture
compression to improve runtime performance.
If you have one large Canvas with thousands of elements, updating a single UI
element forces the whole Canvas to update, potentially generating a CPU spike.
Ensure that all UI elements within each Canvas have the same Z value,
materials, and textures.
You may have UI elements that only appear sporadically in the game
(e.g., a health bar that appears only when a character takes damage).
If your invisible UI element is active, it might still be using draw calls.
Explicitly disable any invisible UI components and re-enable them as needed.
If you only need to turn off the Canvas’s visibility, disable the Canvas component
rather than GameObject. This can save rebuilding the meshes and vertices.
Remove the default GraphicRaycaster from the top Canvas in the hierarchy.
Instead add the GraphicRaycaster only to the individual elements that need
to interact (buttons, scroll rects, and so on).
Layout Groups update inefficiently, so use them sparingly. Avoid them entirely
if your content isn’t dynamic, and use anchors for proportional layouts instead.
Alternately, create custom code to disable the Layout Group components after
they set up the UI.
If you do need to use Layout Groups (Horizontal, Vertical, Grid) for your dynamic
elements, avoid nesting them to improve performance.
Large List and Grid views are expensive. If you need to create a large List or
Grid view (e.g., an inventory screen with hundreds of items), consider reusing
a smaller pool of UI elements rather than creating a UI element for every item.
Check out this sample GitHub project to see this in action.
Layering lots of UI elements (e.g., cards stacked in a card battle game) creates
overdraw. Customize your code to merge layered elements at runtime into
fewer elements and batches.
With mobile devices now using very different resolutions and screen sizes,
create alternate versions of the UI to provide the best experience per device.
Use the Device Simulator to preview the UI across a wide range of supported
devices. You can also create virtual devices in XCode and Android Studio.
If your pause screen or start screen covers everything else in the scene,
disable the camera rendering the 3D scene. Likewise, disable any background
Canvas elements hidden behind the top Canvas.
Leaving the Event or Render Camera field blank forces Unity to fill
in Camera.main, which is unnecessarily expensive.
When using World Space Render Mode, make sure to fill in the Event Camera.
— Small clips (< 200 kb) should Decompress on Load. This incurs CPU cost
and memory by decompressing a sound into raw 16-bit PCM audio data,
so it’s only desirable for short sounds.
When implementing a mute button, don’t simply set the volume to 0. You can
Destroy the AudioSource component to unload it from memory, provided the
player does not need to toggle this on and off very often.
By default, Unity imports animated models with the Generic Rig, but
developers often switch to the Humanoid Rig when animating a character.
A Humanoid Rig consumes 30–50% more CPU time than the equivalent
Generic Rig because it calculates inverse kinematics and animation
retargeting each frame, even when not in use. If you don’t need these
specific features of the Humanoid Rig, use the Generic Rig.
Animators are primarily intended for humanoid characters but are often
used to animate single values (e.g., the alpha channel of a UI element).
Avoid overusing Animators, particularly in conjunction with UI elements.
Whenever possible, use the legacy Animation components for mobile.
Simplify colliders
Use the Physics Debug window (Window > Analysis > Physics Debugger)
to help troubleshoot any problem colliders or discrepancies. This shows
a color-coded indicator of what GameObjects should be able to collide
with one another.
The Physics Debugger helps you visualize how your physics objects can interact with each other.
Everyone should be using some type of version control. Make sure your Editor
Settings have Asset Serialization Mode set to Force Text.
If you’re using an external version control system (such as Git) in the Version
Control settings, make sure the Mode is set to Visible Meta Files.
Version control is essential for working as part of a team. It can help you track
down bugs and bad revisions. Follow good practices like using branches
and tags to manage milestones and releases.
Check out Plastic SCM, our recommended version control solution for Unity
game development.
Watch out for any unused assets that come bundled with third-party plug-ins and
libraries. Many include embedded test assets and scripts, which will become part
of your build if you don’t remove them. Strip out any unneeded resources left over
from prototyping.
The Unity Accelerator is a proxy and cache for the Collaborate service that allows
you to share Unity Editor content faster. If your team is working on the same local
network, you don’t need to rebuild portions of your project, significantly reducing
download time. When used with Unity Teams Advanced, the Accelerator also
shares source assets.
For well-architected projects that have low build times (modular scenes, heavy
usage of AssetBundles, etc.), we perform changes while onsite and reprofile to
uncover new issues.