DirectX 10/11 Tutorial 1: Das Fenster

Hier können Artikel, Tutorials, Bücherrezensionen, Dokumente aller Art, Texturen, Sprites, Sounds, Musik und Modelle zur Verfügung gestellt bzw. verlinkt werden.
Forumsregeln
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
Antworten
Gelöschter Benutzer
Beiträge: 92
Registriert: 26.02.2009, 23:09

DirectX 10/11 Tutorial 1: Das Fenster

Beitrag von Gelöschter Benutzer » 01.03.2009, 22:46

Ein Hallo und herzlich willkommen zum Tutorial zu DirectX 10. In diesem Tutorial werden alle wichtigen Elemente von DirectX 10 und 11 vorgestellt, damit man diese zum eigenen Nutzen verwenden kann.

Damit man mit diesem Tutorial arbeiten kann, muss man sich mit C++ auskennen.
Andernfalls hilft Galileo Computing
:).

In dieser Dokumentation gilt übrigens für allgemeine Funktionen: MSDN und Google sind beste Freunde :).
Ziel der Tutorials ist es hier übrigens nicht Code vorzukauen sondern Vorgehensweisen zu zeigen, um daraus selbstständig Wissen zu schaffen. (Wie im echten Leben.)[/color][/b]

Bevor wir beginnen

Man sollte strikt zwischen einer Game Engine und einer eigenen Implementation einer Game Engine unterscheiden. Während es bei einer fertigen Game Engine oder einem Engine-Paket eher darum geht, fertige Codeteile zusammenzuklastern, geht es in der eigenen Game Engine eher darum, den Beton und das Klebemittel für das Zusammenklastern zu schaffen. Wer also möglichst schnell ein Spiel herzaubern will, ist hier falsch positioniert und sollte sich eher mit Bibliotheken wie 3D Gamestudio befassen. Wer Klebemittel erzeugen lernen will ist hier allerdings richtig.

Im ersten Tutorial geht es darum einfach nur ein Fenster zu erzeugen: ganz unobjektorientiert, damit man ein Gefühl für DirectX erstmal bekommt. Ich könnte natürlich auch auf die objektorientierte Schiene fahren - allerdings wirkt das dann doch eher abschreckend. Wir werden zum späteren Zeitpunkt unser Framework objektorientiert zusammenfassen und nicht jetzt.

Vorbereitung

Am Besten fangen wir damit an ein Projekt zu erzeugen. Starte hierzu Visual Studio, wähle dann "Neues Projekt", wähle VisaulC++ und ein Win32-Projekt - keine Konsolenanwendung. Du wählst den Pfad und es öffnet sich folgt wieder ein Fenster. Hier wählt man einfach im zweiten Punkt eine Windows-Anwendung und erstellt ein leeres Projekt. Danach gibt es so gesehen ein nacktes Projekt und es ist eine ziemliche Mundwüste. Keine Angst: Alles wird noch schön feucht. Ich gehe auch davon aus, dass man mit der IDE schon vertraut ist, da ja C++-Kenntnisse vorausgesetzt werden.

Man erstellt eine "main.cpp" und mit dieser Datei können wir uns jetzt hier schon zufrieden geben. Nun ja, nicht ganz: Es fehlt der Quellcode. Darum zeige ich mal, was wir machen müssen:
zfx_tut_01_window_1.png
Ablauf des Programms
zfx_tut_01_window_1.png (31.75 KiB) 10999 mal betrachtet
Implementation

Fangen wir mit unseren Globalen Variablen an: Da wir eine Windows-Anwendung erzeugen wollen, benötigen wir eine windows.h. Zudem benötigen wir einen globalen Alias zu unserem Fenster - der ist vom Typ HWND (Handle to a Window):

Code: Alles auswählen

// Header-Datei für die Windowsfunktionen
#include <windows.h>

HWND hWnd;	// Das Alias zum Fenster
Unser Programm startet mit einer WinMain()-Funktion. Diese ist so ziemlich gleich in jedem Programm, man kann allerdings auch eigene Variablennamen festlegen. In dieser Funktion werden wir oben genannte Schritte durchführen:

Code: Alles auswählen

int WINAPI WinMain(HINSTANCE hInstance /* Unser Alias zur Anwendung */,
				   HINSTANCE hPrevInstance /* Ehemaliger Alias */,
				   LPSTR lpCmdLine /* Kommandozeile, wenn vorhanden */,
				   int nShowCmd /* Art, wie das Fenster gestartet werden soll */)
{
	// Fenster erstellen & Anwendung registrieren
	if(!AssignApplicationAndCreateWindow(hInstance)) return 0;

	// Die Hauptschleife
	while(RunWindow())
	{
		// Hier der Programmcode der Hauptschleife
	}

	// Programm verlassen
	return 0;
}
Wenn wir also unser Propramm betreten, müssen wir unsere Anwendung mit Windows vertraut machen lassen. Dies erledigen wir in der Funktion bool AssignApplicationAndCreateWindow(HINSTANCE hInstance);. Dazu müssen wir als erstes eine WNDCLASS-Struktur füllen und registrieren lassen:

Code: Alles auswählen

	// WNDCLASS-Struktur für die Applikationsbeschreibung für Windows füllen
	WNDCLASS wc = {0}; // << Auch gleich mit 0 vorinitialisieren
	wc.hbrBackground = HBRUSH(GetStockObject(BLACK_BRUSH));	// Schwarzer Hintergrund
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);				// Normaler Cursor
	wc.hIcon = LoadIcon(NULL, IDC_ARROW);					 // Anwendungsicon
	wc.hInstance = hInstance;							       // Alias der Anwnedung übergeben
	wc.lpfnWndProc = WndProc;							       // Unsere Callback-Funktion
	wc.lpszClassName = L"MyZFXApp";						    // Identifikationsname
	wc.style = CS_HREDRAW|CS_VREDRAW;					     // Anwendungsstil
	// Nun Applikation registrieren
	RegisterClass(&wc);
Interessant sind hier für uns jediglich zwei Variablen: lpfnWndProc und lpszClassName. Wir betrachten erstmal Zweitere: Diese gibt einen einen Namen unserer Instanz an. Der erste ist allerdings eine Callback-Funktion, die die Nachrichten einer Anwendung verarbeitet. Diese wird später eine besondere Bedeutung haben. Doch weiter im Gang hier --> wir erstellen das Fenster:

Code: Alles auswählen

// Das Fenster erstellen
	if(NULL==(hWnd = CreateWindowEx(NULL /* Kein Stil */,
									wc.lpszClassName /* Der Identifikationsname */,
									L"ZFX Tutorial Application" /* Fenstertitel */,
									WS_VISIBLE|WS_POPUP /* Aussehen des Fensters */,
									0, 0,               // Startposition (x, y)
									GetSystemMetrics(0) /* X-Auflösung des Users */,
									GetSystemMetrics(1) /* Y-Auflösung des Users */,
									NULL,        // Ist von keinem Fenster abhängig
									NULL,        // Kein Menü
									hInstance,	// Anwendungsalias
									NULL)))	   // Initialnachricht an das Elternfenster
		return false;

	return true;
GetSystemMetrics() gibt uns Informationen über die Auflösung des Benutzers und die Größe sonstiger Elemente zurück. Ändern wir hier die Konstante WS_POPUP zu WS_OVERLAPPEDWINDOW, so erhalten wir ein normales Windowsfenster mit den bekannten Schaltflächen in Schwarz. MSDN bietet hier viele Informationen über mögliche Konstanten.

Jetzt sind wir gezwungen uns mit der WndProc-Callbackfunktion zu beschäftigen. Zur Erläuterung erstmal der Quellcode:

Code: Alles auswählen

// Unsere Callback-Funktion zum Auswerten der Fensternachrichten
LRESULT CALLBACK WndProc(HWND hWnd /* Das Fenster */, UINT Msg /* Die Nachricht */,
						 WPARAM wParam, LPARAM lParam) // Jeweils Eingabewerte
{
	// Wenn eine Beendigungsnachricht kommt beenden:
	if(Msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
	
	// Wenn ESC gedrückt wurde, Anwendung auch verlassen:
	if(Msg == WM_KEYDOWN && wParam == VK_ESCAPE) { PostQuitMessage(0); return 0; }

	// Andernfalls Nachricht einfach verarbeiten:
	return DefWindowProc(hWnd, Msg, wParam, lParam);
}
Wir prüfen hier einfach mittels Konstanten, ob gewisse Informationen auftreten und reagieren darauf. Drückt man ESC oder verlässt man die Anwendung, so wird das Programm beendet. Andernfalls wird die Nachricht mittels DefWindowProc() normal abgearbeitet. Da wir auch nicht tiefer in die Fenster-Programmierung eingehen - müssen wir uns mit dieser Funktion auch nicht zutiefst beschäftigen.

Zuletzt beschäftigen wir uns mit der Auswertung der Nachricht: Der RunWindow()-Funktion.

Code: Alles auswählen

bool RunWindow()
{
	MSG msg = {0};
	// Nachricht verarbeiten, falls vorhanden
	while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
	{
		// Wenn das Programm beendet werden soll, Hauptschleife verlassen
		if(msg.message == WM_QUIT) return false;

		// Nachricht verarbeiten
		DispatchMessage(&msg);
		TranslateMessage(&msg);
	}

	return true;
}
Wir holen hier mittels PeekMessage() die Nachricht. PeekMessage() statt GetMessage() deshalb, weil GetMessage() auf eine Nachricht wartet und PeekMessage() nur schaut, ob ggf. ein Nachricht vorliegt und entsprechend reagiert. Würden wir GetMessage() verwenden, so hätten wir ernsthafte Probleme später mit DirectX und den Zeichenzyklen. Wenn wir nun eine Nachricht haben sollten, so prüfen wir einfach, ob die Nachricht ein Beenden des Programms wünscht und verlassen ggf. mit return false oder verarbeiten einfach diese Nachricht.

Man muss im Hinterkopf behalten, dass das Programm nur solange läuft, wie wir immer true zurückliefern, da wir while(RunWindow()) geschrieben haben.

In der nun vorhanden Hauptschleife des Programms würden wir nun unser Programm laufen lassen und KI, Grafik, Eingaben, Sound etc. verwalten. Aber das ist Thema eines späteren Tutorials.

Kompilieren wir das Programm, so sollte sich ein schwarzes Fenster über den gesamten Desktop ausbreiten und per ESC beenden lassen.

Für alle gibt es nochmal den gesamten Quellcode hier kompakt:

Code: Alles auswählen

#pragma region Globale Variablen
// Header-Datei für die Windowsfunktionen
#include <windows.h>

HWND hWnd;	// Das Alias zum Fenster
#pragma endregion

#pragma region Callbackfunktion: Auswerten der Fensternachrichten
// Unsere Callback-Funktion zum Auswerten der Fensternachrichten
LRESULT CALLBACK WndProc(HWND hWnd /* Das Fenster */, UINT Msg /* Die Nachricht */,
						 WPARAM wParam, LPARAM lParam) // Jeweils Eingabewerte
{
	// Wenn eine Beendigungsnachricht kommt beenden:
	if(Msg == WM_DESTROY) { PostQuitMessage(0); return 0; }
	
	// Wenn ESC gedrückt wurde, Anwendung auch verlassen:
	if(Msg == WM_KEYDOWN && wParam == VK_ESCAPE) { PostQuitMessage(0); return 0; }

	// Andernfalls Nachricht einfach verarbeiten:
	return DefWindowProc(hWnd, Msg, wParam, lParam);
}
#pragma endregion

#pragma region Funktion: Ein Fenster erstellen
bool AssignApplicationAndCreateWindow(HINSTANCE hInstance)
{
	// WNDCLASS-Struktur für die Applikationsbeschreibung für Windows füllen
	WNDCLASS wc = {0}; // << Auch gleich mit 0 vorinitialisieren
	wc.hbrBackground = HBRUSH(GetStockObject(BLACK_BRUSH));	// Schwarzer Hintergrund
	wc.hCursor = LoadCursor(NULL, IDC_ARROW);				// Normaler Cursor
	wc.hIcon = LoadIcon(NULL, IDC_ARROW);					// Anwendungsicon
	wc.hInstance = hInstance;							// Alias der Anwnedung übergeben
	wc.lpfnWndProc = WndProc;							// Unsere Callback-Funktion
	wc.lpszClassName = L"MyZFXApp";						// Identifikationsname
	wc.style = CS_HREDRAW|CS_VREDRAW;					// Anwendungsstil
	// Nun Applikation registrieren
	RegisterClass(&wc);	

	// Das Fenster erstellen
	if(NULL==(hWnd = CreateWindowEx(NULL /* Kein Stil */,
									wc.lpszClassName /* Der Identifikationsname */,
									L"ZFX Tutorial Application" /* Fenstertitel */,
									WS_VISIBLE|WS_POPUP /* Aussehen des Fensters */,
									0, 0,
									GetSystemMetrics(0) /* X-Auflösung des Users */,
									GetSystemMetrics(1) /* Y-Auflösung des Users */,
									NULL,		// Ist von keinem Fenster abhängig
									NULL,		// Kein Menü
									hInstance,	// Anwendungsalias
									NULL)))		// Initialnachricht an das Elternfenster
		return false;

	return true;
}
#pragma endregion

#pragma region Funktion: Eine Nachricht in der Hauptschleife verarbeiten
bool RunWindow()
{
	MSG msg = {0};
	// Nachricht verarbeiten, falls vorhanden
	while ( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
	{
		// Wenn das Programm beendet werden soll, Hauptschleife verlassen
		if(msg.message == WM_QUIT) return false;

		// Nachricht verarbeiten
		DispatchMessage(&msg);
		TranslateMessage(&msg);
	}

	return true;
}
#pragma endregion

#pragma region Unsere Startfunktion
int WINAPI WinMain(HINSTANCE hInstance /* Unser Alias zur Anwendung */,
				   HINSTANCE hPrevInstance /* Ehemaliger Alias */,
				   LPSTR lpCmdLine /* Kommandozeile, wenn vorhanden */,
				   int nShowCmd /* Art, wie das Fenster gestartet werden soll */)
{
	// Fenster erstellen & Anwendung registrieren
	if(!AssignApplicationAndCreateWindow(hInstance)) return 0;

	// Die Hauptschleife
	while(RunWindow())
	{
		// Hier der Programmcode der Hauptschleife
	}

	// Programm verlassen
	return 0;
}

#pragma endregion
Es gibt das Projekt als Archiv im Anhang zusammengepackt.

Benötigte Bibliotheken: Visual Studio Windowsbibliotheken (Standard)
Benötigte Headerfiles: windows.h

Schlusswort

Nun ist man in der Lage ein Fenster zu erzeugen. Ausgangspunkt einer fast jeden 3D-Anwendung. Es ist ein kleiner Schritt für die Gesamtheit der Themen, aber ein großer Schritt für den Einsteiger!

Zusatzaufgaben

1. Füge der Anwendung eine Titelleiste hinzu.
2. Ändere den Titel zu "Rock'n'DirectX"
3. Ändere die Hintergrundfarbe zu weiß
3. Lass das Programm auch mit E beenden
Dateianhänge
ZFXTutorial1_Fenster.zip
Das ganze Projekt in einem Archiv
(24.77 KiB) 555-mal heruntergeladen
Zuletzt geändert von Gelöschter Benutzer am 02.03.2009, 15:41, insgesamt 2-mal geändert.

Benutzeravatar
Krishty
Establishment
Beiträge: 7090
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: DirectX 10/11 Tutorial 1: Das Fenster

Beitrag von Krishty » 02.03.2009, 14:40

Code: Alles auswählen

   // Nachricht verarbeiten, falls vorhanden
   if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
Müsste das if() an dieser Stelle kein while() sein? Es könnten doch auch durchaus mehrere Nachrichten pro Durchlauf ankommen.
(So steht es zumindest auch in der MSDN).

Gruß, Ky
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Gelöschter Benutzer
Beiträge: 92
Registriert: 26.02.2009, 23:09

Re: DirectX 10/11 Tutorial 1: Das Fenster

Beitrag von Gelöschter Benutzer » 02.03.2009, 15:39

Stimmt, mehrere Nachrichten könnten eintreffen. Ist mir allerdings in den 3 Jahren nie vorgekommen bei undefiniert vielen Anwendungen. Die Chance betrachte ich hierfür auch für Gering bei 16ms pro Frame und dann zwei gleichzeitigen Eingaben.

Das steht dann ja tatsächlich in allen Fachbüchern auf Deutsch und Englisch falsch... . Zur Sicherheit und Richtigkeit ändere ich das im Code allerdings vorsichtshalber.

Benutzeravatar
Krishty
Establishment
Beiträge: 7090
Registriert: 26.02.2009, 12:18
Benutzertext: state is the enemy
Kontaktdaten:

Re: DirectX 10/11 Tutorial 1: Das Fenster

Beitrag von Krishty » 02.03.2009, 15:44

Naja … Nachrichten können ja auch etwas anderes sein Tastatureingaben … und wenn dann in der Nachrichtenschlange bspw. fünf solcher nicht-Eingaben stecken, reagiert die Anwendung auf Eingaben erst fünf Frames später. Insofern glaube ich nicht, dass es nie vorgekommen ist, sondern dass du die sporadischen paar Frames Verzögerung nie bewusst wahrgenommen hast ;)
seziert Ace Combat, Driver, und S.T.A.L.K.E.R.   —   rendert Sterne

Antworten