Tampilkan pesan dalam gim menggunakan Sistem Partikel

gambar

Tugas


Saat mengembangkan game kami The Unliving, kami menetapkan tugas untuk menampilkan berbagai pesan, seperti kerusakan yang dilakukan, kurangnya kesehatan atau energi, nilai hadiah, jumlah titik kesehatan yang dipulihkan, dll., Menggunakan Sistem Partikel. Diputuskan untuk melakukan ini untuk mendapatkan lebih banyak kesempatan untuk menyesuaikan efek dari penampilan dan perilaku lebih lanjut dari pesan-pesan tersebut, yang bermasalah ketika menggunakan elemen standar dari sistem Unity UI.

Selain itu, pendekatan ini menyiratkan penggunaan hanya satu contoh Sistem Partikel untuk setiap jenis pesan, yang memberikan peningkatan besar dalam produktivitas dibandingkan dengan output dari pesan yang sama menggunakan Unity UI.

Laporan kerusakan

gambar

Pesan Teks Kekurangan

gambar

Algoritma keputusan


Menggunakan shader, kami menampilkan tekstur yang disiapkan sebelumnya menggunakan koordinat UV yang benar. Informasi dengan koordinat UV ditransmisikan oleh dua aliran (vertex stream) ke ParticleSystem menggunakan ParticleSystem.SetCustomParticleData dalam bentuk daftar Vector4.

Implementasi kami melibatkan penggunaan tekstur yang berisi 10 baris dan 10 kolom karakter. Font monospace apa pun dapat digunakan sebagai font. Ini untuk menghindari jarak antar karakter pesan yang berbeda.

โ†’ Sumber tekstur di PSD

Langkah demi langkah implementasi


Membuat Vector4 untuk ditransfer ke Vertex Stream

Untuk menggambarkan set karakter, kita akan menggunakan struktur SymbolsTextureData.

Array karakter harus diisi secara manual, dengan menambahkan semua simbol tekstur font ke mulai dari sudut kiri atas.

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

Sebagai hasilnya, kita mendapatkan kelas TextRendererParticleSystem. Saat Anda memanggil metode SpawnParticle publik, satu partikel dari Sistem Partikel akan muncul ke posisi yang diinginkan, dengan nilai, warna, dan ukuran yang diinginkan.

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

Sistem Partikel dalam Persatuan memungkinkan Anda untuk mentransfer data khusus dalam bentuk 2 aliran Vector4.



Kami sengaja menambahkan aliran ekstra dengan UV2 untuk menghindari pergeseran koordinat aliran. Jika ini tidak dilakukan, maka koordinat X dan Y dari vektor Custom1-di C # akan sesuai dengan shader Z dan W TEXCOORD0. Dan karenanya, Custom1.z = TEXCOORD1.x, Custom1.w = TEXCOORD1.y. Yang akan menyebabkan banyak ketidaknyamanan di masa depan.


Seperti dijelaskan sebelumnya, kita akan menggunakan dua Vector4s untuk menyampaikan panjang pesan dan koordinat UV karakter. Karena Vector4 berisi 4 elemen tipe float, secara default kita dapat mengemas 4 * 4 = 16 byte data ke dalamnya. Karena Karena pesan kami hanya akan berisi panjang pesan (nomor dua digit) dan koordinat karakter (nomor dua digit untuk setiap karakter), maka kisaran tipe byte (0-255) berlebihan bagi kami. Saat menggunakan tempat desimal akan baik-baik saja.

Akurasi float adalah 6-9 karakter, yang berarti kita dapat menggunakan 6 bit masing-masing koordinat Vector4 dengan aman dan tidak khawatir tentang integritas dan akurasi data. Sebenarnya, kami mencoba mengemas 7, 8 dan 9 karakter, tetapi akurasi float tidak cukup.

Ternyata di setiap float, menggunakan tempat desimal, kami akan mengemas sebanyak 6 digit, berbeda dengan versi standar dengan empat byte. Total, satu Vector4 akan berisi 24 angka tunggal.

Kami dapat mentransfer 2 vektor dalam aliran, jadi kami akan menggunakan keduanya untuk mengirimkan pesan hingga 23 karakter:

Custom1.xyzw - 12 karakter pertama dari pesan tersebut.
Custom2.xyzw - 11 karakter pesan + panjang pesan lainnya (2 karakter terakhir).

Misalnya, pesan "Halo" akan terlihat seperti ini.


Koordinat karakter sesuai dengan nomor kolom dan garis posisi karakter dalam tekstur.


Dalam kode, membungkus sebuah string dalam dua Vector4 akan terlihat seperti ini:

//   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 dengan CustomData siap. Sudah waktunya untuk secara manual menelurkan partikel baru dengan parameter yang diinginkan. Pemijahan

partikel

Hal pertama yang perlu kita lakukan adalah memastikan bahwa aliran CustomData diaktifkan di pengaturan Renderer pada sistem partikel:

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

Untuk membuat partikel, kami menggunakan metode Emit () dari kelas 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);

Tambahkan kedua blok ke metode SpawnParticle () dan bagian C # siap: pesan dikemas dan dikirim oleh GPU dalam bentuk dua Vector4 di Vertex Stream. Yang paling menarik adalah menerima data ini dan menampilkannya dengan benar.

Kode shader

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

Buat materi dan berikan ke shader kami. Di tempat kejadian, buat objek dengan komponen ParticleSystem, tetapkan materi yang dibuat. Kemudian kami menyesuaikan perilaku partikel dan mematikan parameter Play On Awake. Dari kelas mana pun, kami memanggil metode RendererParticleSystem.SpawnParticle () atau menggunakan metode tawar-menawar.

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

Kode sumber, sumber daya, dan contoh penggunaan dapat ditemukan di sini .

Sistem pesan beraksi

gambar

Itu saja. Output Pesan Menggunakan Sistem Partikel Siap! Kami berharap solusi ini akan menguntungkan pengembang game Unity.

UPD: Kinerja solusi yang diusulkan
Dalam komentar beberapa orang, muncul pertanyaan tentang kinerja metode ini. Pengukuran yang dilakukan khusus oleh profiler persatuan. Kondisinya sama - 1000 benda yang bergerak dan berubah warna.

Hasil menggunakan UI standar (satu-satunya kanvas di mana hanya ada 1000 objek Teks UI):

Total waktu bingkai setidaknya 50 ms, dimana 40ms dihabiskan untuk memperbarui kanvas. Pada saat yang sama, objek bahkan tidak muncul, tetapi hanya bergerak.

Hasil menelurkan 1000 partikel menggunakan solusi kami:

Semua keajaiban terjadi pada GPU, bahkan pada saat menelurkan 1000 partikel, frame dihitung dalam waktu kurang dari 5ms.

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


All Articles