Godot 4: port a TileMap script to TileMapLayer

The scene converter refuses any TileMap that carries a script — a script that extends TileMap cannot extend the Node2D the node becomes. Paste that .gd here and get the port call by call: the layer index turns into the layer node, each removed method gets its replacement named, and whatever needs a decision comes back as a question instead of a silent rewrite. Nothing is uploaded — this runs in your browser.

The example is 2d/navigation_astar/pathfind_astar.gd from Godot's own demo projects, branch 4.2 — unmodified, and one of the three scripted TileMap nodes the scene converter refuses.

Layer names — the difference between “here is the shape of the fix” and “here is the line”

A call like set_cell(0, …) can only be written out in full once something says what layer 0 became. Paste the .tscn that holds this TileMap and the names are read out of it by the same converter that would have rewritten the scene; or type them yourself, in layer order.

Without either, every call is still found and named — the ones that need the layer just say <the node for layer 0> instead of $Ground.

Only the rewrites marked rewritten above are in this text. Everything marked by hand is left exactly as you pasted it, because applying it would be a guess.

Why the converter refuses a script in the first place

Godot 4.3 deprecated TileMap. The scene converter rewrites a .tscn: each TileMap becomes a Node2D of the same name, with one TileMapLayer child per layer. It refuses to touch a TileMap that carries a script, and that refusal is deliberate — a class that extends TileMap would be sitting on a node that is no longer one, and no text transform on the scene can decide what the script meant to do about it.

Run against Godot's own demo projects on branch 4.2, that is 3 of the 17 real TileMap nodes — roughly one in six. This page is what those three get instead of a dead end.

What the port actually is

Almost all of it follows from the shape the converter emits. The layer stops being an argument and becomes a node:

extends TileMap                                    extends Node2D
                                          ->
set_cell(0, pos, 1, Vector2i(2, 3))                $Ground.set_cell(pos, 1, Vector2i(2, 3))

That one sentence covers 20 of the 63 TileMap methods: same name, same behaviour, minus the leading layer argument (and, for seven getters, minus the trailing use_proxies). The wrapper keeps the old name, so $Level/TileMap anywhere else in your project still resolves.

The rest of the 63 fall into three piles:

KindHow manyWhat happens to the call
same12Identical signature on TileMapLayer. Only the receiver changes — from the map to one of its layer nodes. local_to_map(), get_used_rect(), clear().
drop20Same method minus the layer argument, on the layer node. set_cell(), erase_cell(), get_cell_source_id(), set_cells_terrain_connect().
gone27No method of that name exists on TileMapLayer. Most were layer bookkeeping and are now ordinary node operations: set_layer_modulate(1, c) is $Walls.modulate = c, get_layers_count() is counting children, add_layer() is adding a node.
enum4Same arity, but TileMap.VisibilityMode became TileMapLayer.DebugVisibilityMode — three values, all renamed, all still there.

One property moved too: collision_animatable is use_kinematic_bodies, and it lives on each layer node.

What it will not do

It will not guess. A layer index that is not an integer literal, a call whose replacement is an assignment rather than a call, a bare TileMap in a type hint — those come back as findings for a human, never as a silent edit. TileMap is still a real class in 4.7, so none of them stops the script from compiling; they just stop being true about a node that is now a Node2D.

And it is deliberately quiet in a file that never proved it talks to a tilemap. Half these names are not TileMap's alone — clear() is on Array and Dictionary, get_used_rect() is on Image. A same-signature call is only reported once the file has shown its hand: extends TileMap, a method only TileMap ever had, or a layer index written as an integer.

Where the 63-method table comes from

Not from the online class reference. Every name, every argument count and every dropped argument is dumped out of ClassDB inside a real binary — Godot 4.2, 4.3, 4.4 and 4.7 stable — and the table is rebuilt from those dumps. The scanner has to embed the table, because it runs in this page with no engine anywhere near it, so a script re-derives it from the four engines and fails if one byte of the embedded copy has drifted.

That script checks three things, and only the first is about drift:

  1. Drift. Rebuild the table from each engine's own ClassDB dump; it must come out byte for byte like the copy this page loads.
  2. Advice. Every replacement named above has to actually exist on TileMapLayer in each engine that has one. The engine cannot say the advice is right; it can say it is not pointing at something imaginary.
  3. Round trip. The oldest engine writes a scene with a scripted TileMap and prints what the script reads out of it. The converter and this scanner then port both, with no hand edits, and the newest engine runs the result and prints the same thing. A different digest is a failed port.

The script is docs/verify_tilemap_script_api.sh; point it at your own binaries and it will tell you about your own version.

A worked example, end to end

2d/navigation_astar is one of the three refusals. Its pathfind_astar.gd is 80 lines of A* pathfinding that reads the tilemap to find obstacles and writes back the start and end markers. Load it with the button above and you get 11 places to port.

Paste the script alone and 1 of the 11 is mechanical — the extends line — while the other 10 name the call and the shape of the fix but stop at <the node for layer 0>, because nothing has said what layer 0 is. Add the scene and all 11 become exact: that TileMap has one unnamed layer, the converter names it Layer0, and set_cell(0, _start_point, 0, …) is written out as $Layer0.set_cell(_start_point, 0, …).

The same numbers come out of the terminal, over a whole project at once:

node docs/scan_tilemap_script.js 2d/navigation_astar/
  → 1 script, 11 places to port: 11 mechanical, 0 for a human

With --write it applies only the mechanical ones and leaves the rest alone; the exit code is 1 whenever anything still needs a human, so it drops straight into a build script.

After the port

  1. Convert the scene too. The script port assumes the wrapper-plus-layers shape — the scene converter is what produces it.
  2. One layer? Consider moving the script down. If the map had exactly one layer you can put the script on that TileMapLayer child and write extends TileMapLayer instead. Then the calls lose their receiver rather than gaining one — set_cell(0, pos, …) becomes plain set_cell(pos, …).
  3. Check the callers. Other scripts that held a var map: TileMap reference to this node are holding a Node2D now. They compile; they will fail on the first tile call.
  4. Then look at what moved visually. Layers that were indices inside one node are siblings now, ordered by tree position — if you had per-layer z_index or y-sort tricks, that is this page.

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 ported 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 scanner also runs in your terminal, over a whole project at once — it is one of the MIT scripts in the free scanner bundle.