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.
There are a few Project Settings that can impact your mobile performance.
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.
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.
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 drop below 30 fps for slow or relatively static scenes and reserve higher fps settings for gameplay.
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.
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, 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.
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.
Most of your memory will likely go to textures, so the import settings here are critical. In general, try to follow these guidelines:
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.
The only exceptions are:
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.
Much like textures, meshes can consume excess memory if not imported carefully. To minimize meshes’ memory consumption:
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.
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.
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).
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.
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.
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:
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.
New to the Frame Debugger? Check out this introductory tutorial here.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Fullscreen Post-processing effects, like glows, can dramatically slow down performance. Use them cautiously in your title’s art direction.
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.
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.
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.
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.
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.
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.