June 29

Asteroids Pt. 2: Shooting some asteroids

Last time I implemented the player flying around, but now it’s time for letting the player shoot.

First thing I do is create a own class for Bullet objects which takes a position and rotation as an argument:

class Bullet{
  
  constructor(pos, rotation){<
    this.pos = createVector(pos.x, pos.y);
    
    this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
  }
  
  tick(){

  }
  
  render(){

  }
  
}

And in my "sketch.js" I'll add a array to hold my bullets, loop through them to tick and render them, and add a way to create them in the keyPressed function when pressing space, sending my players position and rotation as arguments:

var bullets = [];

function draw() {
  ...

  for(let i = 0; i < bullets.length; i++) bullets[i].render();
  
}

function tick() {
  ...

  for(let i = 0; i < bullets.length; i++) bullets[i].tick();
  
}

function keyPressed() {
  ...

  //Spacebar
  if(keyCode == 32){
    bullets.push(new Bullet(ship.pos, ship.rotation));
  }
}

By taking the position and rotation from the player, I can create the bullet right on top of the player and give it the same direction of the player. I also added the tick and render functions:

class Bullet{
  
  constructor(pos, rotation){
    this.pos = createVector(pos.x, pos.y); //Create a new position based on player position
    
    ////Creates a direction vector based off on players rotation. Offset it by Pi/2 to match the ship
    this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
    
    this.speed = 15;  //Bullet speed
    this.size = 3;    //Bullet size
    
    //Creates a velocity vector by multiplying dir vector with the speed
    this.vel = this.dir.mult(this.speed);  
    
  }
  
  tick(){
    this.pos.add(this.vel); //Moves the bullets pos by the velocity
  }
  
  render(){
    ellipse(this.pos.x, this.pos.y, this.size); //Renders the bullet
  }
  
}

After watching some footage of the original game, I saw that the bullets also goes from one side of the screen to the other. I added the feature by simply copy-pasting the previous made code from "ship.js":

  tick(){
    this.pos.add(this.vel); //Moves the bullets pos by the velocity
    
    //Keeps the bullet inside the canvas
    if(this.pos.x > width + this.size / 2){
      this.pos.x = 0;
    }else if(this.pos.x < 0 - this.size / 2){
      this.pos.x = width;
    }
    
    if(this.pos.y > height){
      this.pos.y = 0
    }else if(this.pos.y < 0){
      this.pos.y = height;
    }
    
  }

However, I need to remove the bullet after it has traveled a certain distance. To do that I created a variable to keep track of the distance traveled, and mark it for removal:

  constructor(pos, rotation){
    ...

    this.dist = 0; //Distance traveled
    this.maxDist = width; //Max distance before removal
    this.remove = false;
  }

  tick(){
    ...

    this.dist += abs(this.vel.x) + abs(this.vel.y); //Records the distance travelled
    
    if(this.dist >= this.maxDist) this.remove = true; //If traveled above the limit, mark for removal
    
   ...
    
  }

The reason I mark it for removal instead of directly removing it on the spot, is because in the tick function I'm still looping through all the bullets. Removing something from a array/list while using it, usually leads to problems with the index number.

Now, to remove the bullets all I have to do is loop through the bullet array backwards (to avoid problems with the index number when you remove an object) and remove those marked in my "sketch.js":

function tick() {
  ...

  //Bullets tick
  for (let i = 0; i < bullets.length; i++) bullets[i].tick();
  
  for (let i = bullets.length - 1; i >= 0; i--) {
    if (bullets[i].remove) bullets.splice(i, 1);
  }

}

And the player can now shoot bullets!

Animated GIF

Now we need something to shoot! For rendering and rotating the asteroids I used some of the same code I made for the player:

class Asteroid {
  constructor(x, y, size) {
    this.pos = createVector(x, y);

    this.size = size;

    this.radius = 15;

    this.angularSpeed = random(0.01, 0.03);
    this.rotation = random(0, 2 * PI);
    this.rotation = 0;
  }

  tick() {
    this.rotation += this.angularSpeed;
  }

  render() {

    push(); //Makes so that translate and rotate functions only apply to this object

    translate(this.pos.x, this.pos.y); //Moves the (0, 0) point to given coords
    rotate(this.rotation); //Rotates the object in radians

    noFill(); //Draw shapes without fill
    stroke(255); //Set the stroke color to white

    let r = this.radius;
    let ox = -6, oy = -17;   //Offsets the asteroid
    
    line(-r * 1.5 + ox, r / 2 + oy, -r / 2 + ox, -r / 2 + oy);
    line(-r / 2 + ox, -r / 2 + oy, r / 2 + ox, r / 4 + oy);
    line(r / 2 + ox, r / 4 + oy, r * 2 + ox, 0 + oy);
    line(r * 2 + ox, 0 + oy, r * 1.75 + ox, r + oy);
    line(r * 1.75 + ox, r + oy, r * 2 + ox, r * 2 + oy);
    line(r * 2 + ox, r * 2 + oy, r / 2 + ox, r * 3 + oy);
    line(r / 2 + ox, r * 3 + oy, -r + ox, r * 2.5 + oy);
    line(-r + ox, r * 2.5 + oy, -r * 1.5 + ox, r / 2 + oy);

    pop(); //Makes so that translate and rotate functions only apply to this object

  }

}

I render multiple line segments to make it look like the asteroids in the game. I decided to render it using a radius variable, since I need to have different sized asteroids. The idea worked perfectly, though I noticed a problem where the asteroids x and y coordinates were not centered around where I actually draw the asteroid. That creates a big problem since I rotate the asteroid around that point. To fix that I created an "ox" and an "oy" variable to offset where I could just offset the rendered asteroid.

I create an array of asteroids and loop through them to both tick and render them, and voila!

Animated GIF

The last thing to do now is making the bullets explode the asteroids. I first need to find a way to do hit detection. I decided to just create some rectangles for the bullets and asteroids since it's the most efficient method. The hitboxes won't align properly with what's rendering the screen though.

P5.js doesn't have any build in support for hitbox detection so I need to create that myself. First thing I do is create "rectangles" for my asteroids and bullets:

Asteroids
  tick() {
    ...

    //Hitbox
    this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
    this.rectRightSide = createVector(this.pos.x + this.radius * 1.5, this.pos.y + this.radius * 1.5);
  }
Bullets
  tick(){
    ...

    //Hitbox
    
    //Only create right side since I already have left side as "pos"
    this.rectRightSide = createVector(this.pos.x + this.size, this.pos.y + this.size);
    
  }

Creating a vector every single tick is not really good performance wise, but for this simple little game it will be fine. I don't really create a rectangle in these objects, but rather keep track off where the top-left side and bottom right-side of a rectangle would be.

Now I need a function to check if two "rectangles" are intersecting. Funnily enough, the fastest way to check if two rectangles are intersecting is to check if they are not intersecting at all:

function intersects(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2){
  return !(r1x1 > r2x2 || r2x1 > r1x2 || r1y1 > r2y2 || r2y1 > r1y2);
}

I can now make a nested loop to check if a bullet and an asteroid is collided, and remove them both:


function tick() {
  ...
  
  //Hitbox detection
  for (let i = 0; i < bullets.length; i++){
    for (let j = 0; j < asteroids.length; j++){
      
      var b = bullets[i];
      var a = asteroids[j];
      
      if(intersects(b.pos.x, b.pos.y, b.rectRightSide.x, b.rectRightSide.y,
                    a.rectLeftSide.x, a.rectLeftSide.y, a.rectRightSide.x, a.rectRightSide.y)){
         
        a.remove = true;
        b.remove = true;
      }
      
    }
  }

}
Animated GIF

Next time I need to make the asteroid split up instead of just being removed, spawn asteroids and make them move!

Full code for "sketch.js"
var ship;
var bullets = [];
var asteroids = [];

function setup() {
  createCanvas(800, 640);

  ship = new Ship(width / 2, height / 2, 12);
  
  asteroids.push(new Asteroid(width / 2, height / 4, 1));
  
}

function draw() {
  background(0);

  tick();

  ship.render();
  for (let i = 0; i < bullets.length; i++) bullets[i].render();
  for (let i = 0; i < asteroids.length; i++) asteroids[i].render();

}

function tick() {
  ship.tick();

  //Bullets tick
  for (let i = 0; i < bullets.length; i++) bullets[i].tick();
  
  for (let i = bullets.length - 1; i >= 0; i--) {
    if (bullets[i].remove) bullets.splice(i, 1);
  }
  
  //Asteroids tick
  for (let i = 0; i < asteroids.length; i++) asteroids[i].tick();
  
  for (let i = asteroids.length - 1; i >= 0; i--) {
    if (asteroids[i].remove) asteroids.splice(i, 1);
  }
  
  //Hitbox detection
  for (let i = 0; i < bullets.length; i++){
    for (let j = 0; j < asteroids.length; j++){
      
      var b = bullets[i];
      var a = asteroids[j];
      
      if(intersects(b.pos.x, b.pos.y, b.rectRightSide.x, b.rectRightSide.y,
                    a.rectLeftSide.x, a.rectLeftSide.y, a.rectRightSide.x,                                   a.rectRightSide.y)){
         
        a.remove = true;
        b.remove = true;
        
      }
      
    }
  }

}

function keyPressed() {
  if (keyCode == UP_ARROW) ship.setAcceleration(0.1);

  if (keyCode == LEFT_ARROW) {
    ship.rotate(-0.075);
  } else if (keyCode == RIGHT_ARROW) {
    ship.rotate(0.075);
  }

  //Spacebar
  if (keyCode == 32) {
    bullets.push(new Bullet(ship.pos, ship.rotation));
  }

}

function keyReleased() {
  if (keyCode == UP_ARROW) ship.setAcceleration(0);

  if (keyCode == LEFT_ARROW) {
    ship.rotate(0);
  }

  if (keyCode == RIGHT_ARROW) {
    ship.rotate(0);
  }

}

function intersects(r1x1, r1y1, r1x2, r1y2, r2x1, r2y1, r2x2, r2y2){
  return !(r1x1 > r2x2 || r2x1 > r1x2 || r1y1 > r2y2 || r2y1 > r1y2);
}

[collapse]
Full code for "ship.js"
class Ship {

  constructor(x, y, size) {
    this.pos = createVector(x, y);
    this.size = size;

    this.acc = createVector(0, 0);
    this.vel = createVector(0, 0);

    this.rotation = 0;
    this.angularSpeed = 0;
    this.dir = createVector(0, 0);

    this.addedAcc = 0;
    this.friction = 0.015;
    this.maxVel = 10;
  }

  tick() {

    //Movement
    this.movement();

    this.rotation += this.angularSpeed;

  }

  movement() {
    
    //Acceleration
    this.acc = p5.Vector.fromAngle(this.rotation - PI / 2).normalize().mult(this.addedAcc);
    
    //Velocity
    this.vel.add(this.acc);
    
    //Prevents Ship from going too fast
    if(this.vel.x > this.maxVel) this.vel.x = this.maxVel;
    if(this.vel.y > this.maxVel) this.vel.y = this.maxVel;
    if(this.vel.x < -this.maxVel) this.vel.x = -this.maxVel;
    if(this.vel.y < -this.maxVel) this.vel.y = -this.maxVel;
    
    //Moves the ship according to the velocity
    this.pos.add(this.vel);
    
    //Friction
    let frictionVec = createVector(this.vel.x, this.vel.y);
    
    if(this.vel.x != 0 || this.vel.y != 0) this.vel.add(frictionVec.normalize().mult(-this.friction));
    
    //Keeps the ship inside the canvas
    if(this.pos.x > width + this.size / 2){
      this.pos.x = 0;
    }else if(this.pos.x < 0 - this.size / 2){
      this.pos.x = width;
    }
    
    if(this.pos.y > height){
      this.pos.y = 0
    }else if(this.pos.y < 0){
      this.pos.y = height;
    }

  }

  render() {

    push();  //Makes so that translate and rotate functions only apply to this object

    translate(this.pos.x, this.pos.y);    //Moves the (0, 0) point to given coords
    rotate(this.rotation);                //Rotates the object in radians

    noFill();      //Draw shapes without fill
    stroke(255);   //Set the stroke color to white

    triangle(-this.size, this.size, 0, -this.size * 1.5, this.size, this.size) //Draws a triangle

    pop();  //Makes so that translate and rotate functions only apply to this object

  }

  setAcceleration(acc) {
    this.addedAcc = acc;
  }

  rotate(angularSpeed) {
    this.angularSpeed = angularSpeed;
  }

}
[collapse]
Full code for "bullet.js"
class Bullet{
  
  constructor(pos, rotation){
    this.pos = createVector(pos.x, pos.y); //Create a new position based on player position
    
    ////Creates a direction vector based off on players rotation. Offset it by Pi/2 to match the ship
    this.dir = p5.Vector.fromAngle(rotation - PI / 2).normalize();
    
    this.speed = 15;  //Bullet speed
    this.size = 3;    //Bullet size
    
    //Creates a velocity vector by multiplying dir vector with the speed
    this.vel = this.dir.mult(this.speed);  
    
    this.dist = 0; //Distance traveled
    this.maxDist = width; //Max distance before removal
    this.remove = false;
    
    this.rectRightSide = createVector(pos.x + this.size, pos.y + this.size);
    
  }
  
  tick(){
    this.pos.add(this.vel); //Moves the bullets pos by the velocity
    
    this.dist += abs(this.vel.x) + abs(this.vel.y); //Records the distance travelled
    
    if(this.dist >= this.maxDist) this.remove = true; //If traveled above the limit, mark for removal
    
    //Keeps the bullet inside the canvas
    if(this.pos.x > width + this.size / 2){
      this.pos.x = 0;
    }else if(this.pos.x < 0 - this.size / 2){
      this.pos.x = width;
    }
    
    if(this.pos.y > height){
      this.pos.y = 0
    }else if(this.pos.y < 0){
      this.pos.y = height;
    }
    
    //Hitbox
    
    //Only create right side since I already have left side as "pos"
    this.rectRightSide = createVector(this.pos.x + this.size, this.pos.y + this.size);
    
  }
  
  render(){
    fill(255);
    stroke(255);
    
    ellipse(this.pos.x, this.pos.y, this.size); //Renders the bullet
    
  }
  
}
[collapse]
Full code for "asteroid.js"
class Asteroid {
  constructor(x, y, size) {
    this.pos = createVector(x, y);

    this.size = size;

    this.radius = 15;

    this.angularSpeed = random(0.01, 0.03);
    this.rotation = random(0, 2 * PI);
    this.rotation = 0;
    
    this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
    this.rectRightSide = createVector(this.radius * 3, this.radius * 3);
    
  }

  tick() {
    this.rotation += this.angularSpeed;
    
    //Hitbox
    this.rectLeftSide = createVector(this.pos.x - this.radius * 1.5, this.pos.y - this.radius * 1.5);
    this.rectRightSide = createVector(this.pos.x + this.radius * 1.5, this.pos.y + this.radius * 1.5);
  }

  render() {

    push(); //Makes so that translate and rotate functions only apply to this object

    translate(this.pos.x, this.pos.y); //Moves the (0, 0) point to given coords
    rotate(this.rotation); //Rotates the object in radians

    noFill(); //Draw shapes without fill
    stroke(255); //Set the stroke color to white

    let r = this.radius;
    let ox = -6, oy = -17;   //Offsets the asteroid
    
    line(-r * 1.5 + ox, r / 2 + oy, -r / 2 + ox, -r / 2 + oy);
    line(-r / 2 + ox, -r / 2 + oy, r / 2 + ox, r / 4 + oy);
    line(r / 2 + ox, r / 4 + oy, r * 2 + ox, 0 + oy);
    line(r * 2 + ox, 0 + oy, r * 1.75 + ox, r + oy);
    line(r * 1.75 + ox, r + oy, r * 2 + ox, r * 2 + oy);
    line(r * 2 + ox, r * 2 + oy, r / 2 + ox, r * 3 + oy);
    line(r / 2 + ox, r * 3 + oy, -r + ox, r * 2.5 + oy);
    line(-r + ox, r * 2.5 + oy, -r * 1.5 + ox, r / 2 + oy);
    
    pop(); //Makes so that translate and rotate functions only apply to this object
    
  }

}
[collapse]


Copyright 2020. All rights reserved.

Posted June 29, 2020 by Atle in category "JavaScript

Leave a Reply

Your email address will not be published. Required fields are marked *