Как я разработал классическую игру в понг на плате Arduino
16 апреля 2022 г.Я нашел способ разработать популярную игру в понг на плате Arduino.
Игра не сложная, но интересно развиваться и играть.
Здесь использовались 0,96-дюймовый OLED-дисплей и две кнопки. Дисплей небольшой, но достаточно для нашего проекта. Две кнопки будут использоваться для перемещения нашей ракетки вверх и вниз.
Размер экрана – 128 x 64 пикселя. Первые и последние 16 пикселей будут использоваться для отображения результатов.
Результаты будут отображаться вертикально.
Одна ракетка будет управляться игроком, а вторая, соперником, будет управляться Arduino. Мы напишем код, чтобы он пытался двигаться к мячу. В начале игры он будет двигаться медленно, а затем постепенно ускоряться.
Мы будем управлять направлением мяча через переменные ball_direction_X и ball_direction_Y. Мяч будет двигаться в указанном направлении каждый момент. Если мяч ударится о стену, ball_direction_Y будет изменен на противоположное, и та же логика будет применена к ball_direction_X и ракеткам.
Код проекта опубликован на моем GitHub [страница проекта Arduino Ping Pong Game] (https://github.com/sultanbekuly/Arduino_ping_pong/blob/main/Arduino_ping_pong.ino).
Связь
Одна ножка кнопок будет подключена к GND. Вторые ноги будут подключены к контактам 6 и 5 Arduino.
Дисплей будет подключен пинами I2C: V - 5V, GND - GND, SCL - A5, SDA - A4.
Разработка Pong на Arduino
Давайте подключим библиотеки и инициируем все необходимые нам переменные. Здесь у нас есть кнопки, очки, игрок, противник и переменные, связанные с мячом.
```нажмите
//oled-библиотеки:
include
include
include
include
// переменные oled:
define SCREEN_WIDTH 128 // Ширина OLED-дисплея в пикселях
define SCREEN_HEIGHT 64 // Высота OLED-дисплея в пикселях
define OLED_RESET 4 // Номер вывода сброса (или -1, если используется общий вывод сброса Arduino)
define SCREEN_ADDRESS 0x3C ///< Адрес см. в таблице данных; 0x3D для 128x64, 0x3C для 128x32
Дисплей Adafruit_SSD1306 (SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//Кнопки:
const int buttonUP = 6;
const int buttonDOWN = 5;
//переменные кнопок:
int lastButtonStateUP = НИЗКИЙ; // предыдущее чтение с входного вывода
int lastButtonStateDOWN = НИЗКИЙ; // предыдущее чтение с входного вывода
unsigned long debounceDelay = 10; // время устранения дребезга; увеличить, если выход мерцает
// ИГРОВЫЕ переменные:
//результаты:
интервал player_score = 0;
инт вражеский_счет = 0;
//игрок:
int player_position_X = 19; // статический
int player_position_Y = 0;
int player_width = 16;
интервал player_thickness = 4;
//враг:
int вражеская_позиция_X = 104; // статический
int вражеская_позиция_Y = 47;
инт вражеский_ширина = 16;
целая вражеская_толщина = 4;
длинное вражеское_последнее_движение_время = 0;
long вражеский_speed_of_moving = 2000;//время обновления в мс
//мяч:
//пусто fillCircle(uint16_t x0, uint16_t y0, uint16_t r, uint16_t color);
int ball_position_X = 63;
int ball_position_Y = 31;
интервал ball_radius = 1;
интервал ball_direction_X = 3;
интервал ball_direction_Y = 3;
int ball_speed = 8;//9,8,7...1
длинный ball_last_move_time = 0;
В настройке мы инициируем кнопки Serial, randomSeed и display. После этого мы покажем стартовый экран и начнем игру:
```нажмите
недействительная установка () {
pinMode (кнопкаUP, INPUT_PULLUP);
pinMode (кнопкаDOWN, INPUT_PULLUP);
Серийный.начать(9600);
Serial.println("Старт");
//инициируем рандом
randomSeed (аналоговое чтение (0));
ball_direction_X = -3;
ball_direction_Y = случайное (-5, 5);
//ball_direction_Y = -5;//тест
// SSD1306_SWITCHCAPVCC = генерировать внутреннее напряжение дисплея от 3,3 В
если(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("Ошибка выделения SSD1306"));
за(;;); // Не продолжать, цикл навсегда
// Показать содержимое буфера начального отображения на экране --
// библиотека инициализирует это с помощью экрана-заставки Adafruit.
дисплей.дисплей();
// Очистить буфер
дисплей.clearDisplay();
//рисовать линии:
display.drawLine(16, 0, 16, 63, SSD1306_WHITE);
display.drawLine(111, 0, 111, 63, SSD1306_WHITE);
дисплей.дисплей();
//поле результатов init:
дисплей.setTextSize (2);
display.setTextColor (SSD1306_WHITE); // Рисуем белый текст
player_score = 8888; // контрольная работа
вражеский_счет = 8888; // контрольная работа
print_score (player_score, 0);
print_score(enemy_score, 115);
дисплей.setTextSize(3);
display.setCursor(28, 0);
display.write("Пинг");
display.setCursor(28, 31);
display.write("Понг");
дисплей.дисплей();
дисплей.setTextSize (2);
задержка (2000 г.); // Пауза на 2 секунды
//НОВАЯ ИГРА:
// Очистить буфер
дисплей.clearDisplay();
//рисовать линии:
display.drawLine(16, 0, 16, 63, SSD1306_WHITE);
display.drawLine(111, 0, 111, 63, SSD1306_WHITE);
дисплей.дисплей();
// Запись баллов:
player_score = 0; //сбросить player_score
вражеский счет = 0; // сбросить вражеский счет
print_score (player_score, 0);
print_score(enemy_score, 115);
//Показать игроков:
//пусто fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_th h, uint16_t color);
display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE);
display.fillRect (позиция_врага_X, позиция_врага_Y, толщина_врага, ширина_врага, SSD1306_WHITE);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
// Отображение мяча:
display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_WHITE);
дисплей.дисплей();
задержка(500); // Пауза на 0,5 секунды
В цикле у нас есть три основные функции, которые будут подробно описаны ниже.
```нажмите
недействительный цикл () {
кнопки_проверить();
move_the_ball_and_check_for_collisions();
move_enemy();
В функции buttons_check мы проверим, нажата ли кнопка UP или DOWN, и соответствующим образом обновим положение ракетки игрока. Изменение положения ракетки выполняется в три основных этапа: рисование черного прямоугольника в старом положении, изменение переменной положения и рисование белого прямоугольника в новом положении.
```нажмите
пустые кнопки_check () {
if (!digitalRead(buttonUP) && !lastButtonStateUP) {
lastButtonStateUP = истина;
// Serial.println("ВВЕРХ нажата");
если (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);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
если (digitalRead(buttonUP) && lastButtonStateUP) {
lastButtonStateUP = ложь;
if (!digitalRead(buttonDOWN) && !lastButtonStateDOWN) {
lastButtonStateDOWN = истина;
// Serial.println("НАЖАТЬ ВНИЗ");
если (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);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
если (digitalRead(buttonDOWN) && lastButtonStateDOWN) {
lastButtonStateDOWN = ложь;
В функции move_the_ball_and_check_for_collisions мы будем перемещать мяч и проверять его на столкновение с горизонтальными стенами, ракетками и вертикальными стенами (игрок выигрывает или проигрывает). Перед перемещением стены проверим требуемое время, прошедшее с последнего движения шарика. Я установил необходимое время как скорость мяча, умноженную на двадцать. Чтобы отобразить мяч, мы нарисуем закрашенный круг. Как правило, обновление положения мяча выполняется так же, как обновление положения ракетки.
Чтобы проверить столкновение мяча с одной из горизонтальных стен, нам нужно убедиться, что сумма 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.
Как всегда та же логика для удара по мячу ракеткой противника.
```нажмите
недействительными move_the_ball_and_check_for_collisions(){
//перемещаем шар:
if(millis() > ball_speed*20+ball_last_move_time){
// стираем мяч на старой позиции:
display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_BLACK);
дисплей.дисплей();
// устанавливаем новую позицию шара:
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;
// рисуем мяч на новой позиции:
display.fillCircle(ball_position_X, ball_position_Y, ball_radius, SSD1306_WHITE);
дисплей.дисплей();
// Serial.print("ball_position_Y: "); Serial.println (ball_position_Y);
ball_last_move_time = миллис();
//Проверяем, не потерял ли игрок:
если(ball_position_X < player_position_X){
Serial.println("Игрок проиграл!");
newRound("враг");//игрок
//проверка на столкновение мяча и игрока:
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("Столкновение мяча и игрока");
//отправляем мяч врагу со случайными значениями:
ball_direction_X = 3;
ball_direction_Y = случайное (-5, 5);
display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
//проверка отсутствия врага:
if(ball_position_X > вражеская_позиция_X+вражеская_толщина){
Serial.println("Враг проиграл!");
newRound("игрок");//враг
//проверка на столкновение мяча и противника:
if(enemy_position_X <= ball_position_X && вражеская_позиция_X+вражеская_толщина >= ball_position_X
&& вражеская_позиция_Y <= ball_position_Y && вражеская_позиция_Y+вражеская_ширина >= ball_position_Y){
Serial.println("Столкновение мяча и противника");
//отправляем мяч игроку со случайными значениями:
ball_direction_X = -3;
ball_direction_Y = случайное (-5, 5);
display.fillRect (позиция_врага_X, позиция_врага_Y, толщина_врага, ширина_врага, SSD1306_WHITE);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
Как вы заметили выше, функция newRound принимает в качестве входных данных победителя (игрок/противник). Ввод требуется для увеличения соответствующего балла. В функции мы сбросим все необходимые переменные, связанные с игрой, распечатаем обновленные результаты, отобразим все игровые объекты и, при необходимости, обновим скорость противника и мяча. Игра будет усложняться по мере прохождения.
```нажмите
void newRound (победитель строки) {
// Очистить буфер
дисплей.clearDisplay();
//рисовать линии:
display.drawLine(16, 0, 16, 63, SSD1306_WHITE);
display.drawLine(111, 0, 111, 63, SSD1306_WHITE);
дисплей.дисплей();
//Обновить баллы:
если(победитель == "враг"){
вражеский_счет++;
}еще{
player_score++;
print_score (player_score, 0);
print_score(enemy_score, 115);
// сбросить игровые переменные:
//игрок:
player_position_X = 19; // статический
player_position_Y = 0;
player_width = 16;
player_thickness = 4;
//мяч:
ball_position_X = 63;
ball_position_Y = 31;
ball_radius = 1;
//задаем случайное направление для го шара:
ball_direction_X = -3;
ball_direction_Y = случайное (-5, 5);
//ball_direction_Y = -5;//тест
ball_last_move_time = 0;
//Показать игроков:
//пусто fillRect(uint16_t x0, uint16_t y0, uint16_t w, uint16_th h, uint16_t color);
display.fillRect(player_position_X, player_position_Y, player_thickness, player_width, SSD1306_WHITE);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
//враг:
вражеская_позиция_X = 104; // статический
вражеская_позиция_Y = 47;
вражеская_ширина = 16;
вражеская_толщина = 4;
вражеский_последний_ход_время = 0;
// проверяем, нужно ли нам обновлять вражескую скорость_оф_движения и мяч_скорость
если((player_score+enemy_score)%5 == 0){
//5,10,15 и так далее
если(ball_speed > 3) ball_speed = ball_speed - 1; //10,9,8...
Serial.print("ball_speed: ");Serial.println(ball_speed);
если((счет_игрока+счет_врага)%10 == 0){
//10,20,30 и так далее
if(скорость_движения_врага > 1) скорость_движения_врага = скорость_движения_врага * 0,9; //2000,1800,1620,1458...
Serial.print("скорость_движения_врага: ");Serial.println(скорость_движения_врага);
задержка(500); // Пауза на 0,5 секунды
Печатать партитуры по вертикали немного сложно. Поэтому я решил выделить его в отдельную функцию. Каждая цифра счета будет отображаться одна за другой, и курсор необходимо соответствующим образом обновить.
```нажмите
void print_score(int temp_num, int X){ //0/115
для (целое я = 48; я> = 0; я- = 16) {
int число = temp_num % 10;
символ cstr[16];
итоа(число, cstr, 10);
display.setCursor(X, я);
display.write(cstr);
дисплей.дисплей();
// Serial.println(cstr);
temp_num = temp_num/10;
если (temp_num == 0) {
перемена;
И последний — перемещение вражеской ракетки. Он попытается ударить по мячу. А для этого постарается сравнять свой центр с центром шара. Здесь у нас есть дополнительные проверки, чтобы ракетка оставалась внутри экрана. Чтобы такого врага можно было победить, его скорость передвижения будет вначале медленной, а по ходу игры будет увеличиваться.
```нажмите
недействительными move_enemy () {
//враг:
if(millis() > вражеская_скорость_движения+вражеское_последнее_время_движения){
display.fillRect (позиция_врага_X, позиция_врага_Y, толщина_врага, ширина_врага, SSD1306_BLACK);
if(ball_position_Y < вражеская_позиция_Y+вражеская_ширина/2){
вражеская_позиция_Y = вражеская_позиция_Y - 3;
}еще{
вражеская_позиция_Y = вражеская_позиция_Y + 3;
//проверка, находится ли враг в стене:
if(enemy_position_Y > 64-player_width) вражеская_позиция_Y = 64-player_width;
если(вражеская_позиция_Y < 0) вражеская_позиция_Y = 0;
// Serial.print("enemy_position_Y: "); Serial.println(вражеская_позиция_Y);
display.fillRect (позиция_врага_X, позиция_врага_Y, толщина_врага, ширина_врага, SSD1306_WHITE);
дисплей.дисплей(); // Обновляем экран с каждым вновь нарисованным прямоугольником
вражеский_последний_ход_время = миллис();
Вывод
Надеюсь, вам понравилось как разработка игры, так и игра. Игра знакома многим с детства и при игре чувствуется ностальгия. У разработчиков есть уникальная возможность познакомиться с игрой не только снаружи, но и увидеть, как все устроено изнутри.
Вот несколько идей по улучшению проекта:
- В настоящее время игра бесконечна. Было бы здорово внедрить Официальные правила настольного тенниса, чтобы определить победителя.
- По мере увеличения скорости мяча делайте визуальный эффект, чтобы у мяча появился хвост.
- Угол полета мяча при отскоке следует поставить в зависимость от места касания мячом ракетки.
Оригинал