Midterm Project – The Cookie Game!

For my midterm project, I decided to channel my love for baking and code a game for baking cookies!

Link to the full screen sketch: https://editor.p5js.org/lr3258/full/e-z5Ur1g3

Concept :

This is a simple game to bake chocolate chip cookies, very direct and fun! The player drags ingredients into a bowl to create the dough, which then bakes into delicious chocolate chip cookies! I wanted it to have a very comfy and cozy vibe, so I chose a colour palette of baby blues and browns. To make the entire game more aesthetically cohesive, I drew the ingredients on Autodesk Sketchbook and made the layouts on Canva. I also drew the egg –> dough sprite sheet since I couldn’t find any suitable ones online.

How it works:

The game isn’t meant to be challenging or competitive, it is more inclined to the process and joy of baking the cookies than being a “game” itself.

First, the user has to click on any key to start the game:

Next, the player has to drag the ingredients into the bowl. Here, when the egg is clicked on, it triggers the animation to display the dough. We can then press the Next button.

After this, we have to wait for 5 seconds, while the cookies “bake” in the oven. After 5 seconds, the screen automatically shifts with a Ding! sound.

Then, the final screen shows the baked cookies! We can then restart the game.

highlights of the code i’m proud of

A part of the game I’m pretty proud of is the sprite sheet animation. It took a lot of time to draw, create and then animate the sprite sheet. It was also the hardest part, and there was a LOT of debugging involved, but in the end I’m happy with how it turned out, even with the ingredients being dragged into the bowl. I’m also very happy with the aesthetic of the game, which really feels like home to me.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
class Ingredient {
constructor(name, x, y, img, isEgg) {
this.name = name;
this.x = x;
this.y = y;
this.img = img;
this.isEgg = isEgg;
this.isDragging = false;
this.offsetX = 0;
this.offsetY = 0;
this.isDropped = false;
this.currentFrame = 0;
this.numFrames = 5;
this.frameWidth = 150;
this.frameHeight = 150;
this.isCracked = false;
this.frameTimer = 0;
this.frameSpeed = 6;
this.originalX = x;
this.originalY = y;
}
display() {
if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) {
this.isDropped = true;
this.x = bowl.x;
this.y = bowl.y;
}
if (this.isEgg) {
let sx = this.currentFrame * this.frameWidth;
image(this.img, this.x, this.y, this.frameWidth, this.frameHeight, sx, 0, this.frameWidth, this.frameHeight);
} else if (!this.isDropped) {
image(this.img, this.x, this.y);
}
}
update() {
if (this.isDragging) {
this.x = mouseX + this.offsetX;
this.y = mouseY + this.offsetY;
}
//dropping ingredient into bowl
if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) {
this.isDropped = true;
this.x = bowl.x;
this.y = bowl.y;
}
// animate the egg spritesheet if it's clicked
if (this.isEgg && this.isCracked) {
this.frameTimer++;
if (this.frameTimer >= this.frameSpeed) {
this.frameTimer = 0; // to reset timer
if (this.currentFrame < this.numFrames - 1) {
this.currentFrame++;
}
}
}
}
checkDragging() {
if (
mouseX > this.x &&
mouseX < this.x + this.img.width &&
mouseY > this.y &&
mouseY < this.y + this.img.height &&
!this.isDropped
) {
this.isDragging = true;
this.offsetX = this.x - mouseX;
this.offsetY = this.y - mouseY;
}
}
checkClicked() {
if (
mouseX > this.x &&
mouseX < this.x + this.img.width &&
mouseY > this.y &&
mouseY < this.y + this.img.height &&
!this.isCracked
) {
this.isCracked = true; //start the animation of egg cracking
this.currentFrame = 0;
}
}
class Ingredient { constructor(name, x, y, img, isEgg) { this.name = name; this.x = x; this.y = y; this.img = img; this.isEgg = isEgg; this.isDragging = false; this.offsetX = 0; this.offsetY = 0; this.isDropped = false; this.currentFrame = 0; this.numFrames = 5; this.frameWidth = 150; this.frameHeight = 150; this.isCracked = false; this.frameTimer = 0; this.frameSpeed = 6; this.originalX = x; this.originalY = y; } display() { if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) { this.isDropped = true; this.x = bowl.x; this.y = bowl.y; } if (this.isEgg) { let sx = this.currentFrame * this.frameWidth; image(this.img, this.x, this.y, this.frameWidth, this.frameHeight, sx, 0, this.frameWidth, this.frameHeight); } else if (!this.isDropped) { image(this.img, this.x, this.y); } } update() { if (this.isDragging) { this.x = mouseX + this.offsetX; this.y = mouseY + this.offsetY; } //dropping ingredient into bowl if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) { this.isDropped = true; this.x = bowl.x; this.y = bowl.y; } // animate the egg spritesheet if it's clicked if (this.isEgg && this.isCracked) { this.frameTimer++; if (this.frameTimer >= this.frameSpeed) { this.frameTimer = 0; // to reset timer if (this.currentFrame < this.numFrames - 1) { this.currentFrame++; } } } } checkDragging() { if ( mouseX > this.x && mouseX < this.x + this.img.width && mouseY > this.y && mouseY < this.y + this.img.height && !this.isDropped ) { this.isDragging = true; this.offsetX = this.x - mouseX; this.offsetY = this.y - mouseY; } } checkClicked() { if ( mouseX > this.x && mouseX < this.x + this.img.width && mouseY > this.y && mouseY < this.y + this.img.height && !this.isCracked ) { this.isCracked = true; //start the animation of egg cracking this.currentFrame = 0; } }
class Ingredient {
constructor(name, x, y, img, isEgg) {
    this.name = name;
    this.x = x;
    this.y = y;
    this.img = img;
    this.isEgg = isEgg;
    this.isDragging = false;
    this.offsetX = 0;
    this.offsetY = 0;
    this.isDropped = false;
    this.currentFrame = 0; 
    this.numFrames = 5; 
    this.frameWidth = 150;
    this.frameHeight = 150;
    this.isCracked = false;
    this.frameTimer = 0;
    this.frameSpeed = 6;  
    this.originalX = x;  
    this.originalY = y;  
  }
  
  display() {
    
    if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) {
  this.isDropped = true;
  this.x = bowl.x;
  this.y = bowl.y;
}
    
    if (this.isEgg) {
      let sx = this.currentFrame * this.frameWidth;
      image(this.img, this.x, this.y, this.frameWidth, this.frameHeight, sx, 0, this.frameWidth, this.frameHeight);
    } else if (!this.isDropped) {
      image(this.img, this.x, this.y);
    }
  }

  update() {
    if (this.isDragging) {
      this.x = mouseX + this.offsetX;
      this.y = mouseY + this.offsetY;
    }
    
    //dropping ingredient into bowl
    if (!this.isDropped && !this.isEgg && bowl.isIngredientInBowl(this)) {
      this.isDropped = true;
      this.x = bowl.x; 
      this.y = bowl.y;
    }

    // animate the egg spritesheet if it's clicked
    if (this.isEgg && this.isCracked) {
      this.frameTimer++;
      if (this.frameTimer >= this.frameSpeed) {
        this.frameTimer = 0;  // to reset timer
      if (this.currentFrame < this.numFrames - 1) {
          this.currentFrame++;  
        }
      }
    }
  }

  checkDragging() {
    if (
      mouseX > this.x &&
      mouseX < this.x + this.img.width &&
      mouseY > this.y &&
      mouseY < this.y + this.img.height &&
      !this.isDropped
    ) {
      this.isDragging = true;
      this.offsetX = this.x - mouseX;
      this.offsetY = this.y - mouseY;
    }
  }

  checkClicked() {
    if (
      mouseX > this.x &&
      mouseX < this.x + this.img.width &&
      mouseY > this.y &&
      mouseY < this.y + this.img.height &&
      !this.isCracked
    ) {
      this.isCracked = true; //start the animation of egg cracking
      this.currentFrame = 0;
    }
  }

 

challenges and improvements:

When it came to challenges, the main concern was the sprite sheet itself. It was also hard to transition from different states smoothly, but I implemented the concept of state machine we discussed in class, and it went pretty well. In the end, the final problem I had was the autoplay of the background audio, which wasn’t working in the full screen version. For this, I used the p5js built in code, userStartAudio(), which starts the audio on click. Implementing the restart button at the end was also a bit tricky, but overall, I’m pleased with the final result.

If I could make some improvements, I’d add some extra elements, like a timer, or maybe animations for the baking time. I would also like to change the design of the buttons, which are currently very basic in contrast to the rest of the game.

Embedded sketch :

Midterm Project: Interactive Fantasy Game

For this project, I decided to transform a short fantasy story I wrote in primary school into an interactive game using p5.js. This game is played by a single player who guides the protagonist, Eden, on her quest to slay the dargon and save her village. The game has multiple choices that affect the storyline, but in the end, all paths lead to the same outcome. There are a total of 5 key decision points and 14 different screens in the game.

How it works:

The game’s structure is built around a series of screens, each representing a different part of Eden’s journey. The transitions between screens are managed efficiently through the mousePressed() function and buttons. The game utilizes object-oriented programming (OOP) for button management.

Proud Achievements and Technical Decisions
One of the most significant achievements in this project is the implementation of the OOP approach for button management. Initially, built-in functions were used, but switching to OOP proved to be a more flexible and maintainable solution. This decision allows for easier customization of button behavior and appearance across different screens.

Another achievment is the overall structure of thegame, with its multiple screens and decision points. This took a long time and required  careful planning and implementation to ensure smooth transitions and logical flow of the story.

The integration of background images and music also adds to the immersive experience of the game. These elements help bring the fantasy world of EverLand to life for the player.

The buttons code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//Creating the buttons class
class GameButton {
//initializing the state of the button
constructor(label) {
this.label = label;
this.x = 0;
this.y = 0;
this.width = 105;
this.height = 30;
this.visible = false;
}
//method to show button at specified coordinates
show(x, y) {
this.x = x;
this.y = y;
this.visible = true;
fill("white");
noStroke();
rect(this.x, this.y, this.width, this.height);
fill("black");
textSize(13);
textAlign(CENTER, CENTER);
text(this.label, this.x + this.width / 2, this.y + this.height / 2);
fill("white") //so trhat the text itself is white
textSize(15); //so that the text itself has a size of 15
}
//method to hide button
hide() {
this.visible = false;
}
//method to identify if mouse is hovering over button
isMouseOver() {
return (
this.visible &&
mouseX > this.x &&
mouseX < this.x + this.width &&
mouseY > this.y &&
mouseY < this.y + this.height
);
}
}
//Creating the buttons class class GameButton { //initializing the state of the button constructor(label) { this.label = label; this.x = 0; this.y = 0; this.width = 105; this.height = 30; this.visible = false; } //method to show button at specified coordinates show(x, y) { this.x = x; this.y = y; this.visible = true; fill("white"); noStroke(); rect(this.x, this.y, this.width, this.height); fill("black"); textSize(13); textAlign(CENTER, CENTER); text(this.label, this.x + this.width / 2, this.y + this.height / 2); fill("white") //so trhat the text itself is white textSize(15); //so that the text itself has a size of 15 } //method to hide button hide() { this.visible = false; } //method to identify if mouse is hovering over button isMouseOver() { return ( this.visible && mouseX > this.x && mouseX < this.x + this.width && mouseY > this.y && mouseY < this.y + this.height ); } }
//Creating the buttons class
class GameButton {
  //initializing the state of the button
  constructor(label) {
    this.label = label;
    this.x = 0;
    this.y = 0;
    this.width = 105;
    this.height = 30;
    this.visible = false;
  }
  //method to show button at specified coordinates
  show(x, y) {
    this.x = x;
    this.y = y;
    this.visible = true;
    fill("white");
    noStroke();
    rect(this.x, this.y, this.width, this.height);
    fill("black");
    textSize(13);
    textAlign(CENTER, CENTER);
    text(this.label, this.x + this.width / 2, this.y + this.height / 2);
    fill("white") //so trhat the text itself is white
    textSize(15); //so that the text itself has a size of 15
  }
  //method to hide button
  hide() {
    this.visible = false;
  }

  //method to identify if mouse is hovering over button
  isMouseOver() {
    return (
      this.visible &&
      mouseX > this.x &&
      mouseX < this.x + this.width &&
      mouseY > this.y &&
      mouseY < this.y + this.height
    );
  }
}

The screens code:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function draw() {
//calling the function that displays the screen based on the screen assignment which happens when the mouse is pressed
if (screen === 0) {
showStartScreen();
} else if (screen === 1) {
showBirthdayScreen();
} else if (screen === 11) {
showSuppliesScreen();
} else if (screen === 12) {
showWeaponScreen();
} else if (screen === 111 || screen === 121) {
showNightScreen();
} else if (screen === 112 || screen === 122) {
showMorningScreen();
} else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) {
showRiverScreen();
} else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) {
showForestScreen();
} else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) {
showNextScreen();
} else if (screen === 5000) {
showDragonCaveScreen();
} else if (screen === 5001) {
showInsideScreen();
} else if (screen === 5002) {
showOutsideScreen();
} else if (screen === 5003) {
showTrapScreen();
} else if (screen === 262626) {
showFinalScreen();
}
}
//function to hide all buttons which i will use when switching screens so that the previous buttons don't appear on the screen
function hideAllButtons() {
enterButton.hide();
suppliesButton.hide();
weaponButton.hide();
nightButton.hide();
morningButton.hide();
riverButton.hide();
forestButton.hide();
fishButton.hide();
riverspiritsButton.hide();
next1Button.hide();
insideButton.hide();
outsideButton.hide();
trapButton.hide();
next2Button.hide();
forestspiritsButton.hide();
firefliesButton.hide();
playButton.hide();
quitButton.hide();
}
//assigns screen with next number based on the previous screen and the button pressed and also hides all buttons so that they aren't layered on top of one another
function mousePressed() {
if (screen === 0 && enterButton.isMouseOver()) {
screen = 1;
hideAllButtons();
} else if (screen === 1) {
if (suppliesButton.isMouseOver()) {
screen = 11;
hideAllButtons();
}else if (weaponButton.isMouseOver()) {
screen = 12;
hideAllButtons();
}
} else if (screen === 11 || screen === 12) {
if (nightButton.isMouseOver()) {
screen = screen * 10 + 1;
hideAllButtons();
}else if (morningButton.isMouseOver()) {
screen = screen * 10 + 2;
hideAllButtons();
}
}else if (screen === 111 || screen === 112 || screen === 121 || screen === 122) {
if (riverButton.isMouseOver()) {
screen = screen * 10 + 1;
hideAllButtons();
} else if (forestButton.isMouseOver()) {
screen = screen * 10 + 2;
hideAllButtons();
}
} else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) {
if (fishButton.isMouseOver()) {
screen = 11000;
hideAllButtons();
} else if (riverspiritsButton.isMouseOver()) {
screen = 12000;
hideAllButtons();
}
} else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) {
if (firefliesButton.isMouseOver()) {
screen = 21000;
hideAllButtons();
} else if (forestspiritsButton.isMouseOver()) {
screen = 22000;
hideAllButtons();
}
} else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) {
if (next1Button.isMouseOver()) {
screen = 5000;
hideAllButtons();
}
} else if (screen === 5000) {
if (insideButton.isMouseOver()) {
screen = 5001;
hideAllButtons();
} else if (outsideButton.isMouseOver()) {
screen = 5002;
hideAllButtons();
} else if (trapButton.isMouseOver()) {
screen = 5003;
hideAllButtons();
}
} else if (screen === 5001 || screen === 5002 || screen === 5003) {
if (next2Button.isMouseOver()) {
screen = 262626;
hideAllButtons();
}
} else if (screen === 262626) {
if (playButton.isMouseOver()) {
restartGame();
} else if (quitButton.isMouseOver()) {
quitGame();
}
}
}
function draw() { //calling the function that displays the screen based on the screen assignment which happens when the mouse is pressed if (screen === 0) { showStartScreen(); } else if (screen === 1) { showBirthdayScreen(); } else if (screen === 11) { showSuppliesScreen(); } else if (screen === 12) { showWeaponScreen(); } else if (screen === 111 || screen === 121) { showNightScreen(); } else if (screen === 112 || screen === 122) { showMorningScreen(); } else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) { showRiverScreen(); } else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) { showForestScreen(); } else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) { showNextScreen(); } else if (screen === 5000) { showDragonCaveScreen(); } else if (screen === 5001) { showInsideScreen(); } else if (screen === 5002) { showOutsideScreen(); } else if (screen === 5003) { showTrapScreen(); } else if (screen === 262626) { showFinalScreen(); } } //function to hide all buttons which i will use when switching screens so that the previous buttons don't appear on the screen function hideAllButtons() { enterButton.hide(); suppliesButton.hide(); weaponButton.hide(); nightButton.hide(); morningButton.hide(); riverButton.hide(); forestButton.hide(); fishButton.hide(); riverspiritsButton.hide(); next1Button.hide(); insideButton.hide(); outsideButton.hide(); trapButton.hide(); next2Button.hide(); forestspiritsButton.hide(); firefliesButton.hide(); playButton.hide(); quitButton.hide(); } //assigns screen with next number based on the previous screen and the button pressed and also hides all buttons so that they aren't layered on top of one another function mousePressed() { if (screen === 0 && enterButton.isMouseOver()) { screen = 1; hideAllButtons(); } else if (screen === 1) { if (suppliesButton.isMouseOver()) { screen = 11; hideAllButtons(); }else if (weaponButton.isMouseOver()) { screen = 12; hideAllButtons(); } } else if (screen === 11 || screen === 12) { if (nightButton.isMouseOver()) { screen = screen * 10 + 1; hideAllButtons(); }else if (morningButton.isMouseOver()) { screen = screen * 10 + 2; hideAllButtons(); } }else if (screen === 111 || screen === 112 || screen === 121 || screen === 122) { if (riverButton.isMouseOver()) { screen = screen * 10 + 1; hideAllButtons(); } else if (forestButton.isMouseOver()) { screen = screen * 10 + 2; hideAllButtons(); } } else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) { if (fishButton.isMouseOver()) { screen = 11000; hideAllButtons(); } else if (riverspiritsButton.isMouseOver()) { screen = 12000; hideAllButtons(); } } else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) { if (firefliesButton.isMouseOver()) { screen = 21000; hideAllButtons(); } else if (forestspiritsButton.isMouseOver()) { screen = 22000; hideAllButtons(); } } else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) { if (next1Button.isMouseOver()) { screen = 5000; hideAllButtons(); } } else if (screen === 5000) { if (insideButton.isMouseOver()) { screen = 5001; hideAllButtons(); } else if (outsideButton.isMouseOver()) { screen = 5002; hideAllButtons(); } else if (trapButton.isMouseOver()) { screen = 5003; hideAllButtons(); } } else if (screen === 5001 || screen === 5002 || screen === 5003) { if (next2Button.isMouseOver()) { screen = 262626; hideAllButtons(); } } else if (screen === 262626) { if (playButton.isMouseOver()) { restartGame(); } else if (quitButton.isMouseOver()) { quitGame(); } } }
function draw() {
  //calling the function that displays the screen based on the screen assignment which happens when the mouse is pressed
  if (screen === 0) {
    showStartScreen();
  } else if (screen === 1) {
    showBirthdayScreen();
  } else if (screen === 11) {
    showSuppliesScreen();
  } else if (screen === 12) {
    showWeaponScreen();
  } else if (screen === 111 || screen === 121) {
    showNightScreen();
  } else if (screen === 112 || screen === 122) {
    showMorningScreen();
  } else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) {
    showRiverScreen();
  } else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) {
    showForestScreen();
  } else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) {
    showNextScreen();
  } else if (screen === 5000) {
    showDragonCaveScreen();
  } else if (screen === 5001) {
    showInsideScreen();
  } else if (screen === 5002) {
    showOutsideScreen();
  } else if (screen === 5003) {
    showTrapScreen();
  } else if (screen === 262626) {
    showFinalScreen();
  }
}

//function to hide all buttons which i will use when switching screens so that the previous buttons don't appear on the screen
function hideAllButtons() {
  enterButton.hide();
  suppliesButton.hide();
  weaponButton.hide();
  nightButton.hide();
  morningButton.hide();
  riverButton.hide();
  forestButton.hide();
  fishButton.hide();
  riverspiritsButton.hide();
  next1Button.hide();
  insideButton.hide();
  outsideButton.hide();
  trapButton.hide();
  next2Button.hide();
  forestspiritsButton.hide();
  firefliesButton.hide();
  playButton.hide();
  quitButton.hide();
}

//assigns screen with next number based on the previous screen and the button pressed and also hides all buttons so that they aren't layered on top of one another
function mousePressed() {
  if (screen === 0 && enterButton.isMouseOver()) {
    screen = 1;
    hideAllButtons();
  } else if (screen === 1) {
    if (suppliesButton.isMouseOver()) {
      screen = 11;
      hideAllButtons();
    }else if (weaponButton.isMouseOver()) {
      screen = 12;
      hideAllButtons();
    }
  } else if (screen === 11 || screen === 12) {
    if (nightButton.isMouseOver()) {
      screen = screen * 10 + 1;
      hideAllButtons();
    }else if (morningButton.isMouseOver()) {
      screen = screen * 10 + 2;
      hideAllButtons();
    }
  }else if (screen === 111 || screen === 112 || screen === 121 || screen === 122) {
    if (riverButton.isMouseOver()) {
      screen = screen * 10 + 1;
      hideAllButtons();
    } else if (forestButton.isMouseOver()) {
      screen = screen * 10 + 2;
      hideAllButtons();
    }
  } else if (screen === 1111 || screen === 1121 || screen === 1211 || screen === 1221) {
    if (fishButton.isMouseOver()) {
      screen = 11000;
      hideAllButtons();
    } else if (riverspiritsButton.isMouseOver()) {
      screen = 12000;
      hideAllButtons();
    }
  } else if (screen === 1112 || screen === 1122 || screen === 1212 || screen === 1222) {
    if (firefliesButton.isMouseOver()) {
      screen = 21000;
      hideAllButtons();
    } else if (forestspiritsButton.isMouseOver()) {
      screen = 22000;
      hideAllButtons();
    }
  } else if (screen === 11000 || screen === 12000 || screen === 21000 || screen === 22000) {
    if (next1Button.isMouseOver()) {
      screen = 5000;
      hideAllButtons();
    }
  } else if (screen === 5000) {
    if (insideButton.isMouseOver()) {
      screen = 5001;
      hideAllButtons();
    } else if (outsideButton.isMouseOver()) {
      screen = 5002;
      hideAllButtons();
    } else if (trapButton.isMouseOver()) {
      screen = 5003;
      hideAllButtons();
    }
  } else if (screen === 5001 || screen === 5002 || screen === 5003) {
    if (next2Button.isMouseOver()) {
      screen = 262626;
      hideAllButtons();
    }
  } else if (screen === 262626) {
    if (playButton.isMouseOver()) {
      restartGame();
    } else if (quitButton.isMouseOver()) {
      quitGame();
    }
  }
}

Areas for Improvement:
While the current version of the game is functional, there are several areas for potential improvement:

Expanding the storyline: Adding more options and choices would increase replayability and make the game more interactive. For example, allowing players to pick specific weapons or have more detailed interactions with characters like the spirits could add depth to the game.

Enhanced interactivity: Implementing features like dialogue systems for conversations with spirits or other characters could make the game more engaging. This could involve creating a more complex dialogue management system.

Challenges:

One of the main challenges faced was transitioning from built-in button functions to the OOP approach. This required refactoring a significant portion of the code and it would’ve been so much easier if I just started with OOP.

Moving forward, the focus will be on enriching the game content and enhancing the player’s ability to influence the story through their choices.

The game sketch:

Midterm Project – Sweet Treats

For my midterm project, I decided to create a memory game. Given my unhelpable sweet tooth, I based my game around a bakery. I was partly inspired by the game Cooking Mama, which was a game on the Nintendo DS that I played frequently. My game involves the user picking a recipe, then having to memorise and select the right ingredients for that recipe within the given timeframe. The player is then “rewarded” with the dish if they are successful.

I wanted my game to have a cohesive theme to it, so I ended up creating all the graphics myself. This was by far one of the most time consuming aspects, as I was my own worst critic when it came to the quality of the images. Overall though, I am very satisfied with the way the graphics came out; I think they give the game a more polished and homey look.

One aspect I am particularly proud of is the Game class. Taking Professor Mang’s advice, I implemented a state machine to make the flow of the game easier to manage, as my game consisted of multiple “stages” that it would need to flow through in order to work coherently. This helped organise the code into more manageable sections, rather than a mess of code I’d need to sort through if I needed to change anything.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
display() {
background(255); // Set background to white
if (this.state === "selection") {
image(shelfImage, 0, 0, width, height); // Display shelf as background
}
textAlign(CENTER, CENTER);
textFont(font);
this.allIngredients.forEach((ingredient) => ingredient.display());
stroke(0);
strokeWeight(1);
fill(0);
if (this.state === "start") {
image(titleImage, 0, 0, width, height); // Display title screen background image
textSize(48);
text("Sweet Treats Bakery", width / 2, height / 2 - 50); // Title Text
textSize(32);
text("Click to Start", width / 2, height / 2 + 50); // Subtitle Text
} else if (this.state === "menu") {
image(menuImage, 0, 0, width, height); // Display menu screen background image
fill("#332211");
textSize(64);
text("MENU", width / 2, 100);
buttons.forEach((button) => button.display());
} else if (this.state === "memory") {
image(recipeImage, 0, 0, width, height); // Display recipe image as background
textSize(32);
text("Memorize These Ingredients!", width / 2, 80);
textFont(honeySaltFont);
textSize(35);
this.correctIngredients.forEach((ing, i) =>
text(ing, width / 2, 300 + i * 50)
);
if (millis() - this.memoryTimer > 3000) {
this.state = "selection";
this.selectionTimer = millis();
}
} else if (this.state === "selection") {
textSize(32);
text("Select the Ingredients!", width / 2, 50);
textSize(24);
text(
`Time left: ${Math.max(
0,
5 - floor((millis() - this.selectionTimer) / 1000)
)}s`,
width / 2,
110
);
if (millis() - this.selectionTimer > 5000) {
this.state = "baking";
this.bakingTimer = millis();
}
} else if (this.state === "baking") {
let elapsedTime = millis() - this.bakingTimer;
let index = floor(elapsedTime / 1500); // Change image every 1.5 seconds
index = min(index, 2);
// Play ticking sound only when baking starts
if (!tickingSound.isPlaying()) {
tickingSound.loop();
tickingSound.setVolume(0.8);
}
// Display the oven images
image(ovenImages[index], 0, 0, width, height);
textSize(32);
fill("#332211");
text("Baking...", width / 2, height / 2);
// Stop ticking and move to result after 4.5 seconds
if (elapsedTime > 4500) {
this.state = "result";
tickingSound.stop();
}
} else if (this.state === "result") {
image(finalImage, 0, 0, width, height); // Display title screen background image
textSize(40);
if (this.checkWin()) {
text(`Your ${this.selectedDish} is now ready!`, width / 2, 200);
image(
dishImages[this.selectedDish],
width / 2 - dishImages[this.selectedDish].width / 2,
300
);
} else {
text("Oh no! Those were the wrong ingredients.", width / 2, height / 2);
}
textSize(20);
text("Click to Play Again", width / 2, height / 2 + 250);
}
}
handleClick() {
if (this.state === "start") {
this.state = "menu";
} else if (this.state === "menu") {
buttons.forEach((button) => {
if (button.checkClick(mouseX, mouseY)) {
this.selectedDish = button.label;
this.correctIngredients = {
// Storing the correct ingredients of the recipes
Brownie: [
"Flour",
"Sugar",
"Eggs",
"Butter",
"Baking Powder",
"Chocolate",
],
Cupcake: ["Flour", "Eggs", "Sugar", "Milk", "Vanilla"],
Pie: ["Flour", "Sugar", "Eggs", "Butter", "Apples"],
}[this.selectedDish];
this.memoryTimer = millis();
this.state = "memory";
}
});
} else if (this.state === "selection") {
this.allIngredients.forEach((ingredient) => {
if (ingredient.checkClick(mouseX, mouseY)) {
ingredient.select();
}
});
} else if (this.state === "result") {
this.resetGame();
}
}
display() { background(255); // Set background to white if (this.state === "selection") { image(shelfImage, 0, 0, width, height); // Display shelf as background } textAlign(CENTER, CENTER); textFont(font); this.allIngredients.forEach((ingredient) => ingredient.display()); stroke(0); strokeWeight(1); fill(0); if (this.state === "start") { image(titleImage, 0, 0, width, height); // Display title screen background image textSize(48); text("Sweet Treats Bakery", width / 2, height / 2 - 50); // Title Text textSize(32); text("Click to Start", width / 2, height / 2 + 50); // Subtitle Text } else if (this.state === "menu") { image(menuImage, 0, 0, width, height); // Display menu screen background image fill("#332211"); textSize(64); text("MENU", width / 2, 100); buttons.forEach((button) => button.display()); } else if (this.state === "memory") { image(recipeImage, 0, 0, width, height); // Display recipe image as background textSize(32); text("Memorize These Ingredients!", width / 2, 80); textFont(honeySaltFont); textSize(35); this.correctIngredients.forEach((ing, i) => text(ing, width / 2, 300 + i * 50) ); if (millis() - this.memoryTimer > 3000) { this.state = "selection"; this.selectionTimer = millis(); } } else if (this.state === "selection") { textSize(32); text("Select the Ingredients!", width / 2, 50); textSize(24); text( `Time left: ${Math.max( 0, 5 - floor((millis() - this.selectionTimer) / 1000) )}s`, width / 2, 110 ); if (millis() - this.selectionTimer > 5000) { this.state = "baking"; this.bakingTimer = millis(); } } else if (this.state === "baking") { let elapsedTime = millis() - this.bakingTimer; let index = floor(elapsedTime / 1500); // Change image every 1.5 seconds index = min(index, 2); // Play ticking sound only when baking starts if (!tickingSound.isPlaying()) { tickingSound.loop(); tickingSound.setVolume(0.8); } // Display the oven images image(ovenImages[index], 0, 0, width, height); textSize(32); fill("#332211"); text("Baking...", width / 2, height / 2); // Stop ticking and move to result after 4.5 seconds if (elapsedTime > 4500) { this.state = "result"; tickingSound.stop(); } } else if (this.state === "result") { image(finalImage, 0, 0, width, height); // Display title screen background image textSize(40); if (this.checkWin()) { text(`Your ${this.selectedDish} is now ready!`, width / 2, 200); image( dishImages[this.selectedDish], width / 2 - dishImages[this.selectedDish].width / 2, 300 ); } else { text("Oh no! Those were the wrong ingredients.", width / 2, height / 2); } textSize(20); text("Click to Play Again", width / 2, height / 2 + 250); } } handleClick() { if (this.state === "start") { this.state = "menu"; } else if (this.state === "menu") { buttons.forEach((button) => { if (button.checkClick(mouseX, mouseY)) { this.selectedDish = button.label; this.correctIngredients = { // Storing the correct ingredients of the recipes Brownie: [ "Flour", "Sugar", "Eggs", "Butter", "Baking Powder", "Chocolate", ], Cupcake: ["Flour", "Eggs", "Sugar", "Milk", "Vanilla"], Pie: ["Flour", "Sugar", "Eggs", "Butter", "Apples"], }[this.selectedDish]; this.memoryTimer = millis(); this.state = "memory"; } }); } else if (this.state === "selection") { this.allIngredients.forEach((ingredient) => { if (ingredient.checkClick(mouseX, mouseY)) { ingredient.select(); } }); } else if (this.state === "result") { this.resetGame(); } }
display() {
    background(255); // Set background to white

    if (this.state === "selection") {
      image(shelfImage, 0, 0, width, height); // Display shelf as background
    }

    textAlign(CENTER, CENTER);
    textFont(font);

    this.allIngredients.forEach((ingredient) => ingredient.display());

    stroke(0);
    strokeWeight(1);
    fill(0);

    if (this.state === "start") {
      image(titleImage, 0, 0, width, height); // Display title screen background image
      textSize(48);
      text("Sweet Treats Bakery", width / 2, height / 2 - 50); // Title Text
      textSize(32);
      text("Click to Start", width / 2, height / 2 + 50); // Subtitle Text
    } else if (this.state === "menu") {
      image(menuImage, 0, 0, width, height); // Display menu screen background image
      fill("#332211");
      textSize(64);
      text("MENU", width / 2, 100);

      buttons.forEach((button) => button.display());
    } else if (this.state === "memory") {
      image(recipeImage, 0, 0, width, height); // Display recipe image as background
      textSize(32);
      text("Memorize These Ingredients!", width / 2, 80);
      textFont(honeySaltFont);
      textSize(35);
      this.correctIngredients.forEach((ing, i) =>
        text(ing, width / 2, 300 + i * 50)
      );

      if (millis() - this.memoryTimer > 3000) {
        this.state = "selection";
        this.selectionTimer = millis();
      }
    } else if (this.state === "selection") {
      textSize(32);
      text("Select the Ingredients!", width / 2, 50);
      textSize(24);
      text(
        `Time left: ${Math.max(
          0,
          5 - floor((millis() - this.selectionTimer) / 1000)
        )}s`,
        width / 2,
        110
      );

      if (millis() - this.selectionTimer > 5000) {
        this.state = "baking";
        this.bakingTimer = millis();
      }
    } else if (this.state === "baking") {
      let elapsedTime = millis() - this.bakingTimer;
      let index = floor(elapsedTime / 1500); // Change image every 1.5 seconds

      index = min(index, 2);

      // Play ticking sound only when baking starts
      if (!tickingSound.isPlaying()) {
        tickingSound.loop();
        tickingSound.setVolume(0.8);
      }

      // Display the oven images
      image(ovenImages[index], 0, 0, width, height);

      textSize(32);
      fill("#332211");
      text("Baking...", width / 2, height / 2);

      // Stop ticking and move to result after 4.5 seconds
      if (elapsedTime > 4500) {
        this.state = "result";
        tickingSound.stop();
      }
    } else if (this.state === "result") {
      image(finalImage, 0, 0, width, height); // Display title screen background image
      textSize(40);
      if (this.checkWin()) {
        text(`Your ${this.selectedDish} is now ready!`, width / 2, 200);
        image(
          dishImages[this.selectedDish],
          width / 2 - dishImages[this.selectedDish].width / 2,
          300
        );
      } else {
        text("Oh no! Those were the wrong ingredients.", width / 2, height / 2);
      }
      textSize(20);
      text("Click to Play Again", width / 2, height / 2 + 250);
    }
  }

  handleClick() {
    if (this.state === "start") {
      this.state = "menu";
    } else if (this.state === "menu") {
      buttons.forEach((button) => {
        if (button.checkClick(mouseX, mouseY)) {
          this.selectedDish = button.label;
          this.correctIngredients = {
            // Storing the correct ingredients of the recipes
            Brownie: [
              "Flour",
              "Sugar",
              "Eggs",
              "Butter",
              "Baking Powder",
              "Chocolate",
            ],
            Cupcake: ["Flour", "Eggs", "Sugar", "Milk", "Vanilla"],
            Pie: ["Flour", "Sugar", "Eggs", "Butter", "Apples"],
          }[this.selectedDish];

          this.memoryTimer = millis();
          this.state = "memory";
        }
      });
    } else if (this.state === "selection") {
      this.allIngredients.forEach((ingredient) => {
        if (ingredient.checkClick(mouseX, mouseY)) {
          ingredient.select();
        }
      });
    } else if (this.state === "result") {
      this.resetGame();
    }
  }

The design of the game is fairly simple in concept, though I still struggled when making the intial base of the code. I had started by creating a prototype version, which I experimented on until I was satisfied with the game flow. That version was plain, with no graphics, and circles used as placeholders for the actual ingredients.

I like to think the final game looks better than this.

Not to say I didn’t struggle even after adjusting the prototype. While inserting the backgrounds and graphics, I realised that I had just simply hardcoded values for the recipes on the recipe selection screen, and that I should use buttons instead in my final code.


Making these buttons turned out to be challenging, as I had to make another class for them. I kept facing an error that would not let me adjust the y and x coordinates of the buttons, leading them to overlap. Several lines of code carefully inspected later, I found the error within the display function of the Game class, which was simultaneously calling the buttons alongside the original button class, leading to the code refreshing everytime to use the wrong coordinates for the buttons. After that hiccup, it was mostly smooth sailing.

I also implemented background music (see if you can recognise the instrumental) and a sound effect to my game. The sound effect is a ticking sound that plays for the duration of the “baking…” scene, which I thought would add a nice little touch to make the game a little more interactive.

Overall, I am very satisfied with my midterm project, and it turned out very close to what I had originally envisioned it to be, making the end product all the more rewarding to see.

Link to full screen version


 

Midterm – Interactive Party

My project is an interactive music visualizer designed to create a party-like experience. It integrates sound, visuals, and interactivity to simulate a dynamic party environment. The motivation behind this project stems from my love for music and aesthetics, aiming to bring both together in a visually engaging way. The experience features different sound modes, including a salsa mode, and reacts dynamically to audio amplitude to enhance the party atmosphere.

Link to Fullscreen:https://editor.p5js.org/Genesisreyes/full/okpJ6jCqf

It features a background DJ image, a GIF animation for the salsa mode, and dynamically changing colors and light effects. OOP is implemented in the LiquidShape class, which generates butterfly-like abstract shapes that move and react to the music’s amplitude. The project also includes button interactions, allowing users to start the party, switch to salsa mode, and switch between songs by clicking, or return to the main page. The use of p5.Amplitude() ensures that elements like background flashing and shape movements are synchronized with the audio intensity, creating a smooth experience.

Key Highlights

1. Object-Oriented Programming (OOP) for Animation

The LiquidShape class is responsible for creating and animating butterfly-like figures that move fluidly across the screen. These shapes:

    • Respond to the music amplitude by changing motion and size.
    • Have two states: floating (trapped) and moving (liberated), which the user can toggle by clicking.
    • Implement procedural animation using noise-based randomization to ensure organic movement.

2. Audio Amplitude Analysis for Visual Synchronization

The p5.Amplitude() function is used to analyze the loudness of the currently playing audio and link it to the visual elements. The butterfly image pulses with the music, making it feel more immersive.

let level = amplitude.getLevel();

let pulseSize = lerp(width * 0.2, width * 0.4, level);
image(butterflyBg, 0, 0, pulseSize, pulseSize);

How this works:

    • amplitude.getLevel() returns a value between 0 (silence) and 1 (loudest part of the track).
    • lerp() smoothly transitions the butterfly image size between 20% and 40% of the screen width based on the music volume.
    • This creates a natural pulsing effect that visually represents the beat.

3. Smooth Audio Transitions Between Tracks

Instead of abruptly switching sounds,  fade-out and fade-in effects were used when transitioning between songs to create a smoother experience.

function mousePressed() {
  if (!started) return;

  let fadeDuration = 1.5; // Fade out in 1.5 seconds
  if (sounds[currentSoundIndex].isPlaying()) {
    sounds[currentSoundIndex].setVolume(0, fadeDuration);
    setTimeout(() => {
      sounds[currentSoundIndex].stop();
      switchTrack();
    }, fadeDuration * 1000);
  } else {
    switchTrack();
  }
}
    • The current song fades out smoothly over 1.5 seconds.
    • After fading, it stops completely and switches to the next track.
    • The new track fades in, avoiding harsh audio jumps.

A major challenge was working with WEBGL, especially in handling 2D elements like buttons alongside 3D rendering. The solution involved creating a separate graphics layer (bgGraphics) for the background image.

function windowResized() {
  resizeCanvas(windowWidth, windowHeight); // Resize the WEBGL canvas
  bgGraphics.resizeCanvas(windowWidth, windowHeight); // Resize 2D graphics buffer
  updateBackground(); // Redraw the background image
}

function updateBackground() {
  bgGraphics.background(0); // Fallback if the image doesn't load

  let imgWidth, imgHeight;
  let canvasAspect = windowWidth / windowHeight;
  let imgAspect = djImage.width / djImage.height;

  if (canvasAspect > imgAspect) {
    imgWidth = windowWidth;
    imgHeight = windowWidth / imgAspect;
  } else {
    imgHeight = windowHeight;
    imgWidth = windowHeight * imgAspect;
  }

  bgGraphics.imageMode(CENTER);
  bgGraphics.image(djImage, windowWidth / 2, windowHeight / 2, imgWidth, imgHeight);
}

One of the best aspects of the project is its ability to capture the energy of a party through interactive visuals and sound. The combination of image overlays, and reactive shapes enhances the user’s engagement.

There are areas that could be improved. One issue is optimizing the responsiveness of visual elements when resizing the window, as some components may not scale perfectly. Another challenge was ensuring seamless audio transitions between different sound modes without abrupt stops. Additionally, adding clear instructions for the users would have been ideal, so they could know that when they click, they can change the music but also collect and liberate the butterflies.

Midterm project – Whack a Mole

For my midterm project, I decided to create my own version of the classic Whack-A-Mole game. The goal was to design an interactive experience where players could test their reflexes by clicking on moles as they randomly appeared on the screen. It was inspired by both traditional arcade versions and mobile games, where I wanted to reach a balance between simple navigation and entertainment.

One of the aspects I’m particularly proud of is the way I structured the game logic. I tried to keep my code modular by organizing key components into functions. This not only made the development process more efficient but also allowed me to tweak game parameters easily. Another feature I really enjoyed implementing was the randomization of mole appearances that ensures that no two games feel exactly the same. The timing and positioning algorithms took some hard work, but I’m happy with how smooth the flow of the game turned out.

How it works: Moles pop up from different holes at random intervals, and the player must click on them to score points. Each mole stays visible for a short duration before disappearing, which requires quick reactions from the player. As the game progresses, the frequency of mole appearances increases, adding a greater challenge over time. To add an element of risk, I implemented a bomb mechanic, meaning that if a player accidentally clicks on a bomb instead of a mole, the game ends immediately. This twist keeps players alert and encourages more precise clicking. The game runs in a continuous loop with constant updates on the screen that track the player’s score. Each successful mole hit adds points, while avoiding bombs becomes crucial for “survival”.

Challenges: One of the biggest issues was making sure the game felt responsive without being overwhelming. Initially, the moles appeared and disappeared too quickly which made it nearly impossible to hit them in time. I had to experiment with different timing settings to find the right balance. Another tricky aspect was collision detection, where I had to make sure that clicks were registered correctly when a mole is hit and not on empty spots or bombs. Debugging this issue took some time, but through testing and refining my hitbox logic, I was able to get it working.

Implementation: The game is built with JavaScript and p5.js, which handle the graphics and interactions. I used free images and sound effects from online stocks to make the game more engaging.  The Game class manages the core mechanics which includes the following:

  • Hole and Object Management – moles and bombs are placed randomly to keep the game unpredictable
  • Scoring System – players earn points for hitting moles, while bombs immediately end the game
  • Difficulty Scaling – as the game progresses, moles appear and disappear faster, making it more challenging
  • Sound Effects and Graphics – Background music, whacking sounds, and animations add to an immersive experienceCode snippets:>>Managing moles and bombs
    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    if (mouseIsPressed) {
    if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) {
    mouseIsPressed = false;
    punched = true;
    sounds.whak.play();
    setTimeout(() => {
    punched = false;
    }, 200);
    if (hole.type == "mole") {
    hole.type = "hole";
    game.score += 1;
    game.timer += 30;
    } else {
    sounds.bomb.play();
    gameOver();
    }
    }
    }
    if (mouseIsPressed) { if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) { mouseIsPressed = false; punched = true; sounds.whak.play(); setTimeout(() => { punched = false; }, 200); if (hole.type == "mole") { hole.type = "hole"; game.score += 1; game.timer += 30; } else { sounds.bomb.play(); gameOver(); } } }
    if (mouseIsPressed) {
      if (dist(mouseX, mouseY, hole.x, hole.y) < hole.d) {
        mouseIsPressed = false;
        punched = true;
        sounds.whak.play();
        setTimeout(() => {
          punched = false;
        }, 200);
        if (hole.type == "mole") {
          hole.type = "hole";
          game.score += 1;
          game.timer += 30;
        } else {
          sounds.bomb.play();
          gameOver();
        }
      }
    }
    

    >>Registering the mouse clicks

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    if (frameCount % (game.difficulty - game.score) == 0) {
    let hole = random(game.holes);
    if (hole.type == "mole") {
    hole.type = "hole";
    } else {
    hole.type = "mole";
    }
    if (random(1) < 0.1) {
    hole.type = "bomb";
    setTimeout(() => {
    hole.type = "hole";
    }, 1000);
    }
    }
    if (frameCount % (game.difficulty - game.score) == 0) { let hole = random(game.holes); if (hole.type == "mole") { hole.type = "hole"; } else { hole.type = "mole"; } if (random(1) < 0.1) { hole.type = "bomb"; setTimeout(() => { hole.type = "hole"; }, 1000); } }
    if (frameCount % (game.difficulty - game.score) == 0) {
      let hole = random(game.holes);
      if (hole.type == "mole") {
        hole.type = "hole";
      } else {
        hole.type = "mole";
      }
      if (random(1) < 0.1) {
        hole.type = "bomb";
        setTimeout(() => {
          hole.type = "hole";
        }, 1000);
      }
    }
    

    >>Game Over logic

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    function gameOver() {
    sounds.gameover.play();
    setTimeout(() => {
    sounds.back.stop();
    image(imgs.blast.img, mouseX, mouseY, 250, 250);
    background(20, 100);
    textSize(64);
    text("Game Over", width / 2, height / 2);
    textSize(16);
    text("click anywhere to restart!", width / 2, height - 50);
    textSize(46);
    text(game.score, width / 2, 35);
    state = "gameOver";
    }, 100);
    noLoop();
    }
    function gameOver() { sounds.gameover.play(); setTimeout(() => { sounds.back.stop(); image(imgs.blast.img, mouseX, mouseY, 250, 250); background(20, 100); textSize(64); text("Game Over", width / 2, height / 2); textSize(16); text("click anywhere to restart!", width / 2, height - 50); textSize(46); text(game.score, width / 2, 35); state = "gameOver"; }, 100); noLoop(); }
    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
      noLoop();
    }
    
    

    Improvements: Looking forward, there are definitely some areas that I’d like to improve. One feature I would like to add is a progressive difficulty system, where players are challenged at different levels of difficulty. Right now, the game is fun but could benefit from more depth. On top of that, I’d like to upgrade the user interface by adding a “start” and “home” screen, score tracker, and possibly a leaderboard.

    Plain text
    Copy to clipboard
    Open code in new window
    EnlighterJS 3 Syntax Highlighter
    let imgs = {};
    let sounds = {};
    let punched = false;
    let state = "start";
    function preload() {
    backImg = loadImage("assets/back.png");
    font = loadFont("assets/Sigmar-Regular.ttf");
    imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 };
    imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 };
    imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 };
    imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 };
    imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 };
    sounds.bomb = loadSound("sounds/Bomb hit.mp3");
    sounds.back = loadSound("sounds/Game main theme.mp3");
    sounds.gameover = loadSound("sounds/game-over.mp3");
    sounds.whak = loadSound("sounds/Whacking a mole.mp3");
    }
    function setup() {
    createCanvas(600, 600);
    imageMode(CENTER);
    textFont(font);
    game = new Game();
    textAlign(CENTER, CENTER);
    }
    function draw() {
    image(backImg, width / 2, height / 2, width, height);
    switch (state) {
    case "start":
    sounds.back.stop();
    textSize(68);
    fill(255);
    text("WhACK!\na MOLE!", width / 2, height / 2 - 120);
    textSize(16);
    text("press anywhere to start!", width / 2, height - 30);
    textSize(26);
    let img = [imgs.hole, imgs.mole, imgs.bomb];
    image(
    img[floor(frameCount / 60) % 3].img,
    width / 2 + img[floor(frameCount / 60) % 3].xoff,
    height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff
    );
    if (mouseIsPressed) {
    mouseIsPressed = false;
    sounds.whak.play();
    state = "game";
    }
    break;
    case "game":
    game.show();
    if (!sounds.back.isPlaying()) sounds.back.play();
    break;
    }
    if (mouseX != 0 && mouseY != 0) {
    push();
    translate(mouseX, mouseY + 10);
    if (punched) {
    rotate(-PI / 2);
    }
    scale(map(game.holes.length, 4, 20, 1, 0.25));
    image(imgs.hammer.img, 0, 0, 150, 150);
    pop();
    }
    }
    function mousePressed() {
    if (state == "gameOver") {
    state = "start";
    game = new Game();
    mouseIsPressed = false;
    loop();
    }
    }
    function gameOver() {
    sounds.gameover.play();
    setTimeout(() => {
    sounds.back.stop();
    image(imgs.blast.img, mouseX, mouseY, 250, 250);
    background(20, 100);
    textSize(64);
    text("Game Over", width / 2, height / 2);
    textSize(16);
    text("click anywhere to restart!", width / 2, height - 50);
    textSize(46);
    text(game.score, width / 2, 35);
    state = "gameOver";
    }, 100);
    noLoop();
    }
    class Game {
    constructor() {
    this.x = 10;
    this.y = height / 2 - 80;
    this.w = width - 20;
    this.h = height / 2 + 70;
    this.holesNum = 4;
    this.holes = [];
    this.difficulty = 60;
    this.score = 0;
    this.timer = 4800;
    }
    show() {
    //timer
    if (this.timer > 4800) this.timer = 4800;
    this.timer -= 1.5;
    fill(20, 100);
    rect(10, 5, width - 20, 10);
    fill(255);
    rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10);
    if (this.timer < 0) {
    mouseX = width / 2;
    mouseY = height / 2;
    gameOver();
    }
    //score
    fill(255);
    textSize(46);
    if (punched) textSize(54);
    text(this.score, width / 2, 35);
    if (this.holesNum != this.holes.length) {
    this.holes = this.findHolePositions(1);
    }
    for (let i = 0; i < this.holes.length; i++) {
    push();
    translate(this.holes[i].x, this.holes[i].y);
    scale(this.holes[i].d / 250);
    let img;
    switch (this.holes[i].type) {
    case "hole":
    img = imgs.hole;
    //nothing
    break;
    case "mole":
    img = imgs.mole;
    break;
    case "bomb":
    img = imgs.bomb;
    break;
    }
    if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") {
    //check mouse click on mole
    if (mouseIsPressed) {
    if (
    dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) <
    this.holes[i].d
    ) {
    mouseIsPressed = false;
    punched = true;
    sounds.whak.play();
    setTimeout(() => {
    punched = false;
    }, 200);
    if (this.holes[i].type == "mole") {
    this.holes[i].type = "hole";
    this.score += 1;
    this.timer += 30;
    } else {
    sounds.bomb.play();
    gameOver();
    }
    }
    }
    }
    image(img.img, img.xoff, img.yoff);
    pop();
    }
    if (this.difficulty - this.score < 20) {
    this.difficulty += 30;
    this.holesNum += 1;
    }
    if (frameCount % (this.difficulty - this.score) == 0) {
    let hole = random(this.holes);
    if (hole.type == "mole") {
    hole.type = "hole";
    } else {
    hole.type = "mole";
    }
    if (random(1) < 0.1) {
    hole.type = "bomb";
    setTimeout(() => {
    hole.type = "hole";
    }, 1000);
    }
    }
    }
    findHolePositions(n, d = 200) {
    let arr = [];
    for (let i = 0; i < this.holesNum; i++) {
    let x = random(this.x + d / 2, this.x + this.w - d / 2);
    let y = random(this.y + d / 2, this.y + this.h - d / 2);
    arr.push({ x: x, y: y, d: d, type: "hole" });
    }
    //no hole should overlap
    for (let i = 0; i < arr.length; i++) {
    for (let j = 0; j < arr.length; j++) {
    if (i != j) {
    let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y);
    if (d_ < d) {
    n += 1;
    if (n > 50) {
    n = 0;
    d *= 0.9;
    return this.findHolePositions(n, d);
    }
    return this.findHolePositions(n, d);
    }
    }
    }
    }
    return arr;
    }
    }
    let imgs = {}; let sounds = {}; let punched = false; let state = "start"; function preload() { backImg = loadImage("assets/back.png"); font = loadFont("assets/Sigmar-Regular.ttf"); imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 }; imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 }; imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 }; imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 }; imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 }; sounds.bomb = loadSound("sounds/Bomb hit.mp3"); sounds.back = loadSound("sounds/Game main theme.mp3"); sounds.gameover = loadSound("sounds/game-over.mp3"); sounds.whak = loadSound("sounds/Whacking a mole.mp3"); } function setup() { createCanvas(600, 600); imageMode(CENTER); textFont(font); game = new Game(); textAlign(CENTER, CENTER); } function draw() { image(backImg, width / 2, height / 2, width, height); switch (state) { case "start": sounds.back.stop(); textSize(68); fill(255); text("WhACK!\na MOLE!", width / 2, height / 2 - 120); textSize(16); text("press anywhere to start!", width / 2, height - 30); textSize(26); let img = [imgs.hole, imgs.mole, imgs.bomb]; image( img[floor(frameCount / 60) % 3].img, width / 2 + img[floor(frameCount / 60) % 3].xoff, height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff ); if (mouseIsPressed) { mouseIsPressed = false; sounds.whak.play(); state = "game"; } break; case "game": game.show(); if (!sounds.back.isPlaying()) sounds.back.play(); break; } if (mouseX != 0 && mouseY != 0) { push(); translate(mouseX, mouseY + 10); if (punched) { rotate(-PI / 2); } scale(map(game.holes.length, 4, 20, 1, 0.25)); image(imgs.hammer.img, 0, 0, 150, 150); pop(); } } function mousePressed() { if (state == "gameOver") { state = "start"; game = new Game(); mouseIsPressed = false; loop(); } } function gameOver() { sounds.gameover.play(); setTimeout(() => { sounds.back.stop(); image(imgs.blast.img, mouseX, mouseY, 250, 250); background(20, 100); textSize(64); text("Game Over", width / 2, height / 2); textSize(16); text("click anywhere to restart!", width / 2, height - 50); textSize(46); text(game.score, width / 2, 35); state = "gameOver"; }, 100); noLoop(); } class Game { constructor() { this.x = 10; this.y = height / 2 - 80; this.w = width - 20; this.h = height / 2 + 70; this.holesNum = 4; this.holes = []; this.difficulty = 60; this.score = 0; this.timer = 4800; } show() { //timer if (this.timer > 4800) this.timer = 4800; this.timer -= 1.5; fill(20, 100); rect(10, 5, width - 20, 10); fill(255); rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10); if (this.timer < 0) { mouseX = width / 2; mouseY = height / 2; gameOver(); } //score fill(255); textSize(46); if (punched) textSize(54); text(this.score, width / 2, 35); if (this.holesNum != this.holes.length) { this.holes = this.findHolePositions(1); } for (let i = 0; i < this.holes.length; i++) { push(); translate(this.holes[i].x, this.holes[i].y); scale(this.holes[i].d / 250); let img; switch (this.holes[i].type) { case "hole": img = imgs.hole; //nothing break; case "mole": img = imgs.mole; break; case "bomb": img = imgs.bomb; break; } if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") { //check mouse click on mole if (mouseIsPressed) { if ( dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) < this.holes[i].d ) { mouseIsPressed = false; punched = true; sounds.whak.play(); setTimeout(() => { punched = false; }, 200); if (this.holes[i].type == "mole") { this.holes[i].type = "hole"; this.score += 1; this.timer += 30; } else { sounds.bomb.play(); gameOver(); } } } } image(img.img, img.xoff, img.yoff); pop(); } if (this.difficulty - this.score < 20) { this.difficulty += 30; this.holesNum += 1; } if (frameCount % (this.difficulty - this.score) == 0) { let hole = random(this.holes); if (hole.type == "mole") { hole.type = "hole"; } else { hole.type = "mole"; } if (random(1) < 0.1) { hole.type = "bomb"; setTimeout(() => { hole.type = "hole"; }, 1000); } } } findHolePositions(n, d = 200) { let arr = []; for (let i = 0; i < this.holesNum; i++) { let x = random(this.x + d / 2, this.x + this.w - d / 2); let y = random(this.y + d / 2, this.y + this.h - d / 2); arr.push({ x: x, y: y, d: d, type: "hole" }); } //no hole should overlap for (let i = 0; i < arr.length; i++) { for (let j = 0; j < arr.length; j++) { if (i != j) { let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y); if (d_ < d) { n += 1; if (n > 50) { n = 0; d *= 0.9; return this.findHolePositions(n, d); } return this.findHolePositions(n, d); } } } } return arr; } }
    let imgs = {};
    let sounds = {};
    let punched = false;
    let state = "start";
    
    function preload() {
      backImg = loadImage("assets/back.png");
      font = loadFont("assets/Sigmar-Regular.ttf");
      imgs.blast = { img: loadImage("assets/blast.png"), xoff: 0, yoff: 0 };
      imgs.bomb = { img: loadImage("assets/bomb.png"), xoff: 0, yoff: 0 };
      imgs.hammer = { img: loadImage("assets/hammer.png"), xoff: 0, yoff: 0 };
      imgs.hole = { img: loadImage("assets/hole.png"), xoff: 0, yoff: 30 };
      imgs.mole = { img: loadImage("assets/mole.png"), xoff: 0, yoff: 0 };
    
      sounds.bomb = loadSound("sounds/Bomb hit.mp3");
      sounds.back = loadSound("sounds/Game main theme.mp3");
      sounds.gameover = loadSound("sounds/game-over.mp3");
      sounds.whak = loadSound("sounds/Whacking a mole.mp3");
    }
    
    function setup() {
      createCanvas(600, 600);
      imageMode(CENTER);
      textFont(font);
      game = new Game();
      textAlign(CENTER, CENTER);
    }
    
    function draw() {
      image(backImg, width / 2, height / 2, width, height);
      switch (state) {
        case "start":
          sounds.back.stop();
    
          textSize(68);
          fill(255);
          text("WhACK!\na MOLE!", width / 2, height / 2 - 120);
          textSize(16);
          text("press anywhere to start!", width / 2, height - 30);
    
          textSize(26);
          let img = [imgs.hole, imgs.mole, imgs.bomb];
          image(
            img[floor(frameCount / 60) % 3].img,
            width / 2 + img[floor(frameCount / 60) % 3].xoff,
            height / 2 + 150 + img[floor(frameCount / 60) % 3].yoff
          );
          if (mouseIsPressed) {
            mouseIsPressed = false;
            sounds.whak.play();
            state = "game";
          }
          break;
        case "game":
          game.show();
          if (!sounds.back.isPlaying()) sounds.back.play();
          break;
      }
      if (mouseX != 0 && mouseY != 0) {
        push();
        translate(mouseX, mouseY + 10);
        if (punched) {
          rotate(-PI / 2);
        }
        scale(map(game.holes.length, 4, 20, 1, 0.25));
        image(imgs.hammer.img, 0, 0, 150, 150);
        pop();
      }
    }
    
    function mousePressed() {
      if (state == "gameOver") {
        state = "start";
        game = new Game();
        mouseIsPressed = false;
        loop();
      }
    }
    function gameOver() {
      sounds.gameover.play();
      setTimeout(() => {
        sounds.back.stop();
        image(imgs.blast.img, mouseX, mouseY, 250, 250);
        background(20, 100);
        textSize(64);
        text("Game Over", width / 2, height / 2);
        textSize(16);
        text("click anywhere to restart!", width / 2, height - 50);
        textSize(46);
        text(game.score, width / 2, 35);
        state = "gameOver";
      }, 100);
    
      noLoop();
    }
    
    class Game {
      constructor() {
        this.x = 10;
        this.y = height / 2 - 80;
        this.w = width - 20;
        this.h = height / 2 + 70;
    
        this.holesNum = 4;
        this.holes = [];
        this.difficulty = 60;
        this.score = 0;
        this.timer = 4800;
      }
      show() {
        //timer
        if (this.timer > 4800) this.timer = 4800;
        this.timer -= 1.5;
        fill(20, 100);
        rect(10, 5, width - 20, 10);
        fill(255);
        rect(10, 5, map(this.timer, 0, 4800, 0, width - 20), 10);
        if (this.timer < 0) {
          mouseX = width / 2;
          mouseY = height / 2;
          gameOver();
        }
    
        //score
        fill(255);
        textSize(46);
        if (punched) textSize(54);
        text(this.score, width / 2, 35);
    
        if (this.holesNum != this.holes.length) {
          this.holes = this.findHolePositions(1);
        }
        for (let i = 0; i < this.holes.length; i++) {
          push();
          translate(this.holes[i].x, this.holes[i].y);
          scale(this.holes[i].d / 250);
          let img;
          switch (this.holes[i].type) {
            case "hole":
              img = imgs.hole;
              //nothing
              break;
            case "mole":
              img = imgs.mole;
              break;
            case "bomb":
              img = imgs.bomb;
              break;
          }
    
          if (this.holes[i].type == "mole" || this.holes[i].type == "bomb") {
            //check mouse click on mole
            if (mouseIsPressed) {
              if (
                dist(mouseX, mouseY, this.holes[i].x, this.holes[i].y) <
                this.holes[i].d
              ) {
                mouseIsPressed = false;
                punched = true;
                sounds.whak.play();
                setTimeout(() => {
                  punched = false;
                }, 200);
                if (this.holes[i].type == "mole") {
                  this.holes[i].type = "hole";
                  this.score += 1;
                  this.timer += 30;
                } else {
                  sounds.bomb.play();
                  gameOver();
                }
              }
            }
          }
          image(img.img, img.xoff, img.yoff);
          pop();
        }
        if (this.difficulty - this.score < 20) {
          this.difficulty += 30;
          this.holesNum += 1;
        }
    
        if (frameCount % (this.difficulty - this.score) == 0) {
          let hole = random(this.holes);
          if (hole.type == "mole") {
            hole.type = "hole";
          } else {
            hole.type = "mole";
          }
          if (random(1) < 0.1) {
            hole.type = "bomb";
            setTimeout(() => {
              hole.type = "hole";
            }, 1000);
          }
        }
      }
    
      findHolePositions(n, d = 200) {
        let arr = [];
    
        for (let i = 0; i < this.holesNum; i++) {
          let x = random(this.x + d / 2, this.x + this.w - d / 2);
          let y = random(this.y + d / 2, this.y + this.h - d / 2);
          arr.push({ x: x, y: y, d: d, type: "hole" });
        }
        //no hole should overlap
        for (let i = 0; i < arr.length; i++) {
          for (let j = 0; j < arr.length; j++) {
            if (i != j) {
              let d_ = dist(arr[i].x, arr[i].y, arr[j].x, arr[j].y);
              if (d_ < d) {
                n += 1;
                if (n > 50) {
                  n = 0;
                  d *= 0.9;
                  return this.findHolePositions(n, d);
                }
                return this.findHolePositions(n, d);
              }
            }
          }
        }
        return arr;
      }
    }
    

    The Visuals:

Midterm Project Plan

Scope of Work
For my midterm project, I am designing a digital version of the classic Whack-a-Mole game, inspired by the attached references. The goal is to create an engaging and interactive game where players use their mouse to “whack” moles as they pop out of holes. The game should challenge players’ reflexes and introduce risk elements like bombs that add complexity.

The game will start with a Start Screen featuring the game title and a simple “Start” button. I also plan to add access settings, like toggling sound on or off. Once the game begins, moles will randomly pop up from a grid of holes, and the player must click or tap on them to score points. Not every hole will be safe. Occasionally, a bomb will pop up, and hitting it will result in losing points or lives. Players will have a limited time (for example, 60 seconds) to score as many points as possible before the timer runs out. As the game progresses, the difficulty will increase, with moles appearing and disappearing faster, making the game more challenging.

Each successful hit of a mole adds 10 points, while mistakenly hitting a bomb will deduct 20 points or reduce a life. The game will display a score counter and a countdown timer to keep players aware of their progress and remaining time. At the end of the game, an End Screen will appear, showing the final score and offering options to “Play Again” or “Quit.”

I want the grid to contain between 9 and 16 holes, depending on the level of complexity I decide to implement. Moles and bombs will randomly pop up in these holes at varying intervals. The randomness is crucial to establish unpredictability. To add to the challenge, the moles will pop up faster as time progresses, requiring quicker reflexes from the player.

Code Structure

For the game’s development, I plan to use an object-oriented approach. The game will be structured around a few core classes:

  • Game Class: Manages the overall game loop, score tracking, and time countdown
  • Mole Class: Controls the mole’s behavior (when it pops up, how long it stays, and how it reacts to player interaction)
  • Bomb Class: Functions similarly to the mole but triggers penalties when a bomb is clicked
  • Hole Class: Represents each position on the grid, randomly spawning moles or bombs
  • UI Class: Manages elements like the start screen, score display, timer, and end screen

The core gameplay loop will rely on these functions:

  • startGame(): Initializes the game and resets scores and timers
  • spawnMole(): Randomly selects holes for moles to appear
  • spawnBomb(): Introduces bombs with a set probability
  • whackMole(): Detects player clicks and updates the score
  • hitBomb(): Triggers penalties for clicking bombs
  • updateTimer(): Counts down and ends the game when time runs out
  • increaseDifficulty(): Speeds up mole appearances as the game progresses

Challenges and Risks

I expect that one of the most complex parts of this project would be ensuring accurate collision detection. In other words, making sure the program properly registers when a player clicks on a mole or bomb.

Timing is also a big concern. Moles need to appear and disappear at unpredictable but balanced intervals to make the pace of the game flawless and not frustrating.

To tackle these challenges, I plan to build a test script focused on collision detection, using simple shapes before applying it to the actual mole and bomb sprites. This will help me adjust the hitboxes and make sure user interactions feel responsive. I might also test randomization algorithms to ensure that mole and bomb appearances are unpredictable yet adequate.

Midterm Progress

I want to create a personalized DJ experience that allows users to choose different music genres, with the environment adapting accordingly. The idea is to present an interactive space where visuals, lighting, and animations react dynamically to the music and make it feel like a real party.

When the experience starts, a button launches the animation, and clicking anywhere switches songs while updating the environment to match the new song. The visuals rely on p5.Amplitude() to analyze the music’s intensity and adjust the movement of butterfly-like shapes accordingly (I reused my previous code to draw the butterflies).

One of the biggest challenges was managing these transitions without them feeling too sudden or chaotic. Initially, switching between songs resulted in jarring color and lighting changes, breaking the immersion. To fix this, I used lerpColor() to gradually shift the background and object colors rather than having them change instantly. Another issue was synchronizing the visuals with the audio in a meaningful way, at first, the amplitude mapping was too sensitive, making the animations look erratic -this still needs improvement maybe I will try modifying the amplitude scaling.

Moving forward, I plan to expand the genre selection with more styles and refine how users interact with the interface. I want each environment to reflect the music’s vibe.

Week 5 response

Computer vision differs from human vision in that humans perceive the world more holistically and understand visual cues based on experience and context, whereas computers use quantitative forms of image representation. Instead of recognizing things based on mental processes, machines use algorithmic and pattern recognition techniques based on pixel-based image representation.

Thus, compared to humans, computers also have difficulty identifying objects with different illuminations and directions, unless they are highly trained with varied databases. Just as humans estimate depth and motion based on vision and general knowledge, computer programs need specific methods such as optical flow detection, edge detection or machine learning algorithms to deduce similar information.

The power of computer vision to capture motion and analyze visual information has a profound effect on interactive art. Artists can take advantage of these technologies and use them to create installations that respond dynamically to the viewer’s movements, gestures, or even facial expressions and create immersive, interactive experiences. However, these technologies can also raise ethical issues, related to privacy and surveillance if we talk about the use of facial recognition and motion detection in interactive artworks. Consequently, artists working with computer vision must carefully weigh their creative possibilities with the ethical implications linked to surveillance culture.

Week 5: Reading Response

Computer vision is really different from how humans see the world. While we naturally process images, depth, and context with our brains, computers rely on algorithms and sensors to make sense of what they’re “seeing.” Humans can instantly recognize faces, emotions, and even artistic meaning without much effort, but computers need tons of data and training to even get close to that level of understanding. Plus, human vision adjusts easily to different lighting or angles, while computers often struggle unless the conditions are just right.

To help computers track or recognize what we want, we use techniques like edge detection, motion tracking, and pattern recognition. Edge detection helps separate objects from the background, while motion tracking follows movement across frames in a video. Pattern recognition is also huge—by training a model with a bunch of images, it can learn to recognize faces, shapes, or even specific objects. But to get accurate results, we usually have to clean up the data first, removing noise or adjusting lighting so the system doesn’t get confused.

The ability of computer vision to track and monitor things has a big impact on interactive art. Artists use it to create pieces that respond to movement or presence, making the experience more immersive and engaging. But at the same time, it raises ethical concerns—these same tools can be used for surveillance, sometimes without people knowing. So while computer vision opens up exciting possibilities for art, it also forces us to think about privacy and how we balance creativity with responsibility.

Week 5: Midterm Progress

Concept:

I decided on a calming spa game where the user (or viewer) sees a person lying in a clinic setting, complete with subtle animations like steam or aroma particles. Even though I haven’t fully finalized all interactivity elements, my plan is to allow some simple interactions, such as choosing different spa treatments or changing certain visual elements. For now, the main focus is creating the environment and making it look professional and aesthetically pleasing.

Code and Design:

I started laying out the main structure in p5.js. I separated the code into different functions to keep things organized:

  • drawClinicBackground(): Sets the scene with the walls, floor, decorations, and additional details like a window or posters.
  • drawTreatmentBed(): Draws the bed and pillow for the patient.
  • drawPatient(): Renders the patient’s upper body and face, including minimal facial features.
  • drawSteam(): Handles the animation of steam or aroma particles rising around the face.

I’m also planning to introduce classes if the animation or interactivity becomes more complex, especially if I need multiple interactive objects or more sophisticated animations. This modular approach helps keep things clean. If I need to expand later—maybe adding sound effects, more interactive objects, or advanced animations—I can easily integrate new code.

This is what the design is supposed to look like:

Version 1.0.0

Frightening/Challenging aspects:

One of the most uncertain parts of my project is making the environment feel truly interactive and alive. I’m worried about how performance might be affected if I add a lot of animations or interactive elements at once. Another concern is making sure the art style and animations blend nicely so that the scene doesn’t look disjointed.

To reduce this risk, I wrote some test code to experiment with particle systems and layering. Specifically, I tested out how many steam particles I can animate in real-time without causing a slowdown. I also experimented with gradient backgrounds, images, and more detailed drawings to see how far I could push the visuals before I start seeing performance drops.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
function drawSteam() {
// Draw and update each steam particle
noStroke();
for (let i = 0; i < steamParticles.length; i++) {
let p = steamParticles[i];
fill(255, 255, 255, p.alpha);
ellipse(p.x, p.y, p.size, p.size);
// Move the particle upwards
p.y -= p.speed;
// Small horizontal "drift"
p.x += map(noise(p.y * 0.01, p.x * 0.01), 0, 1, -0.2, 0.2);
// Gradually fade out
p.alpha -= 0.2;
// Reset the particle when it goes out of range
if (p.y < height / 2 - 80 || p.alpha < 0) {
steamParticles[i] = createSteamParticle();
steamParticles[i].y = height / 2 + random(0, 30);
steamParticles[i].alpha = random(100, 150);
}
}
}
function drawSteam() { // Draw and update each steam particle noStroke(); for (let i = 0; i < steamParticles.length; i++) { let p = steamParticles[i]; fill(255, 255, 255, p.alpha); ellipse(p.x, p.y, p.size, p.size); // Move the particle upwards p.y -= p.speed; // Small horizontal "drift" p.x += map(noise(p.y * 0.01, p.x * 0.01), 0, 1, -0.2, 0.2); // Gradually fade out p.alpha -= 0.2; // Reset the particle when it goes out of range if (p.y < height / 2 - 80 || p.alpha < 0) { steamParticles[i] = createSteamParticle(); steamParticles[i].y = height / 2 + random(0, 30); steamParticles[i].alpha = random(100, 150); } } }
function drawSteam() {
  // Draw and update each steam particle
  noStroke();
  for (let i = 0; i < steamParticles.length; i++) {
    let p = steamParticles[i];
    
    fill(255, 255, 255, p.alpha);
    ellipse(p.x, p.y, p.size, p.size);
    
    // Move the particle upwards
    p.y -= p.speed;
    // Small horizontal "drift"
    p.x += map(noise(p.y * 0.01, p.x * 0.01), 0, 1, -0.2, 0.2);
    // Gradually fade out
    p.alpha -= 0.2;
    
    // Reset the particle when it goes out of range
    if (p.y < height / 2 - 80 || p.alpha < 0) {
      steamParticles[i] = createSteamParticle();
      steamParticles[i].y = height / 2 + random(0, 30);
      steamParticles[i].alpha = random(100, 150);
    }
  }
}

 

Things to prevent:

  • Overcomplicating the Code: I’m trying not to throw everything in one giant file without structure. By using separate functions (and potentially classes), I’ll keep my code organized and easier to debug.
  • Performance Bottlenecks: Adding too many particles or large images could slow down the sketch. I’m keeping an eye on frame rates and testing on different devices so I can catch performance issues early.
  • Poor User Experience: If I add too many clickable elements or extra features, it might overwhelm the user and make the scene less relaxing. I want a balanced level of interaction that doesn’t feel cluttered.
  • Lack of Testing: I plan to test small sections of the code often, rather than waiting until the end. This way, I can catch bugs and performance issues as soon as they pop up.