Exploring the Cosmos – Simon Liu – Gottfried Haider
CONCEPTION AND DESIGN
After narrowing down the six ideas, we decided to continue and further work on “Exploring the Cosmos.”. This project is an interactive space travel game designed to immerse players in a thrilling journey through the cosmos while subtly addressing environmental sustainability.
It’s our initial plan:
The game involves two players with different tasks. One player is responsible for blowing air into the humidity sensor, which is a key factor in ensuring that both players maintain adequate oxygen levels to continue playing. This player also controls the temperature of the spacesuits by adjusting potentiometers to ensure optimal conditions for both. The second player’s role is to collect the trash and control the direction using a light sensor, and the vacuum cleaner is responsible for cleaning up the debris. In short, one astronaut is in charge of trash removal, while the other is responsible for oxygen supply and temperature regulation.
The game unfolds in three stages, each of which presents the player with escalating challenges to overcome. The initial stage introduces satellites as obstacles to trash removal and travel routes. Failure to clear the trash in a timely manner may cause satellites to collide, creating more debris and affecting other satellites. Excessive trash on the screen and collisions with satellites can lead to game over. The second phase involves surviving meteors passing randomly in all directions. Collisions with meteors result in failure. The final phase requires players to evade aliens that block their path, which requires finding alternative routes. Failure to escape the aliens results in game failure. Successfully navigating the culmination of these stages determines whether the player wins or loses.
It’s our updated plan after a discussion with Professor Gottfried Haider and a consideration of the time and resources we have:
The game has two stages:
Phase 1: We designed a control box with four buttons to control up, down, left and right, and a blowhole. In the first stage, a player only needs to control the four directional buttons to allow an astronaut to collect the space junk that is floating while avoiding satellites. If the satellite is hit while moving, the game fails. The speed of the astronaut’s movement is related to the length of time the button is pressed; the longer it is pressed, the faster it is. The sound and visual effects will change according to the events of the game. Collecting a certain number of space junks will be recognized as a win and the game will move on to the next stage.
Phase 2: The left and right buttons on the control box are still active. This level requires the use of a straw to blow air inside the hole. The difference is that instead of using a humidity sensor, we use a sound sensor, and the player controls left and right with the buttons, blowing air (mimicking the delivery of oxygen to the rocket as an accelerant) to move the rocket upwards to avoid the meteors and the red line underneath. Win at the end of the countdown.
Our updated plan has one less stage but retains the core of what we wanted to say: the space environment and space exploration. Both themes were kept and they are mentioned in two separate phases. Instructions and tasks during gameplay are clearer compared to the first version of the plan.
FABRICATION AND PRODUCTION
For this project, my partner Shu and I did most of the work together. But we are mainly responsible for different parts. I’m mainly responsible for coding, and she’s mainly responsible for making the control box and decoration.
Before user testing, we finished writing the code for the two phases separately. We planned how to place four buttons and a sensor, then designed the size of the control box and the position of the cutouts to embed these buttons. After we finished the design on Cuttle, we used the laser cutting machine with Dalin’s help.
We chose black acrylic board to make the control box. Dalin taught us to use acrylic glue, which looked like a bottle of mysterious chemical reagent. This glue works by etching the acrylic and then re-fixing it after the glue has dried. It was the first time for Shu and I to use this type of glue and we didn’t coordinate well, which resulted in spending a lot of time making this box. With Shu’s roommate’s help, we finished gluing the box. Her roommate excelled in this technique. We first found the button samples in the studio and then bought the buttons online. Then, we soldered the wires and connected them to the breadboard. Luckily, the buttons perfectly fit the cutouts, for which we had gone through careful calculations.
During the user testing, we received a lot of useful feedback from our professor, LAs and folks. The good news was that the buttons worked very well. Many suggested that we should add some background music to offer players a sound stimulus. Different sound effects to tell the players if they win or lose. Also, we can add a sound of garbage collection when the astronaut successfully collects space trash. We adopted all the suggestions regarding sound and auditory stimuli. Our professor suggested that we could add some lines of code to reach the effect of acceleration, which made the game more fit into the space environment. Also, he suggested that we should add more instructions to make the game clearer to the players. What’s more, Anya suggested that we could stick to one game and add different grades of difficulty instead of making two different games because it would confuse players when converting to the second phase without more instructions. We took this suggestion into serious consideration, but finally we didn’t make changes accordingly since we had intended to show two big themes in the game rather than one.
After the user testing, we started to optimize our game based on the suggestions we had received. However, problems emerged one after another. The first big problem was that Processing failed to decode some of the sound files. One solution from our professor was to convert MP3 files to WAV files. However, this method didn’t work for our files. And our professor used mysterious software to check some file information and soon came to the conclusion that there was a frequency problem (I’m not sure 😣). Then he helped us fix the problem by changing parameters and generating a new sound file. Processing could successfully decode these sound files. Another big problem that I struggled with was putting the two games together in one sketch. The logic of transitioning to phase 2 and the true or false value of GameWon should be carefully set. The code itself was not difficult to write, but it’s tricky to put the code in the right place or the computer would run the sketch in an unexpected way. It’s a thinking challenge because I need to think about it from the big picture, and it’s no longer a local logic. My professor worked with me and patiently explained the logic for me to understand. After resolving these two obstacles, we turned to designing the control box. We designed a rocket and some stars on Cuttle. Then, we cut these shapes through laser cutting, painted them in different colors and stuck them to the box. Also, we added some instructions to the box. We drew arrows corresponding to the directions on the four buttons. We also wrote “Blow here” to let the players know where to blow. We also used the 3D printing machine to print two cute astronaut models and put them on the box for decoration.
CONCLUSIONS
Our goal for this project is by incorporating elements of environmental management and strategic decision-making, we aim to educate players on the consequences of their choices within a captivating gaming experience. From the perspective of the creators who designed the game, I think the two phases of our game clearly reflect our ideas and purpose. But we are not sure about the player’s point of view and whether they can understand what we really want to express. For players, they will pay more attention to the fun and experience of the game. The feedback we received from users centered on the fact that the second stage was more interesting and thrilling than the first stage and that the acceleration of the astronauts in the first stage was not always a good thing.
Such feedback tells us that the design and final output of the game were satisfactory and that the players were engaged. But we neglected to emphasize our theme of the cosmic environment and cosmic exploration. If we had more time to optimize, I’d like to focus on two areas: thematic emphasis and game presentation. These two ideas could be accomplished by writing a simple short story. A simple plot could be: the environmental problems in space are increasing; for various reasons, there is an increase in garbage in space, which threatens the normal working satellite systems. So we need to clean up the space junk and your (the player’s) first-level task is to collect 10 pieces of trash in space. And we can add an animation of the game demo so that the player understands how to use the control box. The same strategy can be applied to the second phase.
One important takeaway for me is about coding. During the coding process, I could think clearly about each part of the code. But when it came to putting them together, I had a tough time. There should be some adjustments in each part to fit into the bigger picture. For example, I learned to use a parameter to control the stage (let stage equal 1 at the very beginning and then move to stage 2 by saying that stage=2) instead of directly writing “void stage2_draw()” to run the code for stage 2. Also, I had a discussion with our professor to understand the logic for switching from phase I to phase II. It could be very confusing at first, but it’s the most interesting part and I felt a sense of achievement when I could understand it clearly.
DISASSEMBLY
For our project, we rented a sound sensor from the ER and had already returned it. Most of the other materials we used were from our kit. Here is the project disassembly:
APPENDIX
Pictures from: Adobe Firefly
Picture of the astronaut:
Picture of the rocket:
Background picture:
Music from: Pixabay
Pixabay. “Force-Field.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “Game-Start.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “Garbage Disposal.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “Negative_beeps.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “Sci-Fi-ambient-Drone.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “Sucess-1.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
Pixabay. “WinsSquare.” MP3 file. https://pixabay.com/sound-effects/search/game%20finish/
A picture of circuits:
Final documentation:
CODE-Arduino
int buttonLeftPin = 7; int buttonRightPin = 8; int buttonUpPin = 9; int buttonDownPin = 10; void setup() { Serial.begin(9600); pinMode(buttonLeftPin, INPUT); pinMode(buttonRightPin, INPUT); pinMode(buttonUpPin, INPUT); pinMode(buttonDownPin, INPUT); } void loop() { // 0 // 1: left arrow // 2: right arrow // 3: top arrow // 4: bottom arrow // check if the pushbutton is pressed. If it is, the buttonState is HIGH: if (digitalRead(buttonLeftPin) == HIGH) { Serial.print("1,"); } else if (digitalRead(buttonRightPin) == HIGH) { Serial.print("2,"); } else if (digitalRead(buttonUpPin) == HIGH) { Serial.print("3,"); } else if (digitalRead(buttonDownPin) == HIGH) { Serial.print("4,"); } else { // nothing is pressed Serial.print("0,"); } int sensor1 = analogRead(A1); Serial.println(sensor1); delay(10); }
CODE-Processing
import processing.serial.*; import processing.sound.*; Serial serialPort; SoundFile sound1; SoundFile sound2; SoundFile sound3; SoundFile sound4; SoundFile sound5; SoundFile sound6; SoundFile sound7; SoundFile sound8; int stage=1; PImage backgroundImage1; PImage backgroundImage2; PImage satelliteImage; PImage astronautImage; PImage rocketImage; Rocket rocket; ArrayList<Meteor> meteors = new ArrayList<>(); // ArrayList to store meteor objects ArrayList<SpaceTrash> spaceTrashList = new ArrayList<SpaceTrash>(); // List to store space trash float astronautX, astronautY; float astronautSpeedX, astronautSpeedY; float[] satelliteX = new float[2]; float[] satelliteY = new float[2]; float[] satelliteSpeedX = new float[2]; float[] satelliteSpeedY = new float[2]; int score = 0; boolean gameOver = false; boolean gamelose = false; boolean gameWon = false; boolean gameStarted = false; long startTime; long countdownDuration = 25000; int NUM_OF_VALUES_FROM_ARDUINO = 2; /* CHANGE THIS ACCORDING TO YOUR PROJECT */ int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO]; void setup() { fullScreen(); printArray(Serial.list()); serialPort = new Serial(this, "/dev/cu.usbmodem101", 9600); println("Loading images..."); backgroundImage1 = loadImage("background1.jpg"); backgroundImage1.resize(width, height); backgroundImage2 = loadImage("background2.png"); backgroundImage2.resize(40, 40); satelliteImage = loadImage("satellite.png"); satelliteImage.resize(30, 30); rocketImage = loadImage("rocket.jpg"); rocket = new Rocket(width / 2, 50, rocketImage); astronautImage= loadImage("astronaut.jpg"); astronautImage.resize(30, 30); astronautX = width / 2; astronautY = height - 20; for (int i = 0; i < 2; i++) { satelliteX[i] = random(width); satelliteY[i] = random(height); satelliteSpeedX[i] = random(0.5, 1); satelliteSpeedY[i] = random(0.5, 1); } println("Loading soundfiles..."); sound1 = new SoundFile(this, "gamestarttest.wav"); sound2 = new SoundFile(this, "music1test.wav"); sound3 = new SoundFile(this, "music2test.wav"); sound4 = new SoundFile(this, "passphase.wav"); sound5 = new SoundFile(this, "gamelose.wav"); sound6= new SoundFile(this, "garbagedisposal.wav"); sound7= new SoundFile(this, "winboth.wav"); sound8= new SoundFile(this, "warning.wav"); println("Done loading soundfiles..."); } void stage1_draw() { if (!gameOver) { if (sound2.isPlaying() == false) { sound2.play(); } if (arduino_values[0] == 3) { astronautSpeedY -= 0.1; } else if (arduino_values[0] == 4) { astronautSpeedY += 0.1; } else if (arduino_values[0] == 1) { astronautSpeedX -= 0.1; } else if (arduino_values[0] == 2) { astronautSpeedX += 0.1; } else { // slow down if no button is pressed astronautSpeedX = astronautSpeedX * 0.99; astronautSpeedY = astronautSpeedY * 0.99; } // make sure the speed doesn't increase too much astronautSpeedX = constrain(astronautSpeedX, -3, 3); astronautSpeedY = constrain(astronautSpeedY, -3, 3); // update position with speed astronautX = astronautX + astronautSpeedX; astronautY = astronautY + astronautSpeedY; // make sure the astronaut doesn't leave the screen astronautX = constrain(astronautX, 0, width - 30); astronautY = constrain(astronautY, 0, height - 30); } if (!gameStarted) { background(0); fill(255); textAlign(CENTER, CENTER); textSize(32); text("Exploring the cosmos", width / 2, height / 2 - 50); textSize(16); text("Click to Start", width / 2, height / 2 + 50); } else { background(backgroundImage1); image(astronautImage, astronautX, astronautY, 40, 40); if (sound2.isPlaying() == false) { sound2.play(); } for (int i = 0; i < 2; i++) { image(satelliteImage, satelliteX[i] - 15, satelliteY[i] - 15, 30, 30); // Update satellite position satelliteX[i] += satelliteSpeedX[i]; satelliteY[i] += satelliteSpeedY[i]; if (satelliteX[i] < 0 || satelliteX[i] > width) { satelliteSpeedX[i] *= -1; } if (satelliteY[i] < 0 || satelliteY[i] > height) { satelliteSpeedY[i] *= -1; } } // Draw space trash fill(0); if (random(1) < 0.01 && spaceTrashList.size() < 10) { // Randomly spawn space trash with a random direction and low speed float trashX = random(width); float trashY = random(height); float trashSpeedX = random(-0.5, 0.5); float trashSpeedY = random(-0.5, 0.5); spaceTrashList.add(new SpaceTrash(trashX, trashY, trashSpeedX, trashSpeedY)); } if (spaceTrashList.size() > 20) { gameOver = true; if (sound5.isPlaying() == false) { sound5.play(); } } // Update and draw space trash for (int i = spaceTrashList.size() - 1; i >= 0; i--) { SpaceTrash trash = spaceTrashList.get(i); fill(0); rect(trash.x, trash.y, 20, 20); // Update space trash position trash.x += trash.speedX; trash.y += trash.speedY; // Check for collision with space trash if (dist(astronautX, astronautY, trash.x, trash.y) < 30) { // Astronaut caught space trash, increase the score and remove space trash spaceTrashList.remove(i); score++; if (sound6.isPlaying() == false) { sound6.play(); } } for (int j = 0; j < 2; j++) { if (dist(satelliteX[j], satelliteY[j], trash.x, trash.y) < 15) { // Satellite collided with space trash, remove the satellite satelliteX[j] = random(width); satelliteY[j] = random(height); // Spawn three new pieces of space trash at the location of the collision for (int k = 0; k < 3; k++) { float newTrashX = trash.x; float newTrashY = trash.y; float newTrashSpeedX = random(-0.5, 0.5); float newTrashSpeedY = random(-0.5, 0.5); spaceTrashList.add(new SpaceTrash(newTrashX, newTrashY, newTrashSpeedX, newTrashSpeedY)); } // Remove the collided space trash spaceTrashList.remove(i); } } // Remove space trash if it goes off-screen if (trash.x < 0 || trash.x > width || trash.y < 0 || trash.y > height) { spaceTrashList.remove(i); } } textSize(16); fill(255); text("Score: " + score, 40, 20); fill(255, 255, 0); text("Collect space trash!", 80, 40); fill(255, 255, 0); text("Collect 10 space trash to win!", 135, 60); // Check for collision with satellites and end the game if it's over for (int j = 0; j < 2; j++) { float distance = dist(satelliteX[j], satelliteY[j], astronautX, astronautY); if (distance < 15) { gameOver = true; if (sound5.isPlaying() == false) { sound5.play(); } } } if (gameOver) { background(0); fill(255, 0, 0); textSize(32); text("Game Over!Click to continue", width/2, height/2); if (sound5.isPlaying() == false) { sound5.play(); } } if (score > 9) { fill(0, 255, 0); textSize(32); text("You Win!", width/2 - 100, height/2); if (gameWon == false) { gameWon = true; if (sound2.isPlaying()) { sound2.stop(); } sound4.play(); } else { if (sound4.isPlaying() == false) { stage = 2; gameWon = false; startTime = millis(); } } } } } void stage2_draw() { if (gameStarted && !gameOver) { background(0); if (sound2.isPlaying()) { sound2.stop(); } if (sound5.isPlaying()) { sound5.stop(); } if (sound3.isPlaying() == false) { sound3.play(); } // Game logic goes here. Update and display the rocket rocket.update(); rocket.display(); float rocketSpeed = map(arduino_values[1], 0, 1023, 0.2, 3); if (rocketSpeed > 0) { rocket.moveUp(rocketSpeed); } // Check if the rocket touches the bottom boundary if (rocket.getY() >= height - 60) { println("Rocket Y: " + rocket.getY()); gameOver=true; if (sound8.isPlaying() == false) { sound8.play(); } } // Check if the countdown is over if (millis() - startTime >= countdownDuration) { gameWon = true; gameOver = true; } if (!gameOver) { if (arduino_values[0] == 1) { rocket.move(-2); } else if (arduino_values[0] == 2) { rocket.move(2); } } stroke(255, 0, 0); line(0, height-20, width, height-20); fill(255); textSize(16); textAlign(LEFT, TOP); long remainingTime = (long) (max(0, countdownDuration - (millis() - startTime))); float secondsRemaining = remainingTime / 1000.0; text("Time: " + nf(secondsRemaining, 0, 1) + "s", 10, 10); textSize(14); text("Keep the astronaut and rocket safe!", 10, 30); // Update and display meteors updateMeteors(); displayMeteors(); } else { // Game over or beginning page background(200); fill(0); textSize(24); textAlign(CENTER, CENTER); if (gameOver) { if (gameWon == true) { if (sound3.isPlaying()) { sound3.stop(); } if (sound7.isPlaying() == false) { sound7.play(); } background(255); image(backgroundImage2, 400, 400); textAlign(CENTER, CENTER); textSize(18); text("You win!Congrats to finish the game! ", 400, 250); noLoop(); } else { text("You lose! Click to restart", width / 2, height / 2); if (sound5.isPlaying() == false) { sound5.play(); } } } } } void draw() { getSerialData(); if (gameStarted) { //stage =1; } if (stage == 1) { stage1_draw(); } else if (stage == 2) { stage2_draw(); } } void keyPressed() { if (!gameOver) { if (keyCode == UP) { astronautY -= 5; } else if (keyCode == DOWN) { astronautY += 5; } else if (keyCode == LEFT) { astronautX -= 5; } else if (keyCode == RIGHT) { astronautX += 5; } astronautX = constrain(astronautX, 0, width - 30); astronautY = constrain(astronautY, 0, height - 30); } } void mousePressed() { if (!gameStarted || gameOver) { gameStarted = true; gameOver = false; gameWon = false; startTime = millis(); // Record the start time rocket.reset(); // Reset the rocket position meteors.clear(); // Clear the list of meteors } } class SpaceTrash { float x, y; float speedX, speedY; SpaceTrash(float x, float y, float speedX, float speedY) { this.x = x; this.y = y; this.speedX = speedX; this.speedY = speedY; } } void updateMeteors() { // Generate a meteor randomly at intervals if (random(1) < 0.01) { // Adjust the probability based on your preference Meteor meteor = new Meteor(); meteors.add(meteor); } // Update and check for collisions with the rocket for (int i = meteors.size() - 1; i >= 0; i--) { Meteor meteor = meteors.get(i); meteor.update(); meteor.checkCollision(rocket); if (meteor.isOffScreen()) { meteors.remove(i); } } } // Display meteors void displayMeteors() { for (Meteor meteor : meteors) { meteor.display(); } } // Meteor class class Meteor { float x, y; float speedX, speedY; float diameter = 20; // Fixed size for the meteor color meteorColor = color(255, 255, 0); // Yellow color for the meteor Meteor() { x = random(width); y = random(-50, -10); // Start above the canvas speedX = random(-2, 2); // Random horizontal speed speedY = random(3, 5); // Random vertical speed } void update() { x += speedX; y += speedY; } void display() { fill(meteorColor); noStroke(); ellipse(x, y, diameter, diameter); } void checkCollision(Rocket rocket) { float distance = dist(x, y, rocket.x, rocket.y); if (distance < diameter / 2 + 20) { // 20 is half of the rocket's width gameOver = true; } } boolean isOffScreen() { return y > height; } } // Rocket class class Rocket { float x, y; float speedY = 1; // Constant falling speed PImage img; // PImage variable for the rocket image Rocket(float x, float y, PImage img) { this.x = x; this.y = y; this.img = img; // Assign the provided image to the PImage variable } void update() { // Update rocket position y += speedY; // Keep the rocket within the screen boundaries y = constrain(y, 0, height - 50); x = constrain(x, 10, width - 10); } void moveUp(float speed) { // Move the rocket upward based on the provided speed y -= speed; } void display() { imageMode(CENTER); image(img, x, y, 60, 80); // Adjust the size accordingly } float getY() { return y; } void move(float xDirection) { // Move the rocket horizontally x += xDirection; // Keep the rocket within the screen boundaries x = constrain(x, 10, width - 10); } void reset() { // Reset the rocket position x = width / 2; y = 250; } } void getSerialData() { while (serialPort.available() > 0) { String in = serialPort.readStringUntil( 10 ); // 10 = '\n' Linefeed in ASCII if (in != null) { print("From Arduino: " + in); String[] serialInArray = split(trim(in), ","); if (serialInArray.length == NUM_OF_VALUES_FROM_ARDUINO) { for (int i=0; i<serialInArray.length; i++) { arduino_values[i] = int(serialInArray[i]); } } } } }
ACKNOWLEDGEMENT
Credit and special thanks to:
Professor Gottfried Haider, LA Rachel Li, LA Amelia Shao, LA Anya, and my partner Shu.