Grundgerüst für ein Abenteuerspiel

Worum geht es?

Im Artikel über Anfängerfehler stellte ich Ihnen eine typisch primitive Variante eines Abenteuerspiels vor, welche wegen den vielen GOTO-Zeilen verschiedene Nachteile aufweist:

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.

Wie ist eigentlich eine Abenteuerspielwelt aufgebaut?

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.

Das Layout des Beispielabenteuers

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:

Entwurf Abenteuerspiel mit Kästchen
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:

Entwurf Abenteuerspiel mit Kästchen nummeriert
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:

Zustandstabelle Abenteuerspiel
aktuelle Pos. \ BefehlNSW O
1 (Keller)-202-3
2 (Badezimmer)0031
3 (Stube)0002
4 (Küche)030-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

Ergänzung einer Spielstandspeicherfunktion

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)

Gegenstände verwalten

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!

Der Parser als Befehlsverarbeiter

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.

Entwurf eines Abenteuerspiels

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.

Kästchen-Diagramm mit Abenteuerspiel-Welt
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.

Grafik-Abenteuerspiel

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.


Wieder zurück zur Übersicht


© 2000 by Andreas Meile