paint-brush
我如何在 Arduino 板上开发经典乒乓球游戏经过@chingiz
13,839 讀數
13,839 讀數

我如何在 Arduino 板上开发经典乒乓球游戏

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

太長; 讀書

游戏并不复杂,但开发和玩起来很有趣。我想出了一种在 Arduino 板上开发流行的乒乓球游戏的方法。使用了0.96英寸OLED显示屏和两个按钮。显示器很小,但足以满足我们的项目。将使用两个按钮来上下移动我们的球拍。分数将垂直显示。一个球拍将由球员控制,第二个球拍由对手控制。在游戏开始时,它会缓慢移动,然后逐渐加速。

Company Mentioned

Mention Thumbnail
featured image - 我如何在 Arduino 板上开发经典乒乓球游戏
Chingiz Nazar HackerNoon profile picture

我想出了一种在 Arduino 板上开发流行的乒乓球游戏的方法。


游戏并不复杂,但开发和玩起来很有趣。


这里使用了 0.96 英寸 OLED 显示屏和两个按钮。显示器很小,但足以满足我们的项目。将使用两个按钮来上下移动我们的球拍。


显示尺寸为 128x64 像素。第一个和最后 16 个像素将用于显示分数。

分数将垂直显示。


一个球拍将由玩家控制,第二个球拍将由 Arduino 控制。我们将编写代码,使其尝试向球移动。在游戏开始时,它会缓慢移动,然后,它会逐渐加速。


我们将通过 ball_direction_X 和 ball_direction_Y 变量控制球的方向。球每时每刻都会向指定的方向移动。如果球撞到墙上,ball_direction_Y 将被反转,ball_direction_X 和球拍的逻辑相同。


项目代码发布在我的 GitHub 'Arduino Ping Pong Game' 项目页面上

联系

按钮的一条腿将连接到 GND。第二条腿将连接到 Arduino 的引脚 6 和 5。

显示器将通过 I2C 引脚连接:V - 5V、GND - GND、SCL - A5、SDA - A4。

在 Arduino 上开发 Pong

让我们包含库并启动我们需要的所有变量。这里我们有按钮、分数、球员、对手和与球相关的变量。

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


setup中,我们将启动按钮、Serial、randomSeed 和 display。之后,我们将显示一个启动屏幕并开始游戏:

 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 } 


启动画面的图像

循环中,我们有三个主要功能,下面将详细解释。

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


buttons_check函数中,我们将检查是否按下了UP 或DOWN 按钮,并相应地更新玩家球拍的位置。球拍位置更改主要分为三个步骤:在旧位置绘制黑色矩形,更改位置变量,并在新位置绘制白色矩形。

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


move_the_ball_and_check_for_collisions函数中,我们将移动球并检查与水平墙、球拍和垂直墙的碰撞(玩家输赢)。在移动墙壁之前,我们将检查从球的最后一次移动所经过的所需时间。我将所需时间设置为球速乘以 20。为了显示球,我们将画一个实心圆圈。一般来说,球的位置更新将与球拍位置的更新相同。


要检查球与水平墙之一的碰撞,我们需要确保 ball_position_Y 和 ball_direction_Y 的总和在 0 到 63 之间(不小于 -1 且不大于 64)。如果是这样,ball_direction_Y 将被反转。


要检查球员的球拍是否漏球,我们需要检查 ball_position_X 是否小于 player_position_X 并开始新一轮比赛。对敌方球拍的逻辑相同,只是因为它放置在场地的另一侧,所以检查会更多。为了检查球员的球拍是否击中球,我们将检查球是否在球拍内。如果是这样,ball_direction_X 将被反转,并且会给出 ball_direction_Y 的随机值。


用敌人的球拍击球的逻辑与往常一样。

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


正如您在上面注意到的, newRound函数将获胜者(玩家/敌人)作为输入。需要输入以增加适当的分数。在函数中,我们将重置所有必需的游戏相关变量,打印更新的分数,显示所有游戏对象,如果需要,更新敌人和球的速度。随着您的进步,游戏将变得更加困难。

 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 }


垂直打印乐谱有点棘手。所以,我决定把它拆分成一个单独的函数。分数的每一位都会一个一个地显示出来,光标也需要相应地更新。

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


而最后一个是移动敌人的球拍。它会尝试击球。为此,它将尝试使其中心与球的中心相等。在这里,我们有一些补充检查,以将球拍留在屏幕内。为了赢得这样的敌人,它的移动速度一开始会很慢,随着游戏的进行会加速。

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

结论

我希望你喜欢游戏的开发和玩游戏。很多人从小就熟悉这个游戏,玩的时候有一种怀旧的感觉。开发人员有一个独特的机会不仅可以从外部了解游戏,还可以从内部了解一切是如何运作的。


以下是一些改进项目的想法:


  • 目前,游戏是无止境的。最好执行乒乓球官方规则来确定获胜者。
  • 随着球的速度增加,做一个视觉效果,让球有尾巴。
  • 反弹过程中球的飞行角度应取决于球接触球拍的位置。