
Tarefa
Ao desenvolver o jogo The Unliving, nos propusemos a tarefa de exibir várias mensagens, como danos causados, falta de saúde ou energia, valor da recompensa, número de pontos de saúde restaurados etc. usando o Sistema de Partículas. Foi decidido fazer isso para obter mais oportunidades para personalizar os efeitos da aparência e do comportamento posterior dessas mensagens, o que é problemático ao usar elementos padrão do sistema de interface do usuário do Unity.Além disso, essa abordagem implica o uso de apenas uma instância do Particle System para cada tipo de mensagem, o que proporciona um enorme aumento de produtividade em comparação com a saída das mesmas mensagens usando a interface do usuário do Unity.Relatório de danosMensagem de texto em faltaAlgoritmo de decisão
Usando o shader, exibimos a textura pré-preparada usando as coordenadas UV corretas. As informações com coordenadas UV são transmitidas por dois fluxos (fluxos de vértices) ao ParticleSystem usando ParticleSystem.SetCustomParticleData na forma de uma lista Vector4.Nossa implementação envolve o uso de texturas contendo 10 linhas e 10 colunas de caracteres. Qualquer fonte monoespaçada pode ser usada como fonte. Isso evita espaçamento diferente entre os caracteres da mensagem.→ Fonte de textura no PSDImplementação passo a passo
Criando Vector4 para transferência para o Vertex StreamPara descrever o conjunto de caracteres, usaremos a estrutura SymbolsTextureData.A matriz de caracteres deve ser preenchida manualmente, adicionando todos os símbolos de textura da fonte a partir do canto superior esquerdo.[Serializable]
public struct SymbolsTextureData
{
public Texture texture;
public char[] chars;
private Dictionary<char, Vector2> charsDict;
public void Initialize()
{
charsDict = new Dictionary<char, Vector2>();
for (int i = 0; i < chars.Length; i++)
{
var c = char.ToLowerInvariant(chars[i]);
if (charsDict.ContainsKey(c)) continue;
var uv = new Vector2(i % 10, 9 - i / 10);
charsDict.Add(c, uv);
}
}
public Vector2 GetTextureCoordinates(char c)
{
c = char.ToLowerInvariant(c);
if (charsDict == null) Initialize();
if (charsDict.TryGetValue(c, out Vector2 texCoord))
return texCoord;
return Vector2.zero;
}
}
Como resultado, obtemos a classe TextRendererParticleSystem. Quando você chama o método público SpawnParticle, uma partícula do Sistema de Partículas aparece na posição desejada, com o valor, a cor e o tamanho desejados.[RequireComponent(typeof(ParticleSystem))]
public class TextRendererParticleSystem : MonoBehaviour
{
private ParticleSystemRenderer particleSystemRenderer;
private new ParticleSystem particleSystem;
public void SpawnParticle(Vector3 position, string message, Color color, float? startSize = null)
{
}
}
O Particle System no Unity permite transferir dados personalizados na forma de 2 fluxos Vector4.Intencionalmente, adicionamos um fluxo extra com UV2 para evitar uma mudança nas coordenadas dos fluxos. Se isso não for feito, as coordenadas X e Y do vetor Custom1 em C # corresponderão aos shaders Z e W TEXCOORD0. E, portanto, Custom1.z = TEXCOORD1.x, Custom1.w = TEXCOORD1.y. O que causará muitos inconvenientes no futuro.Conforme descrito anteriormente, usaremos dois Vector4s para transmitir o comprimento da mensagem e as coordenadas UV dos caracteres. Como o Vector4 contém 4 elementos do tipo float, por padrão, podemos incluir 4 * 4 = 16 bytes de dados nele. Porque Como nossa mensagem conterá apenas o tamanho da mensagem (número de dois dígitos) e as coordenadas dos caracteres (número de dois dígitos para cada caractere), o intervalo do tipo byte (0-255) é redundante para nós. Ao usar casas decimais, tudo bem.A precisão da flutuação é de 6 a 9 caracteres, o que significa que podemos usar com segurança 6 bits de cada coordenada Vector4 e não nos preocupar com a integridade e a precisão dos dados. Na verdade, tentamos incluir 7, 8 e 9 caracteres, mas a precisão da flutuação não é suficiente.Acontece que em cada carro alegórico, usando casas decimais, empacotaremos até 6 dígitos, em contraste com a versão padrão com quatro bytes. No total, um Vector4 conterá 24 números de um dígito.Como podemos transferir 2 vetores no fluxo, usaremos ambos para transmitir mensagens com até 23 caracteres:Custom1.xyzw - os 12 primeiros caracteres da mensagem.Custom2.xyzw - outros 11 caracteres da mensagem + tamanho da mensagem (últimos 2 caracteres).Por exemplo, a mensagem "Olá" ficaria assim.As coordenadas do caractere correspondem ao número da coluna e à linha de posição do caractere na textura.No código, agrupar uma string em dois Vector4 terá a seguinte aparência:
public float PackFloat(Vector2[] vecs)
{
if (vecs == null || vecs.Length == 0) return 0;
var result = vecs[0].y * 10000 + vecs[0].x * 100000;
if (vecs.Length > 1) result += vecs[1].y * 100 + vecs[1].x * 1000;
if (vecs.Length > 2) result += vecs[2].y + vecs[2].x * 10;
return result;
}
private Vector4 CreateCustomData(Vector2[] texCoords, int offset = 0)
{
var data = Vector4.zero;
for (int i = 0; i < 4; i++)
{
var vecs = new Vector2[3];
for (int j = 0; j < 3; j++)
{
var ind = i * 3 + j + offset;
if (texCoords.Length > ind)
{
vecs[j] = texCoords[ind];
}
else
{
data[i] = PackFloat(vecs);
i = 5;
break;
}
}
if (i < 4) data[i] = PackFloat(vecs);
}
return data;
}
public void SpawnParticle(Vector3 position, string message, Color color, float? startSize = null)
{
var texCords = new Vector2[24];
var messageLenght = Mathf.Min(23, message.Length);
texCords[texCords.Length - 1] = new Vector2(0, messageLenght);
for (int i = 0; i < texCords.Length; i++)
{
if (i >= messageLenght) break;
texCords[i] = textureData.GetTextureCoordinates(message[i]);
}
var custom1Data = CreateCustomData(texCords);
var custom2Data = CreateCustomData(texCords, 12);
}
Vetor com CustomData pronto. É hora de gerar manualmente uma nova partícula com os parâmetros desejados. Geração departículasA primeira coisa que devemos fazer é garantir que os fluxos CustomData sejam ativados nas configurações do Renderer do sistema de partículas:
if (particleSystem == null) particleSystem = GetComponent<ParticleSystem>();
if (particleSystemRenderer == null)
{
particleSystemRenderer = particleSystem.GetComponent<ParticleSystemRenderer>();
var streams = new List<ParticleSystemVertexStream>();
particleSystemRenderer.GetActiveVertexStreams(streams);
if (!streams.Contains(ParticleSystemVertexStream.UV2)) streams.Add(ParticleSystemVertexStream.UV2);
if (!streams.Contains(ParticleSystemVertexStream.Custom1XYZW)) streams.Add(ParticleSystemVertexStream.Custom1XYZW);
if (!streams.Contains(ParticleSystemVertexStream.Custom2XYZW)) streams.Add(ParticleSystemVertexStream.Custom2XYZW);
particleSystemRenderer.SetActiveVertexStreams(streams);
}
Para criar uma partícula, usamos o método Emit () da classe ParticleSystem.
var emitParams = new ParticleSystem.EmitParams
{
startColor = color,
position = position,
applyShapeToPosition = true,
startSize3D = new Vector3(messageLenght, 1, 1)
};
if (startSize.HasValue) emitParams.startSize3D *= startSize.Value * particleSystem.main.startSizeMultiplier;
particleSystem.Emit(emitParams, 1);
var customData = new List<Vector4>();
particleSystem.GetCustomParticleData(customData, ParticleSystemCustomData.Custom1);
customData[customData.Count - 1] = custom1Data;
particleSystem.SetCustomParticleData(customData, ParticleSystemCustomData.Custom1);
particleSystem.GetCustomParticleData(customData, ParticleSystemCustomData.Custom2);
customData[customData.Count - 1] = custom2Data;
particleSystem.SetCustomParticleData(customData, ParticleSystemCustomData.Custom2);
Adicione os dois blocos ao método SpawnParticle () e a parte C # está pronta: a mensagem é compactada e transmitida pela GPU na forma de dois Vector4 no Vertex Stream. O mais interessante é aceitar esses dados e exibi-los corretamente.Código de sombreadorShader "Custom/TextParticles"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
// 10,
_Cols ("Columns Count", Int) = 10
_Rows ("Rows Count", Int) = 10
}
SubShader
{
Tags { "RenderType"="Opaque" "PreviewType"="Plane" "Queue" = "Transparent+1"}
LOD 100
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
fixed4 color : COLOR;
float4 uv : TEXCOORD0;
// customData
float4 customData1 : TEXCOORD1;
float4 customData2 : TEXCOORD2;
};
struct v2f
{
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float4 uv : TEXCOORD0;
float4 customData1 : TEXCOORD1;
float4 customData2 : TEXCOORD2;
};
uniform sampler2D _MainTex;
uniform uint _Cols;
uniform uint _Rows;
v2f vert (appdata v)
{
v2f o;
// w- ?
// .
// 100.
float textLength = ceil(fmod(v.customData2.w, 100));
o.vertex = UnityObjectToClipPos(v.vertex);
// UV , -
o.uv.xy = v.uv.xy * fixed2(textLength / _Cols, 1.0 / _Rows);
o.uv.zw = v.uv.zw;
o.color = v.color;
o.customData1 = floor(v.customData1);
o.customData2 = floor(v.customData2);
return o;
}
fixed4 frag (v2f v) : SV_Target
{
fixed2 uv = v.uv.xy;
//
uint ind = floor(uv.x * _Cols);
uint x = 0;
uint y = 0;
// ,
//0-3 - customData1
//4-7 - customData2
uint dataInd = ind / 3;
// 6 float
uint sum = dataInd < 4 ? v.customData1[dataInd] : v.customData2[dataInd - 4];
// float
for(int i = 0; i < 3; ++i)
{
if (dataInd > 3 & i == 3) break;
// , 10^2 = 99 ..
uint val = ceil(pow(10, 5 - i * 2));
x = sum / val;
sum -= x * val;
val = ceil(pow(10, 4 - i * 2));
y = sum / val;
sum -= floor(y * val);
if (dataInd * 3 + i == ind) i = 3;
}
float cols = 1.0 / _Cols;
float rows = 1.0 / _Rows;
// UV-, - , ,
//
uv.x += x * cols - ind * rows;
uv.y += y * rows;
return tex2D(_MainTex, uv.xy) * v.color;
}
ENDCG
}
}
}
Unity EditorCrie material e atribua-o ao nosso shader. Na cena, crie um objeto com o componente ParticleSystem, atribua o material criado. Em seguida, ajustamos o comportamento das partículas e desativamos o parâmetro Play On Awake. De qualquer classe, chamamos o método RendererParticleSystem.SpawnParticle () ou usamos o método de barganha.[ContextMenu("TestText")]
public void TestText()
{
SpawnParticle(transform.position, "Hello world!", Color.red);
}
O código-fonte, os recursos e os exemplos de uso podem ser encontrados aqui .Sistema de mensagens em açãoIsso é tudo. Saída de mensagem usando o sistema de partículas pronto! Esperamos que esta solução beneficie os desenvolvedores de jogos Unity.UPD: Desempenho da solução propostaNos comentários de várias pessoas, surgiu uma pergunta sobre o desempenho desse método. Medições especialmente feitas pelo criador de perfil de unidade. As condições são as mesmas - 1.000 objetos em movimento e que mudam de cor.O resultado do uso da interface do usuário padrão (a única tela na qual existem apenas 1000 objetos de texto da interface do usuário):
O tempo total do quadro é de pelo menos 50ms, dos quais 40ms são gastos na atualização da tela. Ao mesmo tempo, os objetos nem aparecem, mas simplesmente se movem.O resultado de uma semente de 1000 partículas usando nossa solução:
Toda mágica acontece na GPU, mesmo no momento da geração de 1000 partículas, o quadro é calculado em menos de 5ms.