Die Datenstruktur der Aktionsliste Geeignet für Benutzeroberfläche, KI, Animationen und mehr

Das Aktionsliste ist eine einfache Datenstruktur, die für viele verschiedene Aufgaben in einer Spiel-Engine nützlich ist. Man könnte argumentieren, dass die Aktionsliste immer anstelle einer Form von Zustandsmaschine verwendet werden sollte.

Die gebräuchlichste (und einfachste) Form der Verhaltensorganisation ist a finite state machine. Normalerweise implementiert mit Switches oder Arrays in C oder C ++ oder Schwankungen von ob und sonst Aussagen in anderen Sprachen, Zustandsmaschinen sind starr und unflexibel. Die Aktionsliste ist ein stärkeres Organisationsschema, da sie auf klare Weise modelliert, wie Dinge normalerweise in der Realität passieren. Aus diesem Grund ist die Aktionsliste intuitiver und flexibler als eine Zustandsmaschine.


Schneller Überblick

Die Aktionsliste ist nur ein Organisationsschema für das Konzept von a zeitgesteuerte Aktion. Aktionen werden in einer FIFO-Reihenfolge gespeichert. Dies bedeutet, dass beim Einfügen einer Aktion in eine Aktionsliste die zuletzt in die Vorderseite eingefügte Aktion die erste ist, die entfernt wird. Die Aktionsliste folgt nicht explizit dem FIFO-Format, bleibt aber im Kern das gleiche.

Bei jeder Spielschleife wird die Aktionsliste aktualisiert und Jede Aktion in der Liste wird der Reihe nach aktualisiert. Sobald eine Aktion abgeschlossen ist, wird sie aus der Liste entfernt.

Ein Aktion ist eine Art aufzurufende Funktion, die irgendwie irgendwie funktioniert. Hier sind ein paar verschiedene Arten von Bereichen und die Arbeit, die Aktionen in ihnen ausführen könnten:

  • Benutzeroberfläche: Anzeigen von kurzen Sequenzen wie "Errungenschaften", Abspielen von Animationssequenzen, Durchblättern von Fenstern, Anzeigen von dynamischem Inhalt: Bewegen; drehen; Flip; verblassen; allgemeines Tweening.
  • Künstliche Intelligenz: Warteschlangenverhalten: Bewegen; warten; patrouillieren; fliehen; Attacke.
  • Level Logik oder Verhalten: Plattformen verschieben; Hindernisbewegungen; Ebenen verschieben.
  • Animation / Audio: Abspielen; halt.

Geringfügige Dinge wie Pfadfindung oder Flockung werden mit einer Aktionsliste nicht effektiv dargestellt. Kampf und andere hochspezialisierte, spielspezifische Gameplay-Bereiche sind auch Dinge, die man über eine Aktionsliste wahrscheinlich nicht implementieren sollte.


Aktionsliste Klasse

Hier ist ein kurzer Blick auf die Datenstruktur der Aktionsliste. Bitte beachten Sie, dass genauere Details später im Artikel folgen werden.

 Klasse ActionList public: void Update (float dt); void PushFront (Aktion * Aktion); PushBack ungültig machen (Aktion * Aktion); void InsertBefore (Aktion * Aktion); void InsertAfter (Aktion * Aktion); Aktion * Entfernen (Aktion * Aktion); Aktion * Beginnen (nichtig); Aktion * Ende (nichtig); bool IsEmpty (void) const; float TimeLeft (void) const; bool IsBlocking (void) const; privat: Float-Dauer; Float timeElapsed; Float PercentDone; bool blockieren; unsignierte Gassen; Aktion ** Aktionen; // kann ein Vektor oder eine verknüpfte Liste sein;

Es ist wichtig anzumerken, dass die tatsächliche Speicherung jeder Aktion keine tatsächliche verknüpfte Liste sein muss - etwa das C++ std :: vector würde perfekt funktionieren. Ich persönlich bevorzuge es, alle Aktionen in einem Allokator und Linklisten zusammen mit intrusiv verknüpften Listen zu bündeln. Normalerweise werden Aktionslisten in weniger leistungsabhängigen Bereichen verwendet, sodass eine aufwändige datenorientierte Optimierung beim Entwickeln einer Aktionslistendatenstruktur wahrscheinlich nicht erforderlich ist.


Die Aktion

Die Crux dieses ganzen Shebangs sind die Handlungen selbst. Jede Aktion sollte vollständig in sich abgeschlossen sein, so dass die Aktionsliste selbst nichts über die internen Elemente der Aktion weiß. Dies macht die Aktionsliste zu einem äußerst flexiblen Werkzeug. Eine Aktionsliste kümmert sich nicht darum, ob sie Benutzeroberflächenaktionen ausführt oder die Bewegungen eines modellierten 3D-Charakters verwaltet.

Eine gute Möglichkeit, Aktionen zu implementieren, ist eine einzige abstrakte Schnittstelle. Einige spezifische Funktionen werden vom Aktionsobjekt für die Aktionsliste verfügbar gemacht. Hier ein Beispiel, wie eine Basisaktion aussehen kann:

 Klasse Aktion public: virtuelles Update (Float dt); virtueller OnStart (void); virtuelles OnEnd (void); bool ist fertiggestellt; bool isBlocking; unsignierte Gassen; Schwimmer verstrichen; Float-Dauer; privat: ActionList * ownerList; ;

Das Am Start() und Am Ende() Funktionen sind hier wesentlich. Diese beiden Funktionen müssen ausgeführt werden, wenn eine Aktion in eine Liste eingefügt wird bzw. die Aktion beendet wird. Diese Funktionen ermöglichen, dass Aktionen vollständig in sich abgeschlossen sind.

Blockieren und Nicht-Blockieren von Aktionen

Eine wichtige Erweiterung der Aktionsliste ist die Möglichkeit, Aktionen als beides zu kennzeichnen Blockierung und nicht blockierend. Der Unterschied ist einfach: Eine Blockierungsaktion beendet die Aktualisierungsroutine der Aktionsliste und es werden keine weiteren Aktionen aktualisiert. Eine nicht blockierende Aktion ermöglicht die Aktualisierung der nachfolgenden Aktion.

Ein einzelner boolescher Wert kann verwendet werden, um zu bestimmen, ob eine Aktion blockiert oder nicht blockiert. Hier ist ein Psuedocode, der eine Aktionsliste demonstriert aktualisieren Routine:

 void ActionList :: Update (float dt) int i = 0; while (i! = numActions) Aktion * Aktion = Aktionen + i; Aktion-> Update (dt); if (action-> isBlocking) break; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (Aktion);  ++ i; 

Ein gutes Beispiel für die Verwendung von nicht blockierenden Aktionen wäre, dass einige Verhalten gleichzeitig ausgeführt werden. Wenn wir beispielsweise eine Reihe von Aktionen zum Laufen und Winken von Händen haben, sollte der Charakter, der diese Aktionen ausführt, in der Lage sein, beide gleichzeitig auszuführen. Wenn ein Feind vor dem Charakter flieht, wäre es sehr dumm, wenn er laufen müsste, dann stoppen und mit den Händen winken, dann weiter rennen.

Wie sich herausstellt, passt das Konzept der Blockierung und Nichtblockierung von Aktionen intuitiv zu den meisten einfachen Verhaltensweisen, die in einem Spiel implementiert werden müssen.


Fallbeispiel

Wir zeigen ein Beispiel dafür, wie eine Aktionsliste in einem realen Szenario aussehen würde. Dies hilft, Intuition zu entwickeln, wie eine Aktionsliste verwendet werden kann und warum Aktionslisten nützlich sind.

Problem

Ein Gegner in einem einfachen Top-Down-2D-Spiel muss hin und her patrouillieren. Immer wenn sich dieser Feind in Reichweite des Spielers befindet, muss er eine Bombe auf den Spieler werfen und seine Patrouille anhalten. Es sollte eine kleine Abklingzeit geben, nachdem eine Bombe geworfen wurde, wo der Feind völlig still steht. Wenn sich der Spieler noch in Reichweite befindet, sollte eine andere Bombe gefolgt von einer Abklingzeit geworfen werden. Wenn sich der Spieler außerhalb der Reichweite befindet, sollte die Patrouille genau dort weitermachen, wo sie aufgehört hat.

Jede Bombe sollte durch die 2D-Welt schweben und sich an die Gesetze der auf Fliese basierenden Physik im Spiel halten. Die Bombe wartet nur, bis der Sicherungszeitgeber abgelaufen ist, und sprengt dann. Die Explosion sollte aus einer Animation, einem Sound und einer Entfernung der Kollisionsbox der Bombe und des visuellen Sprites bestehen.

Das Erstellen einer Zustandsmaschine für dieses Verhalten ist zwar möglich und nicht zu schwer, dauert jedoch einige Zeit. Übergänge von jedem Status müssen von Hand codiert werden. Wenn Sie die vorherigen Zustände speichern, um sie später fortzusetzen, kann dies zu Kopfschmerzen führen.

Aktionsliste Lösung

Glücklicherweise ist dies ein ideales Problem, das mit Aktionslisten gelöst werden kann. Stellen wir uns zunächst eine leere Aktionsliste vor. Diese leere Aktionsliste wird eine Liste von Aufgaben enthalten, die der Feind ausführen muss. Eine leere Liste zeigt einen inaktiven Feind an.

Es ist wichtig, darüber nachzudenken, wie das gewünschte Verhalten in kleine Nuggets unterteilt werden kann. Das erste, was Sie tun müssen, ist das Verhalten der Patrouillen. Nehmen wir an, der Feind sollte eine Distanz entfernt patrouillieren, dann rechts die gleiche Distanz und dann wiederholen.

Hier ist was der Patrouille links Aktion könnte so aussehen:

 Klasse PatrolLeft: public Action virtuelles Update (Float dt) // Bewegen Sie den Feind links Feind-> Position.MoveLeft (); // Zeitgeber bis zum Abschluss der Aktion + + dt; if (abgelaufen> = duration) isFinished = true;  virtueller OnStart (void); // nichts tun virtuelles onEnd (void) // füge eine neue Aktion in die Liste ein List-> Insert (new PatrolRight ());  bool isFinished = false; bool isBlocking = true; Feind * Feind; Floatdauer = 10; // Sekunden bis zum Ende des Floats = 0; // Sekunden;

PatrolRight wird fast identisch aussehen, die Richtungen sind umgedreht. Wenn eine dieser Aktionen in die Aktionsliste des Feindes aufgenommen wird, patrouilliert der Feind tatsächlich links und rechts unendlich.

Hier ist ein kurzes Diagramm, das den Ablauf einer Aktionsliste mit vier Momentaufnahmen des Status der aktuellen Aktionsliste für das Patrouillieren zeigt:

Der nächste Zusatz sollte die Erkennung sein, wenn sich der Spieler in der Nähe befindet. Dies kann mit einer nicht blockierenden Aktion durchgeführt werden, die niemals abgeschlossen wird. Diese Aktion prüft, ob sich der Spieler in der Nähe des Feindes befindet. Wenn dies der Fall ist, wird eine neue Aktion erstellt Wurfbombe direkt vor sich in der Aktionsliste. Es wird auch eine Verzögern Aktion gleich nach dem Wurfbombe Aktion.

Die nicht blockierende Aktion wird dort gespeichert und aktualisiert. Die Aktionsliste aktualisiert jedoch alle nachfolgenden Aktionen. Blockieren von Aktionen (z. B. Patrouillieren) wird aktualisiert und die Aktionsliste wird alle nachfolgenden Aktionen nicht mehr aktualisieren. Denken Sie daran, dass diese Aktion nur dazu dient, um zu sehen, ob sich der Spieler in Reichweite befindet und die Aktionsliste nie verlassen wird!

So könnte diese Aktion aussehen:

 class DetectPlayer: public Action virtuelles Update (Float dt) // Wirf eine Bombe und pausiere, wenn der Spieler in der Nähe ist, wenn (PlayerNearby ()) this-> InsertInFrontOfMe (new ThrowBomb ()); // Pause für 2 Sekunden this-> InsertInFrontOfMe (new Pause (2.0));  virtueller OnStart (void); // nichts tun virtuelles onEnd (void) // nichts tun bool isFinished = false; bool isBlocking = false; ;

Das Wurfbombe Aktion wird eine Blockierungsaktion sein, die dem Spieler eine Bombe zuwirft. Es sollte wahrscheinlich ein folgen ThrowBombAnimation, das blockiert und spielt eine feindliche Animation, aber ich habe es aus Gründen der Prägnanz ausgelassen. Die Pause hinter der Bombe wird für die Animation stattfinden und warten, bevor sie fertig ist.

Schauen wir uns ein Diagramm an, wie diese Aktionsliste beim Aktualisieren aussehen könnte:


Blaue Kreise blockieren Aktionen. Weiße Kreise sind nicht blockierende Aktionen.

Die Bombe selbst sollte ein völlig neues Spielobjekt sein und drei oder mehr Aktionen in ihrer eigenen Aktionsliste haben. Die erste Aktion ist eine Blockierung Pause Aktion. Danach sollte eine Aktion zum Abspielen einer Animation für eine Explosion erfolgen. Das Bomben-Sprite selbst muss zusammen mit der Kollisionsbox entfernt werden. Schließlich sollte ein Explosionseffekt abgespielt werden.

Insgesamt sollten etwa sechs bis zehn verschiedene Arten von Aktionen vorhanden sein, die alle zusammen verwendet werden, um das erforderliche Verhalten zu konstruieren. Das Beste an diesen Aktionen ist, dass sie es können wiederverwendet im Verhalten eines jeden Feindtyps, nicht nur des hier gezeigten.


Mehr zu Aktionen

Aktionsstreifen

Jede Aktionsliste in ihrer aktuellen Form hat eine einzige Fahrbahn in denen Aktionen existieren können. Eine Spur ist eine Folge von zu aktualisierenden Aktionen. Eine Spur kann entweder gesperrt oder nicht gesperrt sein.

Die perfekte Umsetzung der Fahrspuren macht Gebrauch Bitmasken. (Informationen zu Bitmask-Details finden Sie unter Eine kurze Bitmask-Anleitung für Programmierer und auf der Wikipedia-Seite.) Mit einer einzigen 32-Bit-Ganzzahl können 32 verschiedene Spuren erstellt werden.

Eine Aktion sollte eine ganze Zahl haben, um alle verschiedenen Spuren darzustellen, auf denen sie sich befindet. Dadurch können 32 verschiedene Fahrstreifen verschiedene Kategorien von Aktionen darstellen. Jede Spur kann während der Aktualisierungsroutine der Liste selbst entweder gesperrt oder nicht gesperrt werden.

Hier ist ein kurzes Beispiel für die Aktualisieren Methode einer Aktionsliste mit Bitmaskenbahnen:

 void ActionList :: Update (float dt) int i = 0; vorzeichenlose Spuren = 0; while (i! = numActions) Aktion * Aktion = Aktionen + i; if (lanes & action-> lanes) weiter; Aktion-> Update (dt); if (action-> isBlocking) lanes | = action-> lanes; if (action-> isFinished) action-> OnEnd (); action = this-> Remove (Aktion);  ++ i; 

Dies bietet ein erhöhtes Maß an Flexibilität, da nun eine Aktionsliste 32 verschiedene Arten von Aktionen ausführen kann, wobei zuvor 32 verschiedene Aktionslisten erforderlich wären, um dasselbe zu erreichen.

Verzögerungsaktion

Eine Aktion, die nichts anderes tut, als alle Aktionen um einen bestimmten Zeitraum zu verzögern, ist sehr nützlich. Die Idee ist, alle nachfolgenden Aktionen zu verzögern, bis ein Timer abgelaufen ist.

Die Implementierung der Verzögerungsaktion ist sehr einfach:

 Klasse Verzögerung: public Aktion public: void Update (float dt) abgelaufen + = dt; if (abgelaufen> dauer) isFinished = true; ;

Aktion synchronisieren

Eine nützliche Aktion ist eine, die blockiert, bis sie die erste Aktion in der Liste ist. Dies ist nützlich, wenn einige andere nicht blockierende Aktionen ausgeführt werden, Sie jedoch nicht sicher sind, in welcher Reihenfolge sie abgeschlossen werden sollen synchronisieren action stellt sicher, dass keine vorherigen nicht blockierenden Aktionen ausgeführt werden, bevor Sie fortfahren.

Die Implementierung der Synchronisierungsaktion ist so einfach wie man sich vorstellen kann:

 Klasse Sync: public Aktion public: void Update (float dt) if (ownerList-> Begin () == this) isFinished = true; ;

Erweiterte Funktionen

Die bisher beschriebene Aktionsliste ist ein ziemlich mächtiges Werkzeug. Es gibt jedoch ein paar Ergänzungen, die gemacht werden können, um die Aktionsliste wirklich zum Strahlen zu bringen. Diese sind etwas fortgeschritten, und ich empfehle die Implementierung nicht, es sei denn, Sie können dies ohne großen Aufwand tun.

Messaging

Die Möglichkeit, eine Nachricht direkt an eine Aktion zu senden oder eine Aktion zum Senden von Nachrichten an andere Aktionen und Spielobjekte zuzulassen, ist äußerst nützlich. Dadurch können Aktionen außerordentlich flexibel sein. Oft kann eine Aktionsliste dieser Qualität als eine "arme" Skriptsprache wirken..

Einige sehr nützliche Nachrichten zum Posten einer Aktion können Folgendes umfassen: gestartet; endete; pausierte; wieder aufgenommen abgeschlossen; abgebrochen; verstopft. Die blockierte ist sehr interessant - wenn eine neue Aktion in eine Liste aufgenommen wird, kann sie andere Aktionen blockieren. Diese anderen Aktionen sollten darüber informiert werden und möglicherweise andere Abonnenten über das Ereignis informieren.

Die Implementierungsdetails von Messaging sind sprachspezifisch und eher nicht trivial. Daher werden die Details der Implementierung hier nicht besprochen, da Messaging nicht im Mittelpunkt dieses Artikels steht.

Hierarchische Aktionen

Es gibt verschiedene Möglichkeiten, Hierarchien von Aktionen darzustellen. Eine Möglichkeit ist, zuzulassen, dass eine Aktionsliste selbst eine Aktion in einer anderen Aktionsliste ist. Dies ermöglicht die Erstellung von Aktionslisten, um große Gruppen von Aktionen unter einer einzigen Kennung zusammenzufassen. Dies erhöht die Benutzerfreundlichkeit und macht es einfacher, komplexere Aktionslisten zu entwickeln und zu debuggen.

Eine andere Methode besteht darin, Aktionen zu haben, deren einziger Zweck darin besteht, andere Aktionen unmittelbar vor sich selbst in der Liste der besitzenden Aktionen zu erzeugen. Ich selbst ziehe diese Methode der obigen vor, auch wenn sie etwas schwieriger zu implementieren ist.


Fazit

Das Konzept einer Aktionsliste und ihre Implementierung wurden ausführlich erörtert, um eine Alternative zu starren Ad-hoc-Zustandsmaschinen zu bieten. Die Aktionsliste bietet eine einfache und flexible Möglichkeit, schnell ein breites Spektrum dynamischer Verhaltensweisen zu entwickeln. Die Aktionsliste ist eine ideale Datenstruktur für die Spielprogrammierung im Allgemeinen.