Search Unity

Optimize your mobile game performance: Expert tips on graphics and assets

August 3, 2021 in Technology | 12 min. read
Skeletons
Skeletons

Our Integrated Success team supports Unity customers with their complex technical issues. We sat down with this team of senior software engineers and asked them to share some of their expertise on mobile game optimization.

Our Accelerate Solutions team knows the source code inside out and works with a plethora of Unity customers to help them get the most out of the engine. In their work, they dive deep into creator projects to help identify points where performance could be optimized for greater speed, stability, and efficiency. As our engineers began to share their insight on mobile game optimization, we pretty quickly realized that there was way too much great information for the single blog post we had planned. Instead, we decided to turn their mountain of knowledge into a full-length e-book (which you can download here), as well as a series of blog posts that spotlight some of these 75+ actionable tips.

In the final installment of this optimization series, we focus on how to improve the performance of your assets, project configuration, and graphics. In case you missed them, check out our previous posts on profiling, memory, and code architecture, as well as physics, UI, and audio, for a more complete picture of how to optimize your game – or, download the free e-book for a rundown of all these topics.

Project configuration

There are a few Project Settings that can impact your mobile performance.

Reduce or disable Accelerometer Frequency 

Unity pools your mobile’s accelerometer several times per second. Disable this if it’s not being used in your application, or reduce its frequency for better performance.

Editor
Ensure your Accelerometer Frequency is disabled if you are not making use of it in your mobile game.

Disable unnecessary Player or Quality settings

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.

In the Quality settings, disable needless Quality levels.

Disable unnecessary physics

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.

Choose the right frame rate 

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 drop below 30 fps for slow or relatively static scenes and reserve higher fps settings for gameplay.

Avoid large hierarchies

Split your hierarchies. If your GameObjects do not need to be nested in a hierarchy, simplify the parenting. Smaller hierarchies benefit from multithreading to refresh the Transforms in your scene. Complex hierarchies incur unnecessary Transform computations and more cost to garbage collection.

See Optimizing the Hierarchy and this Unite talk for best practices with Transforms. 

Transform once, not twice

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

If you need to Instantiate a GameObject at runtime, a simple optimization is to parent and reposition during instantiation:

GameObject.Instantiate(prefab, parent);

GameObject.Instantiate(prefab, parent, position, rotation);

For more details about Object.Instantiate, please see the Scripting API.

Assume Vsync is enabled 

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 for smooth processes.

Don’t rely on default settings. Use the platform-specific override tab to optimize assets such as textures and mesh geometry. Incorrect settings might yield larger build sizes, longer build times, and poor memory usage. Consider using the Presets feature to help customize baseline settings that will enhance a specific project.

See this guide on best practices for art assets or check out this course on 3D Art Optimization for Mobile Applications via Unity Learn for more details.

Import textures correctly 

Most of your memory will likely go to textures, so the import settings here are critical. In general, try to follow these guidelines:

  • Lower the Max Size: Use the minimum settings that produce visually acceptable results. This is non-destructive and can quickly reduce your texture memory.
  • Use powers of two (POT): Unity requires POT texture dimensions for mobile texture compression formats (PVRCT or ETC).
  • Atlas your textures: Placing multiple textures into a single texture can reduce draw calls and speed up rendering. Use the Unity Sprite Atlas or the third-party TexturePacker to atlas your textures.
  • Toggle off the Read/Write Enabled option: When enabled, this option creates a copy in both CPU- and GPU-addressable memory, doubling the texture’s memory footprint. In most cases, keep this disabled. If you are generating textures at runtime, enforce this via Texture2D.Apply, passing in makeNoLongerReadable set to true.
  • Disable unnecessary Mip Maps: Mip Maps are not needed for textures that remain at a consistent size on-screen, such as 2D sprites and UI graphics (leave Mip Maps enabled for 3D models that vary their distance from the camera).
Editor screenshot
Proper texture import settings will help optimize your build size.

Compress textures 

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.

Screenshot
Uncompressed textures require more memory.

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.

The only exceptions are:

  • iOS games targeting A7 devices or lower (e.g., iPhone 5, 5S, etc.) – use PVRTC
  • Android games targeting devices prior to 2016 – use ETC2 (Ericsson Texture Compression)

If compressed formats such as PVRTC and ETC aren’t sufficiently high-quality, 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 on recommended texture compression format by platform.

Adjust mesh import settings

Much like textures, meshes can consume excess memory if not imported carefully. To minimize meshes’ memory consumption:

  • Compress the mesh: Aggressive compression can reduce disk space (memory at runtime, however, is unaffected). Note that mesh quantization can result in inaccuracy, so experiment with compression levels to see what works for your models.
  • Disable Read/Write: Enabling this option duplicates the mesh in memory, which keeps one copy of the mesh in system memory and another in GPU memory. In most cases, you should disable it (in Unity 2019.2 and earlier, this option is checked by default).
  • Disable rigs and BlendShapes: If your mesh does not need skeletal or blendshape animation, disable these options wherever possible.
  • Disable normals and tangents: If you are absolutely certain the mesh’s material will not need normals or tangents, uncheck these options for extra savings.
Editor screenshot
Check your mesh import settings.

Check your polygon counts 

Higher-resolution models mean more memory usage and potentially longer GPU times. Does your background geometry need half a million polygons? Consider cutting down models in your DCC package of choice. Delete unseen polygons from the camera’s point of view, and use textures and normal maps for fine detail instead of high-density meshes.

Automate your import settings using the AssetPostprocessor 

The AssetPostprocessor allows you to run scripts when importing assets. This prompts you to customize settings before and/or after importing models, textures, audio, etc.

Use the Addressable Asset System 

The Addressable Asset System provides a simplified way to manage your content. This unified system loads AssetBundles by “address” or alias, asynchronously from either a local path or a remote content delivery network (CDN).

Editor screenshot

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.

Screenshot
Load assets by “address” using the Addressable Asset System.

Click here to see how the Addressable Asset System can take the hassle out of asset management.

Graphics and GPU optimization

With each frame, Unity determines the objects that 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), whereas 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.

The following guidelines can help you to speed up your graphics.

Batch your draw calls 

Batching objects to be drawn together minimizes the state changes needed to draw each object in a batch. This leads to improved performance by reducing the CPU cost of rendering objects. Unity can combine multiple objects into fewer batches using several techniques:

  • 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 meshes larger than this, so enabling it will waste CPU time looking for small meshes to batch in every frame.
  • Static batching: For non-moving geometry, Unity can reduce draw calls for meshes that share the same material. While it is more efficient than dynamic batching, it uses more memory.
  • GPU instancing: If you have a large number of identical objects, this technique batches them more efficiently through the use of graphics hardware.
  • 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.
Editor screenshot
Organize your GameObjects to take advantage of these batching techniques.

Use the Frame Debugger 

The Frame Debugger shows how each frame is constructed from individual draw calls. This is an invaluable tool for troubleshooting your shader properties that can help you analyze how the game is rendered.

Editor
The Frame Debugger breaks each frame into its separate steps.

New to the Frame Debugger? Check out this introductory tutorial here.

Avoid too many dynamic lights 

It is crucial to avoid adding too many dynamic lights to your mobile application. Consider alternatives like custom shader effects and light probes for dynamic meshes, as well as baked lighting for static meshes.

See this feature comparison table for the specific limits of URP and built-in pipeline real-time lights.

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. Otherwise, you can create blob shadows with custom shaders.

Editor screenshot
Disable shadow casting to reduce draw calls.

Bake your lighting into Lightmaps 

Add dramatic lighting to your static geometry using Global Illumination (GI). Mark objects with Contribute GI so you can store high-quality lighting in the form of Lightmaps.

Enable Contribute GI. 

Baked shadows and lighting can then render without a performance hit at runtime. The Progressive CPU and GPU Lightmapper can accelerate the baking of Global Illumination.

 

Editor screenshot
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.

Use Light Layers 

For complex scenes with multiple lights, separate your objects using layers, then confine each light’s influence to a specific culling mask. 

Editor screenshot
Layers can limit your light’s influence to a specific culling mask.

Use Light Probes for moving objects

Light Probes store baked lighting information about the empty space in your Scene, while providing high-quality lighting (both direct and indirect). They use Spherical Harmonics, which calculate very quickly compared to dynamic lights.

Editor screenshot
Light Probes illuminate dynamic objects in the background.

Use Level of Detail (LOD) 

As objects move into the distance, Level of Detail can adjust or switch them to use simpler meshes with simpler materials and shaders, to aid GPU performance.

Editor screenshot
Example of a mesh using a LOD Group.
Screenshot
Source meshes, modeled at varying resolutions.

Use Occlusion Culling to remove hidden objects 

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 necessary for every scene, culling can improve performance in many cases.

Check out the Working with Occlusion Culling tutorial for more information.

Avoid mobile native resolution 

With phones and tablets becoming increasingly advanced, newer devices tend to sport very high resolutions. 

Use Screen.SetResolution(width, height, false) to lower the output resolution and regain some performance. Profile multiple resolutions to find the best balance between quality and speed.

Limit use of cameras

Each camera incurs some overhead, whether it’s doing meaningful work or not. Only use Camera components required for rendering. On lower-end mobile platforms, each camera can use up to 1 ms of CPU time.

Keep shaders simple 

The Universal Render Pipeline includes several lightweight Lit and Unlit shaders that are already optimized for mobile platforms. Try to keep your shader variations as low as possible, as they can have a dramatic effect on runtime memory usage. If the default URP shaders don’t suit your needs, you can customize the look of your materials using Shader Graph. Find out how to build your shaders visually using Shader Graph here.

 

Screenshot
Create custom shaders with the Shader Graph.

Minimize overdraw and alpha blending

Avoid drawing unnecessary transparent or semi-transparent images. Mobile platforms are greatly impacted by the resulting overdraw and alpha blending. Don’t overlap barely visible images or effects. You can check overdraw using the RenderDoc graphics debugger.

Limit Post-processing effects 

Fullscreen Post-processing effects, like glows, can dramatically slow down performance. Use them cautiously in your title’s art direction.

Editor screenshot
Keep Post-processing effects simple in mobile applications.

Be careful with Renderer.material 

Accessing Renderer.material in scripts duplicates the material and returns a reference to the new copy. This breaks any existing batch that already includes the material. If you wish to access the batched object’s material, use Renderer.sharedMaterial instead.

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, then swap to a simpler MeshRenderer at runtime.

Minimize Reflection Probes 

A Reflection Probe can create realistic reflections, but can be very costly in terms of batches. Use low-resolution cubemaps, culling masks, and texture compression to improve runtime performance.

Download the complete guide to mobile performance optimization

This was the final blog post in the mobile performance optimization series. However, if you want access to the full list of tips and tricks from the team, we’ve also published a 52-page e-book available here

Ebook

DOWNLOAD E-BOOK

If you’re interested in learning more about Integrated Support services and want to give your team direct access to engineers, expert advice, and best practice guidance for your projects, then check out Unity’s success plans here.

Didn’t find what you were looking for?

We want to help you make your Unity applications as performant as they can be. If there’s any optimization topic that you’d like to know more about, please let us know in the comments. 

August 3, 2021 in Technology | 12 min. read
Related Posts