عرض الرسائل داخل اللعبة باستخدام نظام الجسيمات

صورة

مهمة


عند تطوير لعبتنا The Unliving ، وضعنا لأنفسنا مهمة عرض رسائل مختلفة ، مثل الضرر الذي حدث ، ونقص الصحة أو الطاقة ، وقيمة المكافأة ، وعدد النقاط الصحية المستعادة ، وما إلى ذلك ، باستخدام نظام الجسيمات. تقرر القيام بذلك من أجل الحصول على مزيد من الفرص لتخصيص تأثيرات المظهر والمزيد من السلوك لمثل هذه الرسائل ، الأمر الذي يمثل مشكلة عند استخدام العناصر القياسية لنظام واجهة المستخدم Unity.

بالإضافة إلى ذلك ، يعني هذا النهج استخدام مثيل نظام جسيمات واحد فقط لكل نوع من أنواع الرسائل ، مما يعطي زيادة كبيرة في الإنتاجية مقارنة بإخراج نفس الرسائل باستخدام Unity UI.

تقرير الضرر

صورة

رسالة نصية نقص

صورة

خوارزمية القرار


باستخدام التظليل ، نعرض النسيج المحضر مسبقًا باستخدام إحداثيات الأشعة فوق البنفسجية الصحيحة. يتم إرسال المعلومات ذات إحداثيات الأشعة فوق البنفسجية من خلال دفقين (تدفقات قمة) إلى ParticleSystem باستخدام ParticleSystem.SetCustomParticleData في شكل قائمة Vector4.

يتضمن تطبيقنا استخدام مواد تحتوي على 10 صفوف و 10 أعمدة من الأحرف. يمكن استخدام أي خط أحادي المسافة كخط. وذلك لتجنب التباعد بين حروف الرسائل.

مصدر الملمس في PSD

التنفيذ خطوة بخطوة


إنشاء Vector4 للنقل إلى Vertex Stream

لوصف مجموعة الأحرف ، سنستخدم بنية SymbolsTextureData.

يجب تعبئة مجموعة الأحرف يدويًا ، بالترتيب عن طريق إضافة جميع رموز نسيج الخط إليها بدءًا من الزاوية اليسرى العليا.

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

ونتيجة لذلك ، نحصل على فئة TextRendererParticleSystem. عندما تستدعي طريقة SpawnParticle العامة ، سوف يفرخ جسيم واحد من نظام الجسيمات إلى الموضع المطلوب ، مع القيمة واللون والحجم المطلوب.

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

يسمح لك نظام الجسيمات في الوحدة بنقل البيانات المخصصة في شكل دفقين Vector4.



لقد أضفنا عن قصد تيارًا إضافيًا باستخدام UV2 لتجنب حدوث تحول في إحداثيات الجداول. إذا لم يتم ذلك ، فسوف تتطابق الإحداثيات X و Y لمتجه Custom1 في C # مع تظليل Z و W TEXCOORD0. وبالتالي ، Custom1.z = TEXCOORD1.x ، Custom1.w = TEXCOORD1.y. الأمر الذي سيسبب الكثير من الإزعاج في المستقبل.


كما هو موضح سابقًا ، سنستخدم جهازي Vector4s لنقل طول الرسالة وإحداثيات الأشعة فوق البنفسجية من الأحرف. نظرًا لأن Vector4 يحتوي على 4 عناصر من النوع العائم ، يمكننا افتراضيًا تجميع 4 * 4 = 16 بايت من البيانات فيه. لان نظرًا لأن رسالتنا ستحتوي فقط على طول الرسالة (رقم مكون من رقمين) وإحداثيات الأحرف (رقم مكون من رقمين لكل حرف) ، فإن نطاق النوع بايت (0-255) غير ضروري بالنسبة لنا. أثناء استخدام المنازل العشرية سيكون على ما يرام.

دقة التعويم هي 6-9 أحرف ، مما يعني أنه يمكننا استخدام 6 بت بأمان من كل إحداثيات Vector4 ولا تقلق بشأن سلامة ودقة البيانات. في الواقع ، حاولنا أن نحزم 7 و 8 و 9 أحرف ، لكن دقة التعويم ليست كافية.

اتضح أنه في كل عوامة ، باستخدام المنازل العشرية ، سنقوم بتعبئة ما يصل إلى 6 أرقام ، على عكس الإصدار القياسي بأربعة بايت. الإجمالي ، سيحتوي Vector4 واحد على 24 رقمًا أحاديًا.

يمكننا نقل متجهين في الدفق ، لذلك سنستخدم كلاهما لإرسال رسائل يصل طولها إلى 23 حرفًا:

Custom1.xyzw - أول 12 حرفًا في الرسالة.
Custom2.xyzw - 11 حرفًا آخر للرسالة + طول الرسالة (آخر حرفين).

على سبيل المثال ، ستبدو الرسالة "Hello" على هذا النحو.


تتوافق إحداثيات الحرف مع رقم العمود وخط موضع الحرف في النسيج.


في الكود ، فإن التفاف سلسلة في Vector4 سيبدو كما يلي:

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

المتجه مع CustomData جاهز. حان الوقت لتفرخ جسيم جديد يدويًا بالمعلمات المطلوبة. تفرخ

الجسيمات

أول شيء يتعين علينا القيام به هو التأكد من تنشيط تدفقات CustomData في إعدادات Renderer لنظام الجسيمات:

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

لإنشاء جسيم ، نستخدم طريقة Emit () لفئة 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);

أضف كلا الكتل إلى طريقة SpawnParticle () والجزء C # جاهز: يتم تعبئة الرسالة وإرسالها بواسطة وحدة معالجة الرسومات في شكل اثنين من Vector4 في ​​تيار Vertex. الشيء الأكثر إثارة للاهتمام هو قبول هذه البيانات وعرضها بشكل صحيح.

كود شادر

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

محرر الوحدة

إنشاء المواد وتعيينها إلى تظليل لدينا. في المشهد ، قم بإنشاء كائن باستخدام مكون ParticleSystem ، وقم بتعيين المادة التي تم إنشاؤها. ثم نقوم بتعديل سلوك الجسيمات وإيقاف تشغيل معلمة Play On Awake. من أي فئة ، نسمي طريقة RendererParticleSystem.SpawnParticle () أو نستخدم طريقة المساومة.

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

يمكن العثور على رمز المصدر والموارد وأمثلة الاستخدام هنا .

نظام الرسائل في العمل

صورة

هذا كل شئ. إخراج الرسالة باستخدام نظام الجسيمات جاهز! نأمل أن يفيد هذا الحل مطوري ألعاب Unity.

UPD: أداء الحل المقترح
في تعليقات عدة أشخاص ، ثار سؤال حول أداء هذه الطريقة. قياسات خاصة من قبل ملف التعريف للوحدة. الشروط هي نفسها - 1000 قطعة متحركة ومتغيرة اللون.

نتيجة استخدام واجهة المستخدم القياسية (لوحة الرسم الوحيدة التي لا يوجد عليها سوى 1000 عنصر من نصوص واجهة المستخدم):

يبلغ إجمالي وقت الإطار 50 مللي ثانية على الأقل ، ويتم إنفاق 40 مللي ثانية على تحديث اللوحة. في الوقت نفسه ، لا تفرخ الأشياء ، بل تتحرك ببساطة.

نتيجة تولد 1000 جزيء باستخدام محلولنا:

يحدث كل السحر في وحدة معالجة الرسومات ، حتى في وقت تكاثر 1000 جسيم ، يتم حساب الإطار في أقل من 5 مللي ثانية.

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


All Articles