When i started development on my game, Shadow of the Orient, in early 2021 i wasn’t really thinking about game optimization early on. All of my early testing was done primarily on my desktop computer and being that I’m developing a 2d game framerates were insanely fast and game performance was great. When I finally got a little further into development i decided to start testing more on my mobile device, a Samsung Galaxy s10, and first early impressions on gameplay performance were positive…I was getting well over 100fps.
As my game continued to grow with more sprites and a larger codebase i noticed my FPS had to started dip a little on my mobile device but nothing too drastic so I thought things were peachy…nothing could be further from the truth. When I play test my game on my mobile device i like to run several tests to see how the game performs after a certain amount of gameplay so here’s a breakdown of my methods:
- Load up a level, play for a bit (a few minutes) then die on purpose…then rinse and repeat this process roughly 30 times. If my FPS drops significantly after this process its a clear indication that reloading my scene is causing performance issues.
- Load up a level and leave the game running for 30 minutes in an idle state – if the FPS drops after 30 minutes its a clear indication that scripts in my codebase and possibly other factors (such as draw calls) are causing performance issues.
- Play the game from the perspective of a real player – load up the game and just start playing through each level all while keeping an eye on performance. I usually run this test after I’ve made significant optimizations to my game so I can get a sense of what a real user will experience after a certain amount of gameplay time.
I should mention that the target framerate for my game is 60FPS and it’s a good idea to test on as many mobile devices as possible to get a sense of how your game performs on older devices vs newer devices. I wouldn’t expect my game to perform at 60FPS on a device that is 7 or 8 years old so i aim to test on devices in the range of 4-5 years from the current date. That being said i did test my game on an iPhone 6, which is now roughly 8 years old, and my game ran quite well after implementing most of my optimizations.
Ok let’s get into the real meat and potatoes of this article – the optimizations. Since games come in all different sizes the scope of required optimizations can vary – a 3D RTS game for example would require much more optimizations compared to a 2d action platformer game. Below is a list of optimizations that I applied to my game, followed by instructions, which greatly increased overall performance for my game on mobile devices.
- Variable Caching
- Sprite atlases
- Efficient alternatives to CPU intensive tasks
- Object culling
- Object Pooling
- GC Allocations
Variable Caching
I can’t stress enough how important variable caching is when deploying your game for final production. During the development cycle caching can be overlooked since you’re probably developing and play testing on a high end desktop machine but when hitting the small screen you just don’t get the same amount of CPU/GPU bandwidth that you would from a desktop counterpart – and this can cause severe bottlenecks in your mobile game. Below are some samples of caching techniques that I implemented in my code to reduce GC allocations.
Transform / Vectors
private Vector3 _myPos; private Transform _transform; private void Awake() { _transform = transform; } private void Update() { _myPos.Set(10, 10, 0); _transform.position = _myPos; }
Here I’m caching the transform of the game object and caching a Vector3 which is used to manipulate the position of the game object. The key takeaway here is to avoid having to declare the “new” keyword when working with Vectors and caching the transform of the game object to avoid having the Unity engine declare the “new” keyword when modifying the position of a game object.
Coroutines
private WaitForSeconds _waitTime; private void Start() { _waitTime = new WaitForSeconds(1f); } private IEnumerator MyCoroutine() { yield return _waitTime; }
Here, I’m caching WaitForSeconds to avoid having to declare the “new” keyword during each coroutine iteration. The only downside to this is you can’t manipulate the wait time through the inspector while the game is running in the editor so I would recommend implementing this optimization just before publishing your game for production.
Sprite Atlas
I won’t go into too much detail on this, since the Unity docs already explains it well, but if your not familiar with the Unity sprite atlas tool then i highly recommend implementing it in your game to reduce draw calls. To best sum it up ill take a queue right from the docs: In a project with many textures, multiple draw calls become resource-intensive and can negatively impact the performance of your project.
Efficient alternatives to CPU intensive tasks
Performing operations like distance checks and collisions every frame can be CPU intensive so its best to find ways to optimize your code as best as possible. During my playtesting with Shadow of the Orient I noticed significant performance gains after optimizing distance checks – something which I use quite a bit in my game. I also use a lot of collision checks in my game so its worth exploring the difference between things like Physics.OverlapSphere vs Physics.OverlapSphereNonAlloc and see if that provides any performance gains. Other things that can be optimized are enums (treat those as integers) and running physics based code every couple of frames instead of every frame. To be honest this particular topic requires extensive research and testing to see where performance gains can be made on the CPU. Another thing you can try, which was suggested to me by another developer, is to run a single Update call for your entire game…but not sure if that’s really worth the effort for my small scale 2d game.
Object Culling
When you’re developing a game there really is no point in running operations and rendering objects when there out of the camera view and that’s where object culling can really offer significant performance gains for your game. Object culling is a common practice in game development and it only makes sense to allocate CPU resources for objects that are within the camera view. Below is a code snippet of a custom class I created which I use for light occlusion:
using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Rendering.Universal; public class OcclusionManager : MonoBehaviour { [SerializeField] private bool occludeLights = true; [SerializeField] private List<Light2D> lights = new(); private Transform _transform; private void Awake() { _transform = transform; //Get lights if(occludeLights) lights = FindObjectsOfType<Light2D>().Select(a => a.GetComponent<Light2D>()).ToList(); } void Update() { if (occludeLights) { foreach (Light2D light in lights) { if (light.lightType != Light2D.LightType.Global) { if ((_transform.position - light.transform.position).sqrMagnitude > gameData.LightOcclusionDistance * gameData.LightOcclusionDistance) light.enabled = false; else light.enabled = true; } } } } }
Object Pooling
Object pooling is another common practice in game development which is basically a design pattern for creating and re-using game objects in your project without actually destroying the game object that needs to be removed from the scene. If you’re not familiar with object pooling then you should definitely familiarize yourself with it and integrate it into your projects. For my game I built my own custom object pooling system but Unity now comes with object pooling built in so I would recommend learning the new implementation provided by the Unity game engine.
GC Allocations
Garbage collection is a rather huge topic to discuss for this blog post but its something every developer should be aware of during game development. My best advice is to research what causes GC allocations in Unity from a coding standpoint – for example in older versions of Unity a foreach loop caused GC allocations but in newer versions it no longer does. Things like Unity Events create GC allocations when initialized but not when there dispatching events. The golden rule with GC allocations is to try to keep it to a minimum as much as you can – getting 0 bytes of GC allocations every frame is most likely next to impossible. Another things i would suggest is calling the GC collector manually at certain points in your game – maybe when a cinematic sequence occurs or when a certain action occurs such as a game pause.
Well these are some tips that i can recall implementing in my game, all of which have worked very well for me, and I hope you find these tips useful. Every game project is different in scope however so depending on the scale of your game additional optimization methods might need to be implemented to gain more performance.
Have a question? maybe two? drop a comment below or feel free to reach out to me on Discord: Spacelab_Games#4336