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

Bei den meisten Projekten, die ich gesehen habe, müssen die Entwickler viele Aufgaben erledigen, die sich wiederholen und fehleranfällig sind, vor allem, wenn es um die Integration neuer Kunstwerke geht. Das Einrichten eines Charakters erfordert zum Beispiel oft das Ziehen und Ablegen vieler Asset-Referenzen, das Aktivieren von Kontrollkästchen und das Anklicken von Schaltflächen: Setzen Sie das Rig des Modells auf Humanoid, deaktivieren Sie die sRGB der SDF-Textur, setzen Sie die Normal Maps als Normal Maps und die UI-Texturen als Sprites. Mit anderen Worten: Es wird wertvolle Zeit vergeudet, und entscheidende Schritte können trotzdem verpasst werden.
In diesem zweiteiligen Artikel zeige ich Ihnen, wie Sie diesen Arbeitsablauf verbessern können, damit Ihr nächstes Projekt reibungsloser abläuft als Ihr letztes. Zur weiteren Veranschaulichung habe ich einen einfachen Prototyp - ähnlich einem RTS - erstellt, bei dem die Einheiten eines Teams automatisch feindliche Gebäude und andere Einheiten angreifen. Mit jedem Scripting-Hack werde ich einen Aspekt dieses Prozesses verbessern, sei es die Texturen oder die Modelle.
So sieht der Prototyp aus:
Der Hauptgrund, warum Entwickler beim Import von Assets so viele kleine Details einrichten müssen, ist einfach: Da Unity nicht weiß, wie Sie ein Asset verwenden werden, kann es auch nicht wissen, was die besten Einstellungen dafür sind. Wenn Sie einige dieser Aufgaben automatisieren wollen, ist dies das erste Problem, das gelöst werden muss.
Der einfachste Weg, um herauszufinden, wozu ein Asset dient und wie es mit anderen in Beziehung steht, ist die Einhaltung einer bestimmten Namenskonvention und Ordnerstruktur, wie z. B.:
- Benennungskonvention: Wir können Dinge an den Namen des Assets selbst anhängen, also ist Shield_BC.png die Grundfarbe und Shield_N.png die Normal Map.
- Struktur des Ordners: Knight/Animations/Walk.fbx ist eindeutig eine Animation, während Knight/Models/Knight.fbx ein Modell ist, auch wenn beide das gleiche Format (.fbx) haben.
Das Problem dabei ist, dass es nur in eine Richtung gut funktioniert. Während man also vielleicht schon weiß, wozu ein Vermögenswert dient, wenn man seinen Pfad kennt, kann man seinen Pfad nicht ableiten, wenn man nur Informationen darüber hat, was der Vermögenswert tut. Die Möglichkeit, ein Asset zu finden - zum Beispiel das Material für eine Figur - ist nützlich, wenn man versucht, die Einrichtung für einige Aspekte der Assets zu automatisieren. Dieses Problem kann zwar durch die Verwendung einer starren Namenskonvention gelöst werden, um sicherzustellen, dass der Pfad leicht abzuleiten ist, aber es ist immer noch anfällig für Fehler. Selbst wenn Sie sich an die Konvention erinnern, sind Tippfehler keine Seltenheit.
Ein interessanter Ansatz zur Lösung dieses Problems ist die Verwendung von Etiketten. Sie können ein Editor-Skript verwenden, das die Pfade von Assets analysiert und ihnen entsprechende Bezeichnungen zuweist. Da die Beschriftungen automatisiert sind, ist es möglich, die genaue Beschriftung einer Anlage herauszufinden. Sie können sogar nach Assets anhand ihrer Bezeichnung suchen, indem Sie AssetDatabase.FindAssets.
Wenn Sie diese Sequenz automatisieren wollen, gibt es eine sehr praktische Klasse namens AssetPostprozessor. Der AssetPostprocessor empfängt verschiedene Meldungen, wenn Unity Assets importiert. Eine davon ist OnPostprocessAllAssetseine Methode, die immer dann aufgerufen wird, wenn Unity den Import von Assets abgeschlossen hat. Sie erhalten dann alle Pfade zu den importierten Assets und können diese Pfade bearbeiten. Sie können eine einfache Methode, wie die folgende, schreiben, um sie zu verarbeiten:
Im Falle des Prototyps sollten wir uns auf die Liste der importierten Assets konzentrieren - sowohl um neue Assets zu erfassen als auch um verschobene Assets. Denn wenn sich der Pfad ändert, müssen wir vielleicht auch die Beschriftungen aktualisieren.
Um die Etiketten zu erstellen, analysieren Sie den Pfad und suchen nach relevanten Ordnern, Präfixen und Suffixen des Namens sowie nach den Erweiterungen. Sobald Sie die Beschriftungen erstellt haben, kombinieren Sie sie zu einer einzigen Zeichenkette und ordnen sie dem Asset zu.
Um die Beschriftungen zuzuweisen, laden Sie das Asset mit AssetDatabase.LoadAssetAtPath und weisen Sie dann die Beschriftungen mit AssetDatabase.SetLabels zu.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Denken Sie daran, dass es wichtig ist, Etiketten nur dann zu setzen, wenn sie sich tatsächlich geändert haben. Das Setzen von Beschriftungen führt zu einem erneuten Import des Assets, so dass Sie dies nur tun sollten, wenn es unbedingt notwendig ist.
Wenn Sie dies überprüfen, ist der Reimport kein Problem: Beschriftungen werden beim ersten Import eines Assets festgelegt und in der .meta-Datei gespeichert, d. h. sie werden auch in Ihrer Versionskontrolle gespeichert. Ein Neuimport wird nur ausgelöst, wenn Sie Ihre Assets umbenennen oder verschieben.
Wenn die oben genannten Schritte abgeschlossen sind, werden alle Assets automatisch beschriftet, wie in dem unten abgebildeten Beispiel.

Beim Importieren von Texturen in ein Projekt müssen in der Regel die Einstellungen für jede Textur angepasst werden. Ist es eine normale Textur? Eine normale Karte? Ein Sprite? Ist es linear oder sRGB? Wenn Sie die Einstellungen eines Asset-Importers ändern möchten, können Sie erneut den AssetPostprozessor verwenden.
In diesem Fall sollten Sie die Funktion OnPreprocessTexture Nachricht verwenden, die unmittelbar vor dem Import einer Textur aufgerufen wird. Hier können Sie die Einstellungen des Importers ändern.
Wenn es darum geht, die richtigen Einstellungen für jede Textur auszuwählen, müssen Sie überprüfen, mit welcher Art von Texturen Sie arbeiten - genau deshalb sind Beschriftungen im ersten Schritt so wichtig.
Mit diesen Informationen können Sie einen einfachen TexturePreprocessor schreiben:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Es ist wichtig, dass Sie dies nur für Texturen ausführen, die das Art-Label haben (unsere eigenen Texturen). Sie erhalten dann einen Verweis auf den Importer, damit Sie alles einrichten können - angefangen bei der Texturgröße.
Der AssetPostprocessor verfügt über eine Kontexteigenschaft, mit der Sie die Zielplattform bestimmen können. So können Sie plattformspezifische Änderungen vornehmen, z. B. die Texturen für mobile Geräte auf eine niedrigere Auflösung einstellen:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Prüfen Sie dann anhand der Beschriftung, ob es sich um eine UI-Textur handelt, und legen Sie sie entsprechend fest:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Für den Rest der Texturen setzen Sie die Werte auf einen Standardwert. Es ist erwähnenswert, dass Albedo die einzige Textur ist, bei der sRGB aktiviert ist:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Dank des obigen Skripts werden die neuen Texturen automatisch mit den richtigen Einstellungen versehen, wenn Sie sie in den Editor ziehen und dort ablegen.
"Channel-Packing" bezeichnet die Kombination verschiedener Texturen zu einer einzigen, indem die verschiedenen Kanäle genutzt werden. Sie ist weit verbreitet und bietet viele Vorteile. Der Wert des roten Kanals ist zum Beispiel metallisch und der Wert des grünen Kanals ist seine Glätte.
Die Kombination aller Texturen zu einer einzigen erfordert jedoch einige zusätzliche Arbeit des Grafikteams. Wenn die Verpackung aus irgendeinem Grund geändert werden muss (z. B. durch eine Änderung des Shaders), muss das Grafikteam alle Texturen, die mit diesem Shader verwendet werden, neu erstellen.
Wie Sie sehen können, gibt es hier noch Raum für Verbesserungen. Der Ansatz, den ich für Channel-Packing verwende, besteht darin, einen speziellen Asset-Typ zu erstellen, bei dem Sie die "rohen" Texturen festlegen und eine kanalgepackte Textur zur Verwendung in Ihren Materialien erzeugen.
Zunächst erstelle ich eine Dummy-Datei mit einer bestimmten Erweiterung und verwende dann einen Skript-Importer der die gesamte Arbeit beim Importieren dieses Assets übernimmt. Das funktioniert folgendermaßen:
- Die Importeure können Parameter haben, z. B. die Texturen, die Sie kombinieren müssen.
- Über den Importer können Sie die Texturen als Abhängigkeit festlegen, so dass das Dummy-Asset jedes Mal neu importiert wird, wenn sich eine der Quelltexturen ändert. So können Sie die erzeugten Texturen entsprechend umgestalten.
- Der Importeur hat eine Version. Wenn Sie die Art und Weise, wie die Texturen gepackt werden, ändern müssen, können Sie den Importer modifizieren und die Version erhöhen. Dies erzwingt eine Erneuerung aller gepackten Texturen in Ihrem Projekt und alles wird sofort auf die neue Art gepackt.
- Ein angenehmer Nebeneffekt der Generierung in einem Importer ist, dass sich die generierten Assets nur im Bibliotheksordner befinden, so dass Ihre Versionskontrolle nicht überlastet wird.
Um dies zu implementieren, erstellen Sie ein ScriptableObject, das die erstellten Texturen enthält und als Ergebnis des Importers dient. In dem Beispiel habe ich diese Klasse TexturePack genannt.
Nach der Erstellung dieses Dokuments können Sie damit beginnen, die Importer-Klasse zu deklarieren und das ScriptedImporterAttribute hinzuzufügen, um die mit dem Importer verbundene Version und Erweiterung zu definieren:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Geben Sie im Importer die Felder an, die Sie verwenden möchten. Sie werden im Inspektor angezeigt, genau wie MonoBehaviours und ScriptableObjects:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Nachdem Sie die Parameter festgelegt haben, erstellen Sie neue Texturen auf der Grundlage der von Ihnen festgelegten Parameter. Beachten Sie jedoch, dass wir im Präprozessor (aus dem vorherigen Abschnitt) isReadable auf True setzen, um dies zu tun.
In diesem Prototyp sehen Sie zwei Texturen: die Albedo-Textur, die im RGB-Kanal die Albedo und im Alpha-Kanal eine Maske für die Anwendung der Player-Farbe enthält, und die Masken-Textur, die im Rot-Kanal den Metallic-Effekt und im Grün-Kanal die Glättung enthält.
Auch wenn dies vielleicht den Rahmen dieses Artikels sprengen würde, wollen wir uns als Beispiel ansehen, wie die Albedo und die Spielermaske kombiniert werden können. Prüfen Sie zunächst, ob die Texturen festgelegt sind, und wenn ja, holen Sie sich ihre Farbdaten. Dann setzen Sie die Texturen als Abhängigkeiten mit AssetImportContext.DependsOnArtifact. Wie bereits erwähnt, wird dadurch eine Neuberechnung des Objekts erzwungen, wenn sich eine der Texturen ändert.
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Sie müssen auch eine neue Textur erstellen. Holen Sie sich dazu die Größe aus dem TexturePreprocessor, den Sie im vorigen Abschnitt erstellt haben, so dass sie den voreingestellten Einschränkungen entspricht:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Geben Sie anschließend alle Daten für die neue Textur ein. Dies könnte durch den Einsatz von Jobs und Burst massiv optimiert werden (aber das würde einen eigenen Artikel erfordern). Hier werden wir eine einfache Schleife verwenden:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Legen Sie diese Daten in der Textur fest:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Nun können Sie die Methode zur Erzeugung einer anderen Textur auf sehr ähnliche Weise erstellen. Sobald dies fertig ist, erstellen Sie den Hauptteil des Importers. In diesem Fall werden wir nur das ScriptableObject erstellen, das die Ergebnisse enthält, die Texturen erstellt und das Ergebnis des Importers über den AssetImportContext festlegt.
Wenn Sie einen Importer schreiben, müssen alle erzeugten Assets mit AssetImportContext.AddObjectToAsset registriert werden, damit sie im Projektfenster erscheinen. Wählen Sie ein Haupt-Asset mit AssetImportContext.SetMainObject. So sieht es aus:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Das Einzige, was noch zu tun ist, ist die Erstellung der Dummy-Assets. Da diese benutzerdefiniert sind, können Sie nicht die CreateAssetMenuAttribut nicht verwenden. Sie müssen sie stattdessen manuell erstellen.
Geben Sie mit dem Attribut MenuItem den vollständigen Pfad zum Menü Assets/Erstellen an. Um das Asset zu erstellen, verwenden Sie ProjectWindowUtil.CreateAssetWithContent, das eine Datei mit dem von Ihnen angegebenen Inhalt erzeugt und dem Benutzer die Möglichkeit gibt, einen Namen für das Asset einzugeben. Das sieht folgendermaßen aus:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Erstellen Sie schließlich die kanalgepackten Texturen.
Die meisten Projekte verwenden benutzerdefinierte Shader. Manchmal werden sie verwendet, um zusätzliche Effekte hinzuzufügen, wie z. B. einen Auflösungseffekt, um besiegte Feinde auszublenden, und manchmal implementieren die Shader einen benutzerdefinierten Kunststil, wie z. B. Toon-Shader. Unabhängig vom Anwendungsfall erstellt Unity neue Materialien mit dem Standard-Shader, und Sie müssen ihn ändern, um den benutzerdefinierten Shader zu verwenden.
In diesem Beispiel hat der für die Einheiten verwendete Shader zwei zusätzliche Funktionen: den Überblendungseffekt und die Spielerfarbe (rot und blau im Video-Prototyp). Wenn Sie diese in Ihrem Projekt implementieren, müssen Sie sicherstellen, dass alle Gebäude und Einheiten den entsprechenden Shader verwenden.
Um zu überprüfen, ob ein Asset bestimmten Anforderungen entspricht - in diesem Fall, ob es den richtigen Shader verwendet - gibt es eine weitere nützliche Klasse: den AssetModificationProcessor. Mit AssetModificationProcessor.OnWillSaveAssetswerden Sie benachrichtigt, wenn Unity im Begriff ist, ein Asset auf die Festplatte zu schreiben. So können Sie überprüfen, ob das Asset korrekt ist, und es korrigieren, bevor es gespeichert wird.
Außerdem können Sie Unity anweisen, das Asset nicht zu speichern, wenn das Problem nicht automatisch behoben werden kann. Um dies zu erreichen, erstellen Sie die Methode OnWillSaveAssets :
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Um die Anlagen zu bearbeiten, prüfen Sie, ob es sich um Materialien handelt und ob sie mit den richtigen Etiketten versehen sind. Wenn sie mit dem unten stehenden Code übereinstimmen, haben Sie den richtigen Shader:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Praktisch dabei ist, dass dieser Code auch bei der Erstellung des Assets aufgerufen wird, so dass das neue Material den richtigen Shader hat.
Als neue Funktion in Unity 2022 haben wir auch Material-Varianten. Materialvarianten sind unglaublich nützlich bei der Erstellung von Materialien für Einheiten. Sie können sogar ein Basismaterial erstellen und die Materialien für jede Einheit davon ableiten, indem Sie die relevanten Felder (wie die Texturen) überschreiben und die restlichen Eigenschaften übernehmen. Dies ermöglicht solide Standardwerte für unsere Materialien, die bei Bedarf aktualisiert werden können.
Der Import von Animationen ist ähnlich wie der Import von Texturen. Es gibt verschiedene Einstellungen, die vorgenommen werden müssen, und einige davon können automatisiert werden.
Unity importiert standardmäßig die Materialien aller FBX-Dateien (.fbx). Bei Animationen befinden sich die Materialien, die Sie verwenden möchten, entweder im Projekt oder im FBX des Meshes. Die zusätzlichen Materialien aus dem Animations-FBX erscheinen jedes Mal, wenn Sie im Projekt nach Materialien suchen, und fügen ein ziemliches Rauschen hinzu, sodass es sich lohnt, sie zu deaktivieren.
Um das Rig einzurichten - d.h. zwischen Humanoid und Generisch zu wählen und in Fällen, in denen wir einen sorgfältig eingerichteten Avatar verwenden, diesen zuzuweisen - wenden Sie denselben Ansatz an, der bei den Texturen angewendet wurde. Für Animationen wird jedoch die folgende Nachricht verwendet AssetPostprocessor.OnPreprocessModel. Dies wird für alle FBX-Dateien aufgerufen, so dass Sie zwischen Animations-FBX-Dateien und Modell-FBX-Dateien unterscheiden müssen.
Dank der Etiketten, die Sie zuvor erstellt haben, sollte dies nicht allzu kompliziert sein. Die Methode beginnt ähnlich wie bei den Texturen:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Als Nächstes wollen Sie das Rig aus dem FBX-Mesh verwenden, also müssen Sie dieses Asset finden. Um die Kühlstelle zu finden, verwenden Sie erneut die Etiketten. Im Fall dieses Prototyps haben Animationen Bezeichnungen, die auf "Animation" enden, während Meshes Bezeichnungen haben, die auf "Modell" enden. Sie können einen einfachen Austausch vornehmen, um das Etikett für Ihr Modell zu erhalten. Sobald Sie die Bezeichnung haben, suchen Sie Ihr Asset mit AssetDatabase.FindAssets mit "l:label-name".
Beim Zugriff auf andere Vermögenswerte gibt es noch etwas anderes zu beachten: Es ist möglich, dass der Avatar in der Mitte des Importprozesses noch nicht importiert wurde, wenn diese Methode aufgerufen wird. Wenn dies der Fall ist, gibt LoadAssetAtPath null zurück und Sie können den Avatar nicht einstellen. Um dieses Problem zu umgehen, setzen Sie eine Abhängigkeit auf den Pfad des Avatars. Die Animation wird wieder importiert, sobald der Avatar importiert ist, und Sie können sie dort einstellen.
Wenn man all dies in Code umsetzt, sieht das in etwa so aus:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Jetzt können Sie die Animationen in den richtigen Ordner ziehen, und wenn Ihr Mesh fertig ist, wird jede einzelne automatisch eingerichtet. Wenn jedoch beim Importieren der Animationen kein Avatar vorhanden ist, kann das Projekt ihn nicht übernehmen, sobald er erstellt ist. Stattdessen müssen Sie die Animation nach der Erstellung manuell wieder importieren. Klicken Sie dazu mit der rechten Maustaste auf den Ordner mit den Animationen und wählen Sie Reimport.
All dies können Sie in dem unten stehenden Beispielvideo sehen.
Mit genau denselben Ideen aus den vorherigen Abschnitten werden Sie die Modelle, die Sie verwenden werden, einrichten wollen. In diesem Fall verwenden Sie AssetPostrocessor.OnPreprocessModel , um die Importeinstellungen für dieses Modell festzulegen.
Für den Prototyp habe ich den Importer so eingestellt, dass er keine Materialien generiert (ich werde die Materialien verwenden, die ich im Projekt erstellt habe), und ich habe überprüft, ob das Modell eine Einheit oder ein Gebäude ist (indem ich die Beschriftung überprüft habe, wie immer). Die Einheiten sind so eingestellt, dass sie einen Avatar erzeugen, aber die Avatar-Erstellung für die Gebäude ist deaktiviert, da die Gebäude nicht animiert sind.
Für Ihr Projekt sollten Sie die Materialien und Animatoren (und alles andere, was Sie hinzufügen möchten) beim Importieren des Modells festlegen. Auf diese Weise ist die vom Importeur erzeugte Fertigstellung sofort einsatzbereit.
Verwenden Sie dazu die Funktion AssetPostprocessor.OnPostprocessModel Methode. Diese Methode wird aufgerufen, nachdem der Import eines Modells abgeschlossen ist. Sie erhält die erstellte Prefab als Parameter, mit dem wir die Prefab nach Belieben verändern können.
Für den Prototyp habe ich das Material und den Animations-Controller anhand der Beschriftung gefunden, so wie ich auch den Avatar für die Animationen gefunden habe. Mit dem Renderer und Animator in der Prefab stelle ich das Material und den Controller wie im normalen Spiel ein.
Sie können das Modell dann in Ihrem Projekt ablegen und es in jede beliebige Szene einfügen. Nur haben wir keine spielrelevanten Komponenten eingestellt, was ich im zweiten Teil dieses Blogs behandeln werde.
Mit diesen fortgeschrittenen Tipps zur Skripterstellung sind Sie so gut wie startklar. Bleiben Sie dran für den nächsten Teil dieser zweiteiligen Reihe Technik aus den Schützengräben Artikel, in dem es um Hacks zum Ausgleichen von Spieldaten und mehr geht.
Wenn Sie den Artikel diskutieren oder Ihre Ideen nach der Lektüre austauschen möchten, besuchen Sie unser Scripting-Forum. Sie können mich auch auf Twitter unter @CaballolD erreichen.
