Die Community zu .NET und Classic VB.
Menü

Der große VB Spielekurs Teil 3

 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.
Das vorliegende 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.
Im zweiten Teil wurden Optimierungen vorgenommen und die Sprites erneut erläutert.
In diesem Kursabschnitt wird das Spiel "PACMAN" erstellt.

Mit freundlichen Grüßen
Dominik Scherlebeck (PCDVisual)

3.1 Zu diesem Kurs  

Wie schon in den vorigen Kursteilen angekündigt werden Sie nun lernen, wie man ein vollständiges PacMan-Spiel auf VisualBasic schreiben kann. Dafür werden wir die folgenden, bereits besprochenen, Module einsetzen:

MEDIA.FRMDiese Form enthält das MCI-Steurelement, das im Modul MIDI.BAS angesprochen wird (Spiele-Kurs #1)
MIDI.BASDieses Modul steuert das MCI-Steuerelement an, um MIDI-Dateien abzuspielen (Spiele-Kurs #1)
RESOURCE.FRMDiese Form faßt später die Bilder auf, die während der Laufzeit geladen werden (Spiele-Kurs #2)
RESOURCE.BASDie Routinen in diesem Modul dienen zum Laden von Bildern während der Laufzeit (Spiele-Kurs #2)
SPRITE.BASDieses Modul dient zum Erstellen und Zeichnen von Sprites (Spiele Kurs #2).
WAVE.BASDieses Modul enthält zwei Routinen zum Ausgeben von WAVE-Dateien

Beachten Sie bitte, daß es für VisualBasic 3.0 bzw. VB 4.0 - 16 Bit andere Module gibt als für VB 4.0 - 32 Bit und VB 5.0:
Die 16-Bit-Dateien für die folgende Demo finden Sie im Verzeichnis "DEMO", die 32-Bit-Dateien im Verzeichnis "DEMO32". Diese Module wurden in den angegebenen Kursteilen ausführlich besprochen. Das Verständnis der einzelnen Funktionen ist aber in diesem Teil nicht so wichtig. Wenn Sie also im letzten Teil - der zugegebenermaßen recht schwer war - nicht alle Routinen verstanden haben, so wird sich das in diesem Teil nicht auswirken.
Die in diesem Kurs besprochen Module (und das fertige Programm als Ausblick auf den nächsten Teil) finden Sie komplett in den Verzeichnissen "PACMAN", und "PACMAN32".
Also auf ein Neues :-)

3.2 Sprite DEMO  

Die Sprite-Routinen aus dem letzten Teil sind ein wichtiger Bestandteil des Spieles. Daher kommt - bevor wir mit dem Spiel anfangen – ein kleines Programm, daß die Funktion dieser Routinen veranschaulicht.
Erstellen Sie bitte als erstes ein neues Projekt, und fügen Sie nun die folgenden Dateien hinzu (diese finden Sie im Archiv dieses Kurses bzw. die MCI-Datei im WINDOWS\SYSTEM-Verzeichnis auf Ihrer Festplatte ):

  • MCI.VBX bzw. MCI.OCX
  • RESOURCE.FRM
  • RESOURCE.BAS
  • SPRITE.BAS
  • WAVE.BAS

Gleich etwas wichtiges zu Anfang: Wenn Sie mit einer 32-Bit-VB-Version arbeiten, so dürfen die Module nie die Namen tragen, die in dem Modul als Datentypen, Prozeduren oder Variablen verwendet werden! Setzen Sie die Name-Eigenschaft vom Modul SPRITE.BAS nicht auf "SPRITE" sondern ändern Sie diese in "MODUL_SPRITE". Ansonsten gibt VisualBasic beim Startversuch Fehler aus!
Öffnen Sie nun die noch leere Form "Form1". Setzen Sie auf diese ein Bildfeld mit dem Namen "Display", und setzen Sie die Eigenschaft "AutoRedraw" auf "True" und "ScaleMode" auf "3 - Pixel".
Plazieren Sie das Bildfeld möglichst in der Mitte der Form, da wir in diesem Beispiel keine anderen Steuerelemente verwenden. Wenn der Benutzer später mit der Maus auf diese Bildfeld klickt soll an der Stelle ein kleiner Sprite angezeigt werden.
In diesem Programm wollen wir die Funktion SPRITE_SET_MESH benutzen um über einen Befehl eine ganze Reihe von Sprites zu erstellen. Diese Funktion wird aufgerufen mit:

SPRITE_SET_MESH Datenfeld(), Anfang, X, Y, W, H, Bitmap, Maske

Datenfeld()Hier wird ein Sprite-Datenfeld übergeben.
Dim Test(100) As Sprite
SPRITE_SET_MESH Test(), ....
Wenn Sie ein Datenfeld übergeben, dann schreiben Sie den Namen gefolgt von einer leeren Klammer. Sie dürfen keine Zahl schreiben, sonst wird nämlich nur der Eintrag mit diesem Index übergeben und nicht das ganze Datenfeld.
AnfangGibt den Index im Datenfeld an, ab der die Sprites erstellt werden sollen.
In unserem Beispiel verwenden wir hier 0, damit der erste erstelle Sprite den Eintrag 0 bekommt, der zweite den Eintrag 1 usw.
XX gibt an, wie viele Kästchen in X-Richtung in der Ressource sind
YY gibt an, wie viele Kästchen in Y-Richtung in der Ressource sind
WGibt die Breite eines Kästchens in Pixeln an
HGibt die Höhe eines Kästchens in Pixeln an
BitmapHier wird ein Bildfeld übergeben, aus der die Bitmap-Daten kommen
MaskeUnd hier wird noch das Masken-Bildfeld übergebenn

Um die Geschichte mit den Kästchen noch zu verdeutlichen, kommt hier eine kurze Erläuterung. Die Routine SPRITE_SET_MESH geht davon aus, daß das Bildfeld in rechteckige Kästchens aufgeteilt ist. Jedes Kästchen enthält einen Sprite. Um nun aus diesen Kästchen einzelne Sprites zu machen, die man dann einfach über eine Nummer anspricht, benötigt die Routine ein Datenfeld, in daß die Sprites kommen. Außerdem muß die Routine noch wissen, wie viele Kästchen sich auf dem Bild befinden und wie groß die einzelnen Kästchen sind. In dem Bild oben sehen Sie, wie aus 4 x 3 Rechtecken zwölf neue Sprites entstehen. Die Kästchen müssen übrigens durch Linien voneinander getrennt werden. W und H geben nämlich nur die


Abbildung 1: Sprites

Ausdehnung des eigentlichen Sprites (innerhalb der roten Umrandung) an. Bei diesem Beispiel muß X = 4 und Y = 3 sein, damit aus allen Kästchen Sprites entstehen. Die Kästchen in diesem Kurs haben immer die Größe 20 x 20 Pixel. Damit müßte der Aufruf für die obige Situation lauten:

SPRITE_SET_MESH Spr(), 0, 4, 3, 20, 20, Bild1, Bild2
Anschließend sind die Einträge 0 - 11 des Datenfeldes Spr() mit den Sprites aus dem Bild belegt. Daran sehen Sie, wie effektiv sich diese Routine einsetzen läßt.
Nun müssen wir aber für unser Beispiel-Programm erst das Datenfeld definieren. Öffnen Sie dazu die Prozedur "(allgemein) / (Deklarationen)" der Form.

Dim PFAD$
Dim Spr(100) As Sprite

Listing 1: Der Deklarationsteil

In der ersten Zeile definieren wir die Variable PFAD$, die, wie schon im vorletzten Teil, angibt, wo sich die Daten des Programms befinden. In der zweiten Zeile wird unser Sprite-Datenfeld definiert, daß alle Sprites aufnimmt, die wir benutzen werden.
  Jetzt kommt die Ereignisprozedur FORM_LOAD.

Sub FORM_LOAD ()
 
  PFAD$ = "F:\DATEN\DOCS\VBKURS\SPIELE3\DEMO\"

Listing 2: Die Sub "Form Load"

Zuerst setzen wir unsere Variable PFAD$, so daß VB alle Dateien finden kann, die zu dem Spiel gehören. Hier müssen Sie den Pfad auf Ihrer Festplatte eintragen. Befinden sich die Dateien beispielsweise im Verzeichnis "C:\VBKURS\SPIELE3\DEMO\" so setzen Sie auch die PFAD$-Variable auf dieses Verzeichnis. Wenn Sie später eine EXE-Datei erstellen, müssen Sie PFAD$="" schreiben, damit die Dateien im aktuellen Verzeichnis gesucht werden.

B1 = RES_NEW(): RES_LOAD B1, PFAD$ + "BIT\TEST.BMP"
M1 = RES_NEW(): RES_LOAD M1, PFAD$ + "MASKE\TEST.BMP"
Nun werden die Bilder geladen. Dazu wird mit der Funktion RES_NEW() erst eine neue Ressource erstellt, die das Bild aufnehmen soll. Den Rückgabewert sichern wir in der Variable B1 (der Name ist abgeleitet von Bitmap 1). Danach folgt ein Doppelpunkt. Dieser trennt zwei Anweisungen und ermöglicht es, mehrere Befehle in eine Zeile zu schreiben. Beispiel:
A=10
B=5
entspricht
A=10: B=5
Anschließend wird mit dem Befehl RES_LOAD das Bild "BIT\TEST.BMP" in die Ressource mit der Nummer B1 geladen. Das gleiche Spielchen wiederholen wir mit der Variable M1 (abgeleitet von Maske 1) und der Datei "MASKE\TEST.BMP". Nun haben wir zwei Bitmaps geladen, die eine enthält die Bitmapdaten, die andere die Maske.
Die Bilder sind wieder in Kästchen aufgeteilt, wobei ein Kästchen innen 20*20 Pixel groß ist. Uns interessiert nur ein Bereich von 8 Kästchen Länge und drei Kästchen Höhe:


Abbildung 2: Die Aufteilung

Jetzt verwenden wir den SPRITE_SET_MESH-Befehl um die Sprites zu erstellen:

SPRITE_SET_MESH Spr(), 0, 8, 3, 20, 20, RESSOURCEN.RES(B1), _
                RESSOURCEN.RES(M1)
An SPRITE_SET_MESH übergeben wir als erstes das Datenfeld Spr(), daß wir definiert haben. Anschließend übergeben wir die Anzahl der Kästchen in X-Richtung (in diesem Fall 8, wie man an dem Bild oben sehen kann) und in Y-Richtung (3 Kästchen). Anschließend folgt die innere Breite und Höhe eines Kästchens in Pixeln. Nun müssen wir die Bildfelder übergeben, in denen die Bilder für die Bitmapdaten und für die Maske sind.
Jetzt fehlt nur noch das Ende der Prozedur:

End Sub

Listing 3: Ende von "Form_Load"

Damit hätten wir das schwerste dieses Beispiels hinter uns. Die Sprites befinden sich nun in dem Datenfeld Spr() und können mit der Prozedur SPRITE_DRAW ausgegeben werden. In diesem Beispiel soll jeweils ein zufälliger Sprite ausgegeben werden, wenn der Benutzer auf das Bildfeld klickt. Dazu benutzen wir die Ereignisprozedur DISPLAY_MOUSEDOWN.

          Sub DISPLAY_MOUSEDOWN (Button As Integer, Shift As Integer, X
As Single, Y As Single)
           
            SPRITE_DRAW Spr(Fix(Rnd * 20)), X, Y, DISPLAY
            DISPLAY.Refresh
            
          End Sub

Listing 4: Die Sub "DISPLAY_MOSEDOWN

Der Ereignisprozedur MOUSEDOWN werden einige wichtige und vor allem nützliche Parameter übergeben: Button enthält den Status der Mausknöpfe. 1 steht hierbei für die linke Maustaste, 2 für die rechte und 3 für beide. Shift gibt an, ob die Umschalttaste gedrückt ist (=1) oder nicht (=0). X und Y geben die Mausposition relativ zur oberen, linken Ecke des Bildfeldes an.
In der zweiten Zeile wird die Prozedur SPRITE_DRAW aufgerufen. Als erstes übergeben wir SPRITE_DRAW den Sprite, der gezeichnet werden soll. Hierbei benutzen wir den Eintrag des Datenfeldes Spr() mit dem Index FIX(RND*20), was eine Zufallszahl zwischen 0 und 20 ist. Dadurch werden zufällige Sprites ausgegeben.
Anschließend übergeben wir die Position. Hier verwenden wir einfach die Mauskoordinaten. Der letze Übergabeparameter ist das Bildfeld, auf dem der Sprite ausgegeben werden soll. In dem Beispiel ist dies das Bildfeld "DISPLAY".
In der letzen richtigen Zeile der Prozedur wenden wir die REFRESH-Methode des Bildfeldes DISPLAY an, damit die Änderung auch angezeigt werden. Die Prozedur schließen wir nun wieder mit "End Sub" ab.
Damit haben wir das Beispielprogramm abgeschloßen. Führen Sie es mit F5 aus und sehen Sie sich an, was Sie mit diesen wenigen Zeilen Programmcode erreicht haben.


Abbildung 3: Ein Spielfeld

3.3 Typen für Spieler 2  

Im vorigen Kurs haben wir ja schon angefangen, Typen zu definieren, auf denen das spätere Spiel basieren soll. Die Routinen für Ressourcen und Sprites sind zwar sehr wichtig, jedoch spielen sie nur für die Ausgabe auf dem Bildschirm eine Rolle. Der Kern des Spiels fehlt bis jetzt.
Wir wollen natürlich nun nicht wieder mit dem relativ schlechten Programmierstil aus Kurs 1 weitermachen und alle Werte in einzelnen Datenfeldern mit solchen Namen wie ObjetcX(9) speichern. Daher verwenden wir wieder eigene Datentypen.
Auch dieses Spiel soll auf Objekten basieren, die sich auf dem Spielfeld bewegen und agieren können. Damit haben wir praktisch schon die zwei wichtigsten Datentypen: FieldType und ObjType (ObjectType). Fangen wir erst einmal mit FieldType an, da daß Spielfeld ja zum Rahmen des Spiels gehört. Legen Sie dazu ein Modul an, daß Sie unter dem Dateinamen "OBJECT.BAS" speichern. Öffnen Sie die Prozedur "(allgemein) / (deklarationen)".

Type FieldType
  V As Integer   'Visible - Ist das Feld überhaupt belegt? 
  S As Sprite    'Sprite, der für das Feld benutzt wird 
  B As Integer   'Block - handelt es sich um eine solid Wand? 
End Type

Listing 5: Der Typ "FildType"

Der Datentyp ist recht einfach aufgebaut. Das Element V gibt an, ob das Spielfeld sichtbar, also überhaupt benutzt wird. S ist eine Variable des Datentyps Sprite und gibt an, was an dieser Stelle gezeichnet werden soll, und B gibt an, ob der Spieler bzw. die Monster sich durch dieses Feld bewegen können oder ob es eine Wand ist.
Jetzt müssen wir natürlich ein Datenfeld anlegen, daß die Felder aufnimmt:

Global FIELD() As FieldType

Global FIELD_W             'FIELD_W = Breite eines Feldes 
Global FIELD_H             'FIELD_H = Höhe eines Feldes 
Global FIELD_X             'Breite des Spielfeldes (in Feldern) 
Global FIELD_Y             'Höhe des Spielfeldes (in Feldern)

Listing 6: Die Deklarationen der Felder

Das globale Datenfeld FELD() des Typs FieldType bekommt vorläufig noch keine festgelegten Dimensionen. Diese werden erst beim Laden eines Levels festgelegt. Anschließend werden noch einige globale Variablen definiert, die Informationen über das Spielfeld speichern. FIELD_W gibt an, wie breit (in Pixeln) ein Feld ist, FIELD_H gibt die Höhe eines Feldes in Pixeln an, FIELD_X und FIELD_Y geben die Größe des Feldes an.
Nun folgen noch die Prozeduren, die mit diesem Datentyp arbeiten. Es hat ja schließlich keinen Sinn, wenn wir erst ein eigenes Modul für diesen Typ erstellen und dann die ganze Verwaltungsarbeit auf herkömmliche Weise erledigen.

         Sub FIELD_SIZE (X, Y, W, H)
           
           ReDim FIELD(X, Y) As FieldType
           
           FIELD_X = X
           FIELD_Y = Y
           FIELD_W = W
           FIELD_H = H

           For AX = 0 To X
             For AY = 0 To Y
               FIELD(AX, AY).V = 0
             Next AY
           Next AX
         
         End Sub

Listing 7: Die Sub "FIELD_SIZE"

Die Prozedur FIELD_SIZE ist sehr wichtig, damit man überhaupt etwas mit dem Datenfeld FIELD() machen kann. Es bekommt die Werte X,Y,W und H übergeben. X und Y geben die Größe des Spielfeldes an, W und H geben die Größe eines Feldes in Pixeln an.
Jetzt wird mit dem Befehl ReDim das Datenfeld auf die erforderliche Größe eingestellt. Das Datenfeld FIELD() ist zweidimensional, genau wie das Spielfeld eines Brettspiels.
Die Werte von X,Y,W und H werden in den Variablen FIELD_X, FIELD_Y usw. gespeichert, damit man später einfach darauf zurückgreifen kann.
Nun werden alle Einträge von FIELD() durchgegangen und jeweils das Element V auf 0 gesetzt. Dadurch sind standardmäßig alle Felder unsichtbar.
Wir brauchen auch eine Prozedur, mit der wir gezielt auf einzelne Felder zugreifen können, um diese zu verändern. Ansonsten wäre das Spielfeld immer leer, was nicht ganz in unserem Interesse sein dürfte.

 Sub FIELD_SET (X, Y, V, S As Sprite, B)

   FIELD(X, Y).V = V
   FIELD(X, Y).B = B

   FIELD(X, Y).S = S
   FIELD(X, Y).S.NACH.W = FIELD_W
   FIELD(X, Y).S.NACH.H = FIELD_H

 End Sub

Listing 8: Die Sub "FIELD_SET"

Der Prozedur FIELD_SET werden alle Werte übergeben, die für das Feld wichtig sind: X und Y geben an, welches Feld geändert werden soll, V gibt an, ob das Feld sichtbar sein soll (=1), S ist eine Variable des Typs Sprite die angibt, was überhaupt hier gezeichnet werden soll, B gibt eben an, ob das Spielfeld eine Wand(=1) ist oder nicht (=0). Nun werden die Elemente des Eintrags von FIELD() nach und nach auf die übergebenen Werte gesetzt.
FIELD(x,y).V wird natürlich auf V gesetzt. Parallel dazu setzten wir auch FIELD(x,y).B auf B und FIELD(x,y).S auf S.
Jetzt wird FIELD(x, y).S noch etwas bearbeitet, damit auch der übergebene Sprite in das Feld paßt. Die Ausgabebreite und -höhe des Sprites werden auf die Größe eines Feldes gesetzt. So ist sichergestellt das der übergebene Sprite auch wirklich ein Feld ausfüllt und nicht kleiner oder größer ist.
Mit diesen beiden Prozeduren können wir eigentlich schon recht einfach das Spielfeld belegen. Nun muß es aber natürlich noch gezeichnet werden. Dies übernimmt die Prozedur FIELD_DRAW.

Sub FIELD_DRAW (DISPLAY As PictureBox, X, Y)

  For AX = 0 To FIELD_X
     For AY = 0 To FIELD_Y
         If FIELD(AX, AY).V Then 
            FX = X + AX * FIELD_W
            FY = Y + AY * FIELD_H
              If FX <= DISPLAY.ScaleWidth And FY _
               <= DISPLAY.ScaleHeight
                  Then 
              If FX + FIELD_W >= 0 And FY + FIELD_H >= 0 Then 
            SPRITE_DRAW_SOLID FIELD(AX, AY).S, FX, FY, DISPLAY
              End If 
              End If 
         End If 
     Next AY
  Next AX
  
End Sub

Listing 9: Die Sub "FIELD_DRAW"

Der Prozedur FIELD_DRAW wird das Bildfeld übergeben, auf das das Spielfeld gezeichnet werden soll. Danach folgt das X- und das Y-Offset des Spielfeldes. Das bedeutet, wie stark das Spielfeld verschoben dargestellt sein soll.


Abbildung 4: Die Aufteilung

In dem Bild sehen Sie ein Spielfeld, das nach links und nach oben verschoben ist. Das können wir erreichen, indem wir für X einen Wert von –25 (1.5 Kästchen) und für Y ebenfalls –25 übergeben. Wenn wir positive Werte angeben wird das Spielfeld in die entgegengesetzte Richtung verschoben. Nun aber weiter mit der Prozedur.
Es werden alle Einträge des Datenfeldes FIELD() durchlaufen. Dabei wird jeweils geprüft, ob das Feld sichtbar ist. Ist dies der Fall, so wird die Position des Feldes ausgerechnet und geprüft, ob das Feld überhaupt innerhalb des gerade sichtbaren Bereichs liegt.
Anschließend wird eine neue SPRITE-Routine benutzt, um das Feld endgültig auszugeben. Diese haben wir im letzten Kurs nicht besprochen: SPRITE_DRAW_SOLID. Sie funktioniert im Grunde genauso wie die normale SPRITE_DRAW-Funktion, nur wird hier keine Transparenz berücksichtigt. Daher ist die Prozedur schneller als die andere, kann aber auch keine transparenten Stellen darstellen. Hier das Listing dieser Prozedur (fügen Sie diese Bitte auch in das Modul SPRITE.BAS ein, damit Sie die Übersicht behalten):

Sub SPRITE_DRAW_SOLID (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.Bit.hDC, S.Bit.X, S.Bit.Y, S.Bit.W,_
  S.Bit.H, BIT_COPY)
  
End Sub

Listing 10: Die Sub "SPRITE_DRAW_SOLID"

Mit diesen Routinen können wir ein Spielfeld erstellen und anzeigen lassen. Das alleine macht natürlich noch kein Spiel aus. Es fehlt der Spieler, die Gegner, die Gegenstände...

3.4 Die Objekte  

Bevor wir damit anfangen, einen Datentyp für die Objekte zu erstellen, sollten wir uns überlegen, was alles zu einem Objekt gehört.
Das Objekt muß natürlich X- und Y-Koordinaten haben, da wir sonst nicht wissen, wo es sich auf dem Spielfeld befindet. Das Objekt soll sich bewegen können, also benötigen wir zwei Variablen die die X- und Y-Geschwindigkeit angeben. Weiterhin muß das Objekt einen Energiezähler haben, eine Variable die den Typ des Objekts speichert, einen Sprite, der gezeichnet werden soll und eine Variable, die angibt, ob das Objekt sichtbar ist. Zusätzlich benutzen wir noch drei Reservevariablen die wir R1, R2 und R3 nennen.
Um die Handhabung von Extras und PowerUps zu vereinfachen, bekommt jedes Objekt ein Inventar, das Gegenstände aufnehmen kann. Dazu ist ein Unter-Datentyp von Nöten. Öffnen Sie nun wieder das Codemodul "OBJECT.BAS" und dort die Prozedur " (allgemein) " / " (Deklarationen) ".

Type ItemType
  T As Integer 'Typ des Items 
  E As Integer 'Energie / Anzahl / verbleibende Zeit für Extras 
End Type

Listing 11: Ergänzung der Deklarationen

Dieser Datentyp hat nur zwei Elemente: T gibt den Typ an, und E gibt an, wie viele Gegenstände dieses Typs im Inventar sind bzw. wie lange der Gegenstand noch aktiv ist usw.
Jetzt erstellen wir den Datentyp ObjType:

Type ObjType
  X As Integer     'X-Position 
  Y As Integer     'Y-Position 
  OX As Integer    'Vorherige Position (X) 
  OY As Integer    'Vorherige Position (Y) 
  
  SX As Integer    'X-Geschwindigkeit 
  SY As Integer    'Y-Geschwindigkeit 
  
  E As Integer     'Energie 
  T As Integer     'Typ des Objekts 
  FRAME As Sprite 'Sprite des Objekts 
  
  R1 As Integer    'Reserve-Wert #1 
  R2 As Integer    'Reserve-Wert #2 
  R3 As Integer    'Reserve-Wert #3 
  
  V As Integer     'Für andere Objekte "sichtbar" 
  
  ITEM(20) As ItemType    'Welche Items hat das Objekt 
                          'bei sich?
End Type

Listing 12: Der Objekttyp

Wie Sie sehen, habe ich "eigenmächtig" noch die Elemente OX und OY eingeführt. Diese Variablen enthalten Sicherungswerte. Wir werden sie noch benötigen, wenn wir uns mit der Steuerung auseinandersetzen.
Auch für diesen Datentyp brauchen wir einige globale Variablen als Hilfe:

Global ObjDummy As ObjType
Global MAX_DIFF
ObjDummy ist ein leeres Objekt und MAX_DIFF gibt an, wie genau Kollisionen geprüft werden sollen. Nachdem wir den globalen Teil des Moduls erstellt haben, kommen jetzt die OBJ_xxxxxxx Routinen. Auf ihnen basiert das ganze Spiel.
Fangen wir mit OBJ_SET an, da man mit dieser Routine neue Objekte erstellen kann.

Sub OBJ_SET (O As ObjType, X, Y, E, SX, SY, T, _
            FRAME As Sprite, V)
  O.X = X
  O.Y = Y
  O.OX = X
  O.OY = Y
  O.E = E
  O.T = T
  O.FRAME = FRAME
  O.V = V
  
  O.SX = SX
  O.SY = SY
  
End Sub

Listing 13: Die Sub "OBJ_SET"

Der Routine wird einmal das zu setzende Objekt (O As ObjectType) übergeben und die Werte, die für die Einstellungen relevant sind. Nach den Koordinaten, der Energie und der Geschwindigkeit in X- und Y-Richtung wird der Typ des Objekts übergeben. Danach kommt noch der Sprite, der für das Objekt gezeichnet werden soll und die Unsichtbar-Eigenschaft des Objekts.
Parallel dazu die Routine OBJ_CLEAR, die ein bestehendes Objekts löscht, indem es alle Eigenschaften des Objekts auf Null setzt:

 Sub OBJ_CLEAR (O As ObjType)
 
   O.X = 0
   O.Y = 0
   O.E = 0
   O.T = 0
   O.V = 0
   O.SX = 0
   O.SY = 0
  
   O.R1 = 0
   O.R2 = 0
   O.R3 = 0
   
   For I = 0 To UBound(O.ITEM)
       O.ITEM(I).T = 0
   Next I

 End Sub

Listing 14: Die Sub "OBJ_CLEAR"

In dieser Routine werden erst die "normalen" Eigenschaften des übergebenen Objekts O auf Null gesetzt und anschließend wird auch noch das Inventar geleert.
Mit diesen beiden Prozeduren können wir nun Objekte anlegen und wieder löschen. Uns fehlen jetzt noch Routinen zum Anzeigen, zur Kollisionsprüfung und zur Verwaltung des Inventars. Und wenn wir die fertig haben? Wie sollen wir dann das Spiel programmieren? Es gibt zwei Möglichkeiten:
Wir packen den eigentlich Quellcode des Spiels in eine eigene Prozedur und von dort werden die Prozeduren zur Objekt- und Sprite-Verwaltung aufgerufen, ähnlich wie es im letzen Spiel der Fall war.
Jedes Objekt verwaltet sich selbst. Das hört sich vielleicht etwas abstrakt an, allerdings steckt ein einfacher Gedanke dahinter: Immer wenn etwas mit dem Objekt passieren soll, z.B. wenn es gezeichnet wird oder es mit einem anderen Objekt kollidiert wird eine vom Objekttyp abhängige Routine aufgerufen, die dafür sorgt, daß das Objekt auf dieses Ereignis reagiert.
Die Wahl die ich für Sie getroffen habe ist klar: Wir arbeiten entsprechend Punkt 2, da wir die andere Möglichkeit ja schon kennen und wissen, daß sie nicht sehr flexibel ist.
Um diese abgewandelte Form der richtigen objektorientierten Programmierung (OOP) zu verwirklichen benötigen wir einige globale Konstanten. Eine Konstante ist nichts anderes als eine Variable deren Wert nicht geändert werden kann. Um eine globale Konstante zu definieren benutzten wir den Befehl

Global Const <Konstante> =  <Wert>
Diese Anweisung funktioniert nur im Deklarationenteil eines Moduls. Also öffnen Sie im Modul "OBJECT.BAS" die Prozedur " (allgemein) / (Deklarationen) ". Jetzt müssen wir uns überlege welche Konstanten wir benötigen.
Um einfach Ereignisse verarbeiten zu können, bekommt jedes mögliche Ereignis eine feste (konstante) Nummer. Wir brauchen dann nur noch die übergebene Variable, die den Ereigniswert beinhaltet, mit einer Konstante vergleichen und können so entsprechend darauf reagieren. Und warum nehmen wir dafür nicht einfach Zahlen wie etwa "If Ereignis = 1 Then..."? Ganz einfach weil man sich Wörter leichter merken kann als Zahlen und man später den Sinn der Abfrage besser nachvollziehen kann, wenn dort steht "If Ereignis = ID_DRAW Then...".
Nun zu den Konstanten, die wir für das Spiel benötigen:
Global Const ID_COLL = 1
Global Const ID_COLL_WALL = 2
Global Const ID_HIT = 3
Global Const ID_DRAW = 4
Global Const ID_MOVE = 5 
Hier sehen Sie schon, daß wir nur wenige "Ereignis"-Konstanten für das Spiel benötigen.
ID_COLL tritt immer dann auf, wenn das Objekt mit einem anderen Objekt kollidiert.
ID_COLL_WALL tritt auf, wenn ein Objekt eine feste Wand berührt.
ID_HIT wird dann übergeben, wenn ein Objekt ein anderes angegriffen hat.
ID_DRAW ist wohl klar, oder? Es gibt an, daß das Objekt gezeichnet werden soll.
ID_MOVE teilt dem Objekt mit, daß es sich bewegen soll.
Diese Ereignisse werden natürlich nicht ohne unser Zutun ausgelöst. Die Routine OBJ_DRAW, die wir gleich kennenlernen, löst beispielsweise vor dem Zeichnen das Ereignis ID_DRAW aus, damit das Objekt vorher noch den zu zeichnenden Sprite setzen kann.
Jetzt kommt die einfache aber sehr elementare Routine OBJ_DRAW, die ein Objekt überhaupt erst auf den Bildschirm bringt:

Sub OBJ_DRAW (O As ObjType, X, Y, PicBox As PictureBox)
  
  If O.T = 0 Then Exit Sub 
  
  OBJ_EVENT O, ID_DRAW, ObjDummy, 0, 0
  
  XX = X + O.X
  YY = Y + O.Y
  SPRITE_DRAW O.FRAME, XX, YY, PicBox
  
End Sub

Listing 15: Die Sub "OBJ_DRAW"

Der Routine OBJ_DRAW wird außer dem zu zeichnenden Objekt noch ein Koordinatenpaar übergeben, das genauso wie beim Spielfeld aus Offset zur eigenen Position zu verstehen ist. Das bedeutet, der Sprite wird relativ zu seiner Position um X Einheiten nach rechts und um Y Einheiten nach unten verschoben. Dadurch können wir, wenn wir das Offset für das Spielfeld haben, es ebenfalls an die Routine OBJ_DRAW übergeben. Als letzter Parameter wird noch das Ausgabebildfeld übergeben.
In der Routine selbst wird zuerst geprüft, ob das Objekt überhaupt belegt ist. Bekanntlich gibt das Element T ja an, welcher Objekttyp vorliegt. Ist dieser Typ=0 geht die Routine davon aus, daß das Objekt nicht benutzt wird und läßt den Sprite aus. In der nächsten Zeile wird das Ereignis ID_DRAW ausgelöst. Dazu wird die Routine OBJ_EVENT (die wir gleich besprechen werden) ausgelöst. Diese verlangt folgende Parameter:

OBJ_EVENT <Objekt>, <Ereignis>, <Auslöser>, <Zusatzwert1>,_
<Zusatzwert2>
Das Objekt ist in diesem Fall natürlich O, das Ereignis ist ID_DRAW. Der Auslöser ist ein Objekt, das für das Auslösen des Ereignisses verantwortlich ist. In unserem Fall übergeben wir hier ObjDummy, da kein Auslöser vorhanden ist. Für die Zusatzwerte übergeben wir 0. Diese dienen nur für bestimmte Ereignisse, die weitere Parameter erfordern.
Im Anschluß daran werden die Koordinaten berechnet und der Sprite des Objekts (O.FRAME) wird mit Hilfe der Prozedur SPRITE_DRAW ausgegeben. Nun zur Routine OBJ_EVENT, die, ob Sie es glauben oder nicht sehr kurz ausfällt:

  Sub OBJ_EVENT (Self As ObjType, MESSAGE, OTHER As ObjType, _
                 ByVal Par1, ByVal Par2)

    PACMAN_EVENT Self, MESSAGE, OTHER, Par1, Par2

  End Sub

Listing 16

Tja, kurz ist die Routine schon, aber der Prozedurkopf hat es diesmal in sich. Vor einigen Variablen steht ByVal, vor anderen nicht. Was bedeutet das?
ByVal gibt an, daß bei der Parameterübergabe der Wert übergeben werden soll. ByVal ist abgeleitet von "Call By Value" (Aufruf mit Wert). Die anderen Möglichkeit ist eine Parameterübergabe, die die Variable an sich übergibt. Diese Methode nennt man in der Programmierung "Call By Reference".
Aha. Und worin besteht der Unterschied?

3.5 Gute Referenzen  

Normalerweise wird eine Variable mit "Call By Reference" übergeben. Das bedeutet, daß die aufgerufene Routine den Wert der Variable die übergeben wurde ändern kann. Hier ein Beispiel dazu. (Geben Sie es, wenn Sie es ausprobieren möchten, separat ein und schreiben Sie es nicht in das Pacman-Spiel!)

 Sub Test (B)
B = 5    
 End Sub
 
 Sub Form_Load
A = 10
MsgBox STR$(A), 0, "A hat den Wert: "
Test A
MsgBox STR$(A), 0, "A hat den Wert: "
 End Sub

Listing 17: Call By Reference

Zuerst wird die Variable "A" der Prozedur Form_Load auf 10 gesetzt. Dann wird der Wert ausgegeben. Anschließend übergibt Form_Load die Variable an die Prozedur Test.
In dieser Prozedur wird B abgeändert. Da wir die Variable "A" mit "Call By Reference" übergeben haben ändert sich dadurch automatisch der Wert von A! Wenn nun die Prozedur beendet ist und A erneut ausgegeben werden soll hat A auch den Wert 5.
Nun die andere Variante:

 Sub Test (ByVal B)
   B = 5 
 End Sub 
 
 
 Sub Form_Load
A = 10
MsgBox STR$(A), 0, "A hat den Wert: "
Test A
MsgBox STR$(A), 0, "A hat den Wert: "
 End Sub

Listing 18: Call by Value

Auch hier wird als erstes die Variable "A" der Prozedur Form_Load auf 10 gesetzt. Dann wird der Wert ausgegeben. Anschließend übergibt Form_Load die Variable an die Prozedur Test. Diesmal jedoch mit der Methode "Call By Value".
In dieser Prozedur wird B abgeändert. Da wir "Call By Value" eingesetzt haben wird A von der Änderung der Variable B nicht beeinflußt! Nachdem die Prozedur beendet ist wird nun die Variable A, die weiterhin den Wert 10 hat, ausgegeben.
Noch etwas: Eigene Datentypen wie z.B. "ObjType" werden immer mit "Call By Reference" übergeben.

3.6 Noch mehr Objekte  

Wozu benutzen wir denn nun im Prozedurkopf von OBJ_EVENT ByVal? Wir verwenden es hier, um zu verhindern, daß die als Par1 und Par2 übergebenen Variablen geändert werden. Wenn wir beispielsweise OBJ_EVENT O, ID_MOVE, ObjDummy, SX, SY aufrufen, würden wir Gefahr laufen, daß die Werte SX und SY abgeändert werden. Und das liegt hier nicht in unserem Interesse.
Die einzige Quellcode-Zeile in der Prozedur OBJ_EVENT ist

USER_EVENT Self, MESSAGE, OTHER, Par1, Par2
Hier wird einfach das Ereignis an eine andere Ereignisroutine mit dem Namen USER_EVENT weitergeleitet. Das scheint vielleicht umständlich, da wir ja direkt in diese Routine den Code packen könnten. Jedoch wollen wir ja ein Engine-Prinzip verwirklichen. Und ein Grundgedanke einer Engine ist wiederverwendbarer Code, der nicht mehr geändert werden muß. Daher wird hier die Kontrolle an eine vom User (oder besser gesagt vom Programmierer) erstellte Routine weitergegeben. Diese Routine befindet sich aus dem oben genannten Grund auch nicht in dem Modul "OBJECT.BAS" sondern in unserem Spiel in dem Modul "PACMAN.BAS".
Aber wir werden jetzt noch nicht mit diesem Modul anfangen, da noch einige OBJ_xxxx-Routinen fehlen. Im nächsten Teil werden wir dann zum letzen Modul "PACMAN.BAS" übergehen und das Spiel zum Laufen bringen.

3.7 Crashtests  

Wir machen nun mit den Kollisions-Routinen weiter, da diese für einen fehlerfreien Spielablauf sehr wichtig sind. Der erste Vertreter dieser Routinen ist die Funktion OBJ_FIELD_ISFREE(x, y, w, h). Diese Funktion gibt an, ob eine Fläche mit den Koordinaten X, Y und der Größe W*H frei ist, also nicht von Wänden des Spielfelds blockiert wird.

Function OBJ_FIELD_ISFREE (X, Y, W, H)

  X1 = X \ FIELD_W
  Y1 = Y \ FIELD_H
  X2 = (X + W) \ FIELD_W + 1
  Y2 = (Y + H) \ FIELD_H + 1

Listing 19: Die Sub "OBJ_FIELD_ISFREE"

Zuerst werden die Felder errechnet, die überprüft werden müssen. Dazu wird die X-Koordinate durch die Breite eines Feldes geteilt und der Nachkommateil wird abgeschnitten (Division mit einem Backslash "\ " statt normal mit einem Slash "/ "). Das gleiche wird mit der Y-Koordinate gemacht. Das Entfernen der Nachkommastellen ist wichtig, weil wir hier immer abrunden wollen. Hat ein Objekt beispielsweise die Position X=1.9, so müssen wir ab Feld 1 (abgerundet) und nicht erst ab Feld 2 (aufgerundet) auf eine Kollision überprüfen.
Anschließend werden die Koordinaten der linken unteren Ecken errechnet. Auch hierbei wird durch FIELD_W und FIELD_H geteilt und der Nachkommateil abgeschnitten. Nun haben wir die Koordinaten X1,Y1 und X2,Y2. Diese geben die Felder an, die wir nun überprüfen müssen.

For AX = X1 To X2
  For AY = Y1 To Y2

    If AX >= 0 And AY >= 0 And AX <= FIELD_X And AY <= FIELD_Y Then

Listing 20: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Es wird bei jedem Feld geprüft, ob das Feld auch innerhalb des wirklichen Spielfelds liegt. So wird verhindert, daß beispielsweise ein Feld mit dem Index -1/-1 überprüft wird, denn dies würde zu einem Fehler führen.

If FIELD(AX, AY).B And FIELD(AX, AY).V Then

Listing 21: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Zusätzlich muß das aktuelle Feld sichtbar und "blockierend" sein, damit es überhaupt weiter geprüft wird. Andernfalls ist es ja auch nicht wichtig

FX = AX * FIELD_W
FY = AY * FIELD_H

Listing 22: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Nun werden die Koordinaten des Feldes errechnet. Vorher hatten wird ja mit X1,Y1 usw. nur den Index des Feldes (1,2 oder 5,10) nun errechnen wir die richtigen Koordinaten, indem wir den Index mit der Breite bzw. Höhe eines Feldes multiplizieren.

If X + W - MAX_DIFF > FX And X < FX + FIELD_W - MAX_DIFF Then 
  If Y + H - MAX_DIFF > FY And Y < FY + FIELD_H - MAX_DIFF Then

Listing 23: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Kollidiert die übergebene Fläche nun wirklich mit dem Spielfeld (hierbei wird übrigens der Toleranzabstand MAX_DIFF berücksichtigt), so wird 0 (=nicht frei) zurückgegeben. Und die Funktion wird beendet.

        OBJ_FIELD_ISFREE = 0
        Exit Function 
      
      End If 
    End If 
    
  End If 
Else

Listing 24: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Wenn eines der Spielfelder außerhalb des definierten Bereichs liegt (wenn es zum Beispiel einen Index von -1/-1 hat), so gilt das Feld auf jeden Fall als blockiert und es wird ebenfalls 0 zurückgeliefert.

          OBJ_FIELD_ISFREE = 0
          Exit Function 
        End If 

      Next AY
    Next AX

Listing 25: Fortsetung der Sub " OBJ_FIELD_ISFREE "

Wenn keine Kollision festgestellt worden ist wird 1 (=frei) zurückgegeben.

  OBJ_FIELD_ISFREE = 1
End Function

Listing 26: Ende der Sub " OBJ_FIELD_ISFREE "

Puh, das war ja eine ganz schöne Arbeit - aber dafür können wir diese Routine gleich mehrfach einsetzen.
Hier folgt nun eine ganze Reihe von Kollisions-Routinen, die in dem Spiel benutzt werden und auf der obigen Routine basieren:

Sub OBJ_COLL_FIELD (C As ObjType)
  
  If C.T = 0 Then Exit Sub 
  
  If OBJ_FIELD_ISFREE(C.X, C.Y, C.FRAME.NACH.W, _
     C.FRAME.NACH.H) = 0 Then 
  
      OBJ_EVENT C, ID_COLL_WALL, ObjDummy, 0, 0
  End If 
 
End Sub

Listing 27: Die Sub " OBJ_COLL_FIELD"

Diese Funktion wird aufgerufen, um zu überprüfen, ob ein bestimmtes Objekt mit einem Feld kollidiert. In der ersten Zeile wird überprüft, ob das Objekt überhaupt belegt ist, damit man sich keine unnötige Mühe macht. Anschließend wird OBJ_FIELD_ISFREE aufgerufen. Dabei werden die X- / Y-Koordinaten des Objekts und die Breite und Höhe des zugehörigen Sprites übergeben. Gibt nun die Funktion 0 zurück (der angegebene Bereich ist nicht passierbar), so wird das Ereignis ID_COLL_WALL ausgelöst.

Function OBJ_DOWN (C As ObjType, D)
  OBJ_DOWN = OBJ_FIELD_ISFREE(C.X, C.Y + D, _
             (C.FRAME.NACH.W), (C.FRAME.NACH.H))
End Function

Listing 28: Die Sub " OBJ_DOWN"

Die Funktion OBJ_DOWN gibt an, ob das übergebene Objekt D Einheiten nach unten gehen kann, ohne mit einer Wand zu kollidieren. Diese Funktion wird später, genauso wie die Kollegen OBJ_UP, OBJ_LEFT und OBJ_RIGHT für die Gegnersteuerung benötigt.

 Function OBJ_UP (C As ObjType, D)
  OBJ_UP = OBJ_FIELD_ISFREE(C.X, C.Y - D, _
           C.FRAME.NACH.W, C.FRAME.NACH.H)
End Function 

Function OBJ_LEFT (C As ObjType, D)
  OBJ_LEFT = OBJ_FIELD_ISFREE(C.X - D, C.Y,
        (C.FRAME.NACH.W), (C.FRAME.NACH.H))
End Function

Function OBJ_RIGHT (C As ObjType, D)
  OBJ_RIGHT = OBJ_FIELD_ISFREE(C.X + D, C.Y,
         (C.FRAME.NACH.W), (C.FRAME.NACH.H))
End Function

Listing 29: Die restlichen Richtungen

Diese Routinen bespreche ich nicht mehr, da Sie das Gleiche machen, wie OBJ_DOWN, nur für eine andere Bewegungsrichtung.
Jetzt kommt die letzte Kollisionsroutine:

Sub OBJ_COLL_OBJ (C1 As ObjType, C2 As ObjType)

  If C1.T = 0 Then Exit Sub 
  If C2.T = 0 Then Exit Sub 
  
  X1 = C1.X
  Y1 = C1.Y
  W1 = C1.FRAME.NACH.W
  H1 = C1.FRAME.NACH.H
  
  X2 = C2.X
  Y2 = C2.Y
  W2 = C2.FRAME.NACH.W
  H2 = C2.FRAME.NACH.H
  
  If X1 +W1-MAX_DIFF > X2 And X1 < X2+W2-MAX_DIFF Then 
      If Y1+H1-MAX_DIFF > Y2 And Y1 < Y2+H2-MAX_DIFF Then 
          OBJ_EVENT C1, ID_COLL, C2, 0, 0
          OBJ_EVENT C2, ID_COLL, C1, 0, 0
      End If 
  End If 
End Sub

Listing 30: Die Sub "OBJ_COLL_OBJ"

Diese Routine ist ähnlich aufgebaut wie die Kollisionsabfrage für Felder, jedoch werden hier zwei Objekte übergeben. Als erstes wird überprüft, ob beide Objekte belegt sind, damit nicht unnötig Rechenzeit geopfert wird. Anschließend werden die Koordinaten beider Objekte geladen und verglichen. Auch hierbei wird MAX_DIFF benutzt, um eine Toleranzgrenze zu setzen. Wenn die Objekte kollidieren, wird für jedes Objekt das ID_COLL-Ereignis ausgelöst. Bei jedem der beiden Aufrufe ist jeweils das andere Objekt der Auslöser. So kann zum Beispiel das Objekt C1 auf C2 zugreifen, wenn es das ID_COLL-Ereignis bekommt. Auch hier verzichten wir auf weitere Übergabewerte an die Ereignisroutine.

3.8 Inventar  

"press i to see see the inventory" Solche Sätze kennt vielleicht der eine oder andere unter ihnen noch aus den guten alten Adventurespielen. Nun, in diesem Spiel werden wir auch ein Inventar verwenden. Jedoch beschränken wir uns dabei nicht auf "Aufnehmen" und "Ablegen" sonder wir schreiben flexible Routinen zum Verwalten des Inventars. Das klingt vielleicht etwas schwierig, aber es ist eine recht einfache Aufgabe:

Sub OBJ_ITEM_GET (O As ObjType, ITEM, Energy)

  If O.T = 0 Then Exit Sub 

  For I = 0 To UBound(O.ITEM)
      If O.ITEM(I).T = 0 Then 
          O.ITEM(I).T = ITEM
          O.ITEM(I).E = Energy
          Exit Sub 
      End If 
  Next I

End Sub

Listing 31: Die Sub "OBJ_ITEM_GET"

Die Prozedur OBJ_ITEM_GET sorgt dafür, daß ein neuer Gegenstand dem Inventar hinzugefügt wird. Am Anfang kommt die obligatorische Abfrage der Objektbelegung. In den nächsten Prozedur-Beschreibungen werde ich diesen Teil weglassen. Nun werden alle Einträge des Inventars durchlaufen und es wird überprüft, ob noch ein freier Platz da ist. In diesem Fall wird dieser mit der übergebenen Objektnummer und der "Energy" belegt. Anschließend wird die Routine beendet.

Sub OBJ_ITEM_DROP (O As ObjType, ITEM)
  
  If O.T = 0 Then Exit Sub 
  
  For I = 0 To UBound(O.ITEM)
      If O.ITEM(I).T = ITEM Then 
          O.ITEM(I).T = 0
          Exit Sub 
      End If 
  Next I
  
End Sub

Listing 32: Die Sub "OBJ_ITEM_DROP"

OBJ_ITEM_DROP ist die genaue Umkehrung: Es durchsucht das Inventar und wirft den Gegenstand, falls vorhanden, heraus.

Function OBJ_ITEM_HAVE (O As ObjType, ITEM)
  
  If O.T = 0 Then Exit Function 
  
  For I = 0 To UBound(O.ITEM)
      If O.ITEM(I).T = ITEM Then 
          OBJ_ITEM_HAVE = O.ITEM(I).E
          Exit Function 
      End If 
  Next I
  
End Function

Listing 33: Die Function "OBJ_ITEM_HAVE"

OBJ_ITEM_HAVE gibt den Energy-Wert des Gegenstandes zurück. Damit kann man nicht nur prüfen, ob der Gegenstand im Inventar vorhanden ist, sondern man kann damit gleichzeitig feststellen, wie viele Gegenstände es sind bzw. wie lange das Extra noch wirkt o.ä.. Der Aufbau dieser Funktion sollte auch nicht schwer zu verstehen sein, da er parallel zu den bisherigen ist.

Function OBJ_ITEM_ENERGY_GET (O As ObjType, ITEM)
  
 OBJ_ITEM_ENERGY_GET = OBJ_ITEM_HAVE(O, ITEM)
 
End Function

Listing 34: Die Function "OBJ_ITEM_ENERGY_GET"

Was soll denn das? Warum schreiben wir eine Funktion, die das genau das Gleiche macht, wie eine andere? Tja, das hat etwas mit Einheitlichkeit zu tun ;-)
Die Funktion OBJ_ITEM_ENERGY_GET soll nämlich das Gegenstück zu OBJ_ITEM_ENERGY_SET sein. Das die Funktion OBJ_ITEM_HAVE auch die Energie zurückgibt ist eigentlich nur eine Spielerei.

Sub OBJ_ITEM_ENERGY_SET (O As ObjType, ITEM, Energy)
  
  If O.T = 0 Then Exit Sub 
  For I = 0 To UBound(O.ITEM)
      If O.ITEM(I).T = ITEM Then 
          O.ITEM(I).E = Energy
          If Energy <= 0 Then 
              O.ITEM(I).T = 0
          End If 
          Exit Sub 
      End If 
  Next I
  OBJ_ITEM_GET O, ITEM, Energy
  
End Sub

Listing 35: Die Sub "OBJ_ITEM_ENERGY_SET"

Die Routine OBJ_ITEM_ENERGY_SET sucht den übergebenen Gegenstand und setzt seine Energie neu. Ist der Gegenstand noch nicht im Inventar vorhanden, so wird er aufgenommen.
Noch etwas: Alle Inventar-Routinen arbeiten mit Gegenstandsnummern. Sie unterscheiden also nach Gegenstand Typ 1, Typ 2 usw.. Daher werden wir für diese Funktionen später auch mit "Global Const" Konstanten festlegen, die jedem möglichen Gegenstand eine Nummer zuweisen.
Wie oben schon erwähnt hat die Energie im Zusammenhang mit Gegenständen nicht unbedingt wirklich etwas mit Energie zu tun, sondern kann auch angeben, wie lange ein bestimmtes Extra noch hält, oder es steht für eine andere numerische Eigenschaft des Gegenstandes. Noch eine letzte OBJ-Routine, die nichts mit dem Inventar zu tun hat, bevor wir mit dem Hauptprogramm anfangen:

Sub OBJ_ANIMATE (C1, Max1, C2, Max2)
  
  C1 = C1 + 1
  If C1 > Max1 Then 
      C1 = 0
      C2 = C2 + 1
      If C2 > Max2 Then C2 = 0
  End If 
 
End Sub

Listing 36: Die Sub "OBJ_ITEM_ANIMATE"

Diese Routine bekommt vier Werte übergeben. C1 und C2 sind zwei Zählervariablen und Max1 bzw. Max2 sind die entsprechenden Maximalwerte. Jedesmal wenn man diese Routine mit den gleichen Variablen aufruft wird C1 um 1 erhöht. Ist C1 größer als der Maximalwert für C1, so wird C1 auf 0 gesetzt und C2 wird um 1 erhöht. Ist C2 größer als der entsprechende Maximalwert wird auch C2 wieder auf 0 gesetzt. Ein Beispiel dazu:

Sub Form_Load
 Do 
    OBJ_ANIMATE A, 2, B, 4
    MsgBox Str$(A) + "/ " + Str$(B), 0, "Status:"
 Loop 
End Sub

Listing 37: Die Ein verwendungsbeispiel

Ausgabe: 1/0, 2/0, 0/1, 1/1, 2/1, 0/3, 1/3, 2/3, 0/4, 1/4, 2/4....
Diese Routine dient zur Animation. Aber dazu im nächsten Teil mehr.

3.9 Ende Teil 3  

Wie Ende? Warum denn? Das kann ich Ihnen wohl sagen: Wenn ich den Kurs jetzt noch weiter ausdehne fürchte ich, daß ich selbst die Übersicht verliere und nur noch unvollständige und unverständliche Sätze schreibe ;-)
In Teil 4 wird das Hauptprogramm besprochen. Ihm steht auch ein eigener Kursteil zu, da es alleine (vom Speicherbedarf her) so groß ist, wie die bisher besprochenen Module zusammen.
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 [573 KB] [573000 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.