Wie ich 2D-Schatten in Unity gemacht habe

Was kommt einem Indie-Spieleentwickler als Erstes in den Sinn, wenn er ein Feature hinzufügen muss, von dessen Implementierung er keine Ahnung hat? Natürlich wird er nach Spuren derer suchen, die diesen Weg bereits gegangen sind und sich die Mühe gemacht haben, ihre Erfahrungen aufzuschreiben. Also habe ich vor einiger Zeit angefangen, Schatten in meinem Spiel zu erzeugen. Die richtigen Informationen - in Form von Artikeln, Lektionen und Leitfäden - zu finden, war nicht schwierig. Zu meiner Überraschung stellte ich jedoch fest, dass keine der beschriebenen Lösungen einfach zu mir passte. Nachdem ich meine eigenen erkannt hatte, beschloss ich, der Welt davon zu erzählen.

Es ist im Voraus zu warnen, dass dieser Text keine Art Ultimatum-Leitfaden oder Meisterklasse darstellt. Die Methode, die ich verwendet habe, ist möglicherweise nicht universell, keineswegs die effektivste und deckt nicht die Aufgabe ab, zweidimensionale Schatten vollständig zu erzeugen. Es ist eher eine Geschichte darüber, auf welche Tricks ein unerfahrener Entwickler in meinem Gesicht zurückgreifen musste, um ein Ergebnis zu erzielen, das seine Anforderungen erfüllt.

Das Ergebnis selbst liegt vor Ihnen:



Und die Details des Weges zu seiner Erreichung warten auf Sie unter dem Schnitt.

Ein bisschen über das Spiel
Dwarfinator ist ein zweidimensionaler Basisverteidigungs- / Side-Scroll-Shooter, der mit Blick auf die Segmente Mobile und Desktop entwickelt wurde. Das Gameplay besteht aus der systematischen Zerstörung feindlicher Wellen in zwei abwechselnden Modi - Verteidigung und Jagd. Der Fortschritt eines Spielers besteht darin, einen „Panzer“ zu pumpen, indem er verschiedene Elemente wie Waffen, Motoren und Räder verbessert und ersetzt sowie das Level erhöht und aktive und passive Fähigkeiten erlernt. Das Fortschreiten der Umgebung beinhaltet eine konstante Zunahme der Anzahl von Mobs in der Welle, das Hinzufügen neuer Arten von Feinden zu der Welle, während sie sich durch den Ort bewegen, und den sukzessiven Wechsel mehrerer Orte, von denen jeder seine eigene Gruppe von Gegnern hat.

Erklärung des Problems


Zum Zeitpunkt der Entscheidung, dem Spiel Schatten hinzuzufügen, hatte ich also:

  • Ort in Form von zwei Sprites, einer zur Anzeige hinter Mobs und anderen Entities, der zweite zur Anzeige vor ihnen;



  • Mobs und statische zerstörbare Objekte, die ständig animiert sind und aus verschiedenen Sprites in einer Menge von einigen bis einigen Dutzend bestehen;



  • Muscheln, eigene und feindliche, in den meisten Fällen entweder durch ein Sprite oder durch ein Partikelsystem dargestellt, in letzterem Fall war kein Schatten erforderlich;



  • ein Tank, der aus mehreren Teilen besteht, die nach demselben Schema wie Mobs zusammengesetzt sind;



  • Wände mit mehreren festen Zuständen, die wiederum eine Reihe separater Sprites sind.



Für all dies wurden die einfachsten Schatten benötigt, die die Konturen des Objekts wiederholten und von einer einzigen festen Lichtquelle aus geworfen wurden.

Gleichzeitig sollte man ein ausgeprägtes Verhältnis zur Produktivität haben. Aufgrund der Besonderheiten des Genres und der Besonderheiten seiner Implementierung befinden sich die meisten Objekte, die Schatten werfen, jederzeit direkt auf dem Bildschirm. Und ihre Gesamtzahl kann mehr als einhundert betragen, wenn wir über Spieleinheiten sprechen, und ein paar Tausend, wenn wir über einzelne Sprites sprechen.

Implementierung


Eigentlich stellte sich heraus, dass Dwarfinator grob gesagt ein 2.5D-Spiel ist. Die überwiegende Mehrheit der Objekte existiert im zweidimensionalen Raum mit der X- und Y-Achse, und die Z-Achse wird äußerst selten verwendet. Visuell und teilweise im Gameplay wird die Y-Achse verwendet, um sowohl die Höhe als auch die Tiefe anzuzeigen, und zwar auf dieselbe Weise in die virtuellen Y- und Z-Achsen.

Tatsächlich brauchte ich keine ehrliche Beleuchtung, es reichte aus, um manuell einen Schatten für jedes Objekt zu erstellen. Das Einfachste, was mir in den Sinn kam, war, einfach eine Kopie hinter jedes Objekt zu platzieren, das im dreidimensionalen Raum gedreht wurde, um einen Ort auf der Oberfläche zu simulieren. Alle Sprites dieses Pseudoschattens wurden auf Schwarz gesetzt, während die hierarchische Struktur des Schattenbesitzers beibehalten wurde, sodass er vom selben Animator synchron mit dem Besitzer animiert werden konnte.

Eine solche synchrone Animation sah ungefähr so ​​aus:



Der Schatten erforderte jedoch Transparenz. Die einfachste Lösung bestand darin, sie für jedes Schattensprite festzulegen. Eine solche Implementierung sah jedoch nicht zufriedenstellend aus - Sprites überlappten sich und bildeten weniger transparente Bereiche an der Überlagerungsstelle.

Der Screenshot unten zeigt, wie der Schatten mehrerer durchscheinender Segmente aussieht. Die verwendeten Schattenverzerrungsparameter sind ebenfalls sichtbar: Drehung entlang der X-Achse um -50 Grad, Drehung entlang der Y-Achse um -140 Grad und Skalierung entlang der X-Achse um das 1,3-fache gegenüber dem übergeordneten Objekt.



Es wurde deutlich, dass dem Schatten als festem Objekt Transparenz auferlegt werden sollte. Das erste Experiment zu diesem Thema hing am Schatten der Kamera und renderte diesen Schatten in RenderTexture, das dann als Material verwendet wurde, das an das übergeordnete Element des Ebenenschattens angehängt wurde. Er konnte bereits ohne Probleme Transparenz einstellen. Die Schatten selbst befanden sich außerhalb des Rahmens, um eine Überlappung der Kameraerfassungsbereiche zu vermeiden. Der Ansatz funktionierte, aber es stellte sich heraus, dass bereits ein paar Dutzend Schatten ernsthafte Leistungsprobleme verursachten, hauptsächlich aufgrund der Anzahl der Kameras auf der Bühne. Darüber hinaus gingen eine Reihe von Animationen von einer signifikanten Bewegung einzelner Mob-Sprites im Rahmen ihres Stammobjekts aus, wodurch ein Kamerabereich gefunden werden sollte, der die Größe des realen Bildes zu einem bestimmten Zeitpunkt erheblich überschreiten würde.

Die Lösung wurde schnell gefunden - wenn Sie nicht jeden Schatten mit einer separaten Kamera zeichnen können - warum nicht alle Schatten mit einer Kamera zeichnen? Dazu musste lediglich ein separater Bereich der Szene unter dem Schatten platziert werden, der etwas höher als das Sichtfeld der Hauptkamera ist, eine zusätzliche Kamera auf diesen Bereich richten und die Ausgabe zwischen dem Standort und anderen Objekten anzeigen.

Unten sehen Sie ein Beispiel für die Ausgabe dieser Kamera:



Die Produktivität einer solchen Implementierung litt viel weniger, sodass die Lösung als funktionierend angesehen und auf alle Mobs, statischen Objekte und Shells angewendet wurde. Diesem folgte der Ort des Sprites. Es war unmöglich, ein Sprite für alle Objekte zu verwenden, da es zuvor implementiert wurde. Die Verwendung einer Kopie eines Objekts als Schatten funktioniert nur, wenn das Objekt vollständig flach ist. Sogar beim Erstellen von Schatten für Mobs wurde festgestellt, dass Berührungspunkte mit der Oberfläche, die entlang der dritten Koordinate beabstandet sind, die Richtigkeit des Schattens in Bezug auf diese Punkte verletzen.

Der folgende Screenshot zeigt ein Beispiel für einen solchen Verstoß. Die Ferse des Pöbels wird als Berührungspunkt mit der Oberfläche genommen, aber die Schatten der Füße sind bereits jenseits der Füße selbst.



Und wenn Sie bei den Beinen des Ogers die Position des Schattens noch leicht verändern und das Problem maskieren können, dann besteht für einige Dutzend Baumstämme keine Chance. Alle Standortobjekte, die einen Schatten werfen sollten, sollten zu einem separaten GameObject gemacht werden. Dies ist genau das, was ich getan habe, indem ich Kopien der entsprechenden zerstörbaren Objekte auf dem vorgefertigten Standort platziert und Skripte deaktiviert habe, die an dieser Position nicht verwendet werden. Gleichzeitig wurde es dadurch möglich, sie in die allgemeine Sortierung von Szenenobjekten einzubeziehen, und Muscheln, die außerhalb des Ortes flogen, wurden nicht mehr streng auf alle Objekte gezeichnet, sondern flogen zwischen ihnen hin und her. Außerdem wurde es möglich, die Objekte selbst zu animieren.

Aber dann erwartete mich ein neuer Ärger. Mit Schatten und Dutzenden neuer Objekte hat sich die maximale Anzahl von GameObjects gleichzeitig auf der Bühne und damit die Anzahl der Animator- und SpriteRenderer-Komponenten mehr als verdoppelt. Als ich die ganze Welle von Mobs an dem Ort veröffentlichte, der ungefähr 150 Teile umfasste, zeigte mir Profiler vorwurfsvoll ungefähr 40 ms, die nur zum Rendern und Animieren verschwanden, und die Framerate variierte im Allgemeinen um 10. Ich optimierte meine eigenen Skripte verzweifelt und kämpfte für jede Millisekunde. aber das war nicht genug.

Auf der Suche nach zusätzlichen Optimierungstools bin ich auf die umfangreichen Dokumentationen und Anleitungen für die dynamische Stapelverarbeitung gestoßen.

Ein bisschen mehr über das Batching
Kurz gesagt, Batching ist ein Mechanismus zur Minimierung der Anzahl der Draw-Aufrufe und damit der Zeit, die zum Zeitpunkt des Renderns des Frames für die Interaktion zwischen CPU und GPU aufgewendet wird. Anstatt jedes Element einzeln zum Rendern zu senden, werden ähnliche Elemente gleichzeitig gruppiert und gezeichnet. Im Falle von Unity versucht die Engine selbst, diesen Mechanismus maximal zu nutzen, und der Entwickler muss fast keine zusätzlichen Maßnahmen ergreifen.

Frame-Debugger hat gezeigt, dass ich bestenfalls die Details jedes Objekts oder Mobs separat habe. Nachdem ich für den ersten und zweiten Teil des Atlas Sprites erstellt hatte, erreichte ich mit nur wenigen Draw Calls Schattenschatten, aber die Besitzer dieser Schatten weigerten sich hartnäckig, sich selbst zu bekämpfen.

Experimente in einer separaten Szene haben gezeigt, dass die dynamische Stapelverarbeitung unterbrochen wird, wenn Objekte eine SortingGroup-Komponente haben, mit der ich die Anzeige von Objekten auf dem Bildschirm sortiert habe. Es war zwar möglich, darauf zu verzichten, aber theoretisch konnte das separate Einstellen der Sortierwerte für jedes Sprite und Partikelsystem in einem Objekt sogar noch teurer ausfallen als das Fehlen einer Stapelverarbeitung.

Aber etwas hat mich heimgesucht. Das Schattenobjekt, das ein Nachkomme des Hostobjekts in der realen Szene ist, gehörte technisch zur gleichen SortingGroup, es gab jedoch keine Probleme mit der dynamischen Spiegelung von Schattenobjekten. Der einzige Unterschied bestand darin, dass die Hostobjekte von der Hauptkamera direkt auf dem Bildschirm gezeichnet wurden und die Schattenobjekte zuerst in RenderTexture gerendert wurden.

Das war der Haken. Was genau der Grund für dieses Verhalten ist, ist dem Internet unbekannt, aber beim Rendern der Kamerabilder in RenderTexture hat SortingGroup die Stapelverarbeitung nicht mehr unterbrochen. Die Entscheidung schien sehr seltsam, unlogisch und im Allgemeinen die am meisten belastende. Indem ich das Rendern von Objekten mit der gleichen Methode wie das Rendern von Schatten implementiere und so zusätzlich zur Schattenebene eine Objektebene erhalten habe, habe ich bereits akzeptable Leistungswerte erzielt.

Der Screenshot unten zeigt ein Beispiel für das Rendern eines Entity-Layers.



Im Allgemeinen sieht das Rendern einer bestimmten Entität in der Y-Koordinate folgendermaßen aus:

  1. Das Unternehmen befindet sich bei Y - 20;
  2. Eine Entität wird von einer Kamera gerendert, die diese Koordinate in einer RenderTexture für Entitäten beobachtet.
  3. Der Objektschatten wird bei Y + 20 platziert.
  4. Ein Schatten eines Objekts wird von einer Kamera gezeichnet, die diese Koordinate in einer RenderTexture für Schatten beobachtet.
  5. Die Hauptkamera zeichnet das Sprite für die Hauptposition auf dem Bildschirm - das einzige Element, das derzeit direkt auf dem Bildschirm gerendert wird.
  6. Die Hauptkamera zeichnet eine Ebene auf dem Bildschirm mit RenderTexture-Schatten als Material.
  7. Die Hauptkamera zeichnet eine Ebene auf dem Bildschirm mit einer RenderTexture von Objekten als Material.

Eine solche Torte.

In der Abbildung unten ist die Kamera des Editors auf den dreidimensionalen Modus eingestellt, um die Position der Ebenen relativ zueinander zu veranschaulichen.



Nuancen


Da sich jedoch herausstellte, dass die Entscheidung auf andere Unternehmen übertragen wurde, wurden im allgemeinen Fall nicht alle möglichen Szenarien berücksichtigt. Es gab zum Beispiel Objekte, die sich in einer gewissen Höhe zur Oberfläche befanden, insbesondere Muscheln und einige Zwischensequenzen. Zusätzlich hatten die Geschosse die Fähigkeit, sich in Abhängigkeit von der Richtung ihrer Bewegung auf dem Bildschirm zu drehen, weshalb es zusätzlich zur Einstellung des Schnittpunkts des Objekts und seines Schattens erforderlich war, das rotierende Teil als separates untergeordnetes Objekt auszuwählen, um die Rotationslogik des Projektils und deren Animation zu korrigieren.

Der folgende Screenshot zeigt ein Beispiel für die Drehung von Muscheln und ihren Schatten.



Fliegende Charaktere können sich wie geplante fliegende Mobs auch innerhalb ihrer virtuellen Y-Koordinaten bewegen, was die Erstellung eines Mechanismus zur Berechnung der Position des Schattens aus der Position seines Besitzers auf der virtuellen Y-Achse erforderlich machte.

Das folgende GIF zeigt ein Beispiel für das Verschieben eines Objekts in der Höhe.



Ein anderer Fall, der aus dem allgemeinen Konzept herauskam, war ein Panzer. Im Gegensatz zu allen anderen Objekten hat der Panzer entlang der virtuellen Z-Achse eine sehr beachtliche Größe, und die Gesamtimplementierung der Schatten erfordert, wie bereits erwähnt, dass das Objekt fast flach ist. Der einfachste Weg, dies zu umgehen, bestand darin, Schattenformen für einzelne Teile des Tanks manuell zu zeichnen, da Sie alles auf der Schattenebene platzieren konnten.

Für die korrekte Konstruktion von handgezeichneten Schatten musste ich ein Liniendesign auf der Grundlage eines Screenshots eines vorhandenen Schattens erstellen, der im folgenden Screenshot zu sehen ist.



Wenn Sie diese Struktur so skalieren und platzieren, dass sich der obere Teil an einem Punkt des übergeordneten Objekts und der untere Teil am Kontaktpunkt mit der Oberfläche befindet, wird in der rechten Ecke der Struktur die Stelle angezeigt, an der sich der entsprechende Schattenpunkt befinden sollte. Nachdem auf diese Weise mehrere wichtige Punkte projiziert wurden, ist es nicht schwierig, den gesamten Schatten darauf aufzubauen.

Darüber hinaus können einzelne Teile des Panzers unterschiedliche Höhen für die Befestigung von Kinderteilen aufweisen, was wie bei fliegenden Charakteren und Mobs eine Anpassung der Schattenposition jedes einzelnen Teils erforderlich macht.

Der Screenshot unten zeigt den Panzer, seine Schattenmontage und ist auch in Form von separaten Teilen.



Schatten der Wände stellten sich als separater Schmerz heraus. Zu Beginn der Arbeiten an den Schatten waren die Wände von der gleichen Art wie die Details des Panzers - ein Objekt aus mehreren Dutzend verschiedenen Sprites. Die Wände hatten jedoch mehrere Zustände, die vom Animator gesteuert wurden.

Nach gründlichen Überlegungen kam ich zu dem Schluss, dass das Konzept der Wände geändert werden muss. Infolgedessen wurden die Wände in Abschnitte unterteilt, von denen jeder einen eigenen Satz von Zuständen, einen eigenen Animator und einen eigenen Schatten hat. Dies ermöglichte es, für Mobs, die parallel zur X-Achse verlaufen, den gleichen Ansatz für die Erstellung von Schatten zu verwenden wie für Mobs. Für Abschnitte, die dieser Regel nicht entsprachen, mussten sie sich etwas Eigenes einfallen lassen. In einigen Fällen musste ich meinen eigenen Animator für den Abschnittsschatten erstellen und die Position der Sprites manuell festlegen.

Im Fall des in der Abbildung unten gezeigten Abschnitts wird der Schatten beispielsweise erzeugt, indem für jedes einzelne Protokoll eine Verzerrung anstelle des gesamten Abschnitts angewendet wird.



Fazit


Das ist in der Tat alles. Trotz all der oben genannten Nuancen wurde die ursprüngliche Aufgabe vollständig erledigt, und jetzt kann mein Projekt recht anständig aussehende Schatten aufweisen, wenn auch von etwas zweifelhafter Herkunft. Ich hoffe, dass dank dieses Artikels für den nächsten Indie-Entwickler, der mir eine ähnliche Frage gestellt hat, das Internet ein wenig nützlicher wird, wenn nicht als Beispiel, dann zumindest als Fehler eines anderen für Ihr eigenes Lernen.

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


All Articles