Gleichzeitige Ereignisse in einem Echtzeitsystem, bei dem die Verarbeitungsreihenfolge zu unterschiedlichen Ergebnissen führt

7

Ich arbeite an einem Echtzeit-Dungeon-Crawler, der sich auf ein relativ komplexes und flexibles Skill-System konzentriert. Etwas ähnlich wie MMORPGs mit vielen zusammengesetzten Zaubersprüchen, Flächeneffekten, Buffs / Debuffs usw. Ich arbeite an den letzten Subsystemen des Motors.

Ich habe ein Problem mit Aktionen gefunden, die im selben Logikrahmen ausgeführt werden und die Attribute eines Schauspielers ändern.

Nehmen wir zum Beispiel an, zwei Schauspieler verwenden im selben Logik-Tick eine Fertigkeit aufeinander, die 100 Schaden verursacht und einen Debuff hinterlässt, der den verursachten Schaden halbiert.
Wenn ich einfach über jeden Schauspieler iteriere und seine Fähigkeiten verarbeite, verursacht der eine, der früher verarbeitet wird, vollen Schaden, der andere tut dies mit dem entkräfteten Zustand, der nur 50 verursacht. Idealerweise möchte ich, dass beide ähnlich arbeiten. vollen Schaden verursachen und den Debuff verlassen.
Was mich nervt, ist die unbestimmte Natur, die sich aus der Reihenfolge der Iteration ergibt.


Eine Lösung, über die ich nachgedacht habe, bestand darin, alle Änderungen des Statistikwerts und die Buff / Debuff-Anwendung zu verschieben, bis alle Fähigkeiten verarbeitet sind, da dies die einzigen Dinge sind, die das Verhalten von Fähigkeiten beeinflussen können. Im Grunde genommen mache ich eine Schleife über Akteure, verarbeite ihre Fähigkeitsmethoden, stelle aber die Änderungen der Statistik- und Statuseffekte in einer Liste in die Warteschlange.
Sobald ich jede Fähigkeit verarbeitet habe, die ich noch einmal durchlaufe, ändere ich diesmal die tatsächlichen Statistiken von den Werten in der Warteschlange.

Dies behebt das Determinismusproblem, verursacht aber ein anderes.

Um ein anderes Beispiel zu verwenden (da ich den allgemeinen Fall nicht genau
formulieren kann): Nehmen wir an, der Spieler hat 100 Mana und wird von zehn Manaburn-Angriffen in einem Frame getroffen, von denen jeder höchstens 50 Mana brennt und Schaden in Höhe des Manas verursacht verbrannt.
Wenn ich den vorherigen Fix nicht benutze, funktioniert er korrekt, zwei verbrennen die 100 Mana des Spielers, der 100 Schaden verursacht, die restlichen acht tun nichts.

Da sich der Manawert des Spielers noch nicht ändert, landen alle zehn Angriffe und verursachen 500 Schaden, mehr als die Menge an Mana, die der Spieler hat. Dies ist eine noch albernere Situation als zuvor.


Ich bin ein ziemlicher Amateur, ich habe weniger Erfahrung als Witz beim Entwerfen komplexer Systeme. Ist es möglich, ein bestimmtes Verhalten für Aktionen zu bestimmen, die im selben Rahmen stattfinden, und gleichzeitig schlimmere Konsequenzen wie im zweiten Beispiel zu vermeiden?

Ich weiß, dass der Player die Fehler im ersten Fall wahrscheinlich nicht bemerken wird, und ich glaube auch nicht, dass andere Entwickler sich mit diesem Problem beschäftigen.

Ich bin trotzdem neugierig, ist so etwas möglich, ein Wunschtraum, oder habe ich nur eine verzerrte Vorstellung davon, wie die Spielelogik funktionieren soll?

Vielen Dank!

Szoltomi
quelle
1
Sie können das Manaburn-Problem selbst lösen, indem Sie maximalen (50, Mana) Schaden verursachen.
Jmegaffin
Ich stimme zu - das Manaburn-Problem sollte behoben werden, da alles nacheinander ablaufen muss (es ist schließlich ein Computer). Sie können versuchen, denselben Angriff gleichzeitig zu verarbeiten, sodass Sie nicht 10 Manaburn-Angriffe auflösen, sondern einen Manaburn x10-Angriff auflösen und daraus den tatsächlich verursachten Schaden berechnen.
Raven Dreamer
Vielleicht hätte ich mehr betonen sollen, dass der Manaburn-Angriff nur ein Beispiel für eine allgemeine Kategorie von Problemen ist, Fähigkeiten, die sich ändern, aber auch auf einen Wert testen.
Szoltomi
Hoppla, ich habe vergessen, Beiträge den Kommentar einzugeben. Möglicherweise gibt es auch mehr potenzielle Probleme. Eckkoffer saugen. In einem sequentiellen Ansatz ist der Manaburn-Angriff kein Problem, nur mit dem verzögerten Attributwechsel eins. Ich denke derzeit darüber nach, den Rahmen zu unterteilen, in dem Ereignisse als verzögert markiert werden können. Zuerst verarbeite ich jedes Ereignis in einer sequentiellen Reihenfolge und stelle die zurückgestellten Ereignisse / Änderungen, die sie erzeugen, in die Warteschlange. Dann verarbeite ich diese, stelle die zurückgestellten Änderungen in eine Schleife und so weiter in einer Schleife, während (! DeferredEvents.isEmpty ()).
Szoltomi

Antworten:

3

Gute Frage. Ihre beiden Beispiele sind offensichtlich vom Design her nicht kompatibel - entweder sind die Auswirkungen früherer Aktionen bei der Berechnung der Ergebnisse der aktuellen Aktion verfügbar oder nicht.

Zunächst jedoch: Der Satz "Ich arbeite an einem Echtzeit-Dungeon-Crawler" impliziert, dass diese Art von Problem wirklich nicht oft genug auftreten sollte, um ein Problem zu sein. Das Wesentliche an Echtzeit ist, dass die Dinge so schnell ablaufen, dass man leicht akzeptieren kann, dass die andere Person nur Millisekunden schneller als Sie gehandelt hat. Daher ist eine willkürlich aufeinanderfolgende Ausführungsreihenfolge für den Spieler fast nie ein Problem.

Wenn aus irgendeinem Grund jeder dazu neigt, Fähigkeiten mit demselben Tick einzusetzen, können Sie sie einfach mit einem System verteilen, sodass dies viel seltener vorkommt. Machen Sie die Ticks kürzer und häufiger, oder lassen Sie die Charaktere je nach Geschwindigkeit oder Initiativwert auf verschiedene Ticks einwirken. Oder ordnen Sie einfach die Reihenfolge der Logikverarbeitung zufällig an, damit niemand einen dauerhaften Vorteil erhält.

Wenn Sie das Problem jedoch wirklich lösen möchten, besteht eine Möglichkeit darin, das System in Phasen aufzuteilen, eine zur Bewertung gleichzeitiger Aktionen, eine zur Anwendung der Ergebnisse und eine zur Bewertung UND Anwendung sequentieller Aktionen. Auf diese Weise werden die Buffs alle fair aufgelöst und erst wenn dies erledigt ist, werden Effekte wie Manaburn ausgeführt. Im Allgemeinen müssen nur Aktionen sequentiell ausgeführt werden, bei denen das zu ändernde Zeichen dasselbe Zeichen ist, das untersucht wird, da jede Aktion das gesamte Lesen und Schreiben atomar ausführen muss, damit das System einen Sinn ergibt.

Kylotan
quelle
Vielen Dank für die Einsicht, ich habe nicht einmal realisiert, dass Unternehmen einen dauerhaften Vorteil erhalten könnten. Ich implementiere so oder so ein sequentielles System, es ist offensichtlich einfacher und schneller, damit zu arbeiten. Ich werde jedoch über Ihre Lösung nachdenken. Ich habe das Gefühl, dass es zum Beispiel immer noch Probleme mit Schleifen von Zaubersprüchen gibt. Was denkst du darüber? Um ehrlich zu sein, je mehr ich über den gleichzeitigen Ansatz nachdenke, desto mehr potenzielle Aberrationen fallen mir auf.
Szoltomi
1
Ich verstehe nicht, was Sie unter "Schleifen von Zaubersprüchen" verstehen. Keine einzelne Aktion kann beide gewünschten Eigenschaften haben, da sie sich widersprechen. Sie können jedoch auswählen, welche der beiden Aktionen Sie pro Aktion anwenden möchten.
Kylotan
Mit der Abflussschleife meinte ich zum Beispiel, dass Schauspieler A und B eine Mana-Blutegel-Fähigkeit aufeinander anwenden. Ich bedauere es jedoch zu erwähnen, es war eine dumme Idee, die ich nicht einmal klar überlegt habe. Ich hatte eine theoretische Idee, wie man sowohl verzögerte Änderungen an Schauspielern als auch die Verfügbarkeit der aktualisierten Welt kombiniert. Ich werde es in einer Antwort detailliert beschreiben, da es für einen Kommentar etwas lang ist. Edit: Oder wäre ein anderes Medium besser geeignet?
Szoltomi
0

Ich hatte eine theoretische Idee, wie man verzögerte Veränderungen in der Welt mit der Verfügbarkeit der aktualisierten Werte kombiniert. Ich erkannte, dass das Problem hauptsächlich darauf hinausläuft, dass eine definierte Reihenfolge von Ereignissen innerhalb eines Frames auftritt und der Frame in kleinere, aber nicht definierte zeitliche Einheiten unterteilt wird, sowohl in der Länge als auch in der Anzahl.

Die Theorie:
Jedes Ereignis besteht aus einer Reihe einfacher Aktionen, die entweder die Welt abtasten oder sie durch Einfügen neuer Entitäten, Ändern von Attributen oder Anwenden von Effekten ändern. Wie Kylotan betonte, sind Änderungen der Welt, wenn sie sich verzögern, nicht verfügbar, wenn ein Ereignis verarbeitet wird.

Ich dachte darüber nach, eine Art Haltepunkt in ein Ereignis zu setzen, was bedeutet, dass wir den aktualisierten Zustand der Welt brauchen, um fortzufahren. Jedes Segment zwischen Haltepunkten wird nacheinander verarbeitet.

In der Hauptspielschleife erfolgt die Verarbeitung der Ereignisse in einer zweiten Schleife:

1) Jedes Ereignis wird nacheinander bis zum nächsten Haltepunkt verarbeitet.
2) Die Attribute und Statuseffekte der Entitäten werden aktualisiert.

Dies wird wiederholt, bis keine unvollendeten Ereignisse mehr vorhanden sind.

Ich benutze Java, ich habe keine Ahnung, wie dies am saubersten implementiert werden könnte. Eine Idee, die ich hatte, war, die Ereignismethode in einen Schalter zu unterteilen, wobei jedes Segment in einen Fall übergeht.

Dies würde Folgendes übersetzen (Pseudocode)

Event manadrain(){
  player.mana.add(-5); 
  waitWorldUpdate();
  int v = min(target.mana, 10);
  player-mana.add(v);

in:

public void processEvent() {
    switch (segment) {
        case 1:
            player.mana.add(-5);
            return true;
        case 2:
            int v = min(target.mana, 10);
            player - mana.add(v);
            return false;
    }
}

Die Segmentvariable kann intern inkrementiert werden. Der Rückgabewert gibt true zurück, wenn mehr Segmente verarbeitet werden müssen. Die Ereignisse können in einer Liste verarbeitet und entfernt werden, wenn sie false zurückgeben.

Auf diese Weise werden meine beiden Beispiele in der Frage gelöst. Im Beispiel für Schadensdebuff gibt es zwei mögliche Ergebnisse:

  1. Wenn wir den Schaden und die Debuff-Anwendung in einem Segment ausführen, verursachen beide 100 Schaden und erhalten den Debuff.
  2. Wenn wir den Debuff zuerst anwenden, warten Sie auf ein Update und richten Sie dann den Schaden an. Beide verursachen 50 Schaden, wenn der Debuff aktiviert ist.

Das Manadrain-Beispiel ist einfacher, die Ereignisse müssen nicht auseinandergebrochen werden. Da sie nacheinander verarbeitet werden, landen die ersten beiden und verbrennen 2 * 50 Schaden, der Rest schlägt fehl.

Warum sollte eine solche deterministische Lösung wichtig sein?
Wenn beispielsweise zwei ähnliche Entitäten gleichzeitig erstellt werden und sich auf ähnliche Weise wie im Beispiel der ersten Frage gegenseitig beeinflussen, kann es wünschenswert sein, dass sich beide auf dieselbe Weise ändern.

Andererseits, je mehr Zeit ich mit der Arbeit an diesem Problem verbringe, desto mehr potenzielle Aberrationen finde ich. Ganz zu schweigen davon, dass dies wahrscheinlich ein Albtraum wäre, mit dem man debuggen und arbeiten könnte. Ich bin mir nicht mal sicher, ob diese Lösung gut genug ist. Bitte sagen Sie mir, wenn es Fälle gibt, in denen die oben genannte Lösung fehlschlägt.

Szoltomi
quelle
Dies verschiebt lediglich das Problem und macht es komplexer. Jedes zusätzliche Ereignis, das Sie hinzufügen, muss sorgfältig geprüft werden, um sicherzustellen, dass nicht jedes Segment von einem Wert abhängt, den ein anderes Ereignis in diesem Segment ändern kann. z.B. In Anbetracht Ihres Beispiels (und unter der Annahme, dass Sie zweimal denselben Tippfehler in der letzten wichtigen Zeile gemacht haben) kann jedes andere Ereignis, das die Mana-Punktzahl dieses Spielers in Segment 2 liest, falsch sein. Du könnten genügend Segmente hinzufügen, damit jede Variable ihre eigenen erhält, aber ich denke, Sie sollten besser bedient werden, wenn Sie die Ereignisse nacheinander ausführen.
Kylotan