Как создать игру 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));
Джойстик
На джойстике есть кнопки, оси 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.
Оригинал