Wie kann ich viele verschiedene Angriffstypen entwerfen, die kombiniert werden können?

17

Ich mache ein Top-Down-2D-Spiel und möchte viele verschiedene Angriffsarten haben. Ich möchte die Angriffe sehr flexibel und kombinierbar machen, wie die Bindung von Isaac funktioniert. Hier ist eine Liste aller Sammlerstücke im Spiel . Um ein gutes Beispiel zu finden, schauen wir uns den Gegenstand Spoon Bender an .

Spoon Bender gibt Isaac die Möglichkeit, Tränen in die Augen zu schießen.

Wenn Sie sich den Abschnitt "Synergien" ansehen, werden Sie feststellen, dass er mit anderen Sammlerstücken kombiniert werden kann, um interessante und dennoch intuitive Effekte zu erzielen. Wenn es zum Beispiel mit The Inner Eye kombiniert wird , wird es "Isaac ermöglichen, mehrere Homing-Shots gleichzeitig abzufeuern". Das macht Sinn, weil das innere Auge

Gibt Isaac einen dreifachen Schuss

Was ist eine gute Architektur, um solche Dinge zu entwerfen? Hier ist eine Brute-Force-Lösung:

if not spoon bender and not the inner eye then ...
if spoon bender and not the inner eye then ...
if not spoon bender and the inner eye then ...
if spoon bender and the inner eye then ...

Aber das wird sehr schnell außer Kontrolle geraten. Wie lässt sich ein solches System besser entwerfen?

Daniel Kaplan
quelle
Persönlich würde ich einfach alle ausgerüsteten Gegenstände in einer Liste halten und sie eine Art gemeinsames Interface implementieren lassen, das ein Objekt, das Sie mutieren, von Objekt zu Objekt nimmt. "für jeden Gegenstand den geplanten Angriff modifizieren", so dass ein Gegenstand die Anzahl der Projektile verdoppelt, einer den Farbton hinzufügt und den Schaden ändert (wenn also ein Gegenstand rote und ein anderer gelbe Blitze macht, würde man nach beiden Modifikationen des Angriffs einen orangefarbenen Angriff haben) . Sie könnten auch nur eine allgemeine Elementklasse haben, die Parameter enthält, um zu entscheiden, wie sie den geplanten Angriff modifiziert.
Benjamin Danger Johnson
2
Ich möchte darauf hinweisen, dass Kirby 64 den Brute-Force-Ansatz gewählt und verschiedene Effekte für alle möglichen Kombinationen von Fähigkeiten fest programmiert hat, sodass dies machbar ist.
Kevin - Reinstate Monica

Antworten:

16

Sie müssen keine Kombinationen von Hand codieren. Sie können sich stattdessen auf die Eigenschaften konzentrieren, die Ihnen die einzelnen Elemente bieten. Zum Beispiel Artikel A setzt Projectile=Fireball,Targetting=Homing. Gegenstand B setzt FireMode=ArcShot,Count=3. Die ArcShotLogik ist für das Aussenden der CountAnzahl von ProjectileElementen in einem Bogen verantwortlich.

Diese beiden Elemente können mit anderen Elementen kombiniert werden, die diese (oder andere) Eigenschaften frei ändern. Wenn Sie einen neuen Projektiltyp hinzufügen, funktioniert dieser automatisch ArcShot, und wenn Sie einen neuen Schussmodus hinzufügen, funktioniert dieser automatisch mit FireballProjektilen. Ebenso Targettingist eine Eigenschaft, mit der die Steuerung für die Projektile festgelegt wird, während FireModedie Projektile erstellt werden, so dass sie in jeder sinnvollen Kombination einfach und trivial kombiniert werden können.

Sie können auch Eigenschaftsabhängigkeiten und dergleichen festlegen. ArcShotErfordert zum Beispiel, dass Sie einen Anbieter von haben Projectile(was möglicherweise nur der Standard ist). Sie können Prioritäten festlegen, damit bei zwei aktiven Elementen Projectileder Code weiß, welches verwendet werden soll. Sie können auch eine Benutzeroberfläche bereitstellen, über die der Benutzer auswählen kann, welchen Projektiltyp er verwenden möchte, oder den Player einfach dazu auffordern, nicht benötigte Gegenstände mit hoher Priorität oder den neuesten Gegenstand usw. auszurüsten. Sie können außerdem ein System von Inkompatibilitäten zulassen ZB so, dass zwei Gegenstände, die beide nur modifizieren, Projectilenicht gleichzeitig ausgerüstet werden können.

Im Allgemeinen ziehen Sie, wenn möglich, jede Art von datengesteuertem Ansatz (oder deklarativem Ansatz ) prozeduralen Ansätzen (dem großen Durcheinander) vor, wenn es um die Objekte und dergleichen in Ihrem Spiel geht. Übergeordnete generische Logik, die durch einfache Daten konfiguriert werden kann, ist hartcodierten Listen mit Regeln mit Sonderfällen weit vorzuziehen.

Sean Middleditch
quelle
Kleiner Fehler, aber Sie scheinen nicht die richtigen Eigenschaften für die von Ihnen verwendeten Beispiele zu haben. "Spoon Bender" fügt Homing hinzu und "Inner Eye" fügt Triple Shot hinzu. Keiner fügt einen Bogen hinzu und beide sind Tränen. Wenn Sie in Ihrem Beispiel beliebige Eigenschaften verwenden, um das Design zu abstrahieren, ist es einfacher zu lesen, wenn sie nicht irreführend benannt wurden. Ich würde "Item A" und "Item B" dem vorziehen.
Daniel Kaplan
1
@tieTYT: Ich habe die Namen verallgemeinert und das Beispiel erweitert, um Zielmodi zusätzlich zu Projektiltypen und Schussmodi einzuschließen. Spielte nie länger als ein paar Minuten BoI, daher habe ich die Namen nicht so verinnerlicht wie andere. :)
Sean Middleditch
Es scheint mir, dass der schwierige Teil darin besteht, die Kategorien von Eigenschaften zu identifizieren. ZB: Targeting ist eines, FireMode ist ein anderes, Count kann ein drittes sein ... oder auch nicht. Vielleicht könnten 3 Geschosse ein Feuerball und 5 Granaten sein, obwohl es sich um eine Waffe handelt.
Daniel Kaplan
@ tieTYT: absolut. Sie können das System natürlich weiter ausbauen, um spezielle Kombinationen oder Logik zuzulassen, aber dies sollte eher die Ausnahme als die Regel sein. Optimieren Sie für den allgemeinen Fall, nicht für den Eckfall.
Sean Middleditch
Hier ist ein großartiges Video darüber, warum Sie sich in Bezug auf die prozedurale Programmierung irren youtu.be/QM1iUe6IofM , und die datengetriebene Art der Beschreibung ordnet alle if / else-Blöcke einzelnen Datensätzen zu, was im Wesentlichen nichts dazu beiträgt , die Anzahl der erforderlichen Sonderfallszenarien zu verringern programmieren, wie Sie sie alle programmieren müssen ...
RenaissanceProgrammer
8

Wenn Sie eine OOP-Sprache verwenden, ist dies ein guter Ort, um das Decorator-Pattern zu verwenden . Wenn Sie ändern möchten, wie ein Angriff abläuft, verzieren Sie ihn einfach mit der entsprechenden Erweiterung.

Rohes c ++ Beispiel:

class AttackBehaviour
{
    /* other code */
    virtual void Attack(double angle);
};

class TearAttack: public AttackBehaviour
{
    /* other code */
    void Attack(double angle);
};

class TripleAttack: public AttackBehaviour
{
    /* other code */
    AttackBehaviour* baseAttackBehaviour;
    void Attack(double angle);
};

void TripleAttack::Attack(angle)
{
    baseAttackBehaviour->Attack(angle-30);
    baseAttackBehaviour->Attack(angle);
    baseAttackBehaviour->Attack(angle+30);
}

Diese Methode ist am besten geeignet, wenn Sie eine sehr große Anzahl von Angriffen ausführen und diese sich alle mehr oder weniger gleich verhalten müssen. Wenn Sie die Art und Weise, wie der Angriff mit dem Modifikator abläuft, grundlegend ändern möchten (z. B. neue Animation mit Modifikator), ist diese Methode nicht für Sie geeignet.

Nautische Meile
quelle
Wenn ich die Animation ändern möchte, warum ist das nichts für mich? Was ist auch, wenn ich in einer funktionaleren Sprache arbeite? Kennen Sie ein Designmuster, das zu ihnen passt?
Daniel Kaplan
Es ist starrer, weil der Modifikator immer die AttackMethode des Objekts aufruft, das er aggregiert. Die TripleAttackKlasse sollte nichts über die TearAttackKlasse wissen . Wenn dies wahr wäre, würde es genauso viele Kopfschmerzen verursachen wie die else-ifBlockade. Dies bedeutet, dass sich Tränenanimationen im TearAttackBehaviourObjekt befinden müssen. Dieses Objekt weiß (und sollte) nicht, dass es von einem TripleAttackObjekt dekoriert wurde . Das Ergebnis ist, dass die 3 Tränenanimationen unabhängig voneinander ablaufen, da sie unabhängig voneinander sind .
NauticalMile
Es fällt mir schwer, das in Worten zu erklären. Wenn jemand anders es versuchen will, sei mein Gast.
NauticalMile
Um dies in einer funktionaleren Sprache umzusetzen, werde ich eine Weile darüber nachdenken und meine Antwort ändern, wenn ich bereit bin.
NauticalMile
1

Als Fan von Binding of Isaac habe ich mich auch gefragt, wie ich so etwas machen soll. Das System im Spiel ist robust genug, um aufkommende Verhaltensweisen durch die Kombination von Effekten hervorzurufen (das, was mir einfällt, ist Spiegel, Löffelbiegung und einige Reichweitenverstärker, die zu einer wirbelnden, tränenreichen Wand um Isaac im Magneto-Stil führen ). Die bloße Anzahl von ihnen würde einen "Wenn" -Block unpraktisch machen.

Mein Fazit ist, dass Isaac und seine Tränen zwei Einheiten sind, die sich im Zentrum eines massiven Component-Entity-Frameworks befinden . Die Entitäten haben einige grundlegende Werte (Bewegungsgeschwindigkeit, Leben, Reichweite, Schaden, Sprite usw.) und jede Komponente würde einen Stat-Modifikator und ein Verb mitbringen.

Im Code hätten Isaac und seine Tränen jeweils eine Liste, die Dinge einer Schnittstelle enthalten würde. Isaac würde eine Liste von Dingen haben, die IsaacMutator abonnieren, und seine Tränen zerreißen Mutator. IsaacMutator hätte Funktionen, um Isaacs Gesundheit, Geschwindigkeit, Reichweite, Aussehen und ein spezielles Verb zu ändern. TearMutator wäre ähnlich. Einmal pro Spielschleife würde Isaac alle IsaacMutators durchlaufen, die er hat, und auch alle lebenden Tränen. Um nach Ihrem englischen Beispiel zu gehen, würde es wie folgt lauten:

Isaac has IsaacMutators:
--spoonbender which gives no stat change and: Tears are made homing
--MeatEater which give +1 health, +1 damage and: nothing
--MagicMirror which gives no stat change and: Tears are made reflecting

Tears have tearMutators:
--(depends on MeatEater) +1 damage and: nothing
--(depends on MagicMirror) no stat change and: +1 vector towards isaac
--(depnds on spoonbender) no stat change and: +1 vector towards enemytype

und so weiter. Da die Typen additiv sind, können Sie Inhalte stapeln, hinzufügen und entfernen.

Kirbinator
quelle
-5

Ich denke, dein Weg funktioniert am besten. Diese Art von Elementen geben jeweils eine Bedingung an. Wenn sie zusammen eine andere Bedingung ergeben, müssten alle drei möglichen Bedingungen definiert werden.

Sie können auch eine neue Art von Definition erstellen, wenn beide Elemente vorhanden sind. Dies trägt jedoch zur Konvolution bei:

if spoon bender and the inner eye then new spoon bender inner eye

if spoon bender inner eye then ...
RenaissanceProgrammierer
quelle