ما هو تصميم محرك لعبة التوجه؟
() translation by (you can also view the original English article)
ربما سمعت عن تصميم محرك ألعاب موجه للبيانات ، وهو مفهوم جديد نسبيًا يقترح عقلية مختلفة للتصميم التقليدي الأكثر شيوعًا. في هذا المقال ، سأشرح ما هو DOD ، ولماذا يشعر بعض مطوري ألعاب اللعبة بأنه قد يكون بمثابة تذكرة لتحقيق مكاسب مذهلة في الأداء.
القليل من التاريخ
في السنوات الأولى من تطوير اللعبة ، كانت الألعاب ومحركاتها مكتوبة بلغات المدارس القديمة ، مثل C. لقد كانت منتجات متخصصة ، وكان عصر كل دورة على مدار الساعة من الأجهزة البطيئة ، في ذلك الوقت ، أولوية قصوى. في معظم الحالات ، لم يكن هناك سوى عدد متواضع من الأشخاص الذين يقومون باختراق شفرة عنوان واحد ، وكانوا يعلمون مصدر الكود بأكمله عن ظهر قلب. كانت الأدوات التي كانوا يستخدمونها تخدمهم بشكل جيد ، وكان C يوفر فوائد الأداء التي مكنتهم من دفع أقصى استفادة من وحدة المعالجة المركزية - وبما أن هذه الألعاب كانت لا تزال كبيرة مرتبطة بوحدة المعالجة المركزية (CPU) ، بالاعتماد على مخازن الإطار الخاصة بها ، كانت هذه نقطة مهمة جدا.
مع ظهور وحدات معالجة الرسومات (GPUs) التي تعمل على عمل المكوّنات ، مثل texels و pixels وما إلى ذلك ، أصبحنا نعتمد على وحدة المعالجة المركزية بشكل أقل. في الوقت نفسه ، شهدت صناعة الألعاب نمواً مطرداً: المزيد والمزيد من الناس يرغبون في لعب المزيد والمزيد من الألعاب ، وهذا بدوره أدى إلى المزيد والمزيد من الفرق معا لتطويرها.



فرق أكبر تحتاج إلى تعاون أفضل. قبل فترة طويلة ، تطلبت محركات اللعبة ، بمستوىها المعقد ، الذكاء الاصطناعي ، والإعدام ، والمنطق أن يكون المبرمجون أكثر انضباطا ، وكان سلاحهم المفضل هو التصميم الموجه للكائن.
في الشركات الكبيرة ، تميل البرامج إلى كتابة فرق كبيرة (ومتغيرة باستمرار) من المبرمجين المتوسطين. تفرض البرمجة الموجّهة للكائنات انضباطًا على هؤلاء المبرمجين يمنع أيًا منهم من فعل الكثير من الضرر.
سواء أحببنا ذلك أم لا ، يجب أن يكون هذا صحيحًا إلى حد ما ، حيث بدأت الشركات الكبرى في نشر ألعاب أكبر وأفضل ، ومع ظهور معايير الأدوات ، أصبح المتسللون الذين يعملون في الألعاب أجزاء يمكن تبديلها بسهولة أكبر. أصبحت فضيلة قرصنة معينة أقل أهمية.
مشاكل مع تصميم الكائن الموجه
في حين أن التصميم الموجه للكائنات هو مفهوم لطيف يساعد المطورين في المشاريع الكبيرة ، مثل الألعاب ، فيخلقون طبقات متعددة من التجريد ، ويعملون جميعًا على طبقاتهم المستهدفة ، دون الحاجة إلى الاهتمام بتفاصيل التنفيذ الخاصة بتلك الموجودة تحتها ، يعطينا بعض الصداع.
نحن نرى انفجارًا للبرمجة المتوازية - مبرمجون يحصدون كل نواة المعالج المتوفرة لتسريع سرعات الحوسبة - لكن في نفس الوقت ، تصبح مشهد اللعبة أكثر تعقيدًا ، وإذا أردنا مواكبة هذا الاتجاه وما زلنا نقدم الإطارات ثانيًا ، يتوقع لاعبونا ، نحن بحاجة إلى القيام بذلك أيضًا. باستخدام كل السرعة التي لدينا في متناول اليد ، يمكننا فتح الأبواب لإمكانيات جديدة تمامًا: باستخدام وقت وحدة المعالجة المركزية لتقليل عدد البيانات المرسلة إلى GPU تمامًا ، على سبيل المثال.
في البرمجة الموجهة للكائنات ، تحتفظ بحالة داخل كائن ، مما يتطلب منك إدخال مفاهيم مثل البدائل الأساسية للتزامن إذا كنت تريد العمل عليها من عدة سلاسل. لديك مستوى جديد واحد من indirection لكل مكالمة دالة ظاهرية تقوم بها. كما أن أنماط الوصول إلى الذاكرة التي تم إنشاؤها بواسطة الكود المكتوب بطريقة موجهة للكائنات يمكن أن تكون فظيعة - في الحقيقة ، مايك أكتون (ألعاب Insomniac ، ألعاب Rockstar سابقاً) لديها مجموعة كبيرة من الشرائح تشرح أحد الأمثلة.
وبالمثل ، فإن روبرت هاربر ، الأستاذ بجامعة كارنيجي ميلون ، يضع الأمر على هذا النحو:
البرمجة الموجهة للكائنات هي [...] على حد سواء المضادة للوحدات ومكافحة الموازية بطبيعتها ، وبالتالي غير مناسبة لمنهج CS الحديث.
الحديث عن OOP مثل هذا أمر صعب ، لأن OOP يشمل مجموعة كبيرة من الخصائص ، وليس الجميع يوافق على ما تعنيه OOP. وبهذا المعني، معظمها أتحدث عنه صافية كما تنفذها c + +، لأن ذلك هو حاليا باللغة التي تسيطر إلى حد كبير على العالم محرك اللعبة.
لذا ، نعلم أن الألعاب يجب أن تكون متوازية لأن هناك دائمًا المزيد من العمل الذي يمكن أن تقوم به وحدة المعالجة المركزية (ولكن ليس بالضرورة أن تفعله) ، كما أن إنفاق الدورات في انتظار معالجة GPU هو مجرد تبذير. كما نعلم أيضًا أن مقاربات تصميم OO الشائعة تتطلب منا إدخال منافسة تأمين باهظة الثمن ، وفي الوقت نفسه ، يمكن أن تنتهك مكان ذاكرة التخزين المؤقت أو تسبب تفرعات غير ضرورية (والتي يمكن أن تكون مكلفة!) في أكثر الظروف غير المتوقعة.



هذا يثير التساؤل: هل يجب علينا إعادة النظر في نماذجنا؟
أدخل: التصميم الموجه للبيانات
بعض مؤيدي هذه المنهجية أطلقوا عليها بيانات ، ولكن الحقيقة هي أن المفهوم العام قد عرف لفترة أطول. الفرضية الأساسية بسيطة: بناء التعليمات البرمجية الخاصة بك حول هياكل البيانات ، ووصف ما تريد تحقيقه من حيث التلاعب في هذه الهياكل.
لقد سمعنا هذا النوع من الحديث قبل: لينوس تورفالدس، منشئ لينكس، وبوابة، وقال في آخر قائمة البريدية بوابة أنه دعاة "تصميم التعليمات البرمجية حول البيانات، وليس العكس"، وائتمانات ضخمة هذا واحداً من أسباب النجاح لبوابة. يمضي حتى في الادعاء بأن الفرق بين مبرمج جيد وسوء هو ما إذا كانت تقلق بشأن هياكل البيانات ، أو الشفرة نفسها.
قد تبدو المهمة غير منطقية في البداية ، لأنها تتطلب منك تحويل نموذجك العقلي رأسًا على عقب. لكن فكر في الأمر على هذا النحو: لعبة ، أثناء تشغيلها ، تلتقط جميع مدخلات المستخدم ، وجميع الأجزاء الثقيلة من الأداء (تلك التي يكون من المنطقي فيها التخلص من كل شيء قياسي هو الكائن فلسفة) لا تعتمد على الخارج عوامل ، مثل الشبكة أو IPC. بالنسبة إلى كل ما تعرفه ، تستهلك إحدى الألعاب أحداث المستخدم (تم تحريك الماوس ، وضغط زر جويستيك ، وما إلى ذلك) وحالة اللعبة الحالية ، وتحويلها إلى مجموعة جديدة من البيانات - على سبيل المثال ، الدفعات التي يتم إرسالها إلى وحدة معالجة الرسومات ، عينات PCM التي يتم إرسالها إلى بطاقة الصوت وحالة لعبة جديدة.
يمكن تقسيم هذا "متماوج البيانات" إلى المزيد من العمليات الفرعية. يأخذ نظام الرسوم المتحركة بيانات الإطار الرئيسي التالية والحالة الحالية وينتج حالة جديدة. يأخذ نظام الجسيمات حالته الحالية (مواقع الجسيمات ، السرعات ، وهكذا) والتقدم الزمني وينتج حالة جديدة. تأخذ خوارزمية الاستبعاد مجموعة من الترشيقات المرشحة وتنتج مجموعة أصغر من مواد التجسيد. يمكن اعتبار كل شيء تقريبًا في محرك اللعبة بمثابة معالجة لجزء من البيانات لإنتاج جزء آخر من البيانات.
المعالجات تحب محلة المرجع واستخدام ذاكرة التخزين المؤقت. لذا ، في التصميم الموجه نحو البيانات ، نميل إلى تنظيم كل شيء في مجموعات كبيرة ومتجانسة ، وحيثما أمكن ، تشغيل خوارزميات جيدة ذات قوة مخبأة متماسكة مخبأة بدلاً من خوارزميات محتملة التخمين (التي تحتوي على أفضل تكلفة كبيرة O ، لكنه يفشل في احتضان قيود العمارة من الأجهزة التي يعمل عليها).
عند إجراء كل إطار (أو عدة مرات لكل إطار) ، فمن المحتمل أن يؤدي ذلك إلى تقديم مكافآت هائلة للأداء. على سبيل المثال ، يقوم الأشخاص في Scalyr بالبحث في ملفات سجلات الدخول بسرعة 20 غيغابايت / ثانية باستخدام مسح خطي يعمل بطريقة حذرة ساذجة ولكن ساذجة.



مثال
إن التصميم الموجه نحو البيانات يفرض علينا التفكير في كل البيانات ، لذلك دعونا نفعل شيئًا مختلفًا بعض الشيء عما نفعله عادة. ضع في اعتبارك جزء الرمز هذا:
1 |
void MyEngine::queueRenderables() |
2 |
{
|
3 |
for (auto it = mRenderables.begin(); it != mRenderables.end(); ++it) { |
4 |
if ((*it)->isVisible()) { |
5 |
queueRenderable(*it); |
6 |
}
|
7 |
}
|
على الرغم من أنها مبسطة كثيرًا ، إلا أن هذا النمط الشائع هو ما يُشاهد عادةً في محركات الألعاب الموجهة للكائنات. لكن انتظر - إذا لم يكن الكثير من المخطوطات مرئيًا فعليًا ، فإننا نتعرض إلى الكثير من سوء التفسير للفرع مما يجعل المعالج يلقي بعض الإرشادات التي نفذها على أمل أن يتم أخذ فرع معين.
للمشاهد الصغيرة ، من الواضح أن هذا ليس مشكلة. ولكن كم عدد المرات التي تقوم فيها بهذا الأمر ، ليس فقط عند وضع الطابور ، ولكن عند التكرار من خلال أضواء المشهد ، أو خريطة الظل ، أو المناطق ، أو ما شابه؟ ماذا عن منظمة العفو الدولية أو تحديثات الرسوم المتحركة؟ اضرب كل ما تفعله طوال المشهد ، وشاهد عدد دورات الساعة التي تطلقها ، وحساب مقدار الوقت المتاح للمعالج لتوصيل جميع مجموعات GPU للحصول على إيقاع ثابت بمقدار 120FPS ، وترى أن هذه الأشياء يمكن أن تتدرج إلى حد كبير.
سيكون من المضحك إذا ، على سبيل المثال ، أن أحد المتسللين الذين يعملون على تطبيق ويب قد نظروا في مثل هذه التحسينات الدقيقة الصغيرة ، لكننا نعرف أن الألعاب عبارة عن أنظمة في الوقت الحقيقي حيث تكون قيود الموارد ضيقة بشكل لا يصدق ، لذا فإن هذا الاعتبار ليس في غير محله بالنسبة لنا.
لتجنب حدوث ذلك ، دعنا نفكر في الأمر بطريقة أخرى: ماذا لو احتفظنا بقائمة التوضيحات المرئية في المحرك؟ بالتأكيد ، سنضحي ببناء الجملة النظيف لـ myRenerable-> hide()
وننتهك عددًا قليلاً من مبادئ OOP ، ولكن يمكننا القيام بذلك بعد ذلك:
1 |
void MyEngine::queueRenderables() |
2 |
{
|
3 |
for (auto it = mVisibleRenderables.begin(); it != mVisibleRenderables.end(); ++it) { |
4 |
queueRenderable(*it); |
5 |
}
|
6 |
}
|
الصيحة! لا توجد أي أخطاء في حسابات الفروع ، وعلى افتراض أن mVisibleRenderables
عبارة عن std :: vector
(وهو عبارة عن مصفوفة متجاورة) ، فقد كان من الممكن إعادة كتابتها كنوع استدعاء سريع memcpy
(مع بعض التحديثات الإضافية على هياكل البيانات الخاصة بنا ، على الأرجح).
الآن ، يمكنك الاتصال بي على الجبن المطلق لهذه العينات رمز وسوف تكون على حق تماما: يتم تبسيط هذا كثيرا. ولكن لكي أكون صريحًا ، لم أخدش السطح حتى الآن. التفكير في هياكل البيانات وعلاقاتها يفتح لنا الكثير من الاحتمالات التي لم نفكر بها من قبل. دعونا ننظر في بعض منهم المقبل.
الموازية و Vectorization
إذا كان لدينا وظائف بسيطة ومحددة بشكل جيد تعمل على أجزاء كبيرة من البيانات كبنيات أساسية للمعالجة ، فمن السهل إنتاج أربعة أو ثمانية أو 16 مؤشرًا عاملاً وإعطاء كل واحد منها بيانات للاحتفاظ بكل وحدة المعالجة المركزية النوى مشغول. لا توجد كائنات تزيينية ، أو متضاربة ، أو تعارض في القفل ، وبمجرد احتياجك للبيانات ، فإنك تحتاج فقط إلى الانضمام إلى جميع خيوط المناقشة والانتظار حتى تنتهي. إذا كنت بحاجة إلى فرز البيانات بالتوازي (وهي مهمة متكررة جدًا عند إعداد الأشياء لإرسالها إلى وحدة معالجة الرسوميات) ، فيجب التفكير في ذلك من منظور مختلف - قد تساعد هذه الشرائح.
كمكافأة إضافية ، داخل موضوع واحد يمكنك استخدام تعليمات ناقل SIMD (مثل SSE/SSE2/SSE3) لتحقيق زيادة سرعة إضافية. في بعض الأحيان ، لا يمكنك إنجاز ذلك إلا من خلال وضع البيانات الخاصة بك بطريقة مختلفة ، مثل وضع صفائف المتجه في بنية من- المصفوفات (SoA) بطريقة (مثل XXX ... YYY ... ZZZ ...
) بدلاً من مجموعة الهياكل التقليدية (AoS ، التي من شأنها أن تكون XYZXYZXYZ ...
). أنا بالكاد خدش السطح هنا ؛ يمكنك العثور على مزيد من المعلومات في قسم القراءة الإضافية أدناه.



اختبار وحدة كنت لا تعرف كان ممكن
إن وجود وظائف بسيطة بدون أي تأثيرات خارجية يجعلها سهلة لاختبار الوحدة. قد يكون هذا جيدًا بشكل خاص في شكل اختبار الانحدار للخوارزميات التي ترغب في تبديلها وإخراجها بسهولة.
على سبيل المثال ، يمكنك إنشاء مجموعة اختبار لسلوك خوارزمية الاستبعاد ، وإعداد بيئة منسقة ، وقياس كيفية أدائها بالضبط. عند إنشاء خوارزمية جديدة ، يمكنك تشغيل الاختبار نفسه مرة أخرى دون أي تغييرات. يمكنك قياس الأداء والصواب ، بحيث يمكنك إجراء تقييم في متناول يديك.
كلما حصلت على المزيد من مناهج التصميم الموجه نحو البيانات ، ستجد أنه من الأسهل والأسهل اختبار جوانب محرك اللعبة.
الجمع بين الطبقات والكائنات مع البيانات متجانسة
لا يتعارض التصميم الموجه نحو البيانات بأي شكل مع البرمجة الشيئية ، بل بعض أفكارها. ونتيجة لذلك ، يمكنك استخدام الأفكار بدقة من التصميم الموجه نحو البيانات ولا تزال تحصل على معظم النماذج التجريدية والنفسية التي اعتدت عليها.
إلقاء نظرة ، على سبيل المثال ، في العمل على الإصدار 2.0 من OGRE: اختار ماتياس غولدبرغ ، العقل المدبر وراء هذا المسعى ، تخزين البيانات في صفائف كبيرة ومتجانسة ، وله وظائف تتكرر على صفائف كاملة بدلاً من العمل على مسند واحد فقط ، من أجل تسريع الغول. وفقا لمعيار (الذي يعترف هو غير عادل للغاية ، ولكن ميزة الأداء قياس لا يمكن أن يكون فقط بسبب ذلك) أنه يعمل الآن ثلاث مرات أسرع. ليس هذا فقط - احتفظ بالكثير من التجريدات القديمة المألوفة ، لذا كانت API بعيدة عن إعادة كتابة كاملة.
هل من العملي؟
هناك الكثير من الأدلة على أنه يمكن تطوير محركات اللعبة بهذه الطريقة.
تحتوي مدونة تطوير Molecule Engine على سلسلة تسمى Adventures in Data-Oriented Design ، وتحتوي على الكثير من النصائح المفيدة حول مكان وضع DOD مع نتائج رائعة.
يبدو أن DICE مهتمة بالتصميم الموجه للبيانات ، حيث أنها تستخدمه في نظام إطفاء Frostbite Engine (وقد حصلت على قدر كبير من السرعة ، أيضًا!). وتتضمن بعض الشرائح الأخرى أيضًا استخدام تصميم موجه للبيانات في النظام الفرعي لـ AI - يستحق النظر إليه أيضًا.
بالإضافة إلى ذلك ، يبدو أن المطورين مثل Mike Acton المذكور أعلاه يحتضنون هذا المفهوم. هناك بعض المعايير التي تثبت أنها تحقق الكثير في الأداء ، ولكني لم أشاهد الكثير من النشاط على جبهة التصميم الموجهة للبيانات في وقت ما. بالطبع ، يمكن أن تكون مجرد موضة ، لكن مقرها الرئيسي يبدو منطقياً للغاية. من المؤكد أن هناك الكثير من القصور الذاتي في هذا العمل (وأي أعمال تطوير برامج أخرى ، لهذا الأمر) لذا قد يعيق ذلك تبني مثل هذه الفلسفة على نطاق واسع. أو ربما ليست فكرة رائعة كما يبدو. ماذا تعتقد؟ التعليقات هي موضع ترحيب كبير!
قراءة متعمقة
- تصميم موجه للبيانات (أو لماذا قد تصوِّر نفسك في القدم باستخدام OOP)
- مقدمة للتصميم الموجه للبيانات [DICE]
- مناقشة لطيفة على Stack Overflow
- كتاب على الإنترنت من قبل ريتشارد فابيان يشرح الكثير من المفاهيم
- مؤشر يوضح جانبًا آخر من القصة ، وهي نتيجة غير بديهية على ما يبدو
- استعراض مايك Acton OgreNode.cpp ، وكشف عن بعض مطبات لعبة محرك OOP المشتركة