سنوضّح في هذا المقال كيفية إنشاء وحدة تحكم للسيارات في الألعاب ثنائية الأبعاد من الأعلى إلى الأسفل. قد لا يتمكّن المبتدئون من إنشاء شيءٍ يتعامل مع لعبةٍ مشابهة لسيارة حقيقية، لذا سنذكر بعض الأخطاء الشائعة التي قد تظهر في ألعاب السيارات للهواة:
- لا تدور السيارة حول مركزها، إذ لا تنزلق 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 كما نرى في هذا الرسم البياني:
ملاحظة: يمكن تعديل هذه القيم لمعرفة كيفية تغيرها.
سنستدعي في الدالة _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>
لإظهار أو إخفاء لوحة أشرطة التمرير.
الخاتمة
بهذا نكون قد وصلنا لنهاية مقالنا الذي استعرضنا فيه خطوات إنشاء وحدة تحكم لسيارات الألعاب ثنائية الأبعاد بأسلوب واقعي بدءًا من الحركة الأساسية، مرورًا بالتسارع والفرملة، وصولًا إلى الانجراف والانزلاق. يمكن اعتبار هذا المقال بمثابة أساس لتطوير تجربة قيادة أكثر ديناميكية وواقعية في الألعاب.
وللتذكير، يمكن تنزيل شيفرة المشروع البرمجية كاملة من Github.
ترجمة -وبتصرّف- للقسم Car steering من توثيقات Kidscancode.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.