IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 15 - Programujeme Android hru - Animace, zvuky

Vítejte v dalším díle našeho kurzu programování hry pro Android. Než budeme pokračovat, stručně si vysvětlíme princip činnosti našeho kódu z dílu minulého, Programujeme Android hru - Collision detection.

Třída AssetManager.java je jasná, stejně jako u obrázků jsme provedli načtení bitmapového písma (fontu) do paměti - nic nového.

Zajímavější je třída OverlapsManager­.java. Neustále dokola je volána její metoda update(...), která obsahuje na první pohled složitou podmínku:

if (Intersector.overlaps(food.getRectForOverlap(),chicken.getRectForOverlap()) && food.getWasClicked() && (!food.getWasCounted()) && chicken.isStanding())

Vypadá složitě, ale nic na ni není. Používáme zde metodu overlaps(...) vestavěné třídy Intersector, která vrací true, pokud se protínají dva obdélníky předané jako argumenty. V našem případě to jsou obdélníky rectForOverlap v instancích chicken a food. Nám to ale nestačí, to že se krmivo protíná s kuřetem, ještě neznamená přičtení skóre.

Musí být splněny další podmínky. Na krmivo musí být kliknuto (food.getWasClic­ked()) a přičtení skóre musí proběhnout až po zastavení kuřete (chicken.isStan­ding()). Nepravda !food.getWasCou­nted() zajistí, aby skóre bylo přičteno pouze jednou, protože jak již bylo řečeno, metoda update(...) je volána neustále. V okamžiku, kdy jsou tyto podmínky splněny, dojde ke zvýšení skóre o jedničku a nastavení proměnné lifeTime v instanci food na 0, tím fakticky začne losování a následné promítnutí krmiva na nových souřadnicích. Neustálé opakované volání metod update() v našem projektu bychom v současnosti mohli znázornit následujícím diagramem:

Volání metod update - Programujeme Android hru

Domnívám se, že metoda clickedPositi­on(...) je výstižně okomentovaná. Je zavolána instancí inputManager vždy při kliknutí uživatele na obrazovku a jako argumenty přijímá souřadnice tohoto kliku. Stará se o nastavení proměnné wasClicked v instanci krmiva a o zvýšení/reset rychlosti v instanci kuřete.

To je k objasnění minulé lekce vše. Již dobře víme, že v souladu se zásadou OOP zapouzdření "musíme" proměnné třídy deklarovat jako private a vytvořit k nim přístupové metody a také, že po definici třídy musíme vytvořit její instanci (nyní opomeňme statickou třídu). Přidané metody do třídy Render.java není nutné objasňovat, z jejich názvu je jasné, co promítají.

Zvuky a animace

Dnes slepici přidáme zvuky a animaci. Vždy, když hráč klikne na krmivo, slepice mu pípnutím "odpoví", že kliknul na správné místo, kde se potrava nachází. Dále přidáme animaci se zvukem, která se přehraje vždy, když jsou splněny podmínky pro zvýšení skóre, tato animace bude tedy znázorňovat, že slepice právě žere.

Začneme nahráním zdrojů. Otevřeme třídu AssetManager.java a pod deklarace stávajících proměnných přidáme:

public static Texture eatLeft1,eatLeft2,eatLeft3,eatLeft4;      //na animaci eating
public static Texture eatRight1,eatRight2,eatRight3,eatRight4;      //na animaci eating
public static TextureRegion rEatLeft1,rEatLeft2,rEatLeft3,rEatLeft4;    //na animaci eating
public static TextureRegion rEatRight1,rEatRight2,rEatRight3,rEatRight4;//na animaci eating
public static Animation eatLeftAnime;                   //na animaci eating
public static Animation eatRightAnime;                  //na animaci eating

public static Music beepSound,eatSound;

Nyní všechny tyto proměnné načteme, na konec metody load() přidáme následující kód:

/* zacatek nahrani animaci eating */
eatLeft1=new Texture(Gdx.files.internal("eatleft1.png"));
eatLeft1.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatLeft2=new Texture(Gdx.files.internal("eatleft2.png"));
eatLeft2.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatLeft3=new Texture(Gdx.files.internal("eatleft3.png"));
eatLeft3.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatLeft4=new Texture(Gdx.files.internal("eatleft4.png"));
eatLeft4.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);

eatRight1=new Texture(Gdx.files.internal("eatright1.png"));
eatRight1.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatRight2=new Texture(Gdx.files.internal("eatright2.png"));
eatRight2.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatRight3=new Texture(Gdx.files.internal("eatright3.png"));
eatRight3.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
eatRight4=new Texture(Gdx.files.internal("eatright4.png"));
eatRight4.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);

rEatLeft1=new TextureRegion(eatLeft1,0,0,115,90);
rEatLeft1.flip(false, true);
rEatLeft2=new TextureRegion(eatLeft2,0,0,115,90);
rEatLeft2.flip(false, true);
rEatLeft3=new TextureRegion(eatLeft3,0,0,115,90);
rEatLeft3.flip(false, true);
rEatLeft4=new TextureRegion(eatLeft4,0,0,115,90);
rEatLeft4.flip(false, true);
TextureRegion[]rEatLeft1234={rEatLeft1,rEatLeft2,rEatLeft3,rEatLeft4};
eatLeftAnime=new Animation(0.2f,rEatLeft1234);
eatLeftAnime.setPlayMode(Animation.PlayMode.LOOP);

rEatRight1=new TextureRegion(eatRight1,0,0,115,90);
rEatRight1.flip(false, true);
rEatRight2=new TextureRegion(eatRight2,0,0,115,90);
rEatRight2.flip(false, true);
rEatRight3=new TextureRegion(eatRight3,0,0,115,90);
rEatRight3.flip(false, true);
rEatRight4=new TextureRegion(eatRight4,0,0,115,90);
rEatRight4.flip(false, true);
TextureRegion[]rEatRight1234={rEatRight1,rEatRight2,rEatRight3,rEatRight4};
eatRightAnime=new Animation(0.2f,rEatRight1234);
eatRightAnime.setPlayMode(Animation.PlayMode.LOOP);
/* konec nahrani animaci eating */

beepSound = Gdx.audio.newMusic(Gdx.files.internal("pip12.mp3"));
eatSound = Gdx.audio.newMusic(Gdx.files.internal("woodpecker.mp3"));

Uklidíme po sobě, na konec metody dispose() přidáme:

eatLeft1.dispose();
eatLeft2.dispose();
eatLeft3.dispose();
eatLeft4.dispose();
eatRight1.dispose();
eatRight2.dispose();
eatRight3.dispose();
eatRight4.dispose();

beepSound.dispose();
eatSound.dispose();

Přidáme importy, třídu uložíme. Samozřejmě nezapomeneme tyto soubory naimportovat do složky assets.

Zdroje animací i zvuků máme, dále potřebujeme změnit (přepnout) stavové proměnné kuřete na EATLEFT nebo EATRIGHT vždy, když jsou splněny podmínky pro zvýšení skóre - když "slepice žere" s tím, že tímto přepnutím v renderu vyvoláme promítání těchto zdrojů animací. Otevřeme si třídu OverlapsManager­.java a její metodu update(...) změníme do následujícího tvaru:

public void update(float delta) {   // trida hlida, zda se instance food a chicken protinaji!
    if (Intersector.overlaps(food.getRectForOverlap(),chicken.getRectForOverlap()) &&
    food.getWasClicked() && (!food.getWasCounted()) && chicken.isStanding()) {
        score[0]++;
        food.setWasCounted(true);

        if (chicken.getStandingState() == StandingState.STANDLEFT || chicken.getStandingState() == StandingState.STANDRIGHT) {
            if (chicken.getStandingState() == StandingState.STANDLEFT)
                chicken.setStandingState(StandingState.EATLEFT);
            else
                chicken.setStandingState(StandingState.EATRIGHT);
        }
    }
}

V této třídě ještě upravíme metodu clickedPositi­on(...) a to tak, že do její 1. if podmínky přidáme příkaz k přehrání zvuku pípnutí kuřete, takže výsledná podoba této podmínky bude:

if(food.getRectForOverlap().contains(x, y)) {
    food.setWasClicked(true);
    chicken.incrWholeSpeed(10); // vzdy, kdyz tapneme na jidlo, zvysit rychlost
    if (!AssetManager.beepSound.isPlaying()) {
        AssetManager.beepSound.play();
    }
}

Předposlední záležitostí, kterou je nutné pro animace provést, je rozvětvení metody update(...) ve třídě ObjectManager.java tak, že když probíhá animace, bude se deltou odečítat její "doba přehrávání" a když animace neprobíhá, budou se updatovat ostatní objekty. Otevřeme tedy třídu ObjectManager.java a její metodu update(...) rozšiřme do následující podoby:

public void update(float delta) {
    // kdyz probiha animace, tak updatuj prave jen animaci
    if (chicken.getStandingState() == StandingState.EATLEFT || chicken.getStandingState() ==    StandingState.EATRIGHT) {

        if (!AssetManager.eatSound.isPlaying()) {
            AssetManager.eatSound.play();
        }
        if (eatAnimeLifetime <= 0) {    // animace skoncila?
            if(chicken.getStandingState() == StandingState.EATLEFT) {
                chicken.setStandingState(StandingState.STANDLEFT);
                food.setLifeTime(0);
            } else {
                chicken.setStandingState(StandingState.STANDRIGHT);
                food.setLifeTime(0);
            }
        }
        eatAnimeLifetime-=delta;
    } else  {
        chicken.update(delta);
        food.update(delta);
        overlapsMng.update(delta);
        eatAnimeLifetime=2;
    }
}

Přidáme importy, IDE ohlašuje chybu neznámé proměnné eatAnimeLifetime - napravíme. Pod deklarace stávajících proměnných třídy doplníme další:

float eatAnimeLifetime;

a na konci konstruktoru provedeme její inicializaci:

this.eatAnimeLifetime = 2;

Do metody receivePositi­on(...) musíme přidat ještě jednu if podmínku, její výsledná podoba bude:

public void receivePosition(int x,int y) { //souradnice z inputu //pozor, volano z inputu jen, kdyz je stav hry running

    if (!(chicken.getStandingState()==StandingState.EATLEFT || chicken.getStandingState()==StandingState.EATRIGHT) )
    {   // kdyz slepice zere, nereagovat na uzivatelsky vstup, kdyz zere tak nepravda
        overlapsMng.clickedPosition(x,y);
        turnMng.turnChicken(x, y);
        turnMng.setChickenDistance(x, y);
        turnMng.setChickenSpeed();

        chicken.setXPositionAchieved(false);
        chicken.setYPositionAchieved(false);
    }
}

Nyní již OK. Třídu uložíme.

Poslední nutnou věcí je promítnutí animace. Otevřeme třídu Renderer.java, kde řádek s deklaracemi animací rozšíříme do následující podoby:

private Animation standLeftAnime,standRightAnime,eatLeftAnime,eatRightAnime;

Provedeme inicializaci našich dalších animací, nejlépe poblíž stávajících inicializací proměnných standLeftAnime a standRightAnime v metodě initAssetsObjec­ts():

this.eatLeftAnime=AssetManager.eatLeftAnime;
this.eatRightAnime=AssetManager.eatRightAnime;

Na konci metody drawChicken(...) stávající else větev:

else {
    runTime+=delta; // kvuli animaci
    if (chicken.getStandingState() == StandingState.STANDLEFT)
        batcher.draw(standLeftAnime.getKeyFrame(runTime), chicken.getPositionX(), chicken.getPositionY(), chicken.getWidth(), chicken.getHeight());
    else if (chicken.getStandingState() == StandingState.STANDRIGHT)
        batcher.draw(standRightAnime.getKeyFrame(runTime), chicken.getPositionX(), chicken.getPositionY(), chicken.getWidth(), chicken.getHeight());
    else
        Gdx.app.log("renderer:", "Sem by se rizeni nemelo dostat.");
}

rozšíříme na:

else {
    runTime+=delta; // kvuli animaci
    if (chicken.getStandingState() == StandingState.STANDLEFT)
        batcher.draw(standLeftAnime.getKeyFrame(runTime),chicken.getPositionX(),chicken.getPositionY(),chicken.getWidth(),chicken.getHeight());

    else if(chicken.getStandingState() == StandingState.STANDRIGHT)
        batcher.draw(standRightAnime.getKeyFrame(runTime),chicken.getPositionX(),chicken.getPositionY(),chicken.getWidth(),chicken.getHeight());

    else if(chicken.getStandingState() == StandingState.EATLEFT)
        batcher.draw(eatLeftAnime.getKeyFrame(runTime),chicken.getPositionX(),chicken.getPositionY(),chicken.getWidth(),chicken.getHeight());

    else if(chicken.getStandingState() == StandingState.EATRIGHT)
        batcher.draw(eatRightAnime.getKeyFrame(runTime),chicken.getPositionX(),chicken.getPositionY(),chicken.getWidth(),chicken.getHeight());

    else
        Gdx.app.log("renderer:", "Sem by se rizeni nemelo dostat.");
}

Třídu uložíme. Tím máme pro dnešek hotovo. Pokud si nejsme čímkoli jisti, čerpáme ze zdrojového kódu, který je jako vždy včetně assets níže přiložen ke stažení. Aplikaci spustíme, animace eatLeftAnime i eatRightAnime fungují a také oba zvuky jsou v pořádku přehrány.

V příští lekci, Programujeme Android hru - Energie kuřete, si připravíme algoritmus pro řízení energie kuřete.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 25x (6.39 MB)
Aplikace je včetně zdrojových kódů v jazyce Java

 

Předchozí článek
Programujeme Android hru - Collision detection
Všechny články v sekci
Programujeme Android hru
Přeskočit článek
(nedoporučujeme)
Programujeme Android hru - Energie kuřete
Článek pro vás napsal Jaroslav Polívka
Avatar
Uživatelské hodnocení:
Ještě nikdo nehodnotil, buď první!
Autor se věnuje převážně jazykům JAVA a C++
Aktivity