Here are two small tricks that can help if you’re making an isometric 2D game in Unity. Ok, so not actually isometric, but that’s the term we’re used to in videogames, so we’ll go with it. These are quite basic and if you’re working on such a game you’ve probably already tackled them your own way. This is our take on it, hopefully it’s useful to someone.
Sprite Ordering
Normally in a 2D game there is no concept of depth, so if you simply place the sprites in the world, you’ll most likely have objects appearing in the wrong order than what you’d expect in an isometric game.
Thankfully Unity exposes Sorting Layer
and Order In Layer
properties for Renderers.
A quick fix is to set the value of Order in Layer
to depend on the Y position of the object.
[ExecuteInEditMode]
[RequireComponent(typeof(Renderer))]
public class DepthSortByY : MonoBehaviour
{
private const int IsometricRangePerYUnit = 100;
void Update()
{
Renderer renderer = GetComponent();
renderer.sortingOrder = -(int)(transform.position.y * IsometricRangePerYUnit);
}
}
This solves the problem for the simplest case, when we assume all objects rest on the ground.
Let’s assume we want to have an object that is above the ground in the world, like placing a bird house on that tree. Just trying to place it in the world will treat the pivot of the object as being at ground level, with no way to both place it at a proper height and sort it correctly.
There are several options for this. Just to get it out of the system, the first option is to add empty space to the texture below the bird house to make sure the pivot is at ground level (in Unity, the pivot can’t be outside of the sprite). This is baaaad! This is wasting texture space, and all instances of that object will need to be at the same height in the game. There are other, less insane, options.
One is having a height
property in the DepthSortByY
behavior and subtract it from transform.position.y
when computing the sorting order.
Another solution (which we went with) is allowing the DepthSortByY
behavior to make the depth computation based on another object’s transform. This way, the objects will be considered to be at the same point in space as their target and they’ll have the same depth order, even if they’re at different Y positions in the scene. In the bird house example, the bird house uses the tree’s world position for its depth computations.
This solution works better for our game, because it allows artists to move the item freely while staying at the depth (and not have to deal with editing the “height” parameter). And mainly because all the gameplay takes place in the ground’s 2D plane anyway so all objects are guaranteed to have a root object that has the ground position. In your own game, it might be easier to just use the first option.
[ExecuteInEditMode]
[RequireComponent(typeof(Renderer))]
public class IsometricObject : MonoBehaviour
{
private const int IsometricRangePerYUnit = 100;
[Tooltip("Will use this object to compute z-order")]
public Transform Target;
[Tooltip("Use this to offset the object slightly in front or behind the Target object")]
public int TargetOffset = 0;
void Update()
{
if (Target == null)
Target = transform;
Renderer renderer = GetComponent();
renderer.sortingOrder = -(int)(Target.position.y * IsometricRangePerYUnit) + TargetOffset;
}
}
This is how this example is set up in Unity:

And this is how it behaves in practice:
Ground Projection
For certain visual elements and effect, we wanted them to look properly projected on the ground, but also not spend too much time on making art for them. The ‘isometric’ view of the game means that anything that is horizontally on the ground should look squashed vertically.
For simple sprites, this is quite easy. Just draw them directly with the correct perspective and place them in the game.
Things get more complicated when you need something that should be able to rotate in any direction. Like something to show the direction the character is moving in, or some visual effect sweeping the ground towards your attacking direction. Especially if these are things that are animated, drawing them manually for all possible orientations is out of the question (or so the artists claim).
Our solution is: the artists draw and animate these effects as if viewed top-down, and the programmers take care of transforming them at runtime to look as if they were projected on the ground. Without any transformation, just taken from the artists and placed in the game rotating them to match the player’s direction they look like below.
We need to squash them vertically. For a sprite that doesn’t rotate, just scaling on the Y
dimension does the job. But for a rotating sprite this doesn’t work, and it’s even worse for animations. The first thing we tried was a custom shader that transformed the vertices in the local space to squash them vertically (naturally, we went with the most complex solution first), but this needed to break batching to work properly with all sprites and animations. Or I was just bad at writing that shader, maybe…
The final solution is absurdly simple. Just rotate the object around the X axis, and it works!
However, we also wanted to:
- apply the rotation automatically and consistently, and not have to remember or care about setting the X component of the rotation ourselves
- be able to set the
Z
component of the rotation (to make the effect rotate towards any game world direction) - not have to visit all ‘ground projected’ effects when changing the amount of squashing
Basically, the game should not have to know that a rotation on X axis is happening. If an object has the ProjectOnGround
behavior attached, it should just draw properly without additional effort. So we do the math just before rendering, and restore the rotation to its normal value right after. This hides the rotation around the X axis from the rest of the code.
[ExecuteInEditMode]
public class ProjectOnGround : MonoBehaviour
{
private Quaternion savedRotation;
// called before rendering the object
void OnWillRenderObject()
{
savedRotation = transform.rotation;
var eulers = transform.rotation.eulerAngles;
transform.rotation = Quaternion.Euler(Constants.Isometric.PerspectiveAngle, eulers.y, eulers.z);
}
//called right after rendering the object
void OnRenderObject()
{
transform.rotation = savedRotation;
}
}
Simple and easy. Too bad I wasted time trying to write a shader for this. The result looks good and we can simple ‘project’ any object by just adding this behavior to it.
34 Comments
halome123
December 17, 2016 - 11:22 amHey 🙂
What about background?
Im placing quad background behind the hero but he does sort with that background and above half of height of this background, hero is hiding behind that background 🙂
Im using MeshRenderer (Quad)
Catalin
December 19, 2016 - 2:56 pmThe background goes on a different Unity sorting layer. We have:
– ground (the base ground texture)
– groundDecals (grass patches, road decals)
– groundFX (special effects that appear in the horizontal plane)
– Default (all objects, sorted using the code in the above article)
– Weather (some of it)
– UI
This way, ground is always under the hero and other scene objects
halome123
December 20, 2016 - 1:18 pmHey Catalin. Thanks for the reply!
And thanks for that post, was helpful for me.
Cheers!
Brian
August 15, 2017 - 11:07 amHi. Great tips. I was wondering how is handled the collision with the ground?Where is the collider? THank YOU
Brian
January 18, 2017 - 6:38 pmThese tips are absolutely lovely, and very elegant. Thank you!
If you have any extra insight, I’d also love to see how one might go about approaching “platforms” in this type of isometric, 2D environment. That is to say; various sections of the ‘ground’ layer might differ in height, resulting in the player jumping or falling to reach them.
Specifically; if you wanted to incorporate levels with ‘uneven’ ground (ground, platforms and ramps of varying height):
* How would you deal with characters ‘jumping’ or ‘falling’ onto (or off of) each platform?
* What ‘gotchas’ (and workarounds) would you likely encounter, with multiple overlapping floors?
* How might you handle AI pathfinding in such a system?
Catalin
January 19, 2017 - 9:47 amHi Brian,
In case you have platforms, you’d need to track the “height” of each asset. As I mentioned at one point, you could have “a height property in the DepthSortByY behavior and subtract it from transform.position.y when computing the sorting order”. You’d need to track your object’s 3D position in your game logic and convert it to 2D space for sprite sorting and rendering.
I’m pretty sure this will lead to some extra edge cases and you’d need to take care of those, but this would be what I’d start with.
– Dealing with jumping and falling simply means keeping the position on the ground plane constant, and changing the vertical position.
– I’m pretty sure you’d find problems with platforms that seem to pass through one another as their height changes, since you don’t really account for volume. Right now, one of the issues we have even in 2D space is the sorting order of big objects. Since the sprite sorting of all the pixels in the object is computed based on it’s pivot, it’s easy to have sides of the object that should appear behind the character, but actually appear in front, due to the lack of volume and reducing the object’s volume to a single point for sorting order considerations. In this case, we simply use collision boxes to try and make sure the character doesn’t get close enough to the big object to make these artifacts visible.
– I’m not sure how I’d handle AI path-finding even in a simpler side-scroller that had platforms, so I have no idea off the top of my head how I would start handling this.
Overall, for the case of platforms and jumping/falling, I think the safest way would be to have actual 3D assets and the whole game space to be in 3D.
Brian
January 19, 2017 - 4:56 pmThanks, Catalin! Excellent points.
I stumbled across this post while looking for resources and strategies for approaching 2D Beat-Em-Ups. I had not heard of Yaga before, but I love the art style – it reminds me of Monkey Island. I look forward to following its progress.
As far as platforms go, I agree it sounds like there are two ideal options:
– Don’t use platforms at all
– If using platforms, switch to a 3D engine. This would of course bring some all new considerations and challenges, if attempting to mimic a 2D art engine.
Thanks again!
Jorge
February 14, 2017 - 8:28 pmHi, first of all, awesome work!
So i´ve been messing around with depth in 2D games and I`ve found this post right now, so when i tryed the first script Visual Studio gave me problems with – getComponent() – in the line ” Renderer renderer = GetComponent();”
How can i fix it? Thank you :3
Jorge
February 14, 2017 - 8:36 pmI have already tryed GetComponent(); and it doesnt work
Jorge
February 14, 2017 - 8:40 pmOkay, i´ve already fixxed it.
Sorry for the spam. 🙁
Catalin
February 15, 2017 - 8:52 amHey, don’t worry.
Yes, the HTML parser swallowed the type there. It should be GetComponent< Renderer >();
Thiago Obaid
May 1, 2017 - 5:09 pmThis was really helpful, great explanation on isometric sorting. Very well explained. Thank you so much!
Emanuele
May 1, 2017 - 9:53 pmThanks for this awesome tips! Just one question: what do you mean with “Constants.Isometric.PerspectiveAngle” as parameter for Quaternion.Euler in OnWillRenderObject() function?
Catalin ZZ
May 2, 2017 - 8:40 amThat’s just a constant value that you can tweat to get the perspective you want. Use use 60 degrees. You can try different values to see what suits you best.
Thomas
July 12, 2017 - 3:34 pmThis was very helpful, but I do have one question, the above code works perfectly when I apply it to an object with sprite rendering in the Unity program, however when I import a 2D map through ‘Tiled’ the objects turn into Mesh Renderer so when I try to apply the code, it delete’s all of the objects, I can’t figure it out, any help would be appreciated
Matt
August 10, 2017 - 4:27 pmThomas, are you using Sean’s Tiled to Unity code? If so, I’ve been thinking about this, have yet to implement but I think you will have to make his exporter/importer to not “merge” tiles together. I think you will need to split up all your isometric walls and objects into their own objects so that you can put Catalin’s code on each of them.
Alexey
August 28, 2017 - 5:03 pmGreat tips 🙂 thank you for sharing
Jerry Walters
January 15, 2018 - 10:03 amThanks for writing this up 🙂 I’m finding it useful a year later and it’s much appreciated!
Are you using tiles for Yaga? I’m wondering if this is all on a totally flat XY plane, or if you rotate this in Unity there’s space between the sprites in the Z direction?
I’m working on an isometric 2d game that takes place *mostly* on one ground surface, but there might be a couple of instances in the map where you climb stairs or a ladder to get up to higher ground. I was wondering if you’d recommend the same (3D) approach in that situation as you do in the comment above. I’m having a tough time visualizing isometric stair colliders in 2D space, or being behind a building you can walk on top of, but also splitting up my larger sprites to work with the isometric tiles that have depth in the Z axis seems like such a pain.
Jasper Lemmens
January 19, 2018 - 10:31 amHey there, I was just wondering in the case of going for Isometric perspective, how you’d go about handeling things like gates, or big buildings.
ckczm
April 23, 2018 - 8:34 pmThanks guys for this tips! They`re great + simple = genius 🙂
Andrew Pullins
July 5, 2019 - 11:29 pmSo how does this system work if for example another character or enemy is in front of the tree while your character moves behind the tree? A problem that I see with this system without testing if for myself is, if you are changing the z order of the tree relative to the player what is stopping the tree from moving over the other character when they are not supposed to?
Ben Roberts
July 2, 2020 - 8:18 pmGreat tips, many thanks for posting these!
leovar91
August 14, 2020 - 6:14 pmdude, great tips!!!! hope u upload more of this kind!!