Home Page Home Page Articoli Framework DXGWrapper

Framework DXGWrapper

Spiega la filosofia di fondo del framework ideato dal sottoscritto ed usato da tutti i samples degli articoli relativi a DirectX.
Autore: Stefano Cristiano Livello:
Uno dei tool più utili presenti nell’SDK delle DirectX è senz’altro l’Appwizard per Visual C++. Questo appwizard permette in pochi secondi di creare un progetto che utilizzi il Framwork di Microsoft, quello usato da tutti gli esempi del DXSDK.

L’appwizard è fantastico quando su vuole implementare un semplice effetto o fare qualche prova tecnica senza perdere tempo con inizializzazioni e robe varie. Ciò nonostante ho creato un nostro framework per vari motivi: innanzitutto il codice Microsoft è commentato in inglese ed è relativamente complesso, in secondo luogo ho deciso di ampliare il nostro man mano che saranno trattati altri argomenti, in modo da far assimilare volta per volta le modifiche apportate.

Nell’esempio WrappingUpApp potete vedere come è strutturato il nuovo framework che useremo nel resto del libro. Nella cartella WrappingUpApp\Framework ci sono i file comuni che riutilizzeremo nei vari esempi. Il codice da scrivere per inizializzare una applicazione vuota è veramente banale:


#include "stdafx.h"

class WrappingUpApp : public DXGObject
{
public:

bool OnRestoreDeviceObjects()
{
return true;
}

bool OnDraw()
{
return true;
}

bool OnInvalidateDeviceObjects()
{
return true;
}
};

DXGWrapper wrapper;
DXGWrapper* InitDXGWrapper(HWND wnd)
{
bool ok;
ok = wrapper.SetMainDXGObjectAndHWND(new WrappingUpApp(),wnd);

if(!ok)
return 0; //Aime' qualcosa e' andato storto...

return &wrapper;
}


In pratica dobbiamo creare un oggetto che erediti dalla classe DXGObject e passarlo ad una istanza di DXGWrapper, il manager del framework, tramite DXGWrapper::SetMainDXGObjectAndHWND(). Il metodo InitDXGWrapper viene chiamato dal framework stesso, che si occupa anche della gestione di WinMain e della WndProc dei messaggi. Se tutto va bene dobbiamo ritornare l’indirizzo del wrapper, come potete leggere dal codice. Se eseguite il programma vi accorgerete anche che con il tasto F2 si attiva una finestra di configurazione che permette di passare tra modalità finestra e fullscreen e anche di regolare il fattore GAMMA discusso poc’anzi. Il framework gestisce da solo anche il ridimensionamento della finestra e i vari ALT+TAB che l’utente può fare a tutto schermo.

Le classi che gestiscono il tutto sono DXGWrapper (il manager), DXGDeviceConfig (la finestra di configurazione), DXGGammaDeviceConfig (il test per regolare il fattore gamma) e DXGObject o DXGMovableObject (la classe dalla quale tutti i nostri esempi devono ereditare).

Le prime tre non sono nulla di più di quanto descritto in questo capitolo, il loro scopo è solo mettere un po’ d’ordine in quello che abbiamo trattato. Un’ultima importantissima cosa, quando create nuovi progetti utilizzando il DXGWrapper usando il Visual C++, ricordatevi di abilitare la RTTI attraverso Project->Settings->C/C++->C++ Langauage->Enable Run Time Type Information (RTTI). Se non farete questo ricevereti errori inaspettati nell’esecuzione del programma.

La Filosofia del Framework
La programmazione è complessa. “complessa” ha un significato diverso da “difficile”, poiché di solito le situazioni complesse sono scomponibili in tanti piccoli sotto-problemi più facilmente gestibili. Quando andremo avanti acquisiremo tantissime conoscenze e necessariamente dobbiamo trovare un modo per non perderci in questo sconfinato universo. Grazie al cielo qualcuno tempo fa decise di inventare la programmazione ad oggetti, di cui questo libro fa un uso abbastanza pesante.

A mio parere pensare ad oggetti è molto più semplice e naturale che pensare proceduralmente, perché ricalca in parte il nostro modo di ragionare. Quando girate la chiavetta della vostra automobile non vi serve né interessa sapere com’è costruito il motore, l’albero, le sospensioni, voi la girate e la macchina parte. Beh il mio scopo alla fine di questo libro non è solo insegnare come risolvere problematiche specifiche, ma anche abituare a pensare sempre ogni problema come scomponibile in una serie di sotto sistemi tra di loro più o meno indipendenti.

La struttura dati “principe” di questa filosofia è l’albero (tree) e l’algoritmo alla sua base è la ricorsione.

L’albero è costituito da una serie di nodi tra di essi collegati da relazioni di tipo padre-figlio. Ogni nodo può avere un solo padre ma tanti figli quanti ne vuole. A loro volta questi figli hanno un loro padre ma tanti figli. Alcuni di questi nodi possono rappresentare sotto-sistemi molto complessi e ramificati, ma esternamente a noi interessa interagiare con il nodo “padre”. Lasceremo a lui e ai suoi nodi “figli” il compito di portare a termine il lavoro o la funzionalità richiesta.

Il primo immediato vantaggio dell’utilizzo degli alberi è che non dovrete più occuparvi della deallocazione della memoria. Quando create degli oggetti dinamici (attraverso la keyword new del C++ per intenderci) non è sempre chiaro se e dove questi oggetti saranno rilasciati (attraverso la keyword del C++ delete). Spessissimo ci si imbatte in situazioni dove NON si rilasciano tutti gli oggetti allocati (memory leaks) oppure si rilascia per errore più volte lo stesso oggetto, causando errori di protezione (le classiche schermatine di crash di Windows). Attraverso gli alberi invece ci basta dire ad un nodo “cancellati!” e lui provvederà ad eliminarsi e ad eliminare tutta la sua prole ricorsivamente. Ricorsivamente significa che la stessa azione viene ripetuta sui figli e da questi ai loro figli e così via, indefinitivamente. Se provate a testare tutti i samples di questo libro per cercare leaks con il metodo spiegato nel primo capitolo, non ne troverete nessuno.

La classe che definisce il NODO dell’albero è TreeNode, costituita da un puntatore ad un altro TreeNode (che fa da nodo padre) e da un vettore di nodi figli. Quando creiamo un nuovo nodo ci basta “attaccarlo” al padre con il metodo “Attach”. Se vogliamo “staccarlo” ci basta chiamare “Detach.


TreeNode* nodoPadre = new TreeNode;
TreeNode* figlio = new TreeNode;
TreeNode* nipote1 = new TreeNode;
TreeNode* nipote2 = new TreeNode;


nodoPadre->Attach(figlio);
figlio->Attach(nipote1);
figlio->Attach(nipote2); //facciamo quello che vogliamo

delete nodoPadre; //cancella tutta la gerarchia.



In realta’ TreeNode è troppo generica per i nostri gusti, così creiamo una classe di nome DXGObject, da cui ereditano tutti gli oggetti che hanno a che fare con DirectX.


class UnEffettoParticolare : public DXGObject
{
public:

bool OnInit()
{
//Inizializziamo qui..
return true;
}

bool OnDraw()
{
//Disegnamo il nostro effetto qui...
return true;
}

bool OnRelease()
{
//Rilasciamo le risorse qui....
return true;
}

};

class Applicazione : public DXGObject
{

public:

Applicazione()
{
Attach(new UnEffettoParticolare());
}

bool OnDraw()
{
//Che ne so, disegnamo i frames x secondo qui...
return true;
}
};


La nostra “Applicazione” si attacca come figlio un’istanza di UnEffettoParticolare che ridefinendo alcuni metodi di DXGObject può inizializzarsi, disegnare quello che vuole e terminarsi quando il programma finisce. Come potete notare nessuno chiama ESPLICITAMENTE quei metodi, poiché è sempre il framework a gestire internamente il tutto. L’unica cosa da fare è attaccare un nodo con “Attach” (possibilmente nel costruttore o in OnInit() ) e poi sarà DXGObject che chiamerà i metodi opportuni nei momenti giusti (OnDraw, OnAnimate etc).

Ecco qui una lista dei metodi di DXGObject che si possono ridefinire nelle classi derivate:

virtual bool OnInit()
Qui si inizializzano oggetti NON DeviceDependant

virtual bool OnCreateDeviceObjects()
Qui si inizializzano oggetti DeviceDependant che non hanno bisogno di essere reinizializzati quando il device diventa 'lost', in seguito ad un ridimensionamento della finestra di rendering cioe' gli oggetti creati con D3DPOOL_MANAGED

virtual bool OnInvalidateDeviceObjects()
Qui si cancellano gli oggetti Device Dependant in seguito ad un device 'lost',prima che venga chiamato RestoreDeviceObjects() a rimettere tutto a posto, per esempio quelli creati con D3DPOOL_DEFAULT

virtual bool OnRestoreDeviceObjects()
Qui si inizializzano gli oggetti Device Dependant ogni volta che il device viene perso e riacquisito (per esempio dopo un ridimensionamento di finestra o uno switch windowed/fullscreen)

virtual bool OnReleaseDeviceObjects()
Qui vengono eliminati gli oggetti creati in CreateDeviceObjects()

virtual bool OnRelease()
Qui si eliminano gli oggetti NON Device Dependant creati in OnInit()

virtual bool OnPrepareDraw()
Questo metodo viene chiamato PRIMA che venga eseguito il disegno della scena corrente. Utile per fare un “Render To Texture” come vedermo avanti.

virtual bool OnDraw()
Qui si disegna la scena

virtual bool OnAnimate()
Qui si anima la scena, magari alterando la matrice world o di camera o quello che volete insomma.Mi raccomando ogni trasformazione/incremento deve essere dipendente dal tempo attraverso wrapper->fTime o wrapper->fElapsedTime altrimenti perderete ogni temporizzazione, la vostra applicazione girerà a diverse velocità a seconda del sistema operativo/scheda video/processore etc.

Giusto per farvi capire nel vostro oggetto ereditato da DXGObject questi metodi vengono chiamati in questo ordine all’avvio del programma:

1. Costruttore dell’oggetto ovviamente
2. OnInit()
3. OnCreateDeviceObjects()
4. OnInvalidateDeviceObjects()
5. OnRestoreDeviceObjects()

Durante il rendering:
6. OnAnimate()
7. OnPrepareDraw()
8. OnDraw()

Se beccate un evento “Device Lost”, un ridimensionamento di finestra o un cambiamento di risoluzione x esempio:
9. OnInvalidateDeviceObjects()
10. OnRestoreDeviceObjects()

Alla fine del programma (per rilasciare tutte le risorse allocate)

11. OnInvalidateDeviceObjects()
12. OnReleaseDeviceObjects()
13. OnRelease()
14. Distruttore dell’oggetto
Da notare che quando rilasciate oggetti vengono rilasciati anche tutti gli oggetti figli (vedere sezione successiva “Eliminare gli oggetti del framework”).

Tra gli svantaggi dell’utilizzo di questo framework invece c’è il fatto che se non si sta attenti si possono creare delle gerarchie abbastanza sconclusionate e difficili da gestire. Potreste per esempio attaccare una oggetto 3D ad un suono a sua volta attaccato ad una texture, il che non ha molto senso. Per tale motivo attraverso il metodo bool TreeNode::Accept(TreeNode* node) e la tecnica del dynamic_cast si possono limitare i child che si possono attaccare ad un determinato nodo.


class NodoFiglio : public DXGObject
{
//...ci sono tutti i suoi metodi
};

class NodoFiglia : public DXGObject
{
//...ci sono tutti i suoi metodi
};

class NodoEstraneo : public DXGObject
{
//...ci sono tutti i suoi metodi
};

class NodoPadre : public DXGObject
{

public:

bool Accept(TreeNode* node)
{

return (dynamic_cast(node)||
dynamic_cast(node))!=0;
}
};

void program()
{

//...
NodoPadre* padre = new NodoPadre;
NodoFiglio* figlio= new NodoFiglio;
NodoFiglia* figlia= new NodoFiglia;
NodoEstraneo* estraneo = new NodoEstraneo;
padre->Attach(figlio);
padre->Attach(figlia);


//Questo non funziona!!!
padre->Attach(estraneo);
}


L’ultima riga di questo programma fallirà in silenzio, in quanto la classe NodoPadre può accettare solo istanze di classi NodoFiglio e NodoFiglia, come riportato in NodoPadre::Accept(TreeNode*).

In questo modo possiamo evitare di creare gerarchie senza capo né coda.

Penso che sia noto ai più l’utilizzo di dynamic_cast è una keyword del C++ che ci permette di sapere se un puntatore eredita da classi che ci interessano. Ecco un altro esempio


void program()
{

//...
NodoPadre* padre = new NodoPadre;
TreeNode* f1= new NodoFiglio;
padre->Attach(f1);

//Immaginate di trovarvi altrove ora
//e di non sapere più niente su f1…

NodoFiglio* figlio = dynamic_cast(padre->GetChild(0));
NodoFiglio* figlia = dynamic_cast(padre->GetChild(0));

if(figlio)
{

//Qui e' ok!!!
}

if(figlia)
{
//Non arriveremo mai qui!!!
}
}


In questo caso come potete ben vedere il primo child di NodoPadre è un oggetto di tipo NodoFiglio. Quando eseguiamo una Attach in un certo senso “perdiamo” le informazioni sui tipi, perché tutto viene passato come TreeNode. Quando chiamiamo GetChild() su NodoPadre ci serve ben a poco il fatto che il child è di classe TreeNode, e ci interessa sapere qualcosa di più su “chi è realmente”. Per esempio in questo caso vogliamo sapere se è un NodoFiglio o NodoFiglia. Attraverso dynamic_cast come potete vedere la cosa diventa banale, se il puntatore ritornato è diverso da zero allora il cast ha avuto successo altrimenti no. Questa tecnica è usata MOLTISSIMO in tutto il framework e in parcchi esempi (ricordatevi di abilitare la RTTI, Visual C++ non lo fa di default!!!).

DXGObject contiene dei metodi il cui nome inizia con un doppio trattino basso (__XXXXXX) e hanno lo stesso nome dei vari OnRelease(), OnInit. Etc. Quei metodi si prendono cura di mandare ogni messaggio giù nella gerarchia dell’albero, per esempio chiamando OnDraw() su ogni nodo per permettere il disegno della scena. In generale non dovrete mai ridefinire questi metodi, ma ci sono alcuni casi speciali (come vedremo DXGMovableObject e DXGCullableObject per esempio) dove lo faremo e capirete perchè.

Eliminare gli oggetti del framework
Per motivi tecnici del C++ risulta errato cancellare oggetti derivati da DXGObject attraverso la keyword “delete” del C++. Il problema sta nel fatto che nel distruttore non si può usare la VTable e risulta impossibile chiamare metodi virtuali tipo OnReleaseDeviceObjects() oppure OnRelease().

Per questo motivo quando volete rilasciare oggetti derivati da DXGObject dovete chiamare DXGObject::Destroy() oppure utilizzare la macro SUREDESTROY che controlla anche di non rilasciare inavvertitamente più volte lo stesso oggetto.

Gli oggetti derivati direttamente da TreeNode invece possono essere cancellati senza problemi con “delete” o con la comoda macro SUREDELETE.

Più avanti nel libro ci imbatteremo in alcuni casi particolarissimi, dove utilizzeremo la tecnica del REFERENCE COUNT (la stessa spiegata per gli oggetti COM) e per rilasciare chiameremo SURERELEASE. Ecco un quadro riassuntivo di come eliminare gli oggetti.

Oggetto derivato da TreeNode SUREDELETE(oggetto)
Oggetto derivato da DXGObject SUREDESTROY(oggetto)
Oggetto Reference Counted SURERELEASE(oggetto)


Ovviamente queste macro come già detto eliminano un nodo e tutta la sua gerarchia di nodi figli.

Ad ogni modo man mano che andremo avanti prenderete sempre più mano con il framework. I sorgenti sono commentati molto bene quindi è inutile stare a scrivere qui quello che troverete aprendo il progetto con il vostro IDE preferito.
Voto medio articolo: 5.0 Numero Voti: 2
Stefano Cristiano

Stefano Cristiano

3D Coder. Sto lavorando ad un engine chiamato "Muse Engine" di cui potete leggere al mio sito http://pagghiu.gameprog.it Profilo completo

Articoli collegati

Integrare le Direct3D 8.1 con MFC Usando Visual Studio 6.0
In questo articolo mostreremo come creare una semplice applicazione SDI (Single Document Interface), in cui mostrare un oggetto tridimensionale in formato *.X e di farlo ruotare intorno all'asse Y.
Autore: Biagio Iannuzzi | Difficoltà:
Particle System
Brevi cenni su come implementare un particle system in DirectX
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture Blending (Parte II) Quake 3 Arena Shader System
Questo articolo è simile alla Parte I e ripete molti aspetti però tratta anche un pò degli shaders di Quake 3 Arena.
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture Blending (Parte I)
Come gestire gli effetti di blending tra texture. Multitexturing e Multipassing. Effect Files
Autore: Stefano Cristiano | Difficoltà: | Voto:
Sprite
Come creare uno "sprite" e animarlo. Special thanks to Elevator2 per gli sprites di megaman
Autore: Stefano Cristiano | Difficoltà: | Voto:
Texture
Introduzione al concetto di texture, di coordinate texture. Come animare le coordinate texture
Autore: Stefano Cristiano | Difficoltà:
Grafica 2D in DirectX. Vertex Buffers
Introduzione alla grafica 2D. Utilizzo dei vertex buffers
Autore: Stefano Cristiano | Difficoltà: | Voto:
Debbugging di applicazioni DirectX
Consigli e strategie per il Debugging delle applicazioni DirectX.
Autore: Stefano Cristiano | Difficoltà: | Voto:
Gamma Correction e Colori
Una trattazione breve ma con una base matematica che spiega in parole povere il problema del fattore gamma e i colori in generale
Autore: Stefano Cristiano | Difficoltà: | Voto:
Inizializzazione e schermo
Si parla dell'inizializzazione dello schermo e delle modalità video in DirectX e di come gestire gli eventi "lost device", come scrivere un Game Loop e alcune info sul VSYNC
Autore: Stefano Cristiano | Difficoltà: | Commenti: 1 | Voto:
Introduzione a DirectX
Introduzione al mondo DirectX , requisiti, tools necessari, configurazione dell’ambiente di sviluppo.
Autore: Stefano Cristiano | Difficoltà: | Commenti: 2 | Voto:
Copyright © dotNetHell.it 2002-2017
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5