في الآونة الأخيرة ، كنت بحاجة إلى حل مشكلة شائعة جدًا في العديد من الألعاب مع عرض أعلى: لتقديم مجموعة كاملة من أشرطة صحة العدو على الشاشة. شيء مثل هذا:
من الواضح ، كنت أرغب في القيام بذلك بأكبر قدر ممكن من الكفاءة ، ويفضل في مكالمة سحب واحدة. كالعادة ، قبل أن أبدأ العمل ، قمت ببعض الأبحاث عبر الإنترنت حول قرارات الآخرين ، وكانت النتائج مختلفة للغاية.
لن أحزن أحداً على الكود ، لكن يكفي أن أقول إن بعض الحلول لم تكن رائعة تمامًا ، على سبيل المثال ، أضاف شخص ما قطعة قماش إلى كل عدو (وهو غير فعال للغاية).
تختلف الطريقة التي توصلت إليها كنتيجة مختلفة قليلاً عن كل ما رأيته في الآخرين ، ولا تستخدم أي فئات لواجهة المستخدم (بما في ذلك Canvas) على الإطلاق ، لذلك قررت توثيقها للجمهور. بالنسبة لأولئك الذين يرغبون في تعلم الكود المصدري ، قمت بنشره
على جيثب .
لماذا لا تستخدم Canvas؟
من الواضح أن لوحة واحدة لكل عدو كان قرارًا سيئًا ، لكن يمكنني استخدام لوحة قماش مشتركة لجميع الأعداء ؛ قماش واحد من شأنه أن يؤدي أيضا إلى جعل الخلط الدعوة.
ومع ذلك ، أنا لا أحب مقدار العمل المنجز في كل إطار يتعلق بهذا النهج. إذا كنت تستخدم Canvas ، فعليك في كل إطار تنفيذ العمليات التالية:
- حدد أيًا من الأعداء على الشاشة ، وحدد كل منهم من شريط واجهة مستخدم pool.
- عرض موقف العدو في الكاميرا لوضع الشريط.
- تغيير حجم الجزء "ملء" من الشريط ، ربما مثل صورة.
- على الأرجح تغيير حجم الشرائط وفقًا لنوع الأعداء ؛ على سبيل المثال ، يجب أن يكون للأعداء الكبار شرائط كبيرة بحيث لا تبدو سخيفة.
على أي حال ، كل هذا من شأنه أن يلوث المخازن المؤقتة هندسة Canvas ويؤدي إلى إعادة إنشاء جميع بيانات قمة الرأس في المعالج. لم أكن أريد أن يتم كل هذا لمثل هذا العنصر البسيط.
باختصار عن قراري
وصف موجز لعملية عملي:
- نعلق كائنات من شرائط الطاقة للأعداء في 3D.
- هذا يسمح لك بترتيب وتقطيع الشرائط تلقائيًا.
- يمكن ضبط موضع / حجم الشريط وفقًا لنوع العدو.
- سنقوم بتوجيه المشارب إلى الكاميرا في الكود باستخدام التحويل ، والذي لا يزال موجودًا.
- يضمن التظليل دائمًا تقديم كل شيء.
- نحن نستخدم Instancing لتقديم جميع الشرائط في مكالمة سحب واحدة.
- نحن نستخدم إحداثيات الأشعة فوق البنفسجية الإجرائية البسيطة لعرض مستوى ملء الشريط.
الآن دعونا ننظر إلى الحل بمزيد من التفاصيل.
ما هو Instancing؟
في العمل مع الرسومات ، تم استخدام التقنية القياسية منذ فترة طويلة: يتم دمج العديد من الكائنات معًا بحيث تحتوي على بيانات ومواد رأسية مشتركة ويمكن عرضها في مكالمة سحب واحدة. هذا هو بالضبط ما نحتاج إليه ، لأن كل مكالمة سحب هي حمل إضافي على وحدة المعالجة المركزية والجرافيك. بدلاً من إجراء مكالمة سحب واحدة لكل كائن ، نعرضها جميعًا مرة واحدة ونستخدم تظليلًا لإضافة تباين لكل نسخة.
يمكنك القيام بذلك يدويًا عن طريق تكرار بيانات قمة الرأس الشبكية X مرات في مخزن مؤقت واحد ، حيث يمثل X الحد الأقصى لعدد النسخ التي يمكن تقديمها ، ثم استخدام صفيف معلمات التظليل لتحويل / تلوين / تغيير كل نسخة. يجب أن تخزن كل نسخة المعرفة حول المثيل المُرقّم ، من أجل استخدام هذه القيمة كفهرس للصفيف. بعد ذلك ، يمكننا استخدام استدعاء التجميع المفهرس ، والذي يطلب "التجسيد فقط إلى N" ، حيث N هو عدد الحالات المطلوبة
فعليًا في الإطار الحالي ، أقل من الحد الأقصى لعدد X.
تحتوي معظم واجهات برمجة التطبيقات الحديثة بالفعل على رمز لهذا ، لذلك لا تحتاج إلى القيام بذلك يدويًا. تسمى هذه العملية "Instancing" ؛ في الواقع ، تقوم بأتمتة العملية الموضحة أعلاه بقيود محددة مسبقًا.
يدعم محرك الوحدة أيضًا إنشاء التطبيقات ، وله واجهة برمجة تطبيقات خاصة به ومجموعة من وحدات ماكرو التظليل التي تساعد في تنفيذه. يستخدم افتراضات معينة ، على سبيل المثال ، أن كل مثيل يتطلب تحويل ثلاثي الأبعاد كامل. بالمعنى الدقيق للكلمة ، بالنسبة للشرائط ثنائية الأبعاد ، ليست هناك حاجة إليها تمامًا - يمكننا القيام بالتبسيطات ، ولكن نظرًا لوجودها ، سوف نستخدمها. سيؤدي ذلك إلى تبسيط تظليلنا ، وكذلك توفير القدرة على استخدام المؤشرات ثلاثية الأبعاد ، على سبيل المثال ، الدوائر أو الأقواس.
الطبقة قابلة للكسر
سيكون لأعدائنا عنصر يسمى
Damageable
، مما يمنحهم الصحة ويسمح لهم بأخذ أضرار من التصادم. في مثالنا ، الأمر بسيط للغاية:
public class Damageable : MonoBehaviour { public int MaxHealth; public float DamageForceThreshold = 1f; public float DamageForceScale = 5f; public int CurrentHealth { get; private set; } private void Start() { CurrentHealth = MaxHealth; } private void OnCollisionEnter(Collision other) {
كائن HealthBar: الموضع / الدوران
كائن شريط الصحة بسيط للغاية: في الواقع ، إنه مجرد رباعية متصلة بالعدو.

نستخدم
مقياس هذا الكائن لجعل الشريط طويلًا ورقيقًا ، ونضعه فوق العدو مباشرةً. لا تقلق بشأن دورته ، فسنقوم بإصلاحه باستخدام الكود المرفق بالكائن في
HealthBar.cs
:
private void AlignCamera() { if (mainCamera != null) { var camXform = mainCamera.transform; var forward = transform.position - camXform.position; forward.Normalize(); var up = Vector3.Cross(forward, camXform.right); transform.rotation = Quaternion.LookRotation(forward, up); } }
هذا الكود دائمًا يوجه الكواد باتجاه الكاميرا. يمكننا إجراء تغيير الحجم والتناوب في التظليل ، لكنني أطبقه هنا لسببين.
أولاً ، يستخدم Unity instancing دائمًا التحويل الكامل لكل كائن ، وبما أننا نقوم بنقل جميع البيانات على أي حال ، يمكنك استخدامه. ثانياً ، إن ضبط المقياس / الدوران هنا يضمن أن يكون المخطط المتوازي المحيط لتقطيع الشريط صحيحًا دائمًا. إذا جعلنا مهمة الحجم والتناوب مسؤولية التظليل ، فإن Unity يمكنها اقتطاع الأشرطة التي يجب أن تكون مرئية عندما تكون قريبة من حواف الشاشة ، لأن حجم وتناوب متوازي الاضلاع المحيط به لن يتوافق مع ما سنقوم بتقديمه. بالطبع ، يمكننا تنفيذ طريقة الاقتطاع الخاصة بنا ، لكن من الأفضل عادة استخدام ما لدينا إذا كان ذلك ممكنًا (رمز الوحدة أصلي وله حق الوصول إلى البيانات المكانية أكثر مما نفعل).
سأشرح كيف يتم تقديم الشريط بعد إلقاء نظرة على التظليل.
شادر هيلث بار
في هذا الإصدار ، سنقوم بإنشاء شريط أحمر وأخضر كلاسيكي بسيط.
يمكنني استخدام نسيج 2 × 1 مع بكسل أخضر واحد على اليسار وواحد أحمر على اليمين. بطبيعة الحال ، قمت بإيقاف تشغيل mipmapping والتصفية والضغط ، واضبط معلمة وضع العنونة على Clamp ، مما يعني أن البيكسلات الموجودة في الشريط لدينا ستكون دائمًا باللون الأخضر أو الأحمر ، ولن تنتشر حول الحواف. سيتيح لنا ذلك تغيير إحداثيات النسيج في التظليل لتغيير الخط الفاصل بين البيكسلات الحمراء والخضراء لأعلى ولأعلى الشريط.
(نظرًا لوجود لونين فقط هنا ، يمكنني فقط استخدام وظيفة الخطوة في التظليل للعودة إلى نقطة واحدة أو أخرى. ومع ذلك ، فإن هذه الطريقة ملائمة لأنك تستطيع استخدام نسيج أكثر تعقيدًا إذا رغبت في ذلك ، وسيعمل هذا بشكل مشابه أثناء الانتقال منتصف الملمس.)أولاً ، سنعلن الخصائص التي نحتاجها:
Shader "UI/HealthBar" { Properties { _MainTex ("Texture", 2D) = "white" {} _Fill ("Fill", float) = 0 }
_MainTex
هو نسيج أحمر وأخضر ، و
_Fill
قيمة من 0 إلى 1 ، حيث 1 هو كامل الصحة.
بعد ذلك ، نحتاج إلى طلب الشريط لعرضه في قائمة انتظار التراكب ، مما يعني تجاهل كل العمق في المشهد وعرضه فوق كل شيء:
SubShader { Tags { "Queue"="Overlay" } Pass { ZTest Off
الجزء التالي هو رمز التظليل نفسه. نكتب تظليلًا بدون إضاءة (غير مضاءة) ، لذلك لا داعي للقلق بشأن التكامل مع تظليلات سطح الوحدة المختلفة ، فهذا زوج تظليل بسيط من قمة الرأس / الأجزاء. أولاً ، اكتب bootstrap:
CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc"
بالنسبة للجزء الأكبر ، هذا هو bootstrap القياسي ، باستثناء
#pragma multi_compile_instancing
، والذي يخبر برنامج التحويل البرمجي Unity بما يحتاج إلى ترجمة لـ Instancing.
يجب أن تتضمن بنية قمة الرأس بيانات مثيل ، لذلك سنفعل ما يلي:
struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID };
نحتاج أيضًا إلى تحديد ما سيكون بالضبط في بيانات الحالات ، بالإضافة إلى ما تقوم به الوحدة (تحويل) بالنسبة لنا:
UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _Fill) UNITY_INSTANCING_BUFFER_END(Props)
لذلك نحن نبلغ عن ضرورة قيام الوحدة بإنشاء مخزن مؤقت يسمى "الدعائم" لتخزين البيانات لكل مثيل ، وفي داخله سنستخدم تعويم واحد لكل مثيل لخاصية تسمى
_Fill
.
يمكنك استخدام عدة مخازن. يجدر القيام به إذا كان لديك العديد من الخصائص التي تم تحديثها بترددات مختلفة ؛ بتقسيمهم ، على سبيل المثال ، لا يمكنك تغيير مخزن مؤقت واحد عند تغيير آخر ، وهو أكثر كفاءة. لكننا لسنا بحاجة إلى هذا.
يعمل تظليل قمة الرأس الخاص بنا تمامًا على العمل القياسي ، نظرًا لأن الحجم والموضع والدوران يتم نقلهما بالفعل للتحويل. يتم تطبيق ذلك باستخدام
UnityObjectToClipPos
، والذي يستخدم تلقائيًا تحويل كل مثيل. يمكن للمرء أن يتخيل أنه بدون إثبات هذا سيكون عادةً استخدام بسيط لخاصية مصفوفة واحدة. ولكن عند استخدام instancing داخل المحرك ، يبدو وكأنه مجموعة من المصفوفات ، ويختار Unity بشكل مستقل مصفوفة مناسبة لهذه الحالة.
تحتاج أيضًا إلى تغيير UV لتغيير موقع نقطة الانتقال من الأحمر إلى الأخضر وفقًا لخاصية
_Fill
. إليك مقتطف الشفرة ذي الصلة:
UNITY_SETUP_INSTANCE_ID(v); float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill);
UNITY_SETUP_INSTANCE_ID
و
UNITY_ACCESS_INSTANCED_PROP
بكل السحر عن طريق الوصول إلى الإصدار الصحيح من خاصية
_Fill
من المخزن المؤقت الثابت لهذه الحالة.
نحن نعلم أنه في الحالة الطبيعية ، فإن إحداثيات الأشعة فوق البنفسجية للرباع تغطي الفاصل الزمني للنسيج بأكمله ، وأن خط التقسيم للشريط يقع في منتصف النسيج أفقيًا. لذلك ، تقوم الحسابات الرياضية الصغيرة بتحريك الشريط أفقياً إلى اليسار أو اليمين ، وتضمن قيمة المشبك للنسيج ملء الجزء المتبقي.
لا يمكن أن يكون تظليل الأجزاء أكثر بساطة لأن كل العمل قد تم بالفعل:
return tex2D(_MainTex, i.uv);
يتوفر رمز تظليل التعليق الكامل في
مستودع جيثب .
Healthbar المواد
ثم كل شيء بسيط - نحتاج فقط إلى تخصيص المواد التي يستخدمها هذا التظليل لشريطنا. لا يلزم القيام بأي شيء تقريبًا ، ما عليك سوى تحديد التظليل المرغوب في الجزء العلوي ، وتعيين نسيج أحمر وأخضر ، والأهم من ذلك ،
تحديد مربع "تمكين GPU Instancing" .

HealthBar ملء تحديث الملكية
لذلك ، لدينا كائن شريط الصحة والتظليل والمواد المراد تقديمها ، والآن نحتاج إلى تعيين خاصية
_Fill
لكل مثيل. ونحن نفعل ذلك داخل
HealthBar.cs
النحو التالي:
private void UpdateParams() { meshRenderer.GetPropertyBlock(matBlock); matBlock.SetFloat("_Fill", damageable.CurrentHealth / (float)damageable.MaxHealth); meshRenderer.SetPropertyBlock(matBlock); }
نقوم بتحويل
CurrentHealth
class إلى قيمة من 0 إلى 1 ، بتقسيمها على
MaxHealth
. ثم
_Fill
خاصية
_Fill
باستخدام
MaterialPropertyBlock
.
إذا لم تكن قد استخدمت
MaterialPropertyBlock
لنقل البيانات إلى تظليل ، حتى دون حدوث ذلك ، فأنت بحاجة إلى دراستها. لم يتم شرحها بشكل جيد في وثائق الوحدة ، ولكنها الطريقة الأكثر فعالية لنقل البيانات من كل كائن إلى تظليل.
في حالتنا ، عند استخدام instancing ، يتم تعبئة القيم لجميع أشرطة الصحة في مخزن مؤقت ثابت بحيث يمكن نقلها معا ورسمها في وقت واحد.
لا يوجد شيء تقريبًا هنا باستثناء نموذج مرجعي لإعداد المتغيرات ، والرمز ممل إلى حد ما ؛ انظر
مستودع جيثب لمزيد من التفاصيل.
عرض
يحتوي
مستودع GitHub على عرض تجريبي يتم فيه تدمير مجموعة من المكعبات الزرقاء الشريرة بواسطة كرات حمراء بطولية (يا هلا!) ، مع الأخذ في الاعتبار الأضرار التي تظهرها المشارب الموضحة في المقالة. عرض توضيحي مكتوب في الوحدة 2018.3.6f1.
يمكن ملاحظة تأثير استخدام instancing بطريقتين:
لوحة الإحصائيات
بعد النقر فوق "تشغيل" ، انقر فوق الزر "الإحصائيات" أعلى لوحة اللعبة. هنا يمكنك معرفة عدد مكالمات السحب التي يتم حفظها بفضل instancing:

بعد بدء اللعبة ، يمكنك النقر فوق مادة HealthBar وإلغاء
تحديد خانة الاختيار "تمكين GPU Instancing" ، وبعد ذلك سيتم تقليل عدد المكالمات المحفوظة إلى صفر.
مصحح الإطار
بعد بدء اللعبة ، انتقل إلى Window> Analysis> Frame Debugger ، ثم انقر فوق "Enable" (تمكين) في النافذة التي تظهر.
في الجزء السفلي الأيسر ، سترى جميع عمليات التقديم المنجزة. لاحظ أنه على الرغم من وجود العديد من التحديات المنفصلة للأعداء والأصداف (إذا كنت ترغب في ذلك ، فيمكنك تطبيق الاقتراع لهم أيضًا). إذا قمت بالتمرير إلى أسفل ، سترى العنصر "رسم شبكة Healthbar (instanced) Healthbar".
هذه المكالمة الواحدة تجعل جميع الشرائط. إذا نقرت على هذه العملية ، ثم على العملية عليها ، سترى أن جميع الشرائط تختفي ، لأنها مرسومة في مكالمة واحدة. إذا كنت موجودًا في "مصحح أخطاء الإطارات" ، فقم بإلغاء تحديد خانة الاختيار "تمكين GPU Instancing" من المادة ، فسترى أن سطرًا واحدًا تحول إلى عدة خطوط ، وبعد تعيين العلامة مرة أخرى في سطر.
كيفية توسيع هذا النظام
كما قلت سابقًا ، نظرًا لأن هذه القضبان الصحية عبارة عن أشياء حقيقية ، فلا يوجد ما يمنعك من تحويل أشرطة ثنائية الأبعاد بسيطة إلى شيء أكثر تعقيدًا. يمكن أن تكون نصف دائرة تحت أعداء تتناقص في القوس ، أو تدور المعينات فوق رؤوسهم. باستخدام نفس الطريقة ، لا يزال بإمكانك تقديمها جميعًا في مكالمة واحدة.