Einheit: Zeichnen Sie viele Gesundheitsbalken in einem Zug

Vor kurzem musste ich ein Problem lösen, das in vielen Top-View-Spielen häufig vorkommt: eine ganze Reihe feindlicher Gesundheitsbalken auf dem Bildschirm zu rendern. So etwas wie das:


Offensichtlich wollte ich dies so effizient wie möglich tun, vorzugsweise in einem Draw Call. Wie üblich habe ich vor Arbeitsbeginn ein wenig online nach den Entscheidungen anderer gesucht, und die Ergebnisse waren sehr unterschiedlich.

Ich werde niemanden für den Code beschämen, aber es genügt zu sagen, dass einige der Lösungen nicht ganz brillant waren, zum Beispiel hat jemand jedem Feind ein Canvas-Objekt hinzugefügt (was sehr ineffizient ist).

Die Methode, zu der ich als Ergebnis gekommen bin, unterscheidet sich geringfügig von allem, was ich mit anderen gesehen habe, und verwendet überhaupt keine UI-Klassen (einschließlich Canvas). Deshalb habe ich beschlossen, sie für die Öffentlichkeit zu dokumentieren. Und für diejenigen, die den Quellcode lernen wollen, habe ich ihn auf Github gepostet.

Warum nicht Canvas verwenden?


Eine Leinwand für jeden Feind ist offensichtlich eine schlechte Entscheidung, aber ich könnte eine gemeinsame Leinwand für alle Feinde verwenden. Ein einzelner Canvas würde auch zum Rendern von Anrufbatches führen.

Ich mag jedoch nicht die Menge an Arbeit, die in jedem Rahmen im Zusammenhang mit diesem Ansatz ausgeführt wird. Wenn Sie Canvas verwenden, müssen Sie in jedem Frame die folgenden Vorgänge ausführen:

  • Bestimmen Sie, welche der Feinde auf dem Bildschirm angezeigt werden, und wählen Sie jeden von ihnen aus dem Pool-UI-Streifen aus.
  • Projizieren Sie die Position des Feindes in der Kamera, um den Streifen zu positionieren.
  • Ändern Sie die Größe des "Füll" -Teils des Streifens, wahrscheinlich wie bei Image.
  • Ändern Sie höchstwahrscheinlich die Größe der Streifen entsprechend der Art der Feinde; Zum Beispiel sollten große Feinde große Streifen haben, damit es nicht albern aussieht.

Auf jeden Fall würde all dies die Canvas-Geometriepuffer kontaminieren und zu einer Neuerstellung aller Scheitelpunktdaten im Prozessor führen. Ich wollte nicht, dass dies alles für ein so einfaches Element getan wird.

Kurz über meine Entscheidung


Eine kurze Beschreibung meines Arbeitsprozesses:

  • Wir befestigen Objekte mit Energiestreifen in 3D an Feinden.
    • Auf diese Weise können Sie Streifen automatisch anordnen und zuschneiden.
    • Die Position / Größe des Streifens kann je nach Art des Feindes angepasst werden.
    • Wir werden die Streifen im Code mithilfe der Transformation, die noch vorhanden ist, zur Kamera lenken.
    • Der Shader stellt sicher, dass sie immer über allem rendern.
  • Wir verwenden Instancing, um alle Streifen in einem einzigen Draw-Aufruf zu rendern.
  • Wir verwenden einfache prozedurale UV-Koordinaten, um den Grad der Fülle des Streifens anzuzeigen.

Schauen wir uns nun die Lösung genauer an.

Was ist Instanz?


Bei der Arbeit mit Grafiken wird seit langem die Standardtechnik verwendet: Mehrere Objekte werden so kombiniert, dass sie gemeinsame Scheitelpunktdaten und Materialien haben und in einem Zeichenaufruf gerendert werden können. Dies ist genau das, was wir brauchen, denn jeder Draw-Aufruf belastet die CPU und die GPU zusätzlich. Anstatt für jedes Objekt einen einzelnen Zeichenaufruf durchzuführen, rendern wir alle gleichzeitig und verwenden einen Shader, um jeder Kopie Variabilität hinzuzufügen.

Sie können dies manuell tun, indem Sie die Netzscheitelpunktdaten X-mal in einem Puffer duplizieren, wobei X die maximale Anzahl von Kopien ist, die gerendert werden können, und dann das Array von Shader-Parametern verwenden, um jede Kopie zu konvertieren / zu färben / zu variieren. Jede Kopie muss Wissen über die nummerierte Instanz speichern, um diesen Wert als Index des Arrays verwenden zu können. Dann können wir einen indizierten Renderaufruf verwenden, der "Nur auf N rendern" befiehlt, wobei N die Anzahl der Instanzen ist, die im aktuellen Frame tatsächlich benötigt werden, weniger als die maximale Anzahl von X.

Die meisten modernen APIs verfügen bereits über Code dafür, sodass Sie dies nicht manuell tun müssen. Diese Operation wird als "Instanzierung" bezeichnet. Tatsächlich automatisiert es den oben beschriebenen Prozess mit vordefinierten Einschränkungen.

Die Unity-Engine unterstützt auch die Instanzierung . Sie verfügt über eine eigene API und eine Reihe von Shader-Makros, die bei der Implementierung helfen. Es werden beispielsweise bestimmte Annahmen verwendet, dass für jede Instanz eine vollständige 3D-Transformation erforderlich ist. Genau genommen wird es für 2D-Streifen nicht vollständig benötigt - wir können Vereinfachungen vornehmen, aber da dies der Fall ist, werden wir sie verwenden. Dies vereinfacht unseren Shader und bietet auch die Möglichkeit, 3D-Indikatoren zu verwenden, z. B. Kreise oder Bögen.

Klasse schädlich


Unsere Feinde haben eine Komponente namens " Damageable , die ihnen Gesundheit verleiht und ihnen ermöglicht, Schaden durch Kollisionen zu erleiden. In unserem Beispiel ist es ganz einfach:

 public class Damageable : MonoBehaviour { public int MaxHealth; public float DamageForceThreshold = 1f; public float DamageForceScale = 5f; public int CurrentHealth { get; private set; } private void Start() { CurrentHealth = MaxHealth; } private void OnCollisionEnter(Collision other) { // Collision would usually be on another component, putting it all here for simplicity float force = other.relativeVelocity.magnitude; if (force > DamageForceThreshold) { CurrentHealth -= (int)((force - DamageForceThreshold) * DamageForceScale); CurrentHealth = Mathf.Max(0, CurrentHealth); } } } 

HealthBar-Objekt: Position / Drehung


Das Gesundheitsbalkenobjekt ist sehr einfach: Tatsächlich ist es nur ein Quad, das an den Feind gebunden ist.



Wir verwenden die Skala dieses Objekts, um den Streifen lang und dünn zu machen und ihn direkt über dem Feind zu platzieren. Machen Sie sich keine Sorgen um die Drehung, wir werden das Problem mithilfe des Codes beheben, der an das Objekt in HealthBar.cs angehängt ist:

  private void AlignCamera() { if (mainCamera != null) { var camXform = mainCamera.transform; var forward = transform.position - camXform.position; forward.Normalize(); var up = Vector3.Cross(forward, camXform.right); transform.rotation = Quaternion.LookRotation(forward, up); } } 

Dieser Code lenkt das Quad immer in Richtung Kamera. Wir können die Größe ändern und im Shader drehen, aber ich implementiere sie hier aus zwei Gründen.

Erstens verwendet die Unity-Instanzierung immer die vollständige Transformation jedes Objekts, und da wir immer noch alle Daten übertragen, können Sie sie verwenden. Zweitens stellt das Einstellen der Skalierung / Drehung hier sicher, dass das Begrenzungsparallelogramm zum Trimmen des Streifens immer wahr ist. Wenn wir die Aufgabe von Größe und Drehung in die Verantwortung des Shaders legen würden, könnte Unity die Streifen abschneiden, die sichtbar sein sollten, wenn sie sich nahe an den Bildschirmrändern befinden, da die Größe und Drehung ihres Begrenzungsparallelogramms nicht dem entspricht, was wir rendern werden. Natürlich könnten wir unsere eigene Methode zum Abschneiden implementieren, aber normalerweise ist es, wenn möglich, besser, das zu verwenden, was wir haben (Unity-Code ist nativ und hat Zugriff auf mehr räumliche Daten als wir).

Ich werde erklären, wie der Streifen gerendert wird, nachdem wir uns den Shader angesehen haben.

Shader HealthBar


In dieser Version erstellen wir einen einfachen klassischen rot-grünen Streifen.

Ich benutze eine 2x1 Textur mit einem grünen Pixel links und einem roten rechts. Natürlich habe ich das Mipmapping, Filtern und Komprimieren deaktiviert und den Adressierungsmodus-Parameter auf Clamp gesetzt. Dies bedeutet, dass die Pixel in unserem Streifen immer perfekt grün oder rot sind und sich nicht über die Ränder verteilen. Auf diese Weise können wir die Texturkoordinaten im Shader ändern, um die Trennlinie zwischen den roten und grünen Pixeln im Streifen nach unten und oben zu verschieben.

(Da es nur zwei Farben gibt, kann ich einfach die Schrittfunktion im Shader verwenden, um zum einen oder anderen Punkt zurückzukehren. Bei dieser Methode ist es jedoch praktisch, dass Sie eine komplexere Textur verwenden können, wenn Sie dies wünschen. Dies funktioniert ähnlich, während der Übergang ausgeführt wird mittlere Textur.)

Zuerst erklären wir die Eigenschaften, die wir brauchen:

 Shader "UI/HealthBar" { Properties { _MainTex ("Texture", 2D) = "white" {} _Fill ("Fill", float) = 0 } 

_MainTex ist eine rot-grüne Textur und _Fill ist ein Wert von 0 bis 1, wobei 1 die volle Gesundheit ist.

Als nächstes müssen wir den Streifen so anordnen, dass er in der Überlagerungswarteschlange gerendert wird. Dies bedeutet, dass die gesamte Tiefe der Szene ignoriert und über alles gerendert wird:

  SubShader { Tags { "Queue"="Overlay" } Pass { ZTest Off 

Der nächste Teil ist der Shader-Code selbst. Wir schreiben einen Shader ohne Beleuchtung (unbeleuchtet), sodass wir uns nicht um die Integration mit verschiedenen Unity-Oberflächen-Shadern kümmern müssen. Dies sind nur einige Vertex- / Fragment-Shader. Schreiben Sie zuerst Bootstrap:

  CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" 

Dies ist größtenteils ein Standard-Bootstrap, mit Ausnahme von #pragma multi_compile_instancing , das dem Unity-Compiler #pragma multi_compile_instancing , was für die Instanz kompiliert werden muss.

Die Scheitelpunktstruktur muss Instanzdaten enthalten, daher werden wir Folgendes tun:

  struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; 

Wir müssen auch angeben, was genau in den Daten von Instanzen enthalten sein wird, zusätzlich zu den Unity- (Transformations-) Prozessen für uns:

  UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _Fill) UNITY_INSTANCING_BUFFER_END(Props) 

Wir berichten also, dass Unity einen Puffer namens "Props" erstellen muss, um die Daten für jede Instanz zu speichern, und darin einen Float pro Instanz für eine Eigenschaft namens _Fill .

Sie können mehrere Puffer verwenden. Es lohnt sich, wenn Sie mehrere Eigenschaften mit unterschiedlichen Frequenzen aktualisieren lassen. Wenn Sie sie teilen, können Sie beispielsweise einen Puffer nicht ändern, wenn Sie einen anderen ändern, was effizienter ist. Das brauchen wir aber nicht.

Unser Vertex-Shader erledigt die Standardarbeit fast vollständig, da Größe, Position und Drehung bereits zur Transformation übertragen werden. Dies wird mit UnityObjectToClipPos implementiert, das automatisch die Transformation jeder Instanz verwendet. Man könnte sich vorstellen, dass dies ohne Instanzierung normalerweise eine einfache Verwendung einer einzelnen Matrixeigenschaft wäre. Bei Verwendung der Instanz innerhalb der Engine sieht es jedoch wie ein Array von Matrizen aus, und Unity wählt unabhängig eine für diese Instanz geeignete Matrix aus.

Außerdem müssen Sie die UV- _Fill ändern, um die Position des Übergangspunkts gemäß der Eigenschaft _Fill von rot nach grün zu _Fill . Hier ist das relevante Code-Snippet:

  UNITY_SETUP_INSTANCE_ID(v); float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill); // generate UVs from fill level (assumed texture is clamped) o.uv = v.uv; o.uv.x += 0.5 - fill; 

UNITY_SETUP_INSTANCE_ID und UNITY_ACCESS_INSTANCED_PROP erledigen die ganze Magie, indem sie aus dem konstanten Puffer für diese Instanz auf die richtige Version der Eigenschaft _Fill zugreifen.

Wir wissen, dass im Normalzustand die UV-Koordinaten des Quadranten das gesamte Texturintervall abdecken und dass sich die Trennlinie des Streifens horizontal in der Mitte der Textur befindet. Daher verschieben kleine mathematische Berechnungen den Streifen horizontal nach links oder rechts, und der Klemmwert der Textur stellt die Füllung des verbleibenden Teils sicher.

Der Fragment-Shader könnte nicht einfacher sein, da alle Arbeiten bereits ausgeführt wurden:

  return tex2D(_MainTex, i.uv); 

Der vollständige Kommentar-Shader-Code ist im GitHub-Repository verfügbar.

Healthbar-Material


Dann ist alles einfach - wir müssen unserem Strip nur das Material zuweisen, das dieser Shader verwendet. Es muss fast nichts mehr getan werden. Wählen Sie einfach den gewünschten Shader im oberen Teil aus, weisen Sie eine rot-grüne Textur zu und aktivieren Sie vor allem das Kontrollkästchen „GPU-Instanz aktivieren“ .

Bild

HealthBar Fill Property Update


Wir haben also das _Fill , den Shader und das zu rendernde Material. Jetzt müssen wir die _Fill Eigenschaft für jede Instanz _Fill . Wir machen das in HealthBar.cs wie folgt:

  private void UpdateParams() { meshRenderer.GetPropertyBlock(matBlock); matBlock.SetFloat("_Fill", damageable.CurrentHealth / (float)damageable.MaxHealth); meshRenderer.SetPropertyBlock(matBlock); } 

Wir CurrentHealth die CurrentHealth Klasse in einen Wert von 0 bis 1 um und teilen ihn durch MaxHealth . Dann übergeben wir es mit MaterialPropertyBlock an die Eigenschaft _Fill .

Wenn Sie MaterialPropertyBlock zum Übertragen von Daten an Shader verwendet haben, auch ohne Instanz, müssen Sie diese untersuchen. Dies wird in der Unity-Dokumentation nicht ausführlich erläutert, ist jedoch die effizienteste Methode zum Übertragen von Daten von jedem Objekt zu Shadern.

In unserem Fall werden bei Verwendung der Instanzierung die Werte für alle Integritätsbalken in einen konstanten Puffer gepackt, sodass sie alle zusammen übertragen und gleichzeitig gezeichnet werden können.

Hier gibt es fast nichts außer einem Boilerplate zum Einstellen von Variablen, und der Code ist ziemlich langweilig. Weitere Informationen finden Sie im GitHub-Repository .

Demo


Das GitHub-Repository verfügt über eine Testdemo, in der ein Haufen böser blauer Würfel durch heldenhafte rote Kugeln zerstört wird (Hurra!). Dabei wird der durch die im Artikel beschriebenen Streifen verursachte Schaden genommen. Demo in Unity 2018.3.6f1 geschrieben.

Der Effekt der Verwendung von Instanzen kann auf zwei Arten beobachtet werden:

Statistik-Panel


Klicken Sie nach dem Klicken auf "Spielen" auf die Schaltfläche "Statistiken" über dem Spielfeld. Hier können Sie sehen, wie viele Draw Calls dank Instancing gespeichert werden:

Bild

Nach dem Start des Spiels können Sie auf das HealthBar-Material klicken und das Kontrollkästchen "GPU-Instanz aktivieren" deaktivieren. Danach wird die Anzahl der gespeicherten Anrufe auf Null reduziert.

Frame-Debugger


Gehen Sie nach dem Start des Spiels zu Fenster> Analyse> Frame-Debugger und klicken Sie im angezeigten Fenster auf "Aktivieren".

Unten links sehen Sie alle durchgeführten Rendering-Vorgänge. Beachten Sie, dass es für Feinde und Granaten viele verschiedene Herausforderungen gibt (wenn Sie möchten, können Sie auch Instanzen für sie implementieren). Wenn Sie nach unten scrollen, sehen Sie den Punkt "Mesh-Gesundheitsleiste (instanziiert zeichnen)".

Dieser einzelne Aufruf rendert alle Streifen. Wenn Sie auf diesen Vorgang und dann auf den Vorgang klicken, werden alle Streifen ausgeblendet, da sie in einem Aufruf gezeichnet werden. Wenn Sie sich im Frame-Debugger befinden, deaktivieren Sie das Kontrollkästchen GPU-Instanz aktivieren im Material. Sie sehen, dass eine Zeile in mehrere umgewandelt wurde, und nachdem Sie das Flag erneut in eine gesetzt haben.

So erweitern Sie dieses System


Wie ich bereits sagte, da diese Gesundheitsstreifen echte Objekte sind, hindert Sie nichts daran, einfache 2D-Streifen in etwas Komplexeres zu verwandeln. Sie können Halbkreise unter Feinden sein, die in einem Bogen abnehmen, oder rotierende Rauten über ihren Köpfen. Mit demselben Ansatz können Sie immer noch alle in einem Aufruf rendern.

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


All Articles