DirectX (14.)

Ve dvou předešlých lekcích jsme rozebírali celou knihovnu Display.dll, která obsahuje třídy CDisplay a CSurface. Tyto dvě třídy jsou jádrem našeho grafického systému. Dnes na to navážeme a přidáme novou knihovnu Input.dll. Tuto knihovnu jsme již napsali, stačí tedy pouze vložit existující kód do našeho projektu.

1. Knihovna Input.dll

Tuto knihovnu jsme podrobně rozebírali v 9. a 10. lekci kurzu DirectX. Mimo jiné jsme si řekli, že bude nutné předělat systém vykreslovaní kurzoru. A právě to nás čeká v dnešní lekci.

V prvním kroku tedy přidejme knihovnu Input.dll do stávajícího projektu z minulých lekcí. Zkopírujte tedy adresář projektu Input do adresáře Game. V tomto adresáři budeme mít tedy Common, Display, Input, Game a adresáře Debug a Release. Poté z Visual C++ upravíme náš projekt. Z menu Project vyberte položku Insert Project into Workspace. Vyberte soubor Input.dsp v adresáři Input a v Classview se objeví naše známá knihovna Input.dll.

Nyní nastavme závislosti projektů tak, aby projekt Input mohl využít Display a projekt Game mohl využít Input. Nastavte dialog Dependencies takto:

Pokud teď označíte jako aktivní projekt Game a zkompilujete ho, zkompilují  se projekty postupně v pořadí Display, Input a Game.

Abychom mohli volat funkce u Input.dll musíte vložit hlavičkový soubor Input1.h do hlavního souboru projektu Game:

#include "..\Input\Input1.h"
 

Samozřejmě musíme brát v úvahu to, že projekt Input je ve svém adresáři.

Nyní do funkce WinMain() doplňte volání funkce inpCreateDirectInputSystem() a předejte si následující parametry:

Pokud si vzpomínáte, tak při tvorbě Input.dll jsme nepoužívali knihovnu Common.dll. Když se podíváte na kód, tak zjistíte, že místo makra DXTRACE používáme obyčejné TRACE apod. Pro správnou funkci není třeba toto předělávat, ale bude dobré, když těchto pár drobností také upravíme. Musíme proto jen vložit Common.h do souboru Input1.cpp. Dále musíte v nastavení projektu přilinkovat knihovnu Common.dll. Uděláme to úplně stejně jako u ostatních projektů (nezapomeňte na Release):

Nyní tedy přepište makra TRACE na makra DXTRACE nebo DXERR podle toho, jestli chceme rozluštit návratovou hodnotu nebo ne. Já jsem všude kromě jednoho případu použil makro DXERR. Nezapomeňte vymazat znaky \n, protože odřádkování se provádí automaticky a takto byste odřádkovali 2x.

2. Úprava vykreslení kurzoru

Nyní se budeme věnovat hlavnímu tématu dnešní kapitoly. Jsme nároční a chceme používat i animované kurzory. Celé je to o trochu složitější a výsledek vypadá lépe.

Použijeme k tomu zcela novou třídu CCursor, která bude pracovat s DirectDraw. Tato třída se bude starat o vytvoření a vykreslení kurzoru na obrazovku. Zde je deklarace třídy:

class CCursor
{
    BOOL m_bInit;
    int m_iWidth;
    int m_iHeight;
    CString m_csIntPath;

    CSurface m_surCursor;
    int m_iPhase;
    int m_iSpeed;
    int m_nPhases;
    DWORD m_dwOldTime;

public:
    HRESULT Create(CString csIntPath, int cx, int cy);
    HRESULT Create(CString csIntPath, int cx, int cy, int nSpeed);
    HRESULT Update(CPoint ptCursor);

public:
    CCursor();
    ~CCursor();

};

Je to velice jednoduché. V následující tabulce vidíte popis proměnných.
 
Typ proměnné Jméno proměnné Popis
int m_iWidth Šířka jednoho políčka kurzoru
int m_iHeight Výška jednoho políčka kurzoru
CString m_csIntPath Cesta k bitmapě kurzoru
CSurface m_surCursor Povrch kurzoru
int m_iPhase Aktuální sekvence animovaného kurzoru
int m_iSpeed Animační rychlost v milisekundách
int m_nPhases Celkový počet animačních kroků
DWORD m_dwOldTime Uplynulý čas od posledního vykreslení


Dále si proberme metody:
 
Jméno metody a parametry Popis
Create(CString csIntPath, int cx, int cy) Tato verze metody Create() vytváří statický kurzor z bitmapy csIntPath a velikosti cx a cy.
Create(CString csIntPath, int cx, int cy, int nSpeed) Druhá verze je již zajímavější. Vytváří animovaný kurzor z bitmapy csIntPath a velikosti jednoho políčka cx a cy. Animační rychlost je určena parametrem nSpeed.
Update(CPoint ptCursor) Tato funkce vykreslí kurzor na požadovaných souřadnicích.

Konstruktor a destruktor třídy v seznamu neuvádím, ale to je samozřejmost. Třídu můžete přidat pomocí Visual Studia nebo můžete využít příkladu u lekce.

Animovaný kurzor je uložen v bitmapě v tomto formátu:

Černá okolní barva se označí jako průhledná. Jednotlivé sekvence se mění po uplynutí doby určené parametrem iSpeed. Dále parametry cx a cy určují velikost jednoho políčka. Z těchto hodnot se také vypočítá, kolik má kurzor animačních sekvencí. Rozeberme si nyní detailněji co se odehrává v metodách třídy CCursor.

Nejprve následuje první verze metody Create():

HRESULT CCursor::Create(CString csIntPath, int cx, int cy)
{
    DWORD dwRet = 1;
    if(!m_bInit) {

        m_iWidth = cx;
        m_iHeight = cy;
        m_csIntPath = csIntPath;

        dwRet = m_surCursor.Create(csIntPath, DDFLG_COLORKEY);
        if(dwRet != ERROR_SUCCESS) {
           DXERR("Cannot create surface for cursor due", dwRet);
           return dwRet;
        }
        m_bInit = true;
        dwRet = ERROR_SUCCESS;
    }
    return dwRet;
}

Metoda je krátká a srozumitelná. Uložíme si vstupní parametry a vytvoříme povrch pro bitmapu csIntPath. To je celé.

Druhá verze je jen o málo složitější:

HRESULT CCursor::Create(CString csIntPath, int cx, int cy, int iSpeed)
{
    DWORD dwRet = 1;
    if(!m_bInit) {
      
 //
        // Call standard creation

        dwRet = Create(csIntPath, cx, cy);
        if(dwRet == ERROR_SUCCESS) {
            m_iPhase = 0;
            m_iSpeed = iSpeed;
           
//
            // Compute number of phases

            m_nPhases = (int)m_surCursor.Width() / cx;
           
//
            // Check validity of phases

            if((m_nPhases * cx) > (int)m_surCursor.Width()) m_nPhases--;
          
 //
            // Ok

            m_bInit = true;
        }
    }
    return dwRet;
}

Nejprve zavoláme první verzi metody Create() (tím vytvoříme povrch pro bitmapu a uložíme parametry kurzoru). Dále objekt připravíme pro spuštění animace tj. vynulujeme počítadlo aktuální sekvence, nastavíme animační rychlost a vypočteme, kolik sekvencí má naše bitmapa. To provedeme jednoduše: vydělíme šířku bitmapy šířkou jednoho políčka. Protože potřebujeme celé číslo a bitmapa nemusí být přesně dělitelná (měla by být), musíme zkontrolovat zda-li počet sekvencí odpovídá i zpětně. Zkrátka vynásobíme vypočtenou hodnotu šířkou jednoho políčka a zjistíme zda-li tato hodnota nepřekračuje povrch kurzoru, to by totiž způsobilo fatální chybu při vykreslování. Pokud tomu tak skutečně je, tak jen snížíme počet sekvencí o jedničku (horší už to být totiž nemůže).

Nakonec si popíšeme vykreslovací metodu Update(), která je už složitější:

HRESULT CCursor::Update(CPoint ptCursor)
{
    CRect rcDest, rcSrc;
    DWORD dwRet = 1, newTime;
    if(m_bInit) {
       
//
        // For static cursors

        if(m_nPhases == 0) {
            //
            // One static place
            rcSrc.top = 0;
            rcSrc.left = 0;
            rcSrc.right = m_iWidth;
            rcSrc.bottom = m_iHeight;
        }
     
  //
        // Compute move source rectangle

        else {
           
//
            // Compute phase, increment phase if is time

            newTime = GetTickCount();
           
//
            // Get time from last update

            if((newTime - m_dwOldTime) > (DWORD)m_iSpeed) {
              
 //
                // If it right time, increment phase

                m_iPhase++;
               
//
                // Check to overflow phases

                if(m_iPhase == m_nPhases) {
                    m_iPhase = 0;
                }
               
//
                // Save time of last update

                m_dwOldTime = newTime;
            }
            //
          
 // Compute rectangle from current phase
            //
            // Static vertical postion

            rcSrc.top = 0;
            rcSrc.bottom = m_iHeight;
            // Variable horizontal pos.
            rcSrc.left = m_iPhase * m_iWidth;
            rcSrc.right = rcSrc.left + m_iWidth;
        }
     
  //
        // Destionation rectangle is same for both cursors
     
  rcDest.left = ptCursor.x;
        rcDest.top = ptCursor.y;
        rcDest.bottom = rcDest.top + m_iHeight;
        rcDest.right = rcDest.left + m_iWidth;
 
       //
        // Blit cursor to back surface
 
      dwRet = disBlt(rcDest, rcSrc, &m_surCursor);
    }
    return dwRet;
}

 

Metoda je vlastně jednoduchá jen pro statické kurzory. Pokud je totiž počet animačních kroků nulový, jedná se o statický kurzor. Je-li tomu tak, stačí jen zinicializovat zdrojový a cílový obdélník pro funkci disBlt(). Zdrojový je přesně velký jako bitmapa. Rozměry jsme si uložili do proměnných m_iWidth a m_iHeight. Cílový obdélník je pokaždé stejný a je určen místem, kde má být kurzor vykreslen. Horní levý roh je přímo to místo a dolní pravý je jen posunuty o velikost políčka.

U animovaného kurzoru je jiný výpočet zdrojového obdélníka, protože ten se musí pohybovat po bitmapě. V části určené jen pro animované kurzory tedy za prvé modifikujeme aktuální fázi a za druhé inicializujeme tento pohyblivý obdélník. Fázi vždy měníme po uplynutí času m_iSpeed, dále musíme kontrolovat horní mez určenou proměnnou m_iPhases. Při každé změně navíc musíme uložit aktuální čas, abychom mohli počítat časový rozdíl do další změny. Za druhé tedy vypočteme zdrojový obdélník. Ve vertikálním směru je vlastně stejný jako u statického kurzoru, protože políčka jsou v bitmapě uloženy horizontálně. Horizontální pozice záleží na aktuální fázi animace. Levý okraj je tedy jen vynásobení této fáze a šířky políčka. Pravý okraj je oproti levému posunutý o šířku políčka. Zbytek metody je totožný.

Nyní pouze stačí vytvořit ve třídě CInput jeden objekt typu CCursor a zavolat jeho metodu Create(). Dále musíte upravit metodu UpdateCursor():


HRESULT CInput::UpdateCursor()
{
    HRESULT dwRet = 1;
    if(m_bInit && m_bShowCursor) {

        m_theCursor.Update(m_ptCursor);
    }
    return dwRet;
}

Nyní je metoda velice jednoduchá. Prostě jen zavoláme metodu kurzoru Update(), ale jen je-li kurzor viditelný.

TIP: Předešlým postupem jsme vytvořili jeden kurzor, což je dosti nepružné. Zkuste si udělat dynamické pole kurzorů, poté vytvořte metodu CInput::LoadCursor(DWORD, CString), která nahraje kurzor do tohoto pole. Parametr DWORD je nějaké ID kurzoru. Dále udělejte metodu SetCursor(DWORD), která vybere kurzor z tohoto pole podle ID a nastaví ho jako aktuální kurzor, který se bude vykreslovat.

Nakonec ještě malinko upravíme projekt Game, aby bylo vidět, že systém DirectInput pracuje:

void UpdateFrame()
{
    disUpdateBackground();

   
inpProcessInput();

   
// Pri stisknuti klavesy Esc ukoncime aplikaci
    if(inpIsKeyDown(DIK_ESCAPE, FALSE)) {
        PostMessage(g_hWnd, WM_DESTROY, 0, 0);
    }


   
inpUpdateCursor();

    disPresent();
}

Do funkce UpdateFrame() přidejte barevný kód. Nyní se vám bude zobrazovat animovaný kurzor a budete moci použít všechny exportované funkce třídy CInput. Pro příklad využívám klávesu Esc jako výstupní klávesu z programu.

Poznámka:  Ještě nezapomeňte vymazat nepotřebné složky třídy CInput. Je to atribut HICON m_hCursor a metoda SetCursor(HICON).

3. Závěr

Dnešní lekce byla pro změnu trochu kratší, ale určitě byla velkým přínosem pro náš projekt. Možná by se mohlo zdát, že příště už nebudeme mít co dělat, ale opak je pravdou, protože příště již konečně začneme s tím grafickým menu. Doufám, že to bude zajímavější.

Dnešní příklad si samozřejmě budete moci stáhnout v rubrice Downloads.

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

Jiří Formánek