Skip to content
GridCraft
FeaturesPricingGamesDocsEditor
FeaturesPricingGamesDocsEditor
GridCraft

A tile map editor for game developers.

Product

  • Features
  • Pricing
  • Editor

Resources

  • Documentation
  • Get Started
  • Sign In
  • Dashboard

Community

  • Send feedback
  • Discord

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
© 2026 GridCraft. A product of OmniaDev Inc.
Documentation menu
  • Getting started

    • Introduction
  • Editor concepts

    • Tile layers
    • Object layers
    • Tilesets
    • Variable-sized tiles
    • Terrain brushes
    • AutoMap rules
    • Plugins
  • Workflows

    • Open a Tiled file
    • Convert to GridCraft
    • Real-time collaboration
    • Edit with AI agents
    • Save to cloud
    • Use the desktop app
  • Export formats

    • Tiled JSON
    • LDtk
    • Godot 4
    • GameMaker
    • Defold
    • LOVE2D (Lua)
    • CSV
    • Binary (server-ready)
  • Account & cloud

    • Cloud sync, devices, Discord
  • Admin

    • Audit log
  • API reference

    • Overview
    • Auth
    • Maps
    • Billing
    • Admin
    • Discord linking
    • Devices
  • Legal

    • Legal index
  • Getting started

    • Introduction
  • Editor concepts

    • Tile layers
    • Object layers
    • Tilesets
    • Variable-sized tiles
    • Terrain brushes
    • AutoMap rules
    • Plugins
  • Workflows

    • Open a Tiled file
    • Convert to GridCraft
    • Real-time collaboration
    • Edit with AI agents
    • Save to cloud
    • Use the desktop app
  • Export formats

    • Tiled JSON
    • LDtk
    • Godot 4
    • GameMaker
    • Defold
    • LOVE2D (Lua)
    • CSV
    • Binary (server-ready)
  • Account & cloud

    • Cloud sync, devices, Discord
  • Admin

    • Audit log
  • API reference

    • Overview
    • Auth
    • Maps
    • Billing
    • Admin
    • Discord linking
    • Devices
  • Legal

    • Legal index

Binary export

The binary export is the most efficient format GridCraft produces. Two files come out: a gzipped world.dat the server reads at startup, and a map.json the client reads for render hints. The binary is dense and big-endian. It's laid out for streaming, so a game server in any language can parse it without a heavy parser or a JSON decode pass over a 50-megabyte file.

Both files share the same version field so the server and client agree on which map is loaded.

Files

world.dat   (gzipped binary, big-endian)
map.json    (UTF-8 JSON for the client)

Region size

The map is loaded in fixed-size square regions of 24 × 24 tiles. Your map width and height both have to be multiples of 24, otherwise the export warns and the consumer will refuse the file. The constant is exposed in map.json as divisionSize.

world.dat layout (pre-gzip, big-endian)

width:        int16
height:       int16
tileSize:     int16
version:      int64

propertyTilesCount: int16
  for each property tile:
    tileId: int32
    body:   PropertyTile (see below)

entityCount: int32
  for each entity:
    x:    int16
    y:    int16
    key:  string                   (int16 length + UTF-8 bytes; -1 = null/empty)

gridCount: int32
  for each non-empty cell:
    x:        int16
    y:        int16
    plateau:  int8
    idCount:  int8                 (max 127)
    for each id at that cell:
      tileId:    int32
      transform: int8              (flip bits: H=0x04, V=0x02, D=0x01)

objectLayerCount: int16
  for each object layer:
    name: string                   (int16 length + UTF-8 bytes)
    areaObjectCount: int16
    for each area object:
      id:     int16
      x:      int16
      y:      int16
      width:  int16
      height: int16
      polygon: int8 count, or -1 for none
        if present: count * (x:int16, y:int16)
      properties: int8 count, or -1 for none
        if present:
          type: int8                (0=short, 1=int, 2=double, 3=float, 4=bool, 5=string)
          name: string
          value: depends on type

PropertyTile body

An empty property tile (type NORMAL, no flags) is encoded as a single byte: -1 (0xFF). Anything else is written in full:

type:          int8       (-1=EMPTY, 0=NORMAL, 1=ANIMATION, 2=COLLIDING, 3=OBJECT)
width:         int16
height:        int16
collision:     int8       (-1 if not set)
zIndex:        int8       (-1 if not set)
ySort:         int8       (-1 if not set)
objectRadius:  int8       (-1 if not set)
cursorType:    int8       (-1 if none)
sand:          bool       (1 byte)
clay:          bool
invisible:     bool
nonColliding:  bool

Tile property tags

Set these as custom properties on individual tiles in the tileset. The exporter reads them by name and writes them into the property tile body:

  • c: collision. Marks the tile as colliding and stores the numeric value as the collision byte.
  • o: object. Marks the tile as an object with the numeric value as its radius.
  • h or obs: obstructing. When this tile is painted, every tile underneath it in the same cell is dropped.
  • z or v: high tile. Rendered above the player.
  • nc or noncolliding: forces non-colliding, overrides collision flags.
  • invisible: invisible tile.
  • sand, clay: terrain-type tags.
  • cursor: cursor type when hovering. One of: alchemy, axe, bow, cooking, crafting, divinity, fishing, foraging, hand, loot, pickaxe, points, mana, smelting, smithing, spell, sword, talk, bank, kosmetic, startshop, task, billboard, exchange, water, wastelands, housing, adventuring.
  • sfx: sound-effect ID. Plays when the player steps on the tile.
  • Tile animations: defined in the tileset's animation block. The exporter copies frames into both the property tile and the client's animations map.
  • plateau: not a tile property, a layer name. A layer called plateau1, plateau2, ... sets a plateau byte on every painted cell. Useful for combat z-indexing.

Required layers

One tile layer named entities (case-insensitive) has to exist. The exporter throws otherwise. Tiles on this layer reference entries from the entities tileset and export as entity spawn records. Tiles whose GID also has a collision property are skipped so entities don't spawn inside walls.

Layers named trees, rocks, foraging, regions (visualization only), or area names are treated as visual-only and are skipped by the exporter.

map.json (client)

{
  "version":       number,    // matches world.dat version
  "width":         number,
  "height":        number,
  "tileSize":      number,
  "divisionSize":  24,
  "high":          [ tileId, ... ],
  "animations":    { tileId: [ { tileId, duration } ] },
  "soundEffects":  { tileId: "key" },
  "collisions":    { tileId: number | polygons[] }
}

A polygon-collision entry is an array of polygons, each an array of { x, y } points (16-bit signed). A numeric entry is a simple collision marker (1 for object-style, the collision byte otherwise).

Worked example

A 2×2 map with one collision tile at (0,0). One tileset (firstGid 1), one tile with property c=0, one empty entities layer, and one tile layer with the collision tile painted at the origin. tileSize = 16, version = 1.

(2×2 isn't actually a valid size: dimensions need to be multiples of 24. The example just keeps the byte trace short.)

offset  bytes                                       meaning
------  ------------------------------------------  -----------------------------
0x00    00 02                                       width = 2
0x02    00 02                                       height = 2
0x04    00 10                                       tileSize = 16
0x06    00 00 00 00 00 00 00 01                     version = 1

0x0E    00 01                                       propertyTilesCount = 1
0x10    00 00 00 00                                 tileId = 0
0x14    02                                          PropertyTile.type = COLLIDING
0x15    00 10                                       width = 16
0x17    00 10                                       height = 16
0x19    00                                          collision = 0
0x1A    FF                                          zIndex = -1
0x1B    FF                                          ySort = -1
0x1C    FF                                          objectRadius = -1
0x1D    FF                                          cursorType = -1
0x1E    00                                          sand = false
0x1F    00                                          clay = false
0x20    00                                          invisible = false
0x21    00                                          nonColliding = false

0x22    00 00 00 00                                 entityCount = 0

0x26    00 00 00 01                                 gridCount = 1
0x2A    00 00                                       cell.x = 0
0x2C    00 00                                       cell.y = 0
0x2E    00                                          plateau = 0
0x2F    01                                          ids in this cell = 1
0x30    00 00 00 00                                 tileId = 0
0x34    00                                          transform = 0

0x35    00 00                                       objectLayerCount = 0

Total 55 bytes of raw payload. Gzip wraps that. The gzipped wrapper can differ between deflate implementations, but the inflated bytes are deterministic and byte-identical.

The matching map.json is:

{
  "version": 1,
  "width": 2,
  "height": 2,
  "tileSize": 16,
  "divisionSize": 24,
  "high": [],
  "animations": {},
  "soundEffects": {},
  "collisions": { "0": 0 }
}

Group layers

The exporter does not recurse into group layers. Only the top-level layer array is walked. If a group layer exists in your map, the exporter prints a console warning and skips it. Flatten the groups before exporting.