A Dandelion’s Adventure
Livvy(Weiyi Lu) & Leo (Zenan Han) – Instructor Inmi
CONCEPTION AND DESIGN:
Our project, “A Dandelion’s Adventure”, is an entertaining game for a broad audience to have fun and relax. The game rule is inspired by the mobile game “Flappy Bird” developed by the Vietnamese video game artist and programmer Dong Nguyen. In our project, a player will control a flowing dandelion with their hand movement and blowing.
In this semester, we perceived that our peers are feeling stressed so we are curious about the stress level for people nowadays. According to a survey conducted in China by Rakuten Insight in May 2022, almost six in ten respondents claimed that they had a higher level of stress or anxiety in the past 12 months. Thus, based on this fact, we aim to create a space for people to totally entertain and isolate themselves from the huge stress in their study or work lives. And from our research, we put our eyes on a popular game once before – “Flappy Bird”. Our purpose for our project is just to let the audience immerse themselves in the attractive and poetic game and isolate themselves from the stress outside the world.
With regard to our game’s mechanism, we designed the dandelion as the player and a bird as the chaser behind the dandelion. So the game ends either the dandelion touches the obstacle or it gets touched by the chaser. In addition, since the game is shown on the screen through Processing, the way players communicate with the game character is not through the screen or keyboard. Instead, we used the physical input of Arduino to control the character’s movement.
We expected the user to move the hand up and down to control the height of the dandelion to avoid obstacles – vines, as the ultrasonic sensor can sense the position of the player’s hand. Also, the user can blow to the blow sensor to speed up the dandelion in case eaten by the bird.
From the perspective of aesthetics, in Processing, we design the obstacles as vines, the player as a dandelion, and the chaser as a bird by collaging images on Canva. The whole vibe of the game is warm-style and poetic. For the physical appearance, we used laser cutting to build a stage to place the sensors and decorate the stage with artificial green plants, which echo the images in Processing.
During User Test, for the interaction part, we didn’t give sufficient and intuitive instructions so the audience didn’t know that they should move their hands above the ultrasonic sensor. For the technical part, our audience found that the ultrasonic sensor was unstable and didn’t work quite well, which means the dandelion moved unnaturally in the vertical direction. For the aesthetic part, both the digital setting and physical setting was simple and still in process, reducing the attractiveness of our project.
To solve these problems, first, we both designed the instructions on screen before the game start and used laser cutting to engrave instructions on the planks. And that worked during the presentation. In addition, we finally solved the technical problem of the instability ultrasonic sensor, which will be further discussed in the next FABRICATION AND PRODUCTION part. Furthermore, we employed a natural forest theme to display digitally and physically to enhance aesthetics.
FABRICATION AND PRODUCTION:
Our project began with coding the basic game rules in Processing, where I initially used mouseY to control the dandelion’s vertical movement. I used OOP I learned from cclab to made the code more clear and easy to modify. Here is the video and the code.
Processing
ArrayList<Obstacle> obstacles; Player player; Chaser chaser; int score = 0; void setup() { fullScreen(); obstacles = new ArrayList<Obstacle>(); player = new Player(); chaser = new Chaser(); } void draw() { background(255, 255, 255); totalObstacles(); player.update(); player.display(); chaser.update(); chaser.display(); checkCollisions(); displayScore(); } void totalObstacles() { if (frameCount % 150 == 0) { obstacles.add(new Obstacle());//add a new obstacle at regular intervals } for (int i = obstacles.size() - 1; i >= 0; i--) { Obstacle obs = obstacles.get(i); obs.update(); obs.display(); if (obs.x< -width) { obstacles.remove(i);//remove the obstacle if it's off screen } else if (obs.isPassedBy(player)) { score++; } } } void checkCollisions() { if (chaser.collidesWith(player)) { endGame("Chaser caught the player"); } for (Obstacle obs : obstacles) { if (obs.collidesWith(player)) { endGame("Collision with obstacle"); } } } void endGame(String message) { fill(255, 0, 0); rect(0, 0, width, height); fill(255); textSize(32); textAlign(CENTER, CENTER); text(message, width/2, height/2); noLoop(); } void displayScore(){ fill(0); textSize(16); text("Score: " + score, width - 100, 20); }
class Chaser{ float x=0; float y=height/2; float s=100; Chaser(){ this.x=x; this.y=y; this.s=s; } void update(){ x+=0.1; } void display(){ fill(255,0,0); circle(x,y,s); } boolean collidesWith(Player player) { return dist(x,y,player.x,player.y) < (s+player.s)/2; } }
class Obstacle { float x = width;//original position float d = 50;//width float h = 150;//the height of the gap float gapBottom = random(h, height-h);//the bottom position of the gap float gapTop= gapBottom-h; boolean passed = false; Obstacle() { this.x = x; this.d = d; this.h = h; this.gapBottom = gapBottom; this.gapTop = gapTop; } void update() { x-=4; } void display() { fill(0); rect(x, 0, d, gapTop);//up rect(x, gapBottom, d, height-gapBottom);//down } boolean collidesWith(Player player) { return (player.x + player.s/2 > x && player.x - player.s/2 < x+d) && (player.y - player.s/2 < gapTop || player.y + player.s/2 > gapBottom); //The player's right boundary exceeds the left boundary of the obstacle //and the player's left boundary does not exceed the right boundary of the obstacle } boolean isPassedBy(Player player) {//add score if (!passed && player.x > x + d) { passed = true; return true; } return false; } }
class Player { float x=300; float y=height/2; float s=50; Player() { this.x = x; this.y = y; this.s = s; } void update() { y = constrain(mouseY, s/2, height - s/2); } void display() { fill(0); circle(x, y, s); } }
Initially, we planned to use an external microphone to control the dandelion’s vertical movement by voice volume, which means the louder the voice volume, the higher the dandelion’s fly. However, after we discussed it with Professor Inmi, we gave it up due to redundancy and lack of novelty. After exploring different analog inputs, we chose the ultrasonic sensor for a more attractive interaction by moving hands up and down in the air. Unfortunately, after we sent the data from Arduino to Processing, we found that the ultrasonic sensor was unstable, even though we had used the filter provided by the slides. We asked fellow Shengli for help and knew that there wasn’t a perfect way to solve the problem so we had to turn to work on other parts. Here is the video and the code.
Processing
void update() { float ny=map(arduino_values[0], 40, 0, 0, height); y = constrain(ny, s/2, height - s/2); y=ny; println("y: ", arduino_values[0]); }
Arduino
int triggerPin = 9; int echoPin = 10; long distance; float smoothing = 0.05; float smoothed; void setup() { Serial.begin(9600); pinMode(triggerPin, OUTPUT); pinMode(echoPin, INPUT); } void loop() { digitalWrite(triggerPin, LOW); delayMicroseconds(2); digitalWrite(triggerPin, HIGH); delayMicroseconds(10); digitalWrite(triggerPin, LOW); long duration = pulseIn(echoPin, HIGH, 17400); distance = duration / 29 / 2; smoothed = smoothed * (1.0 - smoothing) + distance * smoothing; //delay(100); Serial.println(smoothed); }
Then we incorporated a flow sensor connected to our circuit and sent data from Arduino to Processing. We chose it for its ability to mimic the common action of blowing a dandelion, adding a natural interactive element to the game. Here is the video and the code.
Processing
float xspeed=map(arduino_values[1], 0, 30, 0, 1); x=x+xspeed;
Arduino
int triggerPin = 9; int echoPin = 10; long distance; float smoothing = 0.05; float smoothed; double flow; //Water flow L/Min int flowsensor = 2; unsigned long currentTime; unsigned long lastTime; unsigned long pulse_freq; void pulse () // Interrupt function { pulse_freq++; } void setup() { Serial.begin(9600); pinMode(triggerPin, OUTPUT); pinMode(echoPin, INPUT); pinMode(flowsensor, INPUT); Serial.begin(9600); attachInterrupt(0, pulse, RISING); // Setup Interrupt currentTime = millis(); lastTime = currentTime; } void loop() { digitalWrite(triggerPin, LOW); delayMicroseconds(2); digitalWrite(triggerPin, HIGH); delayMicroseconds(10); digitalWrite(triggerPin, LOW); long duration = pulseIn(echoPin, HIGH, 17400); distance = duration / 29 / 2; smoothed = smoothed * (1.0 - smoothing) + distance * smoothing; //Serial.println(smoothed); //delay(100); currentTime = millis(); // Every second, calculate and print L/Min if(currentTime >= (lastTime + 100))//can be changed { lastTime = currentTime; // Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min. flow = (pulse_freq / 7.5); pulse_freq = 0; // Reset Counter Serial.print(smoothed); Serial.print(","); Serial.print(round(flow*10)); Serial.println(); //Serial.println(" L/Min"); } }
Before User Test, our sketch on Processing appeared too cartoony, so we switched to real images for a better aesthetic, as suggested by our professor. Here is the video.
During the User Test, the issues were discussed in the previous part CONCEPTION AND DESIGN, which set the goals for our next steps – overcoming the technical issue of the ultrasonic sensor, making the instructions more intuitive, and improving aesthetics.
So all it came back to the tough problem of the instability of the ultrasonic sensor. We turned to Professor Rudy for help. He assisted us in modifying the Arduino code and using the New Pin Library. However, the issues with erratic sensor readings still exist. And we found a significant problem although the values have become flatter, there are some positions where values occasionally drop to zero unexpectedly. This is shown by the fact that from near to far, the values change to 7, 8, 9, 10, 11, 0, 12, 13… In addition, there is also a zero at a distance of about 30. That’s why the dandelion sometimes flashed to some strange position. Since the problem existed, Professor Rudy advised us to try other ultrasonic sensors to see if the problem only happened on this single one. Unfortunately, all ultrasonic sensors have this problem. Here is the video after modifying the code which made it kind or more stable than before.
Arduino
#include /** * Is possible set the timeout in the constructor, like: * Ultrasonic ultrasonic(12, 13, 20000UL); */ Ultrasonic ultrasonic(13, 12); double flow; //Water flow L/Min int flowsensor = 2; unsigned long currentTime; unsigned long lastTime; unsigned long pulse_freq; void pulse () // Interrupt function { pulse_freq++; } void setup() { Serial.begin(115200); ultrasonic.setTimeout(40000UL); pinMode(flowsensor, INPUT); attachInterrupt(0, pulse, RISING); // Setup Interrupt currentTime = millis(); lastTime = currentTime; } void loop() { currentTime = millis(); // Every second, calculate and print L/Min if(currentTime >= (lastTime + 100))//can be changed { lastTime = currentTime; // Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min. flow = (pulse_freq / 7.5); pulse_freq = 0; // Reset Counter Serial.print(ultrasonic.read()); //delay(100); Serial.print(","); Serial.print(round(flow*10), DEC); Serial.println(); //Serial.println(" L/Min"); } }
To finish our project, we had to move on to the instructions and aesthetic part next. (I was mainly for the poster designed on Canva, and my partner was mainly for designing plank stage on Cuttle. All other parts we worked together)
We first worked on the digital part. We set several cases on Processing to realize the transition effect: from the introduction page to the instruction page and then to the game, finally leading to two endings, and the audience can turn to the introduction page again. Here is the code.
Processing
switch (gameState) { case "INTRODUCTION": drawIntroduction(); break; case "INSTRUCTION": drawInstruction(); break; case "GAME": runGame(); break; case "END": endGame(); break; }
Then I designed the posters for each case on Canva, and replaced the dandelion, the bird, and the vines with more suitable pictures to fit in with the whole theme.
When I inserted the picture of the gaming background to the game, we surprisingly found that the ultrasonic sensor suddenly became stable and there was no flashing. Along with that the game becomes slightly laggy, as that image has to be loaded every time in draw loop, and a little bit of lag doesn’t affect the presentation at all. I presumed that the lag caused the ultrasonic sensor to stabilize instead.
Since the visual effects were not enough, we added sound effects using free resources from Google, incorporating a piece of serene and beautiful background music to the whole project. We also added additional sound effects for each of the two different endings: The ending where the dandelion hits the vines has the sound of the wind blowing through the leaves, while the ending where the dandelion gets eaten by a bird has the sound of birds chirping. In addition, if the sensor value reaches a certain level, not only the dandelion speed up, but also there is a wind sound to tell the audience that their blowing moves the dandelion forward. Here are the videos.
Apart from the digital decoration, for the physical setup, we designed the stage on Cuttle and used laser cutting for construction. We initially planned to put the flow sensor straight on the table. Professor Inmi reminded us that the audience might not notice the flow sensor and don’t know how to interact with it, so we inserted a small piece of plank into the stage to hang the flow sensor and let the audience pick it up. After that, we bought artificial green plants to wrap it around the stage and put artificial grass and moss at the bottom to to simulate a forest environment that echoes the theme of our project.
Here is the picture. And it’s all done!
CONCLUSIONS:
Our project achieved its stated goal. During the presentation and IMA Show, the audience played the game by moving their hands up and down over an ultrasonic sensor to adjust the dandelion’s height and blowing into a flow sensor to increase its speed. This approach not only met our goal of providing a relaxing escape from stress but also attracted the audience, who found the controls both novel and enjoyable.
My definition of interaction from previous reading analysis is that: “Interaction is an action where two sides respond to each other naturally and spontaneously. ” Our project embodies this concept by enabling audience to immerse themselves in a poetic forest-theme gameplay environment. Their actions directly influenced the course of the game, leading to different endings, and allowing them to play the game freely, making each interaction unique based on their movements.
The challenges we faced, particularly the instability of the ultrasonic sensor, were significant learning opportunities. It taught us the importance of flexibility in design and the surprising results during the construction procedures. Also, some problems discussed in User Test also highlight the critical role of user testing in identifying practical issues that may not be apparent during the initial development phase.
From our accomplishments, we have learned how to effectively combine physical and digital parts to build an interactive design that can create compelling user experiences. The positive feedback and active engagement from our audience during the project presentations and IMA Show highlighted the value of integrating intuitive physical interactions into digital outcomes.
DISASSEMBLY:
APPENDIX:
Full Code from Arduino:
#include /** * Is possible set the timeout in the constructor, like: * Ultrasonic ultrasonic(12, 13, 20000UL); */ Ultrasonic ultrasonic(13, 12); double flow; //Water flow L/Min int flowsensor = 2; unsigned long currentTime; unsigned long lastTime; unsigned long pulse_freq; void pulse () // Interrupt function { pulse_freq++; } void setup() { Serial.begin(115200); ultrasonic.setTimeout(40000UL); pinMode(flowsensor, INPUT); attachInterrupt(0, pulse, RISING); // Setup Interrupt currentTime = millis(); lastTime = currentTime; } void loop() { currentTime = millis(); // Every second, calculate and print L/Min if(currentTime >= (lastTime + 100))//can be changed { lastTime = currentTime; // Pulse frequency (Hz) = 7.5Q, Q is flow rate in L/min. flow = (pulse_freq / 7.5); pulse_freq = 0; // Reset Counter Serial.print(ultrasonic.read()); //delay(100); Serial.print(","); Serial.print(round(flow*10), DEC); Serial.println(); //Serial.println(" L/Min"); } }
Full Code from Processing:
PImage dandelion, vine, sparrow; PImage Introduction, Instructions, Game, Ending1, Ending2; import processing.sound.*; SoundFile sound; SoundFile soundLeaf; SoundFile soundBird; SoundFile soundWind; import processing.serial.*; Serial serialPort; int NUM_OF_VALUES_FROM_ARDUINO = 2; /* This array stores values from Arduino */ int arduino_values[] = new int[NUM_OF_VALUES_FROM_ARDUINO]; ArrayList<Obstacle> obstacles; Player player; Chaser chaser; int score = 0; String gameState = "INTRODUCTION"; int Ending; void setup() { dandelion = loadImage("dandelion.png"); vine = loadImage("vine.png"); sparrow = loadImage("sparrow.png"); Introduction = loadImage("Introduction.png"); Instructions = loadImage("Instructions.png"); Game = loadImage("Game.png"); Ending1 = loadImage("Ending1_Obs.png"); Ending2 = loadImage("Ending2_Bird.png"); sound = new SoundFile(this, "forest.mp3"); sound.loop(); soundLeaf = new SoundFile(this, "leaf.wav"); soundBird = new SoundFile(this, "bird.wav"); soundWind = new SoundFile(this, "wind.mp3"); printArray(Serial.list()); serialPort = new Serial(this, "/dev/cu.usbmodem101", 115200); //size(1920,1080); fullScreen(); obstacles = new ArrayList<Obstacle>(); player = new Player(); chaser = new Chaser(); } void draw() { getSerialData(); background(214, 247, 255); switch (gameState) { case "INTRODUCTION": drawIntroduction(); break; case "INSTRUCTION": drawInstruction(); break; case "GAME": runGame(); break; case "END": if (Ending == 1) { endGame(Ending1); //soundLeaf.loop(); if (soundLeaf.isPlaying() == false) { soundLeaf.play(); } } if (Ending == 2) { endGame(Ending2); //soundBird.loop(); if (soundBird.isPlaying() == false) { soundBird.play(); } } break; } } void keyPressed() { if (gameState.equals("INTRODUCTION") && key == 'i') { gameState = "INSTRUCTION"; } else if (gameState.equals("INSTRUCTION") && key == 's') { gameState = "GAME"; setupGame(); } else if (gameState.equals("END") && key == 'r') { gameState = "INTRODUCTION"; } } void drawIntroduction() { image(Introduction, 0, 0, 1440, 900);//the size of my mac screen //text("Welcome to the Game!\nPress 'I' to go to Instructions.", width / 2, height / 2); } void drawInstruction() { image(Instructions, 0, 0, 1440, 900); //text("Game Instructions:\nPress 'S' to start the game.", width / 2, height / 2); } void setupGame() { fullScreen(); obstacles = new ArrayList<Obstacle>(); player = new Player(); chaser = new Chaser(); image(Game, 0, 0, 1440, 900); soundWind = new SoundFile(this, "wind.mp3"); } void runGame() { image(Game, 0, 0, 1440, 900); totalObstacles(); player.update(); player.display(); chaser.update(); chaser.display(); checkCollisions(); displayScore(); } void totalObstacles() { if (frameCount % 80 == 0) { obstacles.add(new Obstacle()); } for (int i = obstacles.size() - 1; i >= 0; i--) { Obstacle obs = obstacles.get(i); obs.update(); obs.display(); if (obs.x< -width) { obstacles.remove(i);//remove the obstacle if it's off screen } else if (obs.isPassedBy(player)) { score++; } } } void checkCollisions() { if (chaser.collidesWith(player)) { Ending = 2; endGame(Ending2); } for (Obstacle obs : obstacles) { if (obs.collidesWith(player)) { Ending = 1; endGame(Ending1); } } } void endGame(PImage picture) { gameState = "END"; image(picture, 0, 0, 1440, 900); textSize(100); textAlign(CENTER, CENTER); text(score, 150+width/2, height/2+50); } void displayScore() { fill(255); textSize(50); text("Score: " + score, width - 200, 80); } 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]); } } } } }
class Chaser { float x=0; float y=height/2; float s=200; Chaser() { this.x=x; this.y=y; this.s=s; } void update() { x+=0.7; } void display() { image(sparrow, x, y, s, s); } boolean collidesWith(Player player) { return dist(x, y, player.x, player.y) < (s+player.s)/2; } }
class Obstacle { float x = width; // original position float d = 50; // width float h = 230; // the height of the gap float gapBottom = random(200+h, height-h+200); // the bottom position of the gap float gapTop = gapBottom - h; boolean passed = false; //color colorA = color(127, 76, 27); //color colorB = color(127, 35, 60); color colorA = color(73, 149, 44); color colorB = color(#1D480C); void update() { x -= 6; } void display() { image(vine, x, 0, d+50, gapTop); image(vine, x, gapBottom, d+30, height-gapBottom); } boolean collidesWith(Player player) { return (player.x + player.s/2 > x && player.x - player.s/2 < x+d) && (player.y - player.s/2 < gapTop || player.y + player.s/2 > gapBottom); //the right side of player is over the left side of the obstacle //Ans the left side of the player is not over the right side of the obstacle } boolean isPassedBy(Player player) {//add score if the obstacle is passed by the player if (!passed && player.x > x + d) { passed = true; return true; } return false; } }
class Player { float x=300; float xspeed; float y=height/2; float s=50; float smoothedY = y; float alpha = 0.05; //constant number Player() { this.x = x; this.xspeed=xspeed; this.y = y; this.s = s; this.smoothedY = this.y; //this.sound = soundWind; } void update() { float rawY = map(arduino_values[0], 60, 0, 100, height-100); // read raw value from Arduino smoothedY = alpha * rawY + (1 - alpha) * smoothedY; // EMA smooth y = constrain(smoothedY, s/2, height - s/2); //ensure y is in the screen //smoothedY = rawY; println("Raw Y: ", arduino_values[0], " Smoothed Y: ", smoothedY); println("y: ", y); float xspeed=map(arduino_values[1], 0, 30, 0, 1.2); x=x+xspeed; //println(xspeed); if (arduino_values[1]>10) { if (soundWind.isPlaying() == false) { soundWind.play(); } } else { soundWind.stop(); } } void display() { image(dandelion, x, y, s+50, s+50); } }
Citation
“howling wind.” Liecio. pixabay. 2022. https://pixabay.com/sound-effects/howling-wind-109590/.
“In the Forest – Ambient Acoustic Guitar Instrumental Background Music (IG Version 60s).” Lesfem. pixabay. 2021. https://pixabay.com/music/solo-guitar-in-the-forest-ambient-acoustic-guitar-instrumental-background-music-ig-version-60s-9646/.
Simoes, Erick. Ultrasonic Timeout.ino. 2018. https://github.com/ErickSimoes/Ultrasonic/blob/master/examples/Timeout/Timeout.ino.
“Wind in Trees.” SoundsForYou. pixiabay. 2022. https://pixabay.com/sound-effects/wind-in-trees-117477/.
Zhang, Wenyi. “China: Share of People Feeling More Stressed 2022.” Statista, 22 July 2022, www.statista.com/statistics/1321615/china-share-of-people-feeling-more-stressed/.