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

نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي InputEventMouse الذي يتضمن الخاصيتين position و global_position ويرث من هذا الصنف كل من الصنفين InputEventMouseButton و InputEventMouseMotion.

ملاحظة: بإمكاننا تعيين أحداث النقر على أزرار الفأرة من خلال الصنف InputMap وهو صنف متفرد singleton وبالتالي سنتمكن من استخدامها مع الدالة ()is_action_pressed.

استخدام الصنف InputEventMouseButton

يضم الصنف GlobalScope.ButtonList@ قائمة بكل ثوابت اﻷزرار الممكنة *_BUTTON التي قد نحددها في الخاصية button_index. ولنتذكر أن عجلة التمرير في الفأرة scrollwheel تُعد زرًا -أو زرين إن أردنا توخي الدقة- لأن الحدثين BUTTON_WHEEL_UP و BUTTON_WHEEL_DOWN منفصلان.

تلميح: يولد النقر على عجلة تمرير الفأرة الحدث pressed فقط، ولا يوجد حدث لتحرير الزر كما في اﻷزرار الأخرى.

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

func _unhandled_input(event):
    if event is InputEventMouseButton:
      if event.button_index == BUTTON_LEFT:
        if event.pressed:
          print("Left button was clicked at ", event.position)
        else:
          print("Left button was released")
      if event.button_index == BUTTON_WHEEL_DOWN:
        print("Wheel down")

استخدام الصنف InputEventMouseMotion

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

فيما يلي كود يوضح استخدام حركة الفأرة في تدوير شخصية ثلاثية الأبعاد حول المحور الأفقي، حيث تعتمد سرعة التدوير على حساسية الفأرة:

# حساسية الفأرة التي تتحكم في سرعة التدوير عند تحريك الفأرة
var mouse_sensitivity = 0.002

func _unhandled_input(event):
    if event is InputEventMouseMotion:
      rotate_y(-event.relative.x * mouse_sensitivity)

الاحتفاظ بمؤشر الفأرة ضمن نافذة اللعبة

بإمكاننا إخفاء مؤشر الفأرة ومنعها من مغادرة نافذة اللعبة، وهذا سلوك شائع في اﻷلعاب ثلاثية الأبعاد وحتى بعض اﻷلعاب ثنائية البعد. وللفأرة أربعة أنماط يمكنك اختيار أي منها باستخدام Input.mouse_mode:

  • MOUSE_MODE_VISIBLE: المؤشر مرئي ويمكن تحريكه بحرية داخل وخارج نافذة اللعبة
  • MOUSE_MODE_HIDDEN:المؤشر مخفي ويمكن له مغادرة نافذة اللعبة
  • MOUSE_MODE_CAPTURED:المؤشر مخفي ولا يمكن له مغادرة نافذة اللعبة
  • MOUSE_MODE_CONFINED:مؤشر الفأرة مرئي ولا يمكن له مغادرة نافذة اللعبة

الخيار الثالث Captured هو الخيار اﻷكثر شيوعًا، ويمكننا أيضًا ضبط حالة مؤشر الفأرة أثناء التنفيذ:

func _ready():
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

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

func _input(event):
    if event.is_action_pressed("ui_cancel"):
      Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

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

if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:

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

    if event.is_action_pressed("click"):
      if Input.mouse_mode == Input.MOUSE_MODE_VISIBLE:
        Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

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

get_tree().set_input_as_handled()

سحب وتحديد عدة عناصر باستخدام الفأرة

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

01 multi unit select

إعداد عناصر اللعب

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

إعداد عالم اللعبة

سنعالج عملية اختيار العناصر في عالم اللعبة، لهذا سننشئ هذا العالم من خلال اختيار عقدة Node2D وتسميتها World ثم إضافة نسخ من العناصر ضمنها. نضيف بعد ذلك سكريبتًا إلى العقدة World ثم نضيف المتغيرات التالية:

extends Node2D

var dragging = false # هل نسحب الفأرة حاليًا؟
var selected = [] # مصفوفة العناصر المسحوبة
var drag_start = Vector2.ZERO # موقع بداية السحب
var select_rect = RectangleShape2D.new() # شكل التصادم لصندوق السحب

نلاحظ أننا سنحتاج إلى طريقة لتحديد العناصر داخل صندوق السحب بمجرد رسمه. لهذا نستعمل العقدة Rectangle Shape2D التي تستعلم من محرك الفيزياء وتعرف العناصر التي اصطدم بها الصندوق.

رسم الصندوق

نستخدم زر الفأرة اﻷيسر في هذه الطريقة، إذ تبدأ عملية النقر برسم مربع السحب وتنهي عملية تحرير زر الفأرة الرسم، وبهذا يُرسم الصندوق أثناء سحب الفأرة:

func _unhandled_input(event):
    if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
      if event.pressed:
        # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب
        if selected.size() == 0:
          dragging = true
          drag_start = event.position
      # إن حُرر زر الفأرة ونحن في حالة سحب نوقف السحب
      elif dragging:
        dragging = false
        queue_redraw()
    if event is InputEventMouseMotion and dragging:
      queue_redraw()

func _draw():
    if dragging:
      draw_rect(Rect2(drag_start, get_global_mouse_position() - drag_start),
          Color.YELLOW, false, 2.0)

اختيار العناصر

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

نستخدم التابع ()PhysicsDirectSpaceState2D.intersect_shape ﻹيجاد العناصر، ويتطلب التابع رسم شكل مستطيل في حالتنا وإجراء تحويل transform للموقع:

elif dragging:
    dragging = false
    queue_redraw()
    var drag_end = event.position
    select_rect.extents = abs(drag_end - drag_start) / 2

نبدأ بتسجيل الموقع الذي حررنا فيه زر الفأرة، إذ نستخدمه في تحديد خاصية الامتداد extents للكائن RectangleShape2D (تقاس extents من مركز المستطيل فهي تمثل نصف الارتفاع أو الاتساع)

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

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

    # مساحة عالم اللعبة
    var space = get_world_2d().direct_space_state
    # استعلام بحث عن التصادم
    var query = PhysicsShapeQueryParameters2D.new()
    # تحديد الشكل الذي سنبحث عنه
    query.shape = select_rect
    # 2 تحديد العناصر في طبقة التصادم
    query.collision_mask = 2 
    # ضبط موقع الشكل
    query.transform = Transform2D(0, (drag_end + drag_start) / 2)
    # البحث عن التصادمات
    selected = space.intersect_shape(query)

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

ستكون النتيجة عند استدعاء التابع ()intersect_shape مصفوفة تتضمن معلومات عن الأجسام المتصادمة مع الشكل المستطيل، وتبدو كالتالي:

[{ "rid": RID(4093103833089), "collider_id": 32145147326, "collider": Unit2:<CharacterBody2D#32145147326>, "shape": 0 },
{ "rid": RID(4123168604162), "collider_id": 32229033411, "collider": Unit3:<CharacterBody2D#32229033411>, "shape": 0 }]

يدل كل متصادم collider في هذه المصفوفة إلى عنصر، لهذا يمكن استخدامه لتمييز العناصر التي اختيرت وتفعيل طار ملون يحيط بهذه العناصر:

    for item in selected:
      item.collider.selected = true

02 multi unit shader

إرسال اﻷوامر إلى العناصر

بإمكاننا اﻵن إرسال أمر التحرك للعناصر لتتجه نحو مكان ما على الشاشة:

func _unhandled_input(event):
    if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
      if event.pressed:
        # إن نُقر زر الفأرة ولم نختر شيء نبدأ عملية السحب
        if selected.size() == 0:
          dragging = true
          drag_start = event.position
        # وإلا ستخبر النقرة جميع العناصر المختارة بالتحرك
        else:
          for item in selected:
            item.collider.target = event.position
            item.collider.selected = false
          selected = []

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

الخاتمة

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

ترجمة -وبتصرف- للمقالات: Mouse Input و Capturing the mouse و Mouse:Drag-Select multiple units

اقرأ أيضًا


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

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

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



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

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

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

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


×
×
  • أضف...