paint-brush
Cómo desarrollé el clásico juego de pong en una placa Arduinopor@chingiz
13,898 lecturas
13,898 lecturas

Cómo desarrollé el clásico juego de pong en una placa Arduino

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

Demasiado Largo; Para Leer

El juego no es complicado pero es interesante de desarrollar y jugar. Descubrí una manera de desarrollar el popular juego de pong en la placa Arduino. Se ha utilizado una pantalla OLED de 0,96 pulgadas y dos botones. La pantalla es pequeña pero suficiente para nuestro proyecto. Se utilizarán dos botones para mover nuestra raqueta arriba y abajo. Las puntuaciones se mostrarán verticalmente. Una raqueta será controlada por el jugador y la segunda por el oponente. Al comienzo del juego, se moverá lentamente y luego acelerará gradualmente.

Company Mentioned

Mention Thumbnail
featured image - Cómo desarrollé el clásico juego de pong en una placa Arduino
Chingiz Nazar HackerNoon profile picture

Descubrí una manera de desarrollar el popular juego de pong en la placa Arduino.


El juego no es complicado pero es interesante de desarrollar y jugar.


Aquí se han utilizado una pantalla OLED de 0,96 pulgadas y dos botones. La pantalla es pequeña pero suficiente para nuestro proyecto. Se utilizarán dos botones para mover nuestra raqueta arriba y abajo.


El tamaño de la pantalla es de 128x64 píxeles. Los primeros y últimos 16 píxeles se utilizarán para mostrar las puntuaciones.

Las puntuaciones se mostrarán verticalmente.


El jugador controlará una raqueta y el Arduino controlará la segunda, el oponente. Escribiremos código para que intente moverse hacia la pelota. Al comienzo del juego, se moverá lentamente y luego se acelerará gradualmente.


Controlaremos la dirección de la pelota a través de las variables ball_direction_X y ball_direction_Y. La pelota se moverá en una dirección específica cada momento. Si la pelota golpea una pared, ball_direction_Y se invertirá y la misma lógica para ball_direction_X y raquetas.


El código del proyecto está publicado en mi página del proyecto 'Arduino Ping Pong Game' de GitHub.

Conexión

La pata de los botones se conectará a GND. Las segundas patas se conectarán a los pines 6 y 5 del Arduino.

La pantalla se conectará mediante pines I2C: V - 5V, GND - GND, SCL - A5, SDA - A4.

Desarrollando Pong en Arduino

Incluyamos bibliotecas e iniciemos todas las variables que necesitamos. Aquí tenemos botones, puntajes, jugador, oponente y las variables relacionadas con la pelota.

 //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;


En la configuración , iniciaremos los botones, Serial, randomSeed y display. Después de eso, mostraremos una pantalla de inicio e iniciaremos el juego:

 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 } 


Imagen de la pantalla de inicio

En el bucle , tenemos tres funciones principales que se explicarán en detalle a continuación.

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


En la funciónbuttons_check , verificaremos si se presiona el botón ARRIBA o ABAJO y actualizaremos la posición de la raqueta del jugador en consecuencia. El cambio de posición de la raqueta se realiza en tres pasos principales: dibujar un rectángulo negro en la posición anterior, cambiar la variable de posición y dibujar un rectángulo blanco en la nueva posición.

 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; } }


En la función move_the_ball_and_check_for_collisions , moveremos la pelota y comprobaremos si hay colisiones con paredes horizontales, raquetas y paredes verticales (el jugador gana o pierde). Antes de mover la pared, comprobaremos el tiempo requerido transcurrido desde el último movimiento de la bola. He establecido el tiempo requerido como la velocidad de la bola multiplicada por veinte. Para mostrar la pelota dibujaremos un círculo relleno. En general, la actualización de la posición de la pelota se realizará de la misma manera que la actualización de la posición de la raqueta.


Para verificar la colisión de la pelota con una de las paredes horizontales, debemos asegurarnos de que la suma de ball_position_Y y ball_direction_Y esté en el rango entre 0 y 63 (no menos de -1 y no más de 64). Si es así, ball_direction_Y se invertirá.


Para verificar si la raqueta del jugador falló la pelota, debemos verificar si ball_position_X es menor que player_position_X y comenzar una nueva ronda del juego. La misma lógica para la raqueta del enemigo, solo que al estar colocada al otro lado del campo el chequeo será por más. Para verificar si la raqueta del jugador golpea la pelota verificaremos si la pelota está dentro de la raqueta. Si es así, ball_direction_X se invertirá y se dará un valor aleatorio para ball_direction_Y.


Como siempre la misma lógica para golpear la pelota con la raqueta del enemigo.

 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 } } }


Como notó anteriormente, la función newRound toma como entrada al ganador (jugador/enemigo). La entrada es necesaria para aumentar la puntuación adecuada. En la función, restableceremos todas las variables requeridas relacionadas con el juego, imprimiremos puntajes actualizados, mostraremos todos los objetos del juego y, si es necesario, actualizaremos la velocidad del enemigo y la pelota. El juego se volverá más difícil a medida que avances en él.

 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 }


Imprimir partituras verticalmente es un poco complicado. Entonces, decidí dividirlo en una función separada. Cada dígito de la puntuación se mostrará uno por uno y el cursor debe actualizarse en consecuencia.

 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; } } }


Y el último está moviendo la raqueta enemiga . Intentará golpear la pelota. Y para ello intentará igualar su centro con el centro del balón. Aquí tenemos algunos controles adicionales para mantener la raqueta dentro de la pantalla. Para ganar a un enemigo así, su velocidad de movimiento será lenta al principio y se acelerará a medida que avanza el juego.

 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(); } }

Conclusión

Espero que hayas disfrutado tanto del desarrollo del juego como del juego. El juego es familiar para muchos desde la infancia y se siente nostalgia al jugar. Los desarrolladores tienen una oportunidad única de conocer el juego no solo desde fuera, sino también de ver cómo funciona todo desde dentro.


Aquí hay algunas ideas para mejorar el proyecto:


  • Actualmente, el juego es interminable. Sería genial implementar las Reglas Oficiales del Tenis de Mesa para determinar un ganador.
  • A medida que aumenta la velocidad de la pelota, haz un efecto visual para que la pelota tenga cola.
  • El ángulo de vuelo de la pelota durante el rebote debe depender del lugar donde la pelota toca la raqueta.