
في الجزء الأول من المقالة ، درسنا كيفية ترتيب النموذج بحيث يكون سهل الاستخدام ، ولكن تصحيحه وتثبيت الواجهات به بسيط. في هذا الجزء سننظر في عودة أوامر التغييرات في النموذج ، بكل جماله وتنوعه. كما كان من قبل ، ستكون الأولوية بالنسبة لنا هي سهولة تصحيح الأخطاء ، مما يقلل من الإيماءات التي يتعين على المبرمج القيام بها لإنشاء ميزة جديدة ، وكذلك إمكانية قراءة الكود للشخص.
الحلول المعمارية للعبة المحمول. الجزء 1: نموذجالحلول المعمارية للعبة المحمول. الجزء 3: عرض على قوة الدفعلماذا القيادة
يبدو نمط الأوامر مرتفعًا ، لكنه في الحقيقة مجرد كائن يتم فيه إضافة كل شيء ضروري للعملية المطلوبة وتخزينها هناك. نختار هذا النهج ، على الأقل لأنه سيتم إرسال فرقنا عبر الشبكة ، وحتى نحصل على بعض نسخ حالة اللعبة للاستخدام الرسمي. لذلك عندما ينقر المستخدم على الزر ، يتم إنشاء مثيل لفئة الأمر وإرساله إلى المستلم. معنى الحرف C في اختصار MVC مختلف بعض الشيء.
التنبؤ بالنتيجة والتحقق من الأوامر عبر الشبكة
في هذه الحالة ، يكون الرمز المحدد أقل أهمية من الفكرة. وإليك الفكرة:
لا يمكن أن تنتظر لعبة تحترم نفسها أي استجابة من الخادم قبل الرد على الزر. بالطبع ، تتحسن شبكة الإنترنت ، ويمكنك الحصول على مجموعة من الخوادم في جميع أنحاء العالم ، وأنا أعلم حتى بعض الألعاب الناجحة التي تنتظر ردا من الخادم ، واحدة منها هي حتى استدعاء الحروب ، ولكن لا تزال لا تحتاج إلى القيام بذلك. نظرًا لأنه من المرجح أن يكون التأخير في الإنترنت عبر الهاتف المحمول من 5 إلى 15 ثانية هو المعيار أكثر من استثناء ، في موسكو يجب أن تكون اللعبة رائعة حقًا حتى لا يهتم اللاعبون بها.
وفقًا لذلك ، لدينا حالة لعبة تمثل جميع المعلومات اللازمة للواجهة ، ويتم تطبيق الأوامر عليها فورًا ، وبعد ذلك يتم إرسالها إلى الخادم. عادة ، يجلس مبرمجو java المجتهدون على الخادم مكررة جميع الوظائف الجديدة من لغة إلى أخرى. في مشروعنا "الغزلان" ، وصل عددهم إلى 3 أشخاص ، وكانت الأخطاء التي ارتكبت عند النقل مصدرًا دائمًا للفرح بعيد المنال. بدلا من ذلك ، يمكننا أن نفعل ذلك بشكل مختلف. نحن نعمل على خادم .Net ونعمل على جانب الخادم بنفس كود القيادة كما هو الحال على العميل.
يعطينا النموذج الموصوف في المقال الأخير فرصة جديدة مثيرة للاختبار الذاتي. بعد تنفيذ الأمر على العميل ، سنقوم بحساب تجزئة التغيير الذي حدث في شجرة GameState ، وتطبيقه على الفريق. إذا كان الخادم ينفذ نفس رمز الأمر ، ولم يتطابق تجزئة التغييرات ، فسيحدث خطأ ما.
المزايا الأولى:
- هذا الحل يسرع عملية التطوير إلى حد كبير ويقلل من عدد مبرمجي الخادم.
- إذا ارتكب مبرمج أخطاء تؤدي إلى سلوك غير حاسم ، على سبيل المثال ، فقد حصل على القيمة الأولى من القاموس ، أو استخدم DateTime.now ، واستخدم عمومًا بعض القيم غير المكتوبة في حقول الأوامر بشكل صريح ، ثم عندما يتم تشغيلها على الخادم ، لن تتطابق التجزئة ، و سوف نعرف عن ذلك.
- يمكن إجراء تطوير العميل في الوقت الحالي بدون خادم على الإطلاق. يمكنك حتى الذهاب إلى ألفا ودية دون وجود خادم. هذا مفيد ليس فقط للمطورين إيندي في عداد المفقودين في لعبة أحلامهم في الليل. عندما كنت في Piksonik ، كانت هناك حالة عندما فقد مبرمج الخادم جميع البوليمرات ، واضطرت لعبتنا إلى الخضوع للاعتدال ، فبدلاً من الخادم ، دافعت غبيًا عن حالة اللعبة بأكملها مرة واحدة كل فترة.
عيب أن يتم التقليل منهجي لسبب ما:
- إذا ارتكب مبرمج العميل خطأً وكان غير مرئي أثناء الاختبار ، على سبيل المثال ، احتمال وجود سلع في الصناديق الغامضة ، فلا يوجد أحد يكتب الشيء نفسه مرة ثانية ويجد خطأً. يتطلب رمز Autoportable موقفًا أكثر مسؤولية تجاه الاختبار.
معلومات تصحيح مفصلة
إحدى أولوياتنا المعلنة هي تصحيح الأخطاء. إذا وقعنا أثناء إعدام الفريق - كان كل شيء واضحًا ، فإننا نعيد حالة اللعبة ، ونرسل الحالة الكاملة إلى السجلات ونقوم بترتيب الأمر الذي أسقطها عليه ، كل شيء مريح وجميل. يكون الموقف أكثر تعقيدًا إذا كان لدينا تصميم متزامن مع الخادم. لأن العميل قد أكمل بالفعل العديد من الأوامر الأخرى منذ ذلك الحين ، واتضح ليس فقط لمعرفة الحالة التي كان عليها النموذج قبل تنفيذ الأمر الذي أدى إلى الكارثة ، ولكنني أرغب حقًا في ذلك. استنساخ gamestate أمام كل فريق معقدة للغاية ومكلفة. لحل المشكلة ، نقوم بتعقيد المخطط المُخيط تحت غطاء المحرك.
في العميل ، لن نحصل على لعبة gamestate واحدة ، ولكن اثنين. يخدم الأول كواجهة رئيسية لتقديم ، يتم تطبيق الأوامر عليه على الفور. بعد ذلك ، يتم وضع الأوامر المطبقة في قائمة الانتظار لإرسالها إلى الخادم. يقوم الخادم بنفس الإجراء من جانبه ويؤكد أن كل شيء على ما يرام. بعد تلقي التأكيد ، يأخذ العميل نفس الأمر ويطبقه على gamestate الثاني ، ليصل به إلى الحالة التي تم تأكيدها بالفعل بواسطة الخادم على أنها صحيحة. في الوقت نفسه ، لدينا أيضًا فرصة لمقارنة تجزئة التغييرات التي تم إجراؤها لتكون آمنة ، ويمكننا أيضًا مقارنة التجزئة الكاملة للشجرة بأكملها على العميل ، والتي يمكننا حسابها بعد تنفيذ الأمر ، وزنه قليلاً ويعتبر سريعًا بدرجة كافية. إذا لم يوضح الخادم أن كل شيء على ما يرام ، فإنه يطلب من العميل الحصول على تفاصيل حول ما حدث ، ويمكن للعميل أن يرسل له سلسلة ثانية متسلسلة تمامًا كما بدا قبل أن يتم تنفيذ الأمر بنجاح على العميل.
يبدو الحل جذابًا للغاية ، لكنه يثير مشكلتين تحتاج إلى حل على مستوى الكود:
- من بين معلمات الأمر ، لا يمكن أن يكون هناك أنواع بسيطة فحسب ، بل يمكن أيضًا ربطها بنماذج. في gamestate آخر ، في نفس المكان بالضبط توجد كائنات أخرى من النموذج. نحل هذه المشكلة بالطريقة التالية: قبل تنفيذ الأمر على العميل ، نقوم بتسلسل جميع بياناته. من بينها قد تكون هناك روابط لنماذج ، والتي سنكتبها في شكل مسار للنموذج من جذر حالة اللعبة. نحن نفعل هذا قبل الفريق ، لأنه بعد تنفيذه قد تتغير المسارات. بعد ذلك ، نرسل هذا المسار إلى الخادم ، وستتمكن gamestate من الخادم من الحصول على رابط لطرازه على طول الطريق. وبالمثل ، عندما يتم تطبيق فريق على حالة اللعبة الثانية ، يمكن الحصول على النموذج من حالة اللعبة الثانية.
- بالإضافة إلى الأنواع والنماذج الأولية ، قد يكون لدى الفريق روابط إلى المجموعات. قاموس <مفتاح ، نموذج> ، قاموس <نموذج ، مفتاح> ، قائمة <نموذج> ، قائمة <قيمة>. لهم جميعا ، لديهم لكتابة المتسللين. صحيح ، لا يمكنك التسرع في هذا ، في مشروع حقيقي مثل هذه الحقول نادرا ما تنشأ بشكل مفاجئ.
- لا يعد إرسال الأوامر إلى الخادم واحدًا تلو الآخر فكرة جيدة ، لأن المستخدم يمكنه إنتاجها بشكل أسرع مما يستطيع الإنترنت سحبها ذهابًا وإيابًا. بدلاً من إرسال الأوامر واحدًا تلو الآخر ، سوف نرسلها على دفعات متعددة القطع. في هذه الحالة ، بعد تلقي استجابة من الخادم تفيد بحدوث خطأ ما ، ستحتاج أولاً إلى تطبيق جميع الأوامر السابقة من نفس الحزمة التي تم تأكيدها بواسطة الخادم على الحالة الثانية ، ثم تقوم بمسح وإرسال حالة التحكم الثانية إلى الخادم.
راحة وسهولة كتابة الأوامر
رمز تنفيذ الأوامر هو ثاني أكبر وأول رمز مسؤول في اللعبة. كلما كان الأمر أكثر بساطة ووضوحًا ، وكلما احتاج المبرمج إلى بذل المزيد من الجهد بيديه لكتابته ، زادت سرعة كتابة الكود ، وأخطاء أقل ، وكلما كان متوقعًا بدرجة أكبر ، كلما كان المبرمج أكثر سعادة. أضع رمز التنفيذ مباشرة في الأمر نفسه ، بالإضافة إلى الأجزاء والوظائف العامة الموجودة في فئات قاعدة ثابتة ثابتة ، وغالبًا ما تكون في شكل امتدادات إلى فئات النماذج التي تعمل بها. سأريك مثالين لأوامر من مشروعي للحيوانات الأليفة ، أحدهما بسيط للغاية والآخر أكثر تعقيدًا:
namespace HexKingdoms { public class FCSetSideCostCommand : HexKingdomsCommand {
وهنا السجل الذي يتركه هذا الأمر من تلقاء نفسه ، إذا لم يتم تعطيل هذا السجل لذلك.
[FCSetSideCostCommand id=1 match=FCMatchModel[0] newCost=260] Execute:00:00:00.0027546 Apply:00:00:00.0008689 { "LOCAL_PERSISTENTS":{ "@changed":{ "0":{"SIDE_COST":260}, "1":{"POSSIBLE_COST":260}, "2":{"POSSIBLE_COST":260}}}}
المرة الأولى المشار إليها في السجل هي الوقت الذي تم خلاله إجراء جميع التغييرات اللازمة في النموذج ، والثانية هي الوقت الذي تم خلاله إجراء جميع التغييرات بواسطة وحدات التحكم في الواجهة. يجب أن يظهر هذا في السجل حتى لا يقوم بطريق الخطأ بطيء رهيب ، أو يلاحظ في الوقت المناسب إذا بدأت العمليات تأخذ الكثير من الوقت ببساطة بسبب حجم النموذج نفسه.
بصرف النظر عن المكالمات إلى الكائنات الثابتة على Id-shniks ، والتي تقلل إلى حد كبير من قابلية قراءة السجل ، والتي ، بالمناسبة ، كان يمكن تجنبها هنا ، يكون رمز الأمر نفسه والسجل الذي قام به مع حالة اللعبة واضحًا بشكل مثير للدهشة. يرجى ملاحظة أنه في نص الأمر ، لا يقوم المبرمج بحركة إضافية واحدة. كل ما تحتاجه يتم بواسطة المحرك تحت الغطاء.
الآن دعونا نلقي نظرة على مثال لفريق أكبر
namespace HexKingdoms { public class FCSetUnitForPlayerCommand : HexKingdomsCommand {
وهنا السجل الذي تركه الفريق:
[FCSetUnitForPlayerCommand id=3 screen=/UI_SCREENS[main] unit=militia count=1] Execute:00:00:00.0065625 Apply:00:00:00.0004573 { "LOCAL_PERSISTENTS":{ "@changed":{ "2":{ "UNITS":{ "@set":{"militia":1}}, "ASSIGNED":7}}}, "UI_SCREENS":{ "@changed":{ "main":{ "SELECTED_UNITS":{ "@set":{ "militia":{"@new":null, "TYPE":"militia", "REMARK":null, "COUNT":1, "SELECTED":false, "DISABLED":false, "HIGHLIGHT_GREEN":false, "HIGHLIGHT_RED":false, "BUTTON_ENABLED":false}}}}}}}
كما يقولون ، الأمر أكثر وضوحًا. خذ الوقت الكافي لتزويد الفريق بسجل مناسب وصغير وغني بالمعلومات. هذا هو مفتاح سعادتك. يجب أن يعمل النموذج بسرعة كبيرة ، لذلك استخدمنا مجموعة متنوعة من الحيل مع طرق التخزين والوصول إلى الحقول. يتم تنفيذ الأوامر في أسوأ الحالات مرة واحدة في كل إطار ، في الواقع ، عدة مرات أقل في كثير من الأحيان ، لذلك سنفعل التسلسل وإلغاء تسلسل حقول الأوامر دون أي نزوة ، فقط من خلال التفكير. نحن فقط فرز الحقول حسب الأسماء بحيث يتم إصلاح الترتيب ، حسنا ، سنقوم بتجميع قائمة الحقول مرة واحدة خلال حياة الأمر ، والقراءة والكتابة باستخدام الأساليب الأصلية C #.
نموذج المعلومات للواجهة.
دعنا نخطو الخطوة التالية في تعقيد محركنا ، وهي خطوة تبدو مخيفة ، ولكنها تبسط بشكل كبير كتابة وتصحيح الواجهات. في كثير من الأحيان ، لا سيما في نمط MVP ذي الصلة ، يحتوي النموذج على منطق عمل متحكم فيه فقط على الخادم ، ويتم تخزين المعلومات حول حالة الواجهة داخل مقدم العرض. على سبيل المثال ، تريد طلب خمس تذاكر. لقد حددت عددهم بالفعل ، لكنك لم تنقر بعد على زر "الطلب". يمكن تخزين معلومات حول عدد التذاكر التي اخترتها بالضبط في النموذج في مكان ما في الزوايا السرية للفئة ، والتي تعمل كحشية بين النموذج وعرضه. أو ، على سبيل المثال ، ينتقل المشغل من شاشة إلى أخرى ، لكن لا يتغير أي شيء في النموذج ، وحيثما حدث عندما وقعت المأساة ، لا يعرف مبرمج تصحيح الأخطاء سوى كلمات اختبار منضبط للغاية. الطريقة بسيطة ومفهومة ، وتستخدم دائمًا تقريبًا ومضرة قليلاً ، في رأيي. لأنه إذا حدث خطأ ما ، فمن المستحيل للغاية معرفة حالة هذا المقدم ، الذي أدى إلى حدوث خطأ. خاصة إذا حدث الخطأ على خادم المعركة أثناء العملية بمبلغ 1000 دولار ، وليس في المختبر في بيئة قابلة للضبط وقابلة للتكرار.
بدلاً من هذا النهج المعتاد ، نمنع أي شخص باستثناء النموذج من احتواء معلومات حول حالة الواجهة. هذا ، كالعادة ، مزايا وعيوب يجب مكافحتها.
- (+1) الميزة الأهم ، توفير أشهر من عمل البرمجة - إذا حدث خطأ ما ، يقوم المبرمج ببساطة بتحميل حالة اللعبة قبل وقوع الحادث ويتلقى نفس الحالة تمامًا ليس فقط من نموذج العمل ، ولكن من الواجهة بأكملها حتى آخر زر على الشاشة.
- (+2) إذا غير بعض الفريق شيئًا ما في الواجهة ، فيمكن للمبرمج الانتقال بسهولة إلى السجل ومعرفة ما الذي تغير بالضبط في نموذج json مناسب ، كما في القسم السابق.
- (-1) يظهر الكثير من المعلومات الزائدة في النموذج غير الضروري لفهم منطق العمل الخاص باللعبة ولا يحتاجه الخادم مرتين.
لحل هذه المشكلة ، سنقوم بوضع علامة على بعض الحقول على أنها ليست "تحقق من الخادم" ، يبدو كما يلي ، على سبيل المثال ، مثل هذا:
public EDictionary<string, UIStateModel> uiScreens { get { return UI_SCREENS.Get(this); } } public static PDictionaryModel<string, UIStateModel> UI_SCREENS = new PDictionaryModel<string, UIStateModel>() { notServerVerified = true };
سيتعلق هذا الجزء من النموذج وكل شيء تحته بالعميل حصريًا.
إذا كنت لا تزال تتذكر ، فإن علامات ما تحتاجه للتصدير وما لا تبدو عليه:
[Flags] public enum ExportMode { all = 0x0, changes = 0x1, serverVerified = 0x2 }
وفقًا لذلك ، عند تصدير تجزئة أو حسابها ، يمكنك تحديد ما إذا كنت تريد تصدير الشجرة بالكامل أم فقط ذلك الجزء منها الذي يتم التحقق منه بواسطة الخادم.
أول تعقيد واضح ينشأ من هذا هو الحاجة إلى إنشاء أوامر منفصلة تحتاج إلى فحصها من قبل الخادم وتلك التي ليست مطلوبة ، ولكن هناك أيضًا تلك التي تحتاج إلى التحقق منها بالكامل. من أجل عدم تحميل المبرمج بعمليات غير ضرورية لإعداد الأمر ، سنحاول مرة أخرى القيام بكل ما هو ضروري مع غطاء المحرك.
public partial class Command { public virtual void Apply(ModelRoot root) {} public virtual void ApplyClientSide(ModelRoot root) {} }
يمكن للمبرمج الذي يقوم بإنشاء الأمر أن يتخطى إحدى هاتين الوظيفتين أو كليهما. كل هذا ، بالطبع ، شيء رائع ، لكن كيف يمكنني التأكد من أن المبرمج لم يفلت من أي شيء ، وإذا كان قد أفسد شيئًا ما - كيف يمكن أن يساعده بسرعة وسهولة في إصلاحه؟ هناك طريقتان. تقدمت بطلب الأول ، ولكن قد ترغب في الحصول على الثاني.
الطريقة الأولى
نستخدم الميزات الرائعة لنموذجنا:
- يستدعي المحرك الوظيفة الأولى ، وبعدها يتلقى عددًا كبيرًا من التغييرات في الجزء المحدد من الخادم من حالة اللعبة. إذا لم تكن هناك تغييرات ، فنحن نتعامل حصريًا مع فريق العميل.
- نحصل على تجزئة نموذج التغييرات في النموذج بالكامل ، وليس فقط نموذج التحقق من الخادم. إذا كان مختلفًا عن التجزئة السابقة ، فقد أفسد المبرمج وتغير شيئًا في جزء النموذج الذي لم يتم التحقق منه بواسطة الخادم. نلتف حول شجرة الولاية ونفقد المبرمج في شكل تنفيذ قائمة كاملة من الحقول notServerVerified = صواب ويكذب أسفل الشجرة ، والذي قام بتغييره.
- نحن نسمي الوظيفة الثانية. لقد حصلنا من النموذج على عدد من التغييرات التي حدثت في الجزء المحدد. إذا لم يتزامن ذلك مع التجزئة بعد المكالمة الأولى ، فعندئذٍ في المبرمج الثاني قام أي شيء بعمل مبرمج. إذا كنا نرغب في الحصول على سجل معلوماتي للغاية في هذه الحالة ، فإننا نعيد النموذج بأكمله إلى حالته الأصلية ، ونسلمه إلى ملف ، وسيصبح المبرمج في متناول اليد لتصحيح الأخطاء ، ثم استنساخه بالكامل (سطرين - تسلسل إلغاء التسلسل) ، والآن نطبق أولاً وظيفة ، ثم نلتزم التغييرات بحيث يبدو النموذج دون تغيير ، وبعد ذلك نطبق الوظيفة الثانية. وبعد ذلك ، نقوم بتصدير جميع التغييرات في الجزء الذي تم فحصه من الخادم في شكل JSON وإدراجه في التنفيذ التعسفي ، بحيث يمكن للمبرمج المخجل أن يرى على الفور ماذا وأين تغير ، وما الذي لا ينبغي تغييره.
يبدو ، بالطبع ، مخيفًا ، ولكنه في الحقيقة عبارة عن 7 أسطر ، لأن الوظائف التي تفعل ذلك هي كل شيء (باستثناء اجتياز الشجرة من الفقرة الثانية) ونحن على استعداد. وبما أن هذا الاستقبال ، يمكننا أن نسمح لأنفسنا بالتصرف على النحو الأمثل.
الطريقة الثانية
أكثر وحشية ، الآن في ModelRoot لدينا حقل قفل واحد ، ولكن يمكننا تقسيمه إلى حقلين ، واحد سيغلق فقط الحقول المحددة على الخادم ، والآخر فقط الحقول المحددة. في هذه الحالة ، فإن المبرمج الذي ارتكب خطأً سيتلقى شرحًا عنه فورًا وربطه بالمكان الذي قام به. العيب الوحيد لهذا النهج هو أنه إذا تم تحديد خاصية نموذج واحد في الشجرة الخاصة بنا على أنها غير قابلة للتحقق ، فلن يتم فحص كل شيء في الشجرة الموجودة أسفله فيما يتعلق بحساب التجزئة ولن يتم فحص عنصر التحكم في التغيير ، حتى إذا لم يتم تحديد كل حقل. لن يبحث القفل ، بطبيعة الحال ، في التسلسل الهرمي ، مما يعني أنه يجب وضع علامة على جميع حقول الجزء غير المحدد من الشجرة ، ولن تعمل في بعض الأماكن لاستخدام نفس الفئات في واجهة المستخدم والجزء المعتاد من الشجرة. كخيار ، مثل هذا البناء ممكن (سأكتبه مبسطة):
public class GameState : Model { public RootModelData data; public RootModelLocal local; } public class RootModel { public bool locked { get; } }
ثم اتضح أن كل شجرة فرعية لها قفل خاص بها. يرث GameState النماذج ، لأنه أسهل من الخروج بتنفيذ منفصل لكل الوظائف نفسها.
التحسينات اللازمة
بالطبع ، سيتعين على المدير المسؤول عن معالجة الفرق إضافة وظائف جديدة. سيكون جوهر التغييرات هو أنه لن يتم إرسال جميع الأوامر إلى الخادم ، ولكن فقط تلك التي تنشئ التغييرات المحددة. لن يقوم الخادم الموجود على جانبه برفع شجرة حالة اللعبة بأكملها ، ولكن فقط الجزء الذي يتم فحصه ، وبالتالي فإن علامة التجزئة ستتزامن فقط مع الجزء الذي يتم فحصه. عند تنفيذ الأمر ، سيتم تشغيل أول وظيفتين فقط من الأمر على الخادم ، وعند حل الإشارات إلى النماذج الموجودة في gamestate ، إذا أدى المسار إلى جزء غير قابل للتحقق منه من الشجرة ، سيتم وضع null في متغير الأمر بدلاً من النموذج. ستقف جميع الفرق غير المرسلة بأمانة تماشيا مع الفرق المعتادة ، ولكن يجب اعتبارها مؤكدة بالفعل. بمجرد أن يصلوا إلى الخط ولا توجد خطوط غير مؤكدة قبلهم ، سيتم تطبيقها على الفور على الحالة الثانية.
لا يوجد شيء معقد بشكل أساسي في التنفيذ. إن خاصية كل حقل في النموذج تحتوي على شرط واحد إضافي ، هو اجتياز شجرة.
تحسين آخر ضروري - ستحتاج إلى مصنع منفصل لـ ParsistentModel في الأجزاء المحددة والتي لم يتم التحقق منها من الشجرة وستكون NextFreeId مختلفة عنها.
الأوامر التي بدأها الخادم
هناك بعض المشكلات إذا كان الخادم يرغب في دفع أمره إلى العميل ، لأن حالة العميل بالنسبة للخادم يمكن أن تقفز خطوات إلى الأمام بالفعل. الفكرة الرئيسية هي أنه إذا كان الخادم بحاجة لإرسال أمره ، فإنه يرسل إخطار الخادم إلى العميل مع الرد التالي ، ويكتب عليه في الحقل للإشعارات المرسلة إلى هذا العميل. يتلقى العميل إشعارًا ، ويشكل أمرًا على أساسه ويضعه في نهاية قائمة الانتظار ، بعد تلك التي أكملت على العميل ولكنها لم تصل إلى الخادم بعد. بعد مرور بعض الوقت ، يتم إرسال الأمر إلى الخادم كجزء من العملية المعتادة للعمل مع النموذج. بعد تلقي هذا الأمر للمعالجة ، يقوم الخادم بإخراج الإشعار من قائمة الانتظار الصادرة. إذا لم يستجب العميل للإشعار خلال الوقت المحدد مع الحزمة التالية ، فسيتم إرسال أمر إعادة التشغيل إليه. إذا كان العميل الذي تلقى الإشعار قد توقف ، أو يتصل لاحقًا ، أو لسبب ما ، قام بتحميل اللعبة ، فسيقوم الخادم بتحويل جميع الإخطارات إلى أوامر قبل إعطاء الحالة لها ، وتنفيذها من جانبها ، وبعد ذلك فقط يمنح العميل الانضمام حالته الجديدة. يرجى ملاحظة أن اللاعب قد يكون لديه حالة متعارضة ذات موارد سلبية عندما يتمكن اللاعب من إنفاق المال بالضبط في الوقت الذي أخذ فيه الخادم منه. صدفة غير محتملة ، ولكن مع وجود DAU كبير أمر لا مفر منه تقريبا. لذلك ، يجب ألا تقع قواعد الواجهة واللعبة حتى الموت في مثل هذا الموقف.
الأوامر التي تحتاج إلى معرفة استجابة الخادم
الخطأ النموذجي هو التفكير في أنه لا يمكن الحصول على رقم عشوائي إلا من الخادم. لا شيء يمنعك من تشغيل نفس مُنشئ الأرقام العشوائية المزيفة الذي يعمل في وقت واحد من العميل والخادم ، بدءًا من معرف جانبي مشترك. علاوة على ذلك ، يمكن تخزين البذور الحالية مباشرة في gamestate. قد يجد البعض صعوبة في مزامنة استجابة هذا المولد. في الحقيقة ، يكفي الحصول على رقم واحد في نفس المقالة - بالضبط عدد الأعداد التي تم استلامها من المولد حتى هذه اللحظة. إذا لم يتقارب المولد الخاص بك لسبب ما ، فهذا يعني أن لديك خطأ ما في مكان ما ولا تعمل الشفرة بشكل قاطع. وهذه الحقيقة لا ينبغي أن تكون مخفية تحت السجادة ، ولكن تسويتها والبحث عن خطأ. بالنسبة للغالبية العظمى من الحالات ، بما في ذلك الصناديق الغامضة ، فإن هذا النهج يكفي.
ومع ذلك ، هناك أوقات يكون فيها هذا الخيار غير مناسب. على سبيل المثال ، أنت تلعب جائزة باهظة الثمن ولا تريد أن يزيل الرفيق الماكرة اللعبة ، وأن تكتب روبوتًا يخبرك مقدمًا بما سيحدث من صندوق الماس إذا فتحته الآن ، وماذا لو قمت بتدوير الطبلة في مكان آخر قبل ذلك. يمكنك تخزين البذور لكل متغير عشوائي بشكل منفصل ، وهذا سيحمي من القرصنة الأمامية ، لكنه لن يساعد بأي طريقة من روبوت يخبرك بعدد الصناديق التي تحتاجها في الوقت الحالي. حسنًا ، الحالة الأكثر وضوحًا هي أنك قد لا ترغب في التألق في تهيئة العميل بمعلومات حول احتمال حدوث بعض الأحداث النادرة. باختصار ، من الضروري في بعض الأحيان انتظار استجابة الخادم.
يجب حل مثل هذه المواقف ليس من خلال القدرات الإضافية للمحرك ، ولكن بتقسيم الفريق إلى قسمين - الأول يجهز الموقف ويضع الواجهة في حالة انتظار للإشعارات ، والإخطارات الفعلية الثانية ، مع الإجابة التي تحتاجها. حتى إذا قمت بحجب الواجهة بينهما بإحكام ، فقد يتسلل أمر آخر - على سبيل المثال ، ستتم استعادة وحدة الطاقة في الوقت المناسب.
من المهم أن نفهم أن مثل هذه الحالات ليست هي القاعدة ، ولكن الاستثناء. في الواقع ، تحتاج معظم الألعاب إلى فريق واحد فقط في انتظار الإجابة - GetInitialGameState. حزمة أخرى من هذه الأوامر هي التفاعل بين اللاعبين في لعبة التعريف ، GetLeaderboard ، على سبيل المثال. جميع مائتي قطعة أخرى حتمية.
خادم تخزين البيانات والموضوع الموحلة لتحسين الخادم
أعترف على الفور بأنني عميل ، وفي بعض الأحيان سمعت مثل هذه الأفكار والخوارزميات من خوادم مألوفة لم يفلتوا منها حتى من رأسي. من التواصل مع زملائي ، قمت بطريقة ما بتطوير صورة لكيفية عمل بنائي على جانب الخادم في الحالة المثالية. ومع ذلك: هناك موانع ، فمن الضروري التشاور مع خادم متخصص.
أولا عن تخزين البيانات. جانب الخادم الخاص بك قد يكون له قيود إضافية. على سبيل المثال ، قد يتم منعك من استخدام الحقول الثابتة. علاوة على ذلك ، فإن رمز الأوامر والنماذج قابل للنقل تلقائيًا ، ولكن لا يلزم أن يتزامن رمز الخاصية على العميل والخادم على الإطلاق. يمكن إخفاء أي شيء هناك ، حتى التهيئة البطيئة لقيم الحقل من memcache ، على سبيل المثال. يمكن أن تتلقى حقول الخصائص أيضًا معلمات إضافية يستخدمها الخادم ، ولكنها لا تؤثر على عمل العميل.
أول فارق أساسي للخادم: حيث يتم إجراء تسلسل للحقول وتسلسلها. والحل المعقول هو أن معظم شجرة الحالة متسلسلة في حقل ثنائي أو json ضخم واحد. في الوقت نفسه ، يتم أخذ بعض الحقول من الجداول. هذا ضروري لأن قيم بعض الحقول ستكون ضرورية باستمرار لخدمات التفاعل بين اللاعبين للعمل. على سبيل المثال ، ينتشر الأيقونة والمستوى باستمرار من قبل مجموعة متنوعة من الأشخاص. من الأفضل الاحتفاظ بها في قاعدة بيانات منتظمة. والشخص الكامل أو الجزئي ، ولكن الحالة التفصيلية ستحتاج إلى شخص آخر غيره نادرًا جدًا ، عندما يقرر شخص ما النظر إلى إقليمه.
علاوة على ذلك ، فإن سحب الحقول من قاعدة واحدة في المرة الواحدة غير مريح ، وقد يتحول إلى سحب طويل. قد يكون الحل غير القياسي للغاية ، المتاح فقط للهندسة المعمارية لدينا ، في حقيقة أن العميل ، عند تنفيذ أمر ما ، يقوم بجمع معلومات حول جميع الحقول المخزنة بشكل منفصل في الجداول التي تمكنت مجموعاتها من اللمس ، وإضافة هذه المعلومات إلى الأمر حتى يتمكن الخادم من رفع مجموعة الحقول هذه طلب واحد لقاعدة البيانات. بطبيعة الحال ، مع قيود معقولة ، حتى لا تتوسل إلى DDOS الناجم عن المبرمجين منحني الذين لمسوا عن غير قصد كل شيء على الإطلاق.
مع هذا التخزين المنفصل ، ينبغي للمرء أن ينظر في آليات المعاملة عندما يزحف أحد اللاعبين إلى بيانات شخص آخر ، على سبيل المثال ، يسرق الأموال منه. لكن في الحالة العامة ، نقوم بذلك عن طريق الإخطار. أي أن اللص يتلقى أمواله على الفور ، ويتلقى الشخص الذي يتعرض للسرقة إخطارًا يتضمن تعليمات بشطب الأموال عندما يتعلق الأمر بذلك.
كيف تنقسم الفرق بين الخوادم
الآن لحظة مهمة الثانية للخادم. هناك طريقتان. في البداية ، لمعالجة أي طلب (أو حزمة من الطلبات) ، يتم رفع الحالة بأكملها من قاعدة البيانات أو ذاكرة التخزين المؤقت إلى الذاكرة ، ومعالجتها ، ثم إعادتها إلى قاعدة البيانات. يتم تنفيذ العمليات تلقائيًا على مجموعة من الخوادم المنفذة المختلفة ، ولديها فقط قاعدة مشتركة ، وحتى إذا لم يكن ذلك دائمًا. كعميل ، فإن رفع الحالة إلى كل فريق أمر صادم ، لكنني رأيت كيف يعمل ، وهو يعمل بشكل موثوق للغاية وقياسًا. الخيار الثاني هو أن الحالة ترتفع مرة واحدة في الذاكرة وتعيش هناك حتى يسقط العميل من حين لآخر فقط بإضافة حالته الحالية إلى قاعدة البيانات.
لست مؤهلًا لإخبارك بمزايا وعيوب هذه الطريقة أو تلك. سيكون من الجيد أن يشرح لي شخص ما في التعليقات سبب حق الأول في الحياة بشكل عام. يثير الخيار الثاني أسئلة حول كيفية التفاعل بين اللاعبين الذين تبين أنهم صُدموا على خوادم مختلفة. هذا يمكن أن يكون حاسما ، على سبيل المثال ، إذا كان العديد من أعضاء العشيرة يستعدون لشن هجوم مشترك. لا يمكنك إظهار الآخرين حالة عضو حزبه مع تأخير 10 يحفظ. لسوء الحظ ، لن أفتحها هنا ، والتفاعل من خلال الإشعارات الموضحة أعلاه ، أوامر من خادم إلى آخر - في الوقت الحالي ، أصبح حفظ الحالة الحالية للاعب مرفوعًا هناك. إذا كانت الخوادم تتمتع بنفس المستوى من التوفر من أماكن مختلفة ،ويمكنك إدارة الموازن ، يمكنك محاولة نقل المشغل بهدوء من خادم إلى آخر. إذا كنت تعرف حلاً أفضل - تأكد من الوصف في التعليقات.الرقص مع الوقت
لنبدأ بالسؤال الذي أود حقًا أن أسقط الأشخاص في المقابلات: هنا لديك عميل وخادم ، ولكل منهما ساعة دقيقة إلى حد ما. كيفية معرفة مدى اختلافها. محاولة لحل هذه المشكلة في مقهى على منديل يكشف عن أفضل وأسوأ صفات مبرمج. والحقيقة هي أن المشكلة لا يوجد لديها حل صحيح رياضيا. لكن الشخص الذي تجري معه المقابلة يدرك هذا ، كقاعدة عامة ، يستغرق دقيقة في الدقيقة الخامسة وفقط بعد طرح الأسئلة. والطريقة التي يلتقي بها بهذه الرؤية وماذا يفعل بعد ذلك - يقول الكثير عن أهم شيء في شخصيته - ما الذي سيفعله هذا الشخص عندما تبدأ المشاكل الحقيقية في مشروعك.أفضل حل أعرفه يسمح لي لا لمعرفة الفرق الدقيق ، ولكن لتوضيح النطاق الذي يقع فيه الكثير من الإجابات حتى وقت أفضل حزمة تم تشغيلها من عميل إلى خادم ، بالإضافة إلى وقت أفضل حزمة تم تشغيلها من خادم إلى عميل. في المجموع ، سيعطيك هذا بضع عشرات من المللي ثانية من الدقة. هذا أفضل بكثير من اللازم للعبة الوصفية للعبة الجوّال ، وهنا لا يوجد لدينا لعبة VR متعددة اللاعبين أو CS ، ولكن لا يزال من الجيد للمبرمج أن يمثل حجم وطبيعة الصعوبات في مزامنة الساعة. على الأرجح ، سيكون كافياً بالنسبة لك أن تعرف متوسط التباطؤ الذي يتم اعتباره بينج إلى نصفين ، لفترة طويلة مع قطع انحرافات تزيد عن 30٪.الموقف الثاني الذي من المحتمل أن تصادفه هو تنظيم لعبة انزلاقية ، ونقل الساعة على هاتفك. في كلتا الحالتين ، سيتغير الوقت في التطبيق بشكل كبير وفجائي ، ويجب أن يتم ذلك بشكل صحيح. قم بإعادة تشغيل اللعبة على الأقل ، لكن من الأفضل ، بالطبع ، عدم إعادة التشغيل بعد كل قسيمة ، لذلك لا يمكنك استخدام الوقت الذي انقضى في التطبيق منذ إطلاقه.ثالثًا ، الموقف ، لسبب ما ، يمثل مشكلة بالنسبة لبعض المبرمجين لفهمهم ، على الرغم من وجود حل صحيح له: لا يمكن تنفيذ العمليات على خادم الوقت. على سبيل المثال ، ابدأ في إنتاج السلع عندما يصل طلب الإنتاج إلى الخادم. بخلاف ذلك ، قم بتقبيل وداعك الحاسم ، واستحوذ على 35000 desync يوميًا بسبب آراء مختلفة من العميل والخادم حول ما إذا كان من الممكن بالفعل النقر على الجائزة. القرار الصحيح هو أن الفريق يسجل معلومات حول الوقت الذي تم فيه تنفيذه. يقوم الخادم ، بدوره ، بالتحقق مما إذا كان فارق التوقيت بين وقت الخادم الحالي والوقت في الأمر يقع ضمن الفترة الزمنية المسموح بها ، وإذا حدث ذلك ، فإنه ينفذ الأمر من جانبه باستخدام الوقت الذي أعلنه العميل.مهمة أخرى للمقابلة: مهلة بعدها سيحاول العميل إعادة التشغيل - 30 ثانية. ما هي حدود الفارق الزمني المقبول للخادم؟ نصيحة رقم 1: الفاصل الزمني غير متماثل. نصيحة رقم 2: أعد قراءة الفقرة الأولى من هذا القسم مرة أخرى ، وحدد كيفية تمديد الفاصل الزمني حتى لا يتم تسجيل 3000 خطأ يوميًا على تأثيرات الحافة.لكي يعمل هذا بشكل جميل وصحيح ، من الأفضل إضافة معلمة إضافية إلى معلمات استدعاء الأوامر - وقت الاتصال. شيء مثل هذا:
public interface Command { void Apply(ModelRoot root, long time); }
ونصيحتي لك ، بالمناسبة ، لا تستخدم أنواع الوحدة المحلية لبعض الوقت في النموذج - أنت ستخنق. من الأفضل تخزين UnixTime في وقت الخادم ، كلما احتجت إلى طرق تحويل مفيدة في متناول اليد ، وتخزينها في النموذج في حقل PTime خاص يختلف عن PValue <long> فقط في ذلك عند التصدير إلى JSON ، فإنه يضيف معلومات زائدة عن الحاجة في أقواس استيراد: الوقت بتنسيق قابل للقراءة من قبل الإنسان. لا يمكنك طاعة لي. لقد حذرتكالحالة الرابعة: في حالة اللعبة ، هناك مواقف عندما يجب إنشاء فريق بدون مشاركة لاعب ، في الوقت المناسب ، على سبيل المثال ، استعادة الطاقة. وضع شائع جدا ، في الواقع. أريد أن أحصل على مجال ، إنه يمارس بشكل ملائم. على سبيل المثال PTimeOut ، حيث سيكون من الممكن تسجيل نقطة زمنية يجب بعدها إنشاء الأمر وتنفيذه. في الكود ، قد يبدو كالتالي: public class MyModel : Model { public static PTimeOut RESTORE_ENERGY = new PTimeOut() {command = (model, property) => new RestoreEnergyCommand() { model = model}} public long restoreEnergy { get { return RESTORE_ENERGY.Get(this); } set { RESTORE_ENERGY.Set(this, value); }} }
بالطبع ، أثناء التحميل الأولي للاعب ، يجب على الخادم أولاً إثارة إنشاء وتنفيذ جميع هذه الأوامر ، وعندها فقط يعطي الحالة للاعب. المشكلة هنا هي أن كل هذا يتعارض مع الإشعارات التي يمكن أن يحصل عليها اللاعب خلال هذا الوقت. وبالتالي ، سيكون من الضروري أولاً فك الوقت قبل وقت الإخطار الأول ، إذا كنت بحاجة إلى سحب مجموعة من الأوامر في نفس الوقت ، ثم إنشاء أمر من الإخطار نفسه ، ثم فك الوقت حتى الإخطار التالي ، ثم العمل عليه وما إلى ذلك. إذا لم تتوافق عطلة الحياة الكاملة هذه مع انتهاء مهلة الخادم ، وكان ذلك ممكنًا إذا تم اختيار اللاعب كثيرًا من خلال الإشعارات ، نكتب الحالة الحالية من الذاكرة إلى قاعدة البيانات ونستجيب بدلاً من ذلك بأمر لإعادة الاتصال بالعميل.يجب أن تتعرف جميع هذه الأوامر بطريقة أو بأخرى على ما يحتاجون إليه لإنشاء وتنفيذ. إن حلمي المريح ، ولكنه مناسب ، هو أن النموذج يواجه تحديًا آخر ، والذي يتخطى التسلسل الهرمي الكامل للنموذج ، والذي ينتقل بعد تنفيذ كل أمر ، وأيضًا على المؤقت. بالطبع ، هذا حمل إضافي للتجول في الشجرة تقريبًا في أحد التحديثات ، بدلاً من ذلك يمكنك الاشتراك أو إلغاء الاشتراك في حدث CurrentTime الذي يخرج عن حالة اللعبة مع كل تغيير في هذا الحقل: public partial class Model { public void SetCurrentTime(long time); } vs public partial class RootModel { public event Action<long> setCurrentTime; }
هذا أمر جيد ، ولكن المشكلة هي أن النماذج التي تم حذفها من شجرة النموذج إلى الأبد والتي تحتوي على مثل هذا الحقل ستظل مشتركة في هذا الحدث ، ويجب أن تعمل بشكل صحيح. قبل إرسال أمر ، تحقق مما إذا كانوا لا يزالون في الشجرة ولديهم رابط ضعيف لهذا الحدث أو انعكاس السيطرة ، لذلك بسبب ذلك لا يمكنهم الوصول إلى GC.الملحق 1 ، حالة نموذجية مأخوذة من واقع الحياة
آخذ من التعليقات إلى الجزء الأول. في الألعاب ، غالبًا ما لا تحدث بعض الإجراءات بعد الأمر مباشرة للنموذج ، ولكن في نهاية نوع من الرسوم المتحركة. في ممارستنا ، كانت هناك مثل هذه الحالة عندما تفتح صناديق اللغز ، وبالطبع يجب أن يتغير المال فقط عندما يتم تشغيل الرسوم المتحركة حتى النهاية. قرر أحد مطورينا تبسيط حياته ، وعدم تغيير القيمة الموجودة على الأمر ، ولكن أخبر الخادم بأنه قد تغير ، وفي نهاية الرسوم المتحركة قم بتشغيل رد اتصال ، والذي سيصحح القيم الموجودة في النموذج إلى القيم المرغوبة. أحسنت ، باختصار. لقد صنع هذه الصناديق الغامضة لمدة أسبوعين ، ثم ظهرت ثلاثة أخطاء أخرى يصعب ملاحظتها نتيجة لنشاطه ، واضطررنا لقضاء ثلاثة أسابيع أخرى للقبض عليهم ، على الرغم من أن وقت "إعادة الكتابة كالمعتاد" كان ، بالطبع ، لا أحد يستطيع تسليط الضوء. من الذي يتبع بشكل واضحأعتقد أن الاستنتاج هو أنه كان من الأفضل فعل كل شيء من البداية مع التفاعل الطبيعي.لذلك ، قراري شيء من هذا القبيل. بالطبع ، لا يكمن المال في حقل منفصل ، لكنه أحد الكائنات الموجودة في قاموس المخزون ، لكن هذا ليس مهمًا في الوقت الحالي. يحتوي النموذج على جزء واحد يتم فحصه بواسطة الخادم ، وعلى أساسه يعمل منطق العمل ، والآخر موجود فقط على العميل. يتم تجميع الأموال في النموذج الرئيسي فور اتخاذ القرار ، وفي الجزء الثاني من قائمة "العرض المؤجل" ، يتم إنشاء عنصر بنفس المقدار الذي يبدأ في الحركة عن طريق ظهوره ، وعندما تنتهي الرسوم المتحركة ، يتم تشغيل أمر يزيل هذا العنصر. هذه ملاحظة العميل بحتة "لا تظهر هذا المبلغ بعد." وفي حقل حقيقي ، لا يتم عرض قيمة الحقل فقط ، ولكن قيمة الحقل مطروحًا منها جميع تأجيلات العميل. ويتم التقسيم إلى مثل هذا الفريقين فقط لماذا لو قام العميل بإعادة التشغيل بعد الفريق الأول ، ولكن قبل الثانية ، ستكون جميع الأموال التي حصل عليها اللاعب في حسابه دون أي علامات أو استثناءات. في الكود ، سيكون شيء مثل هذا: public class OpenMisterBox : Command { public BoxItemModel item; public int slot;
ماذا لدينا في النهاية؟
هناك عرضان ، أحدهما يتم تشغيل رسوم متحركة معينة ، تنتظر نهايتها عرض المال في طريقة عرض مختلفة تمامًا ، والتي ليس لديها فكرة عمن ولماذا تريد إظهار معنى مختلف. كل شيء رد الفعل. في أي وقت ، يمكنك تحميل الحالة الكاملة لـ GameState في اللعبة وسيبدأ اللعب بالضبط من حيث توقفنا ، بما في ذلك بدء تشغيل الرسوم المتحركة. ستبدأ الحقيقة من البداية ، لأننا لا نمحو مرحلة الرسوم المتحركة ، لكن إذا كنا في حاجة إليها بالفعل ، يمكننا محوها حتى.المجموع
إن تصميم منطق الأعمال للعبة من خلال النماذج والفرق والملفات الثابتة مع القواعد ، وتغليفها من جميع الجوانب بسجلات جميلة مفصلة ومولدة تلقائيًا ومرفقة بحالات إعدام معلومات مفيدة للعديد من الأخطاء النموذجية التي ارتكبها مبرمج يشاهد ميزات جديدة ، وهذا في رأيي هو الطريقة الصحيحة للعيش ضوء أبيض. وليس فقط لأنه يمكنك تقديم وظائف جديدة بشكل أسرع عدة مرات. لا يزال هذا مهمًا ، لأنه إذا كان من السهل بالنسبة لك تنزيل ميزة جديدة وتصحيحها ، فسيكون لدى مصمم اللعبة الوقت الكافي لإجراء تجارب أكثر عدة مرات على تصميم الألعاب مع نفس المبرمجين. مع كل الاحترام الواجب لأعمالنا ، يعتمد علينا ما إذا كانت اللعبة قد فشلت أم لا ، ولكن ما إذا كانت ستطلق النار أم لا تعتمد على الألعاب ، ويجب أن تُمنح مساحة للتجارب.والآن أطلب منك الإجابة عن أسئلة مهمة للغاية بالنسبة لي. إذا كانت لديك أفكار حول كيفية القيام بما فعلته بشكل سيء ، أو كنت ترغب فقط في التعليق على إجاباتي ، فأنا في انتظارك في التعليقات. اقتراح للتعاون وإرشادات حول العديد من أخطاء بناء الجملة ، يرجى في PM.