Цветная графика на Arduino. Пишем игру "Columns"

Возможности Arduino порой просто поражают. Когда-то у меня был компьютер «Микроша», купленный за 500 советских рублей (а это несколько тогдашних зарплат!) и при этом он не имел ни цвета, ни графики. Работал он с ч/б теликом по антенному входу. А сегодня ардуинка, ценой в пару бутылок пива, уделывает того-же «Микрошу» по всем статьям! А сколько продается периферии для нее в виде разных датчиков, дисплеев, джойстиков и т.д. — глаза разбегаются! А количество готовых библиотек в интернете для этой периферии? Похоже, есть все! А ведь тогда для «Микроши» пришлось писать и свой текстовый редактор и Ассемблер и библиотеки…

Но время идет вместе с прогрессом и теперь Arduino у меня воткнут даже в выключатель света. Я уж не говорю о дачном автополиве и других применениях этой невероятно удобной штуки. На страничках муськи я уже раньше рассказывал как научить Arduino разговаривать и играть в Тетрис на ч/б экранчике. А сегодня на очереди уже цветная графика, посмотрим, как ардуина справится с ней…

На днях, роясь в своем загашнике, нашел когда-то заказанный цветной TFT дисплейчик на 1.8" с разрешением 128x160 пикселей и SD-картоприемником. О возможностях сего девайса в связке с Arduino и будет сегодня разговор. А чтобы не было скучно от сухих цифр даташитов, напишем для этого дисплея занятную игру «Columns», что в переводе означает «Столбики». Автор идеи этой игры мне неизвестен, но она еще из тех стародавних времен, когда компьютеры были слабыми, а игры интересными. Эта игрушка динамическая, относительно ресурсоемкая, т.к. основана на цветной графике, поэтому ардуине придется сегодня выложиться по-полной. В этой игре у нас будет все, как положено — и показ следующего столбика и подсчет очков и запись рекордов в энергонезависимую память ардуины. А тем, кого игрушка заинтересовала прямо сейчас, могут скачать ее версию для Андроида в PlayMarket.
Правила игры Columns
В «Стакан» падают столбики, состоящие из 3-х цветных квадратиков. Игрок может перемещать столбик вправо и влево, а также перемещать по кругу квадратики внутри столбика. Если игроку удается выстроить цепочку из квадратиков одного цвета длиной от 4-х и более, то вся эта цепочка уничтожается, а вышестоящие квадратики проваливаются на освободившееся место. В цепочку собираются квадратики, имеющие соседей одного цвета по горизонтали и вертикали. За уничтоженные цепочки игроку начисляются очки. Задача игрока — набрать как можно больше очков.
Вкратце пройдемся по железу, которое нам понадобится. Начнем с Arduino. Я собрал схему на Arduino Uno, просто потому, чтобы не использовать дополнительных макетных плат, но подойдет и Nano и Mini Pro на Atmega 328 (Atmega 168 со стандартным загрузчиком не пойдет из-за недостатка памяти). Для других моделей Arduino придется поменять аппаратные пины SPI на свои из даташитов.
Распиновка 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.
Теперь о дисплее. Брал я его в прошлом году на ебее, где его и сегодня предлагает тот-же продавец за $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». В заголовке примера поменял назначенные пины на свои
вот в этих строчках:

#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 // длительность длинного нажатия. Длительность длинного нажатия в миллисекундах. Длинное нажатие срабатывает при достижении этого значения и только один раз до отпускания кнопки. Короткое нажатие срабатывает только при отпускании кнопки, если время нажатия не превысило времени длинного нажатия.
В конце обзора я, как обычно, представляю видео процесса игры, на котором можно оценить скорость отрисовки графики и плавность перемещения столбиков под управлением энкодера.


Для чего делалось? Просто интересно осваивать новое, а осваивать всегда интереснее, когда в результате получается что-то полезное. Ну, или, по крайней мере, — прикольное, но работающее! У следующего исследуемого дисплея будет уже вдвое большее разрешение и, главное, — тачскрин, осталось дождаться посылки.

Всем хорошего настроения и новых рекордов!

P.S. Если у кого возникнут вопросы по описанным тут железкам и программе, постараюсь ответить.
Добавить в избранное +99 +149
+
avatar
+11
Тоже писал примитивную игру под дисплей hitachi HD44780. По крайней мере я не мог другого применения найти кейпадшилду:

+
avatar
  • sim31r
  • 21 августа 2018, 05:27
+1
Я вижу матрицу ))
+
avatar
  • sir0ta
  • 15 августа 2018, 20:58
+1
У следующего исследуемого дисплея будет уже вдвое большее разрешение и, главное, — тачскрин, осталось дождаться посылки.
Уже более приятный экранчик. Пользуюсь только ими. Подобная мелочь… ну слишком мелкие. 1.8 считай не разглядеть. А уж разместить там что-то полезное.
Ну то что цветные классно, но то что не читаются… ну углы обзора ни о чем. И контрастности. Вот помогал тут товарищу… так после того как вспомнил по Oled… Вышло так что по читаемости 1.3'' Oled оказался в разы бодрее 2.4'' TFT. Есть еще кста 2.8'' экранчики.
+
avatar
  • klop
  • 15 августа 2018, 21:01
0
Вышло так что по читаемости 1.3'' Oled оказался в разы бодрее 2.4'' TFT
Это да. Только OLED выцветают быстро, долго статическую картинку на них не подержишь, а, бывает, надо.
+
avatar
+1
Обзор однозначно плюсанул. Но не потому что дуже все понял или заинтересовался))) но было интересно почитать про такое. Спасибо.
+
avatar
  • shmv
  • 15 августа 2018, 23:42
+1
в чо к телеаизору не подцепил на прямую через вч вход?
как это сделал один чел с esp8266
+
avatar
  • sim31r
  • 21 августа 2018, 05:29
0
Очень сложный программный код там. Для телевизора есть всякие Banana Pi, RPi, сразу HD качество…
+
avatar
  • chukchuk
  • 16 августа 2018, 00:12
+1
вот что пиво с людьми делает, от микроши до ардуино
+
avatar
  • mozgj
  • 16 августа 2018, 01:35
+6
У ардуино уж очень медленный SPI для работы с цветными экранами. Я подключал точно такой же экран к STM32F0, где SPI можно разогнать на 24 МГц, там обновление всего экрана занимает менее 30 мс, и простая анимация (в пределах возможностей STM32F0 с его 8КБ памяти) работает очень плавно. С экраном 320х240 на ILI9341 уже подтормаживает, но все равно кадров 10 в секунду можно добиться.
+
avatar
  • Kruzo
  • 16 августа 2018, 11:02
+1
Логично что у меги SPI медленнее. У меги тактовая 16МГц, а у STM32F0 48МГц. Максимальная частота SPI у меги Fosс/2 = 8МГц. На меге для своих поделок использовал ЧБ дисплей от Nokia 1202 за 1$. На 4МГц SPI полная заливка у меня получалась 120Гц. И весь буфер в память влезает. Сейчас еще и олед есть.
Цветные для меги это, на мой взгляд, перебор.
Цветные дисплеи на али продаются еще и с 8бит и 16бит шиной. Можно взять 8бит и STM32F103. А кому нужна максимальная плавность берут 16 бит и STM32 с FSMC.
+
avatar
  • Kruzo
  • 16 августа 2018, 11:39
0
Если не пугает 8/16 бит шина и пайка шлейфа на плату, то можно посмотреть дисплеи от мобильников в локальных магазинах. Может выйти и дешевле, чем на али.
+
avatar
  • mozgj
  • 16 августа 2018, 13:33
0
С параллельной шиной все-таки многовато возни. Ну и кейл бесплатный только для STM32F0.
Еще, кстати, на скорость влияет адафрутовская библиотека — если заливки, вертикальные и горизонтальные линии там оптимизированы, то шрифты рисуются попиксельно с установкой координат каждый раз (а это 12 байт и 4 смены A0). Это дает возможность рисовать текст поверх фона, но если нужно весь экран заполнить текстом, то блочное рисование раза в 3 быстрее.
+
avatar
  • maxspb69
  • 16 августа 2018, 08:40
0
У следующего исследуемого дисплея будет уже вдвое большее разрешение
Боюсь на таком дисплее слабенькая ардуина сдуется окончательно. Цветные дисплеи вообще для Atmega очень тяжелая задача.
+
avatar
  • klop
  • 16 августа 2018, 11:00
0
Боюсь на таком дисплее слабенькая ардуина сдуется окончательно
Если постоянно перерисовать весь экран, то даже на дисплее из статьи резко падает плавность перемещения столбиков. Поэтому перерисовывается только то, что изменилось. Так, что многое зависит от самой задачи и правильной программной реализации.
+
avatar
  • maxspb69
  • 16 августа 2018, 13:01
0
О постоянном перерисовывании экрана даже речи идти не может, да это и не нужно для такой задачи. Только фрагментами. Но и в этом случае 2 байта на пиксель — тяжело для ATmega. Перерисовка квадратика всего 8х8 пикселей — 128 байт, как-никак…
+
avatar
  • sim31r
  • 21 августа 2018, 05:43
0
А зачем тратить 128 байт? На ходу можно создавать байты «одного цвета» при передаче данных. Так же и для букв.
+
avatar
  • neuzshto
  • 17 августа 2018, 15:17
0
Цветные дисплеи вообще для Atmega очень тяжелая задача.
Можно и так:
http://forum.amperka.ru/threads/%D0%98%D0%9A-%D0%BF%D0%B0%D1%8F%D0%BB%D1%8C%D0%BD%D0%B0%D1%8F-%D1%81%D1%82%D0%B0%D0%BD%D1%86%D0%B8%D1%8F-%D0%BD%D0%B0-arduino-mega-2560-%D0%94%D0%BE%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D1%81%D0%BA%D0%B5%D1%82%D1%87%D0%B0-ars_v2_lilium_jsn.10176/page-71#post-170949
+
avatar
  • Alex_74
  • 16 августа 2018, 08:43
0
А к малине или апельсинке этот экран можно подцепить?
+
avatar
  • maxspb69
  • 16 августа 2018, 08:45
0
к малине можно подцепить экран намного большего разрешения и размера.
+
avatar
+1
Имею печальный опыт цепляния TFT дисплея с тачскрином. А именно — тачскрин выдавал неправильные координаты касания, как если бы был повернут на 45 градусов по часовой стрелке, то есть два касания на одной горизонтали выдавали координаты, как будто изменилась и Х и У координата. Думал математически можно обработать и вычислить правильную координату — нет, нельзя. Две точки расположенные физически, условно говоря в координатах (4,2) и (3,3) выдавали программно одну и ту же координату. Надеюсь, понятно описал проблему. Так что очень интересно, у кого вы заказали другой экран, что за экран и чем у вас дело кончится. Держите в курсе, и дайте ссылку, какой экран заказали еще.
+
avatar
  • klop
  • 16 августа 2018, 10:51
+1
Заказал тут. Вроде продавец нормальный. Звание «Лучший продавец», положительных отзывов 99.4%. Но спасибо за предупреждение. Теперь сначала все проверю перед подтверждением получения. Постараюсь не забыть отписаться.
+
avatar
  • abyrwalg
  • 16 августа 2018, 14:25
0
даже если продавец — просто торгаш, общение с ним может быть полезно (при должной настойчивости)
на профильных форумах совсем ничего не нашлось?
+
avatar
  • Win95CIH
  • 16 августа 2018, 09:58
0
а вы не эксперементировали с матрицей на WS2812?
+
avatar
  • klop
  • 16 августа 2018, 10:46
0
Не эксперементировал
+
avatar
  • pet80
  • 16 августа 2018, 12:50
0
Возможности Arduino порой просто поражают. Когда-то у меня был компьютер «Микроша», купленный за 500 советских рублей (а это несколько тогдашних зарплат!) и при этом он не имел ни цвета, ни графики. Работал он с ч/б теликом по антенному входу.
Интересное сравнение скажем мягко платы продающейся в настоящее время и компьютера 80-х годов прошлого столетия, фактически начала эры компьютеризации. Сравнили бы хотя с ZX Spectrum тех же времен.
+
avatar
  • maxspb69
  • 16 августа 2018, 12:57
+1
Не в тему, но ZX Spectrum тоже компютер из начала 80х прошлого века и применявшиеся в упомянутых девайсах (Микроша и Spectrum) процессоры очень близки по архитектуре и производительности. (8080 и Z80)/ Впрочем, контроллерам ATMEGA, применяемым во всевозможных ардуинах тоже не один десяток лет уже и они далеко не верх совершенства по производительности.
+
avatar
  • sim31r
  • 21 августа 2018, 05:36
+1
ATmega как-раз верх совершенства. Они не для производительности, это микроконтроллеры, нереально универсальные. В приложении автора работают с графикой как аналог ZX Spectrum на полную мощь (их еще и разогнать можно). А у меня они работают от батарейки кнопочки и потребляют 1 мкА в режиме сна, ток сравним с саморазрядом батарейки. Кому-то нужна помехоусойчивость, чтобы работал в сантиметре от разрядника, кому-то работа в жидком азоте (и работают), кому-то при +100 градусах под капотом.
Новые ревизии кстати постоянно выходят, снижают потребление в основном.
Недавно сравнительно вышел ATtiny44, на борту множество инструментов для снижения потребления…
+
avatar
  • klop
  • 16 августа 2018, 13:01
+1
Интересное сравнение скажем мягко платы продающейся в настоящее время и компьютера 80-х годов прошлого столетия
Да, но я сравниваю старый, но полноценный компьютер, на котором я сделал диплом и, хоть и современный, но микроконтроллер.
p.s.
Сравнили бы хотя с ZX Spectrum тех же времен
ZX у меня не было, но глянул в Вики. Проц Z80 работал на частоте 3,5 МГц. Супротив 16 у Atmega 328? Так, что… А сравнивать тогдашние цены на Spectrum и нынешние на ардуину будем? )
+
avatar
  • maxspb69
  • 16 августа 2018, 13:04
+2
Микроконтроллер = микропроцессор + периферия и (иногда) память на одном кристалле. В советские времена микроконтроллеры именовали «однокристалльными микро-ЭВМ. Суть не меняется. Микроконтроллер STM32 легко эмулирует ZX-Spectrгm полностью не напрягаясь ни разу. При этом будучи размером с рублевую монету, потребляя несколько десятков миллиампер и работая на порядок быстрее спектрума, если специально не замедлять. Вдобавок ко всему, кроме питания для этого ему не надо ничего сверху. И он не менее „полноценный компьютер“, чем тот, на котором Вы сделали свой диплом (я, кстати, тоже :) )
+
avatar
  • klop
  • 16 августа 2018, 13:21
+1
И он не менее „полноценный компьютер“, чем тот, на котором Вы сделали свой диплом (я, кстати, тоже :) )
Если подходить формально, то, согласно Вики, различие между контроллером и компьютером кардинальное:
Персона́льный компью́тер, ПК (англ. personal computer, PC), ПЭВМ (персональная электронно-вычислительная машина) — настольная микро-ЭВМ, имеющая эксплуатационные характеристики бытового прибора и универсальные функциональные возможности.
Микроконтро́ллер(англ. Micro Controller Unit, MCU) — микросхема, предназначенная для управления электронными устройствами

Но в жизни грани между ними уже весьма эфемеры. Так, что я с Вами соглашусь.
+
avatar
  • sim31r
  • 21 августа 2018, 05:37
0
Сравнил технологии 1000 нм и 30 нм, причем кроме размера там еще много чего нового поколения, архитектура, материалы, всё почти ))
+
avatar
  • maxspb69
  • 16 августа 2018, 14:49
+1
Тут нельзя впрямую сравнивать тактовую частоту этих совершенно разных микропроцессоров. ATMEGA — RISC-архитектура, Z80 — CISC. То, что Z80 делает одной командой, пускай и за десяток тактов, в атмеге придется выполнять за 20 однотактовых команд. Реальная производительность будет отличаться не в разы.
+
avatar
  • sim31r
  • 21 августа 2018, 05:41
0
В Z80 по моему даже сопроцессора нет, убран для удешевления. По сути всё одно и тоже, только Z80 в разы медленней. Плюс ATmega не числодробилка, а очень экономичный микроконтроллер, хорошо работающий от батарейки и токами в микроамперы, быстродействие просто побочный эффект технологии современной.
+
avatar
  • pet80
  • 16 августа 2018, 15:03
0
Проц Z80 работал на частоте 3,5 МГц. Супротив 16 у Atmega 328?
А сравните игрушки на ZX Spectrum, компьютере а не отдельно взятом процессоре Z80 и Arduino :).
А сравнивать тогдашние цены на Spectrum и нынешние на ардуину будем? )
Сравнивать цены на электронику прошлого века и нынешнего, не ).
+
avatar
0
ТС, на вашем дисплее есть пин регулировки подсветки? Работает ли он если есть?
Просто надоело уже перебирать эти дисплеи (( сколько не брал ни на одном не работает либо без ШИМ либо нет вообще хотя заявлен.
+
avatar
  • klop
  • 16 августа 2018, 21:37
+2
Отдельный пин зачем? Я подсветку через резистор 150 Ом подключил к +5V. Специально, для Вас, провел эксперимент. Теперь подсветку воткнул в пин 6 и написал в setup() строчку analogWrite(6,125); Яркость уменьшилась вдвое. Пин 6 в Atmega 328 поддерживает аппаратный ШИМ. оператором analogWrite(6,x) можно изменять x от 0 до 255. Или я Вас неверно понял?