The above would be a perfectly valid way to implement navigation in our scene. However, it might happen that, despite having used tilemaps to build the scene, we prefer to rely on the performance offered by Godot’s mesh pathfinding, based on what I explained in my article about 2D navigation in Godot. The problem is that in that article we assumed a scene that only included obstacles and a uniform walkable area, so we didn’t explain how to apply costs to specific areas of the walkable zone. Let’s see how to do that.
First, it’s important to clarify that Godot’s interface mixes different navigation methods, using similar terms, which makes it easy to get confused and feel lost.
Navigation integrated into TileMaps
In my opinion, the first source of confusion is that when we enter the configuration of a TileSet—by expanding the Tile Set resource of the TileMap that uses it—the category of Navigation Layers has nothing to do with mesh pathfinding navigation. These navigation layers are assigned to the TileSet here, but they are created in Project > Project Settings... > General > Layer Names > 2D Navigation.
![]() |
| Creating navigation layers in Project Settings |
![]() |
| Assignment of navigation layers to a TileSet resource in the configuration of a TileMapLayer |
Once assigned to the TileSet at the resource level, you can define the presence of each tile in each navigation layer by “drawing” a polygon in the Navigation section of the TileSet tab, similar to how you would define the tile’s physics layer.
![]() |
| Assignment of a tile to a navigation layer. |
After that, you need to enable navigation in the TileMap by checking its Navigation option.
![]() |
| Navigation option, within the TileMap settings. |
With that, everything is ready for an agent to traverse the scene using an API similar to the one we saw in the aforementioned article on 2D navigation in Godot. The difference compared to that article is that you don’t need to place a NavigationRegion2D over the TileMapLayer, and at the agent level, the node to use is not MeshNavigationAgent2D but simply NavigationAgent2D.
This method is faster and more straightforward, but it has two major drawbacks:
- It performs very poorly, so it’s only viable for small scenes.
- It doesn’t allow setting different cost areas in the walkable zone, so it only makes sense if your walkable area is uniform.
In reality, the purpose of assigning navigation layers here is to define which tiles different agents can traverse, not to assign costs. For example, a bat agent will navigate through sky tiles, while the main character agent will move across ground tiles.
These drawbacks are what led me to focus the previous article on 2D navigation in Godot using mesh pathfinding. As we’ll see, this method solves the previous issues but is a bit more complex to set up.
Mesh navigation with slow zones
To explain this method, we’ll start from what I already covered in the article on 2D navigation in Godot mentioned earlier. So if you haven’t read it yet, now would be a good time. In that article, we created a scene with only three types of elements: walls and obstacles (both tiles with colliders configured in their respective physics layers) and floor tiles, which had no collider. We also defined a NavigationRegion2D, which contained the TileMapLayers and covered the entire inner area of the scene. Starting from that setup, in this article we’ll add tiles that are walkable but have a higher-than-normal cost. For example, these “slow” tiles could be grass (green), sand (orange), and water (blue).
When you click “Bake NavigationPolygon,” NavigationRegion2D overlays a polygon on the area you specified and trims zones where it detects a collider. The resulting polygon defines the NavigationRegion2D. The colliders considered in this process are those included in the physics layers marked in the Parsed Collision Mask field of the NavigationPolygon configuration.
![]() |
| Configuration of the physical layers to consider for shaping a NavigationRegion2D |
Be careful because NavigationRegion2D also has a Navigation Layers field, which might make you think that when shaping the NavigationPolygon, only tiles associated with certain navigation layers will be considered. However, that’s not the case. When shaping the NavigationPolygon, only colliders associated with tiles are considered, regardless of their navigation layer. In fact, if you use this method, you probably won’t need to assign any navigation layer to the TileMapLayers. That only made sense with navigation integrated into TileMaps.
![]() |
| NavigationRegion2D configuration |
So, what is the Navigation Layers field in NavigationRegion2D for? It’s actually an output parameter. It associates the generated NavigationPolygon with a specific navigation layer. You can have multiple NavigationRegion2D nodes linked to the same navigation layer. When an agent navigates through a navigation layer, the pathfinding algorithm will build a map that combines all NavigationRegion2D nodes associated with that layer.
Also note that NavigationRegion2D has a Travel Cost field. This parameter is a multiplier of the distance traveled. To calculate the cost of traversing that region, the pathfinding algorithm will treat each unit traveled as equivalent to the multiplier entered in Travel Cost.
Therefore, although Godot’s mesh navigation system doesn’t allow assigning costs to specific tiles, it can assign costs by region and combine multiple regions into a map. Based on that, we’ll create one region covering tiles with the default transit cost (100) and another region for each “slow” tile zone on the map. We’ll set a Travel Cost for these regions according to the tiles they include. All these regions will be associated with the same navigation layer to form a unified map.
The downside is that this requires assigning colliders to walkable tiles—only so they can be detected when generating the NavigationPolygon. In my example, this means creating a physics layer for each navigation type. As with other physics layers, they are created in Project > Project Settings... > General > Layer Names > 2D Physics.
![]() |
| Layers 6, 7, 8, and 9 are used to define navigation costs |
As shown in the screenshot, I created four physics layers (NavGround, NavGrass, NavSand, and NavWater) to associate them with their respective tiles. We don’t want our agents to collide with these tiles. It’s enough to make sure these layers are not marked in the collision masks of our agents.
Once the physics layers are created, they must be associated with the TileMapLayer where we placed the slow tiles. To do this, we’ll add four elements to the Physics Layers field of the TileSet resource for the TileMapLayer. We’ll add one element for each newly created physics layer and ensure that its Collision Layer points to the numbers assigned to the new layers. The Collision Mask of the new elements can be left blank.
With the physics layers added to the TileMapLayer, we can now see them in the TileSet tab to create polygons that define the colliders associated with the tile in that physics layer. For example, considering that the NavWater layer ended up in index 6 of the Physics Layers associated with the TileMapLayer, the configuration of the water tile collider would look like the screenshot.
![]() |
| Configuring the collider for the water tile |
We’ll need to repeat the same operation for the other tiles, making sure to define the colliders in their respective physics layers (in my case, NavGround, NavGrass, NavSand, and NavWater).
Now that we can differentiate each type of tile based on the physics layer of its respective collider, we can define one NavigationRegion2D for each type of walkable tile.
![]() |
| One region for each walkable tile |
In each of these regions, we’ll configure its respective NavigationPolygon so that its Parsed Collision Mask takes into account all physics layers for obstacles and other tiles, except for the tile that gives the region its name. Only then can we regenerate the NavigationPolygon. For example, the configuration for the grass region (GrassNavigationRegion2D) would look like the screenshot.
![]() |
| Configuration for generating the NavigationPolygon of the grass region |
Since the colliders of the physics layers marked in Parsed Collision Mask will trim the resulting NavigationPolygon, the only layer we haven’t marked is precisely NavGrass.
Also, remember to set your agent’s radius to prevent it from brushing against walls while moving. In this example, I set it to 50 pixels, as shown in the previous screenshot.
In my case, since I only have one type of agent, all NavigationRegion2D nodes have the Navigation Layers field set to its default value (1). As I mentioned earlier, this will make all regions combine to form a single map.
Once the new NavigationPolygon for each region is generated, it’s a good time to set the Travel Cost for each one to the desired cost of traversing it. Since in my example distances are measured in pixels, normal floor tiles have a cost of 100 (their width in pixels), while water tiles have a cost of 8,000.
At this point, the sum of all walkable regions looks like the following screenshot.
![]() |
| Sum of the different walkable regions |
You’ll undoubtedly notice an obvious problem: the “slow” regions don’t connect to the walkable region with the default cost. This happens because during the NavigationPolygon formation process, not only are the shapes of the colliders trimmed, but also a distance equivalent to the agent’s radius. This is done to prevent the agent from getting so close to a wall that it brushes against it. We could reduce the agent’s radius to zero, and then the walkable regions would touch, but then agents would unrealistically scrape along walls.
Fortunately, there’s a way to fix this. Through code, we can tell the navigation system to weld the edges of adjacent navigation regions that are within a certain threshold distance. I placed this code in the script attached to the node that contains the different NavigationRegion2D nodes (you can see which one in the hierarchy screenshot shown earlier).
![]() |
| Code to merge the edges of nearby walkable regions |
If we look at how the regions ended up, we’ll see that the maximum distance between two adjacent ones is twice the agent’s radius. Since I set a radius of 50 pixels in the NavigationPolygon configuration, I set 100 as the threshold in this script (line 13).
On line 26, I retrieve the identifier of the active navigation map—the one containing the regions we generated.
And now comes the magic: thanks to the static method MapSetEdgeConnectionMargin() on line 26, it becomes possible to move between walkable regions whose adjacent edges are less than 100 pixels apart.
Since the script is also marked as a [Tool], the Godot editor reflects the change immediately.
![]() |
| Regions welded thanks to our script |
Notice the pink marks the editor uses to indicate the edges of regions that have been welded to another nearby region.
When testing an example similar to this, you’ll see that it works if your agent moves avoiding high-cost zones and only enters them when absolutely necessary or when you explicitly set the target inside one of them.
Another thing you’ll notice is that the agent won’t slow down when crossing high-cost zones. That’s logical because you need to implement that behavior yourself, depending on how you want the zone’s cost to affect your agent. Typically, a higher zone cost translates into reduced speed, but you might prefer it to affect fuel consumption or the agent’s energy instead. In any case, to find out the cost of the NavigationRegion2D the agent is currently traversing, you just need to call NavigationServer2D.MapGetClosestPointOwner() to get the identifier of the region the agent is in. Then, use that identifier with NavigationServer2D.RegionGetTravelCost() to get the transit cost of the region. With that data, it’s up to you to reflect the greater or lesser impact of traveling through a given zone.
Conclusion
Between the previous article and this one, we’ve reviewed the different options for navigating a scene built with a 2D tilemap:
- Use cost data associated with tiles to perform classic pathfinding with our own Dijkstra implementation or Godot’s built-in A*.
- Use navigation integrated with tilemaps.
- Use mesh navigation with slow zones.
If you know you’ll need slow transit zones, you can already rule out option 2. In that case, integrated navigation with tilemaps won’t work for you. From there, if your scenes are moderately sized, I’d start by trying option 2 and only move to option 3 if the performance of the former is insufficient. As you’ve seen, option 3 is the most powerful but also the most cumbersome to configure and maintain.













