Tiled Pipeline
Faerals Phase 1 world editing is Tiled-first.
Maps are authored in Tiled and exported as JSON (.tmj), with external tilesets (.tsj).
This page is the source of truth for:
- Tilemap layer conventions
- Collision authoring
- Zone-to-zone linking
- Animated tile setup (water)
- Validation before commit
File layout
assets/maps/
verdant_brink.tmj
faerals_terrain.tsj
faerals_terrain_placeholder.png
Recommended naming:
- Map:
<zone_slug>.tmj - Tileset:
<theme_slug>.tsj - Tileset image:
<theme_slug>.png
Recommended folders:
- Maps:
assets/maps/** - Tilesets:
assets/tilesets/**
Using a dedicated tileset directory avoids path breakage when maps are nested in subfolders.
Required map layers
Layer names are part of the runtime contract.
| Layer name | Tiled type | Runtime use |
|---|---|---|
bg | tile layer | Ground/background visuals |
props | tile layer | Decorative and non-solid props |
props_collision | tile layer | Props that are visually present and solid |
collisions | tile layer | Dedicated collision tiles (invisible in final art if desired) |
transitions | object layer | Zone triggers (rectangles with properties) |
triggers | object layer | Generic gameplay triggers (event_id, trigger_kind, radius) |
If these names change, the loader will skip them.
Required draw order (top to bottom in Tiled):
bgpropsprops_collisioncollisionstransitionstriggers
transitions must stay as an object layer.
triggers must stay as an object layer.
Tileset contract
- Tile size:
32x32 - Orientation: orthogonal
- Encoding: JSON (
.tmj+ external.tsj) firstgidvalues in map must align with exported tileset order- For nested map folders, tileset resolution is path-resilient:
- preferred central lookup by tileset filename (
assets/tilesets/<name>.tsj) - compatibility fallbacks for legacy map-relative sources
- preferred central lookup by tileset filename (
For terrain sets, keep this stable pattern:
- Ground tiles
- Props tiles
- Solid prop tiles
- Collision marker tile(s)
- Animated water frame block
Stable ordering avoids accidental gid remapping bugs when artists iterate tilesets.
Collision conventions
props_collision and collisions create pixel-aware AABB blockers per occupied tile.
Player movement is reverted to the previous position when intersecting a blocker.
Use this split:
props: non-solid visualsprops_collision: visuals that also blockcollisions: invisible gameplay blockers and world boundaries
Practical rule:
- If an object should feel physically present, paint it on
props_collision. - Keep
collisionsmostly for map perimeter, cliffs, water edges, and invisible gameplay constraints.
Pixel-aware collision note
Runtime currently does tile-AABB collision against the player body rectangle (pixel coordinates). That means collision authoring should assume tile-cell granularity, but movement is continuous (not grid-locked).
Transition objects
In the transitions object layer, create rectangle objects for scene-style links and set:
link_type(string:transition)target_zone(string)target_map(string, map slug without.tmj)spawn_x(float)spawn_y(float)
transition behavior:
- Starts fade sequence then loads target map as scene root and spawns player at
spawn_x/spawn_y. - Use this for interiors, Sanctuaries, or special one-map contexts.
Smooth exploration links are not authored as Tiled transition objects. They come from Content Studio layout positions: adjacent maps are assembled in their registered relative positions, loaded when their bounds enter the viewport, and unloaded when they are no longer visible.
Runtime streaming uses viewport-proximity hysteresis:
- Load margin: maps are loaded slightly before entering view.
- Unload margin: maps are unloaded only when clearly outside view.
- This reduces boundary thrashing and lowers resource usage on wide layouts.
Runtime logs map streaming events:
stream load map='...' origin=(x,y) persistent=...stream unload map='...'
Additional authoring guidance:
- Rectangle should be at least
16x32. - Place trigger slightly before visual edge/door so crossing feels immediate.
- Spawn points should never land on collision tiles.
- Keep object names descriptive, e.g.
to_tidefen_crossing.
Animated tiles (water)
Animation is defined in the .tsj tileset using Tiled tile animations.
Example in faerals_terrain.tsj:
{
"id": 7,
"animation": [
{ "tileid": 7, "duration": 180 },
{ "tileid": 8, "duration": 180 },
{ "tileid": 9, "duration": 180 },
{ "tileid": 10, "duration": 180 }
]
}
At runtime, bevy updates animated tile GIDs every frame duration.
Current sample map uses water starting at GID 8 (with firstgid = 1).
Recommended timings:
- Calm water:
160-220msper frame - Fast current:
90-140msper frame
Avoid durations below 70ms for large water regions; it can look noisy.
How to build new maps
Quick scaffold command:
npm run map:new -- --name tempest_cliffs --width 96 --height 96
This creates assets/maps/tempest_cliffs.tmj with required layers pre-wired:
bgpropsprops_collisioncollisionstransitionstriggers
- Create or open a tileset in Tiled (
.tsj) at 32x32 tile size. - Keep one dedicated animated water tile with 3-4 frames.
- Create a map (
.tmj) and add required layers in exact names/order. - Paint terrain in
bg, props inprops, solid props inprops_collision, blockers incollisions. - Add transition rectangles in
transitionswith required properties. - Add trigger rectangles in
triggersfor scene/cutscene/interaction events as needed. - Export map as JSON (
.tmj) intoassets/maps/. - Point startup map path in client if testing another zone.
Trigger objects
In the triggers object layer, create rectangle objects and set:
event_id(string): identifier such assome_event_id,cutscene_intro,scene_transition_01trigger_kind(string):cutscene,scene_transition,interaction, etc.radius(float): activation distance relative to trigger center
Use triggers for non-zone-link gameplay events. Keep map-to-map links in transitions.
Content Studio workflow
tools/content-studio now supports:
- map tree edits (create folder, move/rename, delete)
- map attribute edits (name, width/height, tile width/height)
- opening selected maps directly in Tiled
- multi-layout canvas editing (grid snapping + zoom)
- auto adjacency metadata from aligned maps
CLI options:
node scripts/new_tiled_map_template.mjs --help
Suggested zone workflow
- Duplicate a known-good map template.
- Block out walkable area and perimeter in
collisions. - Paint biome base in
bg. - Add props and solid props.
- Add scene transitions for doors/interiors/special maps.
- Run local tests and manual playthrough.
Interconnecting maps
Use Content Studio layouts for smooth overworld/chunk links. Place maps next to each other on the layout canvas; runtime streams them at their registered relative positions.
Use transitions rectangles only for scene-style travel.
For bidirectional scene travel:
- In map A, add transition object pointing to map B and B's spawn.
- In map B, add transition object pointing to map A and A's spawn.
- Keep spawn points off collision tiles.
Link graph convention
Document links in PR description as:
verdant_brink -> tidefen_crossing (east gate)
tidefen_crossing -> verdant_brink (west gate)
This catches one-way links early.
Coordinate system reminder
Tiled object coordinates are map-space with origin at top-left. Runtime converts to centered world coordinates and inverts Y for Bevy. Always verify spawn points in-game after export.
Validation checklist
- Layer names match runtime contract exactly.
- Water tile has animation in
.tsj. - Transition objects have all 3 properties.
- Trigger objects include
event_id,trigger_kind,radiuswhen used. - Collision layers contain only blocking tiles.
- Map loads with
cargo test -p faerals-client --test tiled. - Full suite passes with
cargo test.
Troubleshooting
- Symptom: player walks through wall
- Check tile is painted in
props_collisionorcollisions(notprops).
- Check tile is painted in
- Symptom: transition never fires
- Check object is in
transitionslayer and hastarget_zoneproperty.
- Check object is in
- Symptom: water not animating
- Check tile has
animationblock in.tsjand gid range is unchanged.
- Check tile has
- Symptom: map fails to parse
- Re-export as JSON map (
.tmj) with external JSON tileset (.tsj).
- Re-export as JSON map (