Im Artikel über Anfängerfehler
stellte ich Ihnen eine typisch primitive Variante eines Abenteuerspiels vor,
welche wegen den vielen GOTO
-Zeilen verschiedene Nachteile
aufweist:
GOTO
ist im Zeitalter der strukturierten
Programmierung eigentlich weitgehend verpönt und unsauber. Daher steht in
modernen Programmiersprachen mittlerweile kein GOTO
mehr zur
Verfügung.GOTO
-Zeilen fest
»verdrahtetes« Abenteuerspiel-Welt nicht besonders flexibel und
vor allen Dingen nicht skalierbar, die Lösung versagt
also bei einem komplexen Abenteuerspiel.Aus diesen Gründen möchte ich Ihnen im folgenden einen wirklich sauberen und schönen Programm-Design mit Hilfe einer sog. Zustandsmaschine vorstellen, welchen Sie in Ihren eigenen Spielprogrammierprojekten als Grundgerüst verwenden können.
Eine Abenteuerspielwelt lässt sich ganz unabhängig von der im Spiel vorkommenden Handlung abstrakt als sog. Graph (die Graphentheorie ist ein Spezialgebiet der Mathematik und Informatik) darstellen, bei welchem die Räume den Knoten entsprechen, und die Wege (auch Türen) den Kanten.
Im folgenden lasse ich mit Ihnen zusammen ein sehr einfach gehaltenes Text-Abenteuerspiel entstehen. Als erstes sollten Sie die Welt für den Spieler mit Bleistift und Papier entwerfen:
Eine einfache Welt aus 4 Räumen, einem Ziel und 2 tödlichen
Stellen
All diese Räume (Zustände!) müssen Sie nun nummerieren:
Das selbe Szenario durchnummeriert
Nun müssen Sie zu jedem einzelnen Raum einen Folgezustand definieren, welcher aus der Eingabe des Benutzers resultieren soll. In unserem Fall heissen die Kommandos N, S, W und O. Diese Informationen lassen sich als Tabelle darstellen, welche aussagt, wo es weitergeht:
aktuelle Pos. \ Befehl | N | S | W | O |
---|---|---|---|---|
1 (Keller) | -2 | 0 | 2 | -3 |
2 (Badezimmer) | 0 | 0 | 3 | 1 |
3 (Stube) | 0 | 0 | 0 | 2 |
4 (Küche) | 0 | 3 | 0 | -1 |
Hinweis: Die Zustände < 0 brauchen nicht analysiert zu werden, weil man ja hier das Spiel beendet (gestorben bzw. geschafft) hat. Ausserdem definiere ich noch einen Hilfszustand 0, welcher »In diese Richtung kann ich nicht gehen!« (Wand) bedeutet.
Ich hoffe, dass Sie den Aufbau dieser Tabelle verstanden haben: Sie
können daraus jeweils ablesen, welches der Folgeraum darstellt, wenn man
ein bestimmtes Richtungskommando eingibt. Genau diese Tabelle sollten wir jetzt
in DATA
-Zeilen erfassen und in Feldvariablen einlesen, ebenso die
Namen der Räume und die Kommandos. Es folgt dabei noch gleichzeitig die
Game Engine, also das eigentliche Kernprogramm.
' Mini-Adventurespiel CONST nRaeume% = 4 ' Total vier Räume DIM Raum$(1 TO nRaeume%), Nord%(1 TO nRaeume%), Sued%(1 TO nRaeume%) DIM West%(1 TO nRaeume%), Ost%(1 TO nRaeume%) FOR i% = 1 TO nRaeume% READ Raum$(i%), Nord%(i%), Sued%(i%), West%(i%), Ost%(i%) NEXT i% ' Zustandstabelle ' Raum N S W O DATA "Keller", -2, 0, 2, -3 DATA "Badezimmer", 0, 0, 3, 1 DATA "Stube", 4, 0, 0, 2 DATA "Küche", 0, 3, 0, -1 ' Ab hier eigentliches Spiel AktPos% = 2 ' Start im Badezimmer InSpiel% = -1 ' Flag, dass immer noch in Spiel (-1 = TRUE, Boolean) PRINT "Willkommen im Mini-Abenteuer! :-)" PRINT WHILE InSpiel% PRINT "Sie befinden sich im/in der "; Raum$(AktPos%) PRINT "Mögliche Gehrichtungen:"; IF Nord%(AktPos%) <> 0 THEN PRINT " Norden"; END IF IF Sued%(AktPos%) <> 0 THEN PRINT " Süden"; END IF IF West%(AktPos%) <> 0 THEN PRINT " Westen"; END IF IF Ost%(AktPos%) <> 0 THEN PRINT " Osten"; END IF PRINT LINE INPUT "Was soll ich für Sie tun? ", bef$ SELECT CASE bef$ CASE "N", "n" ' Nach Norden gehen NeuPos% = Nord%(AktPos%) CASE "S", "s" ' Nach Süden gehen NeuPos% = Sued%(AktPos%) CASE "W", "w" ' Nach Westen gehen NeuPos% = West%(AktPos%) CASE "O", "o" ' Nach Osten gehen NeuPos% = Ost%(AktPos%) CASE ELSE NeuPos% = AktPos% ' Bleiben PRINT "Leider habe ich Ihr Kommando nicht verstanden" PRINT "Probieren Sie's doch noch einmal" END SELECT IF NeuPos% = 0 THEN PRINT "Da kann ich nicht hingehen!" ELSE AktPos% = NeuPos% IF NeuPos% < 0 THEN InSpiel% = 0 ' 0 = FALSE (Boolean) END IF END IF WEND ' Schlussauswertung IF AktPos% = -1 THEN PRINT "Bravo! :-) Sie haben das Mini-Abenteuer bestanden!" ELSE IF AktPos% = -2 THEN PRINT "Leider sind Sie in den Abgrund gestürzt, da der Boden nur aus" PRINT "einer Lage Papier bestand." ELSEIF AktPos% = -3 THEN PRINT "Leider sind Sie zum Teufel gegangen, der Sie mit der Ofengabel" PRINT "entzweigespiesst hat" END IF PRINT "Sie sind tot." END IF
Da bei Zustandsmaschinen-Prinzip der aktuelle Zustand vollständig in Variablen gespeichert ist, genügt es, diese Variablen abzuspeichern. In unserem Mini-Abenteuer stell AktPos% das einzige Zustand-Speicherregister dar, so dass genügt, dieses in eine Diskdatei abzuspeichern:
' .. (vorheriger Code) CASE "O", "o" ' Nach Osten gehen NeuPos% = Ost%(AktPos%) CASE "save" LINE INPUT "Spielstandname", a$ OPEN a$ FOR OUTPUT AS 1 PRINT#1, "MiniAdvSpiel" ' Nur interne Kennung PRINT#1, AktPos% CLOSE 1 PRINT "Spielstand gespeichert!" CASE "load" LINE INPUT "Spielstandname", a$ OPEN a$ FOR INPUT AS 1 INPUT#1, Kennung$ IF Kennung$ <> "MiniAdvSpiel" THEN PRINT "Keine gültige Spielstanddatei!" ELSE INPUT#1, AktPos% END IF CLOSE 1 CASE ELSE PRINT "Leider habe ich Ihr Kommando nicht verstanden" '.. (Rest gleich)
Ein Abenteuerspiel, bei dem man lediglich in der Welt herumspazieren kann, ist noch nicht allzu interessant, daher sollten wir noch Gegenstände hineinbringen, z.B. das Brett, das man über den Papierboden als Brücke zu werfen hat oder der Helm, der Sie vor dem Ofengabelstich des Teufels verschont und natürlich der bekannte Schlüssel zur Schatztruhe... :-)
Auch hierfür leistet Ihnen das Zustandsmaschinen-Design nützliche Dienste, in dem Sie jedem Gegenstand eine Variable vergeben, welche angibt, in welchem Raum sich der Gegenstand befindet.
Im folgenden erweitern wir also unser Mini-Abenteuer um folgende Gegenstände: Sie tragen zu Beginn bereits ein Schweizer Taschenmesser, in der Küche befindet sich ein Kochtopf und in der Stube ein Foto. Dazu brauchen wir eine kleine Gegenstandverwaltung aufzubauen: Name und Ort. Der Ort entspricht dabei dem Raum, wobei der Wert 0 bedeuten soll, dass Sie den Gegenstand bei sich gerade tragen, und -1 heisst, dass er nicht mehr existiert (zerstört oder verbraucht).
' Gegenstände CONST nGegenstaende% = 3 DIM Gegenstand$(1 TO nGegenstaende%), Ort%(1 TO nGegenstaende%) FOR i%=1 to nGegenstaende% READ Gegenstand$(i%), Ort%(i%) NEXT i% DATA "Schweizer Taschenmesser", 0 DATA "Kochtopf", 4 DATA "Foto", 3
Das ganze Programm, also die Game-Engine, erweitern wir um folgende Kommandos: nimm, leg und inventar. Beachten Sie jetzt allerdings, dass wir bereits einen kleinen Parser schreiben müssen, welcher das Verb vom Rest trennt!
Machen wir doch gleichzeitig unser Parser noch intelligenter, in dem er sinnvolle Befehle wie gehe süd, nimm messer, lade, speichere und hilfe versteht :-). Dabei soll gleichzeitig die Gross/Kleinschreibung gleichgültig sein. Definition der Grammatik: Die Sätze immer aus zwei Worten bestehen: Verb und Objekt. Kurzkommandos sind davon ausgenommen.
' Mini-Adventurespiel ' Version 2 mit Gegenstände CONST nRaeume% = 4 ' Total vier Räume DIM Raum$(1 TO nRaeume%), Nord%(1 TO nRaeume%), Sued%(1 TO nRaeume%) DIM West%(1 TO nRaeume%), Ost%(1 TO nRaeume%) FOR i% = 1 TO 4 READ Raum$(i%), Nord%(i%), Sued%(i%), West%(i%), Ost%(i%) NEXT i% ' Zustandstabelle ' Raum N S W O DATA "Keller", -2, 0, 2, -3 DATA "Badezimmer", 0, 0, 3, 1 DATA "Stube", 4, 0, 0, 2 DATA "Küche", 0, 3, 0, -1 ' Gegenstände CONST nGegenstaende% = 3 DIM Gegenstand$(1 TO nGegenstaende%), Ort%(1 TO nGegenstaende%) FOR i% = 1 TO nGegenstaende% READ Gegenstand$(i%), Ort%(i%) NEXT i% DATA "Schweizer Taschenmesser", 0 DATA "Kochtopf", 4 DATA "Foto", 3 ' Ab hier eigentliches Spiel AktPos% = 2 ' Start im Badezimmer InSpiel% = -1 ' Flag, dass immer noch in Spiel (-1 = TRUE, Boolean) NeuPos% = AktPos% PRINT "Willkommen im Mini-Abenteuer V2! :-)" PRINT WHILE InSpiel% PRINT "Sie befinden sich im/in der "; Raum$(AktPos%) PRINT "Mögliche Gehrichtungen:"; IF Nord%(AktPos%) <> 0 THEN PRINT " Norden"; END IF IF Sued%(AktPos%) <> 0 THEN PRINT " Süden"; END IF IF West%(AktPos%) <> 0 THEN PRINT " Westen"; END IF IF Ost%(AktPos%) <> 0 THEN PRINT " Osten"; END IF PRINT PRINT "Gegenstände in diesem Raum:"; h% = 0 FOR i% = 1 TO nGegenstaende% IF Ort%(i%) = AktPos% THEN PRINT " "; Gegenstand$(i%); h% = h% + 1 END IF NEXT i% IF h% = 0 THEN PRINT " keine" ELSE PRINT END IF LINE INPUT "Was soll ich für Sie tun? ", bef$ ' Befehl zerlegen h% = INSTR(bef$, " ") IF h% > 0 THEN verb$ = LEFT$(bef$, h% - 1) objekt$ = MID$(bef$, h% + 1) ELSE verb$ = bef$ ' Kurzkommandos sind nur ein Wort objekt$ = "" ' kein Objekt END IF SELECT CASE LCASE$(verb$) ' Zur Auswertung in Kleinbuchstaben umwandeln CASE "n" ' Nach Norden gehen NeuPos% = Nord%(AktPos%) CASE "s" ' Nach Süden gehen NeuPos% = Sued%(AktPos%) CASE "w" ' Nach Westen gehen NeuPos% = West%(AktPos%) CASE "o" ' Nach Osten gehen NeuPos% = Ost%(AktPos%) CASE "gehe" ' gehen SELECT CASE LCASE$(objekt$) CASE "nord" ' Nach Norden gehen NeuPos% = Nord%(AktPos%) CASE "süd", "sued" ' Nach Süden gehen NeuPos% = Sued%(AktPos%) CASE "west" ' Nach Westen gehen NeuPos% = West%(AktPos%) CASE "ost" ' Nach Osten gehen NeuPos% = Ost%(AktPos%) CASE ELSE PRINT "In welche Richtung??? Bitte deutlicher reden mit mir!" END SELECT CASE "speichereals" OPEN objekt$ FOR OUTPUT AS 1 PRINT #1, "MiniAdvSpielV2" PRINT #1, AktPos% FOR i% = 1 TO nGegenstaende% ' Lage der Gegenstände ebenfalls sichern! PRINT #1, Ort%(i%) NEXT i% CLOSE 1 PRINT "Spielstand gespeichert!" CASE "lade" OPEN objekt$ FOR INPUT AS 1 INPUT#1, Kennung$ IF Kennung$ <> "MiniAdvSpielV2" THEN PRINT "Keine gültige Spielstanddatei!" ELSE INPUT #1, AktPos% FOR i% = 1 TO nGegenstaende% ' Lage der Gegenstände ebenfalls laden! INPUT #1, Ort%(i%) NEXT i% END IF CLOSE 1 CASE "nimm" ' Ab hier beginnt unsere Gegenstandverwaltung! ' Suchen h% = 0 FOR i% = 1 TO nGegenstaende% IF LCASE$(Gegenstand$(i%)) = LCASE$(objekt$) AND Ort%(i%) = AktPos% THEN h% = i% END IF NEXT i% IF h% <> 0 THEN PRINT Gegenstand$(h%); " aufgelesen" Ort%(h%) = 0 ' Jetzt trägt es der Spieler bei sich ELSE PRINT "Kann in diesem Raum keine(n) "; objekt$; " finden!" END IF CASE "leg" ' Gegenstand ablegen ' Suchen h% = 0 FOR i% = 1 TO nGegenstaende% IF LCASE$(Gegenstand$(i%)) = LCASE$(objekt$) AND Ort%(i%) = 0 THEN h% = i% END IF NEXT i% IF h% <> 0 THEN PRINT Gegenstand$(h%); " abgelegt" Ort%(h%) = AktPos% ' Jetzt liegt der Gegenstand wieder im Raum ELSE PRINT "Ich trage kein "; objekt$; " bei mir!" END IF CASE "inventar" PRINT "Ich trage folgendes bei mir:" h% = 0 FOR i% = 1 TO nGegenstaende% IF Ort%(i%) = 0 THEN PRINT Gegenstand$(i%) h% = h% + 1 END IF NEXT i% IF h% = 0 THEN PRINT "Nichts." ELSE PRINT "Total"; h%; "Gegenstände" END IF CASE "hilfe", "?" PRINT "Mein Befehlswortschatz umfasst folgende Kommandos:" PRINT "gehe nord|süd|west|ost oder Abkürzung n|s|w|o" PRINT "nimm|leg <Gegenstand>" PRINT "inventar|hilfe" PRINT "speichereals|lade <dateiname.erw>" CASE ELSE PRINT "Leider habe ich Ihr Kommando nicht verstanden" PRINT "Probieren Sie's doch noch einmal oder sagen Sie 'hilfe' oder '?' zu mir" END SELECT IF NeuPos% = 0 THEN PRINT "Da kann ich nicht hingehen!" ELSE AktPos% = NeuPos% IF NeuPos% < 0 THEN InSpiel% = 0 ' 0 = FALSE (Boolean) END IF END IF WEND ' Schlussauswertung IF AktPos% = -1 THEN PRINT "Bravo! :-) Sie haben das Mini-Abenteuer bestanden!" IF Ort%(2) <> 0 THEN ' Nur als Demonstration PRINT "Leider haben Sie jedoch vergessen, den Kochtopf mitzunehmen!" END IF ELSE IF AktPos% = -2 THEN PRINT "Leider sind Sie in den Abgrund gestürzt, da der Boden nur aus" PRINT "einer Lage Papier bestand." ELSEIF AktPos% = -3 THEN PRINT "Leider sind Sie zum Teufel gegangen, so dass er Sie mit der Ofengabel" PRINT "entzweigespiesst hat" END IF PRINT "Sie sind tot." END IF
Wichtig ist hierbei, dass Sie den Sinn der Ort%()-Variable genau verstanden haben.
Vorhin haben wir ein hübsches Grundgerüst für ein Abenteuerspiel entworfen. Ein massgebender Faktor für ein erfolgreiches Gelingen Ihres Spielprojekts ist eine gute Vorbereitung mit Papier und Bleistift in Form eines Entwurfs.
Zeichnen Sie alle Räume als lose Kästchen und trägen Sie die Verbindungen durch Türen und Durchgänge als Verbindungslinien an, wobei Sie überall hinschreiben, wie man dort hinkommt (Himmelsrichtung). Ebenso schreiben Sie überall die vorhandenen Gegenstände auf. Nummerieren Sie anschliessend sämtliche Räume von 1 an aufwärts, alle tödlichen Stellen nach Typ mit negativen Zahlen, beispielsweise alle Abgründe -2, alle grünen Fressmonster -3 usw. Ebenso müssen Sie auch die Gegenstände durchnumerieren.
Entwurf des Mini-Abenteuerspiels
Mit diesem »Bauplan« ist es dann eine kleine Routineaufgabe, die
nötige Zustandstabelle sowie die dazugehörigen
DATA
-Zeilen zusammenzustellen, welche die gesamte Topologie der
Räume und Gegenstände beschreiben.
Spezialaktionen wie beispielsweise den Abgrundraum, welchen man nur bei
auf den Boden hingelegtem Brett passieren kann, können Sie mit expliziten
IF
-Bedingungen innerhalb der Game-Engine auslösen:
IF AktPos% = 57 AND Ort%(43) <> 57 THEN .. ' z.B. Spiel Fertig, weil Brett nicht im Raum
Auch Befehle wie Öffne Tür mit Schlüssel
(erweiterter Parser) ist auch kein Problem, in dem Sie einfach weitere
CASE
-Blöcke einfügen.
Für Grafiken eignet sich die BMP-Bibliothek hervorragend, in dem Sie nach Belieben wie unter Verwendung von Bildern aus professionellen Grafikprogrammen beschrieben nach Ihrem Geschmack beispielsweise zu jedem Raum eine Bilddatei erstellen und diese den Räumen zuteilen:
CONST nRaeume% = 4 ' Total vier Räume DIM Raum$(1 TO nRaeume%), Nord%(1 TO nRaeume%), Sued%(1 TO nRaeume%) DIM West%(1 TO nRaeume%), Ost%(1 TO nRaeume%), Bilddatei$(1 TO nRaeume%) FOR i% = 1 TO 4 READ Raum$(i%), Nord%(i%), Sued%(i%), West%(i%), Ost%(i%), Bilddatei$(i%) NEXT i% ' Zustandstabelle ' Raum N S W O .BMP-Dateiname DATA "Keller", -2, 0, 2, -3, "GRAFIK\KELLER" DATA "Badezimmer", 0, 0, 3, 1, "GRAFIK\B_ZIMMER" DATA "Stube", 4, 0, 0, 2, "GRAFIK\STUBE" DATA "Küche", 0, 3, 0, -1, "GRAFIK\KUECHE" DIM BildPuff%(5000)
Diese Grafiken laden Sie unmittelbar vor Gebrauch:
SCREEN 13
WHILE InSpiel%
CLS
LadeBild Bilddatei$(AktPos%), BildPuff%(), LBOUND(BildPuff%)
PUT(10, 10), BildPuff%, PSET
LOCATE 16, 1
PRINT "Sie befinden sich im/in der "; Raum$(AktPos%)
PRINT "Mögliche Gehrichtungen:";
'... (unverändert)
Auch von den Gegenständen können Sie sich solche kleine Grafiken
erstellen, wobei es hier natürlich für einen talentierten
Grafikprogrammierer interessant wird, durch geschickte PUT
(..),..,AND
und PUT (..),..,XOR
den Gegenstand wie ein
transparentes .GIF-Bild einer Web-Seite
hineinzuzeichnen.