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

Im letzten Teil haben wir die Spielansicht verschönert und den Level weiter ausgestaltet. In diesem Teil vollenden wir den Kampf mit dem Skelett, indem wir auch die Treffer des Skeletts beim Spieler registrieren und verarbeiten.

Scary Eyes

Auf den Beispiel-Bildern in Kay Lousberg’s Skeletons Character Pack sieht man, dass die Augen der Skelette glühen. Lichter in den Augen zu platzieren, wie wir das zuletzt für die Kerzen gemacht haben, wäre allerdings nicht sinnvoll: da die Skelette und andere bewegliche Objekte nicht Teil der Lightmap-Generierung sind und Echtzeit-Beleuchtung zu kostspielig für die Performance wäre, müssen wir uns einen anderen Weg überlegen.

Es sind allerdings nicht nur GameObjects mit der “Light” Komponente, die Licht abgeben (emittieren) können. Jede Oberfläche kann das ebenfalls, wenn das Material entsprechend konfiguriert ist. Das steuert das “Emission” Attribut, das standardmäßig deaktiviert ist.

Das “Skeleton_Minion” GO ist in mehrere Skinned Mesh Renderers aufgeteilt, die alle Teile des Skeletts rendern. So gibt es auch ein “Skeleton_Minion_Eyes” Child-GO, das mit dem Standard-Material konfiguriert ist, das wir für das Skelett angelegt hatten:

Wir werden ein Duplikat dieses Materials erzeugen, die Emissive-Eigenschaft aktivieren und es den Augen zuweisen.

Wähle dazu das “skeleton_texture” Material im Ordner Assets/060_Materials im Project View aus und drücke Strg-D/Cmd-D, um das Material zu duplizieren. Benenne das Duplikat zu “skeleton_texture_emissive” um.

Markiere das Material und aktiviere dann im Inspector die Emission Checkbox. Wähle als Emission Map die Textur, die auch als Base Map verwendet wird, und setze die Farbe zunächst auf “weiß” (255, 255, 255) und klicke dann auf den “+2” Button darunter, um die Intensität zu erhöhen. Die Emission Map bestimmt, welche Stellen des Materials eigentlich Licht emittieren, und in welcher Farbe: wäre hier etwas transparent, würde es als “nicht emittierend” interpretiert. Die Farbe wird durch die HDR Color eingefärbt und über die Intensität verstärkt. Man sieht unten rechts in der Vorschau, dass das Material jetzt stark überbeleuchtet wirkt, was aber hier unser Ziel ist.

Weise dieses neue Material nun dem Materials Slot im Skinned Mesh Renderer des “Skeleton_Minion_Eyes” GO zu.

Skelett-Prefab

Mit dem neuen Material für die leuchtenden Augen ist das Skelett jetzt so weit vorbereitet, dass wir aus dem GameObject, das bisher nur in der Szene existiert, ein Prefab machen können. Das ist nötig, damit wir es später zufällig und in größerer Anzahl zur Laufzeit auf dem Friedhof platzieren können.

Ziehe dazu das “Skeleton_Minion” GO aus der Hierarchy in den Assets/020_Prefabs Ordner!

Spieler-Schaden

Das Skelett greift den Spieler ja bereits an, erwirkt aber noch keinen Schaden, weil für den Spieler noch keinerlei solche Funktionalität existiert.

Wiederverwendung des Sword-Scripts

Zur Erinnerung: Beim Skelett haben wir die Kollision im Schwert erkannt, das Kollisionsobjekt auf das “Enemy” Tag geprüft und dann einen Event vom Kollisionsobjekt ausgehend “nach oben” zur Script Machine auf dem Root-GO des Skeletts geschickt. Die Logik dort prüft die “IsDead” Variable, reduziert die HP und spielt eine Animation ab oder macht aus dem Skelett eine Ragdoll.

Können wir den Script Graph des Spieler-Schwerts für das Schwert des Skeletts wiederverwenden?

Der Spieler hat bereits einen Collider in Form der Character Controller Komponente, welche von den XRIT Autoren im “XR Origin (XR Rig)” Prefab verwendet wurde. Eine Kollision sollte also erkannt werden. Das Tag des Kollisionsobjekts (eben jenes Character Controllers) ist allerdings “Player”, nicht “Enemy”. Also erweitern wir den “Sword” Script Graph entsprechend. Wir prüfen mit dem “Game Object Compare Tag” Node zusätzlich noch auf das “Player” Tag (1), wofür wir ebenfalls den Wert aus dem “Collider Get Game Object” Node benötigen, und verknüpfen beide Ergebnisse mithilfe eines “Or” Nodes (2), bevor wir dieses Ergebnis in das “If” geben.

Übrigens: dieser Script Graph enthält ja bereits die Events für das Grab-Handling. Wir können also sehr leicht das Schwert des Skeletts für den Spieler benutzbar machen, indem wir eine XRGrabInteractable Komponente hinzufügen – die sollte allerdings erst aktiviert werden, wenn das Skelett besiegt wurde.

Jetzt fügen wir noch eine Script Machine Komponente zum Schwert des Skeletts hinzu und verknüpfen den “Sword” Script Graph damit:

Kollisionserkennung zwischen Skelett-Schwert und Spieler

Wenn das Skelett jetzt den Spieler angreift, wird noch nichts passieren: der On Collision Enter Node im “Sword” Script Graph wird nicht ausgelöst. So etwas begegnet uns in der Entwicklung regelmäßig und kann viele Ursachen haben: der Abstand zwischen den Objekten ist zu groß, die Bewegung ist zu schnell oder der Collider zu klein, die unterschiedlichen Layer der GOs sind in den Physics-Settings so konfiguriert, dass sie nicht kollidieren…

In unserem Fall liegt es allerdings an einer Unity-Einstellung, die standardmäßig zur Optimierung gesetzt ist. Der Spieler hat keinen Rigidbody und ist damit implizit “kinematic”, das Schwert in der Hand des Skeletts hat einen Rigidbody, ist aber explizit “kinematic”, da die Position durch die Angriffs-Animationen gesteuert wird. In diesem Fall erkennt Unity standardmäßig keine Kollision. Um das zu ändern, kannst du unter Edit → Project Settings… zu den Physics-Settings wechseln und dann im GameObject Tab die Einstellung “Contact Pairs Mode” von Standard “Default Contact Pairs” zu “Enable Kinematic Kinematic Pairs” wechseln.

In einem großen Projekt mit einer umfangreichen Szene und vielen physikalischen Objekten kann das zu Overhead führen, aber in unserem Setup ist das völlig in Ordnung.

Nun wird vom Skelett-Schwert also auch ein Event beim Spieler ausgelöst – der hat aber noch keine eigene Script Machine samt Custom Event Handling.

Lege also eine Script Machine Komponente auf dem Spieler-GO “XR Origin (XR Rig)” an, klicke im Inspector auf “New” und speichere einen neuen Script Graph “Player” im Verzeichnis Assets/030_Scripts.

Mit der Script Machine kommt auch die Variables Komponente. Die nutzen wir, um wieder eine Variable “HP” vom Typ “Integer” und Wert 50 und “IsDead” vom Typ “Boolean” anzulegen.

Jetzt müssen wir den Custom Event “Damage”, der vom Schwert ausgesendet wird, im “Player” Script Graph verarbeiten. Diesen Teil können wir vom Skelett kopieren, lediglich auf den Cooldown verzichten wir:

Zur Erinnerung: diese Logik reagiert auf das “Damage” Event, das vom Schwert ausgesendet wird. Falls der Spieler noch nicht “IsDead” ist, reduzieren wir die HP Variable des Spielers um 10 Punkte und prüfen am Ende, ob der aktuelle Wert <= 0 ist. Ein Cooldown ist nicht nötig, weil die Frequenz, in der ein einzelnes Skelett den Spieler treffen kann, ohnehin durch die Animation begrenzt ist. Würden wir den Cooldown beibehalten, würde der Spieler durch den Angriff mehrerer Skelette gleichzeitig nicht in Bedrängnis geraten, da einige Treffer innerhalb der Cooldown-Periode gar nicht zählen würden!

Was allerdings dann nach dem “If” folgen soll, müssen wir noch besprechen: im Falle eines Treffers mit verbleibenden Rest-HP sollten wir zumindest irgendein Feedback anzeigen und falls der Spieler keine HP mehr hat, sollte das Spiel beendet werden.

Treffer-Feedback für den Spieler

Ein Muster, das sich in “Flat Screen” Spielen etabliert hat, ist eine rot gefärbte Vignette, die dem Spieler einen kritischen Treffer signalisiert. Wir bedienen uns daran und färben die Sicht des Spielers kurz rot.

Dazu positionieren wir in kurzer Distanz von der Kamera im Player-Rig ein leeres GO und darunter einen UI Canvas mit einem Image. Gehe dazu wie folgt vor:

  1. Öffne in der Hierarchy das “Main Camera” GO innerhalb des “XR Origin (XR Rig)” GO.
  2. Klicke mit rechts auf die “Main Camera” und wähle Create Empty. Nenne das neue Child-GO “Fader” und ändere die Position zu (0, 0, 0.02). Damit wird es leicht vor die Kamera versetzt und die Z-Position ist etwas höher als das Near Clipping Plane, ab wo die Kamera beginnt, überhaupt Geometrie zu rendern. (Das kannst du dir im “Main Camera” GO unter Projection → Clipping Planes anschauen.)
  3. Klicke mit rechts auf das neue GO “Fader” und wähle UI → Canvas. Ändere den “Render Mode” zu “World Space”, daraufhin kannst du die Position des Canvas zu (0, 0, 0), Width und Height zu (400, 400) und Scale zu (0.01, 0.01, 0.01) ändern.
  4. Klicke mit rechts auf das neue GO “Canvas” und wähle UI → Image. Nenne das neue Child-GO “Hit” und ändere Width und Height zu (400, 400), so dass das Image den gesamten Canvas ausfüllt. Ändere die Farbe des Image zu rot.

Nach diesen Schritten sollten Hierarchy und Scene so aussehen:

Mit dem Canvas wird übrigens auch automatisch ein “EventSystem” GO auf oberster Ebene angelegt. Das ignorieren wir zunächst.

Wenn du dir den Game View anschaust oder das Spiel sogar auf dem Headset testest, wirst du feststellen, dass die gesamte Ansicht rot ist:

Nachdem wir uns davon vergewissert haben, dass die Positionierung und Dimensionierung des Image passt, können wir die Farbe auf transparent setzen:

Wir werden im Falle eines Treffers dieses Bild ganz kurz einblenden (d.h. die Transparenz auf 255 setzen) und direkt wieder ausblenden. Die Logik dafür könnten wir im Script Graph entwickeln, aber es gibt auch einen anderen Weg: eine Animation. Mithilfe eines Animator Controllers und einer Animation kann man nämlich auch andere Eigenschaften als nur den Transform eines GameObjects animieren!

Da wir das “Fader” GO später auch noch für einen anderen Zweck wiederverwenden werden, werden wir den Animator samt Animator Controller dort platzieren. Starten wir aber zunächst mit dem Animator Controller, um den möglichen Ablauf in Animation States zu modellieren. Klicke mit rechts im Project View auf den Assets/070_Animation Ordner und wähle dann Create → Animation → Animator Controller. Gib als Name “Fader” ein. Öffne dann diesen Animator Controller, wechsle links oben auf das Parameters Tab und lege einen neuen Parameter vom Typ “Trigger” mit dem Namen “Hit” an. Lege dann per Rechtsklick in den Graph und Create State → Empty einen neuen Animation State an. Wenn du dich an das Anlegen der Animation für das Skelett erinnerst, dann weißt du noch, dass es immer einen Default State geben muss. Da wir im “Default” aber nichts abspielen wollen, lassen wir diesen State leer.

Füge dann zum “Fader” GO in der Hierarchy eine Animator Komponente hinzu und ziehe den “Fader” Animator Controller in das “Controller” Feld:

Jetzt müssen wir einen Animation Clip aufnehmen, der die Transparenz des Bildes von 0 kurz auf 255 setzt und dann wieder zurück auf 0. Dazu können wir das Animator Window öffnen: Window → Animation → Animation. Das Tab aus dem neuen Fenster solltest du dir neben das Project View Tab ziehen.

Wenn du nun im Hierarchy weiterhin das “Fader” GO markiert hast, siehst du im Animation View eine Aufforderung: “To begin animating Fader, create an Animation Clip.” mit einem “Create” Button. Klicke den Button, und gib dann im Speichern Dialog “Fader_Hit” als Name für den Animation Clip ein. Achte darauf, dass die Datei im Ordner Assets/070_Animation gespeichert wird.

Die Animation wird mit aktiviertem “Loop Time” Feld aktiviert, das wollen wir in diesem Fall allerdings nicht. Markiere daher im Project View die neue Datei und klicke dann rechts im Inspector auf “Loop Time”, so dass die Checkbox nicht mehr abgehakt ist. Einen separaten “Apply” Button gibt es hier nicht.

Wenn du zum Animator Controller zurückkehrst, siehst do dort, dass ein neuer Animation State “Fader_Hit” angelegt wurde, in dem im “Motion” Feld der neue Animation Clip hinterlegt wurde. Der Animation State steht allerdings noch komplett alleine und ohne Transition. Deshalb ziehen wir eine Transition vom “Any State” zum “Fader_Hit” State und fügen eine Condition zur Transition hinzu, die automatisch mit dem “Hit” Trigger belegt ist. Die “Transition Duration (s)” können wir auf 0 setzen, da wir keinen Übergang von einem Ursprungs-State brauchen: es gibt ja keinen.

Jetzt haben wir die Voraussetzungen geschaffen, dass die Animation von außen ausgelöst werden kann. Nun müssen wir noch die tatsächliche Animation bearbeiten.

Klicke im Project View auf den angelegten Animation Clip, Assets/070_Animations/Fader_Hit. Aktiviere in der Hierarchy das “Hit” GO, wechsle zum Scene View und stelle sicher, dass du dort das Player Rig samt diesem Image gut im Blick hast. Mit aktivierten Gizmos kannst du die Ansicht gut ausrichten.

Wechsle jetzt zum Animation Tab, das wir gerade als Window geöffnet und angedockt haben. Dort sollte “Fader_Hit” noch als Clip-Name zu sehen und der rote “Record” Button aktiv sein.

Die Animations-Ansicht hat eine Zeitachse von 0:00 bis 1:00, das ist die aktuelle Dauer des Animations-Clips in Sekunden. Die aktuelle Position in der Animation wird durch eine vertikale weiße Linie gezeigt und du kannst die Position durch klicken und ziehen auf der Zeitachse verschieben. Damit bewegst du dich zwischen den Frames der Animation, von denen es aktuell insgesamt 60 gibt, da die Animation standardmäßig mit 60 FPS (Frames Per Second) angelegt wird. Links neben der Zeitachse ist ein Eingabefeld, in dem das aktuelle Frame steht. Du kannst zu einem Frame springen, indem du hier eine Zahl eingibst.

Stelle sicher, dass wir bei Frame 0 sind. Klicke dann den roten Record-Button, um die Aufnahme zu starten. Die Zeitachse wird während der Animation rot. Änderungen, die du an GameObjects unterhalb des “Fader” GO vornimmst, erzeugen während der Aufnahme sogenannte “Keyframes” an der aktuellen Position, d.h. die Werte werden beim aktuellen Frame aufgezeichnet.

Klicke nun auf das “Hit” GO und ändere die Farbe in der Image Komponente. Du siehst, dass im Animation Window nun ein Keyframe angelegt wurde. Ändere die Farbe zurück zum Originalwert, damit kein falscher Wert im ersten Keyframe steht. Unity erzeugt erst dann Keyframes, wenn es tatsächlich einen Änderung registriert hat, daher ist “ändern und zurück ändern” eine gängige Praxis zum Anlegen des initialen Keyframes.

Jetzt können wir die Position in der Animation etwas weiterschieben und die Transparenz der Image Color auf 255 verändern. Dann wird ein neues Keyframe mit diesem Wert erzeugt. Bei einer Animation über eine Sekunde und 60 FPS wäre es logisch, ein Keyframe bei Frame 0 anzulegen, die volle Sichtbarkeit auf der Hälfte bei 29 und beim letzten Frame 59 dann wieder ein Keyframe mit Transparenz 0. Eine ganze Sekunde ist aber etwas zu viel für den Effekt, daher springen wir zu Frame 5 und setzen dort die Transparenz auf 255. Zum Abschluss springen wir zu Frame 20 und setzen dort die Transparenz zurück auf 0. Der “Hit” soll also schnell durch das Einblenden sichtbar werden, darf dann aber ruhig ein klein wenig langsamer ausfaden. Apropos faden: die Zwischenwerte werden interpoliert, so dass wir einen sanften Übergang haben.

Den Aufnahmemodus können wir nun durch erneuten Klick auf den roten Aufnahme-Button verlassen. Nach Klick auf den Play-Button sollten wir dieses Ergebnis sehen:

Jetzt müssen wir diesen Effekt in unserem “Player” Script Graph ändern. Wie wir Animations-Trigger auslösen, wissen wir schon. Allerdings kennt der Script Graph das “Fader” Objekt noch nicht. Daher führen wir auf dem “XR Origin (XR Rig)” GO eine Variable “Fader” vom Typ GameObject ein und ziehen das Fader-GO dort hinein:

Jetzt wechseln wir zurück zum “Player” Script Graph und erweitern den “False” Ausgang am “If” Node – d.h. der Treffer ist nicht tödlich und die HP sind noch nicht <= 0 – wie folgt, um die Animation auszulösen:

Die Emission der Augen ist so stark, dass sie sogar durch das “Hit” UI Image strahlt. Nur für den kurzen Augenblick, wo das Image voll opak ist, sind auch die nicht mehr zu sehen.

Das Skelett greift an und fügt dem Spieler Schaden zu, der durch das rote Aufleuchten signalisiert wird.

Spiel-Ende-Feedback für den Spieler

Wenn die HP des Spielers aufgebraucht sind, werden wir einen ähnlichen Effekt wie bei den Treffern anwenden: der Bildschirm soll faden, allerdings diesmal nicht kurz zu rot, sondern langsam (dramatisch!) zu schwarz. Und dann wechseln wir die Kamera und zeigen eine kleine Game-Over-Ansicht.

Das Ausfaden der Ansicht funktioniert ähnlich wie das “Hit” UI Image. Wir legen zunächst unterhalb des Canvas ein weiteres UI Image mit den gleichen Parametern an. Du kannst es auch duplizieren. Unterschiede zur “Hit” Animation liegen in der Farbe – wir färben das UI Image hier schwarz –, der Dauer, und der Tatsache, dass wir irgendwann aus dem abgedunkelten Bildschirmstatus wieder zurückkehren wollen.

Wir wechseln also zum “Fader” GO in der Hierarchy und aktivieren wieder das Animation Tab, in dem wir noch den bereits fertig konfigurierten “Fader_Hit” Animation Clip sehen. Auf diesen Namen klicken wir, um ein Dropdown-Menü zu öffnen, wo wir unten “Create New Clip…” auswählen. Den neuen Clip speichern wir unter dem Namen “Fader_ToBlack”.

Auch für diesen Clip müssen wir das “Loop Time” Attribut deaktivieren, indem wir ihn in der Project View markieren und im Inspector die Checkbox klicken.

Dann wechseln wir zurück zum “Fader” GO in der Hierarchy und starten den Aufnahmemodus durch Klick auf den roten Aufnahme-Button im Animation View, wechseln dann in der Hierarchy zum neuen “Black” GO und machen hier eine kurze Änderung an der Transparenz der Farbe, um das Anlegen eines Keyframes zu erzwingen. Dann schieben wir den Marker weiter zu Frame 30 und setzen hier die Transparenz auf 255. Dann verlassen wir den Aufnahme-Modus durch erneuten Klick auf den Aufnahme-Button wieder.

So sieht der “Fader_ToBlack” Animation Clip im Animation View aus. Ein Keyframe bei 0:00, ein weiteres bei 0:30.

Jetzt wechseln wir in den Animation Controller. Der Animation State “Fader_ToBlack” wurde hier bereits für uns angelegt, lediglich der Parameter und die Transition fehlen.

Diesmal legen wir also einen “Trigger” Parameter mit Namen “ToBlack” an. Die Transition zu “Fader_ToBlack” nutzt diesen Trigger als Transition-Condition.

Dann gehen wir den gleichen Weg für die umgekehrt Blendrichtung: wir legen einen weiteren Animation Clip an, nennen ihn diesmal “Fader_FromBlack”, deaktivieren wieder das “Loop Time” Attribut und nehmen zwei Keyframes auf. Den Abstand zwischen den Keyframes reduzieren wir diesmal auf 10 Frames (= 10 Sekunden), denn das wieder Einblenden kann ruhig etwas schneller gehen als das dramatische Ausblenden.

Auch für diesen neuen State gibt es einen passenden Trigger mit Namen “FromBlack” und eine Transition vom “Any State” mit diesem Trigger als Bedingung.

Wenn dem Spieler die HP ausgehen, wollen wir drei Dinge tun:

  1. Locomotion deaktivieren
  2. die Controller deaktivieren
  3. den Bildschirm zu schwarz ausblenden

1. und 2. erledigen wir, in dem wir die jeweiligen GameObjects aus dem XR Origin deaktivieren. Damit unser “Player” Script Graph diese GOs kennt, müssen wir sie wieder per Object Variable bekannt machen. Wir legen dazu auf dem “XR Origin (XR Rig)” drei Variablen mit den Namen “LeftController”, “RightController” und “Locomotion” vom Typ GameObject an:

Mit diesen Variablen können wir die Verarbeitung ausgehend vom bisher noch unbelegten “True” Ausgang der “If” Node, der das Ergebnis des HP Vergleichs mit 0 verarbeitet, fortsetzen. Wir müssen die “IsDead” Variable auf “True” setzen (1), die Controls des Spielers deaktivieren, indem wir sie per “Game Object Set Active” Node deaktivieren (2, 3, 4), und die Schwarzblende triggern (5):

Es macht Sinn, dass auch das Skelett vom Spieler ablässt, wenn dessen “IsDead” Variable “True” ist. Daher wechseln wir kurz zum Script Graph des Skeletts!

Das hat einen “On Update” Event, in dem wir prüfen, ob das Skelett selbst “IsDead” ist und ob es “IsAttacking” ist, und von dort aus zweigt dann die Bewegungs- oder Angriffs-Logik ab. In den Angriffs-Zweig fügen wir eine weitere Bedingung ein: nur wenn der Spieler nicht “IsDead” ist, soll der Angriff fortgesetzt werden, andernfalls soll die “IsAttacking” Variable des Skeletts zurück auf “False” gesetzt werden:

Wie du siehst, führen wir beim Ermitteln der “IsDead” Variable des Spielers eine weitere Indirektion ein: anstatt die Object Variable bei “This” zu lesen, geben wir als Quellobjekt die Scene Variable “player” ein, in der wir die Spielerreferenz gesichert haben.

Game-Over Bildschirm

Jetzt bauen wir eine kleine “Game Over” Ansicht in die gleiche Szene. Hier ist vorab schonmal mein Ergebnis, damit wir ein gemeinsames Verständnis davon haben, was damit eigentlich gemeint ist:

Bei der Gestaltung sind natürlich keine Grenzen gesetzt und du kannst das ganz anders machen.

Wir beginnen mit einem Parent-GO für den Aufbau. Lege durch Klick in einen freien Bereich der Hierarchy per Create Empty ein neues GO an und nenne es “Game Over”. Hilfreich ist, wenn dieser Teil der Szene nicht mit unserem Spielfeld kollidiert, das macht das Arbeiten darin einfacher und erlaubt uns, diesen Teil einfach aktiv zu lassen: setze daher die Position auf (0, 0, -2000).

In meinem Entwurf habe ich mit einem Grabstein an der gleichen Position begonnen und dazu das “gravestone” Model aus dem Assets/040_Models/Halloween Ordner direkt unter das “Game Over” GO gezogen und im Inspector auf “Static” gesetzt. Ich habe übrigens extra die Models genutzt, weil wir keine extra Funktionalität, die vielleicht am Prefab hängt, benötigen; du kannst aber gerne auch Prefabs verwenden. Ich habe außerdem ein Grab (floor_dirt_grave) davor und ein paar Bodenplatten (floor_dirt) drumherum platziert. Ins Grab habe ich einen Sarg (coffin) platziert und ihn etwas auf der Y-Achse abgesenkt. Zuletzt habe ich ein paar Kerzen platziert und auf den Kerzengruppen je nach Bedarf ein Point Light sowie unser “flame” Prefab. Ebenso habe ich ein schwaches, hellblaues Licht in das Grab gelegt, um den Sarg darin etwas zu beleuchten. Denke daran, dass der “Mode” der Point Lights jeweils auf “Baked” gestellt werden muss! Für die Lichter der Kerzen empfehle ich außerdem, den “Shadow Type” auf “Soft Shadows” zu setzen.

Da wir ein Directional Light in der Szene haben, fällt immer etwas Licht auf die Szene. In diesem Fall würde ich das gerne vermeiden, damit der Spieler bei dieser Ansicht gar nicht merkt, dass die Kacheln irgendwann aufhören. Ebenso würde ich gerne die Skybox ausblenden. Beides erreichen wir dadurch, dass wir diesen Teil der Szene durch einfache Planes (Rechtsklick auf das “Game Over” GO → 3D Object → Plane) von allen Seiten umschließen, wie in einem Schuhkarton. Lediglich auf der Unterseite benötigen wir kein solches Plane.

Damit diese Planes wirklich schwarz sind, erstellen wir ein neues Material “Black” mit “Base Map” Farbe schwarz und “Smoothness” 0 und weisen es allen Planes zu. Die Planes konfigurieren wir außerdem als “Cast Shadows”: “Double Sided”.

Hier ist ein Zwischenstand aus meinem Setup:

Nach Zusammenstellen der Szene habe ich die Beleuchtung neu generiert (Lighting Tab → Generate Lighting), um zu sehen, wie es ungefähr im Spiel aussehen würde.

Um dem Spieler etwas textuelles Feedback zu geben, erzeugen wir ebenfalls unter dem “Game Over” GO einen neuen Canvas und setzen den “Render Mode” auf “World Space”. Jetzt können wir die Position anpassen, ich nutze (0, 2.5, 1.5), Width/Height (600, 200), Rotation (15, 180, 0) sowie Scale (0.01, 0.01, 0.01).

Auf den Canvas können wir dann eine Nachricht platzieren: Rechtsklick auf das “Canvas” GO → UI → Text – TextMeshPro.

Bei TextMeshPro handelt es sich um ein leistungsstarkes Text-Rendering-Tool, das scharfe und flexible Typografie mit erweiterten Anpassungsoptionen und Effekten für hochwertige Textdarstellung ermöglicht. Sobald das erste “TextMeshPro” Objekt in der Szene erzeugt wird, taucht das folgende Popup in Unity auf:

Hier klicken wir auf “Import TMP Essentials” und können das Popup dann wieder schließen. Den Beispieltext des neuen GOs mit der “TextMeshPro – Text (UI)” Komponente ändere ich zu:

Der Skelettkönig lacht zuletzt!
Deine Freunde bleiben im Gruselknast.

Damit der Text gut passt, ändere ich die Höhe und Breite des Rect Transforms zu (600, 200) (wie der Parent-Canvas) und zentriere die Darstellung horizontal und vertikal. Aber so richtig will die Schriftart noch nicht zum Spiel passen, oder?

Dank TextMeshPro können wir jede beliebige TrueType Font verwenden und dank Google Webfonts finden wir davon auch zahlreiche kostenlos online unter https://fonts.google.com/. Während ich diesen Text schreibe, zeigt Google sogar links einen Filter für saisonale Fonts an:

Ich habe mich von der Auswahl inspirieren lassen und bin bei Jolly Lodger hängengeblieben. Diese Font lade ich per Get Font → Download herunter und entpacke das Archiv, in welchem sich neben der Open Font License die Datei JollyLodger-Regular.ttf befindet.

Um diese mithilfe von TextMeshPro in Unity zu importieren, legen wir im Project View einen neuen Ordner Assets/950_Fonts an – um dem Schema treu zu bleiben, dass selten genutzte Assets weit hinten einsortiert werden. In diesen Ordner kopieren wir die gerade entpackte .ttf Datei.

Jetzt öffnen wir unter Window → TextMeshPro → Font Asset Creator den Dialog zum Konvertieren von TrueType Fonts in ein Format, das TextMeshPro kennt.

Als Source Font wählen wir die Font aus dem neuen Ordner aus (1). Dann klicken wir auf “Generate Font Atlas” (2) und abschließend auf “Save”. Der Save-Dialog sollte bereits im 950_Fonts Ordner geöffnet werden, dort kannst du dann wie vorgeschlagen die JollyLodger-Regular SDF.asset Datei abspeichern (oder ähnlich, falls du einen anderen Font gewählt hast).

Die Warnungen, die beim Generieren des Font Atlas entstehen, können wir ignorieren.

Jetzt können wir den Dialog schließen und zum “Text (TMP)” GO mit dem Game Over Text zurückkehren. Dort wählen wir jetzt als “Font Asset” die neu angelegte Schrift an. Zusätzlich färbe ich meinen Text noch blutrot und bin dann bei dem Status, den ich zu Beginn als Beispiel gezeigt hatte.

Dadurch, dass diese kleine Szene fernab des Spielbereichs existiert, dank des Nebels aus dem Spielfeld nicht sichtbar ist und neben ein paar Polygonen und Lightmap-Platz nichts weiter kostet, lassen wir den Teil nun einfach so stehen und sorgen dafür, dass der Spieler diese Szene auch zu sehen bekommt.

Deshalb teleportieren wir den Spieler nach der Schwarzblende einfach in diese Szene! Wir legen daher noch ein letztes, leeres GameObject unter dem “Game Over” GO an, nennen es “Player Target” und setzen die Position auf (0, 0, 6) und die Rotation auf (0, 180, 0). An dieses GO reparenten wir den Spieler einfach.

Damit der “Player” Script Graph weiß, wohin der Spieler beim Game Over durch reparenting teleportiert werden soll, müssen wir dieses neue “Player Target” GO entweder in den Object oder in den Scene Variables bekannt machen. Da wir in diesem Fall bisher nur auf eigene (Object) Variablen zugreift, bleiben wir dabei und legen auf dem “XR Origin (XR Rig)” GO noch eine Variable “GameOverTarget” vom Typ “Game Object” an und ziehen das “Player Target” GO in das entsprechende Feld.

Das Reparenting soll erst stattfinden, wenn der Bildschirm zu schwarz gefadet ist – besser sogar noch etwas später, für etwas Drama. Wir müssen also im Script Graph etwas warten. Das geht auf mehrere Arten, wir wählen hier für uns die einfachste: einen Timer-Node. Mit dem können wir die Ausführung für eine gegebene Zeit unterbrechen und daraufhin wird der Flow fortgesetzt. (Eine Alternative sind die verschiedenen Wait-Nodes, die erfordern allerdings, dass die gesamte Event-Behandlung zu einer Coroutine wird.)

Nach der Schwarzblende warten wir also drei Sekunden (1), hängen dann den Spieler unter unser “Game Over Target” GO (2) und setzen die lokale Position und Rotation zurück, damit der Spieler in die vorgesehene Richtung blickt (3). Dann blenden wir wieder zurück von schwarz zu transparent (4):

Nun würde der Spieler normalerweise den letzten Speicherstand laden oder zum Hauptmenü zurückkehren. Wir haben beides nicht, daher warten wir einfach nochmal einige Sekunden (1), faden das Bild wieder aus (2), warten auf das Ende der Animation (3) und laden dann die ganze Szene neu (4). Zu letzterem verwenden wir den Node “Scene Manager Load Scene”, in den wir den Namen unserer Szene, Graveyard, als Parameter geben.

So sieht das Ganze dann aus:

Ausblick

Bevor wir bald den Friedhof mit Gräbern ausstatten, aus denen mehr als nur ein Skelett emporsteigt, widmen wir uns im nächsten Teil erstmal wieder einer VR Mechanik: wir müssen ja noch die Knochenschlüssel zum Öffnen der Gruft entwickeln.

Leave a Reply

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