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!
quelle
Antworten:
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.
quelle
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)
in:
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:
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.
quelle