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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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:
- Use cost data associated with tiles to perform classic pathfinding with our implementation of Dijkstra or with the A* implementation integrated in Godot.
- 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.