Skip to content

Articles

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.