أضفنا في المقال السابق خاصية القفز والهبوط فوق الأعداء وتدميرهم ضمن لعبة ثلاثية الأبعاد، لكننا لم نجعل اللاعب يموت بعد في حال اصطدم بعدو ما موجود على الأرض، لذا لنعمل على تحقيق هذا الأمر، ونشرح في فقراتنا التالية كيفية تحقيقه خطوة بخطوة عن طريق استخدام هياكل التصادم Hitboxes وعقد Area في محرك جودو.
تحديد ما إذا كان اللاعب على الأرض
ما نحتاج لاكتشافه في البداية هو اصطدامنا مع العدو الذي يمشي على الأرض، إذ نريد أن يموت اللاعب عندما يرتطم بالعدو وهو على الأرض فقط، لا إذا كان في الهواء، ولتحقيق لدينا طريقتان الأولى هي باستخدام رياضيات الأشعة Raycasting لتحديد ما إذا كان اللاعب يلامس الأرض أو في الهواء، إذا كنت تريد أن يموت اللاعب فقط عندما يكون على الأرض، يمكنك استخدام شعاع يتجه من اللاعب نحو الأسفل مثل شعاع Raycast2D للتحقق من وجود سطح تحته.
أو يمكننا بدلاً من ذلك، استخدام عقدة Area3D
والتي تعمل بشكل جيد مع هياكل التصادم Hitboxes في محرك جودو. حيث يمكن تعريف منطقة معينة حول اللاعب. فإذا دخل العدو في هذه المنطقة أثناء وجود اللاعب على الأرض، سنفعّل حدث الاصطدام وننفذ منطق موت اللاعب وهو الأسلوب الذي سنعتمده.
هيكل تصادم Hitbox مع عقدة Area
لنعد إلى مشهد player.tscn
ونضف عقدة فرعية جديدة من النوع Area3D
، سنطلق عليها اسم MobDetector
أي كاشف الأعداء، ومن ثم سنضيف العقدة CollisionShape3D
كعقدة فرعية لها.
بعدها سنعين شكله ليكون أسطوانيًا وذلك باختيار NewClindershape في قائمة الفاحص Inspector.
ولجعل التصادمات تحدث فقط عندما يكون اللاعب على الأرض أو قريبًا منها يمكننا تقليل ارتفاع الإسطوانة ونقلها إلى أعلى الشخصية، وبهذه الطريقة، عندما يقفز اللاعب سيكون الشكل مرتفعًا جدًا بحيث لا يمكن للأعداء الاصطدام به.
نريد أيضًا أن تكون الأسطوانة أوسع من الكرة، فبهذه الطريقة، يُضرب اللاعب قبل الاصطدام ويجري دفعه فوق صندوق الاصطدام الخاص بالعدو، فكلما كانت الأسطوانة أوسع، كلما كان من الأسهل قتل اللاعب.
بعد ذلك سنحدد عقدة MobDetector
مرة أخرى، ونعطّل خاصية قابلية المراقبة Monitorable في الفاحص Inspector، وهذا سيجعل من العقد الفيزيائية الأخرى غير قادرة على اكتشاف المنطقة، إذ تسمح خاصية قابلية المراقبة Monitorable باكتشاف التصادمات، بعدها سنريل طبقة الاصطدام من الطبقة Layer في قسم التصادم Collision ونضبط القناع على طبقة الأعداء.
عندما تكتشف المناطق تصادمًا ستصدر إشارة، وكي نوصّل هذه الإشارة إلى عقدة اللاعب Player
نحدد العقدة MobDetector
وننتقل إلى التبويب Node في نافذة الفاحص Inspector، وننقر مرتين فوق إشارة body_entered
التي جرى وصلها بعقدة Player
.
تطلق العقدة MobDetctor
إشارة body_entered
عندما تدخلها عقدة CharacterBody3D
أو RigidBody3D
، لكن نظرًا لأن العقدة MobDetctor
مهيأة لتعمل فقط مع الطبقات الفيزيائية الخاصة بالأعداء، فإنها ستكتشف فقط الأجسام التي تنتمي إلى هذه الطبقات، أي ستكتشف فقط عقد العدو.
من الناحية البرمجية، يتوجب علينا القيام بأمرين الأول إصدار إشارة سنستخدمها لاحقًا لإنهاء اللعبة والثاني تدمير اللاعب، ويمكننا تغليف هذه العمليات في دالة die()
التي تساعدنا بوضع دلالة مفيدة إلى شيفرتنا كالتالي:
# تنشر عندما يصطدم اللاعب بعدو # ضع هذه في أعلى السكريبت signal hit # وأضف هذه الدالة في الأسفل func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die()
جرب اللعبة مرة أخرى بالضغط على F5
. إذا أعددنا كل شيء بشكل صحيح، فيجب أن تموت الشخصية عندما يصطدم العدو بهيكل التصادم، لاحظ أن السطر التالي سيعطي خطأ في حال عدم وجود عقدة لاعب Player
ففي هذه الحالة لن يكون هناك مرجع إلى
$Player!
var player_position = $Player.position
لاحظ أيضًا أن اصطدام العدو باللاعب وموته يعتمد على حجم وموضع اللاعبPlayer
وأشكال تصادم العدو mob
، قد تحتاج إلى نقلها وتغيير حجمها للحصول على تجربة واقعية في اللعبة.
إنهاء اللعبة
يمكننا استخدام إشارة ضرب hit
اللاعب player
لإنهاء اللعبة، فكل ما يتعين علينا فعله هو توصيله بالعقدة الرئيسية Main
وإيقاف MobTimer
كرد فعل.
لنفتح المشهد الرئيسي للعبة main.tscn
، ونحدد عقدة اللاعب Player
، وفي نافذة العقدة Node نوصّل إشارة hit
الخاصة بها بالعقدة الرئيسية Main
.
ثم نحصل على المؤقت ونوقفه في الدالة _on_player_hit()
.
func _on_player_hit(): $MobTimer.stop()
إذا جربنا اللعبة الآن، فستتوقف الأعداء عن الظهور عندما يموت اللاعب، وستغادر الأعداء المتبقية الشاشة.
يمكننا الآن أن نكافئ نفسنا لقد أنشأنا نموذج أولي للعبة ثلاثية الأبعاد كاملة، حتى لو كانت لا تزال بسيطة بعض الشيء. ومن هناك سنضيف نتيجة وخيارًا لإعادة بدء اللعبة، ونحاول جعل اللعبة تبدو أكثر حيوية من خلال الرسوم المتحركة البسيطة.
الشيفرة النهائية للعبة ثلاثية الأبعاد في جودو
فيما يلي الشيفرات الكاملة للعقد الرئيسية Main
وMob
وPlayer
للرجوع إليها.
شيفرة المشهد الرئيسي
لنبدأ بسكربت main.gd
الذي يعمل على إنشاء الأعداء بشكل عشوائي في مواقع مختلفة على المسار المحدد عند مرور الوقت بناءً على مؤقت العدو، ويحدد هدف العدو وهو اللاعب كما أنه يوقف ظهور الأعداء عندما يتعرض اللاعب للإصابة.
extends Node @export var mob_scene: PackedScene func _on_mob_timer_timeout(): # إنشاء نسخة جديدة من مشهد العدو var mob = mob_scene.instantiate() # اختيار مكان عشوائي على SpawnPath # نخزن المرجع على عقدة SpawnLocation var mob_spawn_location = get_node("SpawnPath/SpawnLocation") # ونعطيه انزياح عشوائي mob_spawn_location.progress_ratio = randf() var player_position = $Player.position mob.initialize(mob_spawn_location.position, player_position) # خلق العدو عن طريق إضافته إلى المشهد الرئيسي add_child(mob) func _on_player_hit(): $MobTimer.stop()
شيفرة العدو
السكربت التالي هو سكربت العدو Mob.gd
الذي يتعامل مع حركة العدو في اللعبة، حيث يوجه العدو عشوائيًا نحو اللاعب، ويحدد سرعته بشكل عشوائي، ويتأكد من تدمير العدو عند خروجه من الشاشة أو عندما يقفز اللاعب عليه.
extends CharacterBody3D # السرعة الدنيا للعدو مقدرة بالمتر في الثانية @export var min_speed = 10 # السرعة القصوى للعدو مقدرة بالمتر في الثانية @export var max_speed = 18 # تنبثق عندما يقفز اللاعب على عدو signal squashed func _physics_process(_delta): move_and_slide() # يتم استدعاء هذه الدالة من المشهد الأساسي func initialize(start_position, player_position): # نحدد مكان العدو عن طريق وضعه في start_position # وندوره نحو player_position لينظر إلى اللاعب look_at_from_position(start_position, player_position, Vector3.UP) #تدوير العدو عشوائيًا ضمن مجال -90 و +90 درجة # لكي لا تتحرك مباشرة نحو اللاعب rotate_y(randf_range(-PI / 4, PI / 4)) # نحسب سرعة عشوائية (عدد صحيح) var random_speed = randi_range(min_speed, max_speed) # نحسب سرعة امامية تمثل السرعة velocity = Vector3.FORWARD * random_speed # ثم ندور شعاع السرعة اعتمادًا على دوران العدو حول Y # للحركة في الاتجاه الذي ينظر إليه العدو velocity = velocity.rotated(Vector3.UP, rotation.y) func _on_visible_on_screen_notifier_3d_screen_exited(): queue_free() func squash(): squashed.emit() queue_free() # تدمير العقدة
شيفرة اللاعب
وأخيرًا، نوضح سكربت Player.gd
الذي يتحكم في حركة اللاعب كالقفز والتحرك على الأرض والسقوط في الهواء، فعندما يصطدم اللاعب بالعدو من الأعلى، سيدمره باستخدام الدالة squash()
ويعطى اللاعب دفعة قفز عمودية، وعند اصطدامه مع العدو يموت باستخدام الدالة die()
.
extends CharacterBody3D signal hit # سرعة حركة اللاعب مقدرة بالمتر في الثانية @export var speed = 14 # التسارع نحو الأسفل في الهواء مقدرة بالمتر في الثانية مربع @export var fall_acceleration = 75 # الدفعة العامودية المطبقة على الشخصية عند القفز مقدرة بالمتر في الثانية @export var jump_impulse = 20 # الدفعة العامودية المطبقة على الشخصية عند القفز على عدو مقدرة بالمتر في الثانية @export var bounce_impulse = 16 var target_velocity = Vector3.ZERO func _physics_process(delta): # أنشأنا متغير محلي لتخزين دخل الاتجاه var direction = Vector3.ZERO # نتحقق من كل دخل حركة ونحدث الاتجاه حسبه if Input.is_action_pressed("move_right"): direction.x = direction.x + 1 if Input.is_action_pressed("move_left"): direction.x = direction.x - 1 if Input.is_action_pressed("move_back"): # لاحظ أننا نعمل المحورين x و z الخاصين بالشعاع # المستوي XZ هو مستوي الأرض في ثلاثي الأبعاد direction.z = direction.z + 1 if Input.is_action_pressed("move_forward"): direction.z = direction.z - 1 # منع الحركة القطرية السريعة if direction != Vector3.ZERO: direction = direction.normalized() $Pivot.look_at(position + direction, Vector3.UP) # السرعة الأرضية target_velocity.x = direction.x * speed target_velocity.z = direction.z * speed # السرعة العمودية if not is_on_floor(): # إذا كان في الهواء يسقط على الأرض أي الجاذبية target_velocity.y = target_velocity.y - (fall_acceleration * delta) # القفز if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse # كرر خلال كل الاصطدامات التي تحصل في الإطار # يكون ذلك بلغة C كالتالي # (int i = 0; i < collisions.Count; i++) for index in range(get_slide_collision_count()): # نحصل على واحد من التصادمات مع اللاعب var collision = get_slide_collision(index) # إذا كان التصادم مع الأرض if collision.get_collider() == null: continue # إذا كان التصادم مع العدو if collision.get_collider().is_in_group("mob"): var mob = collision.get_collider() # نتحقق أننا نصدمه من الأعلى if Vector3.UP.dot(collision.get_normal()) > 0.1: # إذا كان كذلك نسحقه mob.squash() target_velocity.y = bounce_impulse # يمنع أي استدعاءات مكررة break # تحريك الشخصية velocity = target_velocity move_and_slide() # أضف ذلك في الأسفل func die(): hit.emit() queue_free() func _on_mob_detector_body_entered(body): die()
الخاتمة
بهذا نكون شرحنا بالتفصيل كيفية منح الأعداء في لعبتنا القدرة على تدمير اللاعب، وأصبح منطق لعبتنا متوازنًا، لكن اللعبة تبدو حتى الآن مملة نوعًا ما بسبب عدم وجود هدف أو نتيجة نهائية تقيم أدائنا في اللعب، لذا سنبدأ في الدرس التالي على تحقيق ذلك ونضيف النتيجة وخيار إعادة المحاولة في حال خسرنا اللعبة وأردنا إعادة تشغيلها سريعًا!
ترجمة - وبتصرف - لقسم Killing the player من توثيق جودو الرسمي.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.