Как создать игру Arduino Starship, управляемую джойстиком и компьютером

Как создать игру Arduino Starship, управляемую джойстиком и компьютером

1 апреля 2022 г.

В этой статье мы разработаем игру Arduino Starship, которая будет отображаться на ЖК-дисплее 16x2. Игра будет управляться джойстиком и компьютером через Serial Monitor. Кроме того, мы будем хранить высокие баллы в EEPROM и обновлять их, когда запись будет побита.


Код проекта опубликован на моем GitHub страница проекта Arduino_Starship_Game. В статье мы разберемся с каждым элементом отдельно и как все это будет работать вместе.


Проект интересен и открывает возможности узнать новое:


  • Как работает ЖК-дисплей

  • Как сделать и отобразить пользовательский символ на ЖК-дисплее

  • Как читать данные из последовательного монитора

  • Как работает джойстик

  • Как читать и записывать данные в EEPROM (энергонезависимую память)

В игре игрок управляет звездолетом. Перед летающим звездолетом будут враги. Поскольку произойдет столкновение звездолета с одним из врагов, игра будет окончена. У Starship есть пуля для стрельбы, но с пределом. Предел для пули - только одна пуля за раз. Это означает, что пока мы видим пулю на экране, дальше стрелять нельзя, нужно дождаться, пока она не столкнется с одним из врагов или не исчезнет с экрана.


процесс игры


Видео геймплея:


https://www.youtube.com/watch?v=HW6j_PRgFx4


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


ЖК дисплей


Начнем с ЖК-дисплея. В проекте я использовал популярный ЖК-дисплей 16x2, который можно найти почти в каждом наборе Arduino. В моем случае дисплей поставляется с ЖК-адаптером I2C, а соединение будет GND-GND, VCC-5V, SDA-A4 и SCL-A5.


Код


Как всегда, в первую очередь нам нужно подключить библиотеки:


```нажмите


include


include


LCD LiquidCrystal_I2C (0x3F, 16, 2);


В функции LiquidCrystal_I2C lcd(0x3F, 16, 2) мы определяем адрес нашего ЖК-адаптера I2C. И да, это означает, что мы можем подключить множество элементов I2C к Arduino. Адрес по умолчанию 0x3F или 0x27. Следующие два элемента — это размер нашего дисплея.


Вот как мы инициируем и отображаем текст:


```нажмите


недействительная установка ()


ЖК.начало();


ЖК-подсветка();


ЖК.очистить();


lcd.setCursor(0,0);


lcd.print("Привет, мир!");


lcd.setCursor(0,1);


lcd.print("Чингиз");


lcd.begin() - запускает ЖК-дисплей.


lcd.backlight() - включает подсветку LCD.


lcd.clear() – очищает дисплей.


lcd.setCursor(0,0) - устанавливает курсор в позицию записи. Обратите внимание, что первая цифра — ось X, а вторая цифра — ось Y.


lcd.print("Hello World!") - выводит написанный текст на LCD.


Пользовательские символы


Каждая цифра дисплея состоит из 5x8 пикселей. Чтобы создать собственного персонажа в виде космического корабля, нам нужно определить и инициировать его:


```нажмите


байт c1[8]={B00000,B01010,B00000,B00000,B10001,B01110,B00000,B00000}; //Улыбка-1


байт c2[8]={B10000,B10100,B01110,B10101,B01110,B10100,B10000,B00000}; //Звездолет-2


//В настройке:


lcd.createChar(0, c1); //Создание пользовательских персонажей в CG-RAM


lcd.createChar(1, c2); //Создание пользовательских персонажей в CG-RAM


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


Я нашел сайт, который будет полезен при создании пользовательских персонажей на LCD. Здесь вы можете нарисовать своего собственного персонажа, и для него будет автоматически сгенерирован код:


Создание пользовательского персонажа на ЖК-дисплее


И вот как мы отображаем наши пользовательские символы:


```javascript


lcd.print (знак (0));


lcd.print (символ (1));


Hello World с пользовательскими персонажами


Джойстик


На джойстике есть кнопки, оси X и Y. Кнопка работает как обычно. Оси X и Y можно рассматривать как потенциометр, который предоставляет данные от 0 до 1023. Значение по умолчанию составляет половину этого значения. Мы будем использовать только ось X для управления звездолетом. Я подключил SW к контакту 2 и ось X к A1.


Вот запуск джойстика:


```нажмите


// номера выводов Arduino


const int SW_pin = 2; // цифровой вывод, подключенный к коммутационному выходу


константный интервал X_pin = 1; // аналоговый вывод, подключенный к выходу X


//В настройке:


// инициация джойстика


pinMode(SW_pin, INPUT);


цифровая запись (SW_pin, ВЫСОКИЙ); //значение по умолчанию 1


Чтение данных джойстика и обнаружение команд:


```нажмите


//В цикле:


//Ввод команд с помощью джойстика:


если (цифровое чтение (SW_pin) == НИЗКИЙ) {


//Обнаружена огненная пуля


если (аналоговое чтение (X_pin)> 612) {


//Обнаружена команда «вверх»


если (аналоговое чтение (X_pin) <412) {


//Обнаружена команда спуска


Разработка игр


Инициация


Подключим все библиотеки и запустим все необходимые переменные для игры:


  • Я постарался назвать каждую переменную так, чтобы было понятно, для чего она нужна.

  • Три пользовательских персонажа: звездолет, враг и пуля.

  • ЖК-массив 2x16, который использовался для легкой отладки игры.

  • game_score и game_start используются для получения счета игры.

  • и у нас есть некоторые переменные, связанные с пулей и врагами.

```нажмите


include


include


LCD LiquidCrystal_I2C (0x3F, 16, 2);


include


байт c1[8]={B10000,B10100,B01110,B10101,B01110,B10100,B10000,B00000}; //Звездолет


байт c2[8]={B00100,B01000,B01010,B10100,B01010,B01000,B00100,B00000}; //Враг


байт c3[8]={B00000,B00000,B00000,B00110,B00110,B00000,B00000,B00000}; //Пуля


Строка lcd_array[2][16] =


{{"}"," "," "," "," "," "," "," "," "," "," "," "," "," "," "," " },


{" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "}} ;


} - Звездолет


  • Пуля

< - Враг


const беззнаковое целое MAX_MESSAGE_LENGTH = 12;


int starship_possiton = 0;


bool game_is_in_progress = ложь;


unsigned long game_score = 0;


unsigned long game_start = 0;


bool bullet_is_in_progress = ложь;


int bullet_possiton[2];


unsigned long bullet_last_move = 0;


unsigned long bullet_speed = 100;


bool враги_массив[5] = {ложь,ложь,ложь,ложь,ложь};//{ложь,истина,истина,истина,истина};//


длинный номер ранда;


int animals_possiton[5][2] = {{-1,-1},{-1,-1},{-1,-1},{-1,-1},{-1,-1}} ;


unsigned long animals_last_move[5] = {0,0,0,0,0};


беззнаковые длинные враги_overall_last_move = 0;


беззнаковое длинное враги_скорость = 200;


символьное сообщение[MAX_MESSAGE_LENGTH] = ""; //w - ВВЕРХ, s - Вниз, f - Огонь


Команды:


Вверх


Вниз


Огонь


// номера выводов Arduino


const int SW_pin = 2; // цифровой вывод, подключенный к коммутационному выходу


константный интервал X_pin = 1; // аналоговый вывод, подключенный к выходу X


Настраивать


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


```нажмите


недействительная установка () {


Серийный.начать(9600);


ЖК.начало();


//Создание пользовательских персонажей в CG-RAM


lcd.createChar(1, c1);


lcd.createChar(2, c2);


lcd.createChar(3, c3);


//инициируем рандом


randomSeed (аналоговое чтение (0));


// инициация джойстика


pinMode(SW_pin, INPUT);


цифровая запись (SW_pin, ВЫСОКИЙ); //значение по умолчанию 1


//Стартовый экран игры


ЖК-подсветка();


ЖК.очистить();


lcd.setCursor(0,0);


lcd.print ("Звездный корабль");


lcd.setCursor(0,1);


lcd.print (символ (1));


lcd.print("Нажмите любую клавишу, чтобы начать");


Стартовый экран


Петля


В цикле мы будем слушать последовательный монитор, чтобы получить команду на подъем (w), вниз (s) или огонь (f):


```нажмите


в то время как (Серийный.доступный()> 0){


статическое целое число без знака message_pos = 0;


//Чтение следующего доступного байта в буфере последовательного приема


char inByte = Serial.read();


//Приходит сообщение (проверьте, не заканчивается ли символ) и следите за размером сообщения


if ( inByte != '
' && (message_pos < MAX_MESSAGE_LENGTH - 1)){


//Добавляем входящий байт к нашему сообщению


сообщение[сообщение_поз.] = inByte;


сообщение_pos++;


}else{//Полное сообщение получено...


//Добавить нулевой символ в строку


сообщение[сообщение_поз] = '\0';


// Печатаем сообщение (или делаем что-то другое)


Серийный.принт("[[");


Serial.print(сообщение);


Serial.println("]]");


print_array_to_serial();


//Сброс для следующего сообщения


сообщение_поз = 0;


При нажатии одной из клавиш игра запустится:


```нажмите


//Начать игру


if (game_is_in_progress==false && (message[0] == 'w' || сообщение[0] == 's' || сообщение[0] == 'f')){


game_is_in_progress = истина;


game_start = миллис();


Нам нужно обновить положение звездолета, когда мы получим команду «Вверх» или «Вниз». Когда мы получаем команду огня, нам нужно убедиться, что пуля еще не запущена, и после этого она будет инициирована с позицией X 1 и позицией Y в качестве текущей позиции звездолета.


```нажмите


//Обработка ввода


if(message[0] == 'w'){ // Команда вверх


звездолет_позитон = 0;


}else if(message[0] == 's'){ // Команда вниз


звездолет_позитон = 1;


}else if(message[0] == 'f' && bullet_is_in_progress == false){ //Огонь


bullet_possiton[0] = starship_possiton;


bullet_possiton[1] = 1;


bullet_is_in_progress = истина;


bullet_last_move = миллис();


Перемещение пули


Мы проверим, является ли сумма bullet_last_move и bullet_speed меньшей или равной millis(). Из-за этого, если вы хотите сделать пулю быстрее, необходимо уменьшить переменную bullet_speed. Мы будем перемещать пулю до конца экрана, и когда ее положение превысит размер экрана, пуля будет сброшена.


```нажмите


if(bullet_is_in_progress && bullet_last_move+bullet_speed <= миллисекунды()){


если (bullet_possiton[1] != 15){


Serial.println("движущаяся пуля");


bullet_last_move = миллис();


пуля_позитон[1] = пуля_позитон[1]+1;


}иначе, если(bullet_possiton[1] == 15){


bullet_possiton[1] = -1;


bullet_is_in_progress = ложь;


Инициация врагов


У нас будет максимум 5 врагов одновременно. Как и раньше, нам нужно проверить, есть ли у нас неактивный враг, чтобы активировать его. Кроме того, чтобы иметь немного пространства между врагами, мы будем ждать утроения скорости врагов от общего последнего хода врагов. Мы сгенерируем случайное значение от 0 до 6. Если значение равно нулю или единице, враг будет инициирован с соответствующей позицией Y и последней ячейкой (15) в позиции X.


```нажмите


//Инициация врагов


если((массив_врагов[0]==false || массив_врагов[1]==ложь ||


массив_врагов[2]==false || массив_врагов[3]==false || враги_массив[4]==false) &&


враги_овералл_последний_ход+враги_скорость*3 <= миллис() ){


// вывести случайное число от 0 до 6


случайное число = случайное (0, 6);


//      Serial.print("randNumber: "); Serial.println(randNumber);


если(случайноеЧисло==0 || случайноеЧисло==1){


//        Serial.print("Инициация врагов: "); Serial.println(randNumber);


для (целое я = 0; я <5; я ++) {


если (враги_массив [я] == ложь) {


lcd_array[randNumber][15]="<";


враги_массив[i]=истина;


враги_possiton[i][0] = случайноеЧисло;


враги_possiton[i][1] = 15;


враги_последний_ход[i] = миллис();


враги_общий_последний_ход = миллис ();


перерыв;


Перемещение врагов очень похоже на перемещение пули, но в обратном направлении:


```нажмите


//движущиеся враги


для (целое я = 0; я <5; я ++) {


if(enemies_array[i]==true && animals_last_move[i]+enemies_speed <= миллисекунды()){


Враги_позитон[i][1] = Враги_позитон[i][1] - 1;


враги_последний_ход[i] = миллис();


//если противник прошел через звездолет


если(враги_позитон[я][1]==-1){


враги_массив[i]=false;


Обновите lcd_array и проверьте краши.


Мы будем вставлять элементы нашей игры в lcd_array. По умолчанию все ячейки будут пустыми. Затем мы нарисуем звездолет, пулю и врагов. В массиве элементы имеют следующие символы:


  • } - звездолет

    • пуля

  • < - враг

```нажмите


для (целое я = 0; я <2; я ++) {


for(int j=0;j<16;j++)


если(game_is_in_progress){


lcd_array[i][j] = " ";//по умолчанию все ячейки пусты


//рисуем звездолет


если(starship_possiton==i && j==0){


lcd_array[i][j] = "}";


// рисование маркера


if(bullet_is_in_progress == true && bullet_possiton[0] == i &&


bullet_possiton[1] == j){


lcd_array[i][j] = ">";


// рисуем врагов


for(int k=0; k<5; k++){


if(enemies_array[k]==true && враги_позитон[k][0] == i


&& враги_позитон[к][1] == j){


lcd_array[i][j]="<";


Далее мы проверим наличие сбоев:


  • раздавить врага пулей

  • сокрушить врага звездолета

```нажмите


for(int k=0; k<5; k++){


if(bullet_is_in_progress == true && bullet_possiton[0] == i &&


bullet_possiton[1] == j &&


((enemies_array[k]==true && враги_позитон[k][0] == i


&& animals_possiton[k][1] == j) ||


(enemies_array[k]==true && враги_позитон[k][0] == i


&& враги_позитон[к][1] == j-1) )


Serial.println("Сокрушите врага пулями");


массив_врагов[k] = ложь;


враги_позитон[к][0] = -1;


враги_позитон[к][1] = -1;


bullet_is_in_progress = ложь;


bullet_possiton[0] = -1;


bullet_possiton[1] = -1;


lcd_array[i][j]=" ";


//раздавить врага космическим кораблем


если(j==0 && starship_possiton==i){


for(int k=0; k<5; k++){


if(enemies_array[k]==true && враги_позитон[k][0] == i


&& враги_позитон[к][1] == j){


Serial.println("звездолет сокрушает врага");


//Игра окончена. Твой счет. Рекорд


game_score = миллисекунды () - game_start;


//нужно сбросить все игровые значения


звездолет_позитон = 0;


game_is_in_progress = ложь;


bullet_is_in_progress = ложь;


for(int z=0; z<5; z++){


массив_врагов[z] = ложь;


враги_позитон[z][0] = -1;


враги_позитон[z][1] = -1;


скорость_врагов = 200;


сообщение[MAX_MESSAGE_LENGTH] = ""; //w - ВВЕРХ, s - Вниз, f - Огонь


перерыв;


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


https://www.youtube.com/watch?v=E5Qm3N1_h5o


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


После того, как звездолет и враг раздавят, мы получим игровой счет, вычитая миллисекунды начала игры из текущих миллисекунд. Также будут сброшены игровые переменные.


После обновления массива ЖК-дисплея мы напечатаем массив на ЖК-дисплее. Символы врага, пули и врага будут заменены нашими пользовательскими символами:


```нажмите


//Печать игры на ЖК-дисплей


для (целое я = 0; я <2; я ++) {


lcd.setCursor (0, я);


for(int j=0;j<16;j++){


если (lcd_array[i][j] == "}"){


lcd.print (символ (1));


}иначе, если(lcd_array[i][j] == "<"){


lcd.print (символ (2));


}иначе, если(lcd_array[i][j] == ">"){


lcd.print (символ (3));


}еще{


lcd.print(lcd_array[i][j]);


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


```нажмите


если(game_score!=0){


EEPROM.get(0, game_start);


Serial.print("Высокий балл: ");


Serial.println(game_start);


Serial.print("Оценка: ");


Serial.println(game_score);


//Игра окончена


ЖК.очистить();


lcd.setCursor(0,0);


lcd.print("Запись: ");


lcd.print (game_start);


lcd.setCursor(0,1);


lcd.print("Оценка:  ");


lcd.print(game_score);


если(game_score > game_start){


EEPROM.put(0, game_score);


game_score = 0;//сбросить игровой счет для следующей игры


Игра окончена


В конце цикла у нас будет короткая задержка и команда сброса:


```нажмите


задержка(50);


сообщение[0] = ' '; // команда сброса


Печать lcd_array на последовательный монитор была разделена по функциям и может отображаться по запросу или постоянно:


```нажмите


недействительным print_array_to_serial(){


//Вывод игры на последовательный монитор:


Serial.println("lcd_array:");


для (целое я = 0; я <2; я ++) {


for(int j=0;j<16;j++){


Serial.print(lcd_array[i][j]);


Серийный.println("");


А управление игрой джойстиком добавляется вот так просто:


```нажмите


если (цифровое чтение (SW_pin) == НИЗКИЙ) {


сообщение[0] = 'ф';


если (аналоговое чтение (X_pin)> 612) {


сообщение[0] = 'ж';


если (аналоговое чтение (X_pin) <412) {


сообщение[0] = 'с';


Заключение


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


Для поклонников Arduino и разработки игр это хорошая база для оттачивания навыков.


Лучшее после такого большого труда - играть в игру, которую разработали вы.


Надеюсь, проект был вам интересен. Благодаря этому проекту мы узнали и использовали на практике ЖК-дисплей и джойстик.


Вот несколько идей по улучшению проекта, которые вы можете реализовать:


  • Жизни

  • Уровни сложности игры

  • Усложнение игры по мере продвижения игрока

  • Сделайте босса врагом, у которого будет возможность посылать врагов и стрелять пулями.

  • Возможность ввести имя или ник, если рекорд был побит. Которые также будут занесены в EEPROM.


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE