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 Room
Objekte unterteilt. Jedes Room
hat eine Liste von Entity
Objekten, die sich in diesem Raum befinden. Jedes Entity
hat 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 Room
verwendet den Entitätsnamen, um das entsprechende Entity
Objekt 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 Room
Objekt seine eigene Beschreibungszeichenfolge an und hängt dann die Beschreibungszeichenfolgen aller Objekte an Entity
. Die Entity
Beschreibung 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 Entity
Unterklassen 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?
Antworten:
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:
ZUSATZ: aha, ich habe gerade die Antwort von FxIII gelesen und dieses Stück gegen Ende sprang auf mich los:
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
aber die eine Tür mit einer Feuerballfalle wäre
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:
quelle
Entity
nur 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?Entity
und 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?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:
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ügenIn wenigen Worten: Skripte als Konfigurationsdatei, die Code ausführen können.
quelle