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




