-
المساهمات
163 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو ابراهيم الخضور
-
لنفترض أن لدينا شخصيةً ثلاثية الأبعاد من نوع صلب Rigged تم بناؤها ذاتيًا أو حملناها من مصدر خارجي ونريد إعداد وضبط حركاتها في جودو. سنستكشف في هذا المقال طريقة فعل ذلك، وسنستخدم نفس شخصية المغامر وبقية الأصول التي ذكرناها في المقال السابق من سلسلة دليل جودو. إعداد الشخصية اخترنا العقدة CharacterBody3D لتمثّل شخصية المغامر، لهذا ينبغي أن يكون المشهد كالتالي: ملاحظة: أغلقنا فرع العقدة Rig كي لا تطول القائمة. أول ما قد نلاحظه أن يدي المغامر ممتلئتين، فقد زُوّد الفنان المغامر بكل الأسلحة والدروع ووضعها في مكانها واتجاهها الصحيح. بإمكاننا طبعًا التنقل في قائمة العقد وإخفاء ما نريده. استخدام العقدة AnimationTree من الصعب التعامل مع كل الحركات المتوفرة للشخصية من خلال الشيفرة. لذا من المهم التفكير مثلًا بعدد العبارات الشرطية if التي نحتاجها كي نحدد الحركة المناسبة التي يجب تنفيذها في التوقيت المطلوب، وذلك اعتمادًا على ما يفعله اللاعب. لن يكون الأمر صعبًا بالطبع عند وجود عدة حركات فقط، لكن سرعان ما ستخرج الأمور عن السيطرة، ولن تكون هناك جدوى فعلية منها. علينا التفكير أيضًا في الحالة التي يقف فيها اللاعب دون حراك؛ إذ لا بد عندها من تنفيذ حركة الوقوف دون فعل شيء Idle. وعندما يضغط اللاعب مفتاح الحركة إلى الأمام، ستنفذ الشخصية حركة المشي Walking. ستبدو هذه التغيرات السريعة في الحركة مزعجة، ويُفضّل أن تكون هذه الحركات المتعاقبة متمازجةً ببعضها لتعطي انتقالًا أكثر سلاسة. ولحل مشاكل الحركة المعقدة تلك، يجب استخدام عقدة AnimationTree، وهي عقدة مصممة للتحكم بالعقدة AnimationPlayer وتقدم وظائفًا تتحكم بالانتقال بين الحركات المختلفة وتمزجها معًا. سنضيف العقدة AnimationTree إلى المشهد ، ثم نضبط الخاصية Tree Root في الفاحص Inspector على القيمة AnimationNodeStateMachine، ثم نلحق العقدة AnimationPlayer بهذه العقدة من خلال الخاصية Anim Player، ونفعّل بعدها الخاصية Active ملاحظة: قد نلاحظ أننا لن تتمكن من تشغيل الحركات التي تضمها العقدة AnimationPlayer عندما تكون الخاصية Active للعقدة AnimationTree فعّالة، لذا إن أردنا تعديل أي شيء أو اختبار الحركات، فعلينا إلغاء تفعيل هذه الخاصية مؤقتًا. حلقة الحركات التوقف Idle والمشي Walk والجري Run هنالك الكثير من الحركات التي سنجدها في هذه النماذج، لكننا سنركز في مثالنا على حلقة حركات التوقف Idle والمشي Walk والجري Run، مع القفز والهجوم؛ فإن أردنا إضافة حركات أخرى، سنكرر ببساطة ما سنفعله مع الحركة التي نريد. سنبحث عن الحركات Idle و Running_A و Walking_Backwards و Running_Strafe_Left / Running_Strafe_Right ضمن العقدة AnimationPlayer، وسنتأكد من أن هذه الحركات ستعمل على شكل حلقة. يمكننا اختبار ذلك من خلال تشغيل الحركة عبرالزر ▶، وفي حال كانت هناك أي حركة غير معدّة لتكرار نفسها، سنعيد استيراد الشخصية بعد ضبط الحركات على وضع التكرار. سنختار العقدة AnimationTree وسنرى أن هناك لوحة جديدة قد فُتحت أسفل العارض. وكمثال عن طريقة العمل، سننقر بالزر الأيمن للفأرة في أي نقطة فارغة من اللوحة، ونضغط على خيار أضف حركة Add Animation ثم نختار Idle. نكرر العملية ونضيف أيضًا الحركة 1H_Melee_Attack_Chop. سنضغط الآن على أيقونة ربط العقد Connect Nodes، ثم نرسم اتصالًا بين الزر start و الحركة Idle، وسرى كيف تُنفّذ الحركة Idle مباشرةً. نريد الآن الانتقال من التوقف إلى الهجوم ثم العودة إلى حركة التوقف بعد انتهاء حركة الهجوم. ولهذا، سنرسم اتصالين إضافيين من وإلى حركة الهجوم. لن يعمل ذلك كما هو مطلوب، لكنه سينتقل بين الحركتين بسرعة، لأن كلا الاتصالين ضُبطا للانتقال الفوري. لتغير شروط الانتقال، سننتقل إلى وضع الاختيار، وذلك بالنقر على الأيقونة المخصصة، ثم النقر على أحد الاتصالين، وستظهر في نافذة الفاحص خاصيات الاتصال. نحتاج إلى ضبط الخاصية Advanced>Mode على Enabled في الاتصال من الحركة Idle، ويعني ذلك أن هذا الانتقال سيُنفذ فقط عندما نقرر ذلك وليس آليًا. سنلاحظ بعد ذلك أن لون الأيقونة على خط الاتصال سيتغير. سنضبط الخاصية Switch للاتصال الثاني إلى الحركة Idle على AtEnd، وتبقى قيمة الخاصية Advanced>Mode على Auto. عند النقر الآن على الزر ▶ في عقدة الهجوم، ستعمل حركة الهجوم، ثم تعود إلى حركة التوقف عندما تنتهي. يوضح هذا المثال البسيط طريقة إعداد حركات مختلفة وضبط الانتقالات بين هذه الحركات. سنحتاج لاحقًا بطبيعة الحال إلى أكثر من ذلك، لهذا سنحذف العقد الموجودة في لوحة شجرة التحريك باستخدام أيقونة الحذف، ودعنا نحضّر فضاء المزج blendspace. فضاء المزج blendspace ننقر بالزر اليميني للفأرة على أي نقطة فارغة من لوحة شجرة الحركات ثم نختار BlendSpace2D، وننقر بعد ذلك على الأيقونة التي تظهر، مع تغيير اسمها إلى IWR. نضيف بعد ذلك اتصالًا من Start إلى IWR كي يبدأ فضاء المزج بالعمل تلقائيًا. ننقر بعد ذلك على أيقونة القلم في الفضاء IWR لتظهر لنا النافذة التالية: يمثل هذا الفضاء ثنائي البعد شعاع الحركة الأفقية للشخصية، فعندما تقف الشخصية ساكنة، تكون قيمة الشعاع (0,0). ننقر الآن على زر إنشاء نقاط Create Points، ثم ننقر على منتصف الشبكة لإضافة الحركة idle. نضيف الحركة Running_A في المنتصف والأعلى، والحركة Walking_Backwards في المنتصف والأسفل الحركة، ثم نضيف في النهايتين الأفقيتين الحركتين Running_Strafe_Left و Running_Strafe_Right. يمكننا الآن النقر على أيقونة تحديد موضع الخلط في الفراغ، ثم النقر ضمن الشبكة والسحب لرؤية الانتقال السلس بين الحركات الأربعة والتوقف. بعد الانتهاء من تجربة عملية المزج، سننقر على الجذر Root لتعود إلى لوحة شجرة الحركات. ضبط آلة الحالة State machine يمكن عدّ حلقة الحركات IWR على أنها قلب شجرة الحركات، حيث ستقضي الشخصية معظم الوقت في تنفيذ تلك الحركات، و تتفرع بقية الحركات عنها كما فعلنا قبل قليل مع حركة الهجوم. تعرض لقطة الشاشة التالية مجموعة من الحركات التي ترتبط بالحلقة IWR، ومن المهم هنا تذكر أن خاصيات الانتقال تُضبط بنفس الأسلوب الذي اتبعناه في المثال السابق. يمكننا أيضًا النقر على الحركة لتغيير اسمها، وذلك لأن بعض الأسماء طويلة؛ أما الحركة التي تختلف عن غيرها فهي حركة القفز، إذ تقسم هذه الحركة إلى ثلاثة أجزاء هي البداية Start والعودة land التي تُنفّذ عندما تبدأ الشخصية بالقفز وعندما تنهيه، بينما يمثل الجزء الثالث التوقف Idle حركة وصل تُنفّذ طالما أن الشخصية لا تزال في الجو، خاصةً إن انتقلت أثناء القفز مسافة طويلة مثلًا. سنضيف الآن حركات القفز الثلاث كالتالي: وبما أننا نريد الانتقال مباشرةً من الحلقة IWR إلى الحركة Jump_Idle عند السقوط عن حافة، فعند النقر على زر القفز ستُنفّذ الحركة Jump_Start. وإضافة إلى ذلك، أبقينا الانتقال من Jump_Start إلى Jump_Idle على القيمة Auto. لكن بدل تغيير الخاصية Advanced>Mode إلى Enabled، أضفنا شرطًا من خلال ضبط قيمة الخاصية Condition على jumping. وبشكل مشابه، نضع شرطًا على تنفيذ الانتقال بين Jump_Idle و Jump_Land هو grounded؛ وسنتمكن من ضبط هذه الشروط لاحقًا من خلال الشيفرة لتفعيل عملية الانتقال. قد نلاحظ أخيرًا إن أمعنا النظر أن الانتقال بين الحركتين Jump_Land و IWR لا تبدو سلسلة، لأن آخر إطار من الأولى لا يتوافق تمامًا مع أول إطار من الثانية. يمكن أن نتدارك هذا الأمر باختيار الانتقال بينهما وضبط قيمة الخاصية Xfade Time على 0.1 وسنرى نتيجةً مرضية. خلاصة ضبطنا في هذا المقال حركات شخصية ثلاثية الأبعاد وأصبحت جاهزةً للاستخدام؛ وباستخدام العقدة AnimationTree أصبح اختيار الانتقال بين الحركات أكثر سهولةً في شيفرة الحركة. ترجمة -وبتصرف- للمقال: Character Animation. اقرأ أيضًا المقال السابق: استيراد الأصول Assets ثلاثية الأبعاد في محرك الألعاب Godot إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو
-
نقدم في سلسلة المقالات التالية طرائق لاستيراد أصول الألعاب ثلاثية الألعاب والتعامل معها بما في ذلك النماذج Models والرسوم المتحركة Animations ومواد البناء Materials. سنستخدم في مثالنا الذي نبنيه لتوضيح الفكرة بعض الأصول المتاحة على موقع Kay Lousberg: حزمة شخصيات المغامرين Adventurers حزمة Dungeon عند استخدام حزم الأصول التي ذكرناها ببداية المقال، سنلاحظ أن هناك عدة نسخ من الأصول وبتنسيقات مختلفة مثل OBJ و FBX و GLTF، وأنه توجد ملفات إضافية مثل الأمثلة وملفات نقوش منفصلة في حال أردت تعديل أي شيء. وبالطبع لن نحتاج إلى كل ذلك، فالتنسيق GLTF هو الصيغة المفضلة للاستيراد في جودو. إذًا كل ما علينا فعله هو سحب المجلد gltf أو الملفات gltf. أو glb. وهي الصيغة الثنائية لنفس الملف، إلى مجلد مشروعك. سنختار المجلد gltf من حزمة Dungeon، والمجلد characters من حزمة Adventurers، ونسحبهما إلى مجلد مشروعنا ملاحظة: هنالك الكثير من الملفات في حزمة Dungeon وقد يستغرق جودو بعض الوقت في قراءتها. استيراد شخصية سنختار الملف Knight.glb من المجلد characters>gltf ضمن نافذة نظام الملفات FileSystem، ثم ننقر على نافذة استيراد Import في الأعلى إلى جوار نافذة المشهد Scene. سنجد ضمن النافذة بعض خيارات الاستيراد الأساسية، لكن بإمكاننا تفصيل العملية. سننقر على زر إعدادات متقدمة Advanced، وستظهر لنا نافذة جديدة. سنرى في اليمين (أو اليسار في النسخة الإنجليزية) البيانات التي يضمها مشهد GLTF بما في ذلك النقوش textures والحركات Animations؛ وسنلاحظ كيف اُلحقت كل الأسلحة بالشخصية، إضافةً إلى قائمة مطولة بالحركات التي يمكن للشخصية تنفيذها. تظهر الشخصية في وسط النافذة، إضافةً إلى مجموعة من الخيارات إلى اليسار (اليمين في النسخة الإنجليزية) التي تساعدنا على تعديل ضبط العنصر الذي نختاره من القائمة اليمينية. وطالما أننا سنكتب شيفرة اللاعب باعتباره عقدةً من النوع CharacterBody3D، فبإمكاننا تحديد نوع العقدة من خلال النقر على مشهد scene، ثم الانتقال إلى القائمة اليمينة والنقر إلى جوار الخاصية Root Type ثم اختيار العقدة CharacterBody3D من النافذة التي ظهرت. الحركات Animations سننتقل إلى الأسفل في نافذة الإعدادات المتقدمة نحو شجرة الحركات Animations، وسنجد الكثير منها. سنحتاج في مثالنا إلى تنفيذ بعض الحركات مرةً واحدةً فقط مثل الهجوم، في حين سننفذ بعضًا آخر في حلقات مثل المشي walk والتوقف دون فعل Idle والجري Running. ولتنفيذ أي حركة مستمرة بشكل حلقة، سنننقر عليها ثم نضبط الخاصية Loop Mode على Linear. سنكرر هذا الأمر على جميع حركات المشي Walking والجري Running والتوقف Idle المختلفة. وعند الانتهاء من ذلك سننقر على زر إعادة الاستيراد Reimport في أسفل النافذة. ملاحظة: إن كنا نصمم شخصياتنا بأنفسنا، نستطيع تجاوز عملية التعيين اليدوي لتكرار الحركة، والانتقال مباشرةً إلى تنفيذ الأمر تلقائيًا من خلال إضافة اللاحقة loop- إلى اسم الحركة. وللمزيد من التوضيح يمكن مراجعة توثيق جودو. سننقر الآن على الملف knight.glb بالزر اليمين للفأرة ونختار الأمر مشهد مورّث جديد New Inherited Scene. سنرى أن هناك عقدة جديدة ظهرت في نافذة المشهد تضم كل النماذج والحركات AnimationPlayer التي نستطيع تجريبها ضمن نافذة التحريك أسفل نافذة العرض. استيراد عناصر البيئة المحيطة يمكنننا استيراد عناصر البيئة المحيطة بنفس الأسلوب. لنستخدم كمثال عملية استيراد حائط من حزمة dungeon. ونظرًا لوجود الكثير من الملفات في الحزمة، سنكتب wall في مرشح البحث، لنجد المشهد المطلوب الآتي: نريد أن يكون الجدار صلبًا، وسيكون إنشاء جسم من النوع StaticBody3D بأنفسنا مضنيًا مع جميع أشكال التصادم collision shapes التي يجب تعيينها لكل عنصر من الجسم. لكن يستطيع جودو تنفيذ الأمر تلقائيًا عند استيراد العنصر. سننتقل إلى نافذة الإعدادات المتقدمة للمشهد المستورد، ثم نختار العقدة wall من النوع Mesh أيقونتها شبكة حمراء، ثم نفعّل الخيار فيزياء Physics وننتقل إلى الخاصية Physics>Shape Type ونضبطها على Simple Convex. يمثل هذا الخيار الشكل المحدب الذي يغلف الجدار الذي سيساعد في اكتشاف تصادم أي جسم مع الجدار. سننقر الآن على خيار إعادة الاستيراد Reimport، وهكذا سيُنشئ جودو عقدةً من النوع StaticBody3D عند استخدام هذا العنصر في اللعبة. ملاحظة: كما أشرنا سابقًا، هناك طريقة استيراد لتعيين أشكال التصادم تلقائيًا. لذا سنضيف في مشروع Blender اللاحقة col- مما يساعد البرنامج الذي يستورد الملف في تنفيذ العملية تلقائيًا. ويمكن مراجعة توثيق جودو. أتمتة الاستيراد على الرغم من أن إضافة تلميحات إلى أسماء الملفات المستوردة هي الطريقة المفضلة في أتمتة عملية الاستيراد، خاصةً عندما نصمم بأنفسنا الشخصيات والعناصر. لكنه أمر لا يمكن تنفيذه عند استيراد هذه الملفات من حزم جاهزة كالتي نستخدمها في مثالنا. بإمكاننا أيضًا كتابة سكريبت استيراد يُنفّذ على كل عقدة مستوردة من نوع معين، فنستطيع مثلًا أتمتة عملية إنشاء جسم التصادم التي شرحناها في الفقرة السابقة، حيث يتنقل السكريبت التالي خلال جميع عقد الكائن الذي استوردناه ويُنشئ جسم تصادم لكل عقدة من النوع MeshInstance3D @tool extends EditorScenePostImport func _post_import(scene): iterate(scene) return scene func iterate(node): if node != null: if node is MeshInstance3D: node.create_trimesh_collision() for child in node.get_children(): iterate(child) ننتقل في النافذة استيراد Import إلى الخاصية Import Script ثم نختار السكريبت السابق بعد حفظه في مكان مناسب. وهكذا سيُنشئ جودو أشكال تصادم لكل عقدة من النوع MeshInstance3D عند النقر على إعادة الاستيراد Reimport. خلاصة تعلمنا في هذا المقال طريقة استيراد أصول ثلاثية أبعاد جاهزة إلى محرك الألعاب جودو وضبط بعض الخيارات المهمة لعملية الاستيراد وكتابة سكريبت لأتمتتها. ترجمة -وبتصرف- للمقالين: Assets و Importing Assets اقرأ أيضًا المقال السابق: بدء وإنهاء الألعاب في محرك جودو Godot إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو
-
شرحنا في المقال السابق أهم الأسئلة النظرية التي قد تُطرح على المتقدمين لوظيفة مطور بايثون خلال مقابلة التوظيف، وسنوضح في هذا المقال أهم الأسئلة والتمارين العملية التي قد تُطلب منك فوريًا في مقابلات التوظيف بوظيفة مطور بايثون. قد تُطرح في المقابلات التقنية بعض الأسئلة التي تتطلب تحسين أو تصحيح شيفرة برمجية، سنذكر أهمها في ما يلي. التمرين الأول: تحسين الشيفرة قد يحتوي التمرين على أسئلة من نوع: اشرح وظيفة الدالة process_numbers في المثال التالي وما هو الخرج؟ ما هي العملية التي يمكن التخلي عنها دون أن يؤثر ذلك في الخرج؟ def process_numbers(numbers): unique_numbers = list(set(numbers)) unique_numbers.sort() unique_tuple = tuple(unique_numbers) total = sum(unique_tuple) average = total / len(unique_tuple) if unique_tuple else 0 return [num for num in unique_tuple if num > average] print(process_numbers([7,1,3,4,2,3,5,6,4])) وتكون الإجابة على النحو الآتي: الجواب 1: تقبل الدالة السابقة وسيطًا على شكل قائمة list مكونة من أعداد ثم تختار العناصر الفريدة منها وتوجِد متوسطها الحسابي، ثم تعيد قائمةً تضم الأعداد الفريدة الأكبر تمامًا من المتوسط. وسيكون الخرج [7, 6, 5] الجواب 2: عملية تحويل القائمة List إلى قائمة Tuple في السطر الرابع التمرين الثاني: استخدام مكتبات بايثون مقارنة بالتنفيذ اليدوي لخوارزمية قد يحتوي التمرين على أسئلة من نوع: ما وظيفة الشيفرة في هذا التمرين؟ هل يمكنك تنفيذ نفس الوظيفة باستخدام مكتبات جاهزة؟ اكتب شيفرةً تقارن بين زمني تنفيذ الطريقتين السابقتين، ما الذي تلاحظه مع الازدياد الكبير لحجم قائمة البيانات؟ import random random_data = set(random.sample(range(1, 10000), 1000)) def calculate_median(data): data = list(data) data.sort() n = len(data) mid = n // 2 if n % 2 == 1: return data[mid] else: return (data[mid - 1] + data[mid]) / 2 وتكون الإجابة كالآتي: الجواب الأول: تعطي الشيفرة السابقة طريقة حساب وسيط Median لعينة من الأرقام بطريقة يدوية الجواب الثاني: نعم، باستخدام المكتبة statistics أو scipy الجواب الثالث: باستخدام المكتبة time import random import time import statistics random_data = set(random.sample(range(1, 10000), 1000)) # تنفيذ يدوي للخوارزمية def calculate_median(data): data = list(data) data.sort() n = len(data) mid = n // 2 if n % 2 == 1: return data[mid] else: return (data[mid - 1] + data[mid]) / 2 # Statistics استخدام المكتبة def median_using_statistics(data): return statistics.median(data) # تقدير وقت التنفيذ start_manual = time.time() calculate_median(random_data) end_manual = time.time() start_Statistics = time.time() median_using_statistics(random_data) end_Statistics = time.time() # طباعة النتائج print("Manual Sort-Based Time:", end_manual - start_manual) print("Statistics-Based Time:", end_Statistics - start_Statistics) يمكن ملاحظة أنه مع ازدياد حجم البيانات، يصبح وقت التنفيذ أكبر بكثير في حال الطريقة اليدوية، لأن المكتبات تكون محققة باستخدام خوارزميات أسرع وأمثل وتعمل بطريقة أكثر كفاءة، مما يجعل وقت تنفيذها أقل أو أكثر استقرارًا مقارنةً بالأسلوب اليدوي. التمرين الثالث: استخدام المكتبة numpy قد يحتوي التمرين على أسئلة من نوع: أوجد الخطأ في الشيفرة التالية -لا بد من امتلاك خلفية باستخدام المكتبة numpy فسّر الخطأ صحح الخطأ، واكتب الشيفرة الصحيحة import numpy as np def transform_array(arr): arr[arr < 0] = np.nan # NaN استبدل القيم السالبة بقيمة من النوع mean_value = np.nanmean(arr) # NaN احسب المتوسط مهملًا القيم arr = arr / mean_value # سوّي عناصرالمصفوفة بالقسمة على المتوسط return arr data = np.array([3, -1, 7, -5, 10]) print(transform_array(data)) يكون الجواب على النحو الآتي: الجواب الأول: الخطأ في السطر الثالث الجواب الثاني: لا تدعم numpy تخزين القيم NaN ضمن المصفوفات الصحيحة الجواب الثالث: علينا تحويل عناصر المصفوفة صراحة إلى النوع float قبل استبدال الأعداد السالبة import numpy as np def transform_array(arr): arr = arr.astype(float) # float حوّل عناصر المصفوفة إلى النوع arr[arr < 0] = np.nan . . . data = np.array([3, -1, 7, -5, 10]) print(transform_array(data)) التمرين الرابع: استخدام المكتبة pandas قد يحتوي التمرين على أسئلة من نوع: أوجد الخطأ في الشيفرة التالية التي تستخدم المكتبة pandas فسّر الخطأ صحح الخطأ الموجود بالشيفرة، واكتب الشيفرة الصحيحة import pandas as pd data = {'A': [1, 2, 3, 4], 'B': [5, 6, 7, 8]} df = pd.DataFrame(data) df['C'] = df.A * 2 # C أنشئ عمود جديد filtered_df = df[df.C > 5] # أكبر من 5 C احذف الصف عندما تكون القيمة في filtered_df.reset_index() # أعد ضبط فهرس إطار البيانات print(filtered_df) ويكون الجواب على النحو الآتي: الجواب الأول: الخطأ في السطر السادس الجواب الثاني: لن تتغير الفهرسة بعد حذف الصفوف ولن تُطبق على filtered_df الجواب الثالث: علينا استدعاء التابع ()reset_index باستخدام inplace=true على النحو: filtered_df.reset_index(drop=True, inplace=True) التمرين الخامس: الاستيثاق والثغرات الأمنية تحاول الشيفرة التالية الاستيثاق باستخدام JWT، لكنها تعاني من ثغرة أمنية خطيرة والمطلوب: حدد الثغرة الأمنية في هذه الشيفرة اقترح حلًا لها import jwt import datetime SECRET_KEY = "supersecretkey" def generate_token(username): payload = { "user": username, "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30) } return jwt.encode(payload, SECRET_KEY, algorithm="HS256") def verify_token(token): try: decoded = jwt.decode(token, SECRET_KEY, algorithms="HS256") return decoded["user"] except Exception as e: return None وللإجابة عن هذه التساؤلات، سيكون الجواب الأول أن هذه الشيفرة معرضة لهجوم خلط الخوارزميات Algorithm Confusion Attack، نظرًا لإمكانية تعديل المخترق على ترويسة الطلب ببساطة ويلغي استخدام خوارزمية HS256 إلى none وبالتالي يتسلل دون استيثاق أما الحل السريع وهو جواب السؤال الثاني، فيتمثل في إجبار استخدام خوارزمية ضمن الدالة verify_token والتقاط الأخطاء الناتجة. فيما يلي الشيفرة المعدلة لهذه الدالة: def verify_token(token): try: # صرّح عن خوارزمية التشفير المتوقعة من المفتاح decoded = jwt.decode(token, SECRET_KEY, algorithms=["HS256"]) return decoded["user"] except jwt.ExpiredSignatureError: return "Token expired!" except jwt.InvalidTokenError: return "Invalid token!" نصائح للاستعداد لمقابلة العمل من أجل الاستعداد الجيد لمقابلة العمل كمطور بايثون، لا بد من التأكد من اعتماد النصائح الآتية: فهم ما نتعلمه من المهم استيعاب الغاية من كل قاعدة ومبدأ وتقنية وتدوين الملاحظات حول الاستخدام الأمثل الذي وجدناه لها. يحتاج أي مطور بايثون إلى سؤال نفسه: لماذا تقدم بايثون مثلًا عدة بنىً مختلفة لتجميع البيانات مثل list و tuple و range؟ وما الغاية من كل منها؟ وأين يكمن الاستخدام الأمثل لها؟ هذه التساؤلات، ستساعد كل متقدم للوظيفة على تهيئة نفسه للإجابة بدقة واختصار على الأسئلة المطروحة عليه. التحليل والمقارنة لا بد من التحليل والمقارنة سواء باستخدام تعليمات مختلفة أو خوارزمية مختلفة، خاصة عند موااجهة مقاربات مختلفة لحل المشكلة نفسها. علينا أن نفهم كل نهج بدقة وندوّن ملاحظاتنا عليه، ثم نضيف رأينا الخاص ونشرح لأنفسنا ما يجري وما هو الاستخدام الأمثل الذي وجدناه. تطبيق أفضل الممارسات كل منا حر في صياغة الكود البرمجي الخاص به بالطريقة التي يجدها مناسبة، لكن لنتخيل أن يفعل ذلك كل عضو من أعضاء فريق العمل؟ ستكون الامور فوضوية ومربكة لذا علينا أن نعود نفسنا على تطبيق أفضل الممارسات التي يُنصح بها أثناء بناء المشاريع، إذ سيسهّل هذا الأمر الاندماج لاحقًا ضمن أي فريق عمل خاصة في الشركات التي تأخذ تطبيق أفضل الممارسات في كتابة وتنسيق وصيانة الشيفرة على محمل الجد دائمًا. يمكن قراءة مقال كيف تكتب كود أنيق وسهل القراءة باستخدام لغة بايثون للمساعدة بذلك. التركيز على المهارات المطلوبة في العمل لا أحد منا كامل، فقد نمتلك بعضًا من المهارات المطلوبة وينقصنا بعضها، لكن من المهم أن لا نشعر بالإحباط، وأن نحاول تعزيز معارفنا وخبراتنا بالاطلاع على مراجع مفيدة والمراجع كثيرة ومتنوعة، ويمكن هنا الاستفادة من مقالات أكاديمية حسوب حول بايثون، إلى جانب الكتب البرمجية التي تساعد على التعلم بطريقة منهجية منظمة. ختامًا بهذا نكون قد قدمنا في هذا المقال ما ينبغي على المتقدم لوظيفة مطور بايثون أن يحضّره ويتذكر تفاصيله قبل الوصول إلى مقابلة العمل، ثم عرضنا بعضًا من الأسئلة التي قد تُطرح عليه أثناء المقابلة. المراجع توثيق بايثون باللغة العربية على موقع موسوعة حسوب كتاب البرمجة بلغة بايثون من إعداد أكاديمية حسوب Advance Concepts of Python for Python Developer Advanced Python Tutorials: Dive into Complex Concepts ( advanced Python interview questions (and answers Python Coding Interview Questions (Beginner to Advanced) اقرأ أيضًا المقال السابق: أهم الأسئلة النظرية التي قد تطرح في المقابلات لتوظيف مطور بايثون أهم أسئلة المقابلات لتوظيف مطور واجهة أمامية كيف تحضر لمقابلة عمل ناجحة؟ كيف تحضر لأسئلة مقابلة عمل مهندس البرمجيات
-
سنتعرف في هذا المقال وهو جزء من سلسلة دليل جودو، على الخطوة الأخيرة في بناء لعبة سفينة الفضاء ثنائية الأبعاد، وهي كيفية إضافة زر لبدء اللعبة وإعلان نهاية اللعبة Game Over وهو ما سنتعلمه في مقال اليوم. إطلاق اللعبة عندما نشغّل اللعبة حاليًا من داخل محرك الألعاب جودو، سيبدأ اللعب مباشرةً، ولهذا سنضيف زرًا كي لا تبدأ اللعبة إلا بعد النقر عليه. لتحقيق ذلك، سنضع زر البدء في مكان مركزي في واجهة اللعبة ونمكّن اللاعب من النقر عليه لبدء اللعبة. يمكن تحقيق ذلك باستخدام عقدة CenterContainer وCanvasLayer، وضبط إعدادات التموضع Layout لضمان أن الزر سيكون في مركز الشاشة ويستجيب جيدًا على مختلف أحجام الشاشات. سنضيف عقدةً من النوع CenterContainer إلى المشهد الرئيسي وكابن للعقدة CanvasLayer، ونضبط الخاصية Layout>Anchor Preset>Layout Mode لتكون قيمتها على كامل المستطيل Full Rect. سنضيف بعد ذلك عقدة ابن من النوع TextureButton ونسمّيها Start، ثم نضع الصورة START (48 x 8).png في الخاصية Textures>Normal لهذا الزر. سنضيف مرجعًا إلى الزر في أعلى سكريبت المشهد الرئيسي كما يلي: @onready var start_button = $CanvasLayer/CenterContainer/Start والآن سنصل الإشارة pressed الخاصة بالزر إلى العقدة Main ثم نضيف الشيفرة التالية: func _on_start_pressed(): start_button.hide() new_game() تعالج الدالة ()new_game عملية إطلاق اللعبة، ولهذا سنعدّل مضمون الدالة ()ready_ كي تعرض زر البدء بدلًا من نشر الأعداء: func _ready(): start_button.show() # spawn_enemies() سنضيف الآن الدالة ()new_game إلى السكريبت الرئيسي على النحو الآتي: func new_game(): score = 0 $CanvasLayer/UI.update_score(score) $Player.start() spawn_enemies() يجب أن يظهر زر البداية الآن عند تشغيل اللعبة، وسيؤدي الضغط عليه إلى انطلاقها إنهاء اللعبة من أجل إنهاء اللعبة، ومنحها الشكل النهائي بالعبارة الشهيرة Game Over، سنحتاج إلى إضافة عقدة ابن من النوع TextureRect إلى العقدة CenterContainer، وسنسميها GameOver. سنستخدم الصورة GAME_OVER (72 x 8).png كخلفية لها، وستتداخل هذه العقدة مع عقدة زر البداية، لكن لن يؤثر ذلك على اللعبة لأن كلًا منهما سيظهر في توقيت مختلف. سنضيف الآن مرجعًا آخر إلى أعلى السكريبت الرئيسي كما يلي: @onready var game_over = $CanvasLayer/CenterContainer/GameOver نضيف بعد ذلك الدالة ()game_over.hide إلى الدالة ()ready_، ثم نصل إشارة اللاعب died إلى Main: func _on_player_died(): get_tree().call_group("enemies", "queue_free") game_over.show() await get_tree().create_timer(2).timeout game_over.hide() start_button.show() وهنا ستعرض الشيفرة السابقة عبارة Game Over لمدة ثانيتين، ثم تعود باللاعب إلى زر البداية كي يتمكن من إعادة اللعبة. الخلاصة بهذا نكون قد أنهينا بناء لعبة بسيطة ثنائية الأبعاد عبر محرك الألعاب جودو، وإن أردت الاطلاع على المزيد من الأفكار والتقنيات في استخدام محرّك الألعاب جودو، وكيفية تسخيرها في تحويل أفكارك إلى ألعاب احترافية ننصحك بالاطلاع على دورة تطوير الألعاب باستخدام محرك جودو التي تقدمها أكاديمية حسوب التي تنطلق بك من الصفر وحتى الاحتراف من خلال دروس فيديو أعدها وقدمها مطورون خبراء في هذا المجال وباللغة العربية. وإن كنت تشعر أنك فهمت التقنيات التي ناقشناها في هذه السلسلة، فأنت مستعد لتطوير اللعبة أكثر. لهذا، حاول إضافة ميزة واحدة جديدة إلى اللعبة، ونقترح عليك بعضًا منها: إضافة أنواع مختلفة من الأعداء: ستجد أيقونات مختلفة للأعداء استفد من ذلك مجموعات من الأعداء: أضف مجموعة أخرى من الأعداء عندما تنتهي من المجموعة الأولى كبير الأعداء: جرب أن تضيف عدوًا أكبر حجماً من الأعداء العاديين معززات القوة: جرب عرض جوائز تعزز قوة المركبة الفضائية، وفكر كيف ستجمعها إعادة شحن الدرع: أضف معززات لتدعيم الدرع الذي يحمي السفينة الفضائية تطوير القذائف: أضف أنواعًا أخرى من القذائف إضافة مؤثرات صوتية وأصوات: يمكنك إضافة تأثيرات صوتية إلى اللعبة لجعلها أكثر جاذبية ترجمة -وبتصرف للمقالين: Starting and Ending the Game و Wrapping up. اقرأ أيضًا المقال السابق: بناء واجهة المستخدم وعارض النتيجة في لعبة السفينة الفضائية إنشاء مشهد إطلاق العدو للنشر في لعبة السفينة الفضائية إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot
-
سنشرح في هذا المقال وهو جزء من سلسلة دليل جودو، كيفية بناء واجهة المستخدم وعارض النتيجة في الألعاب ثنائية الأبعاد عبر محرك Godot. آخر الأقسام الرئيسية التي علينا بناؤها في لعبتنا ثنائية الأبعاد هي واجهة المستخدم User Interface؛ إذ سنحتاج إلى طريقة لعرض نتيجة اللاعب وغيرها من المعلومات. ولتنفيذ الأمر، سنستخدم عقدةً مختلفة من النوع Control التي يزودنا بها محرك الألعاب جودو لبناء واجهات المستخدم. مشهد واجهة المستخدم سنبدأ المشهد بعقدة من النوع MarginContainer ونسميها UI، حيث تضمن الحاوية MarginContainer ألا يقترب العقد الأبناء كثيرًا من الحافة من خلال إضافة هوامش حولها. سننقر الآن على العقدة UI وننتقل إلى نافذة الفاحص Inspector، ثم نضبط قيم الهوامش الأربعة على 10 ضمن الخاصية Theme Overrides>Constants. ننتقل بعدها إلى نافذة العرض الثنائي وننقر على أيقونة تجهيزات المراسي Anchor Preset وأيقونة المرساة Anchor، ثم نختار موقع الحاوية ليكون في أعلى الشاشة وعلى اتساعها بالعرض بالأعلى Top Wide. نضيف تاليًا عقدةً من النوع HBoxContainer، وهي نوع من الحاويات التي تنظم الأبناء أفقيًا. سنضيف ضمن هذه العقدة عقدة ابن من النوع TextureProgressBar، وهي شريط تقدم يمثل وضع درع المركبة الفضائية، وسنسمي هذه العقدة ShieldBar. لا تضم مجموعة الصور التي نزّلناها في مشروعنا أية صورة مناسبة لشريط التقدم، لذلك سننزلّ الصورتين التاليتين ونحفظهما في مجلد اللعبة. ضع صورة الخلفية الخضراء كقيمة للخاصية Texture>Progress، والصورة البيضاء كقيمة للخاصية Texture>Under. سنلاحظ مباشرة أن الشريط صغير جدًا، لذلك سنغير قيمة الخاصية Minimum Custom Size لتصبح (80,60) وسنرى أن المستطيل البرتقالي قد كبر. وكما هو واضح، لن يكون تمدد الصورة جميلًا، وقد يبدو سيئًا أيضًا، لهذا سنفعّل الخيار Range>Nine Patch Stretch ونضبط بعدها قيم الخاصية Stretch Margin الأربعة التي ستظهر على 3. من المفترض أن نرى الآن شريطًا طويلًا فارغًا. ولمعرفة كيف سيبدو عندما يبدأ بالامتلاء، سنغير قيمة الخاصية Range>Value إلى أي قيمة بين 0 و 100. سنعرض إلى اليسار نتيجة اللاعب، وقد نستخدم عقدةً من النوع Label وخط مناسب. وبالطبع لن تكون الخطوط التي يقدمها نظام التشغيل مناسبةً للعبة، ولهذا سنستخدم أيقونات الأرقام الموجودة في مجلد الصور، وذلك بعد معالجتها عن طريق الشيفرة لعرضها بالشكل الصحيح. إنشاء عداد للنتيجة سننشئ مشهدًا جديدًا أساسه عقدة من النوع HBoxContainer ونسيمها ScoreCounter. سنضبط هذه العقدة ليكون مركز ارتكاز العقدة بالعرض بالأعلى Top Wide، أي أن العقدة ستمتد أفقيًا وتتموضع عند الجزء العلوي من المشهد، ومحاذاتها نهاية الإنكماش End، أي ستكون محاذاة العقدة على الطرف الأيمن أو نهاية المساحة المتاحة باستخدام الزر المجاورة لأيقونة المرساة. سنضبط أيضًا الخاصية Theme Overrides>Constants>Separation على القيمة 0، وهنا علينا تفعيل الخيار إلى جانب الخاصية. سنضيف الآن مجموعةً من العقد النصية من النوع TextureRect ضمن الحاوية الأفقية لعرض الأرقام، وسنضيف عقدةً واحدة أولًا ثم نضاعف العدد. سنسمي العقدة النصية Digit0 ثم ننتقل إلى الفاحص، فالخاصية Texture، ونختار إنشاء واحدة جديدة New AtlasTexture. سننقر بعد ذلك على هذا الخيار لتظهر لنا نافذة أسفلها الخاصية Atlas. نسحب الآن الصورة Number_font (8 x 8).png إلى هذه الخاصية ثم نضبط قيم الخاصية Region على (32, 8, 8, 😎. بعد ذلك ننقر مجددًا على الخاصية Texture لتغلق النافذة، ثم اضبط قيمة الخاصية Stretch Mode على Keep Aspect Centered. سنعود الآن إلى نافذة المشهد ونختار العقدة Digit0، ثم نضغط على المفتاحين Ctrl+D سبع مرات لإنشاء سبع عقد أخرى مماثلة للأولى من ناحية الشكل والخاصيات. حيث ستبدو نافذة العرض بعد هذه الخطوات كما يلي: على الرغم من إنشاء ثمانية عقد TextureRect للأرقام، إلا أن قيمة الخاصية Texture تبقى نفسها، وهذه مشكلة، فعند تغيير الخاصية Region لعرض الأرقام المختلفة ستعرض كل العقد صورة الرقم نفسه. يعود السبب في ذلك إلى أن كائنات الموارد مثل العقدة Texture ستحمّل في الذاكرة مرةً واحدة ثم تتشاركها العقدة المختلفة. وهذا أمر فعّال جدًا لأنه لا يهدر الذاكرة بتحميل نفس النسخة من المورد عدة مرات. إذًا، علينا تخصيص جزء من المورد لكل عقدة حتى تكون الصورة المعروضة في كل عقدة فريدة، وهنا سننقر في كل عقدة على السهم المجاور للخاصية AtlasTexture ثم نضغط على خيار اجعله فريدًا Make Unique. نضيف الآن سكريبت إلى العقدة ScoreCounter، ونختار فيها المنطقة الصحيحة Region من الصورة لكل رقم نريد عرضه: extends HBoxContainer var digit_coords = { 1: Vector2(0, 0), 2: Vector2(8, 0), 3: Vector2(16, 0), 4: Vector2(24, 0), 5: Vector2(32, 0), 6: Vector2(0, 8), 7: Vector2(8, 8), 8: Vector2(16, 8), 9: Vector2(24, 8), 0: Vector2(32, 8) } func display_digits(n): var s = "%08d" % n for i in 8: get_child(i).texture.region = Rect2(digit_coords[int(s[i])], Vector2(8, 8)) تبدأ الشيفرة بتشكيل قائمة من الإحداثيات التي تمثل أماكن من الصورة يحدد كل منها مكان وجود رقم، ثم تنسّق الدالة ()display_digits العدد المطلوب ليكون من 8 أرقام، بحيث لو كان أمامنا الرقم 285 مثلًا، فسيكتب بالشكل 00000258. بعد ذلك، سنضع الرقم المناسب في كل منزلة من العدد السابق اعتمادًا على مصفوفة الإحداثيات. إضافة سكريبت واجهة المستخدم UI نعود الآن إلى المشهد ui ثم نضيف المشهد ScoreCounter إلى العقدة HBoxContainer. نضيف بعد ذلك السكريبت التالي إلى العقدة UI: extends MarginContainer @onready var shield_bar = $HBoxContainer/ShieldBar @onready var score_counter = $HBoxContainer/ScoreCounter func update_score(value): score_counter.display_digits(value) func update_shield(max_value, value): shield_bar.max_value = max_value shield_bar.value = value سنستدعي هاتين الدالتين في المشهد الرئيسي Main في كل مرة نحتاج فيها إلى تغيير النتيجة أو قوة درع السفينة. إضافة المشهد UI إلى المشهد الرئيسي Main سنضيف الآن عقدةً من النوع CanvasLayer إلى المشهد الرئيسي Main، ثم منسخ إليها المشهد UI كعقدة ابن. ستُنشئ العقدة CanvasLayer طبقة رسم جديدة، ولهذا ستُرسم واجهة المستخدم فوق جميع مكونات اللعبة؛ ولحل هذه المشكلة سنغيّر التابع التالي في السكريبت main.gd كما يلي: func _on_enemy_died(value): score += value $CanvasLayer/UI.update_score(score) درع اللاعب يمكننا إضافة الدرع إلى سكريبت اللاعب، عبر إضافة الأسطر التالية إلى الملف player.gd: signal died signal shield_changed @export var max_shield = 10 var shield = max_shield: set = set_shield سنستدعي الدالة ()set_shield من خلال عملية الإسناد = set في كل مرة يُضبط فيها المتغير shield الذي يمثل قيمة درع المركبة كما يلي: func set_shield(value): shield = min(max_shield, value) shield_changed.emit(max_shield, shield) if shield <= 0: hide() died.emit() نستطيع أيضًا وصل إشارة المركبة area_entered كي نلتقط اصطدام العدو بالمركبة: func _on_area_entered(area): if area.is_in_group("enemies"): area.explode() shield -= max_shield / 2 لنضف الآن بعض الضرر إلى درع المركبة عندما تُصاب في سكريبت قذائف العدو enemy_bullet.gd، وذلك على النحو الآتي: func _on_area_entered(area): if area.name == "Player": queue_free() area.shield -= 1 في الأخير، علينا وصل إشارة اللاعب shield_changed إلى الدالة التي تحدّث شريط الدرع في واجهة المستخدم، ولهذا سننتقل إلى الفاحص بعد اختيار عقدة اللاعب Player في المشهد الرئيسي. سننقر بعد ذلك في نافذة العقدة Node نقرًا مزدوجًا على الإشارة، وذلك لفتح نافذة توصل إشارة إلى دالة Connect a Signal. سنختار بعد ذلك العقدة UI ثم نكتب update_shield في صندوق الدالة المتلقية Receiver Method. يمكننا الآن تشغيل اللعبة والتأكد من أن طاقة الدرع تنخفض عندما تصيبه قذيفة أو مركبة معادية. ختامًا بهذا نكون قد تعرفنا على كيفية بناء واجهة المستخدم وعارض النتيجة في الألعاب ثنائية الأبعاد عبر محرك الألعاب جودو، وسنتعرف على كيفية تطوير اللعبة أكثر بالمقالات الموالية. ترجمة -وبتصرف- للمقال: UI and Score. اقرأ أيضًا المقال السابق: إنشاء مشهد إطلاق العدو للنشر في لعبة السفينة الفضائية إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot
-
تُعَد لغة بايثون من أكثر لغات البرمجة قوةً وانتشارًا، فهي تدخل في معظم البنى الرقمية العصرية. ونظرًا لتنوع المسارات الوظيفية التي قد يوفرها احتراف البرمجة بلغة بايثون، سيركز المطوّر غالبًا على إتقان أساسيات مسار أو مسارين ثم يبحث عن فرصة عمل كمتدرب. ويَعُدّ أغلب المطوروين هذه الخطوة على أنها الخطوة الأصعب في المسيرة المهنية، ويعاني الكثيرون في اجتيازها. لهذا السبب، نوجه مقالنا إلى مطوري بايثون الذين يستعدون للدخول إلى سوق العمل. ونناقش فيه أهم النقاط التي قد يُسألون عنها في مقابلات العمل، سواءً لناحية استيعابهم مفاهيم اللغة الأساسية أو لتحديد إمكانياتهم التقنية وقدرتهم على إيجاد الحلول. سنتحدث فيما يلي عن بعض الأسئلة التي تُطرح على مطوري بايثون في مقابلات العمل، وقد قدمناها وفق أقسام تغطي أساسيات اللغة، ثم مكتبات بايثون، وأطر العمل التي تعتمد على بايثون. أسئلة عامة فيما يلي أبرز الأسئلة التي قد تطرح على المتقدمين لوظيفة مطوري بايثون في مقابلات التوظيف. لماذا اخترت العمل مع بايثون؟ ما مميزاتها للإجابة على هذا السؤال، لا بد من توضيح أن بايثون هي لغة واسعة الانتشار تعتمد عليها كبرى الشركات العالمية، ومطلوبة بشدة في سوق العمل، نظرًا لإمكانياتها الكبيرة. ويعزز ذلك الكمّ الهائل من المكتبات التي تغطي تقريبًا جميع الاحتياجات في العالم الرقمي مثل تحليل البيانات وتعلم الآلة والذكاء الصنعي وتطبيقات الويب. أما ما يميز بايثون فبدايةً هي سهولة الصياغة اللغوية، فهي قريبة من الإنكليزية، إلى جانب وجود فكرة الإزاحة لتميز الكتل عن بعضها، مما يجعلها ذلك أسهل من ناحية الفهم والقراءة؛ إلى جانب أن قدرة بايثون على تحديد نوع المتغير دون التصريح عنه يُعَد فعالًا جدًا ومريحًا؛ أما الشيء الأهم، فهو وجود كم هائل من المكتبات التي تغطي معظم احتياجاتي كمطور دون إعادة اختراع العجلة من جديد، إضافةً إلى إمكانية استخدام اللغة لتطوير أي شيء تقريبًا دون الحاجة إلى الاستعانة بلغات أخرى. ما المقصود بأن لغة بايثون ديناميكية النمط dynamically typed بايثون لغة ديناميكية في تحديد نوع المتغير، فهي تحدد تلقائيًا نوع المتغير بمجرد إسناد قيمة له. فإن أسندنا له قيمةً صحيحة، فسيكون المتحول صحيحًا، ونصيًا إن أسندنا إليه نصًا، بالتالي لا حاجة للتصريح عن نوع المتغير. كيف نحدد نوع متغير ما إن احتجنا إلى ذلك لا يمكن تحديد نوع المتغير عند التصريح عنه؛ إذ يرمي عندها المفسّر Interpreter خطأً. ولحل الأمر نستخدم التابع ()type لتحديد نوع المتغير والتعليمة is لمقارنة نوعه مع الأنواع التي تدعمها لغة بايثون وتكون نتيجة العملية نتيجة منطقيةً بوليانية (صحيح أو خاطئ)، وعندها يُنفذ الشرط المتعلق بالنوع أو لا يُنفّذ. قارن بين list و tuple في بايثون، ووضّح ذلك من خلال مثال يقدم كلا النوعين أسلوبًا لترتيب البيانات وفق تسلسل محدد لتخزينها، مثل عرض قائمة بالمقاسات المتاحة لسلعة ما؛ أما وجه الاختلاف بينهما، فهو قابلية التعديل Immutability؛ فالقوائم من النوع list ديناميكية، ويمكن تعديل عناصرها وإضافة أو حذف عناصر بعد إنشائها وفي أي وقت؛ في حين لا يمكن تعديل أي شيء بالقوائم من النوع tuple بعد إنشائها. مع ذلك لكل نوع استخدامه المناسب، حيث نستخدم القائمة list مثلًا لتخزين الاسم المستعار لمستخدم وعمره ومكان إقامته وبريده الإلكتروني، فقد يرغب المستخدم بتغيير أي منها؛ في حين نستخدم القائمة tuple لتخزين أيام الأسبوع مثلًا فهي لن تتغير. كيف نمرر إلى دالة أربعة وسطاء موضعيين Positional دون أن تصرح عن أربع معاملات نصرح في هذه الحالة عن الدالة بالطريقة التقليدية ثم نسمي معاملًا واحدًا فقط يبدأ بالرمز * كالتالي: def func(*args): بهذا الشكل يمكن أن نمرر للدالة أي عدد من الوسطاء، وsنصل إلى أي وسيط من خلال دليل المعامل args مثل args[2] لاستخدام قيمة الوسيط الثالث. متى يحافظ وسيط ممر إلى دالة على قيمته بعد أن تحدّثها الدالة تمرر بايثون الوسطاء إلى الدوال على شكل مرجع إلى كائن، لهذا نحن أمام سلوكين في هذه الحالة: الوسيط القابل للتعديل Mutable مثل القوائم list سيحتفظ المتغير عندها بالتعديلات التي أجرتها الدالة الوسيط غير القابل للتعديل مثل المتغيرات الصحيحة int والنصوص str لن تتغير عندها قيمة المتغير خارج الدالة ما هي المزخرفات Decorators في بايثون؟ المزخرفات Decorators تقنيًا هي دوال تقبل دالة أخرى كوسيط لها وتعمل على زيادة وظائف هذه الدالة أو تغيير وظيفتها وإعادة هذه الدالة بشكلها المحسّن دون الحاجة إلى تغيير الشيفرة الأصلية لها. ما الفرق بين الصنف Class والكائن Object والنسخة instance في بايثون؟ تُعَد هذه المفاهيم أساسيةً في البرمجة كائنية التوجه OOP، فالصنف في بايثون هو مخطط أو هيكل للكائن، تُعرَّف فيه المتغيرات والدوال التي تصف خصائص الكائن وطريقة عمله أو الوظائف التي يقوم بها؛ أما الكائن، فهو كيان فعلي حقيقي تأخذ حيزًا من الذاكرة بٌنيت على أساس مخطط الصنف. والنسخة هي مصطلح يُستخدم لوصف الكائن بعد إنشائه، خاصةً عند وجود أكثر من كائن ومن نفس الصنف. ما عمل الدوال الخاصة __init__ و __new__ و __call__ في بايثون؟ تسمى هذه الدوال الخاصة باسم الدوال السحرية Magic Methods، حيث تتحكم بايثون من خلالها بسلوك الكائنات Objects بطرق خاصة، وتستخدم الدوال المعنية على النحو الآتي: تُستخدم الدالة __new__ للتحكم في عملية إنشاء كائن من الصنف، وتُستدعى قبل __init__ تُفيد في حالات خاصة مثل حالة نمط التصميم singleton أو Metaclass تُستخدم الدالة __init__ لتهيئة خصائص الكائن Object عند إنشائه، وتُستدعى مباشرة بعد __new__ تُستخدم الدالة __call__ لجعل الكائن قابلًا للاستدعاء كأنه دالة، بحيث يمكن تنفيذ كود عند استدعاء هذا الكائن مباشرةً وضح مفهوم الصنف Metaclass في بايثون Metaclass هو صنف خاص في بايثون مهمته تعريف سلوك الأصناف، حيث يتحكم بإنشاء وتعديل وتحديد سلوك هذه الأصناف قبل بناء كائنات منها. ينتُج عن Metaclass في العادة أصناف أخرى، فبينما تُعطي الأصناف Classes كائنات Objects، يعطينا الصنف Metacalss أصنافًا مخصصةً أو يغير خصائص وتوابع صنف ديناميكيًا. ما هو نمط المفردة Singleton في بايثون وأين يُستخدم نمط المفردة singleton هو نموذج في تصميم الأصناف يفرض على الصنف عدم إنتاج أكثر من نسخة منه، وهكذا سيعيد الصنف نفس الكائن عند استدعائه مرةً أخرى، أو عند إنشاء نسخة أخرى عنه. ولهذا النموذج أهميته في حالات استخدام معينة مثل بناء كائنات تسجيل الدخول إلى تطبيق مثلًا أو الحاجة لوجود كائن واحد فقط يؤمن اتصالًا مع قاعدة بيانات. اشرح مفهوم التجاوز Overriding في بايثون يرتبط هذا المفهوم بالبرمجة كائنية التوجه في بايثون وبالوراثة تحديدًا، فلو كان لدينا صنف ابن Class يرث من صنف آخر أب وعرّفنا تابع Method في الصنف الابن بنفس البصمة Signature للدالة الموجودة في الصنف الأب، فإن دالة الابن ستتجاوز وتلغي وتستبدل دالة الأب تلقائيًا. تدعم بايثون التجاوز، وتقدم التابع super إن أردنا استدعاء تابع العنصر الأب الأصلي ضمن الابن. ماهي المكررات Iterators في بايثون المكررات Iterators في بايثون هي آلية تسمح لنا بالتنقل عبر مجموعة من العناصر مثل القوائم list أو الصفوف أو حتى السلاسل النصية تدريجيًا. وبدون الحاجة إلى حفظ جميع العناصر في الذاكرة مرةً واحدة. يمكننا تحويل أي مجموعة قابلة للتكرار إلى مكرر باستخدام التابع ()iterالذي يعيد لنا مكررًا يمكن استخدامه للتنقل عبر العناصر، فلكي نتقل من عنصر إلى عنصر آخر في المكرر، سنستخدم الدالة ()next، وفي حال انتهت العناصر سنحصل على استنثاء من نوع StopIteration لنعرف أننا وصلنا لنهاية عناصر المكرر. فيما يلي مثال لمكرر سلسلة نصية: text = "Ali" # تحويل النص إلى مكرر text_iterator = iter(text) # التنقل عبر الأحرف باستخدام print(next(text_iterator)) # Output: A print(next(text_iterator)) # Output: l print(next(text_iterator)) # Output: i # محاولة الوصول إلى حرف غير موجود try: print(next(text_iterator)) except StopIteration: print("وصلنا إلى نهاية المكرر") حدد استخدامات الكلمة المفتاحية with في بايثون مع ذكر مثال حول ذلك تُستخدم with غالبًا لإدارة الموارد التي يعمل عليها التطبيق كالملفات وقواعد البيانات والاتصالات الشبكية، فمن خلالها سنضمن حيازة المورد واستخدامه بأمان، وإغلاقه تلقائيًا بعد الانتهاء منه حتى في حال وقوع أخطاء؛ فعندما نريد فتح ملف مثلًا، سنستخدم التعليمة open لفتح الملف، ولا بد من إغلاق الملف يدويًا عند الانتهاء باستخدام close، لكن عند استخدام with قبل open، فلا حاجة عندها لإغلاق الملف وسيُغلق تلقائيًا. with open("file.txt", "r") as file: content = file.read() print(content) ما الفرق بين الخيط Thread والعملية Process الخيط هو آلية لتنفيذ مجموعة تعليمات متزامنة مع مجموعة تعليمات أخرى لكن باستخدام نفس الموارد، كمساحة الذاكرة والملفات مثلًا. توحي الخيوط بمعالجة التعليمات على التوازي، لكن ما يجري في الواقع هو الانتقال السريع بين خيوط المعالجة، كما تتبادل الخيوط بيانات فيما بينها لتنسيق ماركة الموارد؛ أما العملية Process فهي آلية مشابهة لكن لكل عملية معزولة ولها مواردها الخاصة ولا تتشارك العمليات الموارد والبيانات فيما بينها إلا إن طُلب منها صراحة فعل ذلك بآليات تواصل خاصة. ما هي فكرة GIL في بايثون، وما تأثيراتها على تنفيذ الشيفرة تعدّ GIL أو مايعرف بالقفل العام للمفسر Global Interpreter Lock آليةً مهمة في بايثون تمنع أكثر من خيط معالجة Thread من تنفيذ شيفرة بايثون في نفس اللحظة ضمن نفس العملية. بمعنى آخر، حتى لو كانت لدينا عدة خيوط تعمل معًا، فإن GIL يسمح فقط لخيط واحد بتنفيذ تعليمات بشيفرة البايت Bytecode في كل وقت. يؤثر وجود GIL بوضوح على أداء البرامج التي تعتمد على المعالجة المكثفة للمعالج CPU-bound، مثل معالجة الصور وتدريب نماذج تعلم الآلة والعمليات الحسابية الثقيلة، وذلك لأنه يمنع الاستفادة الكاملة من جميع أنوية المعالج المتاحة، مما يؤدي إلى أداء أقل من المتوقع. مع ذلك، يمكن التغلب على هذه المشكلة باستخدام تقنية المعالجة المتعددة Multi-Processing بدلًا من الخيوط، بحيث تعمل كل عملية مستقلة بدون مشاركة GIL؛ كما يكمننا استخدام توزيعات بايثون لا تحتوي على GIL مثل Jython أو IronPython. ومن الحلول المتاحة أيضًا الاعتماد على مكتبات مكتوبة بلغة C أو لغات أخرى نتفادى قيد GIL، مثل NumPy وتنسرفلو TensorFlo. هل هناك فرق بين Python و CPython و Jython بالطبع، فبايثون Python هي مجموعة من المواصفات التي تحدد صياغة اللغة، بينما CPython و Jython هما طريقتان لتطبيق وتنفيذ هذه المواصفات؛ حيث أن CPython هي تنفيذ بايثون الرسمي بلغة C، أي أنها كُتبت باستخدام لغة C وتُصرّف إلى شيفرة البايت bytecode قبل أن ينفذها مفسّر مبني بلغة C؛ بينما Jython هي تنفيذ لبايثون بلغة جافا، فقد كُتبت باستخدام جافا وتُنفّذ باستخدام آلة جافا الافتراضية JVM وتدعم كل مكتبات جافا. أسئلة حول المكتبات Libraries فيما يلي أبرز الأسئلة التي قد تطرح على المتقدمين لوظيفة مطوري بايثون في مقابلات التوظيف حول مكتبات بايثون. ما هي وظيفة المكتبة المضمّنة OS؟ وما عمل os.environ تقدم هذه المكتبة وظائف للتفاعل مع نظام التشغيل، بما في ذلك إدارة الملفات ومعالجة العمليات وإدارة استدعاء وظائف نظام التشغيل؛ أما os.environ، فهو كائن شبيه بالقواميس يُستخدم للعمل مع متغيرات البيئة في حالات عديدة مثل ضبط الإعدادات والتعامل مع البيانات الحساسة بأمان. ماهي وظيفة المكتبة asyncio ومتى تُستخدم تُستخدم مكتبة asyncio في بايثون لتنفيذ عدة مهام في نفس الوقت، لاسيما في حال كانت هذه المهام تنتظر بيانات معينةم مثل انتظار رد من قاعدة بيانات أو من الإنترنت؛ فبدلًا من إيقاف البرنامج حتى ترد القاعدة أو السيرفر وحدوق حجب أو تعليق لعملية التنفيذ، تسمح asyncio للبرنامج بمواصلة عمله على مهام أخرى في نفس الوقت دون الحاجة لإنشاء خيوط Threads جديدة أو عمليات متعددة Multiprocessing. يتم استخدام هذه الوظيفة عندما تكون لدينا عمليات مقيدة بالمدخلات والمخرجات I/O bound، أي عمليات تتوقف على انتظار بيانات، مثل إرسال طلبات لشبكة، أو قراءة ملفات، أو عند الحاجة لتنفيذ عدة أشياء بنفس الوقت على التزامن بدون استهلاك موارد كثيرة، وبكفاءة أعلى من الخيوط Threads، فهي تعد أفضل من الخيوط في بعض الحالات لأن بايثون يعتمد تقنية GIL التي تمنع تشغيل خيوط كثيرة بفعالية عالية، وبهذا تجنبنا asyncio هذه المشكلة، فتكون أسرع وأخف على الذاكرة. ما هي المكتبات التي تدعم تقنيتي Multi-Threading و Multi-Processing في بايثون تقدم بايثون المكتبة threading لدعم إنشاء خيوط المعالجة والتحكم بها. وتستخدم في العمليات المقيدة بأجهزة الدخل والخرج كتلك التي تنتظر الكتابة إلى وسيط أو القراءة منه؛ بينما تستخدم المكتبة multiprocessing لدعم العمليات المستقلة التي تقيدها الحمولة العالية للمعالج وعلى التوازي، مثل العمليات الحسابية المعقدة. ما الفرق بين التابعين()copy و ()deepcopy في المكتبة copy توفر المكتبة copy في بايثون طريقتين لنسخ الكائنات، هما: النسخ السطحي Shallow copy باستخدام الدالة ()copy النسخ العميق Deep copy باستخدام الدالة ()deepcopy في حالة النسخ السطحي ننسخ الكائن الخارجي فقط، لكن تبقى العناصر الداخلية للكائن مشتركةً بين النسخة الأصلية والنسخة الجديدة؛ بمعنى أن التغييرات التي نجريها على العناصر الداخلية ستؤثر على الأصل أيضًا؛ أما في حالة النسخ العميقة، فإننا ننسخ الكائن وكل عناصره الداخلية بالكامل، بحيث تكون النسخة مستقلةً تمامًا، وأي تعديل على النسخة لا يؤثر أبدًا على الأصل. على سبيل المثال لو عرفنا كائن قائمة original يحتوي على قائمتين داخلية، ونسخنا منه نسختين سطحية shallow_copy وعميقة deep_copy كما في المثال التالي: import copy # قائمة بداخلها قوائم original = [[1, 2, 3], [4, 5, 6]] # نسخ سطحي shallow_copy = copy.copy(original) # نسخ عميق deep_copy = copy.deepcopy(original) # نعدل على أحد العناصر الداخلية في القائمة الأصلية original[0][0] = 'X' # نطبع النتائج print("القائمة الأصلية:", original) print("النسخة السطحية:", shallow_copy) print("النسخة العميقة:", deep_copy) فستتعدل النسخة السطحية مع هذا التغيير وتبقى النسخة العميقة على حالها كما يوضح الخرج التالي: القائمة الأصلية: [['X', 2, 3], [4, 5, 6]] النسخة السطحية: [['X', 2, 3], [4, 5, 6]] النسخة العميقة: [[1, 2, 3], [4, 5, 6]] ما أشهر المكتبات المستخدمة للعمل مع المصفوفات والحسابات الرياضية في بايثون توفر بايثون العديد من المكتبات القوية التي تساعدنا للعمل مع المصفوفات وإجراء الحسابات الرياضية المتقدمة وأشهرها: Numpy: تدعم المصفوفات متعددة الأبعاد، وتقدم مجموعةً كبيرة من الدوال الرياضية للعمل معها SciPy: مبنية على أساس Numpy وتقدم وظائف علمية وتقنية أكبر مثل التكامل والاستيفاء Pandas: مكتبة قوية لمعالجة وتحليل البيانات تقدم هياكل بيانات مثل إطار البيانات DataFrame لتسهيل التعامل مع الجداول والبيانات الضخمة SymPy: مكتبة للرياضيات الرمزية تتيح إجراء عمليات جبرية مثل حل المعادلات والتفاضل والتكامل ما أشهر مكتبات التعامل مع الرسوميات في بايثون يمكن ذكر المكتبات الآتية: Matplotlib: لرسم صور ثنائية البعد ساكنة أو تفاعلية، وتقديم تمثيل رسومي متحرك Pillow: مكتبة لمعالجة الصور، تسمح بفتحها وتعديلها وحفظها Plotly: مكتبة قوية لإنشاء رسوميات تفاعلية ثلاثية الأبعاد وواجهات عرض ديناميكية تصلح للويب والتطبيقات التفاعلية ما المكتبات التي يشيع استخدامها في تطبيقات الرؤية الحاسوبية أبرز هذه المكتبات هي: OpenCV: مكتبة بايثون قوية لإنجاز مهام الرؤية الحاسوبية، مثل معالجة الصور وتطبيق فلاتر مختلفة عليها وتحويلها بين أنظمة لونية مختلفة، كما تفيد في تحليل الفيديو واكتشاف الأشياء Object Detection SimpleCV: هي مكتبة سهلة الاستخدام للمبتدئين في الرؤية الحاسوبية، تتيح الوصول لأدوات معالجة الصور والكاميرا بسرعة وسهولة ما أشهر مكتبات بايثون لبرمجة الأنظمة المدمجة يمكن الإشارة إلى مكتبتي: MicroPython: تتبع أسلوب خاص في بناء Python3 لبرمجة المتحكمات الصغرية Microcontroller وتجهيزات الأنظمة المدمجة مثل الحساسات PySerial: تقدم وظائف للتخاطب مع واجهات التخاطب التسلسلية مثل UART باستخدام بايثون أسئلة حول اطر العمل Frameworks فيما يلي أبرز الأسئلة التي قد تطرح على المتقدمين لوظيفة مطوري بايثون في مقابلات التوظيف حول اُطر العمل Frameworks ما هو إطار العمل Django وما هي أهم ميزاته يُعَد إطار جانغو Django أحد أشهر أطر العمل الشاملة لتطوير تطبيقات الويب يوفر الكثير من الأدوات الجاهزة المضمّنة لتنفيذ الكثير من المهام، مثل إدارة المستخدمين، والتحقق من الصلاحيات، ونظام إدارة المحتوى، والتعامل مع قواعد البيانات؛ ويستخدم أساسًا لبناء التطبيقات المتوسطة إلى كبيرة. ماهو إطار العمل Flask وما هي أهم ميزاته؟ يُعَد فلاسك Flask إطار خفيف ومرن يسمح بتطوير تطبيقات ويب بمرونة عالية، لكنه لا يتضمن الكثير من الأدوات الجاهزة مثل جانغو Django، ويتطلب خطوات إعداد وتهيئة أكثر منه، وهو يصلح لبناء تطبيقات ويب صغيرة إلى متوسطة، كما يستخدم في تقديم خدمات ويب مصغّرة Microservices بالإضافة إلى بناء واجهات برمجية. ماهو إطار العمل FastAPI وما هي أهم ميزاته؟ يُعَد FastAPI إطار عمل حديثًا وخفيف الوزن لبناء واجهات برمجة التطبيقات APIs سريعة وقوية وعالية الأداء باستخدام لغة بايثون.؛ هو يعتمد كثيرًا على ميزتين قويتين في بايثون هما: تحديد نوع البيانات المتوقعة للمتغيرات Type Hints البرمجة غير المتزامنة Asynchronous Programming ما هي اُطر العمل التي يشيع استخدامها في بناء الشبكات العصبونية وتعلم الآلة يمكن ذكر الآتي: TensorFlow: إطار عمل يُستخدم لبناء وتدريب الشبكات العصبونية PyTorch: إطار عمل مفتوح المصدر يقدم أدوات لبناء وتدريب الشبكات العصبونية ختامًا بهذا نكون قد تعرفنا على أهم الأسئلة النظرية التي قد تطرح على المتقدم إلى وظيفة مطور بلغة بايثون، وسنواصل ففي مقالنا التالي توضيح أهم الأسئلة والتمارين العملية التي قد تطلب في المقابلات لتوظيف مطور بايثون، حتى تكون مستعدًا من كل النواحي لمقابلة العمل. المراجع توثيق بايثون باللغة العربية على موقع موسوعة حسوب كتاب البرمجة بلغة بايثون من إعداد أكاديمية حسوب Advance Concepts of Python for Python Developer Advanced Python Tutorials: Dive into Complex Concepts ( advanced Python interview questions (and answers Python Coding Interview Questions (Beginner to Advanced) اقرأ أيضًا أهم أسئلة المقابلات لتوظيف مطور واجهة أمامية كيف تحضر لمقابلة عمل ناجحة؟ كيف تحضر لأسئلة مقابلة عمل مهندس البرمجيات
-
بعد أن بنينا شخصية لاعب قادر على إطلاق النار على العدو في لعبتنا ثنائية الأبعاد ضمن محرك الألعاب جودو، لا بد من إنشاء عدو في اللعبة ونبرمج طريقة حركته في اللعبة وطريقة إضافة آلية إطلاق النار إليه ليهاجم اللاعب، وهذا ما سنتعلمه في مقالنا الذي هو جزء من سلسلة دليل جودو. بناء المشهد سنستخدم العقدة Area2D كأساس لبناء المشهد لأننا بحاجة إلى اكتشاف تداخل كائن العدو مع اللاعب أو قذائفه، كما سنحتاج إلى عدة عقد نلخصها كالتالي: Enemy: Area2D Sprite2D CollisionShape2D AnimationPlayer MoveTimer: Timer ShootTimer: Timer نختار العقدة Enemy ثم ننقر على النافذة عقدة Node إلى جوار الفاحص inspector وننقر بعدها على مجموعات Groups، ثم على أيقونة + لإضافة مجموعة جديدة سنسميها enemies. ملاحظة: من المهم هنا تذكر أن السكريبت في مشهد القذائف يبحث عن كائنات من مجموعة الأعداء enemies. سننتقل بعد ذلك إلى العقدة Sprite2D ثم نضيف الصورة Bon_Bon (16 x 16).png إلى الخاصية Texture ونضبط قيمة الخاصية Animation>Hframes على 4. ملاحظة: تمثل الصورة Bon_Bon (16 x 16).png مجموعة من الإطارات الرسومية التي تشكل الحركة فكل إطار يمثل وضعية أو جزءًا من هذه الحركة، وعند ضبط Hframes إلى 4 معناه أننا نخبر جودو بأن الصورة مقسمة أفقيًا إلى 4 إطارات، مما يسمح له بالتعامل مع كل إطار على حدة فإذا كانت الصورة بحجم 16 × 16 بكسل فعند تقسيمها إلى 4 إطارات أفقية سيكون كل إطار بعرض 4 بكسل وارتفاع 16 بكسل ويمكن عرض كل إطار بشكل منفصل لتكوين الحركة. ننقر الآن على العقدة CollisonShape2D ثم ننتقل إلى الخاصية Shape ونختار شكل التصادم RectangleShape2D ونضبط حجمه ليناسب أبعاد الأيقونة؛ ثم ننتقل إلى أيقونتي المؤقتات ونفعّل الخاصية One Shot لكل مؤقت. ننقر على العقدة AnimationPlayer وننتقل إلى لوحة التحريك Animation Panel، وننشئ رسمًا متحركًا باسم bounce ونضبطه ليكرر نفسه looping وأن يبدأ العمل تلقائيًا عند تشغيل اللعبة autoplay بالنقر على الأزرار الموافقة. بعد هذه المرحلة، سنضبط الخيار انطباق أو محاذاة Snap في أسفل اللوحة على القيمة 0.05. سننشئ الآن مسارات تحريك لتتبع الخاصيتين Textrure و Hframes للشخصية وهي بحالتنا العقدة Sprite2D، وذلك بالنقر على أيقونة المفتاح إلى جانب كل منهما. وسنحتاج إلى مفاتيح مرجعية على هذه المسارات لاحقًا عندما نضيف رسمًا متحركًا جديدًا باسم explode يستخدم قيمًا مختلفة لهاتين الخاصيتين. سنضيف الآن مسارًا تحريكيًا للخاصية Frame بالنقر بدايةً على المفتاح إلى جانب الخاصية، ونبدأ بعدها بإنشاء مفتاح مرجعي يتغير عنده إطار الشخصية وفق الترتيب التالي 2>1>0>3>0، بحيث يفصل بين كل مفتاحين 0.1 ثانية ماعدا آخر إطارين سيكونان متداخلين. لتنفيذ العملية، سنحرك المقبض الأزرق على الخط الزمني مسافة 0.1 ثانية ونضع قيمة الإطار المطلوب في مربع الخاصية Frame، ثم ننقر على زر المفتاح وهكذا. وسينتج عن تحريك الرسم بهذا الشكل أيقونة تنمو وتعود إلى حجمها الطبيعي في النهاية. وسيبدو شكل لوحة التحريك كالتالي: ننقر على زر التشغيل في لوحة التحريك لرؤية نتيجة العمل، وبإمكاننا طبعًا تعديل الحركة حسب رغبتنا. نضيف الآن رسمًا متحركًا جديدًا باسم explode ونضبط مدته على 0.4 ثانية، كما نستبدل صورة الشخصية بالصورة Explosion (16 x 16).png وننشئ مسارات للخاصيات Frame و Texture و Hframes. سنعدّل قيمة الخاصية Hframes إلى 5، ثم كما فعلنا سابقًا، ونضع مفتاحين مرجعيين في المسار الأول عند اللحظة 0، وتكون قيمة الإطار 0 والثانية عند اللحظة 0.4، كما ستكون قيمة الإطار 4. عند الانتهاء من كل ذكل، سنشغل الرسم المتحرك لرؤية النتيجة. سكريبت شخصية العدو ينتشر الأعداء أعلى شاشة اللعبة على شكل شبكة، حيث ستقترب شبكة الأعداء من اللاعب بعد فترة زمنية محددة، ثم يعود إلى الأعلى ما لم يُدمر منها، كما سيطلق الأعداء النار على اللاعب دوريًا. سنضيف سكريبتًا إلى عقدة العدو ونعرّف المتغيرات التالية: extends Area2D var start_pos = Vector2.ZERO var speed = 0 @onready var screensize = get_viewport_rect().size يتتبع المتغير start_pos موقع انطلاق الأعداء كي يعودوا إلى موقعهم الأصلي بعد الحركة. سنضبط قيمة المتغير عندما ننشر الأعداء في الشبكة، وسنستدعي الدالة ()start الخاصة بهم: func start(pos): speed = 0 position = Vector2(pos.x, -pos.y) start_pos = pos await get_tree().create_timer(randf_range(0.25, 0.55)).timeout var tween = create_tween().set_trans(Tween.TRANS_BACK) tween.tween_property(self, "position:y", start_pos.y, 1.4) await tween.finished $MoveTimer.wait_time = randf_range(5, 20) $MoveTimer.start() $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start() سنستدعي الدالة السابقة عند نشر الأعداء، ونمرر إليها شعاع موضع يمثل مكان ظهور العدو على الشاشة، وسنلاحظ عندها أننا ننشر العدو فوق الحافة العلوية للشاشة. سنعطي y قيمة سالبة كي نتمكن من تحريك العدو عندما يدخل إلى الشاشة باستخدام مولد إطارات بينية Tween، ونضبط أيضًا قيمتي المؤقتين عشوائيًا كي لا يتحرك العدو ويطلق النار في نفس الوقت، وبعد ذلك سنصل إشارة timeout الخاصة بكل مؤقت. func _on_timer_timeout(): speed = randf_range(75, 100) func _on_shoot_timer_timeout(): $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start() يمكن هنا التحرك عند انتهاء المؤقت من عد الفترة الزمنية التي يُضبط عليها، لكننا لم ننشئ قذائف الأعداء بعد، وهذا ما سنتعامل معه لاحقًا. وبما أننا سنغير سرعة حركة الأعداء من خلال المتغير speed، فمعنى ذلك، أن بإمكاننا التحرك عبر كتابة الشيفرة التالية في دالة المعالجة: func _process(delta): position.y += speed * delta if position.y > screensize.y + 32: start(start_pos) إن لم تكن قيمة المتغير speed هي 0، فسنرى العدو يتحرك على الشاشة، وعندما يصل إلى الأسفل يعود من الأعلى مجددًا. لقد كتبنا الدالة التي تعالج انفجار الأعداء عندما تصطدم بها قذيفة اللاعب الدالة ()explode عندما بنينا مشهد القذيفة، لهذا سنضيف نفس الدالة هنا: func explode(): speed = 0 $AnimationPlayer.play("explode") set_deferred("monitoring", false) died.emit(5) await $AnimationPlayer.animation_finished queue_free() سنوقف في هذه الدالة حركة العدو، ثم نشغّل الرسم المتحرك الخاص بعملية الانفجار ونحذف بعدها العدو في نهاية الدالة. وتضمن الدالة ()set_deferred إيقاف الخاصية monitoring في كائن العدو كي لا تصيبه قذيفة أخرى أثناء انفجاره، وعندها سيصيب القذيفة التي تنفجر ويفجرها مجددًا ويستدعي الدالة التي تنفذ عملية التفجير مجددًا، وهكذا. سنضيف الآن الإشارة died إلى أعلى السكريبت: signal died وسنستخدم هذه الإشارة لإبلاغ المشهد الرئيسي أن اللاعب حقق بعض النقاط. نشر العدو سنعود الآن إلى المشهد الرئيسي Main لإضافة بعض الأعداء إلى اللعبة، ولهذا سنضيف سكريبتًا إلى المشهد الرئيسي ونبدأ بتحميل مشهد العدو برمجيًا: extends Node2D var enemy = preload("res://enemy.tscn") var score = 0 لن يبدأ نشر العدو في اللعبة قبل النقر على زر البداية. وطالما أننا لم نرتّب هذا الأمر بعد، فسننشره مباشرة: func _ready(): spawn_enemies() func spawn_enemies(): for x in range(9): for y in range(3): var e = enemy.instantiate() var pos = Vector2(x * (16 + 8) + 24, 16 * 4 + y * 16) add_child(e) e.start(pos) e.died.connect(_on_enemy_died) تنشر الشيفرة السابقة 27 عدوًا ضمن شبكة في النصف الأعلى من الشاشة، ولا بد من التأكد من وصل الإشارة died لكل عدو، ولهذا علينا إضافة الدالة التالية: func _on_enemy_died(value): score += value سنشغل الآن المشهد، وسنرى مجموعةً من الأعداء أعلى الشاشة يسقطون دوريًا نحو الأسفل. إطلاق النار من المركبات المعادية طالما أن مركبات العدو ستتمكن من إطلاق النار، فسنمنحها شيئًا لتطلق النار عليه. مشهد قذائف العدو سننشئ مشهدًا باسم EnemyBullet مشابهًا من ناحية التكوين لمشهد قذائف اللاعب. لن نكرر خطوات الإنشاء هنا طبعًا، لهذا يمكن العودة إلى مشهد قذائف اللاعب bullet في حال مصادفة مشكلة ما؛ إذ سيكون الفرق الوحيد هو الصورة المستخدمة للقذيفة في هذا المشهد، وهي الصورة Enemy_projectile (16 x 16).png، وسيختلف السكريبت هنا قليلًا: extends Area2D @export var speed = 150 func start(pos): position = pos func _process(delta): position.y += speed * delta نصل الإشارتين screen_exited و area_entered العائدتين للعقدتين VisibleOnScreenNotifier2D و Area2D على التوالي: func _on_visible_on_screen_notifier_2d_screen_exited(): queue_free() func _on_area_entered(area): if area.name == "Player": queue_free() ملاحظة: تُطلق الإشارة screen_exited عندما يخرج الكائن من الشاشة، وتُستخدم لحذفه أو إعادة ضبطه وتطلق الإشارة area_entered عندما يدخل جسم إلى منطقة محددة وهي تُستخدم للكشف عن الاصطدامات أو التفاعلات. وكما نلاحظ، نحن هنا نلتقط اصطدام القذيفة المعادية باللاعب، لكننا لم نحصل على النتيجة المطلوبة حتى اللحظة. سنعود إلى ذلك بعد إضافة طريقة تصف تضرر سفينة اللاعب عند إصابته. إضافة آلية إطلاق النار إلى العدو سنحمّل مشهد قذيفة العدو في أعلى سكريبت العدو: var bullet_scene = preload("res://enemy_bullet.tscn") ونعدّل بعد ذلك دالة إطلاق النار إلى النحو الآتي: func _on_shoot_timer_timeout(): var b = bullet_scene.instantiate() get_tree().root.add_child(b) b.start(position) $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start() سنشغّل الآن المشهد Main، وسنرى بعض قذائف العدو تهطل إلى الأسفل بين الفينة والأخرى. ختامًا بهذا نكون قد أتممنا إعداد مشهد العدو الذي يطلق النار في لعبتنا ثنائية الأبعاد، وسنتعرف على المزيد لتطوير لعبتنا في المقالات القادمة. ترجمة -وبتصرف- للمقالين: Enymies و Enemy Shooting. اقرأ أيضًا المقال السابق: إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد عبر محرك Godot بناء مشهد القذيفة وإطلاق النار في لعبة سفينة الفضاء تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot برمجة عدو وحيوان أليف في لعبة Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot
-
سنعمل في هذا المقال وهو جزء من سلسلة دليل جودو على إنشاء المشهد الرئيسي للعبتنا ثنائية الأبعاد، فقبل صناعة الأعداء أو تشغيل السفينة أو أي عمل آخر في اللعبة، سنحتاج إلى توفير مكان تتواجد فيه كل هذه الكائنات معًا، ويُدعى هذا المكان عادة في عالم الألعاب بمشهد المستوى Level أو المشهد الرئيسي Main Scene، وهذا الاسم هو ما سنعتمده في مقالنا. إنشاء الخلفية سنبدأ المشهد بعقدة من النوع Node2D ونسميها Main ثم نحفظ المشهد، بعدها نضيف عقدة ابن للعقدة Main من النوع Sprite2D ونسيمها Background لتمثل صورة الخلفية. نجعل بعد ذلك الصورة باسم Space_BG (2 frames) (64 x 64).png نقشًا خاصًا بها، بحيث ستساعد هذه الخلفية في تغطية كامل شاشة اللعبة عند الضبط الصحيح للإعدادات. تضم الصورة السابقة إطارين أبعاد كل منهما 64x64 ونريد أن نرصف الصورة على كامل خلفية المشهد، لهذا سنبدأ بضبط الإعدادات التالية: ننتقل إلى نافذة الفاحص Inspector بعد النقر على العقدة Background، ثم نضبط قيمة الخاصية Offset>Centerعلى القيمة off ليصبح موقع الزاوية العليا اليسارية للصورة هي مبدأ الإحداثيات وليس منتصف الشاشة نضبط قيمة الخاصية Region>Enabled على on، فتظهر مجموعة الخاصيات Rect؛ وهنا نضبط الارتفاع على 320 والعرض على 240 كي تتوسع الصورة لتغطي كامل الشاشة نضبط قيمة الخاصية Texture>Repeat على Enabled كي تكرر الصورة نفسها على كامل أبعاد نافذة اللعبة نضيف الآن مشهد اللاعب Player كمشهد فرعي إلى المشهد Main، وذلك بالنقر على زر إلحاق مشهد فرعي Instantiate Child Scene. تحريك الخلفية يمكن جعل المشهد أكثر ديناميكية عن طريق تحريك الخلفية، وذلك باستخدام الشيفرة عن طريق تغيير قيمة الخاصية region_rect عند كل إطار؛ مع ذلك سنستخدم العقدة AnimationPlayer، ولهذا نضيف واحدة إلى العقدة Main. تظهر اللوحة Animation تحت نافذة المحرر، وتعرض الكثير من المعلومات التي سنطلع على طريقة تنظيمها: سننقر الآن على زر التحريك Animation، ثم نضغط على الخيار تحريك جديد New Animation، ويمكننا تسمية الرسم المتحرك الجديد بالاسم Scroll. سنضبط بعد ذلك طول المقطع Length على 2 بتغيير قيمة مربع مدة الرسم المتحرك بالثواني ونفعّل خياري تكرار التحريك Looping و تشغيل تلقائي عند التحميل Autoplay بالنقر على زريهما. يعمل الرسم المتحرك عن طريق إضافة مسارات تمثل الخصائص التي تريد التحكم بها من خلال العقدة AnimationPlayer؛ وضمن الخط الزمني للاعب timeline، سنضيف مفاتيح مرجعية keyframes تعرّف القيم الجديدة للخاصيات في الوقت المحدد. يمكننا إضافة إطار أو مفتاح مرجعي Keyframe إلى الرسم المتحرك بالنقر على أيقونة المفتاح التي تُعرض إلى جانب كل خاصية من الخواص في نافذة الفاحص، وهنا قبل وضع الإطار المرجعي، يجب التأكد من أن المقبض الأزرق scrubber متواجد على الخط الزمني في نافذة التحريك على القيمة 0. نختار الآن العقدة Background، ثم ننتقل إلى الفاحص وننقر المفتاح إلى جانب الخاصية Region>Rect، وستظهر عندها رسالة تأكيد تخبرنا بأننا سننشئ مقطعًا جديدًا لخاصية وإضافة مفتاح لها، وسنرى عند تأكيد الرسالة مسارًا جديدًا أضيف إلى لوحة التحريك يحتوي على نقطة صغيرة تمثل المفتاح المرجعي الذي أضفناه. عند هذه المرحلة، سيكفي سحب المقبض إلى الزمن 2 ثم تغيير قيمة y للخاصية Region>Rect إلى 64. وعند النقر على زر تشغيل الرسم Play في لوحة التحريك، سنلاحظ كيف تتحرك الخلفية ببطئ خلف اللاعب. الخاتمة شرحنا في هذا المقال كيفية إعداد وتجهبز المشهد الرئيسي للعبتنا ثنائية الأبعاد وأضفنا الخلفية للمشهد وضبطناها بالطريقة المناسبة. الخطوة التالية التي علينا القيام بها هي إضافة كائنات الأعداء وإعداد طريقة تحركهم ضمن اللعبة، وهذا ما سنفعله في المقال التالي؛ إذ سننشئ مشهد عدو وحيد كما فعلنا مع القذائف في مقال سابق ونكررها في اللعبة عند الحاجة. ترجمة -وبتصرف- للمقال: Main Scene. اقرأ أيضًا المقال السابق: بناء مشهد القذيفة وإطلاق النار في لعبة سفينة الفضاء تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot برمجة عدو وحيوان أليف في لعبة Godot
-
انتهينا في المقال السابق من تحريك سفينة الفضاء، وسنتابع في هذا المقال الذي هو جزء من سلسلة دليل جودو بناء مشهد القذيفة أو الرصاصة وعملية إطلاق النار من قبل سفينة الفضاء التي تمثل اللاعب الأساسي في لعبتنا ثنائية الأبعاد، وسننفذها بشكل كائن قابل لإعادة الاستخدام. الكائنات القابلة لإعادة الاستخدام سيطلق اللاعب كمًا كبيرًا من القذائف مع تقدم اللعبة وجميعها أشياء متطابقة تمامًا، وبالتالي سنتحتاج إلى تنفيذ ما يلي: إظهار القذيفة أمام اللاعب تحريك القذيفة نحو الأمام حتى تغادر الشاشة التقاط حدث تصادم القذيفة مع الأعداء وطالما أن كل القذائف لها نفس الوظيفة، فسنوفر الكثير من العمل بتصميم نموذج أولي للطلقة واستخدامه كنقطة انطلاق في إنشاء العدد الذي نريده من النسخ المتطابقة، ونظام المشاهد في جودو مثالي لهذا الغرض. مشهد القذيفة سننشئ هنا مشهدًا جديدًا من خلال النقر على خيار المشهد Scene ثم على خيار مشهد جديد New Scene، أو بالنقر على الزر + في أعلى نافذة العرض. وكما فعلنا في مشهد اللاعب، لا بد من تحديد العّقد التي نحتاجها حتى تعمل القذيفة كما هو مطلوب. بإمكاننا استخدام العقدة مجددًا طالما أنها قادرة على التقاط حدث الاصطدام بجسم، وهذا يعني بدوره أننا سنحتاج إلى شكل تصادم وشخصية لعرض صورة القذيفة، كما سنحتاج إلى طريقة لاكتشاف خروج القذيفة خارج الشاشة حتى نزيلها تلقائيًا. وستكون العقد على النحو الآتي: - Area2D: Bullet - Sprite2D - CollisionShape2D - VisibleOnScreenNotifier2D سننقر على العقدة Sprite2D ثم نسحب الصورة Player_charged_beam (16 x 16).png من مدير الملفات إلى الخاصية Texture في نافذة الفاحص Inspector. وكما نلاحظ، توجد أكثر من نسخة للقذيفة، ولهذا سنضبط قيمة الخاصية Animation> Hframe على القيمة 2 كي نرى نسخةً واحدةً فقط في كل مرة. سنضبط الآن شكل التصادم CollisionShape2D كما فعلنا في المقال السابق مع شكل التصادم الخاص بعقدة اللاعب. سكريبت القذيفة سنلحق الآن سكريبتًا جديدًا بالعقدة Bullet ونبدأ بتحريك القذيفة كالآتي: extends Area2D @export var speed = -250 func start(pos): position = pos func _process(delta): position.y += speed * delta من المفترض أن يكون الأمر مألوفًا، فهو مشابه لسكريبت اللاعب، وقد غيرنا فقط position.y لأن الحركة ستكون شاقوليةً نحو الأعلى. يسمح لنا التابع ()start الذي عرّفناه بضبط موقع البداية position للقذيفة، لأن اللاعب سيتحرك باستمرار وينشر القذائف باتجاهات مختلفة. ربط الإشارات Signals نختار العقدة Bullet وننقر على النافذة الفرعية عقدة Node إلى جوار الفاحص: تعرض لقطة الشاشة السابقة جميع الإشارات Signals التي يمكن للعقدة المختارة أن تبثها. تخبرنا الإشارات في جودو أن شيئٍا ما قد حدث، وفي حالتنا سنستخدم الإشارة area_entered لتخبرنا متى تلامس فيها هذه القذيفة عقدةً أخرى من النوع Area2D. ولفعل ذلك، سنختار الإشارة area_entered، ثم ننقر على الزر توصيل Connect أسفل النافذة، بعدها ننقر على الزر وصل Connect في النافذة التي تظهر؛ إذ لسنا بحاجة إلى تغيير أي شيء فيها. سنلاحظ الآن أننا قد عدنا إلى محرر السكريبت، كما سنلاحظ وجود دالة جديدة في السكريبت bullet.gd. وإلى جانب الدالة، سنجد أيقونة اتصال خضراء لتدلنا على أن الإشارة متصلة بها. وستُستدعى هذه الدالة في كل مرة تلامس فيها هذه العقدة شيئًا ما. لنضف الآن هذه الشيفرة: func _on_area_entered(area): if area.is_in_group("enemies"): area.explode() queue_free() سنتحقق في هذه الشيفرة مما إذا كانت القذيفة قد أصابت عدوًا، بحيث إن فعلت ذلك، نطلب من العدو أن ينفجر، ومن ثم نمسح القذيفة مباشرةً. نكرر نفس الخطوات السابقة لوصل الإشارة screen_exited العائدة إلى العقدة VisibleOnScreenNotifier2D ونضيف الشيفرة التالية في الدالة التي توّلدها الإشارة: func _on_visible_on_screen_notifier_2d_screen_exited(): queue_free() إطلاق النار يقدم المشهد Bullet كائنًا قابلًا لإعادة الاستخدام يمكن نسخه في كل مرة يطلق فيها اللاعب النار. ولضبط عملية إطلاق النار، سنتبع ما يلي. إضافات إلى مشهد اللاعب لنعد إلى سكريبت اللاعب Player ونضيف بعض المتغيرات الجديدة: @export var cooldown = 0.25 @export var bullet_scene : PackedScene var can_shoot = true يساعدنا التوجيه export@ هنا في ضبط قيم المتغيرات ضمن نافذة الفاحص، وبالتالي سنكون قادرين على ضبط وقت الانتظار قبل إعادة إطلاق النار Cooldown Time. نعود الآن إلى مشهد اللاعب، ثم نغيّر قيمة المتغير bullet_scene بالنقر على الخاصية الموافقة في الفاحص، ثم نختار الملف bullet.tscn، أو بسحب المشهد من نافذة نظام الملفات وسحبه إلى الخاصية. يطلق المبرمجون على المتغير can_shoot تسمية الراية Flag، وهو متغير بولياني منطقي يتحكم بشرط معين. وفي حالتنا سيتحكم بإمكانية إطلاق اللاعب للنار أو لا، وستكون قيمة هذا المتغير في فترة الانتظار false. سنضيف تاليًا دالة ()start مشابهة للتي أضفناها إلى مشهد القذيفة، والتي تساعدنا على ضبط بعض القيم الابتدائية للاعب وإعادة ضبطها عندما تبدأ اللعبة من جديد. func _ready(): start() func start(): position = Vector2(screensize.x / 2, screensize.y - 64) $GunCooldown.wait_time = cooldown تضع الشيفرة السابقة اللاعب في أسفل ومنتصف الشاشة وهو مكان جيد للانطلاق، كما نضمن أن زمن الانتظار يأخذ القيمة المناسبة. تُستدعى الدالة ()shoot في أي وقت نضغط فيه على مفتاح الإطلاق الذي يتفعّل عن ضغط الزر Space: func shoot(): if not can_shoot: return can_shoot = false $GunCooldown.start() var b = bullet_scene.instantiate() get_tree().root.add_child(b) b.start(position + Vector2(0, -8)) تتحقق الدالة بدايةً مما إذا ما كان إطلاق النار مسموحًا، فإن لم يكن كذلك، فسيُنهى تنفيذ الدالة مباشرةً باستخدام الأمر return. وإن كان مسموحًا للاعب بإطلاق النار، فسنضبط قيمة الراية على false ونبدأ العد لفترة الانتظار. سننشئ بعذ ذلك نسخةً عن مشهد القذيفة ونضيفه إلى اللعبة، ونستدعي الدالة ()start الخاصة بالقذيفة للتأكد من وضعها في المكان المناسب فوق سفينة اللاعب. يمكن استدعاء هذه الدالة عندما يضغط اللاعب على مفتاح الإطلاق، لهذا، نضيف ما يلي داخل الدالة ()process_ بعد السطر ()position.clamp: if Input.is_action_pressed("shoot"): shoot() نصل أيضًا الإشارة timeout العائدة للعقدة GunCooldown: func _on_gun_cooldown_timeout(): can_shoot = true عندما تنتهي فترة الانتظار، يُسمح للاعب بإطلاق النار مجددًا؛ وهنا سنشغّل المشهد ونجرب التحرك وإطلاق النار. وكما هو ظاهر في الصورة أعلاه، فقد أضفنا قذائف جديدة كأبناء لشجرة المشاهد get_tree().root وليس إلى مشهد اللاعب، لأننا لو فعلنا ذلك فستكون القذائف أبناءً للسفينة وتتحرك مع حركتها. الخاتمة وهكذا نكون قد أنجزنا مشهد إطلاق النار كاملة في لعبتنا ثنائية الأبعاد، بدءًا من تصميم القذيفة وربط إشارات التصادم، وصولًا إلى تنفيذ آلية الإطلاق والتحكم بفترة التهدئة بين كل طلقة وأخرى، وتعلمنا كيفية استخدام نظام المشاهد في جودو لننشئ كائنات قابلة لإعادة الاستخدام، وكيفية بناء تفاعل ديناميكي بين اللاعب وبيئة اللعب. ترجمة-وبتصرف- للمقالين: Bullet Scene و Shooting. اقرأ أيضًا المقال السابق: كتابة شيفرة التحكم بسفينة الفضاء عبر محرك Godot تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot برمجة صاروخ موجه يتبع هدف متحرك عبر محرك الألعاب Godot
-
أعددنا في مقال سابق بيئة اللعبة ثم بنينا في المقال الذي يليه مشهد اللاعب، وسنتابع في هذا المقال الذي هو جزء من سلسلة دليل جودو العمل على لعبتنا ثنائية الأبعاد في محرك الألعاب جودو ونبدأ بكتابة شيفرة سفينة الفضاء التي يتحكم بها اللاعب. إضافة سكريبت إلى اللاعب يُبنى سلوك الكائنات وآليات اللعب عن طريق كتابة سكريبتات برمجية وإلحاقها بالعقد وبغيرها من الكائنات. وقد رأينا سابقًا كيف يعرض المشهد Player سفينة الفضاء ويُعرّف صندوق التصادم الذي يحيط بها وغيره من الخواص، لكنها لا تستطيع التحرك بعد، ولن يحدث شيء إذا اصطدم بها جسم. لهذا سنكتب شيفرة إضافة هذه الوظائف إلى السفينة. لفعل ذلك، سنختار العقدة Player ثم ننقر على خيار إلحاق نص برمجي Attach script. ليس علينا تغيير أي خيارات في نافذة إلحاق نص برمجي للعقدة، بل علينا النقر فقط على زر Attach Node Script، وسينتقل بنا المحرّك إلى محرر السكريبت. لنلق نظرةً إلى السطر الأول من السكريبت الذي أُضيف تلقائيًا: extends Area2D يعرّف هذا السطر نوع الكائن الذي يرتبط به السكريبت، وبالتالي سيكون السكريبت قادرًا على الوصول إلى جميع الوظائف التي تقدمها العقدة Area2D، ويجب أن يتطابق السطر دائمًا مع نوع العقدة التي تُلحق السكريبت بها. الوصول إلى السكريبتات لا يفعّل السكريبت الكثير بمفرده، بل يكتفي بتعريف وظائف إضافية في أي كائن ترتبط به فقط؛ إذ لن نحتاج أبدًا إلى الوصول إلى متغيرات في بعض السكريبتات، بل الوصول إلى خاصيات الكائن التي يُعرّفها السكريبت، وتمييز هذا الأمر مهم جدًا. تحريك السفينة سنبدأ الآن بتحريك السفينة حول الشاشة، وسنكتب شيفرة تنفّذ ما يلي: التقاط مفاتيح الإدخال التي يضغط عليها اللاعب تحريك السفينة في الاتجاه الذي يفرضه مفتاح الدخل @export var speed = 150 func _process(delta): var input = Input.get_vector("left", "right", "up", "down") position += input * speed * delta يمكّن التوجيه export@ المضاف قبل اسم المتغير في الشيفرة أعلاه من تعديل قيمته في نافذة الفاحص Inspector كما هو موضح بالصورة الآتية: أما باقي محتوى الشيفرة أعلاه، فيعني: تُستدعى الدالة ()process_ مرةً واحدة من قبل المحرّك عند تنفيذ كل إطار، وتُنفّذ الشيفرة التي تضمها يتحقق التابع ()Input.get_vector من حالة المفاتيح المضغوطة من بين المفاتيح الأربعة المخصصة للإدخال ويولّد شعاع مدخلات له نفس اتجاه الحركة نغيّر أخيرًا موقع السفينة position بإضافة شعاع المدخلات وتعديل قيمته إلى السرعة المطلوبة ثم نضربه بالمعامل delta سنشغّل المشهد الآن بالنقر على زر Run Current Scene ونحاول تحريك السفينة. البقاء ضمن الشاشة حتى الآن، إذا حاولنا الاستمرار في التحرك باتجاه محدد، فستغادر السفية شاشة اللعبة، ولهذا علينا تحديد قيمة الخاصية position كي تبقى السفينة داخل مربع الشاشة. ولحل هذه المشكلة سنضيف السطر التالي في أعلى السكريبت. @onready var screensize = get_viewport_rect().size يخبر التوجيه onready@ جودو ألا يضبط قيمة المتغيّر screensize حتى تدخل العقدة Player شجرة المشهد. ويعني ذلك حرفيّا ضرورة الانتظار حتى بداية اللعبة، لعدم وجود نافذة نتحرى أبعادها قبل أم تبدأ اللعبة. ستكون الخطوة التالية هي حصر موقع السفينة ضمن حدود المربع screensize باستخدام التابع ()clamp الذي يضمه الشعاع position كونه من النوع Vector2. func _process(delta): var input = Input.get_vector("left", "right", "up", "down") position += input * speed * delta position = position.clamp(Vector2.ZERO, screensize) بعدها نشغّل المشهد مجددًا ونحاول أن تحرك السفينة. سنلاحظ كيف تتوقف السفينة عند حواف الشاشة لكن نصفها يغادر الشاشة، وذلك لأن موقع السفينة position هو في مركزها أي مركز العقدة Sprite2D. وطالما أن أبعاد السفينة هي 16x16، فبإمكاننا تغيير مقدار الاقتصاص وزيادة ما مقداره 8 بكسل كالتالي: position = position.clamp(Vector2(8, 8), screensize - Vector2(8, 8)) ربط الرسم المتحرك بالاتجاه تتحرك السفية الآن كما هو مطلوب، وبإمكاننا اختيار صور مائلة للسفينة عند التحرك يمينًا أو يسارًا وكذلك صور مائلة للهب كي يعطي الحركة ديناميكية أكبر. وللتحقق من جهة الحركة، نستطيع التحقق من قيمة x لشعاع الإدخال input فيما لو كان موجبًا فيعطي حركة يمينًا، أو سالبًا فيعطي حركة يسارًا، أو صفر بمعنى عدم وجود أي حركة. والآن، سنطبق التغييرات على الخاصية frame للعقدة Sprite2D، وذلك باختيار إطار معين عند الحركة يمينًا أو يسارًا لتغيير شكل صورة السفينة؛ وعلى الخاصية animation للعقدة AnimatedSprite2D لتغيير شكل اللهب كما يلي: func _process(delta): var input = Input.get_vector("left", "right", "up", "down") if input.x > 0: $Ship.frame = 2 $Ship/Boosters.animation = "right" elif input.x < 0: $Ship.frame = 0 $Ship/Boosters.animation = "left" else: $Ship.frame = 1 $Ship/Boosters.animation = "forward" position += input * speed * delta position = position.clamp(Vector2(8, 8), screensize-Vector2(8, 8)) ختامًا بهذا نكون قد وصلنا إلى نهاية المقال الذي تعرفنا فيه على كيفية كتابة شيفرة التحكم بسفينة الفضاء عبر محرك Godot. يجب التأكد من أن كل شيء يعمل بشكل طبيعي كما هو مطلوب قبل الانتقال إلى الخطوة التالية التي سنشرحها في المقال التالي، والذي سننشئ فيها مشهد الرصاصة ونبرمج عملية إطلاق النار. ترجمة -وبتصرف- لمقال Coding the Player. اقرأ أيضًا المقال السابق: تصميم مشهد اللاعب في لعبة ثنائية الأبعاد باستخدام Godot إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو تحريك سفينة فضاء باستخدام RigidBody2D في جودو تعرف على العقد Nodes في محرك ألعاب جودو Godot
-
هيئنا في مقال إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو الإعدادات اللازمة للعبة مركبة الفضاء المقاتلة ونزلنا الأيقونات والصور اللازمة وأصبحنا جاهزين لتصميم مشهد مركبة الفضاء الخاضعة لتحكم اللاعب. إعداد مشهد سفينة الفضاء من الأقسام الشائعة في مجرى بناء ألعاب جودو هو إنشاء المشاهد. وكما رأينا سابقًا، يمثل المشهد في جودو مجموعة من العقد. وفي معظم ألعاب جودو، يُهيئ كل كائن في اللعبة على شكل مشهد يضم عقدًا تعطيه الوظائف المنوطة به، وقد بعض الشيفرة لتخصيص سلوك هذا الكائن. اختيار العقد علينا في البداية اتخاذ قرار بشأن العقد التي يجب أن نبدأ بها المشهد، حيث تًدعى العقدة الأولى التي تضيفها إلى المشهد بالعقدة الجذرية root node، وينبغي أن تكون هذه العقدة هي العقد الأساسية للمشهد بحيث تعرّف سلوك كائن اللعبة. وسنضيف بعض ذلك عقدًا أبناء إلى هذه العقدة معطيةً الكائن وظائفه. ما هي العقدة التي ستكونها السفينة إذًا؟ دعونا نحلل المتطلبات، ونرى ما هي العقد التي ستكون مفيدة في تحقيق تلك المتطلبات. تحتاج سفينة الفضاء إلى: الحركة في فضاء ثنائي البعد: تكفينا عقدة Node2D أساسية، وهي عقدة تمتلك خاصيات موقع position و دوران rotation وغيرها من خاصيات الفضاء 2D عرض صورة: وتناسب هذه الوظيفة العقدة Sprite2D، وهي أيضًا عقدة من النوع Node2D لهذا يمكن التحكم بها وتحريكها اكتشاف تصادمها بأجسام أخرى: سيتحرك الأعداء ويطلقون النار على السفينة، لذا علينا معرفة متى تُصاب السفينة. ستكون العقدة Area2D مثالية، إذ يمكنها اكتشاف التلامس مع كائنات أخرى ولها خاصيات موقع، وليس لها مظهر بحد ذاتها بالنظر إلى تلك المتطلبات سنجد أن Area2D هي من تقدم الوظيفة الأساسية وتعرض العقدة Sprite2D شكل السفينة وهذا كل ما نحتاجه. بناء المشهد ننقر على زر + أو زر عقدة أخرى Other Node في النافذة الفرعية التي يُطلَق عليها تسمية المشهد Scene، ونشرع بكتابة Area2D لتظهر لنا العقدة في القائمة، فنختارها. وعندما نرى العقدة في نافذة المشهد، سننقر على العقدة ونسمّيها Player، ثم نحفظ المشهد باستخدام الاختصار <Ctrl+S>. عرض سفينة الفضاء سنختار العقدة Player ثم نضيف عقدة Sprite2D ونسمّيها Ship حتى نبقي الأمور أكثر تنظيمًا؛ كما سنسحب بعد ذلك الأيقونة Player_ship (16x16).png من موقعها في نافذة الملفات إلى مربع الخاصية ملمس Texture في نافذة الفاحص Inspector. قد نلاحظ مباشرةً وجود ثلاثة مركبات في الصورة، والسبب أن الصورة تضم نسخًا من المركبة التي تتحرك يمينًا ويسارًا، لذا سنستخدم ذلك في خاصيات التحريك Animation من الفاحص. سننقر على خاصية التحريك Animation ثم على الخيار إطارات افقية Hframes ونضبطه على الرقم 3، وهكذا سنتنتقل بين النسخ الثلاث للصورة عند تغيير قيمة الخاصية إطار Frame. سنترك الآن قيم هذه الخاصية على 1. إضافة شكل التصادم سنلاحظ وجود إشارة تحذير صفراء إلى جانب العقدة Area2D، بحيث إذا نقرنا عليها سنجد أنها تنبهنا بأن العقدة لا شكل لها، ولهذا علينا تعريف شكل لها. سنعرّف شكل العقدة بإضافة عقدة أخرى من النوع CollisionShape2D كابن للعقدة Player. سنجد في نافذة الفاحص عند النقر على العقدة CollisionShape2D الخاصية Shape التي تأخذ قيمة افتراضية فارغة <empty>، وبالنقر على الصندوق المجاور، ستظهر قائمة بكل الأشكال المتاحة، سنختار منها New RectangleShape2Dوسنجد مربعًا باللون الأزرق الفاتح قد أحاط بالسفينة. بإمكاننا ضبط حجم الشكل الذي ظهر بسحب الدوائر البرتقالية وتحريكها، أو بالنقر على النقر زر السهم إلى جوار صندوق عقدة الشكل في نافذة الفاحص، ثم اختيار shape ووضع الأبعاد يدويًا. العادم Exhaust ستبدو السفينة أكثر ديناميكيةً وواقعيةً في حال أضفنا بعض الحركة إليها، وسنجد في مجلد الأيقونات بعض الرسوميات التي تمثل العادم أو اللهب الناري الذي يخرج من محركات السفينة ولها الاسم Boosters. سنجد أيضًا ثلاث أيقونات لنسخ أيقونات السفينة الثلاث هي اليمين واليسار والأمام. ولعرض هذه الأيقونات، سنختار العقدة Ship ونضيف إليها عقدة من النوع AnimatedSprite2D ونسميها Boosters. سننتقل بعد ذلك إلى الفاحص ومن ثم إلى شجرة التحريك Animation، وبعدها إلى الخاصية SpriteFrames التي تأخذ افتراضيًا قيمة فارغة <empty>، وننقر عليها لإضافة إطار رسومي جديد New SpriteFrames لننقر بعدها على الخيار SpriteFrames لفتح لوحة التحريك أسفل نافذة المحرر. سننقر الآن نقرًا مزدوجًا على العنصر default ونغير اسمه إلى forward. ولإضافة صور الرسوم المتحركة الآن، علينا النقر على زر إضافة إطارات من الملخص Add frames from sprite sheet الموضح في الصورة التالية بموضع مؤشر الفأرة: سنختار الآن الصورة Boosters (16 x 16).png وستظهر لنا نافذة تحديد الإطارات Select frames مباشرةً كي نختار الإطارات التي نريد. هناك إطاران فقط في الصورة، لكن الشبكة غير متناسقة، لذا سنغيّر قيمة الخاصية Size لتتلائم مع أبعاد الصورة 16x 16، ثم ننقر على كلا الإطارين لاختيارهما وبعدها على زر إضافة إطارين (Add 2 Frame(s. والآن، بعد أن أضفنا الإطارين، سننقر على زر التشغيل Play لتحريك الرسوم، وبإمكاننا أيضًا تفعيل الخيار Autoplay on Load كي يجري تحريك الصورة تلقائيًا. قد نجد هنا أن سرعة تغير اللهب بطيئة، لهذا يمكننا تعديل السرعة لتصبح 5FPS، بعدها سنكرر تنفيذ الخطوات السابقة لإضافة رسم متحرك للهب العادم عندما تتحرك إلى اليمين واليسار ونسميهما left and right. التحكم بسرعة الإطلاق على العدو آخر ما سنفعله لإكمال شخصية اللاعب هو إضافة عقدة Timer للتحكم بسرعة إطلاقه للنار، حيث سنضيف هذه العقدة كابن للعقدة Player ونسميها GunCooldown، وبعدها سنفعّل الخاصية One Shot في نافذة الفاحص كي لا يُفعّل المؤقت تلقائيًا عندما ينتهي من العد. سنفعّل المؤقت برمجيًا في الشيفرة عندما يطلق اللاعب النار، ولن يتمكن من إعادة الإطلاق مجددًا حتى ينتهي المؤقت من العد. الخاتمة إلى هنا نكون قد انتهينا من إعداد مشهد اللاعب في لعبتنا ثنائية الأبعاد في جودو، إذ أضفنا عقدًا تعطي سفينة اللاعب الوظائف التي تحتاجها في اللعبة، وسنضيف في المقال التالي الشيفرة البرمجية اللازمة للتحكم بالسفينة وإطلاق النار، واكتشاف تصادمها مع كائنات أخرى في اللعبة. ترجمة -وبتصرف- للمقال: Designing the Player Scene. اقرأ أيضًا إنشاء لعبة سفينة فضاء ثنائية الأبعاد في جودو تحريك سفينة فضاء باستخدام RigidBody2D في جودو برمجة صاروخ موجه يتبع هدف متحرك عبر محرك الألعاب Godot
-
مع تقدمنا في تعلم لغة HTML من خلال قراءة مصادر متنوعة وتطبيق مختلف الأمثلة وبناء مشاريعنا الخاصة، سنرى باستمرار سمة مشتركة، وهي استخدام المعنى الدلالي Semantic أو الدلالة التي تحملها عناصر HTML، والتي تُدعى أحيانًا POSH وهي اختصار لعبارة الدلالة الصرفة القديمة للغة HTML أو بالإنجليزية Plain Old Semantic HTML. ويعني ذلك استخدام عناصر HTML للغايات التي صممت لأجلها قدر الإمكان. قد تكون أهمية هذا الموضوع غريبة نظرًا لإدراكنا أننا باستخدام CSS وجافا سكريبت نستطيع تغيير سلوك عناصر HTML بالطريقة التي نشاء. بحيث يمكننا مثلًا استخدام الشيفرة التالية لتشغيل فيديو ضمن موقعنا وتتولى CSS وJavaScript باقي التعديلات: <div>Play video</div> لكن سنرى لاحقًا وبتفاصيل أوفى أنه من المنطقي استخدام العنصر المناسب بمكانه المناسب لتنفيذ الأمر، وهو الزر الحقيقي <button>، على النحو الآتي: <button>Play video</button> وللتوضيح، لا تقتصر فائدة العناصر <button> على تنسيقها الملائم الذي يُطبق افتراضيًا، لكنها تدعم سهولة الوصول عبر لوحة المفاتيح؛ إذ يمكن للمستخدم التنقل بين الأزرار باستخدام المفتاح Tab وتفعيل الزر المختار من خلال المفتاح Space أو Return أو Enter. الجيد في الأمر أن كتابة شيفرة HTML دلالية لا يستغرق وقتًا وجهدًا أكبر من كتابة الشيفرة غير الدلالية التي تُعَد ممارسةً سيئةً لسهولة الوصول؛ كما أن الشيفرة الدلالية تقدم ميزات تتعدى سهولة الوصول: أسهل تطويرًا: فكما ذكرنا، تقدم العناصر الدلالية وظائف مضمنة قد تكون أكثر وضوحًا وفهمًا أفضل للأجهزة المحمولة: فقد تكون أقل حجمًا وأسرع من العناصر غير الدلالية، كما تسهّل التصميم المتجاوب لصفحات الويب أفضل لتحسين محركات البحث SEO: تعطي محركات البحث أهمية أكبر للكلمات المفتاحية الموجودة ضمن العناوين الرئيسية والروابط مقارنةً بالكلمات المفتاحية الموجودة في عناصر غير دلالية مثل العناصر <div> مثلًا؛ ولهذا سيكون البحث عن موقعنا أسهل. سنتابع في بقية الفقرات استكشاف ميزات سهولة الوصول التي تقدمها HTML. ملاحظة: من الأفضل تثبيت قارئ شاشة على الحاسوب لاختبار الأمثلة التي نعرضها. دلالات عناصر HTML تحدثنا سابقًا عن أهمية دلالات العناصر ولماذا علينا استخدام عناصر HTML الصحيحة في مشاريعنا. وعلينا أن لا نتجاهل ذلك أبدًا لأنها النقاط الأساسية التي تُخرق فيها معايير سهول الوصول إن لم تُعالج بالطريقة الصحيحة. سنجد في عالم الويب الحقيقي أشخاصًا يكتبون شيفرات غريبة باستخدام HTML؛ فبعضهم يسيء استخدام اللغة بتطبيق ممارسات قديمة لم تُنسى بعد، ويفعلها آخرون لجهلهم؛ لكن أيًا يكن السبب، لا بد من استبدال تلك الشيفرات السيئة. قد لا نستطيع التخلص في بعض الأحيان من تلك الشيفرات بسهولة، فربما كانت ضمن صفحات يولّدها الخادم تلقائيًا وليس لدينا تحكم كامل بها، أو نظرًا لاستخدام محتوى يقدمه طرف خارجي مثل اللوحات الإعلانية. المحتوى النصي من أكثر الأشياء التي تعزز الوصول السهل لمستخدمي قارئات الشاشة هو المحتوى المهيكل بطريقة ممتازة ضمن عناوين رئيسية وفقرات وقوائم وهكذا. ويوضح المثال التالي مثالًا ممتازًا لاستخدام العناصر الدلالية: <h1>My heading</h1> <p>This is the first section of my document.</p> <p>I'll add another paragraph here too.</p> <ol> <li>Here is</li> <li>a list for</li> <li>you to read</li> </ol> <h2>My subheading</h2> <p> This is the first subsection of my document. I'd love people to be able to find this content! </p> <h2>My 2nd subheading</h2> <p> This is the second subsection of my content, which I think is more interesting than the last one. </p> هناك أيضًا نسخة مطولة من هذا المثال متاحة تجربها مع قارئ للشاشة. وإذا حاولنا التنقل ضمنها، سنلاحظ سهولة الأمر. ينطق قارئ الشاشة كل عنوان رئيسي عندما نتقدم في المحتوى وينبهنا إلى العناوين والفقرات وغيرها يقف قارئ الشاشة بعد كل عنصر، مما يسمح لنا بالتنقل بخطوات تناسبنا ضمن المحتوى بإمكاننا التنقل بين العنوان السابق والتالي في العديد من قارئات الشاشة بإمكاننا الحصول على قائمة بكل العناوين الأساسية في العديد من قارئات الشاشة لنستخدمها كجدول مساعد لإيجاد محتوى محدد يكتب المطورون أحيانًا العناوين الرئيسة والفقرات باستخدام عناصر السطر الجديد <br> وإضافة عناصر HTML للتنسيق فقط كالتالي: <span style="font-size: 3em">My heading</span> <br /><br /> This is the first section of my document. <br /><br /> I'll add another paragraph here too. <br /><br /> 1. Here is <br /><br /> 2. a list for <br /><br /> 3. you to read <br /><br /> <span style="font-size: 2.5em">My subheading</span> <br /><br /> This is the first subsection of my document. I'd love people to be able to find this content! <br /><br /> <span style="font-size: 2.5em">My 2nd subheading</span> <br /><br /> This is the second subsection of my content. I think is more interesting than the last one. ولو جرّبنا النسخة المطولة من هذا المثال باستخدام قارئ شاشة فلن نجدها تجربةً جيدة، إذ لن يجد قارئ الشاشة أية إشارات أو دلالات ليستخدمها، كما لن نتمكن من استخلاص جدول بالمحتويات, وسيرى القارئ صفحتنا على شكل كتل كبيرة، بالتالي سيقرؤها دفعةً واحدة. هنالك أيضًا مشاكل أخرى أبعد من سهولة الوصول، إذ من الصعب تنسيق المحتوى لاحقًا باستخدام CSS أو التعامل معه باستخدام جافاسكريبت لعدم وجود عناصر لاستخدامها كمحددات Selectors. يمكن أن تؤثر اللغة التي نستخدمها على سهولة الوصول، ولهذا علينا كتابتها بطريقة واضحة بعيدة عن التعقيد، ودون استخدام مصطلحات أو لهجات محلية، لذا لا بد من تفادي استخدام لغة ومحارف لا يمكن لقارئات الشاشة نطقها بسهولة فمثلًا: لا نستخدم الشرطات إن أمكن تحاشيها. فبدلًا من كتابة 5-7 تكتب من 5 إلى 7 نكتب الاختصارات بعبارتها الكاملة، فبدلًا من كتابة Jan نكتب January نكتب اختصار عبارة بالكامل مرة أو مرتين على الأقل، ثم نستخدم العنصر <abb> لوصفه تخطيط الصفحة استخدم المطورون قديمًا جداول HTML لتخطيط صفحات الويب، واستخدموا خلايا الجدول لتضم الترويسة والتذييل والأشرطة الجانبية وأعمدة المحتوى الرئيسي. وبطبيعة الحال، لا يُعَد ذلك الآن أمرًا مقبولًا لأن قارئ الشاشة سيقرأ المحتوى غالبًا بطريقة مربكة، خاصةً إن كان التخطيط معقدًا ويضم عدة جداول متداخلة. لنجرّب المثال table-layout.html الذي يبدو مشابهًا للشيفرة التالية: <table width="1200"> <!-- main heading row --> <tr id="heading"> <td colspan="6"> <h1 align="center">Header</h1> </td> </tr> <!-- nav menu row --> <tr id="nav" bgcolor="#ffffff"> <td width="200"> <a href="#" align="center">Home</a> </td> <td width="200"> <a href="#" align="center">Our team</a> </td> <td width="200"> <a href="#" align="center">Projects</a> </td> <td width="200"> <a href="#" align="center">Contact</a> </td> <td width="300"> <form width="300"> <label >Search <input type="search" name="q" placeholder="Search query" width="300" /> </label> </form> </td> <td width="100"> <button width="100">Go!</button> </td> </tr> <!-- spacer row --> <tr id="spacer" height="10"> <td></td> </tr> <!-- main content and aside row --> <tr id="main"> <td id="content" colspan="4"> <!-- main content goes here --> </td> <td id="aside" colspan="2" valign="top"> <h2>Related</h2> <!-- aside content goes here --> </td> </tr> <!-- spacer row --> <tr id="spacer" height="10"> <td></td> </tr> <!-- footer row --> <tr id="footer"> <td colspan="6"> <p>©Copyright 1996 by nobody. All rights reversed.</p> </td> </tr> </table> لو حاولنا التنقل بين عناصر المثال عبر قارئ شاشة، فقد يخبرنا بوجود جدول علينا إلقاء نظرة عليه، وعندها من المحتمل وفقًا للقارئ الذي نستخدمه أن ننزل للأسفل لتفقد الجدول والنظر إلى ما يقدمه بشكل منفصل وغير مترابط ومن ثم الخروج من الجدول لمتابعة التنقل ضمن المحتوى. يعود التخطيط القديم للجدول إلى الأيام التي لم تدعم فيها المتصفحات لغة CSS كفاية؛ أما الآن، فهو مصدر لإرباك التقنيات المساعدة مثل قارئات الشاشة. إضافةً إلى ذلك، تحتاج شيفرته المصدرية إلى الكثير من الأسطر، مما يجعله أقل مرونةً وأصعب صيانةً. ولنقف على صحة ما نقوله، لا بد من مقارنة تجربتنا للمثال السابق مع مثال يستخدم هيكلية حديثة لصفحة الويب، والذي قد يبدو بالشكل التالي: <header> <h1>Header</h1> </header> <nav> <!-- main navigation in here --> </nav> <!-- Here is our page's main content --> <main> <!-- It contains an article --> <article> <h2>Article heading</h2> <!-- article content in here --> </article> <aside> <h2>Related</h2> <!-- aside content in here --> </aside> </main> <!-- And here is our main footer that is used across all the pages of our website --> <footer> <!-- footer content in here --> </footer> إن جربنا النسخة ذات الهيكلية الحديثة على صفحة الويب باستخدام قارئ شاشة، فلن نلاحظ حينها تداخلًا أو اختلاطًا عند قراءة المحتوى؛ كما أن شيفرته أقل حجمًا، مما يعني وجود سهولة في صيانة الشيفرة، واستهلاكًا أقل لحزم البيانات، وبالتالي فائدة مضاعفة، خاصةً لمن يعاني من انترنت بطيء. من الأشياء التي يجب أخذها أيضًا بالحسبان عند إنشاء تخطيط الصفحة، استخدام عناصر HTML دلالية كما عرضنا في مثال سابق؛ إذ بإمكاننا استخدام تخطيط باستخدام عناصر <div> متداخلة، لكن من الأفضل استخدام عناصر تقسيم الصفحة للتغليف مثل <nav> و <footer> وعناصر تكرار المحتوى مثل <article>، مما يعطي قيمةً دلالية أكبر لقارئ الشاشة وغيره من التقنيات، ويقدم إشارات أوضح عن المحتوى الذي ينتقل إليه المستخدم. ملاحظة: إضافةً إلى القيمة الدلالية الجيدة، والتخطيط الجذاب، لا بد من ترتيب عناصر المحتوى منطقيًا في الشيفرة المصدرية. بإمكاننا طبعًا وضعها في المكان الذي نريد لاحقًا باستخدام CSS، لكن لا بد من البدء بالترتيب المنطقي للعناصر حتى يكون ما تقرؤه قارئات الشاشة أوضح وأكثر منطقيةً لمستخدميها. عناصر تحكم واجهة المستخدم نقصد بعناصر تحكم واجهة المستخدم UI بالأجزاء الرئيسية التي يتفاعل معها المستخدم في صفحة الويب وأكثرها شيوعًا الأزرار والروابط واستمارات الويب. سنناقش في هذا القسم بعض النقاط الأساسية التي يجب الانتباه لها عند إنشاء عناصر التحكم لدعم سهولة الوصول؛ كما سنناقش في مقال تالٍ اعتبارات أخرى تتعلق بهذه العناصر. من ميزات سهولة الوصول الافتراضية الخاصة بعناصر التحكم هي إمكانية التعامل معها عن طريق لوحة المفاتيح في معظم المتصفحات. بإمكاننا تجريب الأمر من خلال المثال native-keyboard-accessibility.html، والذي يمكن إلقاء نظرة على شيفرته المصدرية). نفتح المثال في نافذة جديدة للمتصفح ونجرّب الضغط على المفتاح Tab، وسنجد بعد عدة ضغطات أن تركيز الدخل سينتقل من عنصر إلى آخر. ولكل عنصر يكتسب تركيز الدخل تظليل مميز يختلف من متصفح لآخر من أجل تمييز أن هذا العنصر قد اكتسب تركيز الدخل. ملاحظة: بإمكاننا تفعيل طبقة تعرض ترتيب التنقل بين العناصر باستخدام Tab من خلال أدوات مطوري ويب في متصفحنا. عندما يكتسب العنصر تركيز الدخل، سنتمكن من النقر على الزر أو على الرابط بالضغط على المفتاح Enter/Return أو الكتابة ضمن العنصر. ولعناصر استمارات الويب المختلفة عناصر تحكم مختلفة، إذ يضم العنصر <select> مثلًا عناصر <option> يمكن التنقل بينها من خلال مفاتيح الأسهم (أعلى وأدنى). ملاحظة: تختلف خيارات التحكم بالعناصر عبر لوحة المفاتيح من متصفح لآخر. وهذا السلوك الافتراضي الذي يدعم سهولة الوصول سيكون جاهزًا للاستخدام دون أي عناء، فقط باستخدام العنصر الصحيح، أي باستخدام عناصر مثل الأزرار والروابط والاستمارات بما في ذلك العناصر <label>. <h1>Links</h1> <p>This is a link to <a href="https://www.mozilla.org">Mozilla</a>.</p> <p> Another link, to the <a href="https://developer.mozilla.org">Mozilla Developer Network</a>. </p> <h2>Buttons</h2> <p> <button data-message="This is from the first button">Click me!</button> <button data-message="This is from the second button">Click me too!</button> <button data-message="This is from the third button">And me!</button> </p> <h2>Form</h2> <form> <div> <label for="name">Fill in your name:</label> <input type="text" id="name" name="name" /> </div> <div> <label for="age">Enter your age:</label> <input type="text" id="age" name="age" /> </div> <div> <label for="mood">Choose your mood:</label> <select id="mood" name="mood"> <option>Happy</option> <option>Sad</option> <option>Angry</option> <option>Worried</option> </select> </div> </form> مع ذلك، يستخدم المطورون أحيانًا HTML بطريقة غريبة، فقد نجد أحيانًا أزرارًا مكتوبةً باستخدام عناصر <div> كما في المثال التالي: <div data-message="This is from the first button">Click me!</div> <div data-message="This is from the second button">Click me too!</div> <div data-message="This is from the third button">And me!</div> طبعًا، لا يُنصح باستخدام HTML بهذا الشكل، إذ سنخسر الوصول السهل عبر لوحة المفاتيح مباشرةً، والتي يمكن أن نضمنها لو استخدمنا العنصر <button>، كما لن نحصل أيضًا على تنسيق CSS افتراضي. في الحالات النادرة -ومستحيلة الوجود- التي نُضطر فيها إلى ذلك، علينا استخدام الخاصية role=button مع عناصر أخرى لكي نجعلها على هيئة أزرار، وعلينا إنجاز جميع وظائف الأزرار بنفسنا، بما في ذلك التفاعل مع الفأرة ولوحة المفاتيح. بناء دعم لسهولة الوصول عبر لوحة المفاتيح يحتاج مثل هذا الدعم إلى القليل من العمل، ويمكن لأجل ذلك الاطلاع على المثال fake-div-buttons.html، وإلقاء نظرة على شيفرته المصدرية)، فكما يبدو مُنحت الأزار المزيفة <div> إمكانية التقاط تركيز الدخل بما في ذلك حالة استخدام المفتاح Tab، وذلك عن طريق ضبط قيمة الخاصية tabindex لكل زر على القيمة "0"، كما أضيفت الخاصية "role="button لتتعرف قارئات الشاشة عليها عند اكتسابها تركيز الدخل أو عند التفاعل معها: <div data-message="This is from the first button" tabindex="0" role="button"> Click me! </div> <div data-message="This is from the second button" tabindex="0" role="button"> Click me too! </div> <div data-message="This is from the third button" tabindex="0" role="button"> And me! </div> إن الوظيفة الأساسية للخاصية tabindex هي السماح بتغيير ترتيب الانتقال بين العناصر الأصلية التي تدعم التنقل باستخدام المفتاح Tab على شكل قيم عددية موجبة، بدلًا من طريقة التنقل الافتراضية وفقًا لموقعها في الشيفرة المصدرية. ويُعد هذا الأسلوب خاطئًا في معظم الحالات، لأنه قد يسبب اختلاطًا حقيقيًا؛ لذا من المهم استخدامه حصرًا عند الحاجة له، مثل الحالة التي يعرض فيها تخطيط صفحة الويب العناصر بطريقة بصرية مختلفة تمامًا عن ترتيب ظهورها في الشيفرة المصدرية، وأردنا أن يكون التنقل منطقيًا أكثر. وهناك خياران للخاصية tabindex: "tabindex="0: تتيح للعناصر التي لا تدعم التنقل باستخدام المفتاح Tab أن تدعم، وهي القيمة الأكثر أهمية "tabindex="-1: تتيح للعناصر التي لا تدعم التنقل باستخدام المفتاح Tab إمكانية استقبال تركيز الدخل برمجيًا باستخدام جافاسكريبت مثلًا، أو كهدف لرابط تشعّبي ومع أن النسخة السابقة من المثال ستدعم التنقل بين الأزرار، إلا أنها لن تسمح لنا بتفعيل هذه الأزرار بالضغط على المفتاح Enter/Return. ولحل هذه المسألة، لا بد من استخدام جافاسكريبت كما يلي: document.onkeydown = (e) => { // The Enter/Return key if (e.key === "Enter") { document.activeElement.click(); } }; وشرح ما فعلناه، فقد أضفنا هنا مترصد أحداث للكائن document لالتقاط الضغط على أي مفتاح من لوحة المفاتيح، ثم نتحقق من المفتاح الذي ضغطه المستخدم الخاصية key لكائن الحدث. فإن كان المفتاح المضغوط Enter/Return، معناها ننفذ الدالة المخزّنة في المعالج onclick من خلال العبارة ()document.activeElement.click؛ إذ يعطينا الكائن activeElement العنصر الحالي الذي تلقى تركيز الدخل في الصفحة. هناك الكثير من الأشياء التي يجب علينا الاهتمام بها عند بناء مثل هذه الوظائف، إذ ترتبط بها مشاكل عدة، ولهذا من الأفضل استخدام العناصر المناسبة للعمل المناسب في المقام الأول. نصوص واضحة ومعبرة للعناوين للعناوين التي ترتبط بعناصر تحكم واجهة المستخدم فوائد كثيرة لجميع المستخدمين، لكن استخدامها بالطريقة الصحيحة له أهمية خاصة بالنسبة لذوي الإعاقة. علينا في البداية أن نتأكد من أن النص المرافق للزر أو الرابط مفهوم ومميز، بحيث لا نستخدم مثلًا عبارات مثل انقر هنا Click here كعناوين، فقد تكون هناك قائمة من الأزرار يصعب على مستخدمي قارئات الشاشة تتبعها بهذا الشكل. تعرض لقطة الشاشة التالية قائمة بعناصر التحكم في مثالنا وضعها قارئ الشاشة VoiceOver على ماك أو اس: نتأكد هنا من أن العناوين التي نضعها منطقية حتى لو خارج سياق ورودها، بحيث يمكن أن تُفهم عند قراءتها ضمن سياقها المخصص أو خارجه. تعرض الشيفرة التالية مثلًا عن نص جيد لرابط تشعبي: <p> Whales are really awesome creatures. <a href="whales.html">Find out more about whales</a>. </p> لكن التالي يْعَد مثالًا سيئًا: <p> Whales are really awesome creatures. To find out more about whales, <a href="whales.html">click here</a>. </p> ملاحظة: بإمكاننا الإطلاع على ممارسات جيدة وسيئة من خلال المثالين good-links.html و bad-links.html. تُعد عناوين عناصر التحكم في استمارات الويب مهمة، فهي تقدم تلميحات عما يجب إدخاله ضمن كل عنصر إدخال. وفي ما يلي مثال منطقي عن الأمر: Fill in your name: <input type="text" id="name" name="name" /> لكن هذا الأمر ليس ذا أهمية لذوي الإعاقة، فلا شيء يربط العنوان بوضوح بعنصر الإدخال النصي ويوضح كيفية ملئه إن لم تكن رؤيته ممكنة. ولو جربنا المثال باستخدام قارئات الشاشة، فقد نحصل فقط على وصف لعناصر الإدخال يقول عدل النص edit text. أما المثال التالي، فيقدم حلًا أفضل: <div> <label for="name">Fill in your name:</label> <input type="text" id="name" name="name" /> </div> يرتبط العنوان في هذه الشيفرة بعنصر الإدخال بوضوح، وسيكون وصف قارئ الشاشة على الشكل " أدخل اسمك: عدل النص Fill in your name: edit text". وكفائدة إضافية، سيمنحنا ربط عنوان بعنصر إدخال، إمكانية النقر على العنوان لتفعيل عنصر الإدخال، مما يمنحنا مساحةً واسعةً لتنقر فيها ويسهل اختيار العنصر. ملاحظة: يمكن الإطلاع على ممارسات جيدة وسيئة عند بناء الاستمارات من خلال المثالين good-form.html و bad-form.html. سهولة الوصول إلى جداول البيانات يمكن كتابة جدول بيانات بسيط من خلال شيفرة HTML بسيطة كالتالي: <table> <tr> <td>Name</td> <td>Age</td> <td>Pronouns</td> </tr> <tr> <td>Gabriel</td> <td>13</td> <td>he/him</td> </tr> <tr> <td>Elva</td> <td>8</td> <td>she/her</td> </tr> <tr> <td>Freida</td> <td>5</td> <td>she/her</td> </tr> </table> لكن المشكلة في هذه الشيفرة هي عدم قدرة قارئات الشاشة على ربط الصفوف بالأعمدة لتجميع البيانات. ولفعل ذلك، لا بد من تحديد سطور الترويسة Header Rows إن كانت في أعلى الأسطر أو الأعمدة، وهكذا. بطبيعة الحال، لا يمكن تنفيذ ذلك على الجدول السابق إلا مرئيًا. ولأجل ذلك، يمكن إلقاء نظرة على المثال bad-table.html وتجربه، ثم الاطلاع على المثال punk bands table example والذي سنرى فيه بعض النقاط التي تدعم سهول الوصول. عُرِّفت ترويسات الجدول من خلال عناصر <th> ويمكن تحديد ما إذا كانت ترويسات لأسطر أو أعمدة من خلال الخاصية scope. وهذا ما يعطي مجموعةً مترابطةً من البيانات التي يمكن لقارئات الشاشة تقديمها ككتلة واحدة. للعنصر <table> والخاصية summary نفس العمل، وهو تقديم نص بديل عن الجدول، مما يمنح مستخدمي قارئ الشاشة وصفًا سريعًا لمحتويات الجدول. ويُفضَّل عادةً العنصر <caption>، لأنه يتيح للمستخدمين الطبيعيين القدرة على الوصول إلى محتواه أيضًا، فقد يجدونه مفيدًا، ولن نحتاج في الواقع إلى كلا العنصرين. النصوص البديلة يُعَد الوصول السهل إلى المحتوى النصي محققًا دائمًا، لكن الوضع ليس كذلك بالنسبة للوسائط المتعددة؛ إذ لا يمكن رؤية الصورة والفيديو من قبل ذوي الإعاقات البصرية، ولن يستطيع ذوو الإعاقات السمعية فهم المحتوى الصوتي. لن نغطي هذا الموضوع بهذا المقال، لكننا سنلقي نظرةً على سهولة الوصول إلى عنصر الصور <img> فقط. لأجل ذلك، يمكن الاطلاع على هذا المثال accessible-image.html الذي يقدم أربعة نسخ عن نفس الصورة: <img src="dinosaur.png" /> <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth." /> <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth." title="The Mozilla red dinosaur" /> <img src="dinosaur.png" aria-labelledby="dino-label" /> <p id="dino-label"> The Mozilla red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth. </p> وكما هو واضح، عندما تُعرض الصورة الأولى من خلال قارئ شاشة، فلن تقدم تلك الفائدة الحقيقية لمستخدمه، إذ سيقرأ مثلًا برنامج VoiceOver الصورة على النحو: "dinosaur.png, image"، أي يقرأ فقط عنوان ملف الصورة. وكل ما يمكن للقارئ فهمه هو أن الصورة عن ديناصور ما، لكن وبما أن الملفات قد تُحمَّل بأسماء توّلد آليًا من قبل كاميرا رقمية مثلًا، فلن تقدم هذه الأسماء أية فائدة لتوضيح محتوى الصورة. ملاحظة: لهذا لا ينبغي أبدًا إضافة محتوى نصي ضمن صورة، إذ لن تصل إليه قارئات الشاشة؛ وللأمر أيضًا مساوئ عدة، فلن نتمكن مثلًا من نسخه ولصقه في مكان آخر. عندما يصل قارئ الشاشة إلى الصورة الثانية، سيقرأ مايلي: A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth تشير هذه الحالة إلى أهمية استخدام نص بديل حتى لو كان اسم ملف الصورة معبرًا، لهذا من المهم التأكد من استخدام الخاصية alt قدر الإمكان، مع الانتباه إلى أن محتوى هذه الخاصية سيقدم وصفًا للصورة وما تعرضه؛ كما يجب أن يكون الوصف مختصرًا ومعبرًا ويوصل كل المعلومات التي تعرضها الصورة، والتي لا تتكرر في المحتوى النصي المحيط بها. يختلف محتوى الخاصية alt لنفس الصورة وفق السياق؛ فإن كانت صورة هي لجرو صغير Fluffy مثلًا بجانب مراجعة عن طعام كلاب، فسيكون المحتوى "alt="fluffy كافيًا في هذا السياق، لكن إن كانت ضمن صفحة لتبني الكلاب، فسيختلف حينها محتوى الخاصية alt لتلائم السياق، وذلك بتقديم وصف لهذه الصورة مثل alt="Fluffy, a tri-color terrier with very short hair, with a tennis ball in her mouth." بعد التأكد من عدم تكرار نفس الوصف في المحتوى النصي الذي يحيط بالصورة. وطالما أن المحتوى النصي الذي يحيط بالصورة قد يعرض فصيلة الكلب وحجمه، فلا حاجة لوضعها ضمن محتوى alt، لكن قد لا يعرض المحتوى النصي معلومات عن طول شعر الكلب ولونه وألعابه المفضلة مثلًا، فقد وضعت ضمن محتوى alt. لا بد من نقل المعلومات التي توصلها الصورة إلى المستخدم المبصر وتتعلق بسياق الموضوع المطروح في الصفحة، وذلك عبر محتوى alt فقط. لذا من المهم إبقاؤه قصيرًا ودقيقًا ومفيدًا. لا ينبغي إضافة أية معلومات شخصية أو توصيفات زائدة، لأنها لن تفيد أشخاصًا لم يروها أو يختبروها مسبقًا؛ فإن كانت الكرة هي لعبة الجرو المفضلة مثلًا أو لم يتمكن صحيحو البصر من تمييز ذلك من الصورة، فلا حاجة لذكر ذلك. ومن الأشياء التي يجب أخذها بالحسبان هو أن يكون للصور معنى محدد في السياق الذي نورده أو أنها فقط للزينة؛ فإن كانت لمجرد الزينة، فمن الأفضل عندها وضع نص فارغ، كقيمة للخاصية alt أو أن نضع هذه الصور في الصفحة على شكل خلفية باستخدام CSS. ملاحظة: يمكن الاطلاع على المقالين: "الصور المتجاوبة" و "الصور في HTML" لمزيد من المعلومات عن إدراج الصور في صفحات الويب وأفضل الممارسات المتبعة في تنفيذ الأمر. وإن لم نشأ إضافة معلومات نصية كثيرة عن الصورة، فيمكن وضع هذه المعلومات في المحتوى النصي الذي يحيط بها أو ضمن الخاصية title كما في الشيفرة السابقة؛ إذ ستقرأ معظم قارئات الشاشة محتوى الخاصية alt ومحتوى الخاصية title واسم ملف الصورة، كما ستعرض المتصفحات محتوى title عندما تمرر مؤشر الفأرة فوق الصورة. لنلق نظرةً سريعةً على الطريقة الرابعة: <img src="dinosaur.png" aria-labelledby="dino-label" /> <p id="dino-label">The Mozilla red Tyrannosaurus…</p> لم نستخدم في هذه الحالة الخاصية alt بل قدمنا وصفًا للصورة ضمن فقرة نصية نمطية وأعطيناها معرفًا فريدًا id واستخدمنا بعدها الخاصية aria-labelledby للإشارة إلى المعرّف السابق، مما سيدفع قارئ الشاشة لاستخدام محتوى الفقرة النصية على أنه محتوى alt الخاص بالصورة. ولهذه الطريقة فائدتها إن أردت استخدام النص نفسه كعنوان لعدة صور وهذا غير ممكن أحيانًا باستخدام alt. ملاحظة: الخاصية aria-labelledby هي جزء من مواصفة WAI-ARIA التي تتيح للمطورين إضافة دلالات لعناصر صفحة الويب لتحسين سهولة الوصول عندما يتطلب الأمر ذلك. الأشكال وعناوين الأشكال تتضمن لغة HTML عنصرين لربط شكل من أي نوع، وهما <figure> و <figcaption>: <figure> <img src="dinosaur.png" alt="The Mozilla Tyrannosaurus" aria-describedby="dinodescr" /> <figcaption id="dinodescr"> A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth. </figcaption> </figure> على الرغم من وجود عدة طرق لدعم قارئات الشاشة في ربط الأشكال بعناوينها، إلا أن تضمين الخاصيتين aria-labelledby أو aria-describedby سينشئ هذا الرابط إن لم يكن موجودًا. ولهذا الأسلوب فائدته في تطبيق تنسيق CSS، كما يقدم طريقةً نضع فيها وصف الصورة إلى جوارها في الشيفرة المصدرية. الخاصية alt الفارغة نكتب الكود الآتي: <h3> <img src="article-icon.png" alt="" /> Tyrannosaurus Rex: the king of the dinosaurs </h3> يتضمن تصميم الصفحات أحيانًا صورًا غرضها التزيين أو الديكور فقط. وسنلاحظ في الشيفرة السابقة أن الخاصية alt للصورة فارغة، وهذا ما يتيح لقارئ الشاشة تمييز الصورة دون محاولة وصفها، إذ ينطق فقط كلمة "صورة image" أو كلمةً مشابهة. يعود سبب استخدام الخاصية الفارغة بدلًا من تجاهلها إلى حقيقة أن قارئ الشاشة يذيع عنوان URL لملف الصورة بأكمله إن لم نضع الخاصية alt؛ فلو عدنا إلى المثال السابق التي نستخدم فيه الصورة لمجرد التزيين للعنوان الرئيسي المتعلقة به، وليس لها أي غرض آخر، فلا بد من استخدام الخاصية ""=alt ضمن العنصر <img>. ومن البدائل المستخدمة الخاصية "role="presentation وفق مواصفة ARIA، فهي توقف أيضًا من قراءة النص البديل. ملاحظة: يمكن استخدام CSS إن أمكن لعرض الصور التزيينية. معلومات أكثر عن الروابط التشعبية يمكن للروابط التشعبية مثل عناصر <a> وخاصيتها href، أن تحسّن أو تسيء إلى سهولة الوصول وفقًا لطريقة استخدامها. وللروابط مظهر يوحي بسهولة الوصول، ويمكنها تعزيزه لمساعدة المستخدم في التنقل بسرعة بين أقسام المستند المختلفة. كما قد تكون مسيئةً إلى سهولة الوصول إن أزيل التنسيق الخاص بسهولة الوصول أو سببت لها جافاسكريبت سلوكًا غير متوقع. تنسيق الروابط التشعبية تختلف الروابط التشعبية من حيث مظهرها عن المحتوى النصي المحيط من ناحيتي ديكور الخط واللون، فهي زرقاء وتحتها خط افتراضيًا، وبنفسجية وتحتها خط عندما يكون المستخدم قد زارها سابقًا، ولها إطار عندما تتلقى تركيز الدخل. لا ينبغي استخدام اللون بمفرده لتمييز الرابط التشعبي عن المحتوى النصي، بل يجب أن يختلف كليًا عن لون الخلفية. إضافةً إلى ذلك، ينبغي أن يختلف الرابط بصريًا عن محيطه وفق تباين مقداره 3:1 كحد أدنى بين نص الرابط والنص المحيط، وبين حالات الرابط المختلفة سواءًا كانت من نوع افتراضي، أو مُزار سابقًا، أو تلقى تركيز الدخل، وذلك بنسبة 4.5:1 بين نص الرابط في جميع الحالات السابقة وخلفية الرابط. الحدث onclick عادةً ما يُساء استخدام وسم المربط anchor tag مع الحدث onclick لإنشاء زر ائف بضبط قيمة الخاصية href على # أو باستخدام الأمر "(javascript:void(0" لمنع الصفحة من إعادة تحديث نفسها. تسبب هذه القيم سلوكًا غير متوقع عند نسخ أو جر الروابط أو فتحها في نافذة جديدة أو تخزينها كعلامة Bookmark أو أثناء تحميل جافاسكريبت أو وقوع خطأ أو عند تعطيل الرابط. ينقل هذا الأمر دلالةً غير صحيحة للتكنولوجيا المساعدة مثل قارئ الشاشة، وفي هذه الحالة يُفضّل استخدام العنصر <button>؛ إذ علينا استخدام الروابط التشعبية للتنقل فقط مستخدمين عناوين URL. الروابط الخارجية والروابط إلى موارد غير HTML لا بد من تضمين مؤشر يصف سلوك الرابط عندما ينقر عليه المستخدم في الحالة التي سيُفتح فيها ضمن صفحة جديدة، مثل التصريح "target="_blank، أو كانت قيمة href تشير إلى ملف. قد يرتبك ضعيفو الرؤية الذي يستخدمون قارئات الشاشة أو من لديهم صعوبات فكرية عندما تظهر نافذة جديدة فجأةً، ودون متوقع، وقد لا تشير النسخ الأقدم من قارئات الشاشة إلى هذا الأمر أصلًا. فيما يلي شيفرة توضح كيفية كتابة كود الروابط التي تفتح نافذة جديدة: <a target="_blank" href="https://www.wikipedia.org/" >Wikipedia (opens in a new window)</a > أما هنا فشيفرة كتابة كود رابط إلى ملف ليس HTML: <a target="_blank" href="2017-annual-report.ppt" >2017 Annual Report (PowerPoint)</a > من المهم عند استخدام أيقونة بدلًا من نص الرابط أن نتأكد من توضيح هذا السلوك للروابط بتضمين وصف بديل مناسب. روابط التجاوز رابط التجاوز Skip Link أو skipnav هو عنصر <a> يُوضع -قدر الإمكان- بالقرب من بداية العنصر <body> الذي يشير إلى بداية المحتوى الرئيسي للصفحة. يسمح رابط التجاوز للمستخدمين بتجاوز المحتوى المكرر عبر الصفحات المتعددة لموقع الويب مثل ترويسة مواقع الويب وشريط التنقل الرئيسي. لروابط التجاوز فائدتها الخاصة لمستخدمي التكنولوجيا المساعدة، مثل مفتاح التحكم، أو الأوامر الصوتية، أو جهاز الفم Mouth Stick، أو عصبة الرأس Head Wands، فقد يكون التنقل عبر الروابط المكررة لهؤلاء أمرًا مضنيًا. التقريب يوضع كم كبير من المحتوى التفاعلي قريبًا من بعضه, بما فيه الروابط، ولا بد من توفير مساحة كافية للفصل بينها. ولهذه المساحة الفارغة أهميتها لمن يعاني من مشاكل في التحكم الدقيق, فقد يفعّل المحتوى التفاعلي الخاطئ أثناء التنقل. يمكن خلق هذه الفراغات باستخدام خاصيات CSS مثل margin. خاتمة بهذا نكون قد وضحنا كيفية كتابة شيفرة HTML سهلة الوصول في مختلف المناسبات، وحددنا لأساسيات التي تهم أي مطور يتعامل مع HTML. جرب تطبيق النصائح المذكورة بالمقال واحظى باحترافية أعلى في تطوير المواقع. ترجمة -وبتصرف- لمقال HTML: A good basis for accessibility اقرأ أيضًا سهولة وصول جميع الزوار لمواقع وتطبيقات الويب معالجة مشاكل سهولة الوصول Accessibility الشائعة للتوافق مع المتصفحات أفضل 15 ممارسة لسهولة الوصول إلى موقع الويب معالجة المشاكل الشائعة للتوافق مع المتصفحات في HTML و CSS
-
يعمل مطورو الواجهات الأمامية لتطبيقات الويب على بناء واجهة مستخدم تفاعلية جذابة تعرض البيانات التي ترسلها الواجهة الخلفية ردًا على طلب من مستخدم التطبيق. وهكذا، ستكون سهولة تفاعل المستخدم مع التطبيق وسهولة استخدام واجهاته وطريقة عرض البيانات ضمنها معايير أساسية في نجاح التطبيق. وهنا يبرز الدور الإبداعي لمطوري الواجهة الأمامية. يسعى معظم مطوري الواجهات الأمامية إلى العمل ضمن فعاليات أو شركات لزيادة معارفهم واكتساب الخبرات اللازمة في طريقهم إلى الاحتراف، لكن الخطوة الأصعب كما يراها الكثيرون هي إجتياز مقابلات العمل وهذا هو موضوع مقالنا. المهارات المطلوبة من مطور الواجهات الأمامية تلخص النقاط التالية المهارات المطلوبة من مطور الواجهات الأمامية: إتقان اللغات الثلاثة الأساسية للويب: لغة HTML ولغة CSS ولغة جافا سكريبت إتقان العمل على إطار عمل جافا سكريبت عصري واحد على الأقل مثل React أو Angular.js أو Vue.js إتقان استخدام مكتبات CSS مثل Bootstrap أو Tailwind وغيرها إتقان العمل على بيئة اختبار مثل Mocha أو Jest إتقان العمل مع الواجهات البرمجية RESTful APIs للتواصل مع الواجهة الخلفية القدرة على تنفيذ تصاميم UI/UX بدقة وسهولة سنعرض في الفقرات التالية بعضًا من الأسئلة التي تُطرح على مطوري الواجهات الأمامية في مقابلات العمل، وقد قدمناها وفق ثلاثة أقسام: يغطي القسم الأول لغة HTML، ويغطي الثاني لغة CSS ومكتباتها، أما القسم الثالث فيغطي جافا سكريبت ومفاهيم تطوير الويب العصرية. سنبدأ مع أهم الأسئلة التي قد تُطرح على المتقدم حول لغة HTML5 وميزاتها، وكيفية الرد على كل سؤال بطريقة مناسبة. أهم الأسئلة التي تُطرح حول HTML من أكثر الأسئلة شيوعًا حول لغة HTML، نذكر الآتي. ما هي لغة HTML5؟ لغة HTML هي لغة توصيف Mark language تعطي صفحة الويب هيكلها، فهي بمثابة الأساسات والجدران والسقف إن شبهنا صفحة الويب بمنزل قيد البناء. أحدث نسخة من هذه اللغة هي HTML5، ويمكن أن نلخّص الغاية الأساسية من تطوير مواصفات HTML5 بإيجاد عناصر أصلية تنفّذ وظائف شائعة مثل استخدام عنصر مشغّل الفيديو <video>، بدلًا من استخدام إضافات خارجية؛ إضافةً إلى تقديم عناصر دلالية Semantic elements تهيكل محتوى الصفحة بطريقة توضح طبيعة المحتوى الذي يضمه العنصر مثل <article> و <table> وغيرها. هل تحتاج الصفحة إلى عناصر HTML أساسية حتى تعمل جيدًا؟ علينا أن نميز هنا بين سياقين أساسيين: عرض الصفحة الذي يقع على عاتق المتصفح غالبًا الهيكلية البنّاءة للصفحة التي تقع على عاتق المطوّر يحاول المتصفح في العادة عرض الصفحة أيًا كانت العناصر المستخدمة، سواءً بشكلها الصحيح أو الخاطئ، ولهذا لا توجد بالمعنى الحرفي عناصر ضرورية لعمل الصفحة في هذا السياق؛ لكن من ناحية أخرى، تَعُد HTML5 عدم استخدام العنصر المناسب في المكان المناسب أمرًا غير مرغوب فيه، لأن ذلك سيصعّب توضيح طبيعة المحتوى ودوره ويزيد من اللبس. لحل الإشكال، تساعد تقنيات ويب أخرى غير المتصفح على تفسير المحتوى بطريقة صحيحة، لذا يمكن القول لا أنه توجد عناصر معينة تمنع عرض الصفحة على متصفح، لكنها قد تفسر بطريقة خاطئة من قِبل عميل آخر. ما الفرق بين عناصر الكتل Block elements والعناصر السطرية Inline elements؟ يبدأ العنصر الكتلي Block elements سطرًا جديدًا في الصفحة ويمتد ليغطي كامل اتساع السطر حتى لو احتاج المحتوى إلى مساحة أقل، في حين لا يبدأ العنصر السطري Inline elements سطرًا جديدًا ويحتل فقط مساحة تعادل المساحة المطلوبة، وبالتالي يمكن أن يأتي عنصر آخر إلى جواره في السطر ذاته. لا يُسمح للمطور بضبط أبعاد العنصر السطري، لكنه يستطيع ذلك مع العناصر الكتلية. ومن الأمثلة على العناصر الكتلية نجد العنصرين <p> و <div>، بينما نجد أن العنصر <span> هو عنصر سطري. ما هي HTML الدلالية؟ وأين تظهر أهميتها؟ تتلخص HTML الدلالية باستخدام عناصر تعرف بكل وضوح طبيعة المحتوى الذي تضمه وطريقة هيكلة هذا المحتوى مثل استخدام عنصر تحديد ترويسة الصفحة <header> وعناصر تقسيم الصفحة إلى أجزاء مثل <section> و <article>. وتظهر أهميته في نواح ثلاث: سهولة القراءة Readability سهولة الوصول Accessibility تحسين محركات البحث SEO ماهي WCAG؟ WCAG هي اختصار لعبارة Web Content Accessibility Guidelines طورتها منظمة اسمها W3C وتعني إرشادات إتاحة محتوى الويب، وهي مجموعة من الإرشادات التي تضمن أن تكون مواقع الويب سهلة الوصول Accessible وقابلة للاستخدام السلس والتشغيل الجيد وأن تكون مفهومة ومناسبة لجميع المستخدمين، بمن في ذلك ذوي الاحتياجات الخاصة. أعط امثلة عن ممارسات في HTML تزيد من سهولة الوصول إلى محتوى الصفحة هنالك العديد من الممارسات التي تزيد من سهولة الوصول إلى الصفحة، نذكر منها ما يلي: استخدام العنصر الأصلي <button> في كل مرة نحتاج فيها إلى زر، بدلًا من أي عناصر أخرى مشابهة مثل <input> أو <div>، فهو يدعم تلقائيًا استخدام لوحة المفاتيح والفأرة استخدام الخاصية alt في الصور <img>، فهي تشرح نصيًا محتوى الصورة في حال لم يتمكن المتصفح من عرضها استخدام عناصر HTML الدلالية مثل <figure> لأنه يقدم إمكانيات أكبر من <img> تتعلق بسهولة الوصول استخدام خاصيات WCAG عندما لا تكون الخاصيات الأصلية للعناصر كافية مثل role و -aria وغيرها ما هي الاستمارات forms في HTML؟ لخص آلية عملها؟ الاستمارة <form> هي عنصر أصلي في HTML، وغايته هي نقل محتوى عناصره إلى الخادم وفق تنسيق معين. تضم الاستمارة عناصر إدخال مختلفة، مثل الأزرار والصناديق النصية وصناديق التحقق وغيرها. نحتاج في الاستمارة إلى خاصيتين: خاصية action لتحديد وجهة البيانات المرسلة خاصية method لتحديد طريقة الإرسال POST أو GET لا بد أيضًا من استخدام الخاصية name لكل عنصر إدخال ضمن الاستمارة، لأن قيمة كل عنصر سترتبط بقيمة الخاصية name قبل إرسالها إلى الخادم، وستعالج سكربتات مخصصة معلومات الاستمارة المرسلة وفقًا لما هو مطلوب. ما الآليات المقترحة لتحسين محركات البحث من خلال هيكلية HTML؟ بإمكاننا الانتباه إلى الكثير من الممارسات أثناء هيكلة الصفحة باستخدام HTML لتحسين محركات البحث منها: استخدام عناصر HTML دلالية لأنها تعطي هيكلة ومعنى محدد لهذه الهيكلية مثل <nav> و <header> و <main> وغيرها استخدام العنصر الوصفي <meta> لوصف الصفحة وتحدي كلمات مفتاحية للبحث وتحديد نافذة العرض وغيرها من البيانات الوصفية استخدام النصوص البديلة عن الصور لأن محركات البحث تفضل النصوص المكتوبة استخدام الروابط الداخلية ضمن الصّفحة الواحدة أو الموقع لتساعد زواحف محركات البحث. نماذج عن الأسئلة التي تُطرح حول CSS وأطر عملها والتصميم المتجاوب فيما يلي أهم النقاط التي قد يُسأل فيها المتقدم للوظيفة مطور واجهات أمامية حول لغة CSS ما هي لغة CSS؟ وما آخر إصداراتها؟ أولًا، لا تُعَدّ CSS لغة برمجة وليست لغة توصيف Mark Up أيضًا، وإنما هي لغة تنسيق صفحات؛ إذ تُستخدم لتطبيق تنسيقات تتعلق باللون والموقع وخطوط الكتابة والأبعاد وغيرها على عناصر HTM. آخر إصدارات اللغة هي CSS3، وتتفاوت المتصفحات في دعم قواعدها فهي لغة محمولة تفسرها المتصفحات. ما هي المحددات Selectors في CSS؟ تحدث عن أهم أنواعها باختصار المحددات هي وسيلة لاستهداف عنصر أو عناصر محددة من عناصر HTML وفقًا لمعايير مختلفة وذلك لتطبيق التنسيقات المطلوبة عليها. ومن أنواع المحددات نجد: محدد المعرفّ ID Selector: ويستهدف العنصر وفق قيمة الخاصية id له محدد الصنف Class Selector: يطبق التنسيق على أي عنصر يمتلك قيمةً محددةً للخاصية class محدد الخاصيات Attribute Selector: يستهدف العناصر التي تمتلك قيمةً محددةً لخاصية محددة محددات زائفة Pseudo Selectors: تستهدف العناصر التي تتميز بوضع معين، كأن تكون العناصر الأولى من نوعها أو أن يكون مؤشر الفأرة مارًا فوقها ما الفرق بين العناصر الزائفة والأصناف الزائفة؟ العناصر والأصناف الزائفة كيانان مختلفان في CSS يشكلان معًا المحدد الزائف؛ فالأصناف الزائفة Pseudo Classes هي أصناف تستهدف العناصر بناءً على حالتها وموقعها مثل hover: الذي يطبق التنسيق على عنصر يمر مؤشر الفأرة فوقه؛ أما العنصر الزائف Pseudo element، فهو عنصر افتراضي يُعرّف فقط لتطبيق تنسيق على جزء محدد من عنصر HTML، كأن تنسّق النص الذي يحدّده المستخدم selection::. اشرح أفضلية تطبيق قواعد التنسيق في CSS إذا لم يكن لقاعدة ما أفضلية على أخرى، فيجدر بنا تطبيق آخر قاعدة تنسيق، وترتب الأفضلية تنازليًا كالتالي: محدد معرّف محدد الصنف محدد العنصر مع ذلك، فقد يُطبق أكثر من محدد على عنصر ولن يكون الترتيب فعالًا، لهذا يُتبع ترتيب عام آخر يعتمد على منح كل محدد عددًا من النقاط وتُطبق القاعدة التي تكتسب عدد نقاط أكبر، فمثلًا محدد المعرف مع محدد الصنف يكون أعلى ترتيبًا من محدد المعرّف مع محدد العنصر. ما هي استعلامات الوسائط في CSS؟ استعلامات الوسائط هي ميزة تقدمها CSS لمطوري الواجهة الأمامية، تسمح لهم بتطبيق تنسيقات مختلفة على مستند وفقًا لنوع الجهاز الذي يعرضه وأبعاد واجهة العرض؛ كأن نغيّر مخطط الصفحة عندما تعرض على هاتف محمول أو على حاسوب مكتبي. وهي أداة أساسية في التصميم المتجاوب لمواقع الويب. ما هي SCSS؟ وكيف تعمل؟ SCSS هي معالج أولي Preprocessor للغة CSS لتوسيع إمكاناتها بإضافة متغيرات ودوال ودعم الوراثة والتداخل وغيرها من الميزات. تفسّر شيفرة SCSS إلى CSS ثم تُستخدم. اشرح كيف تدعم Bootstrap و Tailwind و Bulma الصور المتجاوبة تقدم كل من Bootstrap و Bulma صنفي تنسيق مخصصين للتحكم بطريقة عرض الصورة؛ إذ تقدم Bootstrap الصنف img-fluid.، بينما تقدم Bulma الصنف is-responsive.، أما Tailwind، فلا تقدم صنفًا جاهزًا، لكن يمكن استخدام الفئتين max-w-full و h-auto ونقاط التوقف *-w التي تضبط حجم الصورة وفقًا لأبعاد شاشات العرض. اشرح كيف تنسق Bootstrap و Tailwind و Bulma شريط التنقل Navbar تدعم كل من Bootstrap وBulma تنسيق شريط التنقل بسهولة من خلال الصنف الجاهز navbar، الذي يحمل نفس الاسم في الإطارين ويوفر تصميمًا مدمجًا تلقائيًا. أما في Tailwind CSS، فليس هناك مكون جاهز لشريط التنقل، مما يتطلب بناءه يدويًا باستخدام الأصناف الفردية لتحديد كل جزء من التصميم. ما هو نهج التصميم للهاتف أولا Mobile-First؟ وكيف يعزز التصميم المتجاوب لصفحة الويب؟ يعتمد هذا النهج على تصميم الصفحة للأجهزة ذات الشاشات الصغيرة بدايةً مثل شاشات الهواتف المحمولة، ثم الانتقال إلى الشاشات الأكبر. ولهذا النهج إيجابيات عدة منها: تحسين الأداء لقلة عدد الموارد التي ستُعرض على الشاشات الصغيرة سهولة الصيانة، لأن العناصر في الصفحة تتعاقب طبيعيًا تحسين محركات البحث، إذ يولي محرك البحث جوجل مثلًا أولوية للصفحات المخصصة للهواتف المحمولة وضح كيف يستخدم العنصر الوصفي Viewport في تحسين التصميم المتجاوب للصفحة يُستخدم العنصر كالتالي: <meta name="viewport" content="width=device-width, initial-scale=1.0"> إذ تفرض الخاصية content="width=device-width, initial-scale=1.0" على المتصفح أن يجعل اتساع الصفحة مطابقًا لاتساع شاشة الجهاز الذي يعرضها، وبأبعادها الفعلية دون تكبير أو تصغير. كيف نقدم صورا متجاوبة ونزيد فعالية تحميلها؟ يمكن أن نقدم صورًا متجاوبة بتأمين عدة نسخ من الصورة بأبعاد مختلفة، ثم نستخدم بعد ذلك الخاصيتين scrset و sizes للعنصر <img>، أو استخدام العنصر الدلالي <picture> لتقديم هذه النسخ إلى المتصفح كي يختار الأنسب، وفقًا لأبعاد شاشة العرض. كيف يحسن تخطيط الشبكة الانسيابية Fluid Grid تجاوب الصفحة وكيف يُطبق؟ يقسِّم تخطيط الشبكة الانسيابية الصفحة إلى أعمدة ذات اتساع نسبي وليس ثابت، مما يسمح بتغيير أبعاد العناصر، مثل الصور وخطوط الكتابة دينياميكيًا عند تغير أبعاد شاشة العرض. يُطبق هذا التخطيط باستخدام CSS، كما تطبقه افتراضيًا أطر عمل مثل بوتستراب Bootstrap. نماذج عن الأسئلة التي تطرح حول لغة جافا سكريبت JavaScript وأطر عملها في ما يلي بعض المواضيع التي تطرح بمقابلات توظيف مطوري الواجهات الأمامية حول لغة جافا سكريبت وأطر عمل الواجهات الأمامية التي تعتمد عليها. ماهي لغة جافا سكريبت؟ ما هي أهم ميزاتها؟ جافا سكريبت هي لغة سكريبت خفيفة ومحمولة تُستخدم لبناء صفحات ويب ديناميكية وتفاعلية، تفسرها وتنفذها متصفحات الويب، وتقدم ميزات كثيرة، مثل الرسوميات والتأكد من صلاحية المدخلات والتحديث المباشر للمحتوى والاستجابة لتفاعل المستخدم مع الصفحة. ما هي الأنواع التي تدعمها جافا سكريبت؟ وكيف تحدد النوع؟ تدعم جافا سكريبت الأنواع الأولية string و number و boolean و null و undefined و symbol و bigint، وأيضًا أنواع مركبة مثل object و array و function. تُعَد لغة جافاسكربت ديناميكية من ناحية تحديد نوع المتغيرات، فلن نحتاج إلى تحديد نوع المتغير عند التصريح عنه، بل يحدد لاحقًا أثناء التنفيذ. ما الفرق بين القيمتين null و undefined؟ undefined هي القيمة الافتراضية التي تعطيها جافا سكريبت إلى متغير صُرِّح عنه، وتدل على أن التصريح قد تم، لكن دون إسناد قيمة إلى المتغير؛ أما null فهي قيمة تشير إلى أن متغيرًا لا يمثل قيمة ولا كائنًا، ويمكن فقط للمطور أن يسندها إلى متغير وليس جافا سكريبت. وضح كيف تعمل الدوال المسماة وغير المسماة والدوال من مراتب عليا Higher-Order يمكن استدعاء الدوال المسماة باسمها في أي مكان، بينما تُنفذ الدالة غير المسماة في مكان تصريحها فقط ولا يمكن استدعائها خارج شيفرة تصريحها؛ أما مفهوم الدوال من مراتب عليا، فيتضمن استخدام دالة كمعامل لدالة أخرى أو الدوال التي تعيد دوال. ما هي الواجهات البرمجية الأصلية API في جافا سكريبت؟ اُذكر أهمها يشير مصطلح الواجهات البرمجية الأصلية إلى برمجيات مضمّنة ضمن المتصفح أو بيئة تنفيذ داخل جافا سكريبت تساعد المطور على التفاعل مع المتصفح والوصول إلى بعض وظائف نظام التشفيل وغيرها من الوظائف المهمة دون الحاجة إلى مكتبات خارجية. من أهم هذه الواجهات هي: واجهة DOM واجهة إحضار البيانات Fetch واجهة الطرفية Console واجهتي تخزين البيانات في طرف العميل LocalStorage و SessionStorage واجهة العمل مع الملفات File ما هي واجهة DOM؟ وما هي أهم الدوال المستخدمة فيها؟ تقدم واجهة DOM وسيلةً للتعامل مع هيكلية HTML أو XML للصفحة، وذلك عن طريق ترتيب عناصرها على شكل شجرة تمثل كل عقدة فيها جزءًا من الصفحة عنصر، خاصية، وهكذا. تستخدم الواجهة في إجراء تعديلات أو إضافات إلى هيكلية الصفحة. من الدوال المستخدمة ()document و ()element. وضح بإيجاز مفهوم البرمجة غير المتزامنة والوعد في جافا سكريبت، واعط مثال عن ذلك البرمجة غير المتزامنة هي وسيلة لمتابعة تنفيذ الشيفرة والتأكد مما إن كان تنفيذ عملية ما سيستغرق وقتًا معتبرًا، وذلك كي لا تتجمد صفحة الويب ويفقد المستخدم القدرة على التفاعل معها أثناء تنفيذ تلك العملية. أما الوعد، فهو كائن في جافا سكريبت يظهر عند اكتمال تنفيذ عملية غير متزامنة لمعالجة حالات فشل أو نجاح تلك العملية. وكمثال عن العمليات غير المتزامنة، انتظار نتيجة استعلام من قاعدة بيانات، فقد يطول زمن انتظار إنجاز العملية لهذا نستخدم في هذه الحالة استدعاءً غير متزامن لدالة الاستعلام كي نسمح للمتصفح بمتابعة تنفيذ الشيفرة دون انتظار النتيجة. عندما تكتمل عملية الاستعلام في أي وقت يُنجز الوعد، أي يستطيع كائن الوعد عندها إعلامنا بالنتيجة. ما هو تنسيق JSON؟ وكيف يستخدم في جافا سكريبت؟ هو تنسيق بسيط للبيانات يُستخدم في تخزين وتبادل البيانات. يتميز هذا التنسيق بسهولة كتابته وقرائته بالنسبة إلى المطور أو للآلة. يخزن البيانات على شكل أزواج [مفتاح: قيمة] ويدعم مختلف أنواع القيم سواءً كان صحيح أو نص أو مصفوفة. يستخدم JSON في جافا سكريبت عادةً لتبادل البيانات بين الخادم والعميل، ونستخدم التابع ()JSON.stringify لتحويل كائن جافا سكريبت إلى كائن JSON و ()JSON.parse لتحويل كائن JSON إلى كائن جافا سكريبت. وضح وظيفة الواجهتين XMLhttpRequest و Fetch وما الفرق الجوهري بينهما كلاهما واجهتان لتنفيذ طلبات HTTP من وإلى الخادم. تعتمد الأولى على الاستدعاءات، بينا تعتمد الثانية الأكثر تطورًا على الوعود. لا يُنصح حاليًا باستخدام XHR، وينبغي استبدالها بالواجهة Fetch التي تدعمها معظم المتصفحات الحديثة. تحدث بإيجاز عن LocalStorage و SessionStorage و IndexedDB وحالات استخدامها كل من LocalStorage و SessionStorage و IndexedDB هي وسائل لتخزين البيانات على طرف العميل أو المتصفح، أي أنها توفر لنا طرق لحفظ المعلومات محليًا في جهاز المستخدم بدون الحاجة إلى إرسالها إلى الخادم وفيما يلي أبرز الفروقات فيما بينها. تُعَد SessionStorge ذاكرة تخزين مؤقتة محدودة الحجم يقارب حجمها حوالي 5 ميجابايت، وتستخدم لتخزين البيانات النصية فقط مؤقتًا خلال جلسة العمل، بحيث تفقد محتواها عند إغلاق المتصفح. من حالات استخدامها تخزين بيانات استمارات الويب مؤقتًا خلال الجلسة لتفادي فقدانها. أما LocalStorage فهي مشابهة للذاكرة السابقة من حيث الحجم وطريقة التخزين، إلا أنها لا تفقد محتواها عند إغلاق المتصفح، وتحفظ المعلومات على شكل زوج مفتاح وقيمة لحفظ تفضيلات المستخدم مثل لغة الموقع أو تفعيل الوضع الليلي. في حين أن IndexedDB هي قاعدة بيانات مدمجة في المتصفح، توفر ذاكرة كبيرة الحجم ومفهرسة وتدعم العمليات غير المتزامنة. تخزن هذه الذاكرة مختلف أنواع البيانات مثل تخزين قواعد بيانات أو الملفات الكبيرة لاستخدامها دون اتصال مع الإنترنت. ماهو المكون Component؟ وكيف تقدمه كل من أطر React و Angular و Vue.js؟ المكون Component هو وحدة مستقلة في واجهة المستخدم، تجمع بين الهيكلية بلغة HTML والتنسيق بلغة CSS ومنطق العمل بلغة JavaScript أو TypeScript، وهو قابل لإعادة الاستخدام بسهولة عبر أجزاء مختلفة من التطبيق. تقدم كل مكتبة أو إطار من أطر عمل جافا سكريبت المكونات بطريقة مختلفة ففي مكتبة رياكت React يكون المكوّن عبارة عن دالة أو صنف Class يعيد شيفرة JSX، وهي صيغة قريبة من HTML تكتب داخل JavaScript. أما في إطار عمل أنغولار Angular فيكون المكون عبارة عن صنف مكتوب بلغة TypeScript يُعرّف باستخدام مُزخرف Decorator مثل Component@ لتحديد القالب Template والنمط Style المرتبط به. أما بالنسبة لفيو جي إس Vue.js فالمكون فيه عبارة عن ملف مفرد له الامتداد .vue، يحتوي على ثلاثة أقسام هي القالب Template والتنسيق Style، والمنطق البرمجي Script. اشرح المقصود بإدارة الحالة وكيف نتعامل معه في كل من React و Angular و Vue.js تعني إدارة الحالة State Management تنظيم البيانات المشتركة بين مكونات واجهة المستخدم المختلفة ومتابعة تغييراتها بطريقة تضمن تحديث الواجهة بشكل متناسق وسليم كلما تغيرت هذه البيانات. يتعامل كل إطار عمل جافا سكريبت مع إدارة الحالة بأسلوبه الخاص ففي رياكت React مثلًا، تعتمد إدارة الحالة على استخدام دوال مثل useState وuseReducer لإدارة الحالة داخل المكوّنات، أو عبر مكتبات خارجية مثل Redux و Zustand لإدارة حالات أكثر تعقيدًا ومشاركة الحالة عبر التطبيق. في حين يستخدم أنغولار Angular الخدمات Services لنقل وإدارة البيانات بين المكوّنات، مع دعم قوي للمكتبات التفاعلية مثل RxJS، ويمكن أيضًا الاعتماد على مكتبات إدارة الحالة مثل NgRx لبناء أنظمة معقدة تعتمد على التدفق أحادي الاتجاه للبيانات One-way Data Flow من المصدر للوجهة وبهذا يسهل علينا تتبع أين وكيف تتغير البيانات. مثلًا لو كان لدينا تطبيق يحتوي على صفحة تعرض اسم المستخدم، فبدلاً من أن يعدل كل مكون هذا الاسم مباشرة، نخزنه في مكان مركزي ونرسل التحديثات لصفحة العرض عند الحاجة فقط. أخيرًا في فيو جي إس Vue.js نتعامل مع الحالة محليًا داخل المكون باستخدام الدالة ()data، وعند الحاجة لمشاركة الحالة بين المكونات أو إدارة حالة أوسع، يمكننا استخدام مكتبات مثل Vuex أو Pinia لتوفير إدارة حالة مركزية. ماهو التوجيه Routing؟ وكيف تحققه كل من React و Angular و Vue.js؟ التوجيه Routing هو عملية إدارة التنقل بين صفحات أو مكونات التطبيق دون الحاجة لإعادة تحميل الصفحة بأكملها. وهو يسمح للمطورين ببناء تطبيقات الصفحة الواحدة SPA بسهولة وذلك عن طريق تحميل المحتوى المطلوب فقط وتحديثه على الصفحة الحالية. تحقق المكتبة رياكت React التوجيه من خلال استخدام الوحدة البرمجية react-router-dom التي تسمح بتحديد الوجهات routes داخل التطبيق وتوجيه المستخدم إلى المكونات المناسبة بناءً على عنوان URL. import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; ويستخدم أنغولار Angular الوحدة البرمجية angular/router@ المدمجة داخله لتحديد الوجهات والتعامل مع التنقل بين المكونات ضمن تطبيق واحد دون إعادة تحميل الصفحة. import { RouterModule } from '@angular/router'; بينما يستخدم إطار فيو جي إس Vue.js الوحدة vue-router، وهي مكتبة مخصصة تسهل إدارة الوجهات والتنقل بين المكونات import VueRouter from 'vue-router'; ما المقصود بالتصيير من جانب الخادم SSR؟ اشرح آلية عمله التصيير من جانب الخادم Server-Side Rendering -أو SSR اختصارًا- هو أسلوب يقوم فيه الخادم بتوليد الصفحة كاملة بصيغة HTML قبل إرسالها إلى المتصفح، أي يتولى الخادم مهمة تحضير المحتوى وتجميعه، ثم يرسله للمستخدم جاهزًا للعرض، بدلاً من أن ينتظر المتصفح كي يقوم بتجميع الصفحة. يعمل التصيير من جانب الخادم SSR على النحو التالي: يرسل المستخدم طلب HTTP Request للوصول لصفحة معينة على الموقع يستجيب الخادم للطلب بجلب البيانات المطلوبة من قاعدة بيانات مثلاً، ثم يستخدم قوالب HTML أو مكونات أطر عمل لبناء صفحة HTML كاملة جاهزة للعرض ترسل الصفحة المصيّرة للمتصفح، مما يسمح للمستخدم برؤية المحتوى مباشرة دون الحاجة للانتظار حتى يجري تحميل جافا سكريبت وتشغيلها بعد عرض الصفحة، يبدأ تحميل ملفات جافا سكريبت وإضافة الوظائف التفاعلية للصفحة كالأزرار القابلة للنقر والنماذج الديناميكية ما المقصود بالتوليد الساكن للصفحات SSG؟ اشرح هي آلية عمله توليد الصفحات الساكن Static Site Generation -أو SSG اختصارًا- هو أسلوب لإنشاء صفحات HTML جاهزة في وقت بناء وتجهيز الموقع Build Time، وليس عند كل طلب من المستخدم. أي أن الخادم أو أداة البناء تعد الصفحات مرة واحدة ثم تُخزنها كملفات HTML ثابتة يمكن تقديمها مباشرة عند طلبها. في هذه الحالة يُحمّل المتصفح ملف HTML بأبسط شكل ممكن Minimal HTML file ثم يستخدم جافا سكريبت لإحضار المحتوى وتصييره وعرضه ديناميكيًا وفق الآلية التالية: تنشأ ملفات HTML الثابتة لكل صفحة بشكل مسبق في وقت البناء بحسب البيانات المتوفرة من API مثلاً أو من قاعدة بيانات ويجري حينها التصيير وتحضير الصفحات وحفظها عند زيارة المستخدم للصفحة، يُرسل له ملف HTML ثابت جاهز دون الحاجة إلى توليده من جديد أو إجراء استعلامات من قواعد بيانات تحمّل ملفات جافا سكريبت التي تتولى جلب بيانات إضافية أو إضافة تفاعلية للصفحة عند الحاجة. كيف تتعامل أطر العمل المختلفة مع SSR و SSG؟ تسهم كل من تقنية توليد الصفحات عند الطلب من الخادم SSR وتقنية توليد الصفحات بشكل ثابت أثناء بناء المشروع SSG في تحسين تحميل المواقع وأدائها وتجربة المستخدم، عبر تجهيز الصفحات مسبقًا على الخادم بدلاً من الاعتماد كليًا على المتصفح. تعتمد React على إطار العمل Next.js، الذي يوفّر دعمًا كاملاً لكل من SSR وSSG يعتمد Vue.js على إطار العمل Nuxt.js، الذي يدعم كلا النمطين SSR و SSG *يستخدم أنغولار إطار العمل Angular Universal لتحقيق SSR ولا يدعم SSG بشكل رسمي ما هي لغة TypeScript؟ لغة TypeScript هي لغة مبنية على على جافا سكريبت لتعزيز قدراتها من خلال إضافة ميزات جديدة لها مثل إضافة طريقة لتعريف الأنواع يدويًا، وتقديم واجهات Interfaces لفرض هيكلية محددة على الكائنات، ودعم البرمجة غرضية التوجه OOP والتصريف المبكر لتلافي أخطاء زمن التنفيذ. نصائح للمتقدمين لوظيفة مطور واجهات أمامية حاولنا أن نقدم في هذا المقال بعض الأفكار الأساسية التي يُسأل عنها مطورو الواجهات الأمامية بكثرة في مقابلات العمل. وركزنا في الأسئلة على لغات HTML و CSS وجافا سكريبت وبعض أطر العمل الأكثر شهرة. ونختم المقال بتوجيه بعض النصائح المهمة التي ينبغي للمتقدم أخذها بالحسبان: إتقان الأساسيات ونقصد بذلك مراجعة أساسيات تطوير الواجهات الأمامية وهي لغات HTML و CSS وجافا سكريبت والطريقة التي تتكامل فيها تلك اللغات لبناء صفحات ويب تفاعلية متجاوبة الاطلاع على مشاريع سابقة سواء أكانت من إنتاج المطوّر ذاته، أو مشاريع نموذجية تشرح بعض التقنيات، فهذه المشاريع أداة فعالة في مراجعة المفاهيم الأساسية وربط المعلومات المتعلقة باللغات الأساسية لتطوير الواجهات الأمامية فهم أساسيات مكتبات وأطر عمل الواجهة الأمامية من المهم جدًا إتقان أساسيات بعض أطر عمل جافا سكريبت مثل React أو Angular.js أو Vue.js وفقًا لمتطلبات الشاغر الوظيفي المطلوب فهم أساسيات واجهة المستخدم وتجربة المستخدم UI/UX فهذه الأساسيات أصبحت معايير في تطوير الواجهات الأمامية ومعرفتها أمر مستحب، ويفضل امتلاك أساس جيد حول سهولة الوصول Accessiblity والتصميم المتجاوب Responsive Design ومبادئ تطوير واجهات مستخدم سهلة الفهم والاستخدام. المراجع h5bp/Front-end-Developer-Interview-Questions Front End Developer Interview Questions Front End Interview Handbook اقرأ أيضًا دليلك الشامل لفهم المسارات المهنية لمجال تجربة المستخدم كيف تحضر لمقابلة عمل ناجحة؟ التحضير لمقابلة العمل بهدف الحصول على وظيفة مدير المنتج
-
سنشرح في هذا المقال والمقالات التي تليه خطوات إعداد وتنفيذ لعبة كلاسيكية في فضاء ثنائي البعد في محرك الألعاب جودو، اللعبة التي سنعمل عليها هي لعبة مركبة فضاء مقاتلة، وفيما يلي لقطة شاشة لما سيكون عليه الحال عند الانتهاء من اللعبة. نقطة الانطلاق لسهولة الفهم سنبني في كل مقال من هذه السلسلة جزءًا من اللعبة، ونضيف الميزات تدريجيًا ونشرح بالتفصيل ما نفعله في كل مرة. وإن وجدتم مشكلة في فهم الجانب البرمجي لأي جزء من المشروع ننصحكم بالاطلاع على مصادر مفيدة مثل توثيق جودو الرسمي، وسلسلة مقالات تعلم جودو على أكاديمية حسوب. كما يمكنكم تحميل مشروع اللعبة كاملًا من مستودعه المخصص على جيتهب، أو من هنا مباشرة classic_shmup.zipوتجربتها لديكم لمزيد من الفهم. إعداد المشروع سنأخذكم في هذا المشروع إلى بناء أولى ألعابكم على محرّك الألعاب جودو. وعلى الرغم أن العمل على هذا المشروع لا يتطلب منكم أي خبرة سابقة، لكن من المفيد قبل البدء امتلاك بعض الأساسيات وقراءة مقال مدخل إلى محرك الألعاب جودو لتعلم كيفية التعامل مع المحرر والواجهة الرسومية لمحرك جودو بسرعة ومرونة. اخترنا أن يكون المشروع ثنائي الأبعاد 2D لأن الألعاب ثلاثية الأبعاد 3D أكثر تعقيدًا بالنسبة للمبتدئين ، لكن سترون لاحقًا أن الكثير من الميزات الأساسية التي ستتعلمونها عند بناء اللعبة هي نفسها في حالة الألعاب ثنائية وثلاثية الأبعاد. لهذا ينصح أن تتقنوا أولًا خطوات العمل على لعبة متكاملة ثنائية الأبعاد، وبعدها سيكون الانتقال إلى الفضاء ثلاثي البعد أسهل. دعونا نبدأ العمل، لنفتح الآن محرّك البحث جودو ونبدأ مشروعًا جديدًا . يمكن تسمية المشروع بأي اسم تختارونه، لكننا سنختار لمشروعنا اسم Classic Shump لأننا سنطور لعبة كلاسيكية مبنية على التصويب أو إطلاق النار إلى الأعلى. تحميل أصول اللعبة Assets يمكنكم تحميل المحلقات أو الأصول الخاصة باللعبة من صور وأيقونات مختلفة من موقع Mini Pixel Pack by Grafxkid، بعد التحميل علينا أن نستخرج الملفات وننسخها إلى المشروع بسحب المجلد وإفلاته ضمن نافذة مدير الملفات File System لتبدأ عمليه استيرادها لداخل اللعبة. إعدادات المشروع ننتقل الآن إلى القائمة مشروع Project، ثم نختار إعدادات المشروع Project Settings وننقر على زر التبديل إعدادات متقدمة Advanced settings في الزاوية العليا اليسارية، ونضبط ما يلي في قسم النافذة window الموجود ضمن قسم لإظهار Display كما يلي: عرض منفذ العرض viewport width وطول منفذ العرض viewport height على القيمتين 240 و 320 تجاوز عرض النافذة window width override وتجاوز ارتفاع النافذة window height override على القيمتين 480 و 640 على التوالي الخيار تمدد stretch ضمن القسم وضع mode على القيمة canvas_items لجعل عناصر المشهد اللعبة تتمدد وتتناسب مع حجم الشاشة ستضمن هذه الإعدادات أبعادًا صحيحة للعبة، لأننا نستخدم ضمن اللعبة أيقونات من البكسلات وهي بحد ذاتها صغيرة جدًا، لهذا ستكون هذه القيم مناسبة لعرضها. لكن قد تكون هذه الأبعاد صغيرة بالنسبة إلى الشاشات الحديثة، لهذا ضبطنا بعض الإعدادات الأخرى حتى نتمكن من تغيير الأبعاد بأسلوب متناسب معها. لنضبط مثلًا أبعاد التجاوز override على القيم 720x960 إن كانت شاشتنا بدقة 1080p وسنكون قادرين على تغيير حجم النافذة عند تشغيل اللعبة. الآن لننتقل ضمن إعدادات المشروع للقسم معالجة Rendering ثم نختار ملمس Texture ونضبط الخيار تصفية الملمس الافتراضي Default Texture Filter على القيمة Nearest. سيضمن ذلك بقاء الأيقونات جميلة لأن المحرك سيعرض البكسلات كما هي بدون تنعيم وبهذا تبقى الحواف واضحة كما في الصورة اليمنية لا اليسارية: ننقر الآن على تبويب خريطة الإدخال Input Map في إعدادت المشروع حيث سنضبط هنا عناصر الإدخال التي نستخدمها في اللعبة. نكتب كل كلمة مما يلي right و left و up و down و shoot على حدة في صندوق إضافة إجراء جديد Add New Action ثم ننقر بعدها المفتاح Enter لتتكون لدينا خمسة إجراءات، أربعة منها للتحرك في الاتجاهات المختلفة وواحدة للإطلاق. ننقر بعد ذلك الزر + إلى جانب كل إجراء ثم ننقر أي مفتاح من من لوحة المفاتيح كي نعينه لتنفيذ هذا الإجراء. من المفترض أن نرى نتيجة مشابهة لما يلي: بإمكانكم اختيار المفاتيح التي تشاؤنها إن لم تجدوا أن المفاتيح التي عيّناها مناسبة لكم. الخاتمة بدأنا في هذا المقال أولى خطواتنا في تطوير لعبة سفينة فضاء مقاتلة ثنائية الأبعاد بجودو، وتعلمنا طريقة تحميل أصول اللعبة Assets، وضبط إعدادات المشروع المناسبة للعبتنا والتعامل مع إجراءات الدخل، وبعد أن أنهينا هذه الإعدادات الأساسية سنكون جاهزين في المقال التالي لإنشاء شخصية سفينة الفضاء التي سيتحكم بها اللاعب. ترجمة -وبتصرف- للمقالين: Your first 2D game و Project setup اقرأ أيضًا المقال السابق: تعلم إدارة الصوت في جودو مدخل إلى محرك الألعاب جودو Godot تنظيم مشروع جودو البدء بتطوير لعبةبسيطة ثنائية البعد عبر محرك الألعاب Godot
-
عند تطوير الألعاب باستخدام جودو، من الشائع أن نربط الأصوات بالأحداث التي تقوم بها الشخصيات، كتشغيل صوت معين عند موت الشخصية وصوت آخر عندما تنفذ هجومًا معينًا. ونستخدم غالبًا العقدة AudioStreamPlayer لتشغيل هذه الأصوات لكن هناك مشكلة شائعة قد تواجهنا، فعند إزالة الشخصية من المشهد بسبب موتها أو لأي سبب آخر ستزال كل العقد التابعة لها، بما في ذلك عقدة مشغل الصوت. ونتيجة لذلك، سيتوقف الصوت فجأة، حتى لو لم يكن قد اكتمل تشغيله بعد، وهذه التجربة قد تكون مزعجة للاعب، لأنها تجعل اللعبة تبدو غير طبيعية. سنشرح في الفقرات التالية طريقة مناسبة لحل هذه المشكلة. مشروع مشغل الصوت سنعتمد على عقدة AudioStreamPlayer مستقلة يمكن وضعها في أي مكان داخل شجرة المشهد Scene Tree في محرك ألعاب جودو، لكن من الأفضل أن تكون هذه العقدة مستقلة عن الكائن أو الشخصية. بمعنى ستكون هذه العقدة مسؤولة عن تشغيل المقاطع الصوتية المتعلقة بالأحداث المختلفة في اللعبة، مثل صوت موت الشخصية أو تأثيرات البيئة، دون أن تتأثر بإزالة الكائنات من المشهد. لتحقيق ذلك سننشئ مشروع جودو جديد وننشئ ضمنه مشهد جديد ونحفظه باسم audio_demo.tscn أو أي اسم آخر مناسب ونضيف له مجموعة عقد وفق التسلسل الهرمي التالي: AudioDemo (MarginContainer) ├── CenterContainer (CenterContainer) │ └── GridContainer (GridContainer) ├── CanvasLayer (CanvasLayer) │ └── HBoxContainer (HBoxContainer) │ ├── Label (Label) │ ├── Label2 (Label) │ ├── VSeparator (VSeparator) │ ├── Label3 (Label) │ └── Label4 (Label) يتكوّن المشروع من عقدة جذر من نوع MarginContainer سنسميها AudioDemo، وهي المسؤولة عن تنظيم المحتوى. وننشئ بداخلها عقدة ابن من نوع CenterContainer لمحاذاة المحتويات في المنتصف، والتي سنصيف لها عقدة ابن جديدة GridContainer لتوليد الأزرار الخاصة بتشغيل الملفات الصوتية. كما سنضيف عقدة CanvasLayer لعرض واجهة المستخدم ونضيف لها عقدة ابن من نوع HBoxContainer وضمنها عدة عناصر تسمية Label لعرض إحصائيات عن الملفات الصوتية التي نشغلها، وعدد الملفات الصوتية الموجودة في قائمة الانتظار وعقدة VSeparator للفصل بين العناصر. بعدها نضيف مجلد الموارد assets الذي يتضمن مجموعة ملفات صوتية، ويمكنك الحصول عليه من خلال تحميل هذا المشروع من مستودع جيتهب أو من هنا مباشرة audio_manager.zip. توضح الصورة التالية شكل واجهة مشروعنا الذي سنبنيه بجودو، حيث سيعرض زر لكل ملف صوتي ضمن المجلد assets ويوّلد وفقًا لعددها شبكة مكونة من مجموعة من الأزرار التي تشغل كل ملف صوتي بمجرد النقر على كل منها. وسنرى في أعلى نافذة المشروع إحصائيات عن مدير الصوت. الكود البرمجي لمدير الصوت بعد إضافة العقد المطلوبة وتخصيصها نضيف في البداية سكريبت جديد في محرر جودو ونحفظه باسم audio_manager.gd ونكتب فيه الكود التالي: extends Node var num_players = 8 var bus = "master" var available = [] # المشغلات المتاحة var queue = [] # رتل الأصوات التي ستُشغل func _ready(): # AudioStreamPlayer ننشئ حلقة من العقد for i in num_players: var player = AudioStreamPlayer.new() add_child(player) available.append(player) player.finished.connect(_on_stream_finished.bind(player)) player.bus = bus func _on_stream_finished(stream): # نجعل المشغل متاحًا مجددًا بعد الانتهاء من تشغيل المقطع الصوتي available.append(stream) func play(sound_path): queue.append(sound_path) func _process(delta): # نشغل الأصوات في الرتل إن وجد أي لاعب if not queue.empty() and not available.empty(): available[0].stream = load(queue.pop_front()) available[0].play() available.pop_front() أنشأنا في الكود السابق مجموعة من عقد AudioStreamPlayer وعددها هو num_players وأسندنا لها القيمة الافتراضية 8 وخزناها في قائمة باسم available. كلما أردنا تشغيل صوت نضيفه لرتل أو قائمة انتظار باسم queue مهمته تخزين الأصوات التي نريد تشغيلها لاحقًا، بعد انتهاء تشغيل أي صوت، يعاد المشغل إلى القائمة available ليكون جاهزًا للاستخدام مجددًا. سنضبط هذا السكريبت حتى يُحمّل تلقائيًا Auto-load من إعدادات المشروع كي نتمكن من استدعائه من أي مكان بسهولة، للقيام بذلك سنفتح إعدادات المشروع من خلال القائمة Project ثم Project Settings، نذهب بعدها إلى تبويب Global ثم لتبويب التحميل التلقائي AutoLoad كما في الصورة التالية ونختار ملف السكريبت audio_manager.gd من ملفات المشروع، وفي خانة Node Name نمنحه اسمًا سهل التمييز مثل AudioManager ثم نضغط على Add، وبهذا سيضاف السكربت إلى قائمة التحميل التلقائي، وستُفعَّل خانة Enabled تلقائيًا. بهذا أصبح عندنا نظام صوت مركزي ثابت ومتاح في كل مكان داخل اللعبة، ويمكن أن نستدعيه من أي مكان من مشروعنا نريد فيه تشغيل الصوت بكتابة التالي: AudioManager.play("res://path/to/sound") ملاحظة: يمكن سحب ملفات الصوت مباشرة إلى المحرر النصي في محرك جودو، مما يتيح لنا لصق المسار الخاص بالملف الصوتي في السكريبت بسهولة بدلًا من كتابة المسار يدوياً. الكود البرمجي لواجهة المشروع الديناميكية الخطوة التالية التي سنقوم بها هي توليد الواجهة الديناميكية المكونة من زر لتشغيل كل ملف صوتي، لتحقيق ذلك نلحق سكريبت للعقدة الجذر AudioDemo ونكتب فيه الكود التالي لتشغيل كافة الملفات الصوتية الموجودة في مجلد المشروع: extends MarginContainer # المجلد الذي يحتوي على الملفات الصوتية @export var sound_dir: String = "res://assets" func _ready(): # نحمل كل الملفات الصوتية الموجودة في المجلد var dir = DirAccess.open(sound_dir) if dir: dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": if file_name.get_extension() in ["wav", "ogg"]: add_button(file_name) file_name = dir.get_next() dir.list_dir_end() func add_button(file_name): # إضافة زر لتشغيل الملف الصوتي المخصص له var b = Button.new() $CenterContainer/GridContainer.add_child(b) b.add_theme_font_override("font", load("res://assets/Poppins-Medium.ttf")) b.text = file_name b.pressed.connect(on_audio_button_pressed.bind(b)) func on_audio_button_pressed(button): # تشغيل الصوت المرتبط بالزر var path = sound_dir + "/" + button.text AudioManager.play(path) func _process(delta): # تحديث عدد المشغلات المتاحة وعدد الأصوات في قائمة الانتظار $CanvasLayer/HBoxContainer/Label2.text = str(AudioManager.available.size()) $CanvasLayer/HBoxContainer/Label3.text = str(AudioManager.queue.size()) الخاتمة أنشأنا في هذا المقال نظامًا متكاملًا لإدارة وتشغيل المؤثرات الصوتية في محرك جودو بطريقة مستقرة تضمن استمرار تشغيل الأصوات حتى بعد إزالة الشخصية أو العنصر المرتبط بها، مما يحسّن تجربة اللعب ويجعلها أكثر واقعية وذلك من خلال استخدام التحميل التلقائي للسكريبت الذي يوفّر علينا الجهد في تكرار الكود، ويمنحنا تحكمًا مركزيًا بالملفات الصوتية ضمن اللعبة. ترجمة -وبتصرف- للمقال: Audio manager اقرأ أيضًا المقال السابق: تحريك جسم صلب RigidBody2D بواقعية في الفضاء باستخدام جودو الطريقة الصحيحة للتواصل بين العقد في جودو Godot مدخل إلى محرك الألعاب جودو Godot إضافة النقاط واللعب مجددًا وتأثيرات الصوت للعبة 3D ضمن جودو إضافة المؤثرات الصوتية للعبة المطورة باستخدام بايثون ومكتبة Pygame
-
نشرح في هذا المقال طريقة إنشاء جسم يمثل سفنية تتحرك بطريقة واقعية في الفضاء باستخدام عقدة الجسم الصلب RigidBody2D، حيث يوفر محرك الألعاب جودو أنواعًا مختلفة من الأجسام الفيزيائية ومن ضمنها عقدة RigidBody2D التي تناسب جسمًا يتحرك تلقائيًا وفقًا لقوانين الفيزياء. قد يكون استخدام هذه العقدة مربكًا بعض الشيء لأنها غير قابلة للتحريك المباشر بطريقة مشابهة لعقدة الشخصية CharacterBody2D ومن يتحكم بالكامل هو محرك فيزياء جودو الداخلي Godot physics engine، وبالتالي لا يمكننا ببساطة تغيير موقعها مباشرة من أجل تحريكها بل علينا تطبيق قوى فيزيائية عليها لتحقيق الحركة المطلوبة. ننصح قبل بدء العمل بإلقاء نظرة على توثيق الواجهة البرمجية RigidBody2D لفهم خصائصها بشكل أعمق. بناء الحركة سننشئ مشهد جديد ثنائي الأبعاد في جودو ونستخدم فيه العقد التالية: RigidBody2D (Ship) ├── Sprite2D └── CollisionShape2D وإليكم وظيفة كل عقدة منها: تمثل RigidBody2D(Ship) العقدة الرئيسية للجسم الصلب الذي نريد تحريكه، والذي يتفاعل مع البيئة ويتأثر بقوانين الفيزياء تمثل العقدة Sprite2D الشكل المرئي للجسم الصلب، وهي في حالتنا صورة سفينة الفضاء التي تتحرك تلقائيًا مع الجسم الفيزيائي الصلب تستخدم العقدة CollisionShape2D لتحديد شكل وحجم منطقة التصادم للجسم الصلب، وهي ضرورية كي تتعرف الفيزياء على حدود الجسم وتتفاعل معه بشكل صحيح عند اصطدامه بأجسام أخرى توجيه الشخصية عندما نحرك جسم صلب RigidBody2D في جودو، فإن اتجاهه الأمامي الافتراضي أي الاتجاه الذي يتحرك فيه يُعد دومًا هو الاتجاه الموجب للمحور X أي نحو اليمين في المشهد، لذا علينا في البداية توجيه الجسم الصلب بالشكل الصحيح إذا لم يكن كذلك. هذا الأمر مهم لأننا عندما نطبق قوى أو سرعة على الجسم فإن هذه القيم تُحسب بناءً على المحاور المحلية للجسم، لذا إذا لم يكن الجسم موجّهًا أصلاً نحو اليمين فستكون حركته غير صحيحة أو غير متوقعة. وإن كانت الأيقونة أو صورة الشخصية تتجه باتجاه معاكس، علينا أن ندوّر العقدة Sprite2D وليس الجسم الأب نفسه RigidBody2Dحتى نوجهها بالشكل الصحيح. كما سنستخدم المدخلات التالية في خريطة الإدخال Input Map: المُدخل المفتاح thrust w أو ↑ rotate_right d أو → rotate_left a أو ← بعدها، سنضيف سكريبت إلى الجسم الصلب ونعرّف فيه بعض المتغيرات: extends RigidBody2D @export var engine_power = 800 @export var spin_power = 10000 var thrust = Vector2.ZERO var rotation_dir = 0 يحدد أول متغيران كيفية التحكم بحركة سفينة الفضاء. إذ يتحكم المتغير engine_power بالتسارع أي سرعة الحركة للأمام، بينما يتحكم المتغير spin_power بسرعة دوران السفينة. ويُضبط كل من thrust و rotation_dir بناءً على مدخلات المستخدم، وهذا ما سنفعله تاليًا: func get_input(): thrust = Vector2.ZERO if Input.is_action_pressed("thrust"): thrust = transform.x * engine_power rotation_dir = Input.get_axis("rotate_left", "rotate_right") في البداية، تكون قيمة thrust صفرًا، مما يعني أن السفينة الفضائية لا تتحرك إلى الأمام. عندما يضغط المستخدم مفتاح الدفع مثل السهم العلوي أو مفتاح W، فإننا نضبط thrust بحيث يحدد الشعاع الذي يوجه الحركة الأمامية للسفينة. في الوقت نفسه، تتغير قيمة rotation_dir بمقدار 1 بناءً على مفتاح الإدخال الذي يضغط عليه المستخدم، سواء كان لليمين أو اليسار. يمكن للسفينة الفضائية البدء بالطيران عند تطبيق القيم التالية في الدالة (physics_process(delta_. func _physics_process(_delta): get_input() constant_force = thrust constant_torque = rotation_dir * spin_power ستحلق المركبة الآن في الفضاء، لكن سنجد صعوبة في التحكم بها. حيث سيكون الدوران سريعًا جدًا، وستتسارع بشدة لتغادر الشاشة بعدها، وهنا لا بد من التوقف عن تطبيق فيزياء الفضاء الحقيقية، فطالما أنه لا احتكاك في الفضاء ستتسارع المركبة بسرعة، لهذا من الأسهل في مثالنا أن ندفع المركبة لتتوقف عندما لا تطبَّق عليها قوة دفع، ويمكننا تنفيذ هذا الأمر باستخدام التخميد damping. لننتقل إلى خصائص العقدة ومنها إلى Linear ثم Damp و Angular ثم Damp ونضبط قيمتي هاتين الخاصيتين على 1 و 2 على الترتيب. تحدّ هاتان القيمتان من سرعة الحركة وسرعة الدوران مما يسبب توقف السفينة الفضائية. ملاحظة: يمكن أن نجرّب تغيير قيم هذه الخاصيات ونرى كيف تتفاعل مع engine_power و spin_power. العودة إلى الشاشة تشبه عملية إعادة سفينة الفضاء إلى الجهة المقابلة من الشاشة بعد خروجها من أحد الأطراف انتقالًا لحظيًا عبر المكان. لكن إن حاولنا تعديل خاصية position مباشرةً، فستقفز السفينة فورًا إلى الموقع الجديد، وقد يؤدي ذلك إلى سلوك غير متوقع، لأن محرّك فيزياء جودو يستمر في التحكم بحركة الجسم الصلب. للتغلب على هذه المشكلة، نستخدم دالة رد النداء ()integrate_forces_ الخاصة بالعقدة RigidBody2D، والتي تتيح لنا تعديل الخصائص الفيزيائية مثل الموقع والسرعة بشكل مباشر ومتزامن مع دورة محرك الفيزياء دون أن تتعارض معها. لننتقل الآن إلى screensize في أعلى السكريبت: @onready var screensize = get_viewport_rect().size نضيف بعد ذلك دالة جديدة باسم ()integrate_forces_: func _integrate_forces(state): var xform = state.transform xform.origin.x = wrapf(xform.origin.x, 0, screensize.x) xform.origin.y = wrapf(xform.origin.y, 0, screensize.y) state.transform = xform نلاحظ أن الدالة ()integrate_forces_ تستقبل معاملًا باسم state، وهو كائن من النوع PhysicsDirectBodyState2D يمثل الحالة الفيزيائية الحالية للجسم الصلب. يحتوي هذا الكائن على معلومات مثل موضع الجسم position والقوى المؤثرة forces والسرعة velocity في الاتجاهات المختلفة وغيرها من الخصائص الفيزيائية. وبالتالي يمكن أن نحصل من خلال state على التحويل الحالي للجسم current transform أي موقعه واتجاهه، ثم نعدّل إحداثيات الموقع باستخدام الدالة ()wrapf لتطبيق التفاف حول الشاشة مما يعني أن الجسم سيتجاوز حافة الشاشة ويظهر من الجهة المقابلة لإعطاء تأثير الحركة المستمرة. وأخيرًا، نُعيد ضبط التحويل الجديد داخل state لضمان استمرار حركة الجسم بشكل طبيعي وواقعي. ستبدو الحركة كما في الصورة التالية: الانتقال الفوري لنلقِ نظرة على مثال آخر يوضح استخدام ()integrate_forces_ لتغيير حالة الجسم دون مشكلات. لنضف آلية انتقال آني أو فوري بحيث يتمكن اللاعب من نقل المركبة الفضائية فورًا إلى موقع عشوائي داخل الشاشة عند الضغط على مفتاح مخصص. نضيف بداية متغيرًا جديدًا: var teleport_pos = null نحضّر تاليًا موقعًا عشوائيًا ضمن الدالة ()get_input: if Input.is_action_just_pressed("warp"): teleport_pos = Vector2(randf_range(0, screensize.x), randf_range(0, screensize.y)) وأخيرًا وضمن الدالة ()integrate_forces_ سنستخدم teleport_position إن كان موجودًا ومن ثم نمسحه: if teleport_pos: physics_state.transform.origin = teleport_pos teleport_pos = null وبهذا نكون قد أضفنا ميزة الانتقال الفوري للمركبة الفضائية بشكل آمن ومتوافق مع نظام فيزياء جودو. الخاتمة بهذا نكون وصلنا لختام هذا المقال الذي استعرضنا فيه كيفية استخدام محرك الفيزياء في جودو لتحريك الأجسام الصلبة والتحكم بها بشكل دقيق لتوفير تجربة حركة تفاعلية واقعية ضمن بيئات ثنائية وثلاثية الأبعاد مع التحكم الكامل في حركة الأجسام. بإمكانك تحميل المثال كاملًا عبر مستودعه على جيتهب أو من هنا مباشرة asteroids_physics.zip ترجمة -وبتصرف- للمقال: Asteroids-style Physics (using RigidBody2D) اقرأ أيضًا المقال السابق: التفاعل بين الشخصيات والأجسام الصلبة في جودو بناء وتنفيذ لعبة ثنائية الأبعاد في جودو العمل مع إجراءات الدخل Inputs Actions في جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو التحريك باستخدام SpriteSheet و AnimationTree StateMachine في جودو
-
بدأنا في المقال السابق شرح بعض المفاهيم الرياضية الأساسية التي يحتاج مطور الألعاب لمعرفتها، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء النقطي Dot Product، والجداء الشعاعي Cross Product. وسنستكمل في مقال اليوم شرح مفهوم رياضي مهم وهو التحويلات الهندسية Transforms الذي يسمح لنا بتغيير مكان أو شكل الأشياء في الفضاء باستخدام المصفوفات. متطلبات العمل قبل المتابعة في قراءة هذا المقال، يجب توفر دراية جيدة عن اﻷشعة vectors وكيفية استخدامها في تطوير اﻷلعاب. لهذا ننصح بالعودة إلى سلسلة مقالات الأشعة على أكاديمية حسوب، ومطالعة مقال رياضيات اﻷشعة ضمن توثيق جودو الرسمي. التحويلات في المستوي ثنائي البعد نستخدم في المستوي أو في الفضاء ثنائي البعد اﻹحداثيات المألوفة X و Y، ولنتذكر أنه في محرك ألعاب جودو وفي معظم التطبيقات الرسومية في الحواسيب، يشير المحور Y إلى اﻷسفل كما في الصورة التالية: ولكي نوضح الفكرة، لنتأمل شكل سفينة الفضاء ثنائي البعد في الصورة التالية: تشير السفينة هنا إلى نفس اتجاه المحور X، فلو أردنا منها التحرك نحو اﻷمام، نضيف مقدار الحركة إلى الإحداثي اﻷفقي X فتتحرك نحو اليمين: position += Vector2(10, 0) لكن ما الذي يحدث عندما تدور السفينة؟ كيف يمكن اﻵن تحريكها نحو اﻷمام؟ إن كنتم تتذكرون علم المثلثات في المدرسة، فقد تبدأون بالتفكير في الزوايا والنسب المثلثية sin و cos، ثم تنفذون عملية حسابية مثل: position += Vector2(10 * cos(angle), 10 * sin(angle)) سيعمل هذا الحل، لكن هناك طرق أفضل تلائم عملنا مع اﻷلعاب تعتمد بشكل أساسي على مفهوم التحويلات الهندسية transforms. لنلق نظرة مجددًا على السفينة التي تدور، ولنتخيل هذه المرة أن للسفينة منظومة إحداثياتها الخاصة التي تحملها معها ولا تتعلق بإحداثيات الشاشة العامة: تُخزّن هذه اﻹحداثيات المحلية ضمن الكائن transform، وبالتالي، يمكن تحريك السفينة إلى اﻷمام وفقًا للمحور X الخاص بها ولا حاجة أن نفكر بالزوايا والدوال الرياضية اﻷخرى. ولتنفيذ اﻷمر في جودو، نستخدم الخاصية transform التي تمتلكها جميع العقد المشتقة من Node2D. position += transform.x * 10 ينص السطر السابق على إضافة الشعاع X للتحويل مضروبًا بالعدد 10. لنشرح هذا الأمر بشيء من التفصيل، تضم الخاصية transform اﻹحداثيين x و y الممثلان للإحداثيات المحلية الخاصة بالعقدة، وهما شعاعي واحدة unit vector أي أن طويلة كل منهما تساوي الواحد. كما يطلق على هذان الشعاعان شعاعي توجيه direction vectors، ويدلان على الاتجاه الذي يشير إليه المحور X الخاص بالسفينة. نضرب بعد ذلك شعاعي التوجيه بالعدد 10 لتكبيرهما والانتقال إلى مسافة أبعد. ملاحظة: تتعلق الخاصية transform لعقدة بالعقدة اﻷم لها أي تنسب إحداثياتها الخاصة إلى إحداثيات العقدة اﻷم. فإن أردنا الحصول على اﻹحداثيات العامة بالنسبة إلى الشاشة، نستخدم global_transform. تضم الخاصية transform إضافة إلى المحاور المحلية، مكونًا يُدعى شعاع الأصل origin ويمثل اﻹنسحاب translation أو تغيير الموضع. يمثل الشعاع اﻷزرق في الصور التالية شعاع اﻷصل transform.origin ويساوي شعاع الموضع position للكائن: التحويل بين الفضاء المحلي والعام يمكننا تحويل اﻹحداثيات من الفضاء المحلي للعقدة إلى الفضاء العام عن طريق التحويلات. حيث تضم العقد من النوع Noode2D والنوع Spatial في جودو دوال برمجية مساعدة مثل ()to_local و ()to_global لتحقيق هذا الأمر: var global_position = to_global(local_position) إليكم مثالًا عن كائن في مستوي ثنائي البعد، ونريد تغيير موقع نقرة الفأرة -وهو في الفضاء العام- أي المكان الذي نقرنا فيه على الشاشة، إلى إحداثيات محلية منسوبة إلى الكائن، بمعنى آخر نريد معرفة مكان النقرة من منظور الكائن نفسه بدلاً من المكان على الشاشة، لتحقيق ذلك نكتب الكود التالي: extends Sprite func _unhandled_input(event): if event is InputEventMouseButton and event.pressed: if event.button_index == BUTTON_LEFT: printt(event.position, to_local(event.position)) للمزيد حول آلية التحويل من إحداثيات عامة إلى إحداثيات محلية في محرك جودو ننصحكم بقراءة توثيق Transform2D للاطلاع على كافة الخاصيات والتوابع المتاحة. التحويلات في الفضاء ثلاثي البعد يطبق مفهوم التحويل في الفضاء ثلاثي البعد 3D بنفس أسلوب تطبيقه في الفضاء ثنائي البعد 2D، بل يغدو تطبيقها أهم لأن العمل مع الزوايا في الفضاء ثلاثي البعد سيقود إلى مشكلات عديدة كما سنوضح بعد قليل. ترث العقد ثلاثية البعد من العقدة الأساسية Node3D التي تضم معلومات التحويل. ويحتاج التحويل في الفضاء ثلاثي البعد لمعلومات أكثر مقارنة مع الفضاء الثنائي البعد. حيث يبقى شعاع الموضع Position محفوظًا ضمن الخاصية Origin، لكن الدوران موجود ضمن خاصية تدعى basis تضم ثلاثة أشعة واحدة unit vectors تمثل المحاور اﻹحداثية المحلية الثلاث للعقدة X و Y و Z. وعندما نختار عقدة ثلاثية البعد في محرر جودو، سنتمكن باستخدام نافذة Gizmo من عرض التحويلات والتعامل معها. تفعيل نمط الفضاء المحلي Local Space لنتذكر أن الفضاء العام Global Space هو الفضاء الذي يعتمد على محاور المشهد العامة. بمعنى آخر، إذا كنا نحرك أو ندير كائنًا في هذا الفضاء، فإن تحركاته ستكون بناءً على محاور العالم أو المشهد الذي يوجد فيه هذا الكائن، أما الفضاء المحلي Local Space فهو الفضاء الذي يعتمد على محاور الكائن نفسه. أي أن للكائن لديه محاور خاصة به مثل المحور X و Y و Z الخاص به وعندما نحرك أو ندير الكائن في الفضاء المحلي، فإن تحركاته تكون بالنسبة له هو، وليس بالنسبة للمشهد بأكمله. يتيح لنا محرر جودو عرض الاتجاهات المحلية للجسم والتعامل معها بسهولة، وذلك من خلال تفعيل خيار Local Space Mode، مما يسمح بتحريك الجسم أو تدويره وفقًا لمحاوره الخاصة بدلًا من محاور المشهد العامة، وستمثل المحاور الثلاث الملونة في هذا الوضع المحاور اﻷساسية المحلية للجسم. وكما هو الحال في الفضاء ثنائي البعد، يمكننا في الفضاء ثلاثي الأبعاد استخدام المحاور المحلية لتحريك الجسم إلى اﻷمام. وفي هذه الحالة، يكون المحور Y Y-Upوفق نظام Y-Up أي أنه موجه نحو الأعلى، وبالتالي سيكون الاتجاه الأمامي للجسم بشكل افتراضي هو المحور السالب Z-وبالتالي كي نحرك الجسم للأمام حسب اتجاهه الخاص -وليس حسب اتجاه المشهد- نكتب الكود التالي: position += -transform.basis.z * speed * delta تلميح: يمتلك جودو قيم معرّفة افتراضيًا لبعض الاتجاهات الشائعة، على سبيل المثال يمثل الاختصار Vector3.FORWARD الاتجاه الأمامي في الفضاء ثلاثي الأبعاد: Vector3.FORWARD == Vector3(0, 0, -1) الخاتمة تعلمنا في هذا المقال كيف يمكن لمطور الألعاب أن يتعامل مع التحويلات الهندسية عمليًا داخل محرك ألعاب جودو سواء في المحرك ثنائي البعد 2D أو ثلاثي البعد 3D ويستفيد منها في التحكم بحركة واتجاه العناصر داخل اللعبة من خلال الخصائص المدمجة في المحرك وبعيدًا عن التعقيدات الرياضية مثل الزوايا الدوال المثلثية. ترجمة -وبتصرف- لمقال Transforms اقرأ أيضًا المقال السابق: مفاهيم رياضية أساسية في تطوير اﻷلعاب إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
-
تعتمد الأعمال الرقمية حاليًا على الخدمات السحابية لتسهيل التفاعل مع العملاء، ويتطلب الأمر تجميع وتخزين ومعالجة كميات هائلة من البيانات قبل تقديمها إلى المستخدم النهائي. وهنا يأتي دور تطبيقات الويب السحابية. فعندما نتكلم عن الخدمات السحابية نتذكر مباشرة النماذج التالية: البرمجيات كخدمات سحابية Software as a services واختصارًا SaaS منصات العمل كخدمات سحابية Platform as a Service واختصارًا PaaS البُنى التحتية كخدمات سحابية Infrastructure as Service واختصارًا IaaS سنناقش في مقالنا نماذج الأعمال الثلاث السابقة بالتفصيل لتكوين فكرة واضحة عن فائدة الخدمات السحابية لأعمالنا. لكننا سنلقي نظرة أولًا على مفهوم الحوسبة السحابية قبل أن نخوض في خدماتها. ما هي الحوسبة السحابية الحوسبة السحابية هي طريقة حديثة في الوصول إلى البيانات والمعلومات عبر شبكة الإنترنت بدلًا من الأقراص الصلبة، وهي وسيلة سريعة وآمنة وأكثر فعالية من أنظمة التخزين التقليدية. وقد ازداد استخدام الحوسبة السحابية حاليًا وفي مختلف القطاعات لكونها توفر حلًا ناجحًا للأعمال النامية أو التي أسست حديثًا نظرًا لحرية التوسع عند الحاجة. عند استخدام الخدمات السحابية لن نعتمد على عتاد أجهزتنا المحلية، إذ يمكننا الوصول إلى بياناتنا افتراضيًا ومن أي مكان، وطالما أنها متاحة على الشبكة، سنتمكن من الوصول إليها في أي وقت، فلن نضطر إلى استثمار الكثير على العتاد الصلب عند إطلاق أو توسيع أعمالنا بفضل الحوسبة السحابية، وكل ما علينا حجز مساحات إضافية عندما نحتاج لتوسيع العمل. أنواع الخدمات السحابية فيما يلي مقارنة سريعة بين البرمجيات والمنصات والبنى التحتية كخدمات: IaaS PaaS SaaS طبيعة الخدمة تقدم أساسًا لإنشاء البنى التحتية للخدمات السحابية وتؤمن نموذج الدفع وفقًا للاستخدام أطراف خارجية تؤمن أدوات أو تطبيقات للمستخدمين عبر الإنترنت من خلال خدمات البنية التحتية الخاصة بهم تؤمن وصولًا إلى تطبيقات الويب عبر نموذج الدفع وفقًا للاستخدام الإيجابيات مقبولة التكلفة ومرنة وقابلة للاسترجاع عند حدوث المشاكل. وكذلك سهلة الوصول ويمكن الاعتماد عليه مدروسة التكاليف وانتاجية متزايدة. متجاوبة ورشيقة. سهلة الوصول وقابلة للتوسع بسهولة. قابلة للتوسع وسهل الوصول ومقبولة التكلفة. كما أنها سهلة الترقية والنشر السلبيات صعوبة التحكم بها وتعاني بعض المشاكل الأمنية تعاني مشاكل في التوافق وتغييرات في موزعي الخدمة أمان غير كاف للبيانات وتحكم أقل مزودو الخدمة خدمات أمازون ويب AWS و محرك حوسبة جوجل GCE وديجتال أوشن DigitalOcean AWS ElasticBeanstalk و Apache و OpenShift و Heroku Google Workspace و Salesforce و Cisco و WebEx و Dropbox البُنى التحتية كخدمات سحابية IaaS تؤمن خدمة البنية التحتية السحابية وحدات البناء الأساسية للبنية التحتية لأي سحابة وتقدم موارد حاسوبية مثل المعالجة والآلات الافتراضية والشبكات وأكثر. وتسهل IaaS دعم الأعمال الصغيرة والمنظمات التي تستهدف حلولًا سحابية غير مكلفة وتعمل وفق نموذج الدفع وفقًا للاستخدام pay as you go وبالتالي سيدفع المستخدم تكلفة الخدمات التي يحتاجها فقط دون أية تكاليف إضافية، وهي متاحة لتوزيع الخدمات والموارد بشكل عام أو خاص أو هجين. تلغي هذه الخدمة السحابية التكاليف الإضافية الناتجة عن إدارة واستخدام العتاد الصلب وتوكل أمرها لمزود الخدمة. ويكون المستخدم النهائي مسؤولًا فقط عن إدارة الموارد مثل البيانات والتطبيقات، بينما ينظم المزوّد العمليات الافتراضية وإدارة الشبكة وتخزين البيانات. تساعد IaaS في توفير الوقت والتكلفة لأن مزود الخدمة هو من يهتم بإدارة العتاد الصلب. وطالما أن استخدام تلك الموارد هو فقط عند الحاجة، لن يكون هنالك وجود لمصادر مهملة، وستدفع فقط على ما تستخدمه فعليًا. من الأمثلة عليها نجد: خدمة أمازون ويب AWS، ومحرك حوسبة جوجل GCE، ومايكروسوفت آزور، وديجتال أوشن DigitalOcean. متى نستخدم نموذج IaaS يصلح نموذج IaaS لكل رائد أعمال أو خبير يحتاج لخدمة سحابية تعتمد نموذج الدفع وفقًا للاستخدام سيكون. ويمكن أيضًا الاستفادة من الخدمة إن كنا نحاول توسيع عملنا لكننا نراقب التكاليف بحذر، أو كان لدينا كميات كبيرة من البيانات التي تحتاج لمعالجتها وتخزينها. كما تعد الخدمة كذلك ملائمة للأفراد القلقين من حدوث كوارث أو مشكلات أو فقدان بيانات في البنية التحتية ضمن منازلهم، وهكذا لن ينشغلوا بأمور حماية بياناتهم فهي ليست على صفحة ويب بل داخل مركز بيانات، كما تقدم IaaS الموارد الشبكية الضرورية لتشغيل التطبيقات والخدمات في البيئة السحابية. الإيجابيات غير مكلفة مرنة إمكانية استعادة البيانات عند حدوث كوارث سهل الوصول موثوقة ويمكن الاعتماد عليها السلبيات صعوبة التحكم مشاكل في أمان البيانات منصات العمل كخدمات سحابية PaaS نموذج PaaS هو نموذج خدمات سحابية توفر فيه أطراف خارجية أدوات عبر الإنترنت للمطورين، معتمدة على بنية تحتية خاصة بها لتطوير التطبيقات. حيث يمكن للمطورين من خلال PaaS إنشاء تطبيقات قابلة للتوسع دون الحاجة لإعداد وإدارة قواعد البيانات والخوادم والشبكات والبنى التحتية لتخزين البيانات. ويستفيد المطورون الجدد من PaaS في تسهيل تطوير التطبيقات، وسيتمكن أي مطور من استخدام متصفحه فقط لتطوير التطبيق، كما تتحمل PaaS مسؤولية تحديث البنية التحتية الخاصة بنا وبالتالي لن نقلق بشأن صيانة تطبيقاتنا. كما يستفيد مطورو الأعمال من PaaS لكونها تؤمن بيئة عمل جماعية للمطورين الذين يعملون على المشروع ذاته، وتقدم أسلوبًا سريعًا في إنشاء التطبيقات نظرًا لسهولة توسيعها ومرونتها. من الأمثلة عليها نجد: AWS Elastic Beanstalk و Apache Stratos و Force.com و OpenShift و Heroku متى نستخدم نموذج PaaS إن كنا بحاجة إلى تطوير أعمالنا وتشغيل تطبيقات ويب دون تكلف الكثير على إعداد البرمجيات والعتاد الصلب، يمكن أن نفكر في استخدام PaaS. إذ تركز هذه الخدمة أساسًا على حماية بياناتنا وهو أمر حيوي جدًا في حال اخترنا الخدمة السحابية لتخزين البيانات. وعلينا أيضًا التفكير بخدمة PaaS إن أردنا من فريق المطورين التركيز على بناء التطبيقات بدلًا من الإنشغال بإصدار التحديثات الأمنية، وهذا ما سيخفف تكاليف الحمولات الزائدة ويوفر الوقت والجهد. الإيجابيات تكلفة مناسبة إنتاجية متزايدة رشيقة ومتجاوبة سهلة التوسع سهلة الوصول السلبيات مشاكل في التوافقية تغيّر مقدمي الخدمات البرمجيات كخدمات سحابية SaaS توفر لنا البرمجيات كخدمة SaaS إمكانية الوصول إلى تطبيقات الويب عبر الإنترنت، ولا حاجة معها إلى تنزيل أية أدوات أو برمجيات، وقد تكون مجانية أو تعمل وفق مبدأ الدفع وفقًا للاستخدام. ويمكن للمستخدمين الوصول إلى التطبيقات عبر أي جهاز بصرف النظر عن البنية التحتية لمقدم الخدمة أو صيانة التطبيقات أو أي شيء آخر، فهي أمور يديرها ويحميها مزوّد الخدمة السحابية. تفضل كثير من الأعمال استخدام خدمة SaaS نظرًا لانتشارها الواسع وعدم الحاجة إلى تكاليف خاصة أو تنزيل وتثبيت تلك البرمجيات. مع ذلك، تعتمد هذه الخدمة كليًا على موزعين خارجيين وليس للمستخدم القدرة على التحكم بالخدمة أو تغييرها. من الأمثلة عنها نجد: Google Workspace و Salesforce و Cisco WebEx و Dropbox. متى نستخدم نموذج SaaS إن أردنا الابتعاد عن تثبيت برامجنا محليًا فهذه الخدمة هي الحل، إذ تلغي الحاجة إلى الميزانيات الكبيرة وضغط العمل. تقدم لنا هذه الخدمة التطبيقات التي نحتاجها مستضافة من قبل أطراف خارجية وموزعة وفق معماريات خاصة بتلك الأطراف مما يجعلها قابلة للوصول من خلال الإنترنت. يمكن للأعمال الصغيرة الاستفادة من SaaS في حال لم نمتلك الميزانية الكافية أو كادر العمل لبناء تطبيقات خاصة بها. ويستخدم العديد من متخصصي تقانة المعلومات والمنظمات تطبيقات SaaS، وبإمكان مستخدمي B2B و B2C الاستفادة من تطبيقات SaaS على خلاف الخدمات السحابية الأخرى الإيجابيات قابلة للتوسع وسهلة الوصول غير مكلفة سهلة التحديث سهلة التوزيع السلبيات لا تقدم حماية كافية للبيانات تحكم أقل الاختلافات بين الخدمات السحابية SaaS و PaaS و Iaas عندما نقارن بين هذه الخدمات السحابية من ناحية المرونة تبرز خدمة IaaS. إذ تعتمد المرونة تمامًا على موزع الخدمة الذي نختاره، وكذلك الأمر من ناحية الأمان. وتُدفع تكاليف هذه الخدمة عادة بالساعة وفقًا للاستخدام وبالتالي قد ترتفع تكاليفها نظرًا لأسلوب الدفع الدقيق المرتبط بها. من ناحية أخرى، تعالج PaaS مشكلة البرمجة المتقدمة عالية المستوى بتسهيل وتبسيط العمليات، مما يجعل عملية تطوير التطبيقات أقل كلفة وزمنًا. وبالنسبة إلى التكلفة، فستزداد مع توسيع التطبيق ونموه. وبمجرد أن نلتزم مع موزع محدد فسنكون مقيدين ببيئة العمل والواجهة التي نختارها. أخيرًا، لخدمة SaaS سقف سعر فهي أرخص من كلتا الخدمتين السابقتين، وهي نعمة حقيقية للأشخاص والأعمال الصغيرة. لكن في المقابل سيكون تحكمنا في الخدمة محدودًا أو غير متاح، فمزود الخدمة هو من يدير معظم تفاصيلها. يمثل المخطط التالي الحجم السوقي للخدمات السحابية بين عامي 2018 و2024، وشعبية كل خدمة من الخدمات السابقة: المصدر بالنسبة لحرية التحكم بالخدمة سنجد أن نموذج IaaS في المقدمة، فهو يسمح لنا بإدارة التطبيقات والبيانات والبرمجيات الوسيطة ونظام التشغيل. بينما يسمح لنا نموذج PaaS بإدارة البيانات والتطبيقات فقط، ويدير مقدم الخدمة معظم أو كل النواحي في SaaS. اختيار الخدمة السحابية المناسبة علينا التفكير أولًا بحجم تبادل البيانات في موقعك أو حركة المرور إليه ونستغل قدرات المعالجة والتخزين التي تلائم حركة المرور تلك بأفضل ما يمكن، فقد نواجه مشكلات نحن بغنى عنها إن اخترنا خدمة سحابية غير ملائمة. وقد ينهار موقعنا إن لم تؤمن الخدمة التي اخترناها قدرات معالجة مناسبة وقد نضطر إلى دفع مبالغ إضافية على البنية التحتية السحابية حتى لو كانت حركة المرور إلى موقعنا منخفضة. وعلينا إضافة إلى ذلك أخذ عوامل مهمة أخرى بعين الاعتبار عند اختيار مزود الخدمة السحابية مثل أوقات توقف الخدمة downtime والترحيل migration لنقل التطبيقات إلى مكان آخر. خدمات حوسبة سحابية أخرى إلى جانب الخدمات الثلاث التي تحدثنا عنها في الفقرات السابقة نجد خدمات أخرى مثل: الخدمة السحابية DBaas تُعد قاعدة البيانات كخدمة سحابية DBaaS خدمة سحابية مدارة تستضيف قواعد البيانات وتسمح بالوصول إلى خدماتها دون إدارة أية برمجيات أخرى. وكغيرها من الخدمات لن نحتاج فيها إلى شراء أو إعداد عتادنا الصلب أو التعامل مع أية برمجيات لتثبيت قواعد بيانات. إذ تهتم معمارية هذه الخدمة مع الطرف المزود لها بكل شيء من النسخ الاحتياطي إلى التحديثات لضمان التوفر الدائم للخدمة ومعايير الأمان القوية. الإيجابيات سهولة العمل والتكيف مع التغييرات. غير مكلفة موثوقة لن نحتاج لبناء منظومة قواعد بيانات أو توظيف مطوري قواعد بيانات أوقات توفرها ممتازة السلبيات تحكم محدود مشاكل في خصوصية البيانات الخدمة السحابية Daas تُعد البيانات كخدمة Daas نهجًا مشابهًا لخدمة SaaS، إذ تؤمن خدمات تخزين ومعالجة وتكامل البيانات سحابيًا إلى مستخدميها عبر الإنترنت، ولا تتطلب تثبيت أو إدارة أية برمجيات. وتساعد Daas المستخدمين في الحد من تضخم البيانات data sprawl والحاجة إلى مجمعات تخزين data silos وتحسّن التعاون بين فرق العمل عبر مشاركة البيانات بينهم. الإيجابيات بيئة مقادة بالبيانات صيانة مؤتمتة تحسين نوعية البيانات السلبيات مشكلات في الخصوصية تعقيد البيانات الخدمة السحابية Faas تُعد الدوال كخدمة Functions as a Service واختصارًا FaaS خدمة سحابية ذات معمارية مبنية على الأحداث وخفية الخوادم serverless. تعمل هذه الخدمة على مبدأ كتابة دوال وتنفيذها كاستجابة لحدث ما، وتعتمد نموذج الدفع وفقًا للاستخدام ولن تكون هناك تكاليف إضافية. الإيجابيات ندفع فقط مقابل ما نستخدم زيادة في إنتاجية المطور توسع تلقائي السلبيات دعم محدود للعديد من التقنيات تحكم أقل بالمنظومة الخلاصة لا بد من الاستثمار في الخدمات السحابية إن أردنا مواكبة المعايير التي تتغير باستمرار. ليس لأنها تقدم خدمة أفضل للمستخدم فقط، بل لأنها تساعد أعمالنا على النمو أيضًا. حيث تخفف الخدمات السحابية من سلبيات ومحدودية البنى التقليدية لتقانة المعلومات، وسيعتمد اختيارنا للخدمة المناسبة على طبيعة العمل والطريقة التي نريدها في تشغيل التطبيقات السحابية. ترجمة -وبتصرف- لمقال: IaaS vs. PaaS vs. Saas how are the differents لصاحبه Sarim Javaid اقرأ أيضًا مفهوم السحابة Cloud مقدمة إلى الخوادم السحابية نظرة عامة على الحوسبة السحابية مقدمة إلى الاستضافة السحابية Cloud Hosting
-
يحتاج أي مطور ألعاب لمعرفة بعض المفاهيم الرياضية الأساسية ليتحكم في انتقال الشخصيات من حالة لأخرى بانسيابية ويتحكم في اتجاهاتها وتمكينها من معرفة ماذا يوجد أمامها وخلفها، وسنناقش في مقال اليوم مفاهيم تستخدم بكثرة في تطوير اﻷلعاب، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء الداخلي أو النقطي أو السلمي Dot Product، والجداء الخارجي أو الشعاعي Cross Product. قد تبدو هذه المصطلحات مقعدة وغامضة لمن يسمعها لأول مرة، لكن لا داعي للقلق فبعد قراءة هذا المقال ومعرفة تطبيقاتها العملية في برمجة اﻷلعاب ستغدو سهلة وبسيطة. الاستيفاء العددي تُعطى الصيغة اﻷساسية للاستيفاء الخطي رياضيًا كالتالي: func lerp(a, b, t): return (1 - t) * a + t * b يمثل كل من a و b قيمتين، بينما تمثل t مقدار الاستيفاء بينهما أي النسبة التي تحدد إلى أي درجة ننتقل من a إلى b. وتتراوح قيم t نمطيًا بين 0 وعندها سيعيد الاستيفاء قيمة a، وبين القيمة 1 وعندها سيعيد الاستيفاء قيمة b. يعطي تابع الاستيفاء قيمة ما بين a و b كما في المثال التالي: x = lerp(0, 1, 0.75) # x is 0.75 x = lerp(0, 100, 0.5) # x is 50 x = lerp(10, 75, 0.3) # x is 29.5 x = lerp(30, 2, 0.75) # x is 9 يُدعى هذا الاستيفاء بالاستيفاء الخطي لأنه يعدّ المسافة بين نقطتي الاستيفاء خطًا مستقيمًا. يمكننا تحريك أي خاصية لعقدة ما باستخدام الدالة ()lerp، فلو قسمنا الفترة الزمنية للحركة إلى فترات محددة سنحصل على قيمة بين الصفر والواحد يمكننا استخدامها لتغيير الخاصية المطلوبة بنعومة وسلاسة خلال مدة التنفيذ. كمثال على ذلك، يضاعف السكريبت التالي حجم الشخصية خمس مرات أثناء اختفائها باستخدام modulate.a وتستغرق الحركة ثانيتين: extends Sprite2D var time = 0 var duration = 2 # المدة الزمنية للتأثير func _process(delta): if time < duration: time += delta modulate.a = lerp(1, 0, time / duration) scale = Vector2.ONE * lerp(1, 5, time / duration) الاستيفاء الشعاعي من الممكن الاستيفاء أيضًا بين شعاعين وهذا يعني إيجاد شعاع جديد يقع بينهما بناءً على مقدار معين t تمامًا مثل الاستيفاء بين رقمين، لكن هنا نتعامل مع اتجاهات أو مواقع في الفضاء، إذ توفر كلا العقدتين Vector2 و Vector3 التابع ()linear_interpolate لتنفيذ اﻷمر. فلكي نجد على سبيل المثال شعاعًا يقع في منتصف المسافة بين الشعاع الأمامي واليساري لعقدة من نوع Spatial، نستخدم الاستيفاء الخطي بين هذين الاتجاهين كما في الكود التالي: var forward = -transform.basis.z var left = transform.basis.x var forward_left = forward.linear_interpolate(left, 0.5) كما يحرك المثال التالي الشخصية نحو موقع النقر بالفأرة، وتتحرك العقدة نحو هذا الموقع لكنها لا تقف فجأة حيث تقل سرعة الاقتراب كلما اقترب الكائن أكثر من الهدف: extends Sprite2D var target func _input(event): if event is InputEventMouseButton and event.pressed: target = event.position func _process(delta): if target: position = position.linear_interpolate(target, 0.1) الجداء الشعاعي الداخلي والخارجي يمكن تنفيذ عمليتي جداء على اﻷشعة هما الجداء الداخلي السلمي أو النقطي dot product والذي تكون نتيجته عدد، والجداء الخارجي أو الشعاعي والذي تكون نتيجته شعاعًا. الجداء الداخلي هو عملية حسابية على شعاعين تكون نتيجته عدد حقيقي، وتمثل عادة على أنها مسقط شعاع A على حامل الشعاع اﻵخر B: تُعطى الصيغة الرياضية للجداء الداخلي بالعلاقة: حيث: θ : هي الزاوية بين الشعاعين ||A||: طويلة الشعاع اﻷول ||B||:طويلة الشعاع الثاني ولهذه العلاقة فائدة خاصة عند تسوية الشعاع أي عند جعل طويلته واحد، إذ تصبح العلاقة بالشكل التالي: تشير هذه العلاقة إلى الارتباط المباشر بين الجداء الداخلي والزاوية بين الشعاعين، وطالما أن cos(0)=1 و cos(180)=-1 ستدل قيمة الجداء السلمي على اتجاه الشعاعين بالنسبة لبعضهما، فهما في الزاوية 0 منطبقان وفي الزاوية 180 في اتجاهين مختلفين: وسنرى في فقرة قادمة كيف نستفيد من هذا الجداء عمليًا. الجداء الخارجي ينتح عن الجداء الخارجي لشعاعين شعاع ثالث عمودي على كلا الشعاعين، أي عمود على المستوي الذي يضمهمها، وتتعلق طويلة الشعاع الناتج بطويلتي الشعاعين اﻷصليين والزاوية بينهما. تعطى طويلة الشعاع الناتج عن الجداء الخارجي بالعلاقة: A x B = ||A||.||B||.sin(θ) //هي الزاوية بين الشعاعين θ وإن كانت طويلة كل من الشعاعين هي الواحد ستكون نتيجة الحساب أبسط، إذ تكون طويلة الشعاع الناتج قيمة بين 1- و 1. ملاحظة: طالما أن ناتج الجداء الخارجي بين شعاعين يعطي شعاعًا عموديًا على كلا الشعاعين، فهو عادة ما يُستخدم في المشاهد ثلاثية الأبعاد، حيث أن الشعاع الناتج يكون في اتجاه عمودي على مستوى الفضاء الذي توجد فيه الأشعة الأصلية. من ناحية أخرى، في أطر العمل ثنائية البعد ومن ضمنها جودو، لا يمكن تمثيل الشعاع العمودي داخل نفس المستوى، وبالتالي عند استخدام التابع Vectro2.cross في جودو، فإنه لا يُرجع شعاعًا جديدًا، بل عددًا يمثل طول الشعاع العمودي على الشعاعين في اتجاه الفضاء الثالث أو المحور z وتكون قيمته بين 1- و 1 وتعكس مدى التعامد بين الشعاعين. تطبيقات عملية لنلق نظرة على الصورة المتحركة التالية التي تمثل نتيجة جداء خارجي وداخلي لشعاعين ()Vector2.dot و ()Vector2.cross وكيف تتغير كل نتيجة مع تغير الزاوية بين الشعاعين: توحي هذه الصورة بتطبيقين شائعين لهذين التابعين، فإن كان الشعاع اﻷحمر هو الاتجاه اﻷمامي للكائن وكان اﻷخضر اتجاهًا نحو كائن آخر فسيساعد الجداء الداخلي في معرفة إن كان الكائن الثاني أمامنا -أي عندما تكون قيمة الجداء أكبر من الصفر- أو خلفنا - أي عندما تكون قيمة الجداء أصغر من الصفر. كما يساعد الجداء الخارجي في معرفة إن كان الكائن إلى اليسار -عندما تكون قيمة الجداء أكبر من الصفر- أو إلى اليمين -عندما تكون قيمة الجداء أصغر من الصفر-. الخاتمة تعرفنا في هذا المقال على مفاهيم رياضية أساسية مستخدمة بكثرة في تطوير الألعاب، مثل الاستيفاء الخطي والجداء الداخلي والجداء الخارجي، وتعلمنا كيفية استخدامها للتحكم بحركة شخصيات اللعبة، وتحديد اتجاهاتها، وتحسين تفاعل الكائنات داخل المشهد. من الضروري لأي مطور ألعاب تعلم هذه المفاهيم فهي بمثابة حجر الأساس في تطوير الألعاب وجعل حركة الشخصيات واقعية وسلسة. ترجمة -وبتصرف- للمقالين: Interpolation و Vectors:Using Dot product and Cross product اقرأ أيضًا المقال السابق: سحب وإفلات جسم صلب RigidBody2D في جودو استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تعرف على محرر محرك اﻷلعاب جودو Godot
-
نشرح في هذا المقال طريقة تفاعل شخصية اللاعب في جودو مع الأجسام الصلبة الموجودة في المشهد. ويمكن أن نطبق الطريقة التي سنشرحها على الفضائين الثنائي والثلاثي البعد على حد سواء. التفاعل مع الأجسام الصلبة إذا جربنا استخدام العقدة CharacterBody2D في محرك الألعاب جودو فسنجد أن العقدة CharacterBody2D التي تتحرك افتراضيًا من خلال تنفيذ أحد التابعين ()move_and_slide أو ()move_and_collide تصطدم بالأجسام الصلبة الفيزيائية من حولها لكنها لا تتمكن من دفع أي جسم تتصادم معه مثل صندوق أو عدو، فلن تتفاعل عقدة الجسم الصلب مع عقدة اللاعب إطلاقًا وستسلك سلوك العقدة StaticBody2D. فلو افترضنا أن شخصية اللاعب تمشي وتصطدم بصندوق، عندها ستتوقف أو تغير اتجاهها لكن الصندوق لن يتحرك، قد يكون هذا السلوك في بعض الحالات هو المطلوب فعلًا، لكن إن أردنا أن ندفع هذا الصندوق، سنحتاج لعمل بعض التغييرات. سنستخدم في هذا المثال شخصية ثنائية البعد يمكن تحميلها مع الموارد الأخرى للعبة من هذا المستودع، كما سنستخدم أكثر توابع الحركة شيوعًا لتحريك اللاعب وهو التابع ()move_and_slide الذي يستطيع تحريك الكائنات من نوع CharacterBody2D بشكل آمن ومتوافق مع نظام الفيزياء، حيث يتكفّل بإدارة الاصطدامات والانزلاق على الأسطح والتفاعل مع الجاذبية بشكل تلقائي دون الحاجة لكتابة منطق فيزيائي معقد. سنجد أن أمامنا خيارين لتحديد أسلوب التفاعل مع الأجسام الصلبة فبإمكاننا دفع هذه الأجسام متجاهلين الفيزياء. وهذا الأمر مماثل لخيار العطالة اللانهائية infinite inertia المستخدم في الإصدار 3 من جودو. كما أن بإمكاننا دفع الأجسام بناء على الكتلة المُتخيّلة للشخصية وسرعتها، وسيعطينا ذلك نتيجة واقعية. إذ ستدفع الشخصية الأجسام الثقيلة قليلًا والأجسام الخفيفة كثيرًا، وسنجرب تاليًا كلا الخيارين. العطالة اللانهائية لهذا الخيار إيجابياته وسلبياته. أما الإيجابية الأكبر فهي أنه لا يحتاج إلى شيفرة إضافية. وكل ما علينا هو ضبط طبقات أو أقنعة التصادم collision layers/masks بالشكل الصحيح لكل الأجسام. لتوضيح الأمر، عرّفنا في مثالنا ثلاث طبقات فيزيائية ووضعنا الجسم الصلب ضمن الطبقة رقم 3 وأبقينا على القناع كما هو لتقنيع كل الطبقات: وضعنا بعد ذلك اللاعب في الطبقة الثانية وهي الطبقة player وضبطنا القناع ليتجاهل العناصر الأخرى. عند تشغيل اللعبة، نلاحظ كيف يمكن للاعب دفع الصناديق، ولا يهم في هذه الحالة وزن الصناديق، إذ ستدفع جميعها بنفس المقدار. السلبية التي سنلاحظها في هذا الخيار هو تجاهل فيزياء حركة الصناديق. فبإمكان الصناديق تسلق الجدار، لكن لا يمكن للاعب القفز فوقها. لا بأس بهذا الأمر في بعض الألعاب، لكن إن أردنا منع الجسم من التسلق، علينا الاعتماد على الخيار الثاني. تطبيق الاندفاعات لمنح الجسم المتصادم دفعة لا بد من تطبيق اندفاع impulse، وهو دفعة آنية وكأننا نضرب كرة. وننوه لأن الاندفاع معاكس لمفهوم القوة وهي دفع الجسم باستمرار. # عطالة اللاعب var push_force = 80.0 func _physics_process(delta): # move_and_slide() بعد استدعاء for i in get_slide_collision_count(): var c = get_slide_collision(i) if c.get_collider() is RigidBody2D: c.get_collider().apply_central_impulse(-c.get_normal() * push_force) يتجه ناظم التصادم collision normal خارج الجسم الصلب، لهذا عكسناه ليتجه بعكس اتجاه الشخصية ويُطبّق العامل push_force. وهكذا ستدفع الشخصية الصناديق مجددًا لكنها لن تجبر الصناديق عندما تدفعها نحو الجدار على تسلقه. الخاتمة تعرفنا في هذا المقال على كيفية تفاعل شخصية اللاعب مع الأجسام الصلبة، واستعرضنا طريقتين أساسيتين لتحقيق دفع الشخصية لهذه الأجسام إما بتجاهل الفيزياء باستخدام العطالة اللانهائية، أو بتطبيق الاندفاعات للحصول على سلوك واقعي. يعتمد اختيار الطريقة الأنسب على طبيعة اللعبة والتجربة التي نرغب في تقديمها للاعب لتمنحه سلوكًا منطقيًا. بإمكانك تحميل المشروع كاملًا من مستودعه على جيتهب أو مباشرة من هنا character_vs_rigid.zip. ترجمة -وبتصرف- للمقال: Character to rigid Body interaction اقرأ أيضًا المقال السابق: سحب وإفلات جسم صلب RigidBody2D في جودو استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه استخدام الإشارات Signals في جودو Godot إنشاء خرائط مصغرة MiniMap للألعاب في جودو
-
الخرائط المصغرة هي عبارة عن واجهات رسومية صغيرة تظهر في زاوية شاشة اللعب، تعرض تمثيلًا مصغرًا لخريطة اللعبة الكاملة أو المنطقة المحيطة باللاعب. لتساعد على تحديد موقعنا داخل اللعبة وترينا أماكن الأعداء أو الأشياء المهمة من حولنا، فهي تعمل كنظام رادار لكشف الأعداء والأهداف المخفية وتوفر صورة عامة عن بيئة اللعب ككل. سنبني في هذا المقال خريطة مصغرة تعرض موقع اﻷشياء الواقعة خارج مجال رؤية اللاعب بشكل نقاط أو أيقونات صغيرة وسنحدث مواقع هذه النقاط كلما تحرك اللاعب. إعداد المشروع سننشئ لعبة تخطيطها من اﻷعلى إلى اﻷسفل وسنستخدم ميزة Autotile في محرك الألعاب جودو فهي تُسهّل كثيراً عملية رسم الخرائط باستخدام عناصر رقعة Tiles متداخلة وتمكننا من رسم الجدران أو الأرضيات بحرية، حيث يختار المحرك تلقائيًا الرقعة المناسبة من مجموعة الرقع حتى تتطابق الحواف والزوايا مع الرقع المجاورة، ويمكن تنزيل الصور المطلوبة لتطبيق المقال من مجلد assets من هذا الرابط أو منminimap_assets.zip سيبدو المشهد الرئيسي للعبة كالتالي: نستخدم العقدة CanvasLayer لتجميع عناصر واجهة المستخدم بما في ذلك الخريطة المصغرة التي سننشؤها في هذا المقال، ونستخدم العقدة TileMap لرسم الخريطة باستخدام الرقع tiles، بينما نستخدم العقدة Player لتمثيل شخصية اللاعب. تخطيط واجهة المستخدم الخطوة اﻷولى في مثالنا هي بناء تخطيط للخريطة المصغرة. وللتعامل مع أية عناصر واجهة مستخدم تضمها اللعبة، لا بد من إعادة تحجيمها بشكل سلس ودمجها جيدًا في تخطيط يتلائم مع الحاوية. لهذا سنضيف أولًا العقدة MarginContainer التي تساعدنا على وضع حواشي داخلية padding للعناصر داخل الحاوية، ونضبط الخاصية Constants في القسم Theme Overrides على 5. يضم عنصر التحكم هذا بقية العقد ويضمن أن العناصر داخل الحاوية ستبقى بعيدًا عن حواف الحاوية نفسها بشكل متناسق. سنسمي هذه العقدة MiniMap ثم نحفظ المشهد. نضيف تاليًا العقدة NinePatchRect وهي مشابهة للعقدة TextureRect لكنها تتعامل مع تغيير اﻷبعاد بطريقة مختلفة، إذ لا تمدد الزوايا أو الحواف مما يحافظ على مظهر الصورة بشكل أفضل عند تغيير الأبعاد. نفلت الصورة panel_woodDetail_blank.png من مجلد assets في لعبتنا في الخاصية Texture، وهي صورة أبعادها 128x128 بكسل، لكن إن غيرنا أبعاد العقدة MarginContainer ستصبح الصورة ممددة وسيئة المظهر. لكن مع استخدام NinePatchRect سنضمن أن اﻹطار سيحافظ على أبعاده فعند التمدد ستعمل العقدة على تقسيم الصورة إلى تسعة أجزاء بحيث تبقى الزوايا ثابتة وغير متمددة وتتوزع الحواف بطريقة تمدد سلسة بحيث لا تتشوه الصورة ويتمدد الوسط بطريقة مرنة لتعبئة المساحة. بإمكاننا تعريف هذه الخاصيات رسوميًا في اللوحة TextureRegion ، لكن من اﻷسهل أحيانًا إدخال القيم مباشرة. نضبط الخاصيات اﻷربعة الموجودة في Patch Margin على 64 ونغيّر اسم العقدة إلى Frame. لنلاحظ اﻵن ما يحدث عند تغيير اﻷبعاد: علينا تاليًا ملء الجزء الداخلي من اﻹطار بنمط يمثل شبكة وذلك باستخدام الصورة pattern_blueprintPaper.png: نريد اﻵن أن تملأ الصورة ما داخل اﻹطار تلقائيًا أيًا كانت أبعاده، وطالما أن المنطقة التي تغطيها الشبكة هي المكان الذي ستظهر فيه نقاط علام الخريطة، لا ينبغي أن تمتد الشبكة إذًا خارج حدود اﻹطار. لهذا، نضيف عقدة جديدة MarginContainer كابن للعقدة MiniMap وشقيق للعقدة Frame ثم نضبط خاصيات Constants في القسم Theme Overrides على القيمة 20. نضيف بعد ذلك عقدة TectureRect كابن للعقدة السابقة ثم نضبط قيمة الخاصية Texure على نفس الصورة السابقة، والخاصية Strech Mode على Tile ونسمي العقدة أخيرًا Grid. لنجرّب تغيير أبعاد العقدة اﻷصلية لرؤية تأثير ما فعلناه حتى اللحظة: لنبق أبعاد الخريطة المصغرة حاليًا على 200x200 بكسل، وبإمكاننا التأكد من هذه اﻷبعاد من الخاصية Size في القسم Layout. ستبدو اﻵن شجرة المشهد كالتالي: نقاط علام الخريطة سنضيف للخريطة نقاط العلام Marker ترمز كل منها لأشياء معينة في اللعبة، نضيف أولًا عقدة من النوع Sprite2D كابن للعقدة Grid ونسميها PlayerMarker لتمثل اللاعب ونمنحها الصورة minimapIcon_arrowA.png، وننتبه إلى أن قيمة الخاصية Position في القسم Transform هي (0,0)مما يجعلها في الزاوية العليا اليسارية من العقدة Grid. إن كانت أبعاد الشبكة (150,150)سيكون مركزها عند (75,75)، لنضبط إذًا موقع PlayerMarker على تلك القيمة، ننوه أن هذه العملية ستكون آلية لاحقًا. نضيف اﻵن عقدتين جديدتين من نوع Sprite2D ونسميهما AlertMarker و MobMarker ونمنحهما الصورتين minimapIcon_jewelRed.png التي تمثل جوهرة حمراء و minimapIcon_exclamationYellow.png التي تمثل علامة تعجب صفراء كما يلي: تمثل العقدتان السابقتان نوعين جديدن من نقاط العلام في عالم اللعبة. ننقر على الزر Toggle Visibility المجاور لكل منهما كي لا تظهر العقدة افتراضيًا. كتابة سكريبت نقاط العلام علينا اﻵن اتخاذ بعض القرارات. فطريقة نشر العلامات على الخريطة تتعلق كثيرًا بطريقة إعداد اللعبة. وطالما أن الهدف من المثال هو عرض الفكرة بسهولة، سنبقى العملية بسيطة ما أمكن، لكن في ألعاب أضخم يجب إيجاد نهج أقوى وأفضل. لدينا في مثالنا كائنان اﻷول Mob يتجول عشوائيًا في الخريطة والثاني Crate يمكن للاعب التقاطه. يتبعثر العديد من هذه الكائنات ضمن المشهد الرئيسي، ولا بد من تمثيل كل منها بأحد أنواع نقاط العلام التي تعرضها الخريطة. نضيف كل كائن نريده أن يظهر على الخريطة ضمن المجموعة minimap_objects ثم نضبط المتغير minimap_icon في سكريبت كل كائن على القيمة المناسبة من المجموعة: # mob في سكريبت: var minimap_icon = "mob" # crate في سكريبت: var minimap_icon = "alert" بإمكاننا اﻵن إضافة سكريبت إلى العقدة MiniMap. نرى أولًا في السكريبت مرجعًا إلى العقدة Player لنعطي الخريطة معلومة عن موقع اللاعب، ويمكن تعيين هذا الموقع ضمن الفاحص عند إضافة الخريطة المصغرة إلى المشهد الرئيسي، كما نرى خاصية zoom ومهمتها معايرة المقياس، أي إلى أي مدى نكبر أو نصغر العالم داخل الخريطة. كما أضفنا بعض المتغيرات يسبقها التوجيه Onready@ لجعل الوصول إلى العقد المطلوبة أكثر ملائمة فهو يعني إنشاء المتغير عندما تكون العقدة جاهزة أي بعد تحميلها داخل المشهد. extends MarginContainer class_name Minimap @export var player: Player @export var zoom = 1.5 @onready var grid = $MarginContainer/Grid @onready var player_marker = $MarginContainer/Grid/PlayerMarker @onready var mob_marker = $MarginContainer/Grid/MobMarker @onready var alert_marker = $MarginContainer/Grid/AlertMarker نستخدم تاليًا قاموسًا ونسميه minimap-icon لربط الأنواع بالرموز، حيث نظهر الكائن mob الذي يمثل العدو كنقطة حمراء على الخريطة، والكائن alert الذي يمثل تحذير كعلامة صفراء: @onready var icons = { "mob": mob_marker, "alert": alert_marker } نحتاج أيضُا إلى متغير يخزّن نسبة حجم الخريطة إلى حجم عالم اللعبة. كما نستفيد من قاموس آخر لإسناد نقاط العلام الفعالة إلى كل كائن. وسيكون المفتاح Key في القاموس هو الكائن نفسه أي نسخة عن Mob أو Crate والقيمة value هي نقطة العلام المسندة إليه: var grid_scale var markers = {} نضبط موقع نقطة علام اللاعب في منتصف الخريطة ضمن الدالة ()ready_، ونحسب عامل المقياس. ملاحظة: علينا توصيل اﻷشارة resized وتنفيذ خطوتي تحديد الموقع، وعامل المقياس ضمن دالة رد نداء callback إن كانت أبعاد واجهة المستخدم لدينا ديناميكية ومتغيرة الحجم وإلا فستظهر الرموز في أماكن خاطئة أو ستبدو بحجم غير مناسب. func _ready(): await get_tree().process_frame player_marker.position = grid.size / 2 grid_scale = grid.size / (get_viewport_rect().size * zoom) العقد الموجودة في الحاويات نظرًا للطريقة التي تعامل فيها العقدة Container أبناءها وتغير حجمهم أو موقعهم، لن نحصل على القيمة الصحيحة لأبعاد اﻷبناء وقت تنفيذ الدالة ()ready_، لهذا علينا أن ننتظر حتى اﻹطار التالي لنحصل على أبعاد الشبكة. ننشئ أيضًا نقطة علام لكل لكائن في اللعبة باستخدام المجموعة minimap_objects بمضاعفة العقدة المطابقة لنقطة العلام وربط العلامة بالكائن بالاستفادة من القاموس markers: var map_objects = get_tree().get_nodes_in_group("minimap_objects") for item in map_objects: var new_marker = icons[item.minimap_icon].duplicate() grid.add_child(new_marker) new_marker.show() markers[item] = new_marker بعد أن أنشأنا نقاط العلام وربطناها بالكائنات الموجودة، نستطيع اﻵن تحديث مواقعها ضمن الدالة ()process_. وإن لم يُعين أي لاعب player بعد، لا نفعل شيئًا: func _process(delta): if !player: return وﻹن كان هناك لاعب، ندور أولًا نقطة علام اللاعب لتطابق جهة حركته. وطالما أن نقطة العلامة PlayerMarker تتجه إلى اﻷعلى وليس بالاتجاه اﻷفقي x، لا بد من إضافة 90 درجة: player_marker.rotation = player.rotation + PI/2 نبحث اﻵن عن موقع كل كائن بالنسبة إلى اللاعب ونستخدمه في إيجاد موقع نقطة العلام، لنتذكر إزاحة الموقع بمقدار grid.size/2 لأن نقطة المرجع هي الزاوية العليا اليسارية: for item in markers: var obj_pos = (item.position - player.position) * grid_scale + grid.size / 2 markers[item].position = obj_pos تبقى المشكلة إمكانية ظهور بعض نقاط العلام خارج الشبكة كما في الصورة التالية: وﻹصلاح اﻷمر، نحصر موقع نقطة العلامة بمربع الشبكة باستخدام الدالة clamp بعد حساب المتغير obj_pos وقبل تحديد موقع العلامة: obj_pos = obj_pos.clamp(Vector2.ZERO, grid.size) بإمكاننا أيضًا معالجة العلامات التي تقع خارج الشاشة وخارج مربع الشبكة. باختيار أحد الحلين التاليين وقبل استخدام ()clamp. الخيار اﻷول هو كالتالي: if grid.get_rect().has_point(obj_pos + grid.position): markers[item].show() else: markers[item].hide() الخيار الثاني هو تغيير مظهر العلامات، بأن نجعلها أصغر لتدل على أنها أبعد مسافة: if grid.get_rect().has_point(obj_pos + grid.position): markers[item].scale = Vector2(1, 1) else: markers[item].scale = Vector2(0.75, 0.75) إزالة الكائنات ستزدحم اللعبة وتتوقف إن قُتِل أي كائن Mob أو التُقط كائن Crate لأن نقاط العلام حينها لن تكون صحيحة. لهذا نحتاج إلى طريقة نتأكد من خلالها من إزالة نقاط العلام في حال إزالة الكائنات، وفيما يلي طريقة سريعة سنتبعها في هذا المقال: نضيف signal removed إلى أي كائن وضعناه ضمن المجموعة minimap_object ثم نبث هذه الرسالة عندما يتدمر الكائن أو يُلتقط مع مرجع إلى الكائن نفسه كي تتعرف عليه الخريطة: removed.emit(self) نصل هذه اﻹشارات إلى الخريطة في الدالة ()ready_ للسكريبت الرئيسي: func _ready(): for object in get_tree().get_nodes_in_group("minimap_objects"): object.removed.connect(minimap._on_object_removed) نضيف اﻵن دالة استقبال اﻹشارات إلى سكريبت الخريطة المصغرة لتحرير نقطة العلام وإزالة المرجع: func _on_object_removed(object): if object in markers: markers[object].queue_free() markers.erase(object) تكبير وتصغير الخريطة المصغرة لدينا ميزة أخيرة سنضيفها إلى مثالنا وهو تحديد مستوى التكبير والتصغير للخريطة. إذ يغير تدوير عجلة الفأرة فوق الخريطة مقياسها تكبيرًا أو تصغيرًا. نضف بداية دالة تهيئة setter إلى الخاصية zoom: @export var zoom = 1.5: set = set_zoom func set_zoom(value): zoom = clamp(value, 0.5, 5) grid_scale = grid.size / (get_viewport_rect().size * zoom) نصل في نافذة الفاحص اﻹشارة _gui_input بالعقدة MiniMap حتى نتمكن من معالجة أفعال تدوير عجلة الفأرة: func _on_gui_input(event): if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_WHEEL_UP: zoom += 0.1 if event.button_index == MOUSE_BUTTON_WHEEL_DOWN: zoom -= 0.1 فيما يلي نتيجة الشيفرة: الخاتمة شرحنا في هذا المقال طريقة إضافة خريطة مصغرة لعالم اللعبة وحاولنا أن نجعلها مرنًا بما فيه الكفاية حتى نتمكن من تضمينها في أي لعبة نعمل عليها في جودو، ويمكن تحسين هذه الخريطة بإضافة الأمور التالية إليها: أنواع أكثر من نقاط العلام. إضافة وحدات أخرى للخريطة عند توليدها باستعمال الإشارات كما فعلنا تمامًا عند إزالة الوحدات الحصول على معلومات عند النقر على نقطة العلام استخدام صورة للخريطة الفعلية بدلًا من استخدام صورة الشبكة ترجمة -وبتصرف- للمقال: MinMap/Radar اقرأ أيضًا المقال السابق: إنشاء قائمة لاختيار مستوى اللعبة في جودو بناء شريط صحة ونصوص طافية في ألعاب جودو عرض عداد تنازلي وقائمة دائرية في جودو التعامل مع إجراءات دخل الفأرة في جودو
-
نشرح في هذا المقال طريقة اختيار أجسام صلبة وسحبها وإفلاتها من مكان لآخر باستخدام الفأرة في محرك الألعاب جودو، فقد يكون العمل مع الأجسام الصلبة مربكًا في جودو نظرًا لتحكم محرّك الفيزياء بهذه الحركة، وأي تدخل من قبلنا في الأمر سيقود غالبًا إلى نتائج غير متوقعة. إن مفتاح الحل في هذه الحالة هو استخدام الخاصية mode للجسم، ويُطبق هذا الأمر في الفضاء ثنائي البعد وثلاثي البعد. إعداد الجسم الصلب لننشئ مشروع لعبة جديدة ثنائية الأبعاد في جودو ونعمل مع كائن يمثل الجسم الصلب بإضافة العقدتين Sprite2D و CollisionShape2D. بإمكاننا أيضًا إضافة العقدة PhysicsMaterial إن أردنا ضبط خاصيتي الارتداد Bounce والاحتكاك Friction. كما سنستخدم الخاصية freeze لإبعاد الجسم عن سيطرة محرّك الفيزياء عندما نسحبه. وطالما أننا نريد من الجسم الصلب أن يكون قابلًا لحركة، لا بد من ضبط قيمة الخاصية Freeze في القسم Mode على القيمة kinematic بدلًا من القيمة الافتراضية Static. نضع الجسم ضمن مجموعة تدعى pickable، ونستخدمها لضم نسخ متعددة من الكائنات التي يمكن التقاطها في المشهد الرئيسي. نضيف سكريبت برمجي للجسم الصلب ثم نصل الإشارة _input_event الخاصة به كما يلي. extends RigidBody2D signal clicked var held = false func _on_input_event(viewport, event, shape_idx): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: print("clicked") clicked.emit(self) نبث الإشارة عندما نلتقط حدث النقر على الفأرة التي تتضمن مرجعًا إلى الجسم. ونظرًا لوجود عدة أجسام، سنترك أمر إدارة حالة الأجسام إن كانت قابلة للسحب أو أنها في حالة held أي إيقاف للمشهد الرئيسي main scene. فإن كان الجسم يُسحب، نغيّر موقعه باتباع حركة الفأرة. func _physics_process(delta): if held: global_transform.origin = get_global_mouse_position() فيما يلي الدالتان اللتان سنستدعيهما عند التقاط الجسم وإفلاته. ولنتذكر أن تغيير قيمة الخاصية freeze إلى true سيزيل الجسم من عمليات محرّك الفيزياء. سنلاحظ أن بقية الأجسام لا تزال قادرة على الاصطدام بهذا الجسم، فإن لم نرغب بهذا السلوك، نستطيع تعطيل الخاصية collision_layer مع أو بدون الخاصية collision_mask، ولا ننسى إعادة تمكينهما عند الإفلات. func pickup(): if held: return freeze = true held = true func drop(impulse=Vector2.ZERO): if held: freeze = false apply_central_impulse(impulse) held = false بعد إعادة قيمة الخاصية freeze إلى false في الدالة drop سيعود الجسم إلى سيطرة محرّك الفيزياء أي أنه سيتأثر بالجاذبية والتصادمات بشكل طبيعي. لهذا، يمكننا في هذه الحالة تمرير قيمة اندفاع impulse اختيارية، بإمكاننا إضافة إمكانية رمي الجسم عند تحريره بدل أن يسقط. دورة تطوير الألعاب ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة ومليئة بالتحديات اشترك الآن المشهد الرئيسي ننشئ مشهدًا رئيسيًا مع بعض العقبات أو نستخدم العقدة TileMap وننشر عدة نسخ من الأجسام التي يمكن التقاطها. ونبدأ سكريبت لمشهد الرئيسي بوصل الإشارة clicked في أي جسم قابل للالتقاط موجود في المشهد extends Node2D var held_object = null func _ready(): for node in get_tree().get_nodes_in_group("pickable"): node.clicked.connect(_on_pickable_clicked) نعرّف بعد ذلك الدالة التي نربط بها الإشارات، وتضبط هذه الدالة قيمة held_object حتى نعرف بوجود جسم يُسحب حاليًا، ونستدعي التابع ()pickup الخاص بالجسم. func _on_pickable_clicked(object): if !held_object: object.pickup() held_object = object عندما نحرر الفأرة خلال السحب بإمكاننا تنفيذ الخطوات المعاكسة. func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if held_object and !event.pressed: held_object.drop(Input.get_last_mouse_velocity()) held_object = null لنلاحظ كيف استخدمنا هنا التابع ()get_last_mouse_velocity لتمرير الدفع إلى الكائن. علينا أن ننتبه إلى ذلك جيدّا. مع ذلك، سنجد أننا نطلق الجسم الصلب بسرعة كبيرة وخاصة إذا كانت قيمة الخاصية mass للجسم قليلة. لهذا، من الأجدى أن نحدد القيمة العظمى للكتلة على قيمة مناسبة باستخدام التابع ()clamp. يمكن تجربة عدة قيم حتى نحدد ما يناسب لعبتنا. الخاتمة في هذا المقال، تعلّمنا كيفية التعامل مع الأجسام الصلبة في محرك الألعاب جودو بطريقة تتيح لنا اختيارها وسحبها وتحريكها باستخدام الفأرة. ورأينا كيف أن التحكم اليدوي بأجسام يتحكم بها محرّك الفيزياء قد يؤدي لسلوك غير متوقع، لذلك حللنا المشكلة بتغيير نمط الجسم إلى جسم مرتبط بالحركة Kinematic، وعطلنا خصائص الفيزياء مؤقتًا باستخدام freeze أثناء السحب. كما أنشأنا نظامًا بسيطًا يتيح التقاط الأجسام القابلة للسحب باستخدام الإشارات والمجموعات، وطبّقنا حركة واقعية للجسم عند الإفلات وأضفنا آلية رمي الجسم من خلال التقاط سرعة حركة الفأرة وتمريرها كدفعة. من خلال هذه الخطوات، أصبح بإمكاننا بناء لعبة تفاعلية تعتمد على سحب ورمي الأجسام بطريقة سلسة وطبيعية. يمكن تحميل المثال كاملًا من هذا الرابط لفهمه بصورة جيدة وتجربة التعديل عليه، أو تحميله مباشرة من هنا rigidbody_drag_drop-master.zip ترجمة -وبتصرف- للمقال: RigidBody2D Drag and Drop اقرأ أيضًا المقال السابق: استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو تعرف على واجهة محرك الألعاب جودو إنشاء الوحدات البنائية وشخصيات الخصوم في Unity3D
-
نشرح في هذا المقال طريقة بناء قائمة تتضمن مجموعة خيارات تظهر في بداية اللعبة وتساعد اللاعب على تحديد مستوى اللعبة. سيكون تصميم القائمة على شكل شبكة قابلة للتمرير وضمنها صناديق تمثل كل مستوى، بحيث يمكن التنقل بينها واختيار المرحلة التي يرغب اللاعب في اللعب فيها كما توضح الصورة التالية. مراحل بناء القائمة سنبني القائمة بتصميم شبكة قابلة للتمرير مكونة من صناديق لتحديد المستوى بحيث يمكن للاعب أن يختار فيما بينها. سنبدأ أولًا ببناء صندوق المستوى LevelBox بشكل مستقل. بناء صندوق المستوى LevelBox ستكون هيكلية العقدة اللازمة لبناء هذا الصندوق على النحو التالي: LevelBox: PanelContainer Label MarginContainer TextureRect استخدمنا العناصر التالية لتشكيل الصندوق: حاوية PanelContainer لتنظيم وعرض العناصر داخلها عقدة تسمية نصية Label لعرض رقم المستوى حاوية MarginContainer لإضافة هوامش حول المحتوى عقدة LevelBox من نوع TextureRect لعرض صورة قفل عندما يكون المستوى مغلق، ورقم عندما يكون مفتوح نتأكد من ضبط الخاصية Layout في القسم Custom Minimum Size للعقدة LevelBox على القيمة (110,110) ويمكن اختيار أي حجم آخر مناسب لتخطيط القائمة. نضيف سكريبت إلى العقدة التي تمثل صندوق المستوى من أجل وصل اﻹشارة gui_input التي تُطلق عندما تتلقى العقدة حدث إدخال مثل النقر بالفأرة أو الضغط على لوحة المفاتيح. حيث يمكن أن يكون المستوى مغلقًا أو مفتوحًا، وعند النقر على الصندوق سنرسل إرسال إشارة لاختيار المستوى. @tool extends PanelContainer signal level_selected # إشارة تُطلق عند تحديد المستوى @export var locked = true: # يحدد إذا كان المستوى مغلقًا set = set_locked @export var level_num = 1: # رقم المستوى set = set_level @onready var lock = $MarginContainer/Lock # صورة القفل @onready var label = $Label # رقم المستوى # دالة لضبط حالة القفل وإظهارأو إخفاء العناصر func set_locked(value): locked = value if not is_inside_tree(): # ننتظر حتى يكتمل تحميل العنصر داخل المشهد await ready lock.visible = value # نعرض صورة القفل إذا كان المستوى مغلقًا label.visible = not value # نظهر النص إذا كان المستوى غير مغلق # دالة لضبط رقم المستوى وتحديث النص المعروض func set_level(value): level_num = value if not is_inside_tree(): # ننتظر تحميل العنصر في المشهد await ready label.text = str(level_num) # تحديث النص برقم المستوى # دالة لمعالجة مدخلات المستخدم func _on_gui_input(event): if locked: # إذا كان المستوى مغلقًا، نتجاهل النقر return if event is InputEventMouseButton and event.pressed: # التحقق من النقر بزر الفأرة level_selected.emit(level_num) # إطلاق الإشارة مع رقم المستوى print("Clicked level ", level_num) # طباعة رسالة لاختبار النقر نستخدم في شيفرتنا التوجيه tool@ حتى نتمكن من تغيير قيم الخاصيات عبر نافذة الفاحص inspector ونرى التأثير مباشرة دون الحاجة لتشغيل المشهد. لنجرب اﻵن النقر على الخاصية Locked ونتحقق من رؤية صورة القفل تظهر وتختفي. وطالما أن اللعبة لا تضم مستويات فعلية لتحميلها، ستساعدنا الدالة ()print على اختبار التقاط حدث النقر. بناء الشبكة بعد إنهاء مشهد صندوق المستوى، سنضيف مشهدًا جديدًا يضم العقدة GridContainer ثم نضع ضمنها العدد الذي نريده من نسخ العقدة LevelBox، ونتأكد من ضبط قيمة الخاصية Columns على القيمة المناسبة. ننتقل للقسم Theme Overrides ثم Seperation ونضبط قيمتي الخاصيتين V Seperation و H Seperation على 10، ونحفظ المشهد باسم LevelGrid. سنستخدم في القائمة عدة نسخ عن عقدة الشبكة لعرض العدد المطلوب من المستويات. شاشة القائمة بإمكاننا اﻵن تجميع القائمة النهائية. توضح الصورة التالية التخطيط اﻷولي الذي سننفذه: ننشئ المشهد انطلاقًا من العقد التالية: LevelMenu: MarginContainer VBoxContainer Title: Label HBoxContainer BackButton: TextureButton ClipControl: Control NextButton: TextureButton نضبط خواص العقد كالتالي: العقدة LevelMenu: نضبط Theme Overrides ثم Constants ثم Margins على القيمة 20 العقدة VBoxContainer:نضبط Theme Overrides ثم Constants ثم Margins على القيمة 50 العقدة Title: ننسقها بالطريقة التي نريدها نضبط العقدتين BackButton و NextButton كالتالي: Ignore Texture Size: On لضبط حجم الزر وفقًا لحجمه داخل واجهة المستخدم Stretch Mode: Keep Centered للحفاظ على محاذاة المحتوى داخل الأزرار Layout/Container: On ليعمل الزر كحاوية Sizing/Horizontal/Expand: On لتوسيع الأزرار أفقيًا داخل المساحة المتاحة نضبط العقدة ClipControl كما يلي: Layout/Clip Contents:On لاقتصاص أي محتوى يتجاوز حجم الإطار Layout>Custom Minimum Size:(710, 350) لتحديد الحجم الأدنى للعقدة بعد ذلك نضع الشبكة ضمن العقدة ClipControl، بما أننا تمكين الخاصية Clip Content سيقتص محتوى العقدة إن كانت أكبر من عنصر التحكم. وسنتمكن اﻵن من بناء شبكة من صناديق المستويات قابلة للتمرير، لهذا نضيف عقدة من النوع HBoxContainer تُدعى GridBox إلى ClipControl إضافة إلى ثلاث نسخ من العقدة LevrlGrid أو أكثر إن أردنا. ونتأكد من ضبط الخاصية Theme Overrides ثم Constants ثم Separation على القيمة 0. ينبغي أن يبدو تخطيط المشهد اﻵن كما في الشكل التالي تقريبًا، مع العلم أننا عطلنا الخاصية Clip content لنعرض ما يحدث بشكل أوضح: الشبكات الثلاثة موجودة ضمن Clip Content لكن لا يمكن للمتحكم ClipControl عرض سوى شبكة واحدة كل مرة. لهذا ولكي ننتقل إلى الشبكتين الباقيتين عن طريق التمرير، لا بد من إزاحة GridBox مقدار 710 بكسل يمينًا أو يسارًا 110 (width of each LevelBox) * 6 (grid columns) + 10 (grid spacing) * 5 == 710 قد يتبادر للذهن سؤال عن عدم استخدام العقدة ScrollCointainer. هنا، بالتأكيد يمكن ذلك، لكننا لا نريد التنقل بين الشبكات باستمرار، ولا نريد أيضًا رؤية شريط تمرير. نضيف السكربت التالي إلى العقدة LevelMenu لوصل إشارتي pressed الخاصتين بكل زر: extends MarginContainer var num_grids = 1 var current_grid = 1 var grid_width = 710 @onready var gridbox = $VBoxContainer/HBoxContainer/ClipControl/GridBox func _ready(): # ترقيم جميع صناديق المستويات وإلغاء قفلها # يمكن استبداله بما يتناسب مع نظام المستويات في اللعبة # يمكن أيضًا ربط إشارات "level_selected" هنا num_grids = gridbox.get_child_count() for grid in gridbox.get_children(): for box in grid.get_children(): var num = box.get_position_in_parent() + 1 + 18 * grid.get_position_in_parent() box.level_num = num box.locked = false func _on_BackButton_pressed(): if current_grid > 1: current_grid -= 1 gridbox.rect_position.x += grid_width func _on_NextButton_pressed(): if current_grid < num_grids: current_grid += 1 gridbox.rect_position.x -= grid_width ننقر على زر التالي Next والسابق Back عند تشغيل المشهد ونتأكد من تمرير العناصر كما هو متوقع. من المفترض أن يطبع النقر على صندوق المستوى شيئًا في شاشة الطرفية. الخاتمة بهذا نكون قد انتهينا من مقالنا الذي يشرح طريقة بناء شبكة قابلة للتمرير يمينًا ويسارًا تضم صناديق لتحديد مرحلة أو مستوى اللعبة، ويمكن تحميل المثال بالكامل لرؤية كل شيء يعمل كما يجب بما في ذلك اﻹجراءات وبعض عمليات توليد اﻷطر البينية tweens لتجميل عملية التمرير. ترجمة -وبتصرف- للمقال: Level Select Menu اقرأ أيضًا المقال السابق: بناء شريط صحة ونصوص طافية في ألعاب جودو الاستماع لمدخلات اللاعب في جودو Godot مدخل إلى محرك الألعاب جودو Godot تعرف على أشهر لغات برمجة الألعاب أشهر محركات الألعاب Game Engines
-
نشرح في هذا المقال طريقة تدوير جسم صلب بسلاسة في محرك الألعاب جودو من خلال استخدام العقدة RigidBody2D، ونوضح طريقة توجيه هذا الجسم نحو هدف معين ليطبق عليه، أو يتحرك نحوه. الإطباق على هدف قد يكون استخدام العقدة RigidBody2D مربكًا لأن من يتحكم بها هو محرك الفيزياء الموجود ضمن محرك الألعاب جودو Godot physics engine. فإن أردنا تحريك الجسم ضمن اللعبة التي نطورها بجودو، فعلينا تطبيق قوى معينة عليه بدلًا من تحريكه مباشرة. وننصح قبل بدء العمل على تطبيق خطوات هذا المقال بإلقاء نظرة على توثيق الواجهة البرمجية RigidBody2D. لا بد من تطبيق عزم دوراني torque حتى نتمكن من تدوير الجسم، وبمجرد أن يبدأ الجسم بالدوران، علينا تقليل عزم الدوران تدريجيًا مع اقتراب hg[sl من الوجهة النهائية بحيث لا يفرط الجسم في الدوران ويتوقف في الاتجاه الصحيح بدقة. تُعدُّ هذه حالة مثالية لتطبيق الجداء النقطي المعروف أيضًا باسم الداخلي dot product فهو مناسب جدًا لتحديد الاتجاه المطلوب للوصول للوجهة النهائية. عند حساب الجداء النقطي بين اتجاه الجسم الحالي واتجاه الوجهة، سنحصل على معلومات تساعدنا على ضبط الدوران حيث أن إشارة ناتج الجداء تخبرنا إن كان الهدف موجودًا على جهة اليمين أو اليسار على النحو التالي: ناتج الجداء النقطي موجب هذا يعني أن الزاوية بين اتجاه الجسم الحالي واتجاه الوجهة أقل من 90 درجة، أي أن الجسم يحتاج إلى تعديل بسيط، أو لا يحتاج تعديل للوصول للوجهة لأنه قريب من الاتجاه الصحيح بالفعل. ناتج الجداء النقطي صفر هذا يعني أن الزاوية بين الاتجاهين هي 90 درجة بالضبط، أي أن الوجهة تقع عموديًا على الاتجاه الحالي للجسم، وهنا يجب على الجسم تحديد ما إذا كان عليه الدوران إلى اليمين أو اليسار للوصول للهدف. ناتج الجداء النقطي سالب هذا يشير إلى أن الزاوية بين اتجاه الجسم واتجاه الوجهة أكبر من 90 درجة، مما يعني أن الجسم يحتاج إلى التدوير الكامل نحو الاتجاه المعاكس. وبهذا، تكون الوجهة خلف الجسم. كما تدلنا قيمة ناتج الجداء النقطي عن مقدار ابتعادنا عن اتجاه الهدف الذي نريد مطابقته فكلما كانت قيمة الجداء أكبر، كلما كان الجسم أقرب للوجهة وذلك بالنسبة للمتجهات موحدة الطول. لنلقِ نظرة على الكود التالي المكتوب بلغة GDScript لتدوير جسم ثنائي الأبعاد 2D باتجاه هدف معين بتطبيق عزم دوراني torque: extends RigidBody2D var angular_force = 50000 var target = position + Vector2.RIGHT func _physics_process(delta): var dir = transform.y.dot(position.direction_to(target)) constant_torque = dir * angular_force قد يتساءل البعض عن سبب استخدام transform.y في حساب الجداء النقطي بدل استخدام transform.x ، مع أن transform.x هو من يمثل شعاع توجيه الجسم نحو الأمام! السبب هو أن استخدام transform.x سيجعل قيمة الجداء السلمي في أعلى قيمة له -قريبة من الواحد- عند التطابق مع الهدف ونحن نريد أن يكون العزم معدومًا -أس مساويًا للصفر- في هذه اللحظة لأن الجسم لن يحتاج إلى دوران إضافي، لهذا استخدمنا transform.y حيث يكون العزم أكبر عندما لا يكون اتجاه الجسم مطابقًا لاتجاه الهدف، مما يساعد على تصحيح الدوران. بمعنى آخر عندما يكون الجداء النقطي في أعلى قيمة له، فهذا يعني أن الجسم متوجه نحو الهدف تمامًا وفي هذه الحالة، نرغب في أن يكون العزم المطبق صفر، لأن الجسم لا يحتاج إلى مزيد من الدوران لذا حققنا هذا الهدف باستخدام transform.y بدلاً من transform.x ليزيد العزم عندما يكون الجسم بعيدًا عن الهدف، ويقل تدريجيًا مع الاقتراب منه. تفادي مشكلات تدوير الجسم الصلب RigidBody2D يمكننا تفادي تعقيدات تحريك الجسم الصلب بالامتناع عن تدوير الجسم الصلب نفسه RigidBody2D، ونعمل بدلًا من ذلك على تعديل الخاصية rotation لشخصية الابن child sprite كي يتوجه نحو الهدف. وبالإمكان حينها استخدام التابعين ()lerp و ()Tween لجعل الحركة الدورانية سلسلة قدر المستطاع. ويعد هذا الحل مناسبًا في كثير من الحالات، حيث يمكن أن يكون للجسم الأساسي اتجاهه الخاص بينما تتجه الشخصية أو العنصر المرتبط به في اتجاه مختلف نحو الهدف. إذ يمكن للجسم الأساسي أن يتحرك في اتجاه معين، بينما يمكن للشخصية الملحقة به مثل الرأس أو السلاح أن تدور بشكل مستقل دون الحاجة لأن يكون توجيهها مطابقًا تمامًا لتوجيه الجسم الأساسي. تحرك جسم نحو الهدف قد نواجه مشكلة عند محاولة تحريك RigidBody2D نحو هدف معين في محرك جودو، لأن محرك الفيزياء هو الذي يتحكم في هذه العقدة، ولا يمكننا ببساطة تغيير موقعها يدويًا. بدلاً من ذلك، يجب علينا تطبيق قوة لتحريكها في الاتجاه المطلوب، كما ذكرنا سابقًا. فكيف نجعل الجسم يتحرك بسلاسة نحو الهدف؟ نحتاج لتطبيق قوة في اتجاه الهدف لتحريك الجسم، ثم تخفيف القوة تدريجيًا مع الاقتراب من الهدف حتى لا يتجاوز الجسم موقعه أو يتوقف بشكل مفاجئ. يفيدنا استخدام التابع ()Vector2.distance_to لحل هذه المشكلة بشكل ممتاز، فهو يحسب المسافة بين الجسم والهدف، ويساعدنا على استخدام هذه المسافة لتحديد مقدار القوة المطلوب تطبيقها فعندما يكون الجسم بعيدًا، نطبق قوة أكبر، وكلما اقترب من الهدف، نخفض القوة تدريجيًا لضمان توقف سلس، وبهذه الطريقة، نحقق حركة طبيعية دون اهتزازات أو توقف مفاجئ. لنلقِ نظرة على الكود التالي الذي يجعل الجسم الصلب RigidBody2D يتحرك نحو هدف معين بطريقة سلسة: # تحريك الجسم بسلاسة نحو الهدف extends RigidBody2D var linear_force = 5 var target = position func _physics_process(delta): var dist = position.distance_to(target) constant_force = dir * linear_force * dist يحسب الكود أعلاه القوة الخطية المطلوبة لتحريك الجسم نحو الهدف بناء على المسافة بين الجسم المتحرك والهدف dist. تزداد القوة عندما يكون الجسم بعيدًا عن الهدف وتقل تدريجيًا مع الاقتراب من الهدف، مما يجعل الحركة تبدو طبيعية وسلسة. أهمية الخاصية linear_damp إن حاولنا استخدام إعدادات العقدة RigidBody2D الافتراضية في جودو، فقد نلاحظ أحيانًا تجاوز الجسم الصلب للهدف. يعود السبب إلى الخاصية linear_damp التي تأخذ القيمة 1 افتراضيًا. تمثل هذه القيمة معامل الاحتكاك friction، وتتحكم بكيفية تخميد أو توقف الجسم الصلب عن الحركة عند توقف القوى المحرّكة. فهي تعمل مثل الاحتكاك، مما يؤدي إلى تقليل سرعة الجسم تدريجيًا عندما لا تكون هناك قوة تدفعه. عندما تكون قيمة هذه الخاصية 0، فهذا يعني أن الجسم لن يتباطأ تلقائيًا، بل سيستمر في التحرك بسرعة ثابتة ما لم تؤثر عليه قوة أخرى، مثل الجاذبية أو الاحتكاك. أما عندما تكون قيمتها 1 أو 2، فإن تأثير التخميد يزداد، مما يؤدي إلى تقليل سرعة الجسم تدريجيًا حتى يتوقف عندما لا يكون هناك قوة تحركه. بإمكاننا تعديل هذه القيمة لنضمن توقف الجسم عند بلوغ الهدف، ويمكن أن نجرب أيضًا كيف تتفاعل هذه القيمة مع قيمة الخاصية linear_force حتى نحصل على الحركة التي نريدها تمامًا، حيث تمثل linear_force قوة خطية عندما تُطبّق على الجسم تحرِّكه في اتجاه معين للأمام أو الخلف أو في أي اتجاه يشير إليه القوة مما يؤدي إلى تسارع الجسم. ويمكننا تغيير قيمة linear_force لتسريع أو إبطاء حركة الجسم أثناء تشغيل اللعبة. الخاتمة تعلمنا في مقال اليوم كيفية تحريك وتدوير جسم صلب RigidBody2D في جودو بشكل سلس نحو هدف معين باستخدام القوى والعزم الدوراني، كما شرحنا كيفية التحكم في التباطؤ باستخدام الخاصية linear_damp لضمان حركة طبيعية للجسم. ترجمة -وبتصرف- للمقالين: RigidBody2D: Look at Target و RigidBody2D: Move to Target اقرأ أيضًا تعرف على واجهة محرك الألعاب جودو استخدام الإشارات Signals في جودو Godot تعرف على أشهر محركات الألعاب Game Engines التعامل مع إجراءات دخل الفأرة في جودو
-
نتعرف في هذا المقال على طريقة بناء شريط صحة Health Bar يضم قلوبًا أو غيرها من اﻷيقونات، كما سنتعرف على طريقة عرض نسبة تضرر الشخصية في لعبة على شكل نص يطفو فوق الشخصية. ثلاث طرق لبناء شريط صحة يضم قلوبًا من الطرق الشائعة في إظهار صحة اللاعب عرض سلسلة من اﻷيقونات -غالبًا بشكل قلوب- يختفي بعضها عندما يتعرض اللاعب إلى ضرر. وسنناقش ثلاث طرق لعرض الأيقونات أطلقنا عليها تسميات بسيطة simple وفارغة empty وجزئية partial. تعرض الصورة السابقة ثلاث حالات ممكنة لعرض شريط الصحة: الطريقة البسيطة: تعرض القلوب ممتلئة بالكامل الطريقة الفارغة: تعرض قلوب فارغة وأخرى ممتلئة الطريقة الجزئية: تعرض القلوب نصف ممتلئة إعداد شريط اﻷيقونات نستخدم في هذا المثال صور قلوب أبعادها 53x45 حصلنا عليها من موقع Kenney.nl: Platformer Art Deluxe. ومن المفترض أن يكون وضع الشريط ضمن شاشة عرض معلومات المستخدم HUD أو واجهة المستخدم UI سهلًا، لذا من المنطقي أن نبني هذا الشريط ضمن مشهد مستقل. سنبدأ بعقدة من النوع Node2D كي نُبقى اﻷمور على نفس السوية، ونضبط قيمة الخاصية Sepration ضمن Constants في القسم Theme Overrides من الفاحص على القيمة 5. نضيف بعد ذلك عقدة ابن من النوع TextureRect ثم نسحب أيقونة القلب إلى الخاصية Texture ونضبط قيمة Strech Mode على Keep. نعيد تسمية العقدة لتكون 1 ثم باستخدام مفتاحي Ctrl+D ننسخ هذه العقدة بعدد القلوب التي نريد عرضها في الشريط 5 مثلًا. ستبدو لوحة العقد في محرك جودو كالتالي: إضافة السكريبت يغطي السكريبت التالي حالات الشريط الثلاث التي ذكرناها، حيث سنحمّل في البداية الخامات وهي هنا اﻷيقونات التي نحتاجها ونعرف اﻷشرطة الثلاث، وتجدر الملاحظة بأن الكود سيغطي جميع حالات الشريط الثلاثة، وقد نحتاج لاستخدام حالة واحدة فقط في اللعبة، عندها نزيل الكود المتعلق بالحالات الأخرى كما يلي: extends HBoxContainer enum modes {SIMPLE, EMPTY, PARTIAL} var heart_full = preload("res://assets/hud_heartFull.png") var heart_empty = preload("res://assets/hud_heartEmpty.png") var heart_half = preload("res://assets/hud_heartHalf.png") @export var mode : modes func update_health(value): match mode: MODES.simple: update_simple(value) MODES.empty: update_empty(value) MODES.partial: update_partial(value) يؤدي استدعاء الدالة ()update_health العائدة إلى الشريط عرض القيمة الممرة إليه وفقًا للنمط المختار. ملاحظة: لن نضيف آليات تحقق من حدود القيمة المدخلة كالتأكد مثلًا من أن الصحة بين 0 و 100، فهناك طرق كثيرة لعرض الصحة في اﻷلعاب لذا سنترك الأمر لكم. نتنقل في الدالة ()update_simple بين أشرطة الأيقونات ونضبط ظهور كل عقدة TextureRect: func update_simple(value): for i in get_child_count(): get_child(i).visible = value > i واﻷمر مشابه في الدالة ()update_empty ما عدا أننا نغير اﻷيقونة إلى اﻷيقونة الفارغة بدلًا من إخفائها: func update_empty(value): for i in get_child_count(): if value > i: get_child(i).texture = heart_full else: get_child(i).texture = heart_empty أما في الحالة اﻷخيرة، فلدينا أيقونة ثالثة وضعف القيم الممكنة فمن خلال إنقاص القيمة بمقدار 1 مثلًا يعطي نصف قلب وإنقاص 1 مرة أخرى تعطي قلبًا فارغًا: func update_partial(value): for i in get_child_count(): if value > i * 2 + 1: get_child(i).texture = heart_full elif value > i * 2: get_child(i).texture = heart_half else: get_child(i).texture = heart_empty توضح الصورة أدناه مثالًا عن عمل كل شريط: إنشاء نصوص طافية فوق الشخصية هناك طرق عدة لتحقيق النصوص الطافية floating text، منها استخدام خط كتابة نقطية bitmap font وبناء صورة لكل عدد انطلاقًا من اﻷرقام المكونة له، ومن ثم استخدام العقدة Sprite2D لعرض وتحريك النص الناتج. لكن ما سنفعله في مقالنا هو استخدام العقدة Label واسمها FCT وبهذا سنمتلك مرونة في تغيير الخط إضافة إلى سهولة عرض اﻷعداد كنصوص أو عرض نصوص أخرى مثل "أخفق miss". نضيف المورد الذي نريده في الخاصية Label Settings ونختار خطًا مناسبًا وقياسًا مناسبًا له، وقد استخدمنا في المثال الخط Xolonium.ttf والقياس 28 مع إطار خارجي أسود بعرض 4 بكسل. نضيف اﻵن السكريبت التالي إلى العقدة Label: extends Label func show_value(value, travel, duration, spread, crit=false): نستدعي عند توليد النصوص الطافية الدالة ()show_value التي تضبط قيم المعاملات التالية: value وهو العدد أو النص الذي نريد توليده travel وهو عقدة شعاع Vector2 التي تمثل اتجاه حركة النص أو العدد duration تحدد كم سيبقى النص على قيد الحياة spread يحدد أن الحركة ستكون عشوائية عبر هذا القوس crit يشير لأن الضرر كبير في حال كانت قيمته true وهذا ما تفعله الدالة ()show_value: text = value var movement = travel.rotated(rand_range(-spread/2, spread/2)) rect_pivot_offset = rect_size / 2 تضبط الدالة قيمة النص أو العدد ومن ثم تجعل حركته عشوائية وفقًا لقيمة الانتشار spread مابين 90+ و90- مثلًا. وقد نغير أبعاد النصوص المتحركة، لهذا ضبطنا قيمة الخاصية rect_pivot_offset لتمثل مركز عنصر التحكم وبالتالي يكون تغيير اﻷبعاد منسوبًا إلى المركز. $Tween.interpolate_property(self, "rect_position", rect_position, rect_position + movement, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) $Tween.interpolate_property(self, "modulate:a", 1.0, 0.0, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) نجري بعد ذلك استيفاء interpolation على قيمتي الخاصية بين لحظتين هما rect_position لتحريك العدد الطافي و modulate.a ﻹخفاء هذا النص بشكل تدريجي وسلس: if crit: modulate = Color(1, 0, 0) $Tween.interpolate_property(self, "rect_scale", rect_scale*2, rect_scale, 0.4, Tween.TRANS_BACK, Tween.EASE_IN) إن كانت اﻹصابة بالغة، سنغير لون النص ونزيد حجمه لإظهار التأثير. وتجدر الملاحظة بأننا حددنا لون النص في هذه الحالة ليكون أحمر ومن اﻷفضل أن تعرف متحولًا لإسناد قيمة اللون الذي نريده: $Tween.start() yield($Tween, "tween_all_completed") queue_free() نبدأ بعد ذلك عملية بناء اﻹطارات البينية من خلال التعليمة Tween وننتظر حتى تنتهي ثم نزيل العنوان Label. إدراة النصوص الطافية FCTManager ننشئ اﻵن عقدة صغيرة تدير توليد النصوص وتحديد مكانها، وستُلحق بكيانات اللعبة التي نريد أن نضيف إليها تأثير النصوص الطافية. نسمي هذه العقدة من النوع Node2D بالاسم FCTManager ونضيف إليها السكريبت التالي: extends Node2D var FCT = preload("res://FCT.tscn") export var travel = Vector2(0, -80) export var duration = 2 export var spread = PI/2 func show_value(value, crit=false): var fct = FCT.instance() add_child(fct) fct.show_value(str(value), travel, duration, spread, crit) يمكن تعديل ما نريده من خصائص العقدة من خلال نافذة الفاحص Inspector، لكن الدالة ()show_value أيضًا تولد النص الطافي وتضبط خصائصه. وبإمكاننا إلحاق نسخة من هذه العقدة بأي وحدة من وحدات اللعبة نريدها أن تمتلك تأثير النصوص الطافية، ثم نضيف كود مشابه لما يلي ضمن التابع ()take_damage للوحدة: $FCTManager.show_value(dmg, crit) تجدر الإشارة لأنه في الحالة التي تضم فيها لعبتنا عددًا كبيرًا من الوحدات، فقد يؤثر هذا اﻷمر على اﻷداء جراء توليد وتحرير النصوص الطافية باستمرار لعدد كبير من الوحدات. في حالات كهذه، ينصح بتوليد عدد محدد تمامًا من النصوص الطافية من خلال FCTManager ثم نظهرها ونخفيها بدلًا من توليدها وتحريرها في نهاية الحركة. الخاتمة تعرفنا في هذا المقال على طريقة بناء شريط صحة يضم أيقونات تعرض حالة اللاعب مثل تناقص صحته أو نفاذ ذخيرته، كما تحدثنا عن أحد طرق لتوليد نصوص تطفو حول الشخصية وتختفي للدلالة على حالة معينة مثل مقدار اﻹصابة التي تلقتها. ترجمة -وبتصرف- للمقالين: HeartContainers: 3 ways و Floating Combat Text اقرأ أيضًا المقال السابق: عرض عداد تنازلي Countdown وقائمة دائرية Radial Menu في جودو استخدام الإشارات Signals في جودو Godot الاستماع لمدخلات اللاعب في جودو Godot تعرف على واجهة محرك الألعاب جودو