علامة الوسخ
^title علامة الوسخ ^section أنماط التحسين
النية
تجنّب العمل غير الضروري بتأجيله حتى تصبح النتيجة مطلوبة.
الدافع
تملك كثير من الألعاب شيئاً يُسمّى شجرة المشهد (scene graph). وهذه بنية بيانات ضخمة تحتوي كل الكائنات في العالم. ويستخدم محرّك العرض هذا لتحديد أين تُرسم الأشياء على الشاشة.
في أبسط صورها، شجرة المشهد مجرّد قائمة مسطّحة من الكائنات. ولكل كائن نموذج، أو شكل رسومي بديل آخر، وتحويل (transform). يصف التحويل موضع الكائن ودورانه وحجمه في العالم. ولتحريك كائن أو تدويره، نكتفي بتغيير تحويله.
حين يرسم العارض كائناً، فإنه يأخذ نموذج الكائن، ويطبّق التحويل عليه، ثم يرسمه في ذلك الموضع في العالم. لو كان لدينا كيس مشاهد (scene bag) لا شجرة مشهد (scene graph)، فهنا ينتهي الأمر وتكون الحياة بسيطة.
لكن معظم أشجار المشهد هرمية (hierarchical). فقد يكون للكائن في الشجرة كائن أصل مرتبط به. في تلك الحالة، يكون تحويله نسبياً إلى موضع الأصل وليس موضعاً مطلقاً في العالم.
مثلاً، تخيّل أن عالم لعبتنا فيه سفينة قراصان في البحر. وفوق صاري السفينة عشّ القرصان. وفي ذلك العشّ جالس قرصان، وعلى كتف القرصان يقف ببغاء. يضع التحويل المحلي للسفينة السفينة في البحر، ويضع تحويل العشّ العشّ على السفينة، وهكذا.

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

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

حرّكنا أربعة كائنات فقط، لكننا أجُرينا عشر عمليات حساب للتحويلات العالمية. تلك الست عمليات هدر تُطرح قبل أن يستخدمها العارض أصلاً. لقد حسبنا التحويل العالمي للبناء أربع مرات، لكنه لا يُرسم إلا مرة واحدة.
المشكلة أن التحويل العالمي قد يعتمد على عدة تحويلات محلية. ولأننا نعيد الحساب فوراً كلما تغيّر أحد التحويلات، تنتهي إلى إعادة حساب التحويل نفسه عدّة مرات عندما يتغيّر أكثر من تحويل محلي يعتمد عليها في الإطار نفسه.
إعادة الحساب المؤجَّلة
سنحلّ هذا بأن نفصل تغيير التحويلات المحلية عن تحديث التحويلات العالمية. ف هذا يتيح لنا تغيير كومة من التحويلات المحلية في دفعة واحدة وثم إعادة حساب التحويلات العالمية المتأثّرة مرّة واحدة فقط بعد أن تكتمل كل تلك التعديلات، قبل أن نحتاج إليها للعرض مباشرةً.
ولن نفعل ذلك، نضيف علماً (flag) إلى كل كائن في الشجرة. و«العلامة» و«البت» مترادفتان في البرمجة -- فكلاهما يعني خلية بيانات واحدة من البايت يمكن أن تكون في إحدى حالتين. ونسميهما «صحيح» و«خطأ»، أو أحياناً «مضبوطة» و«ممسوحة». وسأستخدم هذه الكلمات تبادلياً.
حين يتغيّر التحويل المحلي نضبطها. وحين نحتاج إلى التحويل العالمي للكائن، نفحص العلامة. فإن كانت مضبوطة، نحسب التحويل العالمي ثم نمحو العلامة. العلامة تمثّل: «هل التحويل العالمي قديم؟» ولأسباب ليست واضحة تماماً، فالاسم التقليدي لهذه «القديمة» هو «وسخ». وبالتالي: علامة وسخ. و«بت الوسخ» اسم شائع لهذا النمط، لكنني فضّلت الالتزام بالاسم الذي لا يبدو مثيراً للاشمئزاز إلى هذا الحد.
فإن طبّقنا هذا النمط ثم حرّكنا كل الكائنات في مثالنا السابق، تنتهي اللعبة إلى فعل:
هذا أفضل ما يمكنك الأمل فيه -- فالتحويل العالمي لكل كائن متأثّر يُحسب مرّة واحدة بالضبط. وببست بت واحد من البيانات، يفعل هذا النمط لنا عدة أمور:
-
يطوي التعديلات على عدة تحويلات محلية على امتداد سلسلة أصول الكائن في إعادة حساب واحدة على الكائن.
-
يتجنّب إعادة الحساب على الكائنات التي لم تتحرّك.
-
وهيدة صغيرة: إذا أُزيل كائن قبل أن يُرسم، فلن يحسب تحويله العالمي إطلاقاً.
النمط
تتغيّر مجموعة من البيانات الأولية (primary data) مع الزمن. وتُحدَّد مجموعة من البيانات المشتقّة (derived data) منها عبر عملية مكلفة. وتتتبّع علامة «وسخ» متى تكون البيانات المشتقّة خارج التزامن مع البيانات الأولية. وهي تُضبط حين تتغيّر البيانات الأولية. فإذا كانت العلامة مضبوطة عند الحاجة إلى البيانات المشتقّة، فإنها تُعالَج من جديد وتُمسح العلامة. وإلا، فاستُخدمت البيانات المشتقّة المخزَّنة السابقة.
متى تستخدمه
مقارنةً ببعض الأنماط الأخرى في هذا الكتاب، يحلّ هذا النمط مشكلة محدّدة نوعاً ما. كما أنّه، مثل معظم عمليات التحسين، لا ينبغي أن تلجأ إليه إلا حين تكون لديك مشكلة أداء كبيرة بما يكفي لتبرير تعقيد الشيفرة المضاف.
تُطبَّق علامات الوسخ على نوعَي عمل: الحساب والمزامنة. وفي الحالتين، تكون عملية الانتقال من البيانات الأولية إلى البيانات المشتقّة معاصرة للوقت أو مكلفة بطريقة أخرى.
في مثال شجرة المشهد لدينا، تكون العملية بطيئة بسبب كثرة الرياضيات التي يلزم تنفيذها. أما عند استخدام هذا النمط للمزامنة، فالأرجح أكثر أن تكون البيانات المشتقّة في مكان آخر -- على القرص أو عبر الشبكة على آلة أخرى -- وأن مجرّد نقلها من نقطة أ إلى نقطة ب هو ما يكون مكلفاً.
وهناك متطلّبان آخران أيضاً:
-
يجب أن تتغيّر البيانات الأولية أكثر مما تُستخدم فيه البيانات المشتقّة. يعمل هذا النمط عبر تفادي معالجة البيانات المشتقّة عندما يجعل تغيّرٌ لاحقٌ للبيانات الأولية يبطلها قبل أن تُستخدم. فإن وجدت نفسك تحتاج تلك البيانات المشتقّة بعد كل تعديل على البيانات الأولية، فلن ينفعك هذا النمط.
-
ينبغي أن يكون التحديث التزايدي صعباً. لنفترض أن سفينة القراصان في لعبتنا تستطيع أن تحمل قدراً محدوداً فقط من الغنائم. ونحتاج إلى معرفة الوزن الكلي لكل ما في المخزن. يمكننا استخدام هذا النمط ووضع علامة وسخ للوزن الكلي. فكلما أضفنا غنيمة أو أزلناها نضبط العلامة. وحين نحتاج إلى المجموع، نجمع كل الغنائم ونمحو العلامة.
لكن حلاً أبسط هو أن تحتفظ بمجموع تراكمي. فحين تضيف عنصراً أو تزيله، اجمع وزنه أو اطرحه من المجموع الحالي. فإذا استطعنا أن «ندفع enquanto نمضي» على هذا النحو ونُبقي البيانات المشتقّة محدَّثة، فغالباً ما يكون ذلك خياراً أفضل من استخدام هذا النمط وحساب البيانات المشتقّة من الصفر عند الحاجة.
قد يجعل هذا يبدو وكأن علامات الوسخ نادراً ما تكون مناسبة، لكنك ستجد هنا وهناك موضعاً تساعد فيه. والبحث في قاعدة شيفرة لعبتك المعتادة عن كلمة «وسخ» سيكشف غالباً عن استخدامات لهذا النمط.
في بالك
حتى بعد أن تقنع نفسك بأن هذا النمط مناسب، هناك بضع عقبات صغيرة قد تسبب لك بعض الاضطراب.
هناك كلفة للتأجيل أكثر من اللازم
يؤجّل هذا النمط بعض العمل البطيء حتى تصبح النتيجة مطلوبة فعلاً، لكن حين تصبح مطلوبة، فإنها غالباً ما تكون مطلوبة الآن. لكن السبب الذي نستخدم هذا النمط من أجله أصلاً هو أن حساب تلك النتيجة بطيء!
وهذه ليست مشكلة في مثالنا لأننا ما زلنا نستطيع حساب الإحداثيات العالمية بسرعة كافية لأن تتّسع في إطار واحد، لكن يمكنك تصور حالات أخرى يكون فيها العمل الذي تقوم به جزءاً كبيراً يستغرق وقتاً ملحوظاً لإنجازه. فإذا لم تبدأ اللعبة في البدء* في المعالجة إلا في اللحظة التي يتوقّع فيها اللاعب رؤية النتيجة، فقد يتسبّب ذلك في توقّف مرئي مزعج.
والمشكلة الأخرى في التأجيل هي أنه إذا حدث خطأ ما، فقد يفشل تنفيذ العمل نهائياً. وقد يكون هذا مزعجاً بصفة خاصة حين تستخدم هذا النمط لحفظ حالة ما في صورة أكثر ثباتاً.
مثلاً، تعرف محرّكات النصوص ما إذا كان مستندك يحتوي على «تغييرات غير محفوظة». فالنقطة الصغيرة أو النجمة في شريط عنوان الملف هي علامة الوسخ مصوّرة. فالبيانات الأولية هي المستند المفتوح في الذاكرة، والبيانات المشتقّة هي الملف على القرص.
لا تحفظ كثير من البرامج على القرص إلا حين يُغلق المستند أو يُنهى التطبيق. وهذا مقبول في معظم الأحيان، لكن إن سحبت كابل الكهرباء من المقبس بالخطأ، ضاع تحفتك.
والمحرّرات التي تحفظ نسخة احتياطية تلقائياً في الخلفية تعوّض تحديداً عن هذا القصور. وتكرار الحفظ التلقائي هو نقطة على المتصل بين ألّا تفقد عملاً كثيراً عند حدوث انهيار، وألّا تثقل نظام الملفات بحفظ الملف في كل لحظة.
عليك التأكّد من ضبط العلامة في كل مرة تتغيّر فيها الحالة
بما أن البيانات المشتقّة تُحسب من البيانات الأولية، فهي في جوهرها ذاكرة مؤقتة. وكلما كانت لديك بيانات مخزَّنة مؤقتاً، فإن أصعب جوانب ذلك هو إبطال الذاكرة المؤقتة (cache invalidation) -- أي ملاحظة الصحيح متى تكون الذاكرة المؤقتة خارج التزامن مع بيانات مصدرها. وفي هذا النمط، يعني ذلك ضبط علامة الوسخ حين يتغيّر أيّ من البيانات الأولية.
وأي إغفال في موضع واحد، ستستخدم برنامجك بيانات مشتقّة قديمة على نحو خاطئ. ويؤدي ذلك إلى لاعبين محيرين وأخطاء يصعب تتبّعها إلى حدّ كبير. وحين تستخدم هذا النمط، عليك أن تحرص على أن أي شيفرة تعدّل الحالة الأولية تضبط علامة الوسخ أيضاً.
وإحدى الطرق للتخفيف من ذلك هي تغليف التعديلات على البيانات الأولية خلف واجهة ما. فإذا مرّ كل ما يستطيع تغيير الحالة عبر واجهة برمجية واحدة ضيّقة، يمكنك ضبط علامة الوسخ هناك وتطمئن إلى أنها لن تفوت.
عليك الاحتفاظ بالبيانات المشتقّة السابقة في الذاكرة
حين تكون البيانات المشتقّة مطلوبة وعلامة الوسخ غير مضبوطة، فإنها تستخدم البيانات المحسوبة سابقاً. وهذا بديهي، لكنه يعني أيضاً أنك عليك الاحتفاظ بتلك البيانات المشتقّة في الذاكرة تحسّباً لاحتياجك إليها لاحقاً.
فإن لم تكن تستخدم هذا النمط، لاستطعت حساب البيانات المشتقّة مباشرةً كلما احتجت إليها، ثم التخلّص منها حين تنتهي. وهذا يتجنّب كلفة الاحتفاظ بها مخزَّنة في الذاكرة، مقابل الاضطرار إلى تنفيذ ذلك الحساب في كل مرة تحتاج فيها إلى النتيجة.
إذن، مثل كثير من عمليات التحسين، يُضحي هذا النمط بالذاكرة مقابل السرعة. وفي المقابل للاحتفاظ بالبيانات المحسوبة سابقاً في الذاكرة، تتجنّب الحاجة إلى إعادة حسابها حين لم تتغيّر. وتكون هذه المقايضة منطقية حين يكون الحساب بطيئاً والذاكرة رخيصة. أما حين يكون الوقت لديك أكثر من الذاكرة، الأفضل حسابها عند الحاجة.
شيفرة نموذجية
لنفترض أننا استوفينا قائمة المتطلّبات الطويلة والمفاجئة، ولنرَ كيف يبدو النمط في الشيفرة. وكما ذكرتُ سابقاً، فالرياضيات الحقيقية وراء مصفوفات التحويل تتجاوز أهداف هذا الكتاب المتواضعة، لذا سأكتفي بتغليف ذلك في صنف يمكنك افتراض أن تنفيذ موجود في مكان ما في الأثير:
^code transform
العملية الوحيدة التي نحتاجها هنا هي combine() كي نستطيع الحصول على التحويل العالمي
لكائن عبر جمع كل التحويلات المحلية على امتداد سلسلة أصوله. ولديه أيضاً دالة للحصول على
تحويل «الأصل» -- أي مصفوفة الهوية التي تعني لا إزاحة ولا دوران ولا تحجيم على الإطلاق.
بعد ذلك، لنرسم صنف كائن في شجرة المشهد. وهذا أقل ما نحتاجه قبل تطبيق هذا النمط:
^code graph-node
لكل عقدة تحويل محلي يصف أين هي نسبةً إلى أصلها. ولديها mesh وهو الرسم البياني الفعلي
للكائن. (وسنسمح بأن تكون قيمة mesh_ هي NULL أيضاً للتعامل مع العُقد غير البصرية التي
تُستخدم لمجموعات أبنائها فقط.) وأخيراً، لكل عقدة مجموعة من عُقد الأبناء قد تكون
فارغة.
وبهذا، لا «شجرة مشهد» سوى عقدة جذر واحدة GraphNode يكون أبناؤها (وأبناء أبنائها،
إلخ) كل الكائنات في العالم:
^code scene-graph
ولرسم شجرة مشهد، كل ما نحتاج إلى فعله هو المرور على تلك الشجرة من العُقد بدءاً من الجذر، واستدعاء الدالة التالية لكل mesh في العقدة مع التحويل العالمي الصحيح:
^code render
ولن ننفّذ هذا هنا، لكن لو فعلنا لَفعل ما يحتاج إليه العارض لرسم ذلك الـ mesh في الموضع المعطى في العالم. فإذا استطعنا استدعاء ذلك بشكل صحيح وفعّال على كل عقدة في شجرة المشهد، فسنكون سعداء.
مرور غير مُحسَّن
لنُعرّف أيدينا، فلنُلقِ معاً مروراً أساسياً لرسم شجرة المشهد يحسب المواضع العالمية
أثناء العمل. لن يكن مثالياً، لكنه سيكون بسيطاً. وسنضيف دالة جديدة إلى GraphNode:
^code render-on-fly
نمرّر التحويل العالمي لأصل العقدة إلى هنا عبر parentWorld. وبذلك، كل ما يبقى للحصول
على التحويل العالمي الصحيح لـهذه العقدة هو جمعه مع تحويلها المحلي. ولا علينا المرور
إلى أعلى سلسلة الأصول لحساب التحويلات العالمية، لأننا نحسب أثناء العمل بينما نمرّ
إلى أسفل السلسلة.
نحسب التحويل العالمي للعقدة ونخزّنه في world، ثم نرسم الـ mesh إن كان لدينا واحد. وأخيراً
نستدعي الدالة تكرارياً في العُقد الفرعية، مع تمرير تحويل هذه العقدة العالمي. وكل
الشيء في كله، إنها طريقة تكرارية محكمة وبسيطة.
ولرسم شجرة مشهد كاملة، نطلق العملية من عقدة الجذر:
^code render-root
لنُصبح وسخاً
إذن هذه الشيفرة تفعل الشيء الصحيح -- فهي ترسم كل الـ meshes في المواضع الصحيحة --
لكنها لا تفعله بكفاءة. فهي تستدعي local_.combine(parentWorld) على كل عقدة في الشجرة،
في كل إطار. ولنرَ كيف يصلح هذا النمط ذلك. أولاً، نحتاج إلى إضافة حقلين إلى GraphNode:
^code dirty-graph-node
يخزّن الحقل world_ التحويل العالمي المحسوب سابقاً، وdirty_ هو بالطبع علامة
الوسخ. ولاحظ أن العلامة تبدأ بقيمة true. فحين ننشئ عقدة جديدة، لم نحسب تحويلها
العالمي بعد. فهي عند الولادة بالفعل خارج التزامن مع التحويل المحلي.
السبب الوحيد الذي نحتاج معه هذا النمط هو أن الكائنات يمكن أن تتحرّك، لذا لنضف دعم ذلك:
^code set-transform
الجزء المهم هنا هو أنها تضبط علامة الوسخ أيضاً. هل ننسى شيئاً؟ بلى -- العُقد الفرعية!
حين تتحرّك عقدة أصل، فإن الإحداثيات العالمية لكل أبنائها هي أيضاً مبطّلة. لكن هنا لا نضبط علامات وسخهم. يمكننا أن نفعل ذلك، لكنه تكراري وبطيء. بدلاً من ذلك، سنفعل شيئاً ذكياً حين نذهب إلى العرض. لنرَ:
^code dirty-render
وهذا مشابه للتنفيذ الساذج الأصلي. والتغييرات المفتاحية هي أننا نفحص ما إذا كانت العقدة
وسخة قبل حساب التحويل العالمي، وأننا نخزّن النتيجة في حقل بدلاً من متغيّر محلي. وحين
تكون العقدة نظيفة، نتخطّى combine() تماماً ونستخدم قيمة world_ القديمة التي لا تزال
صحيحة.
والجزء الذكي هو ذلك الوسيط dirty. فسيكون true إذا كان
أي عقدة أعلى هذه العقدة في سلسلة الأصول وسخة. وبالطريقة نفسها التي تُحدّث بها
parentWorld التحويل العالمي تزايدياً أثناء مرورنا نزولاً في التسلسل الهرمي،
تتبّع dirty قذارة سلسلة الأصول.
وهذا يتيح لنا تفادي وضع علامة dirty_ على كل ابن بشكل تكراري داخل setTransform().
بدلاً من ذلك، نمرّر علامة وسخ الأب إلى أبنائه حين نرسم، وننظر إليها أيضاً لنرى ما إذا
كنا بحاجة إلى إعادة حساب التحويل العالمي.
والنتيجة النهائية هنا هي بالضبط ما نريده: تغيير التحويل المحلي لعقدة لا يتطلّب سوى بضعة إسنادات، ورسم العالم يحسب بالضبط الحدّ الأدنى من التحويلات العالمية التي تغيّرت منذ الإطار الأخير.
قرارات التصميم
هذا النمط محدّد إلى حدٍّ كبير، لذا لا يوجد سوى بضعة مفاتيح لتدويرها:
متى تُمسح علامة الوسخ؟
-
حين تكون النتيجة مطلوبة:
-
يتجنّب الحساب تماماً إن لم تُستخدم النتيجة أبداً. وبالنسبة إلى البيانات الأولية التي تتغيّر بوتيرة أكبر بكثير من الوصول إلى البيانات المشتقّة، قد يكون هذا مكسباً كبيراً.
-
إن كان الحساب معاصراً للوقت، فقد يسبب توقّفاً ملحوظاً. ف تأجيل العمل حتى اللحظة التي يتوقّع فيها اللاعب رؤية النتيجة قد يؤثّر في تجربة لعبه. وغالباً ما يكون سريعاً بما يكفي حتى لا تكون هذه مشكلة، لكن إن كانت مشكلة فسيضطر إلى إنجاز العمل في وقت أسبق.
-
-
عند نقاط تحقّق محدّدة جيداً:
أحياناً، توجد نقطة في الزمن أو في تقدّم اللعبة يكون فيها تنفيذ المعالجة المؤجَّلة أمراً طبيعياً. مثلاً، قد نرغب في حفظ اللعبة فقط عندما يرسو قرصان في الميناء. أو قد لا تكون نقطة المزامنة جزءاً من آليات اللعبة. فقد نرغب ببساطة في إخفاء العمل خلف شاشة تحميل أو مشهد حركي.
-
إنجاز العمل لا يؤثّر في تجربة المستخدم. وعلى عكس الخيار السابق، يمكنك غالباً إعطاء اللاعب شيئاً يشتّت انتباهه بينما اللعبة مشغولة بالمعالجة.
-
تفقد التحكّم في متى يحدث العمل. فهذه نوعاً ما عكس النقطة السابقة. فأنت تملك تحكّماً على النطاق الصغير في متى تعالج، ويمكنك التأكّد من أن اللعبة تتعامل مع ذلك بأناقة.
وما لا تستطيع فعله هو ضمان أن اللاعب يصل فعلاً إلى نقطة التحقّق أو يستوفي المعايير التي عرّفتها. فإذا تاه أو دخلت اللعبة في حالة غريبة، فقد تنتهي إلى تأجيل أطول مما تتوقّع.
-
-
في الخلفية:
عادةً ما تبدأ مؤقّتاً ثابتاً عند أول تعديل، ثم تعالج كل التغييرات التي تحدث بين ذلك الحين وحين ينطلق المؤقّت.
-
يمكنك ضبط وتيرة تنفيذ العمل. بتعديل الفاصل الزمني للمؤقّت، يمكنك ضمان حدوثه بالوتيرة التي تريدها تماماً.
-
يمكنك إنجاز عمل مكرّر أكثر. فإذا كانت الحالة الأولية تتغيّر بمقدار ضئيل جداً أثناء عمل المؤقّت، فقد تنتهي إلى معالجة جزء كبير من بيانات لم تتغيّر في معظمها.
-
تحتاج إلى دعم لتنفيذ العمل بشكل غير متزامن. فمعالجة البيانات «في الخلفية» تعني أن اللاعب يستطيع مواصلة ما يفعله في الوقت نفسه. وهذا يعني أنك على الأرجح ستحتاج إلى الخيوط أو نوع آخر من دعم التزامن كي تستطيع اللعبة العمل على البيانات أثناء ما زالت تُلعَب.
وبما أن اللاعب على الأرجح يتفاعل مع الحالة الأولية ذاتها التي تعالجها، فستحتاج إلى التفكير في جعل ذلك آمناً للتعديل المتزامن أيضاً.
-
ما دقّة تتبّعك لعلامة الوسخ؟
تخيّل أن لعبة القراصان لدينا تسمح للاعبين ببناء سفينتهم وتخصيصها. وتُحفظ السفن تلقائياً عبر الإنترنت كي يتمكن اللاعب من المتابعة من حيث توقّف. ونحن نستخدم علامات الوسخ لتحديد أيّ أسطح السفينة تمّ تجهيزها وتحتاج إلى إرساله إلى الخادم. وكل كتلة بيانات نرسلها إلى الخادم تحتوي على بعض بيانات السفينة المعدَّلة وقليل من البيانات الوصفية التي تصف أين حدث هذا التعديل على السفينة.
-
إن كان أدقّ:
لنفترض أنك تضع علامة وسخ على كل لوح صغير في كل سطح.
- تعالج فقط البيانات التي تغيّرت فعلاً. فسوف ترسل إلى الخادم أوجه السفينة المعدَّلة بالضبط.
-
إن كان أقل دقّة:
وبدلاً من ذلك، يمكننا ربط بت وسخ بكل سطح. فأي تغيير عليه يوسم السطح كله بأنه وسخ.
-
تنتهي إلى معالجة بيانات لم تتغيّر. أضف مدفعاً واحداً إلى سطح وستضطر إلى إرسال السطح كله إلى الخادم.
-
يُستخدم ذاكرة أقل لتخزين علامات الوسخ. أضف عشرة مدافع إلى سطح وتحتاج إلى بت واحد فقط لتتبّعها جميعاً.
-
يُقضى وقت أقل على التكاليف الثابتة. حين تعالج بعض البيانات المتغيّرة، فغالباً ما يكون هناك قدر من العمل الثابت عليك إضافته فوق معالجة البيانات نفسها. وفي المثال هنا، تلك هي البيانات الوصفية المطلوبة لتحديد أين على السفينة تقع البيانات المتغيّرة. وكلما كانت كتل المعالجة عندك أكبر، كان عددها أقل، أي أن التكاليف الثابتة تكون أقل.
-
انظر أيضاً
-
هذا النمط شائع خارج الألعاب في أطر عمل الويب مثل Angular. فهي تستخدم علامات الوسخ لتتبّع البيانات التي تغيّرت في المتصفّح وتحتاج إلى رفعها إلى الخادم.
-
تتتبّع محرّكات الفيزياء أي الكائنات في حركة وأيها ساكن. فبما أن جسماً ساكناً لن يتحرّك حتى تُطبَّق عليه دفعة، فلا يحتاج إلى معالجة حتى يُلمس. وهذا البت الذي يقول «هل هو متحرّك؟» هو علامة وسخ تدلّ على الكائنات التي طُبِّقت عليها قوى وتُحتاج فيزياءها إلى حلّ.