home..

Creating Tile Prefabs in Unity

unity3d gamedev

Working with the Unity Tilemap

Working with Tiles in the Unity system can be a little frustrating. Unity is not a 2D engine, no matter what they try to tell new developers. Going into “2D” mode in the scene view is essentially just flattening the camera, it does nothing to your gameplay. The only 2D thing they really did was implement a seperate physics system for 2D objects, but everything still lives in 3D whenspace you do that, they just ignore the z-value. I am pretty sure you could get the same behaviour if you just locked the z-axis on all your 3D rigid bodies. Rant aside, this can get a little confusing when you are trying to work in 2d. When working with Tilemaps, they create a “Grid” that your tiles can live on for the Tilemap object, and this is a 2D space that lives on a 3D gameObject. When adjusting layering it can be tempting to adjust the position of the object, rather than setting the “Order in Layer” attribute on the Tilemap Renderer, and when you put in a character that probably doesn’t live on the tilemap, you need to figure out how to have them sort properly in the layer itself.

Solving the concept of “prefabs”

Often when working in a multi-layered tilemap scene, you need to create complex, repeatable objects that live across multiple layers. The Tile Pallette lets you select multiple tiles to place at once, and you can edit the tile palette as a tilemap, so you could just paint copies of your desired tiles into another part of the palette and just select and paint those together if you wanted just a simple shape on a single layer.

However, this doesn’t work if your object lives on multiple layers. For example what if you want a carpet beneath your table, and that carpet lives on the floor tile layer. You could select the floor layer, draw your saved carpet, then select the collidable layer and draw your table if you are doing everything manually, but this is still more steps than should be neccessary if that table appears on top of that carpet in many places. Then we get to my actualy use case: programatically setting tiles.

I wanted to create objects that could be placed programatically in the scene via script, and those objects would be dynamic. Maybe I put together a couple chairs and a table in a specific manner ,and I wanted to have multiple possible layouts of those tables around my bar. Essentially, I wanted a prefab instance of my object that lived in the Tilemap, and as far as I can tell Unity doesn’t provide this funcitonality so I would have to build it myself.

Defining the probem.

I can break down my problem into three different needs:

  1. I need a data structure that holds all the relevant data
  2. I need a way to edit/save these “prefabs”
  3. I need a way to easily input these “prefabs” into my scene at runtime.

Data Structures

My new Data strucutre needs to store the following information:

The Tile type we can store as a Tile or TileBase, which works fine as is. The Location is a Vector3Int, again easy to store. The Color is simple Color type. The transform matrix in unity is a Matrix4x4 type which is easy to store,

That just leaves us with the Tilemap layer. This is going to have to depend on your implmentation of layers, and have some specfic code written to get the layer you want. I am storing it as a int, and for me it is just the numerical representation of its order in the hierarchy, you could also use a string if you wanted. You just need to later have a way to turn this into an actual Tilemap object in your scene.

This gives us a small class that holds the neccessary information to recreate a tile:

[Serializable]
public class TileInfo : IComparable<TileInfo>
{
  public Vector3Int Position;
  public TileBase Tile;
  public int TilemapLayer;
  public Matrix4x4 Matrix;
  public Color Color;
}

Now we need something to store a collection of these TileInfos, and the standard Unity way to do this is with a ScriptableObject:

[CreateAssetMenu(fileName = "TilePrefabObject", menuName = "ScriptableObjects/TilePrefabObject", order = 1)]
public class TilePrefabObject : ScriptableObject
{
  public List<TileInfo> Tiles;
  public Texture2D Preview;
  public Guid Guid = Guid.Null;

  public void ClearTileInfo()
  {
    Tiles.Clear();
  }

  public void AddTileInfo(TileInfo info)
  {
    if (Tiles.Contains(info))
    {
      Logger.LogWarning(1, $"Trying to add an existing tileinfo");
    }
    else
    {
      Tiles.Add(info);
    }
  }

  public void CloneFrom(TilePrefabObject source)
  {
    if (source == null)
    {
      throw new System.ArgumentNullException("Source clone object cannot be null.");
    }
    // Create the tiles list and then add all the values from the source object.
    Tiles = new List<TileInfo>();
    foreach (TileInfo info in source.Tiles)
    {
      Tiles.Add(new TileInfo(info));
    }
    Preview = source.Preview;
  }

  public void OnSave()
  {
    if (Guid == Guid.Null)
    {
      // Double check we have a Guid here.
      Guid = Guid.NewGuid();
    }
  }
}

This stores the TileInfo objects, a unique Guid (not stricly neccessary, but can be useful when trying to save/load this during serialization for a larger object.) and a Texture2D preview (which we’ll get into later).

Editing/Saving Prefabs

Okay so we have the data structure for storing the data we need, but how do we actually create these? There were a couple options I thought of:

  1. Edit these prefabs in the main scene where you are making your game. This would have the lowest barrier to entry prefab wise, as you wouldn’t need to open up a different context in order to edit the prefab. However, the only way I could think of seperating out just the tiles you want would be to painstaking select them and then hit a button to collect them all.

  2. Edit the prefabs in a seperate scene devoted to prefab creation/editing. While this is less convenient in terms of flow, it does allow you to always have control over how the scene will be setup when you are editing these prefabs, allowing you to make assumptions about the layout.

I chose to create a seperate scene for this, one that I create on the fly so that I can control it all in code and link it to my Tilemap manager class.

Setting the scene

So lets first create an EditorWindow script to help us manage this process. First, we want to create the class itself, and have it setup our scene:

public class TilePrefabObjectEditor : EditorWindow
{
    private static TilePrefabObjectEditor _instance;

    [MenuItem("Custom Editors/TilePrefabObject Editor")]
    static void Init()
    {
		TilePrefabObjectEditor window = (TilePrefabObjectEditor)EditorWindow.GetWindow(typeof(TilePrefabObjectEditor));
        window.Show();
    }

    private Camera _camera;
    private Tilemap _tilemap;
    private BarPrefabObject _objectToEdit = null;
    private Dictionary<Tilemap, int> _mapToIndex;
    private Dictionary<int, Tilemap> _indexToMap;
    private bool _hasSetupScene = false;
    private bool _shouldSaveObject = false;
    private int _currentSceneType = -1;

    private const int TILE_PIXELS_PER_UNIT = 1024;
    private const int pixelsPerUnit = 16;
    private const int renderTextureWidth = 256;
    private const int renderTextureHeight = 256;

    private void SetupScene()
    {
	    _instance = this;
	    _hasSetupScene = true;
	    // Load up our prefab scene.
	    EditorSceneManager.OpenScene("Assets/Scenes/PrefabObjectEditing.unity");
	    _mapToIndex = new Dictionary<Tilemap, int>();
	    _indexToMap = new Dictionary<int, Tilemap>();
	    _camera = GameObject.Find("Main Camera").GetComponent<Camera>();
	    _camera.transform.localPosition = new Vector3(8f, 5.8f, -5f);
	    _camera.orthographic = true;
	    GameObject gridObject = GameObject.Find("Grid");
	    if (gridObject != null)
	    {
			GameObject.DestroyImmediate(gridObject);
	    }
	    // Setup the grids to match what we have in the scene.
	    Grid grid = new GameObject("Grid").AddComponent<Grid>();
	    foreach (MyTilemap.Layer layer in System.Enum.GetValues(typeof(MyTilemap.Layer)))
	    {
			GameObject obj = new GameObject($"Layer {layer}");
			obj.transform.parent = grid.transform;
			TilemapRenderer mapRenderer = obj.AddComponent<TilemapRenderer>();
			mapRenderer.sortingOrder = (int)layer;
			Tilemap tilemap = obj.GetComponent<Tilemap>();
			_mapToIndex[tilemap] = (int)layer;
			_indexToMap[(int)layer] = tilemap;
	    }
	    _hasSetupScene = true;
    }

    void OnGUI()
    {
        if (GUILayout.Button("Load Prefab Editor Scene"))
        {
	    _currentSceneType = 0;
            SetupScene();
        }
    }
}

This is a basic script that will get us into a state where we have a scene setup for editing tiles, with all of our desired Tilemap layers setup.

In this case. I am using an enum in MyTilemap.cs called Layer, and using that to recreate all of the layers that I have in my Tilemap manager class. This class Sits on the parent Grid.cs object to my Tilemaps, and handles grabbing and returning the Tilemap objects I ask for:

public class  MyTilemap : MonoBehaviour
{
    public enum Layer : int
    {
		FLOOR = 0,
		WALLS = 1,
		COLLIDABLE = 2,
		NPC_INTERACTABLE = 3,
		PLAYER_INTERACTABLE = 4,
		PREFABS = 5
    }

    public Tilemap GetTilemap(int layer)
    {
		if (layer < Tilemaps.Length)
		{
			return Tilemaps[layer];
		}
		throw new System.IndexOutOfRangeException($"No tilemap for index {layer}({layer})");
    }
}

Saving the prefab

Now we have a scene that we can easily swap to when we want to edit our prefabs, we can make our prefab in the new scene. Once its setup, we need to be able tosave it, so lets add some functions.

First we want to add the ability create a new TilePrefabObject.

void OnGUI()
{
....
	if (CommonEditor.ColoredButton("Create New Prefab", Color.green))
	{
		string path = EditorUtility.SaveFilePanel(
			"Choose where to save new file.",
			"Assets/Data/TilePrefabObjects",
			"NewTileObject",
			"asset");
		path = "Assets" + path.Replace(Application.dataPath, "");
		Logger.Log(1, $"Create new object at path: {path}");
		// Create our new asset at the desired path.
		TilePrefabObject newAsset = (TilePrefabObject)ScriptableObject.CreateInstance(typeof(TilePrefabObject));
		AssetDatabase.CreateAsset(newAsset, path);
		AssetDatabase.SaveAssets();
		AssetDatabase.Refresh();
		// Now load out object from the path we just set, and set it as our editing object.
		_objectToEdit = AssetDatabase.LoadAssetAtPath<TilePrefabObject>(path);
		_shouldSaveObject = true;
		if (_objectToEdit == null)
		{
			Logger.LogError(1, $"Failed to load the newly created prefab.");
		}
	}
....
}

Now lets add the ability to select one from our current Assets:

void OnGUI()
{
....
	GUILayout.BeginHorizontal();
	{
		GUILayout.Label("Bar Prefab Object");
		TilePrefabObject obj = (TilePrefabObject)EditorGUILayout.ObjectField(_objectToEdit, typeof(TilePrefabObject), false);
		if (obj != _objectToEdit && obj != null)
		{
			_objectToEdit = obj;
			LoadObjectIntoScene(_objectToEdit);
		}
		if (GUILayout.Button("X"))
		{
			_objectToEdit = null;
		}
		if (_objectToEdit == null)
		{
			CommonEditor.ColoredLabel("NULL", Color.red);
		}
	}
    GUILayout.EndHorizontal();
	....
}

And finally lets setup a function to clone the existing object, making it a bit easier to create different versions of something:

void OnGUI()
{
....
	// Handle Clone button.
	if (_objectToEdit != null)
	{
		if (CommonEditor.ColoredButton("Clone Current Prefab", Color.blue))
		{
			string path = EditorUtility.SaveFilePanel(
				"Choose where to save new file.", 
				"Assets/Data/TilePrefabObjects", 
				_objectToEdit.name,
				"asset");
			path = "Assets" + path.Replace(Application.dataPath, "");
			Logger.Log(1, $"Cloning current prefab to path: {path}");
			TilePrefabObject newAsset = (TilePrefabObject)ScriptableObject.CreateInstance(typeof(TilePrefabObject));
			newAsset.CloneFrom(_objectToEdit);
			AssetDatabase.CreateAsset(newAsset, path);
			newAsset.OnSave();
			AssetDatabase.SaveAssets();
			AssetDatabase.Refresh();
			// Load the newly cloned asset.
			_objectToEdit = AssetDatabase.LoadAssetAtPath<TilePrefabObject>(path);
			_shouldSaveObject = true;
		}
	}
	else if (CommonEditor.ColoredButton("Nothing to clone)", Color.gray)) {}
....
}

Finally, lets add a function that saves the prefab in the scene to our current _objectToEdit.

private void SaveTilesToPrefab(bool clearPrefabTilesFirst = true)
{
	if (_objectToEdit == null)
	{
	Logger.LogError(1, $"object is null, cant save.");
		return;
	}
	if (clearPrefabTilesFirst)
	{
		_objectToEdit.ClearTileInfo();
	}
	foreach(KeyValuePair<Tilemap, int> pair in _mapToIndex)
	{
		int layer = pair.Value;
		// Go through each tilemap and add all the tile data.
	    ForEachTile(
			pair.Key,
			(Vector3Int pos) =>
			{
				TileBase tile = pair.Key.GetTile(pos);
				if (tile != null)
				{
					// Grab the tile data and add it to the TilePrefabObject
					Matrix4x4 matrix = pair.Key.GetTransformMatrix(pos);
					Color color = pair.Key.GetColor(pos);
					TileInfo info = new TileInfo(tile, pos, layer, matrix, color);
					Logger.Log(1, $"Saved tile info: {info}");
					_objectToEdit.AddTileInfo(info);
				}
			});
	}
}

private static void ForEachTile(Tilemap map, System.Action<Vector3Int> tileAction)
{
	BoundsInt bounds = map.cellBounds;
	for (int i = 0; i < bounds.size.x; i++)
	{
		for (int j = 0; j < bounds.size.y; j++)
		{
			for (int k = 0; k < bounds.size.z; k++)
			{
				Vector3Int tilePos = new Vector3Int(bounds.min.x + i, bounds.min.y + j, bounds.min.z + k);
				tileAction?.Invoke(tilePos);
			}
		}
	}
}
void OnGUI()
{
....
	if (GUILayout.Button("Save Prefab"))
	{
		SaveTilesToPrefab(true);
	}
	if (GUILayout.Button("Add to Prefab"))
	{
		// Honestly not sure when I would ever use this, but its here....
		SaveTilesToPrefab(false);
	}
....
}

The ForEachTile function normally lives in a static helper class I wrote and published here

And huzzah! Now we have a scene and editor window that lets us save/edit our tile prefabs into ScriptableObjects.

Load Prefabs

Okay so now that we have these ScriptableObjects that store our tile prefab data, we need to somehow integrate it into our workflow.

Creating our Tile

I created a new kind of Tile that I called a PrefabTile that inhereits from TileBase.

using UnityEngine;
using UnityEngine.Tilemaps;
using System.Collections;

[CreateAssetMenu(fileName = "New Prefab Tile", menuName = "Tiles/Prefab Tile")]
public class BarPrefabTile : TileBase  // or TileBase or RuleTile or other
{
    public TilePrefabObject PrefabObject;
}

Now we can just right client in whatever folder we want our tile to be saved in, and create that new Tile asset. Then we just assign the TilePrefabObject we want into the PrefabObject field, and select a sprite to represent it.

Populating our prefab tiles.

We now will need to get our prefab actually populating in the scene, and to do that we are going to override the StartUp function.

private delegate void DelayedStartUp();
private event DelayedStartUp OnDelayedStartUp;

public override bool StartUp(Vector3Int location, ITilemap tilemap, GameObject go)
{
    base.StartUp(location, tilemap, go);
    if (!Application.isPlaying)
    {
	// Bail out on the custom startup behaviour if we are in editor mode.
        return true;
    }
	Tilemap map = tilemap.GetComponent<Tilemap>();
	// We want to check if there is an upgraded version of this tile that we should be using instead.
	MyTilemap myTilemap = map.transform.parent.GetComponent<MyTilemap>();
	if (myTilemap.CheckIfPrefabTileIsInvalid(this, location, map))
	{
	    // Bounce out if the MyTilemap says we shouldn't continue.
	    // Removing this tile will be done by the MyTilemap if it needs to.
	    return false;
	}
	// Tell our tilemap that we have a tile with a delayed start.
    MyTilemap.TilesWithDelayedStart++;
    // Move this startup code to the next frame so that the tilemaps are all setup to properly instantiate them.
    OnDelayedStartUp += () =>
	{
		// Find the MyTilemap in the parent.
		if (map != null)
		{
			if (Application.isPlaying)
			{
				// If we are running the game, then we want to automatically populate the MyTilemap with the prefab.
				MyTilemap myTilemap = map.transform.parent.GetComponent<MyTilemap>();
				if (myTilemap != null)
				{
					int totalTiles = 0;
					int setTiles = 0;
					foreach (TileInfo info in PrefabObject.Tiles)
					{
						totalTiles++;
						Vector3Int pos = location + info.Position;
						Tilemap layer = myTilemap.GetTilemap(info.TilemapLayer);
						if (layer != null && info != null)
						{
							Tile currentTile = layer.GetTile<Tile>(location);
							if (currentTile != null)
							{
								Debug.LogWarning($"Overwriting tile {currentTile} at {location} on layer: {info.TilemapLayer}.");
							}
							TileChangeData changeData = info.ChangeData;
							changeData.position = location + changeData.position;
							layer.SetTile(changeData, true);
							setTiles++;
						}
						else
						{
							Debug.LogError($"Failed to find layer {info.TilemapLayer} ({layer == null}) or info was null({info == null})");
						}
					}
					Debug.Log($"Set {setTiles}/{totalTiles} tiles");
				}
				// Now remove this prefab tile.
				map.SetTile(location, null);
			}
		}
		MyTilemap.TilesWithDelayedStart--;
	};
	RoutineOwner.Instance.StartCoroutine(RunOnNextFrame());
    return true;
}

private IEnumerator RunOnNextFrame()
{
    yield return null;
    OnDelayedStartUp?.Invoke();
	// Clear out the event list.
    OnDelayedStartUp = null;
}

This looks a bit complicated, but that was because of some issues I was running into with timing and things populated before everything else in the scene was setup and ready. I then discovered a different bug, and I just haven’t found the time to test removing my delay code to see if it works without it or not.

The core of this Startup is this trimmed down block here:

...
MyTilemap myTilemap = map.transform.parent.GetComponent<MyTilemap>();
if (myTilemap != null)
{
	foreach (TileInfo info in PrefabObject.Tiles)
	{
		Vector3Int pos = location + info.Position;
		Tilemap layer = myTilemap.GetTilemap(info.TilemapLayer);
		if (layer != null && info != null)
		{
			TileChangeData changeData = info.ChangeData;
			changeData.position = location + changeData.position;
			layer.SetTile(changeData, true);
		}
	}
}
// Now remove this prefab tile.
map.SetTile(location, null);
...

We go through each of the TileInfo instances stored in our tile and : 1) get the associate tilemap from our index with MyTilemap.GetTilemap(int index) 2) Get a TileChangeData object from the TileInfo 3) Use this TileChangeData to set the tile on the desired tilemap. 4) Remove the prefab tile from the map.

The TileChangeData isn’t something we talked about before, but its functionally just a struct contianing the position, color, transform and Tile (basically all the information our TileInfo stores). I just put an easy getter into the TileInfo class to get this:

public TileChangeData ChangeData =>
    new TileChangeData(
	    position: Position,
		 tile: Tile,
		 color: Color,
		 transform: Matrix);

And there we have it! We now have TilePrefabs. Stay tuned for an update where I add some extra things like generating a preview of prefab when we save it, as well as creating a simple texture with some text on it for us to use as our Tile sprite so we know whats going in. Also, using gizmos so that we know the shape that the prefab tiles will expand out to so we aren’t working completely blind.

© 2023 Michael Christensen-Calvin   •  Theme  Moonwalk