Individální projekty MPOA

Mikroprocesory s architekturou ARM

Uživatelské nástroje

Nástroje pro tento web


2014:arkanoid-game

Hra Arkanoid

Zadání

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.


Úvod

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.


Hardware

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

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.


funkce main()

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() a DrawBorder()

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()

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 mainu a některé další funkce je mohou měnit přístupem přes ukazatele. Obsah tabulky je formátován do stringů.


funkce CreateGrid()

Funkce CreateGrid() generuje do polí column[columns] a row[rows] inicializovaných v mainu 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()

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 mainu se však zdál příliš komplikovaný.


Takto vypadá displej po vykreslení loga, tabulky s nulovým skóre a 3 zbývajícími životy, ohraničení herní plochy, matice cihel, pálky a míčku ve výchozí pozici.


Funkce reset()

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()

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 mainu 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().


Funkce DrawBall()

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 mainu 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.


Funkce DrawPad()

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.


Demonstrační video

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.


Závěr

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ň, funkce však byly psány tak, aby rozšíření o další úrovně nebylo příliš pracné. Hra je plně funkční a zobrazuje také informace o skóre a počtu zbývajících pokusů. Kompletní kód včetně použitých knihoven lze nalézt v repozitáři projektu.

2014/arkanoid-game.txt · Poslední úprava: 2015/01/18 23:41 autor: Jindřich Šindelář