Optimizing Unity game performance
Introduction
So, you have a game that you build for the world to play. Congratulations. You sent out the game to your family and friends to test the game for you to gather their feedback so that you can work out some problems before you make a public release. In some cases, you might have been getting this feedback is that the game runs mad slow or the frame rate is so low that it is nearly unplayable. True, when the game suffers frame rate issues, it gives the users major undesirable feelings of not playing the game. I myself have faced these issues and try to minimize as much as I can to make the game run smooth. No game is perfect. It is just the minimization of problems that matters.
Optimizing Unity Game Performance is one of the most crucial thing to have in the game development process and it is a lot more important to skills to have especially when you are making a Virtual Reality game, where you need the game to run at least 90 FPS consistently at a recommended VR machine specification. It is also equally important when you are making a mobile game, where device specs are pretty limited and generally anything below 30 FPS is a red flag. This article will focus on Optimizing Unity Game Performance.
Use Profiler
The very first thing that you need to do is to check what is making the game run slow, so that you can take appropriate action to reduce the bottlenecks. The following screenshot shows a glimpse of the Unity Profiler. You will find this window by clicking Window > Analysis > Profiler. The profiler shows the running graph when your game is running.
The screenshot that I am showing you is pretty performing, since it is consistently running at 100 FPS which is good. You will notice spikes that makes the game FPS go down to 70-80 FPS. For a severely under-performing game, the frame rate may drop as low as 15 FPS. If we take a look at top left corner, there are colored labelled categories that are contributing to the CPU usage. Understanding what is contributing to the lower game performance will help figuring out necessary actions to counter FPS drops.
Batching
If you are creating a big game where you have lots of game objects included, the presence of lots of gameobjects will result in issuing a lot of draw calls to render these gameobjects. This is resource intensive and will kill off pretty hefty amount of frames per second. To minimize draw calls, we use the concepts of draw call batching. There are two types of batching: Static and Dynamic. To make batching effective we would like to make the gameobjects share same materials as much as possible so they could be batched together. Also if two gameobjects share same material but different texture, we can combine those textures into a single big texture. The practice is called Texture atlasing. To enable static and dynamic batching we enable them by clicking on Edit > Project Settings > Players Settings > Other Settings and finally checking those two options.
Static batching
This type of batching is done on gameobjects that share same materials and do not move, rotate or scale up or down. To enable static batching, you can select all the gameobjects that you think will not move at all at any point in the game lifecycle, and click on Static checkbox on the top right corner on the inspector. Additionally, you can click on dropdown arrow and select everything to make everything related to the gameobject as static including lighting and shadow.
Dynamic Batching
This batching does not require any thing on our part and it will be done automatically provided that the gameobjects fulfill the criteria required for dynamic batching which includes sharing materials. You will find detailed rulings on the Unity Documentation:
https://docs.unity3d.com/Manual/DrawCallBatching.html
Lighting and Light Probes
As we make gameobjects Static, we can also make the lights that we use in the environment as static, provided we dont move those lights. We can use Baked lighting mode instead of Realtime lighting mode, so that Unity does not render the light while the game is being run. Instead, it will create a lightmap and store lighting information on the light interacting with different gameobject in the scene.
You can further optimize the game by choosing what type of shadow we are going to render. It is advisable to not use shadow if we think it is not necessary in the game. Also we can choose between hard and soft shadows where hard shadows are considerably more optimization friendly. Additionally we can use Light Probes game objects and spread those in the empty spaces on the game scene where light will travel. Light probes can provide stored information on how the light will interact on moving objects in the scene. After we finish building our game, provided we have different gameobjects set to static and lights set to Baked, we allow Unity to Generate Bake and create maps, by clicking on Window > Rendering > Lighting Settings and finally click on Generate Lighting.
Optimizing Physics
Unity Game performance can depend on how many objects are used that interacts with physics. The less the objects dealing with Physics, the smoother the game will run. The very first thing we can check for the number of gameobjects that are using RigidBody as a component. If we think that we do not need Rigidbody for a specific gameobject, we just simply remove the Rigidbody component.
Colliders
We move to the colliders that gameobjects have. If we think that there are far away objects that we would never cross or never interact at all and the object is simply there for aesthetic purposes, then we just remove the colliders attached to those object. Normally colliders are added on default on each gameobject when they are created. Then there is a subject of choosing the right colliders for each object. If we need to use colliders, then we need to access how the object will interact with another object.
Lets take an example. If an object is a terrain that we need to have exact positional response from collision like a ball moving through uneven terrain, then it is ok to use Terrain collider. Terrain collider and mesh collider are resource hungry and should be used as minimal as possible. It is pointless to use mesh collider on some object where you can simply make the same effect using box colliders since box colliders have very low polycount.
Raycasts
Then there are Raycasts which you can limit which objects the Raycasts would hit, by using LayerMask and ignoring Raycast hit to gameobjects assigned in specific Layers, which the Raycast will ignore.
Optimizing code
A lot of CPU and memory saving can be done if your game code is properly utilized. A game contains a considerate amount of scripts and if we go through all of them, we will find ways to improve the code structure. When writing a code, we should always follow this simple rule: MAKE EVERYTHING SIMPLE. Consider the following Update function in the code:
void Update()
{
Process 1 .......;
Process 2 .......;
Process 3 .......;
}
If we can modify the above code into the following:
void Update()
{
SeriesOfProcess();
}
void SeriesOfProcess()
{
Process 1 .......;
Process 2 .......;
Process 3 .......;
}
We are bringing all those three lines of code into a single function. The update statement has to execute only a single line of code. Such example wont make much of a difference but if we are dealing with a single action that has 10-20 lines of code, its wise to keep those in a separate function, and call those function in Update or even Start function. Things can get even more resource hungry if loops are used in Update function. Take the example in the following:
void Update()
{
for (int i = 0; i < myList.Length; i++)
{
if (conditionIsTrue)
{
UpdateListScore(myList[i];
}
}
}
Loops will go through every iteration in order to finish the code block and this will kill some time and cpu power especially when there are nested codes like above. If we change to something like this:
void Update()
{
if (conditionIsTrue)
{
for (int i = 0; i < myList.Length; i++)
{
UpdateListScore(myList[i];
}
}
}
We are skipping some iteration in the code due to the fact the conditional check are met thus saving some more time and CPU power. Now lets move away from the Update function and take a look at the loop itself. Maybe we can put the whole loop into a specific function.
void UpdateUserScore()
{
if (conditionIsTrue)
{
for (int i = 0; i < myList.Length; i++)
{
UpdateListScore(myList[i];
}
}
}
Coroutines
Naturally we could put this on the Update function or even Start or any other functions. However say we are dealing with a big game and we are making updating scores for hundred thousand users. If we leave that operation running either on Start or Update, there is a super high chance that the game may freeze, since the code has to finish its block before it moves to the next available statement. We would want this loop to run on background while simultaneously finish the ongoing loop. We could take the advantage of using Coroutines. I have wrote a detailed tutorial on Coroutines, which you can find on the following link:
Object Pool
There are situation where you need to instantiate objects in the scene and also destroy those objects too. For example, in case of shooting bullets, the bullets that get instantiated travel outside the game window bounds and get destroyed. Lets take a look at the following example:
void ShootBullets()
{
if (Input.GetKeyDown(KeyCode.X))
{
Instantiate(Bullets, transform.position, Quaternion.identity);
}
}
void DestroyBullets()
{
if(Bullets.transform.position > OutOfBounds.position)
{
Destroy();
}
}
Here we are taking scripts from a player and from a bullet gameobject. The player instantiate bullets whereas the bullets destroy itself once it reaches a specific point. The practice of instantiating and destroying gameobjects is resource intensive and should not be encouraged, especially when we are calling huge amount of gameobjects. Also due to presence of large amount of gameobjects in the scene, the game become very slow in the process. You can avoid this by using Object Pool. The following code shows Object Pooling:
First we create a class that stores a List of gameobjects. We fix a number of amount that we want to store and not go beyond it. We instantiate the gameobjects (in this case the bullets) and store them altogether in a list and deactivate them before being used:
List<GameObject>poolList;
public Pool(int amount, GameObject bullets){
poolList = new List<GameObject>();
for(int i = 0 ; i < amount; i++){
GameObject bullet= (GameObject)Instantiate(bullets);
poolList.Add(bullet);
bullet.SetActive(false);
}
}
Later when we shoot bullets, we take one bullet from the list for use and make it active:
public GameObject GetBullet(){
if(poolList.Count > 0){
GameObject bullet= poolList[0];
bullet.RemoveAt(0);
bullet.SetActive(true);
return bullet;
}
return null;
}
Finally when the bullet is out of bounds, instead of destroying, we put it back to Object Pool:
public void DestroyBullets(GameObject bullet){
poolList.Add(bullet);
bullet.SetActive(false);
}
In this way we cache the gameobjects and the resource needed to instantiate and destroy objects are not used.
Conclusion
There are many other ways that we can use to reduce the amount of CPU usage, but these are the primary ones that we use a lot. Optimizing Unity Game Performance is a big subject when it comes to game making. Then there are quality and player setting that we need to take into account, when we build mobile games. That is also a whole big thing and depends on various devices. I hope the above optimization techniques will help you speed up the games drastically, since it helped me in my projects.