The problem with roofs (and more)

Godot Version

4.3

Question

(EVERYTHING HERE HAS BEEN TRANSLATED FROM RUSSIAN INTO ENGLISH, I’M NOT SURE IF THE SPELLING IS CORRECT)

For the second day now, I have been trying to implement a system that when a player enters a building or looks into the passage of the building with his field of view, the roof should disappear, and when he is not in the building or does not look into the open passage, the roof should appear accordingly, but nothing happens.

I write all the code with the help of AI, since I myself am soooo far from the topic of coding, and up to the moment I wanted to implement it, everything worked out for me, now I will explain in more detail the structure of my project.

first of all, this is a 2D game with a top view, here is the structure of the nodes of the scene:

-Node2D
—TileMap
-----TileMapLayer (there are surface tiles here)
-----TileMap Layer 2 (here are the wall tiles)
-----TileMap Layer 3 (here are the roof tiles)
—Character Body 2D
-----Animated Sprite 2D
-----Collision Shape 2D
-----Camera2D
-----FieldOfView
-------CollisionPolygon2D

now about the file system (don’t pay attention to the fact that the names are strange, I just write Russian words with an English layout, it’s more convenient for me to understand):

-Game folder
—sprayty (sprites)
-----beton_stena_old1.png (wall)
-----beton_stena_old(krest).png (wall)
-----beton_stena_old(perekrest).png (wall)
-----beton_stena_old(ugol).png (wall)
-----beton_stena_old.png (wall)
-----Human1(ruki0).png (player)
-----Human1(ruki1).png (player)
-----Human1(ruki).png (player)
-----krysha(metall_old).png (roof)
-----krysha.tres (all roof tiles)
-----krysha_krai1(metall_old).png (roof)
-----krysha_krai(metall_old).png (roof)
-----krysha_ugol1(metall_old).png (roof)
-----krysha_ugol2(metall_old).png (roof)
-----krysha_ugol3(metall_old).png (roof)
-----krysha_ugol(metall_old).png (roof)
-----pesok.png (sand)
-----pesok_plitka.png (sand/tile)
-----pesok_zelen.png (sand)
-----plitka_old.png (tile)
-----poverhnost.tres (all tiles)
-----stena.tres (all wall tiles)
—character_body_2d.gd
—export_presets.cfg
—icon.png
—Player.tscn
—RoofTileMap.gd

in the project settings, in the “layer names” and “2D Physics” sections, I have names for different layers, namely:

Layer 1 stena (wall)
Layer 2 krysha (roof)
Layer 3 igrok (player)
Layer 4 poverhnost (surface)
Layer 5 npc (for the future)
Layer 6 vragi (Enemies for the future)
Layer 7 FieldOfView

just in case, I’ll also write down which nodes are in Nodded and which layers they have in Collision Layer and Collision Mask:

TileMapLayer:

Collision Layer: 4
Mask Layer: nothing

TileMapLayer2:

Collision Layer: 1
Mask Layer: 3, 4, 6, 7

TileMapLayer3:

Collision Layer: 2
Mask Layer: 3, 7

CharacterBody2D:

Layer: 3
Mask: 1

FieldOfView:

Layer: 7
Mask: 1, 2

here is the script that is in CharacterBody2D (if the character is working fine, everything is as it should be, but just in case, I’m describing the whole project to you, because I don’t know how to solve my problem):

extends CharacterBody2D

# Параметры скорости
var walk_speed = 200      # Скорость ходьбы вперёд
var run_speed = 300       # Скорость бега
var slow_speed = 100      # Скорость при движении назад или вбок

func _physics_process(delta):
	var input_vector = Vector2.ZERO

	# Получаем ввод от игрока
	if Input.is_action_pressed("move_forward"):
		input_vector.y -= 1  # Вперёд
	if Input.is_action_pressed("move_backward"):
		input_vector.y += 1  # Назад
	if Input.is_action_pressed("move_left"):
		input_vector.x -= 1  # Влево
	if Input.is_action_pressed("move_right"):
		input_vector.x += 1  # Вправо

	# Нормализуем вектор ввода, если он не нулевой
	if input_vector.length() > 0:
		input_vector = input_vector.normalized()

	# Позиция курсора мыши
	var mouse_position = get_global_mouse_position()

	# Вектор взгляда персонажа
	var look_direction = (mouse_position - global_position).normalized()

	# Поворот персонажа к курсору мыши с корректировкой угла
	var target_rotation = look_direction.angle() + PI / 2
	rotation = lerp_angle(rotation, target_rotation, 0.2)

	if input_vector != Vector2.ZERO:
		# Трансформируем ввод в глобальное направление с учётом поворота
		var movement = input_vector.rotated(rotation)

		# Проверяем, нажата ли клавиша "move_forward" (W)
		var is_moving_forward = Input.is_action_pressed("move_forward")

		# Определяем текущую скорость и анимацию
		var current_speed = slow_speed  # По умолчанию медленная скорость

		if is_moving_forward:
			current_speed = walk_speed  # Нормальная скорость при движении вперёд

			# Разрешаем бег при нажатии клавиши бега
			if Input.is_action_pressed("run"):
				current_speed = run_speed
				set_animation("bezhit")
			else:
				set_animation("idet")
		else:
			# Бег запрещён, движемся медленно
			set_animation("idet_medlenno")

		# Устанавливаем скорость движения
		velocity = movement * current_speed
	else:
		velocity = Vector2.ZERO
		set_animation("stoit")

	# Перемещаем персонажа
	move_and_slide()

func set_animation(animation_name):
	if $AnimatedSprite2D.animation != animation_name:
		$AnimatedSprite2D.animation = animation_name
		$AnimatedSprite2D.play()

(end of the code)

the script that stands in TileMap (which is just supposed to make the roofs disappear and reappear):

extends TileMap

const CELL_SIZE = Vector2(16, 16)  # Укажите фактический размер ваших тайлов
const INVALID_CELL = -1  # Определяем константу для недопустимого ID тайла

var original_roof_tiles = {}

func _process(_delta):
	update_roof_visibility()

func update_roof_visibility():
	# Получаем узлы игрока и поля зрения
	var player = get_node("../CharacterBody2D")
	if player == null:
		print("Не удалось найти узел CharacterBody2D")
		return
	var fov = player.get_node("FieldOfView")
	if fov == null:
		print("Не удалось найти узел FieldOfView")
		return

	# Предполагаем, что крыши находятся на слое 2
	var roof_layer = 3

	# Получаем список всех позиций тайлов на слое крыши
	var all_tiles = get_used_cells(roof_layer)
	if all_tiles.size() == 0:
		print("Нет тайлов на слое крыши")
	else:
		print("Найдено ", all_tiles.size(), " тайлов на слое крыши")

	var roof_tiles = []

	for tile_pos in all_tiles:
		var tile_data = get_cell_tile_data(roof_layer, tile_pos)
		if tile_data != null:
			var tile_id = tile_data.tile_id
			var tile_name = tile_set.tile_get_name(tile_id)
			print("Тайл на позиции ", tile_pos, " имеет имя ", tile_name)
			if tile_name == "Roof":
				roof_tiles.append(tile_pos)
		else:
			print("Нет данных тайла для позиции ", tile_pos)

	if roof_tiles.size() == 0:
		print("Не найдено тайлов с именем 'Roof'")
	else:
		print("Найдено ", roof_tiles.size(), " тайлов крыши с именем 'Roof'")

	for tile_pos in roof_tiles:
		# Вычисляем мировую позицию тайла
		var cell_position = map_to_local(tile_pos)
		var world_pos = global_position + cell_position + (CELL_SIZE / 2)

		# Проверяем, виден ли тайл из поля зрения
		if is_visible_from_fov(world_pos, fov):
			# Убираем тайл крыши
			var current_tile = get_cell_source_id(roof_layer, tile_pos)
			if current_tile != INVALID_CELL:
				print("Убираем тайл крыши на позиции ", tile_pos)
				original_roof_tiles[tile_pos] = current_tile
				set_cell(roof_layer, tile_pos, INVALID_CELL)
		else:
			# Восстанавливаем тайл крыши
			if get_cell_source_id(roof_layer, tile_pos) == INVALID_CELL and tile_pos in original_roof_tiles:
				print("Восстанавливаем тайл крыши на позиции ", tile_pos)
				set_cell(roof_layer, tile_pos, original_roof_tiles[tile_pos])
				original_roof_tiles.erase(tile_pos)

func is_visible_from_fov(target_position, fov):
	var space_state = get_world_2d().direct_space_state
	var query = PhysicsRayQueryParameters2D.new()
	query.from = fov.global_position
	query.to = target_position
	query.exclude = [self, fov]
	# Слой "stena" имеет номер 1
	query.collision_mask = 1 << (1 - 1)  # 1 << 0 = 1
	query.collide_with_bodies = true
	query.collide_with_areas = false
	var result = space_state.intersect_ray(query)
	if result != null:
		print("Луч пересек препятствие при проверке позиции ", target_position)
	else:
		print("Прямая видимость до позиции ", target_position)
	return result == null

(end of the code)

please help me, I have been suffering for two days now, if there are any questions, I will try to answer.

Help Programming Physics Animation godot-4 gdscript 2d game tilemap

Here is a test project with what you went. I commented the code to explain how it work. I’m not English either so they can have some misspelling.
https://www.swisstransfer.com/d/5b5b6812-c0dc-4c18-8daa-b3dae14e3054

In case the link do not work anymore (limited in time and download count) here is the script attach to the roof tilemaplayer :

Roof tilemaplayer script
extends TileMapLayer

const INVALID_CELL = -1

@onready var player_node : CharacterBody2D = get_node("../../CharacterBody2D")
@onready var player_fov_shape : CollisionPolygon2D = player_node.get_node("FieldOfView/CollisionPolygon2D")
var _last_player_position = Vector2.INF
var _max_distance = 0;

var original_roof_tiles = {}

func _ready():
	# calculate the maximum distance FOV can reach
	for v in player_fov_shape.polygon:
		var dist = player_node.position.distance_to(v + player_fov_shape.global_position)
		if dist > _max_distance:
			_max_distance = dist

func _physics_process(delta):
	# Update only if player move
	if (player_node.global_position != _last_player_position):
		_last_player_position = player_node.global_position
		
		# Get bounding box of all tile to test
		var upper_left_corner : Vector2i = local_to_map(to_local(_last_player_position - _max_distance * Vector2.ONE))
		var bottom_right_corner : Vector2i = local_to_map(to_local(_last_player_position + _max_distance * Vector2.ONE))
		
		# NOTE maybe you need to take 1 more tile on each side because replace back 
		# original tile when out of view only occurs in the FOV bounding box
		for x in range(upper_left_corner.x, bottom_right_corner.x + 1):
			for y in range(upper_left_corner.y, bottom_right_corner.y + 1):
				
				var deleted = false
				var tile_position = Vector2(x,y);
				var actual_position = to_global(map_to_local(tile_position));
				# because fov polygon vector are not transformed with it's parent, 
				# I need to apply the reverse rotation of the player to the test 
				# point position before use Geometry2D to check if point is inside
				# FOV polygon
				var transformed_position = (actual_position - _last_player_position).rotated(-player_node.rotation) + _last_player_position - player_fov_shape.global_position;
				
				# Check if point is inside FOV polygon
				if Geometry2D.is_point_in_polygon(transformed_position, player_fov_shape.polygon):
					# Check if actual position is visible by the player (no wall inbetween) 
					if (is_visible_from_player(actual_position)):
						deleted = true;
						# If tile exist, save tile data
						var current_tile = {
							source_id = get_cell_source_id(tile_position), 
							atlas_coord = get_cell_atlas_coords(tile_position)}
						if current_tile.source_id != INVALID_CELL:
							original_roof_tiles[tile_position] = current_tile
							set_cell(tile_position, INVALID_CELL)
							set_cell
				# if cell is outside the FOV -> replace with the original roof tile
				if not deleted:
					if tile_position in original_roof_tiles:
						set_cell(tile_position, original_roof_tiles[tile_position].source_id, original_roof_tiles[tile_position].atlas_coord)
						original_roof_tiles.erase(tile_position)


func is_visible_from_player(target_position):
	var space_state = get_world_2d().direct_space_state
	var query = PhysicsRayQueryParameters2D.new()
	query.from = player_node.global_position
	query.to = target_position
	query.exclude = [player_node]
	# Слой "stena" имеет номер 1
	query.collision_mask = 1 << (1 - 1)  # 1 << (layer_id -1)
	query.collide_with_bodies = true
	query.collide_with_areas = false
	var result = space_state.intersect_ray(query)
	return result.is_empty()