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

Im dritten Teil haben wir ein Schwert eingeführt, mit dem der Spieler interagieren kann. In diesem Teil sorgen wir dafür, dass die Interaktion mit dem Schwert etwas flüssiger wird und nutzen dazu Visual Scripting.

Visual Scripting anhand eines Beispiels

Problemstellung

Wir beginnen mit der Einführung von Visual Scripting in unser Projekt anhand dieses vergleichsweise einfachen Beispiels: das Schwert soll beim Aufnehmen von seiner aktuellen Position in der Scene Hierarchy an den jeweiligen Controller, bzw. genauer, den jeweiligen Interactor, reparented werden, und beim Loslassen wieder zurück zum Scene Root. Die Motivation dazu haben wir am Ende des letzten Teils besprochen.

Die Screenshots zeigen, wie das Ganze hinterher zur Laufzeit aussehen soll:

Ausgangssituation: das Schwert befindet sich wie von uns konfiguriert auf oberster Ebene in der Scene Hierarchy.
Gewünschte Situation nach Reparenting: das Schwert wird zu einem Child-GO unter dem Near-Far Interactor.

Die XRGrabInteractable Komponente, die ja an unserem Sword-GO hängt, stellt zum Glück eine Reihe von Unity Events zur Verfügung, die du sehen kannst, wenn du das Sword-GO auswählst und dann in der Komponente das “Interactable Events” Foldout öffnest. Ganz konkret werden wir die “Select Entered” und “Select Exited” Events benötigen, die dann ausgelöst werden, wenn das Objekt gegriffen oder losgelassen wird. (Warum XRIT das “Select” nennt, während es doch um ein “Grab” Interactable geht, ist mir rätselhaft.)

Das XRGrabInteractable sendet also bei bestimmten Aktionen, inklusive dem Festhalten und Loslassen, Signale aus, die von anderen aktiven GameObjects, oder auch dem gleichen GameObject, empfangen und verarbeitet werden können.

Damit die Verarbeitung funktioniert, enthält ein solches Signal bestimmte Parameter, die hier über den in Klammern angezeigten Typ gezeigt werden. Die Signale sind sogenannte Unity Events und die Objekte, die diese Unity Events empfangen und verarbeiten wollen, sind sogenannte Event Listener.

Mit Klick auf das Plus kann man die Liste der Event Listener um einen Eintrag erweitern:

Ein Eintrag enthält 1. eine Auswahl, wann das Event ausgelöst werden soll, 2. eine Referenz auf das Objekt, das benachrichtigt werden soll, und 3. eine Auswahl der Komponente und Methode, die den Anforderungen zum Aufrufen genügt.

Reparenting per C# Script

Wir machen einen kleinen Exkurs, um zu verstehen, wie die Verarbeitung per C# erfolgen würde. Das hilft, um Parallelen zur Verarbeitung im Visual Scripting zu ziehen. Wenn du das für nicht relevant hältst oder du bereits Erfahrung mit C# und der Verarbeitung von Unity Events hast, kannst du dieses Kapitel überspringen.

Jedes GameObject ist wie ein leerer Baukasten: du kannst ihn mit verschiedenen Komponenten befüllen, um ihm Funktionalität zu verleihen. Eine Komponente, die in jedem GameObject existiert, ist die Transform-Komponente, die du dir auch im Inspector ansehen kannst. Sie enthält neben den sichtbaren Parametern für die Position, Rotation und Skalierung (jeweils lokal, relativ zum Parent) auch Referenzen zum Parent-Transform und alle Child-Transforms. Die BoxCollider oder Rigidbody Komponenten, die wir bereits verwendet haben, sind weitere Beispiele aus der Unity Engine.

Du hast darüber hinaus die Möglichkeit, eigene, wiederverwendbare Komponenten zu entwickeln, die du an solche GameObjects hängen kannst. Dazu existiert eine Klasse namens MonoBehaviour, die einige Methoden im Lebenszyklus eines GameObjects enthält, die überschrieben werden können. Für ein Autorennspiel könntest du so bspw. eine Klasse “CarController” implementieren, die den Rigidbody auf Tastendruck beschleunigt oder abbremst. Das XR Interaction Toolkit hat das Gleiche mit Komponenten wie XRGrabInteractable gemacht.

In solchen Komponenten implementiert man Methoden, die je nach Verwendung öffentlich oder von außen nicht sichtbar sind, wie im Beispiel des Autorennspiels eine Methode “Beschleunigen” oder “Bremsen”. Und solche Methoden können Parameter empfangen, wie z. B. die Bremskraft.

Und so etwas benötigen wir für die “SelectEntered” und “SelectExit” Events aus dem XRGrabInteractable: Methoden, die öffentlich sind und die vorgegebenen Parameter SelectEnterEventArgs bzw. SelectExitEventArgs entgegennehmen.

Wenn du diesen Weg testweise im Projekt anwenden willst, klicke jetzt im Project View mit rechts auf den Ordner 030_Scripts und wähle Create → MonoBehaviour Script. Tippe dann ReparentInteractable als Name für das Script. Nachdem das Skript angelegt ist, kannst du es durch einen Doppelklick öffnen.

Jetzt wird der Editor gestartet, der in Unity konfiguriert ist: unter Windows ist das in der Regel Visual Studio, unter macOS XCode. Dort solltest du dann jetzt die folgende, leere Standard-Implementierung sehen, wie Unity sie stets anlegt, wenn man ein neues MonoBehaviour erzeugt:

using UnityEngine;

public class ReparentInteractable : MonoBehaviour
{
    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

Die Methoden Start und Update gehören zu den vorhin genannten Lebenszyklus-Methoden. Sie genügen aber offensichtlich nicht den Anforderungen, die wir vorhin festgestellt haben: sie sind nicht öffentlich und sie nehmen nicht die nötigen Argumente entgegen. Daher können wir diese Methoden löschen und durch eigene ersetzen:

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class ReparentInteractable : MonoBehaviour
{

    public void OnSelectEntered(SelectEnterEventArgs args)
    {
        // Reparent zu Interactor hier
    }

    public void OnSelectExited(SelectExitEventArgs args)
    {
        // Reparent zu Scene Root hier
    }

}

Wie du siehst, fehlt hier noch die Logik zum Reparenting. Da wir dieses MonoBehaviour an unser Schwert hängen werden und wir ja den Parent genau dieses Transforms verändern wollen, können wir darauf so direkt zugreifen:

transform.SetParent(...);

transform ist ein Attribut jedes MonoBehaviours, mit dem wir schnell auf die Transform-Komponente zugreifen können. SetParent ist eine Methode der Transform-Komponente, die wir aufrufen können.

Stellt sich nur noch die Frage, woher wir den neuen Parent kriegen? Dazu schauen wir uns SelectEnterEventArgs einmal genauer in der Unity Dokumentation an: https://docs.unity3d.com/Packages/com.unity.xr.interaction.toolkit@3.0/api/UnityEngine.XR.Interaction.Toolkit.SelectEnterEventArgs.html

Dieses Objekt enthält sowohl das interactableObject – was dann wohl das Schwert wäre -, als auch das interactorObject – was dann wohl der Near-Far Interactor wäre, mit dem wir das Schwert greifen und an den wir es reparenten wollen!

Den ersten Kommentar, // Reparent zu Interactor hier, können wir also mit folgender Zeile ersetzen:

transform.SetParent(args.interactorObject.transform);

Den zweiten Kommentar, // Reparent zu Scene Root hier, ersetzen wir dann entsprechend mit:

transform.SetParent(null);

Das gesamte Skript sollte dann also so aussehen:

using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class ReparentInteractable : MonoBehaviour
{

    public void OnSelectEntered(SelectEnterEventArgs args)
    {
        transform.SetParent(args.interactorObject.transform);
    }

    public void OnSelectExited(SelectExitEventArgs args)
    {
        transform.SetParent(null);
    }

}

Wenn du das Skript so speicherst und zu Unity zurück wechselst, werden die Änderungen kurz compiliert. Dann kannst du das Skript an das Sword-GO in der Szene hängen, indem du es per Drag and Drop aus dem Project View auf das Sword ziehst (1). Alternativ kannst du das Sword markieren und dann rechts im Inspector auf “Add Component” klicken und den Namen der Klasse, ReparentInteractable, eingeben und auswählen. Dann findet sich die neue Komponente im Inspector (2).

Jetzt haben wir das Reparenting zwar implementiert, aber es wird noch nicht aufgerufen. Wir müssen die Events mit den richtigen Methoden auf unserem Skript verdrahten.

Dazu gehen wir zurück zu den “Select Entered” und “Select Exited” Interactable Events in der XRGrabInteractable Komponente des Sword-GOs. Auf beiden sollte jetzt Platz für genau einen Event Listener geschaffen werden. Dann kann entweder im Inspector die neu angelegte Komponente “Reparent Interactable” auf das Object Field gezogen werden (1) (dort, wo aktuell steht “None (Object)”), oder das Sword-GO von links aus der Hierarchy View.

Daraufhin wird das Dropdown-Control, wo initial “No Function” steht, aktiviert. Jetzt müssen wir hier die passende Funktion über die zugehörige Komponente auswählen (2). Da wir wissen, dass wir die relevante Funktion in der ReparentInteractable Komponente zu finden sein wird, können wir die per Maus auswählen. Dann werden uns Methoden vorgeschlagen, die infrage kommen. Aufgrund der passenden Signatur stehen unsere Methoden jeweils ganz oben (3). Die wählen wir jeweils passend für die “Select Entered” und “Select Exited” Events, so dass das am Ende so aussehen sollte:

Und das war’s! Wenn wir jetzt das Spiel starten, werden diese Methoden aufgerufen und die sichtbare Verzögerung aus dem letzten Teil ist verschwunden!

Wir haben uns also die beiden Unity Events des XRGrabInteractable zunutze gemacht, um das Schwert im richtigen Moment zu reparenten. Das Gleiche wollen wir im nächsten Teil mit Visual Scripting probieren. Deshalb kannst du jetzt beide Listener wieder durch Klick auf das Minus entfernen, die Komponente Reparent Interactable vom Sword löschen und auch das Skript aus der Project View löschen – wir brauchen es nicht mehr!

Reparenting per Visual Scripting

Auch das Visual Scripting wird über eine Komponente gesteuert, nämlich die sogenannte Script Machine. Füge sie zum Sword-GO hinzu:

Sobald die Komponente hinzugefügt wurde, wirst du feststellen, dass automatisch auch noch eine “Variables” Komponente angelegt wurde. Außerdem findet sich in der Scene Hierarchy nun ein neues Objekt “VisualScripting SceneVariables”. Beide werden wir uns später noch genauer ansehen.

Mit Klick auf “New” kannst du jetzt ein neues “Script Graph Asset” anlegen. Als Zielverzeichnis kannst du zunächst unseren 030_Scripts Ordner nutzen – Skript ist Skript! Nenne das Skript auch “Sword”. Es wird dann als Datei abgelegt und in der Script Machine referenziert (1):

Als Nächstes editieren wir den Visual Script Graph über den Edit Graph Button (2). Dann wird ein neues “Script Graph” Fenster geöffnet, das wir jetzt unbedingt per Drag and Drop in die gleiche Tab-Reihe wie den Scene View ziehen sollten. Fasse dazu das Tab “Script Graph” im neu geöffneten Fenster per linker Maustaste, halte die Maustaste gedrückt und ziehe das Tab neben das “Game” Tab:

So können wir etwas einfacher hin und her wechseln. (Wenn du das nicht machst, verschwindet das separate “Script Graph” Fenster gerne mal unnötigerweise im Hintergrund. Falls du mit mehreren Monitoren arbeitest, kannst du es natürlich auch auf einen zweiten Monitor legen.)

Du solltest jetzt das Script Graph Fenster so vor dir sehen:

Der Script Graph besteht aus Nodes, und zwei davon werden immer direkt angelegt: die “On Start” und “On Update” Nodes. Dabei handelt es sich um “Event Nodes”: sie sind Einstiegspunkte für Logik, die entweder zum Start oder zum Update (also jedes Frame) ausgeführt werden soll. Sie sind die gleichen Einstiegspunkte, die wir auch in C# nutzen würden.

Um das einmal live und in Aktion auszuprobieren, probieren wir, unser Schwert durch Visual Scripting kontinuierlich rotieren zu lassen. Fasse dazu mit der Maus mal den kleinen Pfeil neben dem “On Update” Node an und ziehe dann die Maus etwas nach rechts. Wenn du loslässt, siehst du ein Eingabefeld, wo du nach dem nächsten Node-Typ suchen kannst, der von hier aus ausgeführt werden soll. Tippe “rotate” und wähle “Transform: Rotate (Eulers)”:

Nach Auswahl siehst du den neuen Node vor dir:

Oben findest du stets den Titel der Node. Die grünen Pfeile symbolisieren den Programmablauf. Ausgehend vom “On Update” Node wird als nächstes unsere “Transform Rotate” Node ausgeführt.

Der Node hat zwei Parameter:

  1. Die Transform-Komponente, die rotiert werden soll. Die steht hier (und bei den meisten anderen Nodes auch) standardmäßig auf “This”, was bedeutet, dass es sich um die Transform-Komponente handelt, an der auch die Script Machine hängt.
  2. Den Winkel, um den um jede Achse rotiert werden soll, in Form eines “Vector3”.

Mithilfe der kleinen Kreise links der Parameter könnten wir die Eingabe von woanders beigeben, z. B. die Rotation aus einer Berechnung. Wir gehen aber erstmal den einfachen Weg und geben Werte direkt in die kleinen Eingabefelder in der Node ein. Weil die “Update” Funktion sehr oft aufgerufen wird, wählen wir testweise einfach mal ganz kleine Schritte von jeweils 0.1 – bei 30 Bildern pro Sekunde soll das Schwert also 3 Grad pro Sekunde um jede Achse rotieren. Tippe das in die Felder ein und starte dann das Spiel zum Testen:

Übrigens kannst du selbst am Mac für so einen schnellen Test einfach auf “Play” drücken. Wir brauchen kein Headset und keine Controller, wir schauen uns das Ergebnis einfach direkt im Scene View und Visual Script Graph View an:

Im Scene View sehen wir, wie das Schwert zu Boden fällt und rotiert. Im Script Graph View sehen wir anhand der weißen Punkte, die zwischen den Nodes wandern, wie der Programmablauf ist.

Die Rotation um alle Achsen klappt nicht ganz, weil unser Script und die Physik gegeneinander wirken. Effektiv sehen wir nur die Rotation um eine Achse, die anderen werden durch die Gravitation und Kollision des Schwerts mit dem Boden unterbunden. Es kann auch durchaus sein, dass das Schwert aufgrund dieses Konflikts wieder durch den Boden fällt.

Dieser erste kleine Test dient zum Reinschnuppern ins Visual Scripting. Es geht noch viel mehr und wir werden auch noch viel mehr nutzen, aber eins nach dem anderen! Jetzt müssen wir uns überlegen, wie wir eigentlich das Reparenting per Visual Script durchführen.

Zur Erinnerung: wir wollen Event Listener für zwei Unity Events aus dem XRGrabInteractable implementieren, die das Reparenting durchführen. Die Events senden jeweils einen Parameter mit.

Die schlechte Nachricht zuerst: Visual Scripting kann zwar Unity Events aus anderen Komponenten verarbeiten, aber es werden keine Parameter unterstützt. Die entsprechende Dokumentation findet sich hier.

Jetzt die gute Nachricht: jemand hat sich die Mühe gemacht und Erweiterungen für XRIT entwickelt, so dass wir die Events dort als ganz normale Trigger verwenden können. Das Ganze heißt “Visual Scripting extensions for XR Interaction Toolkit” und kommt mit einer ausgezeichneten Anleitung zur Installation, der wir folgen und für die ich hier kurz die Schritte auf Deutsch wiedergebe:

  1. Öffne den Package Manager unter Window → Package Manager
  2. Klicke links oben auf das “+” Icon im Fenster und wähle “Add package from git URL…”
  3. Gib dann “https://github.com/RoadToTheMetaverse/visualscripring.xrinteractiontoolkit.git” (ohne die Anführungszeichen) in das Feld ein. (Die Anleitung enthält noch den alten Repository-Namen, der aber inzwischen zu RoadToTheMetaverse weiterleitet.)
  4. Bestätige mit “Install”. Die Extension wird jetzt installiert.

Wenn die Installation abgeschlossen ist, sollte das neue Package so im Package Manager Fenster angezeigt werden:

Das Package Manager Fenster kann nun wieder geschlossen werden. Als Nächstes müssen wir dem Visual Scripting diese Erweiterungen bekannt machen.

  1. Öffne dazu Project Settings (Edit → Project Settings…, oder alternativ das hoffentlich bereits angeheftete Tab im Hauptfenster öffnen) und wähle links “Visual Scripting”.
  2. Öffne das “Node Library” Foldout und klicke unten auf das “+”, um dann in der Suche nach Eingabe von “xrit” das Package “VisualScripring.Extensions.XRIT” (sic!) hinzuzufügen.
  3. Klicke dann auf “Regenerate Nodes”
  4. Fertig, du kannst die Project Settings wieder verlassen.

Was uns das gebracht hast, können wir nun direkt im Visual Script Graph ausprobieren. Falls das Sword-GO nicht mehr in der Hierarchy markiert ist, wähle es wieder aus und öffne dann das Script Graph Tab. Klicke dann mit rechts in einen freien Bereich des Script Graphs und tippe in den Suchfilter “entered”. Du solltest jetzt die folgenden Events vor dir sehen:

Anhand des Pfads (“in Events/XRIT”) erkennen wir, dass “On Select Entered” das Event ist, das wir suchen, und mit Eingabe von “exited” finden wir auch das “On Select Exited” Event.

Wähle also zunächst “On Select Entered” aus. Das Event wird im Script Graph angelegt. Es sieht optisch aus wie die “On Start” und “On Update” Nodes: am grünen Balken oben und am fehlenden grünen Flow-Eingang links merkst du, dass es genau so ein Einstiegspunkt ist, der einen Visual Script Ablauf auslösen kann. Anhand der Parameter “Interactor” und “Interactable” siehst du, dass die Attribute des SelectEnterEventArgs Parameters bereits als Ausgang zur Verfügung stehen und wir sie weiterverwenden können.

Jetzt müssen wir das Reparenting ausführen. Falls du den C# Teil oben gelesen hast: wir müssen die Visual Scripting Entsprechung zu dieser Codezeile finden:

transform.SetParent(args.interactorObject.transform);

Klicke nochmals rechts in einen freien Bereich des Script Graphs und tippe “parent” in das Suchfeld. Es gibt tatsächlich einige Nodes, die offenbar genau diese Aufgabe übernehmen! Wähle “Transform: Set Parent”.

Dieser Node hat zwei Parameter: der erste ist das Transform, dessen Parent geändert werden soll. Das kann bei der Voreinstellung “This” bleiben. Der zweite Parameter ist der neue Parent. In unserem Fall soll das der “Interactor” sein, der dieses Event ausgelöst hat. Wir können also durch klick auf den grauen Kreis beim “Interactor” der “On Select Entered” Node eine Verknüpfung zu diesem zweiten Transform ziehen. Außerdem müssen wir noch den Flow, also das kleine graue Dreieck, von der “On Select Entered” Node zur neuen “Set Parent” Node ziehen. Wenn du das erledigt hast, sollte es so aussehen:

Falls du dich einmal vertust, kannst du eine Verbindung mit Rechtsklick auf eines der beiden Enden löschen.

Das Gleiche machen wir jetzt mit dem “On Select Exited” Event, mit dem Unterschied, dass wir hier keinen Interactor verdrahten, sondern den Parent wieder zu “Null” zurücksetzen, genau wie in dieser C# Codezeile:

transform.SetParent(null);

Wenn wir den Parent-Transform-Parameter der Node nicht belegen, wird er Orange angezeigt und links im “Graph Inspector” findet sich eine Warnung: “Value cannot be null”

Das ist insofern sinnvoll, als dass irgendein expliziter Wert für den Parent gesetzt werden sollte. Aber der darf durchaus “Null” sein. Wir können einen Node mit einem Null-Wert anlegen und den verknüpfen. Klicke wieder rechts auf eine freie Stelle, tippe “Null” und wähle den ersten Vorschlag. Verknüpfe dann diesen “Null” Node mit dem Parent-Transform-Parameter aus dem “Set Parent” Node.

Der Node bleibt leider Orange und die Warnung verschwindet nicht, aber der Script Graph ist so lauffähig.

Anmerkung: Mir erscheint das wie eine Nachlässigkeit in der Implementierung des Set Parent Node: der Parent darf Null sein. Einen alternativen Weg (z. B. ein Clear Parent Node) scheint es nicht zu geben. Falls du einen anderen Weg kennst, lass es mich gerne wissen!

Zum Abschluss möchte ich noch zwei allgemeine Tipps für’s Visual Scripting geben. Im Moment ist dieses Script, das ja an unserem Schwert hängt, noch recht klein. Über die Zeit wird es wachsen und kann dann auch schnell etwas unübersichtlich werden.

Zum einen kannst du mit einem Doppelklick mit links in einen freien Bereich des Script Graphs die Ansicht vergrößern, außerdem kannst du die Ansicht über den Zoom-Slider verkleinern. Das schafft mehr Übersicht.

Zum anderen ist eine einfache Möglichkeit, etwas Ordnung zu schaffen, das Gruppieren von Nodes. Du kannst mit gedrückter Strg- bzw. Cmd-Taste einen Rahmen um unsere beiden Events ziehen und diesem Rahmen dann durch Doppelklick auf den Titelbereich einen Namen geben, ich nenne es bei mir “Grab Handling”.

So sieht das dann im Vollbild und gruppiert aus:

Ausblick

Nimm dir etwas Zeit, das Gelernte zu rekapitulieren. Spiel vielleicht mal mit den Nodes rum und schau dir an, was Visual Scripting noch alles hergibt. Denn im nächsten Teil intensivieren wir das Ganze und führen einen Gegner ein, den wir mit dem Schwert zur Strecke bringen können. Es wird ein richtiger… Knochenjob! ☠️

Leave a Reply

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