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

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

انقر بزر الفأرة الأيمن على عقدة Player وحدد خيار إضافة سكريبت Attach Script لإضافة سكربت جديد إليها، ثم اضبط القالب Template في النافذة المنبثقة على Empty قبل الضغط على زر إنشاء Create.

1

سنعرّف خصائص الصنف class مثل سرعة الحركة movement speed وتسارع السقوط fall acceleration الذي يمثل الجاذبية، والسرعة التي سنستخدمها لتحريك الشخصية.

extends CharacterBody3D

# سرعة اللاعب بواحدة المتر في الثانية
@export var speed = 14
# التسارع نحو الأسفل في الهواء بوحدة متر في الثانية للتربيع
@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO

هذه الخصائص مشتركة لجسم متحرك حيث أن target_velocity هو شعاع ثلاثي الأبعاد 3D vector يجمع بين السرعة والاتجاه. وقد عرفناه هنا كخاصية لأننا نريد تحديث قيمته وإعادة استخدامه عبر الإطارات.

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

لتحديد اتجاه الحركة نبدأ بحساب شعاع اتجاه الإدخال باستخدام الكائن العام input في ‎_physics_process()‎ لبرمجة الحركة كما في الشيفرة التالية:

func _physics_process(delta):
    # أنشأنا متغير محلي لتخزين اتجاه الإدخال
    var direction = Vector3.ZERO

    # نتحقق من كل خطوة ونحدّث الاتجاه حسب ذلك
    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        # لاحظ أننا نعمل مع محاور أشعة x و z
        # إن المسطح xz في ثلاثي الأبعاد هو مستوي الأرض
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1 

سنجري هنا جميع الحسابات باستخدام الدالة الافتراضية ‎_physics_process()‎ وهي على غرار الدالة ‎_process()‎ التي تتيح لنا تحديث العقدة في كل إطار ولكنها مصممة خصيصًا للشيفرة المتعلقة بالفيزياء مثل تحريك جسم حركي kinematic يتفاعل مع محيطه من خلال الكود البرمجي، أو جسم صلب rigid يعتمد على محرك الفيزياء في جودو للتحرك والتفاعل بشكل واقعي مع البيئة المحيطة به بناءً على القوى المؤثرة عليه مثل الجاذبية أو التصادمات.

ولتعلّم المزيد حول الفرق بين الدالتين ‎ _process()‎ و ‎ ‎ ‎_physics_process()‎ راجع توثيق جودو حول معالجة الفيزياء والسكون ومعرفة كيفية استخدام هذه الدوال لضبط الحركات الفيزيائية بطريقة مستقرة وواقعية.

نبدأ بتهيئة قيمة المتغير direction إلى Vector3.ZERO ليكون متجهًا فارغًا (أي أن قيمته تكون صفرًا في المحاور الثلاثة x و y و z) ثم نتحقق مما إذا كان اللاعب يضغط على إدخال move_*‎ واحد أو أكثر ونغيّر مكوني x وz للشعاع وفقًا لذلك بحيث تتوافق هذه مع محاور مستوى الأرض.

توفر لنا هذه الحالات الأربعة ثمانية احتمالات، وثمانية اتجاهات ممكنة.

سيكون طول الشعاع في حالة ضغط اللاعب مثلًا على كل من W و D في وقت واحد حوالي 1.4، ولكن إذا ضغط على مفتاح واحد سيكون طوله 1. نستدعي تابع normalized()‎ ليكون طول الشعاع ثابتًا ولا يتحرك بشكل أسرع قطريًا.

وفيما يلي شيفرة تحريك أو تدوير الكائن في اتجاه حركة اللاعب بناءً على المدخلات.

#func _physics_process(delta):
    #...

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        # تهيئة خاصية‫ basis سيؤثر على دوارن العقدة.
        $Pivot.basis = Basis.looking_at(direction)

لاحظ أننا نعالج الشعاع فقط إذا كان طوله أكبر من الصفر، مما يعني أن اللاعب يضغط على مفتاح اتجاه، مما يؤدي إلى تحديث direction،بحيث يعكس الاتجاه الذي يريد اللاعب التحرك فيه.

الآن سنحسب الاتجاه الذي ينظر إليه ‎$‎Pivot عن طريق إنشاء Basis ينظر في اتجاه direction، ومن ثم نحدّث قيمة السرعة حيث يتعين علينا حساب سرعة الأرض أو السرعة الأفقية وسرعة السقوط أو السرعة العمودية بشكل منفصل، حيث تُدمج السرعات معًا وتُحرّك الشخصية باستخدام الدالة move_and_slide()‎ لتطبيق الحركة الفعلية مع الفيزياء.

تأكد من الضغط على مفتاح Tab مرة واحدة بحيث تكون الأسطر داخل دالة ‎_physics_process()‎ ولكن خارج الشرط الذي كتبناه للتو أعلاه.

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...

    # السرعة الأرضية
    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)

    # تحريك الشخصية
    velocity = target_velocity
    move_and_slide()

تعيد دالة ‏‎CharacterBody3D.is_on_floor()‎‏ قيمة true إذا تصادم الجسم مع الأرض في هذا الإطار، لهذا السبب نطبق الجاذبية على اللاعب Player فقط عندما يكون في الهواء.

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

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

نستدعي في السطر الأخير التابع CharacterBody3D.move_and_slide()‎ وهو تابع ذو قدرات عظيمة لصنف CharacterBody3D إذ يسمح لك بتحريك الشخصية بسلاسة، حيث سيحاول محرك جودو إصلاح الحركة إذا اصطدمت بحائط في منتصفها باستخدام قيمة السرعة الأصلية CharacterBody3D.

هذه هي كل الشيفرة التي تحتاجها لتحريك الشخصية على الأرض. فيما يلي شيفرة Player.gd الكاملة لاستخدامها كمرجع:

extends CharacterBody3D

# سرعة اللاعب بواحدة المتر في الثانية
@export var speed = 14
# التسارع نحو الأسفل في الهواء بوحدة متر في الثانية للتربيع.
@export var fall_acceleration = 75

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    var direction = Vector3.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.basis = Basis.looking_at(direction)

    # السرعة الأرضية
    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)

    # تحريك الشخصية
    velocity = target_velocity
    move_and_slide()

اختبار حركة اللاعب

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

احفظ المشهد Player وافتح المشهد Main من خلال النقر على تبويب Main  أعلى المحرر.

2

إذا أغلقت المشهد من قبل، فتوجه إلى نافذة نظام الملفات FileSystem Dock وانقر نقرًا مزدوجًا فوق main.tscn لإعادة فتحه.

انقر بزر الفأرة الأيمن على العقدة الرئيسية Main وحدد Instantiate Child Scene لاستنساخ المشهد Player .

3

الآن انقر نقرًا مزدوجًا فوق player.tscn في النافذة المنبثقة لتظهر الشخصية في وسط نافذة العرض.

إضافة كاميرا

سننشئ إعدادًا أساسيًا تمامًا كما فعلنا مع `Pivot. انقر بزر الفأرة الأيمن على عقدة المشهد الرئيسيMainمرة أخرى وحدد خيار "إضافة عقدة فرعية Add Child Node"، ثم أنشئ عقدةMarker3Dجديدة وسمهاCameraPivot، ومن ثم حددCameraPivotوأضف إليها عقدة فرعيةCamera3D` لتبدو شجرة المشهد على هذا النحو

4

ستلاحظ مربع اختيار معاينة Preview يظهر في الزاوية العلوية اليسرى عند تحديد الكاميرا حيث يمكنك النقر فوقه لمعاينة إسقاط الكاميرا داخل اللعبة.

5

سنستخدم المحور Pivot لتدوير الكاميرا كما لو كانت على رافعة، لذا دعنا أولاً نقسم نافذة العرض ثلاثي الأبعاد 3D view لنتمكن من التنقل بحرية في المشهد ورؤية ما تراه الكاميرا. في شريط الأدوات أعلى النافذة مباشرةً، انقر فوق View ثم 2‎ ‎Viewports. يمكنك أيضًا الضغط على Ctrl + 2 (أو Cmd + 2على نظام التشغيل macOS).

6

7

حدد Camera3D في النافذة السفلية، وشغّل معاينة الكاميرا بالنقر فوق مربع الاختيار Preview.

8

حرك الكاميرا في النافذة العلوية حوالي 19 وحدة على المحور Z ذي اللون الأزرق.

9

هنا نرى ثمرة عملنا، حدد CameraPivot ودوره بمقدار ‎-‎45 درجة حول محور X باستخدام الدائرة الحمراء لترى الكاميرا تتحرك كما لو كانت متصلة برافعة.

10

يمكنك تشغيل المشهد بالضغط على F6 ثم الضغط على مفاتيح الأسهم لتحريك الشخصية.

11

يمكننا رؤية مساحة فارغة حول الشخصية بسبب الإسقاط المنظوري perspective projection، لذلك سنستخدم بدلاً منه في هذه اللعبة إسقاطًا متعامدًا orthographic projection لتأطير منطقة اللعب بشكل أفضل وتسهيل قراءة المسافات على اللاعب.

حدد الكاميرا مرة أخرى ومن قائمة الفاحص Inspector اضبط الإسقاط على القيمة "متعامد Orthogonal" والحجم على 19. يجب أن تبدو الشخصية مسطحة أكثر ومنسجمة مع الأرضية.

ملاحظة: تعتمد جودة الظل الاتجاهي directional shadow على قيمة Far للكاميرا عند استخدام كاميرا متعامدة فإعداد Far يحدد المسافة الأبعد التي يمكن للكاميرا رؤيته، وكلما زادت قيمة البعد زادت المسافة التي ستتمكن الكاميرا من الرؤية فيها. بالرغم من ذلك، فإن قيم البعد الأعلى ستقلل أيضًا من جودة الظل حيث يجب أن يغطي عرض الظل مسافة أكبر.

قلل خاصية البعد للكاميرا إلى قيمة أقل مثل 100 إذا بدت الظلال الاتجاهية ضبابية جدًا بعد التبديل إلى كاميرا متعامدة فلا تقلل من خاصية البعد هذه كثيرًا وإلا ستبدأ الكائنات في البعد بالاختفاء.

12

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

الخلاصة

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

ترجمة - وبتصرف - لقسم Moving the player with code من توثيق جودو الرسمي.

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...