Enemies ignore built towers when navigation

4.3.stable

Hi guys!
I want to create a tower defense game where you plant towers and the enemies then get their paths updated to not bump into the towers.
Using these scenes with nav nodes:
Tower: NavigationObstacle2D
Enemy: NavigationAgent2D
NavigationRegion: NavigationRegion2D
Main: NavigationRegion instantiated, enemies spawned, towers placed during runtime.

Tower avoidance layer: 1
Enemy avoidance layer + mask: 1
Everything is on navigation layer: 1

Currently i am trying to create “holes” in the nav region where the towers are placed, but this doesnt seem to work properly.
When i have in the editor placed towers this looks correct and the enemies are avoiding them, but adding towers dynamically doesn’t affect the region.

bake_navigation_polygon doesn’t update.
Feels like i have looked through the entire internet, used all of chatgpts wisdow and nothing works. I am however a simple notice developer still, so some logic is beyond me.

Any ideas how to handle this?

Tower:

extends Node2D

@onready var navigation_obstacle_2d: NavigationObstacle2D = $NavigationObstacle2D
@export var footprint_size: Vector2i = Vector2i(64, 64)

var placed: bool = false

func _ready() -> void:
	#print("tower nav map: ", get_node("NavigationObstacle2D").get_navigation_map())
	pass


func _process(delta: float) -> void:
	pass


func tower_was_placed():
	navigation_obstacle_2d.avoidance_enabled = true
	placed = true
	#print("placed")
	#Global.emit_signal("tower_placed", global_position, footprint_size)

Enemy:

extends CharacterBody2D

@onready var navigation: NavigationAgent2D = $NavigationAgent2D
var enemy_goal: Node2D
var speed: float = 50.0

func _ready() -> void:
	# Get a reference to the enemy goal (make sure the goal node is in a group "enemy_goal")
	enemy_goal = get_tree().get_first_node_in_group("enemy_goal")
	if enemy_goal:
		update_path()  # Create initial path
	else:
		print("Enemy goal not found!")
	
	# Connect to a global signal so that when the navigation mesh is updated (tower placed/removed),
	# the enemy updates its path.
	Global.connect("navigation_mesh_updated", update_path)
	
	
	navigation.avoidance_enabled = true
	
	navigation.avoidance_mask = 1

func on_update_path_timer_timeout() -> void:
	update_path()


func update_path() -> void:
	if enemy_goal:
		
		#Set the target position of the NavigationAgent2D to the goal global position.
		navigation.target_position = enemy_goal.global_position
		navigation.path_changed
		
		#Wait until the path is recalculated.
		await navigation.path_changed
		
		print("New path: ", navigation.get_current_navigation_path())
	else:
		print("No enemy goal to update path for")

func _physics_process(delta: float) -> void:
	# If the agent's path is finished, do nothing.
	if navigation.is_navigation_finished():
		return
	
	# Get the next point in the current path.
	var next_position = navigation.get_next_path_position()
	# Calculate direction toward that point.
	var direction = (next_position - global_position).normalized()
	
	if direction.length() < 0.1:
		update_path()
	
	velocity = direction * speed
	move_and_slide()

NavigationRegion2D:

extends NavigationRegion2D

@onready var bake_mesh_timer: Timer = $BakeMeshTimer


@export var nav_region: NavigationRegion2D
var original_navpoly: NavigationPolygon
var holes_array: Array = []
var holes_index

var needs_update: bool

func _ready():
	Global.connect("tower_placed", add_tower_obstacle)
	
	#Duplicate the original NavigationPolygon
	if nav_region.navigation_polygon:
		original_navpoly = nav_region.navigation_polygon.duplicate() as NavigationPolygon
		print("NavigationPolygon duplicated")
	else:
		push_error("No NavigationPolygon found")
	
	bake_navigation_polygon()



func add_tower_obstacle(tower_global_position, tower_size) -> void:
	var local_pos = nav_region.to_local(tower_global_position) #local position of the placed tower
	var half_size = tower_size / 2 # creates a hole in the nav mesh from the tower
	var tower_poly : PackedVector2Array = [
		local_pos + Vector2(-half_size.x, -half_size.y),
		local_pos + Vector2(half_size.x, -half_size.y),
		local_pos + Vector2(half_size.x, half_size.y),
		local_pos + Vector2(-half_size.x, half_size.y),
	]
	
	holes_array.append(tower_poly)
	#print("current holes: ", holes_array)


func remove_tower_obstacle():
	#nav_region.navigation_polygon = original_navpoly.duplicate()
	nav_region.navigation_polygon.make_polygons_from_outlines()
	Global.emit_signal("navigation_mesh_updated")


func update_navigation_mesh():	
	var nav_poly: NavigationPolygon = nav_region.navigation_polygon
	if nav_poly == null:
		push_error("NavigationRegion2D has no navigation polygon")
	
	var source_data = NavigationMeshSourceGeometryData2D.new()
	
	var outer_outline: PackedVector2Array = original_navpoly.get_outline(0)
	#print("outer outline: ", outer_outline)
	if outer_outline:
		source_data.add_obstruction_outline(outer_outline)
	else:
		push_error("Original NavigationPolygon has no outline defined")
	
	for hole_outline in holes_array:
		#print("hole: ", hole_outline)
		var reversed_outline: PackedVector2Array = hole_outline.duplicate() as PackedVector2Array
		reversed_outline.reverse()
		#print("reversed hole: ", reversed_outline)
		source_data.add_obstruction_outline(reversed_outline)
	#
	NavigationServer2D.parse_source_geometry_data(nav_poly, source_data, nav_region)
	NavigationServer2D.bake_from_source_geometry_data(nav_poly, source_data)
	
	nav_region.force_update_transform()
	NavigationServer2D.map_force_update(nav_region)



func update_nav_polygon(new_navpoly):
	new_navpoly.make_polygons_from_outlines()
	nav_region.navigation_polygon = new_navpoly
	Global.emit_signal("navigation_mesh_updated")


func _on_bake_mesh_timer_timeout() -> void:
	bake_navigation_polygon()
	#print("baked!")
	await get_tree().process_frame
	update_navigation_mesh()
	Global.emit_signal("navigation_mesh_updated")

the tower should only be a staticbody and then get baked into the navmesh. NavigationObstacles are only for moving objects and wont be baked in the navmesh