Week 6 – Midterm Project

1. p5js Sketch

If the experience looks static, please click on the ‘sketch’ version and run it. If you do not see the sketch to the entire width and height, please use the ‘sketch’ version instead.

2. Overall Concept

In my midterm project, the user takes on the role of a beta-tester of a 2D RPG casual adventure game “Tales of Avella” played on a fictional, futuristic ‘Playstation 9’ device. ‘Playstation 9’ is meant to be a much more state-of-the-art video game console than the currently available ‘Playstation 5’ (see Fig. 1). In “Tales of Avella,” the user has the opportunity to explore the world and help someone in need. Upon beginning the experience, the Playstation 9 apparently “turns on” and “loads” the game. The user is greeted with a welcome message from fictional PixelBloom Studios developers, as well as informed with background story for the game and instructions for controlling the user’s character, “Luke.” The background story of the game goes as follows: Luke has just moved into the charming village of Avella and is ready to discover what makes this town so special. The user can control “Luke” to explore the world in “Tales of Avella.” using the arrow keys (left, right, up, down). Depending on the user’s actions, the user could enter new areas and meet a non-playable character (NPC) farmer with a quest to harvest forty-five carrots. Upon completion of the quest, a congratulatory message is displayed, followed by a ‘memory’ snapshot of a gratitude dinner with the farmer and his daughter Lily is displayed. Finally, the user faces an option to restart the game, by pressing ‘Y’ for yes and ‘N’ for no.

Playstation 5 - Photo by Ben Iwara (https://unsplash.com/photos/white-printer-paper-on-brown-wooden-table-tnfbre82_hc)
Playstation 5 – Photo by Ben Iwara (https://unsplash.com/photos/white-printer-paper-on-brown-wooden-table-tnfbre82_hc)
Playing the PlayStation 5, Photo by Yan Krukau: (https://www.pexels.com/photo/close-up-shot-of-person-holding-a-game-controller-9069305/)
Playing the PlayStation 5, Photo by Yan Krukau: (https://www.pexels.com/photo/close-up-shot-of-person-holding-a-game-controller-9069305/)

The user has the opportunity to interact with the world in ways more than moving Luke around. The user could use mouse clicks to progress through the conversation between the farmer and Luke. At some point in the conversation, and if the user decides to accept the farmer’s quest, the farmer’s name is revealed from an initially mysterious title “???”. The user could have Luke grab a tool in Luke’s vicinity by pressing ‘i’. The user could also have a shovel-handling Luke harvest some carrots by pressing ‘i’ and replant some carrots by pressing ‘r.’

3. How Project Works & Good Parts

Good, major game design aspects can be found in project concept, project structure, dialogue flowchart, assets (video, image, audio), character design, story design, quest progress possibilities, quest progress feedback, as well as particular details of the game mechanics. Firstly, the project concept is an attempt on an innovative twist on a simply general game played on a browser on the PC – by simulating the experience of playing a game on a futuristic device, and creating a story for the user as a beta-tester of a game. Thus, there is a frame narrative entailing a story within a story; the character “Luke” within the game “Tales of Avella” has a story that can be played by a “beta-tester” that has a story in supporting the developers at a fictional PixelBloom Studios Inc. The juxtaposition of a pixelated game played in a futuristic device is another game design choice with the intention of bringing an interesting effect – whether it is raising questions, emphasizing contrast in technological development vs historical game interest, or something else. Second, the project has been structured in six stages from 0-5, namely: (0) Opening with Background Story and Instructions; (1) Game Landing Scene (Inside Luke’s House); (2) Game Scene 2 – Luke’s Neighbourhood Outside His House; (3) Game Scene 3 – Farm; (4) Game Scene 4 – Inside Farm Shed; (5) Quest Completion Congratulatory Message with Dinner Memory Fragment and Option to Restart. A gallery containing snapshots of the game progress may be seen below. Third, considering the complex nature of a dialogue between a quest-giving farmer and Luke, I decided to create a flowchart (see flowchart image below the gallery). Fourth, videos were personally designed through Canva, taking into consideration adherence to the concept of the futuristic device interface and visual appeal through positioning of text, etc. I also browsed through audio, considering its appropriateness to the scene at hand; ambient music when the Playstation 9 opens up, relaxing acoustic guitar for the dinner memory, and game atmosphere sounds based on my past experience with Playstation 4/5. Character spritesheets were taken from Spriter’s Resource, chosen based on their closeness to the ideas of the characters I had in mind: a young adult entering a new town and a farmer. Fifth, I spent time on character design (see character traits table below flowchart). Sixth, I designed the message from Pixelbloom Studios Team to the player. Seventh, quest progress possibilities were considered and quest progress feedback was implemented for enhanced user experience.  Since the quest is on harvesting an exact amount of forty-five carrots, if the user harvested less than forty-five carrots, there is a prompt from the farmer to pull more carrots. If the user harvested more than forty-five carrots, the farmer “frowns” and tells Luke, “Over fourty-five. Go back.” At some point in development, I realized that only having the possibility to harvest carrots would impede the user from being able to complete the quest if the user has harvested more than forty-five carrots by accident, so I decided to add the option to replant some carrots – this is one of the changes in my project plan. Last but not least, I find certain little details in the game mechanics are crucial. For example, in Stage 3, the farmer NPC always “faces” Luke, whether Luke is to his right or to his left, mimicking real dialogue.

 

 

Flowchart on Dialogue between Luke and Farmer
Character Traits Table

 

Good technical decisions include setting a game state for which actions happen, structuring characters and objects with behavior (namely Luke, Farmer Giuseppe, message box between Luke and Farmer Giuseppe, shovel, carrot) into classes, structuring code into functions, using arrays to store huge amounts of carrots, designing the game mechanics of the dialogue using if and else if statements, and useful variable names. The game state is crucial because depending on game state, the farmer, carrots and videos may or may not be displayed. Structuring Luke and the Farmer using OOP has been very helpful for organization and readability, and therefore aids in my problem-solving process. Using arrays to store huge amounts of carrots does not only save time, but also helps me control the behavior of certain carrots – whether it’s status is “picked”, “unpicked” or “replanted” based on user behavior. The dialogue is a particularly complex piece of code, thus I paste the snippets below. The first snippet shows the constructor of the message box, which contains x position, y position, image profiles (on the left part of message box to help indicate who is speaking), dialogue_states array, and more arrays containing the speech text of the character(s) for the relevant dialogue_state. The second snippet is a clever way of reducing lines and reusing code as I need to display the profiles as often as I display a dialogue message. The third snippet reveals part of a long function performing actions based on the current dialogue_state. For the “Character Introduction” state, as with many other states, translation has been applied to the message box image and setTimeout is used to have a 200ms cooldown between advances to prevent accidental skips/rapid firing.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Farmer_Giuseppe_And_Luke_Message_Box {
constructor(luke_profile, farmer_profile) {
this.x = 15;
this.y = 500;
this.luke_profile = luke_profile;
this.farmer_profile = farmer_profile;
this.farmer_quest_progress = "not started";
this.dialogue_states = [
"Character Introduction",
"Quest Introduction",
"Player Choice Point",
"Repeated Quest Request",
"Quest Further Details",
"Quest Progress",
"Quest Completed",
"After Quest",
];
this.dialogue_current_state = this.dialogue_states[0];
this.intro_messages = [...];
this.current_message_index = 0;
this.input_blocked_cooldown = false;
this.reveal_title_flag = false;
this.quest_intro_messages = [...];
this.player_choice_yes = [...];
this.player_choice_no = [...];
this.quest_further_details = [...];
this.quest_progress_messages = [...];
this.quest_thanks = [...];
}
class Farmer_Giuseppe_And_Luke_Message_Box { constructor(luke_profile, farmer_profile) { this.x = 15; this.y = 500; this.luke_profile = luke_profile; this.farmer_profile = farmer_profile; this.farmer_quest_progress = "not started"; this.dialogue_states = [ "Character Introduction", "Quest Introduction", "Player Choice Point", "Repeated Quest Request", "Quest Further Details", "Quest Progress", "Quest Completed", "After Quest", ]; this.dialogue_current_state = this.dialogue_states[0]; this.intro_messages = [...]; this.current_message_index = 0; this.input_blocked_cooldown = false; this.reveal_title_flag = false; this.quest_intro_messages = [...]; this.player_choice_yes = [...]; this.player_choice_no = [...]; this.quest_further_details = [...]; this.quest_progress_messages = [...]; this.quest_thanks = [...]; }
class Farmer_Giuseppe_And_Luke_Message_Box {
  constructor(luke_profile, farmer_profile) {
    this.x = 15;
    this.y = 500;

    this.luke_profile = luke_profile;
    this.farmer_profile = farmer_profile;
    this.farmer_quest_progress = "not started";

    this.dialogue_states = [
      "Character Introduction",
      "Quest Introduction",
      "Player Choice Point",
      "Repeated Quest Request",
      "Quest Further Details",
      "Quest Progress",
      "Quest Completed",
      "After Quest",
    ];
    this.dialogue_current_state = this.dialogue_states[0];
    this.intro_messages = [...];
    this.current_message_index = 0;
    this.input_blocked_cooldown = false;
    this.reveal_title_flag = false;

    this.quest_intro_messages = [...];
    this.player_choice_yes = [...];
    this.player_choice_no = [...];
    this.quest_further_details = [...];
    this.quest_progress_messages = [...];
    this.quest_thanks = [...];
  }
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
display_profiles(person_speaking) {
if (person_speaking === "???" || person_speaking === "Farmer Giuseppe") {
push();
translate(110, 550);
image(this.farmer_profile, 0, 0, 130, 100);
pop();
} else if (person_speaking === "Luke") {
push();
translate(110, 550);
image(this.luke_profile, 0, 0, 100, 130);
pop();
}
}
display_profiles(person_speaking) { if (person_speaking === "???" || person_speaking === "Farmer Giuseppe") { push(); translate(110, 550); image(this.farmer_profile, 0, 0, 130, 100); pop(); } else if (person_speaking === "Luke") { push(); translate(110, 550); image(this.luke_profile, 0, 0, 100, 130); pop(); } }
display_profiles(person_speaking) {
    if (person_speaking === "???" || person_speaking === "Farmer Giuseppe") {
      push();
      translate(110, 550);
      image(this.farmer_profile, 0, 0, 130, 100);
      pop();
    } else if (person_speaking === "Luke") {
      push();
      translate(110, 550);
      image(this.luke_profile, 0, 0, 100, 130);
      pop();
    }
  }
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
display_message_box_and_text(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y, farmer_title, carrots) {
if (
dist(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y) <=
width / 8
) {
textAlign(LEFT);
fill(0);
//--------------------------------------------------//
if (this.dialogue_current_state === "Character Introduction") {
push();
// Center horizontally using message_box width
translate(width / 2, 550);
image(message_box, 0, 0);
pop();
// Show current message
let person_speaking =
this.current_message_index % 2 === 0 ? "Luke" : farmer_title;
this.display_profiles(person_speaking);
textSize(17);
text(person_speaking, 200, 500, 85, 120);
text(
this.intro_messages[this.current_message_index],
300,
500,
300,
120
);
// Show continuation prompt
if (this.current_message_index <= this.intro_messages.length - 1) {
text("[Click to continue]", 300, 590, 600, 20);
}
// Advance dialogue on mouse press
if (
mouseIsPressed &&
!this.input_blocked_cooldown &&
frameCount % 10 === 0
) {
if (this.current_message_index < this.intro_messages.length - 1) {
this.current_message_index++;
} else {
this.dialogue_current_state = "Quest Introduction";
this.current_message_index = 0;
}
this.input_blocked_cooldown = true;
// 200ms cooldown between advances
// Prevents accidental skips/rapid firing
setTimeout(() => (this.input_blocked_cooldown = false), 200);
}
}
...
}
display_message_box_and_text(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y, farmer_title, carrots) { if ( dist(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y) <= width / 8 ) { textAlign(LEFT); fill(0); //--------------------------------------------------// if (this.dialogue_current_state === "Character Introduction") { push(); // Center horizontally using message_box width translate(width / 2, 550); image(message_box, 0, 0); pop(); // Show current message let person_speaking = this.current_message_index % 2 === 0 ? "Luke" : farmer_title; this.display_profiles(person_speaking); textSize(17); text(person_speaking, 200, 500, 85, 120); text( this.intro_messages[this.current_message_index], 300, 500, 300, 120 ); // Show continuation prompt if (this.current_message_index <= this.intro_messages.length - 1) { text("[Click to continue]", 300, 590, 600, 20); } // Advance dialogue on mouse press if ( mouseIsPressed && !this.input_blocked_cooldown && frameCount % 10 === 0 ) { if (this.current_message_index < this.intro_messages.length - 1) { this.current_message_index++; } else { this.dialogue_current_state = "Quest Introduction"; this.current_message_index = 0; } this.input_blocked_cooldown = true; // 200ms cooldown between advances // Prevents accidental skips/rapid firing setTimeout(() => (this.input_blocked_cooldown = false), 200); } } ... }
display_message_box_and_text(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y, farmer_title, carrots) {
    if (
      dist(farmer_giuseppe_x, farmer_giuseppe_y, luke_x, luke_y) <=
      width / 8
    ) {
      textAlign(LEFT);
      fill(0);

      //--------------------------------------------------//
      if (this.dialogue_current_state === "Character Introduction") {
        push();
        // Center horizontally using message_box width
        translate(width / 2, 550);
        image(message_box, 0, 0);
        pop();

        // Show current message
        let person_speaking =
          this.current_message_index % 2 === 0 ? "Luke" : farmer_title;
        this.display_profiles(person_speaking);
        textSize(17);
        text(person_speaking, 200, 500, 85, 120);
        text(
          this.intro_messages[this.current_message_index],
          300,
          500,
          300,
          120
        );

        // Show continuation prompt
        if (this.current_message_index <= this.intro_messages.length - 1) {
          text("[Click to continue]", 300, 590, 600, 20);
        }

        // Advance dialogue on mouse press
        if (
          mouseIsPressed &&
          !this.input_blocked_cooldown &&
          frameCount % 10 === 0
        ) {
          if (this.current_message_index < this.intro_messages.length - 1) {
            this.current_message_index++;
          } else {
            this.dialogue_current_state = "Quest Introduction";
            this.current_message_index = 0;
          }
          this.input_blocked_cooldown = true;

          // 200ms cooldown between advances
          // Prevents accidental skips/rapid firing
          setTimeout(() => (this.input_blocked_cooldown = false), 200);
        }
      }
...
}

 

4. Problems Encountered & Areas for Improvement

Throughout the course of the project, I encountered many problems which allowed me to improve in my debugging and coding skills. Often, p5js gives feedback on code errors, which point to a line number. I refer to the line number and check the code around it and code associated to objects in that line. I think this has helped me solve many errors, like simple syntax errors involving a missing closing bracket (which can be easy to do when there are nested if conditions within a function within a class), to slightly more complex errors involving something “undefined” (which could be because I forgot to update the path to video/image in preload function). Personally, the very challenging problems are when the game runs without any error p5js throws out, but does not function as intended. These are logical errors. For example, I encountered this problem: the message box is displayed initially when Luke is in the vicinity of the farmer, and after a certain dialogue state is complete, the message box displays when Luke is farther away from the farmer. My intention was to have the message box display only when Luke is near the farmer. I think that this problem was resolved by being even more specific with my if conditions. One of the most challenging unresolved problems for me was that after the second video called in draw() plays, the video in the next chain of states won’t play. I tried to research online, use AI, try different ways (such as using image() in draw but this produced a static frame, using onended() functions, setting an if condition if the time of the video exceeds the duration-1 second of the video) but these all did not work. It was very difficult, and I may have tried to resolve this for about more than five hours, before deciding to give this idea up, and simply having videos that don’t play in consecutive game states.

A key area for improvement include expanding the affordances of the experience through multiple ways such as adding more characters. Another way is to add objects that are not simply part of the background but can interact with the user. By this, I mean objects similar to the tool that can be “grabbed” by the user and used to do something, such as pull carrots. Perhaps, add a painting that can be clicked on and zoomed in, which depicts a painting of the farmer and his family, along with text on their family history in Avella. A third way to expand affordances is to program the code to not only explore new places, but also go back to previously explored places.

MidTerm Report – Final

Blade – Havoc

Blade Havoc is the game I created, inspired by the early 2000s Japanese T.V series called ‘Beyblade’, which I grew up watching. For this project, I wanted to make use of ML5. Particularly hand tracking and voice detection. Due to limited dexterity with my left hand, I realized that an interactive design ins’t something limited to tangible means of input, but should be open to other ends as well. In my case, and of those who find it difficult or boring to play with the conventional design, this project is meant to be a step in a newer direction. A direction and approach made possible thanks to amazing libraries now available online.

About:

Game Link: Click on Me to play  

Github link for the code (commented) : GitHub

Game-Play instructions :  The game makes use of hand gestures and speech recognition. In order to able to play the game, click on the link above which will redirect you to another window.  Zoom in using Command + or Ctrl + to zoom in and increase the size of the canvas for better viewing.  Make sure  to have adequate adequate lighting,  sit about 70- 90 cm away from the laptop in order to be visible to your webcam’s field of vision.  Controls are fairly simple. Pinch your Thumb and Index Finger together to click, and in order to move the on screen pointers, move your hand. You may use either left or right hand of yours, but make sure to use one at a time, as the program is designed to handle one hand at a time. Likewise,  you cannot plug and play two peripheral devices of the same nature at the same time. Not that you cannot do it, but it isn’t practical! The game greats you with animations, sound, and interactive design. The pictures have been photoshopped together to create canvas for the background. Pictures and sounds such as the sound track are borrowed from the show, and I in no circumstance take credit for the ownership or production of these assets. However, I did happen to photoshop – remove background- and put them together in a specific manner alongside color scheming to meet the needs of the game.

How the game works: The game is based on the story of spinning tops which collide with one another and run out of spin. In Japanese culture, this form of play has been quite popular.

Kites and spinning tops from Japan | Turismo Roma

The show takes cultural and historical inspiration from this, and so does my project. You can toggle the instructions button on the ‘Menu’ to understand how the game works. 

Upon clicking, the game first takes you to the game rules window, and by clicking on the arrow at the bottom, it takes you to the game controls window. You can exit by pressing on the ‘close window’ button on the top right of the window pane.

Order of Events: The user first has to toggle the play button in order to proceed with the game.

The game starts with the view of the stadium, and commentator announcing the match between team USA and Japan. The two main characters are Tyson and Daitchi. Each scene proceeds after one of the character is done with their dialogue.

The match then begins with countdown and audio markers.

The beyblades are loaded into the dish, and the window shows the spin status of both the opponent and your beyblade. It shows special attack status to let the user know wether its active or not. In order to activate it , say ” Dragoon Galaxy turbo”. Each of the events, be it impact with other beyblade or activating special move, each has separate audio to it, which makes it even more interactive.

 

Since I happened to move my beyblade out of the stadium, I lost the game. You can try again by heading to the menu. Upon doing so the window will reload and all of the variables and states will be set to default status to avoid any conflict.

Game Controls: Pinch your finger to move your beyblade around. Open them to attack. If thumb and index finger close, you will be able to move, but cannot attack. In that case, you will take hits, untill you either get thrown out of the stadium, or lose out on spin.

 

Joining Thumb And Index Fingers Hand Sign Semi Flat PNG & SVG Design For T-Shirts
Pinch to active virtual click

Proud moment If there is something I am proud of, it is definitely making the sound-classifier and the image-classifier from the ML5 library to get to work. After spending three days trying my best to debug and make the conflict and errors go away, I was finally able to do so. Note that most of the tutorials on ML5.js are in conflict with newer versions of the sdk. Kindly make sure to go over the documentations to avoid any issues. Moreover, I managed to add physics to each blade as well. Upon impact, the opponent’s blade bounces  off of the walls whilst yours upon impact either pushes the other one away or you get hit away. Moreover, if you leave the parameters of the stadium, you end up losing the game. This edge detection and getting it to work is another accomplishment of mine after the implementation of machine-trained classifiers.

The Code:

Reusability and ML5.js : The code is made with modularity and re-usability in mind. For ml5.js , all of the setup code and functions have been moved to common functions to call them inside draw and setup at an instance, without having to go through heaps of code, when choosing to turn off / on the ml5.js features.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function ml5_preload_function(){
//importing the hand pose image classifier from ml5.js library
handPose = ml5.handPose({flipped:true});
//importing the sound classifier. Doesn't require additional argument.
classifier =
ml5.soundClassifier('https://teachablemachine.withgoogle.com/models/LHGCWnuCY/model.json', ()=>{
// callback function for testing if the sound model is loaded as it wasn't working before.
console.log ("sound model loaded");
});
}
// arranges the setup for ml5
function ml5_setup_logic(){
handPose.detectStart(video,gotHands);
classifyAudio();
}
//the logic for ml5 which goes into draw function
function ml5_draw_logic(){
if(hands.length >0){
let hand = hands[0];
let index = hand.index_finger_tip;
let thumb = hand.thumb_tip;
fill(255,0,0);
let d = dist(index.x,index.y,thumb.x,thumb.y);
if (d <= 20){
fill (0,0,255);
virtual_click = true;
} else if ( d > 20) {
virtual_click = false;
}
noStroke();
circle(index.x,index.y, 16);
circle (thumb.x,thumb.y, 16);
// virtual_click=false
pointerX_pos = (index.x + thumb.x)/2;
pointerY_pos = (index.y + thumb.y)/2;
}
function ml5_preload_function(){ //importing the hand pose image classifier from ml5.js library handPose = ml5.handPose({flipped:true}); //importing the sound classifier. Doesn't require additional argument. classifier = ml5.soundClassifier('https://teachablemachine.withgoogle.com/models/LHGCWnuCY/model.json', ()=>{ // callback function for testing if the sound model is loaded as it wasn't working before. console.log ("sound model loaded"); }); } // arranges the setup for ml5 function ml5_setup_logic(){ handPose.detectStart(video,gotHands); classifyAudio(); } //the logic for ml5 which goes into draw function function ml5_draw_logic(){ if(hands.length >0){ let hand = hands[0]; let index = hand.index_finger_tip; let thumb = hand.thumb_tip; fill(255,0,0); let d = dist(index.x,index.y,thumb.x,thumb.y); if (d <= 20){ fill (0,0,255); virtual_click = true; } else if ( d > 20) { virtual_click = false; } noStroke(); circle(index.x,index.y, 16); circle (thumb.x,thumb.y, 16); // virtual_click=false pointerX_pos = (index.x + thumb.x)/2; pointerY_pos = (index.y + thumb.y)/2; }
function ml5_preload_function(){
    //importing the hand pose image classifier from ml5.js library
  handPose = ml5.handPose({flipped:true}); 
  //importing the sound classifier. Doesn't require additional argument.
 classifier =
   ml5.soundClassifier('https://teachablemachine.withgoogle.com/models/LHGCWnuCY/model.json', ()=>{
//     callback function for testing if the sound model is loaded as it wasn't working before.
   console.log ("sound model loaded");
 });
}


// arranges the setup for ml5
function ml5_setup_logic(){
    handPose.detectStart(video,gotHands);
    classifyAudio();

}



//the logic for ml5 which goes into draw function
function ml5_draw_logic(){
  if(hands.length >0){
    let hand = hands[0];
    let index =  hand.index_finger_tip;
    let thumb = hand.thumb_tip;
    fill(255,0,0);
    let d = dist(index.x,index.y,thumb.x,thumb.y);
    if (d <= 20){
      fill (0,0,255);
      virtual_click = true;
    } else if ( d > 20) {
      virtual_click = false;
    }   
    noStroke();
    circle(index.x,index.y, 16);
    circle (thumb.x,thumb.y, 16); 
    // virtual_click=false
    pointerX_pos = (index.x + thumb.x)/2;
    pointerY_pos = (index.y + thumb.y)/2;
  }
  

These functions are then conveniently called inside the draw and setup functions.

Object Oriented Code : The class ‘Beyblade’ was made use of, which is custom built. Has attributes like spin speed, coordinates, states which return true or false,  and methods like checking for impact.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Beyblade{
constructor (pos1 , pos2 ,size , player=true){
this.xpos = pos1;
this.ypos = pos2;
this.size = size;
this.spin = 100;
this.special_move = false;
this.speedX = 2;
this.speedY = 1;
this.inside_stadium = true;
this.stadium_centerX = 316;
this.stadium_centerY = 200;
this.centerx = (pos1+size)/2;
this.centery = (pos2+size)/2;
this.isPlayer = player;
this.angle = 0;
}
draw_bey(){
if (mytitle === "Activated") {
this.special_move = true ;
}
if (this.special_move ===true & this.isPlayer == true) {
image (player_bey[0],this.xpos,this.ypos,this.size,this.size);
// rotate (this.angle);
this.angle = this.angle + this.speed;
} else if (this.special_move ===false & this.isPlayer == true) {
image (player_bey[1],this.xpos,this.ypos,this.size,this.size);
// rotate (this.angle);
this.angle = this.angle + this.speed;
} else {
image (opponent_bey,this.xpos,this.ypos,this.size,this.size);
// rotate (this.angle);
this.angle = this.angle - this.speed;
}
}
move_bey(){
if (this.isPlayer === true){
if (virtual_click == true){
this.xpos = pointerX_pos;
this.ypos = pointerY_pos;
}
} else if (this.isPlayer == false){
this.xpos = this.xpos + this.speedX;
this.ypos = this.ypos + this.speedY;
}
this.centerx = this.xpos + this.size / 2;
this.centery = this.ypos + this.size / 2;
}
check_impact(something) {
if (dist (this.centerx, this.centery, something.centerx, something.centery) <= this.size) {
if (virtual_click == false){
if (this.special_move == true) {
play_song(9);
this.xpos += 30;
this.ypos +=30;
something.spin = something.spin -10;
something.speedX = -something.speedX;
something.speedY = -something.speedY;
} else {
play_song(9);
this.xpos += 30;
this.ypos +=30;
something.spin = something.spin - 5;
something.speedX = -something.speedX;
something.speedY = -something.speedY;
}
} else {
this.xpos += 30;
this.ypos +=30;
this.spin = this.spin - 5;
play_song(9);
}
}
}
check_insideStadium(){
if (dist (this.stadium_centerX, this.stadium_centerY, this.centerx, this.centery ) > 200){
this.inside_stadium = false;
} else {
this.inside_stadium = true;
}
}
bounce_walls(){
let displ = dist(this.stadium_centerX, this.stadium_centerY, this.centerx, this.centery);
if (displ >= 200) { // 214 is the stadium radius
this.speedX = -this.speedX + random(-1.5, 1.5);
this.speedY = -this.speedY + random(-1.5, 1.5);
// Reverse speed direction
console.log("Beyblade bounced!");
}
}
class Beyblade{ constructor (pos1 , pos2 ,size , player=true){ this.xpos = pos1; this.ypos = pos2; this.size = size; this.spin = 100; this.special_move = false; this.speedX = 2; this.speedY = 1; this.inside_stadium = true; this.stadium_centerX = 316; this.stadium_centerY = 200; this.centerx = (pos1+size)/2; this.centery = (pos2+size)/2; this.isPlayer = player; this.angle = 0; } draw_bey(){ if (mytitle === "Activated") { this.special_move = true ; } if (this.special_move ===true & this.isPlayer == true) { image (player_bey[0],this.xpos,this.ypos,this.size,this.size); // rotate (this.angle); this.angle = this.angle + this.speed; } else if (this.special_move ===false & this.isPlayer == true) { image (player_bey[1],this.xpos,this.ypos,this.size,this.size); // rotate (this.angle); this.angle = this.angle + this.speed; } else { image (opponent_bey,this.xpos,this.ypos,this.size,this.size); // rotate (this.angle); this.angle = this.angle - this.speed; } } move_bey(){ if (this.isPlayer === true){ if (virtual_click == true){ this.xpos = pointerX_pos; this.ypos = pointerY_pos; } } else if (this.isPlayer == false){ this.xpos = this.xpos + this.speedX; this.ypos = this.ypos + this.speedY; } this.centerx = this.xpos + this.size / 2; this.centery = this.ypos + this.size / 2; } check_impact(something) { if (dist (this.centerx, this.centery, something.centerx, something.centery) <= this.size) { if (virtual_click == false){ if (this.special_move == true) { play_song(9); this.xpos += 30; this.ypos +=30; something.spin = something.spin -10; something.speedX = -something.speedX; something.speedY = -something.speedY; } else { play_song(9); this.xpos += 30; this.ypos +=30; something.spin = something.spin - 5; something.speedX = -something.speedX; something.speedY = -something.speedY; } } else { this.xpos += 30; this.ypos +=30; this.spin = this.spin - 5; play_song(9); } } } check_insideStadium(){ if (dist (this.stadium_centerX, this.stadium_centerY, this.centerx, this.centery ) > 200){ this.inside_stadium = false; } else { this.inside_stadium = true; } } bounce_walls(){ let displ = dist(this.stadium_centerX, this.stadium_centerY, this.centerx, this.centery); if (displ >= 200) { // 214 is the stadium radius this.speedX = -this.speedX + random(-1.5, 1.5); this.speedY = -this.speedY + random(-1.5, 1.5); // Reverse speed direction console.log("Beyblade bounced!"); } }
class Beyblade{
  constructor (pos1 , pos2 ,size , player=true){
    this.xpos = pos1;
    this.ypos = pos2;
    this.size = size;
    this.spin = 100;
    this.special_move = false;
    this.speedX = 2;
    this.speedY = 1;
    this.inside_stadium = true;
    this.stadium_centerX = 316;
    this.stadium_centerY = 200;
    this.centerx = (pos1+size)/2;
    this.centery = (pos2+size)/2;
    this.isPlayer = player;
    this.angle = 0;
  }
  
  
  draw_bey(){
    if (mytitle === "Activated") {
        this.special_move = true ;
        } 
    
    if (this.special_move ===true & this.isPlayer == true) {
      image (player_bey[0],this.xpos,this.ypos,this.size,this.size);
      
      // rotate (this.angle);
      this.angle = this.angle + this.speed;
    } else if (this.special_move ===false & this.isPlayer == true) {
               image (player_bey[1],this.xpos,this.ypos,this.size,this.size);
                 // rotate (this.angle);
                this.angle = this.angle + this.speed;
               } else {
                  image (opponent_bey,this.xpos,this.ypos,this.size,this.size);
                  // rotate (this.angle);
                this.angle = this.angle - this.speed;
               }
  }
  
  move_bey(){
    if (this.isPlayer === true){
      if (virtual_click == true){
        this.xpos = pointerX_pos;
        this.ypos = pointerY_pos;
      }
    } else if (this.isPlayer == false){
      this.xpos = this.xpos + this.speedX;
      this.ypos = this.ypos + this.speedY;
    }
      this.centerx = this.xpos + this.size / 2;
      this.centery = this.ypos + this.size / 2;
  }
  
  check_impact(something) {
    if (dist (this.centerx, this.centery, something.centerx, something.centery) <= this.size) {
      if (virtual_click == false){
        if (this.special_move == true) {
          play_song(9);
          this.xpos += 30;
          this.ypos +=30;
          something.spin = something.spin -10;
          something.speedX = -something.speedX;
          something.speedY = -something.speedY;
        } else {
          play_song(9);
          this.xpos += 30;
          this.ypos +=30;
          something.spin = something.spin - 5;
          something.speedX = -something.speedX;
          something.speedY = -something.speedY;
        }
      } else {
        this.xpos += 30;
        this.ypos +=30;
        this.spin = this.spin - 5;
        play_song(9);
        
        
      }
    }
  }
  
  check_insideStadium(){
    if (dist (this.stadium_centerX, this.stadium_centerY, this.centerx,  this.centery ) > 200){
      this.inside_stadium = false;
    } else {
      this.inside_stadium = true;
    }
  }
  
  bounce_walls(){
  
   let displ = dist(this.stadium_centerX, this.stadium_centerY, this.centerx, this.centery);
  
  if (displ >= 200) { //  214 is the stadium radius
    this.speedX = -this.speedX + random(-1.5, 1.5); 
    this.speedY = -this.speedY + random(-1.5, 1.5);
    
    // Reverse speed direction
    
    console.log("Beyblade bounced!");
  }
    
  }

Training the Model: There were series of steps taken to get both the classifiers to get to work. First, I followed some tutorials online and referred to the Ml5.js documentations to get the hand classifier to work. I set the distance between the two fingers to detect when to consider the click and when not to. Daniel Schiffman’s videos were helpful, but a bit outdated.

Ali being Ali

Got the pointers to work

After setting up the Hand-classifier, I inverted the video and drew in the backside of the canvas. As for the sound classifier, I had to go to Teachable Machine by Google to train a model for detecting specific words for game-control. Due to time constraints, this was more convenient way of training the model, as opposed to me training my own.

I had to train it for background noise and the special phrase that triggers the special move and changes the color and image of the beyblade.

Tuning for desired results.

Finally, the trained model was exported, and was implemented inside the sketch.js and html file to make it able to run.

 

Areas of improvement:

I am proud of the game as of now. It is interactive, helps me to relive my childhood memories, and share this joy with others. However, I wanted to add more levels, ability to change the settings and select different beyblades. This I will add in the future. As far as the game logic goes, I would like to work on the collision physics for the objects. I would like to tumble, roll and push back them forth even more, to make it even more realist.

Overall, I am proud of my work, and how the game turned out to be!

Midterm Progress – ML powered Beyblade game

Introduction:

For this midterm project, I wanted to design something different. Something unique. Something meaningful to me in a way that I get to represent my perspective when it comes  interactivity and design.  Most of the games that I have played growing up, have been AAA gaming titles (top-notch graphics intensive games) on my PSP (Playstation Portable).

Is $75 a good price for a light blue Psp 2000? : r/PSP

Be it Grand Theft Auto or God of War, I have played them all. However, if there is one game that I never got a chance to play on my PSP due to it not being released for that gaming platform , was Beyblade V-Force! It was available on Nintendo Go and other previous generation devices, but wasn’t there for the newer ‘PSP’ that I owned.  I till this date, love that cartoon series. Not only was and am a loyal fan of the show, but I have collected most of the toy models from the show as well.

What's your problems with vforce : r/Beyblade

Brainstorming ideas + User interaction and game design:

This project started off with me wondering what is that one thing dearer to me. After spending an hour just short listing topics that I was interested in , I ended up with nothing. Not because I couldn’t think of any, but because I couldn’t decide upon the one single game. I started this project with a broken hand. My left hand is facing some minor issues, and due to which, I cannot type of hold on to things with the left hand. Particularly my thumb. This made me realize that not only does it make it difficult to program the game, but also to play it as well. My misery made me conscious of users who may struggle with the conventional controls offered typically in the gaming environment : a joystick and some buttons. It made me wonder what can I do different in my case and make this game more accessible to people like me who find it difficult to use a tangible medium to interact with the program. Hence I decided to use hand-tracking technology and sound classification. There is this whole buzz around A.I and I thought why not use a machine learning library to enhance and workout my project. Yet still, I couldn’t finalize a topic or genre to work on.

At first, I came up with the idea of a boxing game. Using head-tracking and hand tracking, the user will enter a face-off with the computer, where he/she will have to dodge by moving head left or right to move in the corresponding direction. To hit, they will have to close hand and move their whole arm to land a punch.

Basic Layout visualized.

Flow chart of basic logic construct

I drafted the basic visuals and what I wanted it to look like, but then as I started to work, I realized that violence is being portrayed and is not but un-befitting for an academic setting. Moreover, I wasn’t in a mood to throw punch and knock my laptop down, since I am a bit clumsy person. This was when my genius came into being. 1 day before, I decided to scrap the whole work and get started again on a new project. This time, it is what I love the most. You guessed it right – it is beyblade!

Basic gameplay features:

The whole idea revolves around two spinning metal tops with a top view,  rotating in a stadium. These tops hit one another, and create impact. This impact either slows them down, or knocks one out , and even sometimes both out of the stadium. The first one to get knocked out or stops spinning loses, and the other one wins.  I wanted to implement this, and give user the ability to control their blades using hand gesture. The user will be able to move their blade around using their index finger and thumb pointing and moving in the direction they would like their blade to move. The catch however, is that only when the thumb and index finger are closed, only then you will be able to move the blade, and to attack, only when your thumb and index finger are not touching, only then will you be able to attack and inflict damage on opponent. To save yourself from damage, you either dodge, or keeping fingers opened. These control constructs are unique and are not implemented in any game design of similar genre or nature before. I came up with this, because I cannot properly grab using my left thumb and index finger, and hence wanted to use them in the game.

Game states

I have decided to use states to transition between the menu, instruction screen, gameplay , showing win/lose screen, and returning back to the menu. This makes it convenient to use the modular code and use it inside the ‘draw’ function.

Legal Stadiums-[bc]These are the legal tournament stadiums that bladers can agree to battle on in Official Ranked Battles.[
Stadium  used in the game.
      • ML5:

        To make the controls workout, I will have to rely on Ml5.js. ML5 allows machine learning for web-based  projects. In  my case, I will be making use of handPose and soundClassifier modules, which happen to be pre-trained models. Hence, I won’t have to go through the hassle of training a model for the game.

 

handPose keypoints diagram

Using the key points 4 and 8, and by mapping out their relative distance, I plan on tracking and using these to return boolean values, which will be used to control the movement of the blade. I referred to coding-train’s youtube Chanel as well to learn about it, and implement it into my code.

I am yet to figure out how to use sound-classification, but will post in my final project presentation post.

 Code (functions, classes, interactivity):

Class and Objects – Pseudo code.

Though I am yet to code, and due to limited mobility , my progress has been slowed. Nonetheless, I sketched out the basic  class and constructor function for both objects (i.e player and the opponent). Each blade will have speed, position in vertical and horizontal directions, as well as methods such as displaying and moving the blade. To check for damage and control the main layout of the game, if and else statements will be used in a separate function, will then be called inside the ‘draw function’.

Complex and trickiest part:

The trickiest part is the machine learning integration. During my tests, hand gesture works, but despite training the sound classifier, it still doesn’t return true, which will be used to trigger the signature move. Moreover, I want there to be a delay between user’s hand -gesture and the beyblade’s movement in that particular direction. This  implementation of ‘rag doll’ physics is what is to  be challenging.

Tackling the problems and risks:

To minimize the mess in the p5 coding environment, I am defining and grouping related algorithmic pattern into individual functions, as opposed to dumping them straightaway into the ‘draw’ function. This helps me in keeping the code organized and clean, and allows me to re-use it multiple times. Secondly, using Ml5.js was a bit risky, since this hasn’t been covered in the class, and the tutorial series does require time and dedication to it. Cherry on top, limited hand mobility put me at disadvantage. Yet still, I decided to go with this idea, since I want it to simply be unique. Something which makes the player play the game again. To make this possible, I am integrating the original sound-track from the game, and am using special effects such as upon inflicting damage.  Asides from the theming, I did face an issue with letting the user know wether their hand is being tracked or not in the first place. To implement it, I simply layers the canvas on top of the video, which solved the issue for me. As of now, I am still working on it from scratch , and will document further issues and fixes in the final documentation for this mid-term project.

 

Midterm Project – Balloon Popper

Ever been at a wedding or party, locked in deep conversation, when suddenly—a balloon drifts by, practically begging to be popped? You hesitate. Would it be childish? Would people stare? Well, worry no more! Balloon Popper is here to satisfy that irresistible urge. No awkward glances, no judgment—just pure, unfiltered balloon-bursting joy.

Project Overview

Balloon Popper is a simple game that I worked on, though not always with ease. The objective is to pop balloons as they fall from the top of the canvas using a shooter that fires “bullets” from the bottom. The game consists of a start page, a game environment, and a finish page. It utilizes classes to define key elements such as the shooter, bullets, and balloons, each with specific properties that support collision detection, interaction, and display functions.

Implementation

The game begins with a start page where the player can view the instructions by clicking the instructions button or start the game by pressing the start button. Once the game starts, the player must shoot the balloons before they reach the bottom of the canvas. The player moves left and right along four paths using the arrow keys and fires bullets using the spacebar.

At the top of the canvas, three score trackers display the number of lives, balloons popped, and stray bullets. The lives counter tracks how many balloons have passed the bottom of the canvas, while the stray bullets counter keeps track of bullets that were fired but missed the balloons. If either the lives or stray bullets count reaches five, the game ends. The number of balloons popped is also displayed, but there is no specific win condition—the player can continue playing and challenge themselves to improve their score.

When the game ends, the finish page appears, displaying the reason for the loss, whether due to missing five balloons or using up all stray bullet allowances. The end screen also includes a restart button, allowing the player to reset the game and play again.

Code Highlights

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function displayTextBox(textStr, x, y, textColor) {
textSize(16);
textStyle(BOLD);
textFont("Poppins");
let textWidthValue = textWidth(textStr);
let padding = 5;
fill(255, 255, 255, 200);
rect(x - padding, y - 15, textWidthValue + 10, 20, 5);
fill(textColor);
textAlign(LEFT);
text(textStr, x, y);
}
function displayTextBox(textStr, x, y, textColor) { textSize(16); textStyle(BOLD); textFont("Poppins"); let textWidthValue = textWidth(textStr); let padding = 5; fill(255, 255, 255, 200); rect(x - padding, y - 15, textWidthValue + 10, 20, 5); fill(textColor); textAlign(LEFT); text(textStr, x, y); }
function displayTextBox(textStr, x, y, textColor) {
  textSize(16);
  textStyle(BOLD);
  textFont("Poppins");
  let textWidthValue = textWidth(textStr);
  let padding = 5;

  fill(255, 255, 255, 200);
  rect(x - padding, y - 15, textWidthValue + 10, 20, 5);

  fill(textColor);
  textAlign(LEFT);
  text(textStr, x, y);
}

I particularly love the code snippet above that defines how I displayed the score trackers at the top of the canvas.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
isHit(bullet) {
return dist(this.x, this.y, bullet.x, bullet.y) < this.r / 2;
}
}
isHit(bullet) { return dist(this.x, this.y, bullet.x, bullet.y) < this.r / 2; } }
  isHit(bullet) {
    return dist(this.x, this.y, bullet.x, bullet.y) < this.r / 2;
  }
}

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
for (let i = balloons.length - 1; i >= 0; i--) {
balloons[i].update();
balloons[i].display();
for (let j = bullets.length - 1; j >= 0; j--) {
if (balloons[i].isHit(bullets[j])) {
playPopSound();
balloonsPopped++;
balloons[i].reset();
bullets.splice(j, 1);
break;
}
}
for (let i = balloons.length - 1; i >= 0; i--) { balloons[i].update(); balloons[i].display(); for (let j = bullets.length - 1; j >= 0; j--) { if (balloons[i].isHit(bullets[j])) { playPopSound(); balloonsPopped++; balloons[i].reset(); bullets.splice(j, 1); break; } }
for (let i = balloons.length - 1; i >= 0; i--) {
  balloons[i].update();
  balloons[i].display();

  for (let j = bullets.length - 1; j >= 0; j--) {
    if (balloons[i].isHit(bullets[j])) {
      playPopSound();
      balloonsPopped++;
      balloons[i].reset();
      bullets.splice(j, 1);
      break;
    }
  }

The above two code snippets show how to detect collision between the bullet and the balloon.

Challenges and Areas for Improvement

One of the main challenges I faced was styling the game, including selecting colors, font sizes, and text styles. I realized that I need to work on improving my design skills. Loading the balloon images was another issue, but after consulting resources such as Stack Overflow and AI tools, I was able to find a solution and successfully display the images.

This project has been a great learning experience, and I plan to build on what I’ve learned. In the future, I want to improve button management, refine the game’s sound integration, and enhance the overall visual design to make the game even more engaging. Additionally, I would like to optimize the collision detection and fine-tune the difficulty scaling to provide a more dynamic challenge for players.

ENJOY;

Midterm :)

Project Concept

This project is a simple point and click game based on SpongeBob’s world. The player starts outside SpongeBob, Patrick, and Squidward’s houses and can click on them to enter different rooms. Each room has its own design and sometimes interactive elements like sounds, books, or TV screens. The goal is to explore different parts of their world and experience small interactions.

link to the sketch: https://editor.p5js.org/flipflops/full/Z69RYq4v1

To make the game feel more personal, I drew all the rooms and the background instead of using images online. I tried making it so that all the drawings blend well with the objects I coded, so everything feels like part of the same world:

How it works

The game constantly checks where the user is and updates the background based on their location. When the player clicks on a house, the game detects which house was clicked and then changes the background to the correct room image. Some rooms also have extra interactive features, like playing specific sounds or changing visuals when clicked.

For example, the TV room cycles through different screens each time the player clicks, making it feel like the TV is actually changing channels. The book room has different pages that the player can flip through, and each page includes a funny SpongeBob quote. The game also manages background music, so when the player moves between different rooms, it plays the correct theme without overlapping sounds. Another thing I focused on was making sure the back button always works properly, so the player can return outside without any weird glitches.

Code highlight

One of the coolest parts that I’m proud about is the clarinet room, where the player can actually play Squidward’s clarinet. There are seven different notes, and clicking on different parts of the clarinet will play them. It’s like a mini music feature inside the game, and it makes Squidward’s room more interesting instead of just being another background.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function checkClarinetHoleClick(mx, my) {
// all the clarinet holes coordinates (left to right) stored in array
let clarinetHoles = [
{ x: 174, y: 153 },
{ x: 190, y: 153 },
{ x: 205, y: 153 },
{ x: 218, y: 153 },
{ x: 245, y: 152 },
{ x: 260, y: 152 },
{ x: 274, y: 152 },
];
for (let i = 0; i < clarinetHoles.length; i++) {
//distance between mouse click and centre of each hole
let d = dist(mx, my, clarinetHoles[i].x, clarinetHoles[i].y);
if (d < 5) { // within 5 pixels
// hole clicked
playClarinetSound(i); // play note
break;
}
}
}
function checkClarinetHoleClick(mx, my) { // all the clarinet holes coordinates (left to right) stored in array let clarinetHoles = [ { x: 174, y: 153 }, { x: 190, y: 153 }, { x: 205, y: 153 }, { x: 218, y: 153 }, { x: 245, y: 152 }, { x: 260, y: 152 }, { x: 274, y: 152 }, ]; for (let i = 0; i < clarinetHoles.length; i++) { //distance between mouse click and centre of each hole let d = dist(mx, my, clarinetHoles[i].x, clarinetHoles[i].y); if (d < 5) { // within 5 pixels // hole clicked playClarinetSound(i); // play note break; } } }
function checkClarinetHoleClick(mx, my) {
  // all the clarinet holes coordinates (left to right) stored in array
  let clarinetHoles = [
    { x: 174, y: 153 },
    { x: 190, y: 153 },
    { x: 205, y: 153 },
    { x: 218, y: 153 },
    { x: 245, y: 152 },
    { x: 260, y: 152 },
    { x: 274, y: 152 },
  ];

  for (let i = 0; i < clarinetHoles.length; i++) {
    //distance between mouse click and centre of each hole
    let d = dist(mx, my, clarinetHoles[i].x, clarinetHoles[i].y);
    if (d < 5) { // within 5 pixels
      // hole clicked
      playClarinetSound(i); // play note
      break;
    }
  }
}

 

Improvement

One issue I ran into was getting the highlight effect to work when hovering over objects. I wanted players to see which items they could interact with, but at first, the effect wasn’t showing up correctly. After some testing, I made sure the game checks the mouse position and only highlights objects when the cursor is over them.

In the future, I would like to add more interactive elements, like clickable objects inside rooms that trigger animations or extra dialogues. Right now, the game works fine, but adding more details would make it feel more complete. Also, the game doesn’t have a clear goal, so maybe adding a small mission or hidden secrets to find would make it more fun.

Midterm Project – Cat Rescue Game

 

Midterm Project – Cat Rescue Game

For my midterm project, I was very excited to create a game based on something that I love, cats! As I have grown up and lived in Abu Dhabi for half my life, I have noticed that there are a lot of stray cats. So with this in mind, I wanted to design a game where the player walks around a city, rescues stray cats, and takes them to a shelter. I was inspired by a photography project I completed last semester about the spirit of street cats in Abu Dhabi. I went around the city in Abu Dhabi and captured these cats’ lives and the environment they are in. (link to the photos). The game combines movement mechanics, object interactions, and a simple pet care system. The goal of the game is to rescue and rehome all the stray cats in order to win the game. I started this midterm project by drawing a rough sketch of how I wanted it to look like, and the various screens I wanted to implement. 

Link to sketch:

Midterm Sketch

 

Final Game:

Link to final game

Essentially, my game consists of three main screens, the start, game, and restart screen. I wanted a specific look for my background that I was not able to find online, so I decided to create my own. For both the start screen and the game background screen, I used Illustrator to make various shapes and used some images like trees or garbage bins. Then I converted these to PNG files so that I was able to upload them into p5.js and use them in my code. For the shelter, I used different shapes to make it look like a house. Additionally, I made the start screen display a box with on-screen text explaining how to play the game. Similarly, the restart screen also has a circle with on-screen text showing that you won, and a button to restart. For the interaction aspect of my project, I implemented a way for the player to control the girl using the arrow keys on the keyboard to move around. The stray cats are located at random locations in the city and if the girl touches a cat, it is rescued and sent to a shelter. I also used happy, chill background music that I found fitting to the game that starts once you press “Start Game”.

Overall I am extremely satisfied with the way that my final game turned out. Something I am particularly proud of is that instead of using pre-made designs from the internet, I designed my own backgrounds in Illustrator, which gives my game the unique quality I was seeking. Also, I am proud that I was able to successfully implement a collision detection system that ensures that once the player rescues a cat, it gets added to the shelter. I also liked the three-screen system (Start → Game → Restart) which provides the game with a clear structure. Lastly, I am extremely proud that I was able to draw inspiration from my real-life experiences in Abu Dhabi, which makes the game more unique and personal to me. I was able to connect my previous photography project and this game, linking different creative disciplines. I like that my game has an underlying story about spreading awareness about stray cats, which adds an emotional layer to my project.

 

Car class (which represents the girl’s movement):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Car {
constructor() {
this.x = width / 2;
this.y = height - 80;
this.size = 50;
}
move() {
if (keyIsDown(LEFT_ARROW)) this.x -= 5;
if (keyIsDown(RIGHT_ARROW)) this.x += 5;
if (keyIsDown(UP_ARROW)) this.y -= 5;
if (keyIsDown(DOWN_ARROW)) this.y += 5;
// Keep girl inside canvas
this.x = constrain(this.x, 0, width - this.size);
this.y = constrain(this.y, 0, height - this.size);
}
display() {
image(carImage, this.x, this.y, 60, 100);
}
class Car { constructor() { this.x = width / 2; this.y = height - 80; this.size = 50; } move() { if (keyIsDown(LEFT_ARROW)) this.x -= 5; if (keyIsDown(RIGHT_ARROW)) this.x += 5; if (keyIsDown(UP_ARROW)) this.y -= 5; if (keyIsDown(DOWN_ARROW)) this.y += 5; // Keep girl inside canvas this.x = constrain(this.x, 0, width - this.size); this.y = constrain(this.y, 0, height - this.size); } display() { image(carImage, this.x, this.y, 60, 100); }
class Car {
  constructor() {
    this.x = width / 2;
    this.y = height - 80;
    this.size = 50;
  }

  move() {
    if (keyIsDown(LEFT_ARROW)) this.x -= 5;
    if (keyIsDown(RIGHT_ARROW)) this.x += 5;
    if (keyIsDown(UP_ARROW)) this.y -= 5;
    if (keyIsDown(DOWN_ARROW)) this.y += 5;

    // Keep girl inside canvas
    this.x = constrain(this.x, 0, width - this.size);
    this.y = constrain(this.y, 0, height - this.size);
  }

  display() {
    image(carImage, this.x, this.y, 60, 100);
  }

 

Throughout developing this game, I faced a few challenges that required me to problem-solve. One of the issues was ensuring that the text box and circle were layered properly over the background so they remained visible. It took me some time to adjust their positioning and layering so that they were correctly visible on the screen. Another challenge I came across was making sure that the girl’s movement worked smoothly in all directions (up, down, left, and right). Debugging this movement system took some trial and error, but I was able to fix it. Additionally, I ran into smaller, frustrating issues, like file name errors, for example, a missing capital letter caused images not to load properly. Debugging these minor but important details taught me the importance of careful file management. However, I was able to overcome these challenges and successfully build a functioning game. 

If I had more time, there are a few features I would have liked to add to enhance the user experience. One idea was to expand on the rescue aspect, right now, when the cats reach the shelter, they are considered rescued. However, I would have liked to add an extra step where the player needs to care for the cats (feeding them, treating injuries, etc.) before they are fully rescued. Another improvement would be to introduce different difficulty levels (Easy, Medium, Hard) by adding obstacles that the player must overcome to rescue the cats. Also, smaller interactive details, like the cats meowing when clicked, would also add to the game. Even without these extra features, I am really happy with how my game turned out. This project challenged me to problem-solve, debug, and think creatively, and I’m proud of what I was able to accomplish.

 

Music citation:

Music track: Marshmallow by Lukrembo

Source: https://freetouse.com/music

Midterm Project

Concept

My midterm project is an escape room game where the players must follow a series of clues to find different objects around the room. The game begins with a hint that directs the player to the first object. An once it is found, the object reveals a number that the player must remember, as it is part of the final password. The object also provides the next clue, leading the player to the next item in the sequence. This process continues until all objects have been discovered, all of them giving a number needed for the final passcode. To ensure the correct sequence, my game has an ordered clicking type of logic, this means the players can only interact with objects in a specific order. If they attempt to click on an object out of sequence, it will not respond, preventing them from skipping ahead or guessing the passcode incorrectly. This makes sure the players follow the clues and remember the numbers in the right order, so that they can successfully input the password at the end to escape.

link to sketch: https://editor.p5js.org/tfr9406/full/1UNobzqQo

Code Highlights

I think a very important part of my game’s code is the ordered clicking system, which makes sure players find clues in the right order, and prevents them from skipping ahead . To do this I made it so that the code tracks the order of  the interactions using clickOrder and marks visited clues in the visitedClues object. This means, each clue can only be accessed if the previous one has been clicked. For example, the clock must be clicked first (clickOrder === 0) before moving to clue1, then the computer unlocks clue2, and so on until the final clue, the painting, leads to the password page.

I also made it so that the players can revisit clue while keeping the correct order. This is important because it allows players to go back and see the clue again without breaking the structured order. The visitedClues object keeps track of which clues have already been discovered, making sure that once a clue is unlocked, it remains accessible even if the player navigates away and returns. For example, once the player clicks on the clock, visitedClues.clue1 is set to true, meaning they can go back to it at any time. However, they can’t jump ahead to later clues unless they follow the intended order.  This is the code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (currentPage === "game") {
// checks for valid order before allowing to open next clue page
if (clickOrder === 0 && clock.isClicked(mouseX, mouseY)) {
currentPage = "clue1";
clickOrder = 1; // clock clicked first
visitedClues.clue1 = true; // mark clue1 as visited
} else if (clickOrder === 1 && computer.isClicked(mouseX, mouseY)) {
currentPage = "clue2";
clickOrder = 2; // computer clicked second
visitedClues.clue2 = true; // mark clue2 as visited
} else if (clickOrder === 2 && cupboard.isClicked(mouseX, mouseY)) {
currentPage = "clue3";
clickOrder = 3; // cupboard clicked third
visitedClues.clue3 = true; // mark clue3 as visited
} else if (clickOrder === 3 && books.isClicked(mouseX, mouseY)) {
currentPage = "clue4";
clickOrder = 4; // books clicked fourth
visitedClues.clue4 = true; // mark clue4 as visited
} else if (clickOrder === 4 && painting.isClicked(mouseX, mouseY)) {
currentPage = "password"; // move to password page after painting
}
if (currentPage === "game") { // checks for valid order before allowing to open next clue page if (clickOrder === 0 && clock.isClicked(mouseX, mouseY)) { currentPage = "clue1"; clickOrder = 1; // clock clicked first visitedClues.clue1 = true; // mark clue1 as visited } else if (clickOrder === 1 && computer.isClicked(mouseX, mouseY)) { currentPage = "clue2"; clickOrder = 2; // computer clicked second visitedClues.clue2 = true; // mark clue2 as visited } else if (clickOrder === 2 && cupboard.isClicked(mouseX, mouseY)) { currentPage = "clue3"; clickOrder = 3; // cupboard clicked third visitedClues.clue3 = true; // mark clue3 as visited } else if (clickOrder === 3 && books.isClicked(mouseX, mouseY)) { currentPage = "clue4"; clickOrder = 4; // books clicked fourth visitedClues.clue4 = true; // mark clue4 as visited } else if (clickOrder === 4 && painting.isClicked(mouseX, mouseY)) { currentPage = "password"; // move to password page after painting }
if (currentPage === "game") {
  // checks for valid order before allowing to open next clue page
  if (clickOrder === 0 && clock.isClicked(mouseX, mouseY)) {
    currentPage = "clue1";
    clickOrder = 1; // clock clicked first
    visitedClues.clue1 = true; // mark clue1 as visited
  } else if (clickOrder === 1 && computer.isClicked(mouseX, mouseY)) {
    currentPage = "clue2";
    clickOrder = 2; // computer clicked second
    visitedClues.clue2 = true; // mark clue2 as visited
  } else if (clickOrder === 2 && cupboard.isClicked(mouseX, mouseY)) {
    currentPage = "clue3";
    clickOrder = 3; // cupboard clicked third
    visitedClues.clue3 = true; // mark clue3 as visited
  } else if (clickOrder === 3 && books.isClicked(mouseX, mouseY)) {
    currentPage = "clue4";
    clickOrder = 4; // books clicked fourth
    visitedClues.clue4 = true; // mark clue4 as visited
  } else if (clickOrder === 4 && painting.isClicked(mouseX, mouseY)) {
    currentPage = "password"; // move to password page after painting
  }

To get to this final code, I first tested it out with a simpler object and basic logic to make sure the ordered clicking system worked correctly. Initially, I used a minimal setup with just 3 clickable elements and basic variables to track whether an item had been clicked. This helped me confirm that the logic for the sequential interactions was working before expanding it to include the full set of clues and the ability to revisit them. Below was the initial code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function mousePressed() {
if (!rectClicked && mouseX > 50 && mouseX < 130 && mouseY > 100 && mouseY < 150) { //first rectangle is clicked
rectClicked = true;
} else if (rectClicked && !triClicked && mouseX > 170 && mouseX < 230 && mouseY > 100 && mouseY < 150) { //triangle clicked true if rectangle clicked first
triClicked = true;
} else if (rectClicked && triClicked && !circClicked && dist(mouseX, mouseY, 320, 125) < 25) {//circle clicked true if rectangle and triangle clicked before
circClicked = true;
escape = true; //clicking circle = players escapes
}
}
function mousePressed() { if (!rectClicked && mouseX > 50 && mouseX < 130 && mouseY > 100 && mouseY < 150) { //first rectangle is clicked rectClicked = true; } else if (rectClicked && !triClicked && mouseX > 170 && mouseX < 230 && mouseY > 100 && mouseY < 150) { //triangle clicked true if rectangle clicked first triClicked = true; } else if (rectClicked && triClicked && !circClicked && dist(mouseX, mouseY, 320, 125) < 25) {//circle clicked true if rectangle and triangle clicked before circClicked = true; escape = true; //clicking circle = players escapes } }
function mousePressed() {
  if (!rectClicked && mouseX > 50 && mouseX < 130 && mouseY > 100 && mouseY < 150) { //first rectangle is clicked
    rectClicked = true;
  } else if (rectClicked && !triClicked && mouseX > 170 && mouseX < 230 && mouseY > 100 && mouseY < 150) { //triangle clicked true  if rectangle clicked first
    triClicked = true;
  } else if (rectClicked && triClicked && !circClicked && dist(mouseX, mouseY, 320, 125) < 25) {//circle clicked true if rectangle and triangle clicked before
    circClicked = true;
    escape = true; //clicking circle = players escapes
  }
}

Challenges

For my game I knew I wanted to implement hover animation to improve overall user experience by providing feedback. However, this was tricky at first because my game page was based on images rather than shapes. Unlike with shapes, where I could easily change the fill color on hover, I had to find a way to replace the whole image itself to give that same visual feedback. To solve this, I created an if-else condition that checks the mouse’s position relative to the designated area for hover. I then updated the image only if the mouse is hovering within the defined boundaries of the clickable area, and also made sure that when the hover condition is not met, the image reverts to its original state, which prevented it from being stuck in the wrong image.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// hover effect
function handlePageHoverEffects() {
if (currentPage === "landing") {
// hover for Landing image (switches to landing2 on hover)
if (mouseX >= 346 && mouseX <= 468 && mouseY >= 337 && mouseY <= 403) {
currentImage = landing2; // switch to landing2 on hover
} else {
currentImage = landing1; // switch back to landing1 otherwise
}
// hover effect function handlePageHoverEffects() { if (currentPage === "landing") { // hover for Landing image (switches to landing2 on hover) if (mouseX >= 346 && mouseX <= 468 && mouseY >= 337 && mouseY <= 403) { currentImage = landing2; // switch to landing2 on hover } else { currentImage = landing1; // switch back to landing1 otherwise }
// hover effect 
function handlePageHoverEffects() {
  if (currentPage === "landing") {
    
    // hover for Landing image (switches to landing2 on hover)
    if (mouseX >= 346 && mouseX <= 468 && mouseY >= 337 && mouseY <= 403) {
      currentImage = landing2; // switch to landing2 on hover
    } else {
      currentImage = landing1; // switch back to landing1 otherwise
    }

Improvement

Some of the improvements I could make to the game could maybe include adding different rooms or clues, which would provide more variety and depth to the game. Additionally, introducing difficulty levels would also make the game more accessible to a wider audience. For example, a beginner level with simple clues , and then progressively harder levels with more difficult riddles, hidden objects, and tougher challenges.

 

 

Midterm Project – Traffic Dash

Concept

The main idea was to create a fun, fast paced game which would be very fun and challenging at the same time. The game is in the “endless runner” genre of games (if you can avoid traffic for that long) where the main objective is to survive as long as possible and collect as many coins as you can therefor raising your score.

The game was inspired by other popular endless runner games such as “Subway Surfers” but with a progressive difficulty curve to make the game more difficult. The game also has some strategizing as sometimes the coins can lead you into a trap with no way out and a quick game over!

What are you waiting for, click here and try to dash through the traffic!

How the game works and code parts I am proud of

The game, of course, starts with an instructions screen where the player is asked to press enter to begin the game. As the game starts the player can see that the left and the right arrows control the movement of the car on the bottom of the screen. After a bit cars start spawning on top of the screen, randomly, but on carefully selected coordinates to ensure they don’t spawn off the screen or off the road (some traffic laws can’t be broken).

The player then sees coins appearing also randomly, but moving slower than the cars to ensure the user can pick them up safely. The user needs to decide if it is safe enough to move left or right to collect the coin and be sure to have enough time to move away from oncoming traffic.

Even though the goal is to collect as many coins as possible, as the game progresses, and the player is collecting coins and making their score higher, the speed of the oncoming traffic increases making the game much more difficult, but also making the distance values move faster which can make the survival more rewarding for the high score. The speed of the traffic is maxed at 25 so the game doesn’t become unplayable.

When the user eventually hits the oncoming traffic, they are met with a game over screen displaying the score they achieved in the last run with the text asking the user to press enter if they want to retry and beat the last score.

I am very proud with the collision detection system, which in the beginning did face some challenges (will talk more about it later in the documentation) but now works perfectly!

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//Handling collision detection
function checkCollisions() {
for (let t of trafficCars) {
if (
car.x < t.x + t.w &&
car.x + car.w > t.x &&
car.y < t.y + t.h &&
car.y + car.h > t.y
) {
// print("Collision with traffic!");
crash.play();
game_over = true;
displayGameOverScreen();
noLoop();
}
}
if (
car.x < coin.x + coin.w &&
car.x + car.w > coin.x &&
car.y < coin.y + coin.h &&
car.y + car.h > coin.y
) {
// print("Coin collected!");
coin_collect.setVolume(0.3);
coin_collect.play();
score += 1;
coin.y = random(-300, 0);
coin.x = random(coin_spawn_points);
}
}
//Handling collision detection function checkCollisions() { for (let t of trafficCars) { if ( car.x < t.x + t.w && car.x + car.w > t.x && car.y < t.y + t.h && car.y + car.h > t.y ) { // print("Collision with traffic!"); crash.play(); game_over = true; displayGameOverScreen(); noLoop(); } } if ( car.x < coin.x + coin.w && car.x + car.w > coin.x && car.y < coin.y + coin.h && car.y + car.h > coin.y ) { // print("Coin collected!"); coin_collect.setVolume(0.3); coin_collect.play(); score += 1; coin.y = random(-300, 0); coin.x = random(coin_spawn_points); } }
//Handling collision detection
function checkCollisions() {
  for (let t of trafficCars) {
    if (
      car.x < t.x + t.w &&
      car.x + car.w > t.x &&
      car.y < t.y + t.h &&
      car.y + car.h > t.y
    ) {
      // print("Collision with traffic!");
      crash.play();
      game_over = true;
      displayGameOverScreen();
      noLoop();
    }
  }

  if (
    car.x < coin.x + coin.w &&
    car.x + car.w > coin.x &&
    car.y < coin.y + coin.h &&
    car.y + car.h > coin.y
  ) {
    // print("Coin collected!");
    coin_collect.setVolume(0.3);
    coin_collect.play();
    score += 1;
    coin.y = random(-300, 0);
    coin.x = random(coin_spawn_points);
  }
}

I am also very proud of the speed increase with the score increase which also makes the distance value rise faster.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//Making the game faster as the score goes up
function increaseDifficulty() {
let speedIncrease = min(20, 7 + score * 0.5);
for (let t of trafficCars) {
t.vy = speedIncrease;
}
coin.vy = speedIncrease - 0.5;
// Adjust distance increase interval based on speed
distanceUpdateInterval = max(50, 100 - speedIncrease * 50);
// print(speedIncrease);
}
//Making the game faster as the score goes up function increaseDifficulty() { let speedIncrease = min(20, 7 + score * 0.5); for (let t of trafficCars) { t.vy = speedIncrease; } coin.vy = speedIncrease - 0.5; // Adjust distance increase interval based on speed distanceUpdateInterval = max(50, 100 - speedIncrease * 50); // print(speedIncrease); }
//Making the game faster as the score goes up
function increaseDifficulty() {
  let speedIncrease = min(20, 7 + score * 0.5);
  for (let t of trafficCars) {
    t.vy = speedIncrease;
  }
  coin.vy = speedIncrease - 0.5;

  // Adjust distance increase interval based on speed
  distanceUpdateInterval = max(50, 100 - speedIncrease * 50);
  // print(speedIncrease); 
}

This part took some testing, that is why I left the print statement there to show how I tested the code and the logic of the game.

Problems faced along the way

Unfortunately, like every code ever written, it didn’t come without hardship. The beginning was smooth sailing as I started with simple shapes and just implementing simple game logic.

This was a great start and I was happy with the progress I was making.

The first problem, though, arose when I started uploading photos to use for objects in the game.

This is the image that I used for the player object. I used a PNG file to have the transparent background so it only looks like the car is on the screen. Now, at first I thought this would work perfectly and that I wouldn’t have any issues to deal with, but would quickly learn otherwise. The issue was the collision detection which happened even if in theory there was enough distance between cars.

Why is this? Well even though we don’t see the transparent background of the PNG, it is still there and the collision detection algorithm detects it as a part of the car object. To fix this I used a built in image function which cropped the unnecessary parts of the image.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
car = new Car(160, 300, 50, 100);
for (let i = 0; i < traffic_number; i++) {
trafficCars.push(
new Traffic(random(traffic_spawn_points), random(-500, 0), 65, 115)
);
}
car = new Car(160, 300, 50, 100); for (let i = 0; i < traffic_number; i++) { trafficCars.push( new Traffic(random(traffic_spawn_points), random(-500, 0), 65, 115) ); }
car = new Car(160, 300, 50, 100);

for (let i = 0; i < traffic_number; i++) {
  trafficCars.push(
    new Traffic(random(traffic_spawn_points), random(-500, 0), 65, 115)
  );
}

(The 3rd and 4th values of the functions)

Other issue I faced was with the file loading system, or specifically, writing in the csv file to make the game keep track of the highest score of a player.

My vision was to collect  all the scores of all the players who have every played and display the best, but I later learned that due to security reasons the p5js editor does not allow writing in files and has a read-only access to all the files uploaded. I decided to still keep the file and just display some high score to encourage players to try to beat it. Maybe in the future I could use google docs with their API to help me write in them, but due to the time constraint this will have to do for now.

There is also an issue with the objects sometimes spawning in one another. Fixing this would require changing the whole spawning logic and is unfortunately not possible in the short time span, but is definitely something that can be fixed in the future.

Future improvements and final thoughts

In the future I would love to work on this game even more and polish it to make it perfect. I would like to add some animations for crashes and maybe driving and would love to add power ups to spice up the gameplay. Unfortunately due to time constraints these implementations were not possible for this submission, but I think would be a fun addition to have in the future.

Overall I am happy with how the game turned out. In the beginning the game seemed like a big workload and a heavy task I set upon myself, but as I got to work on it more I got to work with everything we were taught in class and I really started to love the game and the experience of coding it.

This project has not been just a great learning opportunity, but has also inspired me to work on more different projects and become better at creating games. I will definitely be working on this project more and refining it to perfection.

Thank you for reading the documentation!

 

Midterm Project — Candy Collect

Introduction

This project was inspired by the food-theme (specifically, candy/sweets) games I used to play as a kid. I wanted to re-create the “vibe” of those games, also taking aesthetic references from Pinterest images. As I mentioned in my previous progress report, I drew up the idea on my iPad, then created the custom assets on Figma, such as the background image, boxes, buttons, logos, and so forth. The game I decided to create is called “CandyCollect”, where there are three different colored boxes, corresponding to a candy color. As the different colored candies are falling from the sky you have to move the corresponding box to catch the candy. You can switch the boxes by pressing the spacebar and the actively moveable box will glow yellow. You can then move the box by pressing down on the left and right arrow keys. You have a total of 30 seconds to catch as many candies as possible and depending on how you perform, you will be given different feedback from the game.

Custom Assets/Sketch
(on a real note, I actually ended up creating the candy shape with an ellipse and a rounded rectangle instead of using the image as I needed a shape haha.)

Code Highlights 

Glow effect on the box when active:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
if (isActive) {
for (let i = 0; i < 10; i++) {
stroke(255, 255, 0, 255 - (i * 30));
strokeWeight(4);
noFill();
// glow
rect(this.x - i, this.y - i, this.width + 2 * i, 60 + 2 * i);
}
if (isActive) { for (let i = 0; i < 10; i++) { stroke(255, 255, 0, 255 - (i * 30)); strokeWeight(4); noFill(); // glow rect(this.x - i, this.y - i, this.width + 2 * i, 60 + 2 * i); }
if (isActive) {
  for (let i = 0; i < 10; i++) {
    stroke(255, 255, 0, 255 - (i * 30)); 
    strokeWeight(4);
    noFill();
    
    // glow
    rect(this.x - i, this.y - i, this.width + 2 * i, 60 + 2 * i);
  }

I used a loop to create a glowing effect around the active box. The idea is pretty simple: I draw multiple rectangles on top of each other, each with a slightly lower opacity than the last. By tweaking stroke(255, 255, 0, 255 - (i * 30)), I make sure the glow fades out smoothly, giving the box a soft, dynamic highlight.

Collision detection mechanism:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
let box = boxes.find((box) => box.colorName === candies[i].colorName);
if (
candies[i].y + candies[i].size > box.y &&
candies[i].x > box.x &&
candies[i].x < box.x + box.width &&
boxes[activeBoxIndex] === box
) {
plop.play();
score++;
candies.splice(i, 1); // removing caught candy
}
let box = boxes.find((box) => box.colorName === candies[i].colorName); if ( candies[i].y + candies[i].size > box.y && candies[i].x > box.x && candies[i].x < box.x + box.width && boxes[activeBoxIndex] === box ) { plop.play(); score++; candies.splice(i, 1); // removing caught candy }
let box = boxes.find((box) => box.colorName === candies[i].colorName);
if (
  candies[i].y + candies[i].size > box.y &&
  candies[i].x > box.x &&
  candies[i].x < box.x + box.width &&
  boxes[activeBoxIndex] === box
) {
  plop.play();
  score++;
  candies.splice(i, 1); // removing caught candy
}

Even though this snippet is pretty intuitive and short, I actually liked how simple it was to basically add new features to the candy collision. First, it uses boxes.find() to match the candy’s color with the correct box, and then it checks if the candy’s position falls within the box’s bounds. If everything lines up, the plop sound plays, the score goes up, and the candy disappears. Adding additional features as I went was made easy as it was handle largely just in this section.

Problems/Struggles

1. Active Box Collisions:

At first, the collisions were not that accurate. For example, even if a box wasn’t active, it would catch the candy and I didn’t want to leave that in as a bug/feature. In order to fix this, I simply added an additional check in the collision

boxes[activeBoxIndex] === box
boxes[activeBoxIndex] === box detecting if statement.

2. Obstacles Spawning in Wrong Places:

There was this frustrating issue where multiple, and I mean MULTIPLE candies would spawn when the game is restarted, in a way that would be impossible for the user to catch. To fix this, I cleared the candy spawn interval if a previous interval still existed.

3. Audio and Performance Issues

Towards the end of the project, for some reason, some of my files (e.g. images, audios) would get corrupted, and I had a hard time making sure that everything was all accessible to the sketch.js. This was a relatively simple fix as I just double-checked in my assets folder whether all the necessary images and audios were available.

Here’s the demo:

Demo

Webster – Midterm Project

Introduction and Concept

Welcome to Webster! This game comes from an inspiration of a very dear friend, a pet actually. You know what they say ” Do not kill that spider in the corner of your room, it probably thinks you are it’s roommate.” I saw a spider in the corner of a room we do not usually enter in my house, and I called it Webster.

This project is a labor of love that brings together some really fun game design. The game uses solid physics to simulate gravity and rope mechanics, making our little spider swing through a cave that’s so high it even has clouds! I broke the project into clear, modular functions so every bit of the physics—from gravity pulling our spider down to the rope tension that keeps it swinging—is handled cleanly. This means the spider feels natural and responsive, just like it’s really hanging from a web in a bustling cave (maybe IRL a cave of clouds doesn’t exist but its oki)

On the design side, Webster is all about variety and challenge. The game dynamically spawns clouds, flies, and even bees as you progress, keeping the environment fresh and unpredictable. Randomized placements of these elements mean every playthrough feels unique, and the parallax background adds a nice touch of depth. Inspired by classic spider lore and a bit of Spiderman magic, the game makes sure you’re always on your toes—eating flies for points and avoiding bees like your life depends on it (well, Webster’s life does)

Enjoy swinging with Webster!

 

Sketch!

Code Highlights

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// --- Physics-related vars & functions ---
// global vars for gravity, rope stiffness, rope rest length, rope anchor, and damping factor
let gravity, ropeK = 0.5, ropeRestLength, ropeAnchor, damping = 1;
function setup() {
createCanvas(640, 480);
gravity = createVector(0, 0.08); // sets constant downward acceleration
}
class Spider {
constructor(x, y) {
this.pos = createVector(x, y); // starting pos
this.vel = createVector(0, 0); // starting vel
this.radius = 15;
this.attached = false; // not attached initially
}
update() {
this.vel.add(gravity); // apply gravity each frame
if (this.attached && ropeAnchor) {
let ropeVec = p5.Vector.sub(ropeAnchor, this.pos); // vector from spider to rope anchor
let distance = ropeVec.mag(); // current rope length
if (distance > ropeRestLength) { // if rope stretched beyond rest length
let force = ropeVec.normalize().mult((distance - ropeRestLength) * ropeK); // calculate tension force
this.vel.add(force); // apply rope tension to velocity
}
}
this.vel.mult(damping); // simulate friction/air resistance
this.pos.add(this.vel); // update position based on velocity
}
}
// --- Physics-related vars & functions --- // global vars for gravity, rope stiffness, rope rest length, rope anchor, and damping factor let gravity, ropeK = 0.5, ropeRestLength, ropeAnchor, damping = 1; function setup() { createCanvas(640, 480); gravity = createVector(0, 0.08); // sets constant downward acceleration } class Spider { constructor(x, y) { this.pos = createVector(x, y); // starting pos this.vel = createVector(0, 0); // starting vel this.radius = 15; this.attached = false; // not attached initially } update() { this.vel.add(gravity); // apply gravity each frame if (this.attached && ropeAnchor) { let ropeVec = p5.Vector.sub(ropeAnchor, this.pos); // vector from spider to rope anchor let distance = ropeVec.mag(); // current rope length if (distance > ropeRestLength) { // if rope stretched beyond rest length let force = ropeVec.normalize().mult((distance - ropeRestLength) * ropeK); // calculate tension force this.vel.add(force); // apply rope tension to velocity } } this.vel.mult(damping); // simulate friction/air resistance this.pos.add(this.vel); // update position based on velocity } }
// --- Physics-related vars & functions ---
// global vars for gravity, rope stiffness, rope rest length, rope anchor, and damping factor
let gravity, ropeK = 0.5, ropeRestLength, ropeAnchor, damping = 1;

function setup() {
  createCanvas(640, 480);
  gravity = createVector(0, 0.08); // sets constant downward acceleration
}

class Spider {
  constructor(x, y) {
    this.pos = createVector(x, y); // starting pos
    this.vel = createVector(0, 0);   // starting vel
    this.radius = 15;
    this.attached = false; // not attached initially
  }
  update() {
    this.vel.add(gravity); // apply gravity each frame
    if (this.attached && ropeAnchor) {
      let ropeVec = p5.Vector.sub(ropeAnchor, this.pos); // vector from spider to rope anchor
      let distance = ropeVec.mag(); // current rope length
      if (distance > ropeRestLength) { // if rope stretched beyond rest length
        let force = ropeVec.normalize().mult((distance - ropeRestLength) * ropeK); // calculate tension force
        this.vel.add(force); // apply rope tension to velocity
      }
    }
    this.vel.mult(damping); // simulate friction/air resistance
    this.pos.add(this.vel); // update position based on velocity
  }
}

This snippet centralizes all physics computations. Gravity is set as a constant downward acceleration in setup and then applied every frame in the Spider class’s update() method, which makes the spider to accelerate downwards. When attached to a rope, a corrective force is calculated if the rope exceeds its rest length, which simulates tension; damping is applied to slow velocity over time, which mimics friction or air resistance.

 

 

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// --- Spawning Elements Functions ---
// spawnObstacles: checks spider's x pos and adds cloud obs if near last obs; random spacing & y pos
function spawnObstacles() {
if (spider.pos.x + width - 50 > lastObstacleX) { // if spider near last obs, spawn new one
let spacing = random(200, 500); // random gap for next obs
let cloudY = random(height - 50 / 2, height + 1 / 2); // random vertical pos for cloud
obstacles.push({ // add new cloud obs obj
x: lastObstacleX + 500, // x pos offset from last obs
y: cloudY, // y pos of cloud
w: random(80, 150), // random width
h: 20, // fixed height
type: "cloud", // obs type
baseY: cloudY, // store base y for wobble effect
wobbleOffset: random(100000) // random wobble offset for animation
});
lastObstacleX += spacing; // update last obs x pos
}
}
// spawnWorldElements: calls spawnObstacles then spawns collectibles (flies/webPower) and enemies (bees)
// based on frame count and random chance, spawning them ahead of spider for dynamic environment growth
function spawnWorldElements() {
spawnObstacles(); // spawn cloud obs if needed
if (frameCount % 60 === 0 && random() < 0.6) { // every 60 frames, chance to spawn collectible
collectibles.push({
x: spider.pos.x + random(width, width + 600), // spawn ahead of spider
y: random(50, height + 500), // random vertical pos
type: random() < 0.7 ? "fly" : "webPower" // 70% fly, else webPower
});
}
if (frameCount % 100 === 0 && random() < 0.7) { // every 100 frames, chance to spawn enemy
enemies.push({
x: spider.pos.x + random(width, width + 600), // spawn ahead of spider
y: random(100, height + 500), // random vertical pos
speed: random(2, 4) // random enemy speed
});
}
}
// --- Spawning Elements Functions --- // spawnObstacles: checks spider's x pos and adds cloud obs if near last obs; random spacing & y pos function spawnObstacles() { if (spider.pos.x + width - 50 > lastObstacleX) { // if spider near last obs, spawn new one let spacing = random(200, 500); // random gap for next obs let cloudY = random(height - 50 / 2, height + 1 / 2); // random vertical pos for cloud obstacles.push({ // add new cloud obs obj x: lastObstacleX + 500, // x pos offset from last obs y: cloudY, // y pos of cloud w: random(80, 150), // random width h: 20, // fixed height type: "cloud", // obs type baseY: cloudY, // store base y for wobble effect wobbleOffset: random(100000) // random wobble offset for animation }); lastObstacleX += spacing; // update last obs x pos } } // spawnWorldElements: calls spawnObstacles then spawns collectibles (flies/webPower) and enemies (bees) // based on frame count and random chance, spawning them ahead of spider for dynamic environment growth function spawnWorldElements() { spawnObstacles(); // spawn cloud obs if needed if (frameCount % 60 === 0 && random() < 0.6) { // every 60 frames, chance to spawn collectible collectibles.push({ x: spider.pos.x + random(width, width + 600), // spawn ahead of spider y: random(50, height + 500), // random vertical pos type: random() < 0.7 ? "fly" : "webPower" // 70% fly, else webPower }); } if (frameCount % 100 === 0 && random() < 0.7) { // every 100 frames, chance to spawn enemy enemies.push({ x: spider.pos.x + random(width, width + 600), // spawn ahead of spider y: random(100, height + 500), // random vertical pos speed: random(2, 4) // random enemy speed }); } }
// --- Spawning Elements Functions ---
// spawnObstacles: checks spider's x pos and adds cloud obs if near last obs; random spacing & y pos
function spawnObstacles() {
  if (spider.pos.x + width - 50 > lastObstacleX) { // if spider near last obs, spawn new one
    let spacing = random(200, 500); // random gap for next obs
    let cloudY = random(height - 50 / 2, height + 1 / 2); // random vertical pos for cloud
    obstacles.push({ // add new cloud obs obj
      x: lastObstacleX + 500, // x pos offset from last obs
      y: cloudY,             // y pos of cloud
      w: random(80, 150),    // random width
      h: 20,                 // fixed height
      type: "cloud",         // obs type
      baseY: cloudY,         // store base y for wobble effect
      wobbleOffset: random(100000) // random wobble offset for animation
    });
    lastObstacleX += spacing; // update last obs x pos
  }
}

// spawnWorldElements: calls spawnObstacles then spawns collectibles (flies/webPower) and enemies (bees)
// based on frame count and random chance, spawning them ahead of spider for dynamic environment growth
function spawnWorldElements() {
  spawnObstacles(); // spawn cloud obs if needed
  
  if (frameCount % 60 === 0 && random() < 0.6) { // every 60 frames, chance to spawn collectible
    collectibles.push({
      x: spider.pos.x + random(width, width + 600), // spawn ahead of spider
      y: random(50, height + 500),                   // random vertical pos
      type: random() < 0.7 ? "fly" : "webPower"       // 70% fly, else webPower
    });
  }
  
  if (frameCount % 100 === 0 && random() < 0.7) { // every 100 frames, chance to spawn enemy
    enemies.push({
      x: spider.pos.x + random(width, width + 600), // spawn ahead of spider
      y: random(100, height + 500),                   // random vertical pos
      speed: random(2, 4)                           // random enemy speed
    });
  }
}

This snippet groups all spawning logic for environment elements. The spawnObstacles() function checks if the spider is near the last obstacle’s x coordinate and, if so, adds a new cloud obstacle with randomized spacing, vertical position, and dimensions. Then spawnWorldElements() calls this function and also adds collectibles and enemies (bees) ahead of the spider based on frame counts and random chances, to ensure a dynamic and everchanging environment.

Problems I faced (there were many)

There were quite a few quirky issues along the way. One problem was with collision detection—sometimes the spider would bounce off clouds a bit jitterily or not land smoothly, which made the swing feel less natural.  And then there was that pesky web projectile bug where it would linger or vanish unexpectedly if the input timing wasn’t just right, which threw off the feel of shooting a web.

Another area for improvement is enemy behavior. Bees, for example, sometimes weren’t as aggressive as I’d like like, so their collision detection could be sharpened to ramp up the challenge. I also ran into occasional delays in sound effects triggering properly—especially when multiple actions happened at once—which reminded me that asset management in p5.js can be a bit finicky.

Another hiccup was with the custom font and web projectile behavior. Initially, every character was coming out as a single letter because of font issues. When I changed the font extension from ttf to otf, it worked out for some reason.

I also had a lot of problem with the cloud spawning logic, sometimes they would spawn under the spider itself which prevents it from actually swinging as it wont gain any horizontal velocity, this was a PAIN to solve, because I tried every complicated approach which did not work, but the solution was simple, I only had to add a constant (which i chose to be 500) to the initial spawning x coordinates for the clouds. YES! it was that simple, but that part alone took me around 3 hours.

All in all, while Webster is a fun ride, these little details offer plenty of room to refine the game even further!