Pathfinding Guide
intermediate tasksTerrain Grid System 2 · Common Tasks
Pathfinding Guide
Terrain Grid System 2 includes a built-in A* pathfinder that works with hexagonal, box, and irregular topologies. This guide explains the methods available, the main options you can tweak, and shows two common setups: standard two-way movement and a one-way connection between adjacent cells.
TGS. All examples assume a TerrainGridSystem tgs reference, typically obtained with TerrainGridSystem.instance or a serialized field.
Pathfinding Methods
TGS exposes several entry points for computing a path. Pick the one that matches how your game consumes the result.
| Method | Use When |
|---|---|
FindPath(int start, int end, ...) |
Synchronous. Returns a List<int> of cell indices, or null if no path is found. Fastest for short paths computed occasionally. |
FindPath(int start, int end, out float totalCost, ...) |
Same as above but also returns the accumulated cost of the path. |
FindPath(int start, int end, List<int> results, out float totalCost, ...) |
Fills a caller-provided list to avoid per-call allocations. Recommended when paths are recomputed every frame. |
FindPath(int start, int end, List<int> results, out float totalCost, FindPathOptions options) |
Overload that takes a FindPathOptions object. Use this when you need to customize masks, costs, or cross-cell callbacks per call. |
FindPathAsync(int start, int end, Action<FindPathAsyncResult> onComplete, FindPathOptions options = null) |
Runs A* on a background thread and invokes the callback when done. Use for long paths or large grids to avoid frame hitches. |
Task<FindPathAsyncResult> FindPathAsync(int start, int end, FindPathOptions options = null) |
Task-based variant compatible with async/await. |
TGSMultiGridPathFinder |
Separate helper class for computing paths that span multiple TerrainGridSystem instances joined by custom connections. See demo scene Demo33_MultiGrid_PathFinding. |
Global Settings
The following properties on the TerrainGridSystem component act as defaults for every call:
pathFindingHeuristicFormula- Manhattan, Euclidean, MaxDXDY, DiagonalShortCut. Affects path shape on box grids.pathFindingMaxSteps- hard cap on the number of cells evaluated.pathFindingMaxCost- hard cap on the accumulated cost (used whenmaxSearchCostis 0).pathFindingUseDiagonals- whether box grids can cut corners.pathFindingHeavyDiagonalsCost- multiplier applied to diagonal moves.pathFindingIncludeInvisibleCells- whether hidden cells participate in searches.pathFindingTurnCost- extra cost added each time the path changes direction (box grids only). 0 = disabled (default). A small value prefers straighter, fewer-turn paths among equal-cost routes; larger values avoid turns even at a higher movement cost. See demo scene Demo35_PathFinding_TurnCost.
FindPathOptions
When calling the overload that takes FindPathOptions, you can override per-call:
maxSearchCost,maxSteps- local overrides of the global caps.cellGroupMask,cellGroupMaskExactComparison- restrict which cell groups are walkable.canCrossCheckType- control how thecanCrossflag is evaluated on start/end cells (useful to let units leave an obstacle tile they already stand on).ignoreCellCosts- compute shortest-distance paths ignoring terrain costs.includeInvisibleCells- local override of the global flag.minClearance- require a minimum number of adjacent walkable cells (for wide units).gridBordersAsObstacles- when true, grid borders count as obstacles when computing clearance, so wide units (minClearance> 1) keep their distance from the grid edges. Defaultfalsetreats borders as open space so paths can reach and follow the edges.maxCellCrossCost- reject any cell whose total crossing cost exceeds this threshold. Very useful together with high side costs for hard blocks.OnPathFindingCrossCell- delegate invoked during the search to return an extra dynamic cost for a cell.
Cell-Level Walkability and Costs
Before calling FindPath, configure which cells and sides can be traversed. All setters live on the TerrainGridSystem component:
| Method | Purpose |
|---|---|
CellSetCanCross(int cellIndex, bool canCross) | Marks a cell as walkable or blocked entirely (both directions). |
CellSetCrossCost(int cellIndex, float cost, CELL_DIRECTION direction) | Sets a per-cell entering or exiting cost. Defaults to Entering. |
CellSetSideCrossCost(int cellIndex, CELL_SIDE side, float cost, CELL_DIRECTION direction) | Sets the cost of crossing one specific side of a cell. Supports directional costs. |
CellSetGroup(int cellIndex, int group) | Assigns a group used together with cellGroupMask to filter walkable cells. |
100000f) and pass a smaller maxCellCrossCost in FindPathOptions, or check the returned totalCost and reject the path if it exceeds your expected budget.
Use Case 1 - General Two-Way Pathfinding
This is the default setup: units can move freely in both directions, terrain types have different costs, and a few tiles are blocked as obstacles.
using UnityEngine;
using TGS;
public class BasicPathfinding : MonoBehaviour {
public TerrainGridSystem tgs;
public int startCell;
public int endCell;
readonly List<int> pathBuffer = new List<int>(64);
void Start() {
// 1. Mark water tiles as impassable.
foreach (int waterIndex in GetWaterCells()) {
tgs.CellSetCanCross(waterIndex, false);
}
// 2. Mark forest tiles with higher movement cost.
foreach (int forestIndex in GetForestCells()) {
tgs.CellSetCrossCost(forestIndex, 3f, CELL_DIRECTION.Any);
}
}
public bool TryMove() {
pathBuffer.Clear();
int steps = tgs.FindPath(startCell, endCell, pathBuffer, out float totalCost);
if (steps <= 0) {
Debug.Log("No path available");
return false;
}
Debug.Log($"Path found, {steps} cells, cost {totalCost}");
// pathBuffer[0] is the first step after startCell.
return true;
}
IEnumerable<int> GetWaterCells() { /* your logic */ yield break; }
IEnumerable<int> GetForestCells() { /* your logic */ yield break; }
}
Key points:
CellSetCanCross(false)blocks a cell symmetrically, both as a destination and as a transit tile.CellSetCrossCostwithCELL_DIRECTION.Anymakes terrain costs symmetric, which is what you want for normal movement.- Reusing the
pathBufferlist avoids allocations on every recomputation.
Use Case 2 - One-Way Connection (A → B, but not B → A)
Scenarios like ramps, ledges, escalators, portals, or locked gates need asymmetric movement: a unit can go through in one direction but not the opposite. TGS supports this through the CELL_DIRECTION parameter on CellSetSideCrossCost.
How It Works
The enum has three values:
CELL_DIRECTION.Exiting- the cost applies when leaving the given cell through the given side.CELL_DIRECTION.Entering- the cost applies when entering the given cell through the given side (internally this writes the cost on the opposite side of the neighbor cell).CELL_DIRECTION.Any- applies both entering and exiting costs. This is the default.
By setting a very high exiting cost in one direction only, you make the A* planner avoid that transition, while the opposite direction remains cheap.
Example
Assume two adjacent cells, aIndex and bIndex, and you want to allow movement from A to B but block it from B back to A.
using UnityEngine;
using TGS;
public class OneWayBridge : MonoBehaviour {
public TerrainGridSystem tgs;
public int aIndex;
public int bIndex;
const float BLOCKED = 100000f;
void Start() {
// Find the side of B that faces A.
if (!TryGetSideBetween(bIndex, aIndex, out CELL_SIDE sideBtoA)) {
Debug.LogWarning("Cells are not adjacent");
return;
}
// Block exiting B through the side that leads to A.
// A can still enter B from the opposite side because
// A's own exiting cost toward B is not touched.
tgs.CellSetSideCrossCost(bIndex, sideBtoA, BLOCKED, CELL_DIRECTION.Exiting);
}
bool TryGetSideBetween(int fromCell, int toCell, out CELL_SIDE side) {
for (int s = 0; s < 8; s++) {
CELL_SIDE candidate = (CELL_SIDE)s;
if (tgs.GetAdjacentCellCoordinates(fromCell, candidate, out int or, out int oc, out _)) {
if (tgs.CellGetIndex(or, oc) == toCell) {
side = candidate;
return true;
}
}
}
side = default;
return false;
}
}
For a longer chain like A → B → C allowed, C → B → A forbidden, repeat the same step at B (blocking exit toward A) and at C (blocking exit toward B).
Enforcing a Hard Block
A high cost alone still lets A* pick the forbidden side if no other route exists. Two options to make it truly impassable:
- Pass a
FindPathOptionswithmaxCellCrossCostset below the blocked value:var options = new FindPathOptions { maxCellCrossCost = 10000f // BLOCKED is 100000, so it never fits }; tgs.FindPath(startCell, endCell, pathBuffer, out float totalCost, options); - Or check
totalCostafter the call and reject paths whose cost exceeds your normal budget.
CellSetCanCross(cell, false) disables a cell in both directions, so it cannot be used to create a one-way link. Always use CellSetSideCrossCost with CELL_DIRECTION.Exiting or Entering for directional control.
See Also
- Pathfinding API - full signature reference.
- Cells API - cell properties, groups, and side methods.
- Demo scenes: Demo12_PathFinding, Demo14_TerrainPathFinding, Demo33_MultiGrid_PathFinding, Demo34_AsyncPathFinding, Demo35_PathFinding_TurnCost.
Suggest an improvement
Help us improve this documentation page.