Tips and best practices
Article outline
Introduction
Why is performance optimization important?
Performance optimization is one of the key aspects of game development. It allows you to create a project that will run smoothly and stably on various devices, from powerful gaming PCs to mobile phones. High FPS (frames per second) not only improves the user experience, but can also be a decisive factor in the success of your game on the market.
Brief overview of optimization methods
Performance optimization includes several areas, each of which is important in its own way:
Using the Unity Profiler: A performance analysis tool that helps identify bottlenecks.
Script optimization: Reduce the load from your C# scripts by improving logic and data caching.
Memory management: Efficient use of resources and minimization of garbage collection work.
Rendering Optimization: Reducing the number of polygons and using levels of detail (LOD).
Physics optimization: Reduced load on Unity's physics engine.
In this article, we'll take a detailed look at each of these areas and provide illustrative code examples so you can immediately put the knowledge you've gained into practice.
Let's move on to examining each of these points in more detail.
Using the Unity Profiler
How to start and use the profiler
The Unity profiler is a powerful tool that allows you to analyze the performance of your game project. It helps you identify bottlenecks and understand which parts of your code or resources are taking up the most time and memory. Here's how to launch and use the profiler:
Launching the profiler:
Open Unity and run your project.
Go to the menu
Window
>Analysis
>Profiler
.A profiler window will open at the bottom of the screen.
Main tabs of the profiler:
CPU Usage: Shows how much time is spent executing different parts of the code.
GPU Usage: Displays the GPU load.
Memory: Helps track memory usage and garbage collection activity.
Rendering: Analyzes rendering performance, including polygon count and material usage.
Physics: Shows the time spent on physical calculations.
Analysis of key metrics
Now that we know how to run the profiler, let's look at how to analyze key metrics to improve performance.
CPU Usage:
Hierarchy View: Allows you to see which functions and methods take the most time. This will help you determine where exactly you need to optimize your code.
Timeline View: Shows the execution of tasks over time, allowing you to see which operations are running in parallel.
Memory:
Used Heap: Displays RAM usage. High values may indicate memory leaks or poor resource management.
Garbage Collector: Frequent garbage collection can cause freezes in the game. Try to minimize the garbage collector's work by avoiding frequent creation and destruction of objects.
Rendering:
Draw Calls: Number of draw calls. Fewer calls means better performance.
SetPassCalls: Number of material switches. Try to minimize them by using satin textures and combining materials.
Code examples
Here's a code example that demonstrates optimization by caching components:
// Пример плохого кода: частое обращение к GetComponent в методе Update
public class BadExample : MonoBehaviour
{
void Update()
{
GetComponent<Renderer>().material.color = Color.red;
}
}
// Пример хорошего кода: кэширование компонента в методе Start
public class GoodExample : MonoBehaviour
{
private Renderer _renderer;
void Start()
{
_renderer = GetComponent<Renderer>();
}
void Update()
{
_renderer.material.color = Color.red;
}
}
Code Notes:
In the first example, the method is called every frame
GetComponent
which creates additional load on the CPU.In the second example, the component is cached into a variable
_renderer
in the methodStart
which reduces the number of callsGetComponent
and improves performance.
Using the Unity profiler allows you to visually see which aspects of your project need optimization. By analyzing the data provided by the profiler, you can make informed decisions and improve the performance of your game.
Next we will look at script optimization.
Script optimization
Avoiding unnecessary updates
One of the main ways to optimize scripts in Unity is to avoid unnecessary updates. Methods Update
, FixedUpdate
And LateUpdate
are called every frame, which can put a lot of strain on the processor, especially if they contain resource-intensive operations.
Example of bad code:
public class BadUpdateExample : MonoBehaviour
{
void Update()
{
// Проверка состояния каждый кадр
if (Input.GetKeyDown(KeyCode.Space))
{
Jump();
}
}
void Jump()
{
// Логика прыжка
}
}
Example of good code:
public class GoodUpdateExample : MonoBehaviour
{
void Start()
{
// Подписка на событие один раз при запуске
Input.GetKeyDown(KeyCode.Space) += Jump;
}
void Jump()
{
// Логика прыжка
}
}
Code notes:
In the first example the method
Update
checks the key state every frame, which is inefficient.The second example uses an event to perform an action only when needed, reducing CPU load.
Using Component Caching
Frequent use of the method GetComponent
can significantly reduce performance, since each call to this method requires searching for the component in the object hierarchy. Caching components avoids this problem.
Example of bad code:
public class BadGetComponentExample : MonoBehaviour
{
void Update()
{
GetComponent<Renderer>().material.color = Color.red;
}
}
Example of good code:
public class GoodGetComponentExample : MonoBehaviour
{
private Renderer _renderer;
void Start()
{
_renderer = GetComponent<Renderer>();
}
void Update()
{
_renderer.material.color = Color.red;
}
}
Code notes:
In the first example, the method is called every frame
GetComponent
which creates additional load on the CPU.In the second example, the component is cached into a variable
_renderer
in the methodStart
which reduces the number of callsGetComponent
and improves performance.
Example of using events and delegates:
public class EventExample : MonoBehaviour
{
public delegate void OnJumpAction();
public static event OnJumpAction OnJump;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && OnJump != null)
{
OnJump();
}
}
}
public class JumpHandler : MonoBehaviour
{
void OnEnable()
{
EventExample.OnJump += HandleJump;
}
void OnDisable()
{
EventExample.OnJump -= HandleJump;
}
void HandleJump()
{
// Логика прыжка
}
}
Code notes:
Using events and delegates allows you to effectively manage actions that should be performed when certain conditions occur, which improves the readability and maintainability of your code.
Script optimization in Unity includes many methods and techniques that can significantly improve the performance of your project. It is important to analyze each part of the code and apply best practices to achieve the best results.
Memory management
Working with textures and resources
Effective memory management includes optimizing work with textures and resources. Textures can take up a significant amount of memory, especially in projects with high graphics detail.
Texture optimization tips:
Texture sizes:
Use lower resolution textures whenever possible. Large textures increase the amount of memory needed to store them.
Use a compression texture format such as DXT (S3TC) for desktop applications and ASTC/PVRTC for mobile devices.
Mipmap:
Enable Mipmap for textures that are used on objects at different distances from the camera. This helps reduce memory load and improve performance.
Atlas Textures:
Example of texture setup in Unity:
using UnityEngine;
public class TextureOptimization : MonoBehaviour
{
public Texture2D texture;
void Start()
{
// Пример настройки текстуры для использования Mipmap и сжатия
texture.filterMode = FilterMode.Bilinear;
texture.anisoLevel = 4;
texture.wrapMode = TextureWrapMode.Repeat;
}
}
Code notes:
Minimizing the work of the garbage collector
Unity's Garbage Collector automatically manages memory, but running it too often can cause lag and stutter in your game. To minimize the Garbage Collector, follow these tips:
Avoid frequent creation and destruction of objects:
Try to reuse objects instead of creating new ones and destroying old ones.
Use object pools to manage frequently created and destroyed objects.
Use data structures with predictable memory allocation:
Avoid using collections that change size frequently, such as
List<T>
. Use arrays or insteadQueue<T>
with a predetermined size.
Object Caching:
Example of using object pools:
public class ObjectPooler : MonoBehaviour
{
public static ObjectPooler Instance;
public GameObject objectToPool;
public int amountToPool;
private List<GameObject> pooledObjects;
void Awake()
{
Instance = this;
}
void Start()
{
pooledObjects = new List<GameObject>();
for (int i = 0; i < amountToPool; i++)
{
GameObject obj = Instantiate(objectToPool);
obj.SetActive(false);
pooledObjects.Add(obj);
}
}
public GameObject GetPooledObject()
{
foreach (var obj in pooledObjects)
{
if (!obj.activeInHierarchy)
{
return obj;
}
}
GameObject newObj = Instantiate(objectToPool);
newObj.SetActive(false);
pooledObjects.Add(newObj);
return newObj;
}
}
Code notes:
This example creates a pool of objects for reuse, which reduces the number of memory allocations and deallocations, minimizing the work of the garbage collector.
I already wrote an article on the topic Patterns, including about Object Polo
Memory management in Unity is an important aspect of optimization that directly affects the performance of the game. By applying the methods and practices mentioned above, you can significantly reduce the memory load and improve the overall performance of your project.
Rendering Optimization
Reducing the number of polygons
Reducing the number of polygons in a scene is one of the most effective ways to optimize rendering in Unity. Fewer polygons mean less load on the GPU, which is especially important for mobile devices and VR applications.
Tips for reducing polygon count:
Using low poly models:
Create and use models with minimal polygon counts that still provide satisfactory visual quality.
Removing invisible polygons:
Make sure that your models do not contain polygons that will never be visible to the player, such as the interiors of objects.
Using normals and textures:
Example of reducing the number of polygons:
using UnityEngine;
public class LowPolyExample : MonoBehaviour
{
public MeshFilter meshFilter;
void Start()
{
// Применение упрощенной низкополигональной модели
Mesh lowPolyMesh = new Mesh();
// Установка вершин, нормалей и треугольников для низкополигональной модели
// (Примерный код, не учитывающий реальную модель)
lowPolyMesh.vertices = new Vector3[] { new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(1, 0, 0) };
lowPolyMesh.normals = new Vector3[] { Vector3.up, Vector3.up, Vector3.up };
lowPolyMesh.triangles = new int[] { 0, 1, 2 };
meshFilter.mesh = lowPolyMesh;
}
}
Code notes:
This example creates a simple low-poly model consisting of just one triangle. In a real project, you can import low-poly models from 3D editors such as Blender or 3ds Max.
Using Levels of Detail (LOD)
Levels of Detail (LOD) allow the number of polygons of models to be dynamically changed depending on the distance to the camera. This allows the load on the GPU to be reduced when rendering objects located far from the camera, without a noticeable decrease in visual quality.
Setting up LOD in Unity:
Creating a LOD group:
Select an object in the scene.
In the inspector, add the component
LOD Group
(Component > Rendering > LOD Group).
Setting up LOD levels:
Add different LOD levels, each using a model with a different polygon count.
Adjust screen percentages for each LOD level to determine when each model will be used.
Example of LOD group setup:
using UnityEngine;
public class LODExample : MonoBehaviour
{
public GameObject highPolyModel;
public GameObject mediumPolyModel;
public GameObject lowPolyModel;
void Start()
{
LODGroup lodGroup = gameObject.AddComponent<LODGroup>();
LOD[] lods = new LOD[3];
Renderer[] highPolyRenderers = highPolyModel.GetComponentsInChildren<Renderer>();
Renderer[] mediumPolyRenderers = mediumPolyModel.GetComponentsInChildren<Renderer>();
Renderer[] lowPolyRenderers = lowPolyModel.GetComponentsInChildren<Renderer>();
lods[0] = new LOD(0.5f, highPolyRenderers);
lods[1] = new LOD(0.3f, mediumPolyRenderers);
lods[2] = new LOD(0.1f, lowPolyRenderers);
lodGroup.SetLODs(lods);
lodGroup.RecalculateBounds();
}
}
Code notes:
This example creates a LOD group for an object that consists of three levels of detail: high poly, medium poly, and low poly. Each LOD level will be used depending on the distance to the camera, which reduces the number of polygons rendered at once.
Code examples
Let's look at more complex examples that involve optimizing various aspects of rendering.
An example of using texture atlases:
using UnityEngine;
public class TextureAtlasExample : MonoBehaviour
{
public Material atlasMaterial;
void Start()
{
// Применение атласной текстуры к материалу
Renderer renderer = GetComponent<Renderer>();
renderer.material = atlasMaterial;
}
}
Code notes:
Using texture atlases allows you to combine multiple textures into one, which reduces the number of render calls and improves performance.
Optimizing rendering in Unity involves a variety of methods and techniques that can significantly improve the performance of your project. By applying the methods and practices mentioned above, you can significantly reduce the load on the GPU and improve the overall performance of your game.
Physics optimization
Managing the number of physical objects
Unity's physics engine can become a performance bottleneck if there are many physics objects in a scene. Managing the number of physics objects and optimizing their behavior are key steps to improving performance.
Tips for optimizing physical objects:
Reducing the number of objects:
Make sure that only those objects that actually need physical interaction have Rigidbody and Collider components.
Use simple collider shapes (such as BoxCollider or SphereCollider) instead of complex MeshCollider.
Deactivation of physical objects:
Example of managing the number of physical objects:
using UnityEngine;
public class PhysicsOptimization : MonoBehaviour
{
public GameObject[] physicalObjects;
void Update()
{
foreach (GameObject obj in physicalObjects)
{
if (IsVisible(obj))
{
obj.SetActive(true);
}
else
{
obj.SetActive(false);
}
}
}
bool IsVisible(GameObject obj)
{
Plane[] planes = GeometryUtility.CalculateFrustumPlanes(Camera.main);
return GeometryUtility.TestPlanesAABB(planes, obj.GetComponent<Collider>().bounds);
}
}
Code notes:
In this example, physics objects are only activated when they are in the camera's field of view. This helps reduce the load on the physics engine by deactivating unnecessary objects.
Using Layers and Colliders
Using layers and properly setting up interactions between colliders can significantly reduce the amount of physics calculations.
Setting up layers and interactions:
Creating layers:
Go to the menu
Edit
>Project Settings
>Tags and Layers
.Create new layers for different types of physical objects, such as
Player
,Enemies
,Environment
.
Setting up layer interactions:
Go to the menu
Edit
>Project Settings
>Physics
.In chapter
Layer Collision Matrix
Disable interaction between layers that should not interact with each other.
Example of using layers and colliders:
using UnityEngine;
public class LayerOptimization : MonoBehaviour
{
private void Start()
{
// Установка слоя для объекта
gameObject.layer = LayerMask.NameToLayer("Player");
// Игнорирование столкновений между слоями Player и Environment
Physics.IgnoreLayerCollision(LayerMask.NameToLayer("Player"), LayerMask.NameToLayer("Environment"));
}
}
Code notes:
In this example, a layer is assigned to the object.
Player
and the ignoring of collisions between layers is configuredPlayer
AndEnvironment
which reduces the number of unnecessary physical interactions.
Code examples
Let's look at some additional examples involving optimization of various aspects of physics.
Example of using simple colliders:
using UnityEngine;
public class SimpleColliders : MonoBehaviour
{
private void Start()
{
// Замена сложного MeshCollider на простой BoxCollider
MeshCollider meshCollider = GetComponent<MeshCollider>();
if (meshCollider != null)
{
Destroy(meshCollider);
gameObject.AddComponent<BoxCollider>();
}
}
}
Code notes:
In this example, it is complex
MeshCollider
is replaced by a simple oneBoxCollider
which reduces the amount of computation required to handle physical interactions.
An example of using Triggers to optimize interactions:
using UnityEngine;
public class TriggerOptimization : MonoBehaviour
{
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
// Логика при входе игрока в триггер
}
}
}
Code notes:
This example uses a trigger to handle interactions, which reduces the number of physics calculations and improves performance.
Physics optimization in Unity includes many methods and techniques that can significantly improve the performance of your project. By applying the above methods and practices, you can significantly reduce the load on the physics engine and improve the overall performance of your game.
Conclusion
Optimizing performance in Unity is a complex but critical process that directly impacts the success of your game project. In this article, we covered key optimization techniques, including using the Unity profiler, script optimization, memory management, rendering improvements, and physics optimization. Here’s a quick summary of each aspect we covered:
Using the Unity Profiler:
The profiler helps identify performance bottlenecks.
Analyzing CPU, Memory and Rendering metrics helps you identify areas for improvement.
Script optimization:
Avoid unnecessary updates and use component caching.
Use object pools and events to reduce CPU load.
Memory management:
Optimize the use of textures and resources.
Minimize garbage collection by avoiding frequent object creation and destruction.
Rendering Optimization:
Reduce polygon count and use levels of detail (LOD).
Combine textures into atlases to reduce the number of render calls.
Physics optimization:
Manage the number of physical objects and use layers to customize interactions.
Use simple colliders and triggers to reduce the load on the physics engine.
Additional resources for study
If you'd like to deepen your knowledge of performance optimization in Unity, here are some useful resources:
Unity Documentation: Optimization Guide
Courses and tutorials on platforms such as Coursera, Udemy, and Pluralsight.
Forums and Communities: Unity Forums, Stack Overflow, and Reddit.
Optimizing performance takes time and effort, but the results are worth it. By applying the methods and techniques discussed in this article, you can create a game that runs smoothly and stably across multiple devices, providing the best user experience. Good luck with your projects, and keep learning and improving in the art of optimization!