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.
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:
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.
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ů).
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.
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:
Je třeba zadat nulové ID, protože podle tohoto ID zjistíme, že objekt nebyl inicializován.
Dále je potřeba vynulovat pozici a stav, do kterého se sprite dostane po skončení aktuálního stavu.
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.
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.