-
المساهمات
51 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Naser Dakhel
-
حان وقت البرمجة في سلسلتنا عن تطوير الألعاب ثلاثية الأبعاد باستخدام محرك الألعاب جودو، إذ سنستخدم إجراءات الإدخال التي أنشأناها في الدرس الماضي لتحريك الشخصية في هذا الدرس باستخدام الشيفرة البرمجية، إذ ستساعدنا الشيفرة بضبط تسارع حركة اللاعب وسرعته القصوى بالإضافة لسرعة الجاذبية التي تحدد مقدار تأثير الجاذبية الافتراضية على حركة الشخصية في اللعبة وغيرها من المفاهيم المفيدة. انقر بزر الفأرة الأيمن على عقدة Player وحدد خيار "إضافة سكريبت Attach Script" لإضافة سكربت جديد إليها، ثم اضبط "القالب Template" في النافذة المنبثقة على Empty قبل الضغط على زر "إنشاء Create". سنعرّف خصائص الصنف class مثل سرعة الحركة movement speed وتسارع السقوط fall acceleration الذي يمثل الجاذبية، والسرعة التي سنستخدمها لتحريك الشخصية. إليك الشيفرة بلغة GDScript extends CharacterBody3D # سرعة اللاعب بواحدة المتر في الثانية @export var speed = 14 # التسارع نحو الأسفل في الهواء بوحدة متر في الثانية للتربيع @export var fall_acceleration = 75 var target_velocity = Vector3.ZERO هذه الخصائص مشتركة لجسم متحرك حيث أن target_velocity هو شعاع ثلاثي الأبعاد 3D vector يجمع بين السرعة والاتجاه. وقد عرفناه هنا كخاصية لأننا نريد تحديث قيمته وإعادة استخدامه عبر الإطارات. ملاحظة: القيم هنا مختلفة تمامًا عما اعتدت عليه في شيفرات الألعاب ثنائية الأبعاد، وذلك لأن المسافات تقاس هنا بالمتر، أي قد توافق ألف وحدة بالبكسل مع نصف عرض شاشتك فقط في الفضاء ثنائي الأبعاد أما في الفضاء ثلاثي الأبعاد فيكون ذلك مساويًا إلى كيلومتر واحد (بما أن كل وحدة مساوية إلى واحد متر) لذا ستحتاج إلى التكيف مع النظام الثلاثي الأبعاد حيث يمكن أن تمثل القيم أبعادًا أكبر وأكثر واقعية في المسافات والسرعات. لتحديد اتجاه الحركة نبدأ بحساب شعاع اتجاه الإدخال باستخدام الكائن العام input في _physics_process() لبرمجة الحركة كما في الشيفرة التالية: func _physics_process(delta): # أنشأنا متغير محلي لتخزين اتجاه الإدخال var direction = Vector3.ZERO # نتحقق من كل خطوة ونحدّث الاتجاه حسب ذلك if Input.is_action_pressed("move_right"): direction.x += 1 if Input.is_action_pressed("move_left"): direction.x -= 1 if Input.is_action_pressed("move_back"): # لاحظ أننا نعمل مع محاور أشعة x و z # إن المسطح xz في ثلاثي الأبعاد هو مستوي الأرض direction.z += 1 if Input.is_action_pressed("move_forward"): direction.z -= 1 سنجري هنا جميع الحسابات باستخدام الدالة الافتراضية _physics_process() وهي على غرار الدالة _process() التي تتيح لنا تحديث العقدة في كل إطار ولكنها مصممة خصيصًا للشيفرة المتعلقة بالفيزياء مثل تحريك جسم حركي kinematic يتفاعل مع محيطه من خلال الكود البرمجي، أو جسم صلب rigid يعتمد على محرك الفيزياء في جودو للتحرك والتفاعل بشكل واقعي مع البيئة المحيطة به بناءً على القوى المؤثرة عليه مثل الجاذبية أو التصادمات. ولتعلّم المزيد حول الفرق بين الدالتين _process() و _physics_process() راجع توثيق جودو حول معالجة الفيزياء والسكون ومعرفة كيفية استخدام هذه الدوال لضبط الحركات الفيزيائية بطريقة مستقرة وواقعية. نبدأ بتهيئة قيمة المتغير direction إلى Vector3.ZERO ليكون متجهًا فارغًا (أي أن قيمته تكون صفرًا في المحاور الثلاثة x و y و z) ثم نتحقق مما إذا كان اللاعب يضغط على إدخال move_* واحد أو أكثر ونغيّر مكوني x وz للشعاع وفقًا لذلك بحيث تتوافق هذه مع محاور مستوى الأرض. توفر لنا هذه الحالات الأربعة ثمانية احتمالات، وثمانية اتجاهات ممكنة. سيكون طول الشعاع في حالة ضغط اللاعب مثلًا على كل من W و D في وقت واحد حوالي 1.4، ولكن إذا ضغط على مفتاح واحد سيكون طوله 1. نستدعي تابع normalized() ليكون طول الشعاع ثابتًا ولا يتحرك بشكل أسرع قطريًا إليك الشيفرة بلغة GDScript لتحريك أو تدوير الكائن في اتجاه حركة اللاعب بناءً على المدخلات. #func _physics_process(delta): #... if direction != Vector3.ZERO: direction = direction.normalized() # تهيئة خاصية basis سيؤثر على دوارن العقدة. $Pivot.basis = Basis.looking_at(direction) لاحظ أننا نعالج الشعاع فقط إذا كان طوله أكبر من الصفر، مما يعني أن اللاعب يضغط على مفتاح اتجاه، مما يؤدي إلى تحديث direction،بحيث يعكس الاتجاه الذي يريد اللاعب التحرك فيه. الآن سنحسب الاتجاه الذي ينظر إليه $Pivot عن طريق إنشاء Basis ينظر في اتجاه direction، ومن ثم نحدّث قيمة السرعة حيث يتعين علينا حساب سرعة الأرض وسرعة السقوط (أو السرعة الأفقية والسرعة العمودية) بشكل منفصل، حيث تدمج السرعات معًا وتحرك الشخصية باستخدام الدالة move_and_slide() لتطبيق الحركة الفعلية مع الفيزياء. تأكد من الضغط على مفتاح Tab مرة واحدة بحيث تكون الأسطر داخل دالة _physics_process() ولكن خارج الشرط الذي كتبناه للتو أعلاه. func _physics_process(delta): #... if direction != Vector3.ZERO: #... # السرعة الأرضية target_velocity.x = direction.x * speed target_velocity.z = direction.z * speed # السرعة العمودية if not is_on_floor(): # إذا كان في الهواء، فسيسقط على الأرض target_velocity.y = target_velocity.y - (fall_acceleration * delta) # تحريك الشخصية velocity = target_velocity move_and_slide() تعيد دالة CharacterBody3D.is_on_floor() قيمة true إذا تصادم الجسم مع الأرض في هذا الإطار، لهذا السبب نطبق الجاذبية على Player فقط عندما يكون في الهواء. بالنسبة للسرعة العمودية، نطرح تسارع السقوط مضروبًا في وقت دلتا في كل إطار. سيؤدي هذا السطر من الشيفرة إلى سقوط شخصيتنا في كل إطار طالما أنها ليست على الأرض أو تصطدم بها. يمكن لمحرك الفيزياء اكتشاف التفاعلات فقط مع الجدران أو الأرض أو أجسام أخرى خلال إطار معين إذا حدثت الحركة والاصطدامات. وسنستخدم هذه الخاصية لاحقًا لبرمجة القفز. نستدعي في السطر الأخير التابع CharacterBody3D.move_and_slide() وهو تابع ذو قدرات عظيمة لصنف CharacterBody3D إذ يسمح لك بتحريك الشخصية بسلاسة، حيث سيحاول محرك جودو إصلاح الحركة إذا اصطدمت بحائط في منتصفها باستخدام قيمة السرعة الأصلية CharacterBody3D. هذه هي كل الشيفرة التي تحتاجها لتحريك الشخصية على الأرض. فيما يلي شيفرة Player.gd الكاملة لاستخدامها كمرجع: extends CharacterBody3D # سرعة اللاعب بواحدة المتر في الثانية @export var speed = 14 # التسارع نحو الأسفل في الهواء بوحدة متر في الثانية للتربيع. @export var fall_acceleration = 75 var target_velocity = Vector3.ZERO func _physics_process(delta): var direction = Vector3.ZERO if Input.is_action_pressed("move_right"): direction.x += 1 if Input.is_action_pressed("move_left"): direction.x -= 1 if Input.is_action_pressed("move_back"): direction.z += 1 if Input.is_action_pressed("move_forward"): direction.z -= 1 if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.basis = Basis.looking_at(direction) # السرعة الأرضية target_velocity.x = direction.x * speed target_velocity.z = direction.z * speed # السرعة العمودية if not is_on_floor(): # إذا كان في الهواء، فيسقط على الأرض target_velocity.y = target_velocity.y - (fall_acceleration * delta) # تحريك الشخصية velocity = target_velocity move_and_slide() دورة تطوير الألعاب ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة ومليئة بالتحديات اشترك الآن اختبار حركة اللاعب نحتاج إلى استنساخ اللاعب ثم إضافة كاميرا من أجل وضع اللاعب في المشهد Main واختباره. لن ترى أي شيء في الفضاء ثلاثي الأبعاد إذا لم تحوي نافذة العرض كاميرا موجهة نحو شيء ما، على عكس الفضاء ثنائي الأبعاد. احفظ المشهد Player وافتح المشهد Main من خلال النقر على "Main" أعلى المحرر. إذا أغلقت المشهد من قبل، فتوجه إلى نافذة نظام الملفات FileSystem Dock وانقر نقرًا مزدوجًا فوق main.tscn لإعادة فتحه. انقر بزر الفأرة الأيمن على العقدة الرئيسية Main وحدد "تنسيخ المشهد الفرعي Instantiate Child Scene" لاستنساخ المشهد Player . الآن انقر نقرًا مزدوجًا فوق player.tscn في النافذة المنبثقة لتظهر الشخصية في وسط نافذة العرض. إضافة كاميرا سننشئ إعدادًا أساسيًا تمامًا كما فعلنا مع `Pivot. انقر بزر الفأرة الأيمن على عقدة المشهد الرئيسيMainمرة أخرى وحدد خيار "إضافة عقدة فرعية Add Child Node"، ثم أنشئ عقدةMarker3Dجديدة وسمهاCameraPivot، ومن ثم حددCameraPivotوأضف إليها عقدة فرعيةCamera3D` لتبدو شجرة المشهد على هذا النحو ستلاحظ مربع اختيار معاينة Preview يظهر في الزاوية العلوية اليسرى عند تحديد الكاميرا حيث يمكنك النقر فوقه لمعاينة إسقاط الكاميرا داخل اللعبة. سنستخدم المحور Pivot لتدوير الكاميرا كما لو كانت على رافعة، لذا دعنا أولاً نقسم نافذة العرض ثلاثي الأبعاد 3D view لنتمكن من التنقل بحرية في المشهد ورؤية ما تراه الكاميرا. في شريط الأدوات أعلى النافذة مباشرةً، انقر فوق View ثم 2 Viewports. يمكنك أيضًا الضغط على Ctrl + 2 (أو Cmd + 2على نظام التشغيل macOS). حدد Camera3D في النافذة السفلية، وشغّل معاينة الكاميرا بالنقر فوق مربع الاختيار Preview. حرك الكاميرا في النافذة العلوية حوالي 19 وحدة على المحور Z ذي اللون الأزرق. هنا نرى ثمرة عملنا، حدد CameraPivot ودوره بمقدار -45 درجة حول محور X باستخدام الدائرة الحمراء لترى الكاميرا تتحرك كما لو كانت متصلة برافعة. يمكنك تشغيل المشهد بالضغط على F6 ثم الضغط على مفاتيح الأسهم لتحريك الشخصية. يمكننا رؤية مساحة فارغة حول الشخصية بسبب الإسقاط المنظوري perspective projection، لذلك سنستخدم بدلاً منه في هذه اللعبة إسقاطًا متعامدًا orthographic projection لتأطير منطقة اللعب بشكل أفضل وتسهيل قراءة المسافات على اللاعب. حدد الكاميرا مرة أخرى ومن قائمة الفاحص Inspector اضبط الإسقاط على القيمة "متعامد Orthogonal" والحجم على 19. يجب أن تبدو الشخصية مسطحة أكثر ومنسجمة مع الأرضية. ملاحظة: تعتمد جودة الظل الاتجاهي directional shadow على قيمة Far للكاميرا عند استخدام كاميرا متعامدة فإعداد Far يحدد المسافة الأبعد التي يمكن للكاميرا رؤيته، وكلما زادت قيمة البعد زادت المسافة التي ستتمكن الكاميرا من الرؤية فيها. بالرغم من ذلك، فإن قيم البعد الأعلى ستقلل أيضًا من جودة الظل حيث يجب أن يغطي عرض الظل مسافة أكبر. قلل خاصية البعد للكاميرا إلى قيمة أقل مثل 100 إذا بدت الظلال الاتجاهية ضبابية جدًا بعد التبديل إلى كاميرا متعامدة فلا تقلل من خاصية البعد هذه كثيرًا وإلا ستبدأ الكائنات في البعد بالاختفاء. اختبر مشهدك ويجب أن تكون قادرًا على التحرك في جميع الاتجاهات الثمانية (الأمام، الخلف، اليمين، اليسار، بالإضافة إلى الزوايا القطرية) دون أن تخترق الأرضية، إذا تحقق هذا فذلك يشير لأن الأمور تعمل بشكل صحيح وأن الشخصية تتحرك بالشكل المطلوب. الخلاصة شرحنا في هذا المقال طريقة ضبط حركة اللاعب باستخدام الشيفرة البرمجية وتحديد سرعته، بالإضافة لإعداد الكاميرا وكيفية عرض المشهد في لعبتنا ثلاثية الأبعاد. سننتقل في الدرس التالي إلى كيفية برمجة الجزء الثاني من اللعبة ألا وهو الأعداء! ترجمة - وبتصرف - لقسم Moving the player with code من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: أنشئ لعبة ثلاثية الأبعاد باستخدام محرك جودو: مشهد اللاعب وعمليات الإدخال العقد Nodes والمشاهد Scenes في جودو Godot تعرف على أشهر محركات الألعاب Game Engines تعرف على أشهر لغات برمجة الألعاب
-
سنكتمل في مقال اليوم إنشاء لعبتنا ثلاثية الأبعاد باستخدام محرك جودو التي بدأنا العمل عليها في المقال السابق وأعددنا فيها منطقة اللعب، وسنصمم في هذا الدرس مشهد اللاعب ونحقق عمليات إدخال مخصصة ونبرمج حركة اللاعب، وفي النهاية سيكون لديك شخصية لعبة تتحرك في ثمانية اتجاهات. إنشاء مشهد اللاعب أنشئ مشهدًا جديدًا بالانتقال إلى قائمة "مشهد Scene" في أعلى اليسار وانقر فوق "مشهد جديد New Scene" أنشئ عقدة CharacterBody3D كعقدة جذر حيث تستخدم هذه العقدة للتحكم في شخصيات الألعاب ثلاثية الأبعاد، مثل اللاعبين أو الأعداء. وتوفر أدوات للتحكم في الحركة والتصادم. غيّر اسم العقدة CharacterBody3D إلى Player لتحديد أنها تمثل شخصية اللاعب في اللعبة. تشبه هذه العقدة كل من المناطق area والأجسام الصلبة rigid bodies التي استخدمناها في برمجة لعبة ثنائية الأبعاد، إذ يمكنها التحرك والاصطدام بالبيئة مثل الأجسام الصلبة، لكن بدلاً من التحكم بها بواسطة محرك الفيزياء الذي يحدد سلوك الحركة، مثل الجاذبية أو الارتداد، بشكل تلقائ.، فإنك هنا تحدد حركتها بنفسك. وسترى كيف نستخدم ميزات العقدة الفريدة عند برمجة قفزة اللاعب وآلية القتال. وللتعرف على أنواع العقد المختلفة، راجع مقدمة إلى الفيزياء في توثيق جودو. في هذه الخطوة، سنقوم بإنشاء هيكل أساسي لنموذج الشخصية ثلاثية الأبعاد عن طريق إضافة عقدة تدور حول محور، وهذا سيسمح لنا بتدوير النموذج لاحقًا عبر التعليمات البرمجية أثناء تنفيذ الرسوم المتحركة للشخصية. أضف عقدة Node3D كعقدة فرعية للعقدة Player وسمّها Pivot لتكون عقدة وسيطة يمكننا التحكم من خلالها في زاوية دوران النموذج. بعد ذلك، في لوحة "نظام الملفات FileSystem"، افتح مجلد الرسومات art/ بالنقر المزدوج عليه، ثم اسحب وأفلت الملف player.glb على العقدة Pivot. يجب أن يؤدي هذا إلى إنشاء النموذج كعقدة فرعية للعقدة Pivot، كما يمكنك إعادة تسميتها إلى Character. ملاحظة: تحتوي ملفات .glb على بيانات مشهد ثلاثي الأبعاد بناءً على مواصفات GLTF 2.0 مفتوحة المصدر، وهي بديل حديث وقوي لنوع الملفات الاحتكارية FBX، والذي يدعمه جودو أيضًا. وقد صممنا النموذج في Blender 3D لإنتاج هذه الملفات، ثم صدّرناه إلى GLTF. كما هو الحال مع جميع أنواع العقد، نحتاج إلى شكل تصادم لشخصيتنا كي تتصادم به مع البيئة. حدد عقدة Player مرة أخرى وأضف عقدة فرعية CollisionShape3D في قائمة "الفاحص Inspector" ومن خاصية الشكل "Shape" أضف شكل SphereShape3D جديد. سيظهر الإطار السلكي للكرة أسفل الشخصية كما في الصورة التالية. سيكون هذا هو الشكل الذي يستخدمه المحرك الفيزيائي للاصطدام بالبيئة، لذلك نريد أن يتناسب بشكل أفضل مع النموذج ثلاثي الأبعاد. صغّره قليلاً عن طريق سحب النقطة البرتقالية في نافذة العرض viewport، حيث يبلغ نصف قطر الكرة حوالي 0.8 متر، ثم حرك الشكل لأعلى بحيث يتماشى قعره تقريبًا مع مستوى الشبكة. يمكنك إخفاء وإظهار النموذج عن طريق النقر فوق أيقونة العين بجوار عقدة Character أو عقدة Pivot. احفظ المشهد باسم player.tscn نحتاج الآن إلى تعريف بعض إجراءات الإدخال قبل البدء بالبرمجة عندما تصبح العقد جاهزة. إنشاء إجراءات الإدخال سننتظر دخل اللاعب باستمرار لتحريك الشخصية مثل الضغط على مفاتيح الأسهم، وعلى الرغم من أنه يمكننا كتابة جميع إجراءات المفاتيح باستخدام الشيفرة البرمجية في جودو، إلا أن هناك ميزة قوية تسمح لك بتعريف تسمية معيّنة وربطها مع مجموعة من المفاتيح والأزرار، بحيث تستطيع استخدام هذه التسمية فيما بعد في شيفرتك البرمجية بدلًا من استخدام المفاتيح والأزرار بشكل منفصل، ونستفيد بذلك من تبسيط شيفراتنا البرمجية ويجعلها أكثر قابلية للقراءة. هذا النظام هو "خريطة الإدخال Input Map"، وللوصول إلى المحرر الخاص بها، توجه إلى قائمة "المشروع Project" وحدد "إعدادات المشروع Project Settings". توجد علامات تبويب متعددة في الأعلى، انقر فوق "خريطة الإدخال Input Map"، حيث تتيح لك هذه النافذة إضافة تسمياتك في الأعلى، بينما يمكنك في الجزء السفلي ربط المفاتيح بهذه الإجراءات. تأتي مشاريع جودو مع بعض الإجراءات المحددة مسبقًا predefined actions والموجهة لتصميم واجهة المستخدم التي يمكننا استخدامها هنا، لكننا سنحدد إجراءاتنا الخاصة لدعم وحدات التحكم gamepads. سنسمي إجراءاتنا move_left للتحرك يسارًا، و move_right للتحرك يمينًا و move_forward للتحرك للأمام، و move_back للتحرك للخلف، و jump للقفز. اكتب اسم الإجراء في الشريط العلوي واضغط على Enter لإضافته. أنشئ الإجراءات الخمسة التالية: لربط مفتاح أو زر بإجراء، انقر فوق زر إشارة "+" على يمينه. افعل ذلك من أجل move_left للتحرك يسارًا، ثم اضغط على مفتاح السهم الأيسر وانقر فوق موافق OK. اربط أيضًا مفتاح A على إجراء move_left لنضيف الآن دعمًا لعصا التحكم اليسرى في وحدة التحكم Gamepad من خلال النقر فوق الزر "+" مرة أخرى ولكن هذه المرة سنحدد خيار Manual Selection ومن ثم محاور عصا التحكم Joypad Axes حدد المحور السالب X لعصا التحكم اليسرى. اترك القيم الأخرى كقيمة افتراضية واضغط على موافق OK. ملاحظة: إذا كنت تريد أن تحتوي وحدات التحكم على إجراءات إدخال مختلفة، فيجب عليك استخدام خيار أجهزة Devices في قائمة الخيارات الإضافية Additional Options. يقابل الجهاز 0 أول وحدة تحكم موصولة، ويقابل الجهاز 1 ثاني وحدة تحكم ...إلخ. افعل الشيء نفسه لإجراءات الإدخال الأخرى. على سبيل المثال، اربط السهم الأيمن و المفتاح D، والمحور الموجب لعصا التحكم اليسرى بالإجراء move_right، وبعد ربط جميع المفاتيح، يجب أن تبدو واجهتك على هذا النحو. آخر إجراء مطلوب إعداده هو إجراء القفز jump، اربط مفتاح المسافة Space وزر A في وحدة التحكم Gamepad من أجل تحقيقه في لعبتك. يجب أن يبدو إجراء إدخال القفز الخاص بك على هذا النحو. هذه كل الإجراءات التي نحتاجها لهذه اللعبة، ويمكنك استخدام هذه القائمة لوضع تسميات على أي مجموعات من المفاتيح والأزرار في مشاريعك. الخلاصة وصلنا لنهاية مقالنا الذي تعلمنا فيه كيفية ضبط مشهد اللاعب بالإضافة لإعداد عمليات الإدخال وتعديلها بحيث نستطيع التحكم بالشخصية بشكل أسهل عوضًا عن كتابة شيفرة مخصصة لهذا الغرض. سنبرمج في الدرس التالي حركة اللاعب باستخدام الشيفرة البرمجية لضبط سرعة الحركة والتسارع، ومن ثم نختبرها لنتأكد من أن كل شيء يعمل على ما يرام. ترجمة -وبتصرف- لقسم Player scene and input actions من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: أنشئ لعبة ثلاثية الأبعاد باستخدام محرك جودو - إعداد منطقة اللعب الاستماع لمدخلات اللاعب في جودو Godot ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها كيف تحصل على أفكار ألعاب فيديو ناجحة
-
ستُنشئ في هذا المقال والمقالات اللاحقة لعبة كاملة ثلاثية الأبعاد باستخدام محرك الألعاب جودو Godot، وسيكون لديك في نهاية السلسلة مشروع بسيط ومتكامل من تصميمك الخاص، مثل الصورة المتحركة أدناه. ستكون اللعبة التي سنبرمجها هنا مشابهة للعبة ثنائية الأبعاد التي شرحناها في مقالاتنا السابقة، ولكن مع لمسة إضافية وهي القفز بهدف القضاء على الأعداء، فتكون بهذه الطريقة قد تعرفت على أنماط تعلمتها في الدروس السابقة واستفدت منها في بناء شيفرات وميزات جديدة. إليك ما ستتعلمه خلال تطوير لعبة ثلاثية الأبعاد في جودو: العمل مع إحداثيات ثلاثية الأبعاد في حركة القفز. استخدام الأجسام الحركية kinematic bodies لتحريك شخصيات ثلاثية الأبعاد واكتشاف كيف ومتى يحدث التصادم. استخدام طبقات الفيزياء physics layers والتصنيف في مجموعات للكشف عن تفاعلات كيانات محددة. كتابة التعليمات البرمجية الأساسية للعبة عن طريق إنشاء الأعداء على فترات زمنية منتظمة. تصميم الحركة وتغيير سرعتها في وقت التنفيذ. رسم واجهة مستخدم في لعبة ثلاثية الأبعاد، وغيره الكثير. سنبدأ مع تعليمات مشروحة بالتفصيل ونختصرها كلما مررنا بخطوات مشابهة، وإذا كنت من المبرمجين المتمرسين يمكنك النظر إلى الشيفرة البرمجية النهائية على الرابط التالي: Squash the Creep source code لقد أعددنا بعض موارد assets اللعبة حتى نتمكن من البدء مباشرة بكتابة الشيفرة البرمجية. ويمكنك تنزيلها من هنا: Squash the Creeps assets حيث سنعمل أولاً على نموذج أولي أساسي لحركة اللاعب، ثم سنضيف الوحوش التي ستوزع عشوائيًا حول الشاشة، بعد ذلك، سننفذ حركة القفز وآلية القتال قبل تحسين اللعبة ببعض الرسوم المتحركة، وسنختم بالنتيجة مع عبارة إعادة المحاولة. البدء بإعداد منطقة اللعب سنتعلم في البداية كيفية إعداد منطقة اللعب، إذ ستكون أحداث اللعبة تقع بكاملها في هذه المنطقة. لنبدأ بإعداد منطقة اللعب عن طريق استيراد موارد البدء start assets وإعداد مشهد اللعبة. حضّرنا مشروع جودو مع النماذج ثلاثية الأبعاد والأصوات التي سنستخدمها في هذه السلسلة، إذا لم تحمّل الملف المضغوط حتى الآن، يمكنك تنزيله من هنا، بعد ذلك، استخرج ملف .zip على حاسوبك، وافتح مدير مشروع جودو وانقر على زر "استيراد Import". أدخل المسار الكامل للمجلد الذي أُنشئ حديثًا squash_the_creeps_start/ في نافذة الاستيراد المنبثقة، ثم انقر على زر "تصفح Browse" على اليمين لفتح مستعرض الملفات والانتقال إلى ملف project.godot الموجود داخل المجلد. الآن انقر فوق "استيراد وتعديل Import & Edit" لفتح المشروع في المحرر يحتوي مشروع البداية على أيقونة ومجلدين هما art و fonts، حيث ستجد هناك الموارد الفنية كالصور والأيقونات والصوتية التي سنستخدمها في اللعبة. كما هو ملاحظ من الصورة السابقة التي توضّح محتويات نظام الملفات في مشروع جودو؛ هناك نموذجان ثلاثيا الأبعاد هما player.glb الخاص بالشخصية الرئيسية للاعب وmob.glb الخاص بشخصية العدو في مجلد art وبعض المواد التي تنتمي إلى هذه النماذج مع مقطوعة موسيقية. خطوات إنشاء منطقة اللعب سنُنشئ الآن المشهد الرئيسي في اللعبة باستخدام عقدة Node اعتيادية. انقر في نافذة "المشهد Scene" على زر "إضافة عقدة فرعية Add Child Node" المتمثل برمز إشارة الجمع في أعلى اليسار وانقر نقرًا مزدوجًا فوق Node ثم سمّي العقدة الرئيسية باسم Main. هناك طريقة بديلة لإعادة تسمية العقدة وهي النقر بالزر الأيمن فوق العقدة واختيار "إعادة التسمية Rename" أو F2 يمكنك الضغط أيضًا على Ctrl + A (أو Cmd + A على نظام التشغيل macOS) لإضافة عقدة إلى المشهد. احفظ المشهد باسم main.tscn بالضغط على Ctrl + S (Cmd + S على نظام التشغيل macOS). سنبدأ بإضافة أرضية تمنع الشخصيات من السقوط، ويمكنك استخدام عقد StaticBody3D لإنشاء تصادمات مع أجسام ثابتة static colliders مثل الأرضية أو الجدران أو الأسقف بحيث تصطدم بها الشخصيات أو الأجسام الأخرى دون أن تتأثر هي، لكنها تتطلب عقدًا فرعية CollisionShape3D لتحديد منطقة التصادم. أضف عقدة StaticBody3D بعد تحديد العقدة الرئيسية Main، ثم عقدة CollisionShape3D وأعد تسمية العقدة StaticBody3D إلى Ground. يجب أن يظهر تسلسل العقد في المشهد الخاص بك على هذا النحو: سترى علامة تحذير بجوار عقدة CollisionShape3D لأننا لم نحدد شكلها، وإذا نقرت على الأيقونة فستظهر نافذة منبثقة لتزويدك بمزيد من المعلومات. حدد العقدة CollisionShape3D لإنشاء شكل التصادم، ثم توجه إلى قائمة "الفاحص Inspector" وانقر على الحقل بجوار خاصية الشكل "Shape" لإنشاء "BoxShape3D" وهو شكل صندوقي ثلاثي الأبعاد. سنختار شكل الصندوق لكونه مثالي للأرضية المستوية والجدران، ويعتبر موثوقًا من ناحية اعتراض وصد الأجسام سريعة الحركة بفضل سماكته التي تجعل التصادم أكثر استقرارًا. يظهر إطار سلكي للصندوق wireframe في نافذة العرض viewport بثلاث نقاط برتقالية، حيث يمكنك نقر وسحب هذه النقاط لتعديل أبعاد الشكل بشكل تفاعلي. فوظيفة الإطار السلكي هي مساعدة مطور اللعبة على رؤية حجم الشكل وأبعاده وتعديله بسهولة أثناء التصميم. كما يمكنك تحديد الحجم بدقة من قائمة "الفاحص Inspector" من خلال النقر فوق BoxShape3D لتوسيع المورد، ثم ضبط قيمة الحجم إلى 60 على محور X، و 2 على محور Y، و 60 على محور Z. ونظرًا لكون أشكال التصادم غير مرئية، فستحتاج إلى إضافة أرضية مرئية معها من خلال تحديد العقدة Ground وإضافة MeshInstance3D كعقدة فرعية لها. انقر على الحقل بجوار الشبكة "Mesh" في قائمة "الفاحص Inspector"، ثم أنشئ مورد BoxMesh لإنشاء صندوق مرئي. كما تلاحظ؛ يكون الصندوق بشكل اقتراضي صغيرًا جدًا ولذا نحتاج إلى زيادة حجمه عن طريق النقر فوق أيقونة الصندوق لتوسيع المورد واضبط قيمته على 60 و 2 و 60. يجب أن ترى الآن مسطحًا رماديًا عريضًا يغطي الشبكة والمحاور الزرقاء والحمراء في نافذة العرض. سنحرّك الأرضية إلى أسفل حتى نتمكن من رؤية شبكة الأرضية floor grid. حدد العقدة Ground واضغط باستمرار على مفتاح Ctrl لتشغيل التقاط الشبكة grid snapping، ثم انقر واسحب لأسفل على محور Y الذي هو السهم الأخضر في أداة التحريك move gizmo. حرك الأرضية إلى أسفل بمقدار1 متر حتى تصبح شبكة المحرر مرئية، وستخبرك إشارة في الركن الأيسر السفلي من نافذة العرض بمدى انتقال translating العقدة. في النهاية، يجب أن تكون قيمة transform.position.y للعقدة Ground هي -1. لنضف ضوءًا اتجاهيًا directional light حتى لا يكون مشهدنا بالكامل باللون الرمادي ولتوفير إضاءة طبيعية، لذا حدد العقدة الرئيسية Main وأضف عقدة فرعية DirectionalLight3D لها فهذه العقدة هي المسؤولة عن توفير الإضاءة في المشهد. نحن بحاجة إلى تحريك وتدوير العقدة DirectionalLight3D حيث يمكنك تحريكها لأعلى بالنقر والسحب على السهم الأخضر لأداة التحريك، وانقر واسحب على القوس الأحمر لتدويرها حول محور X حتى تضاء الأرضية. شغل الظل من قائمة "الفاحص Inspector"، حيث يمكن تفعيله بالنقر فوق مربع الاختيار جانب "فعّال On". في هذه المرحلة، يجب أن يبدو مشروعك كما يلي. الخلاصة وصلنا إلى نهاية مقالنا الذي تعلمنا فيه كيفية إعداد منطقة اللعب للعبتنا ثلاثية الأبعاد والتي سيكون لاعب اللعبة متواجدًا بها، بدءًا من استيراد الموارد وتنظيمها داخل مشروع جودو بالإضافة لإنشاء شخصية اللاعب، لكن هذه نقطة البداية فحسب! سنتعلم في الدرس التالي كيفية إنشاء مشهد وتحريك اللاعب بشكل أساسي. ترجمة - وبتصرف - لقسم Your first 3D game وقسم Setting up the game area من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: بناء لعبة ثنائية البعد عبر محرك الألعاب Godot - الجزء الأخير: ربط مشاهد اللعبة ووضع اللمسات اﻷخيرة مدخل إلى محرك الألعاب جودو Godot دليلك الشامل إلى برمجة الألعاب أشهر أنواع الألعاب الإلكترونية
-
تسهّل لغة بروسيسنج Processing من عملية إنشاء النماذج الأولية للتطبيقات المرئية. إذ يصبح بناء لعبة بسيطة أسهل مما قد تتصور بفضل تركيباتها البرمجية سهلة الاستخدام وبعض الحسابات. هذا هو الجزء الثالث من سلسلة لغة بروسيسنج Processing. قدمنا في الجزأين الأول والثاني شرحًا تفصيليًا أساسيًا للغة بروسيسنج Processing. والخطوة التي سنشرحها اليوم والتي ستعزز لك تعلم بروسيسنج Processing هي بكل بساطة التعرف على مزيد من الأمثلة البرمجية العملية. سنوضح في هذا المقال كيفية استخدام بروسيسنج Processing لتنفيذ لعبتك الخاصة، خطوة بخطوة. سيتم شرح كل خطوة بالتفصيل. وبعد ذلك، سننقل اللعبة إلى الويب. قبل أن نبدأ التطبيق العملي، إليك شيفرة الشعار الوارد في تمرين شاشة توقف DVD الذي شرحناه في المقال السابق. الدرس التعليمي لبروسيسنج Processing: لعبة بسيطة اللعبة التي سنبنيها في هذا الدرس التعليمي باستخدام Processing هي مزيج بين كل من الألعاب الإلكترونية التالية "Flappy Bird" و "Pong" و "Brick Breaker". وسبب اختيار هذه اللعبة هو احتواؤها على معظم المفاهيم التي يصادفها المبتدئون عند تعلم برمجة الألعاب. وتشمل هذه المفاهيم الجاذبية، والتصادمات، والحفاظ على النقاط، والتعامل مع شاشات مختلفة وتفاعل لوحة المفاتيح/الفأرة. ولعبة "Flappy Pong" تحتوي على كل هذه المفاهيم. العب الآن! See the Pen Flappy Pong by Hsoub Academy (@HsoubAcademy) on CodePen. ليس من السهل بناء ألعابٍ معقدة دون استخدام مفاهيم البرمجة بالكائنات OOP، فألعاب المنصات ذات المستويات المتعددة، ومتعددة اللاعبين تجعل الشيفرة معقدة جدًا والبرمجة كائينة التوجه تساعدك على تنظيمها. وقد بذلنا قصارى جهدنا لجعل هذا الدرس التعليمي منظمًا وبسيطًا. ننصحك بقراءة كامل المقال، والحصول على الشيفرة البرمجية الكاملة ومحاولة فهمها، واللعب به بمفردك لمعرفة منطق اللعب ثم البدء في التفكير في لعبتك الخاصة بأسرع ما يمكن، ثمّ البدء في تنفيذها. لذا دعونا نبدأ. بناء لعبة Flappy Pong الخطوة 1: تهيئة ومعالجة الشاشات المختلفة الخطوة الأولى هي تهيئة مشروعنا. سنكتب كتل الإعداد والرسم كالمعتاد، وما من شيء جديد في هذه الخطوة، ثم سنتعامل مع الشاشات المختلفة (شاشة البداية، شاشة اللعب، شاشة الخروج إلخ). فكيف نجعل بروسيسنج Processing تعرض الصفحة الصحيحة في الوقت الصحيح؟ يعدّ تحقيق هذه المهمة بسيطًا نسبيًا. إذ إننا سننشئ متغيرًا عامًا يخزن معلومات الشاشة النشطة حاليًا. ثم، سنرسم محتويات الشاشة الصحيحة اعتمادًا على هذا المتغير. في كتلة الرسم، ستكون لدينا تعليمة "if" تفحص المتغير وتعرض محتويات الشاشة وفقًا لذلك. كلما أردنا تغيير الشاشة، لذلك سنغيّر هذا المتغير إلى معرف الشاشة التي نريد عرضها. وهكذا يكون مظهر شيفرتنا البرمجية: /********* VARIABLES *********/ // نتحكم بالشاشة التي نريد تنشيطها من خلال تحديث وضبط متغيّر gameScreen // نعرض الشاشة الصحيحة بحسب قيمة هذا المتغير // // 0: الشاشة الأساسية // 1: شاشة اللعبة // 2: شاشة نهاية اللعبة int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // يعرض محتوى الشاشة الحالية if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // الشيفرة البرمجية للشاشة الرئيسية } void gameScreen() { // الشيفرة البرمجية لشاشة اللعبة } void gameOverScreen() { // الشيفرة البرمجية لشاشة انتهاء اللعبة } /********* INPUTS *********/ public void mousePressed() { // إذا كنا على الشاشة الرئيسية عند النقر، ابدأ اللعبة if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // هذا التابع يحدد المتغيرات الضرورية لبدء اللعبة void startGame() { gameScreen=1; } قد تبدو الشيفرة معقدة في البداية، ولكن كل ما فعلناه هو بناء الهيكل الأساسي وفصل الأجزاء المختلفة باستخدام كتل التعليقات. كما ترى، نحدد تابعًا مختلفًا لعرض كل شاشة. ونتحقق ببساطة من قيمة متغير gameScreen في كتلة الرسم الخاصة بنا، ونستدعي التابع المقابل. في الجزء void mousePressed(){...}، نستمع إلى نقرات الفأرة وإذا كانت الشاشة النشطة هي 0، الشاشة الأولية، نستدعي تابع startGame() الذي يبدأ اللعبة. ويغيّر السطر الأول من هذا التابع متغير gameScreen إلى 1 شاشة اللعبة. إذا فهمت ذلك، فالخطوة التالية هي تنفيذ شاشتنا الأولية. وللقيام بذلك، سنحرر تابع initScreen(). الذي يبدأ من هنا : void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); } و الآن، في شاشة البداية، أضفتا خلفية سوداء ونص بسيط يظهر في منتصف الشاشة مكتوب عليه "انقر للبدء Click to start". ومع ذلك، عند النقر على الشاشة، لا يحدث أي شيء، وهذا متوقع لأننا لم نكتب بعد أي كود لتنفيذ محتوى اللعبة بعد الضغط على الزر. حالياً، لا تحتوي الدالة ()gameScreen على أية تعليمات، ولذلك عند الانتقال لشاشة اللعبة، فإن المحتوى السابق (أي النص) لا يزال يظهر لأننا لم نضع تعليمة ()background لإعادة رسم الخلفية وإخفاء المحتويات السابقة. لهذا السبب، يستمر النص في الظهور حتى بعد الانتقال، تمامًا كما كان يحدث في مثال الكرة المتحركة التي كانت تترك أثراً خلفها.لذلك دعونا نمضي قدمًا ونبدأ في تنفيذ شاشة اللعبة. void gameScreen() { background(255); } بعد هذا التغيير، ستلاحظ أن الخلفية تتحول إلى بيضاء ويختفي النص. الخطوة 2: إنشاء الكرة وتطبيق الجاذبية والآن سنبدأ العمل على شاشة اللعبة. سننشئ كرتنا أولًا. يجب أن نحدد متغيرات لإحداثياتها ولونها وحجمها لأننا قد نرغب في تغيير هذه القيم لاحقًا. على سبيل المثال، إذا أردنا زيادة حجم الكرة عندما يسجل اللاعب أعلى النقاط، تصبح اللعبة أكثر صعوبة. سنحتاج إلى تغيير حجمه، لذلك يجب أن يكون متغيرًا. وسنحدد سرعة الكرة أيضًا، بعد أن نطبق الجاذبية. أولًا نضيف ما يلي: ... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); } عرّفنا الإحداثيات كمتغيرات عامّة، وأنشأنا تابعًا لرسم الكرة التي استدعيناها من التابع gameScreen، الشيء الوحيد الذي يجب الانتباه إليه هنا هو أننا هيّئنا الإحداثيات، ولكننا عرفناها في setup(). السبب وراء قيامنا بذلك هو أننا أردنا أن تبدأ الكرة من مسافة ربع من اليسار وخمس من الأعلى. ليس هناك سبب محدد وراء رغبتنا في ذلك، إلا أنّ هذه نقطة جيدة لبدء الكرة. لذلك نحن بحاجة للحصول على width وheight للرسم ديناميكيًا. يتم تعريف حجم الرسم في setup() بعد السطر الأول. لم يتم ضبط width وheight قبل تنفيذ setup()، ولهذا السبب لم نتمكن من تحقيق ذلك إذا حدّدنا المتغيرات في الأعلى. الجاذبية الآن تطبيق الجاذبية هو الجزء السهل في الواقع. إذ لا يوجد سوى عدد قليل من الحيل. وهذا التطبيق أولًا: ... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // ابقِ الكرة داخل الشاشة void keepInScreen() { // تصل الكرة إلى الأرض if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // تضرب الكرة السقف if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } } والنتيجة هي: تمهّل يا عالم الفيزياء! أعلم أن هذه ليست الطريقة التي تعمل بها الجاذبية في الحياة الواقعية. بدلًا من ذلك، هي عملية رسوم متحركة أكثر من أي شيء آخر. المتغير الذي عرفناه gravity هو مجرد قيمة رقمية float حتى نتمكن من استخدام القيم العشرية، وليس فقط الأعداد الصحيحة - نضيفها إلى ballSpeedVert في كل حلقة. و ballSpeedVert هي السرعة الرأسية للكرة، التي تضاف إلى محور Y للكرة (ballY) في كل حلقة. نشاهد إحداثيات الكرة ونتأكد من بقائها في الشاشة. إن لم نفعل ذلك، ستسقط إلى ما لا نهاية. في الوقت الحالي، تتحرك الكرة رأسيًا فقط. لذلك نشاهد حدود الأرضية والسقف للشاشة. نتحقق مما إذا كانتballY (+ نصف القطر) أقل من الارتفاع باستخدام التابع keepInScreen()، وبالمثل ballY (- نصف القطر) أكبر من0. إذا لم تتحقق الشروط، نجعل الكرة ترتد (من الأسفل أو الأعلى) باستخدام تابعيّ makeBounceBottom() وmakeBounceTop(). لجعل الكرة ترتد، نحرك الكرة إلى الموقع المحدد الذي يجب أن ترتد فيه ونضرب السرعة العمودية (ballSpeedVert) في-1 (الضرب في -1 يغير الإشارة). عندما تحتوي قيمة السرعة على علامة سالب، فإن إضافة الإحداثي Y تجعل السرعة (ballY + (-ballSpeedVert، وهي ballY - ballSpeedVert. لذا تغيّر اتجاه الكرة على الفور بنفس السرعة. بعد ذلك، عندما نضيف الجاذبية إلى ballSpeedVert وتكون قيمة ballSpeedVert سالبة، فإنها تبدأ في الاقتراب من 0، وتصبح في النهاية 0، وتبدأ في الزيادة مرة أخرى. وهذا يجعل الكرة ترتفع، وترتفع بشكل أبطأ، إلى أن تتوقف وتبدأ بالسقوط. هناك مشكلة في عملية الرسوم المتحركة لدينا، رغم ذلك، إذ إنّ الكرة تستمر في الارتداد. إذا كان هذا سيناريو حقيقيًا، لكانت الكرة ستواجه مقاومة الهواء والاحتكاك في كل مرة تلمس فيها سطحًا. هذا هو السلوك الذي نريده لعملية الرسوم المتحركة في لعبتنا، لذا فإن تنفيذ ذلك أمر سهل. ونضيف ما يلي: ... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } والآن ستبدو عملية التحريك الخاصة بنا بهذا الشكل: كما يوحي الاسم، friction هو الاحتكاك السطحي و airfriction هو احتكاك الهواء. لذا من الواضح أن friction يجب أن يحدث في كل مرة تلمس فيها الكرة أي سطح. ومع ذلك، يجب أن نطبّق airfriction باستمرار. وهذا ما فعلناه. والآن ننفّذ تابع applyGravity() على كل حلقة، لذلك نحذف 0.0001 بالمئة من قيمتها الحالية من ballSpeedVert في كل حلقة. ثمّ ننفذ تابعي makeBounceBottom() و makeBounceTop() عندما تلمس الكرة أي سطح. لذا، في تلك الطرق، فعلنا نفس الشيء، ولكن هذه المرة باستخدام friction. الخطوة 3: إنشاء مضرب تنس الآن نحتاج إلى مضرب تنس للكرة لكي ترتدّ عليه. ويجب أن نتحكم بالمضرب. والآن لنتحكم به من خلال الفأرة. هذه هي الشيفرة البرمجية: ... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); } لقد عرّفنا اللون، الطول والعرض الخاصين بالمضرب كمتغير عام، لأننا قد نريدهم أن يتغيروا أثناء اللعب. طبّقنا التابع drawRacket() الذي ينفّذ ما يوضحه اسمه (يرسم المضرب). ضبطنا وضع rectMode على المركز، بحيث يكون مضربنا محاذيًا لمركز المؤشر. الآن بعد أن أنشأنا المضرب، علينا أن نجعل الكرة ترتد عليه. ... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // يتحرك المضرب إلى الأعلى if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } } وهذه هي النتيجة: إذن ما يفعله watchRacketBounce() هو التأكد من اصطدام المضرب والكرة. هناك شيئان يجب التحقق منهما هنا، وهما إذا ما كانت الكرة والمضرب مصطفين رأسيًا وأفقيًا. تتحقق عبارة if الأولى مما إذا كان إحداثي X للجانب الأيمن من الكرة أكبر من إحداثي X للجانب الأيسر من المضرب (والعكس صحيح). إذا كان الأمر كذلك، فإن العبارة الثانية تتحقق مما إذا كانت المسافة بين الكرة والمضرب أصغر من أو تساوي نصف قطر الكرة (مما يعني أنهما يتصادمان). لذا، إذا استوفيت هذه الشروط، فسيتم استدعاء تابع makeBounceBottom() وترتد الكرة على مضربنا (عند mouseY، حيث يوجد المضرب). هل لاحظت مقدار المتغير overhead الذي يتم حسابه بواسطة mouseY - pmouseY؟ يخزّن المتغيّران pmouseX و pmouseY إحداثيات الفأرة في الإطار السابق. نظرًا لأن الفأرة يمكن أن تتحرك بسرعة كبيرة، فهناك احتمال كبير أننا قد لا نكتشف المسافة بين الكرة والمضرب بشكل صحيح بين الإطارات إذا كان الفأر يتحرك نحو الكرة بسرعة كافية. لذا، سنأخذ اختلاف إحداثيات الماوس بين الإطارات ونأخذ ذلك في الاعتبار أثناء اكتشاف المسافة. كلما تحركت الفأرة بشكل أسرع، كانت المسافة الأكبر مقبولة. نستخدم أيضًا overhead لسبب آخر. نكتشف الاتجاه الذي تتحرك به الفأرة من خلال التحقق من علامة overhead. إذا كان overhead سالبًا، فهذا يعني أن الفأرة كانت في مكان ما أسفل الإطار السابق، لذا فإن الفأرة (المضرب) يتحرك نحو الأعلى. في هذه الحالة، نريد إضافة سرعة إضافية للكرة وتحريكها أبعد قليلًا من الارتداد العادي لمحاكاة تأثير ضرب الكرة بالمضرب. إذا كان overhead أقل من 0، فإننا نضيفه إلى ballY و ballSpeedVert لجعل الكرة ترتفع إلى أعلى وأسرع. لذا، كلما أسرع المضرب في ضرب الكرة، كلما تحركت إلى أعلى وأسرع. الخطوة 4: الحركة الأفقية والسيطرة على الكرة في هذا القسم، سنضيف الحركة الأفقية إلى الكرة. ثمّ، سنحقّق التحكم بالكرة أفقياً بدون مضرب. لنبدأ: ... // سنبدأ بـ 0، لكننا نعطي 10 للاختبار فقط تعويم الكرة SpeedHorizon = 10؛ float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } } وستكون النتيجة كالتالي: الفكرة هنا هي نفسها التي نفذناها للحركة الرأسية. أنشأنا متغير السرعة الأفقي، ballSpeedHorizon. لقد أنشأنا تابعاً لتطبيق السرعة الأفقية على ballX وإزالة احتكاك الهواء. أضفنا عبارتي if إضافيتين إلى تابع keepInScreen() والتي ستراقب الكرة وهي تضرب الحواف اليسرى واليمنى للشاشة. وأخيرًا، أنشأنا تابعي makeBounceLeft() وmakeBounceRight() للتعامل مع الارتدادات من اليسار واليمين. الآن بعد أن أضفنا السرعة الأفقية إلى اللعبة، نريد التحكم في الكرة بالمضرب. كما هو الحال في لعبة أتاري الشهيرة Breakout وفي جميع ألعاب كسر الطوب الأخرى، يجب أن تتحرك الكرة يسارًا أو يمينًا وفقًا للنقطة التي تضربها على المضرب. يجب أن تمنح حواف المضرب الكرة سرعة أفقية أكبر بينما لا ينبغي أن يكون للوسط أي تأثير. الشيفرة البرمجية أولا: void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } } النتيجة هي: إن إضافة هذا السطر البسيط إلى watchRacketBounce() أدى المهمة. ما فعلناه هو أننا حددنا مسافة النقطة التي تضربها الكرة من مركز المضرب باستخدام ballX - mouseX. ثم نجعلها بالسرعة الأفقية. كان الفرق الفعلي كبيرًا جدًا، لذا أجرينا بعض المحاولات واكتشفنا أن عُشر القيمة تبدو طبيعية أكثر. الخطوة 5: إنشاء الجدران بدأ رسمنا يبدو وكأنه لعبة مع كل خطوة. في هذه الخطوة، سنضيف جدران تتحرك نحو اليسار، تمامًا كما في Flappy Bird: ... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // تقوم قائمة المصفوفات هذه بتخزين بيانات الفجوات بين الجدران. يتم رسم الجدران الفعلية وفقًا لذلك // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // للحصول على إعدادات جدار الفجوة int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // رسم الجدران الفعلية rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } } وهذه نتيجة ما نفّذنا: قد تبدو الشيفرة البرمجية طويلةً ومخيفةً، إلا أننا نعدك أنّه ما من شيءٍ صعب. الشيء الأول الذي يجب ملاحظته هو ArrayList. بالنسبة لأولئك الذين لا يعرفون ما هي ArrayList، فهي مجرد تطبيق للقائمة التي تعمل كالمصفوفة، ولكنها تملك بعض الميزات الإضافية. إذ إنها قابلة لتغيير الحجم، وتحتوي على توابع مفيدة مثل (list.add (index، و(list.get(index و(list.remove(index. نحتفظ ببيانات الجدار كمصفوفات صحيحة داخل قائمة المصفوفات. البيانات التي نحتفظ بها في المصفوفات مخصصة للفجوة بين جدارين. تحتوي المصفوفات على القيم التالية: [gap wall X, gap wall Y, gap wall width, gap wall height] ترسم الجدران الفعلية بناءً على قيم جدار الفجوة. لاحظ أنه يمكننا التعامل مع كل هذه الأمور بشكل أفضل باستخدام الفئات، ولكن نظرًا لأن استخدام البرمجة كائنية التوجه OOP ليس ضمن نطاق البرنامج التعليمي للمعالجة، فهذه هي الطريقة التي سنتعامل بها. لدينا طريقتان أساسيتان لإدارة الجدران هما wallAdder() وwallHandler. يضيف تابع wallAdder() ببساطة جدرانًا جديدة في كل مللي ثانية من wallInterval إلى قائمة المصفوفات. لدينا متغير عام lastAddTime الذي يخزن الوقت الذي تمت فيه إضافة الجدار الأخير (بالمللي ثانية). إذا كانت قيمة المللي ثانية الحالية millis() ناقص آخر مللي ثانية تمت إضافتها lastAddTime أكبر من قيمة الفاصل الزمني الخاصة بنا wallInterval، فهذا يعني أن الوقت قد حان لإضافة جدار جديد. يتم بعد ذلك إنشاء متغيرات الفجوة العشوائية بناءً على المتغيرات العامة المحددة في الأعلى. ثم تتم إضافة جدار جديد (مصفوفة عدد صحيح تخزن بيانات جدار الفجوة) إلى قائمة المصفوفات ويتم تعيين lastAddTime على المللي ثانية الحالية millis(). تنفذ حلقات wallHandler() عبر الجدران الحالية الموجودة في قائمة المصفوفات. ولكل عنصر في كل حلقة، يستدعي كلاً من (wallRemover(i و (wallMover(i و (wallDrawer(i حسب قيمة فهرس قائمة المصفوفات. يقوم هذا التابع بما يوحي اسمه. يرسم التابع wallDrawer() الجدران الفعلية بناءً على بيانات فجوة الجدار. فهو يلتقط مصفوفة بيانات الجدار من قائمة المصفوفات، ويستدعي التابع rect() لرسم الجدران إلى المكان الذي ينبغي أن تكون فيه بالفعل. يلتقط تابع wallMover() العنصر من قائمة المصفوفات، وتغير موقعه X بناءً على المتغير العام wallSpeed. أخيرًا، يزيل التابع wallRemover() الجدران من قائمة المصفوفات الموجودة خارج الشاشة. لأننا إن لم نفعل ذلك، لكانت بروسيسنج Processing قد تعاملت معهم كما لو كانوا لا يزالون في الشاشة. وكان من الممكن أن يكون ذلك خسارة فادحة في الأداء. لذا، عند إزالة جدار من قائمة المصفوفات، لا يتم رسمه في الحلقات اللاحقة. التحدي الأخير المتبقي هو اكتشاف الاصطدامات بين الكرة والجدران. void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // الحصول على إعدادات جدار الفجوة int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // يصطدم بالجدار العلوي } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // يصطدم بالجدار السفلي } } يستدعى التابع watchwallcollision لكل جدار في كلّ حلقة. ثم نلتقط إحداثيات جدار الفجوة، ونحسب إحداثيات الجدران الفعلية (العلوية والسفلى) ونتحقق مما إذا كانت إحداثيات الكرة تصطدم بالجدران. الخطوة 6: تنفيذ شريط الصحة Health Bar الآن بعد أن أصبح بإمكاننا اكتشاف اصطدامات الكرة بالجدران، يمكننا تحديد آليات اللعبة. بعد ضبط للعبة، تمكنت من جعل اللعبة قابلة للعب إلى حد ما. ولكن مع ذلك، كان الأمر صعبًا للغاية. وكان أول ما فكرت به في اللعبة أن أجعلها مثل Flappy Bird، عندما تلمس الكرة الجدران، تنتهي اللعبة. ولكن بعد ذلك أدركت أنه سيكون من المستحيل اللعب. لذا هذا ما فكرت به: يجب أن يكون هناك شريط صحة أعلى الكرة. أي يجب أن تفقد الكرة صحتها أثناء ملامستها للجدران. وبهذا المنطق، ليس من المنطقي أن نجعل الكرة ترتد عن الجدران. لذلك عندما تكون الصحة 0، يجب أن تنتهي اللعبة ويجب أن ننتقل إلى اللعبة على الشاشة. لننفذ شريط صحة مرتبط بالكرة، حيث سيقل تدريجيًا عند كل اصطدام بالجدران. وعندما يصل إلى الصفر، ستنتهي اللعبة كما يلي: int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // جعلها بلا حدود: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } } وهذا تنفيذٌ سريع لما قمنا به: لقد أنشأنا متغيرًا عامًا health يحفظ قيمة صحة الكرة. ثم أنشأنا تابع drawHealthBar() كي يرسم مستطيلين أعلى الكرة. الأول هو شريط الصحة الأساسي الكامل، والآخر هو الشريط النشط الذي يُظهر الصحة الحالية حيث يتغير حجمه بناءً على مقدار الصحة المتبقية، ويحسب باستخدام (healthBarWidth*(health/maxHealth، وهي نسبة الصحة الحالية مقارنة بشريط الصحة الكامل. ثمّ، يتم تعيين ألوان التعبئة لهذا الشريط وفقًا لقيمة الصحة ويعرض أخيرًا الدرجة Score التي حصلت عليها: ... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // تمت إضافة قيمة أخرى في نهاية المصفوفة int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); } كنا بحاجة للتسجيل عندما تمر الكرة بالحائط. لكننا بحاجة إلى إضافة درجة واحدة كحد أقصى لكل جدار. بمعنى، إذا مرت الكرة بالحائط ثم عادت ومرت مرةً أخرى، فلا ينبغي إضافة نتيجة أخرى. ولتحقيق ذلك، أضفنا متغيرًا آخر إلى مصفوفة جدار الفجوة داخل قائمة المصفوفات. يخزن المتغير الجديد 0 إذا لم تتجاوز الكرة هذا الجدار بعد و1 إذا تجاوزت ذلك الجدار. بعد ذلك، عدلنا تابع watchWallCollision(). أضفنا شرطًا لإطلاق تابع Score() ووضع علامة على الجدار كمرور عندما تمر الكرة بجدار لم تتجاوزه من قبل. نحن الآن قريبون جدًا من نهاية تطوير لعبتنا. آخر ما عليك فعله هو برمجة النقر على نص click to restart الظاهر على الشاشة. إلا أنّنا بحاجة إلى تعيين كافة المتغيرات التي استخدمناها إلى قيمتها الأولية، وإعادة تشغيل اللعبة. ها هو. ... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; } لنضف الآن بعض الألوان. تهانينا لديك الآن لعبة Flappy Pong متكاملة، جرب تنفيذها واللعب بها، وفي حال واجهت أي أي مشكلة يمكنك إيجاد الشيفرة البرمجية الكاملة لمعالجة اللعبة هنا نقل شيفرة معالجة اللعبة إلى الويب باستخدام p5.js تعرّف p5.js على أنها مكتبة للغة جافاسكريبت p5.js ذات بنية مشابهة للغة برمجة بروسيسنج Processing . وهي ليست مكتبة قادرة على تنفيذ شيفرة بروسيسنج Processing الموجودة ببساطة فبدلًا من ذلك، تتطلب p5.js كتابة تعليمات جافا سكريبت فعلية، على غرار منفذ جافا سكريبت لبروسيسنج Processing المعروف باسم Processing.js. مهمتنا هي تحويل شيفرة بروسيسنج Processing إلى جافا سكريبت باستخدام p5.js API. تحتوي المكتبة على مجموعة من الوظائف وبناء جملة مشابهة للمعالجة، وعلينا إجراء تغييرات معينة على التعليمات البرمجية الخاصة بنا لجعلها تعمل في جافا سكريبت - ولكن نظرًا لأن كلًّا من بروسيسنج Processing وجافا سكريبت يشتركان في أوجه التشابه مع جافا، فهي أقل انتقالًا مفاجئًا مما يبدو. حتى لو لم تكن مطور جافا سكريبت، فإن التغييرات بسيطة جدًا ويجب أن تكون قادرًا متابعتها بسهولة. أولًا، نحتاج إلى إنشاء ملف Index.html بسيط وإضافة p5.min.js إلى الترويسة. نحتاج أيضًا إلى إنشاء ملف آخر يسمى flappy_pong.js الذي سيحتوي على الشيفرة المحولة الخاصة بنا. <html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html> يجب أن تكون استراتيجيتنا أثناء تحويل الشيفرة بنسخ كل الشيفرات ولصقها في flappy_pong.js ثم تنفيذ جميع التغييرات. وهذا ما فعلنا، إليك الخطوات التي اتخذناها لتحديث الشفيرة: جافا سكريبت هي لغة لغة ديناميكية الأنواع (إذ لا توجد تصريحات عن نوع المتغير مثل int و float) لذلك نحتاج إلى تغيير جميع تصريحات المتغيرات إلى var. لا يوجد void في جافا سكريبت. لذا يجب علينا تغيير جميع هذه الكلمات إلى الكلمة المفتاحية function. نحتاج إلى إزالة تصريحات النوع من الوسائط في توقيعات الدوال (على سبيل المثال نغير void wallMover(var index { لتكون function wallMover(index { . لا يوجد ArrayList في جافا سكريبت. لكن يمكننا تحقيق الشيء ذاته باستخدام مصفوفات جافا سكريبت. نُنشئ التغييرات التالية: غيّر ArrayList<int[]> walls = new ArrayList<int[]>(); إلى var walls = []; غيّر walls.clear(); إلى walls = []; غيّر walls.add(randWall); إلى walls.push(randWall); غيّر walls.remove(index); إلى walls.splice(index,1); غيّر walls.get(index); إلى walls[index] غيّر walls.size() إلى walls.length غيّر تصريح المصفوفة var randWall = {width, randY, wallWidth, randHeight, 0}; إلى var randWall = [width, randY, wallWidth, randHeight, 0]; أزل كل كلمات public. انقل كل تصريحات color(0) إلى داخل الدالة setup() لأن color() لن تُعرَّف قبل استدعاء setup(). تغيير size(500, 500); إلى createCanvas(500, 500); أعد تسمية الدالة gameScreen(){ إلى شيء آخر مثل function gamePlayScreen(){ لأن لدينا بالفعل متغير عام يسمى gameScreen. عندما كنا نعمل مع لغة بروسيسنج، كان أحدها دالة والأخرى متغير int. ولكن جافاسكريبت تخلط بينها لأنها غير مكتوبة النوع. يحدث الشيء نفسه للدالة score(). غيّرنا اسمها إلى addScore(). يمكن العثور على شيفرة جافا سكريبت الكاملة التي تغطي كل شيء في هذا الدرس التعليمي باستخدام Processing من هنا. الخلاصة إلى هنا نكون قد انتهينا من هذه السلسلة التي شرحنا فيها كيفية إنشاء لعبة بسيطة جدًا. ومع ذلك، فإن ما قمنا به في هذا المقال هو مجرد غيض من فيض. باستخدام لغة برمجة بروسيسنج Processing، حيث يمكنك تحقيق أي شيء بها تقريبًا، ويمكن القول أنها أفضل أداة لبرمجة ما كل ما تتخيله، والهدف الأساسي من هذه السلسلة التعليمية هي إثبات أن البرمجة ليست بهذه الصعوبة بدلًا من تدريس بروسيسنج Processing وبناء اللعبة الخاصة بك. إن صناعة لعبتك الخاصة ليس مجرد حلم. وأردنا أن نوضح لك أنه مع القليل من الجهد والحماس، يمكنك القيام بذلك بسهولة. ترجمة -بتصرف- لمقال Ultimate Guide to the Processing Language Part II: Building a Simple Game لكاتبه Oguz Gelal. اقرأ أيضًا المقال السابق: دليلك للغة برمجة Processing | الجزء الثاني: الرسم والتفاعل مع دخل المستخدم دليلك الشامل إلى برمجة الألعاب مطور الألعاب: من هو وما هي مهامه مبادئ كتابة جافا سكريبت متسقة ومفهومة مشروع لعبة منصة باستخدام جافاسكربت
-
بعد أن تعرّفنا في مقالتنا السابقة على لغة برمجة بروسيسنج وبيئة التطوير الخاصة بها وهيكلة البرنامج، سنتابع في الجزء الثاني التعرف على أبرز توابع الرسم الموجودة فيها وكيفية استخدامها، بالإضافة للتفاعل مع دخل المستخدم. رسم الأشكال والنصوص الآن سنتحدث عن بعض الأمور البصرية بما أننا أصبحنا نعرف كيفية ضبط المشروع باستخدام كتلة التهيئة ونعرف كيفية عمل رسم كتلة الرسم، كما سنتعلم الأمور المسلية في بروسيسنج processing مثل كيفية رسم الأشكال. قبل البدء يجب أن نفهم محاور الإحداثيات، إذ يجب تحديد إحداثيات كل شكل يتم رسمه على الشاشة في بروسيسنج processing. الواحدات مقدرة بالبيكسل والمبدأ (نقطة البداية) هي الزاوية اليسرى العلوية، يجب أن تحدد الإحداثيات بالنسبة لهذه النقطة. شيء آخر يجب أن تعرفه هو أن لكل شكل نقطة مرجعية مختلفة، مثلًا النقطة المرجعية للدالة react() هي الزاوية اليسرى العلوية، أما بالنسبة لدالة ellipse() فهي المركز. ويمكنك تغيير النقطة المرجعية باستخدام التوابع مثل rectMode() و ellipseMode() التي سنشرحها في قسم الخصائص والإعدادات. سنقتصر على توفير رؤية عامة أساسية للغة بروسيسنج ولن نتطرق لأي أشكال معقدة مثل الأشعة والأشكال ثلاثية البعد، فالأشكال ثنائية البعد تكفي لإنشاء لعبتنا، رأينا في الصورة أمثلة عن كيفية رسم الأشكال، كل شكل لديه صياغة خاصة به ليُنشئ، والفكرة الأساسية هي إما تحديد أبعاده أو حجمه أو كلاهما، التالي هو بعض الأمثلة التي يجب أن تتعلمها (تعني القيم x و y الإحداثيات مقدرة بالبيكسل لكل من المحاور الإحداثية x و y، وتعني القيم h و w العرض والطول مقدرة بالبيكسل أيضًا). الدالة point(): تستخدم لرسم نقطة بسيطة تحتاج قيمة إحداثيات واحدة، وتستخدم كما يلي: point(x, y) point(x, y, z) // في حالة الأبعاد الثلاثية الدالة line() - لإنشاء سطر، يمكنك إنشاء سطر بتحديد نقطة البداية والنهاية، وتستخدم كما يلي: line(x1, y1, x2, y2) line(x1, y1, z1, x2, y2, z2) // في حالة الأبعاد الثلاثية الدالة triangle() - لإنشاء المثلثات، وتستخدم كما يلي: triangle(x1, y1, x2, y2 ,x3 ,y3) الدالة quad() - لإنشاء المضلعات الرباعية، وتستخدم كما يلي: quad(x1, y1, x2, y2 ,x3, y3, x4 ,y4) الدالة rect(): تستخدم لرسم المربعات والأشكال المستطيلة. وتقع النقطة المرجعية في في الزاوية اليسارية العليا (راجع الصورة السابقة)، تستخدم الدالة كما يلي: rect(x, y, w, h) rect(x, y, w, h, r) // تعني r نصف القطر مقدرًا بالبيكسل لجعل زوايا المربع مدورة rect(x, y, w, h, tl, tr, br, bl) // أنصاف الأقطار للزاويتين العلوية اليسرى واليمنى والسفلية اليمنى واليسرى على الترتيب مقدرة بالبيكسل الدالة ellipse(): تستخدم لرسم قطع ناقص ومن أجل رسم الدوائر إذ يجب تحديد قيم متساوية للعرض والارتفاع، إن النقطة المرجعية هي افتراضيًا في المنتصف. وتستخدم كما يلي: ellipse(x, y, w, h) الدالة arc(): ترسم قوس، وتستخدم كما يلي: arc(x, y, w, h, start, stop) تشير المتغيرات start و stop في هذه الحالة إلى زاوية بداية ونهاية رسم القوس مقدرة بالراديان، ويمكن استخدام القيم التالية PI و HALF_PI و HALF_PI و TWO_PI لحساب الأقواي والمسارات الدائرية بسهولة أكبر. كما تستخدم أيضًا على النحو التالي: arc(x, y, w, h, start, stop, mode) يحدد المتغير mode هنا طريقة تصيير أو رسم القوس وتمرر قيمته كسلسلة نصية وتكون الخيارت المتاحة لقيم ذلك المتغير هي "OPEN" لرسم القوس فقط دون إغلاق الأطراف، و "PIE" لرسم قوس ووصل طرفيه بالمركز حيث نحصل على ما يشبه قطعة من فطيرة و "CHROD" لرسم القوس مع وصل طرفيه بخط مستقيم. إن إظهار النصوص على الشاشة يشابه إظهار الأشكال عن طريق تحديد إحداثيات مكان إظهار النص الدالة text() تظهر النصوص، وتستخدم كما يلي: text(c, x, y) // يشير المعامل c إلى المحرف، ويتم إظهار أي محرف أبجدي text(c, x, y, z) // في حال العمل في الأبعاد الثلاثة text(str, x, y) // تعني str السلسلة النصية المراد إظهارها text(str, x, y, z) // في حال العمل في الأبعاد الثلاثة text(num, x, y) // تعني num القيمة الرقمية المراد إظهارها text(num, x, y, z) // في حال العمل في الأبعاد الثلاثة الإعدادات والخصائص أول شيء يجب معرفته في هذا القسم هو المنطق المستخدم في ضبط خصائص الكائن، ومن الأمثلة عن ذلك هي لون التعبئة، و لون الخلفية، والحدود، وسماكة الحدود، ولون الحدود، ومحاذاة الشكل، وأنماط الحدود ...إلخ. تذكر عند ضبط الخاصية أن الشيفرة تُنفّذ من الأعلى إلى الأسفل، لنقل أنك تريد ضبط خاصية التعبئة اللونية "fill" باللون الأحمر، وبالتالي كل الكائنات التي ستُرسم بعدها ستكون تعبئتها حمراء حتى نعيد تعريف الخاصية، والأمر نفسه ينطبق أيضًا على باقي الخصائص. لكن الجدير بالملاحظة هنا أن بعض الخصائص لا تستبدل بعضها البعض. على سبيل المثال، خاصية لا تستبدل خاصية لون الحد stroke قيمة خاصية التعبئة fill لأنهما يعملان مع بعضهما. يسهل الشكل التالي فهم منطق عمل الخاصيات . لاحظ كيف تؤثر أوامر fill و stroke على جميع الأشكال المرسومة بعدها حتى تقوم بتغييرها أو إلغائها. كما رأيت في الشكل أعلاه يضبط السطر الأول لون التعبئة للأحمر، ويضبط السطر الثاني لون خط الحدود للأزرق، وبالتالي لدينا خاصتان فعالتان هنا وهما التعبئة باللون الأحمر ولون خط الحدود بالأزرق، وبالتالي سيكون كل كائن يرسم في الأسطر التالية من الكود بلون تعبئة حمراء وخط حد أزرق (إذا كان ذلك ممكنًا فبعض الأشكال قد لا تحتوي على حدود)، تمعّن بالشكل لتفهم المنطق بشكل أوضح. فيما يلي نشرح بعض الخصائص والإعدادات المتداولة في التصميم. إعدادات التصميم الدالة fill(): تضبط نوع اللون لتعبئة الكائنات، تستخدم هذه الدالة لتلوين النصوص، ويكفي مبدئيًا تعلم الاستخدامات التالية: fill(r, g, b) // قيم الأحمر والأخضر والأزرق كأعداد صحيحة fill(r, g, b, a) // قيمة alpha إضافية، قيمتها العظمى هي 225 الدالة noFill(): تستخدم لضبط لون التعبئة ليكون شفاف الدالة stroke(): تستخدم لضبط نوع لون خط الحدود للكائنات، يمكن تطبيق خاصية الحد للخطوط والحدود حول الكائنات، ويكفي مبدئيًا تعلم الاستخدامات التالية: stroke(r, g, b) // قيم الأحمر والأخضر والأزرق كأعداد صحيحة stroke(r, g, b, a) // قيمة alpha إضافية، قيمتها العظمى هي 225 الدالة noStrok(): تًزيل الحد الدالة strokeWeight(): تضبط سماكة خط الحد، وفيما يلي مثال على طريقة الاستخدام: strokeWeight(x) // إن قيمة x هو عدد صحيح يمثل عرض الخط مقدرة بالبيكسل الدالة background(): تضبط لون الخلفية، ويكفي مبدئيًا تعلم الاستخدامات التالية: background(r, g, b) // تمرر قيم الأحمر والأخضر والأزرق كأعداد صحيحة. background(r, g, b, a) // قيمة alpha إضافية، وقيمتها العظمى هي 225 إعدادات المحاذاة الدالة ellipseMode(): تستسخدم لضبط موضع النقطة المرجعية للقطع الناقص، وهذا مثال عن طريقة الاستخدام: ellipseMode(mode) يملك المعامل mode القيم الممكنة التالية التي تتحكم في كيفية رسم القطع الناقص: CENTER(default) يحدد المركز كنقطة المرجعية وتكون قيم w و h هي عرض وارتفاع القطع الناقص ككل. RADIUS تأخذ المركز كالنقطة المرجعية، ولكن في هذا النمط تكون قيم w و h المحددة هي نصف العرض ونصف الارتفاع (كما في حالة نصف قطر الدائرة بدل القطر بالكامل) CORNER تحدد الزاوية اليسرى العلوية كنقطة مرجعية، وتكون قيم w و h هما قيمتي العرض والارتفاع. CORNERS تحدد المعاملين الأول والثاني (x و y) كموقع الزاوية اليسرى العلوية، والمعاملين الثالث والرابع (w و h) كموقع الزاوية اليسرى السفلية للقطع الناقص، لذا في هذا النمط قيم العرض و الطول غير مهمتان، ولفهم أفضل يمكن اعتبار الدالة بالشكل التالي ellipse(x_tl,y_tl,x_br,y_br) لتكون أكثر منطقية. الدالة rectMode(): تحدد مكان النقطة المرجعية لمحاذاة المستطيلات، وفيما يلي طريقة الاستخدام: rectMode(mode) يملك المعامل mode لديه المعاملات التالية : CENTER يحدد المركز كنقطة المرجعية للمستطيل وتكون قيم w و h المحددة هي عرض وارتفاع المستطيل. RADIUS تأخذ المركز كالنقطة المرجعية، ولكن في هذا النمط تكون قيم w و h المحددة هي نصف العرض ونصف الارتفاع. CORNER تحدد الزاوية اليسرى العلوية كنقطة مرجعية وقيم w و h المحددة هي عرض وارتفاع المستطيل هذه هي القيمة الافتراضية للمتغير. CORNERS تحدد المعاملين الأول والثاني (x و y) كموقع الزاوية اليسرى العلوية، والمعاملين الثالث والرابع (w و h) كموقع الزاوية اليسرى السفلية للمستطيل، لذا في هذا النمط تكون قيم العرض و الطول غير مهمين، ولفهم أفضل يمكن اعتبار الدالة بالشكل التالي rect(x_tl,y_tl,x_br,y_br) لتكون أكثر منطقية. إعدادات متعلقة بالنص الدالة textSize(): تحدد حجم الخط، وطريقة الاستخدام كالتالي: testSize(size) قيمة حجم الخط كعدد صحيح دالة الدالة textLeading(): تحدد ارتفاع الخط للنصوص، الاستخدام: الدالة textLeading(lineheight): تحدد قيمة الفراغ بين الأسطر مقدر بالبيكسل الدالة textAlign(): تحدد أين هي النقطة المرجعية لمحاذاة النصوص، وهذا مثال على طريقة الاستخدام: textAlign(alignX)// تستخدم alignX للمحاذاة الأفقية وتأخذ القيم LEFT,CENTER,RIGHT textAlign(alignX,alignY) //تستخدم alignY للمحاذاة الشاقولية وتأخذ القيم TOP,BOTTOM,CENTER,BASELINE التحريك تعلمنا حتى الآن كيفية رسم الكائنات والنصوص، ولكنها كلها كائنات ثابتة لا تتحرك؛ سنتعلم الآن تحريكها عن طريق إعطاء الإحداثيات قيم متغيرة بدلًا من أعداد صحيحة ثابتة ويمكننا زيادتها أو إنقاصها، لاحظ الشيفرة التالية // تهيئة x و y إلى القيمة 0 int x=0; int y=0; void setup(){ size(800,600); background(255); // ضبط لون الخلفية للون الأبيض } void draw(){ fill(255,0,0); // لون التعبئة أحمر stroke(0,0,255); // لون خط الحد هو الأزرق ellipseMode(CENTER); // نقطة المرجع هي المركز ellipse(x, y, 20, 20); // رسم القطع الناقص // زيادة x و y x+=5; y+=5; } هل تستطيع معرفة كيفية إدارة عملية التحريك؟ لقد ضبطنا قيم x و y كمتغيرات عامة بقيم أولية 0، وأنشأنا في حلقة الرسم القطع الناقص، ثم ضبطنا لون التعبئة للأحمر، ولون خط الحد للأزرق وضبطنا قيم الإحداثيات x و y، إذ تغير الكرة مكانها عندما نزيد قيمتي x و y ولكن هناك مشكلة في الشيفرة هل تستطيع معرفتها كاختبار لك؟ حاول تجربتها: الهدف هنا أن تعرف كيفية عمل الحلقات في لغة بروسيسنج، تذكر المثال الذي أوردناه في قسم كتلة الرسم في المقال السابق ولماذا حصلنا على "111…" بدلًا من "1234.." إنه السبب ذاته الذي جعل الكرة تترك أثرًا، حيث في كل مرة تكرّر الكرة تزداد قيمة x و y بقيمة 5 وتُرسم الكرة نحو اليمين والأسفل بمقدار 5 بيكسل، ولكن تبقى الكرة المرسومة من التكرار السابق، كيف نستطيع إزالتها؟ لإزالة أثر الكرة نزيل ببساطة background(255) من كتلة التهيئة ونضعها في أول سطر من كتلة الرسم، فعندما كانت شيفرة الخلفية في كتلة التهيئة نُفذت الشيفرة مرة واحدة في البداية لجعل الخلفية بيضاء، ولكن ذلك لا يكفي يجب إعادة ضبط الخلفية للأبيض في كل حلقة لتغطية الكرات المرسومة من الحلقات السابقة. بما أن الخلفية هي السطر الأول فهذا يعني تنفيذه أولًا وتصبح الطبقة الأساسية، وتصبح الخلفية بيضاء في كل حلقة وتُرسم العناصر عليها لذا لا يبقى أي أثر. تعتمد فكرة التحريك في لغة البرمجة بروسيسنج على التلاعب بالشكل وتعديله برمجيًا لتغيير مكانه، ولكن قد تتساءل كيف نستطيع عمل أشياء أكثر تعقيدًا مثل إبقاء الكرة على الشاشة أو تطبيق الجاذبية؟ هذا ما سنتعلمه في المقال التالي عن تطبيق عملي نجرب من خلاله إنشاء لعبة كاملة وقابلة للعب ومسليّة. التفاعل مع الفأرة ولوحة المفاتيح إن التفاعل مع الفأرة ولوحة المفاتيح أمر سهل في لغة بروسيسنج، إذ توجد توابع تُستدعى لكل حدث وما يُنفّذ كل ما يتم كتابته داخلها عند حصول الحدث، و هناك أيضًا متغيرات عامة مثلmousePressed و keyPressed يمكن استخدامها في كتلة الرسم للاستفادة من الحلقة، التالي هي بعض التوابع مع شرحها void setup() { size(500, 500); } void draw() { if (mousePressed) { // تُنفَّذ الشيفرات هنا طالما زر // الفأرة مضغوط if (mouseButton == LEFT){ // تُنفَّذ الأسطر طالما // زر الفأرة المنقور هو زر الفأرة // اليسار } } if (keyPressed) { // تُنفَّذ الشيفرات هنا طالما هناك مفتاح // مضغوط على لوحة المفاتيح if (key == CODED) { // تتحقق هذه تعليمة if هذه إذا كان المفتاح // مُتَعرف عليه من لغة بروسيسنج if (keyCode == ENTER) { // تُنفَّذ الأسطر إذا كان المفتاح // هو مفتاح enter } } else{ // تُنفَّذ هذه الأسطر إذا لم يتم التعرف // على السطر من لغة بروسيسنج } } } void mousePressed() { // تنفذ هذه الشيفرات مرة عندما تنقر الفأرة // لاحظ أن المتغير mouseButton // مستخدم هنا } void keyPressed() { // تنفذ هذه الشيفرات مرة عندما يضغط المفتاح // لاحظ أن المتغيرات key و keyCode // مستخدمة هنا } كما ترى من السهل التحقق إذا ما كانت الفأرة قد تم نقرها أو هناك زر تم الضغط عليه، ولكن هناك خيارات أكثر للمتغيرات mousePressed و keyCode، إن الخيارات المتاحة للمتغير mousePressed هي LEFT و RIGHT و CENTER، وهناك العديد من الخيارات المتاحة للمتغير keyCode مثل (UP و DOWN و LEFT و RIGHT و ALT و CONTROL و SHIFT و DELETE ...إلخ.) هناك أمر وحيد يجب معرفته في متغيرات الفأرة وهو كيفية الحصول على إحداثيات مؤشر الفأرة، وهو أمر سنستخدمه كثيرًا، فللحصول على إحداثيات المؤشر يمكنك استخدام المتغيرين mouseX و mouseY مباشرة في كتلة draw()، وأخيرًا وليس آخرًا هناك العديد من الدوال التي يجب أن تتطلع عليها موثقة في مراجع لغة بروسيسنج. الخاتمة يجب أن تصبح لغة بروسيسنج Processing مألوفة لك الآن، ولكن إذا توقفت هنا ستضيع كل المعلومات، من الأفضل لك أن تستمر في التدريب و تجرب الأشياء التي تعلمتها. إليك تمرينين لمساعدتك على التدرب، يجب أن تحاول أقصى جهدك في تنفيذهما بنفسك ويمكنك البحث عن الحل إذا وجدت الأمور صعبة. ستكون شيفرة حل أول تمرين متاحة، إلا أن الاطلاع عليها يجب أن تكون خيارك الأخير إذا لم تستطع التقدم. التمرين الأول عليم رسم 4 كرات بألوان مختلفة تبدأ من 4 زوايا الشاشة وتنتقل إلى الوسط بسرعات مختلفة، ويجب أن تتوقف الكرة عندما تنقر على أحد الكرات وتستمر في ضغط الزر، وعندما تزيل الضغط تعود الكرة إلى مكانها الأساسي وتستمر في الحركة، أي شيء كالتالي تحقق من الشيفرة هنا بعد تجربة هذا التمرين التمرين الثاني هل تذكر شاشة توقف DVD المشهورة التي يرتد بها شعار DVD حول الشاشة وننتظر بشوق أن تصطدم في الزاوية؟ أريد منك تكرار شاشة التوقف هذه باستخدام مستطيل بدلًا من شعار DVD، عند تشغيل التطبيق يجب أن تكون الشاشة سوداء وينطلق المستطيل من موقع عشوائي، وفي كل مرة يصطدم بزاوية يجب على المستطيل تغيير لونه (وبالطبع اتجاهه). وعند تحريك الفأرة يجب على المستطيل الاختفاء وأن تصبح الشاشة بيضاء، لن تحصل على الحل في هذه المقالة يجب عليك أن تحاول كل جهدك أن تنفذها وسنقدم الشيفرة في المقال التالي. ترجمة -بتصرف- لمقال Guide to the Processing Language Part I: Fundamentals لكاتبه Oguz Gelal. اقرأ أيضًا المقال السابق: دليلك للغة برمجة بروسيسينج Processing | الجزء الأول: الأساسيات المقال التالي: دليلك للغة برمجة بروسيسينج Processing | الجزء الثالث: بناء لعبة بسيطة كيف تصبح مبرمج ألعاب فيديو ناجح دليلك الشامل إلى برمجة الألعاب بناء لعبة ورق في جافا تعرف على أفضل برنامج تصميم الألعاب الإلكترونية
-
يٌعدَ إنشاء النماذج الأولية بسرعة وإنتاج نتائج بصرية سريعة من الميزات المهمة في العديد من لغات وأطر البرمجة. ومع ذلك، تتميز بعض اللغات بجعل هذه الإمكانيات من أهدافها الأساسية، مثل لغة البرمجة بروسيسنج Processing المبنية على جافا. تُتيح لغة Processing للمستخدمين كتابة الشيفرات ضمن بيئة تصميم رسومية، حيث تم تطويرها خصيصًا لتقديم نتائج بصرية فورية. في هذه المقالة، نقدم مقدمة بسيطة عن لغة البرمجة بروسيسنج وتقنياتها الفريدة." فإذا كنت تشعر بالملل وتريد أن تفرغ إبداعك في بناء مشروع مبهر بصريًا أو شيء فني، أو لربما تريد تعلم البرمجة وعمل شيء جميل بأسرع وقت؟ عليك بلغة برمجة بروسيسنج Processing، ويمكن القول أن هذه اللغة واحدة من أكثر اللغات متعة، فهي لغة مباشرة وسهلة التعلم والاستخدام بالإضافة لكونها أداة قوية، فاستخدامها يمنحك الشعور بأنك ترسم على لوحة بيضاء بأسطر برمجية، ولا يوجد قوانين صارمة تحد إبداعك، الحد الوحيد هنا هو خيالك. ما سنتعلمه في هذه السلسلة من المقالات هو التعرف على لغة البرمجة بروسيسنج Processing وتصميم ألعاب بسيطة باستخدامها. تتكون هذه المقالات من ثلاثة أقسام: في المقال الأول سنتحدث عن اللغة Processing ونوفر نظرة عامة عنها. في المقال الثاني نوفر دليلًا شاملًا للغة Processing وبعض النصائح حولها. في المقال الثالث سننشئ لعبة بسيطة خطوة بخطوة كما سنحوّل شيفرة اللعبة إلى لغة جافاسكريبت JavaScript باستخدام P5js لتعمل على متصفح الويب. ماذا يجب أن تعرفه أولًا يجب عليك أن تعرف مبادئ البرمجة الأساسية لفهم ومتابعة هذه المقالة بسهولة، لأننا لن نتحدث عنها، ولن نتحدث عن مفاهيم برمجية معقدة أيضًا لذا يجب أن يكون لديك معرفة بأساسيات البرمجة. وسنتطرق في بعض الأجزاء عن مفاهيم برمجية ذات مستوى منخفض low-level مثل مفهوم البرمجة كائنية التوجه Object Oriented Programming أو OOP اختصارًا، ولكن هذه المفاهيم ليست أساسية للعمل على برمجة اللعبة، إنما هي موجهة للقراء المهتمين ببنية اللغة البرمجية وبالتالي يمكنك تخطي هذه الأجزاء إذا لا ترغب بمعرفتها. عدا ذلك ستحتاج إلى حافز تعلم اللغة والشغف لبرمجة لعبة إلكترونية خاصة بك. كيفية المتابعة من الجيد دومًا التعلم عن طريق التجريب، فكلما تعمقت في لعبتك كلما تمكنت من إتقان لغة بروسيسنج Processing. لذا نقترح بداية بأن تعمل على تجريب كل خطوة في البيئة الخاصة بك. لدى لغة بروسيسنج Processing بيئة تطوير متكاملة (أي محرر شيفرة) سهل الاستخدام، إنه الشيء الوحيد الذي تحتاج لتنزيله وتثبيته للمتابعة. ويمكن تنزيله من هنا بعدها ستكون جاهزًا لتبدأ! ما هي لغة برمجة بروسيسنج Processing؟ يضم هذا القسم مقدمة تقنية مختصرة عن لغة البرمجة Processing وهيكليتها وبعض الملاحظات على عملية التصريف compiling والتنفيذ. ستتحدث التفاصيل عن معلومات متقدمة عن البرمجة وبيئة جافا. يمكنك تخطي قسم "أساسيات Processing" إذا كنت لا تريد معرفة التفاصيل البرمجية النظرية وترغب في البدء بشكل عملي في برمجة اللعبة الخاصة بك. إن لغة بروسيسنج Processing هي لغة برمجة بصرية تسمح لك بالرسم عن طريق الشيفرة، ولكنها ليست لغة برمجة بحد ذاتها، فهي تدعى "شبيهة جافا Java-esque" أي أنها مبنية على منصة جافا ولكنها ليست لغة جافا، وهي تُعالج الشيفرة وتحوَّل بكاملها إلى شيفرة جافا عند التنفيذ. إن صنف جافا PApplet هو الصنف الأساسي لكل رسومات بروسيسنج Processing. لنأخذ كتلتي شيفرة بروسيسنج Processing كمثال. public void setup() { // توضع شيفرات التهيئة هنا } public void draw() { // توضع شيفرات الرسم هنا } سوف تتحول كل من كتلتي الشيفرة في الأعلى إلى شيء يشبه التالي: public class ExampleFrame extends Frame { public ExampleFrame() { super("Embedded PApplet"); setLayout(new BorderLayout()); PApplet embed = new Embedded(); add(embed, BorderLayout.CENTER); embed.init(); } } public class Embedded extends PApplet { public void setup() { // توضع شيفرات التهيئة هنا } public void draw() { // توضع شيفرات الرسم هنا } } سترى أنه تم تغليف كتلة شيفرة بروسيسنج Processing بصنف ممتد extended class من PApplet الخاص بلغة جافا. لذا يجري التعامل مع كل الأصناف المعرفة في شيفرة بروسيسنج كأنها أصناف داخلية inner classes. إن معرفة أن بروسيسنج Processing مبنية من لغة جافا له العديد من الميزات خصوصًا إذا كنت مطور جافا، ليس فقط لكون الصياغة مألوفة، ولكن هذه المعرفة تمنحنا القدرة على تضمين شيفرة جافا والمكاتب وملفات JAR في رسوماتك واستخدام تطبيقات بروسيسنج Processing مباشرة من Eclipse إذا أردت أخذ بعض الوقت لتهيئته. شيء واحد لا تستطيع عمله هو استخدام مكونات AWT و Swing في رسومات بروسيسنج لأنها تتضارب مع طبيعة لغة بروسيسنج ولكن لا تقلق لن نتطرق لذلك في هذه المقالة. أساسيات بروسيسنج Processing تتألف شيفرة لغة بروسيسنج Processing من قسمين أساسيين، هما كتلتَي التهيئة والرسم. تُنفذ كتل التهيئة عندما تُنفَّذ الشيفرة وتستمر كتل الرسم بالتنفيذ دومًا. إن الفكرة الأساسية خلف بروسيسنج Processing هي تنفيذ ما يتم كتابته في كتلة الرسم 60 مرة في الثانية من الأعلى إلى الأسفل لحين إغلاق البرنامج. سنبني كل شيء اعتمادًا على هذه الفكرة، وسنحرك الكائنات ونعرف النتيجة ونتوقع الاصطدامات ونطبق الجاذبية ونعمل أي شيء باستخدام هذه الخاصية. حلقة التحديث هذه هي نبض المشروع. سنشرح كيفية الاستفادة من ذلك لجعل الشيفرة تنبض بالحياة في الأقسام اللاحقة، ولكن أولًا لنتحدث عن بيئة التطوير المتكاملة لبروسيسنج Processing. بيئة التطوير المتكاملة لبروسيسنج Processing IDE نزّل بيئة التطوير المتكاملة لبروسيسنج Processing إذا لم تفعل ذلك لحد الآن. سنحدد بعض المهام السهلة لتنفذها لوحدك خلال المقالة ويمكنك أن تجربها فقط إذا كانت لديك بيئة التطوير المتكاملة.القسم التالي هو مقدمة عن بيئة التطوير المتكاملة لبروسيسنج Processing وهي بسيطة ومفهومة لذا سنختصر الشرح حولها. يقوم زرَّا "إيقاف Stop" و"تنفيذ Run" بما تتوقعه، فعند نقر زر "تنفيذ Run" تُصرَّف الشيفرة وتُنفَّذ. وتعمل برامج بروسيسنج Processing دون توقف إذا لم تُقاطِع تنفيذها. ويمكنك إنهاء التنفيذ برمجيًا أو يمكنك استخدام زر "إيقاف Stop". الزر الذي يشبه الفراشة علي يمين زر "إيقاف Stop" و "تنفيذ Run" هو المُنقّح Debugger، واستخدام المُنقِّح يحتاج لمقالة خاصة وهو خارج نطاق هذه المقالة ويمكن تجاهله الآن. أما اللائحة المنسدلة بجانب زر المُنقح فهي تستخدم عند إضافة وتهيئة الأنماط، إذ تقدم الأنماط Mods بعض الوظائف وتسمح لك بكتابة شيفرة لأندرويد وبايثون وما إلى ذلك. إن الأنماط هي أيضًا خارج نطاق المقالة ويمكنك تجاهلها أيضًا. النافذة في محرر الشيفرة هي مكان تنفيذ الرسوم، وهي سوداء في الصورة السابقة لأننا لم نحدد بعد خاصية مثل الحجم أو لون الخلفية ولم نرسم أي شيء. لا يوجد شيء آخر للتحدث عنه في محرر الشيفرة إنه ببساطة مكان لكتابة الشيفرة. وهناك عدد أسطر -لم تحوي إصدارات بروسيسنج Processing السابقة عليه- الصندوق الأسود السفلي هو الطرفية Console، سنستخدمها لطباعة الأشياء لتنقيح الأخطاء بسرعة. تظهر تبويبة الأخطاء Errors بجانب الطرفية الأخطاء عند ظهورها. هذه ميزة مفيدة تأتي مع إصدار بروسيسنج Processing 3.0. فقد كانت تُطبع الأخطاء في الإصدارت السابقة إلى الطرفية وكانت هناك صعوبة في تتبعها. كتلة التهيئة Setup Block تُنفَّذ كتلة التهيئة كما تحدثنا سابقًا عند تنفيذ البرنامج، ويمكن استخدامها للضبط ولأشياء تريد تنفيذها مرة واحدة مثل تحميل الصور والأصوات. فيما يلي مثال يوضح كتلة تهيئة، نفذ الشيفرة في البيئة الخاصة بك وشاهد النتائج بنفسك: public void setup() { // حجم المشروع هو 600x800 // واستخدم محرك التصيير P2D size(800, 600, P2D); // يمكن استخدام هذه الدالة بدلًا من size() // fullScreen(P2D); // سيكون لون خلفية المشروع هو الأسود //افتراضيًا، إذا لم يحدد غير ذلك background(0); // يمكن استخدام هذه لضبط صورة الخلفية //يجب أن يكون حجم المشروع بحجم الصورة // background(loadImage("test.jpg")); // يتم تعبئة الأشكال والكائنات باللون الأحمر افتراضيًا // مالم يُشار عكس ذلك fill(255,0,0); // يكون للأشكال والكائنات حد أبيض افتراضيًا // مالم يُشار عكس ذلك stroke(255); } سنشرح التوابع المتعلقة بالتصميم (مثل الخلفية والتعبئة وثخانة الخط) في المقال التالي ما تحتاج معرفته الآن هو كيفية تأثير الضبط والإعدادات على المشروع بأكمله بالإضافة لفهم التوابع التالية. تستخدم الدالةsize() -كما يشير اسمها- لضبط حجم الرسم. يجب أن تكون في أول كتلة شيفرة التهيئة، ويمكن استخدامها بالطرق التالية: size(width,height); size(width, height, renderer); يمكن تحديد عرض وارتفاع القيم بالبيكسل. وتقبل دوال الحجم معامل أو وسيط ثالث هو المُصيّر أو العارض renderer المستخدم لتحديد أي محرك تصيير سيتم استخدامه. والمصير الافتراضي هو P2D. والمُصيّرات المتوافرة هي P2D (للمعالجة ثنائية البعد) و P3D (للمعالجة ثلاثية البعد، ويجب استخدامه إذا تضمنت الرسومات رسوميات ثلاثية البعد) و PDF (حيث ترسم الرسوميات ثنائية البعد مباشرة في ملف PDF). لمزيد من المعلومات انقر هنا . ستستخدم كل من P2D و P3D العتاد الرسومي المتوافق مع OpenGL. يمكن استخدام دالة الشاشة الكاملة ()fullScreen بدلًا من دالة size() من إصدار بروسيسنج Processing 3.0 ، وكما في دالة size() يجب أن تكون في أول سطر من كتلة التهيئة أيضًا ويكون استخدامها كالتالي: fullScreen(); fullScreen(display); fullScreen(renderer); fullScreen(display, renderer); إذا استخدمتها بدون أي معامل سيكون رسم بروسيسنج Processing على الشاشة كاملة وسيُنفذ على شاشتك الأساسية. يشير معامل display للإشارة إلى الشاشة التي سيُنفذ الرسم عليها. مثلًا إذا وصلت شاشات خارجية لحاسوبك يمكنك ضبط متغير الإظهار إلى 2 (أو 3 أو 4 إلخ.) وسيُنفذ الرسم هناك. تم شرح معامل renderer في قسم size() في الأعلى. كتلة الإعدادات Settings Block أضيفت هذه الميزة في إصدارات بروسيسنج Processing الجديدة، وهي كتلة شيفرة مثل التهيئة والرسم وتفيد عند تعريف توابع size() و fullScreen() بمعاملات متغايرة. من الضروري أيضًا تعريف size() وخصائص التصميم الأخرى مثل smooth() في كتلة الشيفرة هذه إذا كنت تستخدم بيئات غير بيئة التطوير المتكاملة الخاصة ببروسيسنج Processing مثل Eclipse، ولكن لن تحتاجه في معظم الحالات وبالتأكيد ليس في هذه المقالة. كتلة الرسم Draw Block لا يوجد شيء مميز للتحدث عن كتلة الرسم إلا أنها نبض المشروع، فكل شيء يحصل فيها، فهي قلب البرنامج الذي ينبض 60 مرة في الثانية. تحوي كتلة الشيفرة هذه كل منطق الشيفرة فكافة الأشكال والكائنات..إلخ مكتوبة هنا. إن كل الشيفرة التي سنتحدث عنها في المقالة ستكون في كتلة الرسم لذا من الضروري أن تفهم كيفية عمل هذه الكتلة. لنقدم عرض عن ذلك. جرب التالي: لاحظ أننا يمكن أن نطبع أي شيء في الطرفية باستخدام التابعَين print() أو println(). تطبع دوال الطباعة إلى الطرفية لكن println تطبع وتبدأ بسطر جديد في النهاية لذا تُطبع كل println() في سطر مختلف. شاهد كتلة الشيفرة التالية وحاول أن تعرف ما سيُطبع في الطرفية: void setup(){ } void draw(){ int x = 0; x += 1; print(x+" "); } إذا قلت "1234…."، فهذا جواب متوقع، ولكنه للأسف جوال خاطئ! هذه واحدة من أوجه سوء الفهم في بروسيسنج processing. تذكر أن الكتلة تُنفَّذ بشكل متكرر. فعندما عرّفت المتغير هنا فسيُعرَّف في كل حلقة مرة تلو مرة. وفي كل تكرار قيمة x هي 0 وتزداد بمقدار 1 ويتم طباعتها في الطرفية لذا ستكون النتيجة "111…" هذا المثال واضح نوعًا ما، ولكن قد يكون أكثر صعوبة عندما تتعقد الأمور. لا نريد أن يُعاد تعريف x لذا كيف يمكننا تحقيق ذلك وأن نحصل على النتيجة "1234…"؟ الجواب هو عن طريق استخدام المتغيرات العامة global variables. لذا نعرف المتغير خارج كتلة الرسم كي لا يُعاد تعريفه في كل تكرار، أيضًا سيكون نطاق المتغير قابل للوصول داخل مشروعك. لاحظ الشيفرة التالية: int x = 0; void setup(){ } void draw(){ x += 1; print(x+" "); } ربما تتساءل كيف يمكن أن يعمل متغير معرف خارج النطاق؟ ولماذا لم نستخدم كتلة setup() طالما تُنفَّذ مرة في البداية؟ إن الجواب متعلق بالبرمجة كائنية التوجه والنطاق scope. تذكر كيف شرحنا أن شيفرة بروسيسنج processing تتحول إلى جافا وكيف تُغلف بصنف جافا؟ إن المتغيرات المكتوبة خارج كتل setup() و draw() تُغلَّف أيضًا. واستخدام x+=1 هو نفس استخدام this.x+=1، ويؤدي الغرض ذاته في حالتنا، لا يُعرَّف متغير باسم x في نطاق draw() ويتم البحث في النطاق الخارجي الذي هو نطاق this. وسبب عدم تعريفنا المتغير x في قسم setup() هو: إذا فعلنا بذلك سيُعرف نطاق x في نطاق دالة setup ولن تستطيع كتلة draw() الوصول إليه. الخلاصة تعرّفنا في الجزء الأول من هذه السلسلة على لغة بروسيسنج وميزاتها، ومن ثم بيئة التطوير وهيكل البرنامج فيها. سنتعرّف في الجزء القادم من السلسلة على التوابع المُستخدمة في الرسم وعلى معاملاتها المختلفة. ترجمة -بتصرف- لمقال Guide to the Processing Language Part I: Fundamentals لكاتبه Oguz Gelal. اقرأ أيضًا المقال التالي: دليلك للغة برمجة Processing | الجزء الثاني: الرسم والتفاعل مع دخل المستخدم تعرف على أهمية صناعة الألعاب الإلكترونية كيف تصبح مبرمج ألعاب فيديو ناجح دليلك الشامل إلى برمجة الألعاب بناء تطبيقات كاملة باستعمال مكتبة جافا إف إكس JavaFX بناء لعبة ورق في جافا
-
سنتحدث في هذا الدرس عن الإشارات Signals في محرك الألعاب جودو، التي هي عبارة عن رسائل تصدرها العقد عندما يحصل داخلها شيء أو حدث معين، مثل حدث الضغط على أحد الأزرار عندها يطلق هذا الزر إشارة، ويمكن أن تتصل عقد أخرى مع هذه الإشارة وتستدعي دالة كي تنفذ عند حصول الحدث. تعد الإشارات طريقة للتفويض delegation مضمّنة في محرك الألعاب جودو فهي تسمح لكائن ما بالتفاعل لتغيير كائن آخر دون الحاجة إلى أن تكون الكائنات متصلة ببعضها البعض مباشرة، مما يقلل من الترابط بين الكائنات ويبقى الكود مرنًا. مثلًا قد يكون لديك شريط حياة health bar يمثل صحة اللاعب، تريد أن يتغير شريط الحياة عندما يتضرر اللاعب أو يستخدم عقار لزيادة الصحة، يجب استخدام الإشارات للقيام بذلك. ملاحظة: الإشارات في جودو هي تطبيق لنمط المراقب Observer Pattern في البرمجة وهو نمط تصميم يُستخدم لتمكين كائن (المراقب) من الاستجابة لتغييرات تحدث في كائن آخر دون أن يكون هناك ارتباط مباشر بينهما لتقليل الترابط بين الأجزاء المختلفة من البرنامج وتسهيل إدارة التغييرات والتحديثات بشكل أكثر مرونة.، يمكنك التعلم المزيد عنه هنا وفي الفقرات التالية سنستخدم الإشارات لجعل أيقونة جودو التي حركناها في الدرس السابق تتحرك وتتوقف عن طريق ضغط الأزرار. إعداد المشهد لإضافة زر للعبة يجب إنشاء مشهد أساسي جديد يضم كلًّا من الزر والمشهد sprite_2d.tscn الذي أنشأناه في درس كتابة أول كود برمجي خاص بك في جودو. أنشئ مشهد جديد في جودو عن طريق الذهاب إلى قائمة "مشهد" Scene ومن ثم اختر "مشهد جديد" New Scene في قائمة "المشهد" Scene انقر زر 2D الذي سيضيف عقدة Node2D كجذر للمشهد. انقر واسحب ملف sprite_2d.tscn المحفوظ سابقًا في Node2D في قائمة FileSystem لاستنساخه. انقر بالزر الأيمن للفأرة على Node2D واختر إضافة عقدة لإضافة عقدة أخرى Sprite2D كشقيق أي في نفس المستوى العقدة المحددة. ابحث عن عقدة الزر Button وأضفها كما يلي: تكون هذه العقدة صغيرة افتراضيًا، انقر واسحب المقبض من الزاوية اليمينية السفلى للزر في مجال العرض لتغيير حجمها إذا لم تظهر لك المقابض تأكد من أن أداة الاختيار مفعلة في شريط الأدوات. انقر على الزر واسحبه لتقريبه أكثر من الأيقونة. يمكنك أيضًا كتابة عنوان للزر عن طريق تعديل خاصية النص الخاصة به في الفاحص Inspector، وادخل على Toggle motion. يجب أن تكون شجرة المشهد وإطار الرؤية على النحو التالي: احفظ المشهد باسم node_2d.tscn إن لم تقم بذلك حتى الآن، يمكنك تشغيله عن طريق F6 (أو Cmd+R على macOS)، إن الزر سيكون واضح، ولكن لن يحصل شيء إذا ضغطته. ربط الإشارة بالمحرر نريد ربط إشارة ضغط الزر مع Sprite2D الخاص بنا، ونريد استدعاء دالة جديدة تفعّل أو تبطل حركته، إذ نحن بحاجة شيفرة برمجية مرتبط مع عقدة Sprite2D الذي قمنا به في الدرس السابق. يمكن ربط الإشارات في قائمة العقدة عن طريق اختيار زر العقدة على يمين المحرر والنقر على التبويبة المسماة "عقدة Node" بجانب "الفاحص Inspector". تظهر القائمة عدد من الإشارات المتوافرة للعقدة المختارة انقر نقرة مزدوجة على إشارة "مضغوط pressed" لفتح نافذة ارتباط العقدة يمكنك هنا ربط الإشارة مع عقدة Sprite2D التي تحتاج إلى تابع مستقبل أي إلى دالة تستدعيها جودو عندما يصدر الزر الإشارة، وسينشئ المحرر هذه الدالة لك تلقائيًا، تسمى هذه العقد اصطلاحًا "onnodenamesignalname" وقد أسميناها هنا "onbuttonpressed". ملاحظة: يمكننا استخدام نمطين عند ربط الإشارات باستخدام قائمة محرر العقدة، يسمح لك النمط الأول بربط العقد التي لديها أكواد برمجية مرتبطة معها ويُنشئ تلقائيًا دالة رد نداء عليهم كي تُنفذ عندما يتم إرسال الإشارة وبالتالي يمكنك تحديد ما يجب فعله عند استلام الإشارة من خلال كتابة الأكواد داخل دالة رد النداء هذه. ويتيح لك نمط المشاهدة المتقدمة الذي يوفره جودو إعدادات أكثر تفصيلًا عند ربط الإشارات، بما يتجاوز الخيارات الأساسية المتاحة في الوضع الافتراضي، ويمكِّنك من إجراء الربط مع أي عقدة ودالة مضمّنة وإضافة الوسائط للاستدعاء وضبط كافة الخيارات، يمكنك تفعيل هذا النمط في أسفل ويمين النافذة عن طريق النقر على مفتاح التبديل Advanced كما في الصورة السابقة لتحديد الدالة المخصصة في الكود يدويًا والتي تريد أن يتم تنفيذها عند استلام الإشارة. انقر على زر الربط لإنهاء ارتباط الإشارة والانتقال إلى مكان عمل البرنامج النصي، يجب أن ترى تابعًا جديدًا مع أيقونة الإشارة على الهامش الأيسر. إذا نقرت على الأيقونة مُظهرة معلومات عن الارتباطستظهر نافذة منبثقة تعرض تفاصيل حول كيفية ارتباط الإشارة بالعقدة والدالة المستدعاة والخيارات الإضافية، وهذه الميزة موجودة فقط عند ربط العقد في المحرر. لنستبدل السطر الذي يتضمن الكلمة المفتاحية pass في الشيفرة بشيفرة فعلية تنفذ حركة العقدة. ستتحرك Sprite2D بفضل الشيفرة في دالة _process()، إذ تقدم جودو تابعًا مضمنًا لتفعيل أو تعطيل المعالجة ويمكنك تفعيل أو تعطيل المعالجة المستمرة للدالة _process() على عقدة معينة فإذا مررت true للتابع Node.set_process() سينفذ _process() في كل إطار مما يسمح لك بتحديث الموقع أو الحالة بمرور الوقت؛ وإذا مررت false فلن تنفذ وستتوقف التحديثات المستمرة للعقدة. يفيد تابع آخر لصنف العقدة is_processing() في التحقق مما إذا كانت المعالجة المستمرة المنفذة في الدالة _process() مفعلة أم لا حيث تعيد القيمة true إذا كانت المعالجة الخاصة بالعقدة مفعلة والدالة _process() تنفذ في كل إطار، وتعيد false إذا كانت معالجة العقدة غير نشطة، ويمكننا استخدام الكلمة المفتاحية not لعكس القيمة. بلغة GDScript: func _on_button_pressed(): set_process(not is_processing()) بلغة C#: private void OnButtonPressed() { SetProcess(!IsProcessing()); } تفعل هذه السيفرة دالة المعالجة والتي بدورها تفعل أو تعطل حركة الأيقونة عند ضغط الزر. نحتاج قبل تجربة اللعبة لتبسيط دالة _Process() لنقل العقدة تلقائيًا والانتظار لمدخلات المستخدم، استبدلها بالشيفرة التالية التي شاهدناها منذ درسين سابقين. بلغة GDScript: func _process(delta): rotation += angular_speed * delta var velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta بلغة C#: public override void _Process(double delta) { Rotation += _angularSpeed * (float)delta; var velocity = Vector2.Up.Rotated(Rotation) * _speed; Position += velocity * (float)delta; } يجب أن تكون شيفرة sprite_2d.gd كالتالي: بلغة GDScript: extends Sprite2D var speed = 400 var angular_speed = PI func _process(delta): rotation += angular_speed * delta var velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta func _on_button_pressed(): set_process(not is_processing()) بلغة C#: using Godot; public partial class MySprite2D : Sprite2D { private float _speed = 400; private float _angularSpeed = Mathf.Pi; public override void _Process(double delta) { Rotation += _angularSpeed * (float)delta; var velocity = Vector2.Up.Rotated(Rotation) * _speed; Position += velocity * (float)delta; } private void OnButtonPressed() { SetProcess(!IsProcessing()); } } شغل المشهد الآن وانقر على الزر لترى كيفية تحرك وتوقف الأيقونة مع النقر. ربط الإشارة باستخدام الشيفرة يمكنك ربط الإشارات باستخدام الشيفرة بدلًا من استخدام المحرر، هذا ضروري عند إنشاء العقد أو استنساخ المشاهد دخل البرنامج النصي. لنستخدم عقدة مختلفة، إن لجودو عقدة مؤقت زمني Timer تفيد في تحديد أوقات المهارات أو أوقات إعادة تلقيم الأسلحة النارية في الألعاب. عد إلى مساحة العمل عن طريق النقر على "2D" أعلى الشاشة أو الضغط على Ctrl+F1 (أو Ctrl+Cmd+1على macOS) انقر بالزر الأيمن على عقدة Sprite2D في قائمة "المشهد" Scene وأضف عقدة ابن جديدة، وابحث عن المؤقت Timer وأضف العقدة المرافقة، يجب أن يكون المشهد كالتالي اذهب إلى "الفاحص" Inspector بعد اختيار عقدة المؤقت لتفعيل خاصية البدء التلقائي. انقر على أيقونة السكريبت بجانب Sprite2D للانتقال إلى مكان عمل البرنامج النصي سنحتاج إلى إجراء عمليتين لربط العقد عن طريق الشيفرة البرمجية: الحصول على مرجع من المؤقت إلى Sprite2D استدعاء التابع connect() على إشارة المؤقت "timeout" ملاحظة: تحتاج استدعاء تابع connect() للإشارة التي تريد أن تستمع إليها لربط الإشارة باستخدام الشيفرة، وفي حالتنا نريد الاستماع لإشارة "timeout" الخاصة بالمؤقت. نريد ربط الإشارة عند استنساخ المشهد، ويمكنن القيام بذلك باستخدام الدالة المضمّنة Node._ready() التي يتم استدعائها تلقائيًا عن طريق المحرك عندما يتم استنساخ المشهد بالكامل. نستخدم الدالة Node.get_node() للحصول على مرجع بالنسبة للعقدة الحالية، ويمكننا تخزين المرجع في متغير. بلغة GDScript: func _ready(): var timer = get_node("Timer") بلغة C#: public override void _Ready() { var timer = GetNode<Timer>("Timer"); } تنظر الدالة get_node() إلى أبناء Sprite2D وتحصل على العقد بأسمائها، مثلًا إذا أعدت تسمية عقدة المؤقت إلى "BlinkingTimer" في المحرر، فيجب عليك تغيير الاستدعاء إلى get_node("BlinkingTimer") يمكننا الآن ربط المؤقت إلى Sprite2D في دالة _ready() بلغة GDScript: func _ready(): var timer = get_node("Timer") timer.timeout.connect(_on_timer_timeout) بلغة C#: public override void _Ready() { var timer = GetNode<Timer>("Timer"); timer.Timeout += OnTimerTimeout; } يُقرأ السطر كالتالي: ربطنا إشارة المؤقت "timeout" للعقدة التي يرتبط بها السكربت، وعندما يصدر المؤقت timeout نريد استدعاء الدالة _on_timer_timeout() التي نحتاج لتعريفها، لنضف ذلك إلى أسفل الشيفرة البرمجية ونستخدمه لتفعيل شفافية الأيقونة. ملاحظة: تسمى دالة رد النداء callback اصطلاحًا باسم GDScript "OnNodeNameSignalName" وستكون هنا باسم "_on_timer_timeout" في كود GDScript وباسم "()OnTimerTimeout" في كود #C. بلغة GDScript: func _on_timer_timeout(): visible = not visible بلغة C#: private void OnTimerTimeout() { Visible = !Visible; } إن الخاصية visible بوليانية وتتحكم بشفافية العقدة، يتفعل السطر visible = not visible وإذا كانت القيمة visible هي true تصبح false والعكس صحيح سترى الأيقونة الآن تظهر وتختفي كل ثانية إذا شغلت المشهد البرنامج النصي الكامل لقد انتهينا من أيقونة جودو المتحركة التي تومض والملف التالي هو ملف sprite_2d.gd كاملًا كمرجع. بلغة GDScript: extends Sprite2D var speed = 400 var angular_speed = PI func _ready(): var timer = get_node("Timer") timer.timeout.connect(_on_timer_timeout) func _process(delta): rotation += angular_speed * delta var velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta func _on_button_pressed(): set_process(not is_processing()) func _on_timer_timeout(): visible = not visible بلغة C#: using Godot; public partial class MySprite2D : Sprite2D { private float _speed = 400; private float _angularSpeed = Mathf.Pi; public override void _Ready() { var timer = GetNode<Timer>("Timer"); timer.Timeout += OnTimerTimeout; } public override void _Process(double delta) { Rotation += _angularSpeed * (float)delta; var velocity = Vector2.Up.Rotated(Rotation) * _speed; Position += velocity * (float)delta; } private void OnButtonPressed() { SetProcess(!IsProcessing()); } private void OnTimerTimeout() { Visible = !Visible; } } الإشارات المخصصة هذا القسم هو مرجع لكيفية تعريف واستخدام الإشارات الخاصة بك، ولا يبني على المشروع المُنشئ في الدروس السابقة يمكنك تعريف إشارات مخصصة في برنامج نصي. مثلًا تريد أن تظهر شاشة "انتهت اللعبة" عندما تصل حياة اللاعب للصفر، يمكنك تعريف إشارة اسمها "died" أو "health_depleted" للقيام بذلك عندما تصل حياتهم للصفر. إليك الشيفرة بلغة GDScript: extends Node2D signal health_depleted var health = 10 وبلغة C#: using Godot; public partial class MyNode2D : Node2D { [Signal] public delegate void HealthDepletedEventHandler(); private int _health = 10; } ملاحظة: تمثل الإشارات أحداثًا حصلت للتو، ويمكننا استخدام أفعال بالزمن الماضي في تسميتها. تعمل إشاراتك الخاصة مثل تلك المضمّنة في محرك جودو أي تظهر في تبويبة العقد ويمكنك ربطها بنفس الطريقة لِبَثّ إشارة في برنامج النصي تحتاج لاستدعاءemit() على الإشارة بلغة GDScript: func take_damage(amount): health -= amount if health <= 0: health_depleted.emit() بلغة C#: public void TakeDamage(int amount) { _health -= amount; if (_health <= 0) { EmitSignal(SignalName.HealthDepleted); } } ويمكن أن تصرح الإشارة على وسيط واحد أو أكثر من خلال تحديد أسماء الوسطاء ما بين قوسين. بلغة GDScript: extends Node signal health_changed(old_value, new_value) var health = 10 بلغة C#: using Godot; public partial class MyNode : Node { [Signal] public delegate void HealthChangedEventHandler(int oldValue, int newValue); private int _health = 10; } ملاحظة: تظهر وسائط الإشارة في قائمة محرر العقد، وتستخدمها جودو لإنشاء رد نداء callback خاص بك، ولكن يمكنك إرسال أي عدد من الوسائط عند بث الإشارة، لذا يجب عليك بث القيم الصحيحة. لبث القيم باستخدام الإشارة أضف بعض الوسائط الإضافية إلى الدالة emit() بلغة GDScript: func take_damage(amount): var old_health = health health -= amount health_changed.emit(old_health, health) بلغة C#: public void TakeDamage(int amount) { int oldHealth = _health; _health -= amount; EmitSignal(SignalName.HealthChanged, oldHealth, _health); } الخلاصة تبث أي عقدة في جودو إشارة عندما يحصل فيها شيء معين مثل الضغط على زر، ويمكن للعقد الأخرى أن ترتبط مع إشارات وتتفاعل مع أحداث مختارة. هناك العديد من الاستخدامات للإشارات حيث يمكنك التفاعل مع العقد الداخلة إلى أو الخارجة من عالم اللعبة أو مع الاصطدامات أو دخول أو خروج شخص من منطقة أو مع تغير حجم عنصر من الواجهة والعديد من ذلك على سبيل المثال إذا كان هناك عنصر في اللعبة (مثل قطعة نقدية) يمكن للاعب التقاطها أو جمعها ممثلة بعقدة Area2D فإنها ستصدر أو تَبُثّ إشارة body_entered عندما يدخل جسم اللاعب شكل الاصطدام الخاص بها مما يسمح لك بمعرفة إذا ما تم التقطها من قبل اللاعب أم لا. سنتعلم في القسم التالي كيفية إنشاء لعبة ثنائية الأبعاد كاملة في محرك ألعاب جودو، ونطبق كل ما تعلمناه من مفاهيم على أرض الواقع. ترجمة - وبتصرف - لقسم Using signals من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: الاستماع لمدخلات اللاعب في جودو Godot ألعاب الفيديو: تطورها وأهميتها وخطوات برمجتها لغات البرمجة المتاحة في جودو Godot تعرف على أشهر محركات الألعاب Game Engines
-
نشرح في مقال اليوم كيفية إضافة ميزة مهمة لأي لعبة بناء على المقال السابق الذي أنشأنا فيه سكربت للعبة بسيطة لتحريك أيقونة جودو Godot في مسارات دائرية، ألا وهي إعطاء التحكم بالحركة للاعب، ولذلك نحتاج لتعديل الكود البرمجي الذي كتبناة في ملف sprite_2d.gd. لدينا أداتان رئيسيتان لمعالجة مدخلات اللاعب في جودو هما: دوال رد نداء الدخل المضمّنة في جودو built-in input callbacks، وبالأخص الدالة _unhandled_input()_ التي تستدعى لمعالجة الأحداث التي لم يتم التعامل معها بواسطة العقد الأخرى والدالة _process() وهي دالة افتراضية مضمنّة تستدعيها جودو كل مرة يضغط فيها اللاعب على مفتاح وتُستخدم للتفاعل مع الأحداث التي لا تحصل في كل إطار بشكل مستمر مثل الضغط على Space للقفز، راجع استخدام InputEvent للمزيد عن استرجاع استدعاءات المدخلات الكائن المتفرّد Input، إن الكائنات المتفردة Singelton هي عبارة عن كائنات يمكن الوصول إليها بشكل عام، وتقدم جودو الوصول للعديد منها في السكربتات، إنها الأداة الأفضل لتفقد المدخلات في كل إطار. سنستخدم الكائنInput لأننا نريد أن نعرف إذا ما كان يريد اللاعب التحرك أو الدوران في كل إطار. يجب استخدام المتغير الجديد direction من أجل الدوران، استبدل السطر rotation += angular_speed * delta في دالة _process() بالشيفرة التالية: بلغة GDScript: var direction = 0 if Input.is_action_pressed("ui_left"): direction = -1 if Input.is_action_pressed("ui_right"): direction = 1 rotation += angular_speed * direction * delta بلغة C#: var direction = 0; if (Input.IsActionPressed("ui_left")) { direction = -1; } if (Input.IsActionPressed("ui_right")) { direction = 1; } Rotation += _angularSpeed * direction * (float)delta; إن المتغير المحلي direction هو مُضَاعف multiplier يمثل الاتجاه الذي يريد اللاعب الدوران نحوه وزيادته تضخّم مقدار الدوران، وتمثل القيمة 0 أن اللاعب لم يضغط على مفتاح السهم اليميني أو اليساري، والقيمة 1 تعني أن اللاعب يريد الدوران نحو اليمين، و-1 تعني أنه يريد الدوران نحو اليسار. يجب التصريح عن مجموعة شروط واستخدام Input لإنتاج هذه القيم، نبدأ الشرط بالكلمة المفتاحية if في GDScript وننهيها بنقطتين، ويكون الشرط هو التعبير بين الكلمة المفتاحية ونهاية السطر. نستدعي Input.is_action_pressed() للتحقق فيما إذا كان المفتاح مضغوطًا ضمن هذا الإطار، إذ يأخذ التابع سلسلة نصية تمثل المدخلات ويعيد true إذا كان المفتاح قد ضُغط وإلا يعيد false. إن الفعلين المستخدمين سابقًا "uileft" و "uiright" مُعرفين مسبقًا في جودو، إذ يُفعّلان عندما يضغط اللاعب السهمين اليميني واليساري على لوحة المفاتيح أو الزرين اليمين واليسار على قبضة التحكم. ملاحظة: يمكن مشاهدة وتعديل المدخلات في المشروع الخاص بك بالذهاب إلى "إعدادات المشروع Project settings" والنقر على تبويبة "خريطة الإدخال Input Map". أخيرًا نستخدم direction كمُضاعف عند تحديث زاوية الدوران rotation الخاص بالعقدة: rotation += angular_speed * direction * delta يجب أن تتحرك الأيقونة عند الضغط على Left و Right عند تشغيل المشهد بهذه الشيفرة. التحرك عند الضغط على Up نحتاج لتعديل الشيفرة التي تحسب السرعة velocity من أجل التحرك عند الضغط فقط، بدّل السطر الذي يبدأ بـ var velocity بالشيفرة التالية: بلغة GDScript: var velocity = Vector2.ZERO if Input.is_action_pressed("ui_up"): velocity = Vector2.UP.rotated(rotation) * speed بلغة C#: var velocity = Vector2.Zero; if (Input.IsActionPressed("ui_up")) { velocity = Vector2.Up.Rotated(Rotation) * _speed; } هيئنا متجه السرعة velocity بالقيمة Vector2.ZERO وهو ثابت مضمّن في نوع Vector يمثل متجه ثنائي الأبعاد بطول 0. إذا ضغط اللاعب "ui_up" نحدث قيمة السرعة وتتحرك الشخصية إلى الأمام. البرنامج الكامل التالي هو الملف الكامل sprite_2d.gd كمرجع. بلغة GDScript: extends Sprite2D var speed = 400 var angular_speed = PI func _process(delta): var direction = 0 if Input.is_action_pressed("ui_left"): direction = -1 if Input.is_action_pressed("ui_right"): direction = 1 rotation += angular_speed * direction * delta var velocity = Vector2.ZERO if Input.is_action_pressed("ui_up"): velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta بلغة C#: using Godot; public partial class MySprite2D : Sprite2D { private float _speed = 400; private float _angularSpeed = Mathf.Pi; public override void _Process(double delta) { var direction = 0; if (Input.IsActionPressed("ui_left")) { direction = -1; } if (Input.IsActionPressed("ui_right")) { direction = 1; } Rotation += _angularSpeed * direction * (float)delta; var velocity = Vector2.Zero; if (Input.IsActionPressed("ui_up")) { velocity = Vector2.Up.Rotated(Rotation) * _speed; } Position += velocity * (float)delta; } } تستطيع الآن الدوران باستخدام الأسهم يمين ويسار والتحرك للأمام عن طريق ضغط Up إذا شغّلت المشهد. الخلاصة يمثل كل برنامج نصي في جودو صنفًا ويتوسع في الأصناف المضمّنة، إن أنواع العقد التي ترث منها الأصناف الخاص بك تعطيك وصولًا إلى خاصيًات مثل rotation و position كما في حالتنا، ويمكن أيضًا وراثة العديد من الدوال التي لم تُذكر في هذا المثال. إن المتغيرات التي تضعها في أول الملف بلغة GDScript هي خاصيات الصنف، التي تدعى متغيرات الأعضاء، ويمكن تعريف الدوال التي ستكون في أغلب الأحوال دوال الأصناف الخاصة بك. تقدم جودو العديد من التوابع الافتراضية التي يمكن تعريفها لتتصل مع الأصناف الخاصة بك مع المحرك، وتضم الدالة_process() لتطبيق التغييرات للعقدة في كل إطار، والدالة unhandled_input()_ لاستقبال المدخلات مثل ضغط المفاتيح أو الأزرار من المستخدم، وهناك المزيد أيضًا. كما يسمح الصنف المتفرد Input بالتفاعل مع إدخالات اللاعب في أي مكان من الشيفرة الخاصة بك، وستستخدمها بالتحديد في حلقة process()_ سنتعلم في الدرس التالي مفهوم الإشارات signals في محرك جودو، وتتمكن من بناء علاقات بين البرامج النصية والعقد عن طريق تشغيل العقد للشيفرات في السكريبتات بشكل يجعل الكود أكثر تنظيمًا وأسهل في الصيانة. ترجمة - وبتصرف - لقسم Listening to player input من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: كتابة برنامجك الأول باستخدام جودو Godot مدخل إلى محرك الألعاب جودو Godot العقد Nodes والمشاهد Scenes في جودو Godot إعداد محرك الألعاب جودو Godot للعمل مع قاعدة البيانات SQLite
-
في هذا المقال ستبرمج لعبتك الأولى لتحريك أيقونة جودو Godot في مسارات دائرية باستخدام لغة البرمجة GDScript. وسنفترض أنك على معرفة بأساسيات البرمجة، وتعرف ما هي لغات البرمجة المتاحة في محرك الألعاب جودو لتتمكن من تنفيذ برنامجك الأول الذي سيحرك الصورة في مسار دائري كما في الصورة التالية: إعداد المشروع من الأفضل إنشاء مشروع جديد للبدء من الصفر، ويجب أن يحتوي مشروعك على صورة واحدة فقط هي أيقونة محرك الألعاب جودو التي تستخدم غالبًا في مجتمع المطورين لبناء النماذج الأولية. ستحتاج إلى إنشاء عقدة Sprite2D لعرضها في اللعبة، لذا عليك أن تنتقل للتبويب "مشهد Scene"، ثم تنقر فوق الزر"عقدة أخرى Other Node". اكتب Sprite2D في شريط البحث لتصفية النتائج، ثم انقر نقرًا مزدوجًا على Sprite2D لإنشاء العقدة. يُفترض أن تحوي علامة تبويب المشهد الآن على عقدة Sprite2D فقط. تحتاج عقدة Sprite2D إلى خلفية للعرض. وستلاحظ من قائمة "الفاحص" Inspector على اليمين أن خاصية Texture تشير إلى أنها فارغة، لعرض أيقونة جودو، انقر واسحب ملف الأيقونة icon.svg من قائمة نظام الملفات FileSystem إلى الخانة Texture ملاحظة: يمكنك إنشاء عقد Sprite2D تلقائيًا عن طريق سحب وإفلات الصور في نافذة العرض، ثم انقر واسحب الأيقونة في نافذة العرض لتثبيتها في وسط نافذة اللعبة. إنشاء نص برمجي جديد لإنشاء سكربت أو نص برمجي جديد وربطه بالعقدة، انقر بزر الفأرة الأيمن على Sprite2D في قائمة "المشهد Scene" وحدد خيار "إلحاق نص برمجي Attach Script" كما في الصورة التالية: ستظهر نافذة "إلحاق نص برمجي Attach Node Script"، حيث تتيح لك تحديد لغة النص البرمجي ومسار الملف من بين خيارات أخرى. غيّر حقل "القالب Template" من "Node: Default" (أي عقدة افتراضية) إلى "Object: Empty" (أي كائن فارغ) وذلك لبدء العمل بملف فارغ، ثم اترك الخيارات الأخرى على قيمها الافتراضية وانقر فوق الزر "أنشئ Create" لإنشاء النص البرمجي. ملاحظة: يجب أن تتطابق أسماء ملفات سكربت C# مع اسم الصنف الذي تتبع له. في هذه الحالة، يجب تسمية الملف MySprite2D.cs من المفترض أن تظهر مساحة عمل لكتابة الكود البرمجي مع فتح ملف sprite_2d.gd الجديد وسطر الشيفرة البرمجية التالية: بلغة GDScript: extends Sprite2D بلغة C#: using Godot; public partial class MySprite2D : Sprite2D { } كل ملف GDScript هو عبارة عن صنف class بشكل ضمني. وتُحدد الكلمة المفتاحية extends الصنف الذي يرثه أو يوسعه هذا الكود البرمجي. في هذه الحالة الصنف هو Sprite2D، مما يعني أن الكود البرمجي سيصل إلى جميع خصائص ودوال عقدة Sprite2D، بما في ذلك الأصناف التي ترث منها هذه العقدة مثل Node2D و CanvasItem و Node ففي حالتنا هذه يرث الصنف Sprite2D يرث من Node2D، والذي بدوره يرث من CanvasItem. وبالتالي سيصل صنفك الجديد إلى خصائص ودوال كل من Sprite2D و Node2D و CanvasItem. ملاحظة: إذا أغفلت السطر الذي يحتوي على الكلمة المفتاحية extends في لغة GDScript سيمدّد الصنف تلقائيًا إلى عقدة RefCounted، أي سيصبح بشكل افتراضي صنفًا فرعيًا من RefCounted الذي يستخدمه محرك ألعاب جودو لإدارة ذاكرة تطبيقك. يمكنك رؤية كافة الخصائص الموروثة في قائمة "الفاحص Inspector"، مثل خاصية texture الخاصة بالعقدة مما يسمح لك بتحريرها بسهولة داخل المحرر أو برمجيًا داخل النص البرمجي. ملاحظة: تعرض قائمة "الفاحص Inspector" خصائص العقدة في "حالة العنوان Title Case" افتراضيًا، مع كلمات بأحرف كبيرة ومنفصلة باستخدام مسافة. أما في شيفرة GDScript، فتكون هذه الخصائص مكتوبة بحالة الثعبان snake_case (أي بحالة الأحرف الصغيرة مع كلمات مفصولة عن بعضها باستخدام شرطة سفلية_). يمكنك التمرير فوق اسم أي خاصية في قائمة "الفاحص" Inspector لرؤية وصفها ومعرّفها في الشيفرة. تعليمة طباعة Hello World لا ينفذ النص البرمجي حاليًا أي شيء. سنجعله يطبع النص Hello world في الخرج. أضف الشيفرة التالية إلى نصك البرمجي: بلغة GDScript: func _init() : print(“Hello, world!”) بلغة C#: public MySprite2D() { GD.Print("Hello, world!"); } دعونا نشرح ما سبق. تُحدد الكلمة المفتاحية func دالة جديدة تسمى _init وهو اسم خاص لمنشئ أو باني الصنف لدينا. إذا عرّفت هذه الدالة، فسوف يستدعي جودو دالة _init() لكل كائن أو عقدة عند إنشائه في الذاكرة. ملاحظة: لغة البرمجة GDScript هي لغة تعتمد على المسافة البادئة، فالفراغ في بداية السطر الذي يحوي تعليمة الطباعة ()print ضروري لعمل الشيفرة البرمجية، فإذا أغفلتها أو لم تضع مسافة بادئة في بداية السطر بشكل صحيح سينبهك المحرر عليها باللون الأحمر ويعرض رسالة الخطأ التالية: "مسافة بادئة مطلوبة". احفظ المشهد باسم sprite_2d.tscn إذا لم تكن قد فعلت ذلك بالفعل، ثم اضغط على F6 (Cmd + R على macOS) لتشغيله. انظر إلى اللوحة السفلية للخرج لترى النص Hello world احذف الدالة _init()، بحيث يتبقى لديك فقط السطر extends Sprite2D. تدوير العقدة حان الوقت لتحريك العقدة وتدويرها. لفعل بذلك، سنضيف متغيرين جديدين إلى النص البرمجي هما سرعة الحركة المقاسة بوحدة البكسل في الثانية، والسرعة الزاوية المقاسة بالراديان في الثانية. أضف التالي بعد السطر extends Sprite2D. بلغة GDScript: var speed = 400 var angular_speed = PI بلغة C#: private int _speed = 400; private float _angularSpeed = Mathf.Pi; نكتب المتغيرات الجديدة بالقرب من بداية النص البرمجي، بعد الأسطر التي تحوي extends، وقبل الدوال. وستحتوي كل نسخة عقدة مرتبطة بهذا النص البرمجي على نسخة خاصة بها من خصائص speed و angular_speed. ملاحظة: تقاس الزوايا في محرك جودو بالراديان افتراضيًا، ولكن هناك دوال وخصائص مدمجة متاحة إذا كنت تفضل حساب الزوايا بالدرجات بدلاً من ذلك. لتحريك أيقونة جودو، نحتاج إلى تحديث موقعها وتدويرها في كل إطار ضمن حلقة اللعبة. يمكننا استخدام الدالة الافتراضية _process() الخاصة بالصنف Node. فإذا عرفتها في أي صنف يوسع الصنف Node مثل Sprite2D فسوف يستدعي جودو هذه الدالة في كل إطار ويمرر له قيمة باسم دلتا delta تمثل المدة الزمنية التي انقضت منذ الإطار الأخير. ملاحظة: تعمل الألعاب عن طريق عرض العديد من الصور في الثانية يطلق على كل منها إطار، ويتم ذلك بشكل حلقة متكررة. نقيس المعدل الذي تنتج فيه اللعبة الصور بمعدل الإطارات في الثانية (FPS). تهدف معظم الألعاب إلى 60 إطارًا في الثانية، على الرغم من أنك قد تجد أرقامًا مثل 30 إطارًا في الثانية على أجهزة الجوال الأبطأ أو أرقام بين 90 إلى 240 في ألعاب الواقع الافتراضي. يبذل المطورون قصارى جهدهم لتحديث عالم الألعاب وعرض الصور بفواصل زمنية ثابتة، لكن هناك دائمًا اختلافات بسيطة في أوقات عرض الإطارات. لهذا السبب يعطي جودو قيمة زمن دلتا delta، كي يجعل الحركة مستقلة عن معدل الإطارات. في نهاية الكود البرمجي، عرّف الدالة _process() كما يلي: بلغة GDScript: func _process(delta) : rotation += angular_speed * delta تحدد الكلمة المفتاحية func في الكود السابق دالة جديدة. بعد ذلك علينا كتابة اسم الدالة والقيم التي تأخذها بين قوسين. وتنهي النقطتان : التعريف وتمثل الكتل التي تتبعها محتوى الدالة أو تعليماتها. بلغة C#: public override void _Process(double delta) { Rotation += _angularSpeed * (float)delta; } ملاحظة: لاحظ كيف أن الدالة _process() مثل الدالة _init() تبدأ بشرطة سفلية. ووفق العُرف المتبع، تبدأ جميع دوال جودو الافتراضية بشرطة سفلية وهي ذاتها الدوال المضمنة التي يمكنك تعريفها للتواصل مع جودو. يستخدم السطر rotation += angular_speed * delta داخل الدالة لتحديث دوران العقدة أو الصورة في كل إطار استنادًا إلى سرعة الدوران والمدة الزمنية المنقضية، حيث أن الخاصية rotation هي خاصية موروثة عن الصنف Node2D التي تمتد منها العقدة Sprite2D وهي تتحكم في دوران العقدة. شغّل المشهد لمشاهدة أيقونة جودو تدور في مكانها. متابعة العمل على تحريك العقدة دعنا الآن نجعل العقدة تتحرك في مسار دائري. أضف السطرين التاليين داخل دالة _process() مع التأكد من إضافة مسافة بادئة للسطرين الجديدين بنفس طريقة المسافة البادئة للسطر rotation += angular_speed * delta قبلهما. بلغة GDScript: var velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta بلغة C#: var velocity = Vector2.Up.Rotated(Rotation) * _speed; Position += velocity * (float)delta; كما رأينا سابقًا، تحدد الكلمة المفتاحية var متغيرًا جديدًا. فإذا وضعته في بداية النص البرمجي، فإنه يُحدد خاصية الصنف. بينما إذا وضعته داخل الدالة، فإنه يُحدد متغيرًا محليًا يوجد ضمن نطاق الدالة نفسها فقط. نحدد متغيرًا محليًا يسمى velocity وهو متجه ثنائي الأبعاد يمثل الاتجاه والسرعة معًا. لتحريك العقدة للأمام، نبدأ من ثابت الصنف Vector2 وهو Vector2.UP يمثل متجه يشير لأعلى، ونديره عن طريق استدعاء طريقة Vector2 وهي rotated(). التعبير التالي Vector2.UP.rotated(rotation) هو شعاع يشير إلى الأمام بالنسبة إلى صورة الأيقونة مضروبًا بخاصية speed، حيث يعطينا سرعة يمكننا استخدامها لتحريك العقدة للأمام. نضيف velocity * delta إلى psotion العقدة لتحريكها. والموقع نفسه من نوع Vector2 وهو نوع مدمج في جودو يمثل متجه ثنائي الأبعاد. شغّل المشهد الآن لمشاهدة أيقونة جودو تتحرك في مسار دائري. بهذا نكون انتهينا من كتابة كود برمجي لتحريك العقدة بمفردها في محرك ألعاب جودو. وستتعلّم في الدرس التالي كيفية الحصول على مدخلات من اللاعب واستخدامها للتحكم بالعقدة. النص البرمجي الكامل لتحريك العقدة فيما يلي ملف sprite_2d.gd الكامل لاستخدامه كمرجع. بلغة GDScript: extends Sprite2D var speed = 400 var angular_speed = PI func _process(delta) : rotation += angular_speed * delta var velocity = Vector2.UP.rotated(rotation) * speed position += velocity * delta بلغة C#: using Godot; public partial class MySprite2D : Sprite2D { private int _speed = 400; private float _angularSpeed = Mathf.Pi; public override void _Process(double delta) { Rotation += _angularSpeed * (float)delta; var velocity = Vector2.Up.Rotated(Rotation) * _speed; Position += velocity * (float)delta; } } الخلاصة تعلمنا في مقال اليوم طريقة كتابة برنامج بسيط لتحريك العقد في جودو وضبط سرعتها واتجاهها، والجدير بالذكر أنّ تحريك العقدة الذي تعلمناه في هذا المقال لا يأخذ بعين الاعتبار الاصطدام بالجدران أو الأرض. وستتعلّم المزيد عند إنشاء أول لعبة ثنائية الأبعاد في جودو ونستخدم نهجًا آخر لتحريك الكائنات مع اكتشاف التصادمات. ترجمة بتصرف لقسم Creating your first script من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: تعرف على لغات البرمجة المتاحة في محرك جودو دليلك الشامل إلى برمجة الألعاب كيف تصبح مبرمج ألعاب فيديو ناجح العقد Nodes والمشاهد Scenes في جودو Godot
-
سنلقي الضوء في مقال اليوم على لغات البرمجة المتوفرة في محرك الألعاب جودو، ومتى نختار كل لغة منها ونتعلم محاسن ومساوئ كل خيار، وسنكتب في الجزء التالي أول سكريبت برمجي ضمن محرك جودو باستخدام لغة البرمجة الافتراضية GDScript. ترتبط الأكواد البرمجية بالعقد، فعندما تربط سكريبت برمجي بعقدة معينة، فإنك توسع سلوك تلك العقدة بإضافة وظائف جديدة لها، هذا يعني أن السكريبتات ترث كل وظائف وخصائص العقد المرتبطة بها. على سبيل المثال لنفترض أن تعمل على برمجة لعبة فضائية تتحرك فيها سفينة فضائية كما تبين الصورة التالية، وتريد أن تهتز الكاميرا عندما تتضرر السفينة.عليك أن تضيف عقدة كاميرا Camera2D تابعة لعقدة السفينة في التسلسل الهرمي.كي تتبع الكاميرا حركة السفينة لأن العقدة تتبع العقدة الأب لها في التسلسل الهرمي للعقد بشكل افتراضي، ولجعل الكاميرا تهتز عندما تتضرر السفينة، يجب عليك في هذه الحالة إنشاء سكريبت برمجي وربطه بعقدة Camera2D وتكتب فيه شيفرة الاهتزاز كون هذه الميزة غير متوافرة بشكل ضمني في جودو. لغات البرامج النصية المتوافرة في جودو يوفر محرك الألعاب جودو أربع لغات برمجية هي: GDScript وC# بالإضافة إلى كل من C وC++ من خلال تقنية GDExtention، وهناك لغات أخرى مدعومة من مجتمع جودو ولكن اللغات المذكورة هنا هي الرسمية. يمكنك استخدام عدة لغات برمجة في مشروع واحد، مثلًا يمكنك كتابة شيفرة منطق اللعب باستخدام GDScript كونها سريعة في الكتابة، وتستخدم C# أو C++ لتطبيق خوارزميات معقدة لتحسين أداء الشيفرة، أو يمكنك كتابة كل شيء باستخدام GDScript أو C#، الأمر عائد لك. الهدف من هذه المرونة هو تلبية احتياجات المطورين ومشاريع الألعاب المختلفة. أي لغة برمجة يجب أن أستخدم مع جودو؟ يُنصح باستخدام لغة GDScipt إذا كنت مبتدئًا، إذ أنشئت هذه اللغة لجودو بشكل خاص ولحاجات مطوري الألعاب، وهي تتميز بصياغتها السهلة وقلة حجمها، كما أنها تقدم أفضل تكامل مع محرك الألعاب جودو. ستحتاج بالنسبة للغة C# إلى محرر أكواد خارجي مثل VSCode أو Visual Studio، وعلى الرغم من كون دعم لغة C# أفضل، ولكنك لن تجد العديد من الموارد مقارنة للغة GDScript، لذا يُنصح باستخدام لغة C# لمستخدمي اللغة الخبراء. لنتحدث عن مميزات كل لغة، و محاسنها ومساوئها. لغة GDScript إن لغة GDScript هي لغة أمرية Imperative وكائنية التوجه Object-oriented مبنية خصوصًا لمحرك الألعاب جودو ومصممة لمطوري الألعاب لتوفير الوقت في كتابة شيفرة للألعاب، وتضم المميزات التالية: صياغة بسيطة تعطي ملفات صغيرة الحجم. وقت تحميل قليل وتصيير rendering سريع جدًا. تكامل قوي مع محرر جودو، مع إكمال للشيفرة للعقد والإشارات ومعلومات أكثر مع المشهد المرتبط به. وجود أنواع معطيات لتمثيل المتجهات vectors والتحويلات الهندسية transforms مما يسهل استخدام الجبر الخطي الضروري للألعاب. تدعم الخيوط المتعددة multiple threads وتملك نفس كفاءة اللغات المكتوبة بشكل ساكن (وهي اللغات التي يحدد فيها نوع البيانات بشكل ثابت وقت الترجمة). لا يوجد فيها كنس للمهملات garbage collection، لأن هذه الميزة تعيق إنشاء الألعاب، إذ يحسب المحرك المراجع ويدير لك الذاكرة في معظم الحالات بشكل افتراضي، ولكن يمكنك التحكم بالذاكرة إن احتجت لذلك. توفر ميزة تحديد النوع التدريجي Gradual Typing، فالمتغيرات لها أنواع ديناميكية افتراضيًا، ولكن يمكنك استخدام تلميحات النوع type hinting للتحقق من الأنواع. تشبه لغة GDScipt لغة بايثون Python في الهيكلية عن طريق استخدام كتل الشيفرة والمسافات البادئة، ولكنها لا تعمل بنفس المضمون، فقد طورت بالاعتماد على لغات برمجية متعددة مثل Squirrel وLua وبايثون. ملاحظة: قد تتساءل لماذا لا نستخدم لغة بايثون أو لغة Lua مباشرة في محرك جودو؟ في الواقع كانت لغة جودو تستخدم بايثون و Lua وقد تطلب تكامل اللغتين الكثير من العمل وكان له العديد من المحدوديات، مثلًا كان دعم الخيوط threads تحديًا كبيرًا في بايثون. كما أن تطوير لغة جديدة لا يتطلب جهدًا أكبر ويمكننا تصميمه ليلبي احتياجات مطوري الألعاب، ويتم العمل الآن على تحسينات وميزات كبيرة على اللغة ستكون صعبة التنفيذ مع لغات برمجية من طرف ثالث. لغة .NET/C# دُعمَت لغة C# الخاصة بمايكروسوفت رسميًا في محرك الألعاب جودو لأنها المفضلة لدى مطوري الألعاب، كما أن لغة C# متطورة ومرنة ولديها الكثير من المكتبات المكتوبة لأجلها، وكان إضافة الدعم لها ممكنًا بفضل تبرع سخي من مايكروسوفت لمطوّري جودو. تقدم لغة C# حلًا وسطيًّا بين الأداء وسهولة الاستخدام، ولكن يجب الانتباه من استخدامها لجامع المهملات garbage collector ملاحظة1: يجب استخدام نسخة .NET من محرر جودو لكتابة البرامج النصية باستخدام C#، ويمكن تنزيله من موقع جودو الرسمي. بما أن جودو تستخدم .NET6 فيمكنك نظريًا استخدام أي مكتبة .NET من طرف ثالث أو أي هيكلية في جودو، بالإضافة لأي لغة برمجة متوافقة مع البنية التحتية مثل F# وBoo وClojureCLR، ولكن لغة C# هي خيار .NET الوحيد المدعوم. ملاحظة2: لا تُنفذ شيفرة GDScript بسرعة لغات C# أو C++ المصرّفة، ولكن محرك جودو نفسه يحتوي على الكثير من الخوارزميات الأساسية السريعة التي تستدعي الدوال والوظائف الأساسية المكتوبة بلغة C++ داخل بنية المحرك، ففي معظم الأحوال لن تؤثر كتابة منطق اللعب على الأداء بشكل ملحوظ سواء استخدمت لغة GDScript أو C# أو C++ . استخدام لغة C++ عبر GDExtention تسمح لك GDExtention بكتابة شيفرة اللعبة باستخدام لغة C++ دون الحاجة لإعادة تصريف جودو. يمكنك استخدام أي إصدار من اللغة أو تنويع المصرف والإصدارات للمكتبات المشتركة بفضل استخدام واجهة برمجة التطبيقات المعروفة باسم C API Bridge وهي واجهة برمجية تسمح باستخدام أكواد مكتوبة بلغة C مع لغات برمجة أخرى وتمكن التطبيقات المكتوبة بلغات مختلفة من التفاعل مع بعضها البعض بسهولة. إن GDExtention هي أفضل خيار لتعزيز أداء الألعاب التي تطورها باستخدام جودو، إذ لست بحاجة لاستخدامها في كل اللعبة ويمكنك استخدامها فقط في الأجزاء التي تحتاج إلى تحسين الأداء بشكل كبير وكتابة أكواد بلغات مختلفة باستخدام GDScript أو C#، وعند العمل باستخدام GDExtention ستتشابه الأنواع والدوال والخاصيات لواجهة برمجة تطبيقات C++ مع تلك التي تستخدمها عند البرمجة داخل محرك Godot نفسه. الخلاصة تعرفنا في مقال اليوم على أهمية كتابة الأكواد البرمجية (السكريبتات) في جودو فهي ملفات تحتوي شيفرة برمجية يمكن ربطها مع العقد لتوسيع وظائفها، ورأينا أن محرك ألعاب جودو يدعم أربع لغات برمجة نصية رسمية ليمنح لمبرمجي الألعاب مرونة في اختيار اللغة التي يفضلونها، كما يمكن مزج اللغات لتطبيق خوارزميات ثقيلة باستخدام لغة C أو C++ وكتابة معظم منطق اللعبة باستخدام GDScript أو C#. ترجمة - وبتصرف - لقسم Scripting Languages من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: إنشاء نسخ من المَشاهِد والعقد في جودو Godot تعرف على أشهر لغات برمجة الألعاب تعرف على أشهر محركات الألعاب Game Engines أهمية صناعة الألعاب الإلكترونية دليلك الشامل إلى برمجة الألعاب
-
رأينا في الدرس السابق أنّ المشهد عبارة عن مجموعة من العقد المنظمة على شكل شجرة مع عقدة جذر واحدة، كما تعرفنا على إمكانية تقسيم مشروعك إلى أي عدد تريده من المَشاهد، حيث تساعدك هذه الميزة في تقسيم وتنظيم مكونات لعبتك المختلفة. يمكنك في محرك جودو إنشاء العديد من المَشاهد بالطريقة التي ترغب بها وحفظها كملفات بامتداد .tscn والذي تعني مشهدًا نصيًا وكان ملف label.tscn من الدرس السابق مثالًا على ذلك. ونسمّي تلك الملفات مَشاهد مُخزنّة لأنها تخزن معلومات حول محتوى المشهد. إليك مثالًا على مشهد كرة مكوّن من عقدة جذر RigidBody2D باسم كرة Ball، والتي تسمح للكرة بالسقوط والارتداد على الجدران، وعقدة Sprite2D، وعقدة CollisionShape2D. بمجرد حفظ أي مشهد سيعمل كنموذج أو قالب Template، أي يمكنك استنساخه في مَشاهد أخرى بقدر ما ترغب ونسمي عملية تكرار كائن من قالب كهذا باسم ** استنساخ Instancing ** وكما ذكرنا فيالدرس السابق، تتصرف المشاهد المُستنسخة كعقدة يخفي المحرر محتواها افتراضيًا. فسترى عند استنساخ الكرة عقدتها فقط، لاحظ أيضًا كيف أن لكل نسخة اسمًا فريدًا خاصًّا بها. تبدأ كل نسخة من مشهد الكرة بنفس الهيكل والخصائص كملف ball.tscn ومع ذلك، يمكنك تعديل كل منها بشكل مستقل، مثل تغيير كيفية ارتدادها، وزنها، أو أي خاصية يعرضها المشهد الأصلي. التطبيق العملي سنستخدم استنساخ المشاهد في تطبيق عملي لنرى كيف يعمل في جودو Godot. يمكنك تحميل مشروع الكرة الذي أعددناه لك من هنا instancing_starter.zip استخرج الملف على حاسوبك. ولاستيراده، تحتاج إلى الوصول إلى مدير المشروع Project Manager. يمكن الوصول إليه عن طريق فتح جودو، أو إذا كان مفتوحًا بالفعل، انقر على "المشروع Project" ثم " العودة لقائمة المشاريع Quit to Project List" (Ctrl + Shift + Q, Ctrl + Option + Cmd + Q على macOS) في مدير المشروع، انقر فوق زر "استيراد" Import لاستيراد المشروع في النافذة المنبثقة التي تظهر، انقر فوق زر "تصفح" browse وانتقل إلى المجلد الذي استخرجته. انقر نقرًا مزدوجًا على ملف project.godot لفتحه. وأخيرًا، انقر فوق زر "استيراد وتعديل" Import & Edit. يحتوي المشروع على مشهدين محفوظين هما المشهد الرئيسي main.tscn الذي يحتوي على الجدران التي تصطدم بها الكرة، ومشهد الكرة ball.tscn. يجب أن يفتح المشهد الرئيسي تلقائيًا. وفي حال رأيت مشهدًا فارغًا ثلاثي الأبعاد بدلاً من المشهد الرئيسي فانقر على زر 2D في أعلى الشاشة. لنضف كرة بصفة ابن للعقدة الرئيسية Main. من قائمة "المشهد" Scene، اختر العقدة Main، ثم انقر على أيقونة الرابط في أعلى القائمة. يسمح هذا الزر بإضافة نسخة من مشهد كابن للعقدة المختارة. انقر نقرًا مزدوجًا على مشهد الكرة لإنشاء نسخة منه. تظهر الكرة في الزاوية اليسرى أعلى نافذة العرض كما يلي: انقر عليها واسحبها نحو المنتصف. شغِّل اللعبة بالضغط على F5 (Cmd + B على macOS) ويجب أن ترى الكرة وهي تسقط للأسفل. نريد الآن إنشاء مزيد من النُسخ من عقدة الكرة. بالمحافظة على الكرة محددة، اضغط على Ctrl + D (Cmd + D على macOS) لاستدعاء أمر النسخ. ثم انقر واسحب لنقل الكرة الجديدة إلى موقع مختلف. يمكنك تكرار هذه العملية حتى تحصل على عدة كرات في المشهد. شغِّل اللعبة مرة أخرى. يجب أن ترى الآن كل كرة تسقط بشكل مستقل الأخرى. هذا ما تفعله النُسخ، حيث أن كل نسخة هي إعادة تشكيل مستقل لمشهد حسب قالب معين. تحرير المَشاهد والنُسخ هناك المزيد من المميزات حول النُسخ. إذ يمكنك أيضًا: تغيير خصائص إحدى الكرات دون التأثير على الأخرى باستخدام أمر "الفاحص" Inspector تغيير الخصائص الافتراضية لكل كرة بفتح مشهد ball.tscn وإجراء تغيير على عقدة الكرة هناك وعند الحفظ، ستُحدّث قيم جميع نُسخ الكرة في المشروع. لنجرب التالي: افتح ball.tscn وحدد عقدة الكرة. ومن قائمة "الفاحص" Inspector على اليمين، انقر على خاصية PhysicsMaterial لاستعراضها. اضبط خاصية Bounce إلى 0.5 بالنقر على مجال الأرقام أو كتابة 0.5 والضغط على Enter شغِّل اللعبة بالضغط على F5 (Cmd + B على macOS) ولاحظ كيف تترتّب الكرات الآن بشكل أكبر. حيث أن مشهد الكرة هو قالب لجميع النُسخ، ويُسبب إجراء التعديل والحفظ تحديث جميع النسخ وفقًا لذلك. الآن، دعنا نعدِّل نسخة فردية من هذه النسخ. انتقل مرة أخرى إلى المشهد الرئيسي بالنقر على التبويب المقابل فوق نافذة العرض. حدد إحدى عقد الكرة المُستنسخة واضبط قيمة مقياس الجاذبية إلى 10 من قائمة "الفاحص" Inspector. ستظهر أيقونة رمادية "revert" بجوار الخاصية المعدَّلة. تُشير هذه الأيقونة إلى أنك تجاوزت قيمة من مشهد المصدر المحزَّم، وسيتم الاحتفاظ بالتجاوز في النسخة حتى إذا عدّلت الخاصية في المشهد الأصلي. ستُستعاد الخاصية إلى قيمتها في المشهد المحفوظ، بالنقر على أيقونة revert. أعِد تشغيل اللعبة ولاحظ كيف تسقط هذه الكرة الآن بسرعة أكبر من الكرات الأخرى. ملاحظة: قد تلاحظ أنه لا يمكنك تغيير قيم PhysicsMaterial للكرة. هذا يعود إلى أن PhysicsMaterial هو مورد resource، والموارد هي مكونات أساسية أخرى في ألعاب محرك جودو، سنتحدث عنها في درس لاحق. ويجب عليك جعله فريدًا قبل أن تتمكن من تحريره في مشهد يرتبط بالمشهد الأصلي. ولجعل مورد ما فريدًا لحالة واحدة، انقر بالزر الأيمن عليه من قائمة "الفاحص" Inspector وانقر على "Make Unique" في القائمة التي تظهر لك. حالات المشهد كلغة تصميم تقدم نُسخ ومشاهد جودو لغة تصميم ممتازة، تجعله يبرز عن غيره، فمحرك جودو صّمّم منذ البداية ليناسب هذه الفكرة. لست ملزمًا باتّباع أنماط الشيفرة المعمارية عند صنع ألعاب باستخدام جودو، مثل نموذج-عرض-تحكم (MVC) أو مخططات كيان-علاقة (Entity-Relationship) ويمكنك بدلاً من ذلك، البدء بتخيّل العناصر التي سيراها اللاعبون في لعبتك وبناء شيفرتك منها. على سبيل المثال، يمكنك تقسيم لعبة إطلاق نار على النحو التالي: يمكنك إنشاء مخطط مثل هذا لأي نوع من الألعاب تقريبًا حيث يُمثِّل كل مستطيل في الصورة السابقة كيانًا يظهر في اللعبة من وجهة نظر اللاعب، وتُخبرك الأسهم أي مشهد يمتلك مشهدًا آخر. بمجرد أن تضع مخطط لعبتك فيمكنك حينها إنشاء مشهد لكل عنصر مُدرج في هذا المخطط لتطوير لعبتك. ويمكنك الاستفادة من ميزة الاستنساخ لبناء شجرة مَشاهد، سواء بكتابة كود برمجي لإتشاء المشهد أو يمكنك إنشاؤه مباشرة في المحرر الرسومي لجودو. غالبًا ما يقضي المبرمجون وقتًا طويلًا في تصميم هياكل معمارية مجردة ومحاولة تنسيق المكونات ضمنها، إلا أنّ التصميم بناءً على مشاهد اللعبة يجعل التطويرَ أسرع وأسهل، مما يسمح لك بالتركيز على منطق اللعبة نفسه. ونظرًا لأن معظم مكونات اللعبة تتطابق مباشرة مع مشهد ما، فإن استخدام التصميم المعتمد على المشهد يعني أن بإمكانك العمل على كل مشهد بشكل مستقل والتركيز على العلاقات بين المشاهد ودمج المشاهد الفرعية في المشاهد الرئيسية، وهذا يقلل من حجم الشيفرة البرمجية. إليك مثالًا على مخطط المشهد للعبة عالم مفتوح بأصول كثيرة وعناصر متداخلة: تخيل أننا بدأنا بإنشاء الغرفة، يُمكننا إنشاء عدة مَشاهد غرف مختلفة بترتيبات فريدة من الأثاث في كل منها، ويُمكننا في وقت لاحق إنشاء مشهد منزل يستخدم عدة نسخ غرف داخلية. وسنُنشئ قلعة من العديد من المنازل المُثبتة وأرضية كبيرة نضع عليها القلعة. وكل هذه ستكون حالات استنساخ لمشهد واحد أو أكثر. وفي وقت لاحق، يُمكن إنشاء مَشاهد جديدة تُمثِّل حُراسًا وإضافتهم إلى القلعة. ستتم إضافتهم بشكل غير مباشر إلى عالم اللعب كاملًا، وبفضل هذه الميزة في محرك ألعاب جودو، سيكون من السهل عليك تحسين وتطوير لعبتك بطريقة سهلة ومنظمة إذ أن كل ما عليك فعله هو إنشاء واستنساخ المزيد من المَشاهد. لقد صُمّم المحرر في جودو ليكون سهل الوصول من قبل المبرمجين والمصممين والفنانين على حد سواء، ويُمكن أن يشارك في عملية تطوير اللعبة فريق مؤلف من مصممي نمذجة ثنائية أو ثلاثية الأبعاد، ومصممي مستويات، ومصممي ألعاب، ومصممي رسوم متحركة، ويمكنهم أن يتعاونوا بكل مرونة ويعملوا جميعًا ضمن محرر جودو بطريقة سلسلة ومنظمة. الخلاصة تعرفنا في مقال اليوم على ميزة الاستنساخ التي يقصد بها عملية إنتاج نسخ من كائن ما في محرك ألعاب جودو ورأينا أن لهذه الميزة استخدامات عديدة. كما تعرفنا أكثر على المَشاهد التي تمنحك القدرة على تقسيم لعبتك إلى مكوِّنات قابلة لإعادة الاستخدام وتوفر لك أداة لبناء الأنظمة المعقدة كما توفر لك لغة رسومية تسهل عليك التفكير في هيكل مشروع اللعبة وتحسينها وتطويرها. ترجمة - وبتصرف - لقسم Creating instances من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: العقد Nodes والمشاهد Scenes في جودو Godot الرؤية التصميمية لمحرك اﻷلعاب جودو Godot مدخل إلى محرك الألعاب جودو Godot تعرف على أهمية صناعة الألعاب الإلكترونية
-
تعرفنا في مقال سابق على المفاهيم الرئيسية لمحرك الألعاب جودو Godot، ورأينا أنّ اللعبة عبارة عن مجموعة من المَشاهد على شكل شجرة وأنّ كل مشهد هو شجرة من العقد بحد ذاته. في هذا الدرس، سنشرح المزيد حول العقد والمشاهد ونعلمك كيف يمكن أن تنُشئ أول مشهد في جودو بنفسك. العقد العقد هي الوحدات الأساسية لبناء لعبتك، فهي مثل المكونات في نموذج، وهناك عشرات الأنواع من العقد والتي يمكن أن تعرض صورة أو صوتًا أو كاميرا وغيرها الكثير من الأنواع. تمتلك جميع العقد في جودو الخصائص التالية: اسم. خصائص قابلة للتحرير. تتلقى استدعاءات لتحديثها في كل إطار. يمكنك تطويرها بإضافة خصائص ودوال جديدة. يمكنك إضافتها إلى عقدة أخرى بصفة ابن. الخاصية الأخيرة مهمة حيث تشكّل العقد مع بعضها شجرة وهي ميزة قوية لتنظيم المشاريع. ونظرًا لأن العقد المختلفة لها دوال مختلفة، فإن دمجها معًا ينتج عنه سلوك أكثر تعقيدًا حيث يمكنك بناء شخصية لعبة ثنائية الأبعاد تتبعها الكاميرا باستخدام عقدة CharacterBody2D، وعقدة Sprite2D، وعقدة Camera2D، وعقدة CollisionShape2D. المَشاهد Scenes عندما ترتّب العقد ضمن شجرة، نسمي هذا التركيب مشهدًا تمامًا مثل الشخصية الموضحة في الصورة التالية، وبمجرد حفظها، ستصبح المَشاهد كنوع جديد من العقد في المحرر، حيث يمكنك إضافتها بصفة ابن لعقد موجودة مسبقًا. وفي هذه الحالة، تظهر نسخة من المشهد كعقدة واحدة مع تفاصيلها الداخلية المخفية. تسمح لك المَشاهد ببناء الشيفرة البرمجية للعبتك بالطريقة التي تريدها، كما يمكنك تكوين عقد لإنشاء أنواع مخصصة ومعقدة منها، مثل شخصية لعبة تركض وتقفز، مع شريط حياة Health bar، ومجموعة من الإعدادات التي تمكنك من التفاعل معها، وغيره الكثير. يعدّ محرر الألعاب جودو في الأساس محرر مشاهد فهو يحتوي على العديد من الأدوات لتحرير المَشاهد ثنائية وثلاثية الأبعاد، بالإضافة إلى واجهات المستخدم. كما أن المشروع الذي يُنفذ باستخدام محرك جودو يمكن أن يحتوي على العديد من هذه المَشاهد حسب الحاجة. يتطلّب المحرك وجود مشهد واحد فقط كمشهد رئيسي لتطبيقك، وهو ذاته المشهد الذي يعرضه المحرك في البداية عند تشغيل اللعبة. تتمتع المَشاهد - بالإضافة إلى عملها كعقد - بالخصائص التالية: لديها دائمًا عقدة جذر واحدة، مثل الشخصية في مثالنا. يمكنك حفظها على وسيط التخزين الخاص بك وتحميلها لاحقًا. يمكنك إنشاء عدد لا منتهي من النسخ للمشهد، ويمكنك أن تملك خمس أو عشر شخصيات في لعبتك، تم إنشاؤها من مشهد الشخصية الأساسي الخاص بك. إنشاء أول مشهد لك سنُنشئ أول مشهد بعقدة واحدة، ولتنفيذ ذلك، ستحتاج أولاً إلى إنشاء مشروع جديد. بعد فتح نافذة المشروع، سيظهر لك محرر فارغ. تظهر عدة خيارات في مشهد فارغ، ومن قائمة "مشهد Scene" على اليسار لإضافة عقد جذر بسرعة. تضيف 2D Scene عقد Node2D وتضيف 3D Scene عقد Node3D، وتضيف User Interface عقد Control. هذه التعيينات اختيارية حسب الحاجة وليس بالضرورة اختيارها جميعها. يتيح لك الخيار Other Node تحديد أي عقدة لتكون هي العقدة الجذر. ويعادل خيار Other Node في مشهد فارغ خيار Add Child Node في أعلى الجزء الأيسر من قائمة "مشهد Scene"، والذي عادة ما يضيف عقدًا جديدة كأبناء للعقد التي تم اختيارها مسبقًا. الآن سنضيف عقدة Label واحدة إلى المشهد. تتمثل وظيفتها برسم نص على الشاشة. انقر فوق زر Add Child Node أو Other Node لإنشاء عقدة جذر. ستفتح نافذة إنشاء العقد وتظهر قائمة طويلة من العقد المتوفرة. حدد عقدة Label. يمكنك كتابة اسمها لتصفية القائمة. انقر فوق عقدة Label لتحديدها ثم اضغط على زر "أنشئ Create" في أسفل النافذة. يحدث الكثير في محرك الألعاب عند إضافة أول عقدة إلى المشهد، حيث يتغير المشهد إلى مساحة عمل ثنائية الأبعاد لأن Label هو عقدة من نوع 2D. يظهر Label مُحدَّدًا في الزاوية اليسرى العلوية من نافذة العرض، وتظهر العقدة في القائمة "مشهد Scene" على اليسار، كما تظهر عناصر العقد في قائمة "الفاحص Inspector" على اليمين. تغيير عناصر العقد الخطوة التالية هي تغيير عنصر "Text" في Label. دعونا نغيره إلى Hello World انتقل إلى قائمة الفاحص Inspector على يمين نافذة العرض، ثم انقر داخل الحقل أسفل عنصر Text واكتب Hello World سترى أن النص يرسم ضمن نافذة العرض أثناء كتابتك. يمكنك نقل عقدة Label في نافذة العرض عن طريق تحديد أداة النقل في شريط الأدوات. عند تحديد عقدة Label، انقر واسحب في أي مكان ضمن نافذة العرض لنقلها إلى وسط العرض المحدد بالمستطيل. تشغيل المشهد أصبح كل شيء جاهزًا لتشغيل المشهد. اضغط على زر تشغيل المشهد في أعلى اليمين من الشاشة أو اضغط على F6) Cmd+R على نظام macOS). ستظهر نافذة منبثقة تطلب منك حفظ المشهد، وهذا خيار ضروري لتشغيل المشهد. انقر على زر الحفظ في مستعرض الملفات لحفظه باسم label.tscn يجب أن يفتح التطبيق في نافذة جديدة ويعرض فيها النص Hello World على النحو التالي: أغلق النافذة أو اضغط على F8 (cmd + . على نظام macOS) لإيقاف تشغيل المشهد. تعيين المشهد الرئيسي استخدمنا زر التشغيل لتشغيل المشهد، ويوجد زر آخر بجانبه يسمح لك بتحديد وتشغيل المشهد الرئيسي للمشروع. يمكنك الضغط على F5 (Cmd + B على نظام macOS) لفعل ذلك. ستظهر نافذة منبثقة تطلب منك تحديد المشهد الرئيسي. انقر على زر التحديد ثم انقر مرتين على label.tscn في المربع الذي يظهر لك يجب أن تشغل العرض التوضيحي مرة أخرى وعند تشغيلك للمشروع في المرات القادمة، سيستخدم جودو هذا المشهد كنقطة بداية. الخلاصة تناولنا في هذا المقال مفاهيم العقد Nodes والمشاهد Scenes في محرك الألعاب Godot ورأينا أن العقد هي الوحدات الأساسية التي تشكل مكونات اللعبة وقد تكون عبارة عن صورة أو صوت أو عنصر واجهة مستخدم، وأنها تتكون من خصائص قابلة للتعديل وتستجيب لاستدعاءات التحديث في كل إطار من إطارات اللعبة، كما استعرضنا كيفية تركيب العقد داخل شجرة لتشكيل مشهد يمثل كيانًا مستقلاً في اللعبة مثل شخصية أو مستوى ما. وفي المقال التالي، سنناقش مفهومًا رئيسيًا آخر في برمجة الألعاب باستخدام جودو وهو إنشاء نسخ من المشهد. ترجمة - وبتصرف - لقسم Nodes and Scenes من توثيق جودو الرسمي. اقرأ أيضًا مطور الألعاب: من هو وما هي مهامه تعرف على أشهر لغات برمجة الألعاب دليلك الشامل إلى بناء كاميرا خاصة بشاشات اللمس في محرّك اﻷلعاب جودو إعداد محرك الألعاب جودو Godot للعمل مع قاعدة البيانات SQLite
-
نتعرف في مقال اليوم على خدمة الترجمة الآلية من مايكروسوفت Microsoft Translator التي تعتمد على الخدمات السحابية وتوفر للمطورين والشركات إمكانية ترجمة النصوص والكلام من خلال واجهة برمجية API قوية وتتيح لها تخصيص نماذج الترجمة وتوفر مترجمًأ خاصً Custom Translator للمصطلحات الخاصة بكل صناعة.والمدمجة مع العديد من منتجات مايكروسوفت مثل Microsoft Office. كما نشرح مترجم Bing Translator الذي يوفر واجهة سهلة تعتمد على خدمة ترجمة مايكروسوفت Microsoft Translator ونتعرف على مميزاته وطريقة استخدامه للترجمة بسرعة كبيرة من لغة إلى أخرى. ما هو مترجم Bing Translator مترجم بينغ Bing Translator من مايكروسوفت هو خدمة ترجمة مستقلة موجهة للأفراد، يوفر واجهة أمامية تعتمد في عملها على خدمة الترجمة الأساسية من مايكروسوفت، وقدتأسست هذه الخدمة في عام 2007 وكانت تعرف سابقًا باسم Windows Live Translator وكانت تتيح خدمة الترجمة المجانية على صفحتها على الإنترنت لترجمة الكلمات، أما بالنسبة لترجمة مواقع ويب كاملة فقد كان الأمر من خلال أدوات متخصصة أخرى. واليوم يتكامل مترجم بينغ مع مجموعة متنوعة من منتجات مايكروسوفت مثل Microsoft Office و Skype و Microsoft Teams و Microsoft Edge لتعزيز الوصول والاستخدام، ويمكن الوصول لواجهته عبر متصفح الويب أو عبر تطبيق Bing على الجوال، وعند استخدامه ستلاحظ أنه يركز على البساطة والسرعة وسهولة الاستخدام ويوفر الاحتياجات الأساسية للترجمة، ويوفر واجهة سهلة الاستخدام يمكن للمستخدمين العاديين التعامل معها يسهولة لترجمة أي نصوص يريدونها من وإلى لغات مختلفة. استخدام مترجم Bing Translator إن كنت لا تملك خبرة كبيرة في برمجة وتطوير البرامج وترغب فقط في الحصول على ترجمة لبعض الكلمات والفقرات فإن أبسط طريقة للقيام بذلك هي زيارة موقع Bing Translator الذي يتضمن واجهة ترجمة بسيطة يمكنك من خلالها كتابة النص المُراد ترجمته في مربع النص على اليسار، وتظهر نتيجة الترجمة باللغة المطلوبة على اليمين، كما يمكنك تنزيل التطبيق نفسه على الهواتف المحمولة التي تدعم نظامي التشغيل iOS و Android. ومن أهم وأقوى خدمات Bing Translator المضمنة في متصفح Edge هي الترجمة الفورية للمواقع الإلكترونية من لغة الموقع الأساسية إلى لغة حاسوبك الافتراضية إذ تظهر رسالة في شريط البحث "ترجم هذه الصفحة"، وستترجم لك الخدمة عند الضغط عليها الموقع كاملًا مع المحافظة على تنسيقات الصفحة كما هي، وتسمح هذه الميزة بتصفح مواقع لم يكن بإمكانك سابقًا متابعتها بسبب عائق اللغة فهناك ملايين المواقع التي تنشر فقط بلغتها المحلية ولا تقدم أي نوع من الترجمة لأي لغة أخرى. ما هي خدمة Microsoft Translator هي خدمة ترجمة آلية تستهدف المطورين والمؤسسات والشركات تقدمها مايكروسوفت، وهي تعتمد على الحوسبة السحابية وتوفر واجهة برمجة تطبيقات API قوية لترجمة النصوص Text Translator API يمكن دمجها مع مختلف التطبيقات وتعتمد هذه الواجهة على الترجمة الآلية العصبونية Neural Machine Translation كطريقة أساسية في الترجمة، كما تستخدم قواميس ثنائية اللغة للتأكد من صحة الكلمات المترجمة وإيجاد مرادفات أخرى لهذه الكلمات، وهي تُستخدم اليوم بشكل كبير من قبل الشركات لتكامل خدمات الترجمة في منتجاتها وتطبيقاتها، وتتكامل مع العديد من منتجات وخدمات مايكروسوفت الأخرى. وتتراوح الخدمات المقدمة من خلال Text Translator API من 2 مليون حرف في الشهر للخدمة المجانية إلى مليارات الأحرف في الخدمات المدفوعة، كما تدعم خدمة ترجمة مايكروسوفت الترجمة بين العديد من اللغات واللهجات وأنظمة ترجمة الكلام التي تقدمها خدمات Live Conversation و Skype Translator بالإضافة لتطبيقات Microsoft Translator لكل من iOS و Android. ميزات خدمة Microsoft Translator تدعم خدمة ترجمة Microsoft Translator أكثر من 100 لغة (135 لغة وقت كتابة هذا المقال) ولها العديد من الميزات المتكاملة مع تطبيقات شركة مايكروسوفت وبعض المميزات التي يمكن استخدامها بشكل منفصل ومن أهمها: ترجمة الكلام والمحادثات المباشرة. ترجمة المستندات. ترجمة رسائل البريد الإلكتروني. تخصيص نماذج الترجمة لنشرح كل ميزة من هذه المميزات بمزيد من التفصيل. ترجمة المحادثات المباشرة يمكن لخدمة الترجمة Microsoft Translator ترجمة الكلام والحديث المباشر وبوقت قريب من الوقت الفعلي مع خدمة Translator live conversations لأكثر من 100 متحدث ضمن نفس المحادثة، كلٌّ بحسب لغته وباستخدام حاسوبه الخاص، إذ تترجم الحديث على شاشات كل المتابعين المتصلين مباشرة عند بدء أي شخص مشترك بالكلام. يمكنك من خلال هذه الميزة طرح أي سؤال بلغة لا تتحدثها، كل ماعليك هو اختيار لغتك، وتتحدث بها وسيرى الشخص الآخر الترجمة على جهازه بلغته ويرد عليك. ترجمة مستندات Word يمكن لمستخدمي Microsoft Office 365 ترجمة مستندات وورد Word إلى أي لغة يرغبون بها باستخدام Word Translator التي تترجم هذه المستندات اعتمادًا على خدمة الترجمة السحابية من مايكروسوفت، إذ تمكنك الخدمة من ترجمة جزء من المستند أو ترجمة المستند بأكمله، وبالتالي تخفف عند عناء استخدام طريقة ترجمة أخرى لترجمة النصوص. ترجمة رسائل البريد الإلكتروني إذا كنت من الأشخاص الذين يتواصلون مع جهات تتحدث لغة مختلفة عن طريق رسائل البريد الإلكتروني في عملك الخاص أو في الشركة التي تعمل بها. يمكنك توفير الوقت والجهد من نسخ وترجمة رسائلك إلى مواقع الترجمة المختلفة أو حتى إلى مترجم مايكروسوفت نفسه عن طريق تثبيت الإضافة لمتصفح الايميلات Outlook من الرابط التالي. تخصيص نماذج الترجمة توفر ترجمة مايكروسوفت أيضًا ميزة المترجم المخصص Custom Translator الذي يساعدك على تحسين عملية الترجمة واستخدام مستنداتك المترجمة مسبقًا لبناء نظام ترجمة بأسلوبك والمصطلحات الخاصة بمجال عملك بدلًا من استخدام الترجمة القياسية العامة، كما توفر إمكانية تحميل مستندات بصيغ متعددة مثل TXT و DOCX و XLSX ومطابقة الجمل تلقائيًا عبر المستندات لتحسين عملية الترجمة. ويمكن استخدام هذا الأسلوب من خلال واجهات جاهزة لا تحتاج لخبرة برمجية أو الاستفادة منها برمجيًا عبر واجهة برمجة تطبيقات مخصصة. كيفية استخدام الواجهة البرمجية من Microsoft Translator سنشرح في هذه الفقرة كيف يمكن للمبرمجين والمطورين استخدام واجهة برمجة ترجمة النصوص Text Translation REST API المقدمة من خدمة ترجمة مايكروسوفت لترجمة المواقع والتطبيقات الخاصة بهم. تقدم مايكروسوفت هذه الخدمة كجزء من خدمات Azure Cognitive Services التي توفر العديد من خدمات الذكاء الاصطناعي التابعة لمايكروسوفت عبر منصة Azure السحابية دون الحاجة إلى معرفة متعمقة بالذكاء الاصطناعي أوتقنيات تعلم الآلة،ومن تعد خدمة الترجمة الآلية بين اللغات من أهم هذه الخدمات وتتيح ترجمة النصوص بين مجموعة متنوعة من اللغات ويمكنك استخدامها لترجمة النصوص في التطبيقات والمواقع الإلكترونية وغيرها. تمتاز خدمة الترجمة باعتمادها على الشبكات العصبية بشكل افتراضي، ويمكنها فهم سياق الجمل كاملة قبل الترجمة مما يقدم جودة ترجمة أعلى إضافة لكونها أقرب إلى الترجمة البشرية على عكس أغلب واجهات برمجة التطبيقات (APIs) الأخرى التي تعتمد على الترجمة الآلية الساكنة Static Machine Translation. مثال عملي على تطبيق بلغة بايثون لتضمين ترجمة مايكروسوفت لنكتب تطبيقًا برمجيًا بسيطًا بلغة بايثون يستخدم الواجهة البرمجية الخاصة بالترجمة من مايكروسوفت لترجمة عبارة "Hello World!" من اللغة الإنجليزية إلى عدة لغات هي الألمانية ورمزها de والإيطالية ورمزها it والصينية المبسطة ورمزها zh-Hans. قبل البدء يجب أن يكون لديك: إصدار بايثون 2.7.x أو 3.x افتح نافذة الطرفية واستخدم pip لتثبيت مكتبة الطلبات Requests Library و حزمة uuid pip install requests uuid أنشئ في محرر النصوص المفضل لديك أو في أي بيئة تطوير متكاملة IDE ملفًا اسمه translator.py أضف الشيفرة التالية إلى الملف: # -*- coding: utf-8 -*- # هذا التطبيق البسيط يستخدم المورد '/translate' لترجمة النص من # لغة إلى أخرى import os, requests, uuid, json # استبدل قيمة المتغير التالي بمفتاح الاشتراك الخاص بحسابك على مترجم مايكروسوفت key_var_name = 'TRANSLATOR_TEXT_SUBSCRIPTION_KEY' if not key_var_name in os.environ: raise Exception('Please set/export the environment variable: {}'.format(key_var_name)) subscription_key = os.environ[key_var_name] region_var_name = 'TRANSLATOR_TEXT_REGION' if not region_var_name in os.environ: raise Exception('Please set/export the environment variable: {}'.format(region_var_name)) region = os.environ[region_var_name] # استبدل قيمة المتغير التالي برابط الواجهة البرمجية الخاص بالنص الذي تريد ترجمته endpoint_var_name = 'TRANSLATOR_TEXT_ENDPOINT' if not endpoint_var_name in os.environ: raise Exception('Please set/export the environment variable: {}'.format(endpoint_var_name)) endpoint = os.environ[endpoint_var_name] # إذا واجهت أي مشاكل مع عنوان base_url أو path ، تأكد # من أنك تستخدم أحدث نقطة نهاية: https://docs.microsoft.com/azure/cognitive-services/translator/reference/v3-0-translate path = '/translate?api-version=3.0' params = '&from=en&to=de&to=it&to=zh-Hans' constructed_url = endpoint + path + params headers = { 'Ocp-Apim-Subscription-Key': subscription_key, 'Ocp-Apim-Subscription-Region': region, 'Content-type': 'application/json', 'X-ClientTraceId': str(uuid.uuid4()) } # يمكنك تمرير أكثر من كائن واحد في body. body = [{ 'text' : 'Hello World!' }] request = requests.post(constructed_url, headers=headers, json=body) response = request.json() print(json.dumps(response, sort_keys=True, indent=4, ensure_ascii=False, separators=(',', ': '))) إليك أهم ما قمنا به في الكود أعلاه: وضعنا عبارة -*- coding: utf-8 -*- في البداية لنحدد أن ترميز النص لهذا الملف هو UTF-8 لضمان التعامل الصحيح مع النصوص غير الإنجليزية. ثم استوردنا المكتبات اللازمة وهي المكتبة os للتعامل مع متغيرات البيئة والمكتبة requests لإرسال طلبات HTTP و uuid لإنشاء معرف فريد للعميل و json للتعامل مع البيانات بتنسيق جيسون. تحققنا من وجود متغيرات البيئة الضرورية لعمل التطبيق، وهي مفتاح الاشتراك في الواجهة البرمجية TRANSLATOR_TEXT_SUBSCRIPTION_KEY، والمنطقة TRANSLATOR_TEXT_REGION، ونقطة النهاية TRANSLATOR_TEXT_ENDPOINT. أنشأنا عنوان URL الخاص بالطلب باستخدام المسار path والمعاملات params. وحددنا ترجمة النص من اللغة الإنجليزية إلى الألمانية والإيطالية والصينية المبسطة، وأعددنا ترويسات الطلب والتي تحتوي على مفتاح الاشتراك والمنطقة ومعرف العميل. أعددنا جسم الطلب Request Body الذي يحتوي على النص المراد ترجمته كلائحة من القواميس يحتوي كل منها على النص المراد ترجمته. أخيرًا أرسلنا طلب POST إلى رابط خدمة الترجمة Translator Text API لنحصل على الرد بتنسيق JSON على النحو التالي: [ { "translations": [ { "text": "Hallo Welt!", "to": "de" }, { "text": "Ciao mondo!", "to": "it" }, { "text": "你好,世界!", "to": "zh-Hans" } ] } ] يمكنك معالجة الاستجابة وطباعة هذا الرد بالتنسيق الذي تريده، كما يمكنك تعديل النص واللغات المستهدفة وفقًا للاحتياجات المختلفة من خلال تعديل محتوى جسم الطلب ومعاملات عنوان URL. سلبيات وأوجه النقص في Microsoft Translator كما هي الحالة في كافة خدمات الترجمة الفورية لا تقدم الخدمة الدقة والجودة العالية التي يقدمها المترجمون البشر وهي بحاجة تدقيق من شخص للحصول على نتائج عالية. لا تستطيع الخدمة تقديم ترجمات متخصصة في المواضيع القانونية والطبية مثلًا كون الترجمة الآلية العصبونية مدربة على نصوص عامة، ولكن يمكن الاستعانة بخدمة القواميس المخصصة للحصول على ترجمة أدق وتتعلق بتخصص معين. الخلاصة تعرّفنا في مقال اليوم على خدمة ترجمة مترجم بينغ من مايكروسوفت Bing Translator وخدمة الترجمة الأساسية التي يعتمد عليها Microsoft Translator واستعرضنا مميزات وحالات استخدام كل منهما وأمثلة عملية على استخدامهما، ووجدنا أن Bing Translator مناسبة للمستخدم العادي الذي يحتاج لطريقة سهلة وسريعة لترجمة النصوص، بينما خدمة Microsoft Translator موجهة للمطورين والشركات والمؤسسات وتوفر واجهة برمجية تتيح لهم تضمين خدمات الترجممة في تطبيقاتهم بسهولة، ولك الحرية في استخدام ما يناسبك وفق متطلبات عملك. اقرأ أيضًا أشهر أدوات الترجمة بمساعدة الحاسوب CAT Tools كيفية تعريب متجرك الإلكتروني على منصة WooCommerce تعرف على منصة سمارت كات للترجمة Smartcat مدخل إلى منصة الترجمة Weblate للمبتدئين ما هي خدمة Translation Hub المقدمة من Google؟
-
مع التوسع المستمر للشركات والمؤسسات عبر الحدود الجغرافية وزيادة تفاعلها مع جمهور من مختلف الثقافات واللغات، أصبحت الترجمة وإدارتها أمرًا بالغ الأهمية في عالم الأعمال، واكتسبت أدوات إدارة الترجمة الآليّة أهمية بالغة لمساهتمها في تحقيق هذا الهدف بكفاءة وسهولة. في مقال اليوم سنشرح أداة ترانسيفكس Transifex التي تعد واحدة من أبرز أدوات الترجمة الآلية المعتمدة على الويب وتساعد الشركات والمؤسسات على ترجمة المحتوى الرقمي وإدارة عملية الترجمة الآلية بسرعة واحترافية. ما هي Transifex Live منصة Transifex Live أو اختصارًا Transifex هي منصة شاملة لترجمة المحتوى الرقمي عبر الانترنت، حيث توفر واجهة سهلة الاستخدام ومجموعة متنوعة من الأدوات لإدارة عمليات ترجمة الموارد المختلفة بدءًا من الملفات والمستندات والوثاثق المختلفة ووصولًا للمواقع الإلكترونية والتطبيقات المختلفة. كما تتميز بمرونتها وقدرتها على تعزيز التعاون بين فرق العمل المتعددة إذ يمكن لعدة أشخاص ترجمة نفس المحتوى على التوازي. ميزات أداة Transifex تتفوّق أداة Transifex على غيرها من أدوات الترجمة الآلية، حيث توفر مجموعة واسعة من الميزات التي تجعل عمليات الترجمة أسهل وأكثر كفاءة. سنلقي نظرة على بعض هذه الميزات المميزة: 1.القدرة على إدارة المشاريع والفرق توفر Transifex بيئة عمل متكاملة لإدارة المشاريع والفرق، مما يسهل التنسيق بين أعضاء فريق الترجمة وتوزيع المهام بشكل فعّال. إذ يمكن للمستخدمين إنشاء مشاريع متعددة وتحديد أدوار مختلفة لأعضاء فريق الترجمة، وتخصيص الصلاحيات وإدارة الوصول للمحتوى حسب الحاجة. 2.واجهة بسيطة سهلة الاستخدام توفر منصة Transifex واجهة مستخدم بسيطة وسهلة الاستخدام، مما يسهل التعامل معها من قبل المستخدمين من كافة المستويات، وبفضل تصميمها البسيط والمباشر يمكن للمستخدمين إضافة الملفات وإدارة الترجمات بسهولة دون مواجهة أيه صعوبات تقنية. 3.دعم أنواع الملفات والصيغ المتعددة تتيح Transifex دعمًا شاملاً لأنواع متعددة من الملفات والصيغ تشمل الملفات النصية وملفات HTML، وملفات Word، وجداول Excel، وملفات العروض التقديمية PowerPoint ...إلخ. مما يعني أن بإمكان المستخدمين ترجمة مختلف أنواع المحتوى الرقمي بسهولة دون الحاجة إلى استخدام أدوات خارجية. 4.سهولة ترجمة المواقع والتطبيقات توفر Transifex طريقة بسيطة لترجمة المواقع الإلكترونية وتطبيقات الويب دون الحاجة إلى ترجمة الكود المصدري أو تعديل ملفات التطبيق، كل ما عليك هو إضافة كود جافاسكريبت أو مفتاح الواجهة البرمجية API الخاص بالمنصة Transifex Live إلى موقعك والبدء بترجمة المحتوى في السياق، ثم نشر الترجمة بضغطة زر واحدة! لهذا السبب تعد Transifex خيارًا مثاليًا للشركات والمؤسسات التي تسعى لتسهيل عمليات الترجمة وتحقيق النجاح على جميع الأصعدة بفضل ميزاتها المتقدمة لإدارة المشاريع، وواجهتها البسيطة، وسهولة استخدامها، ودعمها الشامل لأنواع الملفات والصيغ المختلفة. حالات استخدام Transifex يمكنك استخدام منصة Transifex في عدة حالات مختلفة لإدارة الترجمات بشكل فعال وسهل، ولكننا سنتحدث عن ثلاث حالات استخدام رئيسية لأداة Transifex التي تتفوق فيها على غيرها من أدوات الترجمة الآلية وهي: ترجمة التطبيقات. ترجمة المحتوى المنشور عبر الإنترنت. ترجمة المتاجر الإلكترونية. والآن لنناقش كلّ حالةٍ على حدة بمزيد من التفصيل: 1. ترجمة التطبيقات في عالم التقنية الحديث، تعدّ البرمجيات من أهم الصناعات التي تعتمد على الترجمة بشكل كبير للوصول إلى جمهور عالمي متنوع، وباستخدام Transifex يمكن لفرق تطوير المواقع الإلكترونية وتطبيقات الهاتف المحمول ترجمة مواقعهم وتطبيقاتهم وإدارة ترجمتها بشكل فعال وتوفير تجربة مستخدم متعددة اللغات بمنتهى السهولة. 2. نشر المحتوى الإلكتروني المنشور عبر الإنترنت في عصرنا الرقمي، يعد النشر الإلكتروني وإدارة المحتوى عبر الإنترنت جزءًا أساسيًا من استراتيجية التسويق والاتصال للشركات والمؤسسات. وباستخدام Transifex يمكن للناشرين ومديري المحتوى ترجمة المقالات والصفحات المنشورة على الانترنت لمختلف اللغات بسهولة وفعالية، مما يساعدهم على زيادة الوصول إلى جمهور عالمي. 3. ترجمة مواقع التجارة الإلكترونية تعد الترجمة من العوامل بالغة الأهمية لضمان نجاح الأعمال التجارية عبر الإنترنت، وباستخدام Transifex، يمكن لأصحاب المتاجر الإلكترونية ترجمة صفحات المنتجات وسلة التسوق بسهولة، وتوفير تجربة تسوق أفضل لعملائهم من مختلف أنحاء العالم، إضافة إلى توفير وقت وجهد أصحاب العمل والمشاريع التجارية. كما يمكن للشركات تحقيق تحسين مستمر في عمليات الترجمة وتعزيز قدرتها على الوصول إلى جمهور دولي بفعالية أكبر باستخدام منصة Transifex، وذلك من خلال توظيف تقنيات حديثة مثل الذكاء الاصطناعي وتعلم الآلة، إذ يمكن لمنصة Transifex تسهيل عمليات الترجمة وتحسين دقتها وسرعتها بالاعتماد على هذه التقنيات المتقدمة، كما يمكن للشركات توسيع نطاق عملها وتحقيق تفاعل أفضل مع الجمهور المستهدف. استخدام Transifex لترجمة متجر ووردبريس سنشرح في الفقرات التالية الخطوات العملية لترجمة موقع ووردبريس باستخدام Transifex وستجد أن الخطوات سهلة وبسيطة بفضل تكامل منصة Transifex Live مع نظام ووردبريس. التسجيل في منصة Transifex Live عليك بداية أن تسجل حسابًا على منصة Transifex Live إذا لم تقم بهذه الخطوة بعد، بمجرد تسجيلك في حساب ترانسيفكس، سيُطلب منك إنشاء منظمة organization والمقصود بالمنظمة المكان الرئيسي الذي يجمع المشاريع التي ستترجمها باستخدام ترانسيفكس والأشخاص المشاركين في العمل على مشروع الترجمة وبصفتك الشخص الذي أنشأ المنظمة، ستُعين كأحد مدرائها. بعدها عليك أن تنشئ مشروع ترجمة جديد وتضيف له رابط موقعك الإلكتروني وتحصل على كود جافا سكريبت JavaScript (أو مفتاح API key) الذي توفره المنصة لتضيفه إلى موقع ووردبريس الخاص بك كما سنشرح في الخطوة التالية، ومهمة هذا الكود جمع وعرض الترجمات في الوقت الفعلي دون الحاجة إلى إعدادات معقدة. يمكنك الحصول على مفتاح الواجهة البرمجية لمنصة Transifex من خلال الانتقال إلى صفحة إعدادات المشروع الخاص بك، ومن ثم نسخ المفتاح من شيفرة جافا سكريبت البرمجية: ملاحظة: تأكّد من أن مشروع الترجمة الخاص بك يندرج تحت تصنيف Live Project، وتحقق من صحّة رابط الموقع الإلكتروني الخاص بك. إدارة الموارد والمشاريع في منصة Transifex تُنظم كل منظمة في Transifex المشاريع والموارد، وقبل التعمق في كيفية إنشاء مشروع أو إضافة محتوى للترجمة، عليك أن تعرف أن الموارد هي المحتوى الذي تترجمه بين لغتين مختلفتين. أما المشاريع فهي مجموعة الموارد المستخدمة، ويمكنك إنشاء عدد كبير من المشاريع حسب احتياجاتك، ولا يوجد حد لعدد الموارد التي يمكن أن تكون في مشروع واحد. يُمكنك تنظيم المشاريع بالطريقة التي ترغب بها، وإليك بعض الأشياء التي يجب أخذها في الاعتبار عند تنظيم مشاريعك (فكر في المشاريع كأقسام والملفات كموارد): جمع المحتوى ذي الصلة في مشروع واحد. على سبيل المثال، يمكنك وضع تطبيق iOS الخاص بك في مشروع خاص، وموقع الويب الخاص بك في مشروع آخر بحيث يحتوي كل مشروع على إعدادات وخيارات تكوين محددة (مثل فريق الترجمة، ذاكرة الترجمة، إعدادات سير العمل). يمكنك ترجمة كل مشروع إلى لغة واحدة أو أكثر (اللغات المستهدفة)، كل ما عليك هو إنشاء مشاريع منفصلة وإذا كان لديك محتوى يحتاج إلى ترجمة إلى مجموعات منفصلة من اللغات، لنفترض أنك ترغب في وضع محتوى في نفس المشروع يحتاج إلى ترجمة إلى لغات مختلفة. في هذه الحالة، ستحتاج إلى إخطار مترجميك بألا يترجموا تلك الملفات إلى بعض لغات المشروع. أسند كل مشروع أيضًا إلى فريق من المترجمين والمراجعين. فإذا كان لديك موردان وتود أن يتولى كل منهم ترجمة مورد منهم، فضعهم في مشاريع منفصلة. تُتيح لك الموارد الصغيرة المتعددة تجزئة العبارات بطريقة منطقية أكثر وتوفير عدة نسخ من نفس العبارة التي قد تحتاج إلى توفير ترجمات مختلفة لها. الخطوة2: تثبيت وإعداد إضافة Transifex في متجرك الأن عليك ربط منصة ترانسيفكس Transifex مع متجر ووكوميرس من أجل ترجمة وصف المنتجات وترجمة صفحات المتجر الإلكتروني ويتم ذلك من خلال تثبيت وتفعيل إضافة Transifex live عن طريق لوحة التحكم الخاصة بموقعك على ووردبريس، والانتقال لقسم "إضافات"، ثم اختيار "أضف إضافة جديدة"، والبحث عن اسم الإضافة في شريط البحث، والنقر على زر Install Now، ثم Activate كما توضح الصورة التالية: بعد تفعيل الإضافة، انتقل إلى صفحة إعدادات الإضافة والصق مفتاح الواجهة البرمجية API key كما في الصورة التالية: كما يمكنك من خلال إعدادات الإضافة اختيار مكان ظهور مكون الترجمة على موقعك وتخصيص مظهره ليتماشى مع تصميم موقعك، وتدعم الإضافة أيضًا توفير روابط URL متعددة للمحتوى متعدد اللغات مثل تضمين اسم اللغة ضمن نطاق فرعي Subdomain أو دليل فرعي Subdirectory أو كوسيط ضمن الرابط كما في الأمثلة التالية: <! --Subdomain --> http://ar.example.com <! --Subdirectory --> http://www.example.com/ar <! --URL Parameter --> http://www.example.com?lang=ar ترجمة محتوى الموقع في السياق باستخدام Transifex Live بعد إعداد الإضافة ستتمكن من ترجمة محتوى موقعك مباشرة ضمن سياق الموقع. هذه الطريقة سهلة وسريعة وتوفر رؤية مباشرة لكيفية ظهور النصوص المترجمة على الموقع، فبعد ربط موقعك مع Transifex Live ستبدأ المنصة باكتشاف المحتوى القابل للترجمة في موقعك، كل ما عليك هو فتح موقعك كما يراه الزوار، والتنقل بين الصفحات لجمع وتحديد النصوص التي تحتاج ترجمتها، ثم اذهب إلى حسابك في Transifex وافتح المشروع الذي يحتوي على النصوص المجمعة وابدأ بترجمتها. بمجرد أن تكون الترجمات جاهزة، يمكنك نشرها عبر واجهة Transifex Live وتحديد اللغات التي ترغب في نشرها ليتم عرض الترجمات مباشرة على موقعك من خلال شبكة توزيع المحتوى CDN الخاصة بمنصة Transifex مما يضمن سرعة ترجمة موقعك. ملاحظة: تأكد من تفعيل خيار عرض الترجمات في إعدادات الإضافة لتظهر الترجمات التي أضفتها. بعدها كلما قمت بتحديث المحتوى على موقعك، يمكن لمنصة Transifex اكتشاف هذه التغييرات تلقائيًا، مما يسمح لك بالحفاظ على الترجمات محدثة بسهولة كما يمكنك أيضًا إلغاء نشر الترجمة إذا لزم الأمر. التعامل مع ملفات الترجمة po. في منصة Transifex تستخدم مواقع ووردبريس ملفات بصيغة po. لتخزين الترجمات النصية لمكونات الموقع المختلفة مثل القوالب (Themes) والإضافات (Plugins) ويمكنك ترجمتها أيضأ من خلال رفع ملفات po الخاصة بموقعك إلى Transifex واستخدم واجهة Transifex لترجمة النصوص الموجودة في هذه الملفات وبعد إتمام الترجمة، يمكنكك تنزيل ملفات PO المترجمة ورفعها على موقعك. من المهمّ أن تعرف أنّ أداة Transifex لا تغير صيغة وهيكلية الملف الأصلي (المصدر). فإذا رفعت ملف ترجمة بصيغة po. فإن الملف النهائي بعد الترجمة سيكون أيضًا بصيغة .po، والحالة الوحيدة التي يمكن فيها الحصول على صيغة ملف أخرى هي xliff (وهذه الميزة متوفرة في خطة البريميوم للمنصة أو الخطط الأعلى). إذ تستخدم صيغة xliff كصيغة ملف وسيطة للترجمة خارج ترانسيفكس في أداة CAT أخرى. وxliff هي صيغة مبنية على XML وتم إنشاؤها لتوحيد طريقة تمرير البيانات التي يمكن تعريبها بين عدة أدوات وهو صيغة شائعة لتبادل أدوات الترجمة الآلية CAT. في هذه الحالة، يكون سير العمل كالتالي: حمّل ملف المصدر بالصيغة التي ترغب فيها، على سبيل المثال .po. نزّل الملف للترجمة بصيغة xliff. ترجم ملف xliff خارج ترانسيفكس. أعد تحميل ملف xliff إلى ترانسيفكس (باستخدام الخيار "تحميل ملف XLIFF"). ستكون الترجمة الآن متاحة في ترانسيفكس وتتمكن من تنزيل الملف المترجم بصيغة الملف الأصلية .po وكل ما عليك الآن هو استيراد هذا الملف لموقع ووردبريس لترجمته بصورة آلية. تكامل سير العمل بين Transifex وأدوات البرمجة والتطوير يعدّ تكامل أداة الترجمة Transifex مع أدوات التطوير جزءًا أساسيًّا من عملية تطوير وترجمة التطبيقات والمواقع عبر الإنترنت. فهذا التكامل يسهّل على فريقي الترجمة والتطوير العمل بشكل متناغم، ما يحسّن تجربة المستخدم النهائية ويسرّع عملية نشر المحتوى بلغات متعددة. التكامل مع Git و GitHub أحد أهم أشكال التكامل هو التكامل مع Git و GitHub، حيث يمكن لفرق الترجمة العمل بشكل مباشر على المستودعات البرمجية الخاصة بهم ومزامنة التغييرات بسلاسة مع Transifex. مما يسهل على المطورين تحديث الترجمات وإدارتها بشكل فعال دون الحاجة إلى تبادل الملفات يدويًا. التكامل مع أنظمة إدارة المحتوى (CMS) مثل WordPress وDrupal كما رأينا في تجربة ترجمة موقع ووردبريس، تتكامل Transifex أيضًا مع أنظمة إدارة المحتوى (CMS) مثل WordPress و Drupal. إذ يمكن للمستخدمين ترجمة المحتوى الموجود على هذه الأنظمة بسهولة وفعالية باستخدام Transifex، مما يساعدهم على إطلاق المواقع بلغات متعددة والوصول إلى جمهور عالمي أوسع. التكامل مع منصات التطبيقات مثل Android و iOS وبالنسبة لتطبيقات الهاتف المحمول، فإن تكامل Transifex مع منصات التطبيقات مثل Android وiOS يسهل على فرق الترجمة ترجمة وتحديث نصوص التطبيقات بسرعة وفعالية. إذ يمكن للفرق العمل على ترجمة النصوص المخزنة في ملفات المشروع بشكل مباشر من خلال Transifex، مما يجعل عملية تطوير التطبيقات متعددة اللغات أكثر سهولة ويسرع عملية نشر المحتوى بلغات متعددة. الترجمة الآلية باستخدام أداة Transifex تسهّل أداة Transifex عملية إدارة الترجمات الآلية من خلال استخدام تقنيات الذكاء الاصطناعي والتعلم الآلي. فهي تعتمد على خوارزميات متقدمة تساعد في تحسين جودة الترجمات وتسريع عملية الترجمة بشكل كبير. ومن أهم مميزات استخدام أدوات الترجمة الآلية مثل Transifex هو توفير وقت وجهد كبيرين لفرق الترجمة. حيث يمكن للأداة ترجمة كميات كبيرة من المحتوى بشكل سريع وفعال، مما يساعد في تقليل العبء على فرق الترجمة وتحسين كفاءتها. بالإضافة إلى ذلك، تعتبر تقنيات الذكاء الاصطناعي والتعلم الآلي جزءًا أساسيًا من عملية تحسين جودة الترجمات. فهي تساعد في تحسين دقة الترجمات وضبطها بشكل أفضل وأكثر احترافية، مما يسهم في تقديم تجربة مستخدم متفوقة للجمهور. علاوةً على ذلك، يوفر استخدام Transifex مرونة كبيرة للشركات والمؤسسات. فهي تتيح لهم إدارة الترجمات بشكل مستقل وفعال، دون الحاجة إلى الاعتماد على فرق خارجية أو متخصصين في الترجمة. الخلاصة تعرفنا في هذا المقال على إحدى الأدوات القوية للترجمة الآلية Transifex التي تسهل عملية ترجمة المحتوى النصي ومحتوى المواقع والتطبيقات وتساعد الشركات والمؤسسات على إدارة وتنفيذ عمليات الترجمة بكل سهولة وفعالية. ووضحنا أبرز مميزاتها وحالات استخدامها. اقرأ أيضًا أشهر أدوات الترجمة بمساعدة الحاسوب CAT Tools كيفية تعريب متجرك الإلكتروني على منصة WooCommerce تعرف على منصة سمارت كات للترجمة Smartcat مدخل إلى منصة الترجمة Weblate للمبتدئين ما هي خدمة Translation Hub المقدمة من Google؟
-
قد تبدو لغة جافا سكريبت JavaScript للوهلة الأولى سهلةً للغاية، إلا أن اللغة أكثر دقة وقوة وتعقيدًا مما قد تعتقده، إذ تؤدي العديد من التفاصيل الدقيقة في جافا سكريبت إلى عدد من المشكلات الشائعة - نناقش 10 منها في هذا المقال- حيث نركز على المشكلات التي تمنع التعليمات البرمجية من العمل على النحو المطلوب لتكون على دراية بهذه المخاطر وتتجنبها في سعيك لأن تصبح مطور جافا سكريبت محترفًا. تعد جافا سكريبت في يومنا الحاليّ جوهرًا لمعظم تطبيقات الويب الحديثة تقريبًا. ولهذا السبب تقع مهمة اكتشاف مشاكل جافا سكريبت، والأخطاء التي تسببها في مقدمة اهتمامات مطوري الويب، كما أن اعتماد المكتبات والأطر القوية على جافا سكريبت لتطوير تطبيقات الصفحة الواحدة Single Page Applications أو اختصارًا SPA والرسومات والرسوم المتحركة ومنصات جافا سكريبت الموجودة من طرف الخادم ليس بالأمر الجديد، إذ أصبحت جافا سكريبت منتشرة في كل مكان في عالم تطوير تطبيقات الويب، وأصبح من المهمّ تعلّم لغة جافا سكريبت وإتقانها. قد تبدو جافا سكريبت للوهلة الأولى بسيطة جدًا، وفي الواقع تعدّ عملية إنشاء وظائف جافا سكريبت الأساسية في صفحة ويب مهمة بسيطةً إلى حدّ ما لأي مطور برمجيات ذي خبرة، حتى لو كان جديدًا إلى جافا سكريبت، ومع ذلك فإن اللغة أكثر دقة وقوة وتعقيدًا مما قد يعتقده المرء في البداية؛ في الواقع، يمكن أن تؤدي العديد من التفاصيل الدقيقة لجافا سكريبت إلى عدد من المشكلات الشائعة التي تمنعها من العمل، وسنناقش 10 منها هنا. من المهم أن تكون على دراية بهذه المشكلات وتتجنبها أثناء رحلتك لتصبح مطور جافا سكريبت محترفًا. مشكلة جافا سكريبت الأولى: المراجع المغلوطة لـ this ستجد دومًا ارتباكًا بين مطوري جافا سكريبت فيما يتعلق بـكلمة جافا سكريبت المفتاحية this مع تطوّر وزيادة تعقيد تقنيات برمجة جافاسكريبت وأنماط التصميم Design Patterns، بات هناك زيادة مقابلة في انتشار نطاقات المرجعية الذاتية self-referencing scopes ضمن دوال رد النداء callbacks والمنغلقات closures، التي تعد المصدر الأساسي إلى حد ما للارتباك مع this الذي يسبب مشاكل جافا سكريبت. انظر إلى المثال التالي: const Game = function() { this.clearLocalStorage = function() { console.log("Clearing local storage..."); }; this.clearBoard = function() { console.log("Clearing board..."); }; }; Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // What is "this"? }, 0); }; const myGame = new Game(); myGame.restart(); تظهر نتائج تنفيذ الشيفرة البرمجية أعلاه الخطأ التالي: Uncaught TypeError: this.clearBoard is not a function لماذا؟ الأمر كله يتعلق بالسياق فسبب حدوث هذا الخطأ هو أنه عند استدعاء الدالة setTimeout()، فإنك في الواقع تستدعي window.setTimeout(). ونتيجة لذلك، تُعرَّف الدالة المجهولة anonymous function التي تُمرَّر إلى setTimeout() في سياق كائن window الذي لا يحتوي على تابع بالاسم ClearBoard(). الحل التقليدي لهذه المشكلة والمتوافق مع المتصفحات القديمة هو حفظ المرجع إلى this في متغير يمكن بعد ذلك توريثه عبر دالة منغلقة closure function، على سبيل المثال: Game.prototype.restart = function () { this.clearLocalStorage(); const self = this; // Save reference to 'this', while it’s still this! this.timer = setTimeout(function(){ self.clearBoard(); // Oh, OK, I do know who 'self' is! }, 0); }; ويمكن أن نستخدم حلًا مختلفًا في المتصفحات الأحدث وذلك يتضمن استخدام التابع ()bind كما يلي: Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // Bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // OK, back in the context of the right 'this'! }; مشكلة جافا سكريبت الثانية: الاعتقاد بوجود نطاق على مستوى كتلة شيفرة أحد أسباب حدوث ارتباك شائع بين مطوري جافا سكريبت (وبالتالي مصدر شائع للأخطاء) هو افتراض أن جافا سكريبت تنشئ نطاقًا جديدًا لكل كتلة تعليمات برمجية. فبالرغم من أن هذا الأمر صحيح في العديد من اللغات الأخرى، إلا أنه ليس صحيحًا في لغة جافا سكريبت، خذ بعين الاعتبار الشيفرة البرمجية التالية: for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // What will this output? إن كنت تظن أن استدعاء console.log() سيعطي خرجاً undefined أو خطأً ما، فتخمينك خاطئ. صدق أو لا تصدق، سيكون الخرج هنا هو 10. لماذا؟ في معظم اللغات البرمجية الأخرى، ستتسبب الشيفرة البرمجية السابقة في حدوث خطأ برمجي لأنّ دورة حياة المتغير (أو نطاقه) مقيّدةٌ ضمن كتلة for. أما في لغة جافا سكريبت، فالمتغير i يبقى داخل النطاق بعد اكتمال حلقة for، محتفظاً بالقيمة الأخيرة بعد الخروج من الحلقة. (يُعرف هذا التصرف باسم رفع المتغيرات) إن دعم النطاقات على مستوى الكتل موجود في جافا سكريبت عبر كلمة let المفتاحية. وكلمة let كانت مدعومةً بشكلٍ كبير من قبل المتصفحات وبيئنات تشغيل جافا سكريبت مثل Node.js منذ سنوات. وإن كان هذا جديداً عليك، حان الوقت لتتعلم المزيد حول نطاق المتغيرات scopes وكائنات prototype في جافا سكريبت. مشكلة جافا سكريبت الثالثة: التسبب بتسريب الذاكرة يعد تسرب الذاكرة memory leak مشكلةً حتمية تقريبًا في جافا سكريبت إن لم تكن تبرمج بتركيز لتجنبها، وهناك طرق عديدة لحدوث ذلك، ولكننا سنسلط الضوء فقط على اثنتين من أكثر حالات حدوثها شيوعًا. تسريب الذاكرة - المثال1: المراجع المعلقة للكائنات المحذوفة لنضع بعين الاعتبار هذه الشيفرة البرمجية: var theThing = null; var replaceThing = function () { var priorThing = theThing; // Hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // Create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // Invoke 'replaceThing' once every second إن نفّذت الشيفرة البرمجية السابقة وراقبت استخدام الذاكرة، ستجد أنّ لديك تسريباً كبيراً للذاكرة - حوالي ميغابايت في الثانية الواحدة! لدرجة أن كانسات القمامة لن تكون قادرة على المساعدة. ما يعني أنّه يتم تسريب longStr في كلّ مرةٍ نستدعي فيها replaceThing. ولكن لماذا؟ لنفحص الشيفرة بشكلٍ أدقّ: يتضمن كلّ كائن theThing على كائن longStr بحجم ميغابايت واحد. عندما نستدعي replaceThing في كلّ ثانية فإنّه يحتفظ بالمرجع الخاص بكائن theThing السّابق داخل priorThing، هل من الممكن أن تكون هذه هي المشكلة؟ ذلك من المستبعد، إذ يتم بشكل مسبق تحصيل dereference (أي عملية إلغاء المرجع) priorThing في كلّ مرةٍ (عندما نعدّل قيمة priorThing عبر السطر priorThing = theThing;). بالإضافة إلى أنه ذلك، فنحن نحصل على قيمة priorThing داخل الدالة replaceThing وفي تابع unused، ما يعني أنه لم يُستخدم. ما يعني أنّنا لم نجد الإجابة على سؤالنا حول تسريب الذاكرة هنا. نحتاج إلى فهمٍ أعمق لآلية عمل جافا سكريبت الداخلية لفهم ما يحدث. عادةً ما تُطبَّق الدوال المغلّفة عبر كلّ دالة ضمن كائن متصل بكائنٍ بنمط القاموس الذي يمثّل النطاق المعجميّ lexical scope. إن استخدمت كلٌّ من الدالتين اللتين تم تعريفهما داخل replaceThing تعليمة priorThing، سيكون من المهمّ أن تحصل كلاهما على الكائن نفسه، حتى لو تم تعيين priorThing مرارًا وتكرارًا بحيث تشترك كلتا الدالتين في البيئة المعجمية lexical environment (البيئة التي تحتوي على معلومات حول المتغيرات المحلية والعامة بالإضافة إلى الدوال المتاحة في نطاق البرنامج عند تعريف الدالة) ذاتها. ولكن ما إن تستخدم المتغيرات من قبل أيٍّ مغلّف، ستصل إلى البيئة المعجميّة المشتركة بين جميع المغلّفات في هذا النطاق. وهذا الفارق الدقيق هو ما يؤدي إلى تسريب الذاكرة هذا. تسريب الذاكرة - المثال 2: المراجع الذاتية لنرى هذا الجزء من الشيفرة البرمجيّة: function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } } لتعليمة onClick هنا مغلّفًا يحافظ على مرجعٍ إلى element (عبر element.nodeName). وبإسناد onClick إلى element.click يُنشأ المرجع الدوار أي يصبح لدينا: element →onClick → element → onClick → element… ومن المثير للاهتمام أنّه حتّى إن أزلنا element من DOM، فسيمنع المرجع الذاتي circular reference (الذي يشير لنفسه بشكل دوري) أعلاه من أن نحصل على قيمة element و onClick ما يؤدي إلى تسريب الذاكرة. تجنّب تسريب الذاكرة: الأساسيّات تعتمد إدارة ذاكرة جافا سكريبت (وبشكلٍ خاصّ كنس البيانات المهملة ) بشكلٍ كبير على الوصول إلى الكائن. من المفترض أن تكون الكائنات التالية قابلة للوصول كما تعرف باسم "الجذور roots": الكائنات المشار إليها من أي مكان في مكدس الاستدعاءات call stack الحالي (أي جميع المتغيرات والمعاملات المحلية في الدوال التي تُستدعى حاليًا، وجميع المتغيرات في نطاق المغلّف). كلّ المتغيرات العامة. يُحتفظ بالكائنات في الذاكرة طالما يمكن الوصول إليها من أي من الجذور من خلال مرجع أو سلسلة من المراجع. يوجد في المتصفح أداة كنس مهملات تنظف الذاكرة التي تشغلها الكائنات غير القابلة للوصول؛ بمعنى آخر، تُزال الكائنات من الذاكرة فقط إذا اعتقد كانس المهملات أنه لا يمكن الوصول إليها. لسوء الحظ، من السهل أن تخلص بوجود كائنات "زومبي" قديمة لم تعد قيد الاستخدام ولكن لا يزال كانس المهملات يعتقد أنه يمكن الوصول إليها. مشكلة جافا سكريبت الرابعة: عدم فهم المساواة إحدى وسائل جافا سكريبت المرنة هي أنها ستحوّل أي قيمة في سياق بولياني إلى قيمة بوليانية تلقائيًا، ولكن في بعض الحالات يسبب ذلك إرباكًا بدلًا من توفير مرونة في التعامل، ومن المعروف أن التعبيرات التالية مزعجة للعديد من مطوري جافا سكريبت، على سبيل المثال: // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ... على الرغم من كون العنصرين الأخيرين فارغين (مما قد يقودك إلى الاعتقاد بأنهما سيُقيَّمان إلى false)، فإن كلاً من {} و [] هما في الواقع كائنان، وسيُحوَّل أي كائن قسريًا إلى قيمة بوليانيّة مساوية إلى true في جافا سكريبت بما يتوافق مع معايير ECMA-262. وكما توضح هذه الأمثلة، فإن قواعد تحويل النوع القسريّ يمكن أن يكون في بعض الأحيان صعب الفهم. وفقًا لذلك، ما لم تكن ترغب بذلك بشكل صريح، فمن الأفضل عادةً استخدام === و ==! (بدلاً من == و =!) لتجنب أي آثار جانبية غير مقصودة لتحويل النوع القسريّ. (== و =! ينفذان تحويل النوع تلقائيًا عند مقارنة شيئين، بينما === و ==! يقومان بنفس المقارنة بدون تحويل النوع). وبما أنّنا نتحدّث عن تحويل النوع القسري والمقارنة بين النوعين، من المهمّ ذكر أن مقارنة NaN مع أيّ شيء (حتّى NaN!) سيعيد دائمًا false، بالتالي لن تستطيع استخدام عوامل المساواة (==، ===، !=، !==) لتحديد إذا ما كانت القيمة NaN أم لا، وعليك أن تستخدم بدلاً من ذلك الدالة العامة المبنية مسبقًا ()isNaN: console.log(NaN == NaN); // False console.log(NaN === NaN); // False console.log(isNaN(NaN)); // True مشكلة جافا سكريبت الخامسة: التلاعب بنموذج تمثيل المستند DOM غير الفعّال تسهّل جافا سكريبت التعامل مع DOM بشكل نسبي (كإضافة العناصر وتعديلها وإزالتها)، ولكنها لا تفعل ذلك مع التشديد على القيام بذلك بكفاءة. ومن أكثر الأمثلة شيوعًا هو الشيفرة البرمجية التي تضيف سلسلة من عناصر DOM واحدًا تلو الآخر، إذ تعد إضافة عنصر DOM عملية مكلفة، والتعليمات البرمجية التي تضيف عناصر DOM متعددة على التوالي غير فعالة ومن المحتمل ألا تعمل بشكلٍ جيد. أحد البدائل الفعالة عند الحاجة إلى إضافة عناصر DOM متعددة هو استخدام أجزاء المستند فقط، ما يؤدّي إلى تحسين الكفاءة والأداء. كمثال: const div = document.getElementById("my_div"); const fragment = document.createDocumentFragment(); const elems = document.querySelectorAll('a'); for (let e = 0; e < elems.length; e++) { fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true)); بالإضافة إلى تحسين الكفاءة لهذا النهج، فإن إنشاء عناصر DOM المرفقة أمر مكلف، في حين أن إنشائها وتعديلها أثناء فصلها ثم إرفاقها يؤدي إلى أداء أفضل بكثير. مشكلة جافا سكريبت السادسة: الاستخدام الخاطئ لتعريف الدوالّ داخل حلقات for لننظر إلى هذه الشيفرة البرمجيّة: var elements = document.getElementsByTagName('input'); var n = elements.length; // Assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; } بناءً على الشيفرة البرمجيّة أعلاه، إن كان هناك 10 عناصر إدخال، فإن النقر على أيٍّ منها سيعرض "هذا هو العنصر #10"! لأنه عند استدعاء onclick لأي من العناصر، ستكون حلقة for المذكورة أعلاه قد اكتملت وستكون قيمة i تساوي 10 (لجميع العناصر). لنرى كيف يمكننا تصحيح مشكلة جافا سكريبت هذه لتحقيق السلوك المطلوب: var elements = document.getElementsByTagName('input'); var n = elements.length; // Assume we have 10 elements for this example var makeHandler = function(num) { // Outer function return function() { // Inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); } في هذه النسخة المنقحة ننفّذ makeHandler على الفور في كل مرة نمر فيها خلال الحلقة، وفي كل مرة يتم تلقي القيمة الحالية لـ i+1 وربطها بمتغير num محدد النطاق تعيد الدالة الخارجية الدالة الداخلية (التي تستخدم أيضًا متغير num المحدد هذا) ويتم تعيين onclick الخاصة بالعنصر على تلك الدّالة الداخلية. وهذا يضمن أن كل نقرة تتلقى وتستخدم قيمة i المناسبة (عبر المتغير num المحدد). مشكلة جافا سكريبت السابعة: الفشل في الاستفادة بشكل صحيح من الوراثة النموذجية يفشل عددٌ كبيرٌ من مطوري جافا سكريبت في فهم ميزات الوراثة النموذجيّة بشكل كامل، وبالتالي الاستفادة منها بشكل كامل. إليك مثالًا بسيطًا: BaseObject = function(name) { if (typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } }; يبدو ذلك واضحًا ومباشراً. إن أعطيت اسمًا، استخدمه، وإلّا فاضبط الاسم كـ "افتراضيّ default"، مثلاً: var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique' ولكن ماذا لو قمنا بهذا: delete secondObj.name; ثمّ سنحصل على: console.log(secondObj.name); // -> Results in 'undefined' ولكن أليس من الأفضل أن يعود هذا إلى "default"؟ يمكننا القيام بذلك بسهولة إذا عدلنا الشيفرة الأصلية للاستفادة من الوراثة النموذجية، على النحو التالي: BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default'; في هذه النسخة، ترث تعليمة BaseObject ملكيّة name من كائن prototype، الذي يوجد بشكلٍ افتراضيّ في تعليمة default، وبالتالي، إذا استدعي الباني بدون اسم، فسيعيّن الاسم افتراضيًا عبر default، وبالمثل إن أزيلت خاصية name من نسخة BaseObject، فستجرى عملية بحث في سلسلة النموذج الأولي واسترداد خاصية name من كائن prototype حيث تظل قيمته default. والآن نحصل على: var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default' مشكلة جافا سكريبت الثامنة: إنشاء مراجع خاطئة لتوابع النُّسَخ Instance Methods لنعرّف كائنًا بسيطًا وننشئ نسخةً له كما يلي: var MyObjectFactory = function() {} MyObjectFactory.prototype.whoAmI = function() { console.log(this); }; var obj = new MyObjectFactory(); والآن لننشئ مرجعاً للتابع whoAmI لضمان سهولة العمل ولنتمكن من الوصول إليه فقط من خلال whoAmI() بدلاً من التابع ()obj.whoAmI الأطول: var whoAmI = obj.whoAmI; وللتأكّد؛ خزّنّا مرجعًا للدالة، لنطبع القيمة لمتغيّرنا الجديد whoAmI: console.log(whoAmI); الخرج: function () { console.log(this); } يبدو الأمر جيدًا حتّى الآن. ولكن لنرى الفرق عندما نستدعي ()obj.whoAmI بدلاً من مرجع السهولة الخاص بنا ()obj.whoAmI: obj.whoAmI(); // Outputs "MyObjectFactory {...}" (as expected) whoAmI(); // Outputs "window" (uh-oh!) ما الخطأ الذي حصل؟ استدعينا التابع ()obj.whoAmI في مساحة الاسم العامة، لذا فإن this تُضَمّ إلى window (أو في الوضع الدّقيق إلى undefined)، وليس إلى نسخة obj الخاصة بكائن MyObjectFactory! بمعنًى آخر، قيمة this تعتمد على سياق استدعائها. تقدم الدوالّ السهمية {} < = (params) بدلاً من {} (function (params this ثابتة وهذا ليس مبنيًّا على سياق الاستدعاء مثل this للدوالّ الطبيعية، مما يمنحنا الحلّ البديل: var MyFactoryWithStaticThis = function() { this.whoAmI = () => { // Note the arrow notation here console.log(this); }; } var objWithStaticThis = new MyFactoryWithStaticThis(); var whoAmIWithStaticThis = objWithStaticThis.whoAmI; objWithStaticThis.whoAmI(); // Outputs "MyFactoryWithStaticThis" (as usual) whoAmIWithStaticThis(); // Outputs "MyFactoryWithStaticThis" (arrow notation benefit) من الممكن أنّك لاحظت أنّه على الرغم من حصولنا على خرج مطابق إلا أن this هي مرجع لدالة المصنع factory التي تنشئ الكائن وليس مرجعًا إلى نسخة الكائن نفسها وبدلاً من محاولة إصلاح هذه المسألة، من المفيد التفكير في طرق التعامل مع جافا سكريبت التي لا تعتمد على this (أو حتى new) على الإطلاق. مشكلة جافا سكريبت التاسعة: استخدام سلسلة كوسيط أول لدالة setTimeout أو setInterval بدايةً؛ لنوضّح أمرًا ما: استخدام سلسلة كوسيط أول لدالة setTimeout أو setInterval ليس خطأً بحدّ ذاته. فهو شيفرةٌ برمجيّةٌ صحيحةٌ في جافا سكريبت، والمشكلة هنا تتعلق بالأداء والكفاءة. ما يتمّ التغاضي عنه غالبًا هو أنّه إن مررت سلسلة كوسيط أول إلى setTimeout أو setInterval، فإنها ستمرر إلى باني الدالة لتحوَّل إلى دالة جديدة، ويمكن أن تكون هذه العملية بطيئة وغير فعالة، ونادرًا ما تكون ضرورية. البديل لتمرير سلسلة كوسيط أول لهذه التوابع هو تمرير دالة بدلاً من ذلك. لنلقي نظرة على المثال التالي، هنا سنستخدم setInterval و setTimeout استخدامهما الاعتيادي، حيث نمرر سلسلة كمعامل أول: setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000); ويكون الخيار الأفضل في هذه الحالة هو تمرير دالة كوسيط أولي مثل: setInterval(logTime, 1000); // Passing the logTime function to setInterval setTimeout(function() { // Passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000); مشكلة جافا سكريبت العاشرة: الفشل في استخدام الوضع الصارم الوضع الصارم strict mode (بما في ذلك استخدام ;'use strict' في بداية ملفات جافا سكريبت المصدرية) هو وسيلة لفرض تحليل أكثر دقّة ومعالجة الأخطاء في الشيفرة البرمجية الخاصة بك في وقت التنفيذ أيضًا. كوسيلة لجعلها أكثر أمانًا. ورغم أن الفشل في استخدام الوضع الصارم لا يشكل "خطأً" حقيقيًا، إلا أنّ استخدامه يتزايد بشكلٍ كبير ويتم تشجيعه بشكلٍ كبير، كما يعدّ تجاهله أمرًا سيّئًا. هذه بعض الميّزات للوضع الصارم: يجعل تصحيح الأخطاء أسهل. يؤدي وجود الأخطاء في الشيفرة البرمجية التي كان من الممكن تجاهلها أو التي كانت ستفشل بصمت إلى إنشاء أخطاء أو رمي استثناءات، ما ينبهك بشكلٍ مسبق إلى مشاكل جافا سكريبت في مشروعك البرمجي ويوجهك بشكل أسرع إلى مصدرها. يمنع المتغيّرات العامة العرضية. يؤدي تعيين قيمة لمتغير غير معلن عنه تلقائيًا دون الوضع الصارم إلى إنشاء متغير عام بهذا الاسم. وهذا هو أحد أكثر أخطاء جافا سكريبت شيوعًا. وعند تفعيل الوضع الصارم يؤدي ذلك إلى إظهار خطأ. يوقف تغيير النوع التلقائي لـ this. بدون الوضع الصارم، يفرض مرجع إلى قيمة this الخالية أو غير المحددة تلقائيًا إلى المتغير globalThis. يمكن أن يسبب هذا العديد من الأخطاء. بينما في الوضع الصارم، تؤدي الإشارة إلى هذه القيمة الخالية أو غير المحددة إلى ظهور خطأ. يمنع تكرار أسماء الخاصيات أو قيم المعاملات. يظهر الوضع الصارم خطأً عند تحديد اسمين مماثلين لخاصية كائنٍ واحد (مثال؛ var object = {foo: “bar”, foo: “baz”};) أو اسمًا مكرّرًا لدالّة (مثال؛ function foo(val1, val2, val1){})، وبالتالي يساعد باكتشاف ما يكاد أن يكون خطأً في شيفرتك الذي ربما تكون قد أهدرت وقتًا طويلاً في تعقبه. يجعل ()eval أكثر أمانًا. هناك بعض الاختلافات في الطريقة التي تتصرف بها الدالة ()eval بين الوضعين الصارم وغير الصارم . والأهم من ذلك، في الوضع الصارم، لا يتم تُنشئ المتغيرات والدوال المعرّفة داخل تعليمة ()eval في النطاق المحتوي. (بل تُنشأ في النطاق المحتوي في الوضع غير الصارم، الذي يمكن أن يكون أيضًا مصدرًا شائعًا لمشاكل جافا سكريبت). يعطي خطأً عند استخدامٍ خاطئٍ لـ delete. لا يمكن استخدام عامل delete (الذي يستخدم لإزالة الخواص من الكائنات) على خواص كائنات غير قابلة للضبط nonconfigurable properties. ستفشل التعليمات البرمجية غير الصارمة بصمت عند إجراء محاولة لحذف خاصيةٍ غير قابلة للضبط، في حين أن الوضع الصارم سيعطي رسالة خطأ في هذه الحالة. التخفيف من مشاكل جافا سكريبت باستخدام نهج أكثر ذكاءً كما هو الحال مع أي تقنية، كلما فهمت كيفية عمل جافا سكريبت بشكلٍ أفضل، كلما كانت التعليمات البرمجية الخاصة بك أكثر اعتمادية، وكلما تمكنت من تسخير قوة اللغة بشكل أكثر فعالية. بالمقابل، فإن الافتقار إلى الفهم الصحيح لنماذج ومفاهيم جافا سكريبت هو المكان الذي تكمن فيه العديد من مشاكلها، والتعرف جيدًا على الفروق الدقيقة في اللغة وخفاياها هو الاستراتيجية الأكثر فعالية لتحسين كفاءتك وزيادة إنتاجيتك. أسئلة شائعة لنختم مقالنا بجملة من الأسئلة الشائعة حول جافاسكريبت: ما هي الأخطاء الشائعة في جافا سكريبت؟ الأخطاء الشائعة التي يرتكبها المطورون خلال البرمجة باستخدام جافا سكريبت تتضمن الفهم الخاطئ لكيفية عمل كلمة this المفتاحية، والافتراض الخاطئ حول النطاق الكتلي والفشل في تفادي تسريبات الذاكرة. تسببت مسيرة تطوير جافا سكريبت عبر الزمن بالعديد من الأخطاء في حال الاعتماد على أنماط البرمجة القديمة. كيف يمكنني تطوير مهاراتي في جافا سكريبت؟ يمكنك تطوير مهاراتك في جافا سكريبت عن طريق الالتزام بالممارسات المثلى عند كتابة الشيفرة البرمجية، وقراءة التفاصيل الخفية في لغة البرمجة لتكون على علم بمزاياها المتقدمة وأوجه قصورها. لمَ أكتب شيفرة برمجية معرضة بالأخطاء؟ قد تبدو الشيفرة البرمجية المعرضة للأخطاء على ما يرام عند النظر إليها للوهلة الأولى، إلا أنه وبتعلمك لهذه المشاكل الشائعة في جافا سكريبت فستبدي فهمًا أكبر للأنماط البرمجية التي تتسبب بالمشاكل وتتعلم طرق تفاديها في شيفرتك البرمجية. هل جافا سكريبت كثيرة المشاكل؟ قد يزعم البعض أن لغة جافا سكريبت بنفسها مسببة للمشاكل، وعلى الرغم من أنها تشكو من بعض أوجه القصور إلا أنها شائعة ومنتشرة في كل مكان، لذا تعلّم كيفية اكتشاف أخطائها وتصحيحها أمر يستحق الجهد إذا كنت ستعمل معها (وهذا هو الحال مع معظم المطوّرين في يومنا الحالي). ترجمة -وبتصرف- لمقال The 10 Most Common JavaScript Issues Developers Face لكاتبه Ryan J. Peterson اقرأ أيضًا كيفية التعامل مع الأخطاء البرمجية الزلات البرمجية والأخطاء في جافاسكريبت المتغيرات والعوامل والأخطاء البرمجية في لغة جافا كيف تصبح مبرمجًا محترفًا
-
نكتشف في هذا المقال الاختلافات بين مكتبة رياكت React وإطار العمل Next.js وهو إطار عمل مشهور لجافا سكريبت تستخدمه شركات عديدة مثل تيك توك TikTok وهولو Hulu ونايكي Nike لتحسين تطبيقات الويب الخاصة بها. كما سنتعرّف على متى وكيف نستخدم Next.js، ونسرد بعض النصائح حول مفهوم العرض أو التصيير rendering والتوجيه والتصفح والتنقل في هذه التطبيقات. يعدّ Next.js إطار عمل سريع جدًا تعتمد عليه المواقع التي تتطلب في عملها حجم بيانات كبير مثل هولو Hulu ونيتفلكس Netflix. فإذا كنت بالفعل ملمًا بمكتبة ريآكت React، فيجب أن تتعرف بالتأكيد على هذه التقنية التي تزداد انتشارًا يومًا بعد يوم، فعلى الرغم من أن كلاً من React و Next.js يساعدان في تصميم واجهات مستخدم فعالة لمواقع الويب، إلا أنّ لديهما بعض الاختلافات الرئيسية، حيث يعد Next.js أكثر تطورًا وتفصيلًا من React، وهو مناسب تحديدًا للمواقع التي تهتم بتحسين محركات البحث SEO أو التصيير المسبق pre-rendering. Next.js أم React ظهر إطار العمل رياكت React أو ما يعرف كذلك باسم ReactJS أو React.js لأول مرة في عام 2013، وهو أكثر شهرة وانتشارًا من Next.js بكثير، ولكن الإطار Next.js الذي صدر عام 2016 بدأ بالانتشار وازدات شعبيته شيئًا فشيئًا وحصل على أكثر من 117 ألف نجمة على جيتهاب GitHub حتى تاريخ شهر يناير 2024 إلى جانب ملايين التنزيلات الأسبوعية له من مدير الحزم npm. دعونا نرى مقارنة بسيطة في الأداء بين Next.js و React التي توضح متى يجب أن نستخدم Next.js ومتى نستخدم React: سرعة التطوير: يقدم Next.js ميزات حديثة ومبتكرة تسهل عملية التطوير لتصميم تطبيق React متقدم، ومع إدخال ميزة المجمّع compiler الخاص به في الإصدار Next.js 12، زادت سرعة التطوير فيه أيضًا بالمقارنة مع رياكت React، وبالتالي يقلّل Next.js من الوقت الذي ينتظره المبرمج لتحديث الشيفرة البرمجية، مما يقلّل من الإحباط والوقت الضائع خلال التطوير. جلب البيانات وأوقات التحميل: يمكن لإطار Next.js عبور شجرة React والاستعلام عن البيانات الموجودة في الخادم، مما يسمح بالتحميل المُسبق لبيانات الصفحة. وهذا يؤدي في كثير من الأحيان إلى تقليل أوقات تحميل التطبيق للصفحات المصممة باستخدام Next.js مقارنة بالصفحات المكتوبة بإطار React العادي فحسب. التصيير وتحسين محركات البحث: يتميز Next.js بميزة التصيير المُسبق، بينما يستخدم React التصيير المُعتمد على المستخدم وتوفر الصفحات المُصيَّرة والمعدة مسبقًا استراتيجيات تحسين محركات بحث فعالة الصعب تحقيقها في تطبيق React عادي. التوجيه: يوفر Next.js نظام ملفات مبني ومحدّد مسبقًا لعملية التوجيه routing. ويساهم هذا النظام في تقليل المرونة مقارنة بالخيارات المتنوعة لمكتبة React (مثل مكتبة React Router)، ولكنه يبسّط عملية إعداد الصفحة والتوجيه. يخدم React مجموعة متنوعة من أنواع المشاريع بشكل جيد جدًا، بما في ذلك واجهات المستخدم، وأنظمة تطوير الواجهات الخلفية، وأدوات التنظيم الداخلية، وأنظمة عرض البيانات. Next.js هي أداة مثالية لتحسين تطبيقات React التي تستفيد من عملية التصيير المسبق، بما في ذلك المتاجر الإلكترونية، وتطبيقات وسائل التواصل الاجتماعي، وأنظمة حجز التذاكر، والمنصات التعليمية. دعونا نستكشف بعض حالات استخدام Next.js بمزيد من التفصيل، حتى تتمكن من الإجابة عن السؤال "هل يجب أن أستخدم Next.js؟ في تطبيقات الويب أم لا" التصيير في Next.js التصيير rendering هو العملية التي تحول شيفرة React إلى HTML حيث يعرضه المتصفح كواجهة مستخدم ضمن الصفحة. يقدّم Next.js ثلاث طرق للتصيير التصيير المعتمد على المستخدم Client-side rendering أو CSR اختصارًا تصيير الخادم Server-side rendering أو SSR اختصارًا توليد الموقع الثابت static side generation أو SSG اختصارًا بالإضافة إلى إعادة التوليد الثابت التدريجي incremental static regeneration أو ISR اختصارًا. يجمع ISR بين تصيير الخادم مع آلية تخزين شبه ثابتة تخفف من الضغط على الخادم، وتوفر سرعات مشابهة لتلك التي يتم تحقيقها من خلال موقع ثابت. ويندرج تصيير الخادم وتوليد موقع ثابت تحت مظلة التصيير المسبق، حيث يتم إنشاء صفحات HTML قبل إرسالها إلى جهة المستخدم. ولعل الميزة الكبيرة التي يوفرها استخدام إطار Next.js هي أنه يضيف دعمًا قويًا لتصيير تطبيقات React. 1. تصيير المستخدم تصيير المستخدم هو التصيير الافتراضي لتطبيقات React الأساسية. تُولد هذه الطريقة صفحات HTML على جانب المستخدم. بمعنى آخر، تحدث عمليات التصيير في متصفح المستخدم، ويولد جافا سكريبت من جهاز المستخدم شيفرة HTML. تظهر واجهة المستخدم بعد اكتمال التصيير، عندما يكون الموقع تفاعلي أيضًا (أي، مُغذى hydrated). يكون استدعاء المستخدم ممكنًا لمكونات Next.js باستخدام useEffect أو useSWR في React. 2. تصيير الخادم يُتيح Next.js أيضًا إنشاء صفحة HTML على الخادم. في هذه الحالة، يتم إرسال شيفرة HTML المُولَّد إلى جانب المستخدم بحيث تظهر واجهة موقع الويب قبل التفعيل. ثم يكون موقع الويب القابل للعرض جاهزًا للاستخدام بعد اكتمال تهيئة جافا سكريبت على جانب المستخدم. تُضاف بعض دوال التكوين البسيطة إلى الصفحة في الصفحات التي نرغب في أن يؤدي Next.js تصيير الخادم فيها. 3. توليد موقع ثابت يقدّم Next.js أيضًا إمكانية توليد موقع ثابت، حيث يتم توليد جميع صفحات HTML الثابتة من JavaScript أثناء وقت البناء. ويتطلب توليد موقع ثابت من شيفرة React وقت بناء أكثر من تطبيق React ذي صفحة واحدة. ومع ذلك، الفائدة هنا هي وجود محتوى ثابت يمكن تخديمه وتخزينه بأقصى سرعة يسمح بها محتوى الموقع دون الأحمال الحسابية الزائدة لاستدعاء الخادم. يُمكننا تنفيذ SSG على صفحات Next.js التي نرغب في تصميمها بشكل ثابت باستخدام تعليمة getStaticProps() وتعليمة getStaticPaths()، والأخير يحدد المسارات للصفحات الثابتة. تحسين محركات البحث في Next.js تسمح السرعة والقدرة على استدعاء جميع صفحات موقع ويب في Next.js لمحركات البحث بتصفح وفهرسة الموقع بسرعة وسهولة، مما يعزز من عملية تحسين محركات البحث. وتحسين محركات البحث أمر ضروري للعديد من الشركات والمواقع الإلكترونية لأن المواقع التي تمّ تحسين محركات البحث فيها بشكل أفضل؛ تظهر في أعلى النتائج عند البحث. والمواقع ذات التصنيف الأعلى تكون أكثر احتمالًا للنقر عليها من قبل المستخدمين، حيث يصل معدل نقر المستخدمين إلى 27.6% للنتيجة الأولى، وهو معدل يزيد عشر مرات عن معدل نقر المستخدمين على النتيجة العاشرة والذي يبلغ 2.4%. في حين تواجه مواقع React التي تحتوي على كميات كبيرة من المحتوى - الذي يستخدم شيفرة جافا سكريبت للتصيير - تحديات في تحسين محركات البحث عند التعامل مع تصفح وفهرسة جوجل. ملاحظة: إن قدرة Next.js على التصيير من طرف الخادم بسهولة لا تُحسِّن فقط ترتيب الصفحة على محركات البحث فحسب، بل تُحسِّن أيضًا سرعة التحميل الفعلي للموقع من أجل تجربة مستخدم مثالية. البدء مع Next.js لنلقي نظرة على أساسيات تهيئة Next.js والتوجيه والتصفح والتنقل حتى تتمكن من جني فوائد التصيير المسبق وتحسين محركات البحث. قبل البدء تأكد من تحميل أحدث إصدار من Node.js على جهازك. يمكنك التحقق من إصدار Node.js على جهازك باستخدام الأمر node --version وهناك طريقتان لتهيئة مشروع Next.js: تهيئة تلقائية، مع ضبط محدد مسبقًا. تهيئة يدوية مع الضبط. سنتبع التهيئة التلقائية للبدء في مشروعنا بشكل أسهل. تدير أداة سطر الأوامر CLI أو create-next-app التهيئة التلقائية وتجعل من الممكن تصميم التطبيقات بسرعة. دعونا ننشئ مشروعنا مع دعم Next.js المدمج للغة TypeScript؛ وهي لغة مشتقة من جافا سكريبت تضمن نوع متغيرات صحيح: npx create-next-app@latest --typescript سيطلب create-next-app اسم تطبيقك. يجب أن يتكون اسم مشروعك من حروف صغيرة وأن يستخدم الشرطات بدلاً من المسافات. على سبيل المثال، اخترنا الاسم next-js-tutorial. بمجرد اكتمال التهيئة، سترى رسالة نجاح على شاشتك. يفرض Next.js نظامًا صارمًا لهيكلة الملفات، ففي مشروعنا الجديد سنجد الهيكلية التالية: تُنظم الصفحات pages وتنسيق الموقع styles في مجلداتها الخاصة. تُخزَّن واجهات برمجة التطبيقات أو API في مجلد pages/api. تحفظ البيانات المتاحة للعامة في المجلد public. التوجيه والصفحات في Next.js يستخدم Next.js هيكلة الملفات داخل مسار الصفحات pages لتحديد مسارات التطبيق. حيث نعرّف جميع المسارات في مجلد pages، ويُعدّ ملف pages/index.tsx هو نقطة الدخول للتطبيق حيث نحدد الخطوط المخصصة، وأكواد تتبّع التطبيق، وغيرها من العناصر التي تتطلب وصولًا عامًا. هناك طريقتان لإضافة مسارات جديدة: إضافة ملف ينتهي بـ .tsx مباشرة في المجلد pages. إضافة ملف index.tsx ضمن مجلد فرعي جديد من المجلد pages (الملفات index توجّه تلقائيًا إلى بداية المجلد). دعونا نختبر عدة أمثلة محددة لـ Next.js لعملية التوجيه. سننفذ مسار صفحة بسيط، ثم سنتطرق إلى مفاهيم التوجيه المتداخل والديناميكي. مسارات الصفحة يمكننا إضافة مسار صفحة أساسي، مثل about-us، باستخدام ملف about-us.tsx: |— pages | |— _app.tsx | |— about-us.tsx | |— api | |— index.tsx أو يمكننا استخدام ملف index.tsx ضمن مجلد pages/about-us: |— pages | |— _app.tsx | |— about-us | | |— index.tsx | |— api | |— index.tsx استمر وأضف مسار صفحة about-us.tsx إلى مشروعك. import styles from '../styles/Home.module.css' const AboutUs: NextPage = () => { return ( <div className={styles.container}> <main className={styles.main}> <h1 className={styles.title}> About Us Example Page </h1> </main> </div> ) } export default AboutUs سنرى توجيه الصفحة فعليًا عند استخدامها بالتزامن مع التنقل في Next.js. في الوقت الحالي، سنعيد NextPage ككلمة مؤقتة مع سلسلة عنوان حتى يعمل التنقل بشكل صحيح. المسارات المتداخلة تسمح المسارات المتداخلة nested routes بإعادة استخدام عدة تخطيطات اختيارية على صفحة (على سبيل المثال، عندما ينقر المستخدم على عنوان URL، قد ترغب في تحديث محتوى الجسم والاحتفاظ بتخطيطات الرأس والتذييل). |— pages | |— _app.tsx | |— api | |— index.tsx | |— parent | | |— child.tsx نحدد المسار المتداخل/parent/child باستخدام مجلد parent ومجلد أو ملف child متداخل (مثالنا يوضح ملفًا). المسارات الديناميكية تسمح المسارات الديناميكية dynamic routes للتخطيطات بالاستجابة للتغييرات في الوقت الحقيقي عندما لا يفي استخدام المسارات المعرفة مسبقًا pre-defined paths بالغرض. على سبيل المثال، إذا كنا نرغب في إنشاء مسار /product/[productId] (أي عند النقر على منتج، يتوسع مكونه). يُمكننا بسهولة إضافة مسار ديناميكي بافتراض أن productId هو متغير قابل للوصول في تعريفنا لمجلد product، وذلك عبر إنشاء مجلد product وتضمين صفحة productId بين قوسين: |— pages | |— _app.tsx | |— api | |— index.tsx | |— product | | |— [productId].tsx بهذه الطريقة، سيكون لديك مسار مثل product/testId ستُضبط فيه قوائم الإعدادات (أي productId يُضبط إلى testId). أخيرًا، من الممكن أيضًا دمج تقنيات التوجيه. على سبيل المثال، يُمكننا إنشاء مسار ديناميكي مُتداخِل pages/post/[postId]/[comment].tsx. التنقل في Next.js يستخدم Next.js مكونات Link خاصة به بدلاً من وسم <a> في HTML عند التنقل بين صفحات المستخدم للسماح لـNext.js بتحسين التنقل واسترجاع البيانات مسبقًا. يعمل Link بشكل مماثل لوسم <a> ويستخدم href كالمسار الذي سيتم فتحه. يجب عليك استخدام خاصية passHref لإجبار Link على تمرير قيمة مساره إلى المكونات الفرعية (على سبيل المثال، عند استخدام مكونات مخصصة المظهر). <p className={styles.description}> Here's our example <Link href="/about-us">About Us</Link> page </p> الفائدة الرئيسية من استخدام Link بدلاً من وسم <a> هي أنه يحمّل البيانات مسبقًا في الخلفية عندما يضغط المستخدم فوق الرابط أو بالقرب منه. يجعل هذا المحتوى متاحًا بشكل أكبر للمستخدم لمعالجته، مما يؤدي إلى تحسين أداء التطبيق. وما زال بإمكانك استخدام وسم <a> في Next.js عند الربط بصفحات خارجية خارج التطبيق. على سبيل المثال للربط بصفحة About Us من مشروع Next.js، افتح ملف التطبيق الرئيسي pages/index.tsx. سنستورد أولًا مكون الرابط Link من next/link، ثم نضيف فقرة مرتبطة تحت مكون<h1>: الآن يمكننا تشغيل التطبيق باستخدام الأمر npm run dev، اذهب إلى http://localhost:3000، ثم شاهد النص المضاف وصفحة About Us تعمل على http://localhost:3000/about-us (المسار المُعاد بعد النقر على About Us). الخلاصة هناك العديد من العوامل التي يجب مراعاتها قبل اختيار إطار عمل لموقع الويب القادم. على الرغم من أن Next.js أكثر تفصيلًا وأقل مرونة من React، إلا أن الإطار يوفر وظائف مبتكرة جاهزة للاستخدام لمشاريع متقدمة تستهدف تحسين محركات البحث أو إمكانيات العرض المسبق. باستخدام أساسيات Next.js ضمن أدواتك، يمكنك تعزيز موقعك والحصول على ميزات في تطبيقات React الأساسية. أسئلة شائعة نختم مقالنا بمجموعة من الأسئلة الشائعة المتعلقة بإطار عمل Next.js ورياكت React 1. لماذا أستخدم Next.js؟ يجعل Next.js من عملية بناء بعض واجهات مواقع الويب عملية أسهل كما يضيف مزايا على React، ويقدّم مجتمعًا قويًا وتوثيقًا جيدًا، كما أنه يكسب شعبية كبيرة بين مطوري الواجهات الأمامية. 2. هل React أفضل من Next.js؟ لا، React ليس أفضل من Next.js، يجب على المطورين أن يختاروا إطار العمل بناءً على ما يصلح لمشروعهم وبناءً على أولوياتهم (قد يستفيد موقع يركّز على تحسين محركات البحث من Next.js على سبيل المثال، وذلك بالنظر إلى أنه يقلّل من وقت تحميل التطبيق). 3. لم يُنظر إلى Next.js بكونه أفضل من React؟ يقدم Next.js تحسينات على سرعة التطوير ويقلل من وقت تحميل التطبيق، كما يحتوي على التصيير المسبق. 4. ما الفرق بين Next.js و React؟ إن Next.js إطار عمل مبني على React مع بعض المزايا المُضافة بشكل افتراضي، React هو مكتبة جافا سكريبت شائعة تُستخدم لبناء واجهات المستخدم التفاعلية، بالأخص في تطبيقات الويب. 5. ما هي المشاكل التي يحلها Next.js؟ يقدّم Next.js التصيير المسبق ويمكن أن يحسن من نتائج الموقع على محركات البحث مع تقليل وقت التحميل. 6. لم قد أختار React.js؟ يعدّ React إطارًا أقلّ تحكمًا وأكثر شعبية من Next.js ويقدم للمطورين زيادة في المرونة وموارد عديدة. 7. هل React وReact.js يشيران للشيء ذاته؟ نعم، كلا المصطلحين يشيران إلى مكتبة جافا سكريبت ذاتها وهي مكتبة JavaScript تُستخدَم لبناء واجهات المستخدم. 8. هل يعد Next.js إطار عمل؟ نعم، Next.js إطار عمل مفتوح المصدر مبني على React. ترجمة -وبتصرف- للمقال Next.js vs React: A Comparative Tutorial لصاحبه Ayyaz Ali اقرأ أيضًا كل ما تود معرفته عن إطار ريآكت نيتف React Native المصطلحات المستخدمة في React مصادر تعلم مكتبة React لبناء تطبيقات الويب مقدمة إلى Node.js
-
يعد انتشار الذكاء الاصطناعي والروبوتات أمراً مثيراً للفضول لمعظم الأشخاص حول العالم، فهو يعدهم بمستقبلٍ أفضل وأسهل للبشرية، فهو أحد سبل الراحة والرفاهية إضافةً إلى كونه يسهّل عمل العديد من الأشخاص في قطّاع التكنولوجيا. ولتستطيع صناعة وهندسة الروبوتات عليك أن تتقن اللغة التي تتحدثها وتتواصل مع العالم الخارجي من خلالها، وهنا يأتي دور لغات برمجة الروبوتات، التي تمنح الروبوتات القدرة والذكاء لإنجاز المهام بشكلٍ مشابهٍ للأسلوب البشريّ. سنتحدّث عن كيفيّة برمجة الروبوتات وأشهر اللغات المستخدمة في هذا المجال كما سنخبرك بالمسار الصحيح الذي يجب أن تسلكه لتكون مبرمج روبوتاتٍ ناجحاً وتشارك في بناء هذا المستقبل الواعد. ما هي برمجة الروبوتات تعرّف برمجة الروبوتات بأنها التعليمات المحددة المدخلة إلى نظام التحكّم الخاص بالروبوت لتنفيذ مهام معيّنة. وتحدّد هذه التعليمات الطريقة التي يعمل بها الروبوت والمهام التي يقدر على تنفيذها. وتتمّ هذه العملية بأسلوبين: برمجة الروبوتات بشكل مباشر: وذلك من خلال تحريك ذراع الروبوت، على سبيل المثال عبر سلسلة من الوضعيات التي تُسجَّل وتُحفظ في أنظمة التحكم الخاصة بالروبوت. برمجة الروبوتات بشكل منفصل: وهي عندما يقوم المبرمج بكتابة تعليمات دقيقة ومفصّلة على الحاسوب للتحكم بحركات الروبوت ومن ثمّ يحمل هذه التعليمات على الروبوت. ولبرمجة الروبوت، نستخدم العديد من اللغات البرمجية، وجميعها فعّالة وقادرة على أداء المهمة إلا أن أفضل لغات برمجة الروبوتات هي C أو C++ و Python و Java وغيرها من اللغات التي سنتعرف عليها بعد قليل، وتختلف اللغات المستخدمة باختلاف الشركات المصنعة التي غالباً ما تعتمد على برامجها الخاصة لبرمجة الروبوتات، ما يعني أنّ قدرتك على العمل مع روبوت من تصنيع شركة تجارية معينة لا يعني بالضرورة قدرتك على العمل مع روبوت مصنّع من قبل شركةٍ أخرى. لغات برمجة الروبوتات تنقسم اللغات التي سنناقشها هنا ما بين مستوى مبتدأ ومستوى متقدّم، ويعتمد قرارك على الاعتماد على إحدى اللغات بناءً على مستواك البرمجي والتقني عمومًا. ما يعني أنّك ستبدأ رحلتك في مجال برمجة الروبوتات من مرحلةٍ متقدّمة إن كنت مهندس برمجيات قبل ذلك. وسواء كنت متخصصًا أو مبتدئًا من المهم أن تتعلم اللغة التي ستوصلك إلى حيث تريد أن تكون في مجال الروبوتات وتضع حياتك المهنية على المسار الصحيح. المستوى المبتدئ بدايةً؛ لا تركّز على الجانب النظري فقط وتبدأ بقراءة كتاب نظري في محاولةٍ لتعلم الخوارزميات المتقدمة المستخدمة في هذا المجال، وإلّا ستشعر بالملل وتفقد شغفك بعد عدة أيام. بدلًا من ذلك اقرن المعلومات النظري بالتنفيذ العملي، وذلك بالعمل على المشاريع المتعلّقة ببرمجة الروبوتات وبهذه الطريقة يكون لديك هدف واضح ويصبح مسار التعلم الخاص بك ببساطة هو كل ما تحتاج إلى معرفته لتحقيق هدفك. لذا، أثناء قيامك بمشاريع رائعة، ستكتسب المعرفة كمنتج ثانوي! وفي هذه الأثناء ستتعلم لغة البرمجة الأنسب بحسب المشروع الذي تعمل عليه. لكن لنبسّط الأمر قليلًا؛ سنحدّثك عن أفضل لغات برمجة الروبوتات للمبتدئين التي تتميّز بخصائص تعطيها الأفضلية على لغات البرمجة الأخرى مثلسهولة الاستخدام، وعتمادها على كتل شيفرات برمجية جاهزة مسبقًا بدلاً من كتابتها من الصفر، واستخدامها لواجهات متناسبة مع خبرات المبتدئين من مجالات البرمجة. من أفضل لغات برمجة الروبوتات للمبتدئين نذكر: لغة سكراتش Scratch لغة بلوكلي Blockly لغة ليغو مايند ستورمز LEGO Mindstorms لنوضح أبرز مميزات كل لغة من هذه اللغات. سكراتش Scratch لغة سكراتش Scratch هي لغة برمجة تعتمد على سحب وإفلات كتل الشيفرات البرمجية بشكل أساسيّ، وهي مجانية تماماً ولا تتطلب ترخيصًا للاستخدام، فقد صممت لخلق بيئة عملٍ برمجية للطلاب المهتمين بمجال برمجة الروبوتات، ولسهولة الاستخدام، يمكن كتابة التعليمات البرمجية للغة سكراتش باستخدام محررها غير المتصل بالإنترنت أو تنزيلها على جهازك. حيث أنّ كل ما تحتاجه هو جهاز كمبيوتر ومتصفح ويب حديث. ما يميّز لغة سكراتش هو مجتمعها الموجّه للمبتدئين، لذا ستجد عشرات الدروس التعليمية والأمثلة على مختلف الجوانب والمزايا من سكراتش. يمكنك الاطّلاع على إمكانيات اللغة عن طريق تجربة مشاريع عملية نفّذها أعضاء من المجتمع والتعلّم منها عن طريق مراجعة الشيفرة الخاصة بالبرنامج، كما يمكنك البدء بتعلّم مزايا سكراتش عن طريق الأدلة التعليمية. بلوكلي Blockly هي لغة برمجة مرئية طوّرتها Google. وهي مشابهة في عملها للغة Scratch حيث تسمح للمستخدمين بإنشاء برامج عن طريق سحب وإفلات كتل الشيفرات، وبالتالي تكون البرمجة أسهل على المبتدئين. كما تتميز Blockly بأنها مناسبة بشكل خاص للروبوتات، حيث تمكن المطورين من إنشاء برامج معقدة دون الحاجة إلى كتابة الشيفرات البرمجية من الصفر. وهذا يجعلها خيارًا مثاليًا للمبتدئين، أو أولئك الذين قد لا يمتلكون خبرة واسعة في البرمجة. ورغم أنها لغة للمبتدئين إلا أنها قادرة على برمجة الروبوتات بدءًا من الروبوتات التعليمية البسيطة إلى الروبوتات الصناعية المتقدمة. كما تتيح للمبرمجين العمل بشكل تعاوني، حيث يمكن مشاركة الشيفرات وتحريرها داخل المنصة. ليغو مايند ستورمز LEGO Mindstorms هي لغة برمجة بصرية مصممة خصيصًا لبرمجة الروبوتات باستخدام مجموعة ليغو مايندستورمز. وكما اللغات السابقة، تعتمد على واجهة سحب وإفلات لكتل الشيفرات البرمجية، مما يسهل على المبتدئين استخدامها، حتى لو لم يكن لديهم أي خبرة سابقة في البرمجة. أحد الفوائد الرئيسية لهذه اللغة أنها سهلة التعلم والاستخدام. حيث تسمح واجهة السحب والإفلات للمستخدمين بتحديد وترتيب الأوامر والوظائف المختلفة بسهولة، دون القلق بشأن بنية الجملة المعقدة أو المفاهيم البرمجية. بالتالي يمكن للمستخدمين إنشاء مجموعة واسعة من البرامج والمشاريع باستخدام هذه اللغة، بدءًا من المهام البسيطة مثل تحريك الروبوت للأمام أو الخلف، إلى سلوكيات أكثر تعقيدًا مثل تجنب العقبات أو متابعة الخط. وتعد هذه اللغة من أفضل لغات برمجة الروبوتات للمبتدئين حيث تجعل واجهتها سهلة الاستخدام وقابلية التكيف والتفاعلية منها أداة رائعة للتعلم والتجربة، في حين تسمح قدراتها القوية للمستخدمين بإنشاء برامج معقدة ومتطورة كما يكتسبون المزيد من الخبرة والمعرفة. المستوى المتقدّم لا يمكنك أن تعتمد على لغات مثل LEGO Mindstorms أو Blockly لباقي مسيرتك المهنية كمبرمج روبوتات، فعليًا هذه اللغات لا تزيد عن كونها شيئًا سهلًا للبدء به، وبعد فهمك لطبيعة عمل الروبوتات وكيفية برمجتها لابدّ من البدء بتعلّم لغات برمجة الروبوتات الفعلية التي تقدم ميزاتٍ أكثر تخصصًا وتعليماتٍ إضافية لم تكن قادرًا على إعطائها للروبوت عند استخدامك للغات البرمجة المخصصة للمبتدئين. هناك عدّة لغات معتمدة لمبرمجي الروبوتات لإنشاء أنظمة روبوتية معقدة ومتطورة. تشمل بعض أشهر هذه اللغات: بايثون Python سي C ماتلاب Matlab سي بلس بلس C++ جافا Java ليسب LISP لنناقش كلّ لغةٍ على حدًى ونتحدث عن أهم استخداماتها في المجال. لغة بايثون وهي إحدى أسهل لغات البرمجة وأكثرها شعبيةً بين المستخدمين، ذلك لكونها تمتلك مجموعة قويّة من المكتبات التي تسهّل تنفيذ الوظائف والتعليمات الأساسية. كما أنّ هناك حاجة إلى عدد أقل من أسطر الشيفرات البرمجية مع Python أيضًا، ما يجعلها أسرع في الاستخدام والتعلم من C و C++ و Java. تعمل لغة Python على تقليل وقت البرمجة من خلال إلغاء الحاجة إلى تحديد أنواع المتغيرات، والسماح بكتابة الشيفرة البرمجية داخل البرنامج النصي ذاته. ونظرًا لاستخدامها الواسع وشعبيتها، تمتلك بايثون أيضًا مجتمعًا كبيرًا من المبرمجين، والذي يمكن أن يكون مصدرًا ممتازًا للمبتدئين للاستفادة منه أثناء مرحلة التعلم. يمكنك البدء بتعلم لغة بايثون من خلال دورة تطوير التطبيقات باستخدام لغة بايثون المقدّمة من أكاديمية حسوب. لغة سي C (باستخدام آردوينو) آردوينو Arduino هي إحدى المنصات الشائعة لبرمجة الروبوتات باستخدام لغة C، فهو متحكّم مفتوح المصدر يعتمد على أجهزة وبرامج سهلة الاستخدام. صمم ليكون متاحًا للمبتدئين مع التركيز على إمكانياته بحيث يمكن لمستخدميه المتقدمين بناء مشاريع معقدة. باستخدام لغة C مع آردوينو، يمكن للمبرمجين الاستفادة الكاملة من قدرات المتحكم الصغير Microcontroller وتنفيذ خوارزميات وأنظمة تحكم مخصصة، كما تسمح لغة سي للمبرمجين بكتابة شيفرات فعالة ومحسّنة لتطبيقات الروبوتات ، مثل التحكم في المحركات والحساسات وغيرها. بالإضافة إلى ذلك، يوفر أردوينو مجموعة واسعة من المكتبات والأدوات التي تبسط عملية التطوير، وهذا يجعلها منصة مثالية للهواة والطلاب والمحترفين على حدٍّ سواء. بشكل عام، لغة C هي أداة قوية لبرمجة الروبوتات، واستخدامها مع أردوينو يجعلها متاحة لجمهور أوسع وبالتالي الاستفادة منها بشكل أكبر. لغة ماتلاب MATLAB تعد ماتلاب من أفضل لغات برمجة الروبوتات على الإطلاق فهي قادرة على تحليل البيانات وتشغيل المحاكاة ، وتطوير وتنفيذ أنظمة التحكم في الروبوتات بواسطة واجهات مصممة خصيصًا لأجل هذه المهمّة. عند استخدامها مع مجموعة أدوات Robotics Toolbox for MATLAB، التي تتضمن وظائف للحركة والديناميات وتوليد المسارات، كما يمكن للمطورين إنشاء أنظمة لمحاكاة ذراع الروبوت. تدمج هذه اللغة بين البيئة سهلة الاستخدام والكفاءة العالية وتحقيق أكبر قدرٍ ممكن من الاستفادة حيث عندما يتمّ تحليل المعلومات وبناء أنظمة التحكم. كما تساعد على التخلص من الأخطاء التي تظهر عند التنفيذ وذلك عبر السماح للمبرمجين بتحديد المشكلات أثناء مرحلة النمذجة الأولية بدلًا من اكتشافها لاحقًا بعد إنتاج الروبوت وتصنيعه مما سيكبّدك كلفةً أكبر وعملًا أكثر. لغة سي بلس بلس ++C تقف لغتا C و C++ إلى جانب بعضهما البعض لتشكّلا اللغتين الأساسيتين في عالم برمجة الروبوتات فهي توفّر مجموعةً كبيرة ومتنوعة من الأدوات والمكتبات والوظائف المفيدة في مجال الروبوتات. حيث يستخدمها معظم مهندسي الروبوتات الذين يعملون على برمجة الروبوتات بكفاءة وضمان أداءٍ عالٍ واستثمارها بالشكل الأمثل. واحدة من أعظم مزايا ++C كونها اللغة التي تتحكم في واجهة برمجة التطبيقات API لنظام التشغيل مباشرة. وهذا يعني أنها لا تحتاج إلى أي وسيط. بالتالي، يمكن للمبرمجين استخدام المكتبات السريعة الخاصة بالمنصة الأساسية. لغة البرمجة جافا تحظى لغة جافا بشعبية بين المبرمجين العاملين في مجال الذكاء الاصطناعي وبرمجة الروبوتات دونًا عن غيرها من لغات البرمجة حيث يمكن إنشاء شبكات عصبية من خلالها. ومع ذلك، لا تعدّ جافا الخيار الأول لبرمجة الروبوتات بسبب تكاليف تشغيل مكتبات جافا العالية وبطء سرعة المعالجة وقواعد بناء الجمل المعقدة. بالإضافة إلى تقديمها لبعض المزايا الفريدة في الروبوتات، مثل إدخال كشف الكلام وكشف اللغة في روبوتك باستخدام واجهة (Java Speech API) (JSAPI)، كما يمكنك استدعاء ردود الفعل البصرية من الروبوت باستخدام واجهة Computer Vision API of Java. بالإضافة إلى ذلك ، تحتوي جافا على مجموعة واسعة من واجهات برمجة التطبيقات API للذكاء الاصطناعي المصممة خصيصًا لاستخدامها في برمجة الروبوتات. ميزة أخرى لجافا هي آلة جافا الافتراضية Java Virtual Machine التي تسمح للمبرمجين بتقليل وقت البرمجة الإجمالي عن طريق السماح لهم باستخدام الشيفرة البرمجية ذاتها على أجهزة مختلفة. ليسب LISP هي إحدى أقدم لغات البرمجة المستخدمة لبرمجة الروبوتات وتستخدم لإنشاء تطبيقات الذكاء الاصطناعي حيث يتم كتابة نظام ROS - الإطار المفتوح المصدر المستخدم لتطوير تطبيقات الروبوت - بهذه اللغة. وتتميّز بإدارة التخزين التلقائي، والكتابة الديناميكية، وهياكل البيانات الشجرية، والتكرار، ووظائف الترتيب المرتفعة، والترجمة ذاتية الاستضافة وغيرها الكثير. طورت لغة LISP في الأصل للتدوين الرياضي التطبيقي في برامج الحاسوب. ومع ذلك، ستجد أن العديد من الأقسام الهامة لنظام ROS مكتوبة بلغة LISP. ولهذا السبب أصبحت أيضًا لغة حاسمة في مجال الذكاء الاصطناعي. وكلّ هذه الميزات تضع لغة LISP في مقدمة قائمة أفضل لغات برمجة الروبوتات. برامج محاكاة الروبوتات عالم برمجة الروبوتات يشكل المستقبل الواعد للمجال البرمجي، إلا أنه يأتي مع سلبيته الأبرز ألا وهي ارتفاع أسعار الروبوتات بشكلٍ يمنع المبرمجين من القدرة على القيام بالتجارب واكتساب الخبرات، ومن هذا المنطلق تمّ العمل على برامح محاكاة الروبوتات التي تُعرف بكونها برامج طوّرت لتخطيط مسار حركة الروبوت دون الاتصال بالإنترنت حيث تمثّل هذه البرامج حركة الذراع بدقة. يتضمن هيكل الروبوت العديد من المكونات بالإضافة إلى الذراع، وبالتالي يجب أن تكوّن فهمًا كاملًا لكيفية عمل الهيكل وتفاعله مع محيطه، مما يساعدك في تحديد فرص التحسين، وضمان تضمين كل عنصر مهم في الهيكل. يمكن فهم عمل الخلية بشكلٍ شامل باستخدام برنامج محاكاة مصنّع مع نموذج كامل للخلية، إذ يمكن برمجة الروبوت بشكل افتراضي. بحيث تتكامل حركات الروبوت مع المستشعرات وغيرها من المحفزات. أهمية برامج محاكاة الروبوتات تطورت عملية محاكاة الروبوتات بشكل طرديّ مع مرور الوقت لمواكبة القدرات المتزايدة للروبوتات الصناعية، بالإضافة إلى زيادة الطلب على الروبوتات المتقدمة من هذا النوع بما يتناسب مع زيادة تعقيد المنتج وتنوعه والتخصص لتلبية طلبات العملاء. وعند التفكير بالأمر، فإن السبب الرئيسي لاستخدام برامج محاكاة الروبوتات هو قدرتك على تصور العملية، على سبيل المثال، قد يكون أهم ما تحتاجه هو عرض كيفية عمل نظام الروبوت لفريقك. تسمح المحاكاة لزملائك في العمل والإدارة بتصور العملية. ويساعد على رؤية كيفية عمل الروبوت. إضافة إلى الكلفة المادية الأخفض مقارنةً مع أجزاء الروبوتات والقدرة على حل المشاكل والأخطاء قبل الوصول إلى مرحلة الإنتاج النهائية. برامج محاكاة الروبوتات تستمر برامج محاكاة الروبوتات في التطور كل عام، ما يضيف قيمةً ونجاحًا أكبر إلى مجال برمجة الروبوتات وخاصةً مع إطلاق محاكيات روبوتية مجانية مفتوحة المصدر بدأت في منافسة أداء البرامج التجارية. وتدعم معظم برامج محاكاة الروبوتات أيضًا مجموعة واسعة من لغات البرمجة مثل C و C++ وJava وMATLAB و LabVIEW و Python وأهمّها: تطبيق Webots Webots هو تطبيق متعدد المنصات ومفتوح المصدر يستخدم لمحاكاة الروبوتات. يوفر بيئة تطويرية كاملة لنمذجة وبرمجة ومحاكاة الروبوتات ويدعم مجموعة واسعة من المحاكيات بما في ذلك الروبوتات ذات العجلتين والأذرع الصناعية وروبوتات المشي والروبوتات النمطية والسيارات والطائرات بدون طيار والمركبات تحت الماء ذاتية الدفع والروبوتات المسارية ومركبات الفضاء وما إلى ذلك. ويمكن برمجة الروبوتات باستخدام هذا المحاكي بلغات C و C++ وبايثون Python و Java و MATLAB باستخدام واجهة برمجة تطبيقات بسيطة تغطي جميع الاحتياجات الأساسية للروبوتات. محاكي Gazebo محاكي Gazebo هو منصة مفتوحة المصدر ومجانية يمكن استخدامها لتصميم وتطوير واختبار ومحاكاة أي نوع من الروبوتات وهو مدعوم من قبل أنظمة تشغيل Linux و Windows و Mac، يأتي محاكي Gazebo أيضًا مع بعض نماذج الروبوتات مثل PR2 و DX و Irobot Create و TurtleBot، لتبدأ بسرعة حتى لو لم يكن لديك نماذج روبوت خاصة بك. كما يدعم Gazebo مجموعة واسعة من الحساسات، ويمكنك محاكاة الضوضاء وفشل الحساسات لمحاكاة المشاكل الحقيقية في العالم. محاكي V-REP أو CoppeliaSim يتوفر هذا المحاكي مجاناً لأغراض تعليمية، أو من الممكن أن تختار الإصدار الاحترافي إذا كنت تخطط لاستخدامه لمشاريع تجارية. يمكن تشغيل V-REP على أنظمة Windows و Linux و macOS، كما يدعم ستّ لغات برمجةٍ مختلفة. يمكنه التعامل بدقة مع تفاعلات الأجسام مثل التصادمات ونقاط الاتصال والتقاط. كما يدعم V-REP فيزياء الجسيمات لمحاكاة الهواء والماء بدقة، حتى تتمكن من نمذجة محركات الطائرات والمروحيات بدقة. محاكي NVIDIA ISAAC هذا هو محاكي روبوتات قابل للتوسعة، إذ يمكّن المطورين والباحثين من تصميم واختبار وتدريب الروبوتات القائمة على الذكاء الاصطناعي. يُشغَّل بواسطة Omniverse لتوفير بيئات افتراضية قابلة للتوسعة ومفصلة ودقيقة فيزيائياً. كما يدعم مجموعة متنوعة من التطبيقات بما في ذلك التلاعب والتنقل وإنشاء بيانات اصطناعية لتدريب البيانات. يدعم NVIDIA ISAAC وحدات البرامج التي يطلقون عليها GEMs. تتيح هذه الوحدات لك إضافة ميزات بسرعة إلى روبوتاتك مثل أنظمة التعرف المرئي للشبكات العصبية المدربة مسبقًا. محاكي Virtual Reality Simulator محاكي العوالم الافتراضيّة يوفّر تجربة غنيّة للمستخدمين، فهو يهدف في المقام الأول إلى استخدامه كمنصة تعليمية لتعليم الشباب أساسيات الروبوتات. كما تدعم عوالم الروبوت الافتراضية برمجة روبوتات LEGO Mindstorms باستخدام NXT-G أو LabVIEW. يمكنك أيضًا إنشاء نماذج روبوت مخصصة أو مستويات في المحاكي باستخدام نظام التمديد المدمج. مسار تعلّم مبرمج الروبوتات هندسة الروبوتات هي مجال ينمو يوماً بعد يوم يتضمن مزيجًا من تحليل البيانات والهندسة وعلوم الحاسوب. يستخدم الأشخاص العاملون في هذه المجالات البرامج والأجهزة الميكانيكية لتصميم وبناء واختبار الروبوتات وعملياتها المرتبطة بالآلات. ومع ذلك، فإن كل وظيفة في هندسة الروبوتات فريدة من نوعها، وغالبًا ما تعتمد الجوانب العملية للوظيفة في هذا المجال على خلفية الشخص. على سبيل المثال، يميل مهندسو الروبوتات الذين لديهم خلفية في البرمجة إلى التركيز أكثر على الجانب البرمجي، بينما يميل أولئك الذين لديهم خلفية في الهندسة الميكانيكية إلى العناصر الفيزيائية للروبوت. ما يعني أنّ المسار إلى هذا المجال يشمل عمومًا أربع خطوات رئيسية: الحصول على التعليم. كسب الخبرة. الانضمام إلى المشاريع ذات الصّلة. تقديم طلبات العمل. أولًا: دراسة هندسة الروبوتات هندسة الروبوتات هي مجالٌ جديد نوعاً ما، لذلك لا يوجد سوى عدد قليل من البرامج الرسمية المصممة لإعداد المتعلمين للحصول على وظيفة في هذا المجال. ومع ذلك، هناك العديد من المسارات البديلة المتاحة لك في هذا المجال - حتى تتمكن من اختيار المسار الذي يناسبك. والخطوة الأولى نحو مهنة في الروبوتات هي دراسة اختصاص الرياضيات أو الهندسة أو علوم الحاسوب. مع العلم أنّ؛ دراسة علوم الحاسوب تساعد على إعدادك لجوانب البرمجة في العمل، بينما ستساعد دراسة هندسة الميكانيك على إعدادك لبناء عتاد الروبوت وضبط تحركاته. ثانيًا: اكتساب المهارات المطلوبة بمجرد أن تدرس المجال أو الموضوع الذي تختاره، من المهم تطوير مهاراتك الشخصية، أو التدرب من خلال مشاريع شخصية في وقت فراغك، أو الحصول على تدريب في شركة روبوتية. وبعد الانتهاء من برنامج التدريب الخاص بك، قم بإجراء استبيان للمهارات التي اكتسبتها وقارنها بالمهارات المطلوبة لمهندس الروبوتات. تذكر أنه يجب عليك أن تكون ملمًا بالبرمجة والإحصاء والرياضيات والهندسة والأتمتة. وبحسب نقاط الصعف لديك يمكنك أن تعرف المجال الذي يجب أن تقوّي نفسك فيه وتسجل دوراتٍ دراسيّة إضافية. ثالثًا: الانضمام إلى تجمّع مهني يتعلق بالمجال الانضمام إلى المشاريع والجماعات المهنيّة لا يقدّم لك عملاً وحسب، بل يضيف خبرةً ويجعل منك مهندساً أفضل وأكثر مهارة. تأسست جمعية تقدُّم الذكاء الاصطناعي (AAAI) في عام 1979، وهي جمعية غير ربحية تركز على الذكاء الاصطناعي، ملتزمة بـ "تعزيز الفهم العلمي للآليات التي تكمن وراء الفكر والسلوك الذكي وتجسدها في الآلات"، وكذلك جمعية الروبوتات والأتمتة IEEE، والانضمام إلى إحدى هذه الجمعيات من خلال حضور ورش العمل والمؤتمرات القيمة، يؤدي إلى إتاحة فرصة الوصول إلى آلاف من مهندسي الروبوتات للتعلم منهم وإضافتهم إلى شبكة اتصالاتك المهنية. رابعًا: التقدم للوظائف تعتبر الروبوتات مجالًا تنافسيًا، ما يعني أنه عند التقدم للوظائف، يجب أن تكون مفكرًا واستراتيجيًا. ما هي الوظائف التي تهتمّ بها؟ وما هي المؤهلات التي لديك؟ وعند بناء سيرتك الذاتية، تأكد من التركيز على جميع الخبرات والدروس ذات الصلة بالروبوتات، سواء كان ذلك تخصص رياضيات في الجامعة أو روبوت قمت ببنائه كمشروعٍ خاص بك. وحضّر نفسك لمقابلات العمل المحتملة، مثلاً استعرض بعضًا من أهم الأسئلة التي تطرح في مقابلات الروبوتات على LinkedIn؛ ستساعدك هذه الأسئلة على ممارسة الحديث عن بعض الموضوعات الأكثر أهمية التي قد يغطيها المقابل، ليكون لديك فهمٌ واضح عن هذه الوظيفة، وما يجعلك مؤهلاً جيدًا لها. الخاتمة تلعب لغات البرمجة دوراً عظيمًا وحاسمًا في مجال تطوير وبرمجة الروبوتات، لذلك من المهم أن تختار اللغة الأنسب للمجال الذي تريد التخصص به، مع ضمان دعم محاكي الروبوتات الذي ستستخدمه لهذه اللغة. يمكن للمبرمجين المبتدئين في مجال الروبوتات البدء بتعلم لغات البرمجة البسيطة مثل سكراتش أو بلوكلي وثمّ الانتقال إلى لغات أكثر تخصصاً مثل Python و C++ و Java، ثم التحول إلى إتقان لغات خاصة بالروبوتات مثل ROS. كما يمكن للانضمام إلى الجمعيات المهنية والمجتمعات التي تشاركك الشغف ذاته لبناء علاقات مهنية مع مهندسي روبوتات آخرين وحضور ورشات عمل ومؤتمرات متعلقة بالمجال. اقرأ أيضًا برمجة الروبوت: الدليل الشامل تحريك شخصية كرتونية عبر سكراتش في راسبري باي تعلم الآلة Machine Learning تعلم الذكاء الاصطناعي
-
عالم تطوير الألعاب هو عالمٌ غنيّ بالإثارة والإبداع والتطور وأن تكون مبرمج ألعاب فيديو يعني أن تشارك في بناء عالمٍ يدخله ملايين الأشخاص يوميًا، حيث يوجد ملايين اللاعبين حول العالم المهتمين بمجال ألعاب الفيديو ومتابعة كل تطور حاصل فيه ، ما يعزّز فكرة العمل على بناء ألعاب جديدة تفوق توقعات اللاعبين وابتكار بيئات لعب استثنائية تنال استحسانهم وتحقيق مبيعات عالية. ولا عجب أن مجال تطوير الألعاب يعد واحدًا من أسرع مجالات البرمجة نموًا حول العالم، فسواء كان المجال الذي ترغب في العمل به هو التصميم الفني لشخصيات الألعاب وعوالمها، أو برمجة الألعاب، فإن هذه الصناعة تسمح لك بتطوير مهاراتك وتحقيق أحلامك المهنية، ومن الجدير بالذكر أنّ متوسط دخل الوظائف البرمجية المرتبطة بالألعاب الإلكترونية مرتفعٌ مقارنةً مع متوسط دخل باقي الوظائف لا سيما إذا تمكنك من الوصول لمرحلة احترافية في هذا التخصص. سنحدّثك في هذا المقال عن أبرز النقاط الأساسية حول مبرمج ألعاب الفيديو الناجح، وما هي مهامه، وكيف يختار مساره المهنيّ، ونجيبك عن العديد من التساؤلات الأخرى التي قد تخطر ببالك حول هذا التخصص المميز. ما هي مهام مبرمج الألعاب؟ يتطلّب تطوير الألعاب مزيجًا من المهارات الإبداعية والتحليلية، وبالتالي على مبرمج الألعاب الناجح أن يمتلك مهارات العمل وحده أو مع فريق وذلك اعتمادًا على نوع اللعبة التي يعمل عليها وهل سيطورها بمفرده أو ضمن استوديو ألعاب مع فريق عمل متكامل، وأن يكون قادرًا على التفكير خارج الصندوق لابتكار أساليب جديدةٍ وجذابة للعب، وأن يكون منظّمًا بما يكفي لاتباع جدولٍ زمنيٍّ دقيق يمشي عليه طوال فترة تطوير اللعبة والتفاعل مع أعضاء الفريق، والعمل ضمن حدود الميزانية والزمن المخصص لإنجاز اللعبة المطلوبة. وهنا يأتي السؤال؛ ما هي مهام مبرمج الألعاب الأساسية؟ في الواقع تتمثل مهمة مبرمج الألعاب الأساسية في تطوير البرامج اللازمة لإنشاء ألعاب الفيديو، من خلال المنصات والمحركات التي تدعم اللعبة التي يعمل عليها، وكتابة الشيفرات البرمجية المناسبة لبناء لعبةٍ مثالية. وتقسم الأدوار عادة بين كلٍّ من المبرمج والمطوّر والمصمّم، الذين يعملون معًا لصناعة اللعبة الإلكترونية وضمان سير العمل بسلاسة؛ حيث يعمل المبرمجون على كتابة منطق اللعبة البرمجي وتفاعل الكائنات مع بعضها البعض في عالم اللعبة، بينما يعمل المصممون على تصميم هذه الكائنات والانتقالات الخاصة بها. بناءً على ما سبق تتركز مهام مبرمج الألعاب على النقاط التالية: إعداد بيئة التطوير المناسبة للعبة الفيديو. البحث عن مجموعة البرامج والمنصات التي سيتم استخدامها لدعم اللعبة. ضمان تحقيق التصميم الكامل للعبة وأداءها بكفاءة وجودة. العمل وفقًا لجدول زمني محكم والالتزام بالميزانية. إنتاج نماذج أوليّة لاختبارها وتحديد الأخطاء والإصلاحات. إجراء اختبارات ضمان الجودة والاستجابة للآراء والتعليقات المختلفة. التعاون مع جميع الأقسام لحلّ المشكلات التقنية خلال عملية إنتاج اللعبة. توفير الدعم التقني المستمر بعد إطلاق اللعبة والعمل على ترقيتها لمواكبة التطورات التقنيّة. كيف تبرز نفسك كمبرمج ألعاب ناجح ضمن المنافسة؟ إذا كنت مهتمًا لتكون مبرمج ألعاب فعليك أن تضع في الحسبان أن المنافسة في هذ المجال كبيرة وهناك بعض المهارات التي لا بدّ أن تكون موجودة فيك لتتميز في برمجة الألعاب، فامتلاكك لهذه المهارات وتمكّنك منها سيمنحك أفضليّة على منافسيك وبقيّة المبرمجين الآخرين. إذ ينبغي أن يمتلك مبرمج الألعاب عمومًا خلفية واسعة في علوم الحاسوب ويتقن إحدى لغات برمجة الألعاب ويكون ماهرًا في التعامل مع أحد محركات الألعاب؛ لكنك ستكون مخطئًا إن اعتقدتّ أن هذه المهارات الفنية هي الوحيدة التي ستقرّر نجاحك وتضمن توظيفك، إذ تولي استوديوهات ألعاب الفيديو المهارات التالية في مبرمجي الألعاب انتباهًا شديدًا لعوامل أخرى من أبرزها ما يلي: التواصل الفعال: من الضروريّ أن يكون مبرمجو ألعاب الفيديو قادرين على التواصل فيما بينهم بشكلٍ صحيح ليتمكّنوا من فهم طبيعة العمل والمواد المتوفّرة والتقنيات المتاحة. العمل ضمن فريق: على الرغم من قدرتك على كون مبرمج ألعابٍ منفردًا إلّا أن تطوير الألعاب ضمن فريق سواء أكان مستقلًا أو في شركة يتطلّب قدرًا من التعاون مع بقية الأشخاص، لذلك يجب أن يتمتع المبرمجون بقدرات تعاونية ممتازة، بما في ذلك القدرة على التفاعل وتبادل الأفكار وتقديم التعليقات والآراء لتمكين الفريق من تحقيق أهدافه المشتركة. حل المشكلات: يجب على المبرمجين أن يكونوا قادرين على حل المشكلات بفعالية من خلال التفكير المنطقي والبحث والحكم السليم. الإبداع: يتيح الإبداع للمبرمجين التفكير في طرق جديدة لحل المشاكل المعقدة في ألعاب الفيديو والارتقاء إلى مستوى أعلى. القابلية للتكيف: نظرًا لأن صناعة ألعاب الفيديو تتغير باستمرار، فإن القابلية للتكيف والتآلف مع ظهور تقنيات جديدة تؤثر على عملية تطوير الألعاب مهارة حاسمة للمبرمجين. اختيار الأدوات المناسبة لبرمجة ألعاب الفيديو قبل أن تبدأ رحلتك في عالم برمجة الألعاب، عليك بداية تحديد الأدوات التي ستستخدمها. تتمثّل هذه الأدوات بشكل رئيس بلغة البرمجة المُختارة ومحرك الألعاب، وقد تلجأ لتعلّم لغة معينة إذا ناسبك محرك ألعاب معين يعمل بها أو بالعكس، وفيما يلي نسرد لك أهم أدوات مبرمج ألعاب الفيديو لنساعدك على اختيار ما يناسبك من بينها. لغة البرمجة تتنوّع لغات البرمجة في مهامها والمجالات التي تخدمها، ولبرمجة ألعاب الفيديو نصيب من لغات البرمجة المخصصة لها، لذلك إن كنت تريد أن تصبح مبرمج ألعاب فيديو محترف يجب أن تتعرف على لغات البرمجة المتخصصة في مجال برمجة ألعاب الفيديو. وهناك عدّة لغات يجب على أي مبرمج ألعاب فيديو تعلّمها ومن أبرزها: C++ C# Java Python JavaScript ولكي لا تضيع بتعدّد الخيارات يمكنك التعرف على ميزات كل لغة على حدى وأيّ اللغات هي الأنسب للمجال الذي تبحث عنه من خلال قراءة مقال لغات برمجة الألعاب. بعد أن تتعلّم لغة البرمجة الأنسب للعبتك يجب أن تختار محرك الألعاب الذي يحقق الغاية المرجوّة من اللعبة. محرك الألعاب يسعى أيّ مبرمج ألعاب فيديو إلى اختيار محرك ألعاب يسهّل عليه عمله ويشكّل إطارًا خلّاقًا يعكس طبيعة اللعبة وأبعادها، فمحرك الألعاب هو البيئة المسؤولة عن تشغيل اللعبة، حيث يوفر لأي مبرمج ألعاب فيديو إطار عمل برمجيّ يتضمن التعليمات البرمجية والمكتبات المستخدمة والأدوات اللازمة لبرمجة وتطوير هذه اللعبة بسهولة تامة. لهذا تعدّ عملية اختيار محرك الألعاب الخطوة الأولى في طريق إظهار اللعبة إلى النّور، فمحرك الألعاب يوفر البنية التحتية الرقمية اللازمة لتشغيل عناصر اللعبة وجعلها تعمل بالشكل المطلوب. وقد شكّلت محركات الألعاب ثورة برمجية في مجال تطوير الألعاب، فقبل وجودها كان مبرمج ألعاب الفيديو يبرمج تفاصيل اللعبة من الألف إلى الياء ويكتب الشيفرات البرمجية اللازمة لتصميم اللعبة ونشرها، إلا أنه مع محركات الألعاب بات عمل كلّ مبرمج ألعاب فيديو أسهل بكثير من حيث التصميم والنشر وإنشاء ألعاب متعددة المنصات وصار كل ما تحتاجه لتصميم لعبة متكاملة اليوم هو تعلم استخدام أحد محركات الألعاب ومعرفة أساسية بإحدى لغات البرمجة التي يدعمها هذا المحرك على سبيل المثال: محرك يونيتي Unity بلغة سي شارب C# محرك أنريل Unreal بلغة سي بلس بلس C++ محرك جودو Godot بلغة جي دي سكربت GDScript - أو سي شارب C# محرك جيم ميكر GameMaker بلغة برمجة GML المعتمددة على لغات جافا سكريبت و C++ و C# ولمطالعة المزيد عن محركات الألعاب ومميزاتها ولغات البرمجة المستخدمة في كل منها أنصحك بمطالعة مقال تعرف على أشهر محركات برمجة الألعاب الإلكترونية ما الفرق بين مبرمج ألعاب فيديو ومصمم ألعاب فيديو؟ الفرق الرئيس بينهما هو نوع العمل الذي يقومون به في عملية تطوير اللعبة. إذ يتولى مصمم ألعاب الفيديو إنشاء الرؤية العامة والمفهوم للعبة الفيديو. فهو يصمم حركة اللعبة والمستويات والشخصيات وخطوط القصة وعناصر أخرى تشكل اللعبة. ويعمل فريق تصميم الألعاب بشكل وثيق مع فريق البرمجة لضمان تنفيذ تصميم اللعبة بشكل صحيح. من ناحية أخرى، يتولى مبرمج ألعاب الفيديو برمجة منطق اللعبة. وهذا يتضمّن الجوانب التقنية للعبة، مثل إنشاء محرك اللعبة وتصميم واجهة المستخدم وتنفيذ حركات اللعبة، ويتواصل مع المصممين لضمان بناء اللعبة وفقًا لمواصفات التصميم. باختصار، يتعامل مصمم ألعاب الفيديو مع الرؤية الإبداعية للعبة، بينما يكون مبرمج ألعاب الفيديو مسؤولًا عن تحويل تلك الرؤية إلى حقيقة من خلال البرمجة والتنفيذ التقني. هل يجب أن أكون مبرمج ألعاب فيديو مستقلّ أم ضمن شركة؟ إذا كنت مستقلًا، فأنت مسؤولٌ عن عملك وفوائدك الخاصة وتطويره بالشكل الذي تريد، إلا أنه يجب عليك العمل في هذه الحالة على تسويق نفسك على منصات التواصل الاجتماعي وتعلّم إدارة ربحك وتكاليفك بشكل مضاعف. إلّا أنه من الجدير بالذكر أن كلّ مبرمج ألعاب فيديو وضع في عين الاعتبار العمل كمستقل في بداية مسيرته لما يحصل عليه من ميزات. أولًا؛ يمكنك الحصول على دفع أعلى للساعة كمستقل. إذا يجني بعض مبرمجي الألعاب المستقلين الخبراء أكثر من 100$ في الساعة، وهو أكثر بكثير مما يحصلون عليه كموظفين عاديين. ثانيًا؛ المرونة في العمل، يمكنك أن تعمل متى تريد، كيفما تريد، على مشاريع من اختيارك، فأنت رئيس نفسك هنا! إلا أنه لا يمكن نكران صعوبة الحصول على منصب مبرمج ألعاب فيديو في شركات مرموقة تحكي منتجاتها عنها وتجذب مشاريع جديدةً بشكل دائم ما يشجّع مبرمجي ألعاب الفيديو على تطوير مهاراتهم للعمل مع هذه الشركات. فإن كنت قادرًا على التسويق لنفسك وجذب عملائك يمكنك العمل كمستقلّ، أما إن كنت تبحث عن التطور والمشاريع الدائمة وبيئة العمل الجماعيّة فقدّم طلب عملٍ إلى شركات تطوير الألعاب الآن!. متوسّط دخل مبرمجي الألعاب يعتمد متوسط دخل أيّ مبرمج ألعاب فيديو على التقنيات المستخدمة ولغة البرمجة، فلا يمكن أن يكون أجر مبرمج ألعاب فيديو iOS يساوي أجر مبرمج ألعاب فيديو باستخدام لغة C++ لأنهم يستخدمون نهجًا ومهارات مختلفة، بالإضافة إلى عدد سنوات الخبرة في المجال وسرعة الإنجاز والاحترافية في التقنيات والمهارات. إليك قائمة بمتوسط دخل مبرمجي ألعاب الفيديو سنويًا في الولايات المتحدة الأمريكية اعتمادًا على الاختصاص ولغة البرمجة بحسب إحصائية لكيوبت عام 2023 : 2000$ مبرمج ألعاب فيديو مختصّ بنظام iOS 11 113000$ مبرمج ألعاب فيديو مختص بنظام Android 53000$ مبرمج ألعاب فيديو مختص بلغة #C 113000$ مبرمج ألعاب مختص بلغة ++C 98000$ مبرمج ألعاب فيديو مختص بلغة HTML5 89000$ مبرمج ألعاب فيديو مختص بلغة JavaScript كيفية البدء بالعمل كمبرمج ألعاب فيديو للدخول إلى عالم برمجة ألعاب الفيديو، تحتاج إلى استراتيجية عمل متينة توصلك إلى وظيفتك الأولى، فليس هناك طريقٌ واحد يجب أن تسلكه وإنما استراتيجيات تمكنك من الوصول لهدفك والبدء بالعمل. فيما يلي 3 استراتيجيات يمكنك اعتمادها لبدء رحلتك في برمجة الألعاب: برمج لعبتك الأولى: فأكبر مبرمجي الألعاب حول العالم بدؤوا مسيرتهم من خلال أجهزةٍ محدودة الإمكانيات، إلا أن الشغف شجعهم على إنشاء لعبةٍ متميزة. هناك الكثير من أدوات تطوير الألعاب المجانية حولك، كل ما عليك فعله هو اختيار اللعبة التي تريد برمجتها ومن ثمّ نشرها على المنصات الخاصة بالألعاب، والآن أصبح لديك خبرة ومعرض أعمال في المجال، وعندما تلقى لعبتك رواجًا ستلاحظك شركات برمجة الألعاب الشهيرة. أنشئ معرض أعمال: تبحث شركات برمجة الألعاب عن مرشحين يسعون إلى التطور بشكلٍ دائم في مجالهم، لذلك حاول أن تسعى بشكل دائم إلى تطوير نفسك في المجال وزيادة خبراتك وشارك أحدث أخبارك عبر منصات التواصل الاجتماعي المختلفة. صمّم معرض أعمالٍ تضع فيه تفاصيل مشاريعك المختلفة والأدوات والمنصات التي اعتمدتها لتحقيق النتيجة النهائية يمكنك استخدام موقع مثل itch.io لإنشاء معرض أعمال لألعابك ورفعها. شارك في مسابقات الألعاب Game Jams: عندما تشارك في مسابقات الألعاب ستحصل على خبرةٍ تضيفها إلى سيرتك الذاتية، وستكتسب بعض المهارات في العمل مع فريق تحت الضغط، وستوسع شبكتك المهنية عن طريق لقاء أشخاص جدد - كثير منهم قد يكونون مطوري ألعاب محترفين يمكنهم مساعدتك في الحصول على وظيفةٍ يومًا ما. كما توفر لك أكاديمية حسوب دورة تطوير الألعاب التي تختصر عليك وقت التعلم وتمكنك من دخول عالم تطوير الألعاب الرائع، وتشرح لك كيفية بناء ألعاب إلكترونية متنوعة من الصفر حتى الاحتراف بتوجيه من خبراء محترفين يرشدونك خلال رحلة التعلم. دورة تطوير الألعاب ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة ومليئة بالتحديات اشترك الآن الخاتمة لتكون مبرمج ألعاب فيديو محترف يجب أن تتحلّى بالعمل الجاد والتفاني والإصرار على الاستمراريّة لاكتساب الخبرات والمهارات التقنية والإبداعية. ولأجل ذلك استمرّ في تحسين مهاراتك وتواصل مع المحترفين في المجال، وتعلّم طريقة سرد قصص الألعاب وكتابة حبكتها بشكل مشوّق ومقنع، وشارك في أي فعاليات تخصّ صناعة الألعاب للتعرف على الأشخاص العاملين في مجال تطوير الألعاب والاستفادة من أفكارهم وتجاربهم وفهم المزيد حول صناعة الألعاب وتطوير مهاراتك وخبراتك. أخيرًا، ابحث عن فرصة عمل مناسبة من خلال التعاقد مع استوديوهات ألعاب أو شركات متخصصة في تطوير ألعاب احترافية أو اعمل على نفسك بشكل مستقل وطوّر ألعابًا خاصة بك وانشرها على متاجر الألعاب لزيادة شهرتك وتحقيق الأرباح. اقرأ أيضًا أشهر أنواع الألعاب الإلكترونية مطور الألعاب: من هو وما هي مهامه تعرف على أهمية صناعة الألعاب الإلكترونية تعرف على أفضل برنامج تصميم الألعاب الإلكترونية
-
نعرفك في مقال اليوم على طريقة الحصول على أفكار ألعاب فيديو ناجحة ومميزة لمشروعك القادم؟ فسواءً أكنت مبتدئًا أو محترفًا متمرسًا في مجال تطوير الألعاب الإلكترونية، فإن الاستراتيجيات والأفكار في هذه المقالة ستزودك بأهم النصائح والأدوات التي تساعدك على صياغة أفكار ألعاب فيديو ناجحة. لا شك أن عملية إنشاء لعبة إلكترونية ناجحة تتطلب الكثير من الوقت والجهد لتصل بها إلى المستوى المرغوب، ولعل أول ما يتبادر إلى ذهننا عند التفكير بصعوبتها هي البرمجة اللازمة لتطبيقها، وآلية تنفيذ التصميم المقترح لهذه اللعبة وما إلى ذلك. ولكن في الواقع إن سألت أي مصمم ألعاب فيديو أو مطورًا متمرسًا عن ذلك، سيجيبك بأن أصعب خطوة في صناعة الألعاب هي تحديد فكرة اللعبة الأساسية، فقبل البدء بأي مرحلة من مراحل البرمجة أو تصميم شخصيات الألعاب الإلكترونية يلزم ابتكار فكرتها الأساسية، لكن هذه العملية ليست بالسهولة التي تتخيلها نظرًا لوجود عدد هائل من الألعاب المطورة فعلًا الأمر الذي يستلزم منك اختيار أفكار مبتكرة تشهد إقبالًا ورواجًا بين جمهور اللاعبين. أهم النصائح التي تساعدك في ابتكار أفكار ألعاب فيديو قد يكون لديك شغف بصناعة الألعاب الإلكترونية ومعرفة تقنية بلغات برمجة الألعاب الإلكترونية وباستخدام أحد محركات الألعاب المساعدة، لكنك تجد صعوبة في توليد فكرة لعبة جيدة تلقى رواجًا بين اللاعبين. لذلك سنعرض لك أهم الخطوات التي ستساعدك بالحصول على فكرة مبتكرة، ونعرفك على أفضل الاستراتيجيات من أجل الوصول لهذا الهدف. ليس بالضرورة أن تكون فكرة اللعبة الناجحة معقدة، فقد تكون الفكرة الناجحة هي فكرة بغاية البساطة والوضوح. فمثلًا من لا يعرف اللعبة الشهيرة Subway Surfers؟ تدور فكرة هذه اللعبة حول شاب يركض هُروبًا من الشرطي وكلبه، حيث يركض فوق القطارات التي تواجهه ليجمع خلال ركضه أكبر عدد من القطع المعدنية. وبالرغم من بساطة هذه الفكرة إلا أنها مصممة ومبرمجة بشكل جيد، وهذا يجعلها محبوبة من قبل الجميع. وفيما يلي أهم الخطوات التي عليك اتباعها للحصول على أفكار ألعاب فيديو ناجحة: أولًا:حدد جمهورك المُستهدف. ثانيًا: اختر تصنيفًا محددًا. ثالثًا: اعرف نطاق مشروعك. رابعًا: حدد أفكار الألعاب من المواضيع الرائجة حاليًا. خامسًا: تأمل العالم من حولك للبحث عن فكرة لعبة مميزة. سادسًا: اشترك في مسابقة Game Jam. سابعًا: استعن بمولدات الأفكار العشوائية وأدوات الذكاء الاصطناعي. ثامنًا: تعلم واستلهم الأفكار من محبّي الألعاب والمطورين. تاسعًا: اعرف ما هي مواصفات اللعبة - التي لن تلعبها! عاشرًا: جرب مفهوم اللعبة الأساسي قبل الاعتماد على الفكرة. دورة تطوير الألعاب ابدأ رحلة صناعة الألعاب ثنائية وثلاثية الأبعاد وابتكر ألعاب ممتعة تفاعلية ومليئة بالتحديات. اشترك الآن لنناقش كل خطوة من هذه الخطوات بمزيد من التفصيل ونتعرف على دورها وأهميتها في الحصول على أفكار ألعاب ناجحة. أولًا: حدد جمهورك المُستهدف برأيك هل ستنجح لعبة تتضمن قوانين عديدة ومعقدة والكثير من التحركات المتقدمة إن كانت موجهة للأطفال؟ بالتأكيد الجواب هو لا! فمن الضروري قبل البدء بالتفكير باللعبة وقواعدها وغيره من التفاصيل أن تحدد من هي الفئة التي تستهدفها بلعبتك القادمة؟ وعلى هذا الأساس تبدأ في التوسع بتفاصيل هذه اللعبة. هذه العملية مهمة لعدة أسباب: تصميم اللعبة حسب تفضيلات الفئة المستهدفة: يتيح لك فهم الاهتمامات والسلوكيات لجمهورك المستهدف إنشاء لعبة تناسبهم. وذلك يشمل موضوع اللعبة وطريقة اللعب ومستوى الصعوبة. فمثلًا تختلف مستويات اللعبة وقوانينها بحسب الفئة العمرية الموجهة إليها. التسويق: إن معرفة جمهورك المستهدف يمكنك من تسويق لعبتك والترويج لها بشكل فعال للأشخاص المناسبين، فإن كانت اللعبة موجهة للأطفال فعليك أن تستخدم أسلوبًا بسيطًا ومرحًا في شرح فكرتها بالإضافة لألوان زاهية، أما إن كانت موجهة للأعمار الأكبر فمن الجيد تضمين أساليب تشويقية للعبتك. رضا اللاعب: يمكنك إنشاء لعبة توفر تجربة أكثر إرضاءً ومتعة للاعبين من خلال تلبية تفضيلات واحتياجات جمهورك المستهدف. ثانيًا: اختر تصنيفًا محددًا تتبع كل لعبة تصنيف معين تجري فيه أحداثها، وهذه نصيحة أساسية للتركيز على نوع معين من تصنيفات الألعاب وبناء قصتك وأحداثك على أساسها. ومن أشهر الأنواع: ألعاب العالم المفتوح Sandbox: نوع يتمتع فيه اللاعبون بطريقة لعب حرَة ويمكنهم استكشاف عالم اللعبة وإنشائه والتغيير به. مثال: ماين كرافت. استراتيجية الوقت الفعلي (RTS): نوع يتحكم فيه اللاعبون في الموارد ويديرونها، ويبنون القواعد، ويقودون الوحدات في معارك بأسلوب تفاعلي لحظي. مثال: ستار كرافت. ألعاب إطلاق النار (FPS و TPS): نوع يستخدم فيه اللاعبون الأسلحة النارية للمشاركة في القتال. مثال: Counter StrikeGears of War. ساحة معركة متعددة اللاعبين عبر الإنترنت (MOBA): نوع يتحكم فيه اللاعبون في شخصية واحدة في لعبة تنافسية قائمة على الفريق، بهدف تدمير قاعدة الفريق المنافس. مثال: League of Legends ألعاب المحاكاة والرياضة: نوع يحاكي فيه اللاعبون أنشطة العالم الحقيقي أو يشاركون في اللعب المتعلق بالرياضة. مثال: The Sims و FIFA . ألعاب الألغاز: نوع حيث يحلّ اللاعبون الألغاز أو يشاركون في مجموعة ألعاب مصغرة متعددة اللاعبين. مثال: Portal 2. ثالثًا: اعرف نطاق مشروعك لتضمن بدء المشروع بوعي كامل بالتفاصيل ونقاط القوة والضعف والعوائق التي ستواجهك، احرص قبل الشروع بإنشاء لعبتك وحتى قبل البحث عن أفكار ألعاب فيديو على طرح هذه الأسئلة على نفسك لمعرفة هذه الحدود: هل تعمل ضمن فريق؟ ما حجمه؟ أم أنه مشروع خاص بك فقط؟ ما هي المهارات التي تمتلكها أنت وفريقك؟ وهل هي كافية لتطوير فكرة اللعبة؟ ما هي المهارات التي تفتقدها، ما الذي يمكنك تعلمه أو الاستعانة بمصادر خارجية؟ هل هناك حزمة تطوير جاهزة ستستخدمها أم ستبني اللعبة بالكامل من الصفر؟ هل ستكون اللعبة ثنائية الأبعاد 2D أم ثلاثية الأبعاد 3D؟ كم من الوقت يمكنك استثماره في تطوير اللعبة أنت وفريقك وسطيًا؟ رابعًا: حدد أفكار الألعاب من المواضيع الرائجة حاليًا إذا كنت تبحث عن أفكار ألعاب فيديو جيدة ومحببة، خذ نظرة من حولك وتعرف على أبرز الأفكار والاتجاهات الرائجة التي تتجه إليه صناعة الألعاب؟ بالإضافة إلى ذلك، فكّر بأساليب تستطيع من خلالها جذب الجمهور إلى لعبتك، كأن تستغل مثلًا شهرة مسلسل تلفزيوني وتقتبس من أحداثه وشخصياته. وبذلك تضمن وجود إقبال أكبر على لعبتك. مثال على ذلك فكرة لعبة أنتون بلاست ANTONBLAST والمستوحاة بشكل كبير من سلسلة Wario Land المنسية منذ فترة طويلة والتي لم تشهد إصدارًا جديدًا منذ عام 2008، إلا أنها لاقت بعد نشرها إقبالًا كبيرًا من الأشخاص بسبب شهرة هذه السلسلة واستغراب الجمهور من إحيائها بهذه الطريقة. خامسًا: تأمل العالم من حولك للبحث عن فكرة لعبة مميزة. لا يمكنك الخروج بأفكار إبداعية وأنت مستلقي على سريرك في غرفتك وحيدًا، انهض وتأمل الدنيا من حولك! قد يبدو لك الأمر يسيرًا وليس ذا أهمية كبيرة، ولكن هل تعلم أن أعظم الأفكار الإبداعية لألعاب الفيديو تشكلت عبر محاكاة الواقع؟ عليك بمراقبة البيئة المحيطة بك لتنشيط خيالك والبدء في الخروج بأفكار ألعاب إبداعية. انتبه إلى الأشياء الصغيرة، وكيف تبدو وتعمل الأشياء من حولك، شاهد الأفلام المتنوعة، اقرأ الروايات، فقد تجد فيها عدد كبير جدًا من الأفكار الرائعة التي ما كانت لتخطر على بالك. في الواقع، يميل الناس لمحبة الألعاب التي صممت وطورت باستخدام قصص واقعية بشكل أكبر. فالنصيحة الذهبية لك هنا أن تحصل على الإلهام من البيئة المحيطة بك وأن تحاول استخدامها في ألعابك لإبقاء اللاعبين مهتمين ومتحمسين. من الأمثلة على الألعاب التي بنيت على محاكاة الواقع هي سلسلة GTA الشهيرة، حيث تحدث الأحداث في هذه السلسلة ضمن أماكن مستوحاة من مدن الحياة الواقعية، وذلك رغبةً من المطورين في أن يركز اللاعب على إكمال المهام اللازمة للتقدم في القصة مع شعوره بأنه الشخصية بحد ذاتها. مثالٌ آخر، هو لعبة بوكيمون Pokemon، حيث استلهم مطوّرها فكرة لعبته من اهتمامه وهو طفل بجمع الحشرات. سادسًا: اشترك في مسابقة Game Jam مسابقات Game Jam هي عبارة ماراثونات مخصصة لتطوير الألعاب تهدف لجمع مطوري الألعاب بإنشاء ألعاب إلكترونية من الصفر وتعاونهم معًا للخروج بأفكار ألعاب فيديو حول موضوع معين أو بشروط معينة والبدء بإنشائها واختبارها وتلقي الآراء حولها.والمشاركة في هذه المسابقة تساعدك على تطوير إمكانياتك وبناء مجتمع في مجالك والحصول على العديد من أفكار ألعاب الفيديو المميزة خارج الصندوق. من أشهر هذه المسابقات مسابقة GMTK game jam، وهو حدث سنوي يهدف لإنشاء لعبة واحدة أو أكثر خلال فترة زمنية قصيرة تتراوح عادة بين 24 إلى 72 ساعة والمشاركة فيها من شأنه تعزيز الإبداع والابتكار في تصميم الألعاب. سابعًا: استعن بمولدات الأفكار العشوائية وأدوات الذكاء الاصطناعي هناك العديد من أدوات الذكاء الاصطناعي التي يمكنها مساعدتك في توليد أفكار ألعاب فيديو ترغب في إنشائها. فيما يلي أربع أدوات مفيدة بالإضافة إلى معلومات حول كيفية عملها وكيف يمكنك استخدامها: تشات جي بي تي ChatGPT يمكنك الاستفادة من خدمات ChatGPT في جميع مراحل إنشاء اللعبة متضمنًا عملية البحث عن الفكرة الأساسية وتفاصيلها. وذلك من خلال تزويده بوصف لما تبحث عنه في فكرة لعبة الفيديو سواءً كان نوع لعبة محدد اخترته أم فئة معينة توجه لها هذه اللعبة وسيقدم لك أفكار إبداعية بناءً على مدخلاتك. أداة Let's Make a Game هي أداة مصممة خصيصًا لمساعدة مصممي الألعاب على توليد أفكار لألعاب الفيديو إذ يمكنك إدخال كلمات رئيسية أو معايير محددة تتعلق بنوع اللعبة التي ترغب في إنشائها، وسيزودك المولد بمفاهيم مختلفة للعبة تتوافق مع تلك المواصفات. توفر لك هذه الأداة الوقت من خلال اقتراح أفكار الألعاب تلقائيًا بناءً على تفضيلاتك، مما يسمح لك باستكشاف الاحتمالات المختلفة بسرعة. أداة Plot Generator هي أداة متعددة الاستخدامات يمكن استخدامها لتطوير القصص والروايات الخاصة بالألعاب. حيث يُنشئ المولد قصص وشخصيات ومهام فريدة للعبتك من خلال اختيار نوع اللعبة وعناصر القصة الأساسية ما يساعدك على العثور على أفكار ألعاب فيديو مبتكرة قد لا تخطر ببالك. أداة Concept and Art Idea Generator توفر لك هذه الأداة المدعومة بالذكاء الاصطناعي طريقة لتوليد العديد الأفكار الملهمة لألعاب الفيديو التي تريد تطويرها وتعطيك اقتراحات حول بيئة اللعبة ومظهر شخصيات الألعاب التي تشترك في اللعب وغيرها من الأفكار المميزة. وذلك عبر إدخال كلمات رئيسية أو سمات محددة تتعلق بلعبتك. ثامنًا: تعلم واستلهم الأفكار من محبّي الألعاب توجد العديد من المجتمعات والمنتديات على الإنترنت تهتم بتطوير الألعاب وتضم أشخاصًا يشاركونك الاهتمام ذاته، وتستطيع الاشتراك بها ومتابعتها للحصول على العديد من أفكار الألعاب أو طرح فكرتك والحصول على آراء المستخدمين حولها. حيث يمكنك إنشاء على سبيل المثال إنشاء منشور على مجتمع حسوب وطرح فكرة لعبة تدور ببالك للحصول على آراء متعددة ومتنوعة من المهتمين لا سيما إذا كان استهداف لعبتك هو السوق العربيّ فهذا المكان هو الأمثل. تاسعًا: اعرف ما هي مواصفات اللعبة التي لن تلعبها! هل تعلم أن لعب لعبة سيئة وغير محببة قد يساعدك بشكل كبير عندما يتعلق الأمر بالبحث عن أفكار ألعاب! فهذا الأمر من شأنه أن يساعدك بشكل كبيرعلى اكتشاف العيوب والأخطاء التي تواجهك في اللعبة السيئة كي تتجنبها ولا تقع بمثلها عند اختيار فكرة لعبتك، كما يمكنك بذلك استلهام أفكار جديدة من أفكار قديمة لم يتم تطويرها بشكل جيد والعمل على تحسينها وصياغتها بلعبة ناجحة. عاشرًا: جرب مفهوم اللعبة الأساسي قبل الاعتماد على الفكرة تعد تجربة فكرة اللعبة الخاصة بك قبل العمل عليها أمرًا بالغ الأهمية لعدة أسباب: تحديد العيوب: يساعدك اختبار فكرة لعبتك على تحديد أي عيوب أو مشكلات محتملة قبل استثمار وقت وموارد كبيرة في تطويرها. فهو يسمح لك باكتشاف المشكلات في المفهوم أو آليات اللعب ومعالجتها. تقييم الجمهور المستهدف: يتيح لك اختبار فكرة لعبة الفيديو التي تريدها قياس اهتمام وتفاعل جمهورك المستهدف. يمكن أن تساعدك تعليقاتهم المبكرة على فهم ما إذا كانت لعبتك تنال إعجاب الفئة المستهدفة أم لا، مما يساعدك على تحسين المفهوم ليناسب تفضيلاتهم وتوقعاتهم. التحسين: يمكنك تحصيل بيانات حول نقاط نجاح لعبتك وفشلها من من خلال مراقبة أداء اللاعب ودرجة انغماسه في اللعب. تضمن لك هذه العملية أن تطور من لعبتك بما يحقق نجاحها. توقع التكلفة: يمكنك توفير الوقت والموارد في التطوير من خلال اختبار فكرة لعبتك في وقت مبكر، وذلك بتحديد العيوب ونقاط الضعف في مرحلة مبكرة مما يسمح لك بإجراء التعديلات اللازمة قبل استثمار أموالك في البرمجة والتصميم والتفاصيل الأخرى. أمثلة على أفكار ألعاب فيديو لاقت نجاحًا كبيرًا إذا كنت مهتمًا بصناعة ألعاب إلكترونية ومعرفة السر وراء نجاح العديد من الألعاب الرائجة فإليك أمثلة على أبرز الألعاب الإلكترونية التي لاقت رواجًا وأهم الأسباب في نجاحها وشهرتها: لعبة ماين كرافت Minecraft. لعبة كاندي كراش Candy Crash. لعبة سوبر ماريو برو Super Mario Bros. لعبة وي سوبرتس Wii Sports. لعبة أمونغ آس Among Us. لنحاول اكتشاف أبرز الأسباب التي جعلت من أفكار هذه الألعاب محبوبة ومنتشرة بين جمهور كبير من اللاعبين حول العالم. لعبة ماين كرافت Minecraft تقوم فكرة لعبة ماين كرافت على أسلوب اللعب المفتوح من خلال السماح للاعبين ببناء واستكشاف عوالم افتراضية بناءً على تفضيلاتهم الشخصية، مما يوفر فرصة للإبداع والخيال ففي هذه اللعبة لايوجد طريقة أو أسلوب معين للعب وهذه الإمكانيات الإبداعية غير المحدودة وقدرة اللاعبين على مشاركة إبداعاتهم مع الآخرين هي ما أكسب اللعبة شعبية هائلة، كما ساهمت طبيعتها التي يتحكم فيها مجتمع اللاعبين والتحديثات المتكررة لها في زيادة نجاحها. لعبة كاندي كراش Candy Crash هي لعبة ألغاز يقوم اللاعبون فيها بمطابقة ثلاث قطع حلوى أو أكثر من نفس اللون لإزالتها من اللوحة. وقد لاقت اللعبة شهرة واسعة منذ نشرها إلى الآن واستقطبت اللاعبين من مختلف الأعمار بفضل أسلوبها الجذاب وقوانينها البسيطة في اللعب وتصميم رسوماتها الملونة ذات الألوان الزاهية وتضمنها لمئات المستويات التي تزداد صعوبة مع تقدم اللاعب في اللعب وتقدم له مكافآت تحفزه على مواصلة اللعب، كما ساعد توفرها على منصات متعددة كالهواتف الجوالة والأجهزة اللوحية وأجهزة الحاسوب في زيادة شعبيتها وانتشارها على نطاق واسع. لعبة سوبر ماريو Super Mario Bros يعود نجاح فكرة لعبة سوبر ماريو التي طورتها شركة نينتندو Nintendo إلى عدة عوامل من أبرزها أسلوب اللعب الممتع والوضح إلى جانب تصميم شخصية لعبة ماريو المحبب والتي أصبحت واحدة من أشهر شخصيات ألعاب الفيديو، كما ساهم إصدار اللعبة على العديد من المنصات في زيادة شعبيتها وانتشارها وهي واحدة من أكثر ألعاب الفيديو مبيعًا على الإطلاق. لعبة وي سبورتس Wii Sports تعتمد فكرة لعبة Wii Sports التي طورتها شركة Nintendo EAD عام 2006 على تضمين من ألعاب المحاكاة الرياضية وقد حققت هذه اللعبة نجاحًا واسعًا بسبب سهولة لعبها وقواعدها البسيطة التي تسمح للاعبين من مختلف الأعمار والمستويات المهارية اللعب والاستمتاع بها كما تتميز هذه اللعبة باستخدام تقنية الحركة وتتضمن وحدة تحكم Wii التي تستخدم تقنية الاستشعار عن الحركة للتحكم بالألعاب عن طريق حركة الجسم مما وفر تجربة لعب واقعية وممتعة. لعبة أمونغ آس Among Us هي لعبة جماعية تتضمن فريق من اللاعبين الذين يجتمعون معًا لإصلاح سفينة فضائية أثناء التعامل مع وجود المحتالين الذين يحاولون القضاء على الطاقم. تشجع اللعبة على التواصل والشك والعمل الجماعي حيث يحاول اللاعبون التعرف على المحتالين والتصويت ضدهم. لاقت اللعبة نجاحًا عظيمًا بسبب طريقة لعبها البسيطة والجذابة التي تشجع العمل الجماعي والتفكير للوصول إلى استنتاجات. كما ارتفعت شعبيتها مع بدء العديد من اللاعبين وصنّاع المحتوى المشهورين بلعبها على البث المباشر، مما ساهم بخلق ضجة كبيرة حولها على مستوى العالم. الخلاصة لا ريب أن عملية تطوير الألعاب تتطلّب عددًا من المهارات للحصول على منتج نهائي ناجح يستمتع به اللاعبون بدءًا من التطوير والتصميم وصولًا إلى التسويق وإطلاق اللعبة على المتاجر، إلا أن التوصل إلى فكرة ممتعة فريدة من نوعها هو أساس هذه العملية وستضمن أن المنتج النهائي سيبدأ على أسس قوية تضمن نجاحه. هل هناك فكرة في بالك للعبة إلكترونية مميزة وناجحة تعتقد أنها ستحقق النجاح لكنك متردد في آلية تنفيذها؟ اطرحها للمناقشة أسفل المقال، ودع القراء الآخرين يبرزون لك مواضع ضعفها وقوتها! اقرأ أيضًا مطور الألعاب: من هو وما هي مهامه تعرف على أشهر لغات برمجة الألعاب مدخل إلى محرك الألعاب جودو Godot نبذة عن صناعة الألعاب ومحرك Unity3D
-
تعد عملية اختيار وتصميم شخصيات الألعاب جزءًا لا يتجزأ من صناعة الألعاب الإلكترونية، وهي لا تقتصر على التركيز على الجانب الجمالي للتصميم وجعله جذابًا، بل تلعب دورًا حاسمًا في تشكيل تجربة اللاعبين وتضمن تناسق قصة اللعبة وانسجام اللاعب معها. حيث تتمتع الشخصية المنتقاة جيدًا بالقدرة على التأثير على المشاعر، وتحفيز اللاعب على اللعب باستمرار، وتضمن الارتقاء باللعبة في النهاية إلى آفاق جديدة من النجاح. سننظر سويًا في هذا المقال إلى أهم الاعتبارات التي عليك الأخذ بها لاختيار شخصية ناجحة في لعبتك القادمة وأهم برامج وأدوات تصميم شخصية لعبة، ونختم المقال بأمثلة لأبرز شخصيات الألعاب المصممة جيدًا فإذا كنت مهتمًا بتطوير ألعاب الفيديو فتابع قراءة المقال للنهاية. معايير تصميم شخصية لعبة ناجحة لا شك أن تصميم شخصيات الألعاب يلعب دورًا مهمًا جداً في صناعة الألعاب الإلكترونية، فالشخصيات هي وسيلة اللاعبين للتفاعل مع العالم الافتراضي للعبة، فإذا كنت مبرمج ألعاب فيجب أن تكون الشخصيات التي تختارها في لعبتك محببة وجيدة التصميم ومنسجمة مع نوع اللعبة وقصتها، وتعكس الأهداف التي تهدف اللعبة إلى تحقيقها، وإليك مجموعة من المعايير أو النقاط التي عليك الانتباه لها عند اختيار أو تصميم شخصية لعبة إلكترونية: تحديد المفهوم العام لشخصية اللعبة ويشمل: السمات الجسدية والنفسية لشخصية اللعبة. سلوك الشخصية وتفاعلها مع محيطها. نبرة صوت الشخصية أو التأثيرات الصوتية الخاصة بها. نقاط قوة الشخصية ونقاط ضعفها والموازنة فيما بينهما. قصة الشخصية. مظهر شخصية اللعبة. لنناقش كل نقطة من هذه النقاط ونتعرف على دورها عند التفكير في تصميم شخصيات الألعاب الإلكترونية. أولًا: تحديد المفهوم العام للشخصية من المهم تحديد الدور العام لشخصيات الألعاب أولًا قبل البدء بتصميمها، والانتباه لأن تكون لكل شخصية لعبة مميزات وخصائص فريدة مختلفة عن بقية الشخصيات الأخرى لضمان تميزها وتفردها وتحديد خصائصها العامة مثل السمات النفسية والجسدية ونبرة الصوت وغيرها من التفاصيل التي تساهم بتشكيل مفهوم شخصية اللعبة، وإليك أهم العوامل التي تساعدك على تصور المفهوم العام للشخصية: 1. السمات الجسدية والنفسية لشخصية اللعبة أول ما عليك التفكير به عند اختيار شخصية لعبة فريدة هو تحديد السمات الجسدية والنفسية لشخصية اللعبة لأنها تساعدك على تصور شكل جسدها وملامحها بما يتوافق مع هدف الشخصية في اللعبة. فمثلًا من المهم أن تكون الشخصية ذات جسد رياضي في حال كانت اللعبة تتضمن الركض والهروب. 2. سلوك الشخصية وتفاعلها مع محيطها يعد سلوك الشخصية جانبًا حيويًا مهمًا في تصورها وتحديد طريقة تفاعلها مع بيئة اللعبة ومع الشخصيات الأخرى لذا عليك أن تحدد هل الشخصية التي تريدها في لعبتك هي شخصية بطولية أو مغامرة أو مرحة أو مؤذية أن ويتماشى تصميم الشخصية مع أفعالها وسلوكها، فإذا كانت الشخصية عدوانية وشريرة فيجب أن تعكس ملامح وجه الشخصية عدوانيتها حتى تكون مقنعة. 3. نبرة صوت الشخصية أو التأثيرات الصوتية الخاصة بها تشير نبرة الشخصية إلى سلوكها وموقفها العام في اللعبة. فهي تحدد الحالة المزاجية للعبة وتؤثر على كيفية إدراك اللاعبين للشخصيات والتفاعل معها فينبغي أن تكون النغمة متسقة طوال اللعبة، ومتناسبة من الحديث أو الموقف الحاصل مما يعزز هوية الشخصية ويعزز الاتصال العاطفي للاعب معها. كما أن النبرة المميزة والمتغيرة بحسب الموقف مثلًا نبرة حماسيّة، حزينة، مبتهجة …إلخ. تجعل منها شخصية متفردة وتبقى عالقة في ذهن اللاعب. 4. نقاط قوة الشخصية ونقاط ضعفها والموازنة فيما بينهما يعد تحديد نقاط القوة والضعف لدى شخصية لعبة الفيديو أمرًا مهمًا لعدة أسباب فهو يضيف عمقًا وتعقيدًا للشخصية، ويجعلها أكثر واقعية وإقناعًا كما أنه يؤثر على آليات اللعب ويجعله أكثر حماسًا، على سبيل المثال تتميز شخصية ماريو في لعبة Super Mario Bros بعدة نقاط قوة مثل القدرة على الركض السريع والقفز عاليًا مما يسمح له بالهروب من الأعداء والوصول إلى الأماكن المرتفعة وتجاوز العقبات، لكنه يملك نقاط ضعف فهو صغير الحجم ولا يستطيع السباحة فهو يغرق إذا سقط في الماء. ثانيًا: قصّة الشخصية تساهم قصة شخصية اللعبة الجذابة في إضافة عمق وتشويق على اللعبة وتجعل اللاعب يتفاعل معها بشكل أفضل، ويشعر بالإنجاز عندما ينتقل عبر مستويات اللعبة ويفوز في النهاية ويحقق الهدف المطلوب، فلا يمكن التغاضي عن أهمية قصة شخصية لعبة الفيديو أثناء اللعب ودورها في ارتباط اللاعب في اللعبة وتحفيزه واهتمامه بأدق تفاصيلها. وليس بالضرورة أن تكون قصة الشخصية معقدة ومطولة فقد تكون بسيطة بحسب حجم اللعبة. خذ لعبة الطيور الغاضبة Angry Birds على سبيل المثال، إذ أنّ هذه الطيور غاضبة بسبب أن الخنازير الأشرار في اللعبة قد اختطفوا بيوض صغارهم وهذا ما يمنح للعبة دافعًا وسببًا لوجود الشخصيات في عالم اللعبة الإلكترونية. ثالثًا: مظهر الشخصية يلعب مظهر الشخصية وتصميمها المرئي دورًا مهمًا في إنشاء الشخصية وتصميمها إذ يجب أن يكون مظهر الشخصية جذابًا وفريدًا ويعكس شخصيتها ودورها في اللعبة وعند تصميم مظهر الشخصية يجب أن تهتم بعدة عوامل إلى جانب ملامحها الجسدية والنفسية مثل زي الشخصية حيث تلعب أزياء شخصيات ألعاب الفيديو دورًا مهمًا في المساهمة في نجاح اللعبة وتساعد في تحديد الهوية المرئية للشخصية وتجعلها أكثر قابلية للتمييز والتذكر بالنسبة للاعب. كما تعكس أزياء الشخصيات أيضًا إعدادات اللعبة أو الفترة الزمنية لها على سبيل المثال، قد يشتمل زي إحدى الشخصيات في لعبة خيالية من العصور الوسطى على دروع وتروس، بينما قد تتمتع الشخصية في لعبة خيال علمي مستقبلية بملابس عالية التقنية. علاوة على ذلك، يمكن أن تؤثر أزياء الشخصيات أيضًا على آليات اللعب من خلال توفير مكافآت أو قدرات متنوعة. على سبيل المثال، قد يوفر الزي دفاعًا معززًا أو خفة الحركة، أو يمنح قدرات خاصة يمكن استخدامها بشكل استراتيجي أثناء اللعب. رابعًا: حركات الشخصية والمقصود بحركات الشخصية الأفعال التي تقوم بها شخصية اللعبة مثل المشي او الجري أو القفز أو التسلق أو إطلاق النار …إلخ. وتؤثر انسيابية واستجابة حركات الشخصية بشكل مباشر على تجربة اللاعب وقدرته على التنقل في عالم اللعبة بفعالية. حيث يمكن للحركات السلسة والطبيعية أن تجعل تجربة اللعب أكثر متعة وواقعية وتعزز تفاعل اللاعب مع اللعبة وتطيل مدة اللعب، بالمقابل ستتسبب حركات الشخصية المصممة بشكل سيء في إحباط اللاعبين وتقلل من استمتاعهم باللعبة. خطوات تصميم شخصيات الألعاب بعد تعرّفنا على معايير تصميم الشخصية الناجحة، كيف تبدأ فعلًا ببدء التصميم؟ نستطيع تجزئة خطوات تصميم الشخصية إلى ما يلي: تحديد تصنيف الشخصية الأولية. بناء قصة الشخصية. البحث عن موارد لشخصيات الألعاب. استخدام أدوات وبرامج مخصصة لتصميم الشخصية. 1. تحديد الشخصية الأولية قد تكون الشخصية بعد الانتهاء من تصميمها فريدةً من نوعها بمزاياها المختلفة من صفات جسدية وحركة وشكل وقصة، إلا أن تصميم الشخصية يبدأ دائمًا بتحديد شخصيتها الأولية أو الابتدائية، حيث أن تصنيف شخصيات الألعاب يحدد توجه الشخصية العام وكيفية تطبيق تصميمك عليها لتلائمها، وإليك بعض الأمثلة على أشهر شخصيات الألعاب الأولية التي يمكنك اختيارها للعبتك الإلكترونية: شخصية البطل: وهي الشخصية التقليدية في معظم ألعاب الفيديو التي تواجه التحديات بقوة وتنتصر عليها، وتتميز هذه الشخصية بشجاعتها وأخلاقياتها العالية، مثال على هذه الشخصية شخصية سوبرمان أو شخصية لينك من سلسلة أسطورة زيلدا The Legend Of Zelda. شخصية الطيب: تتميز هذه الشخصية بإنسانيتها وشغفها إلا أنها قد تكون غير منطقية في بعض الأحيان مما يتسبب في وقوعها في بعض المشكلات. مثال على هذه الشخصية هي شخصية الكوماندر شيبرد في لعبة ماس إيفيكت Mass Effect. شخصية الساحر: هي شخصية لعبة غامضة تملك حيلًا وأسرار خاصة لتحقيق غاياتها، مثال على هذه الشخصية شخصية هاري بوتر Harry Potter وشخصية جاندالف Gandalf في لعبة أمير الخواتم The Lord Of The Rings: Shadow of Mordor. كما يوجد عدة أنواع أخرى من الشخصيات، كشخصية المتمرد وشخصية المغامر والمحب للاستكشاف والقدوة والساذج والمنبوذ والشرير، وقد تتشارك بعض الشخصيات فيما بينها بعدد من الصفات ومن المهم أن تحدد الشخصية الأولية لكل شخصية تستخدمها في ألعابك لأنها تساعدك في اختيار التصميم الملائم بشكل أفضل. 2. بناء قصة الشخصية بعد تحديد تصنيف شخصيات الألعاب الأولية تحتاج للبدء بتخيل قصة الشخصية وتحديد سبب وجودها في عالم اللعبة ودورها فهذا أمر مهم وذلك لتجعل لاعبي اللعبة يرتبطون عاطفيًا مع الشخصية ويحزنون لحزنها ويفرحون لفرحها. لذا تأكّد من تحديد قصة حياة الشخصية وماضيها وهدفها في قصة اللعبة كما يساعد ذلك أيضًا مصممي الغرافيك ومبرمحي الألعاب على الاعتماد على هذه المعلومات في تصميم الشخصية بصريًا بشكل يلائم قصتها وبرمجة تحركاتها بالشكل الصحيح المناسب لتحقيق أهدافها. 3.البحث عن مراجع لشخصيات الألعاب عندما تفكر في اختيار شخصيات ألعابك ستجد أمامك خيارين الأول هو تصميم الشخصية من الصفر باستخدام أدوات وبرامج مخصصة أو استخدام شخصيات جاهزة، فمن المرجّح أنك عندما تفكر بتصميم لشخصية لعبة خاصة بك ستجد رسومات لشخصيات افتراضية تخيلية تطابق الشخصية التي تريد أن تكون موجودة في لعبتك ويمكنك في هذه الحالة الاعتماد عليها بدلًا من تصميمها من الصفر. هناك عشرات المصادر على الإنترنت التي تستطيع من خلالها تحميل عدد من الشخصيات تشابه الشخصية التي تريدها ومن ثم التعديل عليها أو ربما حتى استخدامها مباشرةً في لعبتك الإلكترونية دون أي تعديلات إن وجدت أنها مطابقة للمواصفات التي تريدها. نذكر من هذه المصادر: متجر محرك يونيتي Unity متجر محرك أنريل Unreal متجر كيني الذي يحتوي على شخصيات مجانية متجر itch.io الشهير موقع Adobe Stock موقع Pinterest وغيرها من المواقع التي توفر مجموعات منوعة من الصور والتصاميم لشخصيات ألعاب مجانية أو مدفوعة يمكنك استخدامها في لعبتك الإلكترونية، لكن انتبه لصيغة الشخصية وتوافقها مع محرك الألعاب الذي تود استخدامه في برمجة اللعبة أو تحريك الشخصية وهذا الأمر مفيد ويوفر عليك الكثير من الوقت خصوصًا لو كنت تعمل ضمن فريق حيث أن مصمم الشخصية مختلف عن محركها وعن مبرمجها. 4. استخدام أدوات وبرامج مخصصة لتصميم الشخصية إذا كنت مهتمًا بتصميم شخصيات الألعاب من الصفر أو لم تعثر على تصميم شخصية يناسب ما رسمته في مخيلتك في المواقع التي توفر مصادر لشخصيات للألعاب، فستجد عدة أدوات وبرامج مخصصة تساعدك على تصميم شخصيات الألعاب وتحويلها من أفكار إلى رسومات ثنائية أو ثلاثية الأبعاد جاهزة لبرمجتها ومن أشهر هذه البرامج نذكر: Piskel: هو تطبيق ويب مجاني ومفتوح المصدر يعمل في المتصفح ويمكنك كذلك تثبيته على أنظمة تشغيل لينكس وويندوز ويوفر العديد من الأدوات السهلة لإنشاء شخصيات ألعاب بسيطة ثنائية الأبعاد المعروفة باسم sprite وحفظها بتنسيق PNG أو GIF. GIMP: هو برنامج مفتوح المصدر لمعالجة الصور وهو يوفر إمكانية تصميم شخصيات ألعاب ثنائية الأبعاد وتصديرها إلى العديد من التنسيقات المختلفة. Sketchbook: برنامج مميز يوفر إصدار مجاني لرسم شخصيات الألعاب ثنائية الأبعاد ويوفر عدة خيارات لتصدير الرسومات. بليندر Blender: هو برنامج مجاني ومفتوح المصدر ومجاني لتصميم ورسم الشخصيات ثلاثية الأبعاد ويمكنه تصدير الرسومات إلى العديد من تنسيقات الملفات الجاهز لاستخدامها في برامج تطوير الألعاب. Magic Voxel: هو برنامج مجاني لتصميم شخصيات ألعاب ثلاثية ويمكنك من تصدير الرسومات إلى التنسيق OBJ. والبدء ببرمجتها في محركات الألعاب. MakeHuman: برنامج مفتوح المصدر لتصميم شخصيات ألعاب ثلاثية قريبة من الواقع. SculptGL: برنامج فعال يعمل في المتصفح ويمكنك من تصميم الأشكال والشخصيات ثلاثية الأبعاد بسهولة. مايا Maya: هو برنامج احترافي مدفوع من شركة Autodesk مخصص للهندسة المعمارية والتصاميم الداخلية كما يستخدم في صناعة الأفلام ورسم ونمذجة شخصيات الألعاب ثلاثية الأبعاد ويوفر ميزات متقدمة للتحكم بإضاءتها وحركتها وغيرها من المميزات الاحترافية. 3Ds Max: برنامج مدفوع من شركة Autodesk يشابه برنامج مايا ويوفر مجموعة قوية من الأدوات لنمذجة لتصميم ونمذجة شخصيات الألعاب والرسوم المتحركة ثلاثية الأبعاد ويوفر أيضًا تراخيص مجانية للطلاب. كانت هذه قائمة بأبرز أدوات تصميم شخصيات الألعاب وهناك الكثير غيرها، لذا عليك اختيار الأداة التي تناسبك يعتمد على طبيعة الرسومات التي تود الحصول عليها ثنائية الأبعاد 2D أم ثلاثية الأبعاد 3D، وعلى ميزانيك وخبرتك في استخدام الأداة، وفي محرك اللعبة الذي ستجلب هذه الشخصية إليه وتبرمجها فيه فبعض محركات الألعاب تدعم تنسيقات ملفات محددة فقط. دورة تطوير الألعاب ابدأ رحلة صناعة الألعاب ثنائية وثلاثية الأبعاد وابتكر ألعاب ممتعة تفاعلية ومليئة بالتحديات. اشترك الآن أمثلة لأبرز شخصيات الألعاب المصممة جيدًا تضفي الشخصية المصممة بدقة الحياة على اللعبة، مما يجعلها أكثر جاذبية وبقاء في الذهن. ومن أهم شخصيات الألعاب وسنختم المقال بأكثر شخصيات الألعاب شهرة بين جمهور اللاعبين ونكتشف سبب شهرتها والتعلق بها بالرغم من بساطة بعضها. شخصية لعبة ماريو أصبح ماريو، الشخصية الشهيرة من سلسلة سوبر ماريو رمزًا لها بالرغم من بساطة تصميمه، ولعل السر في شخصية لعبة ماريو هو مظهره المميز وكلامه وأفعاله. فيمكن التعرف على مظهر ماريو المحبوب على الفور بفضل قبعته الحمراء المميزة، وبدلته الزرقاء، وشاربه الكثيف إلى جانب صوته عالي النبرة وعباراته الشهيرة "هذا أنا ماريو!" التي جعلت منه شخصية محبوبة لأجيال من اللاعبين، كما أن خفة الحركة التي يتمتع بها ماريو في القفز والجري والمناورة عبر المستويات المختلفة جعلت منه شخصيةً ممتعة للتحكم واللعب بها، أضف إلى ذلك دافعه طوال اللعبة لإنقاذ الأميرة المخطوفة من قبل السلحفاة الشريرة باوزر. شخصية باك مان حققت باك مان، الشخصية الدائرية الصفراء من لعبة الآركيد Arcade شهرة واسعة من خلال تصميمها وأسلوب لعبها البسيط. إذ أصبحت شخصيتها الأيقونية رمزًا لألعاب الفيديو من نوع الآركيد التقليدية التي كانت مشهورة في فترة سابقة، وعلى الرغم من القيود الموجودة على أجهزة ألعاب الفيديو آنذاك إلا أن مصممي اللعبة استطاعوا ببراعة خلق شخصية لعبة بقيت عالقةً في أذهان الجميع لحد اليوم على الرغم من بساطتها. فمن منا لا يتذكر باك مان بلونه الأصفر الفاقع وفمه المفتوح والأشباح الملونة التي تلاحقه! ولعل هذه الأمثلة تجعلنا نلاحظ أنه ليس من الضروري للشخصية أن تمتلك الكثير من الأشياء والمميزات لتصبح شخصية لعبة ناجحة، فشخصية باك مان ما هي سوى نقطة صفراء في غاية البساطة، ومع ذلك فقد حصدت الكثير من المعجبين. شخصية لعبة كريتوس من سلسلة God of War استطاعت شخصية كريتوس الوصول إلى الشهرة وجذب أعداد هائلة الجماهير من خلال التركيز على قصة الشخصية وتصميم شكلها بناءً على ذلك، متضمنًا تصميم جسد الشخصية ببنية قوية وملابس ملائمة لدوره في اللعبة بشكل مثالي، فهذه التفاصيل بالإضافة للتأثيرات الصوتية جعلت منه شخصية مهيبة، كما أن قدراته القتالية المتعددة أضافت إلى كريتوس إحساس القوة والتصميم، وهو الشيء ذاته الذي انجذب له الجمهور وجعلت منه شخصية هائلة لا تُنسى. بالإضافة إلى ذلك، فإن أسلوب كريتوس القتالي الوحشي، جنبًا إلى جنب مع خلفيته المعقدة وعمقه العاطفي، جعل منه شخصية مقنعة ومبدعة في عالم ألعاب الفيديو. إلى هنا نكون قد وصلنا لنهاية مقالنا الذي فصلنا فيه مرحلة اختيار شخصيات الألعاب التي تعد من أهم مراحل صناعة ألعاب الفيديو، حيث يقوم مصمم اللعبة بإنشاء المفهوم والأسلوب والعمل الفني الكامل للشخصية من الصفر بعملية معقدة ودقيقة يضمن بها تحليل السمات الشخصية لشخصيات الألعاب من أجل إضافة الحياة إليها وجعلها أكثر واقعيّة. ويجب على من يقوم بهذه الوظيفة أن يتمتع بالموهبة والمهارات المتطورة فتصميم شخصية اللعبة يعد جانبًا حيويًا في تصميم ألعاب الفيديو ومن شأنه أن يترك صدى لدى اللاعبين على مستوى واسع من خلال التركيز على الخصائص الجسدية والنفسية والسلوك والنبرة والمظهر وغيرها من التفاصيل. الخلاصة إن التركيز على شخصيات ألعاب الفيديو الخاصة بك يضمن لك ارتباط اللاعبين بلعبتك ارتباطًا عميقًا ويزيد من شعبية اللعبة، ومن شأنه أيضًا أن يكوّن مجتمعًا من الأشخاص الذين ينتظرون جديد الشخصية من قصص وتطوّرات متعلقة بها فيما إذا أردت تطوير جزء آخر أو لعبة مشتقة من لعبتك السابقة. لذا، تأكد من أنك تمنح هذا الجانب من تصميم لعبتك وقتًا وجهدًا مناسبين. هل هناك لعبةٌ حاضرة في ذهنك من أيام الطفولة بفضل شخصياتها المنفّذة والمصممة بشكل جيد؟ شاركها معنا! اقرأ أيضًا مطور الألعاب: من هو وما هي مهامه نبذة عن صناعة الألعاب ومحرك Unity3D إنشاء الوحدات البنائية وشخصيات الخصوم في Unity3D ما هي برمجة الألعاب؟
-
ننشئ التوابع السحرية العددية والمعكوسة كما رأينا سابقًا كائنات جديدة بدلًا من تعديل الكائنات الموضعية، إلا أن التوابع السحرية الموضعية المُستدعاة باستخدام معاملات الإسناد المدعوم مثل =+ و =* تعدل الكائنات موضعيًا بدلًا من إنشاء كائنات جديدة (هناك استثناء سنشرحه في نهاية الفقرة). تبدأ أسماء هذه التوابع السحرية بحرفi، مثل ()__iadd__ و ()__imul__ من أجل العوامل =+ و =* على التتالي. مثلًا، عندما تنفذ بايثون الشيفرة purse *= 2 لا يكون السلوك المتوقع أن تابع ()__imul__ الخاص بالصنف WizCoin سينشئ ويعيد كائن WizCoin جديد بضعف عدد النقود ويسنده للمتغير purse، ولكن بدلًا من ذلك، يعدل التابع ()__imul__ كائن WizCoin الحالي في purse ليكون له ضعف عدد النقود. هذا فرق بسيط ولكن مهم إذا أردت لأصنافك أن تقوم بتحميل زائد overload لمعاملات الإسناد المدعومة. عرّف الصنف 'WizCoin' الذي أنشأناه العاملين + و *، لذا لنُعرّف التابعين السحريين ()__iadd__ و ()__imul__ ليتمكّنوا بدورهم من تعريف العاملين =+ و =* أيضًا، نستدعي في التعبيرين purse += tipJar و purse *= 2 التابعين ()__iadd__ و ()__imul__ على التتالي وتمرر tipJar و 2 إلى المعامل other على التتالي. ضِف التالي إلى نهاية ملف wizcoin.py: --snip-- def __iadd__(self, other): """Add the amounts in another WizCoin object to this object.""" if not isinstance(other, WizCoin): return NotImplemented # نعدل من قيمة الكائن self موضعيًا self.galleons += other.galleons self.sickles += other.sickles self.knuts += other.knuts return self # تعيد التوابع السحرية الموضعية القيمة self على الدوام تقريبًا def __imul__(self, other): """Multiply the amount of galleons, sickles, and knuts in this object by a non-negative integer amount.""" if not isinstance(other, int): return NotImplemented if other < 0: raise WizCoinException('cannot multiply with negative integers') # يُنشئ الصنف WizCoin كائنات متغيّرة، لذا لا تنشئ كائن جديد كما هو موضح في الشيفرة المعلّقة: #return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) # نعدل من قيمة الكائن self موضعيًا self.galleons *= other self.sickles *= other self.knuts *= other return self # تعيد التوابع السحرية الموضعية القيمة self دائمًا تقريبًا يمكن أن تستخدم كائنات WizCoin العامل =+ مع كائنات WizCoin أخرى والعامل =* مع الأعداد الصحيحة الموجبة. تعدّل التوابع الموضعية الكائن 'self' موضعيًا بدلًا من إنشاء كائن 'WizCoin' جديد بعد التأكد من أن المعامل الآخر صالح. أدخل التالي إلى الصدفة التفاعلية لرؤية كيف تعدل عوامل الإسناد المدعوم كائنات WizCoin موضعيًا: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) 1 >>> purse + tipJar 2 WizCoin(2, 5, 46) >>> purse WizCoin(2, 5, 10) 3 >>> purse += tipJar >>> purse WizCoin(2, 5, 47) 4 >>> purse *= 10 >>> purse WizCoin(20, 50, 470) يستدعي العامل + التابعين السحريين ()__add__ و ()__radd__ لإنشاء وإعادة كائنات جديدة. تبقى الكائنات الأصلية التي يعمل عليها العامل + على حالها. يجب على التوابع السحرية الموضعية أن تعدل الكائنات موضعيًا طالما أن الكائن متغيّر mutable (أي هو كائن يمكن تغيير قيمته). الاستثناء هو للكائنات الثابتة immutable objects، إذ لا يمكن تعديلها ومن المستحيل تعديلها موضعيًا. في هذه الحالة يجب على التابع السحري الموضعي إنشاء وإعادة كائن جديد كما في التوابع السحرية العددية والمعكوسة. إذا لم نجعل السمات galleons و sickles و knuts للقراءة فقط، فهذا يعني أنه يمكن تعديلها، وبالتالي كائنات WizCoin هي متغيّرة، كما أن معظم الأصناف التي تكتبه تُنشئ كائنات متغيّرة لذا يجب تصميم توابع سحرية موضعية لتعديل الكائن موضعيًا. تستدعي بايثون تلقائيًا التابع السحري العددي في حال لم تُنفذ التابع السحري الموضعي. مثلًا، إذا لم يكن للصنف WizCoin تابع ()__imul__ سيستدعي التعبير purse *= 10 التابع ()__mul__ بدلًا عنه ويسند له القيمة المرجعة purse، لأن كائنات WizCoin متغيّرة وهذا سلوك غير متوقع وقد يؤدي لأخطاء بسيطة. توابع المقارنة السحرية يحتوي تابع sort() ودالة sorted() خوارزميات ترتيب فعالة، ويمكن الوصول إليها باستدعاء بسيط، ولكن إذا أردت ترتيب ومقارنة كائنات أصنافك، ستحتاج لإخبار بايثون كيفية المقارنة بين الكائنين عن طريق تنفيذ توابع المقارنة السحرية، تستدعي بايثون التوابع المقارنة في الخلفية عندما تُستخدم الكائنات الخاصة بك في التعبير مع عوامل المقارنة< و > و =< و => و == و =!. قبل أن نستكشف توابع المقارنة السحرية، فلنفحص الدوال الست في وحدة 'operator' التي تنجز نفس وظائف عوامل المُقارنة الستة، إذ ستستدعي توابع المقارنة السحرية هذه الدوال. أدخل التالي في الصدفة التفاعلية: >>> import operator >>> operator.eq(42, 42) # أي يساوي، وهي مماثلة للتعبير 42 == 42 True >>> operator.ne('cat', 'dog') # أي لا يساوي وهي مماثلة للتعبير 'cat' != 'dog' True >>> operator.gt(10, 20) # أكبر من، وهي مماثلة للتعبير 20 < 10 False >>> operator.ge(10, 10) # أكبر من أو يساوي، وهي مماثلة للتعبير 10 =< 10 True >>> operator.lt(10, 20) # أصغر من، وهي مماثلة للتعبير 20 > 10 True >>> operator.le(10, 20) # أصغر من أو يساوي وهي مماثلة للتعبير 10 => 20 True ستعطينا وحدة operator نسخ دوال من عوامل المقارنة ويكون تنفيذها بسيط. مثلًا يمكننا كتابة دالة operator.eq() في سطرين: def eq(a, b): return a == b من المفيد امتلاك نسخ لعوامل المقارنة على هيئة دوال لأنه على عكس العوامل، يمكن تمرير الدوال مثل وسطاء لاستدعاءات الدالة، وسنفعل ذلك لتنفيذ تابع مساعدة لتوابع المقارنة السحرية. أولًا، ضِف التالي إلى بداية الملف wizcoin.py، إذ تعطي تعليمات الاستيراد import هذه الإذن بالوصول للدوال في وحدة operator وتسمح لك بالتحقق أن الوسيط other في التابع هو متتالية sequence عن طريق مقارنته مع collections.abc.Sequence: import collections.abc import operator ثم ضِف التالي في نهاية ملف wizcoin.py: --snip-- 1 def _comparisonOperatorHelper(self, operatorFunc, other): """A helper method for our comparison dunder methods.""" 2 if isinstance(other, WizCoin): return operatorFunc(self.total, other.total) 3 elif isinstance(other, (int, float)): return operatorFunc(self.total, other) 4 elif isinstance(other, collections.abc.Sequence): otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2] return operatorFunc(self.total, otherValue) elif operatorFunc == operator.eq: return False elif operatorFunc == operator.ne: return True else: return NotImplemented def __eq__(self, other): # eq is "EQual" 5 return self._comparisonOperatorHelper(operator.eq, other) def __ne__(self, other): # ne is "Not Equal" 6 return self._comparisonOperatorHelper(operator.ne, other) def __lt__(self, other): # lt is "Less Than" 7 return self._comparisonOperatorHelper(operator.lt, other) def __le__(self, other): # le is "Less than or Equal" 8 return self._comparisonOperatorHelper(operator.le, other) def __gt__(self, other): # gt is "Greater Than" 9 return self._comparisonOperatorHelper(operator.gt, other) def __ge__(self, other): # ge is "Greater than or Equal" a return self._comparisonOperatorHelper(operator.ge, other) تستدعي توابع المقارنة السحرية التابع __comparisonOperatorHelper() وتمرر الدالة المناسبة من وحدة operator إلى المعامل operatorFunc، عند استدعاء operatorFunc() فنحن هنا نستدعي الدالة المُمرّرة إلى معامل operatorFunc الذي هو eq() أو ne() أو lt() أو le() أو gt() أو ge() من وحدة operator، أو سيكون علينا تكرار الشيفرة في __comparisonOperatorHelper() في كل من توابع المقارنة السحرية الستة. ملاحظة: تدعى الدوال (أو التوابع) التي تقبل دوال أخرى على أنها وسطاء، مثل __comparisonOperatorHelper() بدوال المراتب الأعلى higher-order functions. يمكن الآن مقارنة كائنات WizCoin مع كائنات WizCoin أخرى وأعداد صحيحة وعشرية وقيم سلسلة من ثلاث قيم عددية تمثل galleons و sickles و knuts. أدخل التالي في الصدفة التفاعلية لرؤية الأمر عمليًا: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن WizCoin >>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن WizCoin آخر >>> purse.total, tipJar.total # فحص القيم وفقًا إلى knuts (1141, 37) >>> purse > tipJar # المقارنة بين كائنات WizCoin باستخدام عامل مقارنة True >>> purse < tipJar False >>> purse > 1000 # الموازنة مع عدد صحيح True >>> purse <= 1000 False >>> purse == 1141 True >>> purse == 1141.0 # المقارنة مع عدد عشري True >>> purse == '1141' # كائن WizCoin ليس مساويًا لأي قيمة سلسلة نصية False >>> bagOfKnuts = wizcoin.WizCoin(0, 0, 1141) >>> purse == bagOfKnuts True >>> purse == (2, 5, 10) # يمكننا المقارنة مع صف يتكون من ثلاثة أعداد صحيحة True >>> purse >= [2, 5, 10] # يمكننا المقارنة مع قائمة تحتوي على ثلاثة أعداد صحيحة True >>> purse >= ['cat', 'dog'] # يجب أن تتسبب هذه التعليمة بخطأ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\Desktop\wizcoin.py", line 265, in __ge__ return self._comparisonOperatorHelper(operator.ge, other) File "C:\Users\Al\Desktop\wizcoin.py", line 237, in _comparisonOperatorHelper otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2] IndexError: list index out of range يستدعي التابع المساعد isinstance(other, collections.abc.Sequence) لرؤية ما إذا كان other هو نوع بيانات متتالية مثل صف tuple أو قائمة list. بإمكاننا كتابة شيفرة مثل purse >= [2, 5, 10] لعمل مقارنة سريعة، وذلك بجعل كائنات WizCoin قابلة للمقارنة مع متتاليات. مقارنة المتتاليات تضع بايثون أهمية أكبر على العناصر الأولى في المتتالية عند مقارنة كائنين من أنواع المتتاليات المضمنة مثل السلاسل النصية والقوائم والصفوف أي أنها لا تقارن العناصر الأخيرة إلا إذا كانت لدى العناصر الأولى قيم متساوية. مثلًا أدخل التالي في الصدفة التفاعلية: >>> 'Azriel' < 'Zelda' True >>> (1, 2, 3) > (0, 8888, 9999) True تأتي السلسلة النصية Azriel قبل (أي هي أقل من) Zelda لأن 'A' تأتي قبل 'Z'. الصف (3, 2, 1) يأتي بعد (أي هو أكبر من) (9999, 8888, 0) لأن 1 هي أكبر من 0. أدخل التالي في الصدفة التفاعلية: >>> 'Azriel' < 'Aaron' False >>> (1, 0, 0) > (1, 0, 9999) False لا تأتي Azriel قبل Aaron على الرغم من أن 'A' في 'Azriel' تساوي 'A' في 'Aaron' ولكن 'z' التالية في 'Azriel' لا تأتي قبل 'a' في 'Aaron'، ويمكن تطبيق الشيء ذاته في الصفين (1, 0, 0) و (1, 0, 9999)، إذ أن العنصرين في كل صف متساويين لذا تحدد العناصر الثالثة (0 و 9999 على التتالي) أن (0, 0, 1) تأتي قبل (9999, 0, 1). هذا يجبرنا على اتخاذ قرار بشأن تصميم صنف WizCoin فهل يجب أن تأتي WizCoin(0, 0, 9999) قبل أو بعد WizCoin(1, 0, 0)؟ إذا كان عدد galleons أهم من عدد sickles أو knuts فيجب على WizCoin(0, 0, 9999) أن تأتي قبل WizCoin(1, 0, 0)، أما إذا قارننا الكائنات بالاعتماد على قيمة knuts فيجب أن تأتي WizCoin(0, 0, 9999) (قيمتها 9999 knuts) بعد WizCoin(1, 0, 0) (قيمتها 493 knuts).وُضعت قيمة الكائن في ملف wzicoin.py على أنها مقدرة بـ knuts لأنها تجعل السلوك متناسقًا مع كيفية مقارنةWizCoin مع الأعداد الصحيحة والعشرية. هذا نوع من الاختيارات التي يجب أن تفعلها عند تصميم الأصناف الخاصة بك. لا توجد توابع سحرية مقارنة معكوسة مثل ()__req__ أو ()__rne__ تحتاج لتنفيذها، وبدلًا عن ذلك نجد أن ()__lt__ و ()__gt__ تعكس بعضها و ()__le__ و ()__ge__ تعكس بعضها و ()__eq__ و ()__ne__ تعكس نفسها، سبب ذلك هو أن العلاقات التالية صحيحة مهما كانت القيم في يمين أو يسار المعامل. purse > [2, 5, 10] هي نفس [2, 5, 10] < purse purse >= [2, 5, 10] هي نفس [2, 5, 10] <= purse purse == [2, 5, 10] هي نفس [2, 5, 10] == purse purse! = [2, 5, 10] هي نفس [2, 5, 10] != purse بمجرد تطبيقك للدوال السحرية المقارنة، ستستخدم بايثون تلقائيًا دالة sort() لترتيب الكائنات الخاصة بك. أدخل التالي في الصدفة التفاعلية: >>> import wizcoin >>> oneGalleon = wizcoin.WizCoin(1, 0, 0) # تكافئ 493 knut >>> oneSickle = wizcoin.WizCoin(0, 1, 0) # تكافئ 29 knut >>> oneKnut = wizcoin.WizCoin(0, 0, 1) # تكافئ 1 knut >>> coins = [oneSickle, oneKnut, oneGalleon, 100] >>> coins.sort() # رتّب من القيمة الأقل إلى الأعلى >>> coins [WizCoin(0, 0, 1), WizCoin(0, 1, 0), 100, WizCoin(1, 0, 0)] يحتوي الجدول 3 قائمة كاملة من توابع المقارنة السحرية ودوال operator. التابع السحري المعامل معامل المقارنة الدالة في وحدة operator ()__eq__ يساوي == operator.eq() ()__ne__ لا يساوي =! operator.nt() ()__lt__ أصغر من < operator.lt() ()__le__ أصغر أو يساوي => operator.le() ()__gt__ أكبر من < operator.gt() ()__ge__ أكبر أو يساوي =< operator.ge() الجدول 3: توابع المقارنة السحرية ودوال وحدة operator. يمكنك رؤية تطبيق هذه التوابع في https://autbor.com/wizcoinfull. التوثيق الكامل لتوابع المقارنة السحرية في توثيقات بايثون https://docs.python.org/3/reference/datamodel.html#object.lt. الخلاصة تسمح توابع المقارنة السحرية لكائنات الأصناف الخاصة بك أن تستخدم معاملات بايثون للمقارنة بدلًا من إجبارك على إنشاء توابع خاصة بك. إذا كنت تُنشئ توابعًا اسمها equals() و isGreaterThan() فهذه ليست خاصة ببايثون، وعدّ هذه إشارة لك لتبدأ باستخدام توابع المقارنة السحرية. ترجمة -وبتصرف- لقسم من الفصل PYTHONIC OOP: PROPERTIES AND DUNDER METHODS من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد المقال السابق البرمجة كائنية التوجه في بايثون: التوابع السحرية Dunder Methods. كيفية إنشاء الأصناف وتعريف الكائنات في بايثون 3. التوابع السحرية (Magic Methods) في PHP. البرمجة الوظيفية Functional Programming وتطبيقها في بايثون
-
لدى لغة بايثون Python أسماء توابع خاصة تبدأ وتنتهي بشرطتين سفليتين وتختصر بالسحرية، وتسمى عادةً التوابع السحرية أو التوابع الخاصة أو توابع داندر Dunder Methods، أنت تعرف مسبقًا اسم التابع السحري __init__() ولكن لدى بايثون العديد غيره، نستخدمهم عادةً لزيادة تحميل المعامل، أي إضافة سلوكيات خاصة تسمح لنا باستخدام كائنات الأصناف الخاصة بنا مع معاملات بايثون، مثل + أو >=. تسمح التوابع السحرية الأخرى لكائنات الأصناف الخاصة بنا بالعمل مع وظائف بايثون المضمنة مثل len() و repe(). كما هي الحال في __init__() أو توابع الجلب والضبط والحذف، لا نستدعي التوابع السحرية مباشرةً، يل تستدعيهم بايثون في الخلفية عندما تستخدم الكائنات مع المعاملات أو بعض الوظائف المضمنة. مثلًا، إذا أنشأت تابعًا اسمه __len__() أو __repr__() للأصناف الخاصة بك فستُستدعى في الخلفية عندما يمرر كائن من هذا الصنف إلى الدالة len() أو repr() على التوالي. هذه التوابع موثقة على الويب في توثيقات بايثون الرسمية. سنحرص على التوسع في صنف WizCoin أثناء استكشافنا لأنواع التوابع السحرية المختلفة وذلك لتحقيق أكبر استفادة ممكنة. توابع تمثيل السلاسل النصية السحرية يمكن استخدام التوابع السحرية __repr__() و __str__() لإنشاء سلسلة نصية تمثل كائنات لا تتعامل معها بايثون عادةً، إذ تُنشئ بايثون عادةً سلاسل تمثيل نصية للكائنات بطريقتين، سلسلة repr النصية وهي سلسلة نصية لشيفرة بايثون التي تُنشئ نسخة من الكائن عندما تُنفذ، وسلسلة str النصية التي هي سلسلة يستطيع الإنسان قراءتها وتؤمن معلومات واضحة ومفيدة عن الكائن. تعاد سلاسل repr و str عن طريق الدوال المبنية مسبقًا repr() و str() على التوالي. مثلًا، أدخل التالي إلى الصدفة التفاعلية لرؤية السلسلتين النصيتين repr و str للكائن datetime.date: >>> import datetime 1 >>> newyears = datetime.date(2021, 1, 1) >>> repr(newyears) 2 'datetime.date(2021, 1, 1)' >>> str(newyears) 3 '2021-01-01' 4 >>> newyears datetime.date(2021, 1, 1) في هذا المثال، سلسلة repr -أي datetime.date(2021, 1, 1)- للكائن datetime.date(السطر 2) هي حرفيًا سلسلة نصية لشيفرة بايثون التي تُنشئ نسخةً من الكائن (السطر 1). تؤمن هذه النسخة تمثيلًا دقيقًا للكائن، ومن جهة أخرى، السلسلة النصية str -أي 2021-01-01- للكائن datetime.date (السطر 3) هي سلسلة نصية تمثل قيمة الكائن بطريقة سهلة القراءة للبشر. إذا أدخلنا ببساطة الكائن في الصدفة التفاعلية (السطر 4)، تظهِر السلسلة النصية repr. تظهر غالبًا السلسلة النصية str للمستخدمين وتُستخدم السلسلة النصية repr للكائن في السياق التقني مثل رسائل الخطأ والسجلات. تعلم بايثون كيفية إظهار الكائنات في أنواعها المبنية مسبقًا مثل الأعداد الصحيحة والسلاسل النصية، ولكنها لا تعلم كيفية إظهار الكائنات للأصناف التي أنشأناها نحن. إذا لم يعرف repr() كيفية إنشاء سلسلة نصية repr أو str لكائن، ستكون السلسلة النصية مغلفة بأقواس مثلثة وتحتوي عنوان الذاكرة واسم للكائن '<wizcoin.WizCoin object at 0x00000212B4148EE0>' لإنشاء هذا النوع من السلاسل النصية لكائن WizCoin أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> str(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> repr(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> purse <wizcoin.WizCoin object at 0x00000212B4148EE0> لا تمتلك هذه السلاسل فائدة كبيرة وصعبة القراءة، لذا يمكننا إخبار بايثون ما نريد استخدامه عن طريق تطبيق التوابع السحرية __repr__() و __str__()؛ إذ يحدد التابع __repr__() أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا repr()؛ بينما يحدد التابع __str__() أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا str(). ضِف التالي إلى نهاية ملف wizcoin.py: --snip-- def __repr__(self): """Returns a string of an expression that re-creates this object.""" return f'{self.__class__.__qualname__}({self.galleons}, {self.sickles}, {self.knuts})' def __str__(self): """Returns a human-readable string representation of this object.""" return f'{self.galleons}g, {self.sickles}s, {self.knuts}k' عندما نمرر purse إلى repr() و str() يستدعي بايثون التوابع السحرية __repr__() و __str__()، أي نحن لا نستدعي التوابع السحرية في الشيفرة الخاصة بنا. لاحظ أن السلسة النصية f التي تضم الكائن في الأقواس تستدعي ضمنًا str() للحصول على السلسة النصية str. مثلًا أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> repr(purse) # Calls WizCoin's __repr__() behind the scenes. 'WizCoin(2, 5, 10)' >>> str(purse) # Calls WizCoin's __str__() behind the scenes. '2g, 5s, 10k' >>> print(f'My purse contains {purse}.') # Calls WizCoin's __str__(). My purse contains 2g, 5s, 10k. عندما نمرر الكائن WizCoin في purse إلى الدالتين repr() و str()، تستدعي بايثون في الخلفية التابعين __repr__() و __str__() الخاصين بالصنف WizCoin. برمجنا هذين التابعين ليعيدا سلاسلًا نصيةً مفيدةً وسهلة القراءة. إذا أدخلت نص السلسلة النصية repr التالية 'WizCoin(2, 5, 10)' إلى الصدفة التفاعلية ستُنشئ كائن WizCoin لديه نفس سمات الكائن في purse. السلسلة النصية str هي تمثيل أسهل للقراءة لقيمة الكائن 2g, 5s, 10k. إذا استخدمت الكائن WizCoin في السلسلة النصية f، ستستخدم بايثون السلسلة النصية str الخاصة بالكائن. إذا كانت الكائنات WizCoin معقدة لدرجة أنه من المستحيل إنشاء نسخة منها باستدعاء دالة بانية Constructor Function واحدة، نغلف السلسلة النصية repr في قوسين مثلثين للتنويه على أنه لا يمكن أن تصبح شيفرة بايثون. هكذا تكون سلسلة تمثيل نصي العامة، مثل '<wizcoin.WizCoin object at 0x00000212B4148EE0>'. كتابة ذلك في الصَدَفة التفاعلية سيرفع خطأ SyntaxError حتى لا يحدث ارتباك بشيفرة بايثون التي تُنشئ نسخة من ذلك الكائن. نستخدم __self.__class__.__qualname بدلًا من توفير السلسلة النصية WizCoin في الشيفرة داخل التابع __repr__()، إذ يستخدم التابع الموروث __repr__() اسم الصنف الفرعي بدلًا من WizCoin. إذا أعدنا تسمية الصنف WizCoin سيستخدم التابع __repr__() الاسم الجديد تلقائيًا. تظهِر السلسلة النصية str للكائن WizCoin السمة بصورة أنيقة ومختصرة. يُفضّل جدًا تطبيق __repr__() و __str__() في كل الأصناف الخاصة بك. المعلومات الحساسة في سلاسل REPR النصية كما ذكرنا سابقًا، نظهر السلاسل النصية str للمستخدمين ونستعمل السلاسل النصية repr في سياق تقني مثل السجلات. ولكن يمكن أن تسبب السلاسل النصية repr مشاكل أمنية، إذا كان الكائن المُنشئ يحتوي على معلومات حساسة مثل كلمات المرور والتفاصيل الطبية والمعلومات الشخصية؛ ففي هذه الحالة تأكد من خلو التابع __repr__() من هذه المعلومات في السلسلة النصية المرجعة، وعند تعطل البرنامج، يجري إعداده بصورة متكررة لتضمين محتويات المتغيرات في ملف السجل للمساعدة في تصحيح الأخطاء، ولا تُعامل عادةً ملفات الدخول هذه على أنها معلومات حساسة. تحتوي ملفات الدخول المفتوحة للعلن في العديد من الحوادث الأمنية كلمات المرور وأرقام بطاقات بنكية وعناوين المنازل ومعلومات حساسة أخرى، خذ ذلك بالحسبان عند كتابة التوابع __repr__() الخاص بصنفك. التوابع السحرية العددية Numeric Dunder Methods تزيد التوابع السحرية العددية أو التوابع السحرية الرياضية من تحميل عامل بايثون الرياضية، مثل + و - و * و / وما شابه. لا نستطيع حاليًا تنفيذ عملية رياضية مثل جمع كائني WizCoin باستخدام العامل +، وإذا حاولنا فعل ذلك سترفع بايثون استثناء TypeError لأنها لا تعرف كيفية إضافة كائنات WizCoin. أدخل التالي إلى الصدفة التفاعلية لمشاهدة هذا الخطأ: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) >>> purse + tipJar Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'WizCoin' يمكنك استخدام التابع السحري ()__add__ بدلًا من كتابة التابع addWizCoin() لصنف WizCoin، لكي تعمل كائنات WizCoin مع العامل +. أضف التالي إلى نهاية ملف wizcoin.py: --snip-- 1 def __add__(self, other): """Adds the coin amounts in two WizCoin objects together.""" 2 if not isinstance(other, WizCoin): return NotImplemented 3 return WizCoin(other.galleons + self.galleons, other.sickles + self.sickles, other.knuts + self.knuts) تستدعي بايثون التابع ()__add__عندما يكون الكائن WizCoin على يسار المعامل + وتمرر القيمة على الجانب الأيمن من المعامل + للمعامل other (يمكن تسمية المعامل أي شيء ولكن الاصطلاح هو other). تذكر أنه يمكن تمرير أي نوع من أنواع الكائنات إلى التابع ()__add__، لذا يجب على التابع أن يحتوي اختبارات من النوع، فمثلًا ليس من المنطقي إضافة رقم عشري أو عدد صحيح إلى كائن WizCoin لأننا لا نعرف إذا كان يجب إضافته إلى galleons أو sickles أو knuts. يُنشئ التابع ()__add__ كائن WizCoin جديد مع كميات تساوي مجموع السمات galleons و sickles و knuts من self و other3 لأن هذه السمات الثلاث تحتوي الأعداد الصحيحة التي يمكننا استخدام المعامل + عليهم. الآن بعد أن حمّلنا العامل + لصنف WizCoin، يمكننا استخدام العامل + على الكائن WizCoin. يسمح لنا زيادة تحميل العامل + بكتابة شيفرة أكثر قابليّة للقراءة. مثلًا، أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن WizCoin >>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن WizCoin آخر >>> purse + tipJar # إنشاء كائن WizCoin آخر يحتوي على المجموع WizCoin(2, 5, 47) إذا مُرر نوع الكائن الخطأ إلى other، لن يرفع التابع السحري استثناءً ولكنه سيعيد القيمة المبنية مسبقًا NotImplemented، فمثلًا، other في الشيفرة التالية هي عدد صحيح: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse + 42 # لا يمكن إضافة كائنات WizCoin مع الأعداد الصحيحة Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'int' تشير إعادة NotImplemented إلى بايثون لاستدعاء التوابع لتؤدي هذه العملية. سنتوسع حول "التوابع السحرية العددية المعكوسة" بتفصيل أكثر في هذا المقال. تستدعي بايثون في الخلفية التابع ()__add__ مع 42 للمعامل other الذي يعيد NotImplemented مما يؤدي لأن ترفع بايثون TypeError. على الرغم من أنه لا يجب إضافة الأعداد الصحيحة أو طرحهم من الكائن WizCoin إلا أنه من المنطقي السماح للشيفرة بضرب كائنات WizCoin بأعداد صحيحة موجبة عن طريق تعريف تابع سحري ()__mul__. ضِف التالي في نهاية الملف wizcoin.py: --snip-- def __mul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" if not isinstance(other, int): return NotImplemented if other < 0: # Multiplying by a negative int results in negative # amounts of coins, which is invalid. raise WizCoinException('cannot multiply with negative integers') return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) يسمح لك التابع ()__mul__ بضرب كائنات WizCoin بأعداد صحيحة موجبة. إذا كان other عدد صحيح، فهذا يعني أنه نوع البيانات التي يتوقعه التابع ()__mul__ ولا يجب أن نعيد NotImplemented. ولكن إذا كان العدد الصحيح سالبًا، هذا يعني أن ضربه الكائن WizCoin سيعطي قيم سلبية للنقود في الكائن WizCoin لأن هذا يتعارض مع تصميمنا للصنف، نرفع WizCoinException مع رسالة خطأ مفصلة. ملاحظة: لا يجب تغيير الكائن self في التابع السحري العددي، بل يجب على التابع إنشاء وإعادة كائن جديد بدلًا من ذلك، إذ يُتوقع من العامل + ومن باقي العوامل أيضًا تقييم كائن جديد بدلًا من تعديل قيمة الكائن الموضعي. أدخل التالي في الصدفة التفاعلية لمشاهدة عمل التابع السحري ()__mul__: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن WizCoin >>> purse * 10 # اضرب كائن WizCoin بعدد صحيح WizCoin(20, 50, 100) >>> purse * -2 # الضرب بعدد صحيح سالب يتسبب بخطأ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\Desktop\wizcoin.py", line 86, in __mul__ raise WizCoinException('cannot multiply with negative integers') wizcoin.WizCoinException: cannot multiply with negative integers يظهر الجدول 1 قائمة التوابع السحرية العددية، لا توجد حاجة لتنفيذ كل التوابع في الصنف الخاص بك. حدد التوابع التي تفيدك. التابع السحري العملية المعامل أو الدالة المضمنة ()__add__ جمع + ()__sub__ طرح - ()__mul__ ضرب * ()__matmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__truediv__ قسمة / ()__floordiv__ قسمة عدد صحيح // ()__mod__ نسبة % ()__divmod__ قسمة ونسبة divmode() ()__pow__ رفع للأس **, pow ()__lshift__ انتقال لليسار >> ()__rshift__ انتقال لليمين << ()__and__ عملية ثنائية و & ()__or__ عملية ثنائية أو | ()__xor__ عملية ثنائية أو حصرية ^ ()__neg__ سلبي أحادي - كما في -42 ()__pos__ هوية أحادي + كما في +42 ()__abs__ قيمة مطلقة ()abs ()__invert__ عملية ثنائية عكس ~ ()__complex__ شكل العدد العقدي complex() ()__int__ شكل العدد الصحيح int() ()__float__ شكل العدد العشري float() ()__bool__ شكل بولياني bool() ()__round__ التدوير round() ()__trunc__ الاختصار math.trunc() ()__floor__ التدوير للأسفل math.floor() ()__ceil__ التدوير للأعلى math.ceil() الجدول 1: التوابع السحرية العددية بعض هذه التوابع مهمة لصنف WizCoin، حاول كتابة التطبيق الخاص بك لكل من التوابع ()__sub__ و ()__pow__ و ()__int__ و ()__float__ و ()__bool__. يمكنك مشاهدة أمثلة عن التطبيقات من خلال الرابط https://autbor.com/wizcoinfull. التوثيق الكامل للتوابع السحرية العددية موجود في توثيقات بايثون على الرابط https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types. تسمح التوابع السحرية العددية للكائنات الخاصة بأصنافك استخدام العوامل الرياضية الخاصة ببايثون. استخدم التوابع السحرية العددية في حال كتبت توابع تصف مهمة تابع موجود سابقًا أو دالة مبنية مسبقًا، مثل التابعين multiplyBy() أو convertToInt() أو ما شابه، إضافةً إلى التوابع السحرية المعكوسة أو الموضعية المشروحة في الفقرتين التاليتين. التوابع السحرية العددية المعكوسة تستدعي بايثون التوابع السحرية العددية عندما يكون الكائن على يسار العامل الرياضي، ولكنها تستدعي التابع السحري العددي المعكوس (يسمى أيضًا التابع السحري العددي العكوس أو اليد اليمين) عندما يكون الكائن على الطرف اليمين من العامل الرياضي. التوابع السحرية العددية المعكوسة مفيدة لأن المبرمجين الذين يستخدمون الأصناف الخاصة بك لا يكتبون دومًا الكائن على الطرف اليسار من العامل الذي يقود بدوره لسلوك غير متوقع. لنرى مثلًا ما سيحدث عندما تحتوي purse كائن WizCoin وتعطي بايثون القيمة للتعبير 2 * purse حيث purse هي على الطرف اليمين للمعامل. يُستدعى التابع ()__mul__ للصنف int لأن 2 هو عدد صحيح، وذلك مع تمرير purse للمعامل other. لا يعرف التابع ()__mul__ للصنف int كيف يتعامل مع الكائنات WizCoin لذا يُعيد NotImplemented. لا ترفع بايثون الخطأ TypeError الآن لأن purse تحتوي كائن WizCoin، ويُستدعى التابع ()__rmul__ الخاص بالصنف WizCoin باستخدام 2 ويُمرر إلى المعامل other. ترفع بايثون الخطأ TypeError إذا أعاد التابع __rmul__() القيمة NotImplemented. ما عدا ذلك تكون القيمة المعادة من ()__rmul__ هي نتيجة التعبير 2 * purse. يعمل التعبيرpurse * 2 بصورة مختلفة عندما تكون purse على الجانب الأيسر من المعامل: لأن purse تحتوي كائن WizCoin، إذ يُستدعى تابع ()__mul__ الخاص بالصنف WizCoin ويمرر 2 للمعامل other. ينشئ التابع ()__mul__ كائن WizCoin جديد ويعيده. الكائن المُعاد هو قيمة التعبير purse * 2. لدى التوابع السحرية العددية والتوابع السحرية العددية المعكوسة نفس الشيفرة إذا كانت متبادلة. العوامل المتبادلة مثل الجمع لديها نفس النتيجة بالاتجاهين، 3+2 هي نفس 2+3، ولكن المعاملات الأخرى ليست تبادلية فمثلًا 3-2 ليست 2-3. أي عملية تبادلية يمكنها استدعاء نفس التابع السحري العددي الأساسي عندما يُستدعى التابع السحري العددي المعكوس؛ فمثلًا، أضف التالي في نهاية ملف wizcoin.py لتعريف التابع السحري العددي المعكوس لعامل الضرب: --snip-- def __rmul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" return self.__mul__(other) ضرب عدد صحيح بكائن WizCoin هو تبادلي، إذ أن 2 * purse هي نفس purse * 2. بدلًا من نسخ ولصق الشيفرة من ()__mul__ نستدعي فقط self.__mul__() ونمررها للمعامل other. بعد تحديث mizcoin.py، جرب استخدام تابع الضرب السحري المعكوس عن طريق إدخال التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse * 10 # يستدعي __mul__() بقيمة 10 للمعامل other WizCoin(20, 50, 100) >>> 10 * purse # يستدعي __rmul__() بقيمة 10 للمعامل other WizCoin(20, 50, 100) تذكر أن بايثون تستدعي في التعبير 10 * purse تابع ()__mul__ الخاص بالصنف int لمعرفة ما إذا كان بإمكان العدد الصحيح أن يُضرب بكائنات WizCoin. لا يعلم طبعًا صنف بايثون int المبني مسبقًا أي شيء عن الأصناف التي أنشأناها، لذا تعيد NotImplemented. هذا يشير لبايثون باستدعاء التابع ()__rmul__ الخاص بصنف WizCoin، وإذا كان موجودًا للتعامل مع العملية الحسابية،ترفع بايثون استثناء TypeError إذا كان الاستدعائين للتابعين ()__mul__ و ()__rmul__ للصنفين Int و WizCoin على التتالي يعيدان NotImplemented. يمكن إضافة كائنات WizCoin إلى بعضها فقط، وهذا يضمن أن التابع الأول ()__add__ الخاص بالصنف WizCoin سيتعامل مع المعامل لذا لا نحتاج لتنفيذ ()__radd__. مثلًا، في التعبير purse + tipJar يُستدعى التابع ()__add__ للكائن purse وتمرّر tipJar للمعامل other. لا تحاول بايثون استدعاء تابع ()__radd__ الخاص بالكائن tipJar لأن هذا الاستدعاء لن يعيد NotImplemented، وتكون purse هي المعامل other. يحتوي الجدول 2 على قائمة كاملة للتوابع السحرية العددية المعكوسة. التابع السحري العملية المعامل أو الدالة المضمنة ()__radd__ جمع + ()__rsub__ طرح - ()__rmul__ ضرب * ()__rmatmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__rtruediv__ قسمة / ()__rfloordiv__ قسمة عدد صحيح // ()__rmod__ نسبة % ()__rdivmod__ قسمة ونسبة divmode() ()__rpow__ رفع للأس pow, ** ()__rlshift__ انتقال لليسار << ()__rrshift__ انتقال لليمين >> ()__rand__ عملية ثنائية و & ()__ror__ عملية ثنائية أو | ()__rxor__ عملية ثنائية أو حصرية ^ الجدول 2: التوابع السحرية العددية المعكوسة التوثيق الكامل للتوابع السحرية المعكوسة موجود في توثيقات بايثون. الخلاصة تسمح لك بايثون بإعادة تعريف العوامل باستخدام التوابع السحرية التي تبدأ وتنتهي بمحرفي شرطة سفلية، كما يمكن إعادة صياغة العوامل الرياضية الشائعة باستخدام التوابع السحرية العددية والمعكوسة، إذ تقدم هذه التوابع طريقة لعمل عوامل بايثون الموضعية مع كائنات الأصناف التي أنشأتها وإذا لم تكن قادرة على التعامل مع نوع بيانات الكائن على الطرف الأخر من المعامل ستعيد قيمة NotImplemented المبنية مسبقًا. تُنشئ هذه التوابع السحرية وتعيد كائنات جديدة، في حين تُعدل التوابع السحرية الموضعية (التي تُعيد تعريف معاملات الإسناد المدعومة) الكائنات موضعيًا. لا تنفذ التوابع السحرية المقارنة معاملات بايثون الستة للمقارنة فقط، ولكن تسمح لدالة بايثون sort() بترتيب كائنات الأصناف الخاصة بك. ستحتاج لاستخدام الدوال eq() و ne() و lt() و le() و gt() و ge() في وحدة العامل لمساعدتك في تنفيذ هذه التوابع السحرية. تسمح الخواص والتوابع السحرية بكتابة الأصناف الخاصة بك بطريقة متناسقة وقابلة للقراءة، كما تسمح لك بتفادي الشيفرة النمطية التي تتطلبها لغات البرمجة الأخرى مثل جافا. لتعلم كتابة شيفرة بايثون هناك حديثان لريموند هيتغير Raymond Hettiger يتوسعان في هذه الأفكار "تحويل الشيفرة إلى بايثون اصطلاحية". و"ما وراء PEP 8 - أفضل الممارسات لشيفرة جميلة وواضحة" التي تغطي بعض المفاهيم التي ذكرناها وأكثر. ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properities and dunder methods من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد المقال السابق البرمجة كائنية التوجه في بايثون: الخاصيات Properties. الدوال الرياضية المضمنة في بايثون 3. توثيق بايثون.
-
لدى العديد من اللغات البرمجية ميزات البرمجة كائنية التوجه ولكن تمتلك لغة بايثون Python أفضلها. سيساعدك تعلم كيفية استخدام هذه التقنيات الخاصة ببايثون على كتابة شيفرة مختصرة وسهلة القراءة. تسمح الخاصيات بتنفيذ شيفرة معينة في كل مرة تُقرأ أو تُعدل أو تُحذف سمة كائن معين لضمان عدم وضع الكائن في حالة غير صالحة. تسمى هذه التوابع في لغات البرمجة الأخرى بالجالبة getters أو الضابطة setters. تسمح لك التوابع السحرية Dunder methods باستخدام الكائن الخاص بك مع عوامل بايثون، مثل عامل + ويسمح لك ذلك بجمع كائني datetime.timedelta مثل datetime.timedelta(days=2) و datetime.timedelta(days=3) لإنشاء كائن datetime.timedelta(days=5). سنتوسع في الصنف WizCoin الذي بدأنا به سابقًا بالإضافة لاستخدام أمثلة أخرى، وذلك بإضافة الخاصيات وإعادة تعريف العوامل في التابع السحري. ستجعل هذه الميزات كائنات WizCoin معبرةً بصورةٍ أكبر وأسهل للاستخدام في أي تطبيق يستورد وحدة wizcoin. الخاصيات Properties وسَمَ الصنف BankAccount المستخدم سابقًا السمة balance_ بأنها خاصة private عن طريق وضع شرطة سفلية في بداية الاسم، ولكن تذكر أن وصف سمة أنها خاصة هو اصطلاح. كل السمات في بايثون هي تقنيًا عامة، يعني أنه يمكن الوصول لها من شيفرة خارج الصنف، كما أنه لا توجد طريقة لمنع الشيفرة من تغيير سمة balance_ سواءً عن قصد أو بغير قصد إلى قيمة غير صالحة. يمكن منع التغييرات العرضية لهذه السمات الخاصة عن طريق الخواص. إذ أن الخاصيات في بايثون هي سمات خُصصت لها توابع جالبة وضابطة وحاذفة، وهذه التوابع بدورها تنظم كيفية قراءة السمة وتغييرها وحذفها. مثلًا، إذا كان لدى السمة فقط قيم أعداد صحيحة سيتسبب تحويلها إلى سلسلة نصية 42 بأخطاء. تستدعي الخاصية تابع الضبط لتنفيذ الشيفرة التي تحل أو على الأقل تنبه مبكرًا على ضبط قيمة غير صالحة. إذا فكرت "لطالما أردت تنفيذ شيفرة في كل مرة يجري فيها الوصول إلى السمة أو تعديلها بتعليمة إسناد أو حذفها بتعليمة del" إذًا عليك استخدام الخواص. تحويل السمة إلى خاصية لننشئ أولًا صنفًا بسيطًا لديه سمة عادية بدلًا من خاصية. افتح نافذة محرر ملفات جديدة وأدخِل الشيفرة التالية واحفظه على النحو التالي regularAttributeExample.py: class ClassWithRegularAttributes: def __init__(self, someParameter): self.someAttribute = someParameter obj = ClassWithRegularAttributes('some initial value') print(obj.someAttribute) # يطبع 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # يطبع 'changed value' del obj.someAttribute # يحذف السمة someAttribute يحتوي الصنف ClassWithRegularAttributes على سمة عادية اسمها someAttribute. يضبط التابع __init__() السمة someAttribute إلى some initial value، ومن ثم مباشرة نغير قيمة السمة إلى changed value، وعند تنفيذ البرنامَج ستكون المُخرجات على النحو التالي: some initial value changed value يشير هذا الخرج إلى أن الشيفرة بإمكانها تغيير someAttribute لأي قيمة. من مساوئ استخدام السمات العادية هي أن الشيفرة قد تضبط السمة someAttribute إلى قيم غير صالحة. هذه المرونة بسيطة ومريحة ولكن تعني أن someAttribute يمكن أن تُضبط لقيمة غير صالحة وتسبب أخطاء. لنعيد كتابة هذا الصنف باستخدام الخواص، وذلك عن طريق تطبيق الخطوات التالية على سمة تُدعى someAttribute: أعِد تسمية السمة مع بادئة هي شرطة سفلية _someAttribute أنشئ تابعًا اسمه someAttribue مع المزخرف @property. لدى هذا التابع الجالب المعامل self الموجود لدى كل التوابع. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.setter@. لدى هذا التابع الضابط المعاملين self و value. أنشئ تابع آخر اسمه someAttribute مع المزخرف someAttribute.deleter@. لدى هذا التابع الحاذف المعامل self الموجود لدى كل التوابع. افتح نافذة محرر ملفات جديدة وادخل الشيفرة التالية واحفظها على النحو التالي propertiesExample.py: class ClassWithProperties: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # هذا التابع هو الجالب return self._someAttribute @someAttribute.setter def someAttribute(self, value): # هذا التابع الضابط self._someAttribute = value @someAttribute.deleter def someAttribute(self): # هذا التابع الحاذف del self._someAttribute obj = ClassWithProperties() print(obj.someAttribute) # يطبع 'some initial value' obj.someAttribute = 'changed value' print(obj.someAttribute) # يطبع 'changed value' del obj.someAttribute # يحذف السمة _someAttribute خرج هذا البرنامج هو خرج الشيفرة في regularAttributeExample.py ذاتها لأنهما ينفذان المهمة ذاتها وهي طباعة السمة الأولية للكائن ومن ثم تحديث السمة وطباعتها مجددًا. لاحظ أن الشيفرة خارج الصنف لا تصل مباشرةً إلى السمة someAttribute_ (لأنها خاصة)، ولكنها تصل إلى خاصية someAttribute. مكونات هذه الخاصية هي مجردة نوعًا ما، وهي التوابع الجالبة والضابطة والحاذفة. عندما نعيد تسمية سمة اسمها someAttribute إلى _someAttribute أثناء إنشاء توابع جالية وضابطة وحاذفة نسمي ذلك خاصية someAttribute. تسمى في هذا السياق سمة _someAttribute حقل الرجوع backing field أو متغير الرجوع backing variable وهي السمة التي تُبنى عليها الخاصية. لمعظم الخاصيات متغير رجوع ولكن ليس جميعها، سننشئ خاصيات بدون متغير رجوع لاحقًا. عندما تنفذ بايثون شيفرة تصل إلى تابع مثل print(obj.someAttribute)، فإنها تستدعي في الخلفية تابع الجلب وتستخدم القيمة المعادة. عندما تنفذ بايثون تعليمة إسناد مع خاصية، مثل 'obj.someAttribute = 'changed value، فإنها تستدعي في الخلفية تابع الضبط وتمرر السلسلة النصية 'changed value' من أجل المعامل value. عندما تنفذ بايثون تعليمة del مع خاصية مثل del obj.someAttribute، تستدعي في الخلفية تابع الحذف. تعمل الشيفرة في توابع الجلب والضبط والحذف الخاصة بالخاصية على متغير الرجوع مباشرةً، لأن وصول توابع الجلب والضبط والحذف إلى الخاصية قد يسبب أخطاء. فمثلًا عندما يصل تابع الجلب إلى الخاصية مسببًا استدعاء تابع الجلب لنفسه، وهذا يجعله يصل إلى الخاصية مجددًا ويسبب استدعاء نفسه مجددًا وهكذا دواليك إلى أن يتعطل البرنامج. افتح محرر النصوص وادخل الشيفرة التالية واحفظ التالي في badPropertyExample.py. class ClassWithBadProperty: def __init__(self): self.someAttribute = 'some initial value' @property def someAttribute(self): # التابع الجالب # نسينا هنا استخدام الشرطة السفلية (_) مما تسبب باستخدامنا للخاصية واستدعاء التابع الجالب مجددًا return self.someAttribute # هذا يستدعي التابع الجالب مجددًا @someAttribute.setter def someAttribute(self, value): # التابع الضابط self._someAttribute = value obj = ClassWithBadProperty() print(obj.someAttribute) # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة يستمر الجالب باستدعاء نفسه عند تنفيذ هذه الشيفرة إلى أن يعطي بايثون الاستثناء recursionError: Traceback (most recent call last): File "badPropertyExample.py", line 16, in <module> print(obj.someAttribute) # ينتج خطأ هنا بسبب استدعاء الدالة الجالبة للدالة الجالبة File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا File "badPropertyExample.py", line 9, in someAttribute return self.someAttribute # يستدعي هذا السطر الجالب مجددًا [Previous line repeated 996 more times] RecursionError: maximum recursion depth exceeded يجب على الشيفرة داخل توابع الجلب والضبط والحذف أن تعمل دائمًا على متغير الرجوع لمنع هذا التكرار من الحصول (الذي في بداية اسمه شرطة سفلية) وليس على الخاصية. كما لا يوجد ما يمنع من كتابة الشيفرة على متغير الرجوع حتى مع وجود بادئة الشرطة السفلية التي تجعل الوصول خاص. استخدام الضوابط للتحقق من البيانات الغرض الأساسي من استخدام الخاصيات هي للتحقق من البيانات أو التأكد من أنها في الصيغة المرغوبة. ربما لا تريد لشيفرة خارج الصنف أن تضبط سمة لأي قيمة لأن هذا قد يؤدي إلى أخطاء. يمكنك استخدام الخاصيات لإضافة فحوصات checks تضمن أن القيمة التي تسند إلى السمة هي القيمة الصحيحة. تسمح هذه الفحوصات ملاحظة الأخطاء في مراحل تطوير الشيفرة المبكرة لأنها تسمح برفع استثناء عندما تُضبط أي قيمة غير صالحة. لنحدث ملف wizcoin.py الذي أنشأناه سابقًا ليُحوّل السمات galleons و sickles و knuts إلى خاصيات. سنغير ضابطة هذه الخاصيات لتكون الأرقام الصحيحة الموجبة فقط صالحة. تمثل WizCoin كمية النقود ولا يمكن أن تحتوي نصف قطعة نقدية أو أي قيمة أقل من الصفر، سنرفع استثناء WizCoinException في حال حاولت شيفرة خارج الصنف أن تضبط الخاصيات galleons أو sickles أو knuts إلى قيمة غير صالحة . افتح ملف wizoin.py الذي حفظته سابقًا وعدله ليصبح على النحو التالي: 1 class WizCoinException(Exception): 2 """The wizcoin module raises this when the module is misused.""" pass class WizCoin: def __init__(self, galleons, sickles, knuts): """Create a new WizCoin object with galleons, sickles, and knuts.""" 3 self.galleons = galleons self.sickles = sickles self.knuts = knuts # NOTE: __init__() methods NEVER have a return statement. --snip-- @property 4 def galleons(self): """Returns the number of galleon coins in this object.""" return self._galleons @galleons.setter 5 def galleons(self, value): 6 if not isinstance(value, int): 7 raise WizCoinException('galleons attr must be set to an int, not a ' + value.__class__.__qualname__) 8 if value < 0: raise WizCoinException('galleons attr must be a positive int, not ' + value.__class__.__qualname__) self._galleons = value --snip-- تضيف التغييرات الجديدة صنف WizCoinException الذي يرث من صنف Exception المبني مسبقًا في بايثون. توضح سلسلة توثيق النصية docstring الخاصة بالصنف كيف تستخدمه وحدة wizcoin. تُعد هذه ممارسة جيدة لوحدات بايثون يمكن أن ترفعها كائنات صنف wizcoin عندما يُساء استخدامها، وهكذا عندما يرفع كائن WizCoin أصناف استثناءات أخرى مثل ValueError و TypeError، سيشير هذا غالبًا إلى خطأ في صنف WizCoin. ضبطنا في التابع __init__() الخاصيات self.galleons و slef.sickles و self.knuts إلى المعاملات الموافقة. أضفنا في آخر الملف تابع جالب وضابط للسمة self._galleons بعد التابعين total() و weight(). يعيد هذا الجالب القيمة في self._galleons ويتحقق التابع الضابط إذا كان القيمة المسندة إلى الخاصية galleons هي عدد صحيح وموجب، إذا فشل واحد من التحقيقين تُرفع WizCoinException برسالة خطأ، كما يمنع هذا التحقق _galleons من أن تُضبط بقيمة غير صالحة طالما تستخدم الشيفرة الخاصية galleons. لدى كل كائنات بايثون تلقائيًا سمة __class__ التي تشير إلى صنف الكائن. بمعنى أخر، __value.__class هي نفس صنف الكائن الذي يعيده type(value) ، كما أنه لدى كائن الصنف هذا سمة __qualname__ التي هي سلسلة نصية لاسم الصنف. تحديدًا هو الاسم المؤهل للصنف الذي يتضمن أسماء أي أصناف يكون كائن الصنف متداخلًا فيها. الأصناف المتداخلة Nested classes محدودة الاستخدام وخارج نطاق موضوعنا. فمثلًا إذا كانت value قد خزّنت الكائن date المعاد بالصيغة datetime.date(2021, 1, 1)، ستكون __value.__class__.__qualname هي السلسلة النصية 'date'. تستخدِم رسالة الاستثناء __value.__class__.__qualname (في السطر 7) للوصول إلى السلسلة النصية لقيمة اسم الكائن، إذ يجعل اسم الكائن رسالة الخطأ هذه أكثر إفادة للمبرمج الذي يقرأها لأنها تحدّد أن الوسيط 'value' ليس من النوع الصحيح، وتحدد أيضًا ما هو نوعه السابق وما النوع الذي يجب أن يكون. ستحتاج لنسخ الشيفرة من الجالب والضابط ليستخدمها _galleons ومن أجل سمات _sickles و _knuts أيضًا، إذ تكون شيفراتهم نفسها ما عدا أنها تستخدم السمات _sickles و _knuts بدلًا من _galleons للمتغيرات الراجعة. خاصيات القراءة فقط تحتاج الكائنات الخاصة بك بعض خاصيات القراءة فقط التي لا يمكن ضبطها بمعامل الإسناد =، إذ يمكن جعل الخاصية للقراءة فقط عن طريق حذف التوابع الضابطة والحاذفة. مثلًا، يعيد التابع total() في الصنف WizCoin قيمة الكائن في knuts. يمكننا تغيير ذلك من تابع عادي لخاصية القراءة فقط لأنه في النهاية لا توجد طريقة منطقية لضبط total لكائن WizCoin؛ فإذا ضبطنا total إلى العدد الصحيح 1000، هل ذلك يعني 1000 knuts؟ أو 1 galleon و 493 knuts؟ أو أي تشكيلة أخرى؟ لهذا السبب سنجعل total خاصية للقراءة فقط عن طريق إضافة الشيفرة بالخط الغامق في ملف wizcoin.py: @property def total(self): """Total value (in knuts) of all the coins in this WizCoin object.""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) # Note that there is no setter or deleter method for `total`. بعد إضافة مزخرف التابع porperty@ أمام total()، سيستدعي بايثون تابع total() عند الوصول إلى total، ولأنه لا يوجد تابع ضابط ولا حاذف، ترفع بايثون AtrributeError إذا حاولت أي شيفرة تعديل أو حذف total باستخدامه في وسيط أو تعليمة delعلى التتالي. تعتمد قيمة الخاصية total على قيمة الخاصيات galleons و sickles و knuts ولا تعتمد الخاصية على متغير الرجوع المسمى _total . أدخل التالي في الصَدَفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse.total 1141 >>> purse.total = 1000 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute ربما لا تريد لبرنامجك أن يتوقف فورًا عند محاولتك تغيير خاصية للقراءة فقط، ولكن هذا السلوك يُفضل على السماح بتغيير خاصية للقراءة فقط؛ فإذا كان برنامجك يسمح بتعديل خاصية للقراءة فقط، فهذا سيسبب بالتأكيد خطأ في مرحلة ما عندما يُنفذ البرنامج. إذا حصل هذا الخطأ لاحقًا بعد تعديل خاصية للقراءة فقط، سيصبح من الصعب تحديد السبب الرئيس، لهذا يسمح لك التعطيل الفوري بملاحظة المشكلة بأقرب وقت. لا تخلط بين خاصيات للقراءة فقط والمتغيرات الثابتة؛ فالمتغيرات الثابتة تكون مكتوبة كلها بحروف كبيرة وتعتمد على المبرمج بعدم تعديلها، ويُفترض أن تبقى قيمتها ثابتة طول فترة تنفيذ البرنامج. خاصية للقراءة فقط هي مثل أي سمة تكون مرتبطة مع كائن. لا يمكن ضبط أو حذف خاصيات للقراءة فقط مباشرةً ولكنها يمكن أن تُقَيّم مع قيمة متغيرة. تتغير الخاصية total للصنف الخاص بنا WizCoin، إذا تغيرت الخاصيات galleons و sickles و knuts. أين تستخدم الخواص كما رأينا في القسم السابق، تقدم الخاصيات سيطرةً أكثر على كيفية استخدام سمات الصنف، وهذه طريقة خاصة ببايثون لكتابة الشيفرة. تشير التوابع المسماة getSomeAttribute() و setSomeAttribute() أنه يجب استخدام الخاصيات بدلًا عن ذلك. هذا لا يعني أن كل نسخة من تابع مبدوء بجلب get أو ضبط set يجب استبداله مباشرةً بخاصية، بل هناك مواقف يجب فيها استخدام التابع حتى لو بدأ اسمه بجلب أو ضبط وهذه بعض الأمثلة: العمليات البطيئة التي تستغرق أكثر من ثانية أو ثانيتين، مثل تنزيل أو رفع الملفات. العمليات التي لديها آثار جانبية، مثل حدوث تغييرات لسمات وكائنات أخرى. العمليات التي تتطلب وسائط إضافية لتمرر إلى عمليات الجلب أو الضبط، مثل استدعاء تابع emailObj.getFileAttachment(filename). الخلاصة ينظر المبرمجون إلى التوابع بكونها أفعالًا (أي أن التوابع تنجز أعمالًا)، ويعدّوا السمات والخاصيات أسماءً (أي أنها تدل على عنصر أو كائن). إذا كانت الشيفرة الخاصة بك تفعل فعل الجلب أو الضبط بدلًا من جلب أو ضبط العنصر، ربما من الأفضل استخدام تابع جلب أو ضبط. يعتمد هذا القرار على ما يبدو صحيحًا لك كمبرمج بنهاية المطاف. الميزة العظمى لاستخدام خاصيات بايثون هو أنك لست بحاجة لاستخدامها عندما تُنشئ الصنف الخاص بك، إذ يمكنك استخدام السمات العادية وإذا احتجت استخدام الخاصيات لاحقًا يمكنك تحويل السمات إلى خاصيات دون تغيير أو الاستغناء عن أي شيفرة خارج الصنف. عندما ننشئ خاصية باسم السمة، يمكننا إعادة تسمية السمة باستخدام بادئة الشرطة السفلية وسيعمل البرنامج الخاص بنا كما فعل سابقًا. تطبّق بايثون الخاصيات كائنية التوجه بصورةٍ مختلفة عن باقي لغات البرمجة كائنية التوجه مثل جافا و C++، فبدلًا من تقديم توابع جالبة وضابطة محددة، لدى بايثون خاصيات تسمح لك بتدقيق السمات وجعلها للقراءة فقط. ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properties and dunder methods من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد الدليل السريع إلى لغة البرمجة بايثون Python 3 البرمجة كائنية التوجه البرمجة كائنية التوجه (Object Oriented Programming) في بايثون - الجزء الأول مختصر البرمجة كائنية التوجه OOP وتطبيقها في بايثون توثيق بايثون
-
استعرضنا في المقال السابق مفهوم الوراثة في البرامج كائنية التوجه، سنتابع في هذا المقال الموضوع ذاته إذ سنستعرض بعض التوابع المهمة بهذا الخصوص، إضافةً إلى مناقشة مفهوم الوراثة المتعددة الموجودة في لغة بايثون. الدالتين isinstance() و isssubclass() يمكننا تمرير الكائن عندما نريد معرفة نوعه إلى الدالة type() المضمنة كما تحدثنا سابقًا، ولكن إذا أردنا التحقق من نوع كائن فيُفضل استخدام الدالة المبنية مسبقًا isinstance()، التي تعيد الدالة قيمة True إذا كان الكائن في الصنف المعطى أو صنفها الفرعي. اكتب ما يلي في الصدفة التفاعلية: >>> class ParentClass: ... pass ... >>> class ChildClass(ParentClass): ... pass ... >>> parent = ParentClass() # إنشاء كائن ParentClass >>> child = ChildClass() # إنشاء كائن ChildClass >>> isinstance(parent, ParentClass) True >>> isinstance(parent, ChildClass) False 1 >>> isinstance(child, ChildClass) True 2 >>> isinstance(child, ParentClass) True لاحظ أن isinstance() تشير إلى أن كائن ChildClass في child هو نسخةٌ من ChildClass (السطر ذو الرقم 1) ونسخةٌ من ParentClass (السطر ذو الرقم 2)، وهذا منطقي لأن كائن ChildClass له علاقة من نوع "is a" مع نوع كائن ParentClass، أي أنه نوع من هذا الكائن. يمكن أيضًا تمرير صف tuple من كائنات الأصناف مثل وسيط ثانٍ لمعرفة إذا كان الوسيط الأول هو واحد من الأصناف الموجودة في الصف: # تُعيد True إذا كانت القيمة 42 عددًا صحيحًا أو سلسلة نصية أو قيمة بوليانية >>> isinstance(42, (int, str, bool)) True if 42 is an int, str, or bool. True الدالة المضمنة الأخرى issubclass() أقل شيوعًا من isinstance() ويمكنها التعرُّف ما إذا كان كائن الصنف الممر إلى الوسيط الأول هو صنف فرعي (أو نفس الصنف) لكائن الصنف المرر إلى الوسيط الثاني: >>> issubclass(ChildClass, ParentClass) # ChildClass صنف فرعي من ParentClass True >>> issubclass(ChildClass, str) # ChildClass ليس صنفًا فرعيًا من من str False >>> issubclass(ChildClass, ChildClass) # ChildClass هو ChildClass True يمكنك تمرير صف من كائنات الصنف بمثابة وسيط ثاني إلى issubclass() كما هو الحال مع Isinstance()، وذلك لرؤية ما إذا كان الوسيط الأول هو صنف فرعي لأي من الأصناف في الصف. الفارق الأساسي بين isinstance() و issubclass() هو أن issubclass() تمرر كائني صنف و isinstance() تمرر كائن وكائن صنف. توابع الصنف ترتبط توابع الصنف مع صنف أكثر مقارنةً بالكائنات المفردة مثل التوابع العادية. يمكنك ملاحظة تابع الصنف في الشيفرة عندما ترى علامتين، هما: المزخرف @classmethod قبل تعليمة التابع def، واستخدام cls معاملًا أولًا كما في المثال التالي: class ExampleClass: def exampleRegularMethod(self): print('This is a regular method.') @classmethod def exampleClassMethod(cls): print('This is a class method.') # استدعاء تابع الصنف دون إنشاء نسخة كائن ExampleClass.exampleClassMethod() obj = ExampleClass() # بالنظر إلى السطر السابق، السطرين التاليين متكافئين obj.exampleClassMethod() obj.__class__.exampleClassMethod() يعمل المعامل cls مثل self ولكن self تشير إلى كائن بينما يشير المعامل cls إلى صنف الكائن، هذا يعني أن الشيفرة في تابع الصنف لا يمكنها الوصول إلى خاصيات الكائن المفردة أو استدعاء توابع الكائن العادية. تستدعي توابع الأصناف توابع أصناف أخرى وتستطيع الوصول إلى سمات الصنف. نستخدم الاسم cls لأن class هي كلمة مفتاحية في بايثون وكما هو الحال مع باقي الكلمات المفتاحية مثل if و while و import، فنحن لا نستطيع استخدامها في أسماء المعاملات، ونستدعي غالبًا سمات الأصناف من خلال كائن الصنف، مثل ExampleClass.exampleClassMethod()، إلا أنه يمكننا استدعاؤهم من خلال أي كائن من الصنف كما في obj.exampleClassMethod(). لا تُستخدم توابع الصنف عمومًا وأكثر الحالات استخدامًا هي لتوفير بديل عن توابع الباني constructorإضافةً للتابع __init__(). على سبيل المثال، ماذا لو كانت دالة الباني تقبل سلسةً نصيةً من البيانات يحتاجها الكائن الجديد أو سلسلة نصية لاسم ملف يحتوي البيانات التي يحتاجها الكائن الجديد؟ لا نحتاج إلى قائمة معاملات التابع ()__init__ لأنها ستكون طويلة ومعقدة، ونستخدم تابع دالة يعيد كائن جديد بدلًا من ذلك. مثلًا، لننشئ صنف AsciiArt (مررنا عليه سابقًا) الذي يستخدم محارف نصية ليشكل صورة: class AsciiArt: def __init__(self, characters): self._characters = characters @classmethod def fromFile(cls, filename): with open(filename) as fileObj: characters = fileObj.read() return cls(characters) def display(self): print(self._characters) # Other AsciiArt methods would go here... face1 = AsciiArt(' _______\n' + '| . . |\n' + '| \\___/ |\n' + '|_______|') face1.display() face2 = AsciiArt.fromFile('face.txt') face2.display() لدى صنف AsciiArt تابع ()__init__ الذي يمكن أن يمرر محارف النص الصورة مثل سلسلة نصية. لديه أيضًا تابع صنف fromFile() الذي يمكن أن يمرر السلسلة النصية لاسم الملف مثل ملف نصي يحتوي فن آسكي ASCII art. يُنشئ كلا التابعين كائنات AsciiArt. نفذ البرنامج وسيكون هناك ملف face.txt يحتوي على وجه فن آسكي ASCII، ليكون الخرج على النحو التالي: _______ | . . | | \___/ | |_______| _______ | . . | | \___/ | |_______| يجعل تابع الصنف fromFile() الشيفرة الخاصة بك سهلة القراءة مقارنةً بجعل ()__init__ يفعل كل شيء. ميزة أُخرى لتابع الصنف هو أن صنف فرعي من AsciiArt يمكن أن يرث تابع fromFile() الخاص (وإعادة تعريفه إذا لزم)، وهذا هو سبب استدعاء cls(characters) في تابع صنف AsciiArt بدلًا من AsciiArt(characters). يعمل استدعاء ()cls أيضًا في الأصناف الفرعية للصنف AsciiArt دون تعديل لأن صنف AsciiArt ليس متوفرًا في التابع، ولكن استدعاء AsciiArt() يستدعي ()__init__ الخاص بصنف AsciiArt بدلًا من ()__init__ الخاص بالصنف الفرعي. يمكنك التفكير في cls على أنها "كائن يمثل هذا الصنف". خذ بالحسبان أنه يجب أن تستخدم التوابع العادية معامل self في مكان ما في الشيفرة الخاصة بهم، ويجب على تابع الصنف دائمًا استخدام المعامل cls. إذا لم يستخدم أبدًا تابع الصنف المعامل cls، فهذه إشارة أن تابع الصنف الخاص بك يجب أن يكون تابعًا عاديًا. سمات الأصناف سمة الصنف هي متغير ينتمي إلى صنف بدلًا من كائن. ننشئ سمة صنف داخل الصنف ولكن خارج كل التوابع كما أنشأنا متغيرات عامة في ملف ".py" ولكن خارج كل الدوال. هذا مثال عن سمة صنف اسمها count التي تحصي عدد كائنات CreatCounter المُنشأة. class CreateCounter: count = 0 # هذه سمة لصنف def __init__(self): CreateCounter.count += 1 print('Objects created:', CreateCounter.count) # تطبع 0 a = CreateCounter() b = CreateCounter() c = CreateCounter() print('Objects created:', CreateCounter.count) # تطبع 3 لدى صنف CreatCounter سمة صنف واحدة اسمها count. كل كائنات CreatCounter لديهم هذه السمة بدلًا من أن يكون لكل منهم سمات count منفصلة. لهذا يعد السطر CreateCounter.count += 1 في دالة الباني كل كائن CreatCounter مُنشأ. عندما تنفذ البرنامج، يكون الخرج على النحو التالي. Objects created: 0 Objects created: 3 نادرًا ما نستخدم سمات الصنف حتى هذا المثال "عد كم كائن CreatCounter مُنشأ" يمكن عمله باستخدام متغير عام بدلًا من سمة صنف. التوابع الساكنة لا يحتوي التابع الساكن معاملي self و cls، فالتوابع الساكنة هي دوال لأنها لا تستطيع الوصول إلى سمات أو توابع الصنف وكائناتها. نادرًا ما ستحتاج لاستخدام التوابع الساكنة في بايثون، وإذا قررت إنشاء واحد ننصح جدًا بإنشاء تابع عادي بدلًا عنه. نعرّف التوابع الساكنة بوضع مزخرف @staticmethod قبل تعليمة def الخاصة بهم. هذا مثال عن تابع ساكن: class ExampleClassWithStaticMethod: @staticmethod def sayHello(): print('Hello!') # لم يُنشأ أي كائن، فاسم الصنف يسبق sayHello() Note that no object is created, the class name precedes sayHello(): ExampleClassWithStaticMethod.sayHello() لا يوجد فرق تقريبًا بين التابع الساكن sayHello() في صنف ExampleClassWithStaticMethod والدالة sayHello(). ربما تفضل بالواقع استخدام دالة لأنك تستطيع استدعائها دون الدخول إلى اسم الصنف مسبقًا. التوابع الساكنة شائعة في لغات برمجة أخرى ليس لديها ميزات لغة بايثون المرنة. تضمين التوابع الساكنة inclusion of static methods في بايثون هو لمحاكاة اللغات الأخرى ولا يقدم قيمةً عملية. متى تستخدم الأصناف والميزات كائنية التوجه الساكنة؟ نادرًا ما تحتاج لاستخدام توابع الصنف وسمات الصنف والتوابع الساكنة، فهم عرضةً للاستخدام الزائد إذا كنت تعتقد بالتساؤل التالي: " لماذا لا استخدم الدوال أو المتغيرات العامة بدلًا عن ذلك؟" هذا تلميح لعدم استخدام توابع الأصناف أو سمات الأصناف أو التوابع الساكنة. السبب الوحيد الذي جعلنا نناقش هذه المفاهيم في سلسلة المقالات متوسطة المستوى هو للتعرف عليهم عندما تراهم في الشيفرة، ولكن لا يشجع كثير من المبرمجين على استخدامهم، إذ سيكونوا مفيدين إذا أردت استخدام هيكلية خاصة بك مع مجموعة معقدة من الأصناف التي تتوقع أن تكون أصناف فرعية للمبرمجين الذين سيستخدمون الهيكلية، لكنك لا تحتاجهم عندما تكتب تطبيقات بايثون مباشرةً. للمزيد عن هذه الميزات وعن احتياجهم أو لا، اقرأ منشور فيليب ج. ايبي Phillip J. Eby "بايثون ليس جافا" الموجود على الرابط dirtsimple.org/2004/12/python-is-not-java.html ومنشور ريان تومايكو Ryan Tomayko "مفهوم التابع الساكن" على الرابط tomayko.com/blog/2004/the-static-method-thing. كلمات مهمة كائنية التوجه يبدأ شرح البرمجة كائنية التوجه OOP بالكثير من المصطلحات مثل الوراثة والتغليف Encapsulation والتعددية الشكلية Polymorphism. أهمية معرفة هذه المصطلحات مبالغ فيه، ولكن يجب عليك أن يكون لديك فهم أساسي لهم، شرحنا الوراثة سابقًا، لذا سنشرح المصطلحات الباقية تاليًا، ويمكنك الاطلاع على مقال البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C على أكاديمية حسوب لمزيدٍ من المعلومات حول هذه المصطلحات. التغليف لدى كلمة تغليف معنيين شائعين ولكن متقاربين. التعريف الأول هو تجميع البيانات المتعلقة والشيفرة في وحدة واحدة، أي لتغلف يعني أن تضع في صندوق. هذا ما تفعله الأصناف عمومًا؛ فهي تدمج السمات والتوابع. مثلًا، يغلف صنف WizCoin ثلاثة أعداد صحيحة لـ knuts و sickles و galleons إلى كائن WizCoin واحد. أما التعريف الثاني فهو تقنية لإخفاء المعلومات تسمح للكائنات بإخفاء تفاصيل تنفيذ معقدة عن كيفية عمل الكائنات. رأينا ذلك في "السمات والتوابع الخاصة"، إذ يقدم كائن BankAccount توابع deposit() و withdraw() لإخفاء تفاصيل كيفية التعامل مع السمة _balance. تعمل الدوال مثل صندوق أسود: كيفية حساب الدالة math.sqrt() للجذر التربيعي لأي رقم مخفية، كل ما عليك معرفته هو أن تعيد الدالة الجذر التربيعي للرقم الممرر لها. التعددية الشكلية polymorphism تسمح التعددية الشكلية بمعالجة كائنات من نوع ما على أنها كائنات من نوع آخر، فمثلًا تعيد الدالة ()len طول الوسيط الممرر إليها، ويمكنك تمرير سلسلة نصية إلى هذه الدالة لمعرفة عدد المحارف المكونة منه، وكذلك يمكن قائمة list أو قاموس dictionary لمعرفة عدد العناصر، أو عدد أزواج مفتاح-قيمة key-value على الترتيب. يدعى نموذج التعددية الشكلية هذا باسم الدوال المعممة generic functions أو التعددية الشكلية القياسية parametric polymorphism لأنها تعالج كائنات ذات أنواع مختلفة. يمكنك الاطلاع على مقال مفهوم البرمجة المعممة Generic Programming على أكاديمية حسوب لمزيدٍ من المعلومات عن البرمجة المعممة. يمكن الإشارة إلى التعددية الشكلية بمصطلح التعددية الشكلية الخاصة ad hoc polymorphism أو زيادة تحميل للعامل operator overloading، إذ يمكن أن يأخذ المعامل (مثل + أو *) سلوكًا مختلفًا اعتمادًا على نوع الكائنات التي تُجرى عليها العملية؛ فمثلًا يجري المعامل + عملية الجمع الحسابي عندما تُجرى العملية على عددين صحيحين أو عشريين، لكنها توصل السلاسل النصية في حال كانت العملية على سلسلتين بدلًا من عددين. لماذا لا نستخدم الوراثة؟ من السهل زيادة تعقيد الأصناف باستخدام الوراثة. كما يشير لوتشيانو رامالهو Luciano Ramalho : "وضع الكائنات في هرمية يرضي حس الترتيب لدينا، ولكن المبرمجين يفعلونها للتسلية". ننشئ أصناف وأصناف فرعية وأصناف تحت فرعية عندما تكون الحاجة لصنف واحد أو اثنين في الوحدة لتفي بالغرض، ولكن تذكر حكمة بايثون الأهم التي ناقشناها سابقًا؛ الحل البسيط أفضل من الحل المعقد. يسمح استخدام البرمجة كائنية التوجه OOP بتنظيم الشيفرة الخاصة بك إلى وحدات units (في هذه الحالة أصناف) سهلة التعامل بدلًا من ملف ".py" واحد يحتوي مئات التوابع المعرفة بدون ترتيب معين. تفيد الوراثة إذا كان لديك عدة دوال تعمل في نفس القاموس أو هيكل قائمة البيانات؛ ففي هذه الحالة من المفيد ترتيبهم في صنف. هناك بعض الأمثلة لعدم إنشاء الأصناف أو استخدام الوراثة: إذا كان الصنف يتألف من توابع لا تستخدم المعاملين self و cls، احذف الأصناف واستخدم الدوال بدلًا من التوابع. إذا أنشأت أب بصنف ابن واحد ولم تُنشئ كائنات من الصنف الأب، يمكنك جمعهم بصنف واحد. إذا أنشأت أكثر من ثلاثة أو أربعة مستويات من الأصناف الفرعية، ربما تكون قد استخدمت الوراثة على نحوٍ زائد، اجمع هذه الأصناف الفرعية إلى أصناف أقل. كما توضّح سابقًا في نسختي برنامج إكس أو tic-tac-toe (مع برمجة كائنية التوجه وبدونها)، من الممكن الحصول على برنامج يعمل على نحوٍ سليم وبدون أخطاء دون استخدام الأصناف. لست بحاجة لتصميم برنامج مثل شبكة معقدة من الأصناف، إذ أن الحل البسيط أفضل من الحل المعقد الذي لا يعمل. يتحدث جول سبلوسكي Joel Spolsky عن ذلك في منشوره "لا تدع المصممين رواد الفضاء أن يخيفوك" الموجود على الرابط joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you. يجب أن تعرف الآن كيفية عمل مفاهيم البرمجة كائنية التوجه مثل الوراثة، لأنها تساعدك على تنظيم الشيفرة الخاصة بك وجعل التطوير ومعالجة الأخطاء أسهل. تتمتع لغة بايثون بالمرونة، فهي تقدم لك ميزات برمجة كائنية التوجه، لكنها أبضًا لا تطلب منك استخدامها عندما لا تناسب احتياجات البرنامج الخاص بك. الوراثة المتعددة في العديد من لغات البرمجة يكون الصنف أب واحد فقط، ولكن بايثون تدعم آباء متعددين عن طريق تقديم ميزة تدعى الوراثة المتعددة multiple inheritance. مثلًا، يمكننا الحصول على صنف Airplane مع تابع flyInTheAir() وصنف Ship مع تابع floatOnWater()، ويمكننا إنشاء صنف FlyingBoat يرث كلًا من Airplane و Ship عن طريق تحديدهما في تعليمة class مفصولين بفواصل. افتح ملف جديد في محرر النصوص واحفظ التالي flayingboat.py: class Airplane: def flyInTheAir(self): print('Flying...') class Ship: def floatOnWater(self): print('Floating...') class FlyingBoat(Airplane, Ship): pass سيرث الكائن المُنشأ التابعين flyInTheAir() و floatOnWater() كما سنرى في الصدفة التفاعلية: >>> from flyingboat import * >>> seaDuck = FlyingBoat() >>> seaDuck.flyInTheAir() Flying... >>> seaDuck.floatOnWater() Floating... الوراثة المتعددة مفهوم بسيط طالما كانت أسماء توابع الأصناف مميزة ولا تتقاطع، وتسمى هذه الأصناف mixins (هذا مصطلح عام لهذا النوع من الأصناف، إذ لا يوجد في بايثون كلمة mixin مفتاحية)، ولكن ماذا سيحصل إذا ورثنا عدة أصناف معقدة تتشارك بأسماء التوابع؟ مثلًا تذكر أصناف لوحة إكس أو MiniBoard و HintTTTBoard سابقًا، ماذا لو أردنا صنف يظهر لوحة إكس أو مصغرة مع تقديم بعض النصائح؟ يمكننا إعادة استخدام هذه الأصناف الموجودة باستخدام الوراثة المتعددة. ضِف التالي إلى نهاية ملف tictactoe_oop.py ولكن قبل تعليمة if التي تستدعي الدالة main(): class HybridBoard(HintBoard, MiniBoard): pass لا يوجد شيء في هذا الصنف، إذ يُعيد استخدام الشيفرة عن طريق وراثة HintBoard و MiniBoard. عدّل الشيفرة في الدالة main() لتُنشئ كائن HybridBoard: gameBoard = HybridBoard() # إنشاء كائن TTT للّوحة لدى كلا الصنفين الأب MiniBoard و HintBoard تابع اسمه getBoardStr() فما الذي ترثه HybridBoard؟ عندما تنفذ البرنامج سيظهر الخرج لوحة إكس أو مصغرة تحتوي على بعض التلميحات: --snip-- X.. 123 .O. 456 X.. 789 X can win in one more move. يبدو أن بايثون دمجت سحريًا تابع getBoardStr() الخاص بصنف MiniBoard و getBoardStr() الخاص بصنف HintBoard، وهذا ممكن لأننا كتبنا التابعين بشكل يمكّنهما العمل مع بعضهما. إذا بدلت ترتيب الأصناف في تعليمة class في صنف HybridBoard لتصبح على النحو التالي: class HybridBoard(MiniBoard, HintBoard): فستخسر التلميحات كليًا: --snip-- X.. 123 .O. 456 X.. 789 لتفهم لماذا حصل ذلك يجب عليك فهم ترتيب استبيان التوابع method resolution order -أو اختصارًا MRO- الخاص ببايثون وكيفية عمل دالة super(). ترتيب استبيان التابع لدى برنامج إكس أو الخاص بنا أربعة أصناف لتمثيل الألواح، ثلاثة معرفة بتابع getBoardStr() وواحدة بتابع getBoardStr() موروث كما في الشكل 2 [الشكل 2: الأصناف الأربعة في برنامج لوحات إكس أو] عندما نستدعي getBoardStr() على الكائن HybridBoard، يعرف بايثون أن الصنف HybridBoard ليس لديه تابع بذلك الاسم لذا تفحص أصناف الأب، ولكن لدى الصنف هذا صنفين أب وكلاهما لديه تابع getBoardStr()، أي منها يُستدعى؟ يمكننا معرفة ذلك من التحقق من ترتيب استبيان التابع MRO الخاص بصنف HybridBoard وهي القائمة المرتبة من الأصناف التي يتحقق منها بايثون عند وراثة التوابع، أو عندما يستدعي التابع دالة super(). يمكنك رؤية ترتيب استبيان التابع للصنف HybridBoard عن طريق استدعاء mro() في الصدفة التفاعلية: >>> from tictactoe_oop import * >>> HybridBoard.mro() [<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>] يمكنك من خلال القيمة المُعادة رؤية أنه عندما يُستدعى التابع على HybridBoard، يتحقق بايثون من صنف HybridBoard؛ فإذا لم يكن موجودًا، يتحقق بايثون من صنف HintBoard وبعدها من صنف MiniBoard وأخيرًا من صنف TTTBoard. في آخر كل قائمة ترتيب استبيان الدوال MRO هناك صنف object مضمّن يمثل الصنف الأب لكل الأصناف في بايثون. معرفة ترتيب استبيان الدوال MRO من أجل وراثة واحدة أمر سهل؛ فقط اصنع سلسلة chain من أصناف الأب، أما بالنسبة للوراثة المتعددة سيكون الأمر أصعب. يتبع ترتيب استبيان الدوال MRO الخاص ببايثون خوارزمية C3 -التي تقع تفاصيل مناقشتها خارج سياق موضوعنا- ولكنك تستطيع تحديد ترتيب استبيان الدوال MRO بتذكر قاعدتين: يتحقق بايثون من الأصناف الابن قبل أصناف الأب. يتحقق من الأصناف الموروثة في القائمة من اليسار إلى اليمين في تعليمة class. إذا استدعينا getBoardStr() على كائن HybridBoard، يتحقق بايثون من الصنف HybridBoard أولًا وبعدها ونظرًا لكون أصناف الأب من اليسار إلى اليمين هي HintBoard و MiniBorad، يتحقق بايثون من HintBoard. لدى الصنف الأب هذا تابع getBoardStr() لذا يرثها HybridBorad ويستدعيها. لا ينتهي الأمر هنا، يستدعي التابع super().getBoardStr()، إذ أن كلمة "super" هي كلمة مضللة نوعًا ما لدالة super() الخاصة ببايثون، لأنها لا تعيد الصنف الأب ولكن الصنف الذي يليها في ترتيب استبيان التوابع MRO، وهذا يعني عندما نستدعي getBoardStr() على الكائن HybridBoard، يكون الصنف التالي في ترتيب استبيان التوابع MRO بعد HintBoard هو MiniBoard وليس الصنف الأب TTTBoard، لذا استدعاء super().getBoardStr() يستدعي تابع getBoardStr() لصنف MiniBoard الذي يعيد سلسلة نصية للوحة إكس أو المصغرة. تعلّق الشيفرة المتبقية في getBoardStr() الخاصة بصنف HintBoard بعد استدعاء super() نص التلميح لهذه السلسة النصية. إذا غيرنا تعليمة class في صنف HybridBoard لتضع MiniBoard أولًا و HintBoard ثانيًا، سيضع ترتيب استبيان التوابع MRO الصنف MiniBoard قبل الصنف HintBoard، ما يعني أن HybridBorad ترث getBoardStr() من MiniBoard التي لا تحتوي استدعاء super(). هذا الترتيب هو الذي سبّب الخطأ الذي جعل لوحة إكس أو المصغرة تظهر بدون تلميحات؛ فبدون استدعاء super() تابع getBoardStr() الخاص بصنف MiniBoard لا يستدعي تابع getBoardStr() الخاص بصنف HintBoard. تسمح لك الوراثة المتعددة في إنشاء وظائف كثيرة في كمية قليلة من الشيفرة، لكنها تقود إلى شيفرة معقدة وصعبة القراءة. فضّل الوراثة الواحدة أو أصناف mixin أو عدم الوراثة، هذه التقنيات غالبًا ما تكون قادرة على تنفيذ مهام البرنامج الخاص بك. الخلاصة كما تعيد type() نوع الكائن المرر لها، تعيد توابع isinstace() و issubclass() نوع ومعلومات الوراثة عن الكائن الممرر لها. يمكن أن تحتوي الأصناف توابع كائن وسمات، لكنها تحتوي أيضًا توابع صنف وسمات صنف وتوابع ساكنة، على الرغم من أنها نادرة الاستخدام لكن يمكنها أن تسمح بالتقنيات كائنية التوجه التي لا تستطيع المتغيرات العامة والدوال أن تقدمها. بسمح بايثون للأصناف أن ترث من عدة آباء، على الرغم من أن ذلك ينتج شيفرة صعبة الفهم. تستطيع دالة super() وتوابع الصنف اكتشاف كيف سترث التوابع اعتمادًا على ترتيب استبيان التوابع MRO، إذ يمكنك مشاهدة ترتيب استبيان التوابع MRO الخاص بصنف في الصدفة التفاعلية عن طريق استدعاء التابع mro() على الصنف. غطينا في هذا المقال والمقالات السابقة مفاهيمًا عامة في البرمجة كائنية التوجه OOP، وسنتحدث تاليًا عن تقنيات برمجة كائنية التوجه OOP خاصة ببايثون. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق البرمجة كائنية التوجه Object-Oriented Programming والوراثة Inheritance البرمجة كائنية التوجه (Object Oriented Programming) في بايثون البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C الوراثة المتعددة multiple inheritance
-
يوفر عليك استدعاء وتعريف الدوال من عدة أماكن نسخ ولصق الشيفرة المصدرية، إذ أن عدم تكرار الشيفرة هو ممارسة جيدة لأنه إذا أردت تغيير هذه الشيفرة المكرّرة (إما لحل بعض الأخطاء أو لإضافة ميزات جديدة)، فستحتاج فقط لتغييرها في مكان واحد، ويصبح البرنامج أقصر دون شيفرة مكررة وأسهل للقراءة. الأمر مماثل بالنسبة للدوال، الوراثة inheritance هي تقنية لإعادة استخدام الشيفرة، ويمكن تطبيقها في الأصناف، وهي وسيلة لوضع الأصناف في علاقة أب-ابن، بحيث يرث الصنف الابن نسخةً من توابع الصنف الأب، ويخفف عليك عبء تكرار التوابع في عدة أصناف. يعتقد العديد من المبرمجين أن الوراثة أمرٌ مبالغٌ فيه أو خطر بسبب زيادة تعقيد شبكات وراثة الأصناف المضافة إلى البرنامج. ليست المنشورات المدونة بعنوان "الوراثة خطيرة" خاطئة كليًا، فمن السهل استغلال البرنامج، ولكن الاستخدام المحدود لهذه التقنية يمكن أن يوفر الكثير من الوقت بما يتعلق بتنظيم الشيفرة. كيف تعمل الوراثة نضع اسم الصنف الأب الموجود أساسًا بين قوسين في تعليمة class لإنشاء صنف ابن. لتتدرب على إنشاء صنف ابن، نفتح نافذة محرر الملفات ونكتب الشيفرة التالية ونحفظها في ملف inheritanceExample.py: 1 class ParentClass: 2 def printHello(self): print('Hello, world!') 3 class ChildClass(ParentClass): def someNewMethod(self): print('ParentClass objects don't have this method.') 4 class GrandchildClass(ChildClass): def anotherNewMethod(self): print('Only GrandchildClass objects have this method.') print('Create a ParentClass object and call its methods:') parent = ParentClass() parent.printHello() print('Create a ChildClass object and call its methods:') child = ChildClass() child.printHello() child.someNewMethod() print('Create a GrandchildClass object and call its methods:') grandchild = GrandchildClass() grandchild.printHello() grandchild.someNewMethod() grandchild.anotherNewMethod() print('An error:') parent.someNewMethod() عندما ننفذ البرنامج، يكون الخرج على النحو التالي: Create a ParentClass object and call its methods: Hello, world! Create a ChildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Create a GrandchildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Only GrandchildClass objects have this method. An error: Traceback (most recent call last): File "inheritanceExample.py", line 35, in <module> parent.someNewMethod() # ParentClass objects don't have this method. AttributeError: 'ParentClass' object has no attribute 'someNewMethod' أنشأنا ثلاثة أصناف ParentClass في السطر (1) و ChildClass في السطر (3) و GrandchildClass في السطر (4). الصنف ChildClass هو صنف فرعي للصنف ParentClass، يعني أن ChildClass لديها توابع ParentClass نفسها، ونقول أن ChildClass يرث التوابع من ParentClass، وأيضًا GrandchildClass هو صنف فرعي من ChildClass وبالتالي لديه كل توابع ChildClass وأبيها ParentClass. نسخنا ولصقنا الشيفرة من التابع printHello() باستخدام هذه الطريقة إلى الصنفين ChildClass و GrandChild. أي تغيير للشيفرة في PrintHello() لا يحدث فقط في ParentClass بل في ChildClass و GrandchildClass. هذا نفس تغيير الشيفرة في دالة تُحدّث كل استدعاءات الدالة الخاصة بها. يمكنك رؤية هذه العلاقة في الشكل 1. لاحظ في مخططات الأصناف أن السهم ينطلق من الصنف الفرعي ويشير إلى الصنف الأساس. هذا يعكس أن كل صنف يعرف دائمًا الصنف الأساس الخاص به ولكنه لا يعرف أصنافه الفرعية. [الشكل 1: مخطط هرمي (يسار) ومخطط فين Venn (يمين) يبينان العلاقات بين الأصناف الثلاثة والتوابع التي يستخدموها] تمثّل الأصناف أب- ابن عادةً علاقات "is a"، إذ أن كائن ChildClass هو كائن ParentClass لأن لديه نفس التوابع التي لدى كائن ParentClass، إضافةً إلى بعض التوابع الإضافية التي يعرّفها. هذه هي علاقة باتجاه واحد: ليس كائن ParentClass هو كائن ChildClass. إذا حاول كائن استدعاء someNewMethod() الموجود فقط لكائنات ChildClass (وأصناف ChildClass الفرعية) يعطي بايثون Python خطأ AttributeError. يمكنك الاطلاع على مقال مخططات الفئات (Class Diagram) في لغة النمذجة الموحدة UML على أكاديمية حسوب لمزيدٍ من المعلومات على علاقات "is a" وغيرها في مخططات الأصناف. يعتقد المبرمجون أن الأصناف المتعلقة ببعضها تندرج تحت هرمية علاقات "is a" واقعية، فغالبًا ما ترى في تدريبات البرمجة كائنية التوجه OOP أصناف أب وابن وحفيد: Vehicle▶FourWheelVehicle▶Car أو Animal▶Bird▶Sparrow أو Shape▶Rectangle▶Square لكن تذكر أن السبب الأساسي للوراثة هو إعادة استعمال الشيفرة. تسمح لك الوراثة بتفادي نسخ ولصق الشيفرة إذا كان البرنامج الخاص بك يحتاج إلى صنف بمجموعة من التوابع التي هي مجموعة كبرى من توابع صنف أُخر. نسمي أحيانًا الصنف الابن بالصنف الفرعي subclass أو الصنف المُشتق derived class ونسمي الصنف الأب الصنف الأعلى super class أو الصنف الأساس base class، ويمكنك الاطلاع على مقال الوراثة والتعددية الشكلية Polymorphism والأصناف المجردة Abstract Classes في جافا على أكاديمية حسوب لمزيدٍ من المعلومات. إعادة تعريف التوابع ترث الأصناف الفرعية كل توابع الأصناف الأب، ويمكن لصنف ابن إعادة تعريف تابع موروث عن طريق تقديم التابع والشيفرة الخاصين بها. يكون لدى التابع الذي يعيد التعريف اسم تابع الصنف الأب ذاته. لتوضيح هذا المفهوم لنعد إلى لعبة إكس أو Tic-Tac-Toe التي أنشأناها سابقًا، ولكن هذه المرة سننشئ صنفًا جديدًا MiniBoard وهو صنف فرعي من TTTBoard يعيد تعريف getBoardStr() لرسم لوحة إكس أو أصغر. سيسأل البرنامج أي نوع لوح سيستخدم ولا نحتاج إلى نسخ ولصق باقي توابع TTTBoard لأن MiniBoard سيرثهم. ضِف التالي في نهاية ملف ticktactoe_oop.py لإنشاء صنف ابن لصنف TTTBoard الأصلي، ثم أعد كتابة تابع getBoardStr(): class MiniBoard(TTTBoard): def getBoardStr(self): """Return a tiny text-representation of the board.""" # Change blank spaces to a '.' for space in ALL_SPACES: if self._spaces[space] == BLANK: self._spaces[space] = '.' boardStr = f''' {self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123 {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456 {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789''' # Change '.' back to blank spaces. for space in ALL_SPACES: if self._spaces[space] == '.': self._spaces[space] = BLANK return boardStr كما في التابع ()getBoardStr الخاص بصنف TTTBoard سينشئ التابع getBoardStr() الخاص بـ MiniBoard لإظهار سلسلة نصية متعددة الأسطر من لوحة إكس أو عندما تمرر إلى دالة print() ولكن هذه السلسلة النصية هي أقصر وتتجاهل الأسطر بين X و O وتستخدم الفواصل للدلالة على الأماكن الفارغة. غيّر السطر في main() ليستنسخ كائن MiniBoard بدلًا من كائن TTTBoard: if input('Use mini board? Y/N: ').lower().startswith('y'): gameBoard = MiniBoard() # Create a MiniBoard object. else: gameBoard = TTTBoard() # Create a TTTBoard object. يعمل البرنامج كما في السابق ما عدا تغيير هذا السطر الواحد في main() وعندما تنفذ البرنامج الآن سيصبح الخرج على النحو التالي: Welcome to Tic-Tac-Toe! Use mini board? Y/N: y ... 123 ... 456 ... 789 What is X's move? (1-9) 1 X.. 123 ... 456 ... 789 What is O's move? (1-9) --snip-- XXX 123 .OO 456 O.X 789 X has won the game! Thanks for playing! يستطيع البرنامج الآن بسهولة الحصول على تنفيذي صنفي لوح إكس أو، وإذا أردت فقط النسخة المصغرة من اللوحة يمكنك ببساطة استبدال الشيفرة في تابع getBoardStr() في TTTBoard. ولكن إذا أردت الاثنين فالوراثة تسمح لك بسهولة إنشاء صنفين عن طريق إعادة استخدام الشيفرة المشتركة بينهما. يمكننا إضافة سمة attribute جديدة إلى TTTBoard اسمها useMiniBoard إذا لم نستخدم الوراثة، ووضع تعليمة if-else داخل getBoardStr() لتقرر متى تُظهِر اللوحة العادية أو اللوحة المصغرة، سيعمل هذا جيدًا لأن التغيير بسيط، ولكن ماذا لو كان الصنف الفرعي MiniBoard يحتاج لإعادة تعريف تابعين أو ثلاثة توابع أو حتى 100 تابع؟ ماذا لو أردنا إنشاء عدة أصناف فرعية من TTTBoard؟ سيتسبّب عدم استخدام الوراثة بسيل من تعليمات if-else داخل التابع الخاص بنا وزيادة كبيرة في تعقيد الشيفرة. يُمكّننا استخدام الأصناف الفرعية وإعادة تعريف التوابع من ترتيب الشيفرة الخاصة بنا ضمن أصناف منفصلة للتعامل مع حالات استخدام مماثلة. دالة super() يشابه تابع الصنف المعاد تعريفه overridden تابع الصنف الأب؛ فحتى لو كانت الوراثة هي تقنية لإعادة استخدام الشيفرة، قد يتطلب إعادة تعريف التابع إعادة كتابة نفس الشيفرة من تابع الصنف الأب بمثابة جزء من تابع شيفرة الابن. لمنع تكرار الشيفرة: تسمح دالة super() للتابع المعاد تعريفه استدعاء التابع الأصلي في الصنف الأب. مثلًا، لنُنشئ صنفًا جديدًا اسمه HintBoard ليكون صنفًا فرعيًا من TTTBoard، بحيث يعيد هذا الصنف تعريف getBoardStr()، ويضيف بعد رسم لوحة إكس أو تلميحًا hint فيما إذا كان X أو O قد يربح في الخطوة التالية. هذا يعني أن تابع getBoardStr() الخاص بصنف HintBoard سينجز نفس مهام تابع getBoardStr() الخاص بصنف TTTBoard لرسم لوحة إكس أو. بدلًا من تكرار الشيفرة لإنجاز ذلك، يمكننا استخدام super() لاستدعاء تابع getBoardStr الخاص بصنف TTTBoard من تابع getBoardStr() الخاص بصنف HintBoard. ضِف التالي لنهاية ملف tictactoe_oop.ps: class HintBoard(TTTBoard): def getBoardStr(self): """Return a text-representation of the board with hints.""" 1 boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. xCanWin = False oCanWin = False 2 originalSpaces = self._spaces # Backup _spaces. for space in ALL_SPACES: # Check each space: # Simulate X moving on this space: self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = X if self.isWinner(X): xCanWin = True # Simulate O moving on this space: 3 self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = O if self.isWinner(O): oCanWin = True if xCanWin: boardStr += '\nX can win in one more move.' if oCanWin: boardStr += '\nO can win in one more move.' self._spaces = originalSpaces return boardStr أولًا، تنفذ التعليمة super().getBoardStr() في السطر ذو الرقم 1 الشيفرة داخل الصنف getBoardStr() الخاص بصنف TTTBoard، والتي تعيد سلسلةً نصيةً على شكل لوحة إكس أو. نحفظ حاليًا هذه السلسلة في متغير اسمه boardStr. تعالج الشيفرة الباقية إنشاء التلميح بعد إنشاء لوحة السلسلة النصية عن طريق إعادة استخدام getBoardStr() الخاص بصنف TTTBoard. يعيّن تابع getBoardStr() قيمة المتغيرين xCanWin و oCanWin إلى False، وينسخ احتياطيًا القاموس self._spaces إلى المتغير originalSpaces (السطر ذو الرقم 2)، ثم تُنفَّذ حلقة for على كل أماكن اللوحة من 1 إلى 9. تُضبط سمة self._spaces لنسخ المكتبة originalSpaces، وإذا كانت الخلية فارغة تُوضع X مكانها، إذ يحفز هذا تحريك X إلى الفراغ التالي. سيحدد استدعاء self.isWinner() إذا كانت هذه هي الحركة الرابحة؛ فإذا كانت كذلك تصبح xCanWin هي True. تُكرر هذه الخطوات من أجل O لمعرفة ما إذا كان O يربح بالتحرك إلى هذا المكان (السطر ذو الرقم 3). يستخدم هذا التابع وحدة copy لنسخ القاموس في self._spaces لذا نضيف السطر التالي لأول ملف tictactoe.py. import copy نغير بعدها السطر في main() لنستنسخ كائن HintBoard بدلًا من TTTBoard: gameBoard = HintBoard() # Create a TTT board object. يعمل البرنامج كما كان عدا تغيير السطر الوحيد في main() وعندما ينفذ البرنامج سيكون الخرج على النحو التالي: Welcome to Tic-Tac-Toe! --snip-- X| | 1 2 3 -+-+- | |O 4 5 6 -+-+- | |X 7 8 9 X can win in one more move. What is O's move? (1-9) 5 X| | 1 2 3 -+-+- |O|O 4 5 6 -+-+- | |X 7 8 9 O can win in one more move. --snip-- The game is a tie! Thanks for playing! في نهاية التابع: إذا كانت قيمة xCanWin و oCanWin هي True، تُضاف رسالةٌ إضافية تشير إلى ذلك إلى السلسلة النصية boardStr، وأخيرًا تُعاد القيمة boardStr. لا يحتاج كل تابع معاد تعريفه لاستخدام super()؛ فإذا كان يعمل التابع -الذي يعيد التعريف- شيئًا مختلفًا تمامًا عن التابع المُعاد تعريفه في الصنف الأب، لا توجد حاجة لاستدعاء التابع المعاد تعريفه باستخدام super(). تفيد الدالة super() على نحوٍ خاص عندما يكون للصنف أكثر من تابع أب كما موضح في الفقرة "الوراثة المتعددة" لاحقًا. فضل التكون Composition على الوراثة الوراثةهي تقنية جيدة لإعادة استخدام الشيفرة، وقد تفكر باستخدامها فورًا في الأصناف الخاصة بك، ولكن ربما لا تريد دومًا أن يكون الأساس والأصناف الفرعية مرتبطة جدًا، فإنشاء مستويات متعددة من الوراثة لا يرتب الشيفرة الخاصة لك أكثر ما يضيف بيروقراطية. على الرغم من أنه بإمكانك استخدام الوراثة للأصناف ذات العلاقات " is a" (بمعنى آخر، عندما يكون الصنف الابن هو نوع من أنواع الصنف الأب)، من المفضل استخدام تقنية تدعى التكوّن composition للأصناف ذات العلاقات "لديه has a". التكوّن هو تقنية تصميم لضم الكائنات في الأصناف الخاصة بك بدلًا من توارث أصناف تلك الكائنات. هذا ما نفعله عندما نضيف خاصيّات إلى الأصناف الخاصة بنا. عند تصميم الأصناف الخاصة بك باستخدام الوراثة فضّل التكوّن على الوراثة، هذا ما كنا نفعله في كل الأمثلة الحالية والسابقة كما يلي: كائن WizCoin "لديه has a" كمية من النقود من أنواع galleon و sickle و knut. كائن TTTBoard "لديه has a" مصفوفة بتسع فراغات. كائن MiniBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. كائن HintBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. لنعد إلى صنف WizCoin الذي أنشأناه سابقًا. إذا أنشأنا صنف WizardCustomer لتمثل الزبائن في العالم السحري، يجب على هؤلاء الزبائن حمل كمية من المال، الذي نعبّر عنه بصنف WizCoin ولكن لا توجد علاقة "is a" بين الصنفين؛ فكائن WizardCustomer ليس من نوع كائن WizCoin. إذا استخدمنا الوراثة، سنحصل على شيفرة برمجية غير مُعتادة: import wizcoin 1 class WizardCustomer(wizcoin.WizCoin): def __init__(self, name): self.name = name super().__init__(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.weightInGrams()} grams.') في هذا المثال، يرث WizardCustomer توابع الكائن WizCoin مثل value() و weightInGrams(). تقنيًا يمكن للصنف WizardCustomer الذي ورث من WizCoin أن ينجز بجميع المهام التي ينجزها WizardCustomer، والتي تضم كائن WizCoin، كما تفعل السمة، ولكن اسمَي التابعين wizard.value() و wizard.weightInGrams() مضللة؛ إذ يبدو أنها تُعيد قيمة ووزن الساحر بدلًا من قيمة ووزن نقود الساحر. إضافةً إلى ذلك، إذا أردنا لاحقًا إضافة تابع weightInGrams() لوزن الساحر، سيكون هذا الاسم مأخوذًا مسبقًا. من الأسهل أن يكون الكائن WizCoin سمةً لأن الزبون الساحر "لديه" كميةً من نقود الساحر. import wizcoin class WizardCustomer: def __init__(self, name): self.name = name 1 self.purse = wizcoin.WizCoin(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.purse.weightInGrams()} grams.') بدلًا من جعل الصنف WizardCutomer يرث التوابع من WizCoin، نعطي للصنف WizardCutomer سمة purse التي تحتوي كائن WizCoin. أي تغييرات لتوابع الصنف WizCoin عند استخدام التكوّن لن تغير توابع الصنف WizardCustomer. تمنحك هذه الطريقة مرونةً أكبر في تغيير التصميمات المستقبلية لكلا الصنفين وتؤدي إلى شيفرة سهلة الصيانة. مساوئ الوراثة السيئة الأساسية في الوراثة هي أنه أي تغيير مستقبلي يحصل على الأصناف الأب سترثه كل الأصناف الابن. في بعض الحالات هذا الربط الشديد هو ما تحتاجه ولكن في بعض الحالات لا يفي نموذج الوراثة بمتطلبات الشيفرة. مثلًا، لنقل أنه لدينا الأصناف Car و Motorcycle و LunarRover في برنامج محاكاة عربات، ستحتاج هذه الأصناف إلى توابع متماثلة، مثل startIgnition() و changeTire(). بدلًا من نسخ ولصق الشيفرة إلى كل صنف، يمكننا إنشاء صنف أب Vehicle ونجعل Car و Motorcycle و LunarRover يرثونها. الآن نريد إصلاح خطأ في تابع changeTire()، وسنجري التغيير في مكان واحد. هذا مفيد جدًا، إذ لدينا العديد من أصناف العربات التي ترث من Vehicle. ستكون شيفرة هذه الأصناف على النحو التالي: class Vehicle: def __init__(self): print('Vehicle created.') def startIgnition(self): pass # Ignition starting code goes here. def changeTire(self): pass # Tire changing code goes here. class Car(Vehicle): def __init__(self): print('Car created.') class Motorcycle(Vehicle): def __init__(self): print('Motorcycle created.') class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') لكن كل التغييرات المستقبلية على Vehicle ستؤثر على هذه الأصناف الفرعية أيضًا. ماذا سيحدث لو أردنا تابع changeSparkPlug()؟ لدى السيارات والدراجات النارية محركات احتراق بشمعات احتراق ولكن العربات القمرية lunar rovers ليس لديها ذلك. يمكننا -بتفضيل التكوّن على الوراثة- إنشاء صنفي CombustionEngine و ElectricEngine، ثم تصميم صنف Vehicle ليكون "لديه has a" سمة محرك إما CombustionEngine أو ElectricEngine مع التوابع الموافقة. class CombustionEngine: def __init__(self): print('Combustion engine created.') def changeSparkPlug(self): pass # هنا الشيفرة البرمجية التي تعدّل على شمعة الاحتراق class ElectricEngine: def __init__(self): print('Electric engine created.') class Vehicle: def __init__(self): print('Vehicle created.') self.engine = CombustionEngine() # استخدم هذا المحرك افتراضيًا --snip-- class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') self.engine = ElectricEngine() يتطلب هذا إعادة كتابة كمية كبيرة من الشيفرة خصوصًا إذا كان لدينا عدة أصناف ترث من الصنف Vehicle الموجود مسبقًا. كل استدعاءات vehicleObj.changeSparkPlug() ستكون بحاجة لتصبح vehicleObj.engine.changeSparkPlug() لكل كائن في الصنف Vehicle أو أصنافها الفرعية لأن كل تغيير كبير سيحدث أخطاءً ربما تجعل من التابع changeSparkPlug() الخاص بالصنف LunarVehicle لا يفعل شيئًا. تتمثل الطريقة الخاصة ببايثون في هذه الحالة بضبط قيمة changeSparkPlug إلى None في صنف LunarVehicle: class LunarRover(Vehicle): changeSparkPlug = None def __init__(self): print('LunarRover created.') يتبع السطر: changeSparkPlug = None الصياغة المعرفة في "سمات الصنف" التي سنناقشها لاحقًا، وهذا يعيد تعريف التابع changeSparkPlug() الموروث من Vehicle، لذا يسبب استدعاؤه باستخدام كائن LunarRover خطأ: >>> myVehicle = LunarRover() LunarRover created. >>> myVehicle.changeSparkPlug() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not callable يؤدي هذا الخطأ إلى فشل البرنامج وتوقفه سريعًا، ويمكننا مباشرةً ملاحظة المشكلة عند استدعاء التابع غير المناسب باستخدام كائن LunarRover. يرث أيضًا كل صنف ابن للصنف LunarRover القيمة None للتابع changeSparkPlug(). تخبرنا رسالة الخطأ التالية بأن مبرمج الصنف LunarRover تعمّد ضبط قيمة التابع changSprakPlug() إلى None: TypeError: 'NoneType' object is not callable إذا لم يكن هناك بالأساس تابع، سنحصل على رسالة الخطأ التالية: NameError: name 'changeSparkPlug' is not defined تخلق الوراثة أصنافًا فيها تعقيدات وتناقضات لذا يُفضل استخدام التكوّن بدلًا عنها. الخلاصة الوراثة هي تقنية لإعادة استخدام الشيفرة، تسمح لك إنشاء أصناف ابن التي ترث توابع أصناف الأب، يمكنك إعادة تعريف التوابع لتقدم شيفرةً جديدةً لهم واستخدام super() لاستدعاء التابع الأصلي من الصنف الأب. لدى الأصناف الابن علاقة "is a" مع الصنف الأب الخاصة بها، لأن كائن من الصنف الابن هو كائن للصنف الأب. استخدام الأصناف والوراثة في بايثون اختياري، إذ يرى بعض المبرمجين أن التعقيد المرافق للاستخدام الكثير للوراثة لا يبرر فائدته. من المرونة أكثر استخدام التكوّن بدلًا من الوراثة لأنها تنفذ علاقة "has a" مع كائن من أحد الأصناف وكان من أصناف أخرى بدلًا من وراثة التوابع مباشرةً من هذه الأصناف، فمثلًا قد يحتوي كائن Customer على سمة birthday المسندة إلى كائن Date بدلًا من أن يكون هناك أصناف فرعية من صنف Customer للكائن Date. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق مقارنة ما بين برامج بايثون Python الاعتيادية وبرامج بايثون كائنية التوجه البرمجة كائنية التوجه (Object Oriented Programming) في بايثون