UWP komponenta pro zobrazování spritů a frame animací - 5. Použití

Seznam článků

5. Použití

Komponenta pro zobrazování statických i animovaných spritů ze sprite sheet obrázku je tedy hotová a můžeme ji použít. Připomeňme jen, že ač by tato komponenta fungovala v různých typech aplikací pracujících s XAML (WPF, SIlverlight), tak v tomto případě byla cílem platforma UWP, tj. univerzální aplikace Windows 10, spustitelná od mobilů, přes PC, tablety, IoT až třeba po herní konzoli Xbox One. Následující příklad použití tedy bude zasazen do aplikace tohoto typu, tj. nového projektu "Blank App (Universal Windows)". Třídy animační komponenty (AnimationControl, SpriteSheetData a AnimationData) mohou být do projektu přidány přímo, nebo ponechány v projektu vlastním (např. typu "Class Library (Universla Windows)") s pouhou referencí na ně a možností opakovaného použití v dalších projektech.

Jelikož komponenta je určena především pro vytváření z kódu na pozadí (C#), byť určitou část definice by mohla nést i přes XAML definici, bude nám v designu (XAMLu) stránky (např. výchozí MainPage.xaml) stačit pouze pár úprav oproti výchozí definici. Je totiž třeba pojmenovat si kontejner, do kterého budeme animační komponentu vkládat. V tomto případě použijeme rovnou hlavní kontejner (Grid) stránky, který nazveme (Name) MainGrid (hlavní grid), přičemž by vůbec nevadilo použít i jiný typ kontejneru (Canvas, StackPanel, jiný Grid ...) umístěný kdekoli jinde.

Další úpravou v XAMLu bude určení události, která má spustit proces vložení prvku a zahájení jeho animace. V tomto příkladu použijeme událost stránky (Page) Loaded, kam přiřadíme (vygenerujeme) metodu kódu na pozadí Page_Loaded (stránka načtena). Opět by nevadilo vše propojit s jinou událostí, třeba po kliknutí na nějaký objekt. Je však třeba rozlišit dva úkony: vytvoření instance animační komponenty a vytvoření a spuštění herní smyčky. Zatímco animačních komponent může být vytvořeno a zobrazeno v podstatě libovolné množství, herní smyčka by měla být vytvořena a spuštěna právě jednou a obsluhovat všechny tyto komponenty současně. Událost stránky Loaded je v životě stránky spuštěna právě jednou, proto se na vytvoření herní smyčky hodí. Pokud má ale první animace vzniknout až později (popř. třeba i vůbec), může se její vytvoření a spuštění například zapouzdřit v dříve již několikrát použitém návrhovém vzoru Singleton.

<Page ... Loaded="Page_Loaded">
    <Grid Name="MainGrid" ...>
    </Grid>
</Page>

Zbytek závisí již pouze na kódu na pozadí této stránky. Nejprve si v rámci třídy stránky (MainPage v MainPage.xaml.cs) deklarujeme dvě proměnné. První (timer) bude referencí na instanci třídy DispatcherTimer, tj. třídu, která dokáže cyklicky spouštět danou událost vždy po uplynutí zadaného intervalu. Čekání na další spuštění přitom běží ve vlastním vlákně, takže nebrzdí ostatní chod aplikace. Proměnná je deklarována takto v rámci třídy, abychom k "časovači" měli přístup i později a mohli jeho běh, byť třeba jen dočasně, zastavit metodou Stop (např. při "pauznutí" hry či minimalizaci okna nebo jeho vypínání). Druhou proměnnou bude lastTime typu DateTime v níž budeme uchovávat přesný čas poslední aktualizace animací všech prvků, abychom mohli dopočítat, kolik sekund od té doby uplynulo.

DispatcherTimer timer; // Deklarace proměnné pro časovač (Timer) spouštění události po daném intervalu
DateTime lastTime;     // Datum a čas poslední aktualizace animací

Následně doplníme kód do metody Page_Loaded obsluhující událost Loaded této stránky. Předně jí musíme do definice hlavičky dopsat klíčové slovo async, které metodu označí jako asynchronně volanou a umožní nám tak v jejím kódu volat další asynchronní metody pomocí klíčového slova await, bez nutnosti kód rozdělovat do dalších metod (první by spustila asynchronní kód a druhá by přes událost čekala, až bude dokončena daná činnost). V tomto případě toho využijeme hned v prvním příkazu pro načtení obrázku ze souboru aplikace. Načítání obsahu ze souboru je netriviální operace vyžadující určitý operační čas a také přístup k paměťovému médiu, na kterém je soubor uložen, což může v závislosti na dalších procesech operačního systému chvíli trvat. Byť se obvykle jedná jen o desítky milisekund, je lepší (resp. nezbytné) tuto operaci spustit ve vlastním vlákně, aby nebrzdila zbylý chod aplikace a mezitím bylo možné např. používat její další ovládací prvky (posun okna, minimalizace, přepnutí, zavření apod.). Použití await tedy toto vše zařídí a s vykonáváním dalšího kódu metody počká, dokud operace (načtení obrázku v jiném vlákně) úspěšně neskončí a nevrátí příslušnou hodnotu.

Po načtení obrázku (image) pro něj vytvoříme instanci třídy SpriteSheetData s informacemi o spritech na něm (jejich rozměry a šířka mezery mezi nimi) do proměnné imageData. Pokud bychom vytvářeli více animačních prvků zobrazujících sprity z téhož sprite sheetu, tak by pro ně reference v imageData měla být společná, tzn. tyto dva příkazy by proběhly pouze 1x, další dva pak klidně vícekrát (třeba v cyklu). Je také možné deklaraci proměnné imageData provést mimo tuto metodu a referenci v ní obsaženou používat opakovaně i později, když by měl třeba vzniknout další animační prvek za běhu.

Dalším příkazem vytvoříme konečně instanci třídy AnimationControl, tedy animační prvek do proměnné item. Místo pozdější inicializace zde předáme všechny potřebné informace rovnou v konstruktoru, tj. referenci na informace o sprite sheetu, index počátečního spritu, počet následujících spritů vymezujících framy pro animaci, dobu trvání jednoho kola animace a chceme-li či nikoli používat automatickou reverzi animace. Důležité jsou i rozměry prvku na stránce (Width a Height), jež můžeme předat rovnou v rámci konstrukce ve složených závorkách (bez definice rozměrů by se prvek roztáhl přes celou stránku). Jakmile je prvek hotov, můžeme jej rovnou přidat na plochu, v našem případě do hlavního kontejneru stránky nazvaného MainGrid, čímž se prvek stane viditelným.

Reference na prvek (proměnná item) by klidně mohla být zase deklarována mimo metodu, aby se k ní dalo přistupovat i později. Kdyby animačních prvků bylo více, mohla by také být přidána třeba do seznamu (List<AnimationControl>), který by mapoval celou scénu. Možné je i vytvořit třídu nesoucí další dodatečné informace o každém z prvků (např. typ prvku, způsob interakce s hráčem, stav apod.) + tuto referenci na tuto animační komponentu a až instanci této třídy si ukládat do typového seznamu. Vždy záleží na konkrétní situaci a potřebách.

Tímto tedy došlo k vytvoření animační komponenty (případně více komponent, pokud druhé dva příkazy prvního bloku byly volány v cyklu, např. pro načtení zobrazení nějaké mapy). Druhý blok kódu této metody vytváří a spouští herní smyčku. Ten by tedy měl v rámci stránky proběhnout pouze jedenkrát. Tentokrát vytvoříme instanci třídy DispatcherTimer a jeho referenci uložíme do dříve deklarované proměnné timer. Dále nastavíme interval mezi jednotlivými spuštěními herní smyčky. Ten je typu TimeSpan a v tomto případě jej nastavíme pomocí vyjádření v milisekundách (těch je 1000 za sekundu). Jeho hodnota zároveň určuje FPS (frames per second) hry dle vztahu "FPS = 1s / Interval". Při 20ms je to tedy 1000/20 = 50 FPS. Vyšší hodnoty FPS nemá moc cenu nastavovat, protože vše navíc samozřejmě závisí na výpočetních možnostech hardware, přičemž UWP se jej v tomto typu aplikací snaží moc nezatěžovat a standardně omezují FPS celého okna na 60. Rychlost framové animace navíc na této hodnotě nezávisí (není-li tedy FPS příliš nízké, aby občas nějaký frame přeskočilo), protože interval mezi přepínání framů je definován v závislosti na čase jako takovém bez ohledu na FPS.

Poté timeru nastavíme obsluhu události Tick metodou Timer_Tick, jež implementuje dále. Tick je právě ta událost, kterou timer periodicky spouští po každém uplynutí intervalu, tj. v tomto případě každých 20 milisekund. Na závěr již stačí pouze tuto herní smyčku spustit metodou Start.

private async void Page_Loaded(object sender, RoutedEventArgs e)
{
    // Vytvoření animovaného prvku
    var image = await SpriteSheetData.LoadImageFromApp("ms-appx:///sprites.png"); // Načtení obrázku z PNG souboru vloženého do projektu jako Content
    var imageData = new SpriteSheetData(image, 64, 64, 1);                        // Vytvoření informací o obrázku (sprity na něm jsou 64x64 a každý má okolo 1px mezeru)
    var item = new AnimationControl(imageData, 232, 4, 0.4, true)                 // Vytvoření animovaného prvku...
        { Width = 64, Height = 64 };                                              // a nastavení rozměrů tohoto prvku
    MainGrid.Children.Add(item);                                                  // Přidání prvku do hlavního gridu

    // Vytvoření herní smyčky
    timer = new DispatcherTimer();                  // Vytvoření Timeru - spouštěče události
    timer.Interval = TimeSpan.FromMilliseconds(20); // Událost by měla být vyvolána každých 20 milisekund (50x za sekundu)
    timer.Tick += Timer_Tick;                       // Metoda, kterou tato událost spustí, bude Timer_Tick
    lastTime = DateTime.Now;                        // Nastavení času poslední aktualizace na aktuální okamžik
    timer.Start();                                  // Spuštění Timeru - první vykonání události proběhne za 20ms
}

Nyní zbývá jen implementovat metodu Timer_Tick, která bude aktualizovat animaci všech animačních prvků na scéně. Pokud jsou reference na tyto prvky uloženy v typovém seznamu, stačí projít ten a u každého zavolat metodu Animate. Je-li prvek jen jeden a jeho proměnná je deklarována v rámci třídy, pak by stačilo animovat jen tuto proměnnou (není-li null). V tomto příkladu jsme si ovšem žádný seznam nevytvářeli a proměnná prvku byla deklarována pouze v rámci metody, čili přímou referenci na ni k dispozici nemáme. Prvek jsme ovšem přidali mezi potomky (Children) kontejneru MainGrid, který nám tak na něj tuto referenci udržuje. Můžeme tedy projít veškeré prvky, které se v něm nacházejí, přičemž je jedno, je-li tam jen jeden, vícero nebo žádný. U každého prvku je ale třeba zkontrolovat, je-li typu AnimationControl a pokud ne (např. to je jiný kontejner, tlačítko apod.) tak jej nechat být. Díky Elvis operátoru "?." lze toto provést i bez ověřovací podmínky (if), protože výsledek přetypování operátorem as je buď objekt daného typu nebo null (pokud objekt tohoto typu není), což je právě ten případ, jehož další zpracování Elvis operátor zastaví. Je-li vše v pořádku, pak se provede aktualizace animace daného objektu metodou Animate, se vstupním parametrem udávajícím počet sekund (typ double - desetinné číslo, v tomto případě teoreticky 20 ms = 0,05 s) od minulé aktualizace.

Výpočet počtu sekund od minule proběhl před tím do proměnné seconds, jako rozdíl aktuálního času a času poslední aktualizace vyjádřeném v celkovém počtu sekund. Po aktualizaci všech prvků, která by mimo aktualizaci animace samozřejmě mohla spočívat i v dalších faktorech (např. posun prvku po ploše, např. na základě stisknutých kláves či dotyku na obrazovce), si už jen uložíme aktuální čas do proměnné lastTime, abychom příště znovu mohli určit počet sekund od poslední aktualizace. V případě možnosti pauzy hry je pak samozřejmě za potřebí při obnovování běhu pozastavené herní smyčky (timer.Start) také nastavit lastTime na aktuální čas (DateTime.Now), aby interval od minule neposkočil příliš o nereálně vysokou hodnotu. Její výše by se pro případ "lagnutí" aplikace mohla také kontrolovat zde v rámci výpočtu seconds. Kdyby např. překročila 1s (nebo i 0,5s), pak by byl počet seconds upraven na tuto mezní hodnotu, aby po případném dočasném zatuhnutí (např. z důvodu nutnosti obsluhy jiného procesu či aplikace) vše příliš "neposkočilo" a uživateli/hráči to tak neznemožnilo zareagovat na určitou situaci.

private void Timer_Tick(object sender, object e)
{
    double seconds = (DateTime.Now - lastTime).TotalSeconds;  // Počet sekund od poslední aktualizace animací
    foreach (var ch in MainGrid.Children)                     // Zkontrolovat všechny prvky v hlavním gridu
        (ch as AnimationControl)?.Animate(seconds);           // Je-li prvek na gridu typu AnimationControl, tak aktualizovat jeho animaci
    lastTime = DateTime.Now;                                  // Zaznamenání přesného aktuálního času
}

Tímto jsme vytvořenou komponentu i vyzkoušeli a zároveň nastínili další možnosti jejího použití. Pokud jde o použití ve hrách, bylo by možné komponentu zkombinovat např. s ovládáním her s herní smyčkou, jejichž princip byl představen v tomto video-tutoriálu.

 

 

on 13 květen 2016