1
\$\begingroup\$

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 < 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 < 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();
<canvas id="canvas"></canvas><br>
<button onclick="aligned()">Spawn agent aligned</button>
<button onclick="unaligned()">Spawn agent unaligned</button>

\$\endgroup\$
3
  • \$\begingroup\$ If the main issue is that you're trying to fix a situation in which the code doesn't work as expected, wouldn't the right site for this be Stack Overflow? \$\endgroup\$ Commented Apr 27, 2020 at 1:20
  • \$\begingroup\$ @CertainPerformance it works when applying the fix I included. But the fix doesn't feel proper and I felt like Code Review is better as it's more of a "is there a better way" sort of question. \$\endgroup\$
    – superlaks
    Commented Apr 27, 2020 at 1:26
  • \$\begingroup\$ With an aligned target, targetY and agentY and obstacleY are the same, making both vectorTargetY and vectorObstacleY zero. You use these values in multiplications, multiplying anything by zero becomes zero. It all goes downhill from there. \$\endgroup\$
    – konijn
    Commented Apr 27, 2020 at 9:54

1 Answer 1

2
\$\begingroup\$

⚔️ 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 => degrees * (Math.PI / 180);
const rotate = (x, y, radians) => [
  x * Math.cos(radians) - y * Math.sin(radians),
  x * Math.sin(radians) + y * Math.cos(radians)
];

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

if (inProximity && aligned) {
  [agentX, agentY] = rotate(agentX, agentY, preference);
}
```
\$\endgroup\$
2
  • \$\begingroup\$ Thank you! I understood the problem and why my quick fix worked but it felt a bit botched and I was thinking it might lead to bigger problems down the line because of an underlying problem to my entire AI logic. Btw, change turnAway to rotate or the other way around in your answer :) Have a nice weekend. \$\endgroup\$
    – superlaks
    Commented May 2, 2020 at 4:34
  • \$\begingroup\$ @superlaks Whoops, yeah that should have been rotate. Great catch, happy to help :) \$\endgroup\$
    – Maanlamp
    Commented May 2, 2020 at 11:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.