Hallo! In diesem Artikel geht es um das Erstellen eigener visueller Komponenten in der Benutzeroberfläche am Beispiel einer Komponente zur Visualisierung eines Partikelsystems in
Canvas 'e.
Diese Informationen sind nützlich für die Implementierung verschiedener Effekte in der Benutzeroberfläche und können auch zum Generieren oder Optimieren eines Netzes verwendet werden.
Ein bisschen Theorie oder wo man mit der Erstellung von Komponenten beginnen kann
Die Basis für die Benutzeroberfläche von Unity ist
Canvas . Er wird vom Render-System verwendet, um die "mehrschichtige" Geometrie gemäß der internen Hierarchie der UI-Elemente anzuzeigen.
Jede visuelle Benutzeroberflächenkomponente muss von der
Graphic- Klasse (oder ihrer abgeleiteten
MaskableGraphic- Klasse) erben, die alle erforderlichen Daten an die
CanvasRenderer- Komponente
übergibt , um sie zu rendern. Daten werden in der
OnPopulateMesh- Methode erstellt, die jedes Mal aufgerufen wird, wenn eine Komponente ihre Geometrie aktualisieren muss (z. B. beim
Ändern der
Größe eines Elements).
VertexHelper wird als Parameter übergeben, der beim Generieren eines Netzes für die Benutzeroberfläche hilft.
Komponentenerstellung
Basis
Wir beginnen die Implementierung mit der Erstellung eines
UIParticleSystem- Skripts, das von der
MaskableGraphic- Klasse erbt.
MaskableGraphic ist eine Ableitung der
Graphic- Klasse und bietet darüber hinaus die Arbeit mit Masken.
Überschreiben Sie die
OnPopulateMesh- Methode. Die Grundlage für die Zusammenarbeit mit
VertexHelper zum Generieren der Scheitelpunkte eines
Netzpartikelsystems sieht folgendermaßen aus:
public class UIParticleSystem : MaskableGraphic { protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); ... int particlesCount = ... ; for (int i = 0; i < particlesCount; i++) { Color vertexColor = ... ; Vector2[] vertexUV = ... ; UIVertex[] quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 vertixPosition = ... ; quadVerts[j] = new UIVertex() { position = vertixPosition, color = vertexColor, uv0 = vertexUV }; } vh.AddUIVertexQuad(quadVerts); } } }
Zunächst müssen Sie den
VertextHelper aus vorhandenen Daten
löschen, indem Sie die
Clear- Methode aufrufen. Danach können Sie es mit neuen Daten über die Peaks füllen. Hierzu wird die
AddUIVertexQuad- Methode verwendet, mit der Sie Informationen zu 4 Scheitelpunkten gleichzeitig hinzufügen können. Diese Methode wurde aus Gründen der Benutzerfreundlichkeit ausgewählt Jedes Teilchen ist ein Rechteck. Jeder Scheitelpunkt wird durch ein
UIVertex- Objekt beschrieben. Von allen Parametern müssen wir nur die Position, Farbe und einige Koordinaten des
UV- Scans ausfüllen.
VertexHelperVertexHelper verfügt über eine Reihe von Methoden zum Hinzufügen von Vertex-Informationen sowie über ein Paar zum Empfangen aktueller Daten. Für komplexere Geometrien wäre die beste Lösung die Auswahl der AddUIVertexStream- Methode, die eine Liste von Scheitelpunkten und eine Liste von Indizes akzeptiert.
Da sich bei jedem Frame die Position der Partikel, ihre Farbe und andere Parameter ändern, sollte auch das Netz für das Rendern aktualisiert werden.
Zu diesem
Zweck ruft jeder Frame die
SetVerticesDirty- Methode auf, die das Flag für die Notwendigkeit des Nachzählens neuer Daten setzt, was zum Aufruf der
OnPopulateMesh- Methode führt. Wenn sich die Eigenschaften eines Materials ändern, müssen Sie die
SetMaterialDirty- Methode
aufrufen .
protected void Update() { SetVerticesDirty(); }
Überschreiben Sie die
mainTexture- Eigenschaft. Es gibt an, welche Textur an den
CanvasRenderer übergeben und im Material verwendet wird, die
_MainTex- Shader-
Eigenschaft . Erstellen Sie dazu ein
ParticleImage- Feld, das von der
mainTexture- Eigenschaft zurückgegeben wird.
public Texture ParticleImage; public override Texture mainTexture { get { return ParticleImage; } }
Partikelsystem
Die Daten zum Generieren von Netzscheitelpunkten stammen aus der
ParticleSystem- Komponente, die alle Berechnungen zur Position der Partikel, ihrer Größe, Farbe usw. durchführt.
Die ParticleSystemRenderer-Komponente wird am Partikel-Rendering beteiligt sein, das deaktiviert werden
muss .
Daher sind auch andere Komponenten für das Erstellen des Netzes und dessen Rendering in der Benutzeroberfläche verantwortlich -
UIParticleSystem und
CanvasRenderer .
Erstellen Sie die für den Betrieb erforderlichen Felder und initialisieren Sie sie in der
Awake- Methode.
UIBehaviourAwake muss , wie die meisten Methoden, hier neu definiert werden, da sie in UIBehaviour als virtuell aufgeführt sind . Die UIBehaviour- Klasse selbst ist abstrakt und enthält praktisch keine Arbeitslogik, ist jedoch grundlegend für die Graphic- Klasse.
private ParticleSystem _particleSystem; private ParticleSystemRenderer _particleSystemRenderer; private ParticleSystem.MainModule _main; private ParticleSystem.Particle[] _particles; protected override void Awake() { base.Awake(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; int maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; }
Das Feld
_particles wird zum Speichern von
Partikeln im ParticleSystem verwendet
_main wird zur Vereinfachung mit dem
MainModule- Modul verwendet.
Fügen wir die OnPopulateMesh-Methode hinzu und nehmen alle erforderlichen Daten direkt aus dem Partikelsystem. Erstellen Sie die Hilfsvariablen
Vector3 [] _quadCorners und
Vector2 [] _simpleUV .
_quadCorners enthält die Koordinaten der 4 Ecken des Rechtecks relativ zur Mitte des Partikels. Die Anfangsgröße jedes Partikels wird als Quadrat mit den Seiten 1x1 betrachtet.
_simpleUV - Koordinaten des
UV- Scans, in diesem Fall verwenden alle Partikel dieselbe Textur ohne Verschiebungen.
private Vector3[] _quadCorners = new Vector3[] { new Vector3(-.5f, -.5f, 0), new Vector3(-.5f, .5f, 0), new Vector3(.5f, .5f, 0), new Vector3(.5f, -.5f, 0) }; private Vector2[] _simpleUV = new Vector2[] { new Vector2(0,0), new Vector2(0,1), new Vector2(1,1), new Vector2(1,0), };
protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) { var particle = _particles[i]; Vector3 particlePosition = particle.position; Color vertexColor = particle.GetCurrentColor(_particleSystem) * color; Vector3 particleSize = particle.GetCurrentSize3D(_particleSystem); Vector2[] vertexUV = _simpleUV; Quaternion rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); UIVertex[]quadVerts = new UIVertex[4]; for (int j = 0; j < 4; j++) { Vector3 cornerPosition = rotation * Vector3.Scale(particleSize, _quadCorners[j]); Vector3 vertexPosition = cornerPosition + particlePosition; vertexPosition.z = 0; quadVerts[j] = new UIVertex(); quadVerts[j].color = vertexColor; quadVerts[j].uv0 = vertexUV[j]; quadVerts[j].position = vertexPosition; } vh.AddUIVertexQuad(quadVerts); } }
vertexPositionZunächst wird die lokale Position des Scheitelpunkts relativ zum Zentrum des Partikels unter Berücksichtigung seiner Größe (Operation Vector3.Scale (Partikelgröße, _quadCorners [j]) ) und Rotation (Multiplikation der Quaternionsrotation mit einem Vektor) berechnet. Nachdem die Position des Partikels selbst zum Ergebnis hinzugefügt wurde
Erstellen wir nun eine einfache Benutzeroberfläche für den Test mit Standardkomponenten.

Fügen
Sie der ParticleSystem- Komponente ein
UIParticleSystem hinzu
Führen Sie die Szene aus und überprüfen Sie das Ergebnis der Komponente.

Partikel werden entsprechend ihrer Position in der Hierarchie angezeigt und berücksichtigen die verwendeten Masken. Wenn Sie die Auflösung des Bildschirms und seine Proportionen sowie die
Rendermodus- Eigenschaft von
Canvas ändern, verhalten sich die Partikel ähnlich wie alle anderen visuellen Komponenten in
Canvas und werden nur darin angezeigt.
SimulationSpace
Weil Wenn wir das Partikelsystem in der Benutzeroberfläche platzieren, liegt ein Problem mit dem Parameter
SimulationSpace vor . Bei der Simulation im Weltraum werden Partikel nicht dort angezeigt, wo sie sollten. Daher addieren wir die Berechnung der Partikelposition in Abhängigkeit vom Parameterwert.
protected override void OnPopulateMesh(VertexHelper vh) { ... Vector3 particlePosition; switch (_main.simulationSpace) { case ParticleSystemSimulationSpace.World: particlePosition = _rectTransform.InverseTransformPoint(particle.position); break; case ParticleSystemSimulationSpace.Local: particlePosition = particle.position; break; case ParticleSystemSimulationSpace.Custom: if (_main.customSimulationSpace != null) particlePosition = _rectTransform.InverseTransformPoint( _main.customSimulationSpace.TransformPoint(particle.position) ); else particlePosition = particle.position; break; default: particlePosition = particle.position; break; } ... }
Simulieren Sie die ParticleSystemRenderer-Eigenschaften
Jetzt implementieren wir einen Teil der
ParticleSystemRenderer- Funktionalität. Die Eigenschaften von
RenderMode ,
SortMode ,
Pivot .
Renderer
Wir beschränken uns auf die Tatsache, dass sich Partikel immer nur in der Ebene der Leinwand befinden. Daher implementieren wir nur zwei Werte:
Billboard und
StretchedBillboard .
Erstellen wir dazu unsere Aufzählung
CanvasParticleSystemRenderMode .
public enum CanvasParticleSystemRenderMode { Billboard = 0, StretchedBillboard = 1 }
public CanvasParticleSystemRenderMode RenderMode; public float SpeedScale = 0f; public float LengthScale = 1f; protected override void OnPopulateMesh(VertexHelper vh) { ... Quaternion rotation; switch (RenderMode) { case CanvasParticleSystemRenderMode.Billboard: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; case CanvasParticleSystemRenderMode.StretchedBillboard: rotation = Quaternion.LookRotation(Vector3.forward, particle.totalVelocity); float speed = particle.totalVelocity.magnitude; particleSize = Vector3.Scale(particleSize, new Vector3(LengthScale + speed * SpeedScale, 1f, 1f)); rotation *= Quaternion.AngleAxis(90, Vector3.forward); break; default: rotation = Quaternion.AngleAxis(particle.rotation, Vector3.forward); break; } ... }
Wenn Sie den Parameter
StretchedBillboard auswählen, hängt die Partikelgröße von den
Parametern LengthScale und
SpeedScale ab , und ihre Drehung wird nur in Bewegungsrichtung gerichtet.

Sortiermodus
Erstellen
Sie auf ähnliche Weise die
CanvasParticlesSortMode- Enumeration. und wir implementieren nur die Sortierung nach Partikellebensdauer.
public enum CanvasParticlesSortMode { None = 0, OldestInFront = 1, YoungestInFront = 2 }
public CanvasParticlesSortMode SortMode;
Zum Sortieren müssen Daten zur Partikellebensdauer gespeichert werden, die in der Variablen
_particleElapsedLifetime gespeichert werden. Die Sortierung wird mit der
Array.Sort- Methode implementiert.
private float[] _particleElapsedLifetime; protected override void Awake() { ... _particles = new ParticleSystem.Particle[maxCount]; _particleElapsedLifetime = new float[maxCount]; } protected override void OnPopulateMesh(VertexHelper vh) { vh.Clear(); int particlesCount = _particleSystem.GetParticles(_particles); for (int i = 0; i < particlesCount; i++) _particleElapsedLifetime[i] = _particles[i].startLifetime - _particles[i].remainingLifetime; switch (SortMode) { case CanvasParticlesSortMode.None: break; case CanvasParticlesSortMode.OldestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount,Comparer<float>.Default); Array.Reverse(_particles, 0, particlesCount); break; case CanvasParticlesSortMode.YoungestInFront: Array.Sort(_particleElapsedLifetime, _particles, 0, particlesCount, Comparer<float>.Default); break; } ... }
Pivot
Erstellen Sie ein
Pivot- Feld, um den Mittelpunkt des Partikels zu versetzen.
public Vector3 Pivot = Vector3.zero;
Und bei der Berechnung der Scheitelpunktposition addieren wir diesen Wert.
Vector3 cornerPosition = Vector3.Scale(particleSize, _quadCorners[j] + Pivot); Vector3 vertexPosition = rotation * cornerPosition + particlePosition; vertexPosition.z = 0;
Einstellbare Größe
Wenn das Element, an das das Partikelsystem angeschlossen ist, keine festen Größen hat oder sich zur Laufzeit ändern kann, wäre es schön, die Größe des Partikelsystems anzupassen. Machen wir die Quellform proportional zur Größe des Elements.
Die
OnRectTransformDimensionsChange- Methode
wird aufgerufen, wenn die
Größe der
RectTransform- Komponente
geändert wird. Wir definieren diese Methode neu, indem wir eine Skalierungsänderung an der Form implementieren, um sie an die Abmessungen der
RectTransform anzupassen .
Erstellen Sie zunächst die Variablen für die
RectTransform- Komponente und das
ShapeModule- Modul. Erstellen Sie die Variable
ScaleShapeByRectTransform , um die
Formskalierung zu deaktivieren.
Die Skalierung sollte auch durchgeführt werden, wenn die Komponente aktiviert wird, um ihre anfängliche Skalierung festzulegen.
private RectTransform _rectTransform; private ParticleSystem.ShapeModule _shape; public bool ScaleShapeByRectTransform; protected override void Awake() { ... _rectTransform = GetComponent<RectTransform>(); _shape = _particleSystem.shape; ... } protected override void OnEnable() { base.OnEnable(); ScaleShape(); } protected override void OnRectTransformDimensionsChange() { base.OnRectTransformDimensionsChange(); ScaleShape(); } protected void ScaleShape() { if (!ScaleShapeByRectTransform) return; Rect rect = _rectTransform.rect; var scale = Quaternion.Euler(_shape.rotation) * new Vector3(rect.width, rect.height, 0); scale = new Vector3(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)); _shape.scale = scale; }
Bei der Berechnung ist die Drehung der
Form zu berücksichtigen. Die Werte des Endergebnisses müssen modulo genommen werden, da sie sich als negativ herausstellen können, was die Bewegungsrichtung der Partikel beeinflusst.
Führen Sie zum
Testen des
Vorgangs die Animation zur
Größenänderung von RectTransform mit einem daran angeschlossenen Partikelsystem aus.

Initialisierung
Damit das Skript im Editor korrekt ausgeführt wird und Fehler beim Aufrufen der
OnRectTransformDimensionsChange- Methode
vermieden werden, wird die Initialisierung von Variablen in einer separaten Methode
ausgegeben .
Fügen Sie seinen Aufruf den
Methoden OnPopulateMesh und
OnRectTransformDimensionsChange hinzu .
ExecuteInEditModeSie müssen das Attribut ExecuteInEditMode nicht angeben, weil Graphic implementiert dieses Verhalten bereits und das Skript wird im Editor ausgeführt.
private bool _initialized; protected void Initialize() { if (_initialized) return; _initialized = true; _rectTransform = GetComponent<RectTransform>(); _particleSystem = GetComponent<ParticleSystem>(); _main = _particleSystem.main; _textureSheetAnimation = _particleSystem.textureSheetAnimation; _shape = _particleSystem.shape; _particleSystemRenderer = GetComponent<ParticleSystemRenderer>(); _particleSystemRenderer.enabled = false; _particleSystemRenderer.material = null; var maxCount = _main.maxParticles; _particles = new ParticleSystem.Particle[maxCount]; _particlesLifeProgress = new float[maxCount]; _particleRemainingLifetime = new float[maxCount]; } protected override void Awake() { base.Awake(); Initialize(); } protected override void OnPopulateMesh(VertexHelper vh) { Initialize(); ... } protected override void OnRectTransformDimensionsChange() { #if UNITY_EDITOR Initialize(); #endif ... }
Die
OnRectTransformDimensionsChange- Methode kann früher als
Awake aufgerufen werden . Daher ist es bei jedem Aufruf erforderlich, die Variablen zu initialisieren.
Leistung und Optimierung
Dieses Rendern von Partikeln ist etwas teurer als die Verwendung von
ParticleSystemRenderer , was eine umsichtigere Verwendung erfordert, insbesondere auf Mobilgeräten.
Es ist auch erwähnenswert, dass, wenn mindestens eines der
Canvas- Elemente als
Dirty markiert ist, dies zur Neuberechnung der gesamten
Canvas- Geometrie und zur Generierung neuer Rendering-Befehle führt. Wenn die Benutzeroberfläche viele komplexe Geometrien und ihre Berechnungen enthält, lohnt es sich, sie in mehrere eingebettete Leinwände aufzuteilen.

PS: Alle Quellcodes und Demos sind
Git-Links .
Der Artikel wurde vor fast einem Jahr veröffentlicht, nachdem ParticleSystem in der Benutzeroberfläche verwendet werden musste. Zu diesem Zeitpunkt fand ich keine ähnliche Lösung, und die verfügbaren waren für die aktuelle Aufgabe nicht optimal. Einige Tage vor der Veröffentlichung dieses Artikels fand ich beim Sammeln von Material versehentlich eine ähnliche Lösung mit der Graphic.OnPopulateMesh-Methode. Daher halte ich es für notwendig, einen
Link zum Repository anzugeben.