Tampilan
Bab 42 — Pengantar Flame & Game Loop
Bagian ini berbeda dari semua yang sebelumnya. Sampai sekarang kamu membangun antarmuka: widget yang menunggu pengguna berinteraksi, lalu bereaksi. Game bekerja terbalik — ia terus berjalan enam puluh kali per detik, apakah pengguna melakukan sesuatu atau tidak.
Bab ini membangun model mental itu, karena hampir semua yang membingungkan tentang pengembangan game berasal dari perbedaan tersebut.
Apa itu Flame
Flame adalah mesin game 2D yang dibangun di atas Flutter. Ia menyediakan hal-hal yang tidak ada di Flutter biasa:
- Game loop — siklus tak berhenti yang menjalankan gamemu.
- Sistem komponen — cara menyusun objek game.
- Deteksi tabrakan — mengetahui kapan dua benda bersentuhan.
- Sprite dan animasi — menggambar gambar dan urutan gerak.
- Audio — efek suara dan musik latar.
- Input — sentuhan, papan ketik, joystick virtual.
- Kamera — mengikuti pemain di dunia yang lebih besar dari layar.
Yang penting dipahami: Flame tidak menggantikan Flutter. Ia hidup di dalamnya. Gamemu adalah sebuah widget, dan kamu tetap memakai widget Flutter biasa untuk menu, HUD, dan dialog.
┌─────────────────────────────────────────────────────┐
│ APLIKASI FLUTTER │
│ │
│ ┌───────────────────────────────────────────┐ │
│ │ GameWidget │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ FlameGame (game loop) │ │ │
│ │ │ │ │ │
│ │ │ Component Component Component │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────┘ │
│ │
│ Widget Flutter biasa: menu, skor, tombol pause │
└─────────────────────────────────────────────────────┘Game loop: jantung mesin game
Ini konsep terpenting di seluruh bagian ini.
Game loop adalah perulangan tak berhingga yang terus-menerus menjalankan tiga langkah sederhana:
┌──────────────────────────────────────┐
│ │
▼ │
┌─────────────┐ │
│ 1. INPUT │ Ambil masukan │
│ │ (sentuhan, joystick, │
│ │ timer, papan ketik) │
└──────┬──────┘ │
▼ │
┌─────────────┐ │
│ 2. UPDATE │ Perbarui keadaan game │
│ │ (posisi, kecepatan, │
│ │ nyawa, skor) │
└──────┬──────┘ │
▼ │
┌─────────────┐ │
│ 3. RENDER │ Gambar keadaan ke layar │
│ │ (dan mainkan audio) │
└──────┬──────┘ │
│ │
└────────────────────────────────────┘
30–60 kali per detikPerulangan ini berjalan terus-menerus, biasanya 60 kali per detik. Bahkan ketika pemain tidak menyentuh apa pun, loop tetap berjalan — memperbarui posisi benda yang bergerak, memeriksa tabrakan, menghitung waktu.
Ini perbedaan mendasar dari aplikasi biasa:
| Aplikasi Flutter biasa | Game | |
|---|---|---|
| Kapan menggambar ulang | Saat setState dipanggil | 60 kali per detik, selalu |
| Yang memicu perubahan | Interaksi pengguna | Waktu yang berjalan |
| Model mental | Reaktif | Simulasi berkelanjutan |
Memasang
yaml
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
flame: ^1.22.0bash
flutter pub getUntuk fitur tambahan, ada paket pendamping:
yaml
flame_audio: ^2.10.6 # efek suara & musik
flame_forge2d: ^0.18.3 # fisika (opsional)Game pertama
dart
// lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'game_saya.dart';
void main() {
runApp(
GameWidget(game: GameSaya()),
);
}dart
// lib/game_saya.dart
import 'dart:ui';
import 'package:flame/game.dart';
class GameSaya extends FlameGame {
@override
Future<void> onLoad() async {
// Dipanggil SEKALI, sebelum loop dimulai.
// Tempat memuat gambar, suara, dan menyiapkan komponen.
}
@override
void update(double dt) {
// Dipanggil setiap frame — sekitar 60 kali per detik.
// dt = waktu sejak frame terakhir, dalam DETIK.
}
@override
void render(Canvas canvas) {
// Dipanggil setiap frame, setelah update.
// Tempat menggambar.
}
}Itu seluruh kerangkanya. Tiga metode yang akan kamu tulis berulang kali.
GameWidget adalah widget biasa
Kamu bisa menaruhnya di mana saja seperti widget lain:
dart
Scaffold(
appBar: AppBar(title: const Text('Game')),
body: Column(
children: [
const Text('Skor: 0'),
Expanded(
child: GameWidget(game: GameSaya()),
),
],
),
)Memahami dt
Parameter dt di update() adalah konsep yang wajib kamu pahami dengan benar, karena mengabaikannya menghasilkan game yang berperilaku berbeda di perangkat berbeda.
dt adalah delta time — waktu yang berlalu sejak frame sebelumnya, dalam satuan detik.
Perangkat cepat (60 FPS) Perangkat lambat (30 FPS)
───────────────────────── ─────────────────────────
dt ≈ 0.0166 detik dt ≈ 0.0333 detik
(1/60) (1/30)Kenapa ini penting:
dart
// ❌ SALAH — kecepatan bergantung pada FPS perangkat
@override
void update(double dt) {
posisiX += 5; // 5 piksel per FRAME
}
// Di 60 FPS: 300 piksel/detik
// Di 30 FPS: 150 piksel/detik — game jadi setengah kecepatan!
// ✅ BENAR — kecepatan konsisten di semua perangkat
@override
void update(double dt) {
posisiX += 300 * dt; // 300 piksel per DETIK
}
// Di 60 FPS: 300 × 0.0166 × 60 = 300 piksel/detik
// Di 30 FPS: 300 × 0.0333 × 30 = 300 piksel/detik ✓Aturan yang tidak boleh dilanggar: selalu kalikan dengan dt setiap kali kamu mengubah sesuatu berdasarkan waktu — posisi, rotasi, penghitung waktu, pengisian energi.
dart
posisi += kecepatan * dt; // gerakan
sudut += kecepatanRotasi * dt; // rotasi
waktuTersisa -= dt; // penghitung mundur
energi += lajuIsi * dt; // pengisianStruktur game minimal yang bergerak
Mari buat sesuatu yang benar-benar bergerak, agar konsepnya terasa nyata.
dart
// lib/game_kotak.dart
import 'dart:ui';
import 'package:flame/game.dart';
class GameKotak extends FlameGame {
// Keadaan game
double _x = 0;
double _y = 100;
double _kecepatanX = 200; // piksel per DETIK
final _cat = Paint()..color = const Color(0xFF42A5F5);
static const _ukuran = 60.0;
@override
void update(double dt) {
super.update(dt);
// Gerakkan
_x += _kecepatanX * dt;
// Pantulkan di tepi layar
if (_x <= 0) {
_x = 0;
_kecepatanX = _kecepatanX.abs();
} else if (_x + _ukuran >= size.x) {
_x = size.x - _ukuran;
_kecepatanX = -_kecepatanX.abs();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRect(
Rect.fromLTWH(_x, _y, _ukuran, _ukuran),
_cat,
);
}
}Perhatikan size — properti bawaan FlameGame yang berisi ukuran area game dalam bentuk Vector2. Kamu memakainya untuk mengetahui di mana tepi layar berada.
Perhatikan juga cara memantulkan: alih-alih _kecepatanX = -_kecepatanX, kode di atas memakai .abs() dengan tanda eksplisit. Ini mencegah bug di mana objek "tersangkut" di tepi karena membalik arah dua kali berturut-turut.
Metode siklus hidup
FlameGame punya beberapa metode yang dipanggil pada momen tertentu.
dart
class GameSaya extends FlameGame {
@override
Future<void> onLoad() async {
// 1. SEKALI, sebelum frame pertama.
// Muat aset, buat komponen awal.
await super.onLoad();
}
@override
void onMount() {
// 2. Saat game dipasang ke widget tree.
super.onMount();
}
@override
void onGameResize(Vector2 ukuranBaru) {
// 3. Saat ukuran berubah (rotasi layar, jendela diubah).
super.onGameResize(ukuranBaru);
}
@override
void update(double dt) {
// 4. Setiap frame.
super.update(dt);
}
@override
void render(Canvas canvas) {
// 5. Setiap frame, setelah update.
super.render(canvas);
}
@override
void onRemove() {
// 6. Saat game dilepas. Bersihkan di sini.
super.onRemove();
}
}Jangan lupa memanggil super
Hampir setiap metode yang kamu override harus memanggil versi induknya. Melewatkan super.update(dt) berarti komponen anak tidak akan pernah diperbarui — bug yang membingungkan karena tidak ada error, benda-benda hanya diam saja.
Kebiasaan yang aman: tulis super.namaMetode(...) sebagai baris pertama setiap kali kamu meng-override.
Komponen: cara menyusun game
Menggambar semuanya di render() milik game bekerja untuk contoh kecil. Untuk game sungguhan, kamu memakai komponen.
Komponen adalah objek yang punya update() dan render() sendiri. Ia mirip widget di Flutter — cara memecah game menjadi bagian yang bisa dikelola.
dart
// lib/komponen/kotak_memantul.dart
import 'dart:ui';
import 'package:flame/components.dart';
class KotakMemantul extends PositionComponent {
KotakMemantul({
required Vector2 posisi,
required this.kecepatan,
required Color warna,
}) : _cat = Paint()..color = warna,
super(position: posisi, size: Vector2.all(60));
final Vector2 kecepatan;
final Paint _cat;
@override
void update(double dt) {
super.update(dt);
position += kecepatan * dt;
// Pantulkan di tepi area game
final batas = (parent as FlameGame).size;
if (position.x <= 0) {
position.x = 0;
kecepatan.x = kecepatan.x.abs();
} else if (position.x + size.x >= batas.x) {
position.x = batas.x - size.x;
kecepatan.x = -kecepatan.x.abs();
}
if (position.y <= 0) {
position.y = 0;
kecepatan.y = kecepatan.y.abs();
} else if (position.y + size.y >= batas.y) {
position.y = batas.y - size.y;
kecepatan.y = -kecepatan.y.abs();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
// Koordinat di sini RELATIF terhadap komponen,
// jadi (0,0) adalah pojok kiri atas komponen ini
canvas.drawRect(size.toRect(), _cat);
}
}Menambahkannya ke game:
dart
// lib/game_kotak.dart
import 'dart:math';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'komponen/kotak_memantul.dart';
class GameKotak extends FlameGame {
final _acak = Random();
@override
Future<void> onLoad() async {
await super.onLoad();
final warna = [
Colors.red,
Colors.green,
Colors.blue,
Colors.orange,
Colors.purple,
];
for (var i = 0; i < 5; i++) {
add(
KotakMemantul(
posisi: Vector2(
_acak.nextDouble() * (size.x - 60),
_acak.nextDouble() * (size.y - 60),
),
kecepatan: Vector2(
(_acak.nextDouble() - 0.5) * 400,
(_acak.nextDouble() - 0.5) * 400,
),
warna: warna[i],
),
);
}
}
}Sekarang game-nya tidak punya update() atau render() sama sekali — setiap kotak mengurus dirinya sendiri.
Koordinat relatif di dalam komponen
Ini sumber kebingungan yang umum. Di dalam render() milik komponen, koordinat (0, 0) adalah pojok kiri atas komponen itu, bukan layar.
Flame sudah menerjemahkan kanvasnya sebelum memanggil render(). Jadi menggambar di (0, 0) selalu menggambar di posisi komponen, di mana pun ia berada.
Membuat proyek Flame
Flame menyediakan templat proyek:
bash
# Pasang perkakas
dart pub global activate flame_cli
# Buat proyek baru
flame create game_sayaKalau kamu memakai proyek Flutter biasa, struktur yang umum dipakai:
lib/
├── main.dart
├── game_saya.dart ← kelas FlameGame utama
├── komponen/
│ ├── pemain.dart
│ ├── musuh.dart
│ ├── peluru.dart
│ └── latar.dart
├── overlay/ ← widget Flutter di atas game
│ ├── menu_utama.dart
│ ├── hud.dart
│ └── layar_kalah.dart
└── util/
└── konstanta.dart
assets/
├── images/
│ ├── pemain.png
│ └── musuh.png
└── audio/
├── tembak.wav
└── musik.mp3Dan pendaftaran asetnya:
yaml
# pubspec.yaml
flutter:
assets:
- assets/images/
- assets/audio/Flame mencari di folder tertentu
Flame secara bawaan mencari gambar di assets/images/ dan audio di assets/audio/. Kalau kamu menaruhnya di folder lain, kamu harus mengonfigurasi prefix-nya.
Memakai konvensi ini menghemat waktu.
Overlay: widget Flutter di atas game
Menu, skor, dan tombol pause lebih baik dibuat dengan widget Flutter biasa daripada digambar manual di kanvas.
dart
// lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'game_saya.dart';
void main() {
runApp(const AplikasiGame());
}
class AplikasiGame extends StatelessWidget {
const AplikasiGame({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: GameWidget<GameSaya>(
game: GameSaya(),
// Widget yang bisa ditampilkan/disembunyikan dari dalam game
overlayBuilderMap: {
'menu': (context, game) => MenuUtama(game: game),
'hud': (context, game) => Hud(game: game),
'kalah': (context, game) => LayarKalah(game: game),
},
// Overlay yang tampil sejak awal
initialActiveOverlays: const ['menu'],
// Tampilan saat aset masih dimuat
loadingBuilder: (context) => const Center(
child: CircularProgressIndicator(),
),
errorBuilder: (context, kesalahan) => Center(
child: Text('Game gagal dimuat: $kesalahan'),
),
),
),
);
}
}Mengendalikannya dari dalam game:
dart
class GameSaya extends FlameGame {
void mulai() {
overlays.remove('menu');
overlays.add('hud');
resumeEngine();
}
void kalah() {
pauseEngine();
overlays.remove('hud');
overlays.add('kalah');
}
void ulangi() {
// Bersihkan semua komponen lalu susun ulang
removeAll(children);
onLoad();
overlays.remove('kalah');
overlays.add('hud');
resumeEngine();
}
}Overlay-nya adalah widget Flutter biasa:
dart
// lib/overlay/menu_utama.dart
import 'package:flutter/material.dart';
import '../game_saya.dart';
class MenuUtama extends StatelessWidget {
const MenuUtama({super.key, required this.game});
final GameSaya game;
@override
Widget build(BuildContext context) {
return ColoredBox(
color: Colors.black.withValues(alpha: 0.7),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'GAME SAYA',
style: TextStyle(
color: Colors.white,
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 32),
FilledButton(
onPressed: game.mulai,
child: const Text('Mulai Main'),
),
],
),
),
);
}
}Ini pendekatan yang membuat pengembangan game di Flutter menyenangkan: logika game di Flame, antarmuka di Flutter. Kamu memakai keterampilan dari Bagian I sampai V untuk bagian antarmukanya.
Mengendalikan mesin
dart
// Menghentikan sementara — update() berhenti dipanggil
pauseEngine();
// Melanjutkan
resumeEngine();
// Memeriksa
if (paused) { }Ini yang kamu pakai untuk menu pause, dan juga saat aplikasi masuk latar belakang:
dart
class GameSaya extends FlameGame with WidgetsBindingObserver {
@override
void lifecycleStateChange(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
pauseEngine();
case AppLifecycleState.resumed:
resumeEngine();
default:
break;
}
}
}Tanpa ini, game terus berjalan di latar belakang — membuang baterai, dan pemain kembali menemukan karakternya sudah mati.
Latar belakang berwarna
dart
class GameSaya extends FlameGame {
@override
Color backgroundColor() => const Color(0xFF0D0D2B);
}Debugging
Flame menyediakan mode debug yang menampilkan kotak batas dan posisi setiap komponen:
dart
class GameSaya extends FlameGame {
@override
bool get debugMode => true; // matikan sebelum rilis
}Untuk komponen tertentu saja:
dart
class Pemain extends PositionComponent {
@override
bool get debugMode => true;
}Melihat FPS:
dart
import 'package:flame/components.dart';
@override
Future<void> onLoad() async {
await super.onLoad();
add(
FpsTextComponent(
position: Vector2(10, 10),
),
);
}Kalau FPS turun jauh di bawah 60, biasanya penyebabnya salah satu dari:
- Terlalu banyak komponen aktif sekaligus.
- Membuat objek baru di dalam
update()— misalnyaPaint()setiap frame. - Gambar beresolusi jauh lebih besar daripada ukuran tampilnya.
- Perhitungan berat yang bisa dipindahkan ke
onLoad().
Jangan membuat objek di dalam update atau render
Ini kesalahan performa paling umum di Flame:
dart
// ❌ Membuat objek Paint baru 60 kali per detik
@override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), Paint()..color = Colors.red);
}
// ✅ Buat sekali, pakai berulang
final _cat = Paint()..color = Colors.red;
@override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), _cat);
}Hal yang sama berlaku untuk Vector2, Rect, dan objek lain yang dibuat berulang.
Peta perjalanan Bagian VI
Setiap bab membangun di atas yang sebelumnya, dan Bab 50 menggabungkan semuanya menjadi game yang utuh.
Ikhtisar
- Game loop adalah perulangan tak berhingga: ambil input → perbarui keadaan → gambar. Berjalan 30–60 kali per detik, terlepas dari apakah pengguna melakukan sesuatu.
- Ini berbeda mendasar dari aplikasi Flutter biasa yang hanya menggambar ulang saat
setState. dt(delta time) adalah waktu sejak frame terakhir dalam detik. Selalu kalikan dengandtsaat mengubah sesuatu berdasarkan waktu, atau game berjalan dengan kecepatan berbeda di perangkat berbeda.- Tiga metode inti:
onLoad()(sekali, memuat aset),update(dt)(setiap frame, logika),render(canvas)(setiap frame, menggambar). - Selalu panggil
supersaat meng-override — melewatkansuper.update(dt)membuat komponen anak tidak pernah diperbarui, tanpa error apa pun. - Komponen adalah cara memecah game, mirip widget. Setiap komponen punya
update()danrender()sendiri. - Di dalam
render()komponen, koordinat(0, 0)adalah pojok komponen, bukan layar. - Flame mencari gambar di
assets/images/dan audio diassets/audio/secara bawaan. - Overlay memungkinkan memakai widget Flutter biasa untuk menu, HUD, dan dialog — pakai keterampilan dari bagian sebelumnya.
pauseEngine()/resumeEngine()untuk pause; hubungkan ke siklus hidup aplikasi agar game tidak berjalan di latar belakang.- Jangan membuat objek (
Paint,Vector2,Rect) di dalamupdate()ataurender()— buat sekali sebagai properti. debugModemenampilkan kotak batas;FpsTextComponentmenampilkan FPS.
Berikutnya: Bab 43 — Component & Vector.
Transkrip asli
Disintesis dari 5_flutter_flame/1_introduction.md (What is the Flame Engine, What The Course Covers, Concept vs. Code), 2_setup.md, dan 3_2d-game-basics-with-flame-engine.md (7 video: The Game Loop, Game Instantiation, Game Execution, Generating Our First Flame Project, Running Our First Minimalist Game-Loop Code, Adding Explicit Game-Loop Code Hook Methods, What are Game Components). Lihat PDF Flame Engine.