Created 2 April 2009, updated 1 Nov 2020
When I start learning a new platform, I have a simple rule: If you don’t know what to do with it, make pong. What I love about pong is that it’s a simple rule set, easy to understand, and implementable on just about anything with a pixel display. You can generally implement it in a day or less on any platform. And it’s a great example of engaging interaction. People understand what’s going on right away, and, when implemented well, it’s just challenging enough to keep you engaged for several minutes at least. That’s good interaction, to me.
I’m a big believer in starting with the application rather than the platform. I think you do better work when the tools serve the need rather than the other way around. But sometimes you get stuck with the assignment to learn a particular platform or tool, and you have to make up a project on the spot. When that happens, make pong.
As an example of this, I built pong for two platforms yesterday [1 April 2009]: an Arduino Mega with 2 8×8 LED matrices (based on my earlier post), and Processing. Since Arduino’s programming syntax was based closely on Processing’s, I figured it should be possible to port the code from one to the other pretty quickly. It took about ten minutes to go from Arduino to Processing. In 2020, I updated this exercise to write the program in p5.js as well. Following, I’ll describe the thought process of putting the game together for all three, as a hopeful aid to beginning programmers.
Arduino Mega pong
This all started because I had two 8×8 matrices of LEDs being controlled by an Arduino Mega. Whee! The excitement lasted about 30 seconds, then I wanted it to do something. Inspired by Maywa Denki‘s Bitman, JT Nimoy’s MiniPong (JT has died, sadly, and I can no longer find this link), and many others, I started on pong.
Step 1: what happens?
I start by breaking the problem into specific actions:
- the paddles have to move up and down.
- the ball bounces around the screen. When it hits an edge, it bounces. Specifically:
- when it hits the top or bottom, it changes direction vertically
- when it hits the left or right, a point has been scored. The ball resets to center
- the default X velocity and Y velocity is 1 pixel per frame.
- when the ball resets, it pauses for a second so users know a new volley is about to begin
To keep it simple, I’m not going to implement scoring, reset, or anything else. Just hitting or missing the ball is engaging, and lets me learn the platform I’m working on well enough.
In order to make this happen, I need a display and two inputs, one for each paddle. If the inputs are digital (i.e. pushbuttons or keyboard keys), the paddle will move one pixel for each button press. If they are analog, I’ll map the input range to the movement range of the paddles.
To keep troubleshooting simple, I’ll break the program down into a few basic routines:
- read the sensors
- move the ball
- refresh the screen
Before diving into the details of the code, take a look at the video linked from Figure 1, to give you an idea of what happens:
Step 2: what does it look like?
When programming an animation, whether it’s a game or a simple movement, it’s useful to use an offscreen buffer to hold the image, make any changes in the buffer each time something changes, then move the stuff to the screen to update the image. This means you can easily re-use the code from one platform to another. All the major changes will be in the routine that refreshes the screen. To that end, my code starts with some constants and variables to describe the screen:
const int LEFT = 0;
const int RIGHT = 15;
const int TOP = 0;
const int BOTTOM = 7;
Next, a two-dimensional array the size of the screen, 16 pixels wide by 8 pixels high:
int pixels[16][8]; // 2-dimensional array of pixels
This buffer array is just a space in memory, so it doesn’t matter whether I’m writing the programming for Arduino, Processing, or any other platform, I can still use the buffer. I just need to change the data types to match the platform’s types. I can still use the definitions too, though the syntax will change from one programming language to another.
Now, I need to get specific to the hardware, specifically the Arduino Mega and the LEDs. My pixel array for the Arduino Mega pong is a pair of 8×8 LED matrices that looks like Figure 2:
Because of the way the pins on the matrices are laid out and the way I programmed them in my previous example, the LED grid was laid out as shown above, with (0,0 ) on the bottom right and (15, 7 ) on the top left. But most screen-based computers lay out the pixel grid the opposite way, with (0,0) on the top left, as shown in Figure 3:
In order to keep things consistent, I should flip the grid.
The way I control the matrices is by turning on and off the row and column pins in order to light up the LEDs (more details can be found in the previous post). Because of that, flipping the grid is simply a matter of rearranging the arrays of pin numbers. In the previous example, I had a pair of two-dimensional arrays of row and column pin numbers, two arrays with eight pins each. The order went like this:
columns[2][8] = {{array 1}, {array 2}};
and more specifically:
int rows[] = {
{22,23,27,49,28,45,43,25}, {9,8,4,35,3,10,11,6} };
int cols[] = {
{47,41,51,39,26,53,24,29}, {34,12,36,13,5,37,7,2}};
To flip the grid, I ended up with this arrangement:
columns[2][8] = {{array 2}, {array 1}};
I also had to reverse the order of the pins in each array. The final arrangement is:
// 2-dimensional array of row pin numbers (for two matrices):
int row[2][8] = {
{6, 11, 10, 3, 35, 4, 8, 9}
,
{25, 43, 45, 28, 49, 27, 23, 22}
};
// 2-dimensional array of column pin numbers (for two matrices):
int col[2][8] = {
{ 2, 7, 37, 5, 13, 36, 12, 34}
,
{29, 24, 53, 26, 39, 51, 41, 47}
};
Step 3: what are the main variables?
With the screen in order, it’s time to lay in a few global variables. These variables define the animation, specifically the position and velocity of the ball and the paddles, the speed of the game (timeStamp and interval between frames) and the state of the game (paused or in motion):
int ballX = 8; // X position of the ball
int ballY = 4; // Y position of the ball
int ballDirectionY = 1; // X direction of the ball
int ballDirectionX = 1; // Y direction of the ball
int rightPaddleY = 0; // X position of the center of the right paddle
int leftPaddleY = 0; // Y position of the center of the right paddle
long timeStamp = 0; // time stamp to control the pauses between ball moves
long interval = 120; // interval between ball moves, in milliseconds
boolean gamePaused = false; // state of the game
These variables all describe the game, but nothing specific to the hardware, so they should work on any platform that I write pong on.
Step 4: set things up
The setup()
method sets up the initial conditions for the program, as always. For the Arduino Mega pong, I need to do two things: initialize the pins that control the LED display, and fill the buffer array with initial values. Initializing the pins is specific to Arduino, but filling the buffer will work for both platforms. Here’s what the setup looks like for the Mega:
void setup() {
// initialize the I/O pins as outputs:
// iterate over the two matrices:
for (int thisMatrix = 0; thisMatrix < 2; thisMatrix++) {
// iterate over the pins:
for (int thisPin = 0; thisPin < 8; thisPin++) {
// initialize the output pins:
pinMode(col[thisMatrix][thisPin], OUTPUT);
pinMode(row[thisMatrix][thisPin], OUTPUT);
// take the col pins (i.e. the cathodes) high to ensure that
// the LEDS are off:
digitalWrite(col[thisMatrix][thisPin], HIGH);
}
}
// initialize the pixel matrix:
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 8; y++) {
pixels[x][y] = HIGH;
}
}
}
Step 5: controlling the screen
Before getting down to the action, it’s useful to make sure you can control the screen. I usually do this by turning on all the pixels, then turning them off. My Mega example does just that. Take a look at the refreshScreen()
method from that code, it’s identical in this program. Later on you’ll see a similar method for Processing. This method does all the actual work of turning on and off pixels. It reads the buffer array, iterates over the pixels on the screen, and turns them on or off depending on the state of the corresponding pixel in the buffer.
This method is dependent on the hardware, because it’s what controls the actual display! Here’s what it looks like:
void refreshScreen() {
// iterate over the matrices:
for (int thisMatrix = 0; thisMatrix < 2; thisMatrix++) {
// iterate over the rows (anodes):
for (int thisRow = 0; thisRow < 8; thisRow++) {
// take the row pin (anode) high:
digitalWrite(row[thisMatrix][thisRow], HIGH);
// iterate over the cols (cathodes):
for (int thisCol = 0; thisCol < 8; thisCol++) {
// get the state of the current pixel;
int thisPixel = pixels[thisRow + (thisMatrix * 8)][thisCol];
// when the row is HIGH and the col is LOW,
// the LED where they meet turns on:
digitalWrite(col[thisMatrix][thisCol], thisPixel);
// turn the pixel off:
digitalWrite(col[thisMatrix][thisCol], HIGH);
}
// take the row pin low to turn off the whole row:
digitalWrite(row[thisMatrix][thisRow], LOW);
}
}
}
Step 6: the main loop
The main loop describes what happens over and over. Rather than put all the action here, I use it as a place to describe what happens at an abstract level, and do the real work in the methods that it calls. The main loop should look like the basic interaction description as much as possible. It should also be portable from one platform to another. Remember that basic interaction loop described above:
- read the sensors
- move the ball
- refresh the screen
Here’s a more detailed description:
- read the sensors
- if the game is paused
- wait for an appropriate interval
- if the game’s not paused
- move the ball
- refresh the screen
In order to pause the ball while still allowing the paddles to move, and in order to control the speed of the ball, I use a timestamp to keep track of time intervals. I use these all the time to control timing in my code. They work like this:
if (millis() - timeStamp > interval) {
// do something
// then update the timestamp:
timeStamp = millis()
}
millis()
returns the number of milliseconds since the program started in Processing and Arduino; nearly every programming environment has a similar command. The interval variable is simply how many milliseconds you want to pass between each action. Once you’ve done the action, you update the timeStamp with the current time. The main loop checks this statement every time through the loop, but only takes action when the time interval has passed. You can see this block of code in two places below, once to control the speed of the ball movements, and once to control the pause state of the game. In the latter case, the timeStamp update is elsewhere in the code.
Here’s what the main loop looks like:
void loop() {
// read input:
readSensors();
// move the ball:
if (gamePaused) {
if (millis() - timeStamp > interval * 10) {
// if enough time has passed, start the game again:
gamePaused = false;
}
}
// if the game isn't paused, and enough time between ball moves
// has passed, move the ball and update the timestamp:
else {
if (millis() - timeStamp > interval) {
moveBall();
timeStamp = millis();
}
}
// draw the screen:
refreshScreen();
}
Step 7: read the sensors
The details of this step depends on the hardware, but the basic algorithm is the same regardless of platform. It works like this:
- make sure you’re done with the old sensor readings
- get the new readings
- do something with the new readings
It’s pretty normal that you’re going to make things happen based on the change in the sensor readings rather than their actual values at a given instant, so you usually need the old readings and the new ones in order to compare them. In this program, the sensor readings are mapped to the paddle positions, so I make sure the old paddle positions are turned off before I get readings, then I read the sensors and turn the paddles back on with the new readings.
The sensors for the Arduino Mega pong were two potentiometers, so reading them was simple: do an analogRead()
and map the results to the range of the paddles’ movement. You can see it in the middle of the following block.
Here’s what the code looks like:
void readSensors() {
// set the left paddle to off:
setPaddle(LEFT, leftPaddleY, HIGH);
// set the right paddle to off:
setPaddle(RIGHT, rightPaddleY, HIGH);
// read the sensors for X and Y values:
leftPaddleY = map(analogRead(0), 0, 1023, 0, 7);
rightPaddleY = map(analogRead(1), 0, 1023, 0, 7);
// set the left paddle to on:
setPaddle(LEFT, leftPaddleY, LOW);
// set the right paddle to on:
setPaddle(RIGHT, rightPaddleY, LOW);
}
You can see it calls a method called setPaddle()
which is the next step. When I wrote the code, I didn’t fill in the details of setPaddle()
until later, I just gave it a name so I knew it needed to happen.
Step 8: set the paddles
setPaddle()
turns on or off the pixels in the buffer array depending on the sensor readings passed to it. This isn’t dependent on the hardware, because all I’m doing is manipulating values in the buffer array. I pass in the position in the array where the center of the paddle should be, and the state of the paddle (off or on). The method itself also checks to see if the pixel above or below the center needs to be turned on. If the center is at the edge of the screen, I only turn on two pixels, but if it’s in the center of the screen, I turn on three. Here’s the code:
void setPaddle(int paddleX, int paddleY, int state) {
// set the last right paddle to on:
pixels[paddleX][paddleY] = state;
// set the bottom pixel of the paddle:
if (paddleY < BOTTOM) {
pixels[paddleX][paddleY + 1] = state;
}
// set the top pixel of the paddle:
if (paddleY > TOP) {
pixels[paddleX][paddleY - 1] = state;
}
}
Step 9: move the ball (and set it)
The moveBall()
method, like the setPaddles()
method, is not dependent on the hardware, because all it does is manipulate a pixel in the buffer array. It checks to see if the ball’s position is by an edge of the screen, or by a paddle, and if it is, it changes the ball’s direction appropriately. If the ball has gone off the screen, it resets it to the center and pauses the game. This signals the main loop to start counting down time until it can unpause the game.
Though this method is longer than the others, it’s not really more complex. It’s just a series of if statements to check the position of the ball pixel. Like the setPaddle()
method, it makes sure to turn off the previous position of the ball before turning on the new position:
(Note: as of this writing, there’s a bug with the Syntax Highlighter plugin for WordPress, so please substitute & for & in the code below)
void moveBall() {
// check to see if the ball is in the horizontal range
// of the paddles:
// right:
if (ballX >= RIGHT - 1) {
// if the ball's next Y position is between
// the top and bottom of the paddle, reverse its X direction:
if ((ballY + ballDirectionY >= rightPaddleY - 1)
&& (ballY + ballDirectionY <= rightPaddleY + 1)) {
// reverse the ball horizontal direction:
ballDirectionX = -ballDirectionX;
}
}
// left:
if (ballX <= LEFT + 1) {
// if the ball's next Y position is between
// the top and bottom of the paddle, reverse its X direction:
if ((ballY + ballDirectionY >= leftPaddleY - 1 )
&& (ballY + ballDirectionY <= leftPaddleY + 1 )) {
// reverse the ball horizontal direction:
ballDirectionX = -ballDirectionX;
}
}
// if the ball goes off the screen bottom,
// reverse its Y direction:
if (ballY == BOTTOM) {
ballDirectionY = -ballDirectionY;
}
// if the ball goes off the screen top,
// reverse its X direction:
if (ballY == TOP) {
ballDirectionY = -ballDirectionY;
}
// clear the ball's previous position:
pixels[ballX][ballY] = HIGH;
// if the ball goes off the screen left or right:
if ((ballX == LEFT) || (ballX == RIGHT)) {
// reset the ball:
ballX = 8;
ballY = 4;
// pause and note the time you paused:
gamePaused = true;
timeStamp = millis();
}
// increment the ball's position in both directions:
ballX = ballX + ballDirectionX;
ballY = ballY + ballDirectionY;
// if the game isn't paused, set the ball
// in its new position:
if (!gamePaused) {
// set the new position:
pixels[ballX][ballY] = LOW;
}
}
Step 10: play!
That’s it! Here’s the full Arduino Mega Sketch.
But wait! There’s more! Next comes the Processing pong. If you want to compare them side-by-side as you read, here’s theProcessing Sketch link.
Processing pong
Because the control of the hardware is separate from the control of the animation and the reading of the sensors, it’s pretty easy to move this program over to Processing. I often work on both platforms, developing ideas on one, then porting them to the other, depending on which is easier to start the idea on.
The Processing pong is going to look like this:
Step 1: review the code and hardware dependencies
Here are the methods from the program, in summary:
setup() – initializes the I/O pins, fills the display buffer array
loop() – reads the sensors, updates the buffer array, refreshes the screen
readSensors() – reads the sensors, maps them to the paddle positions in the buffer array
setPaddle() – updates the paddle positions in the buffer array
moveBall() – updates the ball position in the buffer array
refreshScreen() – updates the LED display
Of these, only three methods — setup()
, readSensors()
, and refreshScreen()
— deal with hardware. The rest manipulate memory. Those methods, and the global variables, are the only things I need to change in order to port this to Processing. To start. select all, and paste it into a new Processing sketch.
Step 2: change the global variables
Processing doesn’t use const ints, so I have to change the defines to regular integer variables, like so:
// define the edges of the screen:
int LEFT = 0;
int RIGHT = 15;
int TOP = 0;
int BOTTOM = 7;
I also need to add two new variables, HIGH and LOW, since those constants aren’t known to Processing. I want HIGH to mean off and LOW to mean on, like it did in the Arduino sketch, so I’ll set the values like so:
int HIGH = 0;
int LOW = 255;
I don’t need the row and column arrays, so they can be deleted. Also, Processing’s syntax for two-dimensional arrays is different than Arduino’s. The former is based on Java, and the latter on C. So I’ll have to change the declaration of pixels[][]
like so:
int pixels[][] = new int[16][8]; // 2-dimensional array of pixels
The rest of the global variables can stay the same. Here they are again:
int ballX = 8; // X position of the ball
int ballY = 4; // Y position of the ball
int ballDirectionY = 1; // X direction of the ball
int ballDirectionX = 1; // Y direction of the ball
int rightPaddleY = 0; // X position of the center of the right paddle
int leftPaddleY = 0; // Y position of the center of the right paddle
long timeStamp = 0; // time stamp to control the pauses between ball moves
long interval = 120; // interval between ball moves, in milliseconds
boolean gamePaused = false; // state of the game
Step 3: change the setup
The hardware-dependent part of the setup()
is the block that iterates over the matrices and initializes the pins. Replace that with a few commands to set up the Processing window, turn on smoothing, and set the frame rate. The resulting setup()
looks like this:
void setup() {
// initialize the I/O pins as outputs:
size(420, 220);
smooth();
frameRate(30);
// initialize the pixel matrix:
for (int x = 0; x < 16; x++) {
for (int y = 0; y < 8; y++) {
pixels[x][y] = HIGH;
}
}
}
Step 4: change the main loop
The main loop is identical, because it’s not hardware dependent. But Processing calls its main loop draw()
so change the name to draw.
Step 5: change the refreshScreen()
The refreshScreen()
method is probably the most hardware dependent, so it’ll need the most change. Instead of turning on LEDs, I’ll draw an array of circles, and change the color of each circle based on the corresponding element of the buffer array. It’s similar to the Arduino refeshScreen()
in that it iterates over the buffer array and makes changes to the display depending on the values in that array:
void refreshScreen() {
// make the screen black:
background(0);
// iterate over the buffer array:
for (int x = 0; x < 16; x++) {
// determine the circle's X position onscreen:
int circleX = (x + 1) * 24;
for (int y = 0; y < 8; y++) {
// determine the circle's Y position onscreen:
int circleY = (y + 1) * 24;
// get the color of the circle from the buffer array:
fill(pixels[x][y]);
// draw the circle:
ellipse(circleX, circleY, 20, 20);
}
}
}
Step 6: change the readSensors()
The desktop computer’s easiest inputs to work with are the keyboard and the mouse, of course. Since I don’t have two mice, I’ll use the keyboard as the input sensors. I’ll use four keys, two for up and down on the left (‘a’ and ‘z’) and two for up and down on the right (‘l’ and’,’). So instead of analogRead()
, the Processing readSensors()
will have to listen for keyboard input. I’ll add a method to parse the keys if the key is pressed. Here’s the new readSensors()
:
void readSensors() {
// set the left paddle to off:
setPaddle(LEFT, leftPaddleY, HIGH);
// set the right paddle to off:
setPaddle(RIGHT, rightPaddleY, HIGH);
// read the keyboard for X and Y values:
if (keyPressed) {
keyRead();
}
// set the left paddle to on:
setPaddle(LEFT, leftPaddleY, LOW);
// set the right paddle to on:
setPaddle(RIGHT, rightPaddleY, LOW);
}
I need a new method, keyRead()
, to determine which key was pressed and set the new paddle positions. Here it is:
void keyRead() {
switch (key) {
case 'z': // right down
if (leftPaddleY < BOTTOM) {
leftPaddleY++;
}
break;
case 'a': // right up
if (leftPaddleY > TOP) {
leftPaddleY--;
}
break;
case ',': // left down
if (rightPaddleY < BOTTOM) {
rightPaddleY++;
}
break;
case 'l': // left up
if (rightPaddleY > TOP) {
rightPaddleY--;
}
break;
}
}
That’s all the changes. The whole sketch can be downloaded here. Compare it side-by-side with the Arduino Mega Sketch and you’ll see how the logic is portable from one to the other.
2020: P5.js Pong
In 2020, I decided to update this post, since my colleagues now teach our intro to computational media course at ITP in p5.js. Changing the code from Processing to p5.js was very quick. Below are the changes:
Change the Variables
JavaScript is a weakly typed language, while Java and C are strongly typed. So all the variable declarations have to change. To simplify things, I changed them all to var
, except for the constants at the top. Here are the global constants and variables:
// define the edges of the screen:
const LEFT = 0;
const RIGHT = 15;
const TOP = 0;
const BOTTOM = 7;
const HIGH = 0;
const LOW = 255;
var pixelArray = []; // 2-dimensional array of pixels
var ballX = 8; // X position of the ball
var ballY = 4; // Y position of the ball
var ballDirectionY = 1; // X direction of the ball
var ballDirectionX = 1; // Y direction of the ball
var rightPaddleY = 0; // X position of the center of the right paddle
var leftPaddleY = 0; // Y position of the center of the right paddle
var timeStamp = 0; // time stamp to control the pauses between ball moves
var interval = 120; // interval between ball moves, in milliseconds
var gamePaused = false; // state of the game
Change the Function Declarations
Like the variables, the function declarations need to change too. For example,
void setPaddle(int paddleX, int paddleY, int state)
changes to:
function setPaddle(paddleX, paddleY, state)
Change the Array Declaration
The array declaration for the pixels has to change. The word pixels
is a reserved word in p5.js, so I changed it to pixelArray
. To declare it, I set it to an empty array, as shown above. To initialize it, I changed the nested for loops in the setup() to push a new element on the array first, and then set its value. You can see the changes to the setup highlighted below:
function setup() {
// create the canvas:
createCanvas(420, 220);
smooth();
frameRate(30);
// initialize the pixel matrix:
for (var x = 0; x < 16; x++) {
for (var y = 0; y < 8; y++) {
// add an element to the array:
pixelArray.push([x, y]);
// set the element's value:
pixelArray[x][y] = HIGH;
}
}
}
Check for Other API Differences
Since p5.js is derived from Processing, the differences are minimal, but there are some, like the pixels
keyword mentioned earlier. The only other one in this sketch is that the variable keyPressed
in Processing changes to keyIsPressed
in p5.js. This change is on line 85 of the p5.js sketch.
Other than those changes, nothing needs to change from the Processing version to make it work in p5.js. Compare the Arduino version, the Processing version, and the p5.js version side by side to see the differences.
Conclusion
This approach is hardly new — in fact, it’s just good programming hygiene. Three simple rules:
- Break the action down into describable sections, and use those as your core methods.
- When possible, separate the code that controls your inputs and outputs from the code that manipulates memory and makes choices
- Use descriptive names and comment your code within an inch of its life.
if your program is described and laid out sensibly, the troubleshooting is much, much easier, and implementing the idea on a new platform is no problem. It also makes it simpler to revisit your code years later when you can’t remember how you wrote it the first time!
6 Replies to “A Tale of Two (three) Pongs”
Comments are closed.