Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Godot 4's tile_map_data byte layout, decoded (for anyone generating .tscn files outside the editor)

A topic by deforestpeg created 30 days ago Views: 202 Replies: 1
Viewing posts 1 to 2

Posting this because it is undocumented and I had to decode it from engine source, so hopefully it saves someone the afternoon.


If you write .tscn files outside the Godot editor (map generators, level converters, build scripts), a TileMapLayer stores its painted cells in a single tile_map_data PackedByteArray. Layout, little-endian:


Bytes 0-1: uint16 format version. It is 0 in every release from 4.3 through current master, since the enum still has one entry.


Then one 12-byte record per painted cell, in any order:

  +0  int16   cell x   (negative coords are two's complement, so -1 is FF FF)

  +2  int16   cell y

  +4  uint16  source id

  +6  uint16  atlas coords x

  +8  uint16  atlas coords y

  +10 uint16  alternative tile


Total length is always 2 + 12n. The engine checks (len - 2) % 12 == 0 and throws "Corrupted tile map data" otherwise, which is how I found that rule.


Things that cost me time:


Empty cells are never stored. There is no -1 sentinel, so an empty layer is an empty array rather than a grid of blanks.


Flip flags ride in the alternative field, OR-ed on: FLIP_H 0x1000, FLIP_V 0x2000, TRANSPOSE 0x4000.


Both text encodings parse. Since 4.2 the parser accepts PackedByteArray(0, 0, 1, ...) and PackedByteArray("base64..."). Godot's own saver writes decimal while every array is under 64 bytes, then switches the whole file to format=4 with base64. Writing format=3 with base64 is legal and loads in 4.3 through master; the editor just normalises it on the next save.


Worked example, six cells, source 0, alternative 0, at (0,0) (1,0) (2,0) (0,1) (1,1) and (-1,-1):


00 00                                    version 0

00 00 00 00  00 00  00 00 00 00  00 00   (0,0)   atlas 0,0

01 00 00 00  00 00  01 00 00 00  00 00   (1,0)   atlas 1,0

02 00 00 00  00 00  02 00 00 00  00 00   (2,0)   atlas 2,0

00 00 01 00  00 00  00 00 01 00  00 00   (0,1)   atlas 0,1

01 00 01 00  00 00  01 00 01 00  00 00   (1,1)   atlas 1,1

FF FF FF FF  00 00  03 00 00 00  00 00   (-1,-1) atlas 3,0


Version trap: TileMapLayer only exists in 4.3 and later, so a scene using it will not open in 4.2. If you need older support, the deprecated TileMap node holds the same 12 bytes reinterpreted as three int32s per cell in layer_0/tile_data, with a node-level format = 2 selecting that decoding.


One more: the per-tile physics polygons in the companion .tres use tile-CENTER origin, not top-left. A 32px tile's full-square collision is PackedVector2Array(-16, -16, 16, -16, 16, 16, -16, 16). I had that one wrong for a while and the shapes come out offset by half a tile.


All of the above verified by writing files by hand and opening them in 4.7.1.

Good writeup — the (len - 2) % 12 rule alone will save people an afternoon. Same rabbit hole, companion .tres side: we generate TileSet resources programmatically and load-test them in real 4.7, and these three cost us time the same way:

  1. .tres string fields are parsed with escape sequences. A terrain/resource name containing a quote or backslash must be written as " or \, or the file refuses to load with “Parse Error: Unterminated string”. Anything user-supplied that ends up in a name field needs escaping before you write the text.

  2. If you build the TileSet via script instead of writing text: TileData physics must be written AFTER the atlas source is attached with add_source(). Physics layers belong to the TileSet, so before add_source() the TileData sees zero physics layers and collision writes error out (or silently do nothing, depending on the call). Ordering is the whole fix.

  3. Terrain peering bits, if you generate autotiles: in “Match Corners and Sides” mode a corner bit (say NE) only participates when both adjacent side bits (N and E) agree. Setting corners without their sides looks valid in the file and simply never matches while painting.

And I can confirm the tile-center physics origin from the end of your post — hit the exact same half-tile offset before figuring it out.