Crear componentes visuales en Unity UI. Sistema de partículas

Hola Este artículo trata sobre la creación de sus propios componentes visuales en la IU utilizando el ejemplo de un componente para visualizar un sistema de partículas en Canvas 'e.

Esta información será útil para implementar varios efectos en la interfaz de usuario, y también puede usarse para generar una malla u optimizarla.

imagen


Un poco de teoría o dónde comenzar la creación de componentes


La base de la interfaz de usuario de Unity es Canvas . Es él quien es utilizado por el sistema de renderizado para mostrar la geometría "multicapa", de acuerdo con la jerarquía interna de los elementos de la interfaz de usuario.
Cualquier componente de la interfaz de usuario visual debe heredar de la clase Graphic (o su clase derivada MaskableGraphic ), que pasa todos los datos necesarios al componente CanvasRenderer para representarlo. Los datos se crean en el método OnPopulateMesh , que se llama cada vez que un componente necesita actualizar su geometría (por ejemplo, al cambiar el tamaño de un elemento). VertexHelper se pasa como parámetro, lo que ayuda a generar una malla para la interfaz de usuario.

Creación de componentes


Base


Comenzamos la implementación creando un script UIParticleSystem que hereda de la clase MaskableGraphic . MaskableGraphic es un derivado de la clase Graphic y además proporciona trabajo con máscaras. Invalide el método OnPopulateMesh . La base para trabajar con VertexHelper para generar los vértices de una malla de sistema de partículas de malla se verá así:

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

Primero, debe borrar el VertextHelper de los datos existentes llamando al método Clear . Después de eso, puede comenzar a llenarlo con nuevos datos sobre los picos. Para esto, se utilizará el método AddUIVertexQuad , que le permite agregar información sobre 4 vértices a la vez. Este método ha sido seleccionado para facilitar su uso, ya que Cada partícula es un rectángulo. Cada vértice se describe mediante un objeto UIVertex . De todos los parámetros, necesitamos completar solo la posición, el color y algunas coordenadas del escaneo uv .

VertexHelper
VertexHelper tiene un conjunto completo de métodos para agregar información de vértices, así como un par para recibir datos actuales. Para una geometría más compleja, la mejor solución sería seleccionar el método AddUIVertexStream , que acepta una lista de vértices y una lista de índices.

Dado que cada cuadro cambiará la posición de las partículas, su color y otros parámetros, la malla para su representación también debe actualizarse.
Para hacer esto, cada fotograma llamará al método SetVerticesDirty , que establecerá el indicador sobre la necesidad de contar nuevos datos, lo que conducirá a la llamada al método OnPopulateMesh . De manera similar para un material, si sus propiedades cambian, debe llamar al método SetMaterialDirty .

 protected void Update() { SetVerticesDirty(); } 

Invalide la propiedad mainTexture . Indica qué textura se pasará al CanvasRenderer y se usará en el material, la propiedad del sombreador _MainTex . Para hacer esto, cree un campo ParticleImage , que será devuelto por la propiedad mainTexture .

 public Texture ParticleImage; public override Texture mainTexture { get { return ParticleImage; } } 

Sistema de partículas


Los datos para generar vértices de malla se tomarán del componente ParticleSystem , que participa en todos los cálculos sobre la ubicación de las partículas, su tamaño, color, etc.
El componente ParticleSystemRenderer, que deberá deshabilitarse, está involucrado en la representación de partículas, por lo que otros componentes, UIParticleSystem y CanvasRenderer, serán responsables de crear la malla y representarla en la interfaz de usuario.

Cree los campos necesarios para la operación e inicialícelos en el método Despertar .

Comportamiento UIB
Awake , como la mayoría de los métodos, debe redefinirse aquí, ya que se enumeran como virtuales en UIBehaviour . La clase UIBehaviour en sí misma es abstracta y prácticamente no contiene ninguna lógica de trabajo, pero es básica para la clase Graphic .

 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]; } 

El campo _partículas se usará para almacenar partículas ParticleSystem , y
_main se usa por conveniencia con el módulo MainModule .

Agreguemos el método OnPopulateMesh, tomando todos los datos necesarios directamente del sistema de partículas. Cree las variables auxiliares Vector3 [] _quadCorners y Vector2 [] _simpleUV .

_quadCorners contiene las coordenadas de las 4 esquinas del rectángulo, en relación con el centro de la partícula. El tamaño inicial de cada partícula se considera como un cuadrado con lados 1x1.
_simpleUV - coordenadas del escaneo uv , en este caso todas las partículas usan la misma textura sin ningún desplazamiento.

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

posición del vértice
Primero, se calcula la posición local del vértice con respecto al centro de la partícula, teniendo en cuenta su tamaño (operación Vector3.Scale (particleSize, _quadCorners [j]) ) y rotación (multiplicando la rotación del cuaternión por un vector). Después de que la posición de la partícula misma se agrega al resultado

Ahora creemos una interfaz de usuario simple para la prueba usando componentes estándar.

imagen

Agregue un UIParticleSystem al componente ParticleSystem

imagen

Ejecute la escena y verifique el resultado del componente.

imagen

Las partículas se muestran de acuerdo con su posición en la jerarquía y tienen en cuenta las máscaras utilizadas. Cuando cambia la resolución de la pantalla y sus proporciones, así como al cambiar la propiedad del Modo Render de Canvas , las partículas se comportan de manera similar a cualquier otro componente visual en Canvas y solo se muestran en ella.

SimulationSpace


Porque colocamos el sistema de partículas dentro de la interfaz de usuario, hay un problema con el parámetro SimulationSpace . Cuando se simula en el espacio mundial, las partículas no se muestran donde deberían. Por lo tanto, agregamos el cálculo de la posición de la partícula dependiendo del valor del parámetro.

 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; } ... } 

Simular propiedades de ParticleSystemRenderer


Ahora implementamos parte de la funcionalidad ParticleSystemRenderer . A saber, las propiedades de RenderMode , SortMode , Pivot .

Renderizar


Nos limitamos al hecho de que las partículas siempre se ubicarán solo en el plano del lienzo. Por lo tanto, implementamos solo dos valores: Billboard y StretchedBillboard .
Creemos nuestra enumeración CanvasParticleSystemRenderMode para esto.

 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; } ... } 

Si selecciona el parámetro StretchedBillboard , el tamaño de partícula dependerá de los parámetros LengthScale y SpeedScale , y su rotación se dirigirá solo en la dirección del movimiento.

imagen

Sortmode


Del mismo modo, cree la enumeración CanvasParticlesSortMode . y solo implementamos la clasificación por vida útil de las partículas.

 public enum CanvasParticlesSortMode { None = 0, OldestInFront = 1, YoungestInFront = 2 } 

 public CanvasParticlesSortMode SortMode; 

Para ordenar, necesitamos almacenar datos sobre la vida útil de la partícula, que se almacenará en la variable _particleElapsedLifetime . La ordenación se implementa utilizando el método Array.Sort .

 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; } ... } 

Pivote


Cree un campo Pivote para compensar el punto central de la partícula.

 public Vector3 Pivot = Vector3.zero; 

Y al calcular la posición del vértice, agregamos este valor.

 Vector3 cornerPosition = Vector3.Scale(particleSize, _quadCorners[j] + Pivot); Vector3 vertexPosition = rotation * cornerPosition + particlePosition; vertexPosition.z = 0; 

Tamaño ajustable


Si el elemento al que está unido el sistema de partículas no tiene tamaños fijos o pueden cambiar en el tiempo de ejecución, entonces sería bueno adaptar el tamaño del sistema de partículas. Hagamos que la fuente: la forma sea proporcional al tamaño del elemento.

Se llama al método OnRectTransformDimensionsChange cuando se cambia el tamaño del componente RectTransform . Redefinimos este método implementando un cambio de escala a la forma para que se ajuste a las dimensiones de RectTransform .

Primero, cree las variables para el componente RectTransform y el módulo ShapeModule . Para deshabilitar la escala de forma, cree la variable ScaleShapeByRectTransform .

Además, el escalado debe realizarse cuando el componente se activa para establecer su escala inicial.

 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; } 

Al calcular, vale la pena considerar la rotación de Shape . Los valores del resultado final deben tomarse en módulo, ya que pueden resultar negativos, lo que afectará la dirección del movimiento de las partículas.

Para probar la operación, ejecute la animación de cambio de tamaño de RectTransform con un sistema de partículas adjunto.

imagen

Inicialización


Para que el script se ejecute correctamente en el editor y evite errores al llamar al método OnRectTransformDimensionsChange , sacamos la inicialización de las variables en un método separado. Y agregue su llamada a los métodos OnPopulateMesh y OnRectTransformDimensionsChange .

ExecuteInEditMode
No necesita especificar el atributo ExecuteInEditMode , porque Graphic ya implementa este comportamiento y el script se ejecuta en el editor.

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

El método OnRectTransformDimensionsChange se puede llamar antes que Awake . Por lo tanto, cada vez que se llama, es necesario inicializar las variables.

Rendimiento y Optimización


Esta representación de partículas es un poco más costosa que usar ParticleSystemRenderer , que requiere un uso más prudente, en particular en dispositivos móviles.
También vale la pena señalar que si al menos uno de los elementos de Canvas está marcado como sucio , esto conducirá a un recálculo de toda la geometría de Canvas y a la generación de nuevos comandos de renderizado. Si la interfaz de usuario contiene mucha geometría compleja y sus cálculos, entonces vale la pena dividirla en varios lienzos incrustados.

imagen

PD: Todo el código fuente y las demostraciones son git link .
El artículo se lanzó hace casi un año después de que fuera necesario usar ParticleSystem en la interfaz de usuario. En ese momento, no encontré una solución similar, y las disponibles no eran óptimas para la tarea actual. Pero un par de días antes de la publicación de este artículo, mientras recolectaba material, accidentalmente encontré una solución similar usando el método Graphic.OnPopulateMesh. Por lo tanto, considero necesario especificar un enlace al repositorio .

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


All Articles