paint-brush
Comment j'ai développé le jeu Classic Pong sur une carte Arduinopar@chingiz
13,898 lectures
13,898 lectures

Comment j'ai développé le jeu Classic Pong sur une carte Arduino

par Chingiz Nazar1m2022/04/15
Read on Terminal Reader
Read this story w/o Javascript

Trop long; Pour lire

Le jeu n'est pas compliqué mais intéressant à développer et à jouer. J'ai trouvé un moyen de développer le jeu de pong populaire sur la carte Arduino. Un écran OLED de 0,96 pouce et deux boutons ont été utilisés. L'écran est petit mais suffisant pour notre projet. Deux boutons serviront à déplacer notre raquette de haut en bas. Les scores seront affichés verticalement. Une raquette sera contrôlée par le joueur et la seconde par l'adversaire. Au début du jeu, il se déplacera lentement, puis accélérera progressivement.

Company Mentioned

Mention Thumbnail
featured image - Comment j'ai développé le jeu Classic Pong sur une carte Arduino
Chingiz Nazar HackerNoon profile picture

J'ai trouvé un moyen de développer le jeu de pong populaire sur la carte Arduino.


Le jeu n'est pas compliqué mais intéressant à développer et à jouer.


Ici, un écran OLED de 0,96 pouce et deux boutons ont été utilisés. L'écran est petit mais suffisant pour notre projet. Deux boutons serviront à déplacer notre raquette de haut en bas.


La taille d'affichage est de 128x64 pixels. Les 16 premiers et derniers pixels seront utilisés pour afficher les scores.

Les scores seront affichés verticalement.


Une raquette sera contrôlée par le joueur, et la seconde, l'adversaire sera contrôlée par l'Arduino. Nous allons écrire du code pour qu'il essaie de se déplacer vers la balle. Au début du jeu, il se déplacera lentement, puis, il accélérera progressivement.


Nous contrôlerons la direction de la balle via les variables ball_direction_X et ball_direction_Y. La balle se déplacera dans une direction spécifiée à chaque instant. Si la balle touche un mur, ball_direction_Y sera inversé et la même logique pour ball_direction_X et les raquettes.


Le code du projet est publié sur ma page de projet GitHub 'Arduino Ping Pong Game' .

Lien

Une jambe des boutons sera connectée au GND. Les deuxièmes jambes seront connectées aux broches 6 et 5 de l'Arduino.

L'affichage sera connecté par des broches I2C : V - 5V, GND - GND, SCL - A5, SDA - A4.

Développer Pong sur Arduino

Incluons les bibliothèques et initions toutes les variables dont nous avons besoin. Ici, nous avons des boutons, des scores, un joueur, un adversaire et les variables liées au ballon.

 //oled libraries: #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> //oled vars: #define SCREEN_WIDTH 128 // OLED display width, in pixels #define SCREEN_HEIGHT 64 // OLED display height, in pixels #define OLED_RESET 4 // Reset pin # (or -1 if sharing Arduino reset pin) #define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32 Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); //Button pins: const int buttonUP = 6; const int buttonDOWN = 5; //button vars: int lastButtonStateUP = LOW; // the previous reading from the input pin int lastButtonStateDOWN = LOW; // the previous reading from the input pin unsigned long debounceDelay = 10; // the debounce time; increase if the output flickers //GAME vars: //scores: int player_score = 0; int enemy_score = 0; //player: int player_position_X = 19; // static int player_position_Y = 0; int player_width = 16; int player_thickness = 4; //enemy: int enemy_position_X = 104; // static int enemy_position_Y = 47; int enemy_width = 16; int enemy_thickness = 4; long enemy_last_move_time = 0; long enemy_speed_of_moving = 2000;//update time in ms //ball: //void fillCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color); int ball_position_X = 63; int ball_position_Y = 31; int ball_radius = 1; int ball_direction_X = 3; int ball_direction_Y = 3; int ball_speed = 8;//9,8,7...1 long ball_last_move_time = 0;


Dans la configuration , nous allons initier les boutons, Serial, randomSeed et display. Après cela, nous afficherons un écran de démarrage et commencerons le jeu :

 void setup() { pinMode(buttonUP, INPUT_PULLUP); pinMode(buttonDOWN, INPUT_PULLUP); Serial.begin(9600); Serial.println("Start"); //initiate random randomSeed(analogRead(0)); ball_direction_X = -3; ball_direction_Y = random(-5, 5); //ball_direction_Y = -5;//test // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) { Serial.println(F("SSD1306 allocation failed")); for(;;); // Don't proceed, loop forever } // Show initial display buffer contents on the screen -- // the library initializes this with an Adafruit splash screen. display.display(); // Clear the buffer display.clearDisplay(); //draw lines: display.drawLine(16, 0, 16, 63, SSD1306_WHITE); display.drawLine(111, 0, 111, 63, SSD1306_WHITE); display.display(); //scores field init: display.setTextSize(2); display.setTextColor(SSD1306_WHITE); // Draw white text player_score = 8888; // test enemy_score = 8888; // test print_score(player_score, 0); print_score(enemy_score, 115); display.setTextSize(3); display.setCursor(28, 0); display.write("Ping"); display.setCursor(28, 31); display.write("Pong"); display.display(); display.setTextSize(2); delay(2000); // Pause for 2 seconds //NEW GAME: // Clear the buffer display.clearDisplay(); //draw lines: display.drawLine(16, 0, 16, 63, SSD1306_WHITE); display.drawLine(111, 0, 111, 63, SSD1306_WHITE); display.display(); //Write scores: player_score = 0; //reset player_score enemy_score = 0; //reset enemy_score print_score(player_score, 0); print_score(enemy_score, 115); //Display players: //void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color); display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE); display.fillRect(enemy_position_X, enemy_position_Y, enemy_thickness, enemy_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle //Display the ball: display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_WHITE); display.display(); delay(500); // Pause for 0.5 second } 


Image de l'écran de démarrage

Dans la boucle , nous avons trois fonctions principales qui seront expliquées en détail ci-dessous.

 void loop() { buttons_check(); move_the_ball_and_check_for_collisions(); move_enemy(); }


Dans la fonction buttons_check , nous vérifierons si le bouton UP ou DOWN est enfoncé et mettrons à jour la position de la raquette du joueur en conséquence. Le changement de position de la raquette se fait en trois étapes principales : dessiner un rectangle noir dans l'ancienne position, modifier la variable de position et dessiner un rectangle blanc dans la nouvelle position.

 void buttons_check(){ if (!digitalRead(buttonUP) && !lastButtonStateUP) { lastButtonStateUP = true; // Serial.println("UP pressed"); if(player_position_Y > 0){ display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_BLACK); player_position_Y = player_position_Y-3; display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle } } if (digitalRead(buttonUP) && lastButtonStateUP) { lastButtonStateUP = false; } if (!digitalRead(buttonDOWN) && !lastButtonStateDOWN) { lastButtonStateDOWN = true; // Serial.println("DOWN pressed"); if(player_position_Y < 64-player_width){ display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_BLACK); player_position_Y = player_position_Y+3; display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle } } if (digitalRead(buttonDOWN) && lastButtonStateDOWN) { lastButtonStateDOWN = false; } }


Dans la fonction move_the_ball_and_check_for_collisions , nous allons déplacer la balle et vérifier les collisions avec les murs horizontaux, les raquettes et les murs verticaux (le joueur gagne ou perd). Avant de déplacer le mur, nous vérifierons le temps nécessaire écoulé depuis le dernier mouvement de la balle. J'ai défini le temps requis comme vitesse de balle multipliée par vingt. Pour afficher la balle, nous allons dessiner un cercle plein. En général, la mise à jour de la position de la balle se fera de la même manière que la mise à jour de la position de la raquette.


Pour vérifier la collision de la balle avec l'un des murs horizontaux, nous devons nous assurer que la somme de ball_position_Y et ball_direction_Y est comprise entre 0 et 63 (pas moins de -1 et pas plus de 64). Si c'est le cas, la ball_direction_Y sera inversée.


Pour vérifier si la raquette du joueur a raté la balle, nous devons vérifier si ball_position_X est inférieur à player_position_X et commencer un nouveau tour de jeu. Même logique pour la raquette ennemie, seulement puisqu'elle est placée de l'autre côté du terrain le check sera pour plus. Pour vérifier si la raquette du joueur touche la balle on va vérifier si la balle est à l'intérieur de la raquette. Si c'est le cas, la ball_direction_X sera inversée et une valeur aléatoire pour ball_direction_Y sera donnée.


Toujours la même logique pour frapper la balle avec la raquette adverse.

 void move_the_ball_and_check_for_collisions(){ //move th ball: if(millis() > ball_speed*20+ball_last_move_time){ //erase ball on old position: display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_BLACK); display.display(); //set new posion of the ball: ball_position_X = ball_position_X + ball_direction_X; if(ball_position_Y + ball_direction_Y < -1) ball_direction_Y = ball_direction_Y * -1; if(ball_position_Y + ball_direction_Y > 64) ball_direction_Y = ball_direction_Y * -1; ball_position_Y = ball_position_Y + ball_direction_Y; //draw ball on new position: display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_WHITE); display.display(); // Serial.print("ball_position_Y: "); Serial.println(ball_position_Y); ball_last_move_time = millis(); //Check for player loose: if(ball_position_X < player_position_X){ Serial.println("Player lose!"); newRound("enemy");//player } //check for collision of the ball and the player: if(player_position_X <= ball_position_X && player_position_X+player_thickness >= ball_position_X && player_position_Y <= ball_position_Y && player_position_Y+player_width >= ball_position_Y){ Serial.println("Collision of the ball and the player"); //send the ball to enemy with random values: ball_direction_X = 3; ball_direction_Y = random(-5, 5); display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle } //check for enemy loose: if(ball_position_X > enemy_position_X+enemy_thickness){ Serial.println("Enemy lose!"); newRound("player");//enemy } //check for collision of the ball and the enemy: if(enemy_position_X <= ball_position_X && enemy_position_X+enemy_thickness >= ball_position_X && enemy_position_Y <= ball_position_Y && enemy_position_Y+enemy_width >= ball_position_Y){ Serial.println("Collision of the ball and the enemy"); //send the ball to player with random values: ball_direction_X = -3; ball_direction_Y = random(-5, 5); display.fillRect(enemy_position_X, enemy_position_Y, enemy_thickness, enemy_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle } } }


Comme vous l'avez remarqué ci-dessus, la fonction newRound prend comme gagnant d'entrée (joueur/ennemi). L'entrée est nécessaire pour augmenter le score approprié. Dans la fonction, nous réinitialiserons toutes les variables requises liées au jeu, imprimerons les scores mis à jour, afficherons tous les objets du jeu et, si nécessaire, mettrons à jour la vitesse de l'ennemi et de la balle. Le jeu deviendra plus difficile au fur et à mesure de votre progression.

 void newRound(String winner){ // Clear the buffer display.clearDisplay(); //draw lines: display.drawLine(16, 0, 16, 63, SSD1306_WHITE); display.drawLine(111, 0, 111, 63, SSD1306_WHITE); display.display(); //Update scores: if(winner == "enemy"){ enemy_score++; }else{ player_score++; } print_score(player_score, 0); print_score(enemy_score, 115); //reset gaming vars: //player: player_position_X = 19; // static player_position_Y = 0; player_width = 16; player_thickness = 4; //ball: ball_position_X = 63; ball_position_Y = 31; ball_radius = 1; //set random direction for th ball: ball_direction_X = -3; ball_direction_Y = random(-5, 5); //ball_direction_Y = -5;//test ball_last_move_time = 0; //Display players: //void fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_t h, uint16_t color); display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle //enemy: enemy_position_X = 104; // static enemy_position_Y = 47; enemy_width = 16; enemy_thickness = 4; enemy_last_move_time = 0; //checking for if we need to update enemy_speed_of_moving and ball_speed if((player_score+enemy_score)%5 == 0){ //5,10,15 and so on if(ball_speed > 3) ball_speed = ball_speed - 1; //10,9,8... Serial.print("ball_speed: ");Serial.println(ball_speed); } if((player_score+enemy_score)%10 == 0){ //10,20,30 and so on if(enemy_speed_of_moving > 1) enemy_speed_of_moving = enemy_speed_of_moving * 0.9; //2000,1800,1620,1458... Serial.print("enemy_speed_of_moving: ");Serial.println(enemy_speed_of_moving); } delay(500); // Pause for 0.5 seconds }


L'impression verticale des partitions est un peu délicate. J'ai donc décidé de le diviser en une fonction distincte. Chaque chiffre du score sera affiché un par un et le curseur doit être mis à jour en conséquence.

 void print_score(int temp_num, int X){ //0/115 for(int i=48; i>=0; i-=16){ int num = temp_num % 10; char cstr[16]; itoa(num, cstr, 10); display.setCursor(X, i); display.write(cstr); display.display(); // Serial.println(cstr); temp_num = temp_num/10; if(temp_num==0){ break; } } }


Et le dernier déplace la raquette ennemie . Il essaiera de frapper la balle. Et pour cela, il va essayer d'égaliser son centre avec le centre du ballon. Ici, nous avons quelques vérifications supplémentaires pour maintenir la raquette à l'intérieur de l'écran. Pour qu'un tel ennemi soit vaincu, sa vitesse de déplacement sera lente au début et s'accélérera au fur et à mesure que le jeu progresse.

 void move_enemy(){ //enemy: if(millis() > enemy_speed_of_moving+enemy_last_move_time){ display.fillRect(enemy_position_X, enemy_position_Y, enemy_thickness, enemy_width, SSD1306_BLACK); if(ball_position_Y < enemy_position_Y+enemy_width/2){ enemy_position_Y = enemy_position_Y - 3; }else{ enemy_position_Y = enemy_position_Y + 3; } //checking if enemy is within the wall: if(enemy_position_Y > 64-player_width) enemy_position_Y = 64-player_width; if(enemy_position_Y < 0) enemy_position_Y = 0; // Serial.print("enemy_position_Y: "); Serial.println(enemy_position_Y); display.fillRect(enemy_position_X, enemy_position_Y, enemy_thickness, enemy_width, SSD1306_WHITE); display.display(); // Update screen with each newly-drawn rectangle enemy_last_move_time = millis(); } }

Conclusion

J'espère que vous avez apprécié à la fois le développement du jeu et le jeu. Le jeu est familier à beaucoup depuis l'enfance et la nostalgie se fait sentir en jouant. Les développeurs ont une occasion unique de découvrir le jeu non seulement de l'extérieur, mais de voir comment tout fonctionne de l'intérieur.


Voici quelques idées d'améliorations pour le projet :


  • Actuellement, le jeu est sans fin. Ce serait formidable de mettre en œuvre les règles officielles du tennis de table pour déterminer un gagnant.
  • Au fur et à mesure que la vitesse de la balle augmente, créez un effet visuel pour que la balle ait une queue.
  • L'angle de vol de la balle lors du rebond doit être dépendant de l'endroit où la balle touche la raquette.