Codierung verschiedener Zustände in Abenteuerspielen

12

Ich plane ein Abenteuerspiel und kann nicht herausfinden, wie das Verhalten eines Levels in Abhängigkeit vom Fortschritt der Geschichte richtig umgesetzt werden kann.

In meinem Einzelspielerspiel gibt es eine riesige Welt, in der der Spieler an verschiedenen Punkten des Spiels mit Menschen in einer Stadt interagieren muss. Abhängig vom Fortschreiten der Geschichte werden dem Spieler jedoch verschiedene Dinge präsentiert, z. Die Türen wurden nur zu bestimmten Tageszeiten nach Beendigung einer bestimmten Routine geöffnet. Verschiedene Cut-Screen- / Trigger-Ereignisse treten erst auf, nachdem ein bestimmter Meilenstein erreicht wurde.

Ich dachte naiv daran, eine switch {} -Anweisung zu verwenden, um zu entscheiden, was der NPC sagen soll oder wo er zu finden ist, und Questziele erst dann interaktiv zu machen, wenn der Zustand einer globalen game_state-Variablen überprüft wurde. Mir wurde jedoch klar, dass ich schnell auf viele verschiedene Spielzustände und Schaltzustände stoßen würde, um das Verhalten eines Objekts zu ändern. Diese switch-Anweisung wäre auch sehr schwer zu debuggen, und ich denke, es könnte auch schwierig sein, sie in einem Level-Editor zu verwenden.

Also dachte ich, anstatt ein einzelnes Objekt mit mehreren Zuständen zu haben, sollte ich vielleicht mehrere Instanzen desselben Objekts mit einem einzelnen Zustand haben. Auf diese Weise kann ich, wenn ich so etwas wie einen Level-Editor verwende, eine Instanz des NPC an allen verschiedenen Orten platzieren, an denen er jemals erscheinen könnte, und auch eine Instanz für jeden Gesprächszustand, den er hat. Aber das bedeutet, dass es eine Menge inaktiver, unsichtbarer Spielobjekte geben wird, die im Level schweben, was für das Gedächtnis problematisch oder in einem Level-Editor einfach schwer zu erkennen sein kann, ich weiß es nicht.

Oder erstellen Sie einfach für jeden Spielstatus ein identisches, aber separates Level. Dies ist die sauberste und fehlerfreieste Art, Dinge zu tun, aber es fühlt sich nach massiver manueller Arbeit an, um sicherzustellen, dass jede Version des Levels wirklich identisch ist.

Alle meine Methoden fühlen sich so ineffizient an. Gibt es eine bessere oder standardisierte Methode, um ein Verhalten auf einer Ebene zu implementieren, das vom Fortschritt der Story abhängt?

PS: Ich habe noch keinen Level-Editor - ich denke darüber nach, etwas wie JME SDK zu verwenden oder mein eigenes zu erstellen.

Cardin
quelle

Antworten:

9

Ich denke, was Sie in diesem Fall brauchen, ist das State Design Pattern . Erstellen Sie eine einzelne Instanz, anstatt mehrere Instanzen jedes Spielobjekts zu haben, aber kapseln Sie sein Verhalten in einer separaten Klasse. Erstellen Sie mehrere Klassen, eine für jedes mögliche Verhalten, und weisen Sie allen Klassen dieselbe Schnittstelle zu. Verknüpfen Sie ein Objekt mit Ihrem Spielobjekt (dem Anfangszustand). Wenn sich die Bedingungen ändern (ein Meilenstein ist erreicht, die Tageszeit verstrichen usw.), wechseln Sie den Zustand des Objekts (dh verknüpfen Sie es mit einem anderen Objekt, abhängig von Ihrer Spiellogik). und aktualisieren Sie gegebenenfalls seine Eigenschaften.

Ein Beispiel dafür, wie eine Statusschnittstelle aussehen würde (vollständig erstellt - nur um die Steuerungsebene zu veranschaulichen, die Ihnen dieses Schema bietet):

interface NPCState {
    Scene whereAmI(NPC o);
    String saySomething(NPC o);
}

Und zwei implementierende Klassen:

class Busy implements NPCState {
    Scene whereAmI(NPC o) {
        return o.getWorkScene();
    }
    String saySomething(NPC o) {
        return "Can't talk now, I'm busy!";
    }
}

class Available implements NPCState {
    Scene whereAmI(NPC o) {
        return TAVERN;
    }
    String saySomething(NPC o) {
        String[] choices = o.getRandomChat();
        return choices[RANDOM.getInt(choices.length)];
    }
}

Und Schaltzustände:

// The time of day passed from "afternoon" to "evening"
NPCState available = new Available();
for ( NPC o : list ) {
    Scene oldScene = o.state.whereAmI(o);
    o.state = available;
    Scene newScene = o.state.whereAmI(o);
    moveGameObject(o, oldScene, newScene);
    ...

Wichtige NPCs haben möglicherweise ihre benutzerdefinierten Status, die Statusauswahllogik ist möglicherweise anpassbarer, und Sie können unterschiedliche Status für verschiedene Facetten des Spiels haben (in diesem Beispiel habe ich eine einzelne Klasse verwendet, um Standort und Chat zu bestimmen, aber Sie können sich trennen sie und machen viele Kombinationen).

Dies funktioniert auch mit Level-Editoren: Sie können ein einfaches Kombinationsfeld verwenden, um den "globalen" Status eines Levels zu ändern. Anschließend können Sie die Spielobjekte so hinzufügen und neu positionieren, wie sie in diesem Status angezeigt werden sollen. Die Spiel-Engine ist nur dafür verantwortlich, das Objekt tatsächlich zur Szene "hinzuzufügen", wenn es den richtigen Status hat - aber die Parameter können weiterhin auf benutzerfreundliche Weise bearbeitet werden.

(Haftungsausschluss: Ich habe wenig praktische Erfahrung mit Spieleditoren, daher kann ich mit Zuversicht sagen, wie professionelle Editoren funktionieren. Mein Standpunkt zum Statusmuster ist jedoch weiterhin gültig. Die Organisation Ihres Codes auf diese Weise sollte sauber, wartbar und kein Abfall sein Ressourcen.)

mgibsonbr
quelle
Sie könnten dieses State-Design-Muster mit dem von mir beschriebenen assoziativen Array kombinieren. Sie können die Statusobjekte wie hier beschrieben codieren und dann mithilfe eines von mir vorgeschlagenen assoziativen Arrays zwischen verschiedenen Statusobjekten wählen.
jhocking
Ich stimme zu, es ist auch gut, das Spiel von seiner Engine zu trennen, und die Hardcoding-Spielelogik stärkt die Kopplung zwischen ihnen (wodurch die Möglichkeit der Wiederverwendung verringert wird). Es gibt jedoch einen Kompromiss, da abhängig von der Komplexität Ihres beabsichtigten Verhaltens, das versucht, "weichen Code" zu verwenden, alles zu unnötigem Durcheinander führen kann . In diesem Fall könnte ein gemischter Ansatz wünschenswert sein (dh eine "generische" Zustandsübergangslogik, die aber auch die Einbindung von benutzerdefiniertem Code
zulässt
So wie ich es verstehe, gibt es eine Eins-zu-Eins-Zuordnung zwischen NPCState und GameState. Dann lege ich NPCs in ein Array und durchlaufe es, wobei ich den neuen NPCState zugebe, wenn sich der Spielstatus ändert. Der NPCState muss in der Lage sein zu wissen, wie er mit jedem Diff-NPC umgeht, der an ihn gesendet wird, sodass der NPCState im Wesentlichen das Verhalten aller NPCs für einen bestimmten Status enthält. Ich mag es, dass alle Verhaltensweisen sauber in einem einzigen NPCState gespeichert werden, was der Implementierung des Spieleditors sauber zuordnet, aber irgendwie macht es den NPCState ziemlich riesig.
Cardin
Oh, ich glaube, ich habe ur ans falsch verstanden. Ich habe es ein wenig geändert, um Beobachter einzubeziehen. Es ist also ein Diff-NPC-Status für jeden Diff-NPC, mit Ausnahme der Super-Generika wie Crowd-NPC, die denselben Status haben können. Für jeden Spielstatus registriert sich der NPC und sein NPCState bei einem Beobachter. Somit weiß der Beobachter genau, welcher NPC registriert ist, um das Verhalten bei welchem ​​Spielstatus zu ändern, und iteriert einfach durch diese. Und auf der Seite des Game Editors muss der Game Editor lediglich ein Signal an den Observer senden, um den Status des gesamten Levels zu ändern.
Cardin
1
Ja, das ist die Idee! Wichtige NPCs werden viele Zustände haben und der Zustandsübergang wird hauptsächlich von den abgeschlossenen Meilensteinen abhängen. Generische NPCs reagieren manchmal auch auf Meilensteine, und ihr gewählter Status hängt sogar von einer internen Eigenschaft ab (nehmen wir an, alle NPCs haben einen standardmäßigen Anfangsstatus. Wenn Sie zum ersten Mal mit einem von ihnen sprechen, stellt er sich vor und gibt dann den folgenden Befehl ein normaler Zustandswechselzyklus).
mgibsonbr
2

Die Auswahl, die ich in Betracht ziehen würde, besteht entweder darin, die einzelnen Objekte auf unterschiedliche Spielzustände reagieren zu lassen oder in unterschiedlichen Spielzuständen unterschiedliche Ebenen zu bedienen. Die Wahl zwischen diesen beiden Optionen hängt davon ab, was genau ich im Spiel tun möchte (was sind die verschiedenen Zustände? Wie wird das Spiel zwischen den Zuständen wechseln? Usw.).

So oder so würde ich es jedoch nicht tun, indem ich die Zustände fest in den Spielcode einkodiere. Anstelle einer massiven switch-Anweisung in NPC-Objekten würde ich anstelle des NPC-Verhaltens, das aus einer Datendatei in ein assoziatives Array geladen wird, dieses assoziative Array verwenden, um ein anderes Verhalten für die zugehörigen Zustände auszuführen.

if (state in behaviors) {
  behaviors[state]();
}
jhocking
quelle
Wäre diese Datendatei eine Art Skriptsprache? Ich denke, eine reine Textdatei reicht möglicherweise nicht aus, um das Verhalten zu beschreiben. Sie haben jedenfalls Recht, dass es dynamisch geladen werden sollte. Ich kann mir nicht vorstellen, einen Spieleditor zu verwenden, um gültigen Java-Code zu generieren. Er muss auf jeden Fall etwas analysiert werden.
Cardin,
1
Nun, das war mein erster Gedanke, aber nachdem ich die Antwort von mgibsonbr gesehen hatte, wurde mir klar, dass Sie die verschiedenen Verhaltensweisen als separate Klassen codieren und dann in der Datendatei einfach angeben können, welche Verhaltensklassen zu welchem ​​Status gehören. Laden Sie diese Daten zur Laufzeit in ein assoziatives Array.
jhocking
Oh ja, das ist definitiv einfacher! : D Verglichen mit dem Szenario der Einbettung von etwas wie Lua haha ​​..
Cardin
2

Wie wäre es mit einem Beobachtermuster, um nach Meilensteinänderungen zu suchen? Wenn eine Änderung eintritt, würde dies eine Klasse erkennen und beispielsweise eine Änderung vornehmen, die an einem NPC vorgenommen werden muss.

Anstelle des genannten State Design Pattern würde ich ein Strategie-Pattern verwenden.

Wenn ein NPC n Möglichkeiten hat, mit dem Charakter und den m Positionen, an denen er sich befinden könnte, zu interagieren, müssen Sie maximal (m * n) +1 Klassen entwerfen. Wenn Sie das Strategiemuster verwenden, erhalten Sie n + m + 1 Klassen, aber diese Strategien können auch von anderen NPCs verwendet werden.

Es könnte also eine Klasse geben, die die Meilensteine ​​handhabt, und Klassen, die diese Klasse beobachten und entweder mit NPCs oder Feinden umgehen, oder was auch immer geändert werden sollte. Wenn die Beobachter auf den neuesten Stand gebracht werden, entscheiden sie, ob sie etwas an den Instanzen ändern müssen, die sie regieren. Die NPC-Klasse zum Beispiel würde im Konstruktor den NPC-Manager informieren, wann er aktualisiert werden muss und was aktualisiert werden muss ...

TOAOGG
quelle
Das Observer-Muster scheint interessant zu sein. Ich denke, es könnte dem NPC sauber überlassen werden, sich beim Staatsbeobachter zu registrieren. Das Strategiemuster fühlt sich stark wie das Trigger- und KI-Verhalten von Unity Engine an, mit dem Verhalten zwischen verschiedenen Spielobjekten geteilt wird (glaube ich). Das klingt machbar. Ich bin mir nicht sicher, was die Vor- / Nachteile im Moment sind, aber dass Unity die gleiche Methode auch verwendet, ist etwas beruhigend, haha ​​..
Cardin
Ich habe diese beiden Muster nur ein paar Mal verwendet, daher kann ich Ihnen nichts über die Nachteile sagen: - / Aber ich denke, es ist nett, wenn Sie eine Verantwortung tragen und jede Strategie testen können :) Es könnte verwirrend werden, wenn Sie Verwenden Sie eine Strategie in vielen verschiedenen Klassen, und Sie möchten jede Klasse finden, die sie verwendet.
TOAOGG
0

Alle angegebenen Ansätze sind gültig. Das hängt von der Situation ab, in der Sie sich gerade befinden. Viele Abenteuer oder MMOs verwenden eine Kombination aus diesen.

Wenn sich beispielsweise durch ein zentrales Ereignis ein großer Teil des Levels ändert (z. B. ein Schuldner Ihre Wohnung aufräumt und jeder darin festgenommen wird), ist es in der Regel einfacher, den gesamten Raum durch einen zweiten Raum zu ersetzen, der nur ähnlich aussieht.

OTOH, wenn Charaktere auf der Karte herumlaufen und verschiedene Dinge an verschiedenen Orten tun, haben Sie oft einen einzigen Akteur, der sich durch verschiedene Verhaltensobjekte dreht (z. B. geradeaus gehen / keine Konversationen vs. hier stehen / Konversation über Mitch's Tod) "versteckt", wenn ihr Zweck erfüllt wurde.

Das heißt, normalerweise sollten Duplikate eines Objekts, das Sie manuell erstellen, keine Probleme verursachen. Wie viele Objekte können Sie erstellen? Wenn Sie mehr Objekte erstellen können, als Ihr Spiel durchlaufen kann, überprüfen Sie deren "versteckte" Eigenschaft und überspringen Sie, Ihre Engine ist zu langsam. Darum würde ich mir also keine Sorgen machen. Viele Online-Spiele machen das tatsächlich. Bestimmte Charaktere oder Gegenstände sind immer vorhanden, werden aber nicht Charakteren angezeigt, die nicht die entsprechende Mission haben.

Sie können die Ansätze sogar kombinieren: Haben Sie zwei Türen in Ihrem Wohnhaus. Man führt in die Wohnung "vor dem Inkasso", man in die Wohnung danach. Wenn Sie den Korridor betreten, wird nur der angezeigt, der für Ihren Verlauf in der Geschichte gilt. Auf diese Weise können Sie nur einen allgemeinen Mechanismus für "Element ist zum aktuellen Zeitpunkt in der Story sichtbar" und eine Tür mit einem einzelnen Ziel einrichten. Alternativ können Sie auch kompliziertere Türen herstellen, bei denen das Verhalten vertauscht werden kann, und eine davon lautet "Zur vollen Wohnung gehen", die andere "Zur leeren Wohnung gehen". Dies mag unsinnig erscheinen, wenn sich wirklich nur das Ziel der Tür ändert, aber wenn sich auch ihr Aussehen ändert (z. B. ein großes Schloss vor der Tür, das Sie zuerst knacken müssen).

Geschicklichkeit
quelle