Was kann ich tun, um einmalige Markierungen und Überprüfungen im gesamten Code zu vermeiden?

17

Betrachten Sie ein Kartenspiel wie Hearthstone .

Es gibt Hunderte von Karten, die eine Vielzahl von Dingen ausführen, von denen einige sogar für eine einzelne Karte einzigartig sind! Zum Beispiel gibt es eine Karte (genannt Nozdormu), die die Anzahl der Spielerumdrehungen auf nur 15 Sekunden reduziert!

Wie vermeidet man bei einer Vielzahl von möglichen Effekten magische Zahlen und einmalige Überprüfungen im gesamten Code? Wie vermeidet man eine "Check_Nozdormu_In_Play" -Methode in der PlayerTurnTime-Klasse? Und wie kann man den Code so organisieren, dass Sie, wenn Sie noch mehr Effekte hinzufügen , Kernsysteme nicht umgestalten müssen, um Dinge zu unterstützen, die sie noch nie zuvor unterstützt haben?

Sable Träumer
quelle
Ist das wirklich ein Leistungsproblem? Ich meine, mit modernen CPUs kann man in kürzester Zeit
unglaublich
11
Wer hat etwas über Leistungsprobleme gesagt? Das Hauptproblem, das ich sehen würde, ist die ständige Notwendigkeit, den gesamten Code jedes Mal anzupassen, wenn Sie eine neue Karte erstellen.
jhocking
2
Fügen Sie also eine Skriptsprache hinzu und schreiben Sie für jede Karte ein Skript.
Jari Komppa
1
Sie haben keine Zeit, eine richtige Antwort zu geben, aber anstatt den Nozdormu-Check und die 15-Sekunden-Anpassung im Klassencode "PlayerTurnTime" vorzunehmen, der Spielrunden behandelt, können Sie die Klasse "PlayerTurnTime" so codieren, dass sie eine Klasse [class- aufruft, wenn Sie möchten ] Funktion an bestimmten Stellen von außen versorgt. Dann kann der Nozdormu-Kartencode (und alle anderen Karten, die dasselbe Spiel beeinflussen müssen) eine Funktion für diese Anpassung implementieren und diese Funktion bei Bedarf in die PlayerTurnTime-Klasse einfügen. Es kann hilfreich sein, Informationen zu Strategiemustern und Abhängigkeitsinjektionen aus dem klassischen Design Patterns-Buch
Peteris,
2
Irgendwann habe ich frage mich , ob das Hinzufügen Ad-hoc - Kontrollen an die entsprechenden Bits des Codes ist die einfachste Lösung.
user253751

Antworten:

12

Haben Sie sich mit Entitätskomponentensystemen und Event-Messaging-Strategien befasst?

Statuseffekte sollten Komponenten sein, die ihre dauerhaften Effekte in einer OnCreate () -Methode anwenden, ihre Effekte in OnRemoved () verfallen lassen und Spielereignismeldungen abonnieren können, um Effekte anzuwenden, die als Reaktion auf ein Ereignis auftreten.

Wenn der Effekt dauerhaft bedingt ist (für X Runden gültig, aber nur unter bestimmten Umständen), müssen Sie möglicherweise in verschiedenen Phasen nach diesen Bedingungen suchen.

Dann stellen Sie einfach sicher, dass Ihr Spiel auch keine magischen Standardnummern hat. Stellen Sie sicher, dass alles, was geändert werden kann, eine datengesteuerte Variable ist und keine fest programmierten Standardwerte mit Variablen, die für Ausnahmen verwendet werden.

Auf diese Weise nehmen Sie niemals an, wie lang die Drehung sein wird. Es ist immer eine ständig überprüfte Variable, die durch jeden Effekt geändert und möglicherweise später durch den Effekt rückgängig gemacht werden kann, wenn er abläuft. Sie prüfen niemals auf Ausnahmen, bevor Sie nicht Ihre magische Nummer verwenden.

RobStone
quelle
2
Msgstr "Stellen Sie sicher, dass alles, was geändert werden kann, eine datengesteuerte Variable ist und keine fest programmierten Standardwerte mit Variablen, die für Ausnahmen verwendet werden." - Oh, das gefällt mir ziemlich gut. Das hilft sehr, denke ich!
Sable Dreamer
Könnten Sie näher darauf eingehen, "wie sie ihre dauerhaften Auswirkungen haben"? Würde das Abonnieren von turnStarted und das Ändern des Längenwerts den Code nicht fehlerfrei machen und noch schlimmer inkonsistente Ergebnisse liefern (wenn zwischen ähnlichen Effekten interagiert wird)?
Wondra
Nur für Abonnenten, die eine bestimmte Zeitspanne annehmen würden. Sie müssen sorgfältig modellieren. Es kann gut sein, wenn die aktuelle Spielzugszeit von der Spielzugszeit des Spielers abweicht. PTT würde überprüft, um eine neue Kurve zu erstellen. CTT konnte durch Karten überprüft werden. Wenn ein Effekt die aktuelle Zeit erhöhen soll, sollte die Timer-Benutzeroberfläche natürlich folgen, wenn sie zustandslos ist.
RobStone
Um die Frage besser zu beantworten. Nichts anderes speichert die Wendezeit oder irgendetwas, das darauf basiert. Immer darauf achten.
RobStone
11

RobStone ist auf dem richtigen Weg, aber ich wollte es näher erläutern, da ich genau das getan habe, als ich Dungeon Ho !, ein Roguelike, schrieb, das ein sehr komplexes Effektsystem für Waffen und Zaubersprüche hatte.

Mit jeder Karte sollte eine Reihe von Effekten verbunden sein, die so definiert sind, dass sie anzeigen können, um welchen Effekt es sich handelt, worauf sie abzielt, wie und wie lange. Zum Beispiel könnte ein "Schaden des Gegners" -Effekt ungefähr so ​​aussehen.

Effect type: deal damage (enumeration, string, what-have-you)
Effect amount: 20
Source: my weapon
Target: opponent
Effect Cost: 20
Cost Type: Mana

Lassen Sie dann, wenn der Effekt ausgelöst wird, eine allgemeine Routine die Verarbeitung des Effekts durchführen. Wie ein Idiot habe ich eine riesige case / switch-Anweisung verwendet:

switch (effect_type)
{
     case DAMAGE:

     break;
}

Eine weitaus bessere und modularere Methode ist der Polymorphismus. Erstellen Sie eine Effect-Klasse, die alle diese Daten umschließt, erstellen Sie eine Unterklasse für jeden Effekttyp und lassen Sie diese Klasse dann eine für die Klasse spezifische onExecute () -Methode überschreiben.

class Effect
{
    Object source;
    int amount;

    public void onExecute(Object target)
    {
          // Do nothing
    }
}

class DamageEffect extends Effect
{
    public void onExecute(Object target)
    {
          target.health -= amount;
    }
}

Wir hätten also eine Basic-Effect-Klasse und dann eine DamageEffect-Klasse mit einer onExecute () -Methode.

Effect effect = card.getActiveEffect();

effect.onExecute();

Um zu wissen, was im Spiel ist, müssen Sie eine Vektor / Array / verknüpfte Liste / etc. Erstellen. von aktiven Effekten (vom Typ Effekt, Basisklasse), die an ein beliebiges Objekt angehängt sind (einschließlich Spielfeld / "Spiel"). Anstatt zu prüfen, ob ein bestimmter Effekt im Spiel ist, durchlaufen Sie einfach alle Effekte, die an ein Objekt angehängt sind die Objekte und lassen Sie sie ausführen. Wenn ein Effekt keinem Objekt zugeordnet ist, ist er nicht aktiv.

Effect effect;

for (int o = 0; o < objects.length; o++)
{
    for (int e = 0; e < objects[o].effects.length; e++)
    {
         effect = objects[o].effects[e];

         effect.onExecute();
    }
}
Sandalfoot
quelle
Genau so habe ich es gemacht. Das Schöne dabei ist, dass Sie im Wesentlichen ein datengesteuertes System haben und die Logik relativ einfach pro Effekt anpassen können. Normalerweise müssen Sie einige Bedingungsprüfungen in der Ausführungslogik des Effekts durchführen, aber es ist immer noch viel kohärenter, da diese Überprüfungen nur für den betreffenden Effekt gelten.
Manabreak
1

Ich werde eine Handvoll Vorschläge machen. Einige von ihnen widersprechen sich. Aber vielleicht sind einige nützlich.

Betrachten Sie Listen im Vergleich zu Flags

Sie können über die ganze Welt iterieren und bei jedem Gegenstand eine Flagge ankreuzen, um zu entscheiden, ob Sie die Flaggensache ausführen möchten. Oder Sie können eine Liste nur der Elemente führen, die die Flaggensache ausführen sollen.

Berücksichtigen Sie Listen und Aufzählungen

Sie können Ihrer Artikelklasse isAThis und isAThat weiterhin Boolesche Felder hinzufügen. Oder Sie können eine Liste von Zeichenfolgen oder Aufzählungselementen haben, z. B. {“isAThis”, “isAThat”} oder {IS_A_THIS, IS_A_THAT}. Auf diese Weise können Sie der Enumeration (oder den String-Konstanten) neue hinzufügen, ohne Felder hinzuzufügen. Nicht, dass beim Hinzufügen von Feldern wirklich etwas nicht stimmt ...

Betrachten Sie Funktionszeiger

Anstelle einer Liste von Flags oder Aufzählungen könnte eine Liste von Aktionen für dieses Element in verschiedenen Kontexten ausgeführt werden. (Entity-ish ...)

Betrachten Sie Objekte

Einige Leute bevorzugen datengesteuerte oder skriptgesteuerte Ansätze oder Ansätze mit Komponenten. Aber auch altmodische Objekthierarchien sind eine Überlegung wert. Die Basisklasse muss die Aktionen akzeptieren, z. B. "Diese Karte für Phase B spielen" oder was auch immer. Dann kann jede Art von Karte überschreiben und entsprechend reagieren. Es gibt wahrscheinlich auch ein Spielerobjekt und ein Spielobjekt, so dass das Spiel Dinge tun kann wie: if (player-> isAllowedToPlay ()) {mache das Spiel…}.

Betrachten Sie Debug-Fähigkeit

Das Schöne an einem Stapel von Flaggenfeldern ist, dass Sie den Status jedes Elements auf dieselbe Weise prüfen und ausdrucken können. Wenn der Status durch verschiedene Typen oder Beutel mit Komponenten oder Funktionszeigern dargestellt wird oder sich in verschiedenen Listen befindet, reicht es möglicherweise nicht aus, nur die Felder des Elements zu betrachten. Es sind alles Kompromisse.

Letztendlich Refactoring: Betrachten Sie Unit-Tests

Egal wie sehr Sie Ihre Architektur verallgemeinern, Sie können sich Dinge vorstellen, die sie nicht abdeckt. Dann müssen Sie umgestalten. Vielleicht ein bisschen, vielleicht viel.

Eine Möglichkeit, dies sicherer zu machen, sind zahlreiche Unit-Tests. Auf diese Weise können Sie sicher sein, dass die vorhandene Funktionalität auch dann noch funktioniert, wenn Sie die darunter liegenden Elemente (möglicherweise um ein Vielfaches!) Neu angeordnet haben. Jeder Komponententest sieht im Allgemeinen so aus:

void test1()
{
   Game game;
   game.addThis();
   game.setupThat(); // use primary or backdoor API to get game to known state

   game.playCard(something something).

   int x = game.getSomeInternalState;
   assertEquals(“did it do what we wanted?”, x, 23); // fail if x isn’t 23
}

Wie Sie sehen können, ist es für die Unit-Test-Strategie von entscheidender Bedeutung, die API-Aufrufe der obersten Ebene für Spiele (oder Spieler, Karten usw.) stabil zu halten.

David van Rande
quelle
0

Anstatt über jede Karte einzeln nachzudenken, sollten Sie nach Kategorien von Effekten denken, und Karten enthalten eine oder mehrere dieser Kategorien. Um beispielsweise die Zeitdauer eines Spielzugs zu berechnen, können Sie alle im Spiel befindlichen Karten durchlaufen und die Kategorie "Spielzugsdauer ändern" für jede Karte überprüfen, die diese Kategorie enthält. Jede Karte erhöht oder überschreibt dann die Spielzugdauer basierend auf den von Ihnen festgelegten Regeln.

Dies ist im Wesentlichen ein Minikomponentensystem, bei dem jedes "Karten" -Objekt einfach ein Container für eine Reihe von Effektkomponenten ist.

jhocking
quelle
Da die Karten - und auch die zukünftigen Karten - so gut wie alles können, würde ich erwarten, dass jede Karte ein Skript enthält. Trotzdem bin ich mir ziemlich sicher, dass dies kein echtes Leistungsproblem ist.
Jari Komppa
4
Wie aus den Hauptkommentaren hervorgeht: Niemand (außer Ihnen) hat etwas über Leistungsprobleme gesagt. Was das vollständige Skripting als Alternative angeht, erläutern Sie dies in einer Antwort.
jhocking