2. Třída SpriteSheetData - data o zdrojovém obrázku
Pro zjednodušení budeme předpokládat, že všechny sprity v obrázku mají stejný obdélníkový (popř. čtvercový) rozměr. Pokud má být nějaký sprit animovaný, jeho jednotlivé framy následují v obrázku bezprostředně za sebou a to ve směru čtení, tzn. zleva doprava a pak po řádcích. Pokud by toto omezení v budoucnu vadilo, bylo by možné do popisu obrázku zařadit i vymezení oblasti, které se specifikace těchto vlastností týká a pro jeho další oblast zase použít parametry jiné. Pro evidování informací o jednotlivých obrázkových souborech (sprite sheet) vytvoříme třídu SpriteSheetData.
public class SpriteSheetData { }
Ta bude v první řadě obsahovat referenci na obrázek typu BitmapImage načtený do paměti a specifikace potřebné pro lokalizaci jednotlivých spritů.
public BitmapImage Image { get; private set; } // Obrázek s jednotlivými sprity (sprite sheet) public int ImageWidth { get; private set; } // Šířka obrázku v pixelech public int ImageHeight { get; private set; } // Výška obrázku v pixelech public int SpriteSpace { get; private set; } // Mezera mezi sprity (jejich ochranné orámování, které se nezobrazuje) public int SpriteWidth { get; private set; } // Šířka výřezu jednoho spritu (obrázku) public int SpriteHeight { get; private set; } // Výška výřezu jednoho spritu (obrázku) public int SpriteRows { get; private set; } // Počet spritů v jednom sloupci (počet řádků) v obrázku public int SpriteCols { get; private set; } // Počet spritů v jednom řádku (počet sloupců) v obrázku public int SpritesCount { get { return SpriteRows * SpriteCols; } } // Celkový počet spritů v celém obrázku
Hodnoty těchto vlastností jsou pro ostatní objekty pouze pro čtení (set kód je private), takže jejich hodnoty musí nastavit třída sama. To je z toho důvodu, že se v průběhu celé životnosti obrázku měnit nebudou - není k tomu důvod. Obrázek se načte a pak bude až do konce života aplikace stále stejný - stejné budou tedy i jeho parametry. Načtení hodnot vlastností tedy může obstarat přímo konstruktor této třídy, abychom se vyhnuli dalším dodatečným kontrolám, je-li to či ono již nastaveno a v souladu s tím ostatním. Tyto validace by ve smyčce, která má být vykonávána co nejrychleji, pouze spotřebovávaly výpočetní čas, takže se provedou pouze 1x na začátku, při vytváření instance třídy SpriteSheetData. V případě nesouladu některých hodnot (nulový popř. záporný rozměr obrázku, spritu či mezery, nebo nesoulad některého rozměru obrázku s násobkem rozměru spritu) tak bude vyvolána výjimka oznamující co a kde se stalo. Pro případ přejmenování některého z parametrů je použita direktiva nameof podporující refactoring v parametrizovaném textovém řetězci.
public SpriteSheetData(BitmapImage image, int spriteWidth, int spriteHeight = -1, int spriteSpace = 0) { if (spriteHeight < 0) // Pokud je parametr s hodnotou výšky spritu záporný... spriteHeight = spriteWidth; // pak šlo o zkrácenou definici čtvercového spritu (výška = šířka) // Validace vstupních parametrů if (image == null) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(image)} is null"); if (image.PixelWidth == 0 || image.PixelHeight == 0) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(image)}.{nameof(image.PixelWidth)} or {nameof(image)}.{nameof(image.PixelHeight)} is 0"); if (spriteWidth <= 0 || spriteHeight <= 0) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(spriteWidth)} or {nameof(spriteHeight)} is 0 or less"); if (spriteSpace < 0) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(spriteSpace)} is less then 0"); if (image.PixelWidth % (spriteWidth + 2 * spriteSpace) != 0) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(image)}.{nameof(image.PixelWidth)} not coresponds with {nameof(spriteWidth)} and {nameof(spriteSpace)}"); if (image.PixelHeight % (spriteHeight + 2 * spriteSpace) != 0) throw new Exception($"{nameof(SpriteSheetData)}: {nameof(image)}.{nameof(image.PixelHeight)} not coresponds with {nameof(spriteHeight)} and {nameof(spriteSpace)}"); // Uložení hodnot do vlastností Image = image; ImageWidth = image.PixelWidth; ImageHeight = image.PixelHeight; SpriteWidth = spriteWidth; SpriteHeight = spriteHeight; SpriteSpace = spriteSpace; SpriteRows = ImageHeight / (SpriteHeight + SpriteSpace); SpriteCols = ImageWidth / (SpriteWidth + SpriteSpace); }
Konstruktor tedy jako vstupní parametry vyžaduje referenci na obrázek načtený do paměti (image) a alespoň jeden rozměr spritu (popř. framu) v tomto sprite sheetu. Není-li zadána výška spritu (spriteHeight), bude místo ní dosazena jeho šířka (spriteWidth), čili pro čtvercové sprity by bylo možné zápis příkazu zkrátit, protože výchozí hodnota výšky je -1.
Parametr spriteSpace umožňuje definovat šířku (také v pixelech) orámování jednotlivých spritů/framů. Tohoto orámování, obvykle 1px průhledné barvy ze všech čtyř stran každého spritu, se používá z toho důvodu, že obzvláště při transformaci měřítka je výsledná barva každého pixelu zobrazovaného obrázku dopočítávána na základě barev pixelů okolních. To uvnitř spritu nevadí, ale právě na jeho okrajích by se tak mohl zobrazit určitý podíl pixelů z okolí za jeho hranicemi, tzn. od sousedního spritu. Proto je dobré tuto mezeru do obrázku zahrnout a zde umožnit definovat její šířku.
Hodnoty ostatních vlastností se pak rovnou zde uloží popř. dopočítají na základě těch zadaných a obrázku. Jde o rozměry obrázku (ImageWidth a ImageHeight), rozměry spritu (SpriteWidth a SpriteHeight), mezeru mezi sprity (SpriteSpace) a počet řádků a sloupců spritů v obrázku / sprite sheetu (SpriteRows a SpriteCols).
Jedinou nestatickou metodu, kterou tato třída bude disponovat je GetSpritePosition. Ta nám na základě indexu spritu/framu vrátí souřadnice pixelu (Point) jeho levého horního rohu (již uvnitř případného orámování/mezery SpriteSpace). Výřez obrázku od této souřadnice v rozměrech SpriteWidth a SpriteHeight tak bude ukazovat právě tu část obrázku na které je vyobrazen právě sprit na tomto indexu.
/// <summary> /// Vrátí souřadnice pixelu, který je levým horním rohem spritu na daném indexu /// </summary> /// <param name="index">Index framu (ze všech v celém Image), jehož pozici chceme</param> /// <returns>Souřadnice pixelu levého horního rohu spritu na daném indexu</returns> public Point GetSpritePosition(int index) { if (index < 0 || index >= SpritesCount) // Kontrola rozsahu indexu throw new Exception($"{nameof(SpriteSheetData)}.{nameof(GetSpritePosition)}: {nameof(index)} is out of range"); return new Point( (SpriteWidth + 2 * SpriteSpace) * (index % SpriteCols) + SpriteSpace, // X souřadnice (SpriteHeight + 2 * SpriteSpace) * (index / SpriteCols) + SpriteSpace // Y souřadnice ); }
Metoda LoadImageFromApp je statická a asynchronní, tzn. že její kód je prováděn v jiném vlákně, než je hlavní vlákno aplikace. To sice při správném volání této metody (s await) počká s vykonáváním dalšího kódu, než tato metoda skončí a něco vrátí, ovšem paralelně s tím mohou běžet další procesy obsluhy aplikace, která díky tomu nezamrzne a lze např. posouvat její okno na ploše, minimalizovat jej, aplikaci suspendovat nebo ji i ukončit bez chybového stavu. Tento postup je pro déle trvající operace (což jsou všechny manipulující se soubory či přistupující k síti), u zařízení pracujících na akumulátor velmi žádoucí a Microsoftem u UWP aplikací pro Store vynucovaný.
/// <summary> /// Načte obrázek ze obsahu (Content) aplikace /// </summary> /// <param name="uri">Cesta k souboru aplikace ve formátu "ms-appx:///Pictures/picture.png"</param> /// <returns>Načtený obrázek</returns> public static async Task<BitmapImage> LoadImageFromApp(string uri) { var file = await StorageFile.GetFileFromApplicationUriAsync(new Uri(uri)); // Získání informací o souboru s obrázkem var image = new BitmapImage(); // Příprava prázdného obrázku pro načtení dat using (IRandomAccessStream fileStream = await file.OpenAsync(FileAccessMode.Read)) // Otevření souboru pro čtení await image.SetSourceAsync(fileStream); // Načtení obrázku ze streamu return image; // Vrácení načteného obrázku }
Tato metoda tedy načte obrázek do třídy BitmapImage z aplikačního balíčku. Aby se do něho obrázek dostal, musí být přidán do projektu jako soubor a v jeho vlastnostech nastavena Build Action na Content a kopírování do výstupního balíčku (Copy to Output Directory) nastaveno např. na Copy if newer (kopírovat do výstupního sestavení balíčku poprvé nebo pokud byl obrázek změněn).
Adresa takovéhoto souboru (uri) je pak ve formátu odkazujícím na aplikační balíček "ms-appx:///" za čímž následuje již klasická cesta k souboru uvnitř projektu včetně všech podsložek. Jako oddělovač složek je použito obyčejné (nikoli obrácené) lomítko. Je-li tedy obrázek ve složce Assets a jmenuje se MySprites.png, pak bude cesta k němu (uri) následující: "ms-appx:///Assets/MySprites.png".
Jako formát obrázků se sprity je v době psaní tohoto článku nejlepší používat PNG. Ten je na rozdíl od např. JPG bezeztrátový, tzn. nedeformuje obsah obrázku ztrátovou kompresí, přitom ale dosti bezeztrátově komprimovaný (obsahuje-li nějaké souvislé stejnobarevné plochy). Hlavní výhodou je ale fakt, že podporuje transparentní barvu a to dokonce s alfa-kanály. Místo RGB se tedy definují 4 složky - ARGB, kde A znamená alfa - průhlednost. Ta je taktéž definována od 0 do 255 a určuje míru průhlednosti každého pixelu. Částečně průhledné pixely po okraji obrázku tak umožňují to, aby obrázek (sprit) zapadal do jakkoli barevného prostředí (pozadí) aniž by okolo něj byly nějaké zubaté (ať již bílé či černé) okraje.
PNG také podporuje různé rozsahy barevné palety, takže v případě potřeby a možnosti redukce barev, kvůli snížení bytové velikosti souboru s obrázkem (pro snížení velikosti aplikačního balíčku i jejích požadavků na paměť), je zde prostor i na tyto úpravy.