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.getWasClicked()) a přičtení skóre musí proběhnout až po zastavení kuřete (chicken.isStanding()). Nepravda !food.getWasCounted() 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:

Domnívám se, že metoda clickedPosition(...) 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 clickedPosition(...) 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 receivePosition(...) 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ě initAssetsObjects():
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 26x (6.39 MB)
Aplikace je včetně zdrojových kódů v jazyce Java