18
\$\begingroup\$

I have been trying a little bit the new TileMap system in Unity and I was looking for a way to access the tiles as a List or 2D array but I can't find anything other than GetTile(Vector3Int vector) which returns only one tile ... Is there any way to do this ?

\$\endgroup\$
0

5 Answers 5

29
\$\begingroup\$

To get an array with all tiles from a rectangular area of your tilemap, use tilemap.GetTilesBlock(BoundsInt bounds). You will get a one-dimensional array of tiles, so you need to know by yourself when the next row of tiles starts. Any empty cells will be represented with a null value.

If you want all tiles, use tilemap.cellBounds. This gets you a BoundsInt object which covers the complete used area of the tilemap. Here is an example script which gets all tiles from the Tilemap on the same game object and lists the tiles with their coordinates:

using UnityEngine;
using UnityEngine.Tilemaps;

public class TileTest : MonoBehaviour {
    void Start () {
        Tilemap tilemap = GetComponent<Tilemap>();

        BoundsInt bounds = tilemap.cellBounds;
        TileBase[] allTiles = tilemap.GetTilesBlock(bounds);

        for (int x = 0; x < bounds.size.x; x++) {
            for (int y = 0; y < bounds.size.y; y++) {
                TileBase tile = allTiles[x + y * bounds.size.x];
                if (tile != null) {
                    Debug.Log("x:" + x + " y:" + y + " tile:" + tile.name);
                } else {
                    Debug.Log("x:" + x + " y:" + y + " tile: (null)");
                }
            }
        }        
    }   
}

Regarding the bounds and why you might get more tiles than you expect: Conceptually, Unity Tilemaps have an unlimited size. The cellBounds grow as needed when you paint tiles, but they don't shrink again if you erase them. So when your game has a well-defined map size, you might get some surprises if you ever slip while editing maps. There are three ways to work around this issue:

\$\endgroup\$
0
18
\$\begingroup\$

Here is another way to do it with .cellBounds.allPositionsWithin

public Tilemap tilemap;
public List<Vector3> tileWorldLocations;

// Use this for initialization
void Start () {
    tileWorldLocations = new List<Vector3>();

    foreach (var pos in tilemap.cellBounds.allPositionsWithin)
    {   
        Vector3Int localPlace = new Vector3Int(pos.x, pos.y, pos.z);
        Vector3 place = tilemap.CellToWorld(localPlace);
        if (tilemap.HasTile(localPlace))
        {
            tileWorldLocations.Add(place);
        }
    }

    print(tileWorldLocations);
}
\$\endgroup\$
2
\$\begingroup\$

During figuring out how to get all custom tiles from Tilemap and due to ITilemap hasn't GetTilesBlock method which mentioned in answers I suggest to add an extension method like this (2D only):

public static class TilemapExtensions
{
    public static T[] GetTiles<T>(this Tilemap tilemap) where T : TileBase
    {
        List<T> tiles = new List<T>();
        
        for (int y = tilemap.origin.y; y < (tilemap.origin.y + tilemap.size.y); y++)
        {
            for (int x = tilemap.origin.x; x < (tilemap.origin.x + tilemap.size.x); x++)
            {
                T tile = tilemap.GetTile<T>(new Vector3Int(x, y, 0));
                if (tile != null)
                {
                    tiles.Add(tile);
                }
            }
        }
        return tiles.ToArray();
    }
}

In this case if you have, suppose, custom tile TileRoad, inherited from Tile or TileBase, then you can get all TileRoad tiles with call:

TileBase[] = tilemap.GetTiles<RoadTile>();

For ITilemap we can change parameter (this Tilemap tilemap) to (this ITilemap tilemap) (pay attention that second is Interface) but I didn't check that.

\$\endgroup\$
2
  • \$\begingroup\$ In what situation do you need to deal with the ITilemap interface? I didn't encounter it at all during my experiments with the Tilemap system. I always acquired the Tilemap component with GetComponent<Tilemap>(). \$\endgroup\$
    – Philipp
    Dec 1, 2017 at 14:21
  • \$\begingroup\$ ~Philipp, can't tell anything about interface, because I worked with Component only too. \$\endgroup\$ Dec 19, 2017 at 9:33
1
\$\begingroup\$

Using it this way to get a feeling about the bounds and to quickly get the tile based on the local position. Delete the print statements later on.

/* assuming 2d (ignoring z)*/
BoundsInt _area = tilemap.cellBounds;
TileBase[] _allTiles = tilemap.GetTilesBlock(_area); // okay, offset [0] is representing (Tile) of area.yMin,xMin
print($"CellBounds xMin={_area.xMin},yMin={_area.yMin},(zMin={_area.zMin}),xMax={_area.xMax},yMax={_area.yMax},(zMax={_area.zMax}),width={_area.size.x},height={_area.size.y}");
print($"CellBounds origin/center={_area.center.x},{_area.center.y},({_area.center.z})");
print($"GetTilesBlock size={_allTiles.Length}");
int _xDelay = Math.Abs(Convert.ToInt32(_area.center.x) - _area.xMin); // to be added to each local x position to ensure an x value between 0..area.size.x
int _yDelay = Math.Abs(Convert.ToInt32(_area.center.x) - _area.yMin); // to be added to each local y position to ensure an y value between 0..area.size.y
int _modulo = _area.size.x; // to be added or subtracted to the current offset position to keep the x position in the array but jump to the next or previous line of the array matrix.
foreach (var pos in _area.allPositionsWithin)
{
    Vector3Int localPlace = new Vector3Int(pos.x, pos.y, pos.z);
    if (tilemap.HasTile(localPlace))
    {
        int offset = (localPlace.y + _yDelay) * _modulo + localPlace.x + _xDelay;
        Tile tile = (Tile)_allTiles[offset];
        print($"x={localPlace.x}, y={localPlace.y}  ->  offset[{offset}]  ->  tile name={tile.name}");
    }
}
\$\endgroup\$
0
\$\begingroup\$

So drawing from the other answers here, and some other sources, and adding a bit more (as I needed more info on the tiles, so needed the position in the grid), I get the following three options.

To set up for any of the options:

TMBase = GameObject.Find("TileMapBase");

if (TMBase == null)
    return DoError("TileMap GameObject is null");

Tilemap tm = TMBase.GetComponent<Tilemap>();
if (tm == null)
    return DoError("TMBase Tilemap component is null");

Option 1 - read all the tiles in a batch call (note: the bounds being wrong, e.g. bounds from a different TileMap, can give incorrect gridPlace, so be careful with that - that's probably because bounds are in units of the grid's 'grid rectangles' which may have different sizes etc.):

if (resetBounds)
    tm.CompressBounds();

BoundsInt bounds = tm.cellBounds;
TileBase[] allTiles = tm.GetTilesBlock(bounds);

for (int x = 0; x < bounds.size.x; x++)
{
    for (int y = 0; y < bounds.size.y; y++)
    {
        TileBase tile = allTiles[x + y * bounds.size.x];

        if (tile != null)
        {
            // you may not need to find gridPlace, in which case cut next line
            Vector3Int gridPlace = new Vector3Int(
                x + bounds.xMin, y + bounds.yMin, bounds.z);

            // do something
        }
    }
}

Option 2a: Read the tiles one by one:

foreach (var pos in tm.cellBounds.allPositionsWithin)
{
    Vector3Int gridPlace = new Vector3Int(pos.x, pos.y, pos.z);
    TileBase tile = tm.GetTile(gridPlace);
    if (tile != null)
    {
        // do something
    }
}

Option 2b: Read the tiles one by one, checking if there is a tile

foreach (var pos in tm.cellBounds.allPositionsWithin)
{
    Vector3Int gridPlace = new Vector3Int(pos.x, pos.y, pos.z);
    if (tm.HasTile(gridPlace))
    {
        TileBase tile = tm.GetTile(gridPlace);
        // do something
    }
}

In my rather rough testing I found that Option 1 was the fastest, with Option 2a being anywhere from just over 0% to 5% slower, and Option 2b being 15-20% slower. Your mileage may very, please post in comments - and the difference between 2a and 2b may be less significant or even reversed if you had a lot of empty Grid spaces, I'm not sure and didn't test.

You can use gridPlace to get various extra info about the tiles, eg:

Vector3 worldPlace = tm.CellToWorld(gridPlace);
// use Tilemap.GetCellCenterWorld if you need
// the center of the tile instead of bottom left

or

Matrix4x4 m = tm.GetTransformMatrix(gridPlace);
Vector3 rotate = m.rotation.eulerAngles;

If you don't need that then you will not need to find gridPlace in Option 1, speeding it up a little more.

Thanks to everyone whose code I adapted.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .