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; } }