Mapping game coordinates to a tilemap

Not every game that uses tilemaps has the game’s coordinate systems correspond exactly to the tiles. We can write a wrapper class around our tilemap to convert between tiles and world coordinates.

We will start with the generic implementation of a tilemap from a recent post. Next to a basic Tilemap<T> class we also created a Tile<T> type which represents a reference to a specific tile in a tilemap.

We will use that type, as well as plain coordinates to interact with the class we will create.

The purpose of this class is to make it as easy as possible for us to work with our tilemap. We would like to get the tile for any game world coordinates, as well as get the position of any tile, and possibly more.

Implementing spacial wrapper class

The basic implementation of our class will be relatively simple. It needs to know our tilemap, and we have tell it about the scale and offset to use for coordinate conversions.

The scale represents the size of each individual tile in game-world coordinates, and the offset determines where the tilemap is located with respect to the world’s origin.

In many games an offset of (0, 0) may be fine, but it sometimes makes things easier to think of the game world centered around the origin, extending equally in all directions. In that case we want to make sure the tilemap is centered around the origin as well.

Let us call the class TilemapSpacialWrapper<T> where T is the type of tile.

The class’ constructor could look something like this:

class TilemapSpacialWrapper<T>
{
    private readonly Tilemap<T> tilemap;
    private readonly Vector2 offset;
    private readonly float tileSize;
    private readonly float tileSizeInv;
    private readonly float tileSizeHalf;

    public TilemapSpacialWrapper(Tilemap<T> tilemap, Vector2 offset, float tileSize)
    {
        this.tilemap = tilemap;
        this.offset = offset;
        this.tileSize = tileSize;
        this.tileSizeInv = 1f / tileSize;
        this.tileSizeHalf = tileSize / 2f;
    }
}

I am already pre-calculating the half and the inverse tile size here, since we will need them later, and pre-computing them will speed up our eventual coordinate conversions.

We could easily add further constructors that could do some of the work for us. For example, we might prefer specifying the size of the level as well as the size of the tiles, and have the level class create the tilemap for us.

Let us assume we mean also center the world and tilemap around the origin, in which case the constructor might look as follows.

public TilemapSpacialWrapper(float worldWidth, float worldHeight, float tileSize)
{
    var tilesX = (int)Math.Ceil(worldWidth / tileSize);
    var tilesY = (int)Math.Ceil(worldHeight / tileSize);

    this.tilemap = new Tilemap<T>(tilesX, tilesY);

    this.offset = new Vector2(tilesX, tilesY) * (-tileSize / 2);

    this.tileSize = tileSize;
    this.tileSizeInv = 1f / tileSize;
    this.tileSizeHalf = tileSize / 2f;
}

Converting between coordinate systems

With this data in place, we can now write methods to convert between the coordinate systems.

Here are some ideas for functionality I would like to expose:

  • get the tile at a point in the world
  • get the center position of a tile
  • get the top left position of a tile

We can implement the first using indexers, and the other two using methods as follows.

public Tile<T> this[Vector2 point]
{
    get { /* return tile at point */ }
}

public Vector2 GetTileCenter(Tile<T> tile)
{
    /* get the center position of the tile */
}
public Vector2 GetTileTopLeft(Tile<T> tile)
{
    /* get top left position of the tile */
}

To be able to implement these, we need to be able to convert between the two coordinate systems. Since we are dealing with two axis aligned euclidean spaces, we can use a very simple scaling for this.

private void tileSpaceToPosition(float tx, float ty, out float x, out float y)
{
    x = tx * this.tileSize + this.offset.X;
    y = ty * this.tileSize + this.offset.Y;
}

private void positionToTileSpace(float x, float y, out float tx, out float ty)
{
    tx = (x - this.offset.X) * this.tileSizeInv;
    ty = (y - this.offset.Y) * this.tileSizeInv;
}

Using these two methods, we can create one more useful helper function that will call positionToTileSpace and convert the returned coordinates into proper tilemap coordinates (i.e. cast floating points coordinates to integers).

private void positionToTile(float x, float y, out int tx, out int ty)
{
    this.tileSpaceToPosition(x, y, out x, out y)
    tx = (int)x;
    ty = (int)y;
}

With these helpers in place, we can implement our indexer and two methods from above.

public Tile<T> this[Vector2 point]
{
    get
    {
        int x, y;
        this.positionToTile(point.X, point.Y, out x, out y);
        return new Tile<T>(this.tilemap, x, y);
    }
}

public Vector2 GetTileCenter(Tile<T> tile)
{
    return this.GetTileTopLeft(tile)
        + new Vector2(this.tileSizeHalf, this.tileSizeHalf);
}
public Vector2 GetTileTopLeft(Tile<T> tile)
{
    float x, y;
    tileSpaceToPosition(tile.X, tile.Y, out x, out y);
    return new Vector2(x, y);
}

These are the basic but essential operations this wrapper class helps us perform.

Conclusion

Today we have looked at how to convert between single direct coordinates and tile coordinates when working with a tilemap.

We can expand the implemented class further to include not only single-tile queries, but range queries that return all tiles in a certain area, like a rectangle, or circle, or all tiles intersecting a ray.

But those are topics for future posts.

Enjoy the pixels!

Leave a Reply