30 April 2025

2D Navigation in Unity

Someone reading a map in front of a maze
A couple of months ago, I published an article on how to use the 2D navigation system in Godot. It's time to write a similar article for Unity. I'm not going to complicate things, so I'm going to replicate the structure of that article, but adapting the instructions to Unity.

Remember that the starting premise is that we want to have NPC characters that are able to navigate the scene to go from one point to another. Let's start by seeing how to create a scene that can be processed by the navigation system.

Creating a navigable scene 

As a scene, any set of sprites could work, as long as they have associated colliders to define which ones pose an obstacle to the NPC. Since that wouldn't be very interesting, we're going to opt for a somewhat more complex case, but also quite common in a 2D game. Let's assume that our scene is built using tilemaps. The use of tilemaps deserves its own article and you will find plenty of them on the Internet, so here I will assume that you already know how to use them.

In our example, we will assume a scene built with three levels of tilemaps: one for the perimeter walls of the scene (Tilemap Wall), another for the internal obstacles of the scene (Tilemap Obstacles), and another for the ground tilemap (Tilemap Ground).

Hierarchy of tilemaps in the example
Hierarchy of tilemaps in the example

Courtyard is the parent node of the tilemaps and the one that contains the Grid component. With the tilemap grid. Tilemap Ground only contains the visual components Tilemap and TilemapRenderer to display the ground tiles. The Wall and Obstacles tilemaps also have these components, but they also incorporate two additional components: a Tilemap Collider 2D and a Composite Collider 2D. The Tilemap Collider 2D component is used to account for the collider of each tile. This collider is defined in the sprite editor for each of the sprites used as tiles. The problem with Tilemap Collider 2D is that it counts the collider of each tile individually, which is very inefficient due to the number of tiles that any tilemap-based scene accumulates. For this reason, it is very common to accompany the Tilemap Collider 2D component with another component called Composite Collider 2D. This latter component is responsible for merging all the colliders of the tiles, generating a single combined collider that is much lighter to manipulate by the engine.

When using these components in your tilemaps, I advise you to do two things:

  • Set the "Composite Operation" attribute of the Tilemap Collider 2D to the value Merge. This will tell the Composite Collider 2D component that the operation it needs to perform is to merge all the individual colliders into one. 
  • In the Composite Collider 2D, I would set the "Geometry Type" attribute to the value Polygons. If you leave it at the default value, Outlines, the generated collider will be hollow, meaning it will only have edges, so some collider detection operations could fail, as I explained in a previous article

Creating a 2D NavMesh 

A NavMesh is a component that analyzes the scene and generates a graph based on it. This graph is used by the pathfinding algorithm to guide the NPC. Creating a NavMesh in Unity 3D is very simple.

The problem is that in 2D, the components that work in 3D do not work. There are no warnings or error messages. You simply follow the instructions in the documentation and the NavMesh is not generated if you are in 2D. In the end, after much searching on the Internet, I have come to the conclusion that Unity's native implementation for 2D NavMeshes is broken. It only seems to work for 3D.

From what I've seen, everyone ends up using an open-source package, external to Unity, called NavMeshPlus. This package implements a series of components very similar to Unity's native ones, but they do work when generating a 2D NavMesh.

The previous link takes you to the GitHub page of the package, where it explains how to install it. There are several ways to do it, the easiest perhaps is to add the URL of the repository using the "Install package from git URL..." option of the "+" icon in Unity's package manager. Once you do this and the package index is refreshed, you will be able to install NavMeshPlus, as well as its subsequent updates. 

Option to add a git repository to Unity's package manager.
Option to add a git repository to Unity's package manager.

Once you have installed NavMeshPlus, you need to follow these steps: 

  1. Create an empty GameObject in the scene. It should not depend on any other GameObject. 
  2. Add a Navigation Surface component to the previous GameObject. Make sure to use the NavMeshPlus component and not Unity's native one. My advice is to pay attention to the identifying icons of the components shown in the screenshots and make sure that the components you use have the same icons. 
  3. You also need to add the Navigation CollectSources2d component. In that same component, you need to press the "Rotate Surface to XY" button; that's why it's important that these components are installed in an empty GameObject that doesn't depend on any other. If you did it correctly, it will seem like nothing happens. In my case, I made a mistake and added the components to the Courtyard GameObject mentioned earlier, and when I pressed the button, the entire scene rotated. So be very careful. 
  4. Then you need to add a Navigation Modifier component to each of the elements in the scene. In my case, I added it to each of the GameObjects of the tilemaps seen in the screenshot with the hierarchy of tilemaps in the example. These components will help us discriminate which tilemaps define areas that can be traversed and which tilemaps define obstacles. 
  5. Finally, in the Navigation Surface component, you can press the "Bake" button to generate the NavMesh. 

Let's examine each of the previous steps in more detail.

The GameObject where I placed the two previous components hangs directly from the root of the hierarchy. I didn't give it much thought and called it NavMesh2D. In the following screenshot, you can see the components it includes and their configuration.

Configuration of the main components of NavMeshPlus
Configuration of the main components of NavMeshPlus

As you can see in the previous figure, the main utility of the NavigationSurface component is to define which layers we will take into account to build our NavMesh ("Include Layers"). I suppose that if you have very loaded layers, you might be interested in limiting the "Include Layers" parameter only to the layers where there are scene elements. In my case, the scene was so simple that even including all the layers, I didn't notice any slowdown when creating the NavMesh. Another customization I made is to set the "Use Geometry" parameter to "Physics Colliders". This value presents better performance when using tilemaps since simpler geometric shapes are used to represent the scene. The "Render Meshes" option allows you to create a much more detailed NavMesh, but less optimized, especially when using tilemaps.

If you're wondering how to model the physical dimensions of the navigation agent (its radius and height, for example), although they are shown at the top of the "Navigation Surface" component, they are not configured there but in the Navigation tab, which is also visible in the previous screenshot. If you don't see it in your editor, you can open it in Window --> AI --> Navigation.

Navigation tab Navigation tab
Navigation tab Navigation tab

Finally, the Navigation Modifier components allow us to distinguish tilemaps that contain obstacles from tilemaps that contain walkable areas. To do this, we need to check the "Override Area" box and then define the type of area this tilemap contains. For example, the GameObjects of the Wall and Obstacles tilemaps have the Navigation Modifier component from the following screenshot:

Navigation Modifier applied to tilemaps with obstacles
Navigation Modifier applied to tilemaps with obstacles

By marking the area as "Not Walkable," we are saying that what this tilemap paints are obstacles. If it were a walkable area, like the Ground tilemap, we would set it to Walkable.

Once all the Navigation Modifiers are configured, we can create our NavMesh by pressing the "Bake" button on the Navigation Surface component. To see it, you need to click on the compass icon in the lower toolbar (it's the second from the right in the toolbar) of the scene tab. This will open a pop-up panel on the right where you can check the "Show NavMesh" box. If the NavMesh has been generated correctly, it will appear in the scene tab, overlaying the scene. All areas marked in blue will be walkable by our NPC.

NavMesh visualization NavMesh visualization
NavMesh visualization NavMesh visualization


Using the 2D NavMesh 

Once the 2D NavMesh is created, our NPCs should be able to read it.

In the case of Godot, this meant including a MeshNavigationAgent2D node in the NPCs. From there, you would tell that node where you wanted to go, and it would calculate the route and return the location of the different waypoints of the route. The rest of the agent's nodes would be responsible for moving it to that location.

Unity also has a NavMeshAgent component, but the problem is that it is not passive like Godot's; that is, it doesn't just give the different waypoints of the route but also moves the agent to those waypoints. This can be very convenient in many cases when the movement is simple, with a single component you meet two needs: you guide the movement and execute it. However, thinking about it carefully, it is not a good architecture because it does not respect the principle of Separation of Responsibilities, which states that each component should focus on performing a single task. My project strongly configures the movement; it is not homogeneous but changes along a route based on multiple factors. It is a level of customization that exceeds what Unity's NavMeshAgent allows. If Unity had respected the principle of Separation of Responsibilities, as Godot has done in this case, it would have separated route generation and agent movement into two separate components. This way, the route generator component could have been used as is, while the agent movement component could have been wrapped in other components to customize it appropriately.

Fortunately, there is a little-publicized way to query the 2D NavMesh to get routes without needing a NavMeshAgent, which allows replicating Godot's functionality. I will focus this article on that side because it is what I have done in my project. If you are interested in how to use the NavMeshAgent, I recommend consulting Unity's documentation, which explains in great detail how to use it.

Querying the NavMesh to get a route between two points
Querying the NavMesh to get a route between two points

In the previous screenshot, I have provided an example of how to perform these queries.

The key is in the call to the NavMesh.CalculatePath() method on line 99. This method takes 4 parameters: 

  • Starting point: Generally, it is the NPC's current position, so I passed it directly as transform.position.
  • Destination point: In this case, I passed a global variable of the NPC where the location of its target is stored. 
  • A NavMesh area filter: In complex cases, you can have your NavMesh divided into areas. This bitmask allows you to define which areas you want to restrict the query to. In a simple case like this, it is normal to pass NavMesh.AllAreas to consider all areas. 
  • An output variable of type AI.NavMeshPath: this is the variable where the resulting route to the destination point will be deposited. I passed a private global variable of the NPC. 

The call to CalculatePath() is synchronous, meaning the game will pause for a moment until CalculatePath() calculates the route. For small routes and occasional updates, the interruption will not affect the game's performance; but if you spend a lot of time calculating many long routes, you will find that performance starts to suffer. In those cases, it is best to divide the journeys into several shorter segments that are lighter to calculate. In the case of formations, instead of having each member of the formation calculate their route, it is more efficient for only the "commander" to calculate the route and the rest to follow while maintaining the formation.

The output variable of type AI.NavMeshPath, where CalculatePath() dumps the calculated route, could still be passed to a NavMeshAgent through its SetPath() method. However, I preferred to do without the NavMeshAgent, so I processed the output variable in the UpdatePathToTarget() method on line 107 to make it easier to use. An AI.NavMeshPath variable has the "corners" field where it stores an array with the locations of the different waypoints of the route. These locations are three-dimensional (Vector3), while in my project I work with two-dimensional points (Vector2), which is why in the UpdatePathToTarget() method I go through all the points in the "corners" field (line 111) and convert them to elements of a Vector2 array (line 113). This array is then used to direct my movement components to each of the waypoints of the route.

Conclusion 

Done, with this you have everything you need to make your NPCs move intelligently through the scene, navigating to reach the target. At a high level, it is true that the concepts are very similar between Godot and Unity, but the devil is in the details. When you get down to the implementation level, you will find the nuances and differences that we have analyzed in this article, but with the instructions I have given you, the result you obtain in Unity and Godot should be similar.