It’s been quite a journey. I put tremendous time and effort trying to perfect the details and coordinations of everything in the game. Hope you like it. At first I had a bit of trouble deciding which idea to stick to, but I am glad I ended up coming through with the “SPACE DEFENSE” game. As same as other final projects for this course, I brought Arduino and Processing together to make the game. When I first designed the development of the game, I broke it down into three main milestones with the respective order of priority:
- Processing game design
- Arduino architecture
- Serial connection
Accomplishing the first milestone, therefore, took up the most time. I initially made the game without the Arduino part and included the use of the keyboard to make the development more convenient and seamless. This is because it seemed rather easier for me to replace the keyboard use with the button use through serial communication than leaving numerous chunks blank. I still commented out not all but most of the parts necessary to run the keyboard to play the game. So find parts as shown below and simply uncomment them to use the keyboard instead! (apart from the uncommenting you need to do some tweaking into the code of course)
In terms of the code in Processing, I have in total of 4 classes (Player, Laser, Item, and Asteroid). I used polygons instead of images on asteroids for three reasons. First, I resized the images in Processing and that made the whole game slow. After talking with Aaron, I realized I could have cropped or resized images outside of Processing. However, there was another reason I preferred not to use an image for an asteroid. I thought it would be awkward to recreate the specific movement effect on an image, and the way my code generated different shapes, sizes, trajectories made the game more resourceful. Lastly, employing shapes instead of simply importing images seemed like a bigger challenge to me so I went with it. This is because these polygons have different shapes and vertices and so the collision effect on them required more thinking and effort than on uniform images.
The biggest challenge for me was controlling and coordinating the movement of different objects. This is because I had to manage a set of situations where different objects collide with one another. The next challenge was serial communication. I just had to spend some time figuring out the use of data transferred from Arduino within different classes. Also because when an error arose I had to check both Processing and Arduino, so that required extra investigation on a broader scope. After completing the general skeleton code of the game, I initially connected the maneuvering of the spaceship to the potentiometer. So depending on the degree of rotation, the spaceship would either move left or right. But after trying this for a while, I realized how inconvenient and clumsy it gets when using the potentiometer. The rotation of the button was simply too difficult to carry out that it was annoying to invest so much energy when playing the game. Below is a little sneak peak of how the potentiometer was set up in my Arduino board at first.
With this problem and user experience in mind, I decided to use push buttons. I also decided to get rid of UP & DOWN buttons because it made physiologically little sense that LEFT & RIGHT & UP & DOWN buttons are all aligned next to each other. So the spaceship was only allowed to move sideways and this was enough. I then added two push buttons – one for shooting bullets and the other for resetting the game. Below is how my Arduino looked like in the end:
In terms of the design, I tried to make it sort of retro. The song, the font, the color, the spaceship have all been designed in a way that it makes you feel nostalgic (at least for me). The sound track of the game is from a 1996 arcade game called “Out Run”, which is a car racing game but it also suits well in my game in my opinion. Here are the images and sounds I used.
ouch.mp3 (when the player collides with an asteroid)
laser.mp3 (when shooting bullets)
soundtrack.mp3 (sound track from Out Run 1996 arcade game)
life.mp3 (when the player gets an item and health goes up)
crash.mp3 (when an asteroid collides with the land)
Of course, a simple set of instructions are displayed before the game is initiated with the game title on top. I did not add any extra effects on this part because it is retro. To talk about how the game is run, the player can maneuver the starship left and right in order to shoot bullets at the falling “asteroids” that are polygons. I have limited the maneuver to left and right for the reason mentioned earlier. If a player collides with an asteroid he/she will lose 2 health points. If an asteroid collides with the earth, the health will also decrease by 10, as it is a fatal and collateral damage to the global community. If a player successfully hits an asteroid with a bullet, the asteroid will vanish and will be deducted from the total number of asteroids in the current level. There will be earth-looking items falling too and you should shoot at it to get 10+ on your health. Your level goes up when you eliminate all the asteroids in the level. Each level you go up, there will be 1 less earth item and 2 more asteroids. Without further ado, here’s a demo video.
// Main import ddf.minim.*; /************* Serial **************/ import processing.serial.*; Serial myPort; int xPos=0; int left, right, b, reset; /************* Serial **************/ Player player; Item item; PFont font, font2; PImage starship, earth, galaxy; ArrayList<Asteroid> asteroids; ArrayList <Laser> laser; ArrayList<Item> items; boolean instruction = true; boolean [] keys = new boolean[128]; String message; int[] land; int health, asteroidCount, itemCount; int asteroid_ = 0; int item_ = 0; int level = 1; int count = 1; //sound effect Minim minim; AudioPlayer soundtrack, sound, sound2, sound3, sound4; void setup() { size(480, 640); if (count == 1){ /************* Serial **************/ printArray(Serial.list()); String portname = Serial.list()[5]; println(portname); myPort = new Serial(this,portname,9600); myPort.clear(); myPort.bufferUntil('\n'); /***********************************/ } // Sound effect processing minim = new Minim(this); soundtrack = minim.loadFile("soundtrack.mp3"); sound = minim.loadFile("laser.mp3"); sound2 = minim.loadFile("crash.mp3"); sound3 = minim.loadFile("ouch.mp3"); sound4 = minim.loadFile("life.mp3"); sound2.setGain(-10); // Images starship = loadImage("starship.png"); earth = loadImage("earth.png"); // Font font2 = createFont("Cambria-bold",60); font = createFont("Phosphate-Inline",60); // asteroid_ == 0 means when you start the game // else meanse after Level 1 health = 100; if (asteroid_ == 0 && count == 1){ asteroidCount = 10; itemCount = 5; soundtrack.rewind(); soundtrack.play(); } else { asteroidCount = 10 + asteroid_; itemCount = 5 + item_;} /********** Initiating Objects ***********/ // Player player = new Player(); // Meteor asteroids = new ArrayList<Asteroid>(); for (int i=0; i < asteroidCount; i++) { asteroids.add(new Asteroid());} // Bullet laser = new ArrayList(); // Earth item items = new ArrayList<Item>(); for (int i=0; i < itemCount; i++) { items.add(new Item());} /*******************************************/ // Background land = new int [width/10+1]; for (int i = 0; i < land.length; i++) { land[i] = int(random(10));} message = "Level "; } void draw() { if (instruction){ galaxy = loadImage("instruction_background.jpg"); //Using the width and height of the photo for the screen size galaxy.resize(width,height); image(galaxy,0,0); textAlign(CENTER); textFont(font); fill(#FFFF00); text("SPACE DEFENSE",width/2,height/2-120); textFont(font2); textSize(20); fill(255); text("Protect the planet from falling asteroids!\n* * *\nPress YELLOW to move left\nPress BLUE to move right\nPress RED to shoot\nPress GREEN to restart\n* * *\nClick anywhere to proceed",width/2,height/2); return; } background(0); fill(255); noStroke(); ellipse(random(width), random(height-50), 3, 3); textSize(32); fill(255); text(message+level, width/2, 100); drawEarth(); for (int i= asteroids.size()-1; i >= 0; i--) { Asteroid p = asteroids.get(i); p.update(); p.display(); if (player.checkCollision(p)) { if (health > 0) { health -= 2; } sound3.rewind(); sound3.play(); } for (int j= laser.size()-1; j >= 0; j--) { Laser b = laser.get(j); // If an asteroid appears on screen and collides with a bullet if ((p.pos.y + p.r > 0) && (p.checkCollision(b))) { if (asteroids.size() > 0){ asteroids.remove(i); laser.remove(j); asteroidCount = asteroids.size(); } sound2.rewind(); sound2.play(); } } } for (int i= items.size()-1; i >= 0; i--) { Item it = items.get(i); it.update(); it.display(); for (int j= laser.size()-1; j >= 0; j--) { Laser b = laser.get(j); // If an item appears on screen and collides with a bullet if ((it.po.y + 25) > 0 && (it.checkCollision(b))) { if (items.size() > 0){ laser.remove(j); items.remove(i); if (health >= 100) { continue; } health += 10; sound4.rewind(); sound4.play(); } } } } if (asteroids.size() <= 0) { delay(1000); count = 0; asteroid_ += 2; item_ -= 1; setup(); level += 1; } player.display(); player.move(); for (int i= laser.size()-1; i >= 0; i--) { Laser b = laser.get(i); b.move(); b.display(); } if (b == 1) { Laser temp = new Laser(player.pos.x+16, player.posy); laser.add(temp); sound.rewind(); sound.play(); } if (reset == 0) { count = 0; asteroid_ = 0; item_= 0; level = 1; setup(); } textSize(32); if (health > 0) { text(health, width - 100, 50); } else { text(0, width - 100, 50); } if (health <= 0) { String over = "Game Over"; textSize(60); text(over, width/2, height/2); } textSize(32); fill(255, 0, 0); text(asteroidCount, 100, 50); } void asteroidShape(float a, float b, float r, int vertices) { float x = map(r, 0, 40, 50, 255); float degree = TWO_PI / vertices; color col = color(x/2, x/3, 100); beginShape(); for (float i = 0; i < TWO_PI; i += degree) { float sx = a + cos(i) * r; float sy = b + sin(i) * r; fill(col); noStroke(); //curveVertex(sx, sy); vertex(sx, sy); } endShape(CLOSE); } void drawEarth() { fill(250, 150, 0, 60); noStroke(); beginShape(); vertex(0, 640); for ( int i=0; i < land.length; i++) { vertex( i * 11, 640 - 40 - land[i] ); } vertex(480, 640); endShape(CLOSE); } void mousePressed() { if (instruction) { instruction = false; } //level = 1; } void keyPressed() { keys[keyCode] = true; } void keyReleased() { keys[keyCode] = false; } /************* Serial **************/ void serialEvent(Serial myPort){ String s = myPort.readStringUntil('\n'); s = trim(s); if (s != null){ int values[] = int(split(s,',')); if (values.length == 4){ left = int(values[0]); right = int(values[1]); b = int(values[2]); reset = int(values[3]); } } myPort.write('0'); // For key board use: //float xPos = myPort.read(); //player.pos.x = map(xPos, 0.0, 225.0, 0.0, 390.0) //myPort.write(0); } /************* Serial **************/
// Laser class class Laser{ PVector p; PVector a; PVector v; int w, h; Laser(float tx, float ty){ p = new PVector(tx, ty); a = new PVector(0,-0.2); v = new PVector(0, -2.5); } void display(){ noStroke(); fill(random(255), random(255), random(255)); rect(p.x, p.y, 5, 12); } void move(){ v.add(a); p.add(v); } }
// Item class class Item{ PVector po; PVector ac; PVector ve; Item(){ po = new PVector(random(width), random(-height, 0)); ac = new PVector(0, 0.001); ve = new PVector(0, 0); } void update(){ ve.add(ac); ve.limit(1); po.add(ve); // Readjust location if the player goes off screen on the side if((po.x < -25) || (po.x > width + 25)){ po.x = random(width); po.y = random(-height+100, 0); } if (po.y > height - 25){ po.y = random(-height+100,0); po.x = random(width); // meteor crash sound2.rewind(); sound2.play(); } } void display(){ pushMatrix(); translate(po.x, po.y); image(earth, 0, 0); popMatrix(); } boolean checkCollision(Laser b){ if((b.p.y + 6) <= po.y + 25 && b.p.x >= po.x - 10 && b.p.x <= po.x + 50){ sound.rewind(); sound.play(); return true; } else { return false; } } }
// Asteroid class class Asteroid{ PVector pos; PVector acc; PVector vel; int vertices; float r; color c; Asteroid(){ pos = new PVector(random(width), random(-height, 0)); acc = new PVector(random(-0.1, 0.1), random(0.1, 0.7)); vel = new PVector(0, 0); vertices = int(random(3, 15)); // what type of polygon? triangle ~ 15-gon r = random(10, 45); } void update(){ vel.add(acc); vel.limit(2); pos.add(vel); // Readjust location if the player goes off screen on the side if((pos.x + r < 0) || (pos.x - r > width)){ pos.x = random(0, width); pos.y = random(-height, 0); } if (pos.y + r > height){ if (health > 0){ health -= 10; // reduce health } pos.y = random(-height,0); pos.x = random(0, width); // meteor crash sound2.rewind(); sound2.play(); } } void display(){ pushMatrix(); translate(pos.x, pos.y); rotate(frameCount / 50.0); asteroidShape(0, 0, r, vertices); popMatrix(); } boolean checkCollision(Laser b){ if((b.p.y + 6) <= pos.y + r && b.p.x >= pos.x - r && b.p.x <= pos.x + r){ sound.rewind(); sound.play(); return true; } else { return false; } } }
// Player class class Player{ PVector acc; PVector vel; PVector pos; float posy = height-100; Player(){ pos = new PVector(width, height - 100); acc = new PVector(0.2, 0.2); vel = new PVector(0,0); } void display(){ image(starship, pos.x, posy); } void move(){ acc.normalize(); vel.mult(5); vel.limit(10); vel = vel.add(acc); if(left == 1){ pos.x -= vel.x;} if(right == 1){ pos.x += vel.x;} /* Un-comment for keyboard use if(keys[LEFT]){ pos.x -= vel.x;} if(keys[RIGHT]){ pos.x += vel.x;} if(keys[UP]){ pos.y -= vel.y;} if(keys[DOWN]){ pos.y += vel.y;} if(pos.y >= height - 50){ pos.y = height - 82.5;} _____________________________*/ if(pos.x < -19){ pos.x = width -19; } if(pos.x > width - 19){ pos.x = -19; } } boolean checkCollision(Asteroid p){ if (dist(pos.x, posy, p.pos.x, p.pos.y) <= p.r) { textSize(24); text("Collision!", width - 60, height - 12); return true; } else { return false; } } }
// Arduino int turnOn = 1; int prevButtonState = 0; void setup(){ Serial.begin(9600); Serial.println("0,0,0,0"); pinMode(2, INPUT); pinMode(3, INPUT); pinMode(4, INPUT); pinMode(5, INPUT); } void loop(){ if(Serial.available()>0){ char incoming = Serial.read(); int a1 = digitalRead(2); delay(1); int a2 = digitalRead(3); delay(1); int a3 = digitalRead(4); delay(1); int a4 = digitalRead(5); delay(1); Serial.print(a1); Serial.print(","); Serial.print(a2); Serial.print(","); if (a3 == 0 && prevButtonState == 1) { Serial.print(turnOn); } prevButtonState = a3; Serial.print(","); Serial.print(a4); Serial.println(); } }
Here’s also a zip file of my game. Hope you enjoy!