Skip to content

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 detik

Perulangan 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 biasaGame
Kapan menggambar ulangSaat setState dipanggil60 kali per detik, selalu
Yang memicu perubahanInteraksi penggunaWaktu yang berjalan
Model mentalReaktifSimulasi berkelanjutan

Memasang

yaml
# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flame: ^1.22.0
bash
flutter pub get

Untuk 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;        // pengisian

Struktur 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_saya

Kalau 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.mp3

Dan 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() — misalnya Paint() 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 dengan dt saat 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 super saat meng-override — melewatkan super.update(dt) membuat komponen anak tidak pernah diperbarui, tanpa error apa pun.
  • Komponen adalah cara memecah game, mirip widget. Setiap komponen punya update() dan render() sendiri.
  • Di dalam render() komponen, koordinat (0, 0) adalah pojok komponen, bukan layar.
  • Flame mencari gambar di assets/images/ dan audio di assets/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 dalam update() atau render() — buat sekali sebagai properti.
  • debugMode menampilkan kotak batas; FpsTextComponent menampilkan 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.

Rangkuman pembelajaran pribadi, disusun ulang dari beberapa kursus Flutter.