Diy

Arduino Retro Gaming с OLED-дисплеем

Arduino Retro Gaming с OLED-дисплеем

Вы когда-нибудь задумывались, сколько работы нужно, чтобы написать свои собственные ретро-игры? Насколько легко Понг кодировать для Arduino? Присоединяйтесь ко мне, покажу вам, как создать мини-ретро-игровую консоль на базе Arduino и как программировать Pong с нуля. Вот конечный результат:

План строительства

Это довольно простая схема. Потенциометр (горшок) будет контролировать игру, а дисплей OLED будет управляться Arduino. Это будет сделано на макете, однако вы можете сделать это постоянным контуром и установить его в футляр. Мы писали о воссоздании понга раньше, однако сегодня я покажу вам, как писать код с нуля, и ломая каждую часть.

Что вам нужно

Настройка ретро Arduino

Вот что вам нужно:

  • 1 х Arduino (любая модель)
  • 1 х 10 К Потенциометр
  • 1 x 0,96 ″ I2C OLED-дисплей
  • 1 х макет
  • Ассорти мужской> мужской провода

Любой Arduino должен работать, так что посмотрите наше руководство по покупке. если вы не уверены, какую модель купить.

Эти OLED дисплеи очень крутые. Обычно их можно приобрести в белом, синем, желтом цвете или в смеси из трех. Они существуют в полном цвете, однако они добавляют совершенно другой уровень сложности и стоимости этого проекта.

Схема

Это довольно простая схема. Если у вас нет большого опыта работы с Arduino, ознакомьтесь с этими первую очередь.

Вот:

Макет понг

Глядя на переднюю часть кастрюли, подключите левый контакт к + 5В, а правый — к заземлению . Подсоедините средний контакт к аналоговому контакту 0 (A0).

OLED-дисплей подключен с использованием протокола I2C . Подключите VCC и GND к Arduino + 5V и заземлению . Подключите SCL к аналоговой пятерке ( A5 ). Подключите SDA к аналогу 4 ( A4 ). Причина, по которой это связано с аналоговыми контактами, проста; эти контакты содержат схемы, необходимые для протокола I2C. Убедитесь, что они правильно подключены и не пересекаются. Точные контакты будут различаться в зависимости от модели, но A4 и A5 используются на Nano и Uno. Проверьте документацию библиотеки Wire для вашей модели, если вы не используете Arduino или Nano.

Пот тест

Загрузите этот тестовый код (убедитесь, что вы выбрали правильную плату и порт в меню Инструменты > Плата и Инструменты > Меню портов ):

void setup() { // put your setup code here, to run once: Serial.begin(9600); // setup serial } void loop() { // put your main code here, to run repeatedly: Serial.println(analogRead(A0)); // print the value from the pot delay(500); } 

Теперь откройте последовательный монитор ( вверху справа > Serial Monitor ) и поверните горшок. Вы должны увидеть значение, отображаемое на последовательном мониторе. Полностью против часовой стрелки должно быть ноль , а по часовой стрелке должно быть 1023 :

Понг серийный монитор

Вы скорректируете это позже, но пока все в порядке. Если ничего не происходит или значение изменяется без каких-либо действий, отсоедините и дважды проверьте цепь.

OLED тест

OLED Графика

OLED-дисплей немного сложнее в настройке. Сначала вам нужно установить две библиотеки, чтобы управлять дисплеем. Загрузите библиотеки Adafruit_SSD1306 и Adafruit-GFX с Github. Скопируйте файлы в папку с вашими библиотеками. Это зависит от вашей операционной системы:

  • Mac OS: / Пользователи / Имя пользователя / Документы / Arduino / Библиотеки
  • Linux: / home / Имя пользователя / Sketchbook
  • Windows: / Пользователи / Arduino / библиотеки

Теперь загрузите тестовый эскиз. Перейдите в Файл > Примеры > Adafruit SSD1306 > ssd1306_128x64_i2c . Это должно дать вам большой эскиз, содержащий много графики:

OLED Графика

Если после загрузки ничего не происходит, отключите и дважды проверьте ваши подключения. Если примеры не указаны в меню, вам может потребоваться перезапустить вашу Arduino IDE.

Код

Теперь пришло время для кода. Я буду объяснять каждый шаг, так что пропустите до конца, если вы просто хотите запустить его. Это достаточное количество кода, поэтому, если вы не уверены в себе, ознакомьтесь с этими 10 бесплатными ресурсами. кодировать чтобы научиться кодировать.

Начните с включения необходимых библиотек:

 #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> 

SPI и WIRE — две библиотеки Arduino для обработки связи I2C. Adafruit_GFX и Adafruit_SSD1306 — это библиотеки, которые вы установили ранее.

Далее настройте отображение:

 Adafruit_SSD1306 display(4); 

Затем настройте все переменные, необходимые для запуска игры:

 int resolution[2] = {128, 64}, ball[2] = {20, (resolution[1] / 2)}; const int PIXEL_SIZE = 8, WALL_WIDTH = 4, PADDLE_WIDTH = 4, BALL_SIZE = 4, SPEED = 3; int playerScore = 0, aiScore = 0, playerPos = 0, aiPos = 0; char ballDirectionHori = 'R', ballDirectionVerti = 'S'; boolean inProgress = true; 

В них хранятся все данные, необходимые для запуска игры. Некоторые из них хранят местоположение мяча, размер экрана, местоположение игрока и так далее. Обратите внимание, что некоторые из них являются постоянными, то есть они постоянны и никогда не изменятся. Это позволяет компилятору Arduino ускорить процесс.

Разрешение экрана и расположение мяча хранятся в массивах . Массивы представляют собой наборы похожих вещей, а для шара хранятся координаты ( X и Y ). Доступ к элементам в массивах прост (не включайте этот код в ваш файл):

 resolution[1]; 

Поскольку массивы начинаются с нуля, это вернет второй элемент в массиве разрешения ( 64 ). Обновлять элементы еще проще (опять же, не включайте этот код):

 ball[1] = 15; 

Внутри void setup () настройте отображение:

 void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.display(); } 

Первая строка сообщает библиотеке Adafruit, какие размеры и протокол связи используется вашим дисплеем (в данном случае 128 x 64 и I2C ). Вторая строка ( display.display () ) указывает экрану показывать все, что хранится в буфере (что является ничем).

Создайте два метода с именами drawBall и eraseBall :

 void drawBall(int x, int y) { display.drawCircle(x, y, BALL_SIZE, WHITE); } void eraseBall(int x, int y) { display.drawCircle(x, y, BALL_SIZE, BLACK); } 

Они берут координаты x и y шара и рисуют его на экране, используя метод drawCircle из библиотек отображения. При этом используется константа BALL_SIZE, определенная ранее. Попробуйте изменить это и посмотреть, что произойдет. Этот метод drawCircle принимает цвет пикселя — ЧЕРНЫЙ или БЕЛЫЙ . Поскольку это монохроматический дисплей (один цвет), белый цвет означает, что пиксель включен, а черный выключает пиксель.

Теперь создайте метод с именем moveAi :

 void moveAi() { eraseAiPaddle(aiPos); if (ball[1] > aiPos) { ++aiPos; } else if (ball[1] < aiPos) { --aiPos; } drawAiPaddle(aiPos); } 

Этот метод обрабатывает перемещение искусственного интеллекта или ИИ игрока. Это довольно простой компьютерный противник — если мяч находится над веслом, двигайтесь вверх. Если он под веслом, двигайтесь вниз. Довольно просто, но работает хорошо. Символы увеличения и уменьшения используются ( ++ aiPos и –aiPos ), чтобы добавить или вычесть один из aiPosition. Вы можете добавить или вычесть большее число, чтобы заставить ИИ двигаться быстрее, и, следовательно, его будет сложнее победить. Вот как бы вы это сделали:

 aiPos += 2; 

А также:

 aiPos -= 2; 

Знаки « плюс» и « минус» являются сокращением для сложения или вычитания двух из / из текущего значения aiPos. Вот еще один способ сделать это:

 aiPos = aiPos + 2; 

а также

 aiPos = aiPos - 1; 

Обратите внимание, как этот метод сначала стирает весло, а затем рисует его снова. Это должно быть сделано так. Если бы было нарисовано новое положение весла, на экране было бы два перекрывающихся весла.

Метод drawNet использует две петли для рисования сети:

 void drawNet() { for (int i = 0; i < (resolution[1] / WALL_WIDTH); ++i) { drawPixel(((resolution[0] / 2) - 1), i * (WALL_WIDTH) + (WALL_WIDTH * i), WALL_WIDTH); } } 

Это использует переменные WALL_WIDTH, чтобы установить его размер.

Создайте методы, называемые drawPixels и erasePixels . Как и в случае с шариками, единственное различие между ними состоит в цвете пикселей:

 void drawPixel(int posX, int posY, int dimensions) { for (int x = 0; x < dimensions; ++x) { for (int y = 0; y < dimensions; ++y) { display.drawPixel((posX + x), (posY + y), WHITE); } } } void erasePixel(int posX, int posY, int dimensions) { for (int x = 0; x < dimensions; ++x) { for (int y = 0; y < dimensions; ++y) { display.drawPixel((posX + x), (posY + y), BLACK); } } } 

Опять же, оба эти метода используют два цикла for для рисования группы пикселей. Вместо того, чтобы рисовать каждый пиксель, используя метод библиотеки drawPixel , циклы рисуют группу пикселей на основе заданных размеров.

Метод drawScore использует текстовые функции библиотеки, чтобы вывести на экран счет игрока и AI. Они хранятся в playerScore и aiScore :

 void drawScore() { display.setTextSize(2); display.setTextColor(WHITE); display.setCursor(45, 0); display.println(playerScore); display.setCursor(75, 0); display.println(aiScore); } 

Этот метод также имеет аналог eraseScore , который устанавливает черные пиксели или выключает их.

Последние четыре метода очень похожи. Они рисуют и стирают весла игрока и AI:

 void erasePlayerPaddle(int row) { erasePixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH); erasePixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(0, row, PADDLE_WIDTH); erasePixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH); } 

Обратите внимание, как они вызывают метод erasePixel, созданный ранее. Эти методы рисуют и стирают соответствующие весла.

В основном цикле немного больше логики. Вот весь код:

 #include <SPI.h> #include <Wire.h> #include <Adafruit_GFX.h> #include <Adafruit_SSD1306.h> Adafruit_SSD1306 display(4); int resolution[2] = {128, 64}, ball[2] = {20, (resolution[1] / 2)}; const int PIXEL_SIZE = 8, WALL_WIDTH = 4, PADDLE_WIDTH = 4, BALL_SIZE = 4, SPEED = 3; int playerScore = 0, aiScore = 0, playerPos = 0, aiPos = 0; char ballDirectionHori = 'R', ballDirectionVerti = 'S'; boolean inProgress = true; void setup() { display.begin(SSD1306_SWITCHCAPVCC, 0x3C); display.display(); } void loop() { if (aiScore > 9 || playerScore > 9) { // check game state inProgress = false; } if (inProgress) { eraseScore(); eraseBall(ball[0], ball[1]); if (ballDirectionVerti == 'U') { // move ball up diagonally ball[1] = ball[1] - SPEED; } if (ballDirectionVerti == 'D') { // move ball down diagonally ball[1] = ball[1] + SPEED; } if (ball[1] <= 0) { // bounce the ball off the top ballDirectionVerti = 'D'; } if (ball[1] >= resolution[1]) { // bounce the ball off the bottom ballDirectionVerti = 'U'; } if (ballDirectionHori == 'R') { ball[0] = ball[0] + SPEED; // move ball if (ball[0] >= (resolution[0] - 6)) { // ball is at the AI edge of the screen if ((aiPos + 12) >= ball[1] && (aiPos - 12) <= ball[1]) { // ball hits AI paddle if (ball[1] > (aiPos + 4)) { // deflect ball down ballDirectionVerti = 'D'; } else if (ball[1] < (aiPos - 4)) { // deflect ball up ballDirectionVerti = 'U'; } else { // deflect ball straight ballDirectionVerti = 'S'; } // change ball direction ballDirectionHori = 'L'; } else { // GOAL! ball[0] = 6; // move ball to other side of screen ballDirectionVerti = 'S'; // reset ball to straight travel ball[1] = resolution[1] / 2; // move ball to middle of screen ++playerScore; // increase player score } } } if (ballDirectionHori == 'L') { ball[0] = ball[0] - SPEED; // move ball if (ball[0] <= 6) { // ball is at the player edge of the screen if ((playerPos + 12) >= ball[1] && (playerPos - 12) <= ball[1]) { // ball hits player paddle if (ball[1] > (playerPos + 4)) { // deflect ball down ballDirectionVerti = 'D'; } else if (ball[1] < (playerPos - 4)) { // deflect ball up ballDirectionVerti = 'U'; } else { // deflect ball straight ballDirectionVerti = 'S'; } // change ball direction ballDirectionHori = 'R'; } else { ball[0] = resolution[0] - 6; // move ball to other side of screen ballDirectionVerti = 'S'; // reset ball to straight travel ball[1] = resolution[1] / 2; // move ball to middle of screen ++aiScore; // increase AI score } } } drawBall(ball[0], ball[1]); erasePlayerPaddle(playerPos); playerPos = analogRead(A2); // read player potentiometer playerPos = map(playerPos, 0, 1023, 8, 54); // convert value from 0 - 1023 to 8 - 54 drawPlayerPaddle(playerPos); moveAi(); drawNet(); drawScore(); } else { // somebody has won display.clearDisplay(); display.setTextSize(4); display.setTextColor(WHITE); display.setCursor(0, 0); // figure out who if (aiScore > playerScore) { display.println("YOU LOSE!"); } else if (playerScore > aiScore) { display.println("YOU WIN!"); } } display.display(); } void moveAi() { // move the AI paddle eraseAiPaddle(aiPos); if (ball[1] > aiPos) { ++aiPos; } else if (ball[1] < aiPos) { --aiPos; } drawAiPaddle(aiPos); } void drawScore() { // draw AI and player scores display.setTextSize(2); display.setTextColor(WHITE); display.setCursor(45, 0); display.println(playerScore); display.setCursor(75, 0); display.println(aiScore); } void eraseScore() { // erase AI and player scores display.setTextSize(2); display.setTextColor(BLACK); display.setCursor(45, 0); display.println(playerScore); display.setCursor(75, 0); display.println(aiScore); } void drawNet() { for (int i = 0; i < (resolution[1] / WALL_WIDTH); ++i) { drawPixel(((resolution[0] / 2) - 1), i * (WALL_WIDTH) + (WALL_WIDTH * i), WALL_WIDTH); } } void drawPixel(int posX, int posY, int dimensions) { // draw group of pixels for (int x = 0; x < dimensions; ++x) { for (int y = 0; y < dimensions; ++y) { display.drawPixel((posX + x), (posY + y), WHITE); } } } void erasePixel(int posX, int posY, int dimensions) { // erase group of pixels for (int x = 0; x < dimensions; ++x) { for (int y = 0; y < dimensions; ++y) { display.drawPixel((posX + x), (posY + y), BLACK); } } } void erasePlayerPaddle(int row) { erasePixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH); erasePixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(0, row, PADDLE_WIDTH); erasePixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH); } void drawPlayerPaddle(int row) { drawPixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH); drawPixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH); drawPixel(0, row, PADDLE_WIDTH); drawPixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH); drawPixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH); } void drawAiPaddle(int row) { int column = resolution[0] - PADDLE_WIDTH; drawPixel(column, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH); drawPixel(column, row - PADDLE_WIDTH, PADDLE_WIDTH); drawPixel(column, row, PADDLE_WIDTH); drawPixel(column, row + PADDLE_WIDTH, PADDLE_WIDTH); drawPixel(column, row + (PADDLE_WIDTH * 2), PADDLE_WIDTH); } void eraseAiPaddle(int row) { int column = resolution[0] - PADDLE_WIDTH; erasePixel(column, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH); erasePixel(column, row - PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(column, row, PADDLE_WIDTH); erasePixel(column, row + PADDLE_WIDTH, PADDLE_WIDTH); erasePixel(column, row + (PADDLE_WIDTH * 2), PADDLE_WIDTH); } void drawBall(int x, int y) { display.drawCircle(x, y, BALL_SIZE, WHITE); } void eraseBall(int x, int y) { display.drawCircle(x, y, BALL_SIZE, BLACK); } 

Вот то, что вы в конечном итоге:

OLED Pong

Если вы уверены в коде, вы можете внести множество изменений:

  • Добавить меню для уровней сложности (изменить AI и скорость мяча).
  • Добавьте случайное движение к мячу или искусственному интеллекту.
  • Добавьте еще один банк для двух игроков.
  • Добавьте кнопку паузы.

Теперь взгляните на эти проекты ретро-игр Pi Zero. проектов

Вы кодировали Понг, используя этот код? Какие модификации вы сделали? Дайте мне знать в комментариях ниже, я хотел бы показаться несколько фотографий!

Похожие посты
Diy

Вещи, которые вы должны иметь в виду, прежде чем брать ноутбук или смартфон в службу технической поддержки

Diy

Как использовать Arduino для съемки красивой скоростной фотографии

DiyLinux

Пять великолепных расширений Raspberry Pi, которые делают его еще более полезным

Diy

Первые шаги с Arduino: пристальный взгляд на печатную плату и структуру программы