
في المقالات السابقة ، وصفنا كيف ينبغي ترتيب نموذج مناسب وبإمكانات واسعة ، ما هو نوع نظام القيادة الذي سيكون مناسبًا له ، والذي يعمل كجهاز تحكم ، لقد حان الوقت للحديث عن الحرف الثالث من اختصار MVC البديل الخاص بنا.
في الواقع ، يحتوي Assetstore على مكتبة UniRX متطورة للغاية جاهزة والتي تنفذ التفاعل والتحكم في انعكاس الوحدة. لكننا سنتحدث عن ذلك في نهاية المقالة ، لأن هذه الأداة القوية الضخمة والمتوافقة مع RX لقضيتنا لا لزوم لها. إن القيام بكل ما نحتاج إليه ممكن تمامًا دون سحب RX ، وإذا كنت تملكه ، فيمكنك أن تفعل الشيء نفسه بسهولة.
الحلول المعمارية للعبة المحمول. الجزء 1: نموذجالحلول المعمارية للعبة المحمول. الجزء 2: القيادة وقوائم الانتظارعندما يبدأ شخص ما في كتابة اللعبة الأولى ، يبدو من المنطقي له أن توجد وظيفة من شأنها رسم الشكل بالكامل ، أو جزء منه ، وسحبها في كل مرة يتغير فيها شيء مهم. مع مرور الوقت ، يزداد حجم الواجهة ، ويصبح شكل القوالب وأجزاءها مائة ، ثم مائتي ، وعندما تتغير المحفظة حالتها ، يجب إعادة رسم ربعها. ثم يأتي المدير ويقول "كما هو الحال في تلك اللعبة" ، يجب عليك إنشاء نقطة حمراء صغيرة على الزر إذا كان هناك قسم داخل الزر يوجد فيه قسم فرعي يوجد فيه الزر ، والآن لديك موارد كافية للقيام بشيء ما بالنقر فوقه هذا مهم. وهذا كل شيء ، أبحر ...
الخروج عن مفهوم الرسم يحدث على عدة مراحل. أولاً ، يتم حل مشكلة الحقول الفردية. لديك ، على سبيل المثال ، حقل في النموذج وحقل نص يجب عرض جميع محتوياته فيه. حسنًا ، نبدأ في تشغيل كائن يشترك في تحديثات هذا الحقل ، ومع كل تحديث يضيف النتائج إلى حقل نص. في الكود ، شيء مثل هذا:
var observable = new ChildControl(FCPlayerModel.ASSIGNED, Player); observable.onChange(i => Assigned.text = i.ToString())
الآن لا نحتاج إلى اتباع إعادة الرسم ، فقط قم بإنشاء هذا التصميم ، وبعد ذلك كل ما سيحدث في النموذج سوف يقع في الواجهة. جيد ، ولكنه مرهق ، يحتوي على الكثير من الإيماءات غير الضرورية التي سيتعين على مبرمج أن يكتبها بيديه 100500 مرة ، وأحيانًا يرتكب أخطاء. دعنا نلف هذه الإعلانات في وظيفة امتداد ستخفي الحروف الإضافية أسفل الغطاء.
Player.Get(c, FCPlayerModel.ASSIGNED).Action(c, i => Assigned.text = i.ToString());
أفضل بكثير ، ولكن هذا ليس كل شيء. يعد تحويل حقل النموذج إلى حقل النص عملية متكررة ونموذجية سنعمل على إنشاء وظيفة التفاف منفصلة لها. الآن اتضح لفترة وجيزة وبصورة جيدة ، كما يبدو لي.
Player.Get(c, FCPlayerModel.ASSIGNED).SetText(c, Assigned);
لقد أوضحت هنا الفكرة الرئيسية التي سأسترشد بها عند إنشاء واجهات لبقية حياتي: "إذا كان على المبرمج القيام بشيء على الأقل مرتين ، فقم بلفه في وظيفة مريحة وقصيرة خاصة."
جمع القمامة
يتمثل أحد الآثار الجانبية لهندسة الواجهة التفاعلية في إنشاء مجموعة من الكائنات المشتركة في شيء ما وبالتالي لن تترك الذاكرة دون ركلة خاصة. بنفسي ، بالعودة إلى العصور القديمة ، توصلت إلى طريقة ليست جميلة ، ولكنها بسيطة وبأسعار معقولة. عند إنشاء أي نموذج ، يتم إنشاء قائمة بجميع وحدات التحكم التي يتم إنشاؤها في اتصال مع هذا النموذج ؛ للإيجاز ، يطلق عليه ببساطة "ج". تقبل جميع وظائف المجمع الخاصة هذه القائمة باعتبارها المعلمة الأولى المطلوبة ، وعندما DisconnectModel النموذج ، فإنه يمر قائمة جميع الضوابط ويزيلها بلا رحمة مع رمز في الجد المشترك. لا الجمال والنعمة ، ولكن رخيصة وموثوقة وعملية نسبيا. يمكنك الحصول على قدر أكبر من الأمان قليلاً إذا كنت ، بدلاً من ورقة التحكم ، تحتاج إلى إدخال IView وإعطاء هذا لجميع هذه الأماكن. نفس الشيء في الأساس ، لن ينسى نسيان ملء نفس الشيء ، ولكن من الصعب اختراقه. أخشى أن أنسى ، لكنني لست خائفًا جدًا من أن يقوم شخص ما بخرق النظام عن عمد ، لأن مثل هؤلاء الأشخاص الأذكياء يحتاجون إلى خوضهم بحزام وغيره من الأساليب غير البرمجية ، لذلك أنا فقط أقتصر على ج.
يمكن استخلاص نهج بديل من UniRX. ينشئ كل غلاف كائنًا جديدًا له رابط إلى العنصر السابق الذي يستمع إليه. وفي النهاية ، يتم استدعاء الأسلوب AddTo (مكون) ، والذي يعزو سلسلة عناصر التحكم بأكملها إلى كائن قابل للتدمير. في مثالنا ، سيبدو هذا الرمز كما يلي:
Player.Get(FCPlayerModel.ASSIGNED).SetText(Assigned).AddTo(this);
إذا قرر آخر مالك للسلسلة أن يتم تدميره ، فسيرسل إلى جميع عناصر التحكم المسندة إليه الأمر "يقتل نفسك بشأن التخلص إذا لم يستمع إليك أحد ما عدا أنا". ويتم تنظيف السلسلة بأكملها بطاعة. بالطبع ، هذا أكثر إيجازًا ، ولكن من وجهة نظري ، هناك عيب واحد مهم. يمكن نسيان AddTo عن طريق الخطأ ولن يعرفه أحد أبدًا حتى فوات الأوان.
في الواقع ، يمكنك استخدام اختراق Unity القذرة والاستغناء عن أي كود إضافي في طريقة العرض:
public static T AddTo<T>(this T disposable, Component component) where T : IDisposable { var composite = new CompositeDisposable(disposable); Observable .EveryUpdate() .Where(_ => component == null) .Subscribe(_ => composite.Dispose()) .AddTo(composite); return disposable; }
كما تعلمون ، فإن الارتباط بمكون Unicomponent أو GameObject في الوحدة هو باطل. ولكن عليك أن تفهم أن hakokostyl هذا ينشئ مستمع تحديث لكل سلسلة من عناصر التحكم التي يتم تدميرها ، وهذا بالفعل بأدب قليل.
واجهة نموذج مستقلة
مثالينا ، الذي يمكننا تحقيقه بسهولة ، هو الموقف الذي يمكننا فيه تحميل GameState الكامل في أي وقت ، سواء كان نموذج التحقق من الخادم ونموذج البيانات لواجهة المستخدم ، وسيكون التطبيق في نفس الحالة تمامًا ، وصولًا إلى حالة جميع الأزرار. هناك سببان لذلك. الأول هو أن بعض المبرمجين يحبون التخزين داخل وحدة تحكم النموذج ، أو حتى في طريقة العرض نفسها ، مع الإشارة إلى أن دورة حياتهم هي بالضبط نفس دورة النموذج نفسه. والثاني هو أنه حتى لو كانت جميع بيانات النموذج في نموذجه ، فإن أمر إنشاء وملء النموذج نفسه يأخذ شكل استدعاء دالة صريح ، مع بعض المعلمات الإضافية ، على سبيل المثال ، أي حقل من القائمة يجب أن يركز عليه.
ليس عليك التعامل مع هذا إذا كنت لا تريد حقًا راحة التصحيح. لكننا لسنا هكذا ، نريد تصحيح الواجهة بسهولة مثل العمليات الأساسية مع النموذج. للقيام بذلك ، والتركيز التالي. في جزء واجهة المستخدم من النموذج ، يتم إعداد متغير ، على سبيل المثال. رئيسي ، وفيه ، كجزء من الأمر ، يمكنك وضع نموذج النموذج الذي تريد رؤيته. تتم مراقبة حالة هذا المتغير بواسطة وحدة تحكم خاصة ، إذا ظهر نموذج في هذا المتغير ، وفقًا لنوعه ، فإنه يستنسخ النموذج المطلوب ، ويضعه عند الضرورة ، ويرسله مكالمة إلى ConnectModel (الطراز). إذا تم تحرير المتغير من النموذج ، فسوف تقوم وحدة التحكم بإزالة النموذج من اللوحة القماشية واستخدامه. وبالتالي ، لا توجد إجراءات لتجاوز النموذج ، وكل ما قمت به مع الواجهة مرئي بوضوح في نموذج ExportChanges. ثم نسترشد بمبدأ "كل ما تم القيام به مرتين التفاف" واستخدام وحدة تحكم نفسه بالضبط في جميع مستويات الواجهة. إذا كان للقالب مكان لقالب آخر ، فسيتم إنشاء نموذج واجهة مستخدم له ، ويتم إنشاء متغير في نموذج القالب الأصل. بالضبط نفس الشيء مع القوائم.
يتمثل أحد الآثار الجانبية لهذا النهج في إضافة ملفين إلى أي نموذج ، أحدهما يحتوي على نموذج بيانات لهذا النموذج ، والآخر ، عادةً monobah يحتوي على ارتباطات لعناصر واجهة المستخدم ، والتي ، بعد تلقي النموذج في وظيفة ConnectModel ، ستنشئ جميع وحدات التحكم التفاعلية لجميع حقول النموذج وجميع عناصر واجهة المستخدم. حسنًا ، إنها أكثر إحكاما ، بحيث تكون مريحة أيضًا للعمل معها ، وربما يكون ذلك مستحيلًا. إذا كان ذلك ممكنا ، اكتب التعليقات.
قائمة الضوابط
الموقف المعتاد هو عندما يكون للنموذج قائمة ببعض العناصر. نظرًا لأنني أرغب في أن يتم كل شيء بشكل مريح للغاية ، ويفضل أن يكون ذلك في سطر واحد ، فقد أردت أيضًا أن أفعل شيئًا لقوائم ستكون ملائمة للتعامل معها. سطر واحد ممكن ، لكن اتضح أنه طويل بشكل غير مريح. من الناحية التجريبية ، اتضح أن التنوع الكامل للحالات يغطيه نوعان فقط من عناصر التحكم. الأول يراقب حالة المجموعة ويستدعي ثلاث وظائف lambda ، الأول يسمى عند إضافة بعض العناصر إلى المجموعة ، والثاني عندما يغادر العنصر المجموعة ، وأخيرا يتم استدعاء العنصر الثالث عندما تغير عناصر المجموعة الترتيب. النوع الثاني الأكثر شيوعًا من عناصر التحكم يراقب القائمة ، وهو مصدر الاشتراك منه - الصفحات ذات الرقم المحدد. هذا ، على سبيل المثال ، يتبع قائمة بطول 102 عنصرًا ، وتقوم هي نفسها بإرجاع قائمة من 10 عناصر ، من العشرين إلى 29. والأحداث التي تم إنشاؤها هي نفسها تمامًا كما لو كانت قائمة بحد ذاتها.
بالطبع ، طبقًا لمبدأ "إنشاء غلاف لكل شيء تم القيام به مرتين" ، ظهر عدد كبير من الأغلفة المريحة ، على سبيل المثال ، لا يقبل سوى المصنع كمدخل ، وبناء المراسلات بين أنواع النماذج وآرائهم ، ورابطًا إلى Canvas تحتاج إلى إضافة عناصر. والعديد غيرها منها مماثلة ، فقط حوالي عشرة مغلفة للحالات نموذجية.
ضوابط أكثر تعقيدا
في بعض الأحيان تنشأ حالات لا لزوم لها للتعبير عن هذا النموذج ، بقدر ما هي واضحة. هنا ، يمكن أن تتحكم عناصر التحكم التي تؤدي نوعًا من العمليات على قيمة ما ، وكذلك عناصر التحكم التي تراقب عناصر التحكم الأخرى. على سبيل المثال ، الموقف المعتاد: الإجراء له سعر ، والزر نشط فقط إذا كان هناك أموال في الحساب أكثر من سعره.
item.Get(c, FCUnitItem.COST).Join(c, Player.Get(c, MONEY)).Func(c, (cost, money) => cost <= money).SetActive(c, BuyButton);
في الواقع ، فإن الوضع نموذجي لدرجة أنه وفقًا لمبدأي ، يوجد غلاف جاهز له ، ولكن بعد ذلك عرضت محتوياته.
أخذنا العنصر المراد شراؤه ، وأنشأنا كائنًا مشتركًا في أحد حقوله ، وله قيمة كتابة طويلة. لقد أضافوا عنصر تحكم آخر ، وهو أيضًا من النوع الطويل ، وأرجعت الطريقة عنصر تحكم يحتوي على زوج من القيم ، ويتم تشغيل الحدث الذي تم تغييره عندما يتغير أي منها ، ثم يقوم Func بإنشاء كائن لأي تغيير في الإدخال الذي يحسب الوظيفة ، والحدث الذي تم تغييره إذا تم حساب القيمة النهائية لقد تغيرت الوظيفة.
سيقوم المحول البرمجي بنجاح بإنشاء نوع عنصر التحكم الضروري على أساس أنواع بيانات الإدخال ونوع التعبير الناتج. في حالات نادرة عندما يكون النوع الذي يتم إرجاعه بواسطة وظيفة lambda غير واضح ، سيطلب منك المترجم توضيح ذلك بوضوح. أخيرًا ، تستمع آخر مكالمة إلى عنصر التحكم المنطقي ، بناءً على تشغيلها أو إيقاف تشغيلها.
في الواقع ، فإن المجمع الحقيقي في المشروع يقبل زرين للإدخال ، أحدهما للحالة عندما يكون هناك أموال والآخر عندما لا يكون هناك ما يكفي من المال ، كما أن أمر فتح نافذة مشروط "شراء العملات" معلق على الزر الثاني. وكل هذا في سطر واحد بسيط.
من السهل أن ترى أنه باستخدام Join و Func يمكنك بناء هياكل معقدة بشكل تعسفي. في الكود الخاص بي ، كانت هناك وظيفة أدت إلى إنشاء عناصر تحكم معقدة ، وهي حساب مقدار ما يمكن للاعب شراءه من خلال الأخذ في الاعتبار عدد اللاعبين على جانبه ، والقاعدة المتمثلة في أن كل شخص يمكنه تجاوز الميزانية بنسبة 10٪ إذا لم يتجاوز الجميع الميزانية الإجمالية. وهذا مثال على كيفية القيام بذلك ، لأن مقدار ما هو بسيط وسهل تصحيح ما يحدث في النماذج هو بنفس القدر من الصعوبة اكتشاف خطأ في عناصر التحكم التفاعلية. سوف تلتقط التنفيذ وتنفق الكثير من الوقت لفهم ما أدى إليه.
لذلك ، فإن المبدأ العام لاستخدام الضوابط المعقدة هو على النحو التالي: عند وضع نموذج أولي للنموذج ، يمكنك استخدام بنيات على عناصر التحكم التفاعلية ، خاصة إذا لم تكن متأكدًا من أنها ستصبح أكثر تعقيدًا في المستقبل ، ولكن بمجرد أن تشك في أنه إذا حدث كسر ، فلن تفهم ما حدث ، يجب عليك نقل هذه المعالجات على الفور إلى النموذج ، ووضع الحسابات التي تم إجراؤها مسبقًا في عناصر التحكم في طرق التمديد في فئات القاعدة الثابتة.
هذا يختلف اختلافًا كبيرًا عن مبدأ "افعلها بشكل صحيح فورًا" ، المحبوب بين أخصائيي الكمال ، لأننا نعيش في عالم تطوير الألعاب ، وعندما تبدأ في تخطي شكل ما ، لا يمكنك مطلقًا التأكد من ما ستفعله في ثلاثة أيام. كما قال أحد زملائي: "إذا حصلت على خمسة سنتات في كل مرة يغير فيها مصممو اللعبة رأيهم ، فستكون بالفعل شخصًا ثريًا للغاية". في الواقع ، هذا ليس بالأمر السيئ ، لكن العكس صحيح. يجب أن تتطور اللعبة عن طريق التجربة والخطأ ، لأنه إذا لم تكن تقوم بعمل استنساخ غبي ، فلا يمكنك تخيل ما يحتاجه اللاعبون حقًا.
مصدر بيانات واحد لوجهات نظر متعددة
بالنسبة للحالة النموذجية التي تحتاج إلى التحدث عنها بشكل منفصل. يحدث أن يتم تقديم نفس نموذج عنصر كجزء من نموذج واجهة في طريقة عرض مختلفة اعتمادًا على مكان وفي أي سياق يحدث هذا. ونحن نستخدم مبدأ - "نوع واحد ، وجهة نظر واحدة". على سبيل المثال ، لديك بطاقة شراء أسلحة تحتوي على نفس المعلومات غير المعقدة ، ولكن في أوضاع المتجر المختلفة ، يجب تمثيلها بأبنية جاهزة مختلفة. يتكون الحل من جزأين لحالتين مختلفتين.
الأول هو عندما يتم وضع هذا العرض داخل منظرين مختلفين ، على سبيل المثال ، متجر في شكل قائمة قصيرة ومتجر به صور كبيرة. في هذه الحالة ، تم إنشاء مصنعين منفصلين للمساعدة ، وبناء مطابقة نوع الجاهزة. في طريقة ConnectModel في طريقة عرض واحدة ، ستستخدم أحدها والآخر في الآخر. إنها حالة مختلفة تمامًا إذا كنت بحاجة إلى عرض بطاقات بمعلومات متطابقة تمامًا في مكان واحد بشكل مختلف قليلاً. في بعض الأحيان ، في هذه الحالة ، يحتوي نموذج العنصر على حقل إضافي يشير إلى الخلفية الاحتفالية لعنصر معين ، وفي بعض الأحيان يكون فقط لنموذج العنصر وريثًا لا يحتوي على أي حقول ويحتاج فقط إلى رسمه بطبقة مسبقة أخرى. من حيث المبدأ ، لا شيء يتناقض.
قد يبدو حلاً واضحًا ، لكنني رأيت ما يكفي في كود غريب على رقصات غريبة مع الدف حول هذا الموقف ، واعتبر أنه من الضروري أن أكتب عن ذلك.
حالة خاصة: الضوابط مع الجحيم الكثير من التبعيات
هناك حالة واحدة خاصة أريد أن أتحدث عنها بشكل منفصل. هذه عناصر تحكم تراقب عددًا كبيرًا جدًا من العناصر. على سبيل المثال ، عنصر تحكم يراقب قائمة الطرز ويلخص محتويات الحقل الموجود داخل كل عنصر من العناصر. نظرًا لوجود عدد كبير من العناصر الزائدة في القائمة ، على سبيل المثال ، ملء البيانات ، فإن عنصر التحكم هذا يخاطر بالتقاط أكبر عدد ممكن من الأحداث حول التغيير مثل وجود زائد واحد في قائمة العناصر. إن إعادة حساب الوظيفة الكلية مرات عديدة هي بالطبع فكرة سيئة. خاصةً في مثل هذه الحالات ، فإننا نتحكم في اشتراك يشترك في حدث onTransactionFinished ، والذي ينطلق من GameState ، وكما نذكر ، يتوفر رابط إلى GameState في أي نموذج. ومع أي تغيير في المدخلات ، سيضع عنصر التحكم هذا علامة في حد ذاته على أن البيانات الأصلية قد تغيرت ، ولن يتم إعادة حسابها إلا عندما تتلقى رسالة حول نهاية المعاملة ، أو عندما تجد أن المعاملة قد اكتملت بالفعل في الوقت الذي تلقت فيه رسالة من دفق حدث الإدخال . من الواضح أن عنصر التحكم هذا قد لا يكون محميًا من الرسائل غير الضرورية في حالة وجود عنصري تحكم من هذا القبيل في سلسلة معالجة التدفق. أول مجموعة ستجمع سحابة من التغييرات ، تنتظر نهاية المعاملة ، وتبدأ دفق التغييرات أكثر ، وهناك واحدة أخرى قد حصلت بالفعل على مجموعة من التغييرات ، استقبل الحدث حول نهاية المعاملة (كان سيئ الحظ لكونه مدرجًا في قائمة الوظائف التي اشتركت في الحدث سابقًا) ، ثم حساب كل شيء ، بام وحدث تغيير آخر ، وحساب كل شيء مرة ثانية. قد يكون الأمر كذلك ، ولكن نادرًا ، والأهم من ذلك ، أن عناصر التحكم الخاصة بك تقوم بهذه الحسابات الوحشية أكثر من مرة في تدفق واحد من العمليات الحسابية ، فأنت تفعل شيئًا خاطئًا ، وتحتاج إلى نقل كل هذه التلاعب الجهنمية إلى النموذج والقواعد ، حيث في الواقع ، المكان.
مكتبة UniRX الجاهزة
وسيكون من الممكن أن نحصر أنفسنا في كل ما سبق ، وأن نبدأ بهدوء في كتابة تحفة الخاص بك ، خاصة وأن مقارنة فرق النموذج والسيطرة بسيطة للغاية ويتم كتابتها في أقل من أسبوع ، إذا كانت الفكرة التي كنت تخترعها دراجة لا تحجبها ، كل شيء يتم التفكير فيه بالفعل ويتم كتابته أمامي مجانًا للجميع.
عند الكشف عن UniRX ، نجد تصميمًا جميلًا ومتوافقًا مع المعايير يمكنه إنشاء سلاسل من كل شيء بشكل عام ، أو دمجها بذكاء ، أو ترشيحها من الخيط الرئيسي إلى الخيط غير الرئيسي ، أو إعادة التحكم مرة أخرى إلى الخيط الرئيسي ، الذي يحتوي على مجموعة من الأدوات الجاهزة للإرسال إلى أماكن مختلفة ، وما إلى ذلك. أبعد من ذلك. ليس لدينا بالضبط شيئان هناك: البساطة وسهولة تصحيح الأخطاء. هل حاولت تصحيح بعض المباني متعددة الطوابق على Linq في خطوات في مصحح الأخطاء؟ لذلك ، ما زال الوضع أسوأ بكثير. في الوقت نفسه ، نحن نفتقر تمامًا إلى ما تم إنشاء كل هذه الآلات المتطورة فيه. من أجل بساطة حالات تصحيح الأخطاء وإعادة إنتاجها ، نفتقر تمامًا إلى مجموعة متنوعة من مصادر الإشارات ، وكل شيء يحدث في الدفق الرئيسي ، لأن التزاوج مع تعدد العمليات في اللعبة الوصفية أمر ضروري تمامًا ، وكل عدم التزامن في معالجة الأمر مخفي داخل محرك إرسال الأوامر ، وتزامن عدم التزامن نفسه ليس الكثير من المساحة ، يتم إيلاء المزيد من الاهتمام لجميع أنواع الشيكات ، والفحوصات الذاتية ، وإمكانيات التسجيل والتشغيل.
بشكل عام ، إذا كنت تعرف بالفعل كيفية استخدام UniRX ، فسوف أصنعه خصيصًا لك من أجل نماذج IObservable ، ويمكنك استخدام ميزات ورقة رابحة لمكتبتك المفضلة حيث تحتاج إليها ، ولكن بالنسبة للباقي أقترح ألا أحاول إنشاء دبابات من السيارات عالية السرعة والسيارات من الدبابات فقط على الأرض أن كلاهما عجلات.
في نهاية المقال ، عليّ ، أيها القراء الأعزاء ، طرح أسئلة تقليدية مهمة جدًا بالنسبة لي ، وأفكاري حول الجميل ، وآفاق تطوير عملي العلمي والتقني.