上春山(Climb Mountain in Spring)! – Jingyi Ma – Gottfried Haider
Here’s the video for winning this game!
- CONTEXT AND SIGNIFICANCE
In the previous classes, my teammate Jiaqi and I made three projects together: “Race the LED”, “Roating Link” and “Seeing Stars”. “Race the LED” is a project that requires two players to push a button 10 times as quickly as possible to win the game. “Roating Link” requires a stepper motor and a link to transfer cycle movements into back-and-forth movements. “Seeing Stars” is a project that requires using a servo motor and a magnet sensor. By triggering the magnet sensor, 5 stars will pop out from the back of your head. These projects require users’ input and deliver output for interaction. The first project requires pressing the button, the second project requires sending commands through programming on the computer, and the third project requires a digital signal. So in the midterm project, we wish to create a fun and interactive project that can use digital inputs, analog inputs, and servo motors at the same time. The main idea of our project 上春山(Climb Mountain in Spring) is a speed racing game that requires three players. We implemented the logic of “Race the LED” for a speed game, this time with three players. We use 360-degree servo motors to draw the belt for racing. This project combines the latest meme on Chinese social media “上春山”, a stage accident that happened at CMG Spring Festival Gala. In this accident, the actor wearing black forgot to go off the top of the “mountain” and kept staying there, and this accident quickly became a meme. We decided to make this into a “mountain climbing” game, giving everyone an equal chance to stay at the top of the mountain!
Our target audience is the young students and staff that are familiar with this meme. As this is a widespread meme on the Chinese internet, this project creates a dramatic comic effect for its players, and people can play this game to ease their minds during the midterm week.
(If you are interested in what happened at CMG Gala, click here.)
- CONCEPTION AND DESIGN
Our goal is to create a mountain climbing speed racing game with three players. To win the game, the players need to push two buttons one by one at the fastest speed possible. The first person arriving at the mountain will have to sing (or yell) at the microphone for two seconds to win the game, or they will lose the game and be pushed back to the foot of the mountain.
To mimic the effect of “Climbing Mountain in Spring”, after listening to Prof. Gohai’s suggestions, we decided to make two buttons for each player. Only when they push the buttons one by one will it activate the servo motor. This prevents players from clicking a button too fast and also creates a life-like experience of climbing a mountain step by step. We used cardboard and sticks to make the base of the mountain, for they are easy to retrieve and are very solid, providing good support for the belts and motors.
We use plastic wrap roll to make the rollers of the belt. There were no big rolls in the studio, and a friend of ours, Maggie Wang, has a plastic wrap out of use. So we cut the roll in six, wrapped it with black tape, and created six rollers. They were connected by one-time chopsticks, for the sticks in Inter Lab are too short.
Professors initially suggested we use rubber bands from the Fashion Design Lab, but we were afraid that they were not strong enough to pull cardboard figures, so we bought fitness rubber bands. It turned out that they were too hard to be pulled by 360-degree servo motors on user testing, so we switched to rubber bands again for the final version.
I planned to paint the mountain in gradient light green and dark green, with white paper clay on top of it to make a snow-covered “mountain in spring”, and also hide the end of chopsticks.
(The colored sketch of the mountain)
(The circuit diagram)
- FABRICATION AND PRODUCTION
The first step is to draw a sketch based on our proposed ideas. I initially wanted to make a “climbing step” the same as the CMG gala, but Jiaqi considered this would be too hard to make the figures climb “step by step”, so she suggested building a mountain and letting people climb up by belt and rolls. After a discussion with Prof. Gohai, we think Jiaqi’s plan is more feasible.
(The first sketch of the mechanism)
3/4: At the beginning of the construction process, Jiaqi cut three 30*40*50 pieces of cardboard as the main bracket structure of the mountain. We initially wanted to put the track on the “ridge” of the mountain, but we found it wouldn’t be feasible, for rollers are too wide. Then we move the track to the “valley” of the mountain. The first stage of the mountain construction looks like this. We borrowed three 360-degree servos from the ER room. We use small sticks to strengthen the base.
3/5 :
We continue to build the base of the mountain and it looks a bit like a mountain now.
3/6:
In the second phase, we started coding and building circuits. Jiaqi first wrote a small program to test the feasibility of a 360-degree motor on fitness bands, and if yelling at the microphone could trigger the motor to move backwards. She tested the code and found the rubber band couldn’t move because there existed too much resistance between it and the cardboard, so she cut the “valleys” open and made space for the rubber band. After that, the project can run for the first time. Here’s the code for Jiaqi’s testing program, with only one button and one servo.
#include Servo myservo1; Servo myservo2; // Create servo object to control a servo int buttonPin1 = 2; int buttonState = 0; int prevbuttonState = LOW; int servoAngle = 90; int buttonPressCount = 0; int analogPin = A0; int analogInput; int check1 = 7; int button = LOW; int prevbutton = LOW; bool reached1 = false; unsigned long total; unsigned int count; unsigned long whenStarted; const unsigned long INTERVAL = 5000; void setup() { // initialize the pushbutton pin as an input: pinMode(buttonPin1, INPUT); pinMode(check1, INPUT); myservo1.attach(11); myservo2.attach(9); Serial.println(F("Starting ...")); whenStarted = millis(); // myservo2.write(servoAngle); Serial.begin(9600); } void loop() { analogInput = analogRead(analogPin); // Serial.print("Analog Input: "); // Serial.println(analogInput); buttonState = digitalRead(buttonPin1); reached1 = digitalRead(check1); if (buttonState == HIGH && prevbuttonState == LOW) { buttonPressCount++; servoAngle += 5; myservo2.write(servoAngle); } else if (buttonState == LOW && prevbuttonState == HIGH) { myservo2.write(90); } prevbuttonState = buttonState; // Serial.println(reached1); if (button == HIGH && prevbutton == LOW) { reached1 = true; } prevbutton = button; // Serial.println(reached1); if (reached1) { total += analogRead(A0); count++; if (millis() - whenStarted >= INTERVAL) { Serial.print(F("Average = ")); Serial.println(total / count); total = 0; count = 0; whenStarted = millis(); } if (total / count >= 65) { myservo2.write(90); } else { myservo2.write(90); servoAngle -= 5; myservo2.write(servoAngle); } } }
(The mountain was cut open.)
3/7:
I was mainly focused on creating the code, especially the “one step at a time” mode with two buttons. I encountered many difficulties in writing this program, and I asked Prof. Gohai for help. Unfortunately, this version of code cannot satisfy the “one step at a time” goal. I turned to ask Siwei and Kevin, and Kevin instructed me to add Boolean values and a variable “playerAlastbutton” to distinguish between button 1 and button 2 of each player. Kevin also helped me how to make the 360-degree servo move for 100 milliseconds with each push by writing myservo1.(135) and myservo1.(90). Here’s the example code of one-by-one steps:
if (playerAbutton1state == HIGH && playerAbutton1prev == LOW && playerAlastButton == 0 && playerAButtonEnabled == true) { whenStartedA = millis(); playerAButtonEnabled = false; // go up playerAposition = playerAposition + 1; playerAlastButton = 1; } else if (playerAbutton2state == HIGH && playerAbutton2prev == LOW && playerAlastButton == 1 && playerAButtonEnabled == true) { whenStartedA = millis(); playerAButtonEnabled = false; playerAposition = playerAposition + 1; playerAlastButton = 0; } if (millis() <= whenStartedA + 200) { myservo1.write(135); } else if (millis() <= whenStartedA + 300) { myservo1.write(90); } else { playerAButtonEnabled = true; } Serial.print("player 敬亭: "); Serial.println(playerAposition);
By this method, the Arduino detects the buttons’ original state and changing state. The servo will only move for 100 milliseconds if you push the buttons one by one. So the biggest problem has been solved, at least for the time being 😮 (Harder ones are on the way.)
As the mountain was large and we needed to hide wires inside it, we needed to make the wires longer. Jiaqi did the work of soldering and connecting male and female jumpers to make the wires longer. Then she connected the circuits of the project to the Arduino and breadboard. This is 3 wires per button (*6) and 3 wires per servo (*3), 27 wires with no count of other jump cables. So it was indeed very messy.
I drew 3 cardboard figures and glued them on the fitness band. In case any wires drop off, I fetched two hinges from Fab lab and made a door on one side of the mountain. This enabled us to open the mountain and check the wires.
So far, you can go up the mountain one step at a time, and sing at the microphone, but there will be no punishment for not singing. At the 3/8 user testing, it is just a speed racing game.
(Testing the speed function)
3/8:
Our friend from the Humanities, Yuchen came to user testing! We found that as the fitness bands are heavy and smooth, it’s hard for servos to draw them, and we encountered rollers skidding from time to time. There’s also no indication of the start of the game and the end of the game. There’s plenty of things to be fixed.
3/10:
We wanted to make a stage 2 that would draw the player off the mountain if they reached the top but didn’t sing. The mechanism was we would give them 2 seconds to prepare, and they had to sing for 2 seconds to stay at the top of the mountain.
Jiaqi connected a microphone sensor to the top of the mountain by “twisting sticks.” We initially made a paper tube for holding the microphone, but now there were too many wires in the mountain that we couldn’t implement the tube. We initially wanted to use digital inputs, but as long as there’s some noise, the microphone reads 1. She then switched to analog mode. Luckily, it worked this time. She made the belts tighter and lighter to move more smoothly.
Still, we’re struggling to connect the buzzer and push the player off the mountain. After testing, we found that it requires 23 steps for players B and C up the mountain, but 25 steps for player A. As the mountain has been fixed, we couldn’t do it again. This became a flaw in the project that player A needed to press two more times in stage 1.
We were struggling in the “2 seconds to prepare and sing for 2 seconds” part of coding. I went to ask Rudi for help, but unfortunately, we had some misunderstandings in the communication process. As the code became very complicated, we thought there was nothing serious with the code but it reported errors all the time!!! At last, it was just a missing bracket, and the program could run successfully. Still, we are missing the music.
3/11:
There was still no indication of the start of the game and the end of the game. After a failure to transfer music into code, I decided to compose the music by myself. Luckily, it was a pentatonic scale and I only required 8 notes for two rhythms.
(The starting music)
(The ending music)
The music “上春山” is really playful. I’m giving the code of the first and second pieces of music here if anyone wants:
int melody[] = { NOTE_E5, NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_B4, NOTE_CS5, NOTE_GS4, NOTE_B4, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, 0, NOTE_E4, NOTE_CS4, NOTE_E4, 0, NOTE_E4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_E4 }; // note durations: 4 = quarter note, 8 = eighth note, etc.: int noteDurations[] = { 8, 16, 8, 8, 4, 8, 16, 16,8, 8, 8, 8, 2,4, 8, 8, 8, 8, 8, 8, 4, 8, 16, 16, 8, 8, 4 };
int melody[] = { NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_GS4, NOTE_FS4, NOTE_GS4, NOTE_B4, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_CS4, NOTE_CS5, NOTE_B4, NOTE_FS4, NOTE_GS4, 0, NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_GS4, NOTE_FS4, NOTE_GS4, NOTE_B4, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, NOTE_E4, 0, NOTE_CS4, NOTE_E4, 0, NOTE_E5, NOTE_FS5, NOTE_E5, NOTE_E5, NOTE_E5 }; int noteDurations[] = { 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 4, 8, 8, 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 8, 8, 8, 8, 4, 8, 16, 16, 8, 8, 4 };
When I wanted to input the music of the buzzer to the code, bugs appeared again and again, as the music would play without stopping. After discussing with Siwei, I set up stage 3 to make the music play only once.
At this stage, we calculated it would require 4 seconds to push the figure off the mountain. So we wrote the code to make the servo write (0) for 4000 milliseconds and stop.
3/12:
Now we arrived at the final stage of the project for presentation. I didn’t know if my computer got nervous or not, but the project failed 2 times before it worked properly. Luckily, we survived the presentation and reached the wanted effects!!!
The full code of the project is here:
#include <Servo.h>
#include "pitches.h" Servo myservo1; Servo myservo2; Servo myservo3; // Create servo object to control a servo int MicrophoneVal; bool playerAButtonEnabled = true; bool playerBButtonEnabled = true; bool playerCButtonEnabled = true; long state2startTime; long xiashan1; long xiashan2; long xiashan3; bool state2FirstRun = true; int state1Winner = 0; int state = 1; int playerAbutton1pin = 2; int playerAbutton2pin = 4; int playerAbutton1state; int playerAbutton2state; int playerAbutton1prev; int playerAbutton2prev; int playerAlastButton = 0; int playerAposition = 0; int playerBbutton1pin = 6; int playerBbutton2pin = 8; int playerBbutton1state; int playerBbutton2state; int playerBbutton1prev; int playerBbutton2prev; int playerBlastButton = 0; int playerBposition = 0; int playerCbutton1pin = 10; int playerCbutton2pin = 12; int playerCbutton1state; int playerCbutton2state; int playerCbutton1prev; int playerCbutton2prev; int playerClastButton = 0; int playerCposition = 0; unsigned long total; unsigned int count; const unsigned long INTERVAL = 5000; unsigned long whenStartedA = 0; unsigned long whenStartedB = 0; unsigned long whenStartedC = 0; unsigned long Singing = 0; void setup() { Serial.begin(9600); myservo1.attach(7); myservo2.attach(9); myservo3.attach(11); Serial.println(F("******************* 上春山 *******************")); //开始 delay(3000); //麦克风在3pin pinMode(A0, INPUT); int melody[] = { NOTE_E5, NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_B4, NOTE_CS5, NOTE_GS4, NOTE_B4, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, 0, NOTE_E4, NOTE_CS4, NOTE_E4, 0, NOTE_E4, NOTE_FS4, NOTE_E4, NOTE_E4, NOTE_E4 }; // note durations: 4 = quarter note, 8 = eighth note, etc.: int noteDurations[] = { 8, 16, 8, 8, 4, 8, 16, 16,8, 8, 8, 8, 2,4, 8, 8, 8, 8, 8, 8, 4, 8, 16, 16, 8, 8, 4 }; for (int thisNote = 0; thisNote < 27; thisNote++) { // to calculate the note duration, take one second divided by the note type. //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc. int noteDuration = 1000 / noteDurations[thisNote]; tone(3, melody[thisNote], noteDuration); // to distinguish the notes, set a minimum time between them. // the note's duration + 30% seems to work well: int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); // stop the tone playing: noTone(3); } state = 1; } void loop() { if (state == 1) { state1(); } else if (state == 2) { state2(); } else if (state == 3) { state3(); } } void state1() { // ascent state //player A playerAbutton1state = digitalRead(playerAbutton1pin); playerAbutton2state = digitalRead(playerAbutton2pin); if (playerAbutton1state == HIGH && playerAbutton1prev == LOW && playerAlastButton == 0 && playerAButtonEnabled == true) { whenStartedA = millis(); playerAButtonEnabled = false; // go up playerAposition = playerAposition + 1; playerAlastButton = 1; } else if (playerAbutton2state == HIGH && playerAbutton2prev == LOW && playerAlastButton == 1 && playerAButtonEnabled == true) { whenStartedA = millis(); playerAButtonEnabled = false; playerAposition = playerAposition + 1; playerAlastButton = 0; } if (millis() <= whenStartedA + 200) { myservo1.write(135); } else if (millis() <= whenStartedA + 300) { myservo1.write(90); } else { playerAButtonEnabled = true; } Serial.print("player 敬亭: "); Serial.println(playerAposition); //player B 魏晨 playerBbutton1state = digitalRead(playerBbutton1pin); playerBbutton2state = digitalRead(playerBbutton2pin); if (playerBbutton1state == HIGH && playerBbutton1prev == LOW && playerBlastButton == 0 && playerBButtonEnabled == true) { whenStartedB = millis(); playerBButtonEnabled = false; // go up playerBposition = playerBposition + 1; playerBlastButton = 1; } else if (playerBbutton2state == HIGH && playerBbutton2prev == LOW && playerBlastButton == 1 && playerBButtonEnabled == true) { whenStartedB = millis(); playerBButtonEnabled = false; playerBposition = playerBposition + 1; playerBlastButton = 0; } if (millis() <= whenStartedB + 200) { myservo2.write(135); } else if (millis() <= whenStartedB + 300) { myservo2.write(90); } else { playerBButtonEnabled = true; } Serial.print("player 魏晨: "); Serial.println(playerBposition); //player C playerCbutton1state = digitalRead(playerCbutton1pin); playerCbutton2state = digitalRead(playerCbutton2pin); if (playerCbutton1state == HIGH && playerCbutton1prev == LOW && playerClastButton == 0 && playerCButtonEnabled == true) { whenStartedC = millis(); playerCButtonEnabled = false; // go up playerCposition = playerCposition + 1; playerClastButton = 1; } else if (playerCbutton2state == HIGH && playerCbutton2prev == LOW && playerClastButton == 1 && playerCButtonEnabled == true) { whenStartedC = millis(); playerCButtonEnabled = false; playerCposition = playerCposition + 1; playerClastButton = 0; } if (millis() <= whenStartedC + 200) { myservo3.write(135); } else if (millis() <= whenStartedC + 300) { myservo3.write(90); } else { playerCButtonEnabled = true; } Serial.print("player 大勋: "); Serial.println(playerCposition); if (playerAposition >= 25) { myservo1.write(90); myservo2.write(90); myservo3.write(90); Serial.println("End of Climbing!!!"); state = 2; state1Winner = 1; delay(4000); //Serial.println("Listening to microphone"); } else if (playerBposition >= 23) { myservo1.write(90); myservo2.write(90); myservo3.write(90); Serial.println("End of Climbing!!!"); state = 2; state1Winner = 2; delay(4000); } else if (playerCposition >= 23) { myservo1.write(90); myservo2.write(90); myservo3.write(90); Serial.println("End of Climbing!!!"); state = 2; state1Winner = 3; delay(4000); } playerAbutton1prev = playerAbutton1state; playerAbutton2prev = playerAbutton2state; playerBbutton1prev = playerBbutton1state; playerBbutton2prev = playerBbutton2state; playerCbutton1prev = playerCbutton1state; playerCbutton2prev = playerCbutton2state; } // singing state void state2() { if (state2FirstRun == true) { state2startTime = millis(); state2FirstRun = false; } int MicrophoneVal = analogRead(A0); Serial.println(MicrophoneVal); delay(10); if (millis() - state2startTime < 2000 && MicrophoneVal < 56) { if (state1Winner == 1) { myservo1.write(0); // 敬亭下山 delay(4000); myservo1.write(90); Serial.println("敬亭 lost."); state = 3; } else if (state1Winner == 2) { myservo2.write(0); // 魏晨下山 delay(4000); myservo2.write(90); Serial.println("魏晨 lost."); state = 3; } else if (state1Winner == 3) { myservo3.write(0); // 大勋下山 delay(4000); myservo3.write(90); Serial.println("大勋 lost."); state = 3; } } else { Serial.println("You win!!!"); delay(2000); state = 3; } } void state3() { Serial.println("End of Game."); int melody[] = { NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_GS4, NOTE_FS4, NOTE_GS4, NOTE_B4, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, NOTE_E4, NOTE_CS4, NOTE_CS5, NOTE_B4, NOTE_FS4, NOTE_GS4, 0, NOTE_CS5, NOTE_B4, NOTE_CS5, NOTE_E5, 0, NOTE_GS4, NOTE_FS4, NOTE_GS4, NOTE_B4, 0, NOTE_CS5, NOTE_B4, NOTE_GS4, NOTE_FS4, NOTE_E4, 0, NOTE_CS4, NOTE_E4, 0, NOTE_E5, NOTE_FS5, NOTE_E5, NOTE_E5, NOTE_E5 }; int noteDurations[] = { 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 4, 8, 8, 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 4, 4, 4, 8, 8, 8, 8, 8, 8, 4, 8, 16, 16, 8, 8, 4 }; for (int thisNote = 0; thisNote < 45; thisNote++) { // to calculate the note duration, take one second divided by the note type. //e.g. quarter note = 1000 / 4, eighth note = 1000/8, etc. int noteDuration = 1000 / noteDurations[thisNote]; tone(3, melody[thisNote], noteDuration); // to distinguish the notes, set a minimum time between them. // the note's duration + 30% seems to work well: int pauseBetweenNotes = noteDuration * 1.30; delay(pauseBetweenNotes); // stop the tone playing: noTone(3); } while (true); }
- CONCLUSIONS
Our project’s mechanism achieved the goal, but it still has some flaws. It’s a speed game with three stages. At the beginning setup() function, a piece of music will be played and enters the first stage. In the first stage, 3 players will race the speed of climbing up a mountain. When a player hits the required times, they reach the top of the mountain. They have two seconds to prepare and two seconds to sing at the microphone to win the game. If they don’t do as the requirements, the player who reaches the top first will be drawn back for 4 seconds to the foot of the mountain. Then, the program enters stage 3, another piece of music will be played and marks the end of the game.
In my Read and Analysis I, I said that “Interaction is the process of verbal or non-verbal “conversation” between the two entities, and as the conversations have differences from simple to complex, so as interactions.” This project requires players to push buttons after hearing the music and yell at the microphone after reaching the top of the mountains. So there’s rich interaction in this process.
(A successful example of winning the game)
Still, there are some flaws in this project. The first one is that player A will require two more pushes to win, for their track is slightly longer than B and C. The second one is that there’s no indication of who’s the first to reach the top of the mountain and no indication of winners and losers. Also, the microphone can’t detect who’s yelling. If one player is drawn to the foot of the mountain, there will be no winners.
(If you lose…you will go down, other people remain on the mountain, but there will be no winners.)
If I had more time, I would arrange 1 LED light and 1 Microphone for each player. If the player hits the top, the lights will be on and show the winner. And the player only needs to yell at the microphone in front of them. If one player fails to yell, the other player’s LED will be on and show the winner has been changed. (Though, I guess there will be soooooo many wires, I have no idea if the Arduino can hold them.
I learned that when I meet any bugs in my coding, I need to be patient. I should use the Auto format at first, and check for any missing } or ; before turning for help. I also need to be calmer about any possible flaws in the building process. I’m really glad that our project brings people plenty of joy in the midterm week, but I am also aware that I need to be better at time management when doing the final project text time.
- DISASSEMBLY:
(We put all the cardboard into the trash bin.)
Special thanks to my teammate Jiaqi Qian, instructor Prof. Gohai, fellow Kevin, LA Siwei, and Prof. Andy.
- APPENDIX
“上春山” meme:
https://www.google.com/url?sa=i&url=https%3A%2F%2Fm.36kr.com%2Fp%2F2654994601017601&psig=AOvVaw2Om7Fk1atArnmN0IBQXQ-b&ust=1710677183151000&source=images&cd=vfe&opi=89978449&ved=0CBEQjRxqFwoTCMCroaff-IQDFQAAAAAdAAAAABAD