عند تطوير الألعاب باستخدام جودو، من الشائع أن نربط الأصوات بالأحداث التي تقوم بها الشخصيات، كتشغيل صوت معين عند موت الشخصية وصوت آخر عندما تنفذ هجومًا معينًا. ونستخدم غالبًا العقدة 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
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.