22 April 2026

Collision avoidance with other agents in Godot

Article cover

In another article I explained how to use Godot’s NavigationAgent2D node to obtain a path to reach any destination across a level. In that case, the resulting path made it possible to avoid bumping into the level’s static obstacles. However, in that article we didn’t look at how to avoid collisions with other agents. Since agents are dynamic, you can’t prepare a navigation NavigationPolygon in advance as we do with static elements. Still, the same NavigationAgent2D node can also manage collision avoidance with other agents, but it does so using mechanisms that are independent from those used to build movement paths through the level.

In the case of static levels, NavigationAgent2D created a graph based on the NavigationPolygon generated from the static environment. It then ran the A* algorithm on that graph to find the route to the destination.

To avoid colliding with other agents, NavigationAgent2D maintains a simulation in which it places the various game agents (those objects that have a NavigationAgent2D node) and uses their positions, velocities, and radii to predict when there is a risk of a collision. It does this using an algorithm called Reciprocal Velocity Obstacle (RVO). For the NavigationAgent2D node to know where we would like to go, we must provide it with the ideal velocity vector (straight toward our next objective). With that information, the node will do its calculations and recommend the velocity vector closest to the ideal one that guarantees we won’t collide with any nearby agents. It’s up to us to use that vector however we see fit to move the agent. Just as with pathfinding, NavigationAgent2D is passive. It provides information, but it does not move the agent—unlike the equivalent component in Unity, which does move the agent that carries it by default.

Because it is passive, the node can be located anywhere in an object’s architecture. As with the rest of Godot’s nodes, we can attach a script that inherits from it and extends its functionality. When configuring its use to prevent collisions with other agents, we need to focus on the node’s Avoidance section.

Configuring collision avoidance with other agents in the NavigationAgent2D node
Configuring collision avoidance with other agents in the NavigationAgent2D node

To begin with, the collision-avoidance simulation is activated by the checkbox in the section title. If it isn’t checked, the node will limit itself to route resolution.

Once checked, the rest of the options shown in the screenshot will appear. The first one, Radius, is used to define the agent’s size. At minimum, you should set it to the radius of your agent’s collider, but it doesn’t hurt to add a bit more to prevent agents from brushing against each other. Keep in mind that this parameter is only used for collision avoidance with other agents and not for pathfinding. In the latter case, the radius used is the one that appears in the Agents section of the NavigationPolygon.

The Neighbor Distance option defines the range within which we detect other agents. Any agent farther away than that will not be considered by the algorithm.

As you might imagine, Max Neighbors lets us set the maximum number of agents that will be considered in the calculations. Ultimately, it’s a way to cap the algorithm’s maximum load on game performance.

The agent’s reaction speed is configured with the Time Horizon Agents parameter. In reality, it is the minimum collision time from which the algorithm starts computing avoidance vectors. If the algorithm calculates that a collision with another agent would occur later than what this parameter sets, it will discard that agent as a source of danger. If we reduce this parameter too much, the agent will wait much longer before starting evasive maneuvers. In fact, if we overdo it, it may start dodging so late that it ends up crashing. Conversely, if we increase this parameter too much, the agent will start constraining its speed too early and will begin dodging excessively far in advance.

As we said, the algorithm only considers in its simulation those moving objects equipped with the NavigationAgent2D node. But it also considers those equipped with the NavigationObstacle2D node, which it treats as immobile objects. It is to this latter case that the Time Horizon Obstacles parameter applies, with the same consequences that Time Horizon Agents had for moving objects.

The Max Speed parameter defines the agent’s maximum speed. The avoidance vector will never have a magnitude greater than this value.

Avoidance Layers works like a physical collision layer. In this case it is used to define which avoidance layer the agent belongs to, while the Avoidance Mask parameter is used to configure the layers of agents that must be avoided.

Finally, the Avoidance Priority parameter is used to configure this agent’s right-of-way preference relative to others. This agent will not try to avoid other agents with lower priority, because those will enforce stronger avoidance maneuvers to let it pass.

With all of the above, the node will be ready to perform its calculations. Let’s see how to interact with it from code to give it the data it needs and retrieve its results.

The first thing to keep in mind is that the node does not compute the avoidance vector immediately. Once we give it the velocity vector we would like to adopt, it needs time to calculate the closest vector to it that avoids a collision with another agent. In the heat of the game that time may feel instant, but in your code logic I can already warn you that the result will not be available in the same frame. That’s why, once the node finishes calculating the avoidance vector, it emits the VelocityComputed signal. We’ll have to subscribe a callback to that signal to capture the computed avoidance vector. Typically, we do that subscription in _Ready().

Subscribing to the VelocityComputed signal
Subscribing to the VelocityComputed signal

The previous screenshot is in Godot C#, but for GDScript it is very similar. The subscription happens on line 192, where the method OnAvoidVectorComputed is associated with the VelocityComputed signal. In my case, that callback is very simple and just stores the result included in the signal in a global variable.

Capturing the computed avoidance vector
Capturing the computed avoidance vector

That vector will be the one we pass to our movement mechanism to advance toward our destination while avoiding collisions with other agents. To keep that vector up to date, we must ensure we provide, each physics frame (_PhysicsProcess()), the ideal approach vector toward our objective.

How to pass the ideal approach velocity to the node
How to pass the ideal approach velocity to the node

According to Godot’s documentation for this node, that’s all we have to do to move our agent through a level populated with other agents. However, the RVO algorithm used by this node has a weak spot: the case in which two agents are heading straight toward each other. In that situation, the avoidance vector is exactly the opposite of the direction vector, so both cancel each other out. Either the agent stops, or the situation degenerates into one agent chasing the other. The solution I’ve found for that problem is to detect it with a dot product and apply, in that case, an avoidance vector that is perpendicular to the direction vector.

Resolving the problem of agents colliding head-on
Resolving the problem of agents colliding head-on

Since NavigationAgent2D returns a vector that combines the ideal direction we want to take and the avoidance vector, if we only want the avoidance component we have to subtract the ideal direction component. That’s what I do on line 333.

To see if that vector is opposite to the direction vector, we compute a dot product on line 334 using the normalized values of the avoidance vector and the agent’s current velocity. If it is very close to 1 (in absolute value), then they are opposite vectors. In that case, and only in that case, I replace the avoidance vector with a perpendicular version of the agent’s current velocity. Maintained for long enough (hence the timer, activated on line 348), an avoidance vector like that should generate, in the worst case, a chase situation from which the front agent will eventually slip out to the side and then continue toward its original objective. It’s not a pretty maneuver, but it works in the few cases in which we end up with two agents moving straight toward each other.

With this, you now have everything you need to prevent your agents from colliding with one another as they move through the level. In any case, remember that all agents must have a NavigationAgent2D node or they will not be included in the collision-avoidance simulation.