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

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

طريقة إعداد كاميرا ديناميكية تتبع عدة أهداف في وقت واحد

سنوضّح فيما يلي كيفية إعداد كاميرا ديناميكية لإبقاء عدة عناصر على الشاشة في الوقت نفسه.

لنفترض أن لدينا لعبة تحتوي على لاعبَين ويجب إبقاء اللاعبين على الشاشة أثناء تحرّكهما، سواءً عند التباعد عن بعضهما البعض أو عند وجودهما قرب بعضهما البعض كما في المثال التالي:

01_multi_cam_01.gif

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

لحل المشكلة، يمكننا تطبيق ما يلي على الكاميرا:

  1. إضافة أو إزالة أيّ عددٍ من الأهداف
  2. إبقاء موضع الكاميرا متمركزًا عند نقطة المنتصف للأهداف
  3. ضبط تكبير أو تصغير الكاميرا لإبقاء جميع الأهداف على الشاشة

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

سيبدأ السكربت بالتعليمات التالية:

extends Camera2D

@export var move_speed = 30 # ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا
@export var zoom_speed = 3.0  # ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا
@export var min_zoom = 5.0  # لن تقترب الكاميرا أكثر من هذه القيمة
@export var max_zoom = 0.5  # لن تبتعد الكاميرا أكثر من هذه القيمة
@export var margin = Vector2(400, 200)  # تضمين بعض المساحة العازلة حول الأهداف

var targets = []  # مصفوفة الأهداف التي يجب تعقّبها

@onready var screen_size = get_viewport_rect().size

تمكّن هذه الإعدادات من ضبط سلوك الكاميرا، إذ سنستخدم الدالة lerp()‎ التي تمثّل الاستيفاء الخطي لجميع تغييرات الكاميرا، ولهذا سيؤدي ضبط سرعات الحركة/التكبير والتصغير على قيم أقل إلى بعض التأخير في تتبّع الكاميرا للتغييرات المفاجئة.

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

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

func add_target(t):
    if not t in targets:
        targets.append(t)

func remove_target(t):
    if t in targets:
        targets.erase(t)

توجد دالتان مساعدتان لإضافة وإزالة الأهداف، ويمكنك استخدامهما أثناء اللعب لتغيير الأهداف التي يجب تعقّبها مثل دخول اللاعب 3 إل اللعبة. وكما نلاحظ، لا نريد تعقّب الهدف نفسه مرتين، لذا سنرفضه إذا كان موجودًا مسبقًا.

تحدث معظم الوظائف في الدالة ‎_process()‎، ولكن لنبدأ أولًا بتحريك الكاميرا كما يلي:

func _process(delta):
    if !targets:
        return
    # إبقاء الكاميرا متمركزة بين الأهداف
    var p = Vector2.ZERO
    for target in targets:
        p += target.position
    p /= targets.size()
    position = lerp(position, p, move_speed * delta)

سنكرّر ضمن الحلقة التأكيد على مواضع الأهداف ونعثر على المركز المشترك، ونتأكّد باستخدام الدالة lerp()‎ من التحرّك بسلاسة.

بعد ذلك، لا بد لنا من التعامل مع التكبير أو التصغير كما يلي:

# العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف
var r = Rect2(position, Vector2.ONE)
for target in targets:
    r = r.expand(target.position)
r = r.grow_individual(margin.x, margin.y, margin.x, margin.y)
var z
if r.size.x > r.size.y * screen_size.aspect():
    z = 1 / clamp(r.size.x / screen_size.x, min_zoom, max_zoom)
else:
    z = 1 / clamp(r.size.y / screen_size.y, min_zoom, max_zoom)
zoom = lerp(zoom, Vector2.ONE * z, zoom_speed)

تأتي الوظيفة الأساسية من الصنف Rect2، إذ نريد العثور على مستطيل يحيط بكل الأهداف، والذي يمكننا الحصول عليه باستخدام التابع expand()‎، ثم نوسّع المستطيل باستخدام الخاصية margin.

يجب علينا هنا الضغط على زر Tab بالمستطيل المرسوم لتفعيل هذا الرسم في المشروع التجريبي:

02_multi_cam_02.gif

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

السكربت الكامل

سيكون السكربت الكامل للعملية على النحو الآتي:

extends Camera2D

@export var move_speed = 30 # ‫سرعة الاستيفاء الخطي lerp لموضع الكاميرا
@export var zoom_speed = 3.0  # ‫سرعة الاستيفاء الخطي lerp لتكبير وتصغير Zoom الكاميرا
@export var min_zoom = 5.0  # لن تقترب الكاميرا أكثر من هذه القيمة
@export var max_zoom = 0.5  # لن تبتعد الكاميرا أكثر من هذه القيمة
@export var margin = Vector2(400, 200)  # تضمين بعض المساحة العازلة حول الأهداف

var targets = []

@onready var screen_size = get_viewport_rect().size

func _process(delta):
    if !targets:
        return

    # إبقاء الكاميرا متمركزة بين الأهداف
    var p = Vector2.ZERO
    for target in targets:
        p += target.position
    p /= targets.size()
    position = lerp(position, p, move_speed * delta)

    # العثور على التكبير أو التصغير الذي سيحتوي على جميع الأهداف
    var r = Rect2(position, Vector2.ONE)
    for target in targets:
        r = r.expand(target.position)
    r = r.grow_individual(margin.x, margin.y, margin.x, margin.y)
    var z
    if r.size.x > r.size.y * screen_size.aspect():
        z = 1 / clamp(r.size.x / screen_size.x, max_zoom, min_zoom)
    else:
    z = 1 / clamp(r.size.y / screen_size.y, max_zoom, min_zoom)
    zoom = lerp(zoom, Vector2.ONE * z, zoom_speed * delta)

    # لتنقيح الأخطاء
    get_parent().draw_cam_rect(r)

func add_target(t):
    if not t in targets:
        targets.append(t)

func remove_target(t):
    if t in targets:
        targets.remove(t)

ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github.

إضافة تصادمات مع خط مرسوم ثنائي الأبعاد

سنوضّح فيما يلي كيفية إضافة تصادمات مع خط مرسوم ثنائي الأبعاد باستخدام عقدة Line2D.

إعداد العقد

سنحتاج لإضافة العقد التالية إلى مشهدنا، ورسم الخط الذي نريده:

Line2D
    StaticBody2D

لا داعي لإضافة شكل تصادم إلى الجسم حاليًا.

ملاحظة: يمكن استخدام العقدة Area2D عوضًا عن ذلك إذا أردنا اكتشاف التداخل مع الخط بدلًا من التصادم.

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

الخيار الأول: استخدام الشكل SegmentShape2D

يمثّل الشكل SegmentShape2D شكل تصادم لخط ومقطع، حيث نريد إنشاء تصادم مقاطع لكل زوج من النقاط على الخط.

extends Line2D

func _ready():
    for i in points.size() - 1:
        var new_shape = CollisionShape2D.new()
        $StaticBody2D.add_child(new_shape)
        var segment = SegmentShape2D.new()
        segment.a = points[i]
        segment.b = points[i + 1]
        new_shape.shape = segment

الخيار الثاني: استخدام الشكل RectangleShape2D

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

extends Line2D

func _ready():
    for i in points.size() - 1:
        var new_shape = CollisionShape2D.new()
        $StaticBody2D.add_child(new_shape)
        var rect = RectangleShape2D.new()
        new_shape.position = (points[i] + points[i + 1]) / 2
        new_shape.rotation = points[i].direction_to(points[i + 1]).angle()
        var length = points[i].distance_to(points[i + 1])
        rect.extents = Vector2(length / 2, width / 2)
        new_shape.shape = rect

ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github.

ختامًا

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

ترجمة -وبتصرّف- للقسمين Multitarget Camera و Line2D Collision من توثيقات 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.


×
×
  • أضف...