paint-brush
Como desenvolvi o clássico jogo Pong em uma placa Arduinopor@chingiz
13,898 leituras
13,898 leituras

Como desenvolvi o clássico jogo Pong em uma placa Arduino

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

Muito longo; Para ler

O jogo não é complicado, mas interessante de desenvolver e jogar. Eu descobri uma maneira de desenvolver o popular jogo pong na placa Arduino. Tela OLED de 0,96 polegadas e dois botões foram usados. A tela é pequena, mas suficiente para o nosso projeto. Dois botões serão usados para mover nossa raquete para cima e para baixo. As pontuações serão exibidas verticalmente. Uma raquete será controlada pelo jogador, e a segunda, pelo adversário. No início do jogo, ele se moverá lentamente e, gradualmente, acelerará.

Company Mentioned

Mention Thumbnail
featured image - Como desenvolvi o clássico jogo Pong em uma placa Arduino
Chingiz Nazar HackerNoon profile picture

Eu descobri uma maneira de desenvolver o popular jogo pong na placa Arduino.


O jogo não é complicado, mas interessante de desenvolver e jogar.


Aqui foram usados display OLED de 0,96 polegadas e dois botões. A tela é pequena, mas suficiente para o nosso projeto. Dois botões serão usados para mover nossa raquete para cima e para baixo.


O tamanho da tela é de 128x64 pixels. Os primeiros e últimos 16 pixels serão usados para exibir pontuações.

As pontuações serão exibidas verticalmente.


Uma raquete será controlada pelo jogador, e a segunda, o adversário será controlada pelo Arduino. Vamos escrever um código para que ele tente se mover em direção à bola. No início do jogo, ele se moverá lentamente e, em seguida, acelerará gradualmente.


Vamos controlar a direção da bola através das variáveis ball_direction_X e ball_direction_Y. A bola se moverá em uma direção especificada a cada momento. Se a bola atingir uma parede, ball_direction_Y será invertida e a mesma lógica para ball_direction_X e raquetes.


O código do projeto está publicado na página do projeto GitHub 'Arduino Ping Pong Game' .

Conexão

Uma perna dos botões será conectada ao GND. As segundas pernas serão conectadas aos pinos 6 e 5 do Arduino.

O display será conectado pelos pinos I2C: V - 5V, GND - GND, SCL - A5, SDA - A4.

Desenvolvendo Pong no Arduino

Vamos incluir bibliotecas e iniciar todas as variáveis que precisamos. Aqui temos botões, pontuações, jogador, adversário e as variáveis relacionadas à bola.

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


No setup , vamos iniciar os botões Serial, randomSeed e display. Depois disso, mostraremos uma tela inicial e iniciaremos o jogo:

 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 } 


Imagem da tela inicial

No loop , temos três funções principais que serão explicadas em detalhes a seguir.

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


Na função Buttons_check , vamos verificar se o botão UP ou DOWN está pressionado e atualizar a posição da raquete do jogador de acordo. A alteração da posição da raquete é feita em três etapas principais: Desenhar um retângulo preto na posição anterior, alterar a variável de posição e desenhar um retângulo branco na nova posição.

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


Na função move_the_ball_and_check_for_collisions , vamos mover a bola e verificar se há colisões com paredes horizontais, raquetes e paredes verticais (jogador ganha ou perde). Antes de mover a parede, verificaremos o tempo necessário decorrido desde o último movimento da bola. Eu defini o tempo necessário como velocidade da bola multiplicada por vinte. Para exibir a bola, vamos desenhar um círculo preenchido. Em geral, a atualização da posição da bola será feita da mesma forma que a atualização da posição da raquete.


Para verificar a colisão da bola com uma das paredes horizontais, precisamos ter certeza de que a soma de ball_position_Y e ball_direction_Y está no intervalo entre 0 e 63 (não menos que -1 e não maior que 64). Nesse caso, a ball_direction_Y será invertida.


Para verificar se a raquete do jogador errou a bola, precisamos verificar se ball_position_X é menor que player_position_X e iniciar uma nova rodada do jogo. A mesma lógica para a raquete do adversário, só que se ela estiver do outro lado do campo a checagem será para mais. Para verificar se a raquete do jogador bateu na bola vamos checar se a bola está dentro da raquete. Nesse caso, ball_direction_X será revertido e um valor aleatório para ball_direction_Y será fornecido.


Como sempre a mesma lógica para acertar a bola com a raquete do adversário.

 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 você notou acima, a função newRound recebe como entrada o vencedor (jogador/inimigo). A entrada é necessária para aumentar a pontuação apropriada. Na função, redefiniremos todas as variáveis necessárias relacionadas ao jogo, imprimiremos pontuações atualizadas, exibiremos todos os objetos do jogo e, se necessário, atualizaremos a velocidade do inimigo e da bola. O jogo se tornará mais difícil à medida que você avança nele.

 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 é um pouco complicado. Então, decidi dividi-lo em uma função separada. Cada dígito da pontuação será exibido um por um e o cursor precisa ser atualizado de acordo.

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


E o último é mover a raquete inimiga . Ele vai tentar acertar a bola. E para isso tentará igualar seu centro com o centro da bola. Aqui temos algumas verificações complementares para manter a raquete dentro da tela. Para que tal inimigo seja vencido, sua velocidade de movimento será lenta no início e acelerará conforme o jogo avança.

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

Conclusão

Espero que tenham gostado tanto do desenvolvimento quanto do jogo. O jogo é familiar para muitos desde a infância e a nostalgia é sentida ao jogar. Os desenvolvedores têm uma oportunidade única de conhecer o jogo não apenas por fora, mas também para ver como tudo funciona por dentro.


Aqui estão algumas ideias para melhorias no projeto:


  • Atualmente, o jogo é interminável. Seria ótimo implementar as Regras Oficiais do Tênis de Mesa para determinar um vencedor.
  • À medida que a velocidade da bola aumenta, faça um efeito visual para que a bola tenha uma cauda.
  • O ângulo de voo da bola durante o rebote deve depender do local onde a bola toca a raquete.