-
المساهمات
51 -
تاريخ الانضمام
-
تاريخ آخر زيارة
آخر الزوار
3263 زيارة للملف الشخصي
إنجازات Naser Dakhel

عضو نشيط (3/3)
17
السمعة بالموقع
-
يعد تحريك الشخصيات عنصرًا أساسيًا في تطوير الألعاب، ويوفر لنا محرك جودو أدوات مدمجة تساعدنا على إنشاء حركات واقعية وجذابة للشخصيات، سنركز في هذا المقال على شرح استخدام محرر التحريك لجعل شخصيات لعبتنا ثلاثية الأبعاد تطفو وتلتف، ونوضح كيفية استخدام التعليمات البرمجية للتحكم في تحريك الشخصيات وتفاعلها. لنبدأ بمقدمة حول استخدام محرر التحريك Animation Editor المدمج في محرك ألعاب جودو. استخدام محرر التحريك Animation Editor يُستخدم محرر التحريك في جودو لإنشاء حركات للشخصية حيث يتضمن المحرك العديد من أدوات التحريك التي تسهّل إنشاء حركات معقدة وديناميكية مثل حركة المشي أو الركض أو الدوران، ويمكننا بعدها ربط هذه الحركات بالتعليمات البرمجية لتشغيلها عندما تتفاعل الشخصيات مع بعضها أو عندما ينفّذ اللاعب إجراء معين كالضغط على زر ما والتحكم فيها في وقت التشغيل. إنشاء حركة اللاعب لنبدأ بإنشاء حركة اللاعب، سنفتح مشهد اللاعب ونحدد العقدة Player ثم نضيف لها العقدة AnimationPlayer هذه العقدة هي المسؤولة عن تشغيل الحركات، بعد إضافة العقدة ستظهر لوحة خاصة بالتحريك باسم Animation في اللوحة السفلية. كما تبين الصورة أعلاه، نجد في الأعلى شريط أدوات وقائمة منسدلة للتحريك تمكننا من الاختيار بين أنواع الحركات المختلفة، وفي المنتصف محرر مسار track editor dock وهو فارغ حاليًا وستُعرض فيه لاحقًا المسارات التي تتبعها العناصر المتحركة، وفي الأسفل خيارات للتصفية والالتقاط والتكبير و التصغير. لنبدأ بإنشاء حركة، ننقر على القائمة المنسدلة تحريك Animation في الأعلى، ثم نختار جديد New. ينبغي علينا تسمية التحريك الجديد باسم مناسب وليكن float أي طفو أو عوم. بمجرد إنشاء التحريك، سيظهر لنا مخطط زمني timeline يعرض أرقامًا تمثل الوقت بالثواني ويسمح لنا بمراقبة تغير خصائص الشخصية عبر الزمن أثناء تشغيل التحريك. نريد أن يبدأ تشغيل الحركة تلقائيًا في بداية اللعب، وأن تتكرر عملية التحريك بدون توقف، ويمكننا تحقيق بذلك عن طريق النقر فوق الزر الذي يحمل أيقونة A+ في شريط أدوات التحريك والذي يعني التشغيل التلقائي Autoplay، ثم تفعيل الأسهم التي تدل على تكرار الحركة كما في الصورة التالية. يمكننا أيضًا تثبيت محرر التحريك من خلال النقر على أيقونة الدبوس الظاهرة أعلى يمين الصورة، وهذا يمنعها من الطي عند النقر فوق إطار العرض وإلغاء تحديد العقد. سنضبط مدة الحركة إلى 1.2 ثانية في الجزء العلوي الأيمن من الشريط، وهذا هذا يعني أن الحركة التي أنشأناها ستستغرق مدة ثانية و 200 ميلي ثانية لإتمامها بالكامل من بدايتها إلى نهايتها. ينبغي أن نرى الآن الشريط الرمادي يتسع قليلاً، ليُظهر لنا بداية ونهاية الحركة التي صنعناها ونلاحظ الخط الأزرق العمودي الذي يمثل مؤشر الوقت. يمكننا النقر على شريط التمرير وسحبه في الجزء السفلي الأيسر لتكبير وتصغير المخطط الزمني. إنشاء حركة الطفو Float Animation سنعمل على إنشاء حركة الطفو التي تجعل الشخصيات تبدو وكأنها عائمة في الهواء بسلاسة دون تأثيرات مثل الجاذبية، حيث يمكننا تنفيذ الحركات من خلال تحريك معظم الخصائص وعلى أي عدد نريده من العقد باستخدام العقدة AnimationPlayer التي أضفناها سابقًا. فبعد إضافة هذه العقدة سنلاحظ وجود رمز مفتاح بجوار الخصائص المختلفة في الفاحص Inspector كالموضع ووالحجم، وغيرها. يمكننا النقر فوق أي منها لإنشاء إطار رئيسي Keyframe لهذه الخاصية والذي يحتوي على زوج من القيم الأولى تمثل الوقت والثانية تمثل قيمة الخاصية المقابلة في هذا الوقت، سيُدرج الإطار الرئيسي حيث يوجد مؤشر الوقت في المخطط الزمني، فإذا كان مؤشر الوقت عند اللحظة 1 في المخطط الزمني، سيضاف الإطار الرئيسي في تلك اللحظة. دعونا ندرج الإطار الأول، سنجري هنا تحريكًا لكل من الموضع Position والتدوير Rotation للعقدة character، لذا ينبغي علينا تحديد العقدة character ثم فتح قسم التحويل Transform في الفاحص Inspector، ثم النقر فوق رمز المفتاح بجوار خاصيتي الموضع والتدوير. عند النقر على رمز المفتاح يطلب منا تحديد نوع المسار الجديد للخاصية. بالنسبة لسلسلتنا، سنختار إنشاء مسارات RESET وهو الخيار الافتراضي حيث يظهر مساران في المحرر، ويمثل رمز المعين كل إطار رئيسي keyframe. يمكننا النقر على المعين وسحبه لتحريكه للوقت المناسب، سنحركه في مسار الموضع إلى 0.3 ثانية وفي مسار التدوير إلى 0.1 ثانية. كما سنحرك مؤشر الوقت إلى 0.5 ثانية عن طريق النقر والسحب على المخطط الزمني الرمادي. الآن نعود إلى الفاحص Inspector ونعيّن قيمة المحور Y لخاصية الموضع على 0.65 متر ، والمحور X لخاصية التدوير على 8 درجات. بهذا نكون قد أنشأنا إطار رئيسي لكل من خاصيتي الموضع و التدوير. لنحرّك الآن الإطار الرئيسي للموضع إلى0.7 ثانية عن طريق سحبه على المخطط الزمني. ملاحظة: ليس هدفنا في هذه السلسلة شرح مبادئ التحريك بالتفصيل، لكن يكفي أن نضع بعين الاعتبار عدم تحديد وقت ومسافة متساوية لكل شيء بالتساوي حيث يتلاعب مختصو التحريك عادة بالتوقيت والتباعد، وهما المبدآن الأساسيان للتحريك. لكوننا نريد الموازنة والتباين في حركة الشخصية ونجعلها أكثر حيوية. الآن لنحدد كيف ستنتهي الحركة ونحرك مؤشر الوقت إلى الموضع 1.2 ثانية. ثم نضبط المحور Y لخاصية الموضع على القيمة 0.35 والمحور X لخاصية التدوير على 9- درجة، والآن ننشئ مرة أخرى مفتاح لكل خاصية. يمكننا معاينة النتيجة بالنقر فوق زر التشغيل أو الضغط على Shift + D. و لإيقاف التشغيل ننقر فوق زر الإيقاف أو نضغط على S. سنلاحظ أن المحرك يجري عملية تداخل Interpolation بين الإطارات الرئيسية Keyframes التي أنشأناها لإنتاج حركة مستمرة. لكن تبدو الحركة في الوقت الحالي مصطنعة جدًا. وذلك لأن التداخل الافتراضي خطي، مما يسبب انتقالات بتسارع ثابت constant transitions، وهذا يخالف الكيفية التي تتحرك فيها الكائنات الحية في عالمنا الحقيقي. يمكننا التحكم في طريقة الانتقال بين الإطارات الرئيسية باستخدام ما يسمى بمنحنيات التخفيف easing curves. ننقر ونسحب بين المفتاحين الأوليين في المخطط الزمني لتحديدهما ضمن مربع. يمكننا بعدها تحرير خصائص كلا المفتاحين في وقت واحد في الفاحص Inspector، حيث يمكنك رؤية خاصية التخفيف Easing. انقر على المنحني الظاهر في الصورة واسحبه نحو اليسار. سيؤدي ذلك إلى جعل الحركة تتباطئ أو تخف تدريجيًا Ease-out، أي تنتقل سريعًا في البداية وتتباطأ عندما يصل مؤشر الوقت إلى الإطار الرئيسي التالي. عندما نشغّل الحركة مرة أخرى سنرى الفرق، يجب أن يبدو النصف الأول من الحركة الآن أكثر مرونة. بعدها نطبّق التخفيف Easing على الإطار الرئيسي الرئيسي الثاني في مسار التدوير. سنفعل العكس بالنسبة للإطار الرئيسي الثاني لخاصية الموضع ونسحبه إلى اليمين. الآن سوف يطفو اللاعب عند تشغيل اللعبة وستبدو الحركة كما يلي: ملاحظة: تؤثر الحركات التي أضفناها على خصائص العقد المتحركة في كل إطار، مما يعيد تعريف القيم الأولية التي حددناها. لذلك، إذا حركنا عقدة اللاعب مباشرة، فهذا سيمنعنا من تحريكها باستخدام التعليمات البرمجية. وهنا يأتي دور العقدة المحورية Pivot فعلى الرغم من أننا حركنا الشخصية، فلا يزال بإمكاننا تحريك المحور Pivot وتدويره لإضافة تغييرات إضافية على الحركة في السكربت البرمجي. يمكننا على سبيل المثال تحريك Pivot لأعلى في حال كان اللاعب الذي أنشأناه قريبًا جدًا من الأرض. التحكم بالحركة من خلال الشيفرة البرمجية يمكننا التحكم في تشغيل الحركة التي أنشأناها برمجيًا بناء على مدخلات اللاعب. لنجرب على سبيل المثال تغيير سرعة الحركة عندما تتحرك الشخصية. نفتح السكربت البرمجي للعقدة Player من خلال النقر على أيقونة السكربت المجاورة لها. نضيف الشيفرة التالية في الدالة _physics_process()، بعد السطر الذي نتحقق فيه من المتجه direction كما يلي: func _physics_process(delta): #... if direction != Vector3.ZERO: #... $AnimationPlayer.speed_scale = 4 else: $AnimationPlayer.speed_scale = 1 تعمل هذه الشيفرة على تسريع الحركة عن طريق ضرب سرعة التشغيل في 4، ثم إعادة ضبطها إلى الوضع الطبيعي بمجرد توقف اللاعب. وقد ذكرنا أن pivot يمكن أن يساعدنا في تطبيق حركة إضافية على الشخصية. ويمكننا استعماله هنا لجعل الشخصية تشكل قوس عندما تقفز بإضافة الكود التالي في نهاية الدالة _physics_process(). func _physics_process(delta): #... $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse تحريك الأعداء من النصائح المفيدة التي تساعدنا على تنفيذ التحريك في جودو هي نسخ واستخدام العقد بمشاهد مختلفة طالما أن هذه العقد تتماثل في البنية، فهذا يوفر علينا الوقت والجهد في إعادة إعداد المشاهد. على سبيل المثال، يحتوي كل من مشهدي اللاعب Player والعدو Mob على عقدة محورية pivot وعقدة الشخصية character، لذا في هذه الحالة يمكننا إعادة استخدام الحركة بينهما. لنفتح مشهد اللاعب player.tsc، ونحدد العقدة AnimationPlayer ونفتح التحريك float. بعد ذلك، ننقر فوق التحريك Animation ومن ثم نحتار نسخ Copy. ثم نفتح مشهد العدو mob.tscn، وننشئ عقدة فرعية من نوع AnimationPlayer ونحددها، ثم نضغط على تحريك Animation ثم إدارة التحريك Manage Animations ونختار إضافة مكتبة Add Library، سنرى رسالة مفادها "Global library will be created" أي ستنشأ مكتبة عامة نترك الحقل النصي فارغًا ونضغط زر موافق. بعدها نضغط على أيقونة لصق Paste وستظهر الحركة التي نسخناها في النافذة، نضغط زر موافق لإغلاق النافذة. بعد ذلك، علينا التأكد من تفعيل زر التشغيل التلقائي +A وأسهم تكرار الحركة في محرر التحريك في اللوحة السفلية. هذا كل شيء علينا فعله حيث ستتحرك جميع شخصيات الأعداء الآن بحركة العوم float أيضًا. يمكننا تغيير سرعة التشغيل بناءً على السرعة العشوائية random_speed للعدو. لذا سنفتح سكربت العقدة Mob ونضيف في نهاية دالة initialize() السطر التالي: func initialize(start_position, player_position): #... $AnimationPlayer.speed_scale = random_speed / min_speed بإتمام هذه الخطوات، نكون قد أكملنا برمجة أول لعبة ثلاثية الأبعاد بشكل كامل باستخدام محرك جودو، مما يفتح أمامنا الكثير من الإمكانيات لتطوير ألعاب احترافية أكثر إبداعًا. دورة تطوير الألعاب ابدأ رحلة صناعة الألعاب ثنائية وثلاثية الأبعاد وابتكر ألعاب ممتعة تفاعلية ومليئة بالتحديات. اشترك الآن الكود الكامل للعبة ثلاثية الأبعاد بمحرك جودو فيما يلي محتوى ملف السكربت Player.gd الخاص بعقدة اللاعب Player extends CharacterBody3D signal hit # سرعة حركة اللاعب مقدرة بالمتر في الثانية @export var speed = 14 # التسارع نحو الأسفل في الهواء مقدرة بالمتر في الثانية مربع @export var fall_acceleration = 75 # الدفعة العامودية المطبقة على الشخصية عند القفز مقدرة بالمتر في الثانية @export var jump_impulse = 20 # الدفعة العامودية المطبقة على الشخصية عند القفز على عدو مقدرة بالمتر في الثانية @export var bounce_impulse = 16 var target_velocity = Vector3.ZERO func _physics_process(delta): # أنشأنا متغير محلي لتخزين دخل الاتجاه var direction = Vector3.ZERO # نتحقق من كل دخل حركة ونحدث الاتجاه حسبه if Input.is_action_pressed("move_right"): direction.x = direction.x + 1 if Input.is_action_pressed("move_left"): direction.x = direction.x - 1 if Input.is_action_pressed("move_back"): # لاحظ أننا نعمل المحورين x و z الخاصين بالشعاع # المستوي XZ هو مستوي الأرض في ثلاثي الأبعاد direction.z = direction.z + 1 if Input.is_action_pressed("move_forward"): direction.z = direction.z - 1 # يمنع الحركة القطرية السريعة if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.look_at(position + direction,Vector3.UP) $AnimationPlayer.speed_scale = 4 else: $AnimationPlayer.speed_scale = 1 # السرعة الأرضية 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) # القفز if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse # كرر خلال كل الاصطدامات التي تحصل في الإطار # يكون ذلك في C كالتالي # (int i = 0; i < collisions.Count; i++) for index in range(get_slide_collision_count()): # نحصل على واحد من التصادمات مع اللاعب var collision = get_slide_collision(index) # إذا كان التصادم مع الأرض if collision.get_collider() == null: continue # إذا كان التصادم مع العدو if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # نتحقق أننا نصدمه من الأعلى if Vector3.UP.dot(collision.get_normal()) > 0.1: # إذا كان كذلك نسحقه mob.squash() target_velocity.y = bounce_impulse # يمنع أي استدعاءات مكررة. break # تحريك الشخصية velocity = target_velocity move_and_slide() $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse # أضف ذلك في الأسفل. func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die() وفيما يلي ملف السكربت mob.gd الخاص بعقدة العدو Mob extends CharacterBody3D # السرعة الدنيا للعدو مقدرة بالمتر في الثانية @export var min_speed = 10 # السرعة القصوى للعدو مقدرة بالمتر في الثانية @export var max_speed = 18 # تنبثق عندما يقفز اللاعب على عدو signal squashed func _physics_process(_delta): move_and_slide() # يتم استدعاء هذه الدالة من المشهد الأساسي func initialize(start_position, player_position): # نحدد مكان العدو عن طريق وضعه في start_position # وندوره نحو player_position لينظر إلى اللاعب look_at_from_position(start_position, player_position, Vector3.UP) #تدوير العدو عشوائيًا ضمن مجال -90 و +90 درجة # لكي لا تتحرك مباشرة نحو اللاعب rotate_y(randf_range(-PI / 4, PI / 4)) # نحسب سرعة عشوائية (عدد صحيح) var random_speed = randi_range(min_speed, max_speed) # نحسب سرعة امامية تمثل السرعة velocity = Vector3.FORWARD * random_speed # ثم ندور شعاع السرعة اعتمادًا على دوران العدو حول Y # للحركة في الاتجاه الذي ينظر إليه العدو velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() func squash(): squashed.emit() queue_free() # دمّر هذه العقدة الخلاصة بهذا نكون وصولنا لنهاية السلسلة التي شرحنا فيها تطوير لعبة ثلاثية الأبعاد في جودو، وتعلّمنا في هذا الدرس كيفية تحريك الشخصية بواسطة محرر التحريك المضمن الموجود في جودو، مما سيضفي طابعًا من الحيوية إلى لعبتك. لا تدع هذه اللعبة تكون آخر لعبة تعمل عليها، فهذا طريق البداية فحسب! ترجمة - وبتصرف - لقسم Character animation من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: إضافة النقاط واللعب مجددًا وتأثيرات الصوت للعبة 3D ضمن جودو قتل اللاعب عند الاصطدام بالعدو ضمن لعبة 3D بجودو العقد Nodes والمشاهد Scenes في جودو Godot أشهر أنواع الألعاب الإلكترونية
-
في هذا الدرس من سلسلة تطوير لعبة ثلاثية الأبعاد في جودو، سنضيف للعبتنا نظام النقاط وتشغيل المؤثرات الصوتية وإمكانية اللعب مجددًا. سنتعلم كيفية تتبّع النتيجة الحالية بواسطة متغير وعرضها على الشاشة باستخدام واجهة مستخدم بسيطة عن طريق كتابة نصية. إضافة عقدة واجهة المستخدم سنضيف عقدة فرعية جديدة في المشهد الرئيسي من نوع Control إلى العقدة الرئيسية Main ونطلق عليها اسم UserInterface وهذا سينقلنا تلقائيًا إلى واجهة الفضاء ثنائي الأبعاد، حيث يمكننا تعديل واجهة المستخدم UI. نضيف عقدة Label ونسميها ScoreLabel ثم نضبط الخاصية Text ضمن قائمة الفاحص Inspector للتسمية النصية بقيمة افتراضية مثل Score: 0 يكون لون النص أبيضًا بشكل افتراضي مثل خلفية لعبتنا، لذا نحتاج إلى تغيير لونه لرؤيته أثناء تشغيل اللعبة. ننتقل للأسفل إلى Theme Overrides ونفتح الألوان ونفعّل لون الخط Font Color لتلوين النص بالأسود لأنه يظهر بشكل جيد مع المشهد ثلاثي الأبعاد الأبيض. أخيرًا، ننقر ونسحب النص في نافذة العرض لتحريكه بعيدًا عن الزاوية العلوية اليسرى. تتيح لنا عقدة UserInterface تجميع عناصر واجهة المستخدم الخاصة بنا في فرع من شجرة المشهد واستخدام مورد مظهر theme resource يتاح توارثه من قبل كل عناصرها الفرعية والذي سنستخدمه لتعيين خط لعبتنا. إنشاء مظهر واجهة المستخدم نحدد عقدة UserInterface مرة أخرى، وننشئ في لوحة الفاحص Inspector مورد مظهر theme resource جديد عبر الذهاب إلى Theme ومن ثم الحقل Theme كالتالي: ننقر فوقه لفتح محرر المظهر في اللوحة السفلية لنستطيع معاينة كيفية ظهور جميع أدوات واجهة المستخدم المضمنة مع مورد المظهر الخاص بنا. لا يحتوي المظهر إلا على خاصية واحدة بشكل افتراضي، وهي الخط الافتراضي Default Font، ويمكننا أيضًا إضافة المزيد من الخصائص إلى مورد المظهر لتصميم واجهات مستخدم معقدة، ولكن هذا خارج نطاق مقالنا الحالي. يتوقّع حقل ملف الخط ملفات كملفات خطوط الكتابة الموجودة على حاسوبنا، فهناك صيغتان شائعتان لملفات الخطوط هما TTF و OTF من قائمة نظام الملفات FileSystem، نوسّع دليل الخطوط fonts وننقر ونسحب ملف Montserrat-Medium.ttf الذي أضفناه إلى المشروع على حقل الخط الافتراضي Default Font حتى يظهر النص مرة أخرى في معاينة المظهر. سنلاحظ أن النص صغير قليلاً، لذا نضبط حجم الخط الافتراضي Default Font Size على قيمة 22 بكسل لزيادة حجم النص. زيادة قيمة النتيجة للعمل على نظام النقاط، نرفق سكريبت جديد بعقدة ScoreLabel ونعرّف فيه متغير النتيجة score extends Label var score = 0 يجب أن تزداد قيمة النتيجة بمقدار 1 في كل مرة ندمّر عدو، ويمكننا الاستفادة من إشارة squashed الخاصة بالأعداء لمعرفة متى يحدث ذلك، ولكن بما أننا نستنسخ الأعداء من خلال الشيفرة البرمجية، فلا يمكننا توصيل إشارة العدو ScroeLabelعبر المحرر، ويتعين علينا بدلاً من ذلك إنشاء الاتصال من الشيفرة نفسها في كل مرة نولّد فيها عدو. نفتح السكربت الرئيسي main.gd، ويمكن النقر فوق اسمه في العمود الأيسر لمحرر النصوص البرمجية إذا كان لا يزال مفتوحًا. أو بدلاً من ذلك يمكن النقر نقرًا مزدوجًا فوق ملف main.gd في نافذة نظام الملفات FileSystem، وإضافة السطر التالي أسفل دالة onmobtimertimeout(): func _on_mob_timer_timeout(): #... # نربط العدو مع قائمة النتيجة لتحديث النتيجة عند تدمير عدو mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind()) يعني هذا السطر أنه عندما يصدر العدو إشارة squashed، ستستقبل عقدة ScoreLabel الإشارة وتستدعي الدالة _on_mob_squashed ()، ننتقل إلى السكربت ScoreLabel.gd لتعريف دالة رد النداء callback function المسماة _on_mob_squashed()، ونزيد هناك قيمة النتيجة ونحدث النص المعروض. func _on_mob_squashed(): score += 1 text = "Score: %s" % score يستخدم السطر الثاني قيمة متغير النتيجة score لاستبدال الموضع المؤقت %s، ويحوّل محرك جودو القيم تلقائيًا إلى نص عند استخدام هذه الميزة، وهو أمر مفيد حين إخراج النص في التسميات النصية أو حين استخدام دالة print(). لمزيد من المعلومات حول التعامل مع النصوص يمكن مراجعة تنسيقات النصوص في GDScript في توثيق جودو الرسمي. يمكننا الآن تشغيل اللعبة وتدمير بعض الأعداء لمشاهدة زيادة قيمة النتيجة. ملاحظة: من المحبذ فصل واجهة المستخدم تمامًا عن عالم اللعبة في الألعاب المعقدة، وفي هذه الحالة لن تتابع النتيجة على التسمية النصية بل قد ترغب في تخزينها في كائن منفصل مخصص لذلك الغرض ولكن عند إنشاء نموذج أولي أو عندما يكون مشروعك بسيطًا، فلا بأس بالحفاظ على الشيفرة البرمجية بسيطة لأن البرمجة عملية تسىعى للموازنة دائمًا. إعادة اللعب الآن سنضيف القدرة على إعادة اللعب بعد موت اللاعب، فعندما يموت اللاعب، سنعرض رسالة على الشاشة وننتظر إدخالًا منه. لتعد إلى المشهد الرئيسي main.tscn ونحدد عقدة UserInterface ثم نضيف عقدة فرعية جديدة من نوع ColorRect ونسميها Retry تُملأ هذه العقدة مستطيل بلون موحد وتُستخدم كطبقة تظليل لإعتام الشاشة. لاستخدامها على كامل نافذة العرض viewport، نستخدم قائمة Anchor Preset في شريط الأدوات. نفتحها ونطبق أمر مستطيل كامل Full Rect لم يحدث شيء لكن تتحرك المقابض الخضراء الأربعة فقط إلى زوايا مربع التحديد. هذا لأن عقد واجهة المستخدم التي تحتوي على أيقونة خضراء تعمل مع نقاط الربط والهوامش بالنسبة إلى مربع إحاطة العنصر الأب، كما أن عقدة UserInterface هنا لها حجم صغير وعقدة Retry محدودة بها. نحدد UserInterface ونطبّق الأمر مستطيل كامل Full Rect عليها أيضًا من Anchor Preset، ويجب أن تغطي عقدة Retry الآن نافذة العرض بأكملها. دعنا نغيّر لونها لتعتيم منطقة اللعبة، سنحدد Retry وفي لوحة الفاحص Inspector، نضبط لون Color إلى لون غامق وشفاف في نفس الوقت. للقيام بذلك، نسحب مؤشر A إلى اليسار في مُحدّد اختيار اللون. يتحكم هذا المؤشر بقناة ألفا Alpha للون، أي معامل العتامة أو الشفافية. بعدها نضيف عقدة Label كعقدة ابن لعقدة Retry ونكتب فيها نص مفاده اضغط على مفتاح Enter لإعادة المحاولة ومن ثم نطبق الأمر Center من Anchor Preset لنقلها وتثبيتها في مركز الشاشة. برمجة خيار إعادة المحاولة يمكننا الآن الانتقال إلى الشيفرة لإظهار وإخفاء عقدة Retry عند موت اللاعب وإعادة اللعب. نفتح السكربت الرئيسي main.gd. أولاً، نريد إخفاء التراكب في بداية اللعبة. لذا نضيف هذا السطر إلى الدالة _ready(): func _ready(): $UserInterface/Retry.hide() بعدها عندما يتعرض اللاعب للاصطدام، نُظهر عنصر واجهة المستخدم Retry كما يلي: func _on_player_hit(): #... $UserInterface/Retry.show() أخيرًا، عندما تكون عقدة Retry مرئية، نحتاج إلى الاستماع إلى دخل اللاعب من أجل إعادة تشغيل اللعبة إذا ضغط على زر Enter. للقيام بذلك، نستخدم دالة رد النداء _unhandled_input()، والتي يجري تشغيلها عند أي إدخال. إذا ضغط اللاعب على زر الإدخال المحدد مسبقًا ui_accept وكانت عقدة Retry مرئية، فإننا نعيد تحميل المشهد الحالي. func _unhandled_input(event): if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible: # هذا يعيد تشغيل المشهد الحالي get_tree().reload_current_scene() تمنحنا دالة get_tree() الوصول إلى كائن SceneTree العام الذي يسمح لنا بإعادة تحميل وتشغيل المشهد الحالي. إضافة المؤثرات الصوتية سنستخدم الآن ميزة أخرى في جودو لإضافة مؤثرات صوتية تعمل بشكل مستمر في الخلفية ألا وهي التحميل التلقائي autoloads. لتشغيل الصوت، كل ما علينا فعله هو إضافة عقدة AudioStreamPlayer إلى المشهد الخاص بنا وإرفاق ملف صوت بها. عند بدء تشغيل المشهد، يمكن أن يعمل الصوت تلقائيًا. ومع ذلك، عند إعادة تحميل المشهد، كما نفعل لإعادة التشغيل نعيد تعيين عقد الصوت، وتبدأ المؤثرات صوتية من البداية. يمكننا الاستفادة من ميزة التحميل التلقائي Autoload في جودو لتمكين المحرك من تحميل عقدة أو مشهد معين تلقائيًا عند بدء اللعبة، حتى لو كانت خارج المشهد الحالي. هذه الميزة مفيدة أيضًا لإنشاء كائنات عامة يمكن الوصول إليها بسهولة من أي مكان في المشروع، مما يسهم في تحسين تنظيم الكود وإدارة الموارد المشتركة بين المشاهد المختلفة. ننشئ مشهدًا جديدًا بالانتقال إلى قائمة مشهد Scene والنقر على مشهد جديد New Scene أو باستخدام الرمز + بجوار المشهد المفتوح حاليًا. ننقر فوق الزر عقدة أخرى Other Node لإنشاء AudioStreamPlayer ونعيد تسميتها إلى MusicPlayer. أضفنا مقطع صوتي إلى المسار art/ وهو House In a Forest Loop.ogg. لسحب هذا المقطع إلى اللعبة، نضغط عليه ثم نسحبه لخاصية Stream في لوحة الفاحص Inspector. بعد ذلك، نفعّل خيار التشغيل التلقائي Autoplay لتشغيل المؤثرات الصوتية تلقائيًا عند بدء اللعبة. نحفظ المشهد باسم MusicPlayer.tscn، بعدها علينا تسجيله كمشهد تحميل تلقائي، ولفعل ذلك نتوجه إلى قائمة مشروع Project ومن ثم إعدادات المشروع Project Settings وتنقر على تبويبة التحميل التلقائي Autoload. نحتاج إلى إدخال المسار إلى المشهد في حقل المسار Path، ولفعل ذلك ننقر فوق أيقونة المجلد لفتح مستعرض الملفات وننقر نقرًا مزدوجًا فوق MusicPlayer.tscn، ثم ننقر فوق الزر إضافة على اليمين لتسجيل العقدة. يجري الآن تحميل MusicPlayer.tscn في أي مشهد نفتحه أو نشغله. لذا، إذا شغلنا اللعبة الآن، تعمل المؤثرات الصوتية تلقائيًا في أي مشهد. قبل أن ننهي هذا الدرس، لنلقي نظرة سريعة على كيفية عمل الميزة، عند تشغيل اللعبة، تتغير نافذة Scene وتمنحنا تبويبتين هما عن بعد Remote ومحلي Local. يتيح لنا تبويب Remote تصوّر شجرة عقد اللعبة التي نعمل عليها. سنرى هناك العقدة الرئيسية Main وكل ما يشتمل عليه المشهد والأعداء التي استنسخناها في الأسفل. في الأعلى توجد العقدةMusicPlayer التي جرى تحميلها تلقائيًا، والعقدة الجذر root هي نافذة عرض لعبتنا. شيفرة المشهد الرئيسي فيما يلي سكربت main.gd الكامل بلغة GDScript للرجوع إليه extends Node @export var mob_scene: PackedScene func _ready(): $UserInterface/Retry.hide() func _on_mob_timer_timeout(): # إنشاء نسخة جديدة من مشهد العدو var mob = mob_scene.instantiate() # اختيار مكان عشوائي على SpawnPath # نخزن المرجع في عقدة SpawnLocation var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # ونعطيه انزياح عشوائي mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # توليد العدو عن طريق إضافته للمشهد الرئيسي add_child(mob) # نربط العدو مع قائمة النتيجة لتحديث النتيجة عند تدمير عدو mob.squashed.connect($UserInterface/ScoreLabel._on_mob_squashed.bind()) func _on_player_hit(): $MobTimer.stop() $UserInterface/Retry.show() func _unhandled_input(event): if event.is_action_pressed("ui_accept") and $UserInterface/Retry.visible: # هذا يعيد تشغيل المشهد get_tree().reload_current_scene() الخلاصة استطعنا في هذا المقال جعل لعبتنا ثلاثية الأبعاد في جودو شيّقة وممتعة أكثر عن طريق إضافة ميزة إحراز النقاط، كما أدخلنا تحسينات على عملية اللعب بالسماح للاعب باللعب من جديد بشكل سريع في حال خسارته. سنعمل في الجزء التالي على إضافة المزيد من الميزات لجعل تجربة اللعب تفاعلية بشكل أكبر. ترجمة - وبتصرف - لقسم Score and replay من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: قتل اللاعب عند الاصطدام بالعدو ضمن لعبة 3D بجودو إعداد مشهد اللاعب وعمليات الإدخال في لعبة ثلاثية الأبعاد باستخدام جودو القفز وتدمير الأعداء ضمن لعبة ثلاثية الأبعاد في جودو توليد الأعداء في لعبة ثلاثية الأبعاد في محرك الألعاب جودو
-
أضفنا في المقال السابق خاصية القفز والهبوط فوق الأعداء وتدميرهم ضمن لعبة ثلاثية الأبعاد، لكننا لم نجعل اللاعب يموت بعد في حال اصطدم بعدو ما موجود على الأرض، لذا لنعمل على تحقيق هذا الأمر، ونشرح في فقراتنا التالية كيفية تحقيقه خطوة بخطوة عن طريق استخدام هياكل التصادم Hitboxes وعقد Area في محرك جودو. تحديد ما إذا كان اللاعب على الأرض ما نحتاج لاكتشافه في البداية هو اصطدامنا مع العدو الذي يمشي على الأرض، إذ نريد أن يموت اللاعب عندما يرتطم بالعدو وهو على الأرض فقط، لا إذا كان في الهواء، ولتحقيق لدينا طريقتان الأولى هي باستخدام رياضيات الأشعة Raycasting لتحديد ما إذا كان اللاعب يلامس الأرض أو في الهواء، إذا كنت تريد أن يموت اللاعب فقط عندما يكون على الأرض، يمكنك استخدام شعاع يتجه من اللاعب نحو الأسفل مثل شعاع Raycast2D للتحقق من وجود سطح تحته. أو يمكننا بدلاً من ذلك، استخدام عقدة Area3D والتي تعمل بشكل جيد مع هياكل التصادم Hitboxes في محرك جودو. حيث يمكن تعريف منطقة معينة حول اللاعب. فإذا دخل العدو في هذه المنطقة أثناء وجود اللاعب على الأرض، سنفعّل حدث الاصطدام وننفذ منطق موت اللاعب وهو الأسلوب الذي سنعتمده. هيكل تصادم Hitbox مع عقدة Area لنعد إلى مشهد player.tscn ونضف عقدة فرعية جديدة من النوع Area3D، سنطلق عليها اسم MobDetector أي كاشف الأعداء، ومن ثم سنضيف العقدة CollisionShape3D كعقدة فرعية لها. بعدها سنعين شكله ليكون أسطوانيًا وذلك باختيار NewClindershape في قائمة الفاحص Inspector. ولجعل التصادمات تحدث فقط عندما يكون اللاعب على الأرض أو قريبًا منها يمكننا تقليل ارتفاع الإسطوانة ونقلها إلى أعلى الشخصية، وبهذه الطريقة، عندما يقفز اللاعب سيكون الشكل مرتفعًا جدًا بحيث لا يمكن للأعداء الاصطدام به. نريد أيضًا أن تكون الأسطوانة أوسع من الكرة، فبهذه الطريقة، يُضرب اللاعب قبل الاصطدام ويجري دفعه فوق صندوق الاصطدام الخاص بالعدو، فكلما كانت الأسطوانة أوسع، كلما كان من الأسهل قتل اللاعب. بعد ذلك سنحدد عقدة MobDetector مرة أخرى، ونعطّل خاصية قابلية المراقبة Monitorable في الفاحص Inspector، وهذا سيجعل من العقد الفيزيائية الأخرى غير قادرة على اكتشاف المنطقة، إذ تسمح خاصية قابلية المراقبة Monitorable باكتشاف التصادمات، بعدها سنريل طبقة الاصطدام من الطبقة Layer في قسم التصادم Collision ونضبط القناع على طبقة الأعداء. عندما تكتشف المناطق تصادمًا ستصدر إشارة، وكي نوصّل هذه الإشارة إلى عقدة اللاعب Player نحدد العقدة MobDetector وننتقل إلى التبويب Node في نافذة الفاحص Inspector، وننقر مرتين فوق إشارة body_entered التي جرى وصلها بعقدة Player. تطلق العقدة MobDetctor إشارة body_entered عندما تدخلها عقدة CharacterBody3D أو RigidBody3D، لكن نظرًا لأن العقدة MobDetctor مهيأة لتعمل فقط مع الطبقات الفيزيائية الخاصة بالأعداء، فإنها ستكتشف فقط الأجسام التي تنتمي إلى هذه الطبقات، أي ستكتشف فقط عقد العدو. من الناحية البرمجية، يتوجب علينا القيام بأمرين الأول إصدار إشارة سنستخدمها لاحقًا لإنهاء اللعبة والثاني تدمير اللاعب، ويمكننا تغليف هذه العمليات في دالة die() التي تساعدنا بوضع دلالة مفيدة إلى شيفرتنا كالتالي: # تنشر عندما يصطدم اللاعب بعدو # ضع هذه في أعلى السكريبت signal hit # وأضف هذه الدالة في الأسفل func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die() جرب اللعبة مرة أخرى بالضغط على F5. إذا أعددنا كل شيء بشكل صحيح، فيجب أن تموت الشخصية عندما يصطدم العدو بهيكل التصادم، لاحظ أن السطر التالي سيعطي خطأ في حال عدم وجود عقدة لاعب Player ففي هذه الحالة لن يكون هناك مرجع إلى $Player! var player_position = $Player.position لاحظ أيضًا أن اصطدام العدو باللاعب وموته يعتمد على حجم وموضع اللاعبPlayer وأشكال تصادم العدو mob، قد تحتاج إلى نقلها وتغيير حجمها للحصول على تجربة واقعية في اللعبة. إنهاء اللعبة يمكننا استخدام إشارة ضرب hit اللاعب player لإنهاء اللعبة، فكل ما يتعين علينا فعله هو توصيله بالعقدة الرئيسية Main وإيقاف MobTimer كرد فعل. لنفتح المشهد الرئيسي للعبة main.tscn، ونحدد عقدة اللاعب Player، وفي نافذة العقدة Node نوصّل إشارة hit الخاصة بها بالعقدة الرئيسية Main. ثم نحصل على المؤقت ونوقفه في الدالة _on_player_hit(). func _on_player_hit(): $MobTimer.stop() إذا جربنا اللعبة الآن، فستتوقف الأعداء عن الظهور عندما يموت اللاعب، وستغادر الأعداء المتبقية الشاشة. يمكننا الآن أن نكافئ نفسنا لقد أنشأنا نموذج أولي للعبة ثلاثية الأبعاد كاملة، حتى لو كانت لا تزال بسيطة بعض الشيء. ومن هناك سنضيف نتيجة وخيارًا لإعادة بدء اللعبة، ونحاول جعل اللعبة تبدو أكثر حيوية من خلال الرسوم المتحركة البسيطة. الشيفرة النهائية للعبة ثلاثية الأبعاد في جودو فيما يلي الشيفرات الكاملة للعقد الرئيسية Main وMob وPlayer للرجوع إليها. شيفرة المشهد الرئيسي لنبدأ بسكربت main.gd الذي يعمل على إنشاء الأعداء بشكل عشوائي في مواقع مختلفة على المسار المحدد عند مرور الوقت بناءً على مؤقت العدو، ويحدد هدف العدو وهو اللاعب كما أنه يوقف ظهور الأعداء عندما يتعرض اللاعب للإصابة. extends Node @export var mob_scene: PackedScene func _on_mob_timer_timeout(): # إنشاء نسخة جديدة من مشهد العدو var mob = mob_scene.instantiate() # اختيار مكان عشوائي على SpawnPath # نخزن المرجع على عقدة SpawnLocation var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # ونعطيه انزياح عشوائي mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # خلق العدو عن طريق إضافته إلى المشهد الرئيسي add_child(mob) func _on_player_hit(): $MobTimer.stop() شيفرة العدو السكربت التالي هو سكربت العدو Mob.gd الذي يتعامل مع حركة العدو في اللعبة، حيث يوجه العدو عشوائيًا نحو اللاعب، ويحدد سرعته بشكل عشوائي، ويتأكد من تدمير العدو عند خروجه من الشاشة أو عندما يقفز اللاعب عليه. extends CharacterBody3D # السرعة الدنيا للعدو مقدرة بالمتر في الثانية @export var min_speed = 10 # السرعة القصوى للعدو مقدرة بالمتر في الثانية @export var max_speed = 18 # تنبثق عندما يقفز اللاعب على عدو signal squashed func _physics_process(_delta): move_and_slide() # يتم استدعاء هذه الدالة من المشهد الأساسي func initialize(start_position, player_position): # نحدد مكان العدو عن طريق وضعه في start_position # وندوره نحو player_position لينظر إلى اللاعب look_at_from_position(start_position, player_position, Vector3.UP) #تدوير العدو عشوائيًا ضمن مجال -90 و +90 درجة # لكي لا تتحرك مباشرة نحو اللاعب rotate_y(randf_range(-PI / 4, PI / 4)) # نحسب سرعة عشوائية (عدد صحيح) var random_speed = randi_range(min_speed, max_speed) # نحسب سرعة امامية تمثل السرعة velocity = Vector3.FORWARD * random_speed # ثم ندور شعاع السرعة اعتمادًا على دوران العدو حول Y # للحركة في الاتجاه الذي ينظر إليه العدو velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() func squash(): squashed.emit() queue_free() # تدمير العقدة شيفرة اللاعب وأخيرًا، نوضح سكربت Player.gd الذي يتحكم في حركة اللاعب كالقفز والتحرك على الأرض والسقوط في الهواء، فعندما يصطدم اللاعب بالعدو من الأعلى، سيدمره باستخدام الدالة squash() ويعطى اللاعب دفعة قفز عمودية، وعند اصطدامه مع العدو يموت باستخدام الدالة die(). extends CharacterBody3D signal hit # سرعة حركة اللاعب مقدرة بالمتر في الثانية @export var speed = 14 # التسارع نحو الأسفل في الهواء مقدرة بالمتر في الثانية مربع @export var fall_acceleration = 75 # الدفعة العامودية المطبقة على الشخصية عند القفز مقدرة بالمتر في الثانية @export var jump_impulse = 20 # الدفعة العامودية المطبقة على الشخصية عند القفز على عدو مقدرة بالمتر في الثانية @export var bounce_impulse = 16 var target_velocity = Vector3.ZERO func _physics_process(delta): # أنشأنا متغير محلي لتخزين دخل الاتجاه var direction = Vector3.ZERO # نتحقق من كل دخل حركة ونحدث الاتجاه حسبه if Input.is_action_pressed("move_right"): direction.x = direction.x + 1 if Input.is_action_pressed("move_left"): direction.x = direction.x - 1 if Input.is_action_pressed("move_back"): # لاحظ أننا نعمل المحورين x و z الخاصين بالشعاع # المستوي XZ هو مستوي الأرض في ثلاثي الأبعاد direction.z = direction.z + 1 if Input.is_action_pressed("move_forward"): direction.z = direction.z - 1 # منع الحركة القطرية السريعة if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.look_at(position + direction, Vector3.UP) # السرعة الأرضية 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) # القفز if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse # كرر خلال كل الاصطدامات التي تحصل في الإطار # يكون ذلك بلغة C كالتالي # (int i = 0; i < collisions.Count; i++) for index in range(get_slide_collision_count()): # نحصل على واحد من التصادمات مع اللاعب var collision = get_slide_collision(index) # إذا كان التصادم مع الأرض if collision.get_collider() == null: continue # إذا كان التصادم مع العدو if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # نتحقق أننا نصدمه من الأعلى if Vector3.UP.dot(collision.get_normal()) > 0.1: # إذا كان كذلك نسحقه mob.squash() target_velocity.y = bounce_impulse # يمنع أي استدعاءات مكررة break # تحريك الشخصية velocity = target_velocity move_and_slide() # أضف ذلك في الأسفل func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die() الخاتمة بهذا نكون شرحنا بالتفصيل كيفية منح الأعداء في لعبتنا القدرة على تدمير اللاعب، وأصبح منطق لعبتنا متوازنًا، لكن اللعبة تبدو حتى الآن مملة نوعًا ما بسبب عدم وجود هدف أو نتيجة نهائية تقيم أدائنا في اللعب، لذا سنبدأ في الدرس التالي على تحقيق ذلك ونضيف النتيجة وخيار إعادة المحاولة في حال خسرنا اللعبة وأردنا إعادة تشغيلها سريعًا! ترجمة - وبتصرف - لقسم Killing the player من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: القفز وتدمير الأعداء ضمن لعبة ثلاثية الأبعاد في جودو تصميم مشهد الأعداء للعبة ثلاثية الأبعاد في جودو العقد Nodes والمشاهد Scenes في جودو Godot الاستماع لمدخلات اللاعب في جودو Godot
-
نركز في هذا المقال على شرح طريقة تحسين التفاعل بين اللاعب والأعداء في لعبتنا ثلاثية الأبعاد التي بدأنا تطويرها في مقال سابق، ونشرح كيف نجعل اللاعب قادرًا على تدمير العدو إذا هبط عليه من الأعلى. بالمقابل، سنجعل اللاعب يموت إذا اصطدم العدو به أثناء وجوده على الأرض. التحكم في التفاعلات الفيزيائية سنتحكم في التفاعلات الفيزيائية ونضبطها لتحديد كيفية تفاعل الكائنات المختلفة مع بعضها البعض ضمن اللعبة، لتحقيق ذلك يجب أن تكون على دراية بمفهوم طبقات الفيزياء في جودو. حيث تتمتع الأجسام الفيزيائية بميزات تسهّل التحكم في التفاعل بينها، وهي الطبقات Layers والأقنعة Masks. تُحدد الطبقات Layers الطبقة الفيزيائية التي ينتمي إليها الكائن، وتستخدم لتنظيم الكائنات الفيزيائية في اللعبة تُحدد الأقنعة Masks ما هي الطبقات التي يمكن للجسم رصدها أو اكتشافها والتفاعل معها يؤثر هذا التفاعل بين الطبقات والأقنعة بشكل مباشر على كشف الاصطدام Collision Detection فعندما نرغب أن يتفاعل جسمان معًا، يجب أن يتوافق قناع أحدهما مع طبقة الآخر على الأقل. فإذا كنا نريد أن يتفاعل جسمان مثل اللاعب والعدو، يجب التأكد من إعداد الطبقات والأقنعة بحيث تكون طبقة أحدهما مدرجة ضمن قناع الآخر، والعكس، وإذا كنا نريد تجنب التفاعل بينهما، نجعل القناع الخاص بأحدهما لا يراقب الطبقة التي ينتمي إليها الكائن الآخر. ستتضح الفكرة لنا أكثر عند التطبيق العملي. يكفي أن نعرف حاليًا أن الطبقات والأقنعة تساعدنا على التحكم بالتفاعلات الفيزيائية بين الأجسام بدقة، وتمكننا من تصفية التفاعلات غير الضرورية وتحقيق التفاعل للأجسام التي تحتاج للتفاعل فقط، وهي تحسن أداء اللعبة عن طريق تقليل العمليات الحسابية اللازمة لكشف الاصطدامات وتقلل حجم الشيفرة البرمجية وتزيل الحاجة لكتابة شروط إضافية فيها. عند إنشاء الأجسام والمناطق في محرك الألعاب ستُعيّن افتراضيًا إلى الطبقة والقناع رقم 1. هذا يعني أن جميع الأجسام التي عينت بهذا الرقم ستتفاعل وتتصادم مع بعضها تلقائيًا دون الحاجة إلى ضبط إعدادات إضافية. تحديد أسماء الطبقات دعونا الآن نعطي طبقات الفيزياء أسماء مميزة لتسهيل إدارة التفاعلات داخل لعبتنا، للقيام بذلك سنفتح محرك ألعاب جودو وننتقل إلى القائمة العلوية مشروع Project ومن ثم نختار إعدادات المشروع Project Settings. ننتقل للقائمة اليسرى ونذهب للأسفل حتى نصل لقسم أسماء الطبقات، ثم نختار فيزياء 3D أو 3D Physics. ستظهر لنا قائمة بالطبقات وحقل نصي على يمين كل طبقة. يمكن من خلال هذا الحقل النصي تغيير اسم الطبقة، سنسمي الطبقات الثلاث الأولى بأسماء مناسبة وهي player لتمثيل طبقة اللاعب، و enemies لتمثيل طبقة الأعداء، و world لتمثيل عالم اللعبة. بعد تسمية الطبقات، دعونا نخصص كل كائن في اللعبة ليكون جزءًا من إحدى هذه الطبقات. تعيين الطبقات والأقنعة حان دور تعيين الطبقات والأقنعة للكائنات الموجودة في مشهد اللعبة، بداية سنحدد عقدة الأرضية Ground في المشهد الرئيسي Main. ونوسّع قسم الاصطدام Collision في الفاحص Inspector. سنرى عندها طبقات العقدة وأقنعتها على شكل شبكة من الأزرار كما في الصورة التالية: الأرضية جزء من عالم اللعبة، ونريدها أن تكون جزءًا من الطبقة الثالثة في مشهد لعبتنا. للقيام بذلك ليس علينا سوى تعطيل الزر المفعل رقم 1 الذي يمثل الطبقة الأولى، وتفعيل الزر رقم 3 الذي يمثل الطبقة الثالثة بالنقر فوقه، بعدها سنعطّل الزر رقم 1 للقناع بالنقر عليه أيضًا. كما ذكرنا سابقًا، تتيح خاصية القناع للعقدة إمكانية التفاعل مع الكائنات الفيزيائية الأخرى دون الحاجة إلى تسجيل تصادمات فعلية. على سبيل المثال، لا تحتاج عقدة الأرضية إلى الاستماع إلى أي تصادمات، إذ يقتصر دورها على منع العناصر من السقوط. لاحظ أن بإمكانك النقر فوق الثلاث نقاط الموجودة على الجانب الأيمن من الخصائص لعرض قائمة بمربعات الاختيار التي تمثل أسماء الطبقات. تسمح هذه المربعات بتخصيص الطبقات والأقنعة بدقة، وتمكنك من تحديد الطبقات التي تنتمي العقدة إليها، وتفعيل أو تعطيل التفاعلات مع الطبقات الأخرى. سنعالج الآن عقدتي Player و Mob، وللقيام ذلك افتح مشهد اللاعب player.tscn بالنقر المزدوج فوق ملفه الموجود أسفل نافذة نظام الملفات في جودو. وللتحكم في تفاعلات اللاعب مع الأعداء والعالم، حدد عقدة اللاعب وعيّن القناع Mask من قسم الاصطدام Collision لكل من الأعداء enemies والعالم world. يمكنك ترك خاصية الطبقة الافتراضية كما هي لأن الطبقة الأولى هي طبقة اللاعب player. بعدها، لنفتح مشهد الأعداء Mob بالنقر المزدوج على الملف mob.tscn ونحدد عقدة Mob ونضبط طبقة تصادمه من قسم التصادم Collision الموجود داخل قسم الطبقة Layer ونجعل قيمته enemies لنحدد أنه سيصطدم فقط مع الأجسام التي تنتمي إلى طبقة الأعداء، ونلغي ضبط قناع التصادم من القسم تصادم Collision الموجود داخل قسم القناع Mask ونتركه فارغًا، لنمنعه من التفاعل مع الطبقات الأخرى كاللاعب أو البيئة. بعد هذه الإعدادات، ستحدث تداخلات بين الأعداء أي أنهم قد يتصادمون أو يتداخلون مع بعضهم البعض. إذا كنت تريد أن ينزلق الأعداء بعيدًا عن بعضهم عندما يصطدمون، يجب تفعيل قناع الأعداء لضمان عدم تداخلهم وجعلهم يتفاعلون بطريقة تسمح لهم بالابتعاد عن بعضهم البعض عندما يحدث التصادم. ملاحظة: لا يحتاج الأعداء إلى استخدام قناع mask لطبقة العالم world لأن حركتهم محصورة على المستوى XZ الذي يحدد الاتجاهين العرضي والطولي، ونحن لا نطبق عليهم أي جاذبية بشكل افتراضي. تنفيذ القفز في هذه الخطوة، سنكتب كود بسيط لتنفيذ القفز في اللعبة، تتطلب عملية القفز وحدها سطرين فقط من التعليمات البرمجية، ولكتابتها افتح السكربت الخاص بالعقدة Player. سنحتاج إلى قيمة للتحكم في قوة القفزة وتحديث ()_physics_process لبرمجة القفزة. لنضف jump_impulse بعد السطر الذي يحدد fall_acceleration في الجزء العلوي من السكربت. #... # تطبيق الدفعة العمودية للشخصية عند القفر مقدرة بواحدة المتر @export var jump_impulse = 20 ثم أضف الشيفرة البرمجية التالية قبل كتلة التعليمات البرمجية move_and_slide() داخل _physics_process(). func _physics_process(delta): #... # القفز if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse #... هذا كل ما تحتاجه للقفز! يعد التابع is_on_floor() أداة من الصنف CharacterBody3D، فهو يعيد true إذا اصطدم الجسم بالأرضية في هذا الإطار، ولهذا السبب نطبق الجاذبية على شخصية اللاعب فنجعله يصطدم بالأرض بدلاً من أن يطفو فوقها مثل شخصيات الأعداء. عندما تكون الشخصية على الأرض ونضغط على زر القفز، نمنحها دفعة فورية وقوية في الاتجاه العمودي أي على المحور Y حتى تقفز بسرعة. تجعل هذه الطريقة استجابة التحكم سريعة وواقعية. وتجدر الملاحظة بأن المحور Y في الألعاب ثلاثية الأبعاد يكون موجبًا للأعلى أي أن القفز يجعل القيمة في Y تزداد، وهذا يختلف عن الألعاب ثنائية الأبعاد حيث يكون المحور Y موجبًا للأسفل. تدمير الأعداء دعنا نضف ميزة تدمير الأعداء للعبتنا، سنجعل من الشخصية تثب فوق الأعداء وتقتلهم في نفس الوقت. سنكون بحاجة إلى اكتشاف الاصطدامات مع العدو وتمييزها عن الاصطدامات بالأرضية. للقيام بذلك، يمكننا استخدام ميزة التصنيف بالوسوم الخاصة بـجودو. افتح المشهد mob.tscn مرة أخرى وحدد عقدة Mob. انتقل إلى قائمة العقدة Node على اليمين لرؤية قائمة الإشارات. تحتوي قائمة العقدة Node تبوبين هما الإشارات Signals التي استخدمناها سابقًا، والمجموعات Groups التي تسمح لنا بإسناد وسوم للعُقد. انقر عليها لتكشف عن حقل مخصص لكتابة اسم الوسم، اكتب mob في هذا الحقل وانقر فوق زر إضافة Add. سيظهر رمز في قائمة المشهد Scene للإشارة إلى أن العقدة جزء من مجموعة واحدة على الأقل. يمكننا الآن استخدام هذه المجموعة في شيفرتنا البرمجية لتمييز الاصطدامات بالأعداء عن الاصطدامات بالأرض. برمجة عملية تدمير اللاعب لبرمجة عملية التدمير والارتداد في السكربت الخاص باللاعب، سنحتاج إلى إضافة خاصية جديدة تُسمى bounce_impulse في الجزء العلوي من السكربت. لا نريد بالضرورة عند تدمير عدو أن ترتفع الشخصية إلى أعلى مستوى كما هو الحال عند القفز، هنا ستساعدنا هذه الخاصية في ضبط مقدار الارتداد بما يناسب الموقف. # تطبيق الدفعة العمودية للشخصية عند القفر مقدرة بواحدة المتر @export var bounce_impulse = 16 والآن وبعد كتلة شيفرة القفز التي أضفناها أعلاه في _physics_process() نضيف الحلقة التالية، إذ يجعل جودو الجسم يتحرك أحيانًا عدة مرات متتالية لتسهيل حركة الشخصية باستخدام move_and_slide()، لذلك علينا أن نراجع جميع الاصطدامات التي قد تكون حدثت. سنتحقق في في كل تكرار للحلقة فيما إذا كنا لامسنا عدو، وإذا كان الأمر كذلك، فإننا نقتله ونرتد عنه. لن تعمل الحلقة إذا لم يحدث أي تصادم في إطار معين. لاحظ الشيفرة التالية: func _physics_process(delta): #... # كرّر خلال كل الاصطدامات التي تحصل خلال هذا الإطار for index in range(get_slide_collision_count()): # نحصل على واحد من الاصطدامات مع اللاعب var collision = get_slide_collision(index) # إذا كان الاصطدام مع الأرض if collision.get_collider() == null: continue # إذا كان الاصطدام مع العدو if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # نتأكد من الضربة أنها من الأعلى if Vector3.UP.dot(collision.get_normal()) > 0.1: # إذا كان كذلك ندمره ونقفز mob.squash() target_velocity.y = bounce_impulse # تمنع أي استدعاءات مكررة break تأتي الدالتان get_slide_collision_count() وget_slide_collision() كلاهما من الصنف CharacterBody3D وهما مرتبطتان بالدالة move_and_slide(). تُعيد الدالة get_slide_collision() كائن KinematicCollision3D الذي يحتوي على معلومات حول مكان وكيفية حدوث التصادم، على سبيل المثال نستخدم الخاصية get_collider الخاصة بها للتحقق مما إذا كنا قد اصطدمنا بـعدو mob عن طريق استدعاء is_in_group() عليه بهذا الشكل: collision.get_collider().is_in_group("mob") ملاحظة: التابع is_in_group() متاح في كل صنف عقدة Node. بعدها نستخدم جداء الأشعة النقطي vector dot product للتأكد من أننا هبطنا على العدو: Vector3.UP.dot(collision.get_normal()) > 0.1 التصادم الافتراضي هو شعاع ثلاثي الأبعاد متعامد مع المستوي الذي حدث فيه الاصطدام، يتيح لنا الجداء النقطي مقارنته بالاتجاه الصاعد. في حالة الجداء النقطي؛ عندما تكون النتيجة أكبر من 0 يكون المتجهان بزاوية أقل من 90 درجة، والقيمة الأعلى من 0.1 تخبرنا أننا فوق العدو تقريبًا. بعد التعامل مع منطق التدمير والارتداد، نخرج من الحلقة عبر عبارة Break لمنع المزيد من الاستدعاءات المكررة إلى mob.squash()، التي قد تؤدي بخلاف ذلك إلى أخطاء غير مقصودة مثل حساب النتيجة عدة مرات لقتل عدو واحد. نستدعي هنا دالة واحدة غير معرّفة ألا وهي mob.squash()، لذلك يتعين علينا إضافتها إلى صنف العدو Mob. افتح السكربت Mob.gd بالنقر المزدوج عليه في نافذة نظام الملفات. نريد تحديد إشارة جديدة في الجزء العلوي من السكربت تسمى squashed. وفي الأسفل، يمكنك إضافة دالة التدمير، حيث نصدر الإشارة وندمر العدو. # تنبثق عندما يقفز اللاعب على العدو signal squashed # ... func squash(): squashed.emit() queue_free() لتجربة اللعبة، يمكن الضغط على F5 وتعيين main.tscn كمشهد رئيسي للمشروع. الخلاصة بهذا نكون قد تعرفنا على كيفية إضافة خاصية القفز وتدمير الأعداء في الألعاب ثلاثية الأبعاد عبر محرك الألعاب جودو Godot، وسنتعلم في المقالات القادمة كيفية التعامل مع خصائص أخرى قد تحتاجها لتطوير ألعابك، مثل مفهوم الإشارة Signal الذي سنتعرف عليه في المقال التالي لإضافة نقاط لنتائج الألعاب عند تدمير الأعداء وجعل اللاعبين يفارقون الحياة عند اصطدامهم مع الأعداء. ترجمة - وبتصرف - لقسم Jumping and squashing monsters من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: توليد الأعداء في لعبة ثلاثية الأبعاد في محرك الألعاب جودو إعداد مشهد اللاعب وعمليات الإدخال في لعبة ثلاثية الأبعاد باستخدام جودو العقد Nodes والمشاهد Scenes في جودو Godot كتابة برنامجك الأول باستخدام جودو Godot
-
سنتطرق في هذا الدرس إلى شرح كيفية توليد الأعداء في لعبة ثلاثية الأبعاد وذلك ضمن مسار عشوائي نختاره، بحيث يصبح لدينا بضعة أعداء ضمن مساحة اللعب. فلا فائدة من الأعداء ضمن اللعبة إن لم نستطع إيجاد طريقة مناسبة لتوليدهم، نعشرح في الفقرات التالية طريقة القيام بذلك بإنشاء مسار افتراضي وتوليد الأعداء عليه بشكل عشوائي. تغيير دقة اللعبة بداية سنغيّر دقة اللعبة قبل إنشاء المسار، حيث يبلغ حجم النافذة الافتراضية للعبة 1152x648 وسنعدّلها إلى 720x540 لتصبح أصغر حجمًا وأكثر توافقًا مع التصميم المطلوب. نفتح المشهد الرئيسي Main للعبة بالنقر نقرًا مزدوجًا على main.tscn في قائمة نظام الملفات Filesystem dock الموجود على الشريط الجانبي الذي يعرض ملفات المشروع. ننتقل بعدها إلى تبويب المشروع Project ثم نختار إعدادات المشروع Project Settings. ننتقل بعد ذلك إلى القسم Display في القائمة الموجودة يسار نافذة إعدادات المشروع، ثم ونمرر إلى الأسفل حتى نجد قسم Window. ونغير قيمة العرض Width إلى 720، وقيمة الارتفاع Height إلى 540 كما في الصورة أدناه. إنشاء مسار توليد الأعداء Spawn Path نحتاج الآن إلى تصميم مسار ثلاثي الأبعاد وهنا نحتاج لاستخدام العقدة PathFollow3D لتوليد مواقع عشوائية على هذا المسار، الأمر هنا مشابه لما شرحناه عند إنشاء المشهد الرئيسي وتوليد الأعداد في اللعبة ثنائية الأبعاد في جودو، لكن رسم المسار في المجال ثلاثي الأبعاد أكثر تعقيدًا، حيث نريد أن يحيط المسار بنافذة عرض اللعبة، كي يظهر الأعداء مباشرة من خارج نطاق رؤية اللاعب. مع ذلك، إذا رسمنا المسار ضمن هذا المجال، فقد لا يكون مرئيًا في معاينة الكاميرا مما يجعل تحديد موضعه بدقة أمرًا صعبًا. لذلك، سنستخدم طريقة تساعدنا في تصور حدود نافذة العرض وتنسيق المسار بشكل صحيح. يمكننا استخدام بعض الشبكات المؤقتة placeholder meshes للعثور على حدود العرض، والشبكات المؤقتة هي مجسمات ثلاثية الأبعاد بسيطة مثل مربعات أو كرات توفر أدوات مساعدة أثناء التصميم وتساعدنا على تحديد حدود نافذة العرض في مشهد ثلاثي الأبعاد وتصور أين يمكن أن يبدأ المسار وأين تكون العناصر خارج نطاق الكاميرا. يجب أن تكون نافذة العرض مقسمة إلى قسمين مع معاينة الكاميرا في الأسفل. إذا لم تكن كذلك فاضغط على ctrl + 2 أو cmd + 2 في حال كنت تستخدم نظام تشغيل macOS لقسمها إلى قسمين، ثم حدد عقدة Camera3D وانقر فوق مربع الاختيار Preview في أسفل نافذة العرض. إضافة أسطوانات مؤقتة CylinderMesh الآن، يمكن وضع أسطوانات مؤقتة حول المشهد لتصور حدود نافذة العرض. وبمجرد الانتهاء من تصميم المسار بناءً على هذه الأسطوانات، يمكننا إزالتها أو إخفاؤها. لإضافة شبكة اسطوانات مؤقتة، أضف عقدة Node3D جديدة كعقدة فرعية للعقدة الرئيسية وسمّها Cylinders، ثم حددها وأضف عقدة فرعية MeshInstance3D لنستخدمها في تجميع الأسطوانات. حدّد Cylinders لإضافة عقدة MeshInstance3D فرعية. أسند CylinderMesh إلى الخاصية Mesh من قائمة الفاحص Inspector كما يلي: عند العمل في جودو سيساعدنا التبديل بين طرق عرض مختلفة في نافذة المشهد Viewport على رؤية المشهد من زوايا مختلفة. وأحد هذه الزوايا هي Top View التي تعني المنظور العلوي، حيث سترى من خلالها المشهد كما لو كنت تنظر إليه من أعلى، مما يساعدك على ترتيب الأشياء بشكل دقيق. اضبط نافذة العرض على خيار العرض العمودي العُلوي Top View باستخدام القائمة الظاهرة أعلى يسار نافذة العرض. إذا كانت الشبكة الظاهرة تُشتت تركيزك، فيمكنك إيقاف عرضها أو تعديلها بالانتقال إلى قائمة العرض في شريط الأدوات والنقر فوق عرض الشبكة View Grid، عند النقر فوق هذا الخيار، سيتوقف عرض الشبكة على شاشة المحرر إذا كانت مُفعلة مسبقًا وإذا كنت تريد إظهار الشبكة مرة أخرى، يمكنك تفعيل الخيار من جديد. يساعدك تحريك الأسطوانة في المشهد على مراقبة كيفية ظهورها من زاوية الكاميرا، ويسهل عليك ترتيب العناصر بشكل دقيق واحترافي في المستوى الأرضي، لذا ابدأ يتحريك الأسطوانة على طول المستوى الأرضي وانظر لمعاينة الكاميرا أسفل نافذة العرض. يُفضل أن تستخدم الالتقاط الشبكي Grid Sanp لفعل ذلك، حيث يمكنك تفعيله عن طريق النقر على أيقونة المغناطيس في شريط الأدوات كما في الصورة التالية أو الضغط على مفتاح Y. حرك الأسطوانة بحيث تكون خارج رؤية الكاميرا مباشرة في الزاوية العلوية اليسرى كما في الصورة التالية. الآن علينا تكرار الشبكة أو الأسطوانة التي استخدمناها كشبكة مؤقتة ووضعها في مناطق مختلفة حول منطقة اللعب بحيث تتوفر مجموعة نسخ ضمن المشهد، ونسحبها إلى خارج معاينة الكاميرا مباشرة، فهذا سيساعد هذا على تصور حدود اللعبة بشكل جيد. لإنشاء نسخ من الشبكة ووضعها حول منطقة اللعب. نضغط على Ctrl + D أو Cmd + D على نظام تشغيل macOS لتكرار العقدة، ويمكن أيضًا النقر بالزر الأيمن على العقدة في قائمة المشهد وتحديد خيار مضاعفة Duplicate، ثم تحريك النسخة لأسفل على طول المحور Z الأزرق حتى تصل لخارج معاينة الكاميرا مباشرة. نحدد كلا الأسطوانتين بالضغط على مفتاح Shift والنقر على الأسطوانة التي لم يتم تحديدها ومضاعفتها. نحركهم إلى اليمين عن طريق سحب المحور X باللون الأحمر. يصعب رؤية الاسطوانة قليلًا باللون الأبيض كما تلاحظ، لذا من الأفضل إبرازها بشكل أفضل من خلال إعطائها مواد Materials جديدة فالمواد تساعد في تحديد الخصائص المرئية للسطح، مثل لونه وكيفية تفاعل الضوء معه. يمكننا تحديث جميع الأسطوانات الأربعة مرة واحدة من خلال تحديد جميع نسخ الشبكة في قائمة المشهد عن طريق النقر فوق أول واحدة ثم الضغط مفتاح Shift والنقر على آخر واحدة. من قائمة الفاحص Inspector، نوسع قسم المادة Material لتظهر الخيارات الموجودة بداخله، سنضيف الخيار StandardMaterial3D إلى الحيز الأول في القائمة -وهو الحيز رقم 0- وبذلك، ستطبق المادة على الاسطوانات التي حددتها في المشهد. انقر على أيقونة الجسم الكروي لفتح مورد المادة Material resource حيث ستحصل هنا على معاينة للمادة وقائمة طويلة من الخصائص لاستخدامها في إنشاء جميع أنواع الأسطح، من المعدن إلى الصخور أو الماء. لتغيير اللون افتح قسم Albedo داخل إعدادات المادة: اضبط اللون على خيار يتناقض مع الخلفية، مثل اللون البرتقالي الساطع. يمكنك الآن استخدام الأسطوانات كدليل. ضعها في قائمة المشهد بالنقر فوق السهم الرمادي المجاور لها، ويمكنك التبديل بين عرضها وإخفائها من خلال النقر على أيقونة العين بجوار الأسطوانات. الآن أضف عقدة فرعية Path3D إلى العقدة الرئيسيةMain. ستظهر في شريط الأدوات أربع أيقونات، انقر على أداة Add Point التي تحمل علامة "+" الخضراء كما يلي: ملاحظة: يمكنك تمرير المؤشر فوق أي رمز لرؤية تلميح توضيحي يصف الأداة. انقر وسط كل أسطوانة لإنشاء نقطة، ثم انقر فوق رمز إغلاق المنحنى Close Curve في شريط الأدوات لإغلاق المسار. في حال كانت أي من النقاط بعيدة قليلاً، يمكنك النقر فوقها وسحبها لإعادة تموضعها. يجب أن يبدو مسارك كالتالي. نحتاج إلى عقدة PathFollow3D لاختيار مواضع عشوائية عليها، لذا أضف PathFollow3D كعنصر فرعي لعقدة Path3D ثم أعد تسمية العقدتين إلى SpawnLocation و SpawnPath على التوالي فذلك سيوضّح حالة استخدامنا هنا. نحن جاهزون الآن لبرمجة آلية توليد الأعداء في اللعبة. توليد الأعداء عشوائيًا انقر بالزر الأيمن على العقدة الرئيسية وأرفق سكريبت جديد لها من أجل توليد الأعداء بشكل دوري، أي على فترات زمنية منتظمة. نضيف أولاً متغير إلى قائمة الفاحص Inspector بحيث يمكننا إسناد mob.tscn أو أي عدو آخر إليه كما يلي: extends Node @export var mob_scene: PackedScene وبما أننا نريد توليد العدو على فترات زمنية منتظمة، سنحتاج إلى العودة إلى المشهد وإضافة مؤقت، لكن قبل ذلك نحتاج إلى إسناد ملف mob.tscn إلى خاصية mob_scene أعلاه بخلاف ذلك سيكون فارغ. عد إلى الشاشة ثلاثية الأبعاد وحدد العقدة الرئيسية، ثم اسحب mob.tscn من قائمة نظام الملفات إلى حيز مشهد العدو في قائمة الفاحص Inspector. أضف عقدة Timer جديدة كعنصر فرعي للعقدة الرئيسية، وسمها MobTimer اضبط من قائمة الفاحص Inspector وقت الانتظار Wait Time على القيمة 0.5 ثانية وفعّل التشغيل التلقائي Autostart حتى يبدأ توليد الأعداء تلقائيًا عند تشغيل اللعبة. تطلق المؤقتات إشارة timeout في كل مرة تصل فيها إلى نهاية وقت انتظارها، وبشكل افتراضي يُعاد تشغيلها تلقائيًا، مما يؤدي إلى إصدار الإشارة بشكل متواصل، ويمكن الاتصال بها من العقدة الرئيسية لتوليد عدو كل 0.5 ثانية. توجه إلى قائمة العقدة على اليمين أثناء تحديد MobTimer وانقر نقرًا مزدوجًا على إشارة timeout صِلها بالعقدة الرئيسية سيعيدك هذا إلى النص البرمجي مع دالة جديدة _on_mob_timer_timeout() هذه الدالة فارغة ولا تحتوي الدالة أي منطق برمجي لذا نحتاج لبرمجة منطق توليد العدو فيها عن طريق الخطوات التالية: إنشاء نسخة من مشهد العدو باستخدام mob_scene.instantiate() أخذ عينة عشوائية لموقع العدو باستخدام randf_range() الحصول على موضع اللاعب عبر الوصول إلى العقدة Player في المشهد استدعاء دالة initialize() لإعداد العدو باستخدام الموقع العشوائي وموقع اللاعب 5 إضافة العدو كعنصر فرعي للعقدة الرئيسية في المشهد باستخدامadd_child() بهذا يمكن توليد الأعداء بشكل دوري في نقاط عشوائية على المسار، مع الأخذ بعين الاعتبار مكان اللاعب ليتفاعل العدو معه كما توضح الشيفرة التالية: func _on_mob_timer_timeout(): # إنشاء نسخة جديدة من مشهد العدو var mob = mob_scene.instantiate() # اختر مكان عشوائي على SpawnPath # نخزن المرجع إلى عقدة SpawnLocation var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # ونعطيها انزياح عشوائي mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # توليد الأعداء عن طريق إضافتها إلى المشهد الأساسي add_child(mob) تُنتج دالة randf() قيمة عشوائية بين 0 و1، وهي تمثل القيمة المتوقعة للمتغير progress_ratio في عقدة PathFollow. حيث تشير 0 إلى بداية المسار، وتشير 1 إلى نهايته. وبما أن المسار الذي حددناه يحيط بنافذة عرض الكاميرا، لذا فإن أي قيمة عشوائية بين 0 و1 ستحدد موقعًا عشوائيًا على طول حواف نافذة العرض. فيما يلي الكود البرمجي الكامل للملف main.gd للرجوع إليه عند الحاجة: extends Node @export var mob_scene: PackedScene func _on_mob_timer_timeout(): # إنشاء نسخة جديدة من مشهد العدو var mob = mob_scene.instantiate() # اختر مكان عشوائي على SpawnPath # نخزن المرجع إلى عقدة SpawnLocation var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # ونعطيها انزياح عشوائي mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # توليد الأعداء عن طريق إضافتها إلى المشهد الأساسي add_child(mob) يمكنك الآن اختبار المشهد بالضغط على F6 حيث يجب أن ترى الأعداء تظهر وتتحرك بخط مستقيم. تتصادم شخصيات الأعداء حاليًا وتنزلق بمواجهة بعضها عندما تلتقي مساراتها، وسنتحدث عن هذه المسألة في الدرس التالي ونوضح طريقة ضبط التصادمات. الخلاصة وصلنا إلى نهاية مقال اليوم، حيث تعلمنا كيفية توليد الأعداء داخل مشهد لعبتنا ثلاثية الأبعاد باستخدام مسار توليد محدد ينتج شخصيات الأعداء بشكل عشوائي. سنتناول في الدرس القادم، كيفية برمجة وضبط حركة اللاعب بحيث يمكنه القفز على الأعداء وتدميرهم. ترجمة -وبتصرف- لقسم Spawning monsters من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: تصميم مشهد الأعداء للعبة ثلاثية الأبعاد في جودو استخدام الإشارات Signals في جودو Godot دليلك الشامل إلى برمجة الألعاب أشهر أنواع الألعاب الإلكترونية
-
ستتعلم في هذا المقال كيفية برمجة الأعداء في لعبة ثلاثية الأبعاد في محرك الألعاب جودو، حيث سنطلق على كل عدو اسم mob. ستتعرف أيضًا على كيفية توليدهم عشوائيًا في مواقع مختلفة حول منطقة اللعب. إنشاء مشهد الأعداء لنصمم الأعداء في لعبتنا نحتاج لإنشاء مشهد جديد وسيكون هيكل العقدة مشابهًا لمشهد اللاعب الذي أطلقنا عليه اسم player.tscn. أنشئ مشهدًا يحتوي على عقدة CharacterBody3D لتكون بمثابة جذر المشهد فهذه العقدة توفر وظائف فيزياء جاهزة وتوفر حركات واقعية وأطلق عليها اسم Mob، وأضف عقدة فرعية Node3D وأطلق عليها اسم Pivot لتكون كنقطة ارتكاز لتحريك الشخصية، ثم اسحب وأفلت الملف mob.glb من قائمة نظام الملفات إلى Pivot لإضافة نموذج العدو ثلاثي الأبعاد إلى المشهد. بعد سحب الملف ستنشأ عقدة جديدة تمثل الكائن الرسومي داخل اللعب، يمكنك إعادة تسميتها إلى Character نحتاج إلى هيكل تصادم collision shape ليعمل الجسم بالشكل المتوقع ويتمكن من التفاعل مع الفيزياء أو الاصطدامات داخل اللعبة بطريقة صحيحة، لذا انقر بزر الفأرة الأيمن على عقدة جذر المشهد Mob ثم انقر فوق إضافة عقدة فرعية Add Child Node. أضف العقدة CollisionShape3D تحتوي العقدة CollisionShape3D على خاصية الشكل Shape. اربط بهذه الخاصية عقدة BoxShape3D من قائمة الفاحص Inspector لإضافة شكل صندوق ثلاثي الأبعاد لتحديد حدود تصادم الكائنات كما في الصورة التالية: لتحسين تجربة اللعبة، عليك تعديل حجم الصندوق BoxShape3D بحيث يتناسب بشكل أفضل مع النموذج ثلاثي الأبعاد، يمكنك القيام بذلك بسهولة عن طريق النقر على النقاط البرتقالية الظاهرة على الصندوق وسحبها لتغيير حجمه. يجب أن يلمس الصندوق الأرض ويكون أقل ثخانة من النموذج بقليل، حيث تعمل محركات الفيزياء بطريقة تجعل الاصطدام يحدث إذا لامس مجسّم اللاعب الكروي حتى زاوية الصندوق، وقد تموت على مسافة من العدو إذا كان الصندوق كبيرًا جدًا مقارنة بالنموذج ثلاثي الأبعاد، مما يجعل اللعبة غير منصفة للاعبين ولا تحقق تجربة لعب صحيحة. لاحظ أن الصندوق الذي يمثل منطقة التصادم الذي أضفناها للعدو أكبر من النموذج ثلاثي الأبعاد للعدو، لا بأس في هذا ضمن هذه اللعبة لأننا ننظر إلى المشهد من الأعلى ونستخدم منظور ثابت أي أن الكاميرا تعرض المشهد من الأعلى ولا تتحرك، فلا يجب أن تتطابق أشكال التصادم تمامًا مع النموذج وإذا كانت منطقة التصادم الأكبر تجعل اللعبة أكثر سهولة أو متعة عند اختبارها، فلا بأس في ذلك. دورة تطوير الألعاب ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة ومليئة بالتحديات. اشترك الآن إزالة شخصيات الأعداء الموجودين خارج الشاشة سنعمل على توليد شخصيات الأعداء في اللعبة على فترات زمنية منتظمة في مرحلة اللعبة. لكن انتبه فقد يزداد عدد الأعداء في هذه الحالة إلى ما لا نهاية إذا لم نكن حذرين، وليس هذا ما نريده لأن كل نسخة من العدو لها تكلفة ذاكرة ومعالجة ولا حاجة لتحمل تكلفتها عندما يكون العدو خارج الشاشة. فبمجرد أن يخرج عدو ما من نطاق الشاشة فإننا لم نعد بحاجته بعد ذلك ويجب حذفه. لحسن الحظ يوفر محرك الألعاب جودو عقدة تكتشف خروج الكائنات من حدود الشاشة تسمى VisibleOnScreenNotifier3D وسنستخدمها لتدمير الأعداء. ملاحظة: عندما تستمر في استنساخ كائن ما ينصح باستخدام تقنية تسمى pooling وتعني تجميع الكائنات وإعادة استخدامها لتجنب تكلفة إنشاء وتدمير النسخ في كل مرة، فهي تعتمد على إنشاء مسبق لمصفوفة من الكائنات وحفظها في الذاكرة وإعادة استخدامها مرارًا وتكرارًا لتحسين الأداء. لكن لا داعي للقلق بشأن هذا الأمر عند العمل بلغة GDScript، فالسبب الرئيسي لاستخدام تقنية pooling هو تجنب التجمد في اللغات التي تعتمد على كنس المهملات garbage-collected مثل لغة C#، أما لغة GDScript فتستخدم تقنية عد المراجع مختلفة في إدارة الذاكرة. لمراقبة ما إذا كان الكائن قد خرج من الشاشة حدد عقدة Mob وأضف عقدة فرعية VisibleOnScreenNotifier3D، سيظهر صندوق آخر وردي اللون هذه المرة وستصدر العقدة إشارة عندما يخرج هذا المربع تمامًا عن الشاشة. غيّر حجمه باستخدام النقاط البرتقالية حتى يغطي النموذج ثلاثي الأبعاد بالكامل. برمجة حركة العدو دعنا الآن نعمل على حركة الأعداء، وسننجز ذلك على مرحلتين، الأولى سنكتب سكريبت على Mob يعرّف دالة لتهيئة العدو، ثم نبرمج آلية الظهور العشوائي في مشهد main.tscn ونستدعي الدالة من هناك. أرفق سكريبت بالعقدة Mob. فيما يلي الشيفرة البرمجية التي ستنجز الحركة بلغة GDScript، كما تلاحظ عرفنا خاصيتين min_speed و max_speed لتحديد نطاق سرعة عشوائي والذي سنستخدمه لاحقًا لضبط قيمة سرعة الحركة CharacterBody3D.velocity. extends CharacterBody3D # سرعة العدو الدنيا مقدرة بالمتر في الثانية. @export var min_speed = 10 # سرعة العدو القصوى مقدرة بالمتر في الثانية. @export var max_speed = 18 func _physics_process(_delta): move_and_slide() سنحرك العدو على غرار تحريك اللاعب في كل إطار عن طريق استدعاء الدالة CharacterBody3D.move_and_slide(). لكن لا نعمل هذه المرة على تحديث السرعة في كل إطار بل نريد أن يتحرك العدو بسرعة ثابتة ويخرج من الشاشة حتى لو اصطدم بعائق. نحتاج إلى تعريف دالة أخرى لحساب CharacterBody3D.velocity حيث تحوّل هذه الدالة العدو باتجاه اللاعب وتضفي طابعًا عشوائيًا على كل من زاوية حركته وسرعته. ستأخذ الدالة موقع ظهور العدو أول مرة start_position، وموقع اللاعب player_position كوسيط. نضع العدو في الموقع start_position ونوجهه نحو اللاعب باستخدام الدالةlook_at_from_position()، ولجعل الحركة أكثر طبيعية نعطي العدو زاوية عشوائية عن طريق الدوران بمقدار عشوائي حول المحور Y بحيث لا يكون دائمًا موجهًا بشكل مباشر نحو اللاعب. تعطينا الدالة randf_range() في الكود التالي قيمة عشوائية بين -PI/4 راديان و PI/4 راديان وسنستخدمها لتدوير العدو. # يتم استدعاء هذه الدالة من المشهد الأساسي func initialize(start_position, player_position): # نحدد واحد من الأعداء بوضعه في start_position # ونديره باتجاه player_position ليواجه اللاعب look_at_from_position(start_position, player_position, Vector3.UP) # دوّر هذا العدو تلقائيًا بين -45 و +45 درجة # لكي لا تتحرك نحو اللاعب بشكل مباشر. rotate_y(randf_range(-PI / 4, PI / 4)) لقد حصلنا على موقع عشوائي، والآن نحتاج إلى تحديد سرعة عشوائية باستخدام random_speed. سنستخدم الدالة randi_range() للحصول على قيمة عدد صحيح عشوائي حيث سنحدد الحد الأدنى min_speed والحد الأقصى max_speed للسرعة.أما random_speed فهو مجرد عدد صحيح نستخدمه لمضاعفة سرعة الحركة CharacterBody3D.velocity. بعد ذلك، سنوجه شعاع CharacterBody3D.velocity نحو اللاعب مع تطبيق random_speed لتحديد السرعة. func initialize(start_position, player_position): # ... # نحسب السرعة العشوائية (عدد صحيح) var random_speed = randi_range(min_speed, max_speed) # نحسب سرعة أمامية تمثّل السرعة velocity = Vector3.FORWARD * random_speed # ثم ندوّر شعاع السرعة على اتجاه دوران العدو حول Y # لكي يتحرك في اتجاه نظر العدو velocity = velocity.rotated(Vector3.UP, rotation.y) مغادرة الشاشة ما زلنا بحاجة إلى تدمير الأعداء عندما يخرجون من الشاشة، لذلك سنربط إشارة screen_exited الخاصة بعقدة VisibleOnScreenNotifier3D بالعقدة Mob. عد إلى نافذة العرض لاثي الأبعاد، اضغط على التسمية ثلاثي الأبعاد 3D أعلى المحرر أو يمكنك الضغط على Ctrl+F2 أو Alt+2 على نظام التشغيل macOS. حدد العقدة VisibleOnScreenNotifier3D وانتقل إلى القائمة التي تعرض معلومات العقدة في الجانب الأيمن من واجهة المحرر، ثم انقر نقرًا مزدوجًا فوق الإشارة screen_exited()التي تُرسل عندما يخرج الكائن من الشاشة. صِل هذه الإشارة بالعقدة Mob سيؤدي هذا إلى إعادتك إلى محرر النصوص وإضافة دالة جديدة _on_visible_on_screen_notifier_3d_screen_exited() ثم استدعِ من خلالها تابع queue_free() حيث تعمل هذه الدالة على تدمير النسخة التي يتم استدعاؤها عليها. func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() بهذا أصبح العدو جاهزًا للدخول إلى منطقة اللعب وسنشرح في الدرس التالي كيفية توليد الأعداء داخل مستوى اللعبة. إليك الشيفرة البرمجية الكاملة للملف Mob.gd للرجوع إليها: extends CharacterBody3D # سرعة العدو الدنيا مقدرة بالمتر في الثانية @export var min_speed = 10 # سرعة العدو القصوى مقدرة بالمتر في الثانية @export var max_speed = 18 func _physics_process(_delta): move_and_slide() # يتم استدعاء هذه الدالة من المشهد الأساسي func initialize(start_position, player_position): # نحدد واحد من العدو بوضعه في start_position # ونديره باتجاه player_position, ليواجه اللاعب.. look_at_from_position(start_position, player_position, Vector3.UP) # دوّر هذا العدو تلقائيًا بين -45 و +45 درجة, # لكي لا تتحرك نحو اللاعب بشكل مباشر rotate_y(randf_range(-PI / 4, PI / 4)) # نحسب السرعة العشوائية (عدد صحيح) var random_speed = randi_range(min_speed, max_speed) # نحسب سرعة أمامية تمثّل السرعة velocity = Vector3.FORWARD * random_speed # ثم ندوّر شعاع السرعة على اتجاه دوران العدو حول Y # لكي يتحرك في اتجاه نظر العدو velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() الخلاصة تعلمنا في هذا المقال تصميم وضبط مشهد الأعداء في لعبتنا ثلاثية الأبعاد في محرك جودو، كما شرحنا كيفية إزالة شخصيات الأعداء بعد تخطيهم لحدود الشاشة، بهذا أصبح مشروعنا جاهزًا لبرمجة وتصميم التفاعلات بين اللاعب والأعداء، وهو ما سنفعله في الدرس التالي. ترجمة -وبتصرف- لقسم Designing the mob scene من توثيق جودو الرسمي. اقرأ أيضًا المقال السابق: تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو الاستماع لمدخلات اللاعب في جودو Godot الرؤية التصميمية لمحرك اﻷلعاب جودو Godot إعداد محرك الألعاب جودو Godot للعمل مع قاعدة البيانات SQLite
-
حان وقت البرمجة في سلسلتنا التي تشرح تطوير الألعاب ثلاثية الأبعاد باستخدام محرك الألعاب جودو، إذ سنستخدم إجراءات الإدخال التي أنشأناها في الدرس الماضي لتحريك الشخصية في هذا الدرس باستخدام الشيفرة البرمجية، إذ ستساعدنا الشيفرة بضبط تسارع حركة اللاعب وسرعته القصوى بالإضافة لسرعة الجاذبية التي تحدد مقدار تأثير الجاذبية الافتراضية على حركة الشخصية في اللعبة وغيرها من المفاهيم المفيدة. انقر بزر الفأرة الأيمن على عقدة Player وحدد خيار إضافة سكريبت Attach Script لإضافة سكربت جديد إليها، ثم اضبط القالب Template في النافذة المنبثقة على Empty قبل الضغط على زر إنشاء Create. سنعرّف خصائص الصنف class مثل سرعة الحركة movement speed وتسارع السقوط fall acceleration الذي يمثل الجاذبية، والسرعة التي سنستخدمها لتحريك الشخصية. 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() ليكون طول الشعاع ثابتًا ولا يتحرك بشكل أسرع قطريًا. وفيما يلي شيفرة تحريك أو تدوير الكائن في اتجاه حركة اللاعب بناءً على المدخلات. #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 بناء لعبة ورق في جافا
-
Amr El Ramsisy2 بدأ بمتابعة Naser Dakhel
-
سنتحدث في هذا الدرس عن الإشارات 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