البحث في الموقع
المحتوى عن '2d_game'.
-
آخر ما يلزمنا ﻹكمال اللعبة ثنائية الأبعاد تفادي الزواحف التي بدأنا العمل عليها في مقال سابق هو بناء واجهة المستخدم user interface المخصصة عرض أشياء مهمة مثل النتيجة أو عبارة "انتهت اللعبة" وزر ﻹعادة اللعب. لهذا سننشىء في مقال اليوم مشهدًا جديدًا ثم نضيف عقدة من النوع CanvasLayer (واجهة مستخدم) باسم HUD وهي اختصار للكلمات "Heads-up display" بمعنى "شاشة المقدمة" والتي تعرض معلومات عن اللعبة وتظهر على شكل طبقة فوق نافذة عرض اللعبة. إنشاء واجهة اللعبة تسمح لك العقدة CanvasLayer في محرك الألعاب جودو GODOT برسم عناصر واجهة المستخدم على طبقة فوق بقية مشاهد اللعبة، وبالتالي لن تغطيها أية عناصر أخرى كاللاعب أو اﻷعداء. في لعبتنا الحالة، نحتاج لأن تعرض الواجهة HUD المعلومات التالية: النتيجة وتغيّرها ScoreTimer رسالة مثل انتهت اللعبة أو استعد للعب زر ابدأ لكي تبدأ اللعب إن الصنف اﻷساسي لهذه العقدة هو Control، وسنحتاج في واجهتنا لنوعين من عناصر التحكم Control هما عنوان Label وزر Button، لهذا عليك إنشاء ثلاث عقد أبناء للعقدة HUD كالتالي: عنوان Label باسم ScoreLabel عنوان Label باسم Message زر Button باسم StartButton مؤقت Timer باسم MessageTimer انقر على العنوان ScoreLabel ثم اكتب رقمًا في الحقل Text ضمن الفاحص، وكما تلاحظ، فإن الخط المستخدم افتراضيًا للعقدة Control صغير وغير مناسب، لذلك سنستخدم خطًا موجودًا ضمن المجلد "font" يُدعى "Xolonium-Regular.ttf" كالتالي: في نافذة الفاحص وتحت اللوحة انقر على Theme Overridesثم Fonts ثم Font ثم اختر "تحميل" واختر الملف "Xolonium-Regular.ttf": زد حجم الخط إلى 64 في الحقل "Font Size" ثم كرر نفس العملية على العنوان اﻵخر وعلى الزر في المشهد. ملاحظة: للعقدة Control موقع وأبعاد لكنها تمتلك أيضًا ما يُعرف بالمربط Anchor الذي يحدد نقطة اﻷصل، وهي النقطة المرجعية لحواف العقدة. رتب العقد كما في لقطة الشاشة التالية، وبإمكانك سحب العقد إلى المكان الذي تريده يدويًا أو استخدم المربط لضبط الموقع بدقة أكبر: العنوان ScoreLabel أضف النص 0 اضبط المحاذاة اﻷفقية Horizontal Alignment والعمودية Vertical Alignment على Center. اختر القيمة Center Top لضبط المربط (الخاصية Anchor Preset). الرسالة Message أضف النص "تفادي الزواحف". اضبط المحاذاة اﻷفقية Horizontal Alignment والعمودية Vertical Alignment على Center. اضبط قيمة "نمط الالتفاف التلقائي Autowrap Mode" على Word وإلا سيبقى النص على نفس السطر. اضبط قيمة الخاصية Size X الموجودة على المسار Control>Layout>Transform على القيمة 480 اختر القيمة Center لضبط المربط (الخاصية Anchor Preset). الزر StartButton أضف النص "ابدأ". اضبط قيمة الخاصية Size X الموجودة على المسار Control>Layout>Transform على القيمة 200 والخاصية Size Y على القيمة 100. اختر القيمة Center Bottom لضبط المربط (الخاصية Anchor Preset). ا ضبط قيمة الخاصية Position Y الموجودة على المسار Control>Layout>Transform على القيمة 580. وأخيرًا، اضبط الخاصية Wait Time في المؤقت MessageTimer على 2، والخاصية One Shot على "فعّال ON" ثم أضف السكربت التالي إلى HUD: extends CanvasLayer # Notifies `Main` node that the button has been pressed signal start_game نريد اﻵن عرض رسالة مؤقتة مثل " استعد!" لهذا سنضيف الشيفرة التالية: func show_message(text): $Message.text = text $Message.show() $MessageTimer.start() علينا أيضًا معالجة الحالة التي يخسر فيها اللاعب، لهذا ستعرض الشيفرة التالية رسالة "انتهت اللعبة" لمدة ثانيتين ثم تعود إلى الشاشة الرئيسية وتعرض بعد توقف صغير الزر "ابدأ": func show_game_over(): show_message("Game Over") # Wait until the MessageTimer has counted down. await $MessageTimer.timeout $Message.text = "Dodge the\nCreeps!" $Message.show() # Make a one-shot timer and wait for it to finish. await get_tree().create_timer(1.0).timeout $StartButton.show() تُستدعى الدالة اﻷخيرة عندما يخسر اللاعب. ملاحظة: إن أردت أن توقف اللعبة لفترة وجيزة، بإمكانك استخدام الدالة ()create_timer العائدة لشجرة المشاهد بدلًا من استخدام عقدة مؤقت. وهذه الدالة مفيدة جدًا في إضافة تأخير زمني في الحالات المشابهة لحالتنا التي نرغب فيها الانتظار قليلًا قبل عرض زر "إبدأ". أضف الشيفرة التالية اﻵن لتحديث النتيجة: func update_score(score): $ScoreLabel.text = str(score) صل اﻹشارة ()timeout العائدة للمؤقت MessageTimer واﻹشارة ()pressed العائدة للزر StartButton ثم عدل الشيفرة لتصبح كالتالي: func _on_start_button_pressed(): $StartButton.hide() start_game.emit() func _on_message_timer_timeout(): $Message.hide() ربط المشهد HUD بالمشهد الرئيسي Main بعد أن انتهينا من إنشاء المشهد HUD سنعود إلى المشهد الرئيسي Main. انشئ نسخة من المشهد HUDضمن المشهد الرئيسي كما نسخنا مشهد اللاعب سابقًا. ويجب أن تبدو شجرة المشاهد كالتالي حتى يكون كل شيء في مكانه الصحيح: علينا اﻵن وصل وظائف المشهد HUD إلى السكربت الرئيسي، ويتطلب ذلك بعد اﻹضافات إلى المشهد الرئيسي. صل اﻹشارة start_game للمشهد HUD إلى الدالة ()new_game في المشهد الرئيسي بالنقر على نسخة HUD في المشهد الرئيسي ثم الانتقال إلى "عقدة" في الشريط الجانبي واختيار اﻹشارة start_game ثم النقر عليها نقرًا مزدوجًا لتظهر نافذة "قم بوصل اﻹشارة إلى الدالة" ثم النقر على زر "Pick" في أسفلها واختيار الدالة ()new_game. تأكد من وجود اﻷيقونة الخضراء إلى جوار ()func new_game في السكربت الرئيسي، ولا تنس إزالة الاستدعاء ()new_game من الدالة ()ready_ ﻷننا لا نريد أن تبدأ اللعبة تلقائيًا. عدّل الشيفرة الموجودة ضمن الدالة ()new_game لتعرض الرسالة "استعد!": $HUD.update_score(score) $HUD.show_message("Get Ready") كما عليك استدعاء دالة HUD الموافقة للدالة ()game_over في السكربت الرئيسي: $HUD.show_game_over() أضف أخيرًا الشيفرة التالية إلى الدالة ()on_score_timer_timeout_ كي يبقى عداد النتيجة متزامنًا مع التغيرات: $HUD.update_score(score) أصبحت اﻵن جاهزًا للعب! انقر على زر تشغيل المشهد، وإذا طُلب منك اختيار المشهد اﻷساسي اختر main.tscn. إزالة الزواحف القديمة عندما تنتهي اللعبة وتحاول اللعب مجددًا ستبقى الزواحف من اللعبة القديمة موجودة على الشاشة ومن اﻷفضل أن تختفي جميعها قبل البدء بلعبة جديدة. إذًا لابد من طريقة ﻹخبار الزواحف بتدمير نفسها، وذلك باستخدام الميزة "المجموعات group". اختر المشهد ثم اختر العقدة الرئيسية وانقر على نافذة "عقدة" إلى جوار الفاحص وهو نفس المكان الذي تجد فيه اﻹشارات. انقر على العنوان "مجموعات" ثم اكتب اسمًا للمجموعة الجديدة وليكن "mobs" ثم انقر "أضف" وستظهر المجموعة الجديدة تحت الصندوق النصي: أضف السطر التالي إلى الدالة ()new_game في السكربت الرئيسي: get_tree().call_group("mobs", "queue_free") تستدعي الدالة ()call_group تابعًا محددًا (المعامل الثاني لها) باسمه وتطبقه على كل عناصر المجموعة المختارة (المعامل الأول)، وفي حالتنا يحذف كل زاحف نفسه. وضع اللمسات اﻷخيرة على اللعبة اكتملت اللعبة حاليًا من ناحية الوظيفة، وما سنفعله تاليًا هو بعض اﻹضافات لتحسين المظهر العام لها. الخلفية قد يجد البعض أن الخلفية الرمادية غير جذابة لذا دعونا نغير اللون. ومن الطرق المتبعة نجد العقدة ColorRect التي يجب وضعها تحت العقدة Main مباشرة كي تُرسم خلف جميع العقد. ولهذا العقدة خاصية واحدة هي Color. اختر اللون الذي تريده ثم انقر على Layout>Anchor Preset واختر "على كامل المستطيل"، كما يمكنك تنفيذ العملية من خلال شريط اﻷدوات أعلى نافذة العرض. بإمكانك أيضًا استخدام صورة في الخلفية إن أردت لكنك ستحتاج إلى عقدة من نوع TextureRect. إضافة تأثيرات صوتية للعبة مقاطع الصوت والموسيقى من أكثر العوامل التي تزيد من جاذبية تصميم الألعاب الإلكترونية، لهذا وضعنا في المجلد "art" الخاص بلعبتنا ملفين صوتيين الأول هو "House In a Forest Loop.ogg" لموسيقى الخلفية واﻵخر "gameover.wav" لخسارة اللاعب. وعلينا اﻵن إضافة عقدتين من النوع AudioStreamPlayer كأبناء للعقدة Main وتسميتهما باسم Music و DeathSound. انقر بعد ذلك على الخاصية Stream لكل منهما ثم "تحميل" واختر المقطع الصوتي الموافق. تُدرج المقاطع الصوتية تلقائيًا وتكون الخاصية Loop غير مفعلة لها بصورة افتراضية. فإن أردت أن تشغل الموسيقى دون توقف، فانقر على السهم المجاور لاسم الملف الصوتي في الخاصية stream ثم اختر "اجعله فريدًا" وانقر بعدها على المربع الذي يضم اسم العقدة الصوتية أعلى حاوية الفاحص واختر "stream" ثم فعّل قيمة الخاصية Loop. ولتشغيل الموسيقى أضف التابع ()Music.play$ إلى الدالة ()new_game والتابع ()Music.stop$ إلى الدالة ()game_over ثم أضف أخيرًا التابع ()DeathSound.play$ إلى الدالة ()game_over. اختصارات لوحة المفاتيح طالما أن التحكم باللعبة سيكون عن طريق لوحة المفاتيح، فمن الملائم جدًا أن نبدأ اللعبة بالضغط على أحد مفاتيحها. يمكن إنجاز اﻷمر بالاستفادة من الخاصية "Shortcut" للزر Button. لهذا سننشئ إجراء دخل يربط مفتاحًا بالزر "Start". انقر على مشروع>إعدادات الشروع ثم انقر على النافذة الفرعية "خريطة اﻹدخال" في النافذة التي تظهر. انشئ بعد ذلك وبنفس الطريقة التي أنشأت بها سابقًا إجراءات الحركة اﻷربعة إجراءًا جديدًا باسم start_game واربطه بالمفتاح Enter. وإن أردت أيضًا ربط أزرار أي أداة تحكم أخرى باﻹجراءات السابقة، تأكد من توصيل هذه اﻷداة ثم انقر اﻷيقونة "+" الموجودة إلى يمين كل إجراء ثم اضغط زر اﻷداة الذي تريد ربطه بهذا اﻹجراء. عد إلى الواجه HUD وانقر على الزر StartButton ثم ابحث عن الخاصية "Shortcut" ضمن "الفاحص" ثم انشئ اختصارًا جديدًا بالنقر على مربع النص المجاور واختيار "جديدة Shortcut" ثم النقر على الاختصار الجديد وفتح "Events" وإضافة حدث جديد بالنقر على "Array[InputEvent]". أنشئ حدث جديدة InputEventAction باسم start_game: وهكذا عندما يظهر زر "ابدأ" يمكن النقر عليه أو الضغط على المفتاح "Enter" لتبدأ اللعب! الخلاصة لقد انتهيت من تصميم أولى ألعابك ثنائية البعد وقد تعلمت إنشاء الشخصيات التي تتحكم بها والزواحف التي تتكاثر لتملأ الشاشة واستطعت حساب النتيجة وأنجزت طريقة لتبدأ اللعبة وتنهيها وتستخدم واجهة لها وأضفت مقاطع صوتية. تهانينا! لا زال هناك الكثير لتتعلمه بالطبع لتكون مطور ألعاب إلكترونية محترف، لكن خذ الآن قسطًا من الراحة واستمتع بإنجازك! ترجمة -وبتصرف- للمقالين: Heads_up display و Finishing up اقرأ أيضًا المقال السابق: إنشاء المشهد الرئيسي للعبة ثنائية الأبعاد في محرك الألعاب جودو Godot تعرف على أشهر لغات برمجة الألعاب كيف تصبح مبرمج ألعاب فيديو ناجح دليلك الشامل إلى بناء كاميرا خاصة بشاشات اللمس في محرّك اﻷلعاب جودو
-
بدأنا في مقال سابق بإنشاء لعبة إلكترونية باستخدام محرك الألعاب جودو، وحان الوقت اﻵن لضم كل شيء معًا وإنشاء مشهد كامل للعبتنا، لهذا سنبدأ في مقال اليوم باستكمال العمل على اللعبة وإنشاء مشهدها الأساسي وإعداده بالطريقة المناسبة. إنشاء عقدة المشهد الرئيسي للعبة لإنشاء مشهد جديد في محرك ألعاب جودو علينا إنشاء عقدة اسمها Main من النوع Node ولا حاجة أن تكون من النوع Node2D لأنها مجرد حاوية لمنطق اللعبة ولن تحتاج فعلًا إلى وظائف هذا النوع من العقد. انقر على زر إنشاء نسخة (أيقونة السلسلة في نافذة المشهد) ثم اختر المشهد "Player.tscn". أضف بعد ذلك العقد التالية كأبناء للعقدة Main وفق اﻷسماء التالية: مؤقت Timer باسم MobTimer للتحكم بمعدل تكاثر الزواحف، واضبط الخاصية Wait Time له على 0.5 مؤقت Timer باسم ScoreTimer لزيادة النتيجة كل ثانية، واضبط الخاصية Wait Time له على 1 مؤقت Timer باسم StartTimer لإضافة تأخير بسيط قبل بدء اللعبة، واضبط الخاصية Wait Time له على 2 عقدة من النوع Marker2D اسمها StartPosition لتحديد موقع البداية بالنسبة للاعب إضافة إلى ذلك، اضبط الخاصية One Shot للمؤقت StartTimer على "فعّال On" و الخاصية Position للعقدة StartPosition على القيمة التالية(450,240). توليد شخصيات الأعداء نستخدم المشهد الرئيسي Main في توليد شخصيات الأعداء ونريدهم أن يظهروا في أماكن عشوائية على حواف الشاشة، لهذا أضف عقدة من النوع Path2D باسم MobPath كابن للعقدة الرئيسية. سترى عدة أزرار إضافية في أعلى المحرر عند اختيار اﻷيقونة Path2D: اختر الزر الموجود في المنتصف "إضافة عقدة في مساحة خالية" ثم ارسم مسارًا بالنقر ضمن المحرر في النقاط التي يعرضها الشكل التالي. ولكي يكون تحديد النقاط دقيقًا فعّل الخيارين "استخدام المحاذاة للشبكة Use Grid Snap" و "استخدم المحاذاة الذكية Use Smart Snap"، وستجدهما إلى يمين أيقونة"القفل على شكل مغناطيس بجوار عدة نقاط وآخر بجوار شبكة كما توضح الصورة التالية: ملاحظة: ارسم المنحني باتجاه عقارب الساعة وإلا ستتكاثر الزواحف إلى الخارج بدلًا من الداخل. بعد تحديد أربع نقاط، انقر على أيقونة "إغلاق المنحني" وسيكتمل هذا المنحني. أضف بعد الانتهاء من رسم المنحني العقدة PathFollow2D كابن للعقدة MobPath وسمها MobSpawnLocation. ستدور هذه العقدة وتتبع المسار عندما تتحرك وبالتالي يمكن استخدامها لاختيار مواقع واتجاهات عشوائية على طول المسار. سيبدو المشهد الرئيسي اﻵن كما يلي: السكربت الرئيسي أضف نص برمجي إلى المشهد Main ثم أضف له العبارة export var mob_scene: PackedScene@ التي تسمح لنا باختيار مشهد اﻷعداء الذي نريد صنع نسخة عنه: extends Node @export var mob_scene: PackedScene var score انقر على العقدة Main وستجد الخاصية Mob Scene ضمن الفاحص في لوحة "متغيرات السكربت"، وﻹسناد قيمة إلى هذه الخاصية يمكنك اتباع إحدى الطريقتين التاليتين: اسحب المشهد من حاوية نظام الملفات وأفلته ضمن الخاصية Mob Scene. انقر على زر السهم المجاور للخاصية Mob Scene وانقر "تحميل" ثم اختر mob.tscn. اختر نسخة العقدة Player الموجودة ضمن العقدة Main ثم انقر على حاوية "عقدة" إلى جوار "الفاحص" في الشريط الجانبي لترى قائمة تضم جميع اﻹشارات للعقدة Player. جد اﻹشارة hit وانقر عليها نقرة مزدوجة (أو انقر عليها بالزر الأيمن واختر "توصيل"). ستفتح هذه العملية نافذة جديدة نريد فيها إنشاء دالة جديدة ندعوها game_over تعالج ما نريد فعله عند انتهاء اللعبة. اكتب "game_over" في المربع النصي "الدالة المتلقية Receiver Method" أسفل النافذة ثم انقر زر "وصل Connect". وما سيحدث اﻵن أن اﻹشارة hit التي بثها اللاعب Player سيعالجها السكربت اﻷساسي "Main". أضف اﻵن الشيفرة التالية إلى الدالة الجديدة، وأضف أيضًَا الدالة new_game التي تحضّر كل شيء عند بداية اللعبة: func game_over(): $ScoreTimer.stop() $MobTimer.stop() func new_game(): score = 0 $Player.start($StartPosition.position) $StartTimer.start() صل اﻵن اﻹشارة ()timeout العائدة إلى كل عقدة مؤقت (StartTimer و ScoreTimer و MobTimer) إلى السكربت الرئيسي. وسيشغل المؤقت StartTimer المؤقتين الآخرين ويزيد المؤقت ScoreTimer النتيجة بمقدار 1. func _on_score_timer_timeout(): score += 1 func _on_start_timer_timeout(): $MobTimer.start() $ScoreTimer.start() سننشئ ضمن الدالة نسخة عن العدو (الزواحف)، لهذا، سنختار مكانًا عشوائيًا للبدء على المسار Path2D ونضبط حركة العدو. تدور العقدة تلقائيًا عندما تلحق بالمسار، لهذا سنستفيد منها في اختيار جهة حركة الزاحف وموقعه. وعندما تتكاثر الزواحف سنختار قيمة عشوائية لسرعة حركتها بين 150 و 250. وانتبه إلى أن إضافة نسخة جديدة إلى المشهد تكون من خلال التعليمة add_child: func _on_mob_timer_timeout(): # mob scene أنشئ نسخة من مشهد الزاحف var mob = mob_scene.instantiate() # Path2D اختر مكانًا عشوائيًا على المسار. var mob_spawn_location = get_node("MobPath/MobSpawnLocation") mob_spawn_location.progress_ratio = randf() # اجعل اتجاه الزاحف عمودًا على اتجاه المسار var direction = mob_spawn_location.rotation + PI / 2 # اختر موقعًا عشوائيًًًًًًًًًا للزاحف mob.position = mob_spawn_location.position # أضف بعض العشوائية إلى المسار direction += randf_range(-PI / 4, PI / 4) mob.rotation = direction # اختر سرعة الزاحف var velocity = Vector2(randf_range(150.0, 250.0), 0.0) mob.linear_velocity = velocity.rotated(direction) # إجعل الزواحف تتكاثر بإضافتها إلى الشاشةالرئيسية add_child(mob) ملاحظة هامة: قد تتساءل لماذا نستخدم العدد PI في الدوال التي تتعامل مع الزوايا؟ لأن جودو يستخدم الراديان لقياس الزوايا. تمثّل PI نصف دوره كما يمكن استعمال TAU التي تمثّل دورة كاملة. لكن إن كنت تفضّل العمل مع الدرجات ستحتاج إلى الدالتين ()rad_to_deg و ()deg_to_rad للتحويل بين الدرجات والراديان. اختبار المشهد نختبر اﻵن إذا كانت كل شيء على ما يرام حتى اللحظة، لهذا استدعي الدالة new_game ضمن الدالة ()ready_: func _ready(): new_game() لنجعل أيضًا المشهد Main المشهد الرئيسي في اللعبة، وهو المشهد الذي يعمل تلقائيًا. انقر على زر التشغيل واختر main.tscn عندما يُطلب ذلك. من المفترض أن تكون قادرًا على تحريك اللاعب في جميع الاتجاهات وترى الزواحف تتحرك وتتكاثر، وسترى كيف يختفي اللاعب عندما يصطدم بالعدو. عندما تتأكد أن كل شيء يعمل جيدًا أزل الدالة new_game من الدالة ()ready_. الخلاصة تعلمنا في مقال اليوم كيف ننشئ المشهد الأساسي للعتبنا الإلكترونية وسنكتفي في هذا المقال بهذه المرحلة، ومع ذلك لا تزال اللعبة غير مكتملة وتنقصها بعض اللمسات النهائية وإنشاء واجهة مناسبة للعبة، لذا سنشرح خطوات تعزيز اللعبة بواجهة تتضمن تأثيرات صوتية واختصارات لوحة المفاتيح وغيرها من الخيارات وهذا ما سنراه في المقال التالي من هذه السلسلة. ترجمة -وبتصرف- للمقال: The main game scene اقرأ أيضًا المقال السابق: إنشاء وبرمجة مشاهد لعبة ثنائية الألعاب في محرك جودو إعداد محرك الألعاب جودو Godot للعمل مع قاعدة البيانات SQLite مدخل إلى محرك الألعاب جودو Godot مطور الألعاب: من هو وما هي مهامه
-
بعد أن أنشأنا في المقال السابق ملفات مشروع لعبة "تفادي الزواحف" ونظمناه، سنبدأ في هذا المقال بالعمل على شخصيات اللعبة وهي لاعب أساسي وأعداء، إذ سنبني المشهد اﻷول باسم Player الذي يمثل اللاعب، ومشهد آخر باسم Mob يمثل الأعداء، ومن ميزات إنشاء مشهد لكل منهما هو إمكانية اختبارها بشكل مستقل قبل أن ننشئ بقية أجزاء اللعبة. هيكلية العقدة نحتاج بداية إلى عقدة جذرية للاعب، وكقاعدة عامة، لا بد أن تعكس العقدة الجذرية للمشهد الوظيفة المرغوبة للكائن وما هو هذا الكائن. لهذا، انقر على زر + لإضافة عقدة أخرى، وأضف عقدة من نوع Area2D إلى المشهد. سيعرض جودو أيقونة تنبيه بجوار العقدة في شجرة المشاهد، تجاهلها اﻵن وسنعود إليها لاحقًا. نتمكن باستخدام Area2D من استشعار الكائنات التي تتداخل أو تعمل ضمن اللاعب، وسنغير اسم العقدة الجديدة إلى Player بالنقر المضاعف عليها. وبعد إنشاء العقدة الجذرية، سنضيف عقدًا إضافية لمنحها مقدرات وظيفية أكبر. لكن قبل ذلك، علينا أن نتأكد من عدم تحريك أو تغيير العقد اﻷبناء عند النقر عليهم. لهذا انقر على اﻷيقونة الواقعة على يسار أيقونة القفل في شريط أدوات المشهد سيعرض لك وصف اﻷداة عند تمرير مؤشر الفأرة فوقها العبارة "اجعل فروع العقدة المختارة غيرقابلة للاختيار": احفظ المشهد بالنقر على مشهد ثم حفظ أو اضغط Ctrl + S في ويندوز ولينكس أو Cmd + S في ماك أو إس. سنستخدم في مشروعنا أسلوب التسمية المتبع في محرك الألعاب جودو وهو كالتالي حسب لغة البرمجة المستخدمة: في لغة GDScript: نتبع أسلوب باسكال في اﻷصناف (الحرف اﻷول من كل كلمة كبير)، وفي الدوال والمتغيرات أسلوب اﻷفعى حيث تفصل بين كل كلمتين شرطة سفلية _، أما الثوابت فتكتب كل حروفها بالشكل الكبير. في لغة #C: تسمى الأصناف والمتغيرات والتوابع بأسلوب باسكال، ونستخدم في تعريف الحقول الخاصة private والمتغيرات المحلية والمعاملات أسلوب سنام الجمل أي الحرف اﻷول من كل كلمة كبير ما عدا الكلمة اﻷولى. وتأكد من كتابة أسماء التوابع بشكل دقيق عند ربط اﻹشارات. الرسم المتحرك للشخصية Sprite انقر على أيقونة العقدة Player وأضف عقدة ابن من نوع AnimatedSprite2D (استخدم Ctrl + A في ويندوز ولينكس) والتي تتولى أمور مظهر وتحريك اللاعب، ولاحظ وجود إشارة تحذير إلى جانب العقدة. تحتاج العقدة موردًا يُدعى "إطارات السبرايت SpriteFrames"، وﻹنشائه، ابحث عن الخاصية SpriteFrames ضمن النافذة الفرعية Animation في حاوية "الفاحص" ثم انقر على مربع النص empty واختر "جديدة SpriteFrame". انقر مجددًا لفتح لوحة "إطارات-اﻷرسومة". ستجد إلى اليمين قائمة بالرسومات، انقر على الافتراضية وسمها "walk"، ثم انقر على أيقونة إضافة إطار في الزاوية العليا اليمينية وأضف إطارًا آخر سمِّه "up". ابحث بعد ذلك عن الصور المناسبة في المجلد "art" في نظام الملفات وانقل الصور playerGrey_walk[1/2] إلى اﻹطار "walk" بالسحب واﻹفلات، أو بفتح الصورة من خلال أيقونة المجلد وكرر العملية بنقل الصورتين playerGrey_up[1/2] إلى اﻹطار "up". إن أبعاد الصور أكبر من أبعاد نافذة اللعبة، ولا بد من تصغير هذه الصور بالنقر على العقدة AnimatedSprite2D ومن ثم ضبط الخاصية Scale على القيمة Scale. ستجد هذه الخاصية في حاوية الفاحص تحت العنوان Node2D والقائمة "Transform تحويل": أضف أخيرًا عقدة من النوع CollisionShape2D لتكون ابنًا للعقدة Player، وتحدد هذه العقدة "صندوق التصادم" المحيط باللاعب أو حدود منطقة التصادم المحيطة به. وتلائمنا في هذا الصدد كائن من النوع CapsuleShape2D، لهذا انقر في "الفاحص" على المربع إلى جوار العنوان واختر "جديدة CapsuleShape2D". استخدم بعد ذلك مقبضي التحكم بالأبعاد (النقطتين الحمراوين) في نافذة المشهد لتغطية الأرسومة بالغلاف: عندما تنتهي من ذلك سيكون شكل مشهد اللاعب Player كالتالي: تأكد من حفظ المشهد مجددًا بعد هذه التغييرات. سنضيف تاليًا سكربت إلى عقدة اللاعب لتحريكه ثم نُعدّ آلية لترصد التصادم لنعرف إذا ما اصطدم اللاعب بشيء ما. كتابة الشيفرة اللازمة لتحريك اللاعب سنعمل في هذا القسم على كتابة شيفرة لتحريك اللاعب، وإعداده ليترصد التصدامات، لهذا، علينا إضافة بعض الخواص الوظيفية التي لا تقدمها العقد المتوفرة عن طريق إضافة سكربت أو كود برمجي إلى العقدة. انقر على العقدة Player ثم انقر على "إلحاق نص برمجي": لا داعي لتغيير أي شيئ في نافذة إلحاق نص برمجي، اترك كل شيء كما هو وانقر على زر "أنشئ". ملاحظة: إن كنت تريد إنشاء سكربت #C، اختر هذه اللغة من القائمة المنسدلة قبل النقر على "أنشئ". extends Area2D @export var speed = 400 # How fast the player will move (pixels/sec). var screen_size # Size of the game window. تسمح لنا التعليمة export قبل المتغير speed بضبط قيمته في نافذة الفاحص. ولهذا اﻷمر فائدته إن أردت تعديل قيمة المتغير بالطريقة نفسها التي تعدّل فيها خاصيات أي عقدة موجودة أصلًا في جودو. انقر اﻵن على العقدة Player وسترى الخاصية موجودة ضمن قسم "متغيرات السكربت" في حاوية الفاحص تحت نفس الاسم الذي يحمله ملف السكربت. وتذكر أن تغيير القيمة في هذا المكان سيلغي القيمة التي يحملها المتغير في السكربت. يتضمن السكربت player.gd تلقائيًا الدالتين ()ready_ و ()process_. فإن لم تختر القالب الافتراضي للسكربت أنشئ هاتين الدالتين. وتُستدعى الدالة ()ready_ عندما تدخل عقدة شجرة المشاهد وهو وقت مناسب لمعرفة أبعاد نافذة اللعبة func _ready(): screen_size = get_viewport_rect().size بإمكاننا اﻵن استخدام الدالة ()process_ لتحديد ما يفعله اللاعب، وتُستدعى هذه الدالة من أجل كل إطار ونستخدمها لتحديث العناصر في لعبتنا والتي نتوقع أن تتغير أحيانًا. فمن أجل لاعبنا لا بد من: التحقق من وجود دخل. تحريكه في الاتجاه المطلوب. تشغيل الرسوم المتحركة المناسبة. كما ذكرنا علينا بداية التحقق من الدخل، أي هل يضغط اللاعب على زر معين مثلًا؟ ففي لعبتنا هناك عناصر إدخال لأربعة اتجاهات علينا أن نتحقق منها. عُرّفت إجراءات الدخل في إعدادات المشروع تحت عنوان "خريطة الإدخال". وفيها نستطيع تعريف أحداث مخصصة وتعيين أزرار مختلفة، وأحداث تتعلق بالفأرة وغيرها من المدخلات. انقر على المشروع، ثم إعدادات المشروع لتفتح نافذة اﻹعدادات، ثم انقر على النافذة الفرعية "خريطة الإدخال" في الأعلى. اكتب بعد ذلك "move_right" أي تحرك يمينًا في الشريط العلوي وانقر الزر "أضف" ﻹضافة الإجراء move_right. علينا اﻵن أن نربط اﻹجراء بزر معين، لهذا انقر على أيقونة "+" إلى اليسار كي نفتح نافذة "تهيئة الحدث event configuration". كل ما عليك الآن هو النقر على الزر الذي تريد ربطه بالحدث سواء زر لوحة مفاتيح أو زر الفأرة. انقر الآن مفتاح السهم اليميني على لوحة المفاتيح وسيظهر الخيار تلقائيًا في مربع "يتم رصد المدخلات.." انقر بعد ذلك على "حسنًا" لتعيين المفتاح. كرر نفس الخطوات لربط الحركات الثلاث الباقية كالتالي: اربط move_left بالسهم اليساري. اربط move_up بالسهم للأعلى. اربط move_down بالسهم للأسفل. يجب أن تظهر خارطة المدخلات كالتالي: انقر اﻵن على "إغلاق" ﻹغلاق إعدادات المشروع. ملاحظة: ربطنا مفتاح واحد بكل إجراء دخل، لكنك تستطيع أن تربط أكثر من مفتاح أو زر عصا تحكم أو زر فأرة بإجراء الدخل نفسه. تستطيع أن تقرر إذا ما ضُغط زر باستخدام العبارة ()Input.is_action_pressed التي تعيد القيمة true إذا ضُغط الزر وfalse إن لم يُضغط. func _process(delta): var velocity = Vector2.ZERO # The player's movement vector. if Input.is_action_pressed("move_right"): velocity.x += 1 if Input.is_action_pressed("move_left"): velocity.x -= 1 if Input.is_action_pressed("move_down"): velocity.y += 1 if Input.is_action_pressed("move_up"): velocity.y -= 1 if velocity.length() > 0: velocity = velocity.normalized() * speed $AnimatedSprite2D.play() else: $AnimatedSprite2D.stop() بدأنا بضبط قيمة velocity على (0, 0) فلن يتحرك اللاعب افتراضيًا، وبعد ذلك تحققنا من كل المدخلات وأضفنا إلى المتغير velocity أو طرحنا منه للحصول على الاتجاه. فلو ضغطنا على السهم اليميني واليساري في نفس الوقت ستكون نتيجة المتجه velocity هي (1, 1)، وفي هذه الحالة نكون قد أضفنا حركة أفقية وعمودية في نفس الوقت، وسيتحرك اللاعب بشكل أسرع بالاتجاه القطري موازنة بالحالة التي يتحرك فيها أفقيًا فقط. لكن بإمكاننا منع حدوث هذا اﻷمر بتسوية قيمة السرعة بأن نضبط قيمتها على 1 ثم نضربه بالقيمة المطلوبة ولن تكون السرعة في الاتجاه القطري عندها أكبر. كما علينا التحقق فيما لو تحرّك اللاعب كي نستدعي الدالتين ()play و ()stop في AnimatedSprite2D يُعيد $ عقدة معينة إن كانت موجودة في نفس المسار النسبي ويعيد null إن لم تكن موجودة في هذا المسار. وطالما أن AnimatedSprite2D هي عقدة ابن للعقدة الحالية، بإمكاننا استخدام AnimatedSprite2D$. وطالما حددنا الآن اتجاه الحركة، بإمكاننا تحديث موقع اللاعب. كما نستطيع باستخدام الدالة ()clamp منع اللاعب من مغادرة الشاشة وتقييده ضمن مجال محدد. أضف الشيفرة التالية إلى أسفل الدالة ()process_ وانتبه لأن الشيفرة غير مزاحة بعملية indentation تحت التعليمة else: position += velocity * delta position = position.clamp(Vector2.ZERO, screen_size) انقر على الزر "شغل المشهد الحالي" أو انقر على F6 أو Cmd+R في ماك أو إس وتأكد من قدرتك على تحريك اللاعب ضمن المشهد في جميع الاتجاهات. اختيار الرسوم المتحركة بإمكاننا تحريك اللاعب الآن، لكننا نحتاج إلى تغيير الرسم المتحرك الذي يمثّل الكائن وفقًا لاتجاهه. ليدنا الرسم "تحرّك" والذي يعرض اللاعب وهو يتحرك يمينًا، ولا بد من قلبه أفقيًا حتى يعبّر عن التحرك نحو اليسار باستخدام الخاصية flip_h. وكذلك لدينا الرسم "up" الذي يجب أن يُعكس عموديًا لتمثيل الحركة نحو اﻷسفل باستخدام الخاصية flip_v. لهذا عليك اضافة الشيفرة التالية إلى أسفل الدالة ()process_: if velocity.x != 0: $AnimatedSprite2D.animation = "walk" $AnimatedSprite2D.flip_v = false # اطلع على املاحظة التالية بخصوص اﻹسناد المنطقي $AnimatedSprite2D.flip_h = velocity.x < 0 elif velocity.y != 0: $AnimatedSprite2D.animation = "up" $AnimatedSprite2D.flip_v = velocity.y > 0 ملاحظة: يُعد استخدام طريقة اﻹسناد المنطقي في هذه الشيفرة اختصارًا شائعًا. فما نفعله هو اختبار موازنة (منطقي) وإسناد قيمة منطقية، لهذا يمكننا تنفيذ اﻷمرين معًا. وما يفعله هذا الاختصار مطابق لعمل الشيفرة التالية: if velocity.x < 0: $AnimatedSprite2D.flip_h = true else: $AnimatedSprite2D.flip_h = false شغّل المشهد وتأكد من تغيّر الرسم مع تغير اتجاه الحركة. عندما تتأكد أن كل شيء يعمل كما يجب، أضف السطر التالي إلى الدالة ()ready_ كي يختفي اللاعب في بداية اللعبة. hide() إعداد التصادمات نريد من اللاعب Player أن يعرف متى يستطدم بالعدو، لكننا لم نصنع اﻷعداء بعد! لا بأس بذلك لأننا سنستخدم حاليًا إشارات جودو لننجز اﻷمر. أضف اﻷسطر التالية إلى أعلى السكربت. فإن كنت تستخدم GDScript، أضفها بعد العبارة extends Area2D، وإن كنت تستخدم لغة #C ضعها بعد العبارة public partial class Player: Area2D. signal hit تُعرّف التعليمة السابقة إشارة خاصة باسم "hit" يبثها اللاعب (يرسلها) عندما يتصادم مع عدو. وسنستخدم الكائن Area2D لالتقاط هذه اﻹشارة. اختر العقدة Player وانقر على النافذة الفرعية "عقدة" ضمن لوحة "الفاحص" كي تعرض قائمة اﻹشارات التي يمكن للاعب بثها: لاحظ وجود إشارتنا المخصصة hit أيضًا ضمن تلك القائمة. وطالما أن العدو سيكون عقدة من النوع RigidBody2D، سنحتاج إلى الإشارة body_entered(body: Node2D). أوجد تلك اﻹشارة في القائمة ثم انقر عليها بالزر اليميني واختر "يتصل" لتظهر نافذة "قم بوصل اﻹشارة إلى دالة". لا حاجة لتغيير أي شيء، بل انقر فقط على "وصل" وسيوّلد جودو تلقائيًا الدالة المناسبة في الشيفرة: لاحظ اﻷيقونة الخضراء إلى يسار الشيفرة المخصصة للإشارة وتدل على أن إشارة متصلة مع هذه الدالة. أضف اﻵن الشيفرة التالية إلى الدالة: func _on_body_entered(body): hide() # يختفي اللاعب بعد أن يصطدم. hit.emit() # Must be deferred as we can't change physics properties on a physics callback. $CollisionShape2D.set_deferred("disabled", true) في كل مرة يصدم بها العدو اللاعب ستُرسل اﻹشارة، ولا بد من تعطيل التصادم الخاص باللاعب كي لا نفعّل اﻹشارة hit أكثر من مرة. ملاحظة: قد ينتج عن تعطيل غلاف التصادم الخاص بالمنطقة خطأ إن حدث اﻷمر أثناء معالجة المحرّك للتصادمات. لهذا استخدم الدالة ()set_deferred ﻹخبار المحرّك ألا يعطّل غلاف التصادم حتى يرى أن اﻷمر آمن. أضف أخيرًا دالة نستدعيها ﻹعادة ضبط اللاعب عندما تبدأ لعبة جديدة func start(pos): position = pos show() $CollisionShape2D.disabled = false إنشاء شخصية العدو حان الوقت اﻵن ﻹنشاء اﻷعداء الذي يجب على اللاعب تفاديهم. ولن يكون سلوكهم معقدًا جدًا بل سيتحركون عشوائيًا على أطراف الشاشة، يأخذون اتجاهًا عشوائيًا ويتحركون وفق خط مستقيم. نبدأ عملنا بإنشاء مشهد باسم Mob يشكل الأساس الذي نشتق منه أي عدد نحتاجه من هذه الكائنات في لعبتنا. إعداد العقدة انقر على مشهد ثم مشهد جديد ثم أضف العقد التالية وفق الترتيب المبين: RigidBody2D AnimatedSprite2D CollisonShape2D VisibleOnScreenNotifier2D ولا تنسَ ضبط العقدة اﻷم كي لا يمكن اختيار اﻷبناء كما فعلنا سابقًا عند بناء شخصية اللاعب. اختر بعد ذلك العقدة Mob ثم اضبط قيمة الخاصية Gravity Scale على 0، وذلك في قسم RigidBody2D ضمن الفاحص. يمنع هذا اﻷمر الأعداء من السقوط للأسفل. افتح المجموعة "Collision" الموجودة في اللوحة "CollisionObject2D" تحت "RigidBody2D" ضمن الفاحص. الغ بعد ذلك تفعيل الخيار 1 ضمن الخاصية Mask بالنقر عليه كي لا تتصادم اﻷعداء فيما بينها. اضبط العقدة كما فعلنا في مشهد اللاعب، وهنا نستخدم ثلاث رسومات هي fly و swim و walk، وهنالك صورتان لكل مشهد في المجلد art. تُضبط الخاصية Animation Speed أي سرعة التحريك لكل رسم متحرك على حدة، لهذا اضبط كلًا منها على 3: بإمكانك اﻵن النقر على الزر "تشغيل الرسم المتحرك" إلى يسار "سرعة التحريك" لعرض الرسوم المتحركة. سنختار إحدى هذه الرسوم عشوائيًا حتى يكون للأعداء أشكال مختلفة، وكما هو الحال مع رسومات اللاعب لا بد من تصغير هذه الرسومات، وذلك بضبط الخاصية Scale على (0.75, 0.75). علينا اﻵن إضافة غلاف CapsuleShape2D من أجل التصادمات كما فعلنا مع اللاعب. ولكي يتماشى الغلاف مع الرسم المتحرك لا بد من تدويره بضبط الخاصية Rotation Degrees على 90 تحت لوحة "Node2D" والقائمة "Transform" ضمن الفاحص) كتابة شيفرة تحريك العدو أضف سكربت إلى العقدة Mob كما فعلنا سابقًا: extends RigidBody2D نشغّل باستخدام الدالة ()ready_ الرسومات ونختار عشوائيًا أحد اﻷنواع الثلاث لهذه الرسوميات كالتالي: func _ready(): var mob_types = $AnimatedSprite2D.sprite_frames.get_animation_names() $AnimatedSprite2D.play(mob_types[randi() % mob_types.size()]) ما تفعله هذه الشيفرة هو الحصول على أسماء الرسومات من الخاصية frames للعقدة AnimatedSprite2D، وستكون النتيجة مصفوفة تضم اﻷنواع الثلاث: ["walk", "swim", "fly"]. ثم نختار عشوائيًا رقمًا بين 0 و 2 لاختيار أحد اﻹطارات الثلاث من المصفوفة السابقة بتطبيق التعليمة randi() % n، والتي تختار عددًا صحيحًا عشوائيًا بين 0 و n-1. وأخيرًا نحتاج إلى شيفرة كي يحذف العدو نفسه عندما يغادر شاشة اللعبة. ولتنفيذ ذلك صل الإشارة ()Screen_exited العائدة للعقدة ()VisibleOnScreenNotifier إلى العقدة Mob (راجع فقرة وصل إشارة اللاعب التي نفّذناها سابقًا) ثم أضف الأمر ()queue_free إلى الدالة التي تظهر في السكربت كالتالي: func _on_visible_on_screen_notifier_2d_screen_exited(): queue_free() وهكذا سيكتمل مشهد العدو. الخلاصة بهذا نكون قد وصلنا لنهاية مقالنا الحالي الذي أنشأنها فيه مشهدين من مشاهد اللعبة ثنائية الأبعاد في محرك جودو، تابع معنا المقال التالي الذي سنقوم فيه بضم المشهدين معًا والسماح للأعداء بالتكاثر على الشاشة والحركة لتحويل المشاهد إلى لعبة تعمل كما خططنا لها. ترجمة -وبتصرف- للمقالات: Creating the Player scene و Coding the player و Creating the enemy اقرأ أيضًا المقال السابق: تجهيز وضبط ملفات لعبة ثنائية البعد عبر محرك الألعاب Godot كيف تحصل على أفكار ألعاب فيديو ناجحة تعرف على أشهر لغات برمجة الألعاب مدخل إلى محرك الألعاب جودو Godot
-
ننقلك في سلسلة المقالات التالية خطوة بخطوة لإنشاء لعبة كاملة ثنائية البعد باستخدام محرّك اﻷلعاب جودو Godot. وفي نهاية السلسلة ستكون قد أنجزت لعبة بسيطة كتلك الموضحة في الصورة التالية: وسنتعلم من خلال هذه السلسلة كيفية عمل محرّر محرك الألعاب جودو Godot، وكيفية هيكلة المشروع، ومن ثم ستتعلم خطوات بناء اللعبة بشكل عملي. ملاحظة: هذا المقال هو مدخل إلى محرك اﻷلعاب جودو، ويفترض أنك تتمتع ببعض الخبرات البرمجية لاستخدام لغات برمجة الألعاب وبرمجة لعبتك الإلكترونية من خلالها. فإن كنت جديدًا في عالم البرمجة. ننصحك بالعودة إلى أكاديمية حسوب التي تضم عددًا كبيرًا من المقالات والمواضيع التي تناسب القادمين الجدد وتقدم أفضل الدورات التعليمية للبرمجة من الصفر وحتى الاحتراف. سوف نطلق على لعبتا اسم "تفادي الزواحف Dodge the creeps"، ومن المفترض أن تبتعد شخصية اللعبة عن اﻷعداء قدر اﻹمكان. ستتعلم من خلال هذه السلسلة كيف تقوم بما يلي: تنشئ لعبة مكتملة ثنائية البعد باستخدام محرك الألعاب جودو. تهيكل مشروع لعبة بسيطة. تحرك شخصية اللعبة وتغيير شكلها. تنشر أعداء عشوائيًا. تعيد نتيجة اللعبة. لماذا نطور لعبة ثنائية البعد 2D؟ إن كنت جديدًا في تطوير اﻷلعاب أو لا تألف بيئة جودو، ننصحك ان تبدأ بتعلم تصميم اﻷلعاب ثنائية البعد، فهي تسمح لك في تعلم بيئة العمل وترتاح فيها قبل أن تبدأ اﻷلعاب ثلاثية الأبعاد التي تميل أكثر إلى التعقيد. خُصِّصَ هذا المقال والمقالات اللاحقة للمبتدئين الذين لديهم أساسيات في التعامل مع محرك جودو، فإن كنت جديدًا في البرمجة ويصعب عليك كتابة الأكواد من الصفر، بإمكانك الاطلاع على الشيفرة المصدرية للعبة عبر جيت-هب وفهمها. كما حضرنا مسبقًا بعض الملحقات التي تحتاجها، لهذا سنقفز مباشرة إلى الشيفرة التي يمكنك تحميلها من المستودع المخصص على جيت-هب. إعداد المشروع سنُعّد في هذا المقال مشروعنا وننظمه، لهذا شغّل محرك ألعاب جودو وأنشئ مشروعًا جديدًا. ليس عليك سوى تحديد مسار مناسب لتخزين المشروع وبإمكانك ترك بقية القيم كما هي. بعدها عليك تحميل الأرشيف الذي يتضمّن كافة ملفات الصور والمقاطع الصوتية التي سنستخدمها في صنع اللعبة ثنائية الأبعاد الخاصة بنا، ثم استخرج محتوياته وانقل المجلدين /art و /fonts إلى مجلد اللعبة. يجب أن يبدو مجلد اللعبة مشابهًا للقطة الشاشة التالية: صُممت هذه اللعبة لنمط العرض الشاقولي portrait، لهذا لا بد من تعديل قياس نافذة اللعبة. انقر على المشروع ثم اختر إعدادات المشروع لفتح نافذة اﻹعدادات ثم افتح في العمود اليميني القائمة "عرض display" ثم انقر على "نافذة window". واضبط بعد ذلك اتساع نافذة العرض viewport width على 480 وارتفاعها على 720. وتحت الخيار "تمدد Stretch " اضبط "الوضع Mode" على القيمة canvas_items ونسبة العرض على القيمة keep، حيث يساهم هذان الخياران في اتساق العرض على شاشات مختلفة الأبعاد. تنظيم المشروع سنصنع في هذا المشروع ثلاثة مشاهد مستقلة هي Player و Mob وHUD بحيث تجتمع كلها في المشهد الرئيسي Main. ومن اﻷفضل في المشاريع اﻷكبر أن تنشئ مجلدًا يضم المشاهد المختلفة والسكربتات الملحقة بها، لكن لمشروع صغير كهذا، بإمكانك تخزينها في المجلد الجذري للمشروع الذي يُعرّف بالعنوان //:res . وستجد مجلد المشروع في حاوية "نظام الملفات" في الزاوية اليسارية السفلى. الخلاصة تعرفنا في مقال اليوم على أولى مراحل برمجة لعبة ثنائية الأبعاد في محرك الألعاب جودو Godot، وبدأنا بتحديد الشخصيات والمشاهد التي تضمها اللعبة، وحددنا طريقة تنظيم ملفاتها، وندعوك لمتابعة السلسلة التالية من هذه المقالات لتتعرف على الخطوات التالية العملية لإنجاز اللعبة وبرمجتها. ترجمة -وبتصرف- للمقالين: your first 2D game و Setting up the project اقرأ أيضًا المقال السابق: تعلم الميزات الجديدة في محرك الألعاب جودو وطرح الأسئلة حوله إعداد محرك الألعاب جودو Godot للعمل مع قاعدة البيانات SQLite مدخل إلى محرك الألعاب جودو Godot مطور الألعاب: من هو وما هي مهامه