MidTerm Project – Khalifa Alshamsi

Concept and Gameplay

“UFO Escape” transports players into the role of a UFO pilot tasked with the critical mission of avoiding collision with perilously floating rocks in space. The game unfolds across a dynamic background that transitions from the tranquility of space to the adrenaline-pumping danger zone filled with obstacles. Players must use the arrow keys to maneuver their spacecraft, navigating the challenging asteroid field. Each successful dodge rewards the player with points, elevating the stakes and the satisfaction derived from setting new high scores.

Technical Achievements and Design Decisions

In my opinion, one of the game’s standout features is its seamless integration of visual and auditory elements to create an immersive experience, which was the vibe I was going for. The use of distinct background images for different game states — a peaceful space scene for the menu and game over screens and a more in-the-field backdrop for gameplay.

// Displays the space background image only during menu and game over states but displays a different image during gameplay
if (gameState === "MENU" || gameState === "GAME OVER") {
  background(bgImage);
} else if (gameState === "PLAYING") {
  background(gameplayBgImage);
}

The transition between these states is goofy because of its old-school UFO soundtrack that loops during gameplay. This adds a layer of nostalgia that brings me back to the old free online games I used to play growing up in school using the school computers. The soundtrack then abruptly halts upon the game’s end, marking the player’s failure and the game’s conclusion.

Another area of pride would be collision detection, which not only ensures that the game is playable but also maintains a level of difficulty that is engaging without being frustrating, but it was highly frustrating to tune it properly to respond.

// Function to detect collision with obstacles
collidesWith(obstacle) {
  let closestX = constrain(obstacle.x, this.x - this.width / 3, this.x + this.width / 3);
  let closestY = constrain(obstacle.y, this.y - this.height / 3, this.y + this.height / 3);
  let distanceX = obstacle.x - closestX;
  let distanceY = obstacle.y - closestY;
  let distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
  return distanceSquared < (obstacle.radius * obstacle.radius); // Checks if distance is less than obstacle radius
}

The game accurately determines collisions by calculating the closest points between the UFO and the incoming asteroids. This method considers the actual positions and dimensions of both objects, offering a realistic and, for the most part, precise detection mechanism that enhances the player’s experience.

Areas for Improvement and Challenges

While “UFO Escape” is the first game I got to create, and I am extremely proud of it, certain aspects remain ripe for enhancement. The collision detection system, though effective, could be refined to accommodate the irregular shapes of the rocks more accurately.

Another challenge faced during development was ensuring the game’s performance remained smooth despite the increasing number of obstacles. This issue kept bugging the game, making the rocks not properly collide with the UFO ship. This issue was largely mitigated through optimization techniques, such as removing off-screen obstacles from the array to free up resources.

Conclusion

“UFO Escape” embodies the spirit of classic arcade games, but I would say with a bit more graphics. Its development journey was filled with learning opportunities, from refining collision detection algorithms so many times that I lost count to creating an engaging user interface.

Sketch:

Script:

// 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

// Preload function to load game assets before the game starts
function preload() {
  bgImage = loadImage('space.png'); // Loads the background image
    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(400, 600); // Size of the game canvas
  player = new Player(); // Initializes the player object
  textAlign(CENTER, CENTER); // Setting text alignment for drawing text
}

// 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 === "MENU" || gameState === "GAME OVER") {
    background(bgImage);
  } else if (gameState === "PLAYING") {
    background(gameplayBgImage);
  }

  // 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();
  }
}

// Function to display the game menu
function drawMenu() {
  fill(255,0 ,0);
  textSize(24);
  text("UFO Escape", width / 2, height / 2.5);
  textSize(16);
  fill(255);
  text("Avoid the rocks!\nUse ARROW KEYS to move\nPress ENTER to start", width / 2, height / 2);
}

// Function to handle gameplay logic
function playGame() {
  fill(255);
  textSize(16);
  text(`Score: ${score}`, width / 2, 30);

  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])) {
      gameState = "GAME OVER";
    }
    // Removes obstacles that have moved off the screen and increment score
    if (obstacles[i].y > height) {
      obstacles.splice(i, 1);
      score++;
    }
  }
}

// Function to display the game over screen
function drawGameOver() {
  fill(255, 0, 0);
  textSize(32);
  text("Game Over", width / 2, height / 2);
  textSize(16);
  fill(255);
  text("Press ENTER to restart", width / 2, height / 2 + 40);
}

// Function to handle key presses
function keyPressed() {
  // Starts the game or restart after game over when ENTER is pressed
  if (keyCode === ENTER || keyCode === RETURN) {
    if (gameState === "MENU" || gameState === "GAME OVER") {
      gameState = "PLAYING";
      resetGame();
    }
  }
}

// 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
}

// 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
  }

  // Function to display the UFO
  show() {
    fill(255); // Sets color to white
    rectMode(CENTER);
    rect(this.x, this.y, this.width, this.height, 20); // Draws the UFO's body
    fill(255, 0, 0); // Sets the glass color to red
    arc(this.x, this.y - this.height / 4, this.width / 2, this.height / 1, PI, 0, CHORD); // Draws the glass
  }

  // Function to move the player based on arrow key inputs
  move() {
    if (keyIsDown(LEFT_ARROW)) {
      this.x -= 5; // Moves left
    }
    if (keyIsDown(RIGHT_ARROW)) {
      this.x += 5; // Moves right
    }
    if (keyIsDown(UP_ARROW)) {
      this.y -= 5; // Moves up
    }
    if (keyIsDown(DOWN_ARROW)) {
      this.y += 5; // Moves down
    }
    this.x = constrain(this.x, 0, width); // Keeps player within horizontal bounds
    this.y = constrain(this.y, 0, height); // Keep player within vertical bounds
    //This is only so the player cannot exist the canvas
  }

  // Function to detect collision with obstacles
  collidesWith(obstacle) {
    let closestX = constrain(obstacle.x, this.x - this.width / 3, this.x + this.width / 3);
    let closestY = constrain(obstacle.y, this.y - this.height / 3, this.y + this.height / 3);
    let distanceX = obstacle.x - closestX;
    let distanceY = obstacle.y - closestY;
    let distanceSquared = (distanceX * distanceX) + (distanceY * distanceY);
    return distanceSquared < (obstacle.radius * obstacle.radius); // Checks if distance is less than obstacle radius
  }
}

// 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 obstacles position
  update() {
    this.y += gameSpeed; // Moves obstacles down the screen
  }
}

 

Leave a Reply