Qilin – Stress Relieve – Denny Wang
Instructor: Gottfried Haider
Final Outcome Video:
Outcome Video
Screen Record
Concept and Design:
i. Concepts
Our proposal for “Qilin – Stress Relieve” is influenced by interactive artworks like “The Treachery of Sanctuary” and “Connected Worlds” in the preparatory research. We was trying to build interaction between the virtual world(which could be interpreted as projection, screen, etc.)and the users’ body motion. After few discussions on capability and utility with Professor Gottfried, we chose to create a unique interactive video game by incorporating sensors and Arduino. By using bodily movements instead of traditional controls, we seek to provide an innovative gaming experience that fosters a deeper connection between the audience and the artwork.
In the game, two players control objects using bodily movements, replacing keyboard and mouse controls. Their actions trigger responses in the game. The objective is to protect Qilin from excessive homework. Players collect falling scissors to cut the homework, with each scissor being able to cut one homework. They switch to SCISSOR MODE by holding hands together, enabling them to cut homework using scissors. When the scissors run out, players must switch back to HAND MODE by releasing their hands to collect more scissors. The use of multiple sensors changes the traditional mode of playing video games and incorporates bodily motion.
ii. Interface Design:
The game has four main interfaces. The title page provides a brief description and a start button. Clicking the start button leads to the instruction page, which explains the game tasks and how to use the provided “controllers.” Once users feel confident, they can click “Got it” to enter the main game interface. In the main interface, players collect scissors and relive Qilin’s stress by cutting homework. Eventually after the limited time, there is an ending page to tell users if they have succeed or not and there is also a “retry” button for user to click to retry.
Fabrication and Production:
i. Building Process
To create the poles, we stacked two cardboard poles to achieve the desired height, allowing the middle part to move up and down freely. We used a laser cutter for the moving part of the pole. To adjust the detection range of the IR distance sensor, we added a piece of wood. The gloves were made by attaching Velcro to two cardboard circles with copper tape(which is inspired by our first recitation in Interaction Lab this semester), and two wires were soldered onto each glove for triggering the transition between scissors and hands. This mechanism provided instant feedback and reaction compared to using a sensor.
ii. Coding
My partner Tina handled the major coding part for Processing, while I focused on Arduino to retrieve sensor values. Tina created the necessary classes and functions for the game, including Homework, Qilin, User, and Scissor classes. My code utilized Serial communication from Arduino to Processing, as we used in class before.
iii. Graphic Design
Since our game is about stress relieve, so we want to create a interface that is cure, relaxing and user-friendly. We decided to draw the graphic first and use PImage function in Processing to plug in the pictures. I drew all the graphics by using Procreate and exported as .jpg for Tina to plug into the game.
iiii.Feedback from User Testing:
User Testing Video
During the user testing phase, we received valuable feedback that greatly contributed to optimizing the user experience of our game. One notable observation was that despite providing instructions on the instruction page about which pole controlled the X-axis and which controlled the Y-axis, users found it challenging to remember this information during later stages of the game. To address this, we made a modification by adding clear X-Y axis labels directly onto the moving part of the pole, ensuring a more intuitive understanding for players.
Additionally, we encountered an issue where the same icon was used for both collecting scissors and entering SCISSOR MODE. This caused difficulty in locating the cursor amidst a flurry of falling scissors. To tackle this, we implemented a lighting circle effect around the cursor specifically in SCISSOR MODE. This enhancement significantly improved the visibility and differentiation of the cursor from the scissors, creating a more seamless gameplay experience.
Conclusion:
“Qilin – Stress Relieve” is a representation of our effort to develop an interesting and cutting-edge interactive gaming experience. We have converted conventional controls into physical movements using sensors and Arduino, creating a stronger bond between players and the artwork. We created stacked cardboard poles, using laser cutting methods, and altered the IR distance sensor during the production process to improve gameplay. Our project explores the potency of fusing technology, creativity, and user engagement to produce a thrilling and immersive gaming experience. “Qilin – Stress Relieve” offers a refreshing and captivating method to stress release and enjoyment, opening the door for our further investigation in the field of interactive artwork and games.
Appendix
i. TinkerCad Circuits
ii. Works Cited
Benne. “IR Distance Sensor Arduino Tutorial (Sharp GP2Y0A21YK0F).” Makerguides.Com, 2 Mar. 2022, www.makerguides.com/sharp- gp2y0a21yk0f-ir-distance-sensor-arduino-tutorial/.
“Connected Worlds – Interactive Installation.” Design I/O, Design I/O and the New York Hall of Science, 17 July 2015, https://www.design-io.com/projects/connectedworlds.
Milk, Chris. “The Treachery of Sanctuary.” Chris Milk, 16 July 2014, http://milk.co/treachery.
iii. Code
Processing⬇️:
import processing.sound.*; //RECEVING SERIAL INPUT import processing.serial.*; Serial serialPort; //NUM OF ELEMENTS IN ARRAY int NUM_OF_VALUES_FROM_ARDUINO = 3; //ARRAY int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO]; //ARRAYLIST of OBJECTS ArrayList<Homework> works = new ArrayList<Homework>(); ArrayList<Scissor> scis = new ArrayList<Scissor>(); ArrayList<Stress> stress = new ArrayList<Stress>(); //IMAGES PImage bg, start, title, qilin, scs, cry, descript; PImage instruct, gotit, yay, oh, restart, h; //PAGES NUMBER int page = 0; int maxStress = 100; //EVERYTHING NEEDED FOR ONE GAME float startTime; boolean startRecorded = false; int rt1 = 1000; int rt2 = 1500; int stressLevel = 0; float stressVar = 0.8; int scissNum = 0; int counter = 0; //PLAYER OBJECT Player player; //TIME OF GAME int totalTime = 1*60*1000; //int totalTime = 2000; int timeLeft; SoundFile victory, bgm, lose; boolean hasWon = false; boolean lost = false; boolean isHand = true; int xValue; int yValue; /** Every homework is 4% stress**/ void setup() { size(1200, 700); bg = loadImage("images/b.png"); start = loadImage("images/start.png"); restart = loadImage("images/restart.png"); title = loadImage("images/title.png"); qilin = loadImage("images/qilin.png"); scs = loadImage("images/sci.png"); cry = loadImage("images/cry.png"); descript = loadImage("images/descript.png"); gotit = loadImage("images/gotit.png"); instruct = loadImage("images/instruct.png"); yay = loadImage("images/yay.png"); oh = loadImage("images/oh.png"); h = loadImage("images/hands.png"); player = new Player(); victory = new SoundFile(this, "music/victory.mp3"); bgm = new SoundFile(this, "music/bgm.mp3"); lose = new SoundFile(this, "music/lose.mp3"); bgm.loop(); printArray(Serial.list()); serialPort = new Serial(this, "/dev/cu.usbmodem2", 9600); } void draw() { image(bg, 0, 0); getSerialData(); yValue = 7 * int(arduino_values[1]); //yValue = 100*int(arduino_values[1]); xValue = 20 * int(arduino_values[2]); //xValue = mouseX; //DETECTION OF HAND CLAP ////////////////////////////////////////////////////////////////////////// if (arduino_values[0] == 1){ isHand = true; } else{ isHand = false; } if(isHand){ player.ctScissor(); } else{ player.ctHand(); } //START PAGE ////////////////////////////////////////////////////////////////////////// if (page == 0) { firstPage(); } //INSTRUCTIONS PAGE ////////////////////////////////////////////////////////////////////////// else if (page == 1){ instructPage(); if(isHand){ image(scs, width*0.62, height*0.93, 30,30); } else{ image(h, width*0.62, height*0.93, 30,30); } } // PLAYING PAGE ////////////////////////////////////////////////////////////////////////// else if (page == 2) { image(qilin, width*0.4, height*0.6); image(scs, width*0.62, height*0.93, 30,30); fill(0); textSize(50); String sLevel = "Stresslevel: " + stressLevel; String scissN = "Scissors collected: " + scissNum; text(sLevel, width*0.03, height*0.97); text(scissN, width*0.65, height*0.97); player.display(xValue, yValue); if (millis() - startTime > rt1){ randomSeed(millis()); scis.add(new Scissor(random(width*0.1, width*0.9), 0)); rt1 += random(3000, 5000); } if (millis() - startTime > rt2){ works.add(new Homework(random(width*0.1, width*0.9), 0)); rt2 += random(3000, 5000); } if(player.getIsHand()){ checkCollisionHand(); } else{ checkCollisionScis(); } checkHomework(); checkScissor(); drawStressLevel(); drawHomework(); drawScissor(); timeLeft(); if (millis() - startTime > totalTime) { page = 3; } } //END PAGE ////////////////////////////////////////////////////////////////////////// else if (page == 3) { if(stressLevel <= maxStress){ hasWon = true; image(yay, width*0.23, height*0.1); image(qilin, width*0.37, height*0.6); } else{ lost = true; image(oh, width*0.23, height*0.1); image(cry, width*0.37, height*0.6); } image(restart, width*0.75, height-150, 300, 150); if(hasWon && victory.isPlaying() == false && counter == 0){ victory.play(); hasWon = false; counter ++; } else if(lost && lose.isPlaying() == false && counter == 0){ lose.play(); lost = false; counter ++; } } } //MOUSECLICKED ////////////////////////////////////////////////////////////////////////// void mouseClicked() { if (page == 0) { if (mouseX > width/2-150 && mouseX < width/2+150) { if (mouseY > height-150 && mouseY < height) { page = 1; } } } else if (page == 1) { if (mouseX > width/2-150 && mouseX < width/2+150) { if (mouseY > height-150 && mouseY < height) { page = 2; startTime = millis(); startRecorded = true; } } } else if (page == 3) { if (mouseX > width*0.75 && mouseX < width*0.75 + 300) { if (mouseY > height-150 && mouseY < height) { reset(); } } } } //KEYPRESSED ////////////////////////////////////////////////////////////////////////// void keyPressed(){ if (key == 's' || key == 's'){ if(isHand){ player.ctScissor(); isHand = false; } else{ player.ctHand(); isHand = true; } } } //DRAWS THE OBJECTS ////////////////////////////////////////////////////////////////////////// void drawScissor() { for (Scissor s : scis) { s.move(); s.display(); } } void drawHomework() { for (Homework w : works) { w.move(); w.display(); } } void drawStressLevel() { for (Stress st : stress) { st.display(); } } //UPDATE ARRAYLISTS AND STRESSLEVEL ////////////////////////////////////////////////////////////////////////// void checkHomework(){ for (int i=0; i < works.size(); i++){ if(works.get(i).getY() >= 450){ works.remove(i); stressLevel += 3; stress.add(new Stress(int(width*0.03), int(height*stressVar))); stressVar -= 0.03; } } } void checkScissor(){ for (int i=0; i < scis.size(); i++){ if(scis.get(i).getY() >= 450){ scis.remove(i); } } } //PAGE LOADING ////////////////////////////////////////////////////////////////////////// void firstPage() { image(title, 120, -70, 900, 600); image(descript, width/2-200, height/2+135, 400, 75); image(start, width/2-150, height-150, 300, 150); } void instructPage(){ image(instruct, 0,0); image(gotit, width/2-150, height-150, 300, 150); } //CHECK FOR COLLISION OF PLAYER WITH OBJECTS ////////////////////////////////////////////////////////////////////////// void checkCollisionHand() { for (int j = 0; j < scis.size(); j++) { float d = dist(player.getX(), player.getY(), scis.get(j).getX(), scis.get(j).getY()); if (d < 100){ scissNum +=1; scis.remove(j); } } } void checkCollisionScis() { if(scissNum > 0){ for (int i = 0; i < works.size(); i++) { float d = dist(player.getX(), player.getY(), works.get(i).getX(), works.get(i).getY()); if (d < 100){ scissNum -=1; works.remove(i); } } } } //RESET GAME ////////////////////////////////////////////////////////////////////////// void reset(){ page = 0; startRecorded = false; rt1 = 1000; rt2 = 1500; stressLevel = 0; stressVar = 0.8; scissNum = 0; counter = 0; for(int a = 0; a < works.size(); a++){ works.remove(a); } for(int b = 0; b < scis.size(); b++){ scis.remove(b); } for(int c = 0; c < stress.size(); c++){ stress.remove(c); } } //PRINTS TIME LEFT ON SCREEN ////////////////////////////////////////////////////////////////////////// void timeLeft(){ timeLeft = int(startTime) - millis() + totalTime + 1000; String l = "Time left (sec): " + timeLeft/1000; textSize(20); text(l, 5, 20); } 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]); } } } } }
Arduino⬇️:
#include <SharpIR.h> //model and input pin int clapPin = 2; int clap; int start; long delayTime = 5000; #define IRPin A0 //SHARP IR x #define IRPin2 A1 //SHARP IR y #define model 1080 int dist_cm; int dist_cm2; SharpIR mySensor = SharpIR(IRPin, model); SharpIR mySensor2 = SharpIR(IRPin2, model); void setup() { Serial.begin(9600); pinMode(clapPin, INPUT); } void loop() { long start = millis(); while(millis() - start < delayTime){ clap = digitalRead(clapPin); dist_cm = mySensor.distance(); dist_cm2 = mySensor2.distance(); Serial.print(clap); Serial.print(","); Serial.print(dist_cm); Serial.print(","); Serial.print(dist_cm2); Serial.println(); } }