Ich möchte mit diesem Kurs zeigen, dass es entgegen anders lautenden Gerüchten sehr gut möglich ist, in Visual Basic Spiele zu programmieren, die durchaus erstaunliche Ablauf-Geschwindigkeiten erreichen.

Der vorliegende Spiele-Kurs umfasst insgesamt 125 DIN A4-Seiten. Auf Grund dieser Größe wird er in 4 Teilen veröffentlicht. Der erste Teil berichtet über die grundlegenden Techniken, wie Sprites, Sounds Tastaturabfrage usw.

In diesem Kursabschnitt werden Optimierungen und Sprites behandelt. Ich wünsche also viel Vergnügen.


Dominik Scherlebeck 07/2001

Anregungen oder Tipps an Dominik Scherlebeck
 
  2.1 Zu diesem Kurs [ Top ]

Dieser Teil dreht sich wieder um die Programmierung eigener Spiele mit Visual Basic. 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, dass 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 [ Top ]

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, dass Sie nicht jedes Mal, 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-6 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 Schluss die ScaleMode-Eigenschaft auf 3 (Pixel).

 

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

Als erstes wird in der Prozedur die Fehlerüberwachung so eingestellt, dass 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, dass 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, dass 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, dass 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

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

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

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, dass 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

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ässt, 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 [ Top ]

Wie ich schon im Teil 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 Visual Basic Kurs-Teil 3 ansehen.

Im vorigen Kurs haben wir eigentlich nur eine Prozedur für Sprites benötigt:
ZeichneSprite (Nr, X, y)

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 [ Top ]

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):

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

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:

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 das Sprite angezeigt werden soll
End Type

Der Datentyp Sprite basiert nun auf dem Datentyp Pos. Das scheint vielleicht auf den ersten Blick umständlich, jedoch werden Sie sehen, dass 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

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

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

Diese Funktion setzt die Variable P des Typs pos so, dass 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

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, dass 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

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, dass 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_MAX(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

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 Function

Auf den ersten Blick sieht diese Prozedur wahrscheinlich sehr wüst aus, jedoch ist sie eigentlich ziemlich simpel. Diese Prozedur geht davon aus, dass 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 Schluss 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.

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

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

SPRITE_SET_MESH:
Diese Routine dient dazu, ein Bild in Sprites umzuwandeln, dass 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.

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

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 [ Top ]

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 eine eMail. Meine Adresse:

 dominik5@debitel.net

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 Donnerstags um 20:00 Uhr im Konferenzraum 2 von AOL stattfindet (STRG+K Konferenzen, Raum 2).

Ich hoffe, wir sehen uns dann wieder.
Download
Spielekurs_Teil2.zip
 (50,4 kB)
Downloads bisher: [ 6839 ]

Startseite | VB/VBA-Tipps | Projekte | Tutorials | API-Referenz | Komponenten | Bücherecke | VB-/VBA-Forum | VB.Net-Forum | DirectX | DirectX-Forum | Foren-Archiv | VB.Net | Chat | Links | Suchen | Stichwortverzeichnis | Feedback | Impressum

Seite empfehlen Bug-Report

Letzte Aktualisierung: Mittwoch, 15. Dezember 2004