TOPlist

Vyvíjíme pro WP v XNA: Animace a gesta na displeji (10. díl)

V novém díle našeho seriálu o programování her pro Windows Phone v XNA se podíváme na animace a dotyková gesta na displeji. Připravte si raketku a jdeme na to :-)

V minulých dílech jsme si vytvořili poměrně dobře hratelnou verzi naší hry. Nyní si do ní doplníme pár posledních drobností, ukážeme si jak dělat jednoduché animace a jak odchytávat gesta.

Když odpálíme laser (rychlým táhnutím prstu doprava), nebo tlakovou vlnu (roztažením dvou prstů od sebe), budeme chtít, aby se nám na displeji zobrazil obrázek laseru nebo záře. Chtěli bychom ale, aby tam na chvíli zůstal a až po chvilce se rozplynul a zmizel. Bude to tedy taková velmi jednoduchá animace. Do složky GameObjects našeho projektu si přidáme ještě jednu novou třídu, nazveme si ji TimedSprite. Opět si ji uděláme jako public, doplníme jí potřebné usingy Microsoft.Xna.Framework a Microsoft.Xna.Framework.Graphics a podědíme si ji od třídy Sprite.

 -

Doplníme si do ní dvě datové položky. Timeout bude udávat, kolik zbývá milisekund do zmizení, MaxTime bude celkový čas animace. Podle těchto dvou položek si potom budeme moci určovat průhlednost objektu. Přidáme si do ní také konstruktor, ve kterém si tyto položky nastavíme. Celá třída bude vypadat například takhle:

public class TimedSprite : Sprite {
    public double MaxTime, Timeout;

    public TimedSprite(Vector2 position, Texture2D texture, float timeout)
        : base(position, texture)
    {
        MaxTime = Timeout = timeout;
    }
}

Ještě si v ní přetížíme metodu Draw(), podobně, jako jsme to dělali u objektů asteroidů. Naše vykreslování si upravíme tak, aby využívalo průhlednosti. Doplníme si do této třídy tento kód:

public override void Draw(SpriteBatch spriteBatch)
{
    spriteBatch.Draw(Texture, Rect, Color.White * (float)(Timeout / MaxTime));
}

Vidíme zde, že kromě třetího parametru nám vykreslování zůstalo stejné. Pro určení průhlednosti objektu násobíme danou barvou desetinným číslem od nuly do jedné. Jednička znamená normální vykreslení, nula úplnou průhlednost. Toto je nová notace v XNA 4.0, předtím se to zapisovalo trochu jinak. Přepneme se nyní zpátky do třídy Game1 a zde si na obvyklé místo opět nadeklarujeme potřebné položky:

// Lasery a záře
List<TimedSprite> timedSprites = new List<TimedSprite>(); Texture2D laserTexture, glowTexture;

Všechny lasery a záře aktuálně zobrazené na displeji budeme mít uložené v dynamickém poli timedSprites. V metodě LoadContent() si zase načteme potřebné textury:

laserTexture = Content.Load<Texture2D>("laser");
glowTexture = Content.Load<Texture2D>("zare");

A v metodě Draw() si všechny objekty v tomto poli vykreslíme:

foreach (Sprite s in timedSprites)
    s.Draw(spriteBatch);

V metodě Update() si k našim objektům opět doplníme funkcionalitu. Budeme chtít zařídit, aby se jim odečítal čas, případně, aby nám zmizely po vypršení daného limitu. Přidáme si tam tedy tyto řádky (jejich význam by vám už měl být známý):

// Mizení laserů a září (časovaných spritů)
foreach (TimedSprite s in timedSprites) s.Timeout -= gameTime.ElapsedGameTime.TotalMilliseconds; timedSprites.RemoveIf(item => item.Timeout <= 0);

Všem položkám zde odečítáme časový úsek od vykreslení posledního snímku, pokud klesne pod nulu, objekty smažeme. Nyní se přesuneme o něco dál a podíváme se na odchytávání gest. V XNA Frameworku na Windows Phone si můžeme poměrně jednoduše detekovat základní akce typu posunutí prstu po displeji, roztažení dvou prstů apod. V konstruktoru hry (nebo v metodě Initialize()) si zvolíme, na jaká všechna gesta bychom chtěli reagovat, například takto:

TouchPanel.EnabledGestures = GestureType.Pinch | GestureType.PinchComplete | GestureType.Flick;

Do metody Update() si potom doplníme smyčku, ve které si budeme zjišťovat všechna aktuálně dostupná gesta. Bude vypadat například takto:

while (TouchPanel.IsGestureAvailable)
{
    GestureSample gs = TouchPanel.ReadGesture();
    if (gs.GestureType == GestureType.Flick)
    {

    }
}

Procházíme cyklem do té doby, dokud máme ještě některá gesta dostupná. Potom si v podmínce ověřujeme, zda nastalo nějaké konkrétní gesto. Do této podmínky si pro naše flick gesto můžeme doplnit tuto funkcionalitu:

if (availableBonuses > 0 && gs.Delta.X > 0)
{
    TimedSprite laser = new TimedSprite(new Vector2(jet.Size.X - 12f, jet.Position.Y + jet.Size.Y / 2f - 5f), laserTexture, 700f);

    for (int i = 0; i < asteroids.Count; i++)
        if (laser.Intersects(asteroids[i].Rect))
        {
            score += asteroids[i].MaxLife;
            asteroids.RemoveAt(i--);
        }

    timedSprites.Add(laser);
    availableBonuses--;
    break;
}

V případě, že nám tedy nastane flick gesto a směr posunutí v ose X bude směrem doprava, vyšleme do herního plánu objekt laseru. Ověřujeme si zde také, zda jsme vůbec mohli nějaký laser vyslat (jestli jsme předtím kliknuli na přilétávající bonus). Vytvoříme si zde nový objekt typu TimedSprite, umístíme si ho podle pozice raketky. Jako časový údaj, za který má zmizet, mu předáme 700 milisekund. Z pole asteroidů smažeme všechny objekty, se kterými laser koliduje. Používáme zde delší zápis (ne pomocí naší extension metody), podobně, jako jsme to už dělali při klikání na bonusy. Chceme si zde totiž za každý smazaný asteroid ještě přičíst určité body k našemu celkovému skóre. Položku score jsme zatím v naší hře nepotřebovali, nahoru si tedy ještě přidáme tuto deklaraci:

int score = 0;

Můžeme si odzkoušet naši hru spustit a otestovat, jestli už nám jdou flick gestem lasery vysílat. Po kliknutí na některý přilétávající bonus bychom měli být schopni rychlým tažením prstu laser vyslat. Hodnotu aktuálního skóre si na displej zatím nevykreslujeme, stejně tak jako celkový počet, kolik ještě laserů můžeme vyslat.

-

Jako další gesto si naimplementujeme roztažení dvou prstů od sebe. Pinch gesto je v podstatě jakékoliv tažení dvou prstů po displeji, to nám bude pro naši hru stačit. Můžeme si ho odchytávat pomocí dvou typů gest. Pinch se nám detekuje po celou dobu tažení prstů, PinchComplete tehdy, když gesto ukončíme. My si budeme chtít při každém začátku pinch gesta vyslat obrázek záře. Teprve ale až nadetekujeme zavolání PinchComplete, budeme moci vyslat další záři. Zabráníme tím, abychom si vypotřebovali všechny náboje jedním dlouhým tažením po displeji. Za podmínku if (gs.gestureType…) si doplníme ještě toto pokračování:

else if (gs.GestureType == GestureType.Pinch && !pinchStarted)
{                    
    if (availableBonuses > 0)
    {
        TimedSprite glow = new TimedSprite(new Vector2(0f, jet.Position.Y + jet.Size.Y / 2f - 200f), glowTexture, 1000f);
                        
        for (int i = 0; i < asteroids.Count; i++)
            if (IntersectsWithGlow(asteroids[i], glow))
            {
                score += asteroids[i].MaxLife;
                asteroids.RemoveAt(i--);
            }
                        
        timedSprites.Add(glow);
        availableBonuses--;
        pinchStarted = true;
    }
}
else if (gs.GestureType == GestureType.PinchComplete)
{
    pinchStarted = false;
}

Potřebný kód je velice podobný předchozí podmínce. Jak už jsme si popsali, ošetřujeme si zde navíc, aby se nám za každé tažení prstů vyslala jen jedna záře. Abychom toto mohli provádět, nahoře do deklarací si ještě přidáme položku pinchStarted:

bool pinchStarted = false;

Jinak si pro naši záři opět vytváříme nový objekt TimedSprite, umisťujeme si ho podle pozice raketky a nastavujeme dobu jeho animace na jednu sekundu. Ve for cyklu si můžete všimnout ještě volání jedné metody, kterou ještě nemáme naimplementovanou. Budeme si chtít posoudit kolizi asteroidu a záře, konkrétně jako kolizi dvou kruhů. Když si kliknete na podtržený název metody IntersectsWithGlow() a zvolíte Generate method stub…, do třídy Game1 se vám připraví správný předpis této metody. Do ní si doplníte určitý kód, celá bude vypadat například takhle:

private bool IntersectsWithGlow(AsteroidSprite item, TimedSprite glow)
{
    Vector2 center1 = item.Position + item.Size / 2f;
    Vector2 center2 = new Vector2(49f, glow.Position.Y + glow.Size.Y / 2f);
    float asteroidSide = Math.Max(item.Size.X, item.Size.Y);

    return Vector2.Distance(center1, center2) < (asteroidSide / 2f + glow.Size.Y / 2f);
}

Nejdřív si zde zjistíme střed kruhu našeho asteroidu a střed naší kruhové záře. Jako průměr asteroidu si vezmeme jeho delší stranu. Jako odpověď, zda spolu tyto objekty kolidují, potom vrátíme výsledek podmínky, zda vzdálenost jejich středů je menší, než součet jejich poloměrů. Nyní bychom měli být schopni vysílat oba typy zbraní, laser i tlakovou záři, můžeme si to odzkoušet.

Už jsme si popsali, jak odchytávat jednotlivé dotyky na displeji (dělalo se to stejně, jako klikání myší), stejně tak, jak detekovat gesta. Pomocí objektu TouchPanel se ale můžeme dostat i přímo k jednotlivým dotykům multi-touche. Když si budeme potřebovat například odchytávat nějaká vlastní gesta, tohle se nám bude hodit. Jen tak pro informaci přikládám kód, jak byste toto odchytávání více dotyků mohli například využít v metodě Update():

TouchCollection tc = TouchPanel.GetState();
foreach (var t in tc)
    if (t.State == TouchLocationState.Pressed || t.State == TouchLocationState.Moved)
    {
        if (t.Position.Y > 100f)
        {
                        
        }
    }

Z objektu TouchPanel si zde zjišťujeme všechny dostupné dotyky na displeji, potom si je procházíme cyklem. U každého dotyku máme informaci, zda byl právě stisknut (obdoba MouseDown), zda byl posunut, případně zvednut. Každý dotyk má také svoje unikátní ID, které při tažení po displeji zůstává pořád stejné. Poslední informací, kterou odsud můžeme vyčíst, je potom pozice dotyku.

Do našeho projektu si doplníme ještě jednu drobnost, budeme chtít přičítat body k celkovému skóre, i když si asteroid rozstřílíme obyčejnými náboji. V metodě Update() si najdeme řádek asteroids.RemoveIf(item => item.Life <= 0); v sekci kolize střel a asteroidů. Nahradíme si ho za tento cyklus:

for (int i = 0; i < asteroids.Count; i++)
    if (asteroids[i].Life <= 0)
    {
        score += asteroids[i].MaxLife;
        asteroids.RemoveAt(i--);
    }

V dnešním díle by to bylo už vše, podařilo se nám vytvořit už poměrně solidní hru. Samozřejmě prostoru pro zlepšení by zde bylo pořád hodně. Můžete si zkusit opravit a dokončit kolize tak, aby náboje správně reagovaly na kulatost asteroidů. Stejně tak si upravit kolizní oblast raketky, aby nebyla vůči asteroidům posuzována jako obdélník vůči obdélníku. U bonusů si můžete například dokončit, aby na ně nešlo kliknout, když se schovají za prolétávající planetkou. Do hry by se určitě vyplatilo přidat nějakou postupně zvyšující se obtížnost, případně další vylepšení hratelnosti (například objekty, které by nešly rozstřílet). Hráč by mohl dostávat speciální body za to, když se mu podaří sejmout více asteroidů naráz, nebo když mu dlouhou dobu žádný nevyletí ven z obrazovky. Samozřejmě by se do hry mohly naimplementovat i nějaké online žebříčky, aby si hráči mohli vzájemně porovnávat velikost svého skóre. To už nechám ale na vás, jak si svoji hru vyladíte. V příštím díle si konečně vyzkoušíme vykreslit text, případně si povíme něco o přehrávání hudby a dalších drobnostech.

Autor článku Tomáš Slavíček
Tomáš Slavíček

Kapitoly článku