LoL shield jako prosta gra z Arduino

Gdy tylko w moje ręce wpadł shield LoL czyli Lots of LEDs (jak to przetłumaczyć próbowałem to wyszło DoD – Dużo Diód), wiedziałem co chciałem zrobić. Są one dostępne w wersji z diodami czerwonymi lub zielonymi. Różnica jest tylko w kolorze cała reszta jest identyczna.

Ale najpierw trzeba było go zmontować. Wyglądało to na męczące zadanie. Wbrew pozorom zlutowanie prawie 130 diód, które są w tym zestawie nie jest ani takie trudne ani takie męczące. Mnie zmontowanie całości zajęło około 2h, wliczając w to pomoc ze strony bardzo uczynnego 4.5 latka :) Opis na stronie autora jest bardzo dokładny i ułatwia montaż znakomicie.

Wyświetlacz 9×13 którym jest LoL shield to całkiem sporo. Co prawda oddajemy prawie wszystkie wyjścia cyfrowe z Arduino, ale do wyświetlania tekstów – całkiem, całkiem. Aha – zostają nam dwa wolne wyjścia cyfrowe D0 i D1 bo na nich jest serial. Jeżeli mamy już układ którego nie trzeba debugować (he he) to w sumie te dwa wyjścia są do wykorzystania.

Ze względu na to, że brak jest wolnych wyjść cyfrowych więc shield jako taki nie ma złączy, dla wejść analogowych jest wyprowadzone po jednym złączu w formie pola do lutowania. Wyjątkiem są A4 i A5 które są dodatkowo wyprowadzone na krawędź shielda. Dlaczego? Bo A4 i A5 to są piny wykorzystywane przez I2C. A I2C świetnie się nada do komunikacji z wyświetlaczem, który znajduje się gdzieś dalej.

Ale ja nie chciałem wyświetlać tekstów, od razu wiedziałem, że chcę zrobić jakąś bardzo prostą grę. Arkanoid raczej nie (Circuloid’owi rynku nie będę odbierał :) ) więc z klasyków zostają albo Space Invaders, albo Pong. Ale ja wybrałem klona Spy Hunter ;) Pamiętacie tę muzyczkę? Art of Noise, theme z Peter Gunn. Tutaj w trochę innym wykonaniu:

Ale wróćmy do LoL. Biblioteka do LoL jest banalnie prosta w użyciu najpierw LedSign::Init(); a potem można używać LedSign::Set do ustawiania/gaszenia diód. Funkca Set ma trzy argumenty współrzędną x, y oraz wartoś 0/1 odpowiednio gaszącą lub zapalającą wybraną diodę. Dlatego pisząc naszą grę można się skupić na samym game play :) zamiast na żmudnym wyświetlaniu…

A na poważnie – potrzebujemy jakiegoś kontrolera do gry. Mój wybór padł na DYI kontroler czyli:

  1. płytkę stykową BB-400
  2. dwa przyciski
  3. trzy rezystory 10k
  4. trochę zworek

Jak to działa? Z rezystorów robimy dzielnik napięcia, między rezystory wpinamy przełączniki, które zwierają do masy jeden lub dwa rezystory. Analog 0 podpinamy między dwa rezystory (w naszym przykładzie między R3 i R2). I tak jeżeli zewrzemy (wciśniemy) S2 wówczas odczyt z A0 będzie 0 – bo ten punkt został zwarty do masy. Jeżeli S2 jest zwolniony a S1 wciśnięty, wówczas A0 będzie wskazywało około 512 – połowa napięcia Vcc gdyż mamy dwa rezystory 10k i mierzymy między nimi napięcie. R1 jest w tym przypadku zwarty do masy z obu stron więc 'nie bierze udziału’ w naszym układzie. Gdy oba przyciski nie są wciśnięte, wówczas odczyt powinien wynieść około 680 – 2/3 napięcia Vcc (dzielnik napięcia 10k/20k).

Tak po prawdzie, zamiast jednego rezystora można wykorzystać wbudowany w ATMega328 rezystor pull-up na wejściu cyfrowym, ale w tym przykładzie początkowo było więcej przycisków zaplanowanych (tak właśnie jest zrealizowany odczyt z przycisków w LCD shieldzie) i jakoś ten rezystor dodatkowy został w breadbordzie…

Końcowy rezultat wygląda mniej więcej tak:

DIY kontroler gry
DIY kontroler gry

A tutaj schemat i rysunek z Fritzinga, jeśli ktoś potrzebuje sobie zrobić podobny:

Schemat kontrolera
Schemat kontrolera
Rysunek fizycznych połączeń kontrolera
Rysunek fizycznych połączeń kontrolera

Uzbrojeni w wiedzę jak działa kontroler możemy spróbować napisać prostą funkcję odczytującą:

void checkKey() {
  int static prev = 1023;
  
  int r = analogRead(0);
  
  if (r < 20 && prev > 20) {
    key = 1;
  } else if (r >= 20 && r < 530 && (prev < 20 || prev > 530 )) {
    key = -1;
  }
  
  prev = r;
}


Wartość prev jest używana do określenia czy zmienił się odczyt. Słowo kluczowe static powoduje, że zmienna jest inicjalizowana na 1023 tylko za pierwszym wywołaniem funkcji. W następnych wywołaniach wartość prev jest taka jak w momencie zakończenia poprzedniego wywołania.

Odczytujemy wartość z A0 i jeżeli jest bliska zeru i a poprzednio była inna to znaczy, że wciśnięto jeden klawisz. W przeciwnym razie jeżeli wartość jest w okolicy 512 (well, nie przekracza 520, bo 30 trudno uznać za w okolicy 512 ;) ) i jednocześnie poprzednia wartość nie była w tym zakresie to wciśnięto drugi klawisz. Trzecia opcja nic nie robi.

Do szczegółów dlaczego tak zrobiłem jeszcze wrócę.

Teraz – wyświetlacz.

Drogę przechowuję w tablicy road – ma ona tyle wierszy ile jest linii na wyświetlaczu, a w każdym wierszu przechowuję lewą i prawą krawędź drogi (ich współrzędną). Dla uproszczenia całości założyłem, że droga rysowana zawsze ma stałą szerokość, więc w sumie możnaby przechowywać tylko jedną stronę i dodawać/odejmować aby uzyskać drugą współrzędną. Arduino jednak ma na tyle pamięci aby przechować obie. Zresztą dzięki temu łatwiejsze będzie w przyszłości zrobienie na przykład zwężeń drogi.

Jak już droga będzie wygenerowana to musimy ją przesuwać. Służy do tego prosta funkcja shiftRoad:

void shiftRoad() {
  for (int i=MAX-1;i > 0; i-- ) {
    road[i][LEFT] = road[i-1][LEFT];
    road[i][RIGHT] = road[i-1][RIGHT];
  }
}

I tak gdy już zostanie przesunięta droga, dla kompletu potrzebujemy funkcji losującą drogę, która pojawia się na szczycie:

void createRoad() {
  int r = random(0,3);
  switch(r) {
    case 0:
      road[0][LEFT] = road[1][LEFT]-1;
      break;
    case 1:
      road[0][LEFT] = road[1][LEFT]+1;
      break;
    case 2:
      break;
  };

  if (road[0][LEFT] < 0 )
    road[0][LEFT] = 0;
    
  if (road[0][LEFT] > 4 )
    road[0][LEFT] = 4;
    
  road[0][RIGHT] =  road[0][LEFT] + 4;
  
  Serial.print(road[0][LEFT]);
  Serial.print(", ");
  Serial.println(road[0][RIGHT]);
}

Musimy upewnić się, że nam droga nie ucieka z wyświetlacza, dlatego sprawdzamy czy lewa krawędź nie jest za daleko. Jeśli jest, poprawiamy i na końcu wyliczamy gdzie jest prawa krawędź.

Zmienna car przechowuje współrzędną samochodu. I teraz możemy wrócić do wartości którą dostajemy z checkKey – jest to po prostu przesunięcie o ile ma się zmienić położenie samochodu. Gdy żaden klawisz nie został wciśnięty, jest to zero.

Przed przesunięciem gasimy diodę oznaczjącą pozycję samochodu. Potem przesuwamy samochód ( car += key ) i kasujemy wartość key, tak aby przy następnym wywołaniu moveCar nie przesuwać dalej samochodu.

Pozostaje sprawdzić czy czasem nie wpadliśmy na bandę i jeśli tak to zrobić efektowne boom() a jeśli się mieścimy w drodze – wyświetlamy samochód w nowym miejscu.

Teraz tylko wyświetlić drogę:

void displayRoad(){
  for(int i=0; ifor (int j=0; j<9; j++) { 
      LedSign::Set(i,j,0);
    };
    //skip drawing line on edge when initializing
    if (road[i][LEFT] >= 0) {
     LedSign::Set(i,road[i][LEFT],1);
     LedSign::Set(i,road[i][RIGHT],1);
    }
}
}

I co jeszcze? Przy starcie musimy jakoś ładnie narysować drogę i dać graczowi chwilę na zorientowanie się gdzie jest. Ta sama procedura jest wykorzystywana po wypadku do odrysowania całego ekranu, stąd swojska nazwa startAgain:

void startAgain() {
  LedSign::Init();
  for (int i=0;i<= MAX; i++) {
    for (int j=0; j<9; j++) {
      LedSign::Set(i,j,0);
    };
    road[i][LEFT] = -1;
    road[i][RIGHT] = -1;
  };
  delay(1000);
  
  road[0][LEFT] = random(0,4);
  road[0][RIGHT] = road[0][LEFT]+4;
  for(int i=0;i< MAX-1; i++) {
   createRoad();
   shiftRoad();
   displayRoad();
   delay(i*20);
  }
  car = road[MAX-1][LEFT]+2;
  key = 0;
  moveCar();
  delay(1000);

};

Ekran jest czyszczony i droga jest generowana wiersz po wierszu z rosnącym opóźnieniem – ponieważ za każdym razem odrysujemy całość, więc wygląda to jakby droga 'zjeżdżała’ z góry.

Funkcji boom nie muszę specjalnie opisywać, jest to próba narysowania prostego 'wybuchu’.

Wyjaśnić jeszcze trzeba zawartość loop():

void loop(){
  if (loopCntr++ == 20) {
   shiftRoad();
   createRoad();
   displayRoad();
   moveCar();
   loopCntr = 0;
  } else {
    checkKey();
  };
  delay(10);
};

W każdym przebiegu sprawdzamy czy nie został naciśnięty klawisz. Jeżeli nawet został naciśnięty na chwilę, zostanie to zapisane w key. Klawisz nie musi być naciśnięty w momencie gdy jest odświeżany widok, tylko w każdym przebiegu jest wywoływany checkKey a ewentualny wynik jest zapisywany w zmiennej key.

W co 20stym przebiegu nie sprawdzamy klawiszy tylko:
przesuwamy drogę (shiftRoad)
dodajemy brakującą część na górze (createRoad)
rysujemy aktualny wygląd drogi (displayRoad)
sprawdzamy czy trzeba zmienić położeni samochodu i czy mieści się wsh drodze (moveCar)

Manipulując wartości przy której rysujemy drogę (20) oraz opóźnieniem w każdym przebiegu możemy regulować 'responsywność’ i szybkość gry.

A jak wygląda całość?

Aha – chiptune w tle jest z komputera (dla klimatu) a nie Arduino :)

Pozostaje ściągnąć kod całości, który jest udostępniony tutaj.