Tampilan
Bab 45 — Audio dalam Game
Audio adalah bagian yang paling sering ditunda dan paling terasa dampaknya. Game tanpa suara terasa mati; game dengan suara yang tepat terasa hidup bahkan ketika grafisnya sederhana.
Bab ini relatif singkat, tetapi berisi beberapa jebakan yang bisa membuat gamemu tersendat kalau diabaikan.
Memasang
yaml
# pubspec.yaml
dependencies:
flame: ^1.22.0
flame_audio: ^2.10.6
flutter:
assets:
- assets/audio/Flame mencari berkas audio di assets/audio/ secara bawaan, seperti konvensi gambar di Bab 42.
assets/
└── audio/
├── tembak.wav
├── ledakan.wav
├── ambil_koin.wav
├── musik_menu.mp3
└── musik_main.mp3Dua jenis audio
Perbedaan ini menentukan bagaimana kamu memakainya.
┌────────────────────────────────────────────────────────┐
│ EFEK SUARA (SFX) │
│ Pendek, sering, bisa tumpang tindih │
│ Contoh: tembakan, ledakan, langkah kaki │
│ Format: WAV (dekode cepat, tanpa jeda) │
│ Durasi: di bawah 2 detik │
└────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────┐
│ MUSIK LATAR (BGM) │
│ Panjang, berulang, satu pada satu waktu │
│ Contoh: musik menu, musik level │
│ Format: MP3 atau OGG (ukuran kecil) │
│ Durasi: menit │
└────────────────────────────────────────────────────────┘Format berkas penting untuk efek suara
Gunakan WAV untuk efek suara pendek. MP3 harus didekode sebelum diputar, dan jeda beberapa milidetik itu terasa sebagai keterlambatan antara menekan tombol dan mendengar suaranya.
Untuk musik latar, MP3 atau OGG lebih baik karena ukurannya jauh lebih kecil — dan jeda awal tidak masalah untuk musik.
Memuat di awal
Ini langkah yang paling sering dilewati, dan penyebab utama game tersendat saat efek suara pertama kali diputar.
dart
// lib/game_saya.dart
import 'package:flame_audio/flame_audio.dart';
import 'package:flame/game.dart';
class GameSaya extends FlameGame {
@override
Future<void> onLoad() async {
await super.onLoad();
// Muat semua efek suara ke cache SEBELUM game dimulai
await FlameAudio.audioCache.loadAll([
'tembak.wav',
'ledakan.wav',
'ambil_koin.wav',
'terkena.wav',
]);
}
}Tanpa ini, berkas dibaca dari penyimpanan saat pertama kali diputar — dan pembacaan itu terjadi di frame yang sedang berjalan, menyebabkan lonjakan waktu frame yang terlihat sebagai tersendat.
Memutar efek suara
dart
import 'package:flame_audio/flame_audio.dart';
// Sekali putar
FlameAudio.play('tembak.wav');
// Dengan volume (0.0 sampai 1.0)
FlameAudio.play('ledakan.wav', volume: 0.7);Memakainya di komponen:
dart
void tembak() {
if (_sisaJeda > 0) return;
_sisaJeda = _jedaTembak;
FlameAudio.play('tembak.wav', volume: 0.5);
findGame()!.add(Peluru(/* ... */));
}Volume efek suara sebaiknya di bawah 1.0
Beberapa efek yang berbunyi bersamaan pada volume penuh akan terdengar menyakitkan dan bisa terdistorsi. Nilai 0,4–0,7 biasanya terasa lebih nyaman, terutama untuk suara yang sering muncul seperti tembakan.
Musik latar
dart
// Memutar berulang
FlameAudio.bgm.play('musik_main.mp3', volume: 0.4);
// Menghentikan
FlameAudio.bgm.stop();
// Jeda dan lanjutkan
FlameAudio.bgm.pause();
FlameAudio.bgm.resume();FlameAudio.bgm mengelola satu trek pada satu waktu — memutar trek baru otomatis menghentikan yang lama.
Menghubungkannya ke keadaan game:
dart
class GameSaya extends FlameGame {
Future<void> mulaiPermainan() async {
await FlameAudio.bgm.play('musik_main.mp3', volume: 0.35);
overlays.remove('menu');
overlays.add('hud');
resumeEngine();
}
Future<void> kalah() async {
await FlameAudio.bgm.stop();
FlameAudio.play('kalah.wav');
pauseEngine();
overlays.remove('hud');
overlays.add('kalah');
}
Future<void> kembaliKeMenu() async {
await FlameAudio.bgm.play('musik_menu.mp3', volume: 0.3);
overlays.add('menu');
}
}Menghentikan audio saat aplikasi di latar belakang
Ini masalah nyata yang sering luput: musik terus berbunyi setelah pengguna berpindah ke aplikasi lain.
dart
import 'package:flame/game.dart';
import 'package:flame_audio/flame_audio.dart';
import 'package:flutter/widgets.dart';
class GameSaya extends FlameGame {
@override
void lifecycleStateChange(AppLifecycleState state) {
super.lifecycleStateChange(state);
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
case AppLifecycleState.hidden:
FlameAudio.bgm.pause();
pauseEngine();
case AppLifecycleState.resumed:
FlameAudio.bgm.resume();
resumeEngine();
case AppLifecycleState.detached:
FlameAudio.bgm.stop();
}
}
}Ini melengkapi penanganan siklus hidup yang dibahas di Bab 42 — game dan audionya dijeda bersamaan.
Pengaturan suara
Pemain harus bisa mematikan suara. Ini bukan fitur opsional — banyak orang bermain di tempat umum.
dart
// lib/data/pengaturan_audio.dart
import 'package:flame_audio/flame_audio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PengaturanAudio {
PengaturanAudio._();
static final instance = PengaturanAudio._();
static const _kunciSfx = 'audio_sfx_aktif';
static const _kunciBgm = 'audio_bgm_aktif';
static const _kunciVolumeSfx = 'audio_volume_sfx';
SharedPreferences? _prefs;
bool _sfxAktif = true;
bool _bgmAktif = true;
double _volumeSfx = 0.6;
bool get sfxAktif => _sfxAktif;
bool get bgmAktif => _bgmAktif;
double get volumeSfx => _volumeSfx;
Future<void> muat() async {
_prefs = await SharedPreferences.getInstance();
_sfxAktif = _prefs?.getBool(_kunciSfx) ?? true;
_bgmAktif = _prefs?.getBool(_kunciBgm) ?? true;
_volumeSfx = _prefs?.getDouble(_kunciVolumeSfx) ?? 0.6;
}
Future<void> setSfx(bool aktif) async {
_sfxAktif = aktif;
await _prefs?.setBool(_kunciSfx, aktif);
}
Future<void> setBgm(bool aktif) async {
_bgmAktif = aktif;
await _prefs?.setBool(_kunciBgm, aktif);
if (aktif) {
await FlameAudio.bgm.resume();
} else {
await FlameAudio.bgm.pause();
}
}
Future<void> setVolumeSfx(double nilai) async {
_volumeSfx = nilai.clamp(0.0, 1.0);
await _prefs?.setDouble(_kunciVolumeSfx, _volumeSfx);
}
/// Satu-satunya tempat efek suara diputar —
/// sehingga pengaturan selalu dihormati.
void putarSfx(String berkas, {double pengali = 1.0}) {
if (!_sfxAktif) return;
FlameAudio.play(berkas, volume: _volumeSfx * pengali);
}
}Sekarang seluruh kode game memanggil ini alih-alih FlameAudio.play langsung:
dart
PengaturanAudio.instance.putarSfx('tembak.wav', pengali: 0.8);Ini pola yang layak dibiasakan: satu titik masuk untuk audio berarti kamu tidak akan lupa memeriksa pengaturan di salah satu dari dua puluh tempat pemanggilan.
Menginisialisasinya:
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await PengaturanAudio.instance.muat();
runApp(GameWidget(game: GameSaya()));
}Layar pengaturannya memakai widget Flutter biasa lewat overlay:
dart
// lib/overlay/pengaturan.dart
import 'package:flutter/material.dart';
import '../data/pengaturan_audio.dart';
class OverlayPengaturan extends StatefulWidget {
const OverlayPengaturan({super.key, required this.saatTutup});
final VoidCallback saatTutup;
@override
State<OverlayPengaturan> createState() => _OverlayPengaturanState();
}
class _OverlayPengaturanState extends State<OverlayPengaturan> {
final _audio = PengaturanAudio.instance;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.black.withValues(alpha: 0.8),
child: Center(
child: Card(
margin: const EdgeInsets.all(32),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Pengaturan',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 16),
SwitchListTile(
value: _audio.bgmAktif,
onChanged: (nilai) async {
await _audio.setBgm(nilai);
setState(() {});
},
title: const Text('Musik'),
secondary: const Icon(Icons.music_note),
),
SwitchListTile(
value: _audio.sfxAktif,
onChanged: (nilai) async {
await _audio.setSfx(nilai);
setState(() {});
},
title: const Text('Efek Suara'),
secondary: const Icon(Icons.volume_up),
),
if (_audio.sfxAktif)
Slider(
value: _audio.volumeSfx,
onChanged: (nilai) =>
setState(() => _audio.setVolumeSfx(nilai)),
// Simpan hanya setelah selesai menggeser —
// pola dari Bab 32
onChangeEnd: (nilai) => _audio.setVolumeSfx(nilai),
),
const SizedBox(height: 16),
FilledButton(
onPressed: widget.saatTutup,
child: const Text('Tutup'),
),
],
),
),
),
),
);
}
}Membatasi jumlah suara bersamaan
Kalau dua puluh musuh meledak bersamaan, dua puluh efek suara akan diputar sekaligus — terdengar seperti derau dan membebani perangkat.
dart
class PembatasSuara {
PembatasSuara({this.jedaMinimal = 0.06, this.maksBersamaan = 4});
/// Jarak minimal antara dua pemutaran berkas yang SAMA.
final double jedaMinimal;
/// Berapa banyak yang boleh berbunyi dalam jendela waktu itu.
final int maksBersamaan;
final Map<String, List<DateTime>> _riwayat = {};
bool boleh(String berkas) {
final sekarang = DateTime.now();
final daftar = _riwayat.putIfAbsent(berkas, () => []);
// Buang catatan yang sudah lewat
daftar.removeWhere(
(waktu) =>
sekarang.difference(waktu).inMilliseconds >
(jedaMinimal * 1000).round(),
);
if (daftar.length >= maksBersamaan) return false;
daftar.add(sekarang);
return true;
}
}Menggabungkannya ke pengaturan audio:
dart
class PengaturanAudio {
final _pembatas = PembatasSuara();
void putarSfx(String berkas, {double pengali = 1.0}) {
if (!_sfxAktif) return;
if (!_pembatas.boleh(berkas)) return;
FlameAudio.play(berkas, volume: _volumeSfx * pengali);
}
}Sekarang ledakan beruntun terdengar sebagai satu ledakan besar, bukan dua puluh yang saling menimpa.
Variasi suara
Efek suara yang persis sama diulang-ulang terasa monoton dan artifisial. Trik sederhana: sediakan beberapa variasi dan pilih acak.
dart
import 'dart:math';
class PengaturanAudio {
final _acak = Random();
void putarSfxAcak(List<String> pilihan, {double pengali = 1.0}) {
if (pilihan.isEmpty) return;
putarSfx(pilihan[_acak.nextInt(pilihan.length)], pengali: pengali);
}
}dart
PengaturanAudio.instance.putarSfxAcak([
'ledakan_1.wav',
'ledakan_2.wav',
'ledakan_3.wav',
]);Tiga variasi sudah cukup untuk menghilangkan kesan monoton pada sebagian besar suara.
Audio berdasarkan jarak
Untuk game dengan area lebih besar dari layar, suara yang jauh sebaiknya lebih pelan.
dart
void putarDiPosisi(
String berkas,
Vector2 posisiSumber,
Vector2 posisiPendengar, {
double jangkauan = 600,
}) {
final jarak = posisiSumber.distanceTo(posisiPendengar);
// Terlalu jauh — tidak terdengar sama sekali
if (jarak > jangkauan) return;
// Volume menurun secara linier terhadap jarak
final volume = (1 - jarak / jangkauan).clamp(0.0, 1.0);
PengaturanAudio.instance.putarSfx(berkas, pengali: volume);
}Memakainya:
dart
class Musuh extends PositionComponent with HasGameReference<GameSaya> {
void meledak() {
putarDiPosisi('ledakan.wav', position, game.pemain.position);
removeFromParent();
}
}Ini detail kecil yang membuat dunia game terasa punya ruang.
Transisi musik
Berganti musik secara mendadak terasa kasar. Memudarkannya jauh lebih halus.
dart
Future<void> gantiMusik(String berkasBaru, {double volume = 0.35}) async {
// Redupkan yang sedang berjalan
for (var v = volume; v >= 0; v -= 0.05) {
FlameAudio.bgm.audioPlayer.setVolume(v.clamp(0.0, 1.0));
await Future.delayed(const Duration(milliseconds: 40));
}
await FlameAudio.bgm.stop();
await FlameAudio.bgm.play(berkasBaru, volume: 0);
// Naikkan yang baru
for (var v = 0.0; v <= volume; v += 0.05) {
FlameAudio.bgm.audioPlayer.setVolume(v);
await Future.delayed(const Duration(milliseconds: 40));
}
}Jangan berlebihan
Transisi 800 milidetik terasa mulus. Transisi tiga detik terasa lambat dan membuat pemain menunggu. Prinsip yang sama dengan durasi animasi di Bab 11.
Mencari aset audio
Sumber gratis yang legal untuk dipakai:
| Sumber | Catatan |
|---|---|
| freesound.org | Besar; periksa lisensi tiap berkas |
| opengameart.org | Khusus aset game |
| kenney.nl | Paket berkualitas, domain publik |
| incompetech.com | Musik, butuh atribusi |
| sfxr / jsfxr | Buat efek suara gaya retro sendiri |
Periksa lisensi setiap berkas
Freesound khususnya punya berkas dengan lisensi berbeda-beda. Sebagian mengizinkan pemakaian komersial tanpa syarat, sebagian mewajibkan atribusi, dan sebagian melarang penggunaan komersial sama sekali.
Memakai berkas berlisensi non-komersial di game berbayar adalah pelanggaran hak cipta — persoalan yang dibahas di Bab 41.
Simpan catatan sumber dan lisensi setiap aset yang kamu pakai. Membuatnya di awal jauh lebih mudah daripada menelusuri ulang setahun kemudian.
Daftar periksa audio
- Berkas efek suara berformat WAV, di bawah 2 detik
- Musik latar berformat MP3/OGG
- Semua SFX dimuat lewat loadAll() di onLoad()
- Volume SFX di bawah 1.0 (0,4–0,7 nyaman)
- Musik dijeda saat aplikasi masuk latar belakang
- Ada pengaturan untuk mematikan musik dan efek terpisah
- Pengaturan tersimpan dan dipulihkan
- Semua pemutaran lewat SATU fungsi yang memeriksa pengaturan
- Suara beruntun dibatasi agar tidak menumpuk
- Suara yang sering muncul punya beberapa variasi
- Lisensi setiap aset dicatat
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Lapisan Audio Lengkap — ⭐ · 25–40 menit
Tantangan: Ambil game atau prototipe Flame mana pun yang sudah kamu punya dan beri ia lapisan audio yang lengkap: musik latar, tiga efek suara, layar pengaturan yang menyimpan pilihan, dan perilaku yang benar saat aplikasi berpindah ke latar belakang.
Bayangan tampilan:
text
╭───────────────────────────────────╮
│ 9:41 ▂▄▆ 🔋 │
│ Musik [●──○] │
│ Efek Suara [●──○] │
│ ▬▬▬▬ │
╰───────────────────────────────────╯Kriteria selesai:
- Seluruh efek suara dimuat lewat
loadAll()dionLoad(), dan kamu sudah membandingkan bagaimana rasanya tanpa itu pada pemutaran pertama. - Semua pemutaran melewati satu fungsi yang memeriksa pengaturan — bukan pemeriksaan yang diulang di setiap titik pemanggilan.
- Musik dan efek bisa dimatikan terpisah, dan pilihannya bertahan setelah aplikasi ditutup.
- Musik berhenti saat aplikasi masuk latar belakang dan lanjut saat kembali.
- Ada pembatas untuk suara yang bisa terpicu beruntun, sehingga sepuluh kejadian bersamaan tidak terdengar sebagai derau.
Petunjuk: Kriteria kedua adalah yang paling menentukan, dan alasannya bukan soal kerapian melainkan soal keandalan: efek suara dipanggil dari belasan tempat, dan kamu akan melewatkan satu. Pengguna yang mematikan suara lalu tetap mendengar satu efek yang lolos akan menganggap pengaturanmu rusak. Satu fungsi mainkanSfx() yang memeriksa pengaturan di dalamnya menutup seluruh kelas masalah itu sekaligus. Soal format: WAV untuk efek karena dekodenya cepat dan tidak menghasilkan jeda, MP3 atau OGG untuk musik karena ukurannya jauh lebih kecil dan jeda seperempat detik di awal lagu tidak terasa.
Variasi 2: Suara yang Punya Jarak — ⭐⭐ · 30–45 menit
Tantangan: Buat suara yang volumenya bergantung pada jarak dari pemain: mesin yang berdengung makin keras saat didekati, dan ledakan jauh yang terdengar lirih. Tambahkan tiga variasi acak untuk satu efek yang sering muncul.
Kriteria selesai:
- Volume dihitung dari jarak dengan batas atas dan bawah yang jelas.
- Suara di luar jangkauan tertentu tidak diputar sama sekali, bukan diputar dengan volume nol.
- Efek yang sering muncul memakai tiga berkas variasi yang dipilih acak.
- Nada suara sedikit divariasikan kalau pustakanya mendukung, untuk mengurangi kesan berulang.
Petunjuk: Kriteria kedua penting untuk performa: memutar dua puluh suara dengan volume mendekati nol tetap memakan sumber daya audio yang jumlahnya terbatas, dan suara penting bisa kalah bersaing. Batasi di sumbernya. Untuk variasi, tiga berkas sudah cukup mengecoh telinga — otak manusia sangat cepat mengenali pola pada suara yang identik berulang, dan itulah yang membuat game terasa murah.
Variasi 3: Musik yang Merespons — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun musik latar yang berganti sesuai keadaan permainan: tenang saat menjelajah, tegang saat musuh mendekat, dan cepat saat pertempuran. Peralihan antar lagu harus meredup silang, bukan berhenti mendadak.
Kriteria selesai:
- Peralihan meredup silang dengan durasi yang terasa alami.
- Keadaan yang berubah bolak-balik dengan cepat tidak menghasilkan musik yang berkedip — ada jeda minimal sebelum berganti lagi.
- Musik tetap berhenti dan lanjut dengan benar saat aplikasi ke latar belakang, apa pun keadaan permainannya.
- Mematikan musik dari pengaturan menghentikan seluruh lapisan sekaligus.
Petunjuk: Kriteria kedua adalah masalah yang muncul segera setelah fitur ini bekerja: pemain yang berdiri tepat di batas jangkauan musuh akan memicu pergantian musik berkali-kali per detik. Jeda minimal sebelum pergantian berikutnya, atau ambang berbeda untuk masuk dan keluar keadaan, menyelesaikannya — pola yang sama persis dengan histeresis pada termostat.
Ikhtisar
- Flame mencari berkas audio di
assets/audio/secara bawaan. - WAV untuk efek suara (dekode cepat, tanpa jeda), MP3/OGG untuk musik (ukuran kecil).
- Muat semua SFX lewat
loadAll()dionLoad()— tanpa ini, pemutaran pertama menyebabkan tersendat. FlameAudio.play()untuk efek,FlameAudio.bgmuntuk musik yang berulang.- Volume efek suara di bawah 1.0 — beberapa suara bersamaan pada volume penuh terdengar menyakitkan.
- Jeda musik saat aplikasi masuk latar belakang lewat
lifecycleStateChange. - Sediakan pengaturan terpisah untuk musik dan efek suara, tersimpan di
SharedPreferences. - Rutekan semua pemutaran lewat satu fungsi yang memeriksa pengaturan — jauh lebih andal daripada memeriksa di dua puluh tempat.
- Batasi suara beruntun, atau dua puluh ledakan bersamaan terdengar sebagai derau.
- Tiga variasi acak cukup menghilangkan kesan monoton pada suara yang sering muncul.
- Volume berdasarkan jarak membuat dunia game terasa punya ruang.
- Transisi musik sekitar 800 ms terasa mulus; lebih lama terasa lambat.
- Periksa dan catat lisensi setiap aset audio.
Berikutnya: Bab 46 — Collision Detection.
Transkrip asli
Disintesis dari 5_flutter_flame/7_case-study-3-sound-in-games.md (4 video: Using Sound in Flame Engine, Coding Sound Fxs and Music, dan latihan-latihannya). Lihat PDF Flame Engine.