سنشرح في هذا المقال من سلسلة دليل جودو مفهوم Delta في مجال صناعة الألعاب، ونوضح كيفية استخدامه.
يُعَد معامل دلتا delta
أو "زمن دلتا" مفهومًا يُساء فهمه كثيرًا في تطوير الألعاب، لذا سنشرح في هذا المقال كيفية استخدامه وأهمية الحركة المستقلة عن معدل الإطارات وأمثلة عملية لاستخدامه في محرّك الألعاب جودو Godot.
ليكن لدينا عقدة Sprite
تتحرك عبر الشاشة. إذا كان عرض الشاشة 600 بكسل ونريد أن نعبر الشخصية الرسومية Sprite الشاشة خلال 5 ثوانٍ، فيمكننا استخدام العملية الحسابية التالية لإيجاد السرعة اللازمة لذلك:
600 pixels / 5 seconds = 120 pixels/second
سنحرّك الشخصية الرسومية في كل إطار باستخدام الدالة _process()
، بحيث إذا شُغِّلت اللعبة بمعدل 60 إطارًا في الثانية، فيمكننا إيجاد الحركة لكل إطار باستخدام العملية الحسابية التالية:
120 pixels/second * 1/60 second/frame = 2 pixels/frame
ملاحظة: كما نلاحظ، أن وِحدات المقادير متناسقة في جميع العمليات الحسابية السابقة، لذا لا بد من الانتباه دائمًا إليها لتجنب الوقوع في الأخطاء.
تكون الشيفرة البرمجية الضرورية كما يلي:
extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(2, 0) func _process(delta): $Sprite.position += movement
نشغّل الشيفرة البرمجية السابقة وسنجد أن الصورة تعبر الشاشة خلال 5 ثوانٍ.
تحصل المشكلة إذا كان هناك شيء آخر يشغَل وقت الحاسوب، والذي يسمى بالتأخير Lag، الذي يكون له عدة أسباب، حيث يمكن أن يكون السبب هو الشيفرة البرمجية التي نستخدمها أو حتى التطبيقات الأخرى التي تعمل على الحاسوب. وإذا حدث التأخير، فقد يؤدي ذلك إلى زيادة طول الإطار.
إذا تخيلنا مثلًا أن معدل الإطارات انخفض إلى النصف بحيث يستغرق كل إطار 1/30 بدلًا من 1/60 من الثانية. فمعنى هذا أن الأمر سيستغرق ضعف الوقت حتى تصل الشخصية الرسومية إلى طرف الشاشة عند التحرك بمعدل 2 بكسل لكل إطار.
ستؤدي حتى التقلبات الصغيرة في معدل الإطارات إلى سرعة حركة غير متناسقة، وإذا كانت هذه الشخصية الرسومية رصاصة أو جسمًا سريع الحركة، فلن نرغب في إبطائه بهذه الطريقة؛ إذ يجب أن تكون الحركة مستقلة عن معدل الإطارات.
إصلاح مشكلة معدل الإطارات
تتضمن الدالة _process()
تلقائيًا عند استخدامها معاملًا بالاسم delta
يمرّره المحرّك كما في الدالة _physics_process()
التي تُستخدَم في الشيفرة البرمجية المتعلقة بالفيزياء.
المعامل delta
هو قيمة عشرية تمثل الوقت المستغرق منذ الإطار السابق، والذي سيكون 1/60 أو 0.0167 ثانية تقريبًا. يمكننا التوقف عن القلق بشأن مقدار تحريك كل إطار من خلال استخدام المعامل delta
، إذ سنحتاج للاهتمام فقط بالسرعة المطلوبة بالبكسلات في الثانية، والتي هي 120 من العمليات الحسابية السابقة.
سيعطينا ضرب قيمة delta
الخاصة بالمحرك بهذا العدد عددَ البكسلات التي يجب تحريكها في كل إطار، وسيُعدَّل هذا العدد تلقائيًا عند تقلّب زمن الإطار.
# 60 إطار في الثانية 120 pixels/second * 1/60 second/frame = 2 pixels/frame # 30 إطار في الثانية 120 pixels/second * 1/30 second/frame = 4 pixels/frame
وكما نلاحظ، يبدو أنه إذا انخفض معدل الإطارات إلى النصف (أي تضاعف زمن الإطار)، فيجب أن تتضاعف أيضًا الحركة لكل إطار للحفاظ على السرعة المطلوبة، ولهذا سنعدّل الشيفرة البرمجية كما يلي:
extends Node2D # الحركة المطلوبة بالبكسلات لكل إطار var movement = Vector2(120, 0) func _process(delta): $Sprite.position += movement * delta
يكون زمن الانتقال متناسقًا عند التشغيل بمعدل 30 إطارًا في الثانية كما يلي:
إذا أصبح معدل الإطارات منخفضًا جدًا، فلن تكون الحركة سلسةً بعد الآن، ولكن يبقى الزمن كما هو.
استخدام معامل دلتا مع معادِلات الحركة
إذا كانت الحركة التي نريد العمل عليها أكثر تعقيدًا، فسيبقى المفهوم كما هو مع إبقاء الوحدة بالثواني وليس بالإطارات، والضرب بمعامل delta
لكل إطار.
ملاحظة: يُعَد التعامل بالبكسلات والثواني أسهل بكثير لأنه يتعلق بكيفية قياس هذه الكميات في العالم الحقيقي، فمثلًا الجاذبية Gravity هي 100 بكسل/ثانية/ثانية، لذا ستتحرك الكرة بسرعة 200 بكسل/ثانية بعد سقوطها لمدة ثانيتين، وإذا استخدمنا وحدة الإطارات، فيجب استخدام التسارع Acceleration بوحدة البكسل/إطار/إطار، ولكننا سنجد أن هذه الوحدة غير مألوفة.
إذا طبّقنا الجاذبية مثلًا، فإنها تمثّل التسارع، بحيث ستزيد السرعة بمقدارٍ معين في كل إطار، وستغير السرعة موضع العقدة كما هو الحال في المثال السابق؛ وهنا سنضبط قيم delta
و target_fps
في الشيفرة البرمجية التالية لمعرفة النتائج:
extends Node2D # التسارع بالبكسل/ثانية/ثانية var gravity = Vector2(0, 120) # التسارع بالبكسل/إطار/إطار var gravity_frame = Vector2(0, .033) # السرعة بالبكسل/ثانية أو بالبكسل/إطار var velocity = Vector2.ZERO var use_delta = false var target_fps = 60 func _ready(): Engine.target_fps = target_fps func _process(delta): if use_delta: velocity += gravity * delta $Sprite.position += velocity * delta else: velocity += gravity_frame $Sprite.position += velocity
وكما هو ظاهر، فقد ضربنا القيمة المحدثة في الخطوة الزمنية لكل إطار لتحديث السرعة velocity
والموضع position
، إذ يجب ضرب أي كمية مُحدَّثة في كل إطار بقيمة delta
لضمان تغيرها بحيث تكون مستقلة عن معدل الإطارات.
استخدام الدوال الحركية Kinematic
استخدمنا Sprite
للتبسيط في الأمثلة السابقة، مع تحديث الموضع position
في كل إطار.
إذا استخدمنا جسمًا حركيًا Kinematic ثنائي الأبعاد أو ثلاثي الأبعاد، فسنحتاج لاستخدام أحد توابع الحركة الخاصة به بدلًا من ذلك، خاصةً في حالة استخدام التابع move_and_slide()
؛ إذ قد يحدث بعض الارتباك لأنه يستخدم متجه السرعة وليس الموضع، وهذا يعني أننا لن نضرب السرعة بقيمة delta
لإيجاد المسافة، إذ تنجز الدالة ذلك نيابةً عنا، ولكن يجب تطبيقها على أيّ عمليات حسابية أخرى مثل التسارع كما في المثال التالي:
# الشيفرة البرمجية لحركة الشخصية الرسومية: velocity += gravity * delta position += velocity * delta # الشيفرة البرمجية لحركة الجسم الحركي: velocity += gravity * delta move_and_slide()
إن لم نستخدم قيمة delta
عند تطبيق التسارع على السرعة، فسيكون التسارع عرضةً للتقلبات في معدل الإطارات، وقد يكون لذلك تأثير أكثر دقةً على الحركة؛ إذ سيكون غير متناسق مع وجود صعوبة في ملاحظته.
ملاحظة: يجب أيضًا تطبيق قيمة delta
على أي كميات أخرى مثل الجاذبية والاحتكاك وغير ذلك عند استخدام التابع move_and_slide()
.
ختامًا
بهذا نكون قد تعرفنا على مفهوم delta في مجال تطوير الألعاب وكيفية استخدامه، وسنتعرف في المقال التالي على كيفية حفظ واسترجاع البيانات المحلية بين جلسات اللعب.
ترجمة -وبتصرّف- للقسم Understanding delta من توثيقات Kidscancode.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.