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
BLOCKstops it.WALLandFENCEtiles do not. - The step is
precission, and0or 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
BLOCKtile returns0. - 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);