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.
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
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
};
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)
{
m_ppGameElements->OnUpdate(m_pGameTimer->VGetDeltaTime());
}
}
}
void Render()
{
for (int i=0; iPGE_TOTAL; i++)
{
if(m_ppGameElements && m_ppGameElements)
{
// Render the game elements model at the current position
m_ppGameElements->Render();
}
}
}
void Cleanup()
{
if(m_ppGameElements)
{
for (int i=0; i);
}
SafeDeleteArray(&m_ppGameElements);
}
}
At this point you should see something similar to the image below
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
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.