Tampilan
Bab 48 — Parallax
Parallax adalah trik visual tertua dalam pengembangan game 2D, dan salah satu yang paling efektif: latar belakang yang bergerak dengan kecepatan berbeda menciptakan ilusi kedalaman pada layar yang sepenuhnya datar.
Efeknya sederhana untuk dipahami dan sangat murah untuk diterapkan.
Prinsipnya
Perhatikan ketika kamu naik kendaraan. Pagar di pinggir jalan melintas cepat; pepohonan di kejauhan bergerak lebih lambat; gunung di cakrawala nyaris tidak bergerak sama sekali.
Otak menafsirkan perbedaan kecepatan itu sebagai jarak.
Kamera bergerak ke kanan →
Lapisan 1: Awan (jauh) ▁▁▁▁▁▁▁▁▁ bergerak 0,1×
Lapisan 2: Gunung ▲▲▲▲▲▲▲▲▲ bergerak 0,3×
Lapisan 3: Pohon ♣♣♣♣♣♣♣♣♣ bergerak 0,6×
Lapisan 4: Tanah (dekat) ▓▓▓▓▓▓▓▓▓ bergerak 1,0×
Semakin dekat → semakin cepat → terasa semakin di depanAturannya: objek yang lebih dekat bergerak lebih cepat.
ParallaxComponent
Flame menyediakannya siap pakai.
dart
// lib/komponen/latar_parallax.dart
import 'package:flame/components.dart';
import 'package:flame/parallax.dart';
class LatarParallax extends ParallaxComponent {
@override
Future<void> onLoad() async {
await super.onLoad();
parallax = await game.loadParallax(
[
ParallaxImageData('langit.png'),
ParallaxImageData('awan.png'),
ParallaxImageData('gunung.png'),
ParallaxImageData('pohon.png'),
ParallaxImageData('tanah.png'),
],
baseVelocity: Vector2(30, 0), // kecepatan lapisan pertama
velocityMultiplierDelta: Vector2(1.6, 1), // pengali tiap lapisan
);
}
}Menambahkannya:
dart
class GameSaya extends FlameGame {
@override
Future<void> onLoad() async {
await super.onLoad();
// priority rendah agar digambar paling belakang
add(LatarParallax()..priority = -10);
}
}Memahami velocityMultiplierDelta
Ini parameter yang menentukan seluruh efeknya, dan cara kerjanya tidak langsung jelas.
Kecepatan setiap lapisan dihitung sebagai:
kecepatan lapisan ke-n = baseVelocity × (multiplierDelta ^ n)Dengan baseVelocity = 30 dan multiplierDelta = 1.6:
Lapisan 0 (langit): 30 × 1.6⁰ = 30 × 1.00 = 30 px/detik
Lapisan 1 (awan): 30 × 1.6¹ = 30 × 1.60 = 48 px/detik
Lapisan 2 (gunung): 30 × 1.6² = 30 × 2.56 = 77 px/detik
Lapisan 3 (pohon): 30 × 1.6³ = 30 × 4.10 = 123 px/detik
Lapisan 4 (tanah): 30 × 1.6⁴ = 30 × 6.55 = 197 px/detikUrutan lapisan menentukan segalanya
Lapisan pertama dalam daftar adalah yang paling lambat dan digambar paling belakang.
Kalau kamu menaruh tanah lebih dulu dan langit terakhir, efeknya terbalik — tanah bergerak lambat di belakang, langit melesat di depan. Hasilnya terlihat sangat aneh.
Urutkan dari paling jauh ke paling dekat.
Nilai multiplierDelta yang umum dipakai:
| Nilai | Efek |
|---|---|
| 1,0 | Semua lapisan bergerak sama — tidak ada kedalaman |
| 1,2–1,4 | Kedalaman halus, cocok untuk latar realistis |
| 1,5–1,8 | Kedalaman jelas, pilihan paling umum |
| > 2,0 | Sangat dramatis, lapisan depan sangat cepat |
Mengontrol kecepatan
Untuk game di mana latar bergerak mengikuti pemain, kamu perlu mengubah kecepatannya secara dinamis.
dart
class LatarParallax extends ParallaxComponent<GameSaya> {
@override
Future<void> onLoad() async {
await super.onLoad();
parallax = await game.loadParallax(
[
ParallaxImageData('langit.png'),
ParallaxImageData('gunung.png'),
ParallaxImageData('tanah.png'),
],
baseVelocity: Vector2.zero(), // mulai diam
velocityMultiplierDelta: Vector2(1.5, 1),
);
}
@override
void update(double dt) {
super.update(dt);
// Latar bergerak berlawanan arah dengan pemain
parallax?.baseVelocity = Vector2(
-game.pemain.kecepatanX * 0.4,
0,
);
}
}Tanda negatif itu penting: ketika pemain bergerak ke kanan, latar harus bergeser ke kiri agar terasa pemainlah yang bergerak.
Perlambatan mulus saat pemain berhenti:
dart
@override
void update(double dt) {
super.update(dt);
final target = Vector2(-game.pemain.kecepatanX * 0.4, 0);
final sekarang = parallax!.baseVelocity;
// Interpolasi menuju target — menghindari perubahan mendadak
parallax!.baseVelocity = sekarang + (target - sekarang) * (3 * dt);
}Rumus sekarang + (target - sekarang) * faktor adalah interpolasi linier, teknik yang sangat berguna untuk membuat perubahan apa pun terasa halus. Angka 3 menentukan seberapa cepat ia menyusul target.
Membuat gambar parallax
Persyaratan gambarnya punya beberapa aturan yang kalau dilanggar akan terlihat jelas.
┌──────────────────────────────────────────┐
│ Lapisan JAUH (langit, awan) │
│ • Opak, tidak transparan │
│ • Warna rendah kontras │
│ • Detail minimal │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Lapisan TENGAH (gunung, gedung) │
│ • Transparan di bagian atas │
│ • Kontras sedang │
└──────────────────────────────────────────┘
┌──────────────────────────────────────────┐
│ Lapisan DEKAT (pohon, tanah) │
│ • Sebagian besar transparan │
│ • Kontras tinggi, detail jelas │
└──────────────────────────────────────────┘Gambar harus bisa disambung mulus
Ini persyaratan terpenting. Karena gambar diulang terus-menerus, tepi kanan harus menyambung sempurna dengan tepi kiri.
❌ TIDAK MENYAMBUNG ✅ MENYAMBUNG
▲▲▲▲▲▲│▲▲▲▲▲▲ ▲▲▲╱╲▲▲│▲▲▲╱╲▲▲
↑ ↑
garis sambungan terlihat tidak terlihatCara memeriksanya: buka gambar di editor, gandakan, tempelkan salinannya persis di sebelah kanan aslinya. Kalau kamu bisa melihat di mana sambungannya, gambar itu belum siap.
Ukuran yang disarankan:
| Lapisan | Tinggi | Catatan |
|---|---|---|
| Langit | 1080 px | Sering cukup gradasi warna sederhana |
| Tengah | 1080 px | Lebar 2× tinggi agar pengulangan tidak terlihat |
| Dekat | 400–600 px | Hanya bagian bawah layar |
Mode pengulangan
dart
parallax = await game.loadParallax(
[
ParallaxImageData('langit.png'),
ParallaxImageData('gunung.png'),
],
baseVelocity: Vector2(20, 0),
velocityMultiplierDelta: Vector2(1.5, 1),
// Bagaimana gambar diulang
repeat: ImageRepeat.repeatX, // hanya horizontal
// repeat: ImageRepeat.repeat, // kedua arah
// repeat: ImageRepeat.noRepeat, // tidak diulang
// Perataan saat gambar lebih kecil dari layar
alignment: Alignment.bottomLeft,
// Bagaimana gambar diskalakan
fill: LayerFill.height, // penuhi tinggi
// fill: LayerFill.width, // penuhi lebar
// fill: LayerFill.none, // ukuran asli
);Untuk sebagian besar game side-scrolling, kombinasi yang tepat adalah repeatX + bottomLeft + LayerFill.height.
Pengaturan per lapisan
Kadang kamu butuh kendali yang lebih halus daripada satu pengali untuk semua.
dart
@override
Future<void> onLoad() async {
await super.onLoad();
final lapisan = await Future.wait([
game.loadParallaxLayer(
ParallaxImageData('langit.png'),
velocityMultiplier: Vector2(0.1, 0),
fill: LayerFill.height,
),
game.loadParallaxLayer(
ParallaxImageData('awan.png'),
velocityMultiplier: Vector2(0.3, 0),
repeat: ImageRepeat.repeatX,
alignment: Alignment.topCenter,
),
game.loadParallaxLayer(
ParallaxImageData('gunung.png'),
velocityMultiplier: Vector2(0.6, 0),
alignment: Alignment.bottomCenter,
),
game.loadParallaxLayer(
ParallaxImageData('tanah.png'),
velocityMultiplier: Vector2(1.0, 0),
alignment: Alignment.bottomLeft,
),
]);
parallax = Parallax(lapisan, baseVelocity: Vector2(60, 0));
}Cara ini memberi kendali penuh — misalnya awan yang diratakan ke atas sementara gunung ke bawah.
Parallax berbasis kamera
Untuk game dengan dunia yang lebih besar dari layar, parallax sebaiknya mengikuti posisi kamera, bukan waktu.
dart
class LatarKamera extends Component with HasGameReference<GameSaya> {
final List<_LapisanLatar> _lapisan = [];
@override
Future<void> onLoad() async {
await super.onLoad();
_lapisan.addAll([
_LapisanLatar(await Sprite.load('langit.png'), 0.1),
_LapisanLatar(await Sprite.load('gunung.png'), 0.35),
_LapisanLatar(await Sprite.load('pohon.png'), 0.7),
]);
}
@override
void render(Canvas canvas) {
super.render(canvas);
final kamera = game.camera.viewfinder.position;
final ukuran = game.size;
for (final lapisan in _lapisan) {
// Geser berdasarkan posisi kamera, dikali faktor kedalaman
final geser = -kamera.x * lapisan.faktor;
// Bungkus agar berulang mulus
final lebar = lapisan.sprite.srcSize.x;
final offset = geser % lebar;
// Gambar dua kali agar tidak ada celah saat bergeser
lapisan.sprite.render(
canvas,
position: Vector2(offset - lebar, 0),
size: Vector2(lebar, ukuran.y),
);
lapisan.sprite.render(
canvas,
position: Vector2(offset, 0),
size: Vector2(lebar, ukuran.y),
);
lapisan.sprite.render(
canvas,
position: Vector2(offset + lebar, 0),
size: Vector2(lebar, ukuran.y),
);
}
}
}
class _LapisanLatar {
_LapisanLatar(this.sprite, this.faktor);
final Sprite sprite;
final double faktor;
}Menggambar tiga salinan — satu di kiri, satu di tengah, satu di kanan — memastikan tidak pernah ada celah kosong berapa pun posisi geserannya.
Operator % (modulo) yang membuat pengulangannya mulus: berapa pun besar nilai geser, hasilnya selalu berada dalam rentang satu lebar gambar.
Kamera yang mengikuti pemain
Parallax berbasis kamera butuh kamera yang bergerak.
dart
class GameSaya extends FlameGame {
late final Pemain pemain;
@override
Future<void> onLoad() async {
await super.onLoad();
pemain = Pemain();
world.add(pemain);
// Kamera mengikuti pemain
camera.follow(pemain);
// Batasi kamera agar tidak keluar dari dunia
camera.setBounds(
Rectangle.fromLTRB(
size.x / 2,
size.y / 2,
lebarDunia - size.x / 2,
tinggiDunia - size.y / 2,
),
);
}
}Beberapa opsi kamera yang berguna:
dart
// Mengikuti dengan kelembutan — tidak kaku menempel
camera.follow(pemain, maxSpeed: 300);
// Hanya mengikuti secara horizontal
camera.follow(pemain, horizontalOnly: true);
// Berhenti mengikuti
camera.stop();
// Guncangan kamera untuk ledakan
camera.viewfinder.add(
MoveEffect.by(
Vector2(8, 0),
NoiseEffectController(duration: 0.3, frequency: 20),
),
);Guncangan kamera itu adalah salah satu efek dengan rasio dampak terhadap usaha tertinggi dalam desain game — tiga baris kode yang membuat ledakan terasa jauh lebih berbobot.
Parallax tanpa gambar
Untuk gaya visual minimalis, kamu bisa membuat parallax dari bentuk sederhana.
dart
// lib/komponen/bintang_parallax.dart
import 'dart:math';
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class BintangParallax extends Component with HasGameReference {
BintangParallax({this.jumlahLapisan = 3});
final int jumlahLapisan;
final _acak = Random();
final List<_Lapisan> _lapisan = [];
@override
Future<void> onLoad() async {
await super.onLoad();
for (var i = 0; i < jumlahLapisan; i++) {
final kedalaman = (i + 1) / jumlahLapisan;
_lapisan.add(
_Lapisan(
bintang: List.generate(
40 - i * 8, // lapisan jauh punya lebih banyak bintang kecil
(_) => Vector2(
_acak.nextDouble() * game.size.x,
_acak.nextDouble() * game.size.y,
),
),
kecepatan: 20 + kedalaman * 90,
jari: 0.6 + kedalaman * 1.6,
cat: Paint()
..color = Colors.white.withValues(alpha: 0.25 + kedalaman * 0.6),
),
);
}
}
@override
void update(double dt) {
super.update(dt);
for (final lapisan in _lapisan) {
for (final bintang in lapisan.bintang) {
bintang.y += lapisan.kecepatan * dt;
// Bungkus ke atas saat keluar bawah
if (bintang.y > game.size.y) {
bintang.y = -2;
bintang.x = _acak.nextDouble() * game.size.x;
}
}
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
for (final lapisan in _lapisan) {
for (final bintang in lapisan.bintang) {
canvas.drawCircle(
Offset(bintang.x, bintang.y),
lapisan.jari,
lapisan.cat,
);
}
}
}
}
class _Lapisan {
_Lapisan({
required this.bintang,
required this.kecepatan,
required this.jari,
required this.cat,
});
final List<Vector2> bintang;
final double kecepatan;
final double jari;
final Paint cat;
}Ini menghasilkan latar luar angkasa yang meyakinkan tanpa satu pun berkas gambar — dan sangat ringan karena hanya menggambar lingkaran kecil.
Perhatikan bahwa Paint dibuat sekali per lapisan, bukan setiap frame — sesuai peringatan performa di Bab 42.
Performa
Parallax relatif murah, tetapi ada beberapa hal yang perlu diperhatikan.
Jumlah lapisan. Tiga sampai lima sudah memberi kedalaman yang meyakinkan. Lebih dari itu jarang terasa bedanya tetapi tetap membebani.
Ukuran gambar. Gambar 4000 piksel yang ditampilkan pada layar 1080 piksel memakan memori empat kali lipat tanpa manfaat visual. Sesuaikan ukurannya.
Transparansi. Lapisan dengan transparansi lebih mahal digambar daripada yang opak. Buat lapisan paling belakang sepenuhnya opak — ia menutupi seluruh layar, jadi tidak butuh transparansi.
dart
// Memeriksa dampaknya
add(FpsTextComponent(position: Vector2(10, 10)));Uji di perangkat kelas bawah
Parallax terlihat mulus di perangkat baru bahkan ketika implementasinya boros. Masalahnya baru muncul di perangkat lama — yang sering menjadi mayoritas penggunamu.
Sesuai peringatan di Bab 13, ukur selalu dalam mode --profile, bukan debug.
Contoh lengkap
dart
// lib/game_lari.dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/parallax.dart';
import 'package:flutter/material.dart';
class GameLari extends FlameGame {
late final ParallaxComponent _latar;
late final Pemain pemain;
double kecepatanDunia = 120;
@override
Future<void> onLoad() async {
await super.onLoad();
_latar = await _buatLatar();
add(_latar..priority = -10);
pemain = Pemain()..position = Vector2(80, size.y - 120);
add(pemain);
// Percepat secara bertahap
add(
TimerComponent(
period: 10,
repeat: true,
onTick: () {
kecepatanDunia = (kecepatanDunia * 1.12).clamp(120, 420);
},
),
);
}
Future<ParallaxComponent> _buatLatar() async {
final parallax = await loadParallax(
[
ParallaxImageData('langit.png'),
ParallaxImageData('awan.png'),
ParallaxImageData('gunung.png'),
ParallaxImageData('pohon.png'),
ParallaxImageData('tanah.png'),
],
baseVelocity: Vector2(kecepatanDunia * 0.15, 0),
velocityMultiplierDelta: Vector2(1.7, 1),
repeat: ImageRepeat.repeatX,
alignment: Alignment.bottomLeft,
fill: LayerFill.height,
);
return ParallaxComponent(parallax: parallax);
}
@override
void update(double dt) {
super.update(dt);
// Sinkronkan kecepatan latar dengan kecepatan dunia
_latar.parallax?.baseVelocity =
Vector2(kecepatanDunia * 0.15, 0);
}
}Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Empat Lapisan yang Mengikuti — ⭐⭐ · 40–60 menit
Tantangan: Bangun latar bergerak empat lapisan untuk satu adegan pilihanmu, lalu buat kecepatannya mengikuti kecepatan pemain dengan transisi yang halus — bukan berubah mendadak saat pemain mulai dan berhenti bergerak.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ ░░░░░░ (langit, paling jauh) │ ← paling lambat
│ ▒▒ ▒▒▒ (gunung) │
│ ▓▓▓▓▓▓▓▓ (pohon) │
│═══════════════ (tanah, dekat) │ ← paling cepat
│ 🙂→ │
└─────────────────────────────────┘Pemain bergerak ke kanan → semua lapisan geser ke kiri, tapi lapisan tanah (dekat) bergeser jauh lebih cepat daripada lapisan langit (jauh) — itulah ilusi kedalamannya.
Kriteria selesai:
- Empat lapisan tersusun dari yang paling jauh ke yang paling dekat, dengan
velocityMultiplierDeltadi rentang yang memberi kesan kedalaman. - Gambar tiap lapisan bisa disambung mulus — kamu sudah memeriksanya dengan menempelkan salinannya di sebelah aslinya.
- Latar bergerak berlawanan arah dengan pemain, dengan tanda yang benar.
- Perubahan kecepatan dihaluskan dengan interpolasi linier, bukan ditugaskan langsung.
- Ada guncangan kamera pada satu kejadian, sekecil apa pun.
Petunjuk: Kalau kamu tidak punya aset dan tidak ingin mencarinya, bab ini menunjukkan bahwa parallax bisa dibuat tanpa gambar sama sekali — beberapa lapisan bentuk sederhana pada kecepatan berbeda sudah menghasilkan ilusi yang meyakinkan, dan untuk latihan ini itu justru lebih cepat. Rumus interpolasi yang kamu butuhkan adalah sekarang + (target - sekarang) * faktor dipanggil setiap frame dengan faktor kecil; hasilnya mendekati target dengan cepat di awal lalu melambat, yang persis terasa seperti momentum. Soal sambungan gambar: celah setipis satu piksel tidak terlihat saat diam tetapi menjadi garis berkedip saat bergerak, dan itu salah satu detail yang paling cepat membuat sebuah game terasa amatir.
Variasi 2: Dunia yang Luas — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun peta yang jauh lebih besar dari layar dengan kamera yang mengikuti pemain, dan latar parallax yang bergerak berdasarkan posisi kamera, bukan berdasarkan waktu. Pemain harus bisa bergerak ke segala arah, termasuk kembali ke tempat asal.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ (bagian dunia yang terlihat) │
│ 🙂 │
│ │
└─────────────────────────────────┘Dunia jauh lebih besar dari layar; kamera selalu mengikuti pemain dengan lembut, dan berhenti tepat di tepi dunia — bukan menampilkan area kosong begitu pemain mendekati ujung peta.
Kriteria selesai:
- Latar bergerak benar saat pemain berbalik arah, tanpa melompat.
- Pengulangan latar memakai operator modulo, dan tidak ada celah yang terlihat di ujung mana pun.
- Kamera mengikuti pemain dengan kelembutan, bukan menempel kaku.
- Kamera berhenti di tepi dunia alih-alih menampilkan ruang kosong di luar peta.
Petunjuk: Parallax berbasis waktu hanya bekerja untuk gerakan satu arah yang tidak pernah berbalik. Begitu pemain bisa berbalik, posisinya sendiri yang harus menjadi acuan. Untuk pengulangan tanpa celah, gambar tiga salinan lapisan — satu di posisi sekarang, satu di kiri, satu di kanan — karena dengan dua salinan akan selalu ada momen ketika sisi terjauh belum tertutup.
Variasi 3: Langit yang Berubah — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Tambahkan siklus siang-malam ke latar parallax: warna langit berubah bertahap, bintang muncul dan memudar, dan lapisan awan bergerak dengan kecepatan yang berubah menurut "cuaca" yang bisa berganti saat bermain.
Bayangan tampilan (siklus siang-malam):
text
Siang Senja Malam
┌─────────┐ ┌─────────┐ ┌─────────┐
│ ░░░☀░░░ │ ──▶ │ ▒▒▒🌇▒▒▒ │ ──▶ │ ▓▓▓✨🌙▓▓ │
│ (biru) │ │ (jingga) │ │ (gelap, │
│ │ │ │ │ bintang)│
└─────────┘ └─────────┘ └─────────┘Peralihan antar tahap ini mulus (warna berinterpolasi, bintang memudar masuk/keluar), bukan melompat seketika dari satu ke lainnya.
Kriteria selesai:
- Warna langit beralih mulus antar beberapa tahap waktu, bukan melompat.
- Bintang memudar masuk dan keluar sesuai waktu, tanpa muncul mendadak.
- Pergantian cuaca mengubah kecepatan dan opasitas lapisan awan secara halus.
- Seluruh perubahan tetap mulus di perangkat lemah — kamu sudah mengukurnya.
Petunjuk: Interpolasi warna antar lebih dari dua tahap paling rapi ditangani dengan menyusun daftar tahap beserta waktunya, lalu mencari dua tahap yang mengapit waktu sekarang dan menginterpolasi di antaranya. Pola yang sama berlaku untuk apa pun yang berubah bertahap sepanjang siklus. Untuk performa, ingat bahwa menggambar lapisan besar dengan transparansi berlapis-lapis mahal — batasi jumlah lapisan yang benar-benar transparan.
Ikhtisar
- Parallax menciptakan ilusi kedalaman dengan menggerakkan lapisan latar pada kecepatan berbeda. Objek lebih dekat bergerak lebih cepat.
- Urutkan lapisan dari paling jauh ke paling dekat — lapisan pertama adalah yang paling lambat dan paling belakang.
velocityMultiplierDeltabekerja secara pangkat: kecepatan lapisan ke-n =baseVelocity × delta^n. Nilai 1,5–1,8 paling umum.- Gambar wajib bisa disambung mulus. Periksa dengan menempelkan salinannya di sebelah aslinya.
repeatX+bottomLeft+LayerFill.heightadalah kombinasi yang tepat untuk sebagian besar game side-scrolling.- Latar bergerak berlawanan arah dengan pemain — perhatikan tanda negatif.
- Interpolasi linier (
sekarang + (target - sekarang) * faktor) membuat perubahan kecepatan terasa halus. - Untuk dunia besar, pakai parallax berbasis kamera dengan operator modulo untuk pengulangan, dan gambar tiga salinan agar tidak ada celah.
camera.follow(pemain, maxSpeed: 300)memberi pengikutan yang lembut; guncangan kamera adalah efek dengan dampak tertinggi per baris kode.- Tiga sampai lima lapisan sudah cukup meyakinkan.
- Buat lapisan paling belakang sepenuhnya opak — transparansi lebih mahal digambar.
- Parallax bisa dibuat tanpa gambar sama sekali dengan bentuk sederhana seperti bintang.
- Uji di perangkat kelas bawah dalam mode
--profile.
Berikutnya: Bab 49 — Particle System.
Transkrip asli
Disintesis dari 5_flutter_flame/10_case-study-6-parallax.md (6 video: What is Parallax, Coding Parallax, Coding more Parallax, dan latihan-latihannya). Lihat PDF Flame Engine.