بعد أن بنينا شخصية لاعب قادر على إطلاق النار على العدو في لعبتنا ثنائية الأبعاد ضمن محرك الألعاب جودو، لا بد من إنشاء عدو في اللعبة ونبرمج طريقة حركته في اللعبة وطريقة إضافة آلية إطلاق النار إليه ليهاجم اللاعب، وهذا ما سنتعلمه في مقالنا الذي هو جزء من سلسلة دليل جودو.
بناء المشهد
سنستخدم العقدة Area2D
كأساس لبناء المشهد لأننا بحاجة إلى اكتشاف تداخل كائن العدو مع اللاعب أو قذائفه، كما سنحتاج إلى عدة عقد نلخصها كالتالي:
Enemy: Area2D Sprite2D CollisionShape2D AnimationPlayer MoveTimer: Timer ShootTimer: Timer
نختار العقدة Enemy ثم ننقر على النافذة عقدة Node إلى جوار الفاحص inspector وننقر بعدها على مجموعات Groups، ثم على أيقونة +
لإضافة مجموعة جديدة سنسميها enemies.
ملاحظة: من المهم هنا تذكر أن السكريبت في مشهد القذائف يبحث عن كائنات من مجموعة الأعداء enemies.
سننتقل بعد ذلك إلى العقدة Sprite2D
ثم نضيف الصورة Bon_Bon (16 x 16).png
إلى الخاصية Texture
ونضبط قيمة الخاصية Animation>Hframes
على 4
.
ملاحظة: تمثل الصورة Bon_Bon (16 x 16).png
مجموعة من الإطارات الرسومية التي تشكل الحركة فكل إطار يمثل وضعية أو جزءًا من هذه الحركة، وعند ضبط Hframes إلى 4 معناه أننا نخبر جودو بأن الصورة مقسمة أفقيًا إلى 4 إطارات، مما يسمح له بالتعامل مع كل إطار على حدة فإذا كانت الصورة بحجم 16 × 16 بكسل فعند تقسيمها إلى 4 إطارات أفقية سيكون كل إطار بعرض 4 بكسل وارتفاع 16 بكسل ويمكن عرض كل إطار بشكل منفصل لتكوين الحركة.
ننقر الآن على العقدة CollisonShape2D
ثم ننتقل إلى الخاصية Shape
ونختار شكل التصادم RectangleShape2D
ونضبط حجمه ليناسب أبعاد الأيقونة؛ ثم ننتقل إلى أيقونتي المؤقتات ونفعّل الخاصية One Shot
لكل مؤقت.
ننقر على العقدة AnimationPlayer
وننتقل إلى لوحة التحريك Animation Panel، وننشئ رسمًا متحركًا باسم bounce ونضبطه ليكرر نفسه looping وأن يبدأ العمل تلقائيًا عند تشغيل اللعبة autoplay بالنقر على الأزرار الموافقة.
بعد هذه المرحلة، سنضبط الخيار انطباق أو محاذاة Snap في أسفل اللوحة على القيمة 0.05
.
سننشئ الآن مسارات تحريك لتتبع الخاصيتين Textrure
و Hframes
للشخصية وهي بحالتنا العقدة Sprite2D
، وذلك بالنقر على أيقونة المفتاح إلى جانب كل منهما. وسنحتاج إلى مفاتيح مرجعية على هذه المسارات لاحقًا عندما نضيف رسمًا متحركًا جديدًا باسم explode يستخدم قيمًا مختلفة لهاتين الخاصيتين.
سنضيف الآن مسارًا تحريكيًا للخاصية Frame
بالنقر بدايةً على المفتاح إلى جانب الخاصية، ونبدأ بعدها بإنشاء مفتاح مرجعي يتغير عنده إطار الشخصية وفق الترتيب التالي 2>1>0>3>0، بحيث يفصل بين كل مفتاحين 0.1 ثانية ماعدا آخر إطارين سيكونان متداخلين.
لتنفيذ العملية، سنحرك المقبض الأزرق على الخط الزمني مسافة 0.1 ثانية ونضع قيمة الإطار المطلوب في مربع الخاصية Frame، ثم ننقر على زر المفتاح وهكذا. وسينتج عن تحريك الرسم بهذا الشكل أيقونة تنمو وتعود إلى حجمها الطبيعي في النهاية. وسيبدو شكل لوحة التحريك كالتالي:
ننقر على زر التشغيل في لوحة التحريك لرؤية نتيجة العمل، وبإمكاننا طبعًا تعديل الحركة حسب رغبتنا.
نضيف الآن رسمًا متحركًا جديدًا باسم explode ونضبط مدته على 0.4 ثانية، كما نستبدل صورة الشخصية بالصورة Explosion (16 x 16).png
وننشئ مسارات للخاصيات Frame
و Texture
و Hframes
.
سنعدّل قيمة الخاصية Hframes
إلى 5
، ثم كما فعلنا سابقًا، ونضع مفتاحين مرجعيين في المسار الأول عند اللحظة 0، وتكون قيمة الإطار 0 والثانية عند اللحظة 0.4، كما ستكون قيمة الإطار 4.
عند الانتهاء من كل ذكل، سنشغل الرسم المتحرك لرؤية النتيجة.
سكريبت شخصية العدو
ينتشر الأعداء أعلى شاشة اللعبة على شكل شبكة، حيث ستقترب شبكة الأعداء من اللاعب بعد فترة زمنية محددة، ثم يعود إلى الأعلى ما لم يُدمر منها، كما سيطلق الأعداء النار على اللاعب دوريًا.
سنضيف سكريبتًا إلى عقدة العدو ونعرّف المتغيرات التالية:
extends Area2D var start_pos = Vector2.ZERO var speed = 0 @onready var screensize = get_viewport_rect().size
يتتبع المتغير start_pos
موقع انطلاق الأعداء كي يعودوا إلى موقعهم الأصلي بعد الحركة. سنضبط قيمة المتغير عندما ننشر الأعداء في الشبكة، وسنستدعي الدالة ()start
الخاصة بهم:
func start(pos): speed = 0 position = Vector2(pos.x, -pos.y) start_pos = pos await get_tree().create_timer(randf_range(0.25, 0.55)).timeout var tween = create_tween().set_trans(Tween.TRANS_BACK) tween.tween_property(self, "position:y", start_pos.y, 1.4) await tween.finished $MoveTimer.wait_time = randf_range(5, 20) $MoveTimer.start() $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start()
سنستدعي الدالة السابقة عند نشر الأعداء، ونمرر إليها شعاع موضع يمثل مكان ظهور العدو على الشاشة، وسنلاحظ عندها أننا ننشر العدو فوق الحافة العلوية للشاشة. سنعطي y
قيمة سالبة كي نتمكن من تحريك العدو عندما يدخل إلى الشاشة باستخدام مولد إطارات بينية Tween، ونضبط أيضًا قيمتي المؤقتين عشوائيًا كي لا يتحرك العدو ويطلق النار في نفس الوقت، وبعد ذلك سنصل إشارة timeout
الخاصة بكل مؤقت.
func _on_timer_timeout(): speed = randf_range(75, 100) func _on_shoot_timer_timeout(): $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start()
يمكن هنا التحرك عند انتهاء المؤقت من عد الفترة الزمنية التي يُضبط عليها، لكننا لم ننشئ قذائف الأعداء بعد، وهذا ما سنتعامل معه لاحقًا.
وبما أننا سنغير سرعة حركة الأعداء من خلال المتغير speed
، فمعنى ذلك، أن بإمكاننا التحرك عبر كتابة الشيفرة التالية في دالة المعالجة:
func _process(delta): position.y += speed * delta if position.y > screensize.y + 32: start(start_pos)
إن لم تكن قيمة المتغير speed
هي 0، فسنرى العدو يتحرك على الشاشة، وعندما يصل إلى الأسفل يعود من الأعلى مجددًا.
لقد كتبنا الدالة التي تعالج انفجار الأعداء عندما تصطدم بها قذيفة اللاعب الدالة ()explode
عندما بنينا مشهد القذيفة، لهذا سنضيف نفس الدالة هنا:
func explode(): speed = 0 $AnimationPlayer.play("explode") set_deferred("monitoring", false) died.emit(5) await $AnimationPlayer.animation_finished queue_free()
سنوقف في هذه الدالة حركة العدو، ثم نشغّل الرسم المتحرك الخاص بعملية الانفجار ونحذف بعدها العدو في نهاية الدالة. وتضمن الدالة ()set_deferred
إيقاف الخاصية monitoring
في كائن العدو كي لا تصيبه قذيفة أخرى أثناء انفجاره، وعندها سيصيب القذيفة التي تنفجر ويفجرها مجددًا ويستدعي الدالة التي تنفذ عملية التفجير مجددًا، وهكذا.
سنضيف الآن الإشارة died
إلى أعلى السكريبت:
signal died
وسنستخدم هذه الإشارة لإبلاغ المشهد الرئيسي أن اللاعب حقق بعض النقاط.
نشر العدو
سنعود الآن إلى المشهد الرئيسي Main لإضافة بعض الأعداء إلى اللعبة، ولهذا سنضيف سكريبتًا إلى المشهد الرئيسي ونبدأ بتحميل مشهد العدو برمجيًا:
extends Node2D var enemy = preload("res://enemy.tscn") var score = 0
لن يبدأ نشر العدو في اللعبة قبل النقر على زر البداية. وطالما أننا لم نرتّب هذا الأمر بعد، فسننشره مباشرة:
func _ready(): spawn_enemies() func spawn_enemies(): for x in range(9): for y in range(3): var e = enemy.instantiate() var pos = Vector2(x * (16 + 8) + 24, 16 * 4 + y * 16) add_child(e) e.start(pos) e.died.connect(_on_enemy_died)
تنشر الشيفرة السابقة 27 عدوًا ضمن شبكة في النصف الأعلى من الشاشة، ولا بد من التأكد من وصل الإشارة died
لكل عدو، ولهذا علينا إضافة الدالة التالية:
func _on_enemy_died(value): score += value
سنشغل الآن المشهد، وسنرى مجموعةً من الأعداء أعلى الشاشة يسقطون دوريًا نحو الأسفل.
إطلاق النار من المركبات المعادية
طالما أن مركبات العدو ستتمكن من إطلاق النار، فسنمنحها شيئًا لتطلق النار عليه.
مشهد قذائف العدو
سننشئ مشهدًا باسم EnemyBullet مشابهًا من ناحية التكوين لمشهد قذائف اللاعب. لن نكرر خطوات الإنشاء هنا طبعًا، لهذا يمكن العودة إلى مشهد قذائف اللاعب bullet
في حال مصادفة مشكلة ما؛ إذ سيكون الفرق الوحيد هو الصورة المستخدمة للقذيفة في هذا المشهد، وهي الصورة Enemy_projectile (16 x 16).png
، وسيختلف السكريبت هنا قليلًا:
extends Area2D @export var speed = 150 func start(pos): position = pos func _process(delta): position.y += speed * delta
نصل الإشارتين screen_exited
و area_entered
العائدتين للعقدتين VisibleOnScreenNotifier2D
و Area2D
على التوالي:
func _on_visible_on_screen_notifier_2d_screen_exited(): queue_free() func _on_area_entered(area): if area.name == "Player": queue_free()
ملاحظة: تُطلق الإشارة screen_exited
عندما يخرج الكائن من الشاشة، وتُستخدم لحذفه أو إعادة ضبطه وتطلق الإشارة area_entered
عندما يدخل جسم إلى منطقة محددة وهي تُستخدم للكشف عن الاصطدامات أو التفاعلات.
وكما نلاحظ، نحن هنا نلتقط اصطدام القذيفة المعادية باللاعب، لكننا لم نحصل على النتيجة المطلوبة حتى اللحظة. سنعود إلى ذلك بعد إضافة طريقة تصف تضرر سفينة اللاعب عند إصابته.
إضافة آلية إطلاق النار إلى العدو
سنحمّل مشهد قذيفة العدو في أعلى سكريبت العدو:
var bullet_scene = preload("res://enemy_bullet.tscn")
ونعدّل بعد ذلك دالة إطلاق النار إلى النحو الآتي:
func _on_shoot_timer_timeout(): var b = bullet_scene.instantiate() get_tree().root.add_child(b) b.start(position) $ShootTimer.wait_time = randf_range(4, 20) $ShootTimer.start()
سنشغّل الآن المشهد Main، وسنرى بعض قذائف العدو تهطل إلى الأسفل بين الفينة والأخرى.
ختامًا
بهذا نكون قد أتممنا إعداد مشهد العدو الذي يطلق النار في لعبتنا ثنائية الأبعاد، وسنتعرف على المزيد لتطوير لعبتنا في المقالات القادمة.
ترجمة -وبتصرف- للمقالين: Enymies و Enemy Shooting.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.