Grafické aplikace ve Visual C++ (7.)


Na konci minulé lekce, jsem vám slíbil, že si této lekci vytvoříme třídu CSprite, která bude představovat pohybující se obrázky, kterým říkáme sprity. Že nevíte co jsou to sprity? Právě od toho je tu dnešní lekce.  Opět je k dispozici funkční příklad i zdrojový kód, který zkompilujete pod VC++ s nainstalovaným DirectX SDK 8.0.

7.1 Co je to sprite?

Možná je to první otázka, která vám jako první vyvstane v mysli, ale spíš bych řekl, že pokud se již nějakou dobu věnujete grafice, určitě víte co je sprite.

Sprite z anglického překladu znamená duch nebo přízrak. V terminologii programování (konkrétně programování grafických aplikací) je sprite jakýkoliv pohybující se nebo statický "obrázek". Vezmeme si příklad z jakékoliv netextové hry. Veškerým grafickým objektů jako například grafická tlačítka, budovy, jednotky, stromy, ale i texty na obrazovce, můžeme říkat sprity.

7.2 Typy spritů

Sprity jdou dále rozdělit na statické (statické tlačítko) a animované (animovaná postava). Dále na sprity, které jsou stále na stejném místě obrazovky a na sprity, které se pohybují během běhu programu. Pří dalším, ještě podrobnějším dělení, můžeme vytvořit sprity, které ovládá umělá inteligence, a které ovládá uživatel-hráč.

7.3 Třída CSprite

Základem aplikace, která chce používat sprity je třída CSprite, která uchovává data potřebná k vykreslení spritu, k posunutí spritu a ke změně animační fáze spritu. Tyto tři operace budou zajišťovat tři členské funkce: DrawSprite(), MoveSprite() a ChangeAnimationPhase().
V datové části třídy bude určitě poloha spritu na obrazovce. Dále by měl mít sprite, který se má pohybovat po obrazovce, aktuální rychlost ve vertikálním a horizontálním směru. S těmito dvěma atributy se pojí maximální rychlost, které může sprite dosáhnout. Může zde být aktuální fáze animace, celkový počet animačních kroku a rychlost animace. Poslední dva atributy mohou být platné jen pokud se jedná o animační sprite. Navíc si můžete přidat aktuální stav spritu, podle kterého určíte, co má ten který sprite zrovna dělat.

Když to celé dáme dohromady, vznikne takovýto návrh třídy CSprite:
 

Atributy

 

 

Typ atributu

Název proměnné

Popis

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

int

m_iCurrentPhase

Aktuální fáze animace

int

m_iPhasesCount

Počet animačních kroků

int

m_iAnimationSpeed

Rychlost animace

DWORD

m_dwLastUpdate

Čas poslední změny animace

DWORD

m_dwState

Aktuální stav spritu

int

m_iWidth

Šířka jednoho animovaného políčka v pixelech

int

m_iHeight

Výška jednoho animovaného políčka v pixelech

CDisplay*

m_pDisplay

Ukazatel na náš objekt CDisplay, který jsme vytvořili minule

CSurface

m_surSpriteSurface

Objekt zdrojového povrchu pro sprite

Metody

 

 

Typ návratové hodnoty

Jméno a parametry

Popis

HRESULT

CreateStaticSprite(LPCSTR szSpriteBitmap, CPoint ptInitPos)

Inicializuje objekt statického spritu

HRESULT

CreateAnimatedSprite(LPCSTR szSpriteBitmap, CPoint ptInitPos)

Inicializuje objekt animovaného spritu

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

DWORD

GetState()

Vrací aktuální stav spritu

void

GetState(DWORD dwState)

Nastaví aktuální stav spritu

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

SetPosition(CPoint ptPos)

Změní pozici spritu. Tato funkce může mít dvě varianty.

CPoint

GetPosition(void)

Vrátí aktuální pozici spritu.

Nyní můžeme přistoupit k deklaraci třídy:


class CSprite
{
    CPoint m_ptPos;
    int m_iVelX;
    int m_iVelY;
    int m_iMaxVel;
    int m_iCurrentPhase;
    int m_iPhasesCount;
    int m_iAnimationSpeed;
    DWORD m_dwLastUpdate;
    DWORD m_dwState;
    int m_iWidth;
    int m_iHeight;

    CDisplay *m_pDisplay;
    CSurface *m_psurSpriteSurface;

public:
    HRESULT CreateStaticSprite(LPSTR szSpriteBitmap, CPoint ptInitPos);
    HRESULT CreateAnimatedSprite(LPSTR szSpriteBitmap, CPoint ptInitPos, int iPhasesCount, int iAnimationSpeed);
    HRESULT DrawSprite();
    HRESULT MoveSprite();
    HRESULT ChangeAnimationPhase();
    //
    // Inline functions

    DWORD GetState() {return m_dwState;}
    void SetState(DWORD dwState) {m_dwState = dwState;}
    void SetVelX(int VelX) {m_iVelX = VelX;}
    void SetVelY(int VelY) {m_iVelY = VelY;}
    void SetPosition(CPoint ptPos) {m_ptPos = ptPos;}
    void SetPosition(int x, int y) {m_ptPos = CPoint(x, y);}
    CPoint GetPosition() {return m_ptPos;}
    int GetVelX() {return m_iVelX;}

public:
    CSprite();
    ~CSprite();
};

Vidíte, že stačí implementovat pouze 5 metod, protože ostatní jsou inline funkce. Pusťme se tedy do nich.

1) CreateStaticSprite()

HRESULT CSprite::CreateStaticSprite(LPSTR szSpriteBitmap, CPoint ptInitPos)
{
    DWORD dwRet = ERROR_SUCCESS;
    DDSURFACEDESC2 dsdesc;
    dsdesc.dwSize = sizeof(DDSURFACEDESC2);
    //
    // Set position

    m_ptPos = ptInitPos;
    //
    // Create surface for sprite

    dwRet = m_pDisplay->CreateSurfaceFromBitmap(&m_psurSpriteSurface, szSpriteBitmap, 0, 0);
    if(dwRet != DD_OK) {
        TRACE("Cannot create sprite surface due %d\n", dwRet);
        return dwRet;
    }
    //
    // Set color key on surface. Black transparent color.

    dwRet = m_psurSpriteSurface->SetColorKey(0);
    if(dwRet != DD_OK) {
        TRACE("Cannot set color key on surface due %d\n", dwRet);
        return dwRet;
    }
    //
    // Get width and height of created surface

    dwRet = m_psurSpriteSurface->GetDDrawSurface()->GetSurfaceDesc(&dsdesc);
    if(dwRet != DD_OK) {
        TRACE("Cannot get surface info due %d\n", dwRet);
        return dwRet;
    }
    //
    // Set width and height members of sprite

    m_iWidth = dsdesc.dwWidth / m_iPhasesCount;
    m_iHeight = dsdesc.dwHeight;

    return dwRet;
}

Funkce přijímá dva parametry a to sice ukazatel na řetězec, ve kterém je uložena cela cesta k bitmapě spritu a počáteční poloha spritu. Tuto počáteční hodnotu ihned uložíme do členské proměnné třídy.

V dalším kroku vytvoříme povrch naplněný bitmapou. K tomu použijeme odkaz na třídu CDisplay, který jsme inicializovali v konstruktoru. Funkci CreateSurfaceFromBitmap() jsme probírali minule takže jen zkráceně: funkce má tři parametry: ukazatel na adresu budoucího povrchu, řetězec jména bitmapy a požadovanou výšku a šířku.

V dalším kroku nastavíme Colorkey pro povrch. Nastavíme natvrdo černou barvu jako průhlednou, ale můžete tento parametr udělat variabilní (většinou totiž pracujete s černým pozadím).

Dále potřebujeme zjistit šířku a výšku jednoho políčka případně šířku a výšku spritu, to vlastně znamená zjistit velikost vytvořeného povrchu (velikost je stejná jako bitmapa). Parametry povrchu zjistíme funkcí GetSurfaceDesc() rozhraní IDirectDrawSurface7. My ovšem máme ukazatel na CSurface a ne na IDirectDrawSurface7. CSurface ovšem obsahuje funkci, která vrací příslušný ukazatel, pomocí kterého můžeme zavolat zmiňovanou funkci. Funkce GetSurfaceDesc() přijímá ukazatel na strukturu DDSURFACEDESC2. U této struktury je potřeba nejdříve inicializovat velikost, to znamená atribut dwSize. Nakonec vezmeme potřebnou šířku a výšku. Všimněte si, že šířka je dělená počtem snímků animace (u statického spritu je počet snímků 1, takže je výsledná šířka vlastně stejná). Potřebujeme totiž zjistit šířku jednoho políčka.

2) CreateAnimatedSprite()

HRESULT CSprite::CreateAnimatedSprite(LPSTR szSpriteBitmap, CPoint ptInitPos, int iPhasesCount, int iAnimationSpeed)
{
    DWORD dwRet = ERROR_SUCCESS;
    //
    // Set animaiont atributes

    m_iPhasesCount = iPhasesCount;
    m_iAnimationSpeed = iAnimationSpeed;
    //
    // Create static sprite

    dwRet = CreateStaticSprite(szSpriteBitmap, ptInitPos);
    if(dwRet != DD_OK) {
        TRACE("Cannot create static part of sprite due %d\n", dwRet);
    }
    return dwRet;
}

Tuto funkci použijete chcete-li vytvořit animovaný sprite (tj. sprite který má více jak jeden animační krok). První dva parametry má úplně stejné jako předchozí funkce. Další dva určují počet snímku animace a rychlost přehrávání animace v milisekundách.

V prvním kroku uložíme vstupní parametry do členských proměnných třídy.

Pak stačí za zavolat funkci pro vytvoření statického spritu, postup je totiž stejný.

3) DrawSprite()

HRESULT CSprite::DrawSprite()
{
    DWORD dwRet = ERROR_SUCCESS;
    CRect rcSrc;
    //
    // Set source rect

    rcSrc.left  = m_iCurrentPhase * m_iWidth;
    rcSrc.right = (m_iCurrentPhase + 1) * m_iWidth;
    rcSrc.top = 0;
    rcSrc.bottom = m_iHeight;
    //
    // Draw sprite at specified location

    dwRet = m_pDisplay->ColorKeyBlt(m_ptPos.x, m_ptPos.y, m_psurSpriteSurface->GetDDrawSurface(), &rcSrc);
    if(dwRet != DD_OK) {
        TRACE("Cannot render sprite due %d\n", dwRet);
    }
    return dwRet;
}

Tato funkce nepřijímá žádné parametry a vrací to co vrátí blitovací funkce.

Funkce ColorKeyBlt() potřebuje zdrojový obdélník tzn. odkud má kopírovat zdrojová data. Takže v prvním kroku tento obdélník sestavíme. Postup sestavení osvětlí následující obrázek:

Nyní už najdete spojitost s tím co vidíte v kódu a s tím co vidíte na obrázku. Z tohoto vyplývá, že snímky v bitmapě musí být uloženy horizontálně za sebou jak je vidět na obrázku. Takže proměnná m_iCurrentPhase musí běhat u animovaného spritu v rozmezí 0(m_iPhasesCount - 1).   To zařídí funkce ChangeAnimatioSprite().

Takže teď už víte jak sestavit zdrojový obdélník a nyní již stačí jen zavolat správnou blitovací funkci. Používáme Colorkey, takže zavolejte ColorKeyBlt(). Ta přijímá za prvé pozici kam se bude kreslit (to je naše poloha spritu), dále ukazatel na povrch (opět se jedná o rozhraní IDirectDrawSurface7) a jako poslední parametr posíláme ukazatel na zdrojový obdélník (ten který jsme před chvilkou sestavili).

4) MoveSprite()

HRESULT CSprite::MoveSprite()
{
    DWORD dwRet = ERROR_SUCCESS;

    m_ptPos += CPoint(m_iVelX, m_iVelY);

    return dwRet;
}

Funkce MoveSprite() je úplně nejsnazší. Prostě jen přičte hodnoty aktuální rychlosti (v obou směrech) k aktuální pozici a tím posune sprite. Jak vidíte, vrací vždy 0. Do této funkce by případně přibyl clipping spritu. To jest ochrana proti tomu, aby sprite nepřesáhl okraj obrazovky. K tomu potřebujeme rozlišení obrazovky, takže se pravděpodobně budete muset přidat nějaké funkce, které rozlišení vrací. Princip je snadný, po každé změně polohy zkontrolujte jestli tato nová poloha nepřekračuje mimo obrazovku, pokud ano, změňte polohu spritu tak, aby vyhovovala předchozí podmínce.

5) ChangeAnimationSprite()

HRESULT CSprite::ChangeAnimationPhase()
{
    DWORD dwRet = ERROR_SUCCESS;
    DWORD dwNewTime = GetTickCount();
    //
    // Wait for right time to change phase

    if(int(dwNewTime - m_dwLastUpdate) > m_iAnimationSpeed) {
        //
        // Increment phase

        m_iCurrentPhase++;
        //
        // Check upper bound

        if(m_iCurrentPhase == m_iPhasesCount) {
            m_iCurrentPhase = 0;
        }
        // Remember last time of update
        m_dwLastUpdate = dwNewTime;
    }
    return dwRet;
}
 

Funkce ChangeAnimationPhase() je možná naopak nejsložitější.

Fáze se nesmí měnit po každém zavolání této funkce, ale jen jednou za dobu určenou proměnnou m_iAnimationSpeed. Princip je v tom, že si pamatujeme čas, kdy jsme naposledy měnili fázi a od té doby kontrolujeme, kdy doba od poslední změny překročí zmiňovanou animační rychlost. Pak opět měníme fázi a opět si zapamatujeme čas změny. Zároveň kontrolujeme, aby fáze nepřekročila maximální počet snímků (pokud se k této hodnotě přiblíží, je aktuální fáze nastavena na 0 a sekvence jede znovu).

Funkce GetTickCount() vrací čas (v milisekundách) od startu systému (vrací celkem obludné číslo). Spočítáme rozdíl mezi předchozím (čas předchozí animace) a současným časem a když tento rozdíl je větší než animační rychlost, inkrementujeme aktuální fázi (přitom kontrolujeme horní mez fáze) a opět si zapamatujeme čas, kdy k této změně došlo. A to je vlastně vše.

Poznámka: Kdyby animační fáze překročila mez určenou počtem snímků, došlo by ve funkci DrawSprite() k fatální chybě, protože bychom se pokoušeli kopírovat bitmapu z neexistující oblasti.

Poznámka: Nezapomeňte napsat konstruktor, kde budou inicializované proměnné tak, že se vytvoří implicitně statický sprite (počet snímku musí být 1). Důležitým krokem v konstruktoru je také inicializace odkazu na třídu CDisplay, který je vytvořen v CControl. Takže budete muset přidat pár funkcí do třídy aplikace a CControl, aby jste tento odkaz dostali (v příkladu, který je na CD, je vše vidět).

7.4 Příklad

I tentokrát jsem pro vás připravil příklad, který využívá znalosti z dnešní lekce. Opět se jedná o upravený projekt z minulé lekce.

Za prvé musíte vložit novou třídu CSprite a upravit ji tak, jak je ukázáno výše.

Dále musíte někde vytvořit objekty třídy CSprite. Nejlépe uděláte, když vytvoříte sprity přímo v objektu CControl. V příkladu vidíte dva ukázkové sprity. Jeden je statický a druhý animovaný. Dále musíte tyto dva sprity inicializovat ve funkci DDInit(). Pak stačí volat z funkce UpdateFrame() členské funkce CSprite, tak aby se sprite vykresloval případně pohyboval nebo měnil animační fáze.

To je vše. Když zkompilujete projekt přiložený na CD, uvidíte dva sprity, z nichž jeden je statický, ale pohybuje se a druhý je animovaný, ale stojí na stále stejném místě. Příklad můžete stáhnout v sekci Downloads.

7.5 Závěr

Problematika spritů je velice rozsáhlá. Je možno vytvořit velice složitou strukturu na sobě závislých tříd, které dohromady tvoří engine.

Na ukázku vám ukážu jednoduchý obrázek, na kterém je vidět podobná struktura (velice zjednodušená) ze hry Age of Empires 2.

Vidíte, že se zde hojně využívá dědičnosti a polyformismu jazyka C++, což je dobré si uvědomit a zamyslet se nad tím, které znaky jednotlivých spritů jsou podobné a generalizovat tyto znaky do základní třídy (BaseObject). Dále vytvoříte třídu statických spritů (podobně jako jsme to dělali v této lekci), která je odvozená od základní třídy (StaticObject). Dále chceme pohybující se objekty (např. jednotky). To zajišťuje třída MovingObject. Nakonec tu máme třídu MissileObject, který vytváří sprity munice (šípy). Všimněte si virtuální funkce update().

Na úplný závěr vám prozradím, co nás čeká v příští lekci. Jsme prakticky na konci kurzu DirectDraw. V příští lekci vám ještě povím něco uvolňování objektů DirectDraw a o "ztráceni povrchů". A tím prakticky zakončíme DirectDraw. Protože vím, že problematika je skutečně rozsáhlá a každého může zajímat něco jiného, je potřeba abyste mi dali vědět o svých problémech.

Další velice důležitou komponentou DirectX je DirectInput, která se rovněž velmi hodí pro programování her a jiných multimediálních aplikací. Takže dále budu pokračovat právě komponentou DirectInput, která není zdaleka tak složitá jako DirectDraw.


Těším se příště nashledanou.

Jiří Formánek