مطلق النار شبكة المتصفح على Node.js

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

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


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

عند إنشاء اللعبة ، استخدمت الموارد الرسومية من Kenney's Pirate Pack وإطار لعبة Phaser . في هذا البرنامج التعليمي ، يتم تعيين دور مبرمج الشبكة. ستكون نقطة البداية هي إصدار مستخدم واحد يعمل بكامل طاقته من اللعبة ، وستكون مهمتنا هي كتابة خادم على Node.js باستخدام Socket.io لجزء الشبكة. لكي لا أفرط في تحميل البرنامج التعليمي ، سأركز على الأجزاء المتعلقة بالمفاهيم متعددة اللاعبين وتخطي المفاهيم المتعلقة بـ Phaser و Node.js.

لا تحتاج إلى تكوين أي شيء محليًا ، لأننا سنقوم بإنشاء هذه اللعبة بالكامل في المتصفح على Glitch.com ! Glitch هي أداة رائعة لبناء تطبيقات الويب ، بما في ذلك الخلفيات ، وقواعد البيانات ، والمزيد. إنه لأمر رائع بالنسبة للنماذج الأولية والتدريب والتعاون ، ويسعدني جدًا أن أقدم لكم قدراته في هذا البرنامج التعليمي.

دعنا نبدأ.

1. التحضير


لقد نشرت مسودة المشروع على Glitch.com .

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


يحتوي الشريط الجانبي العمودي على اليسار على جميع ملفات التطبيق. لتحرير هذا التطبيق ، يجب عليك إنشاء "ريمكس". لذا سنقوم بإنشاء نسخة منه في حسابنا (أو "شوكة" في المصطلحات git). انقر فوق ريمكس هذا الزر.


في هذه المرحلة ، تقوم بتحرير التطبيق تحت حساب مجهول. لحفظ عملك ، يمكنك تسجيل الدخول (أعلى اليمين).

الآن ، قبل الانتقال ، من المهم بالنسبة لك التعرف على اللعبة التي سنضيف فيها وضعًا متعدد اللاعبين. ألق نظرة على index.html . يحتوي على ثلاث وظائف مهمة تحتاج إلى معرفتها: preload (السطر 99) ، create (السطر 115) و GameLoop (السطر 142) ، بالإضافة إلى كائن المشغل (السطر 35).

إذا كنت تفضل التعلم من خلال الممارسة ، فتأكد من فهم عمل اللعبة من خلال إكمال المهام التالية:

  • قم بزيادة حجم العالم (السطر 29) - لاحظ أن هناك حجمًا عالميًا منفصلًا للعالم داخل اللعبة وحجم نافذة للوحة الصفحة نفسها .
  • اجعل من الممكن المضي قدما بمساعدة "الفضاء" (السطر 53).
  • تغيير نوع سفينة اللاعب (السطر 129).
  • إبطاء حركة القذائف (الخط 155).

قم بتثبيت Socket.io


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

أول شيء يتعين علينا القيام به هو تثبيت وحدة Socket.io. في Glitch ، يمكن القيام بذلك عن طريق الانتقال إلى ملف package.json ، ثم إما إدخال الوحدة النمطية المطلوبة في التبعيات ، أو النقر فوق إضافة حزمة وإدخال "socket.io".


الآن هو الوقت المناسب للتعامل مع سجلات الخادم. انقر على زر السجلات على اليسار لفتح سجل الخادم. يجب أن ترى أنه يقوم بتثبيت Socket.io بكل تبعياته. هذا هو المكان الذي تحتاج فيه إلى البحث عن جميع الأخطاء وإخراج رمز الخادم.


الآن دعنا نذهب إلى server.js . هذا هو المكان الذي يوجد فيه رمز الخادم الخاص بنا. حتى الآن ، لا يوجد سوى بعض التعليمات البرمجية الأساسية لعرض HTML. قم بإضافة سطر إلى أعلى الملف لتمكين Socket.io:

 var io = require('socket.io')(http); //     http 

نحتاج الآن أيضًا إلى تمكين Socket.io في العميل ، لذا دعنا نعود إلى index.html ونضيف الأسطر التالية داخل علامة <head> :

 <!--    Socket.io --> <script src="/socket.io/socket.io.js"></script> 

ملاحظة: يعالج Socket.io تلقائيًا تحميل مكتبة العميل على طول هذا المسار ، لذلك يعمل هذا الخط حتى إذا لم يكن هناك دليل /socket.io/ في مجلداتك.

الآن Socket.io مدرج في المشروع وجاهز للانطلاق!

2. الاعتراف وتفرخ اللاعبين


ستكون خطوتنا الحقيقية الأولى هي قبول الاتصالات على الخادم وإنشاء لاعبين جدد في العميل.

قبول اتصالات الخادم


أضف هذا الرمز إلى الجزء السفلي من server.js :

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); }) 

لذلك نطلب من Socket.io الاستماع إلى جميع أحداث connection التي تحدث تلقائيًا عند اتصال العميل. تقوم المكتبة بإنشاء كائن socket جديد لكل عميل ، حيث socket.id هو المعرف الفريد لهذا العميل.

للتحقق من أن هذا يعمل ، عد إلى العميل ( index.html ) وأضف هذا السطر في مكان ما في وظيفة الإنشاء :

 var socket = io(); //    'connection'   

إذا بدأت اللعبة ونظرت إلى سجل الخادم (انقر على زر السجلات ) ، سترى أن الخادم قد سجل حدث الاتصال هذا!

الآن ، عند توصيل لاعب جديد ، نتوقع منه أن يقدم لنا معلومات حول حالته. في حالتنا ، نحتاج إلى معرفة x و y وزاوية على الأقل من أجل إنشائه بشكل صحيح في النقطة الصحيحة.

كان حدث connection حدثًا مضمَّنًا تم تشغيله بواسطة Socket.io. يمكننا الاستماع إلى أي أحداث محددة بشكل مستقل. سأقوم بتسمية حدثي new-player ، وأتوقع أن يرسله العميل بمجرد أن يتصل بمعلومات حول موقعه. سيبدو هذا:

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); }) }) 

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

يمكننا إرسال رسالة إلى جميع اللاعبين المتصلين الآخرين حتى يعرفوا ظهور لاعب جديد. لدى Socket.io وظيفة مناسبة لهذا:

 socket.broadcast.emit('create-player',state_data); 

عند socket.emit يتم ببساطة تمرير الرسالة إلى هذا العميل الفردي. عند socket.broadcast.emit يتم إرساله إلى كل عميل متصل بالخادم ، فيما عدا تلك التي تم استدعاء هذه الوظيفة بها.

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

يجب أن يظهر رمز الخادم الآن كما يلي:

 //  Socket.io    io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); socket.broadcast.emit('create-player',state_data); }) }) 

أي أنه في كل مرة يتصل فيها اللاعب ، نتوقع منه أن يرسل لنا رسالة تحتوي على معلومات حول موقعه ، ونرسل هذه البيانات إلى جميع اللاعبين الآخرين حتى يتمكنوا من إنشاء نقشته.

تفريخ العميل


الآن ، لإكمال هذه الدورة ، نحتاج إلى تنفيذ إجراءين في العميل:

  1. إنشاء رسالة مع بيانات موقعنا بعد الاتصال.
  2. استمع create-player أحداث create-player وإنشاء لاعب في هذه المرحلة.

لتنفيذ الإجراء الأول بعد إنشاء لاعب في وظيفة الإنشاء (تقريبًا في السطر 135) ، يمكننا إنشاء رسالة تحتوي على بيانات الموقع التي نحتاج إلى إرسالها:

 socket.emit('new-player',{x:player.sprite.x,y:player.sprite.y,angle:player.sprite.rotation}) 

لا داعي للقلق بشأن إجراء تسلسل للبيانات التي يتم إرسالها. يمكنك نقلها في أي نوع من الكائنات ، وسيقوم Socket.io بمعالجتها لنا.

قبل الانتقال ، اختبر الرمز . يجب أن نرى رسالة مماثلة في سجلات الخادم:

 New player has state: { x: 728.8180247836519, y: 261.9979387913289, angle: 0 } 

نعلم الآن أن خادمنا يتلقى إشعارًا بشأن اتصال لاعب جديد ويقرأ البيانات حول موقعه بشكل صحيح!

بعد ذلك ، نريد الاستماع إلى طلبات إنشاء لاعب جديد. يمكننا وضع هذا الكود مباشرة بعد إنشاء الرسالة ، يجب أن يبدو مثل هذا:

 socket.on('create-player',function(state){ // CreateShip -      ,     CreateShip(1,state.x,state.y,state.angle) }) 

اختبر الرمز الآن. افتح نافذتين مع اللعبة وتأكد من أنها تعمل.

يجب أن ترى أنه بعد فتح عميلين ، تم إنشاء سفينتين للعميل الأول ، والثاني لديه واحد فقط.

المهمة: هل يمكنك معرفة سبب حدوث ذلك؟ أو كيف يمكنك إصلاح هذا؟ خطوة بخطوة اتبع منطق العميل / الخادم الذي كتبناه وحاول تصحيحه.

آمل أنك حاولت معرفة ذلك بنفسك! يحدث ما يلي: عندما يتصل اللاعب الأول ، يرسل الخادم حدث create-player إلى جميع اللاعبين الآخرين ، ولكن لا يوجد لاعبون يمكنهم تلقيه بعد. بعد توصيل المشغل الثاني ، يرسل الخادم رسائله مرة أخرى ، ويستقبلها اللاعب الأول ويقوم بإنشاء العفريت بشكل صحيح ، في حين فات اللاعب الثاني رسالة المشغل الأول.

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

تحذير مزامنة حالة اللعبة


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

النهج الثاني هو نقل حالة اللعبة بأكملها. في هذه الحالة ، نقوم فقط في كل مرة تقوم فيها بالاتصال أو قطع الاتصال بإرسال قائمة كاملة لجميع اللاعبين.

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

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

لتنفيذ هذا المخطط ، سأفعل ما يلي:

  1. سأحتفظ بقاموس للاعبين ، سيكون مفتاحهم معرفهم ، وستكون القيمة بيانات عن مواقعهم.
  2. أضف لاعبًا إلى هذا القاموس عندما يكون متصلاً وأرسل حدث تحديث.
  3. إزالة المشغل من هذا القاموس عند إيقاف تشغيله وإرسال حدث تحديث.

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

 //  Socket.io    // 1 -      / var players = {}; io.on('connection', function(socket){ console.log("New client has connected with id:",socket.id); socket.on('new-player',function(state_data){ //   new-player    console.log("New player has state:",state_data); // 2 -      players[socket.id] = state_data; //    io.emit('update-players',players); }) socket.on('disconnect',function(){ // 3-       delete players[socket.id]; //    }) }) 

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

هذه هي الطريقة التي أعالج بها هذا الحدث في العميل:

 //     // : -         other_players = {} socket.on('update-players',function(players_data){ var players_found = {}; //        for(var id in players_data){ //      if(other_players[id] == undefined && id != socket.id){ // ,      var data = players_data[id]; var p = CreateShip(1,data.x,data.y,data.angle); other_players[id] = p; console.log("Created new player at (" + data.x + ", " + data.y + ")"); } players_found[id] = true; //     if(id != socket.id){ other_players[id].x = players_data[id].x; //  ,    ,      other_players[id].y = players_data[id].y; other_players[id].rotation = players_data[id].angle; } } //       for(var id in other_players){ if(!players_found[id]){ other_players[id].destroy(); delete other_players[id]; } } }) 

من جانب العميل ، أقوم بتخزين السفن في القاموس other_players ، والتي قمت بتعريفها للتو في أعلى البرنامج النصي (لا يتم عرضها هنا). نظرًا لأن الخادم يرسل بيانات اللاعب إلى جميع اللاعبين ، يجب أن أضيف شيكًا حتى لا يقوم العميل بإنشاء نقش إضافي لنفسه. (إذا كانت لديك مشكلات في البنية ، فإليك الشفرة الكاملة التي يجب أن تكون في index.html في الوقت الحالي).

اختبر الرمز الآن. يجب أن تكون قادرًا على إنشاء العديد من العملاء ورؤية العدد الصحيح للسفن التي تم إنشاؤها في المواضع الصحيحة!

3. تزامن مواقع السفن


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

  1. إجبار العميل على إنشاء رسالة في كل مرة ينتقل فيها إلى منصب جديد.
  2. قم بتعليم الخادم للاستماع إلى رسالة النقل هذه وتحديث عنصر بيانات اللاعب في قاموس players .
  3. إنشاء حدث تحديث لجميع العملاء.

وهذا يجب أن يكون كافيا! الآن حان دورك لمحاولة تنفيذ ذلك بنفسك.

إذا كنت مرتبكًا تمامًا وتحتاج إلى تلميح ، فقم بإلقاء نظرة على المشروع النهائي .

ملاحظة حول تقليل البيانات المرسلة عبر الشبكة


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

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

حل آخر هو إرسال تحديثات الخادم بتردد ثابت ، بغض النظر عن عدد الرسائل المستلمة من المشغل. المعيار المشترك هو تحديث الخادم حوالي 30 مرة في الثانية.

ومع ذلك ، عند اختيار بنية الخادم الخاص بك ، يجب عليك تقييم عدد الرسائل المرسلة في كل إطار في المراحل الأولى من تطوير اللعبة.

4. تزامن شل


لقد انتهينا تقريبا! آخر جزء خطير هو المزامنة عبر شبكة من الأصداف. يمكننا تنفيذها بنفس الطريقة التي يتم بها تشغيل اللاعبين المتزامنين:

  • يرسل كل عميل مواقع جميع الأصداف الخاصة به في كل إطار.
  • يقوم الخادم بإعادة توجيههم إلى كل لاعب.

لكن هناك مشكلة

حماية الغش


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

للتعامل مع هذه المشكلة جزئيًا ، سنحاول استخدام مخطط آخر:

  • يقوم العميل بتوليد رسالة حول القذيفة مع موقعها واتجاهها.
  • الخادم يحاكي حركة القذائف.
  • يقوم الخادم بتحديث بيانات كل عميل ، ويمرر موضع جميع الأصداف.
  • يقوم العملاء بتقديم الأصداف في المواقع المستلمة من الخادم.

وبالتالي ، فإن العميل مسؤول عن موقع المقذوف ، ولكن ليس عن سرعته وليس عن تحركه الإضافي. يمكن للعميل تغيير موضع الأصداف لنفسه ، ولكن هذا لن يغير ما يراه العملاء الآخرون.

لتنفيذ مثل هذا المخطط ، سنضيف توليد الرسائل عند إطلاقها. لن أقوم بعد الآن بإنشاء العفريت نفسها ، لأن وجودها وموقعها سيحددهما الخادم بالكامل. الآن ستبدو لقطة المقذوفة الجديدة في index.html كما يلي:

 //   if(game.input.activePointer.leftButton.isDown && !this.shot){ var speed_x = Math.cos(this.sprite.rotation + Math.PI/2) * 20; var speed_y = Math.sin(this.sprite.rotation + Math.PI/2) * 20; /*    ,       ,       var bullet = {}; bullet.speed_x = speed_x; bullet.speed_y = speed_y; bullet.sprite = game.add.sprite(this.sprite.x + bullet.speed_x,this.sprite.y + bullet.speed_y,'bullet'); bullet_array.push(bullet); */ this.shot = true; //  ,     socket.emit('shoot-bullet',{x:this.sprite.x,y:this.sprite.y,angle:this.sprite.rotation,speed_x:speed_x,speed_y:speed_y}) } 

أيضا الآن يمكننا التعليق على جزء التعليمات البرمجية بالكامل تحديث الأصداف في العميل:

 /*     ,         //   for(var i=0;i<bullet_array.length;i++){ var bullet = bullet_array[i]; bullet.sprite.x += bullet.speed_x; bullet.sprite.y += bullet.speed_y; //  ,       if(bullet.sprite.x < -10 || bullet.sprite.x > WORLD_SIZE.w || bullet.sprite.y < -10 || bullet.sprite.y > WORLD_SIZE.h){ bullet.sprite.destroy(); bullet_array.splice(i,1); i--; } } */ 

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

 //     socket.on('bullets-update',function(server_bullet_array){ //     ,   for(var i=0;i<server_bullet_array.length;i++){ if(bullet_array[i] == undefined){ bullet_array[i] = game.add.sprite(server_bullet_array[i].x,server_bullet_array[i].y,'bullet'); } else { //      ! bullet_array[i].x = server_bullet_array[i].x; bullet_array[i].y = server_bullet_array[i].y; } } //    ,   for(var i=server_bullet_array.length;i<bullet_array.length;i++){ bullet_array[i].destroy(); bullet_array.splice(i,1); i--; } }) 

وهذا كل ما يجب أن يكون في العميل. سأفترض أنك تعرف بالفعل مكان تضمين أجزاء التعليمات البرمجية هذه وكيفية تجميعها معًا ، ولكن إذا كان لديك أي مشاكل ، يمكنك دائمًا إلقاء نظرة على النتيجة النهائية .

الآن في server.js نحن بحاجة إلى تتبع ومحاكاة الأصداف. أولاً ، سننشئ مصفوفة لتتبع الأصداف ، على غرار صفيف للاعبين:

 var bullet_array = []; //         

بعد ذلك ، نستمع إلى حدث إطلاق المقذوفات:

 //   shoot-bullet        socket.on('shoot-bullet',function(data){ if(players[socket.id] == undefined) return; var new_bullet = data; data.owner_id = socket.id; //    id  bullet_array.push(new_bullet); }); 

الآن نقوم بمحاكاة الأصداف 60 مرة في الثانية:

 //   60       function ServerGameLoop(){ for(var i=0;i<bullet_array.length;i++){ var bullet = bullet_array[i]; bullet.x += bullet.speed_x; bullet.y += bullet.speed_y; // ,       if(bullet.x < -10 || bullet.x > 1000 || bullet.y < -10 || bullet.y > 1000){ bullet_array.splice(i,1); i--; } } } setInterval(ServerGameLoop, 16); 

والخطوة الأخيرة هي إرسال حدث التحديث في مكان ما داخل هذه الوظيفة (ولكن بالتأكيد خارج الحلقة for):

 //  ,    ,    io.emit("bullets-update",bullet_array); 

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

5. تصادم مع قذائف


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

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

  • تحقق مما إذا كانت المقذوفات قريبة بما يكفي من أي لاعب على الخادم.
  • إنشاء حدث لجميع العملاء عندما تضرب قذيفة لاعب.
  • قم بتعليم العميل للاستماع إلى حدث الضربة وجعل السفينة يومض عند الضرب.

يمكنك محاولة تنفيذ هذا الجزء بنفسك. لجعل سفينة اللاعب تومض عند ضربها ، ما عليك سوى تعيين قناة ألفا على 0:

 player.sprite.alpha = 0; 

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

 for(var id in other_players){ if(other_players[id].alpha < 1){ other_players[id].alpha += (1 - other_players[id].alpha) * 0.16; } else { other_players[id].alpha = 1; } } 

قد يكون الجزء الصعب الوحيد هو التحقق من أن اللاعب لا يصيب قذائفه الخاصة (وإلا فإنه سيتحمل الضرر في كل مرة يطلق فيها النار).

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

6. تمهيد الحركة


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

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

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

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

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

لن نطبق نظام shell هذا في هذا البرنامج التعليمي ، ولكن من الجيد أن تعرف أن هذه الطريقة موجودة.

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

 //     if(id != socket.id){ other_players[id].target_x = players_data[id].x; //  ,    ,     other_players[id].target_y = players_data[id].y; other_players[id].target_rotation = players_data[id].angle; } 

بعد ذلك ، في وظيفة التحديث (أيضًا من جانب العميل) ، نتجول حول جميع اللاعبين الآخرين وندفعهم نحو هدفهم:

 //     ,      for(var id in other_players){ var p = other_players[id]; if(p.target_x != undefined){ px += (p.target_x - px) * 0.16; py += (p.target_y - py) * 0.16; //  ,    /  var angle = p.target_rotation; var dir = (angle - p.rotation) / (Math.PI * 2); dir -= Math.round(dir); dir = dir * Math.PI * 2; p.rotation += dir * 0.16; } } 

وبالتالي ، يرسل لنا الخادم تحديثات 30 مرة في الثانية ، ولكن لا يزال بإمكاننا اللعب بمعدل 60 إطارًا في الثانية ولا تزال اللعبة تبدو سلسة!

الخلاصة


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

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

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

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

يمكنك استخدام وظيفة Glitch المفيدة الأخرى ، والتي تتمثل في القدرة على تنزيل أو تصدير مشروعك الخاص من خلال خيارات متقدمة في الزاوية اليسرى العليا:

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


All Articles