AI agent fails when aligned with its target I'm making an AI agent (blue entity) that pursues a target (green entity) while avoiding an obstacle (red entity).

The code works as it should most of the times, but will fail if all the entities are aligned in either the X or Y axis or any direction diagonally. I suspect this is due to dividing by zero.

I fixed the problem by simply checking if the axes were zero and then applying a slight offset.

if (vectorTargetX === 0) vectorTargetX += 0.1;
if (vectorTargetY === 0) vectorTargetY += 0.1;


However, this feels like a cheap fix and it seems like there might be some underlying problems in my code. Are there any better ways to go about this, maybe even more efficient?

You can see the how the AI agent fails by clicking the buttons in the example below.


const ctx = document.getElementById('canvas').getContext('2d');

let targetX = 150;
let targetY = 100;

let obstacleX = 200;
let obstacleY = 100;

let agentX = 300;
let agentY = 101;

function loop() {
  requestAnimationFrame(loop);

  // Calculate difference vectors
  let vectorTargetX = targetX - agentX;
  let vectorTargetY = targetY - agentY;

  let vectorObstacleX = obstacleX - agentX;
  let vectorObstacleY = obstacleY - agentY;

  // Calculate length between vectors
  const lengthTarget = Math.sqrt(vectorTargetX * vectorTargetX + vectorTargetY * vectorTargetY);
  const lengthObstacle = Math.sqrt(vectorObstacleX * vectorObstacleX + vectorObstacleY * vectorObstacleY);

  // Normalize vectors
  vectorTargetX = vectorTargetX / lengthTarget;
  vectorTargetY = vectorTargetY / lengthTarget;

  vectorObstacleX = vectorObstacleX / lengthObstacle;
  vectorObstacleY = vectorObstacleY / lengthObstacle;

  // Check if agent is within collision distance
  if (lengthObstacle &lt; 60) {
    // Append displacement vector
    vectorTargetX -= vectorObstacleX * 0.7;
    vectorTargetY -= vectorObstacleY * 0.7;

    const displacedLength = Math.sqrt(vectorTargetX * vectorTargetX + vectorTargetY * vectorTargetY);

    // Move agent towards target while adding avoidance force
    agentX += vectorTargetX / displacedLength;
    agentY += vectorTargetY / displacedLength;
  } else {
    // Move agent towards target
    agentX += vectorTargetX;
    agentY += vectorTargetY;
  }

  if (lengthTarget &lt; 12) {
    unaligned();
  }

  ctx.clearRect(0, 0, 500, 500);

  ctx.beginPath();
  ctx.fillStyle = '#00ff00';
  ctx.arc(targetX, targetY, 6, 0, Math.PI * 2, false);
  ctx.fill();

  ctx.beginPath();
  ctx.fillStyle = '#ff0000';
  ctx.arc(obstacleX, obstacleY, 6, 0, Math.PI * 2, false);
  ctx.fill();

  ctx.beginPath();
  ctx.fillStyle = '#0000ff';
  ctx.arc(agentX, agentY, 6, 0, Math.PI * 2, false);
  ctx.fill();

  ctx.font = '18px sans-serif';
  ctx.fillStyle = "#000000";
  ctx.fillText("X: " + agentX.toFixed(2), 10, 20);
  ctx.fillText("Y: " + agentY.toFixed(2), 10, 40);
}

function aligned() {
  agentX = 300;
  agentY = 100;
}

function unaligned() {
  agentX = 300;
  agentY = 101;
}

loop();
&lt;canvas id="canvas"&gt;&lt;/canvas&gt;&lt;br&gt;
&lt;button onclick="aligned()"&gt;Spawn agent aligned&lt;/button&gt;
&lt;button onclick="unaligned()"&gt;Spawn agent unaligned&lt;/button&gt;
⚔️ A.I. vs. Physics

To solve this, you need to think a bit about the physics of your simulation, because it creates a problem for your A.I.

The funny thing is, you solved it by just altering the vectors a bit, but you seem to not understand why, so let us change that!







 May the force be with you

To put it simply: when you put two vectors on the same axis perfectly aligned, all that will happen is that the stronger force (i.e. greater vector) wins. Applying that to your simulation: when you align the agents on the same axis, the force of attraction towards the green agent wins over the repulsion from the red agent, forcing it ever closer towards it. I assume we all know what happens when the distances become zero, and you inevitably divide by zero? If you don't know: you can't. Other languages would panic, and Javascript just gives back NaN. Doing arithmetic with NaN will always yield NaN so the program still crashes somewhere down the line.

So that is the problem. But why did your "hack" fix it, and how do you embrace the fix as a proper solution?







 Solution

This is where the A.I. comes in. Think about it as if you were the blue agent: if you were walking straight towards an obstacle you are not allowed to touch, what would you do? I presume you would pick a direction, and turn into that direction to avoid the obstacle. Your agent is not as smart as you, so it does nothing and keeps its direction. When you added a tiny offset to its target vector, you decided it should change direction, albeit in a clunky way.

I propose you add a "preference" to the blue agent, deciding which direction to turn when it is perfectly aligned on some axis. You can use your fix, but I would personally prefer a solution to be more deliberate:

const degreesToRadians = degrees =&gt; degrees * (Math.PI / 180);
const rotate = (x, y, radians) =&gt; [
  x * Math.cos(radians) - y * Math.sin(radians),
  x * Math.sin(radians) + y * Math.cos(radians)
];

const preference = degreesToRadians(15);
const inProximity = lengthObstacle &lt; 60;
const aligned = vectorTargetX === 0 || vectorTargetY === 0;

if (inProximity &amp;&amp; aligned) {
  [agentX, agentY] = rotate(agentX, agentY, preference);
}
```