Zuletzt aktualisiert: Januar 2020, Lesezeit: 10 Min.

Drei Wege, wie Sie Ihr Spiel mit ScriptableObjects entwerfen

What you will get from this page: Tips for how to keep your game code easy to change and debug by architecting it with Scriptable Objects.

These tips come from Ryan Hipple, principal engineer at Schell Games, who has advanced experience using Scriptable Objects to architect games. You can watch Ryan’s Unite talk on Scriptable Objects here; we also recommend you see Unity engineer Richard Fine’s session for a great introduction to Scriptable Objects. Thank you Ryan!

 

 

Was sind ScriptableObjects?

ScriptableObject is a serializable Unity class that allows you to store large quantities of shared data independent from script instances. Using ScriptableObjects makes it easier to manage changes and debugging. You can build in a level of flexible communication between the different systems in your game, so that it’s more manageable to change and adapt them throughout production, as well as reuse components.

Drei Säulen der Spieleentwicklung

Verwenden Sie ein modulares Design:

  • Vermeiden Sie es, Systeme zu erstellen, die direkt voneinander abhängen. Ein Inventarsystem sollte beispielsweise mit den anderen Systemen in Ihrem Spiel kommunizieren können, aber Sie sollten keine harte Referenz zwischen ihnen erstellen, da dies die erneute Umsetzung von Systemen in verschiedenen Konfigurationen und Beziehungen erschwert. 
  • Erstellen Sie Szenen als „saubere Neuanfänge“: Vermeiden Sie Durchgangsdaten zwischen Ihren Szenen. Für jede neue Szene sollte es einen klaren Bruch und einen Ladevorgang geben. Das ermöglicht Ihnen Szenen, die ein einzigartiges Verhalten aufweisen, das in anderen Szenen nicht vorhanden ist, ohne dass Sie einen Hack ausführen müssen. 
  • Stellen Sie Prefabs so ein, dass sie selbständig arbeiten. 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. So finden die meisten Check-Ins auf dem Prefab-Level satt, was zu weniger Konflikten in der Szene führt. 
  • Konzentrieren Sie jede Komponente auf die Lösung eines einzelnen Problems. Das macht es einfacher, mehrere Komponenten zusammenzufügen, um etwas Neues zu erstellen.

 

Machen Sie das Ändern und Bearbeiten von Teilen einfach:

  • Setzen Sie in Ihrem Spiel so viel wie möglich datengesteuert um. Wenn Sie Ihre Spielsysteme wie Maschinen erstellen, die Daten als Anweisungen verarbeiten, können Sie effizient Änderungen am Spiel vornehmen, sogar während es ausgeführt wird. 
  • Wenn Ihre Systeme so modular und komponentenbasiert wie möglich eingerichtet sind, vereinfacht das die Bearbeitung, auch für Ihre Grafiker und Designer. Wenn Designer im Spiel Dinge zusammenfügen können, ohne nach einer bestimmten Funktion fragen zu müssen – größtenteils dank der Implementierung von kleinen Komponenten, die jeweils nur eine Funktion ausführen – haben sie die Möglichkeit, solche Komponenten auf verschiedene Art und Weise zu kombinieren, um ein neues Gameplay bzw. neue Mechaniken zu finden. Ryan Hipple meint, dass einige der großartigsten Merkmale der Spiele, an denen sein Team gearbeitet hat, durch diesen Prozess entstanden sind, den er „Emergent Design“ nennt. 
  • Es ist entscheidend, dass Ihr Team Änderungen am Spiel zur Laufzeit durchführen kann. Je mehr Änderungen Sie zur Laufzeit an Ihrem Spiel durchführen können, desto eher finden Sie Balance und Werte, und wenn Sie Ihren Laufzeit-Status zurückspeichern können, wie es mit skriptfähigen Objekten möglich ist, sind Sie in einer guten Position.

 

Vereinfachen Sie das Debuggen:

Dies ist eher eine „Untersäule“ 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-Status im Inspector sehen können, und betrachten Sie niemals eine Funktion als fertig, bis Sie einen Plan haben, wie Sie sie debuggen können. 

Für Variablen entwickeln

Eine der einfachsten Sachen, die Sie mit einem skriptfähigen Objekt erstellen können, ist eine selbständige, Asset-basierte Variable. Hier sehen Sie beispielhaft eine FloatVariable, die Erläuterung trifft aber auch auf jeden anderen serialisierbaren Variablentyp zu.

Jeder in Ihrem Team kann, unabhängig von den jeweiligen technischen Kenntnissen, durch die Erstellung eines neuen FloatVariable-Assets eine neue Spielvariable definieren. Jede MonoBehaviour oder ScriptableObject kann eine öffentliche FloatVariable statt eines „Public Float“ verwenden, um diesen neuen, geteilten Wert zu referenzieren.

Noch besser: Wenn eine MonoBehaviour den Wert einer FloatVariable ändert, können andere MonoBehaviour diese Änderung sehen. Dadurch entsteht eine Art Messaging-Ebene zwischen Systemen, die keine Referenzen zueinander benötigen. 

FloatVariable.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

Beispiel: Gesundheitspunkte eines Spielers

Ein Anwendungsbeispiel hierfür sind die Gesundheitspunkte (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 „PlayerHP“ geringer. Heilt sich der Spieler, vergrößert sich „PlayerHP“.

Stellen Sie sich jetzt ein Gesundheitsleisten-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. Die Gesundheitsleiste weiß nichts über den Spieler in der Szene. Sie liest nur von derselben Variable ab, in die der Spieler schreibt.

Sobald wir dies so eingerichtet haben, ist es einfach, weitere Dinge hinzuzufügen, um die PlayerHP-Variable im Auge zu behalten. Das Musiksystem kann sich ändern, wenn die PlayerHP sinkt, Feinde können ihre Angriffsmuster ändern, wenn sie wissen, dass der Spieler schwach ist, Screen-Space-Effekte können die Gefahr des nächsten Angriffs unterstreichen usw. 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 den Inspector aufrufen, wenn das Spiel läuft, und den Wert der PlayerHP-Variable ändern, um Dinge zu testen. 

Wenn Sie den Wert einer FloatVariable ändern, könnte es ratsam sein, Ihre Daten in einen Runtime-Wert zu kopieren, um nicht den Wert zu verändern, der auf der Festplatte für das ScriptableObject gespeichert ist. Wenn Sie dies tun, sollten MonoBehaviour auf RuntimeValue zugreifen, um eine Bearbeitung des auf der Festplatte gespeicherten InitialValue zu vermeiden.

RuntimeValue.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
	public float InitialValue;

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

Für Ereignisse entwickeln

Eins von Ryan Hipples bevorzugten Features, das zusätzlich zu ScriptableObjects erstellt werden kann, ist ein Event-System. Event-Architekturen helfen dabei, Ihren Code zu modularisieren, indem sie Nachrichten zwischen Systemen versenden, die nicht in direkter Verbindung zueinander stehen. Sie ermöglichen Reaktionen auf Zustandsänderungen, ohne den Zustand ständig in einer Updateschleife zu überwachen.

Die folgenden Beispielcodes stammen von einem Event-System, das aus zwei Teilen besteht: GameEvent ScriptableObject und 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 auf das Auftreten eines speziellen GameEvent und reagiert mit dem Aufruf eines UnityEvent (was kein „echtes“ Event ist, sondern eher ein serialisierter Funktionsaufruf).

Codebeispiel: GameEvent ScriptableObject

GameEvent ScriptableObject: 

GameEvent ScriptableObject.cs (C#)
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
	private List<GameEventListener> listeners = 
		new List<GameEventListener>();

public void Raise()
{
	for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }

public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}

Codebeispiel: GameEventListener

GameEventListener:

GameEventListener.cs (C#)
public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;

private void OnEnable()
{ Event.RegisterListener(this); }

private void OnDisable()
{ Event.UnregisterListener(this); }

public void OnEventRaised()
{ Response.Invoke(); }
}

Event-System, das den Spielertod behandelt

Ein Beispiel dafür ist, wie der Spielertod in einem Spiel gehandhabt wird. 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 Event-System hilft uns, diese problematischen Abhängigkeiten zu vermeiden.

Wenn der Spieler stirbt, ruft das Player-Skript „Raise“ für das OnPlayerDied-Event 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 jeden Feind OnPlayerDied überwachen lassen und eine Verspotten-Animation oder eine Statusänderung zu einem inaktiven Verhalten auslösen.

Dieses Muster macht es unglaublich einfach, neue Reaktionen auf den Tod eines Spielers hinzuzufügen. Außerdem ist es einfach, diese Reaktionen durch den Aufruf von „Raise“ über einen Test-Code oder eine Schaltfläche im Inspector zu testen.

Das Event-System, das bei Schell Games erstellt wurde, ist zu etwas Komplizierterem angewachsen und beinhaltet Funktionen, die das Übergeben von Daten und automatisches Generieren von Typen zulassen. Dieses Beispiel war im Wesentlichen der Ausgangspunkt von dem, was das Unternehmen heute verwendet.

Für andere Systeme entwickeln

Skriptfähige Objekte müssen nicht nur einfach 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. Versuchen Sie, statt einen InventoryManager auf einer DontDestroyOnLoad MonoBehaviour zu verwenden, ein ScriptableObject einzusetzen.

Da es nicht an die Szene gebunden ist, hat es keine Transformation und erhält keine Update-Funktionen, aber es behält den Status zwischen dem Laden der Szenen ohne besondere Initialisierung bei. Verwenden Sie anstelle eines Singletons eine öffentliche Referenz auf Ihr Inventar-Systemobjekt, wenn ein Skript auf das Inventar zugreifen muss. Dadurch ist es einfacher, ein Test- oder Tutorial-Inventar einzubauen, als wenn Sie ein Singleton verwenden würden.

kStellen Sie sich hierzu ein Player-Skript vor, das auf das Inventarsystem verweist. Bei einem Spawn des Spielers kann es im Inventar nach allen Besitztümern fragen und jedes Ausrüstungsstück erzeugen. Die Ausrüstung-UI kann ebenfalls das Inventar referenzieren und die Gegenstände durchgehen, um zu bestimmen, welche ausgewählt werden sollen. 

Haben Ihnen diese Inhalte gefallen?

Wir verwenden Cookies, damit wir Ihnen die beste Erfahrung auf unserer Website bieten können. In unseren Cookie-Richtlinien erhalten Sie weitere Informationen.

Verstanden