Mostrar mensajes en el juego usando el sistema de partículas

imagen

Tarea


Al desarrollar nuestro juego The Unliving, nos propusimos mostrar varios mensajes, como el daño causado, la falta de salud o energía, el valor de recompensa, la cantidad de puntos de salud restaurados, etc., usando el Sistema de partículas. Se decidió hacer esto para obtener más oportunidades para personalizar los efectos de la apariencia y el comportamiento adicional de dichos mensajes, lo cual es problemático cuando se utilizan elementos estándar del sistema Unity UI.

Además, este enfoque implica el uso de una sola instancia de Particle System para cada tipo de mensaje, lo que proporciona un gran aumento en la productividad en comparación con la salida de los mismos mensajes que utilizan la Unidad de interfaz de usuario.

Reporte de daños

imagen

Mensaje de texto de escasez

imagen

Algoritmo de decisión


Usando el sombreador, mostramos la textura preparada usando las coordenadas UV correctas. La información con coordenadas UV se transmite por dos flujos (flujos de vértices) a ParticleSystem usando ParticleSystem.SetCustomParticleData en forma de una lista de Vector4.

Nuestra implementación implica el uso de texturas que contienen 10 filas y 10 columnas de caracteres. Cualquier fuente monospace puede usarse como fuente. Esto es para evitar un espacio diferente entre los caracteres del mensaje.

Fuente de textura en PSD

Implementación paso a paso


Creación de Vector4 para transferir a Vertex Stream

Para describir el conjunto de caracteres, utilizaremos la estructura SymbolsTextureData.

La matriz de caracteres debe llenarse manualmente, en orden agregando todos los símbolos de textura de fuente a partir de la esquina superior izquierda.

[Serializable]
public struct SymbolsTextureData
{
    //   
    public Texture texture;
    //    ,   - 
    public char[] chars;
    
    //Dictionary     -    
    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;
            //  ,    
            //    , ,     10.
            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, obtenemos la clase TextRendererParticleSystem. Cuando llama al método público SpawnParticle, una partícula del sistema de partículas generará la posición deseada, con el valor, color y tamaño deseados.

[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)
    {
        //  
     }
}

Particle System en Unity le permite transferir datos personalizados en forma de 2 transmisiones Vector4.



Agregamos intencionalmente un flujo adicional con UV2 para evitar un cambio en las coordenadas de los flujos. Si esto no se hace, las coordenadas X e Y del vector Custom1 en C # corresponderán a los sombreadores TEXCOORD0 Z y W. Y en consecuencia, Custom1.z = TEXCOORD1.x, Custom1.w = TEXCOORD1.y. Lo que causará muchos inconvenientes en el futuro.


Como se describió anteriormente, utilizaremos dos Vector4 para transmitir la longitud del mensaje y las coordenadas UV de los caracteres. Dado que Vector4 contiene 4 elementos de tipo flotante, por defecto podemos incluir 4 * 4 = 16 bytes de datos en él. Porque Dado que nuestro mensaje contendrá solo la longitud del mensaje (número de dos dígitos) y las coordenadas de los caracteres (número de dos dígitos para cada carácter), el rango del tipo byte (0-255) es redundante para nosotros. Si bien usar decimales funcionará bien.

La precisión de flotación es de 6-9 caracteres, lo que significa que podemos usar de manera segura 6 bits de cada coordenada Vector4 y no preocuparnos por la integridad y precisión de los datos. En realidad, tratamos de agrupar 7, 8 y 9 caracteres, pero la precisión de flotación no es suficiente.

Resulta que en cada flotante, usando lugares decimales, empacaremos hasta 6 dígitos, en contraste con la versión estándar con cuatro bytes. Total, un Vector4 contendrá 24 números de un solo dígito.

Podemos transferir 2 vectores en la secuencia, por lo que usaremos ambos para transmitir mensajes de hasta 23 caracteres de longitud:

Custom1.xyzw: los primeros 12 caracteres del mensaje.
Custom2.xyzw: otros 11 caracteres del mensaje + longitud del mensaje (últimos 2 caracteres).

Por ejemplo, el mensaje "Hola" se vería así.


Las coordenadas de caracteres corresponden al número de columna y la línea de posición de caracteres en la textura.


En el código, envolver una cadena en dos Vector4 se verá así:

//   Vector2     float
public float PackFloat(Vector2[] vecs)
{
    if (vecs == null || vecs.Length == 0) return 0;            
    //      float
    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;
}

//  Vector4    CustomData
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]; //  24  - 23  +  
    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;
        //  GetTextureCoordinates()  SymbolsTextureData    
        texCords[i] = textureData.GetTextureCoordinates(message[i]);
    }
		
    var custom1Data = CreateCustomData(texCords);
    var custom2Data = CreateCustomData(texCords, 12);
}

Vector con CustomData listo. Es hora de generar manualmente una nueva partícula con los parámetros deseados. Generación de

partículas

Lo primero que debemos hacer es asegurarnos de que las corrientes de CustomData estén activadas en la configuración del Renderer del sistema de partículas:

//   ParticleSystem
if (particleSystem == null) particleSystem = GetComponent<ParticleSystem>();

if (particleSystemRenderer == null)
{
    //   ParticleSystemRenderer,       
    particleSystemRenderer = particleSystem.GetComponent<ParticleSystemRenderer>();
    var streams = new List<ParticleSystemVertexStream>();
    particleSystemRenderer.GetActiveVertexStreams(streams);
    //   Vector2(UV2, SizeXY, etc.),        
    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 crear una partícula, utilizamos el método Emit () de la clase ParticleSystem.

//  
//      
// startSize3D  X,       
//   
var emitParams = new ParticleSystem.EmitParams
{
    startColor = color,
    position = position,
    applyShapeToPosition = true,
    startSize3D = new Vector3(messageLenght, 1, 1)
};
//      ,    SpawnParticle 
//   startSize
if (startSize.HasValue) emitParams.startSize3D *= startSize.Value * particleSystem.main.startSizeMultiplier;
//  
particleSystem.Emit(emitParams, 1);

//     
var customData = new List<Vector4>();
//  ParticleSystemCustomData.Custom1  ParticleSystem
particleSystem.GetCustomParticleData(customData, ParticleSystemCustomData.Custom1);
//   , ..  ,     
customData[customData.Count - 1] = custom1Data;
//   ParticleSystem
particleSystem.SetCustomParticleData(customData, ParticleSystemCustomData.Custom1);

//  ParticleSystemCustomData.Custom2
particleSystem.GetCustomParticleData(customData, ParticleSystemCustomData.Custom2);            
customData[customData.Count - 1] = custom2Data;
particleSystem.SetCustomParticleData(customData, ParticleSystemCustomData.Custom2);

Agregue ambos bloques al método SpawnParticle () y la parte C # está lista: la GPU empaqueta y transmite el mensaje en forma de dos Vector4 en Vertex Stream. Lo más interesante que queda es aceptar estos datos y mostrarlos correctamente.

Código de sombreador

Shader "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 Editor

Cree material y asígnelo a nuestro sombreador. En la escena, cree un objeto con el componente ParticleSystem, asigne el material creado. Luego ajustamos el comportamiento de las partículas y desactivamos el parámetro Play On Awake. Desde cualquier clase, llamamos al método RendererParticleSystem.SpawnParticle () o utilizamos el método de negociación.

[ContextMenu("TestText")]
public void TestText()
{
    SpawnParticle(transform.position, "Hello world!", Color.red);
}

El código fuente, los recursos y los ejemplos de uso se pueden encontrar aquí .

Sistema de mensajes en acción

imagen

Eso es todo. Salida de mensajes con sistema de partículas listo Esperamos que esta solución beneficie a los desarrolladores de juegos de Unity.

UPD: Desempeño de la solución propuesta
En los comentarios de varias personas, surgió una pregunta sobre el desempeño de este método. Mediciones especialmente realizadas por el perfilador de la unidad. Las condiciones son las mismas: 1000 objetos móviles que cambian de color.

El resultado del uso de la interfaz de usuario estándar (el único lienzo en el que solo hay 1000 objetos de texto de la interfaz de usuario):

el tiempo de trama total es de al menos 50 ms, de los cuales 40 ms se dedican a actualizar el lienzo. Al mismo tiempo, los objetos ni siquiera se generan, sino que simplemente se mueven.

El resultado de una generación de 1000 partículas usando nuestra solución:

Toda la magia ocurre en la GPU, incluso en el momento del desove de 1000 partículas, el marco se calcula en menos de 5 ms.

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


All Articles