Kinetic Interfaces – Midterm (Moon) Andrew Huang

Author: Andrew Huang

Project name: Underfire! A bullet hell game

Professor: Moon

Date: Nov 3 2018

Goal: To Create a Upper body immersive bullet hell / dodging game.

Description:

After playing some Undertale and bullet hell games over the last couple of weeks, I was inspired to create a more kinetic and interactive version of the game. I used OpenCV to track the players face and used their X and Y positions in order to control the main character.  The player has to survive by dodging all of the homing fireballs that would come near the user. A camera feed at the bottom of the screen would let the user know their webcam is working and where it is relative to the game. When the user runs out of hp (they also periodically get an hp bump), the game displays GAME OVER, and the users score is displayed ( the number of total balls that the user has been hit by). The main character is a frisk sprite from Undertale and and the fireball is a fireball sprite from google.

 

Problems:

In the beginning, the fireballs would track very poorly because I was adjusting the xy position using linear velocity adjustments rather than using ease. By adding a dampening function, I was easily able to make sure that the velocity of the fireballs slowed down as they approached the player.

Even though some people complained that the bottom of my game had a camera picture and ruined the immersion of the game, I reasoned that it was better for the user to know where their face was relative to the game screen than the for the user to not know anything about the current state of the game.

Another problem I have is that the refresh rate of OpenCV is very poor and that the tracking isn’t good no matter what kind of program I run on my machine. This could be a limit on my machine, or is an inherent problem with OpenCV, further research would need to be conducted in order to address this.

Feedback & Future Development

Perhaps I can add better tracking / cameras. Some types of portable cameras can be integrated with OpenCV and a higher resolution experience using full body and limb tracking would result in a better playing experience. The user could probably use their hands to activate some sort of power-up which would either disperse all of the balls or they can use all of the balls as hp (similar to pacman). I was also thinking about adding a bezier track for a type of tracking that would be more realistic and fun to dodge than just using easing. I could also add a menu and better assets to polish the gameplay experience. Overall, I could see this project have promise, and would consider working on it more for the final.

// ANDREW
// KINETIC INTERFACES MIDTERM
// make tracking easier and add endgame
import gab.opencv.*;
import processing.video.*;
import java.awt.*;

Capture video;
OpenCV opencv;

ArrayList<Ball> balls = new ArrayList<Ball>();
PImage img, flameimg;
Health hp;
void setup() {
  size(500, 500
  );
  background(255);
  noStroke();
  smooth();
  noStroke();
  hp = new Health();
  fill(0);
  rect(mouseX, mouseY, 50, 7);
  img = loadImage("frisk.png");
  flameimg = loadImage("flame.png");

  video = new Capture(this, 640/4, 480/4);
  opencv = new OpenCV(this, 640/4, 480/4);
  opencv.loadCascade(OpenCV.CASCADE_FRONTALFACE);  

  video.start();
}
float inputx = 0, inputy = 0, facex = 0, facey = 0;
void draw() {
  opencv.loadImage(video);
  Rectangle[] faces = opencv.detect();
  background(#354c7c);
  text(frameRate, 20, 20);
  text("Balls added: " + balls.size(), 20, 40); 
  rectMode(CENTER);
  if (faces.length > 0) {
    inputx = map(faces[0].x, 0, 640/4, 500, -120);
    inputy = map(faces[0].y, 0, 480/4, 50, 400);
    facex = faces[0].width;
    facey = faces[0].height;
  }
  pushMatrix();
  image(img, inputx - img.width/20, inputy - img.height/20, img.width/10, img.height/10);
  stroke(100);
  noFill();
 // println(inputx, inputy);
  rect( inputx, inputy, img.width/10, img.height/10);
  popMatrix();
  hp.display();
  for (int i=0; i<balls.size(); i++) {
    balls.get(i).track(inputx, inputy);
    balls.get(i).bounce();
    balls.get(i).display();
    if (balls.get(i).removeBall(i)) {
      hp.hit();
    }
    //balls.get(i).age++;
  }
  if (frameCount % 100 == 0) {
    float r = random(0, 1);
    if (r > 0.5) {
      balls.add(new Ball(random(0, width), 10));
    } else {
      balls.add(new Ball(random(0, width), height));
    }
  }
  if (frameCount % 500 == 0 && hp.health < 100) {
    hp.health = ( hp.health + 20 ) > 100 ? 100 : hp.health + 20;
    for (int i = 0; i < balls.size(); i++) {
      balls.get(i).removeBall(i);
    }
  }
  if (hp.getHealth() <= 0) {
    textSize(40);
    fill(255, 0, 0);
    text("GAME OVER", width/3, height/2);
    text("SCORE: "  + hp.getHits(), width/3, height/2 + 50);
    noLoop();
    //delay(9999999);
  }
  pushMatrix();
  image(video, width/2 - 640/8, height-125);
  popMatrix();
}
void captureEvent(Capture c) {
  c.read();
}

void mouseClicked() {
  float r = random(0, 1);
  if (r > 0.5) {
    balls.add(new Ball(random(0, width), 10));
  } else {
    balls.add(new Ball(random(0, width), height));
  }
}

class Ball {
  float x, y, size;
  color clr;
  float xspeed, yspeed;
  float age = 0;

  Ball(float tempX, float tempY) {
    x = tempX;
    y = tempY;
    size = 20;
    clr = color(random(255), random(255), random(255));

    xspeed = random(-5, 5);
    yspeed = random(3, 5);
  }

  void display() {
    //fill(clr);
    tint(clr);
    image(flameimg, x,y,size*2,size*2);
    noTint();
    //ellipse(x, y, size, size);
  }

  void move() {
    x += xspeed;
    y += yspeed;
  }
  
  void track(float xx, float yy){
    float easing = 0.01;
     x += (xx - x) * easing;
     y += (yy - y) * easing;
  }

  void bounce() {

    if ((x>mouseX-25) && (x<mouseX+25) && (y > mouseY-3.5)) {
      yspeed = -yspeed;
    } 


    if (y < 0) {
      yspeed = -yspeed;
    } 
    if (x < 0) {
      xspeed = -xspeed;
    } else if (x > width) {
      xspeed = -xspeed;
    }
  }

  boolean removeBall(int i) { // hitdetection
    if (dist(inputx, inputy, x, y) < 50 || age > 400) {
      balls.remove(i);
      return true;
    }
    return false;
  }
}
class Health {
  float health = 100;
  float MAX_HEALTH = 100;
  float rectWidth = 200;
  float hits;
  
  Health(){
    return;
  }
  void display(){
    if (health < 25){
    fill(255, 0, 0);
    }  else if (health < 50) {
    fill(255, 200, 0);
    } else {
    fill(0, 255, 0);
    }
  
    noStroke();
    // Get fraction 0->1 and multiply it by width of bar
    float drawWidth = (health / MAX_HEALTH) * rectWidth;
    rectMode(CORNER);
    rect(200, 10, drawWidth, 10); 
    
    // Outline
    stroke(255);
    noFill();
    rect(200, 10, rectWidth, 10);
    stroke(0);
  }
  float getHealth(){
    return health;
  }
  float getHits(){
    return hits;
  }
  void hit(){
    health-= 5;
    hits++;
  }
  
}

Leave a Reply