اذهب إلى المحتوى

أضفنا في المقال السابق خاصية القفز والهبوط فوق الأعداء وتدميرهم ضمن لعبة ثلاثية الأبعاد، لكننا لم نجعل اللاعب يموت بعد في حال اصطدم بعدو ما موجود على الأرض، لذا لنعمل على تحقيق هذا الأمر، ونشرح في فقراتنا التالية كيفية تحقيقه خطوة بخطوة عن طريق استخدام هياكل التصادم Hitboxes وعقد Area في محرك جودو.

تحديد ما إذا كان اللاعب على الأرض

ما نحتاج لاكتشافه في البداية هو اصطدامنا مع العدو الذي يمشي على الأرض، إذ نريد أن يموت اللاعب عندما يرتطم بالعدو وهو على الأرض فقط، لا إذا كان في الهواء، ولتحقيق لدينا طريقتان الأولى هي باستخدام رياضيات الأشعة Raycasting لتحديد ما إذا كان اللاعب يلامس الأرض أو في الهواء، إذا كنت تريد أن يموت اللاعب فقط عندما يكون على الأرض، يمكنك استخدام شعاع يتجه من اللاعب نحو الأسفل مثل شعاع Raycast2D للتحقق من وجود سطح تحته.

أو يمكننا بدلاً من ذلك، استخدام عقدة Area3D والتي تعمل بشكل جيد مع هياكل التصادم Hitboxes في محرك جودو. حيث يمكن تعريف منطقة معينة حول اللاعب. فإذا دخل العدو في هذه المنطقة أثناء وجود اللاعب على الأرض، سنفعّل حدث الاصطدام وننفذ منطق موت اللاعب وهو الأسلوب الذي سنعتمده.

هيكل تصادم Hitbox مع عقدة Area

لنعد إلى مشهد player.tscn ونضف عقدة فرعية جديدة من النوع Area3D، سنطلق عليها اسم MobDetector أي كاشف الأعداء، ومن ثم سنضيف العقدة CollisionShape3D كعقدة فرعية لها.

01 العقدة collisionshape

بعدها سنعين شكله ليكون أسطوانيًا وذلك باختيار NewClindershape في قائمة الفاحص Inspector.

02 new cylindershape

ولجعل التصادمات تحدث فقط عندما يكون اللاعب على الأرض أو قريبًا منها يمكننا تقليل ارتفاع الإسطوانة ونقلها إلى أعلى الشخصية، وبهذه الطريقة، عندما يقفز اللاعب سيكون الشكل مرتفعًا جدًا بحيث لا يمكن للأعداء الاصطدام به.

03 تقليل ارتفاع الأسطوانة

نريد أيضًا أن تكون الأسطوانة أوسع من الكرة، فبهذه الطريقة، يُضرب اللاعب قبل الاصطدام ويجري دفعه فوق صندوق الاصطدام الخاص بالعدو، فكلما كانت الأسطوانة أوسع، كلما كان من الأسهل قتل اللاعب.

بعد ذلك سنحدد عقدة MobDetector مرة أخرى، ونعطّل خاصية قابلية المراقبة Monitorable في الفاحص Inspector، وهذا سيجعل من العقد الفيزيائية الأخرى غير قادرة على اكتشاف المنطقة، إذ تسمح خاصية قابلية المراقبة Monitorable باكتشاف التصادمات، بعدها سنريل طبقة الاصطدام من الطبقة Layer في قسم التصادم Collision ونضبط القناع على طبقة الأعداء.

04 تعطيل monitorable

عندما تكتشف المناطق تصادمًا ستصدر إشارة، وكي نوصّل هذه الإشارة إلى عقدة اللاعب Player نحدد العقدة MobDetector وننتقل إلى التبويب Node في نافذة الفاحص Inspector، وننقر مرتين فوق إشارة body_entered التي جرى وصلها بعقدة Player.

05 إشارة body entered

تطلق العقدة 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.

06 إشارة hit

ثم نحصل على المؤقت ونوقفه في الدالة ‎_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 من توثيق جودو الرسمي.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...