6 Möglichkeiten, wie Ihr Team und Ihr Code von ScriptableObjects profitieren können

Wir freuen uns, Ihnen mitteilen zu können, dass wir ein neues technisches E-Book veröffentlicht haben, Modulare Spielarchitektur in Unity mit ScriptableObjects erstellenveröffentlicht haben, das Best Practices von professionellen Entwicklern für den Einsatz von ScriptableObjects in der Produktion enthält.
Zusammen mit dem E-Book können Sie ein Demoprojekt von GitHub herunterladen, das von der Mechanik klassischer Arcade-Spiele mit Ball und Paddel inspiriert ist. Die Demo zeigt, wie ScriptableObjects Ihnen dabei helfen kann, Komponenten zu erstellen, die testbar und skalierbar sind, während sie gleichzeitig designerfreundlich sind. Obwohl ein Spiel wie dieses mit weit weniger Codezeilen erstellt werden könnte, zeigt diese Demo ScriptableObjects in Aktion.

Dieser Beitrag erklärt die Vorteile von ScriptableObjects, geht aber nicht auf die Grundlagen oder das allgemeine Coding in Unity ein. Wenn Sie neu in der Programmierung in Unity sind, besuchen Sie Unity Learn, wo Sie hilfreiche Einführungs-Tutorials finden. Auch das erste Kapitel des E-Books bietet eine solide Einführung.
Sehen wir uns sechs Möglichkeiten an, wie Sie von der Verwendung von ScriptableObjects in Ihren Projekten profitieren können. Möchten Sie mehr erfahren? Alle diese Beispiele werden in dem E-Book und dem Demoprojekt näher erläutert.
Obwohl viele der hier vorgestellten Techniken auch mit C#-Klassen realisiert werden können, liegt einer der Hauptvorteile von ScriptableObjects in der Zugänglichkeit für Künstler und Designer. Sie können ScriptableObjects verwenden, um Spiellogik in einem Projekt zu konfigurieren und anzuwenden, ohne den Code bearbeiten zu müssen.
Mit dem Editor lassen sich ScriptableObjects bequem anzeigen und bearbeiten, so dass die Designer die Gameplay-Daten ohne große Unterstützung durch das Entwicklerteam einrichten können. Dies gilt auch für die Spiellogik, z. B. für die Anwendung von Verhalten auf einen NSC durch Hinzufügen eines ScriptableObjects (wie in den folgenden Mustern erläutert).
Das Speichern von Daten und Logik in einem einzigen MonoBehaviour kann zu zeitraubenden Konflikten führen, wenn zwei Personen unterschiedliche Teile derselben Prefab oder Szene ändern. Durch die Aufteilung gemeinsam genutzter Daten in kleinere Dateien und Assets mit ScriptableObjects können Designer das Gameplay parallel zu den Entwicklern erstellen, anstatt darauf warten zu müssen, dass letztere das Gameplay im Code fertigstellen, bevor sie es testen.
Es kann zu Problemen kommen, wenn Kollegen mit unterschiedlichen Rollen gleichzeitig auf den Spielcode und die Assets zugreifen. Mit ScriptableObjects kann der Programmierer steuern, welcher Teil des Projekts im Editor bearbeitet werden kann. Darüber hinaus führt die Verwendung von ScriptableObjects zur Organisation Ihres Codes natürlich zu einer Codebasis, die modularer und effizienter zu testen ist.
Christo Nobbs, ein leitender technischer Spieldesigner, der sich auf das Design von Systemspielen und Unity (C#) spezialisiert hat, hat einen Beitrag zu Das Unity-Spielentwickler-Spielbuchbeigetragen und ist der Hauptautor einer Blogpost-Serie über die Entwicklung von Spielsystemen in Unity. Seine Beiträge, "Systeme, die Ökosysteme schaffen: Emergentes Spieldesign" und "Unvorhersehbarer Spaß: Der Wert der Zufallsgenerierung im Spieldesign" bietet interessante Beispiele dafür, wie Designer ScriptableObjects verwenden können.
Modularität ist ein allgemeines Software-Prinzip, das in C# ohne die Verwendung von ScriptableObjects implementiert werden kann. Aber, wie bereits erwähnt, helfen ScriptableObjects dabei, saubere Kodierungspraktiken zu fördern, indem sie Daten von der Logik trennen, was ein erster Schritt in Richtung modularer Spielcode ist. Diese Trennung bedeutet, dass es einfacher ist, Änderungen vorzunehmen, ohne unbeabsichtigte Nebeneffekte zu verursachen, und verbessert die Testbarkeit.
ScriptableObjects eignen sich hervorragend zum Speichern statischer Daten und sind daher sehr praktisch, um statische Gameplay-Werte wie Gegenstände oder NPC-Statistiken, Charakterdialoge und vieles mehr zu konfigurieren. Da ScriptableObjects als Asset gespeichert werden, bleiben sie auch außerhalb des Spielmodus bestehen, so dass sie zum Laden in einer statischen Konfiguration verwendet werden können, die sich zur Laufzeit dynamisch ändert.
Obwohl Änderungen an ScriptableObject-Daten im Editor bestehen bleiben, ist es wichtig zu beachten, dass sie nicht zum Speichern von Spieldaten gedacht sind. In diesem Fall ist es besser, ein Serialisierungssystem wie JSON, XML oder eine binäre Lösung zu verwenden, wenn die Leistung entscheidend ist.
MonoBehaviours sind mit zusätzlichem Overhead verbunden, da sie ein GameObject - und standardmäßig einen Transform - benötigen, um als Host zu fungieren. Das bedeutet, dass Sie eine Menge ungenutzter Daten erstellen müssen, bevor Sie einen einzigen Wert speichern. Ein skriptfähiges Objekt verkleinert diesen Speicherbedarf und lässt das GameObject und Transform weg. Es speichert auch Daten auf Projektebene, was hilfreich ist, wenn Sie von mehreren Szenen aus auf dieselben Daten zugreifen müssen.
Es ist üblich, viele GameObjects zu haben, die auf doppelten Daten beruhen, die sich zur Laufzeit nicht ändern müssen. Anstatt diese doppelten lokalen Daten in jedem GameObject zu haben, können Sie sie in ein ScriptableObject einspeisen. Jedes der Objekte speichert einen Verweis auf den gemeinsamen Datenbestand, anstatt die Daten selbst zu kopieren. Dies kann bei Projekten mit Tausenden von Objekten zu erheblichen Leistungssteigerungen führen.


In der Softwareentwicklung ist dies eine Optimierung, die als Fliegengewichtsmuster bekannt ist. Wenn Sie Ihren Code auf diese Weise mit ScriptableObjects umstrukturieren, vermeiden Sie das Kopieren von Werten und reduzieren Ihren Speicherbedarf. Lesen Sie unser E-Book, Verbessern Sie Ihren Code mit Programmiermustern für Spiele um mehr über die Verwendung von Entwurfsmustern in Unity zu erfahren.
Ein gutes Beispiel dafür, wie ScriptableObjects Ihren Code vereinfachen können, ist ihre Verwendung als Enum für Vergleichsoperationen. Das ScriptableObject kann eine Kategorie oder einen Gegenstandstyp repräsentieren, z. B. einen speziellen Schadenseffekt - Kälte, Hitze, Elektrizität, Magie usw.
Wenn Ihre Anwendung ein Inventarsystem zum Ausrüsten von Spielgegenständen benötigt, können ScriptableObjects Gegenstandstypen oder Waffenslots darstellen. Die Felder im Inspektor dienen dann als Drag-and-Drop-Schnittstelle für die Einrichtung.

Die Verwendung von ScriptableObjects als Enums wird interessanter, wenn man sie erweitern und weitere Daten hinzufügen möchte. Im Gegensatz zu normalen Enums können ScriptableObjects zusätzliche Felder und Methoden haben. Es besteht keine Notwendigkeit, eine separate Nachschlagetabelle zu haben oder mit einer neuen Datenreihe zu korrelieren.
Während herkömmliche Enums einen festen Satz von Werten haben, können ScriptableObject-Enums zur Laufzeit erstellt und geändert werden, so dass Sie bei Bedarf Werte hinzufügen oder entfernen können.
Wenn Sie eine lange Liste von Aufzählungswerten ohne explizite Nummerierung haben, kann das Einfügen oder Entfernen einer Aufzählung deren Reihenfolge ändern. Diese Neuordnung kann zu subtilen Fehlern oder unbeabsichtigtem Verhalten führen. ScriptableObject-basierte Enums haben diese Probleme nicht. Sie können Ihr Projekt löschen oder ergänzen, ohne jedes Mal den Code ändern zu müssen.
Angenommen, Sie wollen einen Gegenstand in einem RPG ausrüstbar machen. Sie könnten ein zusätzliches boolesches Feld an das ScriptableObject anhängen, um dies zu tun. Dürfen bestimmte Charaktere bestimmte Gegenstände nicht besitzen? Sind manche Gegenstände magisch oder haben sie besondere Fähigkeiten? ScriptableObject-basierte Enums können das tun.
Da Sie Methoden für ein ScriptableObject erstellen können, sind sie für die Aufnahme von Logik oder Aktionen ebenso nützlich wie für die Speicherung von Daten. Wenn Sie die Logik aus Ihrem MonoBehaviour in ein ScriptableObject verschieben, können Sie letzteres als Delegatenobjekt verwenden, wodurch das Verhalten modularer wird.
Wenn Sie bestimmte Aufgaben ausführen müssen, können Sie deren Algorithmen in eigene Objekte kapseln. In der ursprünglichen Gang of Four wird dieser allgemeine Aufbau als Strategiemuster bezeichnet. Das folgende Beispiel zeigt, wie man das Strategiemuster durch die Verwendung einer abstrakten Klasse zur Implementierung von EnemyAI nützlicher machen kann. Das Ergebnis sind mehrere abgeleitete ScriptableObjects mit unterschiedlichem Verhalten, das dann zu einem steckbaren Verhalten wird, da jedes Asset austauschbar ist. Ziehen Sie einfach das ScriptableObject Ihrer Wahl in das MonoBehaviour und legen Sie es dort ab.

Ein detailliertes Beispiel für die Verwendung von ScriptableObjects zur Steuerung des Verhaltens finden Sie in der Videoserie Pluggable AI with ScriptableObjects. Diese Sitzungen demonstrieren ein auf endlichen Zustandsmaschinen basierendes KI-System, das mit Hilfe von ScriptableObjects für Zustände, Aktionen und Übergänge zwischen diesen Zuständen konfiguriert werden kann.
Eine häufige Herausforderung in größeren Projekten besteht darin, dass mehrere GameObjects Daten oder Zustände gemeinsam nutzen müssen, wobei direkte Verweise zwischen diesen Objekten zu vermeiden sind. Die Verwaltung dieser Abhängigkeiten in großem Umfang kann einen erheblichen Aufwand erfordern und ist oft eine Quelle von Fehlern. Viele Entwickler verwenden Singletons - eine globale Instanz einer Klasse, die das Laden der Szene überlebt. Singletons führen jedoch globale Zustände ein und erschweren Unit-Tests. Wenn Sie mit einem Prefab arbeiten, das auf ein Singleton verweist, müssen Sie am Ende alle seine Abhängigkeiten importieren, nur um eine isolierte Funktion zu testen. Dadurch wird Ihr Code weniger modular und weniger effizient zu debuggen.
Eine Lösung ist die Verwendung von ScriptableObject-basierten Ereignissen, um Ihre GameObjects bei der Kommunikation zu unterstützen. In diesem Fall verwenden Sie ScriptableObjects, um eine Form des Beobachterentwurfsmusters zu implementieren, bei dem ein Subjekt eine Nachricht an einen oder mehrere lose entkoppelte Beobachter sendet. Jedes beobachtende Objekt kann unabhängig vom Subjekt reagieren, ist sich aber der anderen Beobachter nicht bewusst. Das Subjekt kann auch als "Herausgeber" oder "Sender" und die Beobachter als "Abonnenten" oder "Zuhörer" bezeichnet werden.
Sie können das Beobachtermuster mit MonoBehaviours oder C#-Objekten implementieren. Während dies in der Unity-Entwicklung bereits gängige Praxis ist, bedeutet ein reiner Skriptansatz, dass Ihre Designer für jedes während des Spiels benötigte Ereignis auf das Programmierteam angewiesen sind.

Auf den ersten Blick scheint es, als hätten Sie dem Beobachtermuster eine zusätzliche Ebene hinzugefügt, aber diese Struktur bietet einige Vorteile. Da es sich bei ScriptableObjects um Assets handelt, sind sie für alle Objekte in Ihrer Hierarchie zugänglich und verschwinden nicht beim Laden der Szene.
Der einfache, dauerhafte Zugriff auf bestimmte Ressourcen ist der Grund, warum viele Entwickler Singletons verwenden. ScriptableObjects können oft die gleichen Vorteile bieten, ohne so viele unnötige Abhängigkeiten zu schaffen.
Bei Ereignissen, die auf ScriptableObjects basieren, kann jedes Objekt als Herausgeber (der das Ereignis sendet) und jedes Objekt als Teilnehmer (der auf das Ereignis wartet) dienen. Das ScriptableObject sitzt in der Mitte und hilft bei der Weiterleitung des Signals, indem es wie ein zentraler Vermittler zwischen den beiden agiert.
Man kann sich dies als "Ereigniskanal" vorstellen. Stellen Sie sich das ScriptableObject als einen Funkturm vor, dessen Signale von einer beliebigen Anzahl von Objekten abgehört werden. Ein interessiertes MonoBehaviour kann den Ereigniskanal abonnieren und reagieren, wenn etwas passiert.
Die Demo zeigt, wie das Beobachtermuster bei der Einrichtung von Spielereignissen für UI, Sounds und Punktevergabe hilft.
Zur Laufzeit müssen Sie oft eine Liste von GameObjects oder Komponenten in Ihrer Szene verfolgen. Auf eine Feindesliste muss man zum Beispiel häufig zugreifen, aber es ist auch eine dynamische Liste, die sich ändert, wenn mehr Feinde auftauchen oder besiegt werden. Das Singleton bietet einfachen globalen Zugriff, hat aber mehrere Nachteile. Anstatt ein Singleton zu verwenden, können Sie Daten in einem ScriptableObject als "Runtime Set" speichern. Die ScriptableObject-Instanz erscheint auf der Projektebene, was bedeutet, dass sie Daten speichern kann, die für jedes Objekt in jeder Szene verfügbar sind, und somit einen ähnlichen globalen Zugriff bietet. Da sich die Daten auf einem Asset befinden, ist die öffentliche Liste der Elemente jederzeit zugänglich.
In diesem Anwendungsfall erhalten Sie einen spezialisierten Datencontainer, der eine öffentliche Sammlung von Elementen verwaltet, aber auch grundlegende Methoden zum Hinzufügen und Entfernen aus der Sammlung bietet. Dies kann den Bedarf an Singletons verringern und die Testbarkeit und Modularität verbessern.

Das direkte Lesen von Daten aus einem ScriptableObject ist auch optimaler als das Durchsuchen der Szenenhierarchie mit einer Suchoperation wie Object.FindObjectOfType oder GameObject.FindWithTag. Abhängig von Ihrem Anwendungsfall und der Größe Ihrer Hierarchie sind dies relativ teure Methoden, die für Aktualisierungen pro Frame ineffizient sein können.
Es gibt mehrere ScriptableObjects-Frameworks, die mehr Anwendungsfälle als diese sechs Szenarien bieten. Einige Teams entscheiden sich für eine umfassende Verwendung von ScriptableObjects, während andere ihre Verwendung auf das Laden statischer Daten und die Trennung von Logik und Daten beschränken. Letztendlich hängt es von den Bedürfnissen Ihres Projekts ab, wie Sie sie einsetzen.
Erstellen einer modularen Spielarchitektur in Unity mit ScriptableObjects ist das dritte Handbuch in unserer Serie für fortgeschrittene Unity-Programmierer. Jeder Leitfaden, der von erfahrenen Programmierern verfasst wurde, bietet Best Practices für Themen, die für Entwicklungsteams wichtig sind.
Erstellen Sie einen C# Style Guide: Schreiben Sie sauberen und skalierbaren Code. unterstützt Sie bei der Entwicklung eines Styleguides, der Ihnen hilft, eine einheitliche Herangehensweise an die Erstellung einer kohärenten Codebasis zu finden.
Verbessern Sie Ihren Code mit Programmiermustern für Spielehebt Best Practices für die Verwendung der SOLID-Prinzipien und gängiger Programmiermuster hervor, um eine skalierbare Spielcode-Architektur in Ihrem Unity-Projekt zu erstellen.
Wir haben diese Reihe ins Leben gerufen, um unseren erfahrenen Schöpfern umsetzbare Tipps und Inspirationen zu geben, aber sie sind keine Regelbücher. Es gibt viele Möglichkeiten, Ihr Unity-Projekt zu strukturieren, und was für die eine Anwendung wie selbstverständlich erscheint, ist es für eine andere vielleicht nicht. Bewerten Sie die Vor- und Nachteile jeder Empfehlung, jedes Tipps und jedes Musters gemeinsam mit Ihren Kollegen, bevor Sie sie anwenden.
Weitere Anleitungen und Artikel für Fortgeschrittene finden Sie im Unity Best Practices Hub.
