[CZĘŚĆ 2] STM32Butterfly2: Tetris na STM32 – interfejs użytkownika
Do rysowania pojedynczych pikseli służy funkcja LcdPixel. Przyjmuje ona trzy argumenty: współrzędną X, współrzędną Y oraz wartość PIXEL_ON lub PIXEL_OFF w zależności czy zapalamy czy gasimy dany piksel. Przy obliczaniu współrzędnych korzystamy z zmiennych x i y z pętli. Mnożymy te wartości przez 2, ponieważ każdy segment planszy ma wymiar 2×2, oraz dodajemy wcześniej zdefiniowany offset oraz ewentualne przesunięcie o 1 w obrębie danego segmentu. Analogicznie narysujemy spadający klocek, jednak tym razem użyjemy jednej pętli odliczającej 4 kolejne jego segmenty:
//rysownie spadającego klocka for(i=0;i<4;i++) //odliczanie 4 segmentów klocka { x=klocki[kloceknr][klocekr][i][0] + klocekx-2; //obliczanie współrzędnych na planszy y=klocki[kloceknr][klocekr][i][1] + kloceky-2; //danego segmentu klocka LcdPixel(POFFX+x*2, POFFY+y*2, PIXEL_ON); //rysowanie kolejnych 4 pikseli segmentu LcdPixel(POFFX+x*2+1, POFFY+y*2, PIXEL_ON); LcdPixel(POFFX+x*2, POFFY+y*2+1, PIXEL_ON); LcdPixel(POFFX+x*2+1, POFFY+y*2+1, PIXEL_ON); }
Tym razem, aby obliczyć współrzędne danego segmentu musimy sięgnąć do tablicy definiującej klocek podając numer kształtu klocka oraz jak jest obrócony. Następnie dodać offset planszy oraz współrzędną klocka na planszy. Ponieważ współrzędne klocka umownie wskazują jego środek odejmujemy jeszcze od wszystkiego wartość 2. Tak obliczoną wartość x i y wykorzystujemy do narysowania czterech pikseli danego segmentu klocka.
Identycznie narysujemy drugi klocek w okienku z podpisem NEXT. Jest to podpowiedź dla gracza, jakiego następnego klocka ma się spodziewać. Zmiany będą polegały na wykorzystaniu innych offsetów oraz zmiennej nkloceknr zamiast kloceknr.
Zostało nam jeszcze wyświetlenie wyniku gry. Typowe biblioteki graficzne oferują nam zazwyczaj funkcję wyświetlającą pojedyncze znaki ASCII. Ja mam w swojej bibliotece właśnie taką funkcję: LcdChr(znak_ascii) oraz funkcję LcdGotoXY(wsp.x, wsp.y) do ustawiania kursora na daną pozycję. Przy czym rysowanie kolejnych znaków automatycznie przesuwa kursor do przodu.
Identyczny fragment programu wyświetla liczbę zaliczonych linii. Ponieważ wszystkie dotychczasowe operacje graficzne były dokonywane na buforze, funkcja wyświetlająca kończy się wywołaniem funkcji LcdUpdate, która przenosi zawartość bufora na ekran.
W tym miejscu na pewno każdy nie myśli o niczym innym, jak tylko o tym, aby w końcu wypróbować to, co do tej pory napisaliśmy. Skompilowanie i uruchomienie programu z niemal pustą funkcją main na pewno jest bezcelowe. Musimy dodać główną pętlę programu, w której zawartość ekranu będzie cyklicznie odświeżana w zależności od wartości zmiennej stan_gry. Proponuję dodać do funkcji main następującą procedurę:
while (1) { if(refresh==1) { refresh=0; switch(stan_gry) //sprawdzanie stanu gry { case GRA: //gra uruchomiona RysujEkran(); break; case GAMEOVER: //koniec gry break; case PAUZA: //pauza break; case INTRO: //intro break; } } }
W nieskończonej pętli sprawdzana jest wartość nowej zmiennej refresh. Pozwoli to dać sygnał z innych części programu, kiedy należy odświeżyć zawartość ekranu i zapobiegnie wykonywaniu tej czynności w kółko nawet wtedy, gdy nie jest to konieczne. Aby wygodnie było operować zmienną refresh dodałem prostą funkcję (poniżej), która ją ustawia:
//Funkcja ustawia zmienną refresh void OdswiezEkran(void) { refresh=1; }
W konstrukcji opartej na switch program sprawdza wartość zmiennej stan_gry, ponieważ nie zawsze zawartość wyświetlacza będzie taka sama. Na razie program zadziała tylko w czasie rozgrywki, wyświetlając planszę gry. Z czasem dodamy funkcję wyświetlającą intro oraz komunikaty o pauzie i zakończeniu gry. Podobną konstrukcję opartą o polecenie switch dodamy do obsługi klawisza „ok” (naciśnięcie z góry) joysticka:
switch(stan_gry) { case GRA: //Jeżeli gra włączona stan_gry=PAUZA; //włącz pauzę break; case PAUZA: //Jeżeli gra w czasie pauzy stan_gry=GRA; //wróć do gry break; case INTRO: //Jeżeli włączone intro stan_gry=GRA; break; case GAMEOVER: //Jeżeli nastąpiła przegrana CzyscPlansze(); //wyczyść planszę KlocekNowy(); //zmień klocek na nowy punkty=0; //wyzeruj licznik punktów linie=0; //i zaliczonych linii stan_gry=INTRO; //przejdź do intro break; } OdswiezEkran();
Jeżeli naciśniemy „ok” w trakcie trwania intro, to uruchomi się gra, w czasie gry uruchomimy w ten sposób pauzę, a po zakończeniu gry powrócimy do intro uprzednio kasując punkty i czyszcząc planszę.
Na końcu uruchamiana jest funkcja OdswiezEkran, aby wszystkie zmiany widoczne były natychmiast na ekranie. Jej wywołanie musimy dodać na końcu każdego fragmentu obsługującego poszczególne ruchy joysticka.
Pozostało nam już tylko upiększyć naszą grę, dodać jakąś ładną stronę tytułową i napisy świadczące o wstrzymaniu (pauzie) i zakończeniu gry. Zacznijmy od najprostszego napisu PAUZA, jaki pojawi się podczas przerwy w grze. Okazało się, że dobrze wygląda napis PAUZA nałożony na dotychczasowy ekran z grą. Jeżeli naciśniemy „ok” joysticka to napis zniknie, a gra będzie toczyła się dalej. Spróbujmy dodać ten efekt do gry. Najpierw zaprojektowałem wygląd tekstu informującego o wstrzymaniu gry, który będzie wyświetlany na tle będącym zawartością ekranu.
Jak to zrobić? Tego, co mamy już na wyświetlaczu nie musimy oczywiście odczytywać. Całą zawartość ekranu mamy w buforze, dalej wykorzystamy funkcję, której użyliśmy do wyświetlenia tła (kopiuje 504 bajty z tablicy do bufora ekranu). Można ją łatwo przerobić tak, żeby nakładała grafikę z tablicy na dotychczasową zawartość ekranu. Wykorzystałem do tego funkcję logiczną OR (LcdCache[i] = LcdCache[i]|bitmap[i];) i stworzyłem nową funkcje o nazwie LcdLoadAddBMP, która „dodaje” nową grafikę na tło. Teraz wystarczy zamienić projekt grafiki napisu PAUSE na tablicę dokładnie tak samo jak w przypadku tła i wyświetlić ja przy pomocy nowej funkcji.
Naciśnięcie przycisku „ok” uruchomi pauzę, zmieniając zmienną stangry. Dzieje się to w obsłudze przerwania wywoływanego przez joystick, która kończy się wywołaniem funkcji OdswiezEkran. W konsekwencji zostanie wykonana procedura w funkcji main, w której znajduje się wcześniej przygotowana przez nas struktura case. Właśnie tam musimy umieścić polecenia rysujące napis PAUSE. Żeby napis był czytelny trzeba go otoczyć wygaszonymi pikselami. Czeka nas dodanie kolejnej funkcji graficznej, tym razem „odejmującej”. Przygotowujemy grafikę tła dla naszego napisu. Z efektu naszej pracy generujemy tablicę z danymi, tym razem nazwaną pauza2. Funkcja odejmująca wygląda tak:
void LcdLoadSubBMP (const unsigned char* bitmap) { int i; for ( i = 0; i < LCD_CACHE_SIZE; i++ ) { LcdCache[i] = LcdCache[i]&( 0xFF-bitmap[i]); } }