Snap 2D objects to a Godot TileMap grid
Place 2D furniture, props, and buildings on a TileMapLayer with Grid Placement: anchored scenes, live previews, cursor cells, 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. Grid
Placement owns targeting, preview, validation, and commit; your game supplies
the placeable scenes and reads back where things landed.
1. Author the placeable 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.