كيف وصلنا إلى الارتباط التفاعلي في Unity3D

صورة



سأتحدث اليوم عن كيفية وصول بعض المشاريع في Pixonic إلى ما كان لفترة طويلة هو المعيار للواجهة الأمامية العالمية بأكملها - الارتباط التفاعلي.



الغالبية العظمى من مشاريعنا مكتوبة في Unity 3D. وإذا كانت تقنيات العميل الأخرى ذات التفاعل التفاعلي تعمل بشكل جيد (MVVM ، Qt ، الملايين من أطر عمل JS) ، وتم اعتبارها أمرًا مفروغًا منه ، فإن Unity لا تحتوي على أي ارتباطات مضمنة أو مقبولة بشكل عام.



ربما كان لدى شخص ما في هذا الوقت سؤال: "لماذا؟ نحن لا نستخدم ذلك ونعيش بشكل جيد ".



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



أولاً ، حول المشروع ، الذي تتطلب مشاكله مثل هذا الحل. بالطبع ، نحن نتحدث عن War Robots - مشروع ضخم به العديد من فرق التطوير والدعم والتسويق وما إلى ذلك. نحن مهتمون الآن باثنين منهم فقط: فريق المبرمجين العميل وفريق واجهة المستخدم. فيما يلي ، من أجل التبسيط ، سوف نسميها "رمز" و "تخطيط". لقد حدث أن بعض الأشخاص يشاركون في تصميم وتخطيط واجهة المستخدم ، بينما يقوم البعض الآخر "بتنشيط" كل هذا. هذا منطقي ، وفي تجربتي صادفت العديد من الأمثلة المماثلة لتنظيم الفريق.



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



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



public class PlayerProfileWidget : WidgetBehaviour
{
  [SerializeField] private Text nickname;
  [SerializeField] private Image avatar;
  [SerializeField] private Text level;
  [SerializeField] private GameObject hasUpgradeMark;
  [SerializeField] private Button upgradeButton;

  public void Initialize(ProfileService profileService)
  {
 	RefreshData(profileService.Player);

 	upgradeButton.onClick
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	nickname.text = player.Id;
 	avatar.overrideSprite = Resources.Load<Sprite>($"Avatars/{player.Avatar}_Small");
 	level.text = player.Level.ToString();
 	hasUpgradeMark.SetActive(player.HasUpgrade);
  }
}


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



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



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



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



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



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



في الوقت الذي فكرنا فيه للتو في كل هذا ، كان هناك العديد من حلول الجهات الخارجية. كنا نتطلع نحو Unity Weld و Peppermint Data Binding و DisplayFab. كان لديهم جميعًا إيجابيات وسلبيات. لكن أحد العوائق القاتلة بالنسبة لنا كان شائعًا - الأداء السيئ لأغراضنا. قد تعمل بشكل جيد على واجهات بسيطة ، ولكن بحلول ذلك الوقت لم نتمكن من تجنب تعقيد الواجهات.



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



كانت المهام على النحو التالي:



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




من الأعلى إلى الأسفل ، أو الوصف العام



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



للتبسيط ، قمنا بتقييد مجموعة أنواع الخصائص ذات القيم إلى bool و int و float و string بقدر الإمكان. تم إملاء ذلك من خلال عدة اعتبارات في وقت واحد:



  • تسلسل هذه الأنواع في Unity سهل ؛
  • , -, . , Sprite -, PlayerModel , ;
  • , .


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



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



يبدو نموذج ViewModel هكذا في المحرر:







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



إذا كان ViewModel هو الجزء العلوي من نظام الربط الخاص بنا ، فإن الجزء السفلي هو ما يسمى بالقوائم. هؤلاء هم المشتركون النهائيون لخصائص ViewModel الذين يقومون بكل الأعمال:



  • تمكين / تعطيل GameObject أو المكونات الفردية عن طريق تغيير قيمة الخاصية المنطقية ؛
  • تغيير النص في الحقل اعتمادًا على قيمة خاصية السلسلة ؛
  • بدأ الرسام ، وتغيرت معالمه ؛
  • استبدل الكائن المطلوب من المجموعة بالفهرس أو مفتاح السلسلة.


سأتوقف عند هذا ، لأن عدد التطبيقات محدود فقط بالخيال ونطاق المهام التي تحلها.



هذه هي الطريقة التي ينظر بها بعض أدوات التطبيق في المحرر:









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



  • Boolean - على سبيل المثال ، عندما تحتاج إلى عكس خاصية منطقية أو إرجاع صواب أو خطأ اعتمادًا على قيمة من نوع آخر (أريد حدًا ذهبيًا عندما يكون المستوى أعلى من 15).
  • الحساب . لا تعليق هنا.
  • العمليات على المجموعات : عكس ، خذ جزءًا فقط من مجموعة ، وفرز حسب المفتاح ، وأكثر من ذلك بكثير.


مرة أخرى ، يمكن أن يكون هناك مجموعة كبيرة ومتنوعة من خيارات المحولات المختلفة ، لذلك لن أكمل.











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



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



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



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



public class PlayerProfilePresenter : Presenter
{
  private readonly IMutableProperty<string> _playerId;
  private readonly IMutableProperty<string> _playerAvatar;
  private readonly IMutableProperty<int> _playerLevel;
  private readonly IMutableProperty<bool> _playerHasUpgrade;

  public PlayerProfilePresenter(ProfileService profileService, IViewModel viewModel)
  {
 	_playerId = viewModel.GetString("player/id");
 	_playerAvatar = viewModel.GetString("player/avatar");
 	_playerLevel = viewModel.GetInteger("player/level");
 	_playerHasUpgrade = viewModel.GetBoolean("player/has-upgrade");

 	RefreshData(profileService.Player);

 	viewModel.GetEvent("player/upgrade")
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	_playerId.Value = player.Id;
 	_playerAvatar.Value = player.Avatar;
 	_playerLevel.Value = player.Level;
 	_playerHasUpgrade.Value = player.HasUpgrade;
  }
}


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



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



يعرض الجدول نتائج اختبار معياري يقوم بإنشاء 500 نموذج باستخدام InputField و Text و Button ، مقترنة بنموذج خاصية واحد ووظيفة إجراء واحدة.







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



All Articles