مرحبا يا هبر! أقدم لكم ترجمة ويكي مشروع
Svelto.ECS بقلم سيباستيانو ماندالا.
Svelto.ECS هو نتيجة سنوات عديدة من البحث وتطبيق مبادئ SOLID في تطوير الألعاب على الوحدة. هذا هو أحد التطبيقات العديدة لنمط ECS المتاح لـ C # مع ميزات فريدة متنوعة تم تقديمها لمعالجة أوجه القصور في النمط نفسه.
النظرة الأولى
أسهل طريقة لمشاهدة الميزات الأساسية لـ Svelto.ECS هي تنزيل
مثال Vanilla . إذا كنت تريد التأكد من سهولة استخدامه ، فسأعرض لك مثالًا:
لسوء الحظ ، لا يمكن فهم النظرية الكامنة وراء هذا الرمز بسرعة ، والتي قد تبدو بسيطة ولكنها مربكة في نفس الوقت. لفهم ذلك ، تحتاج إلى قضاء بعض الوقت في قراءة "جدار النص" وتجربة الأمثلة المذكورة أعلاه.
مقدمة
لقد ناقشت
مؤخرًا Svelto.ECS كثيرًا مع العديد من المبرمجين ذوي الخبرة. لقد جمعت الكثير من التعليقات وقدمت العديد من الملاحظات التي سأستخدمها كنقطة بداية لمقالاتي التالية ، حيث سأتحدث أكثر عن النظرية والممارسات الجيدة. مفسد صغير: أدركت أنه عندما تبدأ في استخدام Svelto.ECS ، فإن أكبر عقبة هي
تغيير نموذج البرمجة . إنه لأمر مدهش كم يجب أن أكتب لشرح المفاهيم الجديدة التي قدمتها Svelto.ECS ، مقارنة بكمية صغيرة من التعليمات البرمجية المكتوبة لتطوير الإطار. في الواقع ، في حين أن الإطار نفسه بسيط للغاية وخفيف الوزن ، فإن الانتقال من OOP مع الاستخدام النشط لمكونات الميراث أو الوحدة التقليدية إلى التصميم "الجديد" المعياري والمقرن بشكل فضفاض الذي يقترح Svelto.ECS استخدامه يمنع الأشخاص من التكيف مع الإطار.
يستخدم Svelto.ECS بنشاط في
Freejam (ملاحظة المترجم - المؤلف هو المدير الفني في هذه الشركة). نظرًا لأنني أستطيع دائمًا أن أشرح لزملائي المفاهيم الأساسية للإطار ، فإنهم يستغرقون وقتًا أقل لفهم العمل معه. على الرغم من أن Svelto.ECS قاسية قدر الإمكان ، إلا أنه من الصعب التغلب على العادات السيئة ، لذا يميل المستخدمون إلى إساءة استخدام بعض المرونة التي تسمح لهم بتكييف الإطار مع النماذج "القديمة" التي يشعرون بالراحة بها. يمكن أن يؤدي هذا إلى كارثة بسبب سوء الفهم أو تشويه المفاهيم التي يقوم عليها منطق الإطار. هذا هو السبب في أنني أعتزم كتابة أكبر عدد ممكن من المقالات ، خاصة وأنني متأكد من أن نموذج ECS هو أفضل حل في الوقت الحالي لكتابة كود فعال وقابل للصيانة للمشاريع الكبيرة التي تتغير وإعادة صياغة عدة مرات على مدى عدة سنوات.
Robocraft و
Cardlife هما دليل على ذلك.
لن أتحدث كثيرًا عن النظريات الكامنة وراء هذه المقالة. سأذكرك فقط لماذا رفضت استخدام
حاوية IoC وبدأت في استخدام إطار عمل ECS حصريًا: تعد حاوية IoC أداة خطيرة جدًا إذا تم استخدامها دون فهم جوهر انقلاب التحكم. كما ترون من مقالاتي السابقة ، فإنني أميز بين عكس التحكم في الإنشاء (Inverse of Creation Control) وانقلاب التحكم في التدفق (Inversion of Flow Control). يشبه التحكم في التدفق العكسي مبدأ هوليوود: "لا تتصل بنا ، سنتصل بك". هذا يعني أنه لا ينبغي أبدًا استخدام التبعيات المحقونة بشكل مباشر من خلال الطرق العامة ، لأنك بذلك تقوم ببساطة باستخدام حاوية IoC كبديل لأي شكل آخر من أشكال الحقن العالمي ، مثل singleton. ومع ذلك ، إذا تم استخدام حاوية IoC على أساس انقلاب الإدارة (IoC) ، فإن الأمر كله يرجع في الأساس إلى إعادة استخدام نمط "أسلوب القالب" لتنفيذ المديرين الذين يستخدمون فقط لتسجيل الكائنات التي يديرونها. في السياق الحقيقي لانقلابات التحكم في التدفق ، يكون المديرون دائمًا مسؤولين عن إدارة الكيانات. هل يبدو هذا كنمط ECS؟ بالطبع. بناءً على هذا المنطق ، اتخذت نمط ECS وقمت بتطوير إطار جامد قائم عليه ، واستخدامه يوازي تطبيق نموذج البرمجة الجديد.
تكوين الجذر و EnginesRoot
الفئة الرئيسية هي جذر التكوين للتطبيق. جذر التكوين هو المكان الذي يتم فيه إنشاء التبعيات وتنفيذها (تحدثت كثيرًا عن هذا في مقالاتي). ينتمي جذر التكوين إلى سياق ، ولكن يمكن أن يكون للسياق أكثر من جذر تكوين واحد. على سبيل المثال ، المصنع هو أصل التكوين. قد يحتوي التطبيق على أكثر من سياق واحد ، ولكن هذا سيناريو متقدم ، وفي هذا المثال لن نعتبره.
قبل الغوص في الشفرة ، دعنا نتعرف على القواعد الأولى للغة Svelto.ECS. ECS هو نظام مكون الكيان المختصر. تم تحليل البنية التحتية لـ ECS جيدًا في المقالات من قبل العديد من المؤلفين ، ولكن في حين أن المفاهيم الأساسية شائعة ، إلا أن عمليات التنفيذ تختلف بشكل كبير. بادئ ذي بدء ، لا توجد طريقة قياسية لحل بعض المشاكل التي تنشأ عند استخدام التعليمات البرمجية الموجهة ECS. فيما يتعلق بهذه القضية ، فإنني أبذل قصارى جهدي ، ولكن سأتحدث عن ذلك لاحقًا أو في المقالات التالية. تستند النظرية إلى مفاهيم الجوهر والمكونات (الكيانات) والأنظمة. على الرغم من أنني أفهم سبب استخدام كلمة System تاريخيًا ، لم أجدها منذ البداية بديهية بما يكفي لهذا الغرض ، لذلك استخدمت المحرك كمرادف للنظام ، ويمكنك ، اعتمادًا على تفضيلاتك ، استخدام أحد هذه المصطلحات.
إن فئة EnginesRoot هي جوهر Svelto.ECS. بمساعدتها ، يمكنك تسجيل المحركات وتصميم كل جوهر اللعبة. إن إنشاء المحركات ديناميكيًا ليس له معنى كبير ، لذلك يجب إضافتها جميعًا إلى مثيل EnginesRoot من نفس الجذر للتكوين الذي تم إنشاؤه فيه. لأسباب مماثلة ، يجب عدم نشر مثيل EnginesRoot مطلقًا ، ولا يجب حذف المحركات بعد إضافتها.
لإنشاء تبعيات وتنفيذها ، نحتاج إلى جذر واحد على الأقل من التكوين. نعم ، في تطبيق واحد ، قد يكون هناك أكثر من محرك EnginesRoot واحد ، لكننا لن نتطرق إلى هذا في المقالة الحالية ، والتي أحاول تبسيطها قدر الإمكان. إليك ما يبدو عليه جذر التكوين مع إنشاء المحرك وحقن التبعية:
void SetupEnginesAndEntities() {
هذا الرمز من مثال Survival ، والذي يتم التعليق عليه الآن ويتوافق مع جميع قواعد الممارسات الجيدة التي أقترح تطبيقها ، بما في ذلك استخدام منطق المحرك المستقل والمنظم الذي تم اختباره. ستساعدك التعليقات على فهم معظمها ، ولكن قد يكون من الصعب فهم مشروع بهذا الحجم إذا كنت جديدًا على Svelto.
الكيانات
الخطوة الأولى بعد إنشاء الجذر الفارغ للتكوين ومثيل لفئة EnginesRoot هي تحديد الكائنات التي تريد العمل بها أولاً. من المنطقي أن تبدأ بـ Entity Player. لا ينبغي الخلط بين جوهر Svelto.ECS وبين كائن لعبة الوحدة (GameObject). إذا قرأت مقالات أخرى ذات صلة بـ ECS ، يمكنك أن ترى أنه في كثير منها ، يتم وصف الكيانات غالبًا على أنها فهارس. ربما هذه هي أسوأ طريقة لتقديم مفهوم ECS. على الرغم من أن Svelto.ECS صحيح ، إلا أنه مخفي فيه. أريد أن يقوم مستخدم Svelto.ECS بتمثيل ووصف وتحديد كل كيان من حيث لغة مجال تصميم اللعبة. يجب أن يكون الكيان الموجود في الرمز هو الكائن الموصوف في مستند تصميم اللعبة. سيؤدي أي شكل آخر من أشكال تعريف الكيانات إلى طريقة بعيدة المنال لتكييف وجهات نظرك القديمة مع مبادئ Svelto.ECS. اتبع هذه القاعدة الأساسية ولن تكون مخطئًا. فئة الكيان نفسها غير موجودة في الشفرة ، ولكن لا يزال يتعين عليك عدم تعريفها بشكل مجرد.
المحركات
الخطوة التالية هي التفكير في السلوك الذي يجب أن تسأل الكيانات. يتم دائمًا تصميم كل سلوك داخل المحرك ؛ لا يمكنك إضافة منطق إلى أي فئات أخرى داخل تطبيق Svelto.ECS. يمكننا البدء بتحريك شخصية اللاعب وتحديد فئة
PlayerMovementEngine . يجب أن يكون اسم المحرك شديد التركيز ، لأنه كلما كان أكثر تحديدًا ، زاد احتمال أن يتبع المحرك قاعدة المسؤولية الفردية. تسمية الفئة المناسبة في Svelto.ECS أمر أساسي. والهدف ليس فقط إظهار نواياك بوضوح ، ولكن أيضًا مساعدتك على "رؤيتها" بنفسك.
للسبب نفسه ، من المهم أن يكون محركك في مساحة اسم متخصصة للغاية. إذا قمت بتحديد مساحات الأسماء وفقًا لبنية المجلد ، فتكيف مع مفاهيم Svelto.ECS. يساعد استخدام مساحات أسماء محددة على اكتشاف أخطاء التصميم عند استخدام الكيانات داخل مساحات أسماء غير متوافقة. على سبيل المثال ، لا يُفترض أن يتم استخدام أي كائن معاد داخل مساحة اسم اللاعب ، ما لم يكن الهدف هو كسر القواعد المرتبطة بالوحدات النمطية والاقتران الضعيف للأشياء. الفكرة هي أن كائنات مساحة اسم معينة يمكن استخدامها فقط داخلها أو في مساحة الاسم الأصل. يعد استخدام Svelto.ECS أكثر صعوبة في تحويل التعليمات البرمجية الخاصة بك إلى السباغيتي ، حيث يتم حقن التبعيات يمينًا ويسارًا ، وستساعدك هذه القاعدة على رفع مستوى جودة التعليمات البرمجية حتى عندما يتم تلخيص التبعيات بشكل صحيح بين الفئات.
في Svelto.ECS ، ينتقل التجريد إلى الأمام بضعة أسطر ، لكن ECS يساعد بشكل أساسي على تجريد البيانات من المنطق الذي يجب أن يعالج البيانات. يتم تحديد الكيانات من خلال بياناتهم ، وليس سلوكهم. في هذه الحالة ، تعتبر المحركات مكانًا يمكنك من خلاله وضع السلوك المشترك للكيانات المتطابقة بحيث يمكن للمحركات العمل دائمًا مع مجموعة من الكيانات.
يسمح Svelto.ECS ونموذج ECS لبرنامج التشفير بتحقيق إحدى الكؤوس المقدسة للبرمجة النقية ، وهو التغليف المثالي للمنطق. يجب ألا تحتوي المحركات على وظائف عامة. الوظائف العامة الوحيدة التي يجب أن توجد هي تلك المطلوبة لتنفيذ واجهات الإطار. هذا يؤدي إلى نسيان حقن التبعية ويساعد على تجنب الشفرة السيئة التي تحدث عند استخدام حقن التبعية دون عكس التحكم. يجب عدم تضمين المحركات في أي محرك آخر أو أي نوع آخر من الفئات. إذا كنت تعتقد أنك تريد تنفيذ المحرك ، فأنت ببساطة تقوم بخطأ أساسي في تصميم الكود.
مقارنةً بـ Unity MonoBehaviours ، تُظهر المحركات بالفعل الميزة الضخمة الأولى ، وهي القدرة على الوصول إلى جميع حالات الكيانات من هذا النوع من نفس منطقة التعليمات البرمجية. هذا يعني أن الشفرة يمكن أن تستخدم بسهولة حالة جميع الكائنات مباشرة من نفس المكان حيث سيتم تنفيذ منطق الكائن المشترك. بالإضافة إلى ذلك ، يمكن للمحركات الفردية معالجة نفس الكائنات بحيث يمكن للمحرك تغيير حالة الكائن ، بينما يمكن للمحرك الآخر قراءته ، باستخدام محركين بشكل فعال للاتصال من خلال بيانات الكيان نفسه. يمكن رؤية مثال من خلال النظر إلى
مشغلي PlayerGunShootingEngine و
PlayerGunShootingFxsEngine . في هذه الحالة ، يوجد محركان في نفس مساحة الاسم ، بحيث يمكنهم مشاركة نفس بيانات الكيان. يحدد
PlayerGunShootingEngine ما إذا كان اللاعب (العدو) قد تعرض للتلف ، ويكتب قيمة
lastTargetPosition لمكون
IGunAttributesComponent (وهو مكون
PlayerGunEntity ). يعالج
PlayerGunShootFxsEngine التأثيرات الرسومية للسلاح ويقرأ موضع الهدف الذي
حدده اللاعب. هذا مثال على التفاعل بين المحركات من خلال استطلاع البيانات. لاحقًا في هذه المقالة سأوضح كيفية السماح لآلية للتواصل بينها عن طريق
دفع البيانات (دفع البيانات) أو
ربط البيانات (ربط البيانات) . منطقياً ، لا يجب أن تخزن المحركات الحالة أبداً.
لا تحتاج المحركات إلى معرفة كيفية التفاعل مع المحركات الأخرى. يحدث الاتصال الخارجي من خلال التجريد ، وتقوم Svelto.ECS بحل الاتصال بين المحركات بثلاث طرق رسمية مختلفة ، ولكن سأتحدث عن ذلك لاحقًا. أفضل المحركات هي تلك التي لا تتطلب أي اتصالات خارجية. تعكس هذه المحركات سلوكًا مغلفًا جيدًا وتعمل عادةً من خلال حلقة منطقية. يتم دائمًا تصميم الحلقات باستخدام مهام Svelto.Task داخل تطبيقات Svelto.ECS. نظرًا لأن حركة اللاعب تحتاج إلى تحديث كل علامة جسدية ، فمن الطبيعي إنشاء مهمة يتم تنفيذها على كل علامة جسدية. يتيح لك Svelto.Tasks تشغيل كل نوع من أنواع
IEnumerator على عدة أنواع من برامج الجدولة. في هذه الحالة ، قررنا إنشاء مهمة على
PhysicScheduler ، والتي تسمح لك بتحديث موقف اللاعب:
public PlayerMovementEngine(IRayCaster raycaster, ITime time) { _rayCaster = raycaster; _time = time; _taskRoutine = TaskRunner.Instance.AllocateNewTaskRoutine() .SetEnumerator(PhysicsTick()).SetScheduler(StandardSchedulers.physicScheduler); } protected override void Add(PlayerEntityView entityView) { _taskRoutine.Start(); } protected override void Remove(PlayerEntityView entityView) { _taskRoutine.Stop(); } IEnumerator PhysicsTick() {
يمكن تنفيذ مهام Svelto.Tasks مباشرة أو من خلال كائنات
ITaskRoutine . لن أتحدث كثيرًا عن Svelto. المهام هنا ، حيث كتبت مقالات أخرى لها. السبب الذي جعلني قررت استخدام روتين المهام بدلاً من بدء تنفيذ IEnumerator مباشرة أمر تقديري تمامًا. أردت أن أوضح أنه يمكنك بدء دورة عند إضافة كائن لاعب إلى المحرك وإيقافه عند حذفه.
ومع ذلك ، تحتاج إلى معرفة ذلك عند إضافة كائن وحذفه.Svelto.ECS يدخل الاسترجاعات ل إضافة و إزالة لمعرفة متى يتم إضافة بعض الكيانات أو إزالتها. هذا شيء فريد في Svelto.ECS ، ولكن يجب استخدام هذا النهج بحكمة. لقد رأيت في كثير من الأحيان أن عمليات الاسترداد هذه يتم إساءة استخدامها ، لأنها في كثير من الحالات تكفي للاستعلام عن الكيانات. حتى وجود مرجع كيان كحقل محرك يجب اعتباره استثناءً أكثر من القاعدة.فقط عندما يتم استخدام عمليات الاسترجاعات هذه ، في حالة ورث المحرك إما من SingleEntityViewEngine أو من MultiEntitiesViewEngine <EntityView1، ...، EntityViewN>. مرة أخرى ، يجب أن يكون استخدام هذه البيانات نادرًا ، ولا ينوون بأي حال من الأحوال الإبلاغ عن الكائنات التي سيعالجها المحرك.تقوم المحركات غالبًا بتنفيذ واجهة IQueryingEntityViewEngine . يسمح لك هذا بالوصول إلى البيانات واستخراجها من قاعدة بيانات كيان. تذكر أنه يمكنك دائمًا طلب كائن من داخل المحرك ، ولكن في اللحظة التي تطلب فيها كيانًا غير متوافق مع مساحة الاسم حيث يوجد المحرك ، يجب أن تفهم أنك تقوم بالفعل بشيء خاطئ. يجب ألا تفترض المحركات مطلقًا أن الكيانات يمكن الوصول إليها ، ويجب أن تعمل على مجموعة من الكائنات. لا ينبغي الافتراض أنه سيكون هناك دائمًا لاعب واحد فقط في اللعبة ، كما أفعل في مثال الكود. في EnemyMovementEngine هناك طريقة عامة جدًا لكيفية طلب الأشياء: public void Ready() { Tick().Run(); } IEnumerator Tick() { while (true) { var enemyTargetEntityViews = entityViewsDB.QueryEntityViews<EnemyTargetEntityView>(); if (enemyTargetEntityViews.Count > 0) { var targetEntityView = enemyTargetEntityViews[0]; var enemies = entityViewsDB.QueryEntityViews<EnemyEntityView>(); for (var i = 0; i < enemies.Count; i++) { var component = enemies[i].movementComponent; component.navMeshDestination = targetEntityView.targetPositionComponent.position; } } yield return null; } }
في هذه الحالة ، تبدأ دورة المحرك الرئيسية مباشرة على المجدول المحدد مسبقًا. ضع علامة () .Run ()يوضح أقصر طريقة لبدء IEnumerator باستخدام Svelto.Tasks. سيستمر IEnumerator في الخضوع للإطار التالي حتى يتم العثور على هدف عدو واحد على الأقل. نظرًا لأننا نعلم أنه سيكون هناك دائمًا هدف واحد (افتراض خاطئ آخر) ، فإنني أختار الهدف الأول المتاح. في حين أن هدف Enemy Target يمكن أن يكون واحدًا فقط (على الرغم من أنه قد يكون هناك المزيد!) ، فهناك العديد من الأعداء ، ومع ذلك يعتني المحرك بمنطق الحركة للجميع. في هذه الحالة ، خدعت ، لأنني في الواقع أستخدم نظام Unity Nav Mesh System ، لذا كل ما علي فعله هو ضبط الوجهة على NavMesh. بصراحة ، لم أستخدم رمز Unity NavMesh مطلقًا ، لذلك لست متأكدًا حتى من كيفية عمله ، هذا الرمز موروث للتو من عرض Survival الأصلي.لاحظ أن المكون لا يوفر مطلقًا تبعية Navmesh Unity. يجب أن يعرض عنصر الكيان ، كما سأناقش لاحقًا ، أنواع القيم دائمًا. في هذه الحالة ، تسمح لك هذه القاعدة أيضًا بإبقاء التعليمات البرمجية تحت السيطرة ، حيث يمكن تنفيذ نوع القيمة لحقل navMeshDestination لاحقًا دون استخدام Unity Nav Mesh.لإكمال الفقرة على المحركات ، لاحظ أنه لا يوجد شيء مثل محرك صغير جدًا. لذلك ، لا تخف من كتابة محرك يحتوي على عدة أسطر من التعليمات البرمجية ، لأنه لا يمكنك كتابة المنطق في مكان آخر ، وتحتاج إلى محركاتك لاتباع قاعدة المسؤولية الموحدة.تمثيلات الكيان
قبل ذلك ، قدمنا مفهوم المحرك والتعريف المجرد للجوهر ، دعنا نحدد الآن ما هو تمثيل الجوهر. يجب أن أعترف أنه من بين المفاهيم الخمسة التي بنيت عليها Svelto.ECS ، من المحتمل أن تكون Entity Views هي الأكثر إرباكًا. عُرفت سابقًا بالعقدة (اسم مأخوذ من إطار عمل ECS Ash ) ، أدركت أن اسم "العقدة" لا يعني شيئًا. قد يكون EntityView أيضا مضلل لأن المبرمجين التي ترتبط عادة مع تمثيل مفهوم المنبثقة من القالب تحكم عرض نموذج(وحدة تحكم عرض النموذج) ، ومع ذلك ، يستخدم Svelto.ECS طريقة العرض ، لأن EntityView هو كيف يرى المحرك الكيان. أحب وصفه بهذا الشكل لأنه يبدو طبيعيًا جدًا ، ولكن يمكنني أيضًا أن أسميه EntityMap ، لأن EntityView يعرض مكونات الكيان التي يجب أن يصل إليها المحرك. يجب أن يساعد هذا المخطط من مفاهيم Svelto.ECS قليلاً:
أقترح البدء بالمحرك ، والآن نحن على الجانب الأيمن من هذا المخطط. لكل محرك مجموعته الخاصة من EntityViews. يمكن للمحرك إعادة استخدام EntityViews المتوافقة مع مساحة الاسم ، ولكن في أغلب الأحيان يحدد المحرك EntityViews الخاص به. لا يهتم المحرك بما إذا كان كيان Player محددًا بالفعل أم لا ، فهو يذكر حقيقة أنه يحتاج إلى PlayerEntityViewللعمل. تعتمد كتابة الشفرة على احتياجات المحرك ، ولا يجب إنشاء كيان وحقله قبل فهم كيفية استخدامه. في سيناريو أكثر تعقيدًا ، يمكن أن يكون اسم EntityView أكثر تحديدًا. على سبيل المثال ، إذا اضطررنا إلى كتابة محركات معقدة للتعامل مع منطق المشغل وتقديم رسومات المشغل (أو الرسوم المتحركة ، وما إلى ذلك) ، فقد يكون لدينا PlayerPhysicEngine مع PlayerPhysicEntityView ، بالإضافة إلى PlayerGraphicEngine مع PlayerGraphicEntityView أو PlayerAnimationEngine مع PlayerAnimationEntityView . يمكن استخدام أسماء أكثر تحديدًا ، مثل PlayerPhysicMovementEngine أو PlayerPhysicJumpEngine (إلخ).مكونات
لقد أدركنا أن المحركات تمثل سلوكًا لمجموعة من بيانات الكيان ، ونفهم أن المحركات لا تستخدم الكيانات بشكل مباشر ، ولكنها تستخدم مكونات الكيان من خلال تمثيل الكيانات. لقد أدركنا أن EntityView هي فئة يمكن أن تحتوي فقط على مكونات عامة للكيانات. وألمحت أيضًا إلى أن مكونات الكيان هي دائمًا واجهات ، لذلك دعونا نعطي تعريفًا أفضل:الكيانات عبارة عن مجموعة من البيانات ، ومكونات الكيان هي طريقة للوصول إلى تلك البيانات. إذا لم تكن قد لاحظت ذلك بعد ، فإن تعريف مكونات الكيان كواجهات هو ميزة أخرى فريدة جدًا لـ Svelto.ECS. عادةً ما تكون المكونات الموجودة في الإطارات الأخرى كائنات. بدلاً من ذلك ، يمكن أن يقلل استخدام الواجهات بشكل كبير من التعليمات البرمجية. إذا اتبعت المبدأ" مبدأ الفصل بين الواجهات" ، بعد كتابة واجهات مكون صغيرة ، حتى مع وجود خاصية واحدة لكل منها ، ستلاحظ أنك قد بدأت في إعادة استخدام واجهات المكون داخل كيانات مختلفة. في مثالنا ، يتم إعادة استخدام ITransformComponent في العديد من تمثيلات الكيانات. كما يسمح لهم استخدام المكونات كواجهات بتنفيذ نفس الكائنات ، مما يبسط في كثير من الحالات العلاقة بين الكيانات التي ترى نفس الكيان باستخدام تمثيلات مختلفة للكيانات (أو نفسها ، إن أمكن).لذلك ، في Svelto.ECS ، يكون مكون الكيان دائمًا واجهة ، ويتم استخدام هذه الواجهة فقط من خلال حقل EntityView داخل المحرك. ثم يتم تنفيذ واجهة مكون الكيان بواسطة ما يسمى«». , .يجب أن تخزن المكونات دائمًا أنواعًا ذات مغزى ، وتكون الحقول دائمًا خصائص. لا يمكن إجراء استثناءات إلا لكتابة محددات وبطاقات كطرق لاستخدام الكلمة الأساسية المرجع عند الحاجة إلى التحسين. هذا لا يعني أن الكود موجه نحو البيانات ، ولكنه سيسمح لك بإنشاء كود للاختبارات ، حيث لا يجب أن يعالج منطق المحرك الروابط إلى التبعيات الخارجية. بالإضافة إلى ذلك ، يمنع هذا المبرمجين من الغش في الإطار واستخدام الوظائف العامة (التي قد تشمل المنطق!) للكائنات العشوائية. كان السبب الوحيد الذي يجعلك تشعر بالحاجة إلى استخدام الروابط داخل واجهات مكونات الكيان هو التعامل مع تبعيات الطرف الثالث ، مثل كائنات الوحدة. ومع ذلك ، يوضح مثال Survival كيفية التعامل مع هذا ،ترك رمز اختبار المحرك دون الحاجة إلى القلق بشأن تبعيات الوحدة.هذا هو المكان الذي يأتي فيه واصفات الكيانات إلى الإنقاذ لتجميع كل شيء. نحن نعلم أن المحركات يمكنها الوصول إلى بيانات الكيان من خلال المكونات المخزنة في طرق عرض الكيانات. نحن نعلم أن المحركات هي فئات ، و EntityView هي فئات تحتوي فقط على كيانات المكونات وأن المكونات هي واجهات. على الرغم من أنني قدمت تعريفًا تجريديًا للجوهر ، إلا أننا لم نر فئة واحدة تمثل فعليًا الجوهر. هذا يتوافق مع مفهوم الأشياء التي هي معرفات داخل نظام ECS الحديث. ومع ذلك ، بدون التعريف الصحيح للكيانات ، فإن هذا سيجبر المبرمجين على تحديد الكيانات التي لها تمثيلات للكيانات ، والتي ستكون خاطئة بشكل كارثي. تمثيلات الكيانات هي الطريقة التي يمكن من خلالها للعديد من المحركات رؤية نفس الكيان ،لكنهم ليسوا كيانات. يجب اعتبار الكيان نفسه دائمًا مجموعة من البيانات المحددة من خلال مكونات الكيان ، ولكن حتى هذا التعريف ضعيف. يمكّن مثيل EntityDescriptor برنامج التشفير من تحديد الكيانات الخاصة به بشكل صحيح ، بغض النظر عن المحركات التي ستقوم بمعالجتها. لذلك ، في حالة Entity Player ، نحتاجPlayerEntityDescriptor . سيتم استخدام هذه الفئة لإنشاء كيانات ، وعلى الرغم من أن ما تفعله حقًا شيء مختلف تمامًا ، إلا أن حقيقة أن المستخدم يمكنه كتابة BuildEntity <PlayerEntityDescriptor> () يساعد على تصور الكيانات بسهولة لبناء النوايا وتوصيلها للآخرين. التشفير.ومع ذلك ، فإن ما يفعله EntityDescriptor حقًا هو إنشاء قائمة EntityViews !!! في المراحل الأولى من تطوير الإطار ، سمحت للمبرمجين بإنشاء قائمة EntityViews يدويًا ، مما أدى إلى رمز قبيح جدًا لأنه لم يعد بإمكانه تصور ما كان يحدث بالفعل.إليك ما يبدو عليه PlayerEntityDescriptor : using Svelto.ECS.Example.Survive.Camera; using Svelto.ECS.Example.Survive.HUD; using Svelto.ECS.Example.Survive.Enemies; using Svelto.ECS.Example.Survive.Sound; namespace Svelto.ECS.Example.Survive.Player { public class PlayerEntityDescriptor : GenericEntityDescriptor<HUDDamageEntityView, PlayerEntityView, EnemyTargetEntityView, DamageSoundEntityView, HealthEntityView, CameraTargetEntityView> { } }
واصفات الكيانات (والمنفذون) هي الفئات الوحيدة التي يمكنها استخدام معرفات من مساحات أسماء متعددة. في هذه الحالة ، يحدد PlayerEntityDescriptor قائمة EntityViews للنسخ الفوري والحقن في المحرك عند إنشاء PlayerEntity.EntityDescriptorHolder
EntityDescriptorHolder هو امتداد لـ Unity ويجب استخدامه في حالات معينة فقط. الأكثر شيوعًا هو إنشاء نوع من تعدد الأشكال يخزن معلومات حول الكيانات لبناء GameObject Unity. وبالتالي ، يمكن استخدام نفس الرمز لإنشاء عدة أنواع من الكيانات. على سبيل المثال ، في Robocraft ، نستخدم مصنع مكعب واحد يبني جميع المكعبات التي تتكون منها الآلات. يتم تخزين نوع المكعب في التجميع المسبق للمكعب نفسه. هذا أمر جيد طالما أن المنفذين هم أنفسهم بين المكعبات أو الموجودة في GameObject مثل MonoBehaviour. يُفضل إنشاء الكيانات مباشرةً ، لذا استخدم EntityDescriptorHolders فقط عندما تفهم مبادئ Svelto.ECS بشكل صحيح ، وإلا سيكون هناك خطر إساءة الاستخدام. توضح هذه الوظيفة من المثال كيفية استخدام الفصل: void BuildEntitiesFromScene(UnityContext contextHolder) {
لاحظ أنه في هذا المثال أستخدم دالة BuildEntity غير العامة الأقل تفضيلاً . سأشرح هذا. في هذه الحالة ، فإن المنفذين هم فئات MonoBehaviour المرتبطة بـ GameObject. هذه ليست ممارسة جيدة. كان يجب عليّ إزالة هذا الرمز من المثال ، ولكن بقي لي أن أريكم هذه الحالة الخاصة. يجب أن يكون المنفذون ، كما سنرى لاحقًا ، دروس MonoBehaviours فقط عند الضرورة!الدوافع
قبل إنشاء جوهر الخاص، دعونا تحدد مفهوم آخر في Svelto.ECS، وهو المطبق . كما نعلم ، تكون مكونات الكيان دائمًا واجهات ، ويجب تنفيذ واجهات C #. ويطلق على الكائن الذي يقوم بتنفيذ هذه الواجهات "المنفذ". لدى المنفذين العديد من الخصائص الهامة:- القدرة على فك عدد الكائنات التي سيتم تجميعها من عدد مكونات الكيان اللازمة لتحديد بيانات الكيان.
- القدرة على تبادل البيانات بين المكونات المختلفة ، نظرًا لأن المكونات توفر البيانات من خلال الخصائص ، يمكن أن تُرجع الخصائص المختلفة للمكون نفس مجال التنفيذ.
- القدرة على إنشاء كيان كعب مكون واجهة. هذا مهم من أجل ترك رمز المحرك مختبراً.
- Svelto.ECS (third party) . . Unity, , , Monobehaviour . , Unity, OnTriggerEnter / OnTriggerExit , Unity. , . :
public class EnemyTriggerImplementor : MonoBehaviour, IImplementor, IEnemyTriggerComponent, IEnemyTargetComponent { public event Action<int, int, bool> entityInRange; bool IEnemyTriggerComponent.targetInRange { set { _targetInRange = value; } } bool IEnemyTargetComponent.targetInRange { get { return _targetInRange; } } void OnTriggerEnter(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), true); } void OnTriggerExit(Collider other) { if (entityInRange != null) entityInRange(other.gameObject.GetInstanceID(), gameObject.GetInstanceID(), false); } bool _targetInRange; }
, , . , .إنشاء الكيان
لنفترض أننا قد خلقت لدينا محركات ، وأضاف لهم EnginesRoot ، خلقهم تمثل كيانات تلك الحاجة مكونات واجهات ليتم تنفيذها في إطار Implementorov . لقد حان الوقت لإنشاء أول جوهر لنا. يتم إنشاء جوهر دائما من خلال المثال كيان مصنع (الكيان المصنع)، التي أنشئت خلال EnginesRoot وظيفة GenerateEntityFactory . على عكس مثيل EnginesRoot ، يمكن نشر مثيل IEntityFactory ونقله. يمكن بناء الكائنات داخل جذر التكوين أو ديناميكيًا داخل المصانع ، لذلك في الحالة الأخيرة ، تحتاج إلى تمرير IEntityFactory من خلال معلمة.يأتي IEntityFactory مع العديد من الميزات المماثلة. في هذه المقالة سوف القفز على وظائف تفسير PreallocateEntitySlots و BuildMetaEntity ، إلى التركيز على الوظائف الأكثر استخداما BuildEntity و BuildEntityInGroup .من الأفضل دائمًا استخدام BuildEntityInGroup ، ولكن بالنسبة لمثال Survival الذي لا تحتاج إليه ، فلنرَ كيف يتم استخدام BuildEntity المعتاد في المثال: IEnumerator IntervaledTick() {
تذكر قراءة جميع التعليقات في هذا المثال ، فهي ستساعدك على فهم أفضل لمفاهيم Svelto.ECS. نظرًا لبساطة المثال ، لا أستخدم BuildEntityInGroup ، الذي يُستخدم في المشاريع الأكثر تعقيدًا. في Robocraft ، يقوم كل محرك يعالج منطق المكعبات الوظيفية بمعالجة منطق جميع المكعبات الوظيفية من هذا النوع المعين في اللعبة. ومع ذلك ، غالبًا ما يكون من الضروري معرفة السيارة التي تنتمي إليها المكعبات ، لذا فإن استخدام مجموعة لكل آلة سيساعد في كسر المكعبات من نفس النوع إلى آلات ، حيث يكون معرف الماكينة هو معرف المجموعة. هذا يسمح لنا بتنفيذ أشياء رائعة ، مثل تشغيل مهمة Svelto.Tasks على جهاز داخل نفس المحرك ، والتي يمكن أن تعمل بالتوازي باستخدام multithreading.يعرض هذا الجزء من التعليمات البرمجية مشكلة مهمة واحدة قدأغطيها بمزيد من التفصيل في المقالات التالية ... من التعليق (إذا لم تكن قد قرأته): لا تقم أبدًا بإنشاء MonoBehaviour Imprementors لتخزين البيانات فقط. يجب دائمًا استرداد البيانات من خلال طبقة الخدمة بغض النظر عن مصدر البيانات. الفوائد عديدة ، بما في ذلك حقيقة أنه لتغيير مصدر البيانات ، ما عليك سوى تغيير رمز الخدمة. في هذا المثال البسيط ، لا أستخدم طبقة الخدمة ، ولكن الفكرة بشكل عام واضحة. لاحظ أيضًا أنني أقوم بتحميل البيانات مرة واحدة فقط لكل تشغيل للتطبيق ، خارج الحلقة الرئيسية. يمكنك دائمًا استخدام هذه الحيلة إذا لم يتم تغيير البيانات التي تحتاجها أبدًا.في البداية ، قرأت البيانات مباشرة من MonoBehaviour ، كما يفعل التشفير البطيء الجيد. هذا جعلني أقوم بإنشاء منفذ مُسلسل للقراءة فقط MonoBehaviore. هذا مقبول إذا كنا لا نريد تلخيص مصدر البيانات ، ولكن من الأفضل بكثير إجراء تسلسل للمعلومات في ملف json وقراءتها عند الطلب إلى الخدمة بدلاً من قراءة هذه البيانات من مكون الكيان.التواصل في Svelto.ECS
إحدى المشكلات التي لم يتم توحيد حلها مطلقًا من خلال أي تطبيق ECS هي التواصل بين الأنظمة. هذا مكان آخر حيث فكرت كثيرًا ، و Svelto.ECS يحلها بطريقتين جديدتين. الطريقة الثالثة هي استخدام نمط المراقب / المرصد القياسي ، المقبول في حالات محددة ومحددة للغاية.DispatchOnSet / DispatchOnChange
في وقت سابق رأينا كيفية السماح لمحركات لتبادل البيانات من خلال مكونات الكيان باستخدام استطلاع البيانات. DispatchOnSet و DispatchOnChange هما المرجعان الوحيدان (الأنواع غير المهمة) التي يمكن إرجاعها بواسطة خصائص مكونات الكيان ، ولكن يجب أن يكون نوع المعلمة العامة T نوعًا ذا معنى. تبدو أسماء الوظائف مثل مرسل الحدث ، ولكن بدلاً من ذلك يجب اعتبارها طرقًا لدفع البيانات ، على عكس استطلاعات البيانات ، والتي تشبه إلى حد ما ربط البيانات. هذا كل شيء ، في بعض الأحيان يكون استقصاء البيانات غير مريح ، لا نريد استطلاع متغير في كل إطار عندما نعرف أن البيانات نادرًا ما تتغير. DispatchOnSet و DispatchOnChangeلا يمكن البدء بدون تغيير البيانات ، وهذا يسمح لنا بالنظر إليها كآلية ربط البيانات بدلاً من حدث عادي. لا توجد أيضًا وظيفة إطلاق للاستدعاء ؛ بدلاً من ذلك ، يجب تعيين قيمة البيانات التي تحتفظ بها هذه الفئات أو تغييرها. لا توجد أمثلة رائعة في شفرة Survival ، ولكن يمكنك أن ترى كيف يعمل الحقل targetHit Boolean من IGunHitTargetComponent . الفرق بين DispatchOnSet و DispatchOnChange هو أن الأخير يطلق الحدث فقط عندما تتغير البيانات بالفعل ، والأولى دائمًا.التسلسل
يتم تغليف المحركات المثالية بالكامل ، ويمكنك كتابة منطق هذا المحرك كتسلسل من التعليمات باستخدام Svelto.Tasks و IEnumerators. ومع ذلك ، هذا ليس ممكنًا دائمًا ، لأنه في بعض الحالات يجب على المحركات إرسال الأحداث إلى محركات أخرى. وعادة ما يتم ذلك من خلال بيانات الكيان ، وخاصة باستخدام DispatchOnSet و DispatchOnChangeومع ذلك ، كما هو الحال في الكيانات "المتضررة" في المثال ، فإن سلسلة من المحركات المستقلة وغير ذات الصلة تعمل عليها. في حالات أخرى ، تريد أن يكون التسلسل صارمًا حسب ترتيب استدعاء المحركات ، كما هو الحال في المثال حيث أريد أن يحدث الموت دائمًا لهذا الأخير. في هذه الحالة ، فإن التسلسل ليس فقط سهل الاستخدام للغاية ، ولكنه أيضًا مريح للغاية! إعادة هيكلة التسلسل بسيطة للغاية. لذلك ، استخدم مهام IEnumerator Svelto للمحركات والتتابعات "العمودية" للمنطق "الأفقي" بين المحركات.مراقب / مراقب
لقد تركت الفرصة لاستخدام هذا النمط خصيصًا للحالات التي يجب أن يتفاعل فيها الرمز القديم أو الرمز الذي لا يستخدم Svelto.ECS مع محركات Svelto.ECS. بالنسبة للحالات الأخرى ، يجب استخدامه بحذر شديد ، حيث توجد إمكانية لإساءة استخدام النمط ، لأنه مألوف لمعظم المبرمجين الجدد في Svelto.ECS ، والمتسلقون عادة ما يكونون الخيار الأفضل.