Final Project – Khalifa AlShamsi

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.

Final Concept of the project:

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.

First Prototype:

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.

Leave a Reply