A Godot 4 editor plugin that turns a flat autotile sheet (PNG) into a fully wired TileSet resource: a terrain set, the correct terrain peering bits on every tile, and optional collision — in one click or one GDScript call.
Wiring a blob autotile in Godot by hand means clicking the peering bits for 47 tiles, one neighbor at a time, without getting a single mask wrong. This plugin does the whole pass from the tile's position in the sheet: it knows which of the 47 canonical blob masks each cell represents, and sets the matching terrain bits, terrain mode and collisions automatically.
Above: the separate Blobsmith pixel-art tool used to paint a 47-blob source sheet. This plugin is the next step — it takes such a sheet and wires it into a Godot 4 TileSet.
⬇ Download the starter pack (16 wired TileSets, 200 KB) · release notes · browse the files
Four terrains — grass, stone, sand, water — each at 16px and 32px, in
both of Godot 4's terrain layouts, each shipped as a .png sheet plus an
already-wired .tres. You do not need this plugin, or any plugin, to use them:
drop both files of a pair into your project, add a TileMapLayer, set its
TileSet, and paint in the Terrains tab.
| Match Corners and Sides (47 tiles, 8×6) | Match Sides (16 tiles, 8×2) | |
|---|---|---|
| Grass | grass_47blob_16px · grass_47blob_32px |
grass_16sides_16px · grass_16sides_32px |
| Stone | stone_47blob_16px · stone_47blob_32px |
stone_16sides_16px · stone_16sides_32px |
| Sand | sand_47blob_16px · sand_47blob_32px |
sand_16sides_16px · sand_16sides_32px |
| Water | water_47blob_16px · water_47blob_32px |
water_16sides_16px · water_16sides_32px |
Pick the 47-blob files for ground that meets other ground (they resolve
diagonals); pick the 16-tile files for pipes, fences, wires, roads and cave walls,
where a diagonal touch should not connect — and where you only have to draw a
third as many tiles when you replace this art with your own. Each .tres
declares its own terrain mode, so you do not have to set it.
Every tile carries its terrain peering bits and a collision polygon. Each of the
sixteen was loaded headless in Godot 4.3, 4.4 and 4.7 stable, painted with
set_cells_terrain_connect and read back — 216 checks, 216 passing on each of
the three (verify_starter_pack.gd, which reads each file's tile count and
terrain mode from the pack manifest rather than assuming one shape). It is
procedural placeholder art, not a hand-drawn asset pack: use it to prototype, to
learn how Godot terrains behave, and as a known-correct reference when your own
sheet paints the wrong tile. Details and per-file checks:
examples/starter-pack/README.md.
New: side-view platformer that runs on F5 — examples/platformer/:
47-tile blob ground that connects itself, three one-way platform tiles on their own
physics layer, and a player.gd that walks, jumps up through platforms and drops down
through them (hold down + jump). 16px and 32px TileSets plus a painted demo level.
The real player.gd is driven through the real demo scene in Godot 4.3, 4.4 and 4.7 —
22/22 checks on each, with controls (one-way switched off: the jump hits the plank from below).
⬇ Download the platformer pack (65 KB)
· browse the files · why platforms get their own layer:
docs/why-you-cannot-drop-through-the-one-way-tile.md.
New: isometric, already a diamond — examples/isometric/:
eight isometric 47-tile blob TileSets — grass, stone, sand, water, 16px (32×16 cell) and 32px (64×32 cell) —
with tile_shape Isometric and tile_layout Diamond Right set, terrain peering bits renamed to the
isometric sides and corners, and a diamond collision polygon instead of the bounding box.
Checked in Godot 4.3, 4.4 and 4.7 — 128/128 on each, including painting the same cells on the square
starter pack and demanding the same tile (the check that catches a peering rename rotated by one step).
⬇ Download the isometric pack (189 KB)
· release notes
· browse the files · why a hand-made isometric TileSet paints a rectangle:
docs/why-my-isometric-tilemap-is-not-a-diamond.md.
New: roads, fences, pipes and streams — line tiles that connect — examples/lines/:
eight line-connector TileSets — road, fence, pipe, stream, 16px and 32px — each a Match Sides terrain with
a tile for all 16 side combinations (post, dead end, straight, corner, T, cross), transparent outside the
line so it sits on its own layer over your ground; fences and pipes collide as post + one bar per connected
side. The trap it documents: Connect joins two parallel fences into a ladder, even painted one call per
fence — Path keeps them apart. Checked in Godot 4.3, 4.4 and 4.7 — 120/120 on each.
⬇ Download the roads/fences/pipes pack (74 KB)
· release notes
· browse the files
New: hexagon terrains, pointy-top and flat-top — examples/hex/:
sixteen hexagon TileSets — grass, stone, sand, water, pointy-top and flat-top, 16px and 32px — each a
Match Sides terrain with a tile for all 64 side combinations, the right tile_offset_axis set, and a
hexagon collision polygon. Hex peering-bit names change with the offset axis (bottom_right_side is a
different neighbour pointy-top and flat-top), so each file is wired for its own axis. Checked in Godot 4.3,
4.4 and 4.7 — 256/256 on each, including every side of every cell of a painted island against
get_neighbor_cell.
⬇ Download the hex pack (399 KB)
· release notes
· browse the files
New: top-down dungeon walls that draw their own faces — examples/dungeon/:
four 3/4-view dungeon TileSets — stone brick walls on flagstone and rough rock walls on dirt, 16px and 32px —
where the walls are a 47-tile blob terrain and every wall tile with an open south side draws its front face.
Lay the floor, paint walls over it with Connect, and the faces land on exactly the walls with floor below them;
walls carry a full-cell collision square, the floor none. Checked in Godot 4.3, 4.4 and 4.7 — 64/64 on each,
including 364 painted wall cells whose bits must match their neighbours and a face check read from the
pixels of the tile the engine picked.
⬇ Download the dungeon pack (170 KB)
· release notes
· browse the files
New: Match Corners — the third terrain mode, 16 tiles instead of 47 — examples/corners/:
eight Match Corners TileSets — grass over soil, sand over water, snow over rock, lava over basalt, 16px
and 32px — where the terrain bits are the four corners of the cell, so a complete set is 2⁴ = 16 tiles
and not 47. The tiles are opaque, so one TileMapLayer is enough: the lower terrain is painted by the same
set. Checked in Godot 4.3, 4.4 and 4.7 — 136/136 on each.
⬇ Download the corner-terrain pack (89 KB)
· release notes
· browse the files · why terrain does not connect across two layers:
docs/why-terrain-does-not-connect-across-two-tilemaplayers.md.
New: a painted field that does not look tiled — examples/variants/:
eight Match Sides TileSets — grass, stone, sand, water, 16px and 32px — with 19 tiles instead of 16.
The three extra tiles are decorated copies of the interior tile that declare the same four peering bits,
so set_cells_terrain_connect treats them as interchangeable and picks among them at random, weighted by
each tile's probability (plain 1.0, each decorated one 0.35). That is a Godot 4 feature almost nobody
wires. The gate paints a 24×24 square and counts the 400 interior cells: all four tiles turn up, the plain
one takes the 48.8% its weight asks for, setting the decorated tiles to probability = 0 collapses the field
back to one repeated tile, and set_cell ignores probability entirely. Checked in Godot 4.3, 4.4 and 4.7 —
128/128 on each. Every decorated tile keeps its outer ring of pixels identical to the plain one — the
generator refuses to write one that does not — because a variant is only interchangeable if it meets its
neighbours the same way.
⬇ Download the tile-variants pack (71 KB)
· release notes
· browse the files
New: solid collision the physics server confirms — plus a checker for YOUR TileSet — examples/collision/:
eight solid Match Sides TileSets — rock, brick, ice, wood, 16px and 32px — every one of the 16 tiles
carrying a physics layer, a non-zero collision_layer and a full-cell polygon. The gate does not count
polygons in the .tres: it paints the tiles into a live TileMapLayer and asks
PhysicsDirectSpaceState2D — a dropped ray stops at the cell's top edge, the seam between two cells is
solid, a one-cell hole really is empty, erasing a cell drops its collider, and collision_enabled = false
removes every collider while the TileSet stays perfectly wired. Checked in Godot 4.3, 4.4 and 4.7 —
112/112 on each. The pack also ships check_tileset_collision.gd, which runs headless over your TileSet
and names the four faults that look wired and collide with nothing: no physics layer, collision_layer = 0,
a tile with no polygon, a polygon with fewer than 3 points.
⬇ Download the solid-collision pack (93 KB)
· release notes
· browse the files
New: a floor the navigation server paths across the moment it is painted — plus a checker for YOUR TileSet — examples/navigation/:
eight top-down floor + wall TileSets — dungeon, meadow, deck, cave, 16px and 32px — one Match Sides
terrain set with a Floor and a Wall terrain, 17 tiles: 16 floor tiles each carrying a full-cell navigation
polygon on a layer whose bitmask is 1 (what a NavigationAgent2D asks for by default), authored centred
(-8,-8)..(8,8) and not (0,0)..(16,16), and one wall tile carrying collision and no navigation. The gate
paints an 11×9 room with an inner wall into a live TileMapLayer and asks NavigationServer2D, only after
map_get_iteration_id() has moved: one region per floor cell and none for the walls, a path that goes around
the wall with not one sample inside a wall cell, endpoints at the exact centres of the start and goal cells,
and — with the gap painted shut — a path that comes back shorter, not empty, so if path.is_empty() never
fires. It also measures the two ways to paint it wrong from code: leaving out the 4th argument of
set_cells_terrain_connect (default ignore_empty_terrains = true) painted 20 of the room's 99 cells, and
walls-before-floor leaves 21 floor cells without their edge shading. Checked in Godot 4.3, 4.4 and 4.7 —
168/168 on each. The pack also ships check_tileset_navigation.gd, which runs headless over your
TileSet and names the faults that leave an agent standing still or walking through walls: no navigation
layer, a layers bitmask of 0, a floor tile with no polygon, a polygon that builds nothing, a polygon
authored from (0,0) (half a tile off), a wall carrying a navigation polygon.
⬇ Download the walkable-floor navigation pack (192 KB)
· release notes
· browse the files
New: tiles that say what they are — move cost, walkable, damage, footstep — plus a checker for YOUR TileSet — examples/tiledata/:
eight top-down ground TileSets — meadow, volcano, swamp, snow, 16px and 32px — one Match Sides terrain
set with six ground kinds (path, ground, rough, slow, hazard, blocked), every tile carrying five custom
data layers already filled in: ground (String), move_cost (float, never below 1.0), walkable (bool),
damage (int) and footstep (String) — every value written, the falses and 0s included, because Godot
reads a value never written as the type's default, the same as a real 0. The gate paints a 16×10 map into a live TileMapLayer, reads
every cell back through get_cell_tile_data().get_custom_data(), and feeds an AStarGrid2D from it: the
path takes the road over the bridge at the true minimum cost (a Dijkstra over the same data agrees), while
the same grid fed walkable only cuts across 3 hazard cells. It also measures the ways it goes wrong without
a word: local_to_map(global_position) on a moved, scaled layer found the right cell for 0 of 160 cells, a
tile added after the layers reads ""/0.0/false, a move_cost of 0.0 is accepted and the path stops
being the cheapest, a misspelt layer name returns null, the grid is a copy that keeps routing over a bridge
you painted shut. Checked in Godot 4.3, 4.4 and 4.7 — 161/161 on each. The pack also ships
tile_data_example.gd (the ground under a position, and an A* grid from the tiles) and
check_tileset_custom_data.gd, which runs headless over your TileSet — and, with --scripts, your
code — and names the faults that read back wrong: no custom data layer, a layer with no type or no name, a
value that loaded as null, a tile added after the layers were filled, an empty String, a layer name your
code asks for that no TileSet declares.
⬇ Download the tile custom-data pack (186 KB)
· release notes
· browse the files
New: walls that cast 2D shadows the moment they are painted — plus a checker for YOUR TileSet and scene — examples/occluder/:
eight top-down floor + wall TileSets — dungeon, crypt, forest, scifi, 16px and 32px — one Match Sides
terrain set with a Floor and a Wall terrain, 17 tiles: 16 floor tiles with no occluder, and one wall tile
carrying a full-cell occluder on an occlusion layer whose light_mask is 1 (what a PointLight2D's
shadow_item_cull_mask ships with), authored centred (-8,-8)..(8,8), plus full-cell collision. One file
works on 4.3, 4.4 and 4.7: the occluder is stored under the key 4.3 writes, which 4.4 and 4.7 still read —
the key 4.4+ writes (occlusion_layer_0/polygon_0/polygon) loads on 4.3 as no occluder and no error, and
re-saving the TileSet from 4.4+ (ResourceSaver.save) switches to it. Headless, the gate reads every tile back through the
running version's API (get_occluder on 4.3, get_occluder_polygons_count/get_occluder_polygon on 4.4+)
and finds the occluder on 44 of 44 wall cells and 0 of 73 floor cells of a painted room. The shadow itself
cannot be seen headless (the dummy renderer draws nothing), so the gate also renders the room on a real
OpenGL context and compares every floor pixel with line of sight from the light: 17,989 of 17,989 agree at
16px, 73,355 of 73,355 at 32px. It measures the traps too: shadow_enabled off (the default), an occlusion
light_mask that shares no bit with shadow_item_cull_mask, no occlusion layer — 0 dark pixels each — and
the 4.4 receiver rule (the floor's own light_mask is filtered too: 3,384 dark pixels on 4.3, 0 on 4.4 and
4.7). Checked in Godot 4.3, 4.4 and 4.7 — 181/181 on each (136 headless + 45 rendered). The pack also ships
check_tileset_occluders.gd, which runs headless over your TileSet or scene and names the faults that
leave a light shining through walls: no occlusion layer, a light_mask of 0, a wall with no occluder, a
polygon authored from (0,0), the 4.4+ key in a project that must open in 4.3, a light with shadows off or
no texture, a mask mismatch, occlusion_enabled off, the 4.4+ receiver mask.
⬇ Download the light-occluder pack (301 KB)
· release notes
· browse the files
New: animated water, every tile moving — examples/animated-water/:
a 47-tile blob water-over-grass terrain where all 47 tiles are animated (4 frames, 0.2 s each),
laid out so Godot accepts the animation — frame cells are not tiles, animation_columns set,
mode DEFAULT so ripples line up across tile edges. 16px and 32px TileSets plus a painted pond scene.
Checked in Godot 4.3, 4.4 and 4.7 — 23/23 on each: Connect still picks the canonical tile for every
cell, and the frame on screen is read back from rendered pixels (0, 1, 2, 3, 12 rendered frames each),
with a one-frame control that fails both.
⬇ Download the animated water pack (224 KB)
· release notes
· browse the files · why animated tiles freeze:
docs/why-my-animated-tile-does-not-animate.md.
New: terrain transitions, no seam — examples/transitions/:
grass over sand, sand over water (shoreline), stone over grass (paths),
water over grass (ponds), grass over stone (overgrown floors),
dirt over grass (dirt roads), grass over dirt (tilled ground),
snow over grass (winter maps) and lava over stone (volcanoes, dungeons),
eighteen wired TileSets (16px, 32px) with two terrains in one terrain set, so
Connect paints one over the other and every edge and corner between them matches.
Painted headless in Godot 4.3, 4.4 and 4.7 — 144/144 checks on each, with a control
showing the same paint with a one-terrain set leaves 188 mismatched edges per set.
⬇ Download the transitions pack (18 wired TileSets, 516 KB)
· release notes · browse the files.
The zip is unpacked into an empty project and repainted in 4.3 and 4.7 on every
suite run, so the download is checked, not just the folder.
Want the four terrains in ONE
TileSet? Godot 4 can add a source to aTileSetyou already have open, but nothing in the engine takes two.tresTileSet resources and gives you a third — so the usual answer is to paste them together in a text editor. That is the dangerous version for these files in particular: every.tresin this pack declares its atlas as[sub_resource type="TileSetAtlasSource" id="TileSetAtlasSource_bsmith"], so a hand-pasted file has twosources/entries resolving to the same sub-resource — one terrain is silently gone, in a file that loads with no error and no warning. https://blobsmith.lbwma.com/godot-tileset-merge/ does it in the browser: drop the files, get oneTileSetwith every terrain in it, ids renumbered, physics and custom-data layers deduped, terrain sets grouped by mode — plus the new source id and terrain index for each input, which is what theTileMapLayers you already painted are storing. Checked against this repository: three starter-pack terrains merged and loaded in Godot 4.3 and 4.7, each input also loaded alone so the engine's own reading is compared tile by tile, and the hand merge built and loaded too, to show it losing the terrain — 107 assertions (tres-merge,web-tres-merge).Merging them yourself, in a script or by hand? What actually has to be remapped is the list:
add_source(other.get_source(id))is a move that empties the donor,duplicate(true)keeps both but copies every index unchanged — and a terrain set, a custom data layer or a physics layer is only meaningful inside the TileSet that owns it, so the number survives while the meaning changes. 22 checks, run them on your own build withdocs/verify_tileset_merge.sh /path/to/godot; identical results on 4.2, 4.3, 4.4 and 4.7.
Coming from Godot 3 — Tilesetter, TilePipe2, or your own old project? Your Godot 3 tileset does not open empty in Godot 4 — it opens without the autotile. Godot 4 does have a compatibility path for
format=2, and it is not the "no importer at all" the forums say: the sources, the texture and the single tiles arrive. Every autotile arrives as a source with zero tiles andmargins (0,0), so even its region is gone, and the engine says so once, as a WARNING you probably never scrolled to. Two more things it does not say: a collision shape shared by several tiles is re-origined once per sharer, so the second tile's collision sits half a tile off and the third a whole tile off; and the source ids are re-issued, so a painted map is pointing at numbers that moved. 15 checks on your own build withdocs/verify_godot3_tileset.sh /path/to/godot; identical on 4.3, 4.4 and 4.7.
node docs/convert_godot3_tileset.js old_tileset.tresdoes the conversion the engine skips — geometry exactly (the two region formulas are the same expression), bitmasks canonicalised into Godot 4 terrain bits, tile ids kept as source ids, each polygon re-origined from the tile that owns it — and reports every mask it had to change and every pair that collapsed onto one neighbourhood, by tile and subtile coordinate. It refuses instead of guessing on occluders, navigation polygons, priority maps, rotated shapes andtex_offset. No terminal? The identical rules run in the browser: https://blobsmith.lbwma.com/godot-3-tileset-to-godot-4/
The sheet is yours and you do not want a plugin? The same wiring runs in the browser: https://blobsmith.lbwma.com/godot-tileset-tres-generator/ — drop the PNG, get the
.tresback. It reads the tile size off the image instead of asking (theclassify_sheetpass from this repo, ported), and names the ambiguous readings rather than picking one. Before offering the download it scans the alpha of every slot and names the wired ones that came back fully transparent — a blank cell in a wired sheet is the failure the editor never reports: no error, no gap, just a hole that paints wrong later. What it cannot wire it refuses (wrong column count, missing rows, a tile size that does not divide the sheet) instead of writing a.tresthat opens and misbehaves. Checked against this repository, not against itself: the generator reproduces all 16 starter-pack.tresbyte for byte — both terrain layouts, and the page hands a stranger those same bytes: 164 assertions (test/web-tres-generator.test.js), the download stages in a real browser over HTTP, including a sheet with three slots cleared by hand and a 3x3 sheet that has to be refused. The list of files it must reproduce is read from the pack'smanifest.json, so a tileset added to the pack cannot skip the comparison — that is how the eight 16-tile files came under it the day they were published.
⬇ Download the converter (2 scripts, no dependencies, 29 KB) · release notes · or run them out of docs/ after cloning.
Node 18+, MIT, reads and writes only your .tscn and .gd files — no engine,
no install, no network. The two blocks below are what they do, and what they
refuse to do.
TileMapis deprecated since Godot 4.3, and the editor converts one scene at a time, by hand, and only scenes it can open.node docs/convert_tilemap_to_tilemaplayer.js /path/to/projectconverts the whole project in one pass: everyTileMapbecomes aNode2Dwith oneTileMapLayerchild per layer,layer_N/tile_datais re-encoded intotile_map_data, and every block it does not own comes back byte for byte. It writes nothing without--write, keeps a.tscn.bakwhen it does, and refuses rather than guesses — a script on the node, an unknownlayer_N/key, a duplicate layer name, an uncheckedformat. Over Godot's own demo projects at branch4.2(298 scenes): 14 nodes and 3 427 cells converted, 3 refused, all three carrying scripts. The engine reads both versions back cell by cell indocs/verify_tilemap_convert.sh— 640 checks green across 4.3, 4.4, 4.7 and a 4.2 → 4.7 cross-version pass. No terminal? The same file runs in the browser: https://blobsmith.lbwma.com/godot-tilemap-to-tilemaplayer/ — paste a.tscn, get the converted scene back. Full write-up: convertingTileMaptoTileMapLayer.
The three it refuses are the scripted ones, and those now have a second pass. A script that
extends TileMapcannot extend theNode2Dthe node becomes, so the scene converter stops — but the port itself is mechanical:node docs/scan_tilemap_script.js /path/to/projecttakes the.gdand returns it call by call. The layer index argument becomes a layer node — given the scene (or--layers=Ground,Walls)set_cell(0, …)comes back as$Ground.set_cell(…)and not as a shrug — every removed method gets its replacement named, and anything that needs a human decision comes back as a question instead of a silent rewrite.--writeapplies only the rewrites marked safe and leaves the rest alone; the exit code is 1 while anything still needs a human, so it drops into a build script. No terminal? The same file runs in the browser: https://blobsmith.lbwma.com/godot-tilemap-script-to-tilemaplayer/. The replacement table is dumped fromClassDBon 4.2, 4.3, 4.4 and 4.7 rather than typed from the docs, anddocs/verify_tilemap_script_api.shre-checks it against your own build: 31 checks green on each engine that hasTileMapLayer, plus a round trip where the tools port a scripted scene end to end (22 rewrites, 0 left for a human) and the engine confirms the ported script reads the converted scene exactly as the original read the original.
- One-click wiring — pick a sheet, press Generate TileSet, get a wired
<sheet>_tileset.tressaved next to the PNG. - Two layouts — 47-blob (corners + sides, 8×6 tiles) or 16-tile (sides only, 8×2 tiles).
- Automatic layout detection — the file browser reads the PNG dimensions and pre-fills tile size and mode; a mismatch is reported instead of guessed.
- Correct terrain mode per layout —
MATCH_CORNERS_AND_SIDESfor 47-blob,MATCH_SIDESfor 16-tile. - Per-tile peering bits — derived from each tile's canonical neighbor mask, so painting resolves cleanly.
- Optional collision — full-square collision polygons on physics layer 0 (collision layer/mask 1).
- Named terrain — one terrain with a configurable name and a preset color.
- Scriptable core —
BlobsmithWirerCoreis a plainRefCountedwith no editor dependency; call it from your own tooling or tests. - Headless verification script — builds, saves, reloads and actually terrain-paints a
TileMapLayerto prove the output works.
Everything above is implemented in this repository; nothing here is aspirational.
Masks use a clockwise 8-neighbor encoding (bit → neighbor):
NW N NE 128 1 2
W · E → 64 · 4
SW S SE 32 16 8
A corner bit only counts when both of its adjacent side bits are present — a lone diagonal neighbor does not create a distinct tile. Collapsing every 8-bit combination to that rule yields exactly 47 canonical masks, which is why the blob layout has 47 tiles. blob47() returns those masks in ascending order; tile i in the sheet is expected at column i % 8, row i / 8.
That rule is a claim about what the engine does, so it was asked: a headless script paints all 256 neighborhoods with
set_cells_terrain_connect()and reads the peering bits back off the tile Godot 4.7 chose — 256/256 agree, and the engine distinguishes exactly 47 tiles. Toggle the eight neighbors and see which of the 47 answers your case (and how many of the 256 share it) at https://blobsmith.lbwma.com/godot-autotile-47-tiles/. The same page takes aTileSet.tresand lists the neighborhoods your set cannot answer — measured, a missing tile produces no error and no empty cell: the engine silently substitutes a tile claiming a corner the area does not have.
Why a blob autotile has 47 tiles and not 256 is the long form of the paragraph above: the collapse rule and why it is about what the tile can draw, the closed-form count (a tile answers
2^(4 - adjacent side pairs)neighborhoods, which is why the totals are 7×16 + 8×8 + 16×4 + 16×1 = 256), the engine transcript, the silent-substitution experiment, why a sides-only terrain set is 16 tiles instead, and the full 256 → 47 table with each tile's cell in the sheet.docs/verify_blob47.gdis the sweep itself and takes your tileset —godot --headless --script docs/verify_blob47.gd -- res://your.tresprints every neighborhood your set cannot answer and the tile the engine substitutes for it.
Terrain painting put the wrong tile down — what the engine actually does settles the two answers search gives you, neither of which is measured. It is not random (same paint 30× → one tile, including the ambiguous cases), and it picks the tile that disagrees with the fewest peering bits among tiles belonging to the terrain you are painting — never leaving the cell empty and never borrowing from another terrain, which is exactly why you get an ugly tile instead of an error. Delete the fully-surrounded tile and the engine silently places the one that is a single corner bit off. Also: why the empty cells around your painted region are constraints, why
ignore_empty_terrainsis not the lever people think it is (a measured negative, stated as such), and how the terrain mode caps 47 tiles down to 16 without telling you. The score says how wrong the substitute is and never which tile you get: over all 47 holes the engine always lands on a minimum-mismatch tile, and not once is one tile alone at that minimum — between 3 and 8 tie every time. Separately measured on the same page: "I changed a cell and the neighbours did not update", the complaint behind three open engine issues (#64674, #69737, #89844). One rule covers all of it — terrain fitting travels one cell out and only across a shared edge.set_cell()/erase_cell()re-fit nothing and no refresh call makes them;set_cells_terrain_connect()does rewrite cells you did not pass, but never past the edge-adjacent ring, and a cell touching your terrain only at a corner is never connected in either direction. Same numbers on 4.3, 4.4 and 4.7.docs/verify_terrain_choice.gdasserts all 33 claims against your Godot build —docs/verify_terrain_choice.shruns it in a throwaway project built from the tilesets inexamples/starter-pack, so a fresh clone needs no arguments and no test project — anddocs/predict_terrain_paint.jspredicts a painted region from your.tresalone, no engine — exiting non-zero on the first cell your set cannot answer. The same logic runs, byte for byte, at https://blobsmith.lbwma.com/godot-terrain-wrong-tile/, where you paint a region in the page and it names the cells with no exact tile.
The autotile has a seam exactly where your two TileMapLayers meet is the companion case: the tileset is fine and the paint is fine, but the map is split across two layers.
set_cells_terrain_connect()reads one layer — the one you called it on — so each half autotiles as though the other were empty space. A 2×2 block is four different corner tiles on one layer and two identical isolated strips when split down the middle; painting the second layer does not change one cell of the first, and reversing the order changes nothing. The fix is one layer, not one call: two separateset_cells_terrain_connect()calls on the same layer reach the single-call result byte for byte, so chunk-by-chunk and player-driven painting connect correctly as long as the calls land on the same node. When the split is not negotiable (z_index, collision, a foreground over the player), paint once and copy the finished cells across withset_cell(), which re-runs no terrain logic and keeps the connected tiles. 9 claims, identical on 4.3, 4.4 and 4.7;docs/verify_cross_layer_terrain.shruns them in a throwaway project built fromexamples/starter-pack, so a fresh clone needs no arguments.
Thin lines between tiles — what actually causes them separates the four causes that every answer piles into one paragraph. The most repeated fix, Rendering → Quality → 2D → Enable Pixel Snap, is a Godot 3 setting that does not exist in Godot 4 — its replacement is two settings, both shipping off. The default canvas filter in a new project is Linear, not Nearest (and the same integer
1means Linear in the project setting and Nearest on the node). Re-exporting your atlas with gutters is usually redundant:use_texture_paddingis on by default and already spaces 16px tiles 18px apart in the copy the GPU samples — a hand-cut gutter only costs you grid cells. 26 claims, all asserted on a real build bydocs/verify_tile_seams.gd, plusdocs/find_tile_seam_causes.js—node docs/find_tile_seam_causes.js /path/to/your/projectreads yourproject.godot,.tscnand.tresand tells you which of the four is yours.
The tiles are y-sorted and they still draw in the wrong order measures what decides draw order instead of repeating the advice that does not work. Ticking Y Sort Enabled and dragging Texture Origin — the two steps every answer gives, in that order — move the picture and never the sort key: at
texture_origin.y = -64the art is four rows from home and the tile below is still on top. The field that moves the key isTileData.y_sort_origin, and the key itself is the centre of the cell,cell.y * tile_size.y + tile_size.y/2 + y_sort_origin— which is why "set the origin to the tile's feet" is right in spirit and useless as an instruction. Two layers do sort against each other, per tile, but only withy_sort_enabledticked on each layer and on the node they hang from; tick one and the pixels do not move at all. And a single tile left atTileData.z_index = 1outranks the whole arrangement silently. Draw order is not a property you can print, so all 28 claims are measured by rendering each scene into a SubViewport and reading the contested pixel back:docs/verify_y_sort.gd, run bydocs/verify_y_sort.sh— which needsxvfb-run, because the headless build swaps in a dummy renderer and there is no pixel to read.docs/find_y_sort_causes.jswalks your own.tscn/.tresfor those causes and cites the claim id behind each one.
Which painted tiles will a body walk straight through?
node docs/find_tile_collision_gaps.js /path/to/projectnames them, from the text resources the editor already wrote — no engine, no project import, no dependencies. The failure is silent by design: a tile with no collision polygon is not an error, not a warning and not visibly different in the tile picker, andVisible Collision Shapesdraws the shapes that are there, never the ones that are missing. On a 47-tile blob set that is 47 chances to miss one. Its rules — including what a physics layer does and does not imply, and how an alternative tile inherits (or does not inherit) a shape — are asserted against a real 4.7 build bydocs/verify_tile_collision.gd(26 claims, run bydocs/verify_tile_collision.sh). All 26 are written out — the six silent ways through, thecollision_maskcolumn that cannot cause any of them, and what a clean scan does not prove — in why your player walks through a painted tile.No terminal? The same rules run in the browser: https://blobsmith.lbwma.com/godot-tilemap-collision-not-working/ — paste the
.tresand get the same list, plus the six causes as toggles over a body that actually stops or drops. The page loadsdocs/collision-scan-core.js, the identical bytes this command loads, so the two cannot name different tiles for one file.
Painting the generated
TileSetfrom outside the editor? Thetile_map_databinary format documents the bytes aTileMapLayerstores its cells in — header, 12-byte cell record, transform flags, and the erased-cell and int16-truncation traps — with a headless script that re-verifies every claim against your Godot build. Have a buffer in front of you right now? https://blobsmith.lbwma.com/godot-tile-map-data/ decodes it in the browser — paste thePackedByteArrayout of your.tscnand get the cell table back (coords, source id, atlas coords, alternative, flip/transpose), or type a cell table and get the bytes to paste in. Nothing is uploaded.
I changed one tile at runtime and every copy of it changed is the rule the flip case is one instance of, and it is the one that bites in gameplay code:
layer.get_cell_tile_data(cell).set_custom_data("hp", 3)does not set the hp of a cell. A cell never owns aTileData— the TileSet does, and every cell drawn from that tile, in everyTileMapLayersharing that TileSet, is looking at the same object. The write reads back through all of them, andResourceSaver.save()then writes your runtime value into the.tresunder version control. Two open engine issues (#108067, #93327) are the same fact reported from opposite ends — too wide, and not durable. There is also no private copy to take:TileDataextendsObject, notResource, and has noduplicate(). 22 checks, identical on 4.3, 4.4 and 4.7 (docs/verify_tile_data_sharing.sh, which also runs against your own.tres).
My agent will not walk on the tiles I painted is the eight causes with the guesswork taken out, and the headline is that the most repeated fix is not one: there is no number of frames to wait before the first query. On an idle machine the corridor answers on frame 1 (4.3), 2 (4.4) and 4 (4.7) — six runs each, identical, which is what makes the table look like a fact — and the same 4.7 binary under load answered on frames 2, 11, 13 and 20, crossing the values tabled for the other builds. Poll
map_get_iteration_id()instead; the same code is correct on all three (N30).map_force_update()publishes the regions without making the map answer (N12/N12b), a polygon authored(0,0)-(16,16)lands half a tile down-right because the region sits at the cell centre (N13-N17), and a corridor blocked halfway comes back shorter, not empty, soif path.is_empty()never fires (N23/N24). 31 checks, 31/31 on 4.3, 4.4 and 4.7 (docs/verify_tile_navigation.sh);docs/find_tile_navigation_gaps.jsscans your own.tres/.tscn/.gdfor the five causes that are readable in a file.
A flipped tile is not a new tile — what the three transform bits actually buy you answers the two questions that pull in opposite directions: can I draw half a symmetric sheet and flip the rest? and did my collision flip too? A flip is a property of the cell, not the tile — three bits (
4096,8192,16384) OR-ed into thealternative_tileargument ofset_cell(), authoring nothing and creating no alternative. So the answer to the first is no, and not by a little: the terrain autotiler never emits a transform bit, measured with the mirror tile deliberately withheld — the cell that needed it got a middle tile pointing into empty space instead, the same silent substitution the terrain-choice page describes. Draw all 47. The second answer is the trap: a flipped cell'sget_cell_tile_data()returns the same TileData object as the unflipped one and reports the collision polygon as authored, while the shape the physics server actually holds is mirrored. Both readings are true, so anyone who inspectsTileDataand concludes "collision does not follow the flip" has read a correct value and drawn a false conclusion. Also: why a terrain repaint erases a hand-placed flip, and why an authored alternative — not a transform bit — is the thing that can carry its own terrain bits, custom data or navigation. 33 claims, each with an id and four of them controls that fail if the check stops discriminating, asserted against 4.3, 4.4 and 4.7 bydocs/verify_tile_transforms.gd, run bydocs/verify_tile_transforms.sh— headless is enough, because the physics server is real in a headless build.
The tile under the mouse is the wrong one — what
local_to_mapactually takes is the click-picks-the-wrong-cell bug in every form it takes, each measured.local_to_map()wants the layer's local coordinates: a world position passed raw is off by the layer's offset ((7,3)instead of(1,0)) or drifts with its scale, and subtractingglobal_positionbreaks under a rotated parent. ACamera2Dlives in the canvas transform, not in the node, soevent.position— andto_local(event.position)— ignore it;make_input_local(event)andget_local_mouse_position()are right under a camera, an offset and a scale at once.Vector2i(pos / 16)truncates, so(-1,-1)lands in cell(0,0)whilelocal_to_mapfloors;map_to_localreturns the centre, not the corner; and on a 64×32 isometric layer local(0,0)is cell(-1,-1), with a new TileSet defaulting toSTACKEDlayout. 33 checks, three of them controls, 33/33 on 4.3, 4.4 and 4.7 bydocs/verify_mouse_to_cell.sh.
set_cellran and nothing appeared — what the engine stores instead is the tile-placed-from-code-is-invisible bug, with "nothing" read back from rendered pixels.set_cell(cell)andset_cell(cell, 0)are erases — the defaults are-1and(-1,-1). Atlas coords that were never created as a tile, a missing source id or a missing alternative are stored anyway (the cell is inget_used_cells()) and draw nothing, and neither theset_cellnor the draw prints an error; onlyget_cell_tile_data()returnsnulland complains. Removing and re-adding an atlas makes its id1, not0; a 2×2 tile exists only at its origin;create_tile()on an atlas with no texture creates nothing; andenabled = falsekeeps the cells and draws none. 30 checks, two of them controls, 30/30 on 4.3, 4.4 and 4.7 bydocs/verify_set_cell.sh(needsxvfb-run).
get_custom_datareturns null (or the wrong value) — what the engine reads is the tile-custom-data bug, measured with a realCharacterBody2Dlanding on a real floor. The body restssafe_marginabove the floor's top edge, so the cell at its feet is the empty one above and its tile data isnull; contact point minus normal finds the floor in every version. On 4.5+get_coords_for_body_rid()returns the index of a 16×16 physics chunk, not the cell —(0,0)for a body on cell(2,2)— unlessphysics_quadrant_size = 1. Unset values read0/false/"", a layer left at typeNilreadsnull, a wrong-case name readsnulland prints an error, writes are not type-checked. And an engine bug: afteradd_custom_data_layer(0)ormove_custom_data_layer()from code, lookup by name returns another layer's value until the TileSet is reloaded. 34/35/38 checks (the version-specific ones differ), four of them controls, all passing on 4.3, 4.4 and 4.7 bydocs/verify_tile_custom_data.sh.
Animated tile not animating (or animating when it should not) is the tile-animation page, with the frame on screen read back from rendered pixels at a fixed 60 fps. Frames default to 1.0 s each and are read to the right of the tile (
columns = 1reads them downward); if those atlas cells are already tiles — what "create tiles in non-transparent areas" does — the frame count is refused and the error says "animation columns count". Frame cells stop being tiles, soset_cellto a frame coordinate draws nothing.speed = 0is refused. In modeDEFAULTevery cell shows the same frame, even one placed later;RANDOM_START_TIMESdesyncs them.get_tree().pausedandprocess_mode = DISABLEDdo not stop tile animation;Engine.time_scaledoes, and so does setting the frame count back to 1. 27 checks, two of them controls, 27/27 on 4.3, 4.4 and 4.7 bydocs/verify_tile_animation.sh(needsxvfb-run).
My scene tile is not there (or I cannot find its node) is the Scenes Collection page. The scene id goes in the alternative slot and the first one is 1; a cell pointing at scene id 0 or 99 is stored and creates nothing, with no error.
get_cell_tile_datais null on a scene cell.set_celldoes not create the node —update_internals()or a later frame does; the node sits atmap_to_local(cell), has no owner, and only the first copy keeps the scene's name. The layer replaces the nodes whenever it rebuilds (enabledtoggled, a scene tile added to the TileSet), so state on the node is lost;queue_free()on the node leaves the cell, andset_cellwith the same values will not bring it back. The nodes are not saved with the scene. 47 checks, one of them a control, 47/47 on 4.3, 4.4 and 4.7 bydocs/verify_scene_tiles.sh(headless).
AStarGrid2D walks through my walls (or finds no path) is the grid-pathfinding page.
update()throws away every solid and weight set before it — and everyregion,cell_sizeoroffsetchange needs anotherupdate(), which wipes them again. A new grid has region(0, 0, 0, 0), so every query is "out of bounds". The default diagonal mode slips between two walls that touch at a corner. Points are the cell's top-left corner untiloffset = cell_size / 2, and they are layer-local. A solid start cell still gets a path in 4.3/4.4 and returns[]in 4.7;allow_partial_pathto a solid target returns[]in 4.3. An isometric TileSet is createdSTACKED, which matches noAStarGrid2Dcell shape —DIAMOND_RIGHT/DIAMOND_DOWNdo. 45 checks, one of them a control, 45/45 on 4.3, 4.4 and 4.7 bydocs/verify_astar_grid.sh(headless).
My tiles cast no shadow — what a
PointLight2Dneeds from a TileSet is the 2D lights page.shadow_enabledis off by default, and then nothing casts. A new TileSet has no occlusion layer, and occluder polygons are relative to the tile's center — drawn0..16the shadow moves half a tile. The occlusion layer'slight_maskhas to share a bit with the light'sshadow_item_cull_mask, and since 4.4 the lit floor'slight_maskhas to as well: the same scene that shadows on 4.3 stays lit on 4.4/4.7. 4.4 also addedTileMapLayer.occlusion_enabledand several polygons per layer. 23 checks on 4.3 and 27 on 4.4 and 4.7, three of them controls, all passing, read from rendered pixels bydocs/verify_tile_occlusion.sh(needs xvfb).
My isometric TileMap is not a diamond — shape vs layout is the isometric page. Setting
tile_shapeto Isometric leavestile_layoutatStacked, which lays diamonds out in a rectangle;Diamond Rightis the missing line. Stairs Right lands on the same points as Stacked at 64×32, and Tile Offset Axis moves nothing on an isometric TileSet. Tall art goes intexture_origin, nottile_size; local(0, 0)is cell(0, -1); andRIGHT_SIDEreturns the cell you passed in —+xisTOP_RIGHT_SIDE. 24 checks, 24/24 on 4.3, 4.4 and 4.7 bydocs/verify_isometric_layout.sh(headless).
set_cells_terrain_connectis slow in procedural generation is the generation-speed page. On a 128×128 noise map one call costs ~57–79 µs per cell, linear in the cells passed; one call per cell, or per 16×16 chunk, gives the identical map and costs about 4× and 1.1× that. With a complete 47-tile set, computing the neighbour mask yourself and callingset_cellgives the same map, 0 cells different, 38–56× faster.set_cells_terrain_pathjoins only consecutive cells. 16 checks, 16/16 on 4.3, 4.4 and 4.7 bydocs/verify_terrain_connect_speed.sh(headless; speed asserted as ratios only).
Which tile did the player hit? — why
local_to_map(collision point)gives the neighbour is the tile-hit page.get_collider()is theTileMapLayer, never a tile. The contact point sits on the tile's edge, and the right and bottom edges belong to the next cell: over 88move_and_collideapproaches from 8 directions,local_to_map(get_position())names the hit tile 37 times, 0 of 11 from below or from the right, and every miss is an empty cell. Minus the normal gets 72/88 (rectangle) and 81/88 (capsule) — the rest are tile corners — and a half-pixel probe for a non-empty cell gets 88/88.get_coords_for_body_rid()is the cell on 4.3/4.4 and a 16×16 chunk on 4.7, where two tiles of one chunk share a body, unlessphysics_quadrant_size = 1. Standing on a seam gives one slide collision a frame, not two. 17 checks on 4.3 and 4.4 and 20 on 4.7, three of them controls, all passing bydocs/verify_tile_hit.sh(headless).
I erased the tile and it still collides —
erase_celland the physics body it leaves behind is the mining page. Right aftererase_cell()the cell reads -1, but ray, point,RayCast2Dandmove_and_collideall still hit it; withoutupdate_internals()rays see the change after exactly 1 frame (20/20), and an enabledRayCast2Dreports it one physics frame late.update_internals()removes it on the same line on all three versions. On 4.7 the 8-tile floor is one body that the erase replaces with a new RID, and right afterupdate_internals()rays miss the neighbours too — 0 of 8 floor cells hit for 1 frame, so a ground-check ray next to the mined tile reads false once — whilemove_and_collideis unaffected; insidebody_shape_enteredon 4.7 the call prints two "flushing queries" errors and still works. Stored RIDs are dead. 24 checks, 24/24 on 4.3, 4.4 and 4.7 bydocs/verify_erased_tile.sh(headless).
I can jump up through the one-way tile but not drop down through it is the drop-through page. Holding down on a one-way platform does nothing, even at 2000 px/s, and
floor_snap_lengthis not the reason: the tile lets a body go only once it is deeper thanone_way_margin(a 0.9 px nudge comes back, 1.0 px drops; 3.9 and 4.0 at margin 4). Clearing the collision mask bit drops the player after 3 physics frames, 2 snap back, and restoring it inside the platform does not push the player up. Put platforms on their own TileSet physics layer: with one shared layer the same trick on solid ground leaves the player stuck inside it on 4.3/4.4 or sends it through.add_collision_exception_with(TileMapLayer)prints an error and does nothing; a server exception by collider RID covers one tile on 4.3/4.4 and a whole 16×16 chunk on 4.7. Flipped or transposed tiles keep blocking from above; a layer node rotated 180° blocks from below. 19 checks, 19/19 on 4.3, 4.4 and 4.7 bydocs/verify_one_way_drop.sh(headless).
My box gets stuck between two tiles —
RigidBody2Dcatching onTileMapLayerseams is the crate-and-ball page. On 4.3/4.4 a 62-tile floor is 62 physics bodies, and a rotation-locked box pushed at 120 px/s stops for good 0.2 px past a seam (25.2 / 121.2 / 345.2) where oneStaticBody2Dlets it reach 421.9 / 653.9; a ball is kicked upward at the seams. On 4.7 the same floor is 5 bodies (16×16 chunks): the box matches the control to the tenth of a pixel, but the ball is still kicked at the chunk borders (x = 256, 512).physics_quadrant_size = 64removes those too;= 1brings the 4.3 snag back. Friction 0 only helps. ACharacterBody2Dstalled on 0 frames — floor, capsule, flush ceiling, floating along a wall. 8 checks on 4.3 and 4.4 and 13 on 4.7, two of them controls, all passing bydocs/verify_seam_snag.sh(headless).
I changed the tile size and half the map went invisible is the two-fields page.
TileSet.tile_sizeandTileSetAtlasSource.texture_region_sizeare both called tile size and only the second one re-cuts the sheet under tiles that already exist. Doubling it on a 4x2 sheet of 16px cells deletes no tile and prints nothing: the atlas grid drops to 2x1, six of the eight tiles keep regions that fall outside the texture, andhas_tilestill answers true for them. On screen those cells draw fully transparent, and the tiles that stayed inside draw the centre of four sheet cells instead of their own art. The map data is untouched — same source id, same atlas coords, still inget_used_cells()— so setting the number back restores everything exactly. ChangingTileSet.tile_sizeinstead breaks nothing and just centres 16px of art in a 32px cell. If the bigger region is what you want, the tiles that no longer fit have to be removed, and doing it in a loop overget_tile_id(i)silently removes only 4 of the 6. 19 checks, 19/19 on 4.3, 4.4 and 4.7 bydocs/verify_tile_size_change.sh(needsxvfb-run: it reads back rendered pixels).
| Piece | Detail |
|---|---|
| Language | GDScript (@tool) |
| Engine | Godot 4.3+ (the dialog uses the static EditorInterface API, which exists on 4.2, but TileMapLayer does not — so the .tres it writes has nothing to paint on there) |
| Plugin entry | EditorPlugin registered via plugin.cfg v1.0.0 |
| Output | TileSet .tres (ResourceSaver) |
| Verification | Headless SceneTree script (uses TileMapLayer, so Godot 4.3+) |
PNG sheet (res://…/sheet.png)
│
plugin.gd ──────┤ EditorPlugin
(Tools-menu dialog: browse, tile size,
mode, terrain name, collision)
│ wire_and_save(...)
▼
wirer_core.gd ── BlobsmithWirerCore (RefCounted, no UI)
detect_layout(w,h) → { tile_size, sides_only }
blob47() / blob16() → canonical masks, ascending
build_tileset(...) → TileSet + TileSetAtlasSource
• add_terrain_set + mode
• per-tile set_terrain_peering_bit
• optional collision polygons
│ ResourceSaver.save
▼
<sheet>_tileset.tres (next to the PNG)
The UI (plugin.gd) only collects parameters and reports status. All of the mask math and TileSet construction lives in wirer_core.gd, which has no reference to the editor and is exercised directly by the verification script.
- Godot 4.3 or newer. Measured on 2026-08-20 by running the four engine scripts on each stable
build: 4.3, 4.4 and 4.7 pass (15 + 23 checks, plus 16-mode and the nasty-name round-trip); 4.2 fails
—
TileMapLayerdoes not exist before 4.3, so neither the verification script nor the workflow the plugin tells you to use ("add a TileMapLayer, set its TileSet") is available there. - An autotile sheet in the expected layout: 8 columns, tiles in ascending canonical-mask order — a 47-blob sheet (8×6 tiles) or a 16-tile sheet (8×2 tiles). The sheet under
examples/is one such sheet.
⬇ Download the addon (v1.1.0, 16 KB) · release notes
Unzip it in your project root. It creates addons/blobsmith_wirer/ and nothing
else — no file lands anywhere but that folder, which a test asserts on every run
by unpacking the published zip into an empty project. An example 47-blob sheet
rides along in addons/blobsmith_wirer/examples/ so there is something to wire
before you have drawn your own.
Or copy the folder out of a clone:
# from your Godot project root
cp -r /path/to/blobsmith-autotile-wirer/addons/blobsmith_wirer addons/Then in the editor: Project → Project Settings → Plugins → enable Blobsmith Autotile Wirer.
Nothing to find on the editor's AssetLib tab. Searching the Godot Asset Library for this addon returns no result — it is not listed there. Checked 2026-09-01 against the Asset Library API for every 4.x from 4.3 to 4.7, with a control term that does return results, so the zero is ours and not the query's. The zip above is the current addon; the repository is the current everything.
- Project → Tools → "Blobsmith Autotile Wirer…"
- Browse to your
.pngsheet (tile size and mode auto-fill on selection). - Set the terrain name and whether to add collision.
- Press Generate TileSet. A wired
<sheet>_tileset.tresappears next to the PNG. - Add a
TileMapLayer, assign the generatedTileSet, and paint in its Terrains tab.
If your sheet is not in this layout, step 2 now says what it is instead of
just refusing. From the pixel size alone the dialog names the reading — 6 base
tiles, the Godot 3 3×3-minimal set, 16 tiles in a 4×4 Wang square, the 47-blob
count in the wrong arrangement, a plain grid — and, when a sheet has more than
one honest reading (256×256 is 4×4 tiles at 64px and 16×16 at 16px), it says
the second one out loud instead of picking silently. A sheet no square tile size
divides is reported as exactly that: margins or separation between tiles, which
this wirer does not read. The same line offers the
free pack,
because "set tile size manually" is useless advice when the file you have cannot
be wired at any tile size. BlobsmithWirerCore.classify_sheet(width, height) is
the same function, callable from a script.
# false = 47-blob (corners+sides), true = 16-tile (sides only)
var out := BlobsmithWirerCore.wire_and_save(
"res://tiles/grass_47blob_16px.png", # sheet path
16, # tile size in px
false, # sides_only
true, # add full-square collision
"Grass", # terrain name
)
# out == "res://tiles/grass_47blob_16px_tileset.tres" ("" on failure)build_tileset() returns the TileSet in memory if you want to configure it further before saving.
The verification script (test_verify_addon.gd) is a headless SceneTree program. It hardcodes the sheet at res://tiles/grass_47blob_16px.png and the addon at res://addons/blobsmith_wirer/, so run it from a Godot project laid out that way:
# inside a project containing:
# res://addons/blobsmith_wirer/ (the addon)
# res://tiles/grass_47blob_16px.png (copy from examples/)
godot --headless --script res://test_verify_addon.gd
# prints PASS/FAIL per check, then "ADDON VERIFY: ALL PASS"; exit code 0 on successIt checks the mask tables, layout detection, a full build from the sample sheet, a save/reload round-trip, an actual terrain paint on a TileMapLayer, and the 16-tile mode.
Engines it has actually been run on (Godot_v<x>-stable_linux.x86_64, headless):
| Godot | Result |
|---|---|
| 4.2.stable | fails to parse — no TileMapLayer |
| 4.3.stable | pass |
| 4.4.stable | pass |
| 4.7.stable | pass |
blobsmith-autotile-wirer/
├── addons/
│ └── blobsmith_wirer/
│ ├── plugin.cfg # plugin manifest (name, version 1.0.0, entry script)
│ ├── plugin.gd # EditorPlugin: Tools-menu item + generator dialog
│ └── wirer_core.gd # BlobsmithWirerCore: mask math + TileSet builder (no UI)
├── docs/
│ ├── why-tiles-have-seams.md # the four causes of a line between two tiles
│ ├── verify_tile_seams.gd # 26 claims asked of a real engine (+ .sh runner)
│ ├── find_tile_seam_causes.js # scans YOUR project for those causes, no deps
│ ├── merging-two-tilesets.md # what a TileSet merge has to remap, and why it fails silently
│ ├── tile-map-data-format.md # the TileMapLayer tile_map_data byte layout
│ ├── verify_tile_map_data.gd # headless script proving every claim in that doc
│ ├── tile-map-data-fixtures.json # 12 buffers a real 4.7 wrote + the cells it reads back
│ ├── dump_tile_map_fixtures.gd # regenerates that file from your own Godot build
│ ├── check_js_buffers.gd # hands buffers built elsewhere to a real TileMapLayer
│ ├── why-terrain-paints-the-wrong-tile.md # what the engine picks when your set falls short
│ ├── why-terrain-does-not-connect-across-two-tilemaplayers.md # connect reads one layer; the seam is the split
│ ├── verify_terrain_choice.gd # 33 claims asked of a real engine
│ ├── verify_terrain_choice.sh # runs them on your binary, no test project needed
│ ├── terrain-choice-core.js # the choice logic the CLI and the web page share
│ ├── predict_terrain_paint.js # predicts a painted region from YOUR .tres, no engine
│ ├── terrain-paint-fixtures.json # neighbourhood masks dumped straight out of 4.7
│ ├── dump_terrain_paint_fixtures.gd # regenerates that file from your own Godot build
│ ├── why-y-sort-draws-the-wrong-order.md # what actually decides tile draw order
│ ├── verify_y_sort.gd # 28 claims measured by reading rendered pixels (+ .sh runner, needs xvfb)
│ ├── find_y_sort_causes.js # scans YOUR project for those causes, no deps
│ ├── ysort-scan-core.js # the y-sort rules, filesystem-free (same bytes run in a browser)
│ ├── why-tiles-do-not-collide.md # the six ways a body goes through a painted tile
│ ├── why-one-cell-changed-every-cell.md # a cell never owns its TileData; the TileSet does
│ ├── verify_tile_data_sharing.gd # 22 claims asked of a real engine (+ .sh runner, takes your .tres)
│ ├── why-tiles-are-not-walkable.md # the eight causes of an agent that will not move
│ ├── verify_tile_navigation.gd # 31 claims asked of a real engine (+ .sh runner)
│ ├── find_tile_navigation_gaps.js # scans YOUR project for the five readable causes
│ ├── nav-scan-core.js # those rules, filesystem-free
│ ├── converting-tilemap-to-tilemaplayer.md # TileMap -> TileMapLayer, and what it refuses
│ ├── convert_tilemap_to_tilemaplayer.js # converts a whole project, no engine, no deps
│ ├── tilemap-convert-core.js # the rules; the browser page runs these same bytes
│ ├── scan_tilemap_script.js # ports the GDScript the converter refuses, call by call
│ ├── tilemap-script-scan-core.js # the ClassDB replacement table (same bytes run in a browser)
│ ├── verify_tilemap_script_api.sh # 31 checks per engine + a scripted round trip
│ ├── verify_tilemap_convert.gd # the engine reads before and after, cell by cell
│ ├── verify_tilemap_convert.sh # 640 checks: 4.3, 4.4, 4.7 and 4.2 -> 4.7
│ ├── find_tile_collision_gaps.js # painted tiles with no collision polygon, from .tres/.tscn alone
│ ├── collision-scan-core.js # those rules, filesystem-free (same bytes run in a browser)
│ ├── verify_tile_collision.gd # 26 collision claims asked of a real engine (+ .sh runner)
│ ├── why-a-godot-3-tileset-does-not-open-empty.md # what Godot 4 keeps, drops and displaces from a format=2 TileSet
│ ├── convert_godot3_tileset.js # Godot 3 -> Godot 4 TileSet, with a report of what could not carry over
│ ├── tres3-convert-core.js # those rules, filesystem-free (same bytes run in a browser)
│ ├── verify_godot3_tileset.gd # 15 claims asked of a real engine (+ .sh runner)
│ ├── why-my-animated-tile-does-not-animate.md # frame layout, 1 s default, shared clock, what pauses it
│ ├── verify_tile_animation.gd # 27 claims read from rendered pixels at fixed 60 fps (+ .sh runner, needs xvfb)
│ ├── why-my-scene-tile-is-not-there.md # scene id slot, late creation, node names, rebuilds that drop state
│ ├── verify_scene_tiles.gd # 47 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-astargrid2d-walks-through-walls.md # update() wipes solids, corner cutting, corner vs centre, iso layouts
│ ├── verify_astar_grid.gd # 45 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-my-tiles-cast-no-shadow.md # shadow_enabled, occlusion layer, centered polygon, masks (4.4 change)
│ ├── verify_tile_occlusion.gd # 27 claims read from rendered pixels (+ .sh runner, needs xvfb)
│ ├── why-my-isometric-tilemap-is-not-a-diamond.md # tile_layout, offset axis, texture_origin, origin, neighbour constants
│ ├── verify_isometric_layout.gd # 24 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-set-cells-terrain-connect-is-slow.md # cost per cell, per-cell/chunk loops, precomputed set_cell, path vs connect
│ ├── verify_terrain_connect_speed.gd # 16 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-the-tile-you-hit-is-the-wrong-one.md # collider is the layer, edge contact point, corners, RID vs 4.7 chunks, seams
│ ├── verify_tile_hit.gd # 17/20 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-changing-the-tile-size-emptied-my-tilemap.md # tile_size vs texture_region_size, tiles outside the texture, what the map keeps, how to get back
│ ├── verify_tile_size_change.gd # 19 claims asked of a real engine, rendered pixels (+ .sh runner, needs xvfb)
│ ├── why-you-cannot-drop-through-the-one-way-tile.md # holding down, one_way_margin depth, mask-bit frames, own physics layer, exceptions, flips vs rotation
│ ├── verify_one_way_drop.gd # 19 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-the-erased-tile-still-collides.md # stale body after erase_cell, update_internals, 4.7 chunk rebuild, callbacks, dead RIDs
│ ├── verify_erased_tile.gd # 24 claims asked of a real engine, headless (+ .sh runner)
│ ├── why-bodies-catch-on-tile-seams.md # RigidBody2D stuck/bouncing on seams, one body per cell vs 4.5+ chunks, physics_quadrant_size, CharacterBody2D
│ └── verify_seam_snag.gd # 8/13 claims asked of a real engine, headless (+ .sh runner)
├── examples/
│ ├── starter-pack/ # 16 free wired TileSets (grass/stone/sand/water × 16/32px × 2 layouts)
│ │ ├── *_47blob_*.png # the 8×6 sheets — Match Corners and Sides
│ │ ├── *_16sides_*.png # the 8×2 sheets — Match Sides
│ │ ├── *.tres # the already-wired TileSets — no plugin needed to use these
│ │ ├── manifest.json # base, size, layout, tile count and mode the engine gate iterates over
│ │ ├── README.md # import steps, what is checked, honest note on the art
│ │ └── starter_preview.png # all eight 32px sets, painted by Godot with set_cells_terrain_connect
│ ├── transitions/ # 2 terrains in one terrain set, 16/32px
│ │ ├── grass_on_sand_*.png/.tres # 48 tiles (47 grass-over-sand + full sand), every bit set
│ │ ├── sand_on_water_*.png/.tres # shoreline: 47 sand-over-water + full water
│ │ ├── stone_on_grass_*.png/.tres # path: 47 stone-over-grass + full grass
│ │ ├── water_on_grass_*.png/.tres # pond: 47 water-over-grass + full grass
│ │ ├── grass_on_stone_*.png/.tres # overgrown floor: 47 grass-over-stone + full stone
│ │ ├── dirt_on_grass_*.png/.tres # dirt road: 47 dirt-over-grass + full grass
│ │ ├── grass_on_dirt_*.png/.tres # tilled ground: 47 grass-over-dirt + full dirt
│ │ ├── manifest.json # base, size, tiles and the two terrain names per TileSet
│ │ └── verify_transitions.gd # paints a lake of the top terrain in a real engine (+ .sh runner)
│ ├── platformer/ # side-view: blob ground + one-way platforms on physics layer 2
│ │ ├── platformer_*px.png/.tres # 47 Ground terrain tiles + 3 one-way platform tiles
│ │ ├── platformer_demo.tscn # painted level, player, camera — F6 to play
│ │ ├── player.gd # walk, jump, drop through platforms (clears mask bit 2 for 6 frames)
│ │ └── verify_platformer.gd # drives the real player through the real scene (+ .sh runner)
│ ├── animated-water/ # 47-blob water over grass, every water tile 4 frames of 0.2 s
│ │ ├── animated_water_*px.png/.tres # 47 animated Water tiles (frames to the right) + 1 Grass tile
│ │ ├── animated_water_demo.tscn # painted pond + camera — F6 to watch
│ │ ├── animated_water_preview.gif/.png # the pond animated / frame 0
│ │ └── verify_animated_water.gd # paints, audits the animation, reads frames back from pixels (+ .sh runner)
│ ├── isometric/ # 47-blob isometric (Diamond Right), 2:1 cells, diamond collision
│ │ ├── {grass,stone,sand,water}_iso47_*px.png/.tres # 47 tiles each, 16px (32×16) and 32px (64×32)
│ │ ├── isometric_preview.png # an island Godot painted with the 32px grass set
│ │ ├── manifest.json # base, size, cell and sheet dimensions per TileSet
│ │ └── verify_isometric_pack.gd # paints each set, compares against the square pack (+ .sh runner)
│ ├── godot-47blob-starter-pack.zip # the same sixteen in one download
│ ├── godot-47blob-transitions-pack.zip # the same eighteen in one download, LICENSE inside
│ ├── godot-platformer-starter-pack.zip # the platformer pack in one download, LICENSE inside
│ ├── godot-animated-water-pack.zip # the animated water pack in one download, LICENSE inside
│ ├── godot-isometric-47blob-pack.zip # the isometric pack in one download, LICENSE inside
│ ├── grass_47blob_16px.png # 128×96 sample sheet — 16px tiles, 47-blob layout
│ └── blobsmith-demo.gif # the companion Blobsmith tool painting a sheet
├── test_verify_addon.gd # headless SceneTree verification script
└── icon.png # 256×256 project icon
- Personal project, tagged 1.0.0. The core is covered by the verification script above; there is no CI running it for you.
- The sheet must already be in the expected order. The plugin wires masks by tile position; it does not reorder or validate the pixels of an arbitrary sheet. Feed it a sheet whose tiles are laid out in ascending canonical-mask order.
- Layouts: only the 47-blob and 16-tile arrangements are supported. Other autotile schemes are out of scope.
- Collision is a single full-square polygon per tile — enough for solid terrain, not per-shape edge collision.
- Single terrain, single terrain set. One terrain is created per run.
- The verification script's paths are hardcoded to
res://tiles/…; adjust them or place the sample sheet accordingly before running.
The examples/ sheet is produced by Blobsmith, a separate pixel-art tool that draws a 47-blob sheet from 6 base tiles. This plugin works with any sheet in the same layout.
Released under the MIT License.
- Blobsmith — draw 6 tiles, get a full 47-blob autotile sheet + a wired Godot 4 TileSet (free in-browser version)
- LocGuard — localization QA linter for Godot 4: missing keys, placeholder drift, broken BBCode (Pro: in-editor dock + CI gate)
- The nine Godot 4 scanners, one zip — the four tile scanners from
docs/above plus five localization ones, MIT, no install and no account: what each one printed againstgodotengine/godot-demo-projectsis on the page - Five Godot 4 tools that run in the browser — no install, no account, nothing uploaded; each one refuses what it cannot answer instead of guessing:
wire a sheet into a
.tres· merge twoTileSetresources into one · which neighbourhoods yourTileSetcannot answer · convert aTileMapscene · port a script thatextends TileMap· why the player falls through a painted tile - blobsmith.lbwma.com — the studio site: every release in one place, plus free browser tools (nonogram solver, puzzle generators) and printable PDFs










