If you're building a 2D game in Unity3D, odds are you've come across the TileMap component. The TileMap is a powerful tool that allows you to create a grid of tiles that you can render your tiles with instead of hand-placing individual game objects with sprites. It has a host of built in functionality that you might otherwise find yourself manually writing, like mapping coordinates to particular cells on a map. And what's even cooler about using a TileMap? You don't need to handroll your own editor to paint tiles! I think I'd pass on having to do that. But have you found yourself in a situation where you want to get all of the painted tiles on a TileMap? You may have found it's not quite as obvious as you'd have hoped!
What We Have To Work With On A TileMap
As I mentioned, a TileMap can do a lot for you. In particular, I found myself wanting to get all of the cells that have a Sprite associated with them and what that Sprite actually is. We can use the built in method GetSprite for this:
var cellPosition = new Vector3Int(x, y, 0);
var sprite = tilemap.GetSprite(cellPosition);
Simple enough. But how do we know which coordinates to use for x and y? Is it good enough to just go from 0 to a really high number on the x and y axes with two loops and hope it's good enough? We can do better than that! The TileMap class has a handy property on it called cellBounds. This gives us an integer rectangle that defines the bounds of all of the TileMap cells that have been modified:
tilemap.cellBounds
If you have found you're doing a lot of editing on a TileMap, you may be erasing sections to focus on another area. If that's common, you may benefit from calling CompressBounds() to shrink this back down to the currently assigned cells:
tilemap.CompressBounds()
And with all of these, we can approach how we might tie them all together to get what we need!
Be Careful With TileMap.cellBounds And Starting From Zero!
If you have a keen eye, you've probably realized that we want to use two loops to go through the cell bounds on the x and y axes to get the cells we're interested in on the TileMap. You're spot on! But there's something we should be careful about here!
It's an easy mistake to make because it's how we commonly write loops:
for (int x = 0; x < bounds.max.x; x++) { for (int y = 0; y < bounds.max.y; y++) { // do stuff } }
But what's wrong with this? The starting value! You need to be careful when working in 2D space like in Unity. There's nothing that actually guarantees your Tiles have been drawn starting from position zero on the TileMap and only going in a positive direction. In fact, in my game I had no tiles on the positive y axis! So a simple change to make sure you don't mess up is use the MINIMUM as your starting point:
for (int x = bounds.min.x; x < bounds.max.x; x++) { for (int y = bounds.min.y; y < bounds.max.y; y++) { // do stuff } }
Simple as that!
Wrapping It All Up
You're probably looking for a full-blown code snippet by now. Fair enough. Here's what I whipped up:
public static class TileMapExtensionMethods { public static IEnumerable GetAllTiles(this Tilemap tilemap) { // note: optionally call tilemap.CompressBounds() some time prior to this var bounds = tilemap.cellBounds; // loop over the bounds (from min to max) on both axes for (int x = bounds.min.x; x < bounds.max.x; x++) { for (int y = bounds.min.y; y < bounds.max.y; y++) { var cellPosition = new Vector3Int(x, y, 0); // get the sprite and tile object at the specified location var sprite = tilemap.GetSprite(cellPosition); var tile = tilemap.GetTile(cellPosition); // this is a sanity check that i've included to ensure we're only // looking at populated tiles. you can change this up! if (tile == null && sprite == null) { continue; }// create a data-transfer-object to yield back
var tileData = new TileData(x, y, sprite, tile);
yield return tileData;
}
}
}
}
public sealed class TileData { public TileData( int x, int y, Sprite sprite, TileBase tile) { X = x; Y = y; Sprite = sprite; Tile = tile; }
public int X { get; }
public int Y { get; }
public Sprite Sprite { get; }
public TileBase Tile { get; }
}
The above example provides a nice extension method to get you back a TileData instance for all the populated cells on your TileMap!