Learn the new Navigation system in Godot 3.5 by creating a city map with cars and pedestrians navigating the streets and sidewalks! Here's a quick video of what we'll have by the end of this tutorial.
Introduction
Godot 3.5 is out, and it's awesome. Perhaps the biggest feature for 2D games is the reworked navigation system, which has been ported over from the upcoming 4.0 release. Among the benefits of the new system are a unified navigation server which removed the need for dedicated Navigation2D
nodes and the introduction of the NavigationAgent2D
, which abstracts and simplifies tedious navigation tasks such as pathfinding and collision avoidance.
In this tutorial, we will construct the city of GodotTown
using free assets from the incomparable Kenney. Within our city we will have roads and sidewalks, with cars and pedestrians navigating on them. We'll implement pathfinding with and without using NavigationAgent2D
so that we can compare the pros and cons.
The first few sections will be about designing and building the level, and will mostly deal with TileMaps
. If you already know this stuff and just want to see the new navigation features, skip down to Adding Navigation for Cars.
By end of this guide, we hope you have a better understanding of 2D Navigation in Godot 3.5, and that you're inspired to make some awesome games using it!
Here are the prerequisites:
- Download Godot 3.5
-
Clone/Download this project from Github
- This repo starts as an empty 2D Godot scene, with nothing but the assets we are going to use inside of an
assets
folder - Each branch within the repo corresponds with the start of a section in this tutorial (starting with
step-1/creating-the-tilemap
). We will have instructions below for skipping steps. If you know nothing about Git and this branch thing sounds weird to you, don't worry! Just download the project and follow along, you don't have to touch any of that.
- This repo starts as an empty 2D Godot scene, with nothing but the assets we are going to use inside of an
Let's get started!
Step 1: Creating the Tilemap
In this section, we will be creating the foundation of our game using two TileSets, that we will use in TileMaps for Roads and Sidewalks to build the cityscape of our level. It will look like this:
TLDR: Add two
TileMaps
to the Main Scene (one for roads and one for sidewalks), and createTileSets
usingtilemap_packed.png
.
If you want to skip this section and move on to Adding Cars and Pedestrians, run this commandgit checkout step-2/adding-cars-and-pedestrians
.
A TileMap
is a grid of tiles used to quickly and easily paint a game's layout or background. TileMaps
have a lot of useful features which make them super powerful to use when building the map of your game:
- You can quickly place a large amount of different types of tiles within the viewport
- You can auto-arrange and add a level of randomness to the tiles to make it look more natural (e.g. different planks across a wooden floor) using AutoTiles
- You can add collision, occlusion, and most importantly for this tutorial, navigation to each tile
TileMaps
have TileSets
, which are resources that hold all the properties of the tiles.
Note: This is not a
TileMap
tutorial, so we won't really be going into too much detail about them. If you're not familiar with them, just go through the motions and follow our instructions exactly, and you can focus on Navigation. (Alternatively, scroll back up to the TLDR and follow the instructions to skip to the next section.)
Check out the official Godot Tutorial on Using TileMaps for more info.If you'd like us to create a tutorial on
TileMaps
(or any other topics), please let us know by commenting below!
First, add two TileMap
nodes to the Main Scene, naming one Roads
and the other Sidewalks
. Technically, we could draw the map with one TileMap
, but separate TileMaps
are necessary here for the Navigation we want to achieve. More on this in the coming sections!
Now we'll create the TileSets
for each of the TileMaps
, first for the Roads. Click on the Roads
node in the scene tree, then in the Inspector find the Tile Set
property, which should be empty.
In the dropdown, select New TileSet, then click on the TileSet
and the TileSet Editor should pop up at the bottom (where the console and debugger are located). It's easiest to deal with TileSets
in a larger view, so we'll expand the menu by clicking on this button at the bottom of the right hand hand corner of the editor:
Next, find tilemap_packed.png
in the assets
folder and drag it to the left panel of the TileSet Editor.
Now create a single tile by clicking New Single Tile.
To make sure our selections are accurate, we want grid snapping. To turn it on, click on the Snap To Grid button
Make any selection by clicking anywhere on the tileset. The menu should then open in the right panel. Find the properties under Snap Options and set Step
to x: 16 y:16
. Now we can easily select the upper left-hand corner of the first road tile (towards the bottom left of the spritesheet), which has a crosswalk print on it.
We can continue to create single tiles for the rest of the road; however, instead we're going to create an Atlas, which allows us to create multiple tiles at once by selecting a rectangular region of the spritesheet.
Select New Atlas at the top and drag across all the road tiles in the bottom left of the spritesheet. Once done, expand Selected Tile in the Inspector, and we should see Tile Mode
set to ATLAS_TILE
and Subtile Size
set to x:16 y:16
. Change the Name
of the atlas, so we can see it clearly in the TileMap
menu later on.
Once that's done, we can go back to the 2D Scene Editor and click on the Roads
node again.
Before drawing our map, set the cell_size
to match our step
in the TileSet
. In the Inspector, under Cell set the Size
to x: 16 y: 16
.
Now we can draw our road by clicking on the roads
atlas from the Tilemap menu, and then selecting the tiles we want to paint.
Repeat the process for Sidewalks
. We actually used AutoTiles for our Sidewalks, but another atlas will work as well! After creating the TileSet
, draw the sidewalks as well -- optionally, you can add more TileMaps
for buildings and decorations to liven up your town.
Now we should have a full map like below!
Note: if you notice a faint gray line in between tiles on your finished map, you may need to re-import your spritesheet. By default, Godot uses "filter" mode to import the files, but you can turn it off. Click on
tilemap_packed.png
and go to the Import tab at the top of the left-hand panel. Find theFilter
property and make sure it is unchecked, then click Reimport, and those ugly gray lines should disappear!
Awesome, now we have our map! In the next section, we'll be adding cars and people to the level and scripting them to move around the town.
Step 2: Adding Cars and Pedestrians
Now that we have some roads and sidewalks, we can add cars and pedestrians, each trying to get somewhere.
TLDR: Create two scenes with
KinematicBody2D
as the root node, and name oneCar
and the otherPedestrian
. Assign them a sprite from the assets folder and a collision shape, then make a script that they will both share that moves them towards a goal.
To skip to the next section and move on to Adding Navigation for Cars, run this commandgit checkout step-3/adding-navigation-for-cars
.
Cars and pedestrians will work in basically the same way:
- They will be assigned a goal, which is a
Node2D
- They will move towards that goal at a designated
speed
- They will
queue_free
when they hit the goal
We'll be using a KinematicBody2D
node for both of these, which is a very important node for 2D games! If you're not familiar with it, make sure to check out this tutorial.
For our first draft, cars and pedestrians will ignore any navigation and just move in a straight line towards the goal, disappearing when they get there.
Let's start by creating our Car
scene. Create a new child in our main scene, a KinematicBody2D
node, and then configuring it in-place and saving the branch as a scene. This is an alternative to creating a new empty scene, and it has the advantage of allowing us to visualize our car on the scene it will ultimately be in.
Rename the KinematicBody2D
to Car
. Then create a child Sprite
node, and assign that Sprite
tilemap_packed.png
as its texture. Don't worry, Godot's Sprite
nodes have a handy way to crop the texture, and we're going to use that to get just the portion of the image that has a car.
In the Inspector for the Sprite
node, expand the Region section, enable it, and set the Rect
values to: x: 240 y: 230 w: 32 h: 32
And there we have our car! Move the car to somewhere on the roads, and make sure you are moving the Car
node and not just the sprite!
Now just add a CollisionShape2D
as a child of the Car
node, give it a new RectangleShape2D
from the Shape
property in the Inspector, and set the Extents
to x: 10 y: 10
.
Before we get to the script, let's make goals for the cars and pedestrians to move towards. Create two new Node2D
s at the top level of the main scene, and rename them to CarGoal
and PedestrianGoal
. Drag them to nice spots on the road and sidewalks, respectively.
As a little bonus, we're going to use a new feature introduced in Godot 3.5, Scene Unique Names! Just right-click on our two goal nodes and tick the box that says % Access as Scene Unique Name:
This is going to enable us to refer to the goals in our code by their name, instead of by their relative location in the scene.
Now add a script to the Car
node and call it Movable.gd
. Let's start by exporting some variables that we know we'll need:
export (String) var goal # Our scene unique name
export var current_speed = 50 # The speed we should move at (pixels per second)
export var arrival_tolerance = 10 # How close to a point do we get before we are there
Let's pause and explain arrival_tolerance
. When we eventually implement pathfinding, our cars will get an array of points to follow in sequence. Our script is then going to say:
Try to get to point X. If we are within
arrival_tolerance
of point X, we can consider ourselves there and move on to the next one.
Without this, we would need to perfectly line up our center point at each position along the path, which may not always be possible (for example, if there is a collision preventing us from getting exactly there).
We have our goal
export variable as a string, which will be a scene unique name. We need to find the position of that node when we start up. So let's add the following onready
variable right under the goal
declaration:
onready var goal_pos = get_node(goal).global_position
Next, we'll write a simple movement function using move_and_collide, which we will run at every physics frame:
func move_towards_goal(speed):
# Get a unit-length vector pointing in the direction we want to go
var direction = (goal_pos - global_position).normalized()
# Apply our speed to the direction vector to get our movement vector
var movement = direction * speed
# Move our KinematicBody2D node
move_and_collide(movement)
And finally, call our movement function within _physics_process
. We are using this lifecycle method instead of _process
, because _physics_process
runs at consistent intervals, making movement smooth and independent of framerate.
func _physics_process(delta):
# When we reach our goal (within tolerance), queue_free
if global_position.distance_to(goal_pos) <= arrival_tolerance:
queue_free()
# If we are not at our goal, keep moving!
else:
move_towards_goal(delta * current_speed)
One note about the above script: delta
is the time (in seconds) since the last time _physics_process
was called (i.e., since the last "Physics Frame"). However, since _physics_process
runs at fixed intervals, it should always be the same. Even so, multiplying by that value gives us the desired behavior of speed
being expressed in pixels per second.
And there we have it, our Car
is complete, and our script (sans comments) should be looking like this:
extends KinematicBody2D
export (String) var goal
onready var goal_pos = get_node(goal).global_position
export var current_speed = 50
export var arrival_tolerance = 10
func _physics_process(delta):
if global_position.distance_to(goal_pos) <= arrival_tolerance:
queue_free()
else:
move_towards_goal(delta * current_speed)
func move_towards_goal(speed):
var direction = (goal_pos - global_position).normalized()
var movement = direction * speed
move_and_collide(movement)
Before saving the car as a scene, let's add a value for the Goal
property. Click the Car
and then type in the scene unique name of our goal (%CarGoal
) into the Inspector field, including the percentage sign.
Next, right-click on the Car
node and select Save Branch as Scene. If you run the scene here, the car should move towards the goal!
Adding pedestrians will follow the exact same process, and it should be a snap.
First, add a KinematicBody2D
to the main scene, and rename it to Pedestrian
. Add a Sprite
to it, and give it the same tilemap_packed.png
texture. This time, here are the magic numbers for the Region
property:
x: 384 y: 0 w: 16 h: 16
Give the Pedestrian
a CollisionShape2D
. We can select a RectangleShape2D
again, and give it the following extents: x: 5 y: 8
.
Now we can just grab our Movable.gd
script from the FileSystem section and drag it onto the Pedestrian
. Before we save to a scene, assign a value to the Goal
export value in the Inspector; this time it should be %PedestrianGoal
. Save our dude as a scene by right-clicking and selecting Save Branch as Scene.
Before we run the scene, let's add a little organization so we can add a bunch of cars and pedestrians. Make two new Nodes
in the scene, and name them Cars
and People
. Drag the Car
node into Cars
and the Pedestrian
node into... well, you get it.
Now just click on the Car
and/or Pedestrian
and hit Ctrl + D to duplicate them as many time as you want. You can drag them around to different positions on the map, and modulate their colors (Inspector -> CanvasItem -> Visibility -> Modulate
) to add some diversity to GodotTown! You can also turn the visibility of the CollisionShape2D
off (in the Pedestrian
and/or Car
scene) to see the colors more easily.
Now let's run the scene and see what happens...
We can see that cars are driving all over the sidewalk (and in the void of space!), the pedestrians are jaywalking, everyone is crashing into each other, it's madness! In the next step, we are going to add navigation and use navigation layers to make sure pedestrians only walk on the sidewalks and cars only drive on roads.
Step 3: Adding Navigation for Cars
TLDR: Add navigation polygons to our road tiles, and then change the
Movable.gd
script to make a path withNavigation2DServer
.
To skip to the next section and move on to Adding Navigation for Pedestrians, run this commandgit checkout step-4/adding-navigation-for-pedestrians
.
Now we will add navigation to our Roads
TileMap
! We will define a region on each of the tiles we use that Godot will consider navigable, and when those regions match up along the edge, they will be merged into one larger navigable area. This will allow the NavigationServer2D
to plot a path between any two points on the contiguous region.
First, let's click on our Roads
TileMap
, open the Tileset Editor from the Inspector, and expand it. With a left click on a road tile, or by clicking the left/right arrows on the top of the editor, select the Roads
atlas we defined earlier. You'll know you have the right one when you see the correct Name
property under Selected Tile in the Inspector.
Click on Navigation from the top of the Tileset Editor, and then click New Rectangle from the row underneath, or hit Shift + R. Click into the tile you had selected and you'll see it light up in a light green color; that is a navigation region!
Note that what we just did marked the entire tile as navigable. We could just mark specific parts within the tile as navigable instead by turning off snapping (not recommended) or changing the values of Snap Options -> Step
in the Inspector. This would allow us to do something like excluding the edges of tiles from navigation, which would be useful because the path that is ultimately drawn will be for the center of the car, which means if it runs along the edge, the car will be halfway off the road. We do want to keep our cars off the edge, but we will do so in a simpler way: by only marking the tiles we use for the middle of the road as navigable.
After clicking through the tiles and assigning navigation regions to the middle tiles, we are left with this:
A note and a warning:
-
Note: Our specific spritesheet/tiles makes this process very easy because we are marking entire tiles as navigable, but in future projects we can use either the Rectangle (
Shift + R
) or Polygon (Shift + P
) tool to draw the navigation on only a portion of each tile. (Note: we actually did ) The Rectangle Tool works by clicking-and-dragging a rectangle, and the Polygoon Tool works by clicking on points that will be vertices of the polygon. In both cases, it is helpful to enable grid snapping and adjust theStep
in the Inspector underSnap Options
. - Warning: When using the Polygon Tool for navigation, the first point we click on will not show up as a red dot. It is not until the second point is clicked that both points display. If we double-click the same first point because we thought there was no vertex there, it will create two overlapping points, which will cause errors down the road.
Before we proceed, let's run the game and see our changes. In order to view the navigation region we will need to do two things:
- Click on the
Roads
TileMap
node and in the Inspector select Navigation -> Bake Navigation. This is required for navigation to work, but is off by default because not allTileMap
nodes will have navigation regions. - In the top menu of Godot, select Debug -> Visible Navigation. This will highlight navigation regions in a light transparent green when we run the game.
Now when we run the game, we will see the highlighted navigation regions, which should form a coherent, drivable space for our cars. Of course, we haven't changed their script yet, so the behavior of the cars and people will be unchanged.
Now, let's put that navigation to good use. First, let's assign some Navigation Layers
to the two navigable TileMaps
. These layers will enable us to separate adjacent navigable regions (the roads and the sidewalks), so that cars will only use one, and pedestrians will only use the other. On each TileMap
, expand the Navigation section in the Inspector, and ensure that Navigation Layers
is set to 1
for Roads
, and 2
for Sidewalks
.
Then we can crack open Movable.gd
and add the following export variable:
export (int, LAYERS_2D_NAVIGATION) var nav_layer = 1
This will allow us to keep sharing the same script between cars and pedestrians, while telling them they can only traverse specific Navigation Layers
. The LAYERS_2D_NAVIGATION
constant is a special export flag which will make the variable show up as a bit flag selector in the Inspector.
Just like collisions in Godot, it is important to remember that these values are not "normal" ints, but rather, bit flags. Since we will be working with just 1
and 2
exclusively, this won't affect us much, but if we were to add, say, a cyclist who could traverse both navigation layers, then he would have both layers 1
and 2
enabled, and that would be expressed in code as an integer with a value of 3
.
Now finally we will get to the good part. We will modify our script to use the new Navigation2DServer
to create a path for the cars along the roads, and then follow it. And that can be done with just a few lines of code!
First, add two new variables to the top of Movable.gd
:
var path = null
and var path_idx = 0
that we'll use in our new solution.
And then modify move_towards_goal
:
func move_towards_goal(speed):
# If the path is null or empty, create it and return.
if not path:
# map_get_path returns an array of points, ending at the goal if it is reachable
path = Navigation2DServer.map_get_path(get_world_2d().navigation_map, global_position, goal_pos, false, nav_layer)
# This will point to our next target along the path
path_idx = 0
return
# We are using the same tolerance as before to determine if we have arrived at an intermediate point along the path
while global_position.distance_to(path[path_idx]) <= arrival_tolerance:
path_idx += 1
# Same logic as before, but this time going to the next point in the path instead of the goal
var direction = (path[path_idx] - global_position).normalized()
var movement = direction * speed
move_and_collide(movement)
Let's call some stuff out from that code snippet.
- This function will be called every physics frame, but the navigation will not be baked and ready until the second physics frame. That is why we
return
from themove_towards_goal
function after generating the path, and if the path is an empty array, it will generate again next time (because an empty array is falsy in GDScript). - The key function we are using is
Navigation2DServer.map_get_path
. If you are familiar with the old (Godot <= 3.4) navigation system, this is the replacement ofNavigation2D.get_simple_path
. You'll notice two new parameters though,map
andnavigation_layers
. TheNavigation2DServer
serves as a single object responsible for all navigation, so in order to accomodate for more complex navigation scenarios, Godot 3.5 introducesmaps
, which contain navigationregions
.- We won't be defining additional maps in this guide, but that would be one way to, for instance, have overlapping navigation regions that do not interact. Instead, we get the default navigation map with
get_world_2d().navigation_map
. - We will be using the
navigation_layers
parameter, though, which informs theNavigation2DServer
which layers in the given map should be considered for this path. As you can see above, we will be defining different layers for the roads and sidewalks.
- We won't be defining additional maps in this guide, but that would be one way to, for instance, have overlapping navigation regions that do not interact. Instead, we get the default navigation map with
- We want our cars to move along the path, which means we want them to move to one point at a time along the array returned by
map_get_path
. Instead of modifying this array, we are using a pointer (path_idx
) to choose the next spot to move towards, and we are advancing that pointer when we have "arrived" according to our tolerance.
That's it! Before we test it, open the Car
scene and make sure the Nav Layer
property is set to 1
in the Inspector. Do the same for the Pedestrian
scene, setting it to 2
. Since we have not added the second Navigation Layer
for the Sidewalks
yet, we'd expect the pedestrians to stay still now. Let's run it and find out.
Note: When you run the game at this point, you may see the below error within the debugger --
The navigation should still work as expected with the above instructions, so right now you can ignore this error (though it's annoying). This is a known issue in Godot 3.5 and is being fixed in an upcoming patch.
Looking good! Let's make those people walk now.
Step 4: Adding Navigation for Pedestrians
Now the cars are in motion! But as you can see, when we play the scene the people are still standing in place. Let's go through how to fix that.
TLDR: Add navigation regions to the
Sidewalks
, and ensure that the export varnav_layer
in thePedestrian
scene is set to match theNavigation Layers
property inSidewalks
. In this tutorial, we are using layer 2.
To skip to the next section and move on to Refactoring Pedestrians to use NavigationAgent2D, run this commandgit checkout step-5/refactoring-pedestrians-to-use-navigationagent2d
First, we'll take what we learned in the previous section and add Navigation to the Sidewalk tiles.
Again, open the TileSet
editor by clicking on the Sidewalks
node in the scene tree, then clicking on the TileSet
selection and then expanding the menu from the bottom
Click on the individual Sidewalk tiles, or select the atlas (or Autotile set if you used that) and choose the first tile. This time, we will be adding Navigation Polygons to some of the tiles rather than just plain rectangles. The act of adding polygons is a little wonky, so bear with the process.
The first block of tiles are just rectangles (but we're not including the lighter edges which are the "curbs"), so continue on like we did before for the Roads. Once those are done, select the middle group of tiles shaped like this:
Click on Navigation and then select New Polygon, by clicking the button
or typing Shift + P.
This is where it gets a little tricky. First, you'll select a starting point in the tile for your new polygon. When we click that first point, unfortunately the UI does not show any feedback or indication of where that point is, but make sure not to double click the point -- otherwise, Godot will think that these are two separate points in the Navigation polygon and it may cause pathing to break during runtime. After clicking the starting point, move your mouse to the next corner or point of the shape, and continue on around the tile. Like the starting point, when you've finished the shape, do not double click the final point, as this again could break pathing.
Note: If after you finish this section and run the game, you see the following error:
It may be due to badly drawn navigation polygons -- it's best to delete all the navigation polygons on the affectedTileSet
and carefully re-draw them. This process should be much improved in Godot 4!
Do the same for the rest of the Sidewalk tiles, so that we have the navigation areas looking like this:
Once we are done with the nav regions in this Tileset, go back to the 2D scene editor.
Now, we just need to ensure that the nav layers match up between the Sidewalk
tiles and the Pedestrian
nodes.
In the properties of the Sidewalk
tilemap, go to the Navigation section and ensure that only layer 2
is selected in Navigation Layers
. Then, go to the Pedestrian
scene (not just the node, because we want to change the property for all instances of Pedestrians
) and in the Inspector, make sure that the Nav Layer
property (which corresponds to the export var nav_layer
in our Movable.gd
script) is also set to 2
.
Awesome! Now, we should be able to run the game and see the cars and people moving.
To make things more natural, let's change the speed on some of the pedestrians, and see what happens to the pathing. Pick a couple of the pedestrians in the middle of the map and change their Speed
to something low, like 10
.
Run the game again, and take note of how the people stack up behind the slower pedestrian, getting stuck and having to walk at the same slow speed all the way to the goal -- looks a little weird, right?
Let's take a look at how to make this look more natural in the next section!
Step 5: Refactoring Pedestrians to use NavigationAgent2D
We're in the home stretch! In this last section, we will refactor the code for the Pedestrians
to use NavigationAgent2D
, and really see the strengths of this newly revamped navigation system in Godot 3.5.
TLDR: Add a
NavigationAgent2D
node to thePedestrian
scene. Detach the existingMovable.gd
script fromPedestrian
and create a new script that usesNavigationAgent2D
for navigation and collision avoidance.
To skip this section and see the final result, run this commandgit checkout final-game
Up until now, we have been using the base Navigation2DServer
for the pedestrians walking along the sidewalk. This works pretty well for getting the characters from their starting point to the goal. But as you saw from the last tweak in the previous section where we made some of the people walk slower, collision avoidance isn't happening at all with just the Navigation2DServer
. Ideally, we'd like the people to walk around each other if they are going different speeds, just like they would in real life.
NavigationAgent2D abstracts away many of the pathfinding tasks we did ourselves, and also implements its own collision avoidance system. To be clear, we could accomplish everything that the Navigation Agent does manually in our own code using Navigation2DServer
, but we'd be writing a lot more code, and it could get messy. However, the agent is not without its complications -- here are some pros and cons.
NavigationAgent2D
Pros
- Simplifies previously tedious code for pathfinding
- Easier to use for beginners in Godot Navigation
- Sufficient in most cases for simple navigable regions & obstacles
- Standardized approach to navigation that will increasingly become the norm -- especially upon the release of Godot 4.0
NavigationAgent2D
Cons
- The collision avoidance has limitations, and it takes a lot of tweaking of the agent properties to get it looking smooth
- Can be hard to debug
- It's more of a black box and not as customizable as manually programming the navigation (i.e. we do not have the option to leave the path "unoptimized" as we did before). It's hard to achieve a certain look to your pathing with just the
NavigationAgent2D
, leading developers to sometimes choose a different implementation altogether.
Now that we know what NavigationAgent2D
can do, let's see how it will bring us to our final product.
Go to the Pedestrian
scene and add a NavigationAgent2D
node to the scene tree. Click on this node, and in the Inspector, open the Pathfinding section. Make sure that in Navigation Layers
layer 2
is selected to match the nav layer of the Sidewalks
.
Since we will be writing a new script using the agent for pedestrians, remove the existing Movable.gd
script from the Pedestrian
scene by right clicking the root node, and selecting Detach Script from the menu. Now, let's attach a new script to the Pedestrian
and call it MovableWithAgent.md
.
We still want some of the same properties that Cars
have, so start by copying over the following class variables from the Movable.gd
script -- we'll need:
goal
goal_pos
current_speed
arrival_tolerance
Next, we'll implement the basic movement using the agent with the following code.
extends KinematicBody2D
export (String) var goal
onready var goal_pos = get_node(goal).global_position
export var current_speed = 50
export var arrival_tolerance = 10
func _ready():
# Tell the agent where it should navigate to
$NavigationAgent2D.set_target_location(goal_pos)
func _physics_process(delta):
# Similar to the car, when the pedestrian gets close enough to the goal, despawn it
if global_position.distance_to(goal_pos) <= arrival_tolerance:
queue_free()
# Find the next point in the path to navigate to and update the internal path within the agent
var next_location = $NavigationAgent2D.get_next_location()
# Calculate the direction and movement just as before
var direction = (next_location - global_position).normalized()
var movement = direction * current_speed
move_and_collide(movement * delta)
Let's talk about our current code.
- As soon as the
NavigationAgent2D
enters the scene tree we need to tell the agent where theKinematicBody2D
should go by callingset_target_location()
in the_ready()
function. - As the node moves,
NavigationAgent2D
keeps track of the path and what the next point is. In order to update the path, we must callget_next_location()
in every physics frame -- hence calling it in_physics_process()
-- which both returns the next location in the path, and updates the array of points forming the path.- Note how this is a simpler version of the code in
Movable.gd
that iterates through the entire array of points in the path returned by the Navigation Server. We've now offloaded keeping track of the path to theNavigationAgent2D
.
- Note how this is a simpler version of the code in
-
NavigationAgent2D
is a helper node that builds onNavigation2DServer
. Right now, we essentially have the same functionality as we did using theNavigation2DServer
alone, but optimized. In order to see the similarities, try this: checkout the code at the beginning of this section and modify this line of code --Navigation2DServer.map_get_path(get_world_2d().navigation_map, global_position, goal_pos, true, nav_layer)
-- setting the fourth argument (theoptimize
flag) totrue
.
Great, let's keep going! Set the Goal
property in the 2D Scene Editor for the Pedestrian
as %PedestrianGoal
, just as it was before with the other script. Let's run the script and see what happens.
As you can see, the pedestrians are moving but still piling up behind the slow walkers. This is because we need to do one final step in order to safely avoid collisions along the path.
Let's change a couple of things in our scene and script.
Click on the NavigationAgent2D
node in the Pedestrian
scene tree, and go to the Inspector. Under NavigationAgent2D, find the Avoidance section and turn on Avoidance Enabled
.
Also within the Avoidance section, set the Radius
to 8
, the Neighbor Dist
to 200
, and the Time Horizon
to 75
.
Next, replace the call to move_and_collide()
on the last line of _physics_process()
with the following:
$NavigationAgent2D.set_velocity(movement * delta)
This is where the magic of NavigationAgent2D
starts to shine. Calling set_velocity
internally calls Godot's collision avoidance algorithm that adjusts the velocity of the node as it moves around obstacles. We didn't even have to think about how to make the people smoothly walk around each other -- Godot will handle it for us! Super cool.
Since the call to the collision avoidance algorithm is actually a callback, a Signal called velocity_computed
is emitted once the calculation is done and the node can safely move. This signal also emits the result of the calculation called safe_velocity
. Let's connect this signal to the script.
Go back to the Pedestrian
scene and click on the NavigationAgent2D
node in the scene tree. In the Node menu, under Signals, find the signal called velocity_computed
in the NavigationAgent2D section. Connect it to the Pedestrian
script.
Now, we can simply call move_and_collide()
within the receiver method, using the safe_velocity
calculated by Godot. Our final code should look like this.
extends KinematicBody2D
export (String) var goal
onready var goal_pos = get_node(goal).global_position
export var current_speed = 50
export var arrival_tolerance = 10
func _ready():
# Tell the agent where it should navigate to
$NavigationAgent2D.set_target_location(goal_pos)
func _physics_process(delta):
# Similar to the car, when the pedestrian gets close enough to the goal, free it
if global_position.distance_to(goal_pos) <= arrival_tolerance:
queue_free()
# Find the next point in the path to navigate to and update the internal path within the agent
var next_location = $NavigationAgent2D.get_next_location()
# Calculate the direction and movement just as before
var direction = (next_location - global_position).normalized()
var movement = direction * current_speed
# Sends the movement input to the internal collision avoidance algorithm and sets the agent's velocity
$NavigationAgent2D.set_velocity(movement * delta)
func _on_NavigationAgent2D_velocity_computed(safe_velocity):
move_and_collide(safe_velocity)
Awesome, we're all done! Let's run our game and see the results.
Some final notes about the NavigationAgent2D
:
- Collision avoidance is not actually checking the collision layers of the
KinematicBody2D
. Instead, eachNavigationAgent2D
registers itself as an agent of a certain size with theNavigation2DServer
, and it can see all other registered agents. - The size that the
NavigationAgent2D
registers itself with is itsradius
property (found in the Inspector under the Avoidance section). - The other properties in the Avoidance section relate to how the agent navigates around other agents. Before the tutorial, we tinkered with the values quite a bit before finding the right mix for our pedestrians. Take some time to play with the values on your own and see how it affects the end result!
- If a future project of yours has avoidable nodes that are dynamic but should not themselves move to avoid others, consider adding a
NavigationObstacle2D
as a child. For static obstacles, it is better to update the navigation map itself.
Conclusion
In this tutorial, we learned about Godot's completely new navigation system. They've implemented a system that is flexible and easy to use, especially for those new to game development. Furthermore, they've even given us a built-in way to handle static and dynamic collision avoidance using Navigation Agents, which is extremely powerful.
We hope you've enjoyed this tutorial and gained a good understanding of how to use Godot's new Navigation System and its advantages. We hope you use this knowledge to go forth and create some awesome new games -- please share them below and showcase your work!
Follow this blog and our YouTube channel for even more tutorials. Let us know what other topics you'd like us to cover!
Top comments (0)