
Drei Wege, wie Sie Ihr Spiel mit ScriptableObjects entwerfen
Was Sie von dieser Seite erhalten: Tipps, wie Sie Ihren Spielcode einfach ändern und debuggen können, indem Sie ihn mit Scriptable Objects architektonisch gestalten.
Diese Tipps stammen von Ryan Hipple, Principal Engineer bei Schell Games, der über umfangreiche Erfahrungen mit der Verwendung von Scriptable Objects zur Architektur von Spielen verfügt. Sie können Ryans Unite-Vortrag über Scriptable Objects hier<4> ansehen; wir empfehlen auch, die Sitzung des Unity-Ingenieurs Richard Fine für eine großartige Einführung in Scriptable Objects<5> zu sehen.
Was sind ScriptableObjects?
ScriptableObject ist eine serialisierbare Unity-Klasse, die es Ihnen ermöglicht, große Mengen an gemeinsamen Daten unabhängig von Skriptinstanzen zu speichern. Die Verwendung von ScriptableObjects erleichtert die Verwaltung von Änderungen und des Debuggings. Sie können ein gewisses Maß an flexibler Kommunikation zwischen den verschiedenen Systemen in Ihrem Spiel<1> einrichten, sodass es einfacher ist, sie während der Produktion zu ändern und anzupassen sowie Komponenten wiederzuverwenden.
Drei Säulen der Spieleentwicklung
Verwenden Sie ein modulares Design:
- Vermeiden Sie es, Systeme zu erstellen, die direkt voneinander abhängig sind. Zum Beispiel sollte ein Inventarsystem in der Lage sein, mit anderen Systemen in Ihrem Spiel zu kommunizieren, aber Sie möchten keine harte Referenz zwischen ihnen erstellen, da dies die Wiederzusammenstellung von Systemen in verschiedenen Konfigurationen und Beziehungen erschwert.
- Erstellen Sie Szenen als leere Tafeln: Vermeiden Sie es, dass vorübergehende Daten zwischen Ihren Szenen existieren. Für jede neue Szene sollte es einen klaren Bruch und einen Ladevorgang geben. Dies ermöglicht es Ihnen, Szenen zu haben, die ein einzigartiges Verhalten aufweisen, das in anderen Szenen nicht vorhanden war, ohne einen Hack durchführen zu müssen.
- Richten Sie Prefabs so ein, dass sie eigenständig funktionieren. Soweit möglich, sollte jedes Prefab, das Sie in einer Szene platzieren, die eigene Funktionalität beinhalten. Dies ist für größere Teams hinsichtlich der Versionskontrolle äußerst hilfreich, wobei Szenen eine Liste von Prefabs sind und die Prefabs die einzelne Funktionalität enthalten. Auf diese Weise befinden sich die meisten Ihrer Check-ins auf der Prefab-Ebene, was zu weniger Konflikten in der Szene führt.
- Konzentrieren Sie jede Komponente darauf, ein einzelnes Problem zu lösen. Dies erleichtert es, mehrere Komponenten zusammenzufügen, um etwas Neues zu erstellen.
Gestalten Sie es einfach, Teile zu ändern und zu bearbeiten:
- Machen Sie so viel von Ihrem Spiel datengesteuert wie möglich. Wenn Sie Ihre Spielsysteme so gestalten, dass sie wie Maschinen funktionieren, die Daten als Anweisungen verarbeiten, können Sie Änderungen am Spiel effizient vornehmen, selbst während es läuft.
- Wenn Ihre Systeme so modular und komponentenbasiert wie möglich eingerichtet sind, wird es einfacher, sie zu bearbeiten, auch für Ihre Künstler und Designer. Wenn Designer in der Lage sind, Dinge im Spiel zusammenzufügen, ohne nach einer expliziten Funktion fragen zu müssen – hauptsächlich dank der Implementierung winziger Komponenten, die jeweils nur eine Sache tun – können sie solche Komponenten potenziell auf verschiedene Weise kombinieren, um neues Gameplay/Mechaniken zu finden. Ryan sagt, dass einige der coolsten Funktionen, an denen sein Team in ihren Spielen gearbeitet hat, aus diesem Prozess stammen, was er "emergentes Design" nennt.
- Es ist entscheidend, dass Ihr Team zur Laufzeit Änderungen am Spiel vornehmen kann. Je mehr Sie Ihr Spiel zur Laufzeit ändern können, desto mehr können Sie Balance und Werte finden, und wenn Sie in der Lage sind, Ihren Laufzeitstatus wie Scriptable Objects zurückzuspeichern, sind Sie in einer großartigen Position.
Erleichtern Sie das Debuggen:
Dies ist wirklich ein Unterpfeiler der ersten beiden. Je modularer Ihr Spiel aufgebaut ist, desto einfacher ist es, einzelne Bereiche davon zu testen. Je editierbarer Ihr Spiel ist, das heißt, je mehr Funktionen darin über eine eigene Inspector-Ansicht verfügen, desto einfacher ist das Debugging. Stellen Sie sicher, dass Sie den Debug-Zustand im Inspector anzeigen können, und betrachten Sie eine Funktion niemals als abgeschlossen, bis Sie einen Plan haben, wie Sie sie debuggen werden.
Für Variablen entwickeln
Eine der einfachsten Dinge, die Sie mit ScriptableObjects erstellen können, ist eine eigenständige, assetbasierte Variable. Unten ist ein Beispiel für eine FloatVariable, aber dies erweitert sich auch auf jeden anderen serialisierbaren Typ.
Jeder in Ihrem Team, egal wie technisch, kann eine neue Spielvariable definieren, indem er ein neues FloatVariable-Asset erstellt. Jedes MonoBehaviour oder ScriptableObject kann eine öffentliche FloatVariable anstelle eines öffentlichen Floats verwenden, um auf diesen neuen gemeinsamen Wert zuzugreifen.
Noch besser, wenn ein MonoBehaviour den Wert einer FloatVariable ändert, können andere MonoBehaviours diese Änderung sehen. Dies schafft eine Messaging-Schicht zwischen Systemen, die keine Referenzen zueinander benötigen.

Beispiel: Gesundheitspunkte des Spielers
Ein Beispielanwendungsfall dafür sind die Lebenspunkte (HP) eines Spielers. In Spielen mit einem einzelnen lokalen Spieler können die HP des Spielers eine FloatVariable namens „PlayerHP“ sein. Wenn der Spieler Schaden nimmt, wird er von PlayerHP abgezogen, und wenn der Spieler heilt, wird er zu PlayerHP hinzugefügt.
Stellen Sie sich jetzt ein Gesundheitsbalken-Prefab in der Szene vor. Die Gesundheitsleiste überwacht die PlayerHP-Variable, um ihre Anzeige zu aktualisieren. Ohne eine Codeänderung könnte sie einfach auf etwas anderes hinweisen, beispielsweise eine PlayerMP-Variable. Der Gesundheitsbalken weiß nichts über den Spieler in der Szene, er liest nur von derselben Variable, die der Spieler schreibt.
Sobald wir so eingerichtet sind, ist es einfach, weitere Dinge hinzuzufügen, um PlayerHP zu beobachten. Das Musiksystem kann sich ändern, wenn der PlayerHP niedrig ist, Feinde können ihre Angriffsmuster ändern, wenn sie wissen, dass der Spieler schwach ist, Bildschirmraum-Effekte können die Gefahr des nächsten Angriffs betonen und so weiter. Wichtig hierbei ist, dass das Player-Skript keine Nachrichten an diese Systeme sendet, und diese Systeme nichts über das Spieler-GameObject wissen müssen. Sie können auch im Inspector, während das Spiel läuft, den Wert von PlayerHP ändern, um Dinge zu testen.
Beim Bearbeiten des Wertes einer FloatVariable kann es eine gute Idee sein, Ihre Daten in einen Laufzeitwert zu kopieren, um den auf der Festplatte für das ScriptableObject gespeicherten Wert nicht zu ändern. Wenn Sie dies tun, sollten MonoBehaviours auf RuntimeValue zugreifen, um zu verhindern, dass der InitialValue bearbeitet wird, der auf der Festplatte gespeichert ist.
Für Ereignisse entwickeln
Eine von Ryans Lieblingsfunktionen, die auf ScriptableObjects aufbaut, ist ein Ereignissystem. Event-Architekturen helfen dabei, Ihren Code zu modularisieren, indem sie Nachrichten zwischen Systemen versenden, die nicht in direkter Verbindung zueinander stehen. Sie ermöglichen es, auf eine Zustandsänderung zu reagieren, ohne sie ständig in einer Update-Schleife zu überwachen.
Die folgenden Codebeispiele stammen aus einem Ereignissystem, das aus zwei Teilen besteht: einem GameEvent ScriptableObject und einem GameEventListener MonoBehaviour. Designer können eine beliebige Anzahl von GameEvents im Projekt erstellen, um wichtige Nachrichten zu repräsentieren, die gesendet werden können. Ein GameEventListener wartet darauf, dass ein bestimmtes GameEvent ausgelöst wird, und reagiert, indem er ein UnityEvent (das kein echtes Ereignis ist, sondern eher einen serialisierten Funktionsaufruf) aufruft.
Codebeispiel: GameEvent ScriptableObject
GameEvent ScriptableObject:
Codebeispiel: GameEventListener
GameEventListener:

Event-System, das den Spielertod behandelt
Ein Beispiel dafür ist die Handhabung des Spielersterbens in einem Spiel. Dies ist ein Punkt, wo sich sehr viel bei der Ausführung ändern kann. Es ist aber schwierig festzulegen, wo die ganze Logik programmiert sein soll. Soll das Player-Skript die Game Over-Benutzeroberfläche oder eine Musikänderung auslösen? Sollen Feinde in jedem Frame überprüfen, ob der Spieler noch am Leben ist? Ein Ereignissystem ermöglicht es uns, problematische Abhängigkeiten wie diese zu vermeiden.
Wenn der Spieler stirbt, ruft das Player-Skript Raise beim OnPlayerDied-Ereignis auf. Das Player-Skript muss nicht wissen, welches System sich darum kümmert, es überträgt nur. Die Game-Over-UI lauscht auf das OnPlayerDied-Event und beginnt mit der Animation. Ein Kamera-Skript kann darauf warten und das Bild ausblenden, und ein Musiksystem kann mit einer Änderung der Musik reagieren. Wir können auch jeden Feind auf OnPlayerDied hören lassen, der eine Provokationsanimation oder einen Zustandswechsel auslöst, um zu einem Leerlaufverhalten zurückzukehren.
Dieses Muster macht es unglaublich einfach, neue Reaktionen auf den Tod des Spielers hinzuzufügen. Darüber hinaus ist es einfach, die Reaktion auf den Tod des Spielers zu testen, indem man Raise beim Ereignis aus einem Testcode oder einem Button im Inspector aufruft.
Das Ereignissystem, das sie bei Schell Games entwickelt haben, ist zu etwas viel Komplexerem gewachsen und hat Funktionen, um Daten zu übergeben und Typen automatisch zu generieren. Dieses Beispiel war im Wesentlichen der Ausgangspunkt für das, was sie heute verwenden.
Für andere Systeme entwickeln
Scriptable Objects müssen nicht nur Daten sein. Sehen Sie sich jedes System an, das Sie in eine MonoBehaviour implementieren, und überlegen Sie, ob Sie die Implementierung stattdessen in eine ScriptableObject verschieben können. Anstatt einen InventoryManager auf einem DontDestroyOnLoad MonoBehaviour zu haben, versuchen Sie, ihn auf einem ScriptableObject zu platzieren.
Da es nicht an die Szene gebunden ist, hat es keine Transform und erhält keine Update-Funktionen, aber es behält den Zustand zwischen den Szenenladungen ohne spezielle Initialisierung bei. Verwenden Sie anstelle eines Singletons eine öffentliche Referenz auf Ihr Inventar-Systemobjekt, wenn ein Skript auf das Inventar zugreifen muss. Das macht es einfacher, ein Testinventar oder ein Tutorialinventar auszutauschen, als wenn Sie ein Singleton verwenden würden.
Hier können Sie sich ein Spieler-Skript vorstellen, das eine Referenz zum Inventarsystem nimmt. Bei einem Spawn des Spielers kann es im Inventar nach allen Besitztümern fragen und jedes Ausrüstungsstück erzeugen. Die Ausstattungsbenutzeroberfläche kann ebenfalls auf das Inventar verweisen und durch die Gegenstände schleifen, um zu bestimmen, was gezeichnet werden soll.