البحث في الموقع
المحتوى عن 'دليل جودو'.
-
نتعرف في هذا المقال على طريقة بناء شريط صحة Health Bar يضم قلوبًا أو غيرها من اﻷيقونات، كما سنتعرف على طريقة عرض نسبة تضرر الشخصية في لعبة على شكل نص يطفو فوق الشخصية. ثلاث طرق لبناء شريط صحة يضم قلوبًا من الطرق الشائعة في إظهار صحة اللاعب عرض سلسلة من اﻷيقونات -غالبًا بشكل قلوب- يختفي بعضها عندما يتعرض اللاعب إلى ضرر. وسنناقش ثلاث طرق لعرض الأيقونات أطلقنا عليها تسميات بسيطة simple وفارغة empty وجزئية partial. تعرض الصورة السابقة ثلاث حالات ممكنة لعرض شريط الصحة: الطريقة البسيطة: تعرض القلوب ممتلئة بالكامل الطريقة الفارغة: تعرض قلوب فارغة وأخرى ممتلئة الطريقة الجزئية: تعرض القلوب نصف ممتلئة إعداد شريط اﻷيقونات نستخدم في هذا المثال صور قلوب أبعادها 53x45 حصلنا عليها من موقع Kenney.nl: Platformer Art Deluxe. ومن المفترض أن يكون وضع الشريط ضمن شاشة عرض معلومات المستخدم HUD أو واجهة المستخدم UI سهلًا، لذا من المنطقي أن نبني هذا الشريط ضمن مشهد مستقل. سنبدأ بعقدة من النوع Node2D كي نُبقى اﻷمور على نفس السوية، ونضبط قيمة الخاصية Sepration ضمن Constants في القسم Theme Overrides من الفاحص على القيمة 5. نضيف بعد ذلك عقدة ابن من النوع TextureRect ثم نسحب أيقونة القلب إلى الخاصية Texture ونضبط قيمة Strech Mode على Keep. نعيد تسمية العقدة لتكون 1 ثم باستخدام مفتاحي Ctrl+D ننسخ هذه العقدة بعدد القلوب التي نريد عرضها في الشريط 5 مثلًا. ستبدو لوحة العقد في محرك جودو كالتالي: إضافة السكريبت يغطي السكريبت التالي حالات الشريط الثلاث التي ذكرناها، حيث سنحمّل في البداية الخامات وهي هنا اﻷيقونات التي نحتاجها ونعرف اﻷشرطة الثلاث، وتجدر الملاحظة بأن الكود سيغطي جميع حالات الشريط الثلاثة، وقد نحتاج لاستخدام حالة واحدة فقط في اللعبة، عندها نزيل الكود المتعلق بالحالات الأخرى كما يلي: extends HBoxContainer enum modes {SIMPLE, EMPTY, PARTIAL} var heart_full = preload("res://assets/hud_heartFull.png") var heart_empty = preload("res://assets/hud_heartEmpty.png") var heart_half = preload("res://assets/hud_heartHalf.png") @export var mode : modes func update_health(value): match mode: MODES.simple: update_simple(value) MODES.empty: update_empty(value) MODES.partial: update_partial(value) يؤدي استدعاء الدالة ()update_health العائدة إلى الشريط عرض القيمة الممرة إليه وفقًا للنمط المختار. ملاحظة: لن نضيف آليات تحقق من حدود القيمة المدخلة كالتأكد مثلًا من أن الصحة بين 0 و 100، فهناك طرق كثيرة لعرض الصحة في اﻷلعاب لذا سنترك الأمر لكم. نتنقل في الدالة ()update_simple بين أشرطة الأيقونات ونضبط ظهور كل عقدة TextureRect: func update_simple(value): for i in get_child_count(): get_child(i).visible = value > i واﻷمر مشابه في الدالة ()update_empty ما عدا أننا نغير اﻷيقونة إلى اﻷيقونة الفارغة بدلًا من إخفائها: func update_empty(value): for i in get_child_count(): if value > i: get_child(i).texture = heart_full else: get_child(i).texture = heart_empty أما في الحالة اﻷخيرة، فلدينا أيقونة ثالثة وضعف القيم الممكنة فمن خلال إنقاص القيمة بمقدار 1 مثلًا يعطي نصف قلب وإنقاص 1 مرة أخرى تعطي قلبًا فارغًا: func update_partial(value): for i in get_child_count(): if value > i * 2 + 1: get_child(i).texture = heart_full elif value > i * 2: get_child(i).texture = heart_half else: get_child(i).texture = heart_empty توضح الصورة أدناه مثالًا عن عمل كل شريط: إنشاء نصوص طافية فوق الشخصية هناك طرق عدة لتحقيق النصوص الطافية floating text، منها استخدام خط كتابة نقطية bitmap font وبناء صورة لكل عدد انطلاقًا من اﻷرقام المكونة له، ومن ثم استخدام العقدة Sprite2D لعرض وتحريك النص الناتج. لكن ما سنفعله في مقالنا هو استخدام العقدة Label واسمها FCT وبهذا سنمتلك مرونة في تغيير الخط إضافة إلى سهولة عرض اﻷعداد كنصوص أو عرض نصوص أخرى مثل "أخفق miss". نضيف المورد الذي نريده في الخاصية Label Settings ونختار خطًا مناسبًا وقياسًا مناسبًا له، وقد استخدمنا في المثال الخط Xolonium.ttf والقياس 28 مع إطار خارجي أسود بعرض 4 بكسل. نضيف اﻵن السكريبت التالي إلى العقدة Label: extends Label func show_value(value, travel, duration, spread, crit=false): نستدعي عند توليد النصوص الطافية الدالة ()show_value التي تضبط قيم المعاملات التالية: value وهو العدد أو النص الذي نريد توليده travel وهو عقدة شعاع Vector2 التي تمثل اتجاه حركة النص أو العدد duration تحدد كم سيبقى النص على قيد الحياة spread يحدد أن الحركة ستكون عشوائية عبر هذا القوس crit يشير لأن الضرر كبير في حال كانت قيمته true وهذا ما تفعله الدالة ()show_value: text = value var movement = travel.rotated(rand_range(-spread/2, spread/2)) rect_pivot_offset = rect_size / 2 تضبط الدالة قيمة النص أو العدد ومن ثم تجعل حركته عشوائية وفقًا لقيمة الانتشار spread مابين 90+ و90- مثلًا. وقد نغير أبعاد النصوص المتحركة، لهذا ضبطنا قيمة الخاصية rect_pivot_offset لتمثل مركز عنصر التحكم وبالتالي يكون تغيير اﻷبعاد منسوبًا إلى المركز. $Tween.interpolate_property(self, "rect_position", rect_position, rect_position + movement, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) $Tween.interpolate_property(self, "modulate:a", 1.0, 0.0, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) نجري بعد ذلك استيفاء interpolation على قيمتي الخاصية بين لحظتين هما rect_position لتحريك العدد الطافي و modulate.a ﻹخفاء هذا النص بشكل تدريجي وسلس: if crit: modulate = Color(1, 0, 0) $Tween.interpolate_property(self, "rect_scale", rect_scale*2, rect_scale, 0.4, Tween.TRANS_BACK, Tween.EASE_IN) إن كانت اﻹصابة بالغة، سنغير لون النص ونزيد حجمه لإظهار التأثير. وتجدر الملاحظة بأننا حددنا لون النص في هذه الحالة ليكون أحمر ومن اﻷفضل أن تعرف متحولًا لإسناد قيمة اللون الذي نريده: $Tween.start() yield($Tween, "tween_all_completed") queue_free() نبدأ بعد ذلك عملية بناء اﻹطارات البينية من خلال التعليمة Tween وننتظر حتى تنتهي ثم نزيل العنوان Label. إدراة النصوص الطافية FCTManager ننشئ اﻵن عقدة صغيرة تدير توليد النصوص وتحديد مكانها، وستُلحق بكيانات اللعبة التي نريد أن نضيف إليها تأثير النصوص الطافية. نسمي هذه العقدة من النوع Node2D بالاسم FCTManager ونضيف إليها السكريبت التالي: extends Node2D var FCT = preload("res://FCT.tscn") export var travel = Vector2(0, -80) export var duration = 2 export var spread = PI/2 func show_value(value, crit=false): var fct = FCT.instance() add_child(fct) fct.show_value(str(value), travel, duration, spread, crit) يمكن تعديل ما نريده من خصائص العقدة من خلال نافذة الفاحص Inspector، لكن الدالة ()show_value أيضًا تولد النص الطافي وتضبط خصائصه. وبإمكاننا إلحاق نسخة من هذه العقدة بأي وحدة من وحدات اللعبة نريدها أن تمتلك تأثير النصوص الطافية، ثم نضيف كود مشابه لما يلي ضمن التابع ()take_damage للوحدة: $FCTManager.show_value(dmg, crit) تجدر الإشارة لأنه في الحالة التي تضم فيها لعبتنا عددًا كبيرًا من الوحدات، فقد يؤثر هذا اﻷمر على اﻷداء جراء توليد وتحرير النصوص الطافية باستمرار لعدد كبير من الوحدات. في حالات كهذه، ينصح بتوليد عدد محدد تمامًا من النصوص الطافية من خلال FCTManager ثم نظهرها ونخفيها بدلًا من توليدها وتحريرها في نهاية الحركة. الخاتمة تعرفنا في هذا المقال على طريقة بناء شريط صحة يضم أيقونات تعرض حالة اللاعب مثل تناقص صحته أو نفاذ ذخيرته، كما تحدثنا عن أحد طرق لتوليد نصوص تطفو حول الشخصية وتختفي للدلالة على حالة معينة مثل مقدار اﻹصابة التي تلقتها. ترجمة -وبتصرف- للمقالين: HeartContainers: 3 ways و Floating Combat Text اقرأ أيضًا المقال السابق: عرض عداد تنازلي Countdown وقائمة دائرية Radial Menu في جودو استخدام الإشارات Signals في جودو Godot الاستماع لمدخلات اللاعب في جودو Godot تعرف على واجهة محرك الألعاب جودو
-
سنتعرف في هذا المقال على طريقة بناء أزرار عد تنازلي countdown ضمن ألعاب جودو لتساعدنا في تحقيق ميزة الانتظار في اللعبة، مثلًا يمكن أن لا تتفعّل ميزة أو قدرة معينة للاعب ما إلا بعد مضي فترة زمنية معينة، كما سنشرح طريقة بناء قائمة دائرية الشكل Radial menu تعرض للاعب عدة خيارات موزعة على شكل حلقة لتسهيل الوصول لكل خيار وإضافة طابع فريد لواجهة اللعبة. بناء أزرار العد التنازلي قد نرغب في إضافة عدة أزرار تمنح اللاعب مهارات أو قدرات خاصة ability buttons مع توفير ميزة الانتظار لفترة معينة قبل تمكين اللاعب من النقر على كل زر منها واكتساب القدرة المطلوبة، يمكننا تحقيق ذلك من خلال ميزة العد التنازلي countdown، وفي حال احتجنا لأيقونات ورسومات مناسبة لاستخدامها مع هذه الأزرار فستجد كمًا كبيرًا من التصاميم المناسبة في موقع Game-icons.net وسنستخدم بعضها في مقالنا. إعداد مشهد اللعبة يضم المشهد الذي سنعمل عليه العقد nodes التالية: لنوضح بإيجاز دور كل عقدة منها: العقدة النوع الوظيفة AbilityButton TextureButton زر ينشط قدرة خاصة للاعب عند الضغط عليه Sweep TextureProgress شريط تقدم يظهر عد تنازلي بعد الضغط على الزر Timer Timer مؤقت يتحكم في فترة التهدئة Cooldown قبل التمكن من إعادة استخدام القدرة الخاصة Counter MarginContainer حاوية خاصة تتيح إضافة هوامش Margins بين عناصرها Value Label مكون تسمية توضيحية يعرض التوقيت سنحدد الأيقونة الخاصة بكل زر من خلال الخاصية Textures ثم Normal لزر القدرة AbilityButton حيث يمكننا من هنا تحديد الأيقونة الافتراضية للزر عندما لا يكون مضغوطًا، ثم نختار القيمة Full Rect في العقدة Sweep من القائمة Presets لتحديد تأثيرات التعبئة أو المسح التدريجي ليكون على كامل الزر. بعد ذلك، نضبط الخاصية FillMode للعقدة Counter بالقيمة clockwise. نريد تغيير إضاءة زر الانتظار بشكل تدريجي وفق زاوية قطرية. لتحقيق ذلك، نختار الخاصية Visibilty للزر ثم Modulate ونختار قيمتها لتكون بلون رمادي قاتم مع إضافة بعض الشفافية لجعل الزر يبدو باهتًا في وضع الانتظار. نضبط عقدة المؤقت Timer على القيمة One Shot لجعلها تعمل مرة واحدة فقط، وفيما يخص العقدة Counter وهي حاوية تحتوي النص وتحاذيه، ينبغي ضبط تخطيطها على Bottom Wide وضبط الخاصيتين Margin Right و Margin Left للمسافات الجانبية على القيمة 5 وذلك ضمن القسم Theme Overrides ثم Constants. بالنسبة للعقدة value سنضبط خاصية المحاذاة الأفقية Horizontal Alignment على القيمة Right، وخاصية اقتصاص النص Clip Text على القيمة on لتجنب تجاوز النص لحدود الحاوية. ونختار الخط المناسب من القسم Theme Overrides ثم Font ونضع قيمة 0.0 في الحقل النصي. وطالما أن اﻷيقونة التي نستخدمها سوداء، فمن الجيد ضبط قيمة خاصية حجم الحدود Theme Outline Size من القسم Overrides ثم Constants بالقيمة 1 لجعل الأيقونة أكثر وضوحًا. إضافة كود برمجي لزر العد التنازلي نضيف سكريبت إلى عقدة زر القدرة AbilityButton. ثم نربط إشارة timeout الخاصة بالمؤقت Timer وإشارة pressed الخاصة بزر القدرة. وبالتالي عند النقر على الزر، سيبدأ العد التنازلي وعندما ينتهي العد يمكننا تنفيذ إجراء معين. extends TextureButton class_name AbilityButton @onready var time_label = $Counter/Value @export var cooldown = 1.0 func _ready(): time_label.hide() $Sweep.value = 0 $Sweep.texture_progress = texture_normal $Timer.wait_time = cooldown set_process(false) يبدأ السكريبت بتصدير المتغير cooldown الذي يحدد طول فترة الانتظار قبل تفعيل الزر، ومن ثم نضبط المؤقت Timer داخل التابع ()ready_ لاستخدام هذه القيمة. سنحتاج بعد ذلك لخامة texture لنسندها إلى TextureProgress، سنستخدم نفس خامة الزر، ويمكن استخدام أي خامة أخرى نفضلها. أخيرًا، لنتأكد من أن العمليات الخاصة بالمتغير Sweep قد انتهت بشكل صحيح، سنتأكد إن كانت قيمة Sweep هي 0 ونضبط قيمة معالجة العقدة processing على false. وبما أننا ننفذ التحريك ضمن التابع ()process_ لذا لا نحتاج لتنفيذ هذا التابع إن لم نكن في فترة التهدئة CoolDown. func _process(delta): time_label.text = "%3.1f" % $Timer.time_left $Sweep.value = int(($Timer.time_left / cooldown) * 100) نلاحظ في الكود السابق أننا استخدمنا الخاصية time_left للمؤقت Timer لضبط الخاصية text للعقدة labe والخاصية value للعقدة Sweep. func _on_AbilityButton_pressed(): disabled = true set_process(true) $Timer.start() time_label.show() عندما يُنقر الزر سيبدأ كل شيء: func _on_Timer_timeout(): print("ability ready") $Sweep.value = 0 disabled = false time_label.hide() set_process(false) كما يعود كل شيء إلى وضعه عندما ينتهي المؤقت من العد. بإمكاننا وضع عدة أزرار ضمن عقدة حاوية من النوع HBoxContainer وسنحصل على شريط أفقي من أزرار القدرة كما يلي: بناء قائمة دائرية منبثقة تُستخدم القوائم في العديد من اﻷلعاب للوصول إلى ميزات أو وظائف معينة، كأن نحدد من خلالها المهمة المطلوب تنفيذها في اللعبة حاليًا مثل التحدث أو التفتيش أو الهجوم وهكذا. ينبغي أن يكون مظهر وسلوك القائمة متلائمًا مع لعبتنا، لكننا سنركز في هذا المثال على آلية بناء قوائم دائرية Radial Menu ونترك لك حرية تنسيقها. توضح الصورة التالية قائمة العقد المطلوبة لتنفيذ القائمة: نحتاج لاستخدام عقدة TextureButton من النوع RadialMenuButton لتكون عقدة جذر وهي تمثل الزر الرئيسي الذي سننقره لفتح أو إغلاق القائمة الدائرية، وعقدة Buttons من النوع control كحاوية تتضمن كافة الأزرار التي نريد عرضها في القائمة الدائرية، ونتأكد من ضبط قيمة الخاصية Mouse ثم Filter على القيمة Ignore كي لا تعترض أفعال النقر على الفأرة. كما سنستخدم تسعة أزرار لعرض القدرات الخاصة من نوع العداد التنازلي Cooldown. الخطوة التالية هي إضافة السكريبت التالي للعقدة الجذر: extends TextureButton class_name RadialMenuButton export var radius = 120 export var speed = 0.25 var num var active = false يمثل المتغير radius حجم القائمة وهو قطر الدائرة التي سنوزع عليها اﻷزرار، بينما يُستخدم المتغير speed في تحديد سرعة تحريك أزرار القائمة فالقيم اﻷصغر هي اﻷسرع. ويحدد المتغير num عدد اﻷزرار في القائمة، بينما يمثل المتغير active راية flag تدل على إغلاق أو فتح القائمة. func _ready(): $Buttons.hide() num = $Buttons.get_child_count() for b in $Buttons.get_children(): b.position = position نبدأ بإعداد منطق القائمة في التابع ()ready_ وذلك بإخفاء جميع أزرار القائمة افتراضيًا وضبط المسافة بينها وبين الزر الرئيسي للقائمة. ثم نربط اﻹشارة pressed للزر الرئيسي: func _on_pressed(): disabled = true if active: hide_menu() else: show_menu() سيخفي النقر على الزر القائمة أو يظهرها، ونحتاج أيضًا إلى تعطيل الزر أثناء عملية تحريك الرسومات، وإلا سيعيد النقر عليه توليد اﻹطارات البينية tween وإعادة التحريك من جديد: func _on_tween_finished(): disabled = false if not active: $Buttons.hide() عندما ينتهي تحريك اﻹطارات البينية، ننقل حالة الزر إلى تمكين مجددًا. لنلقِ نظرة على الدالة ()show_menu: func show_menu(): $Buttons.show() var spacing = TAU / num active = true var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): #لوضع الزر اﻷول في اﻷعلى PI/2 اطرح var a = spacing * b.get_position_in_parent() - PI / 2 var dest = Vector2(radius, 0).rotated(a) tw.tween_property(b, "position", dest, speed).from(Vector2.ZERO).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(b, "scale", Vector2.ONE, speed).from(Vector2(0.5, 0.5)).set_trans(Tween.TRANS_LINEAR) نحسب في هذه الدالة المسافة spacing أو بالأصح الزاوية التي نريدها بين كل عنصرين على القائمة، ومن ثم نتنقل بين اﻷزرار ونحدد وجهة كل زر dest وفقًا للزاوية المحسوبة وقيمة نصف القطر radius. ونولد لكل زر خاصيتين هما position و scale لإعطاء اﻷثر المرغوب عند توليد إطارات التحريك tween أثناء تحرك الزر. وتنفذ الدالة ()hide_menu العكس تمامًا: func hide_menu(): active = false var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): tw.tween_property(b, "position", Vector2.ZERO, speed).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN) tw.tween_property(b, "scale", Vector2(0.5, 0.5), speed).set_trans(Tween.TRANS_LINEAR) ستبدو القائمة كالتالي: الخاتمة شرحنا في هذا المقال كيفية إنشاء أزرار عد تنازلي في محرك الألعاب جودو، والتي تتيح لنا ضبط فترات انتظار قبل إعادة استخدام الأزرار، وهي ميزة ضرورية للألعاب التي تعتمد على منح قدرات أو ميزات خاصة بعد فترة انتظار وتحديد فترة انتظار بعد كل استخدام. كما تناولنا آلية بناء قوائم دائرية توفر تجربة تفاعلية سلسة لعرض الأزار من خلال توزيع الأزرار بشكل منظم حول نقطة مركزية لتسهيل الوصول للخيارات المختلفة داخل اللعبة. ترجمة -وبتصرف- للمقالين: CoolDown Button و Radial Popup Menu اقرأ أيضًا المقال السابق: التعامل مع إجراءات دخل الفأرة في جودو تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تعرف على أشهر محركات الألعاب Game Engines الطريقة الصحيحة للتواصل بين العقد في جودو
-
نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي InputEventMouse الذي يتضمن الخاصيتين position و global_position ويرث من هذا الصنف كل من الصنفين InputEventMouseButton و InputEventMouseMotion. ملاحظة: بإمكاننا تعيين أحداث النقر على أزرار الفأرة من خلال الصنف InputMap وهو صنف متفرد singleton وبالتالي سنتمكن من استخدامها مع الدالة ()is_action_pressed. استخدام الصنف InputEventMouseButton يضم الصنف GlobalScope.ButtonList@ قائمة بكل ثوابت اﻷزرار الممكنة *_BUTTON التي قد نحددها في الخاصية button_index. ولنتذكر أن عجلة التمرير في الفأرة scrollwheel تُعد زرًا -أو زرين إن أردنا توخي الدقة- لأن الحدثين BUTTON_WHEEL_UP و BUTTON_WHEEL_DOWN منفصلان. تلميح: يولد النقر على عجلة تمرير الفأرة الحدث pressed فقط، ولا يوجد حدث لتحرير الزر كما في اﻷزرار الأخرى. لاحظ الكود التالي الذي يعرف دالة لمعالجة إدخالات الفأرة، حيث يتحقق فيما إذا كان الحدث يخص زر الفأرة، ثم يحدد ما إذا كان الزر الأيسر قد جرى الضغط عليه أو تحريره، مع طباعة موقع النقر عند الضغط، كما يتعامل الكود أيضًا بالتعامل مع تمرير عجلة الفأرة للأسفل وطباعة رسالة مناسبة عند حدوث ذلك. func _unhandled_input(event): if event is InputEventMouseButton: if event.button_index == BUTTON_LEFT: if event.pressed: print("Left button was clicked at ", event.position) else: print("Left button was released") if event.button_index == BUTTON_WHEEL_DOWN: print("Wheel down") استخدام الصنف InputEventMouseMotion تقع أحداث هذا الصنف عندما يتحرك مؤشر الفأرة، وبإمكاننا إيجاد المسافة المقطوعة وفقًا ﻹحداثيات الشاشة باستخدام الخاصية relative. فيما يلي كود يوضح استخدام حركة الفأرة في تدوير شخصية ثلاثية الأبعاد حول المحور الأفقي، حيث تعتمد سرعة التدوير على حساسية الفأرة: # حساسية الفأرة التي تتحكم في سرعة التدوير عند تحريك الفأرة var mouse_sensitivity = 0.002 func _unhandled_input(event): if event is InputEventMouseMotion: rotate_y(-event.relative.x * mouse_sensitivity) الاحتفاظ بمؤشر الفأرة ضمن نافذة اللعبة بإمكاننا إخفاء مؤشر الفأرة ومنعها من مغادرة نافذة اللعبة، وهذا سلوك شائع في اﻷلعاب ثلاثية الأبعاد وحتى بعض اﻷلعاب ثنائية البعد. وللفأرة أربعة أنماط يمكنك اختيار أي منها باستخدام Input.mouse_mode: MOUSE_MODE_VISIBLE: المؤشر مرئي ويمكن تحريكه بحرية داخل وخارج نافذة اللعبة MOUSE_MODE_HIDDEN:المؤشر مخفي ويمكن له مغادرة نافذة اللعبة MOUSE_MODE_CAPTURED:المؤشر مخفي ولا يمكن له مغادرة نافذة اللعبة MOUSE_MODE_CONFINED:مؤشر الفأرة مرئي ولا يمكن له مغادرة نافذة اللعبة الخيار الثالث Captured هو الخيار اﻷكثر شيوعًا، ويمكننا أيضًا ضبط حالة مؤشر الفأرة أثناء التنفيذ: func _ready(): Input.mouse_mode = Input.MOUSE_MODE_CAPTURED وتمرر أحداث الفأرة بالشكل الطبيعي عند الاحتفاظ بها، لكننا سنواجه بعض المشكلات، فلن نستطيع إغلاق اللعبة أو الانتقال لنافذة أخرى. لهذا من اﻷفضل وجود آلية لتحرير مؤشر الفأرة كأن نحرره عندما يضغط اللاعب على الزر Escape: func _input(event): if event.is_action_pressed("ui_cancel"): Input.mouse_mode = Input.MOUSE_MODE_VISIBLE وهكذا لن تستجيب العبة لحركة الفأرة عندما نكون في نافذة أخرى، ونستطيع التحقق من حالة الاحتفاظ بمؤشر الفأرة في عنصر التحكم بالشخصية من خلال العبارة: if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: وبمجرد أن يتحرر مؤشر الفأرة، سنضطر إلى إعادة الاحتفاظ بها لمتابعة اللعبة. ولنفترض أن لدينا حدثًا ضمن خريطة اﻹدخال يتطلب النقر على الفأرة، عندها، يمكن حل المشكلة كالتالي: if event.is_action_pressed("click"): if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED وطالما أننا قد نستخدم حدث النقر على الفأرة لإطلاق النار أو لتنفيذ إجراء ما، من الجيد إذًا الامتناع عن متابعة تنفيذ الحدث عند الانتهاء من اﻹجراء، وذلك بإضافة ما يلي بعد ضبط حالة مؤشر الفأرة: get_tree().set_input_as_handled() سحب وتحديد عدة عناصر باستخدام الفأرة قد نضطر في اﻷلعاب الاستراتيجية المباشرة لاختيار عدة عناصر أو وحدات لعب دفعة واحدة وإعطائها أوامر معينة، وستكون العملية عادة من خلال رسم صندوق مربع حول هذه العناصر مما يسبب اختيارها، وعندها يمكننا النقر على مكان ما على الخريطة مثلًا لنأمر هذه العناصر بالتحرك إلى هذا المكان. كما في المثال الظاهر في الصورة التالية: إعداد عناصر اللعب لاختبار اﻷمر، نحتاج إلى عدة عناصر تتحرك باتجاه محدد في اللعبة دون أن تتحرك نحو بعضها. إن أردنا أساسًا لبناء هذه الوحدات، يمكن العودة إلى المثال المكتمل ثم إزالة التعليقات عن أسطره لأننا لن نخوض في تفاصيل إنشاء مثل هذه العناصر في المقال. إعداد عالم اللعبة سنعالج عملية اختيار العناصر في عالم اللعبة، لهذا سننشئ هذا العالم من خلال اختيار عقدة Node2D وتسميتها World ثم إضافة نسخ من العناصر ضمنها. نضيف بعد ذلك سكريبتًا إلى العقدة World ثم نضيف المتغيرات التالية: extends Node2D var dragging = false # هل نسحب الفأرة حاليًا؟ var selected = [] # مصفوفة العناصر المسحوبة var drag_start = Vector2.ZERO # موقع بداية السحب var select_rect = RectangleShape2D.new() # شكل التصادم لصندوق السحب نلاحظ أننا سنحتاج إلى طريقة لتحديد العناصر داخل صندوق السحب بمجرد رسمه. لهذا نستعمل العقدة Rectangle Shape2D التي تستعلم من محرك الفيزياء وتعرف العناصر التي اصطدم بها الصندوق. رسم الصندوق نستخدم زر الفأرة اﻷيسر في هذه الطريقة، إذ تبدأ عملية النقر برسم مربع السحب وتنهي عملية تحرير زر الفأرة الرسم، وبهذا يُرسم الصندوق أثناء سحب الفأرة: func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب if selected.size() == 0: dragging = true drag_start = event.position # إن حُرر زر الفأرة ونحن في حالة سحب نوقف السحب elif dragging: dragging = false queue_redraw() if event is InputEventMouseMotion and dragging: queue_redraw() func _draw(): if dragging: draw_rect(Rect2(drag_start, get_global_mouse_position() - drag_start), Color.YELLOW, false, 2.0) اختيار العناصر بعد أن رسمنا صندوق الاختيار علينا إيجاد العناصر التي تقع ضمنه. فعندما نحرر زر الفأرة وتنتهي عملية السحب، لا بد من الاستعلام من الفضاء الفيزيائي المحيط عن عناصره التي اصطدمت بالصندوق. ولنتذكر أن العناصر هي عقد من النوع CharacterBody2D لكن ستتأثر أيضًا العقد من النوع Area2D وغيرها من اﻷجسام. نستخدم التابع ()PhysicsDirectSpaceState2D.intersect_shape ﻹيجاد العناصر، ويتطلب التابع رسم شكل مستطيل في حالتنا وإجراء تحويل transform للموقع: elif dragging: dragging = false queue_redraw() var drag_end = event.position select_rect.extents = abs(drag_end - drag_start) / 2 نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، إذ نستخدمه في تحديد خاصية الامتداد extents للكائن RectangleShape2D (تقاس extents من مركز المستطيل فهي تمثل نصف الارتفاع أو الاتساع) نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، وهو موقع نهاية السحب، حيث نستخدم هذا الموقع لحساب خاصية الامتداد extents للكائن RectangleShape2D، حيث تشير الخاصية extents تشير إلى نصف أبعاد الشكل، وتُقاس من مركز الشكل وهو في حالتنا شكل مستطيل. ننشئ في الكود التالي مربعًا يمثل المساحة التي جرى تحديدها عن طريق سحب الفأرة، ثم نبحث عن العناصر التي تتداخل مع هذا المستطيل في الفضاء الفيزيائي، ونحدد مكان المستطيل باستخدام موقع نقطة السحب بداية ونهاية الفأرة. # مساحة عالم اللعبة var space = get_world_2d().direct_space_state # استعلام بحث عن التصادم var query = PhysicsShapeQueryParameters2D.new() # تحديد الشكل الذي سنبحث عنه query.shape = select_rect # 2 تحديد العناصر في طبقة التصادم query.collision_mask = 2 # ضبط موقع الشكل query.transform = Transform2D(0, (drag_end + drag_start) / 2) # البحث عن التصادمات selected = space.intersect_shape(query) سنستخدم الآن محرك الفيزياء في جودو للاستعلام عن التصادمات بين الشكل المستطيل الذي حددناه وبين العناصر الفيزيائية الأخرى في اللعبة. لذا نعيد مرجعًا إلى محرك الفيزياء، ونهيئ استعلام الشكل باستخدام العقدة PhysicsShapeQueryParameters2D بعد إسناد شكلنا إليها، ونستخدم مركز المساحة التي تكونت نتيجة السحب كمبدأ للتحويل. ستكون النتيجة عند استدعاء التابع ()intersect_shape مصفوفة تتضمن معلومات عن الأجسام المتصادمة مع الشكل المستطيل، وتبدو كالتالي: [{ "rid": RID(4093103833089), "collider_id": 32145147326, "collider": Unit2:<CharacterBody2D#32145147326>, "shape": 0 }, { "rid": RID(4123168604162), "collider_id": 32229033411, "collider": Unit3:<CharacterBody2D#32229033411>, "shape": 0 }] يدل كل متصادم collider في هذه المصفوفة إلى عنصر، لهذا يمكن استخدامه لتمييز العناصر التي اختيرت وتفعيل طار ملون يحيط بهذه العناصر: for item in selected: item.collider.selected = true إرسال اﻷوامر إلى العناصر بإمكاننا اﻵن إرسال أمر التحرك للعناصر لتتجه نحو مكان ما على الشاشة: func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب if selected.size() == 0: dragging = true drag_start = event.position # وإلا ستخبر النقرة جميع العناصر المختارة بالتحرك else: for item in selected: item.collider.target = event.position item.collider.selected = false selected = [] تُنفذ شيفرة العبارة else عندما ننقر الفأرة ونختار عنصر أو أكثر. كما يضبط هدف target كل عنصر، وعلينا التأكد من إلغاء اختيار العنصر عندما يصل إلى وجهته لنتمكن من إعادة عملية اختياره عند الحاجة. الخاتمة تعلمنا في هذا المقال طريقة التعامل مع مدخلات المستخدم عن طريق الفأرة من خلال تحديد موقع مؤشر الفارة والاحتفاظ به ضمن نافذة اللعبة وكيفية استخدام الفأرة لتحديد عدة عناصر. ترجمة -وبتصرف- للمقالات: Mouse Input و Capturing the mouse و Mouse:Drag-Select multiple units اقرأ أيضًا المقال السابق: العمل مع إجراءات الدخل Inputs Actions في جودو حفظ واسترجاع البيانات المحلية بين جلسات اللعب تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
-
نشرح في هذا المقال كيفية استخدام إجراءات الدخل Input Actions في محرك الألعاب جودو، والتي توفر لنا طريقة فعالة للتحكم في الشخصيات والعناصر داخل اللعبة. فبدلاً من تحديد كل مفتاح كتابيًا في الشيفرة البرمجية، يمكننا استخدام إجراءات الدخل لتهيئة المدخلات وتخصيص مفاتيح التحكم باللاعب بطريقة منظمة وسهلة التعديل. لنفترض أن لدينا شخصية تتحرك في لعبتنا من اﻷعلى إلى اﻷسفل وأردنا كتابة شيفرة باستخدام العقدة InputActionKey التي تتيح لنا بالتحكم بالشخصية عبر مفاتيح اﻷسهم في لوحة المفاتيح، لكن سنجد أن الكثير من اللاعبين يفضلون استخادم المفاتيح W و A و S و D للتحكم بالشخصية، قد نحاول العودة إلى اللعبة وإضافة مفاتيح إضافية لكن النتيجة ستكون شيفرة مضاعفة وزائدة. هنا تساعدنا إجراءات الدخل Input Actionsعلى تهيئة الشيفرة البرمجية للشخصية بشكل أفضل بدلًا من تحديد كل مفتاح كتابيًا في الشيفرة، وسنكون قادرين على تعديل وتخصيص المفاتيح المطلوبة دون تغيير كامل الشيفرة. إنشاء المدخلات بإمكاننا تعريف إجراءات دخل وتعيينها من داخل محرر جودو من خلال الانتقال إلى المشروع Project ثم إعدادات المشروع Project Settings ثم الانتقال لتبويب خريطة اﻹدخال Input Map. سنجد عند النقر على هذا التبويب بعض إجراءات الدخل المهيأة افتراضيًا تُعرف جميعها بالاسم *_ui كي نعلم بوجود إجراءات إدخال افتراضية. ملاحظة: نحتاج لتفعيل مفتاح التبديل أظهر الإجراءات المدمجة Show default Actions من أجل عرض إجراءات الدخل. لا بد عمومًا من إنشاء أحداثنا الخاصة بدلًا من الاعتماد على اﻷحداث الموجودة، لهذا، سنفترض أننا نريد السماح للاعب بالتحرك والدوران ضمن اللعبة عبر لوحة المفاتيح والفأرة، ونحتاج لتمكين اللاعب من التصويب من خلال الضغط على زر الفأرة اﻷيسر أو من خلال الضغط على مفتاح المسافة Spacebar. سننشئ إجراءً جديدًا يُدعى shoot بكتابة اسم اﻹجراء ضمن الحقل إضافة إجراء جديد Add New Action ثم ننقر على زر أضف Add أو الضغط على مفتاح Enter، وسنرى أن الإجراء أضيف إلى القائمة الموجودة. سنتمكن اﻵن من تعيين مدخلات لهذا اﻹجراء بالنقر على الزر + إلى اليمين. قد تكون المدخلات من لوحة المفاتيح أو أزرار الفأرة أو عصا التحكم. وقد اخترنا في حالتنا لوحة المفاتيح Key ثم نقرنا ضمن حقل الاستماع إلى المدخلات listen to inputs ونقرنا بعدها على مفتاح المسافة Spacebar لتعيينه كمفتاح لحدث الدخل، ثم نقرنا على زر حسنًا OK للموافقة على الإضافة كما في الصورة التالية: بنفس الطريقة سندخل إجراء آخر ونختار أزرار الفأرة Mouse Button ونتأكد من وجودنا ضمن حقل الاستماع إلى المدخلات listen to inputs ثم ننقر زر الفأرة اﻷيسر لتعيينه. استخدام إجراءات الدخل بإمكاننا التحقق من اﻹجراء باستدعاء الصنف Input في كل إطار: func _process(delta): if Input.is_action_pressed("shoot"): #ستُنقذ هذه الشيفرة في كل إطار طالما أن زر اﻹدخال مضغوط ولهذا اﻷمر أهميته للإجراءات المستمرة التي نريد التحقق منها باستمرار مثل حركة اللاعب. لكن إن أردنا التقاط اﻹجراء لحظة وقوعه، نستطيع استخدام دالة رد النداء ()input_ أو ()unhandled_input_: func _unhandled_input(event): if event.is_action_pressed("shoot"): # ستعمل الشيفرة في اﻹطار بمجرد الضغط على عنصر اﻹدخال بإمكاننا استخدام دوال متعددة للتحقق من حالة اﻹدخال: is_action_pressed: تعيد القيمة true إن كانت حالة اﻹجراء حاليًا pressed is_action_released: تعيد القيمة true إن لم تكن حالة اﻹجراء حاليًا pressed. is_action_just_pressed و is_action_just_released: تعيدان true فقط ضمن إطار واحد عند وقوع الحدث. وهي مفيدة في اﻹجراءات غير المستمرة التي لابد فيها من تحرير الزر ثم ضغطه لتكرار اﻹجراء إضافة إجراءات دخل إلى الشيفرة مباشرة قد نرغب بإضافة إجراءات إدخال لخريطة اﻹدخال أثناء تنفيذ اللعبة أي نريد إضافة إجراء دخل أو أكثر مباشرة إلى السكريبت. سنجد الحل في الصنف InputMap الذي يقدم مجموعة من التوابع لتساعدنا في ذلك. فيما يلي مثال يضيف إجراءً جديدًا باسم attack عند الضغط على مفتاح المسافة Spacebar: func _ready(): InputMap.add_action("attack") var ev = InputEventKey.new() ev.keycode = KEY_SPACE InputMap.action_add_event("attack", ev) وإن أردنا أيضًا إضافة النقر على الزر اﻷيسر للفأرة إلى اﻹجراءات: ev = InputEventMouseButton.new() ev.button_index = MOUSE_BUTTON_LEFT InputMap.action_add_event("attack", ev) ملاحظة: سيرمي التابع ()InputMap.add_action خطأ إن كان اﻹجراء موجودًا مسبقًا، لهذا علينا التحقق من وجوده من خلال التابع ()InputMap.has_action قبل محاولة إضافته. مثال تطبيقي لنفترض أننا أنجزنا شخصية رئيسية في لعبة ونريد إعادة استخدامها في مشروع آخر. في حال قمنا بتخزين المشهد والسكريبت واﻷصول في مجلد واحد فلن نحتاج سوى إلى نسخ هذا المجلد إلى مشروعنا الجديد، مع تعديل خريطة اﻹدخال كي تعمل اﻹجراءات وفق المطلوب. بدلًا من ذلك، نستطيع إضافة الشيفرة التالية إلى سكريبت اللاعب ونتأكد أن المدخلات اللازمة ستُضاف تلقائيًا: var controls = {"walk_right": [KEY_RIGHT, KEY_D], "walk_left": [KEY_LEFT, KEY_A], "jump": [KEY_UP, KEY_W, KEY_SPACE]} func _ready(): add_inputs() func add_inputs(): var ev for action in controls: if not InputMap.has_action(action): InputMap.add_action(action) for key in controls[action]: ev = InputEventKey.new() ev.keycode = key InputMap.action_add_event(action, ev) الخاتمة نأمل أن يكون هذا المقال وضح لكم كيفية استخدام إجراءات الدخل في تطوير الألعاب بجودو وإنشاء إجراءات مخصصة وإضافتها إلى الشيفرة بطريقة ديناميكية، فسواء كنت تطور لعبة بسيطة أو مشروعًا معقدًا، فستحتاج لنظام إجراءات الدخل للتحكم بشخصيات لعبتك بكفاءة وسلاسة. ترجمة -وبتصرف- للمقالين: Input Actions و Adding Input Action in Code اقرأ أيضًا المقال السابق: دليل جودو تنظيم الحركات في جودو باستخدام SpriteSheet و AnimationTree StateMachine تعرف على مفهوم Delta في تطوير الألعاب تعرف على واجهة محرك الألعاب جودو تعرف على أشهر محركات الألعاب Game Engines
-
نشرح في هذا المقال كيفية استخدام جدول SpriteSheet لتنظيم حركة الشخصية في الألعاب الثنائية الأبعاد ضمن محرك الألعاب جودو، كما نوضح دور المتحكم AnimationTreeState Machine في تنظيم حركة الشخصية والتحكم في عمليات التنقل بين حالات الحركة المتختلفة. الحركات داخل جدول الشخصيات SpriteSheet تُعد جداول الشخصيات Spritesheets من الطرق الشائعة لتوزيع الحركات للرسوم المتحركة ثنائية البعد، إذ توضع جميع إطارات الشخصية ضمن صورة واحدة. سنستخدم في مقالنا شخصية المغامر، ويمكن الحصول عليها وعلى غيرها من الشخصيات من متجر Elthen's Pixel Art Shop الذي يوفر ملحقات مفيدة يمكن استخدامها في تطوير الألعاب. تحذير: علينا التأكد من أن الصور في جدول الشخصيات مرتبة ضمن شبكة ذات حجم ثابت، مما يسمح لمحرك جودو باقتطاعها تلقائيًا، بينما إن وضعنا الشخصيات ضمن الجدول بشكل غير منتظم، فلن نتمكن من استخدام التقنيات التي نشرحها تاليًا. إعداد عقد التحريك تستخدم تقنية التحريك العقدة Sprite2D لعرض الخامة، بعدها نحرّك الإطارات المتغيرة باستخدام العقدة AnimationPlayer. يمكن لهذا الترتيب أن يعمل على أي عقدة ثنائية البعد، لكننا سنستخدم في مثالنا العقدة CharacterBody2D. لنضف العقد التالية إلى المشهد: CharacterBody2D: Player Sprite2D CollisionShape2D AnimationPlayer نسند جدول الشخصيات إلى الخاصية Texture للعقدة Sprite2D وسنلاحظ أن الجدول بكامله قد ظهر في نافذة العرض. ولكي نقطعه إلى إطارات فردية، نوسّع قسم التحريك Animation في نافذة الفاحص وناضبط قيم الخاصيتين Hframes و Vframes على 13 و 8 على الترتيب، وهما يمثلان عدد الإطارات الأفقية والعمودية في جدول الشخصيات: لنجرّب تغيير الخاصية Frame لمراقبة تغيّر الصورة، فهي الخاصية التي سنعمل على تحريكها. تحريك الشخصية سنختار العقدة AnimationPlayer ثم ننقر الزر تحريك Animation يتبعه الزر جديد New ونسمي الحركة الجديدة idle. نضبط بعد ذلك مدة الحركة على 2 ثانية وننقر الزر Loop كي تتكرر الحركة باستمرار. نجعل شريط التقدم Scrubber عند الزمن 0 ثم نختار العقدة Sprite2D. نضبط الخاصية Animation>Frame على 0 ثم ننقر على أيقونة المفتاح إلى جوار القيمة. إن حاولنا الآن تشغيل الحركة فلن نرى شيئًا، لأن الإطار الأخير رقم 12 يبدو مشابهًا للإطار الأول رقم 0. مع ذلك لم نتمكن من رؤية الإطارات بينهما. لإصلاح الأمر نغير الخاصية Update Mode للمسار من القيمة الافتراضية Discrete إلى Continuous وسنجد هذا الزر في نهاية المسار من الجانب الأيمن. نلاحظ أن هذا الحل سيعمل فقط مع جداول الشخصيات، حيث تكون الشخصيات مرتبة مسبقًا، فإن لم يكن الأمر كذلك، علينا ترتيب كل إطار على حدى ضمن المسار. يمكن تجربة وضع حركات أخرى مثل حركة القفز التي نجد صورها في الإطارات من 65 إلى 70. استخدام المتحكم AnimationTreeStateMachine لنتخيل أن لدينا كم كبير من الحركات، وأصبح من الصعب علينا التحكم عملية التنقل فيما بينها، وامتلأ السكريبت بعبارات if وكلما أردنا تصحيح شيء أخفق ما تبقى. لحل الأمر نستخدم العقدة AnimationTree لإنشاء مُتحكّم يسمح لنا بترتيب الحركات المختلفة للشخصية وإدارة عملية التنقل فيما بينها. سنستخدم في مثالنا نفس شخصية المغامر التي استخدمناها في المثال السابق، ونفترض أننا هيأنا مسبقًا حركات الشخصية باستخدام العقدة AnimationPlayer. وعندما نستخدم جدول الشخصيات السابق سنجد صورًا توافق الحركات التالية: سكون idle ركض run هجوم attack1 هجوم attack2 إصابة hurt موت die استخدام شجرة الرسوميات AnimationTree نضيف العقدة AnimationTree إلى المشهد ثم نختار New AnimationNodeStateMachine من الخاصية TreeRoot. تتحكم العقدة AnimationTree بالرسوميات التي تنشأ ضمن العقدة AnimationPlayer، ولكي نسمح لها بالوصول إلى الرسوميات الموجودة، ننقر على الخاصية Assign ضمن الخاصية Anim Player ثم نختار عقدة الحركة. يمكننا الآن إعداد متحكم التنقل ضمن نافذة AnimationTree: ننتبه إلى التحذير الظاهر، ونضبط الخاصية Active في نافذة الفاحص على القيمة On ثم ننقر بعد ذلك بالزر اليميني للفأرة ونختار Add Animation. نختار بعد ذلك الحركة idle وسنرى صندوقًا صغيرًا يمثل هذه الحركة. نكرر نفس العملية لإضافة مثل هذه الصناديق إلى بقية الحركات. سنتمكن الآن من إضافة الاتصالات، لهذا ننقر على زر Connect nodes ثم نتنقل بالسحب بن العقد لوصلها مع بعضها. وكمثال على الاتصال سنستخدم الرسوم المتحركة لحالتي الهجوم: عندما تختار حركة، ستتبع الشجرة المسار الذي يصل العقدة الحالية إلى الوجهة. لكن في طريقة إعداد المثال السابق، لن نرى الهجوم الأول attack1 إن شغلنا الهجوم الثاني attack2. يعود السبب في ذلك إلى أن نمط التبديل switch mode للاتصال نوعه مباشر immediate. لهذا، ننقر على زر Move/select ثم ننقر على الاتصال بين attack1 و attack2 ثم نغير من نافذة الفاحص الخاصية Switch Mode إلى At End ونكرر ذلك على الاتصال بين attack2 و idle. ما يحدث الآن أنه عند تشغيل في AnimationTree، أنه عند الانتقال من idle إلى attack2، يجري تشغيل الحركتين attack1 و attack2 على التتابع، ولكن بعد ذلك تتوقف الرسوم المتحركة عند attack2 بدلاً من العودة تلقائيًا إلى idle. لحل هذه المشكلة، نضبط الخاصية Advance>Mode على Auto مما يسمح للشجرة بالعودة إلى الحركة idle بشكل تلقائي بعد تنفيذ حركتي الهجوم attack، ونلاحظ أن أيقونة الاتصال تتحول إلى اللون الأخضر لإظهار ذلك. وهكذا ستُتنفذ الحركات على التتابع بمجرد تفعيلها. استدعاء الحالات في الشيفرة فيما يلي شجرة الحركات بأكملها: لنهيئ الآن الشخصية كي تستخدم هذه الحركات: extends CharacterBody2D var state_machine var run_speed = 80.0 var attacks = ["attack1", "attack2"] @onready var state_machine = $AnimationTree["parameters/playback"] تضم الخاصية state_machine مرجعًا إلى المتحكم بالحالة وهو AnimationNodeStateMachinePlayback، ولاستدعاء حركة محددة، نستخدم التابع travel الذي سيتّبع الاتصالات إلى الرسم المتحرك المحدد: func hurt(): state_machine.travel("hurt") func die(): state_machine.travel("die") set_physics_process(false) لدينا هنا مثال عن الدوال التي قد نستدعيها، إن أصيب اللاعب أو قتل. وبالنسبة إلى بقية الحالات كالركض والهجوم وغيرها فلا بد من جمعها مع شيفرة الحركة وشيفرة معالجة المدخلات. وستحدد الخاصية velocity إن كنا سنرى حالة حركة الركض run أو حركة السكون idle: func get_input(): var current = state_machine.get_current_node() velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * run_speed if Input.is_action_just_pressed("attack"): state_machine.travel(attacks.pick_random()) return # اقلب الشخصية من اليمين إلى اليسار if velocity.x != 0: $Sprite2D.scale.x = sign(velocity.x) # اختر رسمًا متحركًا if velocity.length() > 0: state_machine.travel("run") else: state_machine.travel("idle") move_and_slide() نلاحظ استخدام return بعد الانتقال إلى حركة الهجوم كي نتمكن من الانتقال إلى حالات الحركة أو السكون لاحقًا في الدالة. الخاتمة تعرفنا في هذا المقال على طريقة استخدام SpriteSheet في جودو لتوليد حركات مختلفة للشخصية، كما تعرفنا على استخدام AnimationTree Animation Tree State Machine في إدارة التنقل بين الرسوميات المختلفة للشخصية. وبإمكانك من الاطلاع على المشروع بصيغته المكتملة لتنفيذه وفهمه بصورة أفضل. ترجمة -وبتصرف- للمقالين: SpriteSheet ANimation و Using AnimationTreeStateMachine اقرأ أيضًا الطريقة الصحيحة للتواصل بين العقد في جودو Godot إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
-
سنتعلم في مقال اليوم كيفية حفظ البيانات المحلية بين جلسات اللعب وتحميل هذه البيانات عند الحاجة لها، هذا الموضوع مهم بشكل خاص عندما نريد الاحتفاظ بتقدم اللاعب أو إعدادات اللعبة عبر عدة جلسات لعب، حتى بعد إغلاق اللعبة وإعادة فتحها. كيف نحفظ البيانات المحلية في جودو يعتمد نظام إدخال وإخراج الملفات الخاص بمحرك الألعاب جودو Godot على كائن يسمى FileAccess ويمكنا فتحه من خلال استدعاء التابع open() كما يلي: var file = FileAccess.open("user://myfile.name", File.READ) ملاحظة: يجب تخزين بيانات المستخدم فقط في المسار user://، ويمكننا استخدام المسار res:// عند التشغيل من المحرّر، ولكن يصبح هذا المسار للقراءة فقط عند تصدير مشروعنا. الوسيط الثاني الموجود بعد مسار الملف هو راية الوضع Mode Flag ويمكن أن يكون لها أحد الخيارات التالية: FileAccess.READ: مفتوح للقراءة FileAccess.WRITE: مفتوح للكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه Truncate إذا كان موجودًا مسبقًا FileAccess.READ_WRITE: مفتوح للقراءة والكتابة، ولا يقتطع الملف FileAccess.WRITE_READ: مفتوح للقراءة أو الكتابة، وينشئ الملف إن لم يكن موجودًا مسبقًا ويقتطعه إذا كان موجودًا مسبقًا تخزين البيانات يمكننا حفظ البيانات باستخدام نوع البيانات المحدّد مثل store_float() و store_string() وغير ذلك، أو باستخدام الدالة store_var() المعمَّمة، والتي ستستخدم التسلسل Serialization المُدمَج في جودو لتشفير بياناتك بما في ذلك البيانات المعقدة مثل الكائنات التي سنتحدث عنها لاحقًا. لنبدأ بمثال بسيط لحفظ أعلى نتيجة للاعب، حيث يمكننا كتابة دالة يمكن استدعاؤها كلما احتجنا إلى حفظ النتيجة: var save_path = "user://score.save" func save_score(): var file = FileAccess.open(save_path, FileAccess.WRITE) file.store_var(highscore) نحفظ النتيجة، ولكن يجب تحميلها عند بدء اللعبة كما يلي: func load_score(): if FileAccess.file_exists(save_path): print("file found") var file = FileAccess.open(save_path, FileAccess.READ) highscore = file.get_var() else: print("file not found") highscore = 0 علينا أن لا ننسى التحقق من وجود الملف قبل محاولة القراءة منه، إذ قد لا يكون موجودًا، وإن كان غير موجود، فيمكننا استخدام قيمة افتراضية. كما يمكن استخدام الدالتين store_var() و get_var() عدة مرات حسب حاجتك مع أيّ عدد من القيم. حفظ الموارد تعمل الطريقة السابقة بنجاح عندما نريد أن نحفظ عددًا معينًا من القيم، ولكن يمكننا حفظ بياناتنا في مورد Resource في الحالات الأكثر تعقيدًا كما يفعل جودو الذي يحفظ جميع موارد البيانات الخاصة به على أنها ملفات .tres مثل Animations و TileSets و Shaders وما إلى ذلك حيث يمكننا تطبيق ذلك أيضًا. يمكن حفظ الموارد وتحميلها باستخدام صنفي جودو ResourceSaver و ResourceLoader. لنفترض مثلًا تخزين جميع بيانات الإحصائيات الخاصة بشخصية لعبتنا في مورد كما يلي: extends Resource class_name PlayerData var level = 1 var experience = 100 var strength = 5 var intelligence = 3 var charisma = 2 يمكننا بعد ذلك الحفظ والتحميل كما يلي: func load_character_data(): if ResourceLoader.exists(save_path): return load(save_path) return null func save_character_data(data): ResourceSaver.save(data, save_path) قد تحتوي الموارد على موارد فرعية، لذا يمكن أيضًا تضمين موارد مخزن اللاعب وغير ذلك. هل يمكن تخزين البيانات في ملف JSON قد يخطر في البال سؤال عن إمكانية استخدام صيغة JSON لحفظ البيانات، ولكن يُوصَى بعدم استخدام JSON مع ملفات الحفظ الخاصة بنا. يدعم جودو صيغة JSON، ولكن لا يُعَد حفظ بيانات اللعبة هدف استخدام JSON التي هي صيغة لتبادل البيانات، والغرض منها هو السماح للأنظمة التي تستخدم صيغ بيانات ولغات مختلفة بتبادل البيانات، لذا ستسبّب صيغة JSON قيودًا سلبية عندما يتعلق الأمر بحفظ بيانات اللعبة. إضافة لذلك لا تدعم JSON العديد من أنواع البيانات، إذ لا يوجد نوع البيانات int مقابل نوع البيانات float مثلًا، لذا يجب إجراء الكثير من عمليات التحويل والتحقق لمحاولة حفظ أو تحميل بياناتنا، ويُعَد ذلك أمرًا مرهقًا ويستغرق وقتًا طويلًا. لا ننصح بتضيع الوقت في محاولة كهذه، إذ يمكننا تخزين كائنات جودو الأصيلة مثل العقد والموارد والمشاهد دون أي جهد باستخدام التسلسل المُدمَج في جودو، مما يعني أننا ستستخدم شيفرة برمجية أقل مع وجود أخطاء أقل، ولا يستخدم جودو صيغة JSON لحفظ المشاهد والموارد. الخاتمة تعلمنا في مقال اليوم أساسيات حفظ واسترجاع البيانات المحلية بين جلسات اللعب، وتجدر الإشارة لأن هذا المقال لا يمثل سوى جزء بسيط ممّا يمكنك إنجازه باستخدام FileAccess، لذا ننصح بالاطلاع على توثيق FileAccess للحصول على القائمة الكاملة لتوابعه. ترجمة -وبتصرّف- للقسم Saving/loading data من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: تعرف على مفهوم Delta في تطوير الألعاب الطريقة الصحيحة للتواصل بين العقد في جودو Godot لغات البرمجة المتاحة في جودو Godot إنشاء شخصيات ثلاثية الأبعاد في جودو Godot
-
سنشرح في هذا المقال من سلسلة دليل جودو مفهوم Delta في مجال صناعة الألعاب، ونوضح كيفية استخدامه. يُعَد معامل دلتا delta أو "زمن دلتا" مفهومًا يُساء فهمه كثيرًا في تطوير الألعاب، لذا سنشرح في هذا المقال كيفية استخدامه وأهمية الحركة المستقلة عن معدل الإطارات وأمثلة عملية لاستخدامه في محرّك الألعاب جودو Godot. ليكن لدينا عقدة Sprite تتحرك عبر الشاشة. إذا كان عرض الشاشة 600 بكسل ونريد أن نعبر الشخصية الرسومية Sprite الشاشة خلال 5 ثوانٍ، فيمكننا استخدام العملية الحسابية التالية لإيجاد السرعة اللازمة لذلك: 600 pixels / 5 seconds = 120 pixels/second سنحرّك الشخصية الرسومية في كل إطار باستخدام الدالة _process()، بحيث إذا شُغِّلت اللعبة بمعدل 60 إطارًا في الثانية، فيمكننا إيجاد الحركة لكل إطار باستخدام العملية الحسابية التالية: 120 pixels/second * 1/60 second/frame = 2 pixels/frame ملاحظة: كما نلاحظ، أن وِحدات المقادير متناسقة في جميع العمليات الحسابية السابقة، لذا لا بد من الانتباه دائمًا إليها لتجنب الوقوع في الأخطاء. تكون الشيفرة البرمجية الضرورية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(2, 0) func _process(delta): $Sprite.position += movement نشغّل الشيفرة البرمجية السابقة وسنجد أن الصورة تعبر الشاشة خلال 5 ثوانٍ. تحصل المشكلة إذا كان هناك شيء آخر يشغَل وقت الحاسوب، والذي يسمى بالتأخير Lag، الذي يكون له عدة أسباب، حيث يمكن أن يكون السبب هو الشيفرة البرمجية التي نستخدمها أو حتى التطبيقات الأخرى التي تعمل على الحاسوب. وإذا حدث التأخير، فقد يؤدي ذلك إلى زيادة طول الإطار. إذا تخيلنا مثلًا أن معدل الإطارات انخفض إلى النصف بحيث يستغرق كل إطار 1/30 بدلًا من 1/60 من الثانية. فمعنى هذا أن الأمر سيستغرق ضعف الوقت حتى تصل الشخصية الرسومية إلى طرف الشاشة عند التحرك بمعدل 2 بكسل لكل إطار. ستؤدي حتى التقلبات الصغيرة في معدل الإطارات إلى سرعة حركة غير متناسقة، وإذا كانت هذه الشخصية الرسومية رصاصة أو جسمًا سريع الحركة، فلن نرغب في إبطائه بهذه الطريقة؛ إذ يجب أن تكون الحركة مستقلة عن معدل الإطارات. إصلاح مشكلة معدل الإطارات تتضمن الدالة _process() تلقائيًا عند استخدامها معاملًا بالاسم delta يمرّره المحرّك كما في الدالة _physics_process() التي تُستخدَم في الشيفرة البرمجية المتعلقة بالفيزياء. المعامل delta هو قيمة عشرية تمثل الوقت المستغرق منذ الإطار السابق، والذي سيكون 1/60 أو 0.0167 ثانية تقريبًا. يمكننا التوقف عن القلق بشأن مقدار تحريك كل إطار من خلال استخدام المعامل delta، إذ سنحتاج للاهتمام فقط بالسرعة المطلوبة بالبكسلات في الثانية، والتي هي 120 من العمليات الحسابية السابقة. سيعطينا ضرب قيمة delta الخاصة بالمحرك بهذا العدد عددَ البكسلات التي يجب تحريكها في كل إطار، وسيُعدَّل هذا العدد تلقائيًا عند تقلّب زمن الإطار. # 60 إطار في الثانية 120 pixels/second * 1/60 second/frame = 2 pixels/frame # 30 إطار في الثانية 120 pixels/second * 1/30 second/frame = 4 pixels/frame وكما نلاحظ، يبدو أنه إذا انخفض معدل الإطارات إلى النصف (أي تضاعف زمن الإطار)، فيجب أن تتضاعف أيضًا الحركة لكل إطار للحفاظ على السرعة المطلوبة، ولهذا سنعدّل الشيفرة البرمجية كما يلي: extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(120, 0) func _process(delta): $Sprite.position += movement * delta يكون زمن الانتقال متناسقًا عند التشغيل بمعدل 30 إطارًا في الثانية كما يلي: إذا أصبح معدل الإطارات منخفضًا جدًا، فلن تكون الحركة سلسةً بعد الآن، ولكن يبقى الزمن كما هو. استخدام معامل دلتا مع معادِلات الحركة إذا كانت الحركة التي نريد العمل عليها أكثر تعقيدًا، فسيبقى المفهوم كما هو مع إبقاء الوحدة بالثواني وليس بالإطارات، والضرب بمعامل delta لكل إطار. ملاحظة: يُعَد التعامل بالبكسلات والثواني أسهل بكثير لأنه يتعلق بكيفية قياس هذه الكميات في العالم الحقيقي، فمثلًا الجاذبية Gravity هي 100 بكسل/ثانية/ثانية، لذا ستتحرك الكرة بسرعة 200 بكسل/ثانية بعد سقوطها لمدة ثانيتين، وإذا استخدمنا وحدة الإطارات، فيجب استخدام التسارع Acceleration بوحدة البكسل/إطار/إطار، ولكننا سنجد أن هذه الوحدة غير مألوفة. إذا طبّقنا الجاذبية مثلًا، فإنها تمثّل التسارع، بحيث ستزيد السرعة بمقدارٍ معين في كل إطار، وستغير السرعة موضع العقدة كما هو الحال في المثال السابق؛ وهنا سنضبط قيم delta و target_fps في الشيفرة البرمجية التالية لمعرفة النتائج: extends Node2D # التسارع بالبكسل/ثانية/ثانية var gravity = Vector2(0, 120) # التسارع بالبكسل/إطار/إطار var gravity_frame = Vector2(0, .033) # السرعة بالبكسل/ثانية أو بالبكسل/إطار var velocity = Vector2.ZERO var use_delta = false var target_fps = 60 func _ready(): Engine.target_fps = target_fps func _process(delta): if use_delta: velocity += gravity * delta $Sprite.position += velocity * delta else: velocity += gravity_frame $Sprite.position += velocity وكما هو ظاهر، فقد ضربنا القيمة المحدثة في الخطوة الزمنية لكل إطار لتحديث السرعة velocity والموضع position، إذ يجب ضرب أي كمية مُحدَّثة في كل إطار بقيمة delta لضمان تغيرها بحيث تكون مستقلة عن معدل الإطارات. استخدام الدوال الحركية Kinematic استخدمنا Sprite للتبسيط في الأمثلة السابقة، مع تحديث الموضع position في كل إطار. إذا استخدمنا جسمًا حركيًا Kinematic ثنائي الأبعاد أو ثلاثي الأبعاد، فسنحتاج لاستخدام أحد توابع الحركة الخاصة به بدلًا من ذلك، خاصةً في حالة استخدام التابع move_and_slide()؛ إذ قد يحدث بعض الارتباك لأنه يستخدم متجه السرعة وليس الموضع، وهذا يعني أننا لن نضرب السرعة بقيمة delta لإيجاد المسافة، إذ تنجز الدالة ذلك نيابةً عنا، ولكن يجب تطبيقها على أيّ عمليات حسابية أخرى مثل التسارع كما في المثال التالي: # الشيفرة البرمجية لحركة الشخصية الرسومية: velocity += gravity * delta position += velocity * delta # الشيفرة البرمجية لحركة الجسم الحركي: velocity += gravity * delta move_and_slide() إن لم نستخدم قيمة delta عند تطبيق التسارع على السرعة، فسيكون التسارع عرضةً للتقلبات في معدل الإطارات، وقد يكون لذلك تأثير أكثر دقةً على الحركة؛ إذ سيكون غير متناسق مع وجود صعوبة في ملاحظته. ملاحظة: يجب أيضًا تطبيق قيمة delta على أي كميات أخرى مثل الجاذبية والاحتكاك وغير ذلك عند استخدام التابع move_and_slide(). ختامًا بهذا نكون قد تعرفنا على مفهوم delta في مجال تطوير الألعاب وكيفية استخدامه، وسنتعرف في المقال التالي على كيفية حفظ واسترجاع البيانات المحلية بين جلسات اللعب. ترجمة -وبتصرّف- للقسم Understanding delta من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: الطريقة الصحيحة للتواصل بين العقد في جودو Godot دليلك الشامل إلى برمجة الألعاب مطور الألعاب: من هو وما هي مهامه
-
بعد أن تعرفنا في المقال السابق من سلسلة دليل جودو على كيفية ترتيب معالجة العقد والتنقل في شجرة المشاهد في محرك الألعاب جودو Godot، سنتعرف في هذا المقال على الطريقة السليمة للتواصل بين العقد Nodes ونوضح المشاكل التي قد تحدث عند القيام بممارسات غير متوافقة مع الطريقة الصحيحة المنصوح بها. كما هو معروف، إذا أصبحت لدينا مشاهد ونسخ متعددة مع عدد كبير من العقد، فسيصبح مشروعنا معقدًا؛ وعندها قد نكتب شيفرة برمجية تشبه ما يلي: get_node("../../SomeNode/SomeOtherNode") get_parent().get_parent().get_node("SomeNode") get_tree().get_root().get_node("SomeNode/SomeOtherNode") وهنا إذا كتبنا الكود بهذا الشكل، فسنجد أن مثل مراجع References هذه العقد ستنكسر بكل سهولة، بحيث قد يتسبب إحداث تغيير بسيط في شجرة المشاهد في جعل مراجع العقد غير صالحة؛ ولهذا السبب، لا يجب أن يكون الاتصال بين العقد والمشاهد معقدًا. يجب أن تدير العقد أبناءها وليس العكس؛ حيث إذا استخدمنا الدالة get_parent() أو get_node("..")، فيُحتمَل أننا نتجه إلى مشكلة ما، إذ تكون مثل هذه المسارات للعقد سهلة الكسر. سنذكر ففيما يلي المشاكل الرئيسية الثلاث في هذا الترتيب: لا يمكن اختبار مشهد بطريقة مستقلة؛ فإذا شغّلنا المشهد بمفرده أو في مشهد اختبار لا يحتوي على إعداد العقدة نفسه، فستسبّب الدالة get_node() عطلًا لا يمكننا تغيير الأشياء بسهولة، لأننا إذا قرّرنا إعادة ترتيب أو تصميم الشجرة، فلن تكون المسارات صالحةً مجددًا يبدأ ترتيب الجاهزية من الأبناء أولًا حتى الوصول إلى الأب أخيرًا، وهذا يعني فشل محاولة الوصول إلى خاصية الأب في التابع _ready() الخاصة بالعقدة، لأن الأب غير جاهزٍ بعد ملاحظة: يُنصح بالاطلاع على المقال السابق للحصول على شرح لكيفية دخول العقد إلى الشجرة وكيف تصبح جاهزة. لا بد من توفر إمكانية إنشاء نسخة لعقدة أو مشهد في أيّ مكان في اللعبة التي ننشؤها، مع عدم افتراض أيّ شيء حول ما سيكون عليه الأب. سنستخدم أمثلة مفصلة لاحقًا، ولكن سنتحدث الآن عن القاعدة الذهبية لتواصل العقد، والتي هي: "استدعاء للأسفل، وإشارة Signal للأعلى". تشير هذه القاعدة إلى أنه في حال استدعَت العقدة ابنًا، بمعنى تنقلها إلى أسفل الشجرة، فسيكون استدعاء الدالة get_node() مناسبًا؛ وإذا كانت العقدة بحاجة إلى التواصل مع أعلى الشجرة، فيجب استخدام إشارة لذلك. يؤدي استخدام هذه القاعدة عند تصميم إعداد المشهد الخاص بنا إلى الحصول على مشروع قابل للصيانة ومنظم جيدًا، وسنتجنب بذلك استخدام مسارات العقد المعقدة التي تؤدي إلى مشاكل. لنلقِ الآن نظرةً على كلٍّ من هذه الاستراتيجيات مع بعض الأمثلة. استخدام الدالة get_node() يعبُر استدعاء الدالة get_node() شجرة المشاهد باستخدام مسار معين للعثور على العقدة المسماة. مثال عن استخدام get_node() ليكن لدينا الضبط التالي: يجب أن يُعلِم السكربت الموجود في العقدة Player العقدةَ AnimatedSprite2D بالرسوم المتحركة التي تُشغَّل بناءً على حركة اللاعب، بحيث تعمل الدالة get_node() في هذه الحالة بنجاح كما يلي: extends CharacterBody2D func _process(delta): if speed > 0: get_node("AnimatedSprite2D").play("run") else: get_node("AnimatedSprite2D").play("idle") ملاحظة: يمكننا استخدام المحرف $ في لغة GDScript كاختصار لاستدعاء get_node()، لذا بإمكاننا كتابة $AnimatedSprite2D مباشرةً. طريقة أفضل للاستدعاء تتمثل سلبية الطريقة السابقة في تحديد مسار العقدة، حيث إذا تغير هذا المسار لاحقًا، فيجب تعديل الشيفرة البرمجية أيضًا؛ ولهذا يمكننا استخدام الميزة @export لتحديد عقدة مباشرةً كما يلي: extends CharacterBody2D @export var animation : AnimatedSprite2D func _process(delta): if speed > 0: animation.play("run") else: animation.play("idle") يمكننا باستخدام هذه الطريقة إسناد قيمةٍ إلى المتغير مباشرةً في الفاحص Inspector من خلال اختيار العقدة. استخدام الإشارات Signals يجب استخدام الإشارات لاستدعاء الدوال في العقد الموجودة في مستوًى أعلى من الشجرة أو الموجودة على المستوى نفسه (أي العقد الأشقاء Siblings). يمكننا توصيل إشارة في المحرّر للعقد الموجودة قبل بدء اللعبة في أغلب الأحيان أو في الشيفرة البرمجية للعقد التي تنشِئ نسخةً منها في وقت التشغيل، وتكون صيغة توصيل الإشارة كما يلي: signal_name.connect(target_node.target_function) عند تجربة الاتصال بعقدة شقيقة، قد يبدو أننا سنحتاج إلى مسارات للعقدة، مثل المسار ../Sibling، ولكن كما نرى، ذلك يخالف القاعدة السابقة. ولتفادي أي خلط، لا بد من التأكد دائمًا من أن الأب المشترك هو الذي يجري الاتصالات، نظرًا لتمكن عقدة الأب التي تُعَد أبًا مشتركًا لعقدتي الإشارة والاستقبال من تحديد مكانهما وأنها ستكون جاهزة بعدهما باتباع قاعدة الاستدعاء في الشجرة من الأعلى للأسفل. مثال عن استخدام الإشارات يُعَد تحديث واجهة المستخدم UI الخاصة بك حالة استخدام شائعة جدًا للإشارات، لأننا نريد مثلًا تحديث عرض Label أو ProgressBar كلما اختلف المتغير health الخاص باللاعب، ولكن تكون عقد واجهة المستخدم الخاصة بك منفصلة تمامًا عن اللاعب إذ لا يعرف اللاعب شيئًا عن مكان هذه العقد وكيفية العثور عليها. ليكن لدينا إعداد المثال التالي: يمكننا ملاحظة أن واجهة المستخدم هي نسخة من مشهد، لأننا نعرض العقد المضمَّنة فقط، وهو المكان الذي نرى فيه أشياء مثل get_node("../UI/VBoxContainer/HBoxContainer/Label).text = str(health) التي نريد تجنبها، لذا يصدر اللاعب بدلًا من ذلك إشارة health_changed كلما أضاف أو فقد جزءًا من صحته، ويجب إرسال هذه الإشارة إلى الدالة update_health() الخاصة بواجهة المستخدم UI، والتي تتولّى ضبط قيمة Label. سنستخدم الشيفرة البرمجية التالية في سكربت Player كلما تغيرت صحة اللاعب: health_changed.emit(health) لدينا ما يلي في سكربت واجهة المستخدم UI: onready var label = $VBoxContainer/HBoxContainer/Label func update_health(value): label.text = str(value) كل ما نحتاجه الآن هو توصيل الإشارة بالدالة، والمكان المثالي ذلك موجود في سكربت World الذي يمثّل الأب المشترك للعقدتين ويعرف مكانهما: func _ready(): $Player.health_changed.connect($UI.update_health) استخدام المجموعات تُعَد المجموعات طريقةً أخرى لفك الارتباط بين عقدتين، وخاصةً عندما تكون لدينا الكثير من الكائنات المتشابهة التي تطبّق الشيء نفسه؛ إذ يمكن إضافة عقدة إلى أيّ عددٍ من المجموعات، كما يمكن تغيير العضوية ديناميكيًا في أيّ وقت باستخدام الدالتين add_to_group() و remove_from_group(). من المفاهيم الخاطئة الشائعة حول المجموعات أنها نوع من الكائنات أو المصفوفات التي تحتوي على مراجعٍ للعقد، ولكن المجموعات هي نظام وسم Tagging System، في حين تكون العقدة ضمن مجموعة عند إسناد وسمٍ من هذه المجموعة إليها. تتعقّب شجرة المشهد SceneTree الوسوم ويكون لديها دوال مثل الدالة get_nodes_in_group() للمساعدة في العثور على جميع العقد التي يكون لها وسم معين. مثال عن استخدام المجموعات لتكن لدينا لعبة إطلاق نار فضائية مثل لعبة غالاغا Galaga، مع وجود الكثير من الأعداء الذين يطيرون حول الشخصية الرئيسية، وقد يكون للأعداء أنواع وسلوكيات مختلفة، ونريد إضافة ترقية للقنبلة الذكية Smart Bomb التي تدمّر جميع الأعداء على الشاشة عند تنشيطها. هنا يمكننا الأمر باستخدام المجموعات بأقل قدر من الشيفرة البرمجية. سنحتاج أولًا إلى إضافة جميع الأعداء إلى المجموعة enemies في المحرّر باستخدام تبويب العقدة Node: يمكننا أيضًا إضافة عقد إلى المجموعة في السكربت الخاص بنا كما يلي: func _ready(): add_to_group("enemies") لنفترض أن لكل عدو الدالة explode() التي تتعامل مع ما يحدث عندما يموت، مثل تشغيل رسوم متحركة أو توليد عناصر متساقطة وما إلى ذلك. يمكننا الآن تنفيذ دالة القنبلة الذكية smart bomb الخاصة بنا كما يلي بعد أن أصبح كل عدو في مجموعته: func activate_smart_bomb(): get_tree().call_group("enemies", "explode") استخدام خاصية المالك owner owner هي خاصية عقدة Node التي تُضبَط تلقائيًا عند حفظ مشهد. تُضبَط هذه الخاصية لكل عقدة في هذا المشهد على العقدة الجذر للمشهد، مما يوفر طريقةً ملائمةً لتوصيل إشارات العقد الأبناء بالعقدة الرئيسية. مثال عن استخدام الخاصية owner سنحصل في أغلب الأحيان على تسلسل هرمي عميق ومتداخل من الحاويات وعناصر التحكم في واجهة مستخدم معقدة، وتصدر العقد التي يتفاعل معها المستخدم مثل عقدة Button إشاراتٍ. قد نرغب في ربط هذه الإشارات بالسكربت الموجود على عقدة الجذر لواجهة المستخدم. ليكن لدينا مثلًا الإعداد التالي: يحتوي السكربت الموجود على العقدة الجذر CenterContainer على الدالة التالية التي نريد استدعاءها عند الضغط على أيّ زر: extends CenterContainer func _on_button_pressed(button_name): print(button_name, " was pressed") تُعَد الأزرار هنا نسخًا من مشهد Button، وتمثّل كائنًا قد يحتوي على شيفرة برمجية ديناميكية تضبط نص الزر أو خاصيات أخرى؛ أو قد تكون لدينا أزرار تُضاف أو تُزال ديناميكيًا من الحاوية وفقًا لحالة اللعبة، ولكن ما نحتاجه لتوصيل إشارة الزر هو ما يلي: extends Button func _ready(): pressed.connect(owner._on_button_pressed.bind(name)) وكما هو ظاهر، ستبقى العقدة CenterContainer هي المالك owner بغض النظر عن المكان الذي تضع فيه الأزرار في الشجرة إذا أضفتَ مزيدًا من الحاويات مثلًا. ختامًا بهذا نكون قد تعرفنا على كيفية تحقيق تواصل سليم بين العقد Nodes في محرك الألعاب جودو Godot، وسنتابع في المقال التالي من هذه السلسلة في شرح مفهوم جديد من عالم الألعاب وهو delta. ترجمة -وبتصرّف- للقسم Node communication من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: ترتيب معالجة العقد والتنقل في شجرة المشاهد في Godot تعرف على العقد Nodes في محرك ألعاب جودو Godot تعرف على واجهة محرك الألعاب جودو Godot
-
سنشرح في هذا المقال آلية معالجة العقد ضمن شجرة المشاهد والترتيب الذي يتبعه محرك الألعاب جودو Godot للتعامل معها، كما سنوضح ما هي مسارات العقد وكيفية التنقل بينها، سيساعدنا ذلك على فهم طريقة تنظيم لعبتنا، والتحكم بها بفعالية أكبر. ترتيب معالجة العقد في شجرة المشاهد يتضمن محرك ألعاب جودو مفهوم يسمى شجرة المشاهد Scene Tree تتكون هذه الشجرة من عدة عقد Nodes، تمثل كل عقدة جزءًا من مشهد اللعبة، ويُذكَر مصطلح ترتيب الشجرة Tree Order في توثيق جودو، ولكنه غير واضح بالنسبة للمبتدئين، حيث يكون هذا الترتيب من أعلى الشجرة لأسفلها بدءًا من الجذر نزولًا إلى كل فرع بدوره، أي يبدأ الترتيب من العقد الرئيسية ثم يتنقل عبر الفروع وصولًا للعقد الفرعية، وهذا الترتيب مهم لأن كل عقدة تؤثر في العقد التي تحتها، لاحظ هذا الترتيب في الشكل التالي: سنرفق هذا الكود بكل عقدة: extends Node func _init(): # ملاحظة: العقدة ليس لها اسم بعد هنا print("TestRoot init") func _enter_tree(): print(name + " enter tree") func _ready(): print(name + " ready") # يضمن ما يلي أننا نطبع مرة واحدة فقط في process() var test = true func _process(delta): if test: print(name + " process") test = false يوضح الكود أعلاه كيفية تفاعل العقدة مع الأحداث المختلفة في شجرة المشاهد، ولنوضح ما يمثله استدعاء كل دالة من الدوال الواردة فيه قبل أن نتحدث عن النتائج: تستدعى الدالة _init() عند إنشاء العقدة أو الكائن لأول مرة، وتكون العقدة حينها موجودة في ذاكرة الحاسوب ولم تُضف لشجرة المشاهد تستدعى الدالة _enter_tree() عند إضافة العقدة للشجرة لأول مرة، ويمكن أن يحدث ذلك عند إنشاء نسخة من العقدة أو عند إنشاء عقدة ابن من عقدة ما باستخدام التابع add_child() مثلًا تستدعى الدالة _ready() عند اكتمال إضافة العقدة وأبنائها بنجاح لشجرة المشاهد وجاهزيتها للعمل تستدعى الدالة _process() بشكل دوري في كل إطار -60 مرة في الثانية عادةً- وذلك لكل عقدة في الشجرة ويمكن استخدامها للتعامل مع التحديثات المتكررة إذا شغّلنا الكود على عقدة واحدة بمفردها، فسيكون ترتيب استدعاء الدوال كالمتوقع وفق ما يلي: TestRoot init TestRoot enter tree TestRoot ready TestRoot process لكن إذا أضفنا عقد أبناء، فسيصبح الأمر أكثر تعقيدًا وقد يحتاج إلى بعض التوضيح: TestRoot init TestChild1 init TestChild3 init TestChild2 init TestRoot enter tree TestChild1 enter tree TestChild3 enter tree TestChild2 enter tree TestChild3 ready TestChild1 ready TestChild2 ready TestRoot ready TestRoot process TestChild1 process TestChild3 process TestChild2 process طبعت جميع هذه العقد رسائلها بترتيب الشجرة من الأعلى إلى الأسفل باستثناء الشيفرة البرمجية الخاصة بالتابع _ready() والذي يُستدعَى عندما تكون العقدة جاهزة، أي عندما تدخل العقدة وأبناؤها شجرة المشاهد، فإذا كان للعقدة أبناء، فستُشغَّل استدعاءات التابع _ready الخاصة بالأبناء أولًا، وستتلقى العقدة الأب إشعار الجاهزية بعد ذلك كما يوضح توثيق جودو الرسمي. يقودنا ذلك إلى قاعدة أساسية مهمة يجب تذكرها عند إعداد بنية العقد وهي: يجب أن تدير العقد الأب أبناءها وليس العكس، ويجب أن تكون أيّ شيفرة برمجية للعقدة الأب قادرةً على الوصول الكامل إلى بيانات أبنائها، لذا يجب معالجة استدعاءات التابع _ready() بترتيب الشجرة العكسي. علينا تذكّر ذلك عند محاولة الوصول لعقد أخرى في التابع _ready()، فإذا كنا بحاجة للانتقال لأعلى الشجرة إلى عقدة أب أو عقدة جَد، فيجب تشغيل هذه الشيفرة البرمجية في العقدة الأب وليس في العقدة الابن. فهم مسارات العقد والتنقل في شجرة المشاهد تُستخدم مسارات العقد Node Paths في جودو لفهم كيفية التنقل بين العقد في شجرة المشاهد Scene Tree. وهذه المسارات أساسية لفهم كيفية الوصول للعقد المختلفة داخل الشجرة وتجنب مشكلة وجود مرجع عقدة غير صالح، والتي تظهر على هيئة رسالة خطأ كالتالي: Invalid get index ‘position’ (on base: ’null instance’). يُعَد الجزء الأخير من رسالة الخطأ null instance مصدر هذه المشكلة، وهو يسبب إرباكًا للمبتدئين في جودو، ويمكن تجنّب هذه المشكلة من خلال فهم مفهوم مسارات العقد. مسارات العقد تتكون شجرة المشهد من عقد ترتبط ببعضها البعض بعلاقات أب-ابن، ومسار العقد هو المسار المُتّخَذ للانتقال من عقدة إلى أخرى من خلال التحرّك عبر هذه الشجرة. لنأخذ مثلًا مشهد لاعب بسيط كما يلي: يوجد كود هذا المشهد في العقدة Player. إذا كان السكربت بحاجة إلى استدعاء الدالة play() مع العقدة AnimatedSprite، فسيحتاج إلى مرجع إلى تلك العقدة: get_node("AnimatedSprite").play() إن وسيط الدالة get_node() هو سلسلة نصية تمثّل المسار إلى العقدة المطلوبة، وتكون هذه السلسلة النصية في حالتنا هي ابن العقدة التي يوجد ضمنها الكود. إذا كان المسار المُقدّم لها غير صالح، فسنحصل على خطأ null instance وخطأ عدم العثور على العقدة Node not found أيضًا. يُعَد الحصول على مرجع عقدة باستخدام الدالة get_node() حالة شائعة لدرجة أن لغة GDScript لديها اختصار له حيث يمكنك كتابة $ للوصول إلى العقدة مباشرة بدلًا من استدعاء الدالة، على سبيل المثال للوصول إلى العقدة AnimatedSprite وتشغيل الدالة ()play عليها مباشرة نكتب: $AnimatedSprite.play() ملاحظة: تعيد الدالة get_node() مرجعًا Reference إلى العقدة المطلوبة. لنأخذ الآن مثالًا لشجرة مشهد أكثر تعقيدًا كما يلي: إذا احتاج الكود المرفق بالعقدة Main إلى الوصول إلى العقدة ScoreLabel، فيمكنه ذلك باستخدام هذا المسار: get_node("HUD/ScoreLabel").text = "0" # أو باستخدام الاختصار: $HUD/ScoreLabel.text = "0" ملاحظة: سيكمل محرر جودو المسارات تلقائيًا نيابةً عنا عند استخدام صيغة $، ويمكننا أيضًا النقر بزر الفأرة الأيمن على عقدة ما في تبويب المشهد Scene واختيار نسخ مسار العقدة Copy Node Path. إذا كانت العقدة التي نريد الوصول إليها موجودة في مكان أعلى من الشجرة، فيمكننا استخدام الدالة get_parent() أو ".." للإشارة إلى العقدة الأب، حيث يمكننا الحصول على العقدة Player من العقدة ScoreLabel في شجرة المثال السابق كما يلي: get_node("../../Player") يمثّل المسار "../../Player" الحصول على العقدة التي تقع في مستوى واحد أعلى HUD ثم العقدة التي تقع في مستوى أعلى وهي Main ثم الوصول إلى العقدة الابن لها وهي Player. ملاحظة: تعمل مسارات العقد مثل مسارات المجلدات في نظام التشغيل، حيث تشير الشرطة المائلة / إلى علاقة أب-ابن، وتعني .. مستوى واحد أعلى. المسارات النسبية Relative والمسارات المطلقة Absolute تستخدم جميع الأمثلة السابقة مسارات نسبية، لأنها تبدأ من العقدة الحالية وتتبع المسار إلى الوجهة، ولكن يمكن أن تكون مسارات العقد مطلقة أيضًا بحيث تبدأ من العقدة الجذر للمشهد، فمثلًا يكون المسار المطلق إلى عقدة اللاعب هو: get_node("/root/Main/Player") لا يعيد المسار /root والذي يمكن الوصول إليها أيضًا باستخدام get_tree().root العقدة الجذر لمشهدنا الحالي، ولكنه يعيد العقدة الجذر لنافذة العرض Viewport التي توجد دائمًا في شجرة المشهد SceneTree افتراضيًا. مشكلة في التعامل مع مسارات العقد في جودو تعمل الأمثلة السابقة بنجاح، ولكن توجد بعض الأشياء التي يجب أن نكون على دراية بها والتي قد تسبب مشكلات لاحقًا. لنفترض أن لدينا الحالة التالية: تحتوي العقدة Player على الخاصية health التي نريد عرضها في العقدة HealthBar في مكان ما في واجهة المستخدم الخاصة بنا، لذا يمكن كتابة شيء يشبه ما يلي في كود اللاعب: func take_damage(amount): health -= amount get_node("../Main/UI/HealthBar").text = str(health) قد يكون هذا السكربت جيدًا في البداية، ولكن يمكن أن يواجه خطأ بسهولة، إذ توجد مشكلتان رئيسيتان في هذا النوع من الترتيب وهما: لا يمكننا اختبار مشهد اللاعب بصورة مستقلة، فإذا شغلنا مشهد اللاعب بمفرده أو في مشهد اختبار دون واجهة مستخدم، فسيسبّب سطر الدالة get_node() في حدوث عطل لا يمكننا تغيير واجهة المستخدم الخاصة بنا، فإذا قررنا إعادة ترتيبها أو تصميمها، فلن يكون المسار صالحًا بعد الآن ويجب تغييره لذا علينا تجنب استخدام مسارات العقد التي تنتقل إلى الأعلى في شجرة المشهد. إذا أصدر اللاعب في المثال السابق إشارة عند تغير مستوى صحته health، فيمكن لواجهة المستخدم الاستماع إلى هذه الإشارة لتحديث نفسها، ثم يمكننا إعادة ترتيب العقد والفصل بينها دون الخوف من توقف اللعبة. الخاتمة نأمل أن يكون هذا المقال قد ساعدكم على تكوين فكرة واضحة حول استخدام مسارات العقد في جودو، والطريقة الصحيحة للتنقل بين العقد والاتصال بالعناصر التي تحتاجوها في شجرة المشاهد. ففهم مسارات العقد هو الأساس الذي يمكننا البناء عليه لتفادي العديد من الأخطاء الشائعة ورسائل الخطأ مثل null instance والإشارة لأي عقدة نحتاجها بالطريقة الصحيحة. ترجمة -وبتصرّف- للقسمين Understanding tree order و Understanding node paths من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: إنشاء شخصيات ثلاثية الأبعاد في جودو Godot كتابة سكربتات GDScript وإرفاقها بالعقد في جودو استخدام الإشارات Signals في جودو Godot لغات البرمجة المتاحة في جودو Godot
-
تعرّفنا في المقال السابق على طريقة استيراد كائنات ثلاثية الأبعاد لمحرك الألعاب جودو وكيفية ترتيبها في مشهد اللعبة، وسنضيف في هذا المقال مزيدًا من الكائنات إلى مشهد اللعبة، وسنشرح طريقة إنشاء شخصية ثلاثية الأبعاد يتحكم فيها المستخدم. بناء المشهد سنستمر في هذا المقال استخدام مجموعة الملحقات assets التي توفرها منصة Kenney على هذا الرابط والتي شرحنا طريقة تنزيلها واستيرادها في مقال سابق. نفتح مشروع اللعبة ثلاثية الأبعاد التي بدأناها، ونحدّد الآن جميع ملفات block*.glb، وننتقل إلى التبويب استيراد Import ونضبط نوع الجذر Root Type الخاص بهذه الملفات على StaticBody3D ، بعدها ننقر على زر إعادة الاستيراد Reimport كما في الصورة أدناه. بعدها، نحدّد الكائن block-grass-large.glb بزر الفأرة الأيمن ونختار إنشاء مشهد موروث جديد، ستظهر عقدة جديدة باسم block-grass-large في المشهد، وعقدة ابن لها تُمثّل المجسم Mesh، نحدد العقدة الابن وننقر فوق خيار مجسم Mesh في القائمة العلوية الظاهرة في محرر جودو، ثم نحدد خيار إنشاء شكل تصادم Create Collision Shape ونعيّن قيمة الحقل Collision Shape Placement لتكون Sibling وقيمة الحقل Collision Shape Type لتكون Trimmesh كما فعلنا بالضبط في المقال السابق. عند الضغط على زر الإنشاء سيضيف جودو تلقائيًا العقدة CollionShape3D مع شكل تصادم يطابق المجسم Mesh. يمكننا الآن حفظ المشهد باسم BlockLarge، ويفضل إنشاء مجلد منفصل وليكن platform_objects لحفظ المشهد وكافة المشاهد الأخرى التي تمثل أجزاء منصة اللعبة بأشكالها المختلفة. نفتح مشهد الأرضية Ground مع الصناديق Crates الذي عملنا عليه في المقال السابق ونحذف كافة الصناديق المضافة ونستبدلها بالمشهد الموروث الذي حفظناه للتو، بهذا سنتمكّن من وضع عدة كتل بجانب بعضها البعض بحيث تكون في صف واحد وتشكل منصة اللعبة أو عالم اللعبة. بعدها نحدّد العقدة BlockLarge وننقر خيار تعديل المحاذاة Configure Snap من القائمة العلوية التحوّل Transform الظاهرة أعلى نافذة العرض كالتالي: نضبط خيار ترجمة المحاذاة Translate Snap على القيمة 0.5، ثم ننقر على زر استخدام المحاذاة Snap Mode أو نضغط على مفتاح Y، ثم نكرّر الكتلة عدة مرات ونسحبها لترتيبها ضمن المشهد بالشكل المناسب. يمكن أيضًا إضافة مشاهد لبعض كتل المنصة الأخرى وترتيبها في أيّ شكل نريده. إضافة شخصية Character سننشئ الآن شخصية يمكنها التجول على المنصة التي أنشأناها، لذا نفتح مشهدًا جديدًا ونبدأ باستخدام العقدة CharacterBody3D بالاسم Character، حيث تتصرف عقدة PhysicsBody بطريقة مشابهة جدًا لنظيرتها ثنائية الأبعاد، وتحتوي على التابع move_and_slide() الذي سنستخدمه لإجراء الحركة وكشف التصادم. نضيف عقدة MeshInstance3D على شكل كبسولة، وعقدة CollionShape3D مطابقة لها، ولنتذكّر أن بإمكاننا إضافة عقدة StandardMaterial3D إلى المجسم Mesh وضبط خاصية اللون Color في القسم Albedo. شكل الكبسولة جميل ومناسب للشخصية، ولكن سيكون من الصعب معرفة الاتجاه الذي يمثل الوجه، لذا لنضيف مجسم Mesh آخر على شكل مخروطي CylinderMesh3D، ونضبط نصف قطره العلوي Top Radius على القيمة 0.2 ونصف قطره السفلي Bottom Radius على القيمة 0.001 وارتفاعه Height على القيمة 0.5، ثم نضبط دورانه حول المحور x على -90 درجة. أصبح لدينا الآن شكل مخروطي جميل، لذا نرتّبه بحيث يشير لخارج الجسم على طول محور z السالب، إذ يمكن بسهولة معرفة الاتجاه السالب لأن أسهم أداة Gizmo تشير إلى الاتجاه الموجب. ملاحظة: أضفنا في هذه الشخصية أيضًا مجسمين كرويين بنفس الطريقة لتمثيل عيني الشخصية، ويمكن إضافة أية تفاصيل نريدها. لنضف الآن عقدة كاميرا Camera3D إلى المشهد لتتبع الشخصية. نضع الكاميرا خلف الشخصية وفوقها مع توجيهها للأسفل قليلًا، ثم ننقر على زر معاينة Preview للتحقق من عرض الكاميرا وظهورالشخصية بالشكل المناسب. كود التحكم في الشخصية قبل إضافة سكربت برمجي للتحكم في الشخصية، سوف نفتح إعدادات المشروع Project Settings، ونضيف المدخلات التالية في تبويب خريطة الإدخال Input Map: إجراء الإدخال Input Action المفتاح Key التحرك للأمام move_forward المفتاح W التحرك للخلف move_back المفتاح S الانعطاف لليمين strafe_right المفتاح D الانعطاف لليسار strafe_left المفتاح A القفز jump المسافة Space يمكننا الآن كتابة سكربت لتحريك شخصيتنا ثلاثية الأبعاد كما يلي: extends CharacterBody3D var gravity = ProjectSettings.get_setting("physics/3d/default_gravity") var speed = 4.0 # سرعة الحركة var jump_speed = 6.0 # تحديد ارتفاع القفزة var mouse_sensitivity = 0.002 # سرعة الدوران func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") velocity.x = input.x * speed velocity.z = input.y * speed func _physics_process(delta): velocity.y += -gravity * delta get_input() move_and_slide() نلاحظ أن الشيفرة البرمجية في الدالة _physics_process() بسيطة، حيث نضيف الجاذبية للتسارع في الاتجاه الموجب للمحور Y إلى الأسفل، ثم نستدعي الدالة get_input() للتحقق من الإدخال أي معرفة المفتاح الذي ضغط المستخدم عليه، ثم نستخدم التابع move_and_slide() للتحرك في اتجاه متجه السرعة. نشغّل اللعبة لاختبارها، وسنرى النتيجة التالية: هذا يبدو جيدًا، ولكن يجب أن نكون قادرين على تدوير الشخصية باستخدام الفأرة، لذا سنضيف الشيفرة البرمجية التالية إلى سكربت الشخصية: func _unhandled_input(event): if event is InputEventMouseMotion: rotate_y(-event.relative.x * mouse_sensitivity) عندما نحرك الفأرة أفقيًا في اتجاه المحور x سيعمل السكربت بتدوير الشخصية حول المحور الرأسي Y، وبالتالي إذا حركنا الفأرة إلى اليمين، سيؤدي ذلك إلى تدوير الشخصية إلى اليسار. وإذا حركنا الفأرة إلى اليسار ستدور الشخصية إلى اليمين. لدينا هنا مشكلة، إذ يحرّكنا الضغط على مفتاح W على طول المحور Z لعالم اللعبة بغض النظر عن الاتجاه الذي تتجه له الشخصية، لأن اللعبة تستخدم هنا الإحداثيات العالمية Global Coordinates، ولكننا بحاجة إلى التحرك بناء على اتجاه الكائن، ويمكن حل هذه المشكلة باستخدام التحويلات. الاستفادة من التحويلات Transforms التحويل هو مصفوفة رياضية تحتوي على معلومات حول انتقال الكائن ودورانه وتغيير حجمه في آن واحد، ويُخزَّنه جودو في نوع البيانات Transform، حيث تُسمَّى معلومات الموضع بالاسم transform.origin وتكون معلومات الاتجاه في transform.basis. تشير محاور X و Y و Z الخاصة بأداة Gizmo على طول محاور الكائن نفسه عندما تكون في وضع الحيّز المحلي Local Space Mode، ويشابه ذلك الأساس basis الخاص بالتحويل، حيث يحتوي هذا الأساس على ثلاثة كائنات Vector3 تسمى x و y و z تمثل هذه الاتجاهات، ويمكننا استخدامها لضمان أن الضغط على مفتاح W سيؤدي إلى التحرك في الاتجاه الأمامي للكائن دائمًا. نعدّل الدالة get_input() كما يلي: func get_input(): var input = Input.get_vector("strafe_left", "strafe_right", "move_forward", "move_back") var movement_dir = transform.basis * Vector3(input.x, 0, input.y) velocity.x = movement_dir.x * speed velocity.z = movement_dir.z * speed نطبّق هذا التحويل على المتجه من خلال ضرب متجه الإدخال في أساس التحويل transform.basis. يمثّل الأساس دوران الكائن، لذا لنحوّل الآن اتجاه الأمام والخلف للإشارة على طول المحور Z للكائن، ونحوّل مفاتيح التحريك لليمين واليسار للإشارة على طول المحور X الخاص بالكائن. القفز Jumping سنضيف الآن حركة أخرى إلى اللاعب وهي القفز، لتحقيق ذلك نضيف الأسطر التالية إلى نهاية الدالة _unhandled_input(): if event.is_action_pressed("jump") and is_on_floor(): velocity.y = jump_speed تحسين الكاميرا إذا وقفت الشخصية بالقرب من عائق ما، فيمكن للكاميرا الالتصاق بالكائن بطريقة غير لطيفة. وبالرغم من أن برمجة كاميرا ثلاثية الأبعاد قد تكون أمرًا معقدًا بحد ذاته، ولكن يمكننا استخدام عقد جودو المضمنة للحصول على حل جيد لذلك. نحذف العقدة Camera3D من مشهد الشخصية ونضيف العقدة SpringArm3D التي تعمل كذراع متحركة تحمل الكاميرا أثناء كشف التصادمات، وستقرّب الكاميرا عند وجود عقبة، ثم نضبط خاصية طول الذراع Spring Length على القيمة 5، ونضبط خاصية الموضع Position على القيم (0, 1, 0). نلاحظ ظهور خط بلون أصفر يشير إلى طول النابض Spring Length، حيث ستتحرك الكاميرا على طول هذا الخط، ولكنها ستقترب عند وجود عقبة حتى نهايته كلما أمكن ذلك. نعيد إضافة العقدة Camera3D كابن للعقدة SpringArm3D، ونحاول تشغيل اللعبة مرة أخرى، ويمكن تجربة تدوير ذراع النابض حول محوره X ليشير إلى الأسفل قليلًا حتى الوصول لنتيجة مرضية. الخلاصة شرحنا في هذا المقال كيفية بناء مشهد ثلاثي الأبعاد أكثر تعقيدًا وكتابة الشيفرة البرمجية لحركة شخصية يتحكم فيها المستخدم، وتعلمنا أيضًا مفهوم التحويلات Transforms التي تعد مفهومًا مهمًا جدًا في الألعاب ثلاثية الأبعاد، والتي ستستخدمها بكثرة في المقالات القادمة. ترجمة -وبتصرّف- للقسم Creating a 3D Character من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: استيراد الكائنات ثلاثية الأبعاد في جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو استخدام محرر جودو ثلاثي الأبعاد كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
-
نشرح في هذا المقال كيفية استيراد كائنات ثلاثية الأبعاد موجودة مسبقًا أنشأناها أو نزّلناها من مصدر خارجي إلى داخل محرك ألعاب جودو Godot، ونوضح المزيد حول كيفية استخدام العقد ثلاثية الأبعاد في جودو. استيراد الكائنات ثلاثية الأبعاد في حال كنا على دراية ببرامج النمذجة ثلاثية الأبعاد مثل بلندر Blender، فيمكننا إنشاء نماذجنا الخاصة لاستخدامها في لعبتنا. وفي حال لم نكن كذلك، فهناك العديد من المصادر التي توفر لنا تنزيل الكائنات لأنواع معينة من الألعاب، ومن بينها منصة Kenney التي توفر الكثير من الموارد والملحقات assets المجانية عالية الجودة لصنّاع الألعاب. سنستخدم في أمثلتنا التالية مجموعة ملحقات Platformer Kit للعبة منصات من Kenney، والتي يمكن تنزيلها من هذا الرابط ، حيث تحتوي مجموعة واسعة من الكائنات ثلاثية الأبعاد 3D. وفيما يلي عينة توضّح هذه الملحقات: سنجد بعد التنزيل مجلدًا باسم Models يتضمن مجموعة متنوعة من الصيغ التي يمكن التعامل معها في محرك ألعاب جودو، ولكن صيغة GLTF مفضلة عن الصيغ الأخرى. صيغ الملفات ثلاثية الأبعاد يجب أن نحفظ نماذجنا ثلاثية الأبعاد بصيغة يمكن لجودو استخدامها سواءً أردنا إنشاء نماذجنا الخاصة أو تنزيلها، حيث يدعم جودو صيغ الملفات التالية للنماذج ثلاثية الأبعاد، ولكل منها ميزاته وقيوده: GlTF: صيغ نماذج ثلاثية الأبعاد مدعومة في كل من الإصدارات النصية .gltf والثنائية .glb DAE Collada: صيغة أقدم لكنها لا تزال مدعومة OBJ Wavefront: صيغة أقدم مدعومة، ولكنها محدودة مقارنة بالخيارات الحديثة FBX: صيغة تجارية لها دعم محدود تُعَد صيغة GlTF هي الصيغة الموصى بها كما وضحنا سابقًا، لكونها تحتوي على معظم الميزات ودعمها جيد في جودو، سنضع المجلد GLB format في الموجود ضمن المجلد Models في مجلد مشروع جودو الخاص بنا ونعيد تسميته إلى platformer_kit. نرجع إلى نافذة محرك ألعاب جودو، يجب أن نرى شريط التقدم كما في الصورة التالية أثناء فحص جودو للمجلد platformer_kit واستيراد جميع الكائنات ضمنه. لننقر نقرًا مزدوجًا على أحد هذه الكائنات وليكن crate.glb في تبويب نظام الملفات FileSystem والذي يمثل صندوق ثلاثي الأبعاد كالتالي: خطوات استيراد كائن ثلاثي الأبعاد وتعديل عقدة الجذر عند النقر على كائن ثلاثي الأبعاد يمكننا رؤية خيارات استيراد هذا الكائن في التبويب استيراد Import بجانب تبويب المشهد Scene مع إمكانية ضبط الخاصية نوع الجذر Root Type وتعديل اسم الجذر Root Name، دعونا نضبط نوع الجذر على RigidBody3D ونطلق عليه اسم Crate للتعبير عن كونه يمثل صندوق ثلاثي الأبعاد، ثم ننقر على زر إعادة الاستيراد Reimport. ننقر بعدها بزر الفأرة الأيمن على crate.glb ونحدّد خيار مشهد موروث جديد New Inherited Scene. أصبح لدينا كائن لعبة كلاسيكي هو الصندوق Crate، وعقدة جذر للمشهد هي RigidBody3D بالاسم Crate كما أردنا تمامًا، لكن سنلاحظ ظهور تحذير يشير بأن هذه العقدة لا تحتوي مكون التصادم Collision الضروري للتفاعل مع الكائنات الأخرى. لذا فإن الخطوة التالية التي علينا القيام بها هي إضافة شكل تصادم إلى الكائن ثلاثي الأبعاد، ويمكننا تطبيق ذلك من خلال إضافة عقدة من نوع CollionShape3D كما نفعل عادة في الألعاب ثنائية الأبعاد 2D، ولكن سننجزها هنا بطريقة أسرع. نحدّد العقدة crate، وسنلاحظ ظهور شريط قوائم في الجزء العلوي من نافذة العرض، ننقر على أيقونة المجسم ونحدّد إنشاء شكل تصادم Create Collision shape ونحدد قيمة الحقل Collision Shape Placement لتكون Sibling أي أن أن شكل التصادم سيضاف كعقدة أخ للعقدة الحالية، وقيمة الحقل Collision Shape Type لتكون Trimmesh أي التصادم سيتم بناءً على المجسم ثلاثي الأبعاد للكائن، وعند الضغط على زر الإنشاء سيضيف جودو تلقائيًا العقدة CollionShape3D مع شكل تصادم يطابق Mesh. انتهينا الآن من إعداد كائن الصندوق Crate، لنحفظ المشهد الخاص به ونتعرّف كيف يمكننا استخدامه في اللعبة أو المشروع. بناء مشهد ثلاثي الأبعاد ننشئ مشهدًا جديدًا باستخدام عقدة الجذر Node3D، وأول ابن سنضيفه هو الأرضية لوضع بعض كائنات الصناديق عليها، لذا نضيف عقدة StaticBody3D ونسميها Ground، ونضيف إليها عقدة ابن من نوع MeshInstance3D. ونحدّد خيار BoxMesh جديدة في الخاصية Mesh ضمن قسم الفاحص. ثم نضبط حجمها على القيم التالية (10,0.1,10) بحيث يكون لدينا أرضية كبيرة، ستظهر الأرضية باللون الأبيض بشكل افتراضي وسيبدو شكلها أفضل إن غيرناها للون البني أو الأخضر، وللقيام بذلك ننتقل للخاصية Material الموجودة في قسم خاصيات Mesh فهي تساعدنا على تحديد مظهر الكائن. سنحدّد الخيار StandardMaterial3D جديدة كقيمة للخاصية Mesh، ثم ننقر عليها لنستعرض قائمة كبيرة من الخاصيات، ما يهمنا هو خاصية اللون Color ضمن قسم Albedo لضبط الأرضية باللون الأخضر الداكن. الآن إذا أضفنا صندوقًا، فسيسقط عبر الأرضية، لذا يجب إعطاؤها شكل تصادم من خلال إضافة عقدة التصادم CollisionShape3D كعقدة ابن للأرضية Ground ونحدد قيمة الخاصية Shape لتكون BoxShape3 جديدة، ثم نضبط حجم صندوق التصادم Size ليكون بنفس حجم Mesh. عند إضافة صندوق إلى المشهد، سيسقط تلقائياً عبر الأرضية Ground نظراً لغياب خصائص التصادم الفيزيائي. لحل هذه المشكلة، نحتاج إلى إضافة عقدة CollisionShape3D كعقدة فرعي للأرضية Ground. بمجرد إضافة العقدة، علينا تعيين خاصية Shape لها باختيار BoxShape3D جديدة ، ثم نضبط أبعاد صندوق التصادم ليتطابق مع حجم الشبكة. بهذه الطريقة، ستصبح الأرضية قادرة على منع الأجسام من السقوط عبرها. ننشئ الآن عددًا من الصناديق في المشهد ونرتبها في كومة تقريبية. ونضيف كاميرا Camera ونضعها في مكان يوفر لنا رؤية جيدة للصناديق، ونشغّل المشهد ونشاهد الصناديق تتدحرج. سنلاحظ أن المشهد مظلم بسبب عدم وجود ضوء فيه، إذ لا يضيف جودو افتراضيًا أي إضاءة أو بيئة إلى المشاهد كما يفعل في نافذة عرض المحرّر، ويُعَد ذلك مناسبًا عندما نريد إعداد الإضاءة الخاصة بنا، ولكن يوجد طريقة مختصرة لنضيء مشهدنا البسيط كما سنوضح في القسم التالي. الإضاءة تتوفر عقد إضاءة متعددة في المشاهد ثلاثية الأبعاد يمكننا استخدامها لإنشاء مجموعة متنوعة من تأثيرات الإضاءة. سنبدأ بالعقدة DirectionalLight3D أولًا، ولكن سنجعل جودو يستخدم العقدة نفسها التي يستخدمها في نافذة المحرر بدلًا من إضافتها يدويًا. نلاحظ وجود أيقونتين في الجزء العلوي فوق نافذة العرض يتحكمان في إضاءة المعاينة Preview Lighting وبيئة المعاينة Preview Environment. إذا نقرنا على النقاط الثلاث بجوارهما، فيمكنك رؤية إعداداتهما. ننقر على زر إضافة الشمس إلى المشهد Add Sun to Scene، وبهذا سيضيف جودو العقدة DirectionalLight3D إلى المشهد مباشرة. ننقر على زر إضافة بيئة إلى المشهد Add Environment to Scene، وسيفعل جودو الشيء نفسه مع معاينة السماء من خلال إضافة عقدة WorldEnvironment. نشغّل المشهد مرة أخرى، وسنتمكن من رؤية الصناديق تتساقط بشكل أفضل. تدوير الكاميرا لنجعل الآن الكاميرا تدور ببطء حول المشهد، لذا نحدّد العقدة الجذر ونضيف عقدة Node3D، والتي ستكون موجودة عند النقطة (0,0,0) ونطلق على هذه العقدة اسم CameraHub. نسحب الكاميرا في شجرة المشهد لجعلها ابنًا لهذه العقدة الجديدة، حيث إذا دارت عقدة CameraHub حول المحور y، فسنسحب الكاميرا معها. نضيف سكربتًا إلى العقدة الجذر ونضع فيه ما يلي: extends Node3D func _process(delta): $CameraHub.rotate_y(0.6 * delta) يعمل السكربت أعلاه على تدوير العقدة CameraHub حول المحور Y بشكل مستمر أثناء اللعبة. وتعتمد سرعة التدوير على الزمن بين الإطارات delta ما يجعل الحركة أكثر سلاسة بغض النظر عن أداء الجهاز. الخلاصة تعلّمنا في هذا المقال كيفية استيراد كائنات ثلاثية الأبعاد من مصادر خارجية وكيفية دمجها في مشهد بسيط في جودو، وتعرفنا أيضًا على الإضاءة والكاميرات المتحركة، وسنوضح في المقال التالي كيفية بناء مشهد أكثر تعقيدًا وتضمين شخصية يتحكم فيها اللاعب. ترجمة -وبتصرّف- للقسم Importing 3D Objects من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: استخدام محرر جودو ثلاثي الأبعاد تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو مطور الألعاب: من هو وما هي مهامه تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
-
سنلقي نظرة في هذا المقال على كيفية البدء في استخدام محرّر جودو Godot ثلاثي الأبعاد، حيث ستتعلم كيفية التنقل في هذا المحرّر، وكيفية إنشاء الكائنات ثلاثية الأبعاد ومعالجتها، وكيفية العمل مع بعض العقد الأساسية ثلاثية الأبعاد في جودو مثل الكاميرات والإضاءة. قد يكون تطوير الألعاب ثلاثية الأبعاد أكثر تعقيدًا من تطوير الألعاب ثنائية الأبعاد، حيث يمكن تطبيق العديد من المبادئ نفسها مثل العمل مع العقد، وكتابة السكربتات والتعامل مع المنطق البرمجي، ولكن يتطلب التطوير ثلاثي الأبعاد عدد من الاعتبارات الأخرى، لذا يُفضَّل تعلم تطوير الألعاب ثنائية الأبعاد للمبتدئين، والانتقال إلى تعلم تطوير الألعاب ثلاثية الأبعاد بعد امتلاك الفهم الجيد لمبادئ تطوير الألعاب، يفترض هذا المقال وجود معرفة جيدة بإنشاء لعبة متكاملة ثنائية الأبعاد في جودو Godot 2D على الأقل. البدء في تطوير الألعاب ثلاثية الأبعاد تتمثل إحدى نقاط قوة جودو في قدرته على التعامل مع الألعاب ثنائية الأبعاد وثلاثية الأبعاد بكفاءة. يمكننا تطبيق الكثير مما نتعلّمته من العمل على المشاريع ثنائية الأبعاد كالعقد nodes والمشاهد scenes والإشارات signals وما إلى ذلك على الألعاب ثلاثية الأبعاد، ولكن سنجد بعض التعقيدات والمميزات الجديدة في التطوير ثلاثي الأبعاد، وسنوضح في فقراتنا التالية أهم الميزات الإضافية المتوفرة في نافذة محرر جودو ثلاثي الأبعاد. التوجيه Orienting في الفضاء ثلاثي الأبعاد عندما نفتح مشروعًا جديدًا في جودو لأول مرة فسنرى عرض المشروع ثلاثي الأبعاد وسنلاحظ وجود أدوات وخصائص مُعدة خصيصًا للتطوير ثلاثي الأبعاد 3D كما في الصورة التالي: أول شيء سنلاحظه هو الخطوط الثلاثة الملونة في المنتصف، والتي هي المحور X باللون الأحمر، والمحور Y باللون الأخضر، والمحور Z باللون الأزرق، وتسمى النقطة التي تلتقي فيها هذه المحاور بنقطة الأصل Origin والتي لها الإحداثيات (0,0,0). وسنلاحظ أن مخطط الألوان هذا يُطبَّق أيضًا في أماكن أخرى من الفاحص Inspector. ملاحظة: قد تستخدم برامج تطوير الألعاب ثلاثية الأبعاد اصطلاحات مختلفة للتوجيه Orienting. ففي محرك جودو، يستخدم المحور Y ليحدد الاتجاه للأعلى، أي يشير المحور Y للأعلى والأسفل، ويشير المحور X إلى اليسار واليمين، ويشير المحور Z للأمام والخلف. بينما في بعض البرامج ثلاثية الأبعاد الأخرى، قد يُستخدم المحور Z ليشير للاتجاه نحو الأعلى. لذا، يجب أن نأخذ هذا الأمر في الاعتبار عند الانتقال لتطبيقات مختلفة. يمكننا التنقل في المشروع ثلاثي الأبعاد باستخدام الفأرة ولوحة المفاتيح، وفيما يلي عناصر التحكم الأساسية لكاميرا العرض: نحرك عجلة الفأرة للأعلى أو للأسفل لتكبير أو تصغير المشهد نستخدم الزر الفأرة الأوسط مع السحب لتدوير الكاميرا حول الهدف الحالي نستخدم مفتاح Shift مع الزر الأوسط للفأرة مع السحب لتحريك الكاميرا حول المشهد ننقر بزر الفأرة الأيمن مع السحب لتدوير الكاميرا حول محورها دون تغيير موقعها في المشهد ملاحظة: في بعض الألعاب ثلاثية الأبعاد الشهيرة، يوجد وضع يسمى Freelook يسمح لنا بالتنقل بحرية في المشهد دون قيود. يمكننا تفعيل هذا الوضع أو إيقافه في جودو باستخدام الاختصار Shift + F. عند تفعيل هذا الوضع يمكننا استخدام مفاتيح WASD أي مفتاح W للتقدم للأمام، و A للتحرك لليسار،و S للتحرك للخلف، و D للتحرك لليمين من أجل التحرك حول المشهد بحرية كما نفعل في الألعاب عادة. وفي هذا الوضع، ستتيح لنا الفأرة التوجيه وتحريك الكاميرا حول المشهد أو الهدف كما نريد. بالنسبة لتغيير عرض الكاميرا، سنجد في الزاوية العلوية اليسرى من الشاشة، اسم توضيحي منظوري Perspective. عند النقر عليه، يمكننا تغيير زاوية رؤية الكاميرا، أي يمكننا جعل الكاميرا تتجه إلى اتجاه معين كجعلها تعرض المشهد من الأعلى أو الجوانب أو بأي زاوية نرغب بها. إضافة كائنات ثلاثية الأبعاد لنضف الآن أول عقدة ثلاثية الأبعاد. ترث العقد ثلاثية الأبعاد في جودو من العقدة الأساسية Node3D، وهي توفر نفس الخصائص التي ترثها العقدة Node2D في المشاريع الثنائية الأبعاد، مثل خاصية الموضع position لتحديد مكان الكائن في المشهد، وخاصية الدوران rotation لتحديد زاوية دوران الكائن. عند إضافة عقدة ثلاثية الأبعاد Node3D إلى المشهد، سنرى الكائن أن يظهر عند نقطة الأصل (0,0,0) في الفضاء ثلاثي الأبعاد كما في الصورة التالية: في جودو، عندما نضيف كائن ثلاثي الأبعاد إلى المشهد، لا يُعدّ هذا الكائن عقدة، بل يُسمى Gizmo ثلاثي الأبعاد. تُعدّ Gizmo أداة تتيح لنا التحكم في الكائنات الثلاثية الأبعاد، مثل تحريكها أو تدويرها في الفضاء. تستخدم هذه الأداة ثلاث حلقات كي تتحكم في الدوران، وتستخدم ثلاثة أسهم لتحرك الكائن على طول المحاور الثلاثة، وتكون الحلقات والأسهم ملونة لتتناسب مع ألوان المحاور. يمكن تجربة استخدام هذه الأداة والتعرف عليها، واستخدام زر التراجع Undo لاستعادة الوضع السابق. ملاحظة: قد نجد أن أدوات Gizmo المستخدمة لتحريك الكائنات وتدويرها وتغيير حجمها مزعجة أو متداخلة مع بعضها أثناء العمل. ويمكننا حل هذه المشكلة بسهولة واختيار نوع واحد فقط من التعديلات بالنقر على أيقونات الوضع للتقيد بنوع واحد فقط من التحويلات أي يمكن أن نختار إما وضع التحريك أو وضع التدوير أو وضع التحجيم، فباختيارنا لأحد هذه الأوضاع، سنتمكن من التركيز على تعديل واحد في كل مرة وسيسهل علينا التعامل مع الكائنات في المشهد. الحيز العالمي والحيز المحلي الحيز العالمي والحيز المحلي هما طريقتان للتحكم في تحريك الكائنات داخل جودو حيث يعتمد الحيز العالمي Global Space على محاور ثابتة لا تتغير مهما دورنا الكائن أوحركناه وتشير الأسهم دائمًا إلى هذه المحاور الثابتة، أما في الحيز المحلي Local Space فتتحرك المحاور مع حركة الكائن نفسه وتُعدّل بناء على تحريك الكائن أو تدويره. تعمل عناصر التحكم في أداة Gizmo في الحيز العالمي Global Space افتراضيًا، إذ تبقى أسهم أداة Gizmo تشير على طول المحاور عند تدوير الكائن، ولكن إذا نقرنا على زر استخدام الحيز المحلي Use Local Space، فستتحوّل الأداة إلى تحريك الجسم في حيز محلي. تشير أسهم أداة Gizmo الآن إلى محاور الكائن نفسه بدلاً من محاور العالم عند تدويره. فيمكن أن يساعدنا التبديل بين الحيز المحلي والعالمي في تحديد موقع الكائن بدقة وفقًا لاحتياجاتنا. التحويلات Transforms لنبحث عن العقدة Node3D في الفاحص Inspector، حيث سنرى خاصيات الموضع Position والدوران Rotation والتحجيم Scale في قسم التحويل Transform، لذا نسحب الكائن باستخدام أداة Gizmo ونلاحظ كيف تتغير هذه القيم. تكون هذه الخاصيات متعلقة بأب هذه العقدة كما هو الحال في العقد ثنائية الأبعاد 2D. تشكّل هذه الخاصيات مع بعضها البعض ما يسمى بتحويل العقدة. سنتمكن من الوصول إلى خاصية التحويل transform التي هي كائن جودو Transform3D عند تغيير الخاصيات المكانية للعقدة في الشيفرة البرمجية، ويكون لهذا الكائن خاصيتان هما origin و basis. تمثّل الخاصية origin موضع الجسم، بينما تحتوي الخاصية basis على ثلاثة متجهات تحدّد محاور إحداثيات الجسم المحلية، حيث يمكننا التفكير في أسهم المحاور الثلاثة في أداة Gizmo عندما تكون في وضع الحيز المحلي Local Space، وسنوضّح كيفية استخدام هذه الخاصيات لاحقًا. الشبكات Meshes لا تتمتع عقدة Node3D بحجم أو مظهر خاص بها مثل عقدة Node2D، لذا يمكننا استخدام العقدة Sprite2D لإضافة خامة Texture إلى العقدة في الألعاب ثنائية الأبعاد 2D، ولكننا سنحتاج إلى إضافة شبكة Mesh في الألعاب ثلاثية الأبعاد 3D. الشبكة هي وصف رياضي لشكل ما، وتتكون من مجموعة من النقاط تسمى الرؤوس Vertices، وترتبط هذه الرؤوس بخطوط تسمى الأضلاع Edges، وتشكل الأضلاع المتعددة مع بعضها البعض وجهًا Face، فمثلًا يتكون المكعب من 8 رؤوس و 12 ضلعًا و 6 أوجه. إضافة الشبكات يمكن إنشاء الشبكات باستخدام برامج النمذجة ثلاثية الأبعاد مثل بلندر Blender، ويمكننا أيضًا العثور على العديد من مجموعات النماذج ثلاثية الأبعاد المتاحة للتنزيل إن لم نتمكّن من إنشاء نموذجنا الخاص. قد نحتاج في بعض الأحيان إلى شكل هندسي أساسي فقط مثل المكعب أو الكرة، ويوفر جودو في هذه الحالة طريقة لإنشاء شبكات بسيطة تسمى الأشكال الأولية Primitives. لنضف عقدة MeshInstance3D كعقدة ابن للعقدة Node3D، وننقر على الخاصية Mesh الخاصة بها في الفاحص Inspector: يمكننا الآن رؤية قائمة الأشكال الأولية المتاحة، والتي تمثل مجموعة مفيدة من الأشكال الشائعة المفيدة. سنحدّد خيار BoxMesh جديدة، سنرى مكعبًا يظهر على الشاشة. الكاميرات سنحاول تشغيل المشهد مع كائن المكعب، ولكنا لن نرى أي شيء في نافذة عرض اللعبة ثلاثية الأبعاد دون إضافة العقدة Camera3D، لذا لنضفها إلى العقدة الجذر ونستخدم أداة Gizmo الخاصة بالكاميرا لوضعها في اتجاه المكعب كما يلي: يسمى الشكل الهرمي الوردي الأرجواني على الكاميرا Fustrum وهو يمثل مجال نظر الكاميرا، ونلاحظ السهم الذي له شكل المثلث الصغير ويمثل اتجاه الكاميرا للأعلى. نضغط على زر المعاينة Preview في الجزء العلوي الأيسر أثناء تحريك الكاميرا لمعرفة ما تراه الكاميرا، ونشغّل المشهد للتأكد من أن كل شيء يعمل كما هو متوقع. الخلاصة تعلمنا في هذا المقال كيفية استخدام محرر جودو ثلاثي الأبعاد، وكيفية إضافة عقد ثلاثية الأبعاد مثل Node3D و MeshInstance3D و Camera3D، وكيفية استخدام أدوات Gizmo لوضع الكائنات الخاصة بنا في مكانها، بالإضافة إلى مجموعة من المصطلحات الجديدة. سنوضح في المقال التالي كيفية بناء مشهد ثلاثي الأبعاد من خلال استيراد ملفات الأصول ثلاثية الأبعاد 3D Assets وكيفية استخدام مزيد من عقد جودو ثلاثية الأبعاد. ترجمة -وبتصرّف- للقسم The 3D Editor من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: كتابة سكربتات GDScript وإرفاقها بالعقد في جودو تعرف على واجهة محرك الألعاب جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو العقد Nodes والمشاهد Scenes في جودو Godot
-
تُعَد كتابة السكربتات البرمجية وربطها بالعقد والكائنات الأخرى الأسلوب الأساسي لبناء سلوك الألعاب في محرك جودو. على سبيل المثال، تعرض العقدة Sprite2D صورة تلقائيًا، ولكن يمكننا من خلال إضافة سكربت مخصص لهذه العقدة تحريك هذه الصورة عبر الشاشة، وتحديد سرعة واتجاه الحركة والعديد من الخصائص الأخرى. ما هي لغة GDScript لغة جي دي سكربت GDScript هي لغة مُضمَّنة في جودو لكتابة السكربتات البرمجية والتفاعل مع العقد. وتشير عدة مراجع لأن لغة GDScript تعتمد على لغة بايثون Python، إذ تستخدم لغة GDScript صياغة مبنية على لغة بايثون، ولكنها لغة مميزة مُحسَّنة ومدمجة مع محرك جودو، لذا إذا كنا على دراية باستخدام لغة بايثون، فستكون هذه اللغة مألوفة وسهلة الفهم بالنسبة لنا. ملاحظة: يُفترَض امتلاك بعض الخبرة في أساسيات البرمجة كي نتمكن من تعلّم محرّك الألعاب جودو، فإن لم نكن على دراية بالبرمجة على الإطلاق فسيكون لديك عبء إضافي لتعلم البرمجة. فإذا وجدتم صعوبة في فهم الشيفرة البرمجية في هذا المقال، فننصح قبل البدء بمطالعة مقال أساسيات لغة بايثون ومقال تعلم أساسيات البرمجة. بنية ملف GDScript يجب أن يكون السطر الأول من أي ملف GDScript هو extends <Class> حيث أن <Class> هو إما صنف مُضمَّن موجود مسبقًا، أو صنف يعرّفه المستخدم، فمثلًا إذا أردنا إرفاق سكربت معين بعقدة CharacterBody2D، فسيبدأ السكربت بالعبارة extends CharacterBody2D، وهذا يعني أن السكربت يأخذ جميع وظائف كائن CharacterBody2D المُضمَّن ويوسّعه باستخدام الوظائف الإضافية التي أنشأناها بنفسنا. يمكننا في بقية السكربت تعريفُ أيّ عدد من المتغيرات المعروفة أيضًا باسم خاصيات الصنف والدوال المعروفة أيضًا باسم توابع الصنف. إنشاء سكربت GDScript لننشئ السكربت الأول، لنتذكّر أن بإمكاننا إرفاق سكربت بأيّ عقدة. لذا سنفتح محرّر جودو ونتقل للتبويب مشهد Scene، ونضيف عقدة من نوع Sprite2D إلى المشهد، ثم ننقر بزر الفأرة الأيمن على العقدة الجديدة، ونحدّد خيار إلحاق نص برمجي Attach Script، ويمكننا أيضًا النقر على الأيقونة الموجودة يسار مربع البحث. يجب بعد ذلك أن نختار المكان الذي تريد حفظ السكربت البرمجي فيه ونحدد اسمه، وفي حال سمّينا العقدة باسم ما، فسيُسمَّى السكربت تلقائيًا بذلك الاسم، لذا إن لم نغيِّر أي شيء، فسوف يسمى هذا السكربت sprite2d.gd. نفتح الآن نافذة محرّر السكربت البرمجي سنجد بعض الكود البرمجي فيها، سيكون هذا السكربت مرتبطًا بالشخصية الرسومية Sprite الجديدة الفارغة حتى الآن، فقد وضع جودو تلقائيًا بعض أسطر الشيفرة البرمجية بالإضافة إلى بعض التعليقات التي تشرح الشيفرة. extends Sprite2D # Called when the node enters the scene tree for the first time. func _ready() -> void: pass # Replace with function body. # Called every frame. 'delta' is the elapsed time since the previous frame. func _process(delta: float) -> void: pass أُضيف هذا السكربت إلى العقدة Sprite2D، لذا يكون السطر الأول تلقائيًا هو extends Sprite2D كي يوسّع هذا السكربت الصنف Sprite2D ويكون قادرًا على الوصول إلى جميع الخاصيات والتوابع التي توفرها العقدة Sprite2D ومعالجتها. ملاحظة: إن الخاصيات Properties والتوابع Methods هي المتغيرات والدوال المُعرَّفة في الكائن، قد يخلط المبرمجون المبتدئون في استخدام هذه المصطلحات. سنعرّف بعد ذلك جميع المتغيرات التي سنستخدمها في السكربت، والتي تُسمَّى المتغيرات الأعضاء Member Variables، ولتعريف المتغيرات نستخدم الكلمة المفتاحية var. سنتجاوز التعليقات الموجودة في السكربت ونتحدث عن الجزء التالي، حيث سنرى في السكربت أعلاه الدالة _ready() حيث يبدأ تعريف الدالة في لغة GDScript بالكلمة المفتاحية func. والدالة _ready() هنا هي دالة خاصة ينفّذها جودو بشكل تلقائي عند إضافة عقدة إلى الشجرة، أو عندما يبدأ المشهد بالعمل بالضغط على تشغيل Play. لنفترض أننا نريد نقل الشخصية الرسومية إلى موضع محدد عند بدء اللعبة، عندها يجب علينا ضبط خاصية الموضع Position في الفاحص Inspector، توجد هذه الخاصية في القسم Node2D، وبالتالي ستتوفر هذه الخاصية في أي عقدة من نوع Node2D، وليس فقط في العقد من نوع Sprite2D. لنضبط الآن هذه الخاصية عن طريق الشيفرة البرمجية، إحدى الطرق المتبعة للعثور على اسم الخاصية هي التمرير فوقها في الفاحص Inspector كما يلي: يحتوي جودو على أداة بحث مساعد مدمجة رائعة تساعدنا في التعرف على تفاصيل الأصناف التي نتعامل معها، لبدء استخدام هذه الأداة ننقر على التبويب Classes في الجزء العلوي من نافذة السكربت، ثم نبحث عن اسم الصنف Node2D، ستظهر لنا صفحة المساعدة التي تعرض جميع الخاصيات والتوابع المتاحة بالصنف. إذا مررنا لأسفل قليلاً، سنلاحظ وجود الخاصية position ضمن قسم Member Variables، والتي تُعد من المتغيرات الأعضاء للصنف. ونلاحظ أيضًا أن هذه الخاصية من النوع Vector2، مما يعني أنها تُستخدَم لتخزين الإحداثيات على المحورين X و Y. لنرجع إلى السكربت البرمجي ونستخدم هذه الخاصية كما يلي: func _ready(): position = Vector2(100, 150) نلاحظ كيف يعرض لنا المحرّر اقتراحات أثناء الكتابة فعندما نكتب مثلًا Vector2، سيخبرنا التلميح بوضع عددين عشريين x و y. لدينا الآن سكربت يمثّل ضبط موضع الشخصية الرسومية على القيم (100,150) عند بدء التشغيل، ويمكننا تجربة ذلك بالضغط على زر تشغيل المشهد Play Scene. ملاحظة: يستخدم جودو المتجهات أو الأشعة Vectors للعديد من الأشياء، وسنتحدث عنها بمزيد من التفصيل لاحقًا. وأخيرًا قد يتساءل المبتدئون في برمجة الألعاب عن كيفية حفظ جميع هذه الأوامر البرمجية، والجواب هو مثل أي مهارة أخرى تمامًا، إذ لا يتعلق الأمر بالحفظ بل بالممارسة والتطبيق، فعندما نكرر تنفيذ الأشياء مرارًا وتكرارًا ستصبح بديهية بالنسبة لنا. ومن الجيد الاحتفاظ بمستندات مرجعية في متناول اليد مبدئيًا، واستخدام البحث كلما رأينا شيئًا لا نعرفه، وإذا كان لدينا شاشات متعددة، فلنحتفظ بنسخة مفتوحة من صفحات التوثيق للرجوع إليها بسرعة. الخاتمة أنشأنا في مقال اليوم أول نص برمجي باستخدام GDScript، من الضروري تطبيق وفهم كل ما فعلناه في هذه الخطوة قبل الانتقال إلى الخطوة التالية، حيث سنضيف مزيدًا من الشيفرة البرمجية لتحريك الشخصيات الرسومية حول الشاشة في المقال التالي. ترجمة -وبتصرّف- للقسم Introduction to GDScript: Getting started من توثيقات Kidscancode. اقرأ أيضًا المقال السابق: تعرف على العقد في محرك الألعاب Godot العقد Nodes والمشاهد Scenes في جودو Godot لغات البرمجة المتاحة في جودو Godot كتابة برنامجك الأول باستخدام جودو Godot مدخل إلى محرك الألعاب جودو Godot
-
العقد Nodes هي البنى الأساسية لإنشاء الألعاب في محرك جودو Godot، فالعقدة هي كائن يمكنه تمثيل نوع معين من وظائف اللعبة، فقد تعرض بعض أنواع العقد رسومات graphics أو تشغّل رسومًا متحركة animation أو تمثّل نموذجًا ثلاثي الأبعاد 3D model لكائن. كما تحتوي العقدة أيضًا على مجموعة من الخاصيات properties التي تسمح بتخصيص سلوكها، ويعتمد اختيار أي عقدة على الوظيفة التي نحتاجها، ويمكن أن كل تكون عقدة كوحدة مستقلة تؤدي وظيفة معينة، كما يمكننا دمج العديد من هذه الوحدات أو العقد لتشكيل كائنات اللعبة بطريقة مرنة ومتكاملة. العمل مع العقد برمجيًا، العقد عبارة عن كائنات objects فهي تغلّف البيانات data والسلوك behavior، ويمكنها أن ترث خاصيات properties من عقد أخرى. لننقر الآن على زر + أو زر إضافة/إنشاء عقدة جديدة Add/Create a New Node في تبويب المشهد Scene بدلًا من استخدام أحد الاقتراحات الافتراضية للعقد التي يقترحها علينا جودو وذلك كما يلي: بعد النقر على زر إنشاء عقدة جديدة سنشاهد الآن تسلسل هرمي يتضمن كافة أنواع العقد المتاحة في محرك الألعاب جودو كما يلي: تندرج جميع العقد ذات الأيقونات الزرقاء على سبيل المثال ضمن الفئة Node2D، مما يعني أن هذه العقد سيكون لها خاصيات العقد ثنائية الأبعاد Node2D التي سنتحدث عنها لاحقًا بمزيد من التفصيل. نلاحظ أن القائمة طويلة جدًا، وسيكون صعبًا التمرير عبرها للعثور على العقدة التي نحتاجها في كل مرة، لذا يمكننا استخدام وظيفة البحث للعثور على العقدة المطلوبة باستخدام عدد صغير من الأحرف. مثلًا يمكننا العثور على عقدة Sprite2D بسرعة بكتابة الحرفين sp فقط في حقل البحث وستنتقل إليها مباشرة، بعدها ننقر على زر أنشئ Create لإضافة العقدة للمشهد. أصبح لدينا الآن العقدة Sprite2D في تبويب المشهد Scene، لذا نتأكد من تحديدها، ثم ننظر إلى تبويب الفاحص Inspector على الجانب الأيسر من شاشة محرر جودو حيث سنرى فيها جميع خاصيات العقدة التي حدّدناها. نلاحظ أن الخاصيات الظاهرة للعقدة ليست فقط التي تخص عقدة Sprite2D نفسها، بل تشمل أيضًا الخصائص التي ورثتها من العقد الأخرى التي جاءت قبلها في سلسلة من العقد وهي منظَّمة حسب مصدرها، حيث ترث العقدة Sprite2D العقدة Node2D التي ترث بدورها العقدة CanvasItem التي ترث بدورها العقدة Node الأساسية البسيطة. بعد إضافة هذه العقدة للعبتنا سنلاحظ أن الشخصية الرسومية Sprite لا تظهر كما هو متوقع في واجهة المستخدم. فالغرض من هذه العقدة هو عرض صورة أو خامة Texture. وبما أن الخاصية Texture في حاوية الفاحص Inspector فارغة حاليًا فلهذا السبب لم تظهر في نافذة العرض. لحسن الحظ يأتي كل مشروع جديد من جودو مع صورة باسم icon.svg يمكننا استخدامها حاليًا، وهذه الصورة هي أيقونة محرك الألعاب جودو، لذا سنسحبها من التبويب نظام الملفات Filesystem في الجانب الأيمن ونفلتها في الحقل الخاص بالخاصية Texture. ننقر لتوسيع قسم التحويل Transform في حاوية الفاحص Inspector، ونكتب ضمن خاصية الموضع Position القيمة 50 للمحور الأفقي x والقيمة 50 للمحور العمودي y لنحدد مكان ظهور عقدتنا داخل المشهد. كما يمكننا أيضًا النقر على الشخصية الرسومية Sprite وسحبها ضمن نافذة العرض، وسنرى أن قيم الموضع Position تتغير أثناء تحريكنا لها. إحدى الخصائص المهمة للعقد هي إمكانية ترتيبها في تسلسل هرمي من العقدة الأم إلى العقدة الابن، مما يتيح لنا تنظيم الكائنات داخل المشهد، على سبيل المثال يمكننا إنشاء عقدة رئيسية للاعب Player تحتوي على عقد فرعية متعددة مثل عقدة Sprite2D لتمثيل الشكل المرئي للاعب وعقدة فرعية أخرى AnimationPlayer لتحريك اللاعب. لنجرب إنشاء تسلسل عقد هرمي دعونا نحدد أولاً عقدتنا Sprite2D، ثم نضغط على زر الإضافة مرة أخرى لإضافة عقدة Sprite2D جديدة. بعد ذلك، لنسحب الأيقونة نفسها لليسار قليلًا، ونعين خاصية الخامة Texture الخاصة بهذه العقدة الجديدة. نلاحظ أن قيم الموضع Position للعقدة الأب تتغير أثناء تحريكها. لكن في حال فحص قيم الموضع Position للعقدة الابن سنجد أنها ما تزال (50,50). فقيمة خاصية التحويل Transform لها نسبية وتعتمد على عقدة الكائن الأب. إذا حركنا العقدة الأب، فإن جميع العناصر المرتبطة به أي كافة العقد الأبناء له ستتحرك معها تلقائيًا، لأنها مرتبطة به من حيث الموقع أو الدوران، أو أي تغييرات أخرى تجري عليه بينما إذا حركنا العقدة الابن فقط، فإن العقدة الأب لها لن تتأثر بحركتها المشاهد Scenes يمكن تشبيه المشهد Scenes في جودو على أنه حاوية تتضمن جميع كائنات أو عقد لعبتنا. يمكن أن تمثل هذه العقد شخصيات أو صور خلفية أو حتى الأكواد البرمجية التي تتحكم في كيفية تصرف الأشياء. ويمكن أن يكون المشهد بسيطًا ومكونًا كائن واحد أو معقدًا مثل مستوى كامل في اللعبة. فتجميع العقد مع بعضها البعض ضمن محرك ألعاب جودو يوفر لنا أداة قوية، ويمكّننا من إنشاء كائنات معقدة من وحدات البناء التي تمثلها العقد، فمثلًا قد تحتوي عقدة اللاعب Player في لعبتنا على العديد من العقد الأبناء المرتبطة بها مثل Sprite2D للعرض و AnimationPlayer لتحريكها و Camera2D لمتابعتها وغير ذلك. تُسمى مجموعة العقد المرتبة في بنية شجرية بالمشهد Scene، وسنوضح في المقالات اللاحقة من هذه السلسلة بشكل عملي كيفية استخدام المشاهد في تنظيم كائنات لعبتنا في أجزاء مستقلة تعمل جميعها مع بعضها البعض ونتعرف على الطرق الأمثل لإنشاء وتنظيم مشاهد لعبة جودو. الخاتمة وصلنا لختام مقالنا الذي تعرفنا فيه على مفهوم العقد Nodes في محرك الألعاب جودو Godot والتي تمثل اللبنات الأساسية التي يمكن من خلالها تطوير الألعاب بسهولة ومرونة. تسمح العقد بترتيب الكائنات داخل المشهد بشكل هرمي، مما يتيح للمطورين تنظيم الكائنات وإعطائها خصائص وسلوكيات متنوعة، من الرسومات إلى الرسوم المتحركة والنماذج ثلاثية الأبعاد. بالإضافة إلى ذلك، تعرفنا على مفهوم المشهد الذي يمثل مجموعة من العقد المرتبطة هرميًا ببعضها والذي يمكننا تصميم ألعاب جودو بطريقة منظمة. ترجمة -وبتصرّف- للقسم Nodes: Godot's building blocks من توثيقات Kidscancode. اقرأ أيضًا الرؤية التصميمية لمحرك اﻷلعاب جودو Godot طبقات الحاوية في جودو إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو إنشاء نسخ من المشاهد والعقد في جودو
-
سنتعرّف في هذه السلسلة من المقالات على محرك الألعاب جودو Godot بنسخته الرابعة والميزات التي يوفّرها، وسنبدأ مقال اليوم بشرح واجهة محرك جودو وأهم مكوناتها، ونوضّح التغييرات الرئيسية التي سنلاحظها عند الانتقال لهذا الإصدار من محرك الألعاب. ما هو محرك الألعاب جودو؟ سنتعرّف فيما يلي على محرك الألعاب جودو وسبب استخدامه والميزات التي يوفّرها. محركات الألعاب Game Engines يُعَد تطوير الألعاب أمرًا معقدًا ومثيرًا بنفس الوقت، وهو يتطلب مجموعة متنوعة من المعارف والمهارات، إذ يجب أن تتوفر لدينا الكثير من التقنيات الأساسية لبناء لعبة حديثة قبل أن تتمكّن من إنشائها، تخيل لو كان علينا بناء جهاز الحاسوب الخاص بنا وكتابة نظام التشغيل الخاص به قبل أن نبدأ حتى في البرمجة ألن يكون الأمر غاية في الصعوبة؟ سيكون تطوير الألعاب مشابهًا لذلك في حال أردنا البدء من الصفر وبناء كل شيء نحتاجه بأنفسنا. هنالك أيضًا عدد من الاحتياجات المشتركة لكل لعبة نريد بناءها، فمثلًا سنحتاج أي لعبة إلى رسم أشياء على الشاشة بغض النظر عن نوعها، لذا إذا كانت الشيفرة البرمجية اللازمة للرسم مكتوبة مسبقًا، فسيكون من المنطقي أن نعيد استخدامها بدلًا من إنشائها من جديد لكل لعبة، وهنا يأتي دور محركات الألعاب. فمحرّك الألعاب هو مجموعة من الأدوات والتقنيات المُصمَّمة لمساعدة مطوري الألعاب، مما يسمح لنا بالتركيز أكثر على بناء لعبتنا دون الحاجة لإعادة اختراع العجلة، حيث سنوضّح فيما يلي بعض الميزات التي يوفرها محرك الألعاب الجيد: التصيير أو الإخراج Rendering ثنائي الأبعاد وثلاثي الأبعاد: هو عملية عرض اللعبة على شاشة اللاعب، ويجب أن يعمل محرك التصيير الجيد مع ميزات وحدة معالجة الرسوميات GPU الحديثة، ويدعم الشاشات عالية الدقة، ويحسن المؤثرات مثل الإضاءة ومنظور اللعب Perspective مع الحفاظ في الوقت نفسه على معدّل إطارات مرتفع لضمان تجربة لعب سلسة الفيزياء Physics: يُعَد إنشاء محرك فيزيائي دقيق وقابل للاستخدام مهمة ضخمة، إذ تتطلب معظم الألعاب اكتشاف التصادم والاستجابة له، وتحتاج العديد منها إلى محاكاة فيزيائية مثل الاحتكاك والعطالة أو القصور الذاتي Inertia وغير ذلك، ولن يرغب المطورون بتكبد عناء كتابة الشيفرة البرمجية لهذه المهمة دعم المنصات: قد تحتاج لإصدار لعبتك على منصات متعددة كالهاتف المحمول والويب والحاسوب الشخصي والطرفية Console، فمحرّك الألعاب الجيد يتيح لنا بناء لعبتنا مرة واحدة وتصديرها إلى منصة أو منصات أخرى بيئة التطوير: تُجمَع كل هذه الأدوات في تطبيق واحد، مما يؤدي إلى وجود كل شيء في بيئة واحدة حتى لا تضطر إلى تعلّم سير عمل جديد لكل مشروع تعمل عليه هناك عشرات محركات الألعاب الشهيرة التي يمكننا الاختيار من بينها مثل جودو GoDot ويونتي Unity وآن ريل Unreal وغيرها. وتجدر الإشارة لأن غالبية محرّكات الألعاب الشهيرة هي منتجات تجارية، فقد تكون مجانية للتنزيل وقد لا تكون كذلك، وأنها قد تتطلّب اتفاقية ترخيص أو حقوق ملكية إذا أردنا إصدار ألعابنا وخاصة إذا حقّقت أرباحًا، لذا يجب قراءة وفهم ما نوافق عليه وما يُسمح وما لا يُسمح بفعله عندما ننوي استخدام أي محرك. لماذا نستخدم محرك ألعاب جودو يُعَد محرّك ألعاب جودو مجانيًا ومفتوح المصدر، وهو يصدر وفق ترخيص MIT المرن، فلا توجد رسوم أو تكاليف مخفية أو حقوق ملكية يجب دفعها، ويُعَد جودو محرك ألعاب حديث ومتكامل الميزات. وهو يوفر الكثير من الفوائد لمطوري الألعاب، لأنه غير مثقل بالترخيص التجاري، ويوفر لنا سيطرة كاملة على كيفية ومكان توزيع لعبتنا. كما أن طبيعة جودو مفتوحة المصدر توفر مستوى شفافية أعلى من محركات الألعاب التجارية، فمثلًا إذا وجدنا أن ميزة معينة لا تلبي احتياجاتنا تمامًا، فيمكننا تعديل المحرّك دون الحاجة إلى إذن لذلك. اكتشاف واجهة محرر الألعاب جودو سنكتشف فيما يلي واجهة محرك الألعاب جودو التي ستقضي فيها معظم وقتك عند بناء لعبتك. مدير المشروع Project Manager أول شيء سنراه عند فتح محرك ألعاب جودو هو نافذة مدير المشروع Project Manager، والتي ستبدو كما يلي: سنرى في هذه النافذة قائمة بمشاريع جودو، حيث يمكننا اختيار مشروع موجود مسبقًا والنقر على زر تشغيل Run لتشغيل اللعبة أو النقر على زر تحرير Edit للعمل على اللعبة في محرّر جودو. لنبدأ بالنقر على زر مشروع جديد New Project، في حال لم يكن لدينا أي مشاريع حتى الآن. يمكننا في النافذة السابقة إعطاء اسم للمشروع، وإنشاء مجلد لتخزينه ملفات لعبتنا فيه. ملاحظة: يوجد مشروع جودو في مجلد خاص به، ويمثل ذلك العديد من الفوائد بما في ذلك تسهيل نقل المشاريع ومشاركتها والنسخ الاحتياطي لها، ويجب أيضًا أن تكون جميع ملفات المشروع من صور وملفات صوتية وما إلى ذلك داخل مجلد المشروع. يجب أن نحرص على اختيار اسم يصف عمل مشروع لعبتنا عند تسميته، فمثلًا لا يعد الاسم New Game Project23 جيدًا لكونه لا يساعدنا في تذكر ما يفعله هذا المشروع. يجب أيضًا أن نفكر في التوافق، فقد تكون بعض أنظمة التشغيل حساسة لحالة الأحرف وبعضها الآخر غير حساس لحالة الأحرف، مما يؤدي لحدوث مشكلات إذا نقلنا المشروع أو شاركناه من حاسوب لآخر، لذا يعتمد العديد من المبرمجين قواعد تسمية موحّدة مثل عدم وجود مسافات بين الكلمات واستخدام شرطة سفلية _ بينها. سنسمّي المشروع الجديد getting_started، لذا نكتب هذا الاسم ونتأكد من تفعيل خيار أنشئ مجلد Create Folder، قبل النقر على زر إنشاء وتعديل Create & Edit أسفل النافذة. يمثل الشكل التالي نافذة محرر محرك الألعاب جودو، وهو المكان الذي سنقضي فيه معظم وقتنا عند العمل في جودو، ويكون المحرر مقسمًا إلى أقسام كما يلي: نافذة العرض Viewport: المكان الذي نرى فيه أجزاء لعبتنا أثناء العمل عليها مساحات العمل Workspaces: في الجزء العلوي الأوسط حيث يمكننا التبديل بين العمل في مساحات العمل ثنائية الأبعاد 2D أو ثلاثية الأبعاد 3D أو السكربت، ولكن تكون البداية من مساحة العمل ثلاثية الأبعاد أزرار اختبار اللعب Playtest Buttons: تتيح لنا هذه الأزرار تشغيل اللعبة والتحكم فيها أثناء الاختبار الحاويات Docks أو التبويبات Tabs: توجد على جانبي واجهة جودو عدد من الحاويات Docks والتبويبات حيث يمكننا من خلالها عرض عناصر اللعبة وضبط خاصياتها اللوحة السفلية Bottom Panel: تتضمن هذه اللوحة معلومات خاصة بالسياق لأدوات مختلفة، وأهمها لوحة الخرج Output، حيث سنرى رسائل الأخطاء أو المعلومات عند تشغيل لعبتنا إعدادات المشروع Project Settings تحدثنا عن الأجزاء الرئيسية لواجهة محرر جودو وكيفية عملها، ولنتحدّث الآن عن إعدادات المشروع، فإحدى المهام الأولى عند بدء مشروع جديد هي التأكد من صحة الإعدادات. ننقر على خيار مشروع Project من القائمة ثم نحدّد خيار إعدادات المشروع Project Settings. النافذة السابقة هي نافذة إعدادات المشروع، وتوجد على الجهة اليمنى قائمة من الفئات. تكون الإعدادات الافتراضية مناسبة لمعظم المشاريع، فلا حاجة للقلق بشأن تغييرها إلّا إن كان لدينا خيار محدد يتطلب التغيير. سنلقي نظرة حاليًا على قسم التطبيق Application ثم إعداد Config حيث يمكننا من هنا ضبط عنوان اللعبة، واختيار المشهد الرئيسي الذي سنوضّحه لاحقًا، كما يمكننا من هنا تغيير أيقونة اللعبة. القسم الثاني هو قسم الإظهار Display ثم نافذة Window، وهو المكان الذي يمكننا من تحديد طريقة ظهور لعبتنا وعرضها. إذ يمكننا ضبط العرض width والارتفاع height لضبط حجم نافذة اللعبة، فمثلًا إذا أنشأنا لعبة لهاتف محمول، فيجب ضبطها على دقة وأبعاد الجهاز المستهدف. توجد أيضًا إعدادات لتغيير الحجم Scaling والتمدّد Stretching ووضع ملء الشاشة وغير ذلك. سنترك الحجم الافتراضي كما هو حاليًا، حيث سنتحدث لاحقًا عن كيفية ضبط هذه الإعدادات بدقة لتشغيل اللعبة على أجهزة مختلفة. توجد أيضًا بعض التبويبات في الجزء العلوي من النافذة مثل التبويب عام General الذي تحدّثنا عنه. سنتحدث الآن بإيجاز عن تبويب خريطة الإدخال Input Map، وهو المكان الذي يمكننا فيه تحديد إجراءات إدخال مختلفة للتحكم في إجراءات الإدخال أي كيف نتعامل مع مدخلات لوحة المفاتيح والفأرة وغير ذلك. حيث نهتم في لعبتنا في تحديد المفتاح أو الزر الذي يضغط عليه اللاعب، وتحديد الإجراء الذي سيحدث عند الضغط عليه للتعامل مع مدخلات اللاعب بكفاءة. يوجد أيضًا تبويبات أخرى مثل تبويب التوطين Localization المخصص لدعم لغات متعددة، وتبويب إضافات Plugins التي أنشأ معظمها مجتمع محرك ألعاب جودو، والتي يمكن تنزيلها وإضافتها لتوفير مزيد من الميزات والأدوات المختلفة وغير ذلك وسنتحدث عنها لاحقًا. دليل الانتقال من الإصدار Godot 3.x للإصدار Godot 4.0 سنوضّح فيما يلي التغييرات الرئيسية والمشاكل التي يجب الانتباه إليها إذا أردتَ الانتقال إلى الإصدار 4.0 لمحرك ألعاب جودو. الأسماء الجديدة أكبر تغيير في الإصدار Godot 4 هو توفر مجموعة كاملة من عمليات إعادة التسمية للعقد والدوال وأسماء الخاصيات، حيث تجري معظم هذه العمليات لجعل الأمور متناسقة أو واضحة. إليك فيما يلي بعض التغييرات الكبرى التي يجب الانتباه إليها: العقد ثنائية الأبعاد 2D وثلاثية الأبعاد 3D: حملت العقد ثنائية الأبعاد في الإصدار Godot 3.x اللاحقة 2D، ولكن لم يكن للعقد ثلاثية الأبعاد لاحقة، لذا أصبحت الآن جميع العقد تحمل إما اللاحقة 2D أو اللاحقة 3D لجعل الأمور متناسقة مثل RigidBody2D و RigidBody3D أعيدت تسمية العقدة Spatial إلى Node3D في الفئة ثلاثية الأبعاد لتتناسب معها أعيدت تسمية واحدة من أكثر العقد شهرة وهي KinematicBody إلى CharacterBody2D أو CharacterBody3D، وسنوضّح لاحقًا مزيدًا من تغييرات الواجهة البرمجية API لهذه العقدة. أعيدت تسمية الدالة instance() الخاصة بالعقدة PackedScene إلى instantiate() حلت الخاصيات position و global_position محل الخاصيات translation و global_translation للعقد ثلاثية الأبعاد، مما يجعلها متوافقة مع العقد ثنائية الأبعاد الإشارات Signals والعناصر القابلة للاستدعاء Callables أصبح العمل مع الإشارات منظمًا أكثر في الإصدار 4.0، حيث أصبح النوع Signal نوعًا أصيلًا الآن، لذا سنستخدم عددًا أقل من السلاسل النصية، مما يعني أننا سنحصل على ميزة الإكمال التلقائي والتحقق من الأخطاء. ينطبق الأمر ذاته أيضًا على الدوال، والتي يمكن الآن الرجوع إليها مباشرةً بدلًا من استخدام السلاسل النصية. فيما يلي مثال لتعريف إشارة وتوصيلها وإرسالها: extends Node signal my_signal func _ready(): my_signal.connect(signal_handler) func _input(event): if event.is_action_pressed("ui_select"): my_signal.emit() func signal_handler(): print("signal received") عناصر الانتقال التدريجي Tweens في حال استخدام SceneTreeTween في الإصدار Godot 3.5، فسنكون على دراية باستخدام عناصر الانتقال التدريجي Tween التي تُستخدَم مثلًا لتغيير اللون أو الموقع الخاص بالكائن تدريجيًا في الإصدار Godot 4.0. لم يَعُد Tween عقدة، لذا يمكننا إنشاء كائنات رسوم متحركة للانتقال التدريجي Tween لمرة واحدة كلما احتجنا إليها، وستصبح أكثر قوة وأسهل في الاستخدام من الطريقة القديمة بعد التعود عليها. العقدة AnimatedSprite[2D|3D] يُعَد اختفاء الخاصية playing أكبر تغيير لمستخدمي الإصدار 3.x لهذه العقدة، حيث أصبحت أكثر تناسقًا مع استخدام AnimationPlayer، إذ يمكن تبديل التشغيل التلقائي في لوحة SpriteFrames لتشغيل الرسوم المتحركة تلقائيًا. وعلينا استخدام الدالتين play() و stop() في الشيفرة البرمجية للتحكم في التشغيل. العقدة CharacterBody[2D|3D] أكبر تغيير في هذه العقدة هو استخدام الدالة move_and_slide() التي لم تَعُد تستقبل معاملات، حيث فقد أصبحت جميع المعاملات خاصيات مُدمَجة، ويتضمن ذلك الخاصية velocity الأصيلة، لذا فلا حاجة للتصريح عن هذه الخاصيات. يمكن الاطلاع على محارف المنصة ومحارف FPS الأساسية للحصول على أمثلة تفصيلية لاستخدام هذه العقد. عقدة TileMap جُدّدت العقدة TileMap بالكامل في الإصدار 4.0 ابتداءً من كيفية إنشاء موارد TileSet إلى كيفية رسم عناصر الرقعة Tiles والتفاعل معها. مولّد الأعداد العشوائية RNG هناك بعض التغييرات على دوال توليد الأعداد العشوائية المُدمَجة مع لغة البرمجة GDScript، وهذه التغييرات هي: لم نعد بحاجة لاستدعاء الدالة randomize()، إذ سيكون الاستدعاء تلقائيًا. إذا أردنا الحصول على عشوائية قابلة للتكرار، نستخدم الدالة seed() لضبطها على قيمة محدَّدة مسبقًا. حلّت الدالة randf_range() للأعداد العشرية أو الدالة randi_range() للأعداد الصحيحة محل الدالة القديمة rand_range(). كشف تصادم الأشعة Raycasting توجد واجهة برمجة تطبيقات جديدة عند كشف تصادم الأشعة لاكتشاف التصادم بين الكائنات في الشيفرة البرمجية، حيث تأخذ الدالة PhysicsDirectSpaceState[2D|3D].intersect_ray() كائنًا خاصًا كمعامل، ويحدّد هذا الكائن خاصيات الشعاع، فمثلًا نستخدم ما يلي لكشف تصادم الأشعة في فضاء ثلاثي الأبعاد: var space = get_world_3d().direct_space_state var ray = PhysicsRayQueryParameters3D.create(position, destination) var collision = space.intersect_ray(ray) if collision: print("ray collided") الخاتمة تعرفنا في هذا المقال على محرك الألعاب جودو بنسخته الرابعة، واكتشفنا أهم ميزاته، كما استعرضنا واجهته الرئيسية مثل مدير المشروع ومحرر الألعاب وتعرفنا على كيفية استخدامه لإنشاء مشاريع جديدة وضبط الإعدادات. بالإضافة إلى ذلك، سطنا الضوء على أبرز التغييرات التي سنلاحظها عند الانتقال من الإصدار 3.x إلى الإصدار 4.0، مثل إعادة تسمية العقد والدوال وتعديل طريقة العمل مع الإشارات والعناصر القابلة للاستدعاء. سنعود لنافذة إعدادات المشروع لاحقًا، لذا لنغلقها الآن ونستعد للانتقال إلى مقالنا التالي من هذه السلسلة والذي سنشرح فيه مفهوم العقد Nodes وطريقة التعامل معها في محرك الألعاب جودو. ترجمة -وبتصرّف- للأقسام What is Godot و The Godot Editor و Migrating from 3.x من توثيقات Kidscancode. اقرأ أيضًا الرؤية التصميمية لمحرك اﻷلعاب جودو Godot كتابة برنامجك الأول باستخدام جودو Godot العقد Nodes والمشاهد Scenes في جودو Godot تعرف على أشهر محركات الألعاب Game Engines