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) {
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);
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“ .

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:

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.