0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Урок 47

Урок 47. Игра тетрис на Arduino

В этом уроке мы создадим игру «Тетрис». Это известная многим игра «головоломка» изобретённая и написанная советским программистом Алексеем Пажитновым. Первоначальная версия игры была написана для компьютера «Электроника-60» на языке Паскаль. Игра была выпущена 6 июня 1984 г. Мировую известность игра приобрела благодаря её выпуску на портативной консоли GameBoy компании Nintendo

Правила игры «Тетрис»:

Вверху игрового поля появляются случайные геометрические фигуры, которые падают пока не достигнут низа поля или других фигур. Во время падения, фигуры можно сдвигать по горизонтали (влево / вправо), поворачивать на 90° (по часовой стрелке) и ускорять их падение. Таким образом игрок может выбирать место падения фигуры. Если из упавших фигур соберётся горизонтальный ряд без пробелов (пустот), то этот ряд исчезнет, а всё что было над ним опустится. За каждый исчезнувший ряд игрок получает очки. Скорость падения фигур увеличивается с каждым новым уровнем игры. Уровень игры увеличивается через определённое количество появившихся фигур. Игра заканчивается если новая фигура не может появиться вверху игрового поля по причине того что ей мешают уже имеющиеся там фигуры.

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

Нам понадобится:

  • Arduino Uno х 1шт.
  • Trema OLED-дисплей 128×64 х 1шт.
  • Trema-модуль Кнопка х 4шт. (в ассортименте: синяя, красная, зелёная)
  • Trema Set Shield х 1шт.
    И никаких проводов (кроме USB для загрузки скетча).

Для реализации проекта нам необходимо установить библиотеку:

  • iarduino_OLED — графическая библиотека для работы с Trema OLED дисплеями.

О том как устанавливать библиотеки, Вы можете ознакомиться на странице Wiki — Установка библиотек в Arduino IDE.

Видео:

Схема подключения:

  • Перед подключением модулей, закрепите винтами нейлоновые стойки в отверстиях секций 1, 2, 3, 5 и 6 Trema Set Shield.
  • Установите Trema Set Shield на Arduino Uno .

Остальные модули устанавливаются на Trema Set Shield следующим образом: Trema Кнопки устанавливаются в центр нижних колодок секций 1, 2, 5 и 6, а Trema OLED-дисплей 128×64 устанавливается в верхнюю колодку 3 секции, как это показано на рисунках ниже.

  • Устанавливаем Trema-модуль Кнопку в 1 посадочную площадку.
  • Устанавливаем Trema-модуль Кнопку в 2 посадочную площадку.
  • Устанавливаем Trema OLED-дисплей 128×64 в 3 посадочную площадку, в верхнюю I2C колодоку.
  • Устанавливаем Trema-модуль Кнопку в 5 посадочную площадку.
  • Устанавливаем Trema-модуль Кнопку в 6 посадочную площадку.
  • Полученный результат представлен на рисунке ниже.

После чего закрепите модули вкрутив через их отверстия нейлоновые винты в установленные ранее нейлоновые стойки (нейлоновые стойки и винты входят в комплектацию Trema Set Shield) .

Наличие всего двух колодок в секциях Trema Set Shield, не позволит Вам неправильно установить модули, т.к. при неправильном подключении модули будут смещены относительно разметки своей секции и Вы не сможете закрепить их винтами.

Назначение кнопок:
  • L (Left) смещение фигуры влево.
  • R (Right) смешение фигуры вправо.
  • T (Turn) поворот фигуры на 90° по часовой стрелке.
  • D (Down) сброс фигуры вниз (ускорение падения фигуры).

Код программы:

Код может показаться немного громоздким. Для понимания кода рекомендуем сначала прочитать раздел «алгоритм работы» следующий сразу за кодом. А потом ознакомиться с комментариями в строках самого кода.

Алгоритм работы:

  • В начале скетча (до кода setup) выполняются следующие действия:
    • Подключаем графическую библиотеку iarduino_OLED для работы с Trema OLED дисплеем.
    • Объявляем объект myOLED указывая адрес дисплея на шине I2C, он должен совпадать с адресом установленным переключателем на обратной стороне платы OLED дисплея.
    • Объявляем константы pinBtnL, pinBtnR, pinBtnT, pinBtnD, pinSeed с указанием номеров выводов Arduino, которые будут задействованы в скетче.
    • Объявляем константы GAME_OFF, GAME_ON, GAME_OVER для удобочитаемости скетча.
    • Подключаем шрифты и картинки предустановленные в библиотеке myOLED.
    • Определяем константы с настраиваемыми значениями. Меняя эти значения можно менять размеры игрового стола, размеры фигур, скорость игры и время «залипания» кнопок.
    • Объявляем массивы и переменные участвующие в работе скетча.
    • Объявляем функции используемые в скетче.
  • В коде setup выполняются следующие действия:
    • Инициируем работу с Trema OLED дисплеем и запрещаем автоматический вывод данных.
    • Указываем кодировку текста в скетче (если требуется).
    • Конфигурируем выводы к которым подключены кнопки.
    • Готовим корректную работу функции random() для генерации псевдослучайных чисел.
    • Выводим анимированное приветствие (текст «Тетрис» с появляющимися фигурами).
    • Переводим состояние игры в GAME_OFF «Не играем» (ждём нажатие любой кнопки).
  • В коде loop сначала выполняется чтение состояний кнопок, после чего выполняется 1 из 3 частей:
    • «Не играем» — эта часть кода ожидает нажатия на любую кнопку. Если любая кнопка будет нажата, будут подготовлены переменные, прорисуется игровое поле и игра перейдёт в состояние GAME_ON «Играем».
    • «Играем» — эта часть кода является основной. Здесь в 18 строках кода реализован весь алгоритм игры. Он более подробно описан ниже.
    • «Игра завершена» — эта часть кода содержит анимацию закраски и очистки игрового стола, вывод текста «КОНЕЦ ИГРЫ», вывод анимированного приветствия и перевод игры в состояние GAME_OFF «Не играем».
  • Весть алгоритм игры полностью реализован в разделе «Играем» который состоит из 4 частей:
    (каждая часть этого раздела заключена в тело оператора if).
    • Первая часть сдвигает фигуру на игровом столе влево. Код в теле оператора if выполняется только при нажатии на кнопку Left. Единственная функция shiftFigure() в теле оператора if, выполняет сдвиг фигуры игрового стола на одну клетку. Параметр функции равный 1 указывает сдвинуть фигуру влево.
    • Вторая часть сдвигает фигуру на игровом столе вправо. Код в теле оператора if выполняется только при нажатии на кнопку Right. Единственная функция shiftFigure() в теле оператора if, выполняет сдвиг фигуры игрового стола на одну клетку. Параметр функции равный 2 указывает сдвинуть фигуру вправо.
    • Третья часть выполняет поворот фигуры на игровом столе. Код в теле оператора if выполняется только при нажатии на кнопку Turn. Единственная функция turnFigure() в теле оператора if, выполняет поворот фигуры на 90°. Первый параметр функции равный 1 указывает что повернуть требуется фигуру на игровом столе.
    • Четвёртая часть выполняет сдвиг фигуры игрового стола на одну клетку вниз. Код в теле оператора if выполняется как от нажатия на кнопку Down, так и по достижении времени tmrShift. Параметр функции равный 3 указывает сдвинуть фигуру вниз. Отпускание кнопки Down не заблокирует выполнение кода в теле оператора if при следующем проходе цикла loop (Если нажать и отпустить кнопку Down, то фигура будет сдвигаться вниз пока не достигнет дна или других фигур).
      Код в теле оператора If выполняет следующие действия:
      • Обновляем время tmrShift для следующего сдвига фигуры вниз на игровом столе.
      • Сдвигаем фигуру игрового стола на 1 клетку вниз. Проверяя не достигла ли фигура дна игрового стола или другой фигуры на игровом столе. Если не достигла, то на этом выполнение данного участка кода будет закончено.
      • Если фигура достигла дна игрового стола или другой фигуры (закончила падение), то выполняются следующие действия:
        • Проверяем наличие заполненных строк игрового стола, если они есть, то они удаляются, со сдвигом всего что находится выше и добавлением бала игроку.
        • Сбрасываем флаг нажатия на кнопку Down (на случай если фигура падала принудительно)
        • Обновляем текущий уровень игры в соответствии со значением счётчика созданных фигур.
        • Увеличиваем счётчик созданных фигур.
        • Выводим номер текущего уровня игры и количество набранных баллов.
        • Создаём будущую фигуру с выводом её изображения в поле справа от игрового стола, а ту фигуру которая ранее находилась в этом поле переносим на верх игрового стола (она станет новой фигурой в игре).
        • Если новую фигуру не удалось разместить на игровом столе (ей мешают другие фигуры), значит игра закончена, переводим состояние игры в GAME_OVER «Игра завершена».
      • Действия перечисленные выше выполняются вызовом функций:
        • shiftFigure(); — выполняет сдвиг фигуры на одну клетку игрового стола и возвращает false если сдвиг невозможен.
        • turnFigure(); — поворачивает фигуру игрового стола на 90°.
        • checkTable(); — проверяет наличие заполненных строк игрового стола, возвращая true или false.
        • deletTableRows(); — удаляет все заполненные строки с игрового стола, возвращая количество удалённых строк.
        • createNewFigure(); — создаёт будущую фигуру а предыдущую будущую фигуру делает новой на игровом столе, возвращая false если не удалось вставить фигуру на игровой стол.
Читать еще:  Самодельный пневматический домкрат

Все строки скетча (кода программы) прокомментированы, так что Вы можете подробнее ознакомиться с кодом прочитав комментарии строк.

Как написать свой Тетрис на Java за полчаса

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

Нам, как обычно, понадобятся:

  • 30 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например, Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с графикой (опционально). Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Спрайты, т.е. картинки плиток всех возможных состояний (пустая, и со степенями двойки до 2048). Можно нарисовать самому, или скачать использовавшиеся при написании статьи.

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

С чего начать?

Начать стоит с главного управляющего класса, который в нашем проекте находится выше остальных по уровню абстракции. Вообще отличный совет – в начале работы всегда пишите код вида if (getKeyPressed()) doSomething() , так вы быстро определите фронт работ.

Это наш main() . Он ничем принципиально не отличается от тех, что мы писали в предыдущих статьях – мы всё так же инициализируем поля и, пока игра не закончится, осуществляем по очереди: ввод пользовательских данных ( input() ), основные игровые действия ( logic() ) и вызов метода отрисовки у графического модуля ( graphicsModule.draw() ), в который передаём текущее игровое поле ( gameField ). Из нового разве что метод sync – метод, который должен будет гарантировать нам определённую частоту выполнения итераций. С его помощью мы сможем задать скорость падения фигуры в клетках-в-секунду.

Читать еще:  Магнитный держать для телефона

Вы могли заметить, что в коде использована константа FPS . Все константы удобно определять в классе с public static final полями. Полный список констант, который нам потребуется в ходе разработки, можно посмотреть в классе Constants на GitHub.

Оставим пока инициализацию полей на потом (мы же ещё не знаем, какие нам вообще понадобятся поля). Разберёмся сначала с input() и logic() .

Получение данных от пользователя

Код, честно говоря, достаточно капитанский:

Все данные от ввода мы просто сохраняем в соответствующие поля, действия на основе них будет выполнять метод logic() .

Orion Innovation (Ранее MERA), удалённо

Теперь уже потихоньку становится понятно, что нам необходимо. Во-первых, нам нужны клавиатурный и графический модули. Во-вторых, нужно как-то хранить направление, которое игрок выбрал для сдвига. Вторая задача решается просто – создадим enum с тремя состояниями: AWAITING, LEFT, RIGHT . Зачем нужен AWAITING ? Чтобы хранить информацию о том, что сдвиг не требуется (использования в программе null следует всеми силами избегать). Перейдём к интерфейсам.

Интерфейсы для клавиатурного и графического модулей

Так как многим не нравится, что я пишу эти модули на LWJGL, я решил в статье уделить время только интерфейсам этих классов. Каждый может написать их с помощью той GUI-библиотеки, которая ему нравится (или вообще сделать консольный вариант). Я же по старинке реализовал их на LWJGL, код можно посмотреть здесь в папках graphics/lwjglmodule и keyboard/lwjglmodule.

Интерфейсы же, после добавления в них всех упомянутых выше методов, будут выглядеть следующим образом:

Отлично, мы получили от пользователя данные. Что дальше?

А дальше мы должны эти данные обработать и что-то сделать с игровым полем. Если пользователь сказал сдвинуть фигуру куда-то, то передаём полю, что нужно сдвинуть фигуру в таком-то направлении. Если пользователь сказал, что нужно фигуру повернуть, поворачиваем, и так далее. Кроме этого нельзя забывать, что 1 раз в FRAMES_PER_MOVE (вы же открывали файл с константами?) итераций нам необходимо сдвигать падающую фигурку вниз.

Сюда же добавим проверку на переполнение поля (в Тетрисе игра завершается, когда фигурам некуда падать):

Так, а теперь мы напишем класс для того магического gameField, в который мы всё это передаём, да?

Не совсем. Сначала мы пропишем поля класса Main и метод initFields() , чтобы совсем с ним закончить. Вот все поля, которые мы использовали:

А инициализировать мы их будем так:

Если вы решили не использовать LWJGL и написали свои классы, реализующие GraphicsModule и KeyboardHandleModule , то здесь нужно указать их конструкторы вместо, соответственно new LwjglGraphicsModule() и new LwjglKeyboardHandleModule() .

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

Класс GameField

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

Начнём по порядку.

Хранить информацию о поле…

…и о падающей фигуре

TpReadableColor — простой enum, содержащий элементы с говорящими названиями (RED, ORANGE и т.п.) и метод, позволяющий получить случайным образом один из этих элементов. Ничего особенного в нём нет, код можно посмотреть тут.

Это все поля, которые нам понадобятся. Как известно, поля любят быть инициализированными.
Сделать это следует в конструкторе.

Конструктор и инициализация полей

«Что это за OFFSET_TOP ?» – спросите вы. OFFSET_TOP это количество неотображаемых ячеек сверху, в которых создаются падающие фигуры. Если фигуре не сможет «выпасть» из этого пространства, и хоть одна ячеек theField выше уровня COUNT_CELLS_Y будет заполнена, это будет обозначать, что поле переполнено и пользователь проиграл, поэтому OFFSET_TOP должен быть строго больше нуля.

Читать еще:  Изготовление игрушечного ксилофона из дерева

Далее в конструкторе стоит заполнить массив theField значениями константы EMPTINESS_COLOR , а countFilledCellsInLine – нулями (второе в Java не требуется, при инициализации массива все int‘ы равны 0). Или можно создать несколько слоёв уже заполненных ячейкам — на GitHub вы можете увидеть реализацию именно второго варианта.

А что это там за spawnNewFigure()? Почему инициализация фигуры вынесена в отдельный метод?

Вы правильно догадались, spawnNewFigure() действительно инициализирует поле figure . А в отдельный метод это вынесено, потому что нам придётся делать инициализацию каждый раз, когда будет создаваться новая фигура.

На этом с хранением данных мы закончили. Переходим к методам, которые отдают информацию о поле другим классам.

Методы, передающие информацию об игровом поле

Таких метода всего два. Первый возвращает цвет ячейки (для графического модуля):

А второй сообщает, переполнено ли поле (как это происходит, мы разобрали выше):

Методы, обновляющие фигуру и игровое поле

Начнём реализовывать методы, которые мы вызывали из Main.logic() .

Сдвиг фигуры

За это отвечает метод tryShiftFigure() . В комментариях к его вызову из Main было сказано, что он «пробует сдвинуть фигуру». Почему пробует? Потому что если фигура находится вплотную к стене, а пользователь пытается её сдвинуть в направлении этой стены, никакого сдвига в реальности происходить не должно. Так же нельзя сдвинуть фигуру в статические ячейки на поле.

Что мы сделали в этом методе? Мы запросили у фигуры ячейки, которые бы она заняла в случае сдвига. А затем для каждой из этих ячеек мы проверяем, не выходит ли она за пределы поля, и не находится ли по её координатам в сетке статичный блок. Если хоть одна ячейка фигуры выходит за пределы или пытается встать на место блока – сдвига не происходит. Coord здесь – класс-оболочка с двумя публичными числовыми полями (x и y координаты).

Поворот фигуры

Логика аналогична сдвигу:

Падение фигуры

Сначала код в точности повторяет предыдущие два метода:

Однако теперь, в случае, если фигура дальше падать не может, нам необходимо перенести её ячейки («кубики») в theField , т.е. в разряд статичных блоков, после чего создать новую фигуру:

Так как в результате переноса ячеек какая-то линия может заполниться полностью, после каждого добавления ячейки мы проверяем линию, в которую мы её добавили, на полноту:

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

Теперь GameField реализован почти полностью — за исключением геттера для фигуры. Её ведь графическому модулю тоже придётся отрисовывать:

Теперь нам нужно написать алгоритмы, по которым фигура определяет свои координаты в разных состояниях. Да и вообще весь класс фигуры.

Класс фигуры

Реализовать это всё я предлагаю следующим образом – хранить для фигуры (1) «мнимую» координату, такую, что все реальные блоки находятся ниже и правее неё, (2) состояние поворота (их всего 4, после 4-х поворотов фигура всегда возвращается в начальное положение) и (3) маску, которая по первым двум параметрам будет определять положение реальных блоков:

Rotation мод здесь будет выглядеть таким образом:

Соответственно, от самого класса Figure нам нужен только конструктор, инициализирующий поля:

И методы, которыми мы пользовались в GameField следующего вида:

Вдобавок, у фигуры должен быть цвет, чтобы графический модуль мог её отобразить. В тетрисе каждой фигуре соответствует свой цвет, поэтому цвет мы будем запрашивать у формы:

Форма фигуры и маски координат

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

Храним для каждой фигуры маску координат (которая определяет, насколько каждый реальный блок должен отстоять от «мнимой» координаты фигуры) и цвет:

Реализуем методы, которые использовали выше:

Ну а сами маски координат я предлагаю просто захардкодить следующим образом:

Т.е. для каждого объекта enum‘а мы передаём с помощью импровизированных (других в Java нет) делегатов метод, в котором в зависимости от переданного состояния поворота возвращаем разные реальные координаты блоков. В общем-то, можно обойтись и без делегатов, если хранить в каждом элементе отсупы для каждого из режимов поворота.

Ссылка на основную публикацию
Статьи c упоминанием слов:

Adblock
detector