Erstellen Sie visuelle Komponenten in der Unity-Benutzeroberfläche. Partikelsystem

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.

Bild


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.

VertexHelper
VertexHelper 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.

UIBehaviour
Awake 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); } } 

vertexPosition
Zunä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.

Bild

Fügen Sie der ParticleSystem- Komponente ein UIParticleSystem hinzu

Bild

Führen Sie die Szene aus und überprüfen Sie das Ergebnis der Komponente.

Bild

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.

Bild

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.

Bild

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 .

ExecuteInEditMode
Sie 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.

Bild

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.

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


All Articles