Let’s Surf- Post IM Showcase Post

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:

Extreme Snowboard – Toggi Fun World

Results:

Circuit:

Schematic:

MMA8452Q Hookup Fritzing Diagram

 

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

 

Leave a Reply