مدينة كبيرة للأجهزة المحمولة على الوحدة. خبرة في التطوير والتحسين



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

اللعبة


لعبة تحت عنوان المافيا. في اللعبة ، حاولت إعادة إنشاء أمريكا 30-40. في الأساس ، تعتبر اللعبة استراتيجية اقتصادية للشخص الأول. يلتقط اللاعب العمل ويحاول الحفاظ عليه واقفا على قدميه.
المنفذ: حركة مرور السيارات (إشارات المرور ، تجنب الاصطدام) ، حركة المرور البشرية ، البار ، الكازينو ، النادي ، شقة اللاعب ، شراء البدلة ، تغيير البدلة ، الشراء / الطلاء / التزود بالوقود ، رجال الشرطة ، الأمن / رجال العصابات ، الاقتصاد ، موارد بيع / شراء.

العمارة


الصورة

يؤسفني أنني لم أستخدم ECS ، لكنني حاولت ركوب الدراجة. في النهاية ، تحول كل شيء إلى أن يكون مرهقًا ويعتمد عليه كثيرًا. يحتوي التطبيق على نقطة دخول واحدة - كائن اللعبة للتطبيق (go) ، حيث يتم تعليق فئة التطبيق الذي يحمل نفس الاسم. وهو مسؤول عن التحميل المسبق لقاعدة البيانات وتجمعات السكان والإعدادات الأولية. بالإضافة إلى ذلك ، تقع العديد من فئات مكونات مدير المفرد على أكتاف التطبيق (go).

  • Audiomanager
  • UIManager
  • مدخلات

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

مبسطة AudioManager. يسمح لك بإضافة العديد من مكونات الصوت إلى كائن اللعبة ، وإذا لزم الأمر ، تشغيل الصوت:

public class AudioManager : MonoBehaviour { public static AudioManager instance = null; //  public AudioClip metalHitAC; //   private AudioSource metalHitAS; //    public bool isMetalHit = false; private void Awake() { if (instance == null) instance = this; else if (instance == this) Destroy(gameObject); } void Start() { metalHitAS = AddAudio(metalHitAC, false, false, 0.3f, 1); } void LateUpdate() { if (isMetalHit) { metalHitAS.Play(); isMetalHit = false; } } AudioSource AddAudio(AudioClip clip, bool loop, bool playAwake, float vol, float pitch) { var newAudio = gameObject.AddComponent<AudioSource>(); newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = 10; return newAudio; } public AudioSource AddAudioToGameObject(AudioClip clip, bool loop, bool playAwake, float vol, float pitch, float minDistance, float maxDistance, GameObject go) { var newAudio = go.AddComponent<AudioSource>(); newAudio.spatialBlend = 1; newAudio.clip = clip; newAudio.loop = loop; newAudio.playOnAwake = playAwake; newAudio.volume = vol; newAudio.pitch = pitch; newAudio.minDistance = minDistance; newAudio.maxDistance = maxDistance; return newAudio; } } 

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

 AudioManager.instance.isMetalHit = true; 

في هذا المثال ، سيكون من الأكثر حكمة إعادة تشغيل المصور في الأسلوب.

ما يشبه InputManager المبسطة:

 public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked; //public delegate void ClickActionFloatArg(float arg); //public static event ClickActionFloatArg OnRSliderValueChange, OnGSliderValueChange, OnBSliderValueChange; public void AimKeyDown() { OnAimKeyClicked(); } } 

أضع طريقة AimKeyDown على الزر ، وقم بتوقيع البرنامج النصي للتحكم في الأسلحة على OnAimKeyClicked:

 InputManager.instance.OnAimKeyClicked += GunShot; 

يتم تطبيق نظام الإدخال بالكامل بطريقة مماثلة. لم ألاحظ أي مشاكل بالسرعة. هذا سمح لنا بجمع كل معالجات النقرات في مكان واحد - InputManager.

التحسين


دعنا ننتقل إلى الأكثر إثارة للاهتمام. بالنسبة للمبتدئين ، موضوع التحسين في Unity مؤلم ومحفوف بالعديد من المزالق. سوف أشارك ما كنت أتعامل معه.

1. التخزين المؤقت للمكونات (ابدأ بأساسيات بسيطة)

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

2. باستخدام SendMessage

استخدام SendMessage () أبطأ من GetComponent (). انتقل SendMessage من خلال كل برنامج نصي للعثور على الأسلوب بالاسم المطلوب باستخدام مقارنة السلسلة. يبحث GetComponent البرنامج النصي من خلال مقارنة النوع ويستدعي الأسلوب مباشرة.

3. مقارنة علامات الكائن

استخدم الأسلوب CompareTag بدلاً من obj.tag == "السلسلة". في الوحدة ، يؤدي استخراج السلاسل من كائنات اللعبة إلى إنشاء سلسلة مكررة ، مما يضيف العمل إلى أداة تجميع مجمعي البيانات المهملة. من الأفضل تجنب الحصول على اسم كائن اللعبة. لا يمكنك استدعاء CompareTag في التحديث وكذلك قراءة العمليات الثقيلة.

4. المواد

المواد أقل كان ذلك أفضل. تقليل كمية المواد ممكن. لتحقيق ذلك ، ساعد نسيج الساتان. على سبيل المثال ، تتكون المدينة كلها تقريبًا في لعبتي من 2-3 أطلس. تجدر الإشارة إلى أنه ليس كل الأجهزة المحمولة قادرة على العمل مع الأطالس الكبيرة. لذلك ، إذا كنت ترغب في دعم الأجهزة التي تتراوح أعمارها بين 11 و 13 عامًا ، فمن الجدير بالذكر. قررت رفض الدعم لنظام Android الذي يقل عن 5.1 ، نظرًا لأن هذه الأجهزة قديمة. علاوة على ذلك ، تعمل اللعبة على OpenGL 3.x بسبب Linear Rendering.

5. الفيزياء

من السهل خفض FPS إلى 10. اتضح أنه حتى الكائنات الثابتة تتفاعل وتشارك في العمليات الحسابية. اعتقدت خطأ أن الكائنات المادية الثابتة (الكائنات التي تحتوي على مكون RigidBody) هي سلبية تماما عند الطلب. لقد ضُربت من خلال البرنامج التعليمي القديم الذي قال إنه حيثما كان هناك تصادم ، يجب أن يكون هناك RigidBody. الآن كل ما عندي من الأشياء الثابتة هي ساكنة + BoxCollider. عندما أحتاج إلى الفيزياء ، على سبيل المثال ، أعمدة الإنارة التي يمكن القضاء عليها ، أعتقد أن خفض مكون RigidBody إذا لزم الأمر.

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

6. انسداد اعدام + اللد

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

الصورة

الصورة

7. كائن تجمع

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

تجمع بسيط لإدارة السير القتالية:

  public List<Enemy> enemyPool = new List<Enemy>(); private void Start() { //    Enemy Transform enemyGameObjectContainer = Application.instance.objectPool.Find("Enemy"); //  enemyPool  for (int i = 0; i < enemyGameObjectContainer.childCount; i++) { enemyPool.Add(new Enemy() { Id = i, ParentRoomId = 0, GameObj = enemyGameObjectContainer.GetChild(i).gameObject }); } } public void SpawnEnemyForRoom(int roomId, int amount, Transform spawnPosition, bool combatMode) { //Stopwatch sw = new Stopwatch(); //sw.Start(); foreach (Enemy enemy in enemyPool) { if (amount > 0) { if (enemy.ParentRoomId == 0 && enemy.GameObj.activeSelf == false) { // id   enemy.ParentRoomId = roomId; enemy.GameObj.transform.position = spawnPosition.position; enemy.GameObj.transform.rotation = spawnPosition.rotation; enemy.AICombat = enemy.GameObj.GetComponent<AICombat>(); enemy.AICombat.parentRoomId = roomId; // id  enemy.AICombat.id = enemy.Id; //   enemy.GameObj.SetActive(true); //      if (combatMode) enemy.AICombat.ActivateCombatMode(); amount--; } } if (amount == 0) break; } } 

قاعدة البيانات


يمكنني استخدام sqlite كقاعدة بيانات - مريح وبسرعة. يتم تقديم البيانات في شكل جدول ، يمكنك تقديم استفسارات معقدة. في الفصل للعمل مع قاعدة البيانات ، 800 خطوط متى. لا أستطيع أن أتخيل كيف سيبدو في XML / JSON.

مشاكل وخطط للمستقبل


للانتقال من المدينة إلى "الغرف" اخترت تنفيذ "النقل الفضائي". يقترب اللاعب من الباب ، ويتم تحميل غرفة المشهد ويتم نقل المشغل عن بُعد. هذا يوفر عليك الاضطرار إلى الاحتفاظ بغرف في المدينة. إذا قمت بتنفيذ غرف في المدينة ، + 15 غرفة مع ملء ، فسوف يرتفع استهلاك الذاكرة إلى 1 غيغابايت على الأقل. أنا لا أحب هذا التطبيق ، فهو غير واقعي ويفرض مجموعة من القيود. أظهرت الوحدة مؤخرًا عرضًا تجريبيًا على Megacity ، وهو أمر مثير للإعجاب. أريد نقل اللعبة تدريجيًا للتغلب على التكنولوجيا واستخدامها من Megacity لتحميل المباني والمباني. إنها تجربة رائعة ومثيرة للاهتمام ، وأعتقد أنها ستصبح مدينة نابضة بالحياة حقًا. لماذا لم أستخدم مشهد التحميل غير المتزامن ؟ الأمر بسيط ، لا يعمل ، لا يوجد مشهد تحميل غير متزامن خارج الصندوق في إصدار 2018.3. في البداية ، كنت آمل أن يكون مشهد التحميل غير المتزامن عند التخطيط لمدينة ، ولكن كما اتضح ، على المشاهد الكبيرة ، يجمد اللعبة مثل مشهد تحميل منتظم. تم تأكيد ذلك في منتدى الوحدة ، يمكنك التجول ، ولكن هناك حاجة إلى عكازات.

بعض الإحصاءات:

القوام: 304 / 374.3 ميغابايت
تنسجم: 295 / 304.0 ميغابايت
المواد: 101 / 148.0 كيلو بايت (التناقض المحتمل هنا)
مقاطع الرسوم المتحركة: 24 / 2.8 ميغابايت
AudioClips: 22 / 30.3 ميغابايت
الأصول: 21761
مشاريع الألعاب في المشهد: 29450
مجموع الكائنات في المشهد: 111645
إجمالي عدد الكائنات: 133406
تخصيصات GC لكل إطار: 70 / 2.0 كيلوبايت
ما مجموعه 4800 سطر من رمز C #.

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

يمكنك اختبار النسخة التجريبية المفتوحة والشعور بها هنا: play.google.com/store/apps/details؟id=com.ag.mafiaProject01 (إذا لم يعمل التجميع ، فستحتاج إلى تعبده بعض الشيء ، وتصل التحديثات كل ليلة). آمل أن لا يعتبر هذا رابطًا إعلانيًا ، لأن هذا الإصدار التجريبي والتنزيلات لن يجلب لي التصنيف والأرباح. بالإضافة إلى ذلك ، لا أعتقد أن habr هو الجمهور المستهدف في لعبتي.

لقطات:



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


All Articles