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")