Conception and Design
Our project is titled “Molecular Soccer”, just as the name and the final outlook is suggesting, it is a physical advocative playable game with two players. In the upcoming paragraphs, I will briefly go through our creation process, supports from background researches, and conceptual engagement with user tests.
Our project as what it is eventually displayed is heavily rooted in the context of arcade culture. Several design principles from the project that we achieved are similar or relevant to the characteristics of the arcade culture: easy acquisition of gaming, intuitive physical access, competitive multiplayer gaming mechanisms, and the enhancement of responsive visuals and sound effects. These are the founding principles that we gradually acquired during the making process and eventually revealed as a self-explanatory system.
For a lot of physical or computer games, players feel frustrated to get used to the controlling interface, especially the ones who are not frequent game players. Hence, an intuitive and user-friendly interface is prominent. We have achieved the stage where most users do not feel confused when using our interface, and they can directly engage with our button panel. This fulfills the principle of easy acquisition of gaming and intuitive physical access. One of the reasons that users can directly understand the interface is the linkage of the virtual red and blue circular poles on the screen with the physical buttons, in terms of both color and position. Due to the intuitive nature of the design of buttons, an instantly understanding will be created in the users’ cognition. During the preparatory research, I mentioned the projects “Playground” and “Playground 2” by Collectif Scale. Their implementation of the foot-stepping control interface influenced and convinced me as an excellent example of intuitive physical access, and based on their implementation, me and my partner Ken once thought about using a more complicated physical control interface during the proposal essay stage of the project. Yet due to the complexity of it, we failed to achieve it in the tight schedule.
During the user testing sessions and lots of other occasions, people had given us improving feedbacks, and lots of them came into reality. Before the user testing sessions, we had not actually started our fabrication process, nor had we decided on the precise material to use. As we did not figure out a way to implement the complex pulley mechanism, Professor Eric Parren suggested us to use big-sized arcade buttons as the alternative, so that it at the very most substitute the constraint of the button interface which only contains two states. ◊ Outside of the user testing sessions, our research fellow Dave Santiano tested out the prototype of the program and suggested that I should add more responsive visuals and sound effects, and that became one of our design principles while I added several little visual effects and responsive sounds.
Fabrication and Production
For the production part, Ken and I divided tasks, I wrote the processing program, while we made the circuit and physical fabrication together, he designed the entire buttons panel, 3d models with rendered illustrations, the purchase of equipment, and the posters.
Our production started with the processing program. To implement our goal of creating an interactive bouncing soccer game, I took the use of the example code from Dan Shiffman’s Nature of Code example: a vector-based physical moving object class, with the functional application of vector, the force that works as a simple 2d physics simulation. I made usage of the template, and modified the parameters, added another class for the red and blue bouncing nodes.
class Ball { // base class that then displays the soccer 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)); location = l.get(); size = 30; hue = 0; }
The ball has location, velocity, and acceleration, so that these basic physics parameter functions well to create a dynamic bouncing ball, along with the method applyForce()
that makes the ball move while using a vector.
void applyForce(PVector force) { PVector f = force.get(); f.div(mass); acceleration.add(f); }
On top of that, I add the class for the poles that extends the base class.
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); }
This class has a very important method that is the foundation of our bouncing soccer mechanism: checkPos()
and bounce()
.
boolean checkPos(Ball other) { // this function check the postion of the pole and the ball // return true if the ball touches the pole float dist = this.location.dist(other.location); if (dist <= (size + other.size)/2 + 2) { return true; }else{ return false; } }
And the result of checkPos()
will be sent into the function bounce()
to apply the force.
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); } }
And the next step we have to consider what the user can do to affect this system. As it is shown in the videos, the circular poles are able to pop bigger when the user presses the corresponding button, and that is controlled by another method of the class.
void reactSize(boolean triggerState) { // takes one boolean parameter as the switch if (triggerState){ size += 40; if (size > 300) { size = 300; } } // gradually get smaller when the switch is off else if (size > originalSize){ size = lerp(size, originalSize, 0.03); } }
When the inner movement calculations are finished, now we connect the plug to the data from serial communication. In Arduino, our program is rather straightforward. It is just 10 buttons so that the circuit is a replicant of the example version of “button” at the Arduino examples.
// 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(","); } } 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); }
So that it sends 10 button’s on/off state to the Serial, and in processing, the information can be grabbed and transferred to the reactSize()
method.
for (int i = 0; i < players.length; i++) { //players[i].run(); players[i].bounce(ball); // debug mode, using the computer keyboard //if (i != 9) { // players[i].reactSize(i == Character.getNumericValue(key) - 1 && keyPressed); //}else{ // players[i].reactSize(0 == Character.getNumericValue(key) && keyPressed); //} // actual arduino mode players[i].reactSize(parseBoolean(sensorValues[i])); }
These are the core features of the program. After I implemented the base structure, we started working with the fabrication process. Ken bought the arcade buttons that have a smooth responsive spring and click planted inside. It is user friendly and well-made.
With the model built by Ken, a flat-shaped box with a buttons panel on top, while wires go
under the surface of it, with the breadboard and Arduino stored inside. The model looks almost exactly the same as the final output:
As we laser cut the wood boards, we have to place the buttons into the pre-cut circular positions, so that the diameter perfectly fits the button, and no need for hot glue to let them stay in position.
Eventually, we put everything in place, also leaving an empty deck for the USB cable.
Conclusion
As our project is aimed at a full gaming experience, I believe we have achieved the goal of producing an entertaining, competitive, and fun game, not to include that we have a rhetoric tribute to the arcade culture. Thanks to the production of the interface panel, people all came interested in our project by pressing the buttons, and sooner they will figure how to play this 2-player game very quickly. Actually, it requires a lot of techniques in order to play the game well, it requires precise timing and accuracy, and high energy at a competition. One of the memorable moments is when Prof. Rudy and Andy played against each other during the IMA show, they grew extremely excited with the competition and hugged each other after one goal. I felt this eccentric joy.
Our project conceptually fits my definition of Interaction, indeed, yet differently. Rather than human-computer interaction, our project maintains a human-computer-human interaction. In both ends, the users are the speaker and respondent. Our program is a translative bridge between the discourse that involves gaming, competition, and playfulness.
If we had more time, we might actually implement the pulley structure, we will also turn this into a desktop projection, rather than on the screen, as we originally proposed.
Technical Documentation
Complete resources and code can be found at:
https://github.com/AlanLechengChao/IxLab/tree/master/Molecular_Soccer
Complete Processing Code:
main program:
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); // debug mode, using the computer keyboard //if (i != 9) { // players[i].reactSize(i == Character.getNumericValue(key) - 1 && keyPressed); //}else{ // players[i].reactSize(0 == Character.getNumericValue(key) && keyPressed); //} // actual arduino mode 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.2); //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]); } } } } }
classes:
class Ball { // base class that then displays the soccer 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)); 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.999); //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; } } 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) { // this function check the postion of the pole and the ball // return true if the ball touches the pole float dist = this.location.dist(other.location); if (dist <= (size + other.size)/2 + 2) { return true; }else{ return false; } } void reactSize(boolean triggerState) { // takes one boolean parameter as the switch if (triggerState){ 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); } } }
You must be logged in to post a comment.