A. Molecular Soccer – Ken Wu and Alan Chao – Professor Eric Parren
(Video: World Finals of Molecular Soccer)
B. Conception and Design
The concept of this project was to make a game that was competitive, but simple and could be played by people of all ages. We aimed for this project to be fun for children where there was interaction and struggle when playing the game itself. We looked at how simple interfaces in the Gameboy were playable and enjoyable as well as took inspiration from how this simple the design was. The simplicity of such design influenced me to design the game in such a way that was intuitive for children. We thought about how the game should be played to make the game more fun and entertaining for individuals to give them difficulty and a different experience which led to us to want to create a pulley system for playing the game, however, the ER lacked the material leading to us not being able to trial this out. Buttons were the last option for consideration, but the option we had to go with. As the game is played with hands rather than fingers, the button size was important which is what we focused on to create greater user experience. In the user testing session, we received feedback about the projection of our game, however, we ultimately decided to use an external screen due to the terrible outcome of projection on 3dimensional buttons. I think that this kind of feedback was valuable in trialing the extent that our design could function upon.
C. Fabrication and Production
For fabrication and production, a very crucial part was for the processing to work without much delay. Alan was in charge of the processing debugging to get the wanted result due to his expertise. This process in the fabrication and production process can be seen in his reflection for the work that he put it on it. A very important part was linking his processing with the final design. We were successful with designing the board interface to represent the interface as well as making the product refined through sanding all the edges and the board to create wonderful user experience.
(Figure 1: Interface and Board Parallel)
(Figure 2: Sanding)
(Figure 3: Blowing dust off wood for smooth crisp texture)
Of course, this design wasn’t without small failures with the design. At first, the intended design was a closed box, however, that did not take into consideration the button framework or the hole for the USB to computer. Therefore, I amended the design on Rhino after having laser cut the pieces to see if there was any way to save it. This overlooked aspect led to the open box design.
(Figure 4: Previous design with the border on the edges)
(Figure 5.1: Modeling)
(Figure 5.2: Changed design to accommodate item size and USB hole)
As we decided on an open box design where people can see through the hole, it was important for the wires to be tidy. We then cut wires to be small rather than using jumper wires to make it a clean circuit.
(Figure 6: Short wires for the circuit)
One thing previously mentioned was the button that we would use for playing. On user testing day, we borrowed buttons from the ER which were terrible for our game as they were small and had a lot of limitations in terms of user experience. Therefore, we bought huge buttons which made the experience more friendly.
(Figure 7: Terrible buttons)
(Figure 8: Good buttons)
Essentially, those were all the components that were actually decided in our project where the end product was clean easy and applicable.
(Figure 9: Clean user experience)
D. Conclusions
Our project’s intended goal morphed from having a bit of education input within it to being fully for entertainment. This was a good change as the connection between education and entertainment for our game wasn’t very clear and beneficial. This project was super successful in entertainment though where many of the responses to our project were highly positive and the game had a lot of people into it where they kind of had an addiction to it after playing it for a bit of time. For one, Alan and I were both addicted to this game and actually spent hours playing together in multiple sessions to understand how user experience could be built better. This aligns with my definition of interaction as the actions of someone affect the actions of the other in a continuous sequence. Our game is never-ending in interaction. The focus of the player doesn’t end after pressing one button as they have to continuously focus on playing the game. We would improve our project by retrying the projected interface with pulley system to make this more advanced. This would need more time as it was our initial plan but the engineering of the pulley system was too advanced for my advanced beginner 3dmodeling skills. I think that I learned a lot from my setbacks and failures, particularly in the aspect of realizing the best solution. Our solution couldn’t be fulfilled due to a lack of skill and experience in designing mechanical features which is something I hope to learn and become advanced upon to realize more interactive mechanical designs before integrating it with the digital. I think that my biggest takeaway from the project produced is that there are multiple solutions and sometimes basic solutions aren’t always bad for the project. In fact, it may make the game easier to play. Of course, why should anyone care? I personally believe that people should care about people’s failures, insights, and thoughts as they may drive you from making the same mistakes and being stalled at one point. If one can understand the difficulty of realizing a solution, they should also realize how they can understand how to mitigate the difficulty through learning. Only when people learn can they create the future.
E. Technical Documentation
General Processing Sketch
import processing.sound.*; SoundFile file; SoundFile win; //SoundFile blob; import processing.serial.*; String myString = null; Serial myPort; int NUM_OF_VALUES = 10; /** YOU MUST CHANGE THIS ACCORDING TO YOUR PROJECT **/ int[] sensorValues; PImage soccer; Ball ball; PVector loc; PVector gravity = new PVector(0, 10); boolean playState = true; int count = 0; int left_score = 0; int right_score = 0; float[][] trails = new float[100][2]; Player[] players = new Player[10]; String goalSide; PFont arcade; int winningHue; void setup() { fullScreen(); arcade = createFont("ArcadeClassic", 32); //size(1200, 675); setupSerial(); loc = new PVector(width/2,height/2); //fullScreen(); //pixelDensity(2); ball = new Ball(loc); rectMode(CORNERS); win = new SoundFile(this, "win.wav"); //blob = new SoundFile(this, "blob.wav"); file = new SoundFile(this, "378355__13gpanska-lakota-jan__bouncing-football-ball-off-the-ground.wav"); players[0] = new Player(new PVector(310, height/2 - 117.5), 1); players[1] = new Player(new PVector(310, height/2 + 117.5), 1); players[2] = new Player(new PVector(550, height/2 - 280), 1); players[3] = new Player(new PVector(550, height/2 + 280), 1); players[4] = new Player(new PVector(width/2 + 200, height/2), 1); players[5] = new Player(new PVector(width-310, height/2 - 117.5), 2); players[6] = new Player(new PVector(width-310, height/2 + 117.5), 2); players[7] = new Player(new PVector(width - 550, height/2 - 280), 2); players[8] = new Player(new PVector(width-550, height/2 + 280), 2); players[9] = new Player(new PVector(width/2-200, height/2), 2); } void draw() { background(240); updateSerial(); println(frameRate); //printArray(sensorValues); push(); //textFont(arcade); fill(255, 0,0, 40); textSize(800); textAlign(CENTER); text(left_score, width/2 - 350, height - 200); fill(0, 0, 255, 40); text(right_score, width/2 + 350, height - 200); pop(); drawCourt(); for (int i = 0; i < players.length; i++) { players[i].run(); } if (playState){ //println(mouseX, ",", mouseY); //println(frameRate); gravity.y = 20*noise(frameCount) - 10; ball.applyForce(gravity); int head = frameCount%100; trails[head][0] = ball.location.x; trails[head][1] = ball.location.y; push(); colorMode(HSB); for (int i = 0; i < trails.length; i++) { float dia; if (i >= head) { dia = 0.3*(head - i); }else{ dia = 0.3*(head - (i+100)); } fill(30, 200, 255, 80); circle(trails[i][0], trails[i][1], dia); } pop(); ball.run(); if (ball.location.x <= ball.size/2 + 133 || ball.location.x >= width - ball.size/2 - 133) { ball.velocity.x *= -1; file.play(); ball.hue += random(200); if (ball.location.x < 133) { ball.location.x = ball.size/2 + 20; }else if (ball.location.x > width - 133) { ball.location.x = width - ball.size/2 - 20; } if (ball.location.y > height/2 - 200 && ball.location.y < height/2 + 200 && ball.location.x < width/2) { right_score += 1; goalSide = "Blue"; playState = false; //ball.velocity.mult(0); push(); fill(255,0,0); rect(0, height/2 - 200, 133, height/2 + 200); pop(); } if (ball.location.y > height/2 - 200 && ball.location.y < height/2 + 200 && ball.location.x > width/2) { //ball.velocity.mult(0); left_score += 1; goalSide = "Red"; playState = false; push(); fill(255,0,0); rect(width-133, height/2 - 200, width, height/2 + 200); pop(); } } if (ball.location.y <= ball.size/2 + 5 || ball.location.y >= height - ball.size/2 -5) { ball.velocity.y *= -1; file.play(); ball.hue += random(200); if (ball.location.y < 5) { ball.location.y = ball.size/2 + 20; }else if (ball.location.y > height - 5) { ball.location.y = height - ball.size/2 - 20; } } //println(keyPressed); for (int i = 0; i < players.length; i++) { //players[i].run(); players[i].bounce(ball); //if (i != 9) { // players[i].reactSize(i == Character.getNumericValue(key) - 1 && keyPressed); //}else{ // players[i].reactSize(0 == Character.getNumericValue(key) && keyPressed); //} players[i].reactSize(parseBoolean(sensorValues[i])); } } else{ if (!win.isPlaying()){ win.play(); } count += 1; push(); colorMode(HSB); winningHue += round(10*noise(frameCount)); winningHue %= 360; fill(winningHue, 150, 230); textFont(arcade); textAlign(CENTER); textSize(200); text(goalSide + " Scores", width/2, height/2 + 50*noise(frameCount*0.1) - 25); //println(count); pop(); if (count > 180) { playState = true; win.stop(); count = 0; ball.location.x = width/2; ball.location.y = height/2; //ball.velocity.mult(0); //ball.velocity.y = random(-1,1); } } } void drawCourt () { pushStyle(); stroke(120); noFill(); strokeWeight(3); line(133, 5, 133, height - 5); line(width - 133, 5, width - 133, height - 5); line(133, 5, width -133, 5); line(133, height-5, width -133, height-5); strokeWeight(1); //rect(2, 382, 30, 468); rect(133, height/2 - 200, 94, height/2 + 200); rect(133, height/2 - 350, 215, height/2 + 350); //rect(width-2, 382, width-30, 468); rect(width-133, height/2 - 200, width-94, height/2 + 200); rect(width-133, height/2 - 350, width-215, height/2 + 350); ellipse(width/2, height/2, 200, 200); line(width/2, 5, width/2, height-5); push(); stroke(0, 200, 200); strokeWeight(5); line(133, height/2 - 200, 133, height/2 + 200); line(width - 133, height/2 - 200, width-133, height/2 + 200); pop(); popStyle(); } void setupSerial() { printArray(Serial.list()); myPort = new Serial(this, Serial.list()[ 3 ], 9600); // WARNING! // You will definitely get an error here. // Change the PORT_INDEX to 0 and try running it again. // And then, check the list of the ports, // find the port "/dev/cu.usbmodem----" or "/dev/tty.usbmodem----" // and replace PORT_INDEX above with the index number of the port. myPort.clear(); // Throw out the first reading, // in case we started reading in the middle of a string from the sender. myString = myPort.readStringUntil( 10 ); // 10 = '\n' Linefeed in ASCII myString = null; sensorValues = new int[NUM_OF_VALUES]; } void updateSerial() { while (myPort.available() > 0) { myString = myPort.readStringUntil( 10 ); // 10 = '\n' Linefeed in ASCII if (myString != null) { String[] serialInArray = split(trim(myString), ","); if (serialInArray.length == NUM_OF_VALUES) { for (int i=0; i<serialInArray.length; i++) { sensorValues[i] = int(serialInArray[i]); } } } } }
Sketch of Ball
class Ball { PVector location; PVector velocity; PVector acceleration; float size; float mass = 30; int hue; Ball(PVector l) { acceleration = new PVector(0,0); velocity = new PVector(random(10),random(-2,0)); //velocity = new PVector(0,0); location = l.get(); size = 30; hue = 0; } void run() { update(); display(); } void applyForce(PVector force) { PVector f = force.get(); f.div(mass); acceleration.add(f); } // Method to update location void update() { velocity.add(acceleration); location.add(velocity); acceleration.mult(0); velocity.limit(16); velocity.mult(0.998); //lifespan -= 2.0; } // Method to display void display() { push(); stroke(0); strokeWeight(2); //noStroke(); colorMode(HSB); fill(hue % 360, 200 , 255); ellipse(location.x,location.y,size,size); pop(); } void setSize(float value) { size = value; } // Is the particle still useful? //boolean isDead() { // if (lifespan < 0.0) { // return true; // } else { // return false; // } //} } class Player extends Ball { int side; float originalSize; Player(PVector l, int s) { super(l); this.setSize(130); originalSize = size; side = s; velocity = new PVector(0,0); } void display() { noStroke(); if (side == 1) { fill(200, 0, 0, size * 1.1); } else if (side == 2) { fill(0, 0, 200, size* 1.1); } ellipse(location.x,location.y,size,size); if (size > 160) { push(); for (int i = 0; i < 20; i++) { float d = map(i, 0, 20, 0, size); float op = map(d, 0, size, 80, 0); stroke(255, op); strokeWeight(20); noFill(); circle(location.x,location.y,d); } pop(); } } boolean checkPos(Ball other) { float dist = this.location.dist(other.location); if (dist <= (size + other.size)/2 + 2) { return true; }else{ return false; } } void reactSize(boolean triggerState) { if (triggerState){ //println("yes"); //float rate = sensorValues[i] - pSensorValues[i]; //rate = map(rate, 0, 1023, 30, 100); //if (!blob.isPlaying() || frameCount % 30 == 0){ // blob.play(); //} size += 40; if (size > 300) { size = 300; } } else if (size > originalSize){ size = lerp(size, originalSize, 0.03); } } void bounce(Ball other) { if (this.checkPos(other)) { file.play(); other.hue += random(200); PVector force = new PVector(other.location.x - location.x, other.location.y - location.y); float dist = this.location.dist(other.location); dist = dist / 2; force.normalize(); force.mult((other.velocity.mag()+1) * dist); //force.mult(dist*100); other.applyForce(force); } } }
Arduino Sketch
// IMA NYU Shanghai // Interaction Lab // For sending multiple values from Arduino to Processing int sensors[10]; void setup() { Serial.begin(9600); for (int i=2; i < 12; i++) { pinMode(i, INPUT); } } void loop() { for (int i=2; i < 12; i++) { sensors[i] = digitalRead(i); Serial.print(sensors[i]); if(i != 11) { Serial.print(","); } } // keep this format // Serial.print(sensor1); // Serial.print(","); // put comma between sensor values // Serial.print(sensor2); // Serial.print(","); // Serial.print(sensor3); Serial.println(); // add linefeed after sending the last sensor value // too fast communication might cause some latency in Processing // this delay resolves the issue. delay(100); }