Jump to content

  • Log In with Google      Sign In   
  • Create Account

This article is under review by the community - Current moderation totals:
Mark as peer reviewed: 0 votes
Still needs work: 1 votes (Dave Hunt)

Editor Feedback:
  • Unclear or Incomplete


Like
2Likes
Dislike

Retro Game Programming: How to Make Pong UNDER REVIEW

By Ankur Sheel | Published May 01 2013 11:30 AM in Game Programming

pong classic game c++ directx 3d

Pong is one of the earliest video games to gain popularity. It is a good project to start out with as it has the most basic gameplay. However, it introduces some fundamental concepts which are common to all games.

Who is this article for

You should be familiar with the C++ programming language. You should also know how to integrate third party code. You should also have a basic understanding of a rendering engeine and how the game loop works. This article will cover the gameplay, It will NOT cover the concepts that can be abstracted away by Third party tools/engines.[/note]

Requirements to compile the code

  • C++ compiler (I am using Visual Studio 10, but can use an IDE of your choice)
  • Directx X SDK (June 2010)

Objectives

  • Allow player to move the paddle
  • Check for ball collisions with the paddles and walls
  • Basic AI

Note:  While this project utilizes my custom engine(built on C++ and DirectX), it is possible to extend the logic to other platforms. This project uses 3D models, but you can easily use sprites instead (as long as the engine you are using supports it)


Making the Game


Game Elements


Pong has the following game elements
  • Ball,
  • Left Paddle
  • Right Paddle
  • Top Wall,
  • Bottom Wall,
All the above elements will be represented with the following
  • Initial Position
  • Current Position
  • Model associated with this game element
We will create an array of GameElements that will store the data for each of our game elements

Game.h
class cGame
{
    enum PONGGAMEELEMENTS
    {
        PGE_UNKNOWN = -1,
        PGE_BALL,
        PGE_PADDLE_LEFT,
        PGE_PADDLE_RIGHT,
        PGE_WALL_UP,
        PGE_WALL_DOWN,
        PGE_TOTAL
    }
public:
    // functions and extraneous variables omitted
private:
	cPongGameElement **	 m_ppGameElements; // ptr to the gameelements
};

Note:  In this project (0,0) lies at the center of the screen. I will be using a cGameElementDef structure which will be populated to create the game elements. This will be passed into the Initialize function of the game element and will setup the model and the initial position


void VOnInitialization()
{
    //For the paddles
    cGameElementDef paddleDef;
    paddleDef.strModelName= "cube";
    paddleDef.vPosition= cVector3(m_vScreenTopLeftPos.x, 0.0f, 0.0f);
    m_ppGameElements[PGE_PADDLE_LEFT] = DEBUG_NEW cPaddle();
    m_ppGameElements[PGE_PADDLE_LEFT]->VInitialize(paddleDef);
    
    paddleDef.vPosition= cVector3(m_vScreenBottomRightPos.x, 0.0f, 0.0f);
    m_ppGameElements[PGE_PADDLE_RIGHT] = DEBUG_NEW cPaddle();
    m_ppGameElements[PGE_PADDLE_RIGHT]->VInitialize(paddleDef);
    
    // for the walls
    cGameElementDef wallDef;
    wallDef.strModelName = "cube";
    wallDef.vPosition= cVector3(0, m_vScreenTopLeftPos.y, 0.0f);
    m_ppGameElements[PGE_WALL_UP] = DEBUG_NEW cWall();
    m_ppGameElements[PGE_WALL_UP]->VInitialize(wallDef);
    
    wallDef.vPosition= cVector3(0, m_vScreenBottomRightPos.y, 0.0f);
    m_ppGameElements[PGE_WALL_DOWN] = DEBUG_NEW cWall();
    m_ppGameElements[PGE_WALL_DOWN]->VInitialize(wallDef);
    
    cGameElementDef ballDef;
    ballDef.strModelName = "sphere";
    ballDef.vScale = cVector3(0.5f, 0.5f, 0.5f);
    pGame->m_ppGameElements[pGame->PGE_BALL] = DEBUG_NEW cBall();
    pGame->m_ppGameElements[pGame->PGE_BALL]->VInitialize(ballDef);
}

void VOnUpdate()
{
    for(int i=0; i<PGE_TOTAL; i++)
	{
		if(m_ppGameElements && m_ppGameElements[i])
		{
			m_ppGameElements[i]->OnUpdate(m_pGameTimer->VGetDeltaTime());
		}
	}
}

void Render()
{
    for (int i=0; iPGE_TOTAL; i++)
	{
		if(m_ppGameElements && m_ppGameElements[i])
		{
            // Render the game elements model at the current position
			m_ppGameElements[i]->Render();
		}
	}
}

void Cleanup()
{
    if(m_ppGameElements)
	{
		for (int i=0; i<PGE_TOTAL; i++)
		{
			SafeDelete(&m_ppGameElements[i]);
		}
		SafeDeleteArray(&m_ppGameElements);
	}
}

At this point you should see something similar to the image below

Attached Image: Step1.png

Handling Keyboard Input


For taking player input through the keyboard we will be using the WindowProc callback function. In the code below we use a class function VOnMsgProc to handle the window messages. All we need to do here is check if the appropriate key is pressed and perform the required action. We move the left paddle up/down when W/S is pressed.

bool VOnMsgProc(const AppMsg & msg )
{
	if(!cHumanView::VOnMsgProc(msg))
	{
		if(msg.m_uMsg == WM_KEYDOWN)
		{
			if (msg.m_wParam == VK_ESCAPE  && !IsKeyLocked(VK_ESCAPE))
			{
				// lock the ESC key
				LockKey(VK_ESCAPE);
				PostQuitMessage(-1);
			}
			if (msg.m_wParam == 'S')
			{
				m_pGame->MoveLeftPaddle(true);
			}
			if (msg.m_wParam == 'W')
			{
				m_pGame->MoveLeftPaddle(false);
			}
		}
		else if (msg.m_uMsg == WM_KEYUP)
		{
			if (msg.m_wParam == VK_ESCAPE)
			{
				UnlockKey(VK_ESCAPE);
			}
		}
	}
	return true;
}

Game.cpp
void MoveLeftPaddle(bool bMoveDown)
{
	cPaddle * pPaddle = m_ppGameElements[PGE_PADDLE_LEFT]->CastToPaddle();
	if(pPaddle)
	{
		if (bMoveDown)
		{
			pPaddle->MoveDown(m_pGameTimer->VGetDeltaTime());
		}
		else
		{
			pPaddle->MoveUp(m_pGameTimer->VGetDeltaTime());
		}
	}
}

Paddle.cpp
void MoveDown(const float fElapsedTime)
{
	float fDeltaMovement = m_fMoveFactor * fElapsedTime;
	cVector3 vPredictedPos = GetPosition();
	vPredictedPos.y -= fDeltaMovement;
	SetPosition(vPredictedPos);
}

void MoveUp(const float fElapsedTime)
{
	float fDeltaMovement = m_fMoveFactor * fElapsedTime;
	cVector3 vPredictedPos = GetPosition();
	vPredictedPos.y += fDeltaMovement;
	SetPosition(vPredictedPos);
}

At this point, when you press 'W' the left paddle should move up and when you press 'S' the paddle should move down. But you will notice that if you keep the key pressed, the paddle goes off-screen. This is because we have no collision checks. The next part will constrain the paddle to be on the screen at all times.

Keeping the Paddle on Screen


For all the game elements add a collider to it. For keeping the paddle on the screen we will check if a collision occurs when moving it. If there is a collision, we don't update the paddle's position.

Paddle.cpp
void MoveDown(const float fElapsedTime)
{
	cContact contact;
	float fDeltaMovement = m_fMoveFactor * fElapsedTime;
	shared_ptr const pAABB = IAABB::DuplicateAABB(GetAABB());
	pAABB->VTransalate(cVector3(0, -fDeltaMovement, 0));
	if (!(ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(),
		m_pGame->VGetGameElements()[m_pGame->PGE_WALL_DOWN]->GetAABB(), contact)))
	{
		cVector3 vPredictedPos = GetPosition();
		vPredictedPos.y -= fDeltaMovement;
		SetPosition(vPredictedPos);
	}
}

void MoveUp(const float fElapsedTime)
{
	cContact contact;
	float fDeltaMovement = m_fMoveFactor * fElapsedTime;
	shared_ptr const pAABB = IAABB::DuplicateAABB(GetAABB());
	pAABB->VTransalate(cVector3(0, fDeltaMovement, 0));
	if (!(ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(),
		m_pGame->VGetGameElements()[m_pGame->PGE_WALL_UP]->GetAABB(), contact)))
	{
		cVector3 vPredictedPos = GetPosition();
		vPredictedPos.y += fDeltaMovement;
		SetPosition(vPredictedPos);
	}
}

Moving the Ball


To move the ball, we will give it an initial velocity. On each update cycle we will check if it has collided with the wall or paddles and if so then change its direction and/or speed. If the ball goes off screen, we will restart the level. On restarting the level, we give the ball a random velocity.

Ball.cpp
void VInitialize(const cGameElementDef & def)
{
	cPongGameElement::VInitialize(def);
	m_pRandomGenerator = IRandomGenerator::CreateRandomGenerator();
	
	m_vSpeed = cVector3(static_cast(m_pRandomGenerator->Random(5,10)), static_cast(m_pRandomGenerator->Random(5,10)), 0.0f);
}


void OnRestart()
{
	cPongGameElement::OnRestart();
	m_vSpeed = cVector3(static_cast(m_pRandomGenerator->Random(5,10)), static_cast(m_pRandomGenerator->Random(5,10)), 0.0f);
}	

void OnUpdate(float fElapsedTime)
{
	cContact contact;
	cVector3 vDeltaPos = m_vSpeed * fElapsedTime;
	shared_ptr const pAABB = IAABB::DuplicateAABB(GetAABB());
	pAABB->VTransalate(vDeltaPos);
	cVector3 vPredictedPos = GetPosition();
    
    //check for collision with walls
	if ((ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(), m_pGame->VGetGameElements()[m_pGame->PGE_WALL_DOWN]->GetAABB(), contact))
		|| (ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(), m_pGame->VGetGameElements()[m_pGame->PGE_WALL_UP]->GetAABB(), contact)))
	{
		float nv = m_vSpeed.Dot(contact.vNormal);
		m_vSpeed -= contact.vNormal * 2 * nv;
	}
    
    //check for collision with paddles
	if ((ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(), m_pGame->VGetGameElements()[m_pGame->PGE_PADDLE_LEFT]->GetAABB(), contact))
		|| (ICollisionChecker::GetInstance()->VCheckForCollisions(pAABB.get(), m_pGame->VGetGameElements()[m_pGame->PGE_PADDLE_RIGHT]->GetAABB(), contact)))
	{
		float nv = m_vSpeed.Dot(contact.vNormal);
		m_vSpeed -= contact.vNormal * 2 * nv;
	}
	vPredictedPos = vPredictedPos + vDeltaPos + contact.vDistance;
	SetPosition(vPredictedPos);

    // check if ball is off screen
	if (GetPosition().x VGetScreenTopLeftPos().x)
	{
		cGame * pGame = const_cast(m_pGame);
		pGame->VRoundOver(true);
	}
	else if (GetPosition().x > m_pGame->VGetScreenBottomRightPos().x)
	{
		cGame * pGame = const_cast(m_pGame);
		pGame->VRoundOver(false);
	}
	cPongGameElement::OnUpdate(fElapsedTime);
}

Game.cpp
void VRoundOver(const bool bPlayer1Won)
{
	if (bPlayer1Won)
	{
        // Increment player1 score
	}
	else
	{
		// Increment player2 score
	}
	Restart();
}

Paddle AI


If you are running the game, you will realize it's no fun. There is no one competing with you. The AI does not move the paddle to beat you. So let's go ahead and add a basic AI to play against. Our AI will wait for the ball to come in its half of the screen and then move in the (vertical) direction of the ball

Game.cpp
void VOnUpdate()
{
	//extra code ommitted
	HandlePaddleAI(m_pGameTimer->VGetDeltaTime());
}

void HandlePaddleAI(const float fElapsedTime)
{
	cVector3 vBallPos = m_ppGameElements[PGE_BALL]->GetPosition();
	
    // if the ball is in the players half, there is no need to do anything
	if (vBallPos.x < 0)
	{
		return;
	}
	cVector3 vPaddlePos = m_ppGameElements[PGE_PADDLE_RIGHT]->GetPosition();

	if (vPaddlePos.y - vBallPos.y < 1)
	{
		cPaddle * pPaddle = m_ppGameElements[PGE_PADDLE_RIGHT]->CastToPaddle();
		if(pPaddle)
		{
			pPaddle->MoveUp(fElapsedTime);
		}
		return;
	}
	else if (vBallPos.y - vPaddlePos.y < 1)
	{
		cPaddle * pPaddle = m_ppGameElements[PGE_PADDLE_RIGHT]->CastToPaddle();
		if(pPaddle)
		{
			pPaddle->MoveDown(fElapsedTime);
		}
		return;
	}
}

Running the attached executable


To run the code, You should have Visual Studio 2010 redistributable and DirectX redistributable installed on your machine


Conclusion


This concludes "Retro Games: How to Make Pong". The attached game sample has the basic gameplay. I hope you enjoyed this tutorial and it helped you.

Article Update Log


1 Apr 2013: Initial release
2 Apr 2013: Updated Formatting. Fixed Grammatical Errors
1 May 2013: Added note to highlight scope of the article
29 May 2013: Added "Who is this article for", "Requirements for compiling the code" and "Running the attached executable" heading





License


GDOL (Gamedev.net Open License)




Comments

I think the article is unsuitable for the target audience

 

The dependency on the author's framework introduces a lot of unnecessary complexity into what should be one of the simplest games. The code examples are obscure because so much functionality is assumed.

 

I believe one could write a complete article illustrating the important concepts of a pong game from scratch, including collision detection and rendering, in an article approximately the same size. In my opinion this article is of limited use if the reader is not using the framework.

 

Even from only a quick skim, I would also have to question the quality of the code. There appears to be an infinite loop in the Render() function, and the VOnUpdate() and Cleanup() seem to have wildly inconsistent handing of null elements.

I would have to agree with rip-off, this article is confusing to follow even as an experienced programmer.  Also, you mention that you will not cover collision in this article yet collision is pretty much what pong is all about.  It seems like this article is intended for the new beginner to start their game coding experience but assumes that they are familiar with the complexities of incorporating and compiling third party code into their own project and skips over all of the fundamentals of the game loop itself.

 

Long story short it seems that a better way to write an article of this nature would be to actually start from scratch, explain how to implement a rendering framework (such as Direct X, OpenGL, SDL or XNA / Mono Game).  From there explain a bit about the actual game loop and how to create / implement it into your code.  After that a brief explanation of the basics of collision, some input and that would more adequately teach a newer coder how to actually get the game going from scratch.

Thanks for the feedback rip-off and Dan
@rip-off : The whole point of this article is that it does not depend on any specific engine, but concentrates more on the gameplay. Even if i explained the rendering concepts, they would differ from the API(DIrectX vs OpenGL wars anyone) let alone a specific engine. I felt it would be better to let the reader substitute the framework for any other engine that they might like to use. 
I have updated the article to make the handling of null elements consistent
 
@Dan: Most readers would be using a 3rd party engine. Even if they used DirectX or OpenGL, they would still need to be aware of how to incorporate third party code, They are already lots of collision detection/ rendering tutorials which are articles in their own right.
Teaching how to implement a rendering framework is beyond the scope of this article and would take away from actually building the game. The article also mentions that it assumes the reader has a basic knowledge of the game loop and a rendering engine. I have updated the article to highlight the scope

The game fails to run, with a "stopped working" error, and it changed my desktop resolution to 1280x720 and did not revert it back after the failure. Graphics card is AMD 760G.

Sorry SpeedRun, but I have to agree with rip-off and Dan. Anyone that already has knowledge of rendering frameworks, game loops, collision detection and integrating third party libraries has probably already made their own pong.

 

An article subtitled "How to Make Pong" is clearly geared toward beginning game programmers. To appeal to that audience and make it useful to them, you really need to take them along on a journey from an empty project to a finished game. Keep it simple and provide the basic methods necessary to accomplish the task. Do that and your readers will love you for it and keep coming back for more.

I can't upvote rip-off or Dan Mayor's comments (GD is giving me an error), so I just want to say I agree with them.

 

This article is mostly code with very little explanation, and the code itself isn't very clear and would take a good amount of skill to meaningfully understand it (skill that I doubt those of the target audience would have).

Do you have DirectX and the visual studio 2010 run-times installed?

 

 

The game fails to run, with a "stopped working" error, and it changed my desktop resolution to 1280x720 and did not revert it back after the failure. Graphics card is AMD 760G.

Thanks for the feedback Dave and CornStalks
 
Since the popular consensus seems to be that the article is very complex for beginners, I intend to break it down in parts and explain the basics. I will also try to remove the framework dependencies as far as possible.

Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS