Die Community zu .NET und Classic VB.
Menü

Der große VB Spielekurs Teil 2

 von 

Übersicht 

Ich möchte mit diesem Kurs zeigen, daß es entgegen anders lautenden Gerüchten sehr gut möglich ist, in Visual Basic Spiele zu programmieren, die durchaus erstaunliche Ablauf-Geschwindigkeiten erreichen.
Der Spielekurs umfasst insgesamt 125 DIN A4-Seiten. Auf Grund dieser Größe wird es hier in 4 Teilen veröffentlicht. Der erste berichtete über die grundlegenden Techniken, wie Sprites, Sounds Tastaturabfrage etc.
In diesem Teil werden Optimierungen und Sprites behandelt.

Mit freundlichen Grüßen
Dominik Scherlebeck (PCDVisual)

2.1 Zu diesem Kurs  

Hallo! Nach einer Pause mit den VB-Kursen geht es nun wieder richtig los. Dieser Teil dreht sich wieder um die Programmierung eigener Spiele mit VisualBasic. Im letzten Teil haben Sie gelernt, was man alles zum Programmieren benötigt und wie man ein einfaches Weltraumspiel schreibt.
Jetzt ist es aber an der Zeit, den ganzen internen Ablauf zu verbessern. Wie Sie sich vielleicht erinnern haben wir im 1. Teil zum Thema Spiele mit Datenfeldern ObjectT(), ObjectX() usw. gearbeitet. Das ist jedoch kein besonders guter Programmierstil, da es bei komplexeren Spielen schnell passieren kann, daß man eine Vielzahl von Datenfeldern benutzt und am Ende selbst keinen Überblick mehr hat. Auch ist es nicht sehr sinnvoll, alle benötigten Bilder als Bildfelder direkt in die Form zu bannen. Die Alternative ist, für all diese elementaren Dinge eines Spiels eigene Module zu schreiben, die wichtige Funktionen zusammenfassen.
Im Rahmen dieses und des nächsten Teils werden wir ein komplettes Pacman-Spiel mit Monstern, Türen, Teleportern und anderen Spielelementen aufbauen. Am Ende dieses Teil werden Sie die zu grunde liegenden "Engines" fertiggestellt haben.
Wer übrigens eigene Spiele mit VB schreiben will, kann übrigens die Module die hier vorgestellt werden komplett übernehmen. Allerdings übernehme ich keine Haftung für die richtige Funktion der Module.
Wie auch im Vorgänger sind alle Dateien, die im Kurs besprochen werden, mit im Archiv. Sie brauchen also nicht selbst zu tippen - auch wenn ich Ihnen das zum Üben empfehlen würde.

2.2 Mehr Ressourcen  

Egal welches Spiel man schreiben will, meist benötigt man Bilder und Sprites. Um nicht jedes Bild einzeln als Bildfeld in die Form zu holen, werden wir nun eine Engine schreiben, die sich um das Laden von Bildern während der Laufzeit kümmert. Das hat den Vorteil, daß Sie nicht jedesmal, wenn Sie eines der Bilder ändern, es auch in der Form ändern müssen.
Als erstes brauchen wir eine leere Form, die später die Ressourcen aufnimmt. Gehen Sie also im Menü "Datei" auf "Neue Form" bzw. wenn Sie VB 4 haben im Menü "Einfügen" auf den Punkt "Form".

Nennen Sie die Form "RESSOURCEN". Setzen Sie nun ein Bildfeld auf die leere Form und geben ihm den Namen "RES" und den Index 0. Setzen Sie die Eigenschaften AutoSize und AutoRedraw auf TRUE und ändern Sie zum Schluß die ScaleMode-Eigenschaft auf 3 (Pixel).


Abbildung 1: Das Romular "Ressourcen"

Damit ist die Arbeit an dieser Form abgeschlossen. Auf Code oder weitere Steuerelemente können wir verzichten, da die Form im laufenden Programm sowieso nicht zu sehen ist.
Fügen Sie nun ein neues Codemodul über Datei / Neues Modul bzw. in VB 4 über Einfügen / Codemodul zu Ihrem Projekt hinzu. Um zur Laufzeit mehrere Bilder laden zu können, benötigen wir eine globale Variable, die die Anzahl der Ressourcen speichert, die bisher geladen wurden.
Öffnen Sie das neue Codemodul, und zeigen Sie den Deklarationsteil (Objekt: (allgemein) Prozedur: (Deklarationen)) an.
Um eine globale Variable - also eine Variable, auf die man von allen Formen und Modulen zugreifen kann - zu erstellen, benutzen wir den GLOBAL -Befehl:

Global <Variable> [(Dimensionen)][AS <Typ>]

Die Variable nennen wir "ResCount", abgeleitet von "Resource Counter". Der Code sieht dann so aus:

Global ResCount
Das allein bringt uns natürlich noch nicht viel weiter. Der Programmierer soll ja auch Ressourcen erstellen können. Dazu schreiben wir die Funktion RES_NEW, die den Index der erstellten Ressource zurückliefert:

Function RES_NEW ()
 
    On Local Error Resume Next 
 
    ResCount = ResCount + 1
    Load RESSOURCEN.RES(ResCount)
 
    RES_NEW = ResCount
 
  End Function

Listing 1: Die Funktion RES_NEW

Als erstes wird in der Prozedur die Fehlerüberwachung so eingestellt, daß alle in der Prozedur auftretenden Fehler ignoriert werden. Das ist in vielen Fällen überflüssig, kann jedoch nützlich sein, Fehler zu verhindern, die unter ganz bestimmten Umständen (zum Beispiel wenn der Benutzer die Variable ResCount ändert) auftreten abzufangen. Danach wird die Variable ResCount um 1 erhöht, da ja eine weitere Ressource erstellt wird. Anschließend kommt ein neuer Befehl:

LOAD [<Form>.]<Objekt>(<Index>)

Der LOAD-Befehl erstellt während der Laufzeit ein neues Objekt mit dem Namen <Objekt> und dem angegebenen Index. Voraussetzung dafür ist, daß mindestens schon ein Objekt mit diesem Namen existiert. Dafür haben wir ja auch das Bildfeld RES(0) angelegt. Wenn wir nun mit LOAD ein weiteres Bildfeld RES mit einem anderen Index erstellen, werden alle Eigenschaften des alten Objekts übernommen.

Load RESSOURCEN.RES(ResCount)
Mit dieser Anweisung wird ein neues Objekt mit dem Namen RES und dem Index ResCount erstellt, daß die gleichen Eigenschaften hat, wie RES(0). Also hat AutoSize den Wert TRUE, ScaleMode steht auf 3 usw.
Das so erzeugte Objekt kann genauso benutzt werden wie alle normalen Objekte, die zur Entwurfszeit erstellt worden sind. Am Ende der Funktion wird noch der Wert ResCount zurückgegeben, da das ja auch der Index der neuen Ressource ist.
Jetzt kann der Programmierer relativ einfach ein neues, leeres Bildfeld anlegen:
Nr = RES_NEW()
Beachten Sie, daß Sie die Klammern auch eintippen. Zwar scheint das auf den ersten Blick nicht notwendig zu sein, jedoch interpretiert VB 3.0 dann RES_NEW als Variable und gibt immer 0 zurück. Also nie die Klammern hinter einer Funktion vergessen, sonst kann es leicht zu Fehlern kommen, die man später schwer findet.
Nachdem Sie nun eine neue Ressource angelegt haben wollen sie diese natürlich auch mit einem Bild ausfüllen. Daher hier die Prozedur "RES_LOAD", die ein beliebiges Bild einlädt:

Sub RES_LOAD (RES, Datei$)

    On Local Error Resume Next 
    RESSOURCEN.RES(RES).Picture = LoadPicture(Datei$)

End Sub

Listing 2: Die Sub "RES_LOAD"

Auch in dieser Prozedur werden wieder mit "On Local Error Resume Next" evtl. auftretende Fehler abgeblockt. In der zweiten Zeile wird mit der Funktion LoadPicture die übergebene Datei in die Ressource mit der Nummer RES geladen. Diese Funktion kann man nun sehr einfach einsetzen, um Bilder zu laden:

Nr = RES_NEW()
RES_LOAD Nr, "Test.Bmp"
Eigentlich würde das schon reichen, um mit Ressourcen zu arbeiten, jedoch folgen jetzt noch einige Prozeduren, die das Arbeiten noch mehr vereinfachen sollen:

Function RES_HDC (Nr)
 
    On Local Error Resume Next 
    RES_HDC = RESSOURCEN.RES(Nr).hDC
 
End Function

Listing 3: Die Funktion "RES_HDC"

Die Funktion RES_HDC(Nr) liefert den DeviceContext der Ressource zurück, die den Index Nr hat. Wie Sie schon aus den vorigen Kursteilen wissen, benötigen wir das HDC, um mit den API-Funktionen BitBlt und StretchBlt zu arbeiten.
Die Funktion selbst ist sehr einfach aufgebaut: Zuerst wird wieder der "Fehlerblocker" eingeschaltet, anschließend wird das hDC der Ressource zurückgegeben.

Function RES_RESET ()
 
    On Local Error Resume Next 
 
    For RES_COUNT = RES_COUNT To 0 Step -1
        Unload RESSOURCEN.RES(RES_COUNT)
    Next RES_COUNT
 
    RES_COUNT = 0
 
  End Function

Listing 4: Die Funktion "RES_COUNT"

Die Prozedur RES_RESET dient dazu, alle angelegten Ressourcen zu löschen. In unserem Spiel werden wir diese Prozedur zwar nicht benötigen, jedoch kann ich mir vorstellen, daß es durchaus Einsatzgebiete in anderen Spielen dafür gibt. Und da wir jetzt ja möglichst flexibel programmieren wollen gehört die Prozedur dazu :-)

Sub RES_CLEAR (Nr)

    On Local Error Resume Next 

    RESSOURCEN.RES(ResCount).Picture = LoadPicture()
 
End Sub

Listing 5: Die Sub "RES_CLEAR"

Die letzte Prozedur der "Ressourcen-Engine" heißt RES_CLEAR(Nr) und löscht den Inhalt der Ressource mit dem Index Nr. Dazu wird die Funktion LoadPicture() ohne Datei verwendet. Wenn man hinter LoadPicture den Dateinamen wegläßt, wird ein leeres Bild erzeugt und somit die Ressource geleert.
Diese Prozedur ist sinnvoll, wenn Sie beispielsweise ein großes Hintergrundbild nicht mehr benötigen und keinen Speicher verschwenden wollen.
Damit haben wir jetzt alle Funktionen und Prozeduren dieser Engine fertig. Speichern Sie die Form am besten unter dem Dateiamen "RESOURCE.FRM" und das Codemodul unter dem Namen "RESOURCE.BAS". Diese Dateien werden wir später noch benötigen.

2.3 Typen für Spieler  

Wie ich schon in Spieleprogrammierung 1 gesagt habe, kann man recht gut mit eigenen Datentypen arbeiten, wenn man Spielobjekte oder Sprites verwalten will. Wenn Sie nicht mehr genau wissen, wie man Typen definiert und benutzt, so sollten Sie sich den VisualBasic Kurs-Teil 3 ansehen.
Im vorigen Kurs haben wir eigentlich nur eine Prozedur für Sprites benötigt:

ZeichneSprite (Nr, X, >)

Diese Prozedur würde aber hier nicht mehr funktionieren, da wir die Bilder nicht mehr in den Steuerelemente-Datenfeldern BITMAP() und MASKE() haben. Wir benutzen jetzt eine Ressourcen-Engine. Was liegt also näher als eine Engine für Sprites?

2.4 Die Sprite-Engine  

Um einen Sprite zu zeichnen, müssen wir wissen, wo die Maske ist, wo die Bitmap ist und wohin der Sprite gezeichnet werden soll. (Hier noch einmal das Schema aus dem vorherigen Teil):


Abbildung 2: Sprites

Ein Sprite besteht also aus der Maske, der Bitmap und der Position, an die er gezeichnet werden soll. Daraus wird auch der Datentyp Sprite bestehen. Doch zuerst noch ein anderer, wichtiger Datentyp:

Type pos
    X As Integer             'X-Position (linker Rand) 
    Y As Integer             'Y-Position (oberer Rand) 
    W As Integer             'Breite des Rechtecks 
    H As Integer             'Höhe des Rechtecks 
    HDC As Integer           'DeviceContext des Bildfelds 
End Type

Listing 6: Der Datentyp "pos"

Wie Sie den Kommentaren hinter den Elementen von "pos" entnehmen können beinhaltet "pos" die Koordinaten eines Rechtecks und ein DC. Damit können Sie einen Bildausschnitt auf einem beliebigem Bildfeld definieren:


Abbildung 3: Beispiel zu pos

Der Datentyp "pos" kann nun sehr vielseitig eingesetzt werden: Er kann für die Maske, für die Bitmap und auch für die spätere Position des Sprites eingesetzt werden:

Type Sprite
    Bit As pos    'Bilddaten
    Maske As pos  'Maske
    Nach As pos   'Position, an der der Sprite
   'angezeigt werden soll
End Type

Listing 7: Einsatzbeispiel für den Datentyp "pos"

Der Datentyp "Sprite" basiert nun auf dem Datentyp Pos. Das scheint vielleicht auf den ersten Blick umständlich, jedoch werden Sie sehen, daß es eine Vielzahl von neuen Möglichkeiten gibt. Ein Sprite besteht nun aus den Variablen Bit, Maske und Nach des Typs Pos.
Nachdem wir nun den Aufbau eines Sprites festgelegt haben, kommen natürlich die Prozeduren zum Erstellen und Anzeigen von Sprites.

Sub POS_SET (P As pos, X, Y, W, H, PicBox As PictureBox)
 
    P.X = X
    P.Y = Y
    P.W = W
    P.H = H
    P.HDC = PicBox.HDC
 
  End Sub

Listing 8: Die Sub "POS_SET"

Warum denn schon wieder POS? Ich dachte, jetzt kämen die Sprite-Prozeduren! Schon richtig, aber da Sprites ja auf Variablen des Typs "Pos" basieren benötigen wir auch Funktionen, mit denen wir diese Variable leicht ändern können. Die Prozedur POS_SET macht nichts anderes, als der Variable P die Werte X,Y,W,H und das DeviceContext von PicBox zuzuweisen. PicBox ist, wie Sie oben sehen eine Variable des Typs "PictureBox". Mit anderen Worten: Hier wird ein Bildfeld übergeben! Hier ein Beispiel, wie man mit POS_SET einen Sprite setzen kann:

 Dim Spr As Sprite

   POS_SET Spr.Bit, 0, 0, 100, 100, RESSOURCEN.RES(1)
   POS_SET Spr.Maske, 0, 0, 100, 100, RESSOURCEN.RES(2)
   POS_SET Spr.Nach, 0, 0, 100, 100, DISPLAY

Listing 9: Das setzen von Sprites

So kann könnte man die Position der Bitmap, der Maske und die Ausgangsposition festlegen. Und warum ist das nun einfacher? Tja, das sieht natürlich recht umständlich aus, aber keine Angst, das Programm nimmt Ihnen diese Arbeit ab! In den seltensten Fällen werden Sie selbst die Prozedur POS_SET benutzen.
Eine vereinfachte Funktion von POS_SET gibt es übrigens auch:

Sub POS_SET_MAX (P As pos, PicBox As PictureBox)
 
  P.X = 0
  P.Y = 0
  P.W = PicBox.ScaleWidth
  P.H = PicBox.ScaleHeight
  P.HDC = PicBox.HDC
 
End Sub

Listing 10: Die Sub "POS_SET_MAX"

Diese Funktion setzt die Variable P des Typs "pos" so, daß das Rechteck das ganze Bildfeld PicBox ausfüllt. Das Rechteck hat also die Startposition 0, 0 und die gleiche Breite und Höhe wie das übergebene Bild. Auch diese Funktion wird im Normalfall nur von der Engine selbst benutzt.
Nun aber zu den Sprite-Funktionen, mit denen Sie bzw. die Engine später arbeiten werden:

Sub SPRITE_SET (S As Sprite, Bit As Pos, Maske As Pos)
 
  S.Bit = Bit
  S.Maske = Maske
  S.Nach.X = 0
  S.Nach.Y = 0
  S.Nach.W = Bit.W
  S.Nach.H = Bit.H
  
End Sub

Listing 11: Die Sub "SPRITE_SET"

Die Prozedur SPRITE_SET erwartet als Parameter einmal den zu setzenden Sprite und danach zwei Variablen des Typs "pos", die die Position der Bitmap und der Maske angeben. Hier sehen Sie, daß die Variable "Nach", die ja auch zum Datentyp "Sprite" gehört nicht voll benutzt wird. Deren Werte X, Y und HDC brauchen nicht gesetzt zu werden, da wir die Position des Sprites beim Zeichnen bestimmen.

 Sub SPRITE_SET_MAX (S As Sprite, Bit As PictureBox, _
                                  Maske As PictureBox)
   Dim B As Pos, M As Pos, N As Pos

   POS_SET_MAX B, Bit
   POS_SET_MAX M, Maske

   SPRITE_SET S, B, M

 End Sub

Listing 12: Die Sub "SPRITE_SET_MAX"

Die Prozedur SPRITE_SET_MAX setzt ebenfalls einen Sprite, diesmal wird jedoch nur übergeben, welches Bildfeld die Bitmap und welches Bildfeld die Maske enthält. Der Sprite wird dann so eingestellt, daß jeweils die ganze Bildfelder benutzt werden. Lassen Sie sich jetzt nicht verwirren, es folgt noch eine Übersicht der SPRITE_SET-Prozeduren und ihrer Wirkung!

Sub SPRITE_SET_SYNC (S As Sprite, X, Y, W, H, _
                     Bit As PictureBox, Maske As PictureBox)
  S.Bit.X = X
  S.Bit.Y = Y
  S.Bit.W = W
  S.Bit.H = H
  S.Bit.HDC = Bit.hDC
  S.Maske = S.Bit
  S.Maske.HDC = Maske.hDC
  S.Nach.X = 0
  S.Nach.Y = 0
  S.Nach.W = W
  S.Nach.H = H
End Sub

Listing 13: Die Sub "SPRITE_SET_SYNC"

Diese SPRITE_SET-Prozedur setzt einen Sprite an Hand der übergebenen Position (X, Y, W, H). Zusätzlich wird noch das Bildfeld mit der Bitmap und das Bildfeld mit der Maske übergeben. Der Zweck dieser Prozedur ist es, möglichst einfach einen Sprite zu definieren, wenn die Maske an der gleichen Position liegt, wie die Bitmap nur halt in einem anderen Bildfeld.
Jetzt kommt die SPRITE_SET-Prozedur, die den Aufwand mit Pos, SPRITE_SET_SYNC usw. erforderlich macht:

 Sub SPRITE_SET_MESH (Mesh() As Sprite, Start, X, Y, W, H, _
                         Pb1 As PictureBox, Pb2 As PictureBox)

   YPos = 1
   I = Start

   For Iy = 1 To Y
       XPos = 1
       For Ix = 1 To X
           SPRITE_SET_SYNC Mesh(I), XPos, YPos, W, H, Pb1, Pb2
           I = I + 1
           XPos = XPos + W + 1
       Next Ix
       YPos = YPos + H + 1
   Next Iy
   Start = I

 End Sub

Listing 14: Die Sub "SPRITE_SET_MESH"

Auf den ersten Blick sieht diese Prozedur wahrscheinlich sehr wüst aus, jedoch ist sie eigentlich ziemlich simpel. Diese Prozedur geht davon aus, daß Sie ein Bild in mehrere gleich große Rechtecke unterteilt haben und das in jedem Rechteck ein Sprite ist. Als Parameter geben Sie bei dieser Prozedur ein Datenfeld des Typs Sprite an, in die die definierten Sprites aufgenommen werden. Anschließend folgt der Startindex, ab dem gezählt wird. Danach folgen die Anzahl der Kästchen in X-Richtung und die Anzahl der Kästchen in Y-Richtung. Zum Schluß wird die Breite und die Höhe eines Kästchens angegeben, sowie das Bildfeld mit der Bitmap und der Maske.
Im folgenden finden Sie nun endlich eine anschauliche Darstellung der einzelnen SPRITE_SET-Routinen:

SPRITE_SET : Diese Prozedur setzt den Sprite aus zwei beliebigen Rechtecken zusammen. Das eine Rechteck gibt die Bitmap, das andere die Maske an. Man kann also frei wählen, woher man beides nimmt. Es ist damit auch möglich, Bitmap und Maske in einem Bild unterzubringen.


Abbildung 4: SPRITE_SET

SPRITE_SET_MAX : Diese Routine setzt den Sprite aus zwei ganzen Bildfeldern zusammen. Dieses Verfahren haben wir auch im letzten Kursteil benutzt.


Abbildung 5: SPRITE_SET_MAX

SPRITE_SET_SYNC : Diese Prozedur setzt den Sprite aus zwei Rechtecken zusammen, die die gleiche Position haben, jedoch auf zwei unterschiedlichen Bildern sind


Abbildung 6: SPRITE_SET_SYNC

SPRITE_SET_MESH : Diese Routine dient dazu, ein Bild in Sprites umzuwandeln, daß in mehrere Kästchen unterteilt ist. Dieses Verfahren werden wir im folgenden Kursteil ausführlich besprechen. Sie werden dann auch sehen, warum diese Möglichkeit im Endeffekt wesentlich schneller ist, als die anderen.


Abbildung 7: SPRITE_SET_MESH

Und nun die letzte Prozedur für heute:

 Sub SPRITE_DRAW (S As Sprite, X, Y, PicBox As PictureBox)

   S.Nach.hDC = PicBox.hDC

   r% = StretchBlt(S.Nach.hDC, X, Y, S.Nach.W, S.Nach.H, _
        S.Maske.hDC, S.Maske.X, S.Maske.Y, S.Maske.W, _
        S.Maske.H, BIT_AND)

   r% = StretchBlt(S.Nach.hDC, X, Y, S.Nach.W, S.Nach.H, _
       S.Bit.hDC, S.Bit.X, S.Bit.Y, S.Bit.W, _
       S.Bit.H, BIT_INVERT)

 End Sub

Listing 15: Die Sub "SPRITE_DRAW"

Diese Prozedur bekommt als Parameter den Sprite S, die Position und das Ausgabe-Bildfeld übergeben. Der Sprite wird nun nach dem aus dem vorherigen Teil bekannten Verfahren auf das Bildfeld an die Position X/Y gezeichnet.
Wenn Sie in diesem Teil nicht alle Prozeduren und Funktionen verstanden haben, so ist das nicht weiter tragisch. Wie man die Funktionen verwendet wird im nächsten Teil an Hand des Pacman-Spiels gezeigt. Wenn Sie Lust haben können Sie bis zum nächsten Mal ja selbst etwas mit diesen Routinen arbeiten.

2.5 Ende Teil 2  

Damit wäre der zweite Teil des Spiele-Kurses abgeschlossen. Im nächsten Teil folgt das eigentliche Jump’n’Run-Spiel.
Wenn Sie weitere Themenwünsche oder Ideen für den nächsten Aufbaukurs haben, schreiben Sie mir doch einfach ein E-Mail. Meine Adresse:
Wie immer freue ich mich auch über jede Art von Feedback zu dem Kurs und über Fragen zu VB oder anderen Themen. Wenn Sie Zeit haben, können Sie ja die Programmierer-Konferenz besuchen, die immer am Donnerstag um 20:00 Uhr im Konferenzraum 2 von AOL stattfindet (STRG+K Konferenzen, Raum 2).
Ich hoffe, wir sehen uns dann wieder.

Quellcodes und Artikel (Worddokument) als Download [49.2 KB] [49200 Bytes]

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.