www.codeworx.org/directx_tuts/Sprites unter DirectX 7

Sprites unter DirectX 7
(von Jens Konerow)

Vorwort:

Als erstes möchte ich darauf hinweisen, das gewisse Dinge in diesem Tutorial vorausgesetzt werden. Falls du also noch keine Grundkenntnisse in der DirectX, speziell in der DirectDraw-Programmierung hast, würde ich empfehlen, das du vorher mein erstes Tutorial zur DirectX-Programmierung durchgehst und dann mit diesem fortfährst, da du sonst wahrscheinlich viele Dinge nicht nachvollziehen kannst.

In diesem Tutorial wird es zwei Beispiel-Programme geben. Im ersten werden wir die grundlegenden Sachen zur Realisierung eines Sprites klären. Das Sprite wird fest an einer Position auf dem Bildschirm zu sehen sein, wärend beim zweiten Beispiel das Sprite beweglich ist. Man wird auch die Möglichkeit haben, zwischen dem System- und dem Videospeicher zu wählen, außerdem werden wir den Clipper kennenlernen. Was ein Clipper ist und was er bewirkt, werde ich aber an gegebener Stelle erklären. Jetzt wollen wir aber ans Werk gehen.

Prinzip der Sprites:

Erstmal wollen wir klären, was bei einem Sprite überhaupt geschieht, damit es transparent wirkt. Es ist ja nicht richtig transparent sondern nur an manchen Stellen durchsichtig. Das ist wie eine Wand mit einem Fenster. Durch die Wand können wir nicht durchsehen und dann kommt der Teil der Wand (das Fenster), wo wir eigentlich keine Wand mehr sehen, sonder das was dahinter ist. So ist das bei einem Sprite auch. Fazit: Ein Sprite ist eigentlich gar nicht transparent.

Um nun mit Hilfe von DirectX ein Sprite darzustellen, müssen wir erst einen Farbbereich angeben (oder eine Farbe). Diese Aufgabe hat der COLORKEY. Er speichert den angegeben Farbbereich und gibt somit an, welcher Teil des Sprites beim Blitten nicht berücksichtigt werden soll. Kurz: Der COLORKEY gibt den Farbbereich an, den DirectX beim Blitten, sprich beim Kopieren nicht berücksichtigen soll. Es werden nur die Teile des Sprites mitkopiert, die nicht in diesem Farbbereich liegen. Wenn man die Wand mauert, dann wird der Teil, wo später das Fenster hin soll, ja auch nicht zu gemacht, denn dort soll man ja durchgucken können.

Sprite erstellen (Beispiel 1):

Wir erstellen jetzt eine primäre, eine für den Hintergrund und eine Surface für das Sprite. Bei der Surface für das Sprite gibt es anfangs nichts weiter zu beachten. Das könnte dann folgendermaßen aussehen:

Dim DX As New DirectX7
Dim DD As DirectDraw7
Dim PrimSurf As DirectDrawSurface7
Dim BackSurf As DirectDrawSurface7
Dim SpriteSurf As DirectDrawSurface7
Dim ddsdPrimSurf As DDSURFACEDESC2
Dim ddsdBackSurf As DDSURFACEDESC2
Dim ddsdSpriteSurf As DDSURFACEDESC2
Private Sub Form_load()
   init
End Sub
Private Sub init()
   Set DD = DX.DirectDrawCreate("")
   Call DD.SetCooperativLevel(Me.hWnd,DDSCL_NORMAL)
   'primäre Surface erstellen
   ddsdPrimSurf.lFlags = DDSD_CAPS
   ddsdPrimSurf.ddsCaps.lCaps = DDSCAPS_PRIMARYSURFACE
   Set PrimSurf = DD.CreateSurface(ddsdPrimSurf)
   'BackSurface erstellen (hier wird der Hintergrund geladen)
   ddsdBackSurf.lFlags = DDSD_CAPS
   ddsdBackSurf.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
   Set BackSurf = DD.CreateSurfaceFromFile(App.Path & _
   "\Hintergrund.bmp", ddsdBackSurf)
   'Sprite Surface erstellen
   ddsdSpriteSurf.lFlags = DDSD_CAPS
   ddsdSpriteSurf.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
   Set SpriteSurf = DD.CreateSurfaceFromFile(App.Path & _
   "\Sprite.bmp", ddsdSpriteSurf)
End Sub

So könnte das Erstellen der Surface aussehen. Ich habe bewußt noch nichts neues in den obigen Code genommen, um dir jetzt zu zeigen, daß man nicht viel mehr machen muß, um ein Sprite zu erstellen. Als erstes wollen wir die Variable für den Colorkey deklarieren:

Dim Colorkey As DDCOLORKEY

Nun wollen wir den Farbbereich bzw. in diesem Fall den Farbwert festlegen. Bei unserem Sprite soll DirectX die Farbe schwarz unberücksichtigt lassen:

Colorkey.High = RGB(0, 0, 0)
Colorkey.Low = RGB(0, 0, 0)

Wir müssen bei der Definition des Farbbereiches immer zwei Werte angeben. Zum ersten wäre das der höchste Wert (High) und zum zweiten wäre das der niedrigste Wert (Low). Es spielt aber keine Rolle, welchen Wert du als erstes definierst. Wir könnten die obige Sache auch umdrehen, das stört DirectX nicht. Nun müssen wir der Sprite-Surface den Colorkey noch zuweisen, ansonsten weiß DirectX nicht, auf welche Surface sich der Colorkey bezieht.

SpriteSurf.SetColorKey DDCKEY_SRCBLT, Colorkey

DDCKEY_SRCBLT sagt aus, das dieser Colorkey sich auf Blitteraktionen bezieht und der angegebene Farbbereich dann nicht mitkopiert werden soll. Danach mußt du den Colorkey angeben, der verwendet werden soll. In unserem Beispiel gibt es nur einen, also Colorkey. Legen wir jetzt die RECT-Strukturen fest:

Dim rPrimSurf As RECT
Dim rBackSurf As RECT
Dim rSpriteSurf As RECT
Dim rPosition As RECT
Call DX.GetWindowRECT(Picture1.hWnd, rPrimSurf)
rBackSurf.Bottom = ddsdBackSurf.lHeight
rBackSurf.Right = ddsdBackSurf.lWidth
rSpriteSurf.Bottom = ddsdSpriteSurf.lHeight
rSpriteSurf.Right = ddsdSpriteSurf.lWidth
rPosition.Top = 150
rPosition.Bottom = rPosition.Top + 50
rPosition.Left = 150
rPosition.Right = rPosition.Left + 60

Dir ist sicherlich aufgefallen, das wir eine weitere RECT-Struktur festgelegt haben. Mit dieser RECT-Struktur (rPosition) geben wir, wie der Name schon sagt, die Position und Größe des Sprites auf der primären Surface an. Die Blit-Anweisungen sehen dann wie folgt aus:

Call PrimSurf(rPrimSurf, BackSurf, rBackSurf, DDBLT_WAIT)
Call PrimSurf(rPosition, SpriteSurf, rSpriteSurf, _
DDBLT_KEYSRC Or DDBLT_WAIT)

Wir haben diesmal zwei Blit-Anweisungen: Die erste kopiert das Hintergrundbild auf die primäre Surface und die zweite kopiert das Sprite auf die primäre Surface, dabei müßte dir folgendes Flag auffallen: DDBLT_KEYSRC. Dieses Flag gibt an, daß beim Blitten der Colorkey beachtet werden soll und schließlich der festgelegte Farbbereich unberücksichtigt bleiben muß. Ja, das war der erste Teil dieses Tutorials. Wie du siehst, ist es gar nicht so schwer, ein Sprite zu realisieren.

Bewegliche Sprites (Beispiel 2):

Nun sind wir beim zweiten Teil dieses Tutorials angekommen. Wir werden im Laufe dieses Teils den Clipper und den GameLoop kennenlernen. Außerdem werde ich dir zeigen, wie du zwischen System- und Videospeicher wählen kannst.

Erstmal wollen wir klären, was ein Clipper und was der GameLoop ist.
Ein DirectDrawClipper-Objekt wird als rechteckiger Bereich definiert. Alle Blitter-Methoden die innerhalb dieses Bereiches liegen, sind zur Laufzeit für den Anwender sichtbar und alles was außerhalb des Clipper-Bereiches liegt bleibt unsichtbar. Es ist möglich, mehrere Clipper-Bereiche auf einer Surface festzulegen. Mit dem Clipper kann man Grenzen des Fensters und Bildschirms markieren.
Ein wichtiges Einsatzgebiet ist, daß man durch den Clipper Sprites sanft in einen sichtbaren Bildschirmbereich bewegen kann. Ohne den Clipper würde das Sprite so lange unsichtbar sein, bis die RECT-Struktur völlig im sichtbaren Bereich ist. Durch den Clipper ist DirectDraw in der Lage, zu erkennen, welcher Teil im sichtbaren Bereich und welcher im unsichtbaren Bereich liegt. So können auch nur Teile des Sprites sichtbar werden.
Achtung! Der Clipper kann nur in fenster-basierenden Anwendungen verwendet werden, nicht aber in Fullscreen-Anwendungen.

Der GameLoop ist eigentlich nichts anderes als eine Do... Loop-Schleife, mit der das Sprite letztendlich bewegt wird. Wichtig ist die Anweisung DoEvents, damit auch noch andere Ereignisse bearbeitet werden können (z.B. ein Mausklick).

Gut, fangen wir an! Als erstes wollen wir den Clipper erstellen und seinen Bereich definieren. Folgendermaßen wird der Clipper deklariert:

Dim Clipper As DirectDrawClipper

Dann müssen wir den Clipper erstellen:

Set Clipper = DD.CreateClipper(0)

In der Klammer können Flags gesetzt werden; da wir dies zur Zeit aber nicht brauchen, muß es '0' sein! Nun wollen wir noch den Bereich definieren und anschließend den Clipper der primären Surface zuweisen.

Clipper.SetHWnd Picture1.hWnd
PrimSurf.SetClipper Clipper 'Clipper wird zugewiesen

Kommen wir nun zu der Geschichte mit dem System- und Videospeicher.
Ich habe in der Beispiel-Datei eine Art LogIn mit reingenommen. Dort kann man dann zwischen System- und Videospeicher wählen. Diese Wahl wirkt sich nur auf den BackBuffer aus, nicht aber z.B. auf die Sprite-Surface. Jetzt wirst du dich vielleicht fragen: BackBuffer? Was ist das?

In diesem zweiten Beispiel werden wir den Hintergrund und das Sprite erst im Speicher auf den BackBuffer kopieren. Der BackBuffer ist eigentlich nur eine weitere Surface, auf der dann alles wie ein 'Baukasten' zusammengesetzt wird. Wenn wir das Bild dann fertig 'gebaut' haben, wird es komplett auf die primäre Surface geblittet. Im ersten Beispiel haben wir erst den Hintergrund und dann das Sprite auf die primäre Surface geblittet. Würden wir dies in einer Animation genauso machen, würde der Anwender wahrscheinlich das Aufbauen der Bilder sehen und es gibt ein flackerndes Bild. Doch im ersten Beispiel ging es nur darum, zu zeigen, was man machen muß, um ein Sprite darzustellen. Außerdem war es dort auch nicht weiter tragisch, weil wir ja ein Standbild hatten und kein bewegtes Bild.

Wenn wir nun beim LogIn auf OK klicken, dann wird dieses Formular unsichtbar, und das Haupt-Formular, in dem das Sprite dargestellt werden soll, wird sichtbar. In der Sub init(), bei der Erstellung der BackBuffer-Surface, wirkt sich die Wahl nun aus. Durch ein einfaches If-Then-Else-Konstukt werten wir die Wahl aus und reagieren dementsprechend darauf. Jetzt wollen wir aber erstmal die Surface für den BackBuffer erstellen. Es gibt hier eigentlich nichts weiter zu beachten, nur daß wir hier die Surface nicht gleich füllen, wie z.B. bei der Sprite-Surface.
Das sieht dann wie folgt aus:

'In der Deklaration
Dim BackBuffer As DirectDrawSurface7
Dim ddsdBackBuffer As DDSURFACEDESC2
'In Sub init()
ddsdBackBuffer.lFlags = DDSD_CAPS
ddsdBackBuffer.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
Set BackBuffer = DD.CreateSurface(ddsdBackBuffer)

Wie du siehst, ist hier nichts anderes zu machen, als bei einer anderen Surface auch.


If frmLogIn.optSystem.Value = True Then
  ddsdBackBuffer.ddsCaps.lCaps = _
     DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_SYSTEMMEMORY
Else
  ddsdBackBuffer.ddsCaps.lCaps = _
    DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_VIDEOMEMORY
End If
 


Also die Flags DDSCAPS_SYSTEMMEMORY und DDSCAPS_VIDEOMEMORY geben an, ob der BackBuffer im System- oder Videospeicher liegen soll.

Bevor wir nun aber zum GameLoop kommen, muß ich dich noch auf etwas hinweisen.
Da wir später bestimmt Bildschirmstellen restaurieren müssen, muß die Surface, in dem der Hintergrund geladen wird, möglichst gleich groß sein, wie die primäre Surface. Es ist zwar kein Muß, aber es erleichtert die Arbeit ungemein. Um die Größe der Surface anfangs gleich festzulegen, bevor wir sie überhaupt erstellt haben, müssen wir folgende Flags setzen:

ddsdBackSurf.lFlags = DDSD_CAPS Or DDSD_HEIGHT Or _
DDSD_WIDTH
'Jetzt wird die Größe festgelegt
ddsdBackSurf.lHeight = picDarstellung.Height
ddsdBackSurf.lWidth = picDarstellung.Width

Hinweis: picDarstellung ist hier unsere Picture-Box, in der das Bild später dargestellt wird.
Zum GameLoop: Ich habe eine Variable vom Typ Boolean mit dem Namen GameLoop deklariert. Wenn das Formular nun geladen wird, werden erst alle Surfaces erstellt und die RECT-Strukturen festgelegt. Da die RECT-Strukturen nachher immer wieder aktualisiert werden müssen, habe ich sie etwas 'gekapselt'. Richtig gekapselt ist es ja nicht. Das sieht dann so aus:

Private Sub RECT()
   'Hier werden die RECT-Strukturen festgelegt
   Call DX.GetWindowRect(picDarstellung.hWnd, rPrimSurf)
   rBackBuffer.Bottom = ddsdBackBuffer.lHeight
   rBackBuffer.Right = ddsdBackBuffer.lWidth
   rBackSurf.Bottom = ddsdBackSurf.lHeight
   rBackSurf.Right = ddsdBackSurf.lWidth
   rSpriteSurf.Bottom = ddsdSpritesurf.lHeight
   rSpriteSurf.Right = ddsdSpritesurf.lWidth
   rNeuePosition.Top = 100
   rNeuePosition.Bottom = rNeuePosition.Top + 40
   rNeuePosition.Right = rNeuePosition.Left + 60
   rAltePosition.Top = rNeuePosition.Top
   rAltePosition.Bottom = rNeuePosition.Bottom
   rAltePosition.Right = rAltePosition.Left + 60
End Sub

Ich mache nun einen Verweis auf Sub RECT() von Sub init(). Wenn dann die RECT-Strukturen festgelegt sind, wird der Hintergrund schon in den BackBuffer geblittet. Dies geschieht auch in der Sub init(), weil wir das nur einmal machen.
Dann wird die Sub GameLooping() ausgeführt, in der die Do-Loop-Schleife abläuft und die neue Position berechnet wird.
Dir werden bestimmt schon die beiden RECT-Strukturen rNeuePosition und rAltePosition aufgefallen sein. Die rNeuePosition gibt die neue Position des Sprites an, und rAltePosition hat die alte Position des Sprites gespeichert, die dazu benötigt wird, die Bildschirmstelle zu restaurieren, an der das Sprite vorher war.
Wir schneiden also die Stelle, an der das Sprite vorher war, in der Background-Surface (BackSurf) aus und kopieren sie an die gleiche Stelle im BackBuffer. Somit haben wir diesen Bereich wiederhergestellt, und das Sprite wird an seine neue Position geblittet.
Folgendermaßen sehen nun die Blitter-Aktionen aus:

Private Sub blt()
   Call BackBuffer.blt(rAltePosition, BackSurf, _
     rAltePosition, DDBLT_WAIT)
   Call BackBuffer.blt(rNeuePosition, SpriteSurf, _
     rSpriteSurf, DDBLT_KEYSRC Or DDBLT_WAIT)
   Call PrimSurf.blt(rPrimSurf, BackBuffer, rBackBuffer,
    _ DDBLT_WAIT)
End Sub

Als erstes wird die Stelle, an der das Sprite vorher war, restauriert, dann wird das Sprite and die neue Stelle im BackBuffer geblittet, und anschließen wird dann der gesamte BackBuffer auf die primäre Surface geblittet.
Ob der BackBuffer im System- oder im Videospeicher liegt, macht sich sehr stark bemerkbar, weil der Systemspeicher doch um einiges langsamer ist. Ich besitze eine Voodoo3 3000 AGP. Wenn der BackBuffer nun im Videospeicher meiner Karte liegt, dann kommt das Auge schon gar nicht mehr hinterher. Diese Eigenschaft könnte man in Spielen ausnutzen. Das wars!

Anregungen, Fragen, Hinweise, Kritiken usw. schickt bitte an: JensK@vbpc.de Ich wünsche euch noch viel Spaß beim Programmieren!