كيف قمنا بتحسين النصوص في الوحدة

هناك العديد من المقالات والدروس أداء الوحدة كبيرة. لا نحاول استبدال هذه المقالة أو تحسينها ، فهذا مجرد ملخص موجز للخطوات التي اتخذناها بعد قراءة هذه المقالات ، وكذلك الخطوات التي سمحت لنا بحل مشكلاتنا. أوصي بشدة أن تدرس المواد على الأقل على الموقع https://learn.unity.com/ .

في عملية تطوير لعبتنا ، واجهنا مشاكل من وقت لآخر تسبب في تثبيط عملية اللعبة. بعد قضاء بعض الوقت في Unity Profiler ، وجدنا نوعين من المشاكل:

  • تظليل غير محسن
  • نصوص غير محسّنة في C #

حدثت معظم المشكلات بسبب المجموعة الثانية ، لذلك قررت التركيز على البرامج النصية C # في هذه المقالة (ربما أيضًا لأنني لم أكتب تظليلًا واحدًا في حياتي).

البحث عن نقاط الضعف


الغرض من هذه المقالة ليس لكتابة تعليمي حول استخدام منشئ ملفات التعريف؛ أردت فقط أن أتحدث عن ما كنا مهتمين به بشكل رئيسي أثناء عملية التنميط.

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


بناء إعدادات للتوصيف

إذا حاولت استخدام google "Random lag in Unity" أو طلب آخر مشابه ، فستجد أن معظم الناس يوصون بالتركيز على جمع القمامة ، وهذا هو السبب في أنني فعلت ذلك. يتم إنشاء البيانات المهملة في كل مرة تتوقف فيها عن استخدام بعض الكائنات (مثيل فئة) ، وبعد ذلك يبدأ جامع البيانات المهملة من الوحدة من وقت لآخر لتنظيف الفوضى وتحرير الذاكرة ، الأمر الذي يستغرق وقتًا مجنونًا ويؤدي إلى انخفاض في معدل الإطارات.

كيفية العثور على البرامج النصية غير المرغوب فيه في منشئ ملفات التعريف؟


ما عليك سوى اختيار استخدام وحدة المعالجة المركزية -> اختر طريقة العرض "التسلسل الهرمي" -> "فرز حسب GC Alloc"


خيارات منشئ ملفات التعريف لجمع القمامة

مهمتك هي تحقيق بعض الأصفار في عمود تخصيص GC لمشهد اللعب.

هناك طريقة أخرى جيدة وهي فرز الإدخالات حسب Time ms (وقت التشغيل) وتحسين البرامج النصية بحيث تستغرق أقل وقت ممكن. كان لهذه الخطوة تأثير كبير بالنسبة لنا ، لأن أحد مكوناتنا يحتوي على جزء كبير للحلقة ، استغرق إكماله إلى الأبد (نعم ، لم نعثر بعد على طريقة للتخلص من الحلقة) ، لذلك كان تحسين وقت تنفيذ جميع البرامج النصية ضروريًا للغاية بالنسبة لنا ، لأننا كنا بحاجة إلى توفير وقت التشغيل على هذا المكلف للحلقة ، مع الحفاظ على تردد ثابت قدره 60 إطارًا في الثانية.

بناءً على بيانات ملفات التعريف ، قسمت التحسين إلى قسمين:

  • التخلص من القمامة
  • انخفاض المهلة

الجزء 1: محاربة القمامة


في هذا الجزء سأخبرك بما فعلناه للتخلص من القمامة. هذه هي المعرفة الأساسية التي يجب أن يفهمها أي مطور ؛ لقد أصبحوا جزءًا مهمًا من تحليلنا اليومي في كل طلب سحب / دمج.

القاعدة الأولى: لا توجد كائنات جديدة في أساليب التحديث


من الناحية المثالية ، يجب ألا تحتوي أساليب التحديث و FixedUpdate و LateUpdate على الكلمات الأساسية "الجديدة" . يجب عليك دائمًا استخدام ما لديك بالفعل.

أحيانًا يكون إنشاء كائن جديد مخفيًا في بعض أساليب الوحدة الداخلية ، لذلك ليس واضحًا للغاية. سنتحدث عن هذا لاحقا.

القاعدة الثانية: إنشاء مرة واحدة وإعادة استخدامها!


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

الرمز الذي:

  • يخلق حالات جديدة
  • تبحث عن أي كائنات اللعبة

يجب أن تحاول دائمًا الانتقال من أساليب التحديث إلى البدء أو الاستيقاظ.

فيما يلي أمثلة على تغييراتنا:

تخصيص ذاكرة للقوائم في طريقة البدء ، وإزالتها (مسح) وإعادة استخدامها إذا لزم الأمر.

//Bad code private List<GameObject> objectsList; void Update() { objectsList = new List<GameObject>(); objectsList.Add(......) } //Better Code private List<GameObject> objectsList; void Start() { objectsList = new List<GameObject>(); } void Update() { objectsList.Clear(); objectsList.Add(......) } 

تخزين الروابط وإعادة استخدامها على النحو التالي:

 //Bad code void Update() { var levelObstacles = FindObjectsOfType<Obstacle>(); foreach(var obstacle in levelObstacles) { ....... } } //Better code private Object[] levelObstacles; void Start() { levelObstacles = FindObjectsOfType<Obstacle>(); } void Update() { foreach(var obstacle in levelObstacles) { ....... } } 

ينطبق الشيء نفسه على أسلوب FindGameObjectsWithTag أو أي طريقة أخرى تقوم بإرجاع صفيف جديد.

القاعدة الثالثة: احذر من الأوتار وتجنب تسلسلها


عندما يتعلق الأمر بإنشاء القمامة ، فإن الخطوط فظيعة. حتى أبسط عمليات السلسلة يمكن أن تخلق الكثير من القمامة. لماذا؟ الأوتار صفائف ، وهذه الصفائف غير قابلة للتغيير. هذا يعني أنه في كل مرة تقوم فيها بتسلسل سطرين ، يتم إنشاء مجموعة جديدة ، ويتحول القديم إلى نفاية. لحسن الحظ ، يمكن استخدام StringBuilder لتجنب إنشاء مثل هذه النفايات أو تقليلها.

فيما يلي مثال على كيفية تحسين الموقف:

 //Bad code void Start() { text = GetComponent<Text>(); } void Update() { text.text = "Player " + name + " has score " + score.toString(); } //Better code void Start() { text = GetComponent<Text>(); builder = new StringBuilder(50); } void Update() { //StringBuilder has overloaded Append method for all types builder.Length = 0; builder.Append("Player "); builder.Append(name); builder.Append(" has score "); builder.Append(score); text.text = builder.ToString(); } 

كل شيء على ما يرام مع المثال الموضح أعلاه ، ولكن لا يزال هناك العديد من الاحتمالات لتحسين الكود. كما ترون ، يمكن اعتبار السلسلة بأكملها تقريبًا ثابتة. نقسم السلسلة إلى جزأين لكائنين UI.Text. أولاً ، يحتوي واحد فقط على النص الثابت "Player" + name + "has score" ، والذي يمكن تعيينه في طريقة البدء ، والثاني يحتوي على قيمة النتيجة ، والتي يتم تحديثها في كل إطار. قم دائمًا بجعل الخطوط الثابتة ثابتة حقًا وتوليدها في طريقة البدء أو الاستيقاظ . بعد هذا التحسن ، كل شيء تقريباً في حالة جيدة ، ولكن لا يزال يتم إنشاء القليل من البيانات المهملة عند استدعاء Int.ToString () ، Float.ToString () ، إلخ.

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

 public static readonly string[] NUMBERS_THREE_DECIMAL = { "000", "001", "002", "003", "004", "005", "006",.......... 

القاعدة الرابعة: قيم ذاكرة التخزين المؤقت التي تم إرجاعها بواسطة طرق الوصول


قد يكون هذا أمرًا صعبًا للغاية ، لأنه حتى طريقة الوصول البسيطة مثل تلك الموضحة أدناه تولد البيانات المهملة:

 //Bad Code void Update() { gameObject.tag; //or gameObject.name; } 

حاول تجنب استخدام طرق الوصول في طريقة التحديث. استدعاء طريقة الوصول مرة واحدة فقط في طريقة البدء وتخزين القيمة المرتجعة.

بشكل عام ، أوصي بعدم استدعاء أي أساليب الوصول إلى سلسلة أو أساليب الوصول إلى مجموعة في طريقة التحديث . في معظم الحالات ، يكفي الحصول على الرابط مرة واحدة في طريقة البدء .

فيما يلي مثالان شائعان لرمز طريقة وصول غير محسّن آخر:

 //Bad Code void Update() { //Allocates new array containing all touches Input.touches[0]; } //Better Code void Update() { Input.GetTouch(0); } //Bad Code void Update() { //Returns new string(garbage) and compare the two strings gameObject.Tag == "MyTag"; } //Better Code void Update() { gameObject.CompareTag("MyTag"); } 

القاعدة الخامسة: استخدام الوظائف التي لا تخصص الذاكرة


بالنسبة لبعض وظائف الوحدة ، يمكن العثور على بدائل غير ذاكرة. في حالتنا ، ترتبط كل هذه الوظائف بالفيزياء. يعتمد التعرف على التصادم على

 Physics2D. CircleCast(); 

لهذه الحالة بالذات ، يمكنك العثور على وظيفة غير ذاكرة تسمى

 Physics2D. CircleCastNonAlloc(); 

تحتوي العديد من الوظائف الأخرى أيضًا على بدائل مشابهة ، لذلك تحقق دائمًا من الوثائق المتعلقة بوظائف NonAlloc .

القاعدة السادسة: لا تستخدم LINQ


فقط لا تفعل ذلك. أعني أنك لست بحاجة إلى استخدامه في أي كود يتم تشغيله بشكل متكرر. أعلم أنه عند استخدام LINQ ، يكون الكود أسهل في القراءة ، ولكن في كثير من الحالات يكون الأداء وتخصيص الذاكرة لهذه الشفرة فظيعًا. بالطبع ، يمكن استخدامه في بعض الأحيان ، ولكن بصراحة ، لا نستخدم LINQ على الإطلاق في لعبتنا.

القاعدة السابعة: إنشاء مرة واحدة وإعادة استخدامها ، الجزء 2


هذه المرة نتحدث عن تجميع الأشياء. لن أخوض في تفاصيل التجميع ، لأنه قيل هذا عدة مرات ، على سبيل المثال ، أدرس هذا البرنامج التعليمي: https://learn.unity.com/tutorial/object-pooling

في حالتنا ، يتم استخدام البرنامج النصي لتجميع الكائنات التالي. لدينا مستوى تم إنشاؤه مليء بالعقبات الموجودة لفترة معينة من الوقت حتى يمر اللاعب على هذا الجزء من المستوى. يتم إنشاء مثيلات من هذه العوائق من المباني الجاهزة إذا تم استيفاء شروط معينة. الكود في طريقة التحديث. هذا الكود غير فعال تمامًا من حيث الذاكرة ووقت التشغيل. لقد حللنا المشكلة من خلال إنشاء مجموعة مكونة من 40 عقبة: إذا لزم الأمر ، نحصل على عوائق من التجمع ونعيد الكائن إلى التجمع عندما لا تكون هناك حاجة إليه.

القاعدة الثامنة: أكثر انتباهاً مع تحول التعبئة (Boxing)!


الملاكمة يولد القمامة! ولكن ما هي الملاكمة؟ في أغلب الأحيان ، تحدث الملاكمة عند تمرير نوع قيمة (int ، float ، bool ، وما إلى ذلك) إلى دالة تتوقع كائنًا من النوع Object.

فيما يلي مثال على الملاكمة التي نحتاج إلى إصلاحها في مشروعنا:

قمنا بتطبيق نظام المراسلة الخاص بنا في المشروع. يمكن أن تحتوي كل رسالة على كمية غير محدودة من البيانات. يتم تخزين البيانات في قاموس محدد على النحو التالي:

 Dictionary<string, object> data; 

لدينا أيضًا مضبط يحدد القيم في هذا القاموس:

 public Action SetAttribute(string attribute, object value) { data[attribute] = value; } 

الملاكمة هنا واضحة جدا. يمكنك استدعاء الوظيفة على النحو التالي:

 SetAttribute("my_int_value", 12); 

ثم تتعرض القيمة "12" للملاكمة وهذا يولد القمامة.

لقد قمنا بحل المشكلة عن طريق إنشاء حاويات بيانات منفصلة لكل نوع بدائي ، ويتم استخدام حاوية الكائن السابقة فقط لأنواع المرجع.

 Dictionary<string, object> data; Dictionary<string, bool> dataBool; Dictionary<string, int> dataInt; ....... 

لدينا أيضًا محددات منفصلة لكل نوع بيانات:

 SetBoolAttribute(string attribute, bool value) SetIntAttribute(string attribute, int value) 

ويتم تنفيذ كل هؤلاء المستوطنين بطريقة يطلقون عليها نفس الوظيفة المعممة:

 SetAttribute<T>(ref Dictionary<string, T> dict, string attribute, T value) 

تم حل مشكلة الملاكمة!

اقرأ المزيد حول هذا الموضوع في المقالة https://docs.microsoft.com/cs-cz/dotnet/csharp/programming-guide/types/boxing-and-unboxing .

القاعدة التاسعة: الدورات هي دائما موضع شك


هذه القاعدة تشبه إلى حد كبير الأول والثاني. مجرد محاولة لإزالة كافة التعليمات البرمجية الاختيارية من الحلقات لأسباب تتعلق بالأداء والذاكرة.

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

المادة 10: لا القمامة في المكتبات الخارجية


في حالة اتضح أن جزءًا من البيانات المهملة يتم إنشاؤه بواسطة رمز تم تنزيله من متجر الأصول ، فإن هذه المشكلة بها العديد من الحلول. ولكن قبل القيام بالهندسة العكسية وتصحيح الأخطاء ، ما عليك سوى الرجوع إلى متجر الأصول وتحديث المكتبة. في حالتنا ، لا يزال جميع الأصول المستخدمة مدعومة من قِبل المؤلفين الذين واصلوا إصدار تحديثات تحسين الأداء ، مما أدى إلى حل جميع مشكلاتنا. التبعيات يجب أن تكون ذات صلة! أفضل التخلص من المكتبة بدلاً من الاحتفاظ بها.

الجزء 2: تعظيم وقت التشغيل


تحدث بعض القواعد أعلاه اختلافًا طفيفًا إذا نادراً ما يتم استدعاء الكود. هناك حلقة واحدة كبيرة في الكود لدينا تعمل في كل إطار ، لذلك حتى هذه التغييرات الصغيرة كان لها تأثير كبير.

بعض هذه التغييرات ، إذا تم استخدامها بشكل غير صحيح أو في موقف خاطئ ، يمكن أن تؤدي إلى وقت تشغيل أسوأ. تحقق دائمًا من الملف التعريفي بعد إدخال كل تحسين في الكود للتأكد من أنك تتحرك في الاتجاه الصحيح .

بصراحة ، تؤدي بعض هذه القواعد إلى قراءة رمز أسوأ بكثير ، وأحيانًا تنتهك التوصيات ، على سبيل المثال ، تضمين التعليمات البرمجية المذكور في إحدى القواعد أدناه.

تتداخل العديد من هذه القواعد مع تلك المقدمة في الجزء الأول من المقالة. عادةً ما يكون أداء التعليمات البرمجية لتوليد البيانات المهملة أقل مقارنةً بالكود بدون إنشاء البيانات المهملة.

القاعدة الأولى: ترتيب التنفيذ الصحيح


نقل التعليمات البرمجية من أساليب FixedUpdate و Update و LateUpdate إلى أساليب Start and Awake . أعلم أن هذا يبدو مجنونًا ، لكن صدقوني ، إذا بحثت في الكود ، فستجد مئات أسطر الكود التي يمكن نقلها إلى الطرق التي يتم تنفيذها مرة واحدة فقط.

في حالتنا ، عادةً ما يرتبط هذا الرمز بـ

  • استدعاءات GetComponent <>
  • الحسابات التي ترجع فعليًا نفس النتيجة في كل إطار
  • مثيلات متعددة من نفس الكائنات ، وعادة ما تسرد
  • البحث عن GameObjects
  • الحصول على روابط لتحويل واستخدام طرق الوصول الأخرى

فيما يلي قائمة برمز العينة الذي انتقلنا إليه من أساليب التحديث إلى أساليب البدء:

 //There must be a good reason to keep GetComponent in Update gameObject.GetComponent<LineRenderer>(); gameObject.GetComponent<CircleCollider2D>(); //Examples of calculations returning same result every frame Mathf.FloorToInt(Screen.width / 2); var width = 2f * mainCamera.orthographicSize * mainCamera.aspect; var castRadius = circleCollider.radius * transform.lossyScale.x; var halfSize = GetComponent<SpriteRenderer>().bounds.size.x / 2f; //Finding objects var levelObstacles = FindObjectsOfType<Obstacle>(); var levelCollectibles = FindGameObjectsWithTag("COLLECTIBLE"); //References objectTransform = gameObject.transform; mainCamera = Camera.main; 

القاعدة الثانية: تنفيذ التعليمات البرمجية فقط عند الضرورة


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

 //Bad code Text text; GameState gameState; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); } void Update() { text.text = gameState.CollectedCollectibles.ToString(); } 

لأنه في كل مستوى لا يوجد سوى عدد قليل من العناصر لجمع ، وليس من المنطقي تغيير نص واجهة المستخدم في كل إطار. لذلك ، نقوم بتغيير النص فقط عندما يتغير الرقم.

 //Better code Text text; GameState gameState; int collectiblesCount; void Start() { gameState = StoreProvider.Get<GameState>(); text = GetComponent<Text>(); collectiblesCount = gameState.CollectedCollectibles; } void Update() { if(collectiblesCount != gameState.CollectedCollectibles) { //This code is ran only about 5 times each level collectiblesCount = gameState.CollectedCollectibles; text.text = collectiblesCount.ToString(); } } 

هذا الرمز أفضل بكثير ، خاصةً إذا كانت الإجراءات أكثر تعقيدًا من مجرد تغيير واجهة المستخدم.

إذا كنت تبحث عن حل أكثر شمولاً ، أوصي بتطبيق قالب المراقب باستخدام أحداث C # ( https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/ ).

على أي حال ، لم يكن ذلك كافياً بالنسبة لنا ، وأردنا تنفيذ حل معمم تمامًا ، لذلك أنشأنا مكتبة تنفذ Flux in Unity. أدى ذلك إلى حل بسيط للغاية ، يتم فيه تخزين حالة اللعبة بأكملها في كائن "المتجر" ، ويتم إخطار جميع عناصر واجهة المستخدم والمكونات الأخرى عندما تتغير الحالة وتتفاعل مع هذا التغيير دون رمز في طريقة التحديث.

القاعدة الثالثة: الدورات هي دائما موضع شك


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

القاعدة الرابعة: للحصول على أفضل من Foreach


حلقة Foreach سهلة للغاية في الكتابة ، ولكن "من الصعب جدًا" تنفيذها. داخل حلقة Foreach ، يتم استخدام العداد لمعالجة مجموعة البيانات بشكل متكرر وإرجاع القيمة. هذا الأمر أكثر تعقيدًا من التكرار على المؤشرات في حلقة بسيطة.

لذلك ، في مشروعنا ، استبدلنا حلقات Foreach كلما كان ذلك ممكنًا بـ For:

 //Bad code foreach (GameObject obstacle in obstacles) //Better code var count = obstacles.Count; for (int i = 0; i < count; i++) { obstacles[i]; } 

في حالتنا مع حلقة كبيرة ، هذا التغيير مهم للغاية. بسيطة للحلقة تسريع رمز مرتين .

القاعدة الخامسة: المصفوفات أفضل من القوائم


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

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

القاعدة السادسة: عمليات التعويم أفضل من عمليات المتجهات


بالكاد يكون هذا الاختلاف ملحوظًا إذا لم تقم بإجراء الآلاف من هذه العمليات ، كما كان الحال في حالتنا ، لذلك تبين لنا أن الزيادة في الإنتاجية كانت كبيرة.

لقد أجرينا تغييرات مماثلة:

 Vector3 pos1 = new Vector3(1,2,3); Vector3 pos2 = new Vector3(4,5,6); //Bad code var pos3 = pos1 + pos2; //Better code var pos3 = new Vector3(pos1.x + pos2.x, pos1.y + pos2.y, ......); Vector3 pos1 = new Vector3(1,2,3); //Bad code var pos2 = pos1 * 2f; //Better code var pos2 = new Vector3(pos1.x * 2f, pos1.y * 2f, ......); 

القاعدة السابعة: ابحث عن الأشياء بشكل صحيح


فكر دائمًا فيما إذا كنت حقًا بحاجة إلى استخدام الأسلوب GameObject.Find (). هذه الطريقة ثقيلة وتستغرق وقتًا مجنونًا. يجب ألا تستخدم هذه الطريقة أبدًا في طرق التحديث. لقد وجدنا أن معظم مكالمات Find يمكن استبدالها بروابط مباشرة في المحرر ، والتي ، بالطبع ، أفضل بكثير.

 //Bad Code GameObject player; void Start() { player = GameObject.Find("PLAYER"); } //Better Code //Assign the reference to the player object in editor [SerializeField] GameObject player; void Start() { } 

إذا كان هذا مستحيلًا ، ففكر على الأقل في استخدام العلامات (Tag) والبحث عن كائن عن طريق التسمية باستخدام GameObject.FindWithTag .

لذلك ، في الحالة العامة: رابط مباشر> GameObject.FindWithTag ()> GameObject.Find ()

القاعدة الثامنة: العمل فقط مع الكائنات ذات الصلة


في حالتنا ، كان هذا مهمًا للتعرف على التصادمات باستخدام RayCast-s (CircleCast ، إلخ). بدلاً من التعرف على التصادمات وتحديد أي منها مهم في التعليمات البرمجية ، قمنا بنقل كائنات اللعبة إلى الطبقات المناسبة حتى نتمكن من حساب التعارضات فقط للكائنات الضرورية.

هنا مثال

 //Bad Code void DetectCollision() { var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance); for (int i = 0; i < count; i++) { var obj = results[i].collider.transform.gameObject; if(obj.CompareTag("FOO")) { ProcessCollision(results[i]); } } } //Better Code //We added all objects with tag FOO into the same layer void DetectCollision() { //8 is number of the desired layer var mask = 1 << 8; var count = Physics2D.CircleCastNonAlloc( position, radius, direction, results, distance, mask); for (int i = 0; i < count; i++) { ProcessCollision(results[i]); } } 

القاعدة التاسعة: استخدام التسميات بشكل صحيح


ليس هناك شك في أن التسميات مفيدة للغاية ويمكن أن تحسن أداء التعليمات البرمجية ، ولكن تذكر أن هناك طريقة واحدة صحيحة فقط لمقارنة تسميات الكائنات !

 //Bad Code gameObject.Tag == "MyTag"; //Better Code gameObject.CompareTag("MyTag"); 

القاعدة العاشرة: احذر من الحيل مع الكاميرا!


من السهل جدًا استخدام Camera.main ، لكن أداء هذا الإجراء ضعيف جدًا. والسبب هو أنه وراء الكواليس من كل مكالمة إلى Camera.main ، ينفّذ محرك Unity فعليًا نتائج FindGameObjectsWithTag () ، لذلك نحن ندرك بالفعل أنك لست بحاجة إلى الاتصال بها كثيرًا ، ومن الأفضل حل هذه المشكلة عن طريق تخزين الرابط مؤقتًا في طريقة البدء أو مستيقظا.

 //Bad code void Update() { Camera.main.orthographicSize //Some operation with camera } //Better Code private Camera cam; void Start() { cam = Camera.main; } void Update() { cam.orthographicSize //Some operation with camera } 

القاعدة الحادية عشرة: LocalPosition هو أفضل من الموضع


كلما كان ذلك ممكنًا ، استخدم Transform.LocalPosition للأزواج والأدوات بدلاً من Transform.Position . ضمن كل مكالمة من Transform.Position ، يتم تنفيذ عمليات أكثر من ذلك بكثير ، على سبيل المثال ، حساب الموقف العالمي في حالة استدعاء getter أو حساب الموقف المحلي من الوضع العالمي في حالة استدعاء setter. في مشروعنا ، اتضح أنه يمكنك استخدام LocalPositions في 99٪ من الحالات باستخدام Transform.Position ، ولا تحتاج إلى إجراء أي تغييرات أخرى في التعليمات البرمجية.

القاعدة الثانية عشرة: لا تستخدم LINQ


وقد نوقش هذا بالفعل في الجزء الأول. فقط لا تستخدمه ، هذا كل شيء.

القاعدة الثالثة عشرة: لا تخف (أحيانًا) لكسر القواعد


في بعض الأحيان ، حتى استدعاء وظيفة بسيطة يمكن أن يكون مكلفاً للغاية. في هذه الحالة ، يجب أن تفكر دائمًا في تضمين شفرة (رمز مضمّن). ماذا يعني هذا؟ في الواقع ، نحن فقط نأخذ الكود من الوظيفة وننسخه مباشرة إلى المكان الذي نريد أن نستخدم فيه الوظيفة لتجنب استدعاء طرق إضافية.

في معظم الحالات ، لن يكون لهذا أي تأثير ، لأن دمج الكود يتم تنفيذه تلقائيًا في مرحلة التحويل البرمجي ، ولكن هناك قواعد معينة تقرر من خلالها المترجم ما إذا كان سيتم تضمين الكود (على سبيل المثال ، الأساليب الافتراضية غير مضمنة أبدًا ؛ لمزيد من التفاصيل ، راجع https: //docs.unity3d.com/Manual/BestPracticeUnderstandingPerformanceInUnity8.html ). لذلك ، افتح ملف التعريف فقط ، وابدأ تشغيل اللعبة على الجهاز المستهدف وشاهد ما إذا كان يمكن تحسين شيء ما.

في حالتنا ، كانت هناك العديد من الوظائف التي قررنا دمجها لتحسين الأداء ، خاصةً في الحلبة الكبيرة.

استنتاج


بتطبيق القواعد الواردة في المقال ، حققنا بسهولة 60 إطارًا في الثانية في لعبة iOS ، حتى على iPhone 5S. ربما تكون بعض القواعد خاصة بمشروعنا فقط ، لكنني أعتقد أنه ينبغي تذكر معظمها عند كتابة الكود أو التحقق منه لتجنب المشاكل في المستقبل. من الأفضل دائمًا كتابة التعليمات البرمجية استنادًا إلى الأداء في وقت لاحق لإعادة تشكيل أجزاء كبيرة من التعليمات البرمجية.

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


All Articles