Unity3D: Spielarchitektur, ScriptableObjects, Singletones

Heute werden wir darüber sprechen, wie Daten im Spiel gespeichert, empfangen und gesendet werden. Über das wunderbare Ding namens ScriptableObject und warum es wunderbar ist. Lassen Sie uns ein wenig über die Vorteile von Singletones beim Organisieren von Szenen und Übergängen zwischen ihnen sprechen.

Dieser Artikel beschreibt einen langen und schmerzhaften Weg, um ein Spiel zu entwickeln, verschiedene Ansätze, die in diesem Prozess verwendet werden. Höchstwahrscheinlich gibt es viele nützliche Informationen für Anfänger und nichts Neues für „Veteranen“.

Verknüpfungen zwischen Skripten und Objekten


Die erste Frage, mit der ein unerfahrener Entwickler konfrontiert ist, ist, wie alle geschriebenen Klassen miteinander verknüpft und die Interaktionen zwischen ihnen konfiguriert werden können.

Am einfachsten ist es, einen Link zur Klasse direkt anzugeben:

public class MyScript : MonoBehaviour { public OtherScript otherScript; } 

Und dann - binden Sie das Skript manuell über den Inspektor.

Dieser Ansatz hat mindestens einen wesentlichen Nachteil: Wenn die Anzahl der Skripte mehrere zehn überschreitet und für jedes von ihnen zwei oder drei Links erforderlich sind, verwandelt sich das Spiel schnell in ein Web. Ein Blick auf sie reicht aus, um Kopfschmerzen zu verursachen.

Es ist (meiner Meinung nach) viel besser, ein System von Nachrichten und Abonnements zu organisieren, in dem unsere Objekte die Informationen erhalten, die sie benötigen - und nur das! - ohne ein halbes Dutzend Links miteinander zu benötigen.

Nachdem ich mich mit dem Thema befasst hatte, fand ich heraus, dass vorgefertigte Lösungen in Unity von jedem beschimpft werden, der nicht faul ist. Es schien mir eine nicht triviale Aufgabe zu sein, ein solches System von Grund auf neu zu schreiben, und deshalb werde ich nach einfacheren Lösungen suchen.

Skriptfähiges Objekt


Grundsätzlich gibt es zwei Dinge, die Sie über ScriptableObject wissen sollten:

  • Sie sind Teil der in Unity implementierten Funktionalität wie MonoBehaviour.
  • Im Gegensatz zu MonoBehaviour sind sie nicht an Szenenobjekte gebunden, sondern existieren als separate Assets und können Daten zwischen Spielsitzungen speichern und übertragen .

Ich habe mich sofort in ihre heiße Liebe verliebt. Sie sind in gewisser Weise mein Allheilmittel für jedes Problem geworden:

  • Müssen Sie Spieleinstellungen speichern? ScriptableObject!
  • Inventar erstellen? ScriptableObject!
  • KI schreiben? ScriptableObject!
  • Informationen über einen Charakter, einen Feind oder einen Gegenstand aufzeichnen? ScriptableObject wird Sie niemals im Stich lassen!

Ohne nachzudenken, habe ich mehrere Klassen vom Typ ScriptableObject erstellt und dann ein Repository für sie:

 public class Database: ScriptableObject { public PlayerData playerData; public GameSettings gameSettings; public SpellController spellController; } 

Jedes von ihnen speichert alle nützlichen Informationen und möglicherweise Links zu anderen Objekten in sich. Jeder von ihnen reicht aus, um sich einmal durch den Inspektor zu binden - sie werden nirgendwo anders hingehen.

Jetzt muss ich nicht mehr unendlich viele Links zwischen Skripten angeben! Für jedes Skript kann ich einmal einen Link zu meinem Repository angeben - und es erhält alle Informationen von dort.

Somit sieht die Berechnung der Geschwindigkeit des Charakters sehr elegant aus:

 //   float speed = database.playerData.speed; //    if (database.spellController.haste.active) speed = speed * database.spellController.haste.speedModifier; // ,     if (database.playerData.health<database.playerData.healthThreshold) speed = speed * database.playerData.woundedModifier; 

Und wenn zum Beispiel eine Falle nur auf einen laufenden Charakter schießen sollte:

 if (database.playerData.isSprinting) Activate(); 

Darüber hinaus muss der Charakter nichts über Zauber oder Fallen wissen. Es werden nur Daten aus dem Speicher abgerufen. Nicht schlecht? Nicht schlecht.

Aber fast sofort stoße ich auf ein Problem. ScriptableOnjects kann keine Links zu Szenenobjekten direkt speichern. Mit anderen Worten, ich kann keinen Link zum Spieler erstellen, ihn über den Inspektor binden und die Frage nach den Koordinaten des Spielers für immer vergessen.

Und wenn Sie darüber nachdenken, macht es Sinn! Assets existieren außerhalb der Bühne und können in jeder Szene abgerufen werden. Und was passiert, wenn Sie einen Link zu einem Objekt hinterlassen, das sich in einer anderen Szene innerhalb des Assets befindet?

Nichts Gutes.

Lange Zeit hat eine Krücke für mich funktioniert: Im Repository wird ein öffentlicher Link erstellt, und dann füllte jedes Objekt, an den Sie sich erinnern möchten, diesen Link:

 public class PlayerController : MonoBehaviour { void Awake() { database.playerData.player = this.gameObject; } } 

Unabhängig von der Szene erhält mein Repository also zunächst einen Link zum Player und merkt sich diesen. Jetzt sollte beispielsweise jeder, der Feind, keinen Link zum Spieler speichern und ihn nicht über FindWithTag () suchen (was ein ziemlich ressourcenintensiver Prozess ist). Er greift lediglich auf das Repository zu:

 public Database database; Vector3 destination; void Update () { destination = database.playerData.player.transform.position; } 

Es scheint: Das System ist perfekt! Aber nein. Wir haben noch 2 Probleme.

  1. Ich muss immer noch manuell einen Link zum Repository für jedes Skript angeben.
  2. Es ist umständlich, Verweise auf Szenenobjekte in einem ScriptableObject zuzuweisen.

Über die zweite im Detail. Angenommen, ein Spieler hat einen leichten Zauber. Der Spieler wirft es und das Spiel sagt zum Laden: Das Licht wird geworfen!

 database.spellController.light.CastSpell(); 

Und dies führt zu einer Reihe von Reaktionen:

  • Am Cursorpunkt wird ein neues (oder altes) Spielobjektlicht erstellt.
  • Ein GUI-Modul wird gestartet, das uns mitteilt, dass das Licht aktiv ist.
  • Feinde erhalten beispielsweise einen vorübergehenden Bonus für die Suche nach einem Spieler.

Wie macht man das alles?

Es ist möglich, dass jedes Objekt, das sich für das Licht interessiert, direkt in Update () und schreibt, so und so, jeder Frame dem Licht folgt (if (database.spellController.light.isActive)) und wenn es aufleuchtet - reagieren! Und es ist egal, dass diese Prüfung in 90% der Fälle im Leerlauf funktioniert. Auf mehreren hundert Objekten.

Oder organisieren Sie dies alles in Form von vorbereiteten Links. Es stellt sich heraus, dass die einfache CastSpell () -Funktion Zugriff auf Links zum Spieler, zum Licht und zur Liste der Feinde haben sollte. Und das ist bestenfalls. Viele Links, oder?

Sie können natürlich alles Wichtige in unserem Repository speichern, wenn die Szene beginnt, Links auf Assets streuen, die im Allgemeinen nicht dafür vorgesehen sind ... Aber auch hier verstoße ich gegen das Prinzip eines einzelnen Repositorys und verwandle es in ein Netz von Links.

Singleton


Hier kommt der Singleton ins Spiel. Tatsächlich ist dies ein Objekt, das nur in einer einzigen Kopie existiert (und existieren kann).

 public class GameController : MonoBehaviour { public static GameController Instance; //   ,      public Database database; public GameObject player; public GameObject GUI; public List<Enemy> enemies; public List<Spell> spells; void Awake () { if (Instance == null) { DontDestroyOnLoad (gameObject); Instance = this; } else if (Instance != this) { Destroy (gameObject); } } } 

Ich schnappe es zu einem leeren Szenenobjekt. Nennen wir es GameController.

Somit habe ich ein Objekt in der Szene, das alle Informationen über das Spiel speichert. Darüber hinaus kann es zwischen Szenen wechseln, seine Doppelbilder zerstören (falls sich bereits ein anderer GameController in der neuen Szene befindet), Daten zwischen Szenen übertragen und, falls gewünscht, das Speichern / Laden des Spiels implementieren.

Von allen bereits geschriebenen Skripten können Sie den Link zum Data Warehouse entfernen. Schließlich muss ich es jetzt nicht mehr manuell konfigurieren. Alle Verweise auf Szenenobjekte werden aus dem Store gelöscht und auf unseren GameController übertragen (wir werden sie höchstwahrscheinlich benötigen, um den Status der Szene zu speichern, wenn wir das Spiel beenden). Und dann fülle ich es mit allen notwendigen Informationen auf eine für mich bequeme Weise aus. In Awake () des Spielers und der Feinde (und wichtiger Objekte der Szene) ist es beispielsweise vorgeschrieben, im GameController einen Link zu sich selbst hinzuzufügen. Da ich jetzt mit Monobehaviour arbeite, passen Verweise auf Objekte in der Szene sehr organisch hinein.

Was bekommen wir?

Jedes Objekt kann Informationen über das Spiel erhalten, das es benötigt:

 if (GameController.Instance.database.playerData.isSprinting) ActivateTrap(); 

In diesem Fall ist es absolut nicht erforderlich, Verknüpfungen zwischen Objekten zu konfigurieren. Alles wird in unserem GameController gespeichert.

Jetzt wird es nichts Komplizierteres geben, den Zustand der Szene aufrechtzuerhalten. Immerhin haben wir bereits alle notwendigen Informationen: Feinde, Gegenstände, Spielerposition, Data Warehouse. Es reicht aus, die Informationen zu der Szene auszuwählen, die Sie speichern möchten, und sie beim Beenden der Szene mit FileStream in eine Datei zu schreiben.

Die Gefahren


Wenn Sie bis zu diesem Punkt lesen, sollte ich Sie vor den Gefahren dieses Ansatzes warnen.

Eine sehr schlechte Situation ist, wenn viele Skripte auf eine Variable in unserem ScriptableObject verweisen. Es ist nichts Falsches daran, einen Wert zu erhalten, aber wenn sie eine Variable von verschiedenen Orten aus beeinflussen, ist dies eine potenzielle Bedrohung.

Wenn wir eine gespeicherte playerSpeed-Variable haben und der Player sich mit unterschiedlichen Geschwindigkeiten bewegen muss, sollten wir playerSpeed ​​nicht im Speicher ändern, sondern abrufen, in einer temporären Variablen speichern und bereits Geschwindigkeitsmodifikatoren anwenden.

Der zweite Punkt - wenn ein Objekt Zugriff auf irgendetwas hat - ist eine große Macht. Und eine große Verantwortung. Und Sie müssen vorsichtig vorgehen, damit ein Pilzskript nicht versehentlich alle Ihre KI-Feinde vollständig zerstört. Eine ordnungsgemäß konfigurierte Kapselung verringert die Risiken, rettet Sie jedoch nicht davor.

Vergessen Sie auch nicht, dass Singletones sanfte Wesen sind. Missbrauche sie nicht.

Das ist alles für heute.

Aus offiziellen Unity-Tutorials wurde viel gelernt, einige aus inoffiziellen. Ich musste selbst zu etwas kommen. Die oben genannten Ansätze können also ihre Gefahren und Nachteile haben, die ich übersehen habe.

Und deshalb - die Diskussion ist willkommen!

Source: https://habr.com/ru/post/de414361/


All Articles