| name | tmx |
| description | 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. |
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:
base.tmx → rename to the level name (e.g., Level1.tmx)
StandardTileset.tsx → copy alongside the .tmx
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:
<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 consumed 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 tile after spawning so it doesn't double-draw under the entity (opt out with removeSourceTiles: false). See the levels skill for the API.
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.
<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.
<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 AARects 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.
<tile id="20" type="SolidCollision">
<objectgroup draworder="index" id="2">
<object id="1" x="0" y="8" width="16" height="8"/>
</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 tile objects that represent entity spawn positions. Each tile object references a tile from the tileset via its GID. The tile's Class determines the entity type. Add an <objectgroup> after the tile layers:
<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".
<tileset firstgid="1025" source="MyArtTileset.tsx"/>