So far, all our player movement has been through the use of arrow keys or similar. We’ve yet to create a click-to-move system, or any sort of enemy or NPC (nonplayable character) movement. Before we can do that, we need to build a foundation for pathfinding.
That’s going to be the focus of this chapter. We’re going to look at how Godot handles pathfinding and how we can build maps with obstacles that the player must navigate around.
Getting Set Up
Let’s create a new experiment in our experiment project, called NavigationExperiment. Follow all the same steps you did before to have this new experiment displayed as the default experiment. If you need a refresher, refer to Chapter 2 for how to set up new experiments.
Then, reuse any of the tile sets we’ve already used to create a basic TileMap and TileSet combo:
Creating the TileMap
Increase the Transform ➤ Scale of NavigationExperiment to 3 × 3 and change Project Settings ➤ Window values so that the graphics resize along with the window. 640 × 480 pixels seems like a good window size. Refer to the previous chapter if you get stuck.
This gives us a good starting point for the navigation work we’re about to do.
Adding Basic Movement
Godot has a neat set of tools to help us work out the path we can take between points of interest. The main ones, which we need to add to our experiment, are NavigationRegion2D and NavigationAgent2D. The agent needs to be a child of the player sprite, while the region can be anywhere in the scene tree.
Once added, we will also need to enable Avoidance on the agent so that it can path around any obstacles, and Max Speed to 20 pixels per second:
Regions and agents
I’ve set the player up as a Sprite2D, but you can use a node that has a position property. The code that controls or interacts with these nodes looks like this:
You can think of the NavigationAgent2D as the controller for the movement of the player. It communicates with the NavigationServer to work out the appropriate path between points of interest. It can also send movement or positional information back to the scene where things are moving.
In this case, we can listen for the velocity_computed signal and use the velocity it returns to move the player around the screen. To make sure this signal happens, check the Avoidance Enabled check box in the properties inspector pane.
Signals are the perfect place to use lambda functions, but we can also define named functions and connect them to the same signals. Do what feels best for you.
We need to call the agent's get_next_location method inside the physics process. This gets the next position on the path for the player to travel. It also allows the agent to check if there will be any collisions and adjust the path.
To work out what the new velocity should be, we take the next position along the path and subtract the player’s position. This is the velocity the NavigationAgent2D needs to work out where to move next.
Adding Navigation to Tile Maps
It’s not essential to have a NavigationRegion2D in order for this click-to-move functionality to work. It’s just the simplest way to get started. If you prefer, you can also add this navigation region data to your tile maps.
- 1.
Select the tile map and change Navigation Visibility Mode to Force Show.
- 2.
Click the Tile Set drop-down, and scroll down to Navigation Layers.
- 3.
Click Add Element.
- 4.
Click the Tile Set tab at the bottom of the screen.
- 5.
Click the paint brush icon, and select the Navigation Layer 0 we just created.
- 6.
Click on the tiles that you want to be navigable.
- 7.
Once we remove the NavigationRegion2D, the map should resemble this:
Navigation data in tile sets
We don’t even have to change the code for the NavigationAgent2D to use this new navigation data. We only have to delete the NavigationRegion2D and re-run the game. The player character will now move around the walls instead of through them.
Adding Obstacle Nodes
We could build entire games using only these tile set navigation properties; but our games might need tile sets and obstacle nodes.
The simplest approach is to add more NavigationAgent2D nodes to the map. The player will then attempt to avoid them, though sometimes the pathing is a bit buggy:
Avoidance with other NavigationAgent2D nodes
We can try to replicate this same navigation data using many NavigationRegion2D nodes. It’s going to be a pain in the butt to position them all next to each other and with straight lines:
Navigation data in nodes
Furthermore, the navigation is still buggy. It would be better if we could use collision polygons to carve out non-navigable areas of the navigation rectangle.
Before we jump into the code required to do this, let’s change our obstacles to Area2D nodes with CollisionPolygon2D colliders. We should also add another Area2D node for the walls:
Adding colliders for non-navigable areas
Add each of these to a group. Make it something that you’ll remember, which describes what the purpose of being in this group is:
Adding non-navigable areas to the same group
Now, we can find these nodes and cut them out of the navigation region using code:
When the experiment loads up, we now call cut_out_areas, which loops through each of the grouped nodes and adds their outlines to an array.
We combine these with the outlines in the _region.navpoly so that they are excluded from the starting region polygon.
If you use this approach in your games, don’t forget to call make_polygons_from_outlines, or your navigation region won’t update.
Here’s what this looks like, with Debug ➤ Visible Navigation enabled:
Cutting outlines out of a NavigationPolygon2D
Unfortunately, while the NavigationAgent2D movement is nicer, there are significant drawbacks to this approach, the main one being that the outlines can never touch.
Godot has this nasty habit of ignoring overlapping outlines. You're likely to see the error NavigationPolygon: Convex partition failed! If you leave gaps to avoid this, the player will move straight through the gaps, ignoring the intended route.
Merging Polygons
- 1.
Group overlapping polygons together
- 2.
Combine them into single polygons
- 3.
Add those polygons as outlines to the navpoly
It’s a mission.
Let’ start by creating a method to find intersecting polygons:
This method accepts a reference node, which is any node that has a CollisionPolygon2D in it. It also accepts an array of nodes to compare with. It loops through all the comparable nodes to see if any of them intersect with the subject node.
If it finds intersections, it adds both nodes to the groups array. If there is no group_id, it creates a new group from the subject node and all others from the list of comparable nodes that intersect with it.
Here’s how you can think of it working:
Grouping intersecting polygons
Let’s also add a helper to simplify how we get outlines from collision polygons:
We’ve already seen this code in action, but we’re going to perform the same operation many times; so it’s better to extract it to this helper method.
We can refactor the cut_out_areas method to combine intersecting groups of polygons into single polygons and add all the resulting outlines to the navpoly:
- 1.
We check the comparable node list to create the initial groups of intersecting nodes.
- 2.
We follow this up by checking each intersecting group to make sure we’ve added all the nodes that overlap in the group. If we skipped this step, we could miss intersecting nodes due to the ordering of the initial comparable nodes array.
- 3.
Once we have the final intersecting groups, we combine all their polygons into a single polygon per group.
- 4.
These we add to the navpoly, along with a quick pass through all the non-intersecting polygons.
All this code combines to create a solution to the problem of overlapping outlines in a navpoly mesh. The resulting mesh means our player character will navigate around the obstacles:
Combining polygons before adding them to the navpoly
This allows us to create obstacles dynamically and avoids the need to manually position NavigationRegion2D nodes. A handy trick, to be sure.
Summary
Navigation is one of those things that is easy to learn and difficult to master. Once you step outside of the simplest use case, things can and often do go awry.
Fortunately, we’ve come up with a solid solution to the problem of populating maps with obstacles. This will be super useful for the next game we make.