Grafický projekt 1

V rámci tejto kapitoly si vysvetlíme postup začatia tvorby triedy AnimovanýObjekt, ktorá bude základom pre ďalšie grafické objekty hry. Pripomíname, že tak ako všetko v rámci tohto materiálu, aj toto je len jedno z možných riešení. Programovanie je v širokej miere tvorivá činnosť a pri tomto type úloh je málokedy možné nájsť jednoznačné riešenie úlohy. My ukážeme náš spôsob riešenia a poskytneme vysvetlenie, čo nás k tomu spôsobu riešenia viedlo, kam tým smerujeme a kde jednotlivé časti triedy využijeme v rámci celého projektu.

V tomto materiáli sú niektoré procesy tvorby urýchlené. V praxi je často tvorba podobného projektu zdĺhavejšia. Funkcionalita tried je rozširovaná postupne, podľa potrieb projektu. Často sa postupuje tak, že sa priebežne spisujú požiadavky na rozšírenie funkcionality tried – spracúva sa zoznam požiadaviek (wish-list). Na základe neho sú triedy postupne rozširované, až kým nie je projekt dokončený. Zoznam požiadaviek je dôležitým komunikačným prostriedkom pri projektoch, na ktorých spolupracuje tím viacerých programátorov. Každá požiadavka musí byť sformulovaná veľmi presne!

Jestvujú určité všeobecné pravidlá spolupráce. V rámci nich je dôležité naučiť sa dobre posúdiť, čo má programátor programovať sám, a naopak, aké požiadavky posielať kolegom programujúcim iné časti softvéru (iné triedy). Zásada je, aby to, čo sa dotýka vnútra (správania a stavov) každej triedy, bolo implementované priamo v nej, nie nejakou „obchádzkou“ zvonka. Ak potrebujeme zistiť stav, ovplyvniť správanie objektu určenej triedy, riešime to naprogramovaním metódy alebo skupiny metód vo vnútri konkrétnej triedy, nikdy nie zvonka nej (v inej triede). Porušili by sme princíp objektovej uzavretosti (encapsulation). Ak nie sme autormi triedy, spíšeme požiadavku adresovanú jej autorovi.

Náš projekt je síce produktom jedného autora, spisovanie požiadaviek však vždy dobre poslúži na zlepšenie organizácie práce. Takže zoznam požiadaviek dokáže využiť každý (i samostatný) programátor. Odporúčame založiť si zoznam požiadaviek pri každom projekte čo najskôr. V tomto vzdelávacom materiáli budeme vo väčšine prípadov predpokladať, že zoznam požiadaviek jestvuje a je automaticky priebežne aktualizovaný.

Tvorba každej novej triedy sa začína naprogramovaním konštruktora inicializujúceho objekt. Základný konštruktor (default constructor) neprijíma žiadne argumenty, úplný konštruktor (full constructor) prijíma zoznam všetkých súkromných inštančných premenných, ktoré na základe toho inicializuje. Jednoduchá verzia triedy AnimovanýObjekt s úplným konštruktorom by mohla vyzerať takto:

public class AnimovanýObjekt extends GRobot
{
    private double posunX, posunY;
 
    public AnimovanýObjekt(double posunX, double posunY)
    {
        this.posunX = posunX;
        this.posunY = posunY;
    }
}

Tento jednoduchý konštruktor iba kopíruje hodnoty prijímaných argumentov do súkromných inštančných premenných majúcich (spravidla) rovnaký názov. Na odlíšenie medzi argumentmi a súkromnými premennými slúži ukazovateľ this.

Úplný konštruktor o chvíľu stratí prívlastok „úplný“, pretože budeme potrebovať triedu AnimovanýObjekt rozšíriť. Viacero inštančných premenných budeme inicializovať automaticky. Stanovíme si na to vlastný predpis. Využijeme hodnoty ostatných inštančných premenných. V nasledujúcej tabuľke sú vymenované všetky vnútorné inštančné premenné triedy AnimovanýObjekt. Tabuľka zároveň obsahuje stručné vysvetlenie účelu každej premennej.

Údajový typ

Názov premennej

Účel

Pozícia

pozícia

aktuálne logické umiestnenie tohto objektu (brehy/plť); vymenovací typ Pozícia sme definovali v predchádzajúcej kapitole

String

meno

pomenovanie objektu (nájde využitie na viacerých miestach v rámci tejto triedy)

String

zvuk

názov súboru so zvukom vydaného objektom pri pohybe

String[]

obrázokDoľava,
obrázokDoprava

polia uchovávajúce názvy súborov s obrázkami využívanými na animáciu chôdze pri pohybe doprava a doľava, pričom prvý prvok (s indexom 0) rezervujeme pre statický obrázok objektu

AnimovanýObjekt

cieľ

ukazovateľ na cieľový objekt, ku ktorému sa má tento objekt pohybovať (bude využité pri presune postáv na plť)

double

posunX, posunY

relatívna poloha grafiky (obrázkov vlastného tvaru objektu) voči skutočným súradniciam (stredu) objektu

double

ľavýX, ľavýY

cieľová poloha objektu na ľavom brehu rieky

double

pravýX, pravýY

cieľová poloha objektu na pravom brehu rieky

double

posunPopisuX,
posunPopisuY

relatívna poloha popisu (mena objektu) zobrazovaného pri presunutí ukazovateľa myši ponad neho

int

fázaChôdze

číslo určujúce aktuálnu fázu animácie chôdze

Na dodržanie princípu objektovej uzavretosti, budú všetky premenné súkromné. Tie inštančné premenné, ktoré nebudú inicializované v konštruktore, inicializujeme priamo pri ich definícii:

private Pozícia pozícia = Pozícia.pravýBreh;
private String meno, zvuk;
// …
private double posunPopisuY;
private int fázaChôdze = 0;

Skúste doplniť deklarácie a definície ostatných premenných na miesto označené komentárom. Správe riešenie nájdete v prílohách kapitoly…

Pre triedu definujeme jediný konštruktor prijímajúci najdôležitejšie údaje, podľa ktorých dokážeme inicializovať viaceré ďalšie vnútorné premenné triedy. Konštruktor bude prijímať meno objektu, posun grafiky a posun popisu. Spomínanú inicializáciu viacerých vnútorných premenných môžeme vykonať vďaka dohode stanovenej pre naše potreby. Bude sa dotýkať spôsobu pomenovania (nomenklatúry) grafických a zvukových súborov. V jednej z prípravných kapitol grafického projektu sme už naznačili, ako bude nomenklatúra vyzerať. Budeme sa pridržiavať nasledujúcej schémy:

Účel súboru

Schéma pre názov

obrázok nehybného objektu obráteného doľava

meno + "-ľ.png"

obrázok nehybného objektu obráteného doprava

meno + "-p.png"

fáza animácie číslo i pri pohybe objektu doľava

meno + "-ľ-" + i + ".png"

fáza animácie číslo i pri pohybe objektu doprava

meno + "-p-" + i + ".png"

zvuk vydaný objektom pri pohybe

meno + ".wav"

Všetky názvy využívajú meno objektu. Premenná i bude nadobúdať číselné hodnoty v rozmedzí 1 až počet fáz chôdze (ktorý definujeme neskôr). Ešte pridáme konvenciu ukladania všetkých súborov do spoločného priečinka s názvom súbory. Pri nomenklatúre súborov to zohľadníme tak, že pred každý prvok schémy pridáme text "súbory/". Nomenklatúru mediálnych súborov projektu môžeme považovať za vyriešenú.

Animácia nebude povinná pre všetky objekty, preto inicializáciu s ňou súvisiacich prvkov ponecháme na samostatnú metódu volanú v prípade potreby. Práve sme zhromaždili všetky informácie potrebné na zostavenie konštruktora. Ten bude za týchto okolností vyzerať takto:

public AnimovanýObjekt(String meno, double posunX, double posunY,
    double posunPopisuX, double posunPopisuY)
{
    zdvihniPero();
 
    obrázokDoľava[0] = "súbory/" + meno + "-ľ.png";
    obrázokDoprava[0] = "súbory/" + meno + "-p.png";
    zvuk = "súbory/" + meno + ".wav";
 
    this.meno = meno;
    this.posunX = posunX;
    this.posunY = posunY;
    this.posunPopisuX = posunPopisuX;
    this.posunPopisuY = posunPopisuY;
 
    spôsobKreslenia(KRESLI_NA_STRED);
    rýchlosť(10, false);
}

Objektu sme nastavili konštantnú rýchlosť 10 bodov za jeden tik[6] a spôsob kreslenia „na stred“, vďaka čomu nebudú obrázky vlastného tvaru robota pootočené v aktuálnom smere robota. (V neskoršej fáze projektu si môžete skúsiť, ako by to vyzeralo, keby sme túto vlastnosť nenastavili.) Taktiež sme zdvihli pero, aby objekty pri presune nekreslili na plátno čiaru.

Môžeme prejsť na definovanie vlastného tvaru animovaného objektu (odvodeného od robota). Aj keď prekrytie metódy kresliTvar nie je optimálnym spôsobom kreslenia vlastného tvaru (pozri dokumentáciu skupiny tried GRobot), rozhodli sme sa ho (na zjednodušenie) v rámci toho projektu využiť. Vlastný tvar robota (alias animovaného objektu) bude kreslený podľa aktuálnej orientácie robota:

@Override public void kresliTvar()
{
    skoč(posunX, posunY);
 
    if (smer() >= 90 && smer() <= 270)
        obrázok(obrázokDoľava[0]);
    else
        obrázok(obrázokDoprava[0]);
}

(Na zamyslenie: prečo je podmienka if stanovená práve takto? Na aký účel sme do tela metódy umiestnili prvý príkaz skoč?)

Na to, aby sme mohli do metódy dopracovať animáciu pohybu, musíme vykonať niekoľko ďalších definícií. Najprv definujeme konštantu určujúcu maximálny počet fáz animácie:

private final static int fázChôdze = 2;

Tú spätne využijeme pri definícii inštančných polí obrázokDoľavaobrázokDoprava. Opäť skúste urobiť definíciu najskôr samostatne.

Pre potreby vnútorného riadenia triedy zavedieme konvenciu. Ak bude hodnota premennej fázaChôdze nenulová, bude to pre nás znamenať, že objekt je animovaný a budeme sa spoliehať, že polia obsahujú názvy súborov s animáciou chôdze.

Takouto konvenciou môžeme ušetriť objem vnútorných premenných, prípadne optimalizovať kód, avšak treba konať premyslene. Každú takúto dohodu treba zapracovať do komentárov v zdrojovom kóde. Nikdy si nemôžeme byť na sto percent istí, že triedu po nás nebude niekto v budúcnosti upravovať…

Na tento účel je vhodné definovať samostatnú metódu. Prostredníctvom nej získa objekt možnosť zapnutia animácie i v rámci odvodených tried:

public void umožniAnimáciuChôdze()
{
    for (int i = 1; i <= fázChôdze; ++i)
    {
        obrázokDoľava[i] = "súbory/" + meno + "-ľ-" + i + ".png";
        obrázokDoprava[i] = "súbory/" + meno + "-p-" + i + ".png";
    }
    fázaChôdze = 1;
}

Pred dokončením metódy kresliTvar definujeme ešte jednu pomocnú metódu vPohybe, ktorú umiestnime v rámci projektu všade tam, kde budeme potrebovať detegovať, či sa objekt momentálne pohybuje. Momentálne je jej obsah jednoduchý – objekt sa pohybuje, keď je aktívny. Na prvý pohľad sa zdá, že metóda je zbytočná, pretože namiesto nej by sme mohli použiť priamo metódu aktívny. Napriek tomu odporúčam zvyknúť si tvoriť podobné metódy. Ich benefit sa často prejaví neskôr, napríklad ak treba upraviť správanie odvodenej triedy alebo ak by sme zrazu spätne potrebovali preprogramovať (zovšeobecniť alebo rozšíriť) podmienky detekcie pohybujúceho sa objektu:

public boolean vPohybe()
{
    return aktívny();
}

Z rovnakého dôvodu definujeme metódu myšV, ktorú využijeme všade tam, kde budeme potrebovať zistiť, či sa ukazovateľ myši nachádza nad objektom:

public boolean myšV()
{
    return myšVElipse(50, 60);
}

Obe metódy využijeme aj v rámci kreslenia. Pre tie objekty, ktorým sme umožnili animáciu v pohybe (samozrejme pri spoľahnutí sa, že súbory s fázami animácie jestvujú – vytvárali sme ich počas prípravy grafického projektu), naprogramujeme mechanizmus animácie a nad tými objektmi, nad ktorými sa práve nachádza ukazovateľ myši, zobrazíme ich meno. Úplná verzia metódy kresliTvar bude vyzerať takto:

@Override public void kresliTvar()
{
    skoč(posunX, posunY);
 
    if (vPohybe() && fázaChôdze != 0)
    {
        if (smer() >= 90 && smer() <= 270)
            obrázok(obrázokDoľava[fázaChôdze]);
        else
            obrázok(obrázokDoprava[fázaChôdze]);
    }
    else if (smer() >= 90 && smer() <= 270)
        obrázok(obrázokDoľava[0]); else obrázok(obrázokDoprava[0]);
 
    domov();
    if (myšV())
    {
        skoč(posunPopisuX, posunPopisuY);
        text(meno);
    }
}

Implementácia animovaného objektu nie je len o grafickej stránke. Zvyšnú funkcionalitu dopracujeme v nasledujúcej kapitole.

Príloha 8 – prvá fáza tvorby triedy AnimovanýObjekt

Zobraziť | Prevziať

[6] Tik je udalosť časovača. Vzniká v pravidelných intervaloch určených aktuálnym nastavením časovača.