Godot 4: turn TileMap into TileMapLayer — without opening the editor

Paste a .tscn. Every TileMap in it becomes a Node2D with one TileMapLayer child per layer, the cells are re-encoded into the new byte format, and every block the converter does not own comes back byte for byte. Nothing is uploaded — this runs in your browser.

The example is 2d/hexagonal_map/map.tscn from Godot's own demo projects, branch 4.2 — 159 cells, unmodified.

Back up the original first. Open the project in Godot 4.3+ afterwards and save once, so the engine writes its own uid bookkeeping.

Why this exists when the editor has a button

Godot 4.3 deprecated TileMap and put a TileMap → TileMapLayer extraction in the inspector. It works, and if you have three scenes you should use it. What it is not:

The conversion itself is a text transform on a text file. That is why it can also be a script — and, more importantly, why it can be checked: run the transform, then let a real Godot read the before and the after cell by cell and say whether they hold the same tiles.

What actually changes in the file

The two shapes are not the same bytes, so this is a re-encode, not a copy. A TileMap keeps every layer inside one node, as numbered properties:

[node name="TileMap" type="TileMap" parent="."]
tile_set = ExtResource("1")
format = 2
layer_0/name = "Ground"
layer_0/tile_data = PackedInt32Array(-458747, 0, 0, -458746, 0, 0, ...)

A TileMapLayer is a node, so each layer leaves the property list and becomes a sibling:

[node name="TileMap" type="Node2D" parent="."]

[node name="Ground" type="TileMapLayer" parent="TileMap"]
tile_set = ExtResource("1")
tile_map_data = PackedByteArray(0, 0, 5, 249, 249, 255, 0, 0, 0, 0, 0, 0, ...)
on the old nodeon the new nodewhat happens
layer_N/tile_datatile_map_data3 int32 per cell → 2-byte header + 12 bytes per cell. Re-encoded, field by field.
layer_N/namethe node's nameBlank names become LayerN; . : @ % / " are replaced, because a node name cannot hold them.
layer_N/enabled, modulate, y_sort_enabled, y_sort_origin, z_index, navigation_enabledsame name, per layerCopied verbatim onto the layer that owned them.
tile_set, rendering_quadrant_size, collision_visibility_mode, navigation_visibility_modesame name, on every layerThey belong to tile data, and a Node2D cannot keep them — so they are pushed down to each layer.
collision_animatableuse_kinematic_bodiesRenamed by the engine in 4.3.
format = 2The TileMap's own on-disk version counter. A Node2D has none, so it is dropped.
everything elseunchangedTransform, visibility, groups, material, the other nodes, the sub-resources: re-emitted byte for byte.

Erased cells shrink the file. erase_cell() does not remove the record — it writes a tombstone with source id 0xffff, and the engine keeps it in the file. The converted layer drops them, and the tool tells you how many, because "my file got smaller" should never be a surprise. What the engine reads back is identical either way; that is one of the things the checks below assert.

What it refuses to convert, and why that is the point

A converter that half-understands a scene and rewrites it anyway corrupts a project quietly, and you find out three weeks later. Every case below stops that node, reports it, and leaves the file exactly as it was:

situationwhy it is refused
the node carries a scriptA script that extends TileMap cannot extend the Node2D the node becomes. Port the script to TileMapLayer first — it is the one case where a human has to decide something.
an unknown layer_N/ keyAnything not in the table above would be silently dropped. A newer Godot adding a per-layer property is exactly the case this catches.
format is not 2Only the value this has been checked against is accepted. Open the scene once in a 4.2–4.4 editor to let the engine upgrade it, then convert.
the layer name is already a sibling's nameTwo children with one name is not a tree Godot can hold. You rename one; the tool will not pick for you.
two layers with the same nameSame reason.
layer_N/ overrides on a node with no typeThe scene inherits or instances another one, and the real TileMap lives in that other file. Convert the base scene first.
tile_data that is not a whole number of cellsThree int32 per cell. A remainder means the file is not what this parser thinks it is.

And one last gate that runs on every scene, even a clean one: after the rewrite, the converter checks that every block it does not own is still present, character for character. If a single untouched node came back different, nothing is written and it says so.

What was measured, and against what

The engine reads both versions back

A fixture scene is written by a real Godot, converted on the command line, and read back by a real Godot, which compares cell by cell: coordinates, source id, atlas coordinates, alternative id, and every per-layer property against the engine's own property list. docs/verify_tilemap_convert.sh runs it, and it printed this today:

### same-version pass — 4.3.stable.official.77dcf97d8
TILEMAP CONVERT VERIFY: 160 checks, 0 failed   ALL PASS
### same-version pass — 4.4.stable.official.4c311cbee
TILEMAP CONVERT VERIFY: 160 checks, 0 failed   ALL PASS
### same-version pass — 4.7.stable.official.5b4e0cb0f
TILEMAP CONVERT VERIFY: 160 checks, 0 failed   ALL PASS
### cross-version pass — scene written by 4.2.stable.official.46dc27791,
###   converted on the command line, read back by 4.7.stable.official.5b4e0cb0f
TILEMAP CONVERT VERIFY: 160 checks, 0 failed   ALL PASS
TILEMAP CONVERT: every pass green

640 assertions. The last pass is the one that matters: 4.2 is the last Godot before TileMapLayer existed, and a project that never saw 4.3 is exactly the project this tool is for. A fixture written by a modern engine would quietly assume the file on disk already looks modern.

Godot's own demo projects, branch 4.2

Run over godotengine/godot-demo-projects at branch 4.2 — 298 scene files, nobody's code but Godot's:

resultcount
.tscn files scanned298
TileMap nodes found17
converted, in one pass14 — 13 layers, 3 427 cells
refused3 — every one of them carries a script (dynamic_tilemap_layers, navigation_astar, and the Grid node of role_playing_game)
notes14 — 12 layers with a blank name became Layer0, one TileMap holds no layer data at all

The interesting number is the 3. A converter that "handles everything" would have rewritten those three into a broken scene tree; there is no automatic answer to a script that extends a node which is about to stop being that node. Roughly one real-world TileMap in six is that case, so expect the report to name a few and plan to port them by hand.

Same corpus at master: 396 scenes, zero TileMap nodes left. The demos have finished the migration this tool is for — which is why the measurement above uses the 4.2 branch, the state the projects that still need converting are actually in.

Run it on your whole project

The browser version above takes one scene at a time. The command line takes the project:

# see what would happen — writes nothing
node docs/convert_tilemap_to_tilemaplayer.js /path/to/your/godot/project

# do it — every changed file gets a .tscn.bak next to it first
node docs/convert_tilemap_to_tilemaplayer.js /path/to/your/godot/project --write

Zero dependencies, MIT, reads only .tscn, exits 1 if any node was refused so it drops into a build script. Get it from the Blobsmith Autotile Wirer repo, together with verify_tilemap_convert.sh, which re-runs those 640 checks against your Godot build.

The converter in this page is the same file the terminal runs, byte for byte — tilemap-convert-core.js. A test refuses to ship this page if the copy served here has drifted from the repo. Full write-up, including everything this does not do: converting-tilemap-to-tilemaplayer.md.

After the conversion

  1. Open the project in Godot 4.3+ and save once. The scene is valid text already; saving lets the engine write its own bookkeeping.
  2. Fix the code that talked to the TileMap. $TileMap.set_cell(0, pos, ...) becomes $TileMap/Ground.set_cell(pos, ...) — the layer index argument is gone, because the node is the layer. This tool does not touch .gd files, on purpose.
  3. Check the draw order. Layers used to be ordered by index inside one node; they are now siblings, ordered by their position in the tree. The converter emits them in layer order, which preserves it — but if you had per-layer z_index or y_sort tricks, this is the page for that.

Where the plugin fits

None of the above is our product and none of it costs anything. Blobsmith is the pixel-art tool that turns 6 hand-drawn tiles into a full 47-tile blob sheet, and the Wirer (MIT) wires that sheet into a TileSet with the terrain bits already set. If you just converted a project and want a sheet you know is correct to test against, take the free 47-blob starter pack — 8 wired TileSets, MIT, no e-mail.

This converter also runs in your terminal, over a whole project at once — it is one of the MIT scripts in the free scanner bundle.