Catch

When I presented my butterfly animation project in class, Aaron brought up the possibility of using such animations for a game, such that the character(s) moves in a realistic manner. Later that day, Nahil suggested I could use sprite sheets (which our dear W3 Schools defines as “a collection of images” that are meant to be “put into a single image”) the next time I wanted to try an animation, given that they can be easily found as png files with transparent backgrounds online and usually include enough frames to simulate sufficiently smooth movement.

I thought I could follow their advice for this assignment, given that it asked us to use one of our previous Processing projects and implement serial communication with Arduino. I used the butterfly animation code as the skeleton for this new program, but added several new features.

The game imitates “Flutter” in its simple name (#actionverbs, #yay!) and the random character and background selection. The user can either control a zebra, a tiger, or a reindeer.

Zebra sprite sheet

The user has 30 seconds to catch as many yellow circles as they can (there is only one circle on screen at a time; if the user catches it, a new one appears). These circles get smaller as time runs out, thus decreasing the circles’ target radius (the area where the user “touches” the circle) and making the game a bit more difficult.

The animal’s speed can be increased or decreased with the use of a potentiometer and its movement is controlled with four pushbuttons (if no button is pushed, the animal doesn’t move).

Circled in light blue: four pushbuttons that determine direction (left, down, up, right); in pink: potentiometer that determines speed; in yellow: red LED that turns on when the user has 10 seconds or less before the game ends

There are always three numbers on screen: the user’s score, the high score, and the timer. The user’s score keeps track of how many circles the animal has catched. The high score displays the all-time high score. It pulls it from a text file at the start of the game, and if the user surpasses this number, the new high score overrides the previous one in the text file. If this happens, the high score turns yellow and displays the user’s points. Lastly, the timer decreases from 30 to 0, and turns red at the 10-second mark. The LED on the breadboard turns on simultaneously, to warn the user about their limited time if they happen to be watching the board. I included this feature because I looked at the board a lot as I was testing the game. I think this is the problem of using pushbuttons as controllers: it’s very easy to think  you’re pressing a specific one when you’re actually pressing another.

Initially, I wanted to use a joystick to control movement, but the piece I found had surprisingly fragile parts and they came off fairly easily… so that worked for a while, and then everything fell apart (quite literally). I also thought of incorporating a yellow LED that would turn on when the user established a new high score and a piezo buzzer that would accompany each light with a different tune. However, I only had access to my small Sparkfun kit breadboard when I thought of these options, so these many components and their respective wires and resistors wouldn’t fit in it.

In terms of the animation itself, the main thing I want to fix is the position of the animal when it isn’t moving. Right now, the animal stops and faces forward, no matter what it’s previous movement was. I think this can easily be changed by having a variable that saves the movement in each draw loop (up or down or left or right), and if the animal stops, it retains the last movement that was saved, which would determine which images from the sprite sheet to use.

The following video shows three different rounds of the game that I recorded and put together (as well as its title screen):

(Music by YouTube user rakohus)

This is the code from the Processing sketch:

import processing.serial.*;
Serial myPort;

PImage bg;
PImage f1, f2, f3, f4;
PImage l1, l2, l3, l4;
PImage r1, r2, r3, r4;
PImage b1, b2, b3, b4;

PFont font;

Animal user;

int userChoice;
int speed;
int xDirection = 0;
int yDirection = 0;
int counter;
int time;
int points;

boolean object;
boolean ellipse;
boolean start;
int gameOver;

int highScore;

int objectX;
int objectY;
float radius;

void loadImages(int animal){
 if(animal == 1){ //tiger
 bg = loadImage("bg1.png");
 f1 = loadImage("tiger_front1.png");
 f2 = loadImage("tiger_front2.png");
 f3 = loadImage("tiger_front3.png");
 f4 = loadImage("tiger_front4.png");
 l1 = loadImage("tiger_left1.png");
 l2 = loadImage("tiger_left2.png");
 l3 = loadImage("tiger_left3.png");
 l4 = loadImage("tiger_left4.png");
 r1 = loadImage("tiger_right1.png");
 r2 = loadImage("tiger_right2.png");
 r3 = loadImage("tiger_right3.png");
 r4 = loadImage("tiger_right4.png");
 b1 = loadImage("tiger_back1.png");
 b2 = loadImage("tiger_back2.png");
 b3 = loadImage("tiger_back3.png");
 b4 = loadImage("tiger_back4.png");
 }
 else if(animal == 2){ //deer
 bg = loadImage("bg2.png");
 f1 = loadImage("deer_front1.png");
 f2 = loadImage("deer_front2.png");
 f3 = loadImage("deer_front3.png");
 f4 = loadImage("deer_front4.png");
 l1 = loadImage("deer_left1.png");
 l2 = loadImage("deer_left2.png");
 l3 = loadImage("deer_left3.png");
 l4 = loadImage("deer_left4.png");
 r1 = loadImage("deer_right1.png");
 r2 = loadImage("deer_right2.png");
 r3 = loadImage("deer_right3.png");
 r4 = loadImage("deer_right4.png");
 b1 = loadImage("deer_back1.png");
 b2 = loadImage("deer_back2.png");
 b3 = loadImage("deer_back3.png");
 b4 = loadImage("deer_back4.png");
 }
 else if(animal == 3){ //zebra
 bg = loadImage("bg3.png");
 f1 = loadImage("zebra_front1.png");
 f2 = loadImage("zebra_front2.png");
 f3 = loadImage("zebra_front3.png");
 f4 = loadImage("zebra_front4.png");
 l1 = loadImage("zebra_left1.png");
 l2 = loadImage("zebra_left2.png");
 l3 = loadImage("zebra_left3.png");
 l4 = loadImage("zebra_left4.png");
 r1 = loadImage("zebra_right1.png");
 r2 = loadImage("zebra_right2.png");
 r3 = loadImage("zebra_right3.png");
 r4 = loadImage("zebra_right4.png");
 b1 = loadImage("zebra_back1.png");
 b2 = loadImage("zebra_back2.png");
 b3 = loadImage("zebra_back3.png");
 b4 = loadImage("zebra_back4.png");
 };
};

void reset(){
 xDirection = 0;
 yDirection = 0;
 userChoice = int(random(1, 4));
 loadImages(userChoice);
 user = new Animal();
 speed = 15;
 time = 0;
 points = 0;
 counter = 0;
 object = false;
 start = false;
 gameOver = 0;
 ellipse = false;
 radius = 20;
};

void setup(){
 printArray(Serial.list());
 String portName = Serial.list()[2];
 myPort = new Serial (this, portName, 9600);
 myPort.clear();
 myPort.bufferUntil('\n');
 
 int [] txtFile = int(loadStrings("highscore.txt"));
 highScore = txtFile[0];
 
 size(600, 448);
 font = createFont("ARCADECLASSIC.TTF", 50);
 reset();
}

void draw(){
 textFont(font);
 fill(0);
 if(start == false){
 background(255);
 time = 0;
 textAlign(CENTER);
 text("Catch", width/2, height/2 - 60);
 textSize(20);
 text("POT is speed", width/2, height/2 + 10);
 fill(255, 0, 0);
 text("RED is left", width/2, height/2 + 30);
 fill(255, 211, 25);
 text("YELLOW is down", width/2, height/2 + 50);
 fill(34, 139, 34);
 text("GREEN is up", width/2, height/2 + 70);
 fill(0, 0, 255);
 text("BLUE is right", width/2, height/2 + 90);
 fill(0);
 text("Press any button to start", width/2, height/2 + 140);
 textSize(30);
 if(xDirection != 0 || yDirection != 0){
 start = true;
 object = true;
 }
 }
 else{
 imageMode(CORNER);
 background(bg);
 textSize(35);
 time = 30 - round(counter/80);
 if (radius > 10){
 radius = radius - 0.005;
 };
 if(object == true){
 objectX = int(random(40, width - 70));
 objectY = int(random(180, height - 70));
 object = false;
 };
 ellipseMode(CENTER);
 stroke(0);
 fill(255, 255, 0); 
 ellipse(objectX, objectY, radius, radius);
 if(time == -1){
 reset();
 }
 else if(time <= 10){
 gameOver = 1;
 fill(255, 0, 0);
 }
 else{
 fill(0);
 };
 textAlign(RIGHT);
 text(time, width - 40, 40);
 fill(0);
 textAlign(LEFT);
 text("Score "+points, 30, 40);
 textSize(20);
 if(points <= highScore){
 text("High Score "+highScore, 30, 60);
 }
 else{
 fill(255, 255, 0);
 text("High Score "+points, 30, 60);
 String [] high = {points+""};
 saveStrings("highscore.txt", high);
 };
 user.move();
 counter++;
 };
}

class Animal{
 int X;
 int Y;
 int FRAME;
 int HORIZONTAL;
 int VERTICAL;
 
 Animal(){
 X = width/2;
 Y = height - 40;
 FRAME = 1;
 HORIZONTAL = 0;
 VERTICAL = 0;
 }
 
 void move(){
 HORIZONTAL = xDirection;
 VERTICAL = yDirection;
 imageMode(CENTER);
 if(FRAME == 1){
 if(VERTICAL == -1){
 image(f1, X, Y);
 }
 else if(HORIZONTAL == -1){
 image(l1, X, Y);
 }
 else if(HORIZONTAL == 1){
 image(r1, X, Y);
 }
 else if (VERTICAL == 1){
 image(b1, X, Y);
 }
 else if(VERTICAL == 0 && HORIZONTAL == 0){
 image(f1, X, Y);
 };
 }
 else if(FRAME == 2){
 if(VERTICAL == -1){
 image(f2, X, Y);
 }
 else if(HORIZONTAL == -1){
 image(l2, X, Y);
 }
 else if(HORIZONTAL == 1){
 image(r2, X, Y);
 }
 else if (VERTICAL == 1){
 image(b2, X, Y);
 }
 else if(VERTICAL == 0 && HORIZONTAL == 0){
 image(f2, X, Y);
 };
 }
 else if(FRAME == 3){
 if(VERTICAL == -1){
 image(f3, X, Y);
 }
 else if(HORIZONTAL == -1){
 image(l3, X, Y);
 }
 else if(HORIZONTAL == 1){
 image(r3, X, Y);
 }
 else if (VERTICAL == 1){
 image(b3, X, Y);
 }
 else if(VERTICAL == 0 && HORIZONTAL == 0){
 image(f3, X, Y);
 };
 }
 else if(FRAME == 4){
 if(VERTICAL == -1){
 image(f4, X, Y);
 }
 else if(HORIZONTAL == -1){
 image(l4, X, Y);
 }
 else if(HORIZONTAL == 1){
 image(r4, X, Y);
 }
 else if (VERTICAL == 1){
 image(b4, X, Y);
 }
 else if(VERTICAL == 0 && HORIZONTAL == 0){
 image(f4, X, Y);
 };
 };
 
 if(counter%speed == 0){
 if((X < 0 && HORIZONTAL == -1) || (X > width - 70 && HORIZONTAL == 1)){
 X = X;
 }
 else{
 X = X + 10*HORIZONTAL;
 };
 if((Y < 190 && VERTICAL == 1) || (Y > height - 70 && VERTICAL == -1)){
 Y = Y;
 }
 else{
 Y = Y - 10*VERTICAL;
 };
 if(FRAME == 4){
 FRAME = 1;
 }
 else{
 FRAME++;
 };
 };
 
 if(object == false){
 if(X >= objectX - radius 
 && X <= objectX + radius
 && Y >= objectY - radius - 20
 && Y <= objectY + radius + 20){
 points++;
 object = true;
 };
 };
 }
}

void serialEvent(Serial myPort){
 String arduinoInfo = myPort.readStringUntil('\n');
 if(arduinoInfo != null){
 String [] infoString = arduinoInfo.split(",");
 xDirection = int(infoString[0]);
 yDirection = int(infoString[1]);
 String tempSpeed = infoString[2].trim();
 speed = int(tempSpeed);
 };
 myPort.write(gameOver);
}

And this is the Arduino code:

const int ledRed = 2;
const int leftPot = 12;
const int downPot = 11;
const int upPot = 10;
const int rightPot = 9;

void setup() {
 pinMode(ledRed, OUTPUT);
 pinMode(leftPot, INPUT);
 pinMode(downPot, INPUT);
 pinMode(upPot, INPUT);
 pinMode(rightPot, INPUT);
 Serial.begin(9600);
 Serial.println("0,0,1");
}

void loop() {
 int xDirection;
 int yDirection;
 int left = digitalRead(leftPot);
 int down = digitalRead(downPot);
 int up = digitalRead(upPot);
 int right = digitalRead(rightPot);
 int theSpeed = analogRead(A2);
 int mappedSpeed = map(theSpeed, 0, 1023, 15, 5);
 if(Serial.available() > 0){
 int input = Serial.read();
 if(input == 1){
 digitalWrite(ledRed, HIGH);
 }
 else{
 digitalWrite(ledRed, LOW);
 };
 if(down == HIGH){
 yDirection = -1;
 xDirection = 0;
 }
 if(up == HIGH){
 yDirection = 1;
 xDirection = 0;
 }
 if(left == HIGH){
 xDirection = -1;
 yDirection = 0;
 }
 if(right == HIGH){
 xDirection = 1;
 yDirection = 0;
 }
 if(up == LOW && down == LOW && left == LOW && right == LOW){
 xDirection = 0;
 yDirection = 0;
 }
 Serial.print(xDirection);
 Serial.print(",");
 Serial.print(yDirection);
 Serial.print(",");
 Serial.println(mappedSpeed);
 };
}