Spielzustand-Automaten

Aus Spieleprogrammierer-Wiki
(Unterschied zwischen Versionen)
Wechseln zu: Navigation, Suche
[gesichtete Version][gesichtete Version]
K (OnInit -> Initialize)
 
(42 dazwischenliegende Versionen von 4 Benutzern werden nicht angezeigt)
Zeile 2: Zeile 2:
 
[[Kategorie:C++]]
 
[[Kategorie:C++]]
 
[[Kategorie:C-Sharp]]
 
[[Kategorie:C-Sharp]]
[[Kategorie:Game-Design]]
+
[[Kategorie:Java]]
 +
[[Kategorie:Spiellogik]]
 
[[Kategorie:Für Fortgeschrittene]]
 
[[Kategorie:Für Fortgeschrittene]]
 
Jeder Spieleentwickler beschäftigt sich früher oder später mit der Frage, wie man am besten mit den verschiedenen Phasen eines Spiels umgeht. Allein wenn man beim Planen schon voraus denkt, fällt einem eine logische Aufteilung auf: Hauptmenü, Charaktererstellung, Hauptspiel, Optionsmenü, Multiplayer-Einstellungen, usw. Zwischen diesen verschiedenen Teilen des Spiels soll natürlich auch problemlos gewechselt werden – und das auch in verschiedenen Kombinationen. Für dieses Problem gibt es eine gute Lösung, welche das Problem der Aufteilung mit Übergangen abstrahiert und abbildet: Spielzustand-Automaten.
 
Jeder Spieleentwickler beschäftigt sich früher oder später mit der Frage, wie man am besten mit den verschiedenen Phasen eines Spiels umgeht. Allein wenn man beim Planen schon voraus denkt, fällt einem eine logische Aufteilung auf: Hauptmenü, Charaktererstellung, Hauptspiel, Optionsmenü, Multiplayer-Einstellungen, usw. Zwischen diesen verschiedenen Teilen des Spiels soll natürlich auch problemlos gewechselt werden – und das auch in verschiedenen Kombinationen. Für dieses Problem gibt es eine gute Lösung, welche das Problem der Aufteilung mit Übergangen abstrahiert und abbildet: Spielzustand-Automaten.
Zeile 8: Zeile 9:
 
== Grundidee und Architektur ==
 
== Grundidee und Architektur ==
  
[[Datei:uml_states.png|miniatur|UML-Klassendiagramm eines Spielzustand-Automaten]]
+
Die Grundidee eines Spielzustand-Automaten ist es, das Spiel in einzelne Spielzustände einzuteilen - einen <tt>State</tt>. Jeder <tt>State</tt> behandelt seinen eigenen Kontext an Daten, Aktualisierungen und Visualisierungen. Diese Zustände werden von einem so genannten Zustands-Manager <tt>StateManager</tt> verwaltet. Diesem wird jeder Spielzustand registriert, so dass er verwendet werden kann. Der Zustands-Manager bietet eine Funktion zum Wechsel eines Zustandes zu einem anderen an. Bei diesem Zustandswechsel können Informationen vom Quell- zum Zielzustand übergeben werden.
  
Die Grundidee der Spielzustand-Automaten lässt sich in zwei Klassen teilen: Den Zustands-Manager <tt>StateManager</tt> und ein Interface <tt>State</tt>. Jedes Spiel definiert eine Menge an Zuständen in Form einer Klasse, die das Interface <tt>State</tt> implementiert. Der <tt>StateManager</tt> kennt und besitzt alle Zustände und identifiziert diese über eine ID. Im Folgenden wird auf beide Klassen näher eingegangen.
+
=== Funktionsweise und Einsatz von Spielzustand-Automaten ===
  
=== State ===
+
Beim Start des Programms wird Der Zustands-Manager erstellt und alle Zustände, die der Automat einnehmen kann, registriert. Danach wird ein Zustand als aktueller festgelegt. Während des Programmablaufs wird der aktuelle Zustand kontinuierlich ausgeführt, wie Logik aktualisieren und Darstellung.
 +
Während des Programmablaufs kann der Zustand selbst entscheiden, ob und in welchen Zustand der Zustands-Manager wechseln und welche Informationen übergeben werden sollen. Dazu ein einfaches Beispielszenario:
  
<tt>State</tt> definiert ein Interface für den <tt>StateManager</tt>. Dieses wird von den konkreten Spielzuständen implementiert. Für einen Spielzustand können abhängig von der Situation unterschiedliche Schnittstellen interessant sein. Grundsätzlich können folgende prinzipiell notwendig sein:
+
Dem Zustands-Manager wurden die Zustände ''Hauptmenü'', ''Level wählen'' und ''Hauptspiel'' registriert. Als aktueller Zustand wird ''Hauptmenü'' gesetzt.
 +
Während des Programmablaufes wird im Zustand ''Hauptmenü'' die Eingaben abgefragt und das Menu im Fenster dargestellt.
 +
Der Zustand erhält die Eingabe, dass ein neues Spiel gestartet werden soll und notifiziert dem Zustands-Manager, dass in den Zustand ''Level wählen'' gewechselt werden soll. Nach dem Wechsel durch den Zustands-Manager wird der Zustand ''Level wählen'' aktualisiert und durch die Wahl eines Levels und Schwierigkeitsgrad wird in den Zustand ''Hauptspiel'' gewechselt.  
 +
Beim Wechsel werden vom Quellzustand die Informationen, welches Level und Schwierigkeitsgrad gewählt worden sind, zum Zielzustand weitergereicht. Dieser Empfängt diese Informationen und kann das Spiel beginnen. Am Ende des Spiels wird wieder in den Zustand Hauptmenü gewechselt.
  
* <tt>'''Initialize'''</tt>: Wird das erste Mal zu einem Zustand gewechselt, so wird <tt>Initialize</tt> aufgerufen. Gelöst ist das, indem die Methode <tt>IsInitialized</tt> am Anfang <tt>false</tt> liefert. Nach dem Aufruf von <tt>Initialize</tt> ist dies nicht mehr der Fall.
+
=== Variante: Stapelverarbeitung von Zuständen ===
* <tt>'''IsInitialized'''</tt>: Diese Methode liefert <tt>true</tt> zurück, wenn der Zustand schon initialisiert ist. Das ist nach <tt>Initialize</tt> der Fall.
+
* <tt>'''OnEnter'''</tt>: Der <tt>StateManager</tt> ruft diese Methode auf, wenn von einem anderen Zustand zu diesem Zustand gewechselt wird. Dadurch können z.B. Variablen zurückgesetzt, Eingangsanimationen gestartet oder ein Musikstück abgespielt werden.
+
* <tt>'''OnLeave'''</tt>: Bei der Einleitung eines Zustandswechsels wird, bevor der neue Zustand gesetzt wird, <tt>OnLeave</tt> vom noch aktuellen Zustand aufgerufen. Dadurch können wichtige Nacharbeiten durchgeführt werden (Musik stoppen, Ressourcen freigeben, ...).
+
* <tt>'''OnUpdate'''</tt>: In dieser Funktion wird die Logik des Zustands berechnet. Im Falle des Zustands ''Spiel'' werden üblicherweise KI, Animationen und Physik aktualisiert.
+
* <tt>'''OnRender'''</tt>: Hier findet das gesamte Zeichnen für den aktuellen Zustand statt.
+
* <tt>'''NextState'''</tt>: Liefert den nächsten Zustand, der bei einem Zustandswechsel gewählt werden soll.
+
  
=== StateManager ===
+
Neben dem simplen Setzen von Zuständen ist die Abfolge von Zustandswechsel ein interessantes Thema. Es bietet sich hier die Benutzung eines Stapels (''Stack'') an. Der <tt>StateManager</tt> besitzt einen Stapel, der eine Menge an Zuständen referenziert. Dabei gilt das [http://de.wikipedia.org/wiki/Last_In_–_First_Out LIFO-Prinzip] (''Last In – First Out''). Mit Hilfe der Methoden <tt>PushState</tt> lässt sich ein neuer Zustand auf den Stapel packen, der auch gleichzeitig der Aktive ist. Das Gegenstück lautet <tt>PopState</tt> und nimmt den zuletzt eingefügten Zustand vom Stapel. Durch dieses Prinzip gestalten sich Zustandsübergänge durch eine Kombination aus <tt>PushState</tt> und <tt>PopState</tt> ziemlich einfach. Hier ein einfaches Beispiel:
  
Der <tt>StateManager</tt> verwaltet alle für das Spiel möglichen Zustände. Dazu werden ihm alle Zustände durch <tt>AddState</tt> bekannt gemacht. Für den <tt>StateManager</tt> ist immer genau ein Zustand aktuell. Dieser lässt sich mit der Methode <tt>SetState</tt> festlegen.
+
Ein Spiel besitzt zwei Zustände: ''Hauptmenü'' und ''Spiel''. Der <tt>StateManager</tt> hat vorerst einen leeren Stapel. Beim Starten des Programmes wird der Zustand ''Hauptmenü'' mit <tt>PushState</tt> als erster Zustand gesetzt. Nun entscheidet sich der Spieler, ein neues Spiel zu starten. Im Programm wird mit <tt>PushState</tt> der Zustand ''Spiel'' auf den Stapel gepackt. Dadurch ist ''Spiel'' der aktuelle Zustand. Nachdem der Spieler sein Spiel gespielt und verloren hat, möchte er das Spiel erneut mit einem anderen Schwierigkeitsgrad spielen und beendet das Spiel, um in das Hauptmenü zu gelangen. Dazu wird <tt>PopState</tt> aufgerufen, wodurch der Zustand ''Spiel'' vom Stapel genommen wird und ''Hauptmenü'' wieder der aktuelle Zustand ist.
  
Prinzipiell verwendet man bei Spielen getrennte Logik für Update- und Render-Funktionalität. Daher unterscheidet der <tt>StateManager</tt> auch zwischen diesen beiden Vorgängen, indem es getrennte Methoden gibt. Diese rufen vom aktuellen Zustand <tt>Update</tt> bzw. <tt>Render</tt> auf.
+
[[Datei:Stack states.png]]
  
== Beispielimplementierung ==
+
==== Transparente Zustände ====
  
=== State ===
+
Es gibt Situationen, in denen mehrere Zustände im Stapel sichtbar sein sollen, einige sozusagen ''transparent'' sind. So könnte beispielsweise das Optionsmenü im Hintergrund noch den darunter liegenden Zustand (Hauptmenü oder das Spiel) durchscheinen lassen. Dies kann gelöst werden, indem jeder Zustand angeben kann, ob er transparent ist. Der Stapel wird dazu von oben nach unten durchlaufen, bis entweder das untere Ende erreicht ist oder ein Zustand gefunden wurde, der nicht transparent ist. Dann werden die Zustände von dieser Stelle an von unten nach oben übereinander gezeichnet, der oberste Zustand also zuletzt.
  
==== In C++ ====
+
=== Technische Umsetzung ===
  
<sourcecode lang=cpp tab=4>class State
+
Um Spielzustand-Automaten umzusetzen, ist es hilfreich, sich stichpunktartig zu überlegen, was alles benötigt wird:
{
+
public:
+
virtual ~State() { };
+
virtual void Initialize() = 0;
+
virtual bool IsInitialized() = 0;
+
virtual void OnEnter() = 0;
+
virtual void OnLeave() = 0;
+
virtual bool OnUpdate(float elapsedTime) = 0;
+
virtual void OnRender() = 0;
+
};</sourcecode>
+
  
==== In C# ====
+
# Zustands-Manager muss vorhanden sein
 +
# Spielzustände erstellen
 +
# Spielzustände dem Zustands-Manager registrieren
 +
# aktuellen Zustand setzen
 +
# Zustände ausführen
 +
# Zustand wechseln mit optionaler Informationsübergabe
  
<sourcecode lang=csharp tab=4>public interface State
+
==== Zustands-Manager muss vorhanden sein ====
{
+
void Initialize();
+
bool IsInitialized { get; }
+
void OnEnter();
+
void OnLeave();
+
bool OnUpdate(float elapsedTime);
+
void OnRender();
+
}</sourcecode>
+
  
=== StateManager ===
+
Bei einer objektorientierten Umsetzung bilden sich eine Klasse und eine Schnittstelle heraus: Die Klasse <tt>StateManager</tt> und die Schnittstelle <tt>State</tt>. Durch diese beiden Elemente sind Punkt 1 und 2 erledigt. Jetzt müssen diese beiden Elemente noch mit Feldern und Methoden bestückt werden, um alle Anforderungen zu erfüllen.
  
==== In C++ ====
+
==== Spielzustände dem Zustands-Manager registrieren ====
 +
 
 +
Das lässt sich mit Hilfe einer öffentlichen Methode der Klasse <tt>StateManager</tt> realisieren. Die könnte <tt>RegisterState</tt> heißen und erhält als Parameter eine Instanz auf ein Objekt, welches die Schnittstelle <tt>State</tt> implementiert und eine Zeichenkette als Name des registrierten Zustandes. Über diesen Namen lässt sich der Zustand im Spiel identifizieren.
 +
 
 +
==== Aktuellen Zustand setzen ====
 +
 
 +
Auch dafür bietet sich eine öffentliche Methode an, die <tt>SetCurrentState</tt> heißen könnte. Diese erhält eine Zeichenkette mit dem Namen eines registrierten Zustandes. Der Zustands-Manager sucht dann nach einem Zustand mit diesem Namen und setzt ihn als aktuellen Zustand. Das merken lässt sich durch anlegen eines privaten Feldes der Klasse <tt>StateManager</tt> realisieren. Das Feld könnte <tt>currentState</tt> heißen.
 +
 
 +
==== Zustände ausführen ====
 +
 
 +
Der aktuelle Zustand muss natürlich aktualisiert und gerendert werden. Üblicherweise legt man dafür auch getrennte öffentliche Methoden an, die zum Beispiel <tt>UpdateState</tt> und <tt>RenderState</tt> heißen könnten. Abhängig von verwendeten Bibliotheken werden hier eventuell noch Parameter benötigt.
 +
 
 +
Zusätzlich muss das <tt>State</tt>-Interface
 +
 
 +
[[Datei:uml_states.png|miniatur|UML-Klassendiagramm eines Spielzustand-Automaten]]
 +
 
 +
== Beispielimplementierung ==
  
==== In C# ====
+
TODO: erst, wenn die Theorie festgehalten ist.
  
 
== Vor- und Nachteile ==
 
== Vor- und Nachteile ==
 +
Ein Vorteil des Zustandsautomats ist, dass einzelne Zustände an mehreren Stellen des Spiels verwendet werden können. Dies könnten die Zustände für Menüs sein, wie ein Optionsmenü oder die Menüs für das Speichern und Laden des Spielstands. Mittels des Zustandsautomaten kann auf einfache Art eine Trennung der einzelnen Spielzustände vorgenommen werden, wie beispielsweise dem Hauptmenü, dem Spiel selbst, einzelnen Menüs oder Unterzustände des Spielgeschehens, wie einem Pausenmenü. Durch diese Trennung ist es einfacher möglich, die Übergänge zwischen den Zuständen sauberer zu implementieren.
  
 
== Erweiterungsmöglichkeiten ==
 
== Erweiterungsmöglichkeiten ==

Aktuelle Version vom 19. Juli 2013, 12:30 Uhr

Klicke hier, um diese Version anzusehen.

Meine Werkzeuge
Namensräume
Varianten
Aktionen
Navigation
Werkzeuge