Implementieren des Befehlsentwurfsmusters in Unity

Bild

Haben Sie sich jemals gefragt, wie in Spielen wie Super Meat Boy die Wiedergabefunktion implementiert ist? Eine Möglichkeit, dies zu implementieren, besteht darin, die Eingabe auf die gleiche Weise wie der Player auszuführen, was wiederum bedeutet, dass die Eingabe irgendwie gespeichert werden muss. Sie können das Befehlsmuster für dieses und vieles mehr verwenden.

Die Befehlsvorlage ist auch nützlich, um Rückgängig- und Wiederherstellungsfunktionen in einem Strategiespiel zu erstellen.

In diesem Tutorial implementieren wir die Befehlsvorlage in C # und führen damit den Bot-Charakter durch ein dreidimensionales Labyrinth. Aus dem Tutorial lernen Sie:

  • Die Grundlagen des Befehlsmusters.
  • So implementieren Sie das Befehlsmuster
  • So erstellen Sie eine Warteschlange mit Eingabebefehlen und verzögern deren Ausführung.

Hinweis : Es wird davon ausgegangen, dass Sie bereits mit Unity vertraut sind und über durchschnittliche Kenntnisse in C # verfügen. In diesem Tutorial werden wir mit Unity 2019.1 und C # 7 arbeiten .

An die Arbeit gehen


Laden Sie zunächst die Projektmaterialien herunter. Entpacken Sie die Datei und öffnen Sie das Starter- Projekt in Unity.

Gehen Sie zu RW / Szenen und öffnen Sie die Hauptszene. Die Szene besteht aus einem Bot und einem Labyrinth sowie einer Terminal-Benutzeroberfläche, die Anweisungen anzeigt. Das Level-Design wird in Form eines Gitters erstellt, was nützlich ist, wenn wir den Bot visuell durch das Labyrinth bewegen.


Wenn Sie auf Wiedergabe klicken, werden wir feststellen, dass die Anweisungen nicht funktionieren. Dies ist normal, da wir diese Funktionalität zum Lernprogramm hinzufügen.

Der interessanteste Teil der Szene ist der GameObject Bot . Wählen Sie es im Hierarchiefenster aus, indem Sie darauf klicken.


Im Inspektor können Sie sehen, dass es eine Bot- Komponente gibt. Wir werden diese Komponente verwenden, indem wir Eingabebefehle ausgeben.


Wir verstehen die Logik des Bots


Gehen Sie zu RW / Scripts und öffnen Sie das Bot- Skript im Code-Editor. Sie müssen nicht wissen, was im Bot- Skript passiert. Schauen Sie sich jedoch zwei Methoden an: Move und Shoot . Auch hier müssen Sie nicht herausfinden, was in diesen Methoden vor sich geht, aber Sie müssen verstehen, wie Sie sie verwenden.

Beachten Sie, dass die Move Methode einen Eingabeparameter CardinalDirection empfängt. CardinalDirection ist eine Aufzählung. Ein Aufzählungselement vom Typ CardinalDirection kann Up , Down , Right oder Left . Abhängig von der ausgewählten CardinalDirection bewegt sich CardinalDirection Bot genau ein Quadrat entlang des Gitters in die entsprechende Richtung.


Die Shoot Methode zwingt den Bot, Granaten abzufeuern, die die gelben Wände zerstören, aber gegen andere Wände unbrauchbar sind.


Schauen Sie sich ResetToLastCheckpoint die ResetToLastCheckpoint Methode an. Um zu verstehen, was er tut, schauen Sie sich das Labyrinth an. Im Labyrinth befinden sich Punkte, die als Checkpoint bezeichnet werden . Um das Labyrinth zu passieren, muss der Bot zum grünen Kontrollpunkt gelangen.


Wenn ein Bot einen neuen Kontrollpunkt betritt, wird dies der letzte für ihn. ResetToLastCheckpoint setzt die Position des Bots zurück und verschiebt ihn zum letzten Kontrollpunkt.


Obwohl wir diese Methoden nicht verwenden können, werden wir sie bald beheben. Um zu beginnen, müssen Sie sich mit dem Entwurfsmuster des Befehls vertraut machen.

Was ist das Befehlsentwurfsmuster?


Das Befehlsmuster ist eines von 23 Entwurfsmustern, die im Buch Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software beschrieben sind, das von der „Viererbande“ von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides ( GoF , Viererbande) geschrieben wurde.

Die Autoren berichten, dass "das Befehlsmuster die Anforderung als Objekt kapselt, sodass wir andere Objekte mit anderen Anforderungen, Warteschlangen- oder Protokollanforderungen parametrisieren und umkehrbare Operationen unterstützen können."

Wow! Es ist wie?

Ich verstehe, dass diese Definition nicht sehr einfach ist, also lassen Sie uns sie analysieren.

Kapselung bedeutet, dass ein Methodenaufruf als Objekt gekapselt werden kann.


Die gekapselte Methode kann abhängig vom Eingabeparameter viele Objekte beeinflussen. Dies wird als Parametrisierung anderer Objekte bezeichnet.

Der resultierende „Befehl“ kann zusammen mit anderen Teams gespeichert werden, bis sie ausgeführt werden. Dies ist die Anforderungswarteschlange.


Team-Warteschlange

Umkehrbarkeit bedeutet schließlich, dass Vorgänge mit der Rückgängig-Funktion zurückgesetzt werden können.

OK, aber wie spiegelt sich das im Code wider?

Die Command- Klasse verfügt über eine Execute- Methode, die als Eingabeparameter das Objekt (von dem der Befehl ausgeführt wird) namens Receiver empfängt. Das heißt, die Execute-Methode wird tatsächlich von der Command-Klasse gekapselt .

Viele Instanzen der Command-Klasse können als normale Objekte übergeben werden, dh sie können in Datenstrukturen wie einer Warteschlange, einem Stapel usw. gespeichert werden.

Um einen Befehl auszuführen, müssen Sie seine Execute-Methode aufrufen. Die Klasse, die die Ausführung startet, heißt Invoker .

Das Projekt enthält derzeit eine leere Klasse namens BotCommand . Im nächsten Abschnitt werden wir die Implementierung des oben genannten implementieren, damit der Bot Aktionen mithilfe der Befehlsvorlage ausführen kann.

Bewegen Sie den Bot


Implementierung des Befehlsmusters


In diesem Abschnitt implementieren wir das Befehlsmuster. Es gibt viele Möglichkeiten, dies zu implementieren. In diesem Tutorial werden wir einen von ihnen behandeln.

Gehen Sie zunächst zu RW / Scripts und öffnen Sie das BotCommand- Skript im Editor. Die BotCommand Klasse BotCommand noch leer, aber nicht lange.

Fügen Sie den folgenden Code in die Klasse ein:

  //1 private readonly string commandName; //2 public BotCommand(ExecuteCallback executeMethod, string name) { Execute = executeMethod; commandName = name; } //3 public delegate void ExecuteCallback(Bot bot); //4 public ExecuteCallback Execute { get; private set; } //5 public override string ToString() { return commandName; } 

Was ist hier los?

  1. Die Variable commandName einfach zum Speichern des für Menschen lesbaren Befehlsnamens verwendet. Es ist nicht erforderlich, es in der Vorlage zu verwenden, aber wir werden es später im Tutorial benötigen.
  2. Der Konstruktor von BotCommand erhält eine Funktion und eine Zeichenfolge. Dies hilft uns beim Einrichten der Execute Methode des Command-Objekts und seines name .
  3. Der ExecuteCallback Delegat definiert den Typ der gekapselten Methode. Die gekapselte Methode gibt void zurück und akzeptiert als Eingabeparameter ein Objekt vom Typ Bot (Komponente Bot ).
  4. Die Execute Eigenschaft bezieht sich auf die gekapselte Methode. Wir werden es verwenden, um die gekapselte Methode aufzurufen.
  5. Die ToString Methode wird überschrieben, um die Zeichenfolge commandName . Dies ist beispielsweise für die Verwendung in der Benutzeroberfläche praktisch.

Speichern Sie die Änderungen und das wars! Wir haben das Befehlsmuster erfolgreich implementiert.

Es bleibt zu benutzen.

Teambuilding


Öffnen Sie den BotInputHandler im Ordner RW / Scripts .

Hier erstellen wir fünf Instanzen von BotCommand . Diese Instanzen enthalten Methoden zum Verschieben des GameObject-Bot nach oben, unten, links und rechts sowie zum Schießen.

Um dies zu implementieren, fügen Sie Folgendes in diese Klasse ein:

  //1 private static readonly BotCommand MoveUp = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Up); }, "moveUp"); //2 private static readonly BotCommand MoveDown = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Down); }, "moveDown"); //3 private static readonly BotCommand MoveLeft = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Left); }, "moveLeft"); //4 private static readonly BotCommand MoveRight = new BotCommand(delegate (Bot bot) { bot.Move(CardinalDirection.Right); }, "moveRight"); //5 private static readonly BotCommand Shoot = new BotCommand(delegate (Bot bot) { bot.Shoot(); }, "shoot"); 

In jedem dieser Fälle wird eine anonyme Methode an den Konstruktor übergeben. Diese anonyme Methode wird in das entsprechende Befehlsobjekt eingekapselt. Wie Sie sehen, erfüllt die Signatur jeder der anonymen Methoden die vom ExecuteCallback Delegaten angegebenen Anforderungen.

Darüber hinaus ist der zweite Parameter für den Konstruktor eine Zeichenfolge, die den Namen des Befehls angibt. Dieser Name wird von der ToString Methode der Befehlsinstanz zurückgegeben. Später werden wir es für die Benutzeroberfläche anwenden.

In den ersten vier Fällen rufen anonyme Methoden die Move Methode für das bot Objekt auf. Ihre Eingabeparameter sind jedoch unterschiedlich.

Die MoveUp , MoveDown , MoveLeft und MoveRight übergeben die Move Parameter CardinalDirection.Up , CardinalDirection.Down , CardinalDirection.Left und CardinalDirection.Right . Wie im Abschnitt Was ist das Befehlsentwurfsmuster erwähnt, geben sie verschiedene Richtungen an, in die sich der GameObject-Bot bewegen soll.

In der fünften Instanz ruft die anonyme Methode die Shoot Methode für das bot Objekt auf. Dank dessen feuert der Bot während der Ausführung des Befehls eine Shell ab.

Nachdem wir die Befehle erstellt haben, müssen wir irgendwie darauf zugreifen, wenn der Benutzer eine Eingabe vornimmt.

BotInputHandler unmittelbar nach den Befehlsinstanzen BotInputHandler folgenden Code in den BotInputHandler :

  public static BotCommand HandleInput() { if (Input.GetKeyDown(KeyCode.W)) { return MoveUp; } else if (Input.GetKeyDown(KeyCode.S)) { return MoveDown; } else if (Input.GetKeyDown(KeyCode.D)) { return MoveRight; } else if (Input.GetKeyDown(KeyCode.A)) { return MoveLeft; } else if (Input.GetKeyDown(KeyCode.F)) { return Shoot; } return null; } 

Die HandleInput Methode gibt abhängig von der vom Benutzer gedrückten Taste eine Instanz des Befehls zurück. Speichern Sie Ihre Änderungen, bevor Sie fortfahren.

Befehle anwenden


Großartig, jetzt ist es Zeit, die von uns erstellten Teams einzusetzen. Gehen Sie erneut zu RW / Scripts und öffnen Sie das SceneManager- Skript im Editor. In dieser Klasse sehen Sie einen Link zu einer uiManager Variablen vom Typ UIManager .

Die UIManager Klasse bietet nützliche UIManager für die Terminal-Benutzeroberfläche , die wir in dieser Szene verwenden. Wenn die Methode von UIManager verwendet wird, wird im Lernprogramm erläutert, was sie tut. Für unsere Zwecke ist es jedoch im Allgemeinen nicht erforderlich, die interne Struktur zu kennen.

Darüber hinaus bezieht sich die bot Variable auf die Bot-Komponente, die an den GameObject- Bot angehängt ist.

SceneManager Sie nun der SceneManager Klasse den folgenden Code SceneManager und ersetzen Sie ihn durch Kommentar //1 :

  //1 private List<BotCommand> botCommands = new List<BotCommand>(); private Coroutine executeRoutine; //2 private void Update() { if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else { CheckForBotCommands(); } } //3 private void CheckForBotCommands() { var botCommand = BotInputHandler.HandleInput(); if (botCommand != null && executeRoutine == null) { AddToCommands(botCommand); } } //4 private void AddToCommands(BotCommand botCommand) { botCommands.Add(botCommand); //5 uiManager.InsertNewText(botCommand.ToString()); } //6 private void ExecuteCommands() { if (executeRoutine != null) { return; } executeRoutine = StartCoroutine(ExecuteCommandsRoutine()); } private IEnumerator ExecuteCommandsRoutine() { Debug.Log("Executing..."); //7 uiManager.ResetScrollToTop(); //8 for (int i = 0, count = botCommands.Count; i < count; i++) { var command = botCommands[i]; command.Execute(bot); //9 uiManager.RemoveFirstTextLine(); yield return new WaitForSeconds(CommandPauseTime); } //10 botCommands.Clear(); bot.ResetToLastCheckpoint(); executeRoutine = null; } 

Wow, wie viel Code! Aber mach dir keine Sorgen; Wir sind endlich bereit für den ersten echten Start des Projekts im Spielfenster.

Ich werde den Code später erklären. Denken Sie daran, die Änderungen zu speichern.

Führen Sie das Spiel aus, um die Befehlsvorlage zu testen


Jetzt ist also die Zeit zu bauen; Klicken Sie im Unity-Editor auf Wiedergabe .

Sie sollten in der Lage sein, Bewegungsbefehle mit den WASD-Tasten einzugeben . Drücken Sie die F- Taste, um den Aufnahmebefehl einzugeben. Drücken Sie die Eingabetaste, um Befehle auszuführen.

Hinweis : Bis zum Abschluss des Ausführungsprozesses ist die Eingabe neuer Befehle nicht möglich.



Beachten Sie, dass der Terminal-Benutzeroberfläche Zeilen hinzugefügt werden. Teams in der Benutzeroberfläche werden durch ihre Namen angezeigt. Möglich wird dies durch die Variable commandName .

Beachten Sie außerdem, wie die Benutzeroberfläche vor der Ausführung einen Bildlauf durchführt und wie die Zeilen während der Ausführung gelöscht werden.

Wir studieren die Teams genauer


Es ist Zeit, den Code zu lernen, den wir im Abschnitt "Anwenden von Befehlen" hinzugefügt haben:

  1. In der Liste botCommands werden Links zu Instanzen von BotCommand . Denken Sie daran, dass wir zum Speichern von Speicher nur fünf Instanzen von Befehlen erstellen können, es jedoch möglicherweise mehrere Verweise auf einen Befehl gibt. Darüber hinaus executeCoroutine Variable executeCoroutine auf ExecuteCommandsRoutine , die die Ausführung des Befehls steuert.
  2. Update prüft, ob der Benutzer die Eingabetaste gedrückt hat. In diesem ExecuteCommands wird CheckForBotCommands ExecuteCommands , andernfalls wird CheckForBotCommands .
  3. CheckForBotCommands verwendet die statische HandleInput Methode des BotInputHandler , um zu überprüfen, ob der Benutzer die Eingabe abgeschlossen hat. In diesem BotInputHandler wird der Befehl zurückgegeben . Der zurückgegebene Befehl wird an AddToCommands . Wenn jedoch die Befehle ausgeführt werden, d.h. Wenn executeRoutine nicht null ist, wird es zurückgegeben, ohne dass etwas an AddToCommands . Das heißt, der Benutzer muss bis zum Abschluss warten.
  4. AddToCommands fügt der zurückgegebenen Instanz des Befehls in botCommands einen neuen Link botCommands .
  5. Die InsertNewText Methode der InsertNewText Klasse fügt der Terminal-Benutzeroberfläche eine neue Textzeile hinzu. Eine Textzeichenfolge ist eine Zeichenfolge, die als Eingabeparameter übergeben wird. In diesem Fall übergeben wir den Befehlsnamen.
  6. Die ExecuteCommands Methode startet ExecuteCommandsRoutine .
  7. ResetScrollToTop vom UIManager scrollt die Benutzeroberfläche des Terminals nach oben. Dies erfolgt kurz vor Beginn der Ausführung.
  8. ExecuteCommandsRoutine enthält eine for Schleife, die die Befehle in der botCommands Liste botCommands und sie botCommands ausführt, wobei das bot Objekt an die von der Execute Eigenschaft zurückgegebene Methode übergeben wird. Nach jeder Ausführung wird in CommandPauseTime Sekunden eine Pause hinzugefügt.
  9. Die RemoveFirstTextLine Methode von UIManager löscht die allererste Textzeile in der Terminal-Benutzeroberfläche, falls vorhanden. Das heißt, wenn ein Befehl ausgeführt wird, wird sein Name von der Benutzeroberfläche entfernt.
  10. Nachdem alle Befehle botCommands gelöscht und der Bot wird mit ResetToLastCheckpoint auf den letzten Haltepunkt ResetToLastCheckpoint . Am Ende ist executeRoutine null und der Benutzer kann weiterhin Befehle eingeben.

Implementieren der Funktionen "Rückgängig" und "Wiederherstellen"


Führen Sie die Szene erneut aus und versuchen Sie, zum grünen Kontrollpunkt zu gelangen.

Sie werden feststellen, dass wir den eingegebenen Befehl nicht abbrechen können. Wenn Sie also einen Fehler machen, können Sie erst zurückkehren, wenn Sie alle eingegebenen Befehle ausgeführt haben. Sie können dies beheben, indem Sie die Funktionen " Rückgängig" und " Wiederherstellen" hinzufügen.

Gehen Sie zurück zu SceneManager.cs und fügen Sie unmittelbar nach der List- Deklaration für botCommands die folgende Variablendeklaration botCommands :

  private Stack<BotCommand> undoStack = new Stack<BotCommand>(); 

Die Variable undoStack ist ein Stapel (aus der Collections-Familie), in dem alle Verweise auf Befehle gespeichert werden, die rückgängig gemacht werden können.

Jetzt fügen wir zwei Methoden UndoCommandEntry und RedoCommandEntry , die Undo und Redo ausführen. SceneManager in der SceneManager Klasse nach ExecuteCommandsRoutine SceneManager folgenden Code ExecuteCommandsRoutine :

  private void UndoCommandEntry() { //1 if (executeRoutine != null || botCommands.Count == 0) { return; } undoStack.Push(botCommands[botCommands.Count - 1]); botCommands.RemoveAt(botCommands.Count - 1); //2 uiManager.RemoveLastTextLine(); } private void RedoCommandEntry() { //3 if (undoStack.Count == 0) { return; } var botCommand = undoStack.Pop(); AddToCommands(botCommand); } 

Lassen Sie uns den Code analysieren:

  1. Wenn Befehle ausgeführt werden oder die botCommands Liste leer ist, führt die UndoCommandEntry Methode nichts aus. Andernfalls wird ein Link zum letzten auf dem undoStack Stapel eingegebenen Befehl undoStack . Dadurch wird auch die Verknüpfung zum Befehl aus der botCommands Liste entfernt.
  2. Die RemoveLastTextLine Methode von UIManager entfernt die letzte Textzeile von der Terminal-Benutzeroberfläche, sodass die Benutzeroberfläche mit dem Inhalt von botCommands .
  3. Wenn der undoStack Stapel leer ist, führt RedoCommandEntry nichts aus. Andernfalls wird der letzte Befehl aus dem oberen Bereich von undoStack und mithilfe von AddToCommands wieder zur botCommands Liste AddToCommands .

Jetzt werden wir Tastatureingaben hinzufügen, um diese Funktionen zu verwenden. SceneManager der SceneManager Klasse den Hauptteil der Update Methode durch den folgenden Code:

  if (Input.GetKeyDown(KeyCode.Return)) { ExecuteCommands(); } else if (Input.GetKeyDown(KeyCode.U)) //1 { UndoCommandEntry(); } else if (Input.GetKeyDown(KeyCode.R)) //2 { RedoCommandEntry(); } else { CheckForBotCommands(); } 

  1. Wenn Sie die U- Taste drücken, wird die UndoCommandEntry Methode UndoCommandEntry .
  2. Wenn Sie die Taste R drücken, wird die RedoCommandEntry Methode RedoCommandEntry .

Edge Case Handling


Großartig, wir sind fast fertig! Aber zuerst müssen wir Folgendes tun:

  1. Bei der Eingabe eines neuen Befehls sollte der undoStack Stapel gelöscht werden.
  2. Vor dem Ausführen von Befehlen muss der undoStack Stack gelöscht werden.

Um dies zu implementieren, müssen wir SceneManager zunächst eine neue Methode SceneManager . Fügen Sie nach CheckForBotCommands die folgende Methode CheckForBotCommands :

  private void AddNewCommand(BotCommand botCommand) { undoStack.Clear(); AddToCommands(botCommand); } 

Diese Methode löscht undoStack und ruft dann die AddToCommands Methode auf.

Ersetzen Sie nun den Aufruf von AddToCommands in CheckForBotCommands durch den folgenden Code:

  AddNewCommand(botCommand); 

ExecuteCommands dann die folgende Zeile nach der if in die ExecuteCommands Methode ein, um sie zu löschen, bevor Sie undoStack Befehle ausführen:

  undoStack.Clear(); 

Und wir sind endlich fertig!

Speichern Sie Ihre Arbeit. Erstellen Sie das Projekt und klicken Sie im Play- Editor. Geben Sie die Befehle wie zuvor ein. Drücken Sie U , um die Befehle abzubrechen. Drücken Sie R , um die abgebrochenen Befehle zu wiederholen.


Versuchen Sie, zum grünen Kontrollpunkt zu gelangen.

Wohin als nächstes?


Um mehr über die in der Spielprogrammierung verwendeten Entwurfsmuster zu erfahren, empfehle ich Ihnen, die Spielprogrammierungsmuster von Robert Nystrom zu studieren.

Um mehr über fortgeschrittene C # -Techniken zu erfahren , besuchen Sie den Kurs C # -Sammlungen, Lambdas und LINQ .

Aufgabe


Versuchen Sie als Aufgabe, zum grünen Kontrollpunkt am Ende des Labyrinths zu gelangen. Ich habe eine der Lösungen unter dem Spoiler versteckt.

Lösung
  • moveUp × 2
  • moveRight × 3
  • moveUp × 2
  • moveLeft
  • schießen
  • moveLeft × 2
  • moveUp × 2
  • moveLeft × 2
  • moveDown × 5
  • moveLeft
  • schießen
  • moveLeft
  • moveUp × 3
  • schießen × 2
  • moveUp × 5
  • moveRight × 3

Source: https://habr.com/ru/post/de463953/


All Articles