Verhalten in einem einfachen Abenteuerspiel implementieren

11

Ich habe mich in letzter Zeit mit der Programmierung eines einfachen textbasierten Abenteuerspiels unterhalten und bin bei einem scheinbar sehr einfachen Designproblem festgefahren.

Um einen kurzen Überblick zu geben: Das Spiel ist in RoomObjekte unterteilt. Jedes Roomhat eine Liste von EntityObjekten, die sich in diesem Raum befinden. Jedes Entityhat einen Ereignisstatus, bei dem es sich um eine einfache Zeichenfolge-> Boolesche Zuordnung handelt, und eine Aktionsliste, bei der es sich um eine Zeichenfolge-> Funktionszuordnung handelt.

Benutzereingaben haben die Form [action] [entity]. Der Roomverwendet den Entitätsnamen, um das entsprechende EntityObjekt zurückzugeben, das dann den Aktionsnamen verwendet, um die richtige Funktion zu finden, und führt sie aus.

Um die Raumbeschreibung zu generieren, zeigt jedes RoomObjekt seine eigene Beschreibungszeichenfolge an und hängt dann die Beschreibungszeichenfolgen aller Objekte an Entity. Die EntityBeschreibung kann sich je nach Status ändern ("Die Tür ist offen", "Die Tür ist geschlossen", "Die Tür ist verschlossen" usw.).

Hier ist das Problem: Mit dieser Methode gerät die Anzahl der Beschreibungs- und Aktionsfunktionen, die ich implementieren muss, schnell außer Kontrolle. Allein mein Startraum hat ungefähr 20 Funktionen zwischen 5 Entitäten.

Ich kann alle Aktionen in einer einzigen Funktion kombinieren und if-else / durchschalten, aber das sind immer noch zwei Funktionen pro Entität. Ich kann auch bestimmte EntityUnterklassen für allgemeine / generische Objekte wie Türen und Schlüssel erstellen , aber das bringt mich nur so weit.

EDIT 1: Auf Wunsch Pseudocode-Beispiele dieser Aktionsfunktionen.

string outsideDungeonBushesSearch(currentRoom, thisEntity, player)
    if thisEntity["is_searched"] then
        return "There was nothing more in the bushes."
    else
        thisEntity["is_searched"] := true
        currentRoom.setEntity("dungeonDoorKey")
        return "You found a key in the bushes."
    end if

string dungeonDoorKeyUse(currentRoom, thisEntity, player)
    if getEntity("outsideDungeonDoor")["is_locked"] then
        getEntity("outsideDungeonDoor")["is_locked"] := false
        return "You unlocked the door."
    else
        return "The door is already unlocked."
    end if

Beschreibungsfunktionen verhalten sich ähnlich, überprüfen den Status und geben die entsprechende Zeichenfolge zurück.

EDIT 2: Meine Fragestellung überarbeitet. Angenommen, es gibt möglicherweise eine erhebliche Anzahl von Objekten im Spiel, die kein gemeinsames Verhalten (zustandsbasierte Antworten auf bestimmte Aktionen) mit anderen Objekten aufweisen. Gibt es eine Möglichkeit, diese einzigartigen Verhaltensweisen sauberer und wartbarer zu definieren, als eine benutzerdefinierte Funktion für jede entitätsspezifische Aktion zu schreiben?

Eric
quelle
1
Ich denke, Sie müssen erklären, was diese "Aktionsfunktionen" tun, und vielleicht etwas Code posten, weil ich nicht sicher bin, wovon Sie dort sprechen.
Schockieren
Code hinzugefügt.
Eric

Antworten:

5

Anstatt für jede Kombination von Substantiven und Verben eine separate Funktion zu erstellen, sollten Sie eine Architektur einrichten, in der es eine gemeinsame Schnittstelle gibt, die alle Objekte im Spiel implementieren.

Ein Ansatz auf der Oberseite meines Kopfes wäre, ein Entitätsobjekt zu definieren, das alle spezifischen Objekte in Ihrem Spiel erweitern. Jede Entität verfügt über eine Tabelle (unabhängig von der Datenstruktur, die Ihre Sprache für assoziative Arrays verwendet), die unterschiedliche Aktionen mit unterschiedlichen Ergebnissen verknüpft. Die Aktionen in der Tabelle sind wahrscheinlich Strings (z. B. "open"), während das zugehörige Ergebnis sogar eine private Funktion im Objekt sein kann, wenn Ihre Sprache erstklassige Funktionen unterstützt.

Ebenso wird der Status des Objekts in verschiedenen Feldern des Objekts gespeichert. So können Sie beispielsweise ein Array von Dingen in einem Bush haben, und dann wirkt die mit "Suchen" verknüpfte Funktion auf dieses Array und gibt entweder das gefundene Objekt oder die Zeichenfolge "Es war nichts mehr in den Büschen" zurück.

In der Zwischenzeit ist eine der öffentlichen Methoden so etwas wie Entity.actOn (String-Aktion). Vergleichen Sie dann in dieser Methode die übergebene Aktion mit der Aktionstabelle für dieses Objekt. Wenn diese Aktion in der Tabelle enthalten ist, geben Sie das Ergebnis zurück.

Jetzt sind alle verschiedenen Funktionen, die für jedes Objekt benötigt werden, im Objekt enthalten, so dass es einfach ist, dieses Objekt in anderen Räumen zu wiederholen (z. B. das Türobjekt in jedem Raum mit einer Tür zu instanziieren).

Definieren Sie abschließend alle Räume in XML oder JSON oder was auch immer, damit Sie viele eindeutige Räume haben können, ohne für jeden einzelnen Raum einen eigenen Code schreiben zu müssen. Laden Sie diese Datendatei, wenn das Spiel beginnt, und analysieren Sie die Daten, um die Objekte zu instanziieren, die Ihr Spiel füllen. Etwas wie:

<rooms>
  <room id="room1">
    <description>Outside the dungeon you see some bushes and a heavy door over the entrance.</description>
    <entities>
      <bush>
        <description>The bushes are thick and leafy.</description>
        <contains>
          <key />
        </contains>
      </bush>
      <door connection="room2" isLocked="true">
        <description>It's an oak door with stout iron clasps.</description>
      </door>
    </entities>
  </room>

  <room id="room2">
    etc.

ZUSATZ: aha, ich habe gerade die Antwort von FxIII gelesen und dieses Stück gegen Ende sprang auf mich los:

(no things like <item triggerFlamesOnPicking="true"> that you will use just once)

Obwohl ich nicht der Meinung bin, dass das Auslösen einer Flammenfalle nur einmal passieren würde (ich konnte sehen, dass diese Falle für viele verschiedene Objekte wiederverwendet wird), denke ich, dass ich endlich verstehe, was Sie mit Entitäten gemeint haben, die eindeutig auf Benutzereingaben reagieren. Ich würde mich wahrscheinlich mit Dingen wie der Schaffung einer Feuerballfalle für eine Tür in Ihrem Dungeon befassen, indem ich alle meine Entitäten mit einer Komponentenarchitektur baue (an anderer Stelle ausführlich erläutert).

Auf diese Weise wird jede Türentität als ein Bündel von Komponenten konstruiert, und ich kann Komponenten flexibel zwischen verschiedenen Entitäten mischen und anpassen. Zum Beispiel würde die Mehrheit der Türen so etwas wie Konfigurationen haben

<entity name="door">
  <description>It's an oak door with stout iron clasps.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room2" />
  </components>
</entity>

aber die eine Tür mit einer Feuerballfalle wäre

<entity name="door">
  <description>There are strange runes etched into the wood.</description>
  <components>
    <lock isLocked="true" />
    <portal connection="room7" />
    <fireballTrap />
  </components>
</entity>

und dann ist der einzige eindeutige Code, den ich für diese Tür schreiben müsste, die FireballTrap-Komponente. Es würde die gleichen Lock- und Portal-Komponenten wie alle anderen Türen verwenden, und wenn ich mich später entschließen würde, die FireballTrap auf einer Schatzkiste zu verwenden oder etwas, das so einfach ist wie das Hinzufügen der FireballTrap-Komponente zu dieser Truhe.

Ob Sie alle Komponenten im kompilierten Code oder in einer separaten Skriptsprache definieren oder nicht, ist für mich kein großer Unterschied (so oder so werden Sie den Code irgendwo schreiben ), aber das Wichtigste ist, dass Sie den Wert erheblich reduzieren können Menge an eindeutigem Code, den Sie schreiben müssen. Wenn Sie sich keine Gedanken über die Flexibilität von Level-Designern / Moddern machen (Sie schreiben dieses Spiel schließlich selbst), können Sie sogar alle Entitäten von Entity erben lassen und Komponenten im Konstruktor hinzufügen, anstatt eine Konfigurationsdatei oder ein Skript oder was auch immer:

Door extends Entity {
  public Door() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
  }
}

TrappedDoor extends Entity {
  public TrappedDoor() {
    addComponent(new LockComponent());
    addComponent(new PortalComponent());
    addComponent(new FireballTrap());
  }
}
jhocking
quelle
1
Das funktioniert für gängige, wiederholbare Elemente. Aber was ist mit Entitäten, die eindeutig auf Benutzereingaben reagieren? Durch das Erstellen einer Unterklasse von Entitynur für ein einzelnes Objekt wird der Code zusammengefasst, aber die Menge an Code, die ich schreiben muss, wird nicht reduziert. Oder ist das in dieser Hinsicht eine unvermeidliche Gefahr?
Eric
1
Ich habe die Beispiele angesprochen, die Sie gegeben haben. Ich kann deine Gedanken nicht lesen. Welche Objekte und Eingaben möchten Sie haben?
Schock
Ich habe meinen Beitrag bearbeitet, um meine Absichten besser zu erklären. Wenn ich Ihr Beispiel richtig verstehe, sieht es so aus, als ob jedes Entity-Tag einer Unterklasse von entspricht Entityund die Attribute seinen Anfangszustand definieren. Ich vermute, dass untergeordnete Tags der Entität als Parameter für die Aktion fungieren, mit der das Tag verknüpft ist, oder?
Eric
Ja, das ist die Idee.
Schockieren
Ich hätte mir vorstellen sollen, dass Komponenten Teil der Lösung gewesen wären. Danke für die Hilfe.
Eric
1

Das von Ihnen angesprochene Dimensionsproblem ist ganz normal und nahezu unvermeidbar. Sie möchten einen Weg finden, Ihre Entitäten auszudrücken, der sowohl übereinstimmend als auch flexibel ist .

Ein "Container" (der Busch in der schockierenden Antwort) ist ein zufälliger Weg, aber Sie sehen, dass er nicht flexibel genug ist.

Ich empfehle Ihnen nicht, zu versuchen, eine generische Schnittstelle zu finden und dann Konfigurationsdateien zu verwenden, um das Verhalten zu spezifizieren, da Sie immer das unangenehme Gefühl haben , zwischen einem Felsen (Standard- und langweilige Einheiten, leicht zu beschreiben) und einem harten Ort zu sein ( einzigartige fantastische Entitäten, aber zu lang, um sie zu implementieren).

Mein Vorschlag ist , zu verwenden , eine interpretierte Sprache , um Code Verhaltensweisen.

Denken Sie an das Buschbeispiel: Es ist ein Container, aber unser Busch muss bestimmte Gegenstände enthalten. Das Containerobjekt kann Folgendes haben:

  • eine Methode für den Geschichtenerzähler, um Artikel hinzuzufügen,
  • eine Methode für die Engine, um das darin enthaltene Element anzuzeigen,
  • Eine Methode, mit der der Spieler einen Gegenstand auswählen kann.

Einer dieser Gegenstände hat ein Seil, das eine Vorrichtung auslöst, die wiederum eine Flamme abfeuert, die den Busch verbrennt ... (Sie sehen, ich kann Ihre Gedanken lesen, damit ich die Dinge kenne, die Sie mögen).

Sie können ein Skript verwenden, um diesen Busch zu beschreiben, anstatt eine Konfigurationsdatei, die den relevanten zusätzlichen Code in einen Hook einfügt, den Sie jedes Mal aus Ihrem Hauptprogramm ausführen, wenn jemand ein Element aus einem Container auswählt.

Jetzt haben Sie viele Möglichkeiten zur Architektur: Sie können Verhaltenstools als Basisklassen definieren, indem Sie Ihre Codesprache oder die Skriptsprache verwenden (z. B. Container, türähnlich usw.). Der Zweck dieser Dinge ist es, Ihnen zu ermöglichen , die Entitäten zu beschreiben, die einfache Verhaltensweisen einfach aggregieren und sie mithilfe von Bindungen in einer Skriptsprache konfigurieren .

Alle Entitäten sollten für das Skript zugänglich sein: Sie können jeder Entität einen Bezeichner zuordnen und sie in einen Container einfügen, der im Umfang des Skriptskriptskripts exportiert wird.

Mithilfe von Skriptstrategien können Sie Ihre Konfiguration einfach halten ( <item triggerFlamesOnPicking="true">solche Dinge werden Sie nicht nur einmal verwenden), während Sie ungerade (die lustigen) Beaviours ausdrücken können, indem Sie eine Codezeile hinzufügen

In wenigen Worten: Skripte als Konfigurationsdatei, die Code ausführen können.

FxIII
quelle