Toto je starší verze dokumentu!
Pomocí desky FRDM KL25Z a grafického displeje ITDB02 realizujte hru Arkanoid ovládanou akcelerometrem.
Cílem hry je tedy odpalování míčku pomocí pálky směrem do pole cihel v horní části obrazovky tak, aby byly všechny cihly zničeny.
Původním zamýšleným displejem byl displej ITDB02. Vzhledem k nesnázím s oživením displeje, které si autor nejprve odůvodňoval rozdíly v dokumentaci samotného shieldu a řídícího obvodu pro displej (viz archiv s dokumentací) byl po dohodě s vyučujícím zvolen monochromatický displej s rozlišením 128x64. Při oživování tohoto displeje a konzultaci s kolegy bylo zjištěno, že problém se nachází na kitu FRDM KL25Z - když byly pro přenos dat a řídících signálů do displeje použity vnější řady pinů na konektorech J2 a J1 (kromě těch, které nemohou být použity jako GPIO, viz pinout), displej nezobrazoval správně. Tyto vnější řady pinů byly defaultně použity také při pokusech s prvním displejem (logicky vzhledem k jeho pinoutu), chyba tedy mohla být způsobena kitem FRDM KL25Z a nikoliv ITDB02 shieldem.
Displej byl používán spolu se zapůjčenou doplňující deskou vytvořenou některým ze studentů kurzu MMIA v minulých letech. Tato doplňující deska umožňuje nastavení kontrastu displeje trimrem.
Následující tabulka zobrazuje zvolené mapování datových a řídících pinů displeje:
Displej | D/I | R/W | E | RES | CS1 | CS2 | DB0 | DB1 | DB2 | DB3 | DB4 | DB5 | DB6 | DB7 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
KL25Z | PTE23 | PTE29 | PTE30 | PTE22 | PTE20 | PTE21 | PTC7 | PTC0 | PTC3 | PTC4 | PTC5 | PTC6 | PTC10 | PTC11 |
Firmware byl psán v prostředí mbed.org. Pro komunikaci s displejem byla využita knihovna KS0108. Z časových důvodů není kód rozdělen do více souborů, vše se nachází v souboru main.cpp
, který lze nalézt v repozitáři projektu. Ze stejných důvodů obsahuje hra pouze jednu úroveň, funkce ale byly psány tak, aby bylo možné jednoduše přidat další úrovně.
V následujícím textu budou popsány jednotlivé používané funkce.
Ve funkci main()
jsou nejprve inicializovány displej a akcelerometr a také hodnoty dále používaných proměnných, poté jsou v nekonečné smyčce volány jednotlivé funkce:
int main() { ... DrawLogo(); DrawTable(score,lives); DrawBorder(); ... CreateGrid(&(column[0]), &(row[0]), &(BrickExists[0])); DrawBricks(column, row, BrickExists); while(1) { display.TurnOn(); if (StartFlag){ reset(&XBall, &YBall, &Balldx, &Balldy, &PadPos, &StartFlag, &score, &lives, &(column[0]), &(row[0]), &(BrickExists[0])); } BounceBricks(XBall, YBall, &Balldx, &Balldy, &(BrickExists[0]), row, column, &score, lives, &StartFlag); DrawBall(&XBall, &YBall, &Balldx, &Balldy, BallD, PadPos, &StartFlag, &lives); MovePad(&PadPos, StartFlag); wait(0.05); } }
Jednotlivé funkce jsou popsány dále, na konci nekonečné smyčky bylo přidáno prosté čekání. V důsledku poměrně nízké obnovovací frekvence zobrazení displeje byla hodnota času pro čekání určena experimentálně jako kompromis mezi teoretickou rychlostí a výslednou kvalitou pozorovaného obrazu.
Funkce DrawLogo()
vykresluje pomocí funkcí knihovny KS0108 v levém horním rohu displeje nápis ARMkanoid
. Funkce DrawBorder()
vykresluje ohraničení herního pole. Hranice tohoto pole jsou dány konstantami překladače minX
, maxX
, minY
a maxY
.
Funkce DrawTable(score, lives)
vykresluje v levé spodní části displeje jednoduchou tabulku s údaji o skóre a počtu zbývajících pokusů. Tyto hodnoty jsou uloženy v proměnných definovaných v main
u a některé další funkce je mohou měnit přístupem přes ukazatele. Obsah tabulky je formátován do string
ů.
Funkce CreateGrid()
generuje do polí column[columns]
a row[rows]
inicializovaných v main
u hodnoty hranic řádků a sloupců cihel, které budou později využity pro jejich vykreslení. Hodnoty columns
a rows
jsou konstanty překladače definované na začátku kódu. Zároveň tato funkce plní pole BrickExists[columns*rows]
typu bool
- hodnoty tohoto pole informují o tom, zda má být daná cihla zobrazována, nebo ne (tedy zda byla zničena). Aby se zamezilo využívání globálních proměnných a bylo možné jednou funkcí měnit obsah více proměnných, přistupuje se do těchto polí přes ukazatele.
Funkce DrawBricks()
dle obsahu polí column[]
, row[]
a BrickExists[]
naplněných předchozí funkcí vykresluje matici cihel. Parametry cihel, které jsou pro všechny stejné (šířka, výška) jsou dány konstantami překladače BrickW
, BrickH
. Původně bylo zamýšleno použít místo 3 samostatných polí jedno pole struktur o 3 položkách, přístup do položek struktur v tomto poli ve funkci volané z main
u se však zdál příliš komplikovaný.
V nekonečné smyčce je po aktualizaci vykreslovaného obsahu na základě stavu proměnné StartFlag
typu bool
volána funkce reset()
. Tato funkce obstarává nastavení výchozích poloh míčku a pálky a jejich korektní vykreslení, dále generuje náhodný výchozí úhel pro prvotní odraz míčku od pálky. V případě, že byly zničeny všechny cihly, informuje o výhře, v případě, že byly vyčerpány všechny životy, naopak o prohře. Poté dochází k restartu celé hry. Opět je hojně využíváno ukazatelů. Následující kód znázorňuje určení náhodného úhlu při prvotním odrazu:
float angle = (pi/180)*(90+(rand()%(2*maxangle))); *dxBall = (round(velocity*cos(angle))); if (*dxBall == 0){ *dxBall = 1; } *dyBall = (-round(velocity*sin(angle)));
Ve hře není zahrnuto „tření pálky“, tedy jev, kdy dojde ke změně úhlu odrazu na pálce, pokud se v okamžiku odrazu pálka právě pohybuje. Z tohoto důvodu je po určení náhodných hodnot dxBall
a dyBall
ošetřeno, aby nesmělo dxBall
nabývat hodnoty 0. Pokud by k tomu došlo, nebylo by ve hře možné zničit všechny cihly, míček by se pohyboval pouze po svislé ose. Hodnota dyBall
při prvotním odrazu je vzhledem k orientaci displeje vždy záporná.
Funkce BounceBricks()
vyhodnocuje odrazy míčku od cihel, jejich vymazávání po zásahu a přičítání skóre. Pokud dojde k dosažení maximálního skóre, nastaví flag StartFlag
, na základě toho je v dalším průběhu nekonečné smyčky v main
u funkcí reset
uživatel informován o výhře.
void BounceBricks(uint8_t XBall, uint8_t YBall, int8_t* dxBall, int8_t* dyBall, bool* BrickExists, uint8_t ro[rows], uint8_t col[columns], uint8_t* score, uint8_t lives, bool* Start){ if (YBall <= (ro[rows-1]+BrickH)){ int8_t i, j; for (j = rows - 1; j >= 0; j--){ if (((YBall-BallD) <= (ro[j]+BrickH))&&((YBall+BallD) > ro[j])){ break; } } for (i = columns - 1; i >= 0; i--){ if (((XBall-BallD) <= (col[i]+BrickW))&&((XBall+BallD) > col[i])){ break; } } if (*(BrickExists+j*columns+i)){ *(BrickExists+j*columns+i) = false; display.FullRectangle(col[i], ro[j], (col[i] + BrickW), (ro[j] + BrickH), WHITE); (*score)++; if (*score == (rows*columns)){ *Start = true; } DrawTable(*score, lives); if (!((YBall-*dyBall) <= (minY))){ *dyBall = -(*dyBall); } }else{ if ((YBall-BallD) <= (minY+1)){ *dyBall = -(*dyBall); } } } }
Pokud se míček právě nenachází v oblasti, kde se mohou vyskytovat cihly (vymezené hodnotou nejnižšího řádku matice cihel), žádný z dalších příkazů funkce BounceBricks()
se nevykoná. V opačném případě je ve dvou cyklech zjištěna pozice míčku v mřížce cihel a dále je testováno, zda se na této pozici cihla nachází (tedy zda má příslušný prvek pole BrickExists[]
hodnotu true
). Pokud ano, dochází ke smazání cihly z displeje, nastavení příslušného prvku pole BrickExists[]
na false
, inkrementaci skóre, překreslení tabulky s novou hodnotou skóre a odrazu míčku. Překreslování tabulky se skóre zavádí mírné zpoždění, které se projevuje při odrazu míčku od cihly. Při pokusech o rychlejší přepis dané oblasti displeje mimo funkci DrawTable()
docházelo k nesmyslnému přeskupování znaků na displeji a jejich zasahování do jiných oblastí (např. překreslování loga). Z tohoto důvodu bylo ponecháno řešení s překreslením funkcí DrawTable()
.
Tato funkce obstarává vykreslení míčku a jeho odrazy od stěn a od pálky. V případě, že míček se nepodařilo odrazit pálkou a ten tak propadl, je odečten jeden „život“ či pokus. Pokud je dosaženo nulového počtu pokusů, je nastaven flag StartFlag
a na základě toho je v následujícím průběhu nekonečné smyčky v main
u uživatel informován o prohře prostřednictvím funkce reset
.
void DrawBall(uint8_t* X, uint8_t* Y, int8_t* dx, int8_t* dy, uint8_t D, uint8_t PaddlePos, bool* Start, uint8_t* lives){ //draws the ball, computes reflections from border of gaming area display.FullCircle(*X, *Y, D, WHITE); (*X) += (*dx); (*Y) += (*dy); display.FullCircle(*X, *Y, D, BLACK); if ((((uint8_t)(*X+D)) >= minX)||(((int8_t)(*X-D)) <= maxX)){ *dx = -(*dx); } if (((uint8_t)(*Y-D)) <= minY){ *dy = -(*dy); } if (((int8_t)(*Y+D)) >= PadY){ if ((((uint8_t)(*X+D)) <= (PaddlePos+PadLength+2))&&(((uint8_t)(*X-D)) >= (PaddlePos-2))){ *dy = -(*dy); }else{ *Start = true; if (*lives > 0){ (*lives)--; } } } }
V předchozím kódu je nejprve „přemazán“ míček v původní poloze. Poté dochází ke změně polohy o dx
a dy
. Následují 3 podmínky - první zajišťuje odraz míčku od svislých hranic herní plochy, druhá od horního okraje herní plochy. Odrazy jsou realizovány změnou znaménka dx
nebo dy
, úhel odrazu je tedy roven úhlu dopadu. Poslední z podmínek zajišťuje odraz míčku od pálky a odečtení životů v případě, že se míček odrazit nepodařilo.
Tato funkce zajišťuje vykreslování pálky a její ovládání akcelerometrem. Je využívána osa akcelerometru Y. Pálka nemění rychlost posuvu spojitě dle náklonu desky - jsou definovány 2 rychlosti posuvu pálky, jimž odpovídají 2 prahové hodnoty výstupu akcelerometru.
void MovePad(uint8_t* Pad_ptr, bool Start){ uint8_t PaddleDif; if ((abs(acc.getAccY()))>AccTres1){ display.FullRectangle(*Pad_ptr, PadY, *Pad_ptr+PadLength ,PadY+2,WHITE); if(Start){ *Pad_ptr = XPadInit; }else{ if ((abs(acc.getAccY()))>AccTres2){ PaddleDif = HighSpeed; }else{ PaddleDif = LowSpeed; } if ((acc.getAccY() > 0)&&((*Pad_ptr+PadLength) < (minX - 3))){ *Pad_ptr += PaddleDif; } if ((acc.getAccY() < 0)&&(*Pad_ptr > (maxX + 1))){ *Pad_ptr -= PaddleDif; } } }
Pokud byla vyhodnocena akcelerace desky vyšší, než nižší z prahových hodnot, dochází k přemazání původní pozice pálky. Poté je v závislosti na velikosti výstupu akcelerometru pálka posunuta o konstantu HighSpeed
nebo LowSpeed
ve směru odpovídajícím náklonu.
V první části videa je hra úspěšně dohrána, po restartu je záměrně prohrána pro demonstraci chování v obou případech.
V rámci tohoto projektu byl vyvinut klon hry Arkanoid ovládaný akcelerometrem vývojového kitu FRDM KL25Z. Hra zatím obsahuje pouze jednu úroveň včetně počítání skóre a zbývajících pokusů a je plně funkční.