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

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

  • لا تدور السيارة حول مركزها، إذ لا تنزلق Slide عجلات السيارة الخلفية من جانب إلى آخر إلّا في حالة الانجراف Drifting، وسنتحدث عن ذلك لاحقًا
  • لا يمكن للسيارة أن تستدير إلا عندما تتحرك، إذ لا يمكنها الدوران في مكانها
  • السيارة ليست قطارًا، فهي ليست على قضبان سكة حديدية، لذا يجب أن تتضمن الاستدارة بسرعات عالية بعض الانزلاق أو الانجراف

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

ملاحظة: تعتمد الطريقة التي سنوضّحها فيما يلي على الخوارزمية الموجودة في مقال فيزياء توجيه السيارات في الألعاب ثنائية الأبعاد البسيطة.

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

إعداد المشهد

فيما يلي إعداد لمشهد السيارة:

CharacterBody2D
    Sprite2D
    CollisionShape2D
    Camera2D

سنضيف أي خامة شخصية رسومية Sprite Texture نريدها، حيث سنستخدم في مثالنا حزمة السباق من موقع Kenney. تُعَد العقدة CapsuleShape2D خيارًا جيدًا للتصادم، بحيث لا تكون للسيارة زوايا حادة قد تتسبب في تعطلها بسبب العوائق. سنستخدم أيضًا أربعة إجراءات إدخال هي: steer_right و steer_left و accelerate و brake، لذا يمكننا ضبطها على أيّ مفاتيح إدخال نفضلها.

الجزء الأول: الحركة

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

extends CharacterBody2D

var wheel_base = 70  # المسافة من العجلة الأمامية إلى العجلة الخلفية
var steering_angle = 15  # مقدار دوران العجلة الأمامية بالدرجات

var steer_direction

نضبط المتغير wheelbase على قيمة تتوافق مع شخصيتنا الرسومية، ويمثّل المتغير steer_direction مقدار دوران العجلات.

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

func _physics_process(delta):
    get_input()
    calculate_steering(delta)
    move_and_slide()

يجب التحقق من الإدخال وحساب التوجيه Steering في كل إطار، ثم نمرّر السرعة velocity الناتجة إلى الدالة move_and_slide()‎، ونعرّف الدالتين التاليتين كما يلي:

func get_input():
    var turn = Input.get_axis("steer_left", "steer_right")
    steer_direction = turn * deg_to_rad(steering_angle)
    velocity = Vector2.ZERO
    if Input.is_action_pressed("accelerate"):
        velocity = transform.x * 500

سنتحقق من إدخال المستخدم ونضبط السرعة. وكما نلاحظ فقيمة السرعة 500 مؤقتة حتى نتمكّن من اختبار الحركة، وسنعالجها في الجزء التالي.

سننفّذ بعد ذلك الخوارزمية السابقة كما يلي:

func calculate_steering(delta):
    # 1. العثور على مواضع العجلات
    var rear_wheel = position - transform.x * wheel_base / 2.0
    var front_wheel = position + transform.x * wheel_base / 2.0
    # 2. تحريك العجلات للأمام
    rear_wheel += velocity * delta
    front_wheel += velocity.rotated(steer_direction) * delta
    # 3. العثور على متجه الاتجاه الجديد
    var new_heading = rear_wheel.direction_to(front_wheel)
    # 4. ضبط السرعة والدوران على الاتجاه الجديد
    velocity = new_heading * velocity.length()
    rotation = new_heading.angle()

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

الجزء الثاني: التسارع Acceleration

سنحتاج إلى متغير إعدادٍ آخر ومتغيرًا لتتبّع التسارع الكلي للسيارة كما يلي:

var engine_power = 900  # قوة التسارع للأمام

var acceleration = Vector2.ZERO

علينا الآن التعديل شيفرة الإدخال البرمجية لتطبيق التسارع بدلًا من تغيير سرعة velocity السيارة مباشرةً كما يلي:

func get_input():
    var turn = Input.get_axis("steer_left", "steer_right")
    steer_direction = turn * deg_to_rad(steering_angle)
    if Input.is_action_pressed("accelerate"):
        acceleration = transform.x * engine_power

يمكن تطبيق التسارع على السرعة بعد الحصول عليه كما يلي:

func _physics_process(delta):
    acceleration = Vector2.ZERO
    get_input()
    calculate_steering(delta)
    velocity += acceleration * delta
    move_and_slide()

يجب الآن أن تزيد السيارة من سرعتها تدريجيًا عند تشغليها، ولكن ليس لدينا أيّ طريقة لإبطاء السرعة بعد.

الجزء الثالث: الاحتكاك Friction/السحب Drag

تتعرض السيارة لقوتين مختلفتين لإبطاء السرعة هما: الاحتكاك والسحب.

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

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

سنوضح فيما يلي القيم الابتدائية لهاتين الكميتين:

var friction = -55
var drag = -0.06

تعني هذه القيم أن قوة السحب تتغلب على قوة الاحتكاك عند السرعة 600 كما نرى في هذا الرسم البياني:

01_car_graph_friction(1) ar.png

ملاحظة: يمكن تعديل هذه القيم لمعرفة كيفية تغيرها.

سنستدعي في الدالة ‎_physics_process()‎ دالةً لحساب الاحتكاك الحالي ونطبقّه على قوة التسارع كما يلي:

func _physics_process(delta):
    acceleration = Vector2.ZERO
    get_input()
    apply_friction(delta)
    calculate_steering(delta)
    velocity += acceleration * delta
    velocity = move_and_slide(velocity)

func apply_friction(delta):
    if acceleration == Vector2.ZERO and velocity.length() < 50:
        velocity = Vector2.ZERO
    var friction_force = velocity * friction * delta
    var drag_force = velocity * velocity.length() * drag * delta
    acceleration += drag_force + friction_force

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

الجزء الرابع: الرجوع للخلف/الفرامل Brake

نحتاج إلى متغيرين آخرين للإعدادات:

var braking = -450
var max_speed_reverse = 250

سنضيف الآن الدخل إلى الدالة get_input()‎ كما يلي:

    if Input.is_action_pressed("brake"):
        acceleration = transform.x * braking

يُعَد ذلك جيدًا للتوقف، ولكننا نريد أيضًا أن نتمكن من وضع السيارة في وضع الرجوع للخلف، ولكن ذلك لن ينجح حاليًا، لأن التسارع يُطبَّق في اتجاه التوجّه دائمًا الذي هو إلى الأمام، لذا نحتاج إلى تسارع عكسي عند الرجوع للخلف.

func calculate_steering(delta):
    var rear_wheel = position - transform.x * wheel_base / 2.0
    var front_wheel = position + transform.x * wheel_base / 2.0
    rear_wheel += velocity * delta
    front_wheel += velocity.rotated(steer_angle) * delta
    var new_heading = (front_wheel - rear_wheel).normalized()
    var d = new_heading.dot(velocity.normalized())
    if d > 0:
        velocity = new_heading * velocity.length()
    if d < 0:
        velocity = -new_heading * min(velocity.length(), max_speed_reverse)
    rotation = new_heading.angle()

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

الجزء الخامس: الانجراف Drift والانزلاق Slide

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

لحل المشكلة، يجب أن تتسبب قوة الانعطاف عند السرعات العالية أو حتى المنخفضة إذا رغبنا في ذلك، في انزلاق الإطارات مما يؤدي إلى حركة انزلاقية، وسنفعل ذلك الآتي:

var slip_speed = 400  # السرعة عند تقليل الشدّ‫ Traction
var traction_fast = 2.5 # الشد عالي السرعة
var traction_slow = 10  # الشد منخفض السرعة

سنطبّق هذه القيم عند حساب التوجيه، إذ تُضبَط السرعة مباشرةً على الاتجاه الجديد حاليًا، ولكننا سنستخدم بدلًا من ذلك الاستيفاء باستخدام الدالة lerp()‎ لجعلها تدور جزئيًا نحو الاتجاه الجديد فقط، وستحدّد قيم الشدّ Traction مدى تماسك الإطارات.

func calculate_steering(delta):
    var rear_wheel = position - transform.x * wheel_base / 2.0
    var front_wheel = position + transform.x * wheel_base / 2.0
    rear_wheel += velocity * delta
    front_wheel += velocity.rotated(steer_angle) * delta
    var new_heading = (front_wheel - rear_wheel).normalized()
    # اختر قيمة الشد التي تريد استخدامها، ويجب أن يكون الانزلاق منخفضًا عند السرعات المنخفضة
    var traction = traction_slow
    if velocity.length() > slip_speed:
        traction = traction_fast
    var d = new_heading.dot(velocity.normalized())
    if d > 0:
        velocity = lerp(velocity, new_heading * velocity.length(), traction * delta)
    if d < 0:
        velocity = -new_heading * min(velocity.length(), max_speed_reverse)
    rotation = new_heading.angle()

نختار قيمة الشد التي نريد استخدامها ونطبق الدالة lerp()‎ على السرعة velocity.

التعديلات Adjustments

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

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

05 car sliders

الخاتمة

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

وللتذكير، يمكن تنزيل شيفرة المشروع البرمجية كاملة من Github.

ترجمة -وبتصرّف- للقسم Car steering من توثيقات Kidscancode.

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...