Pathfinding¶
This page covers tile pathfinding: the MapPathFindingBehaviour component that walks a character
to a target around blocking tiles, how it decides which tiles are walkable, and how to run a path
search from your own code.
Concept¶
The search runs over the map's tiles, not over physics colliders. Each tile is a node, and a character can step to any of its eight neighbours, or four if diagonals are off. The search is A*, and it runs a little at a time across frames, so a long search does not stall your game.
There are three layers of code, all in Scripts/MapPathFinding/:
| Type | Namespace | What it does |
|---|---|---|
PathFinding |
CreativeSpore.RpgMapEditor.PathFindingLib |
The A* search itself. Knows nothing about maps. |
MapPathFinding |
CreativeSpore.RpgMapEditor |
Runs the search on the tiles of AutoTileMap.Instance. |
MapPathFindingBehaviour |
CreativeSpore.RpgMapEditor |
A component that keeps a path to TargetPos up to date and steers the character along it. |
Most of the time you only touch the component.
How to use it¶
- Add
MapPathFindingBehaviourto the character. It needs aMovingBehaviouron the same object and adds one if there is none.MovingBehaviouris what moves the transform. - Set
Max SpeedandMax Forceon theMovingBehaviour. Both default to0.01, which is barely moving. The sample prefabs use values between0.5and1.Max Speedis in units per second. - From a script, set
TargetPosto the world position the character should go to.
using UnityEngine;
using CreativeSpore.RpgMapEditor;
[RequireComponent(typeof(MapPathFindingBehaviour))]
public class WalkToTile : MonoBehaviour
{
public int tileX = 10;
public int tileY = 5;
MapPathFindingBehaviour m_pathFinding;
void Start()
{
m_pathFinding = GetComponent<MapPathFindingBehaviour>();
m_pathFinding.OnComputedPath += HandlePathComputed;
GoTo(tileX, tileY);
}
public void GoTo(int x, int y)
{
AutoTileMap map = AutoTileMap.Instance;
// Only send the character to a tile of the map it can stand on.
if (!map.IsValidAutoTilePos(x, y) || map.GetCellAutotileCollision(x, y) != eTileCollisionType.PASSABLE)
return;
m_pathFinding.TargetPos = map.transform.position + RpgMapHelper.GetTileCenterPosition(x, y);
}
void HandlePathComputed(MapPathFindingBehaviour source)
{
if (source.Path.Count == 0)
Debug.Log(name + " cannot reach tile " + tileX + "," + tileY);
}
}
What the component does with that, every physics step (FixedUpdate):
- When
TargetPosmoves into a different tile, it starts a new search from scratch and drops the one in progress. Moving the target within the same tile does not restart anything. - While a search runs, the character keeps following the previous path.
- When the search finishes it replaces
Pathand skips ahead to the path tile after the one nearest the character, so the character does not walk backwards. Then it callsOnComputedPath. - It steers towards the centre of each tile on the path in turn. It moves on to the next tile
once the character is inside the current one and within
Min Dist To Move Next Targetof its centre. - It stops at the centre of the target tile, not at
TargetPositself. To stop on an exact point, take over once the character is in the target tile. The sampleTouchFollowBehaviourdoes this: it disables the pathfinding component and callsMovingBehaviour.Arrivedirectly.
The search itself does not check whether the target tile is walkable, so a target on a blocking
tile can produce a path that ends inside it. Test the target first, as GoTo does above.
Disabling the component stops it steering and stops new searches. The AI scripts use that to switch between wandering and chasing. See AI.
Note
MovingBehaviour moves the transform on its own and does not test tile collisions. The sample
FollowerAI also requires a PhysicCharBehaviour, the framework's tile collision component. Also note the path follows tile centres
in world space as if the map sat at the origin, so keep the map object at (0, 0, 0), as
Tiles and Positions recommends for every position helper.
Which tiles are walkable¶
A tile is walkable or not depending on the collision types set in the tileset
(Tile Collisions). For each tile the search looks through the
Ground layers, starting from the last one in MapLayers, and the first tile it finds decides:
| Collision of that tile | Walkable |
|---|---|
PASSABLE |
Yes |
WALL |
Yes, with the restriction below |
BLOCK or FENCE |
No |
OVERLAY or empty |
Keep looking in the next ground layer |
A tile with nothing on any ground layer is not walkable. Neither is anything outside the map.
Diagonal steps never cut corners. A diagonal move is only allowed when both tiles beside it, the horizontal and the vertical neighbour, are walkable too.
WALL tiles get a special rule for moving between tiles: a step between two neighbouring tiles is
allowed when they hold the same tile on a ground layer, and refused when one of them is a WALL
tile and the other is something else. So a path can run along a block of the same wall tile, but
cannot step onto or off it from other ground.
Warning
The path search and character collision do not agree on WALL tiles. The search treats them
as walkable under the rule above, while PhysicCharBehaviour blocks them. A path can
therefore lead a character into a wall it then refuses to enter. Where characters must go
around something, use BLOCK rather than WALL.
The search also treats the left and right edges of the map as joined: a tile in the first column counts the last column of the row above as a neighbour, and the other way round. Keep the first and last columns blocked in areas where characters use pathfinding.
The walkable test reads the map live, so tiles you change at runtime are taken into account by the next search.
Heuristics and cost¶
MapPathFinding.eHeuristicType sets how strongly the search heads for its goal:
| Value | Behaviour |
|---|---|
None |
Ignores the direction of the goal and spreads out evenly. It visits the most tiles and is the slowest. With no pull towards the goal it does not commit early to a greedy route, so its paths are usually the shortest of the three. |
Manhattan |
The default. Pushes hard towards the goal counting only straight steps, so it visits far fewer tiles. The route is not guaranteed to be the shortest. |
Diagonal |
Like Manhattan, but the distance estimate allows diagonal steps. |
The search does up to 10 steps per frame, and one step examines one tile and its neighbours. It
gives up after 8000 steps. On a big open map, a target that cannot be reached can therefore keep
a character's search busy for up to 800 frames, more than 13 seconds at 60 frames per second,
before OnComputedPath reports an empty path. A
target inside a small closed area fails quickly, because the search runs out of tiles.
The heuristic and diagonal setting live on the MapPathFinding object inside the component, in
the protected field m_mapPathFinding, and they are not shown in the inspector. To change them,
derive from the component:
using CreativeSpore.RpgMapEditor;
// Use this component instead of MapPathFindingBehaviour.
public class FourWayPathFinding : MapPathFindingBehaviour
{
void Awake()
{
m_mapPathFinding.AllowDiagonals = false;
m_mapPathFinding.HeuristicType = MapPathFinding.eHeuristicType.Diagonal;
}
}
The sample scripts look the component up with GetComponent<MapPathFindingBehaviour>(), which
finds a derived class too.
The demo scene¶
Open Samples/Scenes/demo5 - PathFinding and press Play. Move the player with the keyboard. The
DemoPathFinding object, which carries DemoPathFindingBehavior and a MovingBehaviour, searches
for a path from its own position to the player every time the player enters a new tile.
- Turn Gizmos on in the Game view to see the path, drawn as red lines.
MapPathFindingBehaviourdraws it every physics step. - The drop-down at the top switches the heuristic. Watch the red path change shape as you try
each one. The
Compute Time(ms)figure in the box under the buttons is the time between the last two finished searches, so it includes the wait for the player to change tile and is not the cost of one search. Enable Edit Mapopens the in-game map editor so you can paint walls and watch the path change.Enable Debug PathFindingbrings the path view back.- The mouse wheel zooms while the map editor is closed.
DemoPathFindingBehavior derives from MapPathFindingBehaviour and sets TargetPos to the
player's position in FixedUpdate, which is the whole pattern for a character that follows
another. Since 1.6.1 it is in the CreativeSpore.RpgMapEditor namespace like the rest of the
asset. The ZombiePlant and PlayerTouch prefabs in Samples/Prefabs use the same component
through FollowerAI and TouchFollowBehaviour, covered in AI.
Searching from your own code¶
To get a route without moving anything, for a movement range preview or turn based moves, use
MapPathFinding directly. GetRouteFromToAsync returns an IEnumerator to run inside a
coroutine. The last value it produces is a PathFinding.FindingParams, whose computedPath is
the route.
The route comes back starting at the second index and ending at the first. The component relies on this by passing the target first, and the example below does the same, so the list runs from the character to the target.
using System.Collections;
using UnityEngine;
using CreativeSpore.RpgMapEditor;
using CreativeSpore.RpgMapEditor.PathFindingLib;
public class RoutePreview : MonoBehaviour
{
MapPathFinding m_pathFinding = new MapPathFinding();
public IEnumerator ShowRoute(Vector3 target)
{
int fromIdx = RpgMapHelper.GetTileIdxByPosition(transform.position);
int toIdx = RpgMapHelper.GetTileIdxByPosition(target);
IEnumerator search = m_pathFinding.GetRouteFromToAsync(toIdx, fromIdx);
while (search.MoveNext())
yield return null;
PathFinding.FindingParams result = (PathFinding.FindingParams)search.Current;
foreach (IPathNode node in result.computedPath)
{
MapTileNode tile = (MapTileNode)node;
Debug.Log("Step to tile " + tile.TileX + "," + tile.TileY);
}
}
}
Start it with StartCoroutine(ShowRoute(target)). Things to know:
- The route includes the start and the target tiles. It is empty when there is no route, and has one tile when both indices are the same.
- Both indices must be inside the map. Check the positions with
IsValidAutoTilePosfirst (see Tiles and Positions). - Each
MapTileNodehasTileX,TileY,TileIdxandPosition, the tile centre in world space for a map at the origin. - Run one search at a time per
MapPathFindingobject, and let it finish. Give each character its own object, as the component does. - A
MapPathFindingobject caches tile nodes, including their positions. Create a new one after loading a different map or changingCell Size. - There is also a synchronous
GetRouteFromTo(startIdx, endIdx), which finishes the whole search before returning. It can freeze a frame on a large map, which is why the component uses the coroutine.
Reference¶
MapPathFindingBehaviour member |
What it does |
|---|---|
TargetPos |
World position to walk to. Shown as Target Pos. |
MinDistToMoveNextTarget |
Shown as Min Dist To Move Next Target. How close to a path tile's centre the character must get before it heads for the next one. Default 0.02 units. |
Path |
The current route as a LinkedList<IPathNode>, from the character's tile to the target tile. |
CurrentPathNode |
The path tile the character is heading for, or null when it has none. |
IsComputingPath |
True while a search is running. |
ClearPath() |
Forgets the current path and stops steering. The character keeps its current velocity, so also set MovingBehaviour.Veloc to zero if it should stand still. A new path is only computed when TargetPos enters another tile. |
OnComputedPath |
Delegate called when a search finishes. |
MapPathFinding member |
What it does |
|---|---|
HeuristicType |
None, Manhattan (default) or Diagonal. |
AllowDiagonals |
Allow diagonal steps. Default true. |
GetRouteFromToAsync(startIdx, endIdx) |
Search as a coroutine. The route runs from endIdx to startIdx. |
GetRouteFromTo(startIdx, endIdx) |
The same search, finished before it returns. |
IsComputing |
True while a search is running. |