1. قبل البدء
تمثّل الألعاب تجارب سمعية وبصرية. يوفّر Flutter أداة رائعة لإنشاء مرئيات رائعة وواجهة مستخدم صلبة، ما يتيح لك التركيز على العناصر المرئية. المكوّن المتبقّي المتبقّي هو الصوت. ستتعرّف في هذا الدرس التطبيقي على كيفية استخدام المكوّن الإضافي flutter_soloud
لإضافة الصوت والموسيقى في وقت الاستجابة المنخفض إلى مشروعك. تبدأ بسقالة أساسية بحيث يمكنك الانتقال مباشرة إلى الأجزاء المثيرة للاهتمام.
يمكنك بالطبع استخدام المعلومات التي تتعلّمها هنا لإضافة محتوى صوتي إلى تطبيقاتك، وليس فقط الألعاب. على الرغم من أنّ جميع الألعاب تقريبًا تتطلّب الصوت والموسيقى، فإنّ معظم التطبيقات لا تتطلّب ذلك، لذلك يركّز هذا الدرس التطبيقي على الألعاب.
المتطلبات الأساسية
- لديهم إلمام أساسي ببرنامج Flutter.
- معرفة كيفية تشغيل تطبيقات Flutter وتصحيح الأخطاء فيها
المعلومات التي تطّلع عليها
- كيفية تشغيل أصوات لقطة واحدة
- تعرَّف على كيفية تشغيل وتخصيص حلقات موسيقية ثابتة.
- كيفية التلاشي للداخل والخارج.
- كيفية تطبيق مؤثرات بيئية على الأصوات.
- كيفية التعامل مع الاستثناءات
- كيفية التفاف كل هذه الميزات في وحدة تحكم صوت واحدة.
ما تحتاج إليه
- حزمة تطوير البرامج (SDK) مع Flutter
- أداة تعديل الرموز التي تختارها
2. إعداد
- قم بتنزيل الملفات التالية. إذا كان الاتصال بطيئًا، لا داعي للقلق. وستحتاج إلى الملفات الفعلية لاحقًا، لكي تتمكن من السماح بتنزيلها أثناء العمل.
- أنشِئ مشروعًا على Flutter باستخدام اسم من اختيارك.
- أنشئ ملف
lib/audio/audio_controller.dart
في المشروع. - في الملف، أدخِل الرمز التالي:
lib/audio/audio_controller.dart
import 'dart:async';
import 'package:logging/logging.dart';
class AudioController {
static final Logger _log = Logger('AudioController');
Future<void> initialize() async {
// TODO
}
void dispose() {
// TODO
}
Future<void> playSound(String assetKey) async {
_log.warning('Not implemented yet.');
}
Future<void> startMusic() async {
_log.warning('Not implemented yet.');
}
void fadeOutMusic() {
_log.warning('Not implemented yet.');
}
void applyFilter() {
// TODO
}
void removeFilter() {
// TODO
}
}
كما ترى، هذا مجرد هيكل للوظائف المستقبلية. وسننفّذها كلّها خلال هذا الدرس التطبيقي حول الترميز.
- افتح بعد ذلك ملف
lib/main.dart
، ثم استبدِل محتواه بالرمز التالي:
lib/main.dart
import 'dart:developer' as dev;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'audio/audio_controller.dart';
void main() async {
// The `flutter_soloud` package logs everything
// (from severe warnings to fine debug messages)
// using the standard `package:logging`.
// You can listen to the logs as shown below.
Logger.root.level = kDebugMode ? Level.FINE : Level.INFO;
Logger.root.onRecord.listen((record) {
dev.log(
record.message,
time: record.time,
level: record.level.value,
name: record.loggerName,
zone: record.zone,
error: record.error,
stackTrace: record.stackTrace,
);
});
WidgetsFlutterBinding.ensureInitialized();
final audioController = AudioController();
await audioController.initialize();
runApp(
MyApp(audioController: audioController),
);
}
class MyApp extends StatelessWidget {
const MyApp({required this.audioController, super.key});
final AudioController audioController;
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter SoLoud Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.brown),
useMaterial3: true,
),
home: MyHomePage(audioController: audioController),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.audioController});
final AudioController audioController;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const _gap = SizedBox(height: 16);
bool filterApplied = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Flutter SoLoud Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: () {
widget.audioController.playSound('assets/sounds/pew1.mp3');
},
child: const Text('Play Sound'),
),
_gap,
OutlinedButton(
onPressed: () {
widget.audioController.startMusic();
},
child: const Text('Start Music'),
),
_gap,
OutlinedButton(
onPressed: () {
widget.audioController.fadeOutMusic();
},
child: const Text('Fade Out Music'),
),
_gap,
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Apply Filter'),
Checkbox(
value: filterApplied,
onChanged: (value) {
setState(() {
filterApplied = value!;
});
if (filterApplied) {
widget.audioController.applyFilter();
} else {
widget.audioController.removeFilter();
}
},
),
],
),
],
),
),
);
}
}
- بعد تنزيل الملفات الصوتية، أنشِئ دليلاً في جذر مشروعك باسم
assets
. - في الدليل
assets
، أنشئ دليلَين فرعيَين، أحدهما باسمmusic
والآخر باسمsounds
. - انقِل الملفات التي تم تنزيلها إلى مشروعك لكي يكون ملف الأغنية في ملف
assets/music/looped-song.ogg
وتكون أصوات البوق في الملفات التالية:
assets/sounds/pew1.mp3
assets/sounds/pew2.mp3
assets/sounds/pew3.mp3
يجب أن يبدو هيكل مشروعك الآن على النحو التالي:
بعد توفُّر الملفات، عليك إخبار Flutter بها.
- افتح ملف
pubspec.yaml
، ثم استبدِل القسمflutter:
في أسفل الملف بما يلي:
pubspec.yaml
...
flutter:
uses-material-design: true
assets:
- assets/music/
- assets/sounds/
- أضِف تبعية إلى الحزمة
flutter_soloud
وحزمةlogging
.
pubspec.yaml
...
dependencies:
flutter:
sdk: flutter
flutter_soloud: ^2.0.0
logging: ^1.2.0
...
- شغِّل المشروع. لم تعمل أي أداة حتى الآن لأنّك أضفت الوظائف في الأقسام التالية.
/flutter_soloud/src/filters/filters.cpp:21:24: warning: implicit conversion loses integer precision: 'decltype(__x.base() - __y.base())' (aka 'long') to 'int' [-Wshorten-64-to-32];
وتأتي هذه النتائج من مكتبة SoLoud
C++ الأساسية. وليس لها أي تأثير على الوظائف ويمكن تجاهلها بأمان.
3- الإعداد وإيقاف التشغيل
لتشغيل الصوت، يمكنك استخدام المكوّن الإضافي flutter_soloud
. يستند هذا المكوّن الإضافي إلى مشروع SoLoud، وهو محرك صوت C++ للألعاب التي تستخدمها Nintendo SNES Classic، بالإضافة إلى غيرها.
لإعداد محرك الصوت SoLoud، اتّبِع الخطوات التالية:
- في ملف
audio_controller.dart
، استورِد حزمةflutter_soloud
وأضِف حقل_soloud
خاصًا إلى الصف.
lib/audio/audio_controller.dart
import 'dart:ui';
import 'package:flutter_soloud/flutter_soloud.dart'; // ← Add this...
import 'package:logging/logging.dart';
class AudioController {
static final Logger _log = Logger('AudioController');
SoLoud? _soloud; // ← ... and this.
Future<void> initialize() async {
// TODO
}
...
وتدير وحدة التحكّم في الصوت محرّك SoLoud الأساسي من خلال هذا الحقل وستعيد توجيه جميع المكالمات إليه.
- في طريقة
initialize()
، أدخِل الرمز التالي:
lib/audio/audio_controller.dart
...
Future<void> initialize() async {
_soloud = SoLoud.instance;
await _soloud!.init();
}
...
تؤدي هذه الخطوة إلى تعبئة الحقل _soloud
وتنتظر عملية الإعداد. يُرجى ملاحظة ما يلي:
- توفّر SoLoud حقل
instance
مفردًا. ولا يمكن إنشاء مثيل للعديد من مثيلات SoLoud. هذا ليس شيئًا يسمح به محرك C++، لذلك غير مسموح به من خلال المكوّن الإضافي Dart أيضًا. - إعداد المكوّن الإضافي غير متزامن ولن تكتمل حتى يتم إرجاع طريقة
init()
. - للإيجاز في هذا المثال، لا ترصد الأخطاء في جزء
try/catch
. في رمز الإنتاج، عليك إجراء ذلك وإبلاغ المستخدم بأي أخطاء.
- في طريقة
dispose()
، أدخِل الرمز التالي:
lib/audio/audio_controller.dart
...
void dispose() {
_soloud?.deinit();
}
...
ويعدّ إيقاف SoLoud عند الخروج من التطبيق ممارسة جيدة، على الرغم من أنّ كل شيء يسير على ما يرام حتى إذا أهملت ذلك.
- لاحظ أنه تم استدعاء الطريقة
AudioController.initialize()
بالفعل من الدالةmain()
. وهذا يعني أن إعادة التشغيل السريع للمشروع تؤدي إلى تشغيل SoLoud في الخلفية، ولكنها لن تحقّق أي فائدة قبل تشغيل بعض الأصوات فعليًا.
4. تشغيل أصوات بلقطة واحدة
حمِّل مادة عرض وشغِّلها.
الآن بعد أن عرفت أنه تم تهيئة SoLoud عند بدء التشغيل، يمكنك مطالبته بتشغيل الأصوات.
تفرق SoLoud بين مصدر الصوت، أي البيانات والبيانات الوصفية المستخدمة لوصف الصوت، و"المثيلات الصوتية" الخاصة به، وهي الأصوات التي يتم تشغيلها فعليًا. من الأمثلة على مصدر الصوت ملف mp3 يتم تحميله في الذاكرة وجاهز للتشغيل ويتم تمثيله بمثيل من الفئة AudioSource
. ففي كل مرة يتم فيها تشغيل مصدر الصوت هذا، تنشئ SoLoud "مثيلاً صوتيًا" الذي يتم تمثيله بالنوع SoundHandle
.
تحصل على مثيل AudioSource
من خلال تحميله. على سبيل المثال، إذا كان لديك ملف mp3 في مواد العرض، يمكنك تحميله للحصول على AudioSource
. بعد ذلك، يمكنك أن تطلب من SoLoud تشغيل AudioSource
. يمكنك تشغيل اللعبة عدة مرات، حتى في آن واحد.
عند الانتهاء من استخدام مصدر صوت، يمكنك التخلص منه باستخدام طريقة SoLoud.disposeSource()
.
لتحميل مادة عرض وتشغيلها، اتّبِع الخطوات التالية:
- في طريقة
playSound()
للفئةAudioController
، أدخِل الرمز التالي:
lib/audio/audio_controller.dart
...
Future<void> playSound(String assetKey) async {
final source = await _soloud!.loadAsset(assetKey);
await _soloud!.play(source);
}
...
- احفظ الملف وأعِد تحميله سريعًا، ثم اختَر تشغيل الصوت. يُفترض أن تسمع صوت بوق سخيف. يُرجى ملاحظة ما يلي:
- إنّ الوسيطة
assetKey
المقدَّمة تشبهassets/sounds/pew1.mp3
، وهي السلسلة نفسها التي تقدّمها لأي واجهة برمجة تطبيقات Flutter API أخرى تحمِّل مواد عرض، مثل التطبيق المصغَّرImage.asset()
. - يوفّر مثيل SoLoud طريقة
loadAsset()
لتحميل ملف صوتي من مواد عرض مشروع Flutter بشكل غير متزامن مع عرض مثيل من الفئةAudioSource
. هناك طرق مماثلة لتحميل ملف من نظام الملفات (الطريقةloadFile()
)، وتحميله عبر الشبكة من عنوان URL (الطريقةloadUrl()
). - بعد ذلك، يتم تمرير مثيل
AudioSource
الذي تم الحصول عليه حديثًا إلى طريقةplay()
في SoLoud. تعرض هذه الطريقة مثيلاً من النوعSoundHandle
يمثّل الصوت الذي يتم تشغيله حديثًا. بدوره، يمكن تمرير هذا الاسم المعرِّف إلى طرق SoLoud أخرى لتنفيذ إجراءات مثل إيقاف الصوت مؤقتًا أو إيقافه أو تعديل مستوى صوته. - على الرغم من أنّ
play()
هي طريقة غير متزامنة، يبدأ التشغيل بشكل فوري. تستخدم الحزمةflutter_soloud
واجهة الدالة الخارجية (FFI) لـ Dart لاستدعاء الرمز C مباشرةً وبشكل متزامن. فالرسائل المعتادة بين رمز Dart ورمز النظام الأساسي الخاص بمعظم مكوّنات Flutter الإضافية لا تتوفّر في أي مكان. السبب الوحيد الذي يجعل بعض الأساليب غير متزامنة هو أن بعض التعليمات البرمجية للمكون الإضافي تعمل بعزلة خاصة وأن الاتصال بين عناصر عزل Dart غير متزامن. - أنت تؤكد ببساطة أن الحقل
_soloud
ليس فارغًا مع_soloud!
. يعد هذا مرة أخرى للإيجاز. يجب أن يتعامل رمز الإنتاج بسلاسة مع الموقف الذي يحاول فيه المطور تشغيل صوت قبل أن تتاح الفرصة لإعداده بشكل كامل لوحدة التحكم في الصوت.
التعامل مع الاستثناءات
ربما لاحظت أنك، مرة أخرى، تتجاهل الاستثناءات المحتملة. دعونا نصلح ذلك لهذه الطريقة بالذات لأغراض التعلم. (للإيجاز، يعود الدرس التطبيقي حول الترميز إلى تجاهل الاستثناءات بعد هذا القسم.)
- للتعامل مع الاستثناءات في هذه الحالة، عليك إحاطة سطرَي الإجراء
playSound()
بجزء منtry/catch
والتقاط أمثلة منSoLoudException
فقط.
lib/audio/audio_controller.dart
...
Future<void> playSound(String assetKey) async {
try {
final source = await _soloud!.loadAsset(assetKey);
await _soloud!.play(source);
} on SoLoudException catch (e) {
_log.severe("Cannot play sound '$assetKey'. Ignoring.", e);
}
}
...
تنطبق استثناءات مختلفة من SoLoud، مثل استثناءات SoLoudNotInitializedException
أو SoLoudTemporaryFolderFailedException
. تسرد مستندات واجهة برمجة التطبيقات الخاصة بكل طريقة أنواع الاستثناءات التي قد تحدث.
يوفّر SoLoud أيضًا الفئة الرئيسية لجميع استثناءاتها، باستثناء SoLoudException
، كي تتمكّن من رصد جميع الأخطاء المتعلقة بوظائف المحرّك الصوتي. ويُعدّ هذا الإجراء مفيدًا بشكل خاص في الحالات التي لا يكون فيها تشغيل الصوت بالغ الأهمية. على سبيل المثال، إذا كنت لا تريد إيقاف جلسة اللعبة لدى اللاعب فقط بسبب تعذّر تحميل أحد أصوات بوق.
كما هو متوقّع، يمكن أن تؤدي الطريقة loadAsset()
أيضًا إلى ظهور الخطأ FlutterError
إذا قدّمت مفتاح مادة عرض غير متوفّر. يجب معالجة تحميل مواد العرض غير المضمّنة في اللعبة بشكل عام، وبالتالي يكون الخطأ.
تشغيل أصوات مختلفة
ربما لاحظت أنّك شغِّلت ملف pew1.mp3
فقط، ولكن هناك نسختان أخريان من الصوت في دليل مواد العرض. وغالبًا ما يبدو الأمر طبيعيًا عندما تتضمّن الألعاب نسخًا متعددة من الصوت نفسه، ويتم تشغيل الإصدارات المختلفة بطريقة عشوائية أو بالتناوب. وهذا يمنع، على سبيل المثال، من ظهور آثار أقدام الأشخاص وطلقات طلقات نارية بشكل متماثل للغاية، وبالتالي يكون زائفًا.
- وكتمرين اختياري، عدِّل الرمز لتشغيل صوت بوق مختلف في كل مرة يتم فيها النقر على الزر.
5- تشغيل تكرارات الموسيقى
إدارة الأصوات التي تستغرق وقتًا أطول
يتم تشغيل بعض المقاطع الصوتية لفترات زمنية طويلة. الموسيقى هي مثال واضح على ذلك، إلا أنّ العديد من الألعاب يمكن أن تؤدّي دورًا إيجابيًا أيضًا، مثل عوي الهواء عبر الممرات أو تراتيل الرهبان البعيدين أو صرير معدن يعود تاريخه إلى قرون مضت أو أصوات السعال البعيدة التي تصدر عن المرضى.
هذه مصادر الصوت التي تتضمّن أوقات تشغيل يمكن قياسها بالدقائق. عليك تتبُّعها حتى تتمكّن من إيقافها مؤقتًا أو إيقافها عند الحاجة. وغالبًا ما يتم الاحتفاظ بنسخة احتياطية منها بملفات كبيرة الحجم ويمكن أن تستهلك الكثير من الذاكرة، وبالتالي يمكنك تتبُّعها كي تتمكّن من التخلص من مثيل "AudioSource
" عند عدم الحاجة إليه.
لهذا السبب، ستقدم حقلاً خاصًا جديدًا إلى AudioController
. وهو اسم معرِّف للأغنية المشغّلة حاليًا، إن توفّر. أضِف السطر التالي:
lib/audio/audio_controller.dart
...
class AudioController {
static final Logger _log = Logger('AudioController');
SoLoud? _soloud;
SoundHandle? _musicHandle; // ← Add this.
...
بدء الموسيقى
في الواقع، لا يختلف تشغيل الموسيقى عن تشغيل الصوت بنقرة واحدة. عليك أولاً تحميل ملف assets/music/looped-song.ogg
كمثيل للفئة AudioSource
، ثم استخدام طريقة SoLoud لـ play()
لتشغيله.
أما هذه المرة، فستمسك بمقبض الصوت الذي تعرضه طريقة play()
لمعالجة الصوت أثناء تشغيله.
- إذا أردت، يمكنك تنفيذ طريقة
AudioController.startMusic()
بنفسك. لا بأس إذا لم تكن بعض التفاصيل صحيحة. من المهم أن تبدأ الموسيقى عند النقر على بدء الموسيقى.
إليك طريقة التنفيذ المرجعية:
lib/audio/audio_controller.dart
...
Future<void> startMusic() async {
if (_musicHandle != null) {
if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
_log.info('Music is already playing. Stopping first.');
await _soloud!.stop(_musicHandle!);
}
}
final musicSource = await _soloud!
.loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
_musicHandle = await _soloud!.play(musicSource);
}
...
لاحِظ أنّك حمّلت ملف الموسيقى في وضع القرص (التعداد LoadMode.disk
). هذا يعني ببساطة أنه يتم تحميل الملف فقط في أجزاء حسب الحاجة. بالنسبة إلى المقاطع الصوتية التي تعمل لمدة أطول، يُفضّل عمومًا تحميلها في وضع القرص. بالنسبة إلى المؤثرات الصوتية القصيرة، من الأسهل تحميلها وفك ضغطها في الذاكرة (التعداد التلقائي لـ LoadMode.memory
).
لكن لديك مشكلتان. أولاً، تُصدر الموسيقى صوتًا عاليًا جدًا، وتتجاوز هذه الأصوات الأصوات. في معظم الألعاب، يتم تشغيل الموسيقى في الخلفية في معظم الأوقات، ما يمنح الأولوية للمحتوى الصوتي الأكثر ثراءً بالمعلومات، مثل الكلام والمؤثرات الصوتية. ويمكن إصلاح هذا بسهولة باستخدام معلَمة مستوى الصوت في طريقة التشغيل. يمكنك مثلاً تجربة _soloud!.play(musicSource, volume: 0.6)
لتشغيل الأغنية بمستوى صوت 60%. وبدلاً من ذلك، يمكنك ضبط مستوى الصوت في أي وقت لاحق باستخدام خيار مثل _soloud!.setVolume(_musicHandle, 0.6)
.
المشكلة الثانية هي أن الأغنية تتوقف فجأة. ويرجع ذلك إلى أنّها أغنية من المفترض أن يتم تشغيلها بشكل متكرّر وليست نقطة البداية في الملف الصوتي.
وهذا الخيار رائج لتشغيل موسيقى الألعاب، لأنّه يعني أنّ الأغنية تبدأ بمقدمة طبيعية ثم يتم تشغيلها طالما لزم الأمر بدون نقطة تكرار واضحة. وعندما تحتاج اللعبة إلى الانتقال من الأغنية قيد التشغيل حاليًا، سيؤدي ذلك ببساطة إلى تضاؤل الأغنية.
لحسن الحظ، يوفّر تطبيق SoLoud طرقًا لتشغيل الصوت المتكرر. تستخدم الطريقة play()
قيمة منطقية للمَعلمة looping
، وتأخذ أيضًا قيمة نقطة بداية التكرار الحلقي كمَعلمة loopingStartAt
. تظهر التعليمة البرمجية الناتجة على النحو التالي:
lib/audio/audio_controller.dart
...
_musicHandle = await _soloud!.play(
musicSource,
volume: 0.6,
looping: true,
// ↓ The exact timestamp of the start of the loop.
loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);
...
إذا لم يتم ضبط المَعلمة loopingStartAt
، يتم ضبطها تلقائيًا على Duration.zero
(بمعنى آخر، بداية الملف الصوتي). إذا كان لديك مقطع موسيقي يمثل حلقة مثالية بدون أي مقدمة، فهذا ما تريده.
- للتأكّد من التخلص من مصدر الصوت بشكل صحيح عند انتهاء تشغيله، استمِع إلى بث
allInstancesFinished
الذي يوفّره كل مصدر صوت. عند إضافة طلبات السجلّ، تظهر طريقةstartMusic()
على النحو التالي:
lib/audio/audio_controller.dart
...
Future<void> startMusic() async {
if (_musicHandle != null) {
if (_soloud!.getIsValidVoiceHandle(_musicHandle!)) {
_log.info('Music is already playing. Stopping first.');
await _soloud!.stop(_musicHandle!);
}
}
_log.info('Loading music');
final musicSource = await _soloud!
.loadAsset('assets/music/looped-song.ogg', mode: LoadMode.disk);
musicSource.allInstancesFinished.first.then((_) {
_soloud!.disposeSource(musicSource);
_log.info('Music source disposed');
_musicHandle = null;
});
_log.info('Playing music');
_musicHandle = await _soloud!.play(
musicSource,
volume: 0.6,
looping: true,
loopingStartAt: const Duration(seconds: 25, milliseconds: 43),
);
}
...
صوت التلاشي
المشكلة التالية هي أنّ الموسيقى لا تنتهي أبدًا. لنقم بتنفيذ دالة التلاشي.
إحدى الطرق التي يمكن من خلالها تنفيذ ميزة التلاشي هي استخدام دالة ما يتم استدعاؤها عدة مرات في الثانية، مثل Ticker
أو Timer.periodic
، وخفض مستوى صوت الموسيقى بفواصل زمنية صغيرة. سيعمل ذلك، ولكنه يتطلب الكثير من العمل.
مع أطيب التحيات، وفي ما يلي الخطوات التي يجب اتّباعها لإخفاء الموسيقى خلال خمس ثوانٍ ثم إيقاف مثيل الصوت كي لا تستهلك موارد وحدة المعالجة المركزية (CPU) بدون داعٍ. استبدل طريقة fadeOutMusic()
بهذا الرمز:
lib/audio/audio_controller.dart
...
void fadeOutMusic() {
if (_musicHandle == null) {
_log.info('Nothing to fade out');
return;
}
const length = Duration(seconds: 5);
_soloud!.fadeVolume(_musicHandle!, 0, length);
_soloud!.scheduleStop(_musicHandle!, length);
}
...
6- تطبيق تأثيرات
من بين الميزات الرائعة لاستخدام محرك صوت مناسب تحت تصرفك هو أنه يمكنك معالجة الصوت، مثل توجيه بعض الأصوات من خلال صدى صوت أو معادِل صوت أو فلتر تمرير منخفض.
وفي الألعاب، يمكن استخدام ذلك للتمييز السمعي للمواقع. على سبيل المثال، صوت التصفيق في الغابة يختلف عن الصوت في قبو خرساني. على الرغم من أنّ الغابة تساعد في تشتت الصوت واستيعابه، تعكس الجدران العارية للمخبأ الموجات الصوتية، ما يؤدي إلى صدى الصوت. وبالمثل، تبدو أصوات الأشخاص مختلفة عندما تسمعها من خلال الحائط. كلما كانت الترددات الأعلى لتلك الأصوات تُخفف بسهولة أكبر أثناء انتقالها عبر الوسط الصلب، ما ينتج عنه تأثير فلتر تمرير منخفض.
يوفّر تطبيق SoLoud العديد من التأثيرات الصوتية المختلفة التي يمكنك تطبيقها على الصوت.
- لإظهار أنّ اللاعب في غرفة كبيرة، مثل كاتدرائية أو كهف، استخدِم تعداد
FilterType.freeverbFilter
:
lib/audio/audio_controller.dart
...
void applyFilter() {
_soloud!.addGlobalFilter(FilterType.freeverbFilter);
_soloud!.setFilterParameter(FilterType.freeverbFilter, 0, 0.2);
_soloud!.setFilterParameter(FilterType.freeverbFilter, 2, 0.9);
}
void removeFilter() {
_soloud!.removeGlobalFilter(FilterType.freeverbFilter);
}
...
فكما ترى، بفضل الفلاتر، يمكنك الوصول إلى مناطق ذات مستوى أقل. يتم الانتهاء من ضبط معلَمة عامل تصفية باستخدام فهرس هذه المعلمة. على سبيل المثال، معلَمة Wet في الفعل الحر تضم الفهرس 0
، بينما تتضمن معلَمة حجم الغرفة الفهرس 2
.
باستخدام الرمز السابق، يمكنك إجراء ما يلي:
- تفعيل فلتر الفعل الحر بشكل عام أو على مزيج الصوت بأكمله، وليس صوتًا واحدًا فقط.
- يجب ضبط المعلَمة Wet على
0.2
، ما يعني أنّ الصوت الناتج سيكون أصليًا بنسبة 80% ومخرجات تأثير الارتداد بنسبة 20%. إذا ضبطت هذه المَعلمة على1.0
، سيكون الأمر بمثابة سماع الموجات الصوتية فقط التي ترجع إليك من الجدران البعيدة للغرفة وليس الصوت الأصلي. - اضبط مَعلمة حجم الغرفة على
0.9
. ويمكنك تعديل هذه المَعلمة كما تشاء أو حتى تغييرها ديناميكيًا. ويشكّل1.0
كهفًا ضخمًا، بينما يضمّ0.0
حمّامًا.
- إذا أردت ذلك، فغيّر الرمز وطبّق أحد الفلاتر التالية أو مجموعة من الفلاتر التالية:
FilterType.biquadResonantFilter
(يمكن استخدامه كفلتر للتذاكر المنخفضة)FilterType.eqFilter
FilterType.echoFilter
FilterType.lofiFilter
FilterType.flangerFilter
FilterType.bassboostFilter
FilterType.waveShaperFilter
FilterType.robotizeFilter
FilterType.freeverbFilter
7. تهانينا
لقد نفّذت وحدة تحكّم في الصوت لتشغيل الأصوات وتكرار الموسيقى وتطبيق التأثيرات.
مزيد من المعلومات
- جرِّب استخدام وحدة تحكّم الصوت بشكل أكبر باستخدام ميزات، مثل التحميل المُسبق للأصوات عند بدء التشغيل أو تشغيل الأغاني على التوالي أو تطبيق فلتر تدريجيًا بمرور الوقت.
- اقرأ مستندات الحزمة في
flutter_soloud
. - يمكنك قراءة الصفحة الرئيسية لمكتبة C++ الأساسية.
- يمكنك الاطّلاع على مزيد من المعلومات حول Dart FFI، وهي التكنولوجيا المستخدَمة للتفاعل مع مكتبة C++.
- للحصول على أفكار ملهمة، يمكنك مشاهدة حديث غي سومبرغ عن البرمجة الصوتية للألعاب. (هناك أيضًا علامة أطول). عندما يتحدّث "غاي" عن "البرمجيات الوسيطة"، يعني ذلك مكتبات مثل SoLoud وFMOD. يميل باقي التعليمات البرمجية إلى أن يكون خاصًا بكل لعبة.
- صمِّم لعبتك وأفلِتها.