UWP komponenta pro zobrazování spritů a frame animací - 3. Třída AnimationControl - hlavní komponenta

Seznam článků

3. Třída AnimationControl - hlavní komponenta

Třída AnimationControl bude komponentou, kterou bude možné umístit přímo do XAML kódu, byť bez příslušné inicializace nebude nic zobrazovat. Abychom získali všechny potřebné vlastnosti takového XAML-control prvku, můžeme ji založit na třídě některého z prvků již hotových. Je celkem jedno, který to bude, ovšem musí umožňovat nastavení pozadí typu ImageBrush, tj. pozadí definované obrázkem. Tuto vlastnost mají všechny geometrické tvary (Shape) jako je Rectangle, Ellipse, Plygon, Path apod. ve vlastnosti Fill. U kontejnerových polohovacích prvků (Panel) jako např. Grid, StackPanel, Canvas apod. se pozadí definuje ve vlastnosti Background. V tomto případě bude použita jako bázová třída Grid, nicméně stejně by vše fungovalo i s kteroukoli jinou.

public class AnimationControl : Grid
{
}

Základní metody a vlastnosti

Aby bylo možné komponentu vykládat přímo přes XAML, je třeba jí definovat bezparametrický konstruktor. V něm nastavíme bod pro transformace prvku (RenderTransformOrigin) na jeho střed (relativní souřadnice 0.5 z celkové šířky objektu horizontálně a 0.5 z celkové výšky vertikálně). Tento bod se týká transformací komponenty jako celku, nikoli transformací, které budeme provádět s obrázkem v jeho pozadí.

Ukázka relativních souřadnic

V konstruktoru také přidáme zpracování události změny velikosti prvku (SizeChanged), při které budeme muset přepočítat změnu měřítka obrázku tak, aby jeden jeho sprit přesně odpovídal novému rozměru (vždy aktuálnímu) tohoto prvku. Bude tak možné komponentě nastavit např. rozměry na Stretch či Auto a ten se tak bude automaticky přizpůsobovat změnám rozměrů ostatních prvků či celého okna aplikace. Obsluhu této události obstará metoda AnimationControl_SizeChanged, která bude implementována níže.

public AnimationControl()
{
    RenderTransformOrigin = new Point(0.5, 0.5);      // Nastavení středu otáčení a změny měřítka objektu (nikoli obrázku v pozadí) pro tyto jeho transformace
    this.SizeChanged += AnimationControl_SizeChanged; // Hlídání změn velikosti tohoto objektu (komponenty)
}

V případě, že bude scéna složená ze spritů generována ze C# kódu na pozadí, což je pravděpodobnější scénář než přímá definice v XAMLu, tak následující přetížení konstruktoru umožní definovat vše potřebné v rámci jednoho příkazu. V tomto případě konstruktoru předáme jako vstupní parametry referenci na data o obrázku se sprity (imageData) a index spritu (či 1. framu jeho animace) v tomto obrázku frameIndexStart (indexováno je od 0 do počtu spritů - 1). Další nepovinné parametry se týkají již pouze animovaného spritu a určují počet framů této animace (framesCount, 1 = sprit je statický - neanimovaný, tj. má jen jeden frame), dobu trvání animace v sekundách (animationDuration, doba jednoho prostřídání všech framů daného spritu od prvního do posledního) a má-li se animace přehrávat i zpětně (autoReverse).

Auto reverze znamená, že se nejprve v zadaném čase prostřídají všechny framy jednoho spritu od prvního do posledního a pak, místo aby se začaly přehrávat znovu do prvního, tak se (opět v témže čase daném animationDuration) přehrají v pořadí opačném, tj. od posledního k prvnímu. Následně se pak budou zase přehrávat klasicky.

Pro tuto animaci pak místo tohoto stačí toto.
Ukázka animace, kde lze použít reverzi, zdroj: http://www.netanimations.net/Swinging-moving-pendulum-and-metronome-images-and-rhythmic-beat-animations.htm Sprite sheet s framy pro animaci bez autoreverze Sprite sheet s framy pro animaci s autoreverzí
public AnimationControl(SpriteSheetData imageData, int frameIndexStart, int framesCount = 1,
    double animationDuration = 0, bool autoReverse = false)
{
    RenderTransformOrigin = new Point(0.5, 0.5); // Nastavení středu otáčení a změny měřítka objektu (nikoli obrázku v pozadí) pro tyto jeho transformace
    this.SizeChanged += AnimationControl_SizeChanged;   // Hlídání změn velikosti tohoto objektu (spritu / komponenty)
    Initialize(imageData, frameIndexStart, framesCount, animationDuration, autoReverse); // Nastavení výchozích hodnot
}

Nyní můžeme implementovat metodu pro obsluhu změny velikosti tohoto prvku. Ta v podstatě pouze zavolá jinou metodu RecalcTransormations, která bude vše vykonávat i pro další události (např. prvotní inicializace). Tu opět implementujeme až v dalším kódu.

private void AnimationControl_SizeChanged(object sender, Windows.UI.Xaml.SizeChangedEventArgs e)
{
    RecalcTransormations(); // Při změně velikosti komponenty přepočítat jeho transformace
}

Dále nedefinujeme vlastnosti pro nastavení parametrů zobrazování spritu a jeho případné animace. ImageData tedy bude uchovávat informace o celém obrázku (sprite sheetu) a rozměrech jeho spritů (viz třída SpriteSheetData), přičemž společně s jeho definicí bude zapotřebí určit i další parametry tohoto spritu, které by se individuálně měnit neměly (pro uhlídání konzistence všech těchto hodnot). Vlastnost je tedy pro okolí pouze pro čtení (set kód má private) a bude možné ji nastavit pouze přes metodu Initialize (bude implementována níže) společně s dalšími nezbytnými parametry.

Doba trvání animace (AnimationDuration) naopak být za běhu měněna může, neboť se každé přepnutí framu bude počítat individuálně.

Proměnná nextFrameSign bude určovat znaménko pro přepínání framů, tzn. buď bude +1 a index aktuálního framu se bude zvyšovat, nebo bude -1 (pouze při auto revezri) a index aktuálního framu se bude snižovat. Proměnná je soukromá (private), takže si její hodnotu bude měnit a hlídat pouze tato třída sama.

S tím souvisí i vlastnost AutoReverse, která sice může být taktéž měněna za běhu, ale musí si ohlídat právě toto znaménko přepínání framů. Jediná kombinace, která by neměla nastat totiž je, že pokud bude auto-revezre vypnuta, tak se nesmí framy přepínat v opačném pořadí, tzn. pokud se této vlastnosti bude nastavovat hodnota false, tak je třeba znaménko pro posun framů nextFrameSign nastavit na +1.

public SpriteSheetData ImageData { get; private set; } // Informace o obrázku s jednotlivými sprity/framy
public double AnimationDuration { get; set; }          // Doba trvání animace v sekundách
private short nextFrameSign = 1;                       // Směr (znaménko) posunu framů (při AutoReverse se bude střídat z +1 na -1)

private bool autoReverse;
public bool AutoReverse                                // Po dosažení posledního framu nezačínat znovu od prvního, ale přehrát nejprve animaci v opačném pořadí framů
{
    get { return autoReverse; }
    set
    {
        autoReverse = value;                           // Uložení hodnoty do datové položky
        if (!autoReverse)                              // Pokud byla právě AutoReverse vypnuta pak...
            nextFrameSign = 1;                         // ... směr animace nuceně nastavit dopředu
    }
}

Další vlastnosti se týkají vymezení spritu a jeho případné animace v rámci zdrojového obrázku. FrameIndexStart určuje index spritu (či 1. framu jeho animace) v obrázku, přičemž indexováno je od 0 do počtu spritů v obrázku - 1. FramesCount určuje počet framů animace tohoto spritu, takže hodnota 1 znamená, že sprit je statický - neanimovaný, neboli má jen jeden frame. Obě tyto hodnoty podléhají validaci správnosti a zásadně mění průběh animace, proto nemohou být individuálně měněny. Pro jejich změnu je třeba použít metodu Initilalize nebo ChangeFrameIndexes, které budou implementovány níže.

Vlastnost FrameIndexCurrent, zapouzdřující stejnojmennou proměnnou (s malým písmenem na začátku), pak určuje relativní index aktuálně zobrazeného framu daného spritu. Relativní v tom smyslu, že se nepočítá v rámci pozice spritu v celém obrázku, ale pouze ve rozsahu vymezeném indexem prvního framu a počtem framů animace pro tento sprit, tj. od 0 do FramesCount-1. V případě statického spritu bude tedy tato hodnota vždy 0. Změna hodnoty vlastnosti zároveň rovnou mění zobrazovaný výřez obrázku pomocí níže implementované metody SetFrame, takže ve svém set kódu obsahuje i příslušné kontroly této hodnoty.

public int FrameIndexStart { get; private set; } // Index prvního framu v obrázku pro tento sprit 
public int FramesCount { get; private set; }     // Počet framů náležících animaci tohoto spritu

public int frameIndexCurrent = 0;
public int FrameIndexCurrent                     // Relativní index aktuálně zobrazeného framu
{
    get { return frameIndexCurrent; }
    set
    {
        if (value >= 0 && value < FramesCount)   // Index framu musí být z vymezeného rozsahu
        {
            frameIndexCurrent = value;           // Uložení hodnoty nového indexu
            SetFrame(FrameIndexCurrent);         // Upravit transformaci posunu obrázku v pozadí na aktuální frame
        }
    }
}

Inicializace hodnot vlastností

Jak již bylo zmíněno výše, hodnoty některých vlastností nelze měnit individuálně, aby bylo možné efektivně uhlídat jejich konzistentnost (vzájemnou validitu). Pro jejich hromadné nastavení tedy budou sloužit následující dvě metody. První z nich Initialize umožňuje kompletní nastavení prvku, stejně jako parametrizovaný konstruktor této třídy, který tuto metodu používá. V případě, že byla instance této třídy vytvořena v separátním příkazu pomocí bezparametrického konstruktoru, lze tuto metodu zavolat zvlášť dodatečně, což platí i pro prvky vložené přes XAML.

Hlavním faktorem je nastavení vlastnosti ImageData, tj. obrázku se sprity, který se již jinak nastavit nedá. Dále je rovnou vymezen rozsah spritu a jeho framů (frameIndexStart a framesCount) pro animaci a popř. nepovinně i další vlastnosti této animace, které lze ale měnit i dodatečně (animationDuration a autoReverse; null signalizuje. že má ve vlastnosti zůstat ta hodnota, která tam aktuálně je). Pokud by měl být v průběhu animace změněn zdrojový obrázek (např. na jiný skin), pak lze tuto metodu zavolat i opakovaně.

public void Initialize(SpriteSheetData imageData, int frameIndexStart, int framesCount = 1, 
    double? animationDuration = null, bool? autoReverse = null)
{
    ImageData = imageData;
    if (animationDuration != null)
        AnimationDuration = (double)animationDuration;
    if (autoReverse != null)
        AutoReverse = (bool)autoReverse;
    ChangeFrameIndexes(frameIndexStart, framesCount, -2);
    SetBackground();
}

Další metoda ChangeFrameIndexes umožňuje změnit rozsah spritů (jejich indexy) i za běhu, bez nutnosti měnit zdrojový obrázek. To se může hodit např. pokud pohybující se objekt používá jinou animaci pro pohyb do různých stran, nebo během hry dochází ke změně jeho vzhledu. Metoda obsahuje validace vzájemné konzistentnosti všech vstupních parametrů i vzhledem ke zdrojovému obrázku a případné srozumitelné oznámení jejich nesouladu výjimkou. Ohlídá si i aby, index aktuálního framu byl v nově nastaveném rozsahu a pokud není, tak jej do něho vrátí. Jeho hodnota může být nastavena na novou hodnotu (>= 0), ponechána hodnota původní (-1), nebo být dokonce zcela ignorována (-2), což je např. potřeba při volání předchozí inicializační metodou, která obrázek v pozadí a jeho transformace nastaví až po zavolání této metody samostatně, takže by zatím k hodnotám těchto vlastností vůbec neměla přistupovat.

/// <summary>
/// Umožňuje změnit index a počet pro vymezení framů pro animaci tohoto spritu (třeba pro změnu vzhledu za běhu)
/// </summary>
/// <param name="frameIndexStart">Index prvního framu pro animaci</param>
/// <param name="framesCount">Počet framů pro animaci</param>
/// <param name="frameIndexCurrent">Index aktuálního framu (-1 = nechat stávající, vejde-li se do nového rozsahu, -2 = vůbec s hodnotou nemanipulovat)</param>
public void ChangeFrameIndexes(int frameIndexStart, int framesCount, int frameIndexCurrent = -1)
{
    // Změny lze provádět pouze pokud je již nastaveno ImageData
    if (ImageData == null)
        throw new Exception($"{nameof(AnimationControl)}: {nameof(ChangeFrameIndexes)} cannot be invoked before {nameof(Initialize)}");
    // Kontrola parametrů
    if (frameIndexStart < 0 || frameIndexStart >= ImageData.SpritesCount)
        throw new Exception($"{nameof(AnimationControl)}: invalid {nameof(frameIndexStart)}");
    if (framesCount <= 0 || framesCount > ImageData.SpritesCount)
        throw new Exception($"{nameof(AnimationControl)}: invalid {nameof(framesCount)}");
    if (frameIndexStart + framesCount > ImageData.SpritesCount)
        throw new Exception($"{nameof(AnimationControl)}: {nameof(frameIndexStart)} + {nameof(framesCount)} is out of range");

    // Uložení hodnot
    FrameIndexStart = frameIndexStart;
    FramesCount = framesCount;
    if (frameIndexCurrent != -2)
    {
        if (frameIndexCurrent < 0)                                     // -1 -> Nechat současný index
            frameIndexCurrent = FrameIndexCurrent;
        if (frameIndexCurrent >= 0 && frameIndexCurrent < framesCount) // Kontrola, je-li index v rozsahu
            FrameIndexCurrent = frameIndexCurrent;                     // Pokud ano, tak jej nastavit
        else
            FrameIndexCurrent = 0;                                     // Pokud ne, začne animace od začátku
    }
}

Práce s obrázkem v pozadí

Další proměnné a metody budou pracovat s obrázkem v pozadí (sprite sheetem) tak, aby zobrazovaly vždy jen jeho příslušný výřez, tj. jeden sprit/frame na daném indexu.

Soukromé proměnné traBackTrans a traBackScale budou vztaženy k obrázku v pozadí (přes jeho ImageBrush), nikoli k prvku jako celku. První z těchto transformací (traBackTrans) bude umožňovat horizontální (X) a vertikální (Y) posun obrázku v pozadí, čehož bude využito pro nastavení obrázku takovým způsobem, aby pod výřezem komponenty byl právě ten jeden konkrétní sprit/frame.

Druhá transformace (traBackScale) bude umožňovat změnu měřítka obrázku, zvlášť horizontálního (ScaleX) a vertikálního (ScaleY), přičemž např. 1 = 100% velikosti, 0.5 = 50% velikosti a 2 = 200% velikosti originálního obrázku. Této transformace bude využito pro korekci rozměrů obrázku, kdyby rozměr komponenty neodpovídal rozměrům spritu.

private ScaleTransform traBackScale;     // Transformace měřítka obrázku v pozadí
private TranslateTransform traBackTrans; // Transformace posunu obrázku v pozadí

Metoda SetBackground je volána za účelem inicializace obrázku v pozadí. Jinými slovy vezme obrázek z vlastnosti ImageData, vytvoří pozadí obrázku (Background) typu ImageBrush do něhož tento obrázek nastaví a pro toto pozadí připraví a naváže transformace traBackTrans a traBackScale pro jeho přizpůsobování, které sloučí pomocí skupiny transformací (TransformGroup). Na závěr nastaví hodnoty transformací níže implementovanou metodou RecalcTransormations tak, aby odpovídaly aktuálnímu nastavení, tj. komponenta na pozadí vyobrazovala právě jeden sprite/frame na daném indexu.

/// <summary>
/// Obrázek z ImageData nastaví jako pozadí tohoto objektu, a vytvoří mu rovnou i transformace umožňující 
/// změnu jeho velikosti a posun tak, aby ve výřezu obrázku určeným rozměry objektu byl vždy vyobrazen právě 
/// jeden frame na zvoleném indexu.
/// </summary>
private void SetBackground()
{
    var img = new ImageBrush() {                          // Vytvoření obrázkového pozadí a jeho výchozí nastavení       
        ImageSource = ImageData.Image,                    // Obrázek
        Stretch = Stretch.None,                           // Vypnutí automatického roztahování obrázku
        AlignmentX = AlignmentX.Left,                     // Horizontální zarovnání obrázku dle levého okraje
        AlignmentY = AlignmentY.Top                       // Vertikální zarovnání obrázku dle horního okraje
    };
    var tg = new TransformGroup();                        // Vytvoření skupiny transformací pro obrázek v pozadí
    traBackTrans = new TranslateTransform() {             // Vytvoření transformace pro posun obrázku
        X = 0, Y = 0
    };
    traBackScale = new ScaleTransform() {                 // Vytvoření transformace pro měřítko obrázku
        CenterX = 0, CenterY = 0, ScaleX = 1, ScaleY = 1
    };
    tg.Children.Add(traBackTrans);                        // Přidání transformace posunu do skupiny transformací obrázku  (1.)
    tg.Children.Add(traBackScale);                        // Přidání transformace měřítka do skupiny transformací obrázku (2.)
    img.Transform = tg;                                   // Přiřazení transformační skupiny obrázku v pozadí
    RecalcTransormations();                               // Přepočet hodnot obou transformací dle rozměrů obrázku a tohoto objektu
    Background = img;                                     // Obrázek pro zobrazení nastavit až po všech jeho úpravách
}

Výše již několikrát použitá metoda RecalcTransormations má za úkol nastavit obě transformace obrázku v pozadí (traBackTrans a traBackScale) tak, aby komponenta v rámci svých mezí zobrazovala právě jeden sprite/frame na aktuálním indexu. O nastavení transformace měřítka (přizpůsobení velikosti spritu rozměrům prvku) se zde stará přímo metoda sama, výpočtem poměru stran komponenty a spritu, transformaci posunu (posunutí obrázku v pozadí tak, aby byl ve výřezu prvku vidět ten správný sprit/frame) nastavuje pomocí níže implementované metody SetSprite.

/// <summary>
/// Přepočet hodnot transformací obrázku v pozadí podle rozměrů obrázku, rozměrů tohoto objektu a indexu framu, který se má zobrazovat
/// </summary>
private void RecalcTransormations()
{
    // Upravit měřítko obrázku tak, výsledný rozměr jednoho spritu/framu na obrázku v pozadí  přesně odpovídal rozměrům tohot objektu (AnimationControl)
    if (traBackScale != null)                                        // Transformace měřítka již byla vytvořena
    {
        traBackScale.ScaleX = ActualWidth / ImageData.SpriteWidth;   // Výpočet horizontálního měřítka
        traBackScale.ScaleY = ActualHeight / ImageData.SpriteHeight; // Výpočet vertikálního měřítka
    }
    // Posunout obrázek v pozadí tak, aby byl zobrazen frame (sprite) na aktuálním indexu
    SetFrame(FrameIndexCurrent);                                     // Nastavení transformace posunu na aktuální frame
}

Metoda SetSprite má tedy za úkol posunout obrázek na pozadí tak, aby ve výřezu komponenty byl právě požadovaný sprite na daném indexu (parametr index). Metoda si kontroluje, je-li vše správně nastaveno, aby nedošlo k neočekávané výjimce. Následně si pomocí metody GetSpritePosition ze třídy SpriteSheetData nechá určit souřadnice levého horního pixelu požadovaného spritu a nastaví transformaci posunu traBackTrans tak, aby bylo docíleno kýženého efektu. Přitom se předpokládá, že měřítko obrázku bylo upraveno tak, aby rozměry spritu a komponenty přesně seděly. Jelikož je transformace posunu v transformační skupině obrázku v pozadí na prvním místě, je tento posun určen v jednotkách celých pixelů dle originálního obrázku, neboť k transformaci měřítka dojde až po tomto posunu (na pořadí transformací ve skupině totiž záleží).

Parametr index je této metodě zadán relativně (0 ... FrameCount-1), takže pro jeho celkový index v obrázku je k němu přičtena ještě hodnota z vlastnosti FrameIndexStart.

/// <summary>
/// Nastaví transformaci posunu obrázku v pozadí tak, aby ve výřezu obrázku určeném rozměrem tohoto prvku (AnimationControl) 
/// byl frame na zadaném indexu (relativním)
/// </summary>
/// <param name="index">Relativní index framu (od 0 do FramesCount-1), který má být vyobrazen</param>
private void SetFrame(int index)
{
    if (traBackTrans != null && index >= 0 && FramesCount > 0 && index < FramesCount && ImageData != null) // Validace parametrů
    {
        var pos = ImageData.GetSpritePosition(index + FrameIndexStart); // Získání souřadnic pixelu v levém horním rohu požadovaného framu
        traBackTrans.X = -pos.X;  // Horizontální posun obrázku v pozadí
        traBackTrans.Y = -pos.Y;  // Vertikální posun obrázku v pozadí
    }
}

Animování obrázku

S pomocí výše uvedené části kódu třídy je již možné tuto komponentu snadno používat pro zobrazování spritů z větších obrázků (sprite-sheetů), nebo je i efektivně přepínat v nějaké smyčce a provádět tak jejich framové animace. Abychom uživateli této komponenty proces ještě více usnadnili, připravíme i metodu, která veškeré animování zařídí sama, bude-li správně volána.

Nejprve deklarujeme proměnnou animationDelay, ve které se bude počítat čas od posledního přepnutí obrázku (framu). Na začátku a po každém přepnutí framu bude tedy tato proměnná nastavena na 0 a postupně se bude zvyšovat o čas uplynulý od posledního pokusu o animaci. Čas se bude počítat v sekundách popř. v jejich zlomcích.

private double animationDelay = 0;                             // Počítadlo odpočtu pro přepnutí na další obrázek

Metoda Animate je právě ta, kterou je potřeba volat v rámci nějaké herní smyčky neustále dokola. Jejím vstupním parametrem ellapsedTime je čas vyjádřený v sekundách (např. 0.05 = 50ms), který uplynul od posledního volání této metody daného objektu. O jeho výpočet by se mělo starat hlavní vlákno herní smyčky a zde tedy bude přijímán pouze jako parametr.

Metoda následně ověří je-li co animovat, tj. je-li nastaven obrázek a rozsah indexů framů takový, že je mezi čím přepínat. Pak už jen zvyšuje počítadlo animationDelay o uplynul čas ellapsedTime a dosáhne či přesáhne-li čas vymezený pro setrvání jednoho políčka animace (frame) na "scéně" AnimationDuration, pak je počítadlo vynulováno a frame přepnut na další v pořadí níže implementovanou metodou NextFrame. Místo dělení celkové doby přehrání jedné série framů jejich počtem je zde použito rychlejší operace násobení času odpočtu jejich počtem, neboli upravená verze této nerovnice (místo "animationDelay >= AnimationDuration / FramesCount" je zde "animationDelay * FramesCount >= AnimationDuration").

public void Animate(double ellapsedTime)
{
    if (ImageData == null) return;                             // Není-li nastaven obrázek, pak není co animovat
    if (FramesCount > 1 && AnimationDuration > 0)              // Má-li sprit jen jeden frame či nulový čas animace, pak je statický (bez animace)
    {
        animationDelay += ellapsedTime;                        // Odpočet do dalšího přepnutí framu
        if (animationDelay * FramesCount >= AnimationDuration) // Odpočet vypršel...
        {
            animationDelay = 0;                                // Vynulovat počítadlo odpočtu
            NextFrame();                                       // Přepnout na další obrázek (frame) spritu
        }
    }
}

Soukromá metoda NextFrame obstarává přepnutí na další frame animovaného spritu. To, který frame bude zobrazen po tomto přepnutí je závislé na směru, kterým se aktuálně framy přehrávají/střídají/přepínají. Tento směr určuje výše deklarované proměnná nextFrameSign nabývající hodnoty +1 nebo -1. Součtem této proměnné a aktuálního indexu se určí index pro nový frame, který, pokud je v daném rozsahu, je nastaven pro zobrazení, o což se postará set kód vlastnosti FrameIndexCurrent, volající metodu SetFrame.

V případě, že by nový index framu směřoval mimo vymezený rozsah (0..FrameCount-1), tak byla zjevně přehrána již celá série framů. Ty se přehrávají ve smyčce buď stále od prvního do posledního, nebo, je-li zapnuta auto-reverze (AutoReverse), se směr střídá od prvního k poslednímu, od posledního k prvnímu (zpětně) atd. V případě auto-reverze se tedy po překročení rozsahu změní směr přehrávání (znaménko nextFrameSign) na opačný a index nového framu se vypočte znovu s tímto znaménkem. Není-li auto-reverze aktivní, nastaví se prostě FrameIndexCurrent na 0, tj. znovu na první frame animace.

private void NextFrame()
{
    if (FramesCount <= 1) return;                           // Animace probíhá až od 2 a více framů

    int newFrameIndex = FrameIndexCurrent + nextFrameSign;  // Výpočet indexu nového framu
    if (newFrameIndex < 0 || newFrameIndex >= FramesCount)  // Kontrola, je-li index nového framu mimo rozsah spritu
        if (AutoReverse)                                    // Při autoreverzi se bude animovat v opačném směru...
        {
            nextFrameSign *= -1;                            // Změna znaménka (směru) pro posun framů
            FrameIndexCurrent += nextFrameSign;             // Posun na další frame v aktuálním směru posunu
        }
        else                                                // Není zapnuta auto-reverze...
            FrameIndexCurrent = 0;                          // Začít animaci znovu od začátku
    else                                                    // Nový index je stále v rozsahu indexů framů spritu...
        FrameIndexCurrent = newFrameIndex;                  // Nastavit aktuální index na tento nový index
}

Tímto je celý kód komponenty pro zobrazování jednotlivých spritů ze sprite sheetu včetně podpory jejich do jisté míry automatizovaného střídání (animace) kompletní a lze jej již přímo používat. V další části ovšem ještě přidáme podporu pro usnadnění některých standardních animací (otáčení a zmenšování/zvětšování) bez nutnosti každou jejich fázi rozkreslovat do jednotlivých framů.

on 13 kvě 2016