أنماط برمجة الألعاب › المعمارية والأداء والألعاب 26 / 26 السابق

المعمارية والأداء والألعاب

^title المعمارية والأداء والألعاب ^section مقدمة

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

ما هي هندسة البرمجيات؟

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

بدلاً من ذلك، هذا الكتاب عن الشيفرة بين كل ذلك. فهو أقل عن كتابة الشيفرة منه عن تنظيمها. ولكل برنامج بعض التنظيم، حتى لو كان مجرّد «ادفع كل شيء في main() وانظر ماذا يحدث»، لذا أرى أن من الأكثر إثارة للاهتمام أن يتحدث عن ما الذي يصنع تنظيماً جيداً. فكيف نميّز المعمارية الجيدة من السيّئة؟

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

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

ما هي هندسة البرمجيات الجيدة؟

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

يبدو ذلك جميلاً، لكنه ليس قابلاً للتنفيذ تماماً. «فقط اكتب شيفرتك بحيث لا تزعج التغييرات سطحها الهادئ.» حسناً.

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

كيف تُحدث تغييراً؟

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

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

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

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

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

باختصار، مخطّط تدفّق البرمجة هو شيء من هذا الشكل:

احصل على المشكلة → تعلَّم الشفرة → اكتب الحل → نظِّف الكود → ثم عُد إلى البداية.

كيف يمكن لفصل الترابط أن يساعد؟

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

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

وبالنسبة إليّ، هذا هدف مفتاح في هندسة البرمجيات: تقليل المعرفة التي تحتاج إلى الاحتفاظ بها في جمجمتك قبل أن تقدّم أي تقدّم.

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

بأي ثمن؟

يبدو ذلك رائعاً، صحيح؟ افصل كل شيء وستتمكّن من الترميز كالريح. فكل تغيير سيعني لمس طريقة أو طريقتين مختارتين فقط، ويمكنك أن ترقص على سطح قاعدة الشيفرة دون أن تترك أي ظلّ.

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

لكن، ككل أمور الحياة، لا يأتي مجاناً. فالمعمارية الجيدة تتطلّب جهداً وانضباطاً حقيقيين. في كل مرة تجري تغييراً أو تنفّذ ميزة، عليك أن تعمل بجدّ لدمجها بأناقة في بقية البرنامج. وعليك أن تولي عناية بالغة لتنظيم الشيفرة جيداً والحفاظ على تنظيمها عبر آلاف التغييرات الصغيرة التي تشكل دورة تطوير.

عليك أن تفكّر في أي أجزاء من البرنامج ينبغي فصلها، وأن تقدّم التجريدات في تلك النقاط. وكذلك، عليك أن تحدّد أين ينبغي هندسة قابلية التوسّع كي تصبح التغييرات المستقبلية أسهل.

يتحمّس الناس لهذا كثيراً. إنهم يتخيّلون المطوّرين المستقبليين (أو مجرد نفسك في المستقبل) يدخلون قاعدة الشيفرة فيجدونها مفتوحة النهاية، وقوية، ونداعية للتوسّع. إنهم يتخيّلون «محرّك الألعاب الواحد الذي يحكم الجميع».

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

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

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

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

وقواعد الشيفرة من هذا النوع هي ما يُدفع الناس ضد هندسة البرمجيات وأنماط التصميم على وجه الخصوص. فمن السهل أن تنشغل بالشيفرة نفسها إلى حدّ تفقد في الأفق أنك تحاول إطلاق لعبة. فأغنية ابتذال التوسّع تسحب لا يُحصى من المطوّرين الذين يقضون سنوات يعملون على «محرّك» دون أن يدركوا أبداً ما هو المحرّك له.

الأداء والسرعة

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

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

لكن الأداء/documentation كله يقوم على الافتراضات. فممارسة التحسين تزدهر على القيود الملموسة. هل يمكننا أن نفترض بأمان أننا لن نتجاوز أبداً 256 عدوّاً؟ ممتاز، يمكننا أن نحزّم مُعرِّفاً في بايت واحد. هل سنستدعي دالة على نوع ملموس واحد هنا فقط؟ جيد، يمكننا أن نُرسل بشكل ساكن أو نُضمّنها. هل جميع الكيانات ستكون من الصنف نفسه؟ ممتاز، يمكننا أن نصنع مصفوفة متّصلة جميلة منها.

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

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

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

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

الجيّد في الشيفرة السيّئة

يأخذنا هذا إلى النقطة التالية، وهي أن هناك وقتاً ومكاناً لأنماط ترميز مختلفة. وكثير من هذا الكتاب عن صنع شيفرة قابلة للصيانة ونظيفة، لذا مولّي واضح لفعل الأمور بالطريقة «الصحيحة»، لكن للشيفرة سريعة الكتابة قيمة أيضاً.

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

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

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

والنمذجة الأولية -- أي جمع شيفرة شبه عاملة بالكاد للإجابة عن سؤال تصميمي -- ممارسة برمجية مشروعة تماماً. لكن هناك تحفّظ كبير جداً. فإذا كتبت شيفرة تُرمى، يجب أن تتأكّد من قدرتك على رميها. لقد رأيت مديرين سيّئين يلعبون هذه اللعبة مرّة بعد مرّة:

المدير: «مرحباً، لدينا فكرة نريد تجربتها. مجرّد نموذج أولي، فلا تشعر بأن عليك تنفيذها بشكل صحيح. فما مدى سرعة قدرتك على جمع شيء معاً؟»

المطوّر: «حسناً، إن قطعت زوايا كثيرة، ولم أختبره، ولم أوثّقه، وكان مليئاً بالأخطاء، يمكنني أن أعطيك بعض الشيفرة المؤقّتة خلال أيام قليلة.»

المدير: «رائع!»

يمضي بضعة أيام...

المدير: «مرحباً، هذا النموذج الأولي رائع. هل يمكنك أن تقضي بضع ساعات فقط لتنظيفه قليلاً الآن ولنسمّيه الشيء الحقيقي؟»

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

موازنة

لدينا بعض القوى في play:

  1. نريد معمارية جميلة تكون فيها الشيفرة أسهل في الفهم على مدى عمر المشروع.
  2. نريد أداءً سريعاً في وقت التشغيل.
  3. نريد إنجاز ميزات اليوم بسرعة.

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

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

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

ولا يوجد جواب بسيط هنا، بل مقايضات فقط. ومن الرسائل التي أتلقّاها، فإن هذا يُحبِط كثيراً من الناس. ولا سيما المبتدئين الذين يريدون فقط صنع لعبة، فسماع «لا يوجد جواب صحيح، بل نكهات مختلفة من الخطأ» أمر مُرهِب.

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

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

البساطة

في الآونة الأخيرة، أشعر أن أيّ طريقة تخفّف هذه القيود فهي البساطة. فشيفرتي اليوم أبذل جهداً كبيراً لكتابة أنظف حلّ وأكثره مباشرةً للمشكلة. تلك هي نوع الشيفرة التي بعد قراءتها تفهم تماماً ما تفعله ولا تتخيّل أيّ حلّ آخر ممكن.

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

كثيراً ما تكون سريعة التشغيل لمجرّد أن نفقة المعالجة ليست كبيرة ولا توجد شيفرة كثيرة لتنفيذها. (لكن هذا ليس الحال على أي حال. فبإمكانك حشر كثير من الحلقات والتعاود في كمّ ضئيل جداً من الشيفرة.)

لكن لاحظ أنني لا أقول إن الشيفرة البسيطة تستغرق وقتاً أقل في الكتابة. فقد تظنّ ذلك نظراً لناتج شيفرة أقل إجمالاً، لكن الحلّ الجيد ليس تراكمات من الشيفرة، بل هو تقطير لها.

نادراً ما تُعرض علينا مشكلة أنيقة. بدلاً من ذلك، لدينا كومة من حالات الاستخدام. تريد أن يفعل X ما هو Y حين Z، لكن W حين A، وهكذا. بمعنى آخر، قائمة طويلة من سلوكيات المثال المختلفة.

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

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

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

ابدأ، بُحث عن نافس

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

  • التجريد وفصل الترابط يجعلان تطوير برنامجك أسرع وأسهل، لكن لا تُضِع وقتك فيهما ما لم تكن واثقاً من أن الشيفرة المعنية تحتاج تلك المرونة.

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

  • تحرّك بسرعة لاستكشاف فضاء تصميم لعبتك، لكن لا تسرع لدرجة تترك خلفك فوضى. فأنت على أي حال س تعيش معها.

  • إن كنت ستطرح شيفرة، فلا تُضِع وقتك في تجميلها. فنجوم الروك يدمّرون غرف الفنادق لأنهم يعلمون أنهم سيغادرون في الغد.

  • لكن، الأهم من كل ذلك: إن أردت صنع شيء ممتع، فاصنعه وأنت تستمتع.