Netzwerk-Objekte inkl. Rotation spawnen

Netzwerk-Objekte mit einem NetworkIdentity-Script werden zwar an der richtigen Position gespawnt, die Rotation und die Skalierung wird dabei jedoch nicht übernommen. Dies liegt daran, dass beim Spawnen auch nur die Position übertragen wird. Es ist möglich dass dieses Verhalten noch angepasst wird.

Die Rotation der gelben Objekte wurde nicht über das Netzwerk synchronisiert.

Die Rotation der gelben Objekte wurde nicht über das Netzwerk synchronisiert.

Meiner Meinung nach ist das Verhalten so richtig, da es auch Objekte gibt, bei denen die Position/Rotation/Skalierung absolut keine Rolle spielt. Aus diesem Grund gibt es das NetworkTransform-Script, welches die Eigenschaften eines Transforms oder Rigidbodys über Netzwerk synchronisiert. Das funktioniert auch. Jedoch wird die Rotation erst nach einer Verzögerung übernommen und nicht von Anfang an, was unschön ist. Daher bin ich der Ansicht, dass wenn etwas korrigiert wird, es am besten bei diesem Script gemacht werden sollte.

In der Zwischenzeit kann das Problem z.B. mit folgendem kleinen Script gelöst werden:

Einfach zusätzlich zum NetworkIdentity das neue NetworkObject-Script anhängen:

Die Rotation der gelben Objekte wurde über Netzwerk synchronisiert.

Die Rotation der gelben Objekte wurde über Netzwerk synchronisiert.

Wissenswertes zu Koroutinen

Was sind Koroutinen?

Koroutinen sind Funktionen die über mehrere Frames hinweg ausgeführt werden. Sie sind äusserst praktisch um bestimmte Abläufe zu programmieren, z.B. ein Objekt sanft von A nach B gleiten lassen. In einer weiteren Koroutine könnte dann zuerst von A nach B bewegt werden und nach 5 Sekunden Verzögerung von B nach C. Dies erreicht man, indem die eine Koroutine auf die Ausführung einer anderen Koroutine wartet. Hier ein Beispiel:

Der grosse Vorteil ist, dass der Zustand (in diesem Fall die Ziel-Position, aber auch alle lokalen Variablen) über alle Frames hinweg erhalten bleibt. So können selbst komplizierteste Abläufe auf relativ einfache Art und Weise programmiert werden, ohne den Überblick zu verlieren.

 

Wie funktionieren Koroutinen aus technischer Sicht?

Koroutinen laufen zwar parallel, aber verwenden dazu keine Threads. Im Prinzip ist eine Koroutine ein C#-Feature (gibt es bestimmt auch in anderen Programmiersprachen).

Damit eine foreach-Schleife in C# überhaupt funktionieren kann, muss ein Array, eine Liste oder jede andere Klasse von der etwas enumeriert (aufgezählt) werden soll, das Interface IEnumerable implementieren. Dieses Interface besitzt eine einzige Methode:

Diese Methode gibt wiederum ein Objekt zurück, welches das IEnumerator-Interface implementiert. Das IEnumerator-Interface sieht ungefähr so aus:

Eine foreach-Schleife wird dann in den folgenden Code konvertiert:

Möchte man einen eigenen Enumerator programmieren, müsste man also eine Klasse erstellen die das Interface IEnumerator implementiert. An dieser Stelle kommt jedoch nun das oben genannte C#-Feature hinzu. Anstatt eine komplette Klasse programmieren zu müssen, kann auch nur eine Enumerator-Funktionen erstellt werden. Das ist eine vereinfachte Syntax zum Erstellen eigener Enumeratoren. Es ist genau die Syntax die auch in Unity bei den Koroutinen verwendet wird:

Dieser Enumerator, der jedes zweite Zeichen eines Strings zurückgibt, wird ungefähr in den folgenden Code umgewandelt:

Und kann dann so verwendet werden:

Mit den Enumerator-Funktionen funktioniert es ähnlich:

Das mit dem yield return kann man sich nun folgendermassen vorstellen:

  • Beim 1. Aufruf von MoveNext() wird die Funktion ausgeführt bis das erste yield return oder ein yield break kommt.
  • Das was das yield return zurückgibt, ist dann über das Current-Property des Enumerators abrufbar.
  • Beim nächsten Aufruf von MoveNext() wird mitten in die Enumerator-Funktion – auf die Zeile nach dem 1. yield return – gesprungen.
  • Ab da wird die Enumerator-Funktion bis zum nächsten yield return ausgeführt und dieser Vorgang wiederholt sich bis die Funktion entweder zu Ende ist, oder bis ein yield break angetroffen wird. Das yield break sowie das Ende der Funktion sorgen dafür, dass bei MoveNext() ein false zurückgegeben wird und die Schleife unterbrochen wird.

Unity verwendet statt der while-Schleife einfach die Hauptschleife des Spiels. In jedem Frame wird pro Koroutine 1x die Funktion MoveNext() aufgerufen. Gibt MoveNext() false zurück, dann ist die Koroutine beendet. Wird mit yield return ein null zurückgegeben, dann führt Unity im nächsten Frame erneut ein MoveNext() aus. Wird ein WaitForSeconds-Objekt zurückgegeben, dann liest Unity aus diesem Objekt die Anzahl Sekunden heraus und sorgt dafür, dass der nächste MoveNext()-Aufruf erst nach dieser Anzahl Sekunden erfolgt. Gibt yield return eine andere Koroutine zurück, dann wird das Aufrufen von MoveNext() so lange hinausgezögert, bis die andere Koroutine nach dem genau gleichen Prinzip ebenfalls komplett abgearbeitet wurde.

 

Gibt es sonst noch etwas Wissenswertes zu Koroutinen?

Ausgeführt werden die Koroutinen von der Klasse MonoBehaviour, welches die Grundlage bzw. Basisklasse der meisten Unity-Scripts ist. Das heisst wenn ein Game-Objekt zerstört wird oder wenn die Script-Komponente vom Game-Objekt entfernt wird, dann werden sämtliche Koroutinen mitten in der Ausführung abgebrochen. Denn wenn das MonoBehaviour fehlt, welches die Koroutinen ausführt, dann können diese natürlich nicht mehr laufen.

Aus diesem Grund sollte man bei verschachtelten Koroutinen immer aufpassen wo diese ausgeführt werden. Hier ein kleines Beispiel:

Wird StartCoroutine(Movement1()) ausgeführt, dann beginnt sich der Würfel (MyCube) zu bewegen. Wenn das ScriptedMovement-Objekt jetzt zerstört wird, dann stoppt der Würfel mitten in der Bewegung, da die Koroutine innerhalb des ScriptedMovement-Objekts lief.

Verwendet man statt der Movement1 die Movement2-Funktion, so würde sich der Würfel weiterbewegen bis er beim Ziel ankommt, da die Koroutine beim Würfel selbst läuft.

Beachtet man dies nicht, dann kann in komplexeren Fällen schnell mal eine Koroutine unerwartet unterbrochen werden.

Wer der Ansicht ist, dass Koroutinen immer in der Klasse ausgeführt werden sollten in der sie definiert sind, der kann folgenden Trick anwenden:

Hier wurde eine öffentliche Methode erstellt, die nichts anderes tut, als die private Koroutine zu starten und dann ein Coroutine-Objekt zurückzugeben. Dieses Objekt wird von der StartCoroutine()-Methode zurückgegeben.

Ein yield return StartCoroutine(..); könnte also auch so geschrieben werden:

Und daher auch:

In beiden Fällen wird letztendlich ein Coroutine-Objekt zurückgegeben.

Anzeigen oder Verstecken von Variablen im Editor

Wie die meisten bestimmt wissen, werden öffentliche Felder in Unity-Scripts (MonoBehaviour) automatisch von Unity im Editor angezeigt und auch deren Werte innerhalb der Szene gespeichert. Geschützte und private Felder werden hingegen nicht angezeigt oder gespeichert.

Interessant ist, dass man dieses Verhalten mit bestimmten Attributen beeinflussen kann:

Aber in welchem Fall ist was sinnvoll?

HideInInspector ist vor allem dann sinnvoll, wenn es ein aussenstehendes Editor-Script gibt, das dieses Feld bearbeitet. Das Feld kann so vor dem Benutzer versteckt werden, während das Editor-Script weiterhin Zugriff darauf hat. Da es immer noch serialisiert wird, werden die Werte auch in der Szene gespeichert, was im Falle eines Editor-Scripts wünschenswert ist.

SerializeField hingegen, sollte möglichst oft anstelle von öffentlichen Feldern benutzt werden. In den meisten Fällen macht es nämlich keinen Sinn, dass ein Feld öffentlich sichtbar ist und jedes externe Script nach belieben die Werte verändern kann. Mit SerializeField bleiben die Felder vor externen Zugriffen geschützt und doch kann der Benutzer die Werte im Editor anpassen. Die Werte werden natürlich auch in der Szene gespeichert.

System.NonSerialized verhält sich ähnlich wie HideInInspector, allerdings werden die Werte nicht in der Szene gespeichert. Das macht dann Sinn, wenn Felder für andere Scripts zugänglich sein sollen, während sie für den Benutzer nicht relevant sind. Andererseits eignen sich für diesen Fall Properties meistens besser.