# Midterm Project

##### Overview:

As mentioned before, for my midterm project, I was inspired by the mechanics of the Chrome Dino Runner game and tried to create a newer version with more features that I called “Knight Runner”. To avoid obstacles, the avatar can either jump over the fire, or jump/crouch when it’s a bird.

##### Process & Features:
###### Parallax:

To give the game a realistic aspect, I added a Parallax effect in which the far-away clouds and mountains seem to move more slowly than the closer ones, by changing each layer’s (6 layers) position by a different amount (between 1 and 5).

```// Parallax effect
void update(){
x6--; x6_2--;
x5-=2; x5_2-=2;
x4-=3; x4_2-=3;
x3-=3; x3_2-=3;
x2-=4; x2_2-=4;
x1-=5; x1_2-=5;
}```
###### Infinite Side-scrolling:

Instead of making the character move inside the display window, I used an infinite side-scrolling in which the character is static whereas the background moves from the right to the left. To achieve that, I used two images placed next to each other that reappear on the right side once they get out of the display window.

```  // Infinite scrolling
if (x6<=-width){x6=width;} if (x6_2<=-width){x6_2=width;}
if (x5<=-width){x5=width;} if (x5_2<=-width){x5_2=width;}
if (x4<=-width){x4=width;} if (x4_2<=-width){x4_2=width;}
if (x3<=-width){x3=width;} if (x3_2<=-width){x3_2=width;}
if (x2<=-width){x2=width;} if (x2_2<=-width){x2_2=width;}
if (x1<=-width){x1=width;} if (x1_2<=-width){x1_2=width;}```
###### Spritesheet:

To animate the character, I am using 3 sprite sheets stored in a 2D array (running, jumping, sliding, dying), each row has 10 images.

To animate the obstacles, I am using 2 other sprite sheets, one for fire (64 images), and the other for birds (9 images).

I am using frameCount to loop over the sprites

```// Upload all the sprites
// Running
for (int i=0; i<sprites.length;i++){
sprites[i][0].resize(53,74);
}
// Jumping
for (int i=0; i<sprites.length;i++){
sprites[i][1].resize(53,74);
}
// Sliding
for (int i=0; i<sprites.length;i++){
sprites[i][2].resize(53,57);
}
// Dying
for (int i=0; i<sprites.length;i++){
sprites[i][3].resize(73,77);
}
// Fire
for (int i=0; i<firesprites.length;i++){
firesprites[i].resize(60,60);
}
// Bird
for (int i=0; i<birdsprites.length;i++){
birdsprites[i].resize(80,80);
}
}```
###### Gravity:

I am using a gravity effect for both jumping and sliding, to give the animation a realistic aspect. When jumping, the speed is continuously decreased by the amount of gravity, however, when crouching, it gets increased.

```void move(){
ycoord -= speed;
// gravity if jumps
if (ycoord<425){
speed -= gravity;
}
// gravity if crouches
else if (ycoord>425){
ycoord += speed;
speed += gravity;
}
// remain same when running
else{
state=0;
speed=0;
ycoord=425;
}
}
```
###### Jump and Crouch:

The user cannot jump and crouch at the same time. Both the jump and crouch are done using ycoord, speed and the gravity.

```// jump
void jump(){
if (ycoord==425 && crouching==false){
state=1;
gravity=1;
speed= 16;
}
}

// crouch
void crouch(){
if (crouching==true && ycoord==425){
state=2;
ycoord=442;
gravity=1;
speed= -20;
}
}```
###### Generating obstacles:

To generate obstacles I am both using frameCount and two random functions, the first one is used to choose when to generate the obstacle, whereas the second one is used to choose what to generate (fire or bird). The obstacles are automatically stored in an array to keep track of their position and to display them continuously. If the obstacles disappear from the screen (go over the edge) they get immediately remove from the array.

The bird obstacles have a higher speed than fire because technically fire is static so it should have the same speed as the scrolling, whereas the bird is flying.

```// add a new obstacle
if (frameCount % 60 == 0 && random(1)<0.5){
// choose randomly if its fire or a bird
if (random(2)<1){
// add it to the list
}
else {
}
}
}```
###### Collisions:

To flag when the avatar touches one of the obstacles, I first used a rectangle to limit the areas of both the obstacle and avatar, then checked if those areas overlapped. Meaning that the x and y coordinates of the avatar would be inside the obstacle area. The avatar should jump when there is fire, but can either jump or crouch when there is a bird, to avoid collisions with obstacles.

```// Bird
boolean checkfail(float ycoord){
// if crouching, avatar is safe
if (avatar.state==2){
return false;
}
// check if avatar touches the obstacle
return xcoord+25>=100 && xcoord+25<=150 && ycoord>=400 && ycoord<=400+30 || xcoord+40+25>=100 && xcoord+40+25<=150 && ycoord+70>=400 && ycoord<=400+30;
}
// fire
boolean checkfail(float ycoord){
// check if avatar touches the obstacle
return xcoord>=100 && xcoord<=150 && ycoord+70>=540-60-30 || xcoord+40>=100 && xcoord+40<=150 && ycoord+70>=540-60-30;
}```

Similarly, to check which button the user clicked, I used mouseClicked(), mouseX, and mouseY and checked whether the x and y coordinates are inside the area of that specific button. The menu (lobby) is displayed first, then the user has a choice to either start the game, or read the instructions.

To switch between lobby, game, and instructions pages, I am overlapping backgrounds over each other.

###### draw():

Inside my draw() function, I mainly check the status of the game using boolean variables (avatar died, menu, reset, game ongoing…), then proceed with displaying the right images, and the right text.

###### Boolean variables:

Start: flags when the user clicks on the start button, if start is false, then the menu is displayed

Help: flags when the user clicks on the help button, the instructions are then displayed

Dead: flags when the avatar touches an obstacle, the game then ends, and the user is given a choice to replay

Reset: flags when the user chooses to replay, all the games settings are reset, and the arrays get cleared

```void draw(){

// if instructions icon clicked
if (help){
// mute the music
back.amp(0);
image(main,0,0);
imageMode(CENTER);
imageMode(CORNER);
textSize(20);
text("↑ : Jump",width/2,height/2-60);
text("↓ : Crouch",width/2,height/2-20);
text("Try to avoid all obstacles",width/2,height/2+20);
text("(Fire, birds)",width/2,height/2+60);
noFill();
rect(width/2-30,height/2-230,60,60);
}

else if (start){
// unmute the music if alive
// mute the music if dead
else {back.amp(0);}
display(); // display the background images
update(); // parallax effect and infinite scrolling
rect(100,425,50,70);
avatar.show(); // display the avatar
imageMode(CENTER);
// Display the score
imageMode(CORNER);
textSize(20);
textAlign(CENTER);
text("score:  " + round(score),width/2,55);

// Display the obstacles
for (int i=0; i<firelist.size(); i++){
firelist.get(i).show();
firelist.get(i).move();

// check if avatar touches an obstacle
if (firelist.get(i).checkfail(avatar.ycoord)){
}

// remove the obstacles that are not displayed
if (firelist.get(i).xcoord <-70){
firelist.remove(i);
}
}

// Display the obstacles
for (int i=0; i<birdlist.size(); i++){
birdlist.get(i).show();
birdlist.get(i).move();

// check if avatar touches an obstacle
if (birdlist.get(i).checkfail(avatar.ycoord)){
}

// remove the obstacles that are not displayed
if (birdlist.get(i).xcoord <-70){
birdlist.remove(i);
}
}

// If replay button is clicked, reset the game
if (reset==true){
back.amp(1); // unmute the music
start=true;
reset=false;
score=0; // reset the score
// reset the obstacles list
firelist = new ArrayList<Fire>();
birdlist = new ArrayList<Bird>();
}

// stop the parallax
x6++; x6_2++;
x5+=2; x5_2+=2;
x4+=3; x4_2+=3;
x3+=3; x3_2+=3;
x2+=4; x2_2+=4;
x1+=5; x1_2+=5;

// stop the obstacles animation
for (int i=0; i<firelist.size(); i++){
firelist.get(i).xcoord +=5;
}
for (int i=0; i<birdlist.size(); i++){
birdlist.get(i).xcoord +=10;
}
// enable the dying animation
avatar.state=3;
// display the replay button
imageMode(CENTER);
text("REPLAY",width/2,height/2+7-20);
imageMode(CORNER);
}
}

// display the lobby menu
else if (start==false){
// mute the music
back.amp(0);
image(main,0,0);
imageMode(CENTER);
textAlign(CENTER);
textSize(30);
// display the ui
text("PLAY",width/2,height/2+7-20);
text("HELP",width/2,height/2+100+7-20);
image(title,width/2, 100);
noFill();
rect(width/2-75,height/2-55,150,70);
rect(width/2-90,height/2+40,180,80);
imageMode(CORNER);
}

}```
###### Shapes:

I have used 4 blinking rectangles to add some aesthetics to the title.

```noStroke();
if (frameCount%15==0){
noFill();}
else{ fill(0);}
rect(width/2+220, 85,10,30,PI);
rect(width/2+240, 90,10,20,PI);
rect(width/2-225, 85,10,30,PI);
rect(width/2-245, 90,10,20,PI);
fill(255);```
###### User Input:

To detect when the user clicks on a key, I used both keyPressed() and keyReleased() functions for keyboard, and mouseClicked.

```void mouseClicked(){
// if player clicks on start
if ((mouseX>width/2-75) && (mouseX<width/2+75) && (mouseY>height/2-55) && (mouseY<height/2+15)){
start=true;
}
// if player clicks on help
if ((start==false) && (mouseX>width/2-90) && (mouseX<width/2+90) && (mouseY>height/2+40) && (mouseY<height/2+120)){
help=true;
}
// if player clicks on back
else if ((help==true) && (mouseX>width/2-30) && (mouseX<width/2+30) && (mouseY>height/2-230) && (mouseY<height-170)){
help=false;
}
// if player clicks on replay
else if ((dead==true) && (mouseX>width/2-75) && (mouseX<width/2+75) && (mouseY>height/2-55) && (mouseY<height/2+15)){
reset=true;
}
}
void keyPressed(){
// Jump
if (keyCode==UP && dead==false){
avatar.jump();
}
// Crouch
if (keyCode==DOWN && dead==false){
avatar.crouching=true;
avatar.crouch();
}
}

void keyReleased(){
// stop crouching
if (keyCode==DOWN && dead==false){
avatar.crouching=false;
}
}```
###### SFX:

Concerning the sound effects, I have only used two main ones (the background music, and the click sound effect). Instead of stopping the music, I mute it, then unmute it when the game starts.

```SoundFile menu; // Click sound effect
SoundFile back; // background music

// Load the sound effects
back = new SoundFile(this, "assets/back.mp3");
// Play the background music
back.play();
// Loop the background music
back.loop();

// unmute the music if alive
// mute the music if dead
else {back.amp(0);}```
###### Score:

The score gets incremented when the avatar is moving by O.O5 per frame and is displayed on the top-center of the display window. It gets reset when the game restarts.

```// increment the score
###### End of the game:

When the avatar touches an obstacle, the dying animation is enabled and freezes at the last sprite, and all the other animations/motions are stopped. The score is displayed on top, and the replay button appears.

View post on imgur.com

FULL CODE:

```import processing.sound.*;
PImage bg1, bg2, bg3, bg4, bg5, bg6, bg7, platform, main, startmenu, helpmenu, title, instructmenu, scoremenu; // Background images
int x1=0, x1_2=960, x2=0, x2_2=960, x3=0, x3_2=960; // X-coordinates of the images
int x4=0, x4_2=960, x5=0, x5_2=960, x6=0, x6_2=960; // X-coordinates of the images
PImage[][] sprites = new PImage[10][4]; // Store the sprites for the avatar
PImage [] firesprites = new PImage[64]; // Store the sprites for the fire
PImage [] birdsprites = new PImage[9]; // Store the sprites for the birds
ArrayList<Fire> firelist = new ArrayList<Fire>(); // Store all the fire objects
ArrayList<Bird> birdlist = new ArrayList<Bird>(); // store all the bird objects
boolean start=false;
boolean help= false;
boolean reset= false;
float score=0; // Keep track of the score
SoundFile menu; // Click sound effect
SoundFile back; // background music

// Create a new avatar
Avatar avatar = new Avatar(0,425);

// Upload all the sprites
// Running
for (int i=0; i<sprites.length;i++){
sprites[i][0].resize(53,74);
}
// Jumping
for (int i=0; i<sprites.length;i++){
sprites[i][1].resize(53,74);
}
// Sliding
for (int i=0; i<sprites.length;i++){
sprites[i][2].resize(53,57);
}
// Dying
for (int i=0; i<sprites.length;i++){
sprites[i][3].resize(73,77);
}
// Fire
for (int i=0; i<firesprites.length;i++){
firesprites[i].resize(60,60);
}
// Bird
for (int i=0; i<birdsprites.length;i++){
birdsprites[i].resize(80,80);
}
}

// Load the background images
// Resize the images
bg7.resize(960,540);
bg6.resize(960,540);
bg5.resize(960,540);
bg4.resize(960,540);
bg3.resize(960,540);
bg2.resize(960,540);
bg1.resize(960,540);
platform.resize(960,610);
}

// load ui images
// resize the images
}

// Display the background
void display(){
image(bg7,0,0);
image(bg6,x6,0);
image(bg6,x6_2,0);
image(bg5,x5,0);
image(bg5,x5_2,0);
image(bg4,x4,0);
image(bg4,x4_2,0);
image(bg3,x3,0);
image(bg3,x3_2,0);
image(bg2,x2,0);
image(bg2,x2_2,0);
image(bg1,x1,0);
image(bg1,x1_2,0);
// Add a tint to match the background
tint(#7cdfd2);
image(platform,x1,0);
image(platform,x1_2,0);
noTint();
}

// Parallax effect
void update(){
x6--; x6_2--;
x5-=2; x5_2-=2;
x4-=3; x4_2-=3;
x3-=3; x3_2-=3;
x2-=4; x2_2-=4;
x1-=5; x1_2-=5;

// Infinite scrolling
if (x6<=-width){x6=width;} if (x6_2<=-width){x6_2=width;}
if (x5<=-width){x5=width;} if (x5_2<=-width){x5_2=width;}
if (x4<=-width){x4=width;} if (x4_2<=-width){x4_2=width;}
if (x3<=-width){x3=width;} if (x3_2<=-width){x3_2=width;}
if (x2<=-width){x2=width;} if (x2_2<=-width){x2_2=width;}
if (x1<=-width){x1=width;} if (x1_2<=-width){x1_2=width;}
}

// add a new obstacle
if (frameCount % 60 == 0 && random(1)<0.5){
// choose randomly if its fire or a bird
if (random(2)<1){
// add it to the list
}
else {
}
}
}

void setup(){
size(960,540);
main.resize(960,540);
// Load the sound effects
back = new SoundFile(this, "assets/back.mp3");
// Play the background music
back.play();
// Loop the background music
back.loop();
}

void draw(){

// if instructions icon clicked
if (help){
// mute the music
back.amp(0);
image(main,0,0);
imageMode(CENTER);
imageMode(CORNER);
textSize(20);
text("↑ : Jump",width/2,height/2-60);
text("↓ : Crouch",width/2,height/2-20);
text("Try to avoid all obstacles",width/2,height/2+20);
text("(Fire, birds)",width/2,height/2+60);
noFill();
//rect(width/2-30,height/2-230,60,60);
}

else if (start){
// unmute the music if alive
// mute the music if dead
else {back.amp(0);}
display(); // display the background images
update(); // parallax effect and infinite scrolling
//rect(100,425,50,70);
avatar.show(); // display the avatar
imageMode(CENTER);
// Display the score
imageMode(CORNER);
textSize(20);
textAlign(CENTER);
text("score:  " + round(score),width/2,55);

// Display the obstacles
for (int i=0; i<firelist.size(); i++){
firelist.get(i).show();
firelist.get(i).move();

// check if avatar touches an obstacle
if (firelist.get(i).checkfail(avatar.ycoord)){
}

// remove the obstacles that are not displayed
if (firelist.get(i).xcoord <-70){
firelist.remove(i);
}
}

// Display the obstacles
for (int i=0; i<birdlist.size(); i++){
birdlist.get(i).show();
birdlist.get(i).move();

// check if avatar touches an obstacle
if (birdlist.get(i).checkfail(avatar.ycoord)){
}

// remove the obstacles that are not displayed
if (birdlist.get(i).xcoord <-70){
birdlist.remove(i);
}
}

// If replay button is clicked, reset the game
if (reset==true){
back.amp(1); // unmute the music
start=true;
reset=false;
score=0; // reset the score
// reset the obstacles list
firelist = new ArrayList<Fire>();
birdlist = new ArrayList<Bird>();
}

// stop the parallax
x6++; x6_2++;
x5+=2; x5_2+=2;
x4+=3; x4_2+=3;
x3+=3; x3_2+=3;
x2+=4; x2_2+=4;
x1+=5; x1_2+=5;

// stop the obstacles animation
for (int i=0; i<firelist.size(); i++){
firelist.get(i).xcoord +=5;
}
for (int i=0; i<birdlist.size(); i++){
birdlist.get(i).xcoord +=10;
}
// enable the dying animation
avatar.state=3;
// display the replay button
imageMode(CENTER);
text("REPLAY",width/2,height/2+7-20);
imageMode(CORNER);
}
}

// display the lobby menu
else if (start==false){
// mute the music
back.amp(0);
image(main,0,0);
imageMode(CENTER);
textAlign(CENTER);
textSize(30);
// display the ui
text("PLAY",width/2,height/2+7-20);
text("HELP",width/2,height/2+100+7-20);
image(title,width/2, 100);
noFill();
//rect(width/2-75,height/2-55,150,70);
//rect(width/2-90,height/2+40,180,80);
imageMode(CORNER);
noStroke();
if (frameCount%15==0){
noFill();}
else{ fill(0);}
rect(width/2+220, 85,10,30,PI);
rect(width/2+240, 90,10,20,PI);
rect(width/2-225, 85,10,30,PI);
rect(width/2-245, 90,10,20,PI);
fill(255);
}

}

void mouseClicked(){
// if player clicks on start
if ((mouseX>width/2-75) && (mouseX<width/2+75) && (mouseY>height/2-55) && (mouseY<height/2+15)){
start=true;
}
// if player clicks on help
if ((start==false) && (mouseX>width/2-90) && (mouseX<width/2+90) && (mouseY>height/2+40) && (mouseY<height/2+120)){
help=true;
}
// if player clicks on back
else if ((help==true) && (mouseX>width/2-30) && (mouseX<width/2+30) && (mouseY>height/2-230) && (mouseY<height-170)){
help=false;
}
// if player clicks on replay
else if ((dead==true) && (mouseX>width/2-75) && (mouseX<width/2+75) && (mouseY>height/2-55) && (mouseY<height/2+15)){
reset=true;
}
}

void keyPressed(){
// Jump
if (keyCode==UP && dead==false){
avatar.jump();
}
// Crouch
if (keyCode==DOWN && dead==false){
avatar.crouching=true;
avatar.crouch();
}
}

void keyReleased(){
// stop crouching
if (keyCode==DOWN && dead==false){
avatar.crouching=false;
}
}

class Bird{
//bird's x-coordinate
float xcoord;

Bird(){
// generate the bird outside the screen
xcoord = 40 + 960;
}

// display the bird
void show(){
// stop the animation
else{
// play the animation
image(birdsprites[frameCount/2%birdsprites.length],xcoord,380);}
}

// move the bird
void move(){
xcoord -= 10;
noFill();
//rect(xcoord+25, 400, 48,30);
}

boolean checkfail(float ycoord){
// if crouching, avatar is safe
if (avatar.state==2){
return false;
}
// check if avatar touches the obstacle
return xcoord+25>=100 && xcoord+25<=150 && ycoord>=400 && ycoord<=400+30 || xcoord+40+25>=100 && xcoord+40+25<=150 && ycoord+70>=400 && ycoord<=400+30;
}
}

class Fire{
//fire's x-coordinate
float xcoord;

Fire(){
// generate the fire outside the screen
xcoord = 40 + 960;
}

// display the fire
void show(){
// stop the animation
else{
// play the animation
image(firesprites[frameCount/2%firesprites.length],xcoord,435);}
}

// move the fire
void move(){
xcoord -= 5;
}

boolean checkfail(float ycoord){
// check if avatar touches the obstacle
return xcoord>=100 && xcoord<=150 && ycoord+70>=540-60-30 || xcoord+40>=100 && xcoord+40<=150 && ycoord+70>=540-60-30;
}
}

class Avatar{
float xcoord=100; // xcoordinate of avatar
float ycoord; // ycoordinate of avatar
float gravity=1; // gravity of avatar
float speed= 0; // speed of avatar
int state=0; // state of avatar (jumping, crouchin, dying)
boolean crouching =false; // flag if crouching
// used for dying animation
int k=0;
int cnt;

// constructor
Avatar(int state, float ycoord){
this.state= state;
this.ycoord= ycoord;
}

void show(){
// play the dying animation
if (state==3 && cnt<9){
// freeze at last sprite
ycoord=427;
image(sprites[k*frameCount/2%sprites.length][state],xcoord,ycoord);
k=1;
cnt++;
}
// display animation
else if (state==1 || state==2 || state==0){
image(sprites[frameCount/2%sprites.length][state],xcoord,ycoord);
}
else {
// freeze at last sprite
ycoord=427;
image(sprites[9][state],xcoord,ycoord);
}
move();
}

// move the player
void move(){
// increment the score
ycoord -= speed;
// gravity if jumps
if (ycoord<425){
speed -= gravity;
}
// gravity if crouches
else if (ycoord>425){
ycoord += speed;
speed += gravity;
}
// remain same when running
else{
state=0;
speed=0;
ycoord=425;
}
}

// jump
void jump(){
if (ycoord==425 && crouching==false){
state=1;
gravity=1;
speed= 16;
}
}

// crouch
void crouch(){
if (crouching==true && ycoord==425){
state=2;
ycoord=442;
gravity=1;
speed= -20;
}
}
}```