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

خلال تطوير PvP متزامن جديد للأجهزة المحمولة ، واجهنا مشاكل نموذجية من النوع:
- جودة اتصال عملاء الجوال رديئة. هذا متوسط مرتفع نسبيًا لـ ping في المنطقة 200-250 مللي ثانية ، وتوزيع غير مستقر للوقت بينغ مع الأخذ في الاعتبار تغيير نقاط الوصول (على الرغم من أنه ، على عكس الاعتقاد الشائع ، فإن النسبة المئوية لفقدان الحزم في شبكات 3G + المحمولة منخفضة جدًا - حوالي 1 ٪).
- الحلول التقنية الحالية هي أطر عمل ضخمة تدفع المطورين إلى أطر عمل ضيقة.
لقد صنعنا أول نموذج أولي في UNet ، على الرغم من أنه فرض قيودًا على قابلية التوسع والتحكم في مكون الشبكة وإضافة الاعتماد على الاتصال المتقلب للعملاء الرئيسيين. ثم تحولنا إلى رمز شبكة مكتوب ذاتيًا فوق
خادم الفوتون ، ولكن أكثر من ذلك لاحقًا.
فكر في آليات تنظيم التفاعلات بين العملاء في ألعاب PvP متزامنة. الأكثر شعبية:
- P2P أو نظير إلى نظير . يتم استضافة كل منطق المباراة على أحد العملاء ولا يتطلب تقريبًا أي تكاليف مرور منا. لكن نطاق الغشاشين والمتطلبات العالية للعميل الذي يستضيف المباراة ، بالإضافة إلى قيود NAT لم تسمح لنا بأخذ هذا الحل لألعاب الهاتف المحمول.
- خادم العميل . يتيح لك الخادم المخصص ، على العكس من ذلك ، التحكم الكامل في كل ما يحدث في المباراة (وداعًا والغشاشون) ، ويسمح لك أدائه بحساب بعض الأشياء الخاصة بمشروعنا. أيضًا ، لدى العديد من مزودي الاستضافة الكبار بنية الشبكة الفرعية الخاصة بهم ، مما يوفر الحد الأدنى من التأخير للمستخدم النهائي.
تقرر كتابة خادم استبدادي.
التواصل مع نظير إلى نظير (يسار) وخادم العميل (يمين)نقل البيانات بين العميل والخادم
نحن نستخدم
Photon Server - وهذا سمح لنا بنشر البنية التحتية اللازمة للمشروع بسرعة على أساس مخطط تم وضعه بالفعل على مر السنين (في War Robots نستخدمه).
خادم الفوتون هو حل نقل حصري لنا ، بدون تصميمات عالية المستوى مرتبطة بقوة بمحرك ألعاب معين. مما يعطي بعض المزايا ، حيث يمكن استبدال مكتبة نقل البيانات في أي وقت.
خادم اللعبة هو تطبيق متعدد الخيوط في حاوية الفوتون. يتم إنشاء دفق منفصل لكل مباراة ، والذي يغلف منطق العمل بأكمله ويمنع تأثير مباراة على أخرى. يتم التحكم في جميع اتصالات الخادم بواسطة الفوتون ، وتتم إضافة البيانات التي تأتي إليها من العملاء إلى قائمة الانتظار ، والتي يتم تحليلها بعد ذلك إلى ECS.
مخطط عام لتدفقات المطابقة في حاوية خادم الفوتونتتكون كل مباراة من عدة مراحل:
- يصنف عميل اللعبة في خدمة ما يسمى بالمطابقة. بمجرد أن يتم جمع العدد المطلوب من اللاعبين الذين يستوفون شروطًا معينة ، يقوم بإبلاغ خادم اللعبة باستخدام gRPC. في نفس الوقت ، يتم نقل جميع البيانات اللازمة لإنشاء اللعبة.

مخطط عام لإنشاء مباراة - على خادم اللعبة ، يبدأ تهيئة المباراة. تتم معالجة جميع معلمات المطابقة وإعدادها ، بما في ذلك بيانات الخريطة ، وكذلك جميع بيانات العملاء المستلمة من خدمة إنشاء المطابقة. تعني معالجة البيانات وإعدادها أننا نحلل جميع البيانات اللازمة ونكتبها إلى مجموعة فرعية خاصة من الكيانات نسميها RuleBook. يقوم بتخزين إحصائيات المطابقة (التي لا تتغير أثناء الدورة التدريبية) وسيتم إرسالها إلى جميع العملاء أثناء عملية الاتصال والتفويض على خادم اللعبة مرة واحدة أو عند إعادة الاتصال بعد فقد الاتصال. تتضمن بيانات المطابقة الثابتة تكوين الخريطة (عرض الخريطة بواسطة مكونات ECS التي تربطها بالمحرك المادي) ، وبيانات العملاء (الألقاب ، ومجموعة من الأسلحة التي يمتلكونها ولا تتغير أثناء المعركة ، وما إلى ذلك).
- إدارة المباراة. تبدأ أنظمة ECS التي تشكل اللعبة على الخادم في العمل. جميع الأنظمة تدق 30 إطارًا في الثانية.
- يقوم كل إطار بقراءة مدخلات اللاعب أو نسخه وتفريغها إذا لم يرسل اللاعبون مدخلاتهم خلال فترة زمنية معينة.
- ثم ، في نفس الإطار ، تتم معالجة المدخلات في نظام ECS ، وهي: تغيير حالة اللاعب ؛ العالم الذي يؤثر فيه بإسهاماته ؛ ووضع اللاعبين الآخرين.
- في نهاية الإطار ، يتم حزم الحالة العالمية الناتجة للاعب وإرسالها عبر الشبكة.
- في نهاية المباراة ، يتم إرسال النتائج إلى العملاء وإلى الخدمات الصغيرة ، التي تعالج مكافآت المعركة باستخدام gRPC ، وكذلك المحلل في المباراة.
- بعد ذلك ، يتم إغلاق أسافين تدفق المباراة وإغلاق التدفق.
تسلسل الإجراءات على الخادم ضمن إطار واحدمن جانب العميل ، تكون عملية الاتصال بالمباراة كما يلي:
- أولاً ، يتم إجراء طلب في قائمة الانتظار في الخدمة لإنشاء تطابقات من خلال websocket مع التسلسل من خلال protobuf.
- عند إنشاء مباراة ، تُعلم هذه الخدمة العميل بعنوان خادم اللعبة وتنقل الحمولة الإضافية التي يطلبها العميل قبل المباراة. العميل جاهز الآن لبدء عملية التفويض على خادم اللعبة.
- يقوم العميل بإنشاء مأخذ UDP ويبدأ في إرسال طلب إلى خادم اللعبة للاتصال بالمباراة مع بعض بيانات الاعتماد. الخادم ينتظر هذا العميل بالفعل. عند الاتصال ، يمنحه جميع البيانات اللازمة لبدء اللعبة وعرض العالم لأول مرة. وهي تشمل: RuleBook (قائمة بيانات ثابتة للمباراة) ، بالإضافة إلى StringIntMap ، والتي نشير إليها على أنها بيانات حول الخطوط المستخدمة في اللعب والتي سيتم تحديدها بواسطة الأعداد الصحيحة أثناء المباراة). هذا ضروري لحفظ حركة المرور ، لأنه يؤدي تمرير الخطوط في كل إطار إلى إنشاء حمل كبير على الشبكة. على سبيل المثال ، جميع أسماء اللاعبين وأسماء الفئات ومعرفات الأسلحة والحسابات وما شابه ذلك ، يتم كتابة جميع المعلومات إلى StringIntMap ، حيث يتم ترميزها باستخدام بيانات عددية بسيطة.
عندما يؤثر اللاعب بشكل مباشر على المستخدمين الآخرين (يتسبب في حدوث ضرر ، ويفرض تأثيرات ، وما إلى ذلك) ، يتم البحث في سجل الدولة على الخادم لمقارنة عالم اللعبة الذي يراه العميل بالفعل في علامة محاكاة محددة مع ما كان يحدث على الخادم مع الآخرين في تلك اللحظة كيانات اللعبة.
على سبيل المثال ، أنت تطلق النار على عميلك. بالنسبة لك ، يحدث هذا على الفور ، ولكن العميل "هرب" بالفعل لبعض الوقت في المستقبل مقارنة بالعالم المحيط الذي يعرضه. لذلك ، نظرًا للتنبؤ المحلي بسلوك اللاعب ، يحتاج الخادم إلى فهم مكان وحالة الخصوم في وقت اللقطة (ربما كانوا بالفعل ميتين أو على العكس من ذلك). يقوم الخادم بالتحقق من جميع العوامل ويصدر حكمه بشأن الضرر الذي حدث.
طلب إنشاء مباراة والاتصال بخادم اللعبة والتفويضتسلسل وإلغاء تسلسل وتغليف وتفريغ البايتات الأولى من المباراة
لدينا تسلسل بيانات ثنائية مملوكة ، ولنا ننقل البيانات نستخدم UDP.
UDP هو الخيار الأكثر وضوحًا لإرسال الرسائل بسرعة بين العميل والخادم ، حيث يكون عرض البيانات في أسرع وقت ممكن أكثر أهمية من عرضها من حيث المبدأ. تقوم الحزم المفقودة بإجراء تعديلات ، ولكن يتم حل المشكلات لكل حالة على حدة ، مثل نظرًا لأن البيانات تأتي باستمرار من العميل إلى الخادم والعكس ، يمكنك إدخال مفهوم الاتصال بين العميل والخادم.
لإنشاء كود مثالي ومريح بناءً على الوصف التعريفي لهيكل ECS لدينا ، نستخدم إنشاء التعليمات البرمجية. عند إنشاء المكونات ، يتم إنشاء قواعد التسلسل وإلغاء التسلسل لها أيضًا. يعتمد التسلسل على باكر ثنائي مخصص يسمح لك بحزم البيانات بطريقة اقتصادية. مجموعة البايت التي تم الحصول عليها أثناء تشغيلها ليست الأكثر مثالية ، ولكنها تتيح لك إنشاء دفق يمكنك من خلاله قراءة بعض بيانات الحزمة دون الحاجة إلى إلغاء التسلسل الكامل لها.
إن الحد الأقصى لنقل البيانات الذي يبلغ 1500 بايت (ويعرف أيضًا باسم MTU) هو في الواقع الحد الأقصى لحجم الحزمة التي يمكن نقلها عبر إيثرنت. يمكن تكوين هذه الخاصية في كل قفزة من الشبكة وغالباً ما تكون أقل من 1500 بايت. ماذا يحدث إذا قمت بإرسال حزمة أكبر من 1500 بايت؟ يبدأ تجزئة الحزمة. على سبيل المثال سيتم تقسيم كل حزمة بالقوة إلى عدة أجزاء ، والتي سيتم إرسالها بشكل منفصل من واجهة إلى أخرى. يمكن إرسالها عن طريق مسارات مختلفة تمامًا ، ويمكن أن يزيد وقت تلقي هذه الحزم بشكل ملحوظ قبل أن تصدر طبقة الشبكة حزمة ملصقة لتطبيقك.
في حالة الفوتون ، تبدأ المكتبة بالقوة في إرسال مثل هذه الحزم في وضع UDP الموثوق. على سبيل المثال سينتظر الفوتون كل جزء من الحزمة ، بالإضافة إلى إعادة توجيه الأجزاء المفقودة إذا تم فقدها أثناء إعادة التوجيه. لكن هذا العمل من جانب الشبكة غير مقبول في الألعاب التي تتطلب الحد الأدنى من التأخير في الشبكة. لذلك ، يوصى بتقليل حجم الحزم المعاد توجيهها إلى الحد الأدنى وعدم تجاوز 1500 بايت الموصى بها (في لعبتنا ، لا يتجاوز حجم حالة كاملة واحدة في العالم 1000 بايت ؛ حجم الحزمة مع ضغط دلتا هو 200 بايت).
تحتوي كل حزمة من الخادم على رأس قصير يحتوي على عدة بايت تصف نوع الحزمة. يقوم العميل أولاً بفك مجموعة البايت هذه ويحدد الحزمة التي نتعامل معها. نحن نعتمد بشكل كبير على هذه الخاصية لآلية إلغاء التسلسل الخاصة بنا أثناء التفويض: حتى لا يتجاوز حجم الحزمة الموصى به وهو 1500 بايت ، نقوم بتقسيم حزم RuleBook و StringIntMap إلى عدة مراحل ؛ ولفهم ما حصلنا عليه بالضبط من الخادم - قواعد اللعبة أو الدولة نفسها - نستخدم رأس الحزمة.
عند تطوير ميزات جديدة للمشروع ، ينمو حجم العبوة بشكل مطرد. عندما واجهنا هذه المشكلة ، تقرر كتابة نظام ضغط دلتا الخاص بنا ، بالإضافة إلى قصاصة البيانات التي لا يحتاجها العميل.
تحسين حركة مرور الشبكة الحساسة للسياق. ضغط دلتا
تتم كتابة قصاصات البيانات السياقية يدويًا بناءً على البيانات التي يحتاجها العميل لعرض العالم والتنبؤ المحلي للبيانات الخاصة به بشكل صحيح للعمل بشكل صحيح. ثم يتم تطبيق ضغط دلتا على البيانات المتبقية.
لعبتنا كل علامة تنتج حالة جديدة من العالم ، والتي يجب تعبئتها ونقلها إلى العملاء. عادةً ما يكون ضغط دلتا هو إرسال حالة كاملة أولاً مع كافة البيانات الضرورية إلى العميل ، ثم إرسال التغييرات على هذه البيانات فقط. يمكن تمثيل ذلك على النحو التالي:
deltaGameState = newGameState - prevGameStateولكن لكل عميل يتم إرسال بيانات مختلفة ويمكن أن يؤدي فقدان حزمة واحدة فقط إلى حقيقة أنه يجب عليك إعادة توجيه الحالة الكاملة للعالم.
إن إعادة توجيه الحالة الكاملة للعالم مهمة مكلفة إلى حد ما بالنسبة للشبكة. لذلك ، قمنا بتعديل النهج وإرسال الفرق بين الحالة الحالية للمعالجة في العالم والحالة التي يتم تلقيها بالضبط من قبل العميل. للقيام بذلك ، يرسل العميل في حزمته مع الإدخال أيضًا رقمًا محددًا ، وهو معرف فريد لحالة اللعبة التي تلقاها بالفعل بالضبط. الآن يعرف الخادم على أساس الحالة التي من الضروري بناء ضغط دلتا. عادة لا يكون لدى العميل الوقت لإرسال رقم التجزئة الخاص بالخادم قبل أن يقوم الخادم بإعداد الإطار التالي مع البيانات. لذلك ، على العميل هناك تاريخ حالات الخادم في العالم ، والتي يتم تطبيق تصحيح deltaGameState الذي تم إنشاؤه بواسطة الخادم.
توضيح تواتر التفاعل بين العميل والخادم في المشروعدعونا نتناول بمزيد من التفصيل ما يرسله العميل. في الرماة الكلاسيكية ، تسمى هذه الحزمة ClientCmd وتحتوي على معلومات حول المفاتيح المضغوطة للاعب والوقت الذي تم فيه إنشاء الفريق. داخل حزمة الإدخال ، نرسل المزيد من البيانات:
public sealed class InputSample {
هناك بعض النقاط المثيرة للاهتمام. أولاً ، يخبر العميل الخادم الذي يضع علامة على أنه يرى جميع كائنات عالم اللعبة المحيطة به أنه غير قادر على التنبؤ (WorldTick). قد يبدو أن العميل قادر على "إيقاف" الوقت للعالم ، والركض وإطلاق النار على الجميع بنفسه بسبب التنبؤ المحلي. الأمر ليس كذلك. نحن نثق فقط في مجموعة محدودة من القيم من العميل ولا نسمح له بالتصوير في الماضي لأكثر من ثانية واحدة. يُستخدم حقل WorldTick أيضًا كحزمة إقرار ، حيث يتم بناء ضغط دلتا على أساسه.
يمكنك العثور على أرقام الفاصلة العائمة في حزمة. عادةً ، يتم استخدام هذه القيم غالبًا لأخذ قراءات من عصا التحكم الخاصة باللاعب ، ولكنها لا تنتقل بشكل جيد عبر الشبكة ، نظرًا لأنها تحتوي على "ارتداد" كبير وعادة ما تكون دقيقة للغاية. نحن نقيس هذه الأرقام ونحزم باستخدام باكر ثنائي بحيث لا تتجاوز قيمة عدد صحيح يمكن أن تتناسب مع عدة بت ، اعتمادًا على حجمها. وبالتالي ، فإن تغليف المدخلات من عصا التحكم الهدف مكسور:
if (Math.Abs(s.AimMagnitudeCompressed) < float.Epsilon) { packer.PackByte(0, 1); } else { packer.PackByte(1, 1); float min = 0; float max = 1; float step = 0.001f;
ميزة أخرى مثيرة للاهتمام عند إرسال الإدخال هي أنه يمكن إرسال بعض الأوامر عدة مرات. غالبًا ما يُسألنا عما يجب فعله إذا ضغط الشخص على القدرة القصوى ، وفقدت الحزمة مع مدخلاتها؟ نحن فقط نرسل هذا الإدخال عدة مرات. يبدو هذا التسليم مضمونًا ، ولكنه أكثر مرونة وأسرع. لأن حجم حزمة الإدخال صغير جدًا ، يمكننا حزم العديد من مدخلات اللاعب المجاور في الحزمة الناتجة. في الوقت الحالي ، حجم النافذة التي تحدد عددهم هو خمسة.
يتم إنشاء حزم الإدخال على العميل في كل علامة ويتم إرسالها إلى الخادمإن نقل هذا النوع من البيانات هو الأسرع والأكثر موثوقية بما يكفي لحل مشاكلنا دون استخدام UDP موثوق. ننطلق من حقيقة أن احتمال فقدان هذا العدد من الحزم على التوالي منخفض للغاية وهو مؤشر على تدهور خطير في جودة الشبكة ككل. إذا حدث ذلك ، يقوم الخادم ببساطة بنسخ آخر إدخال تم تلقيه من المشغل وتطبيقه ، على أمل أن يظل دون تغيير.
إذا أدرك العميل أنه لم يتلق حزمًا عبر الشبكة لفترة طويلة جدًا ، فستبدأ عملية إعادة الاتصال بالخادم. يراقب الخادم ، من جانبه ، اكتمال قائمة انتظار الإدخال من المشغل.
بدلا من الاستنتاج والمرجعية
هناك العديد من الأنظمة الأخرى الموجودة على خادم اللعبة والتي تكون مسؤولة عن اكتشاف وتصحيح الأخطاء وتعديل التطابقات "من خلال المكسب" ، ويقوم مصممو الألعاب بتحديث التكوين دون إعادة تشغيل وتسجيل ومراقبة حالة الخوادم. نريد أيضًا أن نكتب عن هذا بمزيد من التفصيل ، ولكن بشكل منفصل.
بادئ ذي بدء ، عند تطوير لعبة شبكة على الأنظمة الأساسية للجوّال ، يجب الانتباه إلى التشغيل الصحيح لعميلك مع الأصوات العالية (حوالي 200 مللي ثانية) ، وفقدان البيانات بشكل متكرر أكثر قليلاً ، بالإضافة إلى حجم البيانات المرسلة. وتحتاج إلى أن تتناسب بوضوح مع حد الحزمة 1500 بايت لتجنب التجزئة وتأخير حركة المرور.
روابط مفيدة:
المقالات السابقة عن المشروع:
- "كيف تأرجحنا على مطلق النار سريع الخطى: التكنولوجيا والنهج . "
- "كيف ولماذا كتبنا ECS . "
- "عندما كتبنا رمز شبكة مطلق النار PvP للجوّال: مزامنة اللاعب على العميل" .