Using NavigationRegion2D, NavigationPolygons, and TileMaps

Godot Version

4.2

Question

Hello,
I’ve been trying to create a NavigationPolygon for a tilemap in my scene. I’ve studied
https://forum.godotengine.org/t/creating-navigationregion2d-from-tilemap-data/40969
https://forum.godotengine.org/t/navigation-on-a-procedurally-generated-tilemap-with-obstacles/39309

But I’m unable to figure out exactly what I’m supposed to be doing, or what I’m doing wrong.

In the Ready function of my script I have the following code:

		navRegion = GetNode<NavigationRegion2D>("/root/World/NavigationRegion2D");

		navRegion.NavigationPolygon = new NavigationPolygon();
		navRegion.NavigationPolygon.Clear();
		navRegion.NavigationPolygon.SourceGeometryMode = NavigationPolygon.SourceGeometryModeEnum.RootNodeChildren;
		navRegion.BakeNavigationPolygon();

In my scene, my node tree is as follows:

World

NavigationRegion2D

TileMap

I’ve deactivated the “Navigation Enabled” checkbox on each layer of my TileMap, and the Layer with my colliders is on Layer 0.

However, when I run my scene with the debug setting “Visible Navigation,” I see no navigation mesh. What should I be doing to create a navigation mesh on my TileMap?

1 Like

Did you take note of this from that first thread?

The z ordering of my nodes impacts what debugging elements I can see. I have Visible Navigation checked, and seeing that remain blank sometimes was confusing. What seems to be happening is that elements later/lower in the scene tree will show up on top of those higher. So when using the root mode, I saw my debugging area. When using the group mode, I wasn’t seeing it because my NavigationRegion2D was higher in my scene tree. Moving it lower let me see those elements, as well as changing the Z Index of my TileMap lower (e.g. to -1).

That was what was messing with me.

1 Like

I’ve set my TileMap Z-Indexes very low (-5 to -10), and it’s still not showing. I don’t believe my mesh is being generated at all, or is being generated blank, but without the debug option I’m not sure how to test it. I’m still trying to learn the Navigation system.

You mentioned the layer with colliders is layer 0 on your tilemap. Did you also setup navigation areas on other tiles in the tileset?

1 Like

I did not. My layer 0 only includes my tiles with collision, with the rest of the map being empty. My understanding was that it ignores navigation layers if “Navigation Enabled” is unchecked on a layer. Should Layer 0’s collision tiles have some type of Navigation data attached to them?

Edit: I’ve tried adding a Navigation Layer to my TileMap and giving the solid collision tiles solid on the Navigation Layer tileset.

Quoting @smix8 from that same thread (my bolding):

The TileMap adds all its cells with a navigation polygon on the first TileMapLayer as traversable, while all the TileMap cells with a physics collision polygon are added as obstructions that “cut” into the navmesh later. It is important to note that only the first TileMapLayer gets parsed and you can not stack / overlap navigation polygons.

So I believe you need to create a navigation layer and put navigation polygons on your navigable tiles. If I was at my computer I’d confirm that for you but give that a go!

1 Like

Sorry! I’d just edited my prior post to indicate that I’ve tried that, and it had no impact on my results. I’ve attached an image of my workspace below, which I hope should better demonstrate my situation.

When I run the scene, I do not see a navigation polygon.

Okay so this is unlikely to be THE issue, but your screenshot seems to show that your non-navigation tiles (e.g. the fence) has navigation data. You want navigation mask data to be on your floor tiles, and physics (collision) data on your fences, etc.

Edit: have you tried starting the scene and seeing if navigation works yet to rule out visual/debug issues?

Edit2: is there a reason not to bake in the editor rather than from code?

1 Like

Ok, gotcha! I’ve fixed that. The highlighted tiles on the Navigation layer are the ground tiles, and the tiles that aren’t to be walked on are clear. I’m still not generating a navigation mesh, though, which makes me believe it may have something to do with how I’m setting up my code?

Edit: I have indeed tried running the scene, and am not seeing the map being generated. For my project, I plan to use procedurally generated tilemaps, so I’ll need to be able to generate my navigation meshes at runtime as well.

1 Like

I’ve been doing some research for code examples, and I did find this. On my phone it’s tricky to distinguish all the different types, but perhaps this will crack it open for you, as it seems like you might be missing some steps!

Hmm, I see. I’ve written out this code in an attempt to replicate what they’re doing, and don’t seem to be getting any results. I’m getting an “Invalid Navigation Polygon” error, and unfortunately I’m not familiar enough with the system to really understand what could be going on.

Below I’ve pasted all my code. The only changes I’ve made to the scene is that I’ve removed the NavigationRegion2D and moved the TileMap to the top level of the node tree.

public partial class Navpath : Node2D
{
    //from https://github.com/godotengine/godot-docs/blob/master/tutorials/navigation/navigation_using_navigationmeshes.rst#navigation-mesh-script-templates

	NavigationPolygon poly;
	Rid resourceRegionID;
	Callable Call_Parse;
	Callable Call_Bake;
	NavigationMeshSourceGeometryData2D meshSource;

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
	{

        meshSource = new NavigationMeshSourceGeometryData2D();
        Call_Parse = new Callable(this, Navpath.MethodName.OnParseComplete);
		Call_Bake = new Callable(this, Navpath.MethodName.OnBakeComplete);
        resourceRegionID = NavigationServer2D.RegionCreate();

		NavigationServer2D.RegionSetEnabled(resourceRegionID, true);
		NavigationServer2D.RegionSetMap(resourceRegionID, GetWorld2D().NavigationMap);

		CallDeferred(Navpath.MethodName.ParseSourceGeometry);
    }

    private void ParseSourceGeometry()
    {
		meshSource.Clear();
		Node2D rootNode = this;

		//parse obstructionoutlines from all child nodes of the root node by default
		NavigationServer2D.ParseSourceGeometryData(poly, meshSource, this, Call_Parse);
    }

    private void OnParseComplete()
    {
        GD.Print("Parse complete");

        Vector2[] Points = {new Vector2(0.0f, 0.0f),
            new Vector2(500.0f, 0.0f),
            new Vector2(500.0f, 500.0f),
            new Vector2(0.0f, 500.0f)
        };

        meshSource.AddTraversableOutline(Points);
        NavigationServer2D.BakeFromSourceGeometryDataAsync(poly, meshSource, Call_Bake);
    }
    private void OnBakeComplete()
    {
        GD.Print("Bake complete");
        //update the region with the updated navmesh
        NavigationServer2D.RegionSetNavigationPolygon(resourceRegionID, poly);
    }
}

I suggest that you first try to bake with the NavigationRegion2D node in the editor to confirm that your setup is even working and to rule-out your own script bugs.

The simplest setup is a NavigationRegion2D with a NavigationPolygon and the TileMap as a child node with just some cells with navigation polygon on the TileMapLayer0. If that is not working something else is fundamentally broken.

If the setup is working in the Editor with a NavigationRegion2D doing the baking with scripts should be trivial since you already did the majority of the setup. Also for testing reduce the NavigationPolygon agent radius to zero to also rule out any issues with having too small tiles for the default 10 pixel value.

There is also an endless stream of C# specific bugs with wrong type use that turns the arrays empty. So if the editor setup is working and your C# script isnt also try to do the same in GDScript, and if GDScript is working double-check all your types in C#.

2 Likes

In addition, just to be sure, since I didn’t see this in your posts specifically: you also need to have a navigation polygon that covers your map.

Thank you @smix8 for the correction.

Not necessarily. A TileMap can be independent when it has cells with navigation polygons because those get added to the source geometry in the baking process. Adding a bounding outline with the NavigationRegion2D while the TileMap cells are all nested inside might even cause problems like flipped polygons.

1 Like

My apologies but I’m afraid I am deeply confused by the Navigation Layers in TileMaps. The Navigation Area of a tilemap is the “solid” color blocks painted on the Navigation Layer, which provides the walkable space, correct? So if I have a “ground” tile on a space, that area will be walkable, and in order to make a non-walkable space, I will need to have a non-walkable tile.

Does this mean that ALL tiles will need to be drawn on Layer 0, removing the ability to layer tilemaps for features such as transparency? Or would I need to create an “invisible” simple tilemap of only blocks and clear spaces to sit on Layer 0, behind my “visual” tilemaps?

I apologize if this is a simple question but I’m finding it extremely difficult to find information on these systems.

Edit: I was able to generate a tilemap by following the following steps:

  • Create a NavigationRegion2D
  • Add the Tilemap under it
  • On Layer 0, add any collision bodies.
  • Under the NavigationPolygon, change “SourceGeometryMode” to “Group with Children”
  • Add the “Source Geometry Group Name” to the Tilemap as a Group
  • Draw a bounding box around the level’s geometry in the NavigationRegion2D (MANDATORY, as otherwise you cannot bake a NavigationPolygon)
  • Bake NavigationPolygon

So it seems like the Navigation layers on a tilemap don’t actually matter, and that it’s necessary to put any objects that need to be read by the navigation mesh into a single group. Is this correct?

Edit2:

Moving on from that, I’ve attempted to once again create a mesh from code. This time, I’ve got a NavigationRegion2D in the scene with a NavigationPolygon attached. The NavigationPolygon is set to “Group with Children”, and the TileMap (itself a child of the NavigationRegion2D) is in that group.

My plan would be that this would allow me to generate tiles on the TileMap, and simply regenerate the NavigationRegion2D’s NavigationPolygon as the layout changes.

I used the following code to try and bake the NavigationPolygon, working from the example found here NavigationRegion2D baking with groups (explicit or recursive) doesn't work as expected at runtime (but it does in the editor) · Issue #86414 · godotengine/godot · GitHub in which the user is able to generate NavigationPolygons from code:

public partial class NavRegion_Generator : NavigationRegion2D
{
    Callable Call_Parse;
    Callable Call_Bake;

    NavigationMeshSourceGeometryData2D sourceGeo;

    public override void _Ready()
	{
        Call_Parse = new Callable(this, Navpath.MethodName.OnParseComplete);
        Call_Bake = new Callable(this, Navpath.MethodName.OnBakeComplete);
        CreateNewNavPolygon();
    }

    public void CreateNewNavPolygon()
    {
        this.NavigationPolygon.Clear();
        sourceGeo = new NavigationMeshSourceGeometryData2D();
        CallDeferred(Navpath.MethodName.ParseSourceGeometry);
    }

    public void ParseSourceGeometry()
    {
        NavigationServer2D.ParseSourceGeometryData(this.NavigationPolygon, sourceGeo, this, Call_Parse);
    }

    public void OnParseComplete()
    {
        NavigationServer2D.BakeFromSourceGeometryDataAsync(this.NavigationPolygon, sourceGeo, Call_Bake);
    }

    public void OnBakeComplete()
    {
        GD.Print("BAKE COMPLETE");
    }
}


I can confirm that OnBakeComplete does run, but this generates no NavigationPolygon. I have also rewritten the same code in GDScript and still get no result. The code can be found below:

extends NavigationRegion2D

var Call_Parse : Callable
var Call_Bake : Callable
var sourceGeo : NavigationMeshSourceGeometryData2D
	
# Called when the node enters the scene tree for the first time.
func _ready():
	Call_Parse = Callable(self, "OnParseComplete")
	Call_Bake = Callable(self, "OnBakeComplete")
	CreateNewNavPolygon()

func CreateNewNavPolygon():
	self.navigation_polygon.clear()
	sourceGeo = NavigationMeshSourceGeometryData2D.new()
	call_deferred("ParseSourceGeometry");


func ParseSourceGeometry():
	NavigationServer2D.parse_source_geometry_data(self.navigation_polygon, sourceGeo, self, Call_Parse)

func OnParseComplete():
	NavigationServer2D.bake_from_source_geometry_data_async(self.navigation_polygon, sourceGeo, Call_Bake)

func OnBakeComplete():
	print("BAKE COMPLETE");

Edit3:

I’ve looked here Using navigation meshes — Godot Engine (latest) documentation in English as well, and tried using the code provided. I’ve pasted the code I’ve used below:

public partial class NavMesh_TestTwo : NavigationRegion2D
{
	NavigationPolygon navPolygon;
	Rid regionRID;
	public override void _Ready()
	{
		navPolygon = new NavigationPolygon();
		regionRID = NavigationServer2D.RegionCreate();

		NavigationServer2D.RegionSetEnabled(regionRID, true);
		NavigationServer2D.RegionSetMap(regionRID, GetWorld2D().NavigationMap);


        navPolygon.Vertices = new Vector2[] {new Vector2(0.0f, 0.0f),
								new Vector2(100.0f, 0.0f),
								new Vector2(100.0f, 100.0f),
								new Vector2(0.0f, 100.0f) };

		navPolygon.AddPolygon(new int[] { 0, 1, 2, 3 });


		NavigationServer2D.RegionSetNavigationPolygon(regionRID, navPolygon);
    }
}

As none of these scripts output a NavigationPolygon, it seems that all my attempts to generate a NagivationPolygon have come to nothing. I’m at a bit of a loss as to where to go from here.