# Tile Grid

> Programmatic tile grid in FlatRedBall2. Use when building a code-driven grid at runtime (city builders, dungeon generators, puzzle games, cellular automata) — NOT loading a TMX map from Tiled. Covers the data/visual split, world↔tile coordinate conversion, per-tile visual spawning, and performance guidance for large grids (128×128+). Trigger on any question about tile coordinates, world-to-tile math, programmatic maps, or large grids without TMX files.

- Skill: `vchelaru/tile-grid` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/tile-grid`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/tile-grid/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-grid

---


# Programmatic Tile Grid

Use this skill when the grid is created at runtime in C#, not loaded from a `.tmx` file. Typical cases: city builders, procedural dungeons, puzzle games, cellular automata.

If you have a designer-authored level in Tiled, use the **levels** and **tmx** skills instead.

---

## Data / Visual Split (Required Pattern)

Keep the authoritative state in a C# array and the visuals as a separate layer. Never derive game logic from what's drawn on screen.

```csharp
// Data layer — the truth:
public enum TileType { Empty, Road, ZoneR, ZoneC, ZoneI, PowerPlant, WaterTower }

private TileType[,] _tiles;        // [col, row] indexed
private int _cols, _rows;
private float _tileSize;
private Vector2 _gridOrigin;       // world position of tile (0,0) center

// Visual layer — one ColoredRectangleRuntime per visible tile:
private ColoredRectangleRuntime?[,] _visuals;
```

Always update `_tiles` first, then sync the visual. Never infer tile type from the visual's color.

---

## Grid Setup

```csharp
_cols       = 128;
_rows       = 128;
_tileSize   = 16f;
// Center the grid at world origin:
_gridOrigin = new Vector2(-(_cols * _tileSize) / 2f + _tileSize / 2f,
                          -(_rows * _tileSize) / 2f + _tileSize / 2f);

_tiles   = new TileType[_cols, _rows];
_visuals = new ColoredRectangleRuntime[_cols, _rows];
```

`_gridOrigin` is the **center** of tile (0,0). Add `col * _tileSize` / `row * _tileSize` to get centers of other tiles.

---

## World ↔ Tile Coordinate Conversion

This is the most-called math in any tile game. Use `Math.Floor` — not `(int)` cast — to handle negative world coordinates correctly.

```csharp
// World position → tile indices (returns false if out of bounds):
bool WorldToTile(Vector2 worldPos, out int col, out int row)
{
    col = (int)Math.Floor((worldPos.X - _gridOrigin.X + _tileSize / 2f) / _tileSize);
    row = (int)Math.Floor((worldPos.Y - _gridOrigin.Y + _tileSize / 2f) / _tileSize);
    return col >= 0 && col < _cols && row >= 0 && row < _rows;
}

// Tile indices → world center position:
Vector2 TileToWorld(int col, int row)
    => new Vector2(_gridOrigin.X + col * _tileSize,
                   _gridOrigin.Y + row * _tileSize);
```

> **Gotcha:** `(int)(-0.3f)` rounds toward zero and gives `0`, but the tile at world X = -0.3 is tile `-1`. Always use `Math.Floor`.

---

## Spawning Tile Visuals

For grids up to ~4,000 tiles, spawn one `ColoredRectangleRuntime` per tile in Gum (screen-space; camera does not move these automatically). This is **Option B (canvas-space overlay)** and is separate from the world-space path described in the World ↔ Tile section above. It uses a dedicated `_uiGridOrigin` in canvas-space:

```csharp
private readonly Vector2 _uiGridOrigin = Vector2.Zero;

void SpawnVisual(int col, int row)
{
    var rect = new ColoredRectangleRuntime
    {
        Width  = _tileSize,
        Height = _tileSize,
    };
    // This snippet is canvas-space only (UI overlay grid).
    Add(rect); // screen-space — anchored to Gum canvas
    _visuals[col, row] = rect;
    SyncVisual(col, row);
}

void SyncVisual(int col, int row)
{
    var rect = _visuals[col, row];
    if (rect == null) return;
    // Canvas-space placement for Gum visuals.
    rect.X = _uiGridOrigin.X + col * _tileSize;
    rect.Y = _uiGridOrigin.Y + row * _tileSize;
    rect.Color = TileColor(_tiles[col, row]);
}
```

For very large grids (16,000+ tiles), spawn only the tiles visible in the canvas viewport and re-cull when your UI scroll offset changes. At 128×128 = 16,384 tiles, spawning all at once is slow but acceptable at startup (~500ms). Profile before optimizing.

If your grid lives in world-space instead, prefer world renderables (`AARect`/`Sprite`) so camera transforms are automatic.

---

## Updating a Tile

Always go data-first, then sync visual:

```csharp
void SetTile(int col, int row, TileType type)
{
    if (col < 0 || col >= _cols || row < 0 || row >= _rows) return;
    _tiles[col, row] = type;
    SyncVisual(col, row);
}
```

---

## Adjacency / Neighbor Iteration

```csharp
// 4-way neighbors (N/S/E/W):
static readonly (int dc, int dr)[] Cardinal = [(0,1),(0,-1),(1,0),(-1,0)];

bool HasRoadAccess(int col, int row)
{
    foreach (var (dc, dr) in Cardinal)
    {
        int nc = col + dc, nr = row + dr;
        if (nc >= 0 && nc < _cols && nr >= 0 && nr < _rows)
            if (_tiles[nc, nr] == TileType.Road)
                return true;
    }
    return false;
}
```

---

## Radius Queries (Power, Water Coverage)

```csharp
bool IsInRadius(int srcCol, int srcRow, int targetCol, int targetRow, float radius)
{
    float dx = (targetCol - srcCol) * _tileSize;
    float dy = (targetRow - srcRow) * _tileSize;
    return dx * dx + dy * dy <= radius * radius;
}
```

Use integer math (column/row distances) when possible — it avoids the square root and is significantly faster for large grids.

---

## Performance Notes

- **16,384 tiles (128×128):** Initial spawn is slow (~300–600ms for `ColoredRectangleRuntime` objects). Acceptable for city builders. If it's a problem, spawn only the visible viewport and lazy-spawn as camera pans.
- **Per-tick full-grid scan:** Scanning all 16,384 tiles each sim tick is fast (< 1ms). Do not premature-optimize. Only cache derived data (e.g., "powered tiles" bitset) if profiling reveals it's a bottleneck.
- **Do not use `TileShapes` for visual-only grids.** `TileShapes` is a collision structure, not a rendering structure. Use `ColoredRectangleRuntime` for programmer-art visual tiles.
- **Entity-per-tile is expensive at this scale.** Avoid spawning a full `Entity` subclass for each tile — the per-entity overhead (factory registration, physics update, etc.) at 16k tiles adds up. Use lightweight Gum visuals or `ColoredRectangleRuntime` instead.

