Tampilan
Bab 43 — Component & Vector
Bab ini membedah dua konsep yang mendasari hampir semua yang kamu bangun di Flame: PositionComponent sebagai induk dari hampir seluruh objek game, dan vektor sebagai cara menyatakan posisi, kecepatan, serta arah.
Kita akan membangunnya lewat studi kasus yang dipakai kursus sumbernya: sebuah game sederhana bernama "Squares" — kotak-kotak yang bergerak, berputar, dan bisa diketuk.
PositionComponent
Ini kelas dasar untuk hampir setiap komponen visual di Flame. Memahami propertinya berarti memahami sebagian besar API-nya.
dart
class Kotak extends PositionComponent {
Kotak()
: super(
position: Vector2(100, 100), // di mana ia berada
size: Vector2(80, 80), // seberapa besar
angle: 0, // rotasi dalam RADIAN
anchor: Anchor.center, // titik acuan
priority: 1, // urutan gambar
);
}Lima properti itu menjelaskan hampir semua perilaku visual.
position
Posisi komponen di dalam induknya. Untuk komponen yang ditambahkan langsung ke game, ini adalah posisi di layar.
dart
position = Vector2(100, 200);
position.x = 150;
position += Vector2(10, 0); // geser ke kanan
position.add(Vector2(0, 5)); // sama, tanpa membuat objek baruSistem koordinat game
Berbeda dari matematika sekolah, sumbu Y menunjuk ke bawah:
(0,0) ─────────────────► X bertambah ke kanan
│
│
│ (100, 50)
│ ●
│
▼
Y bertambah ke bawahJadi untuk menggerakkan sesuatu ke atas, kamu mengurangi Y.
size
Ukuran komponen. Ia dipakai untuk deteksi ketukan dan tabrakan, jadi harus mencerminkan ukuran visual yang sebenarnya.
dart
size = Vector2(80, 60);
size = Vector2.all(80); // persegi
size.scale(1.5); // perbesar 50%angle
Rotasi dalam radian, bukan derajat. Ini sumber kebingungan yang sangat umum.
Lingkaran penuh = 360 derajat = 2π radian ≈ 6,283
0 radian = 0° (tidak berputar)
π/2 ≈ 1,571 = 90°
π ≈ 3,142 = 180°
3π/2 ≈ 4,712 = 270°
2π ≈ 6,283 = 360° (kembali ke awal)Konversinya:
dart
import 'dart:math' as math;
double keRadian(double derajat) => derajat * math.pi / 180;
double keDerajat(double radian) => radian * 180 / math.pi;
// Memakainya
angle = keRadian(45); // miring 45 derajat
angle += keRadian(90); // putar 90 derajat lagiUntuk rotasi terus-menerus, ingat aturan dt dari Bab 42:
dart
@override
void update(double dt) {
super.update(dt);
angle += 1.5 * dt; // 1,5 radian per DETIK ≈ 86°/detik
}anchor
Titik pada komponen yang dijadikan acuan position dan pusat rotasi. Ini properti yang paling sering menyebabkan hasil tidak sesuai dugaan.
Anchor.topLeft (BAWAAN) Anchor.center
●─────────────┐ ┌─────────────┐
│ │ │ │
│ position │ │ ● │ ← position
│ ada di │ │ │
│ pojok │ │ │
└─────────────┘ └─────────────┘
Berputar mengelilingi Berputar mengelilingi
pojok kiri atas titik tengahdart
anchor = Anchor.center; // paling sering dipakai
anchor = Anchor.topLeft; // bawaan
anchor = Anchor.bottomCenter; // berguna untuk karakter berdiri
anchor = const Anchor(0.5, 0.8); // kustomAnchor mengubah arti position
Dengan Anchor.topLeft (bawaan), position = Vector2(100, 100) menaruh pojok kiri atas di titik itu.
Dengan Anchor.center, titik yang sama menaruh pusat komponen di sana — artinya komponen berukuran 80×80 akan menempati area dari (60, 60) sampai (140, 140).
Untuk objek yang berputar, Anchor.center hampir selalu yang kamu inginkan. Berputar dengan anchor di pojok menghasilkan gerakan mengayun yang jarang diharapkan.
priority
Menentukan urutan menggambar. Nilai lebih tinggi digambar di atas.
dart
latar.priority = 0;
musuh.priority = 5;
pemain.priority = 10;
efekPartikel.priority = 20;
hud.priority = 100;Ini padanan Stack di Flutter dari Bab 4, tetapi dengan angka alih-alih urutan dalam list.
Vector2
Vector2 adalah pasangan dua angka, tetapi memperlakukannya hanya sebagai "koordinat" melewatkan sebagian besar kegunaannya.
dart
final v = Vector2(3, 4);
v.x; // 3
v.y; // 4
v.length; // 5 — panjangnya (teorema Pythagoras)
v.length2; // 25 — panjang kuadrat, lebih cepat dihitungVektor sebagai kecepatan
Ini penggunaan yang paling penting untuk dipahami. Sebuah vektor bisa mewakili arah dan besaran sekaligus.
dart
final kecepatan = Vector2(100, -50);
// Artinya: 100 piksel/detik ke kanan, 50 piksel/detik ke ATAS
// (ingat, Y negatif = ke atas)
position += kecepatan * dt; Vector2(100, -50)
▲ -Y (atas)
│
╱ │
╱ │ 50
╱ │
●───────┴──────► +X (kanan)
100
Panjang = √(100² + 50²) ≈ 112 piksel/detik
Arah = ke kanan-atasNormalisasi
Sering kali kamu ingin hanya arahnya, tanpa besarannya. Untuk itu ada normalisasi — mengubah vektor menjadi panjang tepat 1.
dart
final arah = Vector2(3, 4);
arah.normalize(); // sekarang panjangnya 1
print(arah); // (0.6, 0.8)
// Sekarang kamu bisa mengalikan dengan kecepatan apa pun
final kecepatan = arah * 250; // 250 piksel/detik ke arah ituKenapa ini penting? Perhatikan bug klasik ini:
dart
// ❌ Gerakan diagonal jadi lebih cepat
if (tekanKanan) posisi.x += 200 * dt;
if (tekanBawah) posisi.y += 200 * dt;
// Menekan keduanya: kecepatan efektif = √(200² + 200²) ≈ 283
// Pemain bergerak 41% lebih cepat secara diagonal!
// ✅ Kecepatan konsisten ke segala arah
final arah = Vector2(
(tekanKanan ? 1 : 0) - (tekanKiri ? 1 : 0),
(tekanBawah ? 1 : 0) - (tekanAtas ? 1 : 0),
);
if (!arah.isZero()) {
arah.normalize();
posisi += arah * 200 * dt;
}Pemeriksaan isZero() penting: menormalisasi vektor nol menghasilkan NaN, yang akan membuat komponenmu menghilang dari layar tanpa error.
Operasi lain yang berguna
dart
final a = Vector2(10, 20);
final b = Vector2(5, 5);
a + b; // Vector2(15, 25)
a - b; // Vector2(5, 15)
a * 2; // Vector2(20, 40)
a.distanceTo(b); // jarak antara dua titik
a.angleTo(b); // sudut antara dua vektor
a.dot(b); // dot product
// Menyalin — PENTING
final c = a.clone(); // objek baru
final d = a; // ❌ referensi yang SAMAVector2 adalah objek yang bisa diubah
Ini jebakan yang menyebabkan bug aneh:
dart
// ❌ Semua kotak berbagi vektor yang sama
final posisiAwal = Vector2(100, 100);
for (var i = 0; i < 5; i++) {
add(Kotak(position: posisiAwal)); // objek yang SAMA
}
// Menggerakkan satu kotak menggerakkan semuanya!
// ✅ Setiap kotak punya vektornya sendiri
for (var i = 0; i < 5; i++) {
add(Kotak(position: posisiAwal.clone()));
}Ini persoalan yang sama dengan final yang mengunci alamat bukan isi, dibahas di Bab 12.
Menghitung arah menuju target
Pola yang sangat sering dipakai — musuh yang mengejar pemain:
dart
@override
void update(double dt) {
super.update(dt);
final arah = (targetPosisi - position)..normalize();
position += arah * kecepatan * dt;
}Notasi ..normalize() adalah cascade dari Bab 2: ia menormalisasi hasil pengurangan lalu mengembalikan objek itu sendiri.
Studi kasus: Squares
Mari bangun sesuatu yang lengkap. Beberapa kotak berwarna yang berputar, bergerak, memantul di tepi layar, dan berubah warna ketika diketuk.
dart
// lib/komponen/kotak.dart
import 'dart:math' as math;
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';
class Kotak extends PositionComponent with TapCallbacks {
Kotak({
required Vector2 posisi,
required this.kecepatan,
required this.kecepatanRotasi,
required Color warna,
}) : _warna = warna,
super(
position: posisi,
size: Vector2.all(60),
anchor: Anchor.center, // agar berputar di tengahnya
);
final Vector2 kecepatan;
final double kecepatanRotasi;
Color _warna;
late Paint _cat = Paint()..color = _warna;
bool _terpilih = false;
static final _acak = math.Random();
@override
void update(double dt) {
super.update(dt);
// Bergerak
position += kecepatan * dt;
// Berputar
angle += kecepatanRotasi * dt;
// Jaga sudut tetap dalam rentang 0–2π agar tidak membesar tanpa batas
if (angle > 2 * math.pi) angle -= 2 * math.pi;
_pantulkanDiTepi();
}
void _pantulkanDiTepi() {
final batas = findGame()!.size;
// Anchor di tengah, jadi setengah ukuran adalah jaraknya ke tepi
final setengah = size.x / 2;
if (position.x - setengah <= 0) {
position.x = setengah;
kecepatan.x = kecepatan.x.abs();
} else if (position.x + setengah >= batas.x) {
position.x = batas.x - setengah;
kecepatan.x = -kecepatan.x.abs();
}
if (position.y - setengah <= 0) {
position.y = setengah;
kecepatan.y = kecepatan.y.abs();
} else if (position.y + setengah >= batas.y) {
position.y = batas.y - setengah;
kecepatan.y = -kecepatan.y.abs();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(size.toRect(), _cat);
// Garis tepi kalau sedang terpilih
if (_terpilih) {
canvas.drawRect(
size.toRect(),
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 3,
);
}
}
// --- Deteksi ketukan ---
@override
void onTapDown(TapDownEvent event) {
_terpilih = true;
// Ganti ke warna acak
_warna = Color.fromARGB(
255,
_acak.nextInt(256),
_acak.nextInt(256),
_acak.nextInt(256),
);
_cat = Paint()..color = _warna;
}
@override
void onTapUp(TapUpEvent event) {
_terpilih = false;
}
@override
void onTapCancel(TapCancelEvent event) {
_terpilih = false;
}
}Beberapa hal yang layak diperhatikan:
with TapCallbacks — mixin yang memberi komponen kemampuan mendeteksi ketukan. Flame memakai size dan position untuk menentukan apakah ketukan mengenai komponen ini.
findGame()! — cara komponen mengakses game induknya. Lebih aman daripada parent as FlameGame karena bekerja meskipun komponennya bersarang beberapa tingkat.
Membatasi angle — tanpa if (angle > 2 * math.pi), nilainya terus membesar. Setelah berjam-jam berjalan, presisi double mulai berkurang dan rotasi menjadi tersendat.
_cat dibuat ulang hanya saat warna berubah, bukan setiap frame — sesuai peringatan performa di Bab 42. Kotak putih di dalam render() memang dibuat setiap frame, tetapi hanya saat terpilih, sehingga dampaknya kecil.
Game-nya:
dart
// lib/game_squares.dart
import 'dart:math';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'komponen/kotak.dart';
class GameSquares extends FlameGame {
final _acak = Random();
@override
Color backgroundColor() => const Color(0xFF1A1A2E);
@override
Future<void> onLoad() async {
await super.onLoad();
_buatKotak(8);
}
void _buatKotak(int jumlah) {
for (var i = 0; i < jumlah; i++) {
add(
Kotak(
posisi: Vector2(
60 + _acak.nextDouble() * (size.x - 120),
60 + _acak.nextDouble() * (size.y - 120),
),
kecepatan: Vector2(
(_acak.nextDouble() - 0.5) * 300,
(_acak.nextDouble() - 0.5) * 300,
),
kecepatanRotasi: (_acak.nextDouble() - 0.5) * 4,
warna: Color.fromARGB(
255,
_acak.nextInt(200) + 55,
_acak.nextInt(200) + 55,
_acak.nextInt(200) + 55,
),
),
);
}
}
}Perhatikan _acak.nextInt(200) + 55 untuk warna — ini menghindari warna yang terlalu gelap sehingga tidak terlihat di latar gelap.
Komposabilitas: komponen di dalam komponen
Ini konsep yang membuat sistem komponen benar-benar berguna. Sebuah komponen bisa punya komponen anak, dan anak-anak itu mengikuti induknya.
Contoh yang dipakai kursus sumbernya: menambahkan bilah nyawa ke kotak.
dart
// lib/komponen/bilah_nyawa.dart
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class BilahNyawa extends PositionComponent {
BilahNyawa({required Vector2 ukuranInduk})
: super(
// Posisi RELATIF terhadap induk
position: Vector2(0, -14),
size: Vector2(ukuranInduk.x, 6),
);
double _nyawa = 1.0; // 0.0 sampai 1.0
final _catLatar = Paint()..color = Colors.black54;
final _catIsi = Paint()..color = Colors.greenAccent;
set nyawa(double nilai) {
_nyawa = nilai.clamp(0.0, 1.0);
// Warna berubah sesuai kondisi
_catIsi.color = switch (_nyawa) {
> 0.6 => Colors.greenAccent,
> 0.3 => Colors.orangeAccent,
_ => Colors.redAccent,
};
}
double get nyawa => _nyawa;
@override
void render(Canvas canvas) {
super.render(canvas);
// Latar
canvas.drawRect(size.toRect(), _catLatar);
// Isi sesuai nyawa
canvas.drawRect(
Rect.fromLTWH(0, 0, size.x * _nyawa, size.y),
_catIsi,
);
}
}Menambahkannya sebagai anak:
dart
class KotakBernyawa extends PositionComponent {
late final BilahNyawa _bilah;
@override
Future<void> onLoad() async {
await super.onLoad();
_bilah = BilahNyawa(ukuranInduk: size);
add(_bilah); // ditambahkan sebagai ANAK
}
void terkenaSerangan(double kerusakan) {
_bilah.nyawa -= kerusakan;
if (_bilah.nyawa <= 0) {
removeFromParent();
}
}
}Karena bilah nyawa adalah anak, ia otomatis:
- Bergerak mengikuti induknya.
- Berputar mengikuti induknya.
- Dihapus ketika induknya dihapus.
Pohon komponen Di layar
───────────────── ──────────────────
FlameGame ┌──────────────┐
├── KotakBernyawa │ ▓▓▓▓▓▓░░░░ │ ← bilah (anak)
│ └── BilahNyawa │ ┌──────────┐ │
├── KotakBernyawa │ │ │ │
│ └── BilahNyawa │ │ kotak │ │ ← induk
└── KotakBernyawa │ │ │ │
└── BilahNyawa │ └──────────┘ │
└──────────────┘Rotasi ikut diwariskan
Ini kadang tidak diinginkan. Kalau kotaknya berputar, bilah nyawanya ikut berputar — dan bilah nyawa yang miring sulit dibaca.
Solusinya: jangan jadikan bilah sebagai anak yang berputar. Taruh sebagai anak langsung game, lalu perbarui posisinya mengikuti kotak:
dart
@override
void update(double dt) {
super.update(dt);
_bilah.position = kotak.position + Vector2(0, -20);
}Jenis komponen bawaan
Flame menyediakan beberapa komponen siap pakai:
dart
// Bentuk sederhana
CircleComponent(radius: 30, paint: cat);
RectangleComponent(size: Vector2(50, 50), paint: cat);
PolygonComponent([Vector2(0,0), Vector2(50,0), Vector2(25,50)]);
// Gambar
SpriteComponent(sprite: sprite, size: Vector2(64, 64));
// Animasi dari sprite sheet
SpriteAnimationComponent(animation: animasi, size: Vector2(64, 64));
// Teks
TextComponent(
text: 'Skor: 100',
textRenderer: TextPaint(
style: const TextStyle(color: Colors.white, fontSize: 24),
),
);
// Wadah tanpa tampilan — untuk mengelompokkan
PositionComponent();Memuat sprite:
dart
class Pemain extends SpriteComponent {
Pemain() : super(size: Vector2(64, 64), anchor: Anchor.center);
@override
Future<void> onLoad() async {
await super.onLoad();
sprite = await Sprite.load('pemain.png');
}
}Flame mencari assets/images/pemain.png — konvensi yang dibahas di Bab 42.
Efek: animasi tanpa menulis update
Flame punya sistem efek yang mirip animasi implisit di Bab 11.
dart
import 'package:flame/effects.dart';
// Bergerak ke posisi tertentu
komponen.add(
MoveToEffect(
Vector2(300, 200),
EffectController(duration: 1.5, curve: Curves.easeInOut),
),
);
// Berputar
komponen.add(
RotateEffect.by(
2 * math.pi,
EffectController(duration: 2, infinite: true),
),
);
// Membesar-mengecil berulang
komponen.add(
ScaleEffect.to(
Vector2.all(1.3),
EffectController(
duration: 0.5,
reverseDuration: 0.5,
infinite: true,
),
),
);
// Memudar lalu hapus
komponen.add(
OpacityEffect.fadeOut(
EffectController(duration: 0.8),
onComplete: () => komponen.removeFromParent(),
),
);
// Beberapa efek berurutan
komponen.add(
SequenceEffect([
MoveToEffect(Vector2(200, 100), EffectController(duration: 1)),
ScaleEffect.to(Vector2.all(0), EffectController(duration: 0.3)),
RemoveEffect(),
]),
);Efek jauh lebih ringkas daripada menulis logika di update(), dan Flame mengurus pembersihannya sendiri.
Menambah dan menghapus komponen
dart
// Menambah
add(komponen);
addAll([a, b, c]);
// Menghapus
komponen.removeFromParent();
remove(komponen);
removeAll(children);
// Memeriksa
if (komponen.isMounted) { }
if (komponen.isRemoving) { }Jangan menghapus saat sedang mengiterasi
dart
// ❌ Mengubah daftar sambil membacanya
for (final anak in children) {
if (anak is Musuh && anak.mati) {
anak.removeFromParent(); // 💥
}
}
// ✅ Kumpulkan dulu, hapus setelahnya
final untukDihapus = children.whereType<Musuh>()
.where((m) => m.mati)
.toList();
for (final m in untukDihapus) {
m.removeFromParent();
}Sebenarnya Flame menunda penghapusan sampai akhir frame, sehingga kasus di atas sering tetap bekerja — tetapi bergantung pada perilaku itu membuat kodemu rapuh.
Menemukan komponen lain
dart
// Dari komponen, akses game
final game = findGame()!;
// Cari komponen berdasarkan tipe
final semuaMusuh = game.children.whereType<Musuh>();
final pemain = game.children.whereType<Pemain>().firstOrNull;
// Cari di seluruh pohon, termasuk yang bersarang
final semua = game.descendants().whereType<Peluru>();Untuk game yang lebih besar, menyimpan referensi langsung lebih efisien daripada mencari setiap frame:
dart
class GameSaya extends FlameGame {
late final Pemain pemain;
@override
Future<void> onLoad() async {
await super.onLoad();
pemain = Pemain();
add(pemain);
}
}
// Komponen lain mengaksesnya langsung
class Musuh extends PositionComponent
with HasGameReference<GameSaya> {
@override
void update(double dt) {
super.update(dt);
final arah = (game.pemain.position - position)..normalize();
position += arah * 80 * dt;
}
}Mixin HasGameReference<T> memberi properti game yang sudah bertipe benar — lebih bersih daripada findGame()! as GameSaya.
Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Penjaga yang Berpatroli — ⭐⭐ · 40–60 menit
Tantangan: Buat sebuah komponen "penjaga" yang berpatroli di antara dua titik, dan ketika pemain (sebuah komponen yang bisa kamu geser dengan jari) mendekat dalam radius tertentu, ia berhenti berpatroli dan mengejar. Ketika pemain menjauh, ia kembali ke jalur patrolinya.
Bayangan tampilan (viewport game):
text
┌─────────────────────────────────┐
│ A┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄B │
│ 👹 (berpatroli) │
│ │
│ ○ (radius deteksi)│
│ 🙂 (pemain) │
└─────────────────────────────────┘Pemain masuk radius → 👹 berhenti berpatroli dan mengejar, tubuhnya berputar menghadap arah geraknya:
text
┌─────────────────────────────────┐
│ A┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄B │
│ 👹→ (mengejar) │
│ 🙂 │
└─────────────────────────────────┘Kriteria selesai:
- Pengejaran memakai pola
(target - position)..normalize()dikalikan kecepatan dandt— bukan menggerakkan X dan Y secara terpisah. - Penjaga memakai
Anchor.center, dan ia berputar menghadap arah geraknya. isZero()diperiksa sebelum normalisasi, sehingga penjaga yang tepat berada di posisi pemain tidak menghilang.- Radius deteksi digambar sebagai lingkaran agar kamu bisa melihat kapan perilakunya berubah.
- Tidak ada satu pun
Vector2yang dibagikan antar komponen tanpa.clone().
Petunjuk: Komponen yang menghilang tanpa error hampir selalu berarti posisinya menjadi NaN, dan NaN hampir selalu berasal dari normalisasi vektor nol — pembagian nol dengan nol. Itu terjadi tepat pada frame ketika penjaga persis menyentuh pemain, yang membuatnya terasa acak padahal sangat deterministik. Soal .clone(): Vector2 bisa diubah, jadi menugaskan position sebuah komponen dari position komponen lain membuat keduanya menunjuk objek yang sama — dan keduanya bergerak bersamaan seperti terikat. Untuk menghadap arah gerak, ingat bahwa angle dalam radian, dan atan2 menerima Y lebih dulu.
Variasi 2: Sistem Tata Surya — ⭐⭐ · 40–60 menit
Tantangan: Bangun tata surya sederhana: matahari di tengah, beberapa planet mengorbit dengan kecepatan berbeda, dan setidaknya satu planet punya bulan yang mengorbitnya. Bulan harus ikut terbawa ketika planetnya bergerak, tanpa kamu menghitung posisinya secara absolut.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ 🌕 (bulan) │
│ 🪐 (planet, mengorbit)│
│ │
│ ☀️ (matahari, tengah) │
│ │
│ 🌍 (planet lain, orbit │
│ lebih cepat) │
└─────────────────────────────────┘Geser 🪐 dengan jari → 🌕 (anak dari planet itu) ikut berpindah otomatis, tanpa kode tambahan untuk menghitung posisi bulan.
Kriteria selesai:
- Bulan adalah anak dari planet, bukan komponen sejajar yang posisinya dihitung manual.
- Menggeser planet dengan jari membawa bulannya ikut, tanpa kode tambahan.
- Kecepatan orbit tiap planet berbeda, dan
prioritymenentukan mana yang digambar di atas. - Sudut orbit dibatasi agar tidak membesar tanpa batas.
Petunjuk: Ini demonstrasi paling langsung dari komposabilitas: posisi anak selalu relatif terhadap induknya, jadi seluruh transformasi induk — posisi, rotasi, skala — otomatis berlaku bagi anak. Menyadari ini menghemat banyak matematika di game yang lebih besar. Untuk sudut yang membesar tanpa batas, biarkan ia berputar melewati dua pi dan kurangi kembali; angka pecahan yang sangat besar mulai kehilangan ketelitian setelah berjam-jam berjalan.
Variasi 3: Gerak Tanpa Update — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun animasi kartu yang dibagikan: lima kartu meluncur dari tumpukan ke posisinya masing-masing dengan jeda berurutan, membesar sedikit lalu kembali, dan berputar ke sudut akhirnya. Kerjakan seluruhnya dengan efek, tanpa menulis satu baris pun di update().
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ [🂠][🂠][🂠][🂠][🂠] ← tumpukan │
│ │
│ 🂡 🂢 🂣 🂤 🂥 │
│ (meluncur berurutan, membesar │
│ sedikit lalu kembali, lalu │
│ berputar ke sudut akhir) │
└─────────────────────────────────┘Kriteria selesai:
- Tidak ada
update()yang di-override di komponen kartu. - Efek berurutan dan efek bersamaan keduanya dipakai, di tempat yang tepat.
- Kurva pergerakan dipilih sadar, bukan linier.
- Kartu yang animasinya selesai memicu satu tindakan lanjutan.
Petunjuk: Sistem efek Flame menangani hal-hal yang biasanya kamu tulis manual: interpolasi, kurva, durasi, dan pembersihan setelah selesai. Ada efek untuk bergerak, berputar, mengubah ukuran, dan mengubah opasitas, plus pembungkus untuk menjalankan beberapa efek berurutan atau bersamaan. Untuk tindakan setelah selesai, efek menerima callback penyelesaian — jauh lebih andal daripada menghitung waktu sendiri.
Variasi 4: Formasi Musuh — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun sekelompok musuh yang bergerak sebagai satu formasi: mereka turun bersama, bergeser ke samping saat mencapai tepi, dan formasi tetap utuh meski sebagian anggotanya dihancurkan. Formasi juga bergerak makin cepat seiring berkurangnya anggota.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ 👾 👾 👾 👾 👾 │
│ 👾 👾 ✗ 👾 👾 │ ← satu hancur di tengah,
│ │ yang lain TIDAK bergeser
│ ← formasi geser ke samping │
│ saat menyentuh tepi layar → │
└─────────────────────────────────┘Kriteria selesai:
- Formasi adalah satu komponen induk, dan tiap musuh adalah anaknya dengan posisi relatif.
- Menghancurkan musuh di tengah tidak menggeser posisi musuh lain.
- Kecepatan formasi dihitung dari jumlah anggota yang tersisa, dengan
clamp. - Deteksi tepi memakai posisi absolut anggota terluar, bukan posisi induknya.
Petunjuk: Kriteria terakhir adalah tempat orang paling sering tersandung: posisi anak bersifat relatif, sehingga memeriksa apakah "formasi menyentuh tepi layar" butuh posisi absolut anggota yang paling kiri dan paling kanan. Komponen Flame menyediakan cara mendapatkan posisi absolut dari posisi relatif — carilah itu alih-alih menjumlahkan sendiri, karena penjumlahan manual akan salah begitu induknya juga berputar.
Variasi 5: Mengikuti Jalur — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Buat kendaraan yang mengikuti jalur berupa deretan titik: ia bergerak menuju titik berikutnya, berbelok halus alih-alih berputar seketika, dan melambat di tikungan tajam. Jalurnya bisa berulang atau bolak-balik.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ ●┄┄┄┄● │
│ ┆ 🚗 (melambat di │
│ ┆ tikungan tajam) │
│ ●┄┄┄┄┄┄┄● │
│ ┆ │
│ ● │
└─────────────────────────────────┘Kriteria selesai:
- Perpindahan ke titik berikutnya terjadi berdasarkan jarak ambang, bukan kesamaan posisi persis.
- Belokan dihaluskan — sudut berubah bertahap menuju sudut target, bukan ditugaskan langsung.
- Kendaraan melambat ketika perubahan sudut yang dibutuhkan besar.
- Jalur bisa diganti saat berjalan tanpa kendaraan melompat.
Petunjuk: Membandingkan posisi dengan kesamaan persis hampir tidak pernah berhasil pada angka pecahan — kendaraan akan melewati titiknya lalu berputar-putar mencarinya. Periksa jaraknya di bawah ambang tertentu. Untuk belokan halus, interpolasi sudut punya jebakan tersendiri: sudut dari 350 derajat ke 10 derajat seharusnya berbelok sepuluh derajat, bukan tiga ratus empat puluh, jadi normalkan selisihnya ke rentang setengah putaran lebih dulu.
Ikhtisar
PositionComponentadalah induk hampir semua komponen visual. Lima properti kuncinya:position,size,angle,anchor,priority.- Sumbu Y menunjuk ke bawah — untuk bergerak ke atas, kurangi Y.
angledalam radian, bukan derajat. Lingkaran penuh = 2π ≈ 6,283.anchormengubah artipositiondan menentukan pusat rotasi. Untuk objek yang berputar,Anchor.centerhampir selalu benar.prioritylebih tinggi digambar di atas — padananStackdengan angka.Vector2mewakili arah dan besaran sekaligus, bukan hanya koordinat.- Normalisasi mengubah vektor menjadi panjang 1 — ini yang mencegah gerakan diagonal menjadi 41% lebih cepat.
- Periksa
isZero()sebelum normalisasi, atau kamu mendapatNaNdan komponen menghilang tanpa error. Vector2bisa diubah. Pakai.clone()saat berbagi, atau semua komponen akan bergerak bersamaan.(target - position)..normalize()adalah pola untuk mengejar target.with TapCallbacksmemberi komponen deteksi ketukan berdasarkansizedanposition.- Batasi nilai
angleagar tidak membesar tanpa batas. - Komponen anak mengikuti induknya — bergerak, berputar, dan ikut terhapus. Rotasi yang diwariskan kadang tidak diinginkan.
- Gunakan efek (
MoveToEffect,ScaleEffect,SequenceEffect) alih-alih menulis animasi manual diupdate(). HasGameReference<T>memberi akses bertipe ke game induk.- Kumpulkan dulu sebelum menghapus saat mengiterasi komponen.
Berikutnya: Bab 44 — Joystick & Peluru.
Transkrip asli
Disintesis dari 5_flutter_flame/5_case-study-1-flame-engine-essentials.md (14 video: Position Component, "Squares" Code Demo, Architectural UML Diagram, Coding "Squares", Importance of Vectors, Adding Vector Dynamics, Component Composability, Architecting Composability, Coding a Health Bar, dan latihan-latihannya). Lihat PDF Flame Engine.