Unity قم بتحسين لعبتك باستخدام ملف التعريف
يعد الأداء جانبًا رئيسيًا في أي لعبة وليس من المستغرب، بغض النظر عن مدى جودة اللعبة، إذا تم تشغيلها بشكل سيء على جهاز المستخدم، فلن تشعر بالمتعة.
نظرًا لأنه ليس لدى كل شخص جهاز كمبيوتر أو جهاز متطور (إذا كنت تستهدف الهاتف المحمول)، فمن المهم أن تضع الأداء في الاعتبار أثناء عملية التطوير بأكملها.
هناك عدة أسباب وراء إمكانية تشغيل اللعبة ببطء:
- العرض (عدد كبير جدًا من الشبكات عالية التظليل أو التظليلات المعقدة أو تأثيرات الصورة)
- الصوت (يرجع السبب في الغالب إلى إعدادات استيراد الصوت غير الصحيحة)
- تعليمات برمجية غير محسنة (البرامج النصية التي تحتوي على وظائف تتطلب الأداء في أماكن خاطئة)
في هذا البرنامج التعليمي، سأعرض كيفية تحسين التعليمات البرمجية الخاصة بك بمساعدة Unity Profiler.
منشئ ملفات التعريف
تاريخيًا، كان تصحيح الأداء في Unity مهمة شاقة، ولكن منذ ذلك الحين، تمت إضافة ميزة جديدة تسمى Profiler.
أداة التعريف هي أداة في Unity تتيح لك تحديد الاختناقات في لعبتك بسرعة من خلال مراقبة استهلاك الذاكرة، مما يبسط عملية التحسين إلى حد كبير.
أداء سيء
يمكن أن يحدث الأداء السيئ في أي وقت: لنفترض أنك تعمل على مثيل العدو وعندما تضعه في المشهد، فإنه يعمل بشكل جيد دون أي مشاكل، ولكن عندما تنتج المزيد من الأعداء، قد تلاحظ إطارًا في الثانية (إطار في الثانية) ) تبدأ في الانخفاض.
تحقق من المثال أدناه:
في المشهد، لدي مكعب مرفق به برنامج نصي، والذي يحرك المكعب من جانب إلى آخر ويعرض اسم الكائن:
SC_ShowName.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
}
بالنظر إلى الإحصائيات، يمكننا أن نرى أن اللعبة تعمل بمعدل جيد يزيد عن 800 إطارًا في الثانية، لذلك بالكاد يكون لها أي تأثير على الأداء.
لكن دعونا نرى ماذا سيحدث عندما نكرر المكعب 100 مرة:
انخفض معدل الإطارات في الثانية بأكثر من 700 نقطة!
ملاحظة: تم إجراء جميع الاختبارات مع تعطيل Vsync
بشكل عام، إنها فكرة جيدة أن تبدأ في التحسين عندما تبدأ اللعبة في إظهار التأتأة أو التجمد أو انخفاض معدل الإطارات في الثانية إلى أقل من 120.
كيفية استخدام ملف التعريف؟
لبدء استخدام Profiler، ستحتاج إلى:
- ابدأ لعبتك بالضغط على Play
- افتح ملف التعريف بالانتقال إلى Window -> Analysis -> Profiler (أو اضغط على Ctrl + 7)
- ستظهر نافذة جديدة تبدو كالتالي:
- قد يبدو الأمر مخيفًا في البداية (خصوصًا مع كل تلك الرسوم البيانية وما إلى ذلك)، ولكنه ليس الجزء الذي سننظر إليه.
- انقر فوق علامة التبويب الجدول الزمني وقم بتغييره إلى التسلسل الهرمي:
- ستلاحظ وجود 3 أقسام (EditorLoop، وPlayerLoop، وProfiler.CollectEditorStats):
- قم بتوسيع PlayerLoop لرؤية جميع الأجزاء التي يتم إنفاق قوة الحساب فيها (ملاحظة: إذا لم يتم تحديث قيم PlayerLoop، فانقر فوق الزر "Clear" الموجود أعلى نافذة ملف التعريف).
للحصول على أفضل النتائج، قم بتوجيه شخصية لعبتك إلى الموقف (أو المكان) الذي تتأخر فيه اللعبة كثيرًا وانتظر بضع ثوانٍ.
- بعد الانتظار قليلاً، أوقف اللعبة ولاحظ قائمة PlayerLoop
أنت بحاجة إلى إلقاء نظرة على قيمة GC Alloc، والتي تشير إلى تخصيص مجموعة البيانات المهملة. هذا هو نوع الذاكرة الذي تم تخصيصه بواسطة component ولكن لم تعد هناك حاجة إليه وينتظر تحريره بواسطة مجموعة البيانات المهملة. من الناحية المثالية، يجب ألا يُنشئ الكود أي بيانات غير صحيحة (أو يكون قريبًا من 0 قدر الإمكان).
يعد الوقت ms أيضًا قيمة مهمة، فهو يوضح المدة التي يستغرقها تشغيل التعليمات البرمجية بالمللي ثانية، لذلك من الناحية المثالية، يجب أن تهدف إلى تقليل هذه القيمة أيضًا (عن طريق تخزين القيم مؤقتًا، وتجنب استدعاء الوظائف التي تتطلب الأداء في كل تحديث، وما إلى ذلك).).
لتحديد الأجزاء المزعجة بشكل أسرع، انقر فوق عمود GC Alloc لفرز القيم من الأعلى إلى الأقل)
- في مخطط استخدام وحدة المعالجة المركزية (CPU)، انقر في أي مكان للانتقال إلى هذا الإطار. على وجه التحديد، نحتاج إلى إلقاء نظرة على القمم، حيث كان معدل الإطارات في الثانية هو الأدنى:
وهنا ما كشف عنه ملف التعريف:
يخصص GUI.Repaint 45.4 كيلو بايت، وهو عدد كبير جدًا، وقد كشف توسيعه عن مزيد من المعلومات:
- يوضح أن معظم التخصيصات تأتي من طريقة GUIUtility.BeginGUI() وOnGUI() في البرنامج النصي SC_ShowName، مع العلم أنه يمكننا البدء في التحسين.
GUIUtility.BeginGUI() يمثل أسلوب OnGUI() فارغًا (نعم، حتى أسلوب OnGUI() الفارغ يخصص قدرًا كبيرًا من الذاكرة).
استخدم Google (أو أي محرك بحث آخر) للعثور على الأسماء التي لا تعرفها.
إليك الجزء OnGUI() الذي يحتاج إلى التحسين:
void OnGUI()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
تحسين
لنبدأ بالتحسين.
يستدعي كل برنامج نصي SC_ShowName أسلوب OnGUI() الخاص به، وهو أمر غير جيد بالنظر إلى أن لدينا 100 مثيل. إذن ما الذي يمكن فعله حيال ذلك؟ الجواب هو: أن يكون لديك برنامج نصي واحد باستخدام طريقة OnGUI() الذي يستدعي طريقة واجهة المستخدم الرسومية لكل مكعب.
- أولاً، قمت باستبدال OnGUI() الافتراضي في البرنامج النصي SC_ShowName بالفراغ العام GUIMethod() والذي سيتم استدعاؤه من برنامج نصي آخر:
public void GUIMethod()
{
//Show object name on screen
Camera mainCamera = Camera.main;
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
}
- ثم قمت بإنشاء برنامج نصي جديد وأطلق عليه اسم SC_GUIMethod:
SC_GUIMethod.cs
using UnityEngine;
public class SC_GUIMethod : MonoBehaviour
{
SC_ShowName[] instances; //All instances where GUI method will be called
void Start()
{
//Find all instances
instances = FindObjectsOfType<SC_ShowName>();
}
void OnGUI()
{
for(int i = 0; i < instances.Length; i++)
{
instances[i].GUIMethod();
}
}
}
سيتم إرفاق SC_GUIMethod بكائن عشوائي في المشهد واستدعاء كافة أساليب واجهة المستخدم الرسومية.
- لقد انتقلنا من وجود 100 طريقة فردية لـ OnGUI() إلى طريقة واحدة فقط، فلنضغط على زر التشغيل ونرى النتيجة:
- GUIUtility.BeginGUI() يخصص الآن 368 بايت فقط بدلاً من 36.7 كيلو بايت، وهو تخفيض كبير!
ومع ذلك، لا يزال أسلوب OnGUI() يخصص الذاكرة، ولكن بما أننا نعرف أنه يستدعي GUIMethod() فقط من البرنامج النصي SC_ShowName، فإننا نتجه مباشرة إلى تصحيح أخطاء هذا الأسلوب.
لكن ملف التعريف يعرض فقط المعلومات العامة، كيف يمكننا أن نرى ما يحدث بالضبط داخل الطريقة؟
لتصحيح الأخطاء داخل الطريقة، يحتوي Unity على واجهة برمجة تطبيقات سهلة الاستخدام تسمى Profiler.BeginSample
يتيح لك Profiler.BeginSample التقاط قسم معين من البرنامج النصي، مما يوضح الوقت المستغرق لإكماله وحجم الذاكرة المخصصة.
- قبل استخدام فئة Profiler في التعليمات البرمجية، نحتاج إلى استيراد مساحة الاسم UnityEngine.Profiling في بداية البرنامج النصي:
using UnityEngine.Profiling;
- يتم التقاط نموذج ملف التعريف عن طريق إضافة Profiler.BeginSample("SOME_NAME"); في بداية الالتقاط وإضافة Profiler.EndSample(); في نهاية الالتقاط، مثل هذا:
Profiler.BeginSample("SOME_CODE");
//...your code goes here
Profiler.EndSample();
نظرًا لأنني لا أعرف أي جزء من GUIMethod() يتسبب في تخصيص الذاكرة، فقد قمت بإحاطة كل سطر في Profiler.BeginSample وProfiler.EndSample (ولكن إذا كانت طريقتك تحتوي على الكثير من الأسطر، فلن تحتاج بالتأكيد إلى تضمينها كل سطر، ما عليك سوى تقسيمه إلى أجزاء متساوية ثم العمل من هناك).
فيما يلي الطريقة النهائية مع تطبيق Profiler Samples:
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
Profiler.EndSample();
}
- الآن أضغط على "تشغيل" وأرى ما يظهر في ملف التعريف:
- للراحة، قمت بالبحث عن "sc_show_" في ملف التعريف، حيث أن جميع العينات تبدأ بهذا الاسم.
- مثير للاهتمام... تم تخصيص قدر كبير من الذاكرة في الجزء الثالث من sc_show_names، والذي يتوافق مع هذا الجزء من الكود:
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), gameObject.name);
بعد إجراء بعض البحث على Google، اكتشفت أن الحصول على اسم الكائن يخصص قدرًا كبيرًا من الذاكرة. الحل هو تعيين اسم الكائن لمتغير سلسلة في void Start()، وبهذه الطريقة سيتم استدعاؤه مرة واحدة فقط.
هنا هو الكود الأمثل:
SC_ShowName.cs
using UnityEngine;
using UnityEngine.Profiling;
public class SC_ShowName : MonoBehaviour
{
bool moveLeft = true;
float movedDistance = 0;
string objectName = "";
// Start is called before the first frame update
void Start()
{
moveLeft = Random.Range(0, 10) > 5;
objectName = gameObject.name; //Store Object name to a variable
}
// Update is called once per frame
void Update()
{
//Move left and right in ping-pong fashion
if (moveLeft)
{
if(movedDistance > -2)
{
movedDistance -= Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x -= Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = false;
}
}
else
{
if (movedDistance < 2)
{
movedDistance += Time.deltaTime;
Vector3 currentPosition = transform.position;
currentPosition.x += Time.deltaTime;
transform.position = currentPosition;
}
else
{
moveLeft = true;
}
}
}
public void GUIMethod()
{
//Show object name on screen
Profiler.BeginSample("sc_show_name part 1");
Camera mainCamera = Camera.main;
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 2");
Vector2 screenPos = mainCamera.WorldToScreenPoint(transform.position + new Vector3(0, 1, 0));
Profiler.EndSample();
Profiler.BeginSample("sc_show_name part 3");
GUI.color = Color.green;
GUI.Label(new Rect(screenPos.x - 150/2, Screen.height - screenPos.y, 150, 25), objectName);
Profiler.EndSample();
}
}
- دعونا نرى ما يعرضه ملف التعريف:
يتم تخصيص كافة العينات 0B، لذلك لا يتم تخصيص المزيد من الذاكرة.