Breadcrumbs

Two Unity tricks for isometric games

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:

The birdhouse references the tree for its depth.
The birdhouse references the tree for its depth.

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.

look at all that 'isometric' stuff on the ground

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.

17 Comments

  1. halome123

    December 17, 2016 - 11:22 am
    Reply

    Hey 🙂
    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 pm
      Reply

      The 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 pm
        Reply

        Hey Catalin. Thanks for the reply!
        And thanks for that post, was helpful for me.
        Cheers!

      • Brian

        August 15, 2017 - 11:07 am
        Reply

        Hi. Great tips. I was wondering how is handled the collision with the ground?Where is the collider? THank YOU

  2. Brian

    January 18, 2017 - 6:38 pm
    Reply

    These 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 am
      Reply

      Hi 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 pm
        Reply

        Thanks, 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!

  3. Jorge

    February 14, 2017 - 8:28 pm
    Reply

    Hi, 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 pm
      Reply

      I have already tryed GetComponent(); and it doesnt work

    • Jorge

      February 14, 2017 - 8:40 pm
      Reply

      Okay, i´ve already fixxed it.

      Sorry for the spam. 🙁

    • Catalin

      February 15, 2017 - 8:52 am
      Reply

      Hey, don’t worry.
      Yes, the HTML parser swallowed the type there. It should be GetComponent< Renderer >();

  4. Thiago Obaid

    May 1, 2017 - 5:09 pm
    Reply

    This was really helpful, great explanation on isometric sorting. Very well explained. Thank you so much!

  5. Emanuele

    May 1, 2017 - 9:53 pm
    Reply

    Thanks 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 am
      Reply

      That’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.

  6. Thomas

    July 12, 2017 - 3:34 pm
    Reply

    This 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 pm
      Reply

      Thomas, 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.

  7. Alexey

    August 28, 2017 - 5:03 pm
    Reply

    Great tips 🙂 thank you for sharing

Leave a Reply