"> ">

Isometric Grid Snapping in Unity 2D

blog/snapping_full.gif

When developing Terrenae we found building levels for it slow and error prone, because Unity doesn't have any tools for editing in 2D isometric. In order to allow our designer to rapidly create and tweak levels, I developed a script to snap to 2D isometric coordinates at edit time. There are two parts to setting up an editor script for custom snapping in Unity, both of which I will cover below.

blog/tile_dimensions.png

The first part is the snapping function itself. In order to convert between normal and isometric 2D grid coordinates, we need to know the tile dimensions as shown. These values are in scene units, and were obtained by dividing the dimensions of a tile by the pixels per unit of the tile sprite. As this is a shared value, it is recommended that all tiles are the same size in Units.

Vector3 tileSizeInUnits = new Vector3(1.0f, 0.5f, 0.5f);

In the game code the world is represented as a simple 2D array of tiles. The above dimensions are used to calculate the skew to transform the 2D grid into an isometric grid.

blog/grid_skew.png

With the tile size in units calculated, we can write a snapping function that snaps any game object to the isometric grid:

Vector3 Snap(Vector3 localPosition) {
        // Calculate ratios for simple grid snap
        float xx = Mathf.Round(localPosition.y / tileSizeInUnits.y - localPosition.x / tileSizeInUnits.x);
        float yy = Mathf.Round(localPosition.y / tileSizeInUnits.y + localPosition.x / tileSizeInUnits.x);

        // Calculate grid aligned position from current position
        Vector3 position;
        float x = (yy - xx) * 0.5f * tileSizeInUnits.x;
        float y = (yy + xx) * 0.5f * tileSizeInUnits.y;
        float z = 1.0f * position.y - 0.1f * position.x;

        return new Vector3(x,y,z);
}

X and Y are calculated using the pre-calculated values. Z is simply a function of X and Y, primarily tied to the Y axis, with adjustments based on X to ensure that tiles are correctly rendered on top when they are closer to the camera. This snap function could be swapped out for a different one if you were working with, say; Hex grids instead of Iso grids.

Now that we've done the maths to figure out WHERE we should put our tile, we need to make the editor use this Snap function when a tile object is moved in the editor. I attached this functionality within the Grid script as it manages the grid and so was a convenient place to calculate and store the tile size in units, plus, there are relatively few Grid objects in the scene; this reduces the amount of code execution at design time. Firstly we need to include the Unity editor commands:

using UnityEditor;

and enable our script to run during edit time:

[ExecuteInEditMode]
public class Grid : MonoBehaviour {
...

In order to track changes made in the editor window we need to subscribe to the SceneView delegate. It is best to do this in OnEnable and OnDisable, as this guarantees it will run as long as the Grid is enabled (note the use of "#if UNITY_EDITOR" to ensure that this code is only run at design time - we don't want it to run when the game is being played:

void OnEnable() {
    #if UNITY_EDITOR
      SceneView.onSceneGUIDelegate += OnSceneGui;
    #endif
}

void OnDisable() {
  #if UNITY_EDITOR
    SceneView.onSceneGUIDelegate -= OnSceneGui;
  #endif
}

The actual code to check if a tile has been moved is quite simple. We get a reference to the currently selected GameObject and check if the mouse has been released, if it has been, then snap the GameObject - obviously this results in false calls to Snap occasionally, but it is a simple check to make, and the cost of the Snap function is not that high.

Tile tile = activeObject.GetComponent<Tile>();
if (tile != null) {
    if (Event.current.type == EventType.mouseUp) {
        Snap(tile.gameObject);
    }
}

There you have it, a simple solution to custom grid snapping in Unity. This method was developed during Global Game Jam, so it is undoubtably not as efficient as it could be. I will post an update if I find a better way to do this in the future. If you have any queries or corrections, hit me up on twitter! Complete source listing below:

#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteInEditMode]
public class Grid : MonoBehaviour {
  Vector3 tileSizeInUnits = new Vector3(1.0f, 0.5f, 0.5f);

  Vector3 Snap(Vector3 localPosition) {
      // Calculate ratios for simple grid snap
      float xx = Mathf.Round(localPosition.y / tileSizeInUnits.y - localPosition.x / tileSizeInUnits.x);
      float yy = Mathf.Round(localPosition.y / tileSizeInUnits.y + localPosition.x / tileSizeInUnits.x);

      // Calculate grid aligned position from current position
      Vector3 position;
      float x = (yy - xx) * 0.5f * tileSizeInUnits.x;
      float y = (yy + xx) * 0.5f * tileSizeInUnits.y;
      float z = 1.0f * position.y - 0.1f * position.x;

      return new Vector3(x,y,z);
  }

  void OnEnable() {
      #if UNITY_EDITOR
        SceneView.onSceneGUIDelegate += OnSceneGui;
      #endif
  }

  void OnDisable() {
    #if UNITY_EDITOR
      SceneView.onSceneGUIDelegate -= OnSceneGui;
    #endif
  }

  #if UNITY_EDITOR
   void OnSceneGui(SceneView view) {
      Tile tile = activeObject.GetComponent<Tile>();
      if (tile != null) {
          if (Event.current.type == EventType.mouseUp) {
              Snap(tile.gameObject);
          }
      }
   }
}