Zeigen Sie Nachrichten im Spiel mit dem Partikelsystem an

Bild

Aufgabe


Bei der Entwicklung unseres Spiels The Unliving haben wir uns die Aufgabe gestellt, mithilfe des Partikelsystems verschiedene Meldungen anzuzeigen, wie z. B. verursachten Schaden, mangelnde Gesundheit oder Energie, Belohnungswert, Anzahl der wiederhergestellten Gesundheitspunkte usw. Dies wurde beschlossen, um mehr Möglichkeiten zum Anpassen der Auswirkungen des Erscheinungsbilds und des weiteren Verhaltens solcher Nachrichten zu erhalten, was bei Verwendung von Standardelementen des Unity-UI-Systems problematisch ist.

Darüber hinaus impliziert dieser Ansatz die Verwendung von nur einer Partikelsysteminstanz für jeden Nachrichtentyp, was zu einer enormen Steigerung der Produktivität im Vergleich zur Ausgabe derselben Nachrichten über die Unity-Benutzeroberfläche führt.

Schadensbericht

Bild

Kurznachricht

Bild

Entscheidungsalgorithmus


Mit dem Shader zeigen wir die vorbereitete Textur mit den richtigen UV-Koordinaten an. Informationen mit UV-Koordinaten werden von zwei Streams (Vertex-Streams) mithilfe von ParticleSystem.SetCustomParticleData in Form einer Vector4-Liste an ParticleSystem übertragen.

Unsere Implementierung beinhaltet die Verwendung von Texturen mit 10 Zeilen und 10 Spalten mit Zeichen. Jede Monospace-Schriftart kann als Schriftart verwendet werden. Dies dient dazu, unterschiedliche Abstände zwischen Nachrichtenzeichen zu vermeiden.

Texturquelle in PSD

Schritt für Schritt Implementierung


Erstellen von Vector4 für die Übertragung an Vertex Stream

Zur Beschreibung des Zeichensatzes verwenden wir die SymbolsTextureData-Struktur.

Das Zeichenarray muss manuell ausgefüllt werden, indem alle Schrifttextursymbole ab der oberen linken Ecke hinzugefügt werden.

[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;
    }
}

Als Ergebnis erhalten wir die TextRendererParticleSystem-Klasse. Wenn Sie die öffentliche SpawnParticle-Methode aufrufen, erscheint ein Partikel des Partikelsystems an der gewünschten Position mit dem gewünschten Wert, der gewünschten Farbe und Größe.

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

Mit Particle System in Unity können Sie benutzerdefinierte Daten in Form von 2 Vector4-Streams übertragen.



Wir haben absichtlich einen zusätzlichen Stream mit UV2 hinzugefügt, um eine Verschiebung der Koordinaten der Streams zu vermeiden. Wenn dies nicht erfolgt, entsprechen die X- und Y-Koordinaten des Custom1-Vektors in C # den Z- und W-TEXCOORD0-Shadern. Und dementsprechend ist Custom1.z = TEXCOORD1.x, Custom1.w = TEXCOORD1.y. Das wird in Zukunft viele Unannehmlichkeiten verursachen.


Wie bereits beschrieben, verwenden wir zwei Vector4s, um die Nachrichtenlänge und die UV-Koordinaten von Zeichen zu übermitteln. Da Vector4 4 Elemente vom Typ float enthält, können wir standardmäßig 4 * 4 = 16 Datenbytes darin packen. weil Da unsere Nachricht nur die Länge der Nachricht (zweistellige Nummer) und die Koordinaten der Zeichen (zweistellige Nummer für jedes Zeichen) enthält, ist der Bereich des Typbytes (0-255) für uns redundant. Bei Verwendung von Dezimalstellen reicht dies völlig aus.

Die Float-Genauigkeit beträgt 6-9 Zeichen, was bedeutet, dass wir 6 Bits jeder Vector4-Koordinate sicher verwenden können und uns nicht um die Integrität und Genauigkeit der Daten sorgen müssen. Eigentlich haben wir versucht, 7, 8 und 9 Zeichen zu packen, aber die Float-Genauigkeit reicht nicht aus.

Es stellt sich heraus, dass wir in jedem Gleitkomma mit Dezimalstellen bis zu 6 Stellen packen, im Gegensatz zur Standardversion mit vier Bytes. Insgesamt enthält ein Vector4 24 einstellige Zahlen.

Wir können 2 Vektoren im Stream übertragen, sodass wir beide verwenden, um Nachrichten mit einer Länge von bis zu 23 Zeichen zu übertragen:

Custom1.xyzw - die ersten 12 Zeichen der Nachricht.
Custom2.xyzw - weitere 11 Zeichen der Nachricht + Nachrichtenlänge (letzte 2 Zeichen).

Zum Beispiel würde die Nachricht "Hallo" so aussehen.


Die Zeichenkoordinaten entsprechen der Spaltennummer und der Zeichenpositionslinie in der Textur.


Im Code sieht das Umschließen einer Zeichenfolge in zwei Vector4 folgendermaßen aus:

//   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);
}

Vektor mit CustomData bereit. Es ist Zeit, manuell ein neues Partikel mit den gewünschten Parametern zu erzeugen.

Partikel-

Spawn Als erstes müssen wir sicherstellen, dass CustomData-Streams in den Renderer-Einstellungen des Partikelsystems aktiviert sind:

//   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);
}

Um ein Partikel zu erstellen, verwenden wir die Emit () -Methode der ParticleSystem-Klasse.

//  
//      
// 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);

Fügen Sie beide Blöcke zur SpawnParticle () -Methode hinzu, und der C # -Teil ist bereit: Die Nachricht wird gepackt und von der GPU in Form von zwei Vector4 in Vertex Stream übertragen. Am interessantesten ist es, diese Daten zu akzeptieren und korrekt anzuzeigen.

Shader-Code

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

Erstellen Sie Material und weisen Sie es unserem Shader zu. Erstellen Sie in der Szene ein Objekt mit der ParticleSystem-Komponente und weisen Sie das erstellte Material zu. Dann passen wir das Partikelverhalten an und deaktivieren den Parameter Play On Awake. Von jeder Klasse rufen wir die RendererParticleSystem.SpawnParticle () -Methode auf oder verwenden die Bargain-Methode.

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

Quellcode, Ressourcen und Verwendungsbeispiele finden Sie hier .

Nachrichtensystem in Aktion

Bild

Das ist alles. Nachrichtenausgabe mit Partikelsystem bereit! Wir hoffen, dass diese Lösung den Entwicklern von Unity-Spielen zugute kommt.

UPD: Leistung der vorgeschlagenen Lösung
In den Kommentaren mehrerer Personen stellte sich die Frage nach der Leistung dieser Methode. Speziell durchgeführte Messungen durch den Profiler der Einheit. Die Bedingungen sind die gleichen - 1000 sich bewegende, farbwechselnde Objekte.

Das Ergebnis der Verwendung der Standard-Benutzeroberfläche (der einzigen Zeichenfläche, auf der sich nur 1000 UI-

Textobjekte befinden ): Die Gesamtrahmenzeit beträgt mindestens 50 ms, von denen 40 ms für die Aktualisierung der Zeichenfläche aufgewendet werden. Gleichzeitig erscheinen Objekte nicht einmal, sondern bewegen sich einfach.

Das Ergebnis eines Laichens von 1000 Partikeln mit unserer Lösung:

Alle Magie geschieht an der GPU, selbst zum Zeitpunkt des Laichens von 1000 Partikeln wird der Frame in weniger als 5 ms berechnet.

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


All Articles