مرحبا بالجميع!
يبدأ التيار الرابع
"مطور C ++" من هنا ، وهي واحدة من أكثر الدورات نشاطًا في بلدنا ، استنادًا إلى اجتماعات حقيقية ، حيث لا يأتي "الصليبيون" فقط للتحدث مع
Dima Shebordaev :) بشكل عام ، نمت الدورة بالفعل إلى واحدة من أكبر الشركات في بلدنا ، لم يطرأ أي تغيير على أن
ديما تجري دروسًا مفتوحة ونختار مواد مثيرة للاهتمام قبل بدء الدورة.
دعنا نذهب!
الدخول
أصبح نظام مكونات الكيان (ECS ، "نظام الكيان - المكون") في ذروة الشعبية كبديل معماري يؤكد على مبدأ التكوين على الميراث. في هذه المقالة ، لن أخوض في تفاصيل المفهوم ، حيث توجد بالفعل موارد كافية حول هذا الموضوع. هناك العديد من الطرق لتنفيذ ECS ، ولكن ، في معظم الأحيان ، أختار طرقًا معقدة إلى حد ما يمكن أن تربك المبتدئين وتستغرق الكثير من الوقت.
في هذا المنشور ، سأصف طريقة بسيطة جدًا لتنفيذ ECS ، والتي لا تتطلب نسختها الوظيفية أي كود تقريبًا ، ولكنها تتبع المفهوم بالكامل.

ECS
بالحديث عن ECS ، غالبًا ما يعني الناس أشياء مختلفة. عندما أتحدث عن ECS ، أعني نظامًا يسمح لك بتحديد الكيانات التي تحتوي على صفر أو أكثر من مكونات البيانات النقية. تتم معالجة هذه المكونات بشكل انتقائي من قبل أنظمة المنطق النقي. على سبيل المثال ، يرتبط موضع العنصر وسرعته وصندوق ضربه وصحته بالكيان E. هم ببساطة تخزين البيانات في أنفسهم. على سبيل المثال ، يمكن للمكون الصحي تخزين اثنين من الأعداد الصحيحة: واحد للصحة الحالية والآخر للحد الأقصى. يمكن أن يكون النظام نظامًا لتجديد الصحة يجد جميع حالات المكون الصحي ويزيدها بمقدار 1 كل 120 إطارًا.
تطبيق نموذجي لـ C ++
هناك العديد من المكتبات التي تقدم تطبيقات ECS. عادة ، تتضمن عنصرًا واحدًا أو أكثر من القائمة:
- وراثة المكون / النظام
GravitySystem : public ecs::System
لفئة GravitySystem : public ecs::System
؛ - الاستخدام النشط للقوالب ؛
- كل من ذلك ، وآخر في بعض نظرة CRTP ؛
- فئة
EntityManager
، التي تتحكم في إنشاء / تخزين الكيانات بطريقة ضمنية.
بعض أمثلة جوجل السريعة:
كل هذه الأساليب لها الحق في الحياة ، ولكن هناك بعض العوائق فيها. وتعني الطريقة التي يعالجون بها البيانات بطريقة مبهمة أنه سيكون من الصعب فهم ما يجري في الداخل وما إذا كان تباطؤ الأداء قد حدث. هذا يعني أيضًا أنه يجب عليك دراسة طبقة التجريد بالكامل والتأكد من أنها تتناسب بشكل جيد مع التعليمات البرمجية الموجودة. لا تنسى الأخطاء الخفية ، والتي ربما تكون مخفية كثيرًا في مقدار الرمز الذي يجب تصحيحه.
يمكن أن يؤثر النهج القائم على القالب بشكل كبير على وقت الترجمة وعدد المرات التي ستضطر فيها إلى إعادة بناء البنية. في حين أن المفاهيم القائمة على الميراث يمكن أن تؤدي إلى تدهور الأداء.
السبب الرئيسي الذي يجعلني أعتقد أن هذه الأساليب مفرطة هو أن المشكلة التي تحلها بسيطة للغاية. في النهاية ، هذه مجرد مكونات بيانات إضافية مرتبطة بالكيان ومعالجتها الانتقائية. فيما يلي سأعرض طريقة بسيطة للغاية لكيفية تنفيذ ذلك.
أسلوبي البسيط
الجوهرفي بعض المناهج ، يتم تعريف فئة الكيان ، في البعض الآخر ، تعمل مع الكيانات كمعرف / مقبض. في نهج المكون ، الكيان ليس سوى المكونات المرتبطة به ، ولهذا لا يلزم وجود فئة. سيكون الكيان موجودًا بشكل صريح بناءً على مكوناته ذات الصلة. للقيام بذلك ، قم بتحديد:
using EntityID = int64_t;
مكونات الكيانالمكونات هي أنواع مختلفة من البيانات المرتبطة بالكيانات الموجودة. يمكننا أن نقول أنه بالنسبة لكل كيان e ، سيكون لدى e أنواع صفر وأكثر سهولة من المكونات. في الجوهر ، هذه علاقة قيمة رئيسية انفجرت ، ولحسن الحظ ، هناك أدوات مكتبة قياسية في شكل بطاقات لهذا الغرض.
لذلك ، أحدد المكونات على النحو التالي:
struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; };
هذا يكفي للإشارة إلى الكيانات من خلال المكونات ، كما هو متوقع من ECS. على سبيل المثال ، لإنشاء كيان له موقع وصحة ، ولكن بدون سرعة ، تحتاج إلى:
لتدمير كيان بمعرف معين ، نحن ببساطة
.erase()
من كل بطاقة.
الأنظمةالعنصر الأخير الذي نحتاجه هو الأنظمة. هذا هو المنطق الذي يعمل مع المكونات لتحقيق سلوك معين. نظرًا لأنني أحب تبسيط الأشياء ، فأنا أستخدم الوظائف العادية. قد يكون نظام التجديد الصحي المذكور أعلاه ببساطة هو الوظيفة التالية.
void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } }
يمكننا وضع المكالمة لهذه الوظيفة في مكان مناسب في الحلقة الرئيسية ونقلها إلى تخزين المكون الصحي. نظرًا لأن المستودع الصحي لا يحتوي إلا على السجلات الخاصة بالكيانات التي تتمتع بالصحة ، فيمكنها معالجتها بمعزل عن غيرها. وهذا يعني أيضًا أن الوظيفة تأخذ البيانات اللازمة فقط ولا تلمس غير ذي صلة.
ولكن ماذا لو كان النظام يعمل مع أكثر من مكون؟ قل نظامًا ماديًا يغير الموضع بناءً على السرعة. للقيام بذلك ، نحتاج إلى تقاطع جميع مفاتيح جميع أنواع المكونات المعنية وتكرارها فوق قيمها. في هذه المرحلة ، لم تعد المكتبة القياسية كافية ، ولكن كتابة المساعدين ليست صعبة للغاية. على سبيل المثال:
void updatePhysics(Positions& positions, const Velocities& velocities) {
أو يمكنك كتابة مساعد أكثر إحكاما يسمح بوصول أكثر كفاءة عبر التكرار بدلا من البحث.
void updatePhysics(Positions& positions, const Velocities& velocities) {
وبالتالي ، فقد أطلعنا على الوظائف الأساسية لنظام ECS العادي.
الفوائد
هذا النهج فعال للغاية ، لأنه مبني من الصفر دون تقييد التجريد. لا يتعين عليك دمج المكتبات الخارجية أو تكييف قاعدة التعليمات البرمجية مع الأفكار المحددة مسبقًا لما يجب أن تكون عليه الكيانات / المكونات / الأنظمة.
وبما أن هذا النهج شفاف تمامًا ، يمكنك على أساسه إنشاء أي أدوات مساعدة ومساعدين. ينمو هذا التنفيذ مع احتياجات مشروعك. على الأرجح ، بالنسبة للنماذج الأولية أو الألعاب البسيطة في لعبة Jam'ov ، سيكون لديك ما يكفي من الوظائف الموضحة أعلاه.
وبالتالي ، إذا كنت جديدًا في مجال ECS بأكمله ، فإن مثل هذا النهج المباشر سيساعد على فهم الأفكار الرئيسية.
القيود
ولكن ، كما هو الحال مع أي طريقة أخرى ، هناك بعض القيود. في تجربتي ، فإن هذا التنفيذ بالتحديد باستخدام
unordered_map
في أي لعبة غير تافهة سيؤدي إلى مشاكل في الأداء.
لا يتم قياس تقاطعات المفاتيح الرئيسية في مثيلات متعددة من
unordered_map
مع كيانات متعددة بشكل جيد لأنك تجري في الواقع عمليات بحث
N*M
، حيث N هو عدد المكونات المتداخلة ، و M هو عدد الكيانات المطابقة ، و
unordered_map
ليست جيدة جدًا في التخزين المؤقت. يمكن إصلاح هذه المشكلة عن طريق استخدام مخزن قيمة مفتاح أكثر ملاءمة للتكرار بدلاً من
unordered_map
.
قيود أخرى هي الغلاية. اعتمادًا على ما تفعله ، يمكن أن يصبح تحديد المكونات الجديدة مملاً. قد تحتاج إلى إضافة إعلان ليس فقط في بنية المكونات ، ولكن أيضًا في وظيفة التفرخ ، والتسلسل ، وأداة التصحيح ، وما إلى ذلك. لقد صادفت هذا بنفسي وقمت بحل المشكلة من خلال إنشاء التعليمات البرمجية - حددت المكونات في ملفات json الخارجية ، ثم أنشأت مكونات C ++ ووظائف المساعدة في مرحلة البناء. أنا متأكد من أنه يمكنك العثور على طرق أخرى تستند إلى القوالب لإصلاح أي مشاكل في اللوحة الأساسية التي تواجهها.
النهاية
إذا كانت لديك أسئلة وتعليقات ، يمكنك تركها هنا أو الذهاب إلى
درس مفتوح مع
ديما ، والاستماع إليه ، والاستفسار بالفعل.