Performance tips for Unity 2d mobile
Update 09/2018: As this is one of my more popular posts, I've updated it with even more tips that I've learned in the meantime. At the time of writing the update, we're using Unity 2018.2.
At the minute I'm looking into performance with our latest game, so I figured I'd gather together all the different tips that I ended up using to get our game running back up at 60fps.
You see, it used to run at 60fps no problem, but as development continued and the Unity versions piled up, occasional hiccups became more and more obvious, so it was time to dig down and figure out what was going on behind the scenes. We're currently Android only (as it's a ton quicker to iterate and get everything working as it should), but The latest Unity version (5.3 as of - original - writing) seemed to exacerbate the performance problems on this.
Our game
There's no one silver bullet to optimisation, and what works for one person mightn't work for another, so with this in mind, this is our game constitutes of:
- It's a 2D golf game, for mobile, using basic Sprites
- It uses multiple scenes, and the new Unity UI
- In the game scene, the world map is built with individual tiles
While the levels are still built with individual tiles, we now render all the static tiles to a single
RenderTexture
, which sharply reduces the number of draw calls - It's multiplayer, so you get CPU spikes as messages come and go
We've since moved the compression/decompression and serialisation/deserialisation to different threads, so while there's still the issue of memory etc, there's less of an impact on the framerate. In the long term, we'd like to move to Unity JSON and the built-in compression etc in .Net 4.5, but it requires some work
- We don't use any lights, multiple cameras, etc
It goes without saying that the performance issues were found using the profiler, so if you have it, it's something you should be using.
Also, these tips are mainly focused on 2D. Your mileage may vary.
The dumb stuff
Let's get the dumb stuff out of the way first. These are the simple things you need to do, and don't need much discussion.
- Set your
Application.targetFrameRate
to 60. By default on mobile, it's 30 - Otherwise you can turn on
vSyncCount
in the quality settings. Apparently, on some Android devices, with Android 5, you can render at more than 60fps, so if you don't force vSync, you run the risk of an unresponsive device, as it's rendering too quickly, so it's possible that future versions of Unity will force thisI originally misunderstood the first two points. I'd assumed that
Application.targetFrameRate
was the "code way" of enforcing vSync. Because we were switching from 60fps in-game to 30fps in the menus (good for battery life), we switched off vSync and just usedApplication.targetFrameRate
. However, they're two very different things, and what ended up happened was that the CPU was issuing frames faster than the GPU could handle them, as we ended up with intermittent stuttering. Enforcing vSync makes the CPU wait. Basically, the framerate went from https://imgur.com/a/lE0LedF to this https://imgur.com/a/EPq6q7T.Now, I'd highly recommend enforcing vSync. Unity doesn't recommend using both vSync and Application.targetFrameRate at the same time though, as it could lead to CPU frame drift. If you'd like the full story, you can find the thread here.
- When testing, test on an actual device - you can run on the desktop to make sure it doesn't crash, but it won't tell you a tap about what's happening on the device
- If you're not using object pools, use object pools
- Use something like TexturePacker to pack your images into one, so you can get Dynamic Batching going
- Remove empty callbacks from components. If they're there, even if there's nothing in them, they'll get called, which slows you down
- Cache components you use regularly. Ditto for GameObjects. No
GameObject.Find()
when you can get away with it, especially not inUpdate/FixedUpdate
- Disable/Disactivate GameObjects/Components when you don't need them, especially if they have
Update/FixedUpdate
calls - If using physics, don't move
Rigidbody2D
yourself (i.e. directly setting the postion), especially if they've been marked as static. The physics engine doesn't like and will let you know. If you need to, disable the GameObject, set the position using theTransform
(there's currently a bug when setting theRigidbody2D.position
while the GameObject is deactivated), then re-activate it - Watch your poly count. We had a pretty high-resolution ball, which was fine with 1/2 players, but we'll be releasing soon with 1v3/teams, so all of a sudden your poly count doubles
- If you have lights in your project, but don't need shadows, or don't need shadows on everything, turn off the Cast shadows/Receive shadows checkboxes on the relevant
MeshRenderers
- Don't use Coroutines when a simple timer will do the trick. Coroutines are heavy
After that comes all the things that require a bit more effort on your part.
Let's go a bit deeper.
Control the garbage collector
One of the worst things that can happen in the middle of playing is the GC kicking in. You'll usually notice if it looks like your rendering is skipping every so often.
Now the important thing to remember is that the GC only kicks in when the application requests more memory. The default behaviour is to try and free some up before asking for more from the OS. This means that if you instantiate everything you need up front, and you have no other memory allocations during play, you'll never have a problem with the GC.
If you're using object pools, easy, right?
The killer is the hidden allocations. These can be big or small, but they build up until eventually your application requests another memory page, resulting in the GC crapping all over your game.
Now you might see something like a few hundred bytes every frame, and think, "No big deal". At 60fps, 100 bytes becomes ~6KB a second, ~350KB a minute. It all adds up.
- Using a
foreach
will allocate 32B of memory for the enumerator. While it might not sound like much, a couple of these inside anUpdate/FixedUpdate
and you'll feel it - Adding or removing a callback from a delegate will allocate 104B of memory. I've not seen a way around this outside of directly calling the method (introduces dependencies etc), or, depending on your use case (e.g. UI needs to show the player's health), you can use ScriptableObjects
- Strings allocate memory as you create them. Keep a special eye out for concatenation -
"Hello " + "world"
- as in this case you're actually allocating 3 strings here, one for each part, and one for the final result (strings are immutable). This is pretty easy to do if your callingDebug.Log()
a lot. UsingStringBuilder
or theString.Format()
where you need to - Know the difference between classes and structs and when to use them. Classes are created on the heap, while structs are passed by value. Be especially careful of Lists of structs
- If you see
SendMouseEvents
showing up, then you're hitting a dumb bug with aGetComponent()
call. There's some code in the MouseEvents class that does a lookup for aGUILayer
component on your camera. NormallyGetComponent()
calls are cached, however Unity can't cache it when it'snull
, meaning you get hit with it every frame. Solution: add aGUILayer
to every camera in the scene. You'll get a warning that it's a deprecated class, but there ya go - If you see
IMGUI
showing up in your GC panel, that's the legacy Unity UI code. Comment out anyOnGUI
calls that you have running outside of the Editor platform - Pre-size any
Lists
andDictionaries
, even if it's a complete guess. The default limit is 0 when it's created, then 4 when something's added to it. Due to the way list resizing works, if you're adding a number of elements, you'll get hit multiple times with the resize-and-copy operation. Print out the max sizes so you get a feel for what you need - If you're using JSON, Unity's built-in JSON is miles ahead of other solutions, such as JSON.Net, both in terms of speed and memory. Use it where you can. There are some downsides, such as having to workaround derived types (deserialise common field (e.g.
"classname"
), the create the right class) and not supportingDictionaries
(though if you know the names of your keys, make it a straight object), but the benefits make up for it - Instantiate
GameObjects
directly into their parent rather than creating, then adding. It's much quicker, and uses a lot less memory - A lot of APIs can allocate memory in the background. For example, calling
Collision2D.contacts
generates a newList
each time. UseGetContacts()
instead. Ditto forInput.touches
andInput.getTouches()
- Be careful of things like getting/setting
GameObject.name
, as it allocates memory with each access, as it's calling native code behind the scenes - Similarly, if you need to compare a
GameObject.tag
with anything, useGameObject.CompareTag()
rather than a==
, as for the same reason, it's accessing native code - If you're not using occlusion culling (possible for a 2D game where everything is on the screen), then turn it off by setting
useOcclusionCulling = false
on yourCamera
components - Beware of unnecessarily heavy code - e.g. checking for duplicates when adding objects to a list. If you're doing this at the start of the game, when you're populating your data structures, and you know there'll be no collisions yet, turn off the check temporarily, as it's a massive waste of time otherwise
In the CPU Usage of the profiler, you can sort by GC Alloc column to find out who's allocating and when. Removing as many of these allocations as possible means your game will have a smoother ride. Some will be outside of your control (such as physics engine allocations), but we can live with those.
In the Memory Area of the profiler, you can also take a snapshot of the current memory state, which you can use to help you see if there's a memory leak somewhere.
You can also call the GC yourself, using System.GC.Collect()
. Do this when you can afford to. We do it, for example, at the end of a shot, when the ball has finished moving, but before the next player has taken their turn. We allow a collection here, which lowers the chance of it happening where it would be visible (such as when balls are moving).
It's possible to turn off Unity's GC, but it's undocumented and doesn't work on iOS, so I wouldn't recommend it. You also run the risk of having your app crash because of an out-of-memory error.
Logging
In relation to one of the previous points, you'll need to remove any calls to Debug.Log()
etc that you have. This is because:
LogStringToConsole()
- the method called to print out your message is super slow. Even if you're not listening for it (i.e. have adb or whatever connected), it'll still get called- As well as that, when profiling, you need to select Development build, so the log message also grabs a stack trace (similar to how you'd see it in the Unity editor), which is even slower. You'll see a spike for every message when profiling
One of the reasons that it's super slow is that it's grabbing a stack trace on both the C# side and the native side. The latest versions of Unity allow you to set this behaviour to None/Script only/Full for each log type.
- As well as that, creating the message to log will entail lots of string concatenation and memory allocation, especially if you've overridden
ToString()
(and you should), and your logs are something likeDebug.Log( "Player " + player + " is doing something in game " + game );
"No problem", you say, "I have my own log function that wraps Debug.Log()
, so I only need to set a compilation constant inside there to early return out and not print anything!"
Sure, that'll solve the call itself, but a call to an empty function will still allocate the memory for the string message passed, giving you eventual garbage collection problems.
In the end, you have two three four solutions (James and Max from the comments showed a pretty neat way of removing Debug.Log()
calls):
- Wrap each call to
Debug.Log()
in a compilation constant so that the call doesn't exist when you do a production build - Before you build, do a Find and Replace or run a script to comment then all out
- Use the C# Conditional attribute. It's essentially a neater, more elegant way of doing conditional compliation. Inside a class, add the attribute to a
static void
function. If the define is declared, then the function, and all its calls are included. If not, it's like they never existed:#define USE_LOGS // NOTE: changed in 5.5 - see note below using System; using System.Diagnostics; public class Trace { [Conditional("USE_LOGS")] public static void Msg(string msg) { Debug.Log(msg); } } // elsewhere in the code - if USE_LOGS is defined, you'll see this output, otherwise, // it's stripped from the build Trace.Msg( "Hello world" );
- NOTE: I'm leaving the previous code example up for historical reasons, but as of Unity 5.5, they've changed the way this works. Apparently the previous behaviour was a bug, and the
#define
now needs to be set where you *call your method*, not where it's declared. Because of the way Unity projects work, this leaves two options: 1) add a#define
in every file, which is awkward, or 2) add your constants in the PlayerSettings > Scripting Define Symbols box. They go in the formUSE_LOGS;SOME_OTHER_DEFINE
. - Update May 2018: Max makes a great point in the comments that you can blanket kill
Debug.Log
calls by setting theDebug.unityLogger.logEnabled
property tofalse
. This is great for removing logs in code that you don't control (read: Plugins)
We originally went with the second option as it's easier in terms of dev (as I didn't know about the third one, but we now use that one as it's amazing), and you only need to run it just before a build, then Git revert the changes. On a Mac, you can make a shell script that goes something like this:
grep -rl --null "Debug.Log" "./Assets/Scripts" | xargs -0 sed -i "" 's|Debug.Log|//Debug.Log|g'
which uses grep to list all the files containing Debug.Log()
calls, and pipes the result to sed which comments them out, in place.
There are a few things to bear in mind however:
- You need to be careful about unclosed conditional statements and the like, otherwise:
can become
if( something ) Debug.Log( "Something happened" ); myObj.Foo();
which leads to unexpected behaviour. I have another script that pulls out all the logs and the line before, that I can feed to a JavaScript tool that prints any non-wrapped log, so I can fix it (JS is great for building quick, crappy tools)if( something ) //Debug.Log( "Something happened" ); myObj.Foo();
- If you're using
Application.logMessageReceived
orApplication.logMessageReceivedThreaded
(e.g. if you're sending errors to the server), then watch out that you don't comment out the logs that you're interested in (you can tweak the regex to ignoreLogError()
calls, for example) - Some code uses
UnityEngine.Log.Debug()
instead, so check for that as well (and first) - If you're using the script, because it's changing the line in place, it's like typing with the Insert key selected, so
Debug.Log( "Hello" )
actually becomes//Debug.Log"Hello" );
. As you're commenting this out, it doesn't really matter, but it's something to keep in mind if you want to adapt the script for something else
NOTE: That whole previous section is great, and I was reasonably pleased with myself when I finally got it working, but just use the third or fourth option :P
Remove Global Illumination
This was one of the more recent additions to Unity, and it seems to be turned on by default. As we're not doing anything relating to lighting, you can turn them all off (Precomputed Realtime GI, Based GI, Fog, everything). You can find the option in the Windows > Lighting panel. You'll need to do it for each of your scenes.
Depending on your Unity version, you might still see some spikes of GI in the profiler. This is apparently a known bug and is already fixed in an upcoming alpha.
Force OpenGLES 2.0
Unity is optimised for OpenGLES 2.0, but will allow you to select 3.0. It's also set by default and kind of hidden in Player Settings > Other Settings, where you need to untick Automatic Graphics API and remove OpenGLES3 from the list.
It'll be selected if your phone supports it, but your performance will tank, so it's better to force 2.0. This one will obviously improve with time though.
To be transparent, we no longer specifically state OpenGLES 2.0. As far as I can tell, any issues have since been fixed.
Set your colour/depth buffer
Make sure you're using the right sized colour/depth buffer. You shouldn't use the 32bit colour buffer unless you notice banding in your gradients. As our app is pretty much gradient free, it was an easy switch to gain a lot in memory. Ditto for the depth buffer. 2D games shouldn't need much in the way of a depth buffer, so you can bring it right down. You can find these in Player Settings > Resolution and Presentation.
NOTE: Unity 5 seems to have done away with the ability to set your depth buffer. While it seems like the default is 16-bit (though I haven't found any confirmation of that so don't quote me on it), you can now only opt to disable it (and the Stencil buffer) altogether. You can also get a small bit more control using the DepthTextureMode property of the camera.
Set your quality settings
A few simple tweaks here can go a long way:
- First of all, disable any selections higher than the defaults (Simple for mobile)
- Set Pixel Light Count to 0 as you're probably not using them
- Set your Texture Quality to as low as you can get away with. Even Half Res on mobile is mostly unnoticable, and you'll gain about 75% memory
- In a similar vein, disable mipmaps if you're not using them (i.e. scaling down), as this'll save you about 33% in memory
- Turn off Anisotropic Textures as you probably don't have any textures that you'll see at an oblique angle
- Turn Anti Aliasing off or set it to 2 passes, which should be enough to cover any jagged lines (only really noticable if you're using stuff like
LineRenderers
anyway, as most of the rest should be imagesReading more into it, because most (all?) mobile GPUs use tile-based rendering like Mali or Vulcan, it means that anti aliasing is essentially free (up to 4x, I think)
- Turn off Soft Particles. They're for blending properly into meshes, but that's a 3D problem
- You can also play with the Resolution Scaling Fixed DPI Factor, which renders the screen resolution below the device's native resolution, then stretches it up to fill the screen. It can make a big difference on high resolution devices, like the iPhone X
- Turn off Shadows
The two big ones are texture quality and anti aliasing. Texture quality directly impacts your memory and how long it takes to upload images to the GPU, while anti aliasing is applied to the entire screen, so on higher resolution devices it's going to be more expensive. The less passes for that, the better.
Canvas and the EventSystem
We're using the new UI (and you should be as well), and every UI element needs a Canvas
parent. By default, one is created when you add a new UI element, and by default, each Canvas
adds it's own Graphics Raycaster
so that you can hittest against it. More than one of these however, and you can expect your performance to tank, so remove any that you don't need. If you have the profiler, you'll see this come up as EventSystem
.
This doesn't seem to be much of an issue any more, however throwing all of your UI elements onto one
Canvas
does. It all comes down to redraw, and dirty blocks.When a UI element moves or is changed, it's marked as dirty, in order to be redrawn. A dirty element will also force its parent to rebuild its geometry, and so on up the line. So basically, if you have one moving element (e.g. a timer, or a health bar), you can end up redrawing the entire
Canvas
.If you have a Sub-canvas, or a
Canvas
nested inside anotherCanvas
, this will isolate their children from their parent. You can alternatively have multiple canvases.Generally speaking, you want all your static UI in one
Canvas
and all your dynamic stuff in another (or more, depending on refresh rates).The Unity fundamentals on UI do an excellent job going in-depth with all of this.
Speaking of UI, unless you specifically want to click on an element, make sure to untoggle raycastTarget
, otherwise that object'll be included for interaction. On UI heavy scenes, this can easily add up (we gained 10ms on our shop scene). If you're coming from flash, this is the equivalent of mouseEnabled
.
I've also read that you can get a performance hit from not having a material on UI elements. I didn't particularly notice any difference, but there are some default UI materials included, so you can add them if you want.
Use the right shaders
Speaking of materials, make sure you're using the right shader on your materials. The default Sprite-Default
for 2D sprites handles transparency, which is not ideal. Transparency on mobile is a real buzz kill, especially if you have it on large areas of the screen. On the profiler you'll see this come up as something like renderForwardTransparent()
. As we're using tiles, changing the default for a lot of them to a basic unlit brought the rendering time down significantly.
This will obviously only work for solid sprites (i.e. square), and the fact of having a different material will split up Dynamic Batching, but the gains are worth it.
Watch out for Unity UI Text
Unity UI Text is pretty powerful, but it's not the fastest, and you need to be very careful how you use it. We have a lot of text on the screen, with different effects such as outline, and it was hitting performance bad. We've recently made the switch to the Text Mesh Pro asset, and aside from the first frame where it's generating the texture, it's takes about half the time to render. This was a huge gain on some scenes. It's not a drop-in replacement though; you'll need to go through and replace your textfields one by one, but it's definitely worth it.
If you want to stick with Unity Text anyway, then make sure:
raycastTarget
is turned off; it's on by default, but you're probably hit-testing against a background anyway- Rich Text is turned off, unless you're rendering HTML etc
- Don't use Best Fit unless you really have to, or at most limit the min-max font size and only have 1 or 2 per screen. If your min font size is 10, and your max is 30, then what happens is Unity goes through the entire render cycle at 30, and checks if it fits. If it doesn't, then it goes down a notch and tries again, and so on. As you can imagine, with each render until you get the right size, this is pretty slow. Either fix your UI so you don't need to do this, or have one or two sizes in code and change them manually
- This one comes from Jaysama in the comments: Avoid dynamic fonts if you can. Dynamic fonts can be useful if you're using a common font and trying to save on download size, or working with asian languages, where the resulting font texture can be quite large. Unity won't pre-generate a font texture, but will use the underlying OS to render the text on the fly. This obviously has performance implications, especially if your text changes often.
Update: Unity have announced that TextMeshPro will become an official part of Unity!
Update: TextMeshPro is now built into Unity directly, and you can access it through the Unity Package Manager. If you already have it in your project (i.e. you bought the asset), then follow this guide on how to update your project. Fair warning, you lose easy access to the code, which is useful if you need to modify it (see the WebGL section below).
Watch out for overdraw
Overdraw is where the same pixel on the screen is hit multiple times. Aside from being redundant, when you combine it with transparent areas of sprites (and multiples thereof), it'll soon start to hurt. In an ideal world, you'd write to each pixel once, but this can be hard, especially with UI. In the Scene view, there's a dropdown that will show you the overdraw for the current scene, so you know where to apply your efforts.
Unity has a polygon mode which will replace your square sprites with much tighter outlines. You have more verticies and polygons, but less overdraw, so it can be a big help. If you're using TexturePacker (and you should be), then this is already built in for you.
Another thing to keep in mind is draw ordering. Normally, we'd be used to drawing a large background covering the screen, then drawing other elements over it. On mobile, it can be better to draw your front elements first, then make use of the depth buffer when drawing the background. A depth check is much quicker than overwriting a pixel.
Draw calls, Dynamic Batching, etc
By default Dynamic Batching should be activated (if not, you can find it in Player Settings > Other Settings). Basically it gathers up all sprites using the same material and renders them all in one draw call. There are a number of things that break batching, such as differing scales, tints, textures etc, so you might need to reorganise how you draw your elements, or combine textures into a spritesheet. A Text
UI element will probably not be rendered with UI sprites, and for one example that we had, simply moving it from behind one Sprite to in front of it brought our draw calls from 3 to 7. Changing the Pos Z
of the element will also probably break it.
Each draw call you have means a context switch for the GPU, which is expensive on mobile. I'd say any more than 10 is probably too many, and more than 20 will give you problems. Basically get it as low as you can, either through batching or combining your models/textures.
Static batching gives a better performance boost, but it means that you can't move, scale, or rotate anything that it's applied to. Unfortunately it also doesn't seem to work yet with SpriteRenderers
, so it's only for meshes at the minute.
If you use the Frame Debugger to analyse your frames, most of the time it'll give you the reason why a batch change occurred.
Set up your audio properly
An easy thing to overlook is your AudioClip
settings.
- Force To Mono: I'd always set this unless you have a specific reason not to. Unless the user is listening with headphones, most speakers are mono anyway
- Load In Background: Set this for every
AudioClip
that you don't need to play immediately. It'll push it on to another thread so it doesn't block the main one - Preload Audio Data: Specifies whether it should be preloaded, or loaded when you first play it. For short SFX, set it, otherwise ignore.
- Load Type: Decompress On Load will decompress it immediately, Compressed in Memory will keep it compressed, but load it anyway, and Streaming will only load what's necesssary to play, when you're playing it. For short SFX, I use Decompress On Load, while setting music files to Streaming
- Compression Format: PCM is the best, but largest, memory-wise, ADPCM is probably the best for short SFX, while Vorbis is the best for music. On mobile, everything is loaded as Vorbis anyway
Update 20/04/2017: As pointed out by Min Shin in the comments, I was actually reading the AudioFiles Manual page wrong. The actual text is:When audio is encoded in Unity the main options for how it is stored on disk is either PCM, ADPCM or Compressed. [...] The default mode is Compressed, where the audio data is compressed with either Vorbis/MP3 for standalone and mobile platforms, or HEVAG/XMA for PS Vita and Xbox One.
which I took to mean that only Vorbis/MP3 is used on mobile, but it's rather that Vorbis/MP3 is used on mobile, *if you choose Compressed*.
Making a few simple changes brought our app time-to-Home-screen down from around 30s to under 15s, as well as freeing up a lot of memory.
Enable Multithreaded rendering
This point is Android only (it's always enabled if you're using Metal on iOS), but in Player Settings > Other Settings you'll see a toggle for Multithreaded Rendering. This puts all rendering on another thread, freeing up your main thread for application logic, obviously a good thing.
It comes with it's own caveat though, as depending on what you're going with your project, especially if you interacting with textures, it can give graphic glitches. The only thing to do is try it for your project and see if you get away with it. It might also not work on some devices, though I'm not sure if Unity turns if off automatically or not.
If you see problems with it on certain devices, I almost say just disable that device, as the benefits are pretty awesome.
Scrolling
Doing any scrolling in your game menus? The new Unity UI has a scroll component built in, but its performance can be horrible. Solution: scroll the camera itself - if necessary create a new camera with the right necessary rect. Moving a camera is a simple matrix change, so the rendering is the same, while moving via a ScrollRect
seems to modify all the RectTransforms
all the way down, meaning everything has to be recalculated, and if you've half-way complicated UI, this can be a huge drain. Scrolling the camera alone can easily make it twice as quick. You might need to jump through hoops to get mouse coordinates working properly, but a bit of extra code to get rid of janky scrolling is doable.
Update 08/2016: TFG Studios got in touch through the comments to show off their optimised ScrollView. It looks pretty nice, but you can check it out yourself on YouTube
Know when to break from Unity
When you first come into Unity, everything is component-this and component-that, but sometimes you need to know when to break from Unity and do things a simpler, more direct way. As an example, when we first started making the map for the games, we did a naive approach where we use tiles and each tile has a component that helped with its behaviour; one for the tile properties (drag, etc), one for the physics, one for special considerations (e.g. flashing, or affecting the ball).
This works for quick projects, but pretty soon you realise that you've about 10k objects in the game, and most of them are useless. The properties were split off into static variables, the physics were removed and a collision/trigger mesh generated by our level tool instead (so one PolygonCollider2D
instead of one for each tile that needs it), and any special cases were merged where possible, so we'd have one loop rather than 50 Update()
methods running.
For the rendering, we also made use of a RenderTexture
and Camera culling masks, so all those tiles would get rendered exactly once to the texture, then hidden. For some tiles we couldn't do this (e.g for the ones that break or flash), but for the vast majority we could, which meant that our map and border tiles are one simple mesh. And because the semi-transparent tiles are baked into the texture, we can apply one super fast non-transparent material to the mesh to get the quickest rendering possible.
Use threads where you need to
Because our game is multiplayer, we use sockets via TcpClient
to send and receive messages. Originally we were doing a synchronous write and an async read. As the EndRead()
method is on another thread, there was no problem when deserialising/decompressing the received message. But that left the problem of sending. Normally it wouldn't be too bad, but sometimes you would get a message being sent out while the ball is moving, and compressing/serialising to JSON are pretty slow operations, so you're looking at minimum 1 dropped frame. Even sending the shoot message as you fired would introduce a tiny bit of lag on the client, which meant the experience wasn't great.
A simple threading system is now in place which means that all that is pushed off the main thread, so there shouldn't be any impact because of it. Some of the logic in the game had to be rewritten to accomodate that - because the writes were synchronous before, we could reuse objects, which we can't do with async - but the performance gains are worth it. Joseph Albahari has a great resource on threading in C#.
WebGL
If you're building your game for web, there are a few things to keep in mind:
- Everything is single threaded, so any cool threading features (that you might have used to gain some speed) are out the window
- In the same vein, all
Jobs
are now run on the main thread - Sockets don't exist, so if web players are playing on the same server as mobile players, you now have an extra layer of redirection, going from WebSocket -> Socket and back again
- The memory that you assign when your project starts is all you get, so it's highly likely you'll get some Out-of-memory crashes in the beginning until you find your sweet spot (NOTE: it increments in steps of 16MB)
As memory is such a big issue, and even an empty project results in a pretty hefty download and an unskippable delay at the start while million-odd lines of JS are compiled, it's super important that you work on getting your project as slim as possible. To that end:
- Make sure Strip Engine Code is ticked in the Build Settings. If you're using something that you're not directly referencing (e.g. an AssetBundle is using a class that your main file isn't), then you'll need to use a link.xml file to explicitly stop those files from being removed
- Use Crunch compression, as it's awesome. There can be some artifacts if you're using gradients, or issues with thin sprites (as it's a block-based compression), or sprites that you use for a clipping mask (e.g. round images can have block artifacts around the edges, and as clipping masks are either on or off, you lose your smooth edge), but it's worth it
- Make use of AssetBundles, if you want to remove as many embedded assets as possible from your project
- Enable Brotli compression if you can. You'll need a web server that can support it, and have to serve your site over HTTPS, but you'll save a few MBs, easy. Modern browsers have brotli decompression built-in, and being HTTPS will also give you access to HTTP/2, so it's win/win
A somewhat broken, but useful tool to use is http://files.unity3d.com/build-report/, which you can use to see which libs are actually going into your build. It'll tell you if a lib is being added because of another library (e.g. Particles might require Wind), or scripts (though not which script).
We found out that we had the 5MB 3D Physics lib being added, even though we're a 2D game. Why, you ask?
Well, aside from UnityPurchasing for some reason (but UnityPurchasing doesn't work on the web, so we could just delete it), it was being brought in by TextMeshPro, which has a feature to let you make physics-enabled text in 3D, and thus had Collider
references.
As mentioned above, because we had the code for TextMeshPro, we could go in and comment out the offending classes, but if you're using the Unity built-in TextMeshPro, you'll have to wait until (or if) they do it.
Check out the Lightweight render pipeline
This is a new addition but it's pretty awesome. Basically, Unity have opened up their rendering, allowing devs to make their own custom pipeline.
Honestly, looking at the code necessary to set it up gave me the shivers, but happily, Unity provide a Lightweight Pipeline and a HD Pipeline.
It's very easy to drop in and setup - if you're using the Unity shaders, you don't need to make any changes - and it simplifies rendering enough to give you a noticeable boost.
Check out the new ECS system
Another new one, and one we've not done yet. The new Entity Component System is definitely on the list for the next game though. Combined with the Job system, it's specifically built to take advantage of multithreaded systems, and lets you write high-performance code straight up.
Entities are reduced to mere ints. Components are nothing more than containers for data. The real meat comes with the Systems. They're made to treat lists of components all at once, and just bang it out.
If you've ever used something like Ash in AS3, you'll be right at home. One of the great advantages of this setup is that it becomes much easier to debug your code. You work on insular Systems, and only need to keep the code relative to that System in your head. You can modify freely without impacting the other Systems. It's a different way of thinking about things, but once you get into the swing of it, it's great.
Resources
While going through all of this over a number of different weeks and months, I started saving a list of useful links. In no particular order:
- The entire Performance Optimisation section - if you're having issues with framerate, the info in the graphics section can help you figure out if you're CPU-bound or GPU-bound
- Pretty much the entire Best Practices section, specifically Assets, UI, and Profiling
- Unite Europe 2016 - Optimizing Mobile Applications
- Unite Europe 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious Scriptable Object Revolution
- Unite Europe 2017 - Squeezing Unity: Tipes for raising performance
- Unite Europe 2017 -Practical guide to profiling tools in Unity
- Unite Austin 2017 - Writing High Performance C# Scripts
- Unite Seoul 2017 - Tips and Tricks for Optimising Unity UI
- Unity UI optimization tips
- When it pays to be cheap: Tips for big games on low-end mobile - some great stuff in here, though a lot of it bypasses Unity
- Understanding optimization in Unity
Conclusion
Optimisation is a pretty big subject, especially when so much of it is guesswork or slowed by the constant change > build > test cycle, which on mobile isn't quick. I also find the profiler on Unity not as good compared to others, such as Scout. Your history extends as far as the window, you can't select multiple frames to get averages (even though they have this information), and you can't save the results (update: you can now), so before and after comparisons either work through your memory, or screenshots.
Still, it's what we have to work with, so if you have it, use it, as sometimes, what's causing your slowdown isn't what you think it is. You don't need to profile all the time, premature optimisation being the devil and all, but you should do it regularly.
If there's anything I've missed, let me know! Happy hunting.
Comments
Very Nice article. Was not aware about the graphic raycaster on canvas. Will download and rate ;) your game for your precious avise
Thanks for the tips, it helps alot!
thanks it help me
much appreciated!
That's really awesome tips, thanks for sharing!
There is a third option for logging. Create a method that takes a format string and arguments, like so:
[Conditional("LOGGING_DEBUG")] public static void LogF(string format, params object[] arguments) { UnityEngine.Debug.Log(string.Format(format, arguments)); }
That way if the method is removed from the final build, you won't incur the string concatenation costs.
Great tip James, I didn't know about that one! It's much handier than the way I'm currently doing it :D
Thank you James, I was searching for this tips for about a year! I Wish you success in all your projects!)
UNITY UI TEXT section was very helpful for me!
I had "best fit" option with bad range - 0-300 and so on.
THX.
Very useful! Thanks! I think u forgot to mention font files, dynamic font was the killer in my previous game, especially if the text keeps changing (like incrementing score).
Thanks Jaysama, I've added a note!
Cool article, it helps me alot
Hi, Damian!
What about particles on mobile?
What about cool assets for profiling and detect problems on mobile?
Please add this sections
Regards
Hi Andrey!
We're currently using the Cartoon FX package for our particles, but I haven't had enough experience with particle systems on Unity to say that it's the best one.
For profiling, you can't go much better than the Unity profiler (though it needs an update for history length, multiple-frame select, and the ability to save profiles). It works on mobile, so you'll be able to get the info you need.
Thanks a lot for this post, sharing right now. Thanks, great job!!
This scroll view is fine for basic things, but for large data sets maybe you'll want to optimize a little. check this https://www.youtube.com/watch?v=FOQV8AA50Rw
Thanks! These tips will come in handy with my 2D game! Wish I had seen this before.
Hi, thanks for a great article!
For the Colour/Depth Buffer settings, you say they can be found at Player Settings > Resolution and Presentation. I don't see these option in Unity 5.4. Have they been moved to another location?
Thanks!
Hi David
The display buffer setting is still there: Player Settings > Resolution & Presentation > Use 32-bit Display Buffer.
As for the depth buffer, it seems like they've replaced the option of having a 16-bit depth buffer with just being able to completely disable it. Not an ideal solution, but the option can be found in Player Settings > Resolution & Presentation > Disable Depth and Stencil. You can get a little bit more control by using the DepthTextureMode of the camera.
Thanks a lot, the best optimization article I've read so far
Thanks a lot for this post!! The tips were really useful.
This is really good :) Can you share some tips for 3D too? I am doing a project on augmented reality where I had to insert about 30 3-d models in 1 scene, I found that upon build on android phone, the app takes about 40 seconds to load and start the camera.
Hi Amit
Most of my experience with Unity is 2D, so I wouldn't be able to give you definitive tips. Most of the same would apply though; watch your draw calls, share materials where you can, and use the profiler to know where your problem areas are.
Best optimization techniques captured in one place on Unity 3d.
Big thanks to Damian for sharing this inputs.Helped us lot to optimize our 2D app.
Wish there was buttons for donation (y).
Wow, thank you! That's by far, the best article I've ever seen on mobile optimization!
Question: Does it cause a problem to scale in the images transform? If not, do you need to match it to the canvas scaler... and do you have recommendations on best settings for the canvas scaler?
Thanks! Chris
Hi Chris
Thank you! To answer your question: in a general sense, scaling itself won't cause any particular issues. Obviously, unless your scale is
(1,1,1)
, you're going to be doing it, but it's generally a matrix step and pretty quick. Where it can potentially cause a problem is the how you deal with any particular artifacts that arrive as a result.Unless you're scaling is perfect (e.g. 1x, 2x, 4x), Unity is going to have to interpolate the pixels to get the final image. This can result in jagged edges or ugly artifacts. These can be combatted using (one or more of) mip-maps, filter mode (point, bilinear, trilinear), or anti-aliasing. Each one has its own performance impact, some more than others (e.g. if you can, don't use anti-aliasing). Most of the time however, it's not really a problem.
For the
CanvasScaler
, we use a screen spaceCanvas
, withUI Scale Mode
on theCanvasScaler
set toScale With Screen Size
and aReference Resolution
set to the iPhone 5 screen size, which is our base model on which we model all our UI. I don't think there's particularly one "right" answer, it's all down to the effect that you're looking for.Thx for the help!
just poked here to say thankyou for this mine of tips, hope this page will keep pinned on the internet for the time being because how precious it is
Thank you, some new tips for me.
May you explain more about "Preload Audio Data". I did not understand, have I tick or untick this option for android game (for big mp3 files)?
Preload Audio Data just loads and decompresses your sound file in memory so that it's ready to play immediately when you go use it. Whether or not to set it depends on the type of sound in question. If it's a short sound file (e.g. a gunshot), then you can set it so you don't suffer the slight delay that comes from having to load the sound initially. For longer sounds, such as music, it's better to leave it off, as you'll suffer longer delays on app/scene startup. You'd also be using memory unnecessarily (long sounds like music should always be streamed, especially on mobile).
Thanks for this helpful article, I've applied some of the tips and my game is doing well now, but the android device still heating up while running it.
Hi Hamza,
If your device is heating up when running your game, then it suggests that you're doing a lot with the CPU or GPU (profile to find out which). Depending on the type of game, this may or may not be a problem. If it's a complex game with tons going on, or if it's running on a lower-end device or specific chips, then this might be "normal". If there's not much going on, then it means that you're over-taking the hardward for nothing (e.g. if you have your game running at 60fps, but it doesn't need to, or at least it doesn't need to all the time. Without knowing more about the game in question, it's hard to be sure.
Thanks a lot for this blog. It helped a lot. I was creating a scroll snapping script (using ScrollRect UI) which worked perfectly in the editor but when I would try it on mobile it wasn't very responsive, but now its great.
One question. Do you think the Text component has improved since this post? i.e using Unity 5.6? I wondering if it still worth changing all my text components to the TextMesh Pro asset?
Hi Brogan,
In terms of performance, it's definitely worth it. In fact, Unity like it so much that TextMesh Pro is now part of Unity :) Right now it's free on the Asset store, and it should be directly integrated in later versions of Unity.
Thanks for the article.
For TextMeshPro (Unity's or not) I have something to say. It uses different material from the usual Unity Text. (It's Distance Field, (you can Google for Valve's paper) which allows crisp text on a big range of scaling despite very small texture size that store the text's texture) So it will impact your draw call if you make a switch from UGUI Text if they are sandwiched in between your other UIs. (Different material = set pass call) If you use UGUI Text it will batch nicely with other Image components.
One solution is to arrange the hierarchy so that Text Mesh Pro is rendered the last, but since Unity's UI render order depends on hierarchy order, you might have to move Text Mesh Pro out of your desired parent that you want. (You might have an animation on that parent that control this text, etc. so you cannot move out.)
But I have found that TMPro included "Distance Field Overlay" which have "Queue"="Overlay" specified in the shader. This means no matter where it is in the hierarchy it will be rendered the last. You should probably use this because most of the time nothing is blocking the text (so that your player can read it, of course) Use the frame debugger to confirm. (It will not really be the last in the screen, but the last in that canvas) You will still have draw call +1 when you make a switch, because the switch from Unity's UI texture to Distance Field, but that worths the crispness of TMPro.
Hi 5argon,
Great points. We've found the same thing in terms of hierarchy order, but Unity generally does a decent job of batching them all together, even if they're in different parents or animated. The frame debugger is essential here - from 5.6 onwards Unity will now also tell you why your call isn't batching, so you can be sure.
You do get the extra draw call, but honestly, if you've any half-way decent amount of text in your games/screens, the gains more than make up for it :)
So R U saying choosing compression format for mobile game is meaningless? Then, why is there option for compression format on mobile overiding option?
Hi Min Shin,
I was basing this on the Unity AudioFiles Manual page where they say:
So it looks like I mis-read this as saying only Vorbis/MP3 is used for mobile, but it's rather that's what happens if you choose Compressed mode, so that's my bad. I've updated the post.
Thank you for this! I tried many of your tips and it works! the looks of my graphics is better than before! and a bit fast! :D
Omg, all the sections este helpfull, The tips and troços lerned here até invaluable, thanks a lot !
Ive been searching the web for this Optimization guide and found this Holy Grounds of Optimization.
i hope you continue this and update this to the current Unity version
Great article, Keep this updated and add more..
very useful and collection of more optimization techniques in one place..
Hello sir great article, I'd like to summarize this with four words: Sleep with my wife. It blew my brains out and I didn't even have to waste any of my shotgun shells! I've been addicted to heroin for the last 20 years but thanks to this article I am finally free. Thank you.
Get around of
Debug.Log
calls:#if UNITY_EDITOR Debug.unityLogger.logEnabled = true; #else Debug.unityLogger.logEnabled = false; #endif
Hi, Unity Profiler allows saving/loading data now.
Submit a comment