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

الصورة أعلاه هي كيف ترى اللعبة بالفعل ، بعيد كل البعد عن أفضل ألعاب المغامرات AAA الحالية. ومع ذلك ، فقد كانوا ممتعين في اللعب وكانوا يسرقون مئات الساعات من وقتك ، وأنت جالس أمام هذا النص ، بمفردك ، محاولًا معرفة كيفية التغلب عليه.
من المفهوم أنه تم استبدال المغامرات النصية على مر السنين بألعاب تقدم صورًا أفضل (على الرغم من أنه يمكن للمرء أن يجادل في أن الكثير منهم قد ضحوا بالقصة من أجل الرسومات) ، وخاصة في السنوات القليلة الماضية ، القدرة المتزايدة على التعاون مع الآخرين الأصدقاء واللعب معًا. هذه الميزة الخاصة هي ميزة تفتقر إليها مغامرات النص الأصلي ، وهي ميزة أريد أن أعيدها في هذه المقالة.
أجزاء أخرى من هذه السلسلة
- الجزء 2: تصميم خادم محرك اللعبة
- الجزء 3: إنشاء عميل المحطة الطرفية
- الجزء 4: إضافة الدردشة إلى لعبتنا
هدفنا
بيت القصيد من هذا المسعى ، كما خمنت على الأرجح الآن من عنوان هذه المقالة ، هو إنشاء محرك مغامرة نصية يسمح لك بمشاركة المغامرة مع الأصدقاء ، مما يتيح لك التعاون معهم بشكل مشابه للطريقة التي تريدها أثناء لعبة Dungeons & Dragons (حيث ، تمامًا كما هو الحال مع المغامرات النصية الجيدة ، لا توجد رسومات لإلقاء نظرة عليها).
في إنشاء المحرك ، يقوم خادم الدردشة والعميل بالكثير من العمل. في هذه المقالة ، سأعرض عليكم مرحلة التصميم ، موضحًا أشياء مثل البنية خلف المحرك ، وكيف سيتفاعل العميل مع الخوادم ، وما هي قواعد هذه اللعبة.
فقط لأعطيك بعض المساعدة البصرية لما سيبدو عليه هذا ، هذا هو هدفي:

هذا هو هدفنا. بمجرد أن نصل إلى هناك ، ستحصل على لقطات شاشة بدلاً من نماذج بالأحجام الطبيعية السريعة والقذرة. لذا ، دعنا نبدأ في هذه العملية. أول شيء سنغطيه هو تصميم كل شيء. بعد ذلك ، سنغطي الأدوات الأكثر صلة التي سأستخدمها لتشفير هذا. أخيرًا ، سأعرض لك بعضًا من أجزاء التعليمات البرمجية الأكثر صلة (مع ارتباط إلى المستودع الكامل ، بالطبع).
نأمل ، في النهاية ، أن تجد نفسك تنشئ مغامرات نصية جديدة لتجربتها مع الأصدقاء!
مرحلة التصميم
بالنسبة لمرحلة التصميم ، سأغطي مخططنا العام. سأبذل قصارى جهدي حتى لا أتحمل حتى الموت ، لكن في نفس الوقت ، أعتقد أنه من المهم إظهار بعض الأشياء التي تحدث خلف الكواليس والتي يجب أن تحدث قبل وضع السطر الأول من التعليمات البرمجية.
المكونات الأربعة التي أريد تغطيتها هنا بقدر مناسب من التفاصيل هي:
- المحرك
سيكون هذا هو خادم اللعبة الرئيسي. سيتم تطبيق قواعد اللعبة هنا ، وستوفر واجهة غير تقليدية من الناحية التكنولوجية لأي نوع من العملاء يستهلكها. سنقوم بتنفيذ عميل طرفي ، ولكن يمكنك فعل الشيء نفسه مع عميل متصفح الويب أو أي نوع آخر تريده. - خادم الدردشة
نظرًا لأنه معقد بدرجة كافية للحصول على مقال خاص به ، ستحتوي هذه الخدمة أيضًا على الوحدة النمطية الخاصة بها. سيعتني خادم الدردشة بالسماح للاعبين بالتواصل مع بعضهم البعض أثناء اللعبة. - الزبون
كما ذكرنا سابقًا ، سيكون هذا عميلًا طرفيًا ، من الناحية المثالية ، سيبدو مشابهًا للحجم الطبيعي من سابقًا. سيستفيد من الخدمات التي يقدمها كل من المحرك وخادم الدردشة. - الألعاب (ملفات JSON)
أخيرًا ، سأنتقل إلى تعريف الألعاب الفعلية. بيت القصيد من هذا هو إنشاء محرك يمكنه تشغيل أي لعبة ، طالما أن ملف اللعبة الخاص بك يتوافق مع متطلبات المحرك. لذا ، على الرغم من أن هذا لن يتطلب ترميزًا ، فسأشرح كيف سأقوم ببناء ملفات المغامرة من أجل كتابة مغامراتنا الخاصة في المستقبل.
المحرك
سيكون محرك اللعبة ، أو خادم اللعبة ، واجهة برمجة تطبيقات REST وسيوفر جميع الوظائف المطلوبة.
ذهبت لواجهة برمجة تطبيقات REST ببساطة لأنه - بالنسبة لهذا النوع من الألعاب - لن يتسبب التأخير المضاف بواسطة HTTP وطبيعته غير المتزامنة في حدوث أي مشكلة. ومع ذلك ، سيتعين علينا السير في مسار مختلف لخادم الدردشة. ولكن قبل أن نبدأ في تحديد نقاط النهاية لواجهة برمجة التطبيقات الخاصة بنا ، نحتاج إلى تحديد ما سيكون المحرك قادرًا عليه. بحيث يتيح الحصول عليه.
ميزة | وصف |
---|---|
انضم إلى لعبة | سيتمكن اللاعب من الانضمام إلى اللعبة عن طريق تحديد معرف اللعبة. |
قم بإنشاء لعبة جديدة | يمكن للاعب أيضًا إنشاء مثيل لعبة جديد. يجب أن يقوم المحرك بإرجاع معرف ، حتى يتمكن الآخرون من استخدامه للانضمام. |
مشهد العودة | يجب أن تعيد هذه الميزة المشهد الحالي حيث توجد المجموعة. في الأساس ، سيعيد الوصف ، مع جميع المعلومات المرتبطة به (الإجراءات الممكنة ، والأشياء الموجودة فيه ، وما إلى ذلك). |
التفاعل مع المشهد | ستكون هذه واحدة من أكثر الأشياء تعقيدًا ، لأنها ستتخذ أمرًا من العميل وتقوم بهذا الإجراء - أشياء مثل التحرك ، والدفع ، والأخذ ، والنظر ، والقراءة ، على سبيل المثال لا الحصر. |
تأكد من المخزون | على الرغم من أن هذه طريقة للتفاعل مع اللعبة ، إلا أنها لا تتعلق مباشرة بالمشهد. لذا ، فإن فحص المخزون لكل لاعب سيعتبر إجراءً مختلفًا. |
كلمة عن الحركة
نحن بحاجة إلى طريقة لقياس المسافات في اللعبة لأن التنقل خلال المغامرة هو أحد الإجراءات الأساسية التي يمكن للاعب اتخاذها. سنستخدم هذا الرقم كمقياس للوقت ، فقط لتبسيط طريقة اللعب. قد لا يكون قياس الوقت بساعة فعلية هو الأفضل ، مع الأخذ في الاعتبار أن هذه الأنواع من الألعاب لها إجراءات تعتمد على الأدوار ، مثل القتال. بدلاً من ذلك ، سنستخدم المسافة لقياس الوقت (مما يعني أن مسافة 8 ستتطلب وقتًا أطول للعبور أكثر من واحدة من 2 ، مما يتيح لنا القيام بأشياء مثل إضافة تأثيرات للاعبين الذين يستمرون لمدة محددة من "نقاط المسافة" ).
جانب آخر مهم يجب مراعاته بشأن الحركة هو أننا لا نلعب بمفردنا. من أجل البساطة ، لن يسمح المحرك للاعبين بتقسيم المجموعة (على الرغم من أن هذا قد يكون تحسينًا مثيرًا للاهتمام في المستقبل). ستسمح النسخة الأولية من هذه الوحدة للجميع بالتحرك أينما يقرر غالبية الحزب. لذلك ، يجب أن يتم الانتقال بالإجماع ، مما يعني أن كل إجراء سينتظر غالبية الحزب ليطلبه قبل أن يحدث.
قتال
تعتبر Combat جانبًا آخر مهمًا جدًا لهذه الأنواع من الألعاب ، وهو جانب علينا التفكير في إضافته إلى محركنا ؛ وإلا ، فسننتهي ببعض المرح.
لنكون صادقين ، هذا ليس شيئًا يحتاج إلى إعادة اختراع. كان القتال الجماعي القائم على الدوران موجودًا منذ عقود ، لذلك سنقوم فقط بتنفيذ نسخة من هذا الميكانيكي. سنقوم بخلطها مع مفهوم Dungeons & Dragons لـ "المبادرة" ، مع طرح رقم عشوائي من أجل الحفاظ على القتال أكثر ديناميكية.
بعبارة أخرى ، سيكون الترتيب الذي يختار به كل شخص مشارك في معركة ما فعلته عشوائيًا ، وهذا يشمل الأعداء.
أخيرًا (على الرغم من أنني سأتطرق إلى هذا بمزيد من التفصيل أدناه) ، سيكون لديك عناصر يمكنك التقاطها برقم "ضرر" محدد. هذه هي العناصر التي ستتمكن من استخدامها أثناء القتال ؛ أي شيء لا يحتوي على تلك الممتلكات سوف يسبب 0 ضررًا لأعدائك. من المحتمل أن نضيف رسالة عندما تحاول استخدام هذه الأشياء للقتال ، حتى تعرف أن ما تحاول القيام به لا معنى له.
التفاعل بين العميل والخادم
دعنا نرى الآن كيف سيتفاعل عميل معين مع خادمنا باستخدام الوظيفة المحددة مسبقًا (لا تفكر في نقاط النهاية بعد ، لكننا سنصل إلى هناك في غضون ثوانٍ):

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

بمجرد تلبية جميع المتطلبات الأساسية ، يمكن للاعبين البدء في لعب المغامرة ومشاركة أفكارهم من خلال الدردشة الجماعية وتطوير القصة. يوضح الرسم البياني أعلاه الخطوات الأربع المطلوبة لذلك.
سيتم تنفيذ الخطوات التالية كجزء من حلقة اللعبة ، مما يعني أنها ستتكرر باستمرار حتى تنتهي اللعبة.
- طلب المشهد .
سيطلب تطبيق العميل البيانات الوصفية للمشهد الحالي. هذه هي الخطوة الأولى في كل تكرار للحلقة. - إعادة البيانات الوصفية .
سيرسل الخادم بدوره البيانات الوصفية للمشهد الحالي. ستتضمن هذه المعلومات أشياء مثل الوصف العام والأشياء الموجودة بداخلها وكيفية ارتباطها ببعضها البعض. - أرسل الأمر .
هنا يبدا المرح. هذا هو المدخل الرئيسي من اللاعب. سوف يحتوي على الإجراء الذي يريدون القيام به ، واختيارياً ، الهدف من هذا الإجراء (على سبيل المثال ، نفخ الشمعة ، والاستيلاء على الصخور ، وما إلى ذلك). - إعادة رد الفعل إلى الأمر المرسل .
يمكن أن تكون هذه ببساطة الخطوة الثانية ، ولكن من أجل التوضيح ، أضفتها كخطوة إضافية. الاختلاف الرئيسي هو أن الخطوة الثانية يمكن اعتبارها بداية هذه الحلقة ، بينما تأخذ هذه الخطوة في الاعتبار أنك تلعب بالفعل ، وبالتالي ، يحتاج الخادم إلى فهم من سيؤثر هذا الإجراء (إما لاعب واحد أو كل اللاعبين).
كخطوة إضافية ، على الرغم من أنه ليس جزءًا من التدفق بالفعل ، سيقوم الخادم بإعلام العملاء بتحديثات الحالة ذات الصلة بهم.
يعود سبب هذه الخطوة المتكررة الإضافية إلى التحديثات التي يمكن للاعب تلقيها من تصرفات اللاعبين الآخرين. أذكر مطلب الانتقال من مكان إلى آخر ؛ كما قلت من قبل ، بمجرد اختيار غالبية اللاعبين للاتجاه ، سيتحرك جميع اللاعبين (لا يلزم إدخال أي مدخلات من جميع اللاعبين).
الشيء المثير للاهتمام هنا هو أن HTTP (لقد ذكرنا بالفعل أن الخادم سيكون واجهة برمجة تطبيقات REST) لا يسمح بهذا النوع من السلوك. إذن ، خياراتنا هي:
- إجراء استقصاء كل X مقدار الثواني من العميل ،
- استخدام نوع من نظام الإخطار الذي يعمل بالتوازي مع اتصال خادم العميل.
من واقع خبرتي ، أميل إلى تفضيل الخيار 2. في الواقع ، سأستخدم (وسأستخدم هذه المقالة) Redis لهذا النوع من السلوك.
يوضح الرسم البياني التالي التبعيات بين الخدمات.

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

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

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

من خلال الرسم البياني الموزون أعلاه ، يمكننا التأكد من عدم تمكن اللاعبين من الانتقال من المدخل إلى الجناح الأيسر. سيتعين عليهم المرور من خلال العقد الموجودة بين هذين ، وسيستهلك ذلك الوقت ، والذي يمكننا قياسه باستخدام الوزن من الوصلات.
الآن ، إلى الجزء "المرح". دعونا نرى كيف سيبدو الرسم البياني بتنسيق JSON. تحمل معي هنا. سيحتوي JSON هذا على الكثير من المعلومات ، لكنني سأستعرض أكبر قدر ممكن منها:
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
{ "graph": [ { "id": "entrance", "name": "Entrance", "north": { "node": "1stroom", "distance": 1 } }, { "id": "1st room", "name": "1st Room", "south": {"node": "entrance", "distance": 1} , "north": { "node": "bigroom", "distance": 1} } , { "id": "bigroom", "name": "Big room", "south": { "node": "1stroom", "distance": 1}, "north": { "node": "bossroom", "distance": 2}, "east": { "node": "rightwing", "distance": 3} , "west": { "node": "leftwing", "distance": 3} }, { "id": "bossroom", "name": "Boss room", "south": {"node": "bigroom", "distance": 2} } { "id": "leftwing", "name": "Left Wing", "east": {"node": "bigroom", "distance": 3} } { "id": "rightwing", "name": "Right Wing", "west": { "node": "bigroom", "distance": 3 } } ], "game": { "win-condition": { "source": "finalboss", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } }, "lose-condition": { "source": "player", "condition": { "type": "comparison", "left": "hp", "right": "0", "symbol": "<=" } } }, "rooms": { "entrance": { "description": { "default": "You're at the entrance of the dungeon. There are two lit torches on each wall (one on your right and one on your left). You see only one path: ahead." }, "items": [ { "id": "littorch1", "name": "Lit torch on the right", "triggers": [ { "action": "grab", //grab Lit torch on the right "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" }, { "id": "littorch2", "name": "Lit torch on the left", "triggers": [ { "action": "grab", //grab Lit torch on the left "effect":{ "statusUpdate": "has light", "target": "game", } } ] , "destination": "hand" } ] }, "1stroom": { "description": { "default": "You're in a very dark room. There are no windows and no source of light, other than the one at the entrance. You get the feeling you're not alone here.", "conditionals": { "has light": "The room you find yourself in appears to be empty, aside from a single chair in the right corner. There appears to be only one way out: deeper into the dungeon." } }, "items": [ { "id": "chair", "name": "Wooden chair", "details": "It's a wooden chair, nothing fancy about it. It appears to have been sitting here, untouched, for a while now.", "subitems": [ { "id": "woodenleg", "name": "Wooden leg", "triggeractions": [ { "action": "break", "target": "chair"}, //break { "action": "throw", "target": "chair"} //throw ], "destination": "inventory", "damage": 2 } ] } ] }, "bigroom": { "description": { "default": "You've reached the big room. On every wall are torches lighting every corner. The walls are painted white, and the ceiling is tall and filled with painted white stars on a black background. There is a gateway on either side and a big, wooden double door in front of you." }, "exits": { "north": { "id": "bossdoor", "name": "Big double door", "status": "locked", "details": "A aig, wooden double door. It seems like something big usually comes through here."} }, "items": [] }, "leftwing": { "description": { "default": "Another dark room. It doesn't look like it's that big, but you can't really tell what's inside. You do, however, smell rotten meat somewhere inside.", "conditionals": { "has light": "You appear to have found the kitchen. There are tables full of meat everywhere, and a big knife sticking out of what appears to be the head of a cow." } }, "items": [ { "id": "bigknife", "name": "Big knife", "destination": "inventory", "damage": 10} ] }, "rightwing": { "description": { "default": "This appear to be some sort of office. There is a wooden desk in the middle, torches lighting every wall, and a single key resting on top of the desk." }, "items": [ { "id": "key", "name": "Golden key", "details": "A small golden key. What use could you have for it?", "destination": "inventory", "triggers": [{ "action": "use", //use on north exit (contextual) "target": { "room": "bigroom", "exit": "north" }, "effect": { "statusUpdate": "unlocked", "target": { "room": "bigroom", "exit": "north" } } } ] } ] }, "bossroom": { "description": { "default": "You appear to have reached the end of the dungeon. There are no exits other than the one you just came in through. The only other thing that bothers you is the hulking giant looking like it's going to kill you, standing about 10 feet from you." }, "npcs": [ { "id": "finalboss", "name": "Hulking Ogre", "details": "A huge, green, muscular giant with a single eye in the middle of his forehead. It doesn't just look bad, it also smells like hell.", "stats": { "hp": 10, "damage": 3 } } ] } } }
أعلم أن الأمر يبدو كثيرًا ، ولكن إذا اختزلته في وصف بسيط للعبة ، فلديك زنزانة تتكون من ست غرف ، كل واحدة متصلة ببعضها البعض ، كما هو موضح في الرسم البياني أعلاه.

مهمتك هي التحرك من خلالها واستكشافها. ستجد هناك مكانين مختلفين حيث يمكنك العثور على سلاح (إما في المطبخ أو في الغرفة المظلمة ، عن طريق كسر الكرسي). ستواجه أيضًا بابًا مغلقًا ؛ لذلك ، بمجرد العثور على المفتاح (الموجود داخل غرفة تشبه المكتب) ، ستتمكن من فتحه ومحاربة الرئيس بأي سلاح قمت بجمعه.
إما أن تربح بقتله أو تخسر بقتلك.
دعنا الآن ندخل في نظرة عامة أكثر تفصيلاً عن بنية JSON بأكملها وأقسامها الثلاثة.
رسم بياني
سيحتوي هذا على العلاقة بين العقد. في الأساس ، يُترجم هذا القسم مباشرةً إلى الرسم البياني الذي نظرنا إليه من قبل.
هيكل هذا القسم واضح ومباشر. إنها قائمة بالعقد ، حيث تشتمل كل عقدة على السمات التالية:
- معرف يحدد بشكل فريد العقدة من بين جميع العناصر الأخرى في اللعبة ؛
- اسم ، وهو في الأساس نسخة يمكن قراءتها من قبل الإنسان من المعرّف ؛
- مجموعة من الروابط إلى العقد الأخرى. يتضح هذا من خلال وجود أربعة مفاتيح محتملة: الشمال والجنوب والشرق والغرب. يمكننا في النهاية إضافة المزيد من الاتجاهات عن طريق إضافة مجموعات من هذه الأربعة. يحتوي كل ارتباط على معرف العقدة ذات الصلة والمسافة (أو الوزن) لتلك العلاقة.
لعبة
سيحتوي هذا القسم على الإعدادات والشروط العامة. على وجه الخصوص ، في المثال أعلاه ، يحتوي هذا القسم على شروط الفوز والخسارة. بعبارة أخرى ، مع هذين الشرطين ، سنخبر المحرك متى يمكن أن تنتهي اللعبة.
لتبسيط الأمور ، أضفت شرطين فقط:
- إما أن تربح بقتل الرئيس ،
- أو تخسر بالقتل.
غرف
هذا هو المكان الذي تأتي منه معظم السطور الـ 163 ، وهي أكثر الأقسام تعقيدًا. هذا هو المكان الذي سنصف فيه جميع الغرف في مغامرتنا وكل شيء بداخلها.
سيكون هناك مفتاح لكل غرفة ، باستخدام المعرف الذي حددناه من قبل. وستحتوي كل غرفة على وصف وقائمة بالعناصر وقائمة بالمخارج (أو الأبواب) وقائمة بالشخصيات غير القابلة للعب (NPCs). من بين هذه الخصائص ، الخاصية الوحيدة التي يجب أن تكون إلزامية هي الوصف ، لأن ذلك مطلوب للمحرك لإعلامك بما تراه. لن يكون الباقون هناك إلا إذا كان هناك شيء لإظهاره.
دعونا ننظر في ما يمكن أن تفعله هذه الخصائص للعبتنا.
الوصف
هذا العنصر ليس بالبساطة التي قد يظن المرء ، لأن رؤيتك للغرفة يمكن أن تتغير حسب الظروف المختلفة. على سبيل المثال ، إذا نظرت إلى وصف الغرفة الأولى ، ستلاحظ أنه افتراضيًا ، لا يمكنك رؤية أي شيء ، ما لم يكن لديك بالطبع مصباحًا مضاءًا معك.
لذلك ، قد يؤدي اختيار العناصر واستخدامها إلى ظهور ظروف عالمية ستؤثر على أجزاء أخرى من اللعبة.
العناصر
هذه تمثل كل الأشياء "التي يمكنك أن تجدها داخل الغرفة. يشترك كل عنصر في نفس المعرف والاسم اللذين كانت للعقد في قسم الرسم البياني.
سيكون لديهم أيضًا خاصية "الوجهة" ، والتي تشير إلى مكان تخزين هذا العنصر ، بمجرد التقاطه. هذا ملائم لأنك ستتمكن من الحصول على عنصر واحد فقط في يديك ، بينما ستتمكن من الحصول على أي عنصر تريده في مخزونك.
أخيرًا ، قد تؤدي بعض هذه العناصر إلى تشغيل إجراءات أخرى أو تحديثات الحالة ، اعتمادًا على ما يقرر اللاعب فعله بها. أحد الأمثلة على ذلك هو المصابيح المضاءة من المدخل. إذا حصلت على واحد منهم ، فستقوم بتشغيل تحديث الحالة في اللعبة ، والذي بدوره سيجعل اللعبة تعرض لك وصفًا مختلفًا للغرفة المجاورة.
يمكن أن تحتوي العناصر أيضًا على "عناصر فرعية" ، والتي يتم تشغيلها بمجرد إتلاف العنصر الأصلي (من خلال إجراء "الفاصل" ، على سبيل المثال). يمكن تقسيم العنصر إلى عدة عناصر ، ويتم تحديد ذلك في عنصر "العناصر الفرعية".
بشكل أساسي ، هذا العنصر هو مجرد مجموعة من العناصر الجديدة ، عنصر يحتوي أيضًا على مجموعة من الإجراءات التي يمكن أن تؤدي إلى إنشائها. يفتح هذا بشكل أساسي إمكانية إنشاء عناصر فرعية مختلفة بناءً على الإجراءات التي تقوم بها على العنصر الأصلي.
أخيرًا ، سيكون لبعض العناصر خاصية "الضرر". لذلك ، إذا كنت تستخدم عنصرًا لضرب NPC ، فسيتم استخدام هذه القيمة لطرح الحياة منها.
المخارج
هذه ببساطة مجموعة من الخصائص التي تشير إلى اتجاه المخرج وخصائصه (وصف ، في حالة رغبتك في فحصه ، واسمه ، وفي بعض الحالات ، حالته).
تعتبر عمليات الخروج كيانًا منفصلاً عن العناصر لأن المحرك سيحتاج إلى فهم ما إذا كان يمكنك اجتيازها بالفعل بناءً على حالتها. لن تسمح لك المخارج المقفلة بالمرور عبرها إلا إذا كنت تعمل على كيفية تغيير حالتها إلى "غير مقفلة".
الشخصيات
أخيرًا ، ستكون الشخصيات غير القابلة للعب جزءًا من قائمة أخرى. إنها في الأساس عناصر بها إحصائيات سيستخدمها المحرك لفهم كيف يجب أن يتصرف كل واحد. تلك التي حددناها في مثالنا هي "hp" ، والتي تعني نقاط الصحة ، و "الضرر" ، والتي ، تمامًا مثل الأسلحة ، هي الرقم الذي ستطرحه كل ضربة من صحة اللاعب.
هذا هو من أجل الزنزانة التي صنعتها. إنه كثير ، نعم ، وفي المستقبل قد أفكر في إنشاء محرر مستوى من نوع ما ، لتبسيط إنشاء ملفات JSON. لكن في الوقت الحالي ، لن يكون ذلك ضروريًا.
في حال لم تكن قد أدركت ذلك بعد ، فإن الفائدة الرئيسية من تحديد لعبتنا في ملف مثل هذا هي أننا سنكون قادرين على تبديل ملفات JSON كما فعلت مع الخراطيش في عصر Super Nintendo. فقط قم بتحميل ملف جديد وابدأ مغامرة جديدة. سهل!
خواطر ختامية
شكرا على القراءة حتى الآن. أتمنى أن تكون قد استمتعت بعملية التصميم التي أجريها لإحياء فكرة. تذكر ، مع ذلك ، أنني أصنع هذا كما أذهب ، لذلك قد ندرك لاحقًا أن شيئًا ما حددناه اليوم لن ينجح ، وفي هذه الحالة سيتعين علينا التراجع وإصلاحه.
أنا متأكد من أن هناك الكثير من الطرق لتحسين الأفكار المقدمة هنا ولجعل محرك واحد جحيم. لكن هذا سيتطلب كلمات أكثر بكثير مما يمكنني وضعه في مقال دون جعله مملًا للجميع ، لذلك سنترك الأمر عند هذا الحد الآن.
أجزاء أخرى من هذه السلسلة
- الجزء 2: تصميم خادم محرك اللعبة
- الجزء 3: إنشاء عميل المحطة الطرفية
- الجزء 4: إضافة الدردشة إلى لعبتنا