Optimizing Sausage Sports Club for Nintendo Switch
I’ve been working on Sausage Sports Club’s Switch version for a while now and getting it to run well has been a big hairy beast. Part of that problem is that resources on optimizing Unity games are few and far between and part of it is the closed source nature of console development. So I want to share some lessons learned (in an NDA friendly way). Hit me up with questions in the comments or @chriswade__ on Twitter.
- First off, get familiar with Unity’s profiler. Most info I’ve gleaned about improving my game’s performance started there. The CPU Usage widget shows all the Unity-sourced function calls, how many times they were called, how long in milliseconds and % of frame they took and how much garbage they created. There’s also a graph that shows a visual distribution of how frame time was spent between rendering, physics, scripts, and profiling related overhead.
- Some things that aren’t noticeably expensive on PC or other console platforms are expensive on Switch. Null checking, Vector.magnitude/sqrMagnitude, Quaternion.Euler, Quaternion.LookRotation, setting transform.position/rotation (localPosition/localRotation are OK though), setting rigidbody.velocity/angularVelocity were all calls I had to slow my roll on.
- Speed tests ON device are super important when developing for consoles. Their low level libraries work differently from my development PC, so I keep a second (mostly empty) project where I test ideas about performance and functionality on Switch without having to wait for my game’s 10 minute build/deploy time.
- GC.Collect will make your framerate stutter and is triggered automatically if too much garbage is created.
- Your target is to create zero garbage in an active scene. That’s not really possible if you use coroutines or most asset store plugins but your Update calls should be garbage free.
- At game start, allocate a huge piece of memory to make the GC’s collection bigger, so automatic cleanups will happen less often.
- Call GC.collect yourself in places where low framerate isn’t noticeable (on scene change, on menu open, etc).
- Object Pooling is a must since creating and destroying objects takes a long time and creates garbage. I recommend this old but still good system: https://github.com/UnityPatterns/ObjectPool/tree/master/Assets/ObjectPool
- Again, get familiar with Unity’s profiler. There’s a Physics widget that shows the number active dynamics/kinematics, static colliders, rigidbodies, trigger overlaps, active constraints and contacts per frame. For my game, having more than 150 dynamics, 20 trigger overlaps or 500 contacts per frame is a sign of issues.
- An easy first step is to make sure you have a sane physics collision matrix. Make sure things that shouldn’t/can’t collide aren’t checking for collision.
- Lots of large triggers are very dangerous and will overlap with lots of things and run up your physics time.
- Collision is an exponential problem because all colliders resolve against all other colliders. One of the easiest ways to optimize physics is to turn off/remove unneeded colliders or physics objects.
- Again! Get familiar with Unity’s profiler. The Rendering widget lets you see in real time how many draw calls happen per frame and how many of them are being dynamically, statically batched and how many objects were instanced. There’s also info about video memory, but I didn’t need to worry about that.
- Also check out the Frame Debugger, which shows a step by step ordered list of all render steps that frame. It even groups by render queues and shows different cameras, image effects and batch/instance counts for each object. Regarding optimization, this tool is best for solving batching and instancing issues.
- Overdraw is the render time killer. For each blended alpha particle quad or mesh placed on top of each other, the occupied pixels need be drawn again. Even on the 1280 x 720 resolution screen of the switch, that cost adds up quickly.
- You can check overdraw with the render type dropdown overdraw setting in the scene view. Also if you turn your camera and suddenly the game chugs, that’s probably overdraw.
- Similarly, expensive shaders with many texture reads and instructions can have a multiplicative effect. The same number of pixels now takes twice as long to draw because you’re throwing twice as many instruction at the GPU. This is harder to debug from Unity, but I’ve had some success using the Tegra Graphics debugger to check which shaders have more operations than built-in shaders: https://developer.nvidia.com/tegra-graphics-debugger
- Your time is more important than frame time. Sometimes you’ll hear about or discover a way to slightly improve performance all over your game. But, make sure to test that the optimization will actually give back enough time to justify the work. Many times I’d discover some minor optimization and lose a day applying that lesson across the game to find I’d only gained back 0.02 milliseconds.
- Add debug commands to test settings during a build. How much physics time would I get back back if I disable half of these toys? LT + RT + Y. How much render time would I get without image effects? LT + RT + X.
- Be wary of bugs you’ll create when rewriting logic, changing collision matrices and removing/simplifying colliders. There will be a lot of them and most will be small enough to not notice right away. QA is your friend.
- Be wary of relying on blackbox plugins because they might have performance issues you can’t fix.