Die Community zu .NET und Classic VB.
Menü

Der große VB Spielekurs Teil 4

 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.
Im letzten Teil des Kurses haben wir fast alle Module aus dem Pacman-Spiel besprochen. Nun ist das Hauptprogramm dran.
Dabei besprechen wir noch das Gerüst mit den Formen und beenden damit die Entwicklung des Spiels.
Am Ende dieses Teiles gibt es dann noch einige Tips zu eigenen Spielen und Verbesserungsvorschläge.

Mit freundlichen Grüßen
Dominik Scherlebeck (PCDVisual)

4.1 Pacman BAS  

Die Datei PACMAN.BAS ist das Hauptprogramm und das letzte unbesprochene Modul unseres Spiels.
Ein besonders wichtiger und elementarer Teil ist die Definition unserer Konstanten. Wie wir im letzten Teil schon besprochen haben, wollen wir Konstanten benutzen, die für jeden Typ einen spezifischen Wert angeben und uns so eine einfache Identifizierung erlauben. Die Identifizierungsnummern der Objekte können wir beliebig aussuchen, wir müssen nur darauf achten, daß kein Wert doppelt vorkommt.
Da jedes Objekt als Sprite auf den Bildschirm gezeichnet wird und die Sprites durchnumeriert im Speicher liegen müssen wir natürlich auch wissen, welcher Sprite welche Nummer hat. Dabei ist es von entscheidender Wichtigkeit, in welcher Reihenfolge die Sprites in den Bildern gespeichert worden sind. Da ich Ihnen die Bilder vorgegeben habe ist das hier natürlich kein größeres Problem. Sobald Sie aber irgendwo Sprites einfügen oder ändern beginnt das Suchen und sie müßten jeden Wert überprüfen. Dieses Problem können wir durch die Benutzung von Konstanten umgehen. Bei größeren Spielen kann man auch die Nummern in Dateien speichern oder eine Scriptsprache schreiben, welche die richtigen Nummern holt.. Für unsere Zwecke ist es jedoch nicht erforderlich. Hier nun eine Übersicht über alle Konstanten. Ich schlage Ihnen vorschlagen diese zu kopieren und nicht abzutippen, da eine Falscheingabe später schwer zu finden ist und zu Fehlern führen kann. Außerdem, wer will schon so viel abtippen ;)

Global Const NR_NOTHING = 200
Global Const NR_ANIMATOR = 201
 
Global Const NR_PACMAN = 1
Global Const ID_PACMAN_LEFT = 24
Global Const ID_PACMAN_RIGHT = 0
Global Const ID_PACMAN_UP = 72
Global Const ID_PACMAN_DOWN = 48
 
Global Const NR_MONSTER_BLUE = 2
Global Const ID_MONSTER_BLUE = 96
Global Const NR_MONSTER_GREEN = 3
Global Const ID_MONSTER_GREEN = 108
Global Const NR_MONSTER_RED = 4
Global Const ID_MONSTER_RED = 120
Global Const NR_MONSTER_BROWN = 5
Global Const ID_MONSTER_BROWN = 132
Global Const NR_MONSTER_MAGENTA = 6
Global Const ID_MONSTER_MAGENTA = 144
Global Const NR_MONSTER_HIPPIE = 7
Global Const ID_MONSTER_HIPPIE = 156
 
Global Const NR_ITEM_COOL = 8
Global Const ID_ITEM_COOL = 172
Global Const NR_ITEM_INVISIBLE = 9
Global Const ID_ITEM_INVISIBLE = 176
Global Const NR_ITEM_TERMINATOR = 10
Global Const ID_ITEM_TERMINATOR = 180
 
Global Const NR_ITEM_DEAD = 11
Global Const ID_ITEM_DEAD = 184
Global Const NR_ITEM_KEY1 = 12
Global Const ID_ITEM_KEY1 = 188
Global Const NR_ITEM_KEY2 = 13
Global Const ID_ITEM_KEY2 = 192
 
Global Const NR_DOT = 14
Global Const ID_DOT = 168
Global Const NR_DOOR1 = 15
Global Const ID_DOOR1 = 196
Global Const NR_DOOR2 = 16
Global Const ID_DOOR2 = 197
Global Const NR_FLAG = 17
Global Const ID_FLAG = 204
 
 
Global Const NR_SWITCH_RED = 18
Global Const ID_SWITCH_RED = 240
Global Const NR_SWITCH_GREEN = 19
Global Const ID_SWITCH_GREEN = 242
Global Const NR_SWITCH_BLUE = 20
Global Const ID_SWITCH_BLUE = 244
Global Const NR_SWITCH_YELLOW = 21
Global Const ID_SWITCH_YELLOW = 246
 
Global Const NR_BLOCK_RED = 22
Global Const ID_BLOCK_RED = 248
Global Const NR_BLOCK_GREEN = 23
Global Const ID_BLOCK_GREEN = 249
Global Const NR_BLOCK_BLUE = 24
Global Const ID_BLOCK_BLUE = 250
Global Const NR_BLOCK_YELLOW = 25
Global Const ID_BLOCK_YELLOW = 251
           
Global Const NR_TELE_RED = 26
Global Const ID_TELE_RED = 252
Global Const NR_TELE_YELLOW = 27
Global Const ID_TELE_YELLOW = 264
Global Const NR_TELE_BLUE = 28
Global Const ID_TELE_BLUE = 260
Global Const NR_TELE_GREEN = 29
Global Const ID_TELE_GREEN = 256

Global Const ID_WALL_GRAY = 276
Global Const ID_WALL_YELLOW = 280
Global Const ID_WALL_ORANGE = 284
Global Const ID_WALL_RED = 288
Global Const ID_WALL_MAGENTA = 292
Global Const ID_WALL_BLUE = 296
Global Const ID_WALL_CYAN = 300
Global Const ID_WALL_GREEN = 304
Global Const ID_WALL_BROWN = 308
 
Global Const ID_ENERGY = 216
Global Const ID_TIME = 233

Listing 1: Deklarationen in "PACMAN.BAS"

Wir benötigen im Hauptprogramm auch noch einige globale Variablen für unser Spiel:

Global KEY_DOWN(255), KEY_LAST
Global Pfad$
Global SICHT_X, SICHT_Y
 
Global COUNT_DOTS
Global LEVEL_SCORE
Global LEVEL_NEXT
Global HIGHSCORE_EINTRAGEN

Listing 2: weitere Deklarationen

Das KEY_DOWN()-Datenfeld gibt für 255 verschiedene Tasten(-codes) an, ob die entsprechende Taste gedrückt ist oder nicht. KEY_LAST gibt die zuletzt betätigte Taste an. PFAD$ ist, wie im letzten Spiel auch, eine globale Variable, die den Pfad setzt, in dem die Spieldaten zu finden sind. SICHT_X und SICHT_Y geben die Scrollposition des Spielfelds und aller Objekte in X- und Y-Richtung an.
COUNT_DOTS gibt an, wie viele eßbare Punkte, also diesen typischen runden gelben Dinger die in jedem PACMAN-Spiel vorkommen, sich auf dem Spielfeld befinden. Damit wird überprüft, ob der Spieler alle Punkte eingesammelt hat oder nicht. LEVEL_SCORE gibt die Punktzahl an, die insgesamt in dem Level erreicht worden ist. LEVEL_NEXT gibt an, ob der Spieler das nächste Level erreicht hat oder "gestorben""ist. Die Variable HIGHSCORE_EINTRAGEN dient dazu, der späteren Form für die Highscoreliste mitzuteilen, ob sie prüfen soll, ob sich der Spieler eintragen kann (=1) oder ob er sie nur ansehen will (=0).
Und nun noch die wichtigsten zwei Datenfelder:

Global Spr(400) As Sprite
Global Obj(200) As ObjType

Listing 3: Die wichtigsten Datenfelder

Nun haben wir für das Spiel alle wichtigen Variablen definiert und Konstanten gesetzt. Es kann also losgehen.

4.2 Pacman in Form  

Wenn man ein Spiel schreiben will, daß in einer Form und unter Windows abläuft muß man sich zwangsweise überlegen, wie man es realisiert, daß gleichzeitig das Menü funktioniert und das Spiel läuft. Die am leichtesten erscheinende Lösung ist vielleicht ein Timer, der beispielsweise alle 10-Millisekunden den Bildschirm aufbaut und die Ereignisse des Spiels verwaltet. Jedoch hat diese Methode auch mehrere entscheidende Nachteile:

  • Diese Methode ist langsam!
  • Sie müssen das Spiel so programmieren, daß man es über einen Zeitgeber verwaltet wird
  • Es ist mit einigem Verwaltungsaufwand verbunden.

Die andere Alternative ist, den Spieß umzudrehen: Das Spiel läuft permanent und nur den Menüs wird bei jedem Durchlauf etwas Zeit gegeben, Ereignisse weiterzugeben. Dazu verwenden wir die DoEvents-Anweisung. Diese gibt die Kontrolle für kurze Zeit an Windows zurück und ermöglicht, daß z.B. Menüereignisse überprüft werden.
Was wir jetzt noch benötigen ist eine Variable, die angibt, welches Ereignis eingegangen ist. Dazu definieren wir folgende Variablen und Konstanten im Deklarationsteil der PACMAN.FRM, die Sie nun neu erstellen:

Const MNU_NEW = 1
Const MNU_QUIT = 2
Const MNU_HIGH = 3
Const MNU_HELP = 4
Const MNU_INFO = 5

Dim Shared MENU_WAHL

Listing 4: Die Ergebnisvariable "MENU_WAHL"

Unser späteres Spiel soll dann in der Prozedur "SPIEL" verwaltet werden und über die Variable MENU_WAHL mitbekommen, ob und wenn ja, welcher Menüeintrag angeklickt wurde. Die Abfrage der Tastatur erfolgt natürlich wieder über DISPLAY_KEYDOWN und DISPLAY_KEYUP und wirkt sich auf das KEY_DOWN()-Datenfeld und die KEY_LAST-Variable aus.
Zuerst behandeln wir den Aufruf der Form. Der Inhalt der FORM_LOAD-Ereignisprozedur ist relativ simpel:

Sub FORM_LOAD ()
  
  Me.Show
  SPIEL
  
End Sub

Listing 5: Die Startprozedur

Als erstes stellen wir sicher, daß die Form schon auf dem Bildschirm zu sehen ist, bevor das Spiel gestartet wird. Anschließend wird die Prozedur SPIEL aufgerufen.
Der nächste Punkt der "Tagesordnung" ist die neue PACMAN.FRM-Datei die neugestaltet werden muß.


Abbildung 1: PACKMAN.FRM

Die Form selbst bekommt später noch ein Menü. Setzen Sie nun zwei Bildfelder und ein Bezeichnungsfeld auf die Form:

BACK (PictureBox/Bildfeld)
AutoRedraw = 1
AutoSize = 1
ScaleMode = 3
Visible = 0

DISPLAY (PictureBox/Bildfeld)
AutoRedraw = 1
ScaleMode =3
Visible=1
Picture= <pal.bmp>

STATUS (Label/Bezeichnungsfeld)
Alignment = 2
BackStyle = 1
BorderStyle = 1
Caption = "VB-Kurs PacMan - 1997 by PCDVisual@AOL.COM"

FORM
Caption = "VB-Kurs Pacman"
Icon = <pacman.ico>
Name = Main

Es folgt das Menü. Rufen Sie dazu wie bekannt den Menüeditor auf.

CaptionName (Shortcut)
&SpielM_S_TITLE
&Neues Spiel startenM_S_NEU (Strg+N)
-M_S_SEP1
&BeendenM_S_BEENDEN (Strg+B)
&InfoM_I_TITLE
&HighscoreM_I_HIGHSCORE (Strg+H)
&Hilfe zum SpielM_I_HILFE (F1)
-M_I_SEP1
&Info überM_I_INFO (Strg+I)

Jetzt zum Ereigniscode für die Form.

Sub DISPLAY_KEYDOWN (KeyCode As Integer, Shift As Integer)
  If KeyCode <= 255 Then 
      KEY_DOWN(KeyCode) = 1
      KEY_LAST = KeyCode
  End If 
End Sub 

Sub DISPLAY_KEYUP (KeyCode As Integer, Shift As Integer)
  If KeyCode <= 255 Then 
    KEY_DOWN(KeyCode) = 0
  End If 
End Sub

Listing 6: Die Events

Die Ereignisroutinen für das Display-Bildfeld sollten Ihnen noch vom letzten Spiel bekannt sein. Hier werden Tastendrücke des Benutzers abgefangen und registriert. Sobald eine Taste gedrückt wird, setzt die Routine den entsprechenden Eintrag im KEY_DOWN()-Datenfeld auf 1 und weist der Variable KEY_LAST den KeyCode zu. Wird die Taste wieder losgelassen, so wird der Eintrag im KEY_DOWN()-Datenfeld wieder zurück auf 0 gesetzt. Somit können wir im Spiel überprüfen, welche Tasten gerade gedrückt sind und welche nicht.
Die FORM_RESIZE-Routine sorgt dafür, daß alle Elemente in der Form jederzeit die richtige Größe haben.

 Sub FORM_RESIZE ()
  
   DISPLAY.Left = 0
   DISPLAY.Top = 0
   DISPLAY.Width = Me.ScaleWidth
   DISPLAY.Height = Me.ScaleHeight - STATUS.Height

   STATUS.Left = 0
   STATUS.Width = Me.ScaleWidth
   STATUS.Top = Me.ScaleHeight - STATUS.Height

   R = StretchBlt(DISPLAY.hDC, 0, 0, DISPLAY.ScaleWidth,_
       DISPLAY.ScaleHeight, BACK.hDC, 0, 0, BACK.ScaleWidth,_
       BACK.ScaleHeight, BIT_COPY)

   DISPLAY.Picture = DISPLAY.Image

 End Sub

Listing 7: Die Events

Das Display-Bildfeld wird auf die Koordinaten 0,0 gesetzt. Das ist die obere linke Ecke direkt unter dem Menü. Die Größe ergibt sich aus der Innengröße der Form minus die Größe des Statusfeldes. Die Breite entspricht der gesamten Breite der Form. Das Statusfenster wird immer am unteren Rand der Form positioniert. Und anschließend wird noch das Hintergrundbild gestreckt und auf das DISPLAY-Bildfeld kopiert.
Da wir in FORM_LOAD die FORM anzeigen gibt es Probleme, sobald die Form geschlossen wird. In diesem Fall würde die FORM_LOAD-Prozedur nämlich dafür sorgen, daß die Form gleich wieder nein angezeigt wird. Aus diesem Grund wäre die Form nicht zu schließen. Daher benutzen wir die FORM_UNLOAD-Prozedur um VB explizit mitzuteilen, daß die Form nicht nur geschlossen sondern auch das ganze Programm beendet werden soll.

Sub FORM_UNLOAD (Cancel As Integer)
  End 
End Sub

Listing 8: Die Events

Jetzt müssen wir noch den Code für die Menüs schreiben:

Sub M_I_HIGHSCORE_CLICK ()
  MENU_WAHL = MNU_HIGH
End Sub

Listing 9: Die Events

Wenn der Benutzer einen Menüeintrag anklickt wird die Variable MENU_WAHL auf einen entsprechenden Wert gesetzt. Auch hierbei benutzen wir Konstanten.

Sub M_I_HILFE_CLICK ()
  MENU_WAHL = MNU_HELP
End Sub 

Sub M_I_INFO_CLICK ()
  MENU_WAHL = MNU_INFO
End Sub 

Sub M_S_BEENDEN_CLICK ()
    MENU_WAHL = MNU_QUIT
End Sub 

Sub M_S_NEU_CLICK ()
    MENU_WAHL = MNU_NEW
End Sub

Listing 10: Die Menü-Events

Und als letzte Routine für dieses Modul verfassen wir die zentrale Routine "SPIEL", in der alle Ereignisse zusammenlaufen und die das ganze Spiel steuert.
In der obersten Zeile wird per "On Local Error Resume Next" dafür gesorgt, daß alle eventuell vorkommende Fehler abgefangen werden.

Sub SPIEL ()
  On Local Error Resume Next

Listing 11: Die Sub "Spiel"

Danach werden die Routinen PACMAN_INIT (Initialisierung des Spiels) und PACMAN_LOAD "LEVEL.001","BACK1.BMP" (Datei LEVEL.001laden und als Hintergrund BACK1.BMP) aufgerufen. Am Ende wird noch einmal FORM_RESIZE aufgerufen, damit gleich am Anfang des Spiels alles richtig aussieht.

PACMAN_INIT
PACMAN_LOAD "LEVEL.001", "BACK1.BMP"
FORM_RESIZE

Listing 12: Die Sub "Spiel"

Die Variable Level (gibt logischerweise das aktuelle Level an) wird auf 1 und die Variable StartTime (Anfangszeit) wird auf TIMER gesetzt. Aus der Anfangs und der aktuellen Zeit können wir immer leicht errechnen, wie lange das Spiel schon läuft ohne daß wir auf einen Timer zurückgreifen müssen.

Level = 1
StartTime = Timer

Listing 13: Die Sub "Spiel"

Nun startet die Hauptschleife, in der das Spiel abläuft.

Do

Listing 14: Die Sub "Spiel"

Zuerst überprüfen wir die Hintergrundmusik und starten sie ggf. wieder, wie wir es aus dem ersten Spiel kennen.

If MIDI_PLAYING() = 0 Then 
    MIDI_PLAY Pfad$ + "MUSIC1.MID"
End If

Listing 15: Die Sub "Spiel"

Nun wird PACMAN_RUN aufgerufen. In dieser Routine wird später das Spiel selbst (also die Bewegungen usw.) untergebracht. Anschließend zeigen wir im Statusfenster noch die Punkte und die bisher vergangene Zeit an, die sich aus der aktuellen Zeit minus der Startzeit errechnet. Danach wird DoEvents benutzt um Windows Zeit für andere Ereignisse zu geben.

         PACMAN_RUN
         STATUS.Caption = "Punkte: " + Str$(LEVEL_SCORE) + _
                                   " Zeit: "_
                                   + Str$(Fix(Timer - StartTime))

         DoEvents

Listing 16: Die Sub "Spiel"

Nun überprüfen wir, ob ein Menüereignis vorliegt. In diesem Fall hat MENU_WAHL einen Wert ungleich Null. Diesen fragen wir dann in einem SELECT-CASE-Block ab und reagieren entsprechend.

If MENU_WAHL <> 0 Then 
    Select Case MENU_WAHL

Listing 17: Die Sub "Spiel"

Wurde der Eintrag "Spiel" / "Neues Spiel starten" gewählt, so wird wieder das erste Level geladen und alle zugehörigen Werte werden zurückgesetzt.

Case MNU_NEW
  PACMAN_LOAD "LEVEL.001", "BACK1.BMP"
  Level = 1
   LEVEL_SCORE = 0
   LEVEL_NEXT = 0
   FORM_RESIZE
   StartTime = Timer

Listing 18: Die Sub "Spiel"

Der Eintrag "Spiel" / "Beenden" ist schnell abgehandelt. Darauf reagiert unser Spiel mit der schlichten Anweisung END die hier nicht weiter besprochen werden muß, oder etwa doch? ;-)

Case MNU_QUIT
  End

Listing 19: Die Sub "Spiel"

Als nächstes kommt der Eintrag "Info" / "Highscore sehen". Dieser hat zur Folge, daß die Variable HIGHSCORE_EINTRAGEN auf Null gesetzt wird (damit die Form weiß, daß der User nur mal reinsehen will) und die Form HIGHSCORE eingetragen wird.

Case MNU_HIGH
   HIGHSCORE_EINTRAGEN = 0
   HIGHSCORE.Show 1

Listing 20: Die Sub "Spiel"

Der Eintrag "Info" / "Hilfe" sorgt für das Aufrufen der Hilfe-Form.

Case MNU_HELP
  HILFE.Show 1

Listing 21: Die Sub "Spiel"

Bei "Info" / "Info über" sieht es ähnlich einfach aus:

Case MNU_INFO
  INFO.Show 1
End Select

Listing 22: Die Sub "Spiel"

Damit wäre der CASE-Block abgeschlossen und die MNU_WAHL-Variable wird wieder auf Null zurückgesetzt.

  MENU_WAHL = 0
End If

Listing 23: Die Sub "Spiel"

Die Variable LEVEL_NEXT wird von PACMAN_RUN auf 1 gesetzt. Falls der Spieler vor dem Sieg abtritt wird sie auf -1 gesetzt. Im ersten Fall wird die Level-Variable erhöht und dann wird der Name der neuen Level-Datei errechnet. Zuerst erstellen wir aus der Variable Level einen String, schneiden die Lücke vorne ab und setzen solange Nullen davor, bis es insgesamt ein String mit drei Zeichen ist. Dann überprüfen wir, ob eine solche Datei (Pfad + "LEVEL. " + Levelendung) existiert. Wenn ja wird sie geladen und LEVEL_NEXT wird zurückgesetzt. Wenn nein wird HIGHSCORE_EINTRAGEN auf 1 gesetzt, die Form wird angezeigt. Anschließend ruft das Programm noch die Form GELÖST auf (die bisher auch noch nicht da ist >g<) und dann wird ein neues Spiel gestartet. Das ist die Standardprozedur, wenn der Spieler gewinnen sollte.

If LEVEL_NEXT > 0 Then 
  Level = Level + 1
  L$ = LTrim$(Str$(Level))
  If Len(L$) < 3 Then L$ = "0" + L$
  If Len(L$) < 3 Then L$ = "0" + L$
  If Dir$(Pfad$ + "LEVEL." + L$) = "" Then 
    HIGHSCORE_EINTRAGEN = 1
    HIGHSCORE.Show 1
    GELÖST.Show 1
    MENU_WAHL = MNU_NEW
  Else 
    PACMAN_LOAD "LEVEL." + L$, "BACK" + _
                       LTrim$(Str$(Level)) + ".BMP"
    LEVEL_NEXT = 0
    FORM_RESIZE
  End If 
End If

Listing 24: Die Sub "Spiel"

Sollte der Spieler verlieren, wird die VERLOREN-Form angezeigt und alle Werte werden zurückgesetzt. Zusätzlich wird hier ebenfalls die Highscoreliste aufgerufen, in der sich der Spieler eintragen kann, wenn er gut genug war :-)

If LEVEL_NEXT < 0 Then 
  LEVEL_NEXT = 0
  MENU_WAHL = MNU_NEW
  VERLOREN.Show 1
  HIGHSCORE_EINTRAGEN = 1
  HIGHSCORE.Show 1
End If

Listing 25: Die Sub "Spiel"

Nun kommt natürlich noch der Abschluß der Schleife und das Ende der Prozedur

  Loop 
End Sub

Listing 26: Die Sub "Spiel"

Damit Sie später beim Schreiben des Hauptprogramms immer nachsehen können wie weit das Spiel läuft, werden wir nun die restlichen Fenster erstellen.

4.3 Weitere Formalitäten  


Abbildung 2: Info


Abbildung 3: Gewonnen


Abbildung 4: Verloren

Drei Formen können Sie praktisch nach Belieben gestalten: INFO, GELÖST und VERLOREN. Diese Fenster dienen nur dazu, eine Mitteilung auszugeben. Das einzige, was Sie hier beachten müssen ist der Name des Fensters und die Tatsache, daß Sie am besten BorderStyle auf 1 oder 3 setzten und MinButton und maxButton auf 0, damit man die Form nicht in der Größe ändern kann.
Außerdem sollten diese Forms einen Abbruchknopf haben. Der Rest bleibt Ihnen überlassen. Ein kleiner Tip nebenbei: Sie können ihre Fenster einfach auf dem Bildschirm zentrieren, wenn Sie folgende Zeilen in die FORM_LOAD-Ereignisprozedur setzen:

Sub FORM_LOAD ()
  Me.Left = (SCREEN.Width - Me.Width) / 2
  Me.Top = (SCREEN.Height - Me.Height) / 2
End Sub

Listing 27: Fenster zentrieren

4.4 Hilfe für Spieler  

Anstatt hier nun auch noch einzubringen, wie man eine Hilfedatei schreibt und einbindet habe ich eine andere, einfache Lösung vorgezogen. Die wenigen Informationen die dieses Spiel benötigt habe ich mit einem Grafikprogramm auf vier Bilder gebannt. Diese kann man nun im Hilfe-Fenster einfach per Knopfdruck wechseln.
Im oberen Teil der Form befindet sich ein Bildfeld, in das Sie schon während der Entwicklung das Bild PACMAN\ELSE\HILFE1.BMP einladen sollten, damit Sie sehen, wie groß die Bilder sind.

SEITE (PictureBox/Bildfeld) AutoSize = True Picture = <else\hilfe1.bmp> ANZEIGEN (CommandButton)
Caption = "&1"
Index = 0

ANZEIGEN (CommandButton)
Caption = "&2"
Index = 1

ANZEIGEN (CommandButton)
Caption = "&3"
Index = 2

ANZEIGEN (CommandButton)
Caption = "&4"
Index = 3

ZURÜCK (CommandButton)
Caption = "&Zurück"

FORM
Caption = "&Hilfe"
BorderStyle = 3
MinButton = 0
MaxButton = 0

Achten Sie darauf, daß die vier ANZEIGEN-Knöpfe ein Steuerelementedatenfeld sind. Die gleichen Namen oben sind also gewollt!
Sobald die Form aufgerufen wird, soll sie als erstes auf dem Schirm zentriert werden. Dies erreichen wir wieder durch die beiden Zeilen in FORM_LOAD. Der Algorithmus, der dahinter steckt, sollte eigentlich aus der Mathematik bekannt sein. Wir teilen die Differenz zwischen der Größe des Bildschirms und der Form durch zwei und erhalten die Koordinaten der oberen, linken Ecke.

Sub FORM_LOAD ()

  Me.Left = (SCREEN.Width - Me.Width) / 2
  Me.Top = (SCREEN.Height - Me.Height) / 2
  
End Sub

Listing 28: Zentrieren des Fensters

Wenn der Benutzer nun auf einen der Knöpfe klickt muß das entsprechende Bild geladen werden (nun gut, man hätte auch vier Bildfelder in die Form setzen können - aber machen wir es mal auf diese Weise). Dazu können wir einfach den Index verwenden:

Sub ANZEIGEN_CLICK (Index As Integer)

  File$ = Pfad$ + "ELSE\HILFE" + LTrim$(Str$(Index + 1)) + ".BMP"
  SEITE.Picture = LoadPicture(File$)
   
End Sub

Listing 29: Die Sub "ANZEIGEN_CLICK"

Die Index-Nummer wird um 1 erhöht und in einen String umgewandelt. Dann wird das vorstehende Leerzeichen abgeschnitten. Davor wird nun der komplette Pfad und der Anfang des Dateinamen gesetzt. Anschließend wird noch ".BMP" angehängt und der Dateiname ist fertig. Nun wird mit LoadPicture(<Datei>) das entsprechende Bild in das Bildfeld SEITE geladen.
Und noch der Code für den ZURÜCK-Knopf, der keine besondere Herausforderung mehr ist ...

Sub ZURÜCK_CLICK ()
 
  Unload Me
 
End Sub

Listing 30: Die Sub "ZURÜCK_CLICK"

4.5 Für die besten Spieler [Highscore-Liste]  

In vielen guten Actionspielen gibt es Highscorelisten, die für den Spieler einen Anreiz darstellen, möglichst viele Punkte zu holen. Auch in diesem Spiel wollen wir darauf eingehen. Wenn Sie Lust haben, können Sie ja versuchen, eine ähnliche Liste in das Actionspiel (aus dem ersten Teil über Spiele) einzubinden.


Abbildung 5: Die Bestenliste

Setzen Sie oben in die Form das Logo (PACMAN\ELSE\LOGO.BMP). Darunter setzten Sie sechs Label mit dem Namen SPIELER und den Indizes 0-6 (also ein Steuerelementedatenfeld). Das wir damit insgesamt 7 Einträge haben spielt keine Rolle. Wenn Sie wollen können Sie auch daraus 10 oder mehr machen - das wäre doch schon eine gute Hausaufgabe oder? :) Das Feld mit Index 0 muß oben stehen, die anderen kommen darunter. Wie in dem Bild oben können Sie die Felder auch farbig verschieden gestalten. Was Sie in der Felder hineinschreiben ist egal, da der Inhalt sowieso mit den Daten aus der Highscore-Liste überschrieben wird. Daneben können Sie einen Trennstrich ziehen. Auf die rechte Seite kommen ebenfalls sechs Felder, diesmal mit dem Namen PUNKTE und wieder den Indizes 0-6. Also stehen jeweils ein Label von SPIELER und ein Label von PUNKTE nebeneinander, die den gleichen Index haben. Das ist wichtig, damit auch dem Namen die richtige Punktzahl zugeordnet wird. Sie sollten die Ausrichtung des Textes bei den Punkte auf "1 - Rechts" stellen, da das einfach besser aussieht.
Unten auf die Form setzen Sie einen Knopf (Name=OK, Caption="&Ok"). Die Form selbst wird mit "Highscore" betitelt und bekommt einen Rahmen, den man nicht verändern kann (BorderStyle = 3, MinButton=0, MaxButton = 0). Die Form erhält den Namen HIGHSCORE.
Eine Highscoreliste, zumindest eine einfache Version wie diese hier, arbeite vom Prinzip her so:
Man hat eine Liste mit Namen und eine zugehörige Liste mit Punktzahlen. Wenn der Benutzer "stirbt" bzw. gewinnt kann er sich eintragen. Das Programm sucht anhand seiner Punktzahl, den wievielten Platz er belegt. Der Rest der Liste wird nach unten geschoben und sein Name und seine Punktzahl werden eingefügt.
Daran kann man schon sehen, daß wir zwei Datenfelder definieren müssen: Eins für die Namen und eins für die Punkte (es sei denn, wir benutzen wieder die TYPE-Anweisung aber man muß es ja nicht übertreiben). Also definieren wir beide im Deklarationen-Teil der Form. Wir geben als Index 6 an, da wir damit Einträge von 0-6 definieren und so auch wieder sieben Einträge erhalten. Wer mehr haben will kann wie auch bei der Form einfach mehr eingeben, z.B. 10.

Dim Shared N$(6), P(6)

Listing 31: Deklarationen

Das N$()-Datenfeld faßt logischerweise die Namen, daß P()-Datenfeld die Punktzahlen auf. Nun müssen wir in FORM_LOAD die alte Highscoreliste laden, die Punktzahl prüfen und die neue Liste dann ausgeben.
Am Anfang zentrieren wir wieder die Form auf dem Schirm.

Sub FORM_LOAD ()
  
  Me.Left = (SCREEN.Width - Me.Width) / 2
  Me.Top = (SCREEN.Height - Me.Height) / 2

Listing 32: Fenster zentrieren

Nun sehen wir mit DIR$(<Datei>) nach, ob schon eine Highscore-Datei da ist. DIR$() ist eine der vielen Dateifunktionen von VisualBasic. Sie liefert die erste gefundene Datei eines Suchmusters zurück. Wir können z.B. Ergebnis$=DIR$("*.TXT") benutzen um die erste Textdatei im Verzeichnis zu finden. Wenn das nicht reicht können wir mit Ergebnis$=DIR$() die nächste Datei suchen, solange, bis DIR$() uns einen leeren String zurückgibt. Dann ist keine weitere Datei mehr da. In diesem Fall nutzen wir das aus, um zu prüfen, ob eine ganz bestimmte Datei, nämlich >Pfad<\SCORE.DAT vorhanden ist. Wenn ja wird diese Datei geöffnet und die Highscore wird eingelesen.

If Dir$(Pfad$ + "SCORE.DAT") <> "" Then 
  Open Pfad$ + "SCORE.DAT" For Input As #1
  For M = 0 To 6
    Input #1, N$(M), P(M)
  Next M
 Close #1
Else

Listing 33: Das Laden der Punkte

Wenn die Datei nicht vorhanden ist wird eine neue Highscoreliste erstellt, in der schon einige Namen und Punktzahlen voreingestellt sind, um Ansporn zu geben, diese Leute zu schlagen und Platz eins zu erobern:

  For M = 0 To 6
    N$(M) = "PCD Visual"
    P(M) = (6 - M) * 100
  Next M
End If

Listing 34: Das Speichern der Punkte

Das ich in diesem Fall meine E-Mail-Adresse benutzt habe ist nebensächlich, ich trage es Ihnen bestimmt nicht nach, wenn Sie hier etwas anderes einsetzten ;-)
Als nächstes wird geprüft, ob der Benutzer sich eintragen kann (also ob er "gestorben" ist oder gewonnen hat). In diesem Fall geht die eigentliche Arbeit lost.

If HIGHSCORE_EINTRAGEN Then

Listing 35: Highscore eintragen

Wir durchlaufen nun mit einer Schleife alle Einträge von 0 bis 6. Jedesmal prüfen wir, ob die Punktzahl dieses Eintrags kleiner oder gleich der des Spielers ist. In diesem Fall erobert nämlich der Spieler den Platz. Das wird hierbei von oben nach unten vorgehen ist wichtig, da sonst der Name des Spieler an alle Plätze unterhalb der eigentlich Position eingetragen würde.

For M = 0 To 6
    If P(M) <= LEVEL_SCORE Then

Listing 36: Dürfen wir uns eintragen?

Haben wir nun den Platz gefunden schieben wir alle Einträge darunter eins nach unten. Dies machen wir, indem wir von 6 an aufwärtszählen bis wir einen Eintrag unter dem des Users sind. Dabei kopieren wir immer den oberen nach unten:

1. Name A
2. Name B
3. Name C
4. Name D

Der User kann sich auf Platz eins in unserem Schema eintragen, daher gehen wir von 4 nach 2 alle durch und machen folgendes:

1. Name A
2. Name B
3. Name C
(wird nach unten kopiert)
4. Name C

Beim nächsten Schleifendurchgang sieht es dann so aus:

1. Name A
2. Name B
(wird nach unten kopiert)
3. Name B
4. Name C

Und schließlich:

1. Name A
(wird nach unten kopiert)
2. Name A
3. Name B
4. Name C

Nun sind alle Namen, Name A einschließlich, nach unten verschoben und wir können Platz 1 überschreiben. Damit wäre unsere Aufgabe erfüllt:

1. User
2. Name A
3. Name B
4. Name C

Nun zur Umsetzung in das Spiel:

For M2 = 6 To M + 1 Step -1
  N$(M2) = N$(M2 - 1)
  P(M2) = P(M2 - 1)
Next M2

Listing 37: Das große Sortieren

Nach dem die Namen verschoben sind können wir den User informieren und nach seinem Namen fragen und diesen dann gleich samt seiner Punktzahl auf den errungenen Platz eintragen. Anschließend müssen (!!) wir die Schleife verlassen, damit er sich nicht auch noch auf den unteren Plätzen einträgt.

          A$ = InputBox$(Herzlichen Glückwunsch! Sie_
               können sich in der Highscore-Liste_
               eintragen!! ", "HIGHSCORE! ", "NoBody)
          N$(M) = A$
          P(M) = LEVEL_SCORE
        Exit For 
    End If 
  Next M
End If

Listing 38: Eingabe und Speicherung des Besten

Jetzt übernehmen wir die aktuelle Liste in die Form. Hier sehen Sie jetzt auch, warum es so wichtig ist, daß die Steuerelementefelder in der Form oben den Index 0 haben und die unteren größere Werte:

          For M = 0 To 6
            SPIELER(M).Caption = N$(M)
            PUNKTE(M).Caption = Str$(P(M))
          Next M

        End Sub

Listing 39: Anzeigen der Liste

Wir können einfach mit einer Schleife alle Werte übernehmen und sind fertig :-)
Wenn der Benutzer genug von dem Bildschirm hat klickt er auf OK und die Form wird mit folgendem Code vom Bildschirm geputzt:

         Sub OK_CLICK ()

           Unload Me

         End Sub

Listing 40: Schließen des Fensters

Moment! Ging das nicht etwas ZU schnell? Habe ich da nicht etwas vergessen?! Ach ja richtig, wir müssen die Highscoreliste auch wieder speichern. Nun könnten wir den Code in FORM_LOAD hinein setzen, ich habe mich aber, ganz eigenmächtig, dafür entschlossen einen anderen Weg zu nehmen bei dem Sie auch gleich noch sehen, wozu FORM_UNLOAD gut sein kann: Wenn der Benutzer die Form schließt oder die Form auf irgendeine andere Art geschlossen werden soll wird zuerst das FORM_UNLOAD Ereignis aufgerufen. Diesem wird ein Wert, Cancel%, übergeben. Wenn wir diesen Wert innerhalb der Ereignisprozedur auf 1 setzten wird die Form nicht verschwinden, da wir damit ausdrücken, daß sie noch da bleiben muß. Hier machen wir uns die Prozedur allerdings nur zu nutze, indem wir dort den Speicher-Code eintragen:

Sub FORM_UNLOAD (Cancel As Integer)
  Open Pfad$ + "SCORE.DAT" For Output As #1
  For M = 0 To 6
    Print #1, N$(M); ","; P(M)
  Next M
  Close #1
End Sub

Listing 41: Schließen des Fensters mit speichern

Es wird wieder die SCORE.DAT geöffnet und die Namen und Punkte werden eingetragen. Damit wir beim Öffnen mit Input #1, N(m), P(m) arbeiten können ist es wichtig, daß wir beim Speichern auch ein Komma einbringen. Dies machen wir, indem wir einen String mit einem Komma zwischen den Werten speichern lassen. Fertig :-)

4.6 Pacmans Innenleben  

Um das Spiel endlich zum Laufen zu bewegen müssen wir nun die PACMAN.BAS fertig schreiben (ich weiß, einige Leser haben schon richtig drauf gewartet <g>).
Fangen wir mit PACMAN_INIT an, da diese Prozedur als erste aufgerufen wird. Die nun folgenden Routinen müssen natürlich in das Modul PACMAN.BAS geschrieben werden...

Sub PACMAN_INIT ()

Listing 42: Die Initilisierungs-Sub

Als erstes setzten wir den Pfad. Zu Testzwecken, also während der Entwicklung, können Sie hier einen absoluten Pfad setzen, damit Sie nicht mit dem VB-Verzeichnis in Konflikt kommen, was bei Version 3.0 öfters der Fall ist. Wenn Sie das Spiel später endgültig kompilieren, sollten Sie die erste Zeile entfernen und vor der zweiten das Hochkomma wegnehmen.

Pfad$ = "E:\DATEN\DOCS\VBKURS\SPIELE\PACMAN\"
'Pfad$ = CurDir$ 
If Right$(Pfad$, 1) <> "\" Then Pfad$ = Pfad$ + "\"

Listing 43: Die Initilisierungs-Sub

Nun setzten wir MAX_DIFF auf drei. Das ist der Wert für die Genauigkeit bei der Kollisionsabfrage. Eine gewisse Toleranz ist wichtig, zum die Steuerung zu vereinfachen.

MAX_DIFF = 3

Listing 44: Die Initilisierungs-Sub

Anschließend laden wir alle nötigen Bilder.

           B1 = RES_NEW(): RES_LOAD B1, Pfad$ + &BIT\PACMAN.BMP&
           M1 = RES_NEW(): RES_LOAD M1, Pfad$ + &MASKE\PACMAN.BMP&

           B2 = RES_NEW(): RES_LOAD B2, Pfad$ + &BIT\MONSTER.BMP&
           M2 = RES_NEW(): RES_LOAD M2, Pfad$ + &MASKE\MONSTER.BMP&

           B3 = RES_NEW(): RES_LOAD B3, Pfad$ + &BIT\OBJECTS.BMP&
           M3 = RES_NEW(): RES_LOAD M3, Pfad$ + &MASKE\OBJECTS.BMP&
        
           B4 = RES_NEW(): RES_LOAD B4, Pfad$ + &BIT\WALLS.BMP&
           M4 = RES_NEW(): RES_LOAD M4, Pfad$ + &MASKE\WALLS.BMP&

Listing 45: Die Initilisierungs-Sub

Jetzt wird die Variable Anzahl auf 0 gesetzt. Danach zerlegen wir die Bilder in das Sprite-Datenfeld, wie wir es bei der Demo gemacht haben.

   Anzahl = 0
   SPRITE_SET_MESH Spr(), Anzahl, 12, 8, 20, 20, _
           RESSOURCEN.RES(B1), RESSOURCEN.RES(M1)
   SPRITE_SET_MESH Spr(), Anzahl, 12, 6, 20, 20, _
           RESSOURCEN.RES(B2), RESSOURCEN.RES(M2)
   SPRITE_SET_MESH Spr(), Anzahl, 12, 9, 20, 20, _
           RESSOURCEN.RES(B3), RESSOURCEN.RES(M3)
   SPRITE_SET_MESH Spr(), Anzahl, 12, 4, 20, 20, _
           RESSOURCEN.RES(B4), RESSOURCEN.RES(M4)

End Sub

Listing 46: Die Initilisierungs-Sub

Nun kommen wir zu PACMAN_LOAD. Diese Prozedur hat sowohl die Aufgabe den Hintergrund zu aktualisieren und die Werte zurückzusetzen und das Level zu laden. Daher wird die Routine etwas länger...

Sub PACMAN_LOAD (Datei$, BACK$)

Listing 47: Die er Aufruf der PACMAN_LOAD

Am Anfang blocken wir wieder alle auftretenden Fehler ab. Vielleicht sollten Sie diese Zeile während der Testphase als Kommentar einfügen (ein Hochkomma davorsetzen), damit Sie Fehler bemerken.

On Local Error Resume Next

Listing 48: Die "PACMAN_LOAD"

Als nächste Laden wir das Hintergrundbild, was in BACK$ übergeben wird:

MAIN.BACK.Picture = LoadPicture(Pfad$ + "ELSE\" + BACK$)

Listing 49: Die "PACMAN_LOAD"

Nun wird die Variable COUNT_DOTS (der Zähler für die eßbaren Punkte) auf Null gesetzt und das Level geöffnet. Beim ersten Durchgang werden nur die Zeilen gezählt und die Datei wird geschlossen. Anschließend wird die Breite des Levels in Zeichen mit der Länge einer Zeile errechnet (Spalten = Len(A$)).

COUNT_DOTS = 0
Open Pfad$ + Datei$ For Input As #1
Do 
    Line Input #1, A$
    Zeilen = Zeilen + 1
Loop Until EOF(1)
Close #1
Spalten = Len(A$)

Listing 50: Die "PACMAN_LOAD"

Da wir die Anzahl der Zeilen und Spalten kennen, können wir nun das Spielfeld initialisieren. Da unsere Routine FIELD_SIZE (bekannt aus den letzten Teilen) die Koordinaten von 0 an zählt müssen wir jeweils noch eine Zeile abziehen. Haben wir beispielsweise 60 Zeilen müssen wir eins abziehen damit wir ein Datenfeld von 0-59 erhalten. Das ergibt 60 - denn 0 ist die erste Zeile.

FIELD_SIZE Spalten - 1, Zeilen - 1, 20, 20

Listing 51: Die "PACMAN_LOAD"

Nun werden alle bisher erstellen Objekte wieder gelöscht und die Steuerungs-Tasten für Pacman (also die Cursor-Tasten) werden auf "ungedrückt" gesetzt.

   For M = 0 To UBound(Obj)
       OBJ_CLEAR Obj(M)
   Next M

   KEY_DOWN(37) = 0
   KEY_DOWN(38) = 0
   KEY_DOWN(39) = 0
   KEY_DOWN(40) = 0

Listing 52: Die "PACMAN_LOAD"

Jetzt setzen wir unseren Objektzähler AObj auf Null und Y auf Null. Dann erstellen wir Pacman an der Position 0,0. Wenn Sie die Syntax von OBJ_SET nicht mehr kennen, so lesen Sie diese bitte in den letzten Teilen nach. Hier sehen Sie auch wieder, daß wir mit den Konstanten NR_xxxxxxx und ID_xxxxxxx gut arbeiten können. Anschließend erhöhen wir AObj für das nächste Objekt. Dieses Verfahren ist wichtig, damit Pacman immer Objekt Nr. 0 ist und so leicht angesprochen werden kann.

Y = 0
AObj = 0
OBJ_SET Obj(AObj), 0, 0, 100, 0, 0, NR_PACMAN,_
    Spr(ID_PACMAN_DOWN), 1
AObj = AObj + 1

Listing 53: Die "PACMAN_LOAD"

Die Leveldatei wird wieder geöffnet, und wir beginnen mit dem eigentlichen Einlesen.

Open Pfad$ + Datei$ For Input As #1
Do 
  Line Input #1, A$

Listing 54: Die "PACMAN_LOAD"

Nachdem wir jeweils eine Zeile gelesen haben, gehen wir diese Zeile mit einer FOR-Schleife durch. Dabei zählen wir von 0 bis Länge-1. Das Zeichen erhalten wir mit Hilfe der MID$()-Funktion:

For X = 0 To Len(A$) - 1
  Zeichen$ = Mid$(A$, X + 1, 1)

Listing 55: Die "PACMAN_LOAD"

Jetzt kommt der längste Teil: Wir haben ein Zeichen aus einer Datei eingelesen. Dieses Zeichen stellt ein Objekt dar. In einem SELECT-CASE-Block sehen wir nun nach, welches Zeichen es ist und welches Objekt wir dafür setzten. Dieser Teil ist für das Design der Level wichtig. Ich habe Ihnen folgendes Format vorgegeben:
Jede Leveldatei ist, wie Sie sicher schon gemerkt haben, wie eine Karte aufgebaut. Jedes Zeichen entspricht einem Objekt, so daß man ein Level sehr gut und einfach mit einem Texteditor und einer unproportionalen Schrift, wie z.B. Courier New, erstellen kann.

WasZeichenObjekt
Wände!Graue Wand
Wände "Gelbe Wand (CHR$(34) ist ein Anführungszeichen)
Wände§Orange Wand
Wände$Rote Wand
Wände%Lila Wand
Wände&Blaue Wand
Wände/Türkise Wand
Wände(Grüne Wand
Wände)Braune Wand
Pacman & ItemsPPacman
Pacman & ItemsCCool-Pacman (Zeit bleibt stehen)
Pacman & ItemsIUnsichtbarkeit
Pacman & ItemsTTerminator-Pacman (kann Monster fressen)
Pacman & ItemsXTotenkopf (zieht Pacman Energie ab)
Pacman & Items.Punkt
Pacman & ItemsFFlagge
Monster1Blau
Monster2Grün
Monster3Rot
Monster4Braun
Monster5Lila
Monster6Hippie (das soll niemanden diskriminieren, aber mir fiel kein anderer Name ein!)
SchlüsselkSchlüssel Typ 1
SchlüsselKSchlüssel Typ 2
SchlüsseldTür für Schlüssel 1
SchlüsselDTür für Schlüssel 2
SchalterRRoter Schalter
SchalterGGrüner Schalter
SchalterBBlauer Schalter
SchalterYGelber Schalter
SchalterrRote Block
SchaltergGrüner Block
SchalterbBlauer Block
SchalteryGelber Block
Teleporter7Roter Teleporter
Teleporter8Grüner Teleporter
Teleporter9Blauer Teleporter
Teleporter0Gelber Teleporter

Wenn Sie selbst einige Level entwerfen wollen, was ich Ihnen bei nur drei Level empfehlen kann, sollten Sie sich die Liste oben ausdrucken und beim Editieren neben den Rechner legen. Sie können natürlich auch einen Leveleditor schreiben, aber dies wollte ich in diesem Kurs auf Grund von Zeit & Platz nicht auch noch machen.
Jetzt muß jedes Spielelement, das auch oben in der Liste steht, in den SELECT-CASE-Block hinein. Der nun folgende Teil sieht zwar SEHR unübersichtlich aus, basiert aber im Grunde nur auf folgenden Teilen (fett gedrucktes ändert sich von Fall zu Fall):

Für Wände:

CASE <Zeichen>: FIELD_SET x, y, 1, <Sprite>, 1

Für Objekte:

 CASE <Zeichen>: OBJ_SET Obj(AObj), X * 20, Y * 20, 100, 0, 0, _
<Nr>, Spr(<Sprite>), 1: AObj=AObj+1

Und mit diesem Wissen ist es leicht einen Block zu erstellen. Beispiel für das blaue Monster nehmen wir als Zeichen "1" (aus der Liste zu entnehmen) und die Konstanten NR_MONSTER_BLUE und ID_MONSTER_BLUE. Hier sehen Sie, warum ich am Anfang Konstanten benutzt habe.

Case "1": OBJ_SET Obj(AObj), X * 20, Y * 20, 100, 0, 0,_
NR_MONSTER_BLUE, Spr(ID_MONSTER_BLUE), 1: AObj = AObj + 1

Nun an die Arbeit! Ich werde nur auf einige besondere Fälle eingehen. Alle anderen, die nach dem oben gezeigten Schema funktionieren werde ich nur auflisten. Ich schlage Ihnen vor den folgenden Code zu kopieren, es sei denn, Sie haben sehr viel Lust die nächsten 15 Minuten zu tippen ;-)

Ach ja, bei den Wänden wird jeweils noch ein Zufallswert zwischen 0 und 4 addiert, damit wir Wände in unterschiedlichen Schattierungen (Helligkeiten) haben.

Select Case Zeichen$

Es kommen jetzt die Wände

Case "!":FIELD_SET X, Y, 1, Spr(ID_WALL_GRAY + Fix(Rnd*4)), 1
Case Chr$(34):FIELD_SET X,Y,1, Spr(ID_WALL_YELLOW+Fix(Rnd*4)),1
Case "§":FIELD_SET X,Y,1, Spr(ID_WALL_ORANGE + Fix(Rnd * 4)), 1
Case "$":FIELD_SET X,Y,1, Spr(ID_WALL_RED + Fix(Rnd*4)), 1
Case "%":FIELD_SET X,Y,1, Spr(ID_WALL_MAGENTA + Fix(Rnd*4)), 1
Case "&":FIELD_SET X,Y,1, Spr(ID_WALL_BLUE + Fix(Rnd*4)), 1
Case "/":FIELD_SET X,Y,1, Spr(ID_WALL_CYAN + Fix(Rnd*4)), 1
Case "(":FIELD_SET X,Y,1, Spr(ID_WALL_GREEN + Fix(Rnd*4)), 1
Case ")":FIELD_SET X,Y,1, Spr(ID_WALL_BROWN + Fix(Rnd*4)), 1

Pacman wird einfach nur verschoben:

Case "P": Obj(0).X = X * 20: Obj(0).Y = Y * 20

Und nun die Extras:

Case "C":OBJ_SET Obj(AObj),X*20,Y*20,100,0,0,NR_ITEM_COOL,_
         Spr(ID_ITEM_COOL), 1: AObj = AObj + 1
Case "I":OBJ_SET Obj(AObj),X*20,Y*20,100,0,0, NR_ITEM_INVISIBLE,_
         Spr(ID_ITEM_INVISIBLE), 1: AObj = AObj + 1
Case "T":OBJ_SET Obj(AObj),X*20,Y*20,100,0,0, NR_ITEM_TERMINATOR,_
         Spr(ID_ITEM_TERMINATOR), 1: AObj = AObj + 1
Case "X":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_ITEM_DEAD,_
         Spr(ID_ITEM_DEAD), 1: AObj = AObj + 1

Ah! Die Monster kommen!!

Case "1":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_MONSTER_BLUE,_
         Spr(ID_MONSTER_BLUE), 1: AObj = AObj + 1
Case "2":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_MONSTER_GREEN,_
         Spr(ID_MONSTER_GREEN), 1: AObj = AObj + 1
Case "3":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_MONSTER_RED,_
         Spr(ID_MONSTER_RED), 1: AObj = AObj + 1
Case "4":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_MONSTER_BROWN,_
         Spr(ID_MONSTER_BROWN), 1: AObj = AObj + 1
Case "5":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, _
         NR_MONSTER_MAGENTA,_
         Spr(ID_MONSTER_MAGENTA), 1: AObj = AObj + 1
Case "6":OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, _
         NR_MONSTER_HIPPIE,_
         Spr(ID_MONSTER_HIPPIE), 1: AObj = AObj + 1

Schlüssel:

 Case "k": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_ITEM_KEY1,_
           Spr(ID_ITEM_KEY1), 1: AObj = AObj + 1
 Case "K": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_ITEM_KEY2,_
           Spr(ID_ITEM_KEY2), 1: AObj = AObj + 1

 Case "d": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_DOOR1,_
           Spr(ID_DOOR1), 1: AObj = AObj + 1
 Case "D": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_DOOR2,_
           Spr(ID_DOOR2), 1: AObj = AObj + 1

Bei den Punkten wir zusätzlich noch COUNT_DOTS erhöht...

Case ".": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_DOT,_
          Spr(ID_DOT), 1: AObj = AObj + 1
          COUNT_DOTS = COUNT_DOTS + 1
Case "F": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_FLAG,_
          Spr(ID_FLAG), 1: AObj = AObj + 1

Jetzt kommen die Schalter:

 Case "R": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_SWITCH_RED,_
           Spr(ID_SWITCH_RED), 1: AObj = AObj + 1
 Case "G": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_SWITCH_GREEN,_
           Spr(ID_SWITCH_GREEN), 1: AObj = AObj + 1
 Case "B": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_SWITCH_BLUE,_
           Spr(ID_SWITCH_BLUE), 1: AObj = AObj + 1
 Case "Y": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, _
           NR_SWITCH_YELLOW,_
           Spr(ID_SWITCH_YELLOW), 1: AObj = AObj + 1

 Case "r": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_BLOCK_RED,_
           Spr(ID_BLOCK_RED), 1: AObj = AObj + 1
 Case "g": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_BLOCK_GREEN,_
           Spr(ID_BLOCK_GREEN), 1: AObj = AObj + 1
 Case "b": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_BLOCK_BLUE,_
           Spr(ID_BLOCK_BLUE), 1: AObj = AObj + 1
 Case "y": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_BLOCK_YELLOW,_
           Spr(ID_BLOCK_YELLOW), 1: AObj = AObj + 1

Und die Teleporter...

 Case "7": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_TELE_RED,_
           Spr(ID_TELE_RED), 1: AObj = AObj + 1
 Case "8": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_TELE_GREEN,_
           Spr(ID_TELE_GREEN), 1: AObj = AObj + 1
 Case "9": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_TELE_BLUE,_
           Spr(ID_TELE_BLUE), 1: AObj = AObj + 1
 Case "0": OBJ_SET Obj(AObj), X*20,Y*20,100,0,0, NR_TELE_YELLOW,_
           Spr(ID_TELE_YELLOW), 1: AObj = AObj + 1

 End Select

Puh, das war ein ganz schönes Stück Arbeit. Aber dafür geht's jetzt erst einmal etwas ruhiger zu. Wir schließen dir FOR-Schleife. Anschließend wird Y um eins erhöht. Die Abbruchbedingung der offenen DO-Schleife lautet UNTIL EOF(1), was bedeutet, daß der ganze Block solange durchlaufen wird, bis die Datei zu Ende ist. Dann wird sie geschlossen und wir sind für's erste durch.

      Next X
    Y = Y + 1
  Loop Until EOF(1)
  Close #1  
End Sub

Listing 56: Das Ende der Schleife

4.7 Pacmans Steuerpult  

So, nun eine etwas einfachere Prozedur: PACMAN_RUN. Diese Routine wird bei jedem Durchlauf der Hauptschleife aufgerufen. Hier sitzt die Steuerung von PACMAN:

Sub PACMAN_RUN ()

Listing 57: Die Sub "PACMAN_RUN"

Als erstes wird die Geschwindigkeit von Pacman auf 0,0 zurückgesetzt.

Obj(0).SX = 0
Obj(0).SY = 0

Listing 58: Die Sub "PACMAN_RUN"

Nun wird geprüft, welche der Cursor-Tasten gedrückt ist. Wenn oben gedrückt ist, und der Block über Pacman frei zugänglich ist (wird mit OBJ_UP geprüft) wird die Geschwindigkeit von Pacman in Y-Richtung dorthin gehend gesetzt. Das gleiche gilt für links, rechts und unten. Übrigens können Sie die Tastencodes einfach herausbekommen, wenn Sie ein Programm schreiben, indem in der FORM_KEYDOWN Routine der Tastencode auf die Titelseite des Fenster (Me.Caption = STR$(KeyCode)) geschrieben wird.

If KEY_DOWN(38) And OBJ_UP(Obj(0), 4) Then Obj(0).SY = -4
If KEY_DOWN(40) And OBJ_DOWN(Obj(0), 4) Then Obj(0).SY = 4
If KEY_DOWN(37) And OBJ_LEFT(Obj(0), 4) Then Obj(0).SX = -4
If KEY_DOWN(39) And OBJ_RIGHT(Obj(0), 4) Then Obj(0).SX = 4

Listing 59: Die Sub "PACMAN_RUN"

Nach der Steuerung rufen wir PACMAN_SHOW auf, die den Bildschirm zeichnet:

  PACMAN_SHOW
  
End Sub

Listing 60: Die Sub "PACMAN_RUN"

4.8 Auf den Bildschirm  

Diese Routine sorgt dafür, daß der gesamte Bildschirm gezeichnet wird und alle auftretenden Ereignisse weitergereicht werden.

Sub PACMAN_SHOW ()

Listing 61: Die Sub "PACMAN_SHOW"

Die SICHT-Koordinaten des Spielfelds errechnen sich aus der Hälfte des Bildfeldes minus Pacman's Koordinaten. Das sorgt dafür, daß die Karte um Pacman zentriert wird.

SICHT_X = Fix(MAIN.DISPLAY.ScaleWidth / 2 - Obj(0).X)
SICHT_Y = Fix(MAIN.DISPLAY.ScaleHeight / 2 - Obj(0).Y)

Listing 62: Die Sub "PACMAN_SHOW"

Die Karte soll aber nicht über die Ränder hinaus gescrollt werden. Daher fragen wir hier ab, ob der SICHT-Koordinaten kleiner als Null bzw. jeweils größer als die Breite/Höhe des Bildfeldes sind und ändern sie ggf..

If SICHT_X > 0 Then SICHT_X = 0
If SICHT_Y > 0 Then SICHT_Y = 0
If SICHT_X + ((FIELD_X + 1)*FIELD_W-MAIN.DISPLAY.ScaleWidth)_
         <0 Then SICHT_X = -((FIELD_X + 1) * FIELD_W -_
         MAIN.DISPLAY.ScaleWidth)
If SICHT_Y + ((FIELD_Y + 1)*FIELD_H- MAIN.DISPLAY.ScaleHeight)_
         <0 Then SICHT_Y = -((FIELD_Y + 1) * FIELD_H -_
         MAIN.DISPLAY.ScaleHeight)

Listing 63: Die Sub "PACMAN_SHOW"

Nun kann das Bildfeld gelöscht und das Spielfeld gezeichnet werden:

MAIN.DISPLAY.Cls
FIELD_DRAW MAIN.DISPLAY, SICHT_X, SICHT_Y

Listing 64: Die Sub "PACMAN_SHOW"

Im Anschluß daran werden alle Objekte gezeichnet:

For M = 0 To UBound(Obj)
  OBJ_DRAW Obj(M), SICHT_X, SICHT_Y, MAIN.DISPLAY
 Next M

Listing 65: Die Sub "PACMAN_SHOW"

Nun ist das Bild fertig, aber wir müssen noch die Ereignisse auslösen, die unseren bunten Bildschirm erst zum Leben erwecken. Daher lösen wir in einer Schleife bei allen Objekten das ID_MOVE-Ereignis aus, das den Objekten die Chance gibt, sich zu bewegen.

For M = 0 To UBound(Obj)
  
  OBJ_EVENT Obj(M), ID_MOVE, ObjDummy, Obj(M).SX, Obj(M).SY

Listing 66: Die Sub "PACMAN_SHOW"

Für den Fall, daß wir gerade unseren PACMAN erwischt haben prüfen wir eine Kollision mit allen anderen Objekten auf der Karte. Sonst prüfen wir, ob sich das Objekt bewegt. Wenn das der Fall ist, prüfen wir auf Kollision mit allen blockierenden Gegenständen, durch die auch Monster nicht hindurch kommen.

  If Obj(M).T = NR_PACMAN Then 
      For M2 = M + 1 To UBound(Obj)
          OBJ_COLL_OBJ Obj(M), Obj(M2)
      Next M2
  Else 
      If Obj(M).SX <> 0 Or Obj(M).SY <> 0 Then 
          For M2 = 0 To UBound(Obj)
              Need = 0
              Select Case Obj(M2).T
                  Case NR_BLOCK_RED: Need = 1
                  Case NR_BLOCK_BLUE: Need = 1
                  Case NR_BLOCK_GREEN: Need = 1
                  Case NR_BLOCK_YELLOW: Need = 1
                  Case NR_DOOR1: Need = 1
                  Case NR_DOOR2: Need = 1
              End Select
              If Need Then OBJ_COLL_OBJ Obj(M), Obj(M2)
          Next M2
     End If 
  End If 
Next M

Listing 67: Die Sub "PACMAN_SHOW"

Wenn Pacman "Extras" aufgenommen hat wird die Zeit errechnet, die noch zur Verfügung steht.

PowerUp = 0
E = OBJ_ITEM_ENERGY_GET(Obj(0), NR_ITEM_COOL)
If E > PowerUp Then PowerUp = E
E = OBJ_ITEM_ENERGY_GET(Obj(0), NR_ITEM_INVISIBLE)
If E > PowerUp Then PowerUp = E
E = OBJ_ITEM_ENERGY_GET(Obj(0), NR_ITEM_TERMINATOR)
If E > PowerUp Then PowerUp = E

Listing 68: Die Sub "PACMAN_SHOW"

Dann wird daraus errechnet, welches Bild der Sanduhr genommen wird, die in der linken oberen Ecke angezeigt wird. Da wir sechs Bilder haben und jedes PowerUp normal 300 Durchgänge hält können wir das Bild mit 6/300*PowerUp errechnen. Dementsprechend wird es ausgegeben.

If PowerUp > 0 Then 
  PowerUp = 6 / 300 * PowerUp
  If PowerUp <= 6 Then 
    SPRITE_DRAW Spr(ID_TIME+6-PowerUp),25,5,MAIN.DISPLAY
  End If 
End If

Listing 69: Die Sub "PACMAN_SHOW"

Ähnlich sieht es auch bei der Energie aus. Auch sie wird mittels eines Sprites in der Bildfeldecke angezeigt. Unterschreitet die Energie gar 0 wird LEVEL_NEXT auf -2 gesetzt, um dem Hauptprogramm den "Tod" des Spielers mitzuteilen.

If Obj(0).E >= 0 Then 
  Energy = 16 / 100 * Obj(0).E
  SPRITE_DRAW Spr(ID_ENERGY+16-Energy), 5, 5, MAIN.DISPLAY
Else 
  LEVEL_NEXT = -2
End If

Listing 70: Die Sub "PACMAN_SHOW"

Zum Schluß wird noch das Bildfeld "refresht", damit unsere Änderungen auch zu sehen sind.

  MAIN.DISPLAY.Refresh
  
End Sub

Listing 71: Die Sub "PACMAN_SHOW"

4.9 Weltbewegende Ereignisse  

Wie im letzten Teil besprochen, basiert Pacman auf einer selbstgestricken Ereignissteuerung. Jedesmal, wenn wir OBJ_EVENT aufrufen, wird das Ereignis an eine, von Spiel zu Spiel andere, USE_EVENT-Routine weitergeleitet. Diese Routine sorgt nun für die Bewegung der Spieler und Monster und für den ganzen Rest.
Die USER_EVENT-Routine für unser Pacman-Spiel ist nicht besonders schwer. Für den Fall, daß das übergebene Objekt den Typ 0 hat, wird die Routine abgebrochen, da leere Objekte nicht berücksichtigt werden. Im anderen Fall wird das Ereignis an mehrere Unterprozeduren weitergeleitet, von denen jede die Ereignisse für ganz bestimmte Bereiche verwaltet. Wir hätten zwar auch alles in diese Routine hineinschreiben können, allerdings wäre sie dann sehr groß und unübersichtlich geworden.

Sub USER_EVENT (Self As ObjType,MESSAGE,Other As _
                ObjType,Par1,Par2)
  If Self.T = 0 Then Exit Sub 
  USER_EVENT_PACMAN Self, MESSAGE, Other, Par1, Par2
  USER_EVENT_DOORS Self, MESSAGE, Other, Par1, Par2
  USER_EVENT_ITEM Self, MESSAGE, Other, Par1, Par2
  USER_EVENT_MONSTER Self, MESSAGE, Other, Par1, Par2
  USER_EVENT_SWITCH Self, MESSAGE, Other, Par1, Par2
  USER_EVENT_TELE Self, MESSAGE, Other, Par1, Par2
End Sub

Listing 72: Die Sub "USER_EVENT"

4.9.1 Pacmans Ereignisse  

Die erste untergeordnete Ereignisprozedur ist OBJ_EVENT_PACMAN. Diese Prozedur verwaltet nur PACMAN selbst, die "eßbaren" Punkte und die Flagge.
Der Prozedurkopf ist relativ simpel und auch eigentlich schon bekannt. Es werden wieder die gleichen Variablen wie bei OBJ_EVENT übergeben. Anschließend öffnen wir einen SELECT-CASE-Block, in dem wir den Typ des übergebenen Objekts auswählen.

         Sub USER_EVENT_PACMAN (Self As ObjType,MESSAGE,Other As_
                                ObjType, Par1, Par2)

           Select Case Self.T

Listing 73: Die Sub "USER_EVENT_PACMAN"

Nun kommt der Teil für Pacman. Hier wählen wir wieder per SELECT CASE, das aufgetretene Ereignis aus:

Case NR_PACMAN:     ' Pacman 
     Select Case MESSAGE

Listing 74: Die Sub "USER_EVENT_PACMAN"

Jetzt kommt als erstes das DRAW-Ereignis. Das Einrücken müssen Sie in diesem Fall selbst noch korrekt erledigen, da sonst fast alle Zeilen länger gewesen wären als die Seitenbreite.

Case ID_DRAW

Listing 75: Die Sub "USER_EVENT_PACMAN"

Am Anfang verwenden wir OBJ_ANIMATE um die Reservewerte R1 und R2 als Spritezähler zu verwenden. R1 wird solange erhöht, bis es den Wert 3 hat. Dann wird es zurückgesetzt und R2 wird erhöht. Wenn R2 den Wert 3 erreicht wird es auf 0 gesetzt. Damit haben wir in R2 die Animationsphase (zwischen 0 und 3), die sich alle vier Durchgänge ändert. So bekommt man einfach langsamere Animationen hin. Dann werden noch R3 auf 0 gesetzt und V (Sichtbar) auf 1.

OBJ_ANIMATE Self.R1, 3, Self.R2, 3

Self.R3 = 0
Self.V = 1

Listing 76: Die Sub "USER_EVENT_PACMAN"

Nun wird überprüft, ob PACMAN irgendwelche Extras bei sicht hat. Zuerst schauen wir nach, ob er das COOL-Item hat (Zeit bleibt stehen) und die Energie dieses Items größer als Null ist. Wenn ja, verringern wir die "Energie" dieses Items verringert und R3 wird auf 12 gesetzt. Der Reservewert R3 wird bei Pacman dazu verwendet, zu bestimmen, in welchem Modus sich Pacman befindet. Dieser Wert wird immer zur Spritenummer hinzuaddiert. Da das COOL-Item die Zeit anhalten soll, so lange es wirkt, setzten wir alle anderen Objekte außer Pacman auf die letzte Position zurück. Ist das Item dann leer, wird es per OBJ_ITEM_DROP aus dem Inventar gestrichen.

If OBJ_ITEM_HAVE(Self, NR_ITEM_COOL) Then 
  Energy = OBJ_ITEM_ENERGY_GET(Self, NR_ITEM_COOL)
  If Energy > 0 Then 
    OBJ_ITEM_ENERGY_SET Self, NR_ITEM_COOL, Energy - 1
    Self.R3 = 12
    For M = 0 To UBound(Obj)
      If Obj(M).T <> NR_PACMAN Then 
        Obj(M).X = Obj(M).OX
        Obj(M).Y = Obj(M).OY
      End If 
    Next M
  Else 
    OBJ_ITEM_DROP Self, NR_ITEM_COOL
  End If 
End If

Listing 77: Die Sub "USER_EVENT_PACMAN"

Etwas einfacher ist es bei dem Unsichbar-Item. Hier gilt zwar das gleiche System mit der Energie, jedoch wird als Auswirkung einfach nur Self.V auf 0 gesetzt, das Pacman für alle Monster unsichtbar macht. Außerdem haben wir hier einen anderen Wert für R3, damit Pacman auch auf dem Spielfeld unsichtbar erscheint.

If OBJ_ITEM_HAVE(Self, NR_ITEM_INVISIBLE) Then 
  Energy = OBJ_ITEM_ENERGY_GET(Self, NR_ITEM_INVISIBLE)
  If Energy > 0 Then 
    OBJ_ITEM_ENERGY_SET Self, NR_ITEM_INVISIBLE, Energy - 1
    Self.R3 = 8
    Self.V = 0
  Else 
    OBJ_ITEM_DROP Self, NR_ITEM_INVISIBLE
  End If 
End If

Listing 78: Die Sub "USER_EVENT_PACMAN"

Bei dem Terminator-Item wird R3 auf 4 gesetzt und die Pacman-Geschwindigkeit mit 1,5 multipliziert. Die Auswirkungen werden an einer anderen Stelle berücksichtigt.

If OBJ_ITEM_HAVE(Self, NR_ITEM_TERMINATOR) Then 
  Energy = OBJ_ITEM_ENERGY_GET(Self, NR_ITEM_TERMINATOR)
  If Energy > 0 Then 
    OBJ_ITEM_ENERGY_SET Self, NR_ITEM_TERMINATOR, Energy - 1
    Self.R3 = 4
    Self.SX = Self.SX * 1.5
    Self.SY = Self.SY * 1.5
  Else 
    OBJ_ITEM_DROP Self, NR_ITEM_TERMINATOR
  End If 
End If

Listing 79: Die Sub "USER_EVENT_PACMAN"

Zum Schluß wird dann der FRAME aus der ID + dem Reservewert R3 errechnet. Zusätzlich wird geprüft, in welche Richtung Pacman sieht. Schaut er nach oben wird PACMAN_UP als Grundnummer benutzt usw.

Self.FRAME = Spr(ID_PACMAN_DOWN + Self.R3)
If Self.SY<0 Then Self.FRAME=Spr(ID_PACMAN_UP+Self.R2 + Self.R3)
If Self.SY>0 Then Self.FRAME=Spr(ID_PACMAN_DOWN+Self.R2+Self.R3)
If Self.SX>0 Then Self.FRAME=Spr(ID_PACMAN_RIGHT+Self.R2+Self.R3)
If Self.SX<0 Then Self.FRAME=Spr(ID_PACMAN_LEFT+Self.R2+Self.R3)

Listing 80: Die Sub "USER_EVENT_PACMAN"

Das war auch schon das komplette Ereignis zum Anzeigen von Pacman in allen Lebenslagen. Jetzt fehlen uns noch andere wichtige Ereignisse: ID_MOVE gibt an, daß sich das Objekt bewegen soll und zwar um die übergebenen Werte Par1 (=X-Bewegung) und Par2 (=Y-Bewegung). Vorher wird hier, wie bei den Monstern auch, die Position gespeichert, damit sie später wieder geladen werden kann. Nach der Bewegung wird OBJ_COLL_FIELD aufgerufen, um zu prüfen, ob PACMAN immer noch auf einem leeren Feld steht.

Case ID_MOVE
  Self.OX = Self.X
  Self.OY = Self.Y
  Self.X = Self.X + Par1
  Self.Y = Self.Y + Par2
  OBJ_COLL_FIELD Self

Listing 81: Die Sub "USER_EVENT_PACMAN"

Ist dies nicht der Fall, ruft die Routine OBJ_COLL_FIELD das Ereignis ID_COLL_FIELD auf. Dieses sieht bei den Akteuren immer gleich aus:

Case ID_COLL_WALL
  Self.X = Self.OX
  Self.Y = Self.OY
  Self.SX = Self.SX / 2
  Self.SY = Self.SY / 2
  OBJ_EVENT Self, ID_MOVE, ObjDummy, Self.SX, Self.SY

Listing 82: Die Sub "USER_EVENT_PACMAN"

Die Position wird zurückgesetzt und die Geschwindigkeit halbiert. Anschließend wird noch einmal ID_MOVE aufgerufen, um zu testen, ob sich das Objekt wenigstens etwas weiterbewegen kann.
Damit wäre PACMAN selbst auch schon abgehandelt. Das war doch nicht so schwer, oder?
Nun zu den "eßbaren" Punkten, die PACMAN während des Spiels einsammeln muß:

Case NR_DOT:        ' Punkte 
  Select Case MESSAGE

Listing 83: Die Sub "USER_EVENT_PACMAN"

Bei einer Kollision wird geprüft, ob PACMAN der Auslöser war. In dem Fall sucht die Routine, ob PACMAN bereits über ein Item NR_DOT verfügt. Wenn ja wird der Wert eingelesen, erhöht und dann wieder zurückgeschrieben. Wenn nein wird dieses Item nun mit der "Energie" (was ja nicht unbedingt für Energie im herkömmlichen Sinne steht, sondern in diesem Fall eher Quantität bedeutet) 1 angelegt. Anschließend bekommt PACMAN noch 5 Punkte, und eine WAVE-Datei wird abgspielt. Am Ende wird das Objekt gelöscht, damit der Punkt nicht noch einmal eingesammelt werden kann.

Case ID_COLL
  If Other.T = NR_PACMAN Then 
    Dots = OBJ_ITEM_ENERGY_GET(Other, NR_DOT) + 1
    OBJ_ITEM_ENERGY_SET Other, NR_DOT, Dots
    OBJ_CLEAR Self
    LEVEL_SCORE = LEVEL_SCORE + 5
    WAV_PLAYBACK Pfad$ + "DOT.WAV"
  End If

Listing 84: Die Sub "USER_EVENT_PACMAN"

Das Zeichnen verläuft denkbar einfach. Wir haben auch wieder einen Animationszähler (R2 nimmt Werte zwischen 0 und 3 an) und addieren den zu ID_DOT, was ja die Position der Punkte im Sprite-Datenfeld ist. Nun wird der Frame aktualisiert.

Case ID_DRAW
  OBJ_ANIMATE Self.R1, 0, Self.R2, 3
  Self.FRAME = Spr(ID_DOT + Self.R2)
End Select

Listing 85: Die Sub "USER_EVENT_PACMAN"

Die Flagge, die das Ziel markiert, ist auch nicht mehr schwer zu realisieren:

Case NR_FLAG:        ' Flagge 
  Select Case MESSAGE

Listing 86: Die Sub "USER_EVENT_PACMAN"

Bei einer Kollision mit der Flagge wird geprüft, ob Pacman das Item NR_DOT bei sich hat und ob dessen "Energie" genauso groß ist, wie die Anzahl der Punkte insgesamt. In diesem Fall bekommt er 50 Punkte und LEVEL_NEXT wird auf 1 gesetzt um dem Hauptprogramm mitzuteilen, daß das Level erledigt ist.

Case ID_COLL
  If Other.T = NR_PACMAN Then 
    If COUNT_DOTS=OBJ_ITEM_ENERGY_GET(Other, NR_DOT) Then 
      LEVEL_NEXT = 1
      LEVEL_SCORE = LEVEL_SCORE + 50
    End If 
  End If

Listing 87: Die Sub "USER_EVENT_PACMAN"

Beim Zeichnen werden zwei Fälle unterschieden: Wenn alle Punkte gesammelt worden sind bewegt sich die Flagge, wird also per Animationszähler verändert, Wenn nicht, wird nur das erste Bild angezeigt. Daran kann der Spieler dann schon immer sehen, ob er sich zum Ausgang begeben kann oder nicht.

Case ID_DRAW
  If COUNT_DOTS = OBJ_ITEM_ENERGY_GET(Obj(0), NR_DOT) Then 
    OBJ_ANIMATE Self.R1, 1, Self.R2, 9
    Self.FRAME = Spr(ID_FLAG + Self.R2)
  Else 
    Self.FRAME = Spr(ID_FLAG)
  End If 
End Select

Listing 88: Die Sub "USER_EVENT_PACMAN"

Und nun das letzte Objekt aus USER_EVENT_PACMAN: Der Animator. Dieses Objekt dient dazu, eine Animation darzustellen. Das Objekt agiert und reagiert nicht. Es wird z.B. benutzt, wenn ein Monster stirbt, die "Überreste" darzustellen ohne einen eigenen Typ zu definieren. Das Objekt stellt immer eine Animation zwischen R3+0 und R3+3 dar. Darum muß der Wert R3 gesetzt werden, bevor man ein solches Objekt erstellt.

Case NR_ANIMATOR:
  Select Case MESSAGE
  Case ID_DRAW
    OBJ_ANIMATE Self.R1, 0, Self.R2, 3
    Self.FRAME = Spr(Self.R3 + Self.R2)
 End Select

Listing 89: Die Sub "USER_EVENT_PACMAN"

4.9.2 Schlüsselerlebnisse  

Die Routine USER_EVENT_DOORS umfaßt natürlich alle Tür- und Schlüssel-Objekte. Mehr gibt es da eigentlich nicht zu sagen.

Sub USER_EVENT_DOORS (Self As ObjType, MESSAGE, Other As ObjType,_
                     Par1, Par2)
  Select Case Self.T

Listing 90: Die Sub "USER_EVENT_DOORS"

Ein Schlüssel reagiert bei Kollision nur auf PACMAN. In dem Fall wird nachgesehen, wie viele Schlüssel dieser Art der Spieler bei sich trägt, dieser Wert wird erhöht und zurückgeschrieben. Dabei wird dann noch eine Sounddatei abgespielt. Auch die Schlüssel löschen sich danach selbst, damit sich der Spieler nicht unendlich daran bedienen kann.

Case NR_ITEM_KEY1
  Select Case MESSAGE
  
   Case ID_COLL
     If Other.T = NR_PACMAN Then 
      Anzahl = OBJ_ITEM_ENERGY_GET(Other, NR_ITEM_KEY1) + 1
      OBJ_ITEM_ENERGY_SET Other, NR_ITEM_KEY1, Anzahl
      OBJ_CLEAR Self
      WAV_PLAYBACK Pfad$ + "ITEM.WAV"
    End If

Listing 91: Die Sub "USER_EVENT_DOORS"

Das Zeichnen des Schlüssels ist wohl keiner Erklärung mehr bedürftig, oder??

Case ID_DRAW
  OBJ_ANIMATE Self.R1, 3, Self.R2, 3
  Self.FRAME = Spr(ID_ITEM_KEY1 + Self.R2)
End Select

Listing 92: Die Sub "USER_EVENT_DOORS"

Auch die Programmierung des zweiten Schlüssel ist kein großes Geheimnis mehr...

Case NR_ITEM_KEY2
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN Then 
        Anzahl = OBJ_ITEM_ENERGY_GET(Other, NR_ITEM_KEY2) + 1
        OBJ_ITEM_ENERGY_SET Other, NR_ITEM_KEY2, Anzahl
        OBJ_CLEAR Self
        WAV_PLAYBACK Pfad$ + "ITEM.WAV"
      End If 
   Case ID_DRAW
     OBJ_ANIMATE Self.R1, 3, Self.R2, 3
     Self.FRAME = Spr(ID_ITEM_KEY2 + Self.R2)
   End Select

Listing 93: Die Sub "USER_EVENT_DOORS"

Nun zu den Türen. Bei der Tür läuft es anders herum: Bei Berührung von PACMAN wird geprüft, ob ein Schlüssel da ist. Wenn ja, wird er entfernt bzw. die "Energie" (hierbei also die Quantität) wird um 1 verringert. Dann löscht sich die Tür selbst um den Weg freizugeben und gibt einen Sound aus. Wenn der Spieler keinen passenden Schlüssel hat wird er mit Hilfe der gesicherten Koordinaten OX und OY zurückgesetzt und gestoppt.

Case NR_DOOR1
  Select Case MESSAGE
  
  Case ID_COLL
    If Other.T = NR_PACMAN Then 
      If OBJ_ITEM_HAVE(Other, NR_ITEM_KEY1) Then 
        Anzahl=OBJ_ITEM_ENERGY_GET(Other,NR_ITEM_KEY1)-1
        OBJ_ITEM_ENERGY_SET Other, NR_ITEM_KEY1, Anzahl
        OBJ_CLEAR Self
        WAV_PLAYBACK Pfad$ + "TÜR.WAV"
      Else 
        Other.X = Other.OX
        Other.Y = Other.OY
        Other.SX = 0
        Other.SY = 0
      End If 
    End If 
  End Select

Listing 94: Die Sub "USER_EVENT_DOORS"

Die zweite Tür läuft parallel, ist aber natürlich nur mit dem 2. Schlüsseltyp "kompatibel".

Case NR_DOOR2
  Select Case MESSAGE
  
  Case ID_COLL
    If Other.T = NR_PACMAN Then 
      If OBJ_ITEM_HAVE(Other, NR_ITEM_KEY2) Then 
        Anzahl=OBJ_ITEM_ENERGY_GET(Other,NR_ITEM_KEY2)-1
        OBJ_ITEM_ENERGY_SET Other, NR_ITEM_KEY2, Anzahl
        OBJ_CLEAR Self
        WAV_PLAYBACK Pfad$ + "TÜR.WAV"
      Else 
        Other.X = Other.OX
        Other.Y = Other.OY
        Other.SX = 0
        Other.SY = 0
      End If 
  End If 
  
End Select

Listing 95: Die Sub "USER_EVENT_DOORS"

4.9.3 Hier gibts was extra  

Die Extras werden in der Prozedur USER_EVENT_ITEM behandelt. Der Kopf der Prozedur ist klar.

Sub USER_EVENT_ITEM (Self As ObjType, MESSAGE, Other As ObjType,_
                     Par1, Par2)
  
  Select Case Self.T

Listing 96: Die Sub "USER_EVENT_ITEMS"

Das COOL-Powerup (oder Speed-PowerUp) stoppt die Zeit. Aber an dieser Stelle ist das unwichtig, da sich das Objekt hier ja nur in "instant"-Form auf dem Bildschirm befindet. Wenn PACMAN es jedoch aufnimmt sorgt es dafür, daß alle anderen aktiven Extras fallengelassen werden (nach dem Motto: Bin ich nicht Dein Lieblings-PowerUp?). Dann löscht es sich selbst und fügt sich dem Inventar zu. Dabei setzt es die Energie auf 300 (in diesem Fall ist "Energie" die Zeit, die das Extra noch anhält). Zum Schluß wird noch eine WAVE-Datei abgespielt. Das ID_DRAW-Event ist hier fast genauso wie bei den Schlüsseln, dem Animator und den Türen. Daher dazu keine weiteren Ausführungen.

Case NR_ITEM_COOL:  ' Speed-PowerUp 
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN Then 
        OBJ_ITEM_DROP Other, NR_ITEM_COOL
        OBJ_ITEM_DROP Other, NR_ITEM_INVISIBLE
        OBJ_ITEM_DROP Other, NR_ITEM_TERMINATOR
        OBJ_ITEM_GET Other, NR_ITEM_COOL, 300
        OBJ_CLEAR Self
        LEVEL_SCORE = LEVEL_SCORE + 20
        WAV_PLAYBACK Pfad$ + "ITEM.WAV"
      End If 
    Case ID_DRAW
      OBJ_ANIMATE Self.R1, 3, Self.R2, 3
      Self.FRAME = Spr(ID_ITEM_COOL + Self.R2)
    End Select

Listing 97: Die Sub "USER_EVENT_ITEMS"

Die meisten anderen PowerUps sehen ähnlich aus. Daher liste ich sie nur noch der Vollständigkeit halber auf und beschreibe als nächstes nur den Totenkopf, da er eine andere Auswirkung hat.

Case NR_ITEM_INVISIBLE:  ' Unsichtbar-PowerUp 
  Select Case MESSAGE
  
    Case ID_COLL
      If Other.T = NR_PACMAN Then 
        OBJ_ITEM_DROP Other, NR_ITEM_COOL
        OBJ_ITEM_DROP Other, NR_ITEM_INVISIBLE
        OBJ_ITEM_DROP Other, NR_ITEM_TERMINATOR
        OBJ_ITEM_GET Other, NR_ITEM_INVISIBLE, 300
           
        OBJ_CLEAR Self
        LEVEL_SCORE = LEVEL_SCORE + 10
        WAV_PLAYBACK Pfad$ + "ITEM.WAV"
      End If 
    Case ID_DRAW
      OBJ_ANIMATE Self.R1, 3, Self.R2, 3
      Self.FRAME = Spr(ID_ITEM_INVISIBLE + Self.R2)
    End Select

 Case NR_ITEM_TERMINATOR:  ' Terminator-PowerUp 
   Select Case MESSAGE
     Case ID_COLL
     
       If Other.T = NR_PACMAN Then 
         OBJ_ITEM_DROP Other, NR_ITEM_COOL
         OBJ_ITEM_DROP Other, NR_ITEM_INVISIBLE
         OBJ_ITEM_DROP Other, NR_ITEM_TERMINATOR
         OBJ_ITEM_GET Other, NR_ITEM_TERMINATOR, 300
         OBJ_CLEAR Self
         LEVEL_SCORE = LEVEL_SCORE + 10
         WAV_PLAYBACK Pfad$ + "ITEM.WAV"
       End If 
      
     Case ID_DRAW
       OBJ_ANIMATE Self.R1, 3, Self.R2, 3
       Self.FRAME = Spr(ID_ITEM_TERMINATOR + Self.R2)
     End Select

Listing 98: Die Sub "USER_EVENT_ITEMS"

Der Totenkopf hat etwas andere Auswirkungen: Er zieht PACMAN 80 Energiepunkte ab, was mehr als die Hälfte ist. Sollte der Spieler also zwei dieser Kollegen berühren, stattet er den ewigen Jagdgründen einen Besuch ab. Natürlich wird auch dieses Objekt gelöscht und ein Ton ausgegeben.

 Case NR_ITEM_DEAD:  ' Todes-"PowerUp" 
  Select Case MESSAGE

  Case ID_COLL
    If Other.T = NR_PACMAN Then 
      Other.E = Other.E - 80
      OBJ_CLEAR Self
      WAV_PLAYBACK Pfad$ + "ITEM.WAV"
    End If 
    
 Case ID_DRAW
   OBJ_ANIMATE Self.R1, 3, Self.R2, 3
   Self.FRAME = Spr(ID_ITEM_DEAD + Self.R2)
 End Select

Listing 99: Die Sub "USER_EVENT_ITEMS"

4.9.4a MONSTER  

Sub USER_EVENT_MONSTER (Self As ObjType, MESSAGE, Other As_
                        ObjType, Par1, Par2)
  Select Case Self.T

Listing 100: Die Sub "USER_EVENT_MONSTER"

Die Steuerung der Monster ist nicht so leicht wie die von Pacman, da wir hier jetzt eine Art KI, eine künstliche Intelligenz, programmieren müssen - wenn die KI nicht gut ist könnte man auch von einer "krankhaften Intelligenz" reden, Daher müssen wir hier einiges tun, damit die Monster nicht immer gegen die Wände laufen oder gar auf Artgenossen losgehen.

Case NR_MONSTER_BLUE
  
  Select Case MESSAGE
    Case ID_DRAW
      MONSTER_DRAW Self, ID_MONSTER_BLUE
    Case ID_MOVE
      MONSTER_MOVE Self, Par1, Par2, 2
      MONSTER_HUNT Self, 20, 2
    Case ID_COLL, ID_COLL_WALL
      MONSTER_COLL Self, Other, 2, 2, ID_MONSTER_BLUE
  End Select

Listing 101: Die Sub "USER_EVENT_MONSTER"

Wie Sie sehen, besteht die Steuerung eines Monsters nur aus dem Aufrufen von Subprozeduren, denen von Monster zu Monster andere Werte übergeben werden. Ich werde nun kurz die Syntax dieser Funktionen ansprechen und mit der Monster-Prozedur (wie schön zweideutig) fortfahren.

MONSTER_COLL <Objekt>, <Anderes>, <Stärke>, <Speed>, <Frame>
MONSTER_DRAW <Objekt>, <Frame>
MONSTER_HUNT <Objekt>, <Sichtweite>, <Speed>
MONSTER_MOVE <Objekt>, <X>, <Y>, <Speed>

<Objekt> steht für das eigene Objek, daher hier Self eintragen.
<Anderes> steht für das andere Objekt, also Other eintragen.
<Stärke> ist die Schlagkraft gegenüber PACMAN
<Speed> ist die normale Geschwindigkeit
<Sichtw.> wie viele Blöcke weit das Monster "sehen" kann
<Frame> ist das Anfangsbild als NR_xxxxxxx

Nun kommen noch die anderen Monster-Typen, die sich vom ersten nur durch andere Zahlen für Geschwindigkeit, Sichtweite und Stärke unterscheiden. Außerdem ist natürlich noch der Grund-Frame anders. Aber von Prinzip her gleich, was soviel bedeutet wie: kein Kommentar ;-)

Case NR_MONSTER_RED
 Select Case MESSAGE
   Case ID_DRAW
     MONSTER_DRAW Self, ID_MONSTER_RED
   Case ID_MOVE
     MONSTER_MOVE Self, Par1, Par2, 3
     MONSTER_HUNT Self, 40, 3
   Case ID_COLL, ID_COLL_WALL
     MONSTER_COLL Self, Other, 4, 3, ID_MONSTER_RED
   End Select
   
Case NR_MONSTER_BROWN
  Select Case MESSAGE
    Case ID_DRAW
      MONSTER_DRAW Self, ID_MONSTER_BROWN
    Case ID_MOVE
      MONSTER_MOVE Self, Par1, Par2, 3
      MONSTER_HUNT Self, 80, 3
    Case ID_COLL, ID_COLL_WALL
      MONSTER_COLL Self, Other, 3, 3, ID_MONSTER_BROWN
   End Select
   
Case NR_MONSTER_MAGENTA
  Select Case MESSAGE
    Case ID_DRAW
      MONSTER_DRAW Self, ID_MONSTER_MAGENTA
    Case ID_MOVE
      MONSTER_MOVE Self, Par1, Par2, 3
      MONSTER_HUNT Self, 80, 5
    Case ID_COLL, ID_COLL_WALL
      MONSTER_COLL Self, Other, 8, 3, ID_MONSTER_MAGENTA
   End Select
    
Case NR_MONSTER_HIPPIE
  Select Case MESSAGE
    Case ID_DRAW
      MONSTER_DRAW Self, ID_MONSTER_HIPPIE
    Case ID_MOVE
      MONSTER_MOVE Self, Par1, Par2, 3
      MONSTER_HUNT Self, 100, 6
   Case ID_COLL, ID_COLL_WALL
      MONSTER_COLL Self, Other, 20, 4, ID_MONSTER_HIPPIE
   End Select

Listing 102: Die Sub "USER_EVENT_MONSTER"

4.9.4b Monster Innereien  

Fangen wir nun mit MONSTER_COLL an. Diese Routine wird immer dann aufgerufen, wenn eine Kollision eines Monsters mit PACMAN vorliegt. Hierbei wird geprüft, ob PACMAN gerade ein "Terminator" ist und das Monster trifft, oder ob das Monster ihm was antut. Der Prozedur werden beide Objekte, die Geschwindigkeitund die Schlagkraft des Monsters übergeben.

Sub MONSTER_COLL(Self As ObjType,Other As _
                 ObjType,Hit,Speed,FRAME)

Listing 103: Die Sub "MONSTER_COLL"

Als erstes wird geprüft, ob es sich um PACMAN handelt. In diesem Fall wird nachgefragt, ob er das Terminator-Item bei sich hat. Wenn ja bekommt das Monster 10 Energiepunkte abgezogen und eine Sounddatei wird ausgegeben. Stirbt das Monster dabei wird es in einen Animator umgewandelt, der die Sprites FRAME+8 bis FRAME+8+3 abspielt. Außerdem bekommt PACMAN Punkte gutgeschrieben die sich aus der doppelten Schlagkraft des Monsters ergeben. Ist PACMAN jedoch im Normalmodus bekommt er die Schlagkraft des Monsters von der Energie abgezogen.

If Other.T = NR_PACMAN Then 
  If OBJ_ITEM_HAVE(Other, NR_ITEM_TERMINATOR) Then 
    Self.E = Self.E - 10
    WAV_PLAYBACK Pfad$ + "DOT.WAV"
    If Self.E < 0 Then 
      Self.T = NR_ANIMATOR
      Self.R3 = FRAME + 8
      LEVEL_SCORE = LEVEL_SCORE + Hit * 2
      WAV_PLAYBACK Pfad$ + "MONSTER.WAV"
    End If 
  Else 
    Other.E = Other.E - Hit
  End If 
Else

Listing 104: Die Sub "MONSTER_COLL"

Handelt es sich bei dem anderen Objekt nicht um PACMAN, wird die Position unter Verwendung der Sicherungskoordinaten OX und OY zurückgesetzt, und die Prozedur MONSTER_RANDOM_SPEED wird aufgerufen. Diese Prozedur wird mehrfach benötigt und sorgt dafür, daß das Monster einen anderen Weg auslöst.

    Self.X = Self.OX
    Self.Y = Self.OY
    MONSTER_RANDOM_SPEED Self, Speed
  End If 
End Sub

Listing 105: Die Sub "MONSTER_COLL"

In MONSTER_RANDOM_SPEED bekommt das Monster zufällig eine neue Geschwindigkeit in eine Richtung. Zugegeben, dieses Verfahren ist sehr primitiv. Man hätte besser prüfen sollen, wohin das Monster ausweichen kann, aber dieses Verfahren funktioniert auch. Es wird hier einfach eine Zufallszahl zwischen 0 und 3 ausgesucht. Danach wird die Richtung entschieden. Die Geschwindigkeit entspricht der übergebenen, damit die verschiedenen Monstergattungen sich auch verschieden schnell bewegen.

Sub MONSTER_RANDOM_SPEED (Self As ObjType, S)
  A = Fix(Rnd * 4)
  Select Case A
    Case 0: Self.SX = -S: Self.SY = 0
    Case 1: Self.SX = 0: Self.SY = -S
    Case 2: Self.SX = S: Self.SY = 0
    Case 3: Self.SX = 0: Self.SY = S
  End Select
End Sub

Listing 106: Die Sub "MONSTER_COLL"

Die nächste Routine auf dem Plan heißt MONSTER_DRAW. Sie ist eigenltich schnell abgehandelt: Per OBJ_ANIMATE wird das Objekt animiert. Weiterhin wird nachgesehen, ob PACMAN das Terminator-Extra hat oder nicht. Wenn ja wird das Monster flüchtend dargestellt. Wenn nein, sieht es normal aus. Hier wird auch überprüft, ob die Geschwindigkeit aus irgendeinem Grund 0 beträgt. Da sich die Monster immer bewegen sollen (keine Angst, die brauchen schon keinen Schlaf) wird dann wieder MONSTER_RANDOM_SPEED aufgerufen.

Sub MONSTER_DRAW (Self As ObjType, Default)
  OBJ_ANIMATE Self.R1, 3, Self.R2, 3
  If OBJ_ITEM_HAVE(Obj(0), NR_ITEM_TERMINATOR) Then 
    Self.FRAME = Spr(Default + Self.R2 + 4)
  Else 
    Self.FRAME = Spr(Default + Self.R2)
  End If 
  If Self.SX = 0 And Self.SY = 0 Then 
    MONSTER_RANDOM_SPEED Self, 2
  End If 
End Sub

Listing 107: Die Sub "MONSTER_COLL"

Nun kommen wir zur eigentlichen Monster-KI. MONSTER_HUNT wird immer dann aufgerufen, wenn ein Monster sich bewegt. Es bekommt die Sichtweite und Geschwindigkeit übergeben und prüft, ob sich PACMAN in Reichweite befindet. (Bei dem folgend Listing sind die Einrückungsabstände kleiner als sonst, damit keine Zeile breiter als die Seite ist. Schließlich ist die Leserlichkeit hier wichtiger als der Ordnungssinn, oder?)

Sub MONSTER_HUNT (Self As ObjType, View, Speed)

Listing 108: Die Sub "MONSTER_HUNT"

Gleich am Anfang wird geprüft, ob das Objekt 0, als unser Kollege PACMAN sichtbar ist. Wenn nicht, hat er das Unsichtbar-Extra und darf nicht gejagt werden, daher EXIT SUB.

If Obj(0).V = 0 Then Exit Sub

Listing 109: Die Sub "MONSTER_HUNT"

Und nun schauen wir nach, ob PACMAN sowohl in X- als auch in Y-Richtung innerhalb des Sichtfeldes ist. Wenn nicht, können wir uns nämlich die Arbeit sparen. Es ist übrigens wichtig, solche Fälle vorher abzucheken, um Rechenzeit zu sparen.

If Abs(Obj(0).X - Self.X) < View Then 
  If Abs(Obj(0).Y - Self.Y) < View Then

Listing 110: Die Sub "MONSTER_HUNT"

Nun setzen wir die Monstergeschwindigkeit zurück. Wir errechnen die Position von PACMAN und dem Monster, wobei wir die Koordinaten jeweils durch 2 teilen. Dies hat folgenden Hintergedanken: Solange die X-Koordinate von PACMAN kleiner als die des Monsters ist läuft das Monster nach links. Aber es kann ja sein, daß das Monster so schnell ist, daß es PACMAN nicht genau trifft und zu weit rennt! In dem Fall würde es normal wieder zurücklaufen. Das Monster würde sich ständig hin und her bewegen. Das können wir so umgehen, indem wir nicht in Pixel rechnen sondern in Schritten.

Self.SX = 0
Self.SY = 0

PacX = Fix(Obj(0).X / Speed)
PacY = Fix(Obj(0).Y / Speed)
MyX = Fix(Self.X / Speed)
MyY = Fix(Self.Y / Speed)

Listing 111: Die Sub "MONSTER_HUNT"

Die eigentliche KI besteht darin, zu prüfen ob PACMAN für das Monster gefährlich ist oder umgekehrt, um dann eine entsprechende Bewegung einzuleiten. Wenn PACMAN harmlos ist wird geprüft, in welcher Richtung er sich befindet UND ob man dorthin gehen kann. Beim anderen Fall wird geprüft, ob man von PACMAN weggehen kann.

If OBJ_ITEM_HAVE(Obj(0), NR_ITEM_TERMINATOR) = 0 Then 
  If PacX < MyX And OBJ_LEFT(Self, 20) Then Self.SX = -Speed
  If PacX > MyX And OBJ_RIGHT(Self, 20) Then Self.SX = Speed
  If PacY < MyY And OBJ_UP(Self, 20) Then Self.SY = -Speed
  If PacY > MyY And OBJ_DOWN(Self, 20) Then Self.SY = Speed
Else 
  If PacX < MyX And OBJ_RIGHT(Self, 20) Then Self.SX = Speed
  If PacX > MyX And OBJ_LEFT(Self, 20) Then Self.SX = -Speed
  If PacY < MyY And OBJ_DOWN(Self, 20) Then Self.SY = Speed
  If PacY > MyY And OBJ_UP(Self, 20) Then Self.SY = -Speed
End If

Listing 112: Die Sub "MONSTER_HUNT"

Nach diesem Abschnitt wird noch getestet, ob kein Fall zutrifft. Dann wird nämlich wieder die bekannte Routine MONSTER_RANDOM_SPEED aufgerufen.

      If Self.SX = 0 And Self.SY = 0 Then 
        MONSTER_RANDOM_SPEED Self, Speed
      End If 
    End If 
  End If 
End Sub

Listing 113: Die Sub "MONSTER_HUNT"

Und nun zur letzten MONSTER-Routine: MONSTER_MOVE wird dann aufgerufen, wenn sich das Monster bewegen soll. Hier wird etwas mehr getan als bei MONSTER_RANDOM_MOVE.

Sub MONSTER_MOVE (Self As ObjType, Par1, Par2, Speed)

Listing 114: Die Sub "MONSTER_MOVE"

Als erstes sichern wir die Koordinaten. Dann bewegen wir das Monster weiter und prüfen auf Kollisionen mit dem Spielfeld.

Self.OX = Self.X
Self.OY = Self.Y
Self.X = Self.X + Par1
Self.Y = Self.Y + Par2
OBJ_COLL_FIELD Self

Listing 115: Die Sub "MONSTER_MOVE"

Jetzt zählen wir, zu wie vielen Seiten das Monster gehen kann. Sind beispielsweise die Felder links und rechts frei, so haben wir zwei Wege zur Auswahl.

Ways = 0
If OBJ_LEFT(Self, 20) Then Ways = Ways + 1
If OBJ_RIGHT(Self, 20) Then Ways = Ways + 1
If OBJ_UP(Self, 20) Then Ways = Ways + 1
If OBJ_DOWN(Self, 20) Then Ways = Ways + 1

Listing 116: Die Sub "MONSTER_MOVE"

Normal ist die Chance, daß sich das Monster in einem Gang (also zwei freie Felder anliegend) nur 2 zu 200. Haben wir jedoch mehr Wege zur Wahl (z.B. an einer Kreuzung oder auf offenem Feld), so ist die Chance 20 zu 200. Das hört sich wenig an, jedoch müssen Sie bedenken, daß das MOVE-Event sehr oft aufgerufen wird, während das Monster eine Kreuzung passiert.

Chance = 2
If Ways > 2 Then Chance = 20

Listing 117: Die Sub "MONSTER_MOVE"

Sollte die Chance gekommen sein, sprich der Zufallswert zwischen 0 und 199 (RND*200) ist kleiner als die Chance, so wird wieder MONSTER_RANDOM_SPEED benutzt, um einen anderen oder evtl. auch den gleichen Weg einzuschlagen.

  If Rnd * 200 < Chance Then 
    MONSTER_RANDOM_SPEED Self, Speed
  End If 
End Sub

Listing 118: Die Sub "MONSTER_MOVE"

Der Vorteil dieser starken Unterteilung liegt auf der Hand: Wollen Sie mal ein neues Monster hinzufügen brauchen Sie nur ein, zwei Werte ändern und sind fertig.

4.9.5 Hebeleien  

Nun sind wir endlich wieder bei den eigentlichen EVENT-Routinen. Diesmal besprechen wir USER_EVENT_SWITCH. Diese Prozedur steuert die Schalter und entsprechenden Blöcke auf dem Spielfeld.

Sub USER_EVENT_SWITCH (Self As ObjType, MESSAGE, Other As _
                       ObjType, Par1, Par2)
  
  Select Case Self.T

Listing 119: Die Sub "USER_EVENT_SWITCH"

Wir besprechen jetzt wieder exemplarisch einen Schalter.

Case NR_SWITCH_RED

Select Case MESSAGE

Listing 120: Die Sub "USER_EVENT_SWITCH"

Bei der Kollision mit PACMAN wird der Schalter umgelegt und das nur, wenn der Schalter sich in Ausgangsstellung (R1=0) befindet. Es werden dann alle Objekte durchsucht. Wenn ein zu dem Schalter passender Block gefunden wurde wird dieser gelöscht und so der Weg freigegeben. Zum Abschluß kommt noch eine WAVE-Datei.

Case ID_COLL
  If Other.T = NR_PACMAN And Self.R1 = 0 Then 
    Self.R1 = 1
    For M = 0 To UBound(Obj)
      If Obj(M).T=NR_BLOCK_RED Then OBJ_CLEAR Obj(M)
    Next M
    WAV_PLAYBACK Pfad$ + "SCHALTER.WAV"
  End If

Listing 121: Die Sub "USER_EVENT_SWITCH"

Beim DRAW-Ereignis wird der Grundframe genommen und R1 hinzuaddiert. Dadurch hat der Schalter in der Ausgangsstellung eine andere Sprite-Nummer als wenn er umgelegt ist.

Case ID_DRAW
  Self.FRAME = Spr(ID_SWITCH_RED + Self.R1)
End Select

Listing 122: Die Sub "USER_EVENT_SWITCH"

Genauso sieht's bei den anderen Schaltern aus.

Case NR_SWITCH_GREEN
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        Self.R1 = 1
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_BLOCK_GREEN Then OBJ_CLEAR Obj(M)
        Next M
        WAV_PLAYBACK Pfad$ + "SCHALTER.WAV"
      End If 
    Case ID_DRAW
      Self.FRAME = Spr(ID_SWITCH_GREEN + Self.R1)
  End Select
  
  
Case NR_SWITCH_BLUE
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        Self.R1 = 1
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_BLOCK_BLUE Then OBJ_CLEAR Obj(M)
        Next M
        WAV_PLAYBACK Pfad$ + "SCHALTER.WAV"
      End If 
    Case ID_DRAW
      Self.FRAME = Spr(ID_SWITCH_BLUE + Self.R1)
  End Select
 
 
Case NR_SWITCH_YELLOW
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        Self.R1 = 1
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_BLOCK_YELLOW Then OBJ_CLEAR Obj(M)
        Next M
        WAV_PLAYBACK Pfad$ + "SCHALTER.WAV"
      End If 
    Case ID_DRAW
      Self.FRAME = Spr(ID_SWITCH_YELLOW + Self.R1)
  End Select

Listing 123: Die Sub "USER_EVENT_SWITCH"

Die Schalter können wir zum Glück kurz und schmerzlos besprechen: Sie sind praktisch passiv und sorgen nur dafür, daß PACMAN zurückgesetzt wird, wenn er die Blöcke berührt. Dies wird, wer hätte es gedacht, mit Hilfe der Sicherungskoordinaten OX und OY gemacht. Außerdem wird PACMANs Geschwindigkeit auf 0 gesetzt.

Case NR_BLOCK_RED, NR_BLOCK_GREEN, NR_BLOCK_BLUE, NR_BLOCK_YELLOW
  Select Case MESSAGE
 
    Case ID_COLL
      If Other.T = NR_PACMAN Then 
        Other.X = Other.OX
        Other.Y = Other.OY
        Other.SX = 0
        Other.SY = 0
      End If 
 
  End Select

Listing 124: Die Sub "USER_EVENT_SWITCH"

Nun schließen wir die Prozedur ab.

  End Select
  
End Sub

Listing 125: Die Sub "USER_EVENT_SWITCH"

4.9.6 Teleportation  

Jetzt, wo schon die Teleportation in der Wissenschaft funktioniert hat (zumindest mit einem Lichtteilchen) liegt der folgende Teil gar nicht mal so sehr im Sience Fiction-bereich: Die Prozedur USER_EVENT_TELE verwaltet die Teleporter auf dem Schirm, mit denen der Spieler sich von einer Seite zur anderen "beamen" kann.

Sub USER_EVENT_TELE (Self As ObjType, MESSAGE, Other As ObjType,_
                     Par1, Par2)
  Select Case Self.T

Listing 126: Die Sub "USER_EVENT_TELE"

Die Teleporter reagieren, sobald PACMAN mit Ihnen kollidiert. Dann sucht der betreffende Teleporter sein Pendant. Dies wird dadurch erreicht, daß wir alle Objekte durchlaufen und prüfen, ob es sich um einen Teleporter der gleichen Farbe handelt, der an einer anderen Position steht. Letzteres ist sehr wichtig, damit wir nicht den gleichen Teleporter erwischen, bei dem die Reise begann.

Case NR_TELE_RED
  Select Case MESSAGE
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
      For M = 0 To UBound(Obj)
      If Obj(M).T = NR_TELE_RED And (Obj(M).X <> Self.X Or_
                                     Obj(M).Y <> Self.Y) Then

Listing 127: Die Sub "USER_EVENT_TELE"

Wurde einer gefunden wird PACMAN teleportiert, indem seine Koordinaten auf den anderen Teleporter gesetzt werden. Danach wird eine Wavedatei ausgegeben. Da er aber nicht darauf stehenbleiben kann wird geprüft, welche Felder es zum Ausweichen gibt. Wird eins gefunden so bewegt sich PACMAN dorthin, und die Routine wird beendet.

      Other.X = Obj(M).X
      Other.Y = Obj(M).Y
      WAV_PLAYBACK Pfad$ + "TELEPORT.WAV"
      If OBJ_LEFT(Obj(M),20)Then Other.X=Other.X-20:Exit Sub 
      If OBJ_RIGHT(Obj(M),20)Then Other.X=Other.X+20:Exit Sub 
      If OBJ_UP(Obj(M),20)Then Other.Y=Other.Y-20:Exit Sub 
      If OBJ_DOWN(Obj(M),20)Then Other.Y=Other.Y+20:Exit Sub 
    End If 
  Next M
End If

Listing 128: Die Sub "USER_EVENT_TELE"

Beim Zeichnen wird eigentlich nichts besonderes gemacht, alles wie gehabt.

Case ID_DRAW
  OBJ_ANIMATE Self.R1, 0, Self.R2, 3
  Self.FRAME = Spr(ID_TELE_RED + Self.R2)

End Select
          
Case NR_TELE_BLUE
  Select Case MESSAGE
 
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_TELE_BLUE And (Obj(M).X <> Self.X Or_
                                          Obj(M).Y <> Self.Y) Then 
            Other.X = Obj(M).X
            Other.Y = Obj(M).Y
            WAV_PLAYBACK Pfad$ + "TELEPORT.WAV"
            If OBJ_LEFT(Obj(M),20)Then Other.X=Other.X-20:Exit Sub 
            If OBJ_RIGHT(Obj(M),20)Then Other.X=Other.X+20:Exit Sub 
            If OBJ_UP(Obj(M),20)Then Other.Y=Other.Y-20:Exit Sub 
            If OBJ_DOWN(Obj(M),20)Then Other.Y=Other.Y+20:Exit Sub 
          End If 
        Next M
      End If 
      
    Case ID_DRAW
      OBJ_ANIMATE Self.R1, 0, Self.R2, 3
      Self.FRAME = Spr(ID_TELE_BLUE + Self.R2)
 
  End Select
 
Case NR_TELE_GREEN
  Select Case MESSAGE
 
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_TELE_GREEN And (Obj(M).X <> Self.X Or_
                                           Obj(M).Y <> Self.Y) Then 
            Other.X = Obj(M).X
            Other.Y = Obj(M).Y
            WAV_PLAYBACK Pfad$ + "TELEPORT.WAV"
            If OBJ_LEFT(Obj(M),20)Then Other.X=Other.X-20:Exit Sub 
            If OBJ_RIGHT(Obj(M),20)Then Other.X=Other.X+20:Exit Sub 
            If OBJ_UP(Obj(M),20)Then Other.Y=Other.Y-20:Exit Sub 
            If OBJ_DOWN(Obj(M),20)Then Other.Y=Other.Y+20:Exit Sub 
          End If 
        Next M
      End If 
 
    Case ID_DRAW
      OBJ_ANIMATE Self.R1, 0, Self.R2, 3
      Self.FRAME = Spr(ID_TELE_GREEN + Self.R2)
 
  End Select
 
 
Case NR_TELE_YELLOW
  Select Case MESSAGE
 
    Case ID_COLL
      If Other.T = NR_PACMAN And Self.R1 = 0 Then 
        For M = 0 To UBound(Obj)
          If Obj(M).T = NR_TELE_YELLOW And (Obj(M).X <> Self.X Or_
                                            Obj(M).Y <> Self.Y) Then 
            Other.X = Obj(M).X
            Other.Y = Obj(M).Y
            WAV_PLAYBACK Pfad$ + "TELEPORT.WAV"
            If OBJ_LEFT(Obj(M),20)Then Other.X=Other.X-20:Exit Sub 
            If OBJ_RIGHT(Obj(M),20)Then Other.X=Other.X+20:Exit Sub 
            If OBJ_UP(Obj(M),20)Then Other.Y=Other.Y-20:Exit Sub 
            If OBJ_DOWN(Obj(M),20)Then Other.Y=Other.Y+20:Exit Sub 
          End If 
        Next M
      End If 
 
    Case ID_DRAW
      OBJ_ANIMATE Self.R1, 0, Self.R2, 3
      Self.FRAME = Spr(ID_TELE_YELLOW + Self.R2)
    
    End Select

Listing 129: Die Sub "USER_EVENT_TELE"

4.10 Ready 2 go  

Geschafft! Das Spiel ist soweit einsatzbereit. Sollte es bei Ihnen aber trotz Sorgfalt beim Abtippen (oder Kopieren<g>) nicht laufen, können Sie den Quellcode aus dem letzten Teil benutzen. Ich wollte hier aus Speichergründen nicht noch einmal die ganzen Dateien anhängen.
Ich konnte auch nicht noch einmal testen, ob der hier dokumentierte Code komplett ist. Wenn also irgendwo etwas fehlt, bitte ein E-Mail an PCDVisual@AOL.COM.
Falls das Spiel richtig läuft, wünsche ich Ihnen erst einmal viel Spaß damit. Vielleicht haben Sie ja noch Lust es zu erweitern oder zu ändern - es steht Ihnen frei.
Kleiner Hinweis: Wenn ich in diesem Kurs von PACMAN rede meine ich natürlich VB-Kurs PACMAN ;-)

4.11 Ende Teil 4  

Damit wäre das PACMAN-Spiel komplett (endlich, endlich, endlich) abgeschlossen. Wie es weitergeht steht bis jetzt noch nicht fest, aber Sie können mir gerne Ihre Meinung sagen bzw. schreiben. Wie wäre es mit einem Kurs zu einem 3D-Spiel? (Diesmal aber nicht ganz so umfangreich, wie dieses Spiel hier). Oder vielleicht ein Kurs zur API oder ein weiterer Teil Grafik mit Dateiformaten?
Wenn Sie weitere Themenwünsche oder Ideen für den nächsten Aufbaukurs haben, schreiben Sie mir doch einfach ein E-Mail. Meine Adresse: scherlebeck@econnsoft.com
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.

Artikel (Worddokument) als Download [239 KB] [239000 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.