Crie componentes visuais na interface do usuário do Unity. Sistema de partículas

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.

imagem


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 .

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

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

vertexPosition
Primeiro, é 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.

imagem

Adicione um UIParticleSystem ao componente ParticleSystem

imagem

Execute a cena e verifique o resultado do componente.

imagem

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.

imagem

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.

imagem

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 .

ExecuteInEditMode
Você 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.

imagem

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 .

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


All Articles