DirectX (15.)

A je tu další lekce o DirectX. Minule jsme připojili knihovnu Input.dll do našeho projektu a dnes budeme pokračovat další možná nejpodstatnější knihovnou Engine.dll. Povíme si, o co se tato knihovna bude starat a velmi brzy zjistíte, k čemu nám budou řádky napsané v minulých lekcích.

15.1. Knihovna Engine

Dříve než vytvoříme novou knihovnu bych rád ještě jednou ukázal celý projekt graficky:

 

Žluté rámečky označují knihovny, které jsme už nějakým způsobem dodělali. To ovšem nevylučuje, že je časem nebudeme dále upravovat podle potřeby. Dále si všimněte, že to co jsme do teď vymysleli a napsali bude využívat knihovna Engine, o které bude dnes řeč. Je to jakási další vrstva. Co ale bude v této knihovně? Dá se říci, že bude obsahovat zbytek funkcionality našeho menu. Jistě, že něco připíšeme i do Game, ale to hlavní bude v Engine.

Například vytvoříme třídu CSprite, která bude základní třída pro všechny ostatní objekty, které mají grafickou reprezentaci (např. tlačítka). I tlačítka ovšem budou mít svoji vlastní třídu CGButton. Systém menu by mohl mít například takovou strukturu:

Bílé linky představují vazbu gen-spec neboli rodič-potomek tj. dědičnost. Naproti tomu žluté linky představují vazbu celek-část. To v praxi znamená, že třídy CGPage obsahuje pole prvků CGItem. Je také vidět, že menu bude obsahovat dva prvky CGLabel a CGButton. Není problém si vytvořit další prvky například CGListBox nebo CGEditBox. Možná se divíte proč jsem oddělil CSprite a CGItem. Je to opět kvůli možnosti rozšíření. časem třeba budete mít prvky, které budou grafické, ale nebudou součástí žádné stránky menu. Tento objekt pak zdědíte od CSprite stejně jako jsem já zdědil CGItem. Třídu CSprite jsme již před časem vytvořili v naší prvotní DirectDraw aplikaci. Nová třída bude velmi podobná.

Začneme tím, že přidáme nový projekt. Postup je standardní. Bude se jednat o MFC projekt typu DLL. Vložte nový projekt z menu Project. Vyberte položku Add to project a New...:

Dále nastavte, že chcete rozšířenou knihovnu MFC:

Poté nastavíme Dependencies pro nový projekt:

       

Projekt Engine bude závislý na Display a Input (obr. vlevo). Zároveň musíte nastavit závislost mezi Game a Engine (obr. vpravo).

Potřebujeme, aby se knihovna Engine.dll vytvářela ve společném adresáři Debug a Release. To nastavíme na kartě Settings...:

Je také samozřejmě potřeba vložit knihovnu Common.dll jako v předchozích projektech.

TIP: Pokud se chcete podívat jaké knihovny vyžadují některé programy či jiné knihovny, vyzkoušejte aplikaci Depencency Walker, která je součást instalace Visual C++ 6.0.
Standardně ji najdete v menu Start->Programy->Microsoft Visual Studio 6.0->Microsoft Visual Studio 6.0 Tools->Depends. V programu lze otevřít libovolný program .exe nebo .dll a uvidíte na jakých knihovnách je závislý.

ClassView by po těchto úpravách mělo vypadat nějak takto:

15.2. Třída CSprite

Začneme rovnou s třídou s CSprite, protože je to základní třída pro prvky menu. Jak jsem předeslal výše, třída bude velmi podobná té, co jsme definovali v lekci 6 a 7. Ne všechny atributy bude třída CGItem využívat, ale budeme počítat s možným rozšířením, takže je tam necháme.

V uvedeném schématu knihovny jsem žlutě vyznačil další třídu, která bude úzce spolupracovat s CSprite. Opět by to šlo pro jednoduchý případ menu udělat jednodušeji, ale pokud využijete třídu i jinak, může se vám to hodit. Jedná se o třídu CSpriteState, která představuje stav objektu CSprite. V tomto stavu se sprite chová podle objektu CSpriteState, například nadefinujeme pro každý stav jinou bitmapu a kdykoliv se přepnete stav, změní se bitmapa (v našem případě toto využijeme u tlačítek, protože každé tlačítko bude mít 4 stavy - normální, stisknuté, s fokusem a nepřístupné). Každý stav bude moci animovat určitou sekvenci (bude mít definované vlastní animační rychlosti apod.). Dále budeme moci nastavit, zda-li se stav bude opakovat po přehrání celé sekvence nebo se nastaví jiný atribut objektu CSprite (například jiný stav, poloha). Všechny tyto možnosti nebudeme v případě menu plně využívat a také je samozřejmě vaše věc jak to upravíte podle vlastních potřeb.

Protože obě třídy jsou k sobě úzce vázány, popíšeme se ve společném souboru.

Podívejme se návrh třídy CSprite:

Atributy

Typ atributu Název proměnné Popis

protected:

CPoint m_ptPos Aktuální pozice spritu.
int m_iVelX Rychlost v horizontálním směru.
int m_iVelY Rychlost ve vertikálním směru.
int m_iMaxVel Maximální rychlost.
CPtrArray m_arStates Pole stavů pro tento objekt CSprite.
CSpriteState m_pCurState Ukazatel na aktuální stav.
CRect m_rcRealRect Skutečný obdélník spritu, viz. dále

private:

int m_iLastUpdate Čas poslední změny animace
int m_iCurrentPhase Aktuální fáze animace
Metody
Typ návratové hodnoty Jméno a parametry Popis
public:    
HRESULT DrawSprite(void) Nakreslí sprite na svém místě
HRESULT MoveSprite(void) Přičte k aktuální pozici hodnoty rychlosti v obou směrech
HRESULT ChangeAnimationPhase(void) Inkrementuje aktuální fázi animace a kontroluje horní mez
virtual void UpdateSprite() Obnoví sprite, postupně volá tyto metody: ChangeAnimationPhase(), MoveSprite() a DrawSprite()
void AddState(CSpriteState * pNewState) Přidá stav objektu do pole stavů spritu
BOOL DetectCollision(CRect * pCollisionRect) Vrací TRUE pokud je průnik obdélníku z parametru a reálného obdélníku spritu nenulový, jinak FALSE.
CSpriteState* GetState() Vrací aktuální stav spritu, vrací ukazatel na stav
void SetState(DWORD dwState) Nastaví aktuální stav spritu podle ID stavu
void SetVelX(int VelX) Změní rychlost v horizontálním směru
int GetVelX(void) Vrátí rychlost v horizontálním směru
void SetVelY(int VelY) Změní rychlost ve vertikálním směru
int GetVelY(void) Vrátí rychlost ve vertikálním směru
void SetMaxVel(int iMaxVel) Nastavuje maximální rychlost
int GetMaxVel() Vrací maximální rychlost
void SetPosition(CPoint ptPos) Změní pozici spritu. Tato funkce může mít dvě varianty.
CPoint* GetPosition(void) Vrátí aktuální pozici spritu.
void SetRealRect(int, int, int, int) Nastavuje reálný obdélník spritu
void SetRealRect(CRect) Nastavuje reálný obdélník spritu
void GetRealRect(CRect*) Vrací reálný obdélník na obrazovce (reálný obdélník + pozice)
void GetDestin(CRect*) Funkce vrací ukazatel na obdélník, do kterého se bude vykreslovat sprite.
void GetSource(CRect*) Narozdíl od předešlé metody, tato metoda vrací zdrojový obdélník, ze kterého se bude kreslit.

Například hodnoty rychlosti m_iVelX, m_iVelY a m_iMaxVel zde vůbec nevyužijeme. Sprite musí mít definovaný alespoň jeden stav, jinak je zcela nepoužitelný. Z objektu stavu CSpriteState čte všechny potřebné informace pro vykreslení spritu.

12.1.1. Detekce kolize

Skutečný obdélník se použije při detekci kolize dvou objektů CSprite. Většina spritu totiž bude mít průhledné okraje a tyto okraje se nesmí brát v úvahu při detekci kolize. Například se podívejme na obrázek:

Černé okraje nastavíme jako průhledné, takže vlastně nejsou součástí spritu (jako sprite bereme pouze letadlo). Kdybychom zkoumali detekci kolize dvou těchto spritů a brali bychom v úvahu obdélník spritu včetně okrajů a nikoliv reálný obdélník, funkce DetectCollision() by vrátila TRUE, už když by se oba objekty dotkly černými okraji a to je bezpochyby špatně. Nastavíme proto skutečný obdélník spritu, který můžete vidět na dalším obrázku:

Ani tento obdélník není úplně přesný a u složitějších objektů si moc nepomůžeme. Můžete tedy definovat složitější strukturu pro popsání skutečného tvaru spritu. V MFC existuje třída CRgn pomocí které lze vytvořit jakýsi region zadaný například několika body. Skutečný region pro výše uvedený sprite by mohl vypadat následovně:

U grafického menu budeme také využívat detekci kolize. Například když uživatel najede kurzorem na tlačítko, tlačítko dostane fokus apod. Budeme ale uvažovat naprosto obdélníkové tlačítka, takže skutečný obdélník bude stejný jako obdélník spritu. Pokud byste ovšem použili například kruhové prvky, už byste museli definovat skutečný obdélník složitěji (případně pomocí regionů).

12.1.2. CSpriteState

Nyní si proberme třídu CSpriteState. Objekt tohoto typu bude uchovávat vlastnosti spritu (například. počet animačních sekvencí nebo rychlost animace). Pokaždé, když metodou SetState() nastavíte stav, změní se chování a třeba i vzhled spritu. Už jsem psal, že každé tlačítko bude mít 4 stavy. Každý tento stav bude definován pomocí stavu spritu. Podrobněji si o tom povíme, až budeme deklarovat třídu CGButton.

Návrh třídy CSpriteButton by mohl vypadat následovně:

Atributy

Typ atributu Název proměnné Popis

private:

DWORD m_dwID ID objektu. Musí být unikátní vůči ostatním stavům jednoho spritu.
int m_iAnimationSpeed Rychlost animace v milisekundách.
int m_iPhasesCount Počet animačních kroků.
CSurface m_surSource Zdrojový povrch pro tento stav. Pokud bude nastaven, sprite se bude vykreslovat z tohoto povrchu.
DWORD m_dwDoAfterAnimationEnds V této proměnné budou uloženy informace o tom, co se má stát, když stav skončí tj. když se přehraje celá sekvence.
CPoint m_ptPoint Tato proměnná označuje místo, kam se přesune sprite po skončení sekvence.
DWORD m_dwState Dále můžeme využít tuto proměnnou k tomu, abychom nastavili jiný stav spritu po skončení sekvence.
int m_iWidth Šířka jednoho políčka spritu. Vypočteme při inicializace z šířky celého povrchu a počtu animačních kroků.
int m_iHeight Výška jednoho políčka je rovna výšce povrchu, protože jednotlivé animace jsou v bitmapě poskládány horizontálně.
Metody
Typ návratové hodnoty Jméno a parametry Popis
public:    
void CreateState(DWORD, LPCSTR, int, int ) Inicializační metoda objektu, která definuje stav spritu. Posléze lze využít ukazatel tohoto objektu jako parametr metody AddState() třídy CSprite. První parametr představuje ID stavu, dále je tu řetězec povrchu stavu, pak máme animační rychlost a nakonec počet sekvencí. Jedná-li se o statický sprite (statický stav), poslední dvě hodnoty necháme implicitně.
void SetDoAfterAnimationEnds(DWORD, DWORD, int, int) Nastavuje vlastnosti spritu, které se nastaví po odeznění animace. První parametr určuje jakou vlastnost nastavujeme, další parametry jsou konkrétní vlastnosti (v našem případě je to pozice a stav objektu).
DWORD GetID() Vrací ID objektu stavu.
int GetAnimationSpeed() Vrací animační rychlost.
int GetPhasesCount() Vrací počet animačních kroků.
CSurface* GetSourceSurface() Vrací ukazatel na zdrojový povrch.
DWORD GetDoAfterAnimationEnds() Vrací hodnotu, která určuje, co se bude dít po skončení animace.
CPoint GetPosAF() Vrací pozici spritu po skončení sekvence.
DWORD GetStateAF() Vrací stav spritu po skončení sekvence.
int GetHeight() Vrací výšku jednoho políčka spritu.
int GetWidth() Vrací šířku políčka spritu.

Metody probereme podrobněji v další části.

15.2. Nové třídy

Nejprve se ale podívejme na deklaraci obou tříd. Už jsem zmínil, že třídy spolu velice úzce spolupracují a proto je vložíme do jednoho souboru. Vložme nejprve třídu CSprite a poté k ní přidáme třídu CSpriteState. Na následujícím obrázku vidíte, jak vyplníme dialog pro přidání nové třídy:

Poté do toho samého souboru vložíme třídu CSpriteState:

Stiskem tlačítka Change... vyvoláte následující dialog, kde nastavíte hlavičkové a implementační soubory pro třídu:

V ClassView se vám tedy objeví dvě nové třídy, ale ve FileView jen soubory Sprite.h a Sprite.cpp.

V této lekci ještě stihneme deklarovat a definovat třídy CSprite a CSpriteState. Podívejme se tedy na deklarace tříd:

#define DAS_NONE 0x0001
#define DAS_POSITION 0x0002
#define DAS_STATE 0x0004

class AFX_EXT_CLASS CSpriteState
{
    DWORD m_dwID;
    int m_iAnimationSpeed;
    int m_iPhasesCount;
    CSurface m_surSource;

    // Tile dim
    int m_iWidth;
    int m_iHeight;

    // What to do after animation ends
    DWORD m_dwDoAfterAnimationEnds;
    CPoint m_ptPoint;
    DWORD m_dwState;

public:
    void CreateState(DWORD dwID, LPCSTR szSurface, int iAnimationSpeed = 0, int iPhases = 1);
    void SetDoAfterAnimationEnds(DWORD dwWhatToDo, DWORD dwState, int x, int y);

    //
    // Inline
    DWORD GetID() {return m_dwID;}
    int GetAnimationSpeed() {return m_iAnimationSpeed;}
    int GetPhasesCount() {return m_iPhasesCount;}
    CSurface* GetSourceSurface() {return &m_surSource;}
    DWORD GetDoAfterAnimationEnds() {return m_dwDoAfterAnimationEnds;}
    CPoint* GetPosAF() {return &m_ptPoint;}
    DWORD GetStateAF() {return m_dwState;}
    int GetHeight() {return m_iHeight;}
    int GetWidth() {return m_iWidth;}

public:
    CSpriteState();
    ~CSpriteState();

};

class AFX_EXT_CLASS CSprite
{
protected:
    CPoint m_ptPos;
    int m_iVelX;
    int m_iVelY;
    int m_iMaxVel;
    CPtrArray m_arStates;
    CSpriteState* m_pCurState;
    CRect m_rcRealRect;

private:
    int m_iLastUpdate;
    int m_iCurrentPhase;

public:
    HRESULT DrawSprite(void);
    HRESULT MoveSprite(void);
    HRESULT ChangeAnimationPhase(void);

    void AddState(CSpriteState * pNewState);
    BOOL DetectCollision(CRect * pCollisionRect);
    //
    // State depent methods
    void SetState(DWORD dwState);
    void GetDestin(CRect* rcDestin);
    void GetSource(CRect* rcSource);
    void SetRealRect(int iLeft, int iTop, int iRight, int iBottom);
    void SetRealRect(CRect * pRealRect);
    void GetRealRect(CRect* pRealRect);
    //
    // Inline
    CSpriteState* GetState() {return m_pCurState;}
    void SetVelX(int VelX) {m_iVelX = VelX;}
    int GetVelX(void) {return m_iVelX;}
    void SetVelY(int VelY) {m_iVelY = VelY;}
    int GetVelY(void) {return m_iVelY;}
    void SetMaxVel(int iMaxVel) {m_iMaxVel = iMaxVel;}
    int GetMaxVel() {return m_iMaxVel;}
    void SetPosition(CPoint ptPos) {m_ptPos = ptPos;}
    CPoint* GetPosition(void) {return &m_ptPos;}
    //
    // Virtual
    virtual void UpdateSprite();

public:
    CSprite();
    ~CSprite();

};

Na začátku definuji symbolické konstanty pro metodu SetDoAfterAnimationEnds() pomocí nichž říkáte, který parametr je platný. Kombinaci těchto hodnot také vrací metoda GetDoAfterAnimationEnds(). Dále si všimněte makra AFX_EXT_CLASS, které exportuje celou třídu z knihovny.

Dále si proberme implementaci obou tříd. Začneme u třídy CSpriteState, protože CSprite je na ní závislá.

Jako první tu máme konstruktor a destruktor třídy. Zde je důležité inicializovat některé atributy, abychom zajistili správnou funkci programu:

  1. Je třeba zadat nulové ID, protože podle tohoto ID zjistíme, že objekt nebyl inicializován.

  2. Dále je potřeba vynulovat pozici a stav, do kterého se sprite dostane po skončení aktuálního stavu.

  3. Nakonec je zde nulování proměnné m_dwDoAfterAnimationEnds, což zajistí, že po skončení aktuální animace nenastane žádná změna ve spritu a animace začne znovu.

Naopak při uvolňování objektu bychom také měli uvolnit povrch stavu, i když to není nutnost (objekt CSurface se automaticky uvolňuje při destrukci).

CSpriteState::CSpriteState()
{
    m_dwID = 0;
    m_dwState = 0;
    m_ptPoint = CPoint(0, 0);
    m_dwDoAfterAnimationEnds = 0;
}

CSpriteState::~CSpriteState()
{
   
//
    // Release surface

    if(m_dwID != 0) {
        m_surSource.Release();
    }
}

Třída CSpriteState obsahuje pouze dvě metody. Za prvé je to metoda CreateState(), která inicializuje objekt. Prvním parametrem určíte ID objektu, které musí být unikátní vůči ostatním stavům daného objektu CSprite. Další parametr je ukazatel na řetězec, ve kterém je uložena cesta k bitmapě. Z této bitmapy se bude vykreslovat, pokud bude nastaven tento stav. Další dva parametry se týkají pouze animovaných stavů. Určují rychlost animace a počet animačních kroků.

void CSpriteState::CreateState(DWORD dwID, LPCSTR szSurface, int iAnimationSpeed, int iPhases)
{
    if(m_dwID == 0) {
        // Fill structure
        m_dwID = dwID;
        m_surSource.Create(szSurface);
        m_iAnimationSpeed = iAnimationSpeed;
        m_iPhasesCount = iPhases;
 
       //
        // Compute dim of one tile
 
      if(iPhases != 0) {
            m_iWidth = int(m_surSource.Width() / iPhases);
        }
        else {
            m_iWidth = m_surSource.Width();
        }
    
   // Height is same as surface
        m_iHeight = m_surSource.Height();
    }
    else {
        DXTRACE("State is already initialized.");
    }
}

Metoda je velice jednoduchá. Pouze inicializuje některé atributy třídy. Všimněte si testování nulového ID a vzpomeňte si na konstruktor třídy. Výpočet šířky jednoho políčka u animovaných stavů se provádí velice podobně jako tomu bylo u kurzoru.

Druhá a poslední metoda nastavuje chování po skončení animace. Prvním parametrem určíme, co se má dělat. Může to být kombinace hodnot DAS_POSITION a DAS_STATE. První z nich určí, že pozice uložená ve třetím a čtvrtém parametru jsou platné. Naopak hodnota DAS_STATE říká, že je platný druhý parametr. Uživatel volá tuto funkci pouze pokud chce nastavit chování spritu po skončení animace. Pokud metodu vůbec nezavolá, vznikne nekonečný stav, který se opakuje do té doby, dokud uživatel nenastaví jiný stav explicitně.


void CSpriteState::SetDoAfterAnimationEnds(DWORD dwWhatToDo, DWORD dwState, int x, int y)
{
    m_dwDoAfterAnimationEnds = dwWhatToDo;
    m_ptPoint = CPoint(x, y);
    m_dwState = dwState;
}

Nyní se podívejme na implementaci třídy CSprite, která bude o něco zajímavější a složitější.

Opět se nejprve podíváme na konstruktor a destruktor. Už jsme si říkali výše, že objekt spritu je nepoužitelný pokud nemá nadefinovaný alespoň jeden stav. Všimněte si, že v konstruktoru nastavujeme ukazatel na aktuální stav na NULL. Tím zaručíme nesprávnému použití objektu CSprite (většina metod testuje tento ukazatel). Implicitně také nemá sprite žádný pohyb, takže všechny rychlosti jsou vynulovány. Pro jistotu nulujeme i pozici spritu. Kdyby totiž uživatel zapomněl tento atribut nastavit, sprite by se vykreslil kdesi v nekonečnu. Dále je velice důležité vynulovat aktuální fázi. Zdrojový obdélník se odvozuje právě od aktuální fáze a kdybychom nechali původní hodnotu, opět by jsme vykreslovali odněkud z nekonečna a program by jisto jistě spadl. Dále nulujeme hodnotu doby od poslední změny animačního kroku. To není zase tak důležité, ale vyvarujeme se neidentifikovatelnému chování na začátku programu. Nakonec inicializujeme skutečný obdélník spritu. Hodnotu -1 na prvním místě si vysvětlíme o trochu později.


CSprite::CSprite()
{
    m_pCurState = NULL;
    m_iVelX = 0;
    m_iVelY = 0;
    m_ptPos = CPoint(0, 0);
    m_iCurrentPhase = 0;
    m_iLastUpdate = 0;
    m_rcRealRect = CRect(-1, 0, 0, 0);
}

CSprite::~CSprite()
{
    m_pCurState = NULL;
}

Pokračujme dále a rozeberme metodu DrawSprite(), která je pro sprite možná nejdůležitější a měla by být co nejjednodušší:

HRESULT CSprite::DrawSprite(void)
{
    DWORD dwRet = S_FALSE;
    CRect rcDestin, rcSource;
   
//
    // State must be selected

    if(m_pCurState) {
       
//
        // Get rectangles

        GetDestin(&rcDestin);
        GetSource(&rcSource);
       
//
        // Draw sprite

        dwRet = disBlt(rcDestin, rcSource, m_pCurState->GetSourceSurface());
    }
    return dwRet;
}

Za prvé otestujeme hodnotu m_pCurState, protože musí být nastaven nějaký stav. Poté získáme zdrojový a cílový obdélník spritu pomocí metod GetSource() a GetDestin() (viz. dále). Nakonec zavoláme známou funkci disBlt() a sprite vykreslíme. Všimněte si použití objektu CSpriteState.
 

Další velice jednoduchá metoda je MoveSprite():

HRESULT CSprite::MoveSprite(void)
{
    DWORD dwRet = S_OK;
  
 //
    // Standard sprite movement

    m_ptPos += CPoint(m_iVelX, m_iVelY);
   
//
    return dwRet;
}

Metoda jen posune sprite podle jeho rychlosti v obou směrech x a y. Metoda vrací pokaždé hodnotu S_OK (0).
 

Konečně je tu trochu zajímavější metoda, která se stará o změnu animačního kroku.

HRESULT CSprite::ChangeAnimationPhase(void)
{
    DWORD dwRet = S_FALSE;
  
 //
    // State must be selected

    if(m_pCurState) {
       
//
        // Get Current phase that will be changed

        int iPhase = m_iCurrentPhase;
       
//
        // Get time from last update and from windows start

        int newTime = GetTickCount();
        int oldTime = m_iLastUpdate;
       
//
        // If is time to update, increment phase

        if((newTime - oldTime) > m_pCurState->GetAnimationSpeed()) {
           
//
            // Increment phase

            iPhase++;
           
//
            // If phase is maximum, reset it and do after segment sequention

            if(iPhase >= m_pCurState->GetPhasesCount() - 1) {

                iPhase = 0;

                // Set pos or state after animation ends
                if(m_pCurState->GetDoAfterAnimationEnds() & DAS_POSITION) {
                    m_ptPos = *m_pCurState->GetPosAF();
                }
                if(m_pCurState->GetDoAfterAnimationEnds() & DAS_STATE) {
                    SetState(m_pCurState->GetStateAF());
                }
            }
           
//
            // Change phase

            m_iCurrentPhase = iPhase;
           
//
            // Remember time of this update

            m_iLastUpdate = newTime;
        }
    }

    return dwRet;
}

V naší první aplikaci s DirectDraw jsme měli podobnou metodu, vzpomínáte? Dnes ji malinko upravíme. Na začátku opět testujeme hodnotu m_pCurState, protože metoda pracuje s objektem stavu. Dále si uložíme aktuální fázi do pomocné proměnné (to je v celku zbytečné). Poté získáme aktuální čas a čas, který uběhl od poslední změny animace. Tyto dva časy od sebe odečteme a zkoumáme, zda-li jejich rozdíl je větší nebo roven animační rychlosti. Pokud ano, je čas posunout animaci vpřed. Pokud po této úpravě je aktuální fáze větší než celkový počet fází, je sekvence u konce a je potřeba fázi vynulovat, případně nastavit některé vlastnosti spritu. K tomu využijeme objekt stavu, protože ten "ví", co se má dělat po skončení animace (v případě nekonečného stavu se neděje nic, jen se vynuluje aktuální fáze). Nakonec zpětně upravíme atribut třídy a uložíme stav, kdy se udála změna animace. Je to snadné, že?

Dále je tu velice jednoduchá funkce, která jen zkracuje zápis při zpracování spritu. Postupně totiž volá metody ChangeAnimationPhase(), MoveSprite() a DrawSprite().

void CSprite::UpdateSprite()
{
    ChangeAnimationPhase();
    MoveSprite();
    DrawSprite();
}

Velmi důležitá metoda je AddState(). Je to vlastně jediný způsob jak přidat předem vytvořený stav objektu CSprite. Metoda má jediný parametr a to je ukazatel na objekt CSpriteState:

void CSprite::AddState(CSpriteState * pNewState)
{
    CSpriteState *pState;
   
//
    // Check input pointer

    if(!pNewState) {
        DXTRACE("Invalid state poitner has been passed.");
        return;
    }
   
//
    // Check if the state is not alredy in states array

    for(int i = 0; i < m_arStates.GetSize(); i++) {

        pState = (CSpriteState*) m_arStates[i];

        if(pState->GetID() == pNewState->GetID()) {
          
 //
            // new state is found in states array

            DXTRACE("State with id: %d is alredy added to this sprite.", pNewState->GetID());
           
// Exit
            return;
        }
    }
  
 //
    // Else add this state

    m_arStates.Add(pNewState);
   
//
    // If not is set, set new state as current

    if(!m_pCurState) {
        SetState(pNewState->GetID());
    }
}

Nejprve zkontrolujeme platnost vstupního ukazatele. Poté projdeme celé pole stavů a zjistíme, zda-li již není vložen stav se stejným ID. Pokud všechny tyto kontroly projdou bez problémů, jednoduše vložíme ukazatel stavu do pole. Nakonec testujeme hodnotu aktuálního stavu a pokud je stále NULL, nastavíme právě přidaný stav. Uživatel tak nemusí explicitně volat metodu SetState(), aby nastavil první stav.

Následující metoda slouží k explicitnímu nastavení stavu spritu. Přijímá jeden parametr a to ID stavu, který chceme nastavit.

void CSprite::SetState(DWORD dwState)
{
    CSpriteState *pState;
 
  //
    // Check if the state is in states array

    for(int i = 0; i < m_arStates.GetSize(); i++) {

        pState = (CSpriteState*) m_arStates[i];

        if(pState->GetID() == dwState) {
           
//
            // State has been found, set it

            m_pCurState = pState;
            return;
        }
    }
   
// If state is not valid, set NULL as current state
    m_pCurState = NULL;
}

Metoda je triviální. Opět projdeme celé pole stavů a hledáme požadovaný stav. Pokud ho najdeme, nastavíme ho jako aktuální stav jinak se ukazatel nastavíme na NULL.

Jak už bylo řečeno výše, metoda DetectCollision() vrací TRUE pokud se skutečný obdélník spritu a obdélník určený parametrem metody překrývají, jiná vrátí FALSE.

BOOL CSprite::DetectCollision(CRect * pCollisionRect)
{
    CRect FirstRect;
   
// Get real rectangle of this sprite
    GetRealRect(&FirstRect);
   
// Check intersection both of rectangles
    return FirstRect.IntersectRect(&FirstRect , pCollisionRect);
}

Použijeme k tomu metodu IntersectRect(), která pracuje přesně tak jako metoda DetectCollision().

Následující metody slouží funkci DrawSprite(). První z nich vrací cílový obdélník tj. určuje místo, kam se bude kreslit. Opět samozřejmě otestuje aktuální stav a poté přímo naplní vstupní obdélník daty. Horní levý roh je určený pozicí spritu a dolní levý roh je posunutý o šířku a výšku jednoho políčka. Pokud je něco v nepořádku, metoda naplní obdélník hodnotami -1, takže můžeme testovat správnost metody.

void CSprite::GetDestin(CRect* rcDestin)
{
    if(m_pCurState || rcDestin) {

        *rcDestin = CRect(  m_ptPos.x, m_ptPos.y,
                            m_ptPos.x + m_pCurState->GetWidth(),
                            m_ptPos.y + m_pCurState->GetHeight() );
    }
    else {
        *rcDestin = CRect(-1, -1, -1, -1);
    }
}


Druhá metoda vrací zdrojový obdélník pro vykreslení tj. určuje místo odkud se bude vykreslovat. Po otestování aktuálního stav inicializujeme vstupní obdélník. Pozice na ose x je závislá na aktuální fázi animace. Pozice na ose y je konstantní. Princip obou metod jsme již mnohokrát rozebírali.

void CSprite::GetSource(CRect* rcSource)
{
    if(m_pCurState || rcSource) {

        *rcSource = CRect(  m_iCurrentPhase * m_pCurState->GetWidth(),
                            0,
                            (m_iCurrentPhase + 1) * m_pCurState->GetWidth(),
                            m_pCurState->GetHeight() );
    }
    else {
        *rcSource = CRect(-1, -1, -1, -1);
    }
}

Poslední tří metody se zabývají skutečným obdélník spritu. První z těchto metod jsou vlastně standardní SET metody, které inicializují skutečný obdélník buď pomocí čtyř celočíselných hodnot nebo pomocí objektu CRect.

void CSprite::SetRealRect(int iLeft, int iTop, int iRight, int iBottom)
{
    m_rcRealRect = CRect(iLeft, iTop, iRight, iBottom);
}

void CSprite::SetRealRect(CRect * pRealRect)
{
    m_rcRealRect = *pRealRect;
}

Naopak metoda GetRealRect() je o trochu složitější. Abychom nemuseli explicitně volat metody SetRealRect(), je zařízeno, že po prvním volání metody GetRealRect() je tento obdélník nastaven standardně tj. na obdélník spritu. První volání poznáme podle -1, kterou jsme nastavili v konstruktoru. Poté již metoda pokračuje normálně a vrací skutečný obdélník, který je posunutý do místa spritu.

void CSprite::GetRealRect(CRect* pRealRect)
{
   
// Check validity of real rect
    if(m_rcRealRect.left == -1 && m_pCurState) {
       
//
        // Init standard real rect from size of tile

        m_rcRealRect = CRect(0, 0, m_pCurState->GetWidth(), m_pCurState->GetHeight());
    }
    *pRealRect = m_rcRealRect + m_ptPos;
}

V dnešním příkladu tedy najdete novou knihovnu Engine.dll, která již obsahuje třídy CSprite a CSpriteState. Příklad si můžete stáhnout v sekci Downloads.


15.3. Závěr

Příště se podíváme na ostatní třídy našeho grafického menu CGMenu atd. Kód z dnešní lekce si můžete libovolně upravit podle vlastních potřeb a můžete ho využít i v jiných grafických aplikacích.  Právě proto je důležité psát obecné třídy, které postupně budete specializovat ve třídách odvozených.

Doufám, že se vám dnešní lekce líbila a těším se příště nashledanou.

Jiří Formánek