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.
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 .
VertexHelperVertexHelper 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 UIBAwake , 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érticePrimero, 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.

Agregue un
UIParticleSystem al componente
ParticleSystem
Ejecute la escena y verifique el resultado del componente.

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.

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.

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

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 .