Introduction: HariFun #136 - How to Write a Game

I wonder how many of you remember Pong? According to Wikipedia, it came out in 1972. So it would be unfair to compare this with today's Call of Duty or GTA.

Actually what got me started on this project was not the original pong, but the one I saw on Reddit.

That it inspired me to make my own version. My goal is to make it as simple as possible so anyone can make it, learn from it, and hopefully even create their own variations. If you make one, please link to it in comments, I'd love to see your versions!

Let's get started!

Step 1: Parts List & Schematic

As you can see, it has very few parts:

  • 1 x Nokia LCD Display ($2.50 each)
  • 1 x 1K resistor for the LCD light (pennies)
  • 2 x 10K Linear Potentiometers (do NOT use Logarithmic for this project, $1 each)
  • 1 x Arduino (any model will do)
  • Piezo beeper (optional, but recommended)

And supporting parts:

  • Breadboard
  • Jumper wires
  • Battery holder and power switch (optional if you just want to power it off the USB cable)

Step 2: Get the LCD to Work

The awesome thing about the Arduino is that there is usually a nice set of libraries already written for almost anything you'd like to connect to it. I'm using the u8glib to drive the LCD. This library allows us to concentrate on our game rather than on how to control the LCD.

In the Arduino IDE, install the u8glib library:
Sketch > Include Library > Manage Libraries > enter u8glib > Click to select then click Install.

Let's start with an example:
File > Examples > u8glib > HelloWorld

The u8g library supports an insanely long list of display varieties! It even drives some printers!
So, the first thing we need to do is tell the library which LCD screen we have. Google showed me that my Nokia 5110 uses a PCD8544 chip to draw the LCD, so I uncommented this line:
U8GLIB_PCD8544 u8g(13, 11, 10, 9, 8); // SPI Com: SCK = 13, MOSI = 11, CS = 10, A0 = 9, Reset = 8

The SCK and MOSI lines MUST be connected to 13 and 11. However, the other pins could be any digital Arduino pins.

For aesthetic reasons, I decided to line up the LCD pins with the Arduino pins and modified the parameters to:
U8GLIB_PCD8544 u8g(13, 11, 9, 8, 10); // SPI Com: SCK = 13, MOSI = 11, CS = 9, A0/DataCommand = 8, Reset = 10

However, you could keep the same code, just make sure that wire up the pins accordingly.

Upload the sketch by clicking on the arrow next to the checkmark. You can also use the keyboard shortcut Control/Command - U.

You should see, surprise surprise... Hello World.

Step 3: Learning to Draw

Now that we know the LCD works, let's learn how to draw a box.
Depending on the orientation of your screen you may or may not need to call the setRot command.
drawBox() is what draws the 4 by 4 box on the screen. As you can see, coordinate 0,0 is the top left of the screen.

// Arduino Pong v0.0 by Hari Wiguna, 2016

#include "U8glib.h" U8GLIB_PCD8544 u8g(13,11, 9,8,10); // SPI Com: SCK = 13, MOSI = 11, CS = 9, A0/DataCommand = 8, Reset = 10 void setup(void) { u8g.setRot180(); // flip screen (if necessary) } void loop(void) { u8g.firstPage(); do { u8g.drawBox(0,0, 4,4); } while ( u8g.nextPage() ); }

Step 4: Animating the Ball

To move the ball, we'll need to change it's position.

  • ballX and ballY keeps track of the top left corner of the ball.
  • ballDirectionX and ballDirectionY keeps track of whether the ball is moving left/right and up/down.
  • animationSpeed is how often the ball moves (lower value = faster animation).
  • courtWidth and courtHeight is set by calling the library.
// Arduino Pong by Hari Wiguna, 2016
// v0.0 - draw ball // v0.1 - move ball // v0.2 - using millis(), move in Y direction too #include "U8glib.h" U8GLIB_PCD8544 u8g(13, 11, 9, 8, 10); // SPI Com: SCK = 13, MOSI = 11, CS = 9, A0/DataCommand = 8, Reset = 10 //== Game Variables == u8g_uint_t courtWidth, courtHeight; // How wide and tall is our screen? u8g_uint_t ballSize = 4; u8g_uint_t ballX; u8g_uint_t ballDirectionX = 1; u8g_uint_t ballY; u8g_uint_t ballDirectionY = 1; unsigned long timeToMove; // Is it time to move the ball? int animationSpeed = 20; void MoveBall() { // millis is how long since we power up the Arduino. if (millis() > timeToMove) { // Is it time to move the ball? if so, compute new ball position. ballX += ballDirectionX; if (ballX >= (courtWidth - ballSize) || ballX <= 0) ballDirectionX = -ballDirectionX;</p><p> ballY += ballDirectionY; if (ballY >= (courtHeight - ballSize) || ballY <= 0) ballDirectionY = -ballDirectionY;</p><p> timeToMove = millis() + animationSpeed; // Set the next time we'll need to move the ball again. } } void setup(void) { u8g.setRot180(); // flip screen (if necessary) courtWidth = u8g.getWidth(); courtHeight = u8g.getHeight(); } void loop(void) { u8g.firstPage(); do { u8g.drawBox(ballX, ballY, ballSize, ballSize); MoveBall(); } while ( u8g.nextPage() ); }

OMG, we got Pong running in just a few minutes! Imagine the complex circuit and programming that they had to do back in 1972! However, to make it playable, we'll need a way to control the paddles...

Step 5: Adding the Paddles

For those new to electronics, these adjustable resistors are called potentiometers.

I prefer to have the knob facing up, so I soldered some leads on the potentiometer. If you do this, make sure to use insulated wires because the potentiometer body is metal and could easily short circuit the leads.

Alternatively, you could just plug the potentiometer leads onto the breadboard. If you do this, I recommend bending the leads 90 degrees so they would not spread the breadboard leads too much. (See photo)

We wire it up as a resistor ladder. One lead goes to ground, the other to +5V and the middle movable lead goes to an Arduino Analog Pin.

On one extreme, the output will be 5V, on the other extreme, the output will be 0V, anywhere in between we'll have a value between 0 and 5V.

We can read this voltage using using analogRead(). The value returned by analogRead is not a fractional value 0 through 5. Instead it returns an integer between 0 and 1023. To map this range into the actual range that corresponds to the paddle vertical position, we'll use the Arduino map() function.

paddleYposition = map( potentiometerValue, 0,1023, 0,36).

Repeat for the other side and we almost wrote a playable game!
The ball still bounces even when it misses the ball. Let's fix that.

// Arduino Pong by Hari Wiguna, 2016
// v0.0 - draw ball // v0.1 - move ball // v0.2 - using millis(), move in Y direction too // v0.3 - Paddles! #include "U8glib.h" //== Preferences == U8GLIB_PCD8544 u8g(13, 11, 9, 8, 10); // SPI Com: SCK = 13, MOSI = 11, CS = 9, A0/DataCommand = 8, Reset = 10 // Analog pins where we connect the potentiometers to int paddle0Pin = A1; int paddle1Pin = A0; //== Game Variables == u8g_uint_t courtWidth, courtHeight; u8g_uint_t ballSize = 4; u8g_uint_t ballX; u8g_uint_t ballDirectionX = 1; u8g_uint_t ballY; u8g_uint_t ballDirectionY = 1; u8g_uint_t paddleWidth = 2; u8g_uint_t paddleHeight = 8; unsigned long timeToMove; int animationSpeed = 20; void MoveBall() { if (millis() > timeToMove) { ballX += ballDirectionX; if (ballX >= (courtWidth - ballSize) || ballX <= 0)ballDirectionX = -ballDirectionX; ballY += ballDirectionY; if (ballY >= (courtHeight - ballSize) || ballY <= 0) ballDirectionY = -ballDirectionY; timeToMove = millis() + animationSpeed; } } void DrawPaddle(u8g_uint_t paddleX, int paddlePin) { int analogValue = analogRead(paddlePin); // returns 0 through 1023 // Convert analogValue ranging from 0..1023 to paddleY ranging from 0..(courtHeight-paddleHeight) u8g_uint_t paddleY = map(analogValue, 0,1023, 0,courtHeight-paddleHeight); // Draw the paddle u8g.drawBox(paddleX, paddleY, paddleWidth, paddleHeight); } void DrawPaddles() { DrawPaddle(0, paddle0Pin); DrawPaddle(courtWidth - paddleWidth, paddle1Pin); } void setup(void) { u8g.setRot180(); // flip screen courtWidth = u8g.getWidth(); courtHeight = u8g.getHeight(); } void loop(void) { u8g.firstPage(); do { MoveBall(); u8g.drawBox(ballX, ballY, ballSize, ballSize); DrawPaddles(); } while ( u8g.nextPage() ); }

Step 6: Collision Detection

The last thing we need to do is to detect whether the ball hits the paddle or not.

We know that player missed the ball when the bottom of the ball is above the top of the paddle, or the top of the paddle is beyond the bottom of the paddle.

We know that the ball will never hit the paddles unless the ball is on the rightmost or leftmost X, so we only need to check paddles in those two scenarios.

bool MissedPaddle(u8g_uint_t py)<br>{
  u8g_uint_t ballTop = ballY;
  u8g_uint_t ballBottom = ballY + ballSize - 1;
  u8g_uint_t paddleTop = py;
  u8g_uint_t paddleBottom = py + paddleHeight - 1;
  return ballBottom < paddleTop || ballTop > paddleBottom;
}

When someone missed a ball, we'll move the ball to the other side, play a missed ball tone, and increment the other player's score. When score reaches winning score, it's game over :-)

void Player0Missed()<br>{
  // When left player missed, move the ball just to the left of rightmost of court
  ballX = courtWidth - ballSize - 1;
  ballY = paddle1Y + paddleHalfHeight; // ball will be served at location of player 1's paddle
  tone(tonePin, missToneFrequency, missToneDuration);
  delay(1000);
  score1++;
  animationSpeed = animationSpeed0;
  if (score1 == winningScore) gameOver = true;
}

void Player1Missed()<br>{
  // When right player missed, move the ball just to the right of the leftmost of court
  ballX = 1;
  ballY = paddle0Y + paddleHalfHeight; // ball will be served at location of player 0's paddle
  tone(tonePin, missToneFrequency, missToneDuration);
  delay(1000);
  score0++;
  animationSpeed = animationSpeed0;
  if (score0 == winningScore) gameOver = true;
}

Step 7: Scoring & Court

Unfortunately there is no drawChar(), so we have to use draw string.

There are many ways to convert a number to an array of characters, but since I only need single digit 0 through 9, I decided the simplest way would be to reserve a two character array (the digit's ASCII representation plus terminating null).

Let's use a smaller font. Oh, check out the number of supported fonts! https://github.com/olikraus/u8glib/wiki/fontsize

void DrawScores(){
  char strScore0[] = "?"; // Sets string length to 1
  char strScore1[] = "?";
  strScore0[0] = '0' + score0; // Overide the string value with single digit score
  strScore1[0] = '0' + score1;
  u8g.setFont(u8g_font_04b_03b);
  u8g_uint_t scoreWidth = u8g.getStrPixelWidth(strScore0);
  const int offset = 5;
  u8g_uint_t scoreY = 9;
  u8g.drawStr( halfCourtWidth - offset - scoreWidth, scoreY, strScore0);
  u8g.drawStr( halfCourtWidth + offset, scoreY, strScore1);
}

And to finish it off, let's draw the tennis court. It's made up of two horizontal lines (one at the very top of the screen, the other at the very bottom of the screen) and a vertical dashed line in the middle of the screen.

void DrawCourt()
{
  u8g.drawHLine(0, 0, courtWidth);
  u8g.drawHLine(0, courtHeight - 1, courtWidth);
  byte dash = 3;
  for (byte y = 0; y < (courtHeight / dash / 2); y++)
  {
    u8g.drawVLine(halfCourtWidth - 1, 2 + y * dash * 2, dash);
  }
}

Step 8: Closing Comments

I hope you enjoyed this video as much as I did sharing it. If you enjoy it, don't forget to give thumbs up and maybe subscribe/follow so you won't miss future videos/instructables.

I love reading comments so please write a comment with your pong memories. If you make this project, I'd love to see it. Please drop a link in comments or click the I made it button.

If you're into social media, please share this instructable and/or YouTube Video.
Thank you, thank you, thank you!

Complete Sketch is below

// Arduino Pong by Hari Wiguna, 2016
// v0.0 - draw ball // v0.1 - move ball // v0.2 - using millis(), move in Y direction too // v0.3 - Paddles! // v0.4 - Collision Detection // v0.5 - Scoring, Speed up, and Sound

#include "U8glib.h"

//== Preferences == U8GLIB_PCD8544 u8g(13, 11, 9, 8, 10); // SPI Com: SCK = 13, MOSI = 11, CS = 9, A0/DataCommand = 8, Reset = 10 int paddle0Pin = A1; // Left player potentiometer int paddle1Pin = A0; // Right player potentiometer int winningScore = 3; // How high before we declare a winner? byte tonePin = 2; // Which digital pin the beeper is attached to int animationSpeed0 = 25; // Initial speed of each round (lower = faster)

//== Game Variables == u8g_uint_t courtWidth, courtHeight, halfCourtWidth; u8g_uint_t ballSize = 4; u8g_uint_t ballX; u8g_uint_t ballDirectionX = 1; u8g_uint_t ballY; u8g_uint_t ballDirectionY = 1; u8g_uint_t paddleWidth = 2; u8g_uint_t paddleHeight = 8; u8g_uint_t paddleHalfHeight = paddleHeight/2; u8g_uint_t paddle0Y; // Left player vertical paddle position u8g_uint_t paddle1Y; // Right player vertical paddle position

int score0, score1; // Left & Right player's scores bool gameOver = false;

int bounceToneFrequency = 523; int bounceToneDuration = 62; int missToneFrequency = 523 / 2; int missToneDuration = 512;

unsigned long timeToMove; // When should we move the ball again? int animationSpeed = animationSpeed0; // Current ball speed (lower = faster)

bool MissedPaddle(u8g_uint_t py) { u8g_uint_t ballTop = ballY; u8g_uint_t ballBottom = ballY + ballSize - 1; u8g_uint_t paddleTop = py; u8g_uint_t paddleBottom = py + paddleHeight - 1; return ballBottom < paddleTop || ballTop > paddleBottom; }

void DrawScores() { char strScore0[] = "?"; // Sets string length to 1 char strScore1[] = "?"; strScore0[0] = '0' + score0; // Overide the string value with single digit score strScore1[0] = '0' + score1; u8g.setFont(u8g_font_04b_03b); u8g_uint_t scoreWidth = u8g.getStrPixelWidth(strScore0); const int offset = 5; u8g_uint_t scoreY = 9; u8g.drawStr( halfCourtWidth - offset - scoreWidth, scoreY, strScore0); u8g.drawStr( halfCourtWidth + offset, scoreY, strScore1); }

void DrawGameOver() { u8g.setFont(u8g_font_timB18); u8g.setFontPosCenter(); // vertical alignment char gameStr[] = "Game"; char overStr[] = "Over"; u8g_uint_t gx = (courtWidth - u8g.getStrPixelWidth(gameStr)) / 2; u8g_uint_t ox = (courtWidth - u8g.getStrPixelWidth(overStr)) / 2; u8g.drawStr(gx, 20, gameStr); u8g.drawStr(ox, 40, overStr); }

void DrawCourt() { u8g.drawHLine(0, 0, courtWidth); u8g.drawHLine(0, courtHeight - 1, courtWidth); byte dash = 3; for (byte y = 0; y < (courtHeight / dash / 2); y++) { u8g.drawVLine(halfCourtWidth - 1, 2 + y * dash * 2, dash); } }

void Player0Missed() { // When left player missed, move the ball just to the left of rightmost of court ballX = courtWidth - ballSize - 1; ballY = paddle1Y + paddleHalfHeight; // ball will be served at location of player 1's paddle tone(tonePin, missToneFrequency, missToneDuration); delay(1000); score1++; animationSpeed = animationSpeed0; if (score1 == winningScore) gameOver = true; }

void Player1Missed() { // When right player missed, move the ball just to the right of the leftmost of court ballX = 1; ballY = paddle0Y + paddleHalfHeight; // ball will be served at location of player 0's paddle tone(tonePin, missToneFrequency, missToneDuration); delay(1000); score0++; animationSpeed = animationSpeed0; if (score0 == winningScore) gameOver = true; }

void BounceX() { tone(tonePin, bounceToneFrequency, bounceToneDuration); ballDirectionX = -ballDirectionX; animationSpeed--; // Speed up game with each bounce }

void MoveBall() { if (millis() > timeToMove) { ballX += ballDirectionX; if (ballX <= 0) if (MissedPaddle(paddle0Y)) Player0Missed(); else BounceX();

if (ballX >= (courtWidth - ballSize)) if (MissedPaddle(paddle1Y)) Player1Missed(); else BounceX();

ballY += ballDirectionY; if (ballY >= (courtHeight - ballSize) || ballY <= 0) { ballDirectionY = -ballDirectionY; animationSpeed--; // Speed up game with each bounce tone(tonePin, bounceToneFrequency, bounceToneDuration); }

timeToMove = millis() + animationSpeed; } }

void DrawPaddle(u8g_uint_t paddleX, int paddleY) { u8g.drawBox(paddleX, paddleY, paddleWidth, paddleHeight); }

void DrawPaddles() { paddle0Y = map(analogRead(paddle0Pin), 0, 1023, 0, courtHeight - paddleHeight); paddle1Y = map(analogRead(paddle1Pin), 0, 1023, 0, courtHeight - paddleHeight);

DrawPaddle(0, paddle0Y); DrawPaddle(courtWidth - paddleWidth, paddle1Y); }

void setup(void) { u8g.setRot180(); // flip screen courtWidth = u8g.getWidth(); courtHeight = u8g.getHeight(); halfCourtWidth = courtWidth / 2; }

void loop(void) { u8g.firstPage(); do {

if (gameOver) DrawGameOver(); else MoveBall();

DrawCourt(); DrawScores(); u8g.drawBox(ballX, ballY, ballSize, ballSize); DrawPaddles(); } while ( u8g.nextPage() ); }

DIY Summer Camp Challenge

Participated in the
DIY Summer Camp Challenge