Articles ·
Snap to a TileMap in Godot: 2D grid placement
How to snap to a TileMap in Godot 4: place 2D furniture, props, and buildings on a TileMapLayer with anchored scenes, live cell previews, and commit results.
This recipe snaps 2D furniture, props, and buildings to a TileMapLayer: pick
an entry, hover the map, watch a live preview lock to cells, and commit. That
is the whole loop a 2D grid-based building system runs on. Grid
Placement owns targeting, preview, validation, and commit; your game supplies
the entry scenes and reads back where things landed.
The same three steps work on an isometric or hexagonal map: the plugin snaps to
whatever cells your TileSet defines, so cell shape and size stay a TileSet
configuration question (see
Isometric 2D Placement).
If you are placing in 3D, the
3D version of this recipe
swaps the TileMapLayer for a GridMap and adds mounts and rotation.
1. Author the entry scene around the cell
The scene root is the placement origin. Grid Placement seats the root on the
targeted cell and replaces the root's saved position, so keep the root at the
scene origin and put deliberate offsets on children: Sprite2D children around
the intended origin, collision children matching the gameplay footprint.
Then set ScenePlacementEntry.cell_anchor_mode per axis using the even/odd
rule: an odd cell span on an axis seats at the cell center, an even span seats
at the minimum cell edge. On a 32px grid a 32×32 sprite (1×1) takes
CELL_CENTER; a 64×64 sprite (2×2) takes CELL_TOP_LEFT. After changing the
mode, re-select the entry so the preview rebuilds. Anchor modes never infer
rotation or facing — that is presentation your game owns, covered in the
2D Object Scene Setup
guide.
2. Read the cursor cell for feedback
Convert the active 2D positioner's world position through the target
TileMapLayer. This is cursor and UI feedback only — never proof that
placement succeeded:
@export var session: PlacementSession
func get_cursor_cell() -> Vector2i:
var targeting: GridTargetingState = session.get_states().targeting
var map: TileMapLayer = targeting.target_map
var positioner: Node2D = targeting.positioner
if map == null or positioner == null:
return Vector2i(-1, -1)
return PlacementPositioning2DUtils.get_tile_from_global_position(positioner.global_position, map)3. Consume the commit result, not the cursor
When placement commits, read the result. In 6.x the success flag and the
placed node arrive on data.report — do not depend on removed legacy fields
of PlacementActionData:
func _ready() -> void:
session.get_building_state().action_performed.connect(_on_action_performed)
func _on_action_performed(data: PlacementActionData) -> void:
if data == null or data.report == null or not data.report.is_successful():
return
var placed: Node = data.report.placed
var map: TileMapLayer = session.get_states().targeting.target_map
if placed == null or map == null:
return
var cell := PlacementPositioning2DUtils.get_tile_from_global_position(placed.global_position, map)
_record_occupancy(cell, placed)That cell is the grid-aligned fact your game keeps: occupancy for
pathfinding, save metadata, or a link between the placed object and the
TileMapLayer cells it covers. The full coordinate rules — including terrain
paint callbacks, which already expose their target cell — are in
Capturing Tile Positions.