Die Community zu .NET und Classic VB.
Menü

Der große VB-Spiele Kurs, Teil 1

 von 

Übersicht 

Ich möchte mit diesem Kurs zeigen, daß es entgegen anders lautenden Gerüchten sehr gut möglich ist, in Visual Basic Spiele zu programmieren, die durchaus erstaunliche Ablauf-Geschwindigkeiten erreichen.
Das vorliegnde Spielekurs umfaßt ingesamt 125 DIN A4-Seiten. Auf Grund dieser Größe wird es hier in 4 Teilen veröffentlicht. Der erste berichtet über die grundlegenden Techniken, wie Sprites, Sounds Tastaturabfrage etc.
In diesem Kursabschnitt wird ein kompletter Shooter erläutert und entwickelt. Dabei werden sowohl die mittlerweile fast nur noch vertretenden 32-Bit- aber auch noch 16-Bit-Umgebungen berücksichtigt. Ich wünsche also viel Vergnügen.

Mit freundlichen Grüßen
Dominik Scherlebeck (PCDVisual)

Anmerkung der Redaktion: Dieser Spielekurs bezieht sich auf die Programmiersprache Visual Basic in der Version 6.0 und früher und ist nicht ohne Weiteres nach VB.net, bzw. VB 2005 und später übertragbar.

1.1 Zu diesem Kurs  

Hallo! Trotz einigen bedauerlichen Verzögerungen liegt Ihnen nun endlich der Spiele-Kurs vor (ich entschuldige mich bei allen, die ich immer und immer wieder "versetzt" habe!). Nachdem in den vorherigen Kursteilen schon über Grafik gesprochen wurde, werden wir jetzt einen Schritt weiter gehen und eigene Spiele auf VisualBasic programmieren. Daß dies auch ohne Assembler oder C++ möglich ist, soll dieser Kurs zeigen.

In diesem Teil werden wir uns mit dem Genre der Actionspiele beschäftigen. Das ist natürlich ein weites Feld. Hier soll es jedoch um ein Weltraumspiel gehen, indem der Spieler ein Raumschiff über den Bildschirm steuert, Asteroiden ausweicht und Gegner abschießt. Das Geschehen wird dabei von der Seite betrachtet. Das Level scrollt dabei von rechts nach links bis das Ende erreicht ist. Das waren auch schon die wesentlichen Grundelemente eines solchen Spiels. Dieses Spielprinzip kann man natürlich mit Power-Ups (also Extras, die der Spieler aufnehmen kann) leicht erweitern. Aber in diesem Kurs werden wir uns auf eine einfache Version eines solchen Spiels beschränken.

Sie finden übrigens alle Dateien, die für diesen Kurs benötigt werden, in der gepackten Datei, die Sie downgeloadet haben. Wenn möglich sollten Sie aber das Programm selbst erarbeiten, da so das vermittelte Wissen besser sitzt. Übrigens: Auch wenn Sie ein Sidescrolling-Spiel nicht sonderlich interessiert, so ist der Inhalt der ersten Seiten doch sehr wichtig, da er die Grundlagen von vielen Spielen beschreibt: Die Sprites.

In dem nächsten Teil werden wir dann ein Pacman-Spiel erarbeiten und eine bessere Grafik-Engine schreiben. In Teil 3 folgt dann ein Jump'N'Run-Spiel und zu guter Letzt werden wir in Teil 4 dreidimensionale Spiele programmieren. Dazu wird eine Erweiterungsdatei für VB mitgeliefert, die den Zugriff auf die WinG-DLL erlaubt und beispielsweise auch Texturemapping unterstützt.

Diese Themen sind natürlich bisher nur geplant. Wenn Sie andere Vorschläge haben, so schreiben Sie mir doch bitte ein E-Mail. Aber nun viel Spaß mit diesem Kursteil.

1.2 Die nötigen Ressourcen  

Bevor man ein Spiel programmiert muß man sich einiges Überlegen. Wo speichere ich die Grafiken? Womit erstelle ich überhaupt die Grafiken? Wie bekomme ich die Sachen auf den Bildschirm? Was für ein Spielkonzept soll ich benutzen? Wir fangen gleich mit den ersten beiden Punkten an:
Die Grafiken für das Spiel sollte am besten 256-Farben-Grafiken sein. Das hat nichts mit irgendwelchen Beschränkungen oder ähnlichen Sachen zu tun. Es geht dabei viel mehr um den Speicherplatz. Eine 8-Bit Grafik (also 256 Farben) ist kleiner und kann außerdem schneller auf dem Bildschirm gezeichnet werden als eine 24-Bit (TrueColor) Grafik. Die Grafiken lassen sich am besten mit einem guten Malprogramm anfertigen. Wenn Ihr Spiel auch im 256-Farben-Modus von Windows laufen soll, so müssen (!!) die Grafiken die gleiche Palette haben. Und noch ein wichtiger Punkt in Bezug auf Grafiken: Sprites müssen Sie doppelt zeichnen! Nachdem Sie den eigentlichen Sprite haben müssen Sie noch die sogenannte Maske zeichnen! Das ist wichtig, damit Sprites transparent dargestellt werden können. Wenn Sie den eigentlichen Sprite zeichnen, so steht dort die Farbe Schwarz für transparent. In der Maske steht Weiß für transparent und Schwarz für undurchsichtig.

Am leichtesten ist es, wenn Sie erst den richtigen Sprite zeichnen, diesen speichern und dann die transparenten Stellen mit Weiß und den Rest mit Schwarz übermalen. Um Ihnen hier die Arbeit etwas zu erleichtern finden Sie zusätzlich zu diesem Kurs das Programm "BMP2SPR.EXE" in dem Archiv (\UTIL). Dieses Programm kann ein beliebiges BMP-Bild laden und dazu eine Maske erstellen. Klicken Sie nach dem Laden des Bildes einfach auf "Umwandeln" und "Speichern". Auf diese Weise erstellen Sie die nötigen Sprites für das Spiel erstellen.


Abbildung 1: Masken für Sprites

Wichtig ist ebenfalls die Animation. Es ist natürlich recht langweilig, wenn der Held beim Laufen nicht die Füße bewegt oder das Raumschiff still daher schwebt. Daher erstellt man mehrere "Frames", also mehrere Einzelbilder. Sie brauchen jetzt natürlich nicht für hunderte von Bewegungen Sprites zeichnen. In dem Spiel, daß wir hier erarbeiten, werden nur sehr wenig Animationsphasen benötigt. Außerdem befinden sich die hier benötigten Bilder und Hintergründe, genauso wie das fertige Spiel im Archiv dieses Kurses.

  • VB16 Hier gibt's die 16-Bit Version des Spiels (VB 3.0 / VB 4.0 - 16 Bit)
  • VB32 In diesem Verzeichnis ist die 32-Bit-Version (VB 4.0 - 32 Bit / VB 5.0/ VB 6.0)
  • DEMO16 Und hier ist das erste Demoprogramm mit Source
  • DEMO32 Und natürlich auch wieder für 32-Bit

Auch ein Hintergrund darf nicht fehlen. Eine einfarbige Fläche ist nämlich sehr langweilig.
Gute Grafik alleine reicht aber für ein Spiel nicht aus. Auch der Sound und Musik dürfen nicht zu knapp kommen. Wenn Sie keine musikalische Ader haben macht das erst einmal nichts. Oft gibt es frei benutzbare Musikstücke oder Sounddateien im Internet oder in den Archiven von AOL. Im Zweifelsfall sollten Sie die Autoren dieser Dateien per E-Mail um Erlaubnis fragen.
Die Musikstücke für ein Spiel sollten immer zum Level passen. Wenn das Level düster aufgemacht ist, so sollte auch die Musik entsprechend "düster" sein. Die Musikdateien sollten am besten im MIDI-Format vorliegen. Dies hat den Vorteil, daß die Dateien sehr klein bleiben, sowohl auf der Festplatte als auch im Arbeitsspeicher.
Die Sounddateien sollten im WAVE-Format sein. Das hat den Vorteil einer guten Qualität und einfacher Handhabung.
Die zusammengestellten Dateien sollten Sie in das Verzeichnis des Spiels kopieren, daß Sie erstellen wollen. Sie müssen übrigens immer gut auf die Pfade achten! Verwenden Sie in Ihrem Spiel keine Angaben wie "C:\MYGAME\1.BMP". Es ist nämlich unwahrscheinlich, daß der spätere Benutzer diese Dateien auch dort plazieren wird. Viel eleganter ist die Möglichkeit, eine Pfadvariable einzuführen. Deklarieren Sie z.B. eine Variable mit dem Namen Pfad$ als globale Variable (wie das alles geht wird später noch ausführlich beschrieben). Diese Variable bekommt vorläufig den Pfad auf Ihrer Festplatte zugewiesen. Zum Beispiel: "C:\MYGAME\". Im Programm verwenden Sie dann Angaben wie Pfad$+"1.BMP". Wenn das Spiel dann fertig ist brauchen Sie nur der Pfadvariable keinen Wert mehr zuweisen - also " " - und schon sind alle Pfadangaben relativ und das Spiel kann in einem beliebigen Verzeichnis installiert werden.

1.3 Reine Formsache  

Um Sie nun etwas mit Sprites und Hintergründen vertraut zu machen, werden wir nun ein erstes Programm schreiben, daß einen Sprite auf einem Hintergrund anzeigt. Dazu brauchen wir eine Form mit vier Bildfeldern. Ein Bildfeld nimmt den Hintergrund auf, zwei für den Sprite (Bitmap und Maske) und das letzte Bildfeld dient zur Ausgabe auf dem Bildschirm.
Oft wird für Spiele ein Zwischenpuffer eingesetzt. Das bedeutet, daß Bild wird auf einem Unsichtbaren Bildfeld aufgebaut und dieses Bildfeld wird anschließend auf ein sichtbares Bildfeld kopiert. Diese Technik müssen wir allerdings nicht anwenden, wenn wir bei dem sichtbaren Bildfeld die AutoRedraw-Eigenschaft auf "True" setzen. Dadurch wirken sich alle Funktionen erst einmal auf den Speicher aus. Das Bildfeld kann dann anschließend mit der Refresh-Methode aktualisiert werden.
Bei allen anderen Bildfelder muß (!) AutoRedraw ebenfalls auf True stehen. Die Hintergründe sind hier unwichtig. Daher werde ich auf genauere Ausführungen verzichten.
Erstellen Sie nun die folgende Form. Die Werte für die Picture-Eigenschaft in dem Diagramm geben die Datei an, die Sie in dieses Bildfeld laden sollen. Die Dateien finden Sie im Verzeichnis \DEMO1 dieses Archivs. Dort finden Sie auch das komplette Programm, wenn Sie keine Lust auf einen "Eigenbau" haben.


Abbildung 2: Formsache

Nachdem Sie nun die Elemente plaziert haben machen Sie noch alle Bildfelder außer "DISPLAY" unsichtbar, indem Sie die Visible-Eigenschaften auf False setzen. Anschließend adjustieren Sie noch die Größe der Form, damit im laufenden Programm nicht so viel leerer Raum zu sehen ist.
Wie Sie vielleicht gemerkt haben, steht die AutoSize-Eigenschaft bei den Bildfeldern mit den Ressourcen auf True. Das erleichtert Ihnen die Arbeit, da so die Größe der Objekte automatisch an die Größe der Bitmap angepaßt wird.
Da die Form nun fertig ist wenden wir uns der eigentlichen Programmierarbeit zu. Um mit Sprites zu arbeiten müssen wir API-Funktionen einsetzen. Wenn Ihnen dieser Begriff nichts sagt oder Sie sich nicht mit den Funktionen BitBlt und StretchBlt auskennen, so lesen Sie sich bitte dazu den VB-Aufbaukurs Grafik 1 & 2 durch.

1.4 Die "Engine"  

Das Wort Engine ist fast schon ein Modebegriff geworden. Wenn Sie mal eine PC-Spiele-Zeitschrift lesen, so finden Sie fast in jedem Bericht so etwas wie "Die neue 3D-Engine, die in diesem Spiel benutzt wird...". Engine heißt nichts weiter als Maschine oder Motor. Eine Engine ist in der Programmierung ein Bestandteil eines Programms, der sich um eine einzelne Aufgabe kümmert. Eine 3D-Engine führt zum Beispiel 3D-Berechnungen durch und eine Grafik-Engine sorgt für die Darstellung der Grafik auf dem Bildschirm. Solche Engines haben den Vorteil, daß man sie wiederverwenden kann. Ein populäres Beispiel ist die 3D-Engine von Ken Silverman. Diese Engine wird in verschiedenen Spielen angewendet.
Nun, so hoch wollen wir erst einmal nicht hinaus - aber die Arbeitsaufteilung ist auch für unsere Zwecke mehr als praktisch. Darum werden wir die Grafikfunktionen in einem eigenen Codemodul unterbringen. Ich werde hier nur den Quellcode zeigen. Eine genauerer Beschreibung finden Sie ebenfalls in dem Grafik-Aufbaukurs.
Wie Sie vielleicht noch wissen gibt es zwei unterschiedliche Versionen der API. Einmal ist da die 16-Bit API. Diese ist die Grundlage von Windows 3.1. Auf der anderen Seite gibt es die 32-Bit API von Windows 95 und Windows NT. An welche API Sie sich halten müssen, liegt an der VisualBasic-Version. Wenn Sie VB 3.0 oder VB 4.0 in der 16-Bit Version benutzen, verwenden Sie die 16-Bit API. Verwenden Sie VB 4.0 in der 32-Bit Version oder VB 5.0 etc., so verwenden Sie die 32-Bit API.
Legen Sie nun ein neues Codemodul an und schreiben Sie unter "Deklarationen" die entsprechenden Anweisungen:

Option Explicit

Public Declare Function BitBlt Lib "Gdi32" (ByVal _
       NachHdc As Long, ByVal x As Long, ByVal _
       Y As Long, ByVal w As Long, ByVal h As Long, ByVal _
       vonHdc As Long, ByVal vonX As Long, ByVal _
       vonY As Long, ByVal Modus As Long) As Long

Public Declare Function StretchBlt Lib "Gdi32" (ByVal _
       Y As NachHdc As Long, ByVal x As Long, ByVal _
       Long, ByVal w As Long, ByVal h As Long, ByVal _
       vonHdc As Long, ByVal vonX As Long, ByVal _
       vonY As Long, ByVal vonW As Long, ByVal _
       vonH As Long, ByVal Modus As Long) As Long

Public Const BIT_COPY = &HCC0020
Public Const BIT_AND = &H8800C6
Public Const BIT_INVERT = &H660046
Public Const BIT_BLACK = &H42&

Listing 1: Die Deklarationen

Beachten Sie, daß die Declare-Anweisungen in eine Zeile geschrieben werden müssen; sonst treten Fehler auf. Wenn das Programm beim Ausführen die Meldung "Falsche DLL-Aufrufkonventionen" ausgibt, haben Sie sich vertippt. Im Zweifelsfall benutzen Sie einfach die mitgelieferten Dateien.

1.5 Sprites in Action  

Nachdem Sie nun die Engine fertig haben, fangen wir mit dem Programm an. Da es ja nur ein Demo sein soll, brauchen wir nicht viel. Wenn der Benutzer die Maus über das Display-Bildfeld bewegt, soll an dieser Stelle der Sprite erscheinen.
Dazu benutzen wir die MouseMove-Eigenschaft des Display-Bildfelds. Jetzt ist nur noch die Frage, welche Anweisungen wir dort hinein schreiben. Da wir mit den API-Funktionen arbeiten und dessen Breiten- und Höhen-Angaben, ist es sinnvoll, wichtige Angaben gleich am Anfang in Variablen zu speichern. Zum Beispiel sichern wir die Breite (in Pixeln) des HGrund-Bildfelds. Dann brauchen wir später nicht immer schreiben HGrund.ScaleWidth usw.
Hier jetzt die erste Version der Ereignisprozedur:

Private Sub Display_MouseMove(Button As Integer, _
                      Shift As Integer, _
                      X As Single, _
                      Y As Single)
    
    Dim w As Long, h As Long
    
    w = Display.ScaleWidth
    h = Display.ScaleHeight
    Call BitBlt(Display.hDC, 0, 0, w, h, _
                HGrund.hDC, 0, 0, BIT_COPY)
    
    Display.Refresh
End Sub

Listing 2: Sprites in Action

Am Anfang werden Breite und Höhe vom Display und vom Hintergrund in Variablen gespeichert. Anschließend wird BitBlt benutzt, um den Hintergrund auf das Display zu bringen. Als Modus wird dabei BIT_COPY angewandt (oft wird diese Konstante SRCCOPY genannt, doch das hat auf das Ergebnis keine Auswirkungen). Zum Schluß wird ein Refresh durchgeführt, damit das Bildfeld den Inhalt sofort anzeigt. Wenn Sie das Programm starten und den Mauszeiger über das Bildfeld bewegen, so erscheint dort der Hintergrund. Jedoch fehlt noch der eigentliche Sprite.
Sie wissen schon, daß Sie für einen Sprite die Bitmap und eine Maske benötigen. Um nun einen Sprite auf einem Hintergrund anzuzeigen sind zwei BitBlt-Operationen notwendig:

1. Die Maske wird mit BIT_AND (bzw SRCAND) auf den Hintergrund kopiert
2. Die Bitmap wird mit BIT_INVERT (bzw SRCINVERT) darauf kopiert

Nach diesen Operationen ist der Sprite transparent auf dem Hintergrund zu sehen. Warum das funktioniert, will ich hier nicht beschreiben. Es ist für uns auch nicht wichtig. Dieses Verfahren ist übrigens allgemein gültig. Jetzt müssen wir noch die entsprechenden Codezeilen eingeben.

Private Sub Display_MouseMove(Button As Integer, _
                              Shift As Integer, _
                              X As Single, _
                              Y As Single)

  Dim w As Long, h As Long
  Dim sw As Long, sh As Long

    w = DISPLAY.ScaleWidth
    h = DISPLAY.ScaleHeight
    sw = BITMAP.ScaleWidth
    sh = BITMAP.ScaleHeight

    Call BitBlt(DISPLAY.hDC, 0, 0, w, h, _
                HGRUND.hDC, 0, 0, BIT_COPY)

    Call BitBlt(DISPLAY.hDC, X, Y, sw, sh, _
                MASKE.hDC, 0, 0, BIT_AND)

    Call BitBlt(DISPLAY.hDC, X, Y, sw, sh, _
                BITMAP.hDC, 0, 0, BIT_INVERT)

    DISPLAY.Refresh
End Sub

Listing 3: Sprite bei Mausbewegung

Auch bei den neu eingefügten Zeilen werden Variablen als "Zwischenwerte" benutzt. Wenn Sie das Programm nun ausführen, werden Sie feststellen, daß es den gewünschten Effekt erzielt. An der Mausposition über dem Display erscheint ein Sprite auf dem Hintergrund.


Abbildung 3: Sprite Demo

Der "Schatten" des Sprites wurde übrigens durch ein Punktgitter erzeugt. Dies ist eine vielfach angewendete Methode für Schatten. Dabei erzeugt man ein Punktmuster aus schwarzen (als durchsichtigen) und anders gefärbten Punkte (beispielsweise dunkelgrau). Später nimmt der Spieler kaum war, daß es sich um ein Raster handelt, aber die Farben erscheinen dunkler an dieser Stelle, da ja jeder 2. Pixel mit dunkelgrau übermalt wird.


Abbildung 4: Schatten, Sprite

1.6 Multimedial  

Das Darstellen von Sprites ist nun ja kein Problem mehr. Allerdings fehlt noch der Soundteil. Wie kann man mit VB WAVE- und MIDI-Dateien abspielen? Fangen wir mit den MIDI-Dateien an. Dazu benötigen wir das MCI-Steuerelement von VisualBasic (MCI = Multimedia Control Interface). Dieses Steuerelement befindet sich in der Datei MCI.VBX bzw. MCI32.OCX. Das Steuerelement selbst sieht wie die Knopfleiste eines Kassettenrecorders aus:


Abbildung 5: mci32.ocx

Was das Objekt im Einzelnen kann, spielt hier keine Rolle. Wir werden es über Unterprozeduren steuern, damit wir später mit nur einem Befehl eine beliebige MIDI-Datei abspielen können. Damit das klappt sind zunächst einige Eigenschaften anzupassen:

DeviceType = "Sequencer"
Name = "Midi"
Visible = False

Jetzt ist das MCI auf MIDI-Dateien eingestellt und trägt auch den Namen MIDI. Da Visible auf False steht ist es im laufenden Programm nicht zu sehen.
Das MCI wird über Befehle gesteuert, die dem Steuerelement mit der Command-Methode übergeben werden und der Filename-Eigenschaft, die die aktuelle Datei angibt. Eine ausführliche Beschreibung aller Befehle des MCI's finden Sie in der Online-Hilfe von VB oder in Ihrem Handbuch. Hier lernen Sie nur die Befehle kennen, die zum Abspielen einer MIDI-Datei wichtig sind.
Da wir jetzt ja neuerdings mit "Engines" programmieren, setzen wir unser MCI-Steuerelement auf eine eigene Form. Die Form bekommt den Namen "MEDIA" und das MCI-Steuerelement den Namen "MIDI". Legen Sie nun ein weiteres Codemodul an. Dieses Modul wird jetzt die Prozeduren zur Steuerung der Wiedergabe fassen. Das wir diese Prozeduren nicht in die Form setzen, hängt damit zusammen, daß man diese unter VB 3.0 dann nicht von überall im Programm ausführen kann. Nachdem Sie das Codemodul angelegt haben erstellen Sie folgende Prozeduren:

Public Sub MIDI_PLAY(DATEI As String)
On Local Error Resume Next

  If Dir$(DATEI) = "" Then Exit Sub
  MEDIA.MIDI.Command = "CLOSE"
  MEDIA.MIDI.filename = DATEI
  MEDIA.MIDI.Command = "OPEN"
  MEDIA.MIDI.Command = "PREV"
  MEDIA.MIDI.Command = "PLAY"
End Sub

Listing 4: Abspielen einer MIDI-Datei

Diese Prozedur gibt erst den Befehl "CLOSE" an das Steuerelement, um eine evtl. geöffnete MIDI-Datei zu schließen. Anschließend wird die Filename-Eigenschaft auf die Datei gesetzt, die abgespielt werden soll. Danach folgen die Befehle "OPEN" (öffnen der Datei), "PREV" (zurückspulen) und "PLAY" (abspielen). Am Anfang der Prozedur werden Vorkehrungen getroffen, daß kein Fehler auftreten kann.

Public MIDI_STOP()
  MEDIA.MIDI.Command = "STOP"
  MEDIA.MIDI.Command = "PREV"
End Sub

Listing 5: MIDI-Datei stoppen

Diese Prozedur stoppt die Wiedergabe mit dem STOP-Befehl. Anschließend wird mit dem PREV-Befehl die Datei "zurückgespult". Somit fängt sie von Vorne an, wenn die Ausgabe fortgesetzt wird.

Public MIDI_STOP()
  MEDIA.MIDI.Command = "STOP"
End Sub

Listing 6: MIDI-Datei pausieren

Hier wird die Ausgabe nur gestoppt, sie kann dann einfach an der gleichen Stelle fortgesetzt werden

Public Sub MIDI_CONTINUE()
  MEDIA.MIDI.Command = "PLAY"
End Sub

Listing 7: MIDI-Datei fortsetzen

Hier wird mit PLAY die Ausgabe gestartet. Ganz egal, ob die Ausgabe angehalten oder ob die Datei zusätzlich zurückgespult wurde.

Public Function MIDI_PLAYING()
  If MEDIA.MIDI.Position = MEDIA.MIDI.Length Then
    MIDI_PLAYING = 0
  Else
    MIDI_PLAYING = 1
  End If
End Function

Listing 8: Prüfen, ob MIDI-Datei noch läuft

Diese Funktion gibt True (1) zurück, wenn die MIDI-Datei noch läuft. Ist dagegen schon das Ende erreicht gibt sie False (0) zurück. Mit Hilfe dieser Funktion läßt sich einfach eine Endloswiedergabe erzielen. Dazu prüft man öfters, ob die Wiedergabe noch läuft. Ist das nicht der Fall ruft man MIDI_STOP und MIDI_CONTINUE auf.
Jetzt haben wir schon zwei Engines in unserem Programm: Die Grafik-Engine und die MIDI-Engine. Sie sollten Ihren Dateien auch hier sinnvolle Namen geben, wie z.B. "MIDI.BAS".
Jetzt kommen wir zu den Soundeffekten, die wir mit der Windows-API abspielen.

1.7 Das Tonstudio  

Wir könnten zwar auch WAVE-Dateien mit dem MCI ausgeben, allerdings ist das Handling zu umständlich. Deshalb greifen wir einfach auf die API zurück. Diese Funktionen unterstützen zwar auch kein Mixen von Tönen, jedoch kümmern Sie sich automatisch darum, welche Töne ausgegeben werden und welche nicht. Und im Verlauf des Spiels fällt es kaum auf, daß nicht zwei Sounds zur gleichen Zeit abgespielt werden können.
Zum Abspielen benötigen wir nur eine einzige API-Funktion. Wir unterscheiden jedoch wieder nach der Definition für VB 3.0 bzw. VB 4.0 - 16 Bit und VB 4.0 - 32 Bit oder höher. Bitte erstellen Sie auch hierfür ein neues Codemodul. Vielleicht glauben Sie, daß es Platzverschwendung ist, aber so lassen sich einzelne Funktionsgruppen (Sprites, Midi, Sound) einfach in anderen Spielen und Programmen einsetzen.

Public Declare Function sndPlaySound Lib "MMSystem" _
       (ByVal Datei As Any, ByVal _
        Hintergrund As Integer) As Integer

Listing 9: VisualBasic 3.0 / VisualBasic 4.0 16-Bit

Public Declare Function sndPlaySound Lib "Winmm" Alias _
       "sndPlaySoundA" (ByVal Datei As Any, ByVal _
       Hingergrund As Long) As Long

Listing 10: VisualBasic 4.0 32-Bit / VisualBasic 5.0

Auch hier müssen die Deklarationen wieder in einer Zeile geschrieben werden.
Nachdem Sie nun die Funktion eingebunden haben, sollten wir noch Routinen schreiben, die die Benutzung vereinfachen. sndPlaySound erwartet als Parameter die Sounddatei und einen Wert. Dieser gibt an, ob die Datei im Hintergrund abgespielt werden soll (1) oder ob das Programm warten soll, bis die Datei vollständig abgespielt ist (0).

Private Sub WAV_PLAY(DATEI As String)
  Call sndPlaySound(DATEI, 0)
End Sub

Listing 11: Abspielen einer WAVE-Datei im Vordergrund

Private Sub WAV_PLAYBACK(DATEI As String)
  Call sndPlaySound(DATEI, 1)
End Sub

Listing 12: Abspielen einer WAVE-Datei im Hintergrund

Der Aufruf dieser Funktion ist sehr einfach, wie man schon aus der Definition sieht. Um eine WAVE-Datei abzuspielen reicht der Befehl WAVE_PLAYBACK <Datei>.
Jetzt sollten Sie ein Projekt haben, daß die Hauptforum (der Sprite-Test), das Sprite-Codemodul, das Wave-Codemodul, das MIDI-Codemodul und die MEDIA-Form umfaßt.
Nun sollten wir noch die neuen Funktionen testen. Erstellen Sie dazu die FORM_LOAD-Prozedur im Hauptfenster:

Sub Form_Load
  WAVE_PLAYBACK "TEST.WAV"
  MIDI_PLAYBACK "TEST.MID"
End Sub

Listing 13: Die Prozedur FORM_LOAD

Setzen Sie oben bitte richtige Dateinamen ein. Sie können beliebige WAVE- und MIDI-Dateien auswählen. Wenn Sie nun das Programm starten hören Sie die WAVE-Datei und die MIDI-Datei. Während die Dateien abgespielt werden, können Sie schon weitermachen und die Maus wie bisher über das Display-Bildfeld schieben, da die Sounddateien alle im Hintergrund laufen.
So ähnlich werden wir auch bei dem Spiel vorgehen, zu dem wir nun endlich kommen.

1.8 Bilder mit Nummern  

Wie sie vielleicht aus dem Grundkurs noch wissen, sind auch mehrere Steuerelemente mit dem gleichen Namen in einer Form möglich. In diesem Fall werden die Steuerelemente automatisch mit einem Index versehen. Haben wir also mehrere Bildfelder mit dem Namen "BITMAP", so trägt das erste den Index 0, das zweite den Index 1 usw.
Im späteren Programm kann man auf diese Steuerelemente zugreifen, indem man den Namen gefolgt vom Index in Klammern benutzt:
BITMAP(1).Visible = 0
Dieses Verfahren verwenden wir auch in dem folgenden Spiel. So können wir (fast) beliebig viele Bilder in der Form haben, auf die wir ganz einfach zugreifen können. Auch eine Animation fällt dadurch viel leichter. Wir brauchen nur den Index des Startbildes und die Anzahl der Frames, und schon können wir auf das entsprechende Einzelbild zugreifen.

1.9 SPACE JOURNEY  

Nun kommen wir endlich zu dem richtigen Spiel. Zuerst müssen wir uns einmal überlegen, worum es im Spiel geht und was es können soll. Hier einige Stichpunkte:

  • Der Spieler steuert einen Kampfjet durch den Weltraum
  • Dabei fliegen ihm Asteroiden entgegen, die er mit seinem Laser zerstören kann
  • Die Asteroiden sollen zufällig erscheinen. Der Spieler weiß also nicht, was auf ihn zukommt
  • Wenn er einen Asteroiden berührt wird dieser zerstört, der Spieler verliert aber Energie
  • Hat der Spiele sein ganze Energie verloren, so explodiert sein Raumschiff
  • Der Spieler kann nicht dauerhaft feuern. Die Feuerkraft lädt sich selbständig wieder auf.
  • Wenn der Spieler schießt, so verliert er Punkte. Zerstört er einen Asteroiden, so bekommt er dafür Punkte

Um dies alles zu bewerkstelligen, brauchen wir nun noch einen Plan, wie wir das Programm aufbauen. Da es sehr viele Möglichkeiten gibt, daß oben skizzierte Spiel zu realisieren, werde ich Ihnen den Plan vorgeben. Diese Methode hat den Vorteil, daß sie sich auch leicht für andere Zwecke einsetzen läßt.
Plan zum Spiel:

1. Prüfen, ob Musik noch läuft
2. Hintergrund zeichnen
3. Feuerkraft des Spielers erhöhen
4. Prüfen, ob Tasten gedrückt wurden -> Tasten speichern
5. Alle Objekte durchgehen:
6.Objekt bewegen
7.Kollision prüfen
8.Zeichnen
9. Feuerkraft & Energie anzeigen
10. Punkte anzeigen
11. Und wieder zu Punkt 1.

1.10 Objekte einmal anders  

Alle Spielobjekte (also NICHT die Bildfelder, Knöpfe o.ä.!!), die sich auf dem Bildschirm, tummeln werden in einem Datenfeld als einzelner Eintrag gespeichert. Dieses Datenfeld enthält sowohl die Koordinaten als auch Energiezähler und andere wichtige Werte der Objekte. Dieses Verfahren hat den großen Vorteil, daß das ganze Geschehen systematisch kontrolliert werden kann. Selbst der Flieger des Spielers wird als Objekt gespeichert.
Das ganze Spiel läuft eigentlich in einer FOR-Schleife ab. Diese Schleife geht das komplette Datenfeld durch. Bei jedem Objekt, also jedem Eintrag des Datenfeldes, wird geprüft, ob das Objekt benutzt wird. Ist dies der Fall, so wird das Objekt bewegt, ggf. wird auf eine Kollision mit einem anderen Objekt geprüft und das Objekt wird gezeichnet. Ist das Objekt unbenutzt, so wird per Zufall entschieden, ob ein neuer Asteroid o.ä. erstellt werden soll. Die Einzelheiten werden wir beim Schreiben der Schleife ausarbeiten.
Es gibt mehrere Möglichkeiten, ein solches Datenfeld zu erstellen. Normalerweise würde man einen eigenen Datentyp erstellen, der alle nötigen Variablen enthält. Aus diesem Typ erstelt man nun das Datenfeld unten:

Type Object_Type
  X As Integer
  Y As Integer
  G As Integer
  E As Integer
End Type
'...
Dim Object(100) As Object_Type

Listing 14: Objekt - Typen in Datenfeldern

Das bedeutete allerdings, daß wir wieder ein weiteres Codemodul brauchen, da VisualBasic (zumindest Version 3.0) keine Typen in Formen definieren kann. Wir benutzen aus diesem Grund ein etwas anderes Verfahren:

Dim ObjectX(100)
Dim ObjectY(100)
Dim ObjectG(100)
Dim ObjectE(100)
'...

Listing 15: Variante zum oberen Code

Jetzt überlegen wir uns, welche Werte ein Objekt benötigt, damit wir entsprechende Datenfelder erstellen können. Wichtig ist natürlich die Position und die Geschwindigkeit. Die Position halten wir mit den Feldern ObjectX und ObjectY fest. Für die Geschwindigkeit brauchen wir nur das Datenfeld ObjectG. Dieses Datenfeld gibt die X-Geschwindigkeit an. Eine Y-Geschwindigkeit benötigen wir nicht, da die meisten Objekte sich nur von rechts nach links bewegen. Die Bewegung des Spielerraumschiffs werden wir anders lösen - aber dazu später mehr.
Wir brauchen auch noch das Datenfeld ObjectE, daß die Energie des Objekts angibt. Die Datenfelder ObjectA und ObjectAC sind Animationszähler. Wir benötigen diese, damit jedes Objekt "weiß", bei welcher Animationsphase es stehengeblieben ist. Und zu guter Letzt auch das Datenfeld ObjectT. Dieses Datenfeld enthält das wichtigste: Den Objekttyp. Dieses Datenfeld gibt an, ob das Objekt nun ein Jet, ein Asteroid oder ein Laserschuß ist.
Nun wissen wir, wie wir die Objekte verwalten und speichern. Jetzt müssen wir noch eine einfache Möglichkeit finden, die Objekte zu bewegen und zu zeichnen. Dazu eignet sich ein Select-Case-Block:

For i = 0 To 100

  Select Case ObjectT(i)
    Case 0: '...
            '...

    Case 1: '...
            '...
  End Select

Next i

Listing 16: Select Case

Diese Struktur wird auch im späteren Spiel Anwendung finden. In jedem Case-Abschnitt folgen die spezifischen Anweisungen für jeden Typ von Spielobjekt.

1.11 "Abgespacte" Grafiken  

Die Grundlage eines jeden Spiels sind die Grafiken und Sounds. Wie ich Ihnen schon gesagt habe, finden Sie alle nötigen Dateien in dem Archiv dieses Kurses. Das Spiel selbst ich recht einfach aufgebaut, wie Sie aus dem vorhergehenden Plan wissen. Der Spieler bewegt einen Jet durch ein Asteroidenfeld. Er muß den Gesteinsbrocken ausweichen oder sie mit dem Bordlaser abschießen.
Daraus ergibt sich auch schon, welche Sprites wir brauchen: Den Jet, einen Asteroiden, einen Laserschuß, einen explodierenden Asteroiden und einen explodierenden Jet, falls der Spieler seine Aufgabe nicht erfüllt ;-)
Die richtigen Sprites finden Sie im Unterverzeichnis BIT, die Masken in MASKE. Hier eine Aufstellung der Dateien:

DateiBeschreibungIndex
JET1.BMPJet - Frame 10
JET2.BMPJet - Frame 21
JET1E.BMPJet explodiert - Frame 12
JET2E.BMPJet explodiert - Frame 23
JET3E.BMPJet explodiert - Frame 34
JET4E.BMPJet explodiert - Frame 45
JET5E.BMPJet explodiert - Frame 56
LASER.BMPLaserschuß - Frame 17
ROCK1.BMPAsteroid - Frame 18
ROCK2.BMPAsteroid - Frame 29
ROCK3.BMPAsteroid - Frame 310
ROCK4.BMPAsteroid - Frame 411
ROCK1E.BMPAsteroid explodiert - Frame 112
ROCK2E.BMPAsteroid explodiert - Frame 213
ROCK3E.BMPAsteroid explodiert - Frame 314
ROCK4E.BMPAsteroid explodiert - Frame 415

Wir haben ja vorher festgelegt, daß alle Sprites in den Steuerelementefeldern BITMAP und MASKE gespeichert werden. Der Eintrag "Index" gibt an, in welches Bildfeld die Dateien geladen werden sollen. Hier ein Beispiel: Bei dem Objekt BITMAP(0) laden Sie das Bild "BIT\JET1.BMP" und bei dem Objekt MASKE(0) laden Sie "MASKE\JET1.BMP". Die Steuerelementefelder BITMAP und MASKE haben also die Indizes von 0 bis 15.
Wie in dem Demoprogramm benötigen wir noch das Bildfeld DISPLAY. Da wir auch einen scrollenden Hintergrund wünschen, ist das Bildfeld HGRUND auch wieder mit von der Partie. Das Bild, was dort hinein kommt heißt "BIT\HGRUND.BMP"

1.12 Tastatur unter Kontrolle  

Die Bilder sind jetzt einsatzbereit. Um die Sounds müssen wir uns sowieso erst später kümmern, wenn die Hauptschleife geschrieben wird. Jetzt steht allerdings ein ganz anderes Problem an: Die Steuerung
Da wir ein Actionspiel programmieren, ist eine Steuerung mit der Maus nicht empfehlenswert. Wir brauchen dazu die Tastatur. Der Benutzer soll natürlich nicht auf Knöpfe klicken, sondern einfach nur die Cursortasten benutzen, um den Jet zu steuern. Dafür benutzen wir die KeyDown- und KeyUp-Ereignisse. KeyDown tritt ein, wenn der Benutzer eine Taste drückt, KeyUp tritt ein, wenn der Benutzer eine Taste wieder losläßt. Dadurch können wir einen Mechanismus entwickeln, der angibt, ob eine bestimmte Taste im Augenblick gedrückt ist oder nicht. Schließlich soll der Benutzer ja auch zwei Aktionen zu gleichen Zeit durchführen können, wie z.B. Fliegen und Schießen.
Geben Sie folgende Zeile im Deklarationsteil der Form ein:

Dim Gedrückt(255)
Hier nun die Ereignisprozeduren:

Private Sub DISPLAY_KeyDown(KeyCode As Integer, _
                            Shift As Integer)

  If KeyCode < 256 Then Gedrückt(KeyCode) = 1
End Sub

'-------------------------------------------------

Private Sub DISPLAY_KeyUp(KeyCode As Integer, _
                          Shift As Integer)

  If KeyCode < 256 Then Gedrückt(KeyCode) = 0
End Sub

Listing 17: Ereignishandling

Wenn KeyDown eintritt, wird der Eintrag mit dem Index des Codes auf 1 gesetzt. Tritt KeyUp ein, so wird der Eintrag wieder auf 0 zurückgesetzt. Wenn wir jetzt wissen wollen, ob eine Taste mit dem Code X gedrückt ist, so brauchen wir nur Gedrückt(x) überprüfen. Hier eine Übersicht von wichtigen KeyCodes:

CodeTaste
17STRG
27ESC
37Links
38Hoch
39Rechts
40Runter

Diese Art der Tastatursteuerung ist sehr flexibel. Sie können eine solche Steuerung leicht in Ihre eigenen Programm einbauen. Wichtig ist aber, daß das Objekt den Focus besitzt. Ansonsten gehen die Key-Ereignisse nicht an das richtige Objekt und die Steuerung läuft nicht. Um dies zu erreichen, sollten Sie die TabIndex-Eigenschaft des Objekts auf 0 setzen. Das bedeutet, das Objekt enthält nach dem Aufruf der Form den Focus. Wenn Sie die Codes von anderen Tasten herausfinden wollen, so können Sie dies dadurch erreichen, daß Sie den Code bei jedem KeyDown-Ereignis mit Hilfe einer MessageBox anzeigen lassen.

1.13 Startvorbereitungen  

Jetzt kommen die letzten Vorbereitungen für das Spiel: Wir brauchen eine Prozedur, die sehr einfach einen Sprite auf den Bildschirm bringen kann. Nachdem wir schon das Demo-Programm fertig haben dürfte das kein Problem sein:

  Private Sub ZeichneSprite(Nr As Long, x As Long, Y As Long)
          Dim sw As Long, sh As Long

      sw = BITMAP(Nr).ScaleWidth
      sh = BITMAP(Nr).ScaleHeight
      Call BitBlt(DISPLAY.hDC, x - sw / 2, Y - sh / 2, sw, sh, _
                  MASKE(Nr).hDC, 0, 0, BIT_AND)
  
      Call BitBlt(DISPLAY.hDC, x - sw / 2, Y - sh / 2, sw, sh, _
                  BITMAP(Nr).hDC, 0, 0, BIT_INVERT)
  End Sub

Listing 18: Erzeugen des Hintergrundes

Diese Prozedur hat allerdings eine Verbesserung gegenüber dem "Vorgänger" erfahren: Am Anfang wird die Breite und Höhe des Sprites geholt. Der zu zeichnende Sprite wird anschließend um die X-/Y-Koordinaten zentriert. Dies wird dadurch erreicht, daß von der X-Position die Hälfte der Breite abgezogen wird und der Sprite an diese Stelle kopiert wird. Gleiches gilt für die Y-Koordinate.
Um uns einen Startknopf oder gar ein Hauptmenü zu ersparen soll das Spiel erst einmal direkt nach dem Start ausgeführt werden. Dazu benutzen wir das Load-Ereignis:

Private Sub Form_Load()
  Me.Show
  Call SpielStart
End Sub

Listing 19: Sofortiger Start

Warum "Me.Show"? Ganz einfach: Da das eigentliche Spiel fast vollständig in einem Unterprogramm abläuft, sorgen wir dafür, daß die Form angezeigt wird, bevor das Spiel startet. Denn was bringt es, wenn das Spiel läuft, Sie aber nichts sehen? Also zeigen wir die Form sofort an. Anschließend wird die Prozedur SpielStart aufgerufen - und die schreiben wir jetzt.

1.14 Der Countdown läuft  

Jetzt bekommt die Form des Spiels noch den letzen Schliff:


Abbildung 6: Der letzte Schliff

Unter das Bildfeld Display setzen Sie zwei weitere Bildfelder. Diese zeigen später im Spiel die Energie und die Schußkraft an. Nennen Sie das obere Bildfeld "ENERGY" und das untere "FKRAFT". Setzen Sie beim oberen die Hintergrundfarbe auf ein dunkles Rot und beim unteren auf ein dunkles Cyan. Darunter kommt ein Bezeichnungsfeld mit dem Namen "PUNKTEANZ". Setzen Sie Caption auf "0", die Hintergrundfarbe auf dunkelgrün und die Vordergrundfarbe auf ein helles Gelb. Setzen Sie außerdem BorderStyle auf 1.
Das Design der Form ist nun abgeschlossen. Die Bildfelder mit den Sprites sind hier nicht zu sehen, da ich diese im unteren Teil der Form plaziert habe. Denken Sie daran, daß Sie alle Bildfelder mit Sprites, mit Masken und mit dem Hintergrund unsichtbar machen. Auch sollten Sie darauf achten, daß das Display nicht (!) höher ist als der Hintergrund. In diesem Fall würde nämlich im späteren Spiel eine Lücke unter dem scrollenden Hintergrund entstehen.
Der Rahmen ist fertig - jetzt kommt das Spiel. Also los!

Private Sub SpielStart()
  Dim ObjectX(20) As Long
  Dim ObjectY(20) As Long
  Dim ObjectT(20) As Long
  Dim ObjectE(20) As Long
  Dim ObjectG(20) As Long
  Dim ObjectA(20) As Long
  Dim ObjectAC(20) As Long

  Dim HGrundX As Long, Punkte As Long
  Dim FeuerKraft As Long, Feuer As Long
  Dim Pfad As String, w As Long, h As Long
  Dim m As Long, i As Long, i2 As Long
  Dim a As Long, x As Long

    Pfad = App.Path & "\"
    MIDI_PLAY Pfad & "MUSIK.MID"

    w = DISPLAY.ScaleWidth
    h = DISPLAY.ScaleHeight

Listing 20: Spielstart

Als erstes werden die Datenfelder erstellt, die die Informationen über die Spielobjekte aufnehmen. Anschließend wird die Variable Pfad auf den Pfad gesetzt, indem sich die Dateien befinden. Hier müssen Sie den Pfad einsetzen, der zu den entsprechenden Sounddateien auf Ihrer Festplatte führt. Vergessen Sie nicht, daß am Ende auf jeden Fall einen Backslash steht! Sonst kommt es zu Fehlern. Später, wenn Sie das Spiel fertig haben und kompilieren wollen, sollten Sie Pfad$ auf " " setzen, damit die Dateien im aktuellen Verzeichnis gesucht werden. Dann brauchen sich die Sounddateien nur im gleichen Verzeichnis mit dem Programm befinden, und sie werden abgespielt. Sollte dabei etwas nicht funktionieren, so ist das auch kein Beinbruch, da die Soundroutinen gegen alle Arten von Fehlern "immun" sind. Schlimmstenfalls kommt der Spieler nicht in den Genuß der akustischen Untermalung ;-)
Jetzt müssen wir das Spielobjekt des Spielers initialisieren. Dazu legen wir jetzt fest, welche Typennummer (ObjectT(x)) für welches Objekt steht:

ObjecT(x)Bedeutung
1Raumschiff
2Asteroid
3Laserschuß
4Explodierender Asteroid
5Explodierendes Raumschiff
Start:

    ObjectX(0) = w / 2
    ObjectY(0) = h / 2
    ObjectT(0) = 1
    ObjectE(0) = 200
    ObjectG(0) = 0
    ObjectA(0) = 0

Listing 21: Startposition setzen

Bevor wir dies tun setzen wir allerdings noch die Marke Start in den Quellcode. Diese benötigen wir, wenn das Raumschiff des Spielers zerstört wird und er von Vorne anfangen muß. Die Startposition des Spielers ist genau in der Mitte des Bildschirm. Der Typ ist 0, wie wir aus der Tabelle oben ersehen können. Die Startenergie des Spielers beträgt 200 Einheiten. Das Raumschiff benötigt keine Geschwindigkeitsangabe, da es direkt über die Tastatur gesteuert wird. Zum Schluß wird noch die Animationsphase auf 0 gesetzt.

For m = 1 To 20
  ObjectT(m) = 0
Next m

Listing 22: weitere Initialisierungen

Jetzt werden alle verbleibenden Objekte (beachten Sie, das wir diesmal nur 21 Objekte haben, einschließlich dem des Spielers) auf Typ 0 gesetzt, was soviel bedeut wie: Kein Objekt

HGrundX = 0
Punkte = 0
FeuerKraft = 1000

Listing 23: Standardwerte setzen

Vor der Hauptschleife noch drei wichtige Werte: Die Variable HGrundX gibt an, wie weit der Hintergrund schon gescrollt ist. Die Bedeutung der Variable Punkte muß ich wohl nicht erklären, oder? FeuerKraft gibt an, wie stark der Bordlaser des Spielers aufgeladen ist. 1000 ist hier der Maximalwert. Näheres dazu, wenn wir zur Schußsteuerung kommen.

Do
  If MIDI_PLAYING() = 0 Then
  MIDI_STOP
  MIDI_CONTINUE
End If

Listing 24: Die Musik

Zuerst wird bei jedem Schleifendurchgang geprüft, ob die MIDI-Musik noch läuft. Wenn nein, wird die Datei wieder von Vorne abgespielt.

HGrundX = HGrundX + 1
If HGrundX > (320 * 4) Then
  HGrundX = HGrundX - (320 * 4)
End If

Call BitBlt(DISPLAY.hDC, 0, 0, w, h, _
            HGRUND.hDC, HGrundX, 0, BIT_COPY)

Call BitBlt(DISPLAY.hDC, (320 * 4) - HGrundX, 0, w, h, _
            HGRUND.hDC, 0, 0, BIT_COPY)

Listing 25: Der Hintergrund

Danach wird der Hintergrund gezeichnet. Dazu wird die Variable HGrundX erst einmal um den Wert 1 erhöht, damit sich der Hintergrund auch weiterbewegt. Hat sich der Hintergrund so weit bewegt, daß er komplett aus dem Bildschirm verschwunden ist, so wird die Variable HGrundX wieder auf 0 gesetzt.


Abbildung 7: Kopiervorgang beim Scrollen

Es wird als erstes ein Ausschnitt in der Größe des Bildfelds DISPLAY (W * H) aus dem Bildfeld HGRUND kopiert. Das ist recht leicht zu berwerkstelligen und zu verstehen: Je weiter der Spieler fliegt, desto weiter verschiebt sich der Bereich, der kopiert wird. Und wozu nun der zweite Kopiervorgang? Wenn der Spieler am Ende des Levels angekommen ist, haben wir die Situation, daß der Bereich den wir kopieren sollen zum Teil außerhalb des Hintergrundpuffers liegt! Wenn wir keinen weiteren Kopiervorgang einbauen, erhalten wir dann an der Stelle "Müll". Deshalb wird der Anfang des Hintergrundpuffers an die Stelle kopiert, an der der erste aufhört. Um diese Koordinate zu bekommen nehmen wir die Breite des Hintergrunds (in unserem Fall ist der Hintergrund 320*4 Pixel breit - das können Sie aber natürlich ändern) und ziehen die Position HGrundX davon ab. Die meiste Zeit wird dieser Bereich außerhalb des Displays liegen, jedoch beeinträchtigt das die Leistung des Spiels kaum, da die API-Funktionen "intelligent" genug sind, um nicht unnötig viel zu kopieren. Wenn Ihnen hierbei noch Zweifel kommen, so probieren Sie mal im fertigen Spiel, den zweiten Kopiervorgang herauszunehmen. Sie werden sehen, daß es ohne diesen nicht geht.

If FeuerKraft < 1000 Then
  If FeuerKraft < 100 Then
    FeuerKraft = FeuerKraft + 5
    If FeuerKraft > 100 Then FeuerKraft = 100
  Else
    FeuerKraft = FeuerKraft + 2
  End If
End If

Listing 26: Die Feuerkraft

Da sich der Laser des Spielers automatisch regeneriert, wird an dieser Stelle die Feuerkraft des Spielers erhöht. Erst wird nachgesehen, ob der Spieler nicht schon volle Feuerkraft erreicht hat. Wenn nicht, dann wird geprüft, ob sich die Feuerkraft unter 100 Punkte befindet. Ist dies der Fall, so lädt der Laser schneller. Ab 100 Punkten wird dann nur noch mit 2 Einheiten pro Bildschirmaufbau geladen. Dadurch wird erreicht, daß Der Spieler, selbst wenn die Feuerkraft auf 0 ist, in kurzer Zeit wenigstens einen Schuß abgeben kann. Will der Spieler den Laser so stark aufladen lassen, daß er einen Feuerstoß abgeben kann muß er viel länger warten. Eine solche Aufteilung ist natürlich kein Muß, macht aber das Spiel interessanter, da die Schüsse nicht immer gleichmäßig kommen.

Display.SetFocus
DoEvents

Listing 27: Aktualisierungen

Mit der Methode SetFocus wird der Focus bei jedem Schleifendurchgang auf das Display-Bildfeld gesetzt. Damit soll verhindert werden, daß der Spieler aus Versehen zu einem anderen Objekt wechselt und die Kontrolle über sein Schiff verliert. Anschließend wird der Befehl DoEvents aufgerufen. Dieser sorgt dafür, daß im auftretende Ereignisse verarbeitet werden. Ohne diesen Befehl hätte Windows keine Möglichkeit, daß KeyDown- oder KeyUp-Ereignis an unser Bildfeld zu senden und die Steuerung würde nicht funktionieren.
Jetzt kommen wir zu der Schleife, die die Objekte durchgeht:

For i = 0 To 20
  ObjectX(i) = ObjectX(i) + ObjectG(i)
  Select Case ObjectT(i)

Listing 28: Die einzelnen Objekte werden durchgegangen

Die Schleife geht die Einträge 0 bis 20 durch und die Nummer wird in der Variable "i" gespeichert. Anschließend wir das aktuelle Objekt bewegt. Hier spielt es überhaupt keine Rolle, ob das Objekt benutzt wird oder nicht. Ein unbenutztes Objekt ist ja so oder so nicht zu sehen und bei einem benutzen Objekt kann uns die automatische Bewegung ja nur recht sein. Jetzt kommt der wichtigste Programmteil: Der Select-Case-Block. Aus dem Plan von eben wissen Sie ja, daß jedes Objekt eine Typennummer hat, die es identifiziert. In dem Select-Case-Block wird jeder Typ gesteuert. Zum Beispiel wird im Case 1-Abschnitt das Raumschiff des Spielers gesteuert.

Case 0: ObjectAC(i) = 0
        ObjectA(i) = 0

        If Feuer > 0 And FeuerKraft > 0 Then
          Feuer = 0
          FeuerKraft = FeuerKraft - 50
          ObjectX(i) = ObjectX(0) + 2
          ObjectY(i) = ObjectY(0) + 8
          ObjectT(i) = 3
          ObjectG(i) = 4
          ObjectE(i) = 3
          WAV_PLAYBACK Pfad$ + "LASER.WAV"
          Punkte = Punkte - 1

Listing 29: Der Schuss wird bewegt

Es folgt der erste Case-Abschnitt. Dieser behandelt alle Spielobjekte mit dem Typ 0, also alle Objekte, die nicht benutzt werden. Zuerst wird hier geprüft, ob der Spieler auf die Feuer-Taste gedrückt hat. Dies wird mit der Variable Feuer geprüft. Hat diese Variable einen Wert größer als 0, so hat der Spieler die Feuertaste gedrückt. Wenn außerdem die Feuerkraft größer oder gleich 50 ist, so kann ein Schuß abgegeben werden.
In diesem Fall wird als erstes die Variable Feuer wieder auf Null gesetzt, damit erst im nächsten Schleifendurchgang wieder gefeuert werden kann. Die Feuerkraft wird um 50 Einheiten verringert. Dies ist die Kraft, die für einen Schuß benötigt wird. Nun wird das aktuelle Objekt (das ja vorher unbenutzt war) als Schuß benutzt. Dazu wird es auf die Koordinaten des Spielers gesetzt. Allerdings wird bei der X-Koordinate der Wert 2 und bei der Y-Koordinate der Wert 8 hinzuaddiert. Dies geschieht, damit es so aussieht, als würde der Laserschuß wirklich aus den Kanonen des Raumschiffs kommen. Es ist also nicht mehr, als eine kleine Verschönerung. Das Spiel würde auch ohne diese Verzierung laufen. Nun wird der Typ des Objekts auf 3 gesetzt. Wie Sie aus der Tabelle der Objekttypen wissen, steht in diesem Spiel der Wert 3 für einen Laserschuß. Die Geschwindigkeit eines Laserschusses beträgt vier Pixel pro Bildschirmaufbau und die Energie drei Einheiten. Nachdem diese Einstellungen getroffen sind wird noch eine Sounddatei ausgegeben, damit der Spieler auch hört, daß er gefeuert hat. Und zum Schluß wird ihm noch ein Punkt für einen Schuß abgezogen. Diese Gemeinheit habe ich eingebaut, damit der Spieler nicht einfach immer die Feuertaste gedrückt hält um sich seinen Weg zu bahnen.

Else
  a = Fix(Rnd * 4000)
  If a < 2 Then
    ObjectX(i) = w + 40
    ObjectY(i) = Rnd * h
    ObjectT(i) = 2
    ObjectG(i) = -2 - Rnd * 3
    ObjectE(i) = 15
  End If
End If

Listing 30: Ein neuer Asteroid wird erstellt

Wenn kein Schuß abgegeben werden kann oder soll wird der andere Teil des IF-Blocks ausgeführt. Hier wird per Zufall entschieden, ob ein neuer Asteroid erstellt werden soll. Dafür wird ein Wert zwischen 0 und 4000 ausgelost. Ist dieser Wert kleiner als 2, so wird ein Asteroid erstellt. Das mag Ihnen vielleicht gering vorkommen, allerdings müssen Sie überlegen, daß dies pro unbenutzten Objekt und pro Bildschirmaufbau gilt. Sie werden schon sehen, wie viele Asteroiden herumfliegen werden. Wenn die Entscheidung zu Gunsten eines neuen Asteroiden erfolgt ist, wird dieser erstellt. Seine X-Position liegt rechts außerhalb vom Bildfeld. Seine Y-Position wird ebenfalls per Zufall festgelegt. Dazu wird ein Wert zwischen 0 und der Höhe des Spielfelds benutzt. Der Typ eines Asteroiden hat die Nummer zwei - also setzen wir sie auch. Die Geschwindigkeit beträgt mindestens -2 Pixel pro Sekunde und maximal -5 Pixel pro Sekunde. Auch hier wird der Zufallsgenerator benutzt, damit die Asteroiden nicht in Reihe und Glied fliegen. Ein Gesteinsbrocken hat 15 Energieeinheiten und muß somit 5 mal von einem Laserschuß (Energie = 3) getroffen werden, bis seine Energie gleich 0 ist, und er explodiert.

Case 1: ObjectAC(i) = ObjectAC(i) + 1
        If ObjectAC(i) > 10 Then
          ObjectA(i) = ObjectA(i) + 1
          ObjectAC(i) = 0
        End If

        If ObjectA(i) > 1 Then
          ObjectA(i) = 0
        End If

Listing 31: Der Asteroid

Jetzt kommen wir zu den Asteroiden. Auch hier wird mit Verzögerungen und Animationszählern gearbeitet. Der Animationszähler enthält hier allerdings Werte zwischen 0 und 3, da ein Asteroid vier Frames hat.

Case 2: ObjectAC(i) = ObjectAC(i) + 1
        If ObjectAC(i) > 10 Then
          ObjectA(i) = ObjectA(i) + 1
          ObjectAC(i) = 0
                   End If
           
        If ObjectA(i) > 3 Then
          ObjectA(i) = 0
        End If
        Call ZeichneSprite(ObjectA(i) + 8, _
                            ObjectX(i), _
                            ObjectY(i))

        If ObjectX(i) < -30 Then ObjectT(i) = 0
        If ObjectE(i) <= 0 Then
          ObjectT(i) = 4
          ObjectA(i) = 0
          ObjectAC(i) = 0
          WAV_PLAYBACK Pfad$ + "EXPLOSIO.WAV"
          Punkte = Punkte + 20
        End If

Listing 32: Die Animationen

Nun wird der Sprite gezeichnet. Hier muß zum Animationszähler der Wert 8 addiert werden, da der erste Frame der Animation sich in Bildfeld acht befindet. Wenn der Asteroid links aus dem Bild verschwindet, also seine X-Koordinate in den Minusbereich geht, wird er gelöscht, indem der Objekttyp auf 0 gesetzt wird. Wenn die Energie des Asteroiden gleich 0 ist, so wird er in Typ 4, einen explodierenden Asteroiden, umgewandelt. Dabei wird genauso vorgegangen wie beim Raumschiff des Spielers: Der Typ wird gesetzt, die Animationszähler werden auf 0 zurückgesetzt und eine WAVE-Datei wird abgespielt. Zusätzlich werden hier die Punkte des Spielers um 20 erhöht. Schließlich soll er ja auch belohnt werden, wenn er einen Asteroiden vernichtet hat ;-)
Jetzt kommen wir zu einem sehr wichtigen Teil im Spiel: Der Kollisionsabfrage. Wenn der Spieler gegen einen Gesteinsbrocken "crasht", so muß dem Spieler ja auch dafür Energie abgezogen werden. Um das zu erreichen, müssen wir bei jedem Asteroiden prüfen, ob er mit dem Spieler zusammengestoßen ist. Das Prinzip einer solchen Abfrage läuft so: Es werden jeweils alle anderen Objekte durchgegangen und es wird geprüft, ob das entsprechende Objekt mit dem aktuellen kollidieren kann. Zum Beispiel sollen die Asteroiden ja nicht miteinander kollidieren. Also prüfen wir den Typ des Objekts. Ist das getan prüfen wir noch, ob der Abstand (sowohl in X- als auch in Y-Richtung) einen bestimmten Wert unterschritten hat. In dem Fall ist eine Kollision eingetreten:

For i2 = 0 To 20
  If ObjectT(i2) = 1 Then
    If Abs(ObjectX(i2) - ObjectX(i)) < 50 Then
      If Abs(ObjectY(i2) - ObjectY(i)) < 40 Then
        ObjectE(i2) = ObjectE(i2) - ObjectE(i)
        ObjectE(i) = 0
        WAV_PLAYBACK Pfad$ + "EXPLOSIO.WAV"
      End If
    End If
  End If
Next i2

Listing 33: Die Expolsion

Es werden also wieder alle Objekte in der Schleife durchgegangen. Die aktuelle Nummer wird in der Variable I2 gespeichert. Es wird jedesmal geprüft, ob das aktuelle Objekt den Typ 1 hat, also ob es der Jet des Spielers ist. Dann wird geprüft, ob die Entfernung der X-Koordinate vom Spielerraumschiff und der vom Asteroiden kleiner als 50 ist. In dem Fall wird auch noch geprüft, ob die Y-Entfernung kleiner als 40 ist. Wenn diese Bedingungen alle zutreffen, so hat eine Kollision stattgefunden. Dem tragen wir Rechnung, indem wir dem Spieler die Energie abziehen, die der Asteroid noch hat. Wenn der Asteroid noch keinmal von einem Laserschuß getroffen wurde, so werden hier dem Spieler 15 Energieeinheiten abgezogen. Anschließend wird die Energie des Asteroiden auf -1 gesetzt. Dadurch wird er beim nächsten Bildschirmaufbau explodieren. Eine Explosion müssen wir hier nicht extra ausgeben, da das beim nächsten Schleifendruchlauf sowieso passiert.
Sie werden sich vielleicht fragen, warum ich an dieser Stelle alle Objekte durchgehe. Schließlich wissen wir doch, daß der Spieler Objekt Nr. 0 ist. Tja, man könnte die Schleife auch weglassen, aber das bringt fast keine Geschwindigkeitsvorteile. Und außerdem wissen wir auch nicht, ober Objekt Nr. 0 immer noch ein Raumschiff ist. Wenn es mittlerweile explodiert ist, befindet sich an dieser Position eine Explosion oder vielleicht ein anderer Asteroid. Ein weiterer Grund ist, daß eine solche Kollisionsabfrage wie wir Sie gerade besprochen haben sehr vielseitig eingesetzt werden kann. Sie sehen sie sogar gleich wieder, wenn wir bei den Laserschüssen sind.
Noch ein Hinweis: Die minimale Entfernung der Objekte läßt sich einfach ermitteln. Angenommen der Jet des Spielers ist 20 Pixel breit und der Asteroid 60 (das stimmt zwar nicht ganz, aber es ist ja nur eine Annahme), dann beträgt der minimale Abstand zwischen dem Mittelpunkt des Jets und dem des Asteroiden 20/2 + 60 /2 Pixel. Also: 40 Pixel. Wird diese Grenze unterschritten, so überlagern Sie sich. Diese Abfrage muß dann noch für die Höhe gemacht werden.Um die Differenz zwischen zwei Werten zu errechnen, subtrahiere ich die beiden Werte voneinander (die Reihenfolge ist hier egal). Anschließend wird der Wert mit der Funktion ABS(x) in eine positive Zahl umgewandelt. Auch diese Formel ist allgemein gültig und findet oft Verwendung.

Case 3: Call ZeichneSprite(7, ObjectX(i), ObjectY(i))
        If ObjectX(i) > w + 5 Then ObjectT(i) = 0
        If ObjectE(i) <= 0 Then ObjectT(i) = 0


        For i2 = 0 To 20
          If ObjectT(i2) = 2 Then
            If Abs(ObjectX(i2) - ObjectX(i)) < 30 Then
              If Abs(ObjectY(i2) - ObjectY(i)) < 30 Then
                ObjectE(i2) = ObjectE(i2) - ObjectE(i)
                ObjectE(i) = 0
              End If
            End If
          End If
        Next i2

Listing 34: Der Laser

Wir behandeln die weiteren Objekt nicht mehr ganz so ausführlich. Daher kommt hier der ganze Abschnitt für den Laserschuß. Der Laserschuß hat keine Animation, folglich können wir uns Zähler und ähnliches sparen. Es wird nur Sprite Nr. 7 an die entsprechende Position gezeichnet. Wie Sie aus der Tabelle von oben wissen, ist Sprite Nr. 7 ja der Laser. Als nächstes wird überprüft, ob der Schuß den Bildschirm verlassen hat. In diesem Fall wird er einfach gelöscht. Fällt die Energie eines Schusses auf 0, so löst er sich auf. Da wir hierfür keine Animation vorgesehen haben löschen wir den Sprite einfach.
Jetzt kommt wieder die Kollisionsabfrage. Diesmal ist das Ziel nicht der Spieler (das wäre ja auch etwas gemein) sondern Asteroiden (Typ 2). Hier hat sich gegenüber der anderen Kollisionsabfrage nicht viel geändert. Nur die minimal Entfernungen haben neue Werte. Die Auswirkungen bleiben gleich. Trift ein Laserschuß einen Asteroiden, so verschwindet der Laserschuß und dem Asteroiden wird Energie abgezogen.

Case 4: ObjectAC(i) = ObjectAC(i) + 1
        If ObjectAC(i) > 10 Then
          ObjectA(i) = ObjectA(i) + 1
          ObjectAC(i) = 0
        End If

        If ObjectA(i) > 3 Then
          ObjectA(i) = 0
          ObjectT(i) = 0
        Else
          Call ZeichneSprite(ObjectA(i) + 12, _
                              ObjectX(i), _
                              ObjectY(i))
        End If

        If ObjectX(i) < -30 Then ObjectT(i) = 0

Listing 35

Nun kommen wir langsam zum Schluß. Typ 4 ist der explodierende Asteroid. Hier brauchen wir wieder einen Animationszähler. Schließlich haben wir ja nicht umsonst vier Animationsphasen gezeichnet! Ist der letzte Frame gezeichnet (also wenn der Animationszähler größer ist als 3), so wird der Zähler zurückgesetzt und der Asteroid gelöscht. Andernfalls wird das Objekt gezeichnet. Dabei wird zum Zähler 12 addiert, da die Animation des explodierenden Asteroiden bei Sprite Nr. 12 anfängt. Beachten Sie hier, daß der Sprite NICHT gezeichnet werden darf, wenn die Animation abgelaufen ist und die Zähler zurückgesetzt wurden. Sonst würde der Spieler ein kurzes Aufblinken des ersten Frames sehen - und das wollen wir ja nicht. Verschwindet die Animation vom Bildschirm bevor die Animation abgelaufen ist, so wird der Sprite direkt gelöscht.

Case 5: ObjectAC(i) = ObjectAC(i) + 1
        If ObjectAC(i) > 10 Then
          ObjectA(i) = ObjectA(i) + 1
          ObjectAC(i) = 0
        End If

        If ObjectA(i) > 4 Then
          ObjectA(i) = 0
          ObjectT(i) = 0
          Goto Start
        Else
          Call ZeichneSprite(ObjectA(i) + 2, _
                             ObjectX(i), _
                             ObjectY(i))
        End If

        If ObjectX(i) < -30 Then ObjectT(i) = 0

Listing 36: Das explodierende Raumschiff

Das explodierende Raumschiff ist genauso aufgebaut, nur daß es 5 Frames hat und so die Animation bei 4 beendet ist. Außerdem beginnt der explodierende Jet bei Sprite Nr. 2.

  End Select
Next i

Display.Refresh

Listing 37: Das Ende der Schleife

Nun haben wir alle Objekte fertig, die wir brauchen. Der Select-Case-Block wird geschloßen, genauso wie die For-Schleife. Anschließend wird das Bildfeld noch "refresht", damit wir auch den Lohn unserer Arbeit sehen. Jetzt fehlen uns noch die Instrumente:

FKRAFT.Cls
x = FKRAFT.ScaleWidth / 1000 * FeuerKraft
FKRAFT.Line (0, 0)-(x, FKRAFT.ScaleHeight), QBColor(11), BF

Listing 38: Die Instrumente

Fangen wir mit der Anzeige der Feuerkraft an. Erst einmal wird das Bildfeld geleert (damit die vorherige Anzeiger gelöscht wird). Da wir eine Prozentanzeige haben wollen, errechnen wir, wie weit der Balken im Verhältnis zum Bildfeld stehen muß. Dazu nehmen wir die Zeichenbreite des Bildfelds und teilen Sie durch 1000 (das ist der Maximalwert für die Schußkraft). Nun multiplizieren wir das Ganze noch mit der aktuellen Schußkraft. Das diese Formel funktioniert, kann man sich leicht überlegen:
Die volle Feuerkraft hat den Wert 1000. Und wenn wir die Balkenlänge durch 1000 teilen und mit 1000 multiplizieren, haben wir wieder die Balkenlänge heraus, und der Balken ist voll. Man kann sich das ganze natürlich auch an einer Verhältnisgleichung veranschaulichen, aber ich will Ihnen ja keine Nachhilfe in Prozentrechnung geben :)
OK, nun haben wir die Länge des Balkens und können ihn einzeichnen. Dazu verwenden wir die Line-Methode. Wir zeichnen eine Box von der linken oberen Ecke bis zu der Balkenposition und dem unteren Rand. Diese Box wird hellcyan gefüllt.

ENERGY.Cls
x = ENERGY.ScaleWidth / 200 * ObjectE(0)
ENERGY.Line (0, 0)-(x, ENERGY.ScaleHeight), QBColor(12), BF

Listing 39: Anzeigen der Energie

Der Energiebalken des Spielers wird analog gezeichnet. Und wird daher nicht mehr erklärt. Er wird übrigens hellrot gefüllt.

PUNKTEANZ.Caption = Punkte

Listing 40: Die Punkte

Die letzte Anzeige ist die Punktetafel. Hier setzen wir einfach die Caption-Eigenschaft auf die aktuelle Punktzahl. Das war's

  Loop
End Sub

Listing 41: Das Ende der Schleife

Jetzt wird noch die Hautpschleife geschlossen. Wir sind fertig. Sie können jetzt Ihr fertig Spiel testen.
Das fertige Spiel befindet sich übrigens für VisualBasic 3.0 und 4.0 - 16 Bit im Verzeichnis "GAME16" und für 4.0 - 32 Bit oder 5.0 im Verzeichnis "GAME32".


Abbildung 8: Das fertige Spiel

Vielleicht haben Sie das Gefühl, das der Aufwand für ein solches Spiel zu groß war. An dieser Stelle kann ich Ihnen versichern, je öfter Sie sich mit Spieleprogrammierung beschäftigen, desto leichter fällt es Ihnen. Dieses Demospiel habe ich (zusammen mit den Grafiken) in ca. 1 ½ Stunden geschrieben.

Quellcodes als Download [519 kB] [519000 Bytes]

1.15 Houston, wir haben ein Problem  

In diesem Kurs kam natürlich eine große Menge an Informationen auf einmal. Das liegt daran, daß ich nicht nur die Grundlagen sondern auch ein praktisches Beispiel erklären wollte. Der Umfang der folgenden Spiele-Kurse wird - so hoffe ich zumindest - etwas geringer ausfallen. Wenn Sie Probleme haben, egal ob Verständnisschwierigkeiten oder ob etwas nicht läuft, fragen Sie mich einfach per E-Mail:

1.16 Ende Teil 1  

Damit wäre der erste Teil des Spiele-Kurses abgeschlossen. Wenn Sie Lust haben können Sie ja probieren, einen anderen Typ von Asteroiden (vielleicht einen kleineren) in das Spiel einzubauen. Auch eine Highscore-Funktion wäre sicher nicht verkehrt.
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

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.