# Tile Node Network

> 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.

- Skill: `vchelaru/tile-node-network` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/tile-node-network`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/tile-node-network/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/tile-node-network

---


# 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`).

```csharp
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

```csharp
// 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

```csharp
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.):

```csharp
node.Tag = MyTerrainType.Mud;
```

## Typical Enemy AI Pattern

Refresh the path on a timer (not every frame) to limit A* cost:

```csharp
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** — `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.

