I'm working on a tiny ping-pong game based on Arduino Nano. Its modes are "person vs person" and "person vs computer" (the computer just tries to keep its racket at the same Y position as the ball). There are 2 "up" buttons, 2 "down" buttons, two 7-segment score indicators and 2 shift registers controlling them. The display is SSD1306 (128x32 pixels).
I'm posting here the entire code because I don't know where the issue lies.
#include <Adafruit_SSD1306.h>
// Up/down buttons
#define UP1 2
#define DOWN1 3
#define UP2 4
#define DOWN2 5
// Shift registers that control score indicators
#define DATA1 6
#define CLOCK1 7
#define LATCH1 8
#define DATA2 9
#define CLOCK2 10
#define LATCH2 11
#define RANDOM_NUMBERS A0 // This port isn't connected to anything and is used to get random seed
#define WRAP 118 // Text on display wraps on this position
#define SSD1306_ADDRESS 0x3C // I2C address of display
Adafruit_SSD1306 ssd1306;
const byte numbers[10] = { // Arab numbers for score indicators
0b1111110, 0b0011000, 0b1101101, 0b0111101, 0b0011011, 0b0110111, 0b1110111, 0b0011100, 0b1111111, 0b0111111
};
short currentDirection = -1;
short directionState = -1; // The sequence number of next move in the period of ball trajectory
short nextMove = -1;
short ballX = 63;
short ballY = 15;
short racket1 = 12;
short racket2 = 12;
short score1 = 0;
short score2 = 0;
bool gameOver = false;
bool playingWithComputer = false;
void setup() {
pinMode(UP1, INPUT_PULLUP);
pinMode(DOWN1, INPUT_PULLUP);
pinMode(UP2, INPUT_PULLUP);
pinMode(DOWN2, INPUT_PULLUP);
pinMode(DATA1, OUTPUT);
pinMode(CLOCK1, OUTPUT);
pinMode(LATCH1, OUTPUT);
pinMode(DATA2, OUTPUT);
pinMode(CLOCK2, OUTPUT);
pinMode(LATCH2, OUTPUT);
randomSeed(analogRead(RANDOM_NUMBERS));
updateScores();
ssd1306.begin(SSD1306_SWITCHCAPVCC, SSD1306_ADDRESS);
ssd1306.display();
delay(2000); // A delay to show the Adafruit logo
ssd1306.clearDisplay();
ssd1306.setTextSize(1);
ssd1306.setTextColor(WHITE);
ssd1306.setCursor(0, 0); // Cursor should be at this position to draw objects correctly
printLongText(5, 0, "Press \"up\" to play with computer, otherwise press \"down\"");
while (true) {
if (!digitalRead(UP1) || !digitalRead(UP2)) { // Both "up" buttons (as well as both "down" ones) are treated the same unless there's a game "person vs person"
playingWithComputer = true;
break;
}
if (!digitalRead(DOWN1) || !digitalRead(DOWN2)) break;
delay(5); // This delay is here to avoid overloading the processor
}
ssd1306.clearDisplay();
ssd1306.display();
delay(100); // This delay is intended to show user that they don't need to hold button any more
}
void loop() {
if (gameOver) {
delay(1000);
return;
}
if (nextMove < 0) {
pickDirection();
return;
}
short oldDirection = currentDirection;
switch (getBounceType()) {
case 0:
verticalFlipDirection();
ballY++;
break;
case 1:
verticalFlipDirection();
ballY--;
break;
case 2:
if (ballY < (racket1 - 1) || ballY > (racket1 + 8)) { // This determines if the ball ran shorto the racket or not (true if it did not)
score2++;
// Ball goes to the center of the screen after goal
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
horizontalFlipDirection();
ballX++;
}
break;
case 3:
if (ballY < (racket2 - 1) || ballY > (racket2 + 8)) {
score1++;
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
horizontalFlipDirection();
ballX--;
}
break;
case 4:
if (racket1 > 1) {
score2++;
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
reverseDirection(); // This occurs when the ball rans exactly shorto the corner of its zone
ballX++;
ballY++;
}
break;
case 5:
if (racket2 > 1) {
score1++;
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
ballX--;
ballY++;
reverseDirection();
}
break;
case 6:
if (racket1 > 1) {
score2++;
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
reverseDirection();
ballX++;
ballY--;
}
break;
case 7:
if (racket2 > 1) {
score1++;
ballX = 63;
ballY = 15;
pickDirection();
updateScores();
}
else {
ballX--;
ballY--;
reverseDirection();
}
break;
}
if (oldDirection != currentDirection) directionState = 0; // Resetting direction state if the ball bounced
pickMove(); // Next move may have changed if the ball bounced
switch (nextMove) {
case 0:
ballY--;
break;
case 1:
ballX++;
break;
case 2:
ballY++;
break;
case 3:
ballX--;
break;
}
directionState++;
pickMove();
ssd1306.clearDisplay();
ssd1306.drawRect(ballX, ballY, 2, 2, WHITE);
if (playingWithComputer) {
if (!digitalRead(UP1) || !digitalRead(UP2)) {
if (racket1 > 0) racket1--;
}
if (!digitalRead(DOWN1) || !digitalRead(DOWN2)) {
if (racket1 < 24) racket1++;
}
// This is the algorithm used by computer — it just tries to keep the computer-controlled racket at the same Y position as the ball
if ((racket2 + 3) > ballY) {
if (racket2 > 0) racket2--;
}
if ((racket2 + 3) < ballY) {
if (racket2 < 24) racket2++;
}
} else {
if (!digitalRead(UP1)) {
if (racket1 > 0) racket1--;
}
if (!digitalRead(DOWN1)) {
if (racket1 < 24) racket1++;
}
if (!digitalRead(UP2)) {
if (racket2 > 0) racket2--;
}
if (!digitalRead(DOWN2)) {
if (racket2 < 24) racket2++;
}
}
ssd1306.drawRect(0, racket1, 2, 8, WHITE);
ssd1306.drawRect(126, racket2, 2, 8, WHITE);
ssd1306.display();
}
// This function picks a random direction for the ball
void pickDirection() {
currentDirection = random(0, 20);
directionState = 0;
pickMove();
}
// This function determines the next move of the ball (up/right/down/left) based on its current direction and direction state
void pickMove() {
short sector = getSector(); // Directions that go to up-right are in sector 0, those going to down-right are in sector 1, etc
switch (currentDirection) {
case 0:
case 5:
case 10:
case 15:
directionState = directionState % 5; // Resetting direction state when the ball reaches next period of its trajectory
// At certain direction states the ball goes to its primary direction (e.g. in an almost vertical trajectory the primary direction would be up)
// At other states it goes to the secondary direction, which when added to the primary one forms the actual trajectory of the ball
// Sectors divide into 2 parts. The first part includes 3 trajectories which have their primary direction number (up - 0, right - 1, down - 2, left - 3) same as sector number
// Secondary directions in first part are sector number plus 1
// In the second part the primary direction is (sector_number + 1) % 4 and the secondary one is the sector number
// There are 20 supported trajectories (5 in each of 4 sectors). They are presented in Ping-Pong.png
// First trajectories in each sector (as well as second ones etc) have the same structure, so there's only one code block
// for handling first trajectories of all sectors. However, their primary and secondary directions differ in each sector
// The following line determines if the ball should now go to the primary or secondary direction and based on that sets nextMove
nextMove = (directionState == 0 || directionState == 1 || directionState == 3) ? sector : (sector + 1) % 4;
return;
case 1:
case 6:
case 11:
case 16:
directionState = directionState % 4;
nextMove = (directionState < 2) ? sector : (sector + 1) % 4;
return;
case 2:
case 7:
case 12:
case 17:
directionState = directionState % 4;
nextMove = (directionState == 0 || directionState == 2) ? sector : (sector + 1) % 4;
return;
case 3:
case 8:
case 13:
case 18:
directionState = directionState % 4;
nextMove = (directionState < 2) ? getSecondPartMove(sector) : sector;
return;
case 4:
case 9:
case 14:
case 19:
directionState = directionState % 5;
nextMove = (directionState == 0 || directionState == 1 || directionState == 3) ? getSecondPartMove(sector) : sector;
return;
}
}
// Just a helper for pickMove (determines primary direction of second-part trajectories in a certain sector)
short getSecondPartMove(short sector) {
return (sector + 1) % 4;
}
// This function determines the current bounce type (-1 - currently no bounce, 0 - bounce from upper wall,
// 1 - bounce from lower wall, 2 - facing the first player's wall, 3 - facing the second player's wall,
// 4 - facing upper-left corner, 5 - upper-right corner, 6 - bottom-left, 7 - bottom right
// On 4-7 the ball reverses its trajectory
short getBounceType() {
if (ballX <= 2 && ballY <= 0) return 4; // Made ballX indents so that the ball bounces from rackets and not bounds of screen
if (ballX >= 124 && ballY <= 0) return 5;
if (ballX <= 2 && ballY >= 30) return 6;
if (ballX >= 124 && ballY >= 30) return 7;
if (ballX <= 2) return 2;
if (ballX >= 124) return 3;
if (ballY <= 0) return 0;
if (ballY >= 30) return 1;
return -1;
}
// This function flips the current trajectory of the ball vertically to bounce from the upper/lower wall
// (source and flipped trajectories have a relationship that differs in different sectors of the source trajectory, see this function)
void verticalFlipDirection() {
short sector = getSector();
switch (sector) {
case 0:
case 1:
currentDirection = 9 - currentDirection;
return;
case 2:
case 3:
currentDirection = 10 + (9 - (currentDirection - 10));
return;
}
directionState = 0;
}
// Same as previous function but the translation is horizontal here
// (the relationship between source and flipped trajectories is different)
void horizontalFlipDirection() {
short sector = getSector();
switch (sector) {
case 0:
case 3:
currentDirection = 19 - currentDirection;
return;
case 1:
case 2:
currentDirection = 5 + (9 - (currentDirection - 5));
return;
}
directionState = 0;
}
// This function reverses the trajectory of the ball
void reverseDirection() {
currentDirection = (currentDirection + 10) % 20;
directionState = 0;
}
// This function determines the sector of the current trajectory
short getSector() {
if (currentDirection < 5) return 0;
else if (currentDirection < 10) return 1;
else if (currentDirection < 15) return 2;
else return 3;
}
// This function prints text on the display, wrapping it at a certain X position
void printLongText(short cursorX, short cursorY, String text) {
ssd1306.setCursor(cursorX, cursorY);
int16_t x, y;
uint16_t w, h;
short lastSpace = -1;
short i;
// Adding character by character to current line until the wrapping position is reacher
// In parallel finding the last space before wrap (it will be where the line breaks)
for (i = 0; i < text.length(); i++) {
if (text.charAt(i) == ' ') lastSpace = i;
ssd1306.getTextBounds(text.substring(0, i + 1), 0, 0, &x, &y, &w, &h);
if (x + w > WRAP && lastSpace >= 0) {
ssd1306.println(text.substring(0, lastSpace + 1)); // Wrapping position reached; breaking the current line at the index of last space in it and printing it
text = text.substring(lastSpace + 1);
i = -1; // Now the same with next line
lastSpace = -1;
ssd1306.setCursor(cursorX, ssd1306.getCursorY());
}
}
ssd1306.println(text); // printing the rest of text
ssd1306.display();
ssd1306.setCursor(0, 0); // Cursor should be at this position to draw objects correctly
}
// This function displays the scores on corresponding indicators and checks if the game if over
// (i.e. one of the players has a score of 7)
void updateScores() {
digitalWrite(LATCH1, false);
shiftOut(DATA1, CLOCK1, MSBFIRST, numbers[score1]); // Sending the necessary mask to the shift registers
digitalWrite(LATCH1, true);
digitalWrite(LATCH2, false);
shiftOut(DATA2, CLOCK2, MSBFIRST, numbers[score2]);
digitalWrite(LATCH2, true);
if (score1 >= 7) {
ssd1306.clearDisplay();
printLongText(10, 12, "Player 1 wins!");
ssd1306.display();
gameOver = true;
}
if (score2 >= 7) {
ssd1306.clearDisplay();
printLongText(10, 12, "Player 2 wins!");
ssd1306.display();
gameOver = true;
}
}
Look how slowly does the ball move, even though a loop
iteration doesn't include any delay
s at all, and there are no complex calculations. Conversion to GIF didn't change the speed.
My circuit looks like this (it's just a test, so no buttons, just pull-ups, and no score indicators).
simulate this circuit – Schematic created using CircuitLab
Answers here didn't help. Changing the I2C clock speed to 400 KHz didn't help either.
So — is such slowness caused by the (not very high) complexity of my code? Or is it some kind of glitch? Please don't suggest using libraries instead of my own moving and bouncing logic.
loop()
iteration by usingmillis()
ormicros()
? My guess is, that the display code is taking rather long. You could also try to not clear the display on every iteration, but drawing over the previous rectangles with new black rectanges. Might work faster