Tutorial: DirectX - DirectDraw 7
I. Einleitung
Direct Draw ist ein wichtiges Werkzeug, dass es Programmierern ermöglichen soll, ohne viel Aufwand, extrem schnelle 2D Grafikoperationen auszuführen. Aber nicht nur für 2D Grafiken wird DirectDraw verwendet. Es wird auch gebraucht, wenn man mit Direct3D arbeiten möchte. Ich möchte Ihnen in diesem Tutorial einen kleinen Einstieg in die Welt von DirectDraw ermöglichen und hoffe, dass Sie mit den gebotenen Informationen das Wissen erhalten um in Zukunft die Performance und Flexibilität dieser Schnittstelle in Ihren Anwendungen einsetzen zu können!
II. DirectDraw in ein Visual Basic Projekt einbinden
Der erste Schritt bei einer Anwendung die mit DDraw arbeiten soll ist immer das einbinden der DirectX Typen und Klassenbibliothek. Denn nur über diese bekommen Sie Zugriff auf die Klassen und Funktionen von DirectX und somit auch von DirectDraw. Klicken Sie also nachdem Sie ein neues Projekt erstellt haben zunächst auf "Projekt/Verweise". Hier müssen Sie in der Liste nach dem Eintrag "DirectX 7 for Visual Basic Type Library" suchen und das entsprechende Kontrollkästchen aktivieren. Das war bereits alles!
III. Ein grober Überblick über den Aufbau von DirectDraw
Bevor wir jedoch mit dem ersten Beispiel beginnen können ist leider erst einmal etwas Theorie erforderlich, um zu begreifen, wie DirectDraw arbeitet. Das wichtigste in DirectDraw sind die so genannten Surfaces (aus dem Englischen und bedeutet soviel wie Oberfläche). Diese können Sie sich einfach als Grafik (fester Größe) vorstellen die (wie die PictureBox in VB) ein X/Y Koordinatensystem besitzt, das in der linken, oberen Ecke beginnt. Surfaces bilden die Grundlage von DirectDraw. Auf ihnen läuft jede Grafikoperation ab. Dabei haben Sie verschiedene Möglichkeiten, Bereiche von einer Surface in eine andere zu kopieren (dies nennt man blitten). Dabei können auch verschiedene Optionen eingestellt werden (transparente Farbe, invertieren, ...). Das ist im Prinzip auch schon alles, was Sie für 2D Spiele benötigen! Alle Surfaces, deren Inhalt (die Grafik) nicht auf dem Bildschirm angezeigt wird (und das sind so gut wie alle), die also nur im Speicher existieren, nennt man Offscreensurfaces. Eine besondere Stellung nimmt hierbei das so genannte Primary Surface ein. Der Inhalt dieses Surfaces ist genau das Bild, was im Moment auf dem Bildschirm angezeigt wird. Über dieses Surface können Sie übrigens auch das angezeigte Bild verändern. Allerdings geht das nur indirekt. Sie können eine Grafik nicht direkt auf die Primary Surface blitten (das würde zum Beispiel bei Spielen zum Flimmern führen und das ist natürlich nicht erwünscht). Vielmehr benutzen Sie dabei einen so genannten Backbuffer. Dieser ist einfach nur eine weitere Surface, deren Größe mit der der Primary Surface übereinstimmt. Auf diesem Backbuffer stellen Sie nun zunächst das Bild das angezeigt werden soll zusammen (durch blitten von anderen Offscreensurfaces). Wenn alles fertig gezeichnet ist, tauschen Sie einfach die Primary Surface mit dem Backbuffer aus. Der Backbuffer enthält dann das Bild, das vorher angezeigt wurde und das jetzt angezeigt Bild ist das was Sie vorher zusammengestellt haben. Jetzt können Sie den Inhalt des Backbuffer wieder neu aufbauen und dann erneut mit der Primary Surface vertauschen (dieser Vorgang heißt übrigens flippen) usw.... Man sieht in der DDraw Welt Backbuffer und Primary Surface als eine Einheit an, die zusammen als "Komplexe Surface" bezeichnet wird.
Ein letztes Wort zu der Performance: Surfaces sind einfach nur gewisse Stellen im Speicher die eine Grafik enthalten. Es ist möglich (und das ist auch im Normalfall so), dass Surfaces nicht im Hauptspeicher des Systems abgelegt werden sondern im Speicher auf der Grafikkarte (Videospeicher). Wenn man dem entsprechend vorgeht, und alle Surfaces im Grafikkartenspeicher liegen, dann können anfallende Kopierarbeiten direkt von dem Prozessor der Grafikkarte übernommen werden. Dadurch wird die CPU extrem entlastet und das Programm (Spiel) läuft wesentlich schneller ab. Logischerweise liegen die Primary Surface und der Backbuffer immer im Videospeicher. Ich sprach vorhin das Flippen an mit dem der Inhalt des Backbuffers und der primären Surface direkt getauscht werden können. Bei modernen Grafikkarten ist das übrigens nochmals schneller möglich, weil deren Speicher die Möglichkeit unterstützt, einfach nur den Zeiger auf die anzuzeigende Speicherstelle zu ändern. Die Grafikkarte kann dann, wenn geflippt werden soll den Zeiger ändern und muss gar keine Daten mehr kopieren. Dadurch wird das Programm deutlisch schneller.
IV. Ein erstes Beispiel zu DirectDraw
So! Das war jetzt zu Beginn ein wenig Theorie, die Sie für den Einstieg brauchen. Jetzt wollen wir endlich zur Tat schreiten, damit Sie auch sehen, was die Begriffe im einzelnen bedeuten und wie man sie einsetzt. Wir wollen zu Beginn also ein ganz einfaches Beispiel schreiben: Wir werden eine Primary Surface und einen Backbuffer erstellen, diese zu einer Komplexen Surface verknüpfen. Dann werden wir eine Offscreen Surface erstellen, ein Bild hineinladen und dieses dann erst auf den Backbuffer kopieren und danach durch flippen anzeigen. Letztendlich wird einfach nur ein Bild auf Ihrem Bildschirm erscheinen. Ich empfehle Ihnen übrigens, den hier aufgeführten Code nicht einfach nur mit der Zwischenablage in Visual Basic einzufügen. Vielmehr sollten Sie den Code Zeile für Zeile abtippen und versuchen die Zeilen auch zu verstehen. Denn nur dadurch lernen Sie wirklich mit DDraw zu programmieren (Learning by doing).
Da DirectDraw zu DirectX gehört benötigen wir als erstes ein DirectX Objekt. Über dieses werden wir später unser DirectDraw Objekt erstellen und erst über dieses DDraw Objekt können dann alle Aufgaben, die wir zu bewerkstelliegen haben, ablaufen (Übrigens werden Sie weiter unten noch einmal alle Codes die hier einzeln beschrieben werden zusammengefasst antreffen und natürlich haben Sie auch die möglichkeit sich die einzelnen Beispiel herunterzuladen):
Dim DirectX As DirectX7
Dim DirectX New DirectX7
Jetzt können wir über das DirectX Objekt ein DirectDraw Objekt erstellen:
Dim DDraw As DirectDraw7
Dim DDraw = DirectX.DirectDrawCreate("")
Wir haben nun bereits alle Objekte, die wir brauchen. Deshalb deklarieren wir jetzt noch ein paar Variablen für unsere Surfaces. Wir brauchen drei Surfaces: Eine Primary Surface, einen Backbuffer und eine Offscreensurface. Für jede Surface benötigt man in DDraw zwei Variablen. Eine enthält alle Eigenschaften der Surface und eine alle Funktionen, die Sie mit der Surface ausführen können. Also deklarieren wir im ganzen sechs Variablen:
Dim Primary_Func As DirectDrawSurface7 'Funktionen der Surface
Dim Primary_Eige As DDSURFACEDESC2 'Eigenschaften der Surface
Dim Backbuffer_Func As DirectDrawSurface7
Dim Backbuffer_Eige As DDSURFACEDESC2
Dim Offscreen_Func As DirectDrawSurface7
Dim Offscreen_Eige As DDSURFACEDESC2
Als nächstes müssen wir zwei Funktionen unseres DirectDraw Objekts aufrufen. Wir müssen DirectDraw mitteilen, wie es mit dem Display interagieren soll. An dieser Stelle entscheidet sich, ob wir eine Vollbildanwendung schreiben oder eine Fensteranwendung. Außerdem müssen Sie die Auflösung, Farbtiefe und Bildwiederholfrequenz festlegen, wenn Sie sich für den Vollbildmodus entscheiden. In diesem Beispiel entscheiden wir uns für eine Vollbildanwendung, weil DDraw in diesem Modus die beste Performance bietet (nur im Vollbildmodus hat DDraw den alleinigen - exklusiven - Zugriff auf die Grafikkarte). Am Ende dieses Tutorials werde ich in einem Kapitel noch kurz beschreiben, wie man Fensteranwendungen schreibt. Doch zunächst sagen wir DDraw das wir einen Vollbildmodus wollen, bei einer Auflösung von 640x480x16 und der Standartwiederholfrequenz. Ich musste übrigens feststellen, dass normalerweise Farbtiefen von 16bit oder 24bit vollkommen ausreichen. 32bit macht die Anwendung nur langsamer und verschlechtert die Grafik (bei transparenz) mit manchen Grafikkarten sogar! Also wählen wir in unseren Beispielen 16bit:
DDraw.SetCooperativeLevel Formular.hWnd, DDSCL_FULLSCREEN Or DDSCL_ALLOWMODEX Or DDSCL_EXCLUSIVE
DDraw.SetDisplayMode 640, 480, 16, 0, DDSDM_DEFAULT
Mit SetCoorperativeLevel sagen wir DDraw, wie es mit dem Display interagieren soll. Dazu benötigen wir erst einmal die Zugriffsnummer (Hwnd) eines Formulars. Mit diesem Formular wird später gearbeitet. Sie können daher ihre Main Schleife in das Formular legen und auch Tastaturereignisse im Formular abfangen und verarbeiten. Wir setzen die Parameter DDSCL_FULLSCREEN (Vollbildmodus), DDSCL_ALLOWMODEX (Damit wir ModeX Auflösungen einstellen können) und DDSCL_EXCLUSIVE (DDraw hat den exklusiven Zugriff auf die Grafikkarte). Mit SetDisplayMode sagen wir DirectDraw die Auflösung, Farbtiefe und Wiederholfrequenz. Die ersten zwei Parameter sind X und Y Zahl der Pixel, der dritte Wert ist die Farbtiefe. Der vierte Parameter sollte 0 sein, um DDraw zu sagen, dass es die Standartwiederholfrequenz verwenden soll und der letzte besagt, dass wir einen ModeX eingestellt haben (die andere Option wäre der Mode 13. Dieser bringt jedoch etliche Nachteile und deshalb gehe ich hier nicht näher darauf ein). Nun da wir das erledigt haben, können wir endlich die Surfaces initialisieren. Zunächst einmal werden wir die Primary Surface und den Backbuffer erstellen und diese beiden zu einer Komplexen Surface verknüpfen (weiter Infos siehe 3):
Dim Caps As DDSCAPS2
Primary_Eige.lFlags = DDSD_CAPS Or DDSD_BACKBUFFERCOUNT
Primary_Eige.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE Or DDSCAPS_COMPLEX Or DDSCAPS_FLIP
Primary_Eige.lBackBufferCount = 1
Set Primary_Func = DDraw.CreateSurface(Primary_Eige)
Caps.lCaps = DDSCAPS_BACKBUFFER
Set Backbuffer_Func = Primary_Func.GetAttachedSurface(Caps)
Backbuffer_Func.GetSurfaceDesc Backbuffer_Eige
Das eigentliche Erstellen der Surface wird mit der Anweisung "Set Primary_Func" ausgelöst. Wie Sie sehen stellen wir zunächst einige Eigenschaften der Surface neu ein. Damit dies überhaupt möglich ist müssen wir DDraw sagen, welche Eigenschaften wir umstellen möchten. Die benötigten Informationen geben wir an DDraw über die Eigenschaft LFlags weiter. Hier müssen wir alle Eigenschaften eintragen, die wir im Folgenden ändern möchten. Da wir gedenken die Caps und Backbuffercount Eigenschaft zu verändern tragen wir in unserem Beispiel diese beiden Werte ein. Danach können wir über die lCaps Eigenschaft festlegen, dass es eine Primary Surface werden soll (DDSCAPS_PRIMARYSURFACE), dass wir eine komplexe Surface generieren möchten (DDSCAPS_COMPLEX) und dass wir gedenken mit der Flip Technik zu arbeiten (DDSCAPS_FLIP). Den Backbuffercount setzen wir auf 1. Dies muss nicht unbedingt sein! Man kann auch komplexe Surface erstellen, die mit mehreren Backbuffern arbeiten. Das ist in den meisten Fällen allerdings nicht sinnvoll und macht eine Andwendung nur unnötig kompliziert, deshalb möchte ich hier auch darauf verzichten und werde immer nur mit einem Backbuffer arbeiten (diese Technik nennt man übrigens Doublebuffer). Jetzt haben wir alle Eigenschaften festgelegt und können die Primäre Suface erstellen. Wir brauchen aber noch den Backbuffer. Deshalb erstellen wir uns eine Variable vom Typ DDSCAPS2, in die wir die entsprechenden Eigenschaften eintragen (diesesmal wollen wir nicht mit der eigentlichen Eigenschaftsvariablen dieser Surface arbeiten). Dann können wir über die Funktion GetAttechedSurface der Primären Surface den Backbuffer erstellen. Als letztes holen wir uns noch alle Eigenschaften des Backbuffersurfaces in die entsprechende Variable. Das machen wir über die Funktion GetSurfaceDesc. Schon haben wir unsere Komplexe Surface erstellt. Fehlt nur noch die Offscreensurface. Ich möchte an dieser Stelle bemerken, das ich, wenn ich eine DDraw Anwendung schreibe, die eben beschriebenen Codezeilen stets direkt über die Zwischenablage neu einfüge und nur die Namen der Variablen ändere. Denn die Anweisungen bleiben immer gleich. So können Sie sich ebenfalls Arbeit ersparen. Aber jetzt kommen wir, wie gesagt zu unserem Offscreensurface. Es gibt mehrere Möglichkeiten solche Surfaces zu erstellen. Wir werden uns in diesem Tutorial zwei von ihnen widmen: Erstellen eines leeren Surfaces, in dem noch keine Grafik ist und Erstellen aus einer Bilddatei (beispielsweise .BMP) heraus. In diesem Beispiel hier möchten wir ein Bild anzeigen. Deshalb erstellen wir unser eines Offscreensurface direkt aus der Bilddatei. Das bringt den Vorteil, dass die Größe (Höhe/Breite) der Surface automatisch an die Bildgröße angepasst wird.
Offscreen_Eige.lFlags = DDSD_CAPS
Offscreen_Eige.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
Set Offscreen_Func = DDraw.CreateSurfaceFromFile("F:\Eigene Dateien\c++.bmp", Offscreen_Eige)
Sie sehen, auch hier stellen wir zunächst eine Eigenschaft um: Die lCaps Eigenschaft bekommt den Wert DDSCAPS_OFFSCREENPLAIN. Das soll DirectDraw mitteilen, dass es sich hierbei um ein Offscreensurface handelt. Da wir eine Eigenschaft umstellen, müsen wir DDraw das vorher wieder ankündiegen (deshalb die erste Zeile). Als letztes initialisieren wir die Surface aus einer Datei unter Verwendung der Eigenschaftenvariablen die wir zuvor eingestellt haben. Ich denke, dass diese Codes eindeutig sind und wie Sie sehen können ist es auch relativ einfach Offscreen Surfaces zu erstellen, was auch gut so ist (denn schließlich braucht ein Programm normalerweise einige davon). Jetzt haben wir alles erstellt, was wir brauchen. Bevor wir jedoch blitten sollten wir den Inhalt des Backbuffersurfaces zunächst überschreiben. Und zwar mit einer einheitlichen Farbe, z.B. schwarz. Denn beim Erstellen dieser Surface haben wir nichts angegeben, was an Grafikdaten hineinkopiert werden soll und deshalb befinden sich noch immer die Daten in unserer Surface, die vorher an dieser Stelle im Speicher abgelegt waren (und diese Daten sind bestimmt keine Grafik). Also füllen wir zunächst unseren Backbuffer mit der Farbe schwarz. Es genügt jedoch nicht, DDraw zu sagen, dass wir den Backbuffer schwarz füllen wollen, sondern wir müssen DDraw auch noch sagen, welchen Bereich (in Koordinaten) wir füllen wollen, was in unserem Fall der gesamte Bereich des Backbuffers ist. Dafür eignen sich Variablen des Typs RECT. Mit ihnen kann man eine rechteckige Fläche auf einer Surface festlegen. Ich habe hier zur Ansicht eingefügt, wie der RECT-Typ aussieht (Sie müssen ihn jedoch nicht extra deklarieren; er ist bei DirectX bereits enthalten :-):
Type RECT
Top As Integer
Bottom As Integer
Left As Integer
Right As Integer
End Type
Also erstellen wir jetzt eine Variable dieses Typs und stellen die einzelnen Eigenschaften so ein, dass die gesamte Fläche des Backbuffers gemeint ist und füllen sie dann schwarz:
Dim FillRect As RECT
FillRect.Top = 0
FillRect.Left = 0
FillRect.Right = Backbuffer_Eige.lWidth
FillRect.Bottom = Backbuffer_Eige.lHeight
Backbuffer_Func.BltColorFill FillRect, vbBlack
Der Einfachheit halber habe ich als Farbe oben den vbBlack Wert von Visual Basic verwendet. Wenn Sie andere Farben generieren möchten sollten Sie die "CreateColorRGB" Funktion des DirectX Objekts verwenden. und schon ist unser gesamter Backbuffer schwarz. Also bleibt nur noch eins zu tun. Wir blitten das Bild in unserem Offscreensurface auf den Backbuffer und Flippen dann um es sichtbar zu machen. Auch hierfür müssen wir zwei rechteckige Bereiche festlegen. Einmal den Bereich des Offscreensurfaces, den wir verwenden möchten und einmal den Bereich des Backbuffers, auf den wir blitten wollen:
Dim DRect As RECT
Dim SRect As RECT
SRect.Top = 0
SRect.Left = 0
SRect.Right = Offscreen_Eige.lWidth
SRect.Bottom = Offscreen_Eige.lHeight
DRect.Top = 100
DRect.Left = 100
DRect.Right = SRect.Right + DRect.Left
DRect.Bottom = SRect.Bottom + DRect.Top
Backbuffer_Func.Blt DRect, Offscreen_Func, SRect, DDBLT_WAIT
Primary_Func.Flip Nothing, DDFLIP_WAIT
Wie Sie sehen können möchte ich das gesamte Bild blitten und zwar auf die Position 100/100 im Backbuffer. Der DDBLT_WAIT und der DDFLIP_WAIT Parameter sagt DDraw lediglich, dass es mit dem Fortfahren in Ihren Programmcodes warten soll, bis der Vorgang abgeschlossen ist. In diesem Beispiel verwenden wir die "Blt" Funktion zum blitten des Bildes. Es gibt noch eine andere Funktion: Die "BltFast". Wie der Name schon verrät, ist sie schneller als die Blt Funktion (bis zu 10% wenn ich richtig informiert bin). Allerdings funktioniert sie nicht auf jeder Grafikkarte. Außerdem hat Sie einen anderen Syntax und deshalb möchte ich nochmal einige Zeilen Code anführen, die dasselbe wie eben bewirken - aber natürlich mit der "BltFast" Funktion:
Dim SRect As RECT
SRect.Top = 0
SRect.Left = 0
SRect.Right = Offscreen_Eige.lWidth
SRect.Bottom = Offscreen_Eige.lHeight
Backbuffer_Func.BltFast 100, 100, Offscreen_Func, SRect, DDBLTFAST_WAIT
Primary_Func.Flip Nothing, DDFLIP_WAIT
Sie sehen, hierbei können wir uns den DestRect Bereich sparen, weil wir die Position direkt übergeben können. Generell empfehle ich Ihnen diese Variante (also BltFast) zu verwenden, auch wenn Sie nicht von allen Grafikkarten unterstützt wird. Bei vielen Blittvorgängen in der Hauptschleife der Anwendung machen sich diese 10% nämlich deutlich bemerkbar! So. Das war praktisch schon fast alles. Eines bleibt noch zu sagen: Beim Verlassen Ihres Programms sollten Sie DirectDraw die eigentliche Auflösung wiederherstellen lassen und dann alle Objekte zerstören. Beispielsweise so:
DDraw.RestoreDisplayMode
DDraw.SetCooperativeLevel Formular.hWnd, DDSCL_NORMAL
Set DDraw = Nothing
Set DirectX = Nothing
Herzlichen Glückwunsch! Sie haben soeben Ihre erste DDraw Anwendung geschrieben! Und weil dieses Code/Beschreibungsgemisch vielleicht für den Anfang etwas schwer zu verstehen war habe ich hier alles nochmal zusammengefasst (weiter unten finden Sie auch einen passenden Download):
Dim DirectX As DirectX7
Dim DDraw As DirectDraw7
Dim Primary_Func As DirectDrawSurface7
Dim Primary_Eige As DDSURFACEDESC2
Dim Backbuffer_Func As DirectDrawSurface7
Dim Backbuffer_Eige As DDSURFACEDESC2
Dim Offscreen_Func As DirectDrawSurface7
Dim Offscreen_Eige As DDSURFACEDESC2
Private Sub Form_KeyPress(KeyAscii As Integer)
Unload Me
End Sub
Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
DDraw.RestoreDisplayMode
DDraw.SetCooperativeLevel Formular.hWnd, DDSCL_NORMAL
Set DDraw = Nothing
Set DirectX = Nothing
End Sub
Private Sub Form_Load()
Set DirectX = New DirectX7
Set DDraw = DirectX.DirectDrawCreate("")
Dim FillRect As RECT
Dim SRect As RECT
InitDDraw
FillRect.Top = 0
FillRect.Left = 0
FillRect.Right = Backbuffer_Eige.lWidth
FillRect.Bottom = Backbuffer_Eige.lHeight
Backbuffer_Func.BltColorFill FillRect, vbBlack
SRect.Top = 0
SRect.Left = 0
SRect.Right = Offscreen_Eige.lWidth
SRect.Bottom = Offscreen_Eige.lHeight
Backbuffer_Func.BltFast 100, 100, Offscreen_Func, SRect, DDBLTFAST_WAIT
Primary_Func.Flip Nothing, DDFLIP_WAIT
End Sub
Private Sub InitDDraw()
'Displayinteraktion
DDraw.SetCooperativeLevel Formular.hWnd, DDSCL_FULLSCREEN Or DDSCL_ALLOWMODEX Or DDSCL_EXCLUSIVE
DDraw.SetDisplayMode 640, 480, 16, 0, DDSDM_DEFAULT
'Wir erstellen die komplexe Surface
Dim Caps As DDSCAPS2
Primary_Eige.lFlags = DDSD_CAPS Or DDSD_BACKBUFFERCOUNT
Primary_Eige.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE Or DDSCAPS_COMPLEX Or DDSCAPS_FLIP
Primary_Eige.lBackBufferCount = 1
Set Primary_Func = DDraw.CreateSurface(Primary_Eige)
Caps.lCaps = DDSCAPS_BACKBUFFER
Set Backbuffer_Func = Primary_Func.GetAttachedSurface(Caps)
Backbuffer_Func.GetSurfaceDesc Backbuffer_Eige
'Jetzt laden wir die Offscreensurface
Offscreen_Eige.lFlags = DDSD_CAPS
Offscreen_Eige.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
Set Offscreen_Func = DDraw.CreateSurfaceFromFile("F:\Eigene Dateien\c++.bmp", Offscreen_Eige)
End Sub
Zusammenfassent hier noch einmal der vorrangegange Abschnitt als Beispielprojekt zum Downloaden.
V. Blitten mit Transparenz
Nun wollen wir uns einem weiteren sehr wichtigen Thema zuwenden: Der Transparenz. Wie Sie wissen ist jedes Bild von Haus aus erst einmal viereckig und so natürlich auch in DirectDraw. Dennoch kommt es ständig vor, dass wir Beispielsweise einen Ball auf ein Bild (Surface) so zeichnen wollen, dass wir auch um den Ball herum das ganze Bild des Hintergrundes sehen können. Mit dem, was Sie bisher gelernt haben ist das noch nicht möglich. Durch eine Einfache Veränderung der Codes von eben, können Sie jedoch eine transparente Farbe erzeugen. Am besten laden Sie sich hierfür das Beispielprojekt herrunter und sehen sich das ganze etwas genauer an. Wie Sie sehen können, habe ich den Hintergrund schwarz eingefärbt. Wir möchten jedoch, dass wir durch das Schwarz auf den weißen Hintergrund unseres Backbuffers hindurchsehen können. Also müsen wir DDraw das natürlich mitteilen. Und das geht ganz einfach über ColorKeys. Hier sehen Sie den entsprechenden Variablen-Typ:
Type DDCOLORKEY
hight As Long
low As Long
End Type
Es sind zwei Variablen in diesem Typ vorhanen, denn mit dem ColorKey lässt sich nicht nur eine Farbe beschreiben, sondern ein ganzer Farbbereich, den wir dann transparent anzeigen können. Da wir aber nur eine Farbe transparent schalten wollen, müssen wir High und Low in unserem Beispiel auf den selben Wert setzen. Dann weisen wir bei der Erstellung der Offscreen Surface den Farbschlüsel zu. Dies gestaltet sich wie folgt:
Dim ColorK As DDCOLORKEY
ColorK.high = DirectX.CreateColorRGB(0, 0, 0)
ColorK.low = DirectX.CreateColorRGB(0, 0, 0)
Offscreen_Eige.lFlags = DDSD_CAPS
Offscreen_Eige.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
Set Offscreen_Func = DDraw.CreateSurfaceFromFile(App.Path & "\bild.bmp", Offscreen_Eige)
Offscreen_Func.SetColorKey DDCKEY_SRCBLT, ColorK 'Das ist die neue Anweisung
Wie Sie sehen können hat sich nicht viel beim Erstellen der Offscrensurface verändern. Allerdings weiß DirectDraw jetzt, welcher Farbbereich beim Blitten transparent sein soll. Damit das auch beachtet wird, müssen wir beim Blit-Funktionsaufruf noch einen Parameter dranhängen; nämlich:
- Bei Blt: DDBLT_KEYSRC
- Bei BltFast: DDBLTFAST_SRCCOLORKEY
Also sieht der Aufruf zum Blitten dann so aus:
Backbuffer_Func.BltFast 50, 50, Offscreen_Func, SRect, DDBLTFAST_WAIT Or DDBLTFAST_SRCCOLORKEY
Und das war auch schon alles. Denoch möchte ich anmerken, dass DirectDraw die Farbwerte anders interpretiert, als man es von Visual Basic gewohnt ist.
VI. Zeichen auf eine Surface
Sie kennen sicherlich die Zeichenmethoden und Möglichkeiten einer Picture Box in Visual Basic oder eines Formulars. Mit DirectDraw Surfaces verhält es sich im Prinzip ähnlich. Sie setzen den Zeichenstil, Breite, Farbe und ein Füllmuster und können dann über einige Funktionen direkt auf die Surface zeichnen. Ich habe Ihnen hier eine kleine Referenz zusammengestellt über Funktionen die Sie dazu benutzen können:
SetDrawStyle (drawStyle as Long) |
Diese Funktion legt den Zeichenstil fest. |
Paramter: |
drawStyle |
0 voll, massiv |
1 gestrichelt |
2 gepunktet |
3 Strich, Punkt |
4 Strich, Punkt, Punkt |
5 transparent |
6 ausgefüllt |
|
SetDrawWidth (drawWidth as Long) |
Diese Funktion legt die Strichbreite beim Zeichnen fest. |
Paramter: |
drawWidth |
Beliebige Breite in Pixeln |
SetFillColor (color as Long) |
Diese Funktion setzt die Füllfarbe fest. |
Paramter: |
color |
Ein beliebiger gültiger Farbcode |
SetFillStyle (fillStyle as Long) |
Diese Funktion legt das Füllmuster fest. |
Paramter: |
fillStyle |
0 voll, massiv |
1 transparent |
2 horizontale Linen |
3 vertikale Linien |
4 aufwärtsdiagonal |
5 abwärtsdiagonal |
6 Kreuz |
7 Schrägkreuz |
|
SetFont (font as Ifont) |
Diese Funktion legt eine neue Schriftart fest. |
Paramter: |
font |
Ein Zeichensatz (Variable vom Typ IFont muss übergeben werden) |
SetFontBackColor (color as Long) |
Stellt die Hintergrundfarbe beim Zeichnen von Text ein. |
Paramter: |
color |
Die Farbe, die verwendet werden soll |
SetFontTransparency (b as Boolean) |
Stellt ein, ob der aktuelle Zeichensatz transparent ist oder nicht. |
Paramter: |
b |
Entweder True oder False (na was bedeutet's wohl? :-) |
SetForeColor (color as Long) |
Diese Funktion legt die Fordergrundfarbe für Zeichenmethoden fest. |
Paramter: |
color |
Die Farbe, die verwendet werden soll |
DrawBox (x1 as Long, y1 as Long, x2 as Long, y2 as Long) |
Diese Funktion zeichnet ein Rechteck. |
Paramter: |
x1, y1
x2, y2 |
Die Koordinaten des Rechtecks |
DrawCircle (x1 As Long, y1 As Long, r As Long) |
Diese Funktion zeichnet einen Kreis. |
Paramter: |
x1, y1 |
Die Koordinaten des Mittelpunkts |
r |
Der Radius des Kreises |
DrawEllipse (x1 As Long, y1 As Long, x2 As Long, y2 as Long) |
Diese Funktion zeichnet ein Ellipse. |
Paramter: |
x1, y1
x2, y2 |
Die Koordinaten des Rechtecks |
DrawLine(x1 as Long, y1 as Long, x2 as Long, y2 as Long) |
Diese Funktion zeichnet eine Linie. |
Paramter: |
x1, y1
x2, y2 |
Die Koordinaten des Linienpunkte |
DrawRoundedBox (x1 as Long, y1 as Long, x2 as Long, y2 as Long, rw as Long, rh as Long) |
Diese Funktion zeichnet ein Rechteck mit abgerundeten Ecken. |
Paramter: |
x1, y1
x2, y2 |
Die Koordinaten des Rechtecks |
rh |
Vertikale Größe der Rundung |
rw |
Horizontale Größe der Rundung |
DrawText (x as Long, y as Long, text as String, b as Long) |
Diese Funktion zeichnet Text. |
Paramter: |
x, y |
Die Position des Textes |
text |
Der Text an sich |
b |
Gibt an, ob die aktuelle Cursorposition (True) oder die angegebene X/Y Position (False) verwendet werden soll, um den Text zu zeichnen |
VII. Eine leere Offscreen Surface erstellen
Im ersten Beispiel zu DirectDraw hatte ich angesprochen, dass ich in diesem Tutorial auch zeigen möchte, wie man eine leere Offscreen Surface erstellen kann. Dieses Kapitel widmet sich nun ganeu diesem Thema. Beim Erstellen einer leeren Offscreensurface müssen Sie dem Rechner sagen, wie groß die Surface werden soll. Das kann man natürlich über die Variable, die die Eigenschaften der Surface enthält regeln. Erstellt wird dann mit dem Befehl "CreateSurface":
EmptySurf_Eige.lFlags = DDSD_CAPS Or DDSD_HEIGHT Or DDSD_WIDTH
EmptySurf_Eige.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
EmptySurf_Eige.lHeight = 115
EmptySurf_Eige.lWidth = 280
Set EmptySurf_Func = DDraw.CreateSurface(EmptySurf_Eige)
Die erste Zeile legt wieder fest, welche Eigenschaften wir ändern möchten. Da wir auch Höhe und Breite ändern müssen, ist es Notwendig den DDSD_HEIGHT und den DDSD_WIDTH Parameter hinzuzufügen. Dann setzen wir die Eigenschaften und erstellen schließlich mit der letzen Zeile die Offscreensurface. Bevor Sie diese Surface jedoch verwenden, empfiehlt es sich, den Inhalt erst zu überschreiben (beispielweise mit der BltColorFill Funktion, die Sie ja schon kennengelernt haben). Denn auch hier befinden sich nach dem Erstellen der Surface noch die Daten an der selben Speicherstelle, und als Bild sieht das nicht besonders gut aus. Natürlich gibt es auch zu diesem Abschnitt ein Beispiel.
VIII. Animationen
Animationen sind relativ einfach realisierbar. Wie Sie wissen ist eine Animation nichts anderes als eine Abfolge von Einzelbildern und daraus ergibt sich, dass Sie, ganz klar, eine Schleife benötiegen, die bei jedem Durchlauf ein anderes Bild auf eine vorgegebene Stelle blittet. Normalerweise wird nicht für jedes Einzelbild eine eigene Surface erstellt sondern alle Bilder aneinandergesetzt. Beim blitten wird dann der Bereich so gewählt, dass nur das Bild angezeigt wird, das in der Abfolge gerade dran ist und der Rest weggeschnitten wird (diese Angaben kann man über die RECT Variablen recht einfach bewerkstelligen). Bleibt also nur noch eines zu sagen: Wenn Sie geschickt vorgehen brauchen Sie keine neue Schleife erstellen für jede einzelne Animation, sondern Sie verwenden die Hauptschleife Ihrer Anwendung einfach mit.
IX. Technik: Hintergrundscrolling
Sicherlich kennen Sie diese Technik von vielen Spielen: Man befindet sich auf einer Landschaft, die man von oben betrachtet und da man die Landschaft nicht ganz sehen kann wird gescrollt, sobald der Mauszeiger sich am Rand des Bildschirms befindet. Doch haben Sie sich schonmal gefragt, wie so etwas überhaupt bewerkstelligt wird? Mit DirectDraw ist es ganz einfach! Sie erstellen zusätzlich zu der Komplexen Surface eine riesige Surface (Von der Höhe und Breite her größer als die Komplexe Surface), in der Sie die gesamte Landschaft zusammenstellen. Dann blitten Sie immer nur einen bestimmten Bereich aus dieser großen Surface auf den Backbuffer und flippen dann. Beim Scrollen ändern Sie einfach die X/Y Position des Bereichs und es entsteht der Eindruck, als bewege man sich über die Landschaft. Da Sie wissen, dass die Landschaft riesengroß ist und Sie viele Animationen haben, müssen Sie diese nicht immer bei jedem Spielschleifendurchlauf neu aufbauen. Sie können auch berechnen, wo sich der Benutzer befindet (was er sieht) und nur diesen Bereich neu zusammenstellen, um die Performance zu erhöhen.
X. Beschreibung eines Anwendungsgerüsts
Wenn Sie jetzt an diesem Punkt angekommen sind und das oben Erklärte alles Begriffen haben und einsetzen können, dann hat das Tutorial eigentlich seinen Zweck bereits erfüllt. Aber wie gesagt, möchte ich am Ende noch einmal kurz auf das schreiben von Fensteranwendungen eingehen. Da das Bild einer solchen Anwendung ständig auf Basis von Ereignissen aktualisiert werden muss möchte ich Ihnen zuerst ein Anwendungsgerüst vorstellen, das ich in meinen Spielen (egal ob Vollbild oder Fenster) meistens verwende. Dazu erst einmal eine kleine Grafik:
Wie Sie sehen können gliedert sich das Gerüst in drei Abschnitte: Variablen, Hauptschleife und Grafikfunktionen. Das ist natürlich nur eine grobe, allgemeine Skizze des Gerüsts. In einer richtigen Anwendung werden sich die Hauptschleife und die Grafikfunktionen wohl auch noch aus verschiedenen Teilen zusammensetzen, aber hier sollen diese zwei genügen. Die Grafikfunktionen sind allein dazu da, ein Bild auf den Bildschirm zu bringen. Sie rufen die Werte ab, die in den Variablen stehen und erstellen daraus das Bild. Wenn sich die Werte der Variablen von einem Aufruf zum nächsten nicht ändern, dann ändert sich auch das Bild nicht (! das ist sehr wichtig). Die Hauptschleife hingegen hat keine Möglichkeit direkt auf die Grafikausgabe Einfluss zu nehmen. Sie kann lediglich anhand der Informationen, die sie über Tastatur, Maus, Mikrofon, Netzwerk, etc. bekommt die Werte der Variablen ändern und so den Grafikfunktionen sagen, was anders sein soll. Die Variablen bilden quasi die Grundlage zur Kommunikation zwischen den einzelnen Funktionen. Dieses Model hat sich in meinen Spielen bisher immer bewährt und ist auch relativ einfach und übersichtlich (mit einzelnen Klassen, die sogar in DLLs ausgelagert werden können) realisierbar. Außerdem ist es sehr gut für Fensteranwendungen geeignet, die öfters eine Aktualisierung der Grafik erfordern, auch wenn sich eigentlich nichts an der Grafik ändert. Aber jetzt zum eigentlichen Thema: Fensteranwendungen mit DirectDraw.
XI. Fensteranwendungen mit DirectDraw
Im Großen und ganzen sieht eine Fensteranwendung nicht wesentlich anders aus, als eine Vollbild Anwendung mit DirectDraw. Es gibt jedoch einige Unterschiede beim Initialisieren und auch beim Blitten und darauf wollen wir im Folgenden eingehen (außerdem muss wie oben schon erwähnt die Grafik häufiger aktualisiert werden).
Beim Initialisieren ändert sich etwas im Aufruf von "SetCooperativeLevel". Wir müssen jetzt anstelle der Parameter DDSCL_FULLSCREEN, DDSCL_ALLOWMODEX Or DDSCL_EXCLUSIVE nur noch den Parameter DDSCL_NORMAL übergeben. Der Aufruf von "SetDisplayMode" entfällt generell, weil wir die Auflösung ja nicht ändern möchten. Daraus folgt, dass wir beim Verlassen des Programms auch keinen Code mehr zum wiederherstellen der alten Auflösung schreiben müssen sondern nur noch die Objekte zerstören die wir initialisiert haben. Des weiteren kann man bei Fensteranwendungen nicht mit einem Backbuffer arbeiten, man hat also nur eine Primary Surface, auf die man direkt blittet und keine Komplexe mehr. Das initialisieren der Primären Surface sieht demnach ebenfalls anders aus:
Primary_Eige.lFlags = DDSD_CAPS
Primary_Eige.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE
Set Primary_Func = DDraw.CreateSurface(Primary_Eige)
Ansonsten ändert sich beim Initialisieren nichts. Das Blitten wird etwas komplizierter. Denn wir haben über die Primary Surface immer noch Zugriff auf den gesamten Bildschirm. Unser Bild soll jedoch beim blitten im Fenster landen, also müssen wir uns zunächst die Koordinaten des Fensters holen. Dafür stellt uns das DirectX Objekt eine kleine Funktion zur Verfügung ("GetWindowRect") Dieser Funktion übergeben wir die Hwnd (Zugriffsnummer) eiens Fensters oder einer Picture Box und eine RECT Variable. Nach dem Funktionsaufruf ist diese Variable dann mit den korrekten Werten gefüllt. Wir wissen dann die X/Y Koordinaten des linken oberen Punktes (LEFT/TOP) und auch die Koordinaten des rechten unteren Punktes (RIGHT/BOTTOM) übergeben. Wenn wir mit der Blt Methode jetzt ein Bild blitten wollen, so können wir die RECT Variable unseres Fensters natürlich nicht direkt übergeben, weil die Größe des Fensters nicht unbedingt mit der Größe des Bildes übereinstimmen muss (und das wird sie auch nicht). Wir müssen also RIGHT und BOTTOM anpassen, so dass die Breite und Höhe insgesamt mit der des Quellbildes übereinstimmt. Das könnten Sie beispielsweise folgendermaßen machen (ich gehe dabei davon aus, dass DRect die Größe des Fensters enthält, die mit GetWindowRect abgefragt wurde und SRect die genaue Größe des Quellbildes enthält!):
DRect.Top = DRect.Top + 100
DRect.Left = DRect.Left + 100
DRect.Right = DRect.Left + (SRect.Right - SRect.Left)
DRect.Bottom = DRect.Top + (SRect.Bottom - SRect.Top)
In diesem Beispiel haben wir natürlich nicht nur die Breite und Höhe von DRect an das Fenster und das Bild anpassen sondern gleichzeitig auch die Position auf dem Ziel um 100 Pixel im X und Y Bereich verschieben (die beiden ersten Zeilen). Im Gegensatz zur Blt Funktion ist die BltFast ja bekanntlich einfacher, weil wir dieser nur die X/Y Position auf dem Zielobjekt übergeben müssen und sie die Breite und Höhe direkt aus der QuellRECT liest. Also müssten wir in diesem Fall nichts anpassen. Wir würden einfach nur "DRect.Top + 100" und "DRect.Left + 100" übergeben. Das war bereits alles zum Blitten! Wenn Sie das alles vieleicht doch etwas verwirrt hat, dann können Sie sich natürlich auch dazu gerne wieder ein kleines Beispiel runterladen. Als Abschluss der Fensteranwendungen bleibt noch zu sagen, dass Sie immer, wenn das Fenster bewegt wird, das Bild neu Blitten müssen (deshalb auch meine Empfehlung zum Anwendungsgerüst oben - dieses ermöglicht so etwas relativ einfach). Am besten fügen Sie im "Form_Paint" Ereignis einen Aufruf ein, der die Grafik aktualisiert (auch das sehen Sie natürlich im Beispielprojekt).
XII. Surfaces explizit im Video- bzw. Systemspeicher erstellen
Ich hatte ganz am Anfang dieses Tutorials schon angesprochen, dass Surfaces normalerweise im Videospeicher liegen sollten, damit der Grafikprozessor Kopierarbeiten übernehmen kann und so die Performance gesteigert wird. Videospeicher ist bekanntlich jedoch ein knappes Gut. Daher gibt es in DirectDraw die Option, beim Erstellen einer Surface explizit anzugeben, in welchem Speicher die Surface erstellt werden soll. Sie können beispielsweise Surfaces, die Grafiken enthalten von denen Sie wissen, dass sie seltener benötigt werden explizit im Systemspeicher ablegen, um Videospeicher zu sparen. Wenn Sie also eine Surface einmal im Systemspeicher erstellen möchten, dann geben Sie beim Erstellen einfach noch folgenden Parameter ein: DDSCAPS_SYSTEMMEMORY. Im Ganzen würde dies dann so aussehen:
Offscreen_Eige.lFlags = DDSD_CAPS
Offscreen_Eige.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_SYSTEMMEMORY
Set Offscreen_Func = DDraw.CreateSurfaceFromFile("F:\Eigene Dateien\c++.bmp", Offscreen_Eige)
» Autor: Thomas Geiger