DEV Community

admoya
admoya

Posted on

Godot 3.5 2D Navigation Tutorial

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.

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:
the final TileMaps

TLDR: Add two TileMaps to the Main Scene (one for roads and one for sidewalks), and create TileSets using tilemap_packed.png.
If you want to skip this section and move on to Adding Cars and Pedestrians, run this command git 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!

two TileMap nodes in our scene

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: the

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 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.

the cell size in the inspector

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 the Filter property and make sure it is unchecked, then click Reimport, and those ugly gray lines should disappear!

the finished TileMaps

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 one Car and the other Pedestrian. 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 command git 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

a yellow car

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 Node2Ds 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.

example locations of the goals

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:
The Scene Unique Name option

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

the Goal name in the inspector

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

a dude

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.

A hidden CollisionShape2D

GodotTown with some residents

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 with Navigation2DServer.
To skip to the next section and move on to Adding Navigation for Pedestrians, run this command git 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.

the tile setting int he 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:

our selected tiles

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 Snap to Grid Button and adjust the Step in the Inspector under Snap 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:

  1. 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 all TileMap nodes will have navigation regions.
  2. 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.

the map with navigation

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)
Enter fullscreen mode Exit fullscreen mode

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 the move_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 of Navigation2D.get_simple_path. You'll notice two new parameters though, map and navigation_layers. The Navigation2DServer serves as a single object responsible for all navigation, so in order to accomodate for more complex navigation scenarios, Godot 3.5 introduces maps, which contain navigation regions.
    • 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 the Navigation2DServer 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 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 --
get_relative_transform_to_parent errors
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 var nav_layer in the Pedestrian scene is set to match the Navigation Layers property in Sidewalks. 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 command git 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 the

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:
a single sidewalk tile

Click on Navigation and then select New Polygon, by clicking the button the
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: Polygon Tool button
It may be due to badly drawn navigation polygons -- it's best to delete all the navigation polygons on the affected TileSet 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:
sidewalk tiles with navigation

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 the Pedestrian scene. Detach the existing Movable.gd script from Pedestrian and create a new script that uses NavigationAgent2D for navigation and collision avoidance.
To skip this section and see the final result, run this command git 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.

the NavigationAgent2D in the scene tree

the navigation layers in the inspector

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)
Enter fullscreen mode Exit fullscreen mode

Let's talk about our current code.

  • As soon as the NavigationAgent2D enters the scene tree we need to tell the agent where the KinematicBody2D should go by calling set_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 call get_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 the NavigationAgent2D.
  • NavigationAgent2D is a helper node that builds on Navigation2DServer. Right now, we essentially have the same functionality as we did using the Navigation2DServer 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 (the optimize flag) to true.

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)
Enter fullscreen mode Exit fullscreen mode

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, each NavigationAgent2D registers itself as an agent of a certain size with the Navigation2DServer, and it can see all other registered agents.
  • The size that the NavigationAgent2D registers itself with is its radius 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)