Skip to content

Tiles and Positions

This page explains how world positions map to tiles, and the RpgMapHelper and AutoTileMap methods that read tiles, write tiles and answer collision questions by position.

Concept

A map uses three kinds of coordinates.

Space Unit Where (0, 0) is
World Scene units The scene origin
Map-relative Scene units, measured from the map object's position The top-left corner of the map
Tile Whole tiles, int x and y The top-left tile

The map grows to the right and downwards from its GameObject's position. In tile coordinates x grows to the right and y grows down, so row 0 is the top row. In world space y grows up, which means every conversion flips the sign of y. Tile (x, y) covers the map-relative rectangle from x * CellSize.x to (x + 1) * CellSize.x across, and from -y * CellSize.y down to -(y + 1) * CellSize.y.

AutoTileMap.CellSize is the size of one tile in world units. It is shown as Cell Size in the map inspector. When the map first loads it is set from the tileset at 100 pixels per unit, so 32 pixel tiles give 0.32. You can change it, so never assume one tile is one unit. Always go through CellSize or the helpers below.

Some methods take a flat tile index instead of x and y. The index is x + y * MapTileWidth, so converting back is:

int x = tileIdx % AutoTileMap.Instance.MapTileWidth;
int y = tileIdx / AutoTileMap.Instance.MapTileWidth;

Warning

Keep the AutoTileMap object at position (0, 0, 0). The tiles are drawn by a separate object at the root of the scene, named after the map with Data on the end, which does not follow the map's transform. The position helpers do subtract the map's position, so after moving the map they no longer agree with what you see. Rotation and scale of the map object are ignored everywhere.

From a position to a tile

All of these take a world position and work on AutoTileMap.Instance.

Method Returns
RpgMapHelper.GetGridX(pos) The tile column. Negative left of the map, MapTileWidth or more right of it.
RpgMapHelper.GetGridY(pos) The tile row. Negative above the map, MapTileHeight or more below it.
RpgMapHelper.GetTileIdxByPosition(pos) The flat index x + y * MapTileWidth, or -1 left of or above the map.
RpgMapHelper.GetTileCenterPosition(pos) The centre of the tile under pos, relative to the map (see below).

GetGridX and GetGridY never clamp. Test the result before you use it:

AutoTileMap map = AutoTileMap.Instance;
int x = RpgMapHelper.GetGridX(position);
int y = RpgMapHelper.GetGridY(position);
if (map.IsValidAutoTilePos(x, y))
{
    // (x, y) is a tile of the map
}

GetTileIdxByPosition only checks the left and top edges. A position past the right edge gives the index of a tile in the next row, and one below the map gives an index past the end. Check the position with IsValidAutoTilePos as above before you trust the index.

A position that falls exactly on the line between two tiles belongs to the tile to its right or below. Floating point error can push such a value either way, so when you compute a position only to turn it back into a tile, use the tile centre rather than a corner.

From a tile to a position

RpgMapHelper.GetTileCenterPosition(tileX, tileY) returns the centre of a tile relative to the map, not in world space. Its doc comment says world, which is wrong. Add the map's position to get a world position:

AutoTileMap map = AutoTileMap.Instance;
Vector3 world = map.transform.position + RpgMapHelper.GetTileCenterPosition(10, 4);

With the map at the origin, as recommended above, the two are the same. The z of the result is 0, so set z yourself if your object sits at another depth:

Vector3 snapped = AutoTileMap.Instance.transform.position + RpgMapHelper.GetTileCenterPosition(transform.position);
snapped.z = transform.position.z;
transform.position = snapped;   // move this object to the centre of its tile

The mouse

RpgMapHelper.GetMouseWorldPosition() returns the world position under the mouse. It reads the mouse through RpgInput, so it works with the old Input Manager and with the Input System package alike (see Input).

It uses the camera set in View Camera on the map inspector (AutoTileMap.ViewCamera). If that field is empty, it is only filled in with the Main Camera when the in-game map editor starts, and until then the method throws a NullReferenceException. Assign the camera in the inspector. The result is only meaningful for an orthographic camera, which is what the demo scenes use.

using UnityEngine;
using CreativeSpore.RpgMapEditor;

public class ClickToPlaceTile : MonoBehaviour
{
    public int tileId = 16;
    public string layerName = "Ground";

    void Update()
    {
        if (RpgInput.GetMouseButtonDown(0))
        {
            int layer = AutoTileMap.Instance.MapLayers.FindIndex(l => l.Name == layerName);
            if (layer < 0)
                return;
            Vector3 mouse = RpgMapHelper.GetMouseWorldPosition();
            RpgMapHelper.SetAutoTileByPosition(mouse, tileId, layer);
        }
    }
}

Use RpgInput rather than UnityEngine.Input in your own scripts for the same reason.

Reading and writing tiles by position

RpgMapHelper.GetAutoTileByPosition(pos, iLayer) returns the AutoTile under a world position on one layer. RpgMapHelper.SetAutoTileByPosition(pos, tileId, iLayer) writes one, and a tileId of -1 erases. Writes outside the map are ignored. iLayer is an index into AutoTileMap.MapLayers: see Scripting.

GetAutoTileByPosition never returns null. Look at Id first:

Id Meaning
0 or more A painted tile.
-1 (AutoTileMap.k_emptyTileId) An empty cell.
-2 (AutoTileMap.k_outofboundsTileId) Outside the map, or a layer index past the end of MapLayers.

On a fog of war layer Id is not a tile id. It packs four fog alpha values, one per quarter of the tile.

The fields you would read on an AutoTile:

Field What it holds
Id The tile id, as above.
TileX, TileY The tile coordinates of the cell. Filled in for empty and outside cells too.
Layer The layer index the tile was painted on.
Type The eTileType: ANIMATED, GROUND, BUILDINGS, WALLS or NORMAL for the parts of an autotile sheet, OBJECTS for a plain tileset.
TilesetIdx The sub-tileset the tile comes from, Id / 256.

Type, TilesetIdx and Layer only mean something when Id is 0 or more. IsWaterTile() returns true for tiles from an animated autotile (the _A1 sheet), so check Id >= 0 before calling it.

A painted cell hands you the map's own object. Read it, but write through SetAutoTile or SetAutoTileByPosition, as changing Id directly does not update the tile.

Reading the tile under a character

using UnityEngine;
using CreativeSpore.RpgMapEditor;

public class GroundProbe : MonoBehaviour
{
    void Update()
    {
        AutoTileMap map = AutoTileMap.Instance;
        int ground = map.FindFirstLayerIdx(eLayerType.Ground);
        if (ground < 0)
            return;

        AutoTile tile = RpgMapHelper.GetAutoTileByPosition(transform.position, ground);
        if (tile.Id >= 0)
        {
            eTileCollisionType collision = map.Tileset.AutotileCollType[tile.Id];
            Debug.Log("Standing on tile " + tile.Id + " at " + tile.TileX + "," + tile.TileY + ", collision " + collision);
        }
    }
}

Collisions

Collision types belong to the tileset, not to the map, so every map that uses a tileset shares them. AutoTileMap.Instance.Tileset.AutotileCollType[tileId] gives the type you set for a tile in the tileset window. Tile Collisions describes each type.

To ask what is at a place on the map, where several layers may overlap, use the queries on AutoTileMap. Both go through the Ground layers starting from the last one in MapLayers (the bottom of the inspector list) and moving towards the first. They skip empty and OVERLAY tiles and return the collision of the first tile left. So a PASSABLE tile on a ground layer lower in the list makes a BLOCK tile on a layer above it in the list walkable. Layers of other types are never checked.

Method Answers
GetAutotileCollisionAtPosition(Vector3 worldPos) The collision at an exact point.
GetCellAutotileCollision(int tileX, int tileY) The collision type of a whole tile.

GetAutotileCollisionAtPosition is the one characters use. For WALL and FENCE tiles only part of the tile blocks, depending on how the autotile joins its neighbours, so the answer depends on where in the tile the point falls: it returns WALL or FENCE over the blocking part and PASSABLE over the rest. It returns PASSABLE outside the map, and for every point when Collision Enabled (IsCollisionEnabled) is off. Treat anything other than PASSABLE as blocked.

GetCellAutotileCollision returns the tile's type without looking inside the tile, and it ignores Collision Enabled. It also returns PASSABLE outside the map. Use it for grid logic, such as deciding whether a tile is worth targeting.

AutoTileMap map = AutoTileMap.Instance;
Vector3 next = transform.position + Vector3.right * map.CellSize.x;
if (map.GetAutotileCollisionAtPosition(next) == eTileCollisionType.PASSABLE)
{
    transform.position = next;   // the spot one tile to the right is free
}

Note

Neither query treats the map edge as a wall. If characters must not leave the map, paint BLOCK tiles along the border or clamp their position yourself.

Raycast

RpgMapHelper.Raycast(Ray2D ray, float distance, float precission = 0f) walks along a ray in steps and tests each point with GetAutotileCollisionAtPosition. It returns the distance of the first point that lands on a BLOCK tile, or -1 if none does within distance. The third parameter is spelled precission in the code, which matters if you pass it by name.

Know how it measures:

  • Only BLOCK stops it. WALL and FENCE tiles do not.
  • The step is precission, and 0 or less uses half a tile width (CellSize.x / 2). The result is always a whole number of steps, so the real edge of the blocking tile lies up to one step closer than the value returned.
  • A step can jump over the corner of a blocking tile. Use a smaller step when a thin gap matters.
  • A ray that starts inside a BLOCK tile returns 0.
  • Every call draws the tested segments with Debug.DrawLine, red on a hit and green otherwise. You see them in the Scene view, and in the Game view with Gizmos on.

The AI follower uses it as a line of sight test, and so can you:

bool CanSee(Vector3 from, Vector3 to)
{
    float dist = Vector2.Distance(from, to);
    float hit = RpgMapHelper.Raycast(new Ray2D(from, to - from), dist);
    return hit < 0f;   // -1 means nothing blocked the line
}

Tile sprites

RpgMapHelper.CreateTileSprite(tileId) makes a Sprite showing a tile, for an inventory icon or a build menu. For an autotile it shows the palette thumbnail. The pivot is the centre and the sprite uses 100 pixels per unit, so its size only matches the map when Cell Size is at its default.

Each call creates a new sprite. Create it once, keep it, and Destroy it when you are done. An id outside the tileset throws an exception, so pass only ids you know exist.

Fog of war

Two helpers clear fog around a point, on the first layer of type FogOfWar. Without such a layer they do nothing.

Method Effect
RpgMapHelper.RemoveFogOfWar(worldPos, sightLength) Clears the fog at once, in a soft-edged circle reaching sightLength tiles out from the tile under worldPos.
RpgMapHelper.RemoveFogOfWarWithFade(worldPos, sightLength) The same, but the fog fades out over the next frames. Play mode only.

The sample PlayerController calls RemoveFogOfWarWithFade each time the player enters a new tile, and a script of your own can do the same. Fog of War covers the layer and the demo scene.

Debug drawing

RpgMapHelper.DebugDrawRect(pos, rect, color) draws the outline of rect, offset by pos, with Debug.DrawLine. The rect is in world units with y up, and the lines sit at pos.z. An overload adds a duration in seconds. Like all debug lines, they show in the Scene view, and in the Game view with Gizmos on.

// Outline the tile under this object for one second.
AutoTileMap map = AutoTileMap.Instance;
Vector3 center = map.transform.position + RpgMapHelper.GetTileCenterPosition(transform.position);
Rect tileRect = new Rect(-map.CellSize.x / 2f, -map.CellSize.y / 2f, map.CellSize.x, map.CellSize.y);
RpgMapHelper.DebugDrawRect(center, tileRect, Color.yellow, 1f);