تحت غطاء محرك السيارة سكريبس - الافتراضية في رمل MMO للمبرمجين

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


ولكن دعونا نتحدث عن كل شيء بالترتيب.


الخلفية


هل تحب البرمجة؟ ليس الترميز المؤسسي الروتيني الذي يجبر الكثير منا على القيام به 40 ساعة في الأسبوع ، ويكافح من أجل التسويف ، ويصب في لتر من القهوة ويحترق مهنًا ؛ والبرمجة هي عملية سحرية لا تضاهى لتحويل الأفكار إلى برنامج عمل ، والحصول على المتعة من حقيقة أن الكود الذي كتبته للتو يتم تجسيده على الشاشة ويبدأ في عيش الحياة التي يخبرها بها المنشئ. في مثل هذه اللحظات ، أريد أن أكتب كلمة "Creator" بحرف كبير - مثل هذا الشعور الذي ينشأ في هذه العملية يكون في بعض الأحيان أقرب إلى الخشوع.



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


نعم ، إنها ألعاب عبر الإنترنت غالبًا ما توفر مصدر إلهام لا ينضب للمبرمجين. حتى الألعاب الأولى في هذا النوع (Ultima Online، Everquest ، ناهيك عن جميع أنواع الطين) جذبت العديد من الحرفيين المهتمين ليس كثيرًا بلعب الدور والاستمتاع بخيال العالم ، ولكن في تطبيق مواهبهم لأتمتة كل شيء وكل شيء في مساحة الألعاب الافتراضية. وحتى هذا اليوم ، يظل هذا مجالًا خاصًا بأولمبياد MMO على الإنترنت: صقل رأيك في كتابة روبوتك بحيث لا يلاحظه أحد من الإدارة ويحصل على أقصى ربح مقارنة بالاعبين الآخرين. أو روبوتات أخرى - كما هو الحال ، على سبيل المثال ، في EVE Online ، حيث التداول في الأسواق المكتظة بالسكان أقل قليلاً من السيطرة الكاملة على النصوص البرمجية للتداول ، تمامًا مثل البورصات الحقيقية.


فكرة لعبة على الإنترنت ، في البداية وموجهة بالكامل للمبرمجين ، تحوم في الهواء. هذه اللعبة التي تكون فيها كتابة روبوت ليست فعلًا يعاقب عليها ، لكنها جوهر اللعبة. حيث لا تكون المهمة هي تنفيذ نفس الإجراءات "وحوش Kill X والعثور على عناصر Y" من وقت لآخر ، ولكن لكتابة برنامج نصي يمكنه تنفيذ هذه الإجراءات نيابة عنك بشكل صحيح. ونظرًا لأنه ينطوي على لعبة على الإنترنت في النوع MMO ، فإن التنافس يحدث مع نصوص اللاعبين الآخرين في الوقت الفعلي في عالم ألعاب مشترك واحد.


لذلك في عام 2014 ، ظهرت لعبة Screeps (من الكلمات "Scripts" و "creeps") - رمل MMO إستراتيجي في الوقت الحقيقي مع عالم واحد كبير ثابت لا يكون للاعبين فيه أي تأثير على ما يحدث باستثناء كتابة برامج AI لوحدات اللعبة الخاصة بهم . جميع آليات اللعبة الإستراتيجية العادية - استخراج الموارد ، وبناء الوحدات ، وبناء قاعدة ، والاستيلاء على الأراضي ، والتصنيع والتجارة - يحتاج اللاعب نفسه إلى البرمجة من خلال واجهة برمجة تطبيقات JavaScript التي يوفرها عالم اللعبة. الفرق بين المسابقات المختلفة في كتابة الذكاء الاصطناعى هو أن عالم اللعبة ، كما ينبغي أن يكون في عالم الألعاب عبر الإنترنت ، يعمل باستمرار ويعيش حياته في الوقت الحقيقي على مدار الساعة طوال الأسبوع طوال الـ 4 سنوات الماضية ، بإطلاق الذكاء الاصطناعي لكل لاعب في كل دورة لعبة.


لذلك ، يكفي عن اللعبة نفسها - يجب أن يكون هذا كافياً لزيادة فهم جوهر المشكلات الفنية التي واجهناها أثناء التطوير. يمكنك الحصول على المزيد من المشاهدات من هذا الفيديو ، ولكن هذا اختياري:


مقطورة فيديو

القضايا الفنية


جوهر ميكانيكا عالم اللعبة هو كما يلي: ينقسم العالم كله إلى غرف متصلة ببعضها البعض عبر أربع نقاط أساسية. غرفة واحدة هي وحدة ذرية لعملية معالجة حالة العالم اللعبة. قد تحتوي الغرفة على كائنات معينة (على سبيل المثال ، وحدات) لها حالة خاصة بها ، وفي كل خطوة من خطوات اللعبة يتلقون أوامر من اللاعبين. يأخذ معالج الخادم غرفة واحدة في كل مرة ، وينفذ هذه الأوامر ، ويغير حالة الكائنات ، ويلزم الحالة الجديدة للغرفة بقاعدة البيانات. يتم تحجيم هذا النظام بشكل أفقي: يمكنك إضافة المزيد من المعالجات إلى الكتلة ، وبما أن الغرف معزولة معمارياً عن بعضها البعض ، حيث يمكن معالجة العديد من الغرف بالتوازي مع وجود معالجات قيد التشغيل.



في الوقت الحالي ، لدينا 42،060 غرفة في اللعبة. تحتوي مجموعة الخوادم المكونة من 36 جهازًا رباعي النواة على 144 معالجات. نستخدم Redis لإنشاء قوائم الانتظار ، يتم كتابة الخلفية بالكامل في Node.js.


كانت هذه مرحلة واحدة من براعة اللعبة. لكن من أين تأتي فرق اللاعبين؟ تفاصيل اللعبة هي أنه لا توجد واجهة يمكنك من خلالها النقر فوق وحدة ما وإخباره بالانتقال إلى نقطة معينة أو إنشاء بنية محددة. الحد الأقصى الذي يمكن القيام به في الواجهة هو وضع علامة غير ملموسة في المكان المناسب في الغرفة. لكي تأتي الوحدة إلى هذا المكان وتتخذ الإجراء اللازم ، من الضروري أن يقوم البرنامج النصي الخاص بك بشيء مثل التالي لعدة علامات لعب:


module.exports.loop = function() { let creep = Game.creeps['Creep1']; let flag = Game.flags['Flag1']; if(!creep.pos.isEqualTo(flag.pos)) { creep.moveTo(flag.pos); } } 

اتضح أنه في كل خطوة من خطوات اللعبة تحتاج إلى أخذ وظيفة loop اللاعب وتنفيذه في بيئة جافا سكريبت كاملة لهذا اللاعب المعين (حيث يوجد كائن Game له) ، والحصول على مجموعة من الطلبات للوحدات ، ومنحهم مرحلة المعالجة التالية. يبدو أن كل شيء بسيط للغاية.



تبدأ المشاكل عندما يتعلق الأمر بفوارق التنفيذ. في الوقت الحالي ، لدينا 1600 لاعب نشط في العالم. لا يمكن بالفعل تسمية البرامج النصية للاعبين الفرديين بـ "البرامج النصية" - بعضها يحتوي على ما يصل إلى 25 ألف سطر من التعليمات البرمجية ، ويتم تجميعها من TypeScript أو حتى من C / C ++ / Rust عبر WebAssembly (نعم ، نحن ندعم wasm!) ، وننفذ مفهوم أنظمة التشغيل المصغرة الحقيقية ، حيث طور اللاعبون مجموعتهم الخاصة من مهام مهام اللعبة وإدارتها من خلال جوهرها ، والتي تأخذ العديد من المهام كما يتضح أنها تؤدي أدائها في براعة معينة من اللعبة ، وتنفذها ، وتعيدها إلى قوائم الانتظار حتى التدبير التالي. نظرًا لأن وحدة المعالجة المركزية وذاكرة المشغل محدودة في كل دورة على مدار الساعة ، فإن هذا الطراز يعمل جيدًا. على الرغم من أنه ليس من الضروري - لبدء اللعبة ، يكفي أن يأخذ المبتدئين نصًا يتكون من 15 سطرًا ، والذي تمت كتابته أيضًا بالفعل كجزء من البرنامج التعليمي.


ولكن الآن دعنا نتذكر أن البرنامج النصي للاعب يجب أن يعمل في جهاز جافا سكريبت حقيقي. وأن اللعبة تعمل في الوقت الفعلي - أي أن جهاز JavaScript لكل لاعب يجب أن يكون موجودًا باستمرار ، ويعمل بوتيرة معينة ، حتى لا يبطئ اللعبة ككل. تعمل مرحلة تنفيذ البرامج النصية للألعاب وتكوين الطلبات للوحدات تقريبًا على نفس مبدأ معالجة الغرف - فكل برنامج نصي للاعب يعد مهمة يقوم بها معالج واحد من المجموعة ، ويعمل الكثير من المعالجين المتوازيين في المجموعة. ولكن على عكس مرحلة غرف المعالجة ، هناك بالفعل الكثير من الصعوبات.


أولاً ، لا يمكنك فقط توزيع المهام عن طريق المعالجات بشكل عشوائي في كل دورة من ساعات الساعة ، حيث يمكن القيام بذلك في حالة الغرف. يجب أن يعمل جهاز جافا سكريبت الخاص بالمشغل دون مقاطعة ، كل إجراء تالٍ هو مجرد استدعاء دالة loop جديدة ، ولكن يجب أن يظل السياق العالمي كما هو. من الناحية التقريبية ، تتيح لك اللعبة القيام بشيء مثل هذا:


 let counter = 0; let song = ['EX-', 'TER-', 'MI-', 'NATE!']; module.exports.loop = function () { Game.creeps['DalekSinger'].say(song[counter]); counter++; if(counter == song.length) { counter = 0; } } 


مثل هذا الزحف سوف يغني على سطر واحد من كل أغنية تغلب عليها. يتم تخزين رقم سطر counter الأغنية في سياق عمومي يتم تخزينه بين المقاييس. إذا تم تنفيذ البرنامج النصي لهذا المشغل في كل مرة في عملية معالج جديد ، فسيتم فقد السياق. هذا يعني أنه يجب تخصيص جميع اللاعبين لمعالجات محددة ، ويجب تغييرها بأقل قدر ممكن. ولكن ماذا عن موازنة الحمل؟ يمكن أن ينفق أحد اللاعبين 500 مللي ثانية من التنفيذ على هذه العقدة ، ويمكن للاعب الآخر أن ينفق 10 مللي ثانية ، ومن الصعب للغاية التنبؤ بذلك مسبقًا. إذا وقع 20 لاعبًا لكل 500 مللي ثانية على عقدة واحدة ، فإن تشغيل هذه العقدة سيستغرق 10 ثوانٍ ، ينتظر خلالها جميع اللاعبين الآخرين اكتمالها ويظلون في وضع الخمول. ومن أجل إعادة التوازن لهؤلاء اللاعبين ورميهم إلى العقد الأخرى ، عليك أن تفقد سياقها.


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


في محاولة للتعامل مع هذه المشاكل ، توصلنا إلى عدة حلول.


الإصدار الأول


استند الإصدار الأول من محرك اللعبة إلى شيئين أساسيين:


  • وحدة vm بدوام كامل في تسليم Node.js ،
  • شوكة عمليات التشغيل.

بدا مثل هذا. على كل جهاز في المجموعة ، كانت هناك 4 عمليات (وفقًا لعدد المراكز) لمعالجات البرامج النصية للألعاب. عندما تم استلام مهمة جديدة من قائمة انتظار البرامج النصية للعبة ، طلب المعالج البيانات الضرورية من قاعدة البيانات ونقلها إلى العملية الفرعية ، والتي تم الحفاظ عليها في حالة تشغيل مستمر ، وأعيد تشغيلها في حالة حدوث فشل ، وتم إعادة استخدامها من قبل لاعبين مختلفين. العملية الفرعية ، التي يتم عزلها عن الأصل (والتي تحتوي على منطق أعمال نظام المجموعة) ، يمكنها أن تفعل شيئًا واحدًا فقط: إنشاء كائن Game من البيانات المستلمة وبدء تشغيل الجهاز الافتراضي للاعب. للبدء ، استخدمنا وحدة vm في Node.js.


لماذا كان هذا القرار غير كامل؟ بالمعنى الدقيق للكلمة ، لم يتم حل المشاكل المذكورة أعلاه اثنين هنا.


يعمل vm في نفس وضع المفرد مترابطة كـ Node.js. لذلك ، من أجل الحصول على أربعة معالجات متوازية على كل نواة على جهاز 4 النواة ، تحتاج إلى 4 عمليات. يؤدي نقل لاعب "حي" في عملية إلى عملية أخرى إلى إعادة إنشاء السياق العالمي بالكامل ، حتى لو حدث ذلك داخل نفس الجهاز.



بالإضافة إلى ذلك ، لا تنشئ vm جهازًا ظاهريًا معزولًا تمامًا. ما تقوم به هو فقط إنشاء سياق أو نطاق معزول ، ولكن تنفيذ التعليمات البرمجية في نفس مثيل الجهاز الظاهري JavaScript ، حيث vm.runInContext مكالمة vm.runInContext . وهذا يعني - في نفس الحالة التي يتم فيها إطلاق لاعبين آخرين. على الرغم من أن اللاعبين مفصولين عن طريق سياقات عالمية معزولة ، لكن كونهم جزءًا من نفس الجهاز الظاهري ، فإن لديهم ذاكرة مشتركة ، ومجمع جامع للقمامة مشتركًا ، ويقومون بإنشاء النفايات معًا. إذا قام اللاعب "A" بإنشاء الكثير من البيانات المهملة أثناء تنفيذ البرنامج النصي للعبة ، والانتهاء من العمل ، والتحكم الذي تم تمريره إلى اللاعب "B" ، في هذه اللحظة قد يتم جمع جميع البيانات المهملة ، وسيقوم اللاعب "B" بدفع وقت وحدة المعالجة المركزية لجمع القمامة لشخص آخر. ناهيك عن حقيقة أن جميع السياقات تعمل في حلقة الحدث نفسها ، ومن الناحية النظرية من الممكن تنفيذ وعد شخص آخر في أي وقت ، على الرغم من أننا حاولنا منع ذلك. أيضًا ، لا يسمح لك vm بالتحكم في مقدار ذاكرة التخزين المؤقت المخصصة لتنفيذ البرنامج النصي ، كل ذاكرة العملية متوفرة.


معزولة vm


هناك يعيش مثل هذا الشخص الرائع اسمه مارسيل لافيردي. بالنسبة للبعض ، أصبح ذات يوم رائعًا لكتابة مكتبة لألياف العقدة ، وللآخرين ، لاختراق موقع Facebook وتم التعاقد معه للعمل هناك . وهو رائع بالنسبة لنا لأنه شارك بسخاء في حملتنا الأولى للتمويل الجماعي ، وحتى يومنا هذا ، إنه معجب كبير بـ Screeps.


مشروعنا مفتوح المصدر منذ عدة سنوات - يتم نشر خادم اللعبة على جيثب. على الرغم من أن العميل الرسمي يباع مقابل رسوم عبر Steam ، إلا أن هناك نسخًا بديلة منه ، والخادم نفسه متاح للدراسة والتعديل على أي نطاق ، وهو ما نشجعه بشدة.


وبمجرد إرسال مارسيل إلينا: "شباب ، لدي خبرة جيدة في تطوير C / C ++ الأصلي لـ Node.js ، وأنا أحب لعبتك ، لكن لا يحب الجميع كيف تعمل - دعنا نكتب واحدة جديدة تقنية إطلاق الجهاز الظاهري لـ Node.js خصيصًا لـ Screeps؟ "


بما أن مارسيل لم يطلب المال ، لم نتمكن من الرفض. بعد عدة أشهر من تعاوننا ، ولدت مكتبة معزولة . وهذا تغير كل شيء على الاطلاق.


يختلف isolated-vm عن vm لأنه لا يعزل السياق ، ولكن يعزل من حيث V8 . دون الخوض في التفاصيل ، هذا يعني أنه يتم إنشاء مثيل منفصل كامل لجهاز JavaScript ، والذي لا يحتوي فقط على سياقه العالمي الخاص ، بل وأيضاً ذاكرة كومة الذاكرة المؤقتة الخاصة به وجامع البيانات المهملة ويعمل كجزء من حلقة أحداث منفصلة. من بين السلبيات: لكل جهاز يعمل ، يلزم وجود حمولة صغيرة من ذاكرة الوصول العشوائي (حوالي 20 ميغابايت) ، ومن المستحيل أيضًا نقل الكائنات أو استدعاء وظائف مباشرة إلى الجهاز ، يجب إجراء تسلسل كامل للتبادل. هذا ينهي السلبيات ، الباقي - إنه مجرد علاج شافي!



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


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


بعد تشغيل هذه الوظيفة في الوضع التجريبي ، تلقينا عددًا كبيرًا من ردود الفعل الإيجابية من اللاعبين الذين بدأت نصوصهم في العمل بشكل أفضل وأكثر استقرارًا وأكثر قابلية للتنبؤ. والآن هذا هو محركنا الافتراضي ، على الرغم من أنه لا يزال بإمكان اللاعبين اختيار وقت التشغيل القديم بحتة للتوافق مع الإصدارات القديمة مع البرامج النصية القديمة (ركز بعض اللاعبين بوعي على تفاصيل البيئة المشتركة في اللعبة).


بالطبع ، لا يزال هناك مجال لمزيد من التحسين ، وهناك أيضًا مجالات أخرى مثيرة للاهتمام في المشروع حيث قمنا بحل العديد من المشكلات الفنية. ولكن المزيد عن ذلك وقت آخر.

Source: https://habr.com/ru/post/ar437836/


All Articles