我如何在 Arduino 板上开发经典乒乓球游戏 by@chingiz
11,297 讀數

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

2022/04/15
1 分钟
经过 @chingiz 11,297 讀數
tldt arrow
ZH
Read on Terminal Reader

太長; 讀書

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

Company Mentioned

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

@chingiz

Chingiz Nazar

Love Robotics and IoT

关于 @chingiz
LEARN MORE ABOUT @CHINGIZ'S EXPERTISE AND PLACE ON THE INTERNET.
react to story with heart

我想出了一种在 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(); } }

结论

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

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

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

相關故事

L O A D I N G
. . . comments & more!
Hackernoon hq - po box 2206, edwards, colorado 81632, usa