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!