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.
Následující text je dokumentací projektu zabývajícího se vývojem klonu Arkanoidu ovládaného akcelerometrem vestavěným na vývojovém kitu FRDM KL25Z. Obsahuje popis hardware i jednotlivých funkcí firmware, které jsou popsány buď slovně, nebo příslušnými úseky kódu. Autor nepovažuje použití vývojových diagramů pro popis projektu tohoto charakteru za nezbytné. Je zde uveden také odkaz na repozitář projektu obsahující veškeré potřebné soubory včetně použitých knihoven. V závěru je přiloženo krátké video pro demonstraci funkce.
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šechny funkce 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ý.
Takto vypadá displej po vyklreslení loga, tabulky pro skóre, ohraničení herní plochy, matice cihel, pálky a míčku.
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í. Kompletní kód včetně použitých knihoven lze nalézt v repozitáři projektu.