كيفية بناء لعبة متعددة المستخدمين في الوقت الحقيقي من الصفر

نشرت: 2022-03-10
ملخص سريع ↬ يسلط هذا المقال الضوء على العملية والقرارات الفنية والدروس المستفادة من إنشاء لعبة Autowuzzler في الوقت الفعلي. تعرف على كيفية مشاركة حالة اللعبة عبر العديد من العملاء في الوقت الفعلي باستخدام Colyseus ، وإجراء العمليات الحسابية الفيزيائية باستخدام Matter.js ، وتخزين البيانات في Supabase.io وإنشاء الواجهة الأمامية باستخدام SvelteKit.

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

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

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

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

في هذه المقالة ، سأصف العملية الكامنة وراء إنشاء Autowuzzler ، والأدوات والأطر التي اخترتها ، وسأشارك بعض تفاصيل التنفيذ والدروس التي تعلمتها.

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

النموذج الأولي العامل (الرهيب)

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

نظرًا لأن اللعبة تحتاج إلى دعم العديد من اللاعبين في الوقت الفعلي ، فقد استخدمت Express كوسيط WebSockets. هنا حيث يصبح الأمر صعبًا.

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

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

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

تحقق من صحة الفكرة ، تفريغ النموذج الأولي

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

لقد حان الوقت لإلقاء النموذج الأولي والبدء بإعداد جديد وهدف واضح.

إعداد مشروع

من الواضح أن نهج "العميل الأول يحكم الكل" يحتاج إلى استبداله بحل تعيش فيه حالة اللعبة على الخادم . في بحثي ، صادفت Colyseus ، والتي بدت وكأنها الأداة المثالية للوظيفة.

بالنسبة إلى اللبنات الأساسية الأخرى للعبة التي اخترتها:

  • Matter.js كمحرك فيزيائي بدلاً من Phaser.js لأنه يعمل في Node ولا يتطلب Autowuzzler إطار عمل كامل للعبة.
  • SvelteKit كإطار عمل للتطبيق بدلاً من Next.js ، لأنه دخل للتو في الإصدار التجريبي العام في ذلك الوقت. (إلى جانب ذلك: أحب العمل مع Svelte.)
  • Supabase.io لتخزين أرقام التعريف الشخصية للعبة التي أنشأها المستخدم.

دعونا نلقي نظرة على تلك اللبنات بمزيد من التفصيل.

المزيد بعد القفز! أكمل القراءة أدناه ↓

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

Colyseus هو إطار لعبة متعدد اللاعبين يعتمد على Node.js و Express. يوفر في جوهره:

  • مزامنة الحالة عبر العملاء بطريقة موثوقة ؛
  • اتصال فعال في الوقت الحقيقي باستخدام WebSockets من خلال إرسال البيانات التي تم تغييرها فقط ؛
  • تجهيزات متعددة الغرف
  • مكتبات العميل لـ JavaScript ، و Unity ، و Defold Engine ، و Haxe ، و Cocos Creator ، و Construct3 ؛
  • خطافات دورة الحياة ، على سبيل المثال ، تم إنشاء الغرفة ، وانضمام المستخدم ، وأوراق المستخدم ، والمزيد ؛
  • إرسال الرسائل ، إما كرسائل بث إلى جميع المستخدمين في الغرفة ، أو إلى مستخدم واحد ؛
  • لوحة مراقبة مدمجة وأداة اختبار الحمل.

ملاحظة : تجعل مستندات Colyseus من السهل البدء في خادم Colyseus المجرد من خلال توفير نص npm init أمثلة.

إنشاء مخطط

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

  • فريقين،
  • عدد محدود من اللاعبين ،
  • كرة واحدة.

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

 class Ball extends Schema { constructor() { super(); this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; } } defineTypes(Ball, { x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number" });

في المثال أعلاه ، يتم إنشاء فئة جديدة توسع فئة المخطط التي يوفرها Colyseus ؛ في المنشئ ، تتلقى جميع الخصائص قيمة أولية. يتم وصف موضع الكرة وحركتها باستخدام الخصائص الخمس: x ، y ، angle ، velocityX, velocityY بالإضافة إلى ذلك ، نحتاج إلى تحديد أنواع كل خاصية . يستخدم هذا المثال بناء جملة JavaScript ، ولكن يمكنك أيضًا استخدام بناء جملة TypeScript صغير الحجم.

يمكن أن تكون أنواع الخصائص إما أنواعًا بدائية:

  • string
  • boolean
  • number (بالإضافة إلى أنواع عدد صحيح وعائم أكثر كفاءة)

أو أنواع معقدة:

  • ArraySchema (على غرار Array في JavaScript)
  • MapSchema (على غرار Map في JavaScript)
  • SetSchema (على غرار تعيين في JavaScript)
  • CollectionSchema (على غرار ArraySchema ، ولكن بدون التحكم في الفهارس)

فئة Ball أعلاه لها خمس خصائص number النوع: إحداثياتها ( x ، y ) ، angle الحالية ومتجه السرعة ( velocityX س ، velocityY ص).

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

 class Player extends Schema { constructor(teamNumber) { super(); this.name = ""; this.x = 0; this.y = 0; this.angle = 0; this.velocityX = 0; this.velocityY = 0; this.teamNumber = teamNumber; } } defineTypes(Player, { name: "string", x: "number", y: "number", angle: "number", velocityX: "number", velocityY: "number", angularVelocity: "number", teamNumber: "number", });

أخيرًا ، يربط مخطط Autowuzzler Room بين الفئات المحددة مسبقًا: مثيل الغرفة الواحدة بها فرق متعددة (مخزنة في ArraySchema). تحتوي أيضًا على كرة واحدة ، لذلك نقوم بإنشاء مثيل Ball جديد في مُنشئ RoomSchema. يتم تخزين اللاعبين في MapSchema للاسترجاع السريع باستخدام معرفاتهم.

 class RoomSchema extends Schema { constructor() { super(); this.teams = new ArraySchema(); this.ball = new Ball(); this.players = new MapSchema(); } } defineTypes(RoomSchema, { teams: [Team], // an Array of Team ball: Ball, // a single Ball instance players: { map: Player } // a Map of Players });
ملاحظة : تم حذف تعريف فئة Team .

إعداد متعدد الغرف ("التوفيق")

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

تسمى عملية تعيين اللاعبين في غرفة اللعب المرغوبة "التوفيق". يجعل Colyseus من السهل جدًا الإعداد باستخدام filterBy بالطريقة عند تحديد غرفة جديدة:

 gameServer.define("autowuzzler", AutowuzzlerRoom).filterBy(['gamePIN']);

الآن ، أي لاعب ينضم إلى اللعبة بنفس اللعبة gamePIN (سنرى كيفية "الانضمام" لاحقًا) في نفس غرفة اللعبة! تقتصر أي تحديثات حالة ورسائل بث أخرى على اللاعبين الموجودين في نفس الغرفة.

الفيزياء في تطبيق Colyseus

يوفر Colyseus الكثير من الأشياء الجاهزة للاستعداد والتشغيل بسرعة باستخدام خادم لعبة موثوق ، لكنه يترك الأمر للمطور لإنشاء ميكانيكا اللعبة الفعلية - بما في ذلك الفيزياء. لا يمكن تنفيذ Phaser.js ، الذي استخدمته في النموذج الأولي ، في بيئة غير مستعرضة ، ولكن محرك الفيزياء المتكاملة Matter.js الخاص بـ Phaser.js يمكن تشغيله على Node.js.

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

يقع "عالم" Matter.js في قلب لعبة Autowuzzler ؛ إنه يحدد مدى سرعة تحرك السيارات ، ومدى ارتداد الكرة ، وأين تقع الأهداف ، وماذا يحدث إذا أطلق شخص ما هدفًا.

 let ball = Bodies.circle( ballInitialXPosition, ballInitialYPosition, radius, { render: { sprite: { texture: '/assets/ball.png', } }, friction: 0.002, restitution: 0.8 } ); World.add(this.engine.world, [ball]);

رمز مبسط لإضافة كائن لعبة "كرة" إلى المسرح في Matter.js.

بمجرد تحديد القواعد ، يمكن تشغيل Matter.js مع أو بدون عرض شيء ما على الشاشة. بالنسبة إلى Autowuzzler ، أستخدم هذه الميزة لإعادة استخدام رمز عالم الفيزياء لكل من الخادم والعميل - مع وجود العديد من الاختلافات الرئيسية:

عالم الفيزياء على الخادم :

  • يتلقى مدخلات المستخدم (أحداث لوحة المفاتيح لتوجيه السيارة) عبر Colyseus ويطبق القوة المناسبة على كائن اللعبة (سيارة المستخدم) ؛
  • يقوم بجميع الحسابات الفيزيائية لجميع الأشياء (اللاعب والكرة) ، بما في ذلك اكتشاف الاصطدامات ؛
  • يرسل الحالة المحدثة لكل كائن لعبة إلى Colyseus ، والتي بدورها تبثها إلى العملاء ؛
  • يتم تحديثه كل 16.6 مللي ثانية (= 60 إطارًا في الثانية) ، يتم تشغيلها بواسطة خادم Colyseus الخاص بنا.

عالم الفيزياء على العميل :

  • لا يتلاعب بأشياء اللعبة مباشرة ؛
  • يتلقى حالة محدثة لكل كائن لعبة من Colyseus ؛
  • يطبق التغييرات في الموضع والسرعة والزاوية بعد تلقي الحالة المحدثة ؛
  • يرسل إدخال المستخدم (أحداث لوحة المفاتيح لتوجيه السيارة) إلى Colyseus ؛
  • تحميل النقوش المتحركة للعبة واستخدام العارض لرسم عالم الفيزياء على عنصر قماش ؛
  • يتخطى كشف الاصطدام (باستخدام خيار isSensor للكائنات) ؛
  • التحديثات باستخدام requestAnimationFrame ، بشكل مثالي بمعدل 60 إطارًا في الثانية.
رسم تخطيطي يوضح كتلتين رئيسيتين: تطبيق خادم Colyseus وتطبيق SvelteKit. يحتوي تطبيق خادم Colyseus على كتلة Autowuzzler Room ، ويحتوي تطبيق SvelteKit على كتلة Colyseus Client. تشترك كلتا الكتلتين الرئيسيتين في كتلة تسمى عالم الفيزياء (Matter.js)
الوحدات المنطقية الرئيسية لبنية Autowuzzler: يتم مشاركة عالم الفيزياء بين خادم Colyseus وتطبيق عميل SvelteKit. (معاينة كبيرة)

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

الاستيفاء على العميل

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

دورة الحياة

فئة Room Autowuzzler هي المكان الذي يتم فيه التعامل مع المنطق المتعلق بالمراحل المختلفة لغرفة Colyseus. يوفر Colyseus عدة طرق لدورة الحياة:

  • onCreate : عند إنشاء غرفة جديدة (عادةً عندما يتصل العميل الأول) ؛
  • onAuth : كخطاف ترخيص للسماح أو رفض الدخول إلى الغرفة ؛
  • onJoin : عندما يتصل العميل بالغرفة ؛
  • onLeave : عندما ينفصل العميل عن الغرفة ؛
  • عند onDispose : عندما يتم التخلص من الغرفة.

تنشئ غرفة Autowuzzler مثيلًا جديدًا لعالم الفيزياء (انظر قسم "الفيزياء في تطبيق Colyseus") بمجرد إنشائها ( onCreate ) وتضيف لاعبًا إلى العالم عندما يتصل العميل ( onJoin ). ثم يقوم بتحديث عالم الفيزياء 60 مرة في الثانية (كل 16.6 مللي ثانية) باستخدام طريقة setSimulationInterval (حلقة لعبتنا الرئيسية):

 // deltaTime is roughly 16.6 milliseconds this.setSimulationInterval((deltaTime) => this.world.updateWorld(deltaTime));

تكون كائنات الفيزياء مستقلة عن كائنات Colyseus ، مما يتركنا مع تبدلين من نفس كائن اللعبة (مثل الكرة) ، أي كائن في عالم الفيزياء وجسم Colyseus يمكن مزامنته.

بمجرد أن يتغير الكائن المادي ، يجب تطبيق خصائصه المحدثة مرة أخرى على كائن Colyseus. يمكننا تحقيق ذلك من خلال الاستماع إلى حدث afterUpdate الخاص بـ Matter.js وتعيين القيم من هناك:

 Events.on(this.engine, "afterUpdate", () => { // apply the x position of the physics ball object back to the colyseus ball object this.state.ball.x = this.physicsWorld.ball.position.x; // ... all other ball properties // loop over all physics players and apply their properties back to colyseus players objects })

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

رسم تخطيطي يوضح الإصدارات الثلاثة من كائن اللعبة: Colyseus Schema Objects و Matter.js Physics Objects و Client Matter.js Physics Objects. يقوم Matter.js بتحديث إصدار Colyseus من الكائن ، ويتزامن Colyseus مع Client Matter.js Physics Object.
يحتفظ Autowuzzler بثلاث نسخ من كل كائن فيزيائي ، وإصدار واحد موثوق (كائن Colyseus) ، وإصدار في عالم فيزياء Matter.js وإصدار على العميل. (معاينة كبيرة)

تطبيق من جانب العميل

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

  • تمكن المستخدمين من إنشاء ومشاركة أرقام التعريف الشخصية للعبة للوصول إلى الغرف الفردية ؛
  • يرسل أرقام التعريف الشخصية للعبة التي تم إنشاؤها إلى قاعدة بيانات Supabase للاستمرار ؛
  • يوفر صفحة اختيارية "الانضمام إلى لعبة" للاعبين لإدخال رقم التعريف الشخصي للعبة ؛
  • يتحقق من صحة أرقام التعريف الشخصية للعبة عندما ينضم اللاعب إلى اللعبة ؛
  • يستضيف اللعبة الفعلية ويعرضها على عنوان URL قابل للمشاركة (أي فريد) ؛
  • يتصل بخادم Colyseus ويتعامل مع تحديثات الحالة ؛
  • يوفر صفحة مقصودة ("تسويق").

لتنفيذ هذه المهام ، اخترت SvelteKit على Next.js للأسباب التالية:

لماذا SvelteKit؟

كنت أرغب في تطوير تطبيق آخر باستخدام Svelte منذ أن أنشأت neolightsout. عندما دخل SvelteKit (إطار التطبيق الرسمي لـ Svelte) في الإصدار التجريبي العام ، قررت أن أبني Autowuzzler معه وأقبل أي صداع يأتي مع استخدام إصدار تجريبي جديد - من الواضح أن متعة استخدام Svelte تعوضه.

دفعتني هذه الميزات الرئيسية إلى اختيار SvelteKit على Next.js للتنفيذ الفعلي للواجهة الأمامية للعبة:

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

إنشاء وتخزين دبابيس اللعبة

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

لقطة شاشة لبدء قسم جديد للعبة على موقع Autowuzzler على الويب تعرض رقم التعريف الشخصي للعبة 751428 وخيارات لنسخ ومشاركة رقم التعريف الشخصي وعنوان URL للعبة.
ابدأ لعبة جديدة عن طريق نسخ رقم التعريف الشخصي للعبة الذي تم إنشاؤه أو مشاركة الرابط المباشر لغرفة اللعبة. (معاينة كبيرة)

هذه حالة استخدام رائعة لنقاط نهاية جانب الخادم SvelteKits جنبًا إلى جنب مع وظيفة Sveltes onMount: تقوم نقطة النهاية /api/createcode بإنشاء رمز PIN للعبة ، وتخزينه في قاعدة بيانات Supabase.io وإخراج PIN للعبة كاستجابة . يتم جلب هذه الاستجابة بمجرد تثبيت مكون الصفحة في صفحة "إنشاء":

رسم تخطيطي يوضح ثلاثة أقسام: إنشاء صفحة وإنشاء نقطة نهاية و Supabase.io. يجلب إنشاء الصفحة نقطة النهاية في وظيفة onMount الخاصة بها ، وتقوم نقطة النهاية بإنشاء رقم تعريف شخصي للعبة وتخزينه في Supabase.io والاستجابة برمز PIN الخاص باللعبة. تعرض صفحة الإنشاء بعد ذلك رقم التعريف الشخصي للعبة.
يتم إنشاء أرقام التعريف الشخصية الخاصة باللعبة في نقطة النهاية ، ويتم تخزينها في قاعدة بيانات Supabase.io ويتم عرضها في صفحة "إنشاء". (معاينة كبيرة)

تخزين أرقام التعريف الشخصية للعبة باستخدام Supabase.io

Supabase.io هو بديل مفتوح المصدر لبرنامج Firebase. يجعل Supabase من السهل جدًا إنشاء قاعدة بيانات PostgreSQL والوصول إليها إما عبر إحدى مكتبات العملاء أو عبر REST.

بالنسبة لعميل JavaScript ، نقوم باستيراد وظيفة createClient وتنفيذها باستخدام المعلمات supabase_url و supabase_key التي تلقيناها عند إنشاء قاعدة البيانات. لتخزين رقم التعريف الشخصي للعبة الذي تم إنشاؤه في كل مكالمة createcode نهاية رمز الإنشاء ، كل ما نحتاج إليه هو تشغيل استعلام insert البسيط هذا:

 import { createClient } from '@supabase/supabase-js' const database = createClient( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_KEY ); const { data, error } = await database .from("games") .insert([{ code: 123456 }]);

ملاحظة : يتم تخزين supabase_url و supabase_key في ملف .env. بسبب Vite - أداة البناء في قلب SvelteKit - من الضروري أن تسبق متغيرات البيئة بـ VITE_ لجعلها قابلة للوصول في SvelteKit.

الوصول إلى اللعبة

أردت أن أجعل الانضمام إلى لعبة Autowuzzler أمرًا سهلاً مثل اتباع الرابط. لذلك ، يجب أن يكون لكل غرفة ألعاب عنوان URL خاص بها بناءً على رقم التعريف الشخصي للعبة الذي تم إنشاؤه مسبقًا ، على سبيل المثال https://autowuzzler.com/play/12345.

في SvelteKit ، يتم إنشاء الصفحات ذات معلمات المسار الديناميكية عن طريق وضع الأجزاء الديناميكية للمسار بين قوسين مربعين عند تسمية ملف الصفحة: client/src/routes/play/[gamePIN].svelte . ستصبح قيمة المعلمة gamePIN متاحة بعد ذلك في مكون الصفحة (راجع مستندات SvelteKit للحصول على التفاصيل). في مسار play ، نحتاج إلى الاتصال بخادم Colyseus ، وإنشاء مثيل لعالم الفيزياء لتقديمه إلى الشاشة ، والتعامل مع تحديثات كائنات اللعبة ، والاستماع إلى إدخال لوحة المفاتيح وعرض واجهة المستخدم الأخرى مثل النتيجة ، وما إلى ذلك.

الاتصال بكوليسيوس وتحديث الدولة

تمكننا مكتبة العميل Colyseus من توصيل العميل بخادم Colyseus. أولاً ، لنقم بإنشاء Colyseus.Client جديد للعميل عن طريق توجيهه إلى خادم Colyseus ( ws://localhost:2567 قيد التطوير). ثم انضم إلى الغرفة بالاسم الذي اخترناه سابقًا ( autowuzzler ) و gamePIN من معلمة المسار. تتأكد معلمة gamePIN من انضمام المستخدم إلى مثيل الغرفة الصحيح (انظر "التوفيق" أعلاه).

 let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN });

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

 onMount(async () => { let client = new Colyseus.Client("ws://localhost:2567"); this.room = await client.joinOrCreate("autowuzzler", { gamePIN }); })

الآن بعد أن أصبحنا متصلين بخادم لعبة Colyseus ، يمكننا البدء في الاستماع إلى أي تغييرات تطرأ على عناصر اللعبة.

فيما يلي مثال على كيفية الاستماع إلى لاعب ينضم إلى الغرفة ( onAdd ) ويتلقى تحديثات الحالة المتتالية لهذا اللاعب:

 this.room.state.players.onAdd = (player, key) => { console.log(`Player has been added with sessionId: ${key}`); // add player entity to the game world this.world.createPlayer(key, player.teamNumber); // listen for changes to this player player.onChange = (changes) => { changes.forEach(({ field, value }) => { this.world.updatePlayer(key, field, value); // see below }); }; };

في طريقة updatePlayer لعالم الفيزياء ، نقوم بتحديث الخصائص واحدة تلو الأخرى لأن Colyseus ' onChange يقدم مجموعة من جميع الخصائص المتغيرة.

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

 updatePlayer(sessionId, field, value) { // get the player physics object by its sessionId let player = this.world.players.get(sessionId); // exit if not found if (!player) return; // apply changes to the properties switch (field) { case "angle": Body.setAngle(player, value); break; case "x": Body.setPosition(player, { x: value, y: player.position.y }); break; case "y": Body.setPosition(player, { x: player.position.x, y: value }); break; // set velocityX, velocityY, angularVelocity ... } }

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

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

 let keys = {}; const keyDown = e => { keys[e.key] = true; }; const keyUp = e => { keys[e.key] = false; }; document.addEventListener('keydown', keyDown); document.addEventListener('keyup', keyUp); let loop = () => { if (keys["ArrowLeft"]) { this.room.send("move", { direction: "left" }); } else if (keys["ArrowRight"]) { this.room.send("move", { direction: "right" }); } if (keys["ArrowUp"]) { this.room.send("move", { direction: "up" }); } else if (keys["ArrowDown"]) { this.room.send("move", { direction: "down" }); } // next iteration requestAnimationFrame(() => { setTimeout(loop, 50); }); } // start loop setTimeout(loop, 50);

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

مضايقات طفيفة

بالنظر إلى الماضي ، هناك شيئان من الفئة التي لم يخبرني أحدًا بها ولكن كان يجب أن يتبادر إلى الذهن:

  • إن الفهم الجيد لكيفية عمل محركات الفيزياء مفيد. لقد قضيت وقتًا طويلاً في ضبط الخصائص والقيود الفيزيائية. على الرغم من أنني قمت ببناء لعبة صغيرة باستخدام Phaser.js و Matter.js من قبل ، كان هناك الكثير من المحاولات والخطأ لجعل الأشياء تتحرك بالطريقة التي تخيلتها.
  • الوقت الحقيقي صعب - خاصة في الألعاب القائمة على الفيزياء. تؤدي التأخيرات الطفيفة إلى تفاقم التجربة إلى حد كبير ، وبينما تعمل مزامنة الحالة عبر العملاء باستخدام Colyseus بشكل رائع ، لا يمكنها إزالة تأخيرات الحساب والإرسال.

مسكتك وتحذيرات مع SvelteKit

منذ أن استخدمت SvelteKit عندما كان طازجًا من الفرن التجريبي ، كان هناك عدد قليل من المحاذير والمحاذير التي أود الإشارة إليها:

  • لقد استغرق الأمر بعض الوقت لمعرفة أن متغيرات البيئة يجب أن تكون مسبوقة بـ VITE_ من أجل استخدامها في SvelteKit. هذا موثق الآن بشكل صحيح في التعليمات.
  • لاستخدام Supabase ، اضطررت إلى إضافة devDependencies إلى كل من قوائم dependencies من package.json. أعتقد أن هذا لم يعد هو الحال.
  • تعمل وظيفة load SvelteKits على كل من الخادم والعميل !
  • لتمكين استبدال الوحدة النمطية الساخنة بالكامل (بما في ذلك حالة الحفظ) ، يجب عليك إضافة سطر تعليق يدويًا <!-- @hmr:keep-all --> في مكونات صفحتك. انظر التعليمات لمزيد من التفاصيل.

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

النشر والاستضافة

في البداية ، استضفت خادم Colyseus (Node) على مثيل Heroku وأهدرت الكثير من الوقت في عمل WebSockets و CORS. كما اتضح ، فإن أداء Heroku dyno الصغير (المجاني) ليس كافيًا لحالة الاستخدام في الوقت الفعلي. قمت لاحقًا بترحيل تطبيق Colyseus إلى خادم صغير في Linode. يتم نشر التطبيق من جانب العميل واستضافته على Netlify عبر محول SvelteKits netlify. لا مفاجآت هنا: لقد عملت Netlify بشكل رائع!

خاتمة

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

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

أخيرًا ، سمح لي استخدام SvelteKit لبناء الواجهة الأمامية للعبة بالتحرك بسرعة - وابتسامة الفرح على وجهي من حين لآخر.

الآن ، انطلق وادعو أصدقائك إلى جولة من Autowuzzler!

مزيد من القراءة في مجلة Smashing

  • "ابدأ مع React عن طريق بناء لعبة Whac-A-Mole ،" Jhey Tompkins
  • "كيفية بناء لعبة واقع افتراضي متعددة اللاعبين في الوقت الفعلي" ، ألفين وان
  • "كتابة محرك مغامرة نصية متعددة اللاعبين في Node.js" فرناندو دوجليو
  • "مستقبل تصميم الويب للجوال: تصميم ألعاب الفيديو ورواية القصص" ، سوزان سكاكا
  • "كيفية بناء لعبة عداء لا نهاية لها في الواقع الافتراضي" ألفين وان