# Tmx

> TMX map file creation and editing for FlatRedBall2. Use when creating or modifying Tiled .tmx level files, placing collision tiles, resizing maps, or adding layers. Covers the base template, StandardTileset tile IDs, layer conventions, and CSV tile data format.

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

---


# TMX Map Files in FlatRedBall2

> **See `content-boundary` skill first.** TMX files are a human-edited content format — AI should scaffold a minimal valid TMX and tell the user to open it in Tiled for real level design. Do not try to author detailed levels in XML.

TMX files are the standard level format. A base template and standard tileset live in `.claude/templates/Tiled/`.

## Hot-reload — see `content-hot-reload` skill

`TileMap.TryReloadFrom(path)` applies tile-data changes (cell GID/flip changes) in place; structural changes (resize, layer set, tilesets, object layers) return `false` so the caller falls back to `RestartScreen(RestartMode.HotReload)`. Hand-authored mutations to a TSC after `GenerateCollisionFromClass` / `GenerateCollisionFromProperty` (e.g. `tsc.AddPolygonTileAtCell(...)` to extend the generated collision) are **wiped** on in-place reload — those live in `CustomInitialize`, which only re-runs on a full screen restart.

## Communication convention — reference tiles by numeric ID

When discussing a tile (its polygon, class, position, collision shape, etc.), always refer to it by its numeric **tile ID** — e.g., "tile ID 11's polygon" rather than "the slope triangle". The ID is the unambiguous handle that matches the `.tsx` file and Tiled's editor; descriptions alone are ambiguous when multiple tiles have similar shapes.

## Setup — Copying Template Files

When a game needs a TMX level, copy these three files into the game's content directory:

1. `base.tmx` → rename to the level name (e.g., `Level1.tmx`)
2. `StandardTileset.tsx` → copy alongside the `.tmx`
3. `StandardTilesetIcons.png` → copy alongside the `.tsx`

The `.tmx` references the `.tsx` by relative path (`source="StandardTileset.tsx"`), so they must be in the same directory.

## .csproj — Copy Tiled Files to Output

TMX files are loaded at runtime via `ParseFromFile`, not the content pipeline. All Tiled files must be copied to the output directory. Add this to the `.csproj`:

```xml
<ItemGroup>
  <Content Include="Content/Tiled/**" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```

This covers `.tmx`, `.tsx`, and `.png` files in the `Content/Tiled/` directory.

## Base Template

`base.tmx` provides:
- 30×30 map, 16×16 pixel tiles
- StandardTileset as the only tileset (firstgid="1")
- One layer: `GameplayLayer` — a walled room (solid border, empty interior)

## StandardTileset Tile IDs

Read `.claude/templates/Tiled/StandardTileset.tsx` for the full list of tile IDs and their Classes. The tileset's `firstgid` is 1, so **GID in CSV = tile id + 1**. GID 0 means empty (no tile). GID 1 is `SolidCollision` — used in virtually every game as the primary wall/floor/ceiling tile.

Tiles fall into two categories:
- **Collision tiles** (e.g., `SolidCollision`, `JumpThroughCollision`) — place on tile layers, consumed by `GenerateCollisionFromClass`. Hand-drawn rectangle/polygon `<object>`s on an object layer with a matching Class are matched too (see "Collision from an object layer" below) — useful for a boundary shape that doesn't align to the stamp grid.
- **Entity marker tiles** (e.g., `Coin`, `PlayerSpawn`, `Boss`, `BreakableCollision`) — place on object layers for precise per-entity placement, or paint them on regular tile layers for grid-aligned bulk spawns (e.g., rows of breakable bricks). Either way, `map.CreateEntities()` finds both and by default removes the source after spawning, so one Tiled source becomes exactly one runtime object (opt out with `removeSourceTiles: false`). Any object shape can be a marker, not just tile objects — a bare `<point>` or rectangle with a matching Class spawns too. See the `levels` skill for the API and the call-order rule.

## Layer Conventions

- **GameplayLayer** (required) — collision/gameplay tiles using StandardTileset. **All collision classes share this one layer.** Solid tiles, ladders, fences, jump-through platforms, spikes, etc. all go into `GameplayLayer` — do NOT create a separate layer per class (`LadderLayer`, `FenceLayer`, …). `GenerateCollisionFromClass("Ladder")` filters by tile Class within any layer it scans; splitting into per-class layers is redundant and breaks the convention games rely on.
- **Do not rename or remove `GameplayLayer`.** The base template ships with it already populated (a walled arena); game code and downstream tooling assume it's present.
- **The template's border walls are part of the scaffold — keep them.** Add new tiles inside the open interior, not on the border where solids already live. Stripping the walls is a silent way to break the "player doesn't fall off the world" guarantee the base template provides for free.
- Additional visual layers are game-specific. Add them above or below GameplayLayer as needed. Visual layers use a separate art tileset (not StandardTileset).

## CSV Tile Data Format

Tile data is CSV-encoded, row-major, left-to-right, top-to-bottom. Each row has `width` comma-separated GIDs. There are `height` rows total.

```
GID,GID,GID,...,GID,
GID,GID,GID,...,GID,
...
GID,GID,GID,...,GID
```

> **Gotcha:** Every row ends with a trailing comma EXCEPT the last row. Match this exactly.

## Common Operations

### Resize the map

Change `width` and `height` on both the `<map>` element and every `<layer>` element. Regenerate CSV data to match the new dimensions.

### Place tiles

Modify CSV values in the GameplayLayer. Row 0 is the top of the map. Use the GID table above.

### Add a visual layer

Add a new `<layer>` element. Increment the `id` and update `nextlayerid` on `<map>`. If the layer uses a different tileset, add a `<tileset>` element with the next available `firstgid`.

```xml
<layer id="2" name="Background" width="30" height="30">
 <data encoding="csv">
0,0,0,...
 </data>
</layer>
```

### Author a slope tile (polygon collision)

Slope collision is defined on a tileset tile via an `<objectgroup>` containing one `<polygon>`. `TileMapCollisions` converts any such polygon to a local-space `Polygon` prototype (centered on the cell, Y-up) and emits it via `TileShapes.AddPolygonTileAtCell` instead of a rect. For platformer floor slopes, set `SlopeMode = PlatformerFloor` on the **player's collision relationship** (not on the collection) — see the `collision-relationships` and `platformer-movement` skills.

```xml
<tile id="11" type="SolidCollision">
 <objectgroup draworder="index" id="2">
  <object id="1" x="0" y="0">
   <polygon points="0,0 16,16 0,16"/>
  </object>
 </objectgroup>
</tile>
```

Points are pixels in tile-local space, **Y-down** (Tiled's convention — origin at tile top-left, `y` increases downward). **FlatRedBall2 uses Y-up** in world space, so the two conventions disagree. You do NOT need to convert — `TileMapCollisions` does the Y-flip and cell-centering for you. Write polygons using Tiled's native coords. The example above is a lower-left triangle inside a 16×16 tile. `StandardTileset.tsx` ships with several slope tiles already authored this way (tile ids 11, 12, 13, 107).

**Polygon authoring — fill the cell, not just the surface.** A slope polygon needs the walking surface AND the solid mass below it, typically as a 4-point shape with the base on y=16 and a side on x=0 or x=16. Don't author a thin wedge triangle (e.g., `(0,12) (16,8) (0,16)`) — the walking edge `(0,12)→(16,8)` is correct but the hypotenuse back to the bottom-left leaves the bottom-right of the cell empty, so collision fails for anything approaching from below or from the right. For a gentle up-slope use a trapezoid like `(0,12) (16,8) (16,16) (0,16)`.

Tile flip flags (H, V, diagonal — the flip buttons in Tiled's editor, also applied when rotating a tile) are honored: a flipped or rotated slope tile emits the transformed polygon. You can author a single slope variant in the tileset and orient it freely in the map.

**Hand-authoring flipped tiles in CSV data** — Tiled's flip flags are packed into the high bits of the GID:

| Flip | Bit mask (hex) | Decimal |
|------|----------------|---------|
| Horizontal | `0x80000000` | `2147483648` |
| Vertical | `0x40000000` | `1073741824` |
| Diagonal | `0x20000000` | `536870912` |

OR the appropriate mask(s) with the base GID. Example: tile ID 11 (GID 12) flipped horizontally → `12 + 2147483648 = 2147483660`. If you author TMX through the Tiled editor, this encoding is automatic. If you hand-edit CSV data, you must OR the bits yourself.

### Sub-cell rectangles

A tileset tile can also carry one or more `<object>` elements with no child shape — plain Tiled rectangles. These emit as `AARect`s placed inside the cell (not the default full-cell rect). Use them for spikes, half-height platforms, or any box that doesn't fill the whole tile.

```xml
<tile id="20" type="SolidCollision">
 <objectgroup draworder="index" id="2">
  <object id="1" x="0" y="8" width="16" height="8"/>  <!-- bottom half of the tile -->
 </objectgroup>
</tile>
```

Multiple rects per tile are fine (e.g., two separate spike-rects). Rects and polygons can coexist on the same tile — both emit. Tile flip flags (H, V, diagonal) are honored for rects the same way they are for polygons.

Adjacent sub-cell rects participate in `SolidSides` seam suppression: if two sub-cell rects share an aligned, overlapping face (e.g., two bottom-half curbs in neighbor cells), or if a sub-cell rect's face aligns with a full-cell neighbor tile's face, the sub-cell rect's face is suppressed. When a sub-cell rect fully covers the adjacent full-cell tile's face (endpoints coincide), the matching face on the full-cell tile is suppressed as well, so a mover crossing the seam sees one clean surface. Partial coverage (e.g., a short spike next to a tall wall) leaves the full-cell face live so the exposed portion still repositions correctly. A sub-cell rect face that is fully covered by an axis-aligned polygon edge along the shared cell boundary (e.g., a slope polygon's vertical back-edge meeting a half-height rect) is also suppressed on the rect side so the mover doesn't snag at the seam; the polygon side is not modified.

**Current limitations:**
- `<ellipse>` and polyline collision objects are ignored — only `<polygon>` and plain `<object>` rectangles are honored.
- Only one `<polygon>` per tile is supported. Authoring a second polygon on the same tile throws `InvalidOperationException` at load time — merge the shapes into a single polygon in Tiled instead. (Rectangles have no such limit.)

### Collision from an object layer

`GenerateCollisionFromClass`/`GenerateCollisionFromProperty` (the all-layers form, i.e. no `layerName` argument) also match `<object>` rectangles and polygons on any object layer whose own Class/property matches — not just tile objects. Passing an explicit `layerName` restricts the scan to a single **tile** layer and skips object layers entirely.

- **Rectangles** support any position, size, and `rotation` that's an exact multiple of 90 — the rect stays axis-aligned (just reoriented/swapped) and is clipped against the grid, so it can span any number of cells and doesn't need to be grid-aligned at all. This is the recommended way to author a hand-drawn collision boundary that doesn't match the tile grid (a wall, a boss-arena floor, an irregular pit).
- **Rotated rectangles at other angles, and all polygons,** are converted to a polygon. One that fits within a single cell is positioned relative to whichever grid cell contains its center (same convention as a tileset tile's sub-cell `<object>`s). One whose bounds span multiple cells (a large diagonal wall, a wide slope) is registered as a free-floating shape instead, so it's still found by broad-phase collision queries regardless of which cell the querying shape occupies — no size limit. Tiled rotates around the object's own `(x,y)`, not its bounding-box center, so don't assume a rotated shape's world position matches a naive center-based mental model.

### Reading object-layer data directly (no entity, no collision)

`TileMap.GetObjectLayerData(layerName)` returns each object's world-space rect (or, for polygons, `ObjectLayerEntry.Points`), Class, tile GlobalId, and merged properties — for object layers used purely as data markers (a Water zone's bounds, Coast tile terrain/side properties) rather than `CreateEntities` spawns or collision generation. Rotation is ignored for every shape here; use `GenerateCollisionFromClass`/`GenerateCollisionFromProperty` if you need rotation-aware collision instead.

### Add an object layer for entity spawns

Object layers hold spawn markers. A tile object references a tile from the tileset via its GID and inherits that tile's Class; any other shape (`<point>`, `<ellipse>`, a bare rectangle, a `<polygon>`) needs the `class` attribute set on the `<object>` itself. Add an `<objectgroup>` after the tile layers:

```xml
<objectgroup id="2" name="Entities">
 <object id="1" gid="29" x="160" y="128" width="16" height="16"/>
 <object id="2" gid="30" x="32" y="320" width="16" height="16"/>
</objectgroup>
```

**Tile object positioning:** For tile objects, Tiled uses the **bottom-left** corner as the anchor, with Y-down from the map's top-left. The engine converts this to world space automatically. `width` and `height` should match the tile size (typically 16×16). Update `nextlayerid` and `nextobjectid` on `<map>` accordingly.

### Add a tileset

Add before the first `<layer>`. The `firstgid` must not overlap with existing tilesets. StandardTileset has 1024 tiles (firstgid=1), so the next tileset should use `firstgid="1025"`.

```xml
<tileset firstgid="1025" source="MyArtTileset.tsx"/>
```

