| name | tile-node-network |
| description | Tile Node Network in FlatRedBall2. Use when working with A* pathfinding, TileNodeNetwork, TileNode, enemy navigation, or grid-based pathfinding. Trigger on any pathfinding or enemy-follows-player question. |
Tile Node Network (A* Pathfinding)
Key Types
FlatRedBall2.AI.TileNodeNetwork — the grid; builds and queries the A* graph
FlatRedBall2.AI.TileNode — one cell; has Position, Tag, Neighbors
FlatRedBall2.AI.DirectionalType — Four (cardinal only) or Eight (+ 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,
yOrigin: tilesOriginY + 8f,
gridSpacing: 16f,
xCount: gridCols,
yCount: gridRows,
directionalType: DirectionalType.Eight);
network.FillCompletely();
foreach (var (col, row) in wallCells)
network.RemoveAt(col, row);
network.EliminateCutCorners();
Getting a Path
List<Vector2> path = network.GetPath(startNode, endNode);
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(worldX, worldY);
TileNode? node = network.GetClosestNode(worldX, worldY);
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;
}
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);
}
Gotchas
- A state is stored on nodes* —
GetPath resets 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.
RemoveAt unlinks neighbors — removing a node severs all its connections. You do not need to call EliminateCutCorners again after individual removals.