البحث في الموقع
المحتوى عن '3d_game'.
-
في هذا الدرس من سلسلة تطوير لعبة ثلاثية الأبعاد في جودو، سنضيف للعبتنا نظام النقاط وتشغيل المؤثرات الصوتية وإمكانية اللعب مجددًا. سنتعلم كيفية تتبّع النتيجة الحالية بواسطة متغير وعرضها على الشاشة باستخدام واجهة مستخدم بسيطة عن طريق كتابة نصية. إضافة عقدة واجهة المستخدم سنضيف عقدة فرعية جديدة في المشهد الرئيسي من نوع 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 دليلك الشامل إلى برمجة الألعاب أشهر أنواع الألعاب الإلكترونية