Der große VB Spielekurs Teil 4
von Dominik Scherlebeck
Ü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) scherlebeck@econnsoft.com
Inhalt
-
4.1 Pacman BAS
-
4.2 Pacman in Form
-
4.3 Weitere Formalitäten
-
4.4 Hilfe für Spieler
-
4.5 Für die besten Spieler [Highscore-Liste]
-
4.6 Pacmans Innenleben
-
4.7 Pacmans Steuerpult
-
4.8 Auf den Bildschirm
-
4.9 Weltbewegende Ereignisse
-
4.9.1 Pacmans Ereignisse
-
4.9.2 Schlüsselerlebnisse
-
4.9.3 Hier gibts was extra
-
4.9.4a MONSTER
-
4.9.4b Monster Innereien
-
4.9.5 Hebeleien
-
4.9.6 Teleportation
-
4.10 Ready 2 go
-
4.11 Ende Teil 4
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.
Caption | Name (Shortcut) |
---|---|
&Spiel | M_S_TITLE |
&Neues Spiel starten | M_S_NEU (Strg+N) |
- | M_S_SEP1 |
&Beenden | M_S_BEENDEN (Strg+B) |
&Info | M_I_TITLE |
&Highscore | M_I_HIGHSCORE (Strg+H) |
&Hilfe zum Spiel | M_I_HILFE (F1) |
- | M_I_SEP1 |
&Info über | M_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.
Was | Zeichen | Objekt |
---|---|---|
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 & Items | P | Pacman |
Pacman & Items | C | Cool-Pacman (Zeit bleibt stehen) |
Pacman & Items | I | Unsichtbarkeit |
Pacman & Items | T | Terminator-Pacman (kann Monster fressen) |
Pacman & Items | X | Totenkopf (zieht Pacman Energie ab) |
Pacman & Items | . | Punkt |
Pacman & Items | F | Flagge |
Monster | 1 | Blau |
Monster | 2 | Grün |
Monster | 3 | Rot |
Monster | 4 | Braun |
Monster | 5 | Lila |
Monster | 6 | Hippie (das soll niemanden diskriminieren, aber mir fiel kein anderer Name ein!) |
Schlüssel | k | Schlüssel Typ 1 |
Schlüssel | K | Schlüssel Typ 2 |
Schlüssel | d | Tür für Schlüssel 1 |
Schlüssel | D | Tür für Schlüssel 2 |
Schalter | R | Roter Schalter |
Schalter | G | Grüner Schalter |
Schalter | B | Blauer Schalter |
Schalter | Y | Gelber Schalter |
Schalter | r | Rote Block |
Schalter | g | Grüner Block |
Schalter | b | Blauer Block |
Schalter | y | Gelber Block |
Teleporter | 7 | Roter Teleporter |
Teleporter | 8 | Grüner Teleporter |
Teleporter | 9 | Blauer Teleporter |
Teleporter | 0 | Gelber 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.