Implementieren der Statusvorlage in Unity

Bild

Beim Programmieren von In-Game-Entitäten treten Situationen auf, in denen sie unter unterschiedlichen Bedingungen auf unterschiedliche Weise agieren müssen, was auf die Verwendung von Zuständen hindeutet.

Wenn Sie sich jedoch für Brute Force entscheiden, verwandelt sich der Code schnell in ein Chaos mit vielen verschachtelten if-else-Anweisungen.

Für eine ordnungsgemäße Lösung dieses Problems können Sie das Entwurfsmuster "Status" verwenden. Wir werden ihm dieses Tutorial widmen!

Aus dem Tutorial Sie:

  • Lernen Sie die Grundlagen der Statusvorlage in Unity kennen.
  • Sie erfahren, was eine Zustandsmaschine ist und wann sie verwendet werden muss.
  • Erfahren Sie, wie Sie diese Konzepte verwenden, um die Bewegung Ihres Charakters zu steuern.

Hinweis : Dieses Tutorial richtet sich an fortgeschrittene Benutzer. Es wird davon ausgegangen, dass Sie bereits mit Unity vertraut sind und über durchschnittliche Kenntnisse in C # verfügen. Darüber hinaus verwendet dieses Lernprogramm Unity 2019.2 und C # 7.

An die Arbeit gehen


Projektmaterialien herunterladen. Entpacken Sie die Zip-Datei und öffnen Sie das Startprojekt in Unity.

Das Projekt enthält mehrere Ordner, die Ihnen den Einstieg erleichtern. Der Ordner Assets / RW enthält die Ordner Animationen , Materialien , Modelle , Prefabs , Ressourcen , Szenen , Skripte und Sounds , die nach den darin enthaltenen Ressourcen benannt sind.

Um das Tutorial zu vervollständigen, arbeiten wir nur mit Szenen und Skripten .

Gehen Sie zu RW / Scenes und öffnen Sie Main . Im Spielmodus sehen Sie eine Figur in einer Kapuze in einer mittelalterlichen Burg.


Klicken Sie auf " Abspielen" und beobachten Sie, wie sich die Kamera an den Zeichenrahmen anpasst. Momentan gibt es in unserem kleinen Spiel keine Interaktionen, wir werden sie im Tutorial bearbeiten.


Erkunde den Charakter


Wählen Sie in der Hierarchie Zeichen . Überprüfen Sie den Inspector . Sie sehen eine Komponente mit demselben Namen, die die Steuerlogik für Zeichen enthält.


Öffnen Sie Character.cs in RW / Scripts .

Das Skript führt viele Aktionen aus, die meisten sind jedoch für uns nicht wichtig. Beachten wir zunächst die folgenden Methoden.

  • Move : Bewegt den Charakter und empfängt Werte vom Typ Gleitgeschwindigkeit als Bewegungsgeschwindigkeit und rotationSpeed als Winkelgeschwindigkeit.
  • ResetMoveParams : Diese Methode setzt die Parameter zurück, mit denen die Bewegung und die Winkelgeschwindigkeit des Zeichens animiert werden . Es wird nur zur Reinigung verwendet.
  • SetAnimationBool : param Parameter param animation vom Typ Bool auf value.
  • CheckCollisionOverlap : Er empfängt einen Typ Vector3 und gibt ein Vector3 zurück, das bestimmt, ob sich innerhalb des angegebenen Radius vom Vector3 befinden.
  • TriggerAnimation : TriggerAnimation den Animationsparameter des Eingabeparameters param .
  • ApplyImpulse : ApplyImpulse Impuls auf Character an, der dem Eingabeparameter force Typ Vector3 .

Nachfolgend sehen Sie diese Methoden. In unserem Tutorial sind deren Inhalt und interne Arbeit nicht wichtig.

Was sind Zustandsautomaten?


Eine Zustandsmaschine ist ein Konzept, bei dem ein Container den Zustand von etwas zu einem bestimmten Zeitpunkt speichert. Basierend auf den Eingabedaten kann es abhängig vom aktuellen Status eine Schlussfolgerung geben, die in diesem Prozess in einen neuen Status übergeht. Zustandsautomaten können als Zustandsdiagramm dargestellt werden . Wenn Sie ein Zustandsdiagramm erstellen, können Sie alle möglichen Zustände des Systems und die Übergänge zwischen ihnen durchdenken.

Zustandsautomaten


Finite State Machines oder FSM (Finite State Machine) ist eine der vier Hauptfamilien von Maschinen . Automaten sind abstrakte Modelle einfacher Maschinen. Sie werden im Rahmen der Automatentheorie - dem theoretischen Zweig der Informatik - untersucht.

Kurzgesagt:

  • FSM besteht aus einer endlichen Menge von Bedingungen . Zu jedem Zeitpunkt ist nur einer dieser Zustände aktiv .
  • Jeder Zustand bestimmt, in welchen Zustand er als Ausgabe geht, basierend auf der empfangenen Sequenz eingehender Informationen .
  • Der Ausgangszustand wird zum neuen aktiven Zustand. Mit anderen Worten, es gibt einen Übergang zwischen Zuständen .


Um dies besser zu verstehen, betrachten Sie den Charakter eines Plattformspiels, das vor Ort ist. Der Charakter befindet sich im stehenden Zustand. Dies ist sein aktiver Zustand, bis der Spieler den Knopf drückt, so dass der Charakter springt.

Der Status " Stehend" kennzeichnet einen Tastendruck als signifikanten Eingang und wechselt als Ausgang in den Status " Springen" .

Angenommen, es gibt eine bestimmte Anzahl solcher Bewegungszustände, und ein Charakter kann sich jeweils nur in einem der Zustände befinden. Dies ist ein Beispiel für FSM.

Hierarchische Zustandsmaschinen


Stellen Sie sich einen Plattformer mit FSM vor, in dem mehrere Zustände eine gemeinsame Physiklogik aufweisen. Sie können sich beispielsweise in den Zuständen Hocken und Stehen bewegen und springen. In diesem Fall führen mehrere eingehende Variablen für zwei verschiedene Zustände zur gleichen Verhaltens- und Informationsausgabe.

In einer solchen Situation wäre es logisch, das allgemeine Verhalten an einen anderen Staat zu delegieren. Glücklicherweise kann dies mit hierarchischen Zustandsautomaten erreicht werden.

In einem hierarchischen FSM gibt es Unterzustände, die eingehende Rohdaten an ihre Unterzustände delegieren . Auf diese Weise können Sie die Größe und Komplexität des FSM unter Beibehaltung seiner Logik elegant reduzieren.

Statusvorlage


In ihrem Buch Design Patterns: Elemente wiederverwendbarer objektorientierter Software definierten Erich Gamma, Richard Helm, Ralph Johnson und John Vlissidis ( Die Viererbande ) die Aufgabe der Vorlage State wie folgt:

„Er muss dem Objekt erlauben, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert. In diesem Fall hat das Objekt anscheinend seine Klasse geändert. “

Um dies besser zu verstehen, betrachten Sie das folgende Beispiel:

  • Ein Skript, das eingehende Informationen für die Bewegungslogik empfängt, ist an eine Entität im Spiel angehängt.
  • Diese Klasse speichert eine aktuelle Statusvariable, die sich einfach auf eine Instanz der Statusklasse bezieht.
  • Eingehende Informationen werden an diesen aktuellen Status delegiert, der sie verarbeitet und ein in sich selbst definiertes Verhalten erzeugt. Es behandelt auch die erforderlichen Zustandsübergänge.

Aufgrund der Tatsache, dass sich die aktuelle Statusvariable zu unterschiedlichen Zeitpunkten auf unterschiedliche Status bezieht, scheint es, dass sich dieselbe Skriptklasse unterschiedlich verhält. Dies ist das Wesentliche der Vorlage "Status".

In unserem Projekt verhält sich die oben genannte Zeichenklasse je nach Status unterschiedlich. Aber wir brauchen ihn, um sich zu benehmen!


Im Allgemeinen gibt es drei Schlüsselpunkte für jede Zustandsklasse, die das Verhalten des gesamten Staates ermöglichen:

  • Eintrag : Dies ist der Moment, in dem eine Entität in einen Zustand eintritt und Aktionen ausführt, die beim Eintritt in den Zustand nur einmal ausgeführt werden müssen.
  • Beenden : Ähnlich wie bei der Eingabe werden hier alle Rücksetzvorgänge ausgeführt, die nur ausgeführt werden müssen, bevor sich der Status ändert.
  • Update-Schleife : Hier ist die grundlegende Update-Logik , die in jedem Frame ausgeführt wird. Es kann in mehrere Teile unterteilt werden, z. B. einen Zyklus zum Aktualisieren der Physik und einen Zyklus zum Verarbeiten der Spielereingaben.


Definieren eines Zustands und einer Zustandsmaschine


Gehen Sie zu RW / Scripts und öffnen Sie StateMachine.cs .

Die Zustandsmaschine stellt , wie Sie vielleicht vermuten, eine Abstraktion für die Zustandsmaschine bereit. Beachten Sie, dass sich CurrentState korrekt in dieser Klasse befindet. Es wird eine Verknüpfung zum aktuellen Status der aktiven Statusmaschine gespeichert.

Um das Konzept des Status zu definieren, rufen Sie RW / Scripts auf und öffnen Sie das Skript State.cs in der IDE.

State ist eine abstrakte Klasse, die wir als Modell verwenden, aus dem alle Klassen von Projektstatus abgeleitet werden. Ein Teil des Codes in den Projektmaterialien ist bereits fertig.

DisplayOnUI zeigt nur den Namen des aktuellen Status in der Bildschirmbenutzeroberfläche an. Sie müssen das interne Gerät nicht kennen. Sie müssen lediglich verstehen, dass es einen Enumerator des Typs UIManager.Alignment als Eingabeparameter empfängt, der Left oder Right . Die Anzeige des Namens des Status im linken oder rechten unteren Teil des Bildschirms hängt davon ab.

Zusätzlich gibt es zwei geschützte Variablen, character und stateMachine . Die character verweist auf eine Instanz der Character- Klasse, und stateMachine verweist auf eine Instanz der Zustandsmaschine, die dem Zustand zugeordnet ist.

Beim Erstellen einer stateMachine bindet der Konstruktor character und stateMachine .

Jede der vielen Instanzen von Character in einer Szene kann einen eigenen Satz von Zuständen und Zustandsautomaten haben.

Fügen Sie State.cs nun die folgenden Methoden hinzu und speichern Sie die Datei:

 public virtual void Enter() { DisplayOnUI(UIManager.Alignment.Left); } public virtual void HandleInput() { } public virtual void LogicUpdate() { } public virtual void PhysicsUpdate() { } public virtual void Exit() { } 

Diese virtuellen Methoden definieren die oben beschriebenen Hauptstatuspunkte. Wenn die Zustandsmaschine einen Übergang zwischen Zuständen durchführt, rufen wir Exit für den vorherigen Zustand auf und Enter neuen aktiven Zustand ein .

HandleInput , LogicUpdate und PhysicsUpdate definieren zusammen eine Aktualisierungsschleife . HandleInput verarbeitet die Player-Eingabe. LogicUpdate verarbeitet grundlegende Logik, während PhyiscsUpdate Logik- und Physikberechnungen verarbeitet.

Öffnen Sie nun StateMachine.cs erneut, fügen Sie die folgenden Methoden hinzu und speichern Sie die Datei:

 public void Initialize(State startingState) { CurrentState = startingState; startingState.Enter(); } public void ChangeState(State newState) { CurrentState.Exit(); CurrentState = newState; newState.Enter(); } 

Initialize konfiguriert den Zustandsautomaten, indem CurrentState auf startingState und die Enter dafür CurrentState . Dies initialisiert die Zustandsmaschine und setzt zum ersten Mal den aktiven Zustand.

ChangeState behandelt ChangeState . Es ruft Exit für den alten CurrentState bevor die Referenz durch newState . Am Ende wird Enter für newState .

So setzen wir den Zustand und die Zustandsmaschine .

Bewegungszustände erstellen


Schauen Sie sich das folgende Zustandsdiagramm an, in dem die verschiedenen Bewegungszustände der In-Game-Essenz des Spielers dargestellt sind. In diesem Abschnitt implementieren wir die Vorlage "Status" für die in der FSM- Abbildung gezeigte Bewegung :


Achten Sie auf die Bewegungszustände Stehen , Ducken und Springen sowie darauf, wie die eingehenden Daten Übergänge zwischen den Zuständen verursachen. Dies ist eine hierarchische FSM, in der Grounded ein Unterzustand für die Unterzustände Ducking und Standing ist .

Kehren Sie zu Unity zurück und gehen Sie zu RW / Scripts / States . Dort finden Sie mehrere C # -Dateien mit Namen, die auf State enden.

Jede dieser Dateien definiert eine Klasse, von denen jede von State geerbt wird. Daher definieren diese Klassen die Zustände, die wir im Projekt verwenden werden.

Öffnen Sie nun Character.cs aus dem RW / Scripts- Ordner.

#region Variables über die Datei #region Variables und fügen Sie den folgenden Code hinzu:

 public StateMachine movementSM; public StandingState standing; public DuckingState ducking; public JumpingState jumping; 

Diese movementSM bezieht sich auf eine Zustandsmaschine, die die Bewegungslogik für die Character Instanz verarbeitet. Wir haben auch Links zu drei Zuständen hinzugefügt, die wir für jede Art von Bewegung implementieren.

Gehen #region MonoBehaviour Callbacks in derselben Datei zu #region MonoBehaviour Callbacks . Fügen Sie die folgenden MonoBehaviour- Methoden hinzu und speichern Sie sie

 private void Start() { movementSM = new StateMachine(); standing = new StandingState(this, movementSM); ducking = new DuckingState(this, movementSM); jumping = new JumpingState(this, movementSM); movementSM.Initialize(standing); } private void Update() { movementSM.CurrentState.HandleInput(); movementSM.CurrentState.LogicUpdate(); } private void FixedUpdate() { movementSM.CurrentState.PhysicsUpdate(); } 

  • In Start Code eine Instanz der Zustandsmaschine und weist sie movementSM . Außerdem werden verschiedene Bewegungszustände instanziiert. Bei der Erstellung der einzelnen Bewegungszustände übergeben wir unter Verwendung des this sowie der movementSM Instanz Verweise auf die Character Instanz. Am Ende rufen wir Initialize for movementSM und übergeben Standing als Ausgangszustand.
  • In der Update Methode rufen wir HandleInput und LogicUpdate für den CurrentState der movementSM Maschine auf. Ebenso FixedUpdate wir in FixedUpdate PhysicsUpdate für den CurrentState der movementSM Maschine auf. Im Wesentlichen delegiert dies Aufgaben an einen aktiven Status. Dies ist die Bedeutung der Vorlage "Status".

Jetzt müssen wir das Verhalten in jedem der Bewegungszustände einstellen. Machen Sie sich bereit, es wird eine Menge Code geben!

Stehend fest


Kehren Sie im Projektfenster zu RW / Scripts / States zurück.

Öffnen Sie Grounded.cs, und beachten Sie, dass diese Klasse über einen Konstruktor verfügt, der dem State Konstruktor entspricht. Das ist logisch, weil diese Klasse davon erbt. Sie werden dasselbe in allen anderen Staatsklassen sehen.

Fügen Sie den folgenden Code hinzu:

 public override void Enter() { base.Enter(); horizontalInput = verticalInput = 0.0f; } public override void Exit() { base.Exit(); character.ResetMoveParams(); } public override void HandleInput() { base.HandleInput(); verticalInput = Input.GetAxis("Vertical"); horizontalInput = Input.GetAxis("Horizontal"); } public override void PhysicsUpdate() { base.PhysicsUpdate(); character.Move(verticalInput * speed, horizontalInput * rotationSpeed); } 

Folgendes passiert hier:

  • Wir definieren eine der in der Elternklasse definierten virtuellen Methoden neu. Um die gesamte Funktionalität des übergeordneten Elements beizubehalten, rufen wir die base mit demselben Namen für jede überschriebene Methode auf. Dies ist eine wichtige Vorlage, die wir weiterhin verwenden werden.
  • Die nächste Zeile, Enter setzt horizontalInput und verticalInput ihre Standardwerte.
  • Innerhalb von Exit rufen wir, wie oben erwähnt, die ResetMoveParams Methode des , um sie zurückzusetzen, wenn Sie in einen anderen Status wechseln.
  • In der HandleInput Methode zwischenspeichern die Variablen horizontalInput und verticalInput HandleInput Werte der horizontalen und vertikalen Eingabeachse. Dank dessen kann der Spieler den Charakter mit den Tasten W , A , S und D steuern .
  • Bei PhysicsUpdate wir Move und übergeben die Variablen horizontalInput und verticalInput multipliziert mit den entsprechenden Geschwindigkeiten. In der variablen speed die Bewegungsgeschwindigkeit und in rotationSpeed die Winkelgeschwindigkeit gespeichert.

Öffnen Sie nun Standing.cs und achten Sie darauf, dass es von Grounded erbt. Es ist passiert, weil Standing , wie wir oben sagten, ein Substate für Grounded ist . Es gibt verschiedene Möglichkeiten, diese Beziehung zu implementieren. In diesem Lernprogramm wird jedoch die Vererbung verwendet.

Fügen Sie die folgenden override und speichern Sie das Skript:

 public override void Enter() { base.Enter(); speed = character.MovementSpeed; rotationSpeed = character.RotationSpeed; crouch = false; jump = false; } public override void HandleInput() { base.HandleInput(); crouch = Input.GetButtonDown("Fire3"); jump = Input.GetButtonDown("Jump"); } public override void LogicUpdate() { base.LogicUpdate(); if (crouch) { stateMachine.ChangeState(character.ducking); } else if (jump) { stateMachine.ChangeState(character.jumping); } } 

  • In Enter konfigurieren wir die von Grounded geerbten Variablen. Wenden Sie die MovementSpeed und RotationSpeed Charakters auf speed und rotationSpeed . Dann beziehen sie sich jeweils auf die normale Bewegungsgeschwindigkeit und die Winkelgeschwindigkeit, die für das Wesen des Charakters bestimmt sind.

    Darüber hinaus werden Variablen zum Speichern von crouch Eingaben und jump auf false zurückgesetzt.
  • In HandleInput speichern die Variablen HandleInput und jump die Eingaben des Spielers für Kniebeugen und Sprünge. Wenn der Spieler in der Hauptszene die Umschalttaste drückt , wird die Hocke auf true gesetzt. Ebenso kann ein Spieler mit der Leertaste jump .
  • In LogicUpdate überprüfen wir die Variablen LogicUpdate und jump vom Typ bool . Wenn crouch true ist, ändert sich movementSM.CurrentState in character.ducking . Wenn jump true ist, ändert sich der Status in character.jumping .

Speichern und montieren Sie das Projekt und klicken Sie dann auf Wiedergabe . Mit den Tasten W , A , S und D können Sie sich in der Szene bewegen . Wenn Sie versuchen, die Umschalt- oder Leertaste zu drücken, tritt ein unerwartetes Verhalten auf, da die entsprechenden Status noch nicht implementiert sind.


Versuchen Sie, sich unter den Tischobjekten zu bewegen. Sie werden sehen, dass dies aufgrund der Höhe des Colliders des Charakters nicht möglich ist. Damit der Charakter dies tun kann, müssen Sie das Verhalten der Hocke hinzufügen.

Wir klettern unter den Tisch


Öffnen Sie das Skript Ducking.cs . Beachten Sie, dass Ducking aus den gleichen Gründen wie Standing auch von der Grounded Klasse erbt. Fügen Sie die folgenden override und speichern Sie das Skript:

 public override void Enter() { base.Enter(); character.SetAnimationBool(character.crouchParam, true); speed = character.CrouchSpeed; rotationSpeed = character.CrouchRotationSpeed; character.ColliderSize = character.CrouchColliderHeight; belowCeiling = false; } public override void Exit() { base.Exit(); character.SetAnimationBool(character.crouchParam, false); character.ColliderSize = character.NormalColliderHeight; } public override void HandleInput() { base.HandleInput(); crouchHeld = Input.GetButton("Fire3"); } public override void LogicUpdate() { base.LogicUpdate(); if (!(crouchHeld || belowCeiling)) { stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); belowCeiling = character.CheckCollisionOverlap(character.transform.position + Vector3.up * character.NormalColliderHeight); } 

  • In der Enter Parameter, der das Umschalten der Kniebeugenanimation bewirkt, auf "geduckt" gesetzt, wodurch die Kniebeugenanimation aktiviert wird. Den Eigenschaften character.CrouchSpeed und character.CrouchRotationSpeed die Werte für speed und rotation zugewiesen, die die Bewegung und Winkelgeschwindigkeit des Zeichens beim Bewegen in einer Hocke zurückgeben .

    Nächstes character.CrouchColliderHeight legt die Größe des Colliders des Zeichens fest, der beim Hocken die gewünschte Collider-Höhe zurückgibt. Am Ende wird belowCeiling auf false zurückgesetzt.
  • Innerhalb von Exit der Parameter für die Squat-Animation auf false festgelegt. Dadurch wird die Squat-Animation deaktiviert. Dann wird die normale Collider-Höhe festgelegt, die vom character.NormalColliderHeight zurückgegeben wird. character.NormalColliderHeight .
  • In HandleInput Variable crouchHeld den Eingabewert des Players fest. crouchHeld in der crouchHeld die Umschalttaste crouchHeld , wird crouchHeld auf true gesetzt.
  • In PhysicsUpdate Variablen belowCeiling ein Wert zugewiesen, indem ein Punkt im Vector3 Format mit dem Kopf des Vector3 des Charakters an die CheckCollisionOverlap Methode übergeben wird. Wenn es in der Nähe dieses Punktes zu einer Kollision kommt, bedeutet dies, dass sich der Charakter unter einer bestimmten Decke befindet.
  • Intern prüft LogicUpdate , ob crouchHeld oder belowCeiling wahr ist. Wenn keines davon wahr ist, ändert sich movementSM.CurrentState in character.standing .

Erstellen Sie das Projekt und klicken Sie auf Abspielen . Jetzt können Sie sich in der Szene bewegen. Wenn Sie die Umschalttaste drücken, setzt sich der Charakter hin und Sie können sich in der Hocke bewegen.

Sie können auch unter der Plattform klettern. Wenn Sie die Umschalttaste unter den Plattformen loslassen, befindet sich der Charakter immer noch in der Hocke, bis er sein Obdach verlässt.


Steig auf!


Öffnen Sie Jumping.cs . Sie sehen eine Methode namens Jump . Mach dir keine Sorgen darüber, wie es funktioniert; Es reicht zu verstehen, dass es verwendet wird, damit der Charakter unter Berücksichtigung von Physik und Animation springen kann.

Fügen Sie nun die üblichen override und speichern Sie das Skript

 public override void Enter() { base.Enter(); SoundManager.Instance.PlaySound(SoundManager.Instance.jumpSounds); grounded = false; Jump(); } public override void LogicUpdate() { base.LogicUpdate(); if (grounded) { character.TriggerAnimation(landParam); SoundManager.Instance.PlaySound(SoundManager.Instance.landing); stateMachine.ChangeState(character.standing); } } public override void PhysicsUpdate() { base.PhysicsUpdate(); grounded = character.CheckCollisionOverlap(character.transform.position); } 

  • Innerhalb von Enter Singleton SoundManager den Sound des Sprungs ab. Dann wird grounded auf den Standardwert zurückgesetzt. Am Ende heißt Jump .
  • In PhysicsUpdate Punkt neben den Beinen des Charakters an CheckCollisionOverlap gesendet. Wenn sich der Charakter auf dem Boden befindet, wird grounded auf True gesetzt.
  • LogicUpdate in LogicUpdate grounded True ist, rufen wir TriggerAnimation auf, um die TriggerAnimation zu aktivieren, ein Landegeräusch wird abgespielt und movementSM.CurrentState ändert sich in character.standing .

Damit haben wir die vollständige Implementierung der FSM-Verschiebung mithilfe der Vorlage „State“ abgeschlossen . Erstellen Sie das Projekt, und führen Sie es aus. Drücken Sie die Leertaste , um den Charakter zum Springen zu bringen.


Wohin als nächstes?


Die Projektmaterialien haben einen Projektentwurf und ein fertiges Projekt.

Trotz ihrer Nützlichkeit weisen Zustandsmaschinen Einschränkungen auf. Concurrent State Machines und Pushdown-Automaten können einige dieser Einschränkungen umgehen. Sie können darüber im Buch von Robert Nystrom Game Programming Patterns nachlesen.

Darüber hinaus kann das Thema vertieft werden, indem die Verhaltensbäume untersucht werden, mit denen komplexere Objekte im Spiel erstellt werden.

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


All Articles