بينما كتبنا رمز شبكة مطلق النار PvP المحمول: مزامنة اللاعب على العميل

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




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

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

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

ونتيجة لذلك ، لم تكن الطرق بدون آلية التنبؤ لأفعال اللاعب المحلي (التنبؤ) مناسبة للمشروع ، واستقرنا على طريقة تزامن حالة العالم ، دون منطق محدد.

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

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

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

آلية التنبؤ بعمل اللاعب المحلي (التنبؤ)


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

مثال على قوائم الأنظمة التي تعمل على العميل والخادم:



في الوقت الحالي ، لدينا حوالي 30 نظامًا يعمل على العميل يوفر توقعات اللاعب وحوالي 80 نظامًا يعمل على الخادم. لكننا لا نتنبأ بأشياء مثل التعامل مع الضرر أو استخدام القدرات أو شفاء الحلفاء. هناك مشكلتان في هذه الميكانيكا:

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

لمثل هذا الميكانيكي ، يخفي التأخر عن اللاعب بطرق أخرى.

مثال: نرسم تأثير الضربة من اللقطة على الفور ، ونحدث حياة العدو فقط بعد أن نتلقى تأكيدًا للضربة من الخادم.

المخطط العام لرمز الشبكة في المشروع




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

تتكون العملية برمتها من الخطوات التالية: يرسل العميل إدخال اللاعب إلى الخادم (أ) → تتم معالجة هذا الإدخال على الخادم بعد حجم مخزن الإدخال المؤقت HRTT + (ب) → يرسل الخادم الحالة العالمية الناتجة إلى العميل (العملاء) → يقوم العميل بتطبيق الحالة العالمية المؤكدة مع وقت الخادم RTT + حجم مخزن الإدخال المؤقت + حجم ذاكرة التخزين المؤقت لحالة اللعبة (d).

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

تتكون عملية الموافقة من جزأين:

  1. مقارنات الحالة المتوقعة للعالم للقراد N المستلم من الخادم. تشارك فقط البيانات المتعلقة باللاعب المحلي في المقارنة. دائمًا ما يتم أخذ بقية بيانات العالم من حالة الخادم ولا تشارك في التنسيق.
  2. أثناء المقارنة ، قد تحدث حالتان:

- إذا تزامنت حالة العالم المتوقعة مع الحالة المؤكدة من الخادم ، فإن العميل ، باستخدام البيانات المتوقعة للاعب المحلي والبيانات الجديدة لبقية العالم ، يواصل محاكاة العالم في الوضع العادي ؛
- إذا لم تتطابق الحالة المتوقعة ، فسيستخدم العميل حالة الخادم بأكملها في العالم وسجل الإدخال من العميل ويعيد سرد الحالة المتوقعة الجديدة لعالم اللاعب.

في التعليمات البرمجية ، يبدو شيء مثل هذا:
GameState Reconcile(int currentTick, ServerGameStateData serverStateData, GameState currentState, uint playerID) { var serverState = serverStateData.GameState; var serverTick = serverState.Time; var predictedState = _localStateHistory.Get(serverTick); //if predicted state matches server last state use server predicted state with predicted player if (_gameStateComparer.IsSame(predictedState, serverState, playerID)) { _tempState.Copy(serverState); _gameStateCopier.CopyPlayerEntities(currentState, _tempState, playerID); return _localStateHistory.Put(_tempState); // replace predicted state with correct server state } //if predicted state doesn't match server state, reapply local inputs to server state var last = _localStateHistory.Put(serverState); // replace wrong predicted state with correct server state for (var i = serverTick; i < currentTick; i++) { last = _prediction.Predict(last); // resimulate all wrong states } return last; } 


تحدث المقارنة بين دولتين عالميتين فقط لتلك البيانات التي تتعلق باللاعب المحلي والمشاركة في نظام التنبؤ. يتم أخذ عينات البيانات من خلال معرف اللاعب.

طريقة المقارنة:
 public bool IsSame(GameState s1, GameState s2, uint avatarId) { if (s1 == null && s2 != null || s1 != null && s2 == null) return false; if (s1 == null && s2 == null) return false; var entity1 = s1.WorldState[avatarId]; var entity2 = s2.WorldState[avatarId]; if (entity1 == null && entity2 == null) return false; if (entity1 == null || entity2 == null) return false; if (s1.Time != s2.Time) return false; if (s1.WorldState.Transform[avatarId] != s2.WorldState.Transform[avatarId]) return false; foreach (var s1Weapon in s1.WorldState.Weapon) { if (s1Weapon.Value.Owner.Id != avatarId) continue; var s2Weapon = s2.WorldState.Weapon[s1Weapon.Key]; if (s1Weapon.Value != s2Weapon) return false; var s1Ammo = s1.WorldState.WeaponAmmo[s1Weapon.Key]; var s2Ammo = s2.WorldState.WeaponAmmo[s1Weapon.Key]; if (s1Ammo != s2Ammo) return false; var s1Reload = s1.WorldState.WeaponReloading[s1Weapon.Key]; var s2Reload = s2.WorldState.WeaponReloading[s1Weapon.Key]; if (s1Reload != s2Reload) return false; } if (entity1.Aiming != entity2.Aiming) return false; if (entity1.ChangeWeapon != entity2.ChangeWeapon) return false; return true; } 


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

كود
 public static bool operator ==(Transform a, Transform b) { if ((object)a == null && (object)b == null) return true; if ((object)a == null && (object)b != null) return false; if ((object)a != null && (object)b == null) return false; if (Math.Abs(a.Angle - b.Angle) > 0.01f) return false; if (Math.Abs(a.Position.x - b.Position.x) > 0.01f || Math.Abs(a.Position.y - b.Position.y) > 0.01f) return false; return true; } 


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

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

لإكمال عملية الموافقة ، يجب تخزين نوعين من البيانات على العميل:

  1. تاريخ حالات اللاعب المتوقعة.
  2. وتاريخ المدخلات.

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

تخفيض وقت التأخير


يوضح الرسم البياني أعلاه أن هناك عازلين في نظام نقل البيانات في اللعبة:

  • مخزن الإدخال المؤقت على الخادم ؛
  • حاجز دول العالم على العميل.

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

في بداية اللعبة ، يبدأ العميل في المزامنة مع الخادم فقط بعد أن يتلقى العديد من حالات العالم من الخادم ومخزن gamestate المؤقت ممتلئ. عادةً ما يكون حجم هذا المخزن المؤقت هو 3 علامات (100 مللي ثانية).

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

في البداية ، قمنا بتطبيق حجم هذه المخازن المؤقتة كثوابت. على سبيل المثال بغض النظر عما إذا كان التشويش موجودًا بالفعل على الشبكة أم لا ، كان هناك تأخير ثابت قدره 200 مللي ثانية (حجم المخزن المؤقت للإدخال + حجم المخزن المؤقت لحالة اللعبة) لتحديث البيانات. إذا أضفنا إلى هذا متوسط ​​ping المقدر على الأجهزة المحمولة في مكان ما حوالي 200 مللي ثانية ، فإن التأخير الحقيقي بين استخدام الإدخال على العميل وتأكيد التطبيق من الخادم كان 400 مللي ثانية!

هذا لا يناسبنا.

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

كان حل هذه المشكلة هو تنفيذ الأحجام الديناميكية لمخازن الإدخال المؤقتة والأشكال:
  • بالنسبة إلى المخزن المؤقت gamestate ، يعرف العميل دائمًا محتوى المخزن المؤقت الحالي. في وقت حساب العلامة التالية ، يتحقق العميل من عدد الحالات الموجودة بالفعل في المخزن المؤقت ؛
  • لمخزن الإدخال المؤقت - بدأ الخادم ، بالإضافة إلى حالة اللعبة ، يرسل إلى العميل قيمة الملء الحالي لمخزن الإدخال المؤقت لعميل معين. يقوم العميل بدوره بتحليل هاتين القيمتين.

خوارزمية تغيير حجم المخزن المؤقت gamestate تقريبًا ما يلي:

  1. يأخذ العميل في الاعتبار متوسط ​​قيمة حجم المخزن المؤقت على مدى فترة من الوقت والتباين.
  2. إذا كان التباين ضمن الحدود الطبيعية (أي لفترة معينة من الوقت لم تكن هناك قفزات كبيرة في التعبئة والقراءة من المخزن المؤقت) ، يتحقق العميل من قيمة متوسط ​​حجم المخزن المؤقت لهذه الفترة من الوقت.
  3. إذا كان متوسط ​​تعبئة المخزن المؤقت أكبر من شرط الحد العلوي (أي ، سيتم ملء المخزن المؤقت أكثر من المطلوب) ، "يقلل" العميل من حجم المخزن المؤقت عن طريق تنفيذ علامة محاكاة إضافية.
  4. إذا كان متوسط ​​تعبئة المخزن المؤقت أقل من شرط الحد الأدنى (أي ، لم يكن لدى المخزن المؤقت وقت لملء قبل أن يبدأ العميل في القراءة منه) - في هذه الحالة ، "يزيد" العميل من حجم المخزن المؤقت بتخطي علامة واحدة من المحاكاة.
  5. في الحالة التي كان فيها التباين فوق المعدل الطبيعي ، لا يمكننا الاعتماد على هذه البيانات ، لأن كانت طفرات الشبكة لفترة معينة كبيرة جدًا. ثم يتجاهل العميل جميع البيانات الحالية ويبدأ في جمع الإحصائيات مرة أخرى.

تعويض تأخر الخادم


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


وبسبب هذا ، فإن الوضع هو أن اللاعب يطلق النار محليًا على هدف موجود على الخادم في مكان آخر.

للتعويض عن التأخير ، نستخدم الترجيع الزمني على الخادم. خوارزمية التشغيل هي تقريبًا ما يلي:

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

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

يبدو رمز نظام التحقق من اللقطة كما يلي:
 public void Execute(GameState gs) { foreach (var shotPair in gs.WorldState.Shot) { var shot = shotPair.Value; var shooter = gs.WorldState[shotPair.Key]; var shooterTransform = shooter.Transform; var weaponStats = gs.WorldState.WeaponStats[shot.WeaponId]; // DeltaTime shouldn't exceed physics history size var shootDeltaTime = (int) (gs.Time - shot.ShotPlayerWorldTime); if (shootDeltaTime > PhysicsWorld.HistoryLength) { continue; } // Get the world at the time of shooting. var oldState = _immutableHistory.Get(shot.ShotPlayerWorldTime); var potentialTarget = oldState.WorldState[shot.Target.Id]; var hitTargetId = _singleShotValidator.ValidateTargetAvailabilityInLine(oldState, potentialTarget, shooter, shootDeltaTime, weaponStats.ShotDistance, shooter.Transform.Angle.GetDirection()); if (hitTargetId != 0) { gs.WorldState.CreateEntity().AddDamage(gs.WorldState[hitTargetId], shooter, weaponStats.ShotDamage); } } } 


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

بعض المشاكل التي واجهناها


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

محاكاة العالم كله في نظام التنبؤ والنسخ


في البداية ، كان لدى جميع الأنظمة في ECS طريقة واحدة فقط: void Execute (GameState gs). في هذه الطريقة ، عادة ما تتم معالجة المكونات المتعلقة بجميع اللاعبين.

مثال على نظام الحركة في التنفيذ الأولي:
 public sealed class MovementSystem : ISystem { public void Execute(GameState gs) { foreach (var movementPair in gs.WorldState.Movement) { var transform = gs.WorldState.Transform[movementPair.Key]; transform.Position += movementPair.Value.Velocity * GameState.TickDuration; } } } 


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

كانت عملية التنبؤ كما يلي:

  1. تم إنشاء نسخة من حالة اللعبة.
  2. تم توفير نسخة لمدخلات ECS.
  3. كان هناك محاكاة للعالم كله في ECS.
  4. تم نسخ جميع البيانات المتعلقة باللاعب المحلي من gamestate المستلمة حديثًا.

تبدو طريقة التنبؤ على النحو التالي:
 void PredictNewState(GameState state) { var newState = _stateHistory.Get(state.Tick+1); var input = _inputHistory.Get(state.Tick); newState.Copy(state); _tempGameState.Copy(state); _ecsExecutor.Execute(_tempGameState, input); _playerEntitiesCopier.Copy(_tempGameState, newState); } 


كانت هناك مشكلتان في هذا التنفيذ:

  1. لأن نحن نستخدم الفصول الدراسية ، وليس الهياكل - يعد النسخ عملية مكلفة للغاية بالنسبة لنا (حوالي 0.1-0.15 مللي ثانية على iPhone 5S).
  2. تستغرق محاكاة العالم كله أيضًا الكثير من الوقت (حوالي 1.5-2 مللي ثانية على iPhone 5S).

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

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

مثال لنظام الحركة بعد التغيير:
 public sealed class MovementSystem : ISystem { public void Execute(GameState gs) { foreach (var movementPair in gs.WorldState.Movement) { Move(gs.WorldState.Transform[movementPair.Key], movementPair.Value); } } public void ExecutePlayer(GameState gs, uint playerId) { var movement = gs.WorldState.Movement[playerId]; if(movement != null) { Move(gs.WorldState.Transform[playerId], movement); } } private void Move(Transform transform, Movement movement) { transform.Position += movement.Velocity * GameState.TickDuration; } } 


بعد التغييرات ، تمكنا من التخلص من النسخ غير الضرورية في نظام التنبؤ وتقليل الحمل على نظام المطابقة.

الكود:
 void PredictNewState(GameState state, uint playerId) { var newState = _stateHistory.Get(state.Tick+1); var input = _inputHistory.Get(state.Tick); newState.Copy(state); _ecsExecutor.Execute(newState, input, playerId); } 


إنشاء وحذف الكيانات في نظام التنبؤ


في نظامنا ، تتم مطابقة الكيانات الموجودة على الخادم والعميل بواسطة معرف صحيح (id). بالنسبة إلى جميع الكيانات ، نستخدم الترقيم الشامل للمعرفات ، لكل كيان جديد القيمة id = oldID + 1.

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

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

تم إنشاء اللقطات على الخادم بترتيب مختلف:



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

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

سمحت هذه الطريقة بحل المشكلة عن طريق اللقطات ، ولكن ليس كل مشكلة إنشاء كيانات على العميل ككل. ما زلنا نعمل على الطرق الممكنة لحل مقارنة الكائنات التي تم إنشاؤها على العميل والخادم.

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

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

ماذا تقرأ


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


All Articles