-
المساهمات
163 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو ابراهيم الخضور
-
نشرح في هذا المقال طريقة إنشاء جسم يمثل سفنية تتحرك بطريقة واقعية في الفضاء باستخدام عقدة الجسم الصلب 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 في جودو
-
عند تطوير الألعاب باستخدام جودو، من الشائع أن نربط الأصوات بالأحداث التي تقوم بها الشخصيات، كتشغيل صوت معين عند موت الشخصية وصوت آخر عندما تنفذ هجومًا معينًا. ونستخدم غالبًا العقدة 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
-
بدأنا في المقال السابق شرح بعض المفاهيم الرياضية الأساسية التي يحتاج مطور الألعاب لمعرفتها، مثل الاستيفاء الخطي 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 باستخدام محرر التحريك في جودو
-
تعتمد الأعمال الرقمية حاليًا على الخدمات السحابية لتسهيل التفاعل مع العملاء، ويتطلب الأمر تجميع وتخزين ومعالجة كميات هائلة من البيانات قبل تقديمها إلى المستخدم النهائي. وهنا يأتي دور تطبيقات الويب السحابية. فعندما نتكلم عن الخدمات السحابية نتذكر مباشرة النماذج التالية: البرمجيات كخدمات سحابية 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
-
يحتاج أي مطور ألعاب لمعرفة بعض المفاهيم الرياضية الأساسية ليتحكم في انتقال الشخصيات من حالة لأخرى بانسيابية ويتحكم في اتجاهاتها وتمكينها من معرفة ماذا يوجد أمامها وخلفها، وسنناقش في مقال اليوم مفاهيم تستخدم بكثرة في تطوير اﻷلعاب، مثل الاستيفاء الخطي 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
-
نشرح في هذا المقال طريقة تفاعل شخصية اللاعب في جودو مع الأجسام الصلبة الموجودة في المشهد. ويمكن أن نطبق الطريقة التي سنشرحها على الفضائين الثنائي والثلاثي البعد على حد سواء. التفاعل مع الأجسام الصلبة إذا جربنا استخدام العقدة 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 للألعاب في جودو
-
الخرائط المصغرة هي عبارة عن واجهات رسومية صغيرة تظهر في زاوية شاشة اللعب، تعرض تمثيلًا مصغرًا لخريطة اللعبة الكاملة أو المنطقة المحيطة باللاعب. لتساعد على تحديد موقعنا داخل اللعبة وترينا أماكن الأعداء أو الأشياء المهمة من حولنا، فهي تعمل كنظام رادار لكشف الأعداء والأهداف المخفية وتوفر صورة عامة عن بيئة اللعب ككل. سنبني في هذا المقال خريطة مصغرة تعرض موقع اﻷشياء الواقعة خارج مجال رؤية اللاعب بشكل نقاط أو أيقونات صغيرة وسنحدث مواقع هذه النقاط كلما تحرك اللاعب. إعداد المشروع سننشئ لعبة تخطيطها من اﻷعلى إلى اﻷسفل وسنستخدم ميزة 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 اقرأ أيضًا المقال السابق: إنشاء قائمة لاختيار مستوى اللعبة في جودو بناء شريط صحة ونصوص طافية في ألعاب جودو عرض عداد تنازلي وقائمة دائرية في جودو التعامل مع إجراءات دخل الفأرة في جودو
-
نشرح في هذا المقال طريقة اختيار أجسام صلبة وسحبها وإفلاتها من مكان لآخر باستخدام الفأرة في محرك الألعاب جودو، فقد يكون العمل مع الأجسام الصلبة مربكًا في جودو نظرًا لتحكم محرّك الفيزياء بهذه الحركة، وأي تدخل من قبلنا في الأمر سيقود غالبًا إلى نتائج غير متوقعة. إن مفتاح الحل في هذه الحالة هو استخدام الخاصية 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
-
نشرح في هذا المقال طريقة بناء قائمة تتضمن مجموعة خيارات تظهر في بداية اللعبة وتساعد اللاعب على تحديد مستوى اللعبة. سيكون تصميم القائمة على شكل شبكة قابلة للتمرير وضمنها صناديق تمثل كل مستوى، بحيث يمكن التنقل بينها واختيار المرحلة التي يرغب اللاعب في اللعب فيها كما توضح الصورة التالية. مراحل بناء القائمة سنبني القائمة بتصميم شبكة قابلة للتمرير مكونة من صناديق لتحديد المستوى بحيث يمكن للاعب أن يختار فيما بينها. سنبدأ أولًا ببناء صندوق المستوى 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
-
نشرح في هذا المقال طريقة تدوير جسم صلب بسلاسة في محرك الألعاب جودو من خلال استخدام العقدة 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 التعامل مع إجراءات دخل الفأرة في جودو
-
نتعرف في هذا المقال على طريقة بناء شريط صحة 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 تعرف على واجهة محرك الألعاب جودو
-
تزايدت شعبية إطار العمل لارافيل 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
-
سنتعرف في هذا المقال على طريقة بناء أزرار عد تنازلي 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 الطريقة الصحيحة للتواصل بين العقد في جودو
-
تبرز منصتا ووردبريس ولارافيل كخيارات مثالية في عالم الويب الذي يتطور باستمرار، هذا قد يدفعنا للتساؤل ما الخيار الأفضل من بينهما لتطوير الويب، والجواب على هذا السؤال يعتمد على طبيعة احتياجاتنا، ويحتاج منا لفهم نقاط القوة التي تميز كل منصة ونقاط الضعف المحتملة. نقارن في هذا المقال بين لارافيل و ووردبريس، ونحاول تقديم رؤية واضحة من خلال البحث في جوانب حساسة تتعلق بالمرونة وإمكانية زيادة حجم الأعمال والأمان وسهولة الاستخدام وقابلية توسيع المنصة 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 تعلم ووردبريس
-
نتحدث في هذا المقال عن طرق التقاط مدخلات الفأرة في جودو، وذلك من خلال العمل مع الصنف الأساسي 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 باستخدام محرر التحريك في جودو
-
نشرح في هذا المقال كيفية استخدام إجراءات الدخل Input Actions في محرك الألعاب جودو، والتي توفر لنا طريقة فعالة للتحكم في الشخصيات والعناصر داخل اللعبة. فبدلاً من تحديد كل مفتاح كتابيًا في الشيفرة البرمجية، يمكننا استخدام إجراءات الدخل لتهيئة المدخلات وتخصيص مفاتيح التحكم باللاعب بطريقة منظمة وسهلة التعديل. لنفترض أن لدينا شخصية تتحرك في لعبتنا من اﻷعلى إلى اﻷسفل وأردنا كتابة شيفرة باستخدام العقدة InputActionKey التي تتيح لنا بالتحكم بالشخصية عبر مفاتيح اﻷسهم في لوحة المفاتيح، لكن سنجد أن الكثير من اللاعبين يفضلون استخادم المفاتيح W و A و S و D للتحكم بالشخصية، قد نحاول العودة إلى اللعبة وإضافة مفاتيح إضافية لكن النتيجة ستكون شيفرة مضاعفة وزائدة. هنا تساعدنا إجراءات الدخل Input Actionsعلى تهيئة الشيفرة البرمجية للشخصية بشكل أفضل بدلًا من تحديد كل مفتاح كتابيًا في الشيفرة، وسنكون قادرين على تعديل وتخصيص المفاتيح المطلوبة دون تغيير كامل الشيفرة. إنشاء المدخلات بإمكاننا تعريف إجراءات دخل وتعيينها من داخل محرر جودو من خلال الانتقال إلى المشروع Project ثم إعدادات المشروع Project Settings ثم الانتقال لتبويب خريطة اﻹدخال Input Map. سنجد عند النقر على هذا التبويب بعض إجراءات الدخل المهيأة افتراضيًا تُعرف جميعها بالاسم *_ui كي نعلم بوجود إجراءات إدخال افتراضية. ملاحظة: نحتاج لتفعيل مفتاح التبديل أظهر الإجراءات المدمجة Show default Actions من أجل عرض إجراءات الدخل. لا بد عمومًا من إنشاء أحداثنا الخاصة بدلًا من الاعتماد على اﻷحداث الموجودة، لهذا، سنفترض أننا نريد السماح للاعب بالتحرك والدوران ضمن اللعبة عبر لوحة المفاتيح والفأرة، ونحتاج لتمكين اللاعب من التصويب من خلال الضغط على زر الفأرة اﻷيسر أو من خلال الضغط على مفتاح المسافة Spacebar. سننشئ إجراءً جديدًا يُدعى shoot بكتابة اسم اﻹجراء ضمن الحقل إضافة إجراء جديد Add New Action ثم ننقر على زر أضف Add أو الضغط على مفتاح Enter، وسنرى أن الإجراء أضيف إلى القائمة الموجودة. سنتمكن اﻵن من تعيين مدخلات لهذا اﻹجراء بالنقر على الزر + إلى اليمين. قد تكون المدخلات من لوحة المفاتيح أو أزرار الفأرة أو عصا التحكم. وقد اخترنا في حالتنا لوحة المفاتيح Key ثم نقرنا ضمن حقل الاستماع إلى المدخلات listen to inputs ونقرنا بعدها على مفتاح المسافة Spacebar لتعيينه كمفتاح لحدث الدخل، ثم نقرنا على زر حسنًا OK للموافقة على الإضافة كما في الصورة التالية: بنفس الطريقة سندخل إجراء آخر ونختار أزرار الفأرة Mouse Button ونتأكد من وجودنا ضمن حقل الاستماع إلى المدخلات listen to inputs ثم ننقر زر الفأرة اﻷيسر لتعيينه. استخدام إجراءات الدخل بإمكاننا التحقق من اﻹجراء باستدعاء الصنف Input في كل إطار: func _process(delta): if Input.is_action_pressed("shoot"): #ستُنقذ هذه الشيفرة في كل إطار طالما أن زر اﻹدخال مضغوط ولهذا اﻷمر أهميته للإجراءات المستمرة التي نريد التحقق منها باستمرار مثل حركة اللاعب. لكن إن أردنا التقاط اﻹجراء لحظة وقوعه، نستطيع استخدام دالة رد النداء ()input_ أو ()unhandled_input_: func _unhandled_input(event): if event.is_action_pressed("shoot"): # ستعمل الشيفرة في اﻹطار بمجرد الضغط على عنصر اﻹدخال بإمكاننا استخدام دوال متعددة للتحقق من حالة اﻹدخال: is_action_pressed: تعيد القيمة true إن كانت حالة اﻹجراء حاليًا pressed is_action_released: تعيد القيمة true إن لم تكن حالة اﻹجراء حاليًا pressed. is_action_just_pressed و is_action_just_released: تعيدان true فقط ضمن إطار واحد عند وقوع الحدث. وهي مفيدة في اﻹجراءات غير المستمرة التي لابد فيها من تحرير الزر ثم ضغطه لتكرار اﻹجراء إضافة إجراءات دخل إلى الشيفرة مباشرة قد نرغب بإضافة إجراءات إدخال لخريطة اﻹدخال أثناء تنفيذ اللعبة أي نريد إضافة إجراء دخل أو أكثر مباشرة إلى السكريبت. سنجد الحل في الصنف InputMap الذي يقدم مجموعة من التوابع لتساعدنا في ذلك. فيما يلي مثال يضيف إجراءً جديدًا باسم attack عند الضغط على مفتاح المسافة Spacebar: func _ready(): InputMap.add_action("attack") var ev = InputEventKey.new() ev.keycode = KEY_SPACE InputMap.action_add_event("attack", ev) وإن أردنا أيضًا إضافة النقر على الزر اﻷيسر للفأرة إلى اﻹجراءات: ev = InputEventMouseButton.new() ev.button_index = MOUSE_BUTTON_LEFT InputMap.action_add_event("attack", ev) ملاحظة: سيرمي التابع ()InputMap.add_action خطأ إن كان اﻹجراء موجودًا مسبقًا، لهذا علينا التحقق من وجوده من خلال التابع ()InputMap.has_action قبل محاولة إضافته. مثال تطبيقي لنفترض أننا أنجزنا شخصية رئيسية في لعبة ونريد إعادة استخدامها في مشروع آخر. في حال قمنا بتخزين المشهد والسكريبت واﻷصول في مجلد واحد فلن نحتاج سوى إلى نسخ هذا المجلد إلى مشروعنا الجديد، مع تعديل خريطة اﻹدخال كي تعمل اﻹجراءات وفق المطلوب. بدلًا من ذلك، نستطيع إضافة الشيفرة التالية إلى سكريبت اللاعب ونتأكد أن المدخلات اللازمة ستُضاف تلقائيًا: var controls = {"walk_right": [KEY_RIGHT, KEY_D], "walk_left": [KEY_LEFT, KEY_A], "jump": [KEY_UP, KEY_W, KEY_SPACE]} func _ready(): add_inputs() func add_inputs(): var ev for action in controls: if not InputMap.has_action(action): InputMap.add_action(action) for key in controls[action]: ev = InputEventKey.new() ev.keycode = key InputMap.action_add_event(action, ev) الخاتمة نأمل أن يكون هذا المقال وضح لكم كيفية استخدام إجراءات الدخل في تطوير الألعاب بجودو وإنشاء إجراءات مخصصة وإضافتها إلى الشيفرة بطريقة ديناميكية، فسواء كنت تطور لعبة بسيطة أو مشروعًا معقدًا، فستحتاج لنظام إجراءات الدخل للتحكم بشخصيات لعبتك بكفاءة وسلاسة. ترجمة -وبتصرف- للمقالين: Input Actions و Adding Input Action in Code اقرأ أيضًا المقال السابق: التحريك باستخدام SpriteSheet و AnimationTree StateMachine في جودو تعرف على مفهوم Delta في تطوير الألعاب تعرف على واجهة محرك الألعاب جودو تعرف على أشهر محركات الألعاب Game Engines
-
هل سبق وشعرتم أنكم تكررون كتابة نفس الشيفرة لتنفيذ مهام روتينية شائعة في مشاريع لارافيل؟ وأن بناء ميزات التطبيق من الصفر تستهلك الكثيرمن الوقت وتبطئ العمل! يمكن حل معظم هذه المشكلات باستخدام حزم لارافيل، وهي وحدات برمجية جاهزة وقابلة للاستخدام المتكرر لتنفيذ المهام والوظائف الشائعة، حيث تسهل تلك الحزم مسار عملنا وتعزز أمان تطبيقاتنا وتحسن وظائفها. نتحدث في هذا المقال عن حزم لارافيل ونستكشف أنواعها ونبحث في الفرق بين الحزم packages والتجميعات bundles، ونعرض قائمة بأفضل الحزم لبناء تطبيقات أسرع. فهرس المقال مفهوم الحزمة package في لارافيل الفرق بين الحزم packages والتجميعات bundles أنواع حزم لارافيل قائمة بأفضل حزم لارافيل حزم تطوير لارافيل حزم أمان لارافيل حزم لوحة تحكم الإدارة في لارافيل حزم لارافيل المخصصة للتجارة الإلكترونية حزم لارافيل لتحسين محركات البحث حزم لارافيل المخصصة لتنقيح الشيفرة حزم اختبار لارافيل مفهوم الحزمة package في لارافيل تُعد الحزمة package بمثابة صندوق أدوات لكل مهام التطوير، وتكون الحزمة على شكل وحدات برمجية مبنية مسبقًا تقدم وظائف مخصصة لتطبيقات لارافيل. توفر الحزم وقت المطور لانها تلغي الحاجة إلى بناء التطبيق من الصفر وتسمح للمطور بالتركيز على الميزات الجوهرية للتطبيق. الفرق بين حزم وتجميعات لارفيل يستخدم كلا المصطلحين حزمة Package وتجميعة Bundle للإشارة إلى نفس التقنية غالبًا على الرغم من وجود اختلاف صغير بينهما: الميزة الحزمة التجميعة المصدر يطورها مجتمع لارافيل أو طرف خارجي يطورها فريق لارافيل الأساسي الحصرية غير موجودة افتراضيًا موجودة عند تثبيت لارافيل أمثلة Debugbar و Socialite Authentication و Caching أنواع حزم لارافيل تأتي حزم لارافيل ضمن فئتين أساسيتين: حزنم مستقلة عن إطار العمل، وحزم مخصصة لإطار عمل الحزم المخصصة لإطار العمل صُمِّمت هذه الحزم خصيصًا لإطار العمل لارفيل. وتعزز هذه الحزم ميزات لارافيل وتقاليد الاستخدام وهندستها لتقديم وظائف مخصصة لتطبيقاتها. من الأمثلة عنها نجد حزم الاستيثاق وموسعّات العمل مع التخزين المؤقت. الحزم المستقلة عن إطار العمل لا ترتبط هذه الحزم بإطار عمل لارافيل ويمكن استخدامها في أي مشروع PHP بصرف النظر عن إطار العمل. تقدم هذه الحزم ميزات لا تتعلق بهندسة لارافيل مما يجعلها متعددة الاستخدامات في مشارع PHP المتنوعة. من الأمثلة عنها نجد مكتبات العمل مع قواعد البيانات وأدوات التحقق من استمارات الويب. قائمة بأفضل حزم لارفيل إليك قائمة بأفضل حزم لارافيل وفقًا لوظائفها المقدمة: الفئة اسم الحزمة الوصف حزم تطوير Laravel Debugbar تضيف أشريط أدوات لأغراض التنقيح. حزم تطوير Laravel User Verification تتولى مهام التحقق من المستخدم وتقييم صلاحية البريد الإلكتروني. حزم تطوير Socialite تمكن من تسجيل الدخول إلى مواقع التواصل الاجتماعي (فيسبوك، غوغل،…). حزم تطوير Laravel Mix أداة تصريف أصول مبنية اعتمادًا على Webpack. حزم تطوير Eloquent-Sluggable توّلد ملاحق عناوين URL محسنة من أجل محركات البحث. حزم تطوير Migrations Generator تولد آليًا ملفات التهجير migration files وفقًا لمخطط قاعدة البيانات. حزم تطوير Laravel Backup تولد نسخًا اختياطية لملفات التطبيق وقاعدة البيانات. حزم تطوير Laravel IDE Helper تعزز تجربة المطوّر من خلال تقديم بيئة تطوير متكاملة IDE. حزم أمان Entrust تقدم أذونات مبنية على أدوار المستخدمين في تطبيقك. حزم أمان No Captcha تقدم خدمة reCaptcha لمنع حالات الوصول غير المرغوبة لوحة تحكم المستخدم Voyager تقدم أداة بناء مرئية للوحة تحكم المدير. لوحة تحكم المستخدم LaraAdmin مولد مفتوح المصدر للوحة تحكم المدير وعمليات قواعد البيانات الأساسية CRUD. لوحة تحكم المستخدم Orchid صندوق أدوات مفتوح المصدر لبناء واجهات الإدارة ولوحة التحكم. تجارة إلكترونية Bagisto حزمة تجارة إلكترونية مفتوحة المصدر لتطبيقات لارافيل. تجارة إلكترونية AvoRed سلة تسوق لارفيل مفتوحة المصدر مع واجهة سهلة الاستخدام للأجهزة المحمولة تحسين محركات بحث Laravel Meta Manager تدير البيانات الوصفية لموقع الويب بغرض تحسين محركات البحث. تحسين محركات بحث SEOTools تحسن ترتيب ظهور الموقع في محركات البحث باعتماد أفضل الخبرات المتوفرة. تحسين محركات بحث Laravel-SEO تضيف وتحذف وتدير البيانات الوصفية لموقع الويب. حزم تنقيح Laravel Telescope تقدم إضاءات على الطلبات والاستثناءات والسجل وغيرها. حزم تطوير لارافيل تبسِّط حزم التطوير مسار العمل بقديمها أدوات تنقيح وتصريف أصول وتوليد شيفرة برمجية. 1. الحزمة Laravel Debugbar تضيف هذه الحزمة الأساسية صندوق أدوات تطوير وتعطينا تلميحات مباشرة عن أداء التطبيق. إذ تعرض الحزمة استعلامات قواعد البيانات والقوالب المصيّرة والمعاملات المُمرَّرة، كما تساعدنا على إضافة رسائل مخصصة لتسهيل التنقيح، وبالتالي زمنًا أكبر في تطوير التطبيق بدلًا من تخمين المشكلات. Debugbar::**info**($object); Debugbar::**error**('Error!'); Debugbar::**warning**('Watch out…'); Debugbar::**addMessage**('Another message', 'mylabel') 2. الحزمة Laravel User Verification تبسّط الحزمة Laravel User Verification تقديم المستخدم إلى الموقع حيث تتحقق من البريد الإلكتروني وصلاحيته. كما تزيد الأداة من مرونة تخصيص قوالب البريد الإلكتروني ومنطق التحقق وتجربة المستخدم كي تتلائم تمامًا مع احتياجات التطبيق، وهي تتكامل بسهولة مع نظام الاستيثاق والتنبيهات في لارافيل مما يساعد على ادخار وقت وجهد المطورين. public function **register**(Request $request) { $this->**validator**($request->**all**())->**validate**(); $user = $this->**create**($request->**all**()); **event**(new **Registered**($user)); $this->**guard**()->**login**($user); UserVerification::**generate**($user); UserVerification::**send**($user, 'My Custom E-mail Subject'); return $this->**registered**($request, $user) ?: **redirect**($this->**redirectPath**()); } 3. الحزمة Socialite طوِّرت حزمة Laravel Socialite من قبل فريق لارافيل لتسهيل عمليات تسجيل الدخول على منصات التواصل الاجتماعي مثل فيسبوك وجوجل وإكس، وهي تتكامل مع نظام الاستيثاق والتنبيهات في لارافيل وتتولى تعقيدات تطبيق بروتوكول OAuth وراء الكواليس دون تدخل منا كي تساعدنا على التركيز على تطوير ميزات التطبيق الأساسية. $user = Socialite::**driver**('github')->**user**(); // OAuth Two Providers $token = $user->token; $refreshToken = $user->refreshToken; // not always provided $expiresIn = $user->expiresIn; // All Providers $user->**getId**(); $user->**getName**(); $user->**getEmail**(); $user->**getAvatar**(); 4. الحزمة Laravel Mix تقدم الحزمة Laravel Mix أداة بسيطة وسهلة الاستخدام لتصريف أصول تطبيق لارافيل. حلّت هذه الحزمة مكان Laravel Elixir وهي تقدم واجهة برمجية واضحة وسهلة الاستخدام لتحديد خطوات بناء التطبيق بطريقة فعالة. تتكامل الأداة بسهول مع أداة التحزيم Webpack مما يحسن ميزة استبدال الوحدات البرمجية أثناء التنفيذ HMR والمزامنة مع المتصفح، مما يساعد في رؤية التغييرات مباشرة دون إعادة تحميل يدوي ويوفر وقت المطورين. mix.**js**('resources/assets/js/app.js', 'public/js') .**sass**('resources/assets/sass/app.scss', 'public/css'); 5. الحزمة Eloquent-Sluggable تقدم Eloquent-Sluggable أداة مفيدة لتوليد عناوين لطيفة slug بعناوين URL وفقًا لخاصيات نموذجنا وبطريقة مؤتمتة، منشئة عناوين تساعد في تحسين محركات البحث في تطبيقك. كما تقدم الحزمة خيارت لتخصيص حقول العناوين الملحقة والفواصل separators وسلوك التحديث. كما توفر أيضًا خطافات لمعالجة الحالات الخاصة وتتكامل مع منطق التطبيق للتأكد من تحسين عناوين URL. class Post extends Eloquent { use Sluggable; protected $fillable = ['title']; public function **sluggable**() { return [ 'slug' => [ 'source' => ['title'] ] ]; } } $post = new **Post**([ 'title' => 'My Awesome Blog Post', ]); // $post->slug is "my-awesome-blog-post 6. الحزمة Migrations Generator تحلل الحزمة Laravel Migrations Generator تخطيط قاعدة البيانات وتولد ملفات التهجير migration files تلقائيًا، مما يوفر علينا الوقت ويساعدنا على التركيز على منطق تطوير التطبيق وليس كتابة تلك الملفات. ويمكننا بكل بساطة تنفيذ أمر التهجير لكل جداول قاعدة البيانات معًا وترك الأمر لهذه الحزمة. php artisan migrate:generate //بالإمكان اختيار جداول معينة فقط php artisan migrate:generate table1,table2 7. الحزمة Laravel Backup تساعدنا الحزمة Laravel Backup في إنشاء نسخ احتياطية عن ملفاتنا عن طريق ضغط المجلدات وقاعدة البيانات في ملف snapshot واحد لضمان حماية وتأمين المشروع، وذلك من خلال تنفيذ أمر واحد. php artisan backup:run 8. الحزمة Laravel IDE Helper تحسن هذه الأداة الأساسية IDE Helper تجربتنا مع محررات الأكواد وبيئات التطوير مثل PhpStorm و VS Code من خلال ميزات عديدة منها الإكمال التلقائي للشيفرة، والتلميح بالنوع خصيصًا لمكونات لارافيل، فلن نضطر مع هذه الأداة إلى البحث عن التعليمات بل سنعرض تلميحات وتوجيهات مباشرة عن التوابع والمعاملات المتاحة. كما تولد الأداة تعليقات توثيقية لتسهّل علينا التنقل ضمن واجهة لارافيل البرمجية ضمن بيئة التطوير التي تفضلها. حزم أمان لارافيل تساعد حزم الأمان في دعم أمان التطبيق من خلال ميزات مثل الاستيثاق والأذونات المبنية على أدوار محددة والتكامل مع خدمة reCAPTCHA. 9. الحزمة Entrust تساعدنا Entrust في تحديد دور كل مستخدم وتسند إليه أذونات أو سماحيات محددة، وبالتالي سيتمكن المستخدم من الوصول إلى وظائف محددة مسبقًا فقط مما يحسن أمان التطبيق. تنشئ هذه الحزمة الجداول الأربعة التالية لأدوار المستخدمين: جدول أدوار لتخزين سجلات الأدوار جدول الأذونات لتخزين سجلت السماحيات جدول أدوار-مستخدم role-user لتخزين علاقات واحد-إلى-أكثر بين الأدوار والمستخدمين جدول أذونات-دور permission_role لتخزين علاقات واحد-إلى-أكثر بين الأدوار والأذونات بإمكاننا إنشاء دور بتنفيذ أسطر الشيفرة التالية: admin = new **Role**(); $admin->name = 'admin'; $admin->display_name = 'User Administrator'; // optional $admin->description = 'User is allowed to manage and edit other users'; // optional $admin->**save**() نسند الدور إلى المستخدم كالتالي: user = User::**where**('username', '=', 'michele')->**first**(); $user->**attachRole**($admin); ثم علينا تحديد الأذونات الخاصة بالدور: $createPost = new **Permission**(); $createPost->name = 'create-post'; $createPost->display_name = 'Create Posts'; $createPost->description = 'create new blog posts'; $createPost->**save**(); $admin->**attachPermission**($createPost); 10. الحزمة No Captcha تعمل الحزمة No Captcha على تحقيق تكامل لتطبيق لارافيل مع خدمة reCAPTCHA من جوجل لحمايته من الروبوتات الآلية bots وتضيف طبقة أمان إضافية إليه. لهذا، ننصح بالحصول على مفتاح الواجهة البرمجية API المجاني لهذه الحزمة وحماية التطبيقات. تقدم الحزمة أيضًا خيارات مخصصة لعنصر التحكم CAPTCHA كي نضمن اندماجه مع تصميم الاستمارات Forms بكل بساطة ودون التأثير على تجربة المستخدم. NoCaptcha::**shouldReceive**('verifyResponse') ->**once**() ->**andReturn**(true); $response = $this->**json**('POST', '/register', [ 'g-recaptcha-response' => '1', 'name' => 'Pardeep', 'email' => 'pardeep@example.com', 'password' => '123456', 'password_confirmation' => '123456', ]); حزم لوحة تحكم إدارة لارافيل تساعد حزم إدارة لوحة التحكم على بناء واجهات سهلة الاستخدام للوحات التحكم لمدير الواجهة الخلفية للتطبيقات 11. Voyager قد يكون بناء لوحة تحكم مدير واضحة وسهلة أمرًا يستهلك الكثير من وقت المطور، لكن الحزمة Voyager ستبسط العملية من خلال واجهة سهلة الاستخدام. تتميز الحزمة بتوثيقها الجيد وتتضمن واجهة واضحة وسهلة الاستخدام، وتوفر بيانات مخصصة للاختبارات ومدير وسائط متعددة متقدم. وتساعدنا في التركيز على بناء وظائف تطبيقات لارافيل لتتكفل هي بإنشاء لوحات التحكم. 12. الحزمة LaraAdmin تُعد LaraAdmin حزمة مجانية قوية ومفتوحة المصدرية لتطوير لارافيل، حيث تسهِّل إنشاء لوحة تحكم إدارة التطبيق بتقديمها ميزات مثل إدارة المستخدمين، والوصول المشروط بالدور الممنوح للمستخدم، والقوائم الديناميكية. تقدم الأداة أيضًا جداول بيانات سهلة التخصيص تدعم الفرز والترشيح وتعدد الصفحات مما يسمح بإدارة البيانات المعقدة بكل سهولة. لهذا ستساعدنا هذه الأداة في التركيز على البناء الوظيفي لتطبيقات لارافيل وتتكفل هي بإنشاء واجهات الإدارة. 13. الحزمة Orchid تُقدم الحزمة Orchid صندوق أدوات يتمتع بتصميم مرن وقابل للتوسع، مما يسمح ببناء واجهات سهلة الاستخدام مفصلة خصيصًا لتلائم تطبيقاتنا. وتتجاوز الأداة إنشاء لوحات تحكم بسيطة وتتصرف كمنظومة لإدارة تطبيقات الويب. إذ يمكن التفكير بها على أنها منظومة إدارة محتوى تسهل إدارة المحتوى والمستخدمين في تطبيقات لارافيل. حزم لارافيل المخصصة للتجارة الإلكترونية تساعدنا هذه الحزم في تسريع تطوير المتاجر الإلكترونية من خلال ميزاتها المتعددة مثل إدارة المنتجات وعربات التسوق وبوابات الدفع الإلكتروني. 14. الحزمة Bagisto تُعد Bagisto حزمة مفتوحة المصدر مخصصة لمتاجر لارافيل الإلكترونية التي تلفت انتباه المطورين بسرعة. إذ تقدم نظام إدارة مستخدمين سهل مع خيارات متعددة لإدارة المخازن وغيرها من الميزات. كما تُجمّع الحزمة Laravel CMS مع أدوات تسهل التنقل بين الأقسام المختلفة ضمن لوحة التحكم، وتقدم وظائف كثيرة للمتجر مثل تعدد العملات، وتخصيص الموقع الجغرافي للمتجر، وتحديد مستويات للوصول إلى البيانات، والتكامل مع بوابات الدفع الإلكتروني وغيرها من الميزات. 15. الحزمة AvoRed تسمح الحزمة AvoRed بتخصيص حزمة تسوق لارافيل مفتوحة المصدر بسهولة وحسب الحاجة، وتقدم افتراضيًا واجهة سهلة الاستخدام في الأجهزة المحمولة وتضم أفضل حزم تحسين محركات البحث في لارافيل. تسمح لك الأداة ببناء كيانات مفيدة مثل الفئات categories والسمات attributes وغيرها، وتقدم إمكانيات فعالة في إدارة طلبات الشراء وتتبّعها وإدارة معلومات العملاء والمتاجر وغيرها. حزم لارافيل لتحسين محركات البحث تساعد هذه الحزم في تحسين تطبيقات لارافيل كي يسهل إيجادها من قبل محركات البحث وذلك من خلال إدارة بيانات الوسوم الوصفية للموقع أو التطبيق وإدارة خارطة الموقع والبيانات المهيكلة. 16. الحزمة Laravel Meta Manager تساعد الأداة Laravel Meta Manager في تحسين ظهور صفحاتنا في محركات البحث ورفع ترتيبها ضمن صفحات نتائج البحث. حيث توضح بيانات الوسوم Meta tag وتسمح بتصحيحها باتباع أفضل الممارسات المتبعة في تحسين محركات البحث. تأتي الحزمة مع مجموعة وسوم ميتا جاهزة وموصى بها تتضمن Standard SEO و Dublin Core و Google Plus و Facebook Open Graph وغيرها. فبعد إتمام الإعداد، كل ما علينا هو إضافة هذه الوسوم المتولّدة إلى ترويسة الصفحة التي تريد كالتالي: @**include**('meta::manager') يستخدم الأمر السابق الإعدادات المعرفة سابقًا لإعادة تعيين الوسوم الوصفية، لكن إن أدرنا تعريف خيارات محددة بسرعة دون تدقيق، نستطيع استخدام الشيفرة التالية: @**include**('meta::manager', [ 'title' => 'My Example Title', 'description' => 'This is my example description', 'image' => 'Url to the image', ]) وإليكم مثالًا: <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> @**include**('meta::manager', [ 'title' => 'My Example Title', 'description' => 'This is my example description', 'image' => '', ]) </head> <body> </body> </html> 17. الحزمة SEOTools تسمح SEOTools بتحسين محركات البحث في تطبيقات لارافيل وفقًا لأفضل الممارسات، وتقدم ميزات تحسين ممتازة لمواقع الويب. ومن السهل دمج الحزمة مع المشاريع ولها واجهة سهلة الاستخدام بالنسبة للمبتدئين، كما تسمح بضبط العناوين والوسوم الوصفية لكل من منصة إكس و Open Graph. 18. الحزمة Laravel-SEO تساعدنا الحزمة Laravel-SEO في التحكم الكامل بقدرة تطبيقنا على تحسين محركات البحث وتبسّط إدارة الوسوم الوصفية من ناحية الإضافة والتحرير والحذف. وتسمح أيضًا بإضافة بيانات مهيكلة لتحسين نتائج محركات البحث وإدارة الوسوم الوصفية للمنصات الشهيرة مثل Open Graph و Dublin Core دون أي جهد، مما يزيد ظهور تطبيق لارافيل ويعزز ترتيبه في نتائج البحث. حزم تنقيح لارافيل تركز هذه الحزم على سلوك التطبيق من خلال أدوات التنقيح وتحليل الأداء. 19. الحزمة Laravel Telescope تُعد الحزمة Laravel Telescope أداة فعالة في يد مطوري لارافيل، إذ تكشف لنا كل ما يحدث خلف الستار، وتتعقب الطلبات القادمة وتنفيذها والسجلات واستعلامات قواعد البيانات وغيرها. وتقدم الأداة ملاحظات مباشرة على سلوك التطبيق أثناء التنفيذ، مما يسمح بتحديد وإصلاح الثغرات بفعالية. تجعل هذه الميزات من Laravel Telescope أداة أساسية لأي مطور لارافيل يعمل في بيئة تطور محلية. حزم اختبار لارافيل تساعدك هذه الحزم على تحسين نوعية الشيفرة وإمكانية صيانتها من خلال إعداد اختبارات وحدات واختبارات تكامل فعالة. 20. الحزمة Orchestral Testbench لن نتمكن من الوصول إلى جميع مساعدي اختبار لارافيل test helpers عندما نكتب الحزمة بنفسنا. فإن أردنا كتابة اختبارات لحزمتنا كما لو كانت ضمن تطبيق لارافيل نمطي ننصح باستخدام Orchestral Testbench package كالتالي: ضبط ملف Composer.json للحزمة الجديدة إضافة مزود خدمة ضبط الأسماء المستعارة Alias إنشاء صنف واجهة facade class هذه الخطوات الأربعة مهمة جدًا في كتابة حزمة لارافيل لتسريع الإنتاجية. الخلاصة قدمنا في هذا المقال قائمة بأفضل حزم لارافيل التي تساعدنا على تحسين إنتاجية تطبيقاتنا. ويعتمد اختيار أفضل حزم لارافيل على متطلبات المشروع بالدرجة الأولى. وطالما أن لارافيل تسهل على المطور تنفيذ عمليات مخصصة يحددونها بأنفسهم، سيكون استخدام تلك الحزم مساعدًا في تنفيذ تلك المهام الوظيفية بفعالية أكبر. أسئلة شائعة 1. ما هي حزم لارافيل؟ الحزم في PHP هي مجموعة من الوجهات routes والمتحكمات controllers والعروض views المهيأة لتوسيع وظائف تطبيق لارافيل. 2. ماهي التجميعة في لارافيل؟ هي تقنية قّدمت أول مرة في لارافيل 3.0، وهي طريقة لتجميع الشيفرة ضمن مكونات يمكن إضافتها إلى تطبيقات لارافيل. ولن يضطر المطور من إنشاء تلك المكونات من الصفر دائمًا عند استخدام التجميعات. ترجمة -وبتصرف- للمقال: Best laravel packages to optimize performance, security and SEO in 2024 اقرأ أيضًا استخدام عمليات CRUD لإنشاء مدونة بسيطة في لارافيل تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder تعرف على مفهوم إطار العمل Framework وأهميته في البرمجة تعرف على لغة PHP
-
نشرح في هذا المقال كيفية استخدام جدول SpriteSheet لتنظيم حركة الشخصية في الألعاب الثنائية الأبعاد ضمن محرك الألعاب جودو، كما نوضح دور المتحكم AnimationTreeState Machine في تنظيم حركة الشخصية والتحكم في عمليات التنقل بين حالات الحركة المتختلفة. الحركات داخل جدول الشخصيات SpriteSheet تُعد جداول الشخصيات Spritesheets من الطرق الشائعة لتوزيع الحركات للرسوم المتحركة ثنائية البعد، إذ توضع جميع إطارات الشخصية ضمن صورة واحدة. سنستخدم في مقالنا شخصية المغامر، ويمكن الحصول عليها وعلى غيرها من الشخصيات من متجر Elthen's Pixel Art Shop الذي يوفر ملحقات مفيدة يمكن استخدامها في تطوير الألعاب. تحذير: علينا التأكد من أن الصور في جدول الشخصيات مرتبة ضمن شبكة ذات حجم ثابت، مما يسمح لمحرك جودو باقتطاعها تلقائيًا، بينما إن وضعنا الشخصيات ضمن الجدول بشكل غير منتظم، فلن نتمكن من استخدام التقنيات التي نشرحها تاليًا. إعداد عقد التحريك تستخدم تقنية التحريك العقدة Sprite2D لعرض الخامة، بعدها نحرّك الإطارات المتغيرة باستخدام العقدة AnimationPlayer. يمكن لهذا الترتيب أن يعمل على أي عقدة ثنائية البعد، لكننا سنستخدم في مثالنا العقدة CharacterBody2D. لنضف العقد التالية إلى المشهد: CharacterBody2D: Player Sprite2D CollisionShape2D AnimationPlayer نسند جدول الشخصيات إلى الخاصية Texture للعقدة Sprite2D وسنلاحظ أن الجدول بكامله قد ظهر في نافذة العرض. ولكي نقطعه إلى إطارات فردية، نوسّع قسم التحريك Animation في نافذة الفاحص وناضبط قيم الخاصيتين Hframes و Vframes على 13 و 8 على الترتيب، وهما يمثلان عدد الإطارات الأفقية والعمودية في جدول الشخصيات: لنجرّب تغيير الخاصية Frame لمراقبة تغيّر الصورة، فهي الخاصية التي سنعمل على تحريكها. تحريك الشخصية سنختار العقدة AnimationPlayer ثم ننقر الزر تحريك Animation يتبعه الزر جديد New ونسمي الحركة الجديدة idle. نضبط بعد ذلك مدة الحركة على 2 ثانية وننقر الزر Loop كي تتكرر الحركة باستمرار. نجعل شريط التقدم Scrubber عند الزمن 0 ثم نختار العقدة Sprite2D. نضبط الخاصية Animation>Frame على 0 ثم ننقر على أيقونة المفتاح إلى جوار القيمة. إن حاولنا الآن تشغيل الحركة فلن نرى شيئًا، لأن الإطار الأخير رقم 12 يبدو مشابهًا للإطار الأول رقم 0. مع ذلك لم نتمكن من رؤية الإطارات بينهما. لإصلاح الأمر نغير الخاصية Update Mode للمسار من القيمة الافتراضية Discrete إلى Continuous وسنجد هذا الزر في نهاية المسار من الجانب الأيمن. نلاحظ أن هذا الحل سيعمل فقط مع جداول الشخصيات، حيث تكون الشخصيات مرتبة مسبقًا، فإن لم يكن الأمر كذلك، علينا ترتيب كل إطار على حدى ضمن المسار. يمكن تجربة وضع حركات أخرى مثل حركة القفز التي نجد صورها في الإطارات من 65 إلى 70. استخدام المتحكم AnimationTreeStateMachine لنتخيل أن لدينا كم كبير من الحركات، وأصبح من الصعب علينا التحكم عملية التنقل فيما بينها، وامتلأ السكريبت بعبارات if وكلما أردنا تصحيح شيء أخفق ما تبقى. لحل الأمر نستخدم العقدة AnimationTree لإنشاء مُتحكّم يسمح لنا بترتيب الحركات المختلفة للشخصية وإدارة عملية التنقل فيما بينها. سنستخدم في مثالنا نفس شخصية المغامر التي استخدمناها في المثال السابق، ونفترض أننا هيأنا مسبقًا حركات الشخصية باستخدام العقدة AnimationPlayer. وعندما نستخدم جدول الشخصيات السابق سنجد صورًا توافق الحركات التالية: سكون idle ركض run هجوم attack1 هجوم attack2 إصابة hurt موت die استخدام شجرة الرسوميات AnimationTree نضيف العقدة AnimationTree إلى المشهد ثم نختار New AnimationNodeStateMachine من الخاصية TreeRoot. تتحكم العقدة AnimationTree بالرسوميات التي تنشأ ضمن العقدة AnimationPlayer، ولكي نسمح لها بالوصول إلى الرسوميات الموجودة، ننقر على الخاصية Assign ضمن الخاصية Anim Player ثم نختار عقدة الحركة. يمكننا الآن إعداد متحكم التنقل ضمن نافذة AnimationTree: ننتبه إلى التحذير الظاهر، ونضبط الخاصية Active في نافذة الفاحص على القيمة On ثم ننقر بعد ذلك بالزر اليميني للفأرة ونختار Add Animation. نختار بعد ذلك الحركة idle وسنرى صندوقًا صغيرًا يمثل هذه الحركة. نكرر نفس العملية لإضافة مثل هذه الصناديق إلى بقية الحركات. سنتمكن الآن من إضافة الاتصالات، لهذا ننقر على زر Connect nodes ثم نتنقل بالسحب بن العقد لوصلها مع بعضها. وكمثال على الاتصال سنستخدم الرسوم المتحركة لحالتي الهجوم: عندما تختار حركة، ستتبع الشجرة المسار الذي يصل العقدة الحالية إلى الوجهة. لكن في طريقة إعداد المثال السابق، لن نرى الهجوم الأول attack1 إن شغلنا الهجوم الثاني attack2. يعود السبب في ذلك إلى أن نمط التبديل switch mode للاتصال نوعه مباشر immediate. لهذا، ننقر على زر Move/select ثم ننقر على الاتصال بين attack1 و attack2 ثم نغير من نافذة الفاحص الخاصية Switch Mode إلى At End ونكرر ذلك على الاتصال بين attack2 و idle. ما يحدث الآن أنه عند تشغيل في AnimationTree، أنه عند الانتقال من idle إلى attack2، يجري تشغيل الحركتين attack1 و attack2 على التتابع، ولكن بعد ذلك تتوقف الرسوم المتحركة عند attack2 بدلاً من العودة تلقائيًا إلى idle. لحل هذه المشكلة، نضبط الخاصية Advance>Mode على Auto مما يسمح للشجرة بالعودة إلى الحركة idle بشكل تلقائي بعد تنفيذ حركتي الهجوم attack، ونلاحظ أن أيقونة الاتصال تتحول إلى اللون الأخضر لإظهار ذلك. وهكذا ستُتنفذ الحركات على التتابع بمجرد تفعيلها. استدعاء الحالات في الشيفرة فيما يلي شجرة الحركات بأكملها: لنهيئ الآن الشخصية كي تستخدم هذه الحركات: extends CharacterBody2D var state_machine var run_speed = 80.0 var attacks = ["attack1", "attack2"] @onready var state_machine = $AnimationTree["parameters/playback"] تضم الخاصية state_machine مرجعًا إلى المتحكم بالحالة وهو AnimationNodeStateMachinePlayback، ولاستدعاء حركة محددة، نستخدم التابع travel الذي سيتّبع الاتصالات إلى الرسم المتحرك المحدد: func hurt(): state_machine.travel("hurt") func die(): state_machine.travel("die") set_physics_process(false) لدينا هنا مثال عن الدوال التي قد نستدعيها، إن أصيب اللاعب أو قتل. وبالنسبة إلى بقية الحالات كالركض والهجوم وغيرها فلا بد من جمعها مع شيفرة الحركة وشيفرة معالجة المدخلات. وستحدد الخاصية velocity إن كنا سنرى حالة حركة الركض run أو حركة السكون idle: func get_input(): var current = state_machine.get_current_node() velocity = Input.get_vector("move_left", "move_right", "move_up", "move_down") * run_speed if Input.is_action_just_pressed("attack"): state_machine.travel(attacks.pick_random()) return # اقلب الشخصية من اليمين إلى اليسار if velocity.x != 0: $Sprite2D.scale.x = sign(velocity.x) # اختر رسمًا متحركًا if velocity.length() > 0: state_machine.travel("run") else: state_machine.travel("idle") move_and_slide() نلاحظ استخدام return بعد الانتقال إلى حركة الهجوم كي نتمكن من الانتقال إلى حالات الحركة أو السكون لاحقًا في الدالة. الخاتمة تعرفنا في هذا المقال على طريقة استخدام SpriteSheet في جودو لتوليد حركات مختلفة للشخصية، كما تعرفنا على استخدام AnimationTree Animation Tree State Machine في إدارة التنقل بين الرسوميات المختلفة للشخصية. وبإمكانك من الاطلاع على المشروع بصيغته المكتملة لتنفيذه وفهمه بصورة أفضل. ترجمة -وبتصرف- للمقالين: SpriteSheet ANimation و Using AnimationTreeStateMachine اقرأ أيضًا الطريقة الصحيحة للتواصل بين العقد في جودو Godot إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو تحريك الشخصية في لعبة 3D باستخدام محرر التحريك في جودو كتابة سكربتات GDScript وإرفاقها بالعقد في جودو
-
يأتي مصطلح SOLID من الأحرف الأولى للمبادئ الخمسة للتصميم الكائني التوجه Object Oriented Design -أو OOD اختصارًا- والتي وضعها روبرت سي مارتن حيث تؤسس هذه المبادئ لممارسات تطبيقية أثناء تطوير البرمجيات مع الأخذ بعين الاعتبار سهولة الصيانة وقابلية التوسع مع نمو المشروع. ويساعد تبني هذه الممارسات على تفادي مشكلات كتابة الشيفرة وتحسين إعادة إنتاجها وتطوير برمجيات تعتمد نهج Agile أو Adaptive. مبادئ SOLID يرتكز مفهوم على المبادئ الخمس التالية: مبدأ المسؤولية المفردة Single-responsibility Principle الحرف S. مبدأ المفتوح والمغلق Open-closed Principle الحرف O. مبدأ استبدال ليسكوف Liskov Substitution Principle الحرف L. مبدأ عزل الواجهة Interface Segregation Principle الحرف I. مبدأ الاعتماديات المتبادلة Dependency Inversion Principle الحرف D. سنوضح في الفقرات التالية كل مبدأ من هذه المبادئ الخمسة على حدة، ونوضح فائدة SOLID في تطوير منتجات برمجية أفضل. ملاحظة: يمكن تطبيق هذه المبادئ على مختلف لغات البرمجة، لكن شيفرة الأمثلة في هذا المقال مكتوبة بلغة PHP. مبدأ المسؤولية المفردة ينص مبدأ المسؤولية المفردة SRP على مايلي: لنتأمل على سبيل المثال تطبيقًا يتلقى مجموعة من الأشكال هي عبارة عن دوائر ومربعات ويحسب مجموع مساحات الأشكال في هذه المجموعة. لتنفيذ المطلوب، ننشئ أصنافًا للأشكال ونضبط المعاملات المطلوبة لحساب المساحة عبر الدوال البانية constructor funcations. سنحتاج هنا إلى المتغير length لتخزين طول ضلع المربع class Square { public $length; public function construct($length) { $this->length = $length; } } كما سنحتاج إلى المتغير radius لتحديد نصف قطر الدائرة: class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } } ننشئ تاليًا الصنف AreaCalculator ونكتب منطق جمع مساحات جميع الأشكال الموجودة، فمساحة المربع هي مربع طول الضلع ومساحة الدائرة هي مربع نصف القطر مضروبًا بالثابت Pi: class AreaCalculator { protected $shapes; public function __construct($shapes = []) { $this->shapes = $shapes; } public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } elseif (is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } public function output() { return implode('', [ '', 'Sum of the areas of provided shapes: ', $this->sum(), '', ]); } } ولاستخدام الصنف AreaCalculator لا بد من إنشاء نسخة عنه نمرر لها مصفوفة من الأشكال ومن ثم نعرض نتيجة حساب مجموع المساحات. إليك مثالًا عن مجموعة من ثلاث أشكال: دائرة نصف قطرها 2 مربع طول ضلعه 5 مربع طول ضلعه 6 $shapes = [ new Circle(2), new Square(5), new Square(6), ]; $areas = new AreaCalculator($shapes); echo $areas->output(); إن المشكلة في تابع الخرج هي أن الصنف AreaCalculator يعالج منطق إخراج البيانات. فلو تأملنا حالة يطلب فيها تحويل الخرج إلى صيغة أخرى مثل صيغة JSON، سيعالج الصنف AreaCalculator في هذه الحالة منطق تحويل الخرج إلى الصيغة المطلوبة وهذا خرق لمبدأ المسؤولية المفردة. لهذا ينبغي أن يكون الصنف AreaCalculator مسؤولًا فقط عن حساب مجموع مساحات الأشكال ولا يهتم بطريقة إخراج النتائج أيًا كانت. ولحل المشكلة، بإمكاننا إنشاء صنف آخر SumCalculatorOutputter واستخدامه لمعالجة منطق الخرج: class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = [ 'sum' => $this->calculator->sum(), ]; return json_encode($data); } public function HTML() { return implode('', [ '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '', ]); } } نستخدم الصنف SumCalculatorOutputter كالتالي: $shapes = [ new Circle(2), new Square(5), new Square(6), ]; $areas = new AreaCalculator($shapes); $output = new SumCalculatorOutputter($areas); echo $output->JSON(); echo $output->HTML(); في هذه الحالة يتولى الصنف SumCalculatorOutputter عملية إخراج البيانات إلى المستخدم ويحقق مبدأ المسؤولية المفردة. مبدأ المفتوح والمغلق ينص المبدأ على ما يلي: ويعني ذلك أن الصنف ينبغي أن يكون قادرًا على التوسع دون الحاجة لتعديل أي شيء موجود فيه. لنعد مجددًا إلى الصنف AreaCalculator ونركز هذه المرة على التابع sum: class AreaCalculator { protected $shapes; public function __construct($shapes = []) { $this->shapes = $shapes; } public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'Square')) { $area[] = pow($shape->length, 2); } elseif (is_a($shape, 'Circle')) { $area[] = pi() * pow($shape->radius, 2); } } return array_sum($area); } } لنتأمل الآن حالة يرغب فيها المستخدم بجمع مساحات أشكال أخرى مثل المثلثات والمخمسات والمسدسات. سيكون علينا في هذا الحالة إضافة شروط جديدة في كتلة if/else، وهذا سيخرق مبدأ المفتوح والمغلق. سيكون أحد الحلول التي تحسن التابع sum هو إزالة منطق حساب مساحة كل شكل وتنفيذ العملية في كل صنف على حدة. نضيف هنا التابع area للصنف square: class Square { public $length; public function __construct($length) { $this->length = $length; } public function area() { return pow($this->length, 2); } } نضيف هنا التابع area للصنف Circle: class Circle { public $radius; public function construct($radius) { $this->radius = $radius; } public function area() { return pi() * pow($shape->radius, 2); } } ويمكن الآن إعادة كتابة التابع sum للصنف AreaCalculator كالتالي: class AreaCalculator { // ... public function sum() { foreach ($this->shapes as $shape) { $area[] = $shape->area(); } return array_sum($area); } } في هذه الحالة بإمكاننا إنشاء أصناف جديدة لأشكال جديدة ثم تمريرها إلى صنف حساب مجموع المساحات دون أن نغير الشيفرة. مع ذلك، ستظهر مشكلة جديدة تتمثل في عدم القدرة على تمييز إن كان الكائن المرر إلى الصنف AreaCalculator هو شكل أو يمتلك تابعًا اسمه area. ولأن بناء واجهة Interface هو جزء أساسي من SOLID، لننشئ واجهة ShapeInterface تملك التابع area: interface ShapeInterface { public function area(); } نعدّل أصناف الأشكال لتتخذ من ShapeInterface واجهة لها، ونبدأ بالصنف Square: class Square implements ShapeInterface { // ... } ثم الدائرة Circle: class Circle implements ShapeInterface { // ... } نتحقق بعد ذلك أثناء تنفيذ التابع sum إن كان للكائن الممرر واجهة شكل، أي أنه نسخة عن ShapeInterface ويرمي استثناء Exceptionخلاف ذلك: class AreaCalculator { // ... public function sum() { foreach ($this->shapes as $shape) { if (is_a($shape, 'ShapeInterface')) { $area[] = $shape->area(); continue; } throw new AreaCalculatorInvalidShapeException(); } return array_sum($area); } } وهكذا نكون قد حققنا مبدأ المفتوح والمغلق. مبدأ استبدال ليسكوف ينص المبدأ على ما يلي: ويعني ذلك أن أي صنف فرعي subclass أو مشتق قابل للاستبدال بواسطة الصنف الأب. لنعد إلى مثالنا السابق ولنفرض وجود صنف جديد VolumeCalculator يوسّع الصنف AreaCalculator: class VolumeCalculator extends AreaCalculator { public function construct($shapes = []) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return an array of output return [$summedData]; } } نتذكر أن الصنف SumCalculatorOutputter هو كالتالي: class SumCalculatorOutputter { protected $calculator; public function __constructor(AreaCalculator $calculator) { $this->calculator = $calculator; } public function JSON() { $data = array( 'sum' => $this->calculator->sum(); ); return json_encode($data); } public function HTML() { return implode('', array( '', 'Sum of the areas of provided shapes: ', $this->calculator->sum(), '' )); } } فلو حاولنا تنفيذ المثال كالتالي: $areas = new AreaCalculator($shapes); $volumes = new VolumeCalculator($solidShapes); $outputArea = new SumCalculatorOutputter($areas); $outputVolume = new SumCalculatorOutputter($volumes); عندما نستدعي التابع ()HTML للكائن outputVoulme$ سنحصل على خطأ E_NOTICE يخبرنا بوجود عملية تحويل من مصفوفة إلى نص. ولحل المشكلة نعيد المتغير summedData$ في الصنف VolumeCalculator بدلًا من إعادة مصفوفة: class VolumeCalculator extends AreaCalculator { public function construct($shapes = []) { parent::construct($shapes); } public function sum() { // logic to calculate the volumes and then return a value of output return $summedData; } } قد يكون المتغير summedData$ من النوع float أو double أو integer وهذا سيحقق مبدأ استبدال ليسكوف. مبدأ فصل الواجهة ينص المبدأ على ما يلي: نوضح هذا المبدأ بمتابعة العمل على مثالنا السابق وننطلق من الواجهة ShapeInterface. سنحتاج الآن إلى دعم ثلاث أشكال ثلاثية الأبعاد هي Cuboid و Spheroid، ونريد حساب حجومها volume. لنتأمل ما قد يحدث إن عدّلنا الواجهة ShapeInterface لإضافة تابع جديد: interface ShapeInterface { public function area(); public function volume(); } في هذه الحالة سيكون كل شكل مجبرًا على تبني تابع الحجم volume، لكن كما نعلم لا أحجام للأشكال ثنائية البعد مثل الدائرة وهذه الواجهة ستجبر الصنف Circle على تنفيذ هذا التابع الذي لا يحتاجه. يُعد هذا الأمر خرقًا لمبدأ عزل الواجهة، وبدلًا من ذلك، بإمكانك إنشاء واجهة جديدة تُدعى ThreeDimensionalShapeInterface تقدم التابع volume وتتبناها الأشكال ثلاثية الأبعاد: interface ShapeInterface { public function area(); } interface ThreeDimensionalShapeInterface { public function volume(); } class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } } هذه النهج أفضل بكثير، لكن الأمر سيبدو مربكًا عند توضيح نوع استخدام الواجهة بالتعليقات مثلًا. لهذا، بدلًا من استخدام الواجهتين السابقتين بإمكاننا إنشاء واجهة أخرى مثل ManageShapeInterface وتطبيقها على الأشكال ثنائية وثلاثية البعد. وهكذا سيكون لدينا واجهة برمجية واحدة تدير الأشكال: interface ManageShapeInterface { public function calculate(); } class Square implements ShapeInterface, ManageShapeInterface { public function area() { // calculate the area of the square } public function calculate() { return $this->area(); } } class Cuboid implements ShapeInterface, ThreeDimensionalShapeInterface, ManageShapeInterface { public function area() { // calculate the surface area of the cuboid } public function volume() { // calculate the volume of the cuboid } public function calculate() { return $this->area(); } } في هذا النهج، بإمكاننا استبدال التابع area في الصنف AreaCalculatot بالتابع calculate، ونتحقق أيضًا أن الكائن هو نسخة عن الصنف ManageShapeInterface وليس ShapeInterface وهكذا يكون مبدأ فصل الواجهة قد تحقق لدينا. مبدأ الاعتماديات المتبادلة ينص هذا المبدأ على ما يلي: يسمح هذا المبدأ بفصل الشيفرة. ولإيضاح المبدأ لنأخذ مثالًا عن صنف PasswordReminder يربط الشيفرة بقاعدة البيانات MySQL: class MySQLConnection { public function connect() { // handle the database connection return 'Database connection'; } } class PasswordReminder { private $dbConnection; public function __construct(MySQLConnection $dbConnection) { $this->dbConnection = $dbConnection; } } يمثل الصنف MySQLConnection الوحدة البرمجية ذات المستوى الأدنى بينما يمثل الصنف PasswordReminder الوحدة ذات المستوى الأعلى لاعتماد الثاني على الأول. لكن ووفقًا لتعريف المبدأ الأخير الذي ينص على ضرورة الاعتماد على التجريد وليس طريقة التنفيذ، يخرق النهج السابق هذه القاعدة لأن الصنف PasswordReminder مجبر على الاعتماد على الصنف MySQLConnection. وإن حاولت مسبقًا تغيير قاعدة البيانات عليك تعديل الصنف PasswordReminder وفي هذا خرق للمبدأ O أي مبدأ المفتوح والمغلق. لا ينبغي أن يهتم الصنف PasswordReminder بنوع قاعدة البيانات المستخدمة، ولتلافي ذلك، بإمكانك بناء واجهة طالما أن المبدأ الأخير ينص على ضرورة اعتماد وحدات المستوى الأعلى والأدنى على التجريد: interface DBConnectionInterface { public function connect(); } تقدم الواجهة السابقة طريقة للاتصال بقاعدة البيانات ويتبناها الصنف MySQLConnection. وبدلًا من الإشارة إلى الصنف MySQLConnection ضمن بانية PasswordReminder بإمكانك الإشارة إلى DBConnectionInterface. وأيًا كان نوع قاعدة البيانات التي تستخدمها، سيتمكن الصنف PasswordReminder من الاتصال بها دون مشاكل، ولن تخرق القاعدة الثانية (المفتوح والمغلق). class MySQLConnection implements DBConnectionInterface { public function connect() { // handle the database connection return 'Database connection'; } } class PasswordReminder { private $dbConnection; public function __construct(DBConnectionInterface $dbConnection) { $this->dbConnection = $dbConnection; } } وهكذا ستعتمد وحدات المستوى الأعلى والأدنى على التجريد. الخلاصة قدمنا في هذا المقال المبادئ الأساسية الخمسة SOLID في كتابة الشيفرة البرمجية، ومن المفيد اتباعها في المشاريع البرمجية لجعلها تتمتع بإمكانيات المشاركة بين أطراف العمل بسهولة، والتعديل والاختبار وإعادة الإنتاج بأدنى مقدار من التعقيدات. ترجمة-وبتصرف- للمقال: SOLID: the first 5 principles of object oriented design لمؤلفيه Samuel Oloruntoba و Anish Singh Walia اقرأ أيضًا كيف تكتب كود برمجي مثل مهندسي البرمجيات قواعد البرمجة ببساطة للمبتدئين البرمجة بالكائنات Object-Oriented Programming تعلم كتابة أكواد بايثون من خلال الأمثلة العملية ما هي أكواد البرمجة
-
أشرنا في المقال السابق إلى إمكانية استخدام قاعدة البيانات المدمجة في المتصفح IndexedDB في تخزين ما هو أعقد من النصوص واﻷرقام، بل يتعداها إلى إمكانية تخزين أي شيئ تريده بما في ذلك الكائنات ذات البنى المعقدة مثل بيانات الفيديوهات والصور الخام. مع ذلك، ليس من الصعب تخزين واسترجاع هذه البيانات مقارنة بغيرها من البيانات التي تعاملنا معها. ولتوضيح هذا اﻷمر، سنطور مثالًا تطبيقيًا باسم IndexedDB video store بإمكانك الاطلاع على عمله مباشرة. ينزّل هذا التطبيق عند تشغيله جميع مقاطع الفيديو من الشبكة ويخزنها في قاعدة البيانات IndexedDB، ويعرض بعدها هذه المقاطع في واجهة المستخدم ضمن العنصر <video>، وعندما تشغّل التطبيق في المرات القادمة، سيجد التطبيق المقاطع ضمن قاعدة البيانات ويعرضها بدلًا من تنزيلها مجددًا مما يجعل العملية أسرع، ويوفّر استهلاك حزمة البيانات المتاحة للاتصال باﻹنترنت. تطبيق عملي: تخزين فيديو في قاعدة البيانات IndexedDB سنعرض تاليًا اﻷجزاء اﻷكثر أهمية في تطبيقنا، ولن نستعرض كل التفاصيل طبعًا، فالكثير منها مشابه تمامًا لما غطيناه في المقال السابق، إضافة إلى وجود تعليقات كافية ضمن الشيفرة توضح خطوات العمل. نخزن بداية أسماء مقاطع الفيديو التي نريد إحضارها ضمن مصفوفة كائنات كما يلي: const videos = [ { name: "crystal" }, { name: "elf" }, { name: "frog" }, { name: "monster" }, { name: "pig" }, { name: "rabbit" }, ]; ننفذ الدالة ()init عند نجاح الاتصال بقاعدة البيانات. ووظيفة هذه الدالة التنقل بين أسماء مقاطع الفيديو السابقة ومحاولة إيجاد سجل يوافق اسم المقطع ضمن قاعدة بيانات الفيديو. فإن وجد مقطع الفيديو ستكون نتيجة request.result هي true وإلا ستكون undefined. ستمرر الدالة بعد ذلك اسم المقطع إن وجد إلى الدالة ()displayVideo لتضعه ضمن واجهة المستخدم وإلا تستدعي الدالة ()fetchVideoFromNetwork ﻹحضار المقطع من اﻹنترنت. function init() { //تنقل بين أسماء مقاطع الفيديو واحدًا تلو اﻵخر for (const video of videos) { // افتح الاتصال مع قاعدة البيانات واحصل على مخزن الكائنات وكل فيديو فيه const objectStore = db.transaction("videos_os").objectStore("videos_os"); const request = objectStore.get(video.name); request.addEventListener("success", () => { // إن وجد المقطع ضمن قاعدة البيانات if (request.result) { //displayVideo احضر المقطع واعرضه على الواجهة باستخدام الدالة console.log("taking videos from IDB"); displayVideo( request.result.mp4, request.result.webm, request.result.name, ); } else { // احضر مقطع الفيديو من الشبكة fetchVideoFromNetwork(video); } }); } } تحضر الدالة ()fetchVideoFromNetwork مقاطع فيديو من النوعين MP4 و WebM باستخدام الطلب ()fetch، بعدها سنستخدم التابع ()response.blob لاستخلاص جسم كل طلب على شكل كائن بيانات ثنائية blob والذي يعطي كائنًا يمثل مقطع الفيديو، ويمكن تخزينه وعرضه لاحقًا. أما المشكلة التي تواجهنا هنا، أن هذين الطلبين غير متزامنين، لكن ما نريده فعلًا هو عرض أو تخزين المقطع فقط عندما يكتمل الوعد promise. لهذا نستخدم التابع ()promise.all الذي يقبل معاملًا واحدًا وهو مصفوفة مراجع إلى كل الوعود التي تريد التحقق من إكتمالها، ويعيد وعدًا يتحقق عندما تتحقق كل الوعود في المصفوفة. نستدعي ضمن التابع ()then المتعلق بهذا الوعد الدالة ()displayVideo كما فعلنا سابقًا لعرض مقطع الفيديو، ثم نستدعي أيضًا الدالة ()storeVideo لتخزين المقطع في قاعدة البيانات: // fetch() إحضار مقاطع الفيديو باستخدام الدالة // blob تحويل أجسام الاستجابات إلى كائن const mp4Blob = fetch(`videos/${video.name}.mp4`).then((response) => response.blob(), ); const webmBlob = fetch(`videos/${video.name}.webm`).then((response) => response.blob(), ); // نفّذ الشيفرة التالية إن تحقق كلا الوعدان Promise.all([mp4Blob, webmBlob]).then((values) => { //displayVideo() اعرض الفيديو الذي أحضرته من الإنترنت باستخدام الدالة displayVideo(values[0], values[1], video.name); //storeVideo() خزن مقطع الفيديو في قاعدة البيانات باستخدام storeVideo(values[0], values[1], video.name); }); يشبه عمل الدالة ()storeVideo ما رأيناه في المقال السابق عندما أضفنا بيانات إلى قاعدة البيانات، إذ نفتح قناة العمليات readwrite مع القاعدة ونتخذ مرجعًا إلى مخزن الكائن video_os ثم ننشئ كائنًا يمثل السجل الذي نريد إضافته إلى القاعدة ونستخدم بعدها التابع ()IDBObjectStore.add: // storeVideo() تعريف الدالة function storeVideo(mp4, webm, name) { // فتح قناة اتصال قراءة وكتابة مع قاعدة البيانات const objectStore = db .transaction(["videos_os"], "readwrite") .objectStore("videos_os"); //Add() إضافة السجل إلى قاعدة البيانات باستخدام const request = objectStore.add({ mp4, webm, name }); request.addEventListener("success", () => console.log("Record addition attempt finished"), ); request.addEventListener("error", () => console.error(request.error)); } تُنشئ الدالة ()displayVideo عناصر شجرة DOM اللازمة لإدراج مقطع الفيديو في واجهة المستخدم ومن ثم تلحق هذه العناصر بالصفحة. أما النقاط اﻷكثر أهمية، فهي التي نستعرضها تاليًا. لعرض كائن البيانات الثنائية الذي يضم الفيديو داخل العنصر <video>، لا بد من إنشاء كائن عنوان URL أي عناوين داخلية تشير إلى كائن البيانات الثنائية المخزن في الذاكرة باستخدام التابع ()URL.creatObjectURL. بعدها يمكننا أن نجعل تلك العناوين قيمًا للسمات src العائدة للعناصر <source> ويعمل عندها كل شيء كما هو متوقع: //displayVideo() تعريف الدالة function displayVideo(mp4Blob, webmBlob, title) { //blob يشير إلى الكائن URL إنشاء كائن const mp4URL = URL.createObjectURL(mp4Blob); const webmURL = URL.createObjectURL(webmBlob); //لإدراج الفيديو في الصفحة DOM إنشاء عنصر في شجرة const article = document.createElement("article"); const h2 = document.createElement("h2"); h2.textContent = title; const video = document.createElement("video"); video.controls = true; const source1 = document.createElement("source"); source1.src = mp4URL; source1.type = "video/mp4"; const source2 = document.createElement("source"); source2.src = webmURL; source2.type = "video/webm"; //في الشجرة DOM إدراج عنصر section.appendChild(article); article.appendChild(h2); article.appendChild(video); video.appendChild(source1); video.appendChild(source2); } تخزين اﻷصول للعمل دون اتصال بالشبكة عرضنا في المثال السابق طريقة إنشاء تطبيق يُخزّن أصولًا assets في قاعدة البيانات IndexedDB حتى لا نضطر إلى تحميلها مجددًا. ويحسن هذا اﻷمر تجربة المستخدم بشكل ملحوظ. لكن لا تزال بعض الأصول المهمة مفقودة كي يعمل التطبيق وهي ملف HTML الرئيسي وملفات CSS وجافا سكريبت، ولا بد من تنزيلها في كل مرة ندخل فيها إلى الموقع، وبالتالي لن يعمل التطبيق بدون الاتصال باﻹنترنت. وهنا يأتي دور عمّال الخدمة service workers والواجهة البرمجية cache API يُعرف عامل الخدمة service worker في جافا سكريبت على أنه ملف يُسجّل تحت مصدر محدد مثل موقع ويب أو جزء من موقع ويب في نطاق معين عندما يلج إليه متصفح ويب. ويتمكن هذا الملف لكونه مسجلًا على نطاق ما أن يتحكم بالصفحات التي تنتمي إلى نفس اﻷصل أو النطاق. ويتوضع هذا الملف في مكان وسط بين الصفحة التي اكتمل تحميلها وشبكة اﻹنترنت ويعترض طلبات الشبكة التي تحوّل من وإلى ذلك المصدر أو اﻷصل. وعندما يعترض العامل الطلب سيكون بمقدوره تنفيذ أي شيئ تريده على هذا الطلب، لكن استخدامه النمطي هو تخزين الاستجابات على طلبات الشبكة والاستجابة لهذا الطلبات في المرات القادمة بدلًا من الاتصال بالشبكة والحصول على الاستجابة. وكنتيجة ستتمكن من بناء صفحة ويب تعمل كليًا دون اتصال بشبكة اﻹنترنت. أما الواجهة البرمجية Cache API فهي آلية أخرى لتخزين البيانات في طرف العميل، مع اختلاف بسيط هو أنها مخصصة لتخزين الاستجابات على طلبات HTTP، لهذا ستعمل جيدًا مع عمال الخدمة service workers. مثال عن عمال الخدمة لنطرح مثالًا يوضح قليلًا الفكرة السابقة. إذ أنشانا نسخة أخرى من مثال تخزين ملفات الفيديو الذي فصلناه في الفقرة السابقة. وتعمل هذه النسخة بنفس اﻷسلوب ما عدا أنها تخزّن أيضًا ملفات HTML و CSS وجافا سكريبت ضمن Cache API من خلال عامل خدمة وبالتالي سيعمل المثال دون اتصال باﻹنترنت. بإمكانك تجريب هذه النسخة مباشرة على جيت-هاب والاطلاع على الشيفرة المصدرية أيضًا. تسجيل عامل الخدمة أول ما تلاحظه هو وجود شيفرة إضافية في ملف جافا سكريبت. تختبر هذه الشيفرة بداية وجود العضو serviceWorker ضمن الكائن Navigator. فإن كان موجودًا (أعادت الشيفرة القيمة true)، نعلم حينها وجود دعم أساسي لعمال الخدمة في المتصفح. نستخدم التابع ()ServiceWorkerContainer.register لتسجيل عامل الخدمة الموجود في الملف sw.js على المصدر الذي يتواجد فيه، وبالتالي سيكون قادرًا على التحكم بالصفحات الموجودة في نفس المجلد أو المجلدات الفرعية. وعندما يتحقق الوعد سيكون عامل الخدمة قد سُجِّل: // تسجيل عامل الخدمة لتتمكن من تشغيل المثال دون اتصال بالشبكة if ("serviceWorker" in navigator) { navigator.serviceWorker .register( "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js", ) .then(() => console.log("Service Worker Registered")); } ملاحظة: يُعطى مسار الملف sw.js بالنسبة إلى أصل الموقع، وليس بالنسبة إلى ملف جافا سكريبت الذي يضم الشيفرة. فالعامل موجود على العنوان: https://mdn.github.io/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js بينما عنوان اﻷصل هو https://mdn.github.io لذا لابد أن يكون العنوان المعطى عند التسجيل هو: /learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/sw.js وإذا أردت استضافة هذا المثال على حاسوبك محليًا، لا بد من تبديل هذه القيم بما يناسب. وعلى الرغم من أنه أمر مربك، لكنه ضروري لأسباب أمنية. تثبيت عامل الخدمة في كل مرة ندخل فيها إلى أحد الصفحات التي تقع تحت سيطرة عامل الخدمة، يُثبَّت عامل الخدمة على هذه الصفحة أي يبدأ العامل بالتحكم فيها. وعندما يحدث ذلك، يقع الحدث install على عامل الخدمة، وستتمكن من كتابة الشيفرة ضمن عامل الخدمة نفسه لتستجيب إلى حدث التثبيت. وإذا ألقينا نظرة على الملف sw.js سنجد أن مترصد حدث التثبيت مسجّل وفق القيمة self أي على العامل نفسه. والتعليمة self طريقة لتشير إلى الطبيعة العامة global scope لعامل الخدمة من داخل ملف عامل الخدمة. نستخدم ضمن دالة المعالج install التابع ()ExtendableEvent.waitUntil العائد إلى كائن الحدث لكي يبلغ المتصفح بعدم تثبيت العامل قبل أن يُنجز الوعد بنجاح. وهنا نجد طريقة عمل الواجهة Cache API الخاصة بعملية التخزين، إذ نستخدم التابع ()CacheStorage.open لفتح كائن تخزين مؤقت جديد cache object لنخزّن ضمنه الاستجابات. وعندما يتحقق الوعد، سيعيد كائن cache يمثل المخزن المؤقت للفيديو video-store. نستخدم بعد ذلك التابع ()Cache.addAll ﻹحضار سلسلة اﻷصول واستجاباتها إلى المخزن المؤقت: self.addEventListener("install", (e) => { e.waitUntil( caches .open("video-store") .then((cache) => cache.addAll([ "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.html", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/index.js", "/learning-area/javascript/apis/client-side-storage/cache-sw/video-store-offline/style.css", ]), ), ); }); وهكذا تنتهي عملية التثبيت. الاستجابة إلى طلبات أخرى مع تسجيل عامل الخدمة ليتحكم بصفحة HTML وإضافة كل اﻷصول إلى المخزن المؤقت، سيكون التطبيق جاهزًا للعمل. لكن بقي هناك شيء واحد وهو كتابة شيفرة تستجيب إلى طلبات HTML اﻷخرى التي ترد، وهذا ما يفعله القسم الثاني من الشيفرة في الملف sw.js. نضيف مترصدًا آخر عامًا إلى عامل الخدمة، يعمل على تنفيذ معالج الحدث عندما يقع حدث اﻹحضار fetch. ويقع هذا الحدث في كل مرة يحاول فيها المتصفح إجراء طلب لأحد اﻷصول الموجودة في نفس المجلد الذي يضم عامل الخدمة. نسجّل بداية ضمن دالة المعالج عنوان URL للأصل المطلوب، ثم نهيئ استجابة مخصصة لهذا الطلب باستخدام التابع ()FetchEvent.respondWith. وضمن كتلة التابع السابق نستخدم التابع ()CacheStorage.match للتحقق من وجود طلب موافق لهذا اﻷصل ضمن المخزن المؤقت. ويتحقق الوعد الموافق لهذا الطلب في حال وجود عنوان في المخزن يطابق عنوان URL للطلب وإلا سيعيد الوعد القيمة undefined. نعيد بعد ذلك الطلب على شكل استجابة مخصصة في حال كان العنوان موجودًا وإلا نستخدم الواجهة ()fetch ﻹحضاره من الشبكة كونه غير موجود في المخزن المؤقت: self.addEventListener("fetch", (e) => { console.log(e.request.url); e.respondWith( caches.match(e.request).then((response) => response || fetch(e.request)), ); }); وهكذا نستخدم عامل الخدمة، علمًا أن استخداماته أوسع من ذلك ولا يسعنا تغطيها في هذا المقال. اختبار المثال دون اتصال بالشبكة لاختبار عمل تطبيقنا، لا بد من تحميله عدة مرات للتأكد من تثبيت عامل الخدمة، وبعدها يمكنك: قطع الاتصال باﻹنترنت. اختيار العمل دون اتصال (اختر ملف ثم العمل دون اتصال إن كنت تستخدم فايرفوكس). الانتقال إلى أدوات مطوري ويب واختر تطبيقات ثم عمال الخدمة Service workers، ولا بد من تفعيل الخيار offline إن كنت تستخدم متصفح كروم. لو حاولت اﻵن تحديث الصفحة سترى أنها ستعيد التحميل دون أية مشكلات لأن كل أصول الصفحة قد خُزّنت في المخزن المؤقت cache، كما أن كل مقاطع الفيديو مخزنة ضمن قاعدة البيانات IndexedDB. الخلاصة تعرفنا في هذا المقال على طريقة لتشغيل صفحة ويب دون اتصال باﻹنترنت عن طريقة استخدام عامل خدمة service worker مع الواجهة البرمجية Cache التي تخزّن الأصول ضمن مخازن مؤقتة في حاسوبك. ترجمة -وبتصرف- للجزء الثالث من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل باستخدام قاعدة البيانات المفهرسة IndexedDB تخزين البيانات في طرف العميل: مخازن ويب Web Storage تخزين البيانات محليا في متصفح الويب عبر جافاسكربت التخزين المحلي (Local Storage) في HTML5 تعرّف على IndexedDB
-
تُعد الواجه البرمجية IndexedDB أو IDB اختصارًا منظومة قواعد بيانات كاملة مضمنة في المتصفح تساعدك على تخزين بيانات مترابطة معقدة لا تقتصر فيها أنواع البيانات على قيم بسيطة مثل النصوص واﻷعداد. إذ تستطيع تخزين مقاطع الفيديو والصور وتقريبًا أي شيء في نسخ منفصلة من القاعدة IndexedDB. وتتيح لك هذه الواجهة البرمجية إنشاء قاعدة بيانات ومن ثم إنشاء مخازن كائنات object stores ضمن القاعدة. وتُعد مخازن الكائنات بمثابة بنى شبيهة بالجداول الموجودة في قواعد البيانات العلاقية relational databases، ويمكن لأي مخزن أن يضم مخازن كائنات أخرى. وبالطبع تأتي هذه الميزات مع آثار لا بد منها، فالواجهة IndexedDB أكثر تعقيدًا من واجهة مخازن ويب Web Stores من ناحية الاستخدام. لهذا سنحاول في هذا المقال تقديم مثال يعرض جزءًا ضئيًلا جدًا من إمكانات هذه الواجهة، لكنه سيزوّدك باﻷساسيات التي تساعدك على الانطلاق. تطبيق عملي: تخزين ملاحظات سنبني تطبيق بسيط يسمح لك بتخزين ملاحظات في المتصفح ومراجعة هذه الملاحظات وحذفها متى شئت. وسيكون البناء خطوة خطوة نشرح فيها اﻷجزاء اﻷكثر أهمية من قاعدة البيانات IndexedDB. سيبدو شكل التطبيق عند انتهائه كالتالي: تتكون كل ملاحظة من عنوان ومحتوى نصي يمكن تحرير أي منهما بشكل مستقل عن اﻵخر. وستجد ضمن شيفرة جافا سكريبت الخاصة بالتطبيق تعليقات كافية لشرح كل خطوة بالتفصيل. نقطة الانطلاق انسخ بداية الملفات index.html و style.css و index-start.js إلى مجلد جديد تُنشئه على حاسوبك. الق نظرة في البداية على تلك الملفات، وسترى أن ملف HTML يُعرّف موقع ويب له ترويسة وتذييل ومنطقة محتوى رئيسي تُعرض فيه الملاحظات، إضافة إلى نموذج ﻹدخالها في قاعدة البيانات. يقدّم ملف CSS مجموعة من قواعد التنسيق لتوضيح ما يجري، بينما يضم ملف جافا سكريبت تصريحًا عن خمسة ثوابت تضم مراجع إلى العنصر <ul> وستُعرض الملاحظات على شكل عنوان ونص ضمن عنصري إدخال <input> كما ننشئ مرجعًا إلى النموذج <form> بحد ذاته، ومرجعًا إلى زر <button>. غير اسم ملف جافا سكريبت إلى index.js. تهيئة قاعدة البيانات لنلق نظرة اﻵن على ما يتوجب علبنا فعله بداية لإعداد قاعدة البيانات: أضف السطر التالي تحت التصريحات عن الثوابت: // إنشاء نسخة عن كائن قاعدة البيانات let db; نصرّح في هذا السطر عن متغير يُدعى db لنستخدمه لاحقًا في تخزين الكائن الذي يمثل قاعدة البيانات. وطالما أننا نستخدمه في عدة أماكن لذلك صرحنا عنه كمتغير عام global لتسهيل اﻷمر. أضف تاليًا الشيفرة التالية: // فتح قاعدة البيانات مما يؤدي إلى إنشائها إن لم تكن موجودة const openRequest = window.indexedDB.open("notes_db", 1); يُنشئ هذا السطر طلبًا لفتح النسخة 1 من قاعدة بيانات تُدعى notes_db، فإن لم تكن موجودة سوف ينشئها السطر الذي يليه. سترى هذا الشكل من الطلبات كثيرًا عند استخدام IndexedDB. وطالما أن العمليات على قواعد البيانات تحتاج وقتًا، فلا ينبغي ايقاف المتصفح ريثما ننتظر نتيجة الطلب، لهذا عمليات قواعد البيانات هي عمليات غير متزامنة لن تُنفّذ مباشرة، بل في فترة ما مستقبلًا وستُبلَّغ بتنفيذها. ولمعالجة اﻷمر في IndexedDB، ننشئ كائن طلب request object ندعوه مثلًا openRequest، ويمكنك حينها استخدام معالج حدث لتنفيذ شيفرة مخصصة عند اكتمال هذا الطلب أو فشله، وهذا ما ستراه بعد قليل. ملاحظة: إن رقم النسخة أمر مهم. فلو أردت تحديث قاعدة بياناتك (مثل تغيير هيكلية الجدول)، عليك تنفيذ شيفرتك مجددًا بعد زيادة رقم النسخة، وتحديد تخطيط مختلف ضمن معالج الحدث upgradeneeded. لكننا لن نغطي تحديث قاعدة البيانات في هذا المقال. أضف اﻵن معالج الحدث التالي تحت الشيفرة السابقة: // معالج خطأ يحدد حالة فشل الاتصال بقاعدة البيانات openRequest.addEventListener("error", () => console.error("Database failed to open"), ); // معالج نجاح يحدد نجاح فتح قاعدة البيانات openRequest.addEventListener("success", () => { console.log("Database opened successfully"); //db خزن قاعدة البيانات المفتوحة ضمن المتغير db = openRequest.result; //التي تعرض الملاحظات الموجودة في قاعدة البيانات displayData() نفّذ الدالة displayData(); }); يُنفَّذ معالج الحدث error عندما يُبلغك النظام أن طلبك قد أخفق، مما يتيح لك التعامل مع المشكلة. وما فعلناه في مثالنا هو عرض رسالة خطأ في طرفية جافا سكريبت. ويُنفَّذ معالج الحدث success عندما ينجح الطلب، بمعنى أن الاتصال إلى قاعدة البيانات تحقق، وأصبح الكائن الذي يمثّل قاعدة البيانات متاحًا ضمن الخاصية openRequest.result وبالتالي إمكانية التعامل من خلاله مع قاعدة البيانات. نخزّن النتيجة ضمن المتغيّر db وننفذ دالة تُدعى ()displayData وظيفتها عرض البيانات الموجودة في قاعدة البيانات داخل العنصر <ul> بمجرد انتهاء تحميل الصفحة، وسترى تعريف هذه الدالة لاحقًا. في نهاية هذا القسم سنضيف معالج الحدث upgradeneeded الذي يُنفَّذ إن لم تكن قاعدة البيانات قد هُيئت مسبقًا أو عندما تكون قاعدة البيانات مفتوحة. لهذا أضف اﻷسطر التالية في نهاية شيفرتك منتبهًا إلى ضرورة استخدام رقم نسخة أعلى من رقم النسخة المخزنة في قاعدة البيانات عندما تريد تحديث القاعدة: // هيئ جداول قاعدة البيانات إن لم تكن مهيأة مسبقًا openRequest.addEventListener("upgradeneeded", (e) => { // احصل على مرجع إلى قاعدة البيانات المفتوحة db = e.target.result; // أنشئ مخزن كائنات في قاعدة البيانات لتخزين الملاحظة مع مفتاح يزداد تلقائيًا ومخزن الكائنات مشابه للجدول في قاعدة البيانات العلاقية const objectStore = db.createObjectStore("notes_os", { keyPath: "id", autoIncrement: true, }); // حدد ما ستضمه عناصر البيانات ومخازن الكائنات objectStore.createIndex("title", "title", { unique: false }); objectStore.createIndex("body", "body", { unique: false }); console.log("Database setup complete"); }); نعرّف هنا التخطيط الذي نعتمده لقاعدة البيانات، أي نحدد مجموعة الأعمدة أو الحقول التي تضمها. وما فعلناه أننا حددنا مرجعًا في البداية إلى قاعدة البيانات الموجودة عبر الخاصية result لمعالج الحدث e.target.result الذي يمثّل كائن الطلب request. وهذا الأمر مكافئ للسطر: db = openRequest.result; داخل معالج الحدث succes، لكن لا بد هنا من تنفيذه بشكل مستقل لأن معالج الحدث upgradeneeded سيُنفذ إن احتجنا إليه قبل المعالج success، ولن يكون المتغير db متاحًا ما لم نفعل ذلك. نستخدم بعد ذلك التابع IDBDatabase.createObjectStore ﻹنشاء مخزن كائن جديد ضمن قاعدة البيانات المفتوحة يُدعى notes_os ويكافئ هذا المخزن جدولًا مفردًا في قواعد البيانات النمطية. كما خصصنا أيضًا حقلًا مفتاحيًا autoIncrement في هذا الكائن وسميناه id تزداد قيمة هذا الحقل كلما أضفنا ملاحظة جديدة ولا حاجة أن يفعل المطوّر هذا صراحة بل هي عملية آلية. يعمل هذا الحقل كمعرّف فريد لكل سجل وذلك عندما نريد تعديل أو حذف هذا السجل. وأنشأنا أيضًا حقلين مفهرسين آخرين باستخدام التابع ()IDBObjectStore.createIndex وهما title و body كي يضما عنوان الملاحظة ونصها. وسيمثَّل كل منهما على شكل كائن له التخطيط التالي، وذلك عندما نبدأ بإضافة الملاحظات إلى قاعدة البيانات وفق التخطيط الذي وضعناه: { "title": "Buy milk", "body": "Need both cows milk and soy.", "id": 8 } إضافة بيانات إلى قاعدة البيانات لنرى اﻵن كيف يمكننا إضافة سجلات إلى قاعدة البيانات باستخدام النموذج الذي صممناه في صفحتنا. لهذا أضف الأسطر التالية تحت معالج الحدث السابق. تضبط هذه اﻷسطر معالج الحدث submit الذي يستدعي الدالة ()addData عند تسليم النموذج (النقر على زر اﻹرسال): //addData() إنشاء معالج حدث لعملية تسليم النموذج يعمل عند تنفيذ الدالة form.addEventListener("submit", addData); لنعرّف اﻵن الدالة ()addData كالتالي: // التصريح عن الدالة function addData(e) { //نمنع السلوك الافتراضي، فلا نريد تسليم النموذج بالطريقة النمطية e.preventDefault(); // الحصول على القيم التي نريد تخزينها ووضعها ضمن عنصر تخزين const newItem = { title: titleInput.value, body: bodyInput.value }; // فتح قناة كتابة وقراءة إلى قاعدة البيانات const transaction = db.transaction(["notes_os"], "readwrite"); // استدعاء مخزن كائنات موجود بالفعل في قاعدة البيانات const objectStore = transaction.objectStore("notes_os"); //إلى مخزن الكائنات newItem إنشاء طلب ﻹضافة الكائن const addRequest = objectStore.add(newItem); addRequest.addEventListener("success", () => { // مسح النموذج استعدادًا ﻹضافة سجل آخر titleInput.value = ""; bodyInput.value = ""; }); // اﻹبلاغ عن نجاح العملية على قاعدة البيانات عند اكتمالها transaction.addEventListener("complete", () => { console.log("Transaction completed: database modification finished."); //مجددًا displayData تحديث عرض البيانات بعد إضافة السجل الجديد باستدعاء الدالة displayData(); }); transaction.addEventListener("error", () => console.log("Transaction not opened due to error"), ); } لنحاول تفسير هذه الشيفرة كونها معقدة نوعًا ما: تنفيذ الدالة ()Event.preventDefault على كائن الحدث لإيقاف إرسال بيانات النموذج بالطريقة النمطية (لأنها تدفع إلى إعادة تحديث الصفحة وإفساد التأثير المطلوب). إنشاء كائن يمثل السجل الذي نريد إدخاله إلى قاعدة البيانات ونشره بالقيم التي نحصل عليها من عناصر اﻹدخال. ولاحظ أنه لا حاجة لتزويد السجل بقيمة للمعرّف id كما ذكرنا سابقًا بل سيضاف تلقائيًا. فتح عملية القراءة والكتابة readwrite إلى مخزن الكائنات notes_os باستخدام التابع ()IDBDatabase.transaction. يساعد كائن عمليات قاعدة البيانات في الوصول إلى مخزن الكائن لتنفيذ العملية المطلوبة عليه مثل إضافة سجل جديد. الوصول إلى مخزن الكائن باستخدام التابع ()IDBTransaction.objectStore وتخزين النتيجة في المتغير objectStore. إضافة السجل الجديد إلى قاعدة البيانات باستخدام التابع ()IDBObjectStore.add الذي يُنشئ كائن طلب بنفس اﻹسلوب الذي رأيناه سابقًا. إضافة مجموعة من معالجات اﻷحداث إلى الكائن request والكائن transaction لتنفيذ الشيفرة المطلوبة عند النقاط المطلوبة خلال دورة حياة التطبيق. وبمجرد نجاح الطلب، نمسح حقول اﻹدخال في النموذج لتحضيرها لعملية إدخال أخرى. وعند اكتمال العملية، ننفذ الدالة ()displayData مجددًا لتحديث ما يُعرض من ملاحظات في الصفحة. عرض البيانات أشرنا إلى الدالة ()displayData مرتين في تطبيقنا، لهذا من اﻷفضل اﻵن تعريف هذه الدالة. أضف اﻷسطر البرمجية التالية بعد تعريف الدالة السابقة: // displayData() تعريف الدالة function displayData() { // نمحي محتوى عناصر القائمة في كل مرة نحدّث فيها ما يُعرض // وإلا ستتكرر العناصر في كل مرة نجري فيها تحديثًا while (list.firstChild) { list.removeChild(list.firstChild); } // افتح مخزن الكائنات واحصل على مؤشر يتنقل بين عناصره المختلفة const objectStore = db.transaction("notes_os").objectStore("notes_os"); objectStore.openCursor().addEventListener("success", (e) => { // احصل على مرجع إلى هذا المؤشر const cursor = e.target.result; // استمر في تنفيذ الشيفرة طالما هناك عناصر يمكن التنقل بينها if (cursor) { //p وفقرة نصية h3 وعنوان itemlist أنشئ عنصر قائمة //كي تضع ضمنها كل عنصر بيانات سيُعرض // ألحق الفقرة والعنوان بعنصر القائمة ثم ألحقه بالقائمة const listItem = document.createElement("li"); const h3 = document.createElement("h3"); const para = document.createElement("p"); listItem.appendChild(h3); listItem.appendChild(para); list.appendChild(listItem); // ضع البيانات الموجودة في المؤشر ضمن الفقرة النصية والعنوان h3.textContent = cursor.value.title; para.textContent = cursor.value.body; // خزن معرّف عنصر البيانات داخل سمة ضمن عنصر القائمة //كي نعرف إلى أي عنصر تنتمي. وسيفيدنا ذلك عند حذف العناصر listItem.setAttribute("data-note-id", cursor.value.id); // أنشئ زرًا وضعه ضمن كل عنصر قائمة const deleteBtn = document.createElement("button"); listItem.appendChild(deleteBtn); deleteBtn.textContent = "Delete"; // اضبط معالج حدث يحذف عنصر القائمة عند النقر على هذا الزر deleteBtn.addEventListener("click", deleteItem); // انقل المؤشر إلى العنصر التالي cursor.continue(); } else { //إن لم تكن هنالك أية عناصر قائمة `No notes stored` اعرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } // إن لم تكن هنالك أية عناصر اخرى للتنقل بينها، ابلغ عن ذلك برسالة console.log("Notes all displayed"); } }); } لنشرح الآن الشيفرة السابقة بشيء من التفصيل: نمحي بداية محتوى العنصر <ul> قبل أن نملأه مجددًا بالمحتوى المُحدَّث، وإلا سينتهي بك اﻷمر إلى قائمة مليئة بالعناصر المكررة التي تُضاف عند كل تحديث. نتخذ مرجعًا إلى مخزن الكائن notes_os باستخدام التابعين ()IDBDatabase.transaction و ()IDBTransaction.objectStore كما فعلنا مع الدالة ()addData، ماعدا أننا نربطهما معًا في سطر واحد هنا. نستخدم تاليًا التابع ()IDBObjectStore.openCursor لطلب للحصول على مؤشر Cursor، وهي بنية تُستخدم للتنقل بين السجلات في مخزن الكائنات. كما نربط معالج حدث النجاح success في نهاية السطر لنجعل الشيفرة أكثر ترابطًا. وعندما يُعاد المؤشر بنجاح يُنفَّذ المعالج. نتخذ مرجعًا إلى المؤشر ذاته (على شكل كائن IDBCursor) باستخدام السطر const cursor= e.target.result نتحقق تاليًا من وجود سجل من مخزن الكائنات ضمن المؤشر ({}if (cursor))، فإن كان اﻷمر كذلك، ننشئ فرعًا في شجرة DOM وننشر ضمنه بيانات السجل ومن ثم نعرضها على الصفحة ضمن العنصر <ul>. كما نضمّن الفرع زرًا يحذف عنصر القائمة الذي يضم بيانات السجل عند النقر على هذا الزر، وذلك بتنفيذ الدالة ()deleteItem التي نتعرف عليها في الفقرة القادمة. في نهاية الكتلة if نستخدم التابع ()IDBCursor.continue لنقل المؤشر إلى السجل التالي في المخزن وتنفيذ محتوى الكتلة if مجددًا إن كان هنالك سجل آخر سيعرض في الصفحة، ومن ثم يتابع المؤشر تفقد وجود سجلات أخرى. عند انتهاء السجلات، يعيد المؤشر القيمة undefined وبالتالي ستعمل الكتلة else هذه المرة. وتتحقق هذه الكتلة من إضافة أية ملاحظات إلى القائمة <ul>، فإن لم تُضاف أية ملاحظات، تعرض رسالة مفادها عدم وجود أية ملاحظات محفوظة. حذف ملاحظة أشرنا سابقا إلى أن النقر على زر الحذف الموجود إلى جوار الملاحظة المعروضة يسبب حذفها. وننفذ ذلك باستخدام الدالة ()deleteitem: // deleteItem() تعريف الدالة function deleteItem(e) { // الحصول على الملاحظة التي ينبغي حذفها على شكل عدد قبل //IDB: IDB key محاولة استخدامها عن طريق الزوج // والانتباه إلى أن القيم حساسة لحالة الأحرف const noteId = Number(e.target.parentNode.getAttribute("data-note-id")); // فتح قناة العمليات مع قاعدة البيانات وحذف الملاحظة التي حصلنا //على رقمها سابقًا عن طريق السمة التي خزنا فيها معرف الملاحظة const transaction = db.transaction(["notes_os"], "readwrite"); const objectStore = transaction.objectStore("notes_os"); const deleteRequest = objectStore.delete(noteId); // اﻹبلاغ عن حذف عنصر القائمة transaction.addEventListener("complete", () => { // حذف العنصر اﻷب للزر وهو في حالتنا عنصر القائمة ولن يُعرض بعدها e.target.parentNode.parentNode.removeChild(e.target.parentNode); console.log(`Note ${noteId} deleted.`); //إن لم تكن هنالك عناصر قائمة `No Notes Stored` عرض رسالة if (!list.firstChild) { const listItem = document.createElement("li"); listItem.textContent = "No notes stored."; list.appendChild(listItem); } }); } نحصل على معّرف السجل الذي نحذفه باستخدام التابع Number(e.target.parentNode.getAttribute('data-note-id'))، وتذكر أن معرّف السجل قد خُزِّن سابقًا ضمن السمة data-note-id لعنصر القائمة <li> عندما عُرض أول مرة. لكن لا بد من تمرير الدالة التي تعطينا قيمة السمة إلى التابع العام ()Number لأنها من النوع النصي وما نريده هو المكافئ العددي للقيمة وإلا لن تميزها قاعدة البيانات التي تتوقع عددًا. نتخذ تاليًا مرجعًا إلى مخزن الكائن مستخدمين اﻷسلوب الذي خبرناه سابقًا ومن ثم التابع ()IDBObjectStore.delete لحذف السجل من قاعدة البيانات بعد أن نمرر له معرف الملاحظة. عند اكتمال العملية على قاعدة البيانات، نحذف العنصر <li> من شجرة DOM ونتحقق مجددًا من خلو القائمة <ul> من العناصر. إن واجهت صعوبة في تطبيق المثال، قارن بين نسختك والنسخة المكتملة كما يمكنك أيضًا الاطلاع على الشيفرة المصدرية. الخلاصة تعرفنا في هذا المقال على قاعدة البيانات المدمجة في المتصفح IndexedDB وكيفية التعامل معها من خلال مثال تطبيقي يعرض أساسيات إضافة وحذف البيانات واسترجاعها. ترجمة -وبتصرف- للجزء الثاني من مقال: Client-side storage اقرأ أيضًا المقال السابق: تخزين البيانات في طرف العميل: مخازن ويب Web Storage التحقق من صحة بيانات استمارة ويب في طرف العميل نظرة على تفاعلات الخادم مع العميل في موقع ويب ديناميكي الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت
-
تقدم المتصفحات الحديثة مجموعة من التقنيات المختلفة التي تسمح بتخزين بيانات تتعلق بمواقع الويب ثم استرجاعها عند الضرورة، مما يسمح لنا بالحفاظ على البيانات لفترة أطول أو تخزينها للعمل دون اتصال باﻹنترنت وغير ذلك. لهذا سنناقش في مقالنا أبسط اﻷساسيات المتعلقة بهذا اﻷمر وكيفية عملها. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. ماذا يعني تخزين البيانات في طرف العميل؟ تحدثنا في مقالات مختلفة عن الفرق بين مواقع الويب الساكنة والدينياميكية، لكن لا بد من اﻹشارة إلى أن معظم مواقع الويب الحديثة ديناميكية، فهي تخزن البيانات في الخادم مستخدمة نوعًا من قواعد البيانات (تخزين في طرف الخادم)، ومن ثم تنفّذ شيفرة في طرف الخادم لاستعادتها ووضعها ضمن قوالب صفحات ساكنة، لتقدّم النتيجة بعدها إلى العميل على شكل صفحاتHTML يعرضها المتصفح. ويعمل التخزين في طرف العميل وفق اﻷسلوب ذاته، لكن له استخدامات خاصة. ولتنفيذ هذه العمليات نحتاج إلى واجهات جافا سكريبت التي تسمح لنا بتخزين البيانات على جهاز العميل واستعادتها عند الحاجة. وتُخّزن البيانات في طرف العميل لاستخدامات محددة مثل: إضفاء خصوصية للمستخدم في الموقع (مثل عرض خيارات يفضلها المستخدم أو اختيار اللون أو حجم الخط). تخزين النشاطات السابقة للمستخدم مثل تخزين محتوى قائمة مشتريات من جلسة سابقة أو تذكر تسجيل الدخول السابق). تخزين البيانات واﻷصول محليًا لتسريع تحميل الموقع (مع احتمال انخفاض كلفة التصفح) وإمكانية التصفح دون الاتصال بالانترنت. تخزين صفحات الويب التي تولدها تطبيقات الويب ديناميكيًا محليًا لاستخدامها دون اتصال بالانترنت. تُستخدم الطريقتان السابقتان في التخزين معًا عادةً، فقد تحمّل مثلًا ملف موسيقى (يُستخدم مع لعبة ويب أو مشغّل موسيقى) ومن ثم تخزّنه في قاعدة بيانات طرف العميل ومن ثم تشغيله عند الحاجة. وهكذا يمكن للمستخدم تحميل الملف لمرة واحدة، وعند الزيارات اللاحقة للموقع يستخرج الملف من قاعدة بياناته المحلية مما يسرع العملية ويقلل تكلفة التصفح. ملاحظة: هناك حد لكمية البيانات التي يمكن تخزينها في طرف العميل عبر الواجهات البرمجية (منفصلة أو مجتمعة) ويختلف هذا الحد وفقًا للمتصفح، وقد يعتمد على اﻹعدادات التي يضبطها المستخدم. استخدام الطريقة التقليدية: ملفات تعريف الارتباط Cookies تُعد تقنية تخزين البيانات في طرف العميل تقنية قديمة، فقد استخدمت المواقع ملفات تعريف الارتباط cookies منذ البدايات الأولى للويب، وذلك لتخزين البيانات وإعطاء طابع شخصي للموقع. وقد كانت أولى أشكال تخزين البيانات في طرف العميل. أما حاليًا، فقد ظهرت تقنيات أفضل وأحدث لتخزين البيانات في طرف العميل، لذلك لن نتحدث عن استخدام ملفات تعريف اﻹرتباط في مقالنا الحالي. ولا يعني ذلك بالطبع أن ملفات تعريف الارتباط عديمة الفائدة في عالم الويب المعاصر، إذ لا تزال شائعة الاستخدام في تخزين البيانات المتعلقة بمعلومات المستخدم الشخصية وحالته مثل معرفات الجلسة session IDs ومفاتيح الوصول المشفرة access token. التقنية الجديدة: مخازن ويب وقاعدة البيانات IndexedDB من الميزات السهلة للتقنيتين اللتين يشير إليهما العنوان نجد: اﻵلية التي تقدمها واجهة مخازن ويب البرمجية Web Storage API في تخزين واسترجاع عناصر البيانات صغيرة الحجم والمكونة من اسم وقيمة موافقة. ولهذا اﻷمر أهميته عندما تحتاج إلى تخزين بيانات بسيطة مثل اسم المستخدم وتاريخ تسجيل الدخول إلى موقع الويب واللون الذي يفضله للخلفية وهكذا. قاعدة البيانات المتكاملة التي تقدمها الواجهة البرمجية IndexedDB API للمتصفح لتخزين البيانات الأكثر تعقيدًا. يمكن استخدام هذه القاعدة مثلًا في تخزين بيانات مجموعة كاملة من سجلات المستخدمين وحتى أنواع معقدة من البيانات مثل ملفات الصوت والفيديو. الواجهة البرمجية Cache صُممت هذه الواجهة لتحزين الاستجابات الناتجة عن طلبات HTTP محددة، وهي مفيدة خصوصًا في أمور مثل تخزين أصول موقع ويب محليًا ليتمكن الموقع من استخدامها باستمرار دون اتصال مع شبكة اﻹنترنت. تُستخدم واجهة التخزين المؤقت cache عادةً بمرافقة واجهة عمال الخدمة Service Worker API على الرغم من عدم الحاجة إلى ذلك فعليًا. ويُعد استخدام واجهة التخزين المؤقت مع واجهة عمال الخدمة موضوعًا متقدمًا لن نغطيه في سلسلة مقالاتنا بالتفصيل، مع ذلك، سنعرض في آخر مقال من هذه السلسلة مثالًا عنها. الواجهة Web Storage وتخزين بيانات بسيطة من السهل جدًا استخدام هذه الواجهة، إذ تخزن البيانات البسيطة على شكل أزواج مكونة من اسم name وقيمة value (محدودة بأنواع مخصصة مثل النصوص واﻷعداد وغيرها)، ومن ثم استرجاع تلك القيم عند الحاجة. الصياغة القواعدية الأساسية لنلق نظرة على ذلك: انتقل بداية إلى القالب الموجود على جيت-هاب وافتحه في نافذة جديدة. افتح طرفية جافا سكريبت في المتصفح. توضع مخازن ويب ضمن بُنى تشبه الكائنات في المتصفح هي sessionStorge و localStorage. تبقى البيانات المخزنة في البنية اﻷولى طالما أن المتصفح يعمل (تُحذف هذه البيانات عند إغلاق المتصفح)، بينما تبقى البيانات في البنية الثانية مقيمة في الذاكرة حتى بعد إغلاق المتصفح. سنستخدم في مقالنا البنية الثانية لأنها أكثر فائدة عمومًا. إذ يسمح التابع ()Strorage.setItem بتخزين البيانات في البنية Storge، وله معاملان: اﻷول هو اسم العنصر، والثاني هو القيمة. جرّب كتابة ما يلي في طرفية جافا سكريبت: localStorage.setItem("name", "Chris"); يأخذ التابع ()Storage.getItem معاملًا واحدًا يمثل عنصر البيانات الذي تريد استرجاع قيمته. جرّب اﻵن الشيفرة التالية: let myName = localStorage.getItem("name"); myName; سترى عند كتابتك الشيفرة السابقة كيف سيضم المتغير myName قيمة عنصر البيانات name. يأخذ التابع ()removeItem معاملًا واحدًا هو اسم عنصر البيانات التي تريد إزالته ومن ثم يزيله من مخزن ويب. جرّب الشيفرة التالية في طرفية جافا سكريبت: localStorage.removeItem("name"); myName = localStorage.getItem("name"); myName; من المفترض أن يعيد تنفيذ السطر الثالث القيمة null للعنصر name لأنه لم يعد موجودًا في مخزن ويب. البيانات المقيمة في الذاكرة من الميزات المهمة لمخازن ويب أن البيانات تبقى موجودة في الفترة التي تُحمّل فيها الصفحات وحتى بعد إغلاق المتصفح عند استخدام local Storage، لنلق نظرة على هذا اﻷمر: افتح مجددًا قالب مخازن ويب السابق لكن في متصفح يختلف عن المتصفح الذي تقرأ فيه هذا المقال. اكتب الشيفرة التالية في طرفية جافا سكريبت لهذا المتصفح: localStorage.setItem("name", "Chris"); let myName = localStorage.getItem("name"); myName; من المفترض أن ترى قيمة عنصر البيانات name. اغلق اﻵن المتصفح ثم افتحه مجددًا. اكتب الشيفرة التالية في طرفية جافا سكريبت: let myName = localStorage.getItem("name"); myName; سترى أن قيمة عنصر البيانات لا تزال متوفرة على الرغم من إغلاق المتصفح وفتحه مجددًا. مخزن منفصل لكل نطاق يُوجد مخزن بيانات منفصل لكل نطاق (لكل عنوان ويب حمّله المتصفح)، وسترى ذلك إن حمّلت موقعين وحاولت تخزين عنصر بيانات في أحدهما، فلن يكون هذا العنصر متاحًا للموقع اﻵخر. وهذا اﻷمر منطقي، فرؤية بيانات موقع من موقع آخر مصدر للكثير من الثغرات اﻷمنية. مثال على مخزن ويب بتفاصيل أكثر سنبني في هذه الفقرة مثالًا نطبق فيه ما تعلمناه ويعطيك فكرة عن كيفية استخدام مخزن ويب. ندخل في هذا المثال اسمًا ثم نُحدّث الصفحة بعد ذلك لترحب بصاحب الاسم شخصيًا. وستبقى هذه الحالة خلال إعادة تحميل الصفحات أو المتصفح لأننا سنخزن الاسم في مخزن ويب. بإمكانك إيجاد نسخة عن ملف HTML المستخدم على جيت-هاب، ويتضمن موقع ويب يتكون من ترويسة ومحتوى وتذييل ونموذج ﻹدخال الاسم. سنبني المثال اﻵن حتى نفهم آلية عمله: انسخ ملف المثال إلى مجلد على حاسوبك. لاحظ كيف يشير ملف HTML إلى ملف جافا سكريبت يُدعى index.js من خلال سطر يشبه السطر <script src="index.js" defer></script>. علينا إذًا إنشاء الملف index.js ضمن نفس المجلد الذي يضم ملف HTML وكتابة شيفرة جافا سكريبت ضمنه. نبدأ الشيفرة ببناء مراجع إلى جميع عناصر HTML التي نريد التعامل معها في مثالنا، وستكون هذه المراجع على شكل ثوابت لأننا لن نغيرها خلال دورة حياة التطبيق. أضف اﻵن الشيفرة التالية: // إنشاء الثوابت المطلوبة const rememberDiv = document.querySelector(".remember"); const forgetDiv = document.querySelector(".forget"); const form = document.querySelector("form"); const nameInput = document.querySelector("#entername"); const submitBtn = document.querySelector("#submitname"); const forgetBtn = document.querySelector("#forgetname"); const h1 = document.querySelector("h1"); const personalGreeting = document.querySelector(".personal-greeting"); علينا اﻵن كتابة مترصد أحداث بسيط لمنع النموذج من تسليم محتوياته عند النقر على زر اﻹرسال، فهذا ليس السلوك الذي نريده. أضف الشيفرة التالية تحت الشيفرة السابقة: // Stop the form from submitting when a button is pressed form.addEventListener("submit", (e) => e.preventDefault()); يجب إضافة الدالة التي تتعامل مع حدث النقر على الزر "Say hello"، وستجد شرحًا وافيًا ضمن تعليقات الشيفرة لكل خطوة، لكن ما تفعله الشيفرة عمومًا هو الحصول على الاسم الذي ندخله ضمن صندوق اﻹدخال النصي وتخزينه في مخزن ويب باستخدام الدالة ()setItem ثم تنفيذ الدالة ()nameDisplayCheck التي تعالج عملية تحديث النص المطلوب من الصفحة. أضف الشيفرة التالية أسفل الشيفرة السابقة: //`Say hello` نفذ الدالة عند النقر على الزر submitBtn.addEventListener("click", () => { // احفظ الاسم في مخزن ويب localStorage.setItem("name", nameInput.value); //لعرض التحية المخصصة nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نحتاج إلى معالج حدث للتعامل مع النقر على الزر "Forget" الذي يظهر فقط بعد النقر على الزر "Say hello". كما نزيل في دالة معالج الحدث العنصر name من مخزن ويب باستخدام التابع ()removeItem ثم ننفذ مجددًا الدالة ()nameDisplayCheck لتحديث ما يُعرض. أضف الآن الشيفرة التالية: //`Forget` نفّذ الدالة عند النقر على الزر forgetBtn.addEventListener("click", () => { //إزالة الاسم المخزن في مخزن ويب localStorage.removeItem("name"); //لعرض التحية الأصلية وتحديث ما يُعرض nameDisplayCheck استخدم الدالة nameDisplayCheck(); }); نعرّف اﻵن الدالة ()nameDisplayCheck التي نتحقق فيها فيما لو خُزِّن العنصر name في مخزن ويب باستخدام التابع ('name('localStorage.getItem من خلال عبارة شرطية. فإن وُجد في المخزن، ستكون نتيجة الشرط true وإلا ستكون false. نعرض في الحالة اﻷولى رسالة الترحيب الخاصة ونعرض الجزء "forget" من النموذج ونخفي الجزء "Say hello"، أما في الحالة الثانية، سنعرض الرسالة الأصلية ونجري عكس ما فعلناه في الحالة الأولى: // nameDisplayCheck() نعرّف الدالة function nameDisplayCheck() { // نتحقق من تخزين عنصر الاسم في مخزن ويب if (localStorage.getItem("name")) { // نعرض رسالة الترحيب المخصصة إن كان الأمر كذلك const name = localStorage.getItem("name"); h1.textContent = `Welcome, ${name}`; personalGreeting.textContent = `Welcome to our website, ${name}! We hope you have fun while you are here.`; //`forget` من الاستمارة ونعرض الجزء `remember` نخفي الجزء forgetDiv.style.display = "block"; rememberDiv.style.display = "none"; } else { // إن لم يكن الايم مخزنًانعرض الرسالة اﻷصلية h1.textContent = "Welcome to our website "; personalGreeting.textContent = "Welcome to our website. We hope you have fun while you are here."; //`remember` من الاستمارة ونعرض الجزء `forget` نخفي الجزء forgetDiv.style.display = "none"; rememberDiv.style.display = "block"; } } ننفذ الدالة ()nameDisplayCheck عند اكتمال تحميل الصفحة. لأن الرسالة المخصصة لن تظهر إن لم نفعل ذلك خلال تحميل الصفحة بشكل متكرر. أضف ما يلي إلى آخر الشيفرة: nameDisplayCheck(); وهكذا يكون مثالنا قد انتهي، وبإمكان الاطلاع في أي وقت على النسخة المكتملة منه على جيت-هاب. ملاحظة: تمنع السمة defer في السطر التالي من تنفيذ شيفرة جافا سكريبت حتى اكتمال تحميل الصفحة <script src="index.js" defer></script>. الخلاصة تعرفنا في هذا المقال على أساسيات تخزين البيانات في طرف العميل من خلال واجهات برمجية مخصصة مثل Web Storage API و IndexedDB. كما شرحنا مخازن ويب Web Storage وطريقة العمل معها من خلال مثال تطبيقي بسيط يعرض أساسيات العمل مع هذه الواجهة البرمجية. ترجمة -وبتصرف- للجزء الأول من مقال: Client-side storage اقرأ أيضًا المقال السابق: الواجهات البرمجية للتعامل مع الصوت والفيديو في جافا سكريبت معالجة المشاكل الشائعة للتوافق مع المتصفحات في شيفرة جافاسكربت الواجهة البرمجية fetch في JavaScript تخزين البيانات محليًا في المتصفح عبر قاعدة البيانات IndexedDB
-
تضم لغة HTML عناصر مخصصة لتضمين الوسائط المتعددة إلى صفحاتك مثل <audio> و <video> والتي تأتي مزوّدة بواجهة برمجية مخصصة للتحكم بتشغيلها وتقديمها وتأخيرها. لهذا سنتعرف في هذا المقال على طرق تنفيذ بعض المهام الشائعة كإنشاء أدوات تحكم متخصصة باستخدام الواجهة البرمجية HTMLMediaElement التي توفر عدة ميزات للتحكم في تشغيل الصوت و الفيديو برمجيًا . ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات Prototype في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. عناصر HTML الخاصة بالصوت والفيديو يسمح العنصران <video> و <audio> بإدراج مقاطع الصوت و الفيديو في صفحات الويب، وستبدو الطريقة النمطية ﻹنجاز اﻷمر كالتالي: <video controls> <source src="rabbit320.mp4" type="video/mp4" /> <source src="rabbit320.webm" type="video/webm" /> <p> Your browser doesn't support HTML video. Here is a <a href="rabbit320.mp4">link to the video</a> instead. </p> </video> تعرض الشيفرة السابقة مشغل فيديو في صفحتك كالتالي: وأكثر ما يثير الاهتمام في الشيفرة السابقة هي السمة controls التي تعرض أدوات التحكم الافتراضية مع مشغل الفيديو، وإن لم تستخدم هذه السمة فلن ترى عناصر التحكم على المشغل: لهذه اﻷدوات إيجابيات، لكن من أبرز مشكلاتها هي اختلافها من متصفح إلى آخر وهذا أمر مربك إن حاولت دعم عدة متصفحات في شيفرتك. ومن المشكلات الكبيرة أيضًا أن أدوات التحكم اﻷصلية في معظم المتصفحات لا تدعم التحكم من خلال لوحة المفاتيح. ويمكن حل كلتا المشكلتين السابقتين بإخفاء أدوات التحكم الأصلية (عن طريق إزالة السمة controls) وبرمجة أدوات تحكم خاصة بك باستخدام HTML و CSS وجافا سكريبت. وسنلقي نظرة في اﻷقسام التالية على اﻷدوات البسيطة المتاحة لهذا الغرض. الواجهة البرمجية HTMLMediaElement تزّودك بعض مواصفات الواجهة البرمجية HTMLMediaElement بميزات تسمح لك بالتحكم في مشغلات الصوت و الفيديو برمجيًا مثل التوابع ()HTMLMediaElement.play و ()HTMLMediaElement.pause وغيرها. وهاتان الواجهتان متاحتان للاستخدام مع العنصرين <audio> و<video> فهما متطابقان من ناحية العمل. لهذا سنوضح طريقة استخدام هذه الواجهات من خلال المثال التالي: يبدو مثالنا عندما يكتمل مشابهًا للتالي: نقطة الانطلاق حتى نبدأ العمل، عليك تنزيل الملف المضغوط الخاص بالمثال ثم تستخرج محتوياته في مجلد جديد على حاسوبك. أما إن حمّلت مستودع اﻷمثلة بأكمله، ستجد المثال في المسار javascript/apis/video-audio/start/. إن حمّلت المثال على متصفحك سترى مشغل فيديو HTML نمطي مع أدوات التحكم الافتراضية. التعرف على ملف HTML عندما تفتح الملف index.html سترى عددًا من العناصر، وستلاحظ أن معظم الشيفرة تدور حول مشغل الفيديو وأدوات التحكم الخاصة به: <div class="player"> <video controls> <source src="video/sintel-short.mp4" type="video/mp4" /> <source src="video/sintel-short.webm" type="video/webm" /> <!-- fallback content here --> </video> <div class="controls"> <button class="play" data-icon="P" aria-label="play pause toggle"></button> <button class="stop" data-icon="S" aria-label="stop"></button> <div class="timer"> <div></div> <span aria-label="timer">00:00</span> </div> <button class="rwd" data-icon="B" aria-label="rewind"></button> <button class="fwd" data-icon="F" aria-label="fast forward"></button> </div> </div> وضعنا المشغل بأكمله داخل العنصر <div> حتى يُنسّق بالكامل عند الحاجة. يحتوي العنصر <video> على عنصرين من النوع <source> كي نتمكن من تحميل تنسيقات مختلفة لمقطع الفيديو وفقًا للمتصفح الذي نستخدمه. وربما تكون أدوات تحكم HTML هي اﻷكثر أهمية هنا: لدينا أربعة أزرار <button> لتشغيل وإيقاف العرض مؤقتًا واﻹطفاء والتقديم للأمام والعودة للخلف. خصصنا لكل زر سمات هي اسم صنف التنسيق class و data-icon لتحديد اﻷيقونة التي تُعرض على الزر (سنرى كيف ننفذ ذلك لاحقًا) و aria-label لتقديم وصف مفهوم عن عمل كل زر، وذلك لأننا لا نقدم عنوانًا مقروءًا ضمن وسم العنصر. ويُقرأ محتوى السمة aria-label من قبل قارئات الشاشة عندما ينتقل التركيز إلى العناصر التي تمتلك هذه السمة. تضم الصفحة أيضًا مؤقتًا ضمن عنصر <div> يعرض الوقت المنقضي من مقطع الفيديو، ولتحسين تجربة المستخدم، زودنا المثال بآليتين لتحديد الوقت المنقضي: الأولى ضمن عنصر <span> يعطي الوقت المنقضي بالدقائق والثواني، والثانية ضمن عنصر <div> يضم شريط أفقي يزداد طوله عندما يتقدم عرض الفيديو. ولكي تأخذ فكرة عما ستكونه الصفحة بشكلها الكامل ألق نظرة هنا.. التعرف على ملف CSS افتح اﻵن ملف CSS وألقِ نظرة عليه. لا يبدو هذا الملف معقدًا، لكننا سنشير إلى النقاط المهمة فيه. لاحظ بداية القاعدة controls.: .controls { visibility: hidden; opacity: 0.5; width: 400px; border-radius: 10px; position: absolute; bottom: 20px; left: 50%; margin-left: -200px; background-color: black; box-shadow: 3px 3px 5px black; transition: 1s all; display: flex; } .player:hover .controls, .player:focus-within .controls { opacity: 1; } بدأنا بالخاصية visibility لمجموعة أدوات التحكم المخصصة وضبطناها على hidden، لكننا سنضبطها لاحقًا عبر جافا سكريبت لتكون visible ونزيل السمة controls من العنصر <video>. وذلك كي يبقى المستخدم قادرًا على تشغيل الفيديو باستخدام أدوات التحكم الافتراضية في حال فشل تحميل شيفرة جافا سكريبت لسبب ما. منحنا أدوات التحكم قتامة افتراضية opacity قيمتها 0.5، كي لا تشتت الانتباه عند عرض الفيديو، لكن عندما تمرر الفأرة فوق المشغّل أو تمنحه تركيز الدخل ستكون اﻷدوات كاملة القتامة. نرتب اﻷزرار ضمن شريط التحكم باستخدام تخطيط الصندوق المرن display: flex لتسهيل ضبط مواقعها. لنلق نظرة تاليًا على أيقونات اﻷزرار: @font-face { font-family: "HeydingsControlsRegular"; src: url("fonts/heydings_controls-webfont.eot"); src: url("fonts/heydings_controls-webfont.eot?#iefix") format("embedded-opentype"), url("fonts/heydings_controls-webfont.woff") format("woff"), url("fonts/heydings_controls-webfont.ttf") format("truetype"); font-weight: normal; font-style: normal; } button:before { font-family: HeydingsControlsRegular; font-size: 20px; position: relative; content: attr(data-icon); color: #aaa; text-shadow: 1px 1px 0px black; } استخدمنا بداية في أعلى ملف CSS الكتلة font-face@ لاستيراد خط ويب مخصص، وهذا الخط هو عبارة عن أيقونات بدلًا من الحرف اﻷبجدية وتستخدم لعرض أيقونات مختلفة يشيع استخدامها في التطبيقات. نولد بعد ذلك محتوىً خاصًا لعرض اﻷيقونات على كل زر: نستخدم المحدد before:: لعرض المحتوى قبل كل زر <button>. نستخدم الخاصية content لضبط المحتوى الذي يعرض في كل حالة ليكون نفسه محتوى السمة data-icon. ففي حالة زر التشغيل مثلًا، سيكون محتوى السمة data-icon هو المحرف P (بشكله الكبير). نطبق خط ويب السابق على اﻷزرار باستخدام الخاصية font-family، وسيكون الحرف P في هذا الخط عمليًا أيقونة التشغيل، وهكذا ستظهر على زر التشغيل أيقونة التشغيل. إن الخطوط التي تعرض أيقونات جميلة ومفيدة ﻷسباب عديدة منها تقليل عدد طلبات HTTP لانك لن تحتاج إلى تحميل تلك اﻷيقونات على شكل ملفات صور، إضافة إلى إمكانية تكبير وتصغير اﻷيقونات بدقة وكذلك إمكانية استخدام خاصيات نصية لتنسيق تلك اﻷيقونات مثل color و text-shadow. لنلق نظرة أيضًا على تنسيق المؤقت الزمني: .timer { line-height: 38px; font-size: 10px; font-family: monospace; text-shadow: 1px 1px 0px black; color: white; flex: 5; position: relative; } .timer div { position: absolute; background-color: rgb(255 255 255 / 20%); left: 0; top: 0; width: 0; height: 38px; z-index: 2; } .timer span { position: absolute; z-index: 3; left: 19px; } ضبطنا قيمة الخاصية flex للعنصر timer. الخارجي على القيمة 5 لتشغل أكبر مساحة من شريط التحكم. كما ضبطنا خاصية الموقع بالشكل position:relative كي نتمكن من ضبط العناصر ضمن العنصر الخارجي كما نشاء وبالنسبة إلى حدوده وليس حدود العنصر <body>. ضبطنا موقع العنصر <div> الداخلي ليكون مطلقًا position:absolute لكي يظهر مباشرة في أعلى العنصر <div> الخارجي. كما ضبطنا قيمة اتساع العنصر على الشكل width:0 كي لا يُرى العنصر إطلاقًا. وعندما يبدأ العرض نستخدم جافا سكريبت لزيادة اتساع العنصر. ضطنا موقع العنصر <span> ليكون مطلقًا وبالتالي سيكون بالقرب من الطرف اﻷيسر لشريط المؤقت. ضبطنا خاصية العلو z-index للكائن <div> الداخلي والكائن <span> كي يُعرض الشريط الزمني في الأعلى وتحته العنصر <div> الداخلي، ونضمن بذلك أنك سترى كل المعلومات ولن يَحجِب صندوق آخر. إنجاز شيفرة جافا سكريبت بعد أن حضرنا واجهتي HTML و CSS، لا بد من كتاب شيفرة اﻷزرار المخصصة للتحكم بمشغل الفيديو. أنشئ ملف جافا سكريبت جديد في نفس المجلد الذي يضم الملف index.html وسمِّه custom-player.js. ضع الشيفرة التالية أعلى الملف: const media = document.querySelector("video"); const controls = document.querySelector(".controls"); const play = document.querySelector(".play"); const stop = document.querySelector(".stop"); const rwd = document.querySelector(".rwd"); const fwd = document.querySelector(".fwd"); const timerWrapper = document.querySelector(".timer"); const timer = document.querySelector(".timer span"); const timerBar = document.querySelector(".timer div"); أنشأنا في الشيفرة السابقة ثوابت لتكون مراجع إلى الكائنات التي نريد التعامل معها، ولدينا ثلاثة مجموعات: العنصر <video> وشريط التحكم. أزرار التحكم "تشغيل/إيقاف مؤقت play/pause" و "للأمام rewind" و "للخلف fast forward". غلاف المؤقت الخارجي <div> والعنصر <span> الذي يعرض المؤقت والعنصر <div> الخارجي الذي يزداد اتساعه عندما يتقدم الفيديو. ضع اﻵن الشيفرة التالية تحت سابقتها: media.removeAttribute("controls"); controls.style.visibility = "visible"; تزيل الشيفرة السابقة مشغل الفيديو الافتراضي الخاص بالمتصفح ويُظهر أدوات التحكم المخصصة: تشغيل وإيقاف الفيديو مؤقتًا سننجز اﻵن شيفرة التحكم بزر التشغيل و اﻹيقاف المؤقت: أضف بداية الشيفرة التالية في أسفل الشيفرة كي تُستدعى الدالة ()playPauseMedia عند النقر على زر التشغيل: play.addEventListener("click", playPauseMedia); ولتعريف الدالة ()playPauseMedia، أضف الشيفرة التالية إلى أسفل الشيفرة السابقة: function playPauseMedia() { if (media.paused) { play.setAttribute("data-icon", "u"); media.play(); } else { play.setAttribute("data-icon", "P"); media.pause(); } } نستخدم هنا عبارة if للتحقق من توقف تشغيل الفيديو، وتعيد الخاصية HTMLMediaElement.paused القيمة true عند توقف التشغيل مؤقتًا بما في ذلك عند ضبطته على القيمة 0 بعد تحميله أول مرة. عند ذلك نضبط قيمة السمة data-icon لزر التشغيل على u التي تعرض بدورها أيقونة التشغيل المؤقت عليه، وتستدعي التابع ()HTMLMediaElement.play لتشغيل الفيديو. وعند النقر على الزر مرة ثانية سيعود الزر كما كان، إذ تظهر أيقونة التشغيل وسيتوقف الفيديو بتنفيذ التابع ()HTMLMediaElement.paused. إيقاف عرض الفيديو نضيف بداية الشيفرة التي تتعامل مع إيقاف تشغيل الفيديو تحت الشيفرة السابقة: stop.addEventListener("click", stopMedia); media.addEventListener("ended", stopMedia); يضيف سطري الشيفرة مترصدي أحداث addEventListener للتعامل مع الحدث click الذي يوقف تشغيل الفيديو بتنفيذ الدالة ()stopMedia عند النقر على زر اﻹيقاف. ولا بد من إيقاف التشغيل أيضًا عند إنتهاء المقطع، لهذا نترصد أيضًا الحدث ended من خلال مترصد الحدث الثاني والذي ينفذ أيضًا الدالة ()stopMedia عند انتهاء مقطع الفيديو. نعرّف تاليًا الدالة ()stopMedia، بإضاف اﻷسطر التالية بعد الدالة ()playpauseMedia: function stopMedia() { media.pause(); media.currentTime = 0; play.setAttribute("data-icon", "P"); } وبما أن الواجهة البرمجية HTMLMediaElement لا تقدم تابعًا مخصصًا ﻹيقاف عرض الفيديو، سنستخدم التابع ()pause ﻹيقاف التشغيل مؤقتًا ثم نضبط قيمة الخاصية currentTime على القيمة 0 ليعود الفيديو إلى البداية. فضبط قيمة هذه الخاصية (بالثواني) سينقل الموقع الحالي للفيديو إلى النقطة الزمنية المحددة. يبقى علينا فقط إظهار أيقونة التشغيل على زر التشغيل. وبصرف النظر عن وضع الفيديو سواءً كان قيد التشغيل أو أوقف مؤقتًا عند النقر على زر إيقاف التشغيل "Stop"، لابد أن تُظهر أن المشغل جاهز للعمل مجددًا. التنقل بالفيديو إلى اﻷمام والخلف ستجد العديد من الطرق لتقديم أو إعادة المشغل إلى نقطة زمنية محددة، وما سنعرضه حاليًا طريقة معقدة نوعًا ما في تنفيذ الأمر لتفادي اﻷخطاء التي قد تحدث عند النقر على أزرار مختلفة بترتيب غير متوقع. أضف مترصدي الحدث التاليين تحت تعريف المترصدين السابقين: rwd.addEventListener("click", mediaBackward); fwd.addEventListener("click", mediaForward); أضف الدالتين ()mediaBackWard و ()mediaForWard التاليتين تحت الدوال السابقة، وستصبح الشيفرة كالتالي: let intervalFwd; let intervalRwd; function mediaBackward() { clearInterval(intervalFwd); fwd.classList.remove("active"); if (rwd.classList.contains("active")) { rwd.classList.remove("active"); clearInterval(intervalRwd); media.play(); } else { rwd.classList.add("active"); media.pause(); intervalRwd = setInterval(windBackward, 200); } } function mediaForward() { clearInterval(intervalRwd); rwd.classList.remove("active"); if (fwd.classList.contains("active")) { fwd.classList.remove("active"); clearInterval(intervalFwd); media.play(); } else { fwd.classList.add("active"); media.pause(); intervalFwd = setInterval(windForward, 200); } } هيأنا أولًا متغيرين intervalFwd و intervlRwd وسترى عملهما لاحقًا، كما ستلاحظ أن عمل الدالتين ()mediaBackWard و ()mediaForWard متطابق لكن بترتيب معكوس: يجب تصفير اﻷصناف والمجالات التي ضبطناها عند تنفيذ وظيفة التقديم السريع للأمام، لأننا لو نقرنا على زر rwd بعد النقر على الرز fwd من المفترض أن نلغي أي إعدادات خاصة بالتقديم السريع للمشغل fwd واستبدالها بإعدادت التراجع rwd، لأن المشغل سيخفق لو حاولنا النقر على كلا الزرين في نفس الوقت. استخدمنا عبارة if للتحقق من ضبط صنف الزر rwd ليكون active في إشارة إلى أن الزر قد نُقر للتو. ويتمتع كل عنصر بالخاصية classlist، وهي خاصية مفيدة تضم كل الأصناف التي يمتلكها العنصر وتقدم توابع ﻹزالة وإضافة اﻷصناف. وقد استخدمنا التابع ()classList.contains للتحقق من جود الصنف active ضمن أصناف الزر، وتعيد قيمة منطقية true/false. في حال كان active أحد أصناف العنصر rwd نزيله باستخدام التابع ()classList.remove ثم نلغي قيمة الفاصل الزمني الذي ضُبط مسبقًا عندما نقرنا على الزر ومن ثم نستخدم التابع ()HTMLMediaElement.play ﻹلغاء العودة للخلف وتشغيل الفيديو بشكل طبيعي. إن لم يمتلك الزر تلك الخاصية نضيفها إليه باستخدام التابع ()clasList.add ومن ثم نوقف الفيديو مؤقتًا باستخدام التابع ()HTMLMediaElement.pause. نضبط بعدها قيمة المتغير intervalRwd ليعادل القيمة المعادة من استدعاء الدالة ()setInterval. تُحدد هذه الدالة فترة زمنية معينة تنفذ بعد انقضائها الدالة التي تُمرر إليها كمعامل أول أما الفترة الزمنية فيحددها المعامل الثاني بالميلي ثانية. وهنا ننفذ الدالة كل 200 ميلي ثانية كي نعيد مشغل الفيديو إلى الخلف بوتيرة ثابتة. ولكي نوقف تنفيذ الدالة ()setInterval نستدعي الدالة ()clearIterval ممرين لها المتغير intervalRwd (الذي أسندت إليه الدالة ()setInterval). عرفنا أخيرًا الدالة ()windBackwrd والدالة ()windForward اللتان تمررا إلى ()setInterval، لهذا أضف الشيفرة التالية تحت الدوال السابقة: function windBackward() { if (media.currentTime <= 3) { rwd.classList.remove("active"); clearInterval(intervalRwd); stopMedia(); } else { media.currentTime -= 3; } } function windForward() { if (media.currentTime >= media.duration - 3) { fwd.classList.remove("active"); clearInterval(intervalFwd); stopMedia(); } else { media.currentTime += 3; } } سنشرح تاليًا الدالة الأولى فقط لكون الدالتين متطابقتان من ناحية الشيفرة ومتعاكستان عملًا. وما فعلناه في الدالة ()windBackward هو التالي (تذكر أنه بمجرد تفعيل الفاصل الزمني الذي سيتراجع فيه المشغل إلى الخلف ستُستدعى هذه الدالة كل 200 ميلي ثانية): نبدأ الشيفرة بالعبارة if التي تتحقق أن المدة المنقضية من المقطع أقل من 3 ثانية، أي سيعود المشغل عند تراجعه إلى ما قبل نقطة البداية، وهذا ما يسبب سلوكًا غريبًا للمشغل. فلو كانت الحالة كذلك، نوقف تشغيل المقطع باستدعاء الدالة ()stopMedia ومن ثم نزيل الصنف active من قائمة أصناف الزر rwd ونمحي قيمة المتغير intervalRwd ﻹيقاف عملية التراجع. وفي حال أهملنا هذه الخطوة اﻷخيرة سيستمر المشغل بالتراجع إلى ما لا نهاية. إن كان الوقت المنقضي أكبر من 3 ثانية، نزيل ثلاث ثوانٍ من الوقت الحالي باستخدام التعليمة media.currentTime -=3، أي نعيد مشغل الفيديو إلى ما قبل ثلاث ثوان وذلك كل 200 ميلي ثانية. تحديث الوقت المنقضي آخر ما سننفذه ﻹنجاز أدوات التحكم المخصصة بمشغل الفيديو هو تحديد الوقت المنقضي من زمن المقطع. لذا نشغّل دالة تحدّث الوقت الذي نعرضه في كل مرة يقع فيها الحدث timeupdate المرتبط بالعنصر <video>. أما تواتر عملية وقوع هذا الحدث، فتعتمد على المتصفح وقوة معالج جهازك. أضف اﻵن السطر التالي الذي يعرّف مترصد تحديث زمن التشغيل: media.addEventListener("timeupdate", setTime); ولتعريف الدالة ()setTime، أضف مايلي في أسفل ملف جافا سكريبت: function setTime() { const minutes = Math.floor(media.currentTime / 60); const seconds = Math.floor(media.currentTime - minutes * 60); const minuteValue = minutes.toString().padStart(2, "0"); const secondValue = seconds.toString().padStart(2, "0"); const mediaTime = `${minuteValue}:${secondValue}`; timer.textContent = mediaTime; const barLength = timerWrapper.clientWidth * (media.currentTime / media.duration); timerBar.style.width = `${barLength}px`; } هذه الدالة طويلة، لهذا سنناقشها خطوة خطوة: نعمل بداية على تحديد الدقائق والثواني المنقضية من خلال قيمة HTMLMediaElement.currentTime. نهيئ بعد ذلك متغيرين إضافيين هما minuteValue و secondValue، ثم نستخدم التابع ()padStart لكي نمثّل قيمة الدقائق والثواني على شكل محرفين فقط حتى لو كانت القيمة رقمًا وحيدًا. أما الوقت الفعلي الذي سيُعرض فهو قيمة المتغير minuteValue تليه نقطتان متعامدتان ثم قيمة المتغير secondValue. نضبط قيمة المؤقت Node.textContent لتعادل قيمة الوقت الحالي وبالتالي ستُعرض هذه القيمة على واجهة المشغّل. نحدد طول عنصر <div> الداخلي (الذي سيعرض شريط تقدم مقطع الفيديو) من خلال تحديد اتساع عنصر <div> الخارجي (نأخذها من الخاصية clientWidth) ومن ثم ضرب هذه القيمة بالوقت الحالي HTMLMediaElement.currentTime ونقسم على المدة الكلية لمقطع الفيديو HTMLMediaElement.duration. نضبط قيمة اتساع العنصر <div> الداخلي ليعادل طول شريط تتبع تقدم الفيديو بعد إضافة القيمة "px" كي تشير إلى الاتساع مقدرًا بالبكسل. إصلاح مشكلات التشغيل واﻹيقاف المؤقت للفيديو هنالك مشكلة واحدة ولا بد من حلها. فعند النقر على زر التشغيل أو إيقاف الفيديو وزر التقديم أو التراجع، فلن يعمل هذا الزر! وما علينا إصلاحه هنا هو إلغاء وظائف التقدم أو التراجع عند النقر على زر التشغيل لمتابع العمل كما هو متوقع، وهذا أمر سهل. أضف بداية الشيفرة التالية ضمن الدالة ()stopMedia: rwd.classList.remove("active"); fwd.classList.remove("active"); clearInterval(intervalRwd); clearInterval(intervalFwd); أضف اﻵن نفس اﻷسطر في بداية الدالة ()playpauseMedia (وقبل عبارة if). يمكنك اﻵن ازالة نفس الأسطر من الدالتين ()windBackwrd و ()windForward لأننا وضعنا هذه الوظيفة المشتركة بينهما في الدالة ()stopMedia. ملاحظة: يمكنك تحسين فعالية الشيفرة أكثر من خلال إنشاء دالة منفصلة تضم اﻷسطر السابقة ومن ثم استدعاء هذه الدالة عند الحاجة بدلًا من تكرار اﻷسطر عدة مرات في الشيفرة. الخلاصة تعلمنا في هذا المقال ما يكفي عن الواجهة HTMLMediaElement التي تقدم كما كبيرًا من الوظائف ﻹنشاء مشغل وسائط متعددة، وما رأيناه هو مجرد جزء ضئيل من إمكانياتها. إليك أخيرًا بعض الاقتراحات التي تساعد في تحسين مثالنا: يختل عمل المؤقت إن كانت مدة المقطع أكثر من ساعة (فلن يعرض الساعات بل الدقائق والثواني فقط). هل يمكنك تعديل الشيفرة لتعرض الساعات أيضًا؟ يمتلك العنصر <audio> نفس وظائف HTMLMediaElement وبالتالي يمكنك تشغيل المقاطع الصوتية بسهولة، جرب لك. هل يمكنك إيجاد طريقة الانتقال إلى مكان ما من المقطع بالنقر على شريط تقدم الفيديو (العنصر <div> الداخلي). وكتلميح يمكنك إيجاد x و y لزوايا الشريط من خلال التابع ()getBoundingClientRect وإيجاد إحداثيي موقع مؤشر الفأرة من خلال كائن الحدث الذي ينتج عن حدث النقر على المستند. إليك مثالًا: document.onclick = function (e) { console.log(e.x, e.y); }; ترجمة -وبتصرف- لمقال: Video and audio APIs اقرأ أيضًا المقال السابق: العمل مع واجهات الرسوميات البرمجية في جافا سكريبت: الحلقات والرسوم المتحركة إضافة مقاطع الفيديو عبر العنصر <video> في HTML5 إضافة محتوى سمعي ومرئي في صفحة HTML تأثيرات التمرير في صفحات الويب باستخدام Javascript وCSS
-
غطينا في مقالنا السابق بعض أساسيات الرسوم ثنائية البعد ضمن العنصر <canvas>، لكنك لن تلمس عمليًا فعالية هذا العنصر ما لم ترى قدرته على تحريك الرسوم. إذ يقدم هذا العنصر إمكانية إنشاء صور ورسومات باستخدام سكربتات مخصصة، لكن إن لم يكن هدفك تحريك أي شيء، عليك استخدام صور ثابتة لتوفر على نفسك عناء العمل. إنشاء الحلقات في Canvas لن يكون صعبًا التعامل مع الحلقات في <canvas>، وما عليك سوى استخدام تعليمات هذا العنصر (التوابع والخاصيات) داخل حلقة for أو غيرها من الحلقات كغيرها من شيفرات جافا سكريبت. لهذا سنعطي مثالًا تطبيقيًا عن الموضوع: أنشئ نسخة جديدة عن القالب الرسومي الذي أنشأناه في المقال السابق وافتحه ضمن محرر الشيفرة الذي تستخدمه. أضف السطر التالي إلى أسفل ملف جافا سكريبت، ويتضمن هذا السطر تابعًا جديدًا هو ()translate الذي يحرّك نقطة المبدأ في لوحة الرسم: ctx.translate(width / 2, height / 2); يسبب ذلك تحريك نقطة المبدأ (0,0) إلى مركز اللوحة بدلًا من كونها في الزاوية العليا اليسارية. ولهذا اﻷمر فائدته في الكثير من الحالات كما في مثالنا، إذ نريد أن يكون التصميم منسوبًا إلى مركز اللوحة. أضف اﻵن الشيفرة التالية: function degToRad(degrees) { return (degrees * Math.PI) / 180; } function rand(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } let length = 250; let moveOffset = 20; for (let i = 0; i < length; i++) {} سننجز هنا نفس الدالة ()degToRad التي رأيناها في مثال المثلث في مقالنا السابق، ونستخدم الدالة التي تعيد رقمًا عشوائيًا بين حدين علوي وسفلي معينين. إضافة إلى ذلك ننشئ المتغيرين length و moveOffset (سنرحهما لاحقًا)، كما نستخدم حلقة for فارغة. ما سنفعله هنا هو رسم شيء ما ضمن اللوحة لكن ضمن الحلقة for ثم نكرر ما نفعله عدة مرات. أضف اﻵن الشيفرة التالية داخل الحلقة for: ctx.fillStyle = `rgb(${255 - length} 0 ${255 - length} / 90%)`; ctx.beginPath(); ctx.moveTo(moveOffset, moveOffset); ctx.lineTo(moveOffset + length, moveOffset); const triHeight = (length / 2) * Math.tan(degToRad(60)); ctx.lineTo(moveOffset + length / 2, moveOffset + triHeight); ctx.lineTo(moveOffset, moveOffset); ctx.fill(); length--; moveOffset += 0.7; ctx.rotate(degToRad(5)); في كل تكرار: نضبط fillStyle ليكون ظلًا بنفسجيًا شفافًا قليلًا ويتغير كل مرة وفقًا لقيمة المتغير length. وكما سترى سيقل الطول في كل تكرار وبالتالي سيكون أثر ذلك على اللون الذي يصبح أكثر لمعانًا مع كل مثلث يُرسم على التتابع. نبدأ مسار الرسم. ننقل قلم الرسم إلى اﻹحداثي (moveOffset, moveOffset) ويُعرّف هذا المتغير المسافة التي يجب أن نحركها في كل مرة نرسم فيها مثلًا جديدًا. نرسم خطًا إلى اﻹحداثي (moveOffset+length, moveOffset) وهذا الخط طوله قيمة المتغير length ويوازي المحور x. نحسب ارتفاع المثلث كما فعلنا في المقال السابق. نرسم خطًا نحو رأس المثلث المتجه نحو الأسفل ومن ثم خطا إلى نقطة بداية المثلث. نستدعى التابع ()fill لملء المثلث. نحدّث قيمة المتغيرات التي تصف سلسلة المثلثات التي نرسمها كي نتمكن من رسم المثلث التالي. نخفض قيمة المتغير length بمقدار 1 وبالتالي سيصغر المثلث كل مرة. كما نزيد قيمة moveOffset بمقدار صغير كي يكون كل مثلث أبعد قليلًا عن سابقه. ونستخدم التابع ()rotate الذي يسمح لنا تدوير اللوحة بأكملها، حيث ندورها بمقدار خمس درجات قبل أن نرسم المثلث التالي. هذا كل ما في اﻷمر، وستبدو نتيجة مثالنا كالتالي: نشجعك اﻵن على إجراء تغييرات في هذا المثال وتجرّب ما تعلمته. إذ يمكنك مثلًا: أن ترسم مربعًا أو قوسًا بدلًا من المثلث. أن تغير قيمة المتغير length أو moveOffset. تغيير اﻷرقام العشوائية التي نولدها باستخدام الدالة ()rand التي وضعناها في الشيفرة السابقة ولم نستخدمها. ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الرسوم المتحركة ما فعلناه في مثالنا السابق أمر جميل، لكن ما تحتاجه حقيقة حلقة ثابتة تستمر وتستمر في أي تطبيق فعلي يعتمد على عنصر <canvs> (مثل اﻷلعاب الإلكترونية، والعرض البصري المباشر). فلو فكرت بلوحة الرسم على أنها فيلم سينمائي، ستعرف أنه عليك تحديث اﻹطارات المعروضة بشكل مستمر وبمعدل 60 إطار في الثانية (القيمة المثالية) كي يبدو المشهد المتحرك ناعمًا ومريحًا للعين البشرية. تقدم لك جافا سكريبت مجموعة من الدوال التي تسمح لك بتنفيذ دوال أخرى بشكل متكرر عدة مرات في الثانية الواحدة. ونجد أنسب هذه الدوال لمثالنا الدالة ()window.requestAnimationFrame التي تأخذ معاملًا واحدًا وهو اسم الدالة التي نريد استدعاءها. فإن رسمت هذه الدالة تحديثًا جديدًا من سلسلة الرسوم المتحركة التي سنعرضها، عليك حينها استدعاء الدالة ()window.requestAnimationFrame مجددًا قبل نهاية الدالة المنفذة للرسم كي تستمر حلقة الرسم. تنتهي الحلقة عندما تتوقف عن استدعاء ()window.requestAnimationFrame أو عند استدعاء الدالة ()window.caancelAnimationFrame بعد استدعاء ()window.requestAnimationFrame وقبل البدء برسم اﻹطار (الذي سيكون اﻷخير حينها). ملاحظة:من الممارسات الجيدة استدعاء الدالة ()window.caancelAnimationFrame من شيفرتك الرئيسية عند الانتهاء من الرسم، كي تضمن عدم وجود أية تحديثات أخرى يمكن أن تُعرض على اللوحة. يُنفّذ المتصفح التفاصيل المعقدة للعملية مثل تحريك الرسوم بمعدل ثابت، والتأكد من عدم تنفيذ رسوميات لا تُرى. ولكي تتعرف على عمل المتصفح، سنلقي نظرة على مثالنا السابق "الكرات القافزة المرتدة" (يمكنك تجربتها مباشرة أو الاطلاع على الشيفرة المصدرية😞 function loop() { ctx.fillStyle = "rgb(0 0 0 / 25%)"; ctx.fillRect(0, 0, width, height); for (const ball of balls) { ball.draw(); ball.update(); ball.collisionDetect(); } requestAnimationFrame(loop); } loop(); نشغل الدالة()loop مرة واحدة في آخر الشيفرة لنبدأ دورة الرسوميات برسم اﻹطار المتحرك الأول ومن ثم تتولى الدالة ()loop مسؤولية استدعاء الدالة (loop(requestAnimationframe التي ستحضر وترسم اﻹطار الثاني من الرسوم المتحركة وتكرر اﻷمر حتى النهاية. وتجدر ملاحظة أننا نمسح اللوحة تمامًا عند رسم كل إطار ومن ثم نعيد رسم كل شيء. إذ نرسم كل كرة ونحدّث موقعها ونتحقق فيما لو اصطدمت بكرة أخرى. وبمجرد أن ترسم شيئًا الى اللوحة، لن تتمكن من تعديله بشكل مستقل كما هو الحال مع عناصر شجرة DOM. ولن تستطيع أيضًا تحريك كل كرة بمفردها ضمن اللوحة، لأنك بمجرد رسم الكرة ستصبح جزءًا من اللوحة وليست كائنًا مستقلًا يمكنك التعامل معه. لهذا عليك مسح وإعادة رسم العناصر، إما بمسح اﻹطار بأكمله وإعادة رسم كل شيء أو كتابة شيفرة تحد تمامًا الجزء الذي يجب مسحه وبالتالي إعادة الرسم في المنطقة المحددة من اللوحة. لهذا يُعد تحسين الرسوم المتحركة اختصاصًا برمجيًا بحد ذاته، ويتطلب استعمال العديد من التقنيات الذكية المتاحة. لكن هذا اﻷمر خارج إطار مقالنا والمثال الذي نعمل عليه. وعمومًا، تتطلب عملية تنفيذ رسوم متحركة ضمن اللوحة الخطوات التالية: مسح محتوى اللوحة باستخدام ()fillRect أو ()clearRect. تخزين الحالة عند الضرورة باستخدام ()save، وذلك عندما تحتاج إلى حفظ اﻹعدادات التي حدّثتها في اللوحة قبل الاستمرار، وللأمر فائدته في التطبيقات المتقدمة. رسم الأشياء التي تريد تحريكها. استعادة الإعدادات التي خزنتها في الخطوة الثانية باستخدام ()restore. استدعاء الدالة ()requestAnimationFrame لجدولة رسم اﻹطار التالي من الرسم المتحرك. ملاحظة: لن نغطي الدالتين ()save و ()restore في مقالنا. تحريك شخصية بسيطة سننشئ اﻵن رسمًا متحركًا خاصًا بنا، تمشي الشخصية المقتبسة من أحد ألعاب الحاسوب القديمة خلال عرض الرسم المتحرك عبر الشاشة. أنشئ نسخة جديدة من القالب الذي نستخدمه في أمثلتنا وافتحه في محرر اﻷلعاب. حدّث شيفرة HTML حتى تعكس الصورة: <canvas class="myCanvas"> <p>A man walking.</p> </canvas> أضف اﻷسطر التالية إلى نهاية ملف جافا سكريبت كي تكون نقطة المبدأ منتصف لوحة الرسم.: ctx.translate(width / 2, height / 2); ننشئ تاليًا كائن HTMLImgeElement ونضبط قيمة الخاصية src له كي تكون عنوان الصورة التي نريد تحميلها ثم نضيف معالجًا للحدث onload الذي يستدعي الدالة ()draw عند اكتمال تحميل الصورة: const image = new Image(); image.src = "walk-right.png"; image.onload = draw; سنضيف اﻵن بعض المتغيرات التي تتعقب موقع الشخصية في اللوحة وعدد الشخصيات التي نرسمها على اللوحة: let sprite = 0; let posX = 0; سنشرح تاليًا صورة الشخصية المأخوذة من التطبيق Walking cycle using CSS animation تتضمن الصورة ست شخصيات تمثل تسلسل حركة الشخصية. عرض صورة كل شخصية 102 بكسل وارتفاعها 148 بكسل. ولرسم كل شخصية على حدة، علينا استخدام التابع ()drawImage لاقتصاص صورة واحدة للشخصية وعرض هذا الجزء فقط، كما فعلنا مع شعار فايرفوكس في مثال سابق. وينبغي ضرب اﻹحداثي X للشريحة بالعدد 102 ويبقى اﻹحداثي Y مساويًا للصفر، وستبقى أبعاد الشريحة دائمًا 102x148 بكسل. سنضع اﻵن شيفرة الدالة ()draw في اﻷسفل لكي نزودها بالشيفرة اللازمة: function draw() {} أما بقية الشيفرة في هذا القسم فستكون ضمن الدالة ()draw. لهذا أضف اﻷسطر التالية التي تمسح اللوحة وتعدها لرسم كل إطار. وانتبه إلى ضرورة تخصيص الزاوية العليا اليسارية من المربع لتكون (width/2, height/2) لأننا اتخذنا مركز اللوحة نقطة البداية. ctx.fillRect(-(width / 2), -(height / 2), width, height); نرسم تاليًا الصورة باستخدام الدالة drawImage التي تقبل تسع معاملات: ctx.drawImage(image, sprite * 102, 0, 102, 148, 0 + posX, -74, 102, 148); وكما ترى: خصصنا image لتكون الصورة التي نرسمها. يحدد المعاملان 2 و 3 إحداثيا الزاوية العليا اليسارية من الشريحة التي نريد اقتصاصها من الصورة المصدرية، ويكون X هو قيمة المتغير sprite مضروبًا بالعدد 102 (حيث يمثل المتغير عدد الشخصيات الموجودة في الصورة من 0 إلى 5) بينما تبقى قيمة Y هي 0. يحدد المعاملان 4 و 5 أبعاد الشريحة التي نقتصها وهي 102x148 بكسل. يحدد المعاملان 6 و 7 الزاوية العليا اليسارية من الصندوق الذي نرسم ضمنه الشخصية، وتكون قيمة اﻹحداثي X هي 0 + posX وبالتالي نستطيع تغيير مكان رسم الخصية بتغيير قيمة posX. يحدد المعاملان 8 و 9 أبعاد الصورة على اللوحة، وعلينا هنا المحافظة على اﻷبعاد اﻷصلية لهذا كانت قمة المعاملين 102 و 148 على التتالي: علينا تعديل قيمة المتغير sprite عند كل رسم if (posX % 13 === 0) { if (sprite === 5) { sprite = 0; } else { sprite++; } } لاحظ كيف وضعنا الشيفرة السابقة ضمن الكتلة ({}if (posX % 13 === 0 واستخدمنا العامل % (عامل باقي القسمة) للتحقق من إمكانية قابلية قسمة قيمة المتغير posX على 13. فإن كان الوضع كذلك ننتقل إلى الشخصية التالية بزيادة قيمة المتغير sprite بمقدار 1 (ثم نعود إلى 0 عندما تصبح قيمته 5). ويعني ذلك فعليًا أننا نغير الشخصية عند اﻹطار 13 وتقريبًا حوالي 5 إطارات في الثانية (تكرر الدالة ()requestAnimationFrame العملية بمعدل 60 إطار في الثانية إن أمكن). وعندما نعرض أخر شخصية نعود بعدها إلى الشخصية 0 وإلا سنزيد المتغير sprite بمقدار 1. سنعمل اﻵن على آلية تغيير قيمة posX مع كل إطار، لهذا عليك إضافة الشيفرة التالية تحت الشيفرة السابقة: if (posX > width / 2) { let newStartPos = -(width / 2 + 102); posX = Math.ceil(newStartPos); console.log(posX); } else { posX += 2; } نستخدم عبارة if...else للتحقق من تجاوز قيمة المتغير posX القيمة width/2 والذي يعني خروج الشخصية من يمين لوحة الرسم، وعندها نحسب موقعًا جديدًا للشخصية يضعها على يسار الحافة اليسرى للوحة. بينما إن لم تتجاوز قيمة المتغير posX تلك القيمة نزيد قيمته بمقدار 2. وبالتالي ستتحرك الشخصية إلى اليمين قليلًا في الإطار التالي. ولا بد أخيرًا من تنفيذ الحركة السابقة باستمرار عن طريق استدعاء ()requestAnimationFrame في نهاية الدالة ()draw: window.requestAnimationFrame(draw); ستبدو نتيجة الشيفرة اﻵن كالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. تطبيق رسومي بسيط كمثال أخير عن الرسوميات، سنعرض تطبيقًا بسيطًا جدًا للرسم نجمع فيه بين الاستجابة لمدخلات المستخدم (حركة الفأرة في هذا المثال) والحلقة التي تبني الرسم المتحرك. لن نشرح بالتفصيل خطوات بناء التطبيق بل سنلقي نظرة على الشيفرة اﻷكثر أهمية. بإمكانك الاطلاع على شيفرة التطبيق من خلال المستودع المخص له على جت-هب. لنلق نظرة على بعض اﻷجزاء المهمة: أولًا: نتتبع موقع الفأرة من خلال إحداثيات x و y، كما نترصد حدث نقر الفأرة وذلك من خلال المتغيرات curX و curY و pressed. وعندما تتحرك الفأرة يقع الحدث onmousemove وننفذ معالجه الذي يلتقط الإحداثيات الحالية لموقع الفأرة. كما نستخدم أيضًا معالجي الحدثين onmousedown و onmouseup لتغيير قيمة المتغير pressed إلى true عندما نضغط زر الفأرة وإلى false عندما نحرر الزر. let curX; let curY; let pressed = false; // حدّث إحداثيات موقع الفأرة document.addEventListener("mousemove", (e) => { curX = e.pageX; curY = e.pageY; }); canvas.addEventListener("mousedown", () => (pressed = true)); canvas.addEventListener("mouseup", () => (pressed = false)); عند النقر على الزر "مسح اللوحة Clean canvas" ننفذ دالة بسيطة تمحي اللوحة بأكملها وتعيدها إلى اللون اﻷسود: clearBtn.addEventListener("click", () => { ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); }); وحلقة الرسم بسيطة هنا، فعندما تكون قيمة المتغير pressed هي true، نرسم دائرة لها اللون الذي يحدده منتقي اﻷلوان color picker ونصف قطر يحدده عنصر تحديد المجال range input. وعلينا رسم الدائرة فوق النقطة المحددة بمقدار 85 بكسل، ذلك أن القياس مأخوذ بالنسبة إلى أعلى شاشة العرض (أعلى نافذة المتصفح) لكننا نرسم الدائرة بالنسبة ﻷعلى اللوحة التي تبدأ تحت شريط التحكم (الذي يضم منتقي اﻷلوان ومحدد نصف القطر) ذو الارتفاع 85 بكسل. ولو رسمنا الدائرة اعتمادًا على قيمة curY ستبدو الدائرة تحت النقطة المحددة للرسم بحدود 85 بكسل. function draw() { if (pressed) { ctx.fillStyle = colorPicker.value; ctx.beginPath(); ctx.arc( curX, curY - 85, sizePicker.value, degToRad(0), degToRad(360), false, ); ctx.fill(); } requestAnimationFrame(draw); } draw(); جميع أنواع عنصر الدخل <input> مدعومة جيدًا من قبل المتصفحات، وإن لم يدعمها متصفح سيعرض حقل نصي نمطي بدلًا عنه. الواجهة WebGL لنترك اﻵن البيئة الرسومية ثنائية البعد ونلقي نظرة سريعة على لوحات الرسم ثلاثية اﻷبعاد. تُستخدم الواجهة البرمجية WebGL API للعمل مع الرسومات ثلاثية البعد، وهي واجهة منفصلة تمامًا عن واجهة البيئة الرسومية ثنائية البعد مع أن شيفرتهما تُصيّر ضمن العنصر نفسه <canvas>. بنيت WebGL على أساس OpenGL (مكتبة الرسوميات المفتوحة Open Graphics Library) وتسمح لك بالتواصل مباشرة مع المعالج الرسومي للحاسوب GPU. لهذا فكتابة شيفرة WebGL خام أشبه بكتابة شيفرات لغات منخفضة المستوى مثل ++C مقارنة بشيفرة جافا سكريبت، فهي معقدة لكنها قوية جدًا. استخدام مكتبة جافا سكريبت خارجبة يستخدم معظم المطورون مكتبات يقدمها طرف آخر عند العمل مع الرسوميات ثلاثية البعد نظرًا لتعقيد WebGL مثل Three.js أو PlayCanvas أو Babylon.js. تعمل هذه المكتبات عومًا على نحو متشابه، فهي تقدم دوال أولية وأشكال مخصصة وكاميرات لعرض الموقع وطرق لتطبيق اﻹضاءة والظل ولتغطية السطوح بخامات مختلفة وغيرها. فهذه المكتبات تتعامل مباشرة مع WebGL بدلًا منك متيحة لك المجال للعمل وفق سوية برمجية أعلى. ويعني هذا بالطبع تعلم واجهات برمجية أخرى (واجهات يقدمها طرف آخر في حالتنا) لكنها أبسط بكثير من التعامل مع شيفرة WebGL الخام. إعادة إنشاء مكعب لنلق نظرة على مثال بسيط يشرح استخدام المكتبة WebGL، وسنختار فيه المكتبة Three.js كونها من أكثر المكتبات استخدامًا. وسنبني في مثالنا مكعب ثلاثي اﻷبعاد يدور حول نفسه. أنشئ نسخة عن ملف المثال) ضمن مجلد جديد ثم احفظ نسخة من الملف metal003.png في المجلد نفسه. ويمثل الملف اﻷخير الصورة التي نستخدمها لتغطية سطح المكعب لاحقًا. أنشئ ملفًا جديدًا باسم script.js في نفس المجلد السابق. نزّل المكتبة Three.min.js وخزنها في نفس المجلد السابق. لدينا اﻵن الملف three.js الذي يرتبط بصفحتنا، ويمكننا كتابة الشيفرة الذي تستخدمه ضمن الملف script.js. لنبدأ بإنشاء مشهد جديد عن طريق إضافة الشيفرة التالية: const scene = new THREE.Scene(); تُنشئ الدالة البانية()scene مشهدًا جديدًا يمثل بيئة عمل ثلاثية اﻷبعاد التي نريد عرضها. ونضيف بعد ذلك كاميرا لرؤية المشهد. ووفق مصطلحات التصميم ثلاثي اﻷبعاد، تمثل الكاميرا موقع المراقب، وﻹنشائها أضف اﻷسطر التالية: const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000, ); camera.position.z = 5; تأخذ الدالة البانية PrespectiveCamera أربع وسطاء: حقل الرؤية: ويدل على اتساع المساحة أمام الكاميرا التي يجب عرضها على الشاشة مقدرة بالدرجات. نسبة العرض aspect ratio: وهي عادة نسبة اتساع الشاشة مقسومًا على ارتفاعها، واستخدام نسب أخرى ستشوه المشهد. مستوي البعد: وتمثل البعد عن الكاميرا الذي لن تصير بعده اﻷشياء. نضبط أيضًا موقع الكاميرا ليكون على بعد خمس وحدات قياس بعيدًا عن المحور z، وهذا مشابه للخاصية z-index في CSS التي تمثل موقع العنصر بعيدًا عن الشاشة باتجاهك. أما المكون الحيوي الثالث فهو المصيّر renderer، وهو كائن يصير المشهد كما يُرى من الكاميرا. وسننشئ اﻵن مصيّرًا باستخدام الدالة البانية ()WebGLRenderer لكننا لن نستخدمه حاليًا: const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); يُنشئ السطر الأول مصيرًا جديدًا والثاني يضبط الأبعاد التي سيرسم المصير ضمنها ما تعرضه الكاميرا، بينما يربط السطر الثالث العنصر <canvas> الذي يُنشئه المصيّر بجسم مستند HTML وسيُعرض كل ما يرسمه المصيّر في نافذة المتصفح. وﻹنشاء المكعب الذي نريد رسمه في اللوحة، عليك إضافة اﻷسطر التالية إلى نهاية ملف جافا سكريبت: let cube; const loader = new THREE.TextureLoader(); loader.load("metal003.png", (texture) => { texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; texture.repeat.set(2, 2); const geometry = new THREE.BoxGeometry(2.4, 2.4, 2.4); const material = new THREE.MeshLambertMaterial({ map: texture }); cube = new THREE.Mesh(geometry, material); scene.add(cube); draw(); }); هناك نقاط عديدة يجدر شرحها في الشيفرة السابقة: ننشئ أولًا المتغير العام cube لكي نصل إلى المكعب في أي مكان من الشيفرة. ننشئ تاليًا كائن TextureLoader جديد ونستدعي التابع ()load العائد له. ويأخذ هذا التابع معاملين في حالتنا (علمًا أنه يأخذ أكثر): الخامة التي نريد أن نحمّلها (صورة PNG) ودالة ننفذها عند اكتمال تحميل الخامة. نستخدم داخل الدالة السابقة خاصيات الكائن لتكرار الصورة التي تغلف جميع أوجه المكعب بمقدار 2x2. ومن ثم ننشئ كائن BoxGeometry وكائن MeshLambertMaterial جديدان ونربطهما معًا ضمن شبكة Mesh ﻹنشاء المكعب. ويحتاج أي كائن نمطيًا إلى بنية هندسية (الشكل الذي سيكون عليه) ومظهر مادي (كيف سيبدو السطح الخارجي). وفي النهاية، نضيف المكعب إلى المشهد ومن ثم نستدعي الدالة ()draw لتبدأ عملية تحريك الرسم. وقبل أن نعرّف الدالة ()draw، نضيف زوجًا من الأضواء إلى المشهد، ليبدو المشهد أكثر حيوية: const light = new THREE.AmbientLight("rgb(255 255 255)"); // soft white light scene.add(light); const spotLight = new THREE.SpotLight("rgb(255 255 255)"); spotLight.position.set(100, 1000, 1000); spotLight.castShadow = true; scene.add(spotLight); يُعد الكائن AmbientLight نوعًا من اﻷضواء البرمجية التي تضيئ المشهد بأكمله بما يشبه الشمس التي تضيء عليك وأنت في الخارج. بينما يمثل الكائن spotLight شعاع ضوئي وفق اتجاه محدد مثل مشعل أو بقعة ضوء. لنضف اﻵن الدالة ()draw إلى أسفل شيفرة جافا سكريبت: function draw() { cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); requestAnimationFrame(draw); } الشيفرة السابقة واضحة عمومًا، إذ ندوّر المكعّب قليلًا في كل إطار حول محوريه اﻷفقي والشاقولي ونصيّر المشهد كما يُرى من الكاميرا ونستدعي أخيرًا الدالة ()requestAnimationFrame لتحضير رسم اﻹطار التالي: إليك المشهد بشكله النهائي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة لا بد وأن تكون في نهاية هذا المقال قد امتلكت فكرة لا بأس بها عن أساسيات برمجة الرسوميات باستخدام الواجهة البرمجية Canvas و المكتبة WebGL وما يمكن فعله باستخدام هاتين الواجهتين، وامتلكت فكرة جيدًا عن الأماكن التي تقصدها لتحصل على معلومات أكثر. ترجمة -وبتصرف- للقسم الثاني من مقال Drawing graphics اقرأ أيضًا المقال السابق: العمل مع الرسوميات في جافا سكريبت: الرسومات ثنائية البعد ضمن العنصر Canvas مقدمة إلى WebGL - إضافة التفاصيل إلى سطح مجسَّم مدخل إلى صناعة ألعاب المتصفح الرسم على لوحة في جافاسكربت
-
يتضمن المتصفح مجموعة أدوات برمجية فعّالة للتعامل الرسوميات ابتداءً من لغة إنشاء الرسوميات الشعاعية SVG، إلى الواجهات التي تسمح لك بالرسم ضمن العنصر <canvas>. لهذا سنقدم في هذا المقال مدخلًا إلى الوجهة البرمجية Canvas، إضافة إلى بعض الموارد اﻷخرى لتزيد من معارفك في هذا المجال. ننصحك قبل المتابعة في قراءة هذه المقالات أن: تكون ملمًا بلغتي HTML و CSS. تكون ملمًا بلغة جافا سكريبت. تطلع على سلاسل المقالات السابقة التي ناقشت أساسيات جافا سكريبت والكائنات في جافا سكريبت. تطلع على أساسيات الواجهات البرمجية في طرف العميل. الرسوميات في الويب ذكرنا في مقالات سابقة أن الويب كان بداية نصيًا -أي يعرض محتوى نصي فقط- بشكل كامل مما جعله ضعيف الجاذبية، لذلك ظهرت الصور بداية من خلال العنصر <img> ولاحقًا من خلال خاصيات لغة التنسيق CSS مثل الخاصية background-image و من ثم بدأ استخدام الصور الشعاعية أو المتجهة SVG. مع ذلك لم يكن كل هذا كافيًا. وعلى الرغم من إمكانية استخدام CSS وجافا سكريبت لتحريك الصور الشعاعية SVG vector images والتعامل معها كونها تُكتب باستخدام تعليمات ترميز markup ولم تكن هناك طريقة لعمل المثل على الصور النقطية bitmap images وكانت الأدوات المتوفرة محدودة. ولم توجد طريقة أصيلة في الويب ﻹنشاء الرسوميات المتحركة أو اﻷلعاب أو المشاهد ثلاثية الأبعاد والتي تحتاج متطلبات خاصة تتعامل معها لغات برمجة منخفضة المستوى مثل ++C وجافا. بدأ الوضع بالتحسن عندما دعمت المتصفحات العنصر والواجهة البرمجية المتعلقة به في عام 2004. وكما سنرى تاليًا، تقدم عناصر canvas بعض اﻷدوات المفيدة التي تساعد في إنشاء رسوميات متحركة ثنائية البعد وألعاب، وعرض البيانات وغيرها من اﻹمكانات وخاصة عندما تتكامل مع واجهات برمجية أخرى تقدمها منصة الويب. لكن كان من الصعب إعدادها للوصول السهل accessibility. سترى في المثال التالي الكرات القافزة المرتدة التي عملنا عليها في مقال سابق وهي مشهد ثنائي البعد مبني على أساس العنصر canvas. وفي الفترة الممتدة بين 2006 إلى 2007 عملت موزيللا على إنجاز عناصر لوحات رسومية canvas ثلاثية اﻷبعاد، وتحولت فيما بعد إلى WebGL التي حظيت باهتمام مطوري المتصفحات وقد جرى توصيفها لتكون معيارًا بين عامي 2009-2010. وتتيح لك الواجهة WebGL إنشاء رسوميات ثلاثية اﻷبعاد ضمن المتصفح. بقدم المثال التالي مكعبًا يدور باستخدام هذه الواجهة: نركز في مقالنا على لوحات الرسم ثنائية البعد، وبما أن شيفرة WebGL الخام شديدة التعقيد، سنعرض طريقة استخدام المكتبة WebGL ﻹنشاء مشهد ثلاثي اﻷيعاد بسهولة أكبر. تطبيق عملي: ابدأ العمل مع لوحة الرسم canvas إن أردت إنشاء رسوميات ثنائية وثلاثية البعد على صفحة ويب عليك أن تنطلق من عنصر HTML الذي يُمثّل لوحة الرسم <canvas>. ويُستخدم هذا العنصر في تحديد منطقة من الصفحة للرسم فيها. واﻷمر بسيط ويتم بإضافة عنصر <canvas> إلى الصفحة كما يلي: <canvas width="320" height="240"></canvas> تنشئ الشيفرة السابقة لوحة رسم أبعادها 230 و 240 بكسل. ولا بد أن تضع شيئًا ما ضمن وسمي البداية والنهاية للعنصر كي يصف محتوى اللوحة لمستخدمي المتصفحات التي لا تدعم العنصر أو لمستخدمي قارئات الشاشة: <canvas width="320" height="240"> <p>اكتب هنا وصف اللوحة للمستخدمين الذين لا يمكنهم رؤيتها </p> </canvas> و لابد أن يعتبر ما تضعه ضمن وسمي العنصر بديلًا مفيدًا عن محتوى اللوحة، فإن كنت تصيّر أو تعرض رسمًا يتغير بشكل مستمر ليعبر عن أسعار البورصة مثلًا ، ينبغي أن يكون المحتوى البديل صورة تتضمن آخر تحديث للرسم مع نص بديل عنها alt يتحدث عن اﻷسعار أو قائمة من الروابط المستقلة لكل صفحة من صفحات هذه البورصة. ملاحظة: لا يمكن الوصول إلى محتوى لوحة الرسم من خلال قارئات الشاشات، لهذا عليك وضع نص يصف محتواها على شكل قيمة للسمة arial-label ضمن العنصر <canvas> نفسه أو استخدام محتوى مستقل ضمن وسمي البداية والنهاية للعنصر. وتذكر أن محتوى <canvas> ليس جزءًا من شجرة DOM لكن العنصر الذي تضعه ضمنه كذلك. إنشاء لوحة رسم وتحديد أبعادها لنبدأ بإنشاء لوحة رسم خاصة بتطبيقنا، لهذا اتبع الخطوات التالية: انسخ مجلد المشروع الذي يتضمن الملفات التالية: index.html script.js style.css افتح الملف index.html ثم أضف الشيفرة التالية ضمنه تحت الوسم <body>: <canvas class="myCanvas"> <p>Add suitable fallback here.</p> </canvas> أضفنا في الكود أعلاه صنفًا إلى العنصر <canvas> حتى يسهل الوصول إليه عن طريق جافا سكريبت في حال كان هناك أكثر من لوحة نريد العمل معها، لكننا أزلنا السمتين width و height حاليًا (بإمكانك إعادتهما إن أردت، لكننا سنضبطهما لاحقًا باستخدام جافا سكريبت). وستأخذ اللوحات افتراضيًا ارتفاعًا مقداره 150 بكسل واتساعًا مقداره 300 بكسل. افتح الملف scripts.js ثم أضف شيفرة جافا سكريبت التالية: const canvas = document.querySelector(".myCanvas"); const width = (canvas.width = window.innerWidth); const height = (canvas.height = window.innerHeight); خزّنا هنا مرجعًا إلى لوحة الرسم ضمن الثابت canvas ومن ثم أنشأنا ثابتًا آخر width وضبطنا قيمته وقيمة اتساع اللوحة لتكون مساوية لاتساع نافذة المتصفح Window.innerWidth، وكررنا ما فعلناه في السطر الثالث لكن مع ارتفاع اللوحة. وهكذا ستملأ لوحة الرسم نافذة المتصفح (تذكر أن الارتفاع هنا هو ارتفاع نافذة العرض viewport). لاحظ أيضًا كيف نفذنا سلسلة من اﻹسنادات باستخدام عدة عوامل مساواة =، وهذا أمر مسموح في جافا سكريبت ويُعد مفيدًا إن أردت إسناد القيمة ذاتها إلى عدة متغيرات. كما حرصنا على تأمين طريقة للوصول إلى أبعاد اللوحة بإسنادها إلى متغيرات، وتأتي فائدة هذه الفكرة إن احتجنا لاحقًا على سبيل المثال إلى رسم شيء ما في وسط اللوحة تمامًا. ملاحظة: علينا غالبًا ضبط أبعاد الصور باستخدام سمات HTML أو خاصيات شجرة DOM كما شرحنا في اﻷعلى. كما يمكنك استخدام CSS لكن تطبيق اﻷبعاد الجديدة سيكون بعد تصيير لوحة الرسم وهكذا قد تتعرض لوحة الرسم كغيرها من الصورة إلى التشوه. ضبط مسار العمل على اللوحة وإنهاء اﻹعداد نحتاج إلى مرجع خاص إلى منطقة العمل حتى نستطيع الرسم على اللوحة يُعرف بمسار العمل context. وننفذ هذا اﻷمر باستخدام التابع ()HTMLCanvasElement.getContext الذي يأخذ معاملًا واحدًا بأبسط حالات استخدامه تمثل نوع مسار العمل الذي نريده. وما نحتاجه في تطبيقنا لوحة ثنائية البعد، لهذا سنضيف شيفرة جافا سكريبت التالية في آخر الشيفرة الموجودة في الملف script.js: const ctx = canvas.getContext("2d"); ملاحظة: بإمكانك اختيار مسارات عمل أخرى مثل webgl من أجل WebGL و webgl2 من أجل 2 WebGL لكننا لن تحتاج هذه المسارات في مقالنا. وهكذا تصبح لوحة الرسم جاهزة في تطبيقنا، وسيحمل المتغير ctx الكائن CanvasRenderingContext2D وسيكون الرسم على اللوحة من خلال التعامل مع هذا الكائن. دعونا قبل إكمال العمل ننفذ شيئًا أخيرًا وهو تلوين خلفية الصفحة لتأخذ فكرة بسيطة عن الواجهة البرمجية Canvas. أضف الشيفرة التالية إلى شيفرة الملف script.js: ctx.fillStyle = "rgb(0 0 0)"; ctx.fillRect(0, 0, width, height); نضبط في هذه الشيفرة لون الخلفية باستخدام الخاصية fillStyle التي تأخذ قيمًا لونية كما هو حال خاصيات CSS المشابهة، ثم نرسم مربعًا يغطي كامل لوحة الرسم باستخدام التابع fillRect، ويمثل أول معاملين له الزاوية العليا اليسارية والمعاملين الباقين اتساع وارتفاع المربع الذي نريد رسمه (أخبرناك أن للمتغيرين width و height فوائد لاحقة). أساسيات الرسوميات ثنائية البعد ضمن العنصر <canvas> ذكرنا سابقًا أن جميع عمليات الرسم تجري من خلال التعامل مع الكائن CanvasRenderingContext2D (وهو ctx في تطبيقنا). وتحتاج الكثير من العمليات إلى إحداثيات لتحديد المكان الذي نرسم فيه بدقة، وتكون الزاوية العليا اليسارية بمثابة مبدأ الجملة اﻹحداثية وتمثل النقطة (0,0)، بينما يتجه المحور الأفقي (x) من اليسار نحو اليمين والعمودي (y) من اﻷعلى إلى اﻷسفل. تميل معظم الرسوميات إلى استخدام المربع البدائي primitive rectangle (الذي يمثل شكل أساسي يستخدم لبناء رسوميات أكثر تعقيدًا) أو تتبع خط عبر مسار محدد ومن ثم ملء الشكل الناتج. وسنشرح تاليًا كيف يجري اﻷمر. مربعات بسيطة سنبدأ برسم بعض المربعات البسيطة، لهذا: انسخ شيفرة قالب لوحة الرسم الذي حضرناه سابقًا (كما يمكنك إنشاء نسخة عن مجلد التطبيق إن لم تتابع معنا الخطوات السابقة). أضف الأسطر التالية من الشيفرة في أسفل شيفرة جافا سكريبت الموجودة: ctx.fillStyle = "rgb(255 0 0)"; ctx.fillRect(50, 50, 100, 150); إن حفظت التغيرات وأعدت تحميل الصفحة سترى مربعًا أحمر اللون ضمن اللوحة، تبعد زاويته العليا اليسارية مقدار 50 بكسل عن الحافتين العليا واليسارية للوحة (كما حددهما أول معاملين) وله اتساع مقداره 100 بكسل وارتفاع 150 بكسل (كما حددهما المعاملان اﻷخيران). لنضف اﻵن مربعًا آخر أخضر هذه المرة: ctx.fillStyle = "rgb(0 255 0)"; ctx.fillRect(75, 75, 100, 100); احفظ التغييرات وأعد تحميل الصفحة لترى النتيجة. تطرح الشيفرة السابقة نقطة هامة وهي أن جميع عمليات الرسم مثل رسم مربع أو خط وغيرها تُنفّذ وفق تسلسل ورودها في الشيفرة. فكّر بالأمر وكأنك ترسم على جدار، حيث تغطي كل طبقة ما تحتها. وبالطبع لا يمكن أن نغيّر هذا اﻷمر، لهذا عليك أن تفكّر مليًا بترتيب ما ترسمه على اللوحة. كما يمكنك إنشاء رسومات شبه شفافة عند اختيارك لونًا شبه شفاف باستخدام التابع ()rgb مثلًا. إذ تُعرّف القناة ألفا alpha channel مقدار الشفافية في اللون، وكلما كانت قيمتها أكبر كلما زادت قتامة اللون وغطّى ما تحته. أضف السطرين التاليين إلى الشيفرة: ctx.fillStyle = "rgb(255 0 255 / 75%)"; ctx.fillRect(25, 100, 175, 50); جرب أن ترسم مربعات لتختبر قدرتك! الإطارات وسماكة الخطوط رسمنا حتى اللحظة مربعات ممتلئة، لكنك تستطيع أيضًا رسم إطارات مربعة strokes. ولضبط لون اﻹطار نستخدم الخاصية strokeStyle ونرسمه باستخدام التابع strokeRect. أضف السطرين التاليين إلى الشيفرة: ctx.strokeStyle = "rgb(255 255 255)"; ctx.strokeRect(25, 25, 175, 200); للإطارات سماكة افتراضية قيمتها 1 بكسل، لكنك تستطيع تعديل السماكة باستخدام الخاصية lineWidth التي تأخذ قيمة تمثل سماكة اﻹطار مقدرة بالبكسل. أضف اﻵن السطر التالي إلى الشيفرة: ctx.lineWidth = 5; لاحظ كيف سيبدو اﻹطار أكثر سماكة. وسيبدو مثالنا حتى اللحظة كالتالي: ملاحظة: ستجد الشيفرة الكاملة لهذا المثال على جت-هب. رسم المسارات لو أردت رسم ما هو أعقد من مربع، لا بد حينها من رسم مسار. ويقتضي اﻷمر بأبسط أشكاله كتابة شيفرة تحدد تمامًا المسار الذي تريد أن يتحرك قلم الرسم عليه ضمن اللوحة حتى يرسم الشكل المطلوب. وتضم الواجهة Canvas دوال لرسم خطوط مستقيمة ودوائر ومنحنيات بيزيه وغيرها الكثير. لنبدأ اﻵن هذا القسم بنسخة جديدة من قالب المثال الذي أعددناه سابقًا، وسنستخدم بعض التوابع والخاصيات الشائعة خلال الأقسام التالية: ()beginPath: يبدأ رسم مسار من النقطة التي يكون عندها القلم حاليًا في اللوحة وستكون هذه النقطة مبدأ اﻹحداثيات إن كانت اللوحة جديدة. ()moveTo: ينقل القم إلى نقطة أخرى من اللوحة دون رسم أو تسجيل المسار بل يقفز القلم إلى النقطة المختارة. ()fill: يرسم شكلًا يملأ المسار الذي رسمه القلم. ()stroke: يرسم إطارًا مبنيًا على المسار الذي يرسمه القلم. باﻹمكان استخدام الخاصيات lineWidth و fillStyle أو strokeStyle مع المسارات أيضًا. تبدو شيفرة رسم مسار نمطي قريبة من التالي: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); // ارسم مسارك ctx.fill(); رسم الخطوط لنرسم اﻵن مثلث متساوي الأضلاع ضمن اللوحة: أضف بداية الدالة المساعدة التالية في أسفل الشيفرة، مهمة هذه الدالة تحويل قيم الزوايا من درجات إلى راديان. تكمن فائدة هذه الدالة في أن جافا سكريبت تفهم قيم الزوايا بالراديان لكننا كبشر نفكر طبيعيًا بالدرجات. function degToRad(degrees) { return (degrees * Math.PI) / 180; } ابدأ المسار بإضافة الشيفرة التالية تحت الشيفرة السابقة، وفيها نضبط لون المثلث ونبدأ رسم المسار ثم ننتقل مباشرة إلى النقطة (0,0) دون رسم أي شيء ومن هذه النقطة نبدأ رسم المثلث: ctx.fillStyle = "rgb(255 0 0)"; ctx.beginPath(); ctx.moveTo(50, 50); أضف اﻷسطر التالية في نهاية الشيفرة السابقة: ctx.lineTo(150, 50); const triHeight = 50 * Math.tan(degToRad(60)); ctx.lineTo(100, 50 + triHeight); ctx.lineTo(50, 50); ctx.fill(); نرسم بداية خطًا من نقطة البداية إلى النقطة (150,50) وسيتجه مسارنا 100 بكسل إلى اليمين وفق المحور x. نحسب بعد ذلك ارتفاع المثلث متساوي اﻷضلاع باستخدام قواعد مثلثية بسيطة إذ نعلم أن زوايا المثلث هي 60 درجة. لهذا نستطيع تقسيم المثلث المتساوي اﻷضلاع الذي نوجهه للأسفل إلى مثلثين قائمين لكل منهما زاويتين حادتين قياسهما 30 و60 درجة. ونعرّف في المثلث القائم: الوتر hypotenuse: وهو أطول أضلاع المثلث القائم. المجاور adjacent: وهو هنا الضلع المجاور للزاوية 60 وطوله 50 بكسل لأنه يمثل نصف طول المسار الذي رسمناه سابقًا. المقابل opposite: وهو هنا الضلع المقابل للزاوية 60 ويمثل ارتفاع المثلث المتساوي اﻷضلاع الذي ننوي رسمه. يُعطى طول المجاور رياضيًا من خلال جداء المقابل بظل الزاوية tan: 50 * Math.tan(degToRad(60)) نستخدم هنا الدالة ()degToRad التي بنيناها سابقًا لتحويل الزاوية 60 درجة إلى راديان وهي القيمة التي يتوقعها التابع ()Math.tan الذي يحسب ظل الزاوية. بعد حساب اﻹرتفاع، نرسم خطًا آخر إلى النقطة (100, 50+triHeight) إلى نقطة أخرى لها إحداثي X يعادل نصف طول المسار المستقيمة السابق وإحداثي Y قيمته تعادل 50 زائدًا طول اﻹرتفاع، ذلك أن قاعدة المثلث تنزاح إلى داخل اللوحة مقدار 50 بكسل عن الحافة العليا لها. أما الخطوة التالية فهي رسم خط من آخر نقطة إلى نقطة البداية ليتكون المثلث. نستدعي في النهاية التابع ()ctx.fill ﻹنهاء المسار وملئ الشكل الناتج. رسم الدوائر لنلق نظرة على طريقة رسم الدوائر في اللوحة. تُنفّذ هذه العملية من خلال التابع ()arc الذي يرسم جزءًا من قوس الدائرة أو قوس الدائرة بأكمله ابتداءًا من نقطة محددة: ﻹضافة دائرة إلى لوحتنا ضع الشيفرة التالية في نهاية الشيفرة السابقة: ctx.fillStyle = "rgb(0 0 255)"; ctx.beginPath(); ctx.arc(150, 106, 50, degToRad(0), degToRad(360), false); ctx.fill(); يأخذ التابع ()arc ست معاملات، يحدد اﻷول والثاني اﻹحداثيين x و y لمركز الدائرة والثالث هو نصف قطر الدائرة، بينما يحدد المعاملين الخامس والسادس زاويتي البداية والنهاية لقوس الدائرة (0 و 360 يرسمان دائرة كاملة) ويحدد المعامل اﻷخير إذا ما كانت الدائرة سترسم باتجاه عقارب الساعة أو عكسها (تعني القيمة false أن الرسم باتجاه عقارب الساعة) ملاحظة: الزاوية 0 هي الزاوية الأفقية إلى اليمين. لنجرب إضافة قوس آخر: ctx.fillStyle = "yellow"; ctx.beginPath(); ctx.arc(200, 106, 50, degToRad(-45), degToRad(45), true); ctx.lineTo(200, 106); ctx.fill(); هناك اختلافان بسيطان عن النمط السابق: ضبطنا قيمة المعامل الأخير للتابع ()arc على القيمة true أي سيرسم القوس بعكس اتجاه عقارب الساعة، فحتى لو كانت زاوية البداية هي 45- وزاوية النهاية 45 درجة فإن القوس يغطي زاوية 270 درجة وليس 90 درجة والتي يمكن أن تحصل عليها إن كانت قيمة المعامل false. رسمنا خطًا إلى مركز الدائرة قبل استدعاء ()fill كي نحصل على دائرة اقتطع منها مثلث. وإن لم نرسم هذا الخط سيصل المتصفح نقطة البداية ونقطة النهاية ويملأ الشكل الناتج وهو دائرة اقتطع منها طرف. ستبدو نتيجة المثال السابق قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم النصوص يتيح لك العنصر <canvas> رسم عبارات نصية، وهذا ما سنتعلمه بإيجاز تاليًا. لنبدأ بإنشاء نسخة جديدة عن قالب التطبيق كي نرسم المثال الجديد. ونستخدم في هذا المثال التابعين: ()fillText: الذي يملأ النص. ()strokeText: الذي يرسم الحواف الخارجية للنص. يأخذ كل تابع منهما ثلاثة خاصيات بشكله البسيط: النص الذي سيُرسم واﻹحداثيين x و y للنقطة التي يبدأ الرسم عندها. هذه النقطة هي عمليًا الزاوية السفلى اليسارية لصندوق النص الذي نرسمه (الصندوق الذي يحيط بالنص). قد يسبب اﻷمر إرباكًا أحيانًا بالنظر إلى أن عمليات الرسم اﻷخرى تميل إلى البدء من الزاويا العليا اليسارية، تذكر ذلك جيدًا. وهنالك أيضًا عدد من الخاصيات التي تساعد في إدارة تصيير النص مثل font التي تسمح بتخصيص عائلة الخط وحجمه وغيرها، وتأخذ قيمها وفق الصيغة نفسها التي نستخدمها مع خاصية CSS التي تحمل نفس الاسم. ولا يمكن لقارئات الشاشة الوصول إلى محتوى العنصر <canvas> لأن النص الذي يُرسم في اللوحة لا يُعد جزءًا من شجرة DOM، لهذا لا بد من جعله متاحًا لذوي الاحتياجات الخاصة. وفي مثالنا جعلنا النص المكتوب ضمن اللوحة قيمة للسمة aria-label. أضف اﻵن الشيفرة التالية إلى نهاية شيفرة جافا سكريبت: ctx.strokeStyle = "white"; ctx.lineWidth = 1; ctx.font = "36px arial"; ctx.strokeText("Canvas text", 50, 50); ctx.fillStyle = "red"; ctx.font = "48px georgia"; ctx.fillText("Canvas text", 50, 150); canvas.setAttribute("aria-label", "Canvas text"); رسمنا باستخدام الشيفرة السابقة سطرين أولهما مفرّغ واﻵخر ممتلئ، ويبدو الشكل النهائي للوحة شبيهًا بالتالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. رسم صور ضمن اللوحة بإمكانك أيضًا تصيّر صور خارجية كي تُرسم ضمن العنصر <canvas>، ويمكن أن تكون الصور بسيطةً أو إطارات من فيديو أو غير ذلك. وسنلقي نظرة على رسم صور بسيطة ضمن اللوحة. أنشئ نسخة جديدة من قالب التطبيق الذي نستخدمه لتنفيذ الرسوميات. إذ ترسم الصور ضمن اللوحة باستخدام التابع ()drawImage. ويأخذ التابع بأبسط أشكاله ثلاث معاملات هي مرجع إلى الصورة واﻹحداثيين x و y للزاوية العليا اليسارية من الصورة. لنبدأ بتحديد مصدر للصورة التي نريد رسمها، لهذا أضف الشيفرة التالية إلى ملف جافا سكريبت: const image = new Image(); image.src = "firefox.png"; أنشأنا في الشيفرة السابقة كائن HTMLImageElement جديد باستخدام الدالة البانية ()Image. وللكائن المعاد النوع ذاته الذي يُعاد عندما ننشئ مرجعًا إلى العنصر <img>، لهذا يمكن ضبط السمة src له كي تكون عنوان URL لصورة شعار فايرفوكس، وفي هذه المرحلة يبدأ المتصفح تحميل الصورة. يمكن اﻵن رسم الصورة ضمن اللوحة باستخدام ()drawImage، لكن علينا أولًا التأكد من اكتمال تحميل الصورة وإلا ستخفق العملية. نتحقق من ذلك عن طريق الحدث load الذي يقع فقط عند إنتهاء تحميل الصورة، لهذا أضف الشيفرة التالية: image.addEventListener("load", () => ctx.drawImage(image, 20, 20)); سترى إن أعدت تحميل اللوحة كيف رُسمت الصورة ضمن اللوحة. لكن بالطبع هناك المزيد. فماذا لو أردت رسم جزء من الصورة فقط أو أردت تغيير أبعادها؟ يمكننا بالطبع تنفيذ كلا اﻷمرين باستخدام صيغة أعقد للتابع ()drawImage. لهذا عدّل استدعاء التابع ()ctx.drawImage ليصبح كالتالي: ctx.drawImage(image, 20, 20, 185, 175, 50, 50, 185, 175); المعامل اﻷول هو مرجع إلى الصورة. يحدد المعاملان 2 و 3 إحداثيات الزاوية العليا اليسارية من المنطقة التي تريد اقتطاعها من الصورة المحمّلة، ولن يُرسم أي شئ أعلى أو إلى يسار قيمتي المعاملين السابقين. يحدد المعاملان 4 و 5 اتساع وارتفاع المنطقة التي تريد اقتطاعها من الصورة التي حملتها. يحدد المعاملان 6 و 7 إحداثيا النقطة التي نريد أن نبدأ فيها رسم الصورة المقتطعة انطلاقًا من الزاوية العليا اليسارية لها نسبة إلى الزاوية العليا اليسارية للوحة. يحدد المعاملان 8 و 9 اتساع وارتفاع المنطقة التي نريد أن نرسم فيها الصورة المقتطعة. وقد حددنا في مثالنا نفس أبعاد الصورة المقتطعة، لكن باﻹمكان إعادة تحجيم الصورة باستخدام قيم مختلفة للمعاملين. في حال غيّرت في الصورة تغييرًا واضحًا لابد من تحديث توصيف الصورة الخاص بسهولة الوصول accessibility. canvas.setAttribute("aria-label", "Firefox Logo"); ستبدو نتيجة المثال قريبة من التالي: ملاحظة: بإمكانك الاطلاع على الشيفرة كاملة لهذا المثال على جت-هب. الخلاصة تعرفنا في هذا المقال على أساسيات الرسم ضمن العنصر <canvas> من حيث إعداد العنصر وضبط معاملاته. ثم تدربنا على رسم الخطوط والمسارات والدوائر والنصوص والصور في بيئة ثنائية البعد. وسنتابع في الجزء الثاني من هذا المقال العمل مع الرسومات المتحركة ثنائية وثلاثية البعد. ترجمة -وبتصرف- للقسم اﻷول من مقال: Drawing graphics اقرأ أيضًا المقال السابق: واجهات برمجية خارجية في جافا سكريبت Third Party APIs الرسم عبر عنصر canvas في HTML5 التعامل مع عنصر Canvas باستخدام جافاسكربت (رسم الأشكال) التعامل مع العنصر Canvas باستخدام جافاسكربت (رسم الصور ) التعامل مع التصاميم، الألوان والخطوط باستخدام Canvas في جافاسكربت