Midterm Project: Runner Game

Introduction and Concept:

For my midterm project, I decided to create an endless runner game, drawing inspiration from the classic “dinosaur game” featured in Google Chrome when there’s no internet connection.

The core objective of the game is to navigate a player-controlled square through various obstacles, aiming to achieve the highest score possible. The design aesthetic is inspired from interplanetary and retro themes, with background music carefully selected to complement the game’s dynamic pace.

Full screen: https://editor.p5js.org/hazieloli/full/D7t8p9wFx

Failed Attempts:

Initially, I drew inspiration from the movie “Rio” and envisioned developing a game reminiscent of “Angry Birds,” incorporating characters and music from the film. Despite following tutorials and exploring libraries for implementing physics, my attempts led to a demo showcasing the initial stage of the game without the desired physics effects.

Subsequently, as I progressed with implementing physics into the game, I encountered complexities in defining game conditions, such as winning and losing.

Finally, I changed my idea towards developing my current endless runner game for a more manageable project.

Sound and Music:

For the music, I used the instrumental version of the song “Mas que nada”:

https://youtu.be/aLR5DikAvUc?si=6J-CDS4vQdfAavDE

For the sound effects, both the jump and gameover sound, I downloaded from Pixabay:

https://pixabay.com/sound-effects/search/jump/

https://pixabay.com/sound-effects/search/game-over/

Design:

 

For the background images, I used Canva to create the start page:

And the second background image was downloaded from Freepik:
https://www.freepik.com/free-vector/night-ocean-landscape-full-moon-stars-shine_17740155.htm#query=game%20background&position=1&from_view=keyword&track=ais&uuid=0b2c3375-e893-4fd3-961d-793b1389a916

Code:


A part of the code that I’m particularly proud of, even though it is simple, is the draw function:

function draw() {
    if (!gameStarted) {
        // Don't run the game if it hasn't started yet
        return;
    }

    // Draw background image
    image(backgroundImage, 0, 0, width, height);

    score += 0.05;
    fill(255);
    textSize(30);
    text(round(score), 10, 32);

    player.show();
    player.move();

    if (random(1) < 0.03) {
        if (score > minScore) {
            blobs.push(new Blob());
            minScore = score + 2 + random(1);
        }
    }

    for (let i = blobs.length - 1; i >= 0; i--) {
        blobs[i].setSpeed(8 + sqrt(score) / 5);
        blobs[i].move();
        blobs[i].show();

        if (player.hits(blobs[i])) {
            print("GAME OVER");
            gameOverSound.play();
            noLoop();
            // blobs.x = width;
            // startGame();
        }

        if (blobs[i].getX() < -50) {
            blobs.splice(i, 1);
            print("Removed");
        }
    }
}

Challenges faced:


I believe one of the most challenging parts of building this project was to think of something new in such a limited time, after spending days working on my failed attempts. Also, it was quite difficult for me to implement the restart function on the game in order to reset the position of everything when it’s game over.

Areas of Improvement:

Reflecting on the project’s development, I acknowledge areas with potential for enhancement:

  1. Design: Addressing glitches on the starting page and creating an additional background image and message for the game-over screen to ensure a seamless and visually engaging user experience. Also, work on the alignment of the pages.
  2. Level of Complexity: Introducing different levels of difficulty to offer players varying challenges and experiences. For example, gradually increasing obstacle speed as the player achieves specific score milestones to heighten the game’s intensity and replay value.

Tutorials: https://youtu.be/TDQzoe9nslY?si=w9yKP0toDZrD_I3thttps://www.youtube.com/live/FHXDywq69v4?si=sFEaLB8_rZKxWNyq

Midterm project – ?sound?

While immersed in the captivating sounds of Giorgio Moroder, hailed as the pioneer of synthesizer disco and electronic dance music, I was struck by the profound realization that synthesizers are not just musical instruments; they are time machines, capable of transporting us to the soundscapes of the future. This revelation sparked an idea in my mind: to create not just any sound generator, but a sound visualizer that encapsulates the essence of music and visual artistry into one cohesive experience. This project is not a synthesizer in the traditional sense, but it’s a homage to the concept of generating “music” in a form that is both simple and visually engaging. It’s an interactive canvas where every user’s interaction weaves a unique auditory and visual narrative, ensuring that no two experiences are alike.

Full screen: https://editor.p5js.org/MarwanWalid2/full/tYNAJRvFm

The interactive sound visualizer I developed is a testament to P5’s versatility, allowing users to manipulate sound and visuals in real-time through mouse movements. Users can control the amplitude and reverb effects of the sounds produced by pressing different keys, each mapped to a distinct frequency resembling a note. The visual component—a series of organic shapes generated using p5.js’s noise() function—evolves in real-time, mirroring the auditory input for a truly synesthetic experience.

let amp = map(mouseY, 0, height, 1, 0); // Control volume with mouse Y
let dryWet = map(mouseX, 0, width, 0, 1); // Control reverb with mouse X

I’m particularly proud of how the project leverages the noise() function to create visual representations of the sound. This function generates organic, ever-changing patterns, ensuring that the visual output is as dynamic and unique as the auditory one. The decision to allow users to influence the sound’s amplitude and reverb through simple mouse movements was driven by a desire to make the experience as intuitive and engaging as possible, removing barriers to creativity and exploration.

Every creative endeavor comes with its set of challenges, and this project was no exception. Balancing the responsiveness of the visual output with the fluidity of the sound was a delicate task. Ensuring that the system could handle real-time input without significant lag required careful optimization and testing. Additionally, while the current implementation offers a novel experience, it barely scratches the surface of what’s possible with sound synthesis and real-time visual generation.

One area ripe for exploration is the integration of more complex sound synthesis techniques, moving closer to the capabilities of a full-fledged synthesizer. Expanding the range of user controls to include different waveforms, modulation effects, and perhaps even a sequencer, could enrich the user experience significantly. Moreover, the visual aspect could be enhanced by introducing more variables influenced by the sound, such as color changes, or shape transformations.

This project stands as a bridge between the past and the future, drawing inspiration from the pioneers of electronic music while inviting users to explore the boundless possibilities of digital creativity. It’s a celebration of the unpredictability and individuality inherent in artistic expression, encapsulated in an experience where no two interactions are the same. I am proud of the foundation laid by this project and excited about the potential it holds for further exploration and development in the realms of sound visualization and interactive art.

Midterm – Asteroids

Concept and Inspiration

The inspiration behind this code is to create a classic arcade-style game similar to Atari’s Asteroids, where players control a spaceship navigating through space, avoiding asteroids, and shooting them down to earn points. The game incorporates simple controls and mechanics to provide an engaging and challenging experience for players.

Full screen link: https://editor.p5js.org/is2431/full/MvWdoI5tz

How It Works

  • The game uses the p5.js library for rendering graphics and handling user input.
  • It defines classes for the spaceship, asteroids, and bullets, each with their own properties and behaviors.
  • The game initializes with a start screen where players can see the controls and start the game by pressing the Enter key.
  • During gameplay, players control the spaceship using the arrow keys to move and the spacebar to shoot bullets.
  • Asteroids move randomly across the screen, and the player’s objective is to shoot them down while avoiding collisions.
  • When all asteroids are destroyed, the player advances to the next level, where more asteroids are spawned.
  • The game ends when the player runs out of lives, and their score and highscore are displayed along with the option to restart.

Highlights of Code I Am Proud Of

One highlight of the code is the generation of asteroid shapes. The Asteroid class utilizes a combination of randomization and mathematical calculations to create visually appealing and diverse asteroid shapes. By varying the number of vertices, radius, and offsets, the code generates asteroids that have unique patterns, enhancing the overall visual aesthetics of the game.

// Asteroid class
class Asteroid {
  constructor(pos, r) {
    if (pos) {
      this.pos = pos.copy();
    } else {
      this.pos = createVector(random(width), random(height));
    }
    this.vel = p5.Vector.random2D();
    this.r = r || random(15, 50);
    this.total = floor(random(10, 20));
    this.offset = [];
    for (let i = 0; i < this.total; i++) {
      this.offset[i] = random(0, 15);
    }
  }

  update() {
    this.pos.add(this.vel);
  }

  edges() {
    if (this.pos.x > width + this.r) {
      this.pos.x = -this.r;
    } else if (this.pos.x < -this.r) {
      this.pos.x = width + this.r;
    }
    if (this.pos.y > height + this.r) {
      this.pos.y = -this.r;
    } else if (this.pos.y < -this.r) {
      this.pos.y = height + this.r;
    }
  }

  display() {
    push();
    translate(this.pos.x, this.pos.y);
    noFill();
    stroke(255);
    beginShape();
    for (let i = 0; i < this.total; i++) {
      let angle = map(i, 0, this.total, 0, TWO_PI);
      let r = this.r + this.offset[i];
      let x = r * cos(angle);
      let y = r * sin(angle);
      vertex(x, y);
    }
    endShape(CLOSE);
    pop();
  }

  breakup() {
    let newAsteroids = [];
    newAsteroids.push(new Asteroid(this.pos, this.r / 2));
    newAsteroids.push(new Asteroid(this.pos, this.r / 2));
    return newAsteroids;
  }
}

 

Challenges with the Project

  1. Collision Detection: Implementing accurate collision detection between the spaceship, bullets, and asteroids while ensuring smooth gameplay was a challenge, requiring careful consideration of position, size, and velocity.
  2. Game State Management: Managing different game states such as start, play, level complete, and game over required careful handling of state transitions and user input to ensure a seamless gaming experience.
  3. UI and Feedback: Designing clear and intuitive user interfaces, including start screens, game over screens, and score displays, posed challenges in terms of layout, readability, and responsiveness.

 

Future Improvements

There are many improvements I can make to the project. For example, the original Atari game had aliens which would shoot at the spaceship. There are other game mechanics I could have added like powerups.

Midterm Project – Shaikha AlKaabi

 

When I first started working on my midterm project, I really wanted to make a game about planting flowers. It sounded fun and I thought it would be something different. But as I kept working on it and thinking more about how it would work, it started to feel less like a game and more like a simulator for planting. It wasn’t as fun as I had hoped it would be, and that was really important to me. So, I decided to change it.

I switched my idea to “Back and Forth.” At first, I thought about making a volleyball game. It seemed like a good idea because it was about moving back and forth, which is what I wanted. But the more I thought about it, the more I realized that making it feel realistic and fun at the same time was going to be really hard. So, I started looking for something similar but a bit simpler, and that’s when I landed on a ping-pong game. Ping-pong still had that back and forth action which I liked, but it felt more doable and still really fun. That’s how I ended up with the idea for my project.


Challenges:

The tricky  part was figuring out how to make the ball start from the center of the screen. But then I remembered the width /2 and height/2 that we used to center things and I used that to position the ball at the center whenever it went out of the screen:

reset() { // Reset the ball’s position and speed this.x = width / 2; this.y = height / 2; this.xSpeed = random([-5, 5]); // Increased speed this.ySpeed = random([-5, 5]); // Increased speed }

Overall, I’m quite happy with my game.

let leftPaddle;
let rightPaddle;
let ball;
let leftScore = 0;
let rightScore = 0;
let gameState = "start";
let tableColor;
let startScreenImage;
let gameOverSound;
let paddleHitSound;

function preload() {
  startScreenImage = loadImage('background-image.jpg');
  gameOverSound = loadSound('game over.mp3');
  paddleHitSound = loadSound('paddle.mp3');
}

function setup() {
  createCanvas(800, 400);
  textAlign(CENTER, CENTER);
  textSize(40);
  leftPaddle = new Paddle(true);
  rightPaddle = new Paddle(false);
  ball = new Ball();
  tableColor = color(138, 43, 226);
}

function draw() {
  background(tableColor);
  drawNet();
  if (gameState === "start") {
    showStartScreen();
  } else if (gameState === "play") {
    leftPaddle.show();
    rightPaddle.show();
    ball.show();
    leftPaddle.update();
    rightPaddle.update();
    ball.update();
    ball.checkPaddleCollision(leftPaddle);
    ball.checkPaddleCollision(rightPaddle);
    ball.checkScore();
    drawScore();
  } else if (gameState === "gameover") {
    showGameOverScreen();
  }
}

function keyPressed() {
  // Start the game when ENTER is pressed
  if (keyCode === ENTER && gameState === "start") {
    gameState = "play";
  }
  // Reset the game when ENTER is pressed after game over
  else if (keyCode === ENTER && gameState === "gameover") {
    gameState = "start";
    leftScore = 0;
    rightScore = 0;
    ball.reset();
  }

  // Control the right paddle with arrow keys
  if (keyCode === UP_ARROW) {
    rightPaddle.ySpeed = -5;
  } else if (keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 5;
  }

  // Control the left paddle with 'W' and 'S' keys
  if (key === 'w' || key === 'W') {
    leftPaddle.ySpeed = -5;
  } else if (key === 's' || key === 'S') {
    leftPaddle.ySpeed = 5;
  }
}

function keyReleased() {
  // Stop the left paddle when 'W' or 'S' key is released
  if (key === 'w' || key === 'W' || key === 's' || key === 'S') {
    leftPaddle.ySpeed = 0;
  }

  // Stop the right paddle when arrow keys are released
  if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 0;
  }
}


function keyReleased() {
  // Stop the left paddle when 'W' or 'S' key is released
  if (key === 'W' || key === 'S') {
    leftPaddle.ySpeed = 0;
  }

  // Stop the right paddle when arrow keys are released
  if (keyCode === UP_ARROW || keyCode === DOWN_ARROW) {
    rightPaddle.ySpeed = 0;
  }
}

function drawNet() {
  // Draw the net in the middle of the table
  stroke(255);
  strokeWeight(2);
  for (let i = 0; i < height; i += 20) {
    line(width / 2, i, width / 2, i + 10);
  }
}

function drawScore() {
  // Display the scores below the player names with text indicating it is the score
  fill(255);
  textSize(20);
  textFont("Courier New");
  textAlign(CENTER, CENTER);
  
  // Player 1 score
  stroke(0)
  text("Player 1", width / 4, 90);
  text("Score: " + leftScore, width / 4, 110);

  // Player 2 score
  stroke(0)
  text("Player 2", (3 * width) / 4, 90);
  text("Score: " + rightScore, (3 * width) / 4, 110);
}


function showStartScreen() {
  // Display the start screen with instructions
  image(startScreenImage, 0, 0, width, height);
  fill(138, 43, 226);
  stroke(255); // Outline color
  strokeWeight(2); // Outline weight
  textFont("Courier New");
  textStyle(BOLD);
  textSize(40);
  text("Back and Forth", width / 2, height / 2 - 100);
  textSize(20);
  text("Press ENTER to Start", width / 2, height / 2 + 50);
  textSize(15);
  text("Press 'W' to move the left paddle up and 'S' to move it down", width / 2, height / 2 + 125);
  textSize(15);
  text("Press Upward-Arrow to move the right paddle up and Down-Arrow to move it down", width / 2, height / 2 + 150);
}

function showGameOverScreen() {
  // Display the game over screen with the winning player's name
  let winner = leftScore > rightScore ? "Player 1" : "Player 2";
  fill(255);
  stroke(0); // Outline color
  strokeWeight(5); // Outline weight
  textFont("Courier New");
  textSize(40);
  text(winner + " Wins!", width / 2, height / 2 - 40);
  textSize(20);
  text("Press ENTER to Play Again", width / 2, height / 2 + 40);
}

class Paddle {
  constructor(isLeft) {
    this.w = 10;
    this.h = 80;
    this.y = height / 2 - this.h / 2;
    if (isLeft) {
      this.x = 20;
    } else {
      this.x = width - 30;
    }
    this.ySpeed = 0;
  }

  show() {
    // Draw the paddle
    fill(255);
    rect(this.x, this.y, this.w, this.h);
  }

  update() {
    // Update the paddle's position based on key inputs
    this.y += this.ySpeed;
    this.y = constrain(this.y, 0, height - this.h);
  }
}

class Ball {
  constructor() {
    this.reset();
  }

  reset() {
    // Reset the ball's position and speed
    this.x = width / 2;
    this.y = height / 2;
    this.xSpeed = random([-5, 5]); // Increased speed
    this.ySpeed = random([-5, 5]); // Increased speed
  }

  show() {
    // Draw the ball
    fill(255);
    ellipse(this.x, this.y, 10, 10);
  }

  update() {
    // Update the ball's position
    this.x += this.xSpeed;
    this.y += this.ySpeed;
    if (this.y < 0 || this.y > height) {
      this.ySpeed *= -1;
    }
  }

  checkPaddleCollision(paddle) {
    // Check for collision with paddles and change direction
    if (this.x - 5 < paddle.x + paddle.w && this.x + 5 > paddle.x && this.y - 5 < paddle.y + paddle.h && this.y + 5 > paddle.y) {
      this.xSpeed *= -1;
      paddleHitSound.play();
    }
  }

  checkScore() {
    // Check for scoring and end game condition
    if (this.x < 0) {
      rightScore++;
      this.reset();
      gameOverSound.play();
    } else if (this.x > width) {
      leftScore++;
      this.reset();
      gameOverSound.play();
    }
    if (leftScore >= 5 || rightScore >= 5) {
      gameState = "gameover";
    }
  }
}

 

Midterm Assignment: Knighthood Arcade

Concept:

https://editor.p5js.org/tt2273/sketches/FyrgY5Ivv

“Knighthood Arcade” immerses players in an exciting quest where they take on the role of a valiant knight tasked with eliminating menacing slimes infesting the kingdom. Set in a vibrant 2D environment, the game unfolds as players navigate challenges, strategize attacks, and seek victory against relentless slime adversaries.

The primary objective is for players to eliminate a specified number of slimes, with a set kill count serving as the victory condition. The game intricately balances combat, movement, and decision-making, requiring players to engage with the environment and enemies strategically.

Players achieve victory by successfully defeating predetermined number of (five in this case) slimes. Each slime defeated contributes to the player’s kill count. The dynamic gameplay evolves as the knight progresses, presenting increasingly challenging encounters with the slimes. The game introduces a perilous scenario where the knight faces defeat under specific circumstances:
– If the knight succumbs to slime attacks due to inadequate defensive maneuvers.
-If one of the slimes gets to the other side without being slain.

Implementation and Parts I am Proud of:

In crafting “Knighthood Arcade”, several key implementations have shaped the gaming experience. The strategic integration of combat dynamics allows players to tactically time attacks, emphasizing the significance of thoughtful engagement with the slimes. Navigating the knight through diverse terrains and overcoming movement challenges showcases the finesse required for successful progression. Furthermore, the integration of a subtle narrative layer, albeit minimal, contributes context and motivation to the knight’s quest. Personally, I take pride in the balance achieved between combat, movement, and decision-making elements, providing players with a challenging yet enjoyable gaming experience. This delicate equilibrium is complemented by the implementation of varying slime behaviors, adding depth and nuance to the adversaries. Adding idle animations to the sprite movement, which makes the game more similar to an arcade game, I believe was an excellent touch. I integrated p5play using cdn, but using the p5play documentation was rather difficult as the user interface was not good. Another aspect of the game that I am proud of is the checkDistance function, which determines whether the sprites are close to each other or not. Overall, the game not only delivers on its primary goal of strategic combat but also offers a dynamic and immersive environment that I find particularly gratifying as a developer and an arcade enthusiast.

Challenged faced:

Throughout the development journey of “Knighthood Arcade” various challenges were encountered, each contributing to the learning and growth of the project. Implementing effective collision detection and response mechanisms presented a significant hurdle, demanding meticulous fine-tuning to ensure seamless interactions between sprites. Managing the complexity of sprite animations, particularly synchronizing the knight’s actions with the game’s logic, posed another formidable challenge.  Addressing unexpected bugs and glitches, inevitable in any development process, demanded a systematic debugging approach. Integrating sound elements, specifically synchronizing background music with in-game events, proved to be a nuanced challenge, adding an auditory layer to the gaming experience. Overcoming these difficulties not only refined technical skills but also underscored the iterative nature of game development. Moreover, managing the sprite animations in the preload function was difficult at first, since the difference between the adjacent sprites were minimal, therefore detecting if the sprite animations were implemented into the array was hard.

Further Improvements:

Looking ahead, “Knighthood Arcade” presents ample opportunities for further refinement and expansion. One key area of improvement involves incorporating a more intricate combat system, introducing diverse enemy behaviors, and enhancing the strategic depth of engagements. Introducing additional levels with progressive difficulty could elevate the overall gaming experience, providing players with a sense of accomplishment as they navigate increasingly challenging scenarios. Implementing a scoring system and perhaps integrating an online leaderboard could foster a competitive element, encouraging players to strive for higher scores and fostering a sense of community around the game. The visual aesthetics and overall presentation could benefit from additional polish, including more detailed background art and refined character animations. Exploring the integration of power-ups, character upgrades, or even new characters could introduce fresh dynamics and keep players engaged over extended gameplay sessions.  These potential enhancements lay the foundation for the game’s evolution, promising an even more captivating and immersive experience for players in future iterations.

Code Snippet:

slimeMovement(slimeSprite);
 
      if (checkDistance(slimeSprite, mySprite, 100) && isAttack) {
        if (!slimeSprite.dead){
          slimeSprite.dead = true;
          slimeSprite.changeAnimation("dead");
          setTimeout(() => {
            slimeSprite.animation.stop();
            slimeSprite.animation.changeFrame(3);
          }, 500)
          killed++;
          console.log(killed);

        setTimeout(() => {
          slimeSprite.position.x = width+100;
          slimeSprite.position.y = random(50, height-50);
          slimeSprite.dead = false;
        }, 1000);
        slimeMovement(slimeSprite);
      }
    } else if (checkDistance(slimeSprite, mySprite, 50) && !isAttack){
          slimeSprite.changeAnimation("attack");
          mySprite.changeAnimation("dead");
          setTimeout(() => {
            slimeSprite.changeAnimation("idle");
            mySprite.position.x = -100;
            mySprite.position.y = -100;
          }, 1500)
          gameState = "end-lose";
      }

Final Sketch:

 

Midterm Project: Immune Defenders!

Introduction and Concept

The idea for this project came as a sort of natural follow-up to my projects in this class so far. I have tried to include elements of bioinspiration in almost every assignment I have done for this class so far (barring the Data Visualization of Week 4), so I already knew I wanted to do something within that theme.

My inspiration for this game came almost directly from the retro arcade game Galaga by Namco (see image), and the many space shooters it inspired.

Namco’s Galaga

So, my goal was to now somehow combine a space shooter with the immune system. I also wanted it to be relatively accurate to the human immune system, so there was that added challenge.

So, the end result is a game where you control an effector B-cell (or plasma cell), to shoot antibodies at and neutralize bacteria, just like your own immune system does millions of times each day, even when you’re healthy.

Code and Design

With a whopping 783 lines of code (including class definitions), this is easily the largest project I have ever coded. Here are some chunks that I am particularly proud of:

if (keyCode == 32) {
  shot.play();
  if (int((millis() - lastboosttime) / 1000) > boosttime) {
    numBullets = 1;
  }
  for (let i = 1; i <= numBullets; i++) {
    bulletx =
      lives[0].x +
      lives[0].r * 0.25 * (-2 * pow(-1, i) * i + pow(-1, i) - 1);
    bullety = lives[0].y;
    bulletr = height / 100;
    bulletspeed = height / 20;
    var bullet = new Bullet(
      bulletx,
      bullety,
      bulletr,
      bulletspeed,
      bulletsprite
    );
    bullets.push(bullet);
  }
}

So, the above code is used to shoot the bullets that are a core component of the game. But the code is written mainly to handle the challenge of summoning multiple bullets together during the Vitamin C powerup (more on that later). I could have just called it a day and made three separate “new bullets” with the correct x-positions, but I wanted a way to make it uniform for no matter how many bullets could be added by the powerup. This tool from Wolfram Alpha was essential for this, as it calculated a mathematical formula (the part which has quite a few terms involving multiplication of -1 raised to the bullet number). So, whether I add 3, 5, or 19 bullets, theoretically, I should get a consistent result.

//bullet display,, movement and collision
for (let i = 0; i < bullets.length; i++) {
  bullets[i].display();
  bullets[i].move();
  //Delete bullet and bacterium, and add score when killing bacteria
  for (let j = 0; j < bacteria.length; j++) {
    if (bullets.length > i) {
      //length condition added as parameter changes before draw() function is next called
      if (bullets[i].collide(bacteria[j])) {
        bullets.splice(i, 1);
        bacteria.splice(j, 1);
        score++;
      }
    }
  }
  //Delete when bullets fly off-screen
  if (bullets.length > i) {
    //length condition added as parameter changes before draw() function is next called
    if (bullets[i].wallCollide()) {
      bullets.splice(i, 1);
    }
  }
}

The above part of the code deals with the collision logic for the bacteria, and is another part I’m proud of. Using splice() instead of pop() ensures that the correct bacterium is deleted, even when they move out of order because of their different speeds.

Other Code Features

Other features of the code that are important for the functioning of the game, but are not particularly remarkable are the event handlers for clicking and key presses. While clicking to change stage, I had to ensure that the user wouldn’t accidentally click out of the game into the game over screen, but that was easy enough with some conditionals. Key event listeners involve the shooting using the keyboard, movement visa left/right or A/D keys, and using powerups via the Z, X, C keys. Each had their respective actions coded within the event handler itself.

There is also a timer that tracks the time from each iteration of the game starting anew (even in the same sketch run), as well as a counter to track score. The timer is used to both indicate the length of time survived by the player, as well as to control the powerup cooldowns.

Classes

There are a total of 7 classes: Bacteria (display, movement and collision of bacteria), Immune (immune cell display), Bullet (display, movement and collision of bullet), Boost, Net, Bomb (display and update of the cooldown of the powerups), and Button (hover/click behavior of end-screen buttons). I realize now that I could have probably included the three powerup classes under one, but I had initially planned to have their respective functions as class methods. I could probably still have done that by inheritance, but I wasn’t aware of how to make parent classes in p5 or JS, and I did not have sufficient time to learn.

Gameplay

As described earlier, the game is basically a Galaga clone. All you primarily do is shoot down or dodge waves of bacteria (enemy spaceships). To make the gameplay more interesting however, I included three powerups.

The first powerup (try pressing the Z key), allows you to shoot out 3 antibodies per shot instead of just one. That turns out to be particularly useful when the bacteria tend to be just off the exact center mark of the white blood cell.

The second powerup (X key) allows you to slow down the bacteria, giving you more time to shoot or dodge them, whichever you prefer. This powerup was based on the ability of some neutrophils (one of the types of white blood cells that act as a first-line defender) to produce traps that literally stick the bacteria in place so that they can be neutralized and then consumed by other immune cells.

The third and final powerup (C key) almost feels like cheating as it completely nukes everything on the screen and gives you the rewards for it. Initially, I wanted to balance it by reducing the score you get back, but I realized that would confuse players (after all, the bacteria are being killed). So, instead I balanced it with a high score cost, which does kind of match real life. Such an attack that kills multiple colonies of bacteria in one go often results in the immune cells attacking the body’s own healthy cells as well, engaging in inflammatory reactions with massive collateral damage, often causing even more damage than the disease on its own.

The best part about these powerups according to me is that they’re all based at least loosely in real biological concepts, and are not just make-believe gameplay conveniences.

Graphics

Graphics were mostly obtained from royalty-free clipart on the web, which I then recolored to increase contrast/make them look more interesting. The title card’s image was generated using DALLE-3 on Bing Copilot. Any editing required was easily done in Microsoft Paint 3D.

All of the graphics elements involved in this project.

Sound and Music

Pixabay has always been a lifesaver for me when it comes to obtaining high-quality royalty-free/Creative Commons-licensed music and sound effects without any pesky registration or annoying PLUS subscriptions (this is not an ad). I already had somewhat of a general idea of the feel of music I was going for, so I just searched up a few different categories and stuck to the music that immediately hit it off with me. I trimmed a few of the sound clips using Audacity and also put them through FileConvert’s compression tool to reduce the burden on p5.js, which does tend to struggle with loading heavy images/sounds. My only regret is not leaving enough time to include SFX for the powerups.

Pixabay User Interface

Challenges, Improvements and Future Considerations

Bug-fixing was probably the most challenging aspect. Because I had worked on the project over several days, I found that there were often things I was forgetting that led to weird interactions and things not going as expected. Asking my roommate to play-test the game definitely did help.

There are many things that I wanted to include that had to be left out in the interest of time. I had planned to include other pathogens, including viruses that didn’t damage your health but instead temporarily blocked your ability to shoot, and even a parasite final boss that would not attack you directly but instead open up more wounds for hordes of bacteria and viruses to enter the blood vessel and overwhelm the player.

Additionally, as mentioned earlier, I would have preferred to have more sound effects, for not just the powerup, but also when the player was hit by a bacterium and lost a life. However, overall, I am happy with the final result, and I can say that it closely matched my initial expectations.

Midterm Demo

Fullscreen Link: https://editor.p5js.org/amiteashp/full/Uvgv-fIWb

Link to project on P5 editor: https://editor.p5js.org/amiteashp/sketches/Uvgv-fIWb

 

Week 6: Midterm Project – Save the Butterfly

Concept 

As I shared in last week’s blog post, I wanted my project to be a gamified story. I also wanted to center it around a butterfly, an extension of a theme I have been committed to for the past few weeks. Additionally, the narrative created would convey my own desire to reunite with my family – a goal that I hope to achieve eventually in my lifetime. This could be seen in the final scene unlocked if the player passes the levels successfully. The butterfly returns to her family of four, which is the number of members in my own family. The storyline and flow of the game go like this:

    1. A butterfly finds herself lost in the city on a rainy day. She flutters through the window of the main player’s room.
    2. the player is prompted to help the butterfly find her way back to her home and family, going through game levels in the city and forest and avoiding (jumping over) obstacles to preserve their health. Here the player has a chance to replenish their health by collecting potions.
    3. If the player manages to successfully complete the mission, they unlock the final scene, in which the butterfly is finally reunited with her family.
    4. If the player loses, they are prompted to restart the game.

In the making of this, I emphasized the animations for an increased focus on the narrative component over the game one. I spent a lot of time playing with different parameters and finding ways to control time and the movement of sprites in response to changes in their environment. The storyboarding I had done last week greatly aided in visualizing how I wanted the animations to eventually look.

Implementation and Parts I am Most Proud of

In terms of execution, I implemented everything as a class based on the rough UML structure I sketched out in my last blog. The main Gameclass had methods for each level and its attributes were objects instantiated from the Player, Butterfly, EnemyPotion, and HealthBar classes. Certain classes were also abstracted from parent classes using inheritance (such as the Playerclass inheriting from a parent Sprite class that has basic attributes and methods shared between all its child classes). Each level/scene is separated by overlays, where the user is prompted to click anywhere on the screen to continue to the next stage of the experience. In terms of assets, all sounds, images, and fonts were sourced from the following open-source community/free platforms:

  1. https://freesound.org/
  2. https://opengameart.org/
  3. https://www.free-stock-music.com/
  4. https://www.dafont.com/

For the mechanics of the game, the Player sprite is placed at the lower left corner of the screen. Its main movement, jumping, is only triggered when the player presses the Space bar. Jumping occurs by setting the vertical velocity to a certain jump power attribute. As the player falls down, the player’s velocity is incremented by an acceleration due to gravity attribute. The player also has the ability to double jump once while in air, which comes in handy if a flock of enemies is headed its way. In terms of the collision detection mechanism, a collision occurs when the distance between the center of the player and that of an enemy object is less than the sum of their respective radii (minus a certain amount to account for the free pixels in the sprite images). Below is the code for the collision detection mechanism, which is a Player class method:

  detectCollision(obj, offset=30) {
    if (!obj.collided) {
      // get distance between the center of the character and that of the enemy object
      let objHeight = obj.h;
      let objWidth = obj.w;
      let playerWidth = this.w;
      let playerHeight = this.h;
      
      // update height and width based on resized parameters if the player/object was resized 
      if (obj.resize) {
        objHeight = obj.resize_y;
        objWidth = obj.resize_x;
      }

      if (this.resize) {
        playerWidth = this.resize_x;
        playerHeight = this.resize_y;
      }
      let centerX = this.x + playerWidth / 2;
      let centerY = this.y + playerHeight / 2;

      let d = dist(
        centerX,
        centerY,
        obj.x + objWidth / 2,
        obj.y + objHeight / 2
      );
      
      // collision detected
      // distance is less than the sum of objects' radii
      // minus a distance to account for free pixels in the sprite images

      if (d < playerWidth / 2 + objWidth / 2 - offset) {
        if (!obj.potion) { // lose health if the object is an enemy
          loseHealth.play();
          this.currHealth -= obj.damage; 
          
        } else if (obj.potion && this.currHealth < game.healthBar.maxHealth) { // regain health if the object is a potion 
          gainHealth.play();
          this.currHealth += 1;
        }
        obj.collided = true; // set object collided to true
        return true; // return true if collision is detected 
      }
    } else {
      return false; // return false if collision is not detected 
    }
  }
}

An implementation highlight that I think is worth noting is the use of the smoothing algorithm in simulating the butterfly’s movement in the first scene as she makes her way from the window to the desk. This was implemented as part of the Butterfly class.

move(targetX, targetY) {
   // move butterfly toward destination -- smoothing algorithm
   if (this.moving) {
     this.x += (targetX - this.x) * 0.009;
     this.y += (targetY - this.y) * 0.007;
   }

There are quite a few things that I am proud of in the implementation of this project. The first is the emotional feel of the overall experience. I made sure that the combination of animations created, the sounds chosen, the font, and the color palettes – going from night to day and from city to nature – created a wholesome backdrop to the game engulfed within. I also am proud that I made sure to bring to life the storyboard from last week’s blog. Finally, more than anything, I loved working on the animated start and final scenes (code shown below) and how the flow from animation to instructions to game and then to animation again (with transition overlays in between) turned out to be quite seamless.

  firstScene() {
    // show background of the scene 
    image(room[this.firstSceneAnimationStep], 0, 0, width, height);
    // change the background based on frameCount to show animated changes in the player's room
    if (frameCount % 7 == 0) {
      this.firstSceneAnimationStep = (this.firstSceneAnimationStep + 1) % 6;
    }
    // show player
    this.player.show();
    // show butterfly, passing in rotation paramaters 
    this.butterfly.show(100, 170);
    
    // player faces the butterfly once it is 40 pixels from the point (100, 170)
    if (dist(this.butterfly.x, this.butterfly.y, 100, 170) < 40) {
      this.player.dir = 1; 
      // move player toward butterfly once it lands on the desk
      if (
        dist(this.player.x, this.player.y, this.butterfly.x, this.butterfly.y) >
        50
      )
        this.player.x -= 0.6;
      else { // once the player is close to the butterfly, display instructions overlay 
        this.gameMode = 1;
        let text = `This poor butterfly seems to be far away from home! 
You have to help her find her way back to her family!

The first step on your journey is to go through the city. 
Beware the obstacles on your way. 

Press the Space bar to jump. 
Collect potions to replenish your health. 

Click anywhere on the screen if you are 
ready to embark on the journey!`;
        twinkleSound.play(); // play twinkle sound
        this.overlay(text);
      }
    }
  }

  finalScene() {
    // display the flower field background
    image(flowerField, 0, 0, width, height);
    this.player.dir = 3; // change direction so that the player' front side is facing the flower field
    this.levelButterfly.dir = 3;

    // resize the butterfly and player to show advancing movement 
    if (frameCount % 10 == 0) {
      this.player.resize_x -= 4;
      this.player.resize_y = this.player.resize_x / 0.75;
    }
    if (frameCount % 15 == 0) {
      this.levelButterfly.resize_x -= 1.5;
      this.levelButterfly.resize_y = this.levelButterfly.resize_x * 2;
    }


    this.resizeObject(this.player);
    this.resizeObject(this.levelButterfly);
    
    // show background butterflies 
    for (let i = 0; i < 4; i++) {
      this.butterflyFamily[i].show();
    }
    
    // stop the animation once the player's y position is less than 
    // 255 pixels 
    if (this.player.y <= 225) {
      this.player.moving = false;
      this.levelButterfly.moving = false;
      // change into overlay, prompting the player to restart the game
      this.gameMode = 5;
      let text = `Click anywhere to restart the game!`;
      twinkleSound.play();
      this.overlay(text);
      noLoop();
    }
    
    // move player and butterfly diagonally across the screen to move 
    // upward through the field 
    this.player.moveDiagonally();
    this.levelButterfly.moveDiagonally();
  }
Challenges Encountered and Proposed Improvements

One of the challenges I encountered was during the implementation of the final scene animation, where the main player and the butterfly had to be iteratively resized to create the animation of moving into the distance. I found that using the resize() method consecutively blurred the images and I, thus, had to find another way to resize them. After some googling, I found a way to resize the image by creating a resized image object and copying the pixels into the resized image as a way to avoid calling the resize() method:

resizeObject(obj) {
  // scale with copy -- 
https://stackoverflow.com/questions/72368646/images-blur-with-resize-with-p5-js
  
  // create an image object with the resized parameters
  let resizedImg = createImage(int(obj.resize_x), int(obj.resize_y));
  
  // get the image to resize from the object's sprites array 
  let srcImg = obj.sprites[obj.dir][obj.step];
  
  // copy the pixels from the source image to the resized image 
  resizedImg.copy(
    srcImg,
    0,
    0,
    srcImg.width,
    srcImg.height,
    0,
    0,
    obj.resize_x,
    obj.resize_y
  );
  
  // rotate object if needed and display the resized image
  if (obj.rotated) {
    push();
    translate(obj.x, obj.y);
    rotate(radians(obj.rotationAngle));
    image(resizedImg, 0, 0);
    pop();
  } else { 
    image(resizedImg, obj.x, obj.y);
  }
}

Another challenge was the length of the Enemy arrays created in the constructor() of the game class for each level as it modulated the duration of each level. The more enemies there are in a level, the longer its duration, as the condition for termination was when a particular level Enemy array became empty. However, I found that the more enemies there were in a level, the more slow and laggy the movements became, possibly due to the number of objects that had to be drawn on the screen. I attempted to fix this by ensuring that objects are only drawn when they are within the screen bounds as their locations are initialized randomly off-screen. While this helped a little, the problem remained. So a future improvement could be to look into this further and perhaps choose lighter/smaller sprites to display or have a different initialization mechanism.

Additionally, here are a few other ideas to elevate the current version a little more:

  • Add some more levels, perhaps in between the city and the forest (e.g. a suburban in-between area/ or a highway).
  • Add different types of potions with varying degrees of health replenishment (the stronger the potion, the rarer it is). This should be accompanied by an increase in the difficulty of the game, e.g. more enemy sprites, faster enemy sprite movements, or an increase in the damage attribute of certain sprites.
  • Add some feedback, such as a jitter, when a collision occurs with an enemy object. An extension of this would be the possibility of annihilating an enemy if the Player sprite jumps on its head (the same way Gombas are annihilated in Super Mario).
Final Sketch

Midterm Project Presentation/Documentation – Jihad Jammal

Concept:

Taking a fresh and creative approach to the beloved Snake game format, my concept introduces players to a comically troublesome dog, embarking on a homework-eating spree. This idea reinvents the classic game mechanic, where instead of a snake that grows with each item consumed, we have a dog that enlarges with every piece of homework it swallows. This creative twist not only injects a dose of humor into the gameplay but also layers in strategic depth and a ticking clock element, with a 30-second time limit creating a sense of urgency.

The concept of this game particularly appeals to me due to my personal fascination with those almost mythical stories we’ve all heard growing up—tales of pets, especially dogs, eating homework and becoming the convenient scapegoat for undone assignments. There’s something universally relatable and humorously nostalgic about the idea, regardless of how fanciful these excuses might have seemed. I was inspired to bring these whimsical narratives to life through gameplay, transforming an age-old excuse into an interactive and entertaining experience.

By integrating this playful storyline with the mechanics of the growing dog within a limited timeframe, I aim to capture not just the essence of these stories but also their inherent humor and the frantic, often comical, attempts to salvage what’s left of the homework before it’s too late. It’s a nod to those shared experiences, a blend of reality and exaggeration, that many of us can chuckle at in hindsight.

Embed sketch:

Include code snippets and one or more images

 

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

 

Describe how your project works and what parts you’re proud of (e.g. good technical decisions, good game design):

My approach to this project was focused on crafting an engaging and enjoyable game loop that balanced challenge and playability. I was acutely aware that the dog’s speed couldn’t start off too fast, as this would alienate new players, nor could it become too slow as the timer neared its end, as this would sap the excitement from the gameplay. Achieving this balance was critical in keeping players engaged and encouraging them to improve over time. Additionally, I aimed to create a cohesive and immersive game environment. From the main menu’s view of the backyard to the transition into a bird’s-eye view of the entire backyard upon starting the game, players are drawn into a world that feels both expansive and detailed. This seamless movement into the game’s space was designed to make players feel as if they’ve stepped into the backyard themselves, observing the chaos from above.

Furthermore, I’m particularly proud of how I tackled the game’s technical and design challenges. After some experimentation, I managed to modularize many of the game’s features, breaking them down into separate functions that could be easily managed and updated. This was a significant departure from my initial approach, which had all the functionalities crammed into the draw function—a classic case of spaghetti code. This early version was overwhelming and led to analysis paralysis, making debugging a tedious chore. However, by compartmentalizing these features, I not only streamlined the development process but also significantly improved the game’s performance and my ability to debug and expand on the game. This decision towards modular design has been a game-changer, allowing for cleaner code, easier maintenance, and the flexibility to add new features or tweak existing ones with minimal hassle. It’s a technical achievement that has had a profound impact on both the development experience and the overall quality of the game.

Below is some of the modularized code:

function displayGameInfo() {
    let timePassed = (millis() - timerStartTime) / 1000;
    let timeLeft = max(30 - timePassed, 0);
    fill(0);
    noStroke();
    textSize(16);
    textAlign(RIGHT, BOTTOM);
    text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

    checkCollisionsAndScore();
    displayScore();
    handleMovement();
}

function checkCollisionsAndScore() {
let hit = rectRectCollision(dog.x - dog.img.width / 2 * dog.scale, dog.y - dog.img.height / 2 * dog.scale, dog.img.width * dog.scale, dog.img.height * dog.scale, whiteSquare.x, whiteSquare.y, whiteSquare.size, whiteSquare.size);
  if (hit) {
    dog.growAndSlow();
    whiteSquare.respawn();
    score++;

    // Play collision sound at 30% volume
    collisionSound.setVolume(0.3); // Set volume to 30%
    collisionSound.play();
  }
}

function displayScore() {
    fill(0);
    stroke(255);
    strokeWeight(2);
    textSize(16);
    textAlign(RIGHT, TOP);
    text("Score: " + score, width - 10, 10);
}

function handleMovement() {
    if (keyIsDown(LEFT_ARROW)) dog.move(-1, 0);
    if (keyIsDown(RIGHT_ARROW)) dog.move(1, 0);
    if (keyIsDown(UP_ARROW)) dog.move(0, -1);
    if (keyIsDown(DOWN_ARROW)) dog.move(0, 1);
}

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

Describe some areas for improvement and problems that you ran into (resolved or otherwise):

Reflecting on the project, I see a few avenues for enhancement that could elevate the gameplay experience and address some challenges encountered along the way. In future iterations, I’d love to delve deeper into refining the game’s core loop. Introducing an in-game store where players can buy stat or ability upgrades seems like an exciting direction. Coupling this with the addition of random power-ups, like speed boosts, time extensions, and score multipliers appearing throughout the 30-second gameplay window, would inject more depth and variability into the strategies players might employ. Given the tight time frame players are working with, these elements could introduce pivotal decision-making moments that are both thrilling and consequential, potentially transforming an average round into an extraordinary one.

On the technical side, one particular hurdle I had to navigate involved the game’s scalability, specifically related to the dog’s growing image. As the dog expanded, it reached a point where it would cause the site or the p5.js environment to crash. Despite my efforts, pinning down the precise cause was challenging, leading me to theorize that it stemmed from an excessive strain on resources at any given moment. Under the pressure of deadlines, I opted for a temporary workaround by imposing a maximum size limit on the dog’s growth. This solution, while effective in preventing crashes, is admittedly more of a band-aid than a cure. Moving forward, dedicating time to resolve this issue comprehensively would not only enhance performance but also ensure that the gameplay’s immersive experience remains uninterrupted and fluid for all players.

Credits:

Sounds:

  • https://pixabay.com/sound-effects/dog-barking-70772/
  • https://pixabay.com/sound-effects/crunchy-paper-33625/
  • https://pixabay.com/sound-effects/merx-market-song-33936/

Art:

  • https://www.freepik.com/free-vector/sticker-template-dog-cartoon-character_20496955.htm#fromView=search&page=1&position=44&uuid=59df1125-5f43-4d72-9319-72464f25d11c
  • https://www.pinterest.com/pin/4433299622478035/
  • https://www.freepik.com/free-vector/scene-backyard-with-fence_24552372.htm
  • https://www.freepik.com/free-vector/seamless-textured-grass-natural-grass-pattern_11930799.htm#query=cartoon%20grass%20texture&position=0&from_view=keyword&track=ais&uuid=7951681d-d20d-4bad-8ce4-d50239a26e77

Code Assistance:

Chatgpt – https://chat.openai.com/

 

Full Code:

let dog; // This will be an instance of MovableImage
let bgImage; // This variable will hold your background image
let startScreenImage; // This variable will hold your start screen image
let gameState = "start"; // "start" for the start screen, "play" for the gameplay
let whiteSquare; // Instance of WhiteSquare
let customFont; // Variable for the custom font
let score = 0; // Tracks the number of times the dog collides with the white square
let hwImage; // This will hold the image of the homework paper
let timerStartTime;
// Global variables for button dimensions
let buttonWidth = 100;
let buttonHeight = 40;
let gameLoopSound;
let collisionSound;


class WhiteSquare {
constructor() {
    this.size = 50; // This can be adjusted based on the actual size of your image
    this.x = 0;
    this.y = 0;
    this.respawn();
  }

  display() {
    // Use the homework image instead of a white rectangle
    image(hwImage, this.x, this.y, this.size, this.size);
  }

  respawn() {
    // Cap the additional distance increase once the score reaches 32
    let cappedScore = Math.min(score, 32);
    let additionalDistance = cappedScore * 5; // The distance increase caps when the score reaches 32
    let minDistance = 100 + additionalDistance; // Base min distance + additional based on capped score

    // Attempt to respawn the square until it's far enough from the dog
    do {
      this.x = random(this.size, width - this.size);
      this.y = random(this.size, height - this.size);
    } while (this.isTooCloseToDog(minDistance));
  }

  isTooCloseToDog(minDistance) {
    if (dog && dog.x !== undefined && dog.y !== undefined) {
      let distance = dist(this.x, this.y, dog.x, dog.y);
      return distance < minDistance;
    }
    return false; // Default to false if dog is not initialized
  }
}


class MovableImage {
  constructor(image, x, y, scale = 1) {
    this.img = image;
    this.x = x;
    this.y = y;
    this.scale = scale; // Added a scale property
    this.speed = 5; // Initial speed - starting with a reasonable speed
  }

  display() {
    // Check if the image is within the bounds of the canvas
    this.x = constrain(this.x, 0, width);
    this.y = constrain(this.y, 0, height);
    
    push();
    translate(this.x, this.y);
    scale(this.scale); // Apply the scaling factor
    image(this.img, 0, 0);
    pop();
  }

  move(stepX, stepY) {
    this.x += stepX * this.speed;
    this.y += stepY * this.speed;
  }

  // Function to set the scale and adjust speed
 growAndSlow() {
   
const maxScale = 0.25; // Adjust this value as needed for balance and performance

  // Only grow if below the maximum scale limit
  if (this.scale < maxScale) {
    this.scale *= 1.025; // Grow by 2.5%
  }

    // Only reduce speed if the score is less than 13
    if (score < 13) {
      this.speed = max(1, this.speed * 0.95); // Reduce speed by 5%, no less than 1
    }
 }

}

function preload() {
  dogImage = loadImage('images/dog1.png');
  bgImage = loadImage('background2.jpg');
  startScreenImage = loadImage('backyard.jpeg');
  customFont = loadFont('MadimiOne-Regular.ttf'); // Load the custom font
  hwImage = loadImage('HW.png'); // Load the image of the homework paper
  buttonSound = loadSound('woof.mp3'); // Make sure the path to your mp3 is correct
    gameLoopSound = loadSound('game_loop_2.mp3'); // Adjust path as needed
    collisionSound = loadSound('paper.mp3'); // Adjust path as needed




}

function setup() {
  createCanvas(400, 400);
  dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
  whiteSquare = new WhiteSquare();

  imageMode(CENTER);
  textFont(customFont); // Set the custom font for all text
  
  
  // Define button properties
  buttonWidth = 100;
  buttonHeight = 40;
  restartButtonX = width / 2 - buttonWidth / 2;
  restartButtonY = height / 2 + 20;
  menuButtonX = width / 2 - buttonWidth / 2;
  menuButtonY = height / 2 + 70; // Positioned below the restart button

}

function rectRectCollision(x1, y1, w1, h1, x2, y2, w2, h2) {
  return x1 < x2 + w2 &&
         x1 + w1 > x2 &&
         y1 < y2 + h2 &&
         y1 + h1 > y2;
}

function isMouseOverButton(x, y) {
  return mouseX >= x && mouseX <= x + buttonWidth &&
         mouseY >= y && mouseY <= y + buttonHeight;
}

function drawButton(label, x, y) {
  fill(100); // Button color
  stroke(0); // Button outline color
  strokeWeight(2); // Button outline weight
  rect(x, y, buttonWidth, buttonHeight, 5); // Draw the button with rounded corners
  noStroke(); // Disable outline for the text
  fill(255); // Text color
  textSize(16); // Text size
  textAlign(CENTER, CENTER);
  text(label, x + buttonWidth / 2, y + buttonHeight / 2); // Draw the text centered on the button
}

function mousePressed() {
    let playButtonX = width / 2 - buttonWidth / 2;
    let playButtonY = height / 2 + 20;
    let instructionsButtonX = width / 2 - buttonWidth / 2;
    let instructionsButtonY = playButtonY + 70; // Positioned below the "Play" button
    let creditsButtonX = width / 2 - buttonWidth / 2;
    let creditsButtonY = instructionsButtonY + 50; // Positioned below the "Instructions" button

    if (gameState === "start") {
        if (isMouseOverButton(playButtonX, playButtonY)) {
            gameState = "play";
            timerStartTime = millis();
            score = 0;
            dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
            whiteSquare = new WhiteSquare();
        } else if (isMouseOverButton(instructionsButtonX, instructionsButtonY)) {
            gameState = "instructions";
        } else if (isMouseOverButton(creditsButtonX, creditsButtonY)) {
            gameState = "credits";
        }
    } else if ((gameState === "instructions" || gameState === "credits") && isMouseOverButton(menuButtonX, menuButtonY)) {
        gameState = "start";
    } else if (gameState === "gameover") {
        if (isMouseOverButton(menuButtonX, menuButtonY)) {
            gameState = "start";
        }
        let restartButtonX = width / 2 - buttonWidth / 2;
        let restartButtonY = playButtonY;
        if (isMouseOverButton(restartButtonX, restartButtonY)) {
            gameState = "play";
            timerStartTime = millis();
            score = 0;
            dog = new MovableImage(dogImage, width / 2, height / 2, 0.009);
            whiteSquare = new WhiteSquare();
        }
    }
    if (isMouseOverButton(playButtonX, playButtonY) || 
      isMouseOverButton(instructionsButtonX, instructionsButtonY) || 
      isMouseOverButton(creditsButtonX, creditsButtonY) || 
      isMouseOverButton(menuButtonX, menuButtonY) || 
      isMouseOverButton(restartButtonX, restartButtonY)) {
    buttonSound.play();
  }
}

function drawGameOverScreen() {
  background(0); // You can change the background color
  fill(255);
  textSize(32);
  textAlign(CENTER, CENTER);
  text("Game Over!", width / 2, height / 2 - 60);
  textSize(24);
  text("Your Score: " + score, width / 2, height / 2 - 20);
  

  // Draw the "Main Menu" button
  drawButton("Main Menu", menuButtonX, menuButtonY);
  
    // Draw the "Restart" button
  drawButton("Restart", restartButtonX, restartButtonY);
}


// Global variables for new button positions (adjust the Y positions as needed)
let instructionsButtonY = 267.5; // Positioned below the "Play" button
let creditsButtonY = 315; // Positioned below the "Instructions" button

function drawBackgroundAndTitle() {
    image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    textSize(30);
    textAlign(CENTER, CENTER);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    fill(255); // White fill for the text
    text("The Dog ate my Homework!", width / 2, height / 2 - 20);
}

function drawStartScreen() {
    drawBackgroundAndTitle();
    drawButton("Instructions", width / 2 - buttonWidth / 2, instructionsButtonY);
    drawButton("Credits", width / 2 - buttonWidth / 2, creditsButtonY);
    drawButton("Play", width / 2 - buttonWidth / 2, height / 2 + 20);
}

function drawPlayScreen() {
    image(bgImage, 200, 200, width, height);
    dog.display();
    whiteSquare.display();
    displayGameInfo();
}

function drawInstructionsScreen() {
    image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    fill(255);
    textSize(16);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    textAlign(CENTER, CENTER);
    text("The aim of the game is to eat as much of your owner's homework in 30 seconds before you get caught. Use the arrow keys to move around.", width / 8, height / 5, 300, 200); // Adjust the box size if needed
    drawButton("Main Menu", menuButtonX, menuButtonY);
}

function drawCreditsScreen() {
   image(startScreenImage, 200, 200, width, height); // Repeated background and title setup
    fill(255);
    textSize(16);
    stroke(0); // Black outline
    strokeWeight(4); // Thickness of the outline
    textAlign(CENTER, CENTER);
    text("Credits:\nGame Design: Jihad\nProgramming: Jihad, Chatgpt \nArtwork: brgfx, babysofja  \nSound: Pixabay",
      width / 8, height / 5, 300, 200); // The text box's width and height
    drawButton("Main Menu", menuButtonX, menuButtonY); // Adjust the Y position to place it below the credits text
}


function displayGameInfo() {
    let timePassed = (millis() - timerStartTime) / 1000;
    let timeLeft = max(30 - timePassed, 0);
    fill(0);
    noStroke();
    textSize(16);
    textAlign(RIGHT, BOTTOM);
    text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

    checkCollisionsAndScore();
    displayScore();
    handleMovement();
}

function checkCollisionsAndScore() {
let hit = rectRectCollision(dog.x - dog.img.width / 2 * dog.scale, dog.y - dog.img.height / 2 * dog.scale, dog.img.width * dog.scale, dog.img.height * dog.scale, whiteSquare.x, whiteSquare.y, whiteSquare.size, whiteSquare.size);
  if (hit) {
    dog.growAndSlow();
    whiteSquare.respawn();
    score++;

    // Play collision sound at 30% volume
    collisionSound.setVolume(0.3); // Set volume to 30%
    collisionSound.play();
  }
}

function displayScore() {
    fill(0);
    stroke(255);
    strokeWeight(2);
    textSize(16);
    textAlign(RIGHT, TOP);
    text("Score: " + score, width - 10, 10);
}

function handleMovement() {
    if (keyIsDown(LEFT_ARROW)) dog.move(-1, 0);
    if (keyIsDown(RIGHT_ARROW)) dog.move(1, 0);
    if (keyIsDown(UP_ARROW)) dog.move(0, -1);
    if (keyIsDown(DOWN_ARROW)) dog.move(0, 1);
}

function draw() {
    switch (gameState) {
        case "start":
            drawStartScreen();
            break;
        case "play":
        // Start the looped sound if it's not already playing and the game state just switched to "play"
      if (!gameLoopSound.isPlaying()) {
        gameLoopSound.loop();
        gameLoopSound.setVolume(0.5); // Set volume to 50%
      }
            drawPlayScreen();
            // Calculate time left here
            let timePassed = (millis() - timerStartTime) / 1000;
            let timeLeft = max(30 - timePassed, 0);
            
            // Display the timer
            fill(0);
            noStroke();
            textSize(16);
            textAlign(RIGHT, BOTTOM);
            text("Time left: " + timeLeft.toFixed(1), width - 10, height - 10);

            if (timeLeft <= 0) {
                gameState = "gameover";
              if (gameLoopSound.isPlaying()) {
          gameLoopSound.stop();
        }
            }

            // Check if the dog collides with the square and handle scoring
            checkCollisionsAndScore();

            // Display the current score
            displayScore();

            // Handle player movement
            handleMovement();
            break;
        case "instructions":
            drawInstructionsScreen();
            break;
        case "credits":
            drawCreditsScreen();
            break;
        case "gameover":
            drawGameOverScreen();
            break;
    }
}

 

Midterm Project: Catch ’em

Game:

Link to fullscreen: https://editor.p5js.org/aneekap/full/En7_lTESA

Overall Concept of the Project:
CATCH ‘EM is a catching game with two levels but with a twist. Inspired by two of my favorite shows, Demon Slayer and Avatar: The Last Airbender, in this game, the player needs to catch the “good guys” and avoid the “bad guy” to survive. Elements fall from the top of the screen randomly, and the player controls a basket at the bottom to catch the falling elements. The goal is to accumulate points by catching enough good guys before the timer runs out. The game incorporates changing themes and increasing difficulty as the player progresses. Additionally, power-ups appear occasionally, providing the player with special abilities such as temporary immunity to the villain.

Working :
The game utilizes OOP to create subclasses of the Element class with varying features. The game grid is defined by invisible tiles, allowing precise positioning of falling elements. Elements move vertically in tile increments, and collision detection occurs when their coordinates align with the basket’s position, triggering consequences. The game employs frame-based updates to control element speeds. Levels are managed through conditional checks on scores. The player controls a basket moves horizontally using left and right arrow keys to catch the elements.

More specifically, the game operates within a structured grid system defined by the number of rows and columns. Each element, whether it be a hero, villain, or power-up, is represented by an object that inherits from the Element class. These objects are initialized with random starting positions along the columns of the grid. The update method of the Element class ensures a downward motion, simulating the falling effect. The catch method detects whether an element is caught by the player’s basket.

The code I wanted to highlight is how the gameplay works for each level. I used arrays for the different classes to create the elements at each level and used framecount to control the number of elements on the screen to avoid overcrowding.

display() {
    if (SCORE >= 3 && SLIDE == 2) {
      SCORE = 0;
      this.villains = [];
      this.powerups = [];
      this.heroes1 = [];
      SLIDE = 3;
      TIMESTART = millis();
    }

    if (SLIDE == 3) {
      image(inst2, 0, 0, width, height);
    }

    if (SCORE >= 5 && SLIDE == 4) {
      SCORE = 0;
      this.villains = [];
      this.powerups = [];
      this.heroes2 = [];
      SLIDE = 5;
    }

    if (SLIDE == 5) {
      WIN = true;
      this.Play = false;
    }

    // LEVEL 1
    if (SLIDE == 2) {
      image(bg1, 0, 0, this.w, this.h);

      if (frameCount % 3 == 0) {
        this.heroes1.push(new Heroes1());
      }
      for (let hero of this.heroes1) {
        hero.display();
        if (hero.y >= height + 40) {
          // Removes character once it exits the screen
          this.heroes1.splice(this.heroes1.indexOf(hero), 1);
        }
      }
      if (frameCount % 5 == 0) {
        this.villains.push(new Villain(1));
      }
      for (let villain of this.villains) {
        villain.display();
        if (villain.y >= height + 40) {
          this.villains.splice(this.villains.indexOf(villain), 1);
        }
      }

      if (this.powerups.length < 2) {
        this.powerups.push(new PowerUp(1));
      }
      for (let powerup of this.powerups) {
        powerup.display();
        if (powerup.y >= height + 40) {
          this.powerups.splice(this.powerups.indexOf(powerup), 1);
        }
      }

      textSize(30);
      text("LEVEL 1", width / 2 - 60, 45);
      textSize(25);
      text("Aim: 3", width - 100, 80);
      textSize(25);
      text("SCORE: " + SCORE, width - 120, 45);

      let COUNTDOWN = 20 - (millis() - TIMESTART) / 1000;
      if (COUNTDOWN <= 0) {
        this.Play = false;
      }
      text("Time: " + floor(COUNTDOWN), 20, 45);

      this.basket.display();
      this.basket.update();
    }

    // LEVEL 2
    if (SLIDE == 4) {
      image(bg2, 0, 0, this.w, this.h);

      if (frameCount % 2 == 0) {
        this.heroes2.push(new Heroes2());
      }
      for (let hero of this.heroes2) {
        hero.display();
        if (hero.y >= height + 40) {
          this.heroes2.splice(this.heroes2.indexOf(hero), 1);
        }
      }

      if (frameCount % 4 == 0) {
        this.villains.push(new Villain(2));
      }
      for (let villain of this.villains) {
        villain.display();
        if (villain.y >= height + 40) {
          this.villains.splice(this.villains.indexOf(villain), 1);
        }
      }

      if (this.powerups.length < 2) {
        this.powerups.push(new PowerUp(2));
      }
      for (let powerup of this.powerups) {
        powerup.display();
        if (powerup.y >= height + 40) {
          this.powerups.splice(this.powerups.indexOf(powerup), 1);
        }
      }

      textSize(32);
      text("LEVEL 2", width / 2 - 75, 45);
      fill(0, 0, 0);
      textSize(30);
      text("Aim: 5", width - 100, 80);
      textSize(25);
      text("SCORE: " + SCORE, width - 120, 45);

      fill(255, 255, 255);
      let COUNTDOWN = 15 - (millis() - TIMESTART) / 1000;
      if (COUNTDOWN <= 0) {
        this.Play = false;
      }
      text("Time: " + floor(COUNTDOWN), 20, 45);

      this.basket.display();
      this.basket.update();
    }
  }

I took the graphics from the web and cropped them accordingly.

 

The background music was taken from YouTube and was taken from the Original Soundtrack of the shows.

Level 1: https://www.youtube.com/watch?v=HcvK20wWQDw

Level 2: https://www.youtube.com/watch?v=OqJec3–RXc

 

Challenges Faced and Areas for Improvement:
The hardest part was having an organized structure of classes and reducing redundancy by using inheritance wherever possible. The other challenge was having so many elements on the screen at once and ensuring it didn’t glitch by deleting elements correctly. I was also not able to make a full-screen mode since the images and tiles were getting stretched and did not work as well.
An area for improvement is ensuring that elements don’t overlap. Since elements are generated randomly, preventing them from overlapping proved difficult. I would also like to add more levels/modes.

Overall, I am happy with the outcome and it was a fun project to work on.

Midterm Project: Dungeon Maze

Concept

This dungeon-based maze game’s premise centers on an exciting and dynamic challenge where players are sent into several mazes that are produced by an algorithm, each with its own layout and set of coins and teleporters.

The game’s main goal, set against an engrossing dungeon backdrop, requires players to maneuver tactically through the maze, collecting gold coins while racing against the clock to locate the exit. The game uses teleporters as a game-changing element to enhance depth and complexity. Players can quickly go to different mazes, which resets the maze’s layout and offers unexpected twists. This makes the ‘Dungeon Master’ change their strategy.

In addition, an interesting limited vision feature simulates the real difficulty of traversing a dimly lit dungeon by preventing players from seeing beyond their immediate surroundings and forcing them to depend solely on memory and spatial awareness in order to advance. A fascinating and unpredictable journey that tests players’ intelligence and flexibility through the mix of random maze creation and other mechanisms like timers and limited vision, making every playthrough a unique experience.

Sketch

Snapshots of the Game

Elements of Pride

The choices made in the design of features like the drawLimitedVision effect, the generateRandomMaze function, and the adaptive design for fullscreen and windowed modes are a demonstration of good game designs and good technical decisions.

generateRandomMaze() {
    let maze = new Array(this.rows);
    for (let y = 0; y < this.rows; y++) {
      maze[y] = new Array(this.cols).fill('#');
    }
    const carvePath = (x, y) => {
      const directions = [[1, 0], [-1, 0], [0, 1], [0, -1]];
      this.shuffle(directions);

      directions.forEach(([dx, dy]) => {
        const nx = x + 2 * dx, ny = y + 2 * dy;
        if (nx > 0 && nx < this.cols - 1 && ny > 0 && ny < this.rows - 1 && maze[ny][nx] === '#') {
          maze[ny][nx] = ' ';
          maze[y + dy][x + dx] = ' ';
          carvePath(nx, ny);
        }
      });
    };

    const startX = 2 * Math.floor(Math.random() * Math.floor((this.cols - 2) / 4)) + 1;
    const startY = 2 * Math.floor(Math.random() * Math.floor((this.rows - 2) / 4)) + 1;
    maze[startY][startX] = ' ';
    carvePath(startX, startY);
    let coinCount = Math.floor(Math.random() * (12 - 8 + 1)) + 8; // 8 to 12 coins
    let teleporterCount = Math.floor(Math.random() * (2 - 1 + 1)) + 1; // 1 to 2 teleporters
    maze[this.rows - 2][this.cols - 2] = 'E';
    this.addRandomItems(maze, 'C', coinCount);
    this.addRandomItems(maze, 'T', teleporterCount);

    return maze;
  }

The generateRandomMaze function is a cornerstone of the game’s dynamic environment, creating a new, solvable maze for each session or level. The function uses a recursive backtracking algorithm, it selects a random starting point within the maze and carves out paths. This is done by moving two cells at a time in a chosen direction (to avoid creating isolated cells) and removing walls between cells to create a path. This process repeats recursively for each new cell reached that has all its surrounding cells intact.

Once the maze is generated, the function strategically places coins (‘C’) and teleporters (‘T’) within the maze. Coins act as collectibles, and teleporters serve as transport points to new mazes. An exit (‘E’) is placed at a predefined location, typically far from the start, ensuring that players must navigate through the maze to find it.

function drawLimitedVision(playerX,playerY,visibilityRadius) {

  // Draw a radial gradient from transparent to dark
  let radialGradient = drawingContext.createRadialGradient(playerX, playerY, 0, playerX, playerY, visibilityRadius);
  radialGradient.addColorStop(0, 'rgba(0,0,0,0)'); // Completely transparent at the center
  radialGradient.addColorStop(1, 'rgba(0,0,0,0.8)'); // More opaque towards the edges

  drawingContext.fillStyle = radialGradient;
  drawingContext.fillRect(0, 0, width, height);
}

The drawLimitedVision function enhances the game’s atmosphere by simulating a limited field of vision for the player, similar to carrying a light source in a dark dungeon. The function creates a radial gradient centered on the player’s current position. This gradient transitions from transparent (at the center) to opaque (at the edges) obscures parts of the maze not immediately near the player.

function calculateCellSize() {
  cellSize = min(width, height) / max(mazeColumns, mazeRows);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  calculateCellSize();
}

The code includes mechanisms to adapt to fullscreen and windowed modes. The calculateCellSize function dynamically calculates the size of each maze cell based on the current dimensions of the game window. This ensures that the maze scales correctly when switching between fullscreen and windowed modes, maintaining consistent gameplay and visual quality. The windowResized function listens for changes in the window size (including toggling fullscreen mode) and adjusts the canvas size accordingly.

Difficulties

One of the trickier parts of developing the game was actually implementing the limited vision functionality. Developing a smooth, radial-gradient of light that would dynamically follow the player’s motions and provide vision around them while keeping the remainder of the maze in complete darkness was the challenge. It took several tries to get this effect just right so that it seemed natural and didn’t interfere with gameplay by being too dark or changing too quickly.

There were further difficulties when trying to get the game to smoothly switch between fullscreen and regular window modes. Flexible design was necessary to make sure that the maze, player character, and user interface (UI) components all scaled and remained proportionate across a range of screen sizes and aspect ratios. This flexibility was essential to maintaining the game’s playability and graphic quality in any viewing mode.

Improvements

A possible area for enhancement is the visual design and user interface. For example, adding more complex visuals to the player’s character, treasures, and maze walls might greatly improve the game’s visual attractiveness. Furthermore, adding more variation to the maze layouts and obstacles—like traps or enemies—could add new and unique difficulties and amplify the action.

The project could also benefit from a more advanced level progression system, where the difficulty gradually increases through levels, possibly by enlarging the maze, decreasing the time limit, or increasing the required percentage of coins to collect before exiting.

 

Full Code