Engine & platform

Skripting-Hacks für Fortgeschrittene, die Ihnen Zeit sparen, Teil 2

JORDI CABALLOL / UNITYSenior Software Engineer
Nov 8, 2022|15 Min.
Skripting-Hacks für Fortgeschrittene, die Ihnen Zeit sparen, Teil 2
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Ich bin zurück für Teil zwei! Wenn Sie den ersten Teil meiner fortgeschrittenen Editor-Scripting-Hacks verpasst haben, lesen Sie ihn hier. In diesem zweiteiligen Artikel erfahren Sie, wie Sie die Arbeitsabläufe im Editor verbessern können, damit Ihr nächstes Projekt reibungsloser abläuft als Ihr letztes.

Jeder Hack basiert auf einem demonstrativen Prototyp, den ich erstellt habe - ähnlich wie bei einem RTS - bei dem die Einheiten eines Teams automatisch gegnerische Gebäude und andere Einheiten angreifen. Zur Auffrischung sehen Sie hier den Prototyp der ersten Version:

Im vorangegangenen Artikel habe ich bewährte Verfahren für den Import und die Einrichtung der Kunstwerke im Projekt beschrieben. Jetzt können wir diese Assets im Spiel verwenden und dabei so viel Zeit wie möglich sparen.

Beginnen wir damit, die Elemente des Spiels auszupacken. Wenn wir die Elemente eines Spiels aufbauen, treffen wir oft auf folgendes Szenario:

Auf der einen Seite haben wir Prefabs, die vom Art-Team stammen - sei es eine Prefab, die vom FBX-Importer generiert wurde, oder eine Prefab, die sorgfältig mit allen entsprechenden Materialien und Animationen eingerichtet wurde, indem Requisiten zur Hierarchie hinzugefügt wurden, usw. Um dieses Prefab im Spiel zu verwenden, ist es sinnvoll, daraus eine Prefab-Variante zu erstellen und dort alle spielrelevanten Komponenten hinzuzufügen. Auf diese Weise kann das Grafikteam das Prefab ändern und aktualisieren, und alle Änderungen werden sofort im Spiel angezeigt. Dieser Ansatz funktioniert zwar, wenn für den Artikel nur ein paar Komponenten mit einfachen Einstellungen erforderlich sind, er kann jedoch viel Arbeit verursachen, wenn Sie jedes Mal etwas Komplexes von Grund auf neu einrichten müssen.

Andererseits werden viele der Gegenstände die gleichen Komponenten mit ähnlichen Werten haben, wie z. B. alle Auto-Vorfertigungen oder Vorfertigungen für ähnliche Gegner. Es ist logisch, dass sie alle Varianten desselben Basisfertigteils sind. Dieser Ansatz ist jedoch ideal, wenn das Einrichten der Kunst des Prefab einfach ist (d. h. das Einstellen des Netzes und seiner Materialien).

Als Nächstes wollen wir uns ansehen, wie wir die Einrichtung von Gameplay-Komponenten vereinfachen können, so dass wir sie schnell zu unseren Kunst-Prefabs hinzufügen und direkt im Spiel verwenden können.

Hack 7: Komponenten von Anfang an einrichten

Das häufigste Setup, das ich für komplexe Elemente in einem Spiel gesehen habe, ist eine "Haupt"-Komponente (wie "Feind", "Pickup" oder "Tür"), die als Schnittstelle für die Kommunikation mit dem Objekt fungiert, und eine Reihe kleiner, wiederverwendbarer Komponenten, die die eigentliche Funktionalität implementieren; Dinge wie "Selectable", "CharacterMovement" oder "UnitHealth" und in Unity integrierte Komponenten wie Renderer und Collider.

Einige der Komponenten hängen von anderen Komponenten ab, damit sie funktionieren. Zum Beispiel könnte die Bewegung der Figur einen NavMesh-Agenten benötigen. Deshalb hält Unity das Attribut RequireComponent bereit, um all diese Abhängigkeiten zu definieren. Wenn es also eine "Haupt"-Komponente für einen bestimmten Objekttyp gibt, können Sie das Attribut RequireComponent verwenden, um alle Komponenten hinzuzufügen, die dieser Objekttyp haben muss.

Die Einheiten in meinem Prototyp haben zum Beispiel diese Attribute:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Legen Sie nicht nur einen leicht zu findenden Ort im AddComponentMenu fest, sondern fügen Sie auch alle zusätzlichen Komponenten ein, die es braucht. In diesem Fall habe ich die Locomotion hinzugefügt, um sich zu bewegen, und die AttackComponent, um andere Einheiten anzugreifen.

Darüber hinaus verfügt die Basisklasse unit (die mit den Gebäuden gemeinsam genutzt wird) über weitere RequireComponent-Attribute, die von dieser Klasse geerbt werden, z. B. die Komponente Health. Auf diese Weise muss ich nur die Komponente Soldier zu einem GameObject hinzufügen, so dass alle anderen Komponenten automatisch hinzugefügt werden. Wenn ich ein neues RequireComponent-Attribut zu einer Komponente hinzufüge, aktualisiert Unity alle bestehenden GameObjects mit der neuen Komponente, was die Erweiterung der bestehenden Objekte erleichtert.

RequireComponent hat auch einen subtileren Vorteil: Wenn wir "Komponente A" haben, die "Komponente B" benötigt, dann stellt das Hinzufügen von A zu einem GameObject nicht nur sicher, dass B ebenfalls hinzugefügt wird - es stellt sogar sicher, dass B vor A hinzugefügt wird. Das bedeutet, dass, wenn die Reset-Methode für Komponente A aufgerufen wird, Komponente B bereits existiert und wir ohne weiteres Zugriff darauf haben. Dies ermöglicht uns, Referenzen auf die Komponenten zu setzen, persistente UnityEvents zu registrieren und alles andere, was wir tun müssen, um das Objekt einzurichten. Durch die Kombination des RequireComponent-Attributs und der Reset-Methode können wir das Objekt durch Hinzufügen einer einzigen Komponente vollständig einrichten.

Hack 8: Daten in unverbundenen Prefabs teilen

Der größte Nachteil der oben gezeigten Methode ist, dass wir, wenn wir uns entscheiden, einen Wert zu ändern, diesen für jedes Objekt manuell ändern müssen. Und wenn die gesamte Einrichtung durch Code erfolgt, wird es für Designer schwierig, diesen zu ändern.

Im vorangegangenen Artikel wurde die Verwendung von AssetPostprocessor zum Hinzufügen von Abhängigkeiten und Ändern von Objekten zum Zeitpunkt des Imports beschrieben. Verwenden wir dies nun, um einige Werte in unseren Prefabs zu erzwingen.

Um es den Designern zu erleichtern, diese Werte zu ändern, werden wir die Werte aus einem Prefab lesen. Auf diese Weise können die Konstrukteure diese Voreinstellung leicht ändern, um die Werte für das gesamte Projekt zu ändern.

Wenn Sie Editor-Code schreiben, können Sie die Werte von einer Komponente in einem Objekt in ein anderes kopieren, indem Sie die Preset-Klasse nutzen.

Erstellen Sie eine Voreinstellung für die ursprüngliche Komponente und wenden Sie sie auf die andere(n) Komponente(n) an:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

So wie es aussieht, werden alle Werte in der Voreinstellung überschrieben, aber das ist wahrscheinlich nicht das, was wir wollen. Kopieren Sie stattdessen nur einige Werte, während Sie den Rest beibehalten. Verwenden Sie dazu eine weitere Überschreibung von Preset.ApplyTo, die eine Liste der Eigenschaften enthält, die angewendet werden müssen. Natürlich könnten wir auch einfach eine hartkodierte Liste der Eigenschaften erstellen, die wir außer Kraft setzen wollen, was für die meisten Projekte gut funktionieren würde, aber sehen wir uns an, wie wir dies vollständig generisch machen können.

Im Grunde genommen habe ich eine Basis-Vorlage mit allen Komponenten erstellt und dann eine Variante, die als Vorlage dient. Dann entschied ich, welche Werte ich aus der Liste der Überschreibungen in der Variante anwenden wollte.

Um die Überschreibungen zu erhalten, verwenden Sie PrefabUtility.GetPropertyModifications. Dadurch erhalten Sie alle Überschreibungen in der gesamten Prefab, so dass Sie nur diejenigen filtern, die für diese Komponente erforderlich sind. Dabei ist zu beachten, dass das Ziel der Modifikation die Komponente der Basis-Prefab ist - nicht die Komponente der Variante -, so dass wir den Verweis darauf mit GetCorrespondingObjectFromSource:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Damit werden nun alle Überschreibungen der Vorlage auf unsere Prefabs angewendet. Das einzige Detail, das noch fehlt, ist, dass die Vorlage eine Variante einer Variante sein könnte, und wir wollen die Überschreibungen dieser Variante ebenfalls anwenden.

Um dies zu tun, müssen wir nur diese rekursiv machen:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Als Nächstes müssen wir die Vorlage für unsere Prefabs finden. Idealerweise werden wir verschiedene Vorlagen für verschiedene Arten von Objekten verwenden wollen. Ein effizienter Weg, dies zu tun, besteht darin, die Vorlagen in denselben Ordner zu legen wie die Objekte, auf die wir sie anwenden wollen.

Suchen Sie ein Objekt namens Template.prefab im gleichen Ordner wie unser Prefab. Wenn wir ihn nicht finden können, suchen wir rekursiv im übergeordneten Ordner:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

An dieser Stelle können wir die Vorlage Prefab ändern, und alle Änderungen werden in den Prefabs in diesem Ordner übernommen, auch wenn es sich nicht um Varianten der Vorlage handelt. In diesem Beispiel habe ich die Standard-Spielerfarbe geändert (die Farbe, die verwendet wird, wenn die Einheit keinem Spieler zugeordnet ist). Beachten Sie, wie alle Objekte aktualisiert werden:

Hack 9: Abgleich von Spieldaten mit ScriptableObjects und Tabellenkalkulationen

Beim Balancing von Spielen werden alle Werte, die Sie anpassen müssen, auf verschiedene Komponenten verteilt und in einem Prefab oder ScriptableObject für jeden Charakter gespeichert. Dies macht den Prozess der Anpassung von Details ziemlich langsam.

Eine gängige Methode zur Vereinfachung der Bilanzierung ist die Verwendung von Tabellenkalkulationen. Sie können sehr praktisch sein, da sie alle Daten zusammenführen, und Sie können Formeln verwenden, um einige der zusätzlichen Daten automatisch zu berechnen. Die manuelle Eingabe dieser Daten in Unity kann jedoch mühsam sein.

Hier kommen die Tabellenkalkulationen ins Spiel. Sie können in einfache Formate wie CSV(.csv) oder TSV(.tsv) exportiert werden, und genau dafür sind die ScriptedImporters gedacht. Unten sehen Sie einen Screenshot der Statistiken für die Einheiten des Prototyps:

Beispiel einer Kalkulationstabelle | Tech from the Trenches

Der Code dafür ist ziemlich einfach: Erstellen Sie ein ScriptableObject mit allen Statistiken für eine Einheit, dann können Sie die Datei lesen. Erstellen Sie für jede Zeile der Tabelle eine Instanz des ScriptableObjects und füllen Sie sie mit den Daten für diese Zeile.

Fügen Sie abschließend alle ScriptableObjects über den Kontext zum importierten Asset hinzu. Wir müssen auch ein Haupt-Asset hinzufügen, das ich einfach auf ein leeres TextAsset setze (da wir das Haupt-Asset hier nicht wirklich für irgendetwas verwenden).

Das funktioniert sowohl für Gebäude als auch für Einheiten, aber Sie sollten prüfen, welche Sie importieren, da Einheiten viel mehr Werte haben.

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Damit sind nun einige ScriptableObjects vorhanden, die alle Daten aus der Kalkulationstabelle enthalten.

Importierte Daten aus der Tabellenkalkulation

Die generierten ScriptableObjects sind bereit, im Spiel nach Bedarf verwendet zu werden. Sie können auch den PrefabPostprozessor verwenden, der zuvor eingerichtet wurde.

In der Methode OnPostprocessPrefab haben wir die Möglichkeit, dieses Asset zu laden und seine Daten zu verwenden, um die Parameter der Komponenten automatisch zu füllen. Mehr noch: Wenn Sie eine Abhängigkeit zu diesem Datenbestand festlegen, werden die Prefabs bei jeder Änderung der Daten neu importiert, so dass alles automatisch auf dem neuesten Stand ist.

Hack 10: Beschleunigung der Iteration im Editor

Wenn man versucht, fantastische Levels zu erstellen, ist es wichtig, dass man Dinge schnell ändern und testen kann, indem man kleine Anpassungen vornimmt und es erneut versucht. Deshalb sind schnelle Iterationszeiten und eine Verringerung der für den Start von Tests erforderlichen Schritte so wichtig.

Eines der ersten Dinge, an die wir denken, wenn es um Iterationszeiten in Unity geht, ist der Domain Reload. Die Funktion "Domain Reload" ist in zwei Schlüsselsituationen von Bedeutung: nach dem Kompilieren von Code, um die neuen dynamisch verknüpften Bibliotheken (DLLs) zu laden, und beim Aufrufen und Beenden des Spielmodus. Das Neuladen der Domäne, das mit dem Kompilieren einhergeht, kann nicht vermieden werden, aber Sie haben die Möglichkeit, das Neuladen im Zusammenhang mit dem Wiedergabemodus unter Projekteinstellungen > Editor > Wiedergabemoduseinstellungen eingeben zu deaktivieren.

Das Deaktivieren des Domain Reload beim Aufrufen des Spielmodus kann einige Probleme verursachen, wenn Ihr Code nicht darauf vorbereitet ist. Das häufigste Problem ist, dass statische Variablen nach dem Spielen nicht zurückgesetzt werden. Wenn Ihr Code mit dieser Deaktivierung funktionieren kann, sollten Sie dies tun. Bei diesem Prototyp ist der Domain Reload deaktiviert, so dass Sie fast sofort in den Spielmodus wechseln können.

Hack 11: Daten automatisch generieren

Ein weiteres Problem mit den Iterationszeiten hat mit der Neuberechnung von Daten zu tun, die für das Spielen erforderlich sind. Dazu müssen häufig einige Komponenten ausgewählt und Schaltflächen angeklickt werden, um die Neuberechnungen auszulösen. In diesem Prototyp gibt es zum Beispiel einen TeamController für jedes Team innerhalb der Szene. Dieser Controller verfügt über eine Liste aller gegnerischen Gebäude, so dass er die Einheiten zum Angriff auf diese schicken kann. Um diese Daten automatisch zu füllen, verwenden Sie die IProcessSceneWithReport Schnittstelle. Diese Schnittstelle wird für die Szenen bei zwei verschiedenen Gelegenheiten aufgerufen: während des Builds und beim Laden einer Szene im Play-Modus. Damit haben Sie die Möglichkeit, jedes beliebige Objekt zu erstellen, zu zerstören und zu verändern. Beachten Sie jedoch, dass sich diese Änderungen nur auf Builds und den Spielmodus auswirken.

In diesem Callback werden die Controller erstellt und die Liste der Gebäude festgelegt. Dadurch ist es nicht mehr notwendig, etwas manuell zu tun. Die Controller mit der aktualisierten Gebäudeliste werden bei Spielbeginn vor Ort sein, und die Liste wird mit den Änderungen, die wir vorgenommen haben, aktualisiert werden.

Für den Prototyp wurde eine Utility-Methode entwickelt, mit der man alle Instanzen einer Komponente in einer Szene abrufen kann. Damit können Sie alle Gebäude erhalten:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Der Rest des Prozesses ist eher trivial: Holen Sie sich alle Gebäude, holen Sie sich alle Teams, zu denen die Gebäude gehören, und erstellen Sie einen Controller für jedes Team mit einer Liste der gegnerischen Gebäude.

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Hack 12: Szenenübergreifendes Arbeiten

Neben der bearbeiteten Szene müssen Sie auch andere Szenen laden, um spielen zu können (z. B. eine Szene mit den Managern, mit der Benutzeroberfläche usw.) Dies kann wertvolle Zeit in Anspruch nehmen. Im Fall des Prototyps befindet sich der Canvas mit den Gesundheitsleisten in einer anderen Szene namens InGameUI.

Eine effektive Möglichkeit, damit umzugehen, besteht darin, der Szene eine Komponente mit einer Liste der Szenen hinzuzufügen, die zusammen mit ihr geladen werden müssen. Wenn Sie diese Szenen synchron in der Awake-Methode laden, wird die Szene geladen und alle ihre Awake-Methoden werden zu diesem Zeitpunkt aufgerufen. Wenn die Start-Methode aufgerufen wird, können Sie also sicher sein, dass alle Szenen geladen und initialisiert sind, wodurch Sie Zugriff auf die darin enthaltenen Daten, wie z. B. Manager-Singletons, erhalten.

Denken Sie daran, dass Sie möglicherweise einige Szenen geöffnet haben, wenn Sie den Wiedergabemodus aufrufen:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Einpacken

In den Teilen eins und zwei dieses Artikels habe ich Ihnen gezeigt, wie Sie einige der weniger bekannten Funktionen von Unity nutzen können. Alles, was hier beschrieben wurde, ist nur ein Bruchteil dessen, was man tun kann, aber ich hoffe, dass Sie diese Hacks für Ihr nächstes Projekt nützlich oder zumindest interessant finden werden.

Die Assets, die zur Erstellung des Prototyps verwendet wurden, können kostenlos im Asset Store gefunden werden:

Wenn Sie über diesen Zweiteiler diskutieren oder Ihre Ideen nach der Lektüre austauschen möchten, besuchen Sie unser Scripting-Forum. Ich melde mich jetzt ab, aber Sie können mich weiterhin auf Twitter unter @CaballolD erreichen. Bleiben Sie dran für zukünftige technische Blogs von anderen Unity-Entwicklern als Teil der fortlaufenden Tech from the Trenches Serie.