javascriptcanvasdrawprojectile

My projectile system not working, what's wrong with it, how can I fix it?


I'm making an asteroid shooter system, that can shoot projectiles where the player is facing, however, when you shoot (press space), the projectile does not move.

I've tried to add cosine and sine to the X and Y, but that obviously doesn't work. I tried adding print statements to see where it went wrong and it looks like the X and Y is just not incrementing at all. How can I make it so that it increments properly and exactly how I want it to?

circle.x += Math.cos(circle.direction) * circle.speed;
circle.y += Math.sin(circle.direction) * circle.speed;

Full code:

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var projectileArray = []
var keydown = false
const gamePlaying = true

const kbd = {
  ArrowLeft: false,
  ArrowUp: false,
  ArrowRight: false,
  ArrowDown: false,
};

const noHoldDown = {
  Space: false,
}

const ship = {
  angle: 0,
  color: "white",
  x: canvas.width / 2,
  y: canvas.height / 2,
  width: 10,
  height: 12,
  drag: 0.9,
  accSpeed: 0.025,
  rotSpeed: 0.007,
  rotv: 0,
  ax: 0,
  ay: 0,
  vx: 0,
  vy: 0,
  rotateLeft() {
    this.rotv -= this.rotSpeed;
  },
  rotateRight() {
    this.rotv += this.rotSpeed;
  },
  accelerate() {
    this.ax += this.accSpeed;
    this.ay += this.accSpeed;
  },
  decelerate() {
    this.ax -= this.accSpeed;
    this.ay -= this.accSpeed;
  },
  shoot() {
    projectileArray.push([this.x, this.y, this.angle])
  },
  move() {
    this.angle += this.rotv;
    this.rotv *= this.drag;
    this.vx += this.ax;
    this.vy += this.ay;
    this.ax *= this.drag;
    this.ay *= this.drag;
    this.vx *= this.drag;
    this.vy *= this.drag;
    this.x += Math.cos(this.angle) * this.vx;
    this.y += Math.sin(this.angle) * this.vy;
  },
  draw(ctx) {
    ctx.save();
    ctx.lineWidth = 3;
    ctx.translate(this.x, this.y);
    ctx.rotate(this.angle);
    ctx.beginPath();
    ctx.moveTo(this.height, 0);
    ctx.lineTo(-this.height, this.width);
    ctx.lineTo(-this.height, -this.width);
    ctx.closePath();
    ctx.strokeStyle = this.color;
    ctx.stroke();
    ctx.restore();
  }
};

document.addEventListener("keydown", event => {
  if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = true;
  }
});
document.addEventListener("keydown", event => {
  if (event.code in noHoldDown) {
    if (!keydown) {
      keydown = true;
      ship.shoot();
    }
  }
});

document.addEventListener('keyup', event => {
  if (event.code in noHoldDown) {
    keydown = false;
  }
});

document.addEventListener("keyup", event => {
  if (event.code in kbd) {
    event.preventDefault();
    kbd[event.code] = false;
  }
});

(function update() {
  ctx.fillStyle = "black";
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const shipActions = {
    ArrowLeft: "rotateLeft",
    ArrowUp: "accelerate",
    ArrowDown: "decelerate",
    ArrowRight: "rotateRight",
  };

  for (const key in shipActions) {
    if (kbd[key]) {
      ship[shipActions[key]]();
    }
  }
  ship.move();
  ship.draw(ctx);
  for (var i = 0; i < projectileArray.length; i++) {
    function MovableCircle() {
      this.x = projectileArray[i][0];
      this.y = projectileArray[i][1];
      this.speed = 1;
      this.direction = projectileArray[i][1];

      this.draw = () => {
        ctx.arc(this.x, this.y, 3.75, 0, 2 * Math.PI);
        ctx.fillStyle = 'white';
        ctx.fill();
      }
    }
    var circle = new MovableCircle();
    ctx.beginPath();
    circle.x += Math.cos(circle.direction) * circle.speed;
    circle.y += Math.sin(circle.direction) * circle.speed;
    circle.draw();
    console.log(circle.x, circle.y)
  }
  requestAnimationFrame(update);
})();


Solution

  • I put so many quarters into Asteroids, I had contribute here! The problem stems from the persistence of projectiles: Rebuilding them at each turn of the update loop forgets any state change made in the prior iteration.

    Projectiles should be first-class objects, held onto by either the game or the ship. Below, are two changes to the OP's otherwise fine script:

    1. A Projectile class, using basically the OP code:
    function Projectile(x,y,speed,direction,duration) {
      Object.assign(this, {x,y,speed,direction,duration});
      this.draw = ctx => {
        ctx.arc(this.x, this.y, 3.75, 0, 2 * Math.PI);
        ctx.fillStyle = 'white';
        ctx.fill();
      }
      this.update = ctx => {
        ctx.beginPath();
        this.x += Math.cos(this.direction) * this.speed;
        this.y += Math.sin(this.direction) * this.speed;
        this.draw(ctx);
        this.duration--;
      }
      this.isDone = () => this.duration <= 0;
    }
    

    Functionally, the only thing I added was a duration, since I think I recall in the original that projectiles which don't hit anything eventually pop out of existence.

    1. Since the ship already has a projectile array, here, it's modified to hold projectile instances:

    The shoot method changes to

      shoot() {
        let mySpeed = Math.sqrt(this.vx*this.vx + this.vy*this.vy)
        let bullet = new Projectile(this.x, this.y, 1+mySpeed, this.angle, 500);
        projectileArray.push(bullet);
      },
    

    Side note: I think, in the original game, projectiles' speed was fixed to be greater than the top speed of the ship, but I don't remember clearly. In the above, I set projectile speed to +1 more than the ship's current speed. This is more physically correct (at sub-relativistic speeds :-)), but probably not accurate to the original.

    And the draw method cleans up significantly, since we moved the state-forgetting projectile code into its own class

      ship.draw(ctx);
      for (var i = 0; i < projectileArray.length; i++) {
        let bullet = projectileArray[i];
        bullet.update(ctx)
      }
      projectileArray = projectileArray.filter(bullet => !bullet.isDone());
    

    Side note: Javascript classes, starting around ES5/ES6 provide a big improvement in syntax. It's worth getting familiar with this.

    Demo

    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    document.body.appendChild(canvas);
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    var projectileArray = []
    var keydown = false
    const gamePlaying = true
    
    const kbd = {
      ArrowLeft: false, 
      ArrowUp: false, 
      ArrowRight: false,
      ArrowDown: false,
    };
    
    const noHoldDown = {
      Space: false,
    }
    
    function Projectile(x,y,speed,direction,duration) {
      Object.assign(this, {x,y,speed,direction,duration});
      this.draw = ctx => {
        ctx.arc(this.x, this.y, 3.75, 0, 2 * Math.PI);
        ctx.fillStyle = 'white';
        ctx.fill();
      }
      this.update = ctx => {
        ctx.beginPath();
        this.x += Math.cos(this.direction) * this.speed;
        this.y += Math.sin(this.direction) * this.speed;
        this.draw(ctx);
        this.duration--;
      }
      this.isDone = () => this.duration <= 0;
    }
    
    
    const ship = {
      angle: 0,
      color: "white",
      x: canvas.width / 2,
      y: canvas.height / 2,
      width: 10,
      height: 12,
      drag: 0.9,
      accSpeed: 0.025,
      rotSpeed: 0.007,
      rotv: 0,
      ax: 0,
      ay: 0,
      vx: 0,
      vy: 0,
      rotateLeft() {
        this.rotv -= this.rotSpeed;
      },
      rotateRight() {
        this.rotv += this.rotSpeed;
      },
      accelerate() {
        this.ax += this.accSpeed;
        this.ay += this.accSpeed;
      },
      decelerate() {
        this.ax -= this.accSpeed;
        this.ay -= this.accSpeed;
      },
      shoot() {
        let mySpeed = Math.sqrt(this.vx*this.vx + this.vy*this.vy)
        let bullet = new Projectile(this.x, this.y, 3+mySpeed, this.angle, 500)
        projectileArray.push(bullet);
        // projectileArray.push([this.x, this.y, this.angle])
      },
      move() {
        this.angle += this.rotv;
        this.rotv *= this.drag;
        this.vx += this.ax;
        this.vy += this.ay;
        this.ax *= this.drag;
        this.ay *= this.drag;
        this.vx *= this.drag;
        this.vy *= this.drag;
        this.x += Math.cos(this.angle) * this.vx;
        this.y += Math.sin(this.angle) * this.vy;
      },
      draw(ctx) {
        ctx.save();
        ctx.lineWidth = 3;
        ctx.translate(this.x, this.y);
        ctx.rotate(this.angle);
        ctx.beginPath();
        ctx.moveTo(this.height, 0);
        ctx.lineTo(-this.height, this.width);
        ctx.lineTo(-this.height, -this.width);
        ctx.closePath();
        ctx.strokeStyle = this.color;
        ctx.stroke();
        ctx.restore();
      }
    };
    
    document.addEventListener("keydown", event => {
      if (event.code in kbd) {
        event.preventDefault();
        kbd[event.code] = true;
      }
    });
    document.addEventListener("keydown", event => {
      if (event.code in noHoldDown) {
        if (!keydown) {
          keydown = true;
          ship.shoot();
        }
      }
    });
    
    document.addEventListener('keyup', event => {
      if (event.code in noHoldDown) {
        keydown = false;
      }
    });
    
    document.addEventListener("keyup", event => {
      if (event.code in kbd) {
        event.preventDefault();
        kbd[event.code] = false; 
      }
    });
    
    (function update() {
      ctx.fillStyle = "black";
      ctx.fillRect(0, 0, canvas.width, canvas.height);  
    
      const shipActions = {
        ArrowLeft: "rotateLeft",
        ArrowUp: "accelerate",
        ArrowDown: "decelerate",
        ArrowRight: "rotateRight",
      };
    
      for (const key in shipActions) {
        if (kbd[key]) {
          ship[shipActions[key]]();
        }
      }
      ship.move();
      ship.draw(ctx);
      for (var i = 0; i < projectileArray.length; i++) {
        let bullet = projectileArray[i];
        bullet.update(ctx)
      }
      projectileArray = projectileArray.filter(bullet => !bullet.isDone());
      requestAnimationFrame(update);
    })();