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:
| |
Where Terrain Data Lives
1. TileSet Resource
The authoritative terrain definition is in the TileSet assigned to your TileMapLayer:
- Terrain Sets (
TileSet.GetTerrainSetsCount()) — ATileSetcan 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
TileMapLayer→TileSet→ “Terrains” tab. Paint peering bits on your atlas tiles. The plugin does not edit theTileSet; it only consumes the terrain definitions you’ve already created.
2. TileMapLayer Cell Data
At runtime, each cell on a TileMapLayer carries terrain metadata:
| Property | Source | Used By |
|---|---|---|
TileData.TerrainSet | Set by SetCellsTerrainConnect | AutoDetect reads this from neighbors |
TileData.Terrain | Set by SetCellsTerrainConnect | AutoDetect reads this from neighbors |
TileData.GetTerrainPeeringBit() | Derived from TileSet | Godot 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):
| Property | Type | Description |
|---|---|---|
TerrainMode | TerrainSyncMode | None = legacy hardcoded tile; Fixed = always use the same terrain; AutoDetect = match neighbors |
TerrainSet | int | Terrain set index used for Fixed mode, or fallback when AutoDetect finds no neighbor terrain |
Terrain | int | Terrain index used for Fixed mode, or fallback when AutoDetect finds no neighbor terrain |
Mode: None (Default)
| |
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:
- Open your
TileSetin the Godot editor. - Add a terrain set (note its index, usually
0). - Add a terrain within that set (note its index, usually
0). - Paint peering bits on the atlas tiles that should connect to this terrain.
- In
TileMapSyncSettings, setTerrainMode = 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:
- Reads
LevelContext.TargetMap(exportedNodePathon theLevelContextnode in your scene). - Falls back to
TileMapSyncSettings.DefaultTileMapLayerPath. - Caches the result until the scene changes.
Important: Make sure
LevelContext.TargetMappoints 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 - previousOccupancyremovedCells = 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:
1tileMapLayer.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:
- Read
TileData.TerrainSetfrom the cell (if any). - Batch cells by their terrain set.
- Call
SetCellsTerrainDisconnect(batch, terrainSet)for each batch. - 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
| |
With this setup, every valid placement will:
- Spawn the placeable
Node2DunderObjects - Connect the corresponding grid cell to terrain
0in terrain set0on theGroundlayer - Godot will automatically pick the correct grass/dirt/road sprite based on the cell’s neighbors