Your browser does not support the canvas tag.

previous        Show / Hide Source        Download        next
/* Tianyao Liu
 * Object-Oriented Toy: Bullet Rain
 * Intro to Media Computation
 *
 * Press Z to start the game.
 * Use Arrow Keys to move, Z to shoot, and Shift to focus.
 * Defeat the enemy while getting hit as little as possible.
 */


boolean gameStart = false; //has the boss been spawned?
Background[] scrollingBackground = new Background[2]; //scrolling background
Player player; //player character
Enemy enemy; //enemy boss
PFont font; //font object for death counter
ArrayList<PlayerBullet> playerBullet = new ArrayList<PlayerBullet>(); //player bullets
ArrayList<EnemyBullet> enemyBullet = new ArrayList<EnemyBullet>(); //enemy bullets
ArrayList<Particle> particle = new ArrayList<Particle>(); //particles

void setup() {
  size(800, 600);
  frameRate(60);
  noCursor();
  rectMode(CENTER);
  ellipseMode(CENTER);
  colorMode(HSB, 360, 100, 100, 100);

  player = new Player(); //create player
  font = createFont("Cirno.ttf", 30); //create font object
  textAlign(LEFT, TOP);
  textFont(font);
  textSize(30);
  
  //create backgrounds
  scrollingBackground[0] = new Background(-600);
  scrollingBackground[1] = new Background(0);
}

void draw() {
  background(0, 0, 0);
  //update backgrounds
  scrollingBackground[0].update();
  scrollingBackground[1].update();
  //display backgrounds
  scrollingBackground[0].display();
  scrollingBackground[1].display();

  //spawn star particle every 4 frames
  if (frameCount % 4 == 0) {
    spawnParticle(random(0, 800), -50, 1, 90, random(2, 11));
  }
  //update and display particles
  for (Particle i : particle) {
    if (!i.unused) {
      i.update();
      i.display();
    }
  }

  //update player
  player.update();
  //display player
  player.display();

  if (gameStart) {
    enemy.update(); //update enemy
    enemy.display(); //display enemy
    //update and display player bullets
    for (PlayerBullet i : playerBullet) {
      if (!i.unused) {
        i.update();
        i.display();
      }
    }
    //update and display enemy bullets
    for (EnemyBullet i : enemyBullet) {
      if (!i.unused) {
        i.update();
        i.display();
      }
    }

    noStroke();
    fill(0, 0, 100);
    text("DEATHS: "+player.deaths, 10, 10); //death counter
    
    //battle start animation for enemy
    if (enemy.timer == 0) {
      noStroke();
      fill(0, 0, 100);
      rect(400, 300, 900, 700);
      spawnParticle(enemy.pos.x, enemy.pos.y, 3, 0, 0);
    }
    //death animation for enemy
    if (!enemy.dead) {
      if (enemy.hp <= 0 && enemy.timer > 0) {
        enemy.dead = true;
        //destroy all enemy bullets
        for (EnemyBullet i : enemyBullet) {
          if (!i.destroyed) {
            i.destroyed = true;
          }
        }
        noStroke();
        fill(0, 0, 100);
        rect(400, 300, 900, 700);
        //explosion!
        for (int j = 0; j < 8; j++) {
          spawnParticle(enemy.pos.x, enemy.pos.y, 2, j*45, 5);
        }
        spawnParticle(enemy.pos.x, enemy.pos.y, 3, 0, 0);
      }
    }
  }
}

//spawn a particle object with the following parameters: x position, y position, type, direction, velocity
void spawnParticle(float x, float y, int t, float d, float v) {
  boolean recycle = false;
  //check for unused particle objects and recycle if available
  for (Particle i : particle) {
    if (i.unused) {
      i.recycle(x, y, t, d, v);
      recycle = true;
      break;
    }
  }
  //create new particle object if no available unused objects
  if (!recycle) {
    particle.add(new Particle(x, y, t, d, v));
  }
}

//key press events
void keyPressed() {
  if (keyCode == RIGHT && !player.right) { //move right
    player.right = true;
  }
  if (keyCode == LEFT && !player.left) { //move left
    player.left = true;
  }
  if (keyCode == DOWN && !player.down) { //move down
    player.down = true;
  }
  if (keyCode == UP && !player.up) { //move up
    player.up = true;
  }
  if (keyCode == SHIFT && !player.focus) { //focus
    player.focus = true;
  }
  if ((key == 'z' || key == 'Z') && !player.fire) { //fire
    player.fire = true;
    //pressing Z starts game
    if (!gameStart) {
      gameStart = true;
      enemy = new Enemy(); //create enemy boss
    }
  }
}

//key release events
void keyReleased() {
  if (keyCode == RIGHT && player.right) { //stop moving right
    player.right = false;
  }
  if (keyCode == LEFT && player.left) { //stop moving left
    player.left = false;
  }
  if (keyCode == DOWN && player.down) { //stop moving down
    player.down = false;
  }
  if (keyCode == UP && player.up) { //stop moving up
    player.up = false;
  }
  if (keyCode == SHIFT && player.focus) { //unfocus
    player.focus = false;
  }
  if ((key == 'z' || key == 'Z') && player.fire) { //stop firing
    player.fire = false;
  }
}
/* Background class
 * Pieces of scrolling background; will constantly scroll downwards, moving above screen after passing bottom edge
 */

class Background {
  float y; //y position
  int col; //colour

  Background(float yPos) {
    y = yPos; //set initial y position
    col = 0;
  }

  //update function
  void update() {
    y += 3; //move down 3 pixels every frame
    col += 1; //change hue 1 unit every frame
    //move back to top once it passes bottom
    if (y == 600) {
      y = -600;
    }
    if (col == 360) {
      col = 0;
    }
  }
  
  //display function
  void display() {
    strokeWeight(1);
    stroke(col, 100, 100);
    noFill();

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 5; j++) {
        rect(j*200, y+i*300, 50, 50);
      }
    }
    for (int i = 0; i < 3; i++) {
      ellipse(i*400, y+150, 50, 50);
    }
    for (int i = 0; i < 3; i++) {
      ellipse(i*400, y+450, 50, 50);
    }
    for (int i = 0; i < 2; i++) {
      rect(i*400+200, y+150, 50, 50);
    }
    for (int i = 0; i < 2; i++) {
      rect(i*400+200, y+450, 50, 50);
    }

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 2; j++) {
        quad(j*400+200, y+i*300-100, j*400+300, y+i*300, j*400+200, y+i*300+100, j*400+100, y+i*300);
      }
    }
    for (int i = 0; i < 3; i++) {
      ellipse(i*400, y+150, 200, 200);
    }
    for (int i = 0; i < 3; i++) {
      ellipse(i*400, y+450, 200, 200);
    }
    
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 2; j++) {
        line(j*400+200-50, y+i*300-50, j*400+200-25, y+i*300-25);
        line(j*400+200+50, y+i*300-50, j*400+200+25, y+i*300-25);
        line(j*400+200-50, y+i*300+50, j*400+200-25, y+i*300+25);
        line(j*400+200+50, y+i*300+50, j*400+200+25, y+i*300+25);
      }
    }
    for (int i = 0; i < 2; i++) {
      for (int j = 0; j < 2; j++) {
        line(j*400+200, y+i*300+150-50, j*400+200, y+i*300+150-25);
        line(j*400+200+100, y+i*300+150, j*400+200+25, y+i*300+150);
        line(j*400+200, y+i*300+150+50, j*400+200, y+i*300+150+25);
        line(j*400+200-100, y+i*300+150, j*400+200-25, y+i*300+150);
      }
    }
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 2; j++) {
        line(j*400+25, y+i*300, j*400+100, y+i*300);
        line(j*400+300, y+i*300, j*400+375, y+i*300);
      }
    }
    for (int i = 0; i < 2; i++) {
      for (int j = 0; j < 2; j++) {
        line(j*400+200-175, y+i*300+150-125, j*400+200-25, y+i*300+150-25);
        line(j*400+200+175, y+i*300+150-125, j*400+200+25, y+i*300+150-25);
        line(j*400+200-175, y+i*300+150+125, j*400+200-25, y+i*300+150+25);
        line(j*400+200+175, y+i*300+150+125, j*400+200+25, y+i*300+150+25);
      }
    }
  }
}
/* Enemy class
 * The enemy boss; moves around shooting bullet patterns, and can be hit by player bullets; is destroyed when its health reaches 0
 */

class Enemy {

  PVector pos; //position vector
  int hp; //health/hitpoints
  int pType; //pattern type
  int timer, pTimer; //timer & pattern timer
  int glow; //glow timer
  int col; //colour
  float calcNum1, calcNum2; //numbers used for various values in bullet patterns (e.g. angle, velocity)
  boolean dead; //is enemy dead?
  
  //initialize all variables
  Enemy() {
    pos = new PVector(400, 120);
    hp = 0;
    pType = 0;
    timer = -120; //equivalent to 2 seconds of startup animation
    pTimer = 0;
    glow = 0;
    calcNum1 = 0;
    calcNum2 = 0;
    dead = false;
  }

  //update function
  void update() {
    if (!dead) {
      timer += 1; //increment timer

      if (timer <= 0) { //startup animation
        hp = (int)((120+timer)*30); //increase hp along with timer
        col = (int)(0.05*hp); //colour corresponds to hp
      } else { //in battle
        //when hp drops below certain points (every 450 hp lost)
        if (hp <= 3600-(pType+1)*450) { 
          pType += 1; //switch to next pattern
          hp = 3600-pType*450;
          pTimer = 0;
          //destroy all enemy bullets
          for (EnemyBullet i : enemyBullet) {
            if (!i.destroyed) {
              i.destroyed = true;
            }
          }
          spawnParticle(pos.x, pos.y, 3, 0, 0);
        }
        //decrement glow timer
        if (glow > 0) {
          glow -= 1;
        }
        //colour corresponds to hp
        col = (int)(0.05*hp);
        //move enemy boss along figure 8 path
        pos.x = wave(400, 960, 200, timer);
        pos.y = wave(120, 480, 40, timer);

        pattern(); //call bullet pattern function
      }
      //detect enemy boss/player bullet collision
      for (PlayerBullet i : playerBullet) {
        if (pos.dist(i.pos) < 45 && !i.destroyed) { //if collided
          if (timer > 0) {
            //decrement health and set glow to last 2 frames
            hp -= 1;
            glow = 2;
          }
          //destroy collided player bullet
          i.destroyed = true;
          i.lifeTime = 0;
        }
      }
    }
  }
  
  //display function
  void display() {
    if (!dead) {
      if (timer <= 0) { //startup animation
        //draw enemy boss
        noStroke();
        fill(col, 100, 100, wave(wave(0, 480, 75, timer+120), 120, -15, frameCount));
        ellipse(pos.x, pos.y, wave(wave(0, 480, 100, timer+120), 120, 5, frameCount), wave(wave(0, 480, 100, timer+120), 120, 5, frameCount));
        fill(col, 25, 100, wave(0, 480, 50, timer+120));
        quad(pos.x-28.284, pos.y-28.284, rotateX(pos.x-80, pos.y-70, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-80, pos.y-70, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateX(pos.x-150, pos.y-80, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-150, pos.y-80, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateX(pos.x-100, pos.y, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-100, pos.y, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)));
        triangle(pos.x-40, pos.y, rotateX(pos.x-90, pos.y+20, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateY(pos.x-90, pos.y+20, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateX(pos.x-100, pos.y+80, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateY(pos.x-100, pos.y+80, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)));
        quad(pos.x+28.284, pos.y-28.284, rotateX(pos.x+80, pos.y-70, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+80, pos.y-70, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateX(pos.x+150, pos.y-80, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+150, pos.y-80, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateX(pos.x+100, pos.y, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+100, pos.y, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)));
        triangle(pos.x+40, pos.y, rotateX(pos.x+90, pos.y+20, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateY(pos.x+90, pos.y+20, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateX(pos.x+100, pos.y+80, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateY(pos.x+100, pos.y+80, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)));
        fill(0, 0, 100, wave(0, 480, 100, timer+120));
        ellipse(pos.x, pos.y, wave(0, 480, 80, timer+120), wave(0, 480, 80, timer+120));

        //draw enemy health bar (circular)
        noFill();
        strokeWeight(3);
        stroke(0, 0, 100);
        strokeCap(SQUARE);
        arc(pos.x, pos.y, 160, 160, -PI/2+(PI*(float)(3600-hp)/1800), 3*PI/2);
        strokeCap(ROUND);
      } else { //in battle
        //draw enemy boss
        noStroke();
        fill(col, 100, 100, wave(75, 120, -15, frameCount));
        ellipse(pos.x, pos.y, wave(100, 120, 5, frameCount), wave(100, 120, 5, frameCount));
        fill(col, 25, 100, 50);
        quad(pos.x-28.284, pos.y-28.284, rotateX(pos.x-80, pos.y-70, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-80, pos.y-70, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateX(pos.x-150, pos.y-80, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-150, pos.y-80, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateX(pos.x-100, pos.y, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)), rotateY(pos.x-100, pos.y, pos.x-28.284, pos.y-28.284, wave(0, 120, 15, frameCount)));
        triangle(pos.x-40, pos.y, rotateX(pos.x-90, pos.y+20, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateY(pos.x-90, pos.y+20, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateX(pos.x-100, pos.y+80, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)), rotateY(pos.x-100, pos.y+80, pos.x-40, pos.y, wave(0, 120, 15, frameCount-15)));
        quad(pos.x+28.284, pos.y-28.284, rotateX(pos.x+80, pos.y-70, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+80, pos.y-70, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateX(pos.x+150, pos.y-80, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+150, pos.y-80, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateX(pos.x+100, pos.y, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)), rotateY(pos.x+100, pos.y, pos.x+28.284, pos.y-28.284, wave(0, 120, -15, frameCount)));
        triangle(pos.x+40, pos.y, rotateX(pos.x+90, pos.y+20, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateY(pos.x+90, pos.y+20, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateX(pos.x+100, pos.y+80, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)), rotateY(pos.x+100, pos.y+80, pos.x+40, pos.y, wave(0, 120, -15, frameCount-15)));
        //yin-yang; will glow brighter when enemy hit
        fill(0, 0, 100);
        ellipse(pos.x, pos.y, 80, 80);
        fill(col, 50-glow*25, 100);
        arc(pos.x, pos.y, 80, 80, PI/2+(float)frameCount/60*PI, 3*PI/2+(float)frameCount/60*PI);
        ellipse(rotateX(pos.x, pos.y-20, pos.x, pos.y, degrees((float)frameCount/60*PI)), rotateY(pos.x, pos.y-20, pos.x, pos.y, degrees((float)frameCount/60*PI)), 40, 40);
        fill(0, 0, 100);
        ellipse(rotateX(pos.x, pos.y+20, pos.x, pos.y, degrees((float)frameCount/60*PI)), rotateY(pos.x, pos.y+20, pos.x, pos.y, degrees((float)frameCount/60*PI)), 40, 40);
        ellipse(rotateX(pos.x, pos.y-20, pos.x, pos.y, degrees((float)frameCount/60*PI)), rotateY(pos.x, pos.y-20, pos.x, pos.y, degrees((float)frameCount/60*PI)), 10, 10);
        fill(col, 50-glow*25, 100);
        ellipse(rotateX(pos.x, pos.y+20, pos.x, pos.y, degrees((float)frameCount/60*PI)), rotateY(pos.x, pos.y+20, pos.x, pos.y, degrees((float)frameCount/60*PI)), 10, 10);
        
        //draw enemy health bar (circular)
        noFill();
        strokeWeight(3);
        stroke(0, 0, 100);
        strokeCap(SQUARE);
        arc(pos.x, pos.y, 160, 160, -PI/2+(PI*(float)(3600-hp)/1800), 3*PI/2);
        strokeCap(ROUND);
      }
    }
  }
  
  //enemy bullet pattern function; bullets determined by current pattern
  void pattern() {
    if (pType == 0) { //rapidly fire 2 expanding rings of bullets with different speeds; one ring accelerates
      if (pTimer == 0) {
        calcNum1 = 90;
      }
      if (pTimer % 20 == 0) { //every 20 frames
        for (int i = 0; i < 10; i++) { //10 bullets per ring
          spawnBullet(pos.x, pos.y, 30, 0, calcNum1+i*36, 4, 3, 0, 0);
          spawnBullet(pos.x, pos.y, 20, 60, -calcNum1-18-i*36, 0, 6, 0.05, 0);
        }
        calcNum1 += 10;
      }
    }
    if (pType == 1) { //rapidly fire 2 expanding rings of bullets with different speeds from a point near the enemy
      if (pTimer % 20 == 0) { //every 20 frames
        float tempX = random(pos.x-100, pos.x+100);
        float tempY = random(pos.y-100, pos.y+100);
        calcNum1 = random(0, 30);
        for (int i = 0; i < 12; i++) { //12 bullets per ring
          spawnBullet(tempX, tempY, 20, 120, calcNum1+i*30, 4, 4, 0, 0);
          spawnBullet(tempX, tempY, 10, 180, calcNum1+i*30, 2, 2, 0, 0);
        }
      }
    }
    if (pType == 2) { //randomly fire lines of accelerating bullets that linger
      if (pTimer % 10 == 0) { //every 10 frames
        calcNum1 = random(angle(pos.x, pos.y, player.pos.x, player.pos.y)-120, angle(pos.x, pos.y, player.pos.x, player.pos.y)+120);
        for (int i = 1; i < 9; i++) { //9 bullets per line
          spawnBullet(pos.x, pos.y, 20, 240, calcNum1, 0, i, (float)i/30, 15);
        }
      }
    }
    if (pType == 3) { //fire 4 turning streams of bullets with varying spread, forming both spread-out and condensed formations
      if (pTimer == 0) {
        calcNum1 = 90;
        calcNum2 = 0;
      }
      if (pTimer % 2 == 0) { //every 2 frames
        for (int i = 0; i < 4; i++) { //4 bullets
          spawnBullet(pos.x, pos.y, 10, 300, calcNum1+i*90, 4, 4, 0, 0);
        }
        calcNum2 += 0.5;
        calcNum1 += calcNum2;
      }
    }
    if (pType == 4) { //fire 3 turning streams of bullets and less often, 4 giant slow bullets
      if (pTimer == 0) {
        calcNum1 = 90;
        calcNum2 = 90;
      }
      if (pTimer % 30 == 0) { //every 30 frames
        for (int i = 0; i < 4; i++) { //4 bullets
          spawnBullet(pos.x, pos.y, 90, 60, calcNum1+i*90, 0, 2, 0.01, 30);
        }
        calcNum1 += 25;
      }
      if (pTimer % 2 == 0) { //every 2 frames
        for (int i = 0; i < 3; i++) { //3 bullets
          spawnBullet(pos.x, pos.y, 10, 0, calcNum2+i*120, 3, 3, 0, 0);
        }
        calcNum2 -= 6;
      }
    }
    if (pType == 5) { //fire 10 condensed streams of bullets with breaks for player to pass through
      if (pTimer == 0) {
        calcNum1 = 90;
        calcNum2 = 90;
      }
      if (pTimer % 60 >= 0 && pTimer % 60 < 45 && pTimer % 2 == 0) { //every 2 frames, with a break every 60 frames lasting 15 frames
        for (int i = 0; i < 4; i++) { //4 bullets
          spawnBullet(pos.x, pos.y, 20, 120, calcNum1+i*90, 0, 8, 0.05, 0);
        }
      }
      if (pTimer % 2 == 0) { //
        calcNum1 -= 2;
      }
      if (pTimer % 40 >= 0 && pTimer % 40 < 20 && pTimer % 2 == 0) { //every 2 frames, with a break every 40 frames lasting 20 frames
        for (int i = 0; i < 6; i++) { //6 bullets
          spawnBullet(pos.x, pos.y, 10, 180, calcNum2+i*60, 4, 4, 0, 0);
        }
        calcNum2 += 1.5;
      }
    }
    if (pType == 6) { //fire 2 lines of accelerating bullets while firing a stream of bullets aimed at the player with varying velocity
      if (pTimer == 0) {
        calcNum1 = 0;
      }
      if (pTimer % 10 == 0) { //every 10 frames
        for (int i = 1; i < 5; i++) { //5 bullets per line
          spawnBullet(pos.x+100, pos.y, 10, 240, calcNum1, i, i, 0, 0);
          spawnBullet(pos.x-100, pos.y, 10, 240, 180-calcNum1, i, i, 0, 0);
        }
        calcNum1 += 7;
        calcNum2 = wave(3, 360, 2, pTimer);
        spawnBullet(pos.x, pos.y, 20, 300, angle(pos.x, pos.y, player.pos.x, player.pos.y), calcNum2, calcNum2, 0, 0); //1 aimed bullet
      }
    }
    if (pType == 7) { //rapidly and randomly fire randomly coloured bullets with random sizes in random directions at random speeds
      if (pTimer % 1 == 0) { //every frame
        float tempVel = random(1, 6);
        //2 bullets
        spawnBullet(pos.x, pos.y, 10*(int)random(1, 4), 60*(int)random(0, 6), random(0, 360), tempVel, tempVel, 0.1, 0);
        spawnBullet(pos.x, pos.y, 10*(int)random(1, 4), 60*(int)random(0, 6), random(0, 360), tempVel, tempVel, 0.1, 0);
      }
    }
    pTimer += 1;
  }
  
  //spawn an enemy bullet object with the following parameters: x position, y position, size, colour, direction, velocity, maximum velocity, acceleration, acceleration delay
  void spawnBullet(float x, float y, float s, int c, float d, float v, float mv, float a, int ad) {
    boolean recycle = false;
    //check for unused enemy bullet objects and recycle if available
    for (EnemyBullet i : enemyBullet) {
      if (i.unused) {
        i.recycle(x, y, s, c, d, v, mv, a, ad);
        recycle = true;
        break;
      }
    }
    //create new enemy bullet object if no available unused objects
    if (!recycle) {
      enemyBullet.add(new EnemyBullet(x, y, s, c, d, v, mv, a, ad));
    }
  }

  //function used for sine wave based animations; adjustable base value, period in frames, and amplitude
  float wave(float base, float period, float amp, int input) { 
    return(base+amp*sin(PI/(period/2)*input));
  }

  //function used for calculating a point's X position after rotating around another point a specified number of degrees
  float rotateX(float pointX, float pointY, float originX, float originY, float degrees) {
    return(originX+(pointX-originX)*cos(radians(degrees))-(pointY-originY)*sin(radians(degrees)));
  }

  //same as above but for Y position
  float rotateY(float pointX, float pointY, float originX, float originY, float degrees) {
    return(originY+(pointY-originY)*cos(radians(degrees))+(pointX-originX)*sin(radians(degrees)));
  }

  //function used to find the angle from one point to another point
  float angle(float x1, float y1, float x2, float y2) {
    float a = degrees(atan((y2-y1)/(x2-x1)));
    if (x1 <= x2) {
      return(a);
    } else {
      return(a+180);
    }
  }
}
/* EnemyBullet class
 * Enemy bullets; spawned by the enemy boss, will kill the player upon collision; highly customizable
 */

class EnemyBullet {

  PVector pos; //position vector
  PVector dir; //direction vector
  float bulletSize; //bullet size
  float vel; //velocity
  float mVel; //maximum velocity
  float accel; //acceleration
  int timer; //timer
  int delay; //delay timer
  int col; //colour
  boolean destroyed=false; //is bullet destroyed?
  boolean unused; //is bullet object unused?

  //initialize all variables; parameters: x position, y position, size, colour, direction, velocity, maximum velocity, acceleration, acceleration delay
  EnemyBullet(float x, float y, float s, int c, float d, float v, float mv, float a, int ad) {
    pos = new PVector(x, y);
    dir = PVector.fromAngle(radians(d));
    bulletSize = s;
    vel = v;
    mVel = mv;
    accel = a;
    delay = ad;
    col = c % 360;
    timer = -15;
    destroyed = false;
    unused = false;
  }

  //identical to constructor but for recycled objects
  void recycle(float x, float y, float s, int c, float d, float v, float mv, float a, int ad) {
    pos.set(x, y);
    dir = PVector.fromAngle(radians(d));
    bulletSize = s;
    vel = v;
    mVel = mv;
    accel = a;
    delay = ad;
    col = c % 360;
    timer = -15;
    destroyed = false;
    unused = false;
  }

  //update function
  void update() {
    if (!destroyed) {
      //startup animation
      if (timer < 0) {
        timer += 1;
      } else { //bullet active
        //delay timer ended
        if (delay == 0) {
          vel += accel; //accelerate
          if (vel > mVel) {
            vel = mVel;
          }
          dir.setMag(vel);
          pos.add(dir); //move bullet
        } else {
          delay -= 1; //decrement delay timer if still active
        }
        //check if off-screen
        if ((pos.x < -bulletSize/2 || pos.x > 800+bulletSize/2 || pos.y < -bulletSize/2 || pos.y > 600+bulletSize/2) && !destroyed) {
          destroyed = true;
          unused = true;
        }
      }
    } else { //bullet destroyed
      timer += 1;
      //delay timer ended
      if (delay == 0) {
        vel += accel; //accelerate
        if (vel > mVel) {
          vel = mVel;
        }
      } else {
        delay -= 1; //decrement delay timer if still active
      }
      dir.setMag(vel);
      pos.add(dir); //move bullet
      //set to unused if destroyed for 15 frames
      if (timer == 15) {
        unused = true;
      }
    }
  }
  
  //display function
  void display() {
    if (!destroyed) {
      //startup animation
      if (timer < 0) {
        noStroke();
        fill(0, 0, 100, 100+(float)timer*6.666);
        ellipse(pos.x, pos.y, bulletSize-(timer*bulletSize/15), bulletSize-(timer*bulletSize/15));
      } else {
        //draw bullet
        strokeWeight(1+bulletSize/10);
        stroke(col, 100, 100);
        fill(0, 0, 100);
        ellipse(pos.x, pos.y, bulletSize, bulletSize);
      }
    } else { //bullet destroyed
      //draw destroyed bullet
      strokeWeight((1+bulletSize/10)-timer*(1+bulletSize/10)/15);
      stroke(col, 100, 100, 100-timer/6.666);
      noFill();
      ellipse(pos.x, pos.y, bulletSize, bulletSize);
    }
  }
}
/* Particle class
 * Particles; used for visual effects, have no effect on gameplay
 */

class Particle {

  PVector pos; //position vector
  PVector dir; //direction vector
  int type; //particle type
  int timer; //timer
  float vel; //velocity
  boolean unused; //is particle object unused?

  //initialize all variables; parameters: x position, y position, type, direction, velocity
  Particle(float x, float y, int t, float d, float v) {
    pos = new PVector(x, y);
    vel = v;
    float angleInRadians=radians(d);
    dir = PVector.fromAngle(angleInRadians);
    dir.mult(vel);
    type = t;
    unused = false;
    timer = 0;
  }

  //identical to constructor but for recycled objects
  void recycle(float x, float y, int t, float d, float v) {
    pos.set(x, y);
    vel = v;
    float angleInRadians=radians(d);
    dir = PVector.fromAngle(angleInRadians);
    dir.mult(vel);
    type = t;
    unused = false;
    timer = 0;
  }

  //update function
  void update() {
    if (!unused) {
      pos.add(dir); //move particle
      if (type == 2) {
        timer += 1;
        if (timer == 100) { //disappears after 100 frames
          unused = true;
        }
      }
      if (type == 3) {
        timer += 1;
        if (timer == 30) { //disappears after 30 frames
          unused = true;
        }
      }
      //check if off-screen
      if (pos.x < -100 || pos.x > 900 || pos.y < -100 || pos.y > 700) {
        unused = true;
      }
    }
  }
  
  //display function
  void display() {
    if (!unused) {
      if (type == 1) { //star particle; small, translucent white circles moving downwards from top
        noStroke();
        fill(0, 0, 100, 25+vel*2);
        ellipse(pos.x, pos.y, vel, vel);
      }
      if (type == 2) { //explode particle; pulsating circles
        noStroke();
        fill(0, 0, 100, 100-timer);
        ellipse(pos.x, pos.y, wave((float)(100-timer), 10, (float)(100-timer)/2, timer), wave((float)(100-timer), 10, (float)(100-timer)/2, timer));
      }
      if (type == 3) { //blast particle; rapidly expanding circle that fades
        strokeWeight((float)(100-timer*3.333)/5);
        stroke(0, 0, 100);
        fill(0, 0, 100, 100-timer*3.333);
        ellipse(pos.x, pos.y, timer*40, timer*40);
      }
    }
  }

  //function used for sine wave based animations; adjustable base value, period in frames, and amplitude
  float wave(float base, float period, float amp, int input) { 
    return(base+amp*sin(PI/(period/2)*input));
  }
}
/* Player class
 * The player-controlled entity; can be moved and can fire player bullets
 */

class Player {

  PVector pos; //position vector
  PVector vel; //velocity vector
  int focusTimer; //focus timer for focus animation
  int fireTimer; //fire timer for firing intervals
  int timer; //timer
  int col; //colour
  int deaths; //number of deaths
  boolean right; //move right
  boolean left; //move left
  boolean down; //move down
  boolean up; //move up
  boolean focus; //focus mode
  boolean fire; //fire mode
  boolean dead=false; //is player dead?

  //initialize all variables
  Player() {
    pos = new PVector(400, 550);
    vel = new PVector(0, 0);
    focusTimer = 0;
    fireTimer = 0;
    timer = 60;
    col = 0;
    deaths = 0;
    focus = false;
    fire = false;
    dead = false;
  }

  //update function
  void update() {
    vel.set(0, 0);
    //change focus animation timer
    if (focusTimer > 0 && !focus) {
      focusTimer -= 1;
    } else if (focusTimer < 10 && focus) {
      focusTimer += 1;
    }
    //decrement firing timer
    if (fireTimer > 0) {
      fireTimer -= 1;
    }

    if (gameStart) {
      //detect player/enemy bullet collision
      for (EnemyBullet i : enemyBullet) {

        if (dist(player.pos.x, player.pos.y, i.pos.x, i.pos.y) < 5+i.bulletSize/2.-(1+i.bulletSize/10.)/2. && !dead && !i.destroyed) { //if collided


          dead = true; //die die die
          timer = -60;
          deaths += 1; //increment death counter
          noStroke();
          fill(0, 100, 100);
          rect(400, 300, 900, 700);
          for (int j = 0; j < 8; j++) { //explosion!
            spawnParticle(pos.x, pos.y, 2, j*45, 5);
          }
          spawnParticle(pos.x, pos.y, 3, 0, 0);
          pos = new PVector(400, 550); //reset position
        }
      }
      //detect player/enemy collision; same death sequence as above
      if (player.pos.dist(enemy.pos) < 45 && !dead && !enemy.dead && enemy.timer > 0) {
        dead = true;
        timer = -60;
        deaths += 1;
        noStroke();
        fill(0, 100, 100);
        rect(400, 300, 900, 700);
        for (int j = 0; j < 8; j++) {
          spawnParticle(pos.x, pos.y, 2, j*45, 5);
        }
        spawnParticle(pos.x, pos.y, 3, 0, 0);
        pos = new PVector(400, 550);
      }
    }

    if (!dead) {
      if (focus) { //focused
        //move 3 pixels in active direction
        if (right) { //move right
          vel.x += 3;
        }
        if (left) { //move left
          vel.x -= 3;
        }
        if (down) { //move down
          vel.y += 3;
        }
        if (up) { //move up
          vel.y -= 3;
        }
      } else { //unfocused
        //move 8 pixels in active direction
        if (right) { //move right
          vel.x += 8;
        }
        if (left) { //move left
          vel.x -= 8;
        }
        if (down) { //move down
          vel.y += 8;
        }
        if (up) { //move up
          vel.y -= 8;
        }
      }
      pos.add(vel); //move player
      //movement bounds; cannot pass screen edges
      if (pos.x < 20) {
        pos.x = 20;
      }
      if (pos.x > 780) {
        pos.x = 780;
      }
      if (pos.y < 20) {
        pos.y = 20;
      }
      if (pos.y > 580) {
        pos.y = 580;
      }

      //spawn 4 fast bullets moving upwards
      if (fire && fireTimer == 0) {
        spawnBullet(rotateX(pos.x, pos.y-50, pos.x, pos.y, -90+focusTimer*6), rotateY(pos.x, pos.y-50, pos.x, pos.y, -90+focusTimer*6), col);
        spawnBullet(rotateX(pos.x, pos.y-50, pos.x, pos.y, 90-focusTimer*6), rotateY(pos.x, pos.y-50, pos.x, pos.y, 90-focusTimer*6), col);
        spawnBullet(rotateX(pos.x, pos.y-50, pos.x, pos.y, -30+focusTimer*2), rotateY(pos.x, pos.y-50, pos.x, pos.y, -30+focusTimer*2), col);
        spawnBullet(rotateX(pos.x, pos.y-50, pos.x, pos.y, 30-focusTimer*2), rotateY(pos.x, pos.y-50, pos.x, pos.y, 30-focusTimer*2), col);
        fireTimer = 5; //may fire every 5 frames
      }
    } else {
      timer += 1;
      if (timer == 60) {
        //destroy all enemy bullets on respawn
        for (EnemyBullet i : enemyBullet) {
          i.destroyed = true;
        }
        dead = false;
        spawnParticle(pos.x, pos.y, 3, 0, 0);
      }
    }

    //change colour slightly every frame
    col += 1;
    if (col == 360) {
      col = 0;
    }
  }

  //display function
  void display() {
    if (timer >= 0) {
      noStroke();
      //draw bullet spawn points
      fill(col, 50-fireTimer*10, 100, wave(0, 240, 80+fireTimer*4, timer));
      ellipse(rotateX(pos.x, pos.y-50, pos.x, pos.y, -90+focusTimer*6), rotateY(pos.x, pos.y-50, pos.x, pos.y, -90+focusTimer*6), 10+fireTimer, 10+fireTimer);
      ellipse(rotateX(pos.x, pos.y-50, pos.x, pos.y, 90-focusTimer*6), rotateY(pos.x, pos.y-50, pos.x, pos.y, 90-focusTimer*6), 10+fireTimer, 10+fireTimer);
      ellipse(rotateX(pos.x, pos.y-50, pos.x, pos.y, -30+focusTimer*2), rotateY(pos.x, pos.y-50, pos.x, pos.y, -30+focusTimer*2), 10+fireTimer, 10+fireTimer);
      ellipse(rotateX(pos.x, pos.y-50, pos.x, pos.y, 30-focusTimer*2), rotateY(pos.x, pos.y-50, pos.x, pos.y, 30-focusTimer*2), 10+fireTimer, 10+fireTimer);
      //draw player
      fill(col, 100, 100, wave(wave(0, 240, 75, timer), 120, -15, frameCount));
      ellipse(pos.x, pos.y, wave(wave(0, 240, 35, timer), 120, 3, frameCount), wave(wave(0, 240, 35, timer), 120, 3, frameCount));
      fill(col, 25, 100, wave(0, 240, 50, timer));
      triangle(pos.x-14.142, pos.y-14.142, rotateX(pos.x-50, pos.y-30, pos.x-14.142, pos.y-14.142, wave(0, 120, 15, frameCount)), rotateY(pos.x-50, pos.y-30, pos.x-14.142, pos.y-14.142, wave(0, 120, 15, frameCount)), rotateX(pos.x-40, pos.y-10, pos.x-14.142, pos.y-14.142, wave(0, 120, 15, frameCount)), rotateY(pos.x-40, pos.y-10, pos.x-14.142, pos.y-14.142, wave(0, 120, 15, frameCount)));
      triangle(pos.x-20, pos.y, rotateX(pos.x-45, pos.y, pos.x-20, pos.y, wave(0, 120, 15, frameCount-10)), rotateY(pos.x-45, pos.y, pos.x-20, pos.y, wave(0, 120, 15, frameCount-10)), rotateX(pos.x-35, pos.y+10, pos.x-20, pos.y, wave(0, 120, 15, frameCount-10)), rotateY(pos.x-35, pos.y+10, pos.x-20, pos.y, wave(0, 120, 15, frameCount-10)));
      triangle(pos.x-14.142, pos.y+14.142, rotateX(pos.x-30, pos.y+20, pos.x-14.142, pos.y+14.142, wave(0, 120, 15, frameCount-20)), rotateY(pos.x-30, pos.y+20, pos.x-14.142, pos.y+14.142, wave(0, 120, 15, frameCount-20)), rotateX(pos.x-20, pos.y+25, pos.x-14.142, pos.y+14.142, wave(0, 120, 15, frameCount-20)), rotateY(pos.x-20, pos.y+25, pos.x-14.142, pos.y+14.142, wave(0, 120, 15, frameCount-20)));
      triangle(pos.x+14.142, pos.y-14.142, rotateX(pos.x+50, pos.y-30, pos.x+14.142, pos.y-14.142, wave(0, 120, -15, frameCount)), rotateY(pos.x+50, pos.y-30, pos.x+14.142, pos.y-14.142, wave(0, 120, -15, frameCount)), rotateX(pos.x+40, pos.y-10, pos.x+14.142, pos.y-14.142, wave(0, 120, -15, frameCount)), rotateY(pos.x+40, pos.y-10, pos.x+14.142, pos.y-14.142, wave(0, 120, -15, frameCount)));
      triangle(pos.x+20, pos.y, rotateX(pos.x+45, pos.y, pos.x+20, pos.y, wave(0, 120, -15, frameCount-10)), rotateY(pos.x+45, pos.y, pos.x+20, pos.y, wave(0, 120, -15, frameCount-10)), rotateX(pos.x+35, pos.y+10, pos.x+20, pos.y, wave(0, 120, -15, frameCount-10)), rotateY(pos.x+35, pos.y+10, pos.x+20, pos.y, wave(0, 120, -15, frameCount-10)));
      triangle(pos.x+14.142, pos.y+14.142, rotateX(pos.x+30, pos.y+20, pos.x+14.142, pos.y+14.142, wave(0, 120, -15, frameCount-20)), rotateY(pos.x+30, pos.y+20, pos.x+14.142, pos.y+14.142, wave(0, 120, -15, frameCount-20)), rotateX(pos.x+20, pos.y+25, pos.x+14.142, pos.y+14.142, wave(0, 120, -15, frameCount-20)), rotateY(pos.x+20, pos.y+25, pos.x+14.142, pos.y+14.142, wave(0, 120, -15, frameCount-20)));
      fill(0, 0, 100, wave(0, 240, 80, timer));
      ellipse(pos.x, pos.y, wave(0, 240, 30, timer), wave(0, 240, 30, timer));
      //draw hitbox
      fill(col, 100, 40, wave(0, 240, 100, timer));
      ellipse(pos.x, pos.y, 10, 10);
      fill(col, 100, 100, wave(0, 240, 100, timer));
      ellipse(pos.x, pos.y, 8, 8);
      fill(0, 0, 100, wave(0, 240, 100, timer));
      ellipse(pos.x, pos.y, wave(6, 120, 1, frameCount), wave(6, 120, 1, frameCount));
    }
  }

  //spawn a player bullet object with the following parameters: x position, y position, colour
  void spawnBullet(float x, float y, int c) {
    boolean recycle = false;
    //check for unused player bullet objects and recycle if available
    for (PlayerBullet i : playerBullet) {
      if (i.unused) {
        i.recycle(x, y, c);
        recycle = true;
        break;
      }
    }
    //create new player bullet object if no available unused objects
    if (!recycle) {
      playerBullet.add(new PlayerBullet(x, y, c));
    }
  }

  //function used for sine wave based animations; adjustable base value, period in frames, and amplitude
  float wave(float base, float period, float amp, int input) { 
    return(base+amp*sin(PI/(period/2)*input));
  }

  //function used for calculating a point's X position after rotating around another point a specified number of degrees
  float rotateX(float pointX, float pointY, float originX, float originY, float degrees) {
    return(originX+(pointX-originX)*cos(radians(degrees))-(pointY-originY)*sin(radians(degrees)));
  }

  //same as above but for Y position
  float rotateY(float pointX, float pointY, float originX, float originY, float degrees) {
    return(originY+(pointY-originY)*cos(radians(degrees))+(pointX-originX)*sin(radians(degrees)));
  }
}
/* PlayerBullet class
 * Player bullets; are fired from the player entity, and collide with the enemy boss to decrease its health
 */

class PlayerBullet {

  PVector pos; //position vector
  int lifeTime; //frames active
  int destroyTimer; //destroy timer
  int col; //colour
  boolean destroyed; //is bullet destroyed?
  boolean unused; //is bullet object unused?

  //initialize all variables; parameters: x position, y position, colour
  PlayerBullet(float x, float y, int c) {
    pos = new PVector();
    pos.set(x, y);
    lifeTime = 0;
    destroyTimer = 0;
    col = c;
    destroyed = false;
    unused = false;
  }

  //identical to constructor but for recycled objects
  void recycle(float x, float y, int c) {
    pos.set(x, y);
    lifeTime = 0;
    destroyTimer = 0;
    col = c;
    destroyed = false;
    unused = false;
  }

  //update function
  void update() {
    if (!destroyed) { //bullet active
      pos.y -= 30; //move up 30 pixels every frame
      lifeTime += 1;
      //check if offscreen
      if (pos.y < -100 && !destroyed) {
        destroyed = true;
        unused = true;
      }
    } else { //bullet destroyed
      destroyTimer += 1;
      //set to unused if destroyed for 15 frames
      if (destroyTimer == 15) {
        unused = true;
      }
    }
  }

  //display function
  void display() {
    if (!destroyed) { //bullet active
      //draw bullet; tail grows longer with time active
      noStroke();
      fill(col, 50, 100, 50);
      quad(pos.x, pos.y+5+lifeTime*4, pos.x-5, pos.y, pos.x, pos.y-5, pos.x+5, pos.y);
      fill(0, 0, 100);
      ellipse(pos.x, pos.y, 6, 6);
    } else { //bullet destroyed
      noStroke();
      fill(col, 50-(float)destroyTimer*3.333, 100, 50);
      //impact heightens and narrows
      quad(pos.x, pos.y+5, pos.x-(5-(float)destroyTimer*0.333), pos.y, pos.x, pos.y-5-destroyTimer*4, pos.x+(5-(float)destroyTimer*0.333), pos.y);
    }
  }
}