Terrain Sync Guide

This document explains how the GridPlacement plugin acquires terrain auto-tiling data from Godot TileSet resources and applies it when synchronizing ECS occupancy to a TileMapLayer.


Overview

Brush paint/remove eligibility (occupied cells, terrain_change_blocked custom data) is documented in Terrain Change Policy.

When a player places or removes an object, the plugin can optionally write to a TileMapLayer using Godot’s terrain system instead of a hardcoded atlas coordinate. This means placed tiles automatically pick the correct sprite based on their neighbors — corners, edges, and junctions are handled by the TileSet itself.

The flow is:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ECS Occupancy System
TileMapSyncManager.ResolveTileMapLayer()
GridSyncOptimizer.SyncToTileMapLayer()
        ├── TerrainMode.None  → SetCell(pos, sourceId, atlasCoord)
        ├── TerrainMode.Fixed → SetCellsTerrainConnect(cells, terrainSet, terrain)
        └── TerrainMode.AutoDetect
                  ├── Scan 8 neighbors → read TileData.TerrainSet / TileData.Terrain
                  ├── Pick majority terrain pair
                  └── SetCellsTerrainConnect(cells, detectedTerrainSet, detectedTerrain)

Where Terrain Data Lives

1. TileSet Resource

The authoritative terrain definition is in the TileSet assigned to your TileMapLayer:

  • Terrain Sets (TileSet.GetTerrainSetsCount()) — A TileSet can have multiple terrain sets (e.g. “Ground”, “Water”, “Roads”).
  • Terrains (TileSet.GetTerrainsCount(terrainSet)) — Each set contains one or more terrain types (e.g. “Grass”, “Dirt”).
  • Peering Bits — Defined per-atlas-coordinate in the TileSet editor. They tell Godot which terrain each tile edge/corner belongs to.

You configure terrains in the Godot editor: select the TileMapLayerTileSet → “Terrains” tab. Paint peering bits on your atlas tiles. The plugin does not edit the TileSet; it only consumes the terrain definitions you’ve already created.

2. TileMapLayer Cell Data

At runtime, each cell on a TileMapLayer carries terrain metadata:

PropertySourceUsed By
TileData.TerrainSetSet by SetCellsTerrainConnectAutoDetect reads this from neighbors
TileData.TerrainSet by SetCellsTerrainConnectAutoDetect reads this from neighbors
TileData.GetTerrainPeeringBit()Derived from TileSetGodot uses this to pick the atlas coord

The plugin queries neighbor cells via TileMapLayer.GetCellTileData(Vector2I) to discover which terrain they belong to.


Configuration

Inspector Settings

On your PlacementSettings resource (or directly on TileMapSyncSettings):

PropertyTypeDescription
TerrainModeTerrainSyncModeNone = legacy hardcoded tile; Fixed = always use the same terrain; AutoDetect = match neighbors
TerrainSetintTerrain set index used for Fixed mode, or fallback when AutoDetect finds no neighbor terrain
TerrainintTerrain index used for Fixed mode, or fallback when AutoDetect finds no neighbor terrain

Mode: None (Default)

1
2
// Result: every occupied cell gets the same atlas coord (0, 0)
tileMapLayer.SetCell(pos, 0, new Vector2I(0, 0));

Use this when you don’t want the plugin to touch terrains — for example, when the TileMapLayer is just an occupancy overlay.

Mode: Fixed

All newly placed cells are connected to the same terrain.

When to use: You have a single buildable terrain (e.g. a “Foundation” or “Road” terrain) and every placed object should extend that terrain.

Setup checklist:

  1. Open your TileSet in the Godot editor.
  2. Add a terrain set (note its index, usually 0).
  3. Add a terrain within that set (note its index, usually 0).
  4. Paint peering bits on the atlas tiles that should connect to this terrain.
  5. In TileMapSyncSettings, set TerrainMode = Fixed, TerrainSet = 0, Terrain = 0.

Mode: AutoDetect

Each newly placed cell inspects its 8 neighbors and adopts the most common (TerrainSet, Terrain) pair found among them.

When to use: Your level already has varied terrain (grass, dirt, sand) and placed objects should blend into whatever is already there.

Fallback behavior: If a cell has no terrained neighbors (e.g. placing on an empty map), it uses the configured TerrainSet + Terrain values as the default.


Data Flow Deep Dive

Step 1 — Resolve the Target Layer

TileMapSyncManager.ResolveTileMapLayer() finds the TileMapLayer to write to:

  1. Reads LevelContext.TargetMap (exported NodePath on the LevelContext node in your scene).
  2. Falls back to TileMapSyncSettings.DefaultTileMapLayerPath.
  3. Caches the result until the scene changes.

Important: Make sure LevelContext.TargetMap points to your terrain layer (e.g. ../Ground), not an overlay or indicator layer. If it points to the wrong layer, terrain tiles will render there instead.

Step 2 — Compute Deltas

GridSyncOptimizer.SyncToTileMapLayer() compares the current ECS occupancy set with the cached set from the previous frame:

  • addedCells = currentOccupancy - previousOccupancy
  • removedCells = previousOccupancy - currentOccupancy

Only these exact deltas are processed. Neighboring cells are not touched directly — Godot’s terrain API handles neighbor atlas-coordinate updates internally.

Step 3 — Placement (Connect)

For each cell in addedCells:

  • Fixed mode: The cell is added to a batch array. After all cells are collected, a single call is made:
    1
    
    tileMapLayer.SetCellsTerrainConnect(cells, terrainSet, terrain);
  • AutoDetect mode: The optimizer calls TileMapLayer.GetCellTileData(neighbor) for each of the 8 neighbors, counts the (TerrainSet, Terrain) pairs, picks the winner, and groups cells by that pair before batching.

Godot then recalculates atlas coordinates for all connected cells (including pre-existing ones) based on the peering-bit rules in the TileSet.

Step 4 — Removal (Disconnect)

For each cell in removedCells:

  1. Read TileData.TerrainSet from the cell (if any).
  2. Batch cells by their terrain set.
  3. Call SetCellsTerrainDisconnect(batch, terrainSet) for each batch.
  4. Call EraseCell(pos) to remove the cell entirely.

Godot again recalculates atlas coordinates for the remaining connected cells so that edges update correctly (e.g. a road ending in a cap instead of a straight segment).


Common Pitfalls

“Tiles render on the overlay layer”

Cause: LevelContext.TargetMap resolves to an overlay TileMapLayer instead of the ground layer.
Fix: In your scene, select the LevelContext node and set TargetMap to the path of your terrain TileMapLayer (e.g. ../Ground).

“Terrain mode does nothing — tiles still look like (0, 0)”

Cause: The TileSet does not have a terrain configured at the requested TerrainSet / Terrain index. Godot silently skips invalid terrain indices.
Fix: Verify your TileSet has the terrain set up, and that the index numbers in TileMapSyncSettings match the editor.

“AutoDetect always uses the fallback”

Cause: Neighbor cells were placed with TerrainMode.None (hardcoded atlas coords) and therefore have TerrainSet == -1. AutoDetect only reads terrain from cells that were previously connected via the terrain API.
Fix: Ensure the base map was painted with terrains (either via Godot’s terrain painting tools or by pre-seeding it with SetCellsTerrainConnect).

“Erasing a placed tile breaks pre-existing map tiles”

Cause: The target layer contains tiles that were not placed by the ECS system (e.g. hand-painted level geometry). The removal logic only touches cells that the optimizer previously placed, but if your occupancy and level geometry share the same layer, conflicts can occur.
Fix: Use a dedicated TileMapLayer for placement-managed terrain, or ensure your level geometry is on a separate layer and LevelContext.TargetMap points only to the placement layer.


Example Scene Setup

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
IsometricLevel (Node2D)
├── Ground (TileMapLayer)          ← LevelContext.TargetMap = "../Ground"
│   └── TileSet = isometric_level_tileset.tres
├── Objects (Node2D)               ← placed Node2D entities parent here
├── LevelContext (Node)
│   └── TargetMap = NodePath("../Ground")
└── GridPlacementBootstrap (Node)
    └── Context = PlacementSettings.tres
        └── TileMapSync.TerrainMode = Fixed
        └── TileMapSync.TerrainSet = 0
        └── TileMapSync.Terrain = 0

With this setup, every valid placement will:

  1. Spawn the placeable Node2D under Objects
  2. Connect the corresponding grid cell to terrain 0 in terrain set 0 on the Ground layer
  3. Godot will automatically pick the correct grass/dirt/road sprite based on the cell’s neighbors