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

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

آلية الالتفاف حول الشاشة

يُعَد السماح للاعب بالالتفاف حول الشاشة والانتقال الفوري من أحد جانبي الشاشة إلى الجانب الآخر ميزةً شائعة، وخاصة في الألعاب ثنائية الأبعاد القديمة مثل لعبة باك مان Pac-man، إذ يمكن السماح للاعب بالالتفاف حول الشاشة من خلال اتباع الخطوات.

الخطوة الأولى هي بالحصول على حجم الشاشة أو نافذة العرض Viewport كما يلي:

@onready var screen_size = get_viewport_rect().size

حيث تتوفر الدالة get_viewport_rect()‎ لأي عقدة مشتقة من CanvasItem.

أما ثاني خطوة، فتتمثل في مقارنة موضع اللاعب كما يلي:

if position.x > screen_size.x:
    position.x = 0
if position.x < 0:
    position.x = screen_size.x
if position.y > screen_size.y:
    position.y = 0
if position.y < 0:
    position.y = screen_size.y

وكما نلاحظ، تم استخدام موضع position العقدة الذي يكون عادةً مركزًا للشخصية الرسومية Sprite و/أو مركز الجسم.

ثالث خطوة هي عبر تبسيط ما سبق باستخدام الدالة wrapf()‎، إذ يمكن تبسيط الشيفرة البرمجية السابقة باستخدام الدالة wrapf()‎ في لغة GDScript، والتي تكرّر القيمة بين الحدود المحدّدة.

position.x = wrapf(position.x, 0, screen_size.x)
position.y = wrapf(position.y, 0, screen_size.y)

برمجة الحركة من الأعلى إلى الأسفل

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

اسم الإجراء المفتاح أو مجموعة المفاتيح
"up" المفتاح W أو ↑
"down" المفتاح S أو ↓
"right" المفتاح D أو →
"left" المفتاح A أو ←
"click" زر الفأرة 1

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

الخيار الأول: الحركة في 8 اتجاهات

يستخدم اللاعب في هذه الحالة مفاتيح الاتجاهات الأربعة للتحرك (بما في ذلك الاتجاهات القطرية) كما يلي:

extends CharacterBody2D

var speed = 400  # السرعة بالبكسلات في الثانية

func _physics_process(delta):
    var direction = Input.get_vector("left", "right", "up", "down")
    velocity = direction * speed

    move_and_slide()

الخيار الثاني: التدوير والتحريك

تدوّر الإجراءات لليسار/لليمين "left/right" في هذه الحالة الشخصية وتحرّك الإجراءات للأعلى/للأسفل الشخصية للأمام وللخلف في أيّ اتجاه تواجهه، ويُشار إلى ذلك أحيانًا باسم "الحركة التي تحاكي حركة الكويكبات".

extends CharacterBody2D

var speed = 400  # سرعة الحركة بالبكسل/ثانية
var rotation_speed = 1.5  # سرعة الدوران بالراديان/ثانية

func _physics_process(delta):
    var move_input = Input.get_axis("down", "up")
    var rotation_direction = Input.get_axis("left", "right")
    velocity = transform.x * move_input * speed
    rotation += rotation_direction * rotation_speed * delta
    move_and_slide()

ملاحظة: يَعُد محرّك ألعاب جودو Godot أن الزاوية 0 درجة تؤشّر على طول المحور x، مما يعني أن اتجاه العقدة للأمام (transform.x) يتجه إلى اليمين، لذا يجب التأكد من رسم الشخصية الرسومية لتشير إلى اليمين.

الخيار الثالث: التصويب بالفأرة

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

extends CharacterBody2D

var speed = 400  # سرعة الحركة بالبكسل/ثانية

func _physics_process(delta):
    look_at(get_global_mouse_position())
    var move_input = Input.get_axis("down", "up")
    velocity = transform.x * move_input * speed
    move_and_slide()

الخيار الرابع: النقر والتحريك

تنتقل الشخصية في هذا الخيار إلى الموقع الذي نقرنا عليه.

extends CharacterBody2D

var speed = 400  # سرعة الحركة بالبكسل/ثانية
var target = null

func _input(event):
    if event.is_action_pressed("click"):
        target = get_global_mouse_position()

func _physics_process(delta):
    if target:
        # look_at(target)
        velocity = position.direction_to(target) * speed
        if position.distance_to(target) < 10:
            velocity = Vector2.ZERO
    move_and_slide()

وكما نلاحظ، سنتوقف عن الحركة إذا اقتربنا من موضع الهدف، فإن لم نفعل ذلك، فستهتز الشخصية ذهابًا وإيابًا بحيث تتحرك قليلًا بعد الهدف وتعود، ثم تعاود الكرّة وهكذا. يمكننا اختياريًا استخدام الدالة look_at()‎ لتكون مواجهةً لاتجاه الحركة.

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

تحريك الشخصيات بالاعتماد على الشبكة Grid

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

إعداد الشخصية

سنوضح فيما يلي العقد التي سنستخدمها للاعب:

  • Area2D (اللاعب "Player"): يمثّل استخدام العقدة Area2D أنه يمكننا اكتشاف التداخل لالتقاط الأشياء أو الاصطدام بالأعداء
  • Sprite2D: يمكن استخدام ورقة الشخصية الرسومية Sprite Sheet هنا، وسنوضّح إعداد الرسوم المتحركة Animations لاحقًا
  • CollisionShape2D: يجب أن لا نجعل مربع الاصطدام كبيرًا جدًا، حيث ستكون التداخلات من عند المركز لأن اللاعب سيقف في منتصف المربع
  • RayCast2D: للتحقق مما إذا كانت الحركة ممكنة في اتجاه محدّد
  • AnimationPlayer: لتشغيل الرسوم المتحركة الخاصة بمشي الشخصية

سنضيف هنا بعض إجراءات الإدخال إلى خريطة الإدخال Input Map، ويمكننا لأجل ذلك استخدام إجراءات up وdown وleft، و right في هذا المثال.

الحركة الأساسية

سنبدأ بإعداد الحركة على المربعات الواحد تلو الآخر دون أي رسوم متحركة أو استيفاء Interpolation.

extends Area2D

var tile_size = 64
var inputs = {"right": Vector2.RIGHT,
    "left": Vector2.LEFT,
    "up": Vector2.UP,
    "down": Vector2.DOWN}

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

func _ready():
    position = position.snapped(Vector2.ONE * tile_size)
    position += Vector2.ONE * tile_size/2

تتيح الدالة snapped()‎ تقريب الموضع إلى أقرب زيادة في المربع، وتضمَن إضافة كمية بمقدار نصف مربع أن يكون اللاعب في مركز المربع.

func _unhandled_input(event):
    for dir in inputs.keys():
        if event.is_action_pressed(dir):
            move(dir)

func move(dir):
    position += inputs[dir] * tile_size

تمثل الشيفرة البرمجية السابقة الحركة الفعلية. حيث إذا ظهر حدث إدخال، فسنتحقق من الاتجاهات الأربعة لمعرفة أيّ منها يتطابق مع الحدث، ثم نمرّره إلى الدالة move()‎ لتغيير الموضع.

01 grid example1

الاصطدام Collision

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

onready var ray = $RayCast2D

func move(dir):
    ray.target_position = inputs[dir] * tile_size
    ray.force_raycast_update()
    if !ray.is_colliding():
        position += inputs[dir] * tile_size

إذا غيّرنا الخاصية target_position الخاصة بعقدة RayCast2D، فلن يعيد محرّك الفيزياء حساب تصادماته حتى الإطار الفيزيائي التالي. تتيح الدالة force_raycast_update()‎ إمكانية تحديث حالة الشعاع مباشرةً، حيث إن لم يكن هناك تصادم، فسنسمح بالتحرك.

02 grid example2

ملاحظة: تتمثل إحدى الطرق الشائعة الأخرى في استخدام أربع عقد RayCast2D من خلال استخدام عقدة لكل اتجاه.

تنشيط الحركة

وأخيرًا، يمكننا استيفاء الموضع بين المربعات، مما يعطي إحساسًا سلسًا بالحركة، حيث سنستخدم عقدة Tween لتحريك خاصية الموضع position.

var animation_speed = 3
var moving = false

سنضيف الآن مرجعًا إلى عقدة Tween ومتغيرًا لضبط سرعة الحركة كما يلي:

func _unhandled_input(event):
    if moving:
        return
    for dir in inputs.keys():
        if event.is_action_pressed(dir):
            move(dir)

سنتجاهل أي إدخال أثناء تشغيل الانتقال التدريجي Tween ونزيل تغيير الموضع position المباشر حتى يتمكّن الانتقال التدريجي من التعامل معه.

func move(dir):
    ray.target_position = inputs[dir] * tile_size
    ray.force_raycast_update()
    if !ray.is_colliding():
        #position += inputs[dir] * tile_size
        var tween = create_tween()
        tween.tween_property(self, "position",
            position + inputs[dir] *    tile_size, 1.0/animation_speed).set_trans(Tween.TRANS_SINE)
        moving = true
        await tween.finished
        moving = false

03 grid example3

سنجرِّب الآن انتقالات تدريجية مختلفة للحصول على تأثيرات حركية مختلفة.

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

تحريك الشخصيات في 8 اتجاهات مختلفة

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

04 8 direction 01

تُنظَّم الرسوم المتحركة ضمن مجلدات، مع وجود صورة منفصلة لكل إطار. سنستخدم العقدة AnimatedSprite2D وسنسمّي الرسوم المتحركة بناءً على اتجاهها.

على سبيل المثال، يشير الرسم المتحرك idle0 إلى اليمين، ثم ننتقل باتجاه عقارب الساعة حتى الوصول إلى الرسم المتحرك idle7، وتختار الشخصية عند تحرّكها رسومًا متحركة بناءً على اتجاه الحركة:

05 8 direction 03w

سنستخدم الفأرة للتحرك، حيث ستواجه الشخصية الفأرة دائمًا وتتحرك في هذا الاتجاه عندما نضغط على زر الفأرة. يمكن اختيار الرسوم المتحركة التي ستعمل من خلال الحصول على اتجاه الفأرة وربطه مع هذا المجال نفسه من 0 إلى 7؛ إذ تعطي الدالة get_local_mouse_position()‎ موضع الفأرة بالنسبة للشخصية، ويمكننا بعد ذلك استخدام الدالة snappedf()‎ لضبط زاوية متجه الفأرة إلى أقرب مضاعف للزاوية 45 درجة (أو PI/4 راديان) مما يعطي النتيجة التالية:

06 8 direction 04w

سنقسّم كل قيمة على 45 درجة ( أو PI/4 راديان) ونحصل على النتيجة التالية:

07 8 direction 02w

في الأخير، يجب ربط المجال الناتج مع المجال ‎0-7 باستخدام الدالة wrapi()‎، وسنحصل على القيم الصحيحة، حيث تعطي إضافة هذه القيمة إلى نهاية اسم الرسوم المتحركة ("idle" و "run" وغيرها) الرسم المتحرك الصحيح كما يلي:

func _physics_process(delta):
    current_animation = "idle"

    var mouse = get_local_mouse_position()
    angle = snappedf(mouse.angle(), PI/4) / (PI/4)
    angle = wrapi(int(angle), 0, 8)

    if Input.is_action_pressed("left_mouse") and mouse.length() > 10:
        current_animation = "run"
        velocity = mouse.normalized() * speed
        move_and_slide()
    $AnimatedSprite2D.animation = current_animation + str(a)

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

08 8 direction 05

الإدخال من لوحة المفاتيح

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

func _process(delta):
    current_animation = "idle"
    var input_dir = Input.get_vector("left", "right", "up", "down")
    if input_dir.length() != 0:
        angle = input_dir.angle() / (PI/4)
        angle = wrapi(int(a), 0, 8)
        current_animation = "run"
    velocity = input_dir * speed
    move_and_slide()
    $AnimatedSprite2D.play(current_animation + str(angle))

الخاتمة

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

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

ترجمة -وبتصرّف- للأقسام Screen wrap و Top-down movement و Grid-based movement و 8-Directional Movement/Animation من توثيقات 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.


×
×
  • أضف...