RSS блога
Подписка
Цветная графика на Arduino. Пишем игру "Columns"
Возможности Arduino порой просто поражают. Когда-то у меня был компьютер «Микроша», купленный за 500 советских рублей (а это несколько тогдашних зарплат!) и при этом он не имел ни цвета, ни графики. Работал он с ч/б теликом по антенному входу. А сегодня ардуинка, ценой в пару бутылок пива, уделывает того-же «Микрошу» по всем статьям! А сколько продается периферии для нее в виде разных датчиков, дисплеев, джойстиков и т.д. — глаза разбегаются! А количество готовых библиотек в интернете для этой периферии? Похоже, есть все! А ведь тогда для «Микроши» пришлось писать и свой текстовый редактор и Ассемблер и библиотеки…
Но время идет вместе с прогрессом и теперь Arduino у меня воткнут даже в выключатель света. Я уж не говорю о дачном автополиве и других применениях этой невероятно удобной штуки. На страничках муськи я уже раньше рассказывал как научить Arduino разговаривать и играть в Тетрис на ч/б экранчике. А сегодня на очереди уже цветная графика, посмотрим, как ардуина справится с ней…
На днях, роясь в своем загашнике, нашел когда-то заказанный цветной TFT дисплейчик на 1.8" с разрешением 128x160 пикселей и SD-картоприемником. О возможностях сего девайса в связке с Arduino и будет сегодня разговор. А чтобы не было скучно от сухих цифр даташитов, напишем для этого дисплея занятную игру «Columns», что в переводе означает «Столбики». Автор идеи этой игры мне неизвестен, но она еще из тех стародавних времен, когда компьютеры были слабыми, а игры интересными. Эта игрушка динамическая, относительно ресурсоемкая, т.к. основана на цветной графике, поэтому ардуине придется сегодня выложиться по-полной. В этой игре у нас будет все, как положено — и показ следующего столбика и подсчет очков и запись рекордов в энергонезависимую память ардуины. А тем, кого игрушка заинтересовала прямо сейчас, могут скачать ее версию для Андроида в PlayMarket.
Вкратце пройдемся по железу, которое нам понадобится. Начнем с Arduino. Я собрал схему на Arduino Uno, просто потому, чтобы не использовать дополнительных макетных плат, но подойдет и Nano и Mini Pro на Atmega 328 (Atmega 168 со стандартным загрузчиком не пойдет из-за недостатка памяти). Для других моделей Arduino придется поменять аппаратные пины SPI на свои из даташитов.
Теперь о дисплее. Брал я его в прошлом году на ебее, где его и сегодня предлагает тот-же продавец за $4.23.
В этом дисплее установлен популярный контроллер ST7735, который работает по SPI интерфейсу, а, поскольку аппаратный SPI в Arduino есть, можно надеяться на приличную производительность видео. На другой стороне дисплея, кроме гребенки пинов, можно увидеть картоприемник для SD карт. Конечно, куда правильнее смотрелся бы картоприемник для микро SD, уж больно много места требует вставленная SD карта. Но, видимо, когда данный дисплей начали выпускать, микро SD не были еще так популярны. Работает картоприемник по тому-же SPI интерфейсу и наличие его на дисплее неслучайно. Дело в том, что для хранения картинок с 16 битовым цветом требуется очень много места, в частности для картинки 128x160 это около 40Кб, а EEPROM память у Atmega 328 всего 1 Кб, а у 168-й вообще 512 байт. Кстати, именно по причине больших объемов картинок, они не могут быть считаны одним разом в ОЗУ и посему загружаются с SD карты в память порциями, сильно замедляя процесс вывода изображения.
Из особенностей дисплея следует отметить, что контроллер ST7735R работает с напряжением 3.3V, включая логику, но в жизни у всех прекрасно работает и на 5V без дополнительных резисторов. У меня лично дисплей работал пару суток без выключения и не сгорел. Но если кто опасается, подключайте логику через резисторы 1-2 кОм. Отдельно подключается подсветка и тут даже продавец указывает — только 3.3V! Но я подключил ее к 5V через резистор 150 Ом и все работает замечательно.
Итак, дисплей может отображать 65536 цветов в палитре R5G6B5, т.е. 5 бит для красного цвета, 6 бит для зеленого и 5 бит для голубого. Для игры нам столько не нужно, достаточно будет восьми цветов.
Если вы уже ознакомились выше с правилами игры, то поняли, что игрок должен как-то управлять перемещениями столбиков влево/вправо и цветными квадратиками внутри столбиков. В прошлый раз, в игре Тетрис, управление было реализовано посредством ИК-приемника на ардуине и ИК пульта от бытовой техники. Здесь управление тоже можно было бы реализовать так-же, но это уже было, а потому не так интересно. А вот недавно я сделал реле времени для электросушилки, где в качестве элемента управления применил энкодер KY-040, который мне очень понравился по удобству работы.
И, поскольку еще один энкодер у меня остался, я подумал — а не попробовать ли мне его прикрутить к игрушке? Все, что нужно, на нем есть. Столбик влево/вправо — поворот вала, перемещать по кругу квадратики внутри столбика — короткое нажатие, а длинным нажатием можно быстро сбрасывать столбик вниз. Забегая вперед, скажу, что управление вышло даже в чем-то удобнее джойстика или кнопок, поскольку на энкодере я могу вращать вал с разной скоростью и, соответственно, с разной скоростью перемещая столбик. На кнопках или джое такое невозможно, там или долбить кнопку с бешеной скоростью или зажав кнопку, внимательно следить за быстрым перемещением столбика, чтобы он не проскочил нужную позицию. А тут я могу, крутанув вал, за мгновение переместить столбик от одной стенки до другой, а могу его двигать медленно, по одному шагу.
Вот такое получилось интересное решение, которое мне ранее нигде не встречалось.
Ну, вот, пожалуй железок нам и хватит. Я еще подключил пьезопищалку для озвучки проваливания одноцветных цепочек, но вполне можно обойтись и без нее. Переходим к сборке и оживлению железа.
Перед тем, как начать писать игру, мне стало интересно проверить вывод изображения с SD карты. Для этого я скачал библиотеку Adafruit-ST7735, присоединил дисплей к ардуине согласно нижеприведенной таблице:
Дисплей Arduino
GND = GND
VCC = +5В
RESET = 8
A0 = 9
SDA = 11
SCK = 13
CS = 10
SCK_SD = 13
MISO = 12
MOSI = 11
CS = 6
и открыл пример из этой библиотеки «spittbitmap». В заголовке примера поменял назначенные пины на свои
вот в этих строчках:
и приготовил в корне SD карты файл «333.bmp» размерами 128x160.
Изображение вывелось прекрасно, жаль только, что фото с Redmi 3S не может правильно передать цвета с TFT дисплея. На фото какая-то синюшность, хотя на деле все выглядит отлично и неотличимо от картинки выше.
На вывод картинки уходит примерно 2-3 сек — медленно конечно, в динамических играх пользоваться растровой графикой с SD карты вряд-ли получится. А вот другой пример, «graphicstest», показал, что графические примитивы из библиотеки «Adafruit_GFX.h» выводятся с довольно приличной скоростью, а это, то, что нужно для нашей игрушки.
Вкратце об используемых библиотеках. Как было упомянуто выше, для работы с контроллером в качестве драйвера была использована библиотека "Adafruit-ST7735", а в качестве библиотеки для работы с графикой "Adafruit_GFX.h". Поскольку в качестве элемента управления решено использовать энкодер KY-040, то для комфортной работы с ним, без дребезга, была подключена библиотека "Encod_er.h" и, необходимая к ней, "TimerOne.h". Данное решение отлично зарекомендовало себя ранее. Для работы с EEPROM я использовал библиотеку "EEPROM2.h".
Соединяем дисплей, энкодер и пищалку с ардуиной согласно описанию из шапки программы и можно заливать скетч игры. Если у вас ардуина другой модели, то пины аппаратного SPI нужно заменить на свои. И, поскольку озвучка использует также аппаратную реализацию ШИМ, то и пин для нее также должен иметь такую поддержку.
Об особенностях игры сейчас расскажу.
При подаче питания на ардуину, в игре включается Демо-режим, в котором наглядно показывается как уничтожаются одноцветные цепочки и начисляются очки. Стакан наполняется случайным образом сгенерированными квадратиками и затем игра вычисляет одноцветные цепочки и уничтожает их. После провала квадратиков на первом цикле возникает новая картина, в которой также могут образоваться такие цепочки и все повторяется до тех пор, пока ни одной цепочки длиннее 3-х на экране не останется. Прикольно, но хочется смотреть и смотреть на этот процесс. Помимо того, что само созерцание этого зрелища завораживает, невольно пытаешься первым обнаружить эти цепочки, еще до их уничтожения.
Теперь о начислении очков. Если игроку удалось уничтожить цепочку длиной до 7 квадратиков, то за каждый квадратик ему начисляется 1 очко. Если удалось построить цепочку длиннее, то очки удваиваются. А вот если после первого цикла уничтожения сформировались новые цепочки, то к полученным очкам второго цикла прибавятся очки первого цикла. Это сделано для того, чтобы игрок стремился не сразу проваливать короткие цепочки, а строил комбинации, чтобы потом единственным перемещением столбика уничтожать длинные цепочки и организовывать провалы в несколько циклов. Задачка, я вам скажу, нетривиальная, но очень интересная. Таким образом, в этой игре будет побеждать тот, кто сможет быстрее в уме строить многоходовые комбинации. Впрочем, изменить систему начисления очков можно в функции MyScore().
Чтобы выйти из режима Демо достаточно нажать на вал энкодера или повернуть его. В этом случае автоматически начнется игра.
Как я упоминал выше, игра, несмотря на кажущуюся простоту, довольно непроста, а поскольку играть в нее могут разные категории людей, в том числе и дети, то, возможно, кому-то захочется упростить ее. Поскольку исходный код я выложил, то ниже, под спойлером, покажу какие переменные отвечают за за различные аспекты игры, чтобы можно было подогнать параметры под свои хотелки. Единственное требование к соблюдению © — при публикации где-либо своих вариантов кода указывать ссылку на первоисточник муську.
В конце обзора я, как обычно, представляю видео процесса игры, на котором можно оценить скорость отрисовки графики и плавность перемещения столбиков под управлением энкодера.
Для чего делалось? Просто интересно осваивать новое, а осваивать всегда интереснее, когда в результате получается что-то полезное. Ну, или, по крайней мере, — прикольное, но работающее! У следующего исследуемого дисплея будет уже вдвое большее разрешение и, главное, — тачскрин, осталось дождаться посылки.
Всем хорошего настроения и новых рекордов!
P.S. Если у кого возникнут вопросы по описанным тут железкам и программе, постараюсь ответить.
Но время идет вместе с прогрессом и теперь Arduino у меня воткнут даже в выключатель света. Я уж не говорю о дачном автополиве и других применениях этой невероятно удобной штуки. На страничках муськи я уже раньше рассказывал как научить Arduino разговаривать и играть в Тетрис на ч/б экранчике. А сегодня на очереди уже цветная графика, посмотрим, как ардуина справится с ней…
На днях, роясь в своем загашнике, нашел когда-то заказанный цветной TFT дисплейчик на 1.8" с разрешением 128x160 пикселей и SD-картоприемником. О возможностях сего девайса в связке с Arduino и будет сегодня разговор. А чтобы не было скучно от сухих цифр даташитов, напишем для этого дисплея занятную игру «Columns», что в переводе означает «Столбики». Автор идеи этой игры мне неизвестен, но она еще из тех стародавних времен, когда компьютеры были слабыми, а игры интересными. Эта игрушка динамическая, относительно ресурсоемкая, т.к. основана на цветной графике, поэтому ардуине придется сегодня выложиться по-полной. В этой игре у нас будет все, как положено — и показ следующего столбика и подсчет очков и запись рекордов в энергонезависимую память ардуины. А тем, кого игрушка заинтересовала прямо сейчас, могут скачать ее версию для Андроида в PlayMarket.
Правила игры Columns
В «Стакан» падают столбики, состоящие из 3-х цветных квадратиков. Игрок может перемещать столбик вправо и влево, а также перемещать по кругу квадратики внутри столбика. Если игроку удается выстроить цепочку из квадратиков одного цвета длиной от 4-х и более, то вся эта цепочка уничтожается, а вышестоящие квадратики проваливаются на освободившееся место. В цепочку собираются квадратики, имеющие соседей одного цвета по горизонтали и вертикали. За уничтоженные цепочки игроку начисляются очки. Задача игрока — набрать как можно больше очков.
Распиновка SPI на популярные модели Arduino
Uno: MOSI соответствует вывод 11 или ICSP-4, MISO – 12 или ICSP-1, SCK – 13 или ICSP-3, SS (slave) – 10.
Mega1280 или Mega2560: MOSI — 51 или ICSP-4, MISO – 50 или ICSP-1, SCK – 52 или ICSP-3, SS (slave) – 53.
Leonardo: MOSI — ICSP-4, MISO –ICSP-1, SCK –ICSP-3.
Due: MOSI — ICSP-4, MISO –ICSP-1, SCK –ICSP-3, SS (master) – 4, 10, 52.
Mega1280 или Mega2560: MOSI — 51 или ICSP-4, MISO – 50 или ICSP-1, SCK – 52 или ICSP-3, SS (slave) – 53.
Leonardo: MOSI — ICSP-4, MISO –ICSP-1, SCK –ICSP-3.
Due: MOSI — ICSP-4, MISO –ICSP-1, SCK –ICSP-3, SS (master) – 4, 10, 52.
В этом дисплее установлен популярный контроллер ST7735, который работает по SPI интерфейсу, а, поскольку аппаратный SPI в Arduino есть, можно надеяться на приличную производительность видео. На другой стороне дисплея, кроме гребенки пинов, можно увидеть картоприемник для SD карт. Конечно, куда правильнее смотрелся бы картоприемник для микро SD, уж больно много места требует вставленная SD карта. Но, видимо, когда данный дисплей начали выпускать, микро SD не были еще так популярны. Работает картоприемник по тому-же SPI интерфейсу и наличие его на дисплее неслучайно. Дело в том, что для хранения картинок с 16 битовым цветом требуется очень много места, в частности для картинки 128x160 это около 40Кб, а EEPROM память у Atmega 328 всего 1 Кб, а у 168-й вообще 512 байт. Кстати, именно по причине больших объемов картинок, они не могут быть считаны одним разом в ОЗУ и посему загружаются с SD карты в память порциями, сильно замедляя процесс вывода изображения.
Из особенностей дисплея следует отметить, что контроллер ST7735R работает с напряжением 3.3V, включая логику, но в жизни у всех прекрасно работает и на 5V без дополнительных резисторов. У меня лично дисплей работал пару суток без выключения и не сгорел. Но если кто опасается, подключайте логику через резисторы 1-2 кОм. Отдельно подключается подсветка и тут даже продавец указывает — только 3.3V! Но я подключил ее к 5V через резистор 150 Ом и все работает замечательно.
Итак, дисплей может отображать 65536 цветов в палитре R5G6B5, т.е. 5 бит для красного цвета, 6 бит для зеленого и 5 бит для голубого. Для игры нам столько не нужно, достаточно будет восьми цветов.
Если вы уже ознакомились выше с правилами игры, то поняли, что игрок должен как-то управлять перемещениями столбиков влево/вправо и цветными квадратиками внутри столбиков. В прошлый раз, в игре Тетрис, управление было реализовано посредством ИК-приемника на ардуине и ИК пульта от бытовой техники. Здесь управление тоже можно было бы реализовать так-же, но это уже было, а потому не так интересно. А вот недавно я сделал реле времени для электросушилки, где в качестве элемента управления применил энкодер KY-040, который мне очень понравился по удобству работы.
И, поскольку еще один энкодер у меня остался, я подумал — а не попробовать ли мне его прикрутить к игрушке? Все, что нужно, на нем есть. Столбик влево/вправо — поворот вала, перемещать по кругу квадратики внутри столбика — короткое нажатие, а длинным нажатием можно быстро сбрасывать столбик вниз. Забегая вперед, скажу, что управление вышло даже в чем-то удобнее джойстика или кнопок, поскольку на энкодере я могу вращать вал с разной скоростью и, соответственно, с разной скоростью перемещая столбик. На кнопках или джое такое невозможно, там или долбить кнопку с бешеной скоростью или зажав кнопку, внимательно следить за быстрым перемещением столбика, чтобы он не проскочил нужную позицию. А тут я могу, крутанув вал, за мгновение переместить столбик от одной стенки до другой, а могу его двигать медленно, по одному шагу.
Вот такое получилось интересное решение, которое мне ранее нигде не встречалось.
Ну, вот, пожалуй железок нам и хватит. Я еще подключил пьезопищалку для озвучки проваливания одноцветных цепочек, но вполне можно обойтись и без нее. Переходим к сборке и оживлению железа.
Перед тем, как начать писать игру, мне стало интересно проверить вывод изображения с SD карты. Для этого я скачал библиотеку Adafruit-ST7735, присоединил дисплей к ардуине согласно нижеприведенной таблице:
Дисплей Arduino
GND = GND
VCC = +5В
RESET = 8
A0 = 9
SDA = 11
SCK = 13
CS = 10
SCK_SD = 13
MISO = 12
MOSI = 11
CS = 6
и открыл пример из этой библиотеки «spittbitmap». В заголовке примера поменял назначенные пины на свои
вот в этих строчках:
#define TFT_CS 10 // Chip select line for TFT display
#define TFT_RST 8 // Reset line for TFT (or see below...)
#define TFT_DC 9 // Data/command line for TFT
#define SD_CS 6 // Chip select line for SD card
...
bmpDraw("333.bmp", 0, 0);
и приготовил в корне SD карты файл «333.bmp» размерами 128x160.
Изображение вывелось прекрасно, жаль только, что фото с Redmi 3S не может правильно передать цвета с TFT дисплея. На фото какая-то синюшность, хотя на деле все выглядит отлично и неотличимо от картинки выше.
На вывод картинки уходит примерно 2-3 сек — медленно конечно, в динамических играх пользоваться растровой графикой с SD карты вряд-ли получится. А вот другой пример, «graphicstest», показал, что графические примитивы из библиотеки «Adafruit_GFX.h» выводятся с довольно приличной скоростью, а это, то, что нужно для нашей игрушки.
Вкратце об используемых библиотеках. Как было упомянуто выше, для работы с контроллером в качестве драйвера была использована библиотека "Adafruit-ST7735", а в качестве библиотеки для работы с графикой "Adafruit_GFX.h". Поскольку в качестве элемента управления решено использовать энкодер KY-040, то для комфортной работы с ним, без дребезга, была подключена библиотека "Encod_er.h" и, необходимая к ней, "TimerOne.h". Данное решение отлично зарекомендовало себя ранее. Для работы с EEPROM я использовал библиотеку "EEPROM2.h".
Соединяем дисплей, энкодер и пищалку с ардуиной согласно описанию из шапки программы и можно заливать скетч игры. Если у вас ардуина другой модели, то пины аппаратного SPI нужно заменить на свои. И, поскольку озвучка использует также аппаратную реализацию ШИМ, то и пин для нее также должен иметь такую поддержку.
Скетч
// © Klop 2018
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <TimerOne.h>
#include <Encod_er.h>
#include <EEPROM2.h>
// пин LED+ дисплея подключаем к +5V через резистор 150 ом
#define TFT_CS 10 // пин 10 подключаем к CS дисплея
#define TFT_RST 8 // пин 8 подключаем к RST дисплея
#define TFT_DC 9 // пин 9 подключаем к DS дисплея
#define pin_DT 4 // пин 4 подключаем к DT энкодера
#define pin_CLK 2 // пин 2 подключаем к CLK энкодера
#define pin_SW 3 // пин 3 подключаем к SW энкодера
#define pin_Speaker 5 // пин 5 подключаем к + пищалки ( - пищалки на землю)
#define NotPush 0
#define ShortPush 1
#define LongPush 2
#define DurationOfLongPush 350 // длительность длинного нажатия
// Color definitions
#define BLACK 0x0000
#define RED 0x001F
#define BLUE 0xF800
#define GREEN 0x07E0
#define YELLOW 0x07FF
#define MAGENTA 0xF81F
#define CYAN 0xFFE0
#define WHITE 0xFFFF
#define MaxX 8
#define MaxY 17
#define SmeX 3
#define SmeY 5
#define LL 97
#define interval 250
#define razmer 12 // размер квадратика
#define NumCol 6 // количество цветов квадратиков
#define MaxLevel 8 // макс. кол-во уровней
#define NextLevel 80 // через сколько столбиков повышать уровень
byte MasSt[MaxX][MaxY], MasTmp[MaxX][MaxY], MasOld[MaxX][MaxY], fignext[3];
byte Level=1, dx, dy, OldLevel, tr, flfirst=1; // flfirst=? сменить для обнуления рекорда
uint16_t MasCol[]={WHITE, BLACK, RED, BLUE, GREEN, YELLOW, MAGENTA, CYAN};
unsigned long TimeOfPush, Counter,Score=0, TScore=0, Record=0, OldRecord, OldScore, myrecord;
word tempspeed;
bool fl, Demo=true, myfl=false, Arbeiten=false, FlNew, FlZ=false;
int8_t VAL, Mp, x,y;
int8_t mmm [4][2]={{-1,0},{0,-1},{1,0},{0,1}};
word MasSpeed[MaxLevel]={500,450,400,350,300,250,200,100}; // задержки уровней
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);
Encod_er encoder(pin_CLK, pin_DT, pin_SW);
//==================================================
void setup()
{
randomSeed(analogRead(0));
tft.initR(INITR_BLACKTAB); // initialize a ST7735S chip, black tab
tft.fillScreen(WHITE);
tft.setCursor(LL, 61);
tft.setTextColor(BLACK);
tft.setTextWrap(true);
tft.setTextSize(1);
tft.print("LEVEL");
tft.setCursor(100, SmeY);
tft.print("NEXT");
tft.setCursor(LL,100);
tft.print("SCORE");
tft.setCursor(LL+5,135);
tft.print("TOP");
FlNew=true;
ViewStacan();
GetNext();
Timer1.initialize(interval); // инициализация таймера 1, период interval мкс
Timer1.attachInterrupt(timerInterrupt, interval); // задаем обработчик прерываний
pinMode (pin_DT,INPUT);
pinMode (pin_CLK,INPUT);
pinMode (pin_SW,INPUT);
digitalWrite(pin_SW, HIGH); // подтягиваем к 5V
delay(100);
encoder.timeLeft= 0;
encoder.timeRight= 0;
tft.drawFastVLine(1,2,157,BLACK);
tft.drawFastVLine(razmer*MaxX+5-MaxX,2,157,BLACK);
tft.drawFastHLine(1,158,razmer*MaxX+5-MaxX,BLACK);
EEPROM_read(0, tr);
if (tr==flfirst) EEPROM_read(1, myrecord);
else { myrecord=0;
EEPROM_write(1, myrecord);
EEPROM_write(0, flfirst);
}
}
//==================================================
// обработчик прерывания
void timerInterrupt()
{ encoder.scanState();
}
//==================================================
byte mypush() // возвращает длинное-2, короткое-1 или осутствие нажатия-0
{ unsigned long tpr=millis();
byte res=NotPush;
if (!digitalRead(pin_SW))
{ if (TimeOfPush==0) TimeOfPush=tpr; else
if (tpr-TimeOfPush>DurationOfLongPush && !myfl)
{ TimeOfPush=0;
myfl=true;
return(LongPush);
}
} else
{ if (TimeOfPush>0 && !myfl) res=ShortPush;
TimeOfPush=0;
myfl=false;
}
return(res);
}
//==================================================
void ViewQuad(byte i,byte j,byte mycolor) // отрисовка 1-го квадрата
{if (j<3) return;
byte wy=SmeY+(j-3)*razmer-j;
byte wx=SmeX+i*razmer-i;
if (mycolor!=0)
{ tft.drawRect(wx,wy,razmer, razmer, BLACK);
tft.fillRect(wx+1, wy+1, razmer-2 , razmer-2, MasCol[mycolor]);
} else
tft.fillRect(wx+1, wy-1, razmer-2 , razmer, WHITE);
}
//==================================================
void ViewStacan()
{ char myStr2[5];
tft.setTextColor(RED);
tft.setTextSize(1);
if (OldScore!=Score || FlNew) { tft.fillRect(LL,113,30,8,WHITE);
tft.setCursor(LL, 113);
sprintf(myStr2,"%05d",Score );
tft.print(myStr2);
OldScore=Score;
}
if (OldRecord!=Record || FlNew) { tft.setCursor(LL, 148);
sprintf(myStr2,"%05d",Record );
tft.fillRect(LL,148,30,8,WHITE);
tft.print(myStr2);
OldRecord=Record;
}
if (OldLevel!=Level || FlNew) { tft.fillRect(107,73,30,20,WHITE);
tft.setCursor(107, 73);
tft.setTextSize(2);
tft.print(Level);
OldLevel=Level;
}
FlNew=false;
for (byte j=3;j<MaxY;j++)
for (byte i=0;i<MaxX;i++)
if (MasSt[i][j]!=MasOld[i][j]) ViewQuad(i,j,MasSt[i][j]);
tft.drawFastHLine(3,156,razmer*MaxX-MaxX,BLACK);
for (byte j=3;j<MaxY;j++)
for (byte i=0;i<MaxX;i++)
MasOld[i][j]=MasSt[i][j];
}
//==================================================
void ClearMas(byte MasStx[MaxX][MaxY])
{
for (byte j=0;j<MaxY;j++)
for (byte i=0;i<MaxX;i++)
(MasStx[i][j]=0);
}
//==================================================
void Sosed(int i,int j,int dx,int dy, byte mode)
{ int nx=i+dx;
int ny=j+dy;
if (nx>=0 && ny>=0 && nx<MaxX && ny<MaxY && MasSt[nx][ny]==MasSt[i][j])
{ if (mode==1) MasTmp[i][j]++; else
if (mode==2 && (MasTmp[nx][ny]>1 || MasTmp[i][j]>2 ))
{ MasTmp[nx][ny]=3;
MasTmp[i][j]=3;
} else
if (mode==3 && MasTmp[nx][ny]==3) if (MasTmp[i][j]!=3) { MasTmp[i][j]=3;
fl=true;
}
}
}
//==================================================
void Sos(int i,int j, byte mode)
{ for (byte k=0;k<4;k++)
Sosed(i,j,mmm[k][0],mmm[k][1],mode);
}
//==================================================
bool FindFull() // ищем одноцветные цепочки
{ byte i,j,k; bool res;
res=false;
for (byte k=2;k<8;k++) // по каждому цвету
{ ClearMas(MasTmp);
for (j=3;j<MaxY;j++)
for (i=0;i<MaxX;i++)
if (MasSt[i][j]==k) Sos(i,j,1);
for (j=3;j<MaxY;j++)
for (i=0;i<MaxX;i++)
if (MasTmp[i][j]>1) Sos(i,j,2);
do
{ fl=false;
for (j=3;j<MaxY;j++)
for (i=0;i<MaxX;i++)
if (MasTmp[i][j]>0) Sos(i,j,3);
} while (fl);
for (j=3;j<MaxY;j++)
for (i=0;i<MaxX;i++)
if (MasTmp[i][j]==3) { MasSt[i][j]=1;
TScore++;
}
}
if (TScore>0)
{ ViewStacan();
FlZ=true;
mydelay(500);
}
for (j=0;j<MaxY;j++)
for (i=0;i<MaxX;i++)
{ while (MasSt[i][MaxY-1-j]==1)
{ for (k=0;k<MaxY-2-j;k++) MasSt[i][MaxY-1-k-j]= MasSt[i][MaxY-2-k-j];
res=true;
}
}
return(res);
}
//==================================================
void GetNext()
{ byte dx=106; byte dy=16;
x=3; y=0;
for (byte i=0;i<3;i++)
{ //fig[i]=fignext[i];
if (!Demo) MasSt[x][i]=fignext[i];
fignext[i]=random(NumCol)+2;
tft.drawRect(dx,dy+razmer*i-i,razmer, razmer, BLACK);
tft.fillRect(dx+1,dy+razmer*i-i+1, razmer-2, razmer-2,MasCol[fignext[i]]);
}
if (!Demo){ Counter++;
if (Counter==NextLevel) { Counter=0;
Level++;
if (Level>MaxLevel) Level=MaxLevel;
}
tempspeed=MasSpeed[Level-1];
}
}
//==================================================
void MyScore()
{
TScore=0;
while(FindFull())
{if (TScore>7) Score=Score+TScore+(TScore-8)*2;
else Score=Score+TScore;
if (!Demo) analogWrite(pin_Speaker,5); // издаем звук
ViewStacan();
analogWrite(pin_Speaker,0);
FlZ=true;
mydelay(1000);
}
FlZ=false;
}
//==================================================
void ProcDemo()
{ Score=0;
GetNext();
for (byte j=3;j<MaxY;j++)
for (byte i=0;i<MaxX;i++)
MasSt[i][j]=random(6)+2;
ViewStacan();
mydelay(1000);
if (!Demo) return;
MyScore();
if (Record<Score) Record=Score;
}
//================================================
void mydelay(int md)
{
unsigned long starttime=millis();
while (millis()-starttime<md)
{
VAL=0;
Mp=mypush();
if (encoder.timeRight!=0) // обрабатываем повороты энкодера
{ VAL=1;
encoder.timeRight=0;
} else
if (encoder.timeLeft!=0)
{ VAL=-1;
encoder.timeLeft=0;
}
if (!digitalRead(pin_SW)) VAL=0;
if ((VAL!=0 || Mp!=NotPush) && Demo) { Demo=false;
NewGame();
}
if (VAL!=0 && figmove(VAL, 0) && !FlZ)
{ for (byte i=0;i<3;i++)
{ MasSt[x+VAL][y+i]=MasSt[x][y+i];
MasSt[x][y+i]=0;
}
ViewStacan();
if (MasSt[x][y+3]==0)
tft.drawFastHLine(SmeX+x*razmer-x+1,SmeY+y*razmer-y-3,razmer-2,WHITE);
x=x+VAL;
}
if (Mp==ShortPush && !FlZ) // перемена цветов фигуры
{byte aa=MasSt[x][y];
MasSt[x][y]=MasSt[x][y+2];
MasSt[x][y+2]=MasSt[x][y+1];
MasSt[x][y+1]=aa;
ViewStacan();
}
if (Mp==LongPush && !FlZ) tempspeed=50; // падение
}
}
//================================================
void NewGame()
{
for (byte i=0;i<8;i++)
tft.drawFastVLine(3+razmer*i-i,2,155,BLACK); // направляющие
for (byte j=3;j<MaxY;j++) // заполним буфер сравнения значением, которого нет
for (byte i=0;i<MaxX;i++)
MasOld[i][j]=255;
Score=0;
FlNew=true;
OldScore=Score;
ClearMas(MasSt);
Arbeiten=true;
GetNext();
Counter=0;
Level=1;
tempspeed=MasSpeed[0];
Record=myrecord;
ViewStacan();
if (tr==flfirst) EEPROM_read(1, myrecord);
else { myrecord=0;
EEPROM_write(1, myrecord);
EEPROM_write(0, flfirst);
}
}
//================================================
void gameover()
{
Arbeiten=false;
tft.drawRect(7,40,81,50, BLACK);
tft.fillRect(8,41,79,48,WHITE);
tft.setCursor(25, 47);
tft.setTextSize(2);
tft.setTextColor(RED);
tft.print("GAME");
tft.setCursor(25, 67);
tft.print("OVER");
if (Score>myrecord)
{ myrecord=Score;
EEPROM_write(1, myrecord);
}
}
//================================================
bool figmove(int dx, int dy)
{ bool fff=false;
if (x+dx<0 || x+dx>MaxX-1) return(false); // попытка выйти за стенки стакана
if (dx!=0) if (MasSt[x+dx][y+dy+2]==0) return(true); else return(false);
if (dy>0) // падение вниз
{ if (y+dy+2>MaxY-1 || MasSt[x+dx][y+dy+2]>0) // либо на дне, либо на квадрате
{if (y<3) gameover();
else fff=true;
} else
{ for (byte i=0;i<3;i++) MasSt[x][y+2-i+dy]=MasSt[x][y+2-i];
MasSt[x][y]=0;
y=y+dy;
}
if (fff) { MyScore();
GetNext();
}
ViewStacan();
}
return(true);
}
//================================================
void loop()
{
if (Demo) ProcDemo();
else
{ if (Arbeiten)
{ mydelay(tempspeed);
figmove(0,1);
} else
if (mypush()==ShortPush) NewGame();
}
}
При подаче питания на ардуину, в игре включается Демо-режим, в котором наглядно показывается как уничтожаются одноцветные цепочки и начисляются очки. Стакан наполняется случайным образом сгенерированными квадратиками и затем игра вычисляет одноцветные цепочки и уничтожает их. После провала квадратиков на первом цикле возникает новая картина, в которой также могут образоваться такие цепочки и все повторяется до тех пор, пока ни одной цепочки длиннее 3-х на экране не останется. Прикольно, но хочется смотреть и смотреть на этот процесс. Помимо того, что само созерцание этого зрелища завораживает, невольно пытаешься первым обнаружить эти цепочки, еще до их уничтожения.
Теперь о начислении очков. Если игроку удалось уничтожить цепочку длиной до 7 квадратиков, то за каждый квадратик ему начисляется 1 очко. Если удалось построить цепочку длиннее, то очки удваиваются. А вот если после первого цикла уничтожения сформировались новые цепочки, то к полученным очкам второго цикла прибавятся очки первого цикла. Это сделано для того, чтобы игрок стремился не сразу проваливать короткие цепочки, а строил комбинации, чтобы потом единственным перемещением столбика уничтожать длинные цепочки и организовывать провалы в несколько циклов. Задачка, я вам скажу, нетривиальная, но очень интересная. Таким образом, в этой игре будет побеждать тот, кто сможет быстрее в уме строить многоходовые комбинации. Впрочем, изменить систему начисления очков можно в функции MyScore().
Чтобы выйти из режима Демо достаточно нажать на вал энкодера или повернуть его. В этом случае автоматически начнется игра.
Как я упоминал выше, игра, несмотря на кажущуюся простоту, довольно непроста, а поскольку играть в нее могут разные категории людей, в том числе и дети, то, возможно, кому-то захочется упростить ее. Поскольку исходный код я выложил, то ниже, под спойлером, покажу какие переменные отвечают за за различные аспекты игры, чтобы можно было подогнать параметры под свои хотелки. Единственное требование к соблюдению © — при публикации где-либо своих вариантов кода указывать ссылку на первоисточник муську.
Переменные для настройки
#define NumCol 6 // количество цветов квадратиков. Можно изменять это число от 1 до 6. Уменьшая количество цветов, можно существенно снизить сложность игры.
#define MaxLevel 8 // макс. кол-во уровней. Можно изменять от 1 до 8. Каждому уровню соответствует своя скорость падения столбиков. К этой строке напрямую относится массив:
word MasSpeed[MaxLevel]={500,450,400,350,300,250,200,100}; // задержки уровней Здесь можно задать задержку падения для каждого уровня в миллисекундах. По желанию, шкалу можно сделать хоть линейной, хоть логарифмической.
#define NextLevel 80 // через сколько столбиков повышать уровень. Уровень повышается, когда игроку выпало столько столбиков от предыдущего уровня. Изменяя это число, мы можем варьировать порог его повышения.
flfirst=1 Изменение значения этой переменной на любое другое число от 0 до 255 обнулит рекорд, записанный в EEPROM.
#define DurationOfLongPush 350 // длительность длинного нажатия. Длительность длинного нажатия в миллисекундах. Длинное нажатие срабатывает при достижении этого значения и только один раз до отпускания кнопки. Короткое нажатие срабатывает только при отпускании кнопки, если время нажатия не превысило времени длинного нажатия.
#define MaxLevel 8 // макс. кол-во уровней. Можно изменять от 1 до 8. Каждому уровню соответствует своя скорость падения столбиков. К этой строке напрямую относится массив:
word MasSpeed[MaxLevel]={500,450,400,350,300,250,200,100}; // задержки уровней Здесь можно задать задержку падения для каждого уровня в миллисекундах. По желанию, шкалу можно сделать хоть линейной, хоть логарифмической.
#define NextLevel 80 // через сколько столбиков повышать уровень. Уровень повышается, когда игроку выпало столько столбиков от предыдущего уровня. Изменяя это число, мы можем варьировать порог его повышения.
flfirst=1 Изменение значения этой переменной на любое другое число от 0 до 255 обнулит рекорд, записанный в EEPROM.
#define DurationOfLongPush 350 // длительность длинного нажатия. Длительность длинного нажатия в миллисекундах. Длинное нажатие срабатывает при достижении этого значения и только один раз до отпускания кнопки. Короткое нажатие срабатывает только при отпускании кнопки, если время нажатия не превысило времени длинного нажатия.
Для чего делалось? Просто интересно осваивать новое, а осваивать всегда интереснее, когда в результате получается что-то полезное. Ну, или, по крайней мере, — прикольное, но работающее! У следующего исследуемого дисплея будет уже вдвое большее разрешение и, главное, — тачскрин, осталось дождаться посылки.
Всем хорошего настроения и новых рекордов!
P.S. Если у кого возникнут вопросы по описанным тут железкам и программе, постараюсь ответить.
Самые обсуждаемые обзоры
+77 |
3989
147
|
+58 |
4162
73
|
Ну то что цветные классно, но то что не читаются… ну углы обзора ни о чем. И контрастности. Вот помогал тут товарищу… так после того как вспомнил по Oled… Вышло так что по читаемости 1.3'' Oled оказался в разы бодрее 2.4'' TFT. Есть еще кста 2.8'' экранчики.
как это сделал один чел с esp8266
Цветные для меги это, на мой взгляд, перебор.
Цветные дисплеи на али продаются еще и с 8бит и 16бит шиной. Можно взять 8бит и STM32F103. А кому нужна максимальная плавность берут 16 бит и STM32 с FSMC.
Еще, кстати, на скорость влияет адафрутовская библиотека — если заливки, вертикальные и горизонтальные линии там оптимизированы, то шрифты рисуются попиксельно с установкой координат каждый раз (а это 12 байт и 4 смены A0). Это дает возможность рисовать текст поверх фона, но если нужно весь экран заполнить текстом, то блочное рисование раза в 3 быстрее.
на профильных форумах совсем ничего не нашлось?
Новые ревизии кстати постоянно выходят, снижают потребление в основном.
Недавно сравнительно вышел ATtiny44, на борту множество инструментов для снижения потребления…
p.s.
ZX у меня не было, но глянул в Вики. Проц Z80 работал на частоте 3,5 МГц. Супротив 16 у Atmega 328? Так, что… А сравнивать тогдашние цены на Spectrum и нынешние на ардуину будем? )
Персона́льный компью́тер, ПК (англ. personal computer, PC), ПЭВМ (персональная электронно-вычислительная машина) — настольная микро-ЭВМ, имеющая эксплуатационные характеристики бытового прибора и универсальные функциональные возможности.
Микроконтро́ллер(англ. Micro Controller Unit, MCU) — микросхема, предназначенная для управления электронными устройствами
Но в жизни грани между ними уже весьма эфемеры. Так, что я с Вами соглашусь.
Просто надоело уже перебирать эти дисплеи (( сколько не брал ни на одном не работает либо без ШИМ либо нет вообще хотя заявлен.