avatarMina Pêcheux

Summary

This context provides a tutorial on implementing 2D point-and-click navigation in Godot 4 using C#.

Abstract

The tutorial explains how to use Godot's built-in navigation utilities to move a unit through a field of asteroids. It covers setting up a simple 2D point-and-click navigation system, handling mouse-click events, moving the unit to the target position, and using Godot's navigation tools to avoid obstacles. The tutorial also provides a link to download a version of Godot with .NET support and offers a practical guide to building a 2D platformer game step-by-step.

Bullet points

  • The tutorial covers implementing 2D point-and-click navigation in Godot 4 using C#.
  • It uses Godot's built-in navigation utilities to move a unit through a field of asteroids.
  • The tutorial explains how to handle mouse-click events and move the unit to the target position.
  • It also covers using Godot's navigation tools to avoid obstacles.
  • The tutorial provides a link to download a version of Godot with .NET support.
  • It offers a practical guide to building a 2D platformer game step-by-step.
  • The tutorial includes code examples and screenshots to illustrate the concepts.
  • It also includes a video version of the tutorial.
  • The tutorial is available on the author's Github page.
  • The assets used in the tutorial are from Kenney's library.

Implementing 2D point-and-click navigation (Godot 4/C#)

Let’s see how to use Godot’s built-in nav utilities to move a unit through a field of asteroids!

Moving the player’s avatar or AI units in your game level can sometimes seem difficult, especially when there are a bunch of walls and obstacles everywhere the characters need to avoid.

Luckily, in Godot, navigating in a scene in an intelligent way is really easy — so, today, let’s explore how this works by setting up a simple 2D point-and-click nav system :)

By the end of this video, you’ll know how to get a mouse-clicked position on the screen, and how to have a unit navigate to this spot in a clever way, while avoiding the obstacles on the way:

As usual, since we’ll be coding our logic in C#, make sure that you have a version of Godot with .NET enabled.

Download a version of Godot with .NET support, to be able to program in C#! :)

And of course, don’t forget that you can get the demo scene and all the assets for this example on my Github 🚀 with all my other Godot tutorials :)

Also, the assets are from Kenney’s library🚀

The tutorial is also available as a video — text version is below:

Oh and — by the way…

If you want to learn more about 2D tools for your future Godot 4/C# game projects, go ahead and check out my brand new “short-read” ebook: L’Almanach: Mini-2D Platformer!

This quick, practical guide will teach you the fundamentals of doing 2D games in this game engine with essential notions like tilemaps, 2D physics, animations, character controllers, and more. You’ll even build your very own 2D platformer game step-by-step!

So if you want to discover some 2D tricks for your next Godot 4/C# game in just about 100 pages and for a low-price, don’t hesitate to have a look at the Gumroad page :)

With all that said, let’s dive in and discover how to setup a simple 2D point-and-click navigation system in Godot and C#!

Clicking to pick a target position

Alright — to begin with, let’s see how to have our unit go to a target position when we click in our scene, regardless of the actual obstacles in the level.

To do this, because we’re working in 2D, it’s actually quite simple: we really just need to use Godot’s built-in input event handling hook, the _Input() method, and check for a mouse-click.

Now, suppose we have a “Unit” node that is a CharacterBody2D:

So it’s moveable via code but still works with physics, as we discussed a while ago in this other episode of the series:

We’ll give it a new C# script called Unit.cs:

And inside, we’ll override our _Input() method to define our own event handling logic:

using Godot;

public partial class Unit : CharacterBody2D
{
    public override void _Input(InputEvent @event)
    {
    }
}

More precisely, we’ll start by checking that our input is really a mouse click. To do this, we usually stack three checks together:

  • First, we validate that our input event is of type InputEventMouseButton.
  • Then, we check that the mouse button was just released, and not pressed.
  • And finally, we check that the button that was pressed was the right one.
public override void _Input(InputEvent @event)
{
    if (
        @event is InputEventMouseButton e &&
        !e.Pressed &&
        e.ButtonIndex == MouseButton.Right
    ) {}
}

Note: Of course, you could totally check whether the left-mouse button was just pressed by switching up the last two checks — that’s totally up to you, and to your specific use case! :)

Ok now, if we add a print in this if-block, like this:

public override void _Input(InputEvent @event)
{
    if (
        @event is InputEventMouseButton e &&
        !e.Pressed &&
        e.ButtonIndex == MouseButton.Right
    ) {
        GD.Print("click!");
    }
}

Then, when we run our game, you see that we indeed get a debug in the console every time we click on the background:

All that’s left to do is to get the actual position of the click, which is stored in the e.Position variable, and mark it as the current target position for our unit:

public partial class Unit : CharacterBody2D
{
    private Vector2 _targetPosition;

    public override void _Input(InputEvent @event)
    {
        if (
            @event is InputEventMouseButton e &&
            !e.Pressed &&
            e.ButtonIndex == MouseButton.Right
        ) {
            _targetPosition = e.Position;
        }
    }
}

Also, just to be safe, we should make sure that this target position is initialised to our unit’s start position in the _Ready() function:

public partial class Unit : CharacterBody2D
{
    private Vector2 _targetPosition;

    public override void _Ready()
    {
        _targetPosition = GlobalPosition;
    }

    public override void _Input(InputEvent @event) { ... }
}

Then, we’ll define some arbitrary speed value for our unit:

public partial class Unit : CharacterBody2D
{
    private Vector2 _targetPosition;
    private float _speed = 300f;

    public override void _Ready() { ... }
    public override void _Input(InputEvent @event) { ... }
}

And we’ll code up a very basic move logic using its _PhysicsProcess() method:

public partial class Unit : CharacterBody2D
{
    // ...

    public override void _PhysicsProcess(double delta)
    {
    }
}

Here, we’ll basically re-use the concepts that we discussed in the previous episode of the series on the 2D character controller. So we’ll do the following:

  • First, we’ll compute the difference between the target position and the current position of our unit to get the direction of the movement. We’ll also normalise it to really make it a direction, and not just a difference.
  • Then, we’ll multiply this direction by our speed parameter to get the velocity of our CharacterBody2D.
  • And we’ll end with a call to its built-in MoveAndSlide() function.
public partial class Unit : CharacterBody2D
{
    // ...

    public override void _PhysicsProcess(double delta)
    {
        Vector2 diff = _targetPosition - GlobalPosition;
        Vector2 dir = diff.Normalized();
        Velocity = dir * _speed;
        MoveAndSlide();
    }
}

Though, we should also check if the distance between our current position and our target position is below some arbitrary threshold, to avoid having our unit just move indefinitely around its final target. So, if we’re below this threshold, we’ll just say we’ve arrived and interrupt our computation:

public override void _PhysicsProcess(double delta)
{
    Vector2 diff = _targetPosition - GlobalPosition;
    if (diff.Length() < 5) return;

    Vector2 dir = diff.Normalized();
    Velocity = dir * _speed;
    MoveAndSlide();
}

At this point, if we run our game again, you see that whenever we click in the scene, the unit starts to move towards the point we clicked, and it stops when it’s reached it:

But, of course, for now, this movement technique is a bit dumb and it doesn’t take the actual shape of my level into account. The unit is basically ignoring the asteroids, and so it gets stuck on them!

Let’s see how to fix this, thanks to Godot’s built-in navigation tools…

Setting up our navigation region

In order to have our unit move about the level in an intelligent way, meaning having it avoid the obstacles and plan its route according to the asteroids on the way to the target point, we’re going to use Godot’s built-in navigation system.

This is a quick and easy way to integrate an A*-based path computation feature in our game, and it first requires us to tell the engine what areas units can or can’t walk into, so that it can auto-deduce the best series of positions to go through to reach a given point.

More precisely, to get all this working, we first need to add a new NavigationRegion2D node in our scene:

This node will hold a navigation mesh that determines where the navigation agents can walk; and so wherever there are obstacles in our level, those will create holes in this mesh to prevent our units from going to those spots.

So, once we’ve added a NavigationRegion2D node to our scene, we need to go to its Inspector and give it a new NavigationPolygon resource to define the overall region for our navigation computation:

Typically, in our case, we can just make a big rectangle that encompasses the whole level:

Then, we’ll move our asteroids inside this new NavigationRegion2D node, as children:

This way, because:

  1. our walls are static bodies with colliders
  2. and our nav mesh Parsed Geometry Type is set to detect both meshes and static colliders by default:

Godot will automatically see those asteroids as navigation obstacles, and “punch holes” in the mesh around them to prevent units from walking there.

To actually compute the nav mesh and check that it indeed takes those obstacles into account, we just need to use the Bake NavigationPolygon button at the top of the Viewport panel (that is available whenever our NavigationRegion2D node is selected):

You see that this instantly overlays a blueish layer on our scene, that shows us the navigation mesh our nav agents will be able to use:

It’s limited to the rectangle region we defined earlier, and it has some holes wherever we have an asteroid, as expected.

If you want to change the margins around the reference static colliders, typically to avoid the units trying to plan a course too close to the asteroids, you might want to change some settings in the nav mesh.

In particular, in the Agents section, you may need to check and tweak the Radius value to get something more natural:

(Don’t forget to re-bake afterwards to actually take these changes into account!)

But of course, now, to really test this out, we need to turn our unit into an actual navigation agent so that it uses this brand new nav mesh for its movement :)

Making our unit a navigation agent

On the hierarchy side, turning our basic unit into a 2D nav agent is really simple: we just need to give our object a new NavigationAgent2D child node:

Now, you might obviously want to play around with the parameters of this agent in the Inspector… but, other than that, this is the only thing we have to do to make our unit a nav agent.

’Cause the real magic happens in the movement script of our unit, where we’re going to replace our basic movement logic with one that uses this new nav agent node ;)

So, first of all, let’s make sure we get a reference to this agent child node in our _Ready() function:

public partial class Unit : CharacterBody2D
{
    private NavigationAgent2D _agent;
    private float _speed = 300f;

    public override void _Ready()
    {
        _agent = GetNode<NavigationAgent2D>("NavigationAgent2D");
    }

    public override void _Input(InputEvent @event) { ... }
    public override void _PhysicsProcess(double delta)
}

With that in place, we can now update our input event handler so that, instead of setting some local target point, it sets the target position of the nav agent node itself.

Basically, just by assigning this value to the TargetPosition field of our 2D nav agent, we’re having it compute and store the best path to this point from our current position, that takes into account the nav mesh of our scene and avoids all the obstacles on the way:

public override void _Input(InputEvent @event)
{
    if (
        @event is InputEventMouseButton e &&
        !e.Pressed &&
        e.ButtonIndex == MouseButton.Right
    ) {
        _agent.TargetPosition = e.Position;
    }
}

Though, a little improvement can be to force the position to actually be on the nav mesh by getting the nearest point to our position on this mesh — this way if we decide to click on an asteroid to mark it as the target location, our unit will still prepare a valid path:

public override void _Input(InputEvent @event)
{
    if (
        @event is InputEventMouseButton e &&
        !e.Pressed &&
        e.ButtonIndex == MouseButton.Right
    ) {
        var map = GetWorld2D().NavigationMap;
        var p = NavigationServer2D.MapGetClosestPoint(map, e.Position);
        _agent.TargetPosition = p;
    }
}

Then, to truly walk this path, we need to update our logic in the _PhysicsProcess() method.

Rather than just aiming for the target point, we’re going to use our agent’s GetNextPathPosition() method to retrieve the next point in the path it computed. This is the intermediary point that we should be aiming at for now, and so we can use this as our target point for this frame.

Meaning that we just need to replace our previous click-target with this new intermediate path point position in our direction computation, and then the end of the function can remain identical :)

public override void _PhysicsProcess(double delta)
{
    Vector2 diff = _agent.GetNextPathPosition() - GlobalPosition;
    if (diff.Length() < 5) return;

    Vector2 dir = diff.Normalized();
    Velocity = dir * _speed;
    MoveAndSlide();
}

Also, to replace our previous threshold proximity check, we can instead see whether our agent has reached its destination or not, and if so then we’ll exit early:

public override void _PhysicsProcess(double delta)
{
    if (_agent.IsNavigationFinished())
        return;

    Vector2 diff = _agent.GetNextPathPosition() - GlobalPosition;
    Vector2 dir = diff.Normalized();
    Velocity = dir * _speed;
    MoveAndSlide();
}

Now, if we run our game with those new updates, you’ll notice two things: first, the unit cleverly moves around the level according to our nav mesh, and it doesn’t just run into obstacles anymore. Second… it doesn’t rotate properly as it moves, so it’s gliding in a pretty weird way!

To solve this, we just need to use the CharacterBody2D’s built-in LookAt() method, and give it the position of the next point in the navigation path as input:

public override void _PhysicsProcess(double delta)
{
    if (_agent.IsNavigationFinished())
        return;

    LookAt(_agent.GetNextPathPosition());
    Vector2 diff = _agent.GetNextPathPosition() - GlobalPosition;
    Vector2 dir = diff.Normalized();
    Velocity = dir * _speed;
    MoveAndSlide();
}

Of course, depending on how your unit hierarchy is setup exactly, and in particular how your sprite is anchored to the body, you may also need to rotate this sprite in the editor to properly match the rotation…

But other than that, here we go: our unit now moves and rotates properly. Meaning that, whenever we click in our scene, it goes to the clicked location while avoiding all the asteroids, thanks to our nav mesh.

Conclusion

So here you go: you know how to setup a basic 2D point-and-click navigation system in Godot thanks to the engine’s built-in navigation utilities!

If you enjoyed the tutorial, feel free to leave some claps and follow me to not miss the next ones — and of course, don’t hesitate to drop a comment with ideas of Godot tricks that you’d like to learn!

As always, thanks a lot for reading, and take care :)

To read more of my content, and articles from many other great writers from Medium, consider becoming a member & following me!

Godot
Csharp
Programming
Tutorial
Game Development
Recommended from ReadMedium