Unite Europe 2016: Überprüfung des Berichts „Sturz der MonoBehavior-Tyrannei in einer glorreichen ScriptableObject-Revolution“



    Wir waren kürzlich in Amsterdam auf der Konferenz Unite Europa im Jahr 2016 , wo sie viele Emotionen und interessanten Erfahrung erhalten. Auf dieser Konferenz gab es viele faszinierende Berichte in verschiedene Richtungen und mit unterschiedlichen Komplexitätsgraden. Das Thema einer der Reden lautete „Sturz der MonoBehavior-Tyrannei in einer glorreichen ScriptableObject-Revolution“, über die Richard Fine ( https://twitter.com/superpig / https://github.com/richard-fine ), ein Spezialist von Unity Technologies, Er sprach ausführlich über ScriptableObject und zeigte anhand von Beispielen, wie es in einem Projekt angewendet werden kann.

    In seinem Bericht ging Richard auf folgende Punkte ein:


    Als nächstes folgt eine kostenlose Übersetzung / Nacherzählung dessen, worüber Richard sprach, mit verschiedenen Ergänzungen.

    Tyrannei MB


    MB Allgemeine Informationen:

    • Die meisten Skripte sind als MB geschrieben.
    • Sie sind an GameObject angehängt (im Folgenden als GO bezeichnet).
    • Lebe in Szenen oder Fertighäusern.
    • Sie erhalten einige Rückrufe von der Engine (wie Start, Update usw.).

    Was sind die Nachteile?

    • Beim Verlassen des Playmods zurücksetzen.
    • Bei der Instantiierung wird eine vollständige Kopie erstellt.
    • Instanzen sind unpraktisch, um zwischen Szenen zu "fummeln".
    • Instanzen sind unpraktisch, um zwischen Projekten zu "fummeln".
    • Schlechte VCS-Granulation (beim Ändern eines Skripts auf einem Szenenobjekt ändert sich die gesamte Szene, es treten häufig Konflikte auf usw.).
    • Kann inkonsistent angepasst werden. Wenn sich in der Szene mehrere identische Objekte befinden, besteht die Möglichkeit, dass versehentlich die Eigenschaften eines dieser Objekte geändert werden. Dies ist nicht immer leicht zu finden und vom Standpunkt des Spieldesigns aus nicht ganz korrekt. Wenn alle Objekte identisch sind, müssen die Eigenschaften identisch sein. Es gibt zwar Ausnahmen, wenn dieses Verhalten zweckmäßig ist, aber dies sind seltenere Fälle.
    • Konzeptionell nicht ganz angemessen: Sehr oft muss mit sauberen Daten mit der Möglichkeit einer systemeigenen und automatischen Serialisierung im Inspektor gearbeitet werden, und nicht mit einer Komponente / einem Objekt mit einer bestimmten Position im Raum usw.)

    Wie kannst du der Tyrannei von MB entkommen?


    Reine statische C # -Klassen?

    • Wird beim Beenden des Playmods immer noch zurückgesetzt.
    • Sie müssen sie manuell serialisieren.
    • Innerhalb solcher Klassen ist es unpraktisch, Unity-Objekte zu bearbeiten.
    • Ich muss meinen eigenen Inspektor schreiben.

    Schließlich verwenden wir speziell den Motor, der all dies „out of the box“ bietet, um Unannehmlichkeiten zu vermeiden!

    Was ist mit Fertigteilen ?
    Sie lösen das Problem der Speicherung und Übertragung von Szene zu Szene und zwischen Projekten und verletzen nicht die VCS-Granulation. Diese Lösung hat aber auch Nachteile:

    • Sie können einfach alles ruinieren, indem Sie beispielsweise versehentlich eine Instanz erstellen oder in die Szene ziehen.
    • Es kann zusätzliche Komponenten geben (zum Beispiel AudioSource), aber warum werden sie benötigt, weil die Daten von solchen Dingen getrennt werden sollten!
    • Konzeptionell ist es immer noch keine ideale Lösung, vielleicht eine akzeptable, aber ...

    SO kommt hier zur Rettung


    SO ist eine Klasse, mit der Sie eine große Menge an Informationen speichern können, die unabhängig von Skriptbeispielen übertragen werden. Sie können von dieser Klasse erben, wenn Sie Objekte erstellen müssen, die nicht an GO angehängt werden.

    Stellen Sie sich vor, es gibt ein Prefab mit einem Skript, das ein Array mit einer Million Ganzzahlen enthält. Das Array belegt 4 Megabyte Speicher und gehört zum Fertighaus. Jedes Mal, wenn eine Instanz dieses Fertighauses erstellt wird, wird auch eine Instanz dieses Arrays erstellt. Wenn Sie 10 Spielobjekte erstellen, beträgt die Größe des von Arrays für diese 10 Instanzen belegten Speichers 40 Megabyte.

    Bei Verwendung von SO ist das Ergebnis völlig anders. Unity serialisiert alle Arten von Grundelementen, Zeichenfolgen, Arrays, Listen und bestimmten Typen, z. B. Vector3 und benutzerdefinierte Klassen, mit dem Attribut Serializable als Kopien, die sich auf das Objekt beziehen, in dem sie definiert sind. Dies bedeutet, dass beim Erstellen einer Instanz der SO-Klasse mit einem darin deklarierten Array von einer Million Ganzzahlen dieses Array zusammen mit dem Sample übertragen wird. In diesem Fall glauben sie, dass sie unterschiedliche Daten haben. SO-Felder oder UnityEngine.Object-Felder wie MonoBehaviour, Mesh, GameObject usw. werden im Gegensatz zu Werten in Links gespeichert. Wenn ein Skript auf SO verweist, das eine Million Ganzzahlen enthält, speichert Unity nur den Verweis auf SO in den Skriptdaten. SO speichert wiederum das Array. 10 vorgefertigte Instanzen, die auf die SO-Klasse verweisen, Bei Verwendung von 4 Megabyte Speicher würden 4 Megabyte anstelle von 40 Megabyte belegt, wie oben erwähnt. Dies ist besonders wichtig, wenn es um eine große Anzahl von Objekten und / oder große Datenmengen in Skripten geht.

    Also so:

    • Es ist wie ein MB, aber keine Komponente (Sie sollten benutzerdefinierte Klassen von SO erben, nicht von MB).
    • Kann nicht an GO / Prefabs angehängt werden.
    • Es kann wie MB im Inspector serialisiert und überprüft werden.
    • Es kann in einer .asset-Datei abgelegt werden (Sie können benutzerdefinierte Elemente erstellen, indem Sie Ihre Texturen / Materialien usw. platzieren).
    • Löst einige Probleme des Polymorphismus. Wenn Sie sich eingehender mit dem Serialisierungscode in Unity befassen, stellt sich heraus, dass beim Erben nicht alle Dinge korrekt serialisiert werden können. Im Fall von SO gibt es keine derartigen Probleme.

    Wie SO uns vor Problemen bewahrt:

    • Die Speicherung in Assets verhindert das Zurücksetzen beim Verlassen des Playmodes.
    • Sie kann referenziert und während der Instanziierung nicht kopiert werden (der Speicherverbrauch nimmt ab, die Instanziierungsgeschwindigkeit steigt).
    • Wie jedes andere Asset kann es zwischen Szenen verwendet und "durchsucht" werden.
    • Es ist einfacher, zwischen Projekten zu „fummeln“: Übertragen Sie einfach eine Asset-Datei, und Sie müssen nicht in die gesamte Hierarchie eintauchen und nach Abhängigkeiten suchen.
    • Ideale VCS-Granulation (eine Datei - ein Objekt).
    • Keine zusätzlichen unnötigen Teile (z. B. Transformieren).

    Aber SO ist auch nicht perfekt:

    • Nur drei Rückrufe: OnEnable / OnDisable, OnDestroy. Obwohl dieses Problem mit MB als Proxy gelöst werden kann.
    • Die allgemeinen Daten sind in der Tat nicht ganz allgemein. Sie können nicht für eine bestimmte Entität geändert werden, die ihre eigenen Vor- und Nachteile hat. Dies hängt vom ausgewählten Ablauf ab.

    So verwenden Sie SO


    Die SO-Klasse muss verwendet werden, wenn der Speicherverbrauch reduziert werden soll, indem das Kopieren von Werten vermieden wird. Es kann aber auch verwendet werden, um zu bestimmen, welche Datensätze einbezogen werden sollen. Es eignet sich hervorragend für Konfigurationsdateien, zum Beispiel für Level-Einstellungen, globale Spieleinstellungen oder individuelle Einstellungen für Charaktere / Feinde / Gebäude usw. (zum Beispiel können Sie maximale Gesundheit, Schaden und andere Parameter speichern). SO ist auch nützlich, um benutzerdefinierte Tools, Level-Editoren usw. zu schreiben.

    Aus SO-Instanzen können Sie schnell und bequem benutzerdefinierte Assets erstellen, diese wiederverwenden, über das Netzwerk laden usw. Wenn Sie einen Vererbenden der SO-Klasse deklarieren, können Sie ihn mit dem CreateAssetMenu- Attribut kennzeichnenHiermit wird ein Element zur Erstellung eines Assets für dieses Objekt zum Kontextmenü "Assets / Erstellen" hinzugefügt.

    Ein Beispiel für ein einfaches Skript mit Einstellungen:

    using UnityEngine;
    [CreateAssetMenu(fileName="EnemyData", menuName="Prefs/Characters/Enemy", order=1)]
    public class EnemyPrefs : ScriptableObject
    {
        public string objectName = "Enemy";
        [Range(10f, 100f)]
        public float maxHP = 50f;
        [Range(1f, 10f)]
        public float maxDamage = 5f;
        public Vector3[] spawnPoints;
    }
    

    Erstellen eines Assets:



    Serialisieren eines Assets mit Daten im Inspektor:



    Wenn Sie im Inspektor mit SO-Instanzen arbeiten, können Sie auf das Verknüpfungsfeld doppelklicken, um den Inspektor für Ihren SO zu öffnen. Es ist auch möglich, einen benutzerdefinierten Editor zu erstellen, um den Typ des Inspektors für Ihren Typ zu bestimmen und die von ihm dargestellten Daten zu verwalten.

    Eine SO-Instanz kann ohne Verweis auf die .asset-Datei programmgesteuert mit SсriptableObject.CreateInstance <> erstellt werden .

    SO Lebenszeit:

    • Das Gleiche wie bei jedem anderen Vermögenswert.
    • Wenn es persistent ist (an .asset-Datei, AssetBundle usw. angehängt):
      • SO kann von GC über Resources.UnloadUnusedAssets entladen werden .
      • bleibt im Speicher erhalten, wenn in anderen Skripten Verknüpfungen vorhanden sind,
      • kann bei Bedarf nachgeladen werden,
    • Wenn es nicht persistent ist (erstellt mit CreateInstance <> und keiner .asset-Datei zugeordnet):

    Gebrauchsmuster


    Die meisten Entwickler betrachten SO als Datencontainer, aber in Wirklichkeit ist es etwas mehr. Betrachten wir einige Muster seiner Anwendung.

    Wie Datenobjekte und Tabellen:

    • POD-Klasse (Plain-Old-Data), die an eine .asset-Datei gebunden ist.
    • Sie können im Inspektor bearbeiten, in VCS als einzelne Datei festschreiben. Zum Beispiel kann ein Spieledesigner einige Einstellungen oder andere Daten ändern, ohne Szenen oder Prefabs zu beeinflussen, was sehr praktisch ist.
    • Mit einem benutzerdefinierten Editor können Sie die Verwendung und die Einstellungen im Inspector noch komfortabler gestalten.
    • Bei der Verwendung von SO lohnt es sich, einen Ansatz zu wählen: Wenden Sie ein Objekt pro Entität oder eines pro Tabelle / Menge von Entitäten an. Wenn Sie beispielsweise eine Lokalisierungstabelle oder eine Konfigurationsdatei erstellen, reicht ein gemeinsames Objekt aus, und wenn Sie Vorlagen für verschiedene Einheiten erstellen müssen, benötigt jedes ein eigenes Objekt.
    • Anwendungsbeispiele: Lokalisierungstabellen, Inventargegenstände, Einheitenvorlagen, Ebenenkonfigurationen usw. Keines der oben genannten Elemente erfordert eine Position im Raum oder eine Art Logik. Hierbei handelt es sich nur um Daten, die geändert werden können, ohne die Szenen und Fertighäuser zu beeinträchtigen, wodurch die Arbeit anderer Teammitglieder nicht beeinträchtigt wird.

    Ein Beispiel:

    class EnemyInfo : ScriptableObject 
    {
       public int MaximumHealth;
       public int DamagePerMeleeHit;
    }
    class Enemy : MonoBehaviour 
    {
       public EnemyInfo info;
    }
    

    Als erweiterbare Listings:

    • In Form eines leeren SO, gebunden an eine .asset-Datei.
    • Sie können nur verwendet werden, um die Gleichheit mit anderen Objekten zu prüfen oder um auf Null zu prüfen.
    • Wie Enums, aber anstatt Code zu schreiben, können sie von Designern direkt im Editor erstellt werden.
    • Anwendungsbeispiele: Inventargegenstände, Ereigniskategorien, Schadensarten, Einheitentypen usw.
    • Bei Bedarf können sie einfach zu Daten- / Tabellenobjekten erweitert werden, indem die erforderlichen Eigenschaften hinzugefügt werden.

    Ein Beispiel:

    class AmmoType : ScriptableObject  { }
    …
    if (inventory[weapon.ammoType] == 0)
    {
        PlayOutOfAmmoSound();
        return;
    }
    …
    inventory[weapon.ammoType] -= 1;
    ...
    

    Duale Serialisierung

    Wie bereits erwähnt, besteht einer der Vorteile von SO in der vollständigen Kompatibilität mit dem Unity-Serialisierungssystem. Dabei:

    • SO kann mit JsonUtility interagieren .
    • Infolgedessen können Sie eine Mischung aus Assets erhalten, die in der Entwurfsphase mit dem Unity-Serializer erstellt wurden, und Assets, die danach programmgesteuert mit JsonUtility erstellt wurden.
    • Anwendungsbeispiele: Integrierte Ebenen (Design, vom Unity-Serializer gespeichert) + Benutzerebenen (zur Laufzeit erstellt und mit JsonUtility gespeichert). Aus Sicht der Architektur müssen Sie sich keine Gedanken darüber machen, warum solche Ebenen erstellt wurden. Sie können als SO in den Speicher geladen werden und universell mit ihnen arbeiten.

      Ein Beispiel:

      [CreateAssetMenu]
      class LevelData : ScriptableObject { /*...*/ }

    • Sie können direkt im Editor über das Menü erstellen, die Werte anpassen usw.

      LevelData LoadLevelFromFile(string path)
      {
         string json = File.ReadAllText(path);
         LevelData result = CreateInstance<>(LevelData);
         JsonUtility.FromJsonOverwrite(result, json);
         return result;
      }
      

    • Mit dieser Methode können Sie eine Textdatei mit JSON lesen (die lokal beim Erstellen der Ebene erstellt wurde oder beispielsweise vom Server stammt) und auf SO destillieren

    Wie Singletones:

    • SO + statische Instanzvariable (eine SO-Instanz wird erstellt und eine Verknüpfung dazu in einer statischen Variablen gespeichert).
    • FindObjectWithType, um eine Instanz nach einem Neuladen der Ebene wiederherzustellen.
    • Anwendungsbeispiele: Globale Spielzustände.

    Ein Beispiel:

    class GameState: ScriptableObject 
    {
       public int lives, score;
       static GameState _instance;
       public GameState Instance
       {
           get
           {
               if (!_instance) _instance = FindObjectOfType();
               if (!_instance) _instance = CreateDefaultGameState();
               return _instance;
           }
       }
    }
    

    Als Delegierte:

    • SO, die Methoden enthalten.
    • MB übergibt den Verweis in den SO-Methoden an sich selbst, SO erledigt die Arbeit und verwendet MB, falls erforderlich.
    • Dadurch ist es möglich, eingebettetes und benutzerdefiniertes Verhalten zu implementieren.
    • Anwendungsbeispiele: KI-Typen, verschiedene Power-Ups, Buffs / Debuffs.

    Ein Beispiel:

    abstaract class PowerupEffect : ScriptableObject
    {
       public abstract void ApplyTo(GameObject go);
    }
    class HealthBooster : PowerupEffect
    {
       public int Amount;
       public override void ApplyTo(GameObject go)
       {
           go.GetComponent().currentValue += Amount;
       }
    }
    class Powerup : MonoBehaviour
    {
       public PowerupEffect effect;
       public void OnTriggerEnter(Collider other)
       {
           effect.ApplyTo(other.gameObject);
       }
    }
    

    Daher delegiert Powerup MB seine Arbeit an PowerupEffect SO, das das Muster ist.

    Fazit


    Zusammenfassend lässt sich feststellen, dass SO Vor- und Nachteile hat, von denen einige im Folgenden aufgeführt sind:

    Vorteile:

    • Es ist perfekt „out of the box“ serialisiert, sowohl mit JsonUtility als auch visuell im Editor (im Gegensatz zu beispielsweise statischen Klassen).
    • Änderungen werden gespeichert und nach dem Beenden des Playmods nicht verworfen.
    • Sie können bequem zwischen anderen Skripten wechseln.
    • Kann leicht vererbt werden.
    • Es wird nicht in den Inspektor gelegt, verschmutzt ihn also nicht.
    • Es hat keinen Overhead und ist leichter als MB.
    • Es wird nicht an GO angehängt und nicht gelöscht, wenn das Objekt zerstört wird.
    • Es kann in einer Szene gespeichert werden, bis mindestens ein Objekt in dieser Szene darauf verweist.
    • Es kann in Assets gespeichert und in anderen Szenen mithilfe von AssetDatabase, zur Laufzeit in bereits implementierten Builds usw. wiederverwendet werden.

    Nachteile:

    • Es wird nicht in der Szene angezeigt - es ist visuell unklar, wie viele SO-Instanzen derzeit verwendet werden, was das Debuggen erschweren kann.
    • Es besteht die Möglichkeit, es implizit zu zerstören, wenn Sie das letzte Objekt zerstören, das darauf verweist (in der Szene).
    • Nicht so bequem in Fertigteilen zu verwenden.
    • Es ist nicht klar, wem das Objekt gehört, wann es zerstört werden soll und in welchem ​​Sichtbereich es sich befindet.
    • Da SO nicht an GO angehängt ist, können über GetComponents keine Links schnell abgerufen werden.

    SO ist ein sehr wichtiges Tool, das MB zwar nicht vollständig ersetzen kann, jedoch in Kombination verwendet werden kann, wodurch die Tyrannei beseitigt wird. SO sollte nicht für alles verwendet werden, aber es gibt Ihnen mehr Flexibilität. Viel mehr als diejenigen, die mit SO oberflächlich vertraut sind oder überhaupt nicht denken. Es bietet auch die Möglichkeit, einen hervorragenden Workflow für Ihr Projekt zu erstellen, praktische und nützliche Tools und Vorlagen für Designer usw. zu erstellen.

    Richard sagte zu Beginn seiner Rede: „MonoBehavior sucks“ (MonoBehavior sucks :)). Es ist schwierig, dem vollständig zuzustimmen, da man auf die eine oder andere Weise kaum darauf verzichten kann (MB). Das Wichtigste ist jedoch, zu verstehen, dass Sie es nicht immer und überall verwenden sollten und dass es verschiedene Alternativen gibt, von denen eine ein leistungsstarkes, flexibles und praktisches ScriptableObject ist. Diese oder andere Mittel müssen je nach Aufgabe unter bestimmten Bedingungen richtig ausgewählt werden.

    Jetzt auch beliebt: