Skip to main content

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 nameTiled typeRuntime use
bgtile layerGround/background visuals
propstile layerDecorative and non-solid props
props_collisiontile layerProps that are visually present and solid
collisionstile layerDedicated collision tiles (invisible in final art if desired)
transitionsobject layerZone triggers (rectangles with properties)
triggersobject layerGeneric gameplay triggers (event_id, trigger_kind, radius)

If these names change, the loader will skip them.

Required draw order (top to bottom in Tiled):

  1. bg
  2. props
  3. props_collision
  4. collisions
  5. transitions
  6. triggers

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)
  • firstgid values 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

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 visuals
  • props_collision: visuals that also block
  • collisions: invisible gameplay blockers and world boundaries

Practical rule:

  • If an object should feel physically present, paint it on props_collision.
  • Keep collisions mostly 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-220ms per frame
  • Fast current: 90-140ms per 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:

  • bg
  • props
  • props_collision
  • collisions
  • transitions
  • triggers
  1. Create or open a tileset in Tiled (.tsj) at 32x32 tile size.
  2. Keep one dedicated animated water tile with 3-4 frames.
  3. Create a map (.tmj) and add required layers in exact names/order.
  4. Paint terrain in bg, props in props, solid props in props_collision, blockers in collisions.
  5. Add transition rectangles in transitions with required properties.
  6. Add trigger rectangles in triggers for scene/cutscene/interaction events as needed.
  7. Export map as JSON (.tmj) into assets/maps/.
  8. 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 as some_event_id, cutscene_intro, scene_transition_01
  • trigger_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

  1. Duplicate a known-good map template.
  2. Block out walkable area and perimeter in collisions.
  3. Paint biome base in bg.
  4. Add props and solid props.
  5. Add scene transitions for doors/interiors/special maps.
  6. 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:

  1. In map A, add transition object pointing to map B and B's spawn.
  2. In map B, add transition object pointing to map A and A's spawn.
  3. Keep spawn points off collision tiles.

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, radius when 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_collision or collisions (not props).
  • Symptom: transition never fires
    • Check object is in transitions layer and has target_zone property.
  • Symptom: water not animating
    • Check tile has animation block in .tsj and gid range is unchanged.
  • Symptom: map fails to parse
    • Re-export as JSON map (.tmj) with external JSON tileset (.tsj).