اذهب إلى المحتوى

رشا سعد

الأعضاء
  • المساهمات

    23
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو رشا سعد

  1. الخادم السحابي Cloud Server هو بنية تحتية متاحة للمستخدمين عبر الإنترنت، تؤمن لهم ما يحتاجونه من الموارد الحاسوبية عن بعد، مثل مساحات التخزين والذواكر والمعالجات وغيرها، يمكنك النظر إليه على أنه حاسوب خاص تستطيع إعداده والتحكم به بالطريقة نفسها التي تتعامل بها مع أجهزتك المحلية، سواء كانت خوادم أو حواسيب شخصية أو محمولة. يعرض هذا المقال بنية الخادم السحابي ومكوناته، والفرق بينه وبين عروض الاستضافة السحابية الأخرى، مع بعض النصائح لاختيار الاستضافة الأنسب لموقعك الإلكتروني أو تطبيقك على الويب. ستصادف في هذا المجال مصطلحات "خادم" و"خادم سحابي" و"خادم ويب" بالتبادل، ويشير الخادم السحابي عادةً إلى بيئة لينكس كاملة أو فعليًّا إلى حاسوب كامل. تعمل الخوادم السحابية بصفتها أجهزة افتراضية (VMs) أو برمجيات تحاكي الحواسيب التقليدية، وتجدها دائمًا ضمن مجموعات كبيرة أو عناقيد Clusters في تقنية تسمى الحوسبة الافتراضية Virtualization. يمكنك معرفة المزيد عن آلية عملها بمراجعة مقال [مقدمة إلى الاستضافة السحابية](). البرمجيات المستخدمة على السحابة التعرُّف عليها ضروري لفهم الخوادم السحابية، سنعرضها باختصار: نظم التشغيل: تثبيت نظام التشغيل هو خطوتك الأولى في إعداد الخادم السحابي، يعتمد معظم مستخدمي السحابة نظام تشغيل لينكس بتوزيعاته المختلفة، مثل: أوبنتو Ubuntu أو روكي لينكس Rocky Linux لبناء خوادمهم، نظرًا لانتشاره الواسع في بيئات الخوادم، والدعم الكبير الذي يحظى به من المجتمع التقني، والذي يسهل عليك إيجاد مصادر للتعلُّم، بالإضافة لكونه مجاني في الأساس، وحتى إن احتجت لتراخيص فتراخيصه مرنة موازنةً بغيره. يمكنك الاطلاع على مقال [كيف تختار توزيعة لينكس المناسبة لك؟]() لتتعرف أكثر على توزيعات لينكس، وتختار ما يناسبك منها. برمجيات جانب الخادم: يقصد بها مجموعة واسعة من البرمجيات والأدوات المستخدمة لإعداد الخوادم، وهي مصممة للعمل في البيئات السحابية، ومناسبة تمامًا لها، حيث لا شاشات عرض ولا بيئة سطح مكتب، فغالبًا ما تُثَبّت برمجيات جانب الخادم Server-Side وتُضبَط إعداداتها باستخدام واجهة سطر الأوامر، ويصل إليها المستخدم النهائي عبر المتصفح أو التطبيقات الأخرى. تتنوع هذه الأدوات وسيساعدك فهم أساسياتها على تجهيز خادمك السحابي. خوادم الويب: تسمح خوادم الويب لخادمك السحابي بالتواصل مع المستخدمين عبر الإنترنت بواسطة بروتوكول HTTP، وهي من أدوات جانب الخادم، تُبَرمَج للاستجابة بطريقة محددة مع كل طلب يطلبه العميل عبر برمجيات جانب العميل Client-Side، لنفترض على سبيل المثال: أن المستخدم طلب URL معين من المتصفح (الذي يلعب هنا دور العميل)، سيستجيب له الخادم، ويجلب مستند HTML المناسب، ثم يرسله للمتصفح فيعرض صفحة الويب المطلوبة. ستحتاج خادم ويب حتمًا، إذا كنت تبني خادمك السحابي من الصفر، وتنوي تجهيزه لاستضافة موقع إلكتروني أو تطبيق ويب، وأشهر خوادم الويب المستخدمة هي Nginx و Apache. يمكنك الاطلاع على مدخل إلى خادم الويب لمزيد من المعلومات. خوادم API: واجهات برمجة التطبيقات أو APIs هي برمجيات وسيطة تسمح للتطبيقات المختلفة بالتخاطب مع بعضها. خادم الويب نفسه هو خادم API يطبق HTTP API، ويوجد أنواع كثيرة غيره، مثل APIs الخاصة بمعلومات الطقس أو رحلات الطيران أو غيرها، يستخدمها الخادم السحابي لتبادل البيانات مع التطبيقات الخارجية ومصادر البيانات الخارجية، وقد يتصل الخادم بأكثر من واجهة API حسب طبيعة تطبيقك، وتدعى نقاط اتصال API بالتطبيق، أي النقاط التي تنفذ عمليات API، بنقاط الوصول أو endpoints، وتكون مثلًا عناوين URL تدل على الموارد المطلوب الاستعلام عنها. خوادم قواعد البيانات: يمكنك تسميها بقواعد البيانات فقط، وهي نوع آخر من خوادم API، لكنها بخلاف خوادم الويب لا تعرض البيانات المطلوبة منها بهيئة صفحات HTML ضمن المتصفح، إنما يجري التواصل معها عبر واجهات برمجة تطبيقات خاصة تسمى واجهات استعلام قاعدة البيانات. بعض قواعد البيانات متاح للاستعلام الخارجي عبر الويب، وبعضها الآخر مخصص للاستخدام الداخلي من قبل برمجياتك السحابية فقط. الخوادم السحابية وبدائلها الخادم السحابي هو حاسوب افتراضي كامل، لذا فجميع المنتجات السحابية الأخرى تتعلق به، ومزودو الخدمة السحابية يقدمون لك في عروضهم ميزة أو وظيفة محددة فقط من وظائف هذا الخادم، على سبيل المثال استضافة ويب مخصصة أو استضافة قاعدة بيانات مخصصة، وكل حل سحابي يوفر خادم ويب أو قاعدة بيانات ينبثق فعليًّا من خادم سحابي، غالبًا ما تبنى الخوادم السحابية باستخدام عناقيد من الخوادم المادية أو الفيزيائية التي تعمل معًا بتقنية الحوسبة الافتراضية. فالفرق الجوهري إذًا بين الخادم السحابي (الذي يسمى VPS أو الخادم الافتراضي الخاص Virtual Private Server) والعروض السحابية الأخرى هو أنك تستطيع تثبيت التطبيقات التي تريدها على الخادم السحابي، وتشغيلها بحرية حسب احتياجك، بينما تفرض عليك العروض الأخرى مجموعة ميزات ووظائف بعينها لا يسعك الخروج عنها. أحدثت الحوسبة الافتراضية تغييرًا كبيرًا في سوق الخدمات السحابية خلال العقود الماضية، فقبل ظهورها كان مزود الخدمة يوفر خادمًا مثل Nginx أو Apache لتقديم الاستضافة لعشرات المواقع مع أذونات مخصصة لكل مشترك، ولم يكن المشترك مسؤولًا عن أي مهام إدارية على الخادم، لكن بالمقابل اقتصرت المواقع المستضافة وقتها على المواقع ذات الطبيعة الثابتة أي التي تتضمن عناصر HTML و CSS وجافا سكريبت ثابتة بدون محرك خلفي، وأيضًا مواقع drop-in PHP التي لا تمتلك أي اعتماديات سوى خادم الويب. ولاحقًا بعد انتشار الحوسبة الافتراضية، ازداد الطلب على عروض VPS أو الخوادم السحابية الكاملة، بسبب مرونتها العالية، لكن تشغيلها وتحديدًا في بيئة الإنتاج، يتطلب منك مهارات خاصة، لا يهتم المبرمجون بتحصيلها عادةً، لأنها لا ترتبط مباشرةً بكتابة الشيفرات البرمجية، أبرزها مثلًا: معرفة جيدة بأساسيات لينكس، وكيفية التعامل مع سطر الأوامر، ومهام إدارة النظام، بالإضافة إلى هندسة DevOps لإدارة عمليات النشر وتلبية المعايير الأمنية للتشغيل. هل أستخدم خادمًا سحابيًا لمشروعي؟ تحتوي الخوادم السحابية ميزات حماية افتراضية مدمجة فيها، ولست مضطرًا لخوض عملية نشر على نطاق تجاري بحذافيرها لتُشغل تطبيقًا مفتوح المصدر بطريقة آمنة وموثوقة على خادم سحابي، أضف إلى ذلك أن إعدادات الأمان الافتراضية مضبوطة بإحكام على معظم الحزم المستخدمة في الخوادم، وتخضع للتحديث الدوري لسد ثغراتها الأمنية، لذا يكفيك في معظم الأحيان تثبيت جدار حماية بسيط مثل ufw أو الجدار الناري غير المعقد ليتحكم ببوابات الشبكة على خادمك السحابي، فيسمح أو يمنع اتصالات معينة حسب متطلبات عملك، أو على الأقل يمررها إلى خادم ويب مخضرم مثل Nginx تعتمده ملايين الخوادم حول العالم. إضافةً إلى الخوادم السحابية، يوفر بعض مزودي الخدمات السحابية عروضًا أخرى يمكن تشبيهها بمضيفي الويب، تلائم مواقع الويب ذات المكونات الثابتة المعتمدة على ميزات جافا سكريبت الحديثة، التي تخفف الحاجة إلى الخادم الخلفي Backend، يسمى هذا النوع من الاستضافة نظام إدارة محتوى بدون رأس Headless CMS، ويقدم المزود معه عادةً أدوات تأليف ونماذج ويب تعد جزءًا من عرض أوسع يسمى البرنامج بصيغة خدمة أو Software As A Service. ويقدم بعض المزودين أيضًا استضافة الخوادم الخفية Serverless أو ما يسمى الوظائف بدون خادم، ويقصد بها كتابتك التعليمات البرمجية لتطبيقك مرةً واحدة بمعزل عن البنية التحتية للخوادم، ثم نشرها في البيئة السحابية ليجري تشغيلها فقط عند الطلب، يطلق على هذا الأسلوب عند استخدامه مع مواقع الويب الثابتة اسم جامستاك Jamstack. تتميز خدمات الحوسبة خفية الخوادم وخدمات نشر مواقع الويب الثابتة بأنها تقنيات قابلة للنقل، وتشبه الاستضافات القديمة إذ تعفي صاحب التطبيق تقريبًا من جميع مسؤوليات الأمن والصيانة التي ترافق عمليات النشر الكاملة على الخوادم، لكنها مع ذلك تظل محدودة النطاق، وقد لا تفي بالغرض في بعض الحالات، فإذا تطلب مشروعك على سبيل المثال نشر حاوية دوكر خلف خادم Nginx في مكدس واحد، لن تكفيك هذه الاستضافات وستحتاج خادمًا سحابيًا كاملًا. عمومًا، أي تطبيق تنشره على الخادم السحابي يمكنك نشره على حاسوبك المحلي أيضًا، مع بعض الاختلافات لصالح الخادم السحابي، فنادرًا ما يثبت الأفراد نظام تشغيل لينكس على حواسيبهم الشخصية، كما هو الحال في الخوادم السحابية، بل يعتمدون ويندوز أو ماك، ومعظم أدوات جانب الخادم لا تعمل مباشرةً مع هذه النظم على عكس لينكس. قد لا تكون هذه النقطة مؤثرة كثيرًا بالنسبة لك، لكن ما نريد إيصاله أن الخادم السحابي يناسب جميع الاحتياجات، ويمكنك إعداده لأي غرض تريده تمامًا كما تفعل مع حاسوبك المحلي. كيفية تحديد مواصفات الخادم يعتمد أداء الخوادم السحابية وسعرها على مواصفات العتاد الصلب، ويخصص لكل خادم سحابي ضمن العنقود Cluster مقدار معين من الموارد الحاسوبية، يمكن زيادته أو إنقاصه بمرونة تامة وبسرعة، على عكس الحواسيب الفيزيائية، لذا قبل حجز خادمك السحابي حدد أولًا مواصفاته الفنية، وادرس تأثيرها على جودة عمل التطبيق، ثم وازن ذلك مع ميزانيتك، وبعدها اختر الخادم المناسب. تُحَدَّد مواصفات الخوادم السحابية عادةً بعدد نوى المعالجات المركزية CPU المتوفرة لديها، وحجم ذاكرة الوصول العشوائي RAM المتاحة، وما يخصص لها من مساحات تخزينية على الأقراص الصلبة. ونظرًا لاختلاف مقاييس سرعة الأقراص والمعالجات في الخوادم السحابية عنها في ظروف التشغيل في العالم الحقيقي، فقد اعتمد معظم مزودي الخدمات السحابية مواصفاتٍ موحدة لها، فمتوسط سرعة الأقراص يعادل تقريبًا سرعة أقراص SSD، وسرعة وحدة المعالجة المركزية تماثل سرعة نواة Intel Xeon. وبالإضافة إلى المواصفات السابقة يفرض بعض المزودين قيودًا على الخوادم السحابية في الطبقة الدنيا، مثل: تحديد عدد عمليات الإدخال والإخراج المسموحة على القرص (IOPS)، وإجمالي حركة المرور المتبادلة مع الخودام عبر الشبكة وغير ذلك، قد تسبب هذه القيود اختناقات لبعض البرامج المستضافة. أما بخصوص الحجوم التخزينية، فيوفر لك معظم المزودين إمكانية حجز مساحات تخزين إضافية حسب احتياجاتك، بتقنيات مثل: التخزين الكتلي والتخزين الكائني، وربطها بخادمك الخاص VPS، وهذا أفضل من احتفاظك بجميع البيانات في مكان واحد على القرص التخزين الأساسي داخل VPS وتوسيع مساحته باستمرار. يُعطى لكل خادم سحابي عنوان IP عام مخصص له، للاتصال به عبر شبكة الإنترنت، وقد يكون العنوان من الإصدار الرابع IP4 وفق الصيغة 123.123.123.123 أو من الإصدار السادس IP6 من نمط 2001:0db8:0000:0000:0000:ff00:0042:8329، والحالة المعتادة أن يقابل عنوان IP اسم دومين مثل: https://my_domain.com. تتضمن بعض عروض الاستضافة السحابية تزويدك بعنوان IP لكل خادم VPS تحجزه، بينما يطلب منك بعض المزودين شراء عناوين IP بنفسك وتسجيلها للخوادم، تسمى هذه العناوين بعناوين IP المحجوزة Reserved IPs، وهي مناسبة لعمليات النشر واسعة النطاق. يمكنك شراء اسم الدومين من مزود الاستضافة نفسه فبعضهم مثل ديجيتال أوشن يقدمون لك المنتجين معًا، وتستطيع أيضًا الحصول عليه من مصدر آخر ثم إنجاز الربط بينهما بواسطة سجلات DNS. الوصول للخادم السحابي يتطلب الوصول إلى الخوادم السحابية إتقانك العمل في بيئة سطر الأوامر أو الطرفية Terminal محليًا أو عن بعد، وأكثر البروتوكولات استخدامًا في هذا المجال بروتوكول SSH الذي يستخدم البوابة 22 ويعتمد عليه مسؤولو الخادم، وبروتوكول HTTP على البوابة 80 و HTTPS على البوابة 443 المخصصين أكثر للمستخدم النهائي، تتمحور إدارة النظام السحابي حول هذه الخوادم والخدمات والبروتوكولات. الخاتمة تناول هذا المقال التعليمي نظرة عامة على الخوادم والعروض السحابية، وكيفية تطورها مع الزمن، ليضعك على بداية الطريق في العمل مع هذه الخدمات، ويعينك على اختيار أنسبها لمشروعك. ترجمة -وبتصرف- للمقال Cloud Servers: An Introduction لصاحبه Alex Garnett. اقرأ أيضًا مفهوم السحابة Cloud تعلم الحوسبة السحابيّة: المتطلبات الأساسيّة، وكيف تصبح مهندس حوسبة سحابيّة الحوسبة الافتراضية virtualization وأوجه اختلافها عن الحاويات containers كل ما تود معرفته عن السحابة الهجينة Hybrid Cloud
  2. يزداد الاعتماد على التطبيقات الذكية في جوانب حياتنا يومًا بعد يوم، ومع هذا الانتشار يزداد تعقيدها، وتتشعب وظائفها، فتصبح الحاجة ملحةً أكثر لابتكار تقنيات جديدة تحسِّن التفاعل بين خوادم التطبيقات والعملاء، ولعل أبرزها في السنوات الأخيرة GraphQL، وهي لغة استعلام مفتوحة المصدر لواجهات برمجة التطبيقات APIs وبيئة تشغيل لتنفيذ الاستعلامات، طورتها شركة فيسبوك Facebook في عام 2012 بهدف التغلب على نقاط الضعف في بنية REST التقليدية، وطرحتها للاستخدام العام في 2015، وأكثر ما يميز GraphQL جودة أدائها وكونها لغة تصريحية declarative وموجهة كليًا لتلبية طلبات العميل وما يحتاجه حقًا من معلومات. فما هي مفاهيم GraphQL الأساسية؟ وما أوجه التشابه والاختلاف بينها وبين REST؟ ما هي GraphQL؟ GraphQL هي اختصار للعبارة Graph Query Language وتعني لغة استعلام بيانية، وهي مختلفة قليلًا عن لغات الاستعلام الأخرى مثل SQL وغيرها، فهي لا تتخاطب مع قاعدة بياناتك مباشرةً إنما تصف نموذج التواصل بين العميل وخادم واجهة برمجة التطبيقات API، ولديها مجموعة مواصفات قياسية بمثابة معيار موحد يحدد خصائصها وقواعد استخدامها، وبما أنك تتبع مواصفات GraphQL، فيمكنك استخدامها مع أي لغة برمجة، ومع أي قاعدة بيانات، ومع جميع أنواع العملاء إذا كانوا قادمين من تطبيق ويب أو تطبيق هاتف محمول، فهي كما ذكرنا مفتوحة المصدر لا تقتصر على أنواع معينة. يُعد Apollo GraphQL من أشهر تطبيقات خادم وعميل GraphQL التجارية وأكثرها انتشارًا بين المطورين، وستجد في هذا المقال على أكاديمية حسوب مثالًا عمليًّا عن طريقة بنائه. خصائص GraphQL سنعرض بعضًا من خصائص GraphQL الأساسية، مثل: استعلاماتها التصريحية declarative والهرمية hierarchical، وكونها ذات قواعد صارمة في التعامل مع أنواع البيانات strongly-typed، وأيضًا استقرائية introspective تسمح بالكشف عن مواصفات مخططاتها الداخلية Schema ليستفيد منها طالب الاستعلام. تصريحية Declarative تعني التصريحية أن العميل سيحدد أو يصرح عن الحقول التي يريد الاستعلام عنها فقط ويطلبها من الخادم، والخادم بدوره سيرجعها هي بالذات دون أي معلومات إضافية. ألقِ نظرةً على المثال التالي لإيضاح الأمر. لنفترض أنك تطلب واجهة برمجية API للعبة ما وتستعلم عن حقول محددة لأحد شخصياتها، على سبيل المثال الاسم name والتصنيف race لشخصية المحارب warrior صاحب المعرف رقم "1"، فسيكون الطلب وفق الآتي: { warrior(id: "1") { name race } } ستعيد الاستجابة المُعادة من تنسيق JSON كائنًا لنسميه data يتضمن الحقلين المطلوبين فقط من بيانات المحارب رقم "1": { "data": { "warrior": { "name": "Merlin", "race": "HUMAN" } } } تتضمن هذه الاستجابة ما يطلبه العميل فقط دون زيادة أو نقصان وتمنح تطبيقك كفاءةً أعلى وأداءً أفضل على الشبكة موازنةً ببدائل GraphQL الأخرى مثل REST التي تُعيد للعميل كامل بيانات العنصر المُستَعلم عنه فتسبب ضغطًا على الشبكة. هرمية Hierarchical يمكنك طلب استعلامات هرمية من GraphQL أي الاستعلام عن أصل وفروعه، وستصلك البيانات المعادة من الخادم بنفس الهرمية التي طلبتها؛ فطلب الاستعلام يحدد شكل الاستجابة. لو عُدنا للمثال السابق واستبدلنا الاستعلام عن اسم المحارب وتصنيفه بالاستعلام عن اسمه وأسلحتهweapons وبالتحديد عن اسم كل سلاح name ودرجة قوته الهجومية attack: { warrior(id: "1") { name weapons { name attack } } } ستتضمن الاستجابة الآن اسم المحارب ومصفوفة كائنات الأسلحة weapons مرتبةً كما طلبناها في الاستعلام warrior، وقد استطاعت GraphQL إحضارها بطلب استعلام واحد فقط، رغم أن بيانات الأسلحة ومقاتلي اللعبة تكون مخزنة غالبًا في جداول منفصلة ضمن قاعدة البيانات وهذه نقطة قوتها. نذكرك هنا أن GraphQL ليست معنية مطلقًا بطريقة تخزين البيانات في قاعدة البيانات إنما بتحديد نموذج الاستعلام فقط. ألقِ نظرةً على الاستجابة الهرمية لاستعلامنا: { "data": { "warrior": { "name": "Merlin", "weapons": [ { "name": "Sword", "attack": 4 }, { "name": "Bow", "attack": 3 }, { "name": "Axe", "attack": 2 } ] } } } صارمة في تحديد الأنواع Strongly-typed توصف GraphQL بأنها صارمة في التعامل مع أنواع البيانات، ولديها نظام خاص لتحديد الأنواع يسمى نظام النوع، يصف إمكانات الخادم أي أنواع البيانات التي يقبلها، وتتدرج من البيانات المفردة Scalars وهي بيانات أولية، مثل: الأعداد الصحيحة والسلاسل النصية والقيم المنطقية، وصولًا إلى أنواع البيانات المعقدة مثل الكائنات التي تتكون من مجموعة حقول من البيانات الأولية. يبين المثال التالي إنشاء نوع في مخطط GraphQL اسمه Weapon، وهو كائن يمتلك حقولًا أولية من نوع نص String وعدد صحيح Int: type Weapon{ name: String! attack: Int range: Int } إذًا، نظام النوع هو المسؤول عن صحة تعريف مخطط GraphQL، ويُقيّم الخادم بواسطته إذا كان طلب الاستعلام الذي كتبته مقبولًا أم لا قبل تنفيذه، ثم يُخضِعه للتحقق للتأكد من سلامته قواعديًا وخلوه من الأخطاء. ذاتية التوثيق Self-documenting يدعم خادم GraphQL خاصية الاستقراء Introspection، وهذا يعطي عملاءه والبرامج المتصلة معه القدرة على استقراء مخططاته الداخلية والاستعلام عن بنيتها، ويُسهل أيضًا تطوير أدوات مساعدة للتعامل معه نحو GraphiQL التي توفر بيئة تطوير متكاملة IDE وبيئة تجريبية Playground تعمل ضمن المتصفح، وغيرها من أدوات التوثيق الآلية. هذا مثال بسيط عن استخدام الاستقراء لاستكشاف معلومات إضافية عن النوع Weapon باستعمال الكلمة المفتاحية schema__: { __schema { types { name kind description } } } سيجيبك خادم GraphQL بصيغة JSON المعتادة وفق الآتي: { "data": { "__schema": { "types": [ { "name": "Weapon", "kind": "OBJECT", "description": "A powerful weapon that a warrior can use to defeat enemies." } ] } } } موجهة بطلبات العميل Client-driven يتركز جُلّ عمل المطور عند بناء GraphQL API في الواجهة الخلفية، فيُعرّف المخطط schema وينفّذه، ويهيئ نقطة الوصول الوحيدة endpoint مع الواجهة البرمجية التي تًميّز GraphQL عن غيره وتُعدّ نقطة قوته، أما من طرف العميل فيمكنه طلب البيانات التي يريدها بدقة عبر الاستعلامات التصريحية، ومهما تغيرت مواصفات الاستعلامات، يستطيع مطور الواجهة الأمامية مواكبتها وإنجاز تصاميم تكرارية سريعة لتطبيقه دون أي تعديل إضافي على الواجهة الخلفية. بنية GraphQL يعمل GraphQL في طبقة التطبيقات Application layer وسيطًا بين العميل والبيانات، إذ يصف خادم GraphQL إمكانات الاستعلام التي تتيحها الواجهة البرمجية API، ويحدد العميل متطلبات طلب الاستعلام حسب احتياجه. الخادم Server يعمل GraphQL في طبقة التطبيق، وهو غير مرتبط ببروتوكول نقل محدد، لكنه يستخدم غالبًا بروتوكول HTTP، ولديه نقطة اتصال وحيدة تسمى عادةً graphql/ وتتيح الوصول لكل موارد الخادم. يمكنك برمجة خادم GraphQL بأي لغة برمجة، فعلى سبيل المثال تساعدك البرمجية الوسيطة express-graphql على إنشاء GraphQL API على خادم Express HTTP في بيئة Node؛ وفيما يخص قواعد البيانات فلا يقتصر خادم GraphQL على نوعٍ محدد منها، إذ يمكنه التعامل مع البيانات المخزنة في MySQL أو PostgreSQL أو MongoDB أو حتى القادمة من مصادر أخرى عبر نقاط اتصال لواجهات REST التقليدية، فالمهم في الأمر أن تُعرّف البيانات في مخطط GraphQL صحيح يبين الاستعلامات التي يستطيع العميل طلبها من الواجهة البرمجية API. العميل Client تدعى الطلبات المرسلة من عميل GraphQL إلى الخادم بالمستندات documents، وقد تكون طلبات قراءة فقط أي استعلامات، أو طلبات كتابة تسبب تعديلًا على البيانات وهذه تسمى طفرات mutations. يمكنك توجيهها بطلب XMLHttpRequest بسيط، أو بعملية fetch لجلب البيانات من متصفح الويب، أو بالاعتماد على أدوات عميل متقدمة نحو عميل Apollo أو ريلي فيسبوك التي تقدم لك مميزات مختلفة مثل التخزين المؤقت. إليك مثال لاستخدام الدالة fetch في جلب البيانات من نقطة الوصول graphql/، وقد مُرِّر مستند GraphQL بهيئة سلسلة نصية ضمن متن الطلب POST: async function fetchwarriors() { const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: `{ warriors { id name }, }`, }), }) const warriors = await response.json() return warriors } fetchwarriors() وهذه هي الاستجابة: { "data": { "warriors": [ { "id": "1", "name": "Merlin" }, { "id": "2", "name": "Gandalf" } ] } } مقارنة بين GraphQL و REST كلاهما يعملان للهدف نفسه، وهو تبادل البيانات بين تطبيقات مختلفة؛ إذ عرّفنا GraphQL في بداية المقال على أنها لغة استعلام بيانية وبيئة تشغيل لتنفيذ الاستعلامات؛ أما REST فهي اختصارٌ للعبارة Representational State Transfer وهي معمارية شهيرة لمشاركة البيانات عبر الويب، و RESTful API هي واجهة برمجة تطبيقات تتبع معايير REST، مثل: انعدام الحالة stateless، وقابلية التخزين في ذاكرة التخزين المخبئية Cache، واستقلال التقنيات المستخدمة في جانبي العميل والخادم عن بعضها، إضافةً إلى الواجهة المعيارية الموحدة التي تستخدم معرفات فريدة مثل عناوين URIs وغيرها. تُعد GraphQL أحدث من REST وقد بُنيت في الأساس لمعالجة نقاط ضعفها بإنشاء واجهة API عالية الكفاءة وموجهة للعميل. لنبدأ الآن الموازنة بينهما لنعرف مزايا وعيوب كل تقنية: البنية: تحتوي بنية REST نقاط وصول متعددة للتخاطب مع الخادم، أما GraphQL فتستخدم نقطة وصول وحيدة، وستعطيك مخططًا للبيانات التي طلبتها باستعلام واحد مهما كانت معقدة ومتشعبة، أما في REST فستحتاج عددًا من الاستعلامات للحصول على البيانات المتشعبة نفسها. إذًا، تتفوق GraphQL على REST في تخفيف الضغط على الشبكة. جلب البيانات: عندما تستعلم عن تفصيل ما من واجهة REST تجيبك بأكثر مما طلبت، وترسل لك مجموعة البيانات المرتبطة بهذا التفصيل كاملةً كما هي معرفة على الخادم، حتى لو كنت تحتاج رقمًا واحدًا منها فقط، وفي حالاتٍ أخرى لا تكون استجابتها كافية، فعلى سبيل المثال قد لا تعطيك نقطة الوصول الخاصة بقائمة ما على الخادم جميع الخصائص التي تريد معرفتها عن القائمة فستحتاج نقاط أخرى معها أيضًا، بينما تخلصك GraphQL باستعلاماتها التصريحية من هذا الإفراط أو التقصير في جلب البيانات، وتحضرها لك بالهيكلية التي يحددها العميل دون زيادة أو نقصان. التعامل مع الأخطاء: لا ينحصر استخدام GraphQL مع بروتوكول HTTP فقط، بل يمكنها الاعتماد على غيره أيضًا، لذا فهي لا تعتمد على رموز استجابة HTTP لعرض أخطاء طلباتها للعميل، فمعظم الطلبات العائدة من نقاط وصول GraphQL ستحمل الرمز 200 سواء كانت صحيحة أو خاطئة، وستجد ضمن استجابة الطلبات الخاطئة رسائل واضحة عن أخطائك جنبًا إلى جنب مع البيانات data وهذا بفضل خاصية الأخطاء errors، أما واجهة RESTful API فتعتمد كليًا على رموز استجابة HTTP، فيشير الرمز 200 دومًا للطلبات الصحيحة، وتعبّر رموز 400 عن الطلبات الخاطئة دون أي تفاصيل عن طبيعة الخطأ. الإصدارات: تسعى GraphQL في جميع تعديلاتها لتجنب التغيرات الجذرية breaking changes، التي من المحتمل أن تسبب أخطاء في جانب العميل، ويحاول مطوروها الحفاظ على التوافقية مع الإصدارات السابقة، فهي توفر إمكانية زيادة ميزات جديدة على الواجهة بإضافة أنواع بيانات جديدة وحقول جديدة بدون الحاجة لإنشاء إصدار جديد، على عكس REST التي تتعامل مع أي تعديل على أنه إصدار جديد لنقاط الوصول، ويُشار للإصدارات صراحةً في عناوين URL، فتجد فيها رموزًا مثل: V1/ أو V2/، ولهذه الآلية أيضًا مشكلاتها. يمكنك عمومًا التعامل مع تغيُّر الإصدارات في التقنيتين وإن كانت طريقة REST أكثر تقليدية. التخبئة Caching: تُعد التخبئة مبدأ من مبادئ REST، ولكونها تعتمد على توابع HTTP لجلب البيانات من نقاط وصولها المتعددة، فبوسعها الاستفادة من ميزة التخبئة -إحدى أساسيات HTTP- وعدم إعادة جلب الموارد نفسها من الخادم في كل مرة يحتاجها العميل. بالمقابل لا تستفيد GraphQL من ميزة التخبئة المُضمّنة في HTTP، لأنها تستخدم نقطة وصول وحيدة لجميع الطلبات، وكل طلب من طلباتها مخصص وفريد عن غيره، ومع ذلك يستطيع عملاء GraphQL استخدام التخبئة بصورة مبسطة باستعمال المُعرّف العمومي للكائن. لنختتم الفقرة الآن بجدول يلخص أوجه الشبه والاختلاف بين GraphQL و REST، واضعًا في حسبانك إمكانية استخدامهما معًا في مشروع واحد، فتكون GraphQL مثل بوابة أو نقطة تجميع تتلقى البيانات الواردة من خدمات REST. الميزة GraphQL REST الوصف GraphQL هي لغة استعلام لواجهات برمجة التطبيقات، ووقت تشغيل من جانب الخادم لتنفيذ الاستعلامات REST هي نمط معماري لتصميم خدمات الويب جلب البيانات عبر نقطة وصول وحيدة تستقبل طلبات الاستعلام المحددة بدقة من طرف العميل والمنقولة ببروتوكول HTTP عبر عدة نقاط وصول HTTP تعيد للعميل مجموعة بيانات محددة مسبقًا من قبل الخادم الإصدارات غير شائعة شائعة رموز استجابة HTTP كافة الاستجابات حتى الخاطئة منها تعود بالرمز 200 تطبق رموز استجابة HTTP بدلالتها المعروفة التحقق التحقق من البيانات الوصفية مضمن في GraphQL المطور يجري عمليات التحقق بنفسه يدويًا التوثيق التوثيق ذاتي بفضل وجود نظام النوع وميزة الاستقراء لا يوجد توثيق ذاتي لكن يمكنك الاستفادة من أدوات خاصة مثل OpenAPI التخبئة غير متاح متاح أساليب الطلب جميع الطلبات سواء كانت استعلامات أو طفرات أو اشتراكات تُرسل بأسلوب POST عبر بروتوكول HTTP تستخدم كل أساليب HTTP مثل GET و POST و PUT و PATCH و DELETE وغيرها صيغة الاستجابة JSON بأي صيغة مثل JSON و XML و HTML وغير ذلك الخاتمة كان هذا المقال الأول من سلسلة مقالات تتناول إدارة البيانات باستخدام GraphQL، وقد تعرفنا فيه على GraphQL لغة الاستعلام مفتوحة المصدر الخاصة بواجهات برمجة التطبيقات، هذه اللغة التي طورتها فيسبوك موجهةً إياها كليًا لتلبية طلبات العميل، عبر كتابة استعلامات تصريحية يوضح فيها ما يحتاجه فعلًا، لتتجاوز بذلك مشكلات REST التقليدية، مثل الإفراط أو التقصير في جلب البيانات، إضافةً إلى انخفاض الكفاءة على الشبكة. لا يعتبر المقال GraphQL بديًلا لواجهات REST، فلكلٍ منهما تطبيقات خاصة تتميز فيها عن الأخرى، وأيضًا طريقة خاصة في إدارة البيانات المتبادلة بين العميل والخادم، وقد عرضنا هنا طريقة GraphQL في عرض البيانات، تابع معنا بقية مقالات السلسلة لتطلع على المزيد. ترجمة -وبتصرف- للمقال An Introduction to GraphQL لصاحبته Tania Rascia. اقرأ المزيد مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة. شرح فلسفة RESTful - تعلم كيف تبني واجهات REST البرمجية. دليلك الشامل إلى برمجة التطبيقات.
  3. تُسهّل GraphQL الاتصال بين الواجهة الأمامية لتطبيقك وقاعدة البيانات عبر استعلاماتها التصريحية والموجهة بطلبات العميل، والبنية التي توضح إمكانات هذه الاستعلامات وتفاصيلها هي مخططات GraphQL المحكومة بنظام النوع، فما هو نظام النوع؟ وكيف يساعدك فهمه على بناء مخططات فعالة تحقق لك الفائدة المرجوة من GraphQL؟ سنعرض ذلك مع بعض الأمثلة التطبيقية على كل نوع بدءًا من الأنواع الخمسة المفردة المبنية مسبقًا built-in scalar، والتعدادات Enums، والقوائم والواجهات Interfaces والأنواع غير الفارغة الغالفة non-null wrapping، ونوع الكائن Object، وأنواع التجريد Abstract والاتحاد Union أيضًا. متطلبات العمل ستحتاج هذه الأساسيات لتطبيق الأمثلة الموجودة هنا: الاطلاع على المقال الأول مقدمة إلى GraphQL لفهم المبادئ الأساسية للتقنية. بيئة عمل تحوي خادم GraphQL، يمكنك تجهيزها بتنفيذ الخطوات الموجودة في مقالنا الثاني إعداد خادم GraphQL في بيئة Node.js. الأنواع المفردة Scalar إذا شبهت استجابة GraphQL بالشجرة متفرعة الأغصان فستكون البيانات المفردة هي الأوراق في نهاية كل غصن، ففي نهاية الأمر تتحلل جميع البيانات في مخطط GraphQL مهما بلغ تعقيدها وتداخلها إلى بيانات مفردة، وللبيانات المفردة خمسة أنواع: نوع العدد الصحيح Int Int هو نوع عدد صحيح لا يحتوي فاصلة عشرية، ومُؤشّر بإشارة موجب أو سالب، ويتكون من 32 بت، لذا تتراوح قيمته بين "2,147,483,647-" و "2,147,483,647"، والعدد الصحيح هو واحد من نوعين فقط لتمثيل الأعداد في GraphQL. نوع العدد العشري Float Float هو نوع عدد عشري ثنائي الدقة، مثل 1.2، ومُؤشّر بإشارة موجب أو سالب، وهو النوع المفرد الثاني لتمثيل الأعداد في GraphQL. نوع السلسلة النصية String String هو نوع سلسلة محارف بترميز UTF-8 تُستخدم لكتابة النصوص والأعداد الكبيرة جدًا، وتُعد السلاسل النصية أكثر الأنواع المفردة استخدامًا. النوع البولياني Boolean يحمل النوع البولياني Boolean إما قيمة صحيحة true أو خاطئة false. نوع الرقم التعريفي ID ID هو رقم تعريفي فريد، يُمثّل دائمًا بصيغة سلسلة نصية حتى لو تضمن أرقامًا، ويُولّد غالبًا بواسطة خوارزميات المُعرّف العالمي الفريد Universally Unique Identifier أو اختصارًا UUID. أنواع مفردة مخصصة تغطي الأنواع المفردة الخمسة السابقة معظم حالات الاستخدام، لكنك قد تحتاج أنواعًا أخرى تناسب طبيعة تطبيقك، مثل الوقت Time، والتاريخ Date، وعناوين url، والتي يمكنك تعريفها باستعمال الكلمة المفتاحية scalar، وسيعتمد الخادم عندها هذه الأنواع معيارًا للتحقق من صحة بياناتك المدخلة ومطابقتها لمواصفات النوع المخصص الذي تنتمي إليه. إليك مثالًا عن تعريف نوع التاريخ Date: scalar Date يستخدم خادم GraphQL الوحدة البرمجية GraphQLScalarType للتعامل مع الأنواع الجديدة المُضافة. نوع التعداد Enum يصف التعداد مجموعةً من القيم المحتملة للعنصر. لو طبقت الأمر على اللعبة التي استخدمناها مثالًا في مقالات السلسلة، ستجد أنك تحتاج نوع التعداد عند تعريف وظائف شخصيات اللعبة Job، وأنواعهم Species، وأسلحتهم وما إلى ذلك؛ فلكل شخصية قيم محددة لا ينبغي أن يقبل النظام غيرها. يُعرّف نوع التعداد باستعمال الكلمة المفتاحية enum، وتكتب التعدادات ضمنه بالحروف الإنجليزية الكبيرة كما يلي: "The job class of the character." enum Job { FIGHTER WIZARD } "The species or ancestry of the character." enum Species { HUMAN ELF DWARF } تضمن بهذه الطريقة أن وظيفة الشخصية Job ستكون إما مقاتل FIGHTER أو ساحر WIZARD فقط، ولن يقبل الخادم أي كلمة أخرى مهما كانت؛ أما لو عرّفت وظيفة الشخصية على أنها سلسلة نصية عادية String، فستكون جميع الكلمات سيان عند الخادم وسيقبل أي مجموعة محارف تُعطى له. تفيدك التعدادات أيضًا في تمرير القيم الوسيطة، إذ يمكنك مثلًا تعريف تعداد enum لتحديد إذا كان سلاح الشخصية يُحمَل بيد واحدة مثل السيف أم أنه ثقيل مثل الفأس ويحتاج لكلتا اليدين، سيساعدك هذا التعداد على التحكم بتجهيز الشخصية بسلاح واحد أو بسلاحين، لنسمي التعداد اليد Hand، ونعرّفه بالتعليمات التالية: enum Hand { SINGLE DOUBLE } "A valiant weapon wielded by a fighter." type Weapon { name: String! attack: Int range: Int hand: Hand } type Query { weapons(hand: Hand = SINGLE): [Weapon] } صُرّح عن التعداد Hand بحالتين لاستعمال اليد، مفردة SINGLE، ومزدوجة DOUBLE، أما الوسيط المسند إلى الحقل weapons في الاستعلام فيحمل القيمة الافتراضية SINGLE، وهي القيمة التي يأخذها في حال لم تمنحه قيمة مغايرة. النوع غير الفارغ Non-Null قد تستغرب عدم وجود النوع null أو undefined بين أنواع GraphQL المفردة رغم أنه يُعدّ نوعًا شائعًا في معظم لغات البرمجة، ويرجع السبب في ذلك إلى أن جميع أنواع GraphQL تقبل القيمة Null افتراضيًا وفي حال أردت تغيير الأمر عليك استخدام إشارة التعجب مع اسم النوع. يُعرّف النوع غير الفارغ Non-Null في GraphQL بأنه مُعدِّل للأنواع الأخرى، فمثلًا الحقل String هو حقل اختياري قد يحمل قيمة أو لا، أما الحقل من النوع !String فهو إجباري أي لا يجب أن يكون فارغًا. نوع القائمة List نوع القائمة هو أيضًا من الأنواع المُعدِّلة للأنواع الأخرى، فأي نوع يوضع بين قوسين مربعين [] يتحول لقائمة. على سبيل المثال يُعرّف النوع [Int] قائمةً من نوع الأعداد الصحيحة Int والنوع [String] قائمةً من السلاسل النصية String وقِس على ذلك، ويمكنك استخدام القائمة مع إشارة التعجب لتحصل على نوع غير فارغ ومُعرّف على أنه قائمة مثل ![String]. نوع الكائن Object شبهنا الأنواع المفردة بأوراق الشجرة في بداية المقال، وتبعًا لهذا التشبيه تكون الكائنات هي الأغصان، وهي معظم ما سنكتبه في مخطط GraphQL. يبدأ تعريف الكائن بالكلمة المفتاحية type، ويحتوي كل كائن على حقل واحد على الأقل. تُسمى الحقول بالمفاتيح وبجانب كل حقل تجد نوع القيمة التي يقبلها. لا يجوز أن تبدأ أسماء الحقول برمز الشرطتين السفليتين __، فهو مخصص لنظام الاستقراء. لو أعطينا مثالًا في سياق أمثلتنا السابقة عن اللعبة، فيمكنك إنشاء كائن يمثل المقاتلين Fighter: "A hero with direct combat ability and strength." type Fighter { id: ID! name: String! level: Int active: Boolean! } يمتلك هذا الكائن أربعة حقول: id رقم تعريفي من نوع ID غير فارغ. name سلسلة نصية String غير فارغة. level عدد صحيح Int. active قيمة منطقية Boolean غير فارغة. ونود الإشارة هنا إلى قدرتك على كتابة تعليق قبل تعريف الكائن مباشرةً بين علامتي اقتباس، مثل: "A hero with direct combat ability and strength." وسيظهر تعليقك بمثابة الوصف للكائن. قد تكون حقول الكائن بيانات مفردة كما في مثالنا السابق أو كائنات أخرى. ألقِ نظرةً على المثال أدناه، إذ عرّفنا في البداية كائنًا من نوع السلاح Weapon يتضمن أسماء الأسلحة وطبيعة هجومها ومدى تأثيرها، ثم أضفنا السلاح weapon على أنه أحد حقول كائن المقاتل Fighter وهو weapon. يمكن لمخطط GraphQL أن يُضبط بحيث يعالج الحقل weapon الكائن Weapon: "A valiant weapon wielded by a fighter." type Weapon { name: String! attack: Int range: Int } "A hero with direct combat ability and strength." type Fighter { id: ID! name: String! level: Int active: Boolean! weapon: Weapon } يمكن أن تتداخل الكائنات ببعضها ويشترك أكثر من كائن بالحقل نفسه. أنواع عمليات الجذر عمليات الجذر هي أنواع خاصة من الكائنات تنطبق عليها جميع قواعد الكائنات، تمثل جذور مخطط GraphQL وتسمى نقاط دخول entrypoints، ولها ثلاثة أنواع هي الاستعلامات والطفرات والاشتراكات. تُعرّف هذه العمليات ضمن كائن الجذر schema في مخطط GraphQL، والمميز بالكلمة المفتاحية schema. ألقِ نظرةً على التعليمات التالية: schema { query: Query mutation: Mutation subscription: Subscription } لنبدأ بنوع الاستعلام Query type، وهو عملية أساسية لا غنى عنها في أي مخطط GraphQL، ويمثّل عملية قراءة، ويقابل الطلب GET في واجهة REST API. ألقِ نظرةً على تعريف استعلام الجذر Query للعبة نفسها، إذ يُرجِع هذا الاستعلام قائمة بالمقاتلين: type Query { fighters: [Fighter] } أما الطفرات فتمثل طلبات الكتابة لأنها تعدل على المخطط، فهي تشبه طلبات PUT و DELETE و POST في REST API. يبين المثال أدناه الطفرة Mutation التي تستخدم الوسيط input لإضافة مقاتل addFighter: type Mutation { addFighter(input: FighterInput): Fighter } عملية الجذر الأخيرة هي الاشتراكات Subscription، التي تفيدك في حالات تدفق الأحداث بين الخادم والعميل، وتعمل بتوافق تام مع تقنية WebSocket. بالعودة للعبة، يمكنك استخدام الاشتراكات لتمثيل الاصطدامات العشوائية في المعارك مثلًا على النحو التالي: type Subscription { randomBattle(enemy: Enemy): BattleResult } وسطاء الحقول Field Arguments حقول الكائن في GraphQL هي دوال تقليدية ترجِِع قيمًا وتقبل متغيراتٍ وسيطة مثلها مثل الدوال الأخرى، وتنتمي وسطاء الحقل إلى أي نوع غير فارغ من أنواع GraphQL باستثناء الكائنات، وتُعرّف بكتابة اسم المتغير يليه نوعه. ألقِ نظرةً على المثال التالي لترشيح الكائن Fighter باستخدام الحقل id، ولاحظ إشارة التعجب بعد النوع للدلالة على عدم قبول قيم ID الفارغة. type Query { fighter(id: ID!): Fighter } تلعب الوسطاء في هذا المثال دورًا في ترشيح كائن المقاتل Fighter بحسب الرقم التعريفي، فتمكّنك من جلب عنصر محدد من مخزن البيانات، وتستطيع استخدامها لأي استعلامات أخرى تريدها. نوع الواجهة Interface تشبه بنية نوع الواجهة بنية الكائنات، فهي تتكون من عدة حقول لكل منها اسم ونوع، والغاية منها وضع نموذج مشترك يمكن تطبيقه على مجموعة كائنات تمتلك حقولًا مشتركة. أنشأنا في الفقرات السابقة كائن المقاتل Fighter وهو يشترك في بعض حقوله مع كائنات أخرى تلعب أدوارًا في اللعبة، مثل المعالجين Healer والسحرة Wizard وغيرهم. يمكنك في هذه الحالة إنشاء واجهة تتضمن الحقول المشتركة بين جميع هذه الشخصيات، ثم إنشاء كائنات منفصلة لكل شخصية تكون تطبيقًا للواجهة. ألقِ نظرةً على المثال التالي لتوضيح الأمر. أنشئ في البداية واجهة تسمى BaseCharacter تبدأ بالكلمة المفتاحية interface وتتضمن كل الحقول المشتركة بين الشخصيات، كما يلي: "A hero on a quest." interface BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job } لكل شخصية في اللعبة رقم تعريفي خاص id واسم name ومستوى level ونوع species وعمل job. خذ مثلًا نوع المقاتل Fighter ونوع الساحر Wizard لدى كل منهم جميع الحقول الموجودة في الواجهة BaseCharacter مع بعض الاختلاف؛ فالمقاتل يستعمل الأسلحة Weapon؛ والساحر يستعمل التعويذات Spell. يمكنك إذًا تطبيق الواجهة لتسهيل تعريف النوعين، وذلك باستخدام الكلمة المفتاحية implements، وفق التالي: "A hero with direct combat ability and strength." type Fighter implements BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job! weapon: Weapon } "A hero with a variety of magical powers." type Wizard implements BaseCharacter { id: ID! name: String! level: Int! species: Species job: Job! spells: [Spell] } تذكر أن شرط تطبيق الواجهة على كائن معين هو امتلاك الكائن لجميع حقول الواجهة دون استثناء. نوع الاتحاد Union يمثل نوع الاتحاد مجموعة كائنات تصلح لأن تكون استجابة لاستعلام واحد. لننشئ مثلًا اتحادًا لشخصيات اللعبة Character تتضمن كافة كائنات الشخصيات، وهي في حالتنا كائنات المقاتل Fighter والساحر Wizard. union Character = Wizard | Fighter يبدأ تعريف الاتحاد بالكلمة المفتاحية Union، والتي تكون مدخلاتها إما كائنات أو واجهات فهي لا تقبل الأنواع المفردة. ترمز إشارة اليساوي = ضمن التعريف إلى إسناد القيمة، أما الخط الرأسي | فيكافئ عبارة OR المنطقية. الآن، إذا رغبت بالاستعلام عن شخصيات اللعبة فيمكنك استخدام الاتحاد Character ليستجيب الخادم بقائمة تضم جميع الكائنات من نوع مقاتل Fighter أو ساحر Wizard. الخاتمة تعرّفنا في هذا المقال على أنواع GraphQL المختلفة، شبهنا المخطط بالشجرة فكانت الأنواع المفردة بمثابة الأوراق التي ينتهي إليها كل شيء، فهي أنواعٌ تساهم بتشكيل الأنواع الأخرى، ولها خمسة أنواع أساسية هي Int و Float و String و Boolean و ID ويمكنك إنشاء أنواعك الخاصة، ثم عرضنا نوع التعداد الذي يمنحك تحكمًا أكبر في استجابة الخادم فتزوده بمجموعة محددة من القيم الصالحة، وبعدها اطلعنا على مُعدِّلات الأنواع أي القائمة والقيم غير الفارغة التي تُغيّر طبيعة الأنواع الأخرى، وتوسعنا في الكائنات لأنها عماد مخططات GraphQL فهي أشبه بأغصان الشجرة بما فيها من كائنات جوهرية تتمثل في عمليات الجذر أي الاستعلامات والطفرات والاشتراكات، ولا ننسَ الواجهات والروابط التي تُسهّل علينا التعامل مع الكائنات ذات الخصائص المشتركة. لديك الآن أساسٌ نظري جيد ننصحك ببناء خادمك الخاص بمساعدة مقال إعداد خادم GraphQL في بيئة Node.js والتدرب عمليًّا على تطبيق الأنواع لحين إتقانها. ترجمة -وبتصرف- للمقال Understanding the GraphQL Type System لصاحبته Tania Rascia. اقرأ أيضًا المقال السابق إعداد خادم GraphQL في بيئة Node.js دليلك الشامل إلى أنواع البيانات مقدمة إلى Node.js كتابة أول برنامج في بيئة Node.js وتنفيذه مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة النسخة الكاملة لكتاب البرمجة باستخدام Node.js
  4. GraphQL لغة استعلام مفتوحة المصدر لواجهات برمجة التطبيقات API وبيئة تشغيل لتنفيذ الاستعلامات، وأُنشِئَت في الأساس لمعالجة مشكلات REST التقليدية وقد عرضنا بعضًا منها، وسنطّلع في هذا المقال على آلية تفاعل مكونات GraphQL مع بعضها، عبر بناء خادم GraphQL API في بيئة Node.js، وسيُستخدم خادم Express API بدلًا من Apollo GraphQL رغم أنه الأشهر بين تطبيقات GraphQL. ستتعلم خلال متابعتك لخطوات العمل إنشاء مخطط GraphQL متوافق مع نظام الأنواع بما فيه من عمليات، مثل الاستعلامات والطفرات ومحللات الاستجابة، وستتآلف مع بيئة التطوير المتكاملة GraphiQL، إذ سنستعملها لاستعراض المخطط وتصحيح أخطائه، ولطلب استعلامات العميل من واجهة GraphQL. متطلبات العمل جهّز المتطلبات التالية لتتابع معنا سير العمل: بيئة Node.js جاهزة على خادمك المحلي، ويمكنك الاسترشاد بأحد المقالين تثبيت Node.js على نظام أبونتو 18.04 أو كيفية تثبيت Node.js على Debian 8 لإعدادها. فهم أساسيات GraphQL. الإلمام ببروتوكول HTTP، ننصحك بقراءة مدخل إلى HTTP. معرفة أساسية بكل من HTML و JavaScript، تفيدك هذه المقالات أساسيات إنشاء موقع ويب باستخدام تعليمات HTML، وأساسيات لغة جافاسكربت. إعداد خادم Express HTTP يبدأ إعداد خادم Express بتثبيت الحزمتين express و cors، باستخدام الأمر npm install ضمن مشروع جديد وفق التالي: npm install express cors Express هو إطار لتطبيقات الويب المبنية على Node.js، وهو مصمم خصيصًا لبناء واجهات برمجة التطبيقات، وسيكون إطار العمل الخاص بخادمنا، أما الحزمة CORS وهي اختصار لمصطلح البرمجيات الوسيطة لمشاركة الموارد ذات الأصل المختلط Cross-Origin Resource Sharing، إذ إن الغاية منها هي السماح لمتصفحك بالوصول إلى موارد الخادم. ثبّت أيضًا الاعتمادية Nodemon مع الراية D بصفتها اعتمادية تطوير، لتستخدمها في مرحلة التطوير فقط: npm install -D nodemon Nodemon هي أداة مساعدة في تطوير تطبيقات Node، تعيد تشغيل التطبيق في كل مرة يطرأ فيها تغيير على ملفات المشروع الموجودة في مجلد الجذر لتأخذ التعديلات مفعولها. سينشأ بتثبيت هذه الحزم المجلد node_modules والملف package.json مع اعتماديتي إنتاج واعتمادية تطوير. إذا فتحت الملف package.json بمحرر نصوص مثل نانو nano ستجده يحتوي المعلومات التالية: { "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" } } عدّل عليه ليصبح وفق التالي: { "main": "server.js", "scripts": { "dev": "nodemon server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" }, "type": "module" } لنشرح التعديلات السابقة، إذ وضعنا ملف الخادم "server.js" -الذي سننشئه بعد قليل- في الحقل main، وبهذا تتأكد من تشغيل npm start للخادم؛ أما الملف النصي المسمى "dev"، فيُسهل التطوير إذ يتضمن أمر تشغيل nodemon server.js. أضفنا في السطر الأخير القيمة module لحقل النوع type لنُفعّل إمكانية استخدام تعليمة الاستيراد import عوضًا عن require -تعليمة CommonJS الافتراضية- وفي هذا أيضًا تسهيلٌ للعمل. احفظ التغييرات، وأغلق الملف، لننتقل لإنشاء ملف الخادم. سنبني الآن خادم Express بسيط عبر إنشاء ملف نصي باسم "server.js"، يتضمن التعليمات المبينة أدناه، يعمل هذا الخادم على المنفذ 4000، وتظهر في صفحته الأولى عبارة الترحيب !Hello, GraphQL: import express from 'express' import cors from 'cors' const app = express() const port = 4000 app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.get('/', (request, response) => { response.send('Hello, GraphQL!') }) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) }) لاحظ أن الملف يبدأ بتعليمة استيراد الدالة express، ثم بعض الإعدادات الخاصة بكل من CORS و JSON، وبعدها استعملنا ('/')app.get لتحديد ما ينبغي إرساله إلى الجذر / ضمن طلبات GET، وفي نهاية الملف استخدمنا ()app.listen لتحديد منفذ خادم API. احفظ الآن التغيرات، وأغلق الملف، ثم نفذ الأمر التالي لتشغيل خادم Node: npm run dev تأكد -قبل الانتقال للخطوات التالية- من سلامة تشغيل الخادم بظهور صفحة الترحيب !Hello, GraphQL عند فتح الرابط "http://localhost:4000" من المتصفح أو تشغيل الأمر: curl http://localhost:4000 إعداد البرمجية الوسيطة لخادم GraphQL HTTP يوجد ثلاث خطوات أساسية لتحقيق التكامل بين مخطط GraphQL مع خادم إكسبريس، وتتلخص في إعداد الاتصال مع مخزن البيانات، وتعريف المخطط، وتعريف محللات الاستجابة. ستحتاج في البداية لتنفيذ الأمر التالي: npm install graphql@14 express-graphql @graphql-tools/schema الذي يثبت الحزم الضرورية للعمل مع GraphQL، وهي: graphql: الحزمة المرجعية التي تستخدمها لغة جافا سكربت لجعل التعامل مع GraphQL متاحًا في اللغة. express-graphql: البرمجية الوسيطة لخادم HTTP للتعامل مع GraphQL. graphql-tools/schema@: مجموعة أدوات تُسرّع تطوير GraphQL. ضِف تعليمات استيراد هذه الحزم إلى الملف "server.js"، لتصبح بدايته على النحو التالي: import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' ... لننجز الآن إعدادات الاتصال مع مخزن البيانات قبل إنشاء مخطط GraphQL القابل للتنفيذ. أنشئ الكائن data بإضافة التعليمات الخاصة به إلى الملف "server.js" مباشرةً بعد تعليمات الاستيراد كما هو مبين أدناه، إذ يمثل هذا الكائن مخزنًا للبيانات في الذاكرة in-memory store، ويحتوي مجموعة بيانات بسيطة لنستعلم عنها من خادم GraphQL. نخفف بهذه الطريقة عبء إنشاء قاعدة بيانات حقيقية والاتصال معها كوننا نجهز بيئة تعليمة بسيطة. import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } … تمثّل هيكلة البيانات جدولًا بسيطًا يدعى warriors، الذي يحتوي سطرين 'Jaime' و 'Jorah' لبيانات اثنين من محاربي لعبة افتراضية سنستخدمها في مثالنا. ملاحظة: لا يغطي هذا المقال حالة اتصال خادم GraphQL بقاعدة بيانات حقيقية، وعادةً ما يستخدم خادم GraphQL دوال الاختزال reducers للوصول إلى قواعد البيانات والتعديل عليها، ويعتمد تقنياتٍ مثل Prisma، وهي أداة ربط العلاقات بالكائنات ORM، أما إعدادات الاتصال مع قاعدة البيانات فيحملها المعامل context أحد معاملات محللات الاستجابة غير المتزامنة. سيمثل المتغير data إعدادات اتصالنا مع مخزن البيانات حتى نهاية هذا المقال. الآن، بعد تثبيت الحزم المطلوبة، وتجهيز بعض البيانات للاستعلام، يمكننا إنشاء المخطط الذي يُعرّف واجهة API، ويصف ماهية البيانات الممكن الاستعلام عنها. مخطط GraphQL سننشئ مخططًا أوليًّا لواجهة API يتضمن الحد الأدنى من التعليمات البرمجية التي تسمح لنا بالاتصال مع نقطة الوصول GraphQL Endpoint. سنفترض في هذا المثال أننا نستعلم عن بيانات لعبة مغامرات فيها أدوار لشخصيات مختلفة، مثل المحاربين والسحرة والمعالجين وغيرهم، لذا سنحرص عند إنشاء المخطط أن يكون مرنًا وقابلًا لإعادة الاستخدام، لتستطيع تكراره مع أي نوع من شخصيات اللعبة وتوسعته ليشمل متغيرات أخرى تخصهم مثل الأسلحة والتعويذات. تستند مخططات GraphQL إلى نظام الأنواع، ويمكنك دائمًا إنشاء أنواعك الخاصة إذا لم تلبِ الأنواع الافتراضية المُضمّنة في النظام حاجتك، وهذا ما سنفعله هنا إذ سننشئ نوعًا type يدعى المحارب warrior له حقلين id و name. type Warrior { id: ID! name: String! } الحقل id نوعه ID رقم تعريفي، والحقل name نوعه سلسلة نصية String، وكلاهما من الأنواع الأولية المُضمّنة في نظام الأنواع، والأنواع الأولية تستخدم لإنشاء أنواع أعقد مثل الكائنات، أما إشارة التعجب ! الواردة في التعليمات أعلاه فتشير لكون الحقل بجانبها لا يقبل قيمة فارغة "Null"، فلا يجب أن يكون فارغًا بأية حال. الآن بعد إنشاء نوع المحارب، سننشئ نوع الاستعلام Query، وهو نقطة الدخول entry point إلى استعلام GraphQL. ألقِ نظرةً على التعليمات المبينة أدناه، إذ يُعرّف هذا النوع المحاربين warriors على أنهم مصفوفة من البيانات التابعة لنوع المحارب Warrior. type Query { warriors: [Warrior] } أصبح مخططنا جاهزًا للاستخدام مع البرمجية الوسيطة GraphQL HTTP، لذا سنمرره للدالة makeExecutableSchema، إحدى أدوات graphql-tools، بصفته typeDefs ليصبح قابلًا للتنفيذ. للدالة makeExecutableSchema معاملان أحدهما للمخطط والآخر لمحللات الاستجابة، وينبغي تعريفها قبل تنفيذ الدالة: typeDefs: لغة مخطط GraphQL، وهي سلسلة نصية. resolvers: محللات الاستجابة، وهي الدوال التي تُستدعى لتنفيذ حقل معين وإرجاع قيمة. اكتب تعليمات تعريف typeDefs ضمن الملف "server.js" تمامًا بعد تعليمات استيراد الاعتماديات، وأسند له قيمة نصية هي مخطط GraphQL، وفق التالي: ... const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` ... إذًا، فقد جهزّنا إلى الآن البيانات التي سنستعلم عنها data، وعرّفنا المخطط ضمن typeDefs، وسنعرّف في الخطوة التالية محللات الاستجابة، التي ستحدد طريقة استجابة خادمنا للطلبات الواردة. محللات الاستجابة محللات الاستجابة Resolvers هي مجموعة من الدوال البرمجية التي توّلد استجابة خادم GraphQL، ولكل محلل استجابة أربعة معاملات: obj: الكائن الأب، غير ضروري في حالتنا لأن الكائن المستخدم لدينا وحيد وهو الأب أو الجذر. args: تمرر هنا أي متغيرات وسيطة يحتاجها GraphQL. context: حقل مشترك بين جميع محللات الاستجابة، ويُخصص غالبًا لمعلومات الاتصال مع قاعدة البيانات. info: معلومات إضافية. سننشئ الآن محلل استجابة لاستعلام الجذر Query، يُرجع لطالب الاستعلام معلوماتٍ عن المحاربين warriors، وسنمرّر له مخزن البيانات في الذاكرة الذي عرّفناه سابقًا. ضِف ما يلي إلى الملف "server.js": ... const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } ... أنشأنا بهذه التعليمات دالة محلل استجابة تدعى warriors، ترتبط بالاستعلام Query وهو نقطة دخولنا إلى خادم GraphQL، ومهمة هذه الدالة هي إرجاع بيانات المحاربين من جدول المحاربين warriors الموجود في قاعدة البيانات التي يشير إليها المعامل context، وهي في حالتنا مخزن البيانات البسيط data. إذًا، فلكل دالة محلل استجابة أربعة معاملات: obj و args و info، إضافةً إلى context، الذي كان أكثرها ارتباطًا بمخططنا، وهذا المعامل مشترك بين جميع محللات الاستجابة، ويشير غالبًا إلى إعدادات الاتصال بين خادم GraphQL وقاعدة البيانات. الآن، بعد أن عرّفنا typeDefs و resolvers معاملات الدالة makeExecutableSchema، يمكننا تحويل المخطط إلى مخطط قابل للتنفيذ بكتابة الأوامر التالية في الملف "server.js": ... const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) ... تنشئ الدالة makeExecutableSchema مخططًا كاملًا يمكنك تمريره إلى نقطة وصل GraphQL. لنستبدل نقطة الوصل الحالية أي عبارة الترحيب !Hello, GraphQL بنقطة الوصول graphql/ عبر كتابة التعليمات التالية: ... const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) ... يتطلب استخدام البرمجية الوسيطة graphqlHTTP مخططًا أنشأناه وسياقًا يمثّل معلومات الاتصال مع مخزن البيانات، وينص الاتفاق على استخدام خادم GraphQL نقطة الوصول graphql/. إليك المحتوى النهائي للملف "server.js" بعد جميع التعديلات: import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const app = express() const port = 4000 // مخزن البيانات في الذاكرة const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } // المخطط const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` // محلل الاستجابة للمحاربين const resolvers = { Query: { warriors: (obj, args, context) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) // نقطة الدخول app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) }) احفظ التغييرات على الملف وأغلقه. تكتمل بذلك واجهة برمجة التطبيقات، يمكنك الآن فتح الرابط "http://localhost:4000/graphql"، واستعراض مخطط GraphQL وتصحيح أخطائه باستخدام بيئة التطوير المتكاملة GraphiQL IDE، وهو موضوع القسم التالي من المقال. استخدام GraphiQL IDE الخطوة الأولى لاستخدام بيئة التطوير GraphiQL IDE هي تفعيلها بإسناد القيمة true للخيار graphiql في كتلة التعليمات الخاصة بالبرمجية الوسيطة ضمن ملف الخادم، ولو رجعت للتعليمات السابقة ستجدها مُفعلة. تعمل GraphiQL ضمن المتصفح، وتفيدك في كتابة استعلامات GraphQL، والتحقق من صحتها، واختبارها لضمان خرج صحيح من الخادم. تنقسم شاشة GraphiQL إلى قسمين، القسم الأيسر لكتابة التعليمات والأيمن لعرض النتائج. سنجري الآن استعلامًا عن المحاربين warriors، لذلك اطلب المُعرّف id والاسم name لكل محارب بكتابة التعليمات التالية في القسم الأيسر من الشاشة: { warriors { id name } } اضغط بعدها على الزر التشغيل أعلى يسار الشاشة، وستظهر نتيجة استعلامك في القسم الأيمن بصيغة JSON، وفق التالي: Output{ "data": { "warriors": [ { "id": "001", "name": "Jaime" }, { "id": "002", "name": "Jorah" } ] } } حاول تعديل الاستعلام، استعلم مثلًا عن الاسم name فقط: { warriors { name } } ولاحظ تغير النتيجة تبعًا لاستعلامك، فهي تشمل الآن أسماء المحاربين دون أرقامهم: Output{ "data": { "warriors": [{ "name": "Jaime" }, { "name": "Jorah" }] } } تعكس هذه التجربة واحدةً من أقوى خصائص GraphQL، وهي أنها موجهة بطلبات العميل [Client-driven]()، إذ تسمح له بالاستعلام عن الحقول التي يحتاجها فقط، وتعرضها هي بالذات دون زيادة أو نقصان. تفيدك GraphiQL أيضًا باستعراض تفاصيل المخطط، فكل ما تحتاجه هو الضغط على زر التوثيق Docs في يمين الشاشة، وسيظهر لك شريط جانبي خاص بالتوثيقات. لننتقل إلى القسم الأخير من مقالنا، وفيه محاكاة لتقديم طلبات استعلام فعلية من العميل إلى واجهة GraphQL API. إرسال طلب استعلام لواجهة GraphQL API من طرف العميل يمكنك إرسال طلبات الاستعلام إلى واجهة GraphQL عبر بروتوكول HTTP بالطريقة نفسها المتبعة مع واجهات REST، وتستطيع أيضًا استعمال واجهات APIs المُضمّنة في المتصفحات مع GraphQL، ومن أمثلتها fetch الخاصة بجلب البيانات. افتح الملف "index.html"، وأنشئ بداخله هيكل HTML مع الوسم <pre> كما يلي: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL Client</title> </head> <pre><!-- البيانات ستعرض هنا --></pre> <body> <script> // Add query here </script> </body> </html> ثم أنشئ دالة غير متزامنة تحت الوسم script، ترسل طلب POST لواجهة GraphQL API: ... <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() // إضافة البيانات إلى الوسم const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // JSON بصيغة } queryGraphQLServer() </script> </body> ... أسند القيمة application/json للحقل Content-Type في ترويسة المقطع script، واكتب الاستعلام الذي تطلبه بصيغة سلسلة نصية ضمن المتن، فيكون تسلسل الإجراءات كما يلي: يستدعي script الدالة غير المتزامنة، فترسل طلبًا للخادم، وعند ورود الاستجابة توضع تحت الوسم pre. إليك المحتوى النهائي للملف "index.html": <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL</title> </head> <pre></pre> <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // Pretty-print the JSON } queryGraphQLServer() </script> </body> </html> احفظ التغييرات، وأغلق الملف "index.html"، ثم استعرضه بواسطة المتصفح، وافتح نافذة الشبكة Network ضمن أدوات المطور لترى الطلب المرسل عبر الشبكة من المتصفح إلى نقطة وصول GraphQL ذات العنوان "http://localhost:4000/graphql"، وافحص بعدها استجابة الخادم؛ فإذا تضمنت البيانات التي استعلمت عنها مع رمز الاستجابة 200، فاستعلامك صحيح وقد نجحت بإنشاء خادم GraphQL API. الخاتمة تناول هذا المقال إعداد خادم GraphQL API في بيئة Node.js باستخدام إطار العمل Express بطريقة مبسطة تعينك على فهم أساسيات GraphQL، وأبرزها أن الخادم يتكون من نقطة وصول وحيدة graphql/ تستقبل جميع طلبات الاستعلام، وواجهة برمجة التطبيقات تحتوي دومًا مخطط ومحلل استجابة. يتضمن المخطط نوعين، نوعًا أساسيًا للاستعلام Query، ونوعًا مخصصًا تُعرّفه حسب حالتك، وفي مثالنا كان نوع المحارب Warrior، ومهمة محلل الاستجابة جلب البيانات المناسبة لهذه الأنواع من مخزن البيانات. نأمل أننا ساعدناك في خطوتك الأولى مع GraphQL، وننصحك بتوسيع بحثك عنها لتتعرف على خياراتها المتقدمة ومكملاتها، مثل أدوات المصادقة والأمان والتخبئة caching، لتزيد فعاليتها في تطبيقاتك. ترجمة -وبتصرف- للمقال How To Set Up a GraphQL API Server in Node.js لصاحبته Tania Rascia. اقرأ أيضًا المقال السابق: مقدمة إلى GraphQL مقدمة إلى Node.js كتابة أول برنامج في بيئة Node.js وتنفيذه مدخل إلى المكتبة GraphQL واستعمالاتها في بناء تطبيقات الويب الحديثة النسخة الكاملة لكتاب البرمجة باستخدام Node.js
  5. جانغو هو أشهر أطر العمل البرمجية المكتوبة بلغة بايثون، إذ يساعدك هذا الإطار المميز على تطوير تطبيقات الويب ووضعها في الخدمة بسرعة ويُسر، ويحتوي جانغو خادم تطوير مُضمّن يتيح لك اختبار الكود محليًا والتأكد من عمله إلّا أنه مبسط يلبي أغراض التجربة فقط، فأي تطبيق يُطلق في بيئة العمل الفعلية مهما بلغت بساطته يتطلب خادم ويب بمواصفات أعلى وحماية أكبر. سنُحاكي في هذا المقال بيئة عمل كاملة لتطبيق جانغو مبنية على خادم أوبونتو إصدار 20.04، ونعرض كيفية تثبيت وإعداد المكونات اللازمة لعمله على الخادم، بدءًا من بناء قاعدة بيانات PostgreSQL بدلًا من SQLite (قاعدة بيانات جانغو الافتراضية)، ومن ثم إعداد خادم التطبيق من نوع Gunicorn، وصولًا إلى تهيئة خادم وكيل عكسي من نوع Nginx يعمل أمام Gunicorn ويقدم لتطبيقنا كل ما يملكه من مميزات تدعم أمانه وأدائه. متطلبات بيئة العمل والأهداف ستحتاج إلى خادم بنظام تشغيل أوبونتو وقد استعملنا في هذا المقال الإصدار 20.04، مع مستخدم –ليس مسؤول- ولكنه يتمتع بصلاحيات عالية sudo، بالإضافة إلى تفعيل جدار الحماية الافتراضي على الخادم، ويمكنك الاستعانة بمقال التهيئة الأولية لخادم أوبونتو. سنثبت جانغو في بيئةٍ افتراضية لما تقدمه من مرونة تتيح لنا التعامل مع التطبيق ومتطلباته الخارجية بطريقة منفصلة أي كل على حدة، وسنبدأ بإعداد قاعدة البيانات وبمجرد الانتهاء منها وتشغيل التطبيق، سنثبت Gunicorn ليتعامل مع طلبات العملاء الواردة عبر HTTP ويحولها إلى أوامر بايثون يعالجها التطبيق، ومن ثم سنشغل خادم وسيط عكسي هو Nginx أمام Gunicorn للاستفادة من قدراته العالية في التعامل الفعال والآمن مع حركة البيانات الواردة إلى التطبيق. لنبدأ التطبيق العملي! تثبيت الحزم اللازمة من مستودعات أوبونتو سنُنثبت معظم المكونات اللازمة للعمل من مستودعات أوبونتو، ومن ثم سنستخدم pip مدير حزم بايثون لتثبيت مكونات أخرى خاصة بالتطبيق. البداية دومًا مع تحديث apt دليل الحزم المحلي لأوبونتو، ومن بعدها سنبدأ بتحميل الحزم اللازمة وتثبيتها بما يتوافق مع إصدار بايثون الذي تستخدمه فالأوامر تختلف قليلًا بين بايثون 2 و 3، علمًا أن جانغو 1.11 هو أحدث إصدار من الإصدارات التي تدعم بايثون 2، لذا ننصحك بترقية إطار العمل الذي تعتمده واستخدام بايثون 3 إن كنت تنشئ مشروعًا جديدًا. إن كنت تستخدم بايثون 3 اكتب الأوامر: sudo apt update sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl أما أوامر بايثون 2 فهي: sudo apt update sudo apt install python-pip python-dev libpq-dev postgresql postgresql-contrib nginx curl ثبتنا بموجب الأوامر السابقة pip وملفات تطوير جانغو اللازمة لبناء Gunicorn لاحقًا و نظام قواعد البيانات Postgres مع المكاتب اللازمة للتفاعل معه وأخيرًا خادم الويب Nginx. إنشاء قاعدة البيانات PostgreSQL ومستخدم التطبيق يمتلك نظام قواعد بيانات Postgres ميزةً افتراضية تسمى توثيق النِّدّ Peer Authentication وهي خاصة بمصادقة الاتصالات المحلية، وتعني أن وجود مستخدم في نظام التشغيل يطابق اسمه اسم مستخدم Postgres فعال، يمنح هذا المستخدم -أي مستخدم نظام التشغيل- القدرة على الدخول إلى Postgres دون الحاجة إلى توثيق. وقد أُنشئ مستخدم لنظام التشغيل اسمه postgres ليتوافق مع مستخدم postgres المدير لنظام PostgreSQL، وسنستفيد من هذا المستخدم للمهام الإدارية، مع إمكانية استخدام sudo وتمرير اسم المستخدم بعد الراية u-. سجل الدخول إلى Postgres عبر الأمر التالي: sudo -u postgres psql ستظهر لك نافذة أوامر PostgreSQL، اكتب فيها تعليمة إنشاء قاعدة بيانات المشروع: CREATE DATABASE myproject; تنويه: تذكر أن تنهي كافة أوامر Postgres بفاصلة منقوطة لتضمن التنفيذ السليم. أنشئ بعدها مستخدم قاعدة البيانات واختر له كلمة سر قوية، وفق الأمر التالي: CREATE USER myprojectuser WITH PASSWORD 'password'; اضبط الآن القيم الافتراضية لمحددات اتصال المستخدم المنشأ بالتوافق مع توصيات جانغو بحيث لا يتطلب الأمر الاستعلام عنها وإعادة تعيينها مع كل تأسيس جديد للاتصال ما سينعكس إيجابًا على تسريع عمليات قاعدة البيانات، والمحددات المطلوب ضبطها هي: الترميز encoding الذي يأخذ القيمة الافتراضية UTF-8، وأسلوب عزل العملية transaction isolation scheme الذي يوضع "read committed" ومهمته منع قراءة العمليات غير المكتملة uncommitted transactions، وأخيرًا المنطقة الزمنية نثبتها على UTC، وفق التعليمات التالية: ALTER ROLE myprojectuser SET client_encoding TO 'utf8'; ALTER ROLE myprojectuser SET default_transaction_isolation TO 'read committed'; ALTER ROLE myprojectuser SET timezone TO 'UTC'; وأخيرًا امنح مستخدم قاعدة البيانات المنشأ صلاحيات الإدارة الكاملة لقاعدة البيانات: GRANT ALL PRIVILEGES ON DATABASE myproject TO myprojectuser; يمكنك الآن الخروج من موجه أوامر PostgreSQL بكتابة الأمر: \q إنشاء بيئة بايثون الافتراضية بعد أن جهزنا قاعدة بيانات التطبيق سننتقل الآن لتثبيت كافة متطلبات بايثون اللازمة له ضمن بيئة افتراضية سهلة الإدارة، ولكن أولًا علينا تثبيت virtualenv بعد تحديث pip، ستختلف أوامر التثبيت اختلافًا بسيطًا بين بايثون 2 وبايثون 3. إن كنت تستخدم بايثون 3 اكتب الأوامر التالية: sudo -H pip3 install --upgrade pip sudo -H pip3 install virtualenv أما لبايثون 2 فاكتب: sudo -H pip install --upgrade pip sudo -H pip install virtualenv أنشئ مجلدًا باسم مشروعك، ثم انتقل إلى داخله: mkdir ~/myprojectdir cd ~/myprojectdir وأنشئ البيئة الافتراضية ضمنه بكتابة الأمر التالي: virtualenv myprojectenv بموجب الأوامر السابقة، سيُنشَأ ضمن مجلد مشروعك myprojectdir مجلدًا فرعيًا للبيئة الافتراضية اسمه myprojectenv، وسيُثبت بداخله نسخة محلية من بايثون ومدير الحزم pip محلي. تؤمن هذه البيئة الافتراضية بيئة بايثون معزولة لمشروعنا ولكنها تحتاج لتفعيل قبل البدء باستخدامها وتنزيل ما يلزم ضمنها، والتفعيل وفق الأمر التالي: source myprojectenv/bin/activate سيتغير بعدها محث سطر الأوامر ليشير إلى أنك تعمل في البيئة الافتراضية وسيبدو مثل: (myprojectenv)user@host:~/myprojectdir$ يمكنك الآن باستخدام pip تثبيت جانغو و Gunicorn و psycopg2 الخاص بالتخاطب مع قاعدة البيانات التي بنيناها: تنويه: يُستخدم pip دومًا داخل البيئة الافتراضية وليس pip3 بغض النظر عن إصدار بايثون المعتمد. pip install django gunicorn psycopg2-binary يفترض الآن تثبيت كل المتطلبات اللازمة لبدء مشروع جانغو Django جديد. إنشاء وإعداد مشروع جانغو جديد يمكننا الآن إنشاء ملفات المشروع بعد أن أعددنا متطلبات بايثون اللازمة له ضمن البيئة الافتراضية. إنشاء مشروع جانغو Django لدينا بالفعل مجلدًا لمشروعنا هو myprojectdir ولذا سنكتبه في أمر إنشاء المشروع حتى يثبت جانغو الملفات ضمنه بدلًا من اختياره مجلدًا آخر تلقائيًا، وعند تنفيذ الأمر سيُنشئ مجلدًا فرعيًا ضمن مجلد المشروع يحمل نفس الاسم ليحتوي على الشيفرة البرمجية أو كود التطبيق ويضاف فيه أيضًا السكربت الخاص بالإدارة: django-admin startproject myproject ~/myprojectdir وبناءً على ذلك سيتضمن مجلد المشروع (وهو في حالتنا myprojectdir/~) العناصر التالية: myprojectdir/manage.py/~: سكربت الإدارة الخاص بجانغو. /myprojectdir/myproject/~: حزمة مشروع جانغو، ويجب أن تتضمن الملفات التالية: ‎init__.py__ settings.py urls.py asgi.py wsgi.py /myprojectdir/myprojectenv/~: مجلد البيئة الافتراضية المنشأة في الخطوة السابقة. ضبط إعدادات مشروع جانغو سنعمل في هذه الفقرة على ثلاث إعدادات هي قائمة العناوين وأسماء النطاقات المسموح باستخدامها للاتصال مع جانغو، وقاعدة البيانات المعتمدة، ومسار ملفات التطبيق الساكنة. افتح ملف الإعدادات باستعمال محرر النصوص لنبدأ بهذه التعديلات بالترتيب: nano ~/myprojectdir/myproject/settings.py ابحث عن المحدد ALLOWED_HOSTS إذ سنبدأ تعديلاتنا به، ويحميك هذا المحدد من الاتصالات غير المشروعة، ووظيفته تعريف الخوادم المسموح باستخدامها للاتصال بالتطبيق، وتُعرف باستخدام عنوان IP الخادم أو اسم النطاق أو اسم النطاق الفرعي، وأي طلب يرد للتطبيق يحمل في ترويسته اسم مضيف غير معرف ضمن هذا المحدد فسيُمنع. تُكتب قائمة عناوين IP أو أسماء النطاقات المسموح لها بالاتصال ضمن الأقواس المربعة، وكل عنوان أو اسم منها ضمن اشارتي اقتباس مفردتين، ويفصل بين كل قيمة وأخرى فاصلة، ولو رغبت بالإشارة إلى النطاق الرئيس وكل ما يحوي من نطاقات فرعية فاستخدم النقطة قبل اسم النطاق، انظر الأمثلة التالية لمزيد من الإيضاح: . . . # The simplest case: just add the domain name(s) and IP addresses of your Django server # ALLOWED_HOSTS = [ 'example.com', '203.0.113.5'] # To respond to 'example.com' and any subdomains, start the domain with a dot # ALLOWED_HOSTS = ['.example.com', '203.0.113.5'] ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . ., 'localhost'] تنويه: تأكد من إضافة الخادم المحلي localhost على قائمة الأجهزة المسموح لها بالاتصال إذ إننا سننشئ عليه وكيل محلي Nginx. المحدد التالي للضبط هو DATABASES الخاص بقواعد البيانات، ابحث عنه ضمن ملف الإعدادات وسنُعدّله ليناسب قاعدة بيانات PostgreSQL التي أعددناها بدلًا من SQLite قاعدة البيانات الافتراضية لجانغو. تتمثل تعديلاتنا عليه في جعل جانغو يستخدم المحول psycopg2 (سبق أن ثبتناه باستخدام pip) ليناسب التخاطب مع قاعدة البيانات الجديدة PostgreSQL، ومن ثم كتابة اسم قاعدة البيانات الصحيح واسم مستخدم قاعدة البيانات وكلمة المرور الخاصة به، وأيضًا تحديد مضيف قاعدة البيانات وهو في حالتنا الخادم المحلي، أما بالنسبة لبوابة الاتصال مع قاعدة البيانات port فيمكنك تركها فارغة، انظر التعليمات أدناه: . . . DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME': 'myproject', 'USER': 'myprojectuser', 'PASSWORD': 'password', 'HOST': 'localhost', 'PORT': '', } } . . . أما التعديل الأخير على الملف settings.py فهو إضافة مسار ملفات التطبيق الساكنة، وهذا التعديل ضروري ليتمكن Nginx من معرفة مكان هذه الملفات والتعامل مع الطلبات الواردة بخصوصها، انتقل الآن إلى نهاية الملف واكتب الأسطر التالية التي ستحدد مكان وجود هذه الملفات ويُشار إليه بـ static: . . . STATIC_URL = '/static/' import os STATIC_ROOT = os.path.join(BASE_DIR, 'static/') والآن احفظ الملف وأغلقه. إكمال الإعداد الأولي للمشروع بعد أن ضبطنا إعدادات قاعدة البيانات يمكننا الآن تهجير مخطط قاعدة البيانات الأولي إلى قاعدة بيانات PostgreSQL المعتمدة في مشروعنا وذلك باستخدام سكريبت الإدارة: ~/myprojectdir/manage.py makemigrations ~/myprojectdir/manage.py migrate ومن بعدها سننشئ حساب المستخدم المسؤول عن إدارة التطبيق بكتابة الأمر التالي: ~/myprojectdir/manage.py createsuperuser وسيُطلب منك النظام إدخال اسم مستخدم لهذا الحساب وكلمة مرور وعنوان بريد إلكتروني. أما الآن سننفذ عملية جمع المكونات الساكنة للتطبيق static assets في المسار الذي حددناه في الفقرة السابقة وذلك وفق الأمر التالي: ~/myprojectdir/manage.py collectstatic سيُطلب منك تأكيد العملية، وبعدها ستُجمع الملفات الساكنة ضمن مجلد مشروعك في المكان الذي أطلقنا عليه اسم static. كما ورد في متطلبات بيئة العمل في بداية المقال فإن الخادم أوبونتو يجب أن يتضمن جدار ناري فعال هو الجدار الناري الأساسي للينكس UFW (يسمى الجدار الناري غير المعقد)، ولذا يتحتم علينا فتح البوابة التي سنستخدمها في هذا الجدار (وهي البوابة 8000) حتى نتمكن من الاتصال مع خادم تطوير جانغو واختباره. يتم فتح البوابة بالأمر التالي: sudo ufw allow 8000 والآن يمكنك اختبار تطبيقك بتشغيل خادم التطوير عبر الأمر التالي: ~/myprojectdir/manage.py runserver 0.0.0.0:8000 استعرض التطبيق بكتابة عنوان IP أو اسم النطاق مع البوابة 8000: ضمن متصفحك: http://server_domain_or_IP:8000 وستحصل على صفحة الدليل index الافتراضية لجانغو: ولو أضفت admin/ في نهاية الرابط، سترى شاشة تسجيل الدخول إلى لوحة التحكم: أدخل اسم المستخدم وكلمة المرور الذي أنشأته عبر استخدام الأمر createsuperuser فتدخل إلى لوحة التحكم: أوقف خادم التطوير بالضغط على ctrl-c داخل شاشة الطرفية حالما تنتهي من اختباراتك. التحقق من قدرة Gunicorn على تخديم المشروع التحقق من قدرة Gunicorn على تخديم المشروع هو الأمر الأخير الذي سنقوم به ضمن البيئة الافتراضية، وذلك عبر الدخول إلى مجلد المشروع ومن ثم استخدام الأمر gunicorn لتشغيل وحدة WSGI الخاصة بتطبيقنا: cd ~/myprojectdir gunicorn --bind 0.0.0.0:8000 myproject.wsgi سيشغل الأمر السابق Gunicorn على نفس الواجهة التي استخدمها خادم تطوير جانغو ويمكنك اختبار ذلك بإعادة طلب صفحات التطبيق عبر المتصفح. تنويه: ستلاحظ أن لوحة التحكم غير مُنسقة لأن Gunicorn لا يملك حتى الآن مسار صفحات CSS المسؤولة عن ذلك. لاحظ أننا استدعينا Gunicorn ومررنا له الوحدة التي تتضمن ملف wsgi.py -الذي يعدّ نقطة الدخول إلى تطبيقنا- مع تحديد موقع هذا الملف (حددناه عبر تنفيذ الأمر في مكان وجود الملف أي في مجلد المشروع)، ويحتوي الملف wsgi.py على دالة تسمى التطبيق application هي المسؤولة عن التواصل مع التطبيق. يمكنك معرفة المزيد حول مواصفات WSGI. اضغط على الاختصار ctrl-c داخل شاشة الطرفية عندما تنتهي من الاختبار لإيقاف خادم Gunicorn. يمكنك الآن إيقاف البيئة الافتراضية بالأمر التالي بعد أن انتهت إعدادات جانغو: deactivate وستلاحظ اختفاء محث البيئة الافتراضية من نافذة سطر الأوامر. إنشاء ملفات تمهيد Gunicorn اختبرنا في الفقرة السابقة تفاعل Gunicorn مع جانغو وتأكدنا من عمله، والآن سننشئ ملفات التمهيد systemd الخاصة به (المِقبس socket والخدمة)، لاستخدامها في إيقاف وتشغيل خادم التطبيق بطريقة أفضل. سيُنشَأ عند الإقلاع مِقبس Gunicorn ويبقى جاهزًا يستمع بانتظار الاتصالات، وعند ورود أي اتصال، فإن systemd سيشغل تلقائيًا عملية Gunicorn لتتعامل مع هذا الاتصال. لنبدأ بإنشاء ملف التمهيد لمِقبس Gunicorn بكتابة الأمر التالي مع امتيازات sudo: sudo nano /etc/systemd/system/gunicorn.socket بنية هذا الملف تتضمن ثلاثة مقاطع سنكتبها ضمنه، وهي: المقطع [Unit] مخصص لوصف المِقبس، والمقطع [Socket] يبين موقع المِقبس، أما المقطع [Install] فيحرص على إنشاء المقبس في الوقت الصحيح، وبذلك يصبح الملف بالشكل التالي: [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target يمكنك الآن إغلاق الملف بعد حفظ التغييرات. ومن ثم أنشئ ملف التمهيد لخدمة Gunicorn وفق الأمر التالي وأيضًا مع امتيازات sudo، لاحظ أن الاسم والمسار متطابقان بين ملف المقبس وملف الخدمة باستثناء اللاحقة فهي التي تحدد نوع الملف: sudo nano /etc/systemd/system/gunicorn.service تتضمن بنية ملف الخدمة ثلاثة مقاطع أيضًا هي: [Unit] و [Service] و [Install]. يُخصص المقطع [Unit] لبيانات التعريف والاعتماديات، ونكتب ضمنه وصف الخدمة ونخبر نظام التمهيد أن تشغيل هذه الخدمة مرتبط بالوصول إلى هدف الشبكة الذي يعتمد على المقبس (عند ورود اتصال على المقبس كما ذكرنا سابقًا) وهذه العلاقة بين المقبس والخدمة سنذكرها ضمن المحدد Requires كما يلي: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target نحدد ضمن المقطع [Service] من يتمتع بصلاحية تشغيل الخدمة من مستخدمين ومجموعات عمل، وهم في حالتنا مستخدم التطبيق sammy وهو مالك كافة الملفات المرتبطة، ومجموعة العمل www-data التي ستتيح لخادم Nginx الاتصال بسهولة مع Gunicorn. ونوضح بعده مجلد العمل والأمر الذي سيُستخدم لتشغيل الخدمة: بكتابة المسار الكامل لملف Gunicorn التنفيذي (الذي تم تثبيته في بيئتنا الافتراضية)، وربط العملية بمقبس يونيكس الذي أنشأناه ضمن المسار المحدد في run/ (انظر المقطع [Socket] في ملف المقبس) حتى تتمكن العملية من التواصل مع Nginx. وأخيرًا سنكتب ضمن المقطع التعليمات اللازمة لتُسجل كافة الأحداث في ملفات خرج قياسية حتى تتمكن journald من التقاط تسجيلات الأحداث الصادرة عن Gunicorn، وننوه إلى إمكانية ضبط تعديلات أخرى خاصة بـ Gunicorn هنا مثل تحديد عدد العمال workers بثلاثة كما في حالتنا. انظر الآن إلى ملف الخدمة بعد إضافة المقطع [Service]: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application أما المقطع [Install] وهو المقطع الأخير، نستخدمه لإخبار نظام التمهيد بمرحلة بدء التشغيل التي سيرتبط بها تشغيل الخدمة في حال فعلّناها لتعمل تلقائيًا عند الإقلاع، وفي حالتنا ضبطنا الموضوع لتعمل الخدمة عندما يصل نظام التشغيل إلى مرحلة تعدد المستخدمين multi-user. انظر الآن الشكل النهائي لملف الخدمة، ولاتنسَ حفظ التغييرات عليه قبل الإغلاق: [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=sammy Group=www-data WorkingDirectory=/home/sammy/myprojectdir ExecStart=/home/sammy/myprojectdir/myprojectenv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ myproject.wsgi:application [Install] WantedBy=multi-user.target يمكننا الآن تشغيل وتفعيل مقبس Gunicorn وفق الأمرين أدناه، وسيؤدي هذا إلى إنشاء ملف المقبس في المسار run/gunicorn.sock/ الآن عند تشغيلنا اليدوي للمقبس وتلقائيًا عند إقلاع نظام التشغيل، وفور ورود اتصال إلى المقبس سيشغل systemd تلقائيًا الخدمة gunicorn.service لتتعامل مع الاتصال: sudo systemctl start gunicorn.socket sudo systemctl enable gunicorn.socket يمكنك التأكد من نجاح العملية عبر التحقق من وجود ملف المقبس. اطلع على معلومات إضافية عن systemd من خلال المقالين: أساسيات Systemd: العمل مع الخدمات، الوحدات Units، واليوميات Journal إدارة الخدمات على لينكس بأدوات نظام التمهيد systemd التحقق من ملف المقبس تأكد من حالة العملية لترى إن كانت فعالة وقابلة للتشغيل عن طريق الأمر التالي: sudo systemctl status gunicorn.socket وستحصل على ما يشبه الخرج التالي في حال كانت فعالة دون أخطاء: ● gunicorn.socket - gunicorn socket Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor prese> Active: active (listening) since Fri 2020-06-26 17:53:10 UTC; 14s ago Triggers: ● gunicorn.service Listen: /run/gunicorn.sock (Stream) Tasks: 0 (limit: 1137) Memory: 0B CGroup: /system.slice/gunicorn.socket تحقق الآن من وجود ملف المقبس gunicorn.sock ضمن المسار run/ بكتابة الأمر التالي: file /run/gunicorn.sock ستحصل على الخرج التالي الذي يبين وجود الملف: /run/gunicorn.sock: socket أما في حال عدم وجود الملف gunicorn.sock، أو ظهور أي خطأ في خرج التعليمة systemctl status السابقة، فهذا يعني حدوث خطأ ما منع اكتمال العملية وإنشاء ملف المقبس، يمكنك تبيانه عبر استعراض سجلات الأحداث الخاصة بمقبس Gunicorn من خلال الأمر: sudo journalctl -u gunicorn.socket ومن ثم فتح الملف gunicorn.socket ضمن المسار etc/systemd/system/ ثانيةً لتصحيح واستدراك أي مشاكل موجودة بينتها السجلات قبل استكمال العمل. التحقق من تفعيل المقبس صحيح أننا شغلنا المقبس gunicorn.socket وتحققنا منه في الخطوات السابقة، ولكن الخدمة gunicorn.service لم تُفعل بعد وهذا طبيعي فهي مرتبطة بالاتصالات الواردة ولن تعمل قبل ورود أول اتصال. ويمكنك التحقق من حالتها بالأمر: sudo systemctl status gunicorn وستحصل على الخرج التالي الذي يبين أنها غير فعالة: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: inactive (dead) أرسل الآن اتصال تجريبي عبر curl لتختبر آلية العمل، وفق التالي: curl --unix-socket /run/gunicorn.sock localhost وستحصل بموجبه على صفحة HTML من تطبيقك داخل شاشة الطرفية، ما يعني أن Gunicorn بدأ بالعمل وأنه قادرٌ على تخديم التطبيق، نفذ مجددًا أمر التحقق من حالة الخدمة: sudo systemctl status gunicorn ولاحظ اختلاف الخرج التنفيذ السابق قبل وجود اتصالات: ● gunicorn.service - gunicorn daemon Loaded: loaded (/etc/systemd/system/gunicorn.service; disabled; vendor preset: enabled) Active: active (running) since Fri 2020-06-26 18:52:21 UTC; 2s ago TriggeredBy: ● gunicorn.socket Main PID: 22914 (gunicorn) Tasks: 4 (limit: 1137) Memory: 89.1M CGroup: /system.slice/gunicorn.service ├─22914 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> ├─22927 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> ├─22928 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> └─22929 /home/sammy/myprojectdir/myprojectenv/bin/python /home/sammy/myprojectdir/myprojectenv/bin/gunicorn --access-logfile - --workers 3 --bind unix:/run/gunico> Jun 26 18:52:21 django-tutorial systemd[1]: Started gunicorn daemon. Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Starting gunicorn 20.0.4 Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Listening at: unix:/run/gunicorn.sock (22914) Jun 26 18:52:21 django-tutorial gunicorn[22914]: [2020-06-26 18:52:21 +0000] [22914] [INFO] Using worker: sync Jun 26 18:52:21 django-tutorial gunicorn[22927]: [2020-06-26 18:52:21 +0000] [22927] [INFO] Booting worker with pid: 22927 Jun 26 18:52:21 django-tutorial gunicorn[22928]: [2020-06-26 18:52:21 +0000] [22928] [INFO] Booting worker with pid: 22928 Jun 26 18:52:21 django-tutorial gunicorn[22929]: [2020-06-26 18:52:21 +0000] [22929] [INFO] Booting worker with pid: 22929 وفي حال حصلت على أي أخطاء في خرج systemctl status أو خرج curl، تفقد عندها سجلات الأحداث عبر الأمر: sudo journalctl -u gunicorn وافتح الملف gunicorn.service ضمن المسار etc/systemd/system/ وصحح المشاكل التي بينتها السجلات قبل إكمال العمل، وانتبه إلى أن أي تعديلات تطبقها عليه لن تصبح سارية المفعول إلّا بعد إعادة تحميل الخدمة ومن ثم إعادة تشغيل Gunicorn وفق الأوامر التالية: sudo systemctl daemon-reload sudo systemctl restart gunicorn إعداد Nginx ليؤدي دور وكيل Gunicorn لنبدأ بإنشاء كتلة خادم جديدة لخادم Nginx ضمن المجلد sites-available. اكتب أولًا الأمر التالي الذي سيفتح ملفًا نصيًا: sudo nano /etc/nginx/sites-available/myproject أنشئ فيه كتلة خادم جديدة وحدد مواصفاتها بحيث تتلقى الطلبات الطبيعية التي ترد إلى البوابة 80 وتطلب اسم نطاقك أو عنوان IP الخادم، وذلك بكتابة التعليمات التالية: server { listen 80; server_name server_domain_or_IP; } وبعدها أضف ضمن الكتلة نفسها التعليمات الخاصة بـ location المبينة أدناه، ووظيفتها جعل Nginx يتجاهل الأخطاء التي تنجم عن عدم العثور على رمز الموقع favicon، وإرشاده إلى موقع ملفات التطبيق الساكنة أي المسارmyprojectdir/static/~ الذي اختصرناه إلى "static/"، إذًا أصبحت كتلة الخادم على الشكل التالي: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } } والخطوة الأخيرة ستكون إنشاء كتلة {} / location التي ستتعامل مع بقية الطلبات، والإشارة ضمنها للملف proxy_params القياسي الذي أنُشئ أثناء تثبيت Nginx، ومن ثم تمرير كامل حركة البيانات مباشرةً إلى مقبس Gunicorn: server { listen 80; server_name server_domain_or_IP; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /home/sammy/myprojectdir; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } } احفظ التغييرات على الملف وأغلقه. ومن ثم فعل الملف عبر ربطه بالمجلد sites-enabled باستخدام الأمر: sudo ln -s /etc/nginx/sites-available/myproject /etc/nginx/sites-enabled اختبر صحة تعليمات Nginx من ناحية قواعد الكتابة من خلال الأمر: sudo nginx -t أعد تشغيل Nginx إن لم تظهر لك أي أخطاء: sudo systemctl restart nginx لنضبط الآن إعدادات الجدار الناري UFW عبر السماح بحركة مرور البيانات عبر البوابة 80، وإغلاق البوابة 8000 التي سمحنا بها سابقًا للتخاطب مع خادم التطوير، فهي لم تعد لازمة. اكتب الأوامر التالية لضبط الجدار الناري: sudo ufw delete allow 8000 sudo ufw allow 'Nginx Full' يمكنك الآن الوصول لتطبيقك باستخدام اسم النطاق أو عنوان IP. تنويه: الخطوة الأهم بعد إعداد الخادم الوكيل هي تأمين شهادات TLS لتشفير حركة البيانات المتبادلة مع التطبيق وحماية البيانات الحساسة مثل كلمات المرور، لذا احرص على تنفيذ هذه الخطوة، ويمكنك الحصول على شهادات مجانية من خدمة Let’s Encrypt في كان لديك اسم نطاق محجوز، واتبع الخطوات الواردة في مقال كيف تؤمّن خادم ويب NGINX على أوبنتو 16.04 لتثبيت الشهادة فالطريقة نفسها. استكشاف الأخطاء وإصلاحها في هذه البيئة إذا لم تُظهر هذه الخطوة الأخيرة تطبيقك، فستحتاج إلى استكشاف أخطاء التثبيت وإصلاحها. يظهر Nginx الصفحة الافتراضية بدلاً من تطبيق جانغو في حال أظهر Nginx صفحته الافتراضية بدلًا من تطبيقك، فهذا يعني عادةً حاجتك لضبط server_name في الملف التالي: /etc/nginx/sites-available/myproject ليشير إلى اسم نطاقك أو عنوان IP للخادم، إذ يستخدم Nginx القيمة الموجودة في server_name ليُحدد أي كتلة خادم ستلبي الطلبات الواردة إليه، وعندما يرجع لك صفحته الافتراضية فهذا يعني أنه لم يتمكن من مطابقة طلبك بدقة مع أي من الكتل الموجودة ضمن ملفاته فيُرجع تلقائيًا الصفحة الموجودة في كتلته الافتراضية في المسار التالي: /etc/nginx/sites-available/default لذا احرص على تحديد server_name في الكتلة الخاصة بمشروعك بدقة وعلى نطاق أضيق من المذكور في الكتلة الافتراضية بحيث لا تسمح لخادم Nginx بتجاهلها والعودة إلى كتلته الافتراضية. ظهور Nginx الخطأ 502 بدلا من تطبيق جانغو يوجد مجموعة واسعة من الاحتمالات التي قد تسبب الخطأ 502 Bad Gateway Error، لذا فهو يحتاج إلى معلومات وتقصي أكثر لبيان سببه الفعلي. سجلات أحداث Nginx هي أهم مصدر معلومات عن الأحداث والأخطاء التي تحصل خلال عمل الوكيل أو الوسيط، ويمكنك تتبعها باستخدام الأمر: sudo tail -F /var/log/nginx/error.log والآن حدث الصفحة في متصفح الإنترنت لتولد خطأ جديدًا في السجل واقرأه بدقة من شأنه تضييق نطاق البحث عن سبب الخطأ، وسنعطيك بعض الاحتمالات عن ما يمكن أن يظهر لك من رسائل خطأ. احتمال الخطأ الأول: connect() to unix:/run/gunicorn.sock failed (2: No such file or directory) وهو يعني أن Nginx لم يتمكن من العثور على ملف المقبس gunicorn.sock في الموقع المحدد، وعندها عليك مقارنة موقع proxy_pass المذكور في الملف التالي: /etc/nginx/sites-available/myproject مع الموقع الفعلي المذكور في ملف المقبس gunicorn.sock المنشئ بواسطة وحدة تمهيد النظام gunicorn.socket. أما في حال لم تجد ملف المقبس gunicorn.sock في المسار run/ فارجع إلى فقرة التحقق من ملف المقبس واتبع إرشاداتها لمعالجة هذه الحالة. احتمال الخطأ الثاني: connect() to unix:/run/gunicorn.sock failed (13: Permission denied) يدل هذا الخطأ على مشاكل في صلاحيات وصول Nginx إلى مقبس Gunicorn، وغالبًا ما ينجم هذا الخطأ عن تنفيذ إجرائية العمل بمستخدم الجذر بدلًا من مستخدم يملك امتيازات sudo وعندها فإن systemd سيُنشئ مقبس Gunicorn ولكنّ Nginx لن يتمكن من الوصول إليه. ومن الممكن أن تظهر نفس المشكلة في حال وجود أي تقييد للصلاحيات على طول المسار الواصل إلى ملف المقبس بدءًا من الجذر / وصولًا إلى الملف gunicorn.sock، ويمكنك استعراض صلاحيات الوصول واسم المالك على كافة المجلدات الموجودة على مسار ملف المقبس باستخدام التعليمة namei وفق التالي: namei -l /run/gunicorn.sock وستحصل على المعلومات التفصيلية عن صلاحيات كافة عناصر المسار ضمن الخرج بالشكل التالي: f: /run/gunicorn.sock drwxr-xr-x root root / drwxr-xr-x root root run srw-rw-rw- root root gunicorn.sock يبين لك العمود الأول الصلاحيات الممنوحة على العنصر، والعمود الثاني يشير إلى اسم المستخدم المالك، بينما العمود الثالث فهو خاص باسم مجموعة العمل التي ينتمي إليها المالك، وبذلك يمكنك معرفة نوع الوصول المسموح إلى ملف المقبس. فمثلًا في الخرج السابق، كافة عناصر المسار تحمل صلاحية القراءة والتنفيذ فهي تنتهي بالرمز r-x بدلًا من --- وهذا يعني إمكانية الوصول إليها من قبل Nginx فلو كان أي منها لا يحمل هذه الصلاحيات لحصلت مشكلة في الوصول إلى المقبس، وتُحل المشكلة آنذاك بإعطاء صلاحية العمل الناقصة أو بضم مستخدم Nginx إلى مجموعة العمل التي تتضمن مالك العنصر. ظهور جانغو (لا يمكن الاتصال بالخادم: تم رفض الاتصال) هي إحدى الرسائل التي قد تتلقاها من جانغو عند محاولتك استعراض بعض أجزاء التطبيق في متصفح الإنترنت: OperationalError at /admin/login/ could not connect to server: Connection refused Is the server running on host "localhost" (127.0.0.1) and accepting TCP/IP connections on port 5432? تعني هذه الرسالة أن جانغو لم يتمكن من الوصول إلى قاعدة البيانات، لذا فالخطوة الأولى هي التأكد من إقلاع نسخة قاعدة البيانات Postgres instance باستخدام الأمر التالي: sudo systemctl status postgresql وفي حال تبين لك أنها لا تعمل فيمكنك تشغيلها ومن ثم تفعيلها لتعمل تلقائيًا عند الإقلاع (ما لم تكن مفعلة للتشغيل التلقائي) وفق الأمرين: sudo systemctl start postgresql sudo systemctl enable postgresql وإن استمرت المشكلة تأكد من إعدادات قاعدة البيانات الموجودة في الملف التالي وصححها: ~/myprojectdir/myproject/settings.py المزيد من استكشاف الأخطاء وإصلاحها استعن دومًا بسجلات الأحداث فهي مصدر مهم للمعلومات وحدد الأخطاء بدقة وابحث عنها، إليك أهم السجلات. للتحقق من سجلات عملية Nginx اكتب: sudo journalctl -u nginx للتحقق من سجلات وصول Nginx اكتب: sudo less /var/log/nginx/access.log للتحقق من سجلات خطأ Nginx اكتب: sudo less /var/log/nginx/error.log للتحقق من سجلات أحداث Gunicorn اكتب: sudo journalctl -u gunicorn للتحقق من سجلات أحداث مقبس Gunicorn اكتب: sudo journalctl -u gunicorn.socket عند تعديل أو تحديث الإعدادات أو التطبيق يجب إعادة تشغيل العمليات لتصبح التعديلات سارية المفعول. ففي حال عدلت تطبيق جانغو يمكنك إعادة تشغيل عملية Gunicorn عبر الأمر: sudo systemctl restart gunicorn وإن عدلت ملفات مقبس أو خدمة Gunicorn فعليك إعادة تحميل العفريت وإعادة تشغيل الخدمة: sudo systemctl daemon-reload sudo systemctl restart gunicorn.socket gunicorn.service أما لو عدلت إعدادات كتلة الخادم لـ Nginx فعليك التحقق من صحة القواعد أولًا ومن ثم إعادة تشغيل Nginx وفق التالي: sudo nginx -t && sudo systemctl restart nginx ستفيدك هذه الأوامر في عكس تعديلاتك على عمل البيئة بصورة سليمة. خاتمة أعددنا في هذا المقال مشروع جانغو ضمن بيئة افتراضية معزولة ومجهز بخادم Gunicorn يوصل طلبات العملاء إلى جانغو حتى يعالجها، وبخادم Nginx الذي يلعب دور الوكيل العكسي لتحسين أداء التطبيق وأمانه، وقدمنا مجموعة من الأدوات المساعدة التي من شأنها خدمة تطبيقاتك العاملة على خادم واحد. ترجمة -وبتصرف- للمقال How To Set Up Django with Postgres, Nginx, and Gunicorn on Ubuntu 20.04 لصاحبه Erin Glass. اقرأ أيضًا المرجع الشامل إلى تعلم PostgreSQL البدء مع إطار العمل جانغو لإنشاء تطبيق ويب كيف تُرقِّي خادم Nginx موجود بدون قطع اتصالات العميل
  6. هذا المقال هو الثالث من سلسلة تعليمية تتضمن ثلاث مقالات عن الانتقال من الحاويات إلى كوبيرنتس Kubernetes باستخدام جانغو Django إطار العمل الخاص بتسريع تطوير تطبيقات الويب المبنية بلغة بايثون، في المقال الأول تعلمنا تعديل عينة تجريبية من تطبيق جانغو (تسمى التطبيق Polls) وفق منهجية Twelve-Factor الخاصة بتطوير تطبيقات ويب سحابية قابلة للتوسع والعمل ضمن الحاويات، وفي المقال الثاني تعرفنا على طرق توسيع هذا التطبيق (المعدّل المغلف في حاوية دوكر) توسيعًا أفقيًا وتأمينه بتشغيل وكيل عكسي أمامه هو خادم Nginx مع استخدام شهادات TLS مصدقة من Let's Encrypt، أما اليوم نشارككم كيفية نشر التطبيق المعدّل نفسه باستعمال عنقود كوبيرنتس Kubernetes Cluster. كوبيرنتس kubernetes هو حل مفتوح المصدر لإدارة الحاويات، ميزته الأساسية أتمتة عمليات نشر وتوسيع وإدارة تطبيقات الويب المغلفة ضمن حاويات، وذلك من خلال مجموعة من الكائنات الخاصة مثل خرائط الإعدادات ConfigMaps والأسرار Secrets التي تسمح بتهيئة مركزية للبيئة من خارج الحاويات، ووحدات التحكم مثل النشر Deployments المسؤولة عن إعادة تشغيل الحاويات تلقائيًا في حال حدوث أي خلل وعن التوسعة التلقائية بتشغيل حاويات إضافية مماثلة لحاوية التطبيق عند ازدياد الطلب عليه، وأيضًا كائن الإدخال ingress أو المُدخل (تترجم أحيانًا إلى كائن الولوج واعتمدنا هنا كائن الإدخال والمُدخل حسب سياق الجملة) المسؤول عن توجيه حركة البيانات HTTP أو HTTPS المشفرة بشهادات TLS من المصادر الخارجية إلى خدمات كوبيرنتس الداخلية عبر قواعد وصول محددة مع وحدة التحكم مفتوحة المصدر ingress-nginx، أما التجديد الدوري للشهادات وتصديقها من Let's Encrypt تنجزه الوظيفة الإضافية مدير الشهادات cert-manager. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي لهذا المقال: عنقود كوبيرنتس بإصدار 1.15 أو أعلى، مع تفعيل خاصية التحكم بالوصول المستند على الدور Role-based access control (RBAC)، استخدمنا في المقال عنقود خدمة DigitalOcean، لكنك لست ملزمًا بذلك يمكنك إعداده بنفسك بالطريقة التي تناسبك. تثبيت أداة سطر الأوامر kubectl على جهازك المحلي وإعدادها للاتصال بالعنقود، يمكنك معرفة المزيد عن تثبيت kubectl بالاطلاع على توثيقات كوبيرنتس الرسمية. أما إن اعتمدت DigitalOcean فاستعن بالمقال إعداد الاتصال مع عنقود كوبيرنتس في DigitalOcean. حجز اسم نطاق لتطبيقك، سنعتمد في مقالنا على الاسم your_domain.com، مع العلم بإمكانية حصولك على اسم نطاق مجاني من Freenom. تثبيت وحدة التحكم ingress-nginx ومدير الشهادات cert-manager وضبط الإعدادات اللازمة للتحقق من الشهادات، يمكنك الاطلاع على كيفية إعداد Nginx Ingress ومدير الشهادات على كوبيرنتس. ربط اسم النطاق بعنوان IP العام لمُدخل موازن الحمل عبر إضافة سجل DNS من النوع A، يمكنك الحصول على تفاصيل إضافية حول DNS بالاطلاع على القسم الخاص بخدمة اسم النطاق DNS على أكاديمية حسوب. خدمة تخزين كائني متوافقة مع S3 ومع إضافة django-storages مثل DigitalOcean Space وظيفتها تأمين المساحة التخزينية اللازمة لملفات تطبيق جانغو الساكنة، مع مجموعة مفاتيح الوصول الخاصة بإدارة هذه المساحة، استرشد بكيفية إعداد مساحة على DigitalOcean Space وعدّل بما يلائم خدمة التخزين التي اخترتها. نظام إدارة قواعد بيانات متوافق مع جانغو مع إنشاء قاعدة بيانات ومستخدم خاص بالتطبيق، نحن اخترنا خادم PostgreSQL، اهتم بالتفاصيل التالية أثناء إنجازك لهذه النقطة لتتمكن من متابعة خطوات المقال: استخدم الاسم polls لقاعدة البيانات والاسم sammy لمستخدم قاعدة البيانات (هذه الأسماء ليست ملزمة ولكننا استخدمناها في المقال واستخدامك لها سيسهل عليك المتابعة)، لمزيد من التفاصيل حول الإنشاء استرشد بالخطوة /1/ من مقالنا السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]()، مع العلم أن هذه المهمة يمكن إنجازها من أي خادم من الخوادم الثلاثة لبيئة العمل. اعتمدنا في المقال على نظام إدارة قواعد البيانات من نوع Managed PostgreSQL cluster الذي توفره DigitalOcean. لكن يمكنك تثبيت PostgreSQL وتشغيله وإعداده بنفسك مع الاستعانة بالفيديو التعليمي تثبيت وإعداد قاعدة بيانات PostgreSQL. حساب على Docker Hub، استرشد بتوثيقات دوكر الخاصة بالمستودعات لإنشائه. تثبيت دوكر على جهازك المحلي، استعن لذلك بمقال كيفية تثبيت دوكر واستخدامه على دبيان. الخطوة 1: استنساخ التطبيق وضبط إعداداته تتلخص هذه الخطوة باستنساخ كود التطبيق من مستودع GitHub بالإضافة إلى ضبط محددات الاتصال مع خادم قاعدة البيانات والمفاتيح الخاصة بخدمة التخزين الكائني المستخدمة. سنحصل على كود التطبيق والملف Dockerfile من القسم polls-docker من مستودع GitHub الذي يتضمن نسخًا تجريبية من التطبيق المسمى Polls وهذه النسخ تتناسب مع أغراض جانغو التعليمية، بالإضافة لكونها مغلفة ومعدّلة بكل ما يلزم لتعمل بفعالية داخل الحاوية وتتكيف مع متغيرات بيئة التشغيل وفق ما تعلمنا في المقال الأول من هذه السلسلة. اكتب الأمر التالي للاستنساخ: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه للمجلد django-polls عبر الأمر: cd django-polls يتضمن هذا المجلد كود التطبيق والملف Dockerfile الذي سيستخدمه دوكر لبناء صورة الحاوية، بالإضافة إلى الملف env الذي يتضمن متغيرات البيئة التي ستمرر للحاوية من بيئة التشغيل المحيطة. استعرض الملف Dockerfile عبر تعليمة لينكس cat: cat Dockerfile وستحصل على الخرج التالي: FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"] وفقًا للخرج السابق، فإن Dockerfile سيستخدم صورة دوكر بإصدار بايثون 3.7.4، ويثبت اعتماديات وحزم بايثون اللازمة لكل من جانغو وخادم gunicorn وفق ملف متطلبات التطبيق django-polls/requirements.txt ومن ثم يحذف الملفات غير اللازمة بعد انتهاء التثبيت، وينسخ بعد ذلك كود التطبيق إلى الصورة ويسند قيمة المتغير PATH، وأخيرًا سيحدد البوابة أو المنفذ 8000 لاستقبال حركة البيانات الواردة إلى الحاوية، ويشغل gunicorn مع عمال workers عدد /3/ عبر المنفذ 8000. لتعرف المزيد عن كل مرحلة ضمن Dockerfile راجع مقالنا الأول السابق من السلسلة. لنبني صورة الحاوية باستخدام docker build: docker build -t polls . سميّنا الصورة باسم polls باستخدام الراية t- ومررنا المسار الحالي . كمسار context للتعليمة build فهو يتضمن كافة الملفات اللازمة لعملية البناء. بعد انتهاء عملية البناء استخدم الأمر docker images لاستعراض الصور المتاحة: docker images وستحصل على الخرج التالي: REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB لنضبط متغيرات البيئة تمهيدًا لعملية التشغيل docker run، وذلك بتعديل الملف env باستعمال محرر النصوص: nano env سيظهر أمامك الملف env بالشكل التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info كما ترى فبعض محددات الملف قيمها موجودة والبعض الآخر يحتاج إلى ضبط بما يناسب بيئتك، سنذكر المحددات التي تحتاج إلى ضبط بالترتيب: المفتاح السري DJANGO_SECRET_KEY اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات Docker نقدم لك في الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces إحدى طرق جانغو لتوليد المفاتيح. أما DJANGO_ALLOWED_HOSTS فأسند له اسم النطاق الخاص بك your_domain.com، أو يمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهذا الرمز يسمح لأي عنوان IP بالوصول، يحميك هذا المحدد من الهجمات المعتمدة على حقن ترويسة HTTP وفي حال أردت المزيد من المعلومات اطلع على توثيقات جانغو الخاصة بهذا المحدد ضمن Core Settings DATABASE_USERNAME ضع اسم مستخدم قاعدة البيانات PostgreSQL الذي أنشأته أثناء إعدادك لمتطلبات العمل. DATABASE_NAME ضع القيمة polls أو الاسم الذي اخترته لقاعدة البيانات أثناء إعدادك لمتطلبات العمل. كذلك الأمر لكلمة المرور DATABASE_PASSWORD. ضع اسم مضيف قاعدة البيانات DATABASE_HOST ورقم بوابة الاتصال معها DATABASE_PORT. أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. احفظ التغيرات على الملف env بعد الانتهاء وأغلقه، ولننتقل للخطوة التالية. الخطوة 2: إنشاء مخطط قاعدة البيانات ورفع ملفات التطبيق إلى وحدة التخزين الكائني شغل صورة الحاوية polls:latest عبر الأمر التالي: docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate" وبموجبه الأمر سيجري تجاوز الأمر الافتراضي عند بدء التشغيل المحدد بتعليمة CMD ضمن الملف Dockerfile وتنشئ أثناء التشغيل مخطط قاعدة بيانات التطبيق وهو الأمر الفرعي: sh -c "python manage.py makemigrations && python manage.py migrate" إن كنت تنفذ الأمر docker run للمرة الأولى فستحصل على الخرج التالي الذي يبين نجاح إنشاء مخطط قاعدة البيانات: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK مع العلم أن جانغو لن يجري أي عملية عندما تنفذ migrate في المرات القادمة طالما لم يطرأ أي تغيير على مخطط قاعدة بيانات التطبيق. نفذ بعدها أمر تشغيل جديد للحاوية مع استدعاء سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env polls sh تظهر بنتيجة هذا الأمر نافذة الصدفة shell ضمن الحاوية اكتب فيها تعليمة إنشاء المستخدم الخاص بتطبيق جانغو: python manage.py createsuperuser استكمل بياناته وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، واضغط بعدها على الاختصار Ctrl+D للخروج من الحاوية وإنهاء عملها. أما العملية الأخيرة فهي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني وتتم وفق الأمر التالي علمًا أنها قد تستغرق بعض الوقت: docker run --env-file env polls sh -c "python manage.py collectstatic --noinput" يظهر بعدها الخرج التالي: 121 static files copied. يمكنك الآن تشغيل التطبيق: docker run --env-file env -p 80:8000 polls ستحصل على الخرج: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 مع العلم أن التشغيل هذه المرة وفق الإعدادات الافتراضية المنصوص عليها في ملف Dockerfile وهي: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وأن البوابة 8000 للحاوية فُتحت لتستقبل حركة البيانات القادمة من البوابة 80 للخادم أوبونتو. يمكنك الآن كتابة عنوان URL الخاص بالتطبيق polls بمتصفح الإنترنت واستعراضه، لاحظ أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان: http://APP_SERVER_1_IP/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / لذا اكتب العنوان التالي لتحصل على واجهة polls: http://APP_SERVER_1_IP/polls اكتب الرابط التالي: http://APP_SERVER_1_ip/admin وستتمكن من الوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. تذكر أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع هذه الخطوات لاختبار جودته وأيضًا التأكد من صحة إحضار الملفات. بعد أن تنتهي من اختبار عمل التطبيق اضغط على الاختصار Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لتنهي عمل الحاوية. والآن بعد إتمام بناء صورة دوكر للتطبيق، وبناء مخطط قاعدة البيانات وتحميل مكونات التطبيق الساكنة إلى وحدة التخزين الكائني، وأخيرًا اختبار عمل الحاوية والتطبيق، يمكننا رفع الصورة إلى سجل صور مثل Docker Hub وهو موضوع خطوتنا التالية. الخطوة 3: رفع صورة جانغو إلى Docker Hub لتستخدم تطبيقك ضمن عنقود كوبيرنتس، ستحتاج أولًا رفع صورة التطبيق إلى سجل صور عام أو خاص مثل Docker Hub حتى تتمكن خدمة كوبيرنتس من سحب الصورة ونشر التطبيق. علمًا أن لسجلات الصور نوعان عام وخاص، السجل العام مجاني إلاّ أنه يجعل صورك في متناول جميع المستخدمين ويمكّنهم من سحبها، أما الخاص فيعطي خصوصية أكبر إذا يسمح لك بمشاركة الصور مع مجموعة محددة من المستخدمين مثل زملاء عملك، وتقدم بعض الشركات -مثل سجل حاويات DigitalOcean على سبيل المثال لا الحصر- عروضًا مجانية لإنشاء سجلات خاصة محدودة، أما Docker Hub فيدعم النوعين العام والخاص. سنقدم في مثالنا طريقة سحب الصور من سجل Docker Hub عام ويمكنك الاطلاع على توثيقات كوبيرنتس لمعرفة طريقة العمل مع السجلات الخاصة. لنبدأ بتسجيل الدخول إلى Docker Hub وفق الحساب الذي ذكرناه في متطلبات العمل: docker login سيُطلب منك إدخال اسم المستخدم وكلمة المرور لحسابك وفق التالي: Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username: تحمل صورة التطبيق في بيئتنا الوسم polls:latest أعطها وسمًا جديدًا يتضمن اسم حسابك على Docker Hub واسم مستودعك باستخدام tag وفق التالي: docker tag polls:latest your_dockerhub_username/your_dockerhub_repo_name:latest استعملنا في مثالنا الاسم sammy للحساب و sammy-django للمستودع وبذلك يصبح الأمر على الشكل التالي: docker push sammy/sammy-django:latest ستحصل على خرج يتضمن تفاصيل عملية الرفع، وبعد إتمام العملية ستصبح الصورة متاحة أمام كوبيرنتس على Docker Hub. الخطوة 4: إعداد خريطة الإعدادات ConfigMap خريطة الإعدادات ConfigMap هي كائن كوبيرنتس المسؤول عن تمرير متغيرات بيئة التشغيل العامة إلى العنقود مثل إعدادات التطبيق، ويشاركه المهمة كائن آخر يدعى السرّ Secret إلاّ أنه يُعنى بالمتغيرات الحساسة مثل البيانات الخاصة بواجهات التخاطب API أو بقاعدة البيانات ولذا فهو يخضع لقواعد خاصة لحمايته والتحكم بوصول المستخدمين إليه مثل تفعيل خاصية encryption at rest، وبناءً على هذا الاختلاف في طبيعة بيانات الكائنين فإن بيانات ConfigMap تخزن بشكل نصي صريح ومقروء للمستخدم، بينما تخزن بيانات Secret معماة بأسلوب base64. تتشابه هذه الآلية في الواقع مع تمرير متغيرات البيئة لحاوية دوكر بالملف env الذي يلقم هذه المعلومات لأمر تشغيل الحاوية docker-run. لنبدأ بإعداد ConfigMap، والخطوة الأولى هي إنشاء مجلد يدعى yaml سنخزن ضمنه وثائق كوبرنيتس الأساسية: mkdir yaml cd ومن ثم أنشئ ضمن المجلد ملف نصي باسم polls-configmap.yaml يتضمن خريطة الإعدادات وفق التالي: nano polls-configmap.yaml وهذا يعني أننا بصدد إنشاء كائن كوبرنتس من نوع ConfigMap يحمل الاسم polls-config. الآن لننتقي من الملف env (الذي عملنا عليه في الخطوة /1/) البيانات العامة للتطبيق التي لا تتمتع بحساسية خاصة ونكتبها في الملف polls-configmap.yaml وذلك وفق الصيغة التالية: apiVersion: v1 kind: ConfigMap metadata: name: polls-config data: DJANGO_ALLOWED_HOSTS: "*" STATIC_ENDPOINT_URL: "https://your_space_name.space_region.digitaloceanspaces.com" STATIC_BUCKET_NAME: "your_space_name" DJANGO_LOGLEVEL: "info" DEBUG: "True" DATABASE_ENGINE: "postgresql_psycopg2" بالطبع مع وضع نفس القيم الموجودة في env وبخصوص المحدد DJANGO_ALLOWED_HOSTS نعطيه القيمة * كوننا نعمل في بيئة تجريبية. نحفظ بعدها التغييرات ونغلق الملف تمهيدًا لإنشاء الكائن ConfigMap بالأمر الأمر التالي: kubectl apply -f polls-configmap.yaml وسنحصل على الخرج التالي الذي يبين نجاح العملية: configmap/polls-config created الخطوة 5: إعداد السر Secret بيانات الكائن من نوع سرّ secret هي بيانات حساسة ومهمة كما ذكرنا سابقًا لذا فهي تُعمَّى بالأساس64، ولتحقيق ذلك لديك طريقتين إما ترميز البيانات خارجيًا من ثم كتابتها معماة ضمن الملف الذي سنعدّه للكائن، أو استخدام الأمر kubectl create مع الراية from-env-file-- وهي الطريقة التي استعملناها هنا. نعود للملف env استخدمنا متغيراته العامة في إنشاء الكائن Configmap والآن سنستخدم بقية المتغيرات التي تعد بيانات حساسة لإعداد الكائن secret. لننتقل للتطبيق العملي، انسخ الملف env إلى المجلد yaml وسمي النسخة polls-secrets وفق التالي: cp ../env ./polls-secrets افتح الملف polls-secrets باستعمال محرر النصوص: nano polls-secrets ستحصل على بيانات الملف env المنسوخ كاملةً: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info الآن احذف منه المتغيرات التي كتبناها ضمن ConfigMap في الخطوة السابقة، وستبقى المتغيرات التالية ضمنه: DJANGO_SECRET_KEY=your_secret_key DATABASE_NAME=polls DATABASE_USERNAME=your_django_db_user DATABASE_PASSWORD=your_django_db_user_password DATABASE_HOST=your_db_host DATABASE_PORT=your_db_port STATIC_ACCESS_KEY_ID=your_space_access_key STATIC_SECRET_KEY=your_space_access_key_secret ضع القيم الخاصة بتطبيقك نفسها التي ضبطناها في الخطوة /1/ من المقال (مثل محددات قاعدة البيانات والمفتاح الخاص وغيرها)، واحفظ التغييرات على الملف polls-secrets وأغلقه. أنشئ الآن الكائن من نوع secret والذي يحمل الاسم polls-secret من خلال الأمر التالي مع تمرير المتغيرات اللازمة للعنقود عبر الملف polls-secrets الذي أغلقته توًا: kubectl create secret generic polls-secret --from-env-file=poll-secrets وستحصل على الخرج الذي يبين نجاح الإنشاء: secret/polls-secret created استعرض بعدها وصف الكائن باستخدام kubectl describe: kubectl describe secret polls-secret لاحظ القيم المرمزة في الخرج: Name: polls-secret Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== DATABASE_PASSWORD: 8 bytes DATABASE_PORT: 5 bytes DATABASE_USERNAME: 5 bytes DJANGO_SECRET_KEY: 14 bytes STATIC_ACCESS_KEY_ID: 20 bytes STATIC_SECRET_KEY: 43 bytes DATABASE_HOST: 47 bytes DATABASE_NAME: 5 bytes مررنا بذلك إعدادات التطبيق جانغو إلى داخل عنقود كوبيرنتس وتم تخزينها باستخدام كائنات من نوع Secret و ConfigMap، وأصبحت بيئتنا جاهزة لنشر التطبيق ضمن العنقود. الخطوة 6: نشر التطبيق باستخدام وحدة تحكم النشر وحدة النشر Deployment هي إحدى وحدات تحكم عنقود كوبيرنتس، تتولى إدارة التطبيقات عديمة الحالة stateless applications التي تعمل ضمن العنقود. إذ أنها تدير أعباء العمل صعودًا ونزولًا فتُدخل العناصر بالخدمة أو تخرجها تبعًا لضغط العمل، بالإضافة إلى إعطائها أوامر إعادة التشغيل للحاويات التي تفشل أثناء الإقلاع. تتحكم وحدة النشر Deployment بواحد أو أكثر من كائنات Pod، أما Pod فهي أصغر عناصر عنقود كوبيرنتس وكل واحدة منها يمكن أن تتضمن حاوية أو أكثر، لمعرفة المزيد يمكنك الاطلاع على تعلم أساسيات Kubernetes على أكاديمية حسوب. افتح ملفًا جديدًا يحمل الاسم polls-deployment.yaml عبر أي محرر تفضله وليكن عبر nano مثلًا: nano polls-deployment.yaml والصق ضمنه التعليمات التالية: apiVersion: apps/v1 kind: Deployment metadata: name: polls-app labels: app: polls spec: replicas: 2 selector: matchLabels: app: polls template: metadata: labels: app: polls spec: containers: - image: your_dockerhub_username/app_repo_name:latest name: polls envFrom: - secretRef: name: polls-secret - configMapRef: name: polls-config ports: - containerPort: 8000 name: gunicorn أكتب اسم الصورة التي بنيناها في الخطوة /2/ ولا تنسَ الإشارة للاسم الذي رفعنا به الصورة على Docker Hub. لنشرح التعليمات السابقة بالترتيب. أطلقنا على وحدة التحكم الاسم polls-app وميّزناها بعنوان label ذا القيمة المزدوجة app: polls، وضبطنا عدد نسخ Pods المطلوب تشغيلها بنسختين وفق replicas: 2 أما إعدادات هذه Pods فتُحددها المواصفات المكتوبة تحت template. أما configMapRef و secretRef المستخدمين بعد envFrom فيحددان على الترتيب قيم المتغيرات التي عرفناها من خلال كائن خريطة الإعدادات واسمه polls-config والكائن سرّ واسمه polls-secret، وفي نهاية الملف عرفنا بوابة الحاوية containerPort وهي تحمل الرقم 8000 والاسم gunicorn. يمكنك تعلم المزيد عن وحدة تحكم النشر من خلال توثيقات كوبيرنتس. أغلق الملف بعد حفظ التغييرات، لنباشر بإنشاء وحدة تحكم النشر عبر الأمر التالي: kubectl apply -f polls-deployment.yaml تحصل على الخرج الذي يؤكد نجاح العملية: deployment.apps/polls-app created يمكنك أيضًا التأكد من الإنشاء عبر الأمر: kubectl get deploy polls-app الذي يعطي النتيجة: NAME READY UP-TO-DATE AVAILABLE AGE polls-app 2/2 2 2 6m38s في حال واجهت أي خطأ في عمل وحدة التحكم وأردت التأكد من حالتها استخدم الأمر: kubectl describe deploy أما لفحص الـ Pods وحالتها فاستخدم: kubectl get pod وستحصل على الخرج التالي: NAME READY STATUS RESTARTS AGE polls-app-847f8ccbf4-2stf7 1/1 Running 0 6m42s polls-app-847f8ccbf4-tqpwm 1/1 Running 0 6m57s الخطوة 7: السماح للمصادر الخارجية بالوصول إلى الخدمة سننشئ في هذه الخطوة خدمة كوبيرنتس خاصة بتطبيقنا، والخدمات في كوبيرنتس عبارة عن مفهوم مجرد يعرّف مجموعة من كائنات Pod لتظهر أمام المصادر الخارجية كخدمة شبكة واحدة أي كطرف واحد endpoint بغض النظر عن توقف أي Pod أو تشغيلها، والمصادر الخارجية في مثالنا هي حركة البيانات باتجاه التطبيق، وبالتالي من يطلب التطبيق جانغو سيطلب خدمة كوبيرنتس واحدة مستقرة ولن يتأثر بتوقف أو إضافة أي Pod داخلية. تتعدد أنواع خدمات كوبيرنتس، مثل خدمة ClusterIP التي تعتمد على عنوان IP الداخلي للعنقود، وخدمة NodePort التي تعرض الخدمة على بوابة محددة من كل عقدة، وخدمة LoadBalancer التي توفر موازنة حمل سحابية لحركة مرور البيانات القادمة من المصادر الخارجية إلى Pods داخل العنقود وذلك عبر خدمات NodePorts الخاصة بكل عقدة، والتي يتم إنشاؤها تلقائيًا في هذه الحالة. يمكنك تعلم المزيد عن هذه الخدمات من خلال توثيقات كوبيرنتس. لنبدأ بإنشاء الخدمة الخاصة بتطبيق جانغو وسنعتمد النوع NodePort. أنشئ ملفًا باسم polls-svc.yaml باستعمال محرر النصوص الذي تفضله وليكن باستعمال نانو nano: nano polls-svc.yaml والصق ضمنه التعليمات التالية: apiVersion: v1 kind: Service metadata: name: polls labels: app: polls spec: type: NodePort selector: app: polls ports: - port: 8000 targetPort: 8000 ``` لاحظ التعليمات السابقة، نوع الخدمة هو NodePort، اسمها polls وهي مميزة بالعنوان app: polls، وحددنا من خلال مُحدِّد selector الربط مع Pods الواجهات الخلفية بالعنوان app: polls و البوابة 8000. أغلق الملف بعد حفظ التغييرات، ومن ثم أنشئ الخدمة بالأمر kubectl apply: kubectl apply -f polls-svc.yaml ونحصل على ما يؤكد الإنشاء وفق الخرج التالي: service/polls created استعرض الخدمة وتأكد من إنشائها عبر kubectl get svc: kubectl get svc polls وسيظهر لك الخرج التالي، الذي يتضمن عنوان IP الداخلي للعنقود وبوابة العقدة NodePort وهي في حالتنا 32654: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls NodePort 10.245.197.189 <none> 8000:32654/TCP 59s حصلنا على رقم البوابة، ونحتاج إلى عنوان IP الخارجي للخدمة للاتصال مع التطبيق، وسنحصل عليه بالأمر التالي: kubectl get node -o wide الذي يعطينا الخرج: NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME pool-7no0qd9e0-364fd Ready <none> 27h v1.18.8 10.118.0.5 203.0.113.1 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fi Ready <none> 27h v1.18.8 10.118.0.4 203.0.113.2 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 pool-7no0qd9e0-364fv Ready <none> 27h v1.18.8 10.118.0.3 203.0.113.3 Debian GNU/Linux 10 (buster) 4.19.0-10-cloud-amd64 docker://18.9.9 أصبحت معلوماتنا كاملة لنستعرض التطبيق بكتابة العنوان التالي في المتصفح: http://203.0.113.1:32654/polls من المفترض حصولك على واجهة التطبيق نفسها التي استعرضتها محليًا في الخطوة /1/: تصفح أيضًا واجهة إدارة التطبيق admin/ للتأكد أكثر: http://203.0.113.1:32654/admin إذًا تطبيق جانغو الآن يستخدم حاويتين متماثلتين ويمكن الوصول له من خارج العنقود عبر خدمة كوبيرنتس مستقرة من نوع NodePort، والخطوة الأخيرة المتبقية لنا هي تأمين هذا الوصول ليتم عبر HTTPS وذلك بالاستفادة من وحدة التحكم ingress-nginx (وهي من متطلبات بيئة العمل المفترض تحضيرها) وبإنشاء كائن إدخال يوجه الحركة الخارجية إلى الخدمة polls. الخطوة 8: إعداد HTTPS باستخدام Nginx Ingress ومدير الشهادات توجه موارد كوبيرنتس من نوع المدخلات Ingresses حركة البيانات HTTP و HTTPS بمرونة إلى داخل العنقود، وذلك من خلال كائنات الإدخال التي تحدد قواعد التوجيه من المصادر الخارجية إلى خدمات كوبيرنتس الموجودة داخل عنقودك، ووحدات تحكم الإدخال التي تنفذ هذه القواعد عبر موازنة الحمل وتوجيه الطلبات إلى خدمات الواجهات الخلفية الأنسب. لابد أنك جهزت المتطلبات الواردة في بداية المقال وأنت تحضر بيئة العمل، ومنها تثبيت وحدة التحكم Ingress-nginx والوظيفة الإضافية cert-manager الخاصة بأتمتة شهادات TLS، مع ما يلزم لتصديق النطاق من Let’s Encrypt، وفي أثناء العملية أنشأت كائن إدخال لاختبار صلاحية الشهادة والتشفير TLS مع اثنين من خدمات الواجهات الخلفية الوهمية المنشأة لهذا الغرض، لذا سنبدأ بحذف echo-ingress الذي أنشأته خلال التحضير وفق التالي: kubectl delete ingress echo-ingress يمكنك أيضًا حذف الخدمات الوهمية المنشأة أثناء التحضير عبر التعليمتين kubectl delete svc kubectl delete deploy لكنه ليس أمرًا ملزمًا في حالتنا. لديك أيضًا (من متطلبات بيئة العمل) سجل DNS من النوع A يربط اسم لنطاقك بعنوان الـ IP العام لمُدخل موازن الحمل، فيمكنك إذًا إنشاء مُدخل لنطاقك your_domain.com والخدمة polls. أنشئ ملف polls-ingress.yaml باستخدام محرر النصوص: nano polls-ingress.yaml والصق ضمنه التعليمات التالية: [polls-ingress.yaml] apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: polls-ingress annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/cluster-issuer: "letsencrypt-staging" spec: tls: - hosts: - your_domain.com secretName: polls-tls rules: - host: your_domain.com http: paths: - backend: serviceName: polls servicePort: 8000 بموجب التعليمات السابقة، وصّفنا كائن إدخال يدعى polls-ingress ليستخدم وحدة التحكم ingress-nginx ووظيفة مدير الشهادات cert-manager مع ضبط ClusterIssuer الخاص بها على خيار staging، كما فعّلنا الشهادات TLS لترتبط باسم النطاق الخاص بنا your_domain.com أما ملف الشهادة والمفتاح الخاص تم تخزينهم في كائن من نوع سرّ يدعى polls-tls، وأخيرًا وجهّنا حركة البيانات التي تطلب النطاق your_domain.com لتمر عبر البوابة 8000 من الخدمة polls. احفظ الملف الآن وأغلقه، لننشئ الكائن بالاعتماد عليه، باستعمال الأمر التالي: kubectl apply -f polls-ingress.yaml سنحصل بعدها على الخرج: ingress.networking.k8s.io/polls-ingress created يمكنك استعراض وصف الكائن وتتبع حالته باستخدام kubectl describe: kubectl describe ingress polls-ingress التي تعطي الخرج: Name: polls-ingress Namespace: default Address: workaround.your_domain.com Default backend: default-http-backend:80 (<error: endpoints "default-http-backend" not found>) TLS: polls-tls terminates your_domain.com Rules: Host Path Backends ---- ---- -------- your_domain.com polls:8000 (10.244.0.207:8000,10.244.0.53:8000) Annotations: cert-manager.io/cluster-issuer: letsencrypt-staging kubernetes.io/ingress.class: nginx Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 51s nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 51s cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 25s nginx-ingress-controller Ingress default/polls-ingress وبنفس الطريقة تستطيع استعراض وصف polls-tls للتأكد من صحته: kubectl describe certificate polls-tls وستظهر لك حالة الشهادة في الخرج: . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Issuing 3m33s cert-manager Issuing certificate as Secret does not exist Normal Generated 3m32s cert-manager Stored new private key in temporary Secret resource "polls-tls-v9lv9" Normal Requested 3m32s cert-manager Created new CertificateRequest resource "polls-tls-drx9c" Normal Issuing 2m58s cert-manager The certificate has been successfully issued بذلك نكون قد وجّهنا الطلبات الواردة إلى التطبيق لتصبح HTTPS مشفرة، ولكن Clusterissuer في وضع staging ما يعني أن شهادتنا التجريبية المزيفة من Let's Encrypt لن تحظى بثقة معظم متصفحات الويب، أكتب الطلب التالي لنطاقك في نافذة سطر الأوامر: wget -O - http://your_domain.com/polls ولاحظ التحذير الظاهر في الخرج بسبب هذه الشهادة: . . . ERROR: cannot verify your_domain.com's certificate, issued by ‘CN=Fake LE Intermediate X1’: Unable to locally verify the issuer's authority. To connect to your_domain.com insecurely, use `--no-check-certificate'. سنتبع المقترح الوارد في الخرج أعلاه ونستخدم الراية no-check-certificate-- مع الطلب wget لتجاوز التحذير الخاص بالشهادة وفق التالي: wget --no-check-certificate -q -O - http://your_domain.com/polls ستحصل عندها على الخرج المبين أدناه والذي يظهر صفحة HTML المقابلة للواجهة polls/ وأيضًا مسار التخزين الكائني الذي أُحضرت منه الصفحة: <link rel="stylesheet" type="text/css" href="https://your_space.nyc3.digitaloceanspaces.com/django-polls/static/polls/style.css"> <p>No polls are available.</p> وهذا دليل على نجاح طلب النطاق، ما يعني أن بإمكاننا الآن تبديل Clusterissuer إلى وضع المنتج production عوضًا عن staging عبر فتح الملف polls-ingress.yaml: nano polls-ingress.yaml وتعديل قيمة cluster-issuer كما ذكرنا: [polls-ingress.yaml] apiVersion: networking.k8s.io/v1beta1 kind: Ingress metadata: name: polls-ingress annotations: kubernetes.io/ingress.class: "nginx" cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: tls: - hosts: - your_domain.com secretName: polls-tls rules: - host: your_domain.com http: paths: - backend: serviceName: polls servicePort: 8000 احفظ التغييرات على الملف وأغلقه، ولكن انتبه فهي لن تصبح نافذة حتى تُحدّث الكائن polls-ingress باستخدام التعليمة kubectl apply: kubectl apply -f polls-ingress.yaml سيشير الخرج إلى نجاح العملية: ingress.networking.k8s.io/polls-ingress configured شاهد التغير الذي طرأ على حالة الشهادات باستخدام: kubectl describe certificate polls-tls أو kubectl describe ingress polls-ingress وستحصل على الخرج التالي الذي يبين نجاح التحقق من الشهادات الجديدة وكونها مخزنة في السرّ polls-tls: . . . Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal CREATE 23m nginx-ingress-controller Ingress default/polls-ingress Normal CreateCertificate 23m cert-manager Successfully created Certificate "polls-tls" Normal UPDATE 76s (x2 over 22m) nginx-ingress-controller Ingress default/polls-ingress Normal UpdateCertificate 76s cert-manager Successfully updated Certificate "polls-tls" استعرض الآن تطبيقك your_domain.com/polls ولاحظ تفعيل HTTPS وعدم وجود أي تحذير بخصوص صلاحية الشهادة ويمكنك استعراض تفاصيل الشهادة وسلطة التصديق بالضغط على رمز القفل قرب الشريط المخصص لكتابة عنوان URL في متصفحك: ننصحك بإجراء اختياري أخير هو تعديل نوع الخدمة polls من NodePort إلى ClusterIP إذ إن هذا النوع يقبل فقط الحركة القادمة من عنوان IP الداخلي للعنقود ما يمنح تطبيقك مزيدًا من الأمان: افتح الملف polls-svc.yaml باستخدام المحرر: nano polls-svc.yaml بدل قيمة المحدد type من NodePort إلى ClusterIP: apiVersion: v1 kind: Service metadata: name: polls labels: app: polls spec: type: ClusterIP selector: app: polls ports: - port: 8000 targetPort: 8000 احفظ التغييرات على الملف وأغلقه، وحدّث بعدها الخدمة عبر الأمر: kubectl apply -f polls-svc.yaml --force سيؤكد لك الخرج التالي نجاح تعديل الخدمة: service/polls configured تحقق من حالة الخدمة المحدثة باستخدام: kubectl get svc polls ولاحظ معلومات الخرج: NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE polls ClusterIP 10.245.203.186 <none> 8000/TCP 22s وبذلك قيدّت الوصول لتطبيقك ليكون حصرًا من خلال اسم النطاق والمُدخل المنشأ في هذه الخطوة. خاتمة تعلمنا في هذا المقال طريقة نشر تطبيق جانغو قابل للتوسيع ومؤمن ببروتوكول HTTPS باستخدام عنقود كوبيرنتس، مع تسليم ملفات التطبيق الساكنة من وحدة تخزين كائني خارجية، وتوفير المرونة التامة لزيادة أو إنقاص عدد Pods التي تُخدم التطبيق بسرعة وبسهولة عبر المحدد replicas من ملف الإعدادات الخاص بوحدة تحكم النشر Deployment والتي أسميناها polls-app، ونذكرك بإمكانية تسريع التسليم باستخدام شبكة توصيل المحتوى CDN كإجراء اختياري. ترجمة -وبتصرف- للمقال How To Deploy a Scalable and Secure Django Application with Kubernetes لصاحبه Hanif Jetha. اقرأ أيضًا ما الفرق بين دوكر Docker وكوبيرنيتيس Kubernetes؟ مدخل إلى Helm: مدير حزم Kubernetes البدء مع إطار العمل جانغو لإنشاء تطبيق ويب
  7. توفر البيئة السحابية حلولًا مرنة لتوسيع التطبيقات العاملة ضمنها مثل جانغو Django وتأمينها، ومن أبرز هذه الحلول التوسع الأفقي بزيادة عدد خوادم التطبيق وعملها معًا لتحقيق إتاحة عالية للخدمة تخفف من فترات التوقف الناجمة عن الأعطال أو الضغط أو غيرها، وزيادة الإنتاجية وسعة المعالجة، واستخدام وكيل عكسي يوجه الطلبات الخارجية إلى خوادم التطبيق. لجهة التوسع الأفقي يمكنك استخدام أكثر من خادم تطبيق لأداء مهمة تشغيل جانغو مع خادم WSGI HTTP الخاص به (وهو في الغالب Gunicorn أو uWSGI)، وحول توجيه الحركة يمكنك إعداد خادم خاص يوزع الطلبات بين خوادم التطبيق مثل Nginx فهو قادر على القيام بدور الوكيل العكسي reverse proxying و موازن الحمل load balancing، بالإضافة إلى تأمين التخزين المؤقت Cache لمكونات وأصول التطبيق الساكنة static assets، علاوةً على أنه يدعم بروتوكول أمان طبقة النقل TLS لتكون كافة اتصالات التطبيق آمنة عبر HTTPS. أما حاويات دوكر المستخدمة في حالتنا فهي تتميز بسهولة تثبيت الحزم وإجراء الإعدادات للمكونات الموجودة داخلها (مثل جانغو وخادم Nginx) كما أنها تضمن للمطوّر أن هذه المكونات التي أعدّها ستعمل دائمًا بنفس الأسلوب بصرف النظر عن البيئة التي تُبنى فيها. ستتعلم في هذا المقال كيفية توسيع تطبيق جانغو Polls -هو اسم التطبيق- المغلف في حاوية توسيعًا أفقيًا بإعداد خادمين للتطبيق يُشغل كل منهما حاوية تتضمن جانغو وخادم Gunicorn، وكيفية تأمينه بتفعيل HTTPS وإعداد خادم ثالث يؤدي وظيفة الوكيل العكسي ويُشغل حاويتين واحدة تتضمن Nginx والأخرى تتضمن عميل Certbot الذي سيوفر لخادم Nginx شهادات TLS مصدقة من Let’s Encrypt وقادرة على منح تطبيقك تقييم عالي بنتائج اختبار SSL Labs، سيتلقى هذا الوكيل العكسي كافة الطلبات الخارجية الواردة ويحولها إلى خادمي تطبيق جانغو للإجابة عليها، وزيادة في الأمان سنقيّد الاتصال الخارجي بحيث يتم عبر الوكيل العكسي حصرًا. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي للمقال: ثلاثة خوادم عليها نظام تشغيل أوبنتو (استعملنا في المقال إصدار 18.04 واستعن بمقال التهيئة الأولية لخادم أوبونتو 18.04): اثنان منها خوادم تطبيق لزوم تشغيل تطبيقك (جانغو/Gunicorn). خادم واحد يؤدي وظيفة الوكيل لزوم تشغيل Nginx و Certbot. مستخدم عادي -غير مسؤول- يتمتع بصلاحية sudo وجدار ناري فعال على الخوادم الثلاثة. تثبيت دوكر على الخوادم الثلاثة، اطلع على كيفية تثبيت دوكر واستخدامه على دبيان لإنجاز المهمة إذ طريقة التثبيت نفسها في النظامين. حجز اسم نطاق لتطبيقك، سنعتمد في مقالنا على الاسم your_domain.com، مع العلم بإمكانية حصولك على اسم نطاق مجاني من Freenom. ربط اسم النطاق بعنوان الـ IP العام للخادم الوكيل عبر إضافة سجل DNS من النوع A، يمكنك الحصول على تفاصيل إضافية حول الموضوع بالاطلاع على القسم الخاص بخدمة اسم النطاق DNS على أكاديمية حسوب. خدمة تخزين كائني متوافقة مع S3 ومع إضافة django-storages مثل DigitalOcean Space وظيفتها تأمين المساحة التخزينية اللازمة لملفات تطبيق جانغو الساكنة، مع مجموعة مفاتيح الوصول الخاصة بإدارة هذه المساحة، استرشد بكيفية إعداد مساحة على DigitalOcean Space وعدّل بما يلائم خدمة التخزين التي اخترتها. نظام إدارة قواعد بيانات متوافق مع جانغو مع إنشاء قاعدة بيانات ومستخدم خاص بالتطبيق، نحن اخترنا خادم PostgreSQL، اهتم بالتفاصيل التالية أثناء إنجازك لهذه النقطة لتتمكن من متابعة خطوات المقال: استخدم الاسم polls لقاعدة البيانات والاسم sammy لمستخدم قاعدة البيانات (هذه الأسماء ليست ملزمة ولكننا استخدمناها في المقال واستخدامك لها سيسهل عليك المتابعة)، لمزيد من التفاصيل حول الإنشاء استرشد بالخطوة /1/ من مقالنا السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]()، مع العلم أن هذه المهمة يمكن إنجازها من أي خادم من الخوادم الثلاثة لبيئة العمل. اعتمدنا في المقال على نظام إدارة قواعد البيانات من نوع Managed PostgreSQL cluster الذي توفره DigitalOcean. لكن يمكنك تثبيت برنامج PostgreSQL وتشغيله وإعداده بنفسك مع الاستعانة بالفيديو التعليمي تثبيت وإعداد قاعدة بيانات PostgreSQL. الخطوة 1: إعداد خادم التطبيق الأول تتلخص العملية باستنساخ مستودع جانغو على الخادم الأول، ومن ثم بناء صورة التطبيق مع كل الإعدادات اللازمة لذلك وأخيرًا اختبارها وتشغيلها باستخدام حاوية دوكر. تنويه: في حال كنت مستمرًا معنا من المقال السابق [بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر]() فهذه الخطوة ستكون جاهزة لديك ويمكنك الانتقال للخطوة /2/ وهي تجهيز الخادم الثاني. سنبدأ بتسجيل الدخول إلى خادم التطبيق الأول واستنساخ polls-docker من قسم تطبيقات جانغو Polls في مستودع GitHub فهذا القسم يتضمن عينات تجريبية من التطبيق Polls مجهزة خصيصًا لتوثيقات جانغو التعليمية. أما القسم polls-docker من المستودع فيتضمن نسخًا معدّلة مسبقًا من تطبيق Polls ومجهزة للعمل بفعالية في بيئة الحاويات، راجع مقالنا السابق بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر لمزيد من المعلومات. إليك تعليمة الاستنساخ اللازم تنفيذها: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه بعدها إلى المجلد django-polls عبر الأمر التالي: cd django-polls يتضمن هذا المجلد كود جانغو المكتوب بلغة بايثون، ويحتوي أيضًا الملف Dockerfile الذي سيستخدمه دوكر لبناء صورة الحاوية، بالإضافة إلى الملف env الذي يتضمن متغيرات البيئة التي ستمرر للحاوية من بيئة التشغيل المحيطة. استعرض الملف Dockerfile عبر تعليمة لينكس cat: cat Dockerfile وستحصل على الخرج التالي وهو عبارة عن البيانات المكتوبة ضمن الملف Dockerfile: FROM python:3.7.4-alpine3.10 ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi"] انظر تعليمات الملف السابق بالترتيب وتابع الشرح الآتي. سيستخدم الملف صورة دوكر بإصدار بايثون 3.7.4، ويثبت اعتماديات وحزم بايثون اللازمة لكل من جانغو وخادم gunicorn وفق ملف متطلبات التطبيق django-polls/requirements.txt ومن ثم يحذف الملفات غير اللازمة بعد انتهاء التثبيت، وينسخ بعد ذلك كود التطبيق إلى الصورة ويسند قيمة المتغير PATH، وأخيرًا سيحدد البوابة 8000 لاستقبال حركة البيانات الواردة إلى الحاوية، ويشغل خادم gunicorn مع عمال workers عدد /3/ عبر البوابة 8000. لننتقل إلى بناء صورة الحاوية باستخدام docker build: docker build -t polls . سميّنا الصورة polls باستخدام الراية t- ومررنا المسار الحالي . كمسار context للتعليمة build فهو يتضمن كافة الملفات اللازمة لعملية البناء. بعد انتهاء عملية البناء استخدم الأمر docker images لاستعراض الصور المتاحة: docker images وستحصل على الخرج التالي: REPOSITORY TAG IMAGE ID CREATED SIZE polls latest 80ec4f33aae1 2 weeks ago 197MB python 3.7.4-alpine3.10 f309434dea3a 8 months ago 98.7MB تشغيل الصورة هي الخطوة التالية لعملية البناء، إلا أنها تتطلب ضبط متغيرات البيئة الموجودة ضمن الملف env وهو أحد لوازم تعليمة التشغيل docker run، لنعدّل عليه: افتح الملف env الموجود ضمن المجلد django-polls باستخدام محرر النصوص nano: nano env سيظهر أمامك الملف بالشكل التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info كما ترى فبعض محددات الملف قيمها موجودة والبعض الآخر يحتاج إلى ضبط بما يناسب بيئتك، سنذكر المحددات التي تحتاج إلى ضبط بالترتيب: المفتاح السري DJANGO_SECRET_KEY: اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات Docker نقدم لك في الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces إحدى طرق جانغو لتوليد المفاتيح. DJANGO_ALLOWED_HOSTS: أسند له اسم النطاق الخاص بك your_domain.com، أو يمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهذا الرمز يسمح لأي عنوان IP بالوصول، يحميك هذا المحدد من الهجمات المعتمدة على حقن ترويسة HTTP وفي حال أردت المزيد من المعلومات اطلع على توثيقات جانغو الخاصة بهذا المحدد ضمن Core Settings DATABASE_USERNAME: ضع اسم مستخدم قاعدة البيانات PostgreSQL الذي أنشأته أثناء إعدادك لمتطلبات العمل. DATABASE_NAME: ضع القيمة polls أو الاسم الذي اخترته لقاعدة البيانات أثناء إعدادك لمتطلبات العمل. كذلك الأمر لكلمة المرور DATABASE_PASSWORD. ضع اسم مضيف قاعدة البيانات DATABASE_HOST ورقم بوابة الاتصال معها DATABASE_PORT أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. احفظ التغيرات على الملف env بعد الانتهاء وأغلقه استعدادًا لتنفيذ أمر تشغيل الحاوية عبر التعليمة docker run التي سنكتبها بطريقة تسمح بتجاوز CMD (التي تحدد افتراضيات بدء التشغيل) وتنشئ أثناء التشغيل مخطط قاعدة البيانات باستخدام كل من manage.py makemigrations و manage.py migrate وفق ما يلي: docker run --env-file env polls sh -c "python manage.py makemigrations && python manage.py migrate" لنشرح الأمر السابق، شغلنا صورة الحاوية polls:latest ومررنا لها متغيرات البيئة التي ضبطناها للتو ضمن الملف env، وتجاوزنا الأمر الافتراضي في ملف Dockerfile لننفذ الأمر التالي بدلًا منه الذي أنشأ مخطط قاعدة البيانات بالمواصفات المحددة في كود التطبيق: sh -c "python manage.py makemigrations && python manage.py migrate" إن كنت تنفذ docker run للمرة الأولى فستحصل على الخرج التالي: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK مع العلم أن جانغو لن يجري أي عملية عندما تنفذ migrate في المرات القادمة طالما لم يطرأ أي تغيير على مخطط قاعدة بيانات التطبيق. نفذ بعدها أمر تشغيل جديد للحاوية مع استدعاء سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env polls sh تظهر بنتيجة هذا الأمر نافذة الصدفة shell ضمن الحاوية اكتب فيها تعليمة إنشاء المستخدم الخاص بتطبيق جانغو: python manage.py createsuperuser استكمل بياناته وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، واضغط بعدها على الاختصار Ctrl+D للخروج من الحاوية وإنهاء عملها. أما العملية الأخيرة فهي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني وتتم وفق الأمر التالي علمًا أنها قد تستغرق بعض الوقت: docker run --env-file env django-polls:v0 sh -c "python manage.py collectstatic --noinput" يظهر بعدها الخرج التالي: 121 static files copied. يمكنك الآن تشغيل التطبيق: docker run --env-file env -p 80:8000 polls ستحصل على الخرج: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 مع العلم أن التشغيل هذه المرة وفق الإعدادات الافتراضية المنصوص عليها في ملف Dockerfile وهي: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وأن البوابة أو المنفذ 8000 للحاوية فُتحت لتستقبل حركة البيانات القادمة من المنفذ 80 للخادم أوبونتو. يمكنك الآن كتابة عنوان URL الخاص بالتطبيق polls بمتصفح الإنترنت واستعراضه، لاحظ أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان: http://APP_SERVER_1_IP/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / لذا اكتب العنوان التالي لتحصل على واجهة polls: http://APP_SERVER_1_IP/polls ** تنويه **: إذا كنت تستخدم جدارًا ناريًا من النوع UFW فإنك ستلاحظ وجود ثغرة أمنية في عمل حاوية دوكر وهي أن دوكر يتجاهل كافة السياسات الأمنية الموجودة على هذا النوع من الجدران النارية ويسمح بالاتصالات بغض النظر عنها، فقد مكّن الاتصال مع البوابة 80 على الخادم دون أن نضبط مسبقًا أي إعداد على الجدار الناري، هذه الثغرة في الواقع موثقة على GitHub Issue وتعالجها الخطوة /5/ من مقالنا عبر تصحيح إعدادات الجدار الناري UFW، أما في حال كنت تستخدم جدارًا ناريًا أقوى وأكثر تطورًا كالذي توفره DigitalOcean’s Cloud Firewalls على سبيل المثال يمكنك تخطي هذا التحذير. اكتب الرابط التالي http://APP_SERVER_1_ip/admin للوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. تذكر أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع هذه الخطوات لاختبار جودته وأيضًا التأكد من صحة إحضار الملفات. بعد أن تنتهي من التصفح اضغط Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لتنهي عمل الحاوية ثم شغلها مجددًا بالنمط المنفصل detached الذي يسمح لها أن تعمل بالخلفية ويمكنك من تسجيل الخروج من جلسة ssh. اكتب الأمر التالي لتشغيل النمط المنفصل: docker run -d --rm --name polls --env-file env -p 80:8000 polls تشير الراية d- إلى النمط المنفصل، بينما تشير الراية rm-- إلى تنظيف نظام ملفات الحاوية بعد إغلاقها، أما polls فهو اسم الحاوية. ننهي بذلك إعداد خادم التطبيق الأول سجل الخروج منه، وافتح رابط التطبيق من المتصفح للمرة الأخيرة للتأكد من عمله بشكل سليم قبل انتقالنا للخطوة /2/ الخاصة بتجهيز الخادم الثاني. الخطوة 2: إعداد خادم التطبيق الثاني سنعرض الأوامر باختصار في هذه الخطوة نظرًا للتشابه الكبير بين إعداد هذا الخادم وخادم التطبيق الأول الذي شرحناه في الخطوة /1/، لنباشر التطبيق العملي. سجل الدخول إلى خادم التطبيقات الثاني وابدأ باستنساخ القسم polls-docker من مستودع django-polls على GitHub: git clone --single-branch --branch polls-docker https://github.com/do-community/django-polls.git توجه إلى المسار التالي: cd django-polls أنشئ صورة التطبيق باستخدام docker build: docker build -t polls . افتح الملف env بمحرر النصوص الذي تفضله: nano env لتحصل على الخرج التالي: DJANGO_SECRET_KEY= DEBUG=True DJANGO_ALLOWED_HOSTS= DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME= DATABASE_PASSWORD= DATABASE_HOST= DATABASE_PORT= STATIC_ACCESS_KEY_ID= STATIC_SECRET_KEY= STATIC_BUCKET_NAME= STATIC_ENDPOINT_URL= DJANGO_LOGLEVEL=info اضبط قيم الملف بما يناسب بيئتك ثم احفظ التغييرات على الملف وأغلقه. وأخيرًا شغل حاوية التطبيق بالنمط المنفصل كما يلي: docker run -d --rm --name polls --env-file env -p 80:8000 polls وافتح التطبيق باستخدام المتصفح للتأكد من سلامة عمله: http://APP_SERVER_2_IP/polls لاحظ الفرق الذي أحدثه النمط المنفصل فيمكنك الآن تسجيل الخروج من الخادم الثاني دون إنهاء عمل الحاوية فهي تعمل في الخلفية. أنهينا تجهيز خادمي تطبيق جانغو لننتقل للخطوة التالية ونجهز حاوية الوكيل العكسي. الخطوة 3: إعداد حاوية دوكر للخادم Nginx يتمتع خادم الويب Nginx بالعديد من المزايا أهمها قدرته على أداء وظائف متعددة مثل الوكيل العكسي وموازنة الحمل والتخزين لملفات الموقع، سيؤدي منها في هذه البيئة وظيفة الوكيل العكسي لحماية الواجهات الخلفية لخادمي جانغو وموازن الحمل لتوزيع الطلبات بينهما، كما سيؤمن تشفير الاتصالات مع خوادم التطبيق باستخدام شهادة TLS التي يوفرها Certbot ما يعني أنه سيجبر العميل على استخدام HTTPS عبر إعادة توجيه كافة الطلبات من نوع HTTP إلى المنفذ 443، ومن ثم إيصال طلبات HTTPS إلى خوادم جانغو بعد فك تشفيرها، أما وظيفة التخزين فلن نحتاجها إذ الآلية التي استخدمناها لإفراغ ملفات جانغو على وحدة التخزين الكائني تفي بالغرض. ونوّد أن نبين لك أن التصميم الذي اخترناه في هذا المقال والمؤلف من خادمي تطبيق كل منهما في حاوية مع حاوية ثالثة تتضمن Nginx هو واحد من عدة خيارات متاحة لكل منها مزايا مختلفة من حيث الأداء والحماية، فعلى سبيل المثال كان الممكن بناء Nginx ضمن إحدى حاويتي التطبيق ليقوم بدور الوكيل محليًا لخادم التطبيق الموجود معه في الحاوية نفسها وللخادم في الحاوية الثانية، وفي احتمالٍ آخر يمكنك بناء حاويتي Nginx فتصبح البنية حاوية Nginx مقابل كل حاوية تطبيق ومن ثم استخدام موازن حمل سحابي من أي مزود خدمة مثل DigitalOcean، أما المفاضلة بين هذه التصاميم المختلفة أنجزها بناءً على تحليل متطلباتك ونتائج اختبار الحمل Load Test الذي يبين لك نقاط الاختناق في كل بنية لتوسعها، فبنية مقالنا مثلًا تتمتع بالمرونة لجهة قابليتها للتوسع بإضافة حاوية Nginx ثانية أو أكثر في أي مرحلة تظهر فيها اختناقات الشبكة بسبب خادم Nginx الوحيد، كذلك يمكننا الاعتماد على موزان حمل سحابي بسرعة أعلى أو موازن حمل من رتبة الطبقة الرابعة L4 مثل HAProxy. والآن بعد أن أعطينا فكرة عن الاحتمالات المتعددة لتصميم البنية لنرجع إلى تطبيقنا العملي. سجل الدخول إلى الوكيل العكسي Nginx وأنشئ مسارًا يدعى config كما يلي: mkdir conf ومن ثم أنشئ ملفًا نصيًا باسم nginx.conf باستعمال أحد محررات النصوص مثل nano: nano conf/nginx.conf والصق ضمن هذا الملف الإعدادات التالية: upstream django { server APP_SERVER_1_IP; server APP_SERVER_2_IP; } server { listen 80 default_server; return 444; } server { listen 80; listen [::]:80; server_name your_domain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name your_domain.com; # SSL ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; client_max_body_size 4G; keepalive_timeout 5; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } location ^~ /.well-known/acme-challenge/ { root /var/www/html; } } جهزنا الإعدادات السابقة بالاعتماد على عدة مصادر تتحدث عن إعداد Nginx مثل GitHub\Certbot و Gunicorn و Docker\Nginx ودراستها وتعديلها لتناسب البيئة، هذه العملية في الواقع خارج نطاق مقالنا لذا ننصحك بالاطلاع على المقال فهم بنية ملف إعدادات nginx وسياقات الإعدادات من أكاديمية حسوب والمقال الذي يتحدث عن خوارزميات اختيار كتل Server و Location ضمن الملف من DigitalOcean لتفهم المزيد عن الإعدادات، أو الاستعانة ببعض الأدوات الخاصة بتجهيز ملف Nginx مثل NGINXConfig على سبيل المثال لا الحصر. لنشرح الآن دلالات هذه الإعدادات وكيفية عملها حيث أنها تتضمن السياقات الرئيسية upstream و server و location وهي تُعنى بإعادة توجيه طلبات HTTP إلى HTTPS ومن ثم إيصالها إلى خادمي التطبيق مع موازنة الحمل بينهما. سياقات الملف conf/nginx.conf بالترتيب هي: سياق الخوادم العليا upstream وهو وحيد، يليه ثلاثة سياقات متتالية تسمى سياق الخادم server، وأخيرًا سياق الموقع location وهو مكرر مرتين متتاليتين في نهاية الملف. يحدد سياق الخوادم العليا upstream الخوادم التي ستتلقى طلبات الوكيل عبر عملية التوجيه proxy_pass: upstream django { server APP_SERVER_1_IP; server APP_SERVER_2_IP; } . . . أطلقنا الاسم django على هذا السياق وعرفنا ضمنه خادمي التطبيق عبر كتابة عنوان الـ IP الخاص بكل خادم، أما في حال كنت تستخدم شبكة سحابية افتراضية خاصة VPC للربط بين الخادمين فاستبدل هذه العناوين بعناوين الخوادم الخاصة Private IP وفق إعدادات الشبكة VPC، ويمكنك الاستعانة بالمثال لمزيد من التفاصيل حول تفعيل هذه الشبكة. بعد توجيه الطلبات من سياق الخوادم العليا، يلتقط أول سياق خادم الطلبات التي لا تخص نطاقك وينهيها، وتعدّ طلبات HTTP المباشرة لخوادم التطبيق مثالًا على الحالات التي يتعامل معها هذا السياق: . . . server { listen 80 default_server; return 444; } . . . أما ثاني سياق من نوع خادم فهو يتلقى الطلبات الواردة إلى نطاقك عبر HTTP ويعيد توجيهها إلى HTTPS عبر توجيه HTTP 301، ليعالجها بعد ذلك سياق الخادم الأخير: . . . server { listen 80; listen [::]:80; server_name your_domain.com; return 301 https://$server_name$request_uri; } . . . تحدد أسطر التوجيه التالية مسارات شهادة TLS والمفتاح الخاص Secret Key، علمًا أن Certbot سيؤمن هذه الملفات، ومن ثم ستُوصل إلى داخل الحاوية عبر التعليمة mount كما سنرى في المراحل التالية: . . . ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem; . . . أما محددات اتصال SSL فتُضبط وفقًا للقيم الافتراضية التي توصي بها Certbot كما يلي: . . . ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; . . . إن رغبت بمعلوماتٍ إضافية عن هذه المحددات وطريقة ضبطها فإننا نرشّح لك مقالين حول الموضوع من Nginx و Mozilla. استرشدنا بتوثيق Gunicorn الخاص بإعدادات Nginx لضبط عدد الاتصالات الأعظمي المسموح به client_max_body_size وزمن جلسة الاتصال keepalive_timeout وهي المدة الزمنية العظمى التي يتم بعدها قطع الاتصال مع العميل وتقدر بالثانية: . . . client_max_body_size 4G; keepalive_timeout 5; . . . لننتقل الآن للسياقات الأخيرة ضمن الملف وهي سياقات الموقع. يوجه أول سياق موقع Nginx أثناء تلقيمه وعكسه الطلبات إلى خوادم التطبيق المعرّفة ضمن سياق الخوادم العليا upstream وذلك عبر بروتوكول HTTP، مع المحافظة على معلومات ترويسة HTTP لاتصال العميل مثل عنوان الـ IP الأصلي وبروتوكول الاتصال المستخدم وعنوان الهدف: . . . location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } . . . اطلع على توثيق Gunicorn وتوثيق النموذج ngx_http_proxy_module لمزيدٍ من المعلومات. يُسجل سياق الموقع الثاني الطلبات ضمن المسار /well-known/acme-challenge/ لاستخدامها من قبل Certbot للتحقق من اسم النطاق وفق تحدي HTTP-01 وهو أحد أنواع تحديات Let’s Encrypt التي تستخدمها للاختبار والتحقق. . . . location ^~ /.well-known/acme-challenge/ { root /var/www/html; } أغلق الآن ملف الإعدادات بعد حفظ التغييرات، حيث أنه سيُستخدم لتشغيل الحاوية. لننفذ الآن أمر تشغيل الحاوية وفق الصيغة التالية، بالاعتماد على الصورة nginx:1.19.0 وهي إحدى صور دوكر الرسمية المخصصة لخادم Nginx: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /var/www/html:/var/www/html \ nginx:1.19.0 ستلاحظ أثناء التشغيل ظهور خطأ مفاده عدم وجود الشهادات TLS المذكورة ضمن ملف الإعدادات nginx.conf، ومع ذلك سيكمل دوكر بناء الحاوية وتشغيلها، أما الخطأ سيُعالج في الخطوة التالية بتوفير الشهادات المطلوبة باستخدام Certbot و Let’s Encrypt. بالرجوع لأمر التشغيل سترى أننا أطلقنا الاسم nginx على الحاوية، وربطنا المنفذ 80 والمنفذ 443 للمضيف بالمنافذ التي تحمل الأرقام نفسها في الحاوية، واستخدمنا الراية v- لوصل ملف الإعدادات nginx.conf الذي ضبطناه مسبقًا إلى المسار etc/nginx/conf.d/nginx.conf/ داخل الحاوية بحيث تعتمده الصورة مع الانتباه إلى التعليمة تضمنت الراية ro التي تشير إلى "read only" أي أن الملف سيكون للقراءة فقط بالنسبة الحاوية فلا يمكن تعديله من داخلها، بالإضافة لذلك وصلنا مسار الجذر لتطبيق الويب إلى المسار var/www/html/ المقابل ضمن الحاوية، أما السطر الأخير في أمر التشغيل وهو nginx:1.19.0 فسيرشد دوكر إلى الصورة المطلوبة على Dockerhub لاستخدامها. الخطوة 4: إعداد Certbot وتجديد الشهادة من Let’s Encrypt Certbot هو عميل Let’s Encrypt الأشهر طورته شركة Electronic Frontier Foundation، وظيفته تزويد خوادم الويب بشهادات TLS مجانية مصدقة من Let’s Encrypt تعطي للمتصفحين الثقة بهوية الموقع. لنبدأ بالتطبيق العملي انطلاقًا من صورة Certbot المتوفرة على Dockerhub، ولكن تأكد أولًا من وجود سجل DNS من النوع A يرتبط بعنوان Public IP للخادم الوكيل، ثم نفذ أمر التشغيل التالي ليوفر عميل certbot الشهادات اللازمة للاختبار staging مع خوادم Let’s Encrypt: docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone --staging -d your_domain.com يشغل الأمر السابق الصورة certbot في الوضع التفاعلي، ويربط البوابة 80 على الخادم المضيف مع البوابة 80 ضمن الحاوية، كما أنه ينشئ المسارين /etc/letsencrypt/ و /var/lib/letsencrypt/ ويوصلهما بنفس المسارات على المضيف. سيعمل Certbot بموجب الأمر السابق بالنمط المستقل أي بدون خادم Nginx ويتصل بخوادم Let’s Encrypt لإنجاز اختبار التشفير والتحقق من النطاق، وأثناء ذلك سيُطلب منك عنوان بريد إلكتروني وتأكيد الموافقة على شروط الخدمة، أدخل المطلوب وانتظر اكتمال العملية وفي حال نجح التحقق من نطاقك ستحصل على الخرج التالي الذي يخبرك بمكان تخزين الشهادة: Obtaining a new certificate Performing the following challenges: http-01 challenge for stubb.dev Waiting for verification... Cleaning up challenges IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/your_domain.com/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/your_domain.com/privkey.pem Your cert will expire on 2020-09-15. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. يمكنك استعراض ملف الشهادة باستخدام cat: sudo cat /etc/letsencrypt/live/your_domain.com/fullchain.pem سنعيد الآن تشغيل الحاوية nginx بنفس أمر التشغيل المستخدم في الخطوة /3/ ولكن مع إضافة مسارات Let’s Encrypt التي بنيناها توًا أثناء تشغيل حاوية certbot: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 فور إقلاع الحاوية nginx يمكنك أن تجرب فتح التطبيق من خلال المتصفح بوضع الرابط http://your_domain.com وستلاحظ وجود تحذير أمني مفاده انتهاء صلاحية الشهادة وهذا متوقع لأننا ما زلنا نعمل على الشهادات المرحلية الاختبارية staging، ولكن مع ذلك ستجد أن طلبك قد تم إعادة توجيهه من HTTP إلى HTTPS. اضغط الآن على الاختصار Ctrl+c للخروج من حاوية Nginx وشغل عميل Cerbot مجددًا ولكن مع حذف الراية الخاص بالاختبار staging--: docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone -d your_domain.com ستظهر لك رسالة لاختيار الإجراء الذي ترغب بتنفيذه تجديد الشهادة أو استبدالها، اضغط على الرقم 2 لخيار التجديد ومن ثم زر الإدخال Enter وستحصل بذلك على شهادة TLS فعالة production، ثم شغل الآن حاوية nginx بالاعتماد على هذه الشهادة وفق التالي: docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 استعرض التطبيق مجددًا بكتابة الرابط في المتصفح: http://your_domain.com لاحظ إعادة توجيه طلبك إلى HTTPS وظهور رسالة "لم يتم العثور على الصفحة" بسبب عدم إعداد توجيه افتراضي للتطبيق Polls، استخدم إذًا الرابط التالي: https://your_domain.com/polls وذلك لاستعراض واجهة التطبيق التي تشبه الصورة التالية: زودنا تطبيقنا لغاية الآن بشهادة TLS عبر حاوية Certbot ووكيل عكسي وموازن حمل لتوجيه وموازنة الطلبات الخارجية الواردة إلى خوادم التطبيق، و قبل الانتقال للخطوة /5/ الختامية يتبقى لدينا موضوع أخير وهو تجهيز آلية مناسبة لتجديد صلاحية TLS حيث أن الشهادات التي تعطيها Let’s Encrypt تحتاج إلى تجديد دوري كل 90 يومًا. سنبدل أولًا نمط عمل Certbot من النمط المستقل standalone إلى النمط webroot ذلك أن بيئتنا تضمن خادم ويب Nginx، وهذا يعني أن عميل Certbot سينشئ الملف الخاص بعملية التحقق ضمن مسار الجذر لخادم الويب التالي: /var/www/html/.well-known/acme-challenge/ كذلك ستصل طلبات التحقق من Let’s Encrypt لهذا المسار وفق قواعد التوجيه الموجودة في سياقات الموقع ضمن ملف إعدادات Nginx، وعند إتمام عملية التجديد سنحتاج إلى إعادة تحميل Nginx ليستخدم الشهادات الجديدة الصالحة. يلجأ المستخدمون عادةً إلى أتمتة هذه العملية حتى ينجزها Certbot تلقائيًا وذلك بعدة أساليب أشهرها إضافة العملية على قائمة المهام المجدولة لنظام التشغيل لينكس عبر تعليمة cron، يمكنك الاطلاع على هذه العملية ضمن مقالٍ آخر حول تأمين Node.js من DigitalOcean. لننهي عمل حاوية Nginx بالضغط على الاختصار Ctrl+c ونعيد تشغيلها في الوضع المنفصل مع الراية d- وفق التالي: docker run --rm --name nginx -d -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.19.0 بينما يعمل خادم Nginx في الخلفية، سنشغل الحاوية certbot مع كل من إضافة webroot-- التي ستحدد مسار الجذر لخادم الويب، والراية dry-run-- لمحاكاة عملية التجديد والتأكد أن كافة الخيارات صحيحة دون القيام بالتجديد فعلًا: docker run -it --rm --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -v "/var/www/html:/var/www/html" \ certbot/certbot renew --webroot -w /var/www/html --dry-run وفي حال كانت المحاكاة سليمة ستحصل على الخرج التالي: Cert not due for renewal, but simulating renewal for dry run Plugins selected: Authenticator webroot, Installer None Renewing an existing certificate Performing the following challenges: http-01 challenge for your_domain.com Using the webroot path /var/www/html for all unmatched domains. Waiting for verification... Cleaning up challenges - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - new certificate deployed without reload, fullchain is /etc/letsencrypt/live/your_domain.com/fullchain.pem - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates below have not been saved.) Congratulations, all renewals succeeded. The following certs have been renewed: /etc/letsencrypt/live/your_domain.com/fullchain.pem (success) ** DRY RUN: simulating 'certbot renew' close to cert expiry ** (The test certificates above have not been saved.) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - يتوجب علينا بعد ذلك إعادة تحميل Nginx ليستخدم الشهادة المجددة وذلك وفق الأمر التالي: docker kill -s HUP nginx حيث ترسل التعليمة HUP إشارة إلى خادم Nginx داخل الحاوية ليعيد تحميل الإعدادات ويستخدم الإعدادات الجديدة بما فيها الشهادات المجددة. لننتقل الآن للخطوة الأخيرة وهي حماية خوادم الواجهات الخلفية للتطبيق عبر تقيّد الاتصالات الخارجية معها بالخادم الوكيل فقط. الخطوة 5: منع الوصول الخارجي إلى خادمي تطبيق جانغو في البنية الموصوفة في هذا المقال يتولى خادم Nginx فك تشفير اتصالات SSL ويوصل البيانات بدون تشفير إلى خوادم جانغو، تعدّ هذه الآلية مناسبة في العديد من الحالات إلا أن بعض التطبيقات (المالية مثلًا) قد تتطلب مستويات أمان أعلى فيعتمد مشغلوّها تقنية التشفير طرف إلى طرف end-to-end، وهو ما يمكنك تنفيذه بإيصال حزم البيانات مشفرة إلى خوادم التطبيق ليُفك تشفيرها هناك، أو بإعادة تشفيرها ثانيةً عند الخادم الوكيل وفك التشفير مجددًا على خوادم تطبيق جانغو، لن نتناول هذه التقنية من التشفير في مقالنا ولكن يمكنك الحصول على المزيد من المعلومات حولها بمراجعة التشفير طرف-إلى-طرف. إذًا الخادم Nginx في بنيتنا هو بوابة العبور بين الاتصالات الخارجية وخادمي جانغو ومن المفترض نظريًا أن لا يتصل أي عميل مع خوادم التطبيق إلا عبر هذه البوابة، ولكن تذكر الثغرة الأمنية المفتوحة التي واجهتنا في الخطوة /1/ حيث تمكن دوكر من تجاوز الجدار الناري UFW وفتح بوابات خارجية دون إعداد أي سياسات صريحة لفتح هذه البوابات على الجدار الناري. سنعالج هذه الثغرة عبر تعديل إعدادات الجدار الناري غير المعقد UFW مع العلم أن بإمكانك معالجتها بتعديل iptables الخاصة بنظام التشغيل مباشرةً بحيث تمنع الوصول غير المصرح به للحاوية دوكر، ويمكنك تعلم المزيد من مقال التّشبيك والتّواصُل على Docker ومقال أساسيات IPTables - قواعد وأوامر شائعة للجدار الناري، لكننا ننصحك باستخدام جدران نارية أكثر تطورًا في البيئة الحقيقية كالجدران السحابية مثلًا. لنبدأ الآن بتعديل إعدادات الجدار الناري UFW المشروحة في مستودع ufw-docker في GitHub لمنع الاتصالات الخارجية مع بوابات المضيف التي فتحها دوكر، فقد مررنا الراية p 80:8000- لحاويات خوادم التطبيق أثناء تشغيلها، ما يعني أن البوابة 80 على المضيف فُتحت ووُجّهت إلى البوابة 8000 على الحاوية وهي بذلك أصبحت متاحة أمام الاتصالات الخارجية أيضًا حيث أنها تمكننا من فتح الرابط: http://your_app_server_1_IP سجل الدخول إلى خادم التطبيق الأول وافتح الملف etc/ufw/after.rules/ بامتيازات sudo: sudo nano /etc/ufw/after.rules أدخل كلمة المرور واضغط زر الإدخال Enter وستظهر لك محتويات الملف after.rules وهي الإعدادات الخاصة بالجدار الناري UFW: # # rules.input-after # # Rules that should be run after the ufw command line added rules. Custom # rules should be added to one of these chains: # ufw-after-input # ufw-after-output # ufw-after-forward # # Don't delete these required lines, otherwise there will be errors *filter :ufw-after-input - [0:0] :ufw-after-output - [0:0] :ufw-after-forward - [0:0] # End required lines # don't log noisy services by default -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input # don't log noisy broadcast -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input # don't delete the 'COMMIT' line or these rules won't be processed COMMIT الصق التعليمات التالية في نهاية الملف السابق (ومصدرها مستودع ufw-docker كما ذكرنا): . . . # BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER تُقيّد التعليمات السابقة الوصول العام إلى البوابات التي فتحها دوكر وتسمح بالوصول لعناوين الشبكة الخاصة فقط وهي هنا 10.0.0.0/8 و 172.16.0.0/12 و 192.168.0.0/16 في حال كنت تستخدم الشبكات الخاصة مثل VPC وبذلك فإن Droplets البوابة الخاصة بهذه الشبكة ستتمكن من الوصول إلى البوابات المفتوحة بينما تُمنع المصادر الخارجية من ذلك، لتعرف المزيد عن إعدادات UFW في هذه الحالة انظر ufw-docker how-it-works. وفي حال أنك وضعت العناوين العامة Public IP لكل من خادمي التطبيق ضمن سياق الخوادم العليا في ملف إعدادات Nginx ولم تعتمد VPC، فعليك إذًا تعديل إعدادات UFW والسماح صراحةً بحركة البيانات القادمة من الخادم الوكيل Nginx إلى خوادم التطبيق عبر البوابة 80 وذلك عبر إنشاء سياسة خاصة بذلك باستخدام التعليمة allow، استعن لكتابتها بالمقال أساسيات IPTables - قواعد وأوامر شائعة للجدار الناري، واحفظ بعدها التغيرات وأغلق الملف. أعد تشغيل الجدار الناري ufw ليأخذ الإعدادات الجديدة عبر الأمر: sudo systemctl restart ufw تصفح بعدها موقع تطبيقك: http://APP_SERVER_1_IP ولاحظ أنه لم يعد متاحًا فقد منعت الاتصالات المباشرة مع خادم التطبيق عبر البوابة 80. سجل الخروج من الخادم الأول، وكرر الخطوات نفسها على خادم التطبيق الثاني وذلك عبر فتح الملف etc/ufw/after.rules/ بصلاحيات sudo: sudo nano /etc/ufw/after.rules ولصق التعليمات التالية في نهايته: . . . # BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER أغلق الملف بعد حفظ التغييرات، ومن ثم أعد تشغيل الجدار الناري: sudo systemctl restart ufw وتصفح موقع الخادم ولاحظ أنه لم يعد متاحًا فقد منعت الاتصالات المباشرة مع هذا الخادم أيضًا.: http://APP_SERVER_2_IP افتح الآن الرابط التالي للتأكد من استمرار قدرة الخادم الوكيل على الوصول إلى التطبيق: https://your_domain_here/polls ولاحظ أن الوصول ممكن وواجهة التطبيق ستظهر أمامك في المتصفح. الخاتمة قدمنا لك هذا المقال نموذجًا لإعداد تطبيق جانغو في بيئة حاويات موزعة ومرنة قابلة للتوسيع بعدة طرق -في حال دعت الحاجة وازداد الطلب على التطبيق- منها توسيع خوادم تطبيق جانغو أو الخادم الوكيل أو قاعدة البيانات. ونقترح عليك بعض الأفكار لتطوير إدارة هذه البيئة منها استخدام أداة التمهيد systemd لإدارة نظامك بشكلٍ أفضل والاطلاع على توثيق دوكر الخاص بالتشغيل التلقائي للحاويات لخيارات متخصصة في التشغيل التلقائي للحاويات، والاستعانة بإحدى أدوات تكوين الحاويات مثل Ansible أو Chef لتسهيل العمل في بعض البيئات التي يستخدم فيها أكثر من مضيف الحاوية نفسها. دون أن ننسى إمكانية استخدام سجل الصور من Docker Hub وكذلك خط أنابيب pipeline لتسهيل نشر واختبار الصور على خوادم متعددة عوضًا عن إعادة بنائها وضبط إعداداتها مجددًا على كل خادم. ترجمة -وبتصرف- للمقال How To Scale and Secure a Django Application with Docker, Nginx, and Let's Encrypt لصاحبه Hanif Jetha. اقرأ أيضًا كيف تضبط جدار IPTables لحماية البيانات المنقولة بين خواديمك الهجمات الأمنية Security Attacks في الشبكات الحاسوبية رموز الإجابة في HTTP كيف تؤمّن خادم ويب NGINX
  8. يُعّد جانغو Django واحدًا من أقوى أطر العمل البرمجية التي تسهل عملية تطوير تطبيقات الويب المبنية باستعمال بايثون Python، وذلك لما يتمتع به من مميزاتٍ مفيدة مثل استخدامه تقنية ORM أي ربط الكائنات العلاقية object-relational mapper وتوفيره آلية للتحقق من هوية المستخدمين وواجهة مرنة قابلة للتخصيص لإدارة تطبيقك، بالإضافة إلى امتلاكه خاصية التخزين المؤقت وتشجيعه على تصميم الكود النظيف من خلال مصمم عناوين URL Dispatcher ونظام القوالب Template system. سنعرض في هذا المقال التعليمي كيفية بناء تطبيق جانغو اسمه Polls قابل للتوسع والنقل بحاويات دوكر Docker، وما يلزم من تعديلات ليعمل بفعالية داخل الحاوية ويتكيف مع متغيرات بيئة التشغيل، كما سنتطرق إلى ضبط تسجيل أحداث التطبيق، وتخزين أو تفريغ أصوله الساكنة static assets وهي صفحات جافاسكريبت وملفات التنسيق CSS في مساحة التخزين الكائني object storage لما له من أثر إيجابي على إدارتها إدارة مركزية سلسة ومنظمة بالأخص في بيئة متعددة الحاويات. وسننشئ بعد تنفيذ التعديلات المطلوبة -المستوحاة من منهجية Twelve-Factor الخاصة بتطوير تطبيقات ويب سحابية قابلة للتوسع والعمل ضمن الحاويات- على عينة من جانغو Polls صورة التطبيق image ونشغلّها ضمن حاوية دوكر. بالنتيجة ستغدو في نهاية هذا المقال قادرًا على وضع تطبيق جانغو في حاوية قابلة للنقل، وفي مقالات لاحقة ستتعلم استخدام Docker Compose مع جانغو وخادم Nginx كوكيل عكسي، ومن ثم تطبيق هذه البنى التي تعرفت عليها في حاويات عنقودية مثل كوبيرنتس Kubernetes. أخيرًا ننصحك بمتابعة خطوات العمل الواردة هنا بالتسلسل لضمان فهم كافة التغييرات التي سنُجريها على التطبيق، ولكن في حال رغبت بتخطي ذلك فيمكنك الحصول على كود التطبيق المعدل من القسم الخاص بتطبيقات polls-docker من مستودع GitHub. هذا المقال جزء من سلسلة قصيرة حول احتواء تطبيقات جانغو بحاويات وإدارتها وإليك فهرس السلسلة: بناء تطبيق جانغو بخادم Gunicorn ووضعه ضمن حاوية دوكر توسيع تطبيق جانغو وتأمينه عبر حاوية دوكر وخادم Nginx وخدمة Let's Encrypt نشر تطبيق جانغو آمن وقابل للتوسيع باستخدام كوبيرنتس متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي لهذا المقال: تهيئة خادم بنظام تشغيل أوبونتو (استعملنا في المقال إصدار 18.04)، يمكنك الاستعانة بالمقال التهيئة الأولية لخادم أوبونتو 18.04 لتهيئته. تثبيت دوكر على الخادم، استعن بمقال كيفية تثبيت دوكر واستخدامه على دبيان فآلية العمل متشابهة في النظامين، دون أن تنسى إضافة مستخدم أوبونتو الخاص بك إلى مجموعة العمل دوكر ليأخذ صلاحيات العمل اللازمة. توفير خدمة تخزين كائني متوافقة مع S3 تؤمن المساحة التخزينية اللازمة لملفات وأصول تطبيق جانغو الساكنة بالإضافة إلى مجموعة من مفاتيح الوصول لإدارة هذه المساحة، وقد استُخدمت خدمة DigitalOcean Space من ديجيتال أوشن DigitalOcean في هذا المقال ويمكنك اختيار خدمة التخزين من المزود الأنسب لك مع إجراء تغييرات بسيطة تلائمها على خطوات الإعداد. نظام إدارة قواعد بيانات متوافق مع جانغو لإنشاء قاعدة بيانات التطبيق، وقد اخترنا DigitalOcean Managed PostgreSQL cluster استرشد بخطواتنا مع تغيير ما يلزم لقاعدة البيانات الخاصة بك. الخطوة 1: إنشاء قاعدة البيانات PostgreSQL مع مستخدم قاعدة البيانات يتلخص ما سنقوم به في هذه الخطوة بثلاث نقاط هي الاتصال بخادم قواعد بيانات PostgreSQL من خادم أوبونتو، وإنشاء قاعدة بيانات ومستخدم ضمنها خاص بتطبيق جانغو، ومن ثم ضبط إعداداتها لتعمل بكفاءة مع التطبيق، ولتحقيق ذلك سنبدأ بتثبيت عميل قاعدة بيانات PostgreSQL من مستودع برامج أوبونتو ولكن بعد تحديث دليل مدير الحزم apt، وفق الأمرين: sudo apt update sudo apt install postgresql-client ثم نؤكد العملية بالضغط على الحرف Y وزر الإدخال Enter، وبعد إتمام التحميل والتثبيت ننتقل للمرحلة التالية وهي إنشاء قاعدة البيانات والمستخدم الخاص بتطبيق جانغو. تتطلب هذه العملية معرفة محددات الاتصال مع PostgreSQL cluster مثل عنوان المضيف واسم قاعدة البيانات ورقم البوابة كذلك اسم المستخدم وكلمة المرور، ويمكنك الحصول على كافة هذه البيانات من لوحة التحكم الخاصة بخدمة PostgreSQL cluster ضمن مربع خاص بمحددات الاتصال إن كنت تستخدم نظام إدارة قواعد بيانات سحابي (مثل DigitalOcean أو غيره). والآن باستخدام هذه المحددات ونسخة العميل من PostgreSQL التي سبق تثبيتها، يمكنك تسجيل الدخول إلى PostgreSQL cluster وفق الأمر التالي: psql -U username -h host -p port -d database --set=sslmode=require ستُطلب منك كلمة المرور، أدخلها، واضغط زر الإدخال Enter، لتنتقل إلى موجه أوامر PostgreSQL الذي يخولك التحكم بقاعدة البيانات. أنشئ قاعدة بيانات خاصة بمشروعك باسم polls وفق التعليمة التالية (مع التنويه إلى أن كافة تعليمات psql يجب أن تنتهي بفاصلة منقوطة): CREATE DATABASE polls; ثم توجه إلى قاعدة البيانات polls الخاصة بك كما يلي: \c polls; وأنشئ ضمنها مستخدم قاعدة بيانات لمشروعك باسم sammy مثلًا -أو أي اسم- كما يلي واحرص على حمايته بكلمة مرور قوية: CREATE USER sammy WITH PASSWORD 'password'; اضبط بعد ذلك القيم الافتراضية لمحددات اتصال المستخدم المنشأ بالتوافق مع توصيات جانغو بحيث لا يتطلب الأمر الاستعلام عنها وإعادة تعيينها مع كل تأسيس جديد للاتصال ما سينعكس إيجابًا على تسريع عمليات قاعدة البيانات، والمحددات المطلوب ضبطها هي: الترميز encoding الذي يأخذ القيمة الافتراضية UTF-8، وآلية عزل العمليات transaction isolation scheme الذي يضبط إلى القيمة "read committed" ومهمته منع قراءة العمليات غير المكتملة uncommitted transactions، وأخيرًا المنطقة الزمنية نثبتها على UTC، وفق التعليمات التالية: ALTER ROLE sammy SET client_encoding TO 'utf8'; ALTER ROLE sammy SET default_transaction_isolation TO 'read committed'; ALTER ROLE sammy SET timezone TO 'UTC'; يتبقى الإعداد الأخير وهو منح مستخدم قاعدة البيانات المنشأ صلاحيات الإدارة الكاملة لقاعدة البيانات: GRANT ALL PRIVILEGES ON DATABASE polls TO sammy; وبعدها اخرج من موجه أوامر PostgreSQL باستخدام: \q جهزنا بذلك قاعدة البيانات ليُديرها تطبيق جانغو، لننتقل للخطوة الثانية المتمثلة بنسخ كود التطبيق Polls من GitHub وتحديد حزم اعتماديات بايثون اللازمة له. الخطوة 2: استنساخ مستودع التطبيق والتصريح عن اعتماديات عمله سنستخدم لعملية الاستنساخ مستودع التطبيق django-polls الذي يتضمن الكود الكامل لتطبيق Polls المجهز لأغراض البرنامج التعليمي في مشروع جانغو. بدايةً سجل الدخول إلى خادم أوبونتو وأنشئ مجلدًا يدعى polls-project ومن ثم استخدم الأمر git لاستنساخ مستودع django-polls من GitHub: mkdir polls-project cd polls-project git clone https://github.com/do-community/django-polls.git انتقل إلى django-polls واستعرض محتوياته بتعليمة ls: cd django-polls ls وسينتج الخرج التالي: LICENSE README.md manage.py mysite polls templates الذي يبين لك محتويات تطبيق django-polls، وهي العناصر المبينة أدناه، نعددها لك بإيجاز ويمكنك تعلم المزيد عنها بالاطلاع على التوثيق الخاص بإنشاء المشاريع من توثيقات جانغو الرسمية: manage.py: أداة سطر الأوامر الرئيسية لتنفيذ أوامر معالجة التطبيق. polls: يتضمن أكواد التطبيق polls. mysite: يتضمن أكواد وإعدادات جانغو project-scope. templates: يتضمن قوالب خاصة بواجهة الإدارة. أنشئ بعدها ملفًا نصيًا باسم requirements.txt ضمن نفس المسار polls-project/django-polls الذي تكلمنا عنه، وافتحه باستخدام محرر النصوص والصق ضمنه اعتماديات بايثون التالية، ثم احفظ التغييرات على الملف وأغلقه: boto3==1.9.252 botocore==1.12.252 Django==2.2.6 django-storages==1.7.2 docutils==0.15.2 gunicorn==19.9.0 jmespath==0.9.4 psycopg2==2.8.3 python-dateutil==2.8.0 pytz==2019.3 s3transfer==0.2.1 six==1.12.0 sqlparse==0.3.0 urllib3==1.25.6 كما تلاحظ فقد أوردنا بدقة كافة اعتماديات بايثون التي يحتاج إليها المشروع ومن بينها إضافة django-storages الخاصة بتخزين ملفات وأصول الموقع الساكنة على مساحة التخزين الكائني، وإضافة gunicorn الخاصة بخادم WSGI، كذلك مُحول psycopg2 لمواءمة التخاطب مع قاعدة بيانات PostgreSQL. والآن بعد أن أنهينا استنساخ التطبيق وحددنا ما يلزمه من اعتماديات لنبدأ بتعديله ليصبح قابلًا للنقل. الخطوة 3: تكييف جانغو مع متغيرات البيئة توصي منهجية Twelve-Factor باستخراج بيانات الإعدادات المدمجة مع كود التطبيق التي لا يمكن تعديلها إلاّ بتعديل الكود، وفصلها عن الكود ما يسمح للتطبيق بالتكيّف وتغيير سلوكه تبعًا لمتغيرات بيئة التشغيل، يوافقها في ذلك توصيات إعداد الحاويات لدى كلٍ من دوكر وكوبيرنتس Kubernetes، لذا سنتبع هذا النمط ونعدّل ملف الإعدادات django-polls/mysite/settings.py الخاص بتطبيق جانغو -وهو وحدة بايثون- بحيث يحصل على البيانات من بيئة التشغيل الفعلية المحلية عوضًا عن قراءتها من الكود، وذلك باستخدام الدالة getenv مع الوحدة os. لنبدأ بالتطبيق عمليًا! سنجعل المحددات المدمجة مع الكود ضمن الملف settings.py تأخذ قيمها من متغيرات البيئة عبر استدعاء الدالة os.getenv وتحديد اسم المتغير المرتبط ضمن قوسين، مع العلم بإمكانية تمرير قيمة ثانية للدالة تسمى بالقيمة الافتراضية أو الاحتياطية تُرجعها الدالة في حال كان المتغير غير معرف، لاحظ التالي: … SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') DEBUG = os.getenv('DJANGO_DEBUG', False) ALLOWED_HOSTS = os.getenv('DJANGO_ALLOWED_HOSTS', '127.0.0.1').split(',') … سيبحث جانغو عن متغير بيئة يدعى DJANGO_SECRET_KEY ويسند قيمته إلى المتغير SECRET_KEY، مع العلم أننا لم نمرر له قيمة احتياطية يعتمدها في حال أن المتغير لم يكن معرفًا، ذلك أننا نريده موحدًا على مستوى جميع خوادم التطبيق، وبنفس الوقت لا يجب أن يكون مدمجًا مع الكود، فكان تعريفه خارجيًا دون قيمة احتياطية هو الحل الأنسب، حيث أن فشل تشغيل التطبيق بسبب عدم توفر المتغير يعدّ بالتأكيد خيارًا أفضل من تشغيله مع المشاكل التي يمكن أن تنجم عن استخدام قيم متباينة من المفتاح السري SECRET_KEY في نسخ التطبيق المختلفة. بنفس الطريقة للمتغير أو المحدد DEBUG سيبحث عن متغير بيئة يدعى DJANGO_DEBUG ويأخذ قيمته، وفي حال لم يكن معرفًا فإنه سيأخذ القيمة الاحتياطية false التي ستحمينا من فقدان أي بيانات قد تكون حساسة ما لم نضبط المتغير إلى قيمة true عمدًا. أما بالنسبة للمتغير أو المحدد ALLOWED_HOSTS فسيُحضر قيمة متغير البيئة المرتبط به DJANGO_ALLOWED_HOSTS ويأخذ قيمته بعد فصله إلى قائمة بايثون باستخدام الدالة ()split مع اعتماد , كفاصل، وفي حال أن المتغير لم يكن معرفًا يأخذ المحدد القيمة 127.0.0.1 الاحتياطية. فور انتهائك من تعديل المحددات الثلاثة السابقة انتقل لتعديل محدد قاعدة البيانات DATABASES ضمن نفس الملف بحيث يأخذ أيضًا قيمته الافتراضية default من متغيرات البيئة، وذلك وفق التالي: . . . # Database # https://docs.djangoproject.com/en/2.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format( os.getenv('DATABASE_ENGINE', 'sqlite3') ), 'NAME': os.getenv('DATABASE_NAME', 'polls'), 'USER': os.getenv('DATABASE_USERNAME', 'myprojectuser'), 'PASSWORD': os.getenv('DATABASE_PASSWORD', 'password'), 'HOST': os.getenv('DATABASE_HOST', '127.0.0.1'), 'PORT': os.getenv('DATABASE_PORT', 5432), 'OPTIONS': json.loads( os.getenv('DATABASE_OPTIONS', '{}') ), } } . . . لاحظ أننا استخدمنا json.loads لتعيين قيمة محدد قاعدة البيانات وفق الصيغة: DATABASES['default']['OPTIONS'] نستنتج من هذه الحالة أن متغيرات البيئة لن تكون دومًا سلاسل بسيطة سهلة القراءة فيمكن أن تأخذ لأنواعًا مختلفة من البيانات، ومن هنا تبرز أهمية المرونة التي يوفرها تمرير المتغيرات بملف JSON حتى ولو كانت على حساب الوضوح في قراءة الإعدادات. دون أن تنسى تفعيل مكتبة json ضمن ملف الإعدادات settings.py وفق التالي حتى تتمكن من استخدام هذا النوع من المتغيرات: """ Django settings for mysite project. Generated by 'django-admin startproject' using Django 2.1. For more information on this file, see https://docs.djangoproject.com/en/2.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/2.1/ref/settings/ """ import os import json . . . نشير أيضًا إلى محدد آخر يستدعي الاهتمام هو اسم قاعدة البيانات: DATABASES['default']['NAME'] فهو يدل على اسم قاعدة البيانات في معظم نظم إدارة قواعد البيانات العلاقية، أما في حال استخدامك قاعدة بيانات SQLite فإنه سيشير إلى ملف قاعدة البيانات لذا انتبه لهذه النقطة. وفي ختام التعديل لا بد لنا من الإشارة إلى أن الملف setting.py هو في النهاية ملف بايثون وبالتالي يمكنك قراءته والتعديل عليه بالطريقة التي تفضلها وما اتبعناه لهذا الغرض كان إحدى سبل التعديل المتاحة. ضبطنا حتى الآن إعدادات جانغو الرئيسية ومتغيراته بما فيها محددات قاعدة البيانات لتناسب النمط العام القابل للنقل، وسنتابع العملية في الخطوة التالية ولكن هذه المرة على مكونات وأصول التطبيق الساكنة مثل جافاسكريبت وصفحات CSS لتكون متاحة مركزيًا على خدمة التخزين الكائني المتوافقة مع S3. الخطوة 4: تفريغ المكونات الساكنة Offloading Static Assets تحتوي بعض بيئات التطوير على عدة حاويات جانغو وقد تكون كثيرة العدد ما يصعب على المطوّر مهمة الاحتفاظ بإصدارات محددة من الملفات والمكونات الساكنة الخاصة بكل حاوية، فيلجأ إلى جعل المشترك منها متوفرًا عبر وحدة تخزين خارجية يتم الوصول إليها من خلال الشبكة عوضًا عن توفيرها محليًا ومحاولة مزامنتها دائمًا باستخدام النسخ الاحتياطي الدوري أو التكرارية أو التحميل الروتيني، تسمى هذه العملية بالتفريغ Offloading. أما عن ضبط المحددات الخاصة بهذه الوحدة سنترك بعضها مدمجًا مع الكود hard-coded ونمرر بعضها الآخر لحاويات تطبيق جانغو عبر متغيرات البيئة بنفس الطريقة التي اتبعناها في الخطوة /3/ السابقة للتعامل مع محددات قاعدة البيانات. يتطلب هذا النوع من التخزين تثبيت حزمة django-storages التي تدعم وجود واجهات خلفية بعيدة للتطبيق تُخزن عليها المكونات والأصول الساكنة بما فيها وحدات التخزين الكائني المتوافقة مع S3 مثل DigitalOcean Space المستخدمة في مقالنا أو غيرها، ويمكنك الاستعانة بمقالة أجنبية أخرى تشرح عملية التفريغ في الخطوة رقم 7 منها. سنبدأ بتعديل الملف django-polls/mysite/settings.py وهو نفس الملف الذي تعاملنا معه في الفقرة السابقة بإضافة التطبيق storages على قائمة التطبيقات التي يستخدمها جانغو وذلك من خلال المحدد INSTALLED_APPS كما يلي: . . . INSTALLED_APPS = [ . . . 'django.contrib.staticfiles', 'storages', ] . . . تذكر أن التطبيق storages هو أحد الاعتماديات التي حددناها في الملف requirements.txt الذي ورد في الخطوة /1/ من الإعداد (وذلك من خلال django-storages). وبعد INSTALLED_APPS سنعدّل المحدد STATIC_URL كما يلي: # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/2.1/howto/static-files/ # Moving static assets to DigitalOcean Spaces as per: # https://www.digitalocean.com/community/tutorials/how-to-set-up-object-storage-with-django AWS_ACCESS_KEY_ID = os.getenv('STATIC_ACCESS_KEY_ID') AWS_SECRET_ACCESS_KEY = os.getenv('STATIC_SECRET_KEY') AWS_STORAGE_BUCKET_NAME= os.getenv('STATIC_BUCKET_NAME') AWS_S3_ENDPOINT_URL = os.getenv('STATIC_ENDPOINT_URL') AWS_S3_OBJECT_PARAMETERS = { 'CacheControl': 'max-age=86400', } AWS_LOCATION = 'static' AWS_DEFAULT_ACL = 'public-read' STATICFILES_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage' STATIC_URL='{}/{}/'.format(AWS_S3_ENDPOINT_URL, AWS_LOCATION) STATIC_ROOT = 'static/' كما أسلفنا في مقدمة الفقرة، فإن بعض المحددات ستبقى مدمجة مع الكود hard-coded وهي: STATICFILES_STORAGE: يحدد الواجهة الخلفية التي يستخدمها جانغو لتفريغ الملفات الساكنة والقيمة S3Boto3Storage لهذا المحدد ستعمل مع كافة خدمات التخزين المتوافقة مع S3 بما فيها DigitalOcean Spaces. AWS_S3_OBJECT_PARAMETERS: يحدد معايير التخزين المؤقت cache control الخاصة بمكونات التطبيق الساكنة. AWS_LOCATION: يحدد مسار تخزين كامل الملفات الساكنة على خدمة التخزين الكائني وهذا المسار يسمى static. AWS_DEFAULT_ACL: يحدد قائمة التحكم بالوصول ACL لملفات التطبيق الساكنة، واختيار القيمة public-read لهذا المحدد يعني أن الملفات قابلة للقراءة من قبل العامة. STATIC_URL: يحدد العنوان الرئيس للملفات الساكنة Base URL ويتشكل بجمع مكونين الأول هو endpoint URL ويشير إلى خدمة التخزين الكائني والثاني هو مسار وجود الملفات الساكنة للتطبيق. STATIC_ROOT: يحدد المسار المحلي لتجميع الساكنة قبل نسخها إلى خدمة التخزين الكائني. أما المحددات التي ستعطي المرونة للاتصال مع وحدة التخزين وتأخذ قيمها من متغيرات البيئة فهي: AWS_ACCESS_KEY_ID: يأخذ قيمته من متغير البيئة STATIC_ACCESS_KEY_ID الذي يشير إلى معرف مفتاح الوصول الخاص بخدمة التخزين (وهي DigitalOcean Spaces في حالتنا). AWS_SECRET_ACCESS_KEY: يأخذ قيمته من المتغير STATIC_SECRET_KEY ويشير إلى المفتاح السري لخدمة التخزين. AWS_STORAGE_BUCKET_NAME: يأخذ قيمته من المتغير STATIC_BUCKET_NAME ويدل على الموقع الذي ستُرفع ملفات جانغو الساكنة إليه على خدمة التخزين. AWS_S3_ENDPOINT_URL: يأخذ قيمته من المحدد STATIC_ENDPOINT_URL ويستخدم للوصول إلى خدمة التخزين الكائني، فسيكون شكله من قبيل هذا العنوان https://nyc3.digitaloceanspaces.com في خدمة DigitalOcean Spaces على سبيل المثال بالطبع مع بعض الاختلافات المتعلقة بالمنطقة التي تحوي ملفاتك. لا تنسَ حفظ التغيرات على الملف settings.py قبل إغلاقه. ننهي بذلك الضبط المتعلق بالمكونات الساكنة، وبموجبه سيرفع جانغو ملفاته إلى وحدة التخزين الكائني البعيدة في كل مرة تشغل فيها manager.py collectionstatic لتُجمَّع الملفات الساكنة لمشروعك. ويتبقى لنا الإشارة لنقطة اختيارية وليست ملزمة لتسريع تسليم موقعك وهي الاشتراك بخدمة شبكة توصيل المحتوى CDN التي تحتفظ بنسخ من ملفات الموقع على خوادم الشبكة وتقوم بتسليم المستخدم الصفحات المطلوبة من أقرب نقطة جغرافية لمكانه، ويمكنك الحصول عليها من مزود خدمة التخزين الكائني نفسه إن كان يوفرها (على غرار ما يفعل DigitalOcean Spaces) أو من مصدر آخر، يمكنك كذلك إعداد نطاق فرعي للمساحة التخزينية المخصصة لك وهو إجراء اختياري أيضًا، للاستزادة استعن بالمقال الخاص بخدمة CDN. لننتقل الآن للخطوة التالية وهي ضبط تسجيل أحداث جانغو ضمن مجرى قياسي للخرج والخطأ STDOUT و STDERR تتعامل معه حاوية دوكر من خلال docker logs، وهذا سيكون تعديلنا الأخير على settings.py. الخطوة 5: ضبط تسجيل الأحداث إذا كنت تعتمد خادم التطوير المدمج في إطار العمل جانغو ليؤدي دور خادم HTTP لتطبيقك، وفعلت خيار التنقيح بإعطاء DEBUG القيمة true فإن جانغو سيُسجل الأحداث تلقائيًا في مجرى خرج قياسي STDOUT ومجرى خطأ قياسي STDERR، أما الإخلال بأي من الشرطين سواءً باستخدام خادم HTTP مغاير لخادم التطوير أو بإعطاء القيمة false للمحدد DEBUG، وكلاهما شائع في بيئة العمل الفعلية، فستتغير آلية التسجيل للتطبيق فبدلًا من تسجيل كافة الأحداث التي تحمل مستوى الخطورة INFO وما فوق ضمن المجرى القياسي، فإنه سيبعث الأحداث التي تحمل المستوى ERROR أو CRITICAL إلى البريد الإلكتروني لمدير النظام. هذا السلوك من جانغو لا يعدّ ملائمًا لبيئة الحاويات بالأخص المتعددة مثل Kubernetes (التي تعتمد النظام العنقودي) ففي هذه البيئات تُجمع تسجيلات الأحداث ضمن ملفات قياسية لكلٍ من الخرج والخطأ وتخزن في مسار مركزي على نظام ملفات العقدة بحيث تكون متاحة لأوامر الحاوية مثل kubectl و docker، وسهلة المراقبة والتتبع على فرق التشغيل. من هذه النقطة تبرز الحاجة لتعديل تسجيل أحداث جانغو ليتماشى مع هذه البنية ويعطي ملفات الخرج المرغوبة، والتطبيق مرن من هذه الناحية، فهو يستخدم وحدة التسجيل logging من مكاتب بايثون القياسية التي تتيح تعريف قاموس مخصص للتسجيل بالتفاصيل والصيغ التي نريدها وذلك باستخدام الدالة logging.config.dictConfig. لنبدأ التطبيق العملي! افتح الملف django-polls/mysite/settings.py عبر محرر النصوص، واكتب ضمنه تعليمة import إضافية خاصة بـ logging.config ستسمح لك بتجاوز سلوك التسجيل الإفتراضي لتطبيق جانغو عبر تمرير قاموس تسجيل الأحداث التي ترغب بها للدالة dictConfig، كما يلي: import json import os import logging.config . . . ثم انتقل إلى نهاية الملف والصق الكود التالي الخاص بإعدادات تسجيل الأحداث: . . . # Logging Configuration # Clear prev config LOGGING_CONFIG = None # Get loglevel from env LOGLEVEL = os.getenv('DJANGO_LOGLEVEL', 'info').upper() logging.config.dictConfig({ 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'console': { 'format': '%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(module)s %(process)d %(thread)d %(message)s', }, }, 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'formatter': 'console', }, }, 'loggers': { '': { 'level': LOGLEVEL, 'handlers': ['console',], }, }, }) لنناقش التعليمات السابقة: أعطينا المحدد LOGGING_CONFIG القيمة none وذلك لتعطيل النمط الإفتراضي لتسجيل جانغو، ومنحنا LOGLEVEL قيمة افتراضية هي info مع ترك المجال مفتوح لتجاوزها عند الحاجة بتمرير أي قيمة أخرى نريدها عن طريق متغير البيئة DJANGO_LOGLEVEL. وأخيرًا استخدمنا الدالة dictConfig لتعيين قاموس تسجيل الأحداث المرغوب الذي ستعتمده الوحدة logging.config، حددنا في القاموس طريقة تنسيق النص عبر formatters بحيث يتضمن مستوى خطورة الحدث ونوع الخط وغيرها من التفضيلات، كذلك حددنا الخرج عن طريق إعداد handlers، ومن ثم وضحنا كل سوية حدث بأي handler ترتبط باستخدام المسجلين loggers. الضبط الذي أجريناه هو الحد الأدنى المقبول من ضبط سجلات الأحداث فهو يمكننا من تحديد سوية خطورة معينة للأحداث عبر المتغير DJANGO_LOGLEVEL ما يجعل جانغو يسجل الأحداث بنماذج قياسية يمكن للحاويات التعامل معها وعرضها عبر docker logs أو kubectl logs تبعًا لنوع الحاوية، وإن رغبت بتفاصيل أعمق استرشد بتوثيقات جانغو حول سجلات الأحداث. ننهي بذلك التعديلات البرمجية على تطبيق جانغو الذي يدعى Polls وسنبدأ بتهيئة الحاوية عبر ملف Dockerfile في الخطوة التالية. الخطوة 6: كتابة الملف Dockerfile لحاوية التطبيق نقدم لك في هذه الفقرة إحدى أبسط الطرق لبناء صورة الحاوية container image اللازمة لتشغيل تطبيق جانغو مع خادم Gunicorn WSGI الخاص به، وكل ما يلزم العملية من إعدادات، مع العلم بوجود طرق أخرى يمكنك اتباعها. اختيار صورة الحاوية المناسبة للبناء عليها يوجد طريقتين لبناء صورة الحاوية إما أن تستخدم scratch الذي يوفر صورًا فارغة لا تحتوي أي نظام ملفات وتبني حاويتك من الصفر أو أن تعتمد صورة حاوية جاهزة، وهنا أيضًا لدينا العديد من الخيارات ولكلٍ منها متطلباته الخاصة من حيث الحزم المطلوبة ونسخة نظام التشغيل كذلك لغة البرمجة وغيرها، لذا يعد اختيار الصورة قرارًا مهمًا ويحتاج لبعض الدراسة. اختيار إحدى صور الحاويات الموجودة في مستودعات دوكر يعدّ خيارًا مناسبًا فهي مختبرة وفق أفضل الممارسات ويتم تحديثها بانتظام لتطويرها ومعالجة الثغرات الأمنية فيها، ويمكنك استعراض صور الحاويات المعتمدة على لغة بايثون من ذلك المستودع فهي متوافقة بالتأكيد مع جانغو المطور بنفس اللغة، ولاحظ أن لكل صورة خصائص معينة من حيث إصدار بايثون وأدواته كذلك نسخة نظام التشغيل، ويمكنك اختيار الصورة الأنسب لبيئتك من جهتنا فضلنا صور Alpine Linux فهي توفر بيئة تشغيل قوية على الرغم من بساطتها وصغر حجم نظام الملفات الخاص بها إلا أنها تتضمن نظامًا كاملًا لإدارة الحزم ومستودعات برمجية متنوعة ما يسهل عليك إضافة الوظائف التي يمكن أن تحتاجها. تنويه: ستلاحظ أثناء استعراضك صور الحاويات المعتمدة على لغة بايثون أن كل صورة تحمل أكثر من وسم tag وأن الوسوم نفسها توضع على صورٍ مختلفة، فمثلًا الوسم 3alpine يشير إلى تثبيت أحدث إصدار من بايثون 3 على أحدث إصدار من Alpine وفي حال نزل إصدار جديد من أحدهما فسيُعاد تعيين الوسم على الصورة الجديدة أيضًا، لذا اختر الصورة التي تحمل الوسوم الأكثر تحديدًا لتحصل على أقرب صورة للبيئة التي ترغب بتحقيقها، وفي مثالنا الحالي سنستخدم الصورة ذات الوسم 3.7.4-alpine3.10. سنحدد المستودع ووسم الصورة في ملف Dockerfile الخاص بتطبيقنا وذلك باستخدام التعليمة from، لنخرج أولًا من مجلد التطبيق django-polls حيث كنا نطبق التعليمات في الخطوات السابقة وذلك وفق الأمر التالي: cd .. أصبحنا الآن في المجلد الأب polls-project لننشئ ضمنه ملف نصي باستخدام المحرر ونسميه Dockerfile ونكتب فيه التعليمة التالية التي تعدّ نقطة البداية في إنشاء صورة الحاوية التي نجهزها للتطبيق: FROM python:3.7.4-alpine3.10 كتابة التعليمات التنفيذية الخاصة بإعداد التطبيق للحاوية تتضمن هذه الفقرة إضافة التعليمات التنفيذية وبعض الاعتماديات اللازمة وتهيئة بيئة التشغيل، قد تجد بعض أوجه التشابه التي تجمعها مع خطوات إعداد خادم التطبيق باستثناء بعض المفاهيم الخاصة بالحاويات. نضيف الكود التالي إلى الملف Dockerfile بعد تعليمة from السابقة: . . . ADD django-polls/requirements.txt /app/requirements.txt RUN set -ex \ && apk add --no-cache --virtual .build-deps postgresql-dev build-base \ && python -m venv /env \ && /env/bin/pip install --upgrade pip \ && /env/bin/pip install --no-cache-dir -r /app/requirements.txt \ && runDeps="$(scanelf --needed --nobanner --recursive /env \ | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \ | sort -u \ | xargs -r apk info --installed \ | sort -u)" \ && apk add --virtual rundeps $runDeps \ && apk del .build-deps ADD django-polls /app WORKDIR /app ENV VIRTUAL_ENV /env ENV PATH /env/bin:$PATH EXPOSE 8000 سنقدم فيما يلي شرحًا لبعض ما ورد ضمن الكود السابق ولمزيد من المعلومات يمكنك الاطلاع على ملف Dockerfile جاهز لتطبيق جانغو. تتضمن التعليمة الأولى نسخ الملف requirements.txt إلى المسار app/requirements.txt/ بحيث تصبح اعتماديات التطبيق اللازمة موجودة ضمن نظام ملفات الصورة ليتم استخدامها في تنزيل كافة حزم بايثون الضرورية لعمل التطبيق، لاحظ أننا وضعنا هذه التعليمة في خطوة منفصلة وذلك ليقوم دوكر بتخزين الصورة التي تتضمن requirements.txt في طبقة منفصلة ما يتيح له إعادة استخدامها عند الحاجة عوضًا عن بنائها من جديد (بالطبع على فرض أن الاعتماديات لم تتغير)، ذلك أن دوكر ينشئ طبقة للصورة على نظام الملفات بعد تنفيذ كل تعليمة ADD أو RUN أو COPY. أما التعليمة الثانية RUN فهي تشتمل على مجموعة من أوامر لينكس يجمع بينها الإشارة && لتنفيذها على التوالي نوردها بإيجاز: تثبيت PostgreSQL وبناء اعتماديتها باستخدام apk مدير حزم Alpine. إنشاء البيئة الافتراضية. تثبيت اعتماديات بايثون المذكورة في الملف requirements.txt وذلك باستخدام pip. إعداد قائمة بالحزم المطلوبة للتشغيل من خلال تحليل متطلبات حزم بايثون المثبتة. إزالة تثبيت أي اعتماديات مبنية ولم تعد لازمة. جمعنا الأوامر السابقة بتعليمة RUN واحدة حتى يجمعها دوكر في طبقة واحدة ما يخفف عدد الطبقات المنشأة أثناء العمل، وحيث أن حذف العناصر المنشأة ضمن طبقة صورة معينة لن يكون متاحًا خارجها في طبقات لاحقة، فقد وضعنا الأمر del ضمن التعليمة Run نفسها التي تتضمن بناء الاعتماديات واستخدامها لتثبيت حزم التطبيق، ومن ثم حذفها عبر apk del، وذلك بهدف توفير المساحات التخزينية. أما التعليمة الثالثة بعد Run كانت ADD وهي تهدف إلى نسخ كود التطبيق ومسار العمل WORKDIR وذلك لضبط مسار العمل في الصورة على مسار التطبيق. تعرّف التعليمة الرابعة ENV اثنين من متغيرات البيئة التي ستكون متاحة ضمن الحاوية، المتغير الأول هو VIRTUAL_ENV ويأخذ القيمة env/ أما المتغير الثاني PATH يضبط على المسار env/bin/، وتحاكي هذه العملية الطريقة التقليدية المتبعة لتفعيل البيئة الافتراضية للتطبيق عبر سكريبت env/bin/activate/. أما العملية الأخيرة في هذه المرحلة فهي تحديد البوابة 8000 للاتصال مع الحاوية في وقت التشغيل عبر تعليمة EXPOSE. شارف الملف Dockerfile الاكتمال بما نفذناه حتى الآن والخطوة المتبقية له هي تحديد الأمر الذي سيُنفذ افتراضيًا عندما تبدأ الحاوية باستخدام الصورة أي عند بدء تشغيل الحاوية. تحديد الأمر الافتراضي يُكتب الأمر الافتراضي لحاوية دوكر في الملف Dockerfile باستخدام تعليمة ENTRYPOINT أو تعليمة CMD أو كلاهما معًا، ويحدد هذا الأمر ما سيُنفذ افتراضيًا عند بدء تشغيل الحاوية. في حال استخدمنا ENTRYPOINT و CMD معًا فإن الأولى ستحدد الأمر التنفيذي (الإجراء) الذي سينفذ عند تشغيل الحاوية والثانية ستحدد قائمة الوسطاء arguments اللازمة له. علمًا أن المستخدم يستطيع بسهولة تجاوز الوسطاء المعرّفين في CMD وذلك بتمرير وسطاء غيرهم لأمر تشغيل الحاوية كما يلي: docker run <image> <arguments> بينما لا يمكنه بنفس السهولة تجاوز الأمر التنفيذي الوارد في ENTRYPOINT لذا يلجأ الأغلبية لجعل الأمر التنفيذي المعطى لـ ENTRYPOINT على هيئة سكريبت يهيئ البيئة وينفذ الإجراءات التي تتناسب مع قائمة الوسطاء التي يتلقاها. ذكرنا فيما سبق طريقة استخدام ENTRYPOINT و CMD معًا، أما لو أردنا استخدام إحداهما فقط فسيكون الوضع كالتالي: استخدام ENTRYPOINT لوحدها يحدد الأمر التنفيذي للحاوية فقط دون تحديد قائمة الوسطاء. أما استخدام CMD لوحدها يتيح تحديد الأمر command والوسطاء arguments مع إمكانية تجاوز الوسطاء لاحقًا عبر docker run كما أسلفنا. لنطبق عمليًا على الصورة التي نبنيها ونناقش المتطلبات التي ستقودنا لاختيار التعليمة المناسبة لبدء التشغيل. نريد للتطبيق أن يعمل افتراضيًا باستخدام الخادم gunicorn أما تقديم قائمة الوسطاء لعمل gunicorn فهو غير ملزم في مرحلة بدء التشغيل، كما أننا نفضل بقاء الفرصة متاحة لتشغيل أوامر أخرى بسهولة عند بدء تشغيل الحاوية مثل عملية تنقيح الأخطاء debugging أو بعض المهام الإدارية (جمع المكونات الساكنة للتطبيق أو تهيئة قاعدة البيانات أو غيرها)، كل ذلك يدفعنا لاستخدام CMD بمفردها دون الحاجة لتعليمة ENTRYPOINT، لنتعرف إذًا على صيغ كتابتها. تكتب CMD عمومًا بثلاث صيغ: تمرير الوسطاء لتعليمة ENTRYPOINT: CMD ["argument 1", "argument 2", . . . ,"argument n"] بصيغة exec: CMD ["command", "argument 1", "argument 2", . . . ,"argument n"] بصيغة shell: CMD command "argument 1" "argument 2" . . . "argument n" تستخدم CMD في الصيغة الأولى مع ENTRYPOINT لتمرير قائمة الوسطاء فقط كما ذكرنا، أما في الصيغتين الثانية والثالثة تكون مسؤولة عن تحديد الأمر الذي سينفذ وكذلك الوسطاء، والفرق بينهما أن صيغة exec (وهي الصيغة الموصى بها) تنفذ الأمر المحدد مباشرةً وتمرر قائمة الوسطاء دون القيام بأي عمليات أو معالجة في الصدفة shell، أما في صيغة الصدفة shell فإن قائمة الوسطاء تُمرر إلى sh -c، وقد تكون ضرورية لك في الحالات التي تحتاج فيها لتبديل أحد متغيرات البيئة التي يستخدمها الأمر. بالنتيجة اعتمدنا الشكل النهائي التالي لتحديد افتراضات تشغيل الصورة، وكُتب ضمن Dockerfile كما يلي: . . . CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "mysite.wsgi:application"] وبموجبه تشغل الحاوية gunicorn على الجهاز المحلي localhost افتراضيًا وذلك عبر البوابة 8000 باستخدام عمال workers عدد 3، كما أنها تشغل الدالة application الموجودة ضمن الملف wsgi.py في المسار mysite. وبذلك أصبحنا جاهزين للمرحلة التالية وهي استخدام الأمر docker build لبناء صورة التطبيق والأمر docker run لتشغيل الحاوية. بناء صورة دوكر يبني docker build الصورة باستخدام كل من ملف dockerfile الذي يتضمن إعدادات البناء، وسياق "context" الذي يشير إلى مسار مجموعة الملفات التي يجب أن تتوفر لعفريت دوكر Docker daemon لينجز عملية البناء. المتغير "context" يطابق في معظم الأحيان المسار الحالي الذي نكون فيه عند تنفيذ الأمر docker build ويُشار إلى المسار الحالي في لينكس برمز النقطة . . للتنفيذ العملي، ابحث عن مسار الملف Dockerfile ونفذ ضمنه التالي مع تمرير اسم الصورة بعد الراية t- بالإضافة إلى تمرير المسار الحالي: docker build -t django-polls:v0 . لاحظ أننا أعطينا الاسم django-polls للصورة أما v0 فهي تشير للإصدار رقم 0 من الصورة، حيث أن عفريت دوكر الذي يعمل في الخلفية يبني الصورة عبر سلسلة من الطبقات بناءً على بنية تعليمات ملف Dockerfile. سنحصل على الخرج التالي فور إكتمال تنفيذ الأمر docker build: Successfully built 8260b58f5713 Successfully tagged django-polls:v0 نظريًا يمكنك تشغيل الحاوية عبر الأمر docker run بعد انتهاء عملية البناء بنجاح، ولكن في الواقع سيفشل الأمر run ما لم تعرف داخل الحاوية كافة متغيرات البيئة التي حددناها خارجيًا في الخطوات السابقة مثل SECRET_KEY ومحددات قاعدة البيانات ضمن الملف setting.py وغيرها، وهو ما سنقوم به في الخطوة التالية. الخطوة 7: ضبط متغيرات بيئة التشغيل واختبار التطبيق يوفر دوكر عدة طرق لتعريف المتغيرات داخل الحاوية، ولكون حالتنا تتطلب إعادة تعريف كامل المتغيرات التي عملنا عليها في الخطوة /1/، سنتبع الطريقة env-file التي تتيح تمرير ملف يتضمن كامل متغيرات البيئة وقيمة كل منها. لتنفيذ ذلك سننشئ ملفًا نصيًا يدعى env ضمن المجلد polls-project مع لصق القائمة التالية ضمنه وهي تحتوي كامل المتغيرات وقيمها: DJANGO_SECRET_KEY=your_secret_key DEBUG=True DJANGO_ALLOWED_HOSTS=your_server_IP_address DATABASE_ENGINE=postgresql_psycopg2 DATABASE_NAME=polls DATABASE_USERNAME=sammy DATABASE_PASSWORD=your_database_password DATABASE_HOST=your_database_host DATABASE_PORT=your_database_port STATIC_ACCESS_KEY_ID=your_space_access_key_id STATIC_SECRET_KEY=your_space_secret_key STATIC_BUCKET_NAME=your_space_name STATIC_ENDPOINT_URL=https://nyc3.digitaloceanspaces.com DJANGO_LOGLEVEL=info لا تنسَ تغيير القيم المطلوبة بما يناسب إعداداتك وفق التالي: بالنسبة للمفتاح السري DJANGO_SECRET_KEY اختر له قيمة فريدة صعبة التخمين كما توصي توثيقات دوكر تقدم لك الخطوة /5/ من مقال كيفية إعداد تطبيق جانغو عبر DigitalOcean Managed Databases and Spaces الأجنبي إحدى طرق جانغو لتوليد المفاتيح. ضع عنوان IP الخاص بالخادم أوبونتو مقابل DJANGO_ALLOWED_HOSTS ويمكنك وضع الرمز * لأغراض الاختبار فقط وليس ضمن بيئة العمل الفعلية فهو يسمح باتصال أي عنوان IP. اكتب اسم مستخدم قاعدة البيانات وكلمة المرور كما حددتهما في الخطوات السابقة مقابل DATABASE_USERNAME و DATABASE_PASSWORD. كذلك الأمر بالنسبة لاعدادات الاتصال مع قاعدة البيانات من حيث اسم مضيف قاعدة البيانات وبوابة اتصال قاعدة البيانات ضع هذه البيانات مقال كل من DATABASE_HOST و DATABASE_PORT. أما المحددات STATIC_ACCESS_KEY_ID و STATIC_SECRET_KEY و STATIC_BUCKET_NAME و STATIC_ENDPOINT_URL فهي تتعلق بخدمة التخزين الكائني الخارجية، اضبطها بما يتناسب مع الخدمة التي تستخدمها. قبل تشغيل الأمر docker run احفظ التغيرات على الملف السابق env وتأكد أن المحدد DEBUG يحمل القيمة false وأن سجلات الأحداث مضبوطة بحيث تعطي التفاصيل المرغوبة. وبعدها نفذ أمر التشغيل للإصدار v0 من الصورة django-polls بطريقة تتجاوز الإعدادات الافتراضية التي حددناها سابقًا ضمن الملف Dockerfile (باستخدام CMD) وتنشئ أثناء التشغيل مخطط قاعدة البيانات الخاص بالتطبيق عبر manage.py makemigrations و manage.py migrate: docker run --env-file env django-polls:v0 sh -c "python manage.py makemigrations && python manage.py migrate" سيظهر الخرج التالي بعد التنفيذ: No changes detected Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying polls.0001_initial... OK Applying sessions.0001_initial... OK ننفذ بعدها أمر تشغيل جديد لحاوية التطبيق نستدعي خلاله سطر الأوامر الخاص بالصدفة Shell كما يلي: docker run -i -t --env-file env django-polls:v0 sh تظهر عندها نافذة صدفة shell ضمن الحاوية وننشئ من خلالها المستخدم الذي يحمل صلاحية مدير تطبيق جانغو: python manage.py createsuperuser يلي ذلك إدخال بيانات المستخدم وهي اسم المستخدم وكلمة المرور والبريد الإلكتروني، ومن ثم الخروج من الحاوية وإنهاء عملها باستخدام الأمر Ctrl+D. أما العملية الأخيرة هي جمع الملفات الساكنة الخاصة بالتطبيق collectstatic ورفعها على خدمة التخزين الكائني: docker run --env-file env django-polls:v0 sh -c "python manage.py collectstatic --noinput" لنحصل على الخرج التالي: 121 static files copied. يمكننا الآن تشغيل التطبيق بالإعدادات الافتراضية المحددة ضمن Dockerfile: docker run --env-file env -p 80:8000 django-polls:v0 ونحصل على الخرج التالي: [2019-10-17 21:23:36 +0000] [1] [INFO] Starting gunicorn 19.9.0 [2019-10-17 21:23:36 +0000] [1] [INFO] Listening at: http://0.0.0.0:8000 (1) [2019-10-17 21:23:36 +0000] [1] [INFO] Using worker: sync [2019-10-17 21:23:36 +0000] [7] [INFO] Booting worker with pid: 7 [2019-10-17 21:23:36 +0000] [8] [INFO] Booting worker with pid: 8 [2019-10-17 21:23:36 +0000] [9] [INFO] Booting worker with pid: 9 كما ذكرنا فإن التشغيل هذه المرة تم وفق الإعدادات الافتراضية ضمن ملف Dockerfile وهي كما حددناها سابقًا: gunicorn --bind :8000 --workers 3 mysite.wsgi:application وبذلك فإن البوابة 80 الخاصة بالخادم أوبونتو ستوجه الحركة إلى البوابة 8000 في الحاوية django-polls:v0. يمكنك الآن فتح التطبيق polls عبر متصفح الإنترنت بكتابة عنوان URL الخاص به، وانتبه أنك ستحصل على الخطأ 404 (لم يتم العثور على الصفحة) في حال اكتفيت بالعنوان التالي مثلًا: http://your_server_ip/ بسبب عدم وجود بيانات للتطبيق تحت الجذر / ، لذا اكتب العنوان التالي: http://your_server_ip/polls اكتب الرابط http://your_server_ip/admin للوصول إلى واجهة الدخول للوحة التحكم الخاصة بالتطبيق: أدخل بيانات المستخدم مدير التطبيق المنشأ سابقًا باستخدام الأمر createsuperuser وستظهر أمامك واجهة الإدارة والتحكم الخاصة بالتطبيق. لاحظ أن تسليم المكونات الساكنة للتطبيق admin و polls يتم من خدمة التخزين الكائني ويمكنك اتباع اختبار جودة والتأكد من صحة إحضار الملفات. اضغط Ctrl+c في نافذة كتابة الأوامر السطرية التي تشغل حاوية دوكر لإنهاء عمل الحاوية. خاتمة استعرضنا في هذا المقال طريقة تكييف تطبيق جانغو ليعمل بفعالية ضمن بيئة تطوير سحابية أصلية cloud-native معتمدة على الحاويات container-based، وكتابة ملف Dockerfile محلي ومبسط لصورة الحاوية ومن ثم تشغيله عبر محرك دوكر، ويمكنك رؤية التغييرات التي طبقناها وتأثيراتها باستعراض قسم polls-docker من مستودع GitHub فهو يتضمن كافة تعديلات التطبيق polls التي ناقشناها في هذا المقال. إن ما تعلمته هنا يمهد لك الطريق لتجهيز بيئة متعددة الحاويات فيمكنك جمع حاوية Django/Gunicorn التي عملنا عليها مع حاوية تتضمن خادم وكيل عكسي Nginx لمعالجة طلبات HTTP بكفاءة، والحصول على شهادات TLS لتشفير هذه الطلبات عبر حاوية ثالثة تتضمن Certbot، وفي النهاية إدارة هذه الحاويات جميعًا من خلال Docker Compose. وقبل الختام لابد لنا من التنويه إلى أن متطلبات العمل في بيئة حقيقية تفرض عليك تشغيل Gunicorn خلف خادم HTTP وإلا فإن تطبيقك سيكون عرضة لهجمات الحرمان من الخدمة، كذلك عليك زيادة عدد العمال عن الذي اعتمدنا في المقال فالعدد 3 بالتأكيد لن يكون مناسبًا، عدّل العدد ليلائم حجم الطلب على الموقع، كذلك الأمر بالنسبة لتفريغ مكونات التطبيق الساكنة على وحدة تخزين كائني خارجية فقد اخترنا ذلك للتخفيف من عبء إدارتها في بيئة متعددة الحاويات أما في بيئتك الفعلية فاختر التصميم الذي تجده مناسبًا لمتطلباتك. أخيرًا يمكنك تسجيل الحاوية التي أعددتها على Dockerhub وجعلها متوفرة لأي نظام يحتوي دوكر سواء أكان خوادم أوبونتو أو بيئات افتراضية أو نظم حاويات عنقودية مثل Kubernetes. ترجمة -وبتصرف- للمقال How to Build a Django and Gunicorn Application with Docker لصاحبيه Hanif Jetha و Justin Ellingwood. اقرأ أيضًا ما هي تقنية Docker؟ التعامل مع قواعد البيانات نظام كوبيرنتس Kubernetes وكيفية عمله مدخل إلى صدفة باش Bash
  9. تجنب الأعداء والركض بعيدًا عنهم شيء والقتال ضدهم شيء آخر تمامًا، فهو يزيد التفاعل بين المستخدم واللعبة، وهو موضوع المقال الختامي من سلسلة بناء لعبة من الصفر باستخدام بايثون، إليك مقالات السلسلة بالترتيب قبل أن نبدأ: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. لديك تفصيلين تهتم بهما في هذه الحالة، الأول هو إنتاج العنصر الذي سيُرمى والثاني هو المعيار أو الشرط الذي سيبقيه فعالًا فالسهام والكرات النارية لابد ستختفي في النهاية قد تختفي عند حافة الشاشة مثلًا أو تظل صالحة لفترة زمنية محدودة ينتهي مفعولها بعدها وهكذا. بالإضافة إلى تحديد قدرة اللاعب على القذف فهو يستطيع أن يقذف عنصرًا واحدًا فقط في كل مرة، يرفع هذا الخيار من مستوى التحدي بإعطاء اللاعب فرصةً وحيدة في كل مرة لإصابة العدو، ويساهم من جهةٍ ثانية بتبسيط تعليماتك البرمجية. يمكنك بعد نهاية القراءة وتعلم الموجود هنا أن تسعى لتطوير البرنامج وتعطي لاعبك القدرة على قذف أكثر من عنصر في وقتٍ واحد. إنشاء صنف الكائن المقذوف ستجد الدالة __init__ ضمن تعليمات الصنف المبينة أدناه لإنتاج كائن بايثون للعنصر المقذوف، وهي الدالة نفسها المستخدمة سابقًا لإنتاج البطل و الأعداء. class Throwable(pygame.sprite.Sprite): """ إنتاج الكائن المقذوف """ def __init__(self, x, y, img, throw): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images',img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.firing = throw الاختلاف الجوهري بين الدالة __init__ الموجودة في هذا الصنف والدالة نفسها الموجودة في صنف اللاعب Player وصنف العدو Enemy هو المتغير self.firing الذي يتتبع حالة الكائن المقذوف فيما إذا كان لا يزال فعالًا وظاهرًا على الشاشة أم انتهت صلاحيته، وتتغير قيمته تبعًا لذلك فهو يحمل القيمة 1 عندما ينشئ الكائن. قياس زمن فعالية الكائن المقذوف تمامًا مثل البطل والعدو يحتاج هذا الكائن إلى الدالة update لتحديث موقع ظهوره على الشاشة حتى يتحرك في الهواء منطلقًا باتجاه العدو بعد أن رماه البطل. أسهل طريقة لقياس زمن فعالية الكائن المقذوف هي وصوله إلى حافة الشاشة وخروجه عن مدى الرؤية، لكن عليك أن تحدد أي حافة ستراقب تبعًا لحركة هذا الكائن إن كانت أفقية أو عمودية. إذا كان البطل يقذفه أفقيًا مثل السهم أو الطاقة السحرية أو ماشابه فعليك مراقبة حافة الشاشة على المحور الأفقي وتحددها القيمة worldx. أما إذا كان يقذف الأشياء عموديًا أو بكلا الاتجاهين يتعين عليك مراقبة الحافة العمودية للشاشة باستخدام القيمة worldy. يفترض هذا المثال أن الجسم المقذوف يتحرك قليلًا للأمام ثم يسقط على الأرضية دون ارتداد فيتابع سقوطه إلى أسفل الشاشة، اطلع على التعليمات التالية وغيّر القيم الواردة فيها قليلًا لترى ما يناسب لعبتك أكثر: def update(self,worldy): ''' فيزياء المقذوفات ''' if self.rect.y < worldy: #المحور العمودي self.rect.x += 15 #سرعة حركته إلى الأمام self.rect.y += 5 #سرعة سقوطه إلى أسفل else: self.kill() #انتهاء فاعلية العنصر المقذوف self.firing = 0 #تحرير المتغير أو تصفيره ارفع قيم self.rect لتسريع حركة الكائن المقذوف. عندما يصل المقذوف إلى حافة الشاشة سيختفي أو بالأحرى يُدَمّر، وتتحرر الذاكرة المؤقتة التي أُشغلت بوجوده وتُضبط قيمة المتغير self.firing إلى الصفر ويتحرر تمهيدًا للرمية التالية. ضبط إعدادات الكائن المقذوف أنشئ في مقطع الإعدادات ضمن برنامج مجموعةً خاصة للكائنات التي يمكن أن يقذفها البطل، بصورةٍ مشابهة لمجموعات البطل والأعداء، وجهز كائنًا قابلًا للقذف غير فعال حتى يبدأ به البطل اللعبة، وإلّا فإن قذفته الأولى ستفشل. يفترض هذا المثال أن سلاح البطل هو الكرة النارية وتحدد مواصفاتها باستعمال المتغير fire ويمكنك في مستوياتٍ لاحقة تطوير السلاح وتغيير صورته ومواصفاته بالاعتماد على الصنف Throwable نفسه. اكتب التعليمات الآتية علمًا أن تعليمات أول سطرين لمعرفة السياق ولا حاجة لكتابتها مجددًا: player_list = pygame.sprite.Group() #للسياق player_list.add(player) #للسياق fire = Throwable(player.rect.x,player.rect.y,'fire.png',0) firepower = pygame.sprite.Group() لاحظ أن مكان إنتاج الكائن المقذوف مطابق لمكان إنتاج كائن البطل وهذا يعطي انطباعًا بأنه ينطلق من جعبة البطل، بعد إنتاج كرة النار الأولى يأخذ المتغير self.firing القيمة صفر ليتيح إمكانية القذف. إضافة القذف إلى الحلقة الرئيسية المبدأ نفسه على طول السلسلة اذكر ما تريد تنفيذه في الحلقة الرئيسية، أضف في البداية تعليمات التحكم التي ستُطلق الكرة النارية عندما يستخدم اللاعب الزر المحدد على لوحة المفاتيح. عند تحريك البطل استخدمنا الضغط على الزر لتبدأ حركة البطل وتحرير الزر أو رفع الضغط عنه ليتوقف البطل عن الحركة، لا يوجد توقف في عملية القذف فلن تحتاج أكثر من إشارة اختر واحدة منهما الضغط أو التحرير. اكتب التعليمات التالية وأول سطرين لتبيان السياق: if event.key == pygame.K_UP or event.key == ord('w'): player.jump(platform_list) if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x,player.rect.y,'fire.png',1) firepower.add(fire) تُسند القيمة 1 للمتغير self.firing على عكس الكرة النارية الأولى غير الفعالة التي كُتبت سابقًا في مقطع الإعدادات. اكتب التعليمات الخاصة بتحديث الكائن المقذوف ورسمه على الشاشة، واحرص على كتابتها في الموقع الصحيح تمامًا كما هو مبين أدناه: enemy.move() # للسياق if fire.firing: fire.update(worldy) firepower.draw(world) player_list.draw(screen) # للسياق enemy_list.draw(screen) # للسياق لاحظ أن تعليمات التحديث تعمل فقط عندما يحمل المتغير self.firing القيمة 1، أما إذا كانت قيمته 0 فإن الشرط لن يتحقق ويتجاوز البرنامج هذه التعليمات دون أن ينفذها، أما لو كتبت هذه التعليمات دون شروط وحاولت تطبيقها بغض النظر عن قيمة المتغير سيفشل عمل اللعبة لأنها لن تجد كرةً نارية لترسمها أو تحدّث ظهورها على الشاشة. شغل اللعبة الآن وحاول استخدام السلاح. اكتشاف التصادم فور تجريب اللعبة ستلاحظ أن البطل يقذف سلاحه باتجاه العدو وقد يصيبه لكن دون أي تأثير، فالبرنامج لا يملك حتى الآن أي آلية تكتشف تصادم الجسم المقذوف بالعدو. الآلية المطلوبة مشابهة جدًا للمذكورة في صنف اللاعب Player، لذا أضف الدالة update التالية في صنف العدو Enemy: def update(self,firepower, enemy_list): """ اكتشاف التصادم مع الكرة النارية """ fire_hit_list = pygame.sprite.spritecollide(self,firepower,False) for fire in fire_hit_list: enemy_list.remove(self) التعليمات بسيطة فهي تتحقق من حدوث التصادم بين كائن العدو وكل كرة نارية firepower تنتمي لمجموعة كائنات الكرات النارية التي يقذفها البطل، وفي حال تبين حدوث التصادم يُزال العدو من مجموعة الأعداء ويختفي عن الشاشة. أضف السطر الأخير من التعليمات الآتية إلى كتلة تعليمات firing في الحلقة الرئيسية: if fire.firing: # للسياق fire.update(worldy) # للسياق firepower.draw(screen) # للسياق enemy_list.update(firepower,enemy_list) # تحديث العدو يتبقى لدينا أمر أخير للتعامل معه وهو اتجاه القذف. تغيير اتجاه القذف يقذف بطل اللعبة حاليًا كراته باتجاه اليمين فقط، فعندما ما عرفنا دالة التحديث في صنف القذف جعلناها تزيد عددًا من البكسلات لموقع الكرة وهي تتحرك على المحور X والزيادة على هذا المحور تعني التحرك يمينًا، فماذا لو استدار البطل وأراد القذف نحو اليسار؟ يتمثل الحل المتبع هنا في تعريف متغير جديد يحدد توجه البطل ومن ثم تحديد اتجاه القذف الأنسب وفقًا لقيمته. عرّف المتغير facing_right في صنف اللاعب وأعطه القيمة الابتدائية True إذ إن صورة اللاعب المستخدمة في هذا المثال تتجه افتراضيًا نحو اليمين: self.score = 0 self.facing_right = True # أضف هذا السطر self.is_jumping = True وبذلك تكون القيمة True مؤشرًا على اتجاه البطل إلى اليمين والقيمة False تشير لاتجاهه يسارًا، وينبغي التبديل بينهما في كل مرة يغير البطل اتجاه حركته، وأنسب مكان ذلك هو التعليمات الخاصة بالتحكم بحركة البطل عبر أزرار لوحة المفاتيح وفق ما يلي: if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) player.facing_right = False # أضف هذا السطر if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True # أضف هذا السطر عدّل أخيرًا الدالة update في صنف القذف Throwable لتجمع أو تطرح عددًا من وحدات البكسل إلى موقع الكرة النارية بناءً على اتجاه حركة البطل: if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5 جرب اللعبة الآن وتفقد النتائج. حاول تطوير اللعبة وامنح البطل بعض النقاط مع كل إصابة موفقة للعدو. برنامج اللعبة كاملًا #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import pygame.freetype import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (80, 80, 155) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) tx = 64 ty = 64 font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) ''' Objects ''' def stats(score, health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64) class Throwable(pygame.sprite.Sprite): """ Spawn a throwable object """ def __init__(self, x, y, img, throw): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.firing = throw def update(self, worldy): ''' throw physics ''' if self.rect.y < worldy: if player.facing_right: self.rect.x += 15 else: self.rect.x -= 15 self.rect.y += 5 else: self.kill() self.firing = 0 # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.damage = 0 self.score = 0 self.facing_right = True self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'walk' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # set damage back to 0 self.health -= 1 # subtract 1 hp ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 def update(self, firepower, enemy_list): """ detect firepower collision """ fire_hit_list = pygame.sprite.spritecollide(self, firepower, False) for fire in fire_hit_list: enemy_list.remove(self) class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 0) firepower = pygame.sprite.Group() eloc = [] eloc = [300, worldy-ty-80] enemy_list = Level.bad(1, eloc) gloc = [] i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) player.facing_right = False if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) player.facing_right = True if event.key == pygame.K_SPACE: if not fire.firing: fire = Throwable(player.rect.x, player.rect.y, 'fire.png', 1) firepower.add(fire) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) if fire.firing: fire.update(worldy) firepower.draw(world) enemy_list.draw(world) enemy_list.update(firepower, enemy_list) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() stats(player.score, player.health) pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add throwing mechanics to your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  10. عرض النتائج على الشاشة جزء مهم وحيوي من أي لعبة وهو موضوع المقال ما قبل الأخير من سلسلة بناء لعبة من الصفر باستخدام بايثون، سنعرض فيه نقاط اللاعب وصحته بناءً على ما اغتنمه من جوائز وما أصابه من ضرر جراء التصادم مع الأعداء. يمكنك مطالعة المقالات ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. اكتسبت على مدار السلسلة كل الأساسيات اللازمة لبناء لعبة فيديو بلغة بايثون وستضيف لها اليوم مهارة جديدة جوهرية لكل مبرمج أيًّا كان اختصاصه، فإضافةً لعرض النتائج على الشاشة بتنسيق وخط مناسبين ستتعلم قراءة توثيقات المكتبة أو اللغة البرمجية ومعرفة إمكاناتها وكيفية استخدام ميزاتها التي لم تتعامل معها قبلًا وهو فعليًا الهدف الحقيقي المقال. عرض النتائج في Pygame يتضمن برنامج اللعبة كل المتغيرات اللازمة لتتبع نقاط اللاعب التي كسبها من الجوائز وأيضًا صحته أو أدواره المتأثرة بالتصادم مع الأعداء، إلّا أن هذه المتغيرات تعمل في الخلفية ولا تُظهر أي نتائج على شاشة اللعبة لذا ينصب اهتمامنا هنا على طريقة العرض واختيار الخط المناسب لها. قراءة التوثيقات معظم وحدات بايثون ومن بينها Pygame لها توثيقات رسمية تشرحها وحتى الجزء البسيط من الوحدات الذي لا تشمله التوثيقات تشرحه Help دالة المساعدة من بايثون، والتوثيقات عمومًا هي توصيف لكافة دوال الوحدة وأصنافها وتكتب بلغة فنية دقيقة تحدد مثلًا أنواع المدخلات المطلوبة وما شابه، وقد تكون مربكة للبعض غير المعتاد عليها فهي لا تشبه السرد البسيط المتبع في المقالات التعليمية. بخصوص Pygame الوحدة التي نستخدمها تستطيع الوصول لتوثيقاتها الغنية عبر رابط في صفحتها الرسمية، ولكن قبل البحث في أي توثيق فكر أولًا بما تريد الحصول عليه، فهنا مثلًا نسعى لإظهار نقاط اللاعب وصحته على شاشة اللعبة، ومن بعدها حاول تخمين العناصر البرمجية المطلوبة لتحقيقه مثل المتغيرات والدوال وغيرها، وإن لم تتمكن من وصف احتياجك بهذه الدقة، صِفهُ بالعموم وببساطة، فهنا مثلًا تحتاج لنصوص ترسمها Pygame على الشاشة لتظهر النتائج، وربما شعرت بقدرتك على إظهارها بطريقة مشابهة لعرض المنصات والكائنات على شاشة اللعبة. تقنيًا لا يوجد ما يمنعك من فعل ذلك، واستخدام صور الأرقام بكل بساطة وعرضها على الشاشة لتمثل نتائج اللعبة عبر Pygame، قد لا تكون هذه الطريقة المثلى لتصل لهدفك لكنها صحيحة في نهاية المطاف وقد أوصلك إليها التفكير العام بأساليب عرض النتائج، بكل الأحوال لو تصفحت توثيقات Pygame ستلفت انتباهك على الفور وحدة الخط وتدعى Font وهي مختصة بتسهيل كتابة النصوص على الشاشة وتنسيق خطوطها. فهم رموز التوثيق الفني يبدأ توثيق وحدة الخط بالدالة ‎pygame.font.init‎(‎)‎ ويصفها التوثيق بأنها دالة التهيئة للوحدة، تُستدعى تلقائيًا بواسطة ()pygame.init المستدعاة أساسًا في برنامج اللعبة، واستخدامها شبيه بما تعلمته خلال السلسلة، إذ ستتمكن من استخدام دوالها وكتابة النتيجة على الشاشة بقراءة بسيطة للتوثيق ومراجعة لما تعلمته. ولو استمريت باستعراض الوحدات في توثيقات Pygame ستصادف الوحدة Pygame.freetype ومن الاسم يتضح ارتباطها بحالتنا وهي أفضل من وحدة الخط إذ يصفها التوثيق بأنها: وفي صفحة التوثيق نفسها المخصصة للوحدة ستجد بعض الأمثلة على طريقة استخدامها ومنها: import pygame import pygame.freetype السطر موجود لدينا في برنامج اللعبة وما نحتاجه هو السطر الثاني، أضفه إذًا لمجموعة تعليمات import لتصبح على الشكل التالي: import pygame import sys import os import pygame.freetype استخدام الخطوط في Pygame استنادًا للتوثيق تعرض الوحدتان النصوص على الشاشة باستعمال الخطوط الإفتراضية في Pygame أو أي خطوط أخرى تزودها بها، وستجد ضمن توثيق الوحدة pygame.freetype التوصيف الفني للدالة التالية: pygame.freetype.Font # إنشاء خط جديد انطلاقًا من أحد ملفات الخطوط المدعومة Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font pygame.freetype.Font.name # اسم ملف الخط pygame.freetype.Font.path # مسار ملف الخط pygame.freetype.Font.size # حجم الخط الافتراضي المستخدم في العرض وهي مسؤولة عن إنشاء كائن Object الخط في Pygame ومواصفاته ولو دققت فيها ستجدها مشابهة لتعليمات إنشاء كائنات اللعبة مثل الأبطال والأعداء فبدلًا من صورة الشخصية ستحدد هنا ملف الخط وبمجرد توفره يمكنك إنشاء كائن الخط باستخدام الدالة السابقة pygame.freetype.Font ومن ثم استدعائه للعرض على الشاشة. إدارة الخط بصفته أحد أصول اللعبة لتضمن عمل لعبتك بصورةٍ جيدة عليك تضمن الخط الذي تعتمده ضمن ملفات اللعبة فمجموعات الخطوط تختلف من حاسبٍ لآخر، لذا أنشئ مجلدًا خاصًا بالخطوط في المجلد الأب للعبتك تمامًا مثل مجلد الصور وضع فيه ملفات الخطوط التي تستخدمها. واحرص على عدم انتهاك حقوق الملكية الفكرية، فاستعمل خطوطًا مفتوحة المصدر أو تتمتع برخصة المشاع الإبداعي، فحتى استعمال أحد ملفات الخطوط المتوفرة على حاسبك الشخصي وجعله خطًا للعبتك لا يعد تصرفًا قانونيًا. وفيما يلي قائمة ببعض المواقع التي توفر خطوطًا مجانية واستخدامها قانوني يمكنك الاستفادة منها: Font Library. Font Squirrel. League of Moveable Type. حمّل الخط الذي أعجبك من هذه المواقع على شكل ملف مضغوط zip أو tar وفك ضغطه لاستخراج ملف الخط، ويكون عادةً بصيغة ttf. أو otf. ومن ثم ضعه في مجلد الخطوط الخاصة باللعبة. لا يوجد أي حاجة لتثبيت الخط في نظام التشغيل لديك بل يكفي وجوده في مجلد اللعبة لتستخدمه Pygame، وفي حال تضمن اسم الملف رموزًا أو فراغات أزلها وعدّل الاسم، ويفضل لو تختر له اسمًا بسيطًا ومختصرًا لتكتبه بسهولة في التعليمات البرمجية. تعليمات الخط في Pygame بناءً على توثيق الدالة pygame.freetype.Font يمكنك إنشاء كائن الخط عبر تحديد مسار ملف الخط المرغوب على الأقل المدخلات الأخرى فهي اختيارية وليست ملزمة. Font(file, size=0, font_index=0, resolution=0, ucs4=False) -> Font أنشئ متغيرًا جديدًا اسمه myfont وأسند له خرج الدالة Font المذكورة وفق التعليمات الآتية وملف الخط الذي اعتمدنا عليه هنا هو amazdoom.ttf على سبيل المثال. font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"fonts","amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) عرض النص على الشاشة في Pygame بعد إنشاء الخط عرف الدالة stats التالية في مقطع الكائنات ضمن برنامج اللعبة، مهمتها رسم النصوص المطلوبة على الشاشة وهي تتطلب ذكر النصوص التي تريد عرضها وتحديد لون معين لها من باستخدام النظام اللوني RGB بالطريقة نفسها التي اتبعناها عند إظهار خلفية عالم اللعبة ومنصاتها، مع ضرورة التنويه إلى أن هذه الدالة عامة ومستقلة ولا تتبع لصنف معين من أصناف برنامج اللعبة. def stats(score,health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLACK, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLACK, None, size=64) والآن استدعيها في حلقة التكرار الرئيسية: stats(player.score,player.health) # draw text إذا شغلت اللعبة الآن ستحصل على خطأ فهي ما زالت بحاجة لبعض التعديلات. تفسير الأخطاء تفسير الأخطاء ومحاولة فهمها عملية مهمة جدًا للمبرمجين وتفيدهم في اصلاح مشاكل البرامج، والأخطاء في لغة بايثون دلالية وسهلة الفهم نوعًا ما لكنها مع ذلك تحتاج لتفسير لتُحلل ما يحدث. عند تشغيل اللعبة في هذه المرحلة ستحصل على هذا الخطأ: Traceback (most recent call last): File "/home/tux/PycharmProjects/game_001/main.py", line 41, in <module> font_size = tx NameError: name 'tx' is not defined يدّل الخطأ صراحةً على عدم تعريف المتغير tx رغم أن المتغير معرف وقد استخدمناه مررًا ضمن البرنامج، ويشير أيضًا إلى السطر رقم 41 لا تعني هذه الإشارة بالضرورة أن الخطأ موجود في السطر 41 بل تعني أن التنفيذ توقف عنده. لو رجعت إلى برنامج اللعبة وتفقدت تعريف المتغير tx ستجده معرفًا مع المغير ty في مقطع الإعدادات ضمن البرنامج وهو يأتي بعد السطر 41 في تسلسل الأسطر. اتضحت الصورة الآن علينا نقل تعريف المتغير إلى موضعٍ آخر ضمن البرنامج ترتيبه يسبق السطر 41 وبذلك يمرّ عليه بايثون قبل الوصول لهذا السطر ويُحل الخطأ. جميع البرامج معرضة للأخطاء وستواجه هذه الحالات دائمًا، قد تكون صعبة أو سهلة دوّن المعلومات التي يظهرها الخطأ وراجع البرنامج في ضوئها بعنايةً وستصل للحل حتى أمهر المبرمجين يواجهون هذه الأخطاء وكلما زاد إتقانك للغة بايثون زادت قدرتك على تفسير الأخطاء وحلها. تشغيل اللعبة شغل اللعبة وتفقد نجاح العملية. يزداد رصيد اللاعب من النقاط مع كل جائزة يغتنمها، وتنخفض مؤشرات سلامته أو صحته كلما اصطدم بعدو. لكنك ستكتشف مع تقدم اللعبة مشكلةً بسيطة أخرى، فنقاط صحة اللاعب تنخفض كثيرًا بطريقةٍ غير عادلة مع كل تصادم بالعدو، وسنعالجها في الفقرة القادمة. لا يعطل هذا النوع من الأخطاء عمل البرنامج فهو ليس من الأخطاء "القاتلة" إنما يعد من المشكلات البسيطة التي تعطي نتائج غير منطقية أو تزعج المستخدم. إصلاح عداد صحة البطل المشكلة في عداد صحة اللاعب أن نقاطه تتناقص مع كل نبضة ساعة يكون فيها العدو والبطل في حالة تصادم، وهذا غير عادلة طبعًا بالعدو بطيء الحركة سينقص صحة اللاعب بمقدار 200- نقطة أو ربما أكثر، يمكنك بكل بساطة زيادة رصيد الصحة الابتدائي للاعبك إلى رقم كبير نسبيًا مثل 10000 سيخفف هذا من أثر المشكلة لكنه لا يعد حلًا مثاليًا. فالحل الأفضل يكتشف حادثتين عوضًا عن واحدة، فلن يكتشف التصادم فقط إنما أيضًا انتهاء التصادم. عرّف المتغير الموجود في السطر الأخير ضمن صنف اللاعب ليمثل حالة التصادم بين البطل والعدو: self.frame = 0 self.health = 10 self.damage = 0 والآن استبدل كتلة التعليمات التالية في دالة التحديث update: for enemy in enemy_hit_list: self.health -= 1 #print(self.health) بهذه الكتلة: if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) يوجد بعض التشابه بين كتلة االتعليمات المحذوفة والكتلة البديلة فكلاهما يؤديان نفس الغرض إلّا أن الجديدة أعقد فهي تعمل لمرة واحدة فقط عند انتهاء التصادم ولا تعمل باستمرار وتنقص النقاط باستمرار مثل التعليمات القديمة، وتسنخدم دالتين من Pygame نتعرف عليهما للمرة الأولى في هذه السلسلة. الدالة الأولى هي self.rect.contains مهمتها معرفة فيما إذا كان العدو متواجدًا داخل المربع المحيط باللاعب في لحظة التنفيذ. والدالة الثانية self.rect.colliderect تعطي المتغير self.damage القيمة 1 إذا كانت نتيجة الدالة الأولى محققة أي أن العدو داخل المربع المحيط بالبطل بغض النظر عن عدد مرات تحققها. وبدءًا من الآن سيُعدّ التصادم بين العدو والبطل لمدة ثلاث ثواني حادث تصادم واحد فقط من وجهة نظر Pygame ويخصم عنه مرة واحدة فقط. يمكننا دومًا اكتشاف هذه الدوال بقراءة توثيقات Pygame وقد وفرت الكثير من العناء والبرمجة، ننصحك دائمًا بقراءة التوثيقات ولا نعني بذلك قراءة كل توثيقات المكتبة أو اللغة وقراءة كل كلمة فيها لكن اهتم بقراءة توثيقات الوحدات الجديدة التي تستخدمها، اطلع على مزاياها سيوفر ذلك عليك إعادة اختراع العجلة كما يقال ويغنيك عن المحاولات المضنية لاكتشاف الحلول لمشاكل محلولة أساسًا. اكتب أخيرًا التعليمات التالية لتكتشف انتهاء حالة التصادم أو التلامس بين اللاعب والعدو وعندها فقط يطرح البرنامج نقطة واحدة من رصيد صحة اللاعب: if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # تصفير قيمة المتغير self.health -= 1 # إنقاص نقطة من صحة اللاعب لا تعمل التعليمات السابقة ما لم يحدث التصادم مع العدو فهي لا تتأثر بركض اللاعب في عالم اللعبة أو جمعه للجوائز والكنوز شرطها الوحيد هو تنشيط المتغير self.damage. وعندما تعمل فإنها تستخدم Self.rect.collidelist لتبيان إن كان تصادم البطل مع عدو ينتمي لمجموعة الأعداء ما زال مستمرًا أم أنه انتهى (ويعطي عنها collidelist القيمة 1-) وبمجرد انتهاء التصادم تعاد قيمة المتغير self.damage وتخصم نقطة من صحة اللاعب. شغل اللعبة الآن. يفتح لك إظهار النتائج خياراتٍ جديدة في لعبتك، يمكنك مثلًا إضافة نوع معين من الجوائز يعوض اللاعب بعضًا من نقاط صحته أو ربما تريد للاعبك أن يعود إلى بداية المستوى عندما يصل رصيد صحته إلى الصفر. كل ما عليك هو تجربة المبادئ التي تعلمتها لتكتشف هذه الأحداث المتنوعة وتطور عالم اللعبة وفق ما تريد. طور مهاراتك طوّر مهارتك في بايثون واستفد من التوثيقات، تعرف على مكاتب ووحداتٍ جديدة وابتكر ألعابًا أو تطبيقات جديدة فهذه اللغة متعددة الأغراض والبرمجة تحتاج للمثابرة، واجعل برامجك مفتوحة المصدر ليساهم معك مطورو بايثون من مختلف أنحاء العالم. سنضيف في المقال التالي بعض الآليات القتالية للعبة، وننهي هذا المقال بتذكيرك ببرنامج اللعبة كامًلا حتى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import pygame.freetype import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (80, 80, 155) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) tx = 64 ty = 64 font_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fonts", "amazdoom.ttf") font_size = tx pygame.freetype.init() myfont = pygame.freetype.Font(font_path, font_size) ''' Objects ''' def stats(score,health): myfont.render_to(world, (4, 4), "Score:"+str(score), BLUE, None, size=64) myfont.render_to(world, (4, 72), "Health:"+str(health), BLUE, None, size=64) # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.damage = 0 self.score = 0 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) if self.damage == 0: for enemy in enemy_hit_list: if not self.rect.contains(enemy): self.damage = self.rect.colliderect(enemy) if self.damage == 1: idx = self.rect.collidelist(enemy_hit_list) if idx == -1: self.damage = 0 # set damage back to 0 self.health -= 1 # subtract 1 hp ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, worldy-ty-80] enemy_list = Level.bad(1, eloc) gloc = [] i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() stats(player.score, player.health) pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add scorekeeping to your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة الجوائز إلى اللعبة المطورة بلغة بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  11. سيُمنح اللاعب بعضًا من الجوائز ليجمعها ويعزز نقاطه في هذا الإصدار من اللعبة المطورة باستخدام بايثون 3 والوحدة Pygame المتخصصة بألعاب الفيديو، والبداية مع روابط مقالات هذه السلسلة. بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. عرضت هذه السلسلة المبادئ الأساسية لتطوير ألعاب الفيديو باستخدام بايثون، ولابدّ أنها منحتك أساسًا جيدًا تنطلق منه لبناء لعبتك الخاصة، لا تتقيد حرفيًا بأسلوب السلسلة، بل استخدم المبادئ نفسها بطرقٍ جديدة، عدّلها لتبرمج عناصرك الخاصة وشروط اللعب التي تريدها. مقال اليوم هو خيرُ مثالٍ على ذلك، إذ ستلاحظ التشابه الكبير في المبادئ بين إضافة المنصات التي تعلمتها سابقًا وإضافة الجوائز، فكلاهما من كائنات اللعبة التي لا تخضع لتحكم خارجي من قبل المستخدم، إنما تُمرر مع عالم اللعبة خلال مستوياتها المتتابعة، وفي الحالتين تحتاج إلى طريقةٍ برمجية لاكتشاف التصادم بين كائن البطل وهذه الكائنات واحتساب تأثيره على نقاط اللاعب وصحته سواءً سلبًا أو إيجابًا. وقبل أن تبدأ جهّز صورًا تمثل الكنوز أو الجوائز التي يجمعها اللاعب، اختر صورًا لعملات معدنية أو جواهر أو ما شابه، يمكنك تحميل حزمة الصور نفسها من موقع kenney.nl التي أخذنا منها صور البلاطات المربعة لبناء منصات اللعبة فهي تتضمن صور جواهر ومفاتيح وكلاهما يفي بالغرض. بناء دالة الجوائز إضافة الجوائز والكنوز إلى لعبة مشابه جدًا لإضافة المنصات كما ذكرنا في المقدمة، في الواقع هو مشابه لدرجة تجعلنا نستدعي صنف المنصة نفسه Platform لإنتاجها. ابدأ بإنشاء دالة جديدة تحمل الاسم loot في صنف المستوى level إذ من المحتمل جدًا أن يتبدل نوع الجوائز بين مستوى وآخر، وتمامًا مثلما فعلنا مع المنصات العائمة والأرضية والأعداء أنشئ في هذه الدالة مجموعة خاصة بكائنات الجوائز سميها loot_list وأضف إليها الجوائز، ستُفيدك هذه المجموعة في عملية اكتشاف التصادمات. def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*9, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list استخدم هذا المثال أبعاد قطع البلاط ty و tx لتحديد إحداثيات ظهور الجوائز من خلال مضاعفاتها للسهولة فقط، ستفيدك هذه الطريقة إن خططت للمستوى على أوراق الرسم البياني مع تمثيل كل بلاطة بمربع أو كان المستوى الذي صممته طويلًا، لكن في النهاية يعود الخيار لك إذ يمكنك عوضًا ذلك كتابة إحداثيات كل جائزة صراحةً برقم مقدر بالبكسل فتصبح هذه الأرقام مدمجة مع برنامج اللعبة hard-coded. لا تنسَ أن باستطاعتك إظهار القدر الذي تريده من الجوائز المتنوعة لكن بشرط أن تضيفها جميعًا لمجموعة الجوائز. أما بالنسبة لوسطاء صنف المنصة Platform في هذه الحالة فهم إحداثيات موقع ظهور الجائزة X و Y بالإضافة إلى أبعاد صورة الجائزة، وننصحك بالتخطيط لأماكن توضع الجوائز منذ البداية وتضمينها في مخطط تصميم المستوى واختيار صور مربعة لتمثيلها تعادل أبعادها أبعاد بلاطات الأرضية والمنصات فهذا سيوفر عليك الكثير. استدعِ الآن دالة الجوائز في مقطع الإعدادات ضمن البرنامج وفق التالي: loot_list = Level.loot(1) وأخيرًا اذكرها ضمن الحلقة الرئيسية لتظهر على الشاشة: loot_list.draw(world) شغل اللعبة ولاحظ النتائج. ظهرت صورة الجائزة بعد تشغيل اللعبة، إلّا أنها لم تُحدث أي تأثير على اللاعب عندما اصطدم بها ولم تُمرر مع تحرك اللاعب وتمرير عالم اللعبة، وهذا هو موضوع فقراتنا اللاحقة. تمرير الجوائز مع عالم اللعبة تمامًا مثل المنصات ينبغي أن تُمرر الجوائز مع عالم اللعبة إلى الأمام والخلف تبعًا لحركة البطل. اكتب السطرين الأخيرين من التعليمات التالية لتُمرر الجوائز إلى الأمام: for e in enemy_list: e.rect.x -= scroll for l in loot_list: # تمرير الجوائز l.rect.x -= scroll # تمرير الجوائز واكتب السطرين الأخيرين من التالي لتُمررها للخلف: for e in enemy_list: e.rect.x += scroll for l in loot_list: # تمرير الجوائز l.rect.x += scroll # تمرير الجوائز تفقد اللعبة الآن واختبر التمرير بالاتجاهين، ولاحظ أن الجوائز أصبحت تتصرف وكأنها عنصر أصيل من عناصر عالم اللعبة وليست مجرد صورة. اكتشاف التصادم استخدم ما تعلمته سابقًا عند اكتشاف تصادم البطل مع الأرضية والمنصات العائمة والأعداء لتكتشف لتصادمه مع الجوائز، الطريقة نفسها تمامًا إلّا أن النتائج المترتبة على التصادم ستكون مختلفة فهي لن تتعلق بالجاذبية ولن تُنقص من صحة اللاعب وأدواره بل ستؤدي إلى زيادة نقاطه واختفاء الجائزة التي اصطدم بها من عالم اللعبة، وذلك عبر إزالتها من مجموعة كائنات الجوائز loot_list ما يعني أنها لن تظهر مجددًا في المرة القادمة التي تُعيد فيها الحلقة الرئيسية رسم عالم اللعبة، وهذا منطقي فالبطل اغتنمها وحاز على بعض النقاط بسببها. اكتب التعليمات التالية في الدالة update ضمن صنف اللاعب Player تمامًا فوق التعليمات المسؤولة عن اكتشاف التصادم مع المنصات، وانتبه للسطر الأخير فهو لتبيان السياق فقط وما من داعٍ لتعيد كتابته: loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) عرّف الآن المتغير الذي استخدمه لزيادة نقاط اللاعب بعد الاصطدام بالجائزة في الدالة __init__ الموجودة في صنف اللاعب وهي المسؤولة عن تحديد خصائصه المختلفة، وذلك بكتابة السطر الثالث من التعليمات أدناه: self.frame = 0 self.health = 10 self.score = 0 طبق المهارات التي تعلمتها في السابق طبق المهارات التي تعلمتها خلال هذه السلسلة وأضف الأعداء في أماكن مناسبة، خطط لذلك ومن ثم عدّل برنامج اللعبة، حاول أيضًا الاستفادة من المبادئ الأساسية، ووظفها بطرقٍ أخرى، وصمم ألعابًا متنوعة وإن كانت وحيدة المستوى في المرحلة كبداية، ولا تنسَ التخطيط الجيد للعبة قبل البرمجة، فكر بأوسع نطاق تريده وبرمج على هذا الأساس، يساعدك ذلك في الحصول على إصداراتٍ جيدة لا تتطلب كثيرًا من التعديلات. وفي ختام المقال نذكرك ببرنامج اللعبة كاملًا من بداية السلسلة. #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 120 BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.score = 0 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list def loot(lvl): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(tx*5, ty*5, tx, ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) enemy_list = Level.bad( 1, eloc ) loot_list = Level.loot(1) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) loot_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Put some loot in your Python platformer game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة خاصية القفز والركض إلى لعبة في بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  12. تناول المقال السابق من هذه السلسلة محاكاةً برمجية لقوى الجاذبية وتأثيرها على تحركات بطل اللعبة، وفي هذا المقال سيتوفر للبطل ما يعينه على مواجهة أثرها ولو للحظات معدودة عبر منحه خاصية القفز. سيَقفز اللاعب وعندما يصل لذروة قفزته ستُطبق عليه الجاذبية من جديد فيعود للسقوط، فما هي الآلية التي تحكم ذلك وكيف يميز البرنامج بين القفز والسقوط؟ الآلية المستخدمة هنا تعتمد على المتغيرات، إذ سنعرف بدايةً متغير خاص يراقب حالة اللاعب ويخبر بايثون بأن اللاعب يقفز الآن ليطبق عليه الجاذبية فيسقط بتأثيرها على أقرب كائن لموقعه من كائنات اللعبة. قبل أن نبدأ نذكرك بمقالات السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. ضبط متغيرات القفز سنضيف متغيرين جديدين إلى صنف اللاعب. يتتبع المتغير الأول حالة القفز عند اللاعب، ويحدد في كل لحظة إن كان الكائن يقفز أم يقف على أرضٍ صلبة. أما المتغير الثاني فيسحب كائن اللاعب إلى أرضية اللعبة. بناءً على ما سبق عرّف المتغيرات المنطقية التالية ضمن صنف اللاعب علمًا أن أول سطرين لتبيان السياق فقط ولا حاجة لإعادة كتابتها: self.frame = 0 self.health = 10 # أدناه المتغيرات الخاصة بالقفز self.is_jumping = True self.is_falling = False إنه الاستخدام الأول للمتغيرات المنطقية في هذه السلسلة والتي تسمى أيضًا بالمتغيرات البوليانية نسبةً إلى عالم الرياضيات جورج بول، تأخذ هذه المتغيرات إحدى قيمتين فقط لا ثالث لهما إما صحيح True أو خاطئ False، أي أن اللاعب يقفز أو لا يقفز وفقًا للمتغير الأول، ويسقط أو لا يسقط وفقًا للمتغير الثاني. لاحظ أننا أعطينا المتغير الأول is_jumping الذي يشير إلى القفز قيمةً ابتدائية هي True، ذلك أن كائن البطل في مثالنا يظهر بدايةً في سماء عالم اللعبة وينبغي سحبه إلى الأسفل مباشرةً ليبدأ اللعب، وكانت هذه أسهل الطرق لإنجاز ذلك رغم أنها لا توظف المتغير المنطقي توظيفًا مثاليًا لكنها تغنينا عن كتابة تعليمات خاصة تحدد موقع إنتاج البطل في كل مستوى، زد على أنها تُذكرنا بالألعاب الكلاسيكية التي يقفز فيها اللاعب إلى عالم اللعبة في لحظة البدء، فاعتمد هذه الخدعة البسيطة وسهّل برنامجك لا بأس في ذلك. أما المتغير الثاني is_falling فقد أُسندت له القيمة الابتدائية False ليهبط اللاعب إلى الأرضية فور بدء اللعبة. شروط تفعيل أثر الجاذبية القفز في العالم الحقيقي هو فعل مقاوم للجاذبية، وفي لعبتنا ستعمل الجاذبية في حالة واحدة فقط هي قفز اللاعب -أي عدم وقوفه على أرضٍ صلبة- وتتوقف في بقية الحالات، أما إن عملت باستمرار كما في المقال السابق فإن اللاعب سيعاني ارتدادًا دائمًا بسبب سحبه للاصطدام بالأرض. الجاذبية في ألعاب الفيديو هي دالة تُبنى خصيصًا في بعض الألعاب لتحاكي الجاذبية الأرضية الحقيقية، وليست ميزة مضمنة في محركات الألعاب مثل Pygame وهي بطبيعة الحال لا تُدار من قبلها، فبعض الألعاب المطورة باستعمال Pygame لا تحتاج للجاذبية مطلقًا وفقًا لقصتها وتصميمها، والسقوط ليس سقوطًا بالمعنى الفعلي للكلمة بل مجرد تحريك للكائن تفرضه الدالة، وحتى تمنح البطل قدرة القفز ومقاومة الجاذبية أو الاصطدام بالمنصات العائمة عليك بالتأكيد تعديل هذه الدالة وجعلها تنشط فقط عندما يقفز. عدّل إذًا دالة الجاذبية التي استخدمناها في المقال السابق لتصبح على الشكل التالي: def gravity(self): if self.is_jumping: self.movey += 3.2 تتسبب هذه الدالة في سقوط البطل إلى أسفل الشاشة مباشرةً متجاوزًا الأرضية والفقرة التالية ستُعالج ذلك من خلال اكتشاف تصادمه مع أرضية اللعبة. برمجة الأرضية الصلبة طبقنا في المقال السابق حيلةً بسيطة لمنع كائن البطل من السقوط بعيدًا إلى أسفل الشاشة وجعلناه يرتد إلى مسافة معينة تبقيه ضمن الشاشة، يمكننا تشبيه هذه الحيلة بإنشاء جدار غير مرئي بارتفاعٍ معين يمتد على عرض الشاشة من الأسفل، لا يسعنا فعليًا التمسك بهذه الطريقة فهي لا تحقق معيار الشيفرة النظيفة في البرمجة، ففي الشيفرة النظيفة تُمثل الكائنات بكائنات برمجية فعلية (ونعني بذلك الجدار الوهمي الذي ذكرناه)، أضف إلى ذلك أننا لابد سنحتاج في بعض الحالات إلى إسقاط بطلنا حتى أسفل عالم اللعبة ربما عقبًا له على قفزة خاطئة أو عند خسارة كل نقاطه أو ما شابه فهذه الحركة شائعة في تصميم الألعاب. اكتب التعليمات التالية في الدالة update ضمن صنف اللاعب: ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # إيقاف القفز # السقوط عبر عالم اللعبة if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty تتحقق هذه التعليمات من حدوث التصادم بين كائن الأرضية وكائن البطل وفق الأسلوب نفسه الذي استخدمناه للتحقق من التصادم بين البطل والعدو. وفي حال حدوث التصادم ستعدّل هذه التعليمات موضع البطل عبر تعديل قيمة self.rect.bottom التي تعني أخفض نقطة في صورة كائن البطل لتصبح مساوية لقيمة g.rect.top التي تعني قمة كائن الأرضية أو أعلى نقطة فيه، وبذلك يبدو البطل كأنه يقف تمامًا فوق أرضية اللعبة ويمنعه ذلك من السقوط إلى الأرضية فهو يقف عليها. علمًا أن هذه التفاصيل (مثل أخفض نقطة وأعلى نقطة في صورة الكائن) هي معلومات توفرها Pygame عن الكائنات عمومًا ويمكنك دائمًا الاستفادة منها. بعد تعديل موضع كائن البطل ستُغَيّر هذه التعليمات قيمة self.is_jumping إلى False ليعرف برنامج اللعبة أن البطل لم يعد في حالة قفز بعد الآن، وقيمة self.movey أيضًا إلى 0 حتى لا يخضع البطل لتأثير الجاذبية (في الواقع هي طرافة في فيزياء اللعبة أن تتوقف عن جذب الكائن إلى الأرض طالما أنه يقف عليها). أما مهمة if الشرطية في الجزء الأخير من التعليمات فهي اكتشاف القفزات الخاطئة لبطل اللعبة التي ينزل بموجبها تحت مستوى الأرضية، لتَخصم بناءً عليها نقطًا من صحته ومن ثم تعيد إنتاجه في أعلى يسار الشاشة باستخدام القيم ty و tx التي تمثل أبعاد بلاط الأرضية وقد استخدمناها للسهولة فقط في حين تستطيع تحديد الموضع الذي تريد، ويمكنك طبعًا عدم خصم نقاط من صحة اللاعب لقاء السقوط دون مستوى الأرضية فأنت من يحدد شروط اللعبة في نهاية المطاف. القفز في Pygame أنشئ أولًا دالة القفز jump لتقلب قيم المتغيرات is_falling و is_jumping. def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True عمليًا الانطلاق الفعلي للقفز يحدث في الدالة update ضمن صنف اللاعب. if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # مقدار علو القفزة تعمل هذه التعليمات بشرط تحقق أمرين أن تكون قيمة المتغير is_jumping هي True وقيمة المتغير is_falling هي False، وستُعَدّل عندها إحداثيات كائن البطل على المحور العمودي Y لتصبح 33- بكسل في فضاء عالم اللعبة وقيمتها سالبة بالطبع لأن الصفر في الأعلى والقيم الأصغر هي الأقرب إلى أعلى الشاشة، يعطيك هذا الرقم قفزةً جيدة لكن يمكنك تعديله لتحظى بقفزاتٍ أعلى أو أخفض. بعد تعديل الإحداثي العمودي تُغَيّر هذه التعليمات قيمة is_falling لتصبح True حتى لا تتراكم القفزات فيُطلق الكائن عاليًا في فضاء اللعبة. استدعاء دالة القفز أشرنا للقفز سابقًا في مقال تحريك شخصية اللعبة عن طريق PyGame وربطناه بالضغط على السهم العلوي من أسهم الاتجاهات على لوحة المفاتيح قبل إنشاء دالة القفز ودون استدعائها وقد اكتفينا حينها بطباعة عبارة تشير للقفز في نافذة الطرفية باستخدام print، والآن يمكننا استدعاء دالة القفز في الحلقة الرئيسية بدلًا من الطباعة. عدّل القيمة المقابلة للضغط على السهم العلوي في الحلقة الرئيسية لتصبح على الشكل التالي: if event.key == pygame.K_UP or event.key == ord('w'): player.jump() يمكنك استخدام زر آخر بدل السهم العلوي مثل زر المسافة إن رغبت بذلك أو الجمع بين الخيارين باستخدام تعليمة if بسيطة، علمًا أن الرمز الذي يعبر عن المسافة في Pygame هو: pygame.K_SPACE الهبوط على المنصات العائمة تعاملنا في فقرةٍ سابقة مع اصطدام اللاعب بأرضية اللعبة وما نتج عنه من إيقاف لعمل الجاذبية، والآن سنعيد الخطوات نفسها مع بقية المنصات العائمة، إذ إن أرضية اللعبة والمنصات في مثالنا لا تنتميان لمجموعة الكائنات نفسها بل لمجموعتين مختلفتين، مع العلم أنك تستطيع دمجها معًا في مجموعةٍ واحدة، والتعامل مع الأرضية بصفتها منصة عادية، لكن المقال اعتمد الخيار الأوسع الذي يسمح بتفضيلات إضافية، في النتيجة السماح لكائن البطل بالوقوف على إحدى المنصات يعني أن يكتشف البرنامج تصادمه مع كائن المنصة ويمنع الجاذبية من سحبه إلى الأسفل. اكتب التعليمات التالية ضمن الدالة update: plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # إيقاف القفز self.movey = 0 # اكتشاف محاولة الوصول للمنصة من الأسفل if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 تفحص هذه التعليمات كافة المنصات العائمة في اللعبة وتتحقق إن حدث أي تصادم بينها وبين كائن البطل، وفي حال ثبت حدوث التصادم تُعدّل قيمة متغير القفز إلى False وتُعطل حركة البطل على المحور العمودي. منصات اللعبة عائمة في الهواء ما يعطي الأريحية للاعب بالوصول إليها من الأعلى أو من الأسفل، عادةً ما تمنع الألعاب وصول البطل للمنصات من الأسفل وتسمح به من الأعلى فقط أي عندما يقفز البطل أعلى من مستوى المنصة، يمكنك تجربة الحالتين لكن هذا المثال تعامل مع المنصات على أنها سقوف والجزء الثاني من التعليمات يسمح للاعب بالقفز على المنصة طالما أن قفزته بلغت مسافةً أعلى من قمة المنصة ويمنعه عندما يحاول القفز عليها من الأسفل. يتحقق السطر الأول من التعليمة if الشرطية فيما إذا كانت قيمة أخفض نقطة من كائن البطل أقل من موقع المنصة ما يعني أن البطل أعلى منها على الشاشة لأن القيم سالبة، وإن تحقق ذلك يُسمح له بالهبوط على المنصة وتتعدل قيمة أخفض نقطة فيه لتصبح مساوية لقيمة قمة كائن المنصة، وإن لم يتحقق الشرط فيزداد الإحداثي Y لموقع لبطل مؤديًا إلى سقوطه نحو الأسفل بعيدًا عن المنصة العائمة التي كان يحاول الوصول إليها. السقوط إذا شغلت اللعبة الآن ستجد أن دالة القفز تعمل وبطلك يتنقل بين المنصات لكنه لا يسقط أرضًا بعد أن يهبط على أي منصة بل يبقى معلقًا في الهواء، والسبب في ذلك أننا عطلنا الجاذبية عند الهبوط على المنصة ولم نكتب أي تعليمة تعيدها للعمل ثانيةً عندما يصل البطل إلى حافة المنصة. عدّل دالة التحديث update في صنف اللاعب لتصبح مسؤولة عن تشغيل وإيقاف تشغيل الجاذبية أثناء حركة الكائن وفق التالي انتبه أنك ستحتاج لإضافة الأسطر التي تتضمن تعليقات فقط. if self.movex < 0: self.is_jumping = True # إيقاف تشغيل الجاذبية self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) if self.movex > 0: self.is_jumping = True # تشغيل الجاذبية self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] إذا فشل التحقق من حدوث تصادم مع إحدى المنصات خلال فترة تشغيل الجاذبية فإن البطل سيسقط أرضًا. شغل اللعبة الآن، ولاحظ أن الوظائف كافة تعمل جيدًا، يمكنك تغيير قيم بعض المتغيرات لتجرب تأثيرها. إليك شيفرة اللعبة كاملًا حتى هذه النقطة، أما في القسم التالي سنمنح البطل القدرة على الركض بحرية عبر تمرير عالم اللعبة جانبيًا. #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) تفعيل الركض ذهابًا وإيابا في عالم اللعبة سنعطي البطل في هذه المرحلة القدرة على الركض ذهابًا وإيابًا عبر تمرير عالم اللعبة جانبيًا. خططت في مقالٍ سابق مستوى للعبة يتضمن عدة منصات وعندما نفذته لم تظهر كافة المنصات على شاشة العرض فقد تجاوزت أبعادها وأعدادها مساحة الشاشة الواحدة، والتمرير الجانبي لعالم اللعبة أو ما يسمى side-scroller هو الحل الأمثل لهذه الحالة ولإظهار المستوى المخطط له كاملًا. تمامًا كما يوحي الاسم، ستتحرك المنصات جانبيًا كلما اقترب البطل من حافة الشاشة لتعطي المستخدم شعورًا يشبه مشاهدة الفيديو وكأن كاميرا متحركة تتنقل في عالم اللعبة وتصوره، يتطلب التمرير حجز منطقة ميتة على جانبي الشاشة الأيمن والأيسر وعندما يصل البطل إلى هذه المناطق سيبقى ثابتًا بينما يُمرر العالم جانبيًا من حوله وتتحرك المنصات. تثبيت نقاط التمرير الجانبي على الشاشة إن رغبت بالسماح للاعب بالتحرك إلى الأمام والخلف فستحتاج إلى نقطتي تمرير على جانبي الشاشة، أما لو سمحت له بالتقدم للأمام فقط تكفيك عندها نقطةٌ واحدة. سنحدد نقاط التمرير باستخدام المتغيرات بمسافة 100 أو 200 بيكسل عن كل جانب من جوانب الشاشة. عرّف إذًا متغيرات التمرير في مقطع المتغيرات ضمن برنامجك على الشكل التالي: forwardx = 600 backwardx = 230 والآن تحقق من اللحظة التي يصل فيها بطلك إلى نقاط التمرير ضمن الحلقة الرئيسية للبرنامج وذلك عبر تعليمة if الشرطية، وإذا وصل اللاعب إلى إحدى هذه النقاط حرك عندها عالم اللعبة يمينًا أو يسارًا. اكتب التعليمات التالية في حلقة التكرار الرئيسية للبرنامج وانتبه أن لا تتداخل مع الحلقة for الخاصة باكتشاف الضغط على أزرار لوحة المفاتيح، الأسطر الثلاثة الأخير هي للسياق فقط لن تحتاج لإعادة كتابتها: # تمرير عالم اللعبة إلى الأمام if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll # تمرير عالم اللعبة إلى الخلف if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll # تعليمات التمرير أعلى هذا السطر world.blit(backdrop, backdropbox) player.gravity() # فحص الجاذبية player.update() شغل اللعبة الآن وجرب التمرير. ستلاحظ بعد التشغيل أن التمرير الجانبي يعمل جيدًا والمنصات تتحرك مع البطل إلى الأمام والخلف أما العنصر الوحيد الذي لا يستجيب للتمرير فهو كائن شخصية العدو. اكتب التعليمات اللازمة لتمرير إن أردت أن يُمرر مثلهم، وتستطيع طبعًا تمريره للأمام فقط أو للخلف فقط أو بكلا الاتجاهين ليلاحق بطل اللعبة. تمرير العدو مع عالم اللعبة طبق التعليمات نفسها الخاصة بتمرير المنصات على شخصية العدو، والأفضل لك أن تطبقها على المجموعة التي تضم جميع كائنات العدو إذ إن اللعبة تتضمن أكثر من عدو، هذه ميزة جمع العناصر المتشابهة في مجموعات فهي تسهل على المبرمج تطبيق التعليمات عليها جميعًا دفعةً واحدة. أضف السطرين الأخيرين من التعليمات التالية ضمن الحلقة الرئيسية في كتلة التمرير إلى الأمام لتمرير العدو إلى الأمام: # تمرير عالم اللعبة إلى الأمام if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: # تمرير العدو e.rect.x -= scroll # تمرير العدو ومن ثم السطرين الأخيرين من هذه التعليمات إلى كتلة التمرير إلى الخلف ضمن الحلقة الرئيسية لتمرير العدو بالاتجاه الآخر: # تمرير عالم اللعبة إلى الخلف if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: # تمرير العدو e.rect.x += scroll # تمرير العدو والآن شغل اللعبة وتفقد النتائج. إليك الشكل النهائي لبرنامج اللعبة حتى هذه المرحلة ليبقى مرجعًا لديك: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) forwardx = 600 backwardx = 230 BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.is_jumping = True self.is_falling = True self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): if self.is_jumping: self.movey += 3.2 def control(self, x, y): """ control player movement """ self.movex += x def jump(self): if self.is_jumping is False: self.is_falling = False self.is_jumping = True def update(self): """ Update sprite position """ # moving left if self.movex < 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.is_jumping = True self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 # print(self.health) ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.bottom = g.rect.top self.is_jumping = False # stop jumping # fall off the world if self.rect.y > worldy: self.health -=1 print(self.health) self.rect.x = tx self.rect.y = ty plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.is_jumping = False # stop jumping self.movey = 0 if self.rect.bottom <= p.rect.bottom: self.rect.bottom = p.rect.top else: self.movey += 3.2 if self.is_jumping and self.is_falling is False: self.is_falling = True self.movey -= 33 # how high to jump self.rect.x += self.movex self.rect.y += self.movey class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((550, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump() if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: # enemy scroll e.rect.x -= scroll # enemy scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: # enemy scroll e.rect.x += scroll # enemy scroll world.blit(backdrop, backdropbox) player.update() player.gravity() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Add jumping to your Python platformer game والمقال Enable your Python game player to run forward and backward لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: محاكاة أثر الجاذبية في لعبة بايثون تصميم وبرمجة لعبة قفز باستخدام سكراتش إنشاء السلالم وختام اللعبة
  13. يدعم تطبيق الشفافية ازدهار المجتمعات مفتوحة المصدر، لذا ينبغي على مديري هذه المجتمعات إدراك ذلك والسعي لتوفير الشفافية بمستوياتها الخمسة على أوسع نطاق ضمن مجتمعاتهم، وفي هذا المقال سنُعرّف كل مستوى من هذه المستويات، ونعرض أهميته وأهدافه. ما الذي يجعل الشفافية مهمة في المجتمعات مفتوحة المصدر؟ خلصنا إلى الأسباب التالية التي تجعل الشفافية مهمة في المجتمعات مفتوحة المصدر وهي: ترفع من مستوى الثقة بين المجتمعات ما يزيد التعاون المثمر بينها. تعمل بفضلها المجتمعات معًا وتتبادل الخبرات بانفتاح. تمنع الاحتكاك بين الأطراف. تتطلب من المدراء عرض النتائج والتقارير بشفافية أمام المساهمين. تشجع على عرض المعلومات الكاملة عن المجتمعات، وهو أمر صحي للغاية ويحفز بناء الثقة المتبادلة. ما هي المستويات الخمسة للشفافية؟ هنالك خمسة مستويات من الشفافية التي تساهم مساهمة كبيرة في ازدهار المجتمعات مفتوحة المصدر وهي: نشر الكود المصدري نشر الإرشادات للمجتمع الاحتفاء بالنجاح الاستماع إلى نبض المجتمع وتحليله الحفاظ على المجتمع سنشرحها تباعًا. المستوى الأول: نشر الكود المصدري يتمحور هذا المستوى حول إصدار الكود المصدري للبرمجيات بموجب ترخيص معتمد من مبادرة المصدر المفتوح OSI وبإصدار عام متاح للجميع عبر نظام خاص بإدارة الإصدارات مثل Git. يهدف المستوى الأول إذًا إلى تأسيس المشروع ليكون مفتوح المصدر. النقطة الأولى بديهية، المشروع مفتوح المصدر يعني إتاحة الكود المصدري للمشروع. الكود المصدري المرخص من مبادرة المصدر المفتوح OSI الذي يتشارك الجميع العمل عليه هو جوهر المشروع مفتوح المصدر. يتيح نظام إدارة الإصدارات التعاون ويسمح لكل من يرغب بتحليل المشروع والمساهمة فيه. المستوى الثاني: نشر الإرشادات للمجتمع يهتم المستوى الثاني من الشفافية بتوفير التوثيقات والمصادر وكل ما يلزم لإرشاد المساهمين، وتنظيم فعاليات خاصة لنشر المعرفة ضمن المجتمع. يتجلى هدف هذا المستوى بإنشاء وتنمية مجتمع المشروع مفتوح المصدر. بناء مجتمع فاعل هو مفهوم أوسع بكثير من مجرد إتاحة الكود المصدري. بقدر ما تتحلى بالشفافية حول كيفية عمل مشروعك، وكيف يمكن للآخرين المساهمة فيه، تزيد فرص انضمامهم إلى المشروع وتطويره. تتضمن تنمية المجتمع تنظيم الفعاليات والأنشطة للمساهمين. المستوى الثالث: الاحتفاء بالنجاح عندما تصل لهذا المستوى ستغدو مشاركة الأفكار والرؤى وعرض تطورات المشروع من أهم أولوياتك. ينصب هدف المستوى الثالث على الاحتفاء بالنجاح والإعلان عنه وتأمين المزيد من الدعم للمراحل اللاحقة. يزداد تتبع الأخبار صعوبةً مع توسع المجتمعات مفتوحة المصدر، ويتعذر على الجميع معرفة المستجدات في كل مكان. أن تعرض النتائج والنشاطات بشفافية في أرجاء المجتمع، يعني أن تمنح المساهمين شعورًا بالتقدير وبأن مشاركاتهم مهمة ومرئية. تتميز التقارير والتحليلات في هذا المستوى بكونها متفرقة ولا تعتمد أدواتٍ خاصة. المستوى الرابع: الاستماع إلى نبض المجتمع وتحليله الاستماع إلى نبض المجتمع ومراقبة تطوره هو جوهر المستوى الرابع من الشفافية، بالإضافة إلى تطوير برامج خاصة تساعد على اتخاذ الإجراءات التصحيحية. أما هدف هذا المستوى فيتمثل في نقل المجتمع إلى المستوى التالي عبر فهم مسار تطوره بطرق علمية ودقيقة. تساعد التقارير وأدوات التحليل في مراقبة ما يحدث. يمكنك مقارنة الأحداث في المجتمع، وتتبع تفاعل المساهمين معها. الاستماع الدائم والتحليل يقود إلى استنباط رؤى أعمق وأكثر إفادة عن المجتمع. المستوى الخامس: الحفاظ على المجتمع يركز المستوى الخامس والأخير على مقاييس المجتمع وتحسين المشاركة المجتمعية من أجل الحفاظ عليه ورعايته على المدى الطويل مهما حصلت من متغيرات. يسعى هذا المستوى إلى اتخاذ قرارات هادفة ومؤثرة تدعم المشاركة المجتمعية. تطبيق نظم مرنة للتفاعل مع التغيرات في مقاييس المجتمع. تحليل وتتبع التغيرات التي يمكن أن تطرأ على المجتمع. وضع قواعد للمشرفين والمطورين مثل اتفاقيات مستوى الخدمة SLA وتحمل المسؤولية ليجعلوا المشاركة المجتمعية في صدارة أهدافهم، مع الحرص على وجود نظم تضمن تحقيق ذلك. الخلاصة يحتاج مديرو المجتمعات مفتوحة المصدر إلى تطبيق هذه المستويات الخمسة من الشفافية حتى يدعموا تطور وازدهار هذه المجتمعات. ترجمة -وبتصرف- للمقال ‎5 levels of transparency for open source communities لأصحابه Georg Link و Anirudha Jadhav و Emilio Galeano Gryciuk. اقرأ أيضًا تراخيص البرامج مفتوحة المصدر ما الذي يحفز المساهمين لتطوير المشاريع مفتوحة المصدر؟ الفرق بين البرمجيات الحرة ومفتوحة المصدر
  14. عالمنا الحقيقي مليء بالحركة، والفيزياء هي ما يحكم ويفسر كافة ظواهره المتعلقة بحركة المادة، أما عالم ألعاب الفيديو فهو خالٍ من المادة ما يعني أنه منطقيًا خالٍ من الفيزياء أيضًا، لذا ينبغي على مطوري الألعاب محاكاة الفيزياء في ألعابهم لمنحها لمسة واقعية، وأكثر المبادئ الفيزيائية أهمية في عالم الألعاب هي الجاذبية والتصادم. تعلمنا قليلًا عن التصادم عندما أضفنا شخصية العدو إلى اللعبة، لكننا سنتناوله بمزيدٍ من التعمق في هذا المقال والذي يليه فالجاذبية تفرض علينا ذلك، فما هي العلاقة بين الجاذبية والتصادم؟ ستجيبك عن ذلك تعليماتنا البرمجية اللاحقة. الجاذبية في العالم الحقيقي كما نعرفها جميعًا من دروس الفيزياء هي ميل الأجسام ذات الكتلة إلى التجاذب المتبادل فيما بينها، وكلما زادت كتلة الجسم زاد تأثير الجاذبية التي يمارسها على غيره من الأجسام، فكيف سينعكس ذلك في عالم الألعاب؟ في الواقع الأثر الأوضح للجاذبية الذي ينبغي علينا محاكاته في الألعاب هو ميل الأجسام للسقوط تجاه الكائن الذي يملك افتراضيًا أكبر كتلة في عالم اللعبة، وهو عالم اللعبة نفسه. قبل أن نبدأ نذكرك بمقالات السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. إضافة دالة الجاذبية سنستفيد من خاصية الحركة التي يملكها كائن اللاعب حتى نسحبه أو نحركه باتجاه أسفل شاشة العرض، بما يحاكي أثر الجاذبية الأرضية، وتذكر أن صفر المحور العمودي في Pygame يقع في الأعلى ما يعني أن الإحداثيات الأكبر هي أكثر قربًا لأسفل الشاشة. الجاذبية انتقائية في ألعاب المنصات فالمبرمج يختار أثرها ولن تؤثر على كل شيء كما في العالم الحقيقي، فلو فعلت ذلك لرأيت منصات اللعبة بأكملها تسقط إلى الأرضية، لذا سنطبق أثر الجاذبية على شخصيتي اللاعب والعدو فقط. أضف أولاً دالة الجاذبية في صنف اللاعب: def gravity(self): self.movey += 3.2 # معدل سرعة سقوط اللاعب الدالة بسيطة فقد ضبطنا حركة اللاعب العمودية لتكون باتجاه أرضية اللعبة دائمًا بغض النظر إن كان أساسًا بحالة حركة أو سكون، أي أنه سيسقط دائمًا وهذا بالفعل معنى الجاذبية الأرضية، واستخدمنا لذلك خاصية الحركة العمودية التي يمتلكها اللاعب. اكتب دالة الجاذبية في الحلقة الرئيسية ليصبح أثرها دائمًا كما في السطر الأول من التعليمات أدناه وبذلك سيُطبقها بايثون على اللاعب مع كل نبضة ساعة. player.gravity() # تحقق من الجاذبية player.update() شغل لعبتك الآن وشاهد النتائج، سيسقط اللاعب سقوطًا حادًا وسريعًا من سماء عالم اللعبة إلى أسفل الشاشة، ربما ترغب بتغيير مقدار سرعة السقوط لتحظى بنتيجة أفضل، لكن أثر الجاذبية يعمل وهذا يكفي في هذه المرحلة. تعيين أرضية الجاذبية ليقف عندها اللاعب بعد السقوط دائمًا ما يلي السقوط إجراءٌ معين في ألعاب الفيديو، قد يكون مسح اللاعب عن شاشة اللعبة وإعادة إنتاجه في مكانٍ مختلف، أو خصم بعض النقاط من رصيده أو ما شابه، بالنتيجة أيًا كان الإجراء الذي تريده فينبغي أولًا إيجاد طريقة تجعل البرنامج يكتشف حادثة السقوط ويقرر إن سقط اللاعب واختفى عن الشاشة، أم أن ذلك لم يحدث، ويُتخذ بعدها الإجراء المطلوب، أما أنسب تعليمة لهذا النوع من الكشف فهي تعليمة if الشرطية. ستتحقق إذًا فيما إذا سقط لاعبك أم لا، وماهو مدى سقوطه، فلو سقط بعيدًا جدًا متجاوزًا الأرضية إلى أخفض نقطة في الشاشة فالوضع غير مقبول ويحتاج لبعض التعديلات فمنطيقًا كان عليه الاستقرار على أرضية اللعبة. لنبسط الموضوع اضبط موقع اللاعب عند 20 بكسل، وعدّل دالة الجاذبية لتصبح: def gravity(self): self.movey += 3.2 # معدل سرعة السقوط if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty ومن ثم شغل اللعبة ولاحظ أن اللاعب يسقط من الأعلى ويتوقف عند أسفل الشاشة، حتى أنك قد لا تتمكن من رؤيته خلف كائن الأرضية، سنعالج الموضوع بإضافة ty- ثانية إلى موقعه الجديد تجعله يرتد أعلى بعد اصطدامه بأسفل الشاشة فيظهر على مسافة معينة ضمن الشاشة، وذلك وفق الأوامر التالية: def gravity(self): self.movey += 3.2 # how fast player falls if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty-ty يحتاج بطل اللعبة إلى طريقة أو قوة دفع تساعده على مقاومة أثر الجاذبية ليجتاز المستويات ويتابع اللعب هذه القوة هي القفز سيناقشها المقال التالي مع توسع إضافي في تصادمه مع الأرضية والمنصات المختلفة. حاول بنفسك تطبيق أثر الجاذبية على كائن شخصية العدو، وانظر الشكل النهائي لكود اللعبة متضمنًا كل فعلناه في السلسلة حتى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def gravity(self): self.movey += 3.2 if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty-ty def control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((500, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.gravity() player.update() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) كان هذا المقال السابع من سلسلة بناء لعبة من الصفر باستخدام بايثون 3 و Pygame. ترجمة -وبتصرف- للمقال Simulate gravity in your Python game لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame إنشاء الجاذبية في اللعبة النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  15. تتحدث كاتبة المقال عن تجربتها الشخصية مع البرمجيات مفتوحة المصدر، وبالتحديد عن الخطأ الأول الذي ارتكبته في بداية مشوارها، وكاد يثنيها عن الشروع بالعمل، وقد عنونت المقال بمتلازمة المحتال Impostor Syndrome في إشارة منها إلى ذلك الشعور الداخلي الذي يتغلب علينا في بعض الأحيان ويزعزع ثقتنا بأنفسنا عبر التشكيك بقدراتنا وما أنجزناه، علّ المقال يكون مفيدًا لكل راغب بدخول هذا المجال. تعلّمتُ -تقول الكاتبة- أساسيات البرمجة من خلال الإنترنت، اتبعت عددًا من البرامج التعليمية وأنجزت المشاريع الملحقة بها، وأحرزت تقدمًا، لكني تساءلت دومًا ماذا بعد؟ ما هي خطوتي التالية للارتقاء إلى المستوى الأعلى؟ وفي هذه المرحلة صادفني منشور على إحدى المدونات يخبر القارئ أن المساهمة في المشاريع مفتوحة المصدر هي الطريقة المثلى لكل مبتدئ يرغب بتطوير مهارته البرمجية والانتقال إلى مستوى أعلى. إذًا بعد أن قرأت ذلك المنشور انتابني شعورٌ قوي أني وصلت للطريق الصحيح، وأن المساهمة في المشاريع مفتوحة المصدر هي السبيل الوحيد لأغدو مطورًا حقيقيًا، لذا بدأت باتباع الإرشادات التي يذكرها المنشور وأولها إنشاء حساب على GitHub، وخلال أقل من عشر دقائق شعرت بالارتباك الشديد لدرجة جعلتني أتخلى عن الفكرة تمامًا. لم أكن شخصًا لا يحب التعلم بل على العكس، لكن المصادر التي اعتمدتها تتوقع من القارئ أن يمتلك معرفةً جيدة ومسبقة بمنصة Git و GitHub وكيف أنها أتاحت للعديد من المطورين حول العالم أن يتعاونوا في مشروعٍ واحد. لذا عدت إلى دروسي ولكن الرغبة في دخول المجال لم تفارقني أبدًا، وعشت صراعًا داخليًا لسنوات بين أن المساهمة في المصدر المفتوح تجربة سهلة وقيمة وينبغي أن أخوضها، وذلك الانطباع الداخلي بأني لست أهلًا بعد لكتابة الأكواد في مشاريع مفتوحة المصدر. لازمتني مشاعر عدم الثقة حتى بعد أن تآلفت مع Git، استمر جدل المحتال الداخلي بأني لم أكتب كودًا لمشاريع حقيقية بعد بل مجرد أكواد لمشاريع تعليمية وتدريبية بل حتى تجارب، وأني لست مؤهلةً للمساهمة في مشاريع حقيقية يستخدمها الآخرون ويعتمدون عليها، في الواقع حتى عندما عملت بصفتي مهندسة برمجيات وكتبت أكوادًا حقيقية، انتابتني المشاعر نفسها، وكانت الحجة أنني أعمل مع لغة برمجة واحدة، فما الذي يضمن نجاحي في مكانٍ آخر ومع أطر عمل وربما لغاتٍ برمجية أخرى. استغرقت فعليًا حوالي السنة ونصف من العمل الجاد بدوامٍ كامل، لأشعر أخيرًا بالثقة الكافية لإغلاق حجج المحتال الداخلي، وتقديم طلب السحب الأول PR على GitHub وبدء رحلتي مع البرمجيات مفتوحة المصدر. أساطير ضارة سنكشف أمامكم زيف اثنتين من أكثر الأساطير الضارة انتشارًا حول البرمجيات مفتوحة المصدر. الأسطورة الأولى: المساهمة في البرمجيات مفتوحة المصدر سهلة تكررت أمامي مرارًا خلال هذه الرحلة رسالةٌ مفادها أن المساهمة في البرمجيات مفتوحة المصدر أمرًا سهلًا، وعندما لم أتمكن من البدء بسهولة بدأت أتساءل عن مهاراتي الخاصة وأشك فيها. أما الآن أفهم معنى هذه الرسالة بطريقةٍ مختلفة، فعندما يصف الناس هذه المساهمة بالسهلة، فهم يقصدون أنها هدف قابل للتحقيق ويمكن للمبتدئين الوصول إليه إن اجتهدوا، أو أنك تستطع المساهمة في البرمجيات المفتوحة المصدر دون كتابة الكثير من التعليمات البرمجية المعقدة، هذا كله صحيح ولكن الأهم أن تعرف أن هذه المساهمة في الواقع صعبة وتتطلب منك وقتًا كافيًا تفهم خلاله قاعدة شيفرة جديدة وأدوات خاصة يستخدمها المطورون. بالتأكيد لا نريد إثناء المبتدئين عن المحاولة، لكننا نرغب في إخبارهم أن مواجهة التحديات أمرًا طبيعيًا ومتوقعًا للغاية خلال العملية. الأسطورة الثانية: لابد أن يساهم كل مطور حقيقي بالبرمجيات مفتوحة المصدر لطالما ذكرني المحتال الداخلي بأن عدم مساهمتي في البرمجيات مفتوحة المصدر يعدّ نقصًا واضحًا ووصمة عار في مسيرتي المهنية، بصراحة حتى وأنا أكتب هذا المقال أشعر بالذنب لأنني لم أساهم أكثر في المجتمع مفتوح المصدر. العمل على هذه البرمجيات طريقة رائعة للتعلم والمشاركة في مجتمع المطورين الأوسع هذا كله لا ريب فيه، إلّا أنها بالتأكيد ليست الطريقة الوحيدة، إذ يمكنك التدوين أو حضور اللقاءات أو العمل في مشاريع أخرى أو القراءة أو الإرشاد أو حتى العودة إلى المنزل في نهاية يوم عمل طويل للاستمتاع بلقاء ودي مع الأهل أو الأصدقاء. ما نريد قوله في النتيجة أن المساهمة في البرمجيات مفتوحة المصدر تحدي ممتع ومفيد حقًا، لكن بشرط أن يكون هو التحدي الذي اخترته. خطئي الأول في المساهمة بالمشاريع مفتوحة المصدر خطئي الأول كان السماح للمحتال الداخلي بالتحكم بقراري، فقد جعلني أتردد لسنوات بحجة أني لست مؤهلةً بعد، حتى أني لم أفهم في حينها حجم العمل الذي أحتاجه للوصول إلى مستوى أشعر معه بالثقة التامة لكتابة كود في مشروع غير مألوف (وهي بالمناسبة نقطة ما زلت أسعى تحقيقها حتى اليوم)، زد على ذلك أني استسلمت لفكرةٍ مفادها أن المساهمة في المجتمع مفتوح المصدر هي الطريقة الوحيدة لإثبات جدارتي بصفتي مطور جيد. في النهاية بدأت، وقدمت طلب السحب الأول على أحد المشاريع واسعة النطاق، ولكن انعدام الثقة بالنفس أفسد علي التجربة بأكملها وجعلها أقل إمتاعًا. لا تدع Git يحبط عزيمتك إذا كنت تحاول تعلم المزيد عن Git ووجدت صعوبة بذلك، أو كنت مبتدئًا ويمنعك Git من تقديم مساهمتك الأولى في هذا المجتمع، فلا تقلق لست الوحيد الذي يشعر بذلك لأن Git معقد بالفعل، ولا يُتوقع منك أن تتعلمه على الفور وتفهمه بمجرد إلقاء نظرة عليه فهو يحتاج وقتًا، ولكن ما إن تتعلمه حتى تكتشف فائدته الجمّة وما يقدم من تسهيلات للمطورين ليعملوا معًا على مشروعٍ واحد وفي وقت واحد ومن ثم قيامه بدمج تعديلاتهم الفردية في الكود النهائي. يوجد العديد من المصادر الجيدة لتتعرف على Git و GitHub (وهو الموقع الذي يستضيف الشيفرات البرمجية ويتيح للأشخاص أن يتعاون عليها باستخدام Git)، ويمكنك أن تبدأ من قسم Git على أكاديمية حسوب سيشكل نقطة انطلاق جيدة إضافة إلى أنه متاح باللغة العربية إذ فيه عدة سلاسل ومقالات مفيدة. أنصحك أن تنتقل إلى سلسلة "المساهمة في المشاريع مفتوحة المصدر" وأن تبدأ بقراءة المقال الأول فيها كيفية المساهمة في المشاريع مفتوحة المصدر: ابدأ بتعلم نظام Git فهي سلسلة مفيدة مؤلفة من ستة مقالات، كما تجد بالقسم أيضًا سلسلة عن تعلم Git مؤلفة من سبعة مقالات. ترجمة -وبتصرف- للمقال My first contribution to open source: Impostor Syndrome لصاحبته Galen Corey. اقرأ أيضًا ما هي البرمجيات مفتوحة المصدر؟ تراخيص البرامج مفتوحة المصدر الإشراف على مشاريع البرمجيات مفتوحة المصدر عبر غيت هب GitHub الفرق بين البرمجيات الحرة ومفتوحة المصدر
  16. يتمحور المقال السادس من سلسلة (بناء لعبة من الصفر باستخدام بايثون) حول إضافة المنصات إلى عالم اللعبة لتتفاعل معها الشخصيات وتتنقل عبرها فهذه اللعبة أولًا وأخيرًا لعبة منصات، ولكن قبل أن نبدأ نذكرك بمقالات السلسلة. بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. المنصات في Pygame كائنات مثلها مثل الشخصيات، تُضاف إلى عالم اللعبة بالطريقة نفسها عبر برمجة كائن بايثون خاص بكل منصة وتحديد إحداثيات ظهورها على الشاشة، ويسهل هذا التشابه برمجة التفاعل بين الطرفين. برمجة كائن المنصة تمامًا مثل بناء كائن شخصية اللاعب فإن بناء كائن للمنصة يعني إنشاء صنف بلغة بايثون، سنسميه Platform ويتطلب إنشائه عددًا من المعلومات عن المنصة مثل نوعها وصورتها وموقع ظهورها على الشاشة، قد لا تملك هذه المعلومات جميعها في لحظة بناء الصنف ولا بأس في ذلك فستُمررها له فيما بعد بالطريقة نفسها التي اتبعناها عند تحريك اللاعب فلم نحدد السرعة حتى نهاية المقال. عرّف الصنف وفق الآتي: # ‫x الإحداثي, y الإحداثي, imgw عرض الصورة, imgh ارتفاع الصورة, img ملف الصورة class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc لابد أنك لاحظت تشابه التعليمات بين هذا الصنف وأصناف شخصيتي البطل والعدو، ومثلهما أيضًا سيؤدي استدعاؤه إلى إنشاء كائن المنصة وإظهار صورتها على الشاشة بالأبعاد المحددة ضمن الصنف وفي الموقع المحدد وفق الإحداثيات X و Y. أنواع المنصات هناك نوعان للمنصات وهي: المنصات المبنية بطريقة قطع البلاط تُذكرنا هذه المنصات بالألعاب الكلاسيكية الشهيرة مثل سوبر ماريو والقنفذ سونيك، ويعدّها البعض من أسهل طرق بناء المنصات من ناحية الصور المطلوبة -وإن لم تكن كذلك لجهة تعقيد الشيفرة والعمليات الحسابية- فكل ما تحتاجه هو مجموعة صور لقطع البلاط أو القرميد أو ما شابه، ومن ثم تبدأ بترتيبها بأشكالٍ مختلفة ضمن اللعبة، حوالي 8 أو 12 نمط تشكل بها الأرضية والمنصات العائمة وتكررها لتبني مستويات اللعبة. المنصات المبنية بطريقة الرسم اليدوي تعني هذه الطريقة رسم أو تصميم كل منصة بصورة منفصلة، وتتطلب منك بذل بعض الوقت والجهد في العمل على برامج التصميم الجرافيكي لإنشاء كل جزء من عالم اللعبة، إلاّ أنها بالمقابل تحتاج قدرًا أقل من العمليات الحسابية، إذ إن كافة الكائنات أو المنصات مجهزة وكاملة وكل ما عليك فعله هو إخبار بايثون بمكان إظهارها على الشاشة. لكل طريقة مزايا وعيوب وأسلوب معين في كتابة التعليمات البرمجية، لذا سنشرح الطريقتين في الفقرات اللاحقة حتى تتمكن من استخدام الطريقة التي تناسبك أو تدمج الطريقتين معًا في مشروعك. وضع مخطط المستوى تعدّ هذه المرحلة جزءًا مهمًا من تصميم المستوى للعبة، يمكنك البدء باستخدام الورقة والقلم، ارسم مربعًا يمثل نافذة اللعبة وعليه المحورين الأفقي والعمودي، ومن ثم ابدأ برسم المنصات التي تريدها واحدةً تلو الأخرى وثبت على كل منها الإحداثيات X و Y وكذلك أبعاد الصورة عرض وارتفاع، لا داعِ لتكون المواقع على الورقة دقيقة جدًا لكن اجعلها منطقية، فمثلًا لا يمكنك وضع 8 قطع قياس كل منها 100 بكسل في شاشة واحدة حجمها 720 بكسل. بعد إنجاز شاشتك الأولى، استمر بالرسم تجاه اليمين حتى تمثل الشاشات التالية التي سينتقل خلالها بطل اللعبة وهو يجتاز المستوى حتى يصل لنهايته. ولو رغبت بتعزيز المخطط بمزيدٍ من الدقة استخدم أوراق الرسم البياني المقسمة إلى مربعات متساوية، فهي مفيدة خصيصًا في رسم منصات البلاط، إذ يمكنك تمثيل كل بلاطة بمربع. الإحداثيات كل ما تعلمته في المدرسة عن نظام الإحداثيات الديكارتي ينطبق هنا على إحداثيات Pygame باستثناء أن نقطة التقاء المحاور أو نقطة الصفر 0,0 تقع في الزاوية العلوية اليسرى للشاشة بدلًا من الوسط مثلما اعتدت سابقًا في دروس الهندسة. يبدأ المحور X عند النقطة 0 في أقصى اليسار ويمتد لا نهائيًا نحو اليمين، كذلك يبدأ المحور Y عند النقطة 0 في أعلى الشاشة لكنه يمتد للأسفل. أبعاد الصور معرفة أبعاد الصور التي تمثل كلًا من الأبطال والأعداء والمنصات مهمة جدًا ليكتمل مخطط المستوى، فبدونها سيغدو مخططك دون فائدة تُذكر، فكيف تعرف هذه الأبعاد؟ لديك طريقتان، إما أن تفتح الصور باستخدام برنامج تصميم متخصص مثل كريتا وتستعلم عن خصائصها، أو تكتب برنامج بسيط بلغة بايثون يعطيك إياها وذلك بالاستفادة من الوحدة Pillow التي توفر مكتبة خاصة بالموضوع تسمى PIL، ولكن طبعًا بعد ذكر الوحدة ضمن ملف متطلبات المشروع requirements.txt وفق مايلي: pygame~=1.9.6 Pillow أنشئ ملفًا جديدًا ضمن Pycharm باسم identify، واكتب فيه: #!/usr/bin/env python3 # GNU All-Permissive License # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. from PIL import Image import os.path import sys if len(sys.argv) > 1: print(sys.argv[1]) else: sys.exit('Syntax: identify.py [filename]') pic = sys.argv[1] img = Image.open(pic) X = img.size[0] Y = img.size[1] print(X, Y) والآن اضغط على تبويب الطرفية الموجود في أسفل شاشة Pycharm، الذي يفتح لك نافذة الطرفية في بيئتك الافتراضية واكتب فيها التالي حتى تثبت الوحدة Pillo. (venv) pip install -r requirements.txt Requirement already satisfied: pygame~=1.9.6 [...] Installed Pillow [...] وعند انتهاء التثبيت شغل البرنامج البسيط الذي كتبناه للحصول على الأبعاد، وشغله من مسار مجلد اللعبة. (venv) python ./identify.py images/ground.png (1080, 97) بذلك تكون قد حصلت على أبعاد صورة الأرضية، وهي في حالتنا 1080 عرض و97 ارتفاع. كتل المنصة إذا رغبت بتصميم صورة خاصة لكل مُكَوِّن من مكونات اللعبة مثل المنصات والأرضية وغيرها، فهذا يعني أنك ستخصص ملفُا منفصلًا لكل مُكَوِّن مثل الظاهر في الصورة: ويمكنك إعادة استخدام كل منصة عدة مرات ضمن لعبتك، بشرط أن يتضمن كل الملف مُكَوِّن واحد فقط كما ذكرنا، فلا يجوز مثلًا أن تتواجد جميع المكونات المبينة أدناه في ملف واحد: وعلى فرض أنك أنشأتها جميعًا في ملف واحد كبير يتضمن كل مكونات الصورة، فلن تملك عندها أي طريقة تجعلك تُمييز المنصة عن خلفية اللعبة، لذا إما أن ترسم المكونات التي تريدها كلٌ في ملفه الخاص أو أن تقصها من الملف الكبير الذي ترغب بالوصول إليه وتحفظها في ملفات منفصلة. تنويه: يمكنك استخدام أحد برامج التصميم الجرافيكي جيمب أو كريتا أو إنكسكيب على سبيل المثال لا الحصر لتُصمم مكونات اللعبة. تظهر المنصات في بداية كل مستوى لذا عليك إضافة دالة المنصة platform إلى صنف المستوى Level، أما المنصة التي تشكل الأرضية سنفرد لها دالة خاصة فهي تتمتع ببعض الخصوصية إذ إنها تبقى ثابتة في بعض الألعاب بينما تطفو فوقها المنصات الأخرى، أو تتحرك في بعضها الآخر أو تُمرر في أثناء سير اللعبة. أضف إذًا الدوال التالية إلى صنف المستوى Level: def ground(lvl,x,y,w,h): ground_list = pygame.sprite.Group() if lvl == 1: ground = Platform(x,y,w,h,'block-ground.png') ground_list.add(ground) if lvl == 2: print("Level " + str(lvl) ) return ground_list def platform( lvl ): plat_list = pygame.sprite.Group() if lvl == 1: plat = Platform(200, worldy-97-128, 285,67,'block-big.png') plat_list.add(plat) plat = Platform(500, worldy-97-320, 197,54,'block-small.png') plat_list.add(plat) if lvl == 2: print("Level " + str(lvl) ) return plat_list الدالة الأولى في التعليمات السابقة هي دالة الأرضية ground، وتتطلب تحديد الإحداثيات X و Y لمعرفة موقع ظهور الأرضية على الشاشة وكذلك تحديد الأبعاد حتى يتبين للوحدة Pygame كيف ستمتد أرضيتك في كلا الاتجاهين، تستخدم هذه الدالة صنف المنصة Platform لإنشاء كائن الأرضية على الشاشة وإضافته إلى مجموعة الأرضيات ground_list. أما الدالة الثانية دالة المنصة platform فتماثل الأولى تمامًا باستثناء أنها تتضمن أكثر من عنصر في مجموعة المنصات، في حالتنا يوجد منصتين فقط لكن يمكنك إضافة القدر الذي تريده بشرط ضمها جميعًا لمجموعة المنصات plat_list وإلاّ فإنها لن تظهر على الشاشة. تعلمنا على طول مقالات السلسلة أن الدوال والأصناف لا تعمل ما لم تستدعيها في البرنامج وهذا ما ستفعله بكتابة هذه التعليمات ضمن مقطع الإعدادات في شيفرة لعبتك: ground_list = Level.ground(1, 0, worldy-97, 1080, 97) plat_list = Level.platform(1) واكتب أيضًا الأسطر التالية في الحلقة الرئيسية، مع العلم أن السطر الأول لمعرفة السياق فقط وهو موجود لديك مسبقًا: enemy_list.draw(world) # تحديث الأعداء ground_list.draw(world) # تحديث الأرضية plat_list.draw(world) # تحديث المنصات منصات البلاط يعد استخدام قطع البلاط لبناء عالم الألعاب أسلوبًا أسهل من استخدام الصور الكاملة كما عرضنا في الفقرة السابقة، فكل ما يترتب عليك هنا هو تجهيز أو رسم بعض الكتل مقدمًا ومن ثم استخدامها مرارًا وتكرارًا في كل منصات اللعبة. توفر مواقع مثل kenney.nl أو OpenGameArt.org مجموعاتٍ واسعة من قطع البلاط التي تتمتع برخص المشاع الإبداعي، يمكنك تحميلها واستخدامها، اعتمد مثالنا قطعًا من kenney.nl تبلغ مساحة كل قطعة 64 بكسل مربع، وفي حال اعتمدت قطعًا بأبعادٍ أخرى راعِ ذلك ضمن الشيفرة. لن يتغير صنف المنصة platform عمّا كتبنا في السابق، لكن الدوال platform و ground في صنف المستوى Level ستتضمن حلقاتٍ خاصة بحساب عدد الكتل اللازمة لبناء كل منصة. لنفترض أنك ترغب بإنشاء أرضية ثابتة عبر تكرار استخدام قطع البلاط على طول نافذة اللعبة، يمكنك إنجاز ذلك بعدة طرق إحداها إعداد قائمة يدوية بالإحداثيات X و Y لمواقع قطع البلاط على طول المسار المطلوب، وعندها سترسم حلقة التكرار قطعة بلاط في كل موقع، انظر السطر التالي لكن لا تكتبه في فهو مجرد مثال، إذ سنعتمد طريقة أخرى للتعامل مع الموضوع. # لا تكتب هذا السطر في شيفرة اللعبة فهو مجرد مثال gloc = [0,656,64,656,128,656,192,656,256,656,320,656,384,656] دقّق قليلًا في المصفوفة السابقة وستجد أن جميع القيم Y متماثلة وهي 656 دائمًا، بينما تزداد قيم X ازديادًا مطردًا بمقدار 64 في كل نقلة الذي يعادل عرض البلاطة الواحدة. أتمتة هذا النوع من التكرار المنتظم سهلة نوعًا ما فهي تتطلب بعض المنطق الرياضي والعمليات الحسابية لتتم آليًا. اكتب لذلك التعليمات التالية ضمن مقطع الإعدادات في برنامجك: gloc = [] tx = 64 ty = 64 i=0 while i <= (worldx/tx)+tx: gloc.append(i*tx) i=i+1 ground_list = Level.ground( 1,gloc,tx,ty ) يُجري بايثون من خلال التعليمات السابقة عملية قسمة يُقسم فيها عرض عالم اللعبة على عرض المربع الواحد أو قطعة البلاط الواحدة، وينتج بذلك مصفوفة تتضمن كافة قيم X اللازمة أما قيم Y فهي ثابتة لا تتغير طالما أن أرضية لعبتك مستوية. استخدم حلقة while لتستثمر المصفوفة السابقة في دالة بناء الأرضية فهي ستضيف بلاطة إلى الأرضية في الموقع المقابل لكل قيمة من قيم المصفوفة. وستصبح الدالة على الشكل التالي، اكتبها ضمن صنف المستوى Level: def ground(lvl,gloc,tx,ty): ground_list = pygame.sprite.Group() i=0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i],worldy-ty,tx,ty,'tile-ground.png') ground_list.add(ground) i=i+1 if lvl == 2: print("Level " + str(lvl) ) return ground_list تتشابه هذه الدالة في الغرض مع دالة الأرضية ground التي تعاملنا معها في الفقرة السابقة إلّا أنها تعتمد هنا على الحلقة while. المبادئ نفسها تقريبًا لدالة المنصات المتحركة، وسنعطيك بعض النصائح الكفيلة بتبسيط الموضوع، فبدلًا من تحديد مساحة كل منصة بالبكسل، يمكنك الاستعاضة عن ذلك بتحديد نقطة بداية المنصة (أو الإحداثي X) ومدى ارتفاعها عن الأرضية (أي الإحداثي Y) وأيضًا عدد قطع البلاط التي تحتويها، وبذلك لن تحتاج لتحديد أبعاد كل منصة. برمجة هذه العملية معقدة بعض الشيء فهي تتضمن حلقتي while الواحدة ضمن الأخرى، إذ إن إنشاء كل منصة يستلزم تحديد ثلاث محددات مقابل كل قيمة من قيم المصفوفة، في حالتنا مثلًا لا يوجد سوى ثلاث منصات تعرفها التعليمات ploc.append لكن بوسعك إضافة القدر الذي تريده من المنصات، وستلاحظ أنها لن تظهر دفعة واحدة عند تنفيذ الشيفرة بل ستظهر المنصات الخاصة بالشاشة الحالية فقط ومع تمرير الشاشة أثناء اللعب ستظهر البقية تباعًا. def platform(lvl,tx,ty): plat_list = pygame.sprite.Group() ploc = [] i=0 if lvl == 1: ploc.append((200,worldy-ty-128,3)) ploc.append((300,worldy-ty-256,3)) ploc.append((500,worldy-ty-128,4)) while i < len(ploc): j=0 while j <= ploc[i][2]: plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'tile.png') plat_list.add(plat) j=j+1 print('run' + str(i) + str(ploc[i])) i=i+1 if lvl == 2: print("Level " + str(lvl) ) return plat_list إذًا فقد أنشأت الدالة المسؤولة عن بناء المنصات، وعليك الآن استدعائها داخل البرنامج وفق الأمر التالي: plat_list = Level.platform(1, tx, ty) وأخيرًا اكتب التالي في الحلقة الرئيسية حتى تظهر المنصات في عالم لعبتك (السطر الأول للحفاظ على السياق فقط فلا داعي لكتابته): enemy_list.draw(world) # تحديث الأعداء ground_list.draw(world) # تحديث الأرضية plat_list.draw(world) # تحديث المنصات شغل اللعبة الآن، ولاحظ توضع المنصات وشكلها، وعدّل ما تريد، علمًا أنك لن تجد المنصات التي لا تنتمي للشاشة الأولى، لا تقلق سنتجاوز ذلك في المقالات اللاحقة عبر تمرير عالم اللعبة. طبق المهارات التي تعلمتها في السابق لم يذكر هذا المقال أين يتوضع العدو أو كيف، لذا طبق المهارات التي تعلمتها سابقًا وأظهره على الأرضية أو فوق إحدى المنصات فهي الأماكن الأنسب له، أما البطل دعه حاليًا فهو محكوم بقوى خاصة بعالم الرسوم المتحركة تحاكي قوى الجاذبية في العالم الحقيقي ستتعلمها في مقالاتنا التالية. إليك الشكل النهائي للشيفرة حتى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' # x location, y location, img width, img height, img file class Platform(pygame.sprite.Sprite): def __init__(self, xloc, yloc, imgw, imgh, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)).convert() self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.frame += 1 if self.frame > 3 * ani: self.frame = 0 self.image = self.images[self.frame // ani] hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance * 2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level: def ground(lvl, gloc, tx, ty): ground_list = pygame.sprite.Group() i = 0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i], worldy - ty, tx, ty, 'tile-ground.png') ground_list.add(ground) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return ground_list def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl)) return enemy_list # x location, y location, img width, img height, img file def platform(lvl, tx, ty): plat_list = pygame.sprite.Group() ploc = [] i = 0 if lvl == 1: ploc.append((200, worldy - ty - 128, 3)) ploc.append((300, worldy - ty - 256, 3)) ploc.append((500, worldy - ty - 128, 4)) while i < len(ploc): j = 0 while j <= ploc[i][2]: plat = Platform((ploc[i][0] + (j * tx)), ploc[i][1], tx, ty, 'tile.png') plat_list.add(plat) j = j + 1 print('run' + str(i) + str(ploc[i])) i = i + 1 if lvl == 2: print("Level " + str(lvl)) return plat_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc) gloc = [] tx = 64 ty = 64 i = 0 while i <= (worldx / tx) + tx: gloc.append(i * tx) i = i + 1 ground_list = Level.ground(1, gloc, tx, ty) plat_list = Level.platform(1, tx, ty) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.update() player_list.draw(world) enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال Put platforms in a Python game with Pygame لصاحبيه Seth Kenlon و Jess Weichler. اقرأ أيضًا المقال السابق: إضافة شخصية العدو للعبة عبر مكتبة Pygame في بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون دليلك الشامل إلى: أشهر لغات برمجة الألعاب
  17. لطالما كانت الأسباب التي تدفع الناس للمساهمة في المشاريع الحرة ومفتوحة المصدر FOSS مثارًا للاهتمام، ومع ذلك فإن أحدث البحوث حولها يعود تاريخه لعشر سنوات سابقة أو ربما أكثر، وقد تغير العالم كثيرًا منذ ذلك الحين. لذا سنعرض في هذا المقال سبع أفكار من دراسة بحثية حديثة أثارت الموضوع مجددًا، وأعادت دراسة الأسباب القديمة، وسألت المساهمين الحاليين عن ما يحفزهم اليوم لتطوير مشاريع حرة مفتوحة المصدر، وخلُّصت في النتيجة لمجموعة أفكار من شأنها مساعدة مديري المجتمع مفتوح المصدر على تطوير مجتمعهم من جهة، وتفسير الأمر للمهتمين به من أفراد عاملين فيه أو منظمات داعمة أو دارسة له من جهةٍ ثانية، ففي النهاية معرفة الأسباب مهمة للتطوير، فمعرفة محفزات المساهمين تفيد الجميع في اتخاذ قرارات مؤثرة وداعمة لهم ولمشاريعهم. لمحة تاريخية عن البحوث التي تناولت محفزات المشاريع مفتوحة المصدر لننظر في أصول المصدر المفتوح حتى نعرف ما الذي جعل دراسة محفزات المساهمين فيه أمرًا مهمًا ومثيرًا للاهتمام. مُذ بدأت حركة البرمجيات مفتوحة المصدر عرفت بأنها تمردٌ، إذ تمردت على المألوف وتحدّت أسلوب الشركات التي تبيع البرمجيات وتعتمد حقوق النشر والرخص لتقييد حرية المستخدم والمطوّر، ولم يكن سهلًا على أحد فهم الأسباب التي أدت إلى إنتاج برمجيات بهذه الجودة العالية على أيدي مطوّرين بدأوا بتطويرها أساسًا لاستخدامهم الشخصي أو تطوّعوا بمهاراتهم في سبيل تحقيقها، فقد وجدت روح تعاونية مذهلة في صلب هذه الحركة أثارت اهتمام الجميع بما فيهم الشركات أيضًا، وبالنتيجة أصبحت البرمجيات مفتوحة المصدر بمثابة تحول فلسفي في مسار التطوير جعل طريقة التعاون هذه متاحة ومقبولة لدى أصحاب الأعمال. نشرت دراسة بحثية من أشمل الدراسات في العام 2012، تلخص المحفزات التي تدفع المساهمين في هذا المجال لكن المحفزات لابد تغيرت على مدى السنوات العشر الماضية، بالأخص مع تزايد اهتمام الشركات بالمشاريع مفتوحة المصدر ووجود موظفين بأجرٍ مدفوع يعملون حاليًا في هذا المجال، وأصبح من الضروري إعادة دراستها وتقييمها. تغير محفزات المساهمين تناولت دراستنا العلمية بعنوان الأسباب تتغير- إعادة نظر في دوافع المساهمين في المشاريع مفتوحة المصدر المحفزات التي تدفع الناس للمساهمة في المشاريع الحرة مفتوحة المصدر FOSS وما يدفعهم للاستمرار فيها بعد ذلك. هدفت هذه الدراسة في المقام الأول إلى البحث في التغيرات التي طرأت على دوافع المساهمين منذ العقد الأول من القرن الحادي والعشرين، ومن ثم إلى نقل البحث لسويةٍ أخرى تتناول تغير هذه الدوافع مع استمرار المساهمين في مساهمتهم. يستند البحث إلى استبيان أُجري في أواخر العام 2020 وأجاب عنه حوالي 300 مساهم في البرمجيات الحرة ومفتوحة المصدر. أهم سبعة محفزات للمساهمة في المشاريع مفتوحة المصدر نلخص أهم نتائج الدراسة فيما يلي. 1. تلعب الدوافع الجوهرية دورًا رئيسيًا بينت الدراسة أن أغلب الناس يساهمون في المشاريع الحرة ومفتوحة المصدر لأسبابٍ تتعلق بالمتعة بنسبة 91٪ والإيثار بنسبة 85٪ والقرابة بنسبة 80٪، أما عند تحليلنا للاختلافات بين دوافع الانضمام والاستمرار في المساهمة، وجدنا أن كلًا من الأيديولوجيا أو الاستخدام الشخصي أو البرامج التعليمية يمكن أن يشكلوا حافزًا لانضمامهم، لكنهم يستمرون لأسباب جوهرية أخرى هي المتعة والإيثار والسمعة وصلة القُربى. 2. السمعة والدافع المهني أقوى من المقابل المادي يسعى المساهمون للسمعة الطيبة بنسبة 68٪ وتدفعهم الرغبة في التطور المهني بنسبة 67٪، بينما أشار أقل من 30٪ منهم بأن دافعهم للانضمام كان ماديًا سعيًا لأجرٍ مدفوع، بموازنة هذه النتائج بالدراسات السابقة نجد أن السمعة اليوم تعد دافعًا أكثر أهمية مما كانت عليه في ما مضى. 3. ازدياد أهمية الجوانب الاجتماعية فقد ارتفعت نسب الرضا والاستمتاع بمساعدة الآخرين إلى 89٪ في التصنيف والقُربى إلى 80٪ موازنةً بالدراسات الاستقصائية التي أجريت في بدايات العقد الأول من القرن الحادي والعشرين. 4. تتغير محفزات الناس كلما زاد عهدهم بالمساهمة تباين محفزات الانضمام والاستمرار من أوضح نتائج الدراسة، فقد أعطى 155 مشارك من أصل 281 أي ما نسبته 55٪ أسبابًا متباينة بين ما انضموا أساسًا لأجله وما يدفعهم اليوم للاستمرار. لاحظ الشكل أدناه فهو يوضح التحولات في محفزات المشاركين بين ما قادهم للانضمام بدايةً وما يدفعهم للاستمرار اليوم، فحجم المربعات الموجودة على اليسار يمثل عدد المساهمين الذين قادهم هذا الحافز للانضمام وبدء المساهمة في البرمجيات الحرة والمفتوحة المصدر، أما تلك الموجودة على اليمين فتشير إلى دافع الاستمرار، وخطوط الوصل بين الطرفين تمثل التحولات من دافع لآخر، أما ثخن كل خط يتناسب مع عدد المساهمين الذين تحولوا من دافعٍ إلى آخر بين المرحلتين. 5. الاستخدام الشخصي مجرد مدخل انخفضت أهمية الحافز الناجم عن تطوير البرامج لغرض الاستخدام الشخصي منذ الأيام الأولى، وتحول أغلب من انضموا لهذا السبب، إلى محفزاتٍ أخرى مثل الإيثار والتعلم والمتعة وتبادل المنفعة والخبرة، راجع الشكل أعلاه ولاحظ ذلك. 6. تختلف المحفزات تبعًا للخبرة والعمر اتجهت حوافز المطورين ذوي الخبرة أكثر باتجاه الإيثار بمعدل 5.6 ضعفًا عن المبتدئين، والأجور بمعدل 5.2 ضعفًا، وكذلك الأيديولوجية 4.6 ضعفًا، بينما أعطى المبتدئون معدلاتٍ أعلى للأسباب المتعلقة بحياتهم المهنية بمعدل 10 أضعاف والتعلم بحوالي 5.5 ضعفًا والمتعة 2.5 ضعفًا. وبالنظر إلى التحولات في محفزات المشاركين وجدنا زيادةً كبيرة في التحول إلى الإيثار حوالي 120% لدى ذوي الخبرة، بينما انخفضت انخفاضًا طفيفًا حوالي 16% لدى المبتدئين. أما فئة المشاركين الشباب فقد انضم عددٌ قليلٌ منهم إلى البرمجيات الحرة والمفتوحة المصدر بدافع مهني، لكن الكثيرين تحولوا بعدها إلى الإيثار بزيادة بلغت 100٪. 7. المحفزات تختلف بين المبرمجين وسواهم أعطى المبرمجون أسبابًا تتعلق بالمتعة أكثر من غيرهم بنحو 4 أضعاف، أما غير المبرمجين فقد أجابوا بمحفزاتٍ تتعلق بالأيديولوجية بمعدلات تعادل 2.5 ضعفًا. تحفيز المساهمين بناء على طول عهدهم في المجال تساعد معرفة اختلاف المحفزات بين المساهمين الجدد وذوي الخبرة في اتخاذ القرارات والخطوات المناسبة لتحفيزهم والحفاظ عليهم ودعمهم بطرقٍ أفضل. فعلى سبيل المثال جذب مساهمين جدد والحفاظ عليهم ليصبحوا قوة عاملة في المستقبل يتطلب من القائمين على المجتمع الاستثمار في المشاريع التي تركز على مسيرتهم المهنية وجوانب المتعة والإيثار والقرابة والتعلم وهو الأكثر أهمية لفئة الشباب. أما بمرور الوقت وازدياد خبرة المساهمين سيميل معظمهم لحافز الإيثار، وعندها يتعين على المشاريع مفتوحة المصدر -التي تهدف إلى الاحتفاظ بالمساهمين ذوي الخبرة حتى يكونوا أعضاء أساسيين أو مشرفين لديها- أن تستثمر في الاستراتيجيات والأدوات التي توضح لهم كيف سيستفيد المجتمع من عملهم، وتدعم الميزات الاجتماعية على منصات الاستضافة لتجمع بين من يحتاج إلى المساعدة ومن يرغب بتقديمها، ومن ثم تهتم بتسليط الضوء على هذه المساعدات والأعمال الإيجابية وعلى مقدميها، وإظهار التقدير اللازم الذي يستحقونه على غرار النجوم الممنوحة للمشاريع مثلًا. كانت هذه بعض الأفكار المستخلصة من نتائج الدراسة التي نأمل أن تكون ملهمة لكل المهتمين والمؤثرين في هذا المجال ليستفيدوا منها ويبنوا عليها المزيد لتحفيز المساهمين الجدد والمتمرسين لما فيه دعم عجلة تطور البرمجيات مفتوحة المصدر ودفعها قدمًا إلى الأمام. أما في الختام نذكركم برابط الورقة البحثية التي اعتمدنا عليها والتقرير الكامل عنها مع الحفاظ على الحقوق الفكرية لكافة المساهمين فيها. ترجمة -وبتصرف- للمقال ?What motivates open source software contributors لأصحابه Igor Steinmacher و Georg Link و Anita Sarma و Gregorio Robles و Bianca Trinkenreich و Christoph Treude و Marco Gerosa و Igor Wiese. اقرأ أيضًا ما المقصود بمصطلح مفتوح المصدر (open source)؟ تراخيص البرامج مفتوحة المصدر ما هي البرمجيات مفتوحة المصدر؟ صيانة المشاريع مفتوحة المصدر الفرق بين البرمجيات الحرة ومفتوحة المصدر
  18. وصلنا للمقال الخامس من سلسلة بناء لعبة من الصفر باستخدام بايثون، وقد تعلمنا في أجزائها الأربعة السابقة كيفية بناء لعبة فيديو باستخدام الوحدة Pygame وطورنا اللعبة بوجود بطل متحرك، والآن سنضيف إليها بعض الإثارة بإضافة شخصية شريرة للمشهد أو عدو ليواجه البطل فهذا تقليدي في عالم الألعاب، ومن ثم سنبني إطار عمل خاص بإضافة مستويات اللعبة. يمكنك مطالعة المقال ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. بناء شخصية العدو مشابه تمامًا لبناء شخصية البطل سنستخدم الوظائف نفسها، وفي حال لم تُحضّر له صورة حتى الآن يمكنك اختيارها من مجموعة الصور المجانية المتاحة على منصة OpenGameArt.org أو نزلها من الملف المرفق opp2_sprites، وننصحك بتعلم المزيد عن رخصة المشاع الإبداعي حتى لا تنتهك حقوق النشر في الصور التي تستخدمها. إنشاء شخصية العدو العملية مشابهة جدًا لعملية إنشاء كائن البطل، فلديك المفاتيح الرئيسة للعمل. أنشئ صنفًا خاصًا بشخصية العدو لإنتاج كائن الشخصية. أنشئ دالة update لتحديث كائن العدو في الحلقة الرئيسية. أنشئ دالة move لتمنح شخصية العدو حركة عشوائية. ابدأ بالصنف بالطريقة نفسها لصنف البطل، ومن ثم حدد الصورة أو مجموعة الصور التي تعتمدها لشخصية العدو واحفظها في المجلد images الخاص بالصور ضمن مجلد مشروعك (المجلد نفسه الذي يتضمن صور البطل)، وأعطها اسمًا دلاليًا مثل enemy.png المستخدم في حالتنا. بالتأكيد ستبدو اللعبة أفضل لو جعلت كافة العناصر تتحرك، لكننا سنترك شخصية العدو دون حركة في هذه المرحلة حتى نخفف عليك التعقيد، ولو رغبت بتحريكه استذكر خطواتنا في المقال السابق. اكتب التعليمات التالية لإنشاء صنف العدو في أعلى المقطع الخاص بالكائنات objects ضمن برنامجك: class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self,x,y,img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images',img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y يمكنك استخدام الصنف نفسه لتوليد عدة أعداء، فكل ما عليك فعله هو استدعاء الصنف وإعطائه صورة لكل شخصية وإحداثيات ظهورها على الشاشة X و Y. تمامًا مثل صنف اللاعب، اكتب صنف العدو في مقطع الإعدادات setup ضمن البرنامج: enemy = Enemy(300,0,'enemy.png') # إنتاج العدو enemy_list = pygame.sprite.Group() # إنشاء مجموعة الأعداء enemy_list.add(enemy) # إضافة عدو للمجموعة أنتجت التعليمات السابقة كائنًا يسمى enemy إحداثيات موقعه هي 300 بيكسل على المحور X و 0 بيكسل على المحور Y، ما يعني أن زاويته العلوية اليسرى عند النقطة 0 وتمتد صورته للأسفل تبعًا لمساحتها، لذا تنبه لأحجام صور الأعداء والأبطال واضبط إحداثياتها لتكون مواضعها مناسبة على الشاشة، وولد الأعداء في مكانٍ يمكنك إيصال بطل اللعبة إليه ففي مثالنا وضعنا العدو في النهاية عند النقطة 0 بيكسل والبطل عند النقطة 30 بيكسل على المحور العمودي Y ليظهرا بنفس المستوى (وهذا بسبب افتقار اللعبة إلى الحركة العمودية)، علمًا أن الصفر على المحور Y يقع في الأعلى ما يعني أن الأرقام تزداد كلما اتجهت نحو أسفل الشاشة. ولاحظ أن صورة البطل مدمجة مع كود الصنف أي ثابتة hard coded في مثالنا، ذلك أن البطل وحيد في اللعبة، أما صور الأعداء متغيرة إذ قد ترغب باستخدام أكثر من نموذج لها، لذا حدد الصورة خلال عملية إنشاء الشخصية، والصورة المستخدمة في مثالنا هي enemy.png. رسم شخصية على الشاشة حاول تشغيل لعبتك الآن، ستعمل، لكن شخصية العدو لن تظهر على الشاشة، اعترضتنا هذه المشكلة قبلًا وبالتأكيد عرفت السبب فتعليمات إنشاء الشخصية لم تكتب ضمن الحلقة الرئيسية، تذكّر دومًا أن أي شيء تريد حدوثه باستمرار لابد من ذكره في حلقة التكرار، أما بدون ذلك فسيظهر العدو على الشاشة لمدة بسيطة جدًا لأجزاء من الثانية ويختفي دون أن تلحظه حتى. أضف السطر الأوسط من التعليمات التالية إلى الحلقة حتى تُحدّث ظهور مجموعة الأعداء enemy_list التي تتكون حاليًا من عدو واحد لكن يمكنك توسيعها بإضافة أعداء آخرين وسيظهرون جميعًا مع كل تكرار للحلقة. player_list.draw(world) enemy_list.draw(world) # تحديث مجموعة الأعداء pygame.display.flip() شغل اللعبة بعد التعديلات، وستجد شخصية العدو على الشاشة في الموقع الذي تحدده الإحداثيات X و Y. إضافة مراحل للعبة يخطط مطور الألعاب لمستويات أو مراحل اللعبة حتى وهي في مهدها، ورغم بساطة لعبتنا وعدم وجود أي مستوى فيها حتى الآن، إلّا أننا سنكتب التعليمات كما لو كنا نخطط لبناء عدة مستويات. فكر في ماهية المستوى الذي تعّده؟ عادةً عندما تلعب كيف تعرف أنك في مستوى معين أو أنك انتقلت لمستوى جديد؟ يمكننا القول أن المستوى هو مجموعة من العناصر المميزة، وفي ألعاب المنصات مثل لعبتنا يتكون المستوى من مجموعة منصات مرتبة ترتيبًا معينًا، بالإضافة إلى نوع خاص بالمستوى من الأعداء والغنائم وما شابه، أما عن طريقة بناء المستوى سننشئ له صنفًا خاصًا يبني المستوى حول اللاعب، ونعيد استخدامه بعد الانتهاء لبناء المستويات اللاحقة. انقل الآن التعليمات التي كتبتها سابقًا لبناء شخصية العدو ومجموعة الأعداء، إلى دالة جديدة واستدعيها مع كل بناء لمستوى جديد، ستحتاج بالطبع لبعض التعديلات لإنتاج أنواع مختلفة من الأعداء لكل مستوى. class Level(): def bad(lvl,eloc): if lvl == 1: enemy = Enemy(eloc[0],eloc[1],'enemy.png') # إنتاج العدو enemy_list = pygame.sprite.Group() # إنشاء مجموعة الأعداء enemy_list.add(enemy) # إضافة عدو لمجموعة الأعداء if lvl == 2: print("Level " + str(lvl) ) return enemy_list مهمة التعليمة return السابقة هي إرجاع قائمة تتضمن مجموعة الأعداء enemy_list التي استخدمتها مع كل تنفيذ للدالة Level.bad. نظرًا لأن إنشاء العدو أصبح جزءًا من إنشاء المستوى، سنجري تعديلًا على مقطع الإعدادات setup، فبدلًا من إنشاء العدو، سنحدد المستوى الذي ينتمي إليه وأين سيتم إنتاجه، وذلك وفق الأوامر التالية: eloc = [] eloc = [300,0] enemy_list = Level.bad( 1, eloc ) شغل اللعبة مجددًا وتأكد من صحة إنشاء المستوى ومن ظهور البطل والعدو. التصادم مع العدو لا تُعدّ شخصية العدو فاعلة ما لم تسبب ضررًا للاعب أو تؤثر عليه، والتصادم هو أشيع أنواع الضرر في الألعاب. أما بعد التصادم علينا فحص حالة البطل أو صحته وسنجري ذلك في صنف البطل، يمكنك بالطبع فحص صحة العدو بالطريقة نفسها لكننا سنكتفي بتتبع صحة اللاعب، وذلك من خلال تعريف متغير خاص يشير إلى صحة اللاعب وفق السطر الثاني من التعليمات التالية أما السطر الأول فذُكر للإشارة إلى السياق فقط. self.frame = 0 self.health = 10 وأيضًا أضف التعليمات التالية في الدالة update ضمن صنف اللاعب. hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) تكشف هذه التعليمات التصادم باستخدام sprite.spritecollide إحدى دوال Pygame ضمن المتغير hit_list لحظة التلامس بين صندوق البطل -الذي تفحص صحته- وصندوق أي شخصية تنتمي لمجموعة العدو enemy_list، ويرسل عندها إشارة إلى الحلقة for تؤدي إلى خصم نقطة من صحة اللاعب في كل مرة يحدث فيها التصادم. ولأن هذه التعليمات مكتوبة في الدالة update لصنف اللاعب التي تُستدعى مع كل تكرار للحلقة الرئيسية، فإن Pygame ستتحق من التصادم مع كل نبضة ساعة. تحريك العدو يعدّ نمط العدو الثابت الذي بنيناه هنا مقبولًا في الألعاب مثل المسامير والمصائد التي تعرقل سير اللاعب، إلاّ أن العدو المتحرك يضيف صعوبةً وإثارة للعبة. سنبدأ بتحريك العدو مع مراعاة أمرين، الأول أنه حركته آلية تُدار من قبل البرنامج ولا يتحكم بها المستخدم مثل حركة البطل، والثاني أن بيئة اللعبة أو عالمها متحرك، فكيف سنجعل العدو يتحرك داخله ذهابًا وإيابًا وهو أساسًا يتحرك؟ لنفترض أنك ترغب ببرمجة حركة العدو ليتحرك 10 خطوات نحو اليمين ومن ثم 10 نحو اليسار، كائن العدو لا يملك في مواصفاته البرمجية ما يمكنه من عدّ الخطوات، لذا ستحتاج إلى تعريف متغير يتتبع خطواته ويعدها، ومن ثم تبرمج عدوك ليتحرك يمينًا أو يسارًا بناءً على عدد الخطوات التي أنجزها. لنبدأ إذًا بتعريف متغير العداد المسؤول عن عدّ الخطوات، عبر إضافة السطر الأخير من التعليمات التالية إلى صنف العدو: self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 # متغير العداد ثم ننشئ دالة الحركة move في صنف العدو أيضًا، وذلك عبر الحلقة if-else بالشروط الآتية، التي ستجعلها حلقة لا نهائية. تحرك يمينًا إذا كانت قيمة العداد بين 0 و 100. تحرك يسارًا إذا كانت قيمة العداد بين 100 و 200. صفّر العداد وأرجعه للقيمة 0 عندما تتجاوز قيمته العدد 200. ستحرك هذه الحلقة كائن العدو يمينًا ويسارًا إلى ما لا نهاية فلا يوجود قيمة للمتغير تجعلها تتوقف فهو دائمًا بين 0 و 100 أو بين 100 و 200. أما عن كيفية اختيار القيم التي ستشكل أقصى مسافة يمكن للكائن الوصول إليها فالموضوع نسبي، يتغير تبعًا لقياس شاشة العرض لديك وقياس منصة اللعبة، لذا ننصحك بالتجريب، ابدأ بأرقام صغيرة ولاحظ النتائج ثم زدها لتصل لغايتك، والأمر نفسه بالنسبة لسرعة الحركة، مثلًا جرب التالي في البداية ويمكنك تعديله حسب رضاك عن النتائج: def move(self): ''' enemy movement ''' distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance*2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 بعد كتابة هذه التعليمات ستعرض عليك بيئة التطوير Pycharm تحذيرًا يتضمن اقتراحًا لتبسيط موازنة السلاسل بهدف تحسين أسلوب كتابة التعليمات، يمكنك قبول الاقتراح وتعلم المزيد عن الكتابة المتقدمة في بايثون، أو تجاهل التحذير بأمان والاستمرار. السؤال الآن، هل سيتحرك العدو عند تشغيل اللعبة؟ أم أننا تحتاج لخطوة إضافية؟ الإجابة هي لا بالطبع، لن يتحرك ما لم تستدعِ الدالة move ضمن حلقة البرنامج الرئيسية، أضف السطرين الأخيرين من التعليمات التالية لتُنجز ذلك: enemy_list.draw(world) #حدّث العدو for e in enemy_list: e.move() شغل اللعبة وشاهد ما يحدث عند اصطدام بطل اللعبة بالعدو، ربما ستحتاج لبعض التعديلات مثل تغيير مكان إنتاج العدو لتصنع التصادم، وعندما يحدث ذلك، تفقد النقاط التي خُصمت من رصيد صحة اللاعب من خلال بيئة التطوير IDLE أو Pycharm. قد تلاحظ أن النقاط تُخصم من صحة اللاعب (أو من أدوار اللاعب) في كل لحظة من الاصطدام، ستتعلم معالجة ذلك مع تطور مهاراتك في بايثون، أما حاليًا جرّب أن تنتج المزيد من الأعداء ووأيًا كانوا لا تنسَ إضافتهم إلى مجموعة الأعداء enemy_list، ومن ثم حاول تغير طريقة حركتهم استخدم حركة مختلفة لكل عدو. الشكل النهائي لبرنامج اللعبة إليك الشكل النهائي لبرنامج اللعبة متضمنًا كامل التعليمات البرمجية التي استخدمناها خلال السلسلة، ليبقى مرجعًا لك: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = self.images[self.frame//ani] hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in hit_list: self.health -= 1 print(self.health) class Enemy(pygame.sprite.Sprite): """ Spawn an enemy """ def __init__(self, x, y, img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images', img)) self.image.convert_alpha() self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): """ enemy movement """ distance = 80 speed = 8 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance*2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 class Level(): def bad(lvl, eloc): if lvl == 1: enemy = Enemy(eloc[0], eloc[1], 'enemy.png') enemy_list = pygame.sprite.Group() enemy_list.add(enemy) if lvl == 2: print("Level " + str(lvl) ) return enemy_list ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 30 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 eloc = [] eloc = [300, 0] enemy_list = Level.bad(1, eloc ) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.update() player_list.draw(world) enemy_list.draw(world) for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps) ترجمة -وبتصرف- للمقال What's a hero without a villain? How to add one to your Python game لصاحبه Seth Kenlon. اقرأ أيضًا المقال السابق: تحريك شخصية في لعبة باستخدام Pygame بناء لعبة نرد بسيطة بلغة بايثون برمجة لعبة حجرة ورقة مقص باستخدام لغة بايثون إضافة لاعب إلى لعبة مطورة باستخدام بايثون ومكتبة Pygame
  19. استخدمنا بايثون لبناء لعبة نرد بسيطة معتمدة على النص في مقالنا الأول من هذه السلسلة، وفي الثاني تعلمنا كيفية تجهيز بيئة لعبة رسومية من الصفر أيضًا بلغة بايثون، أما في المقال الثالث السابق أضفنا كائنًا أو بطلًا للعبة الفارغة، والآن سنتعلم استخدام Pygame لتحريك الكائن والتحكم به مباشرةً من خلال لوحة المفاتيح، بطريقة شبيهة نوعًا ما ببرمجة زر مغادرة اللعبة التي اتبعناها في المقال الثاني، إلّا أنها أكثر تعقيدًا. يمكنك مطالعة المقال ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. تدعم Pygame خياراتٍ أخرى للتحكم مثل الفأرة أو قبضة اللعب لن يغطيها هذا المقال، ولكن لا تقلق فمجرد فهمك لطريقة العمل مع لوحة المفاتيح سيفتح لك الباب لتعلُّم الخيارات الأخرى وتطبيقها بسهولة. إعداد مفاتيح التحكم بحركة الشخصية افتح السكربت الخاص باللعبة باستخدام بيئة التطوير المتكاملة IDLE أو PyCharm أو ما تفضل. والآن حاول أن تخمن، في أي مقطع من مقاطع السكربت ينبغي أن نكتب التعليمات البرمجية الخاصة بمفاتيح التحكم؟ وانتبه إلى أن اللعبة ستبقى في حالة ترقب دائمة لأوامر التحكم الواردة من لوحة المفاتيح، ما يعني أننا بصدد حدث سيتكرر دائمًا. لابد أنك أصبت، فهي بالفعل ستكتب في الحلقة الرئيسية وتذكر أن أي تعليمات لا توضع ضمن حلقة التكرار تُنفذ لمرة واحدة فقط، وقد لا تُنفذ أبدًا إن كانت ضمن وظيفة أو إجراء لم يُستدعى. إذًا، اكتب التعليمات التالية في الحلقة الرئيسية، ولاحظ أننا استخدمنا تعليمة print لاختبار صحة تنفيذها، إذ إن البرنامج الكلي لم يجهز بعد. while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit(); sys.exit() main = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT or event.key == ord('a'): print('left') if event.key == pygame.K_RIGHT or event.key == ord('d'): print('right') if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): print('left stop') if event.key == pygame.K_RIGHT or event.key == ord('d'): print('right stop') if event.key == ord('q'): pygame.quit() sys.exit() main = False شاع عند الكثير استخدام الأزرار W و A و S و D في لوحة المفاتيح لتحريك شخصيات الألعاب بينما يفضل بعضهم الآخر أسهم الاتجاهات، لذا احرص على تضمين الخيارين في برنامجك. تنويه: راعي دومًا احتياجات وتفضيلات أوسع فئة ممكنة من المستخدمين عند تطوير أي تطبيق، خصوصًا إن كانت البرمجة هي مهنتك التي تكسب منها، فإعطاء خيارات متعددة للمستخدم يعدّ واحدًا من أفضل المؤشرات التي تدل على مهارة المبرمج وتمتع تطبيقه بقابلية الوصول وهي ميزة مهمة في تطوير التطبيقات. شغل لعبتك الآن باستخدام بايثون، وتفقد الخرج الظاهر في نافذة الطرفية عند ضغط الأزرار A أو W أو D أو أسهم الاتجاهات. $ python ./your-name_game.py left left stop right right stop jump ستلاحظ أن Pygame تكتشف ضغطات الأزرار على المفاتيح بصورة صحيحة وتفهمها فالتعليمات صحيحة إذًا، لذا دعنا ننتقل إلى الجزء الأصعب وهو تحريك الكائن. تحريك شخصية اللعبة يتطلب تحريك شخصية إنشاء خاصية تمثل الحركة على كل محور، وتعريف متغير يحمل القيمة صفر عندما لا يتحرك الكائن، وأيضًا تتبع الأطر حتى تظل دورة المشي ضمن المسار الصحيح. عرّف المتغيرات التالية ضمن الصنف الخاص بشخصية اللاعب وذلك بإضافة الأسطر الثلاثة الأخيرة فقط أما السطر الأول والثاني فهما لديك أساسًا وأعدنا كتابتها هنا حتى يكون السياق واضحًا. def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 # X الحركة بالاتجاه الأفقي self.movey = 0 # Y الحركة بالاتجاه العمودي self.frame = 0 # عدد الأطر لننتقل إلى برمجة الحركة بعد أن عرفنا المتغيرات. لا يُفترض بشخصية اللعبة أن تستجيب للضغط على أزرار لوحة المفاتيح طوال الوقت ففي بعض الأحيان لا يُطلب منه التحرك، فتعليمات التحكم إذًا هي جزء فقط من الأشياء التي يمكنه فعلها، ولتعمل هذه التعليمات باستقلالية عن بقية الكود سنضعها ضمن دالة خاصة فهذا أسلوب بايثون، والدوال في لغة بايثون تبدأ بالكلمة المفتاحية def وهي الحروف الأولى من كلمة define التي تعني تعريف. عرف إذًا دالة في الصنف الخاص باللاعب تكون مسؤولة عن التحريك عبر إضافة عدد معين من وحدات البكسل -سيحدد في أجزاء لاحقة من البرنامج- إلى الموضع الحالي للكائن، فالتحريك في Pygame يعني تحديد موضع جديد للكائن ليعيد بايثون رسمه فيه. def control(self,x,y): """ control player movement """ self.movex += x self.movey += y سنخصص لعملية تحديث الموضع دالة جديدة في صنف اللاعب تسمى update تُكتب بعد دالة التحكم control السابقة، وذلك وفق التعليمات التي سترد لاحقًا. لتُظهر كائنًا ما على أنه يمشي (أو يطير أو أيًا كان ما عليه القيام به)، عليك أن تغير موضعه على الشاشة بما يتناسب مع الزر الذي ضغطه المستخدم، أما الموضع الجديد فيُحدد بكل من الخاصية self.rect.x و self.rect.y وأيضًا بعدد وحدات البكسل المطبق movex و movey (وهذا العدد سيحدد لاحقًا ضمن الكود). def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex وبالمثل للموضع العمودي Y: self.rect.y = self.rect.y + self.movey لتحقق أثر الرسوم المتحركة، عدّل الأطر كلما تحرك الكائن واستخدم في كل مرة الإطار المناسب للحركة ليكون بمثابة صورة للاعب: # الحركة يسارًا if self.movex < 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = self.images[self.frame//ani] # الحركة يمينًا if self.movex > 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = self.images[self.frame//ani] عرّف الآن متغيرًا يحدد عدد وحدات البكسل التي سيتحرك الكائن بمقدارها عند بدء تنفيذ الدوال الخاصة بذلك، واكتب هذا المتغير في مقطع الإعدادات ضمن سكربت اللعبة وفق الآتي (مع العلم أننا أعدنا كتابة أول سطرين توضيحًا للسياق فقط، أما تعريف المتغير فهو في السطر الأخير). … player_list = pygame.sprite.Group() player_list.add(player) steps = 10 # عدد وحدات البكسل التي سيتغير بمقدارها موضع الكائن … الآن بعد أن جهزنا دالة التحكم بالحركة والمتغير الذي يحدد مقدارها، يمكننا تبديل التعليمة print باسم اللاعب (وهو player) متبوعًا باسم الدالة control. مع تحديد عدد الخطوات التي سيتحركها الكائن مع كل تكرار للحلقة سواء على المحور X الأفقي أو العمودي Y. if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps,0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps,0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps,0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps,0) if event.key == ord('q'): pygame.quit() sys.exit() main = False يشير المتغير steps لعدد وحدات البكسل التي تتحرك بموجبها الشخصية أو اللاعب عند الضغط على أحد أزرار التحكم بالحركة، فلو كان steps هو 10 بكسيل هذا يعني أننا سنضيف 10 خطوات لموضع اللاعب عند الضغط على الزر D مثلًا وفور تحرير الزر أو إيقاف الضغط عليه سنطرح 10 خطوات steps- من موضعه لإعادة زخم الكائن إلى الصفر (ويعرف الزخم Momentum في الفيزياء بأنه كمية تصف مقدار القوة المطلوبة لإيقاف جسم ما). جرب لعبتك الآن، لكنك لن تحظى بما تتوقع فما زالت تحتاج لبعض التعديلات. تعديل صورة الشخصية عند تحريكها ستجد بعد التجريب أن الكائن لم يتحرك، وذلك عائد لعدم استدعاء الدالة update في الحلقة الرئيسية، وهو ما سنقوم به عبر إضافة السطر الأول من التعليمات التالية. player.update() # تحديث موضع اللاعب player_list.draw(world) pygame.display.flip() clock.tick(fps) شغل اللعبة مجددًا وشاهد حركة الكائن على الشاشة يمينًا ويسارًا واستجابته لضغط الأزرار، علمًا أننا لم نبرمج الحركة العمودية حتى الآن فلها قوانين ووظائف خاصة تحكمها مثل الجاذبية، ربما نستعرضها في مقالٍ آخر. يتبقى لدينا مشكلة بسيطة هي أن الكائن لا يلتفت وفقًا للاتجاه الذي يسير فيه، فلو كان وجهه مصممًا أساسًا إلى اليمين في الصورة التي اخترتها، سيبدو وكأنه يسير للخلف عندما يضغط المستخدم على السهم الأيسر ليحرّكه يسارًا، وسيعطي ذلك انطباعًا سيئًا عن اللعبة، فالمستخدم يتوقع استدارة البطل مع اتجاه الحركة. توجيه الشخصية وفقا لاتجاه حركتها تتيح لك Pygame وظيفةً جاهزة لقلب الصور تدعى transform يمكنك استخدامها بكل سهولة عبر كلمة مفتاحية واحدة دون أن تحتاج لكتابة تعليمات معقدة وتعلم مبادئ خاصة للرسم على الشاشة حتى تنجز الموضوع، وهنا تكمن قوة استخدام أطر العمل فكل ما عليك فعله هو استدعاء الدالة المناسبة وترك Pygame تكمل الباقي. إذًا غايتنا هي أن يدير الكائن وجهه وجسده بما يتوافق مع اتجاه الحركة، في حالتنا صورة الشخصية متجهة أساسًا لليمين هذا يعني أننا سنستخدم دالة قلب الصور ضمن كتلة التعليمات التي تحرك الشخصية لليسار حتى نوحي للمشاهد باستدارته يسارًا. بناءً على توثيقات Paygame تحتاج الدالة pygame.transform.flip إلى ثلاثة وسطاء هم: ماذا ستقلب؟ وهل ستقلبه عموديًا؟ وهل ستقلبه أفقيًا؟ في حالتنا لو أعدت النظر للرسم الذي استخدمناه، ستجد أنه يحتاج للقلب أفقيًا ويقابل ذلك القيمة true، ولا يتطلب القلب عموديًا ما يعني تمرير القيمة false مقابل هذا الوسيط. حدّث الآن تعليماتك حتى تتضمن التالي: # الحركة يسارًا if self.movex < 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) لاحظ أن المتغير self.image ما زال يأخذ قيمه من قائمة صور الكائن التي حددناها للبرنامج، ولكنها تُدور أو تقلب في دالة القلب. انتهت خطواتنا في هذا المقال، سنعرض لك الشكل النهائي للكود كاملًا، باستطاعتك تعلم المزيد وتطوير مهاراتك باستكشاف مزايا التحكم الأخرى من Pygame فقد تكونت لديك فكرة جيدة عن الموضوع، جرب الفأرة أو قبضات التحكم وطور اللعبة وفق ما تريد. اللعبة الكاملة نعرض لك الشكل النهائي للكود كاملًا ليكون مرجعًا تستند إليه في تجاربك. #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from typing import Tuple import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 ani = 4 world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() # optimise alpha img.set_colorkey(ALPHA) # set alpha self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def control(self, x, y): """ control player movement """ self.movex += x self.movey += y def update(self): """ Update sprite position """ self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = pygame.transform.flip(self.images[self.frame // ani], True, False) # moving right if self.movex > 0: self.frame += 1 if self.frame > 3*ani: self.frame = 0 self.image = self.images[self.frame//ani] ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 0 # go to y player_list = pygame.sprite.Group() player_list.add(player) steps = 10 ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(-steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(steps, 0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps, 0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps, 0) world.blit(backdrop, backdropbox) player.update() player_list.draw(world) pygame.display.flip() clock.tick(fps) تابع المقال التالي لتتعلم إضافة أعداء إلى اللعبة ومحاكاة أثر الجاذبية في الحركة العمودية، ولا تنسَ تطوير مهاراتك في بايثون وهو الغرض من بناء هذه اللعبة. ترجمة -وبتصرف- للمقال Using Pygame to move your game character around لصاحبه Seth Kenlon. اقرأ أيضًا المقال السابق: إضافة لاعب إلى لعبة مطورة باستخدام بايثون ومكتبة Pygame تعرف على أشهر لغات برمجة الألعاب ناء لعبة رسومية باستخدام بايثون ووحدة الألعاب Pygame بناء لعبة نرد بسيطة بلغة بايثون برمجة لعبة حجرة ورقة مقص باستخدام لغة بايثون
  20. يتميز تطوير التطبيقات باستخدام جانغو Django بالمرونة والسرعة، ويعدّ تجربةً جيدة للمطوّر، فتطبيق جانغو قابل للتطوير والتكيف مع المتغيرات بالإضافة إلى أنه يؤمن مجموعة متنوعة من إعدادات الأمان الأساسية التي تُسهل تحضير التطبيق لمرحلة الإنتاج ما بعد نشر التطبيق، إلّا أن إطلاقه للعلن وعمله الفعلي في بيئة الإنتاج يتطلب عوامل أمان أقوى، وله عدة طرق منها إعادة ترتيب ملفات إعدادات التطبيق وتجزئتها بناءً على تبعيتها لمتغيرات البيئة ما يسمح بإعادة ضبطها بسهولة، وأيضًا الاستفادة من dotenv لإخفاء الإعدادات السرية وضمان عدم تسريب أي تفاصيل قد تعرض مشروعك للخطر. للوهلة الأولى قد تبدو لك الحلول والاستراتيجيات التي نعرضها هنا مضيعةً للوقت، ولكن ما إن تنظمها في مخطط عمل واضح فإنها ستتحول إلى أداة مساعدة تضمن تطوير مشروعك بأمان ومستوى إنتاجية عالي دون أن تتنازل عن أحدهما في سبيل الآخر. ستتعلم في هذا المقال كيفية الاستفادة من مخطط العمل الموجه نحو التطوير الآمن لتطبيق جانغو من خلال ضبط ملفات الإعدادات المستندة إلى البيئة واستخدام dotenv وأيضًا إعدادات الأمان الأساسية المضمّنة في جانغو لتصل في النهاية إلى تطبيق جاهز للنشر بأمان بأي طريقة تختارها. متطلبات بيئة العمل ستحتاج المتطلبات التالية لتتمكن من التطبيق العملي للمقال: نسخة جاهزة من أي تطبيق جانغو، إن لم تتوفر لديك يمكنك إعدادها بإتباع خطوات المقال تثبيت إطار العمل Django وتهيئة بيئته البرمجية على Ubuntu، إذ استُعمل هنا التطبيق testsite المطوّر بموجبه. معرفة ببنية ملفات جانغو وإعداداته الأساسية، وننصحك بقراءة سلسلة بناء مدونة عبر جانغو. تنويه: تم تنظيم هذا المقال وعرضه بطريقة تلائم متطلبات التطبيق testsite فإن كنت تستخدم تطبيقًا آخر ستلاحظ بعض الاختلاف لكن لا بأس يمكنك تنفيذ الفقرات على حدة بما يناسب تطبيقك. الخطوة 1: إعادة هيكلة إعدادات جانغو سنُحلل الملف setting.py ونعيد ترتيبه ضمن مجموعة إعدادات منفصلة تستند إلى متغيرات البيئة، ما يعني أن أي نقل مستقبلي للتطبيق من بيئة إلى أخرى مثل نقله من بيئة التطوير إلى بيئة الإنتاج، سيتطلب تغيير بعض الإعدادات فقط بما يتلائم مع البيئة الجديدة. أنشئ مجلدًا جديدًا باسم settings ضمن المجلد الأب لمشروعك كمايلي: mkdir testsite/testsite/settings الاسم testsite هو اسم التطبيق كما ذكرنا في فقرة متطلبات بيئة العمل، أنشئ بعدها ثلاث ملفات بايثون ضمن المجلد settings كما يلي إذ ستحلّ هذه الملفات المنفصلة بدلًا من الملف setting.py: cd testsite/testsite/settings touch base.py development.py production.py تمامًا كما توحي الأسماء فالملف development.py سيتضمن الإعدادات التي ستحتاج إليها في أثناء التطوير، بينما سيحوي الملف production.py الإعدادات اللازمة لمرحلة الإنتاج وقد فصلنا إعدادات المرحلتين عن بعضهما بسبب اختلاف المتطلبات بينهما، فإعدادات مرحلة الإنتاج مثل توجيه الطلبات إلى HTTPS أو إضافة الترويسات أو استخدام قاعدة بيانات الإنتاج لن تنفع خلال مرحلة التطوير، أما الملف base.py فسيشمل الإعدادات المشتركة الأساسية التي سيرثها كلًا من development.py و production.py فهذ العملية ستساهم في عدم تكرار الإعدادات وتصميم كود نظيف لتطبيقك. طالما أن الملفات الثلاثة ستحمل كامل الإعدادات المطلوبة فلنزيل إذًا setting.py لضمان عمل جانغو بشكلٍ سليم. ضمن نفس المجلد settings نفذ الأمر التالي المتضمن تعديل اسم setting.py ليصبح base.py: mv ../settings.py base.py وضعنا بذلك الخطوط العريضة لعملية إعادة ترتيب الإعدادات تبعًا لمدى اعتمادها على متغيرات بيئة التشغيل، إلّا أنها غير مفهومة بعد بالنسبة للتطبيق وهو ما سيتم في الخطوة التالية. الخطوة 2: استخدام python-dotenv الحزمة python-dotenv هي إحدى اعتماديات التطبيق، مهمتها تحميل متغيرات البيئة من ملف خاص يدعى env.، وتثبيتها ضروري ليتمكن التطبيق من التعامل مع مجلد الإعدادات والملفات الجديدة التي عملنا عليها في الخطوة السابقة. لنبدأ التطبيق العملي: توجه إلى مسار الجذر لمشروعك: cd ../../ وثبت الاعتمادية python-dotenv: pip install python-dotenv ومن ثم اضبط إعدادات جانغو ليستخدم dotenv عبر تعديل الملفين manage.py الخاص ببيئة التطوير و wsgi.py الخاص ببيئة الانتاج كما يلي: افتح أولًا الملف manage.py باستعمال محرر النصوص: nano manage.py وأضف ضمنه التعليمات التالية: import os import sys import dotenv def main(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testsite.settings.development') if os.getenv('DJANGO_SETTINGS_MODULE'): os.environ['DJANGO_SETTINGS_MODULE'] = os.getenv('DJANGO_SETTINGS_MODULE') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() dotenv.load_dotenv( os.path.join(os.path.dirname(__file__), '.env') ) ثم أغلق الملف بعد حفظ التغييرات، وافتح الثاني: nano testsite/wsgi.py وأضف ضمنه التعليمات الناقصة المتعلقة بـ dotenv ليصبح بهذا الشكل: import os import dotenv from django.core.wsgi import get_wsgi_application dotenv.load_dotenv( os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env') ) os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testsite.settings.development') if os.getenv('DJANGO_SETTINGS_MODULE'): os.environ['DJANGO_SETTINGS_MODULE'] = os.getenv('DJANGO_SETTINGS_MODULE') application = get_wsgi_application() لا تنسَ حفظ التغييرات قبل إغلاق الملف. بموجب التعديلات التي أجريتها على الملفين: عند كل تشغيل لهما manage.py في التطوير والملف wsgi.py في الإنتاج، سيبحث جانغو عن الملف env. فإن وجده سيستخدم ملف الإعدادات الذي يوصي به env. وفي حال لم يجده يعتمد إعدادات التطوير افتراضيًا. لننشئ الآن الملف env. ضمن مجلد المشروع كما يلي: nano .env ونضيف ضمنه السطر التالي: DJANGO_SETTINGS_MODULE="testsite.settings.development" تنويه: ننصحك بإضافة env. على قائمة العناصر الموجودة في الملف gitingore. لتُحافظ عليه وتحميه من التعديل فهو في نهاية المطاف يتضمن كلمات المرور وإعدادات API وغيرها من المعلومات الحساسة المتعلقة بالتطبيق، وعندها ستنشئ ملف env. خاص بكل بيئة تنشر تطبيقك فيها، ولذا نوصيك بإنشاء نموذج ملف جاهز باسم env.example. ضمن مشروعك يمكنك تعديله بسهولة ليشكل env. جديد كلما دعت الحاجة. بناءً على السطر الذي أضفناه للملف env.، سيستخدم جانغو افتراضيًا testsite.settings.development وبالمثل إن غيرت قيمة المحدد DJANGO_SETTINGS_MODULE إلى testsite.settings.production فإنه سيبدأ باستخدام إعدادات بيئة الإنتاج. الخطوة 3: إنشاء ملفات الإعدادات لكل من بيئتي التطوير والإنتاج افتح الملف base.py وأضف ضمنه إعدادات التهيئة الخاصة بكل بيئة والتي ستعدلها لاحقًا في ملفي development.py و production.py المنفصلين، وتأكد أنك تملك معلومات الاتصال بقاعدة بيانات الإنتاج إذ ستحتاجها في الملف production.py. تنويه: إن تحديد ماهية إعدادات التهيئة الواجب تعديلها يختلف من مشروعٍ لآخر، وفي مثالنا سنعمل على الإعدادات الأكثر شيوعًا والتي تعد موجبة في معظم التطبيقات مثل إعدادات الأمان ومحددات الربط مع قواعد البيانات فهي تختلف عمومًا بين التطوير والإنتاج، وبناءً على خصوصية تطبيقك قد تكتفي بإعدادتنا أو قد تضيف إليها ما تحتاجه. لنبدأ بنقل الإعدادات من base.py إلى ملف إعدادات التطوير development.py الخاص بتطبيقنا testsite، لذا افتح ملف إعدادات التطوير: nano testsite/settings/development.py واكتب ضمنه تعليمة الاستيراد من base.py ومن بعدها الإعدادات الخاصة ببيئة التطوير وفق التالي: from .base import * DEBUG = True DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } كما ترى، الإعدادات الخاصة ببيئة التطوير لدينا هي التنقيح DEBUG الذي تم تفعيله بالقيمة True ومحدد قاعدة البيانات DATABASES الذي يشير إلى قاعدة البيانات المحلية وهي في حالتنا SQLite، وهذه الإعدادات لن تتماشى مع بيئة الإنتاج إذ إن الإنتاج يتطلب إلغاء تفعيل التنقيح والربط مع قاعدة بيانات الإنتاج. تنويه: يعود إلغاء تفعيل التنقيح في بيئة الإنتاج إلى أسبابٍ أمنية لضمان عدم تسريب أي معلومات سرية عن المشروع من الممكن أن تظهر في أثناء التنقيح، وتعرض تطبيقك للخطر مثل API أو KEY أو PASS أو SECRET أو SIGNATURE أو TOKEN، لذا احرص دائمًا على إلغاء تفعيل التنقيح DEBUG قبل نشر تطبيقك في بيئة الإنتاج. لننتقل للملف production.py: nano testsite/settings/production.py سنعتمد نفس الطريقة، ولكن مع إلغاء تفعيل التنقيح ووضع محددات قاعدة بيانات الإنتاج كما يلي: from .base import * DEBUG = False ALLOWED_HOSTS = [] DATABASES = { 'default': { 'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.sqlite3'), 'NAME': os.environ.get('SQL_DATABASE', os.path.join(BASE_DIR, 'db.sqlite3')), 'USER': os.environ.get('SQL_USER', 'user'), 'PASSWORD': os.environ.get('SQL_PASSWORD', 'password'), 'HOST': os.environ.get('SQL_HOST', 'localhost'), 'PORT': os.environ.get('SQL_PORT', ''), } } يمكنك استخدام dotenv مع وضع قيم افتراضية لضبط محددات قاعدة البيانات وفق المثال المقدم أعلاه، ولكن احرص على تبديل قيم المتغيرات بما يناسب قاعدة بيانات التطوير الخاصة بك. إذًا، فقد ضبطنا التطبيق ليعمل بنموذجين مختلفين من الإعدادات يتم القرار بينهما بناءً على قيمة المحدد DJANGO_SETTINGS_MODULE الموجود ضمن الملف env.، ففي حالتنا على سبيل المثال سيعني العمل بإعدادات الإنتاج أن DEBUG سيحمل القيمة False، وأن ALLOWED_HOSTS سيُحدد، أما التطبيق فسيبدأ باستخدام قاعدة بيانات مختلفة هي قاعدة بيانات الإنتاج (التي يُفترض أنك جهزتها مسبقًا على الخادم الخاص بك). الخطوة 4: ضبط إعدادات الأمان لتطبيق جانغو يتضمن جانغو إعدادات أمانٍ جاهزة لتضيفها إلى مشروعك، وهذه الإعدادات ضرورية لكل تطبيق يعمل في بيئة الإنتاج ويطلق للاستخدام العام. سيقتصر عملنا مع إعدادات الأمان على تهيئة ملف الإنتاج production.py فقط إذا لا يوصى باستخدامها في بيئة التطوير. أهم ما تتناوله هذه الإعدادات هو فرض استخدام HTTPS للعديد من مميزات مواقع الويب مثل ملفات تعريف الارتباط للجلسة وملفات تعريف الارتباط CSRF وتوجيه HTTP إلى HTTPS… إلخ، لذا فإن تنفيذ هذه الخطوة يفترض أن تطبيقك مجهز بخادم ويب وأن لديك اسم نطاق يشير إلى خادم التطبيق. افتح الملف production.py باستخدام محرر النصوص: nano production.py أضف ضمنه القيم الناقصة ليصبح بهذا الشكل: from .base import * DEBUG = False ALLOWED_HOSTS = ['your_domain', 'www.your_domain'] DATABASES = { 'default': { 'ENGINE': os.environ.get('SQL_ENGINE', 'django.db.backends.sqlite3'), 'NAME': os.environ.get('SQL_DATABASE', os.path.join(BASE_DIR, 'db.sqlite3')), 'USER': os.environ.get('SQL_USER', 'user'), 'PASSWORD': os.environ.get('SQL_PASSWORD', 'password'), 'HOST': os.environ.get('SQL_HOST', 'localhost'), 'PORT': os.environ.get('SQL_PORT', ''), } } SECURE_SSL_REDIRECT = True SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SECURE_BROWSER_XSS_FILTER = True لنشرح المحددات بالترتيب: SECURE_SSL_REDIRECT: يوجه كافة الطلبات من HTTP إلى HTTPS بالطبع مع استثناء بعض الحالات المعفاة من ذلك، يستخدم التطبيق إذًا الاتصالات المشفرة ما يستلزم تهيئة شهادات TLS على الخادم، وفي حال كنت تستخدم Nginx أو أباتشي فسيكون هذا الضبط إضافيًا وغير ملزم إذ إن كلا الخادمين يقوم فعلًا بهذا التوجيه. SESSION_COOKIE_SECURE: يخبر هذا المحدد المتصفح بعدم إمكانية التعامل مع ملفات تعريف الارتباط خارج HTTPS. ما يعني أن كل ما يولّده مشروعك من ملفات تعريف ارتباط (مثل تلك الخاصة بعمليات تسجيل الدخول) ستعمل فقط عبر اتصالٍ مشفر. CSRF_COOKIE_SECURE: يشبه المحدد السابق ولكنه يُطبق على مفتاح CSRF الخاص بك. تحمي هذه الطريقة تطبيقك من هجمات تزوير الطلبات عبر المواقع من خلال تأكدها -باستخدام مفاتيح CSRF- من أن كافة النماذج المقدمة سواء لتسجيل الدخول أو الاشتراكات أو غيرها هي نماذج أصلية تم إنشاؤها بواسطة تطبيقك ولم يزوّرها طرفٌ ثالث. SECURE_BROWSER_XSS_FILTER: يضع هذا المحدد الترويسة X-XSS-Protection: 1; mode=block في كافة الردود الصادرة عن التطبيق ما لم تكن موجودة أساسًا، يمنع هذا الإعداد أي طرف ثالث من حقن سكربتات برمجية في مشروعك، فعلى سبيل المثال لو خزن مستخدمٌ ما سكربتًا في قاعدة بياناتك مستغلًا أحد الحقول العامة، فلن يعمل هذا السكربت عند استرجاعه وعرضه للمستخدمين الآخرين. يمكنك الحصول على مزيد من المعلومات حول الإعدادات الأمنية بالاطلاع على توثيقات جانغو. تحذير: توصي توثيقات جانغو بعدم الاعتماد كليًا على SECURE_BROWSER_XSS_FILTER وإغفال الجوانب الأخرى، بل عليك دائمًا التأكد من ضبط حقول الإدخال لتطبيقك لتقبل المدخلات الصالحة فقط. إعدادات إضافية تتعلق الإعدادات الإضافية الواردة أدناه بأمن النقل الصارم HSTS الذي يفرض استخدام SSL على كامل موقعك وفي كل الأوقات: SECURE_HSTS_SECONDS: يعين المدة الزمنية المحددة لاستخدام HSTS مقدرةً بالثانية، ففي حال ضبطت القيمة إلى ساعة واحدة (بالطبع حولها إلى ثواني قبل كتابتها) فهذا يعني أن متصفحك سيستخدم HTTPS حصرًا لمدة ساعة كاملة في كل مرة تزور فيها إحدى صفحات التطبيق، وإن زُرت أي جزء غير آمن من الموقع خلال هذه الساعة فإنك ستحصل على رسالة خطأ. SECURE_HSTS_PRELOAD: استخدامه مشروط بتعيين قيمة للمحدد السابق أي زمن HSTS، يوجه هذا النوع من الترويسات متصفح الإنترنت ليُحمّل موقعك مسبقًا أي ليُضيفه على قائمة خاصة مدمجة مع كود المتصفح وظيفتها فرض استخدام الاتصالات المشفرة مع المواقع الموجودة ضمنها (ومن بينها موقعك)، تعمل هذه الطريقة التي يصطلح على تسميتها Preload مع المتصفحات الشائعة مثل Firefox و Chrome، ولكن احذر في حال اعتمدتها لموقعك فلن يكون التخلي عن التشفير أمرًا سهلًا بعدها، إذ سيتطلب إزالة الموقع يدويًا من قائمة HSTS Preload وقد تمتد العملية لأسابيع. SECURE_HSTS_INCLUDE_SUBDOMAINS: تفعيله يعني تطبيق ترويسة HSTS على كافة النطاقات الفرعية تحت اسم نطاق الرئيس، فلو كان نطاقك your_domain فإن أي رابط فرعي تحته مثل unsecure.your_domain حتى لو كان لا يرتبط بتطبيق جانغو سيخضع للتشفير. تحذير: قد يؤدي الاستخدام الخاطئ لهذه الإعدادات الأمنية الإضافية إلى تعطيل موقعك لفترة لا يستهان بها، لذا تعلم المزيد عنها من توثيقات جانغو قبل تطبيقها في بيئتك الفعلية. الإعدادات التي عرضناها تعد أساسًا جيدًا لمعظم تطبيقات جانغو، ولكن ادرس بيئتك الفعلية وفكر كيف سينعكس تطبيق هذه الإعدادات على مشروعك. الخطوة 5: استخدام python-dotenv مع معلومات التطبيق السرية ستتعلم في هذا الجزء الأخير كيفية الاستفادة المثلى من حزمة python-dotenv لحماية معلومات تطبيقك الحساسة مثل المفتاح السري SECRET_KEY ورابط تسجيل الدخول إلى لوحة التحكم، في الواقع تعدّ هذه الآلية فكرةً ممتازة لمن يرغب برفع تطبيقه على GitHub أو GitLab أو أي منصة أخرى مشابهة فهي ستحمي بياناته السرية من النشر أمام العامة، وبدلًا من نشرها على المنصة سينشئ المطور ملف env. جديد يحدد من خلاله المتغيرات السرية لكل بيئة جديدة يستخدمها سواء محليًا أو على خادم. لنبدأ بإخفاء المفتاح السري SECRET_KEY، افتح الملف env.: nano .env واكتب ضمنه التالي: DJANGO_SETTINGS_MODULE="django_hardening.settings.development" SECRET_KEY="your_secret_key" ثم افتح الملف base.py: nano testsite/settings/base.py وعدّل ضمنه المفتاح السري SECRET_KEY ليأخذ قيمته من متغيرات البيئة المعرفة في env. وفق التالي: . . . SECRET_KEY = os.getenv('SECRET_KEY') . . . أما عمليتنا الأخيرة فهي إخفاء عنوان URL للوحة التحكم عبر إضافة سلسلة طويلة من المحارف العشوائية إليه، سيحميك ذلك هجمات القوة الغاشمة brute force التي تتعرض لها مواقع الويب بهدف فرض حقول تسجيل الدخول عنوةً ومحاولة تخمينها من قبل المهاجم. افتح الملف env. مجددًا: nano .env وأضف على محتوياته المحدد SECRET_ADMIN_URL: DJANGO_SETTINGS_MODULE="django_hardening.settings.development" SECRET_KEY="your_secret_key" SECRET_ADMIN_URL="very_secret_url" لنخفي الآن عنوان URL بالمحدد SECRET_ADMIN_URL، افتح الملف urls.py: nano /testsite/urls.py تنويه: لا تنسَ استبدال your_secret_key و very_secret_url بالسلاسل النصية التي تشكل مفتاحك السري وعنوان URL الخاص بك، وفي حال رغبت باستعمال سلاسل نصية عشوائية فإن بايثون يقدم لك مكتبة مميزة secrets.py لتوليدها، مع مجموعة مفيدة من الأمثلة لإنشاء برامج بسيطة تولد هذا النوع من السلاسل العشوائية الآمنة. ومن ثم عدّله ليصبح كما يلي: import os from django.contrib import admin from django.urls import path urlpatterns = [ path(os.getenv('SECRET_ADMIN_URL') + '/admin/', admin.site.urls), ] وبذلك يكون رابط لوحة التحكم لتطبيقك هو /very_secret_url/admin بدلًا من /admin/ فقط. خاتمة أعددنا في هذا المقال تطبيق جانغو سهل الاستخدام والنشر في بيئاتٍ مختلفة، ويستثمر ميزات python-dotenv للتعامل مع الإعدادات والأسرار، بالإضافة لكونه آمن ومجهز بضوابط الأمان الأساسية اللازمة لمرحلة الإنتاج، وفي حال طبقت المعايير التي ناقشناها والموصى بها فإنك ستحظى بتطبيقٍ آمن يحمل المزايا الأساسية التالية: تشفير كافة الاتصالات عبر SSL/HTTPS (مثل: النطاقات الفرعية وملفات تعريف الارتباط وملفات ارتباط CSRF). الحماية من هجمات البرمجة العابرة للمواقع XSS. الحماية من هجمات تزوير الطلبات عبر المواقع CSRF. إخفاء المفتاح السري للمشروع. إخفاء رابط لوحة التحكم ما يمنع هجمات القوة الغاشمة brute force. فصل إعدادات التطوير عن إعدادات الإنتاج. إن رغبت بمعرفة المزيد حول جانغو وإعداداته اطلع على توثيقاته الرسمية والقسم الخاص به على أكاديمية حسوب. ترجمة -وبتصرف- للمقال How To Harden the Security of Your Production Django Project لصاحبه Ari Birnbaum. اقرأ أيضًا إنشاء تطبيق جانغو وتوصيله بقاعدة بيانات تثبيت إطار العمل جانغو على أوبنتو حزم بايثون الثمانية التي تسهل تعاملك مع Django تخصيص لوحة التحكم المرفقة مع Django
  21. في مقال بناء لعبة نرد بسيطة بلغة بايثون استخدمنا بايثون لبرمجة لعبة نرد بسيطة معتمدة على النص، وفي مقالنا السابق تعلمنا كيفية تجهيز بيئة لعبة رسومية من الصفر أيضًا بلغة بايثون، والآن سنطوّر هذه اللعبة ونضيف إليها شخصية تلعب دور اللاعب أو بطل اللعبة. يمكنك مطالعة المقال ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. يُطلق اسم الكائن أو الشخصية sprite على الأفاتار أو أيقونة الشخصية التي يتحكم بها اللاعب في Pygame، إن كنت لم تجهز له صورةً بعد، فيمكنك تنزيل الصور walk.png ذوات الأرقام 0 و2 و4 و5 من موقع SuperTux الخاصة بلعبة Supertux وهي مفتوحة المصدر أو من الملفات المرفقة، ومن ثم إعادة تسميتها لتصبح hero1.png إلى hero4.png، أو استخدام كريتا أو إنكسكيب لتصميم صور مناسبة، أو الاستعانة بمنصة OpenGameArt مفتوحة المصدر لخياراتٍ أخرى، واحفظ كافة الصور التي ستعتمدها في المجلد images الموجود ضمن مجلد مشروعك، وإن لم يكن موجودًا أنشئ واحدًا. لتجعل لعبتك مثيرة وحقيقية استخدم كائنًا متحركًا، وتحتاج لذلك عدة رسومات للكائن في وضعيات مختلفة لتعطي وهم الحركة، أشهر الحركات المتعارف عليها في الرسوم المتحركة هي دورة المشي وتتطلب أربع صور لتحقق المطلوب. تنويه: كود اللعبة المستخدم هنا يصلح للكائنات الثابتة والمتحركة. سمي صورة كائن اللعبة بالاسم hero.png وإن رغبت بجعله متحركًا فسمي الصور الخاصة بدورة المشي بأرقامٍ متتابعة بدءً من hero1.png، واحفظها جميعًا في المجلد images الموجود ضمن مجلد مشروعك. إنشاء صنف بايثون يتطلب إظهار كائن بايثون Object على الشاشة إنشاء صنف class. إن كنت تستخدم كائنًا ثابتًا ليمثل شخصية بطل اللعبة، فاكتب التعليمات التالية في المقطع الخاص بكائنات بايثون في ملفك: ''' Objects ''' class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.images = [] img = pygame.image.load(os.path.join('images', 'hero.png')).convert() self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() ستُنشئ هذه التعليمات كائن بايثون افتراضي object يشير إلى بطل لعبتك، والكائنات في لغات البرمجة الكائنية تُشتق دومًا من الأصناف. توفر Pygame قوالب جاهزة للكائنات مثل: pygame.sprite.Sprite وهو ما يتيح لك اختيار الصورة التي تريدها لتمثل بطل لعبتك، وبدون هذه القوالب كنت ستحتاج لمعرفة ممتازة بلغة بايثون لتبرمج الكائن من الصفر، وهذه ميزة استخدام مكتبة جاهزة مثل Pygame. أما في حال أردت استخدام دورة المشي لإنشاء شخصية متحركة، فاحفظ صور وضعيات المشي الأربع لبطل اللعبة بأربع صور منفصلة سمّها hero1.png إلى hero4.png ضمن مجلد الصور ومن ثم استخدم حلقة التكرار لينتقل بايثون من صورة إلى أخرى، في الواقع أن يكون لكل صنف مهمة محددة لا تؤثر "بعالم اللعبة" هو واحد من أفضل ميزات البرمجة الكائنية، ففي حالتنا سيعطي الانتقال بين وضعيات المشي وهم الحركة للمشاهد، وسيحدث ذلك بغض النظر أن كل ما يجري في المحيط. ''' Objects ''' class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() إحضار شخصية اللاعب إلى عالم اللعبة بعد أن جهزنا الصنف الذي يشير إلى بطل اللعبة، يتعين علينا استدعائه ضمن اللعبة ليظهر في عالمها، فلو حاولت تشغيل لعبتك الآن ستجدها فارغة كما كانت عليه في نهاية مقالنا السابق، فالكائن الذي يشير لبطل اللعبة لم يرتبط بها بعد ولم يُضاف إلى مجموعة كائنات Pygame. لتنجز ذلك اكتب التعليمات التالية في مقطع الإعدادات ضمن برنامجك: player = Player() # إنتاج اللاعب player.rect.x = 0 # X توجه إلى player.rect.y = 0 # Y توجه إلى player_list = pygame.sprite.Group() player_list.add(player) حاول الآن تشغيل اللعبة، لن تجد أي فرق، إذ إنك لن تلحظ بطل اللعبة فقد ظهر لأجزاء من الثانية فقط واختفى، تذكر ما شرحناه في مقالنا السابق لجهة أن إتاحة أي شيء باستمرار على الشاشة يستلزم إضافته إلى حلقة اللعبة الرئيسية، وبذلك سيرسمه بايثون مع كل تكرار للحلقة. أضف إذًا التعليمات التالية للحلقة: world.blit(backdrop, backdropbox) player_list.draw(world) # draw player pygame.display.flip() clock.tick(fps) ومن ثم شغل اللعبة، ستجد شخصية اللاعب فيها. ضبط قناة الشفافية ألفا لشخصية اللعبة اعتمادًا على الطريقة التي أنشأت بها كائن اللعبة ستظهر حوله مساحة ملونة تحيط به تسمى في الألعاب الحديثة صندوق التصادم hit box، هذه المساحة تشغلها القناة ألفا ويفترض أن تكون غير مرئية، لكن بايثون لا يملك التوجيهات الخاصة بذلك بعد. يمكنك إخبار بايثون باللون الذي تعدّه غير مرئي عبر ضبط إعدادات القناة ألفا باستخدام قيم النظام اللوني RGB، إذا كنت لا تعرف قيمة RGB التي يستخدمها رسمك للقناة ألفا فافتح الشخصية باستخدام أحد البرامج الاختصاصية مثل بينتا أو إنكسكيب واملأ المساحة الفارغة حوله بلونٍ مميز مثل ‎#00ff00 الذي يكافئ اللون الأخضر "للشاشة الخضراء" ثم سجل هذه القيمة المكتوبة بالترميز الست عشري واكتبها في كود بايثون لتشكل قيمة القناة ألفا. أضف الآن السطرين التاليين إلى التعليمات الخاصة بإنشاء كائن اللعبة حتى تضبط القناة ألفا: img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert() img.convert_alpha() # تحسين ألفا img.set_colorkey(ALPHA) # تحديد قيمة ألفا إذا كنت تعتقد أن الصورة التي تعتمدها تملك بالفعل قيمة لقناة ألفا فاضبط المتغير ALPHA على القيمة 0 أو 255 فكلاهما من القيم الشائعة، ولكن لخبرتي مجال المونتاج -يقول كاتب المقال- أفضل إنشاء قناة ألفا وضبطها بنفسي لتكون قيمتها أكيدة دون لبس. أضف المتغير التالي في المقطع الخاص بالمتغيرات: ALPHA = (0, 255, 0) تكافئ القيمة 0,255,0 في نظام RGB اللون الأخضر، ويمكنك على الحصول قيم كافة الألوان باستخدام برنامج رسومات جيد مثل جيمب أو كريتا أو إنكسكيب، أو الاستعانة بأحد نظم اختيار الألوان الشهيرة مفتوحة المصدر مثل KColorChooser أو ColourPicker. إذا كانت الصور التي تعتمدها تملك قيمة مغايرة للقناة ألفا فاختر للمتغير قيمة مناسبة وفق ما يستلزم الأمر، ودائمًا القيم التي تضعها لألفا ستصبح غير مرئية أيًا كانت، ففي حال وضعت قيمة ألفا 000 مثلًا (وهي تعني اللون الأسود) فاجعل عندها خطوط رسمك التي تكون سوداء بالعادة باللون 111 وهو أقرب ما يمكن للأسود ولن يلاحظ المستخدم الفرق. شغل لعبتك الآن ولاحظ النتائج. أما الشكل النهائي للكود بعد كل هذه التعديلات سيكون كما يلي: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from typing import Tuple import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 # frame rate ani = 4 # animation cycles world = pygame.display.set_mode([worldx, worldy]) BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ALPHA = (0, 255, 0) ''' Objects ''' class Player(pygame.sprite.Sprite): """ Spawn a player """ def __init__(self): pygame.sprite.Sprite.__init__(self) self.images = [] for i in range(1, 5): img = pygame.image.load(os.path.join('images', 'hero' + str(i) + '.png')).convert() img.convert_alpha() # optimise alpha img.set_colorkey(ALPHA) # set alpha self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() ''' Setup ''' backdrop = pygame.image.load(os.path.join('images', 'stage.png')) clock = pygame.time.Clock() pygame.init() backdropbox = world.get_rect() main = True player = Player() # spawn player player.rect.x = 0 # go to x player.rect.y = 0 # go to y player_list = pygame.sprite.Group() player_list.add(player) ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False world.blit(backdrop, backdropbox) player_list.draw(world) pygame.display.flip() clock.tick(fps) تابع معنا المقال التالي لتضيف بعض الإثارة وتتعلم كيفية تحريك الكائن. ترجمة -وبتصرف- للمقال How to add a player to your Python game لصاحبه Seth Kenlon. اقرأ أيضًا المقال التالي: تحريك شخصية في لعبة باستخدام Pygame المقال السابق: بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب Pygame برمجة لعبة حجرة ورقة مقص باستخدام لغة بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون تعرف على أشهر لغات برمجة الألعاب
  22. شرحنا في المقال السابق كيفية بناء لعبة نرد بسيطة باستعمال لغة بايثون، وتعرفنا على وحدة السلحفاة Turtle الخاصة برسم الخطوط والأشكال البسيطة. وسنعرض في هذا المقال وحدة أخرى لكنها متقدمة ومتخصصة بالألعاب الرسومية كما يشير اسمها فهي تدعى PyGame، هذه الوحدة ليست مضمنة افتراضيًا في بايثون مثل وحدة السلحفاة، لذا عليك تنزيلها وتثبيتها بنفسك فلها مكتبةٌ خاصة. يمكنك مطالعة المقالات ضمن السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. يعتمد الأسلوب الحديث في تطوير بايثون على مفهوم البيئات الافتراضية، التي توفر لتطبيقك مساحة خاصة ومعزولة تتضمن كل ما يحتاج إليه، وتساعدك في إدارة المكتبات اللازمة له، ما يعني أن متطلبات التشغيل ستكون واضحة أمامك ويمكنك تحديدها بدقة لأي شخص آخر يرغب باستخدام التطبيق أو اللعبة حتى يثبتها لديه. تستطيع إدارة البيئة الافتراضية يدويًا لكن إدارتها وتنزيل حزمها من خلال بيئة تطوير متكاملة IDE التي تبقى الطريقة الأسهل والأكثر مرونة، وهذا ما طبقناه في حالتنا عبر بيئة PyCharm في المقال السابق المشار إليه أعلاه. مكتبة Pygame Pygame هي مكتبة أو وحدة بايثون، تتضمن مجموعة من الشيفرات الجاهزة الشائعة الاستخدام في مجال الألعاب، يمكنك الاستفادة منها عوضًا عن كتابتها من الصفر من جديد فهي توفر عليك إعادة اختراع العجلة كما يقال، تذكر وحدة السلحفاة التي جربّناها في مقالنا السابق وتخيل مثلًا أنك ستكتب الشيفرة البرمجية اللازمة لتنشئ القلم قبل أن ترسم به، كم ستكون العملية معقدة، وبالمثل في ألعاب الفيديو هنا تكمن فائدة Pygame. تحتاج اللعبة إلى عالم افتراضي أو بيئة تجري فيها الأحداث، في Pygame يمكنك إضافة ذلك بإحدى طريقتين: تعيين لون للخلفية. تعيين صورة للخلفية. وفي الحالتين لن تتفاعل شخصيات اللعبة مع الخلفية سواء أكانت صورة أو لون فهي مجرد بيئة جامدة للعبتك. كيفية استعمال Pygame أنشئ أولًا مشروعًا جديدًا، ومعه بطبيعة الحال مجلدًا خاصًا باللعبة لتُحفظ فيه كافة الملفات اللازمة لعملها فهذا التفصيل مهم. إنشاء المشروع الجديد عملية بسيطة في بيئة التطوير المتكاملة، ففي PyCharm المستخدمة في حالتنا، يكفي أن تضغط على قائمة ملف، وتختار "مشروع جديد" وتحدد بعدها اسمًا له (game_001 على سبيل المثال)، لاحظ أن مشروعك سيُحفظ ضمن مجلد خاص بمشاريع PyCharm. ولا تنسَ تفعيل خيار بناء البيئة الافتراضية باستخدام Virtualenv وخيار إنشاء الملف main.py مع الإبقاء على الخيارات الأخرى كما هي افتراضيًا. اضغط الآن على زر إنشاء ليظهر مشروعك الجديد في نافذة PyCharm وهو يتألف من بيئة افتراضية env (تجدها في العمود اليساري) وملف نموذجي main.py. امسح كافة المعلومات من الملف main.py لتكتب بداخله شيفرة برنامجك، عادةً ما تبدأ ملفات بايثون بكتابة نوع الملف ومن ثم اسمك والترخيص الذي تستخدمه، استخدم ترخيصًا مفتوح المصدر لتفسح المجال أمام بقية المطورين لتطوير لعبتك وتحسينها، انظر صيغة الملف المبينة أدناه: #!/usr/bin/env python3 # by Seth Kenlon ## GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. الآن ضمن الملف نفسه، حدد الوحدات التي تحتاج لاستخدامها، وانتبه فلن تكتب إشارة # قبل التعليمات هنا، فهذه الإشارة تعني في لغة بايثون أنك تترك تعليقًا أو ملاحظةً لك أو لغيرك من المطورين. import pygame # ‫لاستيراد الوحدة pygame import sys # تسمح لبايثون باستخدام نظام التشغيل import os # لمساعدة بايثون في التعرف على نظام تشغيلك والتكيف معه ستلاحظ أن PyCharm يميز الكلمة pygame عن كل من os و sys ويعدّها خطأ، فهو لم يتعرف على هذه الوحدة بعد، إذ إنها غير مضمنة افتراضيًا مع بايثون كما ذكرنا في المقدمة، لذا ينبغي علينا تثبيتها قبل استخدامها. لتثبيتها يكفي أن تمرر مؤشر الفأرة فوق الكلمة وستظهر لك نافذة منبثقة تُطالبك بتثبيت الحزمة، انقر على رابط التثبيت وانتظر انتهاء العملية وستصبح Pygame جاهزة في بيئتك الافتراضية. أما لو أردت تثبيتها يدويًا بدون بيئة التطوير المتكاملة فاستخدم الأمر pip. تقسيم شيفرة اللعبة تقسيم الشيفرة البرمجية إلى مقاطع وإضافة التعليقات الكتلية عليها هو عملية تنظيمية تفيد المبرمج في التخطيط لبرنامجه ومعرفة ما ينبغي كتابته في كل مقطع، وتسهل عليه المراجعة والتعديل فيما بعد، علمًا أن هذه التعليقات غير مرئية لبايثون أي أنه يتجاهلها ولا ينفذها وهي تظهر في برنامجك لدى قراءة الشيفرة المصدرية فقط. ''' Variables ''' # ضع المتغيرات في هذا المقطع ''' Objects ''' # ضع هنا أصناف ودوال بايثون ''' Setup ''' # ضع كود التشغيل-لمرة-واحدة ''' Main Loop ''' # ضع هنا الحلقة الرئيسية للعبة لنضبط الآن قياس نافذة اللعبة، ويُراعى في ذلك اختلاف أحجام شاشات العرض بين الحواسيب والهواتف المحمولة وغيرها، لذا سنضع رقمًا وسطيًا يناسب معظم الشاشات، مع العلم بوجود طرق خاصة يتبّعها مطورو الألعاب الحديثة ليتكيف عرض اللعبة تلقائيًا مع حجم شاشة جهاز المستخدم، ولكن بصفتك في بداية الطريق يكفي أن تضع قياسًا واحدًا الآن كما يلي: ''' Variables ''' worldx = 960 worldy = 720 لننتقل للإعدادات الخاصة بمحرك Pygame، وتتمثل في ضبط معدل الأطر frame rate والساعة الداخلية internal clock (باستخدام الكلمة المفتاحية init). اكتب أولًا المتغيرات التالية في المقطع المخصص للمتغيرات: fps = 40 # معدل الأطر ani = 4 # دورات الحركة والآن التعليمات الخاصة بالساعة الداخلية ضمن مقطع الإعدادات: ''' Setup ''' clock = pygame.time.Clock() pygame.init() ضبط الخلفية اختر صورة مناسبة للعبتك أو صممها بأحد برامج التصميم الجرافيكي واحفظها باسم stage.png في المجلد images ضمن مجلد مشروعك. إليك بعض برامج التصميم المجانية التي يمكنك استخدامها على سبيل المثال لا الحصر: جيمب برنامج بسيط وسهل التعلم. كريتا برنامج احترافي يحاكي الطلاء وأدوات الرسم الحقيقية لينتج صورًا مميزة. إنكسكيب برنامج للرسوم المتجهة، توظف فيه الأشكال والخطوط ومنحنيات بيزير لرسم ما تريد. لا داعي لتكون صورة الخلفية معقدة في هذه المرحلة، إذ باستطاعتك تغييرها في أي وقت. والآن بعد أن جهزت الصورة اكتب التعليمات التالية ضمن مقطع الإعدادات: world = pygame.display.set_mode([worldx,worldy]) backdrop = pygame.image.load(os.path.join('images','stage.png')) backdropbox = world.get_rect() أما في حال رغبت بالاكتفاء بتلوين الخلفية فقط دون استخدام صورة، تكفيك هذه التعليمة: world = pygame.display.set_mode([worldx, worldy]) ولكن عليك أولًا تعريف الدرجات اللونية التي تفضلها للأحمر والأخضر والأزرق وفق النظام اللوني RGB، وكتابتها ضمن مقطع المتغيرات كما يلي: ''' Variables ''' BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) اكتشاف الأخطاء بيئة التطوير PyCharm مفيدة في اكتشاف مخالفات قواعد كتابة اللغة، فهي تُميّز التحذيرات باللون الأصفر والأخطاء باللون الأحمر، وتعد صارمة بعض الشيء في هذا المجال. التحذيرات في معظمها مخالفات بسيطة لأسلوب بايثون، وهو أمرٌ ستتعلمه وتتجاوزه مع الزمن والخبرة، لذا يعد تجاهلها آمنًا ولن يؤثر على عمل برنامجك. أما الأخطاء ينبغي عليك تصحيحها بالتأكيد لأنها تعطل تنفيذ البرنامج، فعلى سبيل المثال تشدد PyCharm على وجود ما يدل على إدخال سطر جديد في نهاية السطر الأخير من الكود، لذا عليك الضغط على زر Enter أو Return في لوحة المفاتيح في نهاية الكود حتى يعمل. تشغيل اللعبة احفظ التغييرات وشغل اللعبة، عبر الضغط على زر "تشغيل الوحدة Run Module" من قائمة تشغيل في IDLE أو الضغط على "تشغيل ملف Run file" من شريط الأدوات في PyCharm. ويمكنك أيضًا التشغيل باستخدام نافذة الطرفية في يونيكس أو موجه سطر الأوامر في ويندوز بشرط أن تقوم بذلك وأنت في البيئة الافتراضية لبايثون. لكنك ستلاحظ بعد التشغيل أن لعبتك تعمل لمدة بسيطة جدًا أجزاء من الثانية فقط، وهو موضوع فقرتنا التالية. حلقة التكرار سيعمل البرنامج المكتوب بلغة بايثون لمرة واحدة فقط ما لم تعطه أوامر واضحة للتكرار، وبسبب سرعة معالجة الحواسيب فإن ذلك سيتم خلال أجزاء من الثانية حتى أن المستخدم لن يلحظ اللعبة، لنعالج الموضوع سنستخدم الحلقة While حتى تبقى اللعبة نشطة ومتاحة أمام المستخدم، وسنعيّن لهذه الحلقة متغير نسميه main ونعطيه قيمة ما، يضبط هذا المتغير تكرار الحلقة فهي ستتكرر باستمرار طالما أن قيمته لم تتغير، وبالمناسبة يطلق هذا النوع من الحلقات اسم الحلقة الرئيسية main loop. إذًا اكتب التعليمات التالية في برنامجك ضمن المقطع الخاص بالحلقة الرئيسية: ''' Main loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False ولا تنسَ الضغط على زر Enter أو Return في نهاية السطر الأخير، ليُضاف سطر فارغ في نهاية ملف البرنامج. حدّث أيضًا عالم اللعبة الافتراضي في الحلقة الرئيسية، بكتابة التعليمة التالية إن كنت تعتمد صورة للخلفية: world.blit(backdrop, backdropbox) أو التعليمة أدناه في حال كنت تستخدم الألوان فقط للخلفية: world.fill(BLUE) وأخيرًا أخبر الوحدة Pygame لتحدّث كل ما يظهر على الشاشة مع ساعتها الداخلية، وذلك وفق الأوامر الآتية: pygame.display.flip() clock.tick(fps) احفظ الملف وشغله، لتحظى بأكثر لعبة مملة رأيتها! وهذا طبيعي فلا شيء على الشاشة سوى خلفية اللعبة، لكن لا تقلق سنطوّرها تباعًا في المقالات اللاحقة، يمكنك الآن الضغط على q للخروج. تثبيت البيئة الافتراضية لبايثون تدير PyCharm المكتبات وتوفر لك متطلبات التشغيل المناسبة لبرنامجك، لكن المستخدم لن يشغل البرنامج أو اللعبة من PyCharm كما تفعل أنت، لذا يُعدّ تثبيت بيئتك الافتراضية خطوةً مهمة حتى أنها تعادل في الأهمية حفظ التعديلات على ملف البرنامج. استعرض قائمة الأدوات Tools واختر خيار "التزامن مع متطلبات بايثون" Sync Python Requirements، وستُحفظ عندها كافة اعتماديات المكتبات التي استخدمتها ضمن ملف نصي خاص يدعى requirements.txt. وفي المرة الأولى التي يعمل فيها هذا التزامن ستُطالبك PyCharm بتثبيت بعض الإضافات والاعتماديات اضغط على زر قبول، وسيتشكل عندها ملف المتطلبات الخاص بك requirements.txt ويُحفظ في مجلد مشروعك. الكود النهائي للعبة هذا ما سيبدو عليه كود اللعبة النهائي إلى الآن: #!/usr/bin/env python3 # by Seth Kenlon # GPLv3 # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import pygame import sys import os ''' Variables ''' worldx = 960 worldy = 720 fps = 40 # frame rate ani = 4 # animation cycles main = True BLUE = (25, 25, 200) BLACK = (23, 23, 23) WHITE = (254, 254, 254) ''' Objects ''' # put Python classes and functions here ''' Setup ''' clock = pygame.time.Clock() pygame.init() world = pygame.display.set_mode([worldx, worldy]) backdrop = pygame.image.load(os.path.join('images', 'stage.png')) backdropbox = world.get_rect() ''' Main Loop ''' while main: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() try: sys.exit() finally: main = False if event.type == pygame.KEYDOWN: if event.key == ord('q'): pygame.quit() try: sys.exit() finally: main = False world.blit(backdrop, backdropbox) pygame.display.flip() clock.tick(fps) محطتنا التالية سنطوّر اللعبة في مقالنا القادم ونضيف بعض الشخصيات والحركة لعالمها الفارغ، لذا يمكنك البدء بتحضير بعض الرسومات المميزة منذ الآن والمتابعة معنا. ترجمة -وبتصرف- للمقال Build a game framework with Python using the Pygame module لصاحبه Seth Kenlon. اقرأ أيضًا المقال التالي: إضافة لاعب إلى لعبة مطورة باستخدام بايثون ومكتبة Pygame المقال السابق: بناء لعبة نرد بسيطة بلغة بايثون تعرف على أشهر لغات برمجة الألعاب برمجة لعبة حجرة ورقة مقص باستخدام لغة بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  23. لغة بايثون هي لغة برمجة سهلة التعلم موازنةً باللغات الأخرى مثل لغة جافا ولغة C ولغة ++C، بالإضافة لكونها متعددة الأغراض فهي تستخدم في بناء تطبيقات سطح المكتب وألعاب الفيديو والرسومات ثلاثية الأبعاد والمواقع الإلكترونية وغيرها، زد على أنها لغة قوية برمجيًا وممتازة للتطبيقات المتقدمة، هذا كله جعلها خيارًا مثاليًا للجميع بصرف النظر عن عملهم وعمرهم وعدد سنوات خبرتهم، بالأخص للراغبين بتعلم البرمجة. هذا المقال جزءًا من سلسلة مقالات حول تصميم لعبة بلغة بايثون ومكتبة Pygame وإليك كامل مقالات السلسلة: بناء لعبة نرد بسيطة بلغة بايثون. بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب PyGame. إضافة لاعب إلى اللعبة المطورة باستخدام بايثون و Pygame. تحريك شخصية اللعبة باستخدام PyGame. إضافة شخصية العدو للعبة. إضافة المنصات إلى لعبة بايثون باستخدام الوحدة Pygame. محاكاة أثر الجاذبية في لعبة بايثون. إضافة خاصية القفز والركض إلى لعبة بايثون. إضافة الجوائز إلى اللعبة المطورة بلغة بايثون. تسجيل نتائج اللعبة المطورة بلغة بايثون وعرضها على الشاشة. إضافة آليات القذف إلى اللعبة المطورة بلغة بايثون. تثبيت بايثون ستحتاج إلى تثبيت بايثون الإصدار 3 لتتابع معنا خطوات العمل، لذا اتبع الخطوات المناسبة لنظام تشغيلك. إن كنت تستخدم نظام تشغيل لينكس، فستجد بايثون متوفرًا ضمنه على الأغلب، استعلم عن إصداره بالأمر التالي: python --version وانظر إلى الخرج فلو تبين لك أن الإصدار الموجود هو 2، أو أن بايثون غير مثبت على الإطلاق، حاول كتابة الأمر بالشكل التالي: python3 --version وإن حصلت على رسالة مفادها عدم التعرف على الأمر، فثبت بايثون 3 باستخدام مركز التطبيقات أو مدير الحزم الخاص بتوزيعة لينكس التي تعتمدها مثل apt في أوبونتو أو dnf في فيدورا. إن كانت توزيعتك هي فيدورا، ثبت بايثون 3 بالأمر التالي: sudo dnf install python3 اتبع الخطوات نفسها في نظام تشغيل macOS لتتأكد من وجود بايثون 3 في النظام، وإن لم يكن مثبتًا، حمله من موقع بايثون الرسمي ثم ثبته، وهنا لديك هذه الطريقة فقط إذ إن macOS لا يملك مدير حزم. أما لو كنت تستخدم نظام تشغيل ويندوز فحمل بايثون 3 من الموقع الرسمي في البداية، ويمكنك الاستعانة بالمقال كيفية تثبيت بايثون 3 وإعداد بيئته البرمجية على ويندوز 10 لهذا الغرض. عمومًا أيًا كان نظام التشغيل الذي تعتمده يمكنك الاستعانة بقسم تثبيت بايثون 3 وإعداد بيئتها البرمجية من المرجع الشامل لتعلم بايثون المتوفر باللغة العربية على أكاديمية حسوب. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تشغيل بيئة التطوير المتكاملة IDE فعليًا كل ما تحتاج إليه لكتابة البرامج بلغة بايثون هو محرر شيفرات، ولكن استخدامك بيئة تطوير متكاملة IDE سيسهل عليك كتابة الشيفرة كثيرًا، فهذه البيئات تتضمن وظيفة محرر الشيفرات البسيط مع بعض الأدوات الإضافية الملائمة لكتابة شيفرة بايثون، ويتوفر العديد من الخيارات مفتوحة المصدر لبيئات التطوير المتكاملة، سنعرض منها IDLE 3 و PyCharm. بيئة التطوير IDLE 3 الأساسية هي بيئة التطوير الأساسية المضمنة مع بايثون، وتتميز هذه البيئة بتلوين أجزاء الشيفرة ما يسهل قراءتها واكتشاف الأخطاء على المبرمج، وأيضًا بإعطاء تلميحات لإكمال التعليمات البرمجية، بالإضافة إلى وجود زر تشغيل لتسهيل تشغيلها واختبارها. لبدء استخدام هذه البيئة في لينكس ونظام macOS يكفيك كتابة الأمر idle3 في نافذة الطرفية، أما لاستخدامها في ويندوز فابحث عن بايثون 3 في قائمة ابدأ وستجد هذه البيئة بين خياراتها، وفي حال لم تجد بايثون في قائمة ابدأ، افتح سطر الأوامر بكتابة cmd في خانة البحث، ومن ثم اكتب ضمنه السطر التالي: C:\Windows\py.exe وإن لم ينفع، أعد تثبيت بايثون وتأكد من تفعيل خيار إضافة بايثون إلى متغير البيئة PATH في أثناء عملية التثبيت. وإن لم ينفع ذلك أيضًا، استخدم لينكس ضمن ويندوز نفسه أو بتثبيت لينكس مع ويندوز، فهو في النهاية مجاني، وهذا الانتقال في البيئة لن يكلفك أكثر من حفظ ملفات بايثون خاصتك على قرص USB محمول ونقلها. بيئة التطوير Pycharm النسخة المجتمعية تعدّ Pycharm من أفضل بيئات التطوير مفتوحة المصدر المتوافقة مع بايثون، تتميز بتلوين الشيفرات لتسهيل قراءتها ولاكتشاف أخطاء الكتابة، وإكمال الأقواس وعلامات الاقتباس لضمان صيغ قواعدية سليمة، بالإضافة إلى ميزة ترقيم الأسطر المفيدة في أثناء تنقيح الأخطاء، وعلامات المسافة البادئة، وزر التشغيل لتسهيل اختبار الكود. لتستخدم هذه البيئة ثبّت النسخة المجتمعية من Pycharm، إن كان نظام تشغيلك هو لينكس فيمكنك تثبيتها باستخدام flatpak من منصة FlatHub أو تحميلها من الموقع الرسمي وتثبيتها يدويًا، أما لو كنت تعتمد ويندوز أو macOS فحمل نسخة البرنامج المناسبة لك من موقعه الرسمي، ومن ثم ثبتها. افتح البرنامج بعد التثبيت وأنشئ مشروعًا جديدًا. أخبر بايثون بما تريد تنفيذه الكلمات المفتاحية هي كلمات أساسية تُعرّف الدوال والأصناف وغيرها فهي إذًا الوسيلة التي تخبر بايثون بكل ما تريد تنفيذه. لتجرب إحداها، اكتب السطر التالي ضمن مشروعك الجديد الذي أنشأته على بيئة التطوير: print("Hello world.") والآن شغله بإحدى الطريقتين: إن كنت تستخدم IDLE استعرض قائمة التشغيل وحدد الخيار "تشغيل الوحدة Run Module". أما في PyCharm انقر فوق الزر "تشغيل ملف Run file" ضمن أزرار شريط الأدوات. تطبع print -الكلمة المفتاحية التي جربناها- النص المكتوب بين علامتي اقتباس الموجود ضمن القوسين كما هو أيًا كان. وننوه لك أن بايثون افتراضيًا لا تستخدم إلّا الكلمات المفتاحية الأساسية مثل print و help وتلك الخاصة بالعمليات الحسابية الأساسية وما شابه، ولكن التعليمة import تتيح للمبرمج استيراد المزيد من الوظائف واستخدامها. استخدام وحدة الرسم turtle في بايثون اكتب الأسطر التالية في ملفك (بعد حذف الموجود فيه)، ومن ثم شغله: import turtle turtle.begin_fill() turtle.forward(100) turtle.left(90) turtle.forward(100) turtle.left(90) turtle.forward(100) turtle.left(90) turtle.forward(100) turtle.end_fill() لاحظ الأشكال التي يمكنك رسمها باستخدام مكتبة الرسم turtle والتي تدعى سلحفاة. لتمسح كل الموجود في مساحة الرسم، استخدم: turtle.clear() حاول تخمين معنى الدالة من اسمها الأجنبي: turtle.color("blue") لابد أنك أصبت، سيصبح لون الخط أزرق بدل الأسود، فسهولة فهم التعليمات وتفسيرها هي إحدى ميزات بايثون. سنحاول الحصول على النتيجة نفسها بكتابة كود مختلف أكثر تعقيدًا، وذلك باستخدام الحلقة while التي ستوجه بايثون ليكرر عملية رسم الخط والانعطاف أربع مرات متتالية عوضًا عن إعادة كتابة السطر نفسه أربع مرات كما فعلنا في الفقرة السابقة، أما المتغير الذي سيحدد عدد مرات التكرار فيسمى العداد counter، ستتعلم المزيد عن المتغيرات لاحقًا، ولكن الآن اكتب الأسطر التالية ونفذها لترى كيفية تفاعل المتغير مع الحلقة: import turtle as t import time t.color("blue") t.begin_fill() counter=0 while counter < 4: t.forward(100) t.left(90) counter = counter+1 t.end_fill() time.sleep(2) تعلم بايثون عبر بناء لعبة بسيطة هدفنا أساسًا تعلم البرمجة عبر لغة بايثون، وتمهيدًا لاستخدامها في برمجة متقدمة تعتمد على الرسوميات، سنبدأ في هذا المقال بتعلم منطق اللعبة وكيفية تنظيم البيئة لبناء لعبة نرد بسيطة، يلعب فيها المستخدم مقابل الحاسوب ويلقي كل منهما النرد الافتراضي ليفوز صاحب القيمة الأعلى. التخطيط للعبة يبدأ تطوير البرمجيات عادةً بكتابة توثيق -ولو كان بسيطًا- للمتطلبات والأهداف يبين ما يفترض أن يكون عليه البرنامج، وفي لعبة النرد التي نعمل عليها سيتضمن التوثيق آلية العمل التالية. ابدأ اللعبة واضغط على زر Return أو Enter في لوحة المفاتيح لتبدأ دورة النرد. ستظهر النتيجة على الشاشة. ستُطالب بعدها بإعادة اللعب أو بالخروج. اللعبة بسيطة هذا صحيح، لكن توثيقها سيبين لك كمًّا لا بأس به من التفاصيل لتعمل عليها، مثل حاجتك لهذه المكونات. اللاعب: وهو العنصر البشري الذي سيستخدم اللعبة. AI: وترمز للذكاء الصنعي أي جهاز الحاسوب الذي يحاكي دور اللاعب الخصم، حتى يكون لديك فائز وخاسر. رقم عشوائي: الشائع في لعبة النرد استخدام نرد سداسي الأضلاع وبالتالي الرقم سيكون بين 1 و 6. عامل إجرائي: عملية رياضية بسيطة لموازنة رقمي اللاعبين ومعرفة الأكبر بينهما. رسالة فوز أو خسارة. مطالبة باتخاذ إجراء: إما اللعب مجددًا أو الخروج. بناء الإصدار الأول من لعبة النرد يتضمن الإصدار الأول من البرنامج الميزات الأساسية فقط، قلة هم من يبدأون تطوير برامجهم بميزات كاملة من أول إصدار. سنعرض مفهومين أساسيين قبل البدء. أولًا المتغير variable هو قيمة قابلة للتغيير، يستخدم لتخزين بعض المعلومات التي يحتاجها البرنامج وهو شائع الاستخدام في بايثون، فمثلًا المحرف x هو متغير في المعادلة التالية لأنه يشير إلى قيمةٍ ما أو ينوب عنها: x + 5 = 20 وثانيًا العدد الصحيح integer فيشمل الأعداد الصحيحة الموجبة والسالبة، مثلًا: 1، ‎-1 ،14 ،10947. يتضمن إصدارنا الأول من اللعبة -والمسمى ألفا dice-alpha- متغيرين هما player و ai. لتبدأ بإنشائه، افتح مشروعًا جديدًا، وسمّه dice-alpha ثم اكتب ضمنه الكود التالي: import random player = random.randint(1,6) ai = random.randint(1,6) if player > ai : print("You win") # لاحظ المسافة البادئة else: print("You lose") شغل اللعبة، ولاحظ أن وظيفتها الأساسية تعمل جيدًا، لكنها لا تبدو لعبةً متكاملة حتى الآن، فاللاعب لا يعرف نتيجة رميته ولا رمية الخصم، بالإضافة إلى أنها تنتهي مباشرةً حتى لو رغب اللاعب بالاستمرار، في الواقع يعدّ هذا شائعًا في الإصدارات الأولى من البرامج التي تسمى الإصدارات ألفا، لكنك في نهاية المطاف حللت الإجراء المهم في اللعبة وهو رمي النرد وأصبحت متيقنًا من عمله، فيمكنك استكمال بقية الميزات فيما بعد. تحسين اللعبة سنبني الآن الإصدار الثاني وهو الإصدار بيتا، بإضافة بعض الميزات ليصبح برنامجنا أقرب للعبة فعلًا. 1. إظهار تفاصيل النتيجة في إصدارنا الأول أظهرنا للاعب عبارةً واحدة ليعرف إن فاز أو خسر، والآن سنطوّر الآلية ليعرف اللاعب مقدار العدد العشوائي الذي حصل عليه أي مقدار رميته للنرد وكذلك رمية الخصم. جرب إجراء التعديل التالي في برنامجك، ومن ثم شغله: player = random.randint(1,6) print("You rolled " + player) ai = random.randint(1,6) print("The computer rolled " + ai) بمجرد تشغيله ستحصل على خطأ، والسبب أن بايثون قد فهم برنامجك على أنه عملية حسابية تجمع حروف وأعداد، الحروف هي حروف العبارة التي ستُظهر النتيجة على الشاشة والعدد هو قيمة المتغير player أو المتغير ai، وهذا بالطبع غير منطقي وغير قابل للتطبيق. لتصحيح الخطأ سنجعل بايثون يتعامل مع قيمة المتغير بصفتها نصًا أو بالأحرى سلسلة نصية string بدلًا من معاملتها على أساس أنها عدد صحيح integer. نفذ إذًا التعديل التالي وأعد تشغيل اللعبة ولاحظ النتائج: player = random.randint(1,6) print("You rolled " + str(player) ) ai = random.randint(1,6) print("The computer rolled " + str(ai) ) 2. إضافة عامل التشويق أضفنا بعض التسلية للعبة بإظهار النتائج، والآن سنضيف بعض الإثارة بإبطاء اللعبة في الأجزاء المشوقة، أما وسيلتنا لذلك فهي الدالة time إحدى دوال بايثون. نفذ التالي وتفقد النتائج: import random import time player = random.randint(1,6) print("You rolled " + str(player) ) ai = random.randint(1,6) print("The computer rolls...." ) time.sleep(2) print("The computer has rolled a " + str(player) ) if player > ai : print("You win") # لاحظ المسافة البادئة else: print("You lose") 3. اكتشاف ثغرة التعادل لو ثابرت على اللعب لفترةٍ جيدة، ستكتشف أن لعبتك لا تمتلك التعليمات اللازمة للتعامل مع حالة التعادل أي الحالة التي يحصل فيها اللاعب والحاسوب على القيمة نفسها، وهذه تعدّ ثغرة أو خطأ منطقي في الكود المكتوب، وحتى نعالجها سنجعل بايثون يتحقق من كون القيمتين متساويتين ويعرض عندها نتيجة التعادل. سنستخدم إشارة يساوي مزدوجة == للتحقق من تساوي القيمتين، إذ إن إشارة يساوي مفردة تعني في بايثون إسناد قيمة ما لأحد المتغيرات، أما بخصوص إدراج خيار ثالث للنتيجة، فسنعتمد على الكلمة المفتاحية elif (اختصار لـ else if) وهي مفيدة في الحالات التي تحتاج فيها للمطابقة مع ثلاثة احتمالات أو أكثر ضمن التعليمات الشرطية، بدلاً من مجرد احتمالين أحدهما صحيح والآخر خاطئ. الآن عدّل برنامجك ليغدو كما يلي: if player > ai : print("You win") # لاحظ المسافة البادئة elif player == ai: print("Tie game.") else: print("You lose") وبعدها جرب اللعبة عدة مرات لتصادف حالة التعادل وتتأكد من عملها. يمكنك تعلم المزيد عن الدوال الشرطية في بايثون بالاطلاع على المقال كيفية كتابة التعليمات الشرطية في بايثون 3 ضمن دليل تعلم بايثون على أكاديمية حسوب. بناء الإصدار النهائي للعبة تفوق الإصدار بيتا وظيفيًا على الإصدار ألفا، وكان أكثر قربًا منه للألعاب الحقيقية، والآن سنبني إصدارنا النهائي الأكثر تكاملًا وستتعلم خلاله بناء دالتك الأولى في بايثون. الدالة function هي مجموعة من التعليمات البرمجية التي يتم استدعائها ضمن البرنامج بصورة كتلة واحدة متميزة، تفيد الدوال في تنظيم التطبيقات بالأخص تلك التي تتضمن عددًا كبيرًا من التعليمات البرمجية التي لا يُفترض أن تعمل في وقتٍ واحد وشروطٍ واحدة، والدوال هنا هي من تحدد ما الذي سيحدث وفي أي وقت وشرط سيحدث. عدّل الآن برنامجك ليصبح: import random import time def dice(): player = random.randint(1,6) print("You rolled " + str(player) ) ai = random.randint(1,6) print("The computer rolls...." ) time.sleep(2) print("The computer has rolled a " + str(ai) ) if player > ai : print("You win") elif player == ai: print("Tie game.") else: print("You lose") print("Quit? Y/N") continue = input() if continue == "Y" or continue == "y": exit() elif continue == "N" or continue == "n": pass else: print("I did not understand that. Playing again.") في هذا الإصدار سيُسأل اللاعب إن كان يريد الخروج من اللعبة، وفي حال أجاب بنعم عبر كتابة Y أو y، فإن بايثون سينهي استدعاء الدالة وتتم مغادرة اللعبة. إذًا فقد بنيت دالتك الأولى المسماة نرد dice لكنها لن تعمل ما لم تستدعيها ضمن البرنامج، وهذا ما ستفعله الحلقة While True. أضف التعليمات التالية في نهاية برنامجك، وانتبه للمسافات البادئة فقد أعدنا كتابة آخر سطرين في الكود السابق عن قصد ليكون السياق واضحًا أمامك: … # الحلقة الأساسية while True: print("Press return to roll your die.") roll = input() dice() هذه الحلقة هي أول ما سيعمل في برنامجك، وتتكرر دومًا ما لم يكسرها أمرٌ من بايثون، إذ إن شرط عملها محقق دائمًا وتلقائيًا فهو True، لاحظ تسلسل عملها فهي تطالب المستخدم أولًا باتخاذ إجراء حتى يبدأ اللعب، ومن ثم تستدعي الدالة dice، ومن بعدها بناءً على إجابة المستخدم ستستمر الحلقة بالعمل أو تتوقف. ومن الجدير بالذكر أن الحلقات هي واحدة من أشيع الطرق المستخدمة لبدء التطبيقات فهي تضمن بقاء التطبيق متاحًا أمام المستخدم لفترة كافية تمكنه من استخدام وظائفه المختلفة. يمكنك تعلم المزيد عن الحلقات التكرارية بالاطلاع على المقال كيفية إنشاء حلقات تكرار while في بايثون 3، والمقال كيفية استخدام تعابير break و continue و pass عند التعامل مع حلقات التكرار في بايثون 3 ضمن دليل تعلم بايثون على أكاديمية حسوب. ختامًا تعرفنا في هذا المقال على أساسيات البرمجة بلغة بايثون، وسنتعلم في مقالنا التالي كيفية بناء لعبة فيديو باستخدام الوحدة PyGame المتخصصة في هذا المجال. ترجمة -وبتصرف- للمقال Learn how to program in Python by building a simple dice game لصاحبه Seth Kenlon. اقرأ أيضًا المقال التالي: بناء لعبة رسومية باستخدام بايثون ووحدة الألعاب Pygame تعرف على أشهر لغات برمجة الألعاب برمجة لعبة حجرة ورقة مقص باستخدام لغة بايثون النسخة العربية الكاملة من كتاب البرمجة بلغة بايثون
  24. بدأت رحلتنا في التعرف إلى زوليب Zulip عندما قررت الشركة تطوير منصتها الخاصة بالدردشة الفورية والانتقال لمنصة أخرى تجمع في صفاتها سهولة الاستخدام و البرمجيات مفتوحة المصدر، وهذه المحددات ومميزات عديدة أخرى قادتنا إلى زوليب وهو ما سنعرضه في هذا المقال. كيف توصلت شركتنا إلى ضرورة تطوير حلها للدردشة شركة Backdrop CMS هي شركة متخصصة بأنظمة إدارة المحتوى وهي جزء من مشروع دروبال التي تُعنى بتطوير المواقع الإلكترونية للشركات الصغيرة والمتوسطة الحجم والمؤسسات غير الربحية أي للفئات ذات الميزانيات المنخفضة. في البدايات خلال السنوات الخمس الأولى لعمل الشركة استخدمنا Gitter كأداة للدردشة وكانت تفي بالغرض فهي: مفتوحة المصدر. سهلة الاستخدام. تدعم تسجيل الدخول باستخدام حسابات GitLab و GitHub. يمكن رؤية محادثاتها دون شرط امتلاك حساب على المنصة. بمرور الوقت وتطور عمل الشركة وبوجود أدوات دردشة متخصصة مثل سلاك Slack بدأنا نشعر أن غيتّر Gitter قاصرة عن تلبية احتياجاتنا فهي على سبيل المثال لا توفر قنوات لتنظيم المحادثات كما أن تطبيق الهاتف الجوال الخاص بها أدنى من المستوى المطلوب ومن هنا برزت الحاجة للبحث عن بديل. وبالنظر لمحدودية مواردنا المالية والتضييق الذي تمارسه سلاك على الحسابات المجانية وهي بالمناسبة المنافس القوي الذي يستخدمه معظمنا خارج الشركة، كان التحدي الأكبر لنا العثور على منصة مفتوحة المصدر وسهلة الاستخدام لغير التقنيين وقادرة نوعًا ما على منافسة سلاك. لماذا اخترنا زوليب Zulip بحثنا في المنصات مفتوحة المصدر عن كثب لإيجاد بديل معقول لسلاك وزوليب كان الخيار الأنسب، حيث أن الاستضافة المجانية التي تمنحها منصة زوليب من باب الدعم " لبعض المنظمات الجديرة بذلك" كانت واحدة من أهم العوامل التي دفعتنا باتجاه اختيار التطبيق في ظل ميزانيتنا المحدودة وصعوبة تحملنا للأعباء الناجمة عن صيانة وإدارة خادم خاص بالتطبيق. يتمتع زوليب بميزة مبتكرة تسمح للمستخدم بإنشاء مواضيع ووسم المحادثات الواردة ضمن مجرى التدفق بها وهذه الآلية في تصنيف المحادثات شبيهة إلى حد ما بقنوات الدردشة الموجودة في التطبيقات المنافسة، وبذلك يعطيك زوليب الإمكانية لعرض المحادثات الواردة وفق تسلسلها الزمني في مجرى التدفق أي كما هي وهذا ما اعتدناه في غيتّر، أو تصفيتها تبعًا للمواضيع التي تنتمي إليها وعرض موضوع بعينه. في بداية تشغيل التطبيق تنوعت انطباعات المستخدمين عن هذه الميزة فالبعض عدّها نقطة قوة وأحب استخدامها بينما شكلت تعقيدًا للبعض الآخر وبالأخص للمستخدمين الجدد فالتآلف مع هذه الواجهة واعتيادها يتطلب وقتًا. ولكونها ميزة اختيارية فالمواضيع في حينه لم تأخذ طابعًا رسميًا أما اليوم وبعد مرور زمن على استخدام التطبيق أصبحت مواضيع زوليب أداة تنظيمية مهمة وأهميتها تزداد يومًا بعد يوم دون أن ننكر استمرار استهجانها من قبل المستخدمين الجدد وحتى البعض من ذوي الخبرة. أما بالنسبة لتطبيقات الجوال الخاصة بزوليب فهي جيدة ومستقرة الأداء كما أنها تدعم أنظمة أندرويد أو iOS. تطور استخدام زوليب في الشركة انتقلت محادثات الدعم من المنتدى العام Public online forum إلى قناة الدردشة في زوليب مما أثار قلقنا حول بعض النقاط منها كون المحادثات على زوليب غير مصنفة كما هي في المنتدى وغير مرئية للأشخاص الذين لا يملكون حسابات على المنصة وبالتالي لن يتمكنوا من الاستفادة منها. وسنعرض في هذا الإطار بعض الإحصاءات كمؤشر عن حجم الإقبال على زوليب، إذ يوجد لدينا حاليًا نحو 240 حسابًا على المنصة، ويردنا كمعدل وسطي حوالي 75 رسالة في اليوم من 13 زائر مختلف أما معدل سرعة الرد فيتراوح بين ساعة إلى ست ساعات كحد أقصى. في الواقع تعدد قنوات الاتصال بشكل كبير دون وجود كثافة حقيقية في أي منها من الممكن أن يكون عائقًا أمام نجاح أي مجتمع على الإنترنت. ما زلنا نسعى لإبقاء معظم المحادثات في مجرى التدفق دون تصنيفها كمواضيع مع بعض الاستثناءات فعلى سبيل المثال لدينا موضوع مخصص للرسائل الواردة باللغة الألمانية ومواضيع خاصة بالحوادث والبنية التحتية أو تلك العائدة للإدارة العليا أو المواضيع الأمنية وقد يضاف لها مستقبلًا مواضيع أخرى. وفي الختام تختلف ردود أفعال المستخدمين حول زوليب لكن بالنسبة لتجربتنا فنحن نوصي به لأنه أولًا وأخيرًا مشروع مفتوح المصدر. ترجمة -وبتصرف- للمقال How our community uses Zulip for its open source chat tool لصاحبه Tim Erickson. اقرأ أيضًا مدخل إلى منصة مايكروسوفت تيمز Microsoft Teams كيف تزيد الدردشة المباشرة من معدلات التحويل
  25. تخزين الصور وملفات الفيديو باستخدام صور Google هي واحدة من أكثر طرق التخزين شيوعًا، وكونها الخيار الافتراضي لهواتف أندرويد فهذا يعزز من انتشارها، ولكن في حال رغبت بالتخلي عن هذه الطريقة أو بالأحرى التخلي عن استخدام كافة المنصات مغلقة المصدر المشابهة والانتقال لمنصات إدارة الصور مفتوحة المصدر الأكثر أمانًا، فتطبيق بيويجو Piwigo يعد خيارًا مناسبًا إذ هو مفتوح المصدر مع إمكانية استضافته ذاتيًا وهو ما سنتعرف عليه أكثر في هذا المقال. بيويجو مكتبة الصور مفتوحة المصدر ذات الاستضافة الذاتية بيويجو الحل مفتوح المصدر لإدارة الصور والفيديو. يدعم التطبيق نمطين من أنماط الاستضافة ذاتية وسحابية، فتُخزَّن البيانات في النمط الأول محليًا على حاسب المستخدم ويكون المسؤول الوحيد عن سلامتها وتوفير النسخ الاحتياطي لها، أما في حال استخدام النمط السحابي فستُخزّن كافة البيانات سحابيًا على مخدمات الشركة ومقرها فرنسا وستوفر الشركة عندها خدمة النسخ الاحتياطي كإحدى ميزات الاستضافة. ميزات بيويجو يضمن بيويجو خصوصية المستخدم فضلًا عن طريقة تصميمه المناسبة للأعمال الفردية والجماعية، ويتمتع بمجموعة واسعة من ميزات التحكم بالصور وإدارتها إدارة متخصصة نستعرض أهمها: توفير استضافة واسم نطاق فرعي خاص بكل مستخدم تحت النطاق الرئيس piwigo.com إمكانية تحميل الملفات دفعة واحدة. إنشاء ألبومات الصور. تحديد الصور وتعيينها ضمن ألبومات ومجموعات. مشاركة الصور باستخدام الروابط الخاصة بها. إدارة التحكم بالوصول للملفات بنمطين عام وخاص. القدرة على تنظيم المستخدمين ضمن مجموعات عمل لإدارة الصور والألبومات وهذه الميزة مفيدة للشركات وفرق العمل. التحليلات الأساسية اللازمة لتتبع استخدامية التطبيق ومعدل التخزين المستخدم. إمكانية إضافة الوسوم للصور والألبومات. يدعم العمل بالنمط الليلي. إمكانية التعديل على البيانات الوصفية للصور. محددات تصفية لسهولة البحث عن الصور. يدعم لواحق الصور JPG و JPEG و PNG و GIF فقط بالنسبة لاستضافة الأفراد، بينما يدعم كافة كافة أنواع الملفات في استضافة المنظمات. مساحة تخزين غير محدودة في استضافة الأفراد. يسمح باستخدام أسماء نطاقات مخصصة (حتى في حال اختيار الاستضافة السحابية). يتضمن مجموعة من الإضافات لتوسيع الوظائف. يدعم وجود نمط أو سمة عامة للاستخدام. قابل للعمل مع الهاتف المحمول بنظام Android أو iOS. علاوة على كل الميزات المذكورة يمتلك بيويجو خيارات إضافية أخرى قادرة على رفع إمكانيات إدارة الملفات وعلى تحسين كافة جوانب تجربة المستخدم. وللحصول على نظرة أشمل قمت باستخدام التطبيق باستضافة سحابية تجريبية مدتها 30 يوم مخصصة للأفراد وسأشارك بعضًا من آرائي حول التجربة لتكونوا فكرة عنه قبل استخدامه. استخدام بيويجو لإدارة الصور بدايةً أنشئ حسابك على المنصة وسيُطلب منك اختيار اسم نطاق فرعي تحت النطاق الرئيس لبيويجو وهذا الاسم سيولد الرابط الخاص بك. ومن ثم عليك تقييد صلاحيات الوصول لتكون محصورة بك فقط أو بمن تريد من المستخدمين فالمشاركة العامة للملفات تعني أن كل من يملك الرابط الخاص بك يستطيع الوصول إليها. كما يمكنك التوجه للوحة التحكم الخاصة بالتطبيق لتفقد نسبة استخدام التخزين ونشاطاتك بشكل عام. يوفر التطبيق مجموعة من الإضافات اثنتين منها مفعلة بشكل تلقائي الأولى خاصة بالوظائف الداخلية والثانية بهدف التصدي لمرسلي البريد العشوائي Spam. أما الإضافات الأخرى فهي تتطلب تفعيل يدوي ونذكر منها الإضافة الخاصة بتحسين الإدارة الدفعية أو بتمكين رسائل إدارة النظام أو ضبط عمليات التحميل وكذلك الإضافات المسؤولة عن تفعيل التعليقات على ألبومات الصور أو تحديد مدة صلاحية عرض هذه الألبومات. في الواقع إن تجربة المستخدم لهذا التطبيق جيدة جدًا رغم أنها قد لا تقدم الشكل العصري الأفضل لواجهة المستخدم لكنها سهلة الإدارة وتتمتع بالعديد من المزايا والإضافات التي تجعلها جديرة بالاستكشاف، لذا امنحها بعض الوقت واستعرض هذه الميزات مع الإضافات المتاحة لتتبين فوائدها وتوازنها مع صور Google وغيرها من منصات استضافة الصور فمن المؤكد أنك لن تحظى بإدارة مماثلة لصورك. * أما بالنسبة لبقية الميزات الموجودة فنذكر منها تمكنك من إنشاء وإدارة عدة حسابات للمستخدمين والتحكم بصلاحيات الوصول وإرسال الإشعارات (عبر البريد الالكتروني) بالإضافة إلى بعض عمليات الصيانة. ابدأ باستخدام بيويجو في الواقع التطبيق خيار جيد ويلبي كافة الاحتياجات بدءًا من المستخدم الراغب بتخزين وتنظيم الصور إلى المستخدمين المحتاجين لمنصة مشتركة لعرض وتخزين وإدارة الصور الخاصة بعملهم. إن اخترت الاستضافة الذاتية للتطبيق فاسترشد بالتوثيق الخاص به وبمنصة GitHub دون أن تنسى الاهتمام بالنسخ الاحتياطي لبياناتك فأنت المسؤول كليًا عنها في هذا النوع من الاستضافة. أما في حال فضلت الاستضافة السحابية المدفوعة فلديك نوعين استضافة للأفراد مع مساحة تخزينية غير محدودة لكنها تسمح برفع الصور فقط، واستضافة المنظمات وفيها تتناسب سعات التخزين المتاحة مع الأسعار إلاّ أنها بالمقابل تدعم رفع كافة أنواع الملفات، إذ تفترض مساحات التخزين غير المحدودة في استضافة الأفراد أن التطبيق لن يواجه أي مشكلة إلا في حال إساءة الاستخدام. قد يختار الجزء الأكبر من المستخدمين الاستضافة السحابية ضمن هذه المنصة رغم تكلفتها المرتفعة على حساب خدمات أخرى كصور Google ومثيلاتها وذلك نظرًا لما توفره ميزات تحكم وإدارة بالإضافة إلى دعمها رفع وتخزين الملفات بمختلف الصيغ وضمان خصوصية المستخدم. ترجمة -وبتصرف- للمقال Piwigo: An Open-Source Google Photos Alternative That You Can Self-Host لصاحبه Ankush Das. اقرأ أيضًا المرجع الشامل لاختيار الصور المناسبة لأغراض التسويق 10 أدوات وتطبيقات مجانية تسهل عليك تحسين الصور 50 تصميمًا مميزًا لمواقع إلكترونية نموذجية يحتذى بها
×
×
  • أضف...