بعد أن أنشأنا في المقال السابق ملفات مشروع لعبة "تفادي الزواحف" ونظمناه، سنبدأ في هذا المقال بالعمل على شخصيات اللعبة (لاعب أساسي وأعداء). إذ سنبني المشهد اﻷول Player
(وهو كائن أو عقدة) للاعب وآخر Mob
للأعداء، ومن ميزات إنشاء مشهد مستقل لكل منهما هو إمكانية اختبارها بشكل مستقل وقبل أن ننشئ بقية أجزاء اللعبة.
هيكلية العقدة
نحتاج بداية إلى عقدة جذرية للاعب، وكقاعدة عامة، لا بد أن تعكس العقدة الجذرية للمشهد الوظيفة المرغوبة للكائن وما هو هذا الكائن. لهذا، انقر على زر "عقدة أخرى" وأضف عقدة من نوع Area2D
إلى المشهد:
سيعرض جودو أيقونة تنبيه إلى جوار العقدة في شجرة المشاهد، تجاهلها اﻵن وسنعود إليها لاحقًا.
نتمكن باستخدام Area2D
من استشعار الكائنات التي تتداخل أو تعمل ضمن اللاعب، وسنغير اسم العقدة الجديدة إلى Player
بالنقر المضاعف عليها. وبعد إنشاء العقدة الجذرية، سنضيف عقدًا إضافية لمنحها مقدرات وظيفية أكبر. لكن قبل ذلك، علينا أن نتأكد من عدم تحريك أو تغيير العقد اﻷبناء عند النقر عليهم. لهذا انقر على اﻷيقونة الواقعة على يسار أيقونة القفل في شريط أدوات المشهد (سيعرض لك وصف اﻷداة عند تمرير مؤشر الفأرة فوقها العبارة "اجعل فروع العقدة المختارة غير
قابلة للاختيار"):
احفظ المشهد بالنقر على مشهد>حفظ أو اضغط Ctrl + S في ويندوز ولينكس أو Cmd + S في ماك أو إس.
سوف نستخدم في مشروعنا أسلوب التسمية المتبع في محرك الألعاب جودو وهو كالتالي حسب لغة البرمجة المستخدمة:
-
في لغة GDScript: نتبع أسلوب باسكال في اﻷصناف (الحرف اﻷول من كل كلمة كبير)، وفي الدوال والمتغيرات أسلوب اﻷفعى (تفصل بين كل كلمتين
شرطة سفلية_
)، أما الثوابت فتكتب كل حروفها بالشكل الكبير. - في لغة #C: تسمى الأصناف والمتغيرات والتوابع بأسلوب باسكال، ونستخدم في تعريف الحقول الخاصة private والمتغيرات المحلية والمعاملات أسلوب سنام الجمل (الحرف اﻷول من كل كلمة كبير ما عدا الكلمة اﻷولى). وتأكد من كتابة أسماء التوابع بشكل دقيق عند ربط اﻹشارات.
الرسم المتحرك للشخصية (Sprite)
انقر على أيقونة العقدة Player
وأضف عقدة ابن من نوع AnimatedSprite2D
(استخدم Ctrl + A في ويندوز ولينكس) والتي تتولى أمور مظهر وتحريك اللاعب، ولاحظ وجود إشارة تحذير إلى جانب العقدة. تحتاج العقدة موردًا يُدعى "إطارات السبرايت SpriteFrames"، وﻹنشائه، ابحث عن الخاصية SpriteFrames
ضمن النافذة الفرعية Animation
في حاوية "الفاحص" ثم انقر على مربع النص empty واختر "جديدة SpriteFrame". انقر مجددًا لفتح لوحة "إطارات-اﻷرسومة".
ستجد إلى اليمين قائمة بالرسومات، انقر على الافتراضية وسمها "walk"، ثم انقر على أيقونة إضافة إطار في الزاوية العليا اليمينية وأضف إطارًا آخر سمِّه "up". ابحث بعد ذلك عن الصور المناسبة في المجلد "art" في نظام الملفات وانقل الصور playerGrey_walk[1/2]
إلى اﻹطار "walk" بالسحب واﻹفلات، أو بفتح الصورة من خلال أيقونة المجلد وكرر العملية بنقل الصورتين playerGrey_up[1/2]
إلى اﻹطار "up".
إن أبعاد الصور أكبر من أبعاد نافذة اللعبة، ولا بد من تصغير هذه الصور بالنقر على العقدة AnimatedSprite2D
ومن ثم ضبط الخاصية Scale
على القيمة Scale
. ستجد هذه الخاصية في حاوية الفاحص تحت العنوان Node2D
والقائمة "Transform تحويل":
أضف أخيرًا عقدة من النوع CollisionShape2D
لتكون ابنًا للعقدة Player
، وتحدد هذه العقدة "صندوق التصادم" المحيط باللاعب أو حدود منطقة التصادم المحيطة به. وتلائمنا في هذا الصدد كائن من النوع CapsuleShape2D
، لهذا انقر في "الفاحص" على المربع إلى جوار العنوان واختر "جديدة CapsuleShape2D". استخدم بعد ذلك مقبضي التحكم بالأبعاد (النقطتين الحمراوين) في نافذة المشهد لتغطية الأرسومة بالغلاف:
عندما تنتهي من ذلك سيكون شكل مشهد اللاعب Player
كالتالي:
تأكد من حفظ المشهد مجددًا بعد هذه التغييرات. سنضيف تاليًا سكربت إلى عقدة اللاعب لتحريكه ثم نُعدّ آلية لترصد التصادم لنعرف إذا ما اصطدم اللاعب بشيء ما.
كتابة الشيفرة اللازمة لتحريك اللاعب
سنعمل في هذا القسم على كتابة شيفرة لتحريك اللاعب، وإعداده ليترصد التصدامات، لهذا، علينا إضافة بعض الخواص الوظيفية التي لا تقدمها العقد المتوفرة عن طريق إضافة سكربت أو كود برمجي إلى العقدة. انقر على العقدة Player
ثم انقر على "إلحاق نص برمجي":
لا داعي لتغيير أي شيئ في نافذة إلحاق نص برمجي، اترك كل شيء كما هو وانقر على زر "أنشئ".
ملاحظة: إن كنت تريد إنشاء سكربت #C، اختر هذه اللغة من القائمة المنسدلة قبل النقر على "أنشئ".
extends Area2D @export var speed = 400 # How fast the player will move (pixels/sec). var screen_size # Size of the game window.
تُسمح لنا التعليمة export
قبل المتغير speed
بضبط قيمته في نافذة الفاحص. ولهذا اﻷمر فائدته إن أردت تعديل قيمة المتغير بالطريقة نفسها التي تعدّل فيها خاصيات أي عقدة موجودة أصلًا في جودو. انقر اﻵن على العقدة Player
وسترى الخاصية موجودة ضمن قسم "متغيرات السكربت" في حاوية الفاحص (تحت نفس الاسم الذي يحمله ملف السكربت). وتذكر أن تغيير القيمة في هذا المكان سيلغي القيمة التي يحملها المتغير في السكربت.
اقتباستحذير
إن كنت تستخدم #C، لا بد من إعادة بناء ملفات التجميع assemblies المشروع كلما أردت عرض المتغيرات أو اﻹشارات المصدّرة إلى المحرر من الشيفرة. يمكنك بناء المشروع يدويًا بالنقر على الزر "بناء Build" أعلى يمين المحرر. كما تستطيع بدء عملية البناء من لوحة MSBuild بالنقر على كلمة MSBuild في أسفل نافذة المحرر لعرض اللوحة ثم النقر على الزر "Build".
يتضمن السكربت player.gd
تلقائيًا الدالتين ()ready_
و ()process_
. فإن لم تختر القالب الافتراضي للسكربت أنشئ هاتين الدالتين. وتُستدعى الدالة ()ready_
عندما تدخل عقدة شجرة المشاهد وهو وقت مناسب لمعرفة أبعاد نافذة اللعبة
func _ready(): screen_size = get_viewport_rect().size
بإمكاننا اﻵن استخدام الدالة ()process_
لتحديد ما يفعله اللاعب، وتُستدعى هذه الدالة من أجل كل إطار ونستخدمها لتحديث العناصر في لعبتنا والتي نتوقع أن تتغير أحيانًا. فمن أجل لاعبنا لا بد من:
- التحقق من وجود دخل.
- تحريكه في الاتجاه المطلوب.
- تشغيل الرسوم المتحركة المناسبة.
كما ذكرنا علينا بداية التحقق من الدخل، أي هل يضغط اللاعب على زر معين مثلًا؟ ففي لعبتنا هناك عناصر إدخال لأربعة اتجاهات علينا أن نتحقق منها. عُرّفت إجراءات الدخل في إعدادات المشروع تحت عنوان "خريطة الإدخال". وفيها نستطيع تعريف أحداث مخصصة وتعيين أزرار مختلفة، وأحداث تتعلق بالفأرة وغيرها من المدخلات.
انقر على المشروع، ثم إعدادات المشروع لتفتح نافذة اﻹعدادات، ثم انقر على النافذة الفرعية "خريطة الإدخال" في الأعلى. اكتب بعد ذلك "move_right" (تحرك يمينًا) في الشريط العلوي وانقر الزر "أضف" ﻹضافة الإجراء move_right
.
علينا اﻵن أن نربط اﻹجراء بزر معين، لهذا انقر على أيقونة "+" إلى اليسار كي نفتح نافذة "تهيئة الحدث event configuration".
كل ما عليك الآن هو النقر على الزر الذي تريد ربطه بالحدث سواء زر لوحة مفاتيح أو زر الفأرة. انقر الآن مفتاح السهم اليميني على لوحة المفاتيح وسيظهر الخيار تلقائيًا في مربع "يتم رصد المدخلات.." انقر بعد ذلك على "حسنًا" لتعيين المفتاح.
كرر نفس الخطوات لربط الحركات الثلاث الباقية كالتالي:
-
اربط
move_left
بالسهم اليساري. -
اربط
move_up
بالسهم للأعلى. -
اربط
move_down
بالسهم للأسفل.
يجب أن تظهر خارطة المدخلات كالتالي:
انقر اﻵن على "إغلاق" ﻹغلاق إعدادات المشروع.
ملاحظة: ربطنا مفتاح واحد بكل إجراء دخل، لكنك تستطيع أن تربط أكثر من مفتاح أو زر عصا تحكم أو زر فأرة بإجراء الدخل نفسه.
تستطيع أن تقرر إذا ما ضُغط زر باستخدام العبارة ()Input.is_action_pressed
التي تعيد القيمة true
إذا ضُغط الزر وfalse
إن لم يُضغط.
func _process(delta): var velocity = Vector2.ZERO # The player's movement vector. if Input.is_action_pressed("move_right"): velocity.x += 1 if Input.is_action_pressed("move_left"): velocity.x -= 1 if Input.is_action_pressed("move_down"): velocity.y += 1 if Input.is_action_pressed("move_up"): velocity.y -= 1 if velocity.length() > 0: velocity = velocity.normalized() * speed $AnimatedSprite2D.play() else: $AnimatedSprite2D.stop()
بدأنا بضبط قيمة velocity
على (0, 0)
فلن يتحرك اللاعب افتراضيًا، وبعد ذلك تحققنا من كل المدخلات وأضفنا إلى المتغير velocity
أو طرحنا منه للحصول على الاتجاه. فلو ضغطنا على السهم اليميني واليساري في نفس الوقت ستكون نتيجة المتجه velocity
هي (1, 1)
، وفي هذه الحالة نكون قد أضفنا حركة أفقية وعمودية في نفس الوقت، وسيتحرك اللاعب بشكل أسرع بالاتجاه القطري موازنة بالحالة التي يتحرك فيها أفقيًا فقط. لكن بإمكاننا منع حدوث هذا اﻷمر بتسوية قيمة السرعة بأن نضبط قيمتها على 1 ثم نضربه بالقيمة المطلوبة ولن تكون السرعة في الاتجاه القطري عندها أكبر.
اقتباستلميح
إن لم تستخدم المتجهات سابقًا أو تحتاج إلى تذكرها يمكنك الاطلاع على المقال Vector math.
كما علينا أن تحقق فيما لو تحرّك اللاعب كي نستدعي الدالتين ()play
و ()stop
في AnimatedSprite2D
اقتباستلميح
$
هو اختصار للدالة()get_node
. فالكتابة()AnimatedSprite2D.play$
تماثل العبارة البرمجية()get_node("AnimatedSprite2D").play
يُعيد $
عقدة معينة إن كانت موجودة في نفس المسار النسبي ويعيد null
إن لم تكن موجودة في هذا المسار. وطالما أن AnimatedSprite2D هي عقدة ابن للعقدة الحالية، بإمكاننا استخدام AnimatedSprite2D$
.
وطالما حددنا الآن اتجاه الحركة، بإمكاننا تحديث موقع اللاعب. كما نستطيع باستخدام الدالة ()clamp
منع اللاعب من مغادرة الشاشة وتقييده ضمن مجال محدد. أضف الشيفرة التالية إلى أسفل الدالة ()process_
(انتبه إلى أن الشيفرة غير منزاحة تحت else
?
position += velocity * delta position = position.clamp(Vector2.ZERO, screen_size)
اقتباستلميح
يشير المعامل
delta
إلى طول اﻹطار (الوقت الذي يحتاجه اﻹطار حتى يكتمل). ويضمن استخدام هذه القيمة اتساق الحركة حتى لو تغيّر معدل اﻹطارات.
انقر على الزر "شغل المشهد الحالي" (F6 أو Cmd+R في ماك أو إس) وتأكد من قدرتك على تحريك اللاعب ضمن المشهد في جميع الاتجاهات.
اقتباستحذير:
إن ظهر خطأ في نافذة "منقح اﻷخطاء" ينص على التالي: "Attempt to call function 'play' in base 'null instance' on a null instance"، فمن المحتمل أن يعني هذا الخطأ أن اسم العقدة مكتوب بطريقة خاطئة، فأسماء العقد حساسة لحالة اﻷحرف ولا بد أن يُطابق
NodeName$
الاسم الذي تراه في شجرة المشاهد.
اختيار الرسوم المتحركة
بإمكاننا تحريك اللاعب الآن، لكننا نحتاج إلى تغيير الرسم المتحرك الذي يمثّل الكائن وفقًا لاتجاهه. ليدنا الرسم "تحرّك" والذي يعرض اللاعب وهو يتحرك يمينًا، ولا بد من قلبه أفقيًا حتى يعبّر عن التحرك نحو اليسار باستخدام الخاصية flip_h
. وكذلك لدينا الرسم "up" الذي يجب أن يُعكس عموديًا لتمثيل الحركة نحو اﻷسفل باستخدام الخاصية flip_v
. لهذا عليك اضافة الشيفرة التالية إلى أسفل الدالة ()process_
:
if velocity.x != 0: $AnimatedSprite2D.animation = "walk" $AnimatedSprite2D.flip_v = false # اطلع على املاحظة التالية بخصوص اﻹسناد المنطقي $AnimatedSprite2D.flip_h = velocity.x < 0 elif velocity.y != 0: $AnimatedSprite2D.animation = "up" $AnimatedSprite2D.flip_v = velocity.y > 0
ملاحظة: يُعد استخدام طريقة اﻹسناد المنطقي في هذه الشيفرة اختصارًا شائعًا. فما نفعله هو اختبار موازنة (منطقي) وإسناد قيمة منطقية، لهذا يمكننا تنفيذ اﻷمرين معًا. وما يفعله هذا الاختصار مطابق لعمل الشيفرة التالية:
if velocity.x < 0: $AnimatedSprite2D.flip_h = true else: $AnimatedSprite2D.flip_h = false
شغّل المشهد وتأكد من تغيّر الرسم مع تغير اتجاه الحركة.
اقتباستلميح
من اﻷخطاء الشائعة هنا كتابة أسماء الرسومات بشكل خاطئ. ولا بد أن يكون اسم الرسم المتحرك في لوحة "إطارات الرسومات المتحركة" مطابقًا لاسمه في الشيفرة. فإن اسميت الرسم
"Walk"
مثلًا فلا بد أن يبدأ بحرف كبير أيضًا في الشيفرة.
عندما تتأكد أن كل شيء يعمل كما يجب، أضف السطر التالي إلى الدالة ()ready_
كي يختفي اللاعب في بداية اللعبة.
hide()
إعداد التصادمات
نريد من اللاعب Player
أن يعرف متى يستطدم بالعدو، لكننا لم نصنع اﻷعداء بعد! لا بأس بذلك لأننا سنستخدم حاليًا إشارات جودو لننجز اﻷمر. أضف اﻷسطر التالية إلى أعلى السكربت. فإن كنت تستخدم GDScript، أضفها بعد العبارة extends Area2D
، وإن كنت تستخدم لغة #C ضعها بعد العبارة
public partial class Player: Area2D
.
signal hit
تُعرّف التعليمة السابقة إشارة خاصة باسم "hit" يبثها اللاعب (يرسلها) عندما يتصادم مع عدو. وسنستخدم الكائن Area2D
لالتقاط هذه اﻹشارة. اختر العقدة Player
وانقر على النافذة الفرعية "عقدة" ضمن لوحة "الفاحص" كي تعرض قائمة اﻹشارات التي يمكن للاعب بثها:
لاحظ وجود إشارتنا المخصصة "hit" أيضًا ضمن تلك القائمة. وطالما أن العدو سيكون عقدة من النوع RigidBody2D
، سنحتاج إلى الإشارة body_entered(body: Node2D)
. أوجد تلك اﻹشارة في القائمة ثم انقر عليها بالزر اليميني واختر "يتصل" لتظهر نافذة "قم بوصل اﻹشارة إلى دالة". لا حاجة لتغيير أي شيء، بل انقر فقط على "وصل" وسيوّلد جودو تلقائيًا الدالة المناسبة في الشيفرة:
لاحظ اﻷيقونة الخضراء إلى يسار الشيفرة المخصصة للإشارة وتدل على أن إشارة متصلة مع هذه الدالة. أضف اﻵن الشيفرة التالية إلى الدالة:
func _on_body_entered(body): hide() # يختفي اللاعب بعد أن يصطدم. hit.emit() # Must be deferred as we can't change physics properties on a physics callback. $CollisionShape2D.set_deferred("disabled", true)
في كل مرة يصدم بها العدو اللاعب ستُرسل اﻹشارة، ولا بد من تعطيل التصادم الخاص باللاعب كي لا نفعّل اﻹشارة hit
أكثر من مرة.
ملاحظة: قد ينتج عن تعطيل غلاف التصادم الخاص بالمنطقة خطأ إن حدث اﻷمرأثناء معالجة المحرّك للتصادمات. لهذا استخدم الدالة ()set_deferred
ﻹخبار المحرّك ألا يعطّل غلاف التصادم حتى يرى أن اﻷمر آمن.
أضف أخيرًا دالة نستدعيها ﻹعادة ضبط اللاعب عندما تبدأ لعبة جديدة
func start(pos): position = pos show() $CollisionShape2D.disabled = false
إنشاء شخصية العدو
حان الوقت اﻵن ﻹنشاء اﻷعداء الذي يجب على اللاعب تفاديهم. ولن يكون سلوكهم معقدًا جدًا بل سيتحركون عشوائيًا على أطراف الشاشة، يأخذون اتجاهًا عشوائيًا ويتحركون وفق خط مستقيم. نبدأ عملنا بإنشاء مشهد باسم Mob
يشكل الأساس الذي نشتق منه أي عدد نحتاجه من هذه الكائنات في لعبتنا.
إعداد العقدة
انقر على مشهد>مشهد جديد ثم أضف العقد التالية وفق الترتيب المبين:
-
RigidBody2D:
-
AnimatedSprite2D
-
CollisonShape2D
-
VisibleOnScreenNotifier2D
ولا تنسَ ضبط العقدة اﻷم كي لا يمكن اختيار اﻷبناء كما فعلنا سابقًا عند بناء شخصية اللاعب. اختر بعد ذلك العقدة Mob
ثم اضبط قيمة الخاصية Gravity Scale
على 0
، وذلك في قسم RigidBody2D ضمن الفاحص. يمنع هذا اﻷمر الأعداء من السقوط للأسفل.
افتح المجموعة "Collision" الموجودة في اللوحة "CollisionObject2D" تحت "RigidBody2D" ضمن الفاحص. الغ بعد ذلك تفعيل الخيار 1
ضمن الخاصية Mask
بالنقر عليه كي لا تتصادم اﻷعداء فيما بينها.
اضبط العقدة كما فعلنا في مشهد اللاعب، وهنا نستخدم ثلاث رسومات هي fly
و swim
و walk
، وهنالك صورتان لكل مشهد في المجلد "art". تُضبط الخاصية Animation Speed
(سرعة التحريك) لكل رسم متحرك على حدى، لهذا اضبط كلًا منها على 3
:
بإمكانك اﻵن النقر على الزر "تشغيل الرسم المتحرك" إلى يسار "سرعة التحريك" لعرض الرسوم المتحركة.
سنختار إحدى هذه الرسوم عشوائيًا حتى يكون للأعداء أشكال مختلفة، وكما هو الحال مع رسومات اللاعب لا بد من تصغير هذه الرسومات، وذلك بضبط الخاصية Scale
على (0.75, 0.75)
(راجع فقرة إنشاء اللاعب لتتذكر كيفية العمل).
علينا اﻵن أن نضيف غلاف CapsuleShape2D
من أجل التصادمات كما فعلنا مع اللاعب. ولكي يتماشى الغلاف مع الرسم المتحرك لا بد من تدويره بضبط الخاصية Rotation Degrees
على 90
(تحت لوحة "Node2D" والقائمة "Transform" ضمن الفاحص).
كتابة شيفرة تحريك العدو
أضف سكربت إلى العقدة Mob
كما فعلنا سابقًا:
extends RigidBody2D
نشغّل باستخدام الدالة ()ready_
الرسومات ونختار عشوائيًا أحد اﻷنواع الثلاث لهذه الرسوميات كالتالي:
func _ready(): var mob_types = $AnimatedSprite2D.sprite_frames.get_animation_names() $AnimatedSprite2D.play(mob_types[randi() % mob_types.size()])
ما تفعله هذه الشيفرة هو الحصول على أسماء الرسومات من الخاصية frames
للعقدة AnimatedSprite2D
، وستكون النتيجة مصفوفة تضم اﻷنواع الثلاث: ["walk", "swim", "fly"]
. ثم نختار عشوائيًا رقمًا بين 0 و 2 لاختيار أحد اﻹطارات الثلاث من المصفوفة السابقة (يبدأ العدد في المصفوفات من 0) بتطبيق التعليمة randi() % n
، والتي تختار عددًا صحيحًا عشوائيًا بين 0
و n-1
.
وأخيرًا نحتاج إلى شيفرة كي يحذف العدو نفسه عندما يغادر شاشة اللعبة. ولتنفيذ ذلك صل الإشارة ()Screen_exited
العائدة للعقدة ()VisibleOnScreenNotifier
إلى العقدة Mob
(راجع فقرة وصل إشارة اللاعب التي نفّذناها سابقًا) ثم أضف الأمر ()queue_free
إلى الدالة التي تظهر في السكربت كالتالي:
func _on_visible_on_screen_notifier_2d_screen_exited(): queue_free()
وهكذا سيكتمل مشهد العدو.
الخلاصة
بهذا نكون قد وصلنا لنهاية مقالنا الحالي الذي أنشأنها فيه مشهدين من مشاهد اللعبة ثنائية الأبعاد في محرك جودو، تابع معنا المقال التالي الذي سنقوم فيه بضم المشهدين معًا والسماح للأعداء بالتكاثر على الشاشة والحركة لتحويل المشاهد إلى لعبة تعمل كما خططنا لها.
ترجمة -وبتصرف- للمقالات: Creating the Player scene و Coding the player و Creating the enemy
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.