Problem using custom data layers in tileset

Godot Version

Hello, I’m working on a group project in Godot 4.5.1 for a 2D tactical RPG game. My part of work focuses on units movements and enemy A.I.

Explanation

During the player phase, when we select one of the available player units, it affects the tiles around the selected unit, depending on his movements:

  • the tiles where he can move, or wherer there is another player unit, are painted in blue
  • the tiles where enemy units are present, or which represent the possible attack range of the player after any movement, are painted in red
  • the tiles which correspond to pure obstacles, that cannot be moved or destroyed, are not colored

I want to deal with the tiles involving pure obstacles.

Goal

My goal is to change the way how the astar pathfinding, used by both players and enemy units, detect the pure obstacles. Originally, the program have an array of BLOCKED_Tiles, containing many Vector2i, that correspond to the atlas coordinates of each tile, from the selected tileset, that is considered an obstacle.

But now, I plan to use several tilesets in the future, and have different types of tiles (plain, obstacle, water, etc…). I want to use custom data layer for the tileset, and set the ones I want to obstacles.

Here is a photo:

All the obstacle tiles should be obstacle.

And here is the code I’m using for handling the pathfinding:

# File: res://scripts/pathfinding.gd
extends Node
class_name newPathfinding

# A* object and map properties
var player_astar := AStarGrid2D.new()
var enemy_astar := AStarGrid2D.new()
var map_width: int
var map_height: int
var move_amount: int
var tilemap_layer: TileMapLayer
var all_Positions: Array[Vector2i] = []
var current_unit: Unit

var BLOCKED_TILES: Array[Vector2i] = []
#
#const BLOCKED_TILES: Array[Vector2i] = [
#Vector2i(7, 0),
#Vector2i(7, 1),
#Vector2i(7, 2),
#Vector2i(7, 3),
#Vector2i(8, 0),
#Vector2i(8, 1),
#Vector2i(8, 2),
#Vector2i(8, 3),
#Vector2i(9, 0),
#Vector2i(9, 1),
#Vector2i(9, 2),
#Vector2i(9, 3),
#Vector2i(10, 0),
#Vector2i(10, 1),
#Vector2i(10, 2),
#Vector2i(11, 1),
#Vector2i(11, 2),
#Vector2i(12, 0),
#Vector2i(12, 1),
#Vector2i(12, 2),
#Vector2i(13, 0),
#Vector2i(13, 1),
#Vector2i(13, 2),
#Vector2i(13, 3),
#Vector2i(13, 4),
#Vector2i(14, 0),
#Vector2i(14, 1),
#Vector2i(14, 2),
#Vector2i(14, 3),
#Vector2i(14, 4),
#]

# SET UP

# Must be called first to set up references and grid
func newSetup(tilemap: TileMapLayer, width: int, height: int, tile_size: int) -> void:
	#print("Pathfinding setup called with tilemap:", tilemap)
	tilemap_layer = tilemap
	map_width = width
	map_height = height
	move_amount = tile_size
	
	# NOUVEAU : détecter les obstacles dans le TileSet
	scan_tileset_obstacles()


	new_init_astar(player_astar)
	new_init_astar(enemy_astar)

# Sets up the A* grid for pathfinding
func new_init_astar(astar: AStarGrid2D) -> void:
	astar.region = Rect2i(0, 0, map_width, map_height)
	astar.cell_size = Vector2(move_amount, move_amount)
	#print("INITIATION ASTAR",move_amount, "    ", astar.cell_size)
	astar.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER
	astar.default_compute_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
	astar.default_estimate_heuristic = AStarGrid2D.HEURISTIC_MANHATTAN
	astar.update()

	for x in range(map_width):
		for y in range(map_height):
			var cell := Vector2i(x, y)
			# Mark only terrain-blocked cells as solid
			var atlas: Vector2i = tilemap_layer.get_cell_atlas_coords(cell)
			if BLOCKED_TILES.has(atlas):
				astar.set_point_solid(cell, true)



func scan_tileset_obstacles():
	BLOCKED_TILES.clear()

	var tileset := tilemap_layer.tile_set

	for source_id in tileset.get_source_count():
		var source := tileset.get_source(source_id)

		if source is TileSetAtlasSource:
			for tile_id in source.get_tiles_ids():
				var tile_data : TileData = source.get_tile_data(tile_id, 0)
				print("tilemap_layer:", tilemap_layer)
				print("tile_data:", tile_data)

				if tile_data != null:
					var category : String = tile_data.get_custom_data("category")

					if category == "obstacle":
						var atlas_coords : Vector2i = source.get_tile_atlas_coords(tile_id)
						BLOCKED_TILES.append(atlas_coords)

	print("Tiles obstacles détectées :", BLOCKED_TILES)

	

func isTileType(tile: Vector2i) -> bool:
	var tile_data = tilemap_layer.get_cell_tile_data(tile)

	print("tilemap_layer:", tilemap_layer)
	print("tile_data:", tile_data)

	if tile_data == null:
		print(tile, " → aucune tile")
		return false

	var category = tile_data.get_custom_data("category")
	print(tile, " → category:", category)

	return category == "obstacle"

# TEAM FILTERING

# Returns true if the unit at tile_pos is an ally of `unit`
# Returns false if it's an enemy
# Returns null if no unit is present
func is_enemy(unit: Unit, tile_pos: Vector2i) -> bool:
	var other_unit: Unit = tilemap_layer.get_unit_at(tile_pos)
	if other_unit == null:
		return false  # treat empty as "not ally"

	return not are_allied(unit.team, other_unit.team)

# Helper: define what "allied" means
func are_allied(team_a: int, team_b: int) -> bool:
	if team_a == null or team_b == null:
		return false  # or push_warning("Null team passed to are_allied")
	
	if team_a == team_b:
		return true
	
	if team_a == Unit.TEAM.PLAYER and team_b == Unit.TEAM.ALLY:
		return true
	
	if team_a == Unit.TEAM.ALLY and team_b == Unit.TEAM.PLAYER:
		return true
	
	return false
	
# OBSTACLES CHECK

# Returns true if a tile is an obstacle, whether an enemy or a pure obstacle
func is_occupied(tile_pos: Vector2i) -> bool:
	if tilemap_layer == null:
		return false

	# We retrieve the data of the checked tile
	var atlas: Vector2i = tilemap_layer.get_cell_atlas_coords(tile_pos)
	var unit = tilemap_layer.get_unit_at(tile_pos)
	
	# Terrain obstacles, checking if the tile is amonged the obstacles one
	var terrain_blocked: bool = BLOCKED_TILES.has(atlas)
	
	# Enemies block movement
	var unit_blocked = tilemap_layer.is_tile_free(tile_pos)

	return terrain_blocked or unit_blocked
	
# Returns true if a tile is an obstacle, whether an enemy or a pure obstacle
func is_obstacle_for(unit: Unit, tile_pos: Vector2i) -> bool:
	if unit == null:
		return false
		
	if tilemap_layer == null:
		push_error("TILEMAP_LAYER IS NULL in is_obstacle_for")
		
	var atlas: Vector2i = tilemap_layer.get_cell_atlas_coords(tile_pos)
	var other_unit: Unit = tilemap_layer.get_unit_at(tile_pos)

	if other_unit == unit:
		return false
	
	var terrain_blocked = BLOCKED_TILES.has(atlas)

	if other_unit == null:
		#print("Tile ", tile_pos, " has no unit → terrain_blocked: ", terrain_blocked)
		return terrain_blocked

	# Block if other unit is not allied
	var enemy_blocked = not are_allied(unit.team, other_unit.team)
	#print("Tile ", tile_pos, " has unit ", other_unit.get_character_name(), " → enemy_blocked: ", enemy_blocked)
	
	return enemy_blocked

# Mark obstacles as solid in the A* pathfinding
func mark_player_obstacle(tile_pos: Vector2i, blocked: bool) -> void:
	#print("Marking obstacle at ", tile_pos, " → blocked: ", blocked)
	player_astar.set_point_solid(tile_pos, blocked)
	
	# Mark obstacles as solid in the A* pathfinding
func mark_enemy_obstacle(tile_pos: Vector2i, blocked: bool) -> void:
	#print("Marking obstacle at ", tile_pos, " → blocked: ", blocked)
	enemy_astar.set_point_solid(tile_pos, blocked)
	
func refresh_dynamic_obstacles():
	# 1) RESET BOTH GRIDS TO TERRAIN‑ONLY BLOCKING
	for x in range(map_width):
		for y in range(map_height):
			var cell := Vector2i(x, y)

			# Your existing terrain logic (atlas‑based)
			var atlas := tilemap_layer.get_cell_atlas_coords(cell)
			
			var terrain_blocked := BLOCKED_TILES.has(atlas)

			# Apply to BOTH grids
			player_astar.set_point_solid(cell, terrain_blocked)
			enemy_astar.set_point_solid(cell, terrain_blocked)


	# 2) APPLY UNIT‑BASED BLOCKING RULES
	#    (This is where player/enemy rules differ)
	for pos in tilemap_layer.units.keys():
		var unit : Unit = tilemap_layer.units[pos]

		match unit.team:

			# PLAYER + ALLY UNITS
			Unit.TEAM.PLAYER, Unit.TEAM.ALLY:
				# Player grid: CAN pass through → do nothing
				# Enemy grid: CANNOT pass through → block
				enemy_astar.set_point_solid(pos, true)

			# ENEMY UNITS
			Unit.TEAM.ENEMY:
				# Player grid: CANNOT pass through → block
				player_astar.set_point_solid(pos, true)
				# Enemy grid: CAN pass through → do nothing
	
# PATH CALCULATION

func _get_astar_for(unit: Unit) -> AStarGrid2D:
	if unit.team == Unit.TEAM.ENEMY:
		return enemy_astar
	return player_astar

# Compute a valid path within move cost
func new_compute_path(start: Vector2i, end: Vector2i, move_cost: int, unit: Unit) -> Array[Vector2i]:
	
	current_unit = unit
	
	var astar = _get_astar_for(unit)
	
	# 1. If the destination is an obstacle, no path is possible → return empty.
	if is_obstacle_for(unit, end):
		#print("Tile:", end, " is obstacle")
		return []
	
	# 2. Ask A* for a path.
	var raw_path: PackedVector2Array = astar.get_id_path(start, end)
	#print("raw_path:", raw_path)
	if raw_path.is_empty():
		return []
		
	# 3. Convert to Array[Vector2i]
	var path: Array[Vector2i] = []
	for p in raw_path:
		path.append(Vector2i(p))
		
	# 4. Remove start
	if not path.is_empty() and path[0] == start:
		path.pop_front()
		
	# 5. Clamp by movement
	if path.size() > move_cost:
		return []
		
	#print("Path:", path)
	return path

# Returns all grid positions reachable from 'start' within 'max_steps'
# Uses a breadth‑first search (BFS) to explore the grid
func get_reachable_positions(start: Vector2i, max_steps: int, unit: Unit) -> Array[Vector2i]:
	var result: Array[Vector2i] = []              # Stores all reachable positions
	var frontier: Array[Vector2i] = [start]       # Queue of positions to explore (BFS frontier)
	var dist := { start: 0 }                      # Dictionary: position -> distance (steps taken from start)
	
	var astar = _get_astar_for(unit)

	while frontier.size() > 0:                    # While there are still positions to explore
		var cur: Vector2i = frontier.pop_front()  # Take the next position from the frontier
		result.append(cur)                        # Mark it as reachable

		# Explore the 4 orthogonal neighbors (up, down, left, right)
		for dir in [Vector2i(1,0), Vector2i(-1,0), Vector2i(0,1), Vector2i(0,-1)]:
			var nxt: Vector2i = cur + dir         # Candidate next position

			# Skip if outside the map boundaries
			if nxt.x < 0 or nxt.y < 0 or nxt.x >= map_width or nxt.y >= map_height:
				continue

			# Skip if blocked (solid tile)
			#print("Checking:", nxt, "solid:", astar.is_point_solid(nxt))
			if astar.is_point_solid(nxt):
				continue

			# --- KEY PART: calculate the "cost" (steps taken so far) ---
			var nd: int = dist[cur] + 1            # Distance to neighbor = distance to current + 1 step

			# --- THIS IS WHERE max_steps IS ENFORCED ---
			# Only add this neighbor if:
			# 1. We haven't visited it yet (not in dist)
			# 2. The total steps so far (nd) do not exceed max_steps
			if nd <= max_steps and not dist.has(nxt):
				dist[nxt] = nd                     # Record the distance (cost) to reach this tile
				frontier.append(nxt)               # Add it to the BFS frontier for further exploration
				all_Positions.append(nxt) 
				
	# Include the player's actual location
	all_Positions.append(start) 
	
	# Return all reachable positions
	return result               

func printPath(path: Array[Vector2i]) -> void:
	print("\n=== PATH DEBUG ===")
	
	if path.is_empty():
		print("Path is empty.\n")
		return
	
	for tile in path:
		var unit : Unit = tilemap_layer.get_unit_at(tile)
		
		if unit:
			print("Tile:", tile, " → OCCUPIED by ", unit.get_character_name(),
				  " (team:", unit.team, ", pos:", unit.currentPosition, ")")
		else:
			print("Tile:", tile, " → empty")
	
	print("===================\n")

Problem

The problem, is that when I test my game, and I click on a player unit:

The pure obstacles tiles (the house for exemple) are painted in blue, while they should be not colored.

and also, here is my print text:

Tiles obstacles détectées :
[TurnManager] Player Turn begins.

I also had another kind of issue where I had a reverse problem, where there was almost no painted tiles.

It seems I have trouble to retrieve the custom data layer from a particular layer.

Can someone help me please ?

Thanks in advance

Paul NOWAK