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