Navigation▲
Tutoriel précédent : Mise en place de DirectX 11 avec Visual Studio | Sommaire | Tutoriel suivant : Initialisation de DirectX 11 |
II. Introduction▲
Avant de commencer à coder avec DirectX 11, je vous recommande de construire un squelette de code simple. Cette ossature prendra en charge les fonctionnalités de base des fenêtres et vous fournira un moyen simple d'étendre le code de façon organisée et lisible. Comme le but de ces tutoriels est juste d'essayer différentes fonctionnalités de DirectX 11, nous garderons une base aussi claire que possible.
III. Le cadre de travail▲
Le cadre de travail débutera par quatre éléments. Une fonction WinMain pour traiter le point d'entrée de l'application. Une classe System encapsulant l'ensemble de l'application qui sera appelée au sein de la fonction WinMain. Dans la classe System, nous aurons une classe Input pour la gestion des entrées de l'utilisateur et une classe Graphics pour manipuler le code graphique DirectX. Voici un diagramme de cette configuration :
Maintenant que nous voyons comment le squelette est organisé, commençons par regarder la fonction WinMain dans le fichier main.cpp.
IV. WinMain▲
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : main.cpp
///
/////////////////////////////////////////////////////////////////////////////
#include
"systemclass.h"
int
WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int
iCmdshow)
{
SystemClass*
System;
bool
result;
// Créer l'objet System.
System =
new
SystemClass;
if
(!
System)
{
return
0
;
}
// Initialiser et exécuter l'objet System.
result =
System->
Initialize();
if
(result)
{
System->
Run();
}
// Fermer et libérer l'objet System.
System->
Shutdown();
delete
System;
System =
0
;
return
0
;
}
Comme vous pouvez le voir nous avons gardé la fonction WinMain assez simple. Nous créons un objet System, puis l'initialisons. S'il s'initialise sans problème, nous appelons sa méthode Run qui exécutera sa propre boucle ainsi que tout le code de l'application jusqu'à ce qu'elle se termine. Après cela, nous fermons l'objet System et le libérons. Nous avons donc fait très simple et encapsulé toute l'application à l'intérieur de la classe System. Jetons maintenant un œil au fichier d'en-tête de la classe System.
V. Systemclass.h▲
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : systemclass.h
///
/////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_
Nous définissons ici WIN32_LEAN_AND_MEAN. Nous faisons cela pour accélérer la compilation, réduisant la taille des fichiers d'en-tête Win32 en excluant certaines bibliothèques moins utilisées.
///
////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///
////////////////////////////
#define WIN32_LEAN_AND_MEAN
Windows.h est inclus afin que nous puissions appeler les fonctions pour créer/détruire des fenêtres et utiliser les autres fonctions Win32 utiles.
///
///////////
// INCLUDES //
///
///////////
#include
<windows.h>
Nous avons inclus les en-têtes des deux autres classes de notre ensemble de composants ici afin que nous puissions les utiliser dans la classe System.
///
////////////////////
// MY CLASS INCLUDES //
///
////////////////////
#include
"inputclass.h"
#include
"graphicsclass.h"
La définition de la classe est assez simple. Nous voyons la définition des méthodes Initialize, Shutdown, et Run qui sont appelées dans WinMain, ainsi que quelques fonctions privées qui seront appelées à l'intérieur de ces fonctions. Nous avons également une fonction MessageHandler dans la classe pour gérer les messages du système de fenêtres envoyés à l'application durant son exécution. Enfin, nous avons quelques variables privées m_Input et m_Graphics qui pointeront vers les deux objets se chargeant des graphiques et des entrées.
///
/////////////////////////////////////////////////////////////////////////////
// Nom de la classe : SystemClass
///
/////////////////////////////////////////////////////////////////////////////
class
SystemClass
{
public
:
SystemClass();
SystemClass(const
SystemClass&
);
~
SystemClass();
bool
Initialize();
void
Shutdown();
void
Run();
LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);
private
:
bool
Frame();
void
InitializeWindows(int
&
, int
&
);
void
ShutdownWindows();
private
:
LPCWSTR m_applicationName;
HINSTANCE m_hinstance;
HWND m_hwnd;
InputClass*
m_Input;
GraphicsClass*
m_Graphics;
}
;
///
//////////////////////
// FUNCTION PROTOTYPES //
///
//////////////////////
static
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
///
//////////
// GLOBALS //
///
//////////
static
SystemClass*
ApplicationHandle =
0
;
#endif
La fonction WndProc et le pointeur ApplicationHandle sont également inclus dans ce fichier de classe afin que nous puissions rediriger les messages du système de fenêtres dans notre méthode MessageHandler de la classe System.
Jetons maintenant un œil au fichier source de la classe System :
VI. Systemclass.cpp▲
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : systemclass.cpp
///
/////////////////////////////////////////////////////////////////////////////
#include
"systemclass.h"
Dans le constructeur de la classe, j'initialise les pointeurs d'objet à NULL. Ceci est important parce que si l'initialisation de ces objets échoue, la fonction Shutdown tentera plus tard de libérer ces objets. S'ils ne sont pas NULL, alors cela suppose qu'ils pointent vers des objets valides créés et ayant besoin d'être libérés. Il est également recommandé d'initialiser tous les pointeurs et les variables à NULL dans vos applications. Certaines compilations en release échoueront si vous ne le faites pas.
SystemClass::
SystemClass()
{
m_Input =
0
;
m_Graphics =
0
;
}
Je crée ici un destructeur et un constructeur de copie vides. Je n'ai pas besoin d'eux dans cette classe, mais s'ils ne sont pas définis, certains compilateurs vont les générer pour vous, et je préfère ici qu'ils soient vides.
Vous remarquerez aussi que je ne libère aucun objet dans le destructeur de la classe. Je fais tout cela dans la méthode Shutdown que vous verrez plus tard. La raison est que je n'ai pas la certitude qu'elle est appelée. Certaines fonctions de Windows comme ExitThread() sont connues pour ne pas appeler vos destructeurs de classe, produisant des fuites de mémoire. Vous pouvez dorénavant bien sûr appeler des versions plus sûres de ces fonctions, mais je préfère être très prudent lorsque je programme sous Windows.
SystemClass::
SystemClass(const
SystemClass&
other)
{
}
SystemClass::
~
SystemClass()
{
}
La fonction Initialize qui suit effectue l'installation de l'application. Elle appelle d'abord InitializeWindows qui crée la fenêtre, puis crée et initialise les objets Input et Graphics que l'application utilisera pour la gestion des entrées de l'utilisateur et du rendu.
bool
SystemClass::
Initialize()
{
int
screenWidth, screenHeight;
bool
result;
// Initialise la largeur et hauteur de l'écran à zéro avant d'envoyer les variables à la fonction.
screenWidth =
0
;
screenHeight =
0
;
// Initialise la fenêtre de l'interface
InitializeWindows(screenWidth, screenHeight);
// Crée l'objet Input. Cet objet sera utilisé pour gérer la saisie au clavier par l'utilisateur.
m_Input =
new
InputClass;
if
(!
m_Input)
{
return
false
;
}
// Initialise l'objet Input.
m_Input->
Initialize();
// Crée l'objet Graphics. Cet objet gérera tous les graphiques de l'application.
m_Graphics =
new
GraphicsClass;
if
(!
m_Graphics)
{
return
false
;
}
// Initialise l'objet Graphics.
result =
m_Graphics->
Initialize(screenWidth, screenHeight, m_hwnd);
if
(!
result)
{
return
false
;
}
return
true
;
}
La fonction Shutdown fait le nettoyage. Elle arrête et libère tout ce qui est associé aux objets Graphics et Input. Elle ferme également la fenêtre et libère les identifiants qui lui sont associés.
void
SystemClass::
Shutdown()
{
// Libère l'objet Graphics.
if
(m_Graphics)
{
m_Graphics->
Shutdown();
delete
m_Graphics;
m_Graphics =
0
;
}
// Libère l'objet Input.
if
(m_Input)
{
delete
m_Input;
m_Input =
0
;
}
// Ferme la fenêtre.
ShutdownWindows();
return
;
}
La méthode Run est celle qui met en boucle notre application et effectue tous les traitements jusqu'à ce que nous décidions de quitter l'application. Les traitements sont effectués dans la fonction Frame appelée à chaque itération. Il s'agit d'un concept important à comprendre car le reste de notre application doit être écrit dans cet esprit. Le pseudo-code ressemble à ce qui suit :
tant que la variable 'done' est à false
vérifier les messages système de la fenêtre
traiter les messages système
traiter la boucle de l'application
vérifier si l'utilisateur a voulu fermer l'application durant le traitement
void
SystemClass::
Run()
{
MSG msg;
bool
done, result;
// Initialise la structure de message.
ZeroMemory(&
msg, sizeof
(MSG));
// Boucler jusqu'à obtenir un message de sortie de la fenêtre ou de l'utilisateur.
done =
false
;
while
(!
done)
{
// Gérer les messages de la fenêtre.
if
(PeekMessage(&
msg, NULL
, 0
, 0
, PM_REMOVE))
{
TranslateMessage(&
msg);
DispatchMessage(&
msg);
}
// Si la fenêtre demande à fermer l'application, alors on sort.
if
(msg.message ==
WM_QUIT)
{
done =
true
;
}
else
{
// Autrement, effectuer le traitement de la trame.
result =
Frame();
if
(!
result)
{
done =
true
;
}
}
}
return
;
}
La fonction Frame qui suit est celle qui effectue les traitements de notre application. Jusque-là c'est assez simple, nous interrogeons l'objet Input pour contrôler si l'utilisateur a appuyé sur la touche Échap, voulant fermer l'application. S'il ne veut pas, alors nous utilisons l'objet Graphics pour effectuer les traitements de rendu de cette trame. Au fur et à mesure que notre application évoluera, nous y rajouterons du code.
bool
SystemClass::
Frame()
{
bool
result;
// Vérifie si l'utilisateur a appuyé sur Échap pour fermer l'application.
if
(m_Input->
IsKeyDown(VK_ESCAPE))
{
return
false
;
}
// Effectue le traitement de la trame pour les objets Graphics.
result =
m_Graphics->
Frame();
if
(!
result)
{
return
false
;
}
return
true
;
}
C'est dans la méthode MessageHandler où nous traitons les messages système de la fenêtre. De cette façon, nous pouvons écouter certains évènements qui nous intéressent. Actuellement nous allons juste vérifier si une touche est enfoncée ou relâchée et transmettre cette information à l'objet Input. Tous les autres évènements seront passés au gestionnaire de messages par défaut de Windows.
LRESULT CALLBACK SystemClass::
MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
switch
(umsg)
{
// Vérifie si une touche a été pressée.
case
WM_KEYDOWN:
{
// Si oui, on l'envoie à l'objet Input afin qu'il sauvegarde cet état.
m_Input->
KeyDown((unsigned
int
)wparam);
return
0
;
}
// Vérifie si une touche a été relâchée.
case
WM_KEYUP:
{
// Si oui, on l'envoie à l'objet Input afin qu'il supprime l'état de cette touche.
m_Input->
KeyUp((unsigned
int
)wparam);
return
0
;
}
// Les autres messages sont envoyés au gestionnaire de message par défaut. Nous n'en avons pas besoin dans notre application.
default
:
{
return
DefWindowProc(hwnd, umsg, wparam, lparam);
}
}
}
Dans la méthode InitializeWindows, nous mettons le code pour créer la fenêtre utilisée pour l'affichage. Elle retourne les valeurs screenWidth et screenHeight à la fonction qui l'appelle afin que nous puissions les utiliser dans l'application. À l'aide de paramètres par défaut, nous créons une fenêtre noire sans bordures et l'initialisons. La fonction créera une fenêtreen plein écran ou non selon la variable globale FULL_SCREEN. Si elle est fixée à true alors l'écran couvre toute la fenêtre du bureau de l'utilisateur ; si elle est définie à false nous avons juste une fenêtre 800x600 au milieu de l'écran. J'ai placé la variable globale FULL_SCREEN au début du fichier graphicsclass.h au cas où vous souhaiteriez la modifier. Ce pourquoi je l'ai placée dans ce fichier plutôt que dans l'en-tête de la classe prendra tout son sens plus tard.
void
SystemClass::
InitializeWindows(int
&
screenWidth, int
&
screenHeight)
{
WNDCLASSEX wc;
DEVMODE dmScreenSettings;
int
posX, posY;
// Obtention d'un pointeur externe de cet objet.
ApplicationHandle =
this
;
// Obtention de l'instance de cette application.
m_hinstance =
GetModuleHandle(NULL
);
// Nom de l'application.
m_applicationName =
L"Engine"
;
// Configure la classe de la fenêtre avec les réglages par défaut.
wc.style =
CS_HREDRAW |
CS_VREDRAW |
CS_OWNDC;
wc.lpfnWndProc =
WndProc;
wc.cbClsExtra =
0
;
wc.cbWndExtra =
0
;
wc.hInstance =
m_hinstance;
wc.hIcon =
LoadIcon(NULL
, IDI_WINLOGO);
wc.hIconSm =
wc.hIcon;
wc.hCursor =
LoadCursor(NULL
, IDC_ARROW);
wc.hbrBackground =
(HBRUSH)GetStockObject(BLACK_BRUSH);
wc.lpszMenuName =
NULL
;
wc.lpszClassName =
m_applicationName;
wc.cbSize =
sizeof
(WNDCLASSEX);
// Enregistre la classe de fenêtre.
RegisterClassEx(&
wc);
// Détermine la résolution de l'écran.
screenWidth =
GetSystemMetrics(SM_CXSCREEN);
screenHeight =
GetSystemMetrics(SM_CYSCREEN);
// Configuration, des paramètres de l'écran suivant s'il est exécuté en mode plein écran ou en mode fenêtré.
if
(FULL_SCREEN)
{
// En plein écran, on règle l'écran à la taille maximale du bureau en 32 bits.
memset(&
dmScreenSettings, 0
, sizeof
(dmScreenSettings));
dmScreenSettings.dmSize =
sizeof
(dmScreenSettings);
dmScreenSettings.dmPelsWidth =
(unsigned
long
)screenWidth;
dmScreenSettings.dmPelsHeight =
(unsigned
long
)screenHeight;
dmScreenSettings.dmBitsPerPel =
32
;
dmScreenSettings.dmFields =
DM_BITSPERPEL |
DM_PELSWIDTH |
DM_PELSHEIGHT;
// Modification des paramètres d'affichage en plein écran.
ChangeDisplaySettings(&
dmScreenSettings, CDS_FULLSCREEN);
// Positionnement de la fenêtre dans le coin supérieur gauche..
posX =
posY =
0
;
}
else
{
// En fenêtré, fixer une résolution de 800x600.
screenWidth =
800
;
screenHeight =
600
;
// Centre la fenêtre sur l'écran.
posX =
(GetSystemMetrics(SM_CXSCREEN) -
screenWidth) /
2
;
posY =
(GetSystemMetrics(SM_CYSCREEN) -
screenHeight) /
2
;
}
// Crée la fenêtre selon les paramètres précédents et récupère son identifiant.
m_hwnd =
CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
WS_CLIPSIBLINGS |
WS_CLIPCHILDREN |
WS_POPUP,
posX, posY, screenWidth, screenHeight, NULL
, NULL
, m_hinstance, NULL
);
// Affiche la fenêtre à l'écran et lui donne la main.
ShowWindow(m_hwnd, SW_SHOW);
SetForegroundWindow(m_hwnd);
SetFocus(m_hwnd);
// Cache le pointeur de la souris.
ShowCursor(false
);
return
;
}
ShutdownWindows fait juste ce quelle signifie. Elle remet les paramètres de l'écran à la normale et libère la fenêtre et les identifiants qui lui sont associés.
void
SystemClass::
ShutdownWindows()
{
// Montre le curseur de la souris.
ShowCursor(true
);
// Change les paramètres d'affichage si l'on quitte le mode plein écran.
if
(FULL_SCREEN)
{
ChangeDisplaySettings(NULL
, 0
);
}
// Détruit la fenêtre.
DestroyWindow(m_hwnd);
m_hwnd =
NULL
;
// Détruit l'instance de l'application.
UnregisterClass(m_applicationName, m_hinstance);
m_hinstance =
NULL
;
// Efface toute trace de la classe.
ApplicationHandle =
NULL
;
return
;
}
La méthode WndProc est celle dans laquelle la fenêtre envoie ses messages. Vous remarquerez que nous donnons le nom de la fenêtre lorsque l'on initialise la classe de la fenêtre avec wc.lpfnWndProc = WndProc dans la méthode InitializeWindows ci-dessus. Je l'ai incluse dans ce fichier de classe puisque nous l'utilisons directement dans la classe System en lui faisant envoyer tous les messages à la méthode MessageHandler définie dans SystemClass. Cela nous permet d'attacher les fonctionnalités de messagerie directement dans notre classe et conserver un code propre.
LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
switch
(umessage)
{
// Vérifie si la fenêtre va être détruite.
case
WM_DESTROY:
{
PostQuitMessage(0
);
return
0
;
}
// Vérifie si la fenêtre va être fermée.
case
WM_CLOSE:
{
PostQuitMessage(0
);
return
0
;
}
// Tous les autres messages sont passés au gestionnaire de messages de la classe système.
default
:
{
return
ApplicationHandle->
MessageHandler(hwnd, umessage, wparam, lparam);
}
}
}
VII. Inputclass.h▲
Pour garder les tutoriels simples, j'ai réservé les entrées fenêtre jusqu'au moment où je ferai un tutoriel sur DirectInput (qui est de loin supérieur). La classe Input gère la saisie au clavier de l'utilisateur. Cette classe est donnée en entrée de la fonction SystemClass::MessageHandler. L'objet Input va stocker l'état de chaque touche dans un tableau de touches. Lorsqu'interrogé, il renseignera les fonctions appelantes si une certaine touche est enfoncée. Voici l'en-tête :
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : inputclass.h
///
/////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_
///
/////////////////////////////////////////////////////////////////////////////
// Nom de la classe : InputClass
///
/////////////////////////////////////////////////////////////////////////////
class
InputClass
{
public
:
InputClass();
InputClass(const
InputClass&
);
~
InputClass();
void
Initialize();
void
KeyDown(unsigned
int
);
void
KeyUp(unsigned
int
);
bool
IsKeyDown(unsigned
int
);
private
:
bool
m_keys[256
];
}
;
#endif
VIII. Inputclass.cpp▲
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : inputclass.cpp
///
/////////////////////////////////////////////////////////////////////////////
#include
"inputclass.h"
InputClass::
InputClass()
{
}
InputClass::
InputClass(const
InputClass&
other)
{
}
InputClass::
~
InputClass()
{
}
void
InputClass::
Initialize()
{
int
i;
// Initialise toutes les touches comme relâchées, et non pressées.
for
(i=
0
; i<
256
; i++
)
{
m_keys[i] =
false
;
}
return
;
}
void
InputClass::
KeyDown(unsigned
int
input)
{
// Si une touche est pressée, on sauvegarde son état dans le tableau.
m_keys[input] =
true
;
return
;
}
void
InputClass::
KeyUp(unsigned
int
input)
{
// Si une touche est relâchée, on supprime cet état du tableau.
m_keys[input] =
false
;
return
;
}
bool
InputClass::
IsKeyDown(unsigned
int
key)
{
// Retourne l'état de la touche (pressée/relâchée).
return
m_keys[key];
}
IX. Graphicsclass.h▲
La classe System crée également un objet Graphics. Toutes les fonctionnalités graphiques de cette application seront encapsulées dans cette classe. J'utiliserai aussi l'en-tête de ce fichier pour tous les paramètres graphiques globaux qui nous souhaiterons changer comme le mode plein écran ou fenêtré. Actuellement, cette classe est vide, mais dans les tutoriels à venir, elle contiendra tous les objets graphiques.
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.h
///
/////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_
///
///////////
// INCLUDES //
///
///////////
#include
<windows.h>
///
//////////
// GLOBALS //
///
//////////
const
bool
FULL_SCREEN =
false
;
const
bool
VSYNC_ENABLED =
true
;
const
float
SCREEN_DEPTH =
1000.0
f;
const
float
SCREEN_NEAR =
0.1
f;
Pour commencer, nous aurons besoin de ces quatre variables globales.
///
/////////////////////////////////////////////////////////////////////////////
// Nom de la classe : GraphicsClass
///
/////////////////////////////////////////////////////////////////////////////
class
GraphicsClass
{
public
:
GraphicsClass();
GraphicsClass(const
GraphicsClass&
);
~
GraphicsClass();
bool
Initialize(int
, int
, HWND);
void
Shutdown();
bool
Frame();
private
:
bool
Render();
private
:
}
;
#endif
X. Graphicsclass.cpp▲
J'ai gardé cette classe entièrement vide pour l'instant, étant donné que nous sommes en train de construire le squelette dans ce tutoriel.
///
/////////////////////////////////////////////////////////////////////////////
// Nom du fichier : graphicsclass.cpp
///
/////////////////////////////////////////////////////////////////////////////
#include
"graphicsclass.h"
GraphicsClass::
GraphicsClass()
{
}
GraphicsClass::
GraphicsClass(const
GraphicsClass&
other)
{
}
GraphicsClass::
~
GraphicsClass()
{
}
bool
GraphicsClass::
Initialize(int
screenWidth, int
screenHeight, HWND hwnd)
{
return
true
;
}
void
GraphicsClass::
Shutdown()
{
return
;
}
bool
GraphicsClass::
Frame()
{
return
true
;
}
bool
GraphicsClass::
Render()
{
return
true
;
}
XI. Résumé▲
Nous avons donc maintenant une base de travail et une fenêtre qui apparaît à l'écran. Ce cadre va servir de base pour tous les tutoriels à venir. Sa compréhension est donc assez importante. S'il vous plaît essayez de faire l'exercice afin de vous assurer que le code compile et fonctionne avant de passer au prochain tutoriel. Si vous ne comprenez pas cette base de travail, avancez sur les autres tutoriels, ils pourront vous aider à mieux comprendre ce squelette lors de son utilisation.
XII. Exercices à faire▲
Changez le paramètre FULL_SCREEN à true dans l'en-tête graphicsclass.h puis recompilez et exécutez le programme. Appuyez sur la touche Échap pour quitter l'application une fois la fenêtre affichée.
XIII. Code source▲
Projet Visual Studio 2010 : dx11tut02.zip
Source seule : dx11src02.zip
Executable seul : dx11exe02.zip
XIV. Remerciements▲
Cet article est une traduction autorisée de l'article paru sur RasterTek.
Navigation▲
Tutoriel précédent : Mise en place de DirectX 11 avec Visual Studio | Sommaire | Tutoriel suivant : Initialisation de DirectX 11 |