Wir entwickeln ein Halloween VR Spiel mit Unity 6, XR Interaction Toolkit 3 und Visual Scripting – Teil 5

Im letzten Teil haben wir das Verhalten des Schwerts mit Visual Scripting verbessert. In den nächsten Teilen erstellen wir einen Gegner und entwickeln sein Verhalten ebenfalls mit Visual Scripting.

Überblick

Direkt vorab möchte ich erwähnen, dass dieser Teil nichts VR-spezifisches enthält. Die feierwütigen Skelette aus unserer Story verhalten sich in einem Flat-Screen-Spiel nunmal genau so wie in einem VR-Spiel.

Ein Skelett soll mindestens zwei verschiedene Zustände unterstützen: es soll umherwandern und es soll den Spieler verfolgen und angreifen. Während dem Umherwandern gibt es zwei Gründe, warum es zum Angreifen wechseln würde: 1. Es entdeckt den Spieler in unmittelbarer Nähe, 2. Der Spieler greift es an.

Das 3D Modell finden wir im Skeletons Character Pack. Enthalten sind vier Skelette – Magier (Mage), Untertan (Minion), Schurke (Rogue) und Krieger (Warrior). Alle Charaktere kommen mit einer Reihe von Animationen, die wir zum Herumlaufen, Angreifen und Sterben verwenden können. Für den einfachen und am häufigsten auftretenden Gegner, dem der Spieler auf dem Friedhof begegnet, werden wir den Untertan (Minion) verwenden.

Minion Skelett Beispielgrafik aus dem Skeletons Character Pack von Kay Lousberg (CC0)

Außerdem gibt es einige Waffen. Wir starten wieder mit einem einfachen Schwert.

Zum Umherwandern, Angreifen und Sterben müssen wir jeweils die richtigen Animationen auslösen.

Für einen Schwertkampf und Schaden benötigen sowohl Spieler als auch Skelett Energie; und wem zuerst die Energie ausgeht, der muss dann auch aus dem Leben bzw. aus dem Tod scheiden.

Assets für den Skelett-Gegner

Wir verfahren zunächst ähnlich wie beim Schwert und kopieren sowohl Modell als auch Textur in die vorgesehenen Verzeichnisse. Kopiere die Datei KayKit_Skeletons_1.0_FREE/characters/fbx/Skeleton_Minion.fbx in den Ordner Assets/040_Models im Projekt. Kopiere außerdem die Datei KayKit_Skeletons_1.0_FREE/characters/fbx/skeleton_texture.png in den Ordner Assets/050_Textures.

Als Nächstes benötigen wir wieder ein Material. Erzeuge es im Ordner Assets/060_Materials, nenne es skeleton_texture, setze die Smoothness auf 0 und weise der “Base Map” die gerade kopierte Textur zu. (In Teil 3 kannst du die Schritte nochmal mit mehr Detail nachlesen, sofern nötig.)

Jetzt kannst du das Modell Skeleton_Minion aus dem Assets/040_Models Ordner in die Hierarchy ziehen. Ändere im Inspector die Position zu (0, 0, 8) und die Rotation zu (0, 180, 0). So steht es in etwas Entfernung zum Spieler.

Ein paar Dinge müssen wir beim Skelett anders machen:

Zum einen besteht das Skelett im Gegensatz zum Schwert nicht nur aus einem Mesh Filter / Mesh Renderer, sondern aus mehreren Skinned Mesh Renderers. Was das ist, beleuchten wir separat, aber hier ist wichtig, dass jeder dieser Renderer einen eigenen Material-Slot hat. Um also das Standard-Material aus dem Modell mit unserem eigenen, weniger glänzenden Material zu ersetzen, können wir alle Child-GOs bis auf das “Rig” markieren (1) und das neue Material aus unserem Ordner in das “Element 0” der Materials ziehen (2).

Zum anderen erzeugen wir diesmal kein separates Parent-GO, wie wir das beim Schwert gemacht haben, denn im Falle eines Charakters macht uns das das Visual Scripting etwas einfacher. Während die Grafik des Schwerts weitestgehend alleine steht, jederzeit ausgetauscht werden kann und der Rest der Logik woanders stattfindet, verhält sich das bei Charakteren etwas anders: hier ist Logik stark mit dem 3D Modell verknüpft, wenn man z. B. eine Angriffs-Animation auslösen muss. Eine Aufteilung in ein Parent-GO und ein Child-GO mit dem 3D Modell wäre zwar möglich, aber für diesen Artikel zu umständlich.

Idle-Animation

Wenn wir das Spiel jetzt starten würden, stände das Skelett genau so herum, wie wir es gerade im Editor sehen. Da das Skelett aber mit einigen Animationen kommt, können wir dem Skelett… etwas Leben einhauchen!?

Zunächst benötigen wir einen sogenannten Animator Controller, der die Animation steuert. Lege ihn per Rechtsklick im Project View Ordner 070_Animation an:

Nenne den Animator Controller “Skeleton”. Wir könnten ihn auch wie das Modell nennen, “Skeleton_Minion”, aber ich gehe im Moment davon aus, dass wir diesen Animator Controller auch für andere Skelette wiederverwenden können, deshalb können wir auch den Namen etwas allgemeiner halten. Öffne den Animator Controller dann durch einen Doppelklick.

Du solltest dann so etwas vor dir sehen:

Der Animator Controller funktioniert ähnlich zu dem Script Graph, den wir zuletzt bearbeitet haben. Die verschiedenen Rechtecke sind Ein- und Ausgangs-Knoten, “Entry” ist vergleichbar zum “On Start” Node im Script Graph.

Zum Vergleich: Unitys Visual Scripting Script Graph bietet eine auf Nodes basierende Umgebung zur Erstellung und Steuerung von Spiel-Logik, die das Eventhandling und komplexe Verhaltensweisen ohne Code ermöglicht. Animator Controllers hingegen sind speziell für das Management von Animationszuständen (States) und Übergängen ausgelegt und fungieren als Finite State Machine (FSM), die speziell zur Steuerung von Charakter- oder Objektanimationen dient.

Wir sprechen hier also von States und beginnen mit einem “Idle” State für unser Skelett. Klicke mit rechts auf eine freie Stelle und wähle Create State → Empty. Du findest dann ein neues, orangenes Rechteck vor dir, das automatisch vom “Entry” Node aus verknüpft wurde. Es handelt es sich hierbei um eine sogenannte “Default Entry Transition”, die erscheint, sobald du einen State anlegst. Wenn du mehrere States erzeugst, kannst du diesen Default später verändern.

Wenn du den neuen State auswählst (1), kannst du rechts im Inspector einige Parameter ändern. Das erste Feld ist der angezeigte Name, den können wir zu “Idle” ändern (2). Das Feld “Motion” ist die Animation, die abgespielt werden soll. Da wir im Moment nur ein Modell mit Animationen im Projekt haben, können wir die Idle-Animation sehr leicht auswählen: klicke auf das kleine Auswahl-Icon im “Motion” Feld (3) und tippe dann im Suchfenster “idle” ein. Wähle die “Idle_B” Animation aus der Liste (4).

Wechsle nun zurück zur Scene View, damit du das Skelett wieder vor dir siehst. Damit es diese Animation auch abspielt, müssen wir ihm den Animator Controller zuweisen. Das geht auf zwei Arten: entweder ziehst du den “Skeleton” Animator Controller aus der Project View direkt auf das Skelett im Scene View. Dann wird dem Skelett automatisch eine Animator Komponente hinzugefügt und dem “Controller” Attribut dieser Komponente der gerade bearbeitete Animator Controller zugewiesen. Oder du fügst im Inspector des Skeletts die “Animator” Komponente manuell hinzu und wählst dann dort im “Controller” Feld den “Skeleton” Animator Controller aus.

So soll das dann hinterher aussehen:

Jetzt kannst du das Spiel zum Testen wieder kurz direkt aus dem Editor starten. Wenn du das Skelett dann im Scene View beobachtest, wirst du feststellen, dass es sich für einen Augenblick bewegt und dann nichts mehr macht:

Du kannst im Animator Controller über den blauen Balken im “Idle” State beobachten, wie der Fortschritt der Idle-Animation verläuft. Zum Ende bleibt der einfach genau dort stehen.

Ursache ist die Konfiguration der Animation. Die steht standardmäßig nicht auf “wiederholen”, das müssen wir nachholen.

Klicke im Project View auf das Skeleton_Minion Modell im Ordner Assets/040_Models. Im Inspector siehst du jetzt die “Skeleton_Minion Import Settings” mit mehreren Tabs: Model, Rig, Animation und Materials. Klicke auf das Animation Tab. Hier siehst du eine lange Liste von Animationen, die in der Datei gespeichert sind. Scrolle nach ganz unten, als letztes Element in der Liste findest du die Animation, die wir verwenden: Idle_B. Klicke auf diese Animation, so dass die Zeile ausgewählt ist. Scrolle dann weiter nach unten, um die Eigenschaften dieser Animation anzusehen. Hier gibt es ein Feld “Loop Time”, das wir aktivieren müssen. Klicke danach “Apply”, um die Änderung zu speichern.

Bewegung auf dem NavMesh

Das Skelett soll sich zunächst zufällig über das Spielfeld bewegen. Wie immer gibt es viele Möglichkeiten dazu; wir wählen den Weg über das AI Navigation Package von Unity, das bereits mit Unity 6 installiert wurde. Das besteht aus zwei wesentlichen Bestandteilen:

  1. NavMeshes – kurz für Navigation Meshes. Das ist die triangulierte Repräsentation der Szene, über die navigiert werden kann. Ein NavMesh wird aus einem vorab definierten Bereich der Szene generiert und nutzt entweder die gerenderten Meshes oder die Collider der Spielobjekte, um den navigierbaren Bereich zu ermitteln. NavMeshes werden pro NavMeshAgent Typ generiert (siehe unten).
  2. NavMeshAgents – für Objekte, die über das NavMesh navigieren können. In Unity kann man verschiedene Typen von NavMeshAgents definieren, die sich durch Parameter wie z. B. Höhe oder Radius unterscheiden. Ein Mensch hat andere Navigationsmöglichkeiten wie z. B. eine Maus. Das spiegelt sich im Verhalten des Agents und im zugrundeliegenden NavMesh wider.

Beginnen wir also damit, ein NavMesh für unser Skelett zu erzeugen. Dazu machen wir eine kleine Änderung in der Hierarchy: wir verschieben unseren bisherigen Untergrund in ein neues Parent-GO, damit wir neben dem Plane bald mehr Objekte wie Kürbisse, Grabsteine, usw. platzieren können. Klicke rechts auf die “Plane” und wähle Create Empty Parent:

Benenne den neuen Parent zu “Static” um, denn der Inhalt wird jegliche statische Levelgeometrie werden, die sich entsprechend auf das NavMesh auswirkt.

Füge dann zum neuen “Static” GO die “NavMesh Surface” Komponente hinzu:

Die Komponente hat einige Einstellungen. Im Moment machen wir nur eine Änderung: im Foldout “Object Collection” wird festgelegt, welche Objekte eigentlich zur Erstellung des NavMeshes herangezogen werden. Da wir in genau diesem GameObject alle statischen Objekte unterbringen wollen, ändern wir die Einstellung “Collect Objects” von “All Game Objects” zu “Current Object Hierarchy” (1). Klicke dann auf “Bake” (2), um das NavMesh zu erzeugen.

Wenn du weiterhin die Gizmos in der Scene View aktiviert hast, wirst du feststellen, dass jetzt das gesamte “Plane” GO von einer blauen Fläche überzogen ist – zumindest fast, denn wenn du an die Ränder navigierst, wirst du feststellen, dass hier ein kleiner Abstand besteht. Dieser Abstand ist den Einstellungen des Agent Type “Humanoid”, der Standard in den Unity-Einstellungen ist und für den wir das NavMesh generiert haben, geschuldet: er hat einen gewissen Radius und damit der Agent nicht plötzlich über den navigierbaren Teil hinausragt – z. B. in eine Wand hinein oder eben über den Spielfeldrand hinaus – wird der beim NavMesh generieren entsprechend berücksichtigt.

Damit sich das Skelett über diese Fläche bewegen kann, müssen wir es zu einem NavMeshAgent machen. Füge also diese Komponente dem “Skeleton_Minion” GO hinzu.

Mit dem fertiggestellten NavMesh und dem NavMeshAgent können wir jetzt das Skelett über die Fläche bewegen. Das machen wir wieder mit Visual Scripting!

Füge also dem “Skeleton_Minion” GO zusätzlich eine Script Machine Komponente hinzu und erzeuge durch Klick auf den “New” Button in der Komponente einen Script Graph, den du wieder unter Assets/030_Scripts speichern kannst. Nenne den Script Graph “Skeleton”.

Die beiden neuen Komponenten sollten dann so aussehen:

Klicke jetzt auf den “Edit Graph” Button, damit der Script Graph angezeigt wird. Wie schon beim Schwert startet der mit zwei Nodes für Start und Update.

Während das Skelett nicht kämpft, soll es zufällig umherwandern. Im Script Graph benötigen wir dazu also eine Möglichkeit, ein zufälliges Ziel auf dem NavMesh zu ermitteln und das Skelett dorthin zu schicken. Es macht Sinn, diese Logik im “Update” Node auszuführen, damit es regelmäßig passiert, es macht allerdings keinen Sinn, ein neues Ziel zu wählen, während das Skelett noch auf dem Weg ist. Und zu guter letzt reicht es auch, all das nur 1x pro Sekunde auszuführen, anstatt jedes Frame.

In Pseudocode könnte das so aussehen:

Update:
  Ist eine Sekunde seit dem letzten Test vergangen?
    Ja:
      Hat der NavMeshAgent bereits einen Pfad, der verfolgt wird?
        Ja:
          Abbruch, nichts zu tun
        Nein:
          Kann eine zufällige Position auf dem NavMesh in der Nähe der aktuellen Position ermittelt werden?
            Ja:
              Setze diese Position als nächstes Ziel
            Nein:
              Abbruch, neuer Versuch im nächsten Durchlauf
    Nein:
      Abbruch, warte bis eine Sekunde vergangen ist

Die Nodes dazu existieren auch:

Zum Aufrufen der Logik einmal pro Sekunde nutzen wir die sog. Cooldown-Node.
Hier setzen wir die “Duration” des Cooldowns auf eine Sekunde und sobald die abgelaufen ist, wird der Flow am “Ready” Ausgang fortgesetzt.

Ob der NavMeshAgent aktuell einen Pfad hat oder ob wir einen neuen ermitteln müssen, prüfen wir über die Abfrage des “has path” Attributs mithilfe dieser Node. (Wie du siehst, fehlt hier der grüne “Flow”: das Ergebnis dieser Node wird als Eingabe in eine andere Node verwendet)

Um eine neue Zielposition auf dem NavMesh zu ermitteln nutzen wir die SamplePosition Node. Die erhält eine Position, von der aus sie mit einem gegebenen Maximalabstand die nächste Position auf dem NavMesh ermitteln soll.

Wenn wir eine Position ermittelt haben, extrahieren wir sie aus dem “Hit” Ausgang der letzten Node und nutzen dann die “Set Destination” Node, um den NavMeshAgent an diese Stelle zu lenken.

Beginnen wir also mithilfe dieser Nodes, unseren oben formulierten Pseudocode in den Script Graph zu gießen. Die erste Node ist die Cooldown-Node direkt nach dem Update. Den Programmverlauf danach wollen wir nur fortsetzen, wenn der NavMeshAgent noch keinen Pfad hat. Dazu schleifen wir das Ergebnis der “Has Path” Node in eine “If” Node. “Has Path” liefert “True”, wenn ein Pfad existiert, und “False”, wenn nicht. Dementsprechend setzen wir den Verlauf gleich am “False” Ausgang fort:

Zur Erinnerung, die Nodes kannst du durch Rechtsklick in einen freien Bereich anlegen. Es öffnet sich dann der “Fuzzy Finder”, wo du die Node durch Eingabe eines Stichworts suchen und auswählen kannst. Die Verknüpfungen ziehst du durch Klicken und Ziehen, falls du eine Verknüpfung falsch ziehst, kannst du sie durch Rechtsklick auf eines der Enden wieder löschen.

Als Nächstes müssen wir eine Position auf dem NavMesh ermitteln. Dazu benötigen wir einen zufälligen Punkt, der als Ausgangspunkt für die Suche dienen soll. Dieser Punkt sollte in der Nähe der aktuellen Position des Skeletts liegen, damit die Pfadsuche und darauffolgende Bewegung nicht über die gesamte Karte stattfindet – das ist möglich, aber gar nicht nötig.

Den folgenden Schritt besprechen wir etwas ausführlicher:

Zunächst ermitteln wir einen zufälligen Punkt auf einer Einheitskugel (1), also einer Kugel mit dem Radius 1. Das Ergebnis ist ein Vektor, naturgemäß ebenfalls mit Länge 1, den wir jetzt mit 10 multiplizieren und so verlängern (2). Ebenso ermitteln wir die aktuelle Position des Skeletts (3), die ebenfalls ein Vektor ist. Nun addieren wir beide Vektoren aufeinander. Wir haben jetzt eine zufällige Position im Abstand von 10 Längeneinheiten von der aktuellen Position. Da die Position auf einer Kugel ermittelt wurde, kann diese neue Position auch über oder unter dem Boden liegen, auf dem das Skelett gerade wandelt – das spielt aber keine Rolle, denn der nächste Schritt sorgt dafür, dass die nächste Position dazu auf dem NavMesh ermittelt wird (5). Die “Sample Position” Methode erhält neben der Ausgangsposition einen maximalen Abstand und auch eine sog. “Area Mask” als Parameter. Für letzteres geben wir einfach die Maske für “alle Areas” rein (6).

Anmerkung: die Nutzung von “All Areas” ist gefährlich, da sie auch Bereiche einschließen könnte, die der NavMeshAgent gar nicht ansteuern kann. Da wir aber nur “Walkable” Areas haben und die anderen durch Unity vordefinierten Area Types wie “Not Walkable” oder “Jump” gar nicht verwenden, geschweige denn eigene definieren, ist das hier unkritisch.

Die “Sample Position” Node liefert zwei Werte: ein “Result” Boolean, das uns sagt, ob eine Position gefunden wurde (True/False), und einen “Hit” vom Typ Nav Mesh Hit, der Details für die Position enthält.

Beim Versuch, den letzten Teil zu modellieren, werden wir auf ein Problem stoßen: die “Set Destination” Node des NavMeshAgents erwartet einen “Vector 3” mit der Zielposition als Eingabe. Die “Sample Position” Node liefert aber einen “Nav Mesh Hit”. Diese Typ beinhaltet die Position, aber auf Anhieb wird sich kein Weg finden, diesen Wert zu extrahieren. Um das zu bewerkstelligen, müssen wir etwas an den Visual Scripting Einstellungen in den Project Settings ändern.

Gehe also zu Edit → Project Settings… → Visual Scripting (oder öffne das hoffentlich angedockte Tab) und klappe das oberste Foldout “Type Options” auf. Hier finden sich alle Datentypen, die das Visual Scripting kennen sollte. Wenn du durchscrollst, siehst du hier Typen, mit denen wir schon gearbeitet haben, wie Boolean und Vector 3. Wir müssen “NavMeshHit” noch bekanntmachen. Klicke dazu auf das “+” am Ende der Liste, öffne dann die Suche durch Klick auf den neuen Eintrag und tippe “navmesh” ein. Wähle dann Nav Mesh Hit aus der Auswahl:

Jetzt können wir den letzten Teil der Logik modellieren:

Von links verknüpfen wir den Flow und das Ergebnis des “Sample Position” Nodes in eine “If” Node. Nur wenn das Ergebnis von “Sample Position” True ist, d.h. eine gültige Position gefunden wurde, soll mit dem letzten Schritt “Set Destination” fortgesetzt werden. Und auch nur dann wird zur Bestückung des “Target” Parameters über die “Get Position” Node der Vektor aus dem Nav Mesh Hit extrahiert, die von links den “Hit” aus dem “Sample Position” Node als Eingabe erhält.

Der gesamte Script Graph sollte nach diesen Änderungen wie folgt aussehen:

Wenn wir nun das Spiel starten, sehen wir das Skelett umherwandern:

Ganz offensichtlich fehlt hier noch eine Animation beim Laufen, schließlich haben wir bisher nur eine “Idle” Animation.

Move-Animation

Wenn wir beim Bewegen eine Animation abspielen wollen, dann müssen wir auch wissen, wann das der Fall ist. Wir werden dazu die Geschwindigkeit des NavMeshAgents verwenden!

Wenn du dir das Video oben noch einmal genau ansiehst, stellst du fest, dass über dem Kopf des Skeletts ein hellblauer Pfeil angezeigt wird, der in die Bewegungsrichtung zeigt und beim Beschleunigen länger und beim Abbremsen vorm Ziel kürzer wird. Das ist die “Velocity”, die Bewegungsgeschwindigkeit des NavMeshAgents als Vektor, die Richtung und Geschwindigkeit des Agenten beschreibt. Das Feld ist schreibgeschützt und wird zur Laufzeit abhängig von den im NavMeshAgent konfigurierten Daten berechnet.

Ein Animator Controller hat die Möglichkeit, diverse Parameter von außen zu empfangen. Diese Parameter können auf verschiedene Weisen verwendet werden, die häufigste Verwendung ist als Bedingung für Transitionen von einem State zum anderen. Wenn wir also neben unseren bisherigen “Idle” State mit einer “Idle” Animation noch einen weiteren “Move” State mit einer “Move” Animation setzen, benötigen wir einen Parameter, der als Kriterium für einen solchen Übergang zum “Move” State und wieder zurück zum “Idle” State dienen kann.

Wenn die “Velocity” des NavMeshAgents sowohl Richtung als auch Geschwindigkeit repräsentiert (in Form der Länge des Vektors), erscheint letzteres als sinnvoller Parameter für die Steuerung der Übergänge.

Beginnen wir zunächst damit, im Animator Controller einen solchen Parameter anzulegen. Öffne den Animator Controller des Skeletts im Project View in der Datei Assets/070_Animation/Skeleton. Klicke dann auf das kleine “+” Symbol links oben in der Ansicht und wähle “Float” als Typ des neuen Parameters. Gib dann “Speed” als Name des Parameters ein.

Lege jetzt einen neuen State an. Klicke rechts an eine freie Stelle im Animator Controller und wähle Create State → Empty. Ich platziere den neuen State so, dass er mit etwas Entfernung unter dem “Idle” State liegt. Ändere jetzt den Namen des neuen States im Inspector zu “Move” und wähle als “Motion” die Animation “Walking_A”.

Für diese Animation müssen wir übrigens genau wie zuvor beim Idle die Wiederholung aktivieren. Wechsele also noch einmal kurz auf das Modell im Project View, suche die Animation “Walking_A” in der Liste, setze “Loop Time” und klicke “Apply”.

Wechsele dann wieder zum Animator Controller.

Damit wir vom Idle zum Move State wechseln können, benötigen wir Übergänge, sog. Transitions. Klicke zuerst mit rechts auf den “Idle” State und dann auf “Make Transition”. Klicke dann auf den “Move” State. Während du die Maus bewegst, siehst du, wie eine Linie mit einem Richtungspfeil dem Mauspfeil folgt. Sobald du links auf “Move” klickst, wird dieser State zum Ende der Transition.

Wenn du jetzt auf die Transition klickst, siehst du im Inspector die Einstellungen, insbesondere, wenn du das “Settings” Foldout aufklickst. Unter den Settings findest du außerdem eine Liste von “Conditions”, die wir uns gleich genauer ansehen.

Ändere in den Settings die “Exit Time” auf 0, dann deaktiviere das “Has Exit Time” Feld. Sobald du das tust, taucht unter den Conditions eine Warnung auf: “Transition needs at least one condition or an Exit Time to be valid, otherwise it will be ignored.”.

Die Exit-Time käme zum Tragen, wenn der Übergang bedingungslos stattfinden sollte. Dann würde nach “Exit Time” Sekunden der Ursprungsstate “Idle” über eine Übergangszeit von “Transition Duration (s)” in den Zielstate “Move” überblenden.

Wir wollen den Übergang aber nur dann machen, wenn das Skelett in Bewegung ist, also wenn der “Speed” Parameter, den wir gleich noch von außen setzen werden, nicht mehr 0 ist. Dazu nutzen wir eine Condition, die wir jetzt mit Klick auf das “+” unter “Conditions” anlegen. Da wir bisher nur einen möglichen Parameter haben, der als Bedingung infrage kommt, wird uns dieser direkt vorgeschlagen. Die vorkonfigurierte Bedingung “Greater” ist auch in Ordnung, denn wir wollen von “Idle” zu “Move” wechseln, wenn der “Speed” “Greater” 0 ist, oder besser mit etwas Toleranz, 0.01. Ändere die Condition entsprechend:

Jetzt wiederholen wir den Prozess, aber in die andere Richtung: eine Transition zurück von “Move” zu “Idle”, mit der Bedingung, dass der Speed <0.01 ist. Klicke rechts auf den “Move” State, wähle “Make Transition”, klicke auf den “Idle” State. Klicke auf die neu angelegte Transition (achte darauf, dass du die richtige erwischst, erkennbar am Pfeil von “Move” zu “Idle” und an der fehlenden Condition), setze Exit-Time auf 0 und deaktiviere “Has Exit Time”. Lege dann eine neue Condition an und konfiguriere sie wie besprochen:

Anmerkung: Die Exit-Time muss eigentlich nicht auf 0 gesetzt werden, da wir sie eh deaktivieren. Ich bevorzuge es aber, das so explizit zu konfigurieren.

Jetzt können wir dafür sorgen, dass der “Speed” Parameter auf dem Animator Controller richtig gesetzt wird. Das passiert wieder im Script Graph des Skeletons.

Für das Herumwandern des Skeletts haben wir bereits Logik an den Ausgang des Update-Triggers gebunden. Wir können aber problemlos einen weiteren solchen “On Update” Einstiegspunkt anlegen. Klicke rechts auf eine freie Stelle und gib “on update” in die Suche ein. Wähle dann den ersten Vorschlag. Folge diesem Vorgehen für die folgenden genannten Nodes.

Was wir von dort aus auslösen wollen, ist eine “Animator Set Float” Node (1). Klicke wieder rechts, gib das in die Suche ein und wähle den ersten Vorschlag. Verbinde den “Flow” (grüner Pfeil) von der neuen “On Update” Node mit der “Animator Set Float” Node. Als “Name” können wir “Speed” direkt in der Node eingeben, denn dieser Wert ist ja fest vergeben. Nur die “Value” müssen wir noch ermitteln. Erzeuge dazu links der “Animator Set Float” Node eine weitere Node “Nav Mesh Agent Get Velocity” (2) und verknüpfe den Ausgangswert (ein Vektor) mit einer weiteren Node “Vector 3 Get Magnitude” (3), um die Länge des Vektors zu ermitteln. Verknüpfe dann diese Ausgabe mit dem “Value” Parameter.

Wir haben nun im gleichen Script Graph zwei “On Update” Einstiegspunkte, einen für die Bewegung und einen für die Aktualisierung der Geschwindigkeit. Dieser neue Teil kann auch ruhig ständig stattfinden, damit jede Änderung direkt in den Animator Controller geschrieben wird.

Hier ist dann übrigens ein gutes Beispiel, warum ich weiter oben in diesem Teil schrieb, dass wir das animierte Skelettmodell nicht in ein Child-GO lagern. So können wir jetzt sowohl auf den Nav Mesh Agent als auch auf den Animator einfach mit “This” zugreifen und müssen hier keine Änderungen vornehmen, denn diese Komponenten existieren auf dem gleichen GO wie die Script Machine selbst.

Schauen wir uns das Ergebnis im Editor an:

Nicht schlecht, oder? Es gibt noch zwei Kleinigkeiten, die ich gerne ändern möchte.

Das Skelett dreht sich etwas zu langsam. Es beginnt sich schon in eine Richtung zu bewegen, während die Drehung noch nicht abgeschlossen ist. Mit etwas mehr Zeit und Animationen könnten wir nun einen Blendtree im Animation Controller anlegen, der die Bewegung in diesem Fall etwas natürlicher aussehen lassen könnte. Wir belassen es aber bei einer Verdoppelung des “Angular Speed” in der Nav Mesh Agent Komponente von 120 auf 240.

Außerdem ist die Geschwindigkeit der Animation unabhängig von der Geschwindigkeit des Nav Mesh Agents. Wenn wir die Geschwindigkeit im Feld “Speed” von aktuell 3.5 testweise auf 7 verdoppeln, passt die Animation einfach nicht mehr. Auch die Beschleunigung und das Abbremsen kommen nicht zum Tragen.

Wir können aber den “Speed” Parameter auf die tatsächliche Geschwindigkeit der Animation anwenden! Öffne dazu den Animator Controller “Skeleton” und klicke auf den “Move” State. Rechts findest du nun ein Feld “Speed” mit aktuellem Wert “1” sowie darunter ein Feld “Multiplier”, das deaktiviert ist. Setze das Häkchen bei “Parameter” (1), der einzige Parameter “Speed” wird dann automatisch angezeigt. Ändere den Speed dann zu “0.3” (2).

Damit ist die Animationsgeschwindigkeit abhängig vom Speed Parameter, d.h. zwischen 0.01 * 0.3 = 0.003 (da 0.01 der Mindestwert ist, ab wann der “Move” State aktiviert wird) und 3.5 * 0.3 = 1.05 (da 3.5 der “Speed” im Nav Mesh Agent ist).

Es lohnt, mit den Werten zu spielen. Vielleicht findest du eine Kombination, die dir noch besser gefällt. Schreibe mir gerne bessere Optionen in die Kommentare!

Assets für das Schwert

Nun wollen wir uns darum kümmern, dass das Skelett mit seiner Waffe ausgestattet wird. Wir verwenden das Schwert, das Kay Lousberg auch oben in seinem Promo-Screenshot für das “Minion” Skelett vorgesehen hat.

Kopiere die Datei KayKit_Skeletons_1.0_FREE/assets/fbx(unity)/Skeleton_Blade.fbx in den Ordner Assets/040_Models im Projekt.

Wähle dann in der Hierachy View das “Skeleton_Minion” GO und öffne das “Rig” Child so weit, bis du das “handslot.r” GO sehen kannst:

Dem Namen nach zu urteilen dient dieses GO dazu, Objekte aufzunehmen, die das Skelett in der Hand halten soll. Ziehe daher nun das Schwert “Skeleton_Blade” aus dem Assets/040_Models Ordner im Project View auf dieses GO:

Wechsle nun zum 060_Materials Ordner und ziehe das “skeleton_texture” Material auf das Schwert in der Scene View, oder markiere das Schwert in der Scene View und ziehe dann das Material in den ersten Slot der Materials in der Mesh Renderer Komponente:

Wenn du jetzt das Spiel im Editor startest und das Skelett beobachtest, wirst du feststellen, dass das Schwert falsch herum in der Knochenhand liegt:

Ich habe das Schwert um 180 Grad gedreht und ein kleines bisschen auf der Z-Achse verschoben, damit es mittig in der Hand liegt. So macht es auf den ersten Blick einen ganz passablen Eindruck. Bitte nimm ebenfalls diese Änderungen vor:

Du kannst ein paar Werte ausprobieren, wenn du das Spiel pausierst und die entsprechenden Änderungen in der Transform-Komponente eingibst. Du musst allerdings daran denken, dass diese Änderungen nach Stoppen des Spiels verworfen werden. Falls du also weitere Änderungen machst, schreib sie dir auf oder kopiere die Komponenten-Werte, um sie nach Stoppen wieder einzufügen.

Attack-Animation(en)

Zum Ende dieses Teils kümmern wir uns noch um ein paar Angriffs-Animationen. Das Skelett bringt einige mit, wir werden zwei davon integrieren.

Die Angriffs-Animationen sind im Gegensatz zur Lauf-Animation nicht von der Geschwindigkeit abhängig. Sie werden stattdessen “getriggert”, wenn sie abgespielt werden sollen. Dementsprechend legen wir im Animator Controller zwei neue Parameter “Attack1” und “Attack2” vom Typ Trigger an und zwei neue States mit Transitions, deren Eingangs-Bedingung der jeweilige Trigger ist und die danach wieder zum vorherigen State zurückkehren.

Einzige Frage ist hier, was der vorherige State ist? In diesem Setup gehe ich erstmal davon aus, dass der Angriff immer aus dem Stand passiert: das Skelett nähert sich dem Spieler, bleibt stehen und wechselt entsprechend aus dem “Move” zurück in den “Idle” State und dann wird von dort die Angriffs-Animation ausgelöst.

So soll das Ganze gleich aussehen:

Auf der linken Seite siehst du zwei neu angelegte Parameter vom Typ “Trigger”, genannt “Attack1” und “Attack2” (1).

Im Hauptbereich siehst du zwei neue States “Attack1” und “Attack2” (2). Als Animationen habe ich hier “1H_Melee_Attack_Slice_Diagonal” und “1H_Melee_Attack_Slice_Horizontal” verwendet.

Die Eingangs-Transitions zu beiden States gehen jeweils von “Idle” ab (3) und haben diesmal den jeweiligen Trigger als Condition (4).

Lediglich an den Transitions zurück von “Attack1” zu “Idle” bzw. “Attack2” zu “Idle” musst du gar nichts ändern: hier darf die “Exit Time” etc. genau so bleiben, wie Unity sie uns vorschlägt. So wird die Angriffs-Animation vollständig abgespielt und am Ende wieder sanft in die Idle-Animation übergeblendet.

Um das Auslösen der Angriffs-Animationen kümmern wir uns im nächsten Teil. Du kannst sie aber bereits jetzt ausprobieren, wenn du per Mausklick zur Laufzeit einen der Trigger auslöst. Der Trigger bleibt aktiv, bis er verarbeitet werden kann, d.h. die Transition zum jeweiligen Attack-State wird dann ausgelöst, wenn die aktuelle Bewegung abgeschlossen, der “Speed” wieder <0.01 und der Animator Controller wieder zurück im State “Idle” ist.

Ich habe mir zum Testen wieder das Animator Tab unter das Scene Tab geschoben, damit ich beides gleichzeitig sehe:

Ausblick

Damit endet dieser Teil. Beim nächsten Mal werden wir dem skelettierten Hohlkopf etwas mehr Hirn einhauchen und dafür sorgen, dass er den Spieler verfolgt und angreift.

Leave a Reply

Your email address will not be published. Required fields are marked *