KategorieUnity

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:

using UnityEngine.Networking;

public class NetworkObject : NetworkBehaviour
{
    public override bool OnSerialize(NetworkWriter writer, bool initialState)
    {
        if (initialState)
        {
            // Transmit the rotation (and scale) only once (initalState)
            writer.Write(transform.localRotation);
            //writer.Write(transform.localScale); // Uncomment this line, if you need scale too
            return true;
        }
        // Don't send further state updates
        return false;
    }

    public override void OnDeserialize(NetworkReader reader, bool initialState)
    {
        if (initialState)
        {
            // Apply the retrieved rotation (and scale)
            transform.localRotation = reader.ReadQuaternion();
            //transform.localScale = reader.ReadVector3(); // Uncomment this line, if you need scale too
        }
    }
}

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:

class MovingObject : MonoBehaviour
{
    void Start()
    {
        // Start the first coroutine
        StartCoroutine(Move());
    }

    // Move to 0,0,10 then wait 5 seconds and then move to 10,0,10
    IEnumerator Move()
    {
        // Stop execution until the MoveTo-coroutine has completed
        yield return StartCoroutine(MoveTo(new Vector3(0,0,10)));
        // Stop execution until 5 seconds have passed
        yield return new WaitForSeconds(5);
        // Stop execution until the MoveTo-coroutine has completed
        yield return StartCoroutine(MoveTo(new Vector3(10,0,10)));
    }

    // Move to the specified target over multiple frames (smoothed out)
    IEnumerator MoveTo(Vector3 target)
    {
        Transform t = transform;
        while(Vector3.Distance(t.position, target) > 0.2f)
        {
            t.position = Vector3.Lerp(t.position, target, Time.deltaTime);
            // Stop execution until the next frame
            yield return null;
        }
    }
}

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:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

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

interface IEnumerator
{
    // Gets the current element in the collection
    object Current { get; }

    // Advances the enumerator to the next element of the collection
    bool MoveNext();

    // Sets the enumerator to its initial position, which is before the first element in the collection
    void Reset();
}

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

IEnumerator enumerator = myList.GetEnumerator();
while (enumerator.MoveNext())
{
    ListElementType myElement = (ListElementType)enumerator.Current;
    // .. code that does something with myElement
    ..
}

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:

IEnumerator EverySecondChar(string name)
{
    for (int i = 0; i < name.Length; i += 2)
    {
        yield return name[i];
    }
}

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

class EverySecondCharEnumerator : IEnumerator
{
    private string _name;
    private int _index;

    public EverySecondCharEnumerator(string name)
    {
        _name = name;
        _index = -1;
    }

    public object Current
    {
        get { return _name[_index]; }
    }

    public bool MoveNext()
    {
        if (_index + 2 >= _name.Length)
            return false;
        _index += 2;
        return true;
    }

    public void Reset()
    {
        _index = -1;
    }
}

Und kann dann so verwendet werden:

EverySecondCharEnumerator e = new EverySecondCharEnumerator(name);
while (e.MoveNext())
{
    char c = (char)e.Current;
    // ...
}

Mit den Enumerator-Funktionen funktioniert es ähnlich:

IEnumerator e = EverySecondChar(name);
while (e.MoveNext())
{
    char c = (char)e.Current;
    // ...
}

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:

class MyCube : MonoBehaviour
{
    public IEnumerator MoveAsync(Vector3 target)
    {
        // movement code with yield returns
    }
}

class ScriptedMovement : MonoBehaviour
{
    public MyCube cube;

    IEnumerator Movement1(Vector3 target)
    {
        // MoveAsync will be executed on this object
        yield return StartCoroutine(cube.MoveAsync(target));
    }

    IEnumerator Movement2(Vector3 target)
    {
        // MoveAsync will be executed on the cube object
        yield return cube.StartCoroutine(cube.MoveAsync(target));
    }
}

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:

class MyCube : MonoBehaviour
{
    // Only this function can be called from the outside,
    // which returns a Coroutine-Object
    public Coroutine Move(Vector3 target)
    {
        // MoveAsync will run on this object
        return StartCoroutine(MoveAsync(target));
    }

    // The coroutine can't be called from the
    // outside because it's private
    private IEnumerator MoveAsync(Vector3 target)
    {
        // movement code with yield returns
    }
}

class ScriptedMovement : MonoBehaviour
{
    public MyCube cube;

    IEnumerator Movement(Vector3 target)
    {
        // The coroutine MoveAsync will run on the cube object because
        // Move(..) contains the StartCoroutine(MoveAsync(..)) call
        yield return cube.Move(target);
    }
}

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:

Coroutine c = StartCoroutine(...);
yield return c;

Und daher auch:

Coroutine c = cube.Move(target);
yield return c;

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:

public class EditorVisibility : MonoBehaviour
{
	// Fields decorated with the HideInInspector-attribute do
	// not show up in the editor but will be serialized
	[HideInInspector] public int invisible = 5;

	// Fields decorated with the SerializeField-attribute are
	// visible in the editor and will be serialized
	[SerializeField] private int visible = 5;

	// Properties can't be serialized
	// (You can serialize the backing field of a property, however)
	[SerializeField] public int MyProperty { get; set; } // does not work

	// Fields decorated with the NonSerialized-attribute do
	// not show up in the editor and won't be serialized
	[System.NonSerialized] public int invisibleNotSerialized = 5;
}

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.