Tile Node Network (A* Pathfinding)
Key Types
FlatRedBall2.AI.TileNodeNetwork— the grid; builds and queries the A* graphFlatRedBall2.AI.TileNode— one cell; hasPosition,Tag,NeighborsFlatRedBall2.AI.DirectionalType—Four(cardinal only) orEight(+ diagonals)
Standard Setup
The standard FlatRedBall2 tile size is 16 units. Node origins are offset by half a tile from the TileShapes corner, so node centers fall at 8, 24, 40, … (i.e. tileCollection.X + 8, tileCollection.Y + 8).
var network = new TileNodeNetwork(
xOrigin: tilesOriginX + 8f, // center of first column
yOrigin: tilesOriginY + 8f, // center of bottom row
gridSpacing: 16f,
xCount: gridCols,
yCount: gridRows,
directionalType: DirectionalType.Eight);
network.FillCompletely();
// Remove nodes where walls are (call after all wall tiles are placed)
foreach (var (col, row) in wallCells)
network.RemoveAt(col, row);
// For 8-way: prevent squeezing diagonally through wall corners
network.EliminateCutCorners();
Getting a Path
// Allocating — returns a new List<Vector2>
List<Vector2> path = network.GetPath(startNode, endNode);
// Non-allocating — clears and fills an existing list; returns true if path found
bool found = network.GetPath(startNode, endNode, _path);
The start node is excluded; the end node is the last entry. Returns empty / false if no path exists.
Finding Nodes
TileNode? node = network.NodeAt(col, row); // by grid index, null if empty
TileNode? node = network.NodeAtWorld(worldX, worldY); // by world position, null if empty
TileNode? node = network.GetClosestNode(worldX, worldY); // nearest occupied node
Use GetClosestNode to find the start/end nodes from entity world positions.
TileNode.Tag
Attach game data to nodes (terrain type, waypoint markers, etc.):
node.Tag = MyTerrainType.Mud;
Typical Enemy AI Pattern
Refresh the path on a timer (not every frame) to limit A* cost:
private readonly List<Vector2> _path = new();
private int _waypointIndex;
private float _pathTimer;
void Activity(FrameTime time)
{
_pathTimer -= time.DeltaSeconds;
if (_pathTimer <= 0f)
{
var start = _network.GetClosestNode(X, Y);
var end = _network.GetClosestNode(_target.X, _target.Y);
if (start != null && end != null)
_network.GetPath(start, end, _path);
_waypointIndex = 0;
_pathTimer = 1f; // refresh once per second
}
// Advance past reached waypoints
while (_waypointIndex < _path.Count)
{
float dx = _path[_waypointIndex].X - X;
float dy = _path[_waypointIndex].Y - Y;
if (dx * dx + dy * dy <= WaypointRadius * WaypointRadius)
_waypointIndex++;
else break;
}
var dest = _waypointIndex < _path.Count
? _path[_waypointIndex]
: new Vector2(_target.X, _target.Y); // path exhausted — steer directly
// Apply velocity toward dest …
}
Gotchas
- A state is stored on nodes* —
GetPathresets all node state at the start of each call. Concurrent calls on the same network (same frame, multiple enemies) are fine because the game loop is single-threaded and each call resets before using. - Diagonal cost is √2 × gridSpacing naturally — node positions are used for distance, so diagonals cost more without any extra configuration.
RemoveAtunlinks neighbors — removing a node severs all its connections. You do not need to callEliminateCutCornersagain after individual removals.