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 void loadsprites(){ // Running for (int i=0; i<sprites.length;i++){ sprites[i][0]=loadImage("assets/run/run"+i+".png"); sprites[i][0].resize(53,74); } // Jumping for (int i=0; i<sprites.length;i++){ sprites[i][1]=loadImage("assets/jump/jump"+i+".png"); sprites[i][1].resize(53,74); } // Sliding for (int i=0; i<sprites.length;i++){ sprites[i][2]=loadImage("assets/slide/slide"+i+".png"); sprites[i][2].resize(53,57); } // Dying for (int i=0; i<sprites.length;i++){ sprites[i][3]=loadImage("assets/dying/Dead__00"+i+".png"); sprites[i][3].resize(73,77); } // Fire for (int i=0; i<firesprites.length;i++){ firesprites[i]=loadImage("assets/fire/tile0"+i+".png"); firesprites[i].resize(60,60); } // Bird for (int i=0; i<birdsprites.length;i++){ birdsprites[i]=loadImage("assets/bird/tile00"+i+".png"); 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 void addObstacle(){ 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 firelist.add(new Fire()); } else { birdlist.add(new Bird()); } } }
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; }
Menu:
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); image(instructmenu,width/2,height/2); image(backmenu,width/2, height/2-200); 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 if (dead==false) {back.amp(1);} // 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 addObstacle(); // add an obstacle imageMode(CENTER); // Display the score image(scoremenu, width/2, 50); 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)){ dead=true; } // 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)){ dead=true; } // 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 dead=false; start=true; reset=false; score=0; // reset the score // reset the obstacles list firelist = new ArrayList<Fire>(); birdlist = new ArrayList<Bird>(); } if (dead==true){ // 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); image(startmenu,width/2, height/2-20); text("REPLAY",width/2,height/2+7-20); imageMode(CORNER); } } // display the lobby menu else if (start==false){ // mute the music back.amp(0); menu(); image(main,0,0); imageMode(CENTER); textAlign(CENTER); textSize(30); // display the ui image(startmenu,width/2, height/2-20); text("PLAY",width/2,height/2+7-20); image(helpmenu,width/2, height/2+100-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; menu.play(); } // 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; menu.play(); } // 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; menu.play(); } // 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; menu.play(); } } 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 menu = new SoundFile(this, "assets/menu.wav"); back = new SoundFile(this, "assets/back.mp3"); // Play the background music back.play(); // Loop the background music back.loop(); // unmute the music if alive if (dead==false) {back.amp(1);} // 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 if (!dead) {score+=0.05;}
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.
FULL CODE:
import processing.sound.*; PImage bg1, bg2, bg3, bg4, bg5, bg6, bg7, platform, main, startmenu, helpmenu, title, instructmenu, scoremenu; // Background images PImage backmenu; 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 dead= 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 void loadsprites(){ // Running for (int i=0; i<sprites.length;i++){ sprites[i][0]=loadImage("assets/run/run"+i+".png"); sprites[i][0].resize(53,74); } // Jumping for (int i=0; i<sprites.length;i++){ sprites[i][1]=loadImage("assets/jump/jump"+i+".png"); sprites[i][1].resize(53,74); } // Sliding for (int i=0; i<sprites.length;i++){ sprites[i][2]=loadImage("assets/slide/slide"+i+".png"); sprites[i][2].resize(53,57); } // Dying for (int i=0; i<sprites.length;i++){ sprites[i][3]=loadImage("assets/dying/Dead__00"+i+".png"); sprites[i][3].resize(73,77); } // Fire for (int i=0; i<firesprites.length;i++){ firesprites[i]=loadImage("assets/fire/tile0"+i+".png"); firesprites[i].resize(60,60); } // Bird for (int i=0; i<birdsprites.length;i++){ birdsprites[i]=loadImage("assets/bird/tile00"+i+".png"); birdsprites[i].resize(80,80); } } // Load the background images void load(){ bg7 = loadImage("assets/bg.png"); // Resize the images bg7.resize(960,540); bg6 = loadImage("assets/bg6.png"); bg6.resize(960,540); bg5 = loadImage("assets/bg5.png"); bg5.resize(960,540); bg4 = loadImage("assets/bg2.png"); bg4.resize(960,540); bg3 = loadImage("assets/bg4.png"); bg3.resize(960,540); bg2 = loadImage("assets/bg1.png"); bg2.resize(960,540); bg1 = loadImage("assets/bg3.png"); bg1.resize(960,540); platform = loadImage("assets/platform.png"); platform.resize(960,610); } // menu loading void menu(){ // load ui images startmenu = loadImage("assets/b_3.png"); helpmenu = loadImage("assets/b_4.png"); // resize the images startmenu.resize(150,70); helpmenu.resize(180,80); title = loadImage("assets/title.png"); instructmenu = loadImage("assets/b_5.png"); scoremenu = loadImage("assets/bar_1.png"); scoremenu.resize(250,40); instructmenu.resize(350,300); backmenu = loadImage("assets/b_6.png"); backmenu.resize(60,60); } // 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 void addObstacle(){ 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 firelist.add(new Fire()); } else { birdlist.add(new Bird()); } } } void setup(){ size(960,540); load(); // load the background images loadsprites(); // load the sprites main = loadImage("assets/main.png"); main.resize(960,540); // Load the sound effects menu = new SoundFile(this, "assets/menu.wav"); 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); image(instructmenu,width/2,height/2); image(backmenu,width/2, height/2-200); 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 if (dead==false) {back.amp(1);} // 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 addObstacle(); // add an obstacle imageMode(CENTER); // Display the score image(scoremenu, width/2, 50); 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)){ dead=true; } // 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)){ dead=true; } // 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 dead=false; start=true; reset=false; score=0; // reset the score // reset the obstacles list firelist = new ArrayList<Fire>(); birdlist = new ArrayList<Bird>(); } if (dead==true){ // 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); image(startmenu,width/2, height/2-20); text("REPLAY",width/2,height/2+7-20); imageMode(CORNER); } } // display the lobby menu else if (start==false){ // mute the music back.amp(0); menu(); image(main,0,0); imageMode(CENTER); textAlign(CENTER); textSize(30); // display the ui image(startmenu,width/2, height/2-20); text("PLAY",width/2,height/2+7-20); image(helpmenu,width/2, height/2+100-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; menu.play(); } // 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; menu.play(); } // 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; menu.play(); } // 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; menu.play(); } } 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 if (dead){image(birdsprites[6],xcoord,380);} 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 if (dead){image(firesprites[10],xcoord,435);} 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 if (!dead) {score+=0.05;} 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; } } }