I was recently working on a game, where a player could navigate around quite a big city. The city itself was collection of 2d sprites created in Adobe Illustrator, and exported as two layers:
- one background sprite,
- building sprites (separate for each building).
This is not very complex problem, and usually appropriate sprite ordering is satisfactory solution. I will describe simple sprite ordering method, so you will see that most of the cases can be solved this way. Most of the cases, that is – when building sprites are rectangular shaped and of similar size. But this time, building sizes and shapes varied a lot, and I had to deal also with dynamic objects, which caused extra issues.

So, first thing I did was asking the artist to export building positions as well. As he was using Adobe Illustrator, it was not a problem for him to do it. What I received was SVG (xml) file containing transformation matrix for each of the sprites.
<image overflow="visible" width="166" height="150" xlink:href="../city/35_hangar.png" transform="matrix(0.9999 0 0 0.9999 3236.022 2434.0176)">
It was more than enough to setup the scene. The scene consists of the following elements:
- background sprite at Z=0
- building sprites (with positions taken from Illustrator transformation matrix, and Z=-100
- camera (orthographic, 2d Toolkit camera positioned at Z=-1000)

When we take a closer look, we can see that some of the buildings overlap other buildings where they should not.

Well, all the buildings have the same Z position, so it is not a big surprise that such things happen ;). Ok, so first thing we will do is sorting the buildings so that the Z position of each building is based on its placement in 2d world (X, Y).
Sorting (ordering) the buildings
The easiest approach to think of is to sort Z positions of the objects, based on their distance from the bottom edge of the map. In other words – object that is closest to the bottom edge of the map is at the same time closest to the camera (Z1). The building that is farthest from the bottom edge of the map is at the same time farthest from the camera (Z4).

Assuming, that the sprite transform setup looks like this (that is – Z pointing the same direction as the camera, Y up):

The easiest way to do it would be to assign Y value to Z component.
Vector3 position = transform.position;
position.z = position.y;
transform.position = position;
It would not be enough though, as we also need to take the sprite height into account. Otherwise sprite vertical dimension would impact our sort order.

The code would look like this:
float halfHeight = SpriteReference.GetBounds().size.y / 2f;
Vector3 position = transform.position;
position.z = position.y – halfHeight;
transform.position = position;
We don’t need to call this code each frame, as the buildings will not move. So let’s call this code offline (in the editor).
Great! We have our buildings ordered, and now they display properly.

Let’s add dynamic agent to our city. The agent will move along the streets, and should hide behind the buildings and overlap them when necessary.

The agent position, unlike the buildings, will be calculated on the fly, so its Z coordinate will change, depending on its Y coordinate always when the agent moves:
if ( prevFramePosition != transform.position )
{
Vector3 position = transform.position;
position.z = position.y – halfHeight;
transform.position = position;
}
prevFramePosition = transform.position;
Let’s see how that works in real-life situation:

It seems that our sorting algorithm is doing good job for the agent, as it’s positioned quite well among the buildings in the city. Well, it would be too easy 😉 On first sight it may seem that everything is OK, but further experiments lead us to this situation:

Agent is being overlapped by this long building. Let’s take a closer look to sort order for these two objects.

The building boundary (blue rectangle) is so large that it contains the agent boundary (orange rectangle). Building distance from the bottom edge of the map is therefore smaller than agent distance. That is the reason that building appears closer to the camera than the agent.
In real 3d world, perspective would do the trick for us – as the building would not be perpendicular to the camera view vector. Instead, its Z position would change as we move along its local X axis. Because we do not have information about depth in 2d space, we have to do a trick to achieve similar effect.
Slicing the buildings (perspective trick)
What prevented us from changing Z-ordering of different parts of the building, was the fact that the building was one, indivisible object. Why not slicing the building into to smaller pieces, and treat each of the slice individually in terms of depth sorting?
2d Toolkit has a component called Clipped Sprite, which will help us here. It allows displaying clipped region of the sprite.

Now, when we have the building sliced, it is time to sort individual slices in the same manner I we did it earlier for the buildings. But how to determine the Z-order of individual slice? Slice Y coordinate is exactly the same as the building Y coordinate, so it is not possible to use the Y coordinate of the building to determine slice Z-order.
Let’s introduce new property, called “Slice Local Height”. Slice local height is defined as the distance (in pixels) from bottom edge of the sprite, to the first non-transparent area of the sprite.

Following code calculates local heights for building slices, for 10 pixel wide slices, with alpha threshold 0.7:
int SLICE_WIDTH = 10;
float ALPHA_TRESHOLD = 0.7f;
// Building sprite
Texture2D texture = AssetDatabase.LoadAssetAtPath ("sprite.png", typeof(Texture2D)) as Texture2D;
int SLICE_COUNT = texture.width / SLICE_WIDTH;
// Colors array
Color[] colors = texture.GetPixels();
// Local heights array
float[] localHeights = new float[SLICE_COUNT];
for (int i = 0; i < localHeights.Length; i++)
localHeights [i] = float.MaxValue;
// For each slice
for (int x = 0; x < SLICE_COUNT; x++)
{
for (int y = texture.height-1; y >= 0; y--)
{
int index = x * SLICE_WIDTH + y * texture.width;
Color c = colors[index];
if ( c.a > ALPHA_TRESHOLD )
{
// localHieghts contains pixel distance from the edge of the sprite
localHeights [x] = Mathf.Min(localHeights [x], y );
if (localHeights [x] == float.MaxValue ) localHeights [x] = 0f;
}
}
}
Array of local heights creates bottom boundary of the sprite, and from now on I will call it “Slice Local Height Function”.

Ok, but what do we need that for?
Slice Local Height is the property we want to use in depth ordering, as its value tells us how “deep” this slice should be, relative to building it belongs to.
So we want each slice Z coordinate to be directly proportional to Slice Local Height.
for (int segmentIndex = 0; segmentIndex < SLICE_COUNT; segmentIndex++)
{
Transform sliceTransform = transform.FindChild(string.Format("Slice{0}", segmentIndex));
sliceTransform.position += sliceTransform.forward * localHeights [ segmentIndex ];
}
But are the Unity3d world units compatible with Slice Local Height, which is measured in pixels? It is not a problem here, as 2d Toolkit does the job for us. Because we did not change default settings for Sprite Collection, we have default size set to 1 Pixel Per Meter (for more info: http://www.unikronsoftware.com/2dtoolkit/doc/2.10/tutorial/tk2dcamera.html ).
Here is the result of our experiment – the sliced building with ordered slices. I recorded short video, so you could see how it looks from different angles:
http://www.youtube.com/watch?v=udIEHHOHUlY
Ordered slices
As you can see, slices Z position is determined based on their local height. The building is still 2d sprite oriented to camera, but divided into smaller slices. Each of the slices has its own Z-order.
Let’s get back to our “agent under the building” situation.

As you can see, the building does not overlap our agent anymore. Again, here is a short movie to “visualize” this situation:
Optimization
We generated quite a few draw calls with all these slices. That’s OK, as Unity will batch them for us, as they share the texture atlas. Anyway lots of slices can still be pretty heavy, so you have to be careful not to slice every building in your game, as it could greatly impact your performance. In most cases, simple Z-Ordering without slices will be enough. The situation where you will need slicing are:
- buildings which sprites are very large – that is, buildings which bounding area would encapsulate your moving agent bounding area,
- buildings which shapes are not aligned to horizontal border AND your agent will move along this shape.
Examples of such sprites:
When you decide to use slices, you should carefully decide what slice width will be the best for your setup. Wide slices mean less performance impact and less precision in terms of overlapping, while narrow slices mean better precision and could lead to performance problems. The best way is to experiment, as slice width depends on many factors (how far the agent will be moving from your building, how large is the agent sprite, what is the slope of Slice Local Height Function etc.).
If your agent sprite is wide, or you use different widths to represent agents, you can also consider slicing agents. In that case, remember to perform Z-ordering of the slices offline.
General rules, that you should keep in mind when using slices:
- the less, the better; each slice adds new GameObject to the scene. Single GameObject would not cost you much, but lots of them will increase CPU and memory load. Static batching uses additional memory for batched objects.
- remember to mark all the slices as Static, and enable Static Batching in Player Settings (http://docs.unity3d.com/Documentation/Manual/class-PlayerSettings.html)
- do not move / rotate / scale the buildings in game, or they will not be batched (rather move the Camera).