Punch Hockey

By Mallika Sen and Shurjo Banerjee

Description

Punch Hockey is a virtual game design that emulates Air Hockey.

Air Hockey is a two-player game wherein each player must try to score points in the other player's goal on a frictionless table. The equipment required for this game consists of two mallets(held by players) and a puck. Our version of the game involves allowing users to simulate the experience of playing Air Hockey while holding controllers.

Punch Hockey is a game that allows the users to control the virtual puck by gesture-dependant controllers. Stronger gestures amount to faster shots. In addition to the two-player option, there is a single-player option that allows the user to play against the computer's AI.

User-Interface

The user-interface for the game is shown below.

/media/uploads/Mallika/screen_shot_2014-12-10_at_2.02.08_am.png

Controller

The mbed-NXP-LPC1768 was used as the main processing unit in this wired game, whereas the LSM303D 3D Compass and Accelerometer was used for the controller.

/media/uploads/Mallika/screen_shot_2014-12-10_at_2.08.40_am.png

The LSM303D combines a digital 3-axis accelerometer and 3-axis magnetometer into a single package that is ideal for making a tilt-compensated compass. The six independent readings, whose sensitivities can be set in the ranges of ±2 to ±16 g and ±2 to ±12 gauss, are available through I²C and SPI interfaces. This LSM303 carrier board includes a 3.3 V voltage regulator and integrated level shifters that allows operation from 2.5 to 5.5 V, and the 0.1″ pin spacing makes it easy to use with standard solderless breadboards and 0.1″ perfboards.

The C++ code was run in Visual Studio 10.

Code

Code

#include "airHockey.h"
int global_x = 0; int global_y = 0;
int global_x2 = 0; int global_y2 = 0;

void airHockey::setup(){
	
	//CT.setup(); //Color Tracker initialized
	avgConstant = 4;

	// Set stuff to zero
	hitCounter = playerScore = opponentScore = screenshotCounter = 0;
	goalDisplayCounter = 0;

	ballPos.x = ballPos.y = 0;
	ballPosOld = ballPosAvg = ballPos;
	gameComplete = goal = false;

	// Assign colors
	ofBackground(252,250,230);   

	puck.color[0] = 43;
	puck.color[1]= 74;
	puck.color[2] = 228;

	playerBat.color[0] = 40;
	playerBat.color[1] = 242;
	playerBat.color[2] = 28;

	opponentBat.color[0] = 240;
	opponentBat.color[1] = 27;
	opponentBat.color[2] = 23;

	// Add balls to balls array. Yes.
	balls[0] = &puck; 
	balls[1] = &playerBat;
	balls[2] = &opponentBat;
	playerBat.radius = 30;
	opponentBat.radius = 30;


	// set ball parameters.
	puck.mass = 6;
	puck.radius = 20;
	puck.maximum_velocity = 10;
	puck.location.set(40 + fWidth/2, 40 + fHeight + 50,0.0f);
	puck.velocity.set(0,0,0);
	playerBat.mass = opponentBat.mass = 10;
	playerBat.maximum_velocity = opponentBat.maximum_velocity = 6;
	playerBat.location.set(40 + fWidth/2, 40 + 2*fHeight/3 + fHeight ,0.0f);
	opponentBat.location.set(40 + fWidth/2, 40 + fHeight/3,0.0f);

	positionArrayTop = -1; positionArrayBottom = 5;
	for(int i = 0; i < 3; i++)
		for(int j = 0;j < 6; j++)
			positionArr[i][j] = 0;

	// load fonts and sound
	verdana.loadFont("verdana.ttf",12, true, true);
	verdana.setLineHeight(20.0f);
	magneto.loadFont("magnetob.ttf",66, true, true);
	magneto.setLineHeight(30.0f);
	bahaus.loadFont("bauhs93.ttf",35, true, true);
	bahaus.setLineHeight(30.0f);
	soundHit.loadSound("ballHit.wav");

	serial.setup("\\\\.\\COM23",9600);

	for (int i=0; i<10; i++)
	{
		global_x += readAccelerometer('a');
		global_y += readAccelerometer('b');
		global_x2 += readAccelerometer('m');
		global_y2 += readAccelerometer('n');
	}

	global_x /= 100;
	global_y /= 100;
	global_x2 /= 100;
	global_y2 /= 100;
}

int airHockey::readAccelerometer(char t)
{
	
	serial.writeByte(t);
	string num = "";
	while (!(serial.available())); 
	while((serial.available()>0))
	{
		num += serial.readByte();
	}

	return atoi(num.c_str());

}


void airHockey::draw(){

	ofEnableSmoothing();

	// DRAW ARENA
	ofSetColor(0);
	ofRect(40, 40, fWidth+1, wHeight+1);
	//ofRect(20+fWidth+40, 40, fWidth+1, fHeight+1);
	ofLine(40,240+40,fWidth+40,240+40);
	//ofCircle(40+fWidth/2, 40 + fHeight,50);
	ofBeginShape();     
	for(int i = 0; i < 100; i++) {   
		float angle = i*3.1415926/100;   
		ofVertex(40 + fWidth/2 + (cos(angle) * 50), 40 + (sin(angle) * 50));  
	}   
	ofEndShape();   
	ofBeginShape();
	for(int i = 0; i < 100; i++) {   
		float angle = 2*i*3.1415926/100;   
		ofVertex(40 + fWidth/2 + (cos(angle) * 50), 40 +1+ fHeight+(sin(angle) * 50));  
	}   
	ofEndShape();   

	ofBeginShape();     
	for(int i = 0; i < 100; i++) {   
		float angle = i*3.1415926/100;   
		ofVertex(40 + fWidth/2 + (cos(angle) * 50), 40 + wHeight + 1+(sin(-angle) * 50));  
	}
	ofEndShape();   

	ofFill();
	ofEnableAlphaBlending();
	ofSetColor(playerBat.color[0], playerBat.color[1], playerBat.color[2], 150);
	ofRect(40 + fWidth/2 - 50, 40 - 18, 100,15);
	ofSetColor(opponentBat.color[0], opponentBat.color[1], opponentBat.color[2], 150);
	ofRect(40 + fWidth/2 - 50, 40 + wHeight + 4, 100,15);
	ofNoFill();

	// DRAW INFO TEXT

	ofSetColor(0xffffff);
	//displayImg.draw(40, 40, fWidth, fHeight);
	//displayImg.mirror(false,true);
	//displayImg.draw(40,40 + fHeight, fWidth, fHeight);
	//displayImg.draw(20+fWidth+40,40, fWidth, fHeight);
	ofSetColor(0x60dd47);   
	magneto.drawString("Punch\n\n\nHockey",40+fWidth+20, fHeight);

	verdana.drawString("Shurjo Banerjee\nMallika Sen",40+fWidth+20, fHeight+160);
	
	
	if(!gameComplete)    
	{
		ofSetColor(0x5555FF);    
		verdana.drawString(reportStr,40+fWidth+190, fHeight + 260);
		sprintf(reportStr,"Press 'R' to start new match");
		verdana.drawString(reportStr,40+fWidth+85, fHeight + 280);
		ofSetColor(playerBat.color[0], playerBat.color[1], playerBat.color[2],150);
		sprintf(reportStr,"%d",playerScore);
		bahaus.drawString(reportStr,40 + fWidth - 40, 70 + fHeight + 20);
		ofSetColor(opponentBat.color[0], opponentBat.color[1], opponentBat.color[2],150);
		sprintf(reportStr,"%d",opponentScore);
		bahaus.drawString(reportStr,40 + fWidth - 40, 60 + fHeight - 43);
	}

	//ofDrawBitmapString(reportStr, 60+fWidth+40, fHeight + 140);
	if(ballPos.x >= 0 && ballPos.y >=0 )
	{

		ofFill();
		ofEnableAlphaBlending();
		ofSetColor(250,0,0,80);
		//ofCircle(40+ fWidth + 20+ballPos.x,40+ballPos.y,10);        
		//drawTrails();
		ofDisableAlphaBlending();
		ofNoFill();
		if(!true)
		{
			ofSetColor(0x5555FF);
			sprintf(reportStr, "Player Paddle Position: (%d,%d)", ((int)playerBat.location.x - 15),((int)playerBat.location.y-fHeight - 18));        
			verdana.drawString(reportStr,40+fWidth+40,fHeight+240);
		}

	}

	// DRAW STUFF FROM COLOR TRACKER

	/*for (int i = 0; i < contourFinder.nBlobs; i++)
	contourFinder.blobs[i].draw(20+fWidth+(40),40);  */

	// DRAW BATS AND PUCK
	if(!gameComplete)
	{
		drawBat(puck);
		drawBat(playerBat);
		drawBat(opponentBat);
		if(goalDisplayCounter < 30 && goalDisplayCounter>0)
		{

			sprintf(reportStr, "Goal!");
			ofSetColor(0x5555FF);
			bahaus.drawString(reportStr,140,fHeight+50);            
			goalDisplayCounter++;                           
			if(goal) ofSleepMillis(800);
		}
		else
			goalDisplayCounter = 0;
	}
	else
	{
		// Game Over.
		if(playerScore==maxScore)
			sprintf(reportStr, "  Game Over!\n\n   You Win! ");
		else
			sprintf(reportStr, "  Game Over!\n\nComputer Wins! ");
		if(playerScore>opponentScore)
			ofSetColor(playerBat.color[0], playerBat.color[1], playerBat.color[2],150);
		else
			ofSetColor(opponentBat.color[0], opponentBat.color[1], opponentBat.color[2],150);
		bahaus.drawString(reportStr, 35,fHeight+ 20);
		sprintf(reportStr,"%d - %d", playerScore, opponentScore);
		bahaus.drawString(reportStr, 143,fHeight + 130);
		sprintf(reportStr, "Press ENTER to start a new match");
		ofSetColor(0x5555FF);
		verdana.drawString(reportStr,50,fHeight+ 180);
	}
}

void airHockey::update(){
	// update everything
	//ballPos = CT.getLocation();
	//ballPos.x += 40;
	//ballPos.y += 270 + playerBat.radius;
	pushPosition();

	checkCollision();
	if(goal) goal = false;
	checkGoal();
	puck.updateBall();
	updatePlayerBat();
	updateOpponentBat();   
}

void airHockey::checkGoal()
{
	//ofxVec3f gPlayer(40+fWidth/2, gOpponent;
	if(puck.location.x > (fWidth/2 + 40 - 50) && puck.location.x < (fWidth/2 + 40 + 50))
	{
		if(puck.location.y < 40 + 3)
		{
			//Player Scored
			puck.location.set(40 + fWidth/2, 40 + fHeight - 50,0.0f);
			puck.velocity.set(0,0,0);
			opponentBat.location.set(40 + fWidth/2, 40 + fHeight/3,0.0f);    
			if(!gameComplete)
			{
				playerScore++;
				goal = true;
				goalDisplayCounter++;
			}

		}
		else if(puck.location.y > 40 +wHeight - 3)
		{    
			puck.location.set(40 + fWidth/2, 40 + fHeight + 50,0.0f);
			puck.velocity.set(0,0,0);
			opponentBat.location.set(40 + fWidth/2, 40 + fHeight/3,0.0f);    
			if(!gameComplete)
			{
				opponentScore++;
				goal = true;
				goalDisplayCounter++;
			}
			//Computer scored
		}
	}
	if(playerScore==maxScore||opponentScore == maxScore)
	{
		gameOver();
	}

}

void airHockey::gameOver()
{
	gameComplete = true;
}

void airHockey::updatePlayerBat(){
	Ball &b = playerBat;
	/*if(CT.contourFinder.nBlobs)
	{        
		b.location.set(ballPosAvg.x, ballPosAvg.y,0);
		b.velocity.set((ballPosAvg.x - ballPosOld.x)*2.0,(ballPosAvg.y - ballPosOld.y)*2.0,0);
		ballPosOld = ballPosAvg;
	}*/

	int x = (readAccelerometer('a')-global_x)/500;
	int y = (readAccelerometer('b')-global_y)/500;
	b.velocity.set(x,-y,0);
	b.updateBall();


	if(b.velocity.x > b.maximum_velocity)
		b.velocity.x = b.maximum_velocity * (b.velocity.x>0?1:-1);
	if(b.velocity.y > b.maximum_velocity)
		b.velocity.y = b.maximum_velocity * (b.velocity.y>0?1:-1);

	if (b.location.y > (wHeight - b.radius +40) ) {
		b.velocity.y *= -b.bounce;
		b.location.y = wHeight- b.radius +40;
	}
	if(b.location.y  < (40 + 240 + b.radius)) {
		b.velocity.y *= -b.bounce;
		b.location.y = 40 + b.radius+240;
	}
	if (b.location.x > (fWidth - b.radius + 40)) {
		b.velocity.x *= -b.bounce;
		b.location.x = fWidth - b.radius + 40;
	}
	if (b.location.x < (40+b.radius)) {
		b.velocity.x *= -b.bounce;
		b.location.x = b.radius+40;
	}

}


// Really lame AI stuff done here, could someone help me out please?
void airHockey::updateOpponentBat()
{   
	Ball &b = opponentBat;

	if (true) {
	int x = (readAccelerometer('m')-global_x)/500;
	int y = (readAccelerometer('n')-global_y)/500;
	b.velocity.set(x,-y,0);
	b.updateBall();


	if(b.velocity.x > b.maximum_velocity)
		b.velocity.x = b.maximum_velocity * (b.velocity.x>0?1:-1);
	if(b.velocity.y > b.maximum_velocity)
		b.velocity.y = b.maximum_velocity * (b.velocity.y>0?1:-1);

	if (b.location.y > (wHeight/2 - b.radius +40) ) {
		b.velocity.y *= -b.bounce;
		b.location.y = wHeight/2 - b.radius +40;
	}
	if(b.location.y  < (40 + b.radius)) {
		b.velocity.y *= -b.bounce;
		b.location.y = 40 + b.radius;
	}
	if (b.location.x > (fWidth - b.radius + 40)) {
		b.velocity.x *= -b.bounce;
		b.location.x = fWidth - b.radius + 40;
	}
	if (b.location.x < (40+b.radius)) {
		b.velocity.x *= -b.bounce;
		b.location.x = b.radius+40;
	}
	}

	else
	{
	
		if(puck.location.y < 40 + fHeight) //If puck inside opponent area
		{
			ofxVec3f direction;
			direction = puck.location - b.location;    

			if(puck.location.y > 40 + fHeight/4)// && (puck.location.x> 40 + fWidth -30 || puck.location.x < 40 + 30)) //Attack puck
			{
				if(sqrt(direction.length()) < 7)//Puck close enough to be hit
				{
					if(!gameComplete)soundHit.play();
					direction /= (sqrt(direction.length()));
					puck.velocity = (direction)*(opponentBat.velocity.length()*0.5);//4
					opponentBat.velocity = puck.velocity * (-5);
					hitCounter++;
					if(hitCounter > 20)
					{
						//ofxVec3f tmp;
						//tmp = puck.location;
						if(opponentBat.location.x < 40 + fWidth/2)
							opponentBat.location.set(40 + 40, 40 + 10,0);
						else
							opponentBat.location.set(fWidth, 40 + 10,0);
						puck.velocity.set(0,8,0);
						hitCounter = 0;
						//printf("Damn! Stuck.\n");
					}

				}
				direction /= (sqrt(direction.length()));
				b.velocity = 1.8 * direction;
				/*				
				0.9 - Easy
				1.2 - Medium
				1.8 - You nuts bro?
				*/
				b.location += b.velocity;
			}
			else // defend goal
			{
				//30-290
				//map(30,290,110,210)
				float k = puck.location.x - 40;
				k = (k - 30)*(210-110)/(290-30) + 110;
				//velo
				printf("%f\n",k);
				//b.location.set(k,65,0);
			}

		}
		else
		{
			// Keep goofing around.
			if(puck.velocity.length())
			{    
				b.velocity.set(puck.velocity.x*0.4,puck.velocity.y*0.4,0);
				/*else if(playerBat.velocity.length() && 
				(playerBat.location.x > 40 + playerBat.radius && playerBat.location.y < 40 + playerBat.radius + fWidth))
				{        
				b.velocity.set(playerBat.velocity.x*0.4,playerBat.velocity.y*0.4,0);
				}*/
				//b.location.set(40 + fWidth/2, 40 + fHeight/3,0.0f);
				b.location += b.velocity;
				b.location.y = 40 + fHeight/3;
			}
		}
	}
}

void airHockey::drawBat(Ball &b)
{

	ofSetCircleResolution(60);
	ofSetColor(b.color[0], b.color[1], b.color[2],150);
	ofNoFill();
	ofCircle(b.location.x, b.location.y, b.radius);
	ofCircle(b.location.x, b.location.y, b.radius+1);
	ofFill();
	ofEnableAlphaBlending();
	ofCircle(b.location.x, b.location.y, b.radius);
	ofDisableAlphaBlending();	
	ofSetColor(0xffffff);
	if(b.radius < 15)
		ofCircle(b.location.x, b.location.y,6);    
	else
		ofCircle(b.location.x, b.location.y,8);    
	ofNoFill();
	/*sprintf(reportStr, "Actual Co-ords.: (%d,%d)", (int)b.location.x,(int)b.location.y);        
	ofSetColor(0,0,255);
	ofDrawBitmapString(reportStr,20+fWidth+40,fHeight+180);*/
}


// Stores the position values in an array(circular queue) and
// Smoothens out the position of blob by averaging.
void airHockey::pushPosition()
{
	if(positionArrayTop >= 5) positionArrayTop = 0;
	else positionArrayTop ++;
	if(positionArrayBottom >=5) positionArrayBottom = 0;
	else positionArrayBottom ++;
	positionArr[positionArrayTop][0] = ballPos.x;
	positionArr[positionArrayTop][1] = ballPos.y;
	ballPosAvg.x = ballPosAvg.y = 0;   
	for(int i = 0; i < 2; i++)
		for(int j = 0; j < avgConstant; j++)
		{
			if(i == 0)
				ballPosAvg.x += positionArr[j][i];
			else 
				ballPosAvg.y += positionArr[j][i];
		}
		ballPosAvg.x /= (float)avgConstant;    
		ballPosAvg.y /= (float)avgConstant;
}

// Draw trails behind the puck. Turned out to be stupid.
void airHockey::drawTrails()
{
	ofFill();
	ofEnableAlphaBlending();
	ofSetColor(250,0,0,60);    

	for(int i = positionArrayTop, j = 0; j < 2; j++)
	{
		ofCircle(40+ fWidth +positionArr[i][0],40+positionArr[i][1] - 260,10);        
		if(i == 5) 
			i = 0;
		else 
			i++;        
	}
	ofSetColor(0,0,255);
	ofLine(ballPosAvg.x + 40 + fWidth,40, ballPosAvg.x + 40 + fWidth, 40 + fHeight);
	ofLine(40 + fWidth + 20,40+ballPosAvg.y - fHeight-20,40+2*fWidth+20,40+ballPosAvg.y-fHeight-20);
	ofDisableAlphaBlending();
	ofNoFill();
}

// No more needed, but still left it since it had taken me some time to figure
// this one out.
void airHockey::getThresholdImg(ofxCvColorImage img){
	/*
	cvInRangeS operates on pixel arrays. So we'll be creating
	two IplImage pointers. hsvImage stores the camera frame
	in HSV scale. The thresholded image is stored in imgTh.
	thImg stores the value of imgTh as a ofxCvGrayscaleImage.
	Note the use of converting ofximage to IplImage and vice-versa.
	*/
	hsvImg = img;
	hsvImg.convertRgbToHsv();    
	IplImage* imgHsv = hsvImg.getCvImage();    
	IplImage* imgTh = cvCreateImage(cvGetSize(imgHsv), 8, 1);
	cvInRangeS(imgHsv,cvScalar(7, 85, 135), cvScalar(23, 255, 255),imgTh);  
	//cvInRangeS(imgHsv,cvScalar(10, 150, 150), cvScalar(23, 255, 255),imgTh);  
	//hue - 18-44 sat - 160-255 val - 160-219. HSV Values for orange ball
	//Note - ofxOpenCv hue range is [0,180], i.e half of the normal(?) [0,360] range
	int widthImage = imgTh->width;
	int heightImage =imgTh->height;
	thImg.setFromPixels((unsigned char*)imgTh->imageData,widthImage,heightImage);    
}

void airHockey::checkCollision()
{
	int i, j;
	for(i=0; i<numBalls; i++)
	{
		for(j=i+1; j<numBalls; j++)
		{
			if(balls[i]->location.x+2*balls[i]->radius > balls[j]->location.x && balls[i]->location.x < balls[j]->location.x+2*balls[i]->radius)
			{
				if (balls[i]->location.y+2*balls[i]->radius > balls[j]->location.y && balls[i]->location.y < balls[j]->location.y+2*balls[i]->radius)
				{
					balls[i]->collision(balls[j]);
				}
			}
		}
	}
}

void airHockey::mousePressed(int x, int y, int button)
{
	
}

void airHockey::mouseReleased(int x, int y, int button)
{
	  
}

void airHockey::mouseDragged(int x, int y, int button)
{
	
}

// Press R to reset the game. Enter to continue.
void airHockey::keyPressed  (int key){
	//Enter - 13, Esc - 33
	if(key == 'R' || key == 'r')
	{
		resetGame();
	}
	if(gameComplete && key == OF_KEY_RETURN)
		resetGame();
}


void airHockey::resetGame()
{
	// Restart teh game.
	avgConstant = 3;
	hitCounter = playerScore = opponentScore = goalDisplayCounter = 0;
	goal = false;
	ballPos.x = ballPos.y = 0;
	ballPosOld = ballPosAvg = ballPos;
	gameComplete = false;    
	puck.location.set(40 + fWidth/2, 40 + fHeight + 50,0.0f);
	puck.velocity.set(0,0,0);    
	playerBat.location.set(40 + fWidth/2, 40 + 2*fHeight/3 + fHeight ,0.0f);
	opponentBat.location.set(40 + fWidth/2, 40 + fHeight/3,0.0f);    
	positionArrayTop = -1; positionArrayBottom = 5;
	for(int i = 0; i < 3; i++)
		for(int j = 0;j < 6; j++)
			positionArr[i][j] = 0;
}
int main( ){

	ofAppGlutWindow window;    
	ofSetupOpenGL(&window, 2*fWidth + 100,fHeight+325, OF_WINDOW);

	// can be OF_WINDOW or OF_FULLSCREEN
	// pass in width and height too:
	ofRunApp( new airHockey());
}

Demonstration

Below is a demonstartion of the single-player version.


Please log in to post comments.