Die Macht der Endlichen Zustandsmaschinen (FSM): Konzept und Erstellung
German (Deutsch) translation by Katharina Grigorovich-Nevolina (you can also view the original English article)
Dieses zweiteilige Tutorial behandelt die Erstellung eines mehrstufigen Autos mit einer endlichen Zustandmaschine. Wir werden mit Procedural FSM beginnen und in das Designmuster State Pattern übergehen. Konzept und Erstellung stehen bei diesem ersten Teil im Vordergrund. Wir werden dann im zweiten Teil in Anwendung und Erweiterung gehen.
Finale Ergebnisvorschau
Werfen wir einen Blick auf das Endergebnis, auf das wir hinarbeiten werden:
Was ist eine endliche Zustandsmaschine?
Wikipedia definiert eine FSM (Finite State Machine) als eine mathematische Abstraktion, die manchmal verwendet wird, um digitale Logik oder Computerprogramme zu entwerfen. Es ist ein Verhaltensmodell, das aus einer endlichen Anzahl von Zuständen, Übergängen zwischen diesen Zuständen und Aktionen besteht, ähnlich einem Flussgraphen, in dem man untersuchen kann, wie die Logik abläuft, wenn bestimmte Bedingungen erfüllt sind.
Es hat endliches internes Gedächtnis; ein Eingabemerkmal, das Symbole nacheinander in einer Sequenz liest, ohne rückwärts zu gehen; und ein Ausgabefeature, das in der Form einer Benutzerschnittstelle sein kann, sobald das Modell implementiert ist. Die Operation einer FSM beginnt von einem der Zustände (als Startzustand bezeichnet), geht durch Übergänge (abhängig von den Eingaben) in verschiedene Zustände und kann in jedem der verfügbaren Zustände enden - jedoch markiert nur ein bestimmter Satz von Zuständen a erfolgreicher Ablauf der Operation.
In meinen eigenen Worten ist ein FSM ein Gerät, das von Entwicklern verwendet wird, um Objekte zu erstellen, die unterschiedliche Verhaltensweisen haben, die durch den aktuellen Zustand bestimmt werden. Abhängig von der Eingabe kann das Objekt reagieren und/oder in einen anderen Zustand übergehen.
Ein gutes Beispiel wäre eine 1970 HK VP70Z-Maschinenpistole, die drei Schussmodi hat: Sicherheit, Einzelschuss und halbautomatischer Drei-Runden-Stoß. Abhängig vom aktuellen Modus, den Sie auf (Zustand) eingestellt haben, ist das Ergebnis (Ausgang) unterschiedlich, wenn Sie den Auslöser (Eingang) ziehen.
Werkzeuge: Wenn Sie eine Idee konzeptionieren (das Objekt mit mehreren Zuständen, das Sie erstellen möchten), empfiehlt es sich, eine Zustandsübergangstabelle zu verwenden, um zu erfahren, welche Zustände und Aktionen für diese Zustände hinzugefügt werden müssen.
Schritt 1: Installation
Es ist Zeit, ein neues Projekt zu beginnen. Erstellen Sie mit FlashDevelop ein neues AS3-Projekt. Für den Namen, setzen Sie CarFSM. Klicken Sie auf "Durchsuchen ..." und speichern Sie sie an Ihrem gewünschten Ort. Gehen Sie in den Paket-Slot und geben Sie "com.activeTuts.fsm" ein. Stellen Sie sicher, dass das Kontrollkästchen "Verzeichnis für Projekt erstellen" ausgewählt ist, und klicken Sie zum Abschluss auf "OK".
Sobald das Projekt in FlashDevelop geladen wurde, klicken Sie auf "Ansicht" und wählen Sie "Projektmanager". Sehen Sie den Ordner "src"? Klicken Sie mit der rechten Maustaste darauf und wählen Sie "Durchsuchen".
Wenn Sie diesen Ordner geöffnet haben, sollten Sie den Ordner "com" sehen, den Sie zuvor erstellt haben. Öffnen Sie den Quellcode, den ich in diesem Tutorial eingefügt habe, und ziehen Sie den Ordner "assets" in "src"; vergewissern Sie sich, dass Sie es nicht in den Ordner "com" legen.
Als nächstes gehen Sie in den Quellcode "com" Ordner und ziehen Sie den "bit101" Ordner in den "com" Ordner in "src". Sie können auch minimalComps hier herunterladen, wenn Sie es direkt von der Quelle beziehen möchten.
Schließlich, reißenSie innerhalb der "com" Ordner (innerhalb "src") bis hin zu "fsm" auf und doppelklicken Sie auf Main.as. Das sollte jetzt in FlashDevelop geöffnet sein (vorausgesetzt, Sie haben FD als Standardanwendung für die Erweiterung).
Schritt 2: Aufwärmen
Wir beginnen mit der Betrachtung der beiden Zustände eines noch einfacheren Beispiels: das Kontrollkästchen von MinimalComps.
Nehmen wir an, wir möchten ein Kontrollkästchen, die ihren aktuellen Status widerspiegelt, indem sie ihre Bezeichnung ändert. Unten ist die Übergangstabelle für das Kontrollkästchen.
Jetzt für den Code. Fügen Sie in Main.as, eine Zeile unterhalb der Klassenimporte, die unten gezeigten Metadaten hinzu.
[SWF (width = 500, height = 350, frameRate = 60, backgroundColor = 0xFFFFFF)]
Als nächstes gehen Sie innerhalb der init()
-Methode und setzen Sie den Cursor unter dem Punkt "Einstiegspunkt". Fügen Sie dann einen Methodenaufruf simpleExample()
hinzu. Stellen Sie als Nächstes sicher, dass der Cursor innerhalb des Methodenaufrufs aktiv ist, und drücken Sie die Tastenkombination "CTRL + SHIFT + 1". Eine Eingabeaufforderung wird angezeigt. Wählen Sie "Private Funktion generieren" und drücken Sie die Eingabetaste.
Kopieren Sie nun den folgenden Code und fügen Sie ihn in die neu erstellte Methode ein. Als nächstes setzen Sie Ihren Cursor in das Wort "CheckBox" und drücken Sie "CTRL + SHIFT + 1", um automatisch die gewünschte Klasse zu importieren. Sobald Sie fertig sind, drücken Sie "CTRL + ENTER", um die Anwendung auszuführen.
Wenn Sie Fehler feststellen, vergleichen Sie bitte Ihre Klassen mit denen, die ich mit dem Quelle-Herunterladen hinzugefügt habe.
var checkBox:CheckBox = new CheckBox (this, 240, 160, 'false', showValue); checkBox.scaleX = checkBox.scaleY = 2; function showValue (e:Event):void { CheckBox (e.target).label = Boolean (e.target.selected).toString (); }
Sie sollten etwas haben, das dem ähnelt, was Sie über dieser Linie sehen. Es gibt Ihre zwei Zustände, ON
und OFF
. Jedes Mal, wenn Sie klicken, schaltet das Kontrollkästchen Zustände um und ändert auch seine Beschriftung als Ausgabeform.
Auf zum echten "Auto" FSM-Projekt. Stellen Sie sicher, dass das Projekt im Modus "Debug" ausgeführt wird, um trace()
-Anweisungen zu aktivieren.
Schritt 3: Die prozedurale Auto-FSM
Okay, vergessen wir die Vorschau oben auf der Seite und starten die Auto-FSM von Grund auf neu. Heben Sie in Main.as die init()
-Methode zusammen mit der simpleExample()
-Methode hervor und drücken Sie die Taste "BACK", um sie zu entfernen.
Gehen Sie eine Zeile über den Konstruktor und fügen Sie die folgenden Variablen hinzu.
private var _past:Number; private var _present:Number; private var _tick:Number; private var _car:Car; private var _initiatedTest1:Boolean; private var _initiatedTest2:Boolean; private var _initiatedTest3:Boolean; private var _initiatedTest4:Boolean; private var _initiatedTest5:Boolean; private var _initiatedTest6:Boolean; private var _finalActions:Boolean; private var _counter:Number = 0;
Die Variablen _past, _present, _tick und _counter werden alle für die zeitgesteuerte Ausführung verwendet. Ich werde bald mehr darüber erklären. Die _car-Variable enthält einen Verweis für die Car-Klasse, die die prozeduralen Car FSM-Aktionen einkapselt. Der Rest sind Boolesche Eigenschaften, die zum Auslösen von zeitgesteuerten Aktionen verwendet werden.
Arbeiten wir an der zeitgesteuerten Ausführung. Fügen Sie den folgenden Code in den Konstruktor ein.
_present = getTimer (); trace ('Start of constructor = ' + _present * 0.001 + ' seconds\n'); _past = _present; addEventListener (Event.ENTER_FRAME, update);
Bewegen Sie den Cursor in das Wort "update" und drücken Sie "CTRL + SHIFT + 1" und wählen Sie "Event-Handler generieren". de. Wenn Sie die Anwendung testen, sehen Sie einen Ausdruck ähnlich "Start des Konstruktors = 2.119 Sekunden" (es könnte weniger sein, wenn Sie einen schnelleren PC haben). Es entspricht dem Teilen von getTimer()
mit 1000, aber die Multiplikation ist schneller.
Gehen wir zur update()
-Methode. Fügen Sie den folgenden Code hinzu.
_present = getTimer (); _tick = (_present - _past) * .001; ///converted to 1/1000 milliseconds _past = _present; _counter += _tick; ///used for tests with timed execution. if (_counter >= 2)//2 seconds { _counter -= 2; trace (_counter + ' 2 seconds'); }
Wenn Sie es jetzt erneut testen, werden Sie alle zwei Sekunden eine trace()
-Anweisung sehen. Der _counter wird dann auf die Überlappung zurückgesetzt, die er hatte, um die Zeitgenauigkeit einzuhalten.
Versuchen Sie es mit einem anderen Wert als zwei Sekunden und führen Sie es ein paar Mal aus.
Zur Car-Klasse. Bevor Sie fortfahren, gehen Sie voran und entfernen Sie die if()
-Anweisung in der update()
-Methode.
Schritt 4: Die Car-Klasse
Wie ich bereits erwähnt habe, beginnen wir mit einer neuen Idee, ein mehrstufiges Auto zu schaffen. Nehmen wir an, wir haben beschlossen, ein Auto zu haben, das ein- und ausgeschaltet werden kann, auch vorwärts gefahren werden kann und kein Benzin mehr hat. Das würde uns vier verschiedene Zustände geben - ON
, OFF
, DRIVE_FORWARD
und OUT_OF_FUEL
.
Das erste, was zu tun ist, ist, die verschiedenen Zustände und Handlungen für diese Staaten in einer Zustandsübergangs-Tabelle auszuarbeiten. Sie können einen Stift und ein leeres Blatt Papier verwenden, um alle Zustände und Aktionen, die das Auto-Objekt benötigen würde, schnell zu notieren. Sehen Sie das Bild unten.
Verwenden Sie immer eine "update()" -Methode, um Ihre Bundesstaaten in Echtzeit zu steuern. Wie verbrauchen Sie eine höhere Menge an Kraftstoff beim Fahren als bei Leerlauf im Park.
Es ist einfach zu sagen, wie jeder Staat auf jede der Aktionen reagieren sollte. Es erscheint einfach, weil wir (Menschen) alle denken, dass Objekte in dem einen oder anderen Zustand sind.
Jetzt können wir die Klasse programmieren.
Wechseln Sie in der Konstruktormethode in Main.as eine Zeile vor dem Ereignisereignis ENTER_FRAME
und fügen Sie den folgenden Code hinzu.
_car = new Car; addChild (_car);
Jetzt, da es keine Car-Klasse gibt, bewegen Sie den Cursor in das Wort "Car" und drücken Sie "CTRL + SHIFT + 1", wählen Sie "Erstellen Sie eine neue Klasse" und drücken Sie die "ENTER"-Taste.
Verwenden Sie dieselben Informationen wie unten gezeigt. Klicken Sie auf "OK", um zu beenden.
Sie sollten jetzt die Car-Klasse in FlashDevelop geöffnet haben.
Schritt 5: Car-Variablen
Fügen Sie den Code unterhalb einer Zeile über dem Klassenkonstruktor hinzu.
public static const ONE_SIXTH_SECONDS:Number = 1 / 6; //6 times per second private const IDLE_FUEL_CONSUMPTION:Number = .0055; private const DRIVE_FUEL_CONSUMPTION:Number = .011; ///CAR STATES private static const ENGINE_OFF:String = 'off'; private static const ENGINE_ON:String = 'on'; private static const ENGINE_DRIVE_FORWARD:String = 'driving forward'; private static const ENGINE_OUT_OF_FUEL:String = 'out of fuel'; private var _currentState:String = ENGINE_OFF; private var _engineTimer:Number = 0; private var _fullCapacity:Number = 1; private var _fuelSupply:Number = _fullCapacity; //in gallons
Das Auto ist so eingestellt, dass es nur 6 Mal pro Sekunde Kraftstoff verbraucht. Das wird durch die Klassenkonstante ONE_SIXTH_SECONDS
repräsentiert. Außerdem hängt die Verbrauchsmenge davon ab, ob das Auto im Leerlauf ist oder vorwärts fährt. Wir verwenden IDLE_FUEL_CONSUMPTION
und DRIVE_FUEL_CONSUMPTION
für diese Zwecke.
Die vier Zustände werden durch String-Konstanten repräsentiert, wobei ENGINE_OFF
als Standard gesetzt ist.
Die Eigenschaft _engineTimer
wird verwendet, um consumeFuel()
alle 1/6 Sekunden auszulösen, jedoch nur, wenn der Status ENGINE_ON
oder ENGINE_DRIVE_FORWARD
lautet.
Schließlich nimmt _fuelSupply
(was consumeFuel()
langsam wegnimmt) den Wert von _fuelCapacity
für einen vollen Tank ein.
Schritt 6: Methoden aus der Zustandsübergangstabelle
Lassen Sie den Car-Konstruktor für jetzt leer. Gehen Sie darunter und fügen Sie die unten gezeigte update()
-Methode hinzu.
public function update ($tick:Number):void { switch (_currentState) { case ENGINE_OFF: //nothing break; case ENGINE_ON: _engineTimer += $tick; //gas consumption and trace statement if (_engineTimer >= ONE_SIXTH_SECONDS) //6 times per second interval { trace ('vm');//you may comment this out if you like _engineTimer -= ONE_SIXTH_SECONDS; // 0 + overlap consumeFuel (IDLE_FUEL_CONSUMPTION);///30 seconds gas supply } break; case ENGINE_DRIVE_FORWARD: _engineTimer += $tick; if (_engineTimer >= ONE_SIXTH_SECONDS) { trace ('vroomm');//you may comment this out if you like _engineTimer -= ONE_SIXTH_SECONDS; consumeFuel (DRIVE_FUEL_CONSUMPTION);///15 seconds gas supply } break; case ENGINE_OUT_OF_FUEL: //nothing break; } }
Wir rufen Main.as diese Methode bei jedem ENTER_FRAME
-Ereignis auf, das die verstrichene Zeit zwischen Frames verstreichen lässt. Einmal aufgerufen, überprüft es den aktuellen Zustand des Autos und führt die entsprechende Aktion aus.
Wenn dieser Status nicht überschritten wird, kann der Statusübergang nur über consumeFuel()
erfolgen, wodurch der Wert auf OUT_OF_FUEL
gesetzt wird, wenn _fuelSupply
abgelaufen ist.
Hinweis: Aktionen, die sich in Ihrer Zustandsübergangstabelle befinden, sind immer öffentlicher Zugriff, der als Eingabesteuerelemente verwendet wird. Das gilt unabhängig davon, ob Sie Procedural FSM oder das State Pattern verwenden.
Schritt 7: Einschalten des Autos
Fügen Sie den folgenden Code nach der update()
-Methode hinzu.
public function turnKeyOn ():void { trace ("attempting to turn the car on..."); switch (_currentState) { case ENGINE_OFF: trace ("Turning the car on...the engine is now running!"); _currentState = ENGINE_ON; break; case ENGINE_ON: trace ("the engine's already running you didn't have to crank the ignition!"); break; case ENGINE_DRIVE_FORWARD: trace ("you're driving so don't crank the ignition!"); break; case ENGINE_OUT_OF_FUEL: trace ("no fuel - the car will not start, get some fuel before anything. Returning the key to the off position."); break; } }
Genau wie die update()
-Methode wird _currentState
überprüft und die entsprechende Aktion ausgeführt. Es erklärt sich ziemlich genau.
Schritt 8: Das Auto ausschalten.
Das gleiche gilt für das Ausschalten des Autos. Fügen Sie es als nächstes hinzu.
public function turnKeyOff ():void { trace ("attempting to turn the car off..."); switch (_currentState) { case ENGINE_OFF: trace ("The car's already off, you can't turn the key counter-clockwise any further..."); break; case ENGINE_ON: trace ("click... the engine has been turned off from park."); _currentState = ENGINE_OFF; break; case ENGINE_DRIVE_FORWARD: trace ("nvrm...click... rolling to a stop...the engine has been turned off."); _currentState = ENGINE_OFF; break; case ENGINE_OUT_OF_FUEL: trace ("you already did this when the fuel ran out earlier..."); break; } }
Es wird sehr einfach, die Methoden zu erstellen. Kopieren Sie einfach die vorherige, fügen Sie sie ein und nehmen Sie dann ein paar Änderungen vor.
Schritt 9: Vorwärts fahren
Gehen Sie immer zu Ihrer Zustandsübergangstabelle zurück, um zu sehen, was für jeden Zustand geschehen muss, wenn Sie die Eingabemethode aufrufen, an der Sie gerade arbeiten.
Fügen Sie den Code unterhalb der TurnKeyOff()
-Methode hinzu.
public function driveForward ():void { trace ("attempting to drive forward..."); switch (_currentState) { case ENGINE_OFF: trace ("click, changing the gear to drive doesn't do anything...the car is not running, returning the gear to park..."); break; case ENGINE_ON: trace ("click, changing gears to drive ...now were going somewhere..."); _currentState = ENGINE_DRIVE_FORWARD; break; case ENGINE_DRIVE_FORWARD: trace ("already driving - no need to change anything..."); break; case ENGINE_OUT_OF_FUEL: trace ("click, changing the gear to drive won't do anything...the car has no fuel, returning the gear to park..."); break; } }
Schritt 10: Fuel verbrauchen
Diese Methode ist privat, da nur das Auto darauf zugreifen muss. Es ruft sechs Mal pro Sekunde von update()
.
Setzen Sie den Code nach der Methode driveForward()
.
private function consumeFuel ($consumption:Number):void { if ((_fuelSupply -= $consumption) <= 0) { _fuelSupply = 0; trace ("phit...phit - the engine has stopped, no more fuel to run, returning the gear to park..."); _currentState = ENGINE_OUT_OF_FUEL; } }
Jetzt sehen Sie, wie der Code geht, wie zuvor im Abschnitt Klassenvariablen erklärt.
Schritt 11: Tanken Sie das Auto
Diese Methode ist privat, da nur das Auto darauf zugreifen muss.
Setzen Sie den Code als nächstes.
public function reFuel ():void { trace ("attempting to refuel..."); if (_fuelSupply == 1) { trace ("no need to refuel right now, the tank is full."); return; } switch (_currentState) { case ENGINE_OFF: trace ("getting out of the car and..."); break; case ENGINE_ON: trace ("turning the key to off position, getting out of the car and..."); _currentState = ENGINE_OFF; break; case ENGINE_DRIVE_FORWARD: trace ("changing gear from drive to park and turning the key to off position, getting out of the car and..."); _currentState = ENGINE_OFF; break; case ENGINE_OUT_OF_FUEL: trace ("turning the key to the off position, getting out of the car and..."); _currentState = ENGINE_OFF; break; } var neededSupply:Number = _fullCapacity - _fuelSupply; _fuelSupply += neededSupply; trace ("filling up to " + _fuelSupply + " gallon(s) of fuel."); }
Die Methode prüft zuerst, ob das Auto einen vollen Tank hat. Wenn ja, sagt es Ihnen, dass es einen vollen Tank hat und verlässt die Methode.
Wenn das Auto andererseits keinen vollen Tank hat, durchläuft es die bekannte case/switch-Anweisung und führt die richtige trace()
-Anweisung aus.
Das letzte Bit des Codes berechnet die Menge an verbrauchtem Kraftstoff und füllt nur diesen auf, um einen vollen Tank zu behalten. Es druckt dann den Wert eines vollen Tanks.
Schritt 12: Verwenden von toString()
, um trace()
Anweisungen zu unterstützen
Diese Methode musste außer Kraft gesetzt werden, da das Car von Sprite erbt, das wiederum von EventDispatcher erbt.
Es gibt nur eine String-Anweisung zurück, die unten angezeigt wird. Fügen Sie es als letzte Methode für die Klasse Auto hinzu.
override public function toString ():String { return "The car is currently " + _currentState + " with a fuel amount of " + _fuelSupply.toFixed (2) + " gallon(s)."; }
So, jetzt, wenn Sie rufen trace(_car
) von Main.as, anstelle von "[object Car]" erhalten Sie eine Aussage wie "Das Auto ist derzeit aus mit einer Kraftstoffmenge von 1,00 Gallonen (s)."
Lassen Sie uns zum Testen zu Main.as zurückkehren. Speichern Sie Ihre Arbeit, bevor Sie fortfahren.
Schritt 13: Belastungstest
Innerhalb des Konstruktors von Main, direkt nach dem Sie den Ereignis-Listener ENTER_FRAME
hinzugefügt haben. Geben sie den untenstehenden Code ein.
///test 0 _car.turnKeyOff (); trace (_car); _car.driveForward (); trace (_car); _car.turnKeyOn (); trace (_car);
An diesem Punkt wird das Auto alle sechs Aktionen ohne Zeitverlust ausführen. Das Ereignis ENTER_FRAME
wurde noch nicht gestartet.
Als nächstes gehen Sie in die update()
-Methode direkt unterhalb von wo _tick
zu _counter
hinzugefügt wird und fügen Sie den Code ein, der als nächstes angezeigt wird.
_car.update (_tick); //test 1 after 5 seconds of running the car and //only if _initiatedTest1 has a value of false. if (_counter >= 5 && ! _initiatedTest1) { _initiatedTest1 = true; _car.reFuel (); _car.reFuel (); _car.driveForward (); _car.turnKeyOff (); _car.turnKeyOff (); _car.driveForward (); _car.turnKeyOn (); _car.driveForward (); trace (_car); } //test 2 after 11 seconds if (_counter >= 11 && ! _initiatedTest2) { _initiatedTest2 = true; _car.turnKeyOff (); _car.turnKeyOn (); _car.driveForward (); trace (_car); } //test 3 after 30 seconds if (_counter >= 30 && ! _initiatedTest3) { _initiatedTest3 = true; _car.turnKeyOn (); _car.turnKeyOff (); _car.turnKeyOn (); _car.driveForward (); _car.turnKeyOn (); _car.turnKeyOff (); _car.turnKeyOn (); trace (_car); } //test 4 after 35 seconds if (_counter >= 35 && ! _initiatedTest4) { _initiatedTest4 = true; _car.reFuel (); _car.reFuel (); trace (_car); _car.turnKeyOff (); _car.driveForward (); _car.turnKeyOff (); _car.turnKeyOff (); _car.turnKeyOn (); trace (_car); } //test 5 after 42 seconds if (_counter >= 42 && ! _initiatedTest5) { _initiatedTest5 = true; _car.driveForward (); trace (_car); } //test 6 after 45 seconds if (_counter >= 45 && ! _initiatedTest6) { _initiatedTest6 = true; _car.turnKeyOn (); _car.turnKeyOn (); _car.driveForward (); trace (_car); } ///stop the car after 60 seconds if (_counter >= 60 && ! _finalActions) { _finalActions = true; trace ('elapsed ' + getTimer () / 1000); _car.turnKeyOff (); trace (_car); }
Ich weiß, es ist viel Code, aber es ist selbsterklärend. Führen Sie die Anwendung aus und überprüfen Sie Ihre Ausgabe.
Wenn Sie Fehler erhalten, vergewissern Sie sich, dass Sie Ihren Code mit den Klassen vergleichen, die in diesem Lernprogramm enthalten sind.
Versuchen Sie, _fuelCapacity
in Car zu ändern und verwechseln Sie die Methoden in einigen oder allen Testabschnitten und führen Sie sie erneut aus. Sie werden feststellen, dass der Code solide ist und diese prozedurale FSM wirksam ist. Und das ist es! Wir sind fertig.
Warten Sie eine Minute. Da alles gut ist, warum fügen wir nicht die Fähigkeit hinzu, rückwärts und turbo zu fahren? Währenddessen können wir auch Animation und Sound hinzufügen. Stellen Sie sich nun vor, wie aufgebläht die Auto-Klasse wird, wenn Sie dafür sorgen, dass all die Dinge erledigt werden, die das Auto oben auf der Seite macht. Wir schauen uns vielleicht 2000 Zeilen Code an - zumindest. LOL! Ich würde wahrscheinlich sagen: Ja, klar, ich kann das hacken. Aber der Code wird sehr zerbrechlich und leicht zu knacken. Es könnte also eine gute Idee sein, eine andere Technik zu verwenden.
Wenn das FSM-Objekt ein einfaches Verhalten aufweist, verwenden Sie diese Technik auf jeden Fall. Wenn Sie jedoch ein komplexes Objekt haben, das in Zukunft möglicherweise neue Funktionen hinzufügen muss. Vielleicht fügen Sie noch ein paar weitere Zustände hinzu - nun, hier kommt das State Pattern ins Spiel.
Schritt 14: Einführung des Zustandsmusters
Begrüßen Sie den "großen Bruder" von Procedural FSM. Durch die Verwendung dieses Entwurfsmusters können Sie Ihre Status einfach verwalten und ändern, aber der beste Teil - andere Status können jetzt hinzugefügt werden, ohne dass der Code ruiniert werden muss.
Um dieses Muster anzuwenden, verweisen wir erneut auf unsere vertrauenswürdige Zustandsübergangstabelle. Sehen Sie Schritt 4. Das Zustandsmuster besteht aus drei Teilen. Das erste ist die Zustandsschnittstelle, diese enthält alle Aktionen, die Sie in der Zustandsübergangstabelle sehen. Darüber hinaus kann diese Zustandsschnittstelle auch Methoden enthalten, die von allen State-Klassen gemeinsam genutzt werden. Zweitens, die State-Klassen, die für jeden in Ihrer Zustand-Transition-Tabelle gezeigten Staat entsprechen. Und drittens die Zustandsmachine - das ist normalerweise dein umgewandeltes Procedural FSM-Objekt (die Car-Klasse). Nach der Konvertierung stellt das Auto öffentliche Accessoren und Modifikatoren zur Verfügung, um externe Kontrolle von allen staatlichen Klassen zu ermöglichen. Das Auto wird Aktionen an den derzeit aktiven Zustand delegieren.
Schritt 15: Beginn der Konvertierung
Klicken Sie auf "Ansicht" und wählen Sie "Projektmanager". Machen Sie in "src" einen Drilldown, bis Sie den Ordner "fsm" sehen. Klicken Sie mit der rechten Maustaste darauf und wählen Sie "Hinzufügen > Neue Schnittstelle ...(Add > New Interface)" und drücken Sie "ENTER".
Nennen Sie es "IState". Schnittstellen beginnen mit "I" für die Namenskonvention.
Sobald FlashDevelop die Klasse öffnet, fügen Sie den folgenden Code hinzu.
function turnKeyOff ():void; function turnKeyOn ():void; function driveForward ():void; function reFuel ():void; function update ($tick:Number):void; function toString ():String;
Diese IState-Schnittstelle wird von allen State-Klassen implementiert. Die letzte Funktion toString()
hat nichts mit der Kontrolle des Autos zu tun, aber alle Staatsklassen benutzen es.
Weitere Informationen zu Interfaces finden Sie unter AS3 101: Einführung in Interfaces. Beginnen wir mit dem Hinzufügen der Zustandsklassen.
Schritt 16: Die EngineOff-Klasse
Folgen Sie dem gleichen Verfahren, wenn Sie die IState-Schnittstelle erstellt haben, wählen Sie aber stattdessen "Neue Klasse hinzufügen".
Nennen Sie es "EngineOff". Klicken Sie für den Schnittstellensteckplatz auf Hinzufügen, und geben Sie "IState" ein. Das sollte die IState-Klasse innerhalb desselben Ordners finden. Außerdem sollte das Kontrollkästchen für "Implementierungen der Schnittstellenmethoden generieren" ausgewählt sein. Klicken Sie zum Abschluss auf "OK".
Die neue Klasse kommt halbwegs fertig. Es sollte dem, was unten angezeigt wird, sehr ähnlich sein.
package com.activeTuts.fsm { public class EngineOff implements IState { public function EngineOff() { } /* INTERFACE com.activeTuts.fsm.IState */ public function turnKeyOff():void { } public function turnKeyOn():void { } public function driveForward():void { } public function reFuel():void { } public function update($tick:Number):void { } public function toString():String { } } }
Diese Statusklassen müssen Sprite nicht erweitern, da alle Media-Assets (zweiter Teil) hinzugefügt und über das Auto gesteuert werden. Die Zustände werden instanziiert, indem sich die Car-Klasse selbst als Referenz definiert. Eine Zweiwege-Kompositionsstruktur wird verwendet, um die Kommunikation zwischen dem Auto und den staatlichen Klassen zu ermöglichen.
Schritt 17: Beenden unserer EngineOff-Klasse
Ändern Sie die Konstruktormethode so, dass sie dem folgenden Code entspricht.
private var _car:Car; public function EngineOff ($car:Car) { _car = $car; }
Ich habe die Variable _car über den modifizierten Konstruktor eingefügt. Jetzt können wir die Car-Klasse von diesem Zustand aus kontrollieren.
Gehen wir zu den Implementierungen der Schnittstellenmethode über.
Rufen Sie die Methode turnKeyOff()
auf. Überprüfen Sie Ihre Zustandsübergangstabelle, um zu sehen, was hier passiert. Als nächstes vergleichen Sie das mit der prozeduralen turnKeyOff()
Methode innerhalb der Car-Klasse. Denken Sie daran, dass wir noch die Car-Klasse in Procedural FSM haben. Sobald Sie das Spiel sehen. Kopieren Sie die Aktion für den Status ENGINE_OFF
in die leere Methode. Die turnKeyOff()
-Methode sollte das widerspiegeln, was Sie unten sehen.
public function turnKeyOff ():void { _car.print ("The car's already off, you can't turn the key counter-clockwise any further..."); }
Die Anweisung trace()
wurde durch print()
ersetzt, die wir später zur Klasse Car hinzufügen.
Gehen Sie nun in die Methode turnKeyOn()
und fügen Sie den folgenden Code hinzu.
_car.print ("Turning the car on...the engine is now running!"); _car.changeState (_car.getEngineOnState ());
Überprüfen Sie es gegen Sie Zustandsübergangstabelle und prozedurale turnKeyOn()
-Methode für den ENGINE_OFF
-Zustand, um zu sehen, ob es gleich ist. Die changeState()
-Methode wird an das Fahrzeug delegiert, das im abgerufenen Status übergeben wird.
Der Rest der Methoden wird auf die gleiche Weise verarbeitet. Kopieren Sie den folgenden Code und ersetzen Sie die leeren Methoden damit.
public function driveForward ():void { _car.print ("click, changing the gear to drive doesn't do anything...the car is not running, returning the gear to park..."); } public function reFuel ():void { if (_car.hasFullTank () == false) { _car.print ("getting out of the car and adding " + Number (_car.refillWithFuel ()).toFixed (2) + " gallon(s) of fuel."); } } public function update ($tick:Number):void { } public function toString ():String { return 'off'; }
Die driveForward()
-Methode funktioniert genauso wie die procedure driveForward()
-Methoden, wobei _currentState
als ENGINE_OFF
festgelegt ist
reFuel()
fragt das Auto, ob der Tank nicht voll ist. Wenn nicht, wird das Auto dann mit Kraftstoff nachfüllen. Sie werden sehen, wie diese beiden Methoden funktionieren, wenn wir die Car-Klasse später ändern.
Die update()
-Methode bleibt leer, da das Auto nicht läuft.
toString()
funktioniert genauso wie die toString()
-Methode des Autos.
Das schließt die EngineOff-Klasse ab.
Bevor wir den Rest der anderen Zustandsklassen erstellen, ändern wir die Car-Klasse und konvertieren sie in eine eigene Zustandsmaschine.
Schritt 18: Die Car-Zustandsmaschine
Wichtig: Erstellen Sie eine doppelte Car-Klasse, bevor Sie das folgende Verfahren ausführen. Eine Textkopie würde ausreichen, aber speichern Sie sie als Referenz für später.
Anstatt die Änderungen Element für Element durchzugehen, kopiere einfach den unten stehenden Code und füge dann den Inhalt deiner Auto-Klasse ein und ersetze ihn.
package com.activeTuts.fsm { import flash.display.Sprite; import flash.events.Event; public class Car extends Sprite { ///CAR STATES //engine off state //engine on state //drive state //no gas state public static const ONE_SIXTH_SECONDS:Number = 1 / 6; //6 times per second public static const IDLE_FUEL_CONSUMPTION:Number = .0055; public static const DRIVE_FUEL_CONSUMPTION:Number = .011; private var _engineOffState:IState; private var _engineOnState:IState; private var _engineDriveForwardState:IState; private var _engineOutOfFuelState:IState; private var _fuelCapacity:Number = 1; private var _fuelSupply:Number = _fuelCapacity; //starting on a full tank (in gallons) private var _engineTimer:Number = 0; private var _currentState:IState; public function Car () { init (); } private function init ():void { initializeStates (); } private function initializeStates ():void { _engineOffState = new EngineOff (this); _engineOnState = new EngineOn (this); _engineDriveForwardState = new EngineDriveForward (this); _engineOutOfFuelState = new EngineOutOfFuel (this); _currentState = _engineOffState;//default state } public function update ($tick:Number):void { _currentState.update ($tick); } ///car functions public function turnKeyOn (e:Event = null):void { _currentState.turnKeyOn (); } public function turnKeyOff (e:Event = null):void { _currentState.turnKeyOff (); } public function driveForward (e:Event = null):void { _currentState.driveForward (); } public function reFuel (e:Event = null):void { _currentState.reFuel (); } public function consumeFuel ($consumption:Number):void { if ((_fuelSupply -= $consumption) <= 0) { _fuelSupply = 0; print ("the engine has stopped, no more fuel to run..."); changeState (_engineOutOfFuelState); } } public function refillWithFuel ():Number { var neededSupply:Number = _fuelCapacity - _fuelSupply; _fuelSupply += neededSupply; return neededSupply; } public function hasFullTank ():Boolean { var fullTank:Boolean = _fuelCapacity == _fuelSupply ? true : false; if (fullTank) print ("no need to refuel right now, the tank is full..."); return fullTank; } public function getEngineOffState ():IState { return _engineOffState; } //explicit, you know you're calling a method public function getEngineOnState ():IState { return _engineOnState; } public function getEngineOutOfFuelState ():IState { return _engineOutOfFuelState; } public function getEngineDriveForwardState ():IState { return _engineDriveForwardState; } public function changeState ($state:IState):void { _currentState = $state; } public function get engineTimer ():Number { return _engineTimer; } //implicit, as if you're accessing a public variable public function set engineTimer ($value:Number):void { _engineTimer = $value; } public function print ($text:String):void { trace ($text); } override public function toString ():String { return 'The car is currently ' + _currentState + ' with a fuel amount of ' + _fuelSupply + ' gallon(s).'; } } }
Lassen Sie uns über die Änderungen gehen.
Beginnt bei der Definition von Variablen. Sie werden feststellen, dass die vier Zustände den Typ in IState geändert haben und keine statischen Konstanten mehr sind.
Als nächstes ruft der Konstruktor init()
auf, der wiederum initializeState()
aufruft. Alle Zustandsklassen werden durch diese Methode instanziiert.
Dann kommt der einfache Teil, keine Switch-Anweisungen mehr. Das Auto delegiert die Aktionen nur an den aktuellen Status. Sehen Sie turnKeyOff()
runter zu reFuel()
Die Methode consumeFuel()
musste für EngineOn und EngineDriveForward öffentlich zugänglich werden.
Und dann die beiden Methoden, die wir in der reFuel(
)-Methode von EngineOff verwendet haben - hasFullTank()
und refillWithFuel()
.
Unter ihnen sind die expliziten Getter, die Zugriff für alle vier Zustände bereitstellen. Es mag wie ein merkwürdiges Protokoll erscheinen, aber es ist nur Verkapselung bei der Arbeit.
Das changeState()
tut genau das, was es sagt, es ändert den _currentState.
Nach der strengen Regel von OOP kann die _engineTimer
-Eigenschaft erneut über diese beiden Methoden aufgerufen und geändert werden: get engineTimer()
und set engineTimer()
.
print()
wird jetzt den String-Parameter nur an eine trace()
-Anweisung übergeben. Und dann die toString()
-Methode.
Schritt 19: Erstellen der anderen drei Zustände
Um die Erstellung der drei anderen Klassen zu vereinfachen, wechseln Sie in die Klasse Car innerhalb der initialize()
-Methode. Setzen Sie den Cursor in das Wort "EngineOn" und drücken Sie "CTRL + SHIFT + 1", um eine Eingabeaufforderung zu generieren. Wählen Sie "Neue Klasse erstellen" und drücken Sie "ENTER".
Passen Sie die Informationen wie in der Abbildung unten an und klicken Sie auf "OK".
Das ähnelt Schritt 16 beim Erstellen der EngineOff-Klasse. Nur dieses Mal haben wir FD Shortcuts benutzt. Außerdem werden Sie feststellen, dass das car-Objekt im Konstruktor bei der Instanziierung übergeben wurde. Vergessen Sie nicht, das "$"- Zeichen zum car-Parameter für Ihren Konstruktor und die einzige Klassenvariable _car
oben hinzuzufügen.
Vergleichen Sie es mit dem folgenden Code.
package com.activeTuts.fsm { public class EngineOn implements IState { private var _car:Car; public function EngineOn($car:Car) { _car = $car; } /* INTERFACE com.activeTuts.fsm.IState */ public function turnKeyOff ():void { } public function turnKeyOn ():void { } public function driveForward ():void { } public function reFuel ():void { } public function update ($tick:Number):void { } public function toString ():String { } } }
Gehen Sie nun zurück in die Methode initialize()
in Car und wiederholen Sie den Vorgang für die letzten beiden verbleibenden Statusklassen.
Schritt 20: Abschließen der Klasse EngineOn
Denken Sie, Sie können es mit der Car.txt (Duplikat) und Zustandsübergangstabelle zusammensetzen?
Probieren Sie es aus, folgen Sie einfach Schritt 17 und Sie werden es gut machen. Denken Sie daran, dass Sie jetzt an Aktionsergebnissen für ENGINE_ON
arbeiten.
Ausgezeichnet! Wenn Sie fertig sind, vergleichen Sie Ihren Code mit den Klassen im Ordner "StatePatternPartial1", die im Quelldownload enthalten sind.
Schritt 21: Letzte Prüfung
Wenn Sie mit allen Ihren State-Klassen fertig sind, gehen Sie zurück zu Main.as und führen Sie Ihre Anwendung aus.
Hoffentlich lief alles gut und Sie haben keine Fehler bekommen. Informationen sollten beginnen, aus dem Ausgabefeld zu drucken.
Wir beenden den ersten Teil des Tutorials hier. Für den zweiten Teil beginnen wir mit den beiden anderen Zuständen "EngineDriveReallyFast" und "EngineDriveBackward". Dann fügen wir Steuerelemente für Animation und Sound hinzu, um zu zeigen, wie einfach das Ändern und Skalieren ist.
Schlussfolgerung
Von all den Designmustern, mit denen ich gespielt habe, ist diese bei mir die wichtigste (vor allem im Bereich Game Design). Sie werden sehen, warum, wenn Sie damit beginnen, Ihr nächstes Spielobjekt zu erstellen. Warum versuchen Sie nicht, die Pistole zu erstellen, die ich am Anfang dieses Tutorials erwähnt habe? Sie werden genießen, es zu erstellen! Vergessen Sie nicht, klein anzufangen. Erstellen Sie es immer von Procedural FSM und konvertieren Sie es dann in das Zustandsmuster.
Hier sind die Schritte:
- Skizzieren Sie die Zustandsübergangstabelle für Ihr Objekt.
- Erstellen Sie Ihr prozedurales FSM-Objekt.
- Wenn Procedural FSM funktioniert, müssen Sie es in das Zustandsmuster konvertieren, wenn Sie viel mehr Funktionen und/oder Zustände hinzufügen müssen.
- Erstellen Sie zuerst Ihre IState-Schnittstelle.
- Erstellen Sie die erste/Standard-State-Klasse (siehe State Transition Table und Prozedurale FSM-Aktionen).
- Duplizieren Sie eine Kopie Ihres prozeduralen FSM-Objekts, und erlauben Sie dann den öffentlichen Zugriff auf alle Eigenschaften, die die Statusklassen steuern müssen.
- Erstellen Sie den Rest der State-Klassen.
- Fügen Sie Merkmale/Zustände gemäß Ihren Anforderungen hinzu. Diese präsentieren sich normalerweise, während Sie an Ihren Zustandsaktionen arbeiten.
Wir sehen uns im zweiten Teil!
Wie immer, für Kommentare, Vorschläge oder Bedenken, schreiben Sie bitte eine Notiz in den Kommentarbereich.
Danke fürs Lesen!