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

سنوضّح في هذا المقال كيفية إعداد خوارزمية للبحث عن المسار للسماح بالتنقل في بيئة قائمة على الشبكة Grid، حيث يوفّر محرّك الألعاب جودو Godot عددًا من الطرق لتحديد المسار، ولكننا سنستخدم في هذا المقال خوارزمية A*‎، التي لها استخدام واسع في العثور على أقصر مسار بين نقطتين، ويمكن استخدامها في أيّ هيكل بيانات قائمٍ على الرسم البياني Graph، وليس في بيئة شبكية فقط.

يُعَد الصنف AStarGrid2D نسخةً متخصصةً من الصنف AStar2D الأعم في جودو، لذا يُعَد إعداده أسرع وأسهل، نظرًا لأنه متخصص للاستخدام مع الشبكة، إذ لسنا مضطرين لإضافة جميع خلايا الشبكة الفردية واتصالاتها يدويًا.

إعداد الشبكة

يُعَد ضبط حجم الخلايا والشبكة أهم خطوة، حيث سنستخدم الحجم ‎(64, 64)‎ في مثالنا، وسنستخدم حجم النافذة لتحديد عدد الخلايا الملائمة للشاشة، ولكن كل شيء سيعمل بالطريقة نفسها بغض النظر عن حجم الخلية.

سنضيف الآن الشيفرة البرمجية التالية إلى العقدة Node2D:

extends Node2D

@export var cell_size = Vector2i(64, 64)

var astar_grid = AStarGrid2D.new()
var grid_size

func _ready():
    initialize_grid()

func initialize_grid():
    grid_size = Vector2i(get_viewport_rect().size) / cell_size
    astar_grid.size = grid_size
    astar_grid.cell_size = cell_size
    astar_grid.offset = cell_size / 2
    astar_grid.update()

نقسم حجمَ الشاشة على حجم الخلية cell_size في هذه الشيفرة البرمجية لحساب حجم الشبكة بالكامل، مما يتيح ضبط الخاصية size الخاصة بالصنف AStarGrid2D.

تُستخدَم خاصية الإزاحة offset عندما نطلب مسارًا بين نقطتين، ويمثّل استخدام cell_size / 2 حساب المسار من مركز كل خلية بدلًا من زواياها، ويجب استدعاء الدالة update()‎ بعد ضبط أو تغيير خاصيات الصنف AStarGrid2D.

رسم الشبكة

سنرسم الشبكة على الشاشة في الشيفرة البرمجية للتوضيح، ولكن قد تكون لدينا عقدة TileMap أو أيّ تمثيل مرئي آخر لعالمنا في تطبيق اللعبة. يمكن رسم الشبكة باستخدام الشيفرة البرمجية التالية:

func _draw():
    draw_grid()

func draw_grid():
    for x in grid_size.x + 1:
        draw_line(Vector2(x * cell_size.x, 0),
            Vector2(x * cell_size.x, grid_size.y * cell_size.y),
            Color.DARK_GRAY, 2.0)
    for y in grid_size.y + 1:
        draw_line(Vector2(0, y * cell_size.y),
            Vector2(grid_size.x * cell_size.x, y * cell_size.y),
            Color.DARK_GRAY, 2.0)

وسنحصل على الشبكة التالية:

01 astar grid 01

رسم المسار

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

var start = Vector2i.ZERO
var end = Vector2i(5, 5)

بعد ذلك نضيف الأسطر التالية في الدالة ‎_draw()‎ لإظهار هذه النقاط:

    draw_rect(Rect2(start * cell_size, cell_size), Color.GREEN_YELLOW)
    draw_rect(Rect2(end * cell_size, cell_size), Color.ORANGE_RED)

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

func update_path():
    $Line2D.points = PackedVector2Array(astar_grid.get_point_path(start, end))

وسنحصل على النتيجة التالية:

02 astar grid 02

وكما نلاحظ، لدينا خطًا قطريًا بين النقطتين لأن المسار يستخدم الخطوط القطرية افتراضيًا، ويمكن تعديل ذلك من خلال تغيير الخاصية diagonal_mode باستخدام القيم التالية:

  • DIAGONAL_MODE_ALWAYS: القيمة الافتراضية، وتستخدم الخطوط القطرية
  • DIAGONAL_MODE_NEVER: تكون الحركة عمودية
  • DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE: تسمح هذه القيمة بالخطوط القطرية، ولكنها تمنع المسار من المرور بين العوائق الموضوعة قطريًا
  • DIAGONAL_MODE_AT_LEAST_ONE_WALKABLE: تسمح بالخطوط القطرية في المناطق المفتوحة فقط، وليس بالقرب من العوائق

قد يؤدي تعديل هذه الخاصية إلى إعطاء نتائج مختلفة جدًا، لذا تأكد من التجربة بناءً على الإعداد الذي تستخدمه، لذا يجب إضافة ما يلي في الدالة initialize_grid()‎:

astar_grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_NEVER

وأصبح لدينا الآن الحركات المتعامدة فقط كما يلي:

03 astar grid 03

إضافة العوائق

يمكننا أيضًا إضافة العوائق إلى الشبكة، حيث لن يتضمن المسار خليةً ما من خلال وضع علامة عليها بوصفها صلبة Solid، ويمكن التبديل بين القيمتين صلبة وغير صلبة للخلية باستخدام الدالة set_point_solid()‎.

سنضيف الشيفرة البرمجية التالية لرسم الجدران (إن وُجدت) من خلال العثور على الخلايا الصلبة وتلوينها:

func fill_walls():
    for x in grid_size.x:
        for y in grid_size.y:
            if astar_grid.is_point_solid(Vector2i(x, y)):
                draw_rect(Rect2(x * cell_size.x, y * cell_size.y, cell_size.x, cell_size.y), Color.DARK_GRAY)

سنستدعي الآن هذه الدالة في الدالة ‎_draw()‎، ويمكن بعد ذلك استخدام الفأرة للنقر على الخلايا وتبديل حالتها كما يلي:

func _input(event):
    if event is InputEventMouseButton:
        # إضافة أو إزالة حائط
        if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            var pos = Vector2i(event.position) / cell_size
            if astar_grid.is_in_boundsv(pos):
                astar_grid.set_point_solid(pos, not astar_grid.is_point_solid(pos))
            update_path()
            queue_redraw()

يمكن ملاحظة أننا نستخدم التابع is_in_boundsv()‎ أولًا، مما يمنع حدوث أخطاء إذا نقرنا خارج حدود الشبكة.

يمكننا الآن رؤية تأثير العوائق على المسار كما يلي:

04 astar grid 04

الاختيار الاستكشافي أو التجريبي Heuristic

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

تستخدم المسافة الإقليدية مثلًا نظرية فيثاغورس لتقدير المسار الذي يجب تجربته كما يلي:

05 astar grid 03

بينما تأخذ مسافة مانهاتن في حساباتها المسافة في اتجاهات الشمال/الجنوب أو الشرق/الغرب فقط كما يلي:

06 astar grid manhattan

وتعطي طريقة التجزيء الاستكشافية Octile Heuristic المسار التالي:

07 astar grid octile

يمكن استخدام الاختيار الاستكشافي باستخدام الخاصية التالية:

astar_grid.default_estimate_heuristic = AStarGrid2D.HEURISTIC_OCTILE

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

ملاحظة: يمكن تنزيل شيفرة المشروع البرمجية من Github لتجربة الإعداد، ويمكن استخدام أزرار الفأرة الأيمن/الأوسط لتحريك مواقع النهاية/البداية بالإضافة إلى وضع الجدران.

ترجمة -وبتصرّف- للقسم Pathfinding on a 2D Grid من توثيقات 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.


×
×
  • أضف...