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

ابراهيم الخضور

الأعضاء
  • المساهمات

    163
  • تاريخ الانضمام

  • تاريخ آخر زيارة

آخر الزوار

6402 زيارة للملف الشخصي

إنجازات ابراهيم الخضور

عضو نشيط

عضو نشيط (3/3)

84

السمعة بالموقع

  1. نشرح في هذا المقال طريقة إنشاء جسم يمثل سفنية تتحرك بطريقة واقعية في الفضاء باستخدام عقدة الجسم الصلب RigidBody2D، حيث يوفر محرك الألعاب جودو أنواعًا مختلفة من الأجسام الفيزيائية ومن ضمنها عقدة RigidBody2D التي تناسب جسمًا يتحرك تلقائيًا وفقًا لقوانين الفيزياء. قد يكون استخدام هذه العقدة مربكًا بعض الشيء لأنها غير قابلة للتحريك المباشر بطريقة مشابهة لعقدة الشخصية CharacterBody2D ومن يتحكم بالكامل هو محرك فيزياء جودو الداخلي Godot physics engine، وبالتالي لا يمكننا ببساطة تغيير موقعها مباشرة من أجل تحريكها بل علينا تطبيق قوى فيزيائية عليها لتحقيق الحركة المطلوبة. ننصح قبل بدء العمل بإلقاء نظرة على توثيق الواجهة البرمجية RigidBody2D لفهم خصائصها بشكل أعمق. بناء الحركة سننشئ مشهد جديد ثنائي الأبعاد في جودو ونستخدم فيه العقد التالية: RigidBody2D (Ship) ├── Sprite2D └── CollisionShape2D وإليكم وظيفة كل عقدة منها: تمثل RigidBody2D(Ship) العقدة الرئيسية للجسم الصلب الذي نريد تحريكه، والذي يتفاعل مع البيئة ويتأثر بقوانين الفيزياء تمثل العقدة Sprite2D الشكل المرئي للجسم الصلب، وهي في حالتنا صورة سفينة الفضاء التي تتحرك تلقائيًا مع الجسم الفيزيائي الصلب تستخدم العقدة CollisionShape2D لتحديد شكل وحجم منطقة التصادم للجسم الصلب، وهي ضرورية كي تتعرف الفيزياء على حدود الجسم وتتفاعل معه بشكل صحيح عند اصطدامه بأجسام أخرى توجيه الشخصية عندما نحرك جسم صلب RigidBody2D في جودو، فإن اتجاهه الأمامي الافتراضي أي الاتجاه الذي يتحرك فيه يُعد دومًا هو الاتجاه الموجب للمحور X أي نحو اليمين في المشهد، لذا علينا في البداية توجيه الجسم الصلب بالشكل الصحيح إذا لم يكن كذلك. هذا الأمر مهم لأننا عندما نطبق قوى أو سرعة على الجسم فإن هذه القيم تُحسب بناءً على المحاور المحلية للجسم، لذا إذا لم يكن الجسم موجّهًا أصلاً نحو اليمين فستكون حركته غير صحيحة أو غير متوقعة. وإن كانت الأيقونة أو صورة الشخصية تتجه باتجاه معاكس، علينا أن ندوّر العقدة Sprite2D وليس الجسم الأب نفسه RigidBody2Dحتى نوجهها بالشكل الصحيح. كما سنستخدم المدخلات التالية في خريطة الإدخال Input Map: المُدخل المفتاح thrust w أو ↑ rotate_right d أو → rotate_left a أو ← بعدها، سنضيف سكريبت إلى الجسم الصلب ونعرّف فيه بعض المتغيرات: extends RigidBody2D @export var engine_power = 800 @export var spin_power = 10000 var thrust = Vector2.ZERO var rotation_dir = 0 يحدد أول متغيران كيفية التحكم بحركة سفينة الفضاء. إذ يتحكم المتغير engine_power بالتسارع أي سرعة الحركة للأمام، بينما يتحكم المتغير spin_power بسرعة دوران السفينة. ويُضبط كل من thrust و rotation_dir بناءً على مدخلات المستخدم، وهذا ما سنفعله تاليًا: func get_input(): thrust = Vector2.ZERO if Input.is_action_pressed("thrust"): thrust = transform.x * engine_power rotation_dir = Input.get_axis("rotate_left", "rotate_right") في البداية، تكون قيمة thrust صفرًا، مما يعني أن السفينة الفضائية لا تتحرك إلى الأمام. عندما يضغط المستخدم مفتاح الدفع مثل السهم العلوي أو مفتاح W، فإننا نضبط thrust بحيث يحدد الشعاع الذي يوجه الحركة الأمامية للسفينة. في الوقت نفسه، تتغير قيمة rotation_dir بمقدار 1 بناءً على مفتاح الإدخال الذي يضغط عليه المستخدم، سواء كان لليمين أو اليسار. يمكن للسفينة الفضائية البدء بالطيران عند تطبيق القيم التالية في الدالة (physics_process(delta_. func _physics_process(_delta): get_input() constant_force = thrust constant_torque = rotation_dir * spin_power ستحلق المركبة الآن في الفضاء، لكن سنجد صعوبة في التحكم بها. حيث سيكون الدوران سريعًا جدًا، وستتسارع بشدة لتغادر الشاشة بعدها، وهنا لا بد من التوقف عن تطبيق فيزياء الفضاء الحقيقية، فطالما أنه لا احتكاك في الفضاء ستتسارع المركبة بسرعة، لهذا من الأسهل في مثالنا أن ندفع المركبة لتتوقف عندما لا تطبَّق عليها قوة دفع، ويمكننا تنفيذ هذا الأمر باستخدام التخميد damping. لننتقل إلى خصائص العقدة ومنها إلى Linear ثم Damp و Angular ثم Damp ونضبط قيمتي هاتين الخاصيتين على 1 و 2 على الترتيب. تحدّ هاتان القيمتان من سرعة الحركة وسرعة الدوران مما يسبب توقف السفينة الفضائية. ملاحظة: يمكن أن نجرّب تغيير قيم هذه الخاصيات ونرى كيف تتفاعل مع engine_power و spin_power. العودة إلى الشاشة تشبه عملية إعادة سفينة الفضاء إلى الجهة المقابلة من الشاشة بعد خروجها من أحد الأطراف انتقالًا لحظيًا عبر المكان. لكن إن حاولنا تعديل خاصية position مباشرةً، فستقفز السفينة فورًا إلى الموقع الجديد، وقد يؤدي ذلك إلى سلوك غير متوقع، لأن محرّك فيزياء جودو يستمر في التحكم بحركة الجسم الصلب. للتغلب على هذه المشكلة، نستخدم دالة رد النداء ()integrate_forces_ الخاصة بالعقدة RigidBody2D، والتي تتيح لنا تعديل الخصائص الفيزيائية مثل الموقع والسرعة بشكل مباشر ومتزامن مع دورة محرك الفيزياء دون أن تتعارض معها. لننتقل الآن إلى screensize في أعلى السكريبت: @onready var screensize = get_viewport_rect().size نضيف بعد ذلك دالة جديدة باسم ()integrate_forces_: func _integrate_forces(state): var xform = state.transform xform.origin.x = wrapf(xform.origin.x, 0, screensize.x) xform.origin.y = wrapf(xform.origin.y, 0, screensize.y) state.transform = xform نلاحظ أن الدالة ()integrate_forces_ تستقبل معاملًا باسم state، وهو كائن من النوع PhysicsDirectBodyState2D يمثل الحالة الفيزيائية الحالية للجسم الصلب. يحتوي هذا الكائن على معلومات مثل موضع الجسم position والقوى المؤثرة forces والسرعة velocity في الاتجاهات المختلفة وغيرها من الخصائص الفيزيائية. وبالتالي يمكن أن نحصل من خلال state على التحويل الحالي للجسم current transform أي موقعه واتجاهه، ثم نعدّل إحداثيات الموقع باستخدام الدالة ()wrapf لتطبيق التفاف حول الشاشة مما يعني أن الجسم سيتجاوز حافة الشاشة ويظهر من الجهة المقابلة لإعطاء تأثير الحركة المستمرة. وأخيرًا، نُعيد ضبط التحويل الجديد داخل state لضمان استمرار حركة الجسم بشكل طبيعي وواقعي. سيبدو الأمر كما في الصورة التالية: الانتقال الفوري لنلقِ نظرة على مثال آخر يوضح استخدام ()integrate_forces_ لتغيير حالة الجسم دون مشكلات. لنضف آلية انتقال آني أو فوري بحيث يتمكن اللاعب من نقل المركبة الفضائية فورًا إلى موقع عشوائي داخل الشاشة عند الضغط على مفتاح مخصص. نضيف بداية متغيرًا جديدًا: var teleport_pos = null نحضّر تاليًا موقعًا عشوائيًا ضمن الدالة ()get_input: if Input.is_action_just_pressed("warp"): teleport_pos = Vector2(randf_range(0, screensize.x), randf_range(0, screensize.y)) وأخيرًا وضمن الدالة ()integrate_forces_ سنستخدم teleport_position إن كان موجودًا ومن ثم نمسحه: if teleport_pos: physics_state.transform.origin = teleport_pos teleport_pos = null وبهذا نكون قد أضفنا ميزة الانتقال الفوري للمركبة الفضائية بشكل آمن ومتوافق مع نظام فيزياء جودو. الخاتمة بهذا نكون وصلنا لختام هذا المقال الذي استعرضنا فيه كيفية استخدام محرك الفيزياء في جودو لتحريك الأجسام الصلبة والتحكم بها بشكل دقيق لتوفير تجربة حركة تفاعلية واقعية ضمن بيئات ثنائية وثلاثية الأبعاد مع التحكم الكامل في حركة الأجسام. بإمكانك تحميل المثال كاملًا عبر مستودعه على جيتهب أو من هنا مباشرة asteroids_physics.zip ترجمة -وبتصرف- للمقال: Asteroids-style Physics (using RigidBody2D) اقرأ أيضًا المقال السابق: التفاعل بين الشخصيات والأجسام الصلبة في جودو العمل مع إجراءات الدخل Inputs Actions في جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو التحريك باستخدام SpriteSheet و AnimationTree StateMachine في جودو
  2. عند تطوير الألعاب باستخدام جودو، من الشائع أن نربط الأصوات بالأحداث التي تقوم بها الشخصيات، كتشغيل صوت معين عند موت الشخصية وصوت آخر عندما تنفذ هجومًا معينًا. ونستخدم غالبًا العقدة AudioStreamPlayer لتشغيل هذه الأصوات لكن هناك مشكلة شائعة قد تواجهنا، فعند إزالة الشخصية من المشهد بسبب موتها أو لأي سبب آخر ستزال كل العقد التابعة لها، بما في ذلك عقدة مشغل الصوت. ونتيجة لذلك، سيتوقف الصوت فجأة، حتى لو لم يكن قد اكتمل تشغيله بعد، وهذه التجربة قد تكون مزعجة للاعب، لأنها تجعل اللعبة تبدو غير طبيعية. سنشرح في الفقرات التالية طريقة مناسبة لحل هذه المشكلة. مشروع مشغل الصوت سنعتمد على عقدة AudioStreamPlayer مستقلة يمكن وضعها في أي مكان داخل شجرة المشهد Scene Tree في محرك ألعاب جودو، لكن من الأفضل أن تكون هذه العقدة مستقلة عن الكائن أو الشخصية. بمعنى ستكون هذه العقدة مسؤولة عن تشغيل المقاطع الصوتية المتعلقة بالأحداث المختلفة في اللعبة، مثل صوت موت الشخصية أو تأثيرات البيئة، دون أن تتأثر بإزالة الكائنات من المشهد. لتحقيق ذلك سننشئ مشروع جودو جديد وننشئ ضمنه مشهد جديد ونحفظه باسم audio_demo.tscn أو أي اسم آخر مناسب ونضيف له مجموعة عقد وفق التسلسل الهرمي التالي: AudioDemo (MarginContainer) ├── CenterContainer (CenterContainer) │ └── GridContainer (GridContainer) ├── CanvasLayer (CanvasLayer) │ └── HBoxContainer (HBoxContainer) │ ├── Label (Label) │ ├── Label2 (Label) │ ├── VSeparator (VSeparator) │ ├── Label3 (Label) │ └── Label4 (Label) يتكوّن المشروع من عقدة جذر من نوع MarginContainer سنسميها AudioDemo، وهي المسؤولة عن تنظيم المحتوى. وننشئ بداخلها عقدة ابن من نوع CenterContainer لمحاذاة المحتويات في المنتصف، والتي سنصيف لها عقدة ابن جديدة GridContainer لتوليد الأزرار الخاصة بتشغيل الملفات الصوتية. كما سنضيف عقدة CanvasLayer لعرض واجهة المستخدم ونضيف لها عقدة ابن من نوع HBoxContainer وضمنها عدة عناصر تسمية Label لعرض إحصائيات عن الملفات الصوتية التي نشغلها، وعدد الملفات الصوتية الموجودة في قائمة الانتظار وعقدة VSeparator للفصل بين العناصر. بعدها نضيف مجلد الموارد assets الذي يتضمن مجموعة ملفات صوتية، ويمكنك الحصول عليه من خلال تحميل هذا المشروع من مستودع جيتهب أو من هنا مباشرة audio_manager.zip. توضح الصورة التالية شكل واجهة مشروعنا الذي سنبنيه بجودو، حيث سيعرض زر لكل ملف صوتي ضمن المجلد assets ويوّلد وفقًا لعددها شبكة مكونة من مجموعة من الأزرار التي تشغل كل ملف صوتي بمجرد النقر على كل منها. وسنرى في أعلى نافذة المشروع إحصائيات عن مدير الصوت. الكود البرمجي لمدير الصوت بعد إضافة العقد المطلوبة وتخصيصها نضيف في البداية سكريبت جديد في محرر جودو ونحفظه باسم audio_manager.gd ونكتب فيه الكود التالي: extends Node var num_players = 8 var bus = "master" var available = [] # المشغلات المتاحة var queue = [] # رتل الأصوات التي ستُشغل func _ready(): # AudioStreamPlayer ننشئ حلقة من العقد for i in num_players: var player = AudioStreamPlayer.new() add_child(player) available.append(player) player.finished.connect(_on_stream_finished.bind(player)) player.bus = bus func _on_stream_finished(stream): # نجعل المشغل متاحًا مجددًا بعد الانتهاء من تشغيل المقطع الصوتي available.append(stream) func play(sound_path): queue.append(sound_path) func _process(delta): # نشغل الأصوات في الرتل إن وجد أي لاعب if not queue.empty() and not available.empty(): available[0].stream = load(queue.pop_front()) available[0].play() available.pop_front() أنشأنا في الكود السابق مجموعة من عقد AudioStreamPlayer وعددها هو num_players وأسندنا لها القيمة الافتراضية 8 وخزناها في قائمة باسم available. كلما أردنا تشغيل صوت نضيفه لرتل أو قائمة انتظار باسم queue مهمته تخزين الأصوات التي نريد تشغيلها لاحقًا، بعد انتهاء تشغيل أي صوت، يعاد المشغل إلى القائمة available ليكون جاهزًا للاستخدام مجددًا. سنضبط هذا السكريبت حتى يُحمّل تلقائيًا Auto-load من إعدادات المشروع كي نتمكن من استدعائه من أي مكان بسهولة، للقيام بذلك سنفتح إعدادات المشروع من خلال القائمة Project ثم Project Settings، نذهب بعدها إلى تبويب Global ثم لتبويب التحميل التلقائي AutoLoad كما في الصورة التالية ونختار ملف السكريبت audio_manager.gd من ملفات المشروع، وفي خانة Node Name نمنحه اسمًا سهل التمييز مثل AudioManager ثم نضغط على Add، وبهذا سيضاف السكربت إلى قائمة التحميل التلقائي، وستُفعَّل خانة Enabled تلقائيًا. بهذا أصبح عندنا نظام صوت مركزي ثابت ومتاح في كل مكان داخل اللعبة، ويمكن أن نستدعيه من أي مكان من مشروعنا نريد فيه تشغيل الصوت بكتابة التالي: AudioManager.play("res://path/to/sound") ملاحظة: يمكن سحب ملفات الصوت مباشرة إلى المحرر النصي في محرك جودو، مما يتيح لنا لصق المسار الخاص بالملف الصوتي في السكريبت بسهولة بدلًا من كتابة المسار يدوياً. الكود البرمجي لواجهة المشروع الديناميكية الخطوة التالية التي سنقوم بها هي توليد الواجهة الديناميكية المكونة من زر لتشغيل كل ملف صوتي، لتحقيق ذلك نلحق سكريبت للعقدة الجذر AudioDemo ونكتب فيه الكود التالي لتشغيل كافة الملفات الصوتية الموجودة في مجلد المشروع: extends MarginContainer # المجلد الذي يحتوي على الملفات الصوتية @export var sound_dir: String = "res://assets" func _ready(): # نحمل كل الملفات الصوتية الموجودة في المجلد var dir = DirAccess.open(sound_dir) if dir: dir.list_dir_begin() var file_name = dir.get_next() while file_name != "": if file_name.get_extension() in ["wav", "ogg"]: add_button(file_name) file_name = dir.get_next() dir.list_dir_end() func add_button(file_name): # إضافة زر لتشغيل الملف الصوتي المخصص له var b = Button.new() $CenterContainer/GridContainer.add_child(b) b.add_theme_font_override("font", load("res://assets/Poppins-Medium.ttf")) b.text = file_name b.pressed.connect(on_audio_button_pressed.bind(b)) func on_audio_button_pressed(button): # تشغيل الصوت المرتبط بالزر var path = sound_dir + "/" + button.text AudioManager.play(path) func _process(delta): # تحديث عدد المشغلات المتاحة وعدد الأصوات في قائمة الانتظار $CanvasLayer/HBoxContainer/Label2.text = str(AudioManager.available.size()) $CanvasLayer/HBoxContainer/Label3.text = str(AudioManager.queue.size()) الخاتمة أنشأنا في هذا المقال نظامًا متكاملًا لإدارة وتشغيل المؤثرات الصوتية في محرك جودو بطريقة مستقرة تضمن استمرار تشغيل الأصوات حتى بعد إزالة الشخصية أو العنصر المرتبط بها، مما يحسّن تجربة اللعب ويجعلها أكثر واقعية وذلك من خلال استخدام التحميل التلقائي للسكريبت الذي يوفّر علينا الجهد في تكرار الكود، ويمنحنا تحكمًا مركزيًا بالملفات الصوتية ضمن اللعبة. ترجمة -وبتصرف- للمقال: Audio manager اقرأ أيضًا المقال السابق: تحريك جسم صلب RigidBody2D بواقعية في الفضاء باستخدام جودو الطريقة الصحيحة للتواصل بين العقد في جودو Godot مدخل إلى محرك الألعاب جودو Godot إضافة النقاط واللعب مجددًا وتأثيرات الصوت للعبة 3D ضمن جودو إضافة المؤثرات الصوتية للعبة المطورة باستخدام بايثون ومكتبة Pygame
  3. بدأنا في المقال السابق شرح بعض المفاهيم الرياضية الأساسية التي يحتاج مطور الألعاب لمعرفتها، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء النقطي Dot Product، والجداء الشعاعي Cross Product. وسنستكمل في مقال اليوم شرح مفهوم رياضي مهم وهو التحويلات الهندسية Transforms الذي يسمح لنا بتغيير مكان أو شكل الأشياء في الفضاء باستخدام المصفوفات. متطلبات العمل قبل المتابعة في قراءة هذا المقال، يجب توفر دراية جيدة عن اﻷشعة vectors وكيفية استخدامها في تطوير اﻷلعاب. لهذا ننصح بالعودة إلى سلسلة مقالات الأشعة على أكاديمية حسوب، ومطالعة مقال رياضيات اﻷشعة ضمن توثيق جودو الرسمي. التحويلات في المستوي ثنائي البعد نستخدم في المستوي أو في الفضاء ثنائي البعد اﻹحداثيات المألوفة X و Y، ولنتذكر أنه في محرك ألعاب جودو وفي معظم التطبيقات الرسومية في الحواسيب، يشير المحور Y إلى اﻷسفل كما في الصورة التالية: ولكي نوضح الفكرة، لنتأمل شكل سفينة الفضاء ثنائي البعد في الصورة التالية: تشير السفينة هنا إلى نفس اتجاه المحور X، فلو أردنا منها التحرك نحو اﻷمام، نضيف مقدار الحركة إلى الإحداثي اﻷفقي X فتتحرك نحو اليمين: position += Vector2(10, 0) لكن ما الذي يحدث عندما تدور السفينة؟ كيف يمكن اﻵن تحريكها نحو اﻷمام؟ إن كنتم تتذكرون علم المثلثات في المدرسة، فقد تبدأون بالتفكير في الزوايا والنسب المثلثية sin و cos، ثم تنفذون عملية حسابية مثل: position += Vector2(10 * cos(angle), 10 * sin(angle)) سيعمل هذا الحل، لكن هناك طرق أفضل تلائم عملنا مع اﻷلعاب تعتمد بشكل أساسي على مفهوم التحويلات الهندسية transforms. لنلق نظرة مجددًا على السفينة التي تدور، ولنتخيل هذه المرة أن للسفينة منظومة إحداثياتها الخاصة التي تحملها معها ولا تتعلق بإحداثيات الشاشة العامة: تُخزّن هذه اﻹحداثيات المحلية ضمن الكائن transform، وبالتالي، يمكن تحريك السفينة إلى اﻷمام وفقًا للمحور X الخاص بها ولا حاجة أن نفكر بالزوايا والدوال الرياضية اﻷخرى. ولتنفيذ اﻷمر في جودو، نستخدم الخاصية transform التي تمتلكها جميع العقد المشتقة من Node2D. position += transform.x * 10 ينص السطر السابق على إضافة الشعاع X للتحويل مضروبًا بالعدد 10. لنشرح هذا الأمر بشيء من التفصيل، تضم الخاصية transform اﻹحداثيين x و y الممثلان للإحداثيات المحلية الخاصة بالعقدة، وهما شعاعي واحدة unit vector أي أن طويلة كل منهما تساوي الواحد. كما يطلق على هذان الشعاعان شعاعي توجيه direction vectors، ويدلان على الاتجاه الذي يشير إليه المحور X الخاص بالسفينة. نضرب بعد ذلك شعاعي التوجيه بالعدد 10 لتكبيرهما والانتقال إلى مسافة أبعد. ملاحظة: تتعلق الخاصية transform لعقدة بالعقدة اﻷم لها أي تنسب إحداثياتها الخاصة إلى إحداثيات العقدة اﻷم. فإن أردنا الحصول على اﻹحداثيات العامة بالنسبة إلى الشاشة، نستخدم global_transform. تضم الخاصية transform إضافة إلى المحاور المحلية، مكونًا يُدعى شعاع الأصل origin ويمثل اﻹنسحاب translation أو تغيير الموضع. يمثل الشعاع اﻷزرق في الصور التالية شعاع اﻷصل transform.origin ويساوي شعاع الموضع position للكائن: التحويل بين الفضاء المحلي والعام يمكننا تحويل اﻹحداثيات من الفضاء المحلي للعقدة إلى الفضاء العام عن طريق التحويلات. حيث تضم العقد من النوع Noode2D والنوع Spatial في جودو دوال برمجية مساعدة مثل ()to_local و ()to_global لتحقيق هذا الأمر: var global_position = to_global(local_position) إليكم مثالًا عن كائن في مستوي ثنائي البعد، ونريد تغيير موقع نقرة الفأرة -وهو في الفضاء العام- أي المكان الذي نقرنا فيه على الشاشة، إلى إحداثيات محلية منسوبة إلى الكائن، بمعنى آخر نريد معرفة مكان النقرة من منظور الكائن نفسه بدلاً من المكان على الشاشة، لتحقيق ذلك نكتب الكود التالي: extends Sprite func _unhandled_input(event): if event is InputEventMouseButton and event.pressed: if event.button_index == BUTTON_LEFT: printt(event.position, to_local(event.position)) للمزيد حول آلية التحويل من إحداثيات عامة إلى إحداثيات محلية في محرك جودو ننصحكم بقراءة توثيق Transform2D للاطلاع على كافة الخاصيات والتوابع المتاحة. التحويلات في الفضاء ثلاثي البعد يطبق مفهوم التحويل في الفضاء ثلاثي البعد 3D بنفس أسلوب تطبيقه في الفضاء ثنائي البعد 2D، بل يغدو تطبيقها أهم لأن العمل مع الزوايا في الفضاء ثلاثي البعد سيقود إلى مشكلات عديدة كما سنوضح بعد قليل. ترث العقد ثلاثية البعد من العقدة الأساسية Node3D التي تضم معلومات التحويل. ويحتاج التحويل في الفضاء ثلاثي البعد لمعلومات أكثر مقارنة مع الفضاء الثنائي البعد. حيث يبقى شعاع الموضع Position محفوظًا ضمن الخاصية Origin، لكن الدوران موجود ضمن خاصية تدعى basis تضم ثلاثة أشعة واحدة unit vectors تمثل المحاور اﻹحداثية المحلية الثلاث للعقدة X و Y و Z. وعندما نختار عقدة ثلاثية البعد في محرر جودو، سنتمكن باستخدام نافذة Gizmo من عرض التحويلات والتعامل معها. تفعيل نمط الفضاء المحلي Local Space لنتذكر أن الفضاء العام Global Space هو الفضاء الذي يعتمد على محاور المشهد العامة. بمعنى آخر، إذا كنا نحرك أو ندير كائنًا في هذا الفضاء، فإن تحركاته ستكون بناءً على محاور العالم أو المشهد الذي يوجد فيه هذا الكائن، أما الفضاء المحلي Local Space فهو الفضاء الذي يعتمد على محاور الكائن نفسه. أي أن للكائن لديه محاور خاصة به مثل المحور X و Y و Z الخاص به وعندما نحرك أو ندير الكائن في الفضاء المحلي، فإن تحركاته تكون بالنسبة له هو، وليس بالنسبة للمشهد بأكمله. يتيح لنا محرر جودو عرض الاتجاهات المحلية للجسم والتعامل معها بسهولة، وذلك من خلال تفعيل خيار Local Space Mode، مما يسمح بتحريك الجسم أو تدويره وفقًا لمحاوره الخاصة بدلًا من محاور المشهد العامة، وستمثل المحاور الثلاث الملونة في هذا الوضع المحاور اﻷساسية المحلية للجسم. وكما هو الحال في الفضاء ثنائي البعد، يمكننا في الفضاء ثلاثي الأبعاد استخدام المحاور المحلية لتحريك الجسم إلى اﻷمام. وفي هذه الحالة، يكون المحور Y Y-Upوفق نظام Y-Up أي أنه موجه نحو الأعلى، وبالتالي سيكون الاتجاه الأمامي للجسم بشكل افتراضي هو المحور السالب Z-وبالتالي كي نحرك الجسم للأمام حسب اتجاهه الخاص -وليس حسب اتجاه المشهد- نكتب الكود التالي: position += -transform.basis.z * speed * delta تلميح: يمتلك جودو قيم معرّفة افتراضيًا لبعض الاتجاهات الشائعة، على سبيل المثال يمثل الاختصار Vector3.FORWARD الاتجاه الأمامي في الفضاء ثلاثي الأبعاد: Vector3.FORWARD == Vector3(0, 0, -1) الخاتمة تعلمنا في هذا المقال كيف يمكن لمطور الألعاب أن يتعامل مع التحويلات الهندسية عمليًا داخل محرك ألعاب جودو سواء في المحرك ثنائي البعد 2D أو ثلاثي البعد 3D ويستفيد منها في التحكم بحركة واتجاه العناصر داخل اللعبة من خلال الخصائص المدمجة في المحرك وبعيدًا عن التعقيدات الرياضية مثل الزوايا الدوال المثلثية. ترجمة -وبتصرف- لمقال Transforms اقرأ أيضًا المقال السابق: مفاهيم رياضية أساسية في تطوير اﻷلعاب إعداد منطقة اللعب للعبة ثلاثية الأبعاد باستخدام جودو إنشاء شخصيات ثلاثية الأبعاد في جودو Godot تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
  4. تعتمد الأعمال الرقمية حاليًا على الخدمات السحابية لتسهيل التفاعل مع العملاء، ويتطلب الأمر تجميع وتخزين ومعالجة كميات هائلة من البيانات قبل تقديمها إلى المستخدم النهائي. وهنا يأتي دور تطبيقات الويب السحابية. فعندما نتكلم عن الخدمات السحابية نتذكر مباشرة النماذج التالية: البرمجيات كخدمات سحابية Software as a services واختصارًا SaaS منصات العمل كخدمات سحابية Platform as a Service واختصارًا PaaS البُنى التحتية كخدمات سحابية Infrastructure as Service واختصارًا IaaS سنناقش في مقالنا نماذج الأعمال الثلاث السابقة بالتفصيل لتكوين فكرة واضحة عن فائدة الخدمات السحابية لأعمالنا. لكننا سنلقي نظرة أولًا على مفهوم الحوسبة السحابية قبل أن نخوض في خدماتها. ما هي الحوسبة السحابية الحوسبة السحابية هي طريقة حديثة في الوصول إلى البيانات والمعلومات عبر شبكة الإنترنت بدلًا من الأقراص الصلبة، وهي وسيلة سريعة وآمنة وأكثر فعالية من أنظمة التخزين التقليدية. وقد ازداد استخدام الحوسبة السحابية حاليًا وفي مختلف القطاعات لكونها توفر حلًا ناجحًا للأعمال النامية أو التي أسست حديثًا نظرًا لحرية التوسع عند الحاجة. عند استخدام الخدمات السحابية لن نعتمد على عتاد أجهزتنا المحلية، إذ يمكننا الوصول إلى بياناتنا افتراضيًا ومن أي مكان، وطالما أنها متاحة على الشبكة، سنتمكن من الوصول إليها في أي وقت، فلن نضطر إلى استثمار الكثير على العتاد الصلب عند إطلاق أو توسيع أعمالنا بفضل الحوسبة السحابية، وكل ما علينا حجز مساحات إضافية عندما نحتاج لتوسيع العمل. أنواع الخدمات السحابية فيما يلي مقارنة سريعة بين البرمجيات والمنصات والبنى التحتية كخدمات: IaaS PaaS SaaS طبيعة الخدمة تقدم أساسًا لإنشاء البنى التحتية للخدمات السحابية وتؤمن نموذج الدفع وفقًا للاستخدام أطراف خارجية تؤمن أدوات أو تطبيقات للمستخدمين عبر الإنترنت من خلال خدمات البنية التحتية الخاصة بهم تؤمن وصولًا إلى تطبيقات الويب عبر نموذج الدفع وفقًا للاستخدام الإيجابيات مقبولة التكلفة ومرنة وقابلة للاسترجاع عند حدوث المشاكل. وكذلك سهلة الوصول ويمكن الاعتماد عليه مدروسة التكاليف وانتاجية متزايدة. متجاوبة ورشيقة. سهلة الوصول وقابلة للتوسع بسهولة. قابلة للتوسع وسهل الوصول ومقبولة التكلفة. كما أنها سهلة الترقية والنشر السلبيات صعوبة التحكم بها وتعاني بعض المشاكل الأمنية تعاني مشاكل في التوافق وتغييرات في موزعي الخدمة أمان غير كاف للبيانات وتحكم أقل مزودو الخدمة خدمات أمازون ويب AWS و محرك حوسبة جوجل GCE وديجتال أوشن DigitalOcean AWS ElasticBeanstalk و Apache و OpenShift و Heroku Google Workspace و Salesforce و Cisco و WebEx و Dropbox البُنى التحتية كخدمات سحابية IaaS تؤمن خدمة البنية التحتية السحابية وحدات البناء الأساسية للبنية التحتية لأي سحابة وتقدم موارد حاسوبية مثل المعالجة والآلات الافتراضية والشبكات وأكثر. وتسهل IaaS دعم الأعمال الصغيرة والمنظمات التي تستهدف حلولًا سحابية غير مكلفة وتعمل وفق نموذج الدفع وفقًا للاستخدام pay as you go وبالتالي سيدفع المستخدم تكلفة الخدمات التي يحتاجها فقط دون أية تكاليف إضافية، وهي متاحة لتوزيع الخدمات والموارد بشكل عام أو خاص أو هجين. تلغي هذه الخدمة السحابية التكاليف الإضافية الناتجة عن إدارة واستخدام العتاد الصلب وتوكل أمرها لمزود الخدمة. ويكون المستخدم النهائي مسؤولًا فقط عن إدارة الموارد مثل البيانات والتطبيقات، بينما ينظم المزوّد العمليات الافتراضية وإدارة الشبكة وتخزين البيانات. تساعد IaaS في توفير الوقت والتكلفة لأن مزود الخدمة هو من يهتم بإدارة العتاد الصلب. وطالما أن استخدام تلك الموارد هو فقط عند الحاجة، لن يكون هنالك وجود لمصادر مهملة، وستدفع فقط على ما تستخدمه فعليًا. من الأمثلة عليها نجد: خدمة أمازون ويب AWS، ومحرك حوسبة جوجل GCE، ومايكروسوفت آزور، وديجتال أوشن DigitalOcean. متى نستخدم نموذج IaaS يصلح نموذج IaaS لكل رائد أعمال أو خبير يحتاج لخدمة سحابية تعتمد نموذج الدفع وفقًا للاستخدام سيكون. ويمكن أيضًا الاستفادة من الخدمة إن كنا نحاول توسيع عملنا لكننا نراقب التكاليف بحذر، أو كان لدينا كميات كبيرة من البيانات التي تحتاج لمعالجتها وتخزينها. كما تعد الخدمة كذلك ملائمة للأفراد القلقين من حدوث كوارث أو مشكلات أو فقدان بيانات في البنية التحتية ضمن منازلهم، وهكذا لن ينشغلوا بأمور حماية بياناتهم فهي ليست على صفحة ويب بل داخل مركز بيانات، كما تقدم IaaS الموارد الشبكية الضرورية لتشغيل التطبيقات والخدمات في البيئة السحابية. الإيجابيات غير مكلفة مرنة إمكانية استعادة البيانات عند حدوث كوارث سهل الوصول موثوقة ويمكن الاعتماد عليها السلبيات صعوبة التحكم مشاكل في أمان البيانات منصات العمل كخدمات سحابية PaaS نموذج PaaS هو نموذج خدمات سحابية توفر فيه أطراف خارجية أدوات عبر الإنترنت للمطورين، معتمدة على بنية تحتية خاصة بها لتطوير التطبيقات. حيث يمكن للمطورين من خلال PaaS إنشاء تطبيقات قابلة للتوسع دون الحاجة لإعداد وإدارة قواعد البيانات والخوادم والشبكات والبنى التحتية لتخزين البيانات. ويستفيد المطورون الجدد من PaaS في تسهيل تطوير التطبيقات، وسيتمكن أي مطور من استخدام متصفحه فقط لتطوير التطبيق، كما تتحمل PaaS مسؤولية تحديث البنية التحتية الخاصة بنا وبالتالي لن نقلق بشأن صيانة تطبيقاتنا. كما يستفيد مطورو الأعمال من PaaS لكونها تؤمن بيئة عمل جماعية للمطورين الذين يعملون على المشروع ذاته، وتقدم أسلوبًا سريعًا في إنشاء التطبيقات نظرًا لسهولة توسيعها ومرونتها. من الأمثلة عليها نجد: AWS Elastic Beanstalk و Apache Stratos و Force.com و OpenShift و Heroku متى نستخدم نموذج PaaS إن كنا بحاجة إلى تطوير أعمالنا وتشغيل تطبيقات ويب دون تكلف الكثير على إعداد البرمجيات والعتاد الصلب، يمكن أن نفكر في استخدام PaaS. إذ تركز هذه الخدمة أساسًا على حماية بياناتنا وهو أمر حيوي جدًا في حال اخترنا الخدمة السحابية لتخزين البيانات. وعلينا أيضًا التفكير بخدمة PaaS إن أردنا من فريق المطورين التركيز على بناء التطبيقات بدلًا من الإنشغال بإصدار التحديثات الأمنية، وهذا ما سيخفف تكاليف الحمولات الزائدة ويوفر الوقت والجهد. الإيجابيات تكلفة مناسبة إنتاجية متزايدة رشيقة ومتجاوبة سهلة التوسع سهلة الوصول السلبيات مشاكل في التوافقية تغيّر مقدمي الخدمات البرمجيات كخدمات سحابية SaaS توفر لنا البرمجيات كخدمة SaaS إمكانية الوصول إلى تطبيقات الويب عبر الإنترنت، ولا حاجة معها إلى تنزيل أية أدوات أو برمجيات، وقد تكون مجانية أو تعمل وفق مبدأ الدفع وفقًا للاستخدام. ويمكن للمستخدمين الوصول إلى التطبيقات عبر أي جهاز بصرف النظر عن البنية التحتية لمقدم الخدمة أو صيانة التطبيقات أو أي شيء آخر، فهي أمور يديرها ويحميها مزوّد الخدمة السحابية. تفضل كثير من الأعمال استخدام خدمة SaaS نظرًا لانتشارها الواسع وعدم الحاجة إلى تكاليف خاصة أو تنزيل وتثبيت تلك البرمجيات. مع ذلك، تعتمد هذه الخدمة كليًا على موزعين خارجيين وليس للمستخدم القدرة على التحكم بالخدمة أو تغييرها. من الأمثلة عنها نجد: Google Workspace و Salesforce و Cisco WebEx و Dropbox. متى نستخدم نموذج SaaS إن أردنا الابتعاد عن تثبيت برامجنا محليًا فهذه الخدمة هي الحل، إذ تلغي الحاجة إلى الميزانيات الكبيرة وضغط العمل. تقدم لنا هذه الخدمة التطبيقات التي نحتاجها مستضافة من قبل أطراف خارجية وموزعة وفق معماريات خاصة بتلك الأطراف مما يجعلها قابلة للوصول من خلال الإنترنت. يمكن للأعمال الصغيرة الاستفادة من SaaS في حال لم نمتلك الميزانية الكافية أو كادر العمل لبناء تطبيقات خاصة بها. ويستخدم العديد من متخصصي تقانة المعلومات والمنظمات تطبيقات SaaS، وبإمكان مستخدمي B2B و B2C الاستفادة من تطبيقات SaaS على خلاف الخدمات السحابية الأخرى الإيجابيات قابلة للتوسع وسهلة الوصول غير مكلفة سهلة التحديث سهلة التوزيع السلبيات لا تقدم حماية كافية للبيانات تحكم أقل الاختلافات بين الخدمات السحابية SaaS و PaaS و Iaas عندما نقارن بين هذه الخدمات السحابية من ناحية المرونة تبرز خدمة IaaS. إذ تعتمد المرونة تمامًا على موزع الخدمة الذي نختاره، وكذلك الأمر من ناحية الأمان. وتُدفع تكاليف هذه الخدمة عادة بالساعة وفقًا للاستخدام وبالتالي قد ترتفع تكاليفها نظرًا لأسلوب الدفع الدقيق المرتبط بها. من ناحية أخرى، تعالج PaaS مشكلة البرمجة المتقدمة عالية المستوى بتسهيل وتبسيط العمليات، مما يجعل عملية تطوير التطبيقات أقل كلفة وزمنًا. وبالنسبة إلى التكلفة، فستزداد مع توسيع التطبيق ونموه. وبمجرد أن نلتزم مع موزع محدد فسنكون مقيدين ببيئة العمل والواجهة التي نختارها. أخيرًا، لخدمة SaaS سقف سعر فهي أرخص من كلتا الخدمتين السابقتين، وهي نعمة حقيقية للأشخاص والأعمال الصغيرة. لكن في المقابل سيكون تحكمنا في الخدمة محدودًا أو غير متاح، فمزود الخدمة هو من يدير معظم تفاصيلها. يمثل المخطط التالي الحجم السوقي للخدمات السحابية بين عامي 2018 و2024، وشعبية كل خدمة من الخدمات السابقة: المصدر بالنسبة لحرية التحكم بالخدمة سنجد أن نموذج IaaS في المقدمة، فهو يسمح لنا بإدارة التطبيقات والبيانات والبرمجيات الوسيطة ونظام التشغيل. بينما يسمح لنا نموذج PaaS بإدارة البيانات والتطبيقات فقط، ويدير مقدم الخدمة معظم أو كل النواحي في SaaS. اختيار الخدمة السحابية المناسبة علينا التفكير أولًا بحجم تبادل البيانات في موقعك أو حركة المرور إليه ونستغل قدرات المعالجة والتخزين التي تلائم حركة المرور تلك بأفضل ما يمكن، فقد نواجه مشكلات نحن بغنى عنها إن اخترنا خدمة سحابية غير ملائمة. وقد ينهار موقعنا إن لم تؤمن الخدمة التي اخترناها قدرات معالجة مناسبة وقد نضطر إلى دفع مبالغ إضافية على البنية التحتية السحابية حتى لو كانت حركة المرور إلى موقعنا منخفضة. وعلينا إضافة إلى ذلك أخذ عوامل مهمة أخرى بعين الاعتبار عند اختيار مزود الخدمة السحابية مثل أوقات توقف الخدمة downtime والترحيل migration لنقل التطبيقات إلى مكان آخر. خدمات حوسبة سحابية أخرى إلى جانب الخدمات الثلاث التي تحدثنا عنها في الفقرات السابقة نجد خدمات أخرى مثل: الخدمة السحابية DBaas تُعد قاعدة البيانات كخدمة سحابية DBaaS خدمة سحابية مدارة تستضيف قواعد البيانات وتسمح بالوصول إلى خدماتها دون إدارة أية برمجيات أخرى. وكغيرها من الخدمات لن نحتاج فيها إلى شراء أو إعداد عتادنا الصلب أو التعامل مع أية برمجيات لتثبيت قواعد بيانات. إذ تهتم معمارية هذه الخدمة مع الطرف المزود لها بكل شيء من النسخ الاحتياطي إلى التحديثات لضمان التوفر الدائم للخدمة ومعايير الأمان القوية. الإيجابيات سهولة العمل والتكيف مع التغييرات. غير مكلفة موثوقة لن نحتاج لبناء منظومة قواعد بيانات أو توظيف مطوري قواعد بيانات أوقات توفرها ممتازة السلبيات تحكم محدود مشاكل في خصوصية البيانات الخدمة السحابية Daas تُعد البيانات كخدمة Daas نهجًا مشابهًا لخدمة SaaS، إذ تؤمن خدمات تخزين ومعالجة وتكامل البيانات سحابيًا إلى مستخدميها عبر الإنترنت، ولا تتطلب تثبيت أو إدارة أية برمجيات. وتساعد Daas المستخدمين في الحد من تضخم البيانات data sprawl والحاجة إلى مجمعات تخزين data silos وتحسّن التعاون بين فرق العمل عبر مشاركة البيانات بينهم. الإيجابيات بيئة مقادة بالبيانات صيانة مؤتمتة تحسين نوعية البيانات السلبيات مشكلات في الخصوصية تعقيد البيانات الخدمة السحابية Faas تُعد الدوال كخدمة Functions as a Service واختصارًا FaaS خدمة سحابية ذات معمارية مبنية على الأحداث وخفية الخوادم serverless. تعمل هذه الخدمة على مبدأ كتابة دوال وتنفيذها كاستجابة لحدث ما، وتعتمد نموذج الدفع وفقًا للاستخدام ولن تكون هناك تكاليف إضافية. الإيجابيات ندفع فقط مقابل ما نستخدم زيادة في إنتاجية المطور توسع تلقائي السلبيات دعم محدود للعديد من التقنيات تحكم أقل بالمنظومة الخلاصة لا بد من الاستثمار في الخدمات السحابية إن أردنا مواكبة المعايير التي تتغير باستمرار. ليس لأنها تقدم خدمة أفضل للمستخدم فقط، بل لأنها تساعد أعمالنا على النمو أيضًا. حيث تخفف الخدمات السحابية من سلبيات ومحدودية البنى التقليدية لتقانة المعلومات، وسيعتمد اختيارنا للخدمة المناسبة على طبيعة العمل والطريقة التي نريدها في تشغيل التطبيقات السحابية. ترجمة -وبتصرف- لمقال: IaaS vs. PaaS vs. Saas how are the differents لصاحبه Sarim Javaid اقرأ أيضًا مفهوم السحابة Cloud مقدمة إلى الخوادم السحابية نظرة عامة على الحوسبة السحابية مقدمة إلى الاستضافة السحابية Cloud Hosting
  5. يحتاج أي مطور ألعاب لمعرفة بعض المفاهيم الرياضية الأساسية ليتحكم في انتقال الشخصيات من حالة لأخرى بانسيابية ويتحكم في اتجاهاتها وتمكينها من معرفة ماذا يوجد أمامها وخلفها، وسنناقش في مقال اليوم مفاهيم تستخدم بكثرة في تطوير اﻷلعاب، مثل الاستيفاء الخطي linear interpolation -أو lerp اختصارًا- والجداء الداخلي أو النقطي أو السلمي Dot Product، والجداء الخارجي أو الشعاعي Cross Product. قد تبدو هذه المصطلحات مقعدة وغامضة لمن يسمعها لأول مرة، لكن لا داعي للقلق فبعد قراءة هذا المقال ومعرفة تطبيقاتها العملية في برمجة اﻷلعاب ستغدو سهلة وبسيطة. الاستيفاء العددي تُعطى الصيغة اﻷساسية للاستيفاء الخطي رياضيًا كالتالي: func lerp(a, b, t): return (1 - t) * a + t * b يمثل كل من a و b قيمتين، بينما تمثل t مقدار الاستيفاء بينهما أي النسبة التي تحدد إلى أي درجة ننتقل من a إلى b. وتتراوح قيم t نمطيًا بين 0 وعندها سيعيد الاستيفاء قيمة a، وبين القيمة 1 وعندها سيعيد الاستيفاء قيمة b. يعطي تابع الاستيفاء قيمة ما بين a و b كما في المثال التالي: x = lerp(0, 1, 0.75) # x is 0.75 x = lerp(0, 100, 0.5) # x is 50 x = lerp(10, 75, 0.3) # x is 29.5 x = lerp(30, 2, 0.75) # x is 9 يُدعى هذا الاستيفاء بالاستيفاء الخطي لأنه يعدّ المسافة بين نقطتي الاستيفاء خطًا مستقيمًا. يمكننا تحريك أي خاصية لعقدة ما باستخدام الدالة ()lerp، فلو قسمنا الفترة الزمنية للحركة إلى فترات محددة سنحصل على قيمة بين الصفر والواحد يمكننا استخدامها لتغيير الخاصية المطلوبة بنعومة وسلاسة خلال مدة التنفيذ. كمثال على ذلك، يضاعف السكريبت التالي حجم الشخصية خمس مرات أثناء اختفائها باستخدام modulate.a وتستغرق الحركة ثانيتين: extends Sprite2D var time = 0 var duration = 2 # المدة الزمنية للتأثير func _process(delta): if time < duration: time += delta modulate.a = lerp(1, 0, time / duration) scale = Vector2.ONE * lerp(1, 5, time / duration) الاستيفاء الشعاعي من الممكن الاستيفاء أيضًا بين شعاعين وهذا يعني إيجاد شعاع جديد يقع بينهما بناءً على مقدار معين t تمامًا مثل الاستيفاء بين رقمين، لكن هنا نتعامل مع اتجاهات أو مواقع في الفضاء، إذ توفر كلا العقدتين Vector2 و Vector3 التابع ()linear_interpolate لتنفيذ اﻷمر. فلكي نجد على سبيل المثال شعاعًا يقع في منتصف المسافة بين الشعاع الأمامي واليساري لعقدة من نوع Spatial، نستخدم الاستيفاء الخطي بين هذين الاتجاهين كما في الكود التالي: var forward = -transform.basis.z var left = transform.basis.x var forward_left = forward.linear_interpolate(left, 0.5) كما يحرك المثال التالي الشخصية نحو موقع النقر بالفأرة، وتتحرك العقدة نحو هذا الموقع لكنها لا تقف فجأة حيث تقل سرعة الاقتراب كلما اقترب الكائن أكثر من الهدف: extends Sprite2D var target func _input(event): if event is InputEventMouseButton and event.pressed: target = event.position func _process(delta): if target: position = position.linear_interpolate(target, 0.1) الجداء الشعاعي الداخلي والخارجي يمكن تنفيذ عمليتي جداء على اﻷشعة هما الجداء الداخلي السلمي أو النقطي dot product والذي تكون نتيجته عدد، والجداء الخارجي أو الشعاعي والذي تكون نتيجته شعاعًا. الجداء الداخلي هو عملية حسابية على شعاعين تكون نتيجته عدد حقيقي، وتمثل عادة على أنها مسقط شعاع A على حامل الشعاع اﻵخر B: تُعطى الصيغة الرياضية للجداء الداخلي بالعلاقة: حيث: θ : هي الزاوية بين الشعاعين ||A||: طويلة الشعاع اﻷول ||B||:طويلة الشعاع الثاني ولهذه العلاقة فائدة خاصة عند تسوية الشعاع أي عند جعل طويلته واحد، إذ تصبح العلاقة بالشكل التالي: تشير هذه العلاقة إلى الارتباط المباشر بين الجداء الداخلي والزاوية بين الشعاعين، وطالما أن cos(0)=1 و cos(180)=-1 ستدل قيمة الجداء السلمي على اتجاه الشعاعين بالنسبة لبعضهما، فهما في الزاوية 0 منطبقان وفي الزاوية 180 في اتجاهين مختلفين: وسنرى في فقرة قادمة كيف نستفيد من هذا الجداء عمليًا. الجداء الخارجي ينتح عن الجداء الخارجي لشعاعين شعاع ثالث عمودي على كلا الشعاعين، أي عمود على المستوي الذي يضمهمها، وتتعلق طويلة الشعاع الناتج بطويلتي الشعاعين اﻷصليين والزاوية بينهما. تعطى طويلة الشعاع الناتج عن الجداء الخارجي بالعلاقة: A x B = ||A||.||B||.sin(θ) //هي الزاوية بين الشعاعين θ وإن كانت طويلة كل من الشعاعين هي الواحد ستكون نتيجة الحساب أبسط، إذ تكون طويلة الشعاع الناتج قيمة بين 1- و 1. ملاحظة: طالما أن ناتج الجداء الخارجي بين شعاعين يعطي شعاعًا عموديًا على كلا الشعاعين، فهو عادة ما يُستخدم في المشاهد ثلاثية الأبعاد، حيث أن الشعاع الناتج يكون في اتجاه عمودي على مستوى الفضاء الذي توجد فيه الأشعة الأصلية. من ناحية أخرى، في أطر العمل ثنائية البعد ومن ضمنها جودو، لا يمكن تمثيل الشعاع العمودي داخل نفس المستوى، وبالتالي عند استخدام التابع Vectro2.cross في جودو، فإنه لا يُرجع شعاعًا جديدًا، بل عددًا يمثل طول الشعاع العمودي على الشعاعين في اتجاه الفضاء الثالث أو المحور z وتكون قيمته بين 1- و 1 وتعكس مدى التعامد بين الشعاعين. تطبيقات عملية لنلق نظرة على الصورة المتحركة التالية التي تمثل نتيجة جداء خارجي وداخلي لشعاعين ()Vector2.dot و ()Vector2.cross وكيف تتغير كل نتيجة مع تغير الزاوية بين الشعاعين: توحي هذه الصورة بتطبيقين شائعين لهذين التابعين، فإن كان الشعاع اﻷحمر هو الاتجاه اﻷمامي للكائن وكان اﻷخضر اتجاهًا نحو كائن آخر فسيساعد الجداء الداخلي في معرفة إن كان الكائن الثاني أمامنا -أي عندما تكون قيمة الجداء أكبر من الصفر- أو خلفنا - أي عندما تكون قيمة الجداء أصغر من الصفر. كما يساعد الجداء الخارجي في معرفة إن كان الكائن إلى اليسار -عندما تكون قيمة الجداء أكبر من الصفر- أو إلى اليمين -عندما تكون قيمة الجداء أصغر من الصفر-. الخاتمة تعرفنا في هذا المقال على مفاهيم رياضية أساسية مستخدمة بكثرة في تطوير الألعاب، مثل الاستيفاء الخطي والجداء الداخلي والجداء الخارجي، وتعلمنا كيفية استخدامها للتحكم بحركة شخصيات اللعبة، وتحديد اتجاهاتها، وتحسين تفاعل الكائنات داخل المشهد. من الضروري لأي مطور ألعاب تعلم هذه المفاهيم فهي بمثابة حجر الأساس في تطوير الألعاب وجعل حركة الشخصيات واقعية وسلسة. ترجمة -وبتصرف- للمقالين: Interpolation و Vectors:Using Dot product and Cross product اقرأ أيضًا المقال السابق: سحب وإفلات جسم صلب RigidBody2D في جودو استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه تطبيق الجداء النقطي Dot Product على الأشعة في التصاميم 3D تعرف على محرر محرك اﻷلعاب جودو Godot
  6. نشرح في هذا المقال طريقة تفاعل شخصية اللاعب في جودو مع الأجسام الصلبة الموجودة في المشهد. ويمكن أن نطبق الطريقة التي سنشرحها على الفضائين الثنائي والثلاثي البعد على حد سواء. التفاعل مع الأجسام الصلبة إذا جربنا استخدام العقدة CharacterBody2D في محرك الألعاب جودو فسنجد أن العقدة CharacterBody2D التي تتحرك افتراضيًا من خلال تنفيذ أحد التابعين ()move_and_slide أو ()move_and_collide تصطدم بالأجسام الصلبة الفيزيائية من حولها لكنها لا تتمكن من دفع أي جسم تتصادم معه مثل صندوق أو عدو، فلن تتفاعل عقدة الجسم الصلب مع عقدة اللاعب إطلاقًا وستسلك سلوك العقدة StaticBody2D. فلو افترضنا أن شخصية اللاعب تمشي وتصطدم بصندوق، عندها ستتوقف أو تغير اتجاهها لكن الصندوق لن يتحرك، قد يكون هذا السلوك في بعض الحالات هو المطلوب فعلًا، لكن إن أردنا أن ندفع هذا الصندوق، سنحتاج لعمل بعض التغييرات. سنستخدم في هذا المثال شخصية ثنائية البعد يمكن تحميلها مع الموارد الأخرى للعبة من هذا المستودع، كما سنستخدم أكثر توابع الحركة شيوعًا لتحريك اللاعب وهو التابع ()move_and_slide الذي يستطيع تحريك الكائنات من نوع CharacterBody2D بشكل آمن ومتوافق مع نظام الفيزياء، حيث يتكفّل بإدارة الاصطدامات والانزلاق على الأسطح والتفاعل مع الجاذبية بشكل تلقائي دون الحاجة لكتابة منطق فيزيائي معقد. سنجد أن أمامنا خيارين لتحديد أسلوب التفاعل مع الأجسام الصلبة فبإمكاننا دفع هذه الأجسام متجاهلين الفيزياء. وهذا الأمر مماثل لخيار العطالة اللانهائية infinite inertia المستخدم في الإصدار 3 من جودو. كما أن بإمكاننا دفع الأجسام بناء على الكتلة المُتخيّلة للشخصية وسرعتها، وسيعطينا ذلك نتيجة واقعية. إذ ستدفع الشخصية الأجسام الثقيلة قليلًا والأجسام الخفيفة كثيرًا، وسنجرب تاليًا كلا الخيارين. العطالة اللانهائية لهذا الخيار إيجابياته وسلبياته. أما الإيجابية الأكبر فهي أنه لا يحتاج إلى شيفرة إضافية. وكل ما علينا هو ضبط طبقات أو أقنعة التصادم collision layers/masks بالشكل الصحيح لكل الأجسام. لتوضيح الأمر، عرّفنا في مثالنا ثلاث طبقات فيزيائية ووضعنا الجسم الصلب ضمن الطبقة رقم 3 وأبقينا على القناع كما هو لتقنيع كل الطبقات: وضعنا بعد ذلك اللاعب في الطبقة الثانية وهي الطبقة player وضبطنا القناع ليتجاهل العناصر الأخرى. عند تشغيل اللعبة، نلاحظ كيف يمكن للاعب دفع الصناديق، ولا يهم في هذه الحالة وزن الصناديق، إذ ستدفع جميعها بنفس المقدار. السلبية التي سنلاحظها في هذا الخيار هو تجاهل فيزياء حركة الصناديق. فبإمكان الصناديق تسلق الجدار، لكن لا يمكن للاعب القفز فوقها. لا بأس بهذا الأمر في بعض الألعاب، لكن إن أردنا منع الجسم من التسلق، علينا الاعتماد على الخيار الثاني. تطبيق الاندفاعات لمنح الجسم المتصادم دفعة لا بد من تطبيق اندفاع impulse، وهو دفعة آنية وكأننا نضرب كرة. وننوه لأن الاندفاع معاكس لمفهوم القوة وهي دفع الجسم باستمرار. # عطالة اللاعب var push_force = 80.0 func _physics_process(delta): # move_and_slide() بعد استدعاء for i in get_slide_collision_count(): var c = get_slide_collision(i) if c.get_collider() is RigidBody2D: c.get_collider().apply_central_impulse(-c.get_normal() * push_force) يتجه ناظم التصادم collision normal خارج الجسم الصلب، لهذا عكسناه ليتجه بعكس اتجاه الشخصية ويُطبّق العامل push_force. وهكذا ستدفع الشخصية الصناديق مجددًا لكنها لن تجبر الصناديق عندما تدفعها نحو الجدار على تسلقه. الخاتمة تعرفنا في هذا المقال على كيفية تفاعل شخصية اللاعب مع الأجسام الصلبة، واستعرضنا طريقتين أساسيتين لتحقيق دفع الشخصية لهذه الأجسام إما بتجاهل الفيزياء باستخدام العطالة اللانهائية، أو بتطبيق الاندفاعات للحصول على سلوك واقعي. يعتمد اختيار الطريقة الأنسب على طبيعة اللعبة والتجربة التي نرغب في تقديمها للاعب لتمنحه سلوكًا منطقيًا. بإمكانك تحميل المشروع كاملًا من مستودعه على جيتهب أو مباشرة من هنا character_vs_rigid.zip. ترجمة -وبتصرف- للمقال: Character to rigid Body interaction اقرأ أيضًا المقال السابق: سحب وإفلات جسم صلب RigidBody2D في جودو استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه استخدام الإشارات Signals في جودو Godot إنشاء خرائط مصغرة MiniMap للألعاب في جودو
  7. الخرائط المصغرة هي عبارة عن واجهات رسومية صغيرة تظهر في زاوية شاشة اللعب، تعرض تمثيلًا مصغرًا لخريطة اللعبة الكاملة أو المنطقة المحيطة باللاعب. لتساعد على تحديد موقعنا داخل اللعبة وترينا أماكن الأعداء أو الأشياء المهمة من حولنا، فهي تعمل كنظام رادار لكشف الأعداء والأهداف المخفية وتوفر صورة عامة عن بيئة اللعب ككل. سنبني في هذا المقال خريطة مصغرة تعرض موقع اﻷشياء الواقعة خارج مجال رؤية اللاعب بشكل نقاط أو أيقونات صغيرة وسنحدث مواقع هذه النقاط كلما تحرك اللاعب. إعداد المشروع سننشئ لعبة تخطيطها من اﻷعلى إلى اﻷسفل وسنستخدم ميزة Autotile في محرك الألعاب جودو فهي تُسهّل كثيراً عملية رسم الخرائط باستخدام عناصر رقعة Tiles متداخلة وتمكننا من رسم الجدران أو الأرضيات بحرية، حيث يختار المحرك تلقائيًا الرقعة المناسبة من مجموعة الرقع حتى تتطابق الحواف والزوايا مع الرقع المجاورة، ويمكن تنزيل الصور المطلوبة لتطبيق المقال من مجلد assets من هذا الرابط أو منminimap_assets.zip سيبدو المشهد الرئيسي للعبة كالتالي: نستخدم العقدة CanvasLayer لتجميع عناصر واجهة المستخدم بما في ذلك الخريطة المصغرة التي سننشؤها في هذا المقال، ونستخدم العقدة TileMap لرسم الخريطة باستخدام الرقع tiles، بينما نستخدم العقدة Player لتمثيل شخصية اللاعب. تخطيط واجهة المستخدم الخطوة اﻷولى في مثالنا هي بناء تخطيط للخريطة المصغرة. وللتعامل مع أية عناصر واجهة مستخدم تضمها اللعبة، لا بد من إعادة تحجيمها بشكل سلس ودمجها جيدًا في تخطيط يتلائم مع الحاوية. لهذا سنضيف أولًا العقدة MarginContainer التي تساعدنا على وضع حواشي داخلية padding للعناصر داخل الحاوية، ونضبط الخاصية Constants في القسم Theme Overrides على 5. يضم عنصر التحكم هذا بقية العقد ويضمن أن العناصر داخل الحاوية ستبقى بعيدًا عن حواف الحاوية نفسها بشكل متناسق. سنسمي هذه العقدة MiniMap ثم نحفظ المشهد. نضيف تاليًا العقدة NinePatchRect وهي مشابهة للعقدة TextureRect لكنها تتعامل مع تغيير اﻷبعاد بطريقة مختلفة، إذ لا تمدد الزوايا أو الحواف مما يحافظ على مظهر الصورة بشكل أفضل عند تغيير الأبعاد. نفلت الصورة panel_woodDetail_blank.png من مجلد assets في لعبتنا في الخاصية Texture، وهي صورة أبعادها 128x128 بكسل، لكن إن غيرنا أبعاد العقدة MarginContainer ستصبح الصورة ممددة وسيئة المظهر. لكن مع استخدام NinePatchRect سنضمن أن اﻹطار سيحافظ على أبعاده فعند التمدد ستعمل العقدة على تقسيم الصورة إلى تسعة أجزاء بحيث تبقى الزوايا ثابتة وغير متمددة وتتوزع الحواف بطريقة تمدد سلسة بحيث لا تتشوه الصورة ويتمدد الوسط بطريقة مرنة لتعبئة المساحة. بإمكاننا تعريف هذه الخاصيات رسوميًا في اللوحة TextureRegion ، لكن من اﻷسهل أحيانًا إدخال القيم مباشرة. نضبط الخاصيات اﻷربعة الموجودة في Patch Margin على 64 ونغيّر اسم العقدة إلى Frame. لنلاحظ اﻵن ما يحدث عند تغيير اﻷبعاد: علينا تاليًا ملء الجزء الداخلي من اﻹطار بنمط يمثل شبكة وذلك باستخدام الصورة pattern_blueprintPaper.png: نريد اﻵن أن تملأ الصورة ما داخل اﻹطار تلقائيًا أيًا كانت أبعاده، وطالما أن المنطقة التي تغطيها الشبكة هي المكان الذي ستظهر فيه نقاط علام الخريطة، لا ينبغي أن تمتد الشبكة إذًا خارج حدود اﻹطار. لهذا، نضيف عقدة جديدة MarginContainer كابن للعقدة MiniMap وشقيق للعقدة Frame ثم نضبط خاصيات Constants في القسم Theme Overrides على القيمة 20. نضيف بعد ذلك عقدة TectureRect كابن للعقدة السابقة ثم نضبط قيمة الخاصية Texure على نفس الصورة السابقة، والخاصية Strech Mode على Tile ونسمي العقدة أخيرًا Grid. لنجرّب تغيير أبعاد العقدة اﻷصلية لرؤية تأثير ما فعلناه حتى اللحظة: لنبق أبعاد الخريطة المصغرة حاليًا على 200x200 بكسل، وبإمكاننا التأكد من هذه اﻷبعاد من الخاصية Size في القسم Layout. ستبدو اﻵن شجرة المشهد كالتالي: نقاط علام الخريطة سنضيف للخريطة نقاط العلام Marker ترمز كل منها لأشياء معينة في اللعبة، نضيف أولًا عقدة من النوع Sprite2D كابن للعقدة Grid ونسميها PlayerMarker لتمثل اللاعب ونمنحها الصورة minimapIcon_arrowA.png، وننتبه إلى أن قيمة الخاصية Position في القسم Transform هي (0,0)مما يجعلها في الزاوية العليا اليسارية من العقدة Grid. إن كانت أبعاد الشبكة (150,150)سيكون مركزها عند (75,75)، لنضبط إذًا موقع PlayerMarker على تلك القيمة، ننوه أن هذه العملية ستكون آلية لاحقًا. نضيف اﻵن عقدتين جديدتين من نوع Sprite2D ونسميهما AlertMarker و MobMarker ونمنحهما الصورتين minimapIcon_jewelRed.png التي تمثل جوهرة حمراء و minimapIcon_exclamationYellow.png التي تمثل علامة تعجب صفراء كما يلي: تمثل العقدتان السابقتان نوعين جديدن من نقاط العلام في عالم اللعبة. ننقر على الزر 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 تبقى المشكلة إمكانية ظهور بعض نقاط العلام خارج الشبكة كما في الصورة التالية: وﻹصلاح اﻷمر، نحصر موقع نقطة العلامة بمربع الشبكة باستخدام الدالة clamp بعد حساب المتغير obj_pos وقبل تحديد موقع العلامة: obj_pos = obj_pos.clamp(Vector2.ZERO, grid.size) بإمكاننا أيضًا معالجة العلامات التي تقع خارج الشاشة وخارج مربع الشبكة. باختيار أحد الحلين التاليين وقبل استخدام ()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) إزالة الكائنات ستزدحم اللعبة وتتوقف إن قُتِل أي كائن 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 فيما يلي نتيجة الشيفرة: الخاتمة شرحنا في هذا المقال طريقة إضافة خريطة مصغرة لعالم اللعبة وحاولنا أن نجعلها مرنًا بما فيه الكفاية حتى نتمكن من تضمينها في أي لعبة نعمل عليها في جودو، ويمكن تحسين هذه الخريطة بإضافة الأمور التالية إليها: أنواع أكثر من نقاط العلام. إضافة وحدات أخرى للخريطة عند توليدها باستعمال الإشارات كما فعلنا تمامًا عند إزالة الوحدات الحصول على معلومات عند النقر على نقطة العلام استخدام صورة للخريطة الفعلية بدلًا من استخدام صورة الشبكة ترجمة -وبتصرف- للمقال: MinMap/Radar اقرأ أيضًا المقال السابق: إنشاء قائمة لاختيار مستوى اللعبة في جودو بناء شريط صحة ونصوص طافية في ألعاب جودو عرض عداد تنازلي وقائمة دائرية في جودو التعامل مع إجراءات دخل الفأرة في جودو
  8. نشرح في هذا المقال طريقة اختيار أجسام صلبة وسحبها وإفلاتها من مكان لآخر باستخدام الفأرة في محرك الألعاب جودو، فقد يكون العمل مع الأجسام الصلبة مربكًا في جودو نظرًا لتحكم محرّك الفيزياء بهذه الحركة، وأي تدخل من قبلنا في الأمر سيقود غالبًا إلى نتائج غير متوقعة. إن مفتاح الحل في هذه الحالة هو استخدام الخاصية mode للجسم، ويُطبق هذا الأمر في الفضاء ثنائي البعد وثلاثي البعد. إعداد الجسم الصلب لننشئ مشروع لعبة جديدة ثنائية الأبعاد في جودو ونعمل مع كائن يمثل الجسم الصلب بإضافة العقدتين Sprite2D و CollisionShape2D. بإمكاننا أيضًا إضافة العقدة PhysicsMaterial إن أردنا ضبط خاصيتي الارتداد Bounce والاحتكاك Friction. كما سنستخدم الخاصية freeze لإبعاد الجسم عن سيطرة محرّك الفيزياء عندما نسحبه. وطالما أننا نريد من الجسم الصلب أن يكون قابلًا لحركة، لا بد من ضبط قيمة الخاصية Freeze في القسم Mode على القيمة kinematic بدلًا من القيمة الافتراضية Static. نضع الجسم ضمن مجموعة تدعى pickable، ونستخدمها لضم نسخ متعددة من الكائنات التي يمكن التقاطها في المشهد الرئيسي. نضيف سكريبت برمجي للجسم الصلب ثم نصل الإشارة _input_event الخاصة به كما يلي. extends RigidBody2D signal clicked var held = false func _on_input_event(viewport, event, shape_idx): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if event.pressed: print("clicked") clicked.emit(self) نبث الإشارة عندما نلتقط حدث النقر على الفأرة التي تتضمن مرجعًا إلى الجسم. ونظرًا لوجود عدة أجسام، سنترك أمر إدارة حالة الأجسام إن كانت قابلة للسحب أو أنها في حالة held أي إيقاف للمشهد الرئيسي main scene. فإن كان الجسم يُسحب، نغيّر موقعه باتباع حركة الفأرة. func _physics_process(delta): if held: global_transform.origin = get_global_mouse_position() فيما يلي الدالتان اللتان سنستدعيهما عند التقاط الجسم وإفلاته. ولنتذكر أن تغيير قيمة الخاصية freeze إلى true سيزيل الجسم من عمليات محرّك الفيزياء. سنلاحظ أن بقية الأجسام لا تزال قادرة على الاصطدام بهذا الجسم، فإن لم نرغب بهذا السلوك، نستطيع تعطيل الخاصية collision_layer مع أو بدون الخاصية collision_mask، ولا ننسى إعادة تمكينهما عند الإفلات. func pickup(): if held: return freeze = true held = true func drop(impulse=Vector2.ZERO): if held: freeze = false apply_central_impulse(impulse) held = false بعد إعادة قيمة الخاصية freeze إلى false في الدالة drop سيعود الجسم إلى سيطرة محرّك الفيزياء أي أنه سيتأثر بالجاذبية والتصادمات بشكل طبيعي. لهذا، يمكننا في هذه الحالة تمرير قيمة اندفاع impulse اختيارية، بإمكاننا إضافة إمكانية رمي الجسم عند تحريره بدل أن يسقط. دورة تطوير الألعاب ابدأ رحلتك في برمجة وتطوير الألعاب ثنائية وثلاثية الأبعاد وصمم ألعاب تفاعلية ممتعة ومليئة بالتحديات اشترك الآن المشهد الرئيسي ننشئ مشهدًا رئيسيًا مع بعض العقبات أو نستخدم العقدة TileMap وننشر عدة نسخ من الأجسام التي يمكن التقاطها. ونبدأ سكريبت لمشهد الرئيسي بوصل الإشارة clicked في أي جسم قابل للالتقاط موجود في المشهد extends Node2D var held_object = null func _ready(): for node in get_tree().get_nodes_in_group("pickable"): node.clicked.connect(_on_pickable_clicked) نعرّف بعد ذلك الدالة التي نربط بها الإشارات، وتضبط هذه الدالة قيمة held_object حتى نعرف بوجود جسم يُسحب حاليًا، ونستدعي التابع ()pickup الخاص بالجسم. func _on_pickable_clicked(object): if !held_object: object.pickup() held_object = object عندما نحرر الفأرة خلال السحب بإمكاننا تنفيذ الخطوات المعاكسة. func _unhandled_input(event): if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if held_object and !event.pressed: held_object.drop(Input.get_last_mouse_velocity()) held_object = null لنلاحظ كيف استخدمنا هنا التابع ()get_last_mouse_velocity لتمرير الدفع إلى الكائن. علينا أن ننتبه إلى ذلك جيدّا. مع ذلك، سنجد أننا نطلق الجسم الصلب بسرعة كبيرة وخاصة إذا كانت قيمة الخاصية mass للجسم قليلة. لهذا، من الأجدى أن نحدد القيمة العظمى للكتلة على قيمة مناسبة باستخدام التابع ()clamp. يمكن تجربة عدة قيم حتى نحدد ما يناسب لعبتنا. الخاتمة في هذا المقال، تعلّمنا كيفية التعامل مع الأجسام الصلبة في محرك الألعاب جودو بطريقة تتيح لنا اختيارها وسحبها وتحريكها باستخدام الفأرة. ورأينا كيف أن التحكم اليدوي بأجسام يتحكم بها محرّك الفيزياء قد يؤدي لسلوك غير متوقع، لذلك حللنا المشكلة بتغيير نمط الجسم إلى جسم مرتبط بالحركة Kinematic، وعطلنا خصائص الفيزياء مؤقتًا باستخدام freeze أثناء السحب. كما أنشأنا نظامًا بسيطًا يتيح التقاط الأجسام القابلة للسحب باستخدام الإشارات والمجموعات، وطبّقنا حركة واقعية للجسم عند الإفلات وأضفنا آلية رمي الجسم من خلال التقاط سرعة حركة الفأرة وتمريرها كدفعة. من خلال هذه الخطوات، أصبح بإمكاننا بناء لعبة تفاعلية تعتمد على سحب ورمي الأجسام بطريقة سلسة وطبيعية. يمكن تحميل المثال كاملًا من هذا الرابط لفهمه بصورة جيدة وتجربة التعديل عليه، أو تحميله مباشرة من هنا rigidbody_drag_drop-master.zip ترجمة -وبتصرف- للمقال: RigidBody2D Drag and Drop اقرأ أيضًا المقال السابق: استخدام RigidBody2D في جودو للتوجه نحو هدف والتحرك نحوه إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو تعرف على واجهة محرك الألعاب جودو إنشاء الوحدات البنائية وشخصيات الخصوم في Unity3D
  9. نشرح في هذا المقال طريقة بناء قائمة تتضمن مجموعة خيارات تظهر في بداية اللعبة وتساعد اللاعب على تحديد مستوى اللعبة. سيكون تصميم القائمة على شكل شبكة قابلة للتمرير وضمنها صناديق تمثل كل مستوى، بحيث يمكن التنقل بينها واختيار المرحلة التي يرغب اللاعب في اللعب فيها كما توضح الصورة التالية. مراحل بناء القائمة سنبني القائمة بتصميم شبكة قابلة للتمرير مكونة من صناديق لتحديد المستوى بحيث يمكن للاعب أن يختار فيما بينها. سنبدأ أولًا ببناء صندوق المستوى LevelBox بشكل مستقل. بناء صندوق المستوى LevelBox ستكون هيكلية العقدة اللازمة لبناء هذا الصندوق على النحو التالي: LevelBox: PanelContainer Label MarginContainer TextureRect استخدمنا العناصر التالية لتشكيل الصندوق: حاوية PanelContainer لتنظيم وعرض العناصر داخلها عقدة تسمية نصية Label لعرض رقم المستوى حاوية MarginContainer لإضافة هوامش حول المحتوى عقدة LevelBox من نوع TextureRect لعرض صورة قفل عندما يكون المستوى مغلق، ورقم عندما يكون مفتوح نتأكد من ضبط الخاصية Layout في القسم Custom Minimum Size للعقدة LevelBox على القيمة (110,110) ويمكن اختيار أي حجم آخر مناسب لتخطيط القائمة. نضيف سكريبت إلى العقدة التي تمثل صندوق المستوى من أجل وصل اﻹشارة gui_input التي تُطلق عندما تتلقى العقدة حدث إدخال مثل النقر بالفأرة أو الضغط على لوحة المفاتيح. حيث يمكن أن يكون المستوى مغلقًا أو مفتوحًا، وعند النقر على الصندوق سنرسل إرسال إشارة لاختيار المستوى. @tool extends PanelContainer signal level_selected # إشارة تُطلق عند تحديد المستوى @export var locked = true: # يحدد إذا كان المستوى مغلقًا set = set_locked @export var level_num = 1: # رقم المستوى set = set_level @onready var lock = $MarginContainer/Lock # صورة القفل @onready var label = $Label # رقم المستوى # دالة لضبط حالة القفل وإظهارأو إخفاء العناصر func set_locked(value): locked = value if not is_inside_tree(): # ننتظر حتى يكتمل تحميل العنصر داخل المشهد await ready lock.visible = value # نعرض صورة القفل إذا كان المستوى مغلقًا label.visible = not value # نظهر النص إذا كان المستوى غير مغلق # دالة لضبط رقم المستوى وتحديث النص المعروض func set_level(value): level_num = value if not is_inside_tree(): # ننتظر تحميل العنصر في المشهد await ready label.text = str(level_num) # تحديث النص برقم المستوى # دالة لمعالجة مدخلات المستخدم func _on_gui_input(event): if locked: # إذا كان المستوى مغلقًا، نتجاهل النقر return if event is InputEventMouseButton and event.pressed: # التحقق من النقر بزر الفأرة level_selected.emit(level_num) # إطلاق الإشارة مع رقم المستوى print("Clicked level ", level_num) # طباعة رسالة لاختبار النقر نستخدم في شيفرتنا التوجيه tool@ حتى نتمكن من تغيير قيم الخاصيات عبر نافذة الفاحص inspector ونرى التأثير مباشرة دون الحاجة لتشغيل المشهد. لنجرب اﻵن النقر على الخاصية Locked ونتحقق من رؤية صورة القفل تظهر وتختفي. وطالما أن اللعبة لا تضم مستويات فعلية لتحميلها، ستساعدنا الدالة ()print على اختبار التقاط حدث النقر. بناء الشبكة بعد إنهاء مشهد صندوق المستوى، سنضيف مشهدًا جديدًا يضم العقدة GridContainer ثم نضع ضمنها العدد الذي نريده من نسخ العقدة LevelBox، ونتأكد من ضبط قيمة الخاصية Columns على القيمة المناسبة. ننتقل للقسم Theme Overrides ثم Seperation ونضبط قيمتي الخاصيتين V Seperation و H Seperation على 10، ونحفظ المشهد باسم LevelGrid. سنستخدم في القائمة عدة نسخ عن عقدة الشبكة لعرض العدد المطلوب من المستويات. شاشة القائمة بإمكاننا اﻵن تجميع القائمة النهائية. توضح الصورة التالية التخطيط اﻷولي الذي سننفذه: ننشئ المشهد انطلاقًا من العقد التالية: LevelMenu: MarginContainer VBoxContainer Title: Label HBoxContainer BackButton: TextureButton ClipControl: Control NextButton: TextureButton نضبط خواص العقد كالتالي: العقدة LevelMenu: نضبط Theme Overrides ثم Constants ثم Margins على القيمة 20 العقدة VBoxContainer:نضبط Theme Overrides ثم Constants ثم Margins على القيمة 50 العقدة Title: ننسقها بالطريقة التي نريدها نضبط العقدتين BackButton و NextButton كالتالي: Ignore Texture Size: On لضبط حجم الزر وفقًا لحجمه داخل واجهة المستخدم Stretch Mode: Keep Centered للحفاظ على محاذاة المحتوى داخل الأزرار Layout/Container: On ليعمل الزر كحاوية Sizing/Horizontal/Expand: On لتوسيع الأزرار أفقيًا داخل المساحة المتاحة نضبط العقدة ClipControl كما يلي: Layout/Clip Contents:On لاقتصاص أي محتوى يتجاوز حجم الإطار Layout>Custom Minimum Size:(710, 350) لتحديد الحجم الأدنى للعقدة بعد ذلك نضع الشبكة ضمن العقدة ClipControl، بما أننا تمكين الخاصية Clip Content سيقتص محتوى العقدة إن كانت أكبر من عنصر التحكم. وسنتمكن اﻵن من بناء شبكة من صناديق المستويات قابلة للتمرير، لهذا نضيف عقدة من النوع HBoxContainer تُدعى GridBox إلى ClipControl إضافة إلى ثلاث نسخ من العقدة LevrlGrid أو أكثر إن أردنا. ونتأكد من ضبط الخاصية Theme Overrides ثم Constants ثم Separation على القيمة 0. ينبغي أن يبدو تخطيط المشهد اﻵن كما في الشكل التالي تقريبًا، مع العلم أننا عطلنا الخاصية Clip content لنعرض ما يحدث بشكل أوضح: الشبكات الثلاثة موجودة ضمن Clip Content لكن لا يمكن للمتحكم ClipControl عرض سوى شبكة واحدة كل مرة. لهذا ولكي ننتقل إلى الشبكتين الباقيتين عن طريق التمرير، لا بد من إزاحة GridBox مقدار 710 بكسل يمينًا أو يسارًا 110 (width of each LevelBox) * 6 (grid columns) + 10 (grid spacing) * 5 == 710 قد يتبادر للذهن سؤال عن عدم استخدام العقدة ScrollCointainer. هنا، بالتأكيد يمكن ذلك، لكننا لا نريد التنقل بين الشبكات باستمرار، ولا نريد أيضًا رؤية شريط تمرير. نضيف السكربت التالي إلى العقدة LevelMenu لوصل إشارتي pressed الخاصتين بكل زر: extends MarginContainer var num_grids = 1 var current_grid = 1 var grid_width = 710 @onready var gridbox = $VBoxContainer/HBoxContainer/ClipControl/GridBox func _ready(): # ترقيم جميع صناديق المستويات وإلغاء قفلها # يمكن استبداله بما يتناسب مع نظام المستويات في اللعبة # يمكن أيضًا ربط إشارات "level_selected" هنا num_grids = gridbox.get_child_count() for grid in gridbox.get_children(): for box in grid.get_children(): var num = box.get_position_in_parent() + 1 + 18 * grid.get_position_in_parent() box.level_num = num box.locked = false func _on_BackButton_pressed(): if current_grid > 1: current_grid -= 1 gridbox.rect_position.x += grid_width func _on_NextButton_pressed(): if current_grid < num_grids: current_grid += 1 gridbox.rect_position.x -= grid_width ننقر على زر التالي Next والسابق Back عند تشغيل المشهد ونتأكد من تمرير العناصر كما هو متوقع. من المفترض أن يطبع النقر على صندوق المستوى شيئًا في شاشة الطرفية. الخاتمة بهذا نكون قد انتهينا من مقالنا الذي يشرح طريقة بناء شبكة قابلة للتمرير يمينًا ويسارًا تضم صناديق لتحديد مرحلة أو مستوى اللعبة، ويمكن تحميل المثال بالكامل لرؤية كل شيء يعمل كما يجب بما في ذلك اﻹجراءات وبعض عمليات توليد اﻷطر البينية tweens لتجميل عملية التمرير. ترجمة -وبتصرف- للمقال: Level Select Menu اقرأ أيضًا المقال السابق: بناء شريط صحة ونصوص طافية في ألعاب جودو الاستماع لمدخلات اللاعب في جودو Godot مدخل إلى محرك الألعاب جودو Godot تعرف على أشهر لغات برمجة الألعاب أشهر محركات الألعاب Game Engines
  10. نشرح في هذا المقال طريقة تدوير جسم صلب بسلاسة في محرك الألعاب جودو من خلال استخدام العقدة RigidBody2D، ونوضح طريقة توجيه هذا الجسم نحو هدف معين ليطبق عليه، أو يتحرك نحوه. الإطباق على هدف قد يكون استخدام العقدة RigidBody2D مربكًا لأن من يتحكم بها هو محرك الفيزياء الموجود ضمن محرك الألعاب جودو Godot physics engine. فإن أردنا تحريك الجسم ضمن اللعبة التي نطورها بجودو، فعلينا تطبيق قوى معينة عليه بدلًا من تحريكه مباشرة. وننصح قبل بدء العمل على تطبيق خطوات هذا المقال بإلقاء نظرة على توثيق الواجهة البرمجية RigidBody2D. لا بد من تطبيق عزم دوراني torque حتى نتمكن من تدوير الجسم، وبمجرد أن يبدأ الجسم بالدوران، علينا تقليل عزم الدوران تدريجيًا مع اقتراب hg[sl من الوجهة النهائية بحيث لا يفرط الجسم في الدوران ويتوقف في الاتجاه الصحيح بدقة. تُعدُّ هذه حالة مثالية لتطبيق الجداء النقطي المعروف أيضًا باسم الداخلي dot product فهو مناسب جدًا لتحديد الاتجاه المطلوب للوصول للوجهة النهائية. عند حساب الجداء النقطي بين اتجاه الجسم الحالي واتجاه الوجهة، سنحصل على معلومات تساعدنا على ضبط الدوران حيث أن إشارة ناتج الجداء تخبرنا إن كان الهدف موجودًا على جهة اليمين أو اليسار على النحو التالي: ناتج الجداء النقطي موجب هذا يعني أن الزاوية بين اتجاه الجسم الحالي واتجاه الوجهة أقل من 90 درجة، أي أن الجسم يحتاج إلى تعديل بسيط، أو لا يحتاج تعديل للوصول للوجهة لأنه قريب من الاتجاه الصحيح بالفعل. ناتج الجداء النقطي صفر هذا يعني أن الزاوية بين الاتجاهين هي 90 درجة بالضبط، أي أن الوجهة تقع عموديًا على الاتجاه الحالي للجسم، وهنا يجب على الجسم تحديد ما إذا كان عليه الدوران إلى اليمين أو اليسار للوصول للهدف. ناتج الجداء النقطي سالب هذا يشير إلى أن الزاوية بين اتجاه الجسم واتجاه الوجهة أكبر من 90 درجة، مما يعني أن الجسم يحتاج إلى التدوير الكامل نحو الاتجاه المعاكس. وبهذا، تكون الوجهة خلف الجسم. كما تدلنا قيمة ناتج الجداء النقطي عن مقدار ابتعادنا عن اتجاه الهدف الذي نريد مطابقته فكلما كانت قيمة الجداء أكبر، كلما كان الجسم أقرب للوجهة وذلك بالنسبة للمتجهات موحدة الطول. لنلقِ نظرة على الكود التالي المكتوب بلغة GDScript لتدوير جسم ثنائي الأبعاد 2D باتجاه هدف معين بتطبيق عزم دوراني torque: extends RigidBody2D var angular_force = 50000 var target = position + Vector2.RIGHT func _physics_process(delta): var dir = transform.y.dot(position.direction_to(target)) constant_torque = dir * angular_force قد يتساءل البعض عن سبب استخدام transform.y في حساب الجداء النقطي بدل استخدام transform.x ، مع أن transform.x هو من يمثل شعاع توجيه الجسم نحو الأمام! السبب هو أن استخدام transform.x سيجعل قيمة الجداء السلمي في أعلى قيمة له -قريبة من الواحد- عند التطابق مع الهدف ونحن نريد أن يكون العزم معدومًا -أس مساويًا للصفر- في هذه اللحظة لأن الجسم لن يحتاج إلى دوران إضافي، لهذا استخدمنا transform.y حيث يكون العزم أكبر عندما لا يكون اتجاه الجسم مطابقًا لاتجاه الهدف، مما يساعد على تصحيح الدوران. بمعنى آخر عندما يكون الجداء النقطي في أعلى قيمة له، فهذا يعني أن الجسم متوجه نحو الهدف تمامًا وفي هذه الحالة، نرغب في أن يكون العزم المطبق صفر، لأن الجسم لا يحتاج إلى مزيد من الدوران لذا حققنا هذا الهدف باستخدام transform.y بدلاً من transform.x ليزيد العزم عندما يكون الجسم بعيدًا عن الهدف، ويقل تدريجيًا مع الاقتراب منه. تفادي مشكلات تدوير الجسم الصلب RigidBody2D يمكننا تفادي تعقيدات تحريك الجسم الصلب بالامتناع عن تدوير الجسم الصلب نفسه RigidBody2D، ونعمل بدلًا من ذلك على تعديل الخاصية rotation لشخصية الابن child sprite كي يتوجه نحو الهدف. وبالإمكان حينها استخدام التابعين ()lerp و ()Tween لجعل الحركة الدورانية سلسلة قدر المستطاع. ويعد هذا الحل مناسبًا في كثير من الحالات، حيث يمكن أن يكون للجسم الأساسي اتجاهه الخاص بينما تتجه الشخصية أو العنصر المرتبط به في اتجاه مختلف نحو الهدف. إذ يمكن للجسم الأساسي أن يتحرك في اتجاه معين، بينما يمكن للشخصية الملحقة به مثل الرأس أو السلاح أن تدور بشكل مستقل دون الحاجة لأن يكون توجيهها مطابقًا تمامًا لتوجيه الجسم الأساسي. تحرك جسم نحو الهدف قد نواجه مشكلة عند محاولة تحريك RigidBody2D نحو هدف معين في محرك جودو، لأن محرك الفيزياء هو الذي يتحكم في هذه العقدة، ولا يمكننا ببساطة تغيير موقعها يدويًا. بدلاً من ذلك، يجب علينا تطبيق قوة لتحريكها في الاتجاه المطلوب، كما ذكرنا سابقًا. فكيف نجعل الجسم يتحرك بسلاسة نحو الهدف؟ نحتاج لتطبيق قوة في اتجاه الهدف لتحريك الجسم، ثم تخفيف القوة تدريجيًا مع الاقتراب من الهدف حتى لا يتجاوز الجسم موقعه أو يتوقف بشكل مفاجئ. يفيدنا استخدام التابع ()Vector2.distance_to لحل هذه المشكلة بشكل ممتاز، فهو يحسب المسافة بين الجسم والهدف، ويساعدنا على استخدام هذه المسافة لتحديد مقدار القوة المطلوب تطبيقها فعندما يكون الجسم بعيدًا، نطبق قوة أكبر، وكلما اقترب من الهدف، نخفض القوة تدريجيًا لضمان توقف سلس، وبهذه الطريقة، نحقق حركة طبيعية دون اهتزازات أو توقف مفاجئ. لنلقِ نظرة على الكود التالي الذي يجعل الجسم الصلب RigidBody2D يتحرك نحو هدف معين بطريقة سلسة: # تحريك الجسم بسلاسة نحو الهدف extends RigidBody2D var linear_force = 5 var target = position func _physics_process(delta): var dist = position.distance_to(target) constant_force = dir * linear_force * dist يحسب الكود أعلاه القوة الخطية المطلوبة لتحريك الجسم نحو الهدف بناء على المسافة بين الجسم المتحرك والهدف dist. تزداد القوة عندما يكون الجسم بعيدًا عن الهدف وتقل تدريجيًا مع الاقتراب من الهدف، مما يجعل الحركة تبدو طبيعية وسلسة. أهمية الخاصية linear_damp إن حاولنا استخدام إعدادات العقدة RigidBody2D الافتراضية في جودو، فقد نلاحظ أحيانًا تجاوز الجسم الصلب للهدف. يعود السبب إلى الخاصية linear_damp التي تأخذ القيمة 1 افتراضيًا. تمثل هذه القيمة معامل الاحتكاك friction، وتتحكم بكيفية تخميد أو توقف الجسم الصلب عن الحركة عند توقف القوى المحرّكة. فهي تعمل مثل الاحتكاك، مما يؤدي إلى تقليل سرعة الجسم تدريجيًا عندما لا تكون هناك قوة تدفعه. عندما تكون قيمة هذه الخاصية 0، فهذا يعني أن الجسم لن يتباطأ تلقائيًا، بل سيستمر في التحرك بسرعة ثابتة ما لم تؤثر عليه قوة أخرى، مثل الجاذبية أو الاحتكاك. أما عندما تكون قيمتها 1 أو 2، فإن تأثير التخميد يزداد، مما يؤدي إلى تقليل سرعة الجسم تدريجيًا حتى يتوقف عندما لا يكون هناك قوة تحركه. بإمكاننا تعديل هذه القيمة لنضمن توقف الجسم عند بلوغ الهدف، ويمكن أن نجرب أيضًا كيف تتفاعل هذه القيمة مع قيمة الخاصية linear_force حتى نحصل على الحركة التي نريدها تمامًا، حيث تمثل linear_force قوة خطية عندما تُطبّق على الجسم تحرِّكه في اتجاه معين للأمام أو الخلف أو في أي اتجاه يشير إليه القوة مما يؤدي إلى تسارع الجسم. ويمكننا تغيير قيمة linear_force لتسريع أو إبطاء حركة الجسم أثناء تشغيل اللعبة. الخاتمة تعلمنا في مقال اليوم كيفية تحريك وتدوير جسم صلب RigidBody2D في جودو بشكل سلس نحو هدف معين باستخدام القوى والعزم الدوراني، كما شرحنا كيفية التحكم في التباطؤ باستخدام الخاصية linear_damp لضمان حركة طبيعية للجسم. ترجمة -وبتصرف- للمقالين: RigidBody2D: Look at Target و RigidBody2D: Move to Target اقرأ أيضًا تعرف على واجهة محرك الألعاب جودو استخدام الإشارات Signals في جودو Godot تعرف على أشهر محركات الألعاب Game Engines التعامل مع إجراءات دخل الفأرة في جودو
  11. نتعرف في هذا المقال على طريقة بناء شريط صحة Health Bar يضم قلوبًا أو غيرها من اﻷيقونات، كما سنتعرف على طريقة عرض نسبة تضرر الشخصية في لعبة على شكل نص يطفو فوق الشخصية. ثلاث طرق لبناء شريط صحة يضم قلوبًا من الطرق الشائعة في إظهار صحة اللاعب عرض سلسلة من اﻷيقونات -غالبًا بشكل قلوب- يختفي بعضها عندما يتعرض اللاعب إلى ضرر. وسنناقش ثلاث طرق لعرض الأيقونات أطلقنا عليها تسميات بسيطة simple وفارغة empty وجزئية partial. تعرض الصورة السابقة ثلاث حالات ممكنة لعرض شريط الصحة: الطريقة البسيطة: تعرض القلوب ممتلئة بالكامل الطريقة الفارغة: تعرض قلوب فارغة وأخرى ممتلئة الطريقة الجزئية: تعرض القلوب نصف ممتلئة إعداد شريط اﻷيقونات نستخدم في هذا المثال صور قلوب أبعادها 53x45 حصلنا عليها من موقع Kenney.nl: Platformer Art Deluxe. ومن المفترض أن يكون وضع الشريط ضمن شاشة عرض معلومات المستخدم HUD أو واجهة المستخدم UI سهلًا، لذا من المنطقي أن نبني هذا الشريط ضمن مشهد مستقل. سنبدأ بعقدة من النوع Node2D كي نُبقى اﻷمور على نفس السوية، ونضبط قيمة الخاصية Sepration ضمن Constants في القسم Theme Overrides من الفاحص على القيمة 5. نضيف بعد ذلك عقدة ابن من النوع TextureRect ثم نسحب أيقونة القلب إلى الخاصية Texture ونضبط قيمة Strech Mode على Keep. نعيد تسمية العقدة لتكون 1 ثم باستخدام مفتاحي Ctrl+D ننسخ هذه العقدة بعدد القلوب التي نريد عرضها في الشريط 5 مثلًا. ستبدو لوحة العقد في محرك جودو كالتالي: إضافة السكريبت يغطي السكريبت التالي حالات الشريط الثلاث التي ذكرناها، حيث سنحمّل في البداية الخامات وهي هنا اﻷيقونات التي نحتاجها ونعرف اﻷشرطة الثلاث، وتجدر الملاحظة بأن الكود سيغطي جميع حالات الشريط الثلاثة، وقد نحتاج لاستخدام حالة واحدة فقط في اللعبة، عندها نزيل الكود المتعلق بالحالات الأخرى كما يلي: extends HBoxContainer enum modes {SIMPLE, EMPTY, PARTIAL} var heart_full = preload("res://assets/hud_heartFull.png") var heart_empty = preload("res://assets/hud_heartEmpty.png") var heart_half = preload("res://assets/hud_heartHalf.png") @export var mode : modes func update_health(value): match mode: MODES.simple: update_simple(value) MODES.empty: update_empty(value) MODES.partial: update_partial(value) يؤدي استدعاء الدالة ()update_health العائدة إلى الشريط عرض القيمة الممرة إليه وفقًا للنمط المختار. ملاحظة: لن نضيف آليات تحقق من حدود القيمة المدخلة كالتأكد مثلًا من أن الصحة بين 0 و 100، فهناك طرق كثيرة لعرض الصحة في اﻷلعاب لذا سنترك الأمر لكم. نتنقل في الدالة ()update_simple بين أشرطة الأيقونات ونضبط ظهور كل عقدة TextureRect: func update_simple(value): for i in get_child_count(): get_child(i).visible = value > i واﻷمر مشابه في الدالة ()update_empty ما عدا أننا نغير اﻷيقونة إلى اﻷيقونة الفارغة بدلًا من إخفائها: func update_empty(value): for i in get_child_count(): if value > i: get_child(i).texture = heart_full else: get_child(i).texture = heart_empty أما في الحالة اﻷخيرة، فلدينا أيقونة ثالثة وضعف القيم الممكنة فمن خلال إنقاص القيمة بمقدار 1 مثلًا يعطي نصف قلب وإنقاص 1 مرة أخرى تعطي قلبًا فارغًا: func update_partial(value): for i in get_child_count(): if value > i * 2 + 1: get_child(i).texture = heart_full elif value > i * 2: get_child(i).texture = heart_half else: get_child(i).texture = heart_empty توضح الصورة أدناه مثالًا عن عمل كل شريط: إنشاء نصوص طافية فوق الشخصية هناك طرق عدة لتحقيق النصوص الطافية floating text، منها استخدام خط كتابة نقطية bitmap font وبناء صورة لكل عدد انطلاقًا من اﻷرقام المكونة له، ومن ثم استخدام العقدة Sprite2D لعرض وتحريك النص الناتج. لكن ما سنفعله في مقالنا هو استخدام العقدة Label واسمها FCT وبهذا سنمتلك مرونة في تغيير الخط إضافة إلى سهولة عرض اﻷعداد كنصوص أو عرض نصوص أخرى مثل "أخفق miss". نضيف المورد الذي نريده في الخاصية Label Settings ونختار خطًا مناسبًا وقياسًا مناسبًا له، وقد استخدمنا في المثال الخط Xolonium.ttf والقياس 28 مع إطار خارجي أسود بعرض 4 بكسل. نضيف اﻵن السكريبت التالي إلى العقدة Label: extends Label func show_value(value, travel, duration, spread, crit=false): نستدعي عند توليد النصوص الطافية الدالة ()show_value التي تضبط قيم المعاملات التالية: value وهو العدد أو النص الذي نريد توليده travel وهو عقدة شعاع Vector2 التي تمثل اتجاه حركة النص أو العدد duration تحدد كم سيبقى النص على قيد الحياة spread يحدد أن الحركة ستكون عشوائية عبر هذا القوس crit يشير لأن الضرر كبير في حال كانت قيمته true وهذا ما تفعله الدالة ()show_value: text = value var movement = travel.rotated(rand_range(-spread/2, spread/2)) rect_pivot_offset = rect_size / 2 تضبط الدالة قيمة النص أو العدد ومن ثم تجعل حركته عشوائية وفقًا لقيمة الانتشار spread مابين 90+ و90- مثلًا. وقد نغير أبعاد النصوص المتحركة، لهذا ضبطنا قيمة الخاصية rect_pivot_offset لتمثل مركز عنصر التحكم وبالتالي يكون تغيير اﻷبعاد منسوبًا إلى المركز. $Tween.interpolate_property(self, "rect_position", rect_position, rect_position + movement, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) $Tween.interpolate_property(self, "modulate:a", 1.0, 0.0, duration, Tween.TRANS_LINEAR, Tween.EASE_IN_OUT) نجري بعد ذلك استيفاء interpolation على قيمتي الخاصية بين لحظتين هما rect_position لتحريك العدد الطافي و modulate.a ﻹخفاء هذا النص بشكل تدريجي وسلس: if crit: modulate = Color(1, 0, 0) $Tween.interpolate_property(self, "rect_scale", rect_scale*2, rect_scale, 0.4, Tween.TRANS_BACK, Tween.EASE_IN) إن كانت اﻹصابة بالغة، سنغير لون النص ونزيد حجمه لإظهار التأثير. وتجدر الملاحظة بأننا حددنا لون النص في هذه الحالة ليكون أحمر ومن اﻷفضل أن تعرف متحولًا لإسناد قيمة اللون الذي نريده: $Tween.start() yield($Tween, "tween_all_completed") queue_free() نبدأ بعد ذلك عملية بناء اﻹطارات البينية من خلال التعليمة Tween وننتظر حتى تنتهي ثم نزيل العنوان Label. إدراة النصوص الطافية FCTManager ننشئ اﻵن عقدة صغيرة تدير توليد النصوص وتحديد مكانها، وستُلحق بكيانات اللعبة التي نريد أن نضيف إليها تأثير النصوص الطافية. نسمي هذه العقدة من النوع Node2D بالاسم FCTManager ونضيف إليها السكريبت التالي: extends Node2D var FCT = preload("res://FCT.tscn") export var travel = Vector2(0, -80) export var duration = 2 export var spread = PI/2 func show_value(value, crit=false): var fct = FCT.instance() add_child(fct) fct.show_value(str(value), travel, duration, spread, crit) يمكن تعديل ما نريده من خصائص العقدة من خلال نافذة الفاحص Inspector، لكن الدالة ()show_value أيضًا تولد النص الطافي وتضبط خصائصه. وبإمكاننا إلحاق نسخة من هذه العقدة بأي وحدة من وحدات اللعبة نريدها أن تمتلك تأثير النصوص الطافية، ثم نضيف كود مشابه لما يلي ضمن التابع ()take_damage للوحدة: $FCTManager.show_value(dmg, crit) تجدر الإشارة لأنه في الحالة التي تضم فيها لعبتنا عددًا كبيرًا من الوحدات، فقد يؤثر هذا اﻷمر على اﻷداء جراء توليد وتحرير النصوص الطافية باستمرار لعدد كبير من الوحدات. في حالات كهذه، ينصح بتوليد عدد محدد تمامًا من النصوص الطافية من خلال FCTManager ثم نظهرها ونخفيها بدلًا من توليدها وتحريرها في نهاية الحركة. الخاتمة تعرفنا في هذا المقال على طريقة بناء شريط صحة يضم أيقونات تعرض حالة اللاعب مثل تناقص صحته أو نفاذ ذخيرته، كما تحدثنا عن أحد طرق لتوليد نصوص تطفو حول الشخصية وتختفي للدلالة على حالة معينة مثل مقدار اﻹصابة التي تلقتها. ترجمة -وبتصرف- للمقالين: HeartContainers: 3 ways و Floating Combat Text اقرأ أيضًا المقال السابق: عرض عداد تنازلي Countdown وقائمة دائرية Radial Menu في جودو استخدام الإشارات Signals في جودو Godot الاستماع لمدخلات اللاعب في جودو Godot تعرف على واجهة محرك الألعاب جودو
  12. تزايدت شعبية إطار العمل لارافيل Laravel بشكل كبير فهو اليوم واحد من أبرز أطر عمل لغة PHP، لا سيما في مجال تطوير تطبيقات الويب ومنصات التجارة الإلكترونية. سنشرح لكم في هذا المقال عدة نصائح وملاحظات لتحسين أداء التطبيقات المطورة بلارافيل. أهمية تحسين تطبيقات لارافيل أصبح إطار العمل لارافيل شائعًا جدًا في تطوير تطبيقات الأعمال التي تساعد في اتخاذ القرارات الإدارية بسرعة وفعالية. ولضمان أداء جيد لهذه التطبيقات، نحتاج لتحسينها والتأكد من تنفيذها للمهام بسرعة وسلاسة، لا سيما إذا كانت هذه التطبيقات تؤثر في اتخاذ قرارات مهمة. يوفر تحسين أداء تطبيقات لارافيل جملة من الفوائد تشمل زيادة فعالية التطبيق وتعزيز الفرصة لوصوله إلى جمهور أوسع، وتوفير الوقت وتقليل الهدر في الموارد، إلى جانب تحسين التعامل مع حركة البيانات وإدارة الطلبات المتزايدة بكفاءة مع زيادة حجم التطبيق. متطلبات العمل سنفترض في هذه المقال أن لدينا تطبيق لارافيل مثبت على خادم تتوفر فيه الأمور التالية: الإصدار Laravel 9.0 من إطار العمل لارافيل الإصدار PHP 8.0 من لغة PHP قاعدة البيانات MariaDB نصائح لتحسين أداء تطبيقات لارفيل فيما يلي نورد مجموعة من النصائح التي تساعد في تحسين أداء تطبيقات لارافيل. التخزين المؤقت للإعدادات يمكننا من خلال حفظ إعدادات التطبيق في ذاكرة التخزين المؤقت cache memory تسريع الوصول لهذه الإعدادات، بدلاً من تحميلها من الملفات في كل مرة نشغل فيها التطبيق وهذا الأمر يساهم في تحسين أداء التطبيق بشكل عام. لتنفيذ هذا الأمر في إطار العمل لارافيل نستخدم الأمر التالي: php artisan config:cache بمجرد تخزين الإعدادات في ذاكرة التخزين المؤقت، فإن أي تغييرات نجريها على هذه الإعدادات لن تؤثر على هذه النسخة في الذاكرة. فإذا أردنا تحديث الذاكرة المؤقتة لتخزن الإعدادات الجديدة، فعلينا تنفيذ الأمر السابق مرة أخرى. ولمسح التخزين المؤقت للإعدادات نستخدم الأمر التالي: php artisan config: clear لا ينبغي استخدام أمر تخزين الإعدادات مؤقتًا أثناء تطوير التطبيق، لأن الإعدادات قد تتغير بصورة مستمرة طيلة مرحلة التطوير، وبإمكاننا استخدام أداة OPcache لتحسين التطبيق، فهو يخزّن شيفرة PHP الذاكرة المؤقتة، مما يلغي الحاجة لإعادة تصريف شيفرة PHP في كل مرة نطلبها من الخادم، ويساهم في تسريع التطبيق. التخزين المؤقت للوجهات Routes يفيد التخزين المؤقت للوجهات routes في لارافيل في تحسين التطبيقات، لا سيما تلك التي تحتوي عدد كبير من الوجهات والإعدادات. فمن خلال تخزين المصفوفة الخاصة بالوجهات في الذاكرة المؤقتة، سنتمكن من تحميلها بسرعة أكبر. لتنفيذ هذه الميزة، نستخدم الأمر التالي: php artisan route: cache لا ننسى تنفيذ هذا الأمر كلما تغير ملفات الإعدادات أو الوجهات لمشروعنا، وإلا سيحمّل لارافيل القيم المحفوظة سابقًا. لمسح التخزين المؤقت للوجهات routes نستخدم الأمر التالي: php artisan route: clear إزالة الخدمات غير المستخدمة الغاية الأساسية لإطار لارافيل هي تسهيل عمل المطورين ولتحقيق هذه الغاية يحمّل إطار لارافيل عند تشغيله مجموعة متنوعة من الخدمات المدمجة والمحددة في الملف config/app.php. لكن قد لا يحتاج المطور للعديد من هذه الخدمات كخدمة التعامل مع قوالب العرض View Service وخدمة إدارة الجلسة Session Service، وسيفيدنا تعطيل الخدمات غير الضرورية في تحسين أداء التطبيق. تحسين تحميل الملفات يستدعي إطار لارافيل ملفات متعددة للتعامل مع الطلبات requests، وبالتالي قد يرتبط تطبيق لارافيل متوسط التعقيد بعدة ملفات، ومن النصائح البسيطة لتحسين الأداء التصريح عن كل الملفات التي يحتاجها التطبيق ضمن ملف واحد يُستدعى ويُحمّل لكل الطلبات. نستخدم الأمر التالي لتطبيق هذه النصيحة: php artisan optimize --force تحسين الملحن Composer الملحن Composer هو أداة مفيدة لإدارة الاعتماديات dependencies في تطبيقات PHP وكذلك في تطبيقات لارافيل. فعند تثبيت هذه الأداة، ستعمل على تحميل جميع اعتماديات بيئة التطوير الخاصة بإطار العمل، وبالرغم من أن هذه الاعتماديات مفيدة في تسريع عملية التطوير، لكن بمجرد اكتمال العمل على التطبيق وتحويله إلى بيئة الإنتاج، تصبح هذه الاعتماديات غير ضرورية وقد تؤثر سلبًا على أداء التطبيق وتبطئه. يمكننا تحسين الأداء في بيئة الإنتاج بتنفيذ الأمر التالي الذي يثبيت الاعتماديات الضرورية فقط، ويتجنب تحميل الحزم غير المطلوبة: composer install --prefer-dist --no-dev -o تحديد المكتبات المضمّنة في التطبيق من المميزات المفيدة في إطار لارافيل إمكانية تضمين العدد الذي نريده من المكتبات في التطبيق. لكن بالمقابل ستظهر الآثار السلبية لتلك الميزة في بطئ الأداء والتأثير سلبًا على تجربة المستخدمين. لذا، من المهم مراجعة جميع المكتبات التي نضمنّها في التطبيق، ونحذف المكتبات التي يمكننا الاستغناء عنها من الملف config/app.php لتسريع التطبيق. من المهم أيضًا مراجعة الملف composer.json. الذي يحتوي على قائمة بالاعتماديات أو المكتبات التي يعتمد عليها التطبيق والتأكد من أن لكل مكتبة استخدام فعلي في التطبيق وإلا من الأفضل إزالتها. استخدام مصرّف JIT يمكن لتطبيقات لارافيل الاستفادة بشكل كبير من تقنية المصرّف Just In Time أو JIT اختصارًا لتحسين سرعة المعالجة وتقليل وقت الاستجابة، خصوصًا في التطبيقات الكبيرة التي تحتاج لمعالجة معقدة. فعند تنفيذ شيفرة PHP، فهي تترجم أولاً إلى شيفرة البايت Bytecode ثم تنفذ، وهو ما يتطلب استخدامًا مكثفًا للموارد. ولتنفيذ ذلك، يحتاج تطبيق لارافيل لأدوات وسيطة مثل Zend Engine التي تعمل على تشغيل برامج فرعية Subroutines بلغة C تُنفّذ في كل مرة نشغّل فيها التطبيق. المشكلة أن هذه العمليات تستهلك وقتًا وموارد، لذا يكون من الأفضل تنفيذ عملية الترجمة مرة واحدة، وهذا ما تقوم به تقنية المصرّف JIT الذي يصرّف الشيفرة إلى شيفرة بلغة الآلة مباشرة عند الحاجة فقط، مما يساهم في تسريع الأداء بشكل كبير. ولتحقيق أفضل استفادة من هذه التقنية، يُفضّل استخدام المصرّف HHVM الذي طورته فيسبوك مع تطبيقات لارافيل. اختيار أسرع برمجيات لتخزين بيانات الذاكرة المؤقتة والجلسة من أفضل طرق تحسين أداء لارافيل تخزين بيانات الذاكرة المؤقتة وبيانات الجلسة ضمن ذواكر RAM. لهذا تُستخدم برمجيات لتخزين البيانات المؤقتة في الواجهة الخلفية مثل برنامج Memcached مما يساهم في تحسين أداء الإصدار لارافيل. كما يمكننا العثور على إعدادات مكتبة تخزين الجلسات أو برنامج إدارة الجلسات في الملف app/config/session.php بينما يمكننا تحديد برنامج إدارة الذاكرة المؤقتة من خلال الإعداد الموجود في الملف app/config/cache.php. التخزين المؤقت لنتائج الاستعلام يساعدنا التخزين المؤقت لنتائج الاستعلامات التي تتكرر كثيرًا في تطبيقاتنا على تحسين أداء لارافيل. ننصح باستخدام الدالة remember كالتالي: $posts = Cache::remember('index.posts', 30, function() { return Post::with('comments', 'tags', 'author', 'seo') ->whereHidden(0) ->get(); }); يخزن الكود السابق نتائج استعلام قاعدة البيانات كالمنشورات مع التعليقات، والوسوم، والمؤلف، وتحسين محركات البحث في الذاكرة المؤقتة. فإذا كانت البيانات موجودة في الذاكرة المؤقتة بالفعل، سنسترجعها بسرعة. وإذا لم تكن موجودة، سينفذ الاستعلام لاسترجاع البيانات من قاعدة البيانات ويخزنها في الذاكرة المؤقتة لمدة 30 دقيقة لتحسين الأداء. استخدام التوسع الأفقي للتطبيق ما نعنيه بالتوسيع الأفقي للتطبيق توزيع الحمل على عدة خوادم بدلاً من الاعتماد على خادم واحد فقط. هذه الطريقة مفيدة عند تزايد عدد الطلبات على الواجهة البرمجية لتطبيقنا، مما يساهم في تحسين الأداء والقدرة على التعامل مع عدد أكبر من المستخدمين على التزامن. يمكن التفكير في توسيع تطبيق لارافيل أفقيًا في حال ازدادت كثافة المرور على الواجهة البرمجية فهذا يساهم في توزيع الحمل على عدد من الخوادم ويسمح لتطبيقنا بالتعامل طلبات الاتصال المتزامنة بفعالية أكبر. يمكننا من خلال نشر نسخ إضافية من التطبيق على خوادم أخرى توزيع الحمل وتخفيف الضغط وضمان استجابة سريعة، وحتى إن تعطل أحد الخوادم سيستمر التطبيق في العمل مما يزيد ثقة العملاء بالتطبيق ويضمن توافره باستمرار. استخدام التحميل الشره Eager Loading للبيانات تقدم لارافيل الأداة Eloquent، وهي أداة ممتازة لربط العلاقات بالكائنات Object-Relational Mapping حيث تنشئ نماذج تجرّد جداول قاعدة البيانات وتساعد المطورين على استخدام بنى بسيطة للبيانات واستخدام Eloquent لتنفيذ كل العمليات الأساسية على قواعد البيانات بلغة PHP. عند استخدام التحميل الشره في لارافيل، ستسترجع كافة البيانات المرتبطة بالنموذج -مثل العلاقات بين الجداول- في نفس الاستعلام الأولي. بمعنى آخر، بدلاً من الانتظار لإجراء استعلامات إضافية عند الوصول إلى هذه البيانات المرتبطة، سينفذ تحميلها جميعًا مرة واحدة وتضمن في الاستجابة الأولى للتطبيق ما يساهم في تحسين الأداء ويقلل عدد الاستعلامات إلى قاعدة البيانات. لنقارن التحميل البطيء lazy loading مع التحميل الشره، إذ يبدو الاستعلام في حالة التحميل البطيء كالتالي: $books = App\Book::all(); foreach ($books as $book) { echo $book->author->name; } ويبدو كما يلي في حالة التحميل الشره: $books = App\Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; } هذا الكود أكثر كفاءة لأنه يستخدم التحميل الشره لتحميل العلاقة مع المؤلف مرة واحدة بدلاً من تحميل المؤلف في كل مرة وصول إليه، مما يقلل عدد الاستعلامات المرسلة إلى قاعدة البيانات. التصريف المسبق للأصول Assets Pre-compiling عند تطوير تطبيقات لارافيل، يعمل المطورون على توزيع التطبيق عبر عدة ملفات مثل ملفات config و routes، مما يجعل الشيفرة واضحة وسهلة الإدارة. لكن هذا الأسلوب يتطلب الكثير من العمل لجعل الشيفرة جاهزة للعمل. يقدم لارافيل بعض الأوامر البسيطة لمساعدة المطورين في تحسين الأداء وتحويل الشيفرة إلى نسخة إنتاجية حيث يمكن استخدامها لتجميع الأصول كالأنماط والسكريبتات والصور في ملفات واحدة وضغطها. تساهم هذه الأوامر في تسريع تحميل التطبيق وزيادة كفاءة الشيفرة في بيئة الإنتاج: php artisan optimize php artisan config:cache php artisan route:cache استخدام شبكة توزيع المحتوى CDN شبكة توزيع المحتوى CDN هي مجموعة من الخوادم الموزعة جغرافيًا في أماكن مختلفة حول العالم، والتي تعمل على تخزين وتوزيع المحتوى الثابت مثل ملفات JavaScript، CSS، الصور، ومقاطع الفيديو بكفاءة أكبر. يفيدنا استخدام شبكة CDN لتطبيقات لارافيل في تحسين أداء تحميل الأصول الثابتة للموقع من خلال تقديمها من أقرب خادم جغرافي للمستخدم بدلاً من تحميلها من الخادم الرئيسي للموقع. عندما يزور المستخدم الموقع، تعمل CDN على توجيه الطلب إلى أقرب خادم له، مما يقلل المسافة الجغرافية التي يجب على البيانات قطعها، وبالتالي يسرع من عملية تحميل الصفحة خاصة في الحالات التي يكون فيها المحتوى ثابتًا وغير قابل للتغيير بشكل متكرر، مثل الصور أو ملفات CSS. استخدام أداة Mix لتجميع الأصول تأتي الأداة Mix افتراضيًا مع كل تطبيقات لارافيل، تؤمن هذه الأداة واجهة برمجية فعالة لبناء حزمة Webpack لتطبيقات PHP باستخدام المعالجات الأولية pre processors لملفات CSS وملفات جافا سكريبت مما يساعد على تحسين إدارة أصول التطبيق وتحسين الأداء. على سبيل المثال، يمكننا دمج عدة ملفات CSS أو JavaScript في ملف واحد، مما يقلل عدد الطلبات المرسلة للخادم ويحسن من سرعة تحميل الصفحة. mix.styles([ 'public/css/vendor/normalize.css', 'public/css/styles.css' ], 'public/css/all.css'); تُنشئ الشيفرة السابقة ملف CSS باسم all.css يضم التنسيقات الموجودة في الملفين normalize.css و styles.css. وبهذا سنتمكن من استخدام الملف الجديد بسهولة ضمن شيفرة HTML بدلًا من تضمين الملفين السابقين. تصغير حجم الأصول عند تجميع الأصول في ملف واحد، قد ينتج عن ذلك ملف كبير جدًا. وعلى الرغم من أن هذا يحسن من عدد الطلبات، إلا أنه قد لا يحقق أفضل استفادة من فكرة التصريف أو التخزين المؤقت caching التي تحدثنا عنها سابقًا. لهذا، من الأفضل تقليل حجم الأصول بقدر المستطاع لتسريع تحميل الصفحة من خلال الأمر التالي: $ npm run production يُنفذ الأمر السابق جميع مهام Mix اللازمة لضبط الأصول بشكل يتناسب مع النسخة النهائية من التطبيق. وهو يتضمن تصغير ملفات CSS و JavaScript ودمجها لتقليل حجمها ما يؤدي إلى تسريع تحميلها وبالتالي تحسين أداء التطبيق. استخدام أحدث نسخ PHP يساهم تحميل أحدث إصدار من لغة PHP في تحسين أداء التطبيق بشكل كبير. لذا، من الضروري التأكد من تشغيل أحدث نسخة من PHP في تطبيقات لارافيل للاستفادة من التحسينات التي تقدمها النسخ الجديدة، سواء على صعيد الأداء أو الأمان، وتجنب المشكلات المرتبطة بالسرعة وضمان تجربة مستخدم أفضل. استخدام المنقح Debugger على الرغم من أن منقّح لارافيل Laravel Debugbar ليس تقنية مباشرة لتحسين أداء تطبيقات لارافيل، إلا أداة هامة لمراقبة وتحليل أداء التطبيقات. فهو يساعدنا في دمج شريط تنقيح PHP مع لارافيل ويظهره في المتصفح أثناء تشغيل التطبيق، مما يفيدنا في مراقبة عدة أمور في تطبيقنا مثل مدة استجابة التطبيق، ووقت تنفيذ الاستعلامات، وفحص الشيفرة البرمجية، لتحديد المشكلات التي قد تؤثر على كفاءة التطبيق أو التي تؤثر سلبًا على الأداء. استخدام إضافات مفيدة لتحسين تطبيقات لارافيل يمكننا تنزيل أدوات مفيدة في تحسين تطبيقات لارافيل مثل إضافة تسريع صفحات لارافيل Laravel Page Speed التي تساعد في تقليل حجم البيانات المرسلة إلى المتصفح، مما يسرّع تحميل الصفحات. نثبت الحزمة ونستخرج الملفات من renatomarinho/laravel-page-speed package باستخدام الملحن Composer ونضيف اسم الحزمة مع تفاصيل نسختها إلى الملف composer.json: "require": { ...... ...... "renatomarinho/laravel-page-speed": "^1.8" }, ثم ننفذ أمر تحديث الملحن Composer كما يلي: composer update بعد استخراج ملفات الحزمة السابقة، نضيف مزود الخدمة الخاص بها من خلال الانتقال إلى الملف config/app.php وإضافة السطر التالي: 'providers' => [ .... .... RenatoMarinho\LaravelPageSpeed\ServiceProvider::class, ], بعد ذلك، يجب نشر الحزمة حتى نتمكن من استخدامها في التطبيق. يساعد الأمر التالي غلى نشر الحزمة، ولا نستطيع استخدامها قبل تنفيذه: php artisan vendor:publish --provider="RenatoMarinho\LaravelPageSpeed\ServiceProvider" بعد نشر الحزمة، لا بد من إضافة تفاصيل Middleware الخاص بها في الملف app/Http/Kernel.php حتى تتمكن من العمل مع الطلبات الواردة إلى التطبيق. لهذا، ننسخ الشيفرة التالية ونلصقها بعد المصفوفة middlewareGroups$: protected $middlewareGroups = [ 'web' => [ // بعض الوسطاء السابقين... \RenatoMarinho\LaravelPageSpeed\Middleware\InlineCss::class, \RenatoMarinho\LaravelPageSpeed\Middleware\ElideAttributes::class, \RenatoMarinho\LaravelPageSpeed\Middleware\InsertDNSPrefetch::class, \RenatoMarinho\LaravelPageSpeed\Middleware\RemoveComments::class, \RenatoMarinho\LaravelPageSpeed\Middleware\TrimUrls::class, \RenatoMarinho\LaravelPageSpeed\Middleware\RemoveQuotes::class, \RenatoMarinho\LaravelPageSpeed\Middleware\CollapseWhitespace::class, ] ]; /* تعريف وجهة */ Route::get('/listView', function () { return view('listView'); }); ننشئ في النهاية قالب Laravel Blade ونضع ضمنه الشيفرة لعرضها. أدوات مفيدة لمراقبة أداء تطبيقات لارافيل يساعدنا مراقبة أداء تطبيقات لارافيل على تحديد الجوانب التي تحتاج إلى تطوير وتحسين في تطبيقنا، وفيما يلي نستعرض بعض الأداوت المفيدة لتحقيق ذلك. الأداة Blackfire.io تُعد Blackfire.io أداة مفيدة لمراقبة أداء التطبيقات وتنقيحها، فهي تزود المطورين ببيانات مفصلة عن أداء تطبيقاتهم وتوفر لهم إرشادات مفيدة لتحديد أي اختناقات تؤدي إلى بطء التطبيق. كما تساعد على تحديد المشكلات التي تؤثر على الأداء سواء كانت على مستوى الشيفرة أو استعلامات قواعد البيانات أو استدعاءات واجهات برمجية خارجية. وتتكامل هذه الأداة بسلاسة مع أطر عمل PHP مثل لارافيل و Symfony وغيرها، مما يُسهل إعدادها واستخدامها في تحليل الأداء. الأداة Laravel Dusk أداة اختبار End-to-End لإطار العمل لارافيل مصممة لتبسيط الاختبارات المؤتمتة للمتصفح في تطبيقات الويب، فاختبار End-to-End هو نوع من الاختبارات الآلية التي تحاكي سلوك المستخدم الحقيقي في التطبيق، مثل تسجيل الدخول والخروج وتعبئة النماذج وإرسالها والتنقل بين الصفحات والتأكد من ظهور العناصر في الواجهة بشكل صحيح. الأداة LoadForge وهي منصة سحابية تساعد المطورين في قياس قدرة التطبيقات والواجهات البرمجية على التعامل مع ضغط العمل. فمن خلال هذه الأداة يمكننا محاكاة دخول آلاف المستخدمين في نفس الوقت، مما يتيح لنا تقييم أداء المواقع والواجهات البرمجية تحت ضغط مكثف ويساعدنا في تحديد فيما إذا كان التطبيق يحتاج إلى التوسيع لتحمل المزيد من المستخدمين. الخاتمة استعرضنا في هذا المقال مجموعة من النصائح والممارسات الفعالة لتحسين أداء تطبيقات لارافيل، إلى جانب أدوات قوية تساعد في التعامل مع مختلف متطلبات العمل. ننصح بتطبيق هذه النصائح والاستفادة منها لبناء تطبيقات أسرع وأكثر كفاءة، وموثوقية، ويمكنها التكيف مع توسع التطبيق وزيادة عدد المستخدمين بسلاسة. ترجمة -وبتصرف- للمقال: 17 Tips for Laravel Performance Optimization in 2024 لصاحبه Mansoor Ahmed Khan اقرأ أيضًا أفضل الحزم البرمجية لتحسين تطبيقات لارافيل مقارنة بين استخدام ووردبريس ولارافيل في تطوير الويب إنشاء واجهة أمامية لمدونة باستخدام لارافيل تعرف على لغة PHP
  13. سنتعرف في هذا المقال على طريقة بناء أزرار عد تنازلي countdown ضمن ألعاب جودو لتساعدنا في تحقيق ميزة الانتظار في اللعبة، مثلًا يمكن أن لا تتفعّل ميزة أو قدرة معينة للاعب ما إلا بعد مضي فترة زمنية معينة، كما سنشرح طريقة بناء قائمة دائرية الشكل Radial menu تعرض للاعب عدة خيارات موزعة على شكل حلقة لتسهيل الوصول لكل خيار وإضافة طابع فريد لواجهة اللعبة. بناء أزرار العد التنازلي قد نرغب في إضافة عدة أزرار تمنح اللاعب مهارات أو قدرات خاصة ability buttons مع توفير ميزة الانتظار لفترة معينة قبل تمكين اللاعب من النقر على كل زر منها واكتساب القدرة المطلوبة، يمكننا تحقيق ذلك من خلال ميزة العد التنازلي countdown، وفي حال احتجنا لأيقونات ورسومات مناسبة لاستخدامها مع هذه الأزرار فستجد كمًا كبيرًا من التصاميم المناسبة في موقع Game-icons.net وسنستخدم بعضها في مقالنا. إعداد مشهد اللعبة يضم المشهد الذي سنعمل عليه العقد nodes التالية: لنوضح بإيجاز دور كل عقدة منها: العقدة النوع الوظيفة AbilityButton TextureButton زر ينشط قدرة خاصة للاعب عند الضغط عليه Sweep TextureProgress شريط تقدم يظهر عد تنازلي بعد الضغط على الزر Timer Timer مؤقت يتحكم في فترة التهدئة Cooldown قبل التمكن من إعادة استخدام القدرة الخاصة Counter MarginContainer حاوية خاصة تتيح إضافة هوامش Margins بين عناصرها Value Label مكون تسمية توضيحية يعرض التوقيت سنحدد الأيقونة الخاصة بكل زر من خلال الخاصية Textures ثم Normal لزر القدرة AbilityButton حيث يمكننا من هنا تحديد الأيقونة الافتراضية للزر عندما لا يكون مضغوطًا، ثم نختار القيمة Full Rect في العقدة Sweep من القائمة Presets لتحديد تأثيرات التعبئة أو المسح التدريجي ليكون على كامل الزر. بعد ذلك، نضبط الخاصية FillMode للعقدة Counter بالقيمة clockwise. نريد تغيير إضاءة زر الانتظار بشكل تدريجي وفق زاوية قطرية. لتحقيق ذلك، نختار الخاصية Visibilty للزر ثم Modulate ونختار قيمتها لتكون بلون رمادي قاتم مع إضافة بعض الشفافية لجعل الزر يبدو باهتًا في وضع الانتظار. نضبط عقدة المؤقت Timer على القيمة One Shot لجعلها تعمل مرة واحدة فقط، وفيما يخص العقدة Counter وهي حاوية تحتوي النص وتحاذيه، ينبغي ضبط تخطيطها على Bottom Wide وضبط الخاصيتين Margin Right و Margin Left للمسافات الجانبية على القيمة 5 وذلك ضمن القسم Theme Overrides ثم Constants. بالنسبة للعقدة value سنضبط خاصية المحاذاة الأفقية Horizontal Alignment على القيمة Right، وخاصية اقتصاص النص Clip Text على القيمة on لتجنب تجاوز النص لحدود الحاوية. ونختار الخط المناسب من القسم Theme Overrides ثم Font ونضع قيمة 0.0 في الحقل النصي. وطالما أن اﻷيقونة التي نستخدمها سوداء، فمن الجيد ضبط قيمة خاصية حجم الحدود Theme Outline Size من القسم Overrides ثم Constants بالقيمة 1 لجعل الأيقونة أكثر وضوحًا. إضافة كود برمجي لزر العد التنازلي نضيف سكريبت إلى عقدة زر القدرة AbilityButton. ثم نربط إشارة timeout الخاصة بالمؤقت Timer وإشارة pressed الخاصة بزر القدرة. وبالتالي عند النقر على الزر، سيبدأ العد التنازلي وعندما ينتهي العد يمكننا تنفيذ إجراء معين. extends TextureButton class_name AbilityButton @onready var time_label = $Counter/Value @export var cooldown = 1.0 func _ready(): time_label.hide() $Sweep.value = 0 $Sweep.texture_progress = texture_normal $Timer.wait_time = cooldown set_process(false) يبدأ السكريبت بتصدير المتغير cooldown الذي يحدد طول فترة الانتظار قبل تفعيل الزر، ومن ثم نضبط المؤقت Timer داخل التابع ()ready_ لاستخدام هذه القيمة. سنحتاج بعد ذلك لخامة texture لنسندها إلى TextureProgress، سنستخدم نفس خامة الزر، ويمكن استخدام أي خامة أخرى نفضلها. أخيرًا، لنتأكد من أن العمليات الخاصة بالمتغير Sweep قد انتهت بشكل صحيح، سنتأكد إن كانت قيمة Sweep هي 0 ونضبط قيمة معالجة العقدة processing على false. وبما أننا ننفذ التحريك ضمن التابع ()process_ لذا لا نحتاج لتنفيذ هذا التابع إن لم نكن في فترة التهدئة CoolDown. func _process(delta): time_label.text = "%3.1f" % $Timer.time_left $Sweep.value = int(($Timer.time_left / cooldown) * 100) نلاحظ في الكود السابق أننا استخدمنا الخاصية time_left للمؤقت Timer لضبط الخاصية text للعقدة labe والخاصية value للعقدة Sweep. func _on_AbilityButton_pressed(): disabled = true set_process(true) $Timer.start() time_label.show() عندما يُنقر الزر سيبدأ كل شيء: func _on_Timer_timeout(): print("ability ready") $Sweep.value = 0 disabled = false time_label.hide() set_process(false) كما يعود كل شيء إلى وضعه عندما ينتهي المؤقت من العد. بإمكاننا وضع عدة أزرار ضمن عقدة حاوية من النوع HBoxContainer وسنحصل على شريط أفقي من أزرار القدرة كما يلي: بناء قائمة دائرية منبثقة تُستخدم القوائم في العديد من اﻷلعاب للوصول إلى ميزات أو وظائف معينة، كأن نحدد من خلالها المهمة المطلوب تنفيذها في اللعبة حاليًا مثل التحدث أو التفتيش أو الهجوم وهكذا. ينبغي أن يكون مظهر وسلوك القائمة متلائمًا مع لعبتنا، لكننا سنركز في هذا المثال على آلية بناء قوائم دائرية Radial Menu ونترك لك حرية تنسيقها. توضح الصورة التالية قائمة العقد المطلوبة لتنفيذ القائمة: نحتاج لاستخدام عقدة TextureButton من النوع RadialMenuButton لتكون عقدة جذر وهي تمثل الزر الرئيسي الذي سننقره لفتح أو إغلاق القائمة الدائرية، وعقدة Buttons من النوع control كحاوية تتضمن كافة الأزرار التي نريد عرضها في القائمة الدائرية، ونتأكد من ضبط قيمة الخاصية Mouse ثم Filter على القيمة Ignore كي لا تعترض أفعال النقر على الفأرة. كما سنستخدم تسعة أزرار لعرض القدرات الخاصة من نوع العداد التنازلي Cooldown. الخطوة التالية هي إضافة السكريبت التالي للعقدة الجذر: extends TextureButton class_name RadialMenuButton export var radius = 120 export var speed = 0.25 var num var active = false يمثل المتغير radius حجم القائمة وهو قطر الدائرة التي سنوزع عليها اﻷزرار، بينما يُستخدم المتغير speed في تحديد سرعة تحريك أزرار القائمة فالقيم اﻷصغر هي اﻷسرع. ويحدد المتغير num عدد اﻷزرار في القائمة، بينما يمثل المتغير active راية flag تدل على إغلاق أو فتح القائمة. func _ready(): $Buttons.hide() num = $Buttons.get_child_count() for b in $Buttons.get_children(): b.position = position نبدأ بإعداد منطق القائمة في التابع ()ready_ وذلك بإخفاء جميع أزرار القائمة افتراضيًا وضبط المسافة بينها وبين الزر الرئيسي للقائمة. ثم نربط اﻹشارة pressed للزر الرئيسي: func _on_pressed(): disabled = true if active: hide_menu() else: show_menu() سيخفي النقر على الزر القائمة أو يظهرها، ونحتاج أيضًا إلى تعطيل الزر أثناء عملية تحريك الرسومات، وإلا سيعيد النقر عليه توليد اﻹطارات البينية tween وإعادة التحريك من جديد: func _on_tween_finished(): disabled = false if not active: $Buttons.hide() عندما ينتهي تحريك اﻹطارات البينية، ننقل حالة الزر إلى تمكين مجددًا. لنلقِ نظرة على الدالة ()show_menu: func show_menu(): $Buttons.show() var spacing = TAU / num active = true var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): #لوضع الزر اﻷول في اﻷعلى PI/2 اطرح var a = spacing * b.get_position_in_parent() - PI / 2 var dest = Vector2(radius, 0).rotated(a) tw.tween_property(b, "position", dest, speed).from(Vector2.ZERO).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT) tw.tween_property(b, "scale", Vector2.ONE, speed).from(Vector2(0.5, 0.5)).set_trans(Tween.TRANS_LINEAR) نحسب في هذه الدالة المسافة spacing أو بالأصح الزاوية التي نريدها بين كل عنصرين على القائمة، ومن ثم نتنقل بين اﻷزرار ونحدد وجهة كل زر dest وفقًا للزاوية المحسوبة وقيمة نصف القطر radius. ونولد لكل زر خاصيتين هما position و scale لإعطاء اﻷثر المرغوب عند توليد إطارات التحريك tween أثناء تحرك الزر. وتنفذ الدالة ()hide_menu العكس تمامًا: func hide_menu(): active = false var tw = create_tween().set_parallel() tw.finished.connect(_on_tween_finished) for b in $Buttons.get_children(): tw.tween_property(b, "position", Vector2.ZERO, speed).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_IN) tw.tween_property(b, "scale", Vector2(0.5, 0.5), speed).set_trans(Tween.TRANS_LINEAR) ستبدو القائمة كالتالي: الخاتمة شرحنا في هذا المقال كيفية إنشاء أزرار عد تنازلي في محرك الألعاب جودو، والتي تتيح لنا ضبط فترات انتظار قبل إعادة استخدام الأزرار، وهي ميزة ضرورية للألعاب التي تعتمد على منح قدرات أو ميزات خاصة بعد فترة انتظار وتحديد فترة انتظار بعد كل استخدام. كما تناولنا آلية بناء قوائم دائرية توفر تجربة تفاعلية سلسة لعرض الأزار من خلال توزيع الأزرار بشكل منظم حول نقطة مركزية لتسهيل الوصول للخيارات المختلفة داخل اللعبة. ترجمة -وبتصرف- للمقالين: CoolDown Button و Radial Popup Menu اقرأ أيضًا المقال السابق: التعامل مع إجراءات دخل الفأرة في جودو تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تعرف على أشهر محركات الألعاب Game Engines الطريقة الصحيحة للتواصل بين العقد في جودو
  14. تبرز منصتا ووردبريس ولارافيل كخيارات مثالية في عالم الويب الذي يتطور باستمرار، هذا قد يدفعنا للتساؤل ما الخيار الأفضل من بينهما لتطوير الويب، والجواب على هذا السؤال يعتمد على طبيعة احتياجاتنا، ويحتاج منا لفهم نقاط القوة التي تميز كل منصة ونقاط الضعف المحتملة. نقارن في هذا المقال بين لارافيل و ووردبريس، ونحاول تقديم رؤية واضحة من خلال البحث في جوانب حساسة تتعلق بالمرونة وإمكانية زيادة حجم الأعمال والأمان وسهولة الاستخدام وقابلية توسيع المنصة extensibility كي نتخذ قرارًا مدروسًا. نظرة عامة على لارافيل و ووردبريس قبل أن ننتقل إلى المقارنة بين إطاري العمل، لنلقِ نظرة سريعة عامة عليهما. نظرة عامة على لارافيل لارافيل هو إطار عمل framework لتطوير الويب يعتمد على لغة PHP ويكتسب شعبية متزايدة بين المطورين، وقد اكتسب شعبيته نظرًا لصياغته المحسنّة وتوثيقه الشامل ووجود مجتمع داعم كبير ونشط. تظهر قوة الإطار في إنتاج تطبيقات ويب مخصصة وقابلة للتوسع. يقدم لارافيل مجموعة ملفتة من الميزات ويضم طبقة ORM فعّالة للتفاعل مع قاعدة البيانات حيث توفر هذه الطبقة واجهة لتمثيل الجداول في قاعدة البيانات ككائنات objects. ومنظومة توجيه Routing قوية، وصياغة قواعدية Syntax سهلة الفهم. تجعل هذه الميزات من لارافيل خيارًا ممتازًا للمطورين الذي يرغبون في بناء تطبيقات أكثر تخصصًا وقدرة على التكيف. نظرة عامة على ووردبريس تخطى ووردبريس الغاية الأصلية له كمنصة لإنشاء المدونات وتعداها ليصبح نظام إدارة محتوى Content Management System متعدد المهام يدعم أكثر من 40% من مواقع الويب عالميًا، ولا تزال سمعته كمنصة لإنشاء وإدارة محتوى ويب سهل الاستخدام لا جدال فيها. تمكن ووردبريس من جذب الكثير من الاهتمام بفضل واجهته الواضحة سهلة الاستخدام والعدد الكبير من القوالب والإضافات التي يوفرها وسهولة تشغيله. ويعد إطاراً مناسبًا للأفراد أو الأعمال الصغيرة التي تطمح إلى تأسيس موقع ويب دون الحاجة إلى خبرة تقنية كبيرة. مقارنة سريعة بين لارافيل و ووردبريس الميزة لارافيل ووردبريس نوع إطار العمل إطار عمل PHP مع أسلوب MVC نظام إدارة محتوى CMS نموذج التطوير برمجة كائنية التوجه OOP برمجة إجرائية مع بعض الكائنية تنظيم الشيفرة تتبع أسلوب MVC أي نموذج وعرض ومتحكم مرن نسبيًا، يتبع هيكلية القوالب والإضافات إمكانية التخصيص عالية جدًا محدودة بالقوالب والإضافات منحني التعلم منحني تعلم متدرج أسهل تعلمًا وخاصة للمبتدئين الأداء محسّن جدًا وسريع قد يتأثر الأداء بالإضافات دعم قواعد البيانات يدعم عدة قواعد بيانات من خلال تقنية ORM مثل Eloquent دعم مضمّن لقاعدة البيانات MySQL مع دعم قواعد بيانات أخرى الأمان يوفر معايير أمان مرتفعة عرضة للهجمات إن لم نعمل على صيانته باستمرار إمكانية التوسع مناسب لبناء تطبيقات قابلة للتوسع يعتمد على الإضافات وعلى الخادم مجتمع الداعمين مجتمع كبير ونشط مجتمع ضخم وداعم حالات الاستخدام تطبيقات ويب معقدة وواجهات برمجية مدونات ومواقع ويب صغيرة إلى متوسطة سرعة التطوير يتطلب وقتًا في تنفيذ الإعدادات الأولية وضبطها إعدادات سريعة وميزات وقوالب جاهزة للاستخدام الصيانة يحتاج إلى تحديث منتظم وصيانة يحتاج إلى تحديث متكرر وصيانة التكلفة مجاني ومفتوح المصدر مجاني ومفتوح المصدر المقارنة الشاملة بين لارافيل و ووردبريس بعد أن تعرفنا سريعًا على كلتا المنصتين، بإمكاننا الآن المقارنة بينهما من عدة نواحٍ: 1- الأداء وإمكانية التوسع تعد ميزات الأداء والتوسع ذات أهمية كبيرة في تطوير الويب حاليًا لأنها تؤثر بشكل كبير على تجربة المستخدمين، وزمن تحميل الصفحات، والاستجابة الكلية للتطبيق أو الموقع. لنوضح كيف تتعامل كلتا المنصتين مع هذه النواحي الحيوية: لارافيل لارفيل هو إطار عمل للغة PHP كما ذكرنا سابقًا، لهذا يستخدم مجموعة من التقنيات لتحسين الأداء وتنفيذ العمليات بسلاسة. ونظرًا لكون لارافيل مبني على الوحدات البرمجية وتوسيع الشيفرة فهو يتيح للمطورين بناء تطبيقات قادرة على النمو. ومن خلال دعم لارافيل لإمكانيات التوسع الأفقي، فإنه يستطيع إدارة التطبيقات الموزعة على عدة خوادم باستخدام أدوات مثل موازنات الحمل. كما يسهل لارافيل التعامل مع المهام المتزامنة كونه يدعم أنظمة الأرتال queueing systems مثل Redis و RabbiMO. إضافة إلى ذلك، تستفيد منصة لارافيل من فعالية محرّك PHP الذي يُحدّث ويُحسّن باستمرار. ووردبريس بُني نظام ووردبريس بشكل رئيسي باستخدام PHP، وقد خضع لترقيات مهمة في السنوات الأخيرة لتحسين الأداء قدر الإمكان. يوفر ووردبريس مكتبة واسعة من الإضافات والقوالب التي تسمح للمستخدمين يتطوير وتخصيص مواقعهم بسرعة وسهولة. وعلى الرغم من أن نظام ووردبريس قد صمم أساسًا لبناء مواقع ويب صغيرة الحجم ومحدودة الموارد، إلا أنه يسمح لنا بتعزيز موارد الموقع عند الحاجة باستخدام إضافات التخزين المؤقت، وشبكات توزيع المحتوى CDN، واستخدام بيئة استضافة محسنة لإدارة حركة البيانات الكثيفة فهذه التقنيات تعزز أداء مواقع ووردبريس بشكل كبير. 2- سهولة التطوير يضع المطورون الأولوية لسهولة تطوير التطبيقات عند اختيارهم لأي منصة أو إطار العمل. لهذا، نناقش في هذه الفقرة الفرق بين لارافيل و ووردبريس من ناحية منحنيات التعلم والموارد والتوثيق ودعم المجتمع الخاص بكل إطار. لارافيل يتميز لارافيل بصياغته القواعدية الأنيقة وميزاته المصممة خصيصًا لتسهيل عمل المطورين، وأولوياته في دعم سهولة تطوير المنتجات. إذ يقدم لارافيل أساس برمجي واضح البناء ومباشر وشيفرة سهلة الصيانة. وتبسط بيئة عمل لارافيل التي تضم حزم مضمّنة جاهزة المهام البرمجية الشائعة مثل التوجيه routing والاستيثاق authentication والتفاعل مع قواعد البيانات والتخزين المؤقت للبيانات. تتمحور فلسفة لارافيل الجوهرية حول فكرة إنتاجية المطور وسهولة الاستخدام، وتعزز إمكانية إعادة استخدام الشيفرة لتسهيل صيانة وتحديث المنتج. ووردبريس تدور فكرة التطوير في ووردبريس حول البساطة وسهولة الوصول، فهو يزود المستخدم بلوحة إدارة سهلة الاستخدام لتسريع إدارة المحتوى وتخصيصه. وتُعد قوالب وإضافات ووردبريس عناصر بناء التطبيقات التي تسمح للمطور بتوسيع التصميم ووظائف التطبيق دون العودة إلى نقطة الصفر. وتمكّن خطافات ووردبريس hooks ومرشحاته filters وقوالبه ذات البنية الهرمية وميزة التحديث التلقائي من بناء أقسام مميزة في مواقع الويب، وتعديل الوظائف البنيوية وضمان بقاء الموقع محدّثًا بأدنى جهد ممكن. 3- سهولة الاستخدام تؤثر سهولة استخدام المنصات على تجربة المستخدم ورضاه، لنلق نظرة الآن على ما يقدمه كل من لارافيل و ووردبريس في هذا الشأن لارافيل يركز لارافيل على سهولة الاستخدام من قبل المطورين أساسًا وبعدها سهولة الاستخدام من قبل المستخدم النهائي، حيث يوفر للمطور قاعدة واضحة وسهلة القراءة لكتابة الشيفرة. كما يبسّط التوثيق الشامل والهيكلية الجيدة للواجهات البرمجية من عملية التطوير. كما صممت ميزات لارافيل مثل التوجيه routing وإدارة قواعد البيانات والتوثيق لتكون سهلة الاستخدام من قبل المطورين، مما يعطي فعالية أكبر في التطوير دون المساس بالمرونة. ووردبريس يتمير ووردبريس بسهولة الاستخدام، سواء للمطورين أو المستخدمين غير المختصين. إذ فهو يوفر للمستخدمين واجهة سهلة الاستخدام، إضافة إلى لوحة تحكم واضحة ومكتبات كثيرة للقوالب والإضافات ويوفر أدوات لبناء الصفحات عبر تقنية السحب والإفلات، مما يجعل تخصيص مواقع الويب أمرًا سهلًا دون الحاجة إلى خبرات برمجية كبيرة. 4- الأمان والصيانة يعد موضوع الأمان والصيانة من أكثر الأمور أهمية عند اختيار منصة العمل، لنلق نظرة على ما تقدمه لارافيل و ووردبريس في هاتين الناحيتين. لارافيل يقدم لارافيل حماية مضمّنة من ثغرات الويب الشائعة مثل السكربتات العابرة للمواقع cross-site scripting وسكربتات انتحال الشخصية request forgery وهجوم حقن SQL. إذ يتجاوز باني الاستعلامات وطبقة ORM في لارافيل أي محارف مشبوهة في مدخلات المستخدم للحماية من حقن SQL، وتدعم كلمات السر المرمّزة بما في ذلك ميزات أمنية مثل مفاتيح التحقق CSFR والتعامل الآمن مع الجلسات. ووردبريس يلتزم ووردبريس بتقديم منصة آمنة وتنفيذ معايير أمنية متعددة. وتستهدف تحديثاتها المنتظمة الثغرات التي تظهر ويراقب فريق متخصص المشاكل الأمنية بنشاط ويستجيب لأي مشكلات قد تظهر لاحقًا. إضافة إلى ذلك، يحسن ووردبريس من القدرات الأمنية للتطبيقات من خلال إضافات مخصصة تقدم ميزات مثل فحص البرمجيات الضارة والحماية خلف جدار ناري وتسجيل الدخول الآمن. 5- الشعبية تلعب شعبية المنصة دورًا مهمًا في اختيارها، فهي تعكس نسبة تبني هذه المنصة والدعم المقدم من مجتمعها وطبيعة بيئة العمل فيها. لارافيل يعود سبب الزيادة الكبيرة في شعبية لارافيل إلى الدعم القوي الذي يقدمه مجتمع دعم المنصة وبيئة العمل التي تدعم التوسع. المصدر trends.builtwith يضمن التطوير الفعال للارافيل والتحديثات المستمرة التي تنفذ عليها وصول المطورين إلى أحدث الميزات والتحسينات والتحديثات الأمنية. وتشير شعبية لارافيل المتزايدة إلى الطلب الكبير عليه في سوق العمل، وهذا ما يقدم فرص عمل أوسع لمطوري لارافيل المحترفين. ووردبريس ترجع شعبية ووردبريس إلى القاعدة المعرفية الواسعة لهذه المنصة والدعم الكافي من قبل مجتمعها. المصدر trends.builtwith تساعد القوالب والإضافات الكثيرة والمتنوعة في تقديم تصاميم ووظائف مختلفة، كما تثري التطويرات المستمرة التي يقدمها مجتمع ووردبريس من بيئة العمل وتعززها. 6- الاستضافة إن اختيار الاستضافة المناسبة لموقعنا أو تطبيقنا أمر حيوي جدًا لأدائه وأمانه وتوفّره المستمر. لنرى كيف يكون نهج الاستضافة في كلتا المنصتين. لارافيل لا تتطلب منصة لارافيل أية متطلبات خاصة بالاستضافة نظرًا لمرونتها وكونها إطار عمل للغة PHP. ويمكن للمطورين الاستفادة من مزودات الاستضافة التي تقدم عروضًا مخصصة لمنصة لارافيل أو التي تقدم بيئة عمل محسنة للتطبيقات المبنية على هذه المنصة. ووردبريس صممت منصة ووردبريس لتكون متوافقة مع عدد كبير من بيئات الاستضافة، لكن علينا التفكير بعدة عوامل مثل أداء الخادم وسعة التخزين وحزمة تخديم البيانات ودعم العملاء عند البحث عن استضافة مناسبة لووردبريس. كما تقدم مزودات الاستضافة المخصصة لإدارة تطبيقات ووردبريس إعدادات محسنة للخادم وتحديثات تلقائية وميزات أمان إضافية. المقارنة بين لارافيل و ووردبريس من ناحية التكلفة من الأمور المهمة جدًا في المقارنة بين لارافيل ووردبريس مسألة التكلفة المادية. فمعرفة هيكلية التسعير وخيارات الاستضافة ستساعدنا في اختيار الحل الأكثر فعالية. تكلفة لارافيل لارافيل بحد ذاته مجاني الاستخدام، لكن أي مساعدة في تطوير تطبيقات لارافيل وصيانتها لن تكون رخيصة. وقد يكون مجال تلك التكلفة في الولايات المتحدة الأمريكية مثلًا بين 20-100 دولار في الساعة، مما يجعل كلفة موقع الويب بأكمله بين 3000 و 250 ألف دولار، وفقًا لمتطلبات المشروع وتعقيدها، وبالطبع تختلف هذه الأرقام من منطقة جغرافية لأخرى. كما نضيف إلى ذلك كلفة الاستضافة وقيمتها بالحد الأدنى 10 دولارات شهريًا. تكلفة ووردبريس تختلف تكلفة تطبيقات ووردبريس بحسب إن كنا سنصمم التطبيق بأنفسنا أو نستعين بشركة أو وكالة تطوير. فقد يكلف التطبيق إن قررنا تصميمه بنفسنا بين 20 إلى 300 دولار كما يصل إلى مجال 500-5000 دولار في حال الاستعانة بمطور مستقل وترتفع تكلفة التطوير إلى 3000-100000 دولار إن أردنا الاعتماد على شركة تطوير، ومن جديد ننوه لأن هذه الأرقام تختلف من منطقة جغرافية لأخرى. وتتراوح نفقات الصيانة بين 25 دولار في الشهر للصيانة الذاتية وما بين 50 إلى 100 دولار إن استعنا بمطور مستقل، كما قد تصل إلى 450 دولار إن اعتمدنا على شركة مختصة. كما نضيف إلى ذلك كلفة الاستضافة بحد ذاتها والتي تختلف في حدها الأدنى وفق مزود الاستضافة، وقد لا تقل عن 10 دولار شهريًا. وتتعلق تكلفة الاستضافة عمومًا بحجم الموقع وحجم تبادل البيانات المسموح شهريًا إضافة إلى بعض الميزات الأخرى التي تقع خارج إطار هذا المقال. بإمكاننا بالطبع الاستفادة من المستقلين العرب المتواجدين في منصة مستقل منصة العمل العربي الأكثر شهرة لبناء تطبيقات ويب مميزة باستخدام لارافيل أو ووردبريس بحرفية عالية وأسعار مدروسة. خلاصة: ما الخيار الأفضل لارافيل أم ووردبريس؟ يعتمد الخيار الأمثل على احتياجاتنا الخاصة وأهدافنا من التطبيق أو موقع الويب. إذ تكون لارافيل خيارًا مفضلًا عند إنشاء تطبيقات ويب مخصصة لأنها تقدم مرونة كبيرة وقابلية لتوسيع التطبيق وبيئة عمل قوية مفصلة خصيصًا للمطورين. بينما تشتهر ووردبريس في المقابل بسهولة استخدامها وفعاليتها في إدارة المحتوى وفهارسها التي تمتلئ بالقوالب والإضافات، مما يجعلها خيارًا مفضلًا للأعمال الفردية الصغيرة التي تهدف إلى تأسيس مكان على الإنترنت بسرعة. ومن الضروري جدًا أخذ متطلبات مشروعنا بعين الاعتبار، وخبراتنا في تطوير التطبيقات، ورؤيتنا المستقبلية للمنتج قبل أن نقرر ما هي المنصة الملائمة. أسئلة شائعة 1- من أفضل لارافيل أم ووردبريس الجواب: للمنصتين غايتان مختلفتان فلارافيل هو إطار عمل قوي للغة PHP يستخدم في تطوير تطبيقات الويب، بينما يشتهر ووردبريس بكونه نظام إدارة محتوى متألق في بناء المدونات ومواقع الويب. لهذا السبب، لا يمكن المقارنة بين المنصتين وفق هذا السياق. 3- هل ووردبريس سهل الاستخدام أكثر من لارافيل الجواب: يصنف ووردبريس بأنه أسهل استخدامًا وأبسط تعلمًا من لارافيل. فواجهته الواضحة وسهولة استخدامه عند إنشاء مواقع الويب وإدارة محتوياتها تجعله مفضلًا للمبتدئين الذين يفتقرون إلى الخبرة التقنية في مجال تطوير الويب، بينما يحتاج لارافيل في المقابل إلى معرفة برمجية جيدة لتطوير تطبيقات ويب فعالة. 4- متى نفضل لارافيل على ووردبريس الجواب: لارافيل هو الحل المناسب لبناء تطبيقات ويب معقدة بوظائف خاصة وقدرة على التوسع والتخصيص. بينما ووردبريس هو خيار مثالي عند بناء مواقع ويب هدفها الأساسي تقديم محتوى مثل المدونات والمواقع التعريفية، لأنه يقدم طريقة سريعة في إعداد الموقع من خلال عمليات واضحة وسهلة الاستخدام. ترجمة -وبتصرف- لمقال: Laravel vs WordPress: choosing the Ideal platform for your peb development needs لكاتبه Inshal Ali اقرأ أيضًا تعرف على إطار عمل تطوير الويب الشهير لارافيل Laravel إنشاء واجهة أمامية لمدونة باستخدام لارافيل أفضل الحزم البرمجية لتحسين تطبيقات لارافيل تعلم PHP تعلم ووردبريس
  15. نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي 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() سحب وتحديد عدة عناصر باستخدام الفأرة قد نضطر في اﻷلعاب الاستراتيجية المباشرة لاختيار عدة عناصر أو وحدات لعب دفعة واحدة وإعطائها أوامر معينة، وستكون العملية عادة من خلال رسم صندوق مربع حول هذه العناصر مما يسبب اختيارها، وعندها يمكننا النقر على مكان ما على الخريطة مثلًا لنأمر هذه العناصر بالتحرك إلى هذا المكان. كما في المثال الظاهر في الصورة التالية: إعداد عناصر اللعب لاختبار اﻷمر، نحتاج إلى عدة عناصر تتحرك باتجاه محدد في اللعبة دون أن تتحرك نحو بعضها. إن أردنا أساسًا لبناء هذه الوحدات، يمكن العودة إلى المثال المكتمل ثم إزالة التعليقات عن أسطره لأننا لن نخوض في تفاصيل إنشاء مثل هذه العناصر في المقال. إعداد عالم اللعبة سنعالج عملية اختيار العناصر في عالم اللعبة، لهذا سننشئ هذا العالم من خلال اختيار عقدة 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 إرسال اﻷوامر إلى العناصر بإمكاننا اﻵن إرسال أمر التحرك للعناصر لتتجه نحو مكان ما على الشاشة: 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 اقرأ أيضًا المقال السابق: العمل مع إجراءات الدخل Inputs Actions في جودو حفظ واسترجاع البيانات المحلية بين جلسات اللعب تحريك اللاعب برمجيًا في لعبة ثلاثية الأبعاد باستخدام محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو
×
×
  • أضف...