In this short tutorial I will explain how to optimize draw calls when using sprites in Unity. I don’t intend to do a “full cover”, just sharing my experiences. Code snippets are in C#.
What are “draw calls”?
Very simply speaking, “draw calls” or “batches” are screen fill actions handled by the Unity graphic engine internally when anything is on screen and has to be drawn. A draw call includes accessing the material, and, if any, the texture of an object. Since all active objects on the screen have to be drawn every frame (on every Update), having too many draw calls puts a heavy load on the system and may kill frame rate, especially on mobile devices, or if “too many” is *really* a lot (~1000+, depending on hardware).
Using sprite sheets
If there’s a singe sprite, say [A] on the screen, that’s one draw call. [A][B] will be done in two, if their textures are on different sprite sheets, or are completely individual textures, as after accessing the texture of [A] and drawing it, Unity must access the texture of [B] to draw it. If both textures were on the same sprite sheet, it would still be one draw call. So, the first thing you should do is to use sprite sheets. This can be done in two separate ways, depending on your needs:
1. Use the “multiple” sprite format, but use it wisely. This is the “classic” sprite sheet method: one image contains many sprites using the slicing function, so they basically share one texture, thus only need one batch to be drawn. There are a few setbacks, however. First of all, unless you’re using a sprite sheet with the animator or assign each sprite manually in the editor, you’ll need to load all sprites at run time from script using something like Sprite[] mySprites = Resources.LoadAll<Sprite>(myPath + mySpriteSheet), provided myPath is in “Assets/Resources/”. The order of sprites in mySprites will match their order on the sprite sheet (unfold the sprite sheet in the asset browser and you get to see it). Even if you delicately rename each slice, you won’t be able to access them by name from script without some special workarounds; the easiest one perhaps is to use a dictionary that binds array indices to names, but you still have to provide a list of names that somehow matches the sprite order when setting up the dictionary.
2. Use the sprite packer. It is a built-in function in Unity that can create sprite atlases from any sprites, irrespective of where they actually are. You can access it in the Windows menu. Basically, you have to assign the sprites you want to pack the same “packing tag”, then select them, and press the “Pack” button in the sprite packer. Sprites on the same atlas require only one batch to draw, like they were part of the same sprite sheet.
Draw order
The above methods are fundamental in reducing the number of draw calls, but in many cases, they are far from enough. Take two sprite sheets, S1, that contains the sprites [A], [B] and [C], and S2, that contains [1], [2], [3]. Drawing [A][C][A][1][3][2] (in this order) needs two batches, but [A][C][1][A][3][2] would need four, because after drawing [C], Unity has to access S2, then S1, then S2 again. Introducing a third, fourth, etc. sheet would mess it up even more–but you can’t pack everything into one! The solution is to organize the draw order smartly.
The screen is filled from top-left to bottom-right. If your scene contains varied sprites from multiple sheets (e.g. map elements from multiple tilesets, plus actors), it is inevitable that these will require multiple texture accesses to draw, as they will be “mixed up” in the 2D draw order. What you can do is to assign them to multiple depth layers, because drawing is executed depth layer by depth layer! There are two ways you can do this:
1. Assign an “Order in layer” value wisely. This controls the draw order of objects in the same sorting layer (all objects are on the Default layer by default). A greater value means that the object will be drawn later in the queue, overlapping those that were drawn before. This is not only useful to avoid random graphical intersections between overlapping objects, but to reduce the number of batches. If sprites on S1 were all map or background elements, and the ones on S2 all actors (like a player entity), setting a lower layer order for all objects using a texture from S1 and a higher one for the others would reduce the four batches of [A][C][1][A][3][2] to only two, because the order would effectively become [A][C][A] + [1][3][2].
2. Assign sprites to sorting layers. They basically do the same, but are to be configured globally for the project in Edit/Project settings/Tags and Layers. You can create an indefinite number of sorting layers, each with a unique name. Then you can assign an object to them by setting the Sorting layer property. Layers that appear higher on the list are drawn first. The order specified by the Order in layer property will remain effective, but only relative to the actual sorting layer.
If you carefully design the contents of your screen, this will help drastically in reducing the batches needed to fill it. You only need to consider what elements do always overlap others, and organize the draw orders based on this. Elements that always or very often appear together should share the same sprite atlas.
Conclusion
I didn’t mention static and dynamic batching, because they don’t really do their job (as of Unity 5.3.5f1) when working with sprites. For whatever reason, the diffuse sprite shader breaks dynamic batching, no matter what you do. Static batching is also not a valid option, especially if you generate your levels from script, and don’t sculpt them in the editor by hand. The solutions I covered are basic ones, but only doing both together with a well thought out draw structure will ensure that the number of your draw calls stays low–using only atlases OR sorting layers is not enough in complex scenes.