
أرغب في مشاركة عملية تطوير لعبة محمولة بسيطة بواسطة مطورين وفنان. هذه المقالة إلى حد كبير وصف للتنفيذ التقني.
الحذر ، والكثير من النص!
المقال ليس مرشدًا أو درسًا ، على الرغم من أنني آمل أن يتمكن القراء من تعلم شيء مفيد منه. مصممة للمطورين الذين لديهم دراية بالوحدة مع بعض الخبرة البرمجية
المحتويات:
فكرةاللعبقصةتصميمجوهر- العناصر الكهربائية
- حلالا
- ElementsProvider
- CircuitGenerator
فصول اللعبة- نهج التنمية و DI
- ترتيب
- العناصر الكهربائية
- إدارة اللعبة
- مستوى التحميل
- مشهد
- اللعب إضافية
- تسييل
- واجهة المستخدم
- تحليلات
- الكاميرا لتحديد المواقع والرسوم البيانية
- مخططات الألوان
ملحقات محرر
- مولد كهربائي
- حلالا
مفيد- AssertHelper
- SceneObjectsHelper
- CoroutineStarter
- الأداة
تجريبملخص التنميةفكرة
محتوىكان هناك فكرة لجعل لعبة الجوال بسيطة في فترة قصيرة.
الشروط والأحكام:
- من السهل تنفيذ اللعبة
- الحد الأدنى لمتطلبات الفن
- وقت تطوير قصير (عدة أشهر)
- مع التشغيل الآلي السهل لإنشاء المحتوى (المستويات والمواقع وعناصر اللعبة)
- قم بإنشاء مستوى بسرعة إذا كانت اللعبة تتكون من عدد محدود من المستويات
من أجل اتخاذ قرار ، ولكن ماذا تفعل في الواقع؟ بعد كل شيء ، جاءت الفكرة لجعل لعبة ، وليس فكرة لعبة. تقرر البحث عن الإلهام من متجر التطبيقات.
تضاف إلى العناصر المذكورة أعلاه:
- يجب أن تتمتع اللعبة بشعبية معينة بين اللاعبين (عدد مرات التنزيل + التصنيفات)
- يجب ألا يكون متجر التطبيقات مزدحمًا بألعاب مشابهة
تم العثور على لعبة بها طريقة لعب تعتمد على البوابات المنطقية. لم تكن هناك عمليات مماثلة بأعداد كبيرة ، حيث تحتوي اللعبة على العديد من التنزيلات والتقييمات الإيجابية. ومع ذلك ، بعد المحاولة ، كانت هناك بعض العيوب التي يمكن أخذها في الاعتبار في لعبتك.
طريقة اللعب في اللعبة هي أن المستوى عبارة عن دائرة رقمية بها العديد من المدخلات والمخرجات. يجب أن يختار المشغل مجموعة من المدخلات بحيث يكون الإخراج منطقيًا 1. لا يبدو الأمر صعبًا للغاية. قامت اللعبة أيضًا بإنشاء مستويات تلقائيًا ، مما يوحي بأن القدرة على إنشاء المستويات تلقائيًا ، رغم أنها لا تبدو بسيطة جدًا. اللعبة جيدة أيضًا للتعلم ، والتي أعجبتني حقًا.
الايجابيات:
- بساطة فنية من اللعب
- تبدو سهلة للاختبار مع autotests
- القدرة على إنشاء المستويات تلقائيًا
سلبيات:
- يجب عليك أولا إنشاء المستويات
الآن استكشاف عيوب اللعبة التي ألهمت.
- لا تتكيف مع نسبة العرض إلى الارتفاع المخصصة ، مثل 18: 9
- لا توجد وسيلة لتخطي مستوى صعب أو الحصول على تلميح
- في الاستعراضات كانت هناك شكاوى حول عدد قليل من المستويات
- اشتكت الاستعراضات من عدم وجود مجموعة متنوعة من العناصر
ننتقل إلى تخطيط لعبتنا:
- نستخدم البوابات المنطقية القياسية (AND، NAND، OR، NOR، XOR، XNOR، NOR، NOT)
- يتم عرض البوابات مع صورة بدلاً من تعيين نص ، وهو أسهل للتمييز. نظرًا لأن العناصر لها تدوين ANSI قياسي ، فإننا نستخدمها.
- نتجاهل رمز التبديل الذي يربط أحد المدخلات بأحد المخرجات. نظرًا لأنه يتطلب منك النقر فوق نفسك ولا تنسجم مع العناصر الرقمية الحقيقية قليلاً. نعم ، ومن الصعب تخيل مفتاح تبديل في شريحة.
- إضافة عناصر التشفير وفك التشفير.
- نقدم وضعًا يتعين على المشغل فيه تحديد العنصر المرغوب فيه في الخلية بقيم ثابتة عند مدخلات الدائرة.
- نحن نقدم المساعدة للاعب: تلميح + مستوى تخطي.
- سيكون من الجميل أن تضيف بعض المؤامرة.
اللعب
محتوىالوضع 1: يتلقى اللاعب دائرة ولديه حق الوصول لتغيير القيم عند المدخلات.
الوضع 2: يتلقى اللاعب دائرة يمكنه فيها تغيير العناصر ولكن لا يمكنه تغيير القيم عند المدخلات.
سيكون اللعب في شكل مستويات معدة مسبقًا. بعد الانتهاء من المستوى ، يجب أن يحصل اللاعب على بعض النتائج ، وسيتم ذلك في شكل النجوم الثلاثة التقليدية ، اعتمادًا على نتيجة المقطع.
ماذا يمكن أن تكون مؤشرات الأداء:
عدد الإجراءات: كل تفاعل مع عناصر اللعبة يزيد من العداد.
عدد الاختلافات في الحالة الناتجة عن الأصل. لا يأخذ في الاعتبار عدد محاولات اللاعب لإكماله. لسوء الحظ ، لا يتناسب مع النظام الثاني.
سيكون من الجميل إضافة نفس الوضع مع إنشاء مستوى عشوائي. ولكن الآن ، تأجيله في وقت لاحق.
قصة
محتوىأثناء التفكير في اللعب وبدء التطوير ، ظهرت أفكار مختلفة لتحسين اللعبة. وبدا فكرة مثيرة للاهتمام بما فيه الكفاية - لإضافة مؤامرة.
إنه عن مهندس يصمم الدوائر. ليس سيئًا ، لكنه غير كامل ، ربما يستحق عرض تصنيع الرقائق استنادًا إلى ما يفعله اللاعب؟ الروتين بطريقة أو بأخرى ، لا توجد نتيجة مفهومة وبسيطة.
الفكرة! يقوم المهندس بتطوير روبوت بارد باستخدام داراته المنطقية. الروبوت هو شيء بسيط إلى حد ما مفهوم ويناسب تماما مع اللعب.
تذكر الفقرة الأولى ، "الحد الأدنى لمتطلبات الفن"؟ شيء لا يتلاءم مع cutscenes في المؤامرة. ثم يأتي فنان مألوف إلى الإنقاذ ، الذي وافق على مساعدتنا.
الآن دعونا نقرر تنسيق وإدماج cutscenes في اللعبة.
يجب أن يتم عرض قطعة الأرض على شكل قطع بدون سجل أو وصف نصي يزيل مشاكل الترجمة ، ويبسط فهمها ، والعديد من اللعب على الأجهزة المحمولة بدون صوت. اللعبة عبارة عن عناصر حقيقية جدًا للدوائر الرقمية ، أي أنه من الممكن ربط هذا بالواقع.
يجب أن تكون المشاهد والمستويات مشاهد منفصلة. قبل مستوى معين ، يتم تحميل مشهد معين.
حسنا ، تم تعيين المهمة ، وهناك موارد للوفاء ، وبدأ العمل في الغليان.
تصميم
محتوىقررت على الفور على المنصة ، وهذا هو الوحدة. نعم مبالغة قليلا ، لكنني أعرفها.
أثناء التطوير ، يتم كتابة الكود فورًا مع الاختبارات أو حتى بعده. ولكن للحصول على سرد كلي ، يتم وضع الاختبار في قسم منفصل أدناه. سيصف القسم الحالي عملية التطوير بشكل منفصل عن الاختبار.
جوهر
محتوىيبدو جوهر اللعبة بسيطًا جدًا وغير مرتبط بالمحرك ، لذلك بدأنا بالتصميم في شكل رمز C #. يبدو أنه يمكنك تحديد منطق أساسي منفصل. أخرجه إلى مشروع منفصل.
تعمل الوحدة مع حل C # والمشروعات بداخلها غير معتادة قليلاً لمطور .Net العادي ، وملفات .sln و .csproj يتم إنشاؤها بواسطة Unity نفسها ولا يتم قبول التغييرات داخل هذه الملفات للنظر فيها من جانب Unity. وقال انه ببساطة الكتابة عليها وحذف جميع التغييرات. لإنشاء مشروع جديد ، يجب عليك استخدام ملف
تعريف التجميع .


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

تشير الأسهم إلى اتجاه البيانات ، واعتماد العناصر في الاتجاه المعاكس.
تحديد واجهة الموصل. يمكنك الحصول على القيمة منه.
public interface IConnector { bool Value { get; } }
فقط كيفية توصيله إلى موصل آخر؟
تحديد المزيد من واجهات.
public interface IInputConnector : IConnector { IOutputConnector ConnectedOtherConnector { get; set; } }
IInputConnector هو موصل إدخال ، وله رابط إلى موصل آخر.
public interface IOutputConnector : IConnector { IElectricalElement Element { set; get; } }
يشير موصل الإخراج إلى عنصره الذي سيطلب منه قيمة.
public interface IElectricalElement { bool GetValue(byte number = 0); }
يجب أن يحتوي العنصر الكهربائي على طريقة تُرجع قيمة على ناتج معين ، والرقم هو رقم الإخراج.
لقد أطلق عليها IElectricalElement ، على الرغم من أنها تنقل مستويات الجهد المنطقي فقط ، ولكن من ناحية أخرى ، يمكن أن تكون عنصرًا لا يضيف منطقًا على الإطلاق ، فقط ينقل قيمة ، مثل الموصل.الآن دعنا ننتقل إلى التنفيذ
public class InputConnector : IInputConnector { public IOutputConnector ConnectedOtherConnector { get; set; } public bool Value { get { return ConnectedOtherConnector?.Value ?? false; } } }
قد لا يتم توصيل الموصل الوارد ، وفي هذه الحالة سيعود خطأ.
public class OutputConnector : IOutputConnector { private readonly byte number; public OutputConnector(byte number = 0) { this.number = number; } public IElectricalElement Element { get; set; } public bool Value => Element.GetValue(number); } }
يجب أن يحتوي الإخراج على رابط لعنصره ورقمه بالنسبة للعنصر.
علاوة على ذلك ، باستخدام هذا الرقم ، يطلب قيمة من العنصر.
public abstract class ElectricalElementBase { public IInputConnector[] Input { get; set; } }
الفئة الأساسية لجميع العناصر ، فقط يحتوي على مجموعة من المدخلات.
مثال على تنفيذ عنصر:
public class And : ElectricalElementBase, IElectricalElement { public bool GetValue(byte number = 0) { bool outputValue = false; if (Input?.Length > 0) { outputValue = Input[0].Value; foreach (var item in Input) { outputValue &= item.Value; } } return outputValue; } }
يعتمد التنفيذ كليًا على العمليات المنطقية بدون جدول الحقيقة الصعب. ربما لا تكون واضحة كما هو الحال مع الجدول ، لكنها ستكون مرنة ، وستعمل على أي عدد من المدخلات.
جميع البوابات المنطقية لها ناتج واحد ، وبالتالي فإن القيمة في الإخراج لن تعتمد على رقم الإدخال.
تصنع العناصر المقلوبة على النحو التالي:
public class Nand : And, IElectricalElement { public new bool GetValue(byte number = 0) { return !base.GetValue(number); } }
تجدر الإشارة إلى أن أسلوب GetValue هنا قد تم تجاوزه ، ولم يتم تجاوزه تقريبًا. يتم ذلك استنادًا إلى المنطق القائل بأنه إذا نجح Nand في And ، فسوف يستمر في التصرف مثل و. كان من الممكن أيضًا تطبيق التكوين ، ولكن هذا سيتطلب رمزًا إضافيًا ، وهو ما لا معنى له.
بالإضافة إلى الصمامات التقليدية ، تم إنشاء العناصر التالية:
مصدر - مصدر قيمة ثابت من 0 أو 1.
موصل - فقط نفس أو موصل ، لديه فقط تطبيق مختلف قليلا ، انظر الجيل.
AlwaysFalse - إرجاع دائمًا 0 ، مطلوب من أجل الوضع الثاني.
حلالا
محتوىبعد ذلك ، يُعد الفصل مفيدًا في العثور تلقائيًا على المجموعات التي تعطي 1 في إخراج الدائرة.
public interface ISolver { ICollection<bool[]> GetSolutions(IElectricalElement root, params Source[] sources); } public class Solver : ISolver { public ICollection<bool[]> GetSolutions(IElectricalElement root, params Source[] sources) {
الحلول هي القوة الغاشمة. لهذا ، يتم تحديد الحد الأقصى لعدد والتي يمكن التعبير عنها من خلال مجموعة من البتات بمبلغ يساوي عدد المصادر. وهذا هو ، 4 مصادر = 4 بت = العدد الأقصى 15. نحن فرز من خلال جميع الأرقام من 0 إلى 15.
ElementsProvider
محتوىلتوفير الراحة للجيل ، قررت تحديد رقم لكل عنصر ، وللقيام بذلك ، قمت بإنشاء فئة ElementsProvider مع واجهة IElementsProvider.
public interface IElementsProvider { IList<Func<IElectricalElement>> Gates { get; } IList<Func<IElectricalElement>> Conductors { get; } IList<ElectricalElementType> GateTypes { get; } IList<ElectricalElementType> ConductorTypes { get; } } public class ElementsProvider : IElementsProvider { public IList<Func<IElectricalElement>> Gates { get; } = new List<Func<IElectricalElement>> { () => new And(), () => new Nand(), () => new Or(), () => new Nor(), () => new Xor(), () => new Xnor() }; public IList<Func<IElectricalElement>> Conductors { get; } = new List<Func<IElectricalElement>> { () => new Conductor(), () => new Not() }; public IList<ElectricalElementType> GateTypes { get; } = new List<ElectricalElementType> { ElectricalElementType.And, ElectricalElementType.Nand, ElectricalElementType.Or, ElectricalElementType.Nor, ElectricalElementType.Xor, ElectricalElementType.Xnor }; public IList<ElectricalElementType> ConductorTypes { get; } = new List<ElectricalElementType> { ElectricalElementType.Conductor, ElectricalElementType.Not }; }
أول قائمتين يشبهان المصانع التي تعطي عنصرًا في الرقم المحدد. آخر قائمتين هما عكاز يجب استخدامه نظرًا لميزات الوحدة. حول هذا الموضوع كذلك.
CircuitGenerator
محتوىالآن الجزء الأصعب من التطوير هو توليد الدوائر.
تتمثل المهمة في إنشاء قائمة بالمخططات التي يمكنك منها تحديد المخطط الذي تريده في المحرر. هناك حاجة إلى جيل فقط للصمامات بسيطة.
يتم تعيين معلمات معينة للمخطط ، وهي: عدد الطبقات (الخطوط الأفقية للعناصر) والحد الأقصى لعدد العناصر في الطبقة. من الضروري أيضًا تحديد البوابات التي تحتاجها لإنشاء دوائر.
كان مقالي هو تقسيم المهمة إلى جزأين - إنشاء الهيكل واختيار الخيارات.
يحدد مُنشئ البنية مواقع ووصلات العناصر المنطقية.
يحدد المولد البديل مجموعات صالحة من العناصر في المواضع.
StructureGenerator
يتكون الهيكل من طبقات من عناصر المنطق وطبقات الموصلات / المحولات. الهيكل كله لا يحتوي على عناصر حقيقية ولكن حاويات لهم.
الحاوية عبارة عن فئة موروثة من IElectricalElement ، والتي تحتوي داخلها على قائمة بالعناصر الصالحة ويمكن التبديل بينها. كل عنصر له رقمه الخاص في القائمة.
ElectricalElementContainer : ElectricalElementBase, IElectricalElement
يمكن للحاوية أن تضبط "نفسها" على أحد العناصر من القائمة. أثناء التهيئة ، يجب أن تعطيه قائمة بالمفوضين الذين سيقومون بإنشاء العناصر. في الداخل ، يستدعي كل مندوب ويحصل على العنصر. بعد ذلك يمكنك ضبط نوع محدد من هذا العنصر ، وهذا يربط العنصر الداخلي بنفس المدخلات كما في الحاوية وسيتم إخراج المخرج من الحاوية من إخراج هذا العنصر.

طريقة لإعداد قائمة العناصر:
public void SetElements(IList<Func<IElectricalElement>> elements) { Elements = new List<IElectricalElement>(elements.Count); foreach (var item in elements) { Elements.Add(item()); } }
بعد ذلك ، يمكنك ضبط النوع بهذه الطريقة:
public void SetType(int number) { if (isInitialized == false) { throw new InvalidOperationException(UnitializedElementsExceptionMessage); } SelectedType = number; RealElement = Elements[number]; ((ElectricalElementBase) RealElement).Input = Input; }
بعد ذلك سوف يعمل كبند محدد.
تم إنشاء الهيكل التالي للدائرة:
public class CircuitStructure : ICloneable { public IDictionary<int, ElectricalElementContainer[]> Gates; public IDictionary<int, ElectricalElementContainer[]> Conductors; public Source[] Sources; public And FinalDevice; }
تخزن القواميس هنا رقم الطبقة في المفتاح ومجموعة من الحاويات لهذه الطبقة. التالي هو مجموعة من المصادر و FinalDevice واحد الذي يرتبط كل شيء.
وبالتالي فإن المولد الهيكلي يخلق الحاويات ويربطها ببعضها البعض. يتم إنشاء كل ذلك في طبقات ، من الأسفل إلى الأعلى. القاع هو الأوسع (معظم العناصر). تحتوي الطبقة أعلاه على عناصر أقل بمرتين وهكذا حتى نصل إلى الحد الأدنى. يتم توصيل مخرجات جميع عناصر الطبقة العليا بالجهاز النهائي.
تحتوي طبقة العنصر المنطقي على حاويات للبوابات. في طبقة الموصلات توجد عناصر ذات مدخل واحد و مخرج. يمكن أن يكون هناك عناصر إما موصل أو عنصر NO. يقوم الموصل بتمرير ما جاء إلى المخرجات ، ويقوم عنصر NO بإرجاع القيمة المقلوبة في الإخراج.
أول من خلق مجموعة من المصادر. يحدث الجيل من أسفل إلى أعلى ، يتم إنشاء طبقة الموصلات أولاً ، ثم طبقة المنطق ، وعند إخراجها مرة أخرى الموصلات.

لكن مثل هذه المخططات مملة للغاية! لقد أردنا تبسيط حياتنا بشكل أكبر وقررنا أن نجعل الهياكل التي تم إنشاؤها أكثر إثارة للاهتمام (معقدة) ، فقد تقرر إضافة تعديلات الهيكل مع المتفرعة أو الاتصال عبر العديد من الطبقات.
حسنًا ، لقول "مبسط" - هذا يعني تعقيد حياتك في شيء آخر.
لقد أصبح إنشاء دوائر بأقصى مستوى من قابلية التعديل مهمة شاقة وليست عملية للغاية. لذلك ، قرر فريقنا القيام بما يفي بهذه المعايير:
تطوير هذه المهمة لم يستغرق الكثير من الوقت.
أكثر أو أقل جيل كافية من الهياكل المعدلة.
لم تكن هناك التقاطعات بين الموصلات.
نتيجة لبرمجة طويلة وشاقة ، تمت كتابة الحل في الساعة 4 بعد الظهر.
دعنا نلقي نظرة على الكود و ̶̶̶̶̶̶̶̶̶̶.
هنا تتم مواجهة فئة OverflowArray. لأسباب تاريخية ، تمت إضافته بعد الجيل الهيكلي الأساسي وله علاقة أكثر بالجيل البديل ، لذلك يوجد أدناه. الارتباط . public IEnumerable<CircuitStructure> GenerateStructure(int lines, int maxElementsInLine, StructureModification modification) { var baseStructure = GenerateStructure(lines, maxElementsInLine); for (int i = 0; i < lines; i++) { int maxValue = 1; int branchingSign = 1; if (modification == StructureModification.All) { maxValue = 2; branchingSign = 2; } int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length; var elementArray = new OverflowArray(lengthOverflowArray, maxValue); double numberOfOption = Math.Pow(2, lengthOverflowArray); for (int k = 1; k < numberOfOption - 1; k++) { elementArray.Increase(); if (modification == StructureModification.Branching || modification == StructureModification.All) { if (!CheckOverflowArrayForAllConnection(elementArray, branchingSign, lengthOverflowArray)) { continue; } }
بعد عرض هذا الرمز ، أود أن أفهم ما يحدث فيه.
لا تقلق! شرح موجز دون تفاصيل يسرع لك.
أول شيء نفعله هو إنشاء هيكل (أساسي) عادي.
var baseStructure = GenerateStructure(lines, maxElementsInLine);
بعد ذلك ، نتيجة لفحص بسيط ، قمنا بتعيين علامة التفرع (branchingSign) على القيمة المناسبة ، لماذا هذا ضروري؟ كذلك سيكون واضحا.
int maxValue = 1; int branchingSign = 1; if (modification == StructureModification.All) { maxValue = 2; branchingSign = 2; }
الآن نحن نحدد طول OverflowArray لدينا وتهيئته.
int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length; var elementArray = new OverflowArray(lengthOverflowArray, maxValue);
لكي نتمكن من متابعة عملياتنا مع البنية ، نحتاج إلى معرفة عدد الاختلافات المحتملة في OverflowArray. للقيام بذلك ، هناك صيغة تم تطبيقها على السطر التالي.
int lengthOverflowArray = baseStructure.Gates[(i * 2) + 1].Length;
التالي هو حلقة متداخلة يحدث فيها كل "السحر" والتي كان لها كل هذه المقدمة ، في البداية ، نزيد قيم صفيفنا.
elementArray.Increase();
بعد ذلك ، نرى التحقق من الصحة ، ونتيجة لذلك نذهب إلى أبعد من ذلك أو التكرار التالي.
if (modification == StructureModification.Branching || modification == StructureModification.All) { if (!CheckOverflowArrayForAllConnection(elementArray, branchingSign, lengthOverflowArray)) { continue; } }
إذا نجحت المصفوفة في التحقق من الصحة ، فسنقوم باستنساخ هيكلنا الأساسي. هناك حاجة إلى الاستنساخ حيث سنقوم بتعديل هيكلنا للعديد من التكرارات.
وأخيراً ، نبدأ في تعديل الهيكل وتنظيفه من العناصر غير الضرورية. أصبحت غير ضرورية نتيجة التعديل الهيكلي.
ModifyStructure(structure, elementArray, key, modification); ClearStructure(structure);
لا أرى النقطة بمزيد من التفصيل لتحليل العشرات من الوظائف الصغيرة التي يتم تنفيذها "في مكان ما" في الأعماق.
VariantsGenerator
تسمى العناصر + هيكل التي يجب أن يكون فيه CircuitVariant.
public struct CircuitVariant { public CircuitStructure Structure; public IDictionary<int, int[]> Gates; public IDictionary<int, int[]> Conductors; public IList<bool[]> Solutions; }
الحقل الأول هو رابط للهيكل. القواميس الثانيان حيث يكون المفتاح هو رقم الطبقة ، والقيمة عبارة عن صفيف يحتوي على أرقام العناصر في أماكنهم في الهيكل.
ننتقل إلى اختيار مجموعات. يمكننا الحصول على عدد معين من العناصر المنطقية والموصلات الصالحة. في المجموع ، يمكن أن يكون هناك 6 عناصر المنطق و 2 الموصلات.
يمكنك تخيل نظام أرقام بقاعدة 6 والحصول على الأرقام التي تتوافق مع العناصر في كل فئة. وبالتالي ، من خلال زيادة هذا الرقم السداسي عشر ، يمكنك تصفح جميع مجموعات العناصر.
أي أن الرقم السداسي عشر المكون من ثلاثة أرقام سيكون 3 عناصر. تجدر الإشارة فقط إلى أنه يمكن إرسال عدد العناصر لا 6 ولكن 4.
لأداء مثل هذا الرقم ، لقد حددت الهيكل
public struct ClampedInt { public int Value { get => value; set => this.value = Mathf.Clamp(value, 0, MaxValue); } public readonly int MaxValue; private int value; public ClampedInt(int maxValue) { MaxValue = maxValue; value = 0; } public bool TryIncrease() { if (Value + 1 <= MaxValue) { Value++; return false; }
التالي هو فئة تحمل اسمًا غريبًا
OverflowArray . جوهرها أنه يخزن صفيف
ClampedInt ويزيد الترتيب العالي في حالة
حدوث تجاوز
في الترتيب
المنخفض وهكذا حتى يصل إلى الحد الأقصى للقيمة في جميع الخلايا.
وفقًا لكل ClampedInt ، يتم تعيين قيم ElectricalElementContainer المقابلة. وبالتالي ، فمن الممكن الفرز من خلال جميع المجموعات الممكنة. تجدر الإشارة إلى أنه إذا كنت تريد إنشاء مخطط بعناصر (على سبيل المثال ، و (0) و Xor (4)) ، فلن تحتاج إلى فرز جميع الخيارات ، بما في ذلك العناصر 1،2،3. لهذا ، أثناء التوليد ، تحصل العناصر على أرقامها المحلية (على سبيل المثال ، = 0 ، Xor = 1) ، وبعد ذلك يتم تحويلها مرة أخرى إلى أرقام عمومية.
حتى تتمكن من التكرار على جميع المجموعات الممكنة في جميع العناصر.
بعد ضبط القيم الموجودة في الحاويات ، يتم فحص الدائرة بحثًا عن حلول لها باستخدام
Solver . إذا مرت الدائرة على القرار ، فستعود.
بعد إنشاء الدائرة ، يتم فحص عدد الحلول. يجب ألا يتجاوز الحد المسموح به ويجب ألا يكون له قرارات تتكون بالكامل من 0 أو 1.
الكثير من الكود public interface IVariantsGenerator { IEnumerable<CircuitVariant> Generate(IEnumerable<CircuitStructure> structures, ICollection<int> availableGates, bool useNot, int maxSolutions = int.MaxValue); } public class VariantsGenerator : IVariantsGenerator { private readonly ISolver solver; private readonly IElementsProvider elementsProvider; public VariantsGenerator(ISolver solver, IElementsProvider elementsProvider) { this.solver = solver; this.elementsProvider = elementsProvider; } public IEnumerable<CircuitVariant> Generate(IEnumerable<CircuitStructure> structures, ICollection<int> availableGates, bool useNot, int maxSolutions = int.MaxValue) { bool manyGates = availableGates.Count > 1; var availableLeToGeneralNumber = GetDictionaryFromAllowedElements(elementsProvider.Gates, availableGates); var gatesList = GetElementsList(availableLeToGeneralNumber, elementsProvider.Gates); var availableConductorToGeneralNumber = useNot ? GetDictionaryFromAllowedElements(elementsProvider.Conductors, new[] {0, 1}) : GetDictionaryFromAllowedElements(elementsProvider.Conductors, new[] {0}); var conductorsList = GetElementsList(availableConductorToGeneralNumber, elementsProvider.Conductors); foreach (var structure in structures) { InitializeCircuitStructure(structure, gatesList, conductorsList); var gates = GetListFromLayersDictionary(structure.Gates); var conductors = GetListFromLayersDictionary(structure.Conductors); var gatesArray = new OverflowArray(gates.Count, availableGates.Count - 1); var conductorsArray = new OverflowArray(conductors.Count, useNot ? 1 : 0); do { if (useNot && conductorsArray.EqualInts) { continue; } SetContainerValuesAccordingToArray(conductors, conductorsArray); do { if (manyGates && gatesArray.Length > 1 && gatesArray.EqualInts) { continue; } SetContainerValuesAccordingToArray(gates, gatesArray); var solutions = solver.GetSolutions(structure.FinalDevice, structure.Sources); if (solutions.Any() && solutions.Count <= maxSolutions && !(solutions.Any(s => s.All(b => b)) || solutions.Any(s => s.All(b => !b)))) { var variant = new CircuitVariant { Conductors = GetElementsNumberFromLayers(structure.Conductors, availableConductorToGeneralNumber), Gates = GetElementsNumberFromLayers(structure.Gates, availableLeToGeneralNumber), Solutions = solutions, Structure = structure }; yield return variant; } } while (!gatesArray.Increase()); } while (useNot && !conductorsArray.Increase()); } } private static void InitializeCircuitStructure(CircuitStructure structure, IList<Func<IElectricalElement>> gates, IList<Func<IElectricalElement>> conductors) { var lElements = GetListFromLayersDictionary(structure.Gates); foreach (var item in lElements) { item.SetElements(gates); } var cElements = GetListFromLayersDictionary(structure.Conductors); foreach (var item in cElements) { item.SetElements(conductors); } } private static IList<Func<IElectricalElement>> GetElementsList(IDictionary<int, int> availableToGeneralGate, IReadOnlyList<Func<IElectricalElement>> elements) { var list = new List<Func<IElectricalElement>>(); foreach (var item in availableToGeneralGate) { list.Add(elements[item.Value]); } return list; } private static IDictionary<int, int> GetDictionaryFromAllowedElements(IReadOnlyCollection<Func<IElectricalElement>> allElements, IEnumerable<int> availableElements) { var enabledDic = new Dictionary<int, bool>(allElements.Count); for (int i = 0; i < allElements.Count; i++) { enabledDic.Add(i, false); } foreach (int item in availableElements) { enabledDic[item] = true; } var availableToGeneralNumber = new Dictionary<int, int>(); int index = 0; foreach (var item in enabledDic) { if (item.Value) { availableToGeneralNumber.Add(index, item.Key); index++; } } return availableToGeneralNumber; } private static void SetContainerValuesAccordingToArray(IReadOnlyList<ElectricalElementContainer> containers, IOverflowArray overflowArray) { for (int i = 0; i < containers.Count; i++) { containers[i].SetType(overflowArray[i].Value); } } private static IReadOnlyList<ElectricalElementContainer> GetListFromLayersDictionary(IDictionary<int, ElectricalElementContainer[]> layers) { var elements = new List<ElectricalElementContainer>(); foreach (var layer in layers) { elements.AddRange(layer.Value); } return elements; } private static IDictionary<int, int[]> GetElementsNumberFromLayers(IDictionary<int, ElectricalElementContainer[]> layers, IDictionary<int, int> elementIdToGlobal = null) { var dic = new Dictionary<int, int[]>(layers.Count); bool convert = elementIdToGlobal != null; foreach (var layer in layers) { var values = new int[layer.Value.Length]; for (int i = 0; i < layer.Value.Length; i++) { if (!convert) { values[i] = layer.Value[i].SelectedType; } else { values[i] = elementIdToGlobal[layer.Value[i].SelectedType]; } } dic.Add(layer.Key, values); } return dic; } }
إرجاع كل من المولدات متغير باستخدام عبارة العائد. وبالتالي ، فإن CircuitGenerator باستخدام StructureGenerator و VariantsGenerator يولد IEnumerable. (ساعد النهج مع العائد الكثير في المستقبل ، انظر أدناه)
يتبع من حقيقة أن مولد الخيارات يتلقى قائمة الهياكل. يمكنك إنشاء خيارات لكل هيكل بشكل مستقل. يمكن أن يكون هذا متوازياً ، لكن إضافة AsParallel لم تنجح (ربما تؤدي إلى تداخل). سيكون التوازي يدويًا وقتًا طويلًا ، لأننا نتجاهل هذا الخيار.
في الواقع ، حاولت أن أفعل الجيل الموازي ، لقد نجحت ، لكن كانت هناك بعض الصعوبات ، لأنها لم تذهب إلى المستودع.فصول اللعبة
نهج التنمية و DI
محتوىتم بناء المشروع تحت
Dependency Injection (DI). هذا يعني أن الفئات يمكنها ببساطة أن تطلب من نفسها نوعًا من الكائنات المطابقة للواجهة وعدم المشاركة في إنشاء هذا الكائن. ما هي الفوائد:
- يتم تعريف مكان تكوين وتهيئة كائن التبعية في مكان واحد ويفصلها عن منطق الفئات التابعة ، مما يزيل ازدواجية الكود.
- يلغي الحاجة لاستخراج شجرة التبعية بأكملها والبدء في إنشاء كل التبعيات.
- يتيح لك بسهولة تغيير تطبيق الواجهة ، والذي يتم استخدامه في العديد من الأماكن.
كحاوية DI في المشروع ،
يتم استخدام
Zenject .
يحتوي Zenject على العديد من السياقات ، وأنا أستخدم اثنين فقط منها:
- سياق المشروع - تسجيل التبعيات داخل التطبيق بأكمله.
- سياق المشهد: تسجيل الفصول الموجودة فقط في مشهد معين وعمرها محدود بفترة حياة المشهد.
- السياق الثابت هو سياق عام لكل شيء بشكل عام ، والخصوصية هي أنه موجود في المحرر. يمكنني استخدامها للحقن في المحرر
يتم تخزين تسجيل الفئة في
المثبت . يمكنني استخدام
ScriptableObjectInstaller لسياق المشروع ، و
MonoInstaller لسياق المشهد.
معظم الفئات التي أسجلها في AsSingle ، نظرًا لأنها لا تحتوي على حالة ، من الأرجح أن تكون مجرد حاويات للطرق. يمكنني استخدام AsTransient للفئات حيث توجد حالة داخلية لا ينبغي أن تكون شائعة في الفئات الأخرى.بعد ذلك ، تحتاج إلى إنشاء فئات MonoBehaviour بطريقة ما لتمثل هذه العناصر. كما خصصت فئات متعلقة بالوحدة لمشروع منفصل اعتمادًا على المشروع الأساسي.
بالنسبة إلى فصول MonoBehaviour ، أفضل إنشاء واجهات خاصة بي. هذا ، بالإضافة إلى المزايا القياسية للواجهات ، يتيح لك إخفاء عدد كبير جدًا من أعضاء MonoBehaviour.من أجل الراحة ، غالبًا ما تنشئ DI فصلًا بسيطًا يدير كل المنطق ومجمع MonoBehaviour لذلك. على سبيل المثال ، لدى الفصل طرق Start و Update ، أقوم بإنشاء هذه الأساليب في الفصل ، ثم في فئة MonoBehaviour أقوم بإضافة حقل تبعية وفي الطرق المقابلة التي أسميها Start و Update. هذا يعطي الحقن "الصحيح" للمنشئ ، وفصل الفئة الرئيسية من حاوية DI والقدرة على الاختبار بسهولة.ترتيب
المحتوىحسب التكوين ، أعني البيانات الشائعة للتطبيق بأكمله. في حالتي ، هذه هي المباني الجاهزة ، ومعرّفات الإعلانات والمشتريات ، والعلامات ، وأسماء المشهد ، إلخ. لهذه الأغراض ، أستخدم ScriptableObjects:- لكل مجموعة بيانات ، يتم تخصيص فئة سليل ScriptableObject.
- يخلق الحقول اللازمة للتسلسل
- تتم إضافة خصائص القراءة من هذه الحقول.
- يتم تمييز الواجهة مع الحقول أعلاه
- تسجل الفئة إلى واجهة في حاوية DI
- ربح
public interface ITags { string FixedColor { get; } string BackgroundColor { get; } string ForegroundColor { get; } string AccentedColor { get; } } [CreateAssetMenu(fileName = nameof(Tags), menuName = "Configuration/" + nameof(Tags))] public class Tags : ScriptableObject, ITags { [SerializeField] private string fixedColor; [SerializeField] private string backgroundColor; [SerializeField] private string foregroundColor; [SerializeField] private string accentedColor; public string FixedColor => fixedColor; public string BackgroundColor => backgroundColor; public string ForegroundColor => foregroundColor; public string AccentedColor => accentedColor; private void OnEnable() { fixedColor.AssertNotEmpty(nameof(fixedColor)); backgroundColor.AssertNotEmpty(nameof(backgroundColor)); foregroundColor.AssertNotEmpty(nameof(foregroundColor)); accentedColor.AssertNotEmpty(nameof(accentedColor)); } }
للتكوين ، مثبت منفصل (رمز مختصر): CreateAssetMenu(fileName = nameof(ConfigurationInstaller), menuName = "Installers/" + nameof(ConfigurationInstaller))] public class ConfigurationInstaller : ScriptableObjectInstaller<ConfigurationInstaller> { [SerializeField] private EditorElementsPrefabs editorElementsPrefabs; [SerializeField] private LevelCompletionSteps levelCompletionSteps; [SerializeField] private CommonValues commonValues; [SerializeField] private AdsConfiguration adsConfiguration; [SerializeField] private CutscenesConfiguration cutscenesConfiguration; [SerializeField] private Colors colors; [SerializeField] private Tags tags; public override void InstallBindings() { Container.Bind<IEditorElementsPrefabs>().FromInstance(editorElementsPrefabs).AsSingle(); Container.Bind<ILevelCompletionSteps>().FromInstance(levelCompletionSteps).AsSingle(); Container.Bind<ICommonValues>().FromInstance(commonValues).AsSingle(); Container.Bind<IAdsConfiguration>().FromInstance(adsConfiguration).AsSingle(); Container.Bind<ICutscenesConfiguration>().FromInstance(cutscenesConfiguration).AsSingle(); Container.Bind<IColors>().FromInstance(colors).AsSingle(); Container.Bind<ITags>().FromInstance(tags).AsSingle(); } private void OnEnable() { editorElementsPrefabs.AssertNotNull(); levelCompletionSteps.AssertNotNull(); commonValues.AssertNotNull(); adsConfiguration.AssertNotNull(); cutscenesConfiguration.AssertNotNull(); colors.AssertNOTNull(); tags.AssertNotNull(); } }
العناصر الكهربائية
المحتوياتالآن أنت بحاجة إلى تخيل بطريقة ما العناصر الكهربائية public interface IElectricalElementMb { GameObject GameObject { get; } string Name { get; set; } IElectricalElement Element { get; set; } IOutputConnectorMb[] OutputConnectorsMb { get; } IInputConnectorMb[] InputConnectorsMb { get; } Transform Transform { get; } void SetInputConnectorsMb(InputConnectorMb[] inputConnectorsMb); void SetOutputConnectorsMb(OutputConnectorMb[] outputConnectorsMb); } [DisallowMultipleComponent] public class ElectricalElementMb : MonoBehaviour, IElectricalElementMb { [SerializeField] private OutputConnectorMb[] outputConnectorsMb; [SerializeField] private InputConnectorMb[] inputConnectorsMb; public Transform Transform => transform; public GameObject GameObject => gameObject; public string Name { get => name; set => name = value; } public virtual IElectricalElement Element { get; set; } public IOutputConnectorMb[] OutputConnectorsMb => outputConnectorsMb; public IInputConnectorMb[] InputConnectorsMb => inputConnectorsMb; }
public interface IInputConnectorMb : IConnectorMb { IOutputConnectorMb OutputConnectorMb { get; set; } IInputConnector InputConnector { get; } }
public class InputConnectorMb : MonoBehaviour, IInputConnectorMb { [SerializeField] private OutputConnectorMb outputConnectorMb; public Transform Transform => transform; public IOutputConnectorMb OutputConnectorMb { get => outputConnectorMb; set => outputConnectorMb = (OutputConnectorMb) value; } public IInputConnector InputConnector { get; } = new InputConnector(); #if UNITY_EDITOR private void OnDrawGizmos() { if (outputConnectorMb != null) { Handles.DrawLine(transform.position, outputConnectorMb.Transform.position); } } #endif }
لدينا السطر العام IElectricalElement Element {get؛ مجموعة؛ }
هنا فقط هو كيفية تثبيت هذا البند؟قد يكون الخيار الجيد هو جعلGeneralE: الفئة العامة ElectricalElementMb: MonoBehaviour، IElectricalElementMb حيث T: IElectricalElementلكن المهم هو أن Unity لا تدعم عام في فصول MonoBehavior. علاوة على ذلك ، لا تدعم الوحدة تسلسل الخصائص والواجهات.ومع ذلك ، في وقت التشغيل ، من الممكن تمامًا المرور في IElectricalElement Element {get؛ مجموعة؛ }
القيمة المطلوبة.أنا جعلت التعداد ElectricalElementType الذي سيكون هناك جميع الأنواع اللازمة. يتم تعداد التعداد بشكل جيد بواسطة Unity وعرضه بشكل جيد في المفتش كقائمة منسدلة. حدد نوعين من العناصر: التي يتم إنشاؤها في وقت التشغيل والتي يتم إنشاؤها في المحرر ويمكن حفظها. وبالتالي ، هناك IElectricalElementMb و IElectricalElementMbEditor ، والذي يحتوي أيضًا على حقل من النوع ElectricalElementType.يحتاج النوع الثاني أيضًا إلى التهيئة في وقت التشغيل. للقيام بذلك ، هناك فئة في البداية ستتجاوز جميع العناصر وتهيئتها حسب النوع الموجود في حقل التعداد. على النحو التالي: private static readonly Dictionary<ElectricalElementType, Func<IElectricalElement>> ElementByType = new Dictionary<ElectricalElementType, Func<IElectricalElement>> { {ElectricalElementType.And, () => new And()}, {ElectricalElementType.Or, () => new Or()}, {ElectricalElementType.Xor, () => new Xor()}, {ElectricalElementType.Nand, () => new Nand()}, {ElectricalElementType.Nor, () => new Nor()}, {ElectricalElementType.NOT, () => new NOT()}, {ElectricalElementType.Xnor, () => new Xnor()}, {ElectricalElementType.Source, () => new Source()}, {ElectricalElementType.Conductor, () => new Conductor()}, {ElectricalElementType.Placeholder, () => new AlwaysFalse()}, {ElectricalElementType.Encoder, () => new Encoder()}, {ElectricalElementType.Decoder, () => new Decoder()} };
إدارة اللعبة
المحتوىبعد ذلك ، يطرح السؤال ، أين نضع منطق اللعبة نفسها (التحقق من شروط المرور ، عد قراءات المقطع ومساعدة اللاعب)؟لهذا ، أقوم بتمييز فئات معينة من المديرين المسؤولين عن فئة معينة من المهام.DataManager هي المسؤولة عن تخزين البيانات من نتائج تمرير إعدادات المستخدم واللعبة. تم تسجيله بواسطة AsSingle في سياق المشروع. هذا يعني أنه واحد للتطبيق بأكمله. أثناء تشغيل التطبيق ، يتم تخزين البيانات مباشرة في الذاكرة ، داخل DataManager.انه يستخدم IFileStoreService ، وهو المسؤول عن تحميل وحفظ البيانات و IFileSerializerمسؤولة عن إجراء تسلسل للملفات في نموذج جاهز للحفظ.LevelGameManager هو مدير اللعبة في مشهد واحد.حصلت على GodObject قليلاً ، لأنه لا يزال مسؤولاً عن واجهة المستخدم ، أي فتح وإغلاق القائمة ، رد فعل على الأزرار. لكن هذا أمر مقبول ، نظرًا لحجم المشروع وعدم وجود حاجة لتوسيعه ، لذا تسلسل الإجراءات أسهل وأكثر وضوحًا.هناك خياران. هذا هو ما يسمى LevelGameManager1 و LevelGameManager2 للوضع 1 و 2 ، على التوالي.في الحالة الأولى ، يعتمد المنطق على رد الفعل لحدث تغيير في القيمة في أحد المصادر والتحقق من القيمة عند إخراج الدائرة.في الحالة الثانية ، يستجيب المنطق لحدث تغيير العنصر ويتحقق أيضًا من القيم عند إخراج الدائرة.هناك بعض معلومات المستوى الحالي مثل رقم المستوى ومساعدة اللاعب.يتم تخزين البيانات حول المستوى الحالي في CurrentLevelData . يتم تخزين رقم المستوى هناك - خاصية Boolean مع التحقق من المساعدة ، وعلم العرض لتقييم اللعبة والبيانات لمساعدة اللاعب. public interface ICurrentLevelData { int LevelNumber { get; } bool HelpExist { get; } bool ProposeRate { get; } } public interface ICurrentLevelDataMode1 : ICurrentLevelData { IEnumerable<SourcePositionValueHelp> PartialHelp { get; } } public interface ICurrentLevelDataMode2 : ICurrentLevelData { IEnumerable<PlaceTypeHelp> PartialHelp { get; } }
. , .
, . , Unity .
,
LevelGameManager ICurrentLevelData .
بشكل عام ، لدي نهج يحركه الحدث لتوصيل العناصر. من ناحية ، هو منطقي ومريح. من ناحية أخرى ، هناك فرصة للحصول على مشاكل دون إلغاء الاشتراك عند الضرورة. ومع ذلك ، لم تكن هناك مشاكل في هذا المشروع ، والحجم ليس كبيرًا جدًا. عادةً ما يحدث الاشتراك خلال بداية المشهد لكل ما تحتاجه. لا يتم إنشاء أي شيء تقريبًا في وقت التشغيل ، لذلك لا يوجد أي تشويش.مستوى التحميل
المحتوىيتم تمثيل كل مستوى في اللعبة بمشهد Unity ، ويجب أن يحتوي على بادئة مستوى ورقم ، على سبيل المثال ، "Level23". يتم تضمين البادئة في التكوين. يحدث تحميل المستوى بالاسم ، والذي يتكون من البادئة. وبالتالي ، يمكن تحميل الفئة LevelsManager المستويات حسب العدد.مشهد
محتوياتcutscene هي مشاهد الوحدة العادية مع أرقام في العنوان ، على غرار المستويات.يتم تنفيذ الرسوم المتحركة نفسها باستخدام الجدول الزمني. لسوء الحظ ، ليس لدي مهارات في الرسوم المتحركة ولا القدرة على العمل مع الجدول الزمني ، لذلك "لا تطلقوا النار على عازف البيانو - إنه يلعب ما يستطيع"
تحولت الحقيقة إلى أن مشهد واحد منطقي يجب أن يتكون من مشاهد مختلفة بأشياء مختلفة. اتضح أن هذا قد لوحظ متأخرا بعض الشيء ، ولكن تقرر ببساطة: عن طريق وضع أجزاء من القطع على المسرح في أماكن مختلفة وتحريك الكاميرا على الفور.
اللعب إضافية
المحتوىيتم تقييم اللعبة من خلال عدد الإجراءات لكل مستوى واستخدام القرائن. العمل أقل كان ذلك أفضل. يؤدي استخدام تلميح الأدوات إلى تقليل الحد الأقصى للتصنيف إلى نجمتين ، وتخطي المستوى إلى نجمة واحدة. لتقييم المقطع ، يتم تخزين عدد خطوات المرور. يتكون من قيمتين: القيمة الدنيا (لمدة 3 نجوم) والحد الأقصى (نجمة واحدة).لا يتم تخزين عدد خطوات تمرير المستويات في ملف المشهد نفسه ، ولكن في ملف التكوين ، حيث تحتاج إلى عرض عدد النجوم لمستوى النجاح. هذا معقد قليلا عملية إنشاء المستويات. كان من المثير للاهتمام بشكل خاص رؤية التغييرات في نظام التحكم في الإصدار:
. , Unity, .
— , . , .
, . 1 .
, , .
تسييل
المحتوىهناك نوعان من تحقيق الدخل في اللعبة: عرض الإعلانات وتعطيل الإعلانات مقابل المال. يتضمن عرض الإعلانات عرض الإعلانات بين المستويات وعرض الإعلانات المكافأة لتخطي المستوى.إذا كان اللاعب مستعدًا لدفع تكاليف تعطيل الإعلانات ، فيمكنه القيام بذلك. في هذه الحالة ، لن يتم عرض الإعلانات بين المستويات وعند تخطي المستوى.للإعلان ، تم إنشاء فئة تسمى AdsService ، مع واجهة public interface IAdsService { bool AdsDisabled { get; } void LoadBetweenLevelAd(); bool ShowBetweenLevelAd(int level, bool force = false); void LoadHelpAd(Action onLoaded = null); void ShowHelpAd(Action onRewarded, Action onClosed); bool HelpAdLoaded { get; } }
هنا HelpAd هو إعلان مكافأة لتخطي المستوى. في البداية ، أطلقنا على المساعدة المساعدة الجزئية والكاملة. جزئي هو تلميح ، والكامل هو مستوى تخطي.تحتوي هذه الفئة داخل الحد من تكرار عرض الإعلانات حسب الوقت ، بعد الإطلاق الأول للعبة.يستخدم التطبيق Google Mobile Ads Unity Plugin .من خلال الإعلانات المكافأة ، صعدت إلى أشعل النار - اتضح أنه يمكن استدعاء المندوبين المخلصين في خيط آخر ، وليس من الواضح سبب ذلك. لذلك ، من الأفضل ألا يقوم هؤلاء المفوضون باستدعاء أي شيء في التعليمات البرمجية المتعلقة بالوحدة. إذا تم إجراء عملية شراء لتعطيل الإعلان ، فلن يتم عرض الإعلان وسيقوم المفوض بتنفيذ العرض الناجح للإعلان على الفور.هناك واجهة للتسوق public interface IPurchaseService { bool IsAdsDisablePurchased { get; } event Action DisableAdsPurchased; void BuyDisableAds(); void RemoveDisableAd(); }
يستخدم Unity IAP في التنفيذ، وهناك خدعة لشراء قطع اتصال الإعلان. لا يبدو أن Google Play يوفر معلومات قام المشترى بشراءها. فقط تأكيد سيأتي أنها مرت مرة واحدة. ولكن إذا وضعت حالة المنتج بعد الشراء غير مكتمل ولكن معلق ، فسيسمح لك ذلك بالتحقق من خاصية منتج hasReceipt . إذا كان هذا صحيحًا ، فقد اكتمل الشراء.على الرغم من أنه يخلط بالطبع مثل هذا النهج ، إلا أنني أظن أنه قد لا يكون سلسًا.هناك حاجة إلى طريقة RemoveDisableAd في وقت الاختبار ، فهي تزيل انقطاع الإعلانات المشتراة.واجهة المستخدم
المحتوى تعملجميع عناصر الواجهة وفقًا لمنهج موجه نحو الحدث. لا تحتوي عناصر الواجهة نفسها عادة على منطق بخلاف الأحداث التي تستدعيها الأساليب العامة التي يمكن أن تستخدمها الوحدة. على الرغم من أنه يحدث أيضًا أداء بعض الواجبات المتعلقة فقط بالواجهة. public abstract class UiElementBase : MonoBehaviour, IUiElement { public event Action ShowClick; public event Action HideCLick; public void Show() { gameObject.SetActive(true); ShowClick?.Invoke(); } public void Hide() { gameObject.SetActive(false); HideCLick?.Invoke(); } } public class PauseMenu : UiElementEscapeClose, IPauseMenu { [SerializeField] private Text levelNumberText; [SerializeField] private LocalizedText finishedText; [SerializeField] private GameObject restartButton; private int levelNumber; public event Action GoToMainMenuClick; public event Action RestartClick; public int LevelNumber { set => levelNumberText.text = $"{finishedText.Value} {value}"; } public void DisableRestartButton() { restartButton.SetActive(false); } public void GoToMainMenu() { GoToMainMenuClick?.Invoke(); } public void Restart() { RestartClick?.Invoke(); } }
في الواقع ، هذا ليس هو الحال دائما. من الجيد ترك هذه العناصر كطريقة عرض نشطة ، وإنشاء مستمع للحدث منها ، مثل جهاز التحكم الذي سيؤدي إلى اتخاذ الإجراءات اللازمة على المديرين.تحليلات
المحتوىعلى طريق الأقل مقاومة ، تم اختيار تحليلات الوحدة . سهل التنفيذ ، على الرغم من محدودية الاشتراك المجاني - من المستحيل تصدير البيانات المصدر. يوجد أيضًا حد لعدد الأحداث - 100 / ساعة لكل لاعب.بالنسبة للتحليلات ، تم إنشاء فئة المجمّع AnalyticsService . يحتوي على طرق لكل نوع من الأحداث ، ويتلقى المعلمات اللازمة ويؤدي إلى إرسال الحدث باستخدام الأدوات المدمجة في Unity. من المؤكد أن إنشاء طريقة لكل حدث ليس أفضل ممارسة ككل ، ولكن في مشروع صغير عن قصد ، يكون أفضل من القيام بشيء كبير ومعقد.جميع الأحداث المستخدمة هي CustomEvent.. وهي مبنية من اسم الحدث واسم المعلمة القاموس والقيمة. يحصل AnalyticsService على القيم المطلوبة من المعلمات ويقوم بإنشاء قاموس من الداخل.يتم وضع جميع أسماء الأحداث والمعلمات في الثوابت. ليس في شكل نهج تقليدي مع ScriptableObject ، حيث يجب ألا تتغير هذه القيم أبدًا.مثال على الطريقة: public void LevelComplete(int number, int stars, int actionCount, TimeSpan timeSpent, int levelMode) { CustomEvent(LevelCompleteEventName, new Dictionary<string, object> { {LevelNumber, number}, {LevelStars, stars}, {LevelActionCount, actionCount}, {LevelTimeSpent, timeSpent}, {LevelMode, levelMode} }); }
الكاميرا لتحديد المواقع والرسوم البيانية
المحتوياتالمهمة هي وضع FinalDevice في أعلى الشاشة ، على نفس المسافة من الحد العلوي ، كما أن المصادر من أسفل دائمًا على مسافة متساوية من الحد السفلي. بالإضافة إلى ذلك ، تأتي الشاشات بنسب أبعاد مختلفة ، تحتاج إلى ضبط حجم الكاميرا قبل بدء المستوى بحيث يناسب الدائرة بشكل صحيح.للقيام بذلك ، يتم إنشاء فئة CameraAlign . خوارزمية الحجم:- العثور على جميع العناصر اللازمة على المسرح
- ابحث عن الحد الأدنى للعرض والارتفاع استنادًا إلى نسبة العرض إلى الارتفاع
- تحديد حجم الكاميرا
- اضبط الكاميرا في الوسط
- انقل FinalDevice إلى أعلى الشاشة
- انقل المصادر إلى أسفل الشاشة
public class CameraAlign : ICameraAlign { private readonly ISceneObjectsHelper sceneObjectsHelper; private readonly ICommonValues commonValues; public CameraAlign(ISceneObjectsHelper sceneObjectsHelper, ICommonValues commonValues) { this.sceneObjectsHelper = sceneObjectsHelper; this.commonValues = commonValues; } public void Align(Camera camera) { var elements = sceneObjectsHelper.FindObjectsOfType<IElectricalElementMb>(); var finalDevice = sceneObjectsHelper.FindObjectOfType<IFinalDevice>(); var sources = elements.OfType<ISourceMb>().ToArray(); if (finalDevice != null && sources.Length > 0) { float leftPos = elements.Min(s => s.Transform.position.x); float rightPos = elements.Max(s => s.Transform.position.x); float width = Mathf.Abs(leftPos - rightPos); var fPos = finalDevice.Transform.position; float height = Mathf.Abs(sources.First().Transform.position.y - fPos.y) * camera.aspect; float size = Mathf.Max(width * commonValues.CameraOffset, height * commonValues.CameraOffset); camera.orthographicSize = Mathf.Clamp(size, commonValues.MinCameraSize, float.MaxValue); camera.transform.position = GetCenterPoint(elements, -1); fPos = new Vector2(fPos.x, camera.ScreenToWorldPoint(new Vector2(Screen.width, Screen.height)).y - commonValues.FinalDeviceTopOffset * camera.orthographicSize); finalDevice.Transform.position = fPos; float sourceY = camera.ScreenToWorldPoint(Vector2.zero).y + commonValues.SourcesBottomOffset; foreach (var item in sources) { item.Transform.position = new Vector2(item.Transform.position.x, sourceY); } } else { Debug.Log($"{nameof(CameraAlign)}: No final device or no sources in scene"); } } private static Vector3 GetCenterPoint(ICollection<IElectricalElementMb> elements, float z) { float top = elements.Max(e => e.Transform.position.y); float bottom = elements.Min(e => e.Transform.position.y); float left = elements.Min(e => e.Transform.position.x); float right = elements.Max(e => e.Transform.position.x); float x = left + ((right - left) / 2); float y = bottom + ((top - bottom) / 2); return new Vector3(x, y, z); } }
تسمى هذه الطريقة عندما يبدأ المشهد في فئة برنامج الالتفاف.مخططات الألوان
المحتوىنظرًا لأن اللعبة ستتمتع بواجهة بدائية للغاية ، فقد قررت أن أجعلها مزودة بنسقَي ألوان ، أبيض وأسود.للقيام بذلك ، إنشاء واجهة public interface IColors { Color ColorAccent { get; } Color Background { get; set; } Color Foreground { get; set; } event Action ColorsChanged; }
Unity . .
Background Foreground, .
, . , .
:
CameraColorAdjustment — ,
UiColorAdjustment —
TextMeshColorAdjustment- يحدد لون الأرقام على المصادر. يستخدم UiColorAdjustment أيضًا العلامات. في المحرر ، يمكنك تعليم كل عنصر بعلامة تشير إلى نوع اللون الذي يجب تعيينه له (الخلفية ، المقدمة ، الأكسنتول واللون الثابت). يتم تعيين كل هذا في بداية المشهد أو حدث تغيير في نظام الألوان.النتيجة:


ملحقات محرر
, . Unity - EditorWindow. UiElements, , .
, - UnityEditor , , . :
- Assets/Editor
- #if UNITY_EDITOR
يستخدم المشروع الطريقة الأولى وأحيانًا #if UNITY_EDITOR ، إذا لزم الأمر ، أضف جزءًا صغيرًا للمحرر إلى الفئة المطلوبة في الإنشاء.جميع الفئات المطلوبة فقط في المحرر الذي حددته في التجميع ، والتي ستكون متاحة فقط في المحرر. إنها لن تذهب إلى بناء اللعبة.
سيكون من الجيد الآن أن يكون لديك DI في ملحقات المحرر. لهذا أنا استخدم Zenject.StaticContext. من أجل تعيينها في المحرر ، يتم استخدام فئة مع السمة InitializeOnLoad ، حيث يوجد مُنشئ ثابت. [InitializeOnLoad] public class EditorInstaller { static EditorInstaller() { var container = StaticContext.Container; container.Bind<IElementsProvider>().To<ElementsProvider>().AsSingle(); container.Bind<ISolver>().To<Solver>().AsSingle(); .... } }
لتسجيل فئات ScriptableObject في سياق ثابت ، أستخدم الكود التالي: BindFirstScriptableObject<ISceneNameConfiguration, SceneNameConfiguration>(container); private static void BindFirstScriptableObject<TInterface, TImplementation>(DiContainer container) where TImplementation : ScriptableObject, TInterface { var obj = GetFirstScriptableObject<TImplementation>(); container.Bind<TInterface>().FromInstance(obj).AsSingle(); } private static T GetFirstScriptableObject<T>() where T : ScriptableObject { var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name); string path = AssetDatabase.GUIDToAssetPath(guids.First()); var obj = AssetDatabase.LoadAssetAtPath<T>(path); return obj; }
TImplementation مطلوب فقط لهذا السطر. AssetDatabase.LoadAssetAtPath (path)لا يمكن إضافة تبعية إلى المُنشئ. بدلاً من ذلك ، أضف السمة [Inject] إلى حقول التبعية في فئة النافذةواتصل بـ StaticContext.Container.Inject (هذا) عند بدء تشغيل النافذة ؛أوصي أيضًا بإضافة فحص دورة خالية من أحد الحقول التابعة إلى دورة تحديث النافذة ، وإذا كان الحقل فارغًا ، فقم بإجراء السطر أعلاه. لأنه بعد تغيير الكود في المشروع ، يمكن للوحدة إعادة إنشاء النافذة وعدم استدعاء الاستيقاظ عليها.مولد كهربائي
المحتويات:
العرض الأولي للمولد ،يجب أن توفر النافذة واجهة لإنشاء قائمة من المخططات مع المعلمات ، وعرض قائمة من المخططات ووضع المخطط المحدد على المشهد الحالي.تتكون النافذة من ثلاثة أقسام من اليسار إلى اليمين:- إعدادات الجيل
- قائمة الخيارات في شكل أزرار
- الخيار المحدد كنص
يتم إنشاء الأعمدة باستخدام EditorGUILayout.BeginVertical () و EditorGUILayout.EndVertical (). لسوء الحظ ، لم ينجح الأمر في تحديد الأحجام والحد منها ، لكن هذا ليس بالغ الأهمية.اتضح أن عملية التوليد على عدد كبير من الدوائر ليست بهذه السرعة. يتم الحصول على الكثير من المجموعات مع عناصر I. كما أوضح المحلل ، فإن أبطأ جزء هو الدائرة نفسها. التوازي مع ذلك ليس خيارًا ؛ كل الخيارات تستخدم مخططًا واحدًا ، لكن من الصعب استنساخ هذا الهيكل.ثم اعتقدت أنه من المحتمل أن تعمل كل أكواد ملحقات المحرر في وضع تصحيح الأخطاء. تحت الإصدار ، لا يعمل التصحيح بشكل جيد ، لا تتوقف نقاط التوقف ، يتم تخطي الخطوط ، إلخ. في الواقع ، بعد قياس الأداء ، اتضح أن سرعة المولد في الوحدة تتوافق مع مجموعة Debug التي تم إطلاقها من تطبيق وحدة التحكم ، والتي هي أبطأ من الإصدار بـ 6 مرات. ضع هذا في الاعتبار.
بدلاً من ذلك ، يمكنك القيام بالتجميع الخارجي والإضافة إلى DLL Unity مع التجميع ، لكن هذا يؤدي إلى تعقيد التجميع والتحرير للمشروع.جلبت على الفور عملية التوليد إلى مهمة منفصلة مع رمز يحتوي على هذا:circuitGenerator.Generate (خطوط ، maxElementsInLine ، availableLogicalElements ، useNOT ، تعديل) .ToList ()بالفعل أفضل ، لا يتعطل المحرر في وقت الإنشاء . ولكن لا يزال من الضروري الانتظار لفترة طويلة ، لعدة دقائق (أكثر من 20 دقيقة على دوائر كبيرة الحجم). بالإضافة إلى ذلك ، كانت هناك مشكلة في أن المهمة لا يمكن أن تكتمل بهذه السهولة وتستمر في العمل حتى اكتمال الجيل.الكثير من الكود internal static class Ext { public static IEnumerable<CircuitVariant> OrderVariants(this IEnumerable<CircuitVariant> circuitVariants) { return circuitVariants.OrderBy(a => a.Solutions.Count()) .ThenByDescending(a => a.Solutions .Select(b => b.Sum(i => i ? 1 : -1)) .OrderByDescending(b=>b) .First()); } } public interface IEditorGenerator : IDisposable { CircuitVariant[] FilteredVariants { get; } int LastPage { get; } void FilterVariants(int page); void Start(int lines, int maxElementsInLine, ICollection<int> availableGates, bool useNOT, StructureModification? modification, int maxSolutions); void Stop(); void Fetch(); } public class EditorGenerator : IEditorGenerator { private const int PageSize = 100; private readonly ICircuitGenerator circuitGenerator; private ConcurrentBag<CircuitVariant> variants; private List<CircuitVariant> sortedVariants; private Thread generatingThread; public EditorGenerator(ICircuitGenerator circuitGenerator) { this.circuitGenerator = circuitGenerator; } public void Dispose() { generatingThread?.Abort(); } public CircuitVariant[] FilteredVariants { get; private set; } public int LastPage { get; private set; } public void FilterVariants(int page) { CheckVariants(); if (sortedVariants == null) { Fetch(); } FilteredVariants = sortedVariants.Skip(page * PageSize) .Take(PageSize) .ToArray(); int count = sortedVariants.Count; LastPage = count % PageSize == 0 ? (count / PageSize) - 1 : count / PageSize; } public void Fetch() { CheckVariants(); sortedVariants = variants.OrderVariants() .ToList(); } public void Start(int lines, int maxElementsInLine, ICollection<int> availableGates, bool useNOT, StructureModification? modification, int maxSolutions) { if (generatingThread != null) { Stop(); } variants = new ConcurrentBag<CircuitVariant>(); generatingThread = new Thread(() => { var v = circuitGenerator.Generate(lines, maxElementsInLine, availableGates, useNOT, modification, maxSolutions); foreach (var item in v) { variants.Add(item); } }); generatingThread.Start(); } public void Stop() { generatingThread?.Abort(); sortedVariants = null; variants = null; generatingThread = null; FilteredVariants = null; } private void CheckVariants() { if (variants == null) { throw new InvalidOperationException("VariantsGeneration is not started. Use Start before."); } } ~EditorGenerator() { generatingThread.Abort(); } }
, , . . , . “”: , . 1 1 1 1 1 0 1 1.

, , . , .
تعد ميزة الوحدة مزعجة للغاية لأنه عند النقر فوق تشغيل ، تتم إعادة تعيين محتويات النافذة ، مثل كل البيانات التي تم إنشاؤها. إذا كانت قابلة للتسلسل بسهولة ، يمكن تخزينها كملفات. بهذه الطريقة ، يمكنك حتى تخزين نتائج الجيل في ذاكرة التخزين المؤقت. ولكن للأسف ، من الصعب إجراء تسلسل لبنية معقدة حيث تشير الأشياء إلى بعضها البعض.بالإضافة إلى ذلك ، أضفت خطوط لكل بوابة ، مثل if (Input.Length == 2) { return Input[0].Value && Input[1].Value; }
مما أدى إلى تحسن كبير في الأداء.حلالا
المحتوياتعندما تقوم بتجميع دائرة في المحرر ، يجب أن تكون قادرًا على أن تفهم بسرعة ما إذا كان قد تم حلها وعدد الحلول التي لديها. للقيام بذلك ، قمت بإنشاء نافذة "حلالا". يوفر حلولًا للمخطط الحالي في شكل نص ،
منطق "الواجهة الخلفية": public string GetSourcesLabel() { var sourcesMb = sceneObjectsHelper.FindObjectsOfType<SourceMb>().OrderBy(s => s.name); var sourcesLabelSb = new StringBuilder(); foreach (var item in sourcesMb) { sourcesLabelSb.Append($"{item.name.Replace("Source", "Src")}\t"); } return sourcesLabelSb.ToString(); } public IEnumerable<bool[]> FindSolutions() { var elementsMb = sceneObjectsHelper.FindObjectsOfType<IElectricalElementMbEditor>(); elementsConfigurator.Configure(elementsMb); var root = sceneObjectsHelper.FindObjectOfType<FinalDevice>(); if (root == null) { throw new InvalidOperationException("No final device in scene"); } var sourcesMb = sceneObjectsHelper.FindObjectsOfType<SourceMb>().OrderBy(s => s.name); var sources = sourcesMb.Select(mb => (Source) mb.Element).ToArray(); return solver.GetSolutions(root.Element, sources); }
مفيد
محتوىAssertHelper
المحتوياتللتحقق من أن القيم تم تعيينها في الأصول ، استخدم طرق الإضافة التي أطلبها في OnEnable public static class AssertHelper { public static void AssertType(this IElectricalElementMbEditor elementMbEditor, ElectricalElementType expectedType) { if (elementMbEditor.Type != expectedType) { Debug.LogError($"Field for {expectedType} require element with such type, but given element is {elementMbEditor.Type}"); } } public static void AssertNOTNull<T>(this T obj, string fieldName = "") { if (obj == null) { if (string.IsNullOrEmpty(fieldName)) { fieldName = $"of type {typeof(T).Name}"; } Debug.LogError($"Field {fieldName} is not installed"); } } public static string AssertNOTEmpty(this string str, string fieldName = "") { if (string.IsNullOrWhiteSpace(str)) { Debug.LogError($"Field {fieldName} is not installed"); } return str; } public static string AssertSceneCanBeLoaded(this string name) { if (!Application.CanStreamedLevelBeLoaded(name)) { Debug.LogError($"Scene {name} can't be loaded."); } return name; } }
قد تفشل في بعض الأحيان التحقق من قدرة المشهد على التحميل ، رغم أنه قد يتم تحميل المشهد. ربما هذا خطأ في الوحدة.أمثلة على الاستخدام: mainMenuSceneName.AssertNOTEmpty(nameof(mainMenuSceneName)).AssertSceneCanBeLoaded(); levelNamePrefix.AssertNOTEmpty(nameof(levelNamePrefix)); editorElementsPrefabs.AssertNOTNull(); not.AssertType(ElectricalElementType.NOT);
SceneObjectsHelper
المحتوياتللعمل مع عناصر المشهد ، كانت الفئة SceneObjectsHelper مفيدة أيضًا:الكثير من الكود namespace Circuit.Game.Utility { public interface ISceneObjectsHelper { T[] FindObjectsOfType<T>(bool includeDisabled = false) where T : class; T FindObjectOfType<T>(bool includeDisabled = false) where T : class; T Instantiate<T>(T prefab) where T : Object; void DestroyObjectsOfType<T>(bool includeDisabled = false, bool immediate = false) where T : class; void Destroy<T>(T obj, bool immediate = false) where T : Object; void DestroyAllChildren(Transform transform); void Inject(object obj); T GetComponent<T>(GameObject obj) where T : class; } public class SceneObjectsHelper : ISceneObjectsHelper { private readonly DiContainer diContainer; public SceneObjectsHelper(DiContainer diContainer) { this.diContainer = diContainer; } public T GetComponent<T>(GameObject obj) where T : class { return obj.GetComponents<Component>().OfType<T>().FirstOrDefault(); } public T[] FindObjectsOfType<T>(bool includeDisabled = false) where T : class { if (includeDisabled) { return Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().ToArray(); } return Object.FindObjectsOfType<Component>().OfType<T>().ToArray(); } public void DestroyObjectsOfType<T>(bool includeDisabled = false, bool immediate = false) where T : class { var objects = includeDisabled ? Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().ToArray() : Object.FindObjectsOfType<Component>().OfType<T>().ToArray(); foreach (var item in objects) { if (immediate) { Object.DestroyImmediate((item as Component)?.gameObject); } else { Object.Destroy((item as Component)?.gameObject); } } } public void Destroy<T>(T obj, bool immediate = false) where T : Object { if (immediate) { Object.DestroyImmediate(obj); } else { Object.Destroy(obj); } } public void DestroyAllChildren(Transform transform) { int childCount = transform.childCount; for (int i = 0; i < childCount; i++) { Destroy(transform.GetChild(i).gameObject); } } public T FindObjectOfType<T>(bool includeDisabled = false) where T : class { if (includeDisabled) { return Resources.FindObjectsOfTypeAll(typeof(Object)).OfType<T>().FirstOrDefault(); } return Object.FindObjectsOfType<Component>().OfType<T>().FirstOrDefault(); } public void Inject(object obj) { diContainer.Inject(obj); } public T Instantiate<T>(T prefab) where T : Object { var obj = Object.Instantiate(prefab); if (obj is Component) { var components = ((Component) (object) obj).gameObject.GetComponents<Component>(); foreach (var component in components) { Inject(component); } } else { Inject(obj); } return obj; } } }
هنا ، قد لا تكون بعض الأشياء فعالة للغاية عندما تكون هناك حاجة إلى أداء عالٍ ، ولكن نادراً ما يتم استدعاؤها بالنسبة لي ولا تخلق أي تأثير. لكنهم يسمحون لك بالعثور على كائنات عن طريق الواجهة ، على سبيل المثال ، تبدو جميلة.CoroutineStarter
محتوياتإطلاق Coroutine يمكن MonoBehaviour فقط. لذلك قمت بإنشاء فئة CoroutineStarter وسجلتها في سياق المشهد. public interface ICoroutineStarter { void BeginCoroutine(IEnumerator routine); } public class CoroutineStarter : MonoBehaviour, ICoroutineStarter { public void BeginCoroutine(IEnumerator routine) { StartCoroutine(routine); } }
بالإضافة إلى الراحة ، سهّل إدخال هذه الأدوات الاختبار التلقائي. على سبيل المثال ، تنفيذ coroutine في الاختبارات: coroutineStarter.When(x => x.BeginCoroutine(Arg.Any<IEnumerator>())).Do(info => { var a = (IEnumerator) info[0]; while (a.MoveNext()) { } });
الأداة
المحتوياتلراحة عرض العناصر غير المرئية ، أوصي باستخدام صور gizmo مرئية فقط في المشهد. أنها تجعل من السهل تحديد عنصر غير مرئي بنقرة واحدة. أيضا اتصالات من العناصر في شكل خطوط: private void OnDrawGizmos() { if (outputConnectorMb != null) { Handles.DrawLine(transform.position, outputConnectorMb.Transform.position); } }

تجريب
المحتوىأردت الحصول على أقصى استفادة من الاختبار التلقائي ، لأنه تم استخدام الاختبارات حيثما كان ذلك ممكنًا وسهل الاستخدام.بالنسبة لاختبارات الوحدة ، من المعتاد استخدام كائنات وهمية بدلاً من الفئات التي تنفذ الواجهة التي تعتمد عليها فئة الاختبار. لهذا ، استخدمت مكتبة NSubstitute . ما هو مسرور جدا.الوحدة لا تدعم NuGet ، لذلك اضطررت إلى الحصول على DLL بشكل منفصل ، ثم التجميع ، حيث تتم إضافة تبعية إلى ملف AssemblyDefinition ويتم استخدامها دون مشاكل.
للاختبار التلقائي ، تقدم Unity TestRunner ، والذي يعمل مع إطار اختبار NUnit المشهور جدًا . من وجهة نظر TestRunner ، هناك نوعان من الاختبارات:- EditMode — , . Nunit . , . GameObject Monobehaviour . , EditMode .
- PlayMode — .
EditMode. في تجربتي ، كان هناك العديد من الإزعاج والسلوك الغريب في هذا الوضع. ومع ذلك ، فهي مريحة للتحقق تلقائيا من صحة التطبيق ككل. كما أنها توفر التحقق الصادق للرمز في أساليب مثل البدء والتحديث وما شابه ذلك.يمكن وصف اختبارات PlayMode بأنها اختبارات NUnit عادية ، ولكن يوجد بديل. في PlayMode ، قد تحتاج إلى الانتظار لبعض الوقت أو عدد معين من الإطارات. للقيام بذلك ، يجب أن توصف الاختبارات بطريقة مماثلة ل Coroutine. يجب أن تكون القيمة التي تم إرجاعها IEnumerator / IEnumerable وداخلها ، لتخطي الوقت ، يجب عليك استخدام ، على سبيل المثال: yield return null;
أو
yield return new WaitForSeconds(1);
هناك قيم الإرجاع الأخرى.يحتاج مثل هذا الاختبار إلى تعيين سمة UnityTest . هناك أيضًاسمات UnitySetUp و UnityTearDown تحتاج إلى استخدام نهج مماثل.أنا ، بدوره ، مشاركة اختبارات EditMode للوحدات النمطية والتكامل.اختبار وحدة اختبار فئة واحدة فقط في عزلة تامة عن الفصول الأخرى. غالبًا ما تسهل مثل هذه الاختبارات إعداد البيئة للفئة المختبرة ، وتتيح لك الأخطاء ، عند اجتيازها ، تحديد مكان المشكلة بدقة أكبر.في اختبارات الوحدة ، أختبر العديد من الفصول الأساسية والفصول المطلوبة مباشرة في اللعبة.اختبارات عناصر الدائرة متشابهة جدا ، لذلك قمت بإنشاء فئة أساسية public class ElectricalElementTestsBase<TElement> where TElement : ElectricalElementBase, IElectricalElement, new() { protected TElement element; protected IInputConnector mInput1; protected IInputConnector mInput2; protected IInputConnector mInput3; protected IInputConnector mInput4; [OneTimeSetUp] public void Setup() { element = new TElement(); mInput1 = Substitute.For<IInputConnector>(); mInput2 = Substitute.For<IInputConnector>(); mInput3 = Substitute.For<IInputConnector>(); mInput4 = Substitute.For<IInputConnector>(); } protected void GetValue_3Input(bool input1, bool input2, bool input3, bool expectedOutput) {
تبدو اختبارات العناصر الإضافية كما يلي: public class AndTests : ElectricalElementTestsBase<And> { [TestCase(false, false, false)] [TestCase(false, true, false)] [TestCase(true, false, false)] [TestCase(true, true, true)] public new void GetValue_2Input(bool input1, bool input2, bool output) { base.GetValue_2Input(input1, input2, output); } [TestCase(false, false)] [TestCase(true, true)] public new void GetValue_1Input(bool input, bool expectedOutput) { base.GetValue_1Input(input, expectedOutput); } }
ربما يكون هذا تعقيدًا من حيث سهولة الفهم ، وهو أمر غير ضروري عادة في الاختبارات ، لكنني لم أرغب في نسخ اللصق بنفس الشيء 11 مرة.هناك أيضا اختبارات GameManagers. نظرًا لأن لديهم الكثير من القواسم المشتركة ، فقد حصلوا أيضًا على فئة أساسية من الاختبارات. يجب أن يكون لدى مديري الألعاب في كلا الوضعين بعض الوظائف المتماثلة وبعضها مختلف. يتم اختبار الأشياء العامة باستخدام نفس الاختبارات لكل خليفة ويتم اختبار السلوك المحدد بالإضافة إلى ذلك. على الرغم من نهج الحدث ، لم يكن من الصعب اختبار السلوك الذي قام به الحدث: [Test] public void FullHelpAgree_FinishLevel() {
في اختبارات التكامل ، اختبرت أيضًا فصول المحرر ، وأخذتها من السياق الثابت لحاوية DI. وبالتالي التحقق بما في ذلك الحقن الصحيح ، والذي لا يقل أهمية عن اختبار الوحدة. public class PlacerTests { [Inject] private ICircuitEditorPlacer circuitEditorPlacer; [Inject] private ICircuitGenerator circuitGenerator; [Inject] private IEditorSolver solver; [Inject] private ISceneObjectsHelper sceneObjectsHelper; [TearDown] public void TearDown() { sceneObjectsHelper.DestroyObjectsOfType<IElectricalElementMb>(immediate: true); } [OneTimeSetUp] public void Setup() { var container = StaticContext.Container; container.Inject(this); } [TestCase(1, 2)] [TestCase(2, 2)] [TestCase(3, 4)] public void PlaceSolve_And_NoModifications_AllVariantsSolved(int lines, int elementsInLine) { var variants = circuitGenerator.Generate(lines, elementsInLine, new List<int> {0}, false); foreach (var variant in variants) { circuitEditorPlacer.PlaceCircuit(variant); var solutions = solver.FindSolutions(); CollectionAssert.IsNOTEmpty(solutions); } } [TestCase(1, 2, StructureModification.Branching)] [TestCase(1, 2, StructureModification.ThroughLayer)] [TestCase(1, 2, StructureModification.All)] [TestCase(2, 2, StructureModification.Branching)] [TestCase(2, 2, StructureModification.ThroughLayer)] [TestCase(2, 2, StructureModification.All)] public void PlaceSolve_And_Modifications_AllVariantsSolved(int lines, int elementsInLine, StructureModification modification) { var variants = circuitGenerator.Generate(lines, elementsInLine, new List<int> {0}, false, modification); foreach (var variant in variants) { circuitEditorPlacer.PlaceCircuit(variant); var solutions = solver.FindSolutions(); CollectionAssert.IsNOTEmpty(solutions); } }
يستخدم هذا الاختبار تطبيقات حقيقية لجميع التبعيات وكذلك يعين الكائنات على المسرح ، وهو أمر ممكن تمامًا في اختبارات EditMode. صحيح أن الاختبار جعلها عاقلة - لدي فكرة قليلة عن كيفية ذلك ، لذلك أتحقق من أن الدائرة المنشورة لديها حلول.في التكامل ، هناك أيضًا اختبارات لـ CircuitGenerator (StructureGenerator + VariantsGenerator) و Solver public class CircuitGeneratorTests { private ICircuitGenerator circuitGenerator; private ISolver solver; [SetUp] public void Setup() { solver = new Solver(); var gates = new List<Func<IElectricalElement>> { () => new And(), () => new Or(), () => new Xor() }; var conductors = new List<Func<IElectricalElement>> { () => new Conductor(), () => new Not() }; var elements = Substitute.For<IElementsProvider>(); elements.Conductors.Returns(conductors); elements.Gates.Returns(gates); var structGenerator = new StructureGenerator(); var variantsGenerator = new VariantsGenerator(solver, elements); circuitGenerator = new CircuitGenerator(structGenerator, variantsGenerator); } [Test] public void Generate_2l_2max_ReturnsVariants() {
تستخدم اختبارات PlayMode كاختبارات للنظام. انهم يتحققون الجاهزة ، حقن ، الخ من الخيارات الجيدة استخدام المشاهد الجاهزة حيث يتم فقط تحميل الاختبار وينتج عنه بعض التفاعلات. لكنني أستخدم مشهدًا فارغًا جاهزًا للاختبار ، حيث تختلف البيئة عما سيكون عليه في اللعبة. كانت هناك محاولة لاستخدام PlayMode لاختبار عملية اللعبة بأكملها ، مثل إدخال القائمة وإدخال المستوى وما إلى ذلك ، ولكن تبين أن عمل هذه الاختبارات غير مستقر ، لذلك تقرر تأجيله إلى وقت لاحق (أبدًا).من المناسب استخدام أدوات تقييم التغطية لكتابة الاختبارات ، لكن للأسف لم أجد أي حلول تعمل مع Unity.لقد وجدت مشكلة أنه مع ترقية Unity إلى 2018.3 ، بدأت الاختبارات تعمل بشكل أبطأ ، حتى 10 مرات أبطأ (في مثال اصطناعي). يحتوي المشروع على 288 EditMode الاختبارات التي تعمل لمدة 11 ثانية ، على الرغم من عدم القيام بأي شيء هناك لفترة طويلة.ملخص التنمية
محتوى
لقطة من مستوى اللعبةيمكن صياغة منطق بعض الألعاب بغض النظر عن النظام الأساسي. في مرحلة مبكرة ، يمنح هذا سهولة التطوير وقابلية الاختبار بواسطة الاختبارات الذاتية.DI مريحة. حتى مع الأخذ في الاعتبار حقيقة أن Unity لا تملكها أصليًا ، فإن الشد على الجانب يعمل بشكل محتمل.الوحدة تسمح لك باختبار المشروع تلقائيًا. صحيح ، نظرًا لأن جميع مكونات GameObject المضمنة لا تحتوي على واجهات ولا يمكن استخدامها إلا مباشرة للسخرية من أشياء مثل Collider و SpriteRenderer و MeshRenderer ، إلخ. لن تنجح. على الرغم من أن GetComponent يسمح لك بالحصول على مكونات على الواجهة. كخيار ، اكتب الأغلفة الخاصة بك لكل شيء., . ., , / . DI, , scriptable objects , , , Zenject, , .
الوحدة تولد قدرا كبيرا من الأخطاء والحوادث. غالبًا ما يتم حل الأخطاء عن طريق إعادة تشغيل المحرر. تواجه خسارة غريبة من الإشارات إلى الكائنات في الجاهزة. في بعض الأحيان ، يتم إتلاف التركيب المسبق حسب المرجع (ToString () يُرجع "لاغٍ") ، على الرغم من أن كل شيء يبدو سارياً ، يتم سحب التجهيز المسبق إلى المشهد ولا يكون الرابط فارغًا. في بعض الأحيان تضيع بعض الاتصالات في جميع المشاهد. يبدو أنه تم تثبيت كل شيء ، لكن عند التبديل إلى فرع آخر ، يتم كسر جميع المشاهد - لا توجد روابط بين العناصر.لحسن الحظ ، غالبًا ما تم تصحيح هذه الأخطاء عن طريق إعادة تشغيل المحرر أو في بعض الأحيان حذف مجلد "المكتبة".في المجموع ، لقد مر حوالي نصف عام من الفكرة إلى النشر على Google Play. استغرق التطوير نفسه حوالي 3 أشهر ، في وقت الفراغ من العمل الرئيسي.