1
\$\begingroup\$

I'm making a game with HTML canvas (not WEBGL). I have zombies that go to the center of the screen, but for now they are just circles. I want to give them two arms, like the arms players have in surviv.io. I have looked at this question: Moving an object in a circular path. I don't know what trigonometry to use to calculate the angle. Here is my code:

var canvas = document.querySelector('#canvas');
var context = canvas.getContext('2d');

function Enemy(x, y) {
  this.x = x;
  this.y = y;
  this.r = 5;
  this.color = `hsl(${Math.floor(Math.random() * 360)},50%,50%)`;
  this.vx = 0;
  this.vy = 0;
  this.draw = () => {
    this.x += this.vx;
    this.y += this.vy;
    context.beginPath();
    context.fillStyle = this.color;
    context.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    context.fill();
  }
}
var enemy = new Enemy(0, 0);

function loop() {
  requestAnimationFrame(loop);
  context.fillStyle = 'rgba(0,0,0,0.2)';
  context.fillRect(0, 0, canvas.width, canvas.height);
  var angle = Math.atan2(canvas.height / 2 - enemy.y, canvas.width / 2 - enemy.x);
  enemy.vx = Math.cos(angle);
  enemy.vy = Math.sin(angle);
  enemy.draw();
}
requestAnimationFrame(loop)
<canvas id='canvas'>

I want the arms facing the center, and I want them to move when the enemy changes direction.

\$\endgroup\$
1
\$\begingroup\$

I'm not familiar with the game you mention, but on a quick image search it looks like the entity arms are represented by circles, either holding a gun or in resting positions when not holding anything. Regardless, the basic approach is pretty much the same:

const angle = Math.atan2(this.vy, this.vx);
ctx.beginPath();
ctx.arc(
  this.x + Math.cos(angle + armOffsetRadians) * armDistance,
  this.y + Math.sin(angle + armOffsetRadians) * armDistance,
  this.r / 2, 
  0, Math.PI * 2
);
ctx.stroke();

An alternative approach is to use something like

ctx.save();
ctx.transform(entity.x, entity.y);
ctx.rotate(entityAngle);
// draw the entity facing a neutral direction -- rotation is already handled
ctx.restore();

Here's a naive example of the top approach running:

var canvas = document.querySelector('#canvas');
var context = canvas.getContext('2d');

function Enemy(x, y) {
  this.x = x;
  this.y = y;
  this.r = 5;
  this.color = `hsl(${Math.floor(Math.random() * 360)},50%,50%)`;
  this.vx = 0;
  this.vy = 0;
  this.move = () => {
    this.x += this.vx;
    this.y += this.vy;
  };
  this.draw = ctx => {
    const armOffsetRadians = 0.5;
    const armDistance = 8;
    const angle = Math.atan2(this.vy, this.vx);
    ctx.fillStyle = this.color;
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
    ctx.fill();
    ctx.beginPath();
    ctx.arc(
      this.x + Math.cos(angle + armOffsetRadians) * armDistance,
      this.y + Math.sin(angle + armOffsetRadians) * armDistance,
      this.r / 2, 
      0, Math.PI * 2
    );
    ctx.fill();
    ctx.beginPath();
    ctx.arc(
      this.x + Math.cos(angle - armOffsetRadians) * armDistance, 
      this.y + Math.sin(angle - armOffsetRadians) * armDistance,
      this.r / 2, 
      0, Math.PI * 2
    );
    ctx.fill();
  };
}
var enemy = new Enemy(0, 0);
var mouseX = canvas.width / 2;
var mouseY = canvas.height / 2;
var canRect = canvas.getBoundingClientRect();
canvas.addEventListener("mousemove", e => {
  mouseX = e.clientX - canRect.left;
  mouseY = e.clientY - canRect.top;
});

function loop() {
  requestAnimationFrame(loop);
  context.fillStyle = 'rgba(0,0,0,0.2)';
  context.fillRect(0, 0, canvas.width, canvas.height);
  var angle = Math.atan2(
    mouseY - enemy.y, 
    mouseX - enemy.x
  );
  enemy.vx = Math.cos(angle);
  enemy.vy = Math.sin(angle);
  enemy.move();
  enemy.draw(context);
}
requestAnimationFrame(loop);
<canvas id='canvas'>

I couldn't resist passing the context in as a parameter to Enemy.draw -- if you rely on globals, the function breaks if the name or scope changes.

I also separated move and draw functions. This is good engineering practice in general, since functions should only do one thing, but it's even more important in an animation, because rendering is a totally distinct step from updating positions, computing collision, etc. The typical approach is to loop over all entities and update their positions, handle collisions and complete a full logical update on the game, then loop over the game state in a separate pass and render everything by z-index.

You can see 4 arms when the enemy stops. That's because it's moving back and forth really fast on the destination coordinates. You'll probably want to add a condition not to move if the entity's distance to the destination is below a certain threshold to avoid this "spazzing out" effect.

Beyond that, you can see a lot of repeated work happening here. I'm not sure how you ultimately want to design your game, but adding a setter for an enemy's angle would avoid multiple calls to Math.atan2. Setting enemy.vx and enemy.vy should probably be internal to the enemy and occur when setAngle is called.

I'll leave this design suggestion as an exercise since your primary question is a trigonometric one and I don't want to assume too much.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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