LET’S SURF!
Concept:
The end of an era! I’m proud to say that my game was a huge success and came out exactly how I envisioned it which was an immersive and interactive surfing simulator game. The idea came to me when I went to the arcade and saw a snowboarding game and it sparked my interest to know how it was made and if I could replicate it with something I enjoyed and miss which is surfing. The game uses a physical skateboard found in the IM lab equipped with an accelerometer to capture the player’s movements and mimics the snowboarding game actions, and a P5.js-based game that simulates surfing and where the user collects shells and avoids sharks (3 bites gets them killed) and the speed increases as the game goes on.
Inspiration:
Results:
Circuit:
Schematic:
Real-life:
In Action:
Arduino Setup
- Hardware: The core of the physical interaction is an accelerometer (like the MMA8452Q) mounted on a skateboard. This sensor detects the angle and intensity of the skateboard’s tilt.
- Arduino Sketch:
Setup()
: Initializes serial communication at a specific baud rate and sets up the accelerometer.Loop()
: Continuously reads the X and Y-axis data from the accelerometer. This data is formatted into a string (e.g., “X,Y”) and sent over serial communication to the P5.js sketch. Additionally, the sketch listens for incoming data from P5.js, which could be used for future features like haptic feedback based on game events.
Code:
// Define variables for images, screens, sounds, buttons, game state, and other parameters let bgImage, bgImage2, catImage, sharkImage, shellImage; // Images for background, cat, shark, and shell let welcomeScreen, instructionScreen, countdownScreen, gameOverScreen; // Images for different game screens let bgSound, collectedSound, sharkBiteSound; // Sound effects for background, collecting items, and shark bite let replayButton, homeButton, pauseButton; // Buttons for replay, home, and pause (not implemented in the code) let catY, sharks = [], shells = []; // Y position of the cat, arrays for sharks and shells let bgX1 = 0, bgX2, speed = 4, hits = 0, score = 0; // Background positions, game speed, hit count, and score let waterLevel; // Level of the water in the game let gameState = 'welcome'; // Current state of the game (e.g., welcome, play, paused) let countdownValue = 3; // Countdown value before the game starts let accelerometerX, accelerometerY; // Variables to store accelerometer values (for physical device interaction) let left = 50; // Value to be sent to Arduino let right = -50; // Value to be sent to Arduino // Preload function to load images and sounds before the game starts function preload() { // Load images bgImage = loadImage('background.png'); bgImage2 = loadImage('background2.png'); catImage = loadImage('cat.png'); sharkImage = loadImage('shark.png'); shellImage = loadImage('seashell.png'); welcomeScreen = loadImage('welcome screen.png'); instructionScreen = loadImage('instruction screen.png'); countdownScreen = loadImage('countdown screen.png'); gameOverScreen = loadImage('game over screen.png'); // Setup sound formats and load sound files soundFormats('wav'); bgSound = loadSound('background sound.wav'); collectedSound = loadSound('collected.wav'); sharkBiteSound = loadSound('shark bite.wav'); } // Function to read data from a serial connection (presumably from an Arduino) function readSerial(data) { // Check if the data is not null if (data != null) { let fromArduino = split(trim(data), ","); if (fromArduino.length == 2) { // Ensure the data has two parts (X and Y) accelerometerX = int(fromArduino[0]); // Parse X value console.log(accelerometerX); // Log X value (for debugging) accelerometerY = int(fromArduino[1]); // Parse Y value console.log(accelerometerY); // Log Y value (for debugging) } } let sendToArduino = left + "," + right + "\n"; // Prepare data to send back to Arduino console.log('writing'); // Log writing action (for debugging) writeSerial(sendToArduino); // Send data to Arduino (writeSerial function not provided in the code) } // Setup function runs once when the program starts function setup() { createCanvas(windowWidth, windowHeight); // Create a canvas that fills the window catY = height / 2; // Set initial Y position of the cat waterLevel = height / 2.5; // Set water level position bgX2 = width; // Set initial position of the second background image bgSound.loop(); // Play background sound in a loop } // Draw function continuously executes the lines of code contained inside its block function draw() { clear(); // Clear the canvas displayScreenBasedOnState(); // Call function to display the appropriate screen based on the game state } // Function to display the current screen based on the game's state function displayScreenBasedOnState() { // Use a switch statement to handle different game states switch (gameState) { case 'welcome': image(welcomeScreen, 0, 0, width, height); // Display welcome screen break; case 'instructions': image(instructionScreen, 0, 0, width, height); // Display instructions screen break; case 'countdown': image(countdownScreen, 0, 0, width, height); // Display countdown screen displayCountdown(); // Call function to display countdown break; case 'play': playGame(); // Call function to play the game break; case 'paused': fill(173, 216, 230, 130); // Set color for pause screen overlay rect(0, 0, width, height); // Draw rectangle overlay fill(255); // Set color for text (white) textSize(48); // Set text size textAlign(CENTER, CENTER); // Set text alignment text("Game Paused", width / 2, height / 2); // Display pause text break; case 'gameOver': image(gameOverScreen, 0, 0, width, height); // Display game over screen noLoop(); // Stop the draw loop, effectively pausing the game break; } } // Function to display the countdown before the game starts function displayCountdown() { fill(255); // Set color for text (white) textSize(70); // Set text size textAlign(CENTER, CENTER); // Set text alignment text(countdownValue, width / 2, height / 2); // Display countdown number // Decrease countdown value every second if (frameCount % 60 === 0 && countdownValue > 0) { countdownValue--; } else if (countdownValue === 0) { gameState = 'play'; // Change game state to play when countdown reaches 0 countdownValue = 3; // Reset countdown value for next time } } // Function to handle the gameplay function playGame() { backgroundScrolling(); // Call function to scroll the background displayCat(); // Call function to display the cat handleSharks(); // Call function to handle sharks handleSeashells(); // Call function to handle seashells displayScore(); // Call function to display the score increaseDifficulty(); // Call function to increase game difficulty over time } // Function to handle background scrolling function backgroundScrolling() { image(bgImage, bgX1, 0, width, height); // Draw the first background image image(bgImage2, bgX2, 0, width, height); // Draw the second background image bgX1 -= speed; // Move the first background image leftward bgX2 -= speed; // Move the second background image leftward // Reset background positions for continuous scrolling effect if (bgX1 <= -width) bgX1 = bgX2 + width; if (bgX2 <= -width) bgX2 = bgX1 + width; } // Function to display the cat character function displayCat() { // Move the cat up or down based on key presses or accelerometer data if (keyIsDown(UP_ARROW) || keyIsDown(85)) catY -= 5; // Move up with UP_ARROW or 'U' key if (keyIsDown(DOWN_ARROW) || keyIsDown(68)) catY += 5; // Move down with DOWN_ARROW or 'D' key catY = accelerometerY + 500; // Position cat based on accelerometer data catY = constrain(catY, waterLevel, height - 100); // Constrain cat's movement within the canvas image(catImage, 50, catY, 125, 125); // Draw the cat image at the calculated position } // Function to handle sharks in the game function handleSharks() { // Generate new sharks at regular intervals if (frameCount % 150 === 0) { let sharkY = random(waterLevel - 1, height - 90); // Random Y position for sharks sharks.push({ x: width, y: sharkY }); // Add new shark to the sharks array } // Loop through all sharks and update their positions for (let i = sharks.length - 1; i >= 0; i--) { let shark = sharks[i]; shark.x -= speed; // Move shark leftward image(sharkImage, shark.x, shark.y, 300, 300); // Draw shark image // Check for collision between cat and shark if (dist(30, catY, shark.x, shark.y) < 84) { hits++; // Increase hits count sharkBiteSound.play(); // Play shark bite sound sharks.splice(i, 1); // Remove the collided shark from the array if (hits >= 3) { gameState = 'gameOver'; // End the game after 3 hits } } } } // Function to handle seashells in the game function handleSeashells() { // Generate new seashells at regular intervals if (frameCount % 300 === 0) { let shellY = random(waterLevel + 100, height - 70); // Random Y position for seashells shells.push({ x: width, y: shellY }); }// Add new shell to the shells array // Loop through all seashells and update their positions for (let i = shells.length - 1; i >= 0; i--) { let shell = shells[i]; shell.x -= speed; // Move shell leftward image(shellImage, shell.x, shell.y, 50, 50); // Draw shell image // Check for collision between cat and shell if (dist(50, catY + 62.5, shell.x, shell.y) < 100) { score++; // Increase score shells.splice(i, 1); // Remove the collected shell from the array collectedSound.play(); // Play sound upon collecting a shell } } } // Function to display the game score function displayScore() { fill(255); // Set text color to white textSize(27); // Set text size text('Score: ', 70, 30); // Display "Score: " text(score, 150, 30); // Display the current score image(shellImage, 110, 11, 30, 30); // Display shell icon next to score text('Bitten: ', 70, 68); // Display "Bitten: " text(hits, 150, 70); // Display the number of times the cat has been bitten image(sharkImage, 73, 17, 100, 100); // Display shark icon next to hits } // Function to gradually increase the difficulty of the game function increaseDifficulty() { if (frameCount % 500 === 0) speed += 0.5; // Increase the speed of the game every 500 frames } // Function to toggle the game's pause state function togglePause() { if (gameState === 'play') { gameState = 'paused'; // Change game state to paused noLoop(); // Stop the draw loop, effectively pausing the game } else if (gameState === 'paused') { gameState = 'play'; // Resume playing the game loop(); // Restart the draw loop } } // Function that handles key press events function keyPressed() { if (key === 'u') { // If 'u' is pressed, set up serial communication setUpSerial(); // Function to set up serial communication (not provided in the code) console.log('u clicked'); // Log action for debugging } else if (keyCode === 32) { // If space bar is pressed, toggle pause togglePause(); } else if (keyCode === 82) { // If 'R' is pressed, reset the game resetGame(); } else if (keyCode === 72) { // If 'H' is pressed, go to home screen homeGame(); } else if (key === 'p' || key === 'P') { // If 'P' is pressed, proceed to the next game state if (gameState === 'welcome') { gameState = 'instructions'; } else if (gameState === 'instructions') { gameState = 'countdown'; } } } // Function to reset the game to its initial state function resetGame() { sharks = []; // Clear the sharks array shells = []; // Clear the shells array score = 0; // Reset score to 0 hits = 0; // Reset hits to 0 catY = height / 2; // Reset the cat's position to the middle gameState = 'countdown'; // Change the game state to countdown countdownValue = 3; // Reset the countdown value loop(); // Restart the draw loop } // Function to return to the game's home screen function homeGame() { sharks = []; // Clear the sharks array shells = []; // Clear the shells array score = 0; // Reset score to 0 hits = 0; // Reset hits to 0 catY = height / 2; // Reset the cat's position to the middle gameState = 'welcome'; // Change the game state to welcome countdownValue = 3; // Reset the countdown value loop(); // Restart the draw loop } // Function to handle window resizing function windowResized() { resizeCanvas(windowWidth, windowHeight); // Resize the canvas to the new window size } //ARDUINO CODE // #include <Wire.h> // #include <SparkFun_MMA8452Q.h> // Include the SparkFun MMA8452Q accelerometer library // MMA8452Q accel; // Create an instance of the MMA8452Q class // void setup() { // Serial.begin(9600); // Wire.begin(); // Initialize I2C communication // if (accel.begin() == false) { // Initialize the accelerometer // Serial.println("Not Connected. Please check connections and restart the sketch."); // while (1); // } // // Start the handshake // while (Serial.available() <= 0) { // Serial.println("0,0"); // Send a starting message to p5.js // delay(300); // Wait for a bit // } // } // void loop() { // // If there is data from p5.js, read it and blink the LED // if (Serial.available()) { // // Read the incoming data from p5.js // int left = Serial.parseInt(); // int right = Serial.parseInt(); // } // // Read data from the accelerometer // if (accel.available()) { // accel.read(); // Read the accelerometer data // int x = accel.getX(); // Get the X-axis data // int y = accel.getY(); // Get the Y-axis data // int z = accel.getZ(); // Get the Z-axis data // // Send the accelerometer data to p5.js // Serial.print(x); // Serial.print(","); // Serial.println(y); // } // delay(100); // Delay before the next loop iteration // }
P5.js:
- Game Environment: The game features various interactive elements – a cat character, shark obstacles, collectible seashells, and a scrolling background to simulate underwater movement.
- Control Mechanism: The game interprets the serial data received from the Arduino as control commands for the cat character. The X and Y values from the accelerometer dictate the cat’s vertical position on the screen.
- Game States: Includes multiple states like ‘welcome’, ‘instruction’, ‘play’, ‘pause’, and ‘game over’, each managed by different functions and displaying appropriate screens.
- Game Dynamics:
- Sharks appear at random intervals and positions. Collisions between the cat and sharks result in a ‘hit’.
- Seashells also appear randomly and can be collected for points.
- The game’s difficulty increases over time by gradually speeding up the background scroll and the frequency of obstacles.
Interaction Design:
- Physical Interaction: The player stands on the skateboard. By tilting the board, they control the cat’s vertical position. The degree of tilt corresponds to the direction of the cat’s movement.
- Visual and Auditory Feedback: The game provides immediate visual responses to the player’s actions. Collecting shells and colliding with sharks triggers distinct sound effects.
Communication Between Arduino and P5.js:
- Serial Communication: Utilizes the P5.serialport library. The Arduino sends accelerometer data, which P5.js receives, parses, and uses for gameplay control.
- Data Format and Handling: The data is sent as a comma-separated string of X and Y values (e.g., “45,-10”). P5.js parses this string and maps these values to the cat’s movement.
Highlights:
- Innovative Control Mechanism: The use of a skateboard with an accelerometer as a game controller stands out as a unique feature. This approach not only adds a physical dimension to the gaming experience but also encourages physical activity.
- Responsive and Intuitive Gameplay: One of the key successes of the project is the fluid responsiveness of the game to the skateboard’s movements. The calibration of the accelerometer was fine-tuned to ensure that even subtle tilts are accurately reflected in the game, providing an intuitive control system that players can easily adapt to.
- Engaging Game Environment: The theme complete with sharks, seashells, and a dynamic background, creates a visually appealing and engaging world. This immersive setting, combined with sound effects for different interactions (like collecting shells and shark encounters), enriches the gaming experience.
Challenges:
- Calibrating the Accelerometer: One of the main challenges was calibrating the accelerometer to accurately reflect the skateboard’s movements in the game. This required extensive testing and adjustments to ensure that the data from the accelerometer translated into appropriate movements of the cat character without being overly sensitive or unresponsive.
- Understanding Accelerometer Usage: Learning to effectively use the accelerometer was a considerable challenge. It involved understanding the principles of motion and tilt detection, and then implementing this understanding in code that could accurately interpret and utilize the data.
- Serial Communication Issues: Ensuring consistent and error-free serial communication between the Arduino and P5.js was a significant challenge. This involved not only setting up the communication channel but also ensuring that the data was sent, received, and parsed correctly, without any loss or corruption.
Future Ideas:
- Multiplayer Capability: Adding a multiplayer option could transform the game into a more social and competitive experience, possibly even allowing for collaborative play.
- Enhanced Physical Design: Improving the skateboard’s design for increased stability, comfort, and safety would make the game more accessible to a wider range of players.
- Adding Levels: Implementing difficulty levels based on the player’s performance could make the game more engaging and challenging for players of all skill levels.
- Haptic Feedback Integration: Incorporating haptic feedback on the skateboard based on game events could significantly enhance the immersive quality of the game.
- High Score: Adding a high score in the end in order to keep track of everyone’s score so that they can keep trying to beat it.
Following the showcase, I am just extremely satisfied and proud of my entire game and how it turned out because a lot of people really loved and enjoyed it a lot and kept returning to try to beat their score again and even if it drove me crazy several times I’m so glad that I persevered and made something that I’m proud of.
References:
https://learn.sparkfun.com/tutorials/mma8452q-accelerometer-breakout-hookup-guide