Godot Version: 4.1.1 .NET
Difficulty: Intermediate
GitHub Repository: Completed tutorial hereRelated Project: Statue (Concept)
Previous Godot Tutorial: Weeping Angel Effect in Godot Engine
Introduction
Godot has some pretty good built-in tools for pathfinding and navigation, but when getting set up I found that the Godot docs don’t have a complete end-to-end example of getting everything setup. There’s lots of separate tutorials that cover different parts of the process, and if you can follow them all you can get it working, but I wanted to put together a single tutorial that covers everything from start to finish.
Also pretty much every Godot tutorial out there that isn’t the official Godot docs appears to be a YouTube video, which was frustrating to me as someone who prefers to read tutorials instead of watching them.
This tutorial will take you through the process of adding a NavMesh to a scene. Once the NavMesh is created, we’ll add agents to the scene and have them pathfind around the NavMesh towards the player. We’ll also add some obstacles to the scene and see how the agents react to them.
This tutorial will be using C# for the scripts, but GDScript should work just as well. This tutorial will also assume that you’re familiar enough with Godot to know how to create a scene, how nodes work, and adding scripts to those nodes. If you’re not familiar with Godot, I recommend checking out the Godot Docs and following their step-by-step tutorial and “Your first 3D game” tutorial.
This tutorial will not be an exhaustive guide to Godot’s navigation tools, just a simple tutorial to help you get started. As always, if you want to know more, check out the Godot Docs.
Creating the NavMesh
Creating the Scene
Before we can have agents that pathfind around an area, we need some space for them to pathfind in.
Godot has some good whiteboxing tools to make a rough scene with. All we need for this tutorial is a room with some walls and a floor. It’s best if the room you make is divided with walls into sections so that the agents can’t always move directly towards their objective.
When making your scene, place all your scenery objects underneath a single Node3D. This will make it easier to add the NavMesh later.
For this tutorial, I’ll be using the scene I made in my previous tutorial, Weeping Angel Effect in Godot Engine. You don’t need to follow the tutorial, I’m only using the scene.
If you want to use my scene, you can find that in the playground.tscn
file located in this Github Repository.
Don’t worry about the spheres in the image just yet, we’ll get to that later. Those are going to become the agents that navigate around the scene.
Once your scene is completed drop in a player-controlled object so that you can move and look around the scene. I’ll repeat what I wrote in the last tutorial:
For a player-controlled object with built-in camera, I’ve found FPSControllerMono to be an okay starting point, but as of writing this the plugin has not yet been updated for Godot 4.0 and fails to compile cleanly, so you may need to make some fixes.
The FPSControllerMono isn’t very difficult to make yourself by hand if you’re experienced with Godot, but it helps to have a starting point. It’s just a CharacterBody3D with a Camera and a CollisionShape, with a script to handle input and mouse looking.
Creating the NavigationRegion
Once you have a scene ready, it’s time to create the NavMesh. Start by adding a NavigationRegion3D node to the scene, and move the parent node of the scenery objects underneath it.
Then, with the NavigationRegion3D selected, in the right-hand sidebar under the Inspector
tab, create a new NavigationMesh.
For the most part, the default settings are okay. The only thing you really need to make sure is correct is the setting under Agents/Radius
. This is what tells Godot how large the agents navigating your scene are.
Important: If you find that your agents are getting stuck on walls, or if the navmesh just isn’t showing up in certain parts of the scene, adjust the agent radius setting and/or the
Cell Size
setting underCells/Size
. The smaller the cells, the more accurate the NavMesh will be, but the more expensive it will be to calculate.If you update the cell size, make sure to also update the default navigation map cell size in your project settings, under
Project Settings
undernavigation/3d/default_cell_size
.
Another important setting to know about is under Geometry/Parsed Geometry Type
. This tells Godot what kind of objects to look for in the scene when sampling the scene to make the NavMesh. You have a choice of “Mesh Instances”, “Static Colliders” or “Both”. The default option is “Mesh Instances” which is fine for this tutorial. However, for complicated scenes you may instead want to use the “Static Colliders” option, as that allows you to specify the exact collision layers you want your NavMesh to use. The documentation also suggests that using “Static Colliders” is more performant when baking the NavMesh, so it’s something to keep in mind if you think you may want to rebake the NavMesh during runtime.
Why would you want to rebake a NavMesh at runtime? You need to if you have a dynamic scene that you want the agents to path around dynamically. For example if you have a door that opens and closes, you may want to rebake the NavMesh after it moves so that the agents can pathfind through the door when it opens, or route around it when it’s closed.
Implementing NavMesh rebaking during runtime is outside the scope of this tutorial, but you can read more about it in the Godot Docs.
Once you’ve set the radius, click the Bake NavMesh
button at the top of your viewport. This will create the NavMesh, and you should see it appear in your scene as a bunch of cyan triangles floating above the ground.
We’ve got our NavMesh, now let’s add some agents to navigate it.
Adding Agents
Setting up the target
Before we can pathfind anywhere we need to have a destination in mind. For this tutorial, we’re going to be making the agents navigate towards the player all the time. To do this, the agents need to know which object is the one controlled by the player. And to do that, we’ll be using groups.
Note: Instead of using groups, you could just hardcode a path to the player node in the agent’s script, but this is likely a bad idea. Things will break if you ever change the name of your player object, or fail to keep things consistent from scene to scene.
Using groups allows the agents to not need to care about the layout of a scene in order to function.
In the right hand sidebar, click Node
, then Groups
, then finally Manage Groups
.
In the Manage Groups
menu, enter in a name for the group (“player” works fine) and click “Add”. Then find the root node of the player in the middle column, then “> Add” to add the player node to the group.
Click “OK” to close the menu. You can verify that it worked if you see a new symbol on your player node in the scene tree.
Preparing the Agent
Now we need to make a scene for the objects that will be moving around. Create a new scene with a CharacterBody3D
as the root node. Give it a CollisionShape3D
a MeshInstance3D
, a NavigationAgent3D
, and add a script to the root node.
Clear out any unnecessary code from the default script. Yours should look something like this:
using Godot;
using System;
public partial class agent : CharacterBody3D
{
public const float Speed = 5.0f;
// Get the gravity from the project settings to be synced with RigidBody nodes.
public float gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();
public override void _Ready()
{
}
public override void _PhysicsProcess(double delta)
{
Vector3 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
velocity.Y -= gravity * (float)delta;
Velocity = velocity;
MoveAndSlide();
}
}
We’ll need to retrieve a reference to the player node, as well as our local NavigationAgent3D
. Create some members to hold the references, then retrieve them in the _Ready
method.
public override void _Ready()
{
// Get the navigation agent
navigationAgent = GetNode<NavigationAgent3D>("NavigationAgent3D");
// Get the player node
var player_group = GetTree().GetNodesInGroup("player");
if (player_group.Count > 0)
{
player = player_group[0] as Node3D;
}
else
{
GD.PrintErr("No player found!");
}
}
Tip: If you were to simply write
player = GetTree().GetNodesInGroup("player")[0];
, things would work just fine most of the time. However, use caution when doing this. If there is no player node in the scene, or if you accidentally typo the group name, Godot won’t crash. A NullReferenceException will be quietly logged to the editor, but the game will continue to run.It can be confusing to figure out why your agents aren’t navigating if you don’t have an error message to tell you what’s wrong.
Now let’s get started with pathfinding.
Code for pathfinding to the player
NavigationAgent3D
needs to be told a destination before it can start performing pathfinding. For this tutorial, that’ll be the player’s position. And we’ll need to set it more than once, since the player is going to be moving around the scene. But how often should we update the path?
The simplest way to do this is by just setting navigationAgent.TargetPosition
to the player’s transform every frame. That’ll work, but it’s not very efficient. For small scenes with small numbers of agents it’s no problem, but it could have performance issues in a larger scene with many pathfinding agents.
Instead let’s add a member to keep track of the last time we updated the target and only update it every so often. We’ll also add a member to keep track of how often we want to update the target.
// How often to update the target (in seconds)
private const double TargetUpdateInterval = 0.5f;
// How long since we last updated the target
private double lastTargetUpdate = TargetUpdateInterval; // Start at the interval so we update immediately.
Then, in the _PhysicsProcess
method, we’ll update the target if it’s been long enough since the last update.
public override void _PhysicsProcess(double delta)
{
// Check if we should update the target
lastTargetUpdate += delta;
if (lastTargetUpdate >= TargetUpdateInterval)
{
lastTargetUpdate = 0.0;
navigationAgent.TargetPosition = player.GlobalTransform.Origin;
}
Note: You may notice sometimes with this approach that the agents will stop short of the player. This is because they’re hitting the position the player was at when we updated the target, but they’ve moved since then.
If you were using this logic for a real game, you might consider dynamically adjusting the target update interval based on the distance to the player. This is left as an excerise for the reader 😊.
Now that we’ve set a destination, NavigationAgent3D
provides a helper method for us to get the path to it: GetNextPathPosition()
. This returns the next point in the path to the target in world space. We’ll use this to move towards the target.
Note: It’s okay to call
GetNextPathPosition
on every physics frame, in fact theNavigationAgent3D
expects/requires it to be called that often.
Vector3 nextPathPosition = navigationAgent.GetNextPathPosition();
Vector3 velocity = Velocity;
// Add the gravity.
if (!IsOnFloor())
velocity.Y -= gravity * (float)delta;
// Move towards the next point in the path.
Vector2 direction = new Vector2(nextPathPosition.X, nextPathPosition.Z) - new Vector2(GlobalTransform.Origin.X, GlobalTransform.Origin.Z);
direction = direction.Normalized();
velocity.X = direction.X * Speed;
velocity.Z = direction.Y * Speed;
Velocity = velocity;
MoveAndSlide();
And if you just want pathfinding, you’re done! You can now add as many agents as you want to your scene and they’ll all pathfind towards the player.
Let’s see it in action:
But notice how the agents kind of clump together when they all have the same target? That’s not very interesting to look at. How can we make them spread out?
Avoidance
You can think of avoidance as a kind of ’layer’ above the normal pathfinding. Adding avoidance to an agent doesn’t change its path, only how it moves along that path. We’ll use this to make the agents avoid each other while they’re moving towards the player. It’ll also allow us to make areas where the agents won’t go, which can be used for your own custom gameplay mechanics.
Avoidance works by giving the NavigationAgent3D
a little more control over the agent’s velocity. Instead of setting Velocity
directly, we’ll tell the agent the direction we want it to move, then it’ll tell us what our velocity should be so that we don’t collide with obstacles.
Avoiding other agents
Go back to your agent scene and select the NavigationAgent3D
node. In the right-hand sidebar, back under the Inspector
tab, unfold the Avoidance
section and check Avoidance Enabled
.
The Radius
setting here is separate from the one we set on the NavigationMesh
earlier. This radius doesn’t affect pathfinding in any way whatsoever. This is only used to figure out at what distance other agents should move away from us. If you increase this, the agents will spread out more. Keep in mind that they’ll also generally traverse a scene slower too, as they have to move around each other in addition to the scene.
Another important option on this menu is Max Speed
. This is the maximum speed that the agent will move at; when you pass in a velocity to the agent, it will be capped to this speed. Even if you pass in a higher velocity by increasing Speed
in your agent’s script, the agent will never move faster than this speed.
In effect, you now have two places you need to manage this agent’s speed. This can be confusing, so I recommend setting Max Speed
to be the same as Speed
in your agent’s script, or finding some other way to keep them in sync.
The other settings in this menu are outside the scope of this tutorial, but feel free to play around with them and see what they do. There are tooltip descriptions on the labels which are pretty helpful for figuring out what they control.
Important: Make sure that
Avoidance Layers
has at least one layer selected, and also make sureAvoidance Mask
has that same layer selected. This is what tells the agents to avoid each other.
Now let’s update our code to have NavigationAgent3D
provide our velocity.
Remove the following in _PhysicsProcess
:
Velocity = velocity;
MoveAndSlide();
And replace it with this:
navigationAgent.Velocity = velocity;
That’ll start the async process of calculating the agent’s proper velocity to avoid obstacles. Once it’s done, it’ll emit an event that we can listen to.
Add the following to the _Ready
method:
navigationAgent.VelocityComputed += OnVelocityComputed;
And implement the OnVelocityComputed
method:
public void OnVelocityComputed(Vector3 safeVelocity)
{
Velocity = safeVelocity;
MoveAndSlide();
}
And that’s it! Now the agents will avoid each other while they’re moving towards the player. For this video I’ve increased the agent’s radius to be larger than the mesh just to make the avoidance more obvious.
Avoiding obstacles
Avoiding obstacles is very similar to avoiding other agents. To add an obstacle, we need to add a NavigationObstacle3D
node to the scene. The NavigationAgent3D
will automatically avoid any NavigationObstacle3D
nodes in the scene (so long as the layers and masks are set correctly).
Important: It can be a little bit confusing to understand, but if you add an obstacle that completely blocks the path to the target, the agent will not re-route their path around the obstacle.
Let’s say for example you place an obstacle in the center of a hallway that doesn’t leave a large enough gap for the agent to go around. As I mentioned, avoidance does not affect pathfinding, so the agent will just keep trying to force their way past the obstacle.
If you want to dynamically block off an area of the navmesh, you’ll need to rebake the navmesh at runtime, as mentioned above.
To your scene add a NavigationObstacle3D
node, and move it wherever you like.
Note: Currently, as of Godot version 4.1.1, you can’t actually use the transform gizmo to move the obstacle around the XZ plane. If you try, it’ll start creating points for a static obstacle instead.
As a workaround, you can manually modify the node’s transform in the inspector.
Then, look at the settings in the right-hand sidebar.
The settings for NavigationObstacle3D
are a little bit convoluted. There are actually two different kinds of obstacles that are both configured here. Dynamic obstacles and static obstacles.
An obstacle is considered dynamic if it uses the Radius
property. If it uses the Vertices
property, it’s considered static. And it’s also possible to use both, but make sure you read the Godot Docs to understand how that works.
For this tutorial, to keep things easy we’re just going to set a radius, even though we won’t be moving the obstacle around dynamically. I will be using the same avoidance layer as the agents (layer 1 by default). However, if you would like to use a different avoidance layer for obstacles, make sure to update the avoidance mask on the agents to include that layer.
Note: If it looks like the agents aren’t avoiding the obstacle, try increasing the
Height
setting, or moving the obstacle up so that the center of the obstacle is roughly the same height as the origin/center of your agents.
Now if you run the scene, you should see the agents avoid the obstacle as well as each other.
Debugging
If you’re having trouble getting your agents to navigate the way you’d like them to, Godot provides a way for you to see the navmesh, obstacles, and collision shapes while running the game. In the topbar menu, under “Debug” there are options for visualizations:
There’s also an option for visible paths, but I haven’t gotten that to work yet.
Enable these options, and now you can see them while the game is running:
Conclusion
And there you have it, a complete end-to-end example for setting up navigation in Godot. I hope this tutorial was helpful to you! Navigation is a complex topic, and this is only a simple example. If you want to know more, as always, check out the Godot Docs. They go far more in-depth than I have here, and less like to be out of date than this tutorial 😊.