Eine Zustandsmaschine ist ein Modell, das zur Darstellung und Steuerung des Ausführungsflusses verwendet wird. Es ist ideal für die Implementierung von KI in Spielen und liefert großartige Ergebnisse ohne komplexen Code. Dieses Tutorial beschreibt die Theorie, Implementierung und Verwendung einfacher und stapelbasierter Finite-State-Maschinen.
Alle Icons von Lorc und verfügbar auf http://game-icons.net.
Hinweis: Obwohl dieses Tutorial mit AS3 und Flash geschrieben wurde, sollten Sie in der Lage sein, in fast jeder Spieleentwicklungsumgebung dieselben Techniken und Konzepte anzuwenden.
Eine endliche Zustandsmaschine oder kurz FSM ist ein Berechnungsmodell, das auf einer hypothetischen Maschine basiert, die aus einem oder mehreren Zuständen besteht. Es kann immer nur ein Zustand gleichzeitig aktiv sein. Daher muss die Maschine von einem Zustand in den anderen wechseln, um verschiedene Aktionen ausführen zu können.
FSMs werden häufig verwendet, um einen Ausführungsablauf zu organisieren und darzustellen, der für die Implementierung der KI in Spielen nützlich ist. Das "Gehirn" eines Feindes kann zum Beispiel mit einem FSM implementiert werden: Jeder Staat repräsentiert eine Aktion, wie z Attacke
oder ausweichen
:
Eine FSM kann durch eine Grafik dargestellt werden, wobei die Knoten die Zustände und die Kanten die Übergänge sind. Jede Kante hat eine Beschriftung, die über den Übergang informiert, z Spieler ist in der Nähe
Beschriftung in der Abbildung oben, die angibt, dass die Maschine wechselt wandern
zu Attacke
Wenn der Spieler in der Nähe ist.
Die Implementierung eines FSM beginnt mit den Zuständen und Übergängen, die es haben wird. Stellen Sie sich die folgende FSM vor, die das Gehirn einer Ameise darstellt, die nach Hause geht:
FSM, das das Gehirn einer Ameise darstellt.Der Ausgangspunkt ist der Blatt finden
Zustand, der solange aktiv bleibt, bis die Ameise das Blatt findet. Wenn dies der Fall ist, wird der aktuelle Status in geändert nach Hause gehen
, das bleibt aktiv, bis die Ameise nach Hause kommt. Wenn die Ameise endlich nach Hause kommt, wird der aktive Zustand Blatt finden
wieder, so wiederholt die Ameise ihre Reise.
Wenn der aktive Zustand ist Blatt finden
und der Mauszeiger nähert sich der Ameise, es erfolgt ein Übergang zur weglaufen
Zustand. Während dieser Zustand aktiv ist, läuft die Ameise vom Mauszeiger weg. Wenn der Cursor keine Bedrohung mehr ist, erfolgt ein Übergang zurück zum Blatt finden
Zustand.
Da gibt es Übergänge, die verbinden Blatt finden
und weglaufen
, Die Ameise läuft immer vom Mauszeiger weg, wenn sie sich nähert solange die Ameise das Blatt findet. Das wird nicht passiert, wenn der aktive Zustand ist nach Hause gehen
(siehe untenstehende Abbildung). In diesem Fall geht die Ameise furchtlos nach Hause und wechselt nur zum Blatt finden
Zustand, wenn es nach Hause kommt.
weglaufen
und nach Hause gehen
. Ein FSM kann in einer einzigen Klasse mit dem Namen implementiert und verkapselt werden FSM
zum Beispiel. Die Idee ist, jeden Zustand als Funktion oder Methode unter Verwendung einer aufgerufenen Eigenschaft zu implementieren activeState
in der Klasse, um festzustellen, welcher Status aktiv ist:
öffentliche Klasse FSM private var activeState: Funktion; // zeigt auf die aktuell aktive Statusfunktion public function FSM () public function setState (state: Funktion): void activeState = state; public function update (): void if (activeState! = null) activeState ();
Da jeder Zustand eine Funktion ist, während ein bestimmter Zustand aktiv ist, wird die Funktion, die diesen Zustand darstellt, bei jedem Spielupdate aufgerufen. Das activeState
property ist ein Zeiger auf eine Funktion und zeigt daher auf die Funktion des aktiven Zustands.
Das aktualisieren()
Methode der FSM
Die Klasse muss bei jedem Spielfeld aufgerufen werden, damit sie die von der activeState
Eigentum. Dieser Aufruf aktualisiert die Aktionen des aktuell aktiven Status.
Das setState ()
Die Methode wechselt den FSM in einen neuen Zustand, indem Sie auf die activeState
Eigenschaft auf eine neue Zustandsfunktion. Die State-Funktion muss kein Mitglied von FSM sein. es kann zu einer anderen Klasse gehören, die das macht FSM
Klasse generischer und wiederverwendbar.
Verwendung der FSM
Klasse bereits beschrieben, ist es Zeit, das "Gehirn" eines Charakters zu implementieren. Die zuvor erläuterte Ameise wird von einem FSM verwendet und gesteuert. Das Folgende ist eine Darstellung von Zuständen und Übergängen, wobei der Code im Mittelpunkt steht:
Die Ameise wird durch die dargestellt Ameise
Klasse, deren Eigenschaft benannt ist Gehirn
und eine Methode für jeden Staat. Das Gehirn
Eigenschaft ist eine Instanz von FSM
Klasse:
öffentliche Klasse Ant öffentliche var Position: Vector3D; public var Geschwindigkeit: Vector3D; public var brain: FSM; öffentliche Funktion Ant (posX: Number, posY: Number) position = new Vector3D (posX, posY); Geschwindigkeit = neuer Vector3D (-1, -1); Gehirn = neues FSM (); // Das Gehirn muss nach dem Blatt suchen. brain.setState (findLeaf); / ** * Der Status "findLeaf". * Die Ameise bewegt sich in Richtung des Blattes. * / public function findLeaf (): void / ** * Der Status "goHome". * Die Ameise bewegt sich in Richtung ihrer Heimat. * / public function goHome (): void / ** * Der Zustand "runAway". * Die Ameise läuft vom Mauszeiger weg. * / public function runAway (): void public function update (): void // Aktualisieren Sie die FSM, die das "Gehirn" steuert. Es wird die aktuell // aktive Statusfunktion aufgerufen: findLeaf (), goHome () oder runAway (). brain.update (); // Wende den Geschwindigkeitsvektor auf die Position an, damit sich die Ameise bewegt. moveBasedOnVelocity (); (…)
Das Ameise
Klasse hat auch eine Geschwindigkeit
und ein Position
Diese Eigenschaft wird zur Berechnung der Bewegung mithilfe der Euler-Integration verwendet. Das aktualisieren()
Diese Methode wird bei jedem Spielrahmen aufgerufen, daher wird der FSM aktualisiert.
Der Einfachheit halber sollte der Code zum Bewegen der Ameise verwendet werden, z moveBasedOnVelocity ()
, wird weggelassen. Weitere Informationen dazu finden Sie in der Reihe "Verständnis für Lenkverhalten".
Unten ist die Implementierung jedes Staates, beginnend mit findLeaf ()
, der für die Führung der Ameise in die Blattstellung zuständige Staat:
public function findLeaf (): void // Bewegen Sie die Ameise in Richtung Blatt. Velocity = new Vector3D (Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); if (Abstand (Game.instance.leaf, this) <= 10) // The ant is extremelly close to the leaf, it's time // to go home. brain.setState(goHome); if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // It will make the brain start calling runAway() from // now on. brain.setState(runAway);
Das nach Hause gehen()
Zustand, verwendet, um die Ameise nach Hause zu führen:
public function goHome (): void // Bewege die Ameise in Richtung Heimatgeschwindigkeit = new Vector3D (Game.instance.home.x - position.x, Game.instance.home.y - position.y); if (Entfernung (Game.instance.home, this) <= 10) // The ant is home, let's find the leaf again. brain.setState(findLeaf);
Endlich, das weglaufen()
Zustand, um die Ameise vom Mauszeiger fliehen zu lassen:
public function runAway (): void // Bewege die Ameise von der Mauszeigergeschwindigkeit = new Vector3D (position.x - Game.mouse.x, position.y - Game.mouse.y); // Ist der Mauszeiger noch geschlossen? if (distance (Game.mouse, this)> MOUSE_THREAT_RADIUS) // Nein, der Mauszeiger ist weg. Lass uns nach dem Blatt suchen. brain.setState (findLeaf);
Das Ergebnis ist eine Ameise, die von einem FSM "Gehirn" kontrolliert wird:
Ameise von einem FSM kontrolliert. Bewegen Sie den Mauszeiger, um die Ameise zu bedrohen.Stellen Sie sich vor, die Ameise muss auch vom Mauszeiger weglaufen, wenn sie nach Hause geht. Der FSM kann wie folgt aktualisiert werden:
Ant FSM mit neuen Übergängen aktualisiert.Es scheint eine triviale Modifikation zu sein, das Hinzufügen eines neuen Übergangs, aber es entsteht ein Problem: Wenn der aktuelle Zustand ist weglaufen
und der Mauszeiger ist nicht mehr in der Nähe, in welchen Zustand sollte die Ameise wechseln: nach Hause gehen
oder Blatt finden
?
Die Lösung für dieses Problem ist a Stack-basiertes FSM. Im Gegensatz zu unserem vorhandenen FSM verwendet ein stapelbasierter FSM einen Stapel, um Zustände zu steuern. Der obere Teil des Stapels enthält den aktiven Status. Übergänge werden durch Push- oder Popping-Status aus dem Stack behandelt:
Stack-basiertes FSMDer derzeit aktive Status kann entscheiden, was während eines Übergangs zu tun ist:
Übergänge in einem stapelbasierten FSM: Pop selbst + Push New; Pop selbst; drücken Sie neu.Es kann sich vom Stapel lösen und Drücken Sie einen anderen Status, was einen vollständigen Übergang bedeutet (genau wie der einfache FSM). Es kann sich selbst vom Stapel entfernen, was bedeutet, dass der aktuelle Status abgeschlossen ist und der nächste Status im Stapel aktiv werden sollte. Schließlich kann es einfach einen neuen Status vorschieben, was bedeutet, dass sich der derzeit aktive Status für eine Weile ändert. Wenn er sich jedoch vom Stack löst, wird der zuvor aktive Status wieder übernommen.
Ein stapelbasierter FSM kann auf dieselbe Weise wie zuvor implementiert werden, diesmal jedoch mit einem Array von Funktionszeigern, um den Stapel zu steuern. Das activeState
Diese Eigenschaft wird nicht mehr benötigt, da die Spitze des Stapels bereits auf den aktuell aktiven Status zeigt:
öffentliche Klasse StackFSM private var stack: Array; öffentliche Funktion StackFSM () this.stack = new Array (); public function update (): void var currentStateFunction: Function = getCurrentState (); if (currentStateFunction! = null) currentStateFunction (); public function popState (): Funktion return stack.pop (); public function pushState (state: Funktion): void if (getCurrentState ()! = state) stack.push (state); public function getCurrentState (): Funktion return stack.length> 0? stack [stack.length - 1]: null;
Das setState ()
Methode wurde durch zwei neue Methoden ersetzt: pushState ()
und popState ()
; pushState ()
fügt einen neuen Zustand am oberen Rand des Stapels hinzu, während popState ()
Entfernt den Status am oberen Rand des Stapels. Bei beiden Methoden wird die Maschine automatisch in einen neuen Status versetzt, da sie den oberen Rand des Stapels ändern.
Bei der Verwendung eines stapelbasierten FSM ist zu beachten, dass jeder Zustand dafür verantwortlich ist, sich vom Stapel zu entfernen. Normalerweise entfernt sich ein Status vom Stapel, wenn er nicht mehr benötigt wird, z. B. wenn Attacke()
ist aktiv, aber das Ziel ist gerade gestorben.
Bei Verwendung des am-Beispiels sind nur wenige Änderungen erforderlich, um den Code an die Verwendung eines stapelbasierten FSM anzupassen. Das Problem, den Status des Übergangs nicht zu kennen, ist dank der stapelbasierten FSM jetzt nahtlos gelöst:
öffentliche Klasse Ant (…) public var brain: StackFSM; öffentliche Funktion Ant (posX: Number, posY: Number) (…) brain = new StackFSM (); // Das Gehirn muss nach dem Blatt suchen. brain.pushState (findLeaf); (…) / ** * Der Status "findLeaf". * Die Ameise bewegt sich in Richtung des Blattes. * / public function findLeaf (): void // Bewegen Sie die Ameise in Richtung Blatt. Velocity = new Vector3D (Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y); if (Abstand (Game.instance.leaf, this) <= 10) // The ant is extremelly close to the leaf, it's time // to go home. brain.popState(); // removes "findLeaf" from the stack. brain.pushState(goHome); // push "goHome" state, making it the active state. if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // The "runAway" state is pushed on top of "findLeaf", which means // the "findLeaf" state will be active again when "runAway" ends. brain.pushState(runAway); /** * The "goHome" state. * It makes the ant move towards its home. */ public function goHome() :void // Move the ant towards home velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y); if (distance(Game.instance.home, this) <= 10) // The ant is home, let's find the leaf again. brain.popState(); // removes "goHome" from the stack. brain.pushState(findLeaf); // push "findLeaf" state, making it the active state if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) // Mouse cursor is threatening us. Let's run away! // The "runAway" state is pushed on top of "goHome", which means // the "goHome" state will be active again when "runAway" ends. brain.pushState(runAway); /** * The "runAway" state. * It makes the ant run away from the mouse cursor. */ public function runAway() :void // Move the ant away from the mouse cursor velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y); // Is the mouse cursor still close? if (distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) // Nein, der Mauszeiger ist weg. Gehen wir zurück zum vorherigen Status. brain.popState (); (…)
Das Ergebnis ist eine Ameise, die vom Mauszeiger weglaufen kann und vor der Bedrohung wieder in den zuvor aktiven Zustand übergeht:
Ant wird von einem stapelbasierten FSM gesteuert. Bewegen Sie den Mauszeiger, um die Ameise zu bedrohen.Maschinen mit endlichen Zuständen sind nützlich, um die KI-Logik in Spielen zu implementieren. Sie können einfach mit einer Grafik dargestellt werden, sodass ein Entwickler das Gesamtbild sehen, optimieren und das Endergebnis optimieren kann.
Die Implementierung eines FSM mit Funktionen oder Methoden zur Darstellung von Zuständen ist einfach, aber leistungsstark. Noch komplexere Ergebnisse können mit einem stapelbasierten FSM erzielt werden, der einen überschaubaren und präzisen Ausführungsablauf gewährleistet, ohne den Code zu beeinträchtigen. Es ist an der Zeit, alle Feinde mit einem FSM intelligenter zu machen!