Concept:
The concept of this project revolves around creating an interactive UFO game titled “UFO Escape.” The main goal is to score points by avoiding collisions with asteroids and navigating through space. What sets this game apart is its unique control scheme: players use a glove equipped with a gyroscope sensor as the game controller. This setup allows the game to translate the player’s hand tilt movements into navigational commands within the game environment to turn the UFO in different directions.
Video of interaction:
Description of interaction design
Implementing the “UFO Escape” game involves an interaction design that merges physical computing with digital interfaces to create an immersive gaming experience. Here’s a detailed breakdown of the interaction design and implementation:
Hardware Setup:
Gyroscope Sensor (3-Axis Gyroscope L3GD20H):
This sensor is integrated into a wearable glove made from velcro, foam tape, and cloth. The glove is then connected to an Arduino UNO using 4 jumper wires soldered to a 90-cm length. It is then covered by heat-shrinkable rubber tubing for aesthetics and a better wire management setup.
I kept changing the glove’s prototypes due to either wiring issues or the fact that none were non-adjustable. Below are the different versions of the glove.
This version did not allow the cables to function properly due to the positioning of the gyroscope.
Second Prototype:
This glove secured the gyroscope, but due to it not being adjustable, it kept causing issues with the wires whenever a different sized hand would wear it.
Final Prototype:
This glove concept fixed all the issues mentioned previously, from sizing difficulties to wire management, making it the ideal version for various hand sizes. It was made from scratch in the IM lab using different materials to secure the glove and a metal piece to lock in the velcro I got from home from a metal glove.
Cabling:
The cable was soldered on a stranded wire to extend the jumper wires for a more usable length for the glove.
How the code works:
The gyroscope sensor extracts the X and Y variables read by the Arduino to determine the hand’s position and movement. The Arduino Uno sends those as instructions for where the UFO should be positioned, which the P5 file reads and translates.
// Function to move the player based on arrow key inputs move() { let moveSpeed = 10; // Speed multiplier this.gotoX += gyroData.x * moveSpeed; this.gotoY += gyroData.y * moveSpeed; this.gotoX = constrain(this.gotoX, 30, width - 30); this.gotoY = constrain(this.gotoY, 30, height - 30); this.x = lerp(this.x, this.gotoX, 0.1); this.y = lerp(this.y, this.gotoY, 0.1); if (frameCount % 100 === 0) { obstacles.push(new Obstacle()); // Add new obstacles } this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds // //This is only so the player cannot exist the canvas }
This processes the gyroscope data to interpret the player’s hand movements as specific commands (left, right, up, down, accelerate).
Serial Communication:
The Arduino transmits the processed data to a computer via serial communication, and in my case, it provided the P5 with the full library of serial communication, which made the process work for me.
/** * p5.webserial * (c) Gottfried Haider 2021-2023 * LGPL * https://github.com/gohai/p5.webserial * Based on documentation: https://web.dev/serial/ */ 'use strict';
Game Mechanics:
The game is developed in a way that makes it hard for the player to achieve a high score by making the meteorites come down faster and faster as the player progresses to higher scores.
// Function to update obstacle position update() { this.y += map(score, 0, 10, gameSpeed, gameSpeed + 5); // Moves obstacles down the screen //this will make game harder as time grows } }
Also, the player should be included in the provided map of the game so no cheating occurs and the game is played fairly based on skill levels.
this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds // //This is only so the player cannot exist the canvas }
Player Interaction:
Players wear the gyroscope-equipped glove and move their hands to control the UFO within the game. Movements are intuitive: tilting the hand to the sides steers the UFO laterally, while tilting forward or backward would control vertical movement, and depending on how fast you tilt your hands, you could possibly send the UFO in any direction of the map faster than needed making you very aware of what move you would wanna make next as the meteoroids are coming down.
Visuals/Audio:
The game provides real-time visual feedback by updating the position and movements of the UFO based on the player’s actions as well as background noise for when the UFO is flying around.
P5 Sketch:
let port; // Serial port instance let gyroData = { x: 0, y: 0 }; // Placeholder for gyro data let serialInterval; // Interval for polling serial data // Variables for game assets let bgImage; // Variable to hold the background image for menu and game over screens let bgMusic; // Variable to hold the background music for gameplay let player; // Player object let obstacles = []; // Array to store obstacles let gameSpeed = 6; // Speed at which obstacles move let score = 0; // Player's score let gameState = "MENU"; // Initial game state; that is "MENU", "PLAYING", or "GAME OVER" let rockImage; // Variable to hold the rock image let gameOverImage; // Preload function to load game assets before the game starts function preload() { // bgImage = loadImage("space.png"); // Loads the background image menuImage = loadImage("menu.jpeg"); // gameplayBgImage = loadImage("gameplay.jpg"); // Loads the gameplay background image bgMusic = loadSound("gameplaysound.mp3"); // Loads the background music rockImage = loadImage("rock-2.png"); // Loads the rock image } // The setup function to initialize the game function setup() { createCanvas(750, 775).parent("canavs-container"); // Size of the game canvas openButton = createButton("Connect Arduino") .position(410, 20) .style("background-color", "rgba(244,238,238,0.1)(255, 50)") .style("border-radius", "70px") .style("padding", "10px 20px") .style("font-size", "15px") .style("color", "white") .mousePressed(openSerialPort); // Click to open serial port player = new Player(); // Initializes the player object textAlign(CENTER, CENTER); // Setting text alignment for drawing text textFont("arial"); } function openSerialPort() { port = createSerial(); // Initialize the serial port instance if (port && typeof port.open === "function") { port.open("Arduino", 9600); // Open with a predefined preset // Set up polling to check for serial data every 100ms serialInterval = setInterval(readSerialData, 100); // Poll for data } else { console.error("Failed to initialize the serial port."); } } function readSerialData() { if (port && port.available()) { let rawData = port.readUntil("\n"); // Reads data till newline if (rawData && rawData.length > 0) { let values = rawData.split(","); // Splits by commas if (values.length === 2) { gyroData.x = parseFloat(values[0]); // Parse X value gyroData.y = parseFloat(values[1]); // Parse Y value } else { console.error("Unexpected data format:", rawData); // Error handling } } } } // Draw function called repeatedly to render the game function draw() { // Displays the space background image only during menu and game over states but displays a different image during gameplay if (gameState === "PLAYING") { background("#060C15"); noStroke(); fill(255); for (let star of stars) { circle(star.x, star.y, star.r); star.y += star.yv; if (star.y > height + 5) star.y = -5; } } // Handles game state transitions if (gameState === "MENU") { drawMenu(); } else if (gameState === "PLAYING") { if (!bgMusic.isPlaying()) { bgMusic.loop(); // Looping the background music during gameplay } playGame(); } else if (gameState === "GAME OVER") { bgMusic.stop(); // Stops the music on game over drawGameOver(); } else { //info drawInfo(); } } function drawInfo() { background("#060C15"); noStroke(); fill(255); for (let star of stars) { circle(star.x, star.y, star.r); star.y += star.yv; if (star.y > height + 5) star.y = -5; } textSize(16); text( "Connect the Arduino\n Wear the glove\n, Control the UFO through tilting your hand \nLeft, Right, Up and Down.", width / 2, height / 2 ); stroke(255); Button("MENU", width / 2, height - 100, 100, 40); } // ---------------Function to display the game menu function drawMenu() { background(menuImage); fill(200, 100, 100); stroke(200, 100, 100); textSize(62); strokeWeight(2); text("UFO ESCAPE", width / 2, 140); fill(255); text( "UFO ESCAPE", width / 2 - map(mouseX, 0, width, -5, 5), 135 - map(mouseY, 0, height, -2, 2) ); let x, y; if (frameCount % 40 < 20) { x = width / 2 - map(frameCount % 40, 0, 20, -5, 5); y = 400; } else { x = width / 2 - map(frameCount % 40, 20, 40, 5, -5); y = 400; } Ufo(x, y, 60, 30); //startButton Button("START", width / 2, 550); //info page Button("INFO", width - 115, 40, 100, 40); } // Function to handle gameplay logic function playGame() { fill(255); textSize(25); text(`Score: ${score}`, width / 2, 50); player.show(); // Displays the player player.move(); // Moves the player based on key inputs // Adding a new obstacle at intervals if (frameCount % 120 == 0) { obstacles.push(new Obstacle()); } // Updates and displays obstacles for (let i = obstacles.length - 1; i >= 0; i--) { obstacles[i].show(); obstacles[i].update(); // Checks for collisions if (player.collidesWith(obstacles[i])) { gameOverImage = get(); gameState = "GAME OVER"; } // Removes obstacles that have moved off the screen and increment score if (obstacles[i].y > height) { obstacles.splice(i, 1); i--; score++; } } } // Function to display the game over screen function drawGameOver() { if (gameOverImage) image(gameOverImage, 0, 0); fill(200, 100, 100); stroke(200, 100, 100); textSize(46); text("GAME OVER", width / 2, height / 2 + 50); fill(255); text(score, width / 2, height / 2 - 50); Button("RESTART", width / 2, 550); } // Function to reset the game to its initial state function resetGame() { obstacles = []; // Clear existing obstacles score = 0; // Reset score player = new Player(); // Reinitialize the player stars = []; for (let i = 0; i < 100; i++) { let r = random(1, 3); stars.push({ x: random(width), y: random(height), r: r, yv: map(r, 1, 3, 0.01, 0.1), }); } } // Player class class Player { constructor() { this.width = 60; // Width of the UFO this.height = 30; // Height of the UFO this.x = width / 2; // Starting x position this.y = height - 100; // Starting y position this.gotoX = this.x; this.gotoY = this.y; this.speed = 5; } // Function to display the UFO show() { this.y = lerp(this.y, this.gotoY, 0.1); this.x = lerp(this.x, this.gotoX, 0.1); //and change this.gotoX stroke(200, 100, 100); Ufo(this.x, this.y, this.width, this.height); } // Function to move the player based on arrow key inputs move() { let moveSpeed = 10; // Speed multiplier this.gotoX += gyroData.x * moveSpeed; this.gotoY += gyroData.y * moveSpeed; this.gotoX = constrain(this.gotoX, 30, width - 30); this.gotoY = constrain(this.gotoY, 30, height - 30); this.x = lerp(this.x, this.gotoX, 0.1); this.y = lerp(this.y, this.gotoY, 0.1); if (frameCount % 100 === 0) { obstacles.push(new Obstacle()); // Add new obstacles } this.gotoX = constrain(this.gotoX, 30, width - 30); // Keeps player within horizontal bounds this.gotoY = constrain(this.gotoY, 30, height - 30); // Keep player within vertical bounds // //This is only so the player cannot exist the canvas } // Function to detect collision with obstacles collidesWith(obstacle) { return ( dist( obstacle.x + obstacle.radius, obstacle.y + obstacle.radius, this.x, this.y ) < 45 ); } } // Obstacle class class Obstacle { constructor() { this.radius = random(15, 30); // Random radius for obstacle this.x = random(this.radius, width - this.radius); // Random x position this.y = -this.radius; // Starts off-screen so it looks like its coming towards you } // Function to display the rocks show() { image(rockImage, this.x, this.y, this.radius * 2, this.radius * 2); // Draws them as a circle } // Function to update obstacle position update() { this.y += map(score, 0, 10, gameSpeed, gameSpeed + 5); // Moves obstacles down the screen //this will make game harder as time grows } } function Button(txt, x, y, w = 200, h = 60) { fill(255, 50); if ( mouseX > x - w / 2 && mouseX < x + w / 2 && mouseY > y - h / 2 && mouseY < y + h / 2 ) { fill(255, 80); if (mouseIsPressed) { mouseIsPressed = false; //so only one click happnes action(txt); } } rect(x, y, w, h, h / 2); fill(255); textSize(h / 2); text(txt, x, y); } function action(txt) { switch (txt) { case "START": gameState = "PLAYING"; resetGame(); break; case "RESTART": gameState = "MENU"; resetGame(); break; case "INFO": stars = []; for (let i = 0; i < 100; i++) { let r = random(1, 3); stars.push({ x: random(width), y: random(height), r: r, yv: map(r, 1, 3, 0.01, 0.1), }); } gameState = "INFO"; break; case "MENU": gameState = "MENU"; break; } } function Ufo(x, y, w, h) { fill(255); // Sets color to white rectMode(CENTER); rect(x, y, w, h, 20); // Draws the UFO's body fill(20); // Sets the glass color to red arc(x, y - h / 4, w / 2, h / 1, PI, 0, CHORD); // Draws the glass stroke(255); let a = map(x, 0, width, 0, PI / 4); arc(x, y - h / 4, w / 2 - 5, h - 5, -PI / 4 - a, -PI / 6 - a); for (let i = 1 + frameCount; i < 10 + frameCount; i++) { let x_ = map(i % 10, -1, 10, -30, 30); circle(x + x_, y, 5); } }
Arduino Code:
#include <Wire.h> #include <Adafruit_L3GD20_U.h> // Initialize the L3GD20 object Adafruit_L3GD20_Unified gyro = Adafruit_L3GD20_Unified(20); // Sensor ID void setup() { Serial.begin(9600); // Start serial communication at 9600 baud // Initialize the L3GD20 gyroscope if (!gyro.begin()) { Serial.println("Failed to find L3GD20 gyroscope"); while (1) { delay(10); // Halt if initialization fails } } } void loop() { sensors_event_t event; gyro.getEvent(&event); // Send the X and Y gyro data with a newline at the end Serial.print(event.gyro.x, 4); // X-axis gyro data with 4 decimal places Serial.print(","); // Comma separator Serial.println(event.gyro.y, 4); // Y-axis gyro data with 4 decimal places delay(100); // Adjust delay as needed }
What are some aspects of the project that you’re particularly proud of?
To be honest, the whole final project makes me proud that I was able to create such a complicated code with actual use in the end. The glove prototypes are also something that I was proud of because I have never created a glove from scratch before. I am just glad that it all worked out in the end and that the cables stopped popping out of the gloves whenever someone would put them on.
What are some areas for future improvement?
I would say that expanding on the game itself and making different levels and maps for the UFO to fly around in would also exemplify the game. Also, creating a button in the glove to restart once you press it would make it easier for the player to control the whole game if they lose.