How to correctly generate NavigationObstacle2Ds at run time?

Godot Version

v4.3.stable.arch_linux

Question

I’m trying to use the 2D Navigation nodes to enable character pathfinding in my project, which features a procedurally-generated 2D world sorted into 256x256 pixel chunks, represented in the engine as custom Node2Ds.

I generate a NavigationRegion2D for each chunk upon chunk generation like so:

chunk.navigation_region = NavigationRegion2D.new()
var navigation_mesh = NavigationPolygon.new()
var navigation_mesh_vertices = PackedVector2Array([
	Vector2(chunk_size * x, chunk_size * y), 
	Vector2(chunk_size * x, chunk_size * (y+1)), 
	Vector2(chunk_size * (x+1), chunk_size * (y+1)), 
	Vector2(chunk_size * (x+1), chunk_size * y)
])
navigation_mesh.vertices = navigation_mesh_vertices
var polygon_indices = PackedInt32Array([0, 1, 2, 3])
navigation_mesh.add_polygon(polygon_indices)
chunk.navigation_region.navigation_polygon = navigation_mesh
chunk.add_child(chunk.navigation_region)

My characters each have a NavigationAgent2D, and up to this point in the setup they have no problem setting their target_position and walking to it after querying get_next_path_position().

My problem occurs when I try to add a NavigationObstacle2D to the chunk, as a child of the chunk’s NavigationRegion2D (“Palm” in the variable names because this obstacle represents the trunk of a palm tree):

var palm_navigation_obstacle = NavigationObstacle2D.new()
var palm_navigation_obstacle_vertices = PackedVector2Array([
	Vector2(8, 8), 
	Vector2(-8, 8), 
	Vector2(-8, 0), 
	Vector2(8, 0)
])
palm_navigation_obstacle.vertices = palm_navigation_obstacle_vertices
palm_navigation_obstacle.position = palm.position
palm_navigation_obstacle.affect_navigation_mesh = true
palm_navigation_obstacle.avoidance_enabled = true
palm_navigation_obstacle.carve_navigation_mesh = true
palm_navigation_obstacle.radius = 8
chunk.navigation_region.add_child(palm_navigation_obstacle)

The agents completely ignore these obstacles, they walk right through them. Assuming perhaps this is because carve_navigation_mesh doesn’t work until baked, I try re-baking the NavigationRegion after the chunk is added to the world…

chunk.navigation_region.bake_navigation_polygon()

…Which breaks agent navigation completely, they just stand in place. I assume because this call also breaks the NavigationRegion2D.

Please aid me, Godot geniuses! Is there a viable way here to generate NavigationObstacle2Ds / carve up existing NavigationRegion2Ds at run time?

[edit: I’ve since confirmed using the debug visuals, that calling bake_navigation_polygon() on the NavigationRegion2Ds at run time deletes their polygon geometry. I’ve also tested with Polygon2D child objects in place of NavigationObstacle2D child objects, same result! Instead of the child scene geometry punching holes in the mesh, the mesh just disappears.]

1 Like

In the first code you are creating a procedural 2d navmesh directly, which is fine, but also entirely unrelated to any navmesh baking.

In the second code you are trying to bake a navmesh without any traversable geometry. Having no geometry this obviously can not work and yields an empty navmesh.

So what you need to do is …

First, make up your mind if you want to work with procedural navmesh or with baked navmesh. You can’t do both for the same region because a baked navmesh will override your procedural navmesh data entirely.

Second, assuming you want to go with baked navmesh you need to add actual traversable geometry for the baking. Either you add the geometry as region outlines, or with some kind of Nodes for that regions to bake, or you use a NavigationMeshSourceGeometryData2D object and add all your geometry procedual and bake with the NavigationServer2D.

If you have this traversable geometry you can “cut” inside that geometry by adding obstruction geometry. Either with your obstacles or any other bakeable obstruction geometry like StaticBody2D nodes or scripts that add the arrays.

The navmesh documentation has script examples for all that. Using navigation meshes — Godot Engine (latest) documentation in English

1 Like

Thanks. But I thought that a NavigationMesh2D’s vertices field does represent the traversable geometry? When I pass it a square represented by the PackedVector2Array (As in my first code block above) it creates a square nav mesh that my nav agents do indeed traverse.

I actually don’t want to have to pre-bake anything if it can be reasonably avoided. Since my game world is procedural, I think my navmeshes should also be procedural. I guess I’m just confused about when you can and cannot bake a navmesh, because the documentation (In fact, the page you linked) does explicitly say “The NavigationRegion2D baking can also be used at runtime with scripts.” and shows the same bake_navigation_polygon() call that is breaking the mesh for me. Working off that information, up until now my plan had been: 1) Make a nav mesh that covers the entire chunk 2) Add procedurally-placed scene objects to the chunk childed below the navmesh 3) Bake the nav mesh to incorporate the new scene objects into the mesh.

So I guess my question then boils down to: If I want to generate a nav mesh with obstacle holes (Informed by child geo) at run time, what is the “correct” way to do it? I did come across the NavigationMeshSourceGeometryData2D-based method in the docs, I haven’t tried that yet because I was relying on debug visuals and the docs say that the nav server doesn’t update debug visuals. But, would that be the recommended way?

[edit: I GOT IT WORKING! After pouring over the 2D navmesh demo here (Which it looks like you created? Thank you!) and working to replicate it’s steps in my own code: godot-demo-projects/2d/navigation_mesh_chunks/navmesh_chhunks_demo_2d.gd at master · godotengine/godot-demo-projects · GitHub I’ll add a new reply detailing my own solution after I get it cleaned up]

Here is my solution, adapted from your 2D example I linked above. Your example features a chunk-based finite world, while mine is a chunk-based infinite world. Therefore I cannot use a single NavigationMeshSourceGeometryData2D, instead I create a new one alongside each new chunk I generate.

For anyone in the future working on a 2D infinite procedural world looking to get starting with the navigation system, here’s my world generator stripped down to just the navigation code, and heavily annotated:

class_name world_manager extends Node2D

const chunk_size: int = 256
var chunks = {}
@onready var world: Node2D = get_node("my root node")

class WorldChunk extends Node2D:
	var coordinates: Vector2

func _ready() -> void:
	NavigationServer2D.set_debug_enabled(true)
	var map: RID = get_world_2d().navigation_map
	NavigationServer2D.map_set_cell_size(map, 1.0)
	NavigationServer2D.map_set_use_edge_connections(map, false)

**CODE HERE THAT CALLS GENERATE_CHUNK AS NEEDED**
	
func generate_chunk(x: int, y: int) -> void:
	var chunk = WorldChunk.new()
	world.add_child(chunk)
	
	**CODE HERE THAT POPULATES THE CHUNK WITH CHILD NODES CONTAINING GEOMETRY DATA, LIKE POLYGON2D**
	
	#Initialize a NavigationMeshSourceGeometryData2D for geometry parsing
	#as well as a NavigationPolygon which acts first as settings for the SourceGeo
	#and then as the container for the baked output
	var source_geometry = NavigationMeshSourceGeometryData2D.new()
	var navigation_polygon = NavigationPolygon.new()
	
	#These settings are specific to my scene, yours will vary
	navigation_polygon.parsed_geometry_type = NavigationPolygon.PARSED_GEOMETRY_BOTH
	navigation_polygon.agent_radius = 4.0
	
	#have the SourceGeo parse geometry from chunk child nodes
	NavigationServer2D.parse_source_geometry_data(navigation_polygon, source_geometry, chunk)
	
	#Calculate a bounds that contains all the parsed source geometry,
	#then grow it in size by one chunk to account for neighboring geo 
	#that overhangs into this chunk
	var input_geometry_bounds = Rect2(x * chunk_size, y * chunk_size, chunk_size, chunk_size)
	var input_geometry_bounds_grow = input_geometry_bounds.grow(chunk_size)
	navigation_polygon.baking_rect = input_geometry_bounds_grow
	navigation_polygon.border_size = chunk_size
	
	#Add a traversable_outline to the SourceGeo which will be the "blank canvas"
	#upon which our parsed geometry data will punch holes.
	#I make this as big as the baking_rect because I find that otherwise
	#my border_size doesn't prevent the resulting polygon from getting shrunk
	#by agent_radius pixels.
	var traversable_outline: PackedVector2Array = PackedVector2Array([
		Vector2(chunk_size * x - chunk_size, chunk_size * y - chunk_size), 
		Vector2(chunk_size * x - chunk_size, chunk_size * (y+1) + chunk_size), 
		Vector2(chunk_size * (x+1) + chunk_size, chunk_size * (y+1) + chunk_size), 
		Vector2(chunk_size * (x+1) + chunk_size, chunk_size * y - chunk_size)
	])
	source_geometry.add_traversable_outline(traversable_outline)
	
	#Now bake! Punch those holes!
	NavigationServer2D.bake_from_source_geometry_data(navigation_polygon, source_geometry)

	#Snap verticies to avoid float precision issues
	var navmesh_vertices: PackedVector2Array = navigation_polygon.vertices
	for i in navmesh_vertices.size():
		var vertex: Vector2 = navmesh_vertices[i]
		navmesh_vertices[i] = vertex.snappedf(0.1)
	navigation_polygon.vertices = navmesh_vertices
	
	#Slap that processed NavigationPolygon onto a new NavigationRegion
	#and store it on the chunk
	var navigation_region: NavigationRegion2D = NavigationRegion2D.new()
	navigation_region.navigation_polygon = navigation_polygon
	chunk.add_child(navigation_region)
	
	chunks[str(x) + "_" + str(y)] = chunk