Ich schreibe eine Computerversion des Spiels Dominion . Es ist ein rundenbasiertes Kartenspiel, bei dem Aktionskarten, Schatzkarten und Siegpunktkarten im persönlichen Deck eines Spielers gesammelt werden. Ich habe die Klassenstruktur ziemlich gut entwickelt und beginne, die Spiellogik zu entwerfen. Ich verwende Python und kann später eine einfache Benutzeroberfläche mit Pygame hinzufügen.
Die Spielreihenfolge der Spieler wird von einem sehr einfachen Automaten gesteuert. Wendet sich im Uhrzeigersinn und ein Spieler kann das Spiel nicht beenden, bevor es vorbei ist. Das Spiel einer einzelnen Runde ist auch eine Zustandsmaschine; Im Allgemeinen durchlaufen die Spieler eine "Aktionsphase", eine "Kaufphase" und eine "Aufräumphase" (in dieser Reihenfolge). Basierend auf der Antwort auf die Frage Wie implementiert man eine rundenbasierte Spiel-Engine? Die Zustandsmaschine ist eine Standardtechnik für diese Situation.
Mein Problem ist, dass sie während der Aktionsphase einer Spielerin eine Aktionskarte verwenden kann, die Nebenwirkungen auf sich selbst oder auf einen oder mehrere der anderen Spieler hat. Beispielsweise kann ein Spieler mit einer Aktionskarte unmittelbar nach Abschluss des aktuellen Zuges einen zweiten Zug ausführen. Eine andere Aktionskarte bewirkt, dass alle anderen Spieler zwei Karten aus ihren Händen ablegen. Eine weitere Aktionskarte bewirkt in der aktuellen Runde nichts, erlaubt es einem Spieler jedoch, in ihrer nächsten Runde zusätzliche Karten zu ziehen. Um die Dinge noch komplizierter zu machen, gibt es häufig neue Erweiterungen des Spiels, die neue Karten hinzufügen. Es scheint mir, dass eine harte Codierung der Ergebnisse jeder Aktionskarte in die Zustandsmaschine des Spiels sowohl hässlich als auch nicht anpassbar wäre. Die Antwort auf die rundenbasierte Strategie-Schleife geht nicht auf eine Detailebene ein, die Entwürfe zur Lösung dieses Problems anspricht.
Welche Art von Programmiermodell sollte ich verwenden, um die Tatsache zu berücksichtigen, dass das allgemeine Muster für das Abwechseln durch Aktionen geändert werden kann, die innerhalb der Abwechslung stattfinden? Sollte das Spielobjekt die Auswirkungen jeder Aktionskarte verfolgen? Oder, wenn die Karten ihre eigenen Effekte implementieren sollen (z. B. durch Implementieren einer Schnittstelle), welches Setup ist erforderlich, um ihnen genügend Leistung zu geben? Ich habe mir ein paar Lösungen für dieses Problem ausgedacht, aber ich frage mich, ob es einen Standardweg gibt, um es zu lösen. Insbesondere würde ich gerne wissen, welches Objekt / welche Klasse / was auch immer dafür verantwortlich ist, die Aktionen zu verfolgen, die jeder Spieler als Folge einer gespielten Aktionskarte ausführen muss, und auch, wie sich dies auf vorübergehende Änderungen in der normalen Reihenfolge von auswirkt die Turn-State-Maschine.
quelle
Antworten:
Ich stimme Jari Komppa zu, dass das Definieren von Karteneffekten mit einer leistungsstarken Skriptsprache der richtige Weg ist. Ich bin jedoch der Meinung, dass der Schlüssel zu maximaler Flexibilität die Skript-fähige Ereignisbehandlung ist.
Damit Karten mit späteren Spielereignissen interagieren können, können Sie eine Skript-API hinzufügen, um bestimmten Ereignissen, z. B. dem Beginn und dem Ende von Spielphasen oder bestimmten Aktionen, die die Spieler ausführen können, "Skript-Hooks" hinzuzufügen. Das heißt, das Skript, das beim Ausspielen einer Karte ausgeführt wird, kann eine Funktion registrieren, die beim nächsten Erreichen einer bestimmten Phase aufgerufen wird. Die Anzahl der Funktionen, die für jede Veranstaltung registriert werden können, sollte unbegrenzt sein. Wenn es mehr als eine gibt, werden sie in der Reihenfolge ihrer Registrierung aufgerufen (es sei denn, es gibt natürlich eine Kernspielregel, die etwas anderes besagt).
Es sollte möglich sein, diese Haken für alle Spieler oder nur für bestimmte Spieler zu registrieren. Ich würde auch vorschlagen, die Möglichkeit für Hooks hinzuzufügen, um selbst zu entscheiden, ob sie weiter angerufen werden sollen oder nicht. In diesen Beispielen wird der Rückgabewert der Hook-Funktion (true oder false) verwendet, um dies auszudrücken.
Ihre Double-Turn-Karte würde dann ungefähr so aussehen:
(Ich habe keine Ahnung, ob Dominion überhaupt so etwas wie eine "Aufräumphase" hat - in diesem Beispiel ist es die hypothetische letzte Phase, in der die Spieler an der Reihe sind.)
Eine Karte, mit der jeder Spieler zu Beginn seiner Ziehphase eine weitere Karte ziehen kann, sieht folgendermaßen aus:
Eine Karte, die den Zielspieler dazu bringt, einen Trefferpunkt zu verlieren, wenn er eine Karte ausspielt, sieht folgendermaßen aus:
Sie werden nicht darum herumkommen, einige Spielaktionen wie das Ziehen von Karten oder das Verlieren von Trefferpunkten hart zu codieren, da ihre vollständige Definition - was genau es bedeutet, eine Karte zu ziehen - Teil der Kernmechanik des Spiels ist. Ich kenne zum Beispiel einige TCGs, bei denen du das Spiel verlierst, wenn du aus irgendeinem Grund eine Karte ziehen musst und dein Deck leer ist. Diese Regel ist nicht auf jeder Karte aufgedruckt, mit der Sie Karten ziehen können, da sie im Regelbuch enthalten ist. Sie sollten also auch nicht bei jedem Kartenskript nach diesem Verlustzustand suchen müssen. Solche Überprüfungen sollten Teil der fest programmierten
drawCard()
Funktion sein (was übrigens auch ein guter Kandidat für ein Hook-fähiges Ereignis wäre).Übrigens: Es ist unwahrscheinlich, dass Sie in der Lage sein werden, vorausschauend zu planen , was in zukünftigen Ausgaben im Dunkeln liegt. Unabhängig davon, was Sie tun, müssen Sie von Zeit zu Zeit neue Funktionen für zukünftige Ausgaben hinzufügen (in diesem Fall) Fall ein Konfetti werfendes Minispiel).
quelle
Ich habe dieses Problem - die flexible computergesteuerte Kartenspiel-Engine - vor einiger Zeit überlegt.
Zunächst einmal würde ein komplexes Kartenspiel wie Chez Geek oder Fluxx (und, glaube ich, Dominion) erfordern, dass Karten skriptfähig sind. Grundsätzlich wird jede Karte mit einer Reihe von Skripten geliefert, die den Status des Spiels auf verschiedene Weise ändern können. Auf diese Weise können Sie das System zukunftssicher machen, da die Skripte möglicherweise Dinge ausführen können, an die Sie derzeit nicht denken können, die aber in Zukunft möglicherweise erweitert werden.
Zweitens kann die starre "Drehung" Probleme verursachen.
Sie benötigen eine Art "Rundenstapel", der die "Spezialrunden" enthält, z. B. "2 Karten abwerfen". Wenn der Stapel leer ist, wird der normale Standardzug fortgesetzt.
In Fluxx ist es durchaus möglich, dass eine Runde ungefähr so abläuft:
..und so weiter und so fort. Das Entwerfen einer Turn-Struktur, die den oben genannten Missbrauch bewältigen kann, kann daher recht schwierig sein. Hinzu kommen die zahlreichen Spiele mit "wann" -Karten (wie in "chez geek"), bei denen die "wann" -Karten den normalen Kartenfluss stören können, indem zum Beispiel die zuletzt gespielte Karte storniert wird.
Im Grunde genommen würde ich damit beginnen, eine sehr flexible Rundenstruktur zu entwerfen, die so gestaltet ist, dass sie als Skript beschrieben werden kann (da jedes Spiel ein eigenes "Masterskript" benötigt, das die grundlegende Spielstruktur handhabt). Dann sollte jede Karte skriptfähig sein. Die meisten Karten machen wahrscheinlich nichts Seltsames, andere dagegen schon. Karten können auch verschiedene Attribute haben - ob sie in der Hand gehalten, "wann immer" gespielt werden können, ob sie als Gut aufbewahrt werden können (wie Fluxx "Keeper" oder verschiedene Dinge in "Chez Geek" wie Essen) ...
Ich habe nie damit begonnen, etwas davon zu implementieren. In der Praxis gibt es also möglicherweise viele andere Herausforderungen. Der einfachste Weg wäre, mit dem System zu beginnen, das Sie implementieren möchten, und es auf skriptfähige Weise zu implementieren, wobei so wenig wie möglich in Stein gemeißelt wird. Wenn also eine Erweiterung folgt, müssen Sie keine Änderungen vornehmen das Basissystem - viel. =)
quelle
Hearthstone scheint Dinge zu tun, die in Beziehung stehen, und ich denke, der beste Weg, um Flexibilität zu erreichen, ist eine ECS-Engine mit datenorientiertem Design. Ich habe versucht, einen Hearthstone-Klon zu erstellen, und es hat sich als unmöglich erwiesen. Alle Randfälle. Wenn Sie mit vielen dieser seltsamen Fälle konfrontiert sind, ist dies wahrscheinlich der beste Weg, dies zu tun. Ich bin allerdings ziemlich voreingenommen, weil ich diese Technik ausprobiert habe.
Bearbeiten: ECS wird je nach gewünschter Flexibilität und Optimierung möglicherweise gar nicht benötigt. Es ist nur eine Möglichkeit, dies zu erreichen. DOD habe ich fälschlicherweise als prozedurale Programmierung angesehen, obwohl sie viel miteinander zu tun haben. Was ich meine ist. Das sollten Sie in Betracht ziehen, zumindest OOP ganz oder größtenteils abzuschaffen und stattdessen Ihre Aufmerksamkeit auf die Daten und deren Organisation zu lenken. Vermeiden Sie Vererbung und Methoden. Konzentrieren Sie sich stattdessen auf öffentliche Funktionen (Systeme), um Ihre Kartendaten zu manipulieren. Jede Aktion ist keine Vorlage oder Logik, sondern nur Rohdaten. Wo Ihre Systeme es dann verwenden, um Logik auszuführen. Ganzzahlschalter oder die Verwendung einer Ganzzahl für den Zugriff auf ein Array von Funktionszeigern helfen dabei, die gewünschte Logik effizient aus den Eingabedaten zu ermitteln.
Grundregeln, die zu befolgen sind, sind, dass Sie vermeiden sollten, Logik direkt mit Daten zu verknüpfen, dass Sie vermeiden sollten, Daten so weit wie möglich voneinander abhängig zu machen (möglicherweise gelten Ausnahmen), und dass, wenn Sie flexible Logik wünschen, die sich nicht erreichbar anfühlt ... Überlegen Sie, ob Sie es in Daten konvertieren möchten.
Das hat Vorteile. Jede Karte kann eine (n) Aufzählung (en) oder eine (n) Zeichenfolge (n) haben, um ihre (n) Aktion (en) darzustellen. Mit diesem Praktikanten können Sie Karten anhand von Text- oder JSON-Dateien entwerfen und vom Programm automatisch importieren lassen. Wenn Sie Spieleraktionen zu einer Datenliste machen, ist dies noch flexibler, insbesondere, wenn eine Karte wie Hearthstone von früherer Logik abhängt oder wenn Sie das Spiel oder eine Wiederholung eines Spiels zu einem beliebigen Zeitpunkt speichern möchten. Es gibt das Potenzial, KI einfacher zu erstellen. Vor allem bei der Verwendung eines "Utility Systems" anstelle eines "Behaviour Tree". Die Vernetzung wird auch einfacher, da nicht mehr herausgefunden werden muss, wie ganze möglicherweise polymorphe Objekte über die Leitung übertragen werden und wie die Serialisierung nachträglich eingerichtet wird. Ihre bereits vorhandenen Spielobjekte sind nichts anderes als einfache Daten, die sich wirklich leicht bewegen lassen. Und nicht zuletzt können Sie auf diese Weise einfacher optimieren, denn anstatt Zeit mit Code zu verschwenden, können Sie Ihre Daten besser organisieren, damit der Prozessor leichter damit umgehen kann. Python kann hier Probleme haben, aber schauen Sie nach "Cache-Zeile" und wie es sich auf Spielentwickler bezieht. Vielleicht nicht wichtig für das Prototyping von Sachen, aber später wird es sich als nützlich erweisen.
Einige nützliche Links.
Hinweis: Mit ECS können Variablen (so genannte Komponenten) zur Laufzeit dynamisch hinzugefügt / entfernt werden. Ein Beispiel c Programm, wie ECS "aussehen" könnte (es gibt eine Menge Möglichkeiten, dies zu tun).
Erstellt eine Reihe von Objekten mit Textur- und Positionsdaten und zerstört am Ende ein Objekt mit einer Texturkomponente, die sich zufällig am dritten Index des Texturkomponenten-Arrays befindet. Sieht schrullig aus, ist aber eine Möglichkeit, Dinge zu tun. Hier ist ein Beispiel, wie Sie alles rendern würden, was eine Texturkomponente enthält.
quelle