For the 10th installment of Unity for Software Engineers, we’ll be doing things a bit differently. Instead of a single long-form article going in-depth on a single topic, I will be releasing a collection of short-form overviews over the course of this week. The goal: introduce readers to a breadth of the Unity toolkit.
Pathfinding with NavMesh
Pathfinding is a common system in video games; you might have enemies that need to move throughout the scene to reach and attack a player, or a point-and-click game where you command the main character by simply clicking at a destination, or a real-time strategy game where all units navigate around obstacles and each other dynamically.
Pathfinding is a well-studied and well-understood problem in computer science. You need a sense of the navigable area, the location of any obstacles, and a tried-and-tested pathfinding algorithm, like A*, repeatedly running as a scene changes.
Unity, for its part, provides a decent, performant implementation in the NavMesh Navigation System.
The NavMesh system consists of:
-
A NavMesh (Navigation Mesh): A static pre-computed definition of all walkable areas in the scene.
A NavMesh is baked ahead of time.
-
Dynamic NavMesh obstacles.
This allows you to constrain the pre-computed static NavMesh with obstacles that appear, disappear, or change during run-time.
-
Dynamic Off-mesh links
This allows you to add additional paths within or between a NavMesh. For example, modeling a jump between two levels within a NavMesh or a connection between two disjoint meshes (for example, if you have multiple scenes loaded simultaneously in an open-world setting, you can only navigate between each scene’s mesh via off-mesh links).
-
A collection of NavMesh Agents that move about the mesh.
A
NavMeshAgent
is a component that can be added to any object. In C#, it can be found in theUnityEngine.AI
namespace. By default, aNavMeshAgent
will control the position and rotation of its object viaNavMeshAgent.SetDestination(Vector3)
.A
NavMeshAgent
can also be used to provide pathfinding capabilities through methods likeNavMeshAgent.CalculatePath
and others, allowing the developers to use that path in other ways.
Using a NavMesh Agent
Assuming you have already baked your NavMesh, you are ready to start using your first NavMesh Agent. Remember to re-bake your scene when you add new obstacles (walls, etc.) or your layout changes.
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(Animator), typeof(NavMeshAgent))]
public class NavMeshMover : MonoBehaviour
{
[Header("Animation")]
private Animator _animator;
private int _speedParam;
[Header("Navigation")]
private NavMeshAgent _agent;
private void Awake()
{
_animator = GetComponent<Animator>();
_speedParam = Animator.StringToHash("Speed");
_agent = GetComponent<NavMeshAgent>();
}
public void NavigateTo(Vector3 destination)
{
float distance = Vector3.Distance(transform.position, destination);
if (distance > _agent.stoppingDistance)
{
_agent.SetDestination(destination);
}
else
{
transform.LookAt(destination, Vector3.up);
}
}
private void Update()
{
_animator.SetFloat(_speedParam, _agent.velocity.magnitude);
}
}
An external component can then control how to move from one place to the other. For a player, it could be based on mouse clicks on various destinations. For an enemy, it could be based on developer-written AI logic based on its field-of-view, distance to a player, etc.
You can also decide to de-couple your “Mover” interface from this
MonoBehaviour
. For example, you might also want a TeleportMover
or
FancyCustomAdvancedAlgorithmMover
. You can do that in a few ways:
-
Extract an interface
IMoverComponent
:public interface IMoverComponent { void NavigateTo(Vector3 destination); }
and implement it:
public class NavMeshMover : MonoBehaviour, IMoverComponent { // ... }
Using
GetComponent<IMoverComponent>()
will give you the interface you want.The drawback is that interfaces don’t work with serialized fields. That means that if you’re referencing the mover component explicitly from the inspector, you won’t be able to do that. In that case:
-
An abstract
MonoBehaviour
could work.public abstract class MoverComponent : MonoBehaviour { public abstract void NavigateTo(Vector3 destination); }
Your
NavMeshMover
can then only implementMoverComponent
. You can now explicitly set that using the inspector, or use[RequireComponent(typeof(MoverComponent))]
where needed. The disadvantage here is that a component can not use multiple inheritance from multiple abstract classes.