Arduino Ultrasonic Pong

> 8 min read

Using the prototype created as the next iteration of the ultrasonic keyboard,  I decided to repurposed it into something more other than tone generation, and this is when the idea of using the ultrasonic sensor as a game interface came about.

The Classical Pong

This is one of the earliest arcade game that is created in the 70s, whereby it is a simple game that simulates table tennis represented in simple 2D graphics (of rectangles & circle). The game consists of 2 rectangular paddles (which are located on the sides of the screen), with 2 players controlling one padfle each. The main objective of the game is straightforward: to reach 11 points first. Points are awarded when the other player fails to return the ball to the opposite player. (When the ball hits the left/right edge of the screen.)

Overview

Since it is a simple & graphic light, I decided to implement this game on a microcontroller (Maker Uno), using the ultrasonic sensor as a control interface to move the paddle.

Pong is typically played via keyboard, mouse, but for our project, the ultrasonic sensor is used instead.

As the game is between 2 players, there needs to be 2 controllers for each players. But, for this project, the second player would be substituted with a simple AI instead of another human player. One less hardware controller!

The scores would be reset when one of the player reaches the winningScore. (Which defaults at 11)

It would have another interesting feature: when the ultrasonic sensor does not detect a distance within the stipulated range, it would move by itself.

Hardware

The hardware used would be from the previous ultrasonic sensor keyboard project, consisting of the Maker Uno Board, ultrasonic sensor, and a 0.96″ OLED Display.

Software

The software of the project would be broken down into a few aspects.

“Distance-position” translation: Paddle control

The ultrasonic sensor would be used to control the left paddle, which represents the main player. (or Player One) The distance read by the ultrasonic sensor would first be remapped/translated within the game constrain, which would then be translated into the position of the paddle in the game.

Raw Ultrasonic Reading -> Map data to game constrain -> Move Paddle

The ultrasonic sensor would only move if it’s within a range from 10cm to 40cm, else the paddle would move by itself with respect to the ball’s y-axis position.

Within the range, the distance would be remapped to the constrain of the game (would be using screen height as a limit) of 0 to 64.

Graphics

We would be using the u8g2 graphics library, which would be used to draw the game interface. This would include erawing rectangles (paddle and ball) and numbers (for scoring).

The scores would be located at the center top of the screen, and the paddles would have a fixed x-axis that is 2/5 screen width away from the screen center.

uint8_t areaw = WD * 4 / 5; //Take up 4/5 of screen
...
uint8_t p1x = (WD / 2) - areaw / 2; //Paddle 1 x loc, fixed

Game Physics

The game elements needs to know how to interact with one another: such as when the ball meets the paddle, hits the sides of the screen, etc. This is when a physics engine comes in (collision detection) , followed by the game control (ball motion, paddle control).

The collision engine would dictate the behaviour and response of the various game elements, such as the paddles and the ball. It comprises of a 2 parts:

  • Paddle collision
  • Ball collision

The paddle collision detection would limit the paddle movement within the limits stipulated (the screen), and ensures that the paddles would remain within the screen.

The ball collision detection would be implemented to respond to the following scenarios:

  • Paddle contact detection
  • Screen contact detection

For paddle contact detection, the position of the ball (offset with it’s width/2) would be compared to the position of the paddle (offset by the width/2 of the paddle).

  • If the ball “hits” the front of the paddle, the (x-axis) direction of the ball would be reflected and would continue moving in another direction.
  • If the sides of the paddle (top/bottom) are hit, the ball y-axis direction would be flipped.

Likewise for the screen detection, the ball position (with offset) would be compared to the screen position (with offset).

  • If the ball hits the top/bottom of the screen, the ball (y-axis) direction would be flipped.
  • If it hits the left/right sides of the screen, the player would earn a point accordingly and the game would be reset for a new round.

Scoring system

When the ball hits the left or right side of the screen, the player would did not managed to prevent the ball from hiting the screen would not score a point, whereas the opposite player would earn a point. After score addition, the game would reset to start a new round.

Ball spawning

At the start of the game (or when the game is reset), the ball would start at the center of the screen and is assigned a random speed.

Player Two

Player 2 would be controlled by simple AI, which moves according to the (y-axis) position of the ball. (Is it even considered an AI? In some way yes, I think.) The sensitivity of the paddle movement would be determined by the paddle speed variable, p2sy. (Default value: 2)

Game control/mode

There would be 2 modes: Idle mode & Active mode.

Idle mode: The paddle (Player 1) would play automatically if the ultrasonic sensor readings does not fall between the activation range specified.

//Active range,cm
uint8_t distMin = 10;
uint8_t distMax = 40;

Active mode: The sensors reading falls between the specified constrain. The position of the paddle would be determined by the distance read by the ultrasonic sensor.

Demo

 

Issues

There are a few issues with the program, mainly the collision detection algorithm.

  • Ball gets stuck in the paddle
  • Ball bounces of the left/right side of the ball
  • At certain angle, the ball fails to detect the paddle and goes straight through it (which is rather frustrating to the player)

Another issue would be the ultrasonic sensor: one may experience that the paddle would move on it’s own as the player would accidentally exit the “control” range.

Conclusion

While the main goal of the project is about interfacing the ultrasonic sensor, it was the game implementation that would take up most of the time for the project. (The hardware section is really straightforward)

Through this project, one would have also learnt how to implement a simple game: collision detection of the game elements, ball and paddle behaviour, etc.

Improvement could be implemented in the future such as an improved collision detection system, including a time idle delay for the activation of automatic control of Player 1’s paddle, and many more.

Till then, have fun and enjoy the game!

Code

Version 1.0

The code is available at Github: https://github.com/1487quantum/ard-ultrasonic-pong/tree/old

/*
  Arduino Ultrasonic Pong
  >> The classic pong game that uses an ultrasonic sensor to move the paddle.
     Powered by Arduino & u8g2 lib.
  By: 1487Quantum (https://github.com/1487quantum)
*/
#include 

#ifdef U8X8_HAVE_HW_I2C
#include 
#endif

//Pins
#define USPIN 9         //Ultrasound

//Display
#define WD 128
#define HG 64

U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE);

//Ultrasound
long duration, dist;
//Active range,cm
uint8_t distMin = 10;
uint8_t distMax = 40;

//Ball
uint8_t bw = 4;   //Width & height same

//Ball coordinate
uint8_t bx = WD / 2;
uint8_t by = HG / 2;

//Ball Speed
uint8_t bsx = 3;
uint8_t bsy = 3;

//Arena: Define paddle loc
uint8_t areaw = WD * 4 / 5; //Take up 4/5 of screen

//Paddle global
//Size
uint8_t pw = bw;  //Paddle width = ball width
uint8_t ph = 5 * bw;  //Paddle 5x bigger than ball

//Paddle 1
//Loc
uint8_t p1x = (WD / 2) - areaw / 2; //Paddle 1 x loc, fixed
uint8_t p1y = 10;                   //Paddle 1 y loc,centre
//Spd
uint8_t p1sy = 3;                   //Move up/down

//Paddle 2
//Loc
uint8_t p2x = (WD / 2) + areaw / 2; //Paddle 2 x loc, fixed
uint8_t p2y = 10;                   //Paddle 2 y loc,centre
//Spd
uint8_t p2sy = 2;                   //Move up/down

//Scoring
uint8_t p1Score = 0;          //Player 1 Score
uint8_t p2Score = 0;          //Player 1 Score
uint8_t winningScore = 11;    //Score to reach to win, keep it max at 99 (else text offset needs to be changed)

//Activation range for controls
bool activeRange() {
  return  dist >= distMin && dist <= distMax ;
}

void movPaddle() {
  //Control paddle 1 via distance
  if (activeRange()) {
    p1y = map(dist, distMin, distMax, 0, HG);   //Remap distance to screen height (max)
  } else {
    if (by < p1y) {
      p1y -= p1sy;
    } else {
      p1y += p1sy;
    }
  }


  if (by < p2y) { p2y -= p2sy; } else { p2y += p2sy; } //Paddle collision if (p1y + ph > HG) {
    p1y = HG - ph;
  } else if (p1y < 0) { p1y = 0; } if (p2y + ph > HG) {
    p2y = HG - ph;
  } else if (p1y < 0) { p2y = 0; } } //Collision //Width Limit:Check whether ball is touching paddle x aixs bool withinX(uint8_t xpos) { return bx + bw >= xpos && bx <= xpos + pw; } //Height Limit:Check whether ball is touching paddle y aixs bool withinY(uint8_t ypos) { return by + bw >= ypos && by <= ypos + ph;
}

//Check for paddle front collision, isFirst true: Check collision for first paddle (Left)
bool fCollision(bool isFirst) {
  if (isFirst) {
    return bx <= p1x + pw && withinY(p1y); } else { return bx + bw >= p2x && withinY(p2y);
  }
}

//Check for paddle top/down collision, isFirst true: Check collision for first paddle (Left), if top true: check top
bool tpCollision(bool isFirst, bool top) {
  if (isFirst) {
    return top ? (by + bw >= p1y) : (by <= p1y + ph) && withinX(p1x); } else { return top ? (by + bw >= p2y) : (by <= p2y + ph) && withinX(p2x);
  }
}

void rdmSpd() {
  bsx = random(1, 5);
  bsy = random(1, 5);
}

void updatePos() {
  //Left,right collision
  if (bx - bw - 2 <= 0 || bx + bw / 2 >= WD)  {
    if (bx - bw - 2 <= 0) { p2Score++; rdmSpd(); if (p2Score >= winningScore) {
        p2Score = 0;
      }
    } else {
      p1Score++;
      rdmSpd();
      if (p1Score >= winningScore) {
        p1Score = 0;
      }
    }
    bx = WD / 2;
    by = HG / 2;
  } else if (fCollision(1) || fCollision(0)) {
    bsx = -bsx;
  }
  if (by - bw / 2 <= 0 || by + bw / 2 >= HG ) {
    // Top,Bottom edge collision
    bsy = -bsy;
  }

  bx += bsx;
  by += bsy;

  movPaddle();
}

void drawBall() {
  updatePos();
  u8g2.drawBox(bx, by, bw, bw);
}

void drawPaddle() {
  u8g2.drawBox(p1x, p1y, pw, ph);
  u8g2.drawBox(p2x, p2y, pw, ph);
}

void drawText() {
  char sc1[3];  //P1
  char sc2[3];  //P2
  char dst[8];  //Dst
  sprintf (sc1, "%02d", p1Score);   //Displays 2 digits, shows 0 when score is below 10
  sprintf (sc2, "%02d", p2Score);
  sprintf (dst, "D:%dcm", dist);

  //Scores
  u8g2.setFont(u8g2_font_fub17_tr);
  u8g2.drawStr((WD * 1 / 3) - 14, (HG / 2 ) - 12, sc1);
  u8g2.drawStr((WD / 2) - 3, (HG / 2 ) - 12, ":");
  u8g2.drawStr((WD * 2 / 3) - 8, (HG / 2 ) - 12, sc2);

  u8g2.setFont(u8g2_font_6x12_t_symbols);
  u8g2.drawStr(WD / 3, HG * 5 / 6 , dst);

}

void getDist() {
  // establish variables for duration of the ping,
  // and the distance result in inches and centimeters:

  // The PING))) is triggered by a HIGH pulse of 2 or more microseconds.
  // Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
  pinMode(USPIN, OUTPUT);
  digitalWrite(USPIN, LOW);
  delayMicroseconds(2);
  digitalWrite(USPIN, HIGH);
  delayMicroseconds(5);
  digitalWrite(USPIN, LOW);

  // The same pin is used to read the signal from the PING))): a HIGH
  // pulse whose duration is the time (in microseconds) from the sending
  // of the ping to the reception of its echo off of an object.
  pinMode(USPIN, INPUT);
  duration = pulseIn(USPIN, HIGH);

  // convert the time into a distance
  dist = microsecondsToCentimeters(duration);
}

long microsecondsToCentimeters(long microseconds)
{
  // The speed of sound is 340 m/s or 29 microseconds per centimeter.
  // The ping travels out and back, so to find the distance of the
  // object we take half of the distance travelled.
  return microseconds / 29 / 2;
}

void splash() {
  u8g2.setFont(u8g2_font_t0_17_tr  );
  u8g2.drawStr((WD / 2) - 17, (HG / 2 ) + 5, "PONG");
  u8g2.sendBuffer();
  delay(3000);    //Show for 2s
}

void setup() {
  // put your setup code here, to run once:
  DDRD |= 0b11111110; // set digital  1,2- 7 to output
  DDRB = 0b00111111; // set digital  8-13 to output

  //For Maker Uno: Turn off all onboard LEDS
  PORTD &= 0;
  PORTB &= 0;

  u8g2.begin();
  u8g2.setFlipMode(0);      //To flip display
  splash();                 //Show splashscreen
}

void loop() {
  u8g2.clearBuffer();
  drawText();
  drawBall();
  getDist();
  drawPaddle();
  u8g2.sendBuffer();
}

Version 1.1 Update

The updated version of the code is found here: https://github.com/1487quantum/ard-ultrasonic-pong

New features include:

  • Beep on collision
  • Game Over screen at the end of the game, whereby the game would reset after 5 seconds.
  • Attributes/variables related to the player’s score, paddle location is transferred to a class (Player.cpp).

 

You may also like...