Correct way to impliment destructable autotiles

Godot Version 4.4

I have code that procedurally generates a gave level using autotiles. I have a player instatiated and can move around the caves fine.

I have tried to use erase_cell(pos) to erase a cell when digging is triggered by keypress and this works fine. My problem comes with the tilemap not automatically reconfiguring when I remove one of the tiles.

I suspect you should call queue_redraw on the tile map layer after erasing the cell.

To erase a tile in a terrain you’ll need to set the terrain parameter of TileMapLayer.set_cells_terrain_connect() or TileMapLayer.set_cells_terrain_path() to -1 (no terrain)

Thanks @mrcdk, I have the tile being erased now and after getting the tilemap coords of the erased tiles neighbors, most of the autotiles refresh except the left one. My new code is below with debug ‘print’ statements which gives the below output…

Starting cave generation…
Generated rock_cells count: 2941
[DEBUG] Trying to mine at: (3, 5)
Attempting to remove tile at: (3, 5)
Erased cell from tilemap at: (3, 5)
Removed position from rock_cells list.
Tile removal from tracking list for: (3, 5) processed.
Forcing terrain update for neighbors of: (3, 5)
Checking neighbor at (2, 5) with source_id: 0 and atlas coords: (0, 1)
Updated rock tile at: (2, 5) with tile: (0, 1)
Checking neighbor at (2, 4) with source_id: -1 and atlas coords: (-1, -1)
Skipping neighbor at (2, 4) - source_id: -1 not in tracking lists
Checking neighbor at (2, 6) with source_id: 0 and atlas coords: (3, 1)
Updated rock tile at: (2, 6) with tile: (3, 1)
Checking neighbor at (4, 5) with source_id: 0 and atlas coords: (0, 1)
Updated rock tile at: (4, 5) with tile: (1, 0)
Checking neighbor at (4, 4) with source_id: -1 and atlas coords: (-1, -1)
Skipping neighbor at (4, 4) - source_id: -1 not in tracking lists
Checking neighbor at (4, 6) with source_id: 0 and atlas coords: (4, 1)
Updated rock tile at: (4, 6) with tile: (4, 1)
Checking neighbor at (3, 4) with source_id: -1 and atlas coords: (-1, -1)
Skipping neighbor at (3, 4) - source_id: -1 not in tracking lists
Checking neighbor at (3, 6) with source_id: 0 and atlas coords: (0, 1)
Updated rock tile at: (3, 6) with tile: (0, 1)

extends Node2D

@onready var tilemap = $TileMapLayer

# Map dimensions
const MAP_WIDTH = 96
const MAP_HEIGHT = 40

# Noise parameters
var noise: FastNoiseLite
@export var noise_scale: float = 0.05
@export var noise_threshold: float = 0.3

var rock_cells = []

# --- Tile Configuration Constants ---
# 1. Tile Source IDs (IDs of the TileSet sources in your TileMap node)
const SOURCE_ID_ROCK = 0  # Example: Assumes your rock TileSet is source 0

# 2. Terrain Set IDs (ID of the terrain set WITHIN each respective TileSet source)
const TERRAIN_SET_ID_ROCK = 0   # Rock uses Terrain Set 0 within its Source

# 3. Specific Terrain IDs (The ID of the terrain type WITHIN its respective Terrain Set)
const TERRAIN_ID_ROCK = 0       # Rock terrain id is 0

signal generation_completed

func _ready():
	noise = FastNoiseLite.new()
	noise.seed = 100
	noise.frequency = noise_scale

func generate_cave():
	print("Starting cave generation...")
	generate_noise_cave()
	emit_signal("generation_completed")

func generate_noise_cave() -> void:
	if not tilemap:
		printerr("TileMap node not assigned in generate_noise_cave.")
		return
	tilemap.clear()
	rock_cells.clear()
	
	for x in range(MAP_WIDTH):
		for y in range(MAP_HEIGHT):
			if y < 5:
				continue
			var noise_value = noise.get_noise_2d(x,y)
			if noise_value < noise_threshold:
				rock_cells.append(Vector2i(x, y))

	# Use fully specific terrain information when setting cells
	tilemap.set_cells_terrain_connect(rock_cells, SOURCE_ID_ROCK, TERRAIN_SET_ID_ROCK, TERRAIN_ID_ROCK)
	print("Generated rock_cells count: ", rock_cells.size())

func remove_tile(pos: Vector2i) -> void:
	print("Attempting to remove tile at: ", pos)

	if not tilemap:
		printerr("TileMap node not assigned or is null in remove_tile.")
		return

	var original_source_id = tilemap.get_cell_source_id(pos)
	var original_atlas_coords = tilemap.get_cell_atlas_coords(pos)
	
	if original_atlas_coords != Vector2i(-1, -1): # A tile exists
		tilemap.erase_cell(pos)
		print("Erased cell from tilemap at: ", pos)

		var removed_from_list = false
		if original_source_id == SOURCE_ID_ROCK:
			if rock_cells.has(pos):
				rock_cells.erase(pos)
				print("Removed position from rock_cells list.")
				removed_from_list = true
			else:
				print("Warning: Erased rock tile (source %d) at %s was not found in rock_cells list." % [SOURCE_ID_ROCK, pos])
		else:
			if original_source_id != -1:
				print("Warning: Tile erased at %s had source_id %d, which is not tracked." % [pos, original_source_id])
		
		if removed_from_list:
			print("Tile removal from tracking list for: ", pos, " processed.")
		elif original_source_id != -1 :
			print("Note: Tile at ", pos, " was erased from map but not found in or removed from current tracking lists. Original Source ID: ", original_source_id)

		print("Forcing terrain update for neighbors of: ", pos)
		
		# Define neighbors 
		var ordered_neighbors = [
			# Left side first
			pos + Vector2i(-1, 0),  # Left
			pos + Vector2i(-1, -1), # Top Left
			pos + Vector2i(-1, 1),  # Bottom Left
			# Right side second
			pos + Vector2i(1, 0),   # Right
			pos + Vector2i(1, -1),  # Top Right
			pos + Vector2i(1, 1),   # Bottom Right
			# Vertical neighbors
			pos + Vector2i(0, -1),  # Top
			pos + Vector2i(0, 1),   # Bottom
		]

#		# Update each neighbor individually to ensure proper autotiling
		for neighbor_pos in ordered_neighbors:
			var neighbor_source_id = tilemap.get_cell_source_id(neighbor_pos)
			var neighbor_atlas_coord = tilemap.get_cell_atlas_coords(neighbor_pos)
			print("Checking neighbor at ", neighbor_pos, " with source_id: ", neighbor_source_id, " and atlas coords: ", neighbor_atlas_coord)
			if neighbor_source_id == SOURCE_ID_ROCK and rock_cells.has(neighbor_pos):
#				# Update individual rock tile
				tilemap.set_cells_terrain_connect([neighbor_pos], SOURCE_ID_ROCK, TERRAIN_SET_ID_ROCK, TERRAIN_ID_ROCK)
				var new_neighbor_atlas_coords = tilemap.get_cell_atlas_coords(neighbor_pos)
				print("Updated rock tile at: ", neighbor_pos, " with tile: ",new_neighbor_atlas_coords)
			else:
				print("Skipping neighbor at ", neighbor_pos, " - source_id: ", neighbor_source_id, " not in tracking lists")
	else:
		print("No tile to remove at: ", pos, " (cell was already empty).")

The original tile at (0,1) should be getting replaced with the tile at (2,0)

Everything behaves correctly when manually drawing and erasing tiles in the editor.