Our final project was inspired by a recent Korean zombie movie called “Train to Busan”. We localized the title and changed the storyline.
One day Ross receives a text message from Alex: Alex is stuck in a train and surrounded by zombies. The player, as Ross, has to pick up a gun and save his friend from dying from zombies.
Alex was mainly in charge of the game mechanics and Ross was mainly in charge of the art design and hardware.
The making of the game wasn’t easy. We had to keep multiple versions/drafts in the process of making this game because we want to work on the game at the same time on two different machines. After Alex finishes part of the code, he has to copy and paste onto the code on Ross’ computer to make sure it works. Sometimes it can be confusing as a single line of code missing would result in the whole program not working.
Thanks to Aaron’s help with the ps3eye library and the setup of the camera, the computer vision part was not very difficult. We just had to adopt the existing program to our game.
The hardware and communications between arduino and processing proved to be quite frustrating. Sometimes we had to unplug and plug the USB to make sure processing reads input properly. It was probably simply because of the faulty port on Ross’ computer. The button was broken quite a few times as players stepped on the wire or push too hard on the button. It would be ideal if we had longer wires or made it wireless.
The gameplay could be improved as well. If we had more time, we would have designed more levels with different kinds of zombies as well as a more proper ending to the story.
Here are the codes:
class Zombie{ boolean direction; int counterImg; float locx, locy; PImage img1; Zombie(float x, float y, boolean dir){ locx = x; locy = y; direction = dir; } void displayImages(){ this.update(); if(frameCount % 10 == 0){ counterImg++; } pushMatrix(); //if(!direction){ // translate(img1.width,0); //} if(gameover == false){ img1 = zombieImages1[counterImg % 4]; if(!direction){ scale(-1, 1); image(img1,-locx, locy); }else{ //scale(1, -1); image(img1, locx, locy); } } popMatrix(); } void update(){ if (frameCount % 10 == 0){ if(direction){ this.locx += 10; }else{ this.locx -= 10; } } if (frameCount % 120 == 0){ this.direction = !direction; } } }
class ZombieFast{ boolean direction; int counterImg; float locx, locy; PImage img1; ZombieFast(float x, float y, boolean dir){ locx = x; locy = y; direction = dir ; } void displayImages(){ this.update(); if(frameCount % 10 == 0){ counterImg++; } pushMatrix(); //if(!direction){ // translate(img1.width,0); //} if(gameover == false){ img1 = zombieFastImages[counterImg % 4]; if(!direction){ scale(-1, 1); image(img1,-locx, locy); }else{ //scale(1, -1); image(img1, locx, locy); } } popMatrix(); } void update(){ if (frameCount % 10 == 0){ if(direction){ this.locx += 40; }else{ this.locx -= 40; } } if(frameCounter % 25 == 0){ tempVar++; println(tempVar); if(tempVar % 4 == 0){ this.direction = !direction; } } } }
class ZombiePeek{ boolean direction; int counterImg; float w, h, locx, locy; PImage img1; ZombiePeek(float x, float y, float a, float b, boolean dir){ locx = x; locy = y; w = a; h = b; direction = dir; } void displayImages(){ this.update(); //img1 = loadImage("zombie4f1.png"); //image(img1, locx, locy, width/w, height/h); if(frameCount % 20 == 0){ counterImg++; } pushMatrix(); if(gameover == false){ img1 = zombiePeekImages[counterImg % 4]; image(img1, locx, locy, 128/w, 128/h); } popMatrix(); } void update(){ if(frameCounter % 25 == 0){ tempVar++; println(tempVar); if(tempVar % 4 == 0){ this.direction = !direction; } } if(direction){ this.w += 0.05; this.h += 0.05; }else{ this.w -= 0.05; this.h -= 0.05; } } }
import processing.video.*; import processing.sound.*; PFont title; PFont typewriter; PFont instructionFont; Serial myPort; int currentButtonState = 0; int prevButtonState = 0; int menu; int substr_cnt; int substr_cnt2; int substr_cnt3; boolean msg2Display = false; boolean msg3Display = false; boolean moviePlay = false; boolean SoundPlayed = false; Movie myMovie; SoundFile typingSound; SoundFile vibratingSound; SoundFile gunshot; SoundFile slimeSound; PImage dubai; PImage instruction; import java.util.ArrayList; import processing.serial.*; int alreadyAdded = 0; int numOfZombies = 4; int numOfJumpingZombies = 6; int numOfFastZombies = 4; int numOfPeekZombies = 1; //Zombie zombie = new Zombie(100, 100, 1, true); Bullet bullet = new Bullet(mouseX,mouseY); String[] explosionImageNames = {"explosionf1.png", "explosionf2.png", "explosionf3.png", "explosionf4.png", "explosionf5.png", "explosionf6.png", "explosionf7.png", "explosionf8.png"}; PImage[] explosionImages = new PImage[explosionImageNames.length]; String[] zombieImageNames = {"zombie1f1.png", "zombie1f2.png", "zombie1f3.png", "zombie1f4.png"}; PImage[] zombieImages1 = new PImage[zombieImageNames.length]; String[] zombieJumpingImageNames = {"zombie2f1.png", "zombie2f2.png", "zombie2f3.png", "zombie2f4.png"}; PImage[] zombieJumpingImages = new PImage[zombieJumpingImageNames.length]; String[] zombieFastImageNames = {"zombie3f1.png", "zombie3f2.png", "zombie3f3.png", "zombie3f4.png"}; PImage[] zombieFastImages = new PImage[zombieFastImageNames.length]; String[] zombiePeekImageNames = {"zombie4f1.png", "zombie4f2.png", "zombie4f3.png", "zombie4f4.png"}; PImage[] zombiePeekImages = new PImage[zombiePeekImageNames.length]; //Zombie[] zombies = new Zombie[numOfZombies]; ArrayList<Zombie> zombies = new ArrayList<Zombie>(4*numOfZombies); ArrayList<ZombieJump> zombieJump = new ArrayList<ZombieJump>(numOfJumpingZombies); ArrayList<ZombieFast> zombieFast = new ArrayList<ZombieFast>(numOfFastZombies); ArrayList<ZombiePeek> zombiePeek = new ArrayList<ZombiePeek>(numOfPeekZombies); PImage background; PImage cursor; boolean skipInstruction = false; import gab.opencv.*; import com.thomasdiewald.ps3eye.PS3EyeP5; import java.awt.Rectangle; PS3EyeP5 ps3eye; OpenCV opencv; int blobSizeThreshold = 1; void setup(){ fullScreen(); title = createFont("BankGothicBold.ttf",32); instructionFont = createFont("BrookeS8.ttf",24); textFont(title); typewriter = createFont("type.ttf",22); menu = 1; background(0, 0, 30); myMovie = new Movie(this, "intro.mov"); typingSound = new SoundFile(this, "typewriter.mp3"); vibratingSound = new SoundFile(this, "vibratingSound.mp3"); slimeSound = new SoundFile(this, "slimeSound.mp3"); gunshot = new SoundFile(this,"gunshot.wav"); dubai = loadImage("dubai.jpg"); instruction = loadImage("instruction.png"); printArray(Serial.list()); String portname = Serial.list()[4]; println(portname); myPort = new Serial(this,portname,9600); ps3eye = PS3EyeP5.getDevice(this); ps3eye.start(); opencv = new OpenCV(this, 640, 480); background = loadImage("trainbackground.png"); background(background); cursor = loadImage("cursor.png"); image(cursor, width/2, height/2); //preload bullet images for(int i = 0; i < explosionImageNames.length; i++){ String imageName = explosionImageNames[i]; explosionImages[i] = loadImage(imageName); } //preload zombie images for(int i = 0; i < zombieImageNames.length; i++){ String imageName = zombieImageNames[i]; zombieImages1[i] = loadImage(imageName); } for(int i = 0; i < zombieJumpingImageNames.length; i++){ String imageName = zombieJumpingImageNames[i]; zombieJumpingImages[i] = loadImage(imageName); } for(int i = 0; i < zombieFastImageNames.length; i++){ String imageName = zombieFastImageNames[i]; zombieFastImages[i] = loadImage(imageName); } for(int i = 0; i < zombiePeekImageNames.length; i++){ String imageName = zombiePeekImageNames[i]; zombiePeekImages[i] = loadImage(imageName); } } void draw(){ println(currentButtonState); if (menu == 1 && currentButtonState == 49 ) { menu = 2; substr_cnt = 0; gunshot.play(); } if (menu == 1) { //ellipse (random (width), random (height), random (4), random (4)); image(dubai,0,0, width, height); textAlign(CENTER); textSize(78); text ("Train to Dubai", width/2,height/2); textSize(32); text("Shoot to Continue", 350, 325); textSize(20); text("Powered by Ross Jiang and Alex Wuqi Zhang",1100,700); } if (menu == 2) { textFont(typewriter); background (0, 0, 30); fill(255, 255, 255); String msg1 = "Today, you find the streets of Dubai unusually quiet .."; String msg2 = "Suddenly, your phone starts vibrating .."; String msg3 = "It is from your stupid friend Alex .."; text (msg1.substring(0,constrain(int(substr_cnt/5),0,msg1.length())), 380, 160); if(!SoundPlayed){ typingSound.play(); SoundPlayed=true; } substr_cnt++; if( substr_cnt == 6 * msg1.length() ){ msg2Display = true; vibratingSound.play(); } if(msg2Display){ text (msg2.substring(0,constrain(int(substr_cnt2/5),0,msg2.length())), 295, 230); substr_cnt2++; } if( substr_cnt2 == 6 * msg2.length() ){ msg3Display = true; } if(msg3Display){ text (msg3.substring(0,constrain(int(substr_cnt3/5),0,msg3.length())), 275, 300); substr_cnt3++; } if( substr_cnt3 == 6 * msg3.length() ){ moviePlay = true; } if(moviePlay){ myMovie.play(); image(myMovie, 0, 0,width,height); } if(myMovie.time() == myMovie.duration()){ menu = 3; } } if(menu == 3){ textFont(instructionFont); image(instruction, 0,0, width,height); } if(menu ==3 && currentButtonState ==49){ gunshot.play(); menu =4; } if ( menu == 4 ) { textFont(typewriter); background(background); textSize(32); opencv.loadImage(ps3eye.getFrame()); opencv.threshold(100); noFill(); stroke(255, 0, 0); strokeWeight(3); for (Contour contour : opencv.findContours()) { //contour.draw(); Rectangle r = contour.getBoundingBox(); stroke(255, 0, 0); fill(255, 0, 0, 150); strokeWeight(2); println(r.getCenterX()); clickedX = (int)map((float)r.getCenterX(),640,0,width,0); clickedY = (int)map((float)r.getCenterY(),480,0, 0, height); ellipse(clickedX,clickedY,5,5); } if(currentButtonState == 49){ gunshot.play(); for(int i = 0; i < zombies.size(); i++){ if(collisionCheck(zombies.get(i))){ zombies.remove(i); kills++; slimeSound.play(); } } for(int i = 0; i < zombieJump.size(); i++){ if(collisionCheckJump(zombieJump.get(i))){ zombieJump.remove(i); kills++; slimeSound.play(); } } for(int i = 0; i < zombieFast.size(); i++){ if(collisionCheckFast(zombieFast.get(i))){ zombieFast.remove(i); kills++; slimeSound.play(); } } for(int i = 0; i < zombiePeek.size(); i++){ if(collisionCheckPeek(zombiePeek.get(i))){ zombiePeek.remove(i); kills++; slimeSound.play(); } } click = true; bullet = new Bullet(clickedX, clickedY); } //zombie wave 1 if(wave == 1 && alreadyAdded == 0){ for(int i = 0; i < numOfZombies; i++){ zombies.add(new Zombie((210*(i+1)), (650), true)); } zombieJump.add(new ZombieJump(300, 500)); zombieJump.add(new ZombieJump(500, 500)); zombieJump.add(new ZombieJump(700, 500)); zombieJump.add(new ZombieJump(300, 300)); zombieJump.add(new ZombieJump(500, 300)); zombieJump.add(new ZombieJump(700, 300)); zombiePeek.add(new ZombiePeek(1050, 200, 0.75, 0.75, true)); alreadyAdded++; } for(int i = 0; i < zombies.size(); i++){ zombies.get(i).displayImages(); } for(int i = 0; i < zombieJump.size(); i++){ zombieJump.get(i).displayImages(); } for(int i = 0; i < zombieFast.size(); i++){ zombieFast.get(i).displayImages(); } for(int i = 0; i < zombiePeek.size(); i++){ zombiePeek.get(i).displayImages(); } imageMode(CENTER); image(cursor, clickedX, clickedY, 320, 320); //Bullet bullet = new Bullet(-100,-100); if(frameCount % 4 == 0){ frameCounter++; } if(click == true){ bullet.displayBullet(); } deathCounter(zombies.size() + zombieJump.size() + zombieFast.size() + zombiePeek.size()); waveCounter(zombies.size() + zombieJump.size() + zombieFast.size() + zombiePeek.size()); killsCounter(); //zombie wave 2 if(wave == 2 && alreadyAdded == 1){ zombieFast.add(new ZombieFast(100, (100), true)); zombieFast.add(new ZombieFast(100, (300), true)); zombieFast.add(new ZombieFast(100, (500), true)); zombieFast.add(new ZombieFast(100, (700), true)); zombieFast.add(new ZombieFast(1220, (100), false)); zombieFast.add(new ZombieFast(1220, (300), false)); zombieFast.add(new ZombieFast(1220, (500), false)); zombieFast.add(new ZombieFast(1220, (700), false)); alreadyAdded++; } //zombie wave 3 if(wave == 3 && alreadyAdded == 2){ for(int i = 0; i < numOfZombies; i++){ zombies.add(new Zombie((210*(i+1)), (650), true)); } for(int i = 0; i < numOfZombies; i++){ zombies.add(new Zombie((210*(i+1)), (500), true)); } for(int i = 0; i < numOfZombies; i++){ zombies.add(new Zombie((210*(i+1)), (350), true)); } for(int i = 0; i < numOfZombies; i++){ zombies.add(new Zombie((210*(i+1)), (200), true)); } alreadyAdded++; } gameWin(); gameOver(); } } void serialEvent(Serial myPort){ currentButtonState = myPort.read(); } void movieEvent(Movie m) { m.read(); } // reference: //https://forum.processing.org/two/discussion/26427/how-to-incorporate-the-typewriter-effect-into-my-game
int clickedX, clickedY, kills, tempVar; int frameCounter = 1; boolean click = false; boolean gameover = false; int gameCounter = 6; int wave = 1; boolean exitGame = false; class Bullet{ int bulletX, bulletY; int bulletCounter = 0; PImage img; int time; Bullet(int x, int y){ bulletX = x; bulletY = y; } void displayBullet(){ if(click == true){ int frame = (frameCounter % 8) / 1 + 1; img = explosionImages[frame-1]; image(img, bulletX, bulletY); if(frame == 8){ click = false; } } } } void deathCounter(int remainingZombies){ if(gameover == false && frameCount % 60 == 0){ gameCounter--; } fill(255); textSize(60); text(gameCounter, width/2-20, 100); if(gameCounter == 0 && remainingZombies > 0){ gameover = true; //textSize(40); } } void killsCounter(){ fill(255); textSize(60); text("kills: " + kills, width - 300, 100); } void waveCounter(int remainingZombies){ if(gameCounter == 0 && remainingZombies == 0){ wave++; gameCounter = 6; } } void gameWin(){ if(wave == 4){ fill(255); textSize(80); text("YOU WIN!!", width/2-150, height/2-100); textSize(40); text("Shoot to Play Again", width/2-200, height/2 +200); if(clickedX > 0 && clickedX < width && clickedY > 0 && clickedY < height && click == true){ println("CLICKEDD"); //gameover = false; wave = 1; alreadyAdded = 0; kills = 0; frameCounter = 1; tempVar = 0; gameCounter = 6; } } } void gameOver(){ if(gameover == true){ if(zombies.size() != 0){ for(int i = 0; i < zombies.size(); i++){ zombies.remove(i); } } if(zombieJump.size() != 0){ for(int i = 0; i < zombieJump.size(); i++){ zombieJump.remove(i); } } if(zombieFast.size() != 0){ for(int i = 0; i < zombieFast.size(); i++){ zombieFast.remove(i); } } if(zombiePeek.size() != 0){ for(int i = 0; i < zombiePeek.size(); i++){ zombiePeek.remove(i); } } fill(255); textSize(80); text("game over", width/2-200, height/2-100); textSize(40); text("Shoot to Play Again", width/2-200, height/2 + 200); if(clickedX > 0 && clickedX < width && clickedY > 0 && clickedY < height && click == true){ println("CLICKEDD"); gameover = false; wave = 1; alreadyAdded = 0; kills = 0; frameCounter = 1; tempVar = 0; if(zombies.size() != 0){ for(int i = 0; i < zombies.size(); i++){ zombies.remove(i); } } if(zombieJump.size() != 0){ for(int i = 0; i < zombieJump.size(); i++){ zombieJump.remove(i); } } if(zombieFast.size() != 0){ for(int i = 0; i < zombieFast.size(); i++){ zombieFast.remove(i); } } if(zombiePeek.size() != 0){ for(int i = 0; i < zombiePeek.size(); i++){ zombiePeek.remove(i); } } gameCounter = 6; } } } boolean collisionCheck(Zombie z){ println("used"); if((z.locx - 56) < clickedX && (z.locx + 56) > clickedX && (z.locy - 56) < clickedY && (z.locy + 56) > clickedY){ //z = null; println("hit"); return true; } return false; } boolean collisionCheckJump(ZombieJump z){ println("used"); if((z.locx - 56) < clickedX && (z.locx + 56) > clickedX && (z.locy - 56) < clickedY && (z.locy + 56) > clickedY){ //z = null; println("hit"); return true; } return false; } boolean collisionCheckFast(ZombieFast z){ println("used"); if((z.locx - 56) < clickedX && (z.locx + 56) > clickedX && (z.locy - 56) < clickedY && (z.locy + 56) > clickedY){ //z = null; println("hit"); return true; } return false; } boolean collisionCheckPeek(ZombiePeek z){ println("used"); if((z.locx - 56) < clickedX && (z.locx + 56) > clickedX && (z.locy - 56) < clickedY && (z.locy + 56) > clickedY){ //z = null; println("hit"); return true; } return false; }