Skip to main content
Arduino

Return to Question

Rollback to Revision 2
Source Link
SNBS
  • 143
  • 8

UPDATE for future visitors: bad performance is not a fault of the display, even though not everyone, and not me as well, can fully overcome it (~30 ms for updating the screen — still rather slow). What a pity it is that the guy who published this post didn't explain how he achieved that high frame rate! (I don't think that the communication interface, I2C in my case and SPI in his case, makes much difference.)

UPDATE for future visitors: bad performance is not a fault of the display, even though not everyone, and not me as well, can fully overcome it (~30 ms for updating the screen — still rather slow). What a pity it is that the guy who published this post didn't explain how he achieved that high frame rate! (I don't think that the communication interface, I2C in my case and SPI in his case, makes much difference.)

added a little note
Source Link
SNBS
  • 143
  • 8

UPDATE for future visitors: bad performance is not a fault of the display, even though not everyone, and not me as well, can fully overcome it (~30 ms for updating the screen — still rather slow). What a pity it is that the guy who published this post didn't explain how he achieved that high frame rate! (I don't think that the communication interface, I2C in my case and SPI in his case, makes much difference.)

UPDATE for future visitors: bad performance is not a fault of the display, even though not everyone, and not me as well, can fully overcome it (~30 ms for updating the screen — still rather slow). What a pity it is that the guy who published this post didn't explain how he achieved that high frame rate! (I don't think that the communication interface, I2C in my case and SPI in his case, makes much difference.)

deleted 6 characters in body
Source Link
SNBS
  • 143
  • 8
#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); // ThisA delay is necessaryto forshow the displayAdafruit 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;
 }
}
#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); // This delay is necessary for the display
 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;
 }
}
#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;
 }
}
Became Hot Network Question
Source Link
SNBS
  • 143
  • 8
Loading

AltStyle によって変換されたページ (->オリジナル) /