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

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

إعداد المشروع

سننشئ لعبة تخطيطها من اﻷعلى إلى اﻷسفل وسنستخدم ميزة Autotile في محرك الألعاب جودو فهي تُسهّل كثيراً عملية رسم الخرائط باستخدام عناصر رقعة Tiles متداخلة وتمكننا من رسم الجدران أو الأرضيات بحرية، حيث يختار المحرك تلقائيًا الرقعة المناسبة من مجموعة الرقع حتى تتطابق الحواف والزوايا مع الرقع المجاورة، ويمكن تنزيل الصور المطلوبة لتطبيق المقال من مجلد assets من هذا الرابط أو منminimap_assets.zip

سيبدو المشهد الرئيسي للعبة كالتالي:

01 minimap main scene

نستخدم العقدة CanvasLayer لتجميع عناصر واجهة المستخدم بما في ذلك الخريطة المصغرة التي سننشؤها في هذا المقال، ونستخدم العقدة TileMap لرسم الخريطة باستخدام الرقع tiles، بينما نستخدم العقدة Player لتمثيل شخصية اللاعب.

تخطيط واجهة المستخدم

الخطوة اﻷولى في مثالنا هي بناء تخطيط للخريطة المصغرة. وللتعامل مع أية عناصر واجهة مستخدم تضمها اللعبة، لا بد من إعادة تحجيمها بشكل سلس ودمجها جيدًا في تخطيط يتلائم مع الحاوية. لهذا سنضيف أولًا العقدة MarginContainer  التي تساعدنا على وضع حواشي داخلية padding للعناصر داخل الحاوية، ونضبط الخاصية Constants في القسم Theme Overrides على 5. يضم عنصر التحكم هذا بقية العقد ويضمن أن العناصر داخل الحاوية ستبقى بعيدًا عن حواف الحاوية نفسها بشكل متناسق. سنسمي هذه العقدة MiniMap ثم نحفظ المشهد.

نضيف تاليًا العقدة NinePatchRect وهي مشابهة للعقدة TextureRect لكنها تتعامل مع تغيير اﻷبعاد بطريقة مختلفة، إذ لا تمدد الزوايا أو الحواف مما يحافظ على مظهر الصورة بشكل أفضل عند تغيير الأبعاد. نفلت الصورة panel_woodDetail_blank.png من مجلد assets في لعبتنا في الخاصية Texture، وهي صورة أبعادها 128x128 بكسل، لكن إن غيرنا أبعاد العقدة MarginContainer ستصبح الصورة ممددة وسيئة المظهر.

02 minimap ui

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

بإمكاننا تعريف هذه الخاصيات رسوميًا في اللوحة TextureRegion ، لكن من اﻷسهل أحيانًا إدخال القيم مباشرة. نضبط الخاصيات اﻷربعة الموجودة في Patch Margin على 64 ونغيّر اسم العقدة إلى Frame. لنلاحظ اﻵن ما يحدث عند تغيير اﻷبعاد:

03 minimap ui no streach

علينا تاليًا ملء الجزء الداخلي من اﻹطار بنمط يمثل شبكة وذلك باستخدام الصورة pattern_blueprintPaper.png:

04 pattern blueprintpaper

نريد اﻵن أن تملأ الصورة ما داخل اﻹطار تلقائيًا أيًا كانت أبعاده، وطالما أن المنطقة التي تغطيها الشبكة هي المكان الذي ستظهر فيه نقاط علام الخريطة، لا ينبغي أن تمتد الشبكة إذًا خارج حدود اﻹطار.

لهذا، نضيف عقدة جديدة MarginContainer كابن للعقدة MiniMap وشقيق للعقدة Frame ثم نضبط خاصيات  Constants  في القسم Theme Overrides على القيمة 20. نضيف بعد ذلك عقدة TectureRect كابن للعقدة السابقة ثم نضبط قيمة الخاصية Texure على نفس الصورة السابقة، والخاصية Strech Mode على Tile ونسمي العقدة أخيرًا Grid. لنجرّب تغيير أبعاد العقدة اﻷصلية لرؤية تأثير ما فعلناه حتى اللحظة:

05 minimap ui grid streach

لنبق أبعاد الخريطة المصغرة حاليًا على 200x200 بكسل، وبإمكاننا التأكد من هذه اﻷبعاد من الخاصية Size في القسم Layout. ستبدو اﻵن شجرة المشهد كالتالي:

06 scene tree

نقاط علام الخريطة

سنضيف للخريطة نقاط العلام Marker ترمز  كل منها لأشياء معينة في اللعبة، نضيف أولًا عقدة من النوع Sprite2D كابن للعقدة Grid ونسميها PlayerMarker لتمثل اللاعب ونمنحها الصورة minimapIcon_arrowA.png، وننتبه إلى أن قيمة الخاصية Position في القسم Transform هي (0,0)مما يجعلها في الزاوية العليا اليسارية من العقدة Grid.

07 plaayer marker top left

إن كانت أبعاد الشبكة (150,150)سيكون مركزها عند (75,75)، لنضبط إذًا موقع PlayerMarker على تلك القيمة، ننوه أن هذه العملية ستكون آلية لاحقًا.

08 player marker center

نضيف اﻵن عقدتين جديدتين من نوع Sprite2D ونسميهما AlertMarker و MobMarker ونمنحهما الصورتين minimapIcon_jewelRed.png التي تمثل جوهرة حمراء و minimapIcon_exclamationYellow.png التي تمثل علامة تعجب صفراء كما يلي:

09 three markers

تمثل العقدتان السابقتان نوعين جديدن من نقاط العلام في عالم اللعبة. ننقر على الزر Toggle Visibility المجاور لكل منهما كي لا تظهر العقدة افتراضيًا.

كتابة سكريبت نقاط العلام

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

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

نضيف كل كائن نريده أن يظهر على الخريطة ضمن المجموعة minimap_objects ثم نضبط المتغير minimap_icon في سكريبت كل كائن على القيمة المناسبة من المجموعة:

# mob في سكريبت:
var minimap_icon = "mob"

# crate في سكريبت:
var minimap_icon = "alert"

بإمكاننا اﻵن إضافة سكريبت إلى العقدة MiniMap.

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

extends MarginContainer
class_name Minimap

@export var player: Player
@export var zoom = 1.5

@onready var grid = $MarginContainer/Grid
@onready var player_marker = $MarginContainer/Grid/PlayerMarker
@onready var mob_marker = $MarginContainer/Grid/MobMarker
@onready var alert_marker = $MarginContainer/Grid/AlertMarker

نستخدم تاليًا قاموسًا ونسميه minimap-icon  لربط الأنواع بالرموز، حيث نظهر الكائن  mob  الذي يمثل العدو كنقطة حمراء على الخريطة، والكائن  alert  الذي يمثل تحذير كعلامة صفراء:

@onready var icons = {
    "mob": mob_marker,
    "alert": alert_marker
}

نحتاج أيضُا إلى متغير يخزّن نسبة حجم الخريطة إلى حجم عالم اللعبة. كما نستفيد من قاموس آخر لإسناد نقاط العلام الفعالة إلى كل كائن. وسيكون المفتاح Key في القاموس هو الكائن نفسه أي نسخة عن Mob أو Crate والقيمة value هي نقطة العلام المسندة إليه:

var grid_scale
var markers = {}

نضبط موقع نقطة علام اللاعب في منتصف الخريطة ضمن الدالة ()ready_، ونحسب عامل المقياس.

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

func _ready():
    await get_tree().process_frame
    player_marker.position = grid.size / 2
    grid_scale = grid.size / (get_viewport_rect().size * zoom)

العقد الموجودة في الحاويات

نظرًا للطريقة التي تعامل فيها العقدة Container أبناءها وتغير حجمهم أو موقعهم، لن نحصل على القيمة الصحيحة لأبعاد اﻷبناء وقت تنفيذ الدالة ()ready_، لهذا علينا أن ننتظر حتى اﻹطار التالي لنحصل على أبعاد الشبكة.

ننشئ أيضًا نقطة علام لكل لكائن في اللعبة باستخدام المجموعة minimap_objects بمضاعفة العقدة المطابقة لنقطة العلام وربط العلامة بالكائن بالاستفادة من القاموس markers:

    var map_objects = get_tree().get_nodes_in_group("minimap_objects")
    for item in map_objects:
      var new_marker = icons[item.minimap_icon].duplicate()
      grid.add_child(new_marker)
      new_marker.show()
      markers[item] = new_marker

بعد أن أنشأنا نقاط العلام وربطناها بالكائنات الموجودة، نستطيع اﻵن تحديث مواقعها ضمن الدالة ()process_. وإن لم يُعين أي لاعب player بعد، لا نفعل شيئًا:

func _process(delta):
    if !player:
      return

وﻹن كان هناك لاعب، ندور أولًا نقطة علام اللاعب لتطابق جهة حركته. وطالما أن نقطة العلامة PlayerMarker تتجه إلى اﻷعلى وليس بالاتجاه اﻷفقي x، لا بد من إضافة 90 درجة:

player_marker.rotation = player.rotation + PI/2

نبحث اﻵن عن موقع كل كائن بالنسبة إلى اللاعب ونستخدمه في إيجاد موقع نقطة العلام، لنتذكر إزاحة الموقع بمقدار grid.size/2 لأن نقطة المرجع هي الزاوية العليا اليسارية:

for item in markers:
    var obj_pos = (item.position - player.position) * grid_scale + grid.size / 2
    markers[item].position = obj_pos

تبقى المشكلة إمكانية ظهور بعض نقاط العلام خارج الشبكة كما في الصورة التالية:

10 markers out of grid

وﻹصلاح اﻷمر، نحصر موقع نقطة العلامة بمربع الشبكة باستخدام الدالة clamp بعد حساب المتغير obj_pos وقبل تحديد موقع العلامة:

obj_pos = obj_pos.clamp(Vector2.ZERO, grid.size)

11 clmp to grid

بإمكاننا أيضًا معالجة العلامات التي تقع خارج الشاشة وخارج مربع الشبكة. باختيار أحد الحلين التاليين وقبل استخدام ()clamp

الخيار اﻷول هو كالتالي:

if grid.get_rect().has_point(obj_pos + grid.position):
    markers[item].show()
else:
    markers[item].hide()

الخيار الثاني هو تغيير مظهر العلامات، بأن نجعلها أصغر لتدل على أنها أبعد مسافة:

if grid.get_rect().has_point(obj_pos + grid.position):
    markers[item].scale = Vector2(1, 1)
else:
    markers[item].scale = Vector2(0.75, 0.75)

12 marker rescale

إزالة الكائنات

ستزدحم اللعبة وتتوقف إن قُتِل أي كائن Mob أو التُقط كائن Crate لأن نقاط العلام حينها لن تكون صحيحة. لهذا نحتاج إلى طريقة نتأكد من خلالها من إزالة نقاط العلام في حال إزالة الكائنات، وفيما يلي طريقة سريعة سنتبعها في هذا المقال:

نضيف signal removed إلى أي كائن وضعناه ضمن المجموعة minimap_object ثم نبث هذه الرسالة عندما يتدمر الكائن أو يُلتقط مع مرجع إلى الكائن نفسه كي تتعرف عليه الخريطة:

removed.emit(self)

نصل هذه اﻹشارات إلى الخريطة في الدالة ()ready_ للسكريبت الرئيسي:

func _ready():
    for object in get_tree().get_nodes_in_group("minimap_objects"):
      object.removed.connect(minimap._on_object_removed)

نضيف اﻵن دالة استقبال اﻹشارات إلى سكريبت الخريطة المصغرة لتحرير نقطة العلام وإزالة المرجع:

func _on_object_removed(object):
    if object in markers:
      markers[object].queue_free()
      markers.erase(object)

تكبير وتصغير الخريطة المصغرة

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

نضف بداية دالة تهيئة setter إلى الخاصية zoom:

@export var zoom = 1.5:
    set = set_zoom

func set_zoom(value):
    zoom = clamp(value, 0.5, 5)
    grid_scale = grid.size / (get_viewport_rect().size * zoom)

نصل في نافذة الفاحص اﻹشارة _gui_input بالعقدة MiniMap حتى نتمكن من معالجة أفعال تدوير عجلة الفأرة:

func _on_gui_input(event):
    if event is InputEventMouseButton and event.pressed:
      if event.button_index == MOUSE_BUTTON_WHEEL_UP:
        zoom += 0.1
      if event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
        zoom -= 0.1

فيما يلي نتيجة الشيفرة:

minimap_10.gif

الخاتمة

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

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

ترجمة -وبتصرف- للمقال: MinMap/Radar

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...