26 January 2026

Cost Assignment to 2D Navigation Using Tilemaps in Unity

Article Cover

 Recently, we saw how it could be done in Godot to assign costs to the tiles of a tilemap so that those costs could be used in navigation algorithms. Unity has its own particular way of doing this, but, as in the case of Godot, it will depend on whether we want to use those costs with a custom navigation algorithm or with the engine’s navmesh navigation. Let’s see how it’s done in both cases.

In this article, I will assume you have read my previous article on how to assign data to tiles in Unity. Here, we will take advantage of some of the things I explained there.

Costs for Custom Navigation Algorithms

When using custom algorithms, such as our own implementations of Dijkstra or A*, we will want to access the transit cost of a given cell. We could choose to assign a game object to the tile, as we saw in the mentioned article, and have that game object include a script with the cost data. But that would be excessively complex, since we would need to assign a collider to that game object to detect it with a volume sensor when applying the sensor to the cell’s position. No, it is definitely much better in this case to use the other approach we saw in that article: creating a class that inherits from Tile, adding a cost field, and using it to draw cells that have a higher transit cost. It would be a class like the following:

Implementation of Our Custom Tile
Implementation of Our Custom Tile

The class simply has a field to input the tile’s cost through the inspector (line 13).

To be able to query the tile’s cost from our code, line 18 includes a read-only property.

Essentially, a Tile is a kind of Scriptable Object, so it is extended in a very similar way. As with Scriptable Objects, we can decorate them with the [CreateAssetMenu] attribute (line 6) so that the editor shows a menu entry to create instances of this tile. In this case, I created it so that it appears under the path “Scriptable Objects > CoutyardTile.”

Creating a tile of this type would be similar to creating a Scriptable Object. We would right-click in the folder where we want to save the tile and choose the option configured in the previous paragraph.

Creating an Instance of the Custom Tile
Creating an Instance of the Custom Tile

Once the tile instance is created, we can give it the appearance we want and assign a value to the cost field. My example is visually very simple. I gave a water tile a cost of 80.

Water Tile Configuration
Water Tile Configuration

In my example, I created four types of walkable tiles:

  • Ground: Gray. It has no transit difficulties, so its cost is 1.
  • Grass: Green. Cost is 2.
  • Sand: Orange. Cost is 4.
  • Water: Blue. Cost is 80.

Also, remember to assign a sprite. Otherwise, no matter what color value you assign, the tile will not be visible. I simply used a white sprite so that the color applies over it.

Once the custom tiles are created, you just need to switch the Tile Palette tab to edit mode and drag the tiles onto it from their folder.

My Tile Palette
My Tile Palette

With that, we can now draw the “slow” areas of the scene. Mine is bordered by impassable walls and has internal black walls.

My Example Scene
My Example Scene

Once the costs are associated with their tiles, how would we retrieve them? We just need a method like the one in the following screenshot.

Method to Retrieve the Cost Associated with Each Tile
Method to Retrieve the Cost Associated with Each Tile

The method assumes there is a global variable walkableTilemap with a reference to the tilemap where all the walkable tiles of the scene are drawn. With that reference, we use its WorldToCell() method (line 319) to convert the global position of the scene we want to analyze into the tilemap coordinate corresponding to that position. Then, we use the GetTile() method (line 320) to retrieve the tile associated with those coordinates. Once we have the tile, we can access its Cost property to return its value (line 321).

Mesh Navigation with Slow Zones

The previous method is useful if we want our agents to move through the tilemap using our own navigation algorithms. However, for performance or simplicity, we might prefer to use the mesh navigation system that comes with the engine. The problem in Unity is that its mesh navigation system is designed for 3D and does not work as-is for 2D scenes like the example. Fortunately, there is a free module called NavMeshPlus that solves this problem for 2D and is used almost the same way as Unity’s native 3D mesh navigation. I explained its installation and use in a previous article where I covered 2D navigation in Unity. Based on what was said in that article, here we will explain how to implement slow zones that are considered by the pathfinding algorithm.

So, assuming you have read that article and followed its instructions to generate a NavMesh, now you need to create a type of walkable area for each type of tile we have. To do this, go to Window > AI > Navigation. The window that opens has two tabs: Agents and Areas. As you can imagine, the one we are interested in is Areas. There, the default areas of Unity’s navigation system are already defined: Walkable, Not Walkable, and Jump. Notice that the right column defines the transit cost of each area. This is where we will define the area types we saw in the previous section, with their corresponding costs.

Configuration of Walkable Areas and Their Costs
Configuration of Walkable Areas and Their Costs

Once the areas are defined, we need to associate each tile with the area type it belongs to. If you followed the 2D navigation article in Unity, you will have included a NavigationModifier component in the same GameObject as the tilemap containing the walkable tiles. Uncheck the Override Area field.

Then, add a NavigationModifierTilemap component. It has a list called Tile Modifiers. Add one element to that list for each type of walkable tile you want to define. Then drag each tile resource from its folder onto its respective element. This way, you can define which area each tile belongs to.

In my case, the configuration looks like this:

Assigning Tiles to Each Area
Assigning Tiles to Each Area

Now we need to regenerate the NavMesh so that it includes the transit costs we just defined for each area.

However, before clicking Bake in the NavigationSurface component, you must ensure you change the Render Meshes parameter. If it is configured as we left it in the 2D navigation article, this parameter will have the value Physics Colliders, so it will only consider objects with physical colliders to “cut” the walkable area of the NavMesh. We need to change it to Render Meshes. This way, it will use the tiles we defined as walkable (which are visual, renderable components) as a reference to shape the NavMesh. Once changed, we can click Bake.

My NavMesh Configuration
My NavMesh Configuration

If you make the editor show the NavMesh, you will see that it has several colors to indicate the different areas it detected.

NavMesh Generated with Different Areas
NavMesh Generated with Different Areas

Now you have your NavMesh, but if you run the scene at this point, your agent will probably stay still. To make it move, you must ensure it is configured to use the walkable areas. This is done by setting the AreaMask parameter of the NavMeshAgent so that it includes all the zones where walking is allowed.

NavMeshAgent Configuration
NavMeshAgent Configuration

In the screenshot above, you can see that I configured the AreaMask to include the areas Ground, Grass, Water, and Sand.

Now, if you run the game, your agent should be able to move through the walkable zones, always choosing the path with the lowest cost.

You will probably notice that, although the agent chooses the lowest-cost route, it will not slow down if it is forced to cross a slow zone. To make that happen, you will need to call the static method NavMesh.SamplePosition() from the agent’s Update() to analyze its current position at all times. This method returns an output parameter of type AI.NavMeshHit, which has a mask property with the area associated with that point. Normally, a point is associated with only one area, but even so, the mask property is (as its name suggests) a 32-bit mask—one bit for each navigation area Unity allows to be defined. You just need to find the index of the bit set to 1 to know the navigation area of the analyzed point. With that index, you can call another static method, NavMesh.GetAreaCost(), to obtain the cost of the area we are on and act accordingly (usually by dividing the agent’s speed by the area’s cost).

Conclusion

With this, we have reviewed the two options we have in Unity to navigate a scene built with a tilemap:

  1. Use cost data associated with tiles to perform classic pathfinding with our implementation of Dijkstra or with the A* implementation integrated in Godot.
  2. Use mesh navigation with slow zones.

If your scenes are moderately sized, you can afford option 2 if, for some reason, you prefer to model the scene with a graph.

However, as soon as your scene grows in size, you will prefer option 3. It offers the best performance and is the easiest to configure.

22 January 2026

Cost Assignment for 2D Tilemap Navigation in Godot

In my previous article, I explained how to assign custom data to the tiles of a tilemap, which we can use to build a 2D scene. One of these data points could be the transit cost of a tile, to be used by a Dijkstra algorithm we implement, or by the AStar2D component already included in Godot, to create paths through the scene.

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
Creating navigation layers in Project Settings

Assignment of navigation layers to a TileSet resource in the configuration of a TileMapLayer
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.
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.
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:

  1. It performs very poorly, so it’s only viable for small scenes.
  2. 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
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
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
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
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
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
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
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
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
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:

  1. Use cost data associated with tiles to perform classic pathfinding with our own Dijkstra implementation or Godot’s built-in A*.
  2. Use navigation integrated with tilemaps.
  3. 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.