Curse Breakers! Final :D

It’s done, everyone !!!! I DID IT !!! I made a video game and it works wonderfully!!!

Aaaaand here’s how I did it!!

You can take a look at my previous posts  documenting my progress:
https://intro.nyuadim.com/2021/11/27/toomies-final-project-progress-jjk-video-game/
https://intro.nyuadim.com/2021/12/08/curse-breakers-user-testing-progress/

I’m not entirely sure where to start with this, but I’ll do my best to walk you through it!

Let’s go through the game mechanics first:

The object of the game is to shoot the ghosts or ‘curses’ and collect 5 fingers within the time limit to win!

As (I think) I mentioned in my previous posts, I made classes for the fingers, the curses and the bullets.

Here are the classes:

The curses:

class Curse {
  float curx, cury;
  float spx, spy;
  float curw, curh;
  boolean disp = true;

  Curse(float _curx, float _cury, float _spx, float _spy) {
    curx = _curx;
    cury = _cury;

    spx = _spx;
    spy = _spy;

    curw = 260;
    curh = 160;
  }

  void display() {
    if (disp) {
      image(leftghost, curx, cury, curw, curh);
    }
    // displays the enemies on screen
  }

  void move() {
    curx += spx;

    if (curx>width+200) {
      curx=0-100;
    }
  }
}// ghost class end

The curses have random x and y positions and they move at random speeds. Once they collide with Yuji (the character) they disappear and the player loses a life. (You start with 5 lives, and you can also lose a life when you allow the ghosts to go offscreen).  The curses also disappear when you shoot them. This class has two functions: display and move. Display shows the image if the boolean (disp) is true, and move makes the ghosts move in the x direction and come back on the left side of the screen if they leave the screen on the right.

This is the bullets class: (I have 2 in my code but they are essentially the same so I’ll only copy one onto here):

class Bullet {

  float posx, posy;
  float speed;
  float a; //acceleration
  float r;
  boolean disp = true;
  color col;

  Bullet(float _posx, float _posy, float _speed, float _a, color _col) {
    posx = _posx;
    posy = _posy;

    col = _col;

    speed = _speed;
    a = _a;
    r =50;
  }

  void display() {
    fill(col);
    if (disp) {
      circle(posx, posy, r);
    }
  }

  void move() {
    posx -= speed;
    speed += a;
  }

  float distance(float x, float y) {
    return dist(posx, posy, x, y);
  }


  boolean done() {
    // removing bullets once they leave the screen
    if (posx < 0-r) {
      return true;
    } else {
      return false;
    }
  }
}

The bullets have the same functions as the curses class, with the addition of a color argument in the constructor (since there are two types of bullets in the game , one for the fingers and the other for the curses).

There is a distance variable which measures the distance of each bullet, since the bullets are in an array list.

The boolean ‘done’ removes the bullets once they’re offscreen to save memory!

The ‘Sukuna’Class refers to the fingers, here it is:

class Sukuna {
  float posx, posy;
  boolean disp = true;
  float sizew, sizeh;
  float fingertimer;


  Sukuna(float _posx, float _posy) {
    posx = _posx;
    posy = _posy;

    sizew = 140;
    sizeh = 160;
  }


  void display() {
    if (disp) {
      image(finger, posx, posy, sizew, sizeh);
    }
  }
}

It only has a display function, which is exactly the same as the other classes display functions, since the finger doesn’t move on screen once it appears.

As for the main code, I’ll go through it bit by bit:

Here is my beginning code, which is mostly filled with initializing variables:

import ddf.minim.*;
Minim minim;
AudioPlayer powerup;
AudioPlayer theme;
AudioPlayer gameover;
AudioPlayer life;
AudioPlayer woosh;
AudioPlayer winsound;

import processing.serial.*;
Serial myPort;

int score = 0;
float analog;
float starttime;
int fingercount = 0;
int lives = 5;
float prevbullettime=0;
int yujix;
PImage yuji, yujishoot, yujiidle, logo, battlebg, leftghost, finger, win, lose;
PFont gamefont;
int direction = 1;
int yujiR = 70;
int ghostR =60;
int bulletR =40;
int fingerR =70;
int speed = 3;
float acc = .2;
float gamemode = 1;
float countdown=0;
float respawntimer=0;
float fingertimer;
boolean timerActivated = false;
ArrayList<Bullet> bullets;
ArrayList<SBullet> sbullets;
Curse[] curses;
Sukuna[] sukuna;

Here is my setup code:

void setup () {
  fullScreen(); 
  battlebg = loadImage("battlebg.png");
  battlebg.resize(displayWidth, displayHeight);
  translate((displayWidth-1425)/2, (displayHeight-780)/2 +100);
  
  yujix = width-100;
  yujiidle = loadImage("yujiidle.png");
  yujishoot= loadImage("yujishoot.png");
  logo = loadImage("logo.png");
 leftghost = loadImage("leftghost.png");
  finger = loadImage("finger.png");
  win = loadImage("win.jpg");
  lose = loadImage("losepic.png");

  bullets = new ArrayList<Bullet>();
  sbullets = new ArrayList<SBullet>();

  minim = new Minim(this);
  powerup =minim.loadFile("powerup.wav");
  theme = minim.loadFile("jjkop.wav");
  theme.loop();
  gameover = minim.loadFile("gameover.wav");
  life = minim.loadFile("life.wav");
  woosh = minim.loadFile("swoosh.wav");
  winsound = minim.loadFile("winsound.wav");



  gamefont = createFont("/Users/fatimaaljneibi/Library/Fonts/Anime Inept.otf", 32);
  imageMode(CENTER);
  textAlign(CENTER);

  //ghosts:
  curses = new Curse[8] ;
  for (int i = 0; i<curses.length; i++) {
    curses[i]= new Curse(0, random(200, height-200), random(2, 8), random(-5, 5) );
  }

  sukuna = new Sukuna[1] ;
  for (int i = 0; i<sukuna.length; i++) {
    sukuna[i]= new Sukuna(random(300, width-300), random(200, height-200));
    sukuna[i].disp = false;
  }

  //serial setup stuff :
  printArray(Serial.list());
  String portname=Serial.list()[17];
  println(portname);
  myPort = new Serial(this, portname, 9600);
  myPort.clear();
  myPort.bufferUntil('\n');
}

I set the game to be in fullscreen,  loaded all my images and fonts, and called my classes ( there are 8 ghosts, 1 finger, and array lists for the bullets).  I ended up using Minim instead of the processing sound library, because I found it easier to use for my sounds to play once.

My void draw code has all 4 of my game screens, here’s the code for the starting screen:

// starting screen:
 if (gamemode == 1) {
   if (!theme.isPlaying()) {
     theme.rewind();
     theme.loop();
   }
   background(#728DC6);
   textFont(gamefont, 150);
   image(logo, width/2, height/2-270, 400, 200);
   fill(#1D194D);
   text("CURSE BREAKERS!", width/2, height/2-100);
   ////title ^^ instructions below
   textFont(gamefont, 40);
   text("-press the red button to start-", width/2, height/2);
   //personal note: might turn the controller instructions into an image
   text("help yuji kill the curses and collect sukuna's fingers!", width/2, height/2+60);
   text("use the slider to move yuji, the yellow button to attack", width/2, height/2+100);
   text("and the blue button to collect the fingers!", width/2, height/2+140);
   text("collect 5 fingers within the time limit to win!", width/2, height/2+190);
 } //start screen end

It’s mostly just instructional text and starting the theme song loop.  Here’s a screenshot of what it looks like in the game!

Here’s the main game code:

 //game screen:
  if (gamemode == 2) {
    background(battlebg);
    if (!theme.isPlaying()) {
      theme.loop();
    }
    image(yuji, yujix, analog, 140, 200);

    // respawn curses:
    if (timerActivated==true) {
      if (millis()>respawntimer ) {
        timerActivated = false;
        for (int i=0; i<curses.length; i++) {
          if (curses[i].disp == false) {
            curses[i].curx = random(-100, -10) ;
            curses[i].cury = random(200, height-200);
            curses[i].disp=true;
          }
        }
      }
    }
    if (timerActivated == false && gamemode == 2) {
      respawntimer = millis()+3000; // the curses reappear on screen
      timerActivated = true;
    }

    //collision detection:

    for (int i=0; i<curses.length; i++) {

      float d1 = dist(yujix, analog, curses[i].curx, curses[i].cury);
      if ( d1 <= ghostR + yujiR && curses[i].disp==true) {
        lives-= 1;

        life.rewind();
        life.play();

        curses[i].disp=false;  //subtracts a life from yuji when he gets in contact with a ghost and makes it disappear on contact
      }
      for (int j = bullets.size()-1; j >= 0; j--) {
        float d2 = bullets.get(j).distance(curses[i].curx, curses[i].cury);
        if ( d2 <= ghostR + bulletR && curses[i].disp==true) {
          curses[i].disp=false;
          bullets.get(j).disp=false;
          bullets.remove(j);
          score += 10;
          woosh.rewind();
          woosh.play();
        }
      }
    }

    //shooting the fingers:
    for (int i=0; i<sukuna.length; i++) {
      for (int j = sbullets.size()-1; j >= 0; j--) {
        float d2 = sbullets.get(j).distance(sukuna[i].posx, sukuna[i].posy);
        if ( d2 <= fingerR + bulletR && sukuna[i].disp==true) {
          sukuna[i].disp=false;
          sbullets.get(j).disp=false;
          sbullets.remove(j);
          fingercount += 1;
          powerup.rewind();
          powerup.play();
        }
      }
    }

    //scoreboard + timer:
    pushStyle();
    textAlign(LEFT);
    textFont(gamefont, 30);
    fill(255);
    text(" Fingers collected: " + fingercount, 0, 30);
    text(" Score: " + score, 0, 60);
    text(" Lives left: " + lives, 0, 90);
    countdown = 60 -int((millis()-starttime)/1000);
    text(" Time left: " + countdown, 0, 120);
    popStyle();

    // calling class functions for the curses + bullets:
    for (int i=0; i<curses.length; i++) {
      curses[i]. display();
      curses[i]. move();
    }

    for (int i = bullets.size()-1; i >= 0; i--) {
      Bullet bullet = bullets.get(i);
      bullet.display();
      bullet.move();
      if (bullet.done()) {
        bullets.remove(i);
        // removes the bullets from the arraylist once they leave the screen to save memory
      }
    }
    for (int i = sbullets.size()-1; i >= 0; i--) {
      SBullet sbullet = sbullets.get(i);
      sbullet.display();
      sbullet.move();
      if (sbullet.done()) {
        sbullets.remove(i);
        // removes the bullets from the arraylist once they leave the screen to save memory
      }
    }
//win condition:
    if (countdown<=0 && fingercount <5) {
      gamemode = 4;
      theme.pause();
      winsound.rewind();
      for (int i = sbullets.size()-1; i >= 0; i--) {
        sbullets.remove(i);
      }
      for (int i = bullets.size()-1; i >= 0; i--) {
        bullets.remove(i);
      }
      //lose condition 2: (if time runs out)
    } else if (countdown>=0 && fingercount == 5) {
      gamemode =3;
      theme.pause();
      for (int i = sbullets.size()-1; i >= 0; i--) {
        sbullets.remove(i);
      }
      for (int i = bullets.size()-1; i >= 0; i--) {
        bullets.remove(i);
      }
    }

    //finger mechanics:

    for (int i=0; i<sukuna.length; i++) {
      sukuna[i].display();
    }
    for (int i=0; i<sukuna.length; i++) {
      if (score % 100 ==0 && score >0) {
        fingertimer =3000+millis();
        sukuna[i].disp = true;
      }
      if (millis() > fingertimer) {
        sukuna[i].disp = false;
        fingertimer =0;
      }
    }
    // if the curses leave the screen without being shot, you lose 1 life
    for (int i=0; i<curses.length; i++) {
      if (curses[i].curx > width && curses[i].disp == true) {
        lives -= 1;
        life.rewind();
        life.play();
        curses[i].disp = false;
      }
    }

    // you lose if your lives are at 0
    if (lives <= 0) {
      gameover.rewind();
      gamemode = 4;
    }
  } // game screen end

There’s quite a lot going on here. The character is drawn here, with his Y position being the slider (analog input). We’ll go over the serial communication in a bit!

I made a respawn timer for the curses, basically the logic here is that if the curses disp = false ( i.e. they’re not visible on screen) and the current time > respawn timer, the curses will respawn at a new random height on screen, with the width being offscreen on the left, so they move forward to the right.

After that, there’s the code for collision detection. The logic here is that if the distance between the two objects is less than the sum of their radii (which are variables that I set at the very beginning of my code) then something happens! The code for what that ‘something’ is differs depending on what two objects they are. There’s collision detection for the bullets and the curses, and for the curses and yuji, and for the bullets and the fingers.

Then, there’s code for the scoreboard and the timer. You can see the scoreboard in the top left of this screenshot:
It has a finger counter(increases whenever you shoot a finger), a lives counter(starts at 5, decreases by one if Yuji gets hit by a ghost, or lets one get away), a score(based on the ghosts that you hit) and a timer! The timer is 60 seconds from the start of the game.

Following this code is the code to call class functions for the curses and bullets.

After this comes the code for win conditions. You can only win after collecting 5 of Sukuna’s fingers. A finger appears after you hit a score that’s divisible by 100, and it disappears after a few seconds if you don’t collect it in time! Once you win, you go to the winning screen!

To lose the game, you either lose all of your lives or you don’t collect 5 fingers by the time the timer ends! Once you lose, you are taken to the losing screen ><.

Then, we have the code for the finger mechanics! It’s pretty simple, I called the display function for the finger, and the disp boolean is initially false. It becomes true once you hit a score divisible by zero, and disappears shortly after.

The code following that is to decrease the lives variable by 1 every time a ghost leaves the screen.

Heres the winning and losing screen’s code, accompanied by screenshots of both in-game:

 // winning & losing screens:

  //win screen, score/time , sound effect:
  if (gamemode == 3) {
    theme.pause();
    winsound.play();
    background(#1D194D);
    textFont(gamefont, 100);
    fill(#728DC6);
    text("YOU WIN!", width/2, height-250);
    //title ^^   info below
    textFont(gamefont, 50);
    text("score:"+score, width/2, height-200);
    text("time left:"+countdown, width/2, height-150);
    text("fingers collected:"+ fingercount, width/2, height-100);

    textFont(gamefont, 40);
    text("-press the red button to try again!-", width/2, height-50);
    image(win, width/2, 250, 700, 400);
  }//win screen end

  if (gamemode == 4) {
    //losing screen, click to try again, sound effect
    theme.pause();
    gameover.play();

    background(#1D194D);
    textFont(gamefont, 100);
    fill(#728DC6);
    text("YOU LOSE:(", width/2, height-200);
    //title ^^   info below
    textFont(gamefont, 50);
    text("score:"+score, width/2, height-150);
    text("fingers collected:"+ fingercount, width/2, height-100);
    textFont(gamefont, 40);
    text("-press the red button to try again!-", width/2, height-50);
    image(lose, width/2, 300, 700, 400);
  }//losing screen end
}//draw bracket


Also, I’ve added various sound effects to the game, I’ll list them here:
A ‘whoosh’ sound when you shoot the curses
A powerup sound effect when you collect a finger
The theme song (which plays only during the start screen and the game screen)
A victory sound plays in the win screen
A game over sound plays on the lose screen

Here is the serial communications code:

Processing:

void serialEvent(Serial myPort) {
  String s=myPort.readStringUntil('\n');
  s=trim(s);
  //int values = parseInt(s);
  //boolean shoottest = false;
  if (s!=null) {
    //println(s);
    int values[]=int(split(s, ','));

    // note: values: 0 = slider, 1= yellow, 2= blue, 3= red

    //slider:
    analog =map(values[0], 0, 1023, 100, height-70);
    yuji = yujiidle;
    //attacking (yellow) button:
    if (values[2] == 1 && gamemode == 2) {
      // not working:
      yuji = yujishoot;
      color Ccircle = color(#98F7FF, 150);
      noStroke();
      if (millis() - prevbullettime > 400) {
        bullets.add(new Bullet(width-150, analog+20, speed, acc, Ccircle));
        prevbullettime= millis();
      }
    }

    // start button:
    if (values[3] == 1 && gamemode == 1) {
      starttime=millis();
      fingercount = 0;
      score = 0;
      gamemode = 2;
      //replay : (red button)
    } else if (values[3] == 1 && gamemode == 3 || values[3] == 1 && gamemode ==4) {
      fingercount = 0;
      lives = 5;
      gamemode =1;
      delay(1000); // so you dont skip to the start screen too quickly after pressing the button
      for (int i=0; i<curses.length; i++) {
        curses[i].curx = random(-100, -10);
      }
    } //fingers (blue)
    if (values[1] == 1 && gamemode == 2) {
      yuji = yujishoot;
      color Scircle = color(#FF8B8B, 150);
      noStroke();
      if (millis() - prevbullettime > 400) {
        sbullets.add(new SBullet(width-150, analog+20, speed, acc, Scircle ));
        prevbullettime= millis();
      }
    }
    
  }

  myPort.write(int(0)+"\n");
}

Arduino:

int bluepin = 5;
int yellowpin = 3;
int redpin = 6;
int sliderpin = A5;

void setup() {
  pinMode(bluepin , INPUT);
  pinMode(yellowpin , INPUT);
  pinMode(redpin , INPUT);
  pinMode(sliderpin , INPUT);
  Serial.begin(9600);
  Serial.println("0,0,0,0");
}

void loop() {
  int bluebutton = digitalRead(bluepin);
  int yellowbutton = digitalRead(yellowpin);
  int redbutton = digitalRead(redpin);
  int slider = analogRead(sliderpin);

//checking if everything is connected properly:
//comment this out to run processing
//    Serial.print(bluebutton);
//    Serial.print(yellowbutton);
//    Serial.print(redbutton);
//    Serial.println(slider);

  while (Serial.available()) {
    int fromP = Serial.parseInt();

    if (Serial.read() == '\n') {
      Serial.print(slider);
      Serial.print(",");
      Serial.print(yellowbutton);
      Serial.print(",");
      Serial.print(bluebutton);
      Serial.print(",");
      Serial.println(redbutton);
    }

  }


}

I have 4 parts connected to my circuit: 3 buttons and a slider. I built a controller box out of foam to safely conceal my circuit while also making it easy for players to use! Here is my process making that:

It looked like this at first, but I spray painted it black afterwards and added a 3rd button! (the red one, to start playing the game. initially, you would use either the blue or yellow to start playing the game and switch between screens, but that wasn’t giving me great results, so I decided to add a third button)

the button in this image was tiny because I couldn’t find one that looks like my other two, thankfully I asked on discord and got one!! (thank you bhavicka!!)

Here’s the final product!

Aaaaaand that’s it! TA-DA!! I’m very proud of what I’ve learned and achieved in this course, I had lots of fun!! Thank you 🙂

This is a gameplay video of the final game!!
https://youtube.com/shorts/cLE6tk_7XO4?feature=share

 

Leave a Reply