Modulare Sprite-Charaktere und ihre Animation

Dieser Blog-Beitrag ist ganz meinem Charakter-Animationssystem gewidmet. Er enthält nützliche Tipps und Code-Schnipsel.

In den letzten zwei Monaten habe ich bis zu 9 neue Spieleraktionen (so lustige Dinge wie Blockieren mit einem Schild, Ausweichen vor einem Sprung und Waffen), 17 neue tragbare Gegenstände, 3 Rüstungssätze (Platte, Seide und Leder) und 6 Arten von Frisuren erstellt. Ich habe auch die gesamte Automatisierung und die Tools fertiggestellt, sodass bereits alles im Spiel verwendet wird. In dem Artikel werde ich erzählen, wie ich das erreicht habe!


Ich hoffe, diese Informationen sind nützlich und beweisen, dass es nicht notwendig ist, ein Genie zu sein, um solche Tools / Automatisierungen unabhängig zu erstellen.

Kurzbeschreibung


Zunächst wollte ich prüfen, ob es möglich ist, übereinanderliegende Sprites mit synchronisierten Animatoren zu kombinieren, um einen modularen Charakter mit austauschbaren Frisuren, Geräten und tragbaren Gegenständen zu schaffen. Ist es möglich, handgezeichnete Pixelanimationen mit einem wirklich anpassbaren Charakter zu kombinieren?

Natürlich werden solche Funktionen in 3D- und 2D-Spielen mit vorgerenderten Sprites oder in 2D-Spielen mit Skelettanimation aktiv verwendet, aber meines Wissens gibt es nicht viele Spiele, die manuell erstellte Animationen und modulare Charaktere kombinieren (normalerweise, weil sich herausstellt, dass dies der Fall ist zu eintönig).


Ich habe dieses alte GIF meines ersten Monats mit Unity entdeckt. Tatsächlich war dieses modulare Sprite eines meiner ersten Experimente in der Spieleentwicklung!

Ich habe einen Prototyp mit dem Unity-Animationssystem erstellt und dann ein Hemd, eine Hose, eine Frisur und drei Elemente hinzugefügt, um das Konzept zu testen. Dies erforderte 26 separate Animationen.

Zu dieser Zeit habe ich alle meine Animationen in Photoshop erstellt und mich nicht um die Automatisierung des Prozesses gekümmert, daher war es sehr langweilig. Dann dachte ich: "Also, die Grundidee hat funktioniert, später werde ich neue Animationen und Geräte hinzufügen." Es stellte sich heraus, dass "später" einige Jahre später ist.

Im März dieses Jahres habe ich das Design einer großen Menge an Rüstungen gezeichnet (siehe meinen vorherigen Beitrag) und festgestellt, wie dieser Prozess bequemer gestaltet werden kann. Ich habe die Implementierung weiter verschoben, weil ich trotz der Automatisierung nervös war, dass nichts funktionieren würde.

Ich hatte erwartet, dass ich die Anpassung des Charakters aufgeben und den einzigen Hauptcharakter erstellen müsste, wie in den meisten Spielen mit manueller Animation. Aber ich hatte einen Aktionsplan und es war Zeit zu prüfen, ob ich dieses Monster besiegen konnte!



Spoiler: Alles ist super geworden. Unten werde ich meine *** Geheimnisse *** enthüllen

Modulares Sprite-System


I. Kennen Sie Ihre Grenzen


Zuvor habe ich viele Kunst- und Zeitkontrolltests durchgeführt, um herauszufinden, wie lange solche Arbeiten dauern können und ob ein ähnliches Qualitätsniveau für mich erreichbar ist.

Ich schrieb alle meine Ideen für Animationen auf, stellte sie in einer Tabelle zusammen und ordnete sie nach verschiedenen Kriterien wie Nützlichkeit, Schönheit und wiederholter Verwendung. Zu meiner Überraschung war das allererste auf dieser Liste die Besetzungsanimation des Gegenstands (Tränke, Bomben, Messer, Äxte, Ball).

Ich habe für jede Animation eine numerische Bewertung erstellt und alles mit schlechter Leistung aufgegeben. Anfangs hatte ich vor, 6 Rüstungssätze zu erstellen, erkannte aber schnell, dass es zu viel war, und warf drei Typen aus.

Der Aspekt der Zeiterfassung erwies sich als sehr wichtig, und ich empfehle dringend, ihn zur Beantwortung von Fragen wie "Wie viele Feinde kann ich mir leisten, um ihn im Spiel zu erstellen?" Zu verwenden. Nach nur wenigen Tests gelang es mir, eine ziemlich genaue Schätzung zu extrapolieren. Bei weiteren Arbeiten an Animationen verfolgte ich weiterhin die Zeit und überarbeitete meine Erwartungen.

Ich werde eine Kopie meines Arbeitstagebuchs der letzten zwei Monate teilen. Bitte beachten Sie, dass diese Zeit zusätzlich zu meiner üblichen Arbeit ist, bei der ich 30 Stunden pro Woche verbringe:

https://docs.google.com/spreadsheets/d/1Nbr7lujZTB4pWMsuedVcgBYS6n5V-rHrk1PxeGxr6Ck/edit?usp=sharing

II. Ändern der Palette für eine bessere Zukunft


Wenn Sie die Farben im Sprite-Design mit Bedacht verwenden, können Sie ein Sprite zeichnen und durch Ändern der Palette viele verschiedene Variationen erstellen. Sie können nicht nur Farben ändern, sondern auch verschiedene Ein- und Ausschaltelemente erstellen (z. B. Farben durch Transparenz ersetzen).

Jeder Rüstungssatz besteht aus 3 Variationen. Durch Mischen des oberen und unteren Teils erhalten Sie viele Kombinationen. Ich plane, ein System zu implementieren, in dem Sie einen Satz Rüstungen für das Aussehen des Charakters und einen anderen für seine Eigenschaften (wie in Terraria) sammeln können.


Dabei war ich angenehm überrascht von den entdeckten merkwürdigen Kombinationen. Wenn Sie die Plattenoberseite mit einer Seidenunterseite verbinden, können Sie etwas im Stil eines Kriegsmagiers erhalten.

Es ist am besten, die Paletten mithilfe der Farben zu ändern, die den Wert im Sprite codieren, damit Sie sie später verwenden können, um die tatsächliche Farbe aus der Palette zu ermitteln. Ich weiß, dass ich ein bisschen vereinfache. Hier ist ein Video, um Ihnen den Einstieg zu erleichtern:


Ich werde nicht alles im Detail erklären, sondern über Möglichkeiten zur Implementierung dieser Technik in Unity und deren Vor- und Nachteile sprechen.

1. Suchen Sie die Textur für jede Palette


Dies ist die beste Strategie, um Variationen von Feinden, Hintergründen und allem zu erstellen, wo viele Sprites dieselbe Palette / dasselbe Material haben. Verschiedene Materialien können nicht in Chargen gruppiert werden, selbst wenn sie dasselbe Sprite / denselben Atlas verwenden. Das Arbeiten mit Texturen ist sehr schmerzhaft, aber Sie können Paletten in Echtzeit ändern, indem Sie Materialien mit SpriteRenderer.sharedMaterial.SetTexture oder MaterialPropertyBlock ersetzen, wenn Sie für jede Instanz des Materials unterschiedliche Paletten benötigen. Hier ist ein Beispiel für eine Shader-Fragment-Funktion:

sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = tex2D(_PaletteTex, half2(lookup.r * (_PaletteTex_TexelSize.x / 0.00390625f), 0.5)); color.a *= lookup.a; return color * input.color; } 

2. Farbreihe


Ich habe mich für diese Entscheidung entschieden, weil ich die Paletten jedes Mal ersetzen musste, wenn sich das Aussehen des Charakters ändert (z. B. beim Anlegen von Gegenständen), und einige Paletten dynamisch erstellen musste (um die vom Spieler gewählten Haar- und Hautfarben anzuzeigen). Es schien mir, dass es zur Laufzeit und im Editor für diese Zwecke viel einfacher wäre, mit Arrays zu arbeiten.

Code:

 sampler2D _MainTex; half4 _Colors[32]; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half4 color = _Colors[round(lookup.r * 255)]; color.a *= lookup.a; return color * input.color; } 

Ich habe meine Paletten als ScriptableObject-Typ dargestellt und sie mit dem MonoBehaviour-Tool bearbeitet. Nachdem ich lange Zeit an der Bearbeitung von Paletten gearbeitet hatte, um Animationen in Aseprite zu erstellen, erkannte ich, welche Tools ich benötigte, und schrieb diese Skripte entsprechend. Wenn Sie Ihr eigenes Tool zum Bearbeiten von Paletten schreiben möchten, empfehle ich auf jeden Fall folgende Funktionen:

- Aktualisieren von Paletten auf verschiedenen Materialien beim Bearbeiten von Farben, um Änderungen in Echtzeit anzuzeigen.

- Zuweisen von Namen und Ändern der Reihenfolge der Farben in der Palette (verwenden Sie das Feld, um den Farbindex zu speichern, nicht dessen Reihenfolge im Array).

- Wählen und bearbeiten Sie mehrere Farben gleichzeitig. (Tipp: Sie können die Farbfelder in Unity kopieren und einfügen: Klicken Sie einfach auf eine Farbe, kopieren Sie, klicken Sie auf eine andere Farbe, fügen Sie sie ein - jetzt sind sie gleich!)

- Überlagerungsfarbe auf die gesamte Palette auftragen

- Palette in Textur aufnehmen

3. Eine einzelne Suchtextur für alle Paletten


Wenn Sie die Paletten im laufenden Betrieb wechseln möchten, aber gleichzeitig einen Stapel erstellen müssen, um die Anzahl der Draw-Aufrufe zu verringern, können Sie diese Technik verwenden. Es kann für mobile Plattformen nützlich sein, aber die Verwendung ist ziemlich unpraktisch.

Zunächst müssen Sie alle Paletten in eine große Textur packen. Anschließend verwenden Sie die in der SpriteRenderer-Komponente angegebene Farbe (AKA-Scheitelpunktfarbe), um die Linie zu bestimmen, die aus der Palettentextur in den Shader gelesen werden soll. Das heißt, die Palette dieses Sprites wird über SpriteRenderer.color gesteuert. Die Scheitelpunktfarbe ist die einzige SpriteRenderer-Eigenschaft, die geändert werden kann, ohne die Bedingung zu verletzen (vorausgesetzt, alle Materialien sind gleich).

In den meisten Fällen ist es am besten, den Index über den Alphakanal zu steuern, da Sie wahrscheinlich keine Sprites mit unterschiedlicher Transparenz benötigen.

Code:

 sampler2D _MainTex; sampler2D _PaletteTex; float4 _PaletteTex_TexelSize; half4 frag(v2f input) : SV_TARGET { half4 lookup = tex2D(_MainTex, input.uv); half2 paletteUV = half2( lookup.r * _(PaletteTex_TexelSize.x / 0.00390625f), input.color.a * _(PaletteTex_TexelSize.y / 0.00390625f) ) half4 color = tex2D(_PaletteTex, paletteUV); color.a *= lookup.a; color.rgb *= input.color.rgb; return color; } 


Die Wunder des Ersetzens von Paletten und Sprite-Ebenen. So viele Kombinationen.

III. Automatisieren Sie alles und verwenden Sie die richtigen Werkzeuge.


Um diese Funktion zu implementieren, war eine Automatisierung unbedingt erforderlich, da ich ungefähr 300 Animationen und Tausende von Sprites erhielt.

Mein erster Schritt bestand darin, einen Exporter für Aseprite zu erstellen, um mein verrücktes Sprite-Layer-Schema über eine praktische Befehlszeilenschnittstelle zu verwalten. Dies ist nur ein Perl-Skript, das alle Ebenen und Beschriftungen in meiner Aseprite-Datei umgeht und Bilder in ein bestimmtes Verzeichnis und eine bestimmte Namensstruktur exportiert, damit ich sie später lesen kann.

Dann schrieb ich einen Importeur für Unity. Aseprite zeigt eine praktische JSON-Datei mit Rahmendaten an, sodass Sie Animationselemente programmgesteuert erstellen können. Der Umgang mit Aseprite JSON und das Schreiben dieses Datentyps erwiesen sich als ziemlich mühsam, daher bringe ich sie hierher. Sie können sie einfach mit JsonUtility.FromJson <AespriteData> in Unity laden. Denken Sie daran, Aseprite mit der Option --format 'json-array' auszuführen.

Code:

 [System.Serializable] public struct AespriteData { [System.Serializable] public struct Size { public int w; public int h; } [System.Serializable] public struct Position { public int x; public int y; public int w; public int h; } [System.Serializable] public struct Frame { public string filename; public Position frame; public bool rotated; public bool trimmed; public Position spriteSourceSize; public Size sourceSize; public int duration; } [System.Serializable] public struct Metadata { public string app; public string version; public string format; public Size size; public string scale; } public Frame[] frames; public Metadata meta; } 

Auf der Unity-Seite hatte ich an zwei Stellen ernsthafte Probleme: Laden / Schneiden eines Sprite-Blatts und Erstellen eines Animationsclips. Ein klares Beispiel würde mir sehr helfen. Hier ist ein Codeausschnitt meines Importeurs, damit Sie nicht so sehr leiden:

Code:

 TextureImporter textureImporter = AssetImporter.GetAtPath(spritePath) as TextureImporter; textureImporter.spriteImportMode = SpriteImportMode.Multiple; SpriteMetaData[] spriteMetaData = new SpriteMetaData[aespriteData.frames.Length]; // Slice the spritesheet according to the aesprite data. for (int i = 0; i < aespriteData.frames.Length; i++) { AespriteData.Position spritePosition = aespriteData.frames[i].frame; spriteMetaData[i].name = aespriteData.frames[i].filename; spriteMetaData[i].rect = new Rect(spritePosition.x, spritePosition.y, spritePosition.w, spritePosition.h); spriteMetaData[i].alignment = (int)SpriteAlignment.Custom; // Same as "Pivot" in Sprite Editor. spriteMetaData[i].pivot = new Vector2(0.5f, 0f); // Same as "Custom Pivot" in Sprite Editor. Ignored if alignment isn't "Custom". } textureImporter.spritesheet = spriteMetaData; AssetDatabase.ImportAsset(spritePath, ImportAssetOptions.ForceUpdate); Object[] assets = AssetDatabase.LoadAllAssetsAtPath(spritePath); // The first element in this array is actually a Texture2D (ie the sheet itself). for (int i = 1; i < assets.Length; i++) { sprites[i - 1] = assets[i] as Sprite; } // Create the animation. AnimationClip clip = new AnimationClip(); clip.frameRate = 40f; float frameLength = 1f / clip.frameRate; ObjectReferenceKeyframe[] keyframes = new ObjectReferenceKeyframe[aespriteData.frames.Length + 1]; // One extra keyframe is required at the end to express the last frame's duration. float time = 0f; for (int i = 0; i < keyframes.Length; i++) { bool lastFrame = i == keyframes.Length - 1; ObjectReferenceKeyframe keyframe = new ObjectReferenceKeyframe(); keyframe.value = sprites[lastFrame ? i - 1 : i]; keyframe.time = time - (lastFrame ? frameLength : 0f); keyframes[i] = keyframe; time += lastFrame ? 0f : aespriteData.frames[i].duration / 1000f; } EditorCurveBinding binding = new EditorCurveBinding(); binding.type = typeof(SpriteRenderer); binding.path = ""; binding.propertyName = "m_Sprite"; AnimationUtility.SetObjectReferenceCurve(clip, binding, keyframes); AssetDatabase.CreateAsset(clip, "Assets/Animation/" + name + ".anim"); AssetDatabase.SaveAssets(); 

Wenn Sie es noch nicht getan haben, glauben Sie mir - es ist sehr einfach, eigene Tools zu erstellen. Der einfachste Trick besteht darin, ein GameObject in der Szene mit dem MonoBehaviour zu platzieren, an das das Attribut [ExecuteInEditMode] angehängt ist. Fügen Sie einen Knopf hinzu und Sie sind bereit für den Kampf! Denken Sie daran, dass Ihre persönlichen Werkzeuge nicht gut aussehen müssen, sondern rein nützlich sein können.

Code:

 [ExecuteInEditMode] public class MyCoolTool : MonoBehaviour { public bool button; void Update() { if (button) { button = false; DoThing(); } } } 

Bei der Arbeit mit Sprites ist es recht einfach, Standardaufgaben zu automatisieren (z. B. das Erstellen von Palettentexturen oder das Ersetzen von Farben durch Stapel in mehreren Sprite-Dateien). Hier ist ein Beispiel, anhand dessen Sie lernen können, wie Sie Ihre Sprites ändern.

Code:

 string path = "Assets/Whatever/Sprite.png"; Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(path); TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter; if (!textureImporter.isReadable) { textureImporter.isReadable = true; AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); } Color[] pixels = texture.GetPixels(0, 0, texture.width, texture.height); for (int i = 0; i < pixels.Length; i++) { // Do something with the pixels, eg replace one color with another. } texture.SetPixels(pixels); texture.Apply(); textureImporter.isReadable = false; // Make sure textures are marked as un-readable when you're done. There's a performance cost to using readable textures in your project that you should avoid unless you plan to change a sprite at runtime. byte[] bytes = ImageConversion.EncodeToPNG(texture); File.WriteAllBytes(Application.dataPath + path.Substring(6), bytes); AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate); 

Wie ich Mecanim-Möglichkeiten überwunden habe: Beschwerde


Im Laufe der Zeit wurde der Prototyp eines modularen Sprite-Systems, das ich mit Mecanim erstellt habe, zum größten Problem beim Upgrade von Unity, da sich die API ständig änderte und schlecht dokumentiert war. Im Fall einer einfachen Zustandsmaschine ist es ratsam, den Status jedes Clips abzufragen oder die Clips zur Laufzeit zu ändern. Aber nein! Aus Leistungsgründen backt Unity Clips in ihrem Zustand und zwingt uns, ein ungeschicktes Neudefinitionssystem zu verwenden, um sie zu ändern.

Mecanim selbst ist kein so schlechtes System, aber es scheint mir, dass es nicht gelingt, sein deklariertes Hauptmerkmal - die Einfachheit - zu verwirklichen. Die Idee der Entwickler war es, das Komplizierte und Schmerzhafte (Scripting) durch etwas Einfaches (Visual State Machine) zu ersetzen. Allerdings:

- Jede nicht triviale endliche Zustandsmaschine verwandelt sich schnell in ein wildes Netz von Knoten und Verbindungen, deren Logik über verschiedene Schichten verteilt ist.

- Einfache Anwendungsfälle werden durch allgemeine Systemanforderungen behindert. Um eine oder zwei Animationen abzuspielen, müssen Sie einen neuen Controller erstellen und Zustände / Übergänge zuweisen. Natürlich gibt es eine übermäßige Verschwendung von Ressourcen.

- Es ist lustig, dass Sie daher immer noch Code schreiben müssen, denn damit die Zustandsmaschine etwas Interessantes tun kann, benötigen Sie ein Skript, das Animator.SetBool und ähnliche Methoden aufruft.

- Für die mehrfache Verwendung der Zustandsmaschine mit anderen Clips müssen Sie sie duplizieren und die Clips manuell ersetzen. In Zukunft müssen Sie an mehreren Stellen Änderungen vornehmen.

- Wenn Sie zur Laufzeit den Status ändern möchten, treten Probleme auf. Die Lösung ist entweder eine schlechte API oder ein verrückter Graph mit einem Knoten für jede mögliche Animation.


Die Geschichte, wie Firewatch-Entwickler in die Hölle der visuellen Skripterstellung gerieten. Das Lustige ist, dass wenn der Sprecher die einfachsten Beispiele zeigt, sie immer noch verrückt aussehen. Die Zuschauer stöhnen buchstäblich um 12:41 Uhr . Wenn Sie enorme Wartungskosten hinzufügen, werden Sie verstehen, warum ich dieses System nicht mag.

Viele dieser Probleme sind nicht einmal die Schuld der Mecanim-Entwickler, sondern einfach das natürliche Ergebnis inkompatibler Ideen: Sie können kein gemeinsames und gleichzeitig einfaches System erstellen, und die Beschreibung der Logik mithilfe von Bildern ist schwieriger als nur Wörter / Symbole (erinnert sich jemand an die UML-Flussdiagramme?) . Ich habe mich an ein Fragment aus Zack McClendons Bericht bei Practice NYC 2018 erinnert , und wenn Sie Zeit haben, empfehle ich Ihnen, das gesamte Video anzuschauen!

Ich habe es jedoch herausgefunden. Visuelles Scripting wird immer von aggressiven Nerds "Write Your Own Engine" zensiert, die die Bedürfnisse des Künstlers nicht verstehen. Darüber hinaus kann nicht geleugnet werden, dass der größte Teil des Codes wie unverständlicher Fachjargon aussieht.

Wenn Sie bereits ein kleiner Programmierer sind und Spiele mit Sprites erstellen, müssen Sie möglicherweise zweimal überlegen. Als ich anfing, war ich mir sicher, dass ich nie etwas Besseres über die Engine schreiben könnte als Unity-Entwickler.

Und weißt du was? Es stellte sich heraus, dass der Sprite-Animator nur ein Skript ist, das das Sprite nach einer bestimmten Anzahl von Sekunden ändert. Wie dem auch sei, ich musste immer noch meine eigenen schreiben. Seitdem habe ich meinem spezifischen Projekt Animationsereignisse und andere Funktionen hinzugefügt, aber die Basisversion, die ich an einem halben Tag geschrieben habe, deckt 90% meiner Bedürfnisse ab. Es besteht aus nur 120 Zeilen und kann hier kostenlos heruntergeladen werden: https://pastebin.com/m9Lfmd94 . Vielen Dank für das Lesen meines Artikels. Bis dann!

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


All Articles