5

I'm making a javascript game in which I want enemy ships to be able to rotate towards a specified point. Their movement can then be calculated from their angle. The following code for the rotation works only if the ship is below and to the right of its target. If the ship is to the left, it rotates to ~0 degrees and jitters there. To the top right it continuously rotates counterclockwise. What the heck am I doing wrong here? Any suggestions as to a better method?

obj.angle %= 360;
// Calculate angle to target point
var targetAngle = Math.atan2(obj.mode.dest.y - obj.y, obj.mode.dest.x - obj.x) * (180 / Math.PI) + 90;
// Get the difference between the current angle and the target angle
var netAngle = Math.abs(obj.angle - targetAngle) % 360;
// Turn in the closest direction to the target
netAngle > 180 ? obj.angle += obj.shipType.turnSpeed : obj.angle -= obj.shipType.turnSpeed;
if(obj.angle < 0) obj.angle += 360;
if(obj.angle > 360) obj.angle -= 360;

My question is very similar to this one, which explains it better but unfortunately is in C#.

EDIT: Here's the working code, for anyone who might find it useful:

obj.angle %= 360;
var targetAngle = Math.atan2(obj.mode.dest.y - obj.y, obj.mode.dest.x - obj.x) * (180 / Math.PI) + 90;
targetAngle = (targetAngle + 360) % 360;
if(obj.angle != targetAngle)
{
    var netAngle = (obj.angle - targetAngle + 360) % 360;
    var delta = Math.min(Math.abs(netAngle - 360), netAngle, obj.shipType.turnSpeed);
    var sign  = (netAngle - 180) >= 0 ? 1 : -1;
    obj.angle += sign * delta + 360;
    obj.angle %= 360;
}
Community
  • 1
  • 1
Austen
  • 1,931
  • 2
  • 19
  • 28
  • Solutions should be posted as a [self answer](https://stackoverflow.com/help/self-answer) rather than an edit into the question, which is confusing and bypasses voting. Thanks. – ggorlen Aug 20 '22 at 00:32

2 Answers2

3

With little change to your code it appears to work:

http://jsfiddle.net/57hAk/11/

tobyodavies
  • 27,347
  • 5
  • 42
  • 57
  • That is certainly a step in the right direction. However, it still has some quirks. Check out how it overturns then turns back when the ship is to the bottom right of the target: http://jsfiddle.net/57hAk/4/. I also noticed it jumping to the target angle up to 45 degrees at once, but I haven't been able to reproduce that yet. – Austen Aug 06 '12 at 18:25
  • Ok, here it is in the top left jumping from 3 to 135: http://jsfiddle.net/57hAk/5/ – Austen Aug 06 '12 at 18:38
  • tidied it up a bit and fixed the overrotation – tobyodavies Aug 07 '12 at 00:09
  • Relevant code should be in the answer itself, not an external link. Thanks. – ggorlen Aug 20 '22 at 00:27
1

This can be done without the conversion to degrees.

The steps are:

  1. Compute the angle theta between the source and destination vectors with Math.atan2.
  2. Normalize theta to the range 0..Tau.
  3. Take the difference between the source angle and theta, taking care of wraparound below -PI during the subtraction.

This produces a diff value between -Pi..+Pi. If it's negative, turn clockwise, otherwise turn counterclockwise.

The formula only works if the player angle is constrained to the range -Pi..+Pi.

Here's a working example.

const {PI} = Math;
const TAU = 2 * PI;
const canvas = document.querySelector("canvas");
canvas.width = canvas.height = 240;
const ctx = canvas.getContext("2d");

const player = {
  x: canvas.width / 2,
  y: canvas.height / 2,
  angle: 0,
  radius: 20,
};

const mouse = {x: canvas.width / 2, y: canvas.height / 2};
canvas.addEventListener("mousemove", e => {
  mouse.x = e.offsetX;
  mouse.y = e.offsetY;
});

(function rerender() {
  requestAnimationFrame(rerender);

  let theta = Math.atan2(mouse.y - player.y, mouse.x - player.x);
  theta = theta < 0 ? theta + TAU : theta;
  let diff = player.angle - theta;
  diff = diff < -PI ? diff + TAU : diff;

  if (Math.abs(diff) > 0.02) {
    player.angle -= 0.04 * Math.sign(diff);

    if (player.angle > PI) {
      player.angle -= TAU;
    }
    else if (player.angle < -PI) {
      player.angle += TAU;
    }
  }

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // draw player
  ctx.save();
  ctx.strokeStyle = "black";
  ctx.lineWidth = 8;
  ctx.translate(player.x, player.y);
  ctx.rotate(player.angle);
  ctx.beginPath();
  ctx.arc(0, 0, player.radius, 0, TAU);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(0, 0);
  ctx.lineTo(player.radius * 2, 0);
  ctx.stroke();
  ctx.restore();

  // draw crosshair
  ctx.strokeStyle = "red";
  ctx.lineWidth = 4;
  ctx.beginPath();
  ctx.moveTo(mouse.x - 10, mouse.y);
  ctx.lineTo(mouse.x + 10, mouse.y);
  ctx.stroke();
  ctx.beginPath();
  ctx.moveTo(mouse.x, mouse.y - 10);
  ctx.lineTo(mouse.x, mouse.y + 10);
  ctx.stroke();
})();
canvas {
  border: 2px solid black;
}
<canvas></canvas>

Another option:

const theta = Math.atan2(mouse.y - player.y, mouse.x - player.x);
let diff = player.angle - theta;
diff = diff > PI ? diff - TAU : diff;
diff = diff < -PI ? diff + TAU : diff;

This can probably be simplified further; feel free to leave a comment.

ggorlen
  • 44,755
  • 7
  • 76
  • 106