Oi Este artigo é sobre como criar seus próprios componentes visuais na interface do usuário usando o exemplo de um componente para visualizar um sistema de partículas no
Canvas 'e.
Essas informações serão úteis para implementar vários efeitos na interface do usuário e também podem ser usadas para gerar uma malha ou otimizá-la.
Um pouco de teoria ou por onde começar a criação de componentes
A base da interface do usuário do Unity é o
Canvas . É ele quem é usado pelo sistema de renderização para exibir a geometria "multicamada", de acordo com a hierarquia interna dos elementos da interface do usuário.
Qualquer componente visual da interface do usuário deve herdar da classe
Graphic (ou sua classe
MaskableGraphic derivada), que passa todos os dados necessários para o componente
CanvasRenderer para renderizá-lo. Os dados são criados no método
OnPopulateMesh , chamado sempre que um componente precisa atualizar sua geometria (por exemplo, ao redimensionar um elemento).
VertexHelper é passado como um parâmetro, o que ajuda a gerar uma malha para a interface do usuário.
Criação de componentes
Base
Iniciamos a implementação criando um script
UIParticleSystem que herda da classe
MaskableGraphic .
MaskableGraphic é um derivado da classe
Graphic e, além disso, fornece trabalho com máscaras. Substitua o método
OnPopulateMesh . A base para trabalhar com o
VertexHelper para gerar os vértices de uma malha do sistema de partículas de malha terá a seguinte aparência:
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); } } }
Primeiro, você precisa limpar o
VertextHelper dos dados existentes chamando o método
Clear . Depois disso, você pode começar a preenchê-lo com novos dados sobre os picos. Para isso, será utilizado o método
AddUIVertexQuad , que permite adicionar informações sobre 4 vértices de uma só vez. Este método foi selecionado para facilitar o uso, como cada partícula é um retângulo. Cada vértice é descrito por um objeto
UIVertex . De todos os parâmetros, precisamos preencher apenas a posição, cor e algumas coordenadas da digitalização
uv .
VertexHelperO VertexHelper possui todo um conjunto de métodos para adicionar informações sobre vértices, além de um par para receber dados atuais. Para geometria mais complexa, a melhor solução seria selecionar o método AddUIVertexStream , que aceita uma lista de vértices e uma lista de índices.
Como em cada quadro, a posição das partículas, sua cor e outros parâmetros serão alterados, a malha para sua renderização também deverá ser atualizada.
Para fazer isso, cada quadro chamará o método
SetVerticesDirty , que definirá o sinalizador para a necessidade de recontar novos dados, o que levará à chamada para o método
OnPopulateMesh . Da mesma forma, para um material, se suas propriedades mudarem, você precisará chamar o método
SetMaterialDirty .
protected void Update() { SetVerticesDirty(); }
Substitua a propriedade
mainTexture . Indica qual textura será passada para o
CanvasRenderer e usada no material, a
propriedade shader
_MainTex . Para fazer isso, crie um campo
ParticleImage , que será retornado pela propriedade
mainTexture .
public Texture ParticleImage; public override Texture mainTexture { get { return ParticleImage; } }
Sistema de partículas
Os dados para gerar vértices de malha serão obtidos do componente
ParticleSystem , que é envolvido em todos os cálculos sobre a localização das partículas, seu tamanho, cor, etc.
O componente ParticleSystemRenderer, que precisará ser desativado, está envolvido na renderização de partículas; portanto, outros componentes,
UIParticleSystem e
CanvasRenderer, serão responsáveis por criar a malha e renderizá-la na interface do usuário.
Crie os campos necessários para a operação e inicialize-os no método
Awake .
UIBehaviourDesperto , como a maioria dos métodos, precisa ser redefinido aqui, pois eles são listados como virtuais no UIBehaviour . A própria classe UIBehaviour é abstrata e praticamente não contém nenhuma lógica de trabalho, mas é básica para a classe 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]; }
O campo
_particles será usado para armazenar partículas do
ParticleSystem e
_main é usado por conveniência com o módulo
MainModule .
Vamos adicionar o método OnPopulateMesh, obtendo todos os dados necessários diretamente do sistema de partículas. Crie as variáveis auxiliares
Vector3 [] _quadCorners e
Vector2 [] _simpleUV .
_quadCorners contém as coordenadas dos 4 cantos do retângulo, em relação ao centro da partícula. O tamanho inicial de cada partícula é considerado um quadrado com lados 1x1.
_simpleUV - coordenadas da varredura
uv ; nesse caso, todas as partículas usam a mesma textura sem nenhum deslocamento.
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); } }
vertexPositionPrimeiro, é calculada a posição local do vértice em relação ao centro da partícula, levando em consideração seu tamanho (operação Vector3.Scale (particleSize, _quadCorners [j]) ) e rotação (multiplicando a rotação do quaternion por um vetor). Depois que a posição da partícula é adicionada ao resultado
Agora vamos criar uma interface simples para o teste usando componentes padrão.

Adicione um
UIParticleSystem ao componente
ParticleSystem
Execute a cena e verifique o resultado do componente.

As partículas são exibidas de acordo com sua posição na hierarquia e levam em consideração as máscaras usadas. Quando você altera a resolução da tela e suas proporções, bem como ao alterar a propriedade
Rendere Mode do
Canvas , as partículas se comportam de maneira semelhante a qualquer outro componente visual do
Canvas e são exibidas apenas nele.
SimulationSpace
Porque Se colocarmos o sistema de partículas dentro da interface do usuário, há um problema com o parâmetro
SimulationSpace . Quando simuladas no espaço do mundo, as partículas não são mostradas onde deveriam. Portanto, adicionamos o cálculo da posição da partícula dependendo do valor do 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; } ... }
Propriedades de Simular ParticleSystemRenderer
Agora, implementamos parte da funcionalidade
ParticleSystemRenderer . Ou seja, as propriedades de
RenderMode ,
SortMode ,
Pivot .
Renderder
Nos limitamos ao fato de que as partículas sempre estarão localizadas apenas no plano da tela. Portanto, implementamos apenas dois valores:
Billboard e
StretchedBillboard .
Vamos criar nossa enumeração
CanvasParticleSystemRenderMode para isso.
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; } ... }
Se você selecionar o parâmetro
StretchedBillboard , o tamanho da partícula dependerá dos
parâmetros LengthScale e
SpeedScale , e sua rotação será direcionada apenas na direção do movimento.

Sortmode
Da mesma forma, crie a enumeração
CanvasParticlesSortMode . e apenas implementamos a classificação por tempo de vida das partículas.
public enum CanvasParticlesSortMode { None = 0, OldestInFront = 1, YoungestInFront = 2 }
public CanvasParticlesSortMode SortMode;
Para a classificação, precisamos armazenar dados sobre a vida útil das partículas, que serão armazenadas na variável
_particleElapsedLifetime . A classificação é implementada usando o 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; } ... }
Pivô
Crie um campo
Pivô para deslocar o ponto central da partícula.
public Vector3 Pivot = Vector3.zero;
E ao calcular a posição do vértice, adicionamos esse valor.
Vector3 cornerPosition = Vector3.Scale(particleSize, _quadCorners[j] + Pivot); Vector3 vertexPosition = rotation * cornerPosition + particlePosition; vertexPosition.z = 0;
Tamanho ajustável
Se o elemento ao qual o sistema de partículas está conectado não tiver tamanhos fixos ou eles puderem mudar em tempo de execução, seria bom adaptar o tamanho do sistema de partículas. Vamos tornar a fonte -
forma proporcional ao tamanho do elemento.
O método
OnRectTransformDimensionsChange é chamado quando o componente
RectTransform é
redimensionado . Redefinimos esse método implementando uma alteração de escala na forma para ajustar as dimensões do
RectTransform .
Primeiro, crie as variáveis para o componente
RectTransform e o módulo
ShapeModule . Para desativar o dimensionamento da forma, crie a variável
ScaleShapeByRectTransform .
Além disso, a escala deve ser executada quando o componente é ativado para definir sua 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; }
No cálculo, vale a pena considerar a rotação do
Shape . Os valores do resultado final devem ser tomados no módulo, pois podem vir a ser negativos, o que afetará a direção do movimento das partículas.
Para testar a operação, execute a animação de
redimensionamento RectTransform com um sistema de partículas conectado a ela.

Inicialização
Para que o script seja executado corretamente no editor e evite erros ao chamar o método
OnRectTransformDimensionsChange , lançamos a inicialização de variáveis em um método separado. E adicione sua chamada aos
métodos OnPopulateMesh e
OnRectTransformDimensionsChange .
ExecuteInEditModeVocê não precisa especificar o atributo ExecuteInEditMode , porque O gráfico já implementa esse comportamento e o script é executado no 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 ... }
O método
OnRectTransformDimensionsChange pode ser
chamado antes do
Awake . Portanto, cada vez que é chamado, é necessário inicializar as variáveis.
Desempenho e Otimização
Essa renderização de partículas é um pouco mais cara do que usar o
ParticleSystemRenderer , que requer uso mais prudente, principalmente em dispositivos móveis.
Também é importante notar que, se pelo menos um dos elementos do
Canvas estiver marcado como
Dirty , isso levará ao recálculo de toda a geometria do
Canvas e à geração de novos comandos de renderização. Se a interface do usuário contiver muita geometria complexa e seus cálculos, vale a pena dividi-la em várias telas incorporadas.

PS: Todo o código fonte e demos são
link git .
O artigo foi lançado quase um ano atrás, depois que foi necessário o uso do ParticleSystem na interface do usuário. Naquela época, não encontrei uma solução semelhante e as disponíveis não eram ideais para a tarefa atual. Mas, alguns dias antes da publicação deste artigo, ao coletar material, encontrei acidentalmente uma solução semelhante usando o método Graphic.OnPopulateMesh. Portanto, considero necessário especificar um
link para o repositório .