In this tutorial, we will take a look at Navigation2D, a node that can be used to find the shortest path between two points. It is mainly used to move characters with automatic obstacles avoidance.

Starting point

First, go to this Github repository and download the project’s Zip file, so you will have a starting point to follow the tutorial.

Extract the contents of the Zip file. Then, open Godot and press Scan to import the project.

Press scan to import a project

Browse to the path where you extracted the Zip file, select the Navigation2D-Starting-Point folder and press Select this Folder to add the project to the Godot Project Manager.

Now, you can open the project.

Let’s look at the node tree. We simply have a root node (Root), which contains two Sprite nodes, one for the background and one for the player’s character. Both Root and Background nodes already have a script attached that currently do nothing.

Creating Navigation2D node and navigation polygons

Add a Navigation2D node to Root. A Navigation2D node provides navigation and pathfinding within an area defined as a set of NavigationPolygon resources. By default, these polygons resources are automatically obtained from child NavigationPolygonInstance nodes.

So, to define the area in which the character will be able to move, we need to add a NavigationPolygonInstance node as a Navigation2D child. Once the node is created, you will see a warning sign next to its name, which tells us that we need to create a NavigationPolygon resource.

With NavigationPolygonInstance selected, go to the Inspector and, next to the Navpoly property, select new NavigationPolygon.

Add a NavigationPolygon resource

Now you can draw the polygon that represents the area accessible to the character. When NavigationPolygonInstance is selected, in the workspace toolbar we have 3 new buttons to create and edit the navigation polygons:

Select the Create points tool and click on the editor to add points to the navigation polygon. Draw the entire perimeter, going around the obstacles, until you close it by clicking on the first point created.

Since the character has a size, be sure to leave some margin around obstacles, so that the player’s sprite does not overlap them.

Once the perimeter is finished, there will only be one obstacle left to do, that is the hole in the upper right corner of the map. To add it, simply draw a path around it to create a hole in the polygon.

The final result will be something like this:

The last node we need to add is a Line2D one. We will use it to draw the path calculated by Navigation2D. In the Inspector, set its Width property to 1 and choose a color that stands out on the green background (I chose red).

Pathfinding script

Let’s start by working on the Root node script. This script will be the one that will take care of requesting the path to the Navigation2D node.

Let’s click on the script icon next to Root to open the script editor. For now, the script contains only this code:

extends Node2D

func _unhandled_input(event):
	pass

We want the player to move whenever we click the left mouse button on the map, like in Point and Click games. To handle mouse input, we will use the _unhandled_input() function, which is already declared in the script but is currently empty (the keyword pass is used to declare that the function is empty). This function is the recommend one to manage user input, because Godot calls it only after handling the user interface input events.

Replace the function code with the following:

func _unhandled_input(event):
	if event is InputEventMouseButton:
		if event.button_index == BUTTON_LEFT and event.pressed:
			var path = $Navigation2D.get_simple_path($Player.position, event.position)
			$Line2D.points = path
			$Player.path = path

The first two lines of the function check that the detected input event is a mouse event, and that this event is the left click.

If the two conditions are true, we ask the Navigation2D node for the path using the get_simple_path() method. This method has two mandatory parameters:

  • the starting point, which in our case is the current position of the player;
  • the destination point, that for us is the point of the map clicked with the mouse.

The get_simple_path() function returns a PoolVector2Array which contains the list of the points forming the shortest path from the starting point to the destination point.

After getting the path, the function does two things. The first is to set the points property of Line2D, so that the path is drawn on the screen. The second is to assign the path to Player‘s path variable. This variable is declared in the player script. We will use it for moving the character sprite.

If you run the project now, you will see that when you click a point on the map, the path to reach that point is drawn on the screen.

If you try to click on a point where the player cannot move (such as one of the gorges), the path will stop at the accessible point nearest to where you clicked.

Player movement script

Let’s move on to the player’s movement script. If you open it, you’ll see that it already includes this code:

extends Sprite

var speed = 50
var path : = PoolVector2Array()

func _process(delta):
	pass

The script has a speed variable to store player movement speed, and there is the path property we used before in the Root.gd script. This variable is initially set to an empty PoolVector2Array object, to indicate that the character does not have a path to follow.

We’re going to use the _process() method, that run on every frame, to move the character along the path. So, replace its code with the following:

func _process(delta):
	# Calculate the movement distance for this frame
	var distance_to_walk = speed * delta
	
	# Move the player along the path until he has run out of movement or the path ends.
	while distance_to_walk > 0 and path.size() > 0:
		var distance_to_next_point = position.distance_to(path[0])
		if distance_to_walk <= distance_to_next_point:
			# The player does not have enough movement left to get to the next point.
			position += position.direction_to(path[0]) * distance_to_walk
		else:
			# The player get to the next point
			position = path[0]
			path.remove(0)
		# Update the distance to walk
		distance_to_walk -= distance_to_next_point

How does this code work?

As we mentioned earlier, a path is a list of points. The character will move along the segments that connect these points.

A path is an array of points

At each frame, the character can move a certain distance, calculated as the product of its speed and the time elapsed from the previous frame (var distance_to_walk = speed * delta).

Once the distance to the next point of the route has been calculated (var distance_to_next_point = position.distance_to(path[0])), two things can happen:

  • the distance to be covered is less than the distance from the next point of the path: in this case, the player moves towards the next point covering the entire distance of movement (position += position.direction_to(path[0]) * distance_to_walk).
  • the distance to be covered is greater than the distance from the next point: in this case, the player is placed on the next point (position = path[0]), which will then be removed from the path (path.remove(0)); then, the distance to walk is reduced by the movement already performed (distance_to_walk -= distance_to_next_point) and the procedure is repeated until the movement is completed (while distance_to_walk > 0 and path.size() > 0:).

Final result

Run the game and try to click anywhere: the character will move to the place you click on the map (or to the nearest point if you click outside the accessible area.

Click below if you want to try the code:

Did you enjoy this article? Then consider buying me a coffee! Your support will help me cover site expenses and pay writers to create new blog content.


10 Comments

Avatar

Lawrie · February 1, 2020 at 10:35 am

Thank you for this! I was struggling to find details on non-Tilemap-based pathfinding, and this was SUPER straightforward. Only thing to note: in your `_process` function for the Player sprite, I think you missed a couple of characters off the end: `distance_to_walk -= distance_to_next_poi` should be `distance_to_walk -= distance_to_next_point`.

Going to read through some of your other posts now – this place is a goldmine!

    Avatar

    Davide Pesce · February 2, 2020 at 10:20 pm

    Thanks for the feedback Lawrie! You’re right, two characters were missing!

Avatar

Phigiro · August 11, 2020 at 4:14 pm

Hey!
i tried to get the player animated.
For that i copied the animates_player(direction) in func _process(delta) and tried to get the direction.
That doesn´t seem to work! 🙁

A little bit confusing is, that the animates_player(direction) has an animation output like down_idle etc. when i debug the function.

where is my mistake?

    Avatar

    Davide Pesce · August 11, 2020 at 9:18 pm

    Hi Phigiro! In this tutorial the movement code is very different from the one in the RPG tutorials, you can’t just copy the animates_player() function. You need to edit Player’s script like this:

    extends AnimatedSprite
    
    var speed = 50
    var path : = PoolVector2Array()
    var direction = Vector2.DOWN
    
    func _process(delta):
    	if path.size() == 0:
    		var animation = get_animation_direction(direction) + "_idle"
    		play(animation)
    		return
    	
    	# Calculate the movement distance for this frame
    	var distance_to_walk = speed * delta
    	
    	# Move the player along the path until he has run out of movement or the path ends.
    	while distance_to_walk > 0 and path.size() > 0:
    		var distance_to_next_point = position.distance_to(path[0])
    		if distance_to_walk <= distance_to_next_point:
    			# The player does not have enough movement left to get to the next point.
    			direction = position.direction_to(path[0])
    			position += direction * distance_to_walk
    			var animation = get_animation_direction(direction) + "_walk"
    			play(animation)
    		else:
    			# The player get to the next point
    			position = path[0]
    			path.remove(0)
    		# Update the distance to walk
    		distance_to_walk -= distance_to_next_point
    
    func get_animation_direction(direction: Vector2):
    	var norm_direction = direction.normalized()
    	if norm_direction.y >= 0.707:
    		return "down"
    	elif norm_direction.y <= -0.707:
    		return "up"
    	elif norm_direction.x <= -0.707:
    		return "left"
    	elif norm_direction.x >= 0.707:
    		return "right"

    First, we need to declare the direction variable to store the direction the character is moving.

    At the beginning of the _process() function, we have to check if path is empty. If so, the character is still, and we can play the idle animation. If it is moving instead, we play the walk animation. In both cases we use the get_animation_direction() function to find out the animation direction (we can copy that function from the other project as it is).

    Of course all of the above implies that you have already turned the Player node into an AnimatedSprite, imported the player sprites from the other project and copied all the animations!

Avatar

Phigiro · August 13, 2020 at 7:55 pm

Oh Wow! 😀
Many thanks to you! 🙂

Avatar

Phigiro · August 13, 2020 at 9:20 pm

ok i , there was one little Bug:
play(animation) has to be $Sprite.play(animation) 😀

than the player faces in the right direction along the path, but he isn´t animated.
(i did the whole tutorial an the player is an animatedsprite 😀 )

i hope you know what i mean, my english is so bad, sorry!

    Avatar

    Davide Pesce · August 13, 2020 at 10:22 pm

    ok i , there was one little Bug:
    play(animation) has to be $Sprite.play(animation) 😀

    Since Player is the AnimatedSprite node, and the script is attached to it, the statement $Sprite.play(animation) is wrong (it would call the play() method of a child node named Sprite, which does not exist). It’s correct to write play(animation), which calls the play() method of the node to which the script is attached.

    than the player faces in the right direction along the path, but he isn´t animated.
    (i did the whole tutorial an the player is an animatedsprite 😀 )

    This point is not clear to me… in this tutorial we don’t use an AnimatedSprite, so if you followed it, you shouldn’t have one. Have you changed the node type from Sprite to AnimatedSprite?

      Avatar

      Phigiro · August 14, 2020 at 5:33 pm

      Hi,
      my Player is an KinematicBody2d and it has an animatedSprite Node what is called Sprite.

      I solved the Problem.
      I had to put the pathfinding logic in the _physics_process(delta) with an if condition, witch separated it from the if not attack_playing condition.
      like that:

      if path.size() == 0:
      # Animate player based on direction
      if not attack_playing:
      #print(direction);
      animates_player(direction)
      else:
      # Calculate the movement distance for this frame
      var distance_to_walk = speed * delta

      # Move the player along the path until he has run out of movement or the path ends.
      while distance_to_walk > 0 and path.size() > 0:
      var distance_to_next_point = position.distance_to(path[0])
      if distance_to_walk <= distance_to_next_point:
      # The player does not have enough movement left to get to the next point.
      path_direction = position.direction_to(path[0])
      position += path_direction * distance_to_walk
      var animation = get_animation_direction(path_direction) + "_walk"
      #print(animation)
      $Sprite.play(animation)
      else:
      # The player get to the next point
      position = path[0]
      path.remove(0)
      # Update the distance to walk
      distance_to_walk -= distance_to_next_point

      Now it works.
      The player is animated on the path und with the Key-Input.

      Thanks again for your help. 😀

Avatar

Jeudy · September 17, 2020 at 7:04 am

Thanks for this tutorial! It was super helpful.

How would you handle “dynamic” obstacles? Example: if you can build structures in the world that would change the NavigationPolygon (I think of it as if the area of the building would be substracted to the navitation polygon, like creating a “hole”) in it.

Is something like this possible?

    Avatar

    Davide Pesce · September 18, 2020 at 12:03 pm

    Hi Jeudy!

    Yes, it is possible to change the NavigationPolygon at runtime. The easiest way is to use the add_outline() function, to which you must pass the vertices of the polygon to add, followed by make_polygons_from_outlines().
    Whenever you modify the NavigationPolygon, you must remember to set the enabled property of NavigationPolygonInstance first to false and then to true, in this way you will trigger the recalculation of the navigation data.

    However, you must be careful when adding a new polygon. If it overlaps an existing one, the navigation may stop working (in your case it shouldn’t be a problem, when you build a structure, presumably you already know that there is no other polygon in that position)

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close