Skip to content

Pathfinding

Super Tilemap Editor includes a grid A* pathfinder that reads its walls straight from the tile colliders of a tilemap group. This page explains what it considers passable, how to set it up, and how to call it from your own code.

Concept

The code lives in Scripts/MapPathFinding and has three parts:

Type Role
MapPathFinding A serializable class you hold in a field. It owns the node grid and answers route requests.
MapTileNode One node per cell, created the first time a search reaches it. Decides whether its cell is passable.
PathfindingBehaviour A demo agent that walks to wherever you click. The Rogue Like sample scene uses it.

The algorithm itself is PathFinding in the CreativeSpore.SuperTilemapEditor.PathFindingLib namespace, and its node base class is IPathNode in the same namespace. IPathNode is a class despite the I in its name.

The grid covers a TilemapGroup, not a single tilemap, so a floor layer, a wall layer and a decoration layer are all taken into account.

What counts as passable

A cell is passable when both of these hold:

  • At least one tilemap in the group has Collider Type set to 2D or 3D and contains the cell inside its map bounds.
  • None of those tilemaps has a tile with a collider in that cell. A tile's collider is set in the Tile Properties window; eTileCollider.Full and Polygon both block, whatever the polygon's shape.

Two things follow. Tilemaps with Collider Type set to None are ignored entirely. And outside the bounds of every collider tilemap, cells are blocked, so the edge of the map works as a wall.

Passable Detection Mode adds a physics check on top of that:

Mode Effect
TileColliderCheck Only the tile collider test above. This is the default.
Raycast2D Also casts a 2D ray from each node to each neighbour, and blocks the move if it hits a collider on Raycast Detection Layer Mask. Triggers are ignored.
Raycast3D The same with 3D physics.

The tile collider test always runs, whichever mode you pick. The raycast modes are for obstacles that are not tiles, such as props, closed doors or other agents. If both raycast flags are set, only the 2D raycast runs.

Diagonal moves are only allowed when both orthogonal cells beside the diagonal are passable, so agents never cut a corner. With a raycast mode those flanking moves are raycast too.

How to use it

Scene setup

  1. Put your tilemaps under a TilemapGroup (GameObject > SuperTilemapEditor > TilemapGroup).
  2. Give the wall tiles a collider in the Tile Properties window, and set Collider Type on the wall tilemap to 2D or 3D.
  3. Make sure the walkable area is inside the map bounds of a tilemap with colliders. The wall tilemap usually covers it already. If it does not, set the floor tilemap's Collider Type too; floor tiles without a collider stay passable.
  4. Keep the group and its tilemaps at the world origin, with no rotation or scale. The node grid is laid out in world space from the origin, using the cell size of the first tilemap in the group, and the map bounds test reads that grid as tilemap cells.

To try it, add PathfindingBehaviour to a sprite, assign the group to Path Finding > Tilemap Group (left empty, it takes the first group it finds), enter play mode and click. It reads the mouse through the Input Manager, so it needs Active Input Handling set to Input Manager (Old) or Both.

From code

using System.Collections.Generic;
using UnityEngine;
using CreativeSpore.SuperTilemapEditor;
using CreativeSpore.SuperTilemapEditor.PathFindingLib;

public class Pathfinder : MonoBehaviour
{
    [SerializeField] private TilemapGroup m_tilemapGroup;

    private readonly MapPathFinding m_pathFinding = new MapPathFinding();
    private readonly List<Vector3> m_waypoints = new List<Vector3>();

    void Awake()
    {
        m_pathFinding.TilemapGroup = m_tilemapGroup;
        m_pathFinding.HeuristicType = MapPathFinding.eHeuristicType.Manhattan;
        m_pathFinding.AllowDiagonals = true;
    }

    // Returns the centres of the cells from start to end, both included.
    // An empty list means no path was found.
    public List<Vector3> FindPath(Vector2 startWorld, Vector2 endWorld)
    {
        m_waypoints.Clear();
        LinkedList<IPathNode> nodes = m_pathFinding.GetRouteFromTo(startWorld, endWorld);
        foreach (IPathNode node in nodes)
            m_waypoints.Add(node.Position);
        return m_waypoints;
    }

    // Passability of a single cell, with the same rules the search uses.
    public bool IsPassable(int gridX, int gridY)
    {
        return m_pathFinding.GetMapTileNode(gridX, gridY).IsPassable();
    }
}

GetRouteFromTo runs the whole search before it returns. Its result:

  • starts at the start cell and ends at the destination cell, one node per cell;
  • holds only the start node when the destination is blocked, or when start and destination are the same cell;
  • is empty when there is no route, or when MaxIterations ran out first. The second case also logs a warning.

Node Position is the centre of the cell in world space, which is what you steer an agent towards.

For long searches, GetRouteFromToAsync returns an IEnumerator that runs the search a few nodes at a time. It nests one enumerator inside another, so do not hand it to StartCoroutine directly. PathfindingBehaviour.UpdatePathAsync shows how to step it and pull out the finished list; copy that method rather than writing your own.

Passability is evaluated during each search, not cached, so tiles you paint or erase at runtime take effect on the next call. Nodes themselves are cached. ClearNodeDictionary() drops them if the map shrinks a lot and you want the memory back.

One MapPathFinding runs one search at a time. A second GetRouteFromTo while an async search is still running logs a warning and returns an empty list. Give each agent that searches in parallel its own instance.

Checking passability without the pathfinder

If you use another pathfinding package, the test it needs is the one above: does the cell hold a tile with a collider.

static bool IsBlocked(STETilemap tilemap, int gridX, int gridY)
{
    Tile tile = tilemap.GetTile(gridX, gridY);
    return tile != null && tile.collData.type != eTileCollider.None;
}

A raycast against edge colliders is a poor substitute. An EdgeCollider2D only has an outline, so a ray that starts inside a solid block of tiles hits nothing. Test the cell instead.

Reference

MapPathFinding fields, as the inspector shows them on PathfindingBehaviour:

Field What it does
Heuristic Type None, Manhattan or Diagonal. None explores the most cells. The other two explore fewer and can return a path that is not the shortest.
Max Distance Longest path allowed, in steps. 0 or below means no limit.
Passable Detection Mode See the table above.
Raycast Detection Layer Mask Layers the raycast modes test against.
Tilemap Group The group to search.
Cell Size Node spacing. Left at zero, it is taken from the first tilemap in the group.
Allow Diagonals Allows the eight way moves. Diagonal steps cost 1.414, straight steps 1.
Allow Blocked Destination Lets the destination be a blocked cell, for walking up to a chest or a door.

From code only:

Member What it does
MaxIterations How many nodes a search may expand before giving up. Default 8000.
IsComputing True while an async search is running.
GetMapTileNode(gridX, gridY), GetMapTileNode(worldPos) The node for a cell.

PathfindingBehaviour adds Compute Mode (Synchronous or Asynchronous), Async Coroutine Iterations, Moving Speed, Reach Node Distance, and an OnComputedPath delegate raised when an async search finishes.

Grid coordinates must stay within -32768 to 32767 on each axis, because a node's key packs both into one integer.