Tampilan
Bab 44 — Joystick & Peluru
Sebuah game butuh pemain yang bisa dikendalikan. Bab ini membahas joystick virtual sebagai kendali gerak, lalu menembakkan peluru — yang ternyata melibatkan satu masalah trigonometri yang menjebak hampir semua orang pada percobaan pertama.
Anatomi joystick Flame
Flame menyediakan JoystickComponent siap pakai. Ia terdiri dari dua bagian:
┌─────────────────────────────┐
│ │
│ ╭───────────────╮ │
│ │ BACKGROUND │ │ ← lingkaran besar, diam
│ │ │ │
│ │ ╭───╮ │ │
│ │ │KNB│ ● │ │ ← knob, digeser jari
│ │ ╰───╯ │ │
│ │ │ │
│ ╰───────────────╯ │
│ │
└─────────────────────────────┘
delta = pergeseran knob dari pusat (Vector2)
direction = arah dalam 8 penjuru (enum)
intensity = 0.0 (di tengah) sampai 1.0 (di tepi)Tiga properti itu yang akan kamu baca setiap frame.
Membuat joystick
dart
// lib/game_joystick.dart
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/palette.dart';
import 'package:flutter/material.dart';
class GameJoystick extends FlameGame {
late final JoystickComponent joystick;
late final Pemain pemain;
@override
Color backgroundColor() => const Color(0xFF0F1020);
@override
Future<void> onLoad() async {
await super.onLoad();
final catKnob = BasicPalette.white.withAlpha(180).paint();
final catLatar = BasicPalette.white.withAlpha(60).paint();
joystick = JoystickComponent(
knob: CircleComponent(radius: 22, paint: catKnob),
background: CircleComponent(radius: 55, paint: catLatar),
margin: const EdgeInsets.only(left: 40, bottom: 40),
);
pemain = Pemain(joystick: joystick)
..position = size / 2;
// Joystick ditambahkan ke game agar tetap di posisi layar
add(pemain);
add(joystick);
}
}Joystick tidak boleh ikut kamera
Kalau gamemu memakai kamera yang mengikuti pemain (dibahas di Bab 48), joystick harus ditambahkan ke viewport, bukan ke dunia — kalau tidak, ia akan bergeser saat kamera bergerak.
dart
camera.viewport.add(joystick);Menggerakkan pemain
dart
// lib/komponen/pemain.dart
import 'dart:math' as math;
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Pemain extends PositionComponent {
Pemain({required this.joystick})
: super(size: Vector2.all(48), anchor: Anchor.center);
final JoystickComponent joystick;
static const double _kecepatanMaks = 260;
final _cat = Paint()..color = Colors.cyanAccent;
final _catMoncong = Paint()..color = Colors.white;
@override
void update(double dt) {
super.update(dt);
// Joystick di tengah = tidak bergerak
if (joystick.direction == JoystickDirection.idle) return;
// intensity membuat gerakan proporsional:
// geser sedikit = jalan pelan, geser penuh = lari
position += joystick.relativeDelta * _kecepatanMaks * dt;
// Hadapkan pemain ke arah gerak
angle = joystick.delta.screenAngle();
_jagaDiDalamLayar();
}
void _jagaDiDalamLayar() {
final batas = findGame()!.size;
final setengah = size.x / 2;
position.x = position.x.clamp(setengah, batas.x - setengah);
position.y = position.y.clamp(setengah, batas.y - setengah);
}
@override
void render(Canvas canvas) {
super.render(canvas);
// Badan
canvas.drawCircle(
Offset(size.x / 2, size.y / 2),
size.x / 2,
_cat,
);
// Moncong — menunjukkan arah hadap.
// Karena screenAngle() menganggap 0 = ke ATAS,
// moncongnya digambar di bagian atas komponen.
canvas.drawRect(
Rect.fromLTWH(size.x / 2 - 3, 0, 6, size.y / 2),
_catMoncong,
);
}
}Dua detail yang penting.
relativeDelta versus delta. relativeDelta sudah dinormalisasi ke rentang 0–1, sehingga langsung bisa dikalikan dengan kecepatan. delta adalah pergeseran mentah dalam piksel, yang bergantung pada ukuran joystick.
clamp untuk menjaga di layar. Ini cara paling sederhana; alternatifnya adalah membiarkan pemain keluar dan kamera mengikutinya.
Masalah sudut
Sekarang bagian yang menjebak.
Vector2 punya dua metode sudut yang berbeda, dan memilih yang salah menghasilkan objek yang menghadap ke arah yang keliru — 90 derajat meleset.
dart
final v = Vector2(1, 0); // menunjuk ke kanan
v.angleTo(Vector2(1, 0)); // sudut relatif terhadap vektor lain
v.screenAngle(); // sudut dengan 0 = ke ATAS, searah jarum jam MATEMATIKA STANDAR screenAngle() FLAME
(atan2)
0
π/2 │
│ │
π ────┼──── 0 3π/2 ───┼─── π/2
│ │
3π/2 │
π
0 = ke KANAN 0 = ke ATAS
berlawanan jarum jam searah jarum jamKarena screenAngle() menganggap 0 radian = menghadap ke atas, sprite dan gambar yang kamu buat harus menghadap ke atas dalam keadaan tidak berputar.
Sprite harus menghadap ke atas
Ini penyebab bug "kapalku menghadap ke samping" yang hampir semua orang alami.
Kalau sprite kapalmu digambar menghadap ke kanan (kebiasaan umum dalam desain), maka angle = screenAngle() akan membuatnya menghadap 90 derajat meleset.
Dua solusinya:
dart
// 1. Gambar ulang sprite agar menghadap ke atas — paling bersih
// 2. Atau kompensasi di kode
angle = joystick.delta.screenAngle() + math.pi / 2;Pilihan pertama lebih baik, karena kompensasi manual akan terlupa di tempat lain.
Menembakkan peluru
Sekarang bagian yang menggabungkan semuanya.
Peluru harus keluar dari moncong pemain dan bergerak ke arah yang dihadapi pemain. Ini butuh mengubah sudut kembali menjadi vektor arah.
Dari sudut ke vektor
dart
import 'dart:math' as math;
/// Mengubah sudut layar Flame (0 = atas, searah jarum jam)
/// menjadi vektor arah satuan.
Vector2 arahDariSudut(double sudutLayar) {
return Vector2(
math.sin(sudutLayar), // sin untuk X
-math.cos(sudutLayar), // -cos untuk Y (karena Y ke bawah)
);
}Perhatikan bahwa sin dan cos tertukar dari rumus matematika biasa, dan cos diberi tanda negatif. Ini konsekuensi dari dua hal: sudut nol menunjuk ke atas, dan sumbu Y menunjuk ke bawah.
Mari periksa dengan beberapa nilai:
sudut = 0 (ke atas)
x = sin(0) = 0
y = -cos(0) = -1
→ Vector2(0, -1) = ke atas ✓
sudut = π/2 (ke kanan)
x = sin(π/2) = 1
y = -cos(π/2) = 0
→ Vector2(1, 0) = ke kanan ✓
sudut = π (ke bawah)
x = sin(π) = 0
y = -cos(π) = 1
→ Vector2(0, 1) = ke bawah ✓Komponen peluru
dart
// lib/komponen/peluru.dart
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Peluru extends PositionComponent {
Peluru({
required Vector2 posisi,
required this.arah,
this.kecepatan = 520,
}) : super(
position: posisi,
size: Vector2(6, 14),
anchor: Anchor.center,
);
final Vector2 arah; // sudah dinormalisasi
final double kecepatan;
/// Peluru menghilang setelah waktu ini, agar tidak menumpuk
/// selamanya kalau tidak mengenai apa pun.
double _sisaUmur = 2.0;
final _cat = Paint()..color = Colors.amberAccent;
@override
Future<void> onLoad() async {
await super.onLoad();
// Hadapkan peluru ke arah geraknya
angle = arah.screenAngle();
}
@override
void update(double dt) {
super.update(dt);
position += arah * kecepatan * dt;
_sisaUmur -= dt;
if (_sisaUmur <= 0) {
removeFromParent();
return;
}
// Hapus kalau sudah keluar layar
final batas = findGame()!.size;
if (position.x < -20 ||
position.y < -20 ||
position.x > batas.x + 20 ||
position.y > batas.y + 20) {
removeFromParent();
}
}
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRRect(
RRect.fromRectAndRadius(size.toRect(), const Radius.circular(3)),
_cat,
);
}
}Peluru wajib punya batas umur
Tanpa _sisaUmur atau pemeriksaan keluar layar, setiap peluru yang ditembakkan akan hidup selamanya di memori. Setelah beberapa menit bermain, ribuan komponen akan berjalan di latar belakang dan FPS akan anjlok.
Ini kebocoran yang sangat mudah terjadi dan sangat mudah dicegah.
Menembakkan dari pemain
dart
// lib/komponen/pemain.dart — tambahan
import 'peluru.dart';
class Pemain extends PositionComponent {
// ... kode sebelumnya
/// Jeda antar tembakan, agar tidak bisa menembak setiap frame.
static const double _jedaTembak = 0.18;
double _sisaJeda = 0;
@override
void update(double dt) {
super.update(dt);
// Kurangi penghitung jeda
if (_sisaJeda > 0) _sisaJeda -= dt;
// ... logika gerak
}
void tembak() {
if (_sisaJeda > 0) return;
_sisaJeda = _jedaTembak;
final arah = _arahHadap();
// Keluarkan peluru dari MONCONG, bukan dari pusat pemain —
// kalau tidak, peluru langsung mengenai pemain sendiri
final posisiMoncong = position + arah * (size.x / 2 + 8);
findGame()!.add(
Peluru(posisi: posisiMoncong, arah: arah),
);
}
Vector2 _arahHadap() {
return Vector2(
math.sin(angle),
-math.cos(angle),
);
}
}Dua detail yang menyelamatkan banyak waktu debugging:
Jeda tembak. Tanpa ini, menahan tombol menembakkan 60 peluru per detik.
Posisi moncong. Peluru yang lahir di pusat pemain akan langsung bertabrakan dengan pemain itu sendiri ketika deteksi tabrakan ditambahkan di Bab 46.
Tombol tembak
dart
// lib/game_joystick.dart — tambahan
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
@override
Future<void> onLoad() async {
await super.onLoad();
// ... joystick dan pemain
add(
HudButtonComponent(
button: CircleComponent(
radius: 34,
paint: Paint()..color = Colors.redAccent.withValues(alpha: 0.6),
),
buttonDown: CircleComponent(
radius: 34,
paint: Paint()..color = Colors.redAccent,
),
margin: const EdgeInsets.only(right: 40, bottom: 50),
onPressed: pemain.tembak,
),
);
}HudButtonComponent menempatkan dirinya relatif terhadap tepi layar lewat margin, sehingga posisinya benar di berbagai ukuran layar.
Untuk menembak terus-menerus selama tombol ditahan:
dart
class TombolTembak extends HudButtonComponent {
TombolTembak({
required super.button,
required super.margin,
required this.pemain,
});
final Pemain pemain;
bool _ditahan = false;
@override
void onTapDown(TapDownEvent event) {
super.onTapDown(event);
_ditahan = true;
}
@override
void onTapUp(TapUpEvent event) {
super.onTapUp(event);
_ditahan = false;
}
@override
void onTapCancel(TapCancelEvent event) {
super.onTapCancel(event);
_ditahan = false;
}
@override
void update(double dt) {
super.update(dt);
// Jeda tembak di dalam Pemain yang mengatur lajunya
if (_ditahan) pemain.tembak();
}
}Menembak ke arah sentuhan
Alternatif yang sering lebih nyaman di ponsel: pemain menembak ke titik yang disentuh, bukan ke arah hadapnya.
dart
// lib/game_joystick.dart
import 'package:flame/events.dart';
class GameJoystick extends FlameGame with TapCallbacks {
// ...
@override
void onTapDown(TapDownEvent event) {
super.onTapDown(event);
final titik = event.localPosition;
final arah = (titik - pemain.position)..normalize();
add(
Peluru(
posisi: pemain.position + arah * 30,
arah: arah,
),
);
}
}Perhatikan bahwa di sini kita tidak perlu trigonometri sama sekali — arahnya langsung didapat dari selisih dua posisi. Ini sering lebih sederhana daripada bekerja dengan sudut.
Kapan memakai vektor, kapan memakai sudut
- Punya dua titik dan butuh arah? Pakai
(target - asal)..normalize(). Tidak butuh trigonometri. - Punya sudut dan butuh arah? Pakai
sin/cosseperti di atas. - Punya arah dan butuh sudut untuk rotasi? Pakai
.screenAngle().
Bekerja dengan vektor hampir selalu lebih sederhana dan lebih sedikit bug daripada bekerja dengan sudut.
Kendali papan ketik
Untuk desktop dan web:
dart
import 'package:flame/events.dart';
import 'package:flutter/services.dart';
class GameJoystick extends FlameGame with KeyboardEvents {
final Set<LogicalKeyboardKey> _tombolDitekan = {};
@override
KeyEventResult onKeyEvent(
KeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
_tombolDitekan
..clear()
..addAll(keysPressed);
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.space) {
pemain.tembak();
}
return KeyEventResult.handled;
}
Vector2 get arahPapanKetik {
final arah = Vector2.zero();
if (_tombolDitekan.contains(LogicalKeyboardKey.keyW) ||
_tombolDitekan.contains(LogicalKeyboardKey.arrowUp)) {
arah.y -= 1;
}
if (_tombolDitekan.contains(LogicalKeyboardKey.keyS) ||
_tombolDitekan.contains(LogicalKeyboardKey.arrowDown)) {
arah.y += 1;
}
if (_tombolDitekan.contains(LogicalKeyboardKey.keyA) ||
_tombolDitekan.contains(LogicalKeyboardKey.arrowLeft)) {
arah.x -= 1;
}
if (_tombolDitekan.contains(LogicalKeyboardKey.keyD) ||
_tombolDitekan.contains(LogicalKeyboardKey.arrowRight)) {
arah.x += 1;
}
// Normalisasi agar diagonal tidak lebih cepat —
// masalah yang dibahas di Bab 43
if (!arah.isZero()) arah.normalize();
return arah;
}
}Menggabungkan keduanya di pemain:
dart
@override
void update(double dt) {
super.update(dt);
final game = findGame()! as GameJoystick;
// Prioritaskan joystick kalau sedang dipakai
var arah = joystick.direction != JoystickDirection.idle
? joystick.relativeDelta.clone()
: game.arahPapanKetik;
if (!arah.isZero()) {
position += arah * _kecepatanMaks * dt;
angle = arah.screenAngle();
}
_jagaDiDalamLayar();
}Sekarang game-nya bisa dimainkan dengan sentuhan di ponsel dan papan ketik di desktop — tanpa kode terpisah.
Alur lengkap
Kesalahan yang sering terjadi
Sprite menghadap ke arah yang salah. screenAngle() menganggap 0 = ke atas. Gambar spritemu menghadap ke atas.
Gerakan diagonal lebih cepat. Normalisasi vektor arah.
Peluru mengenai penembaknya sendiri. Keluarkan dari moncong, bukan dari pusat.
Ribuan peluru menumpuk di memori. Beri batas umur dan hapus saat keluar layar.
Menembak 60 kali per detik. Tambahkan jeda tembak.
Joystick bergeser saat kamera bergerak. Tambahkan ke camera.viewport, bukan ke dunia.
Membuat Vector2 baru setiap frame.
dart
// ❌ Objek baru 60 kali per detik
position += Vector2(kecepatan * dt, 0);
// ✅ Ubah yang sudah ada
position.x += kecepatan * dt;Latihan Mandiri
Kerjakan salah satu, beberapa, atau semuanya secara berurutan untuk melatih pemahamanmu sampai benar-benar lekat.
Variasi 1: Tembak Otomatis — ⭐⭐ · 45–60 menit
Tantangan: Buat pemain yang digerakkan joystick dan menembak otomatis ke arah musuh terdekat. Peluru harus keluar dari moncong, punya batas umur, dan menghilang saat keluar layar. Tambahkan jeda tembak yang bisa kamu atur.
Bayangan tampilan:
text
┌─────────────────────────────────┐
│ 👾 (musuh terdekat) │
│ │
│ ↗ peluru │
│ 🙂 (pemain) │
│ │
│ ⊙ (joystick di layar) │
└─────────────────────────────────┘Peluru keluar dari moncong (ujung senjata pemain), bukan dari pusat tubuhnya — sehingga tidak langsung bertabrakan dengan penembaknya sendiri.
Kriteria selesai:
- Gerakan memakai
relativeDelta, sehingga menggeser joystick setengah berarti bergerak setengah kecepatan. - Arah tembak dihitung dari selisih dua posisi, bukan dari sudut — dan kamu bisa menjelaskan kenapa itu lebih sederhana.
- Peluru keluar dari moncong, bukan dari pusat pemain, sehingga ia tidak langsung bertabrakan dengan penembaknya.
- Setiap peluru punya batas umur dan dihapus saat keluar layar.
- Menahan tombol tembak tidak menghasilkan enam puluh peluru per detik.
Petunjuk: Kriteria kedua adalah pelajaran terbesar bab ini. Kalau kamu punya posisi pemain dan posisi musuh, (musuh.position - pemain.position)..normalize() memberi arah tembak langsung, tanpa satu pun fungsi trigonometri. Jalur lewat sudut bekerja juga, tetapi kamu harus ingat bahwa screenAngle() menganggap nol radian menghadap ke atas dan searah jarum jam, yang berbeda dari konvensi matematika — dan konversi baliknya adalah Vector2(sin(sudut), -cos(sudut)) dengan sin dan cos yang tertukar serta cos yang negatif. Kalau kamu bisa menghindari semua itu, hindari. Untuk jeda tembak, satu penghitung yang dikurangi dt setiap frame sudah cukup — tembak hanya kalau penghitungnya sudah nol atau kurang.
Variasi 2: Dua Cara Mengendalikan — ⭐⭐⭐ · 45–60 menit
Tantangan: Dukung joystick di layar dan papan ketik sekaligus dalam satu game, sehingga game yang sama enak dimainkan di ponsel maupun desktop. Tambahkan juga tombol aksi di layar yang tidak menghalangi pandangan.
Bayangan tampilan:
text
Di ponsel: Di desktop:
┌─────────────────────────┐ ┌─────────────────────────┐
│ 🙂 │ │ 🙂 │
│ │ │ │
│ ⊙ (joystick) [ Aksi ] │ │ (kendali di layar │
└─────────────────────────┘ │ disembunyikan, │
│ pakai WASD/panah) │
└─────────────────────────┘Menekan atas+kanan bersamaan di papan ketik → kecepatan gerak tetap sama dengan gerak lurus, tidak 41% lebih cepat karena diagonal.
Kriteria selesai:
- Kedua kendali bekerja bersamaan tanpa saling mengganggu — memakai papan ketik saat joystick di posisi netral tidak menghasilkan gerakan yang bertabrakan.
- Gerakan diagonal papan ketik tidak lebih cepat daripada gerakan lurus.
- Kendali di layar disembunyikan otomatis di platform desktop.
- Joystick dan tombol berada di
camera.viewport, sehingga tidak ikut bergerak bersama kamera.
Petunjuk: Gerakan diagonal yang lebih cepat adalah bug klasik: menekan atas dan kanan bersamaan menghasilkan vektor dengan panjang akar dua, sekitar empat puluh satu persen lebih cepat. Normalisasi vektor arah sebelum mengalikannya dengan kecepatan menyelesaikannya. Joystick sudah menghasilkan vektor ternormalisasi, itulah kenapa masalah ini hanya muncul di jalur papan ketik.
Variasi 3: Pola Tembakan — ⭐⭐⭐ · 45–60 menit
Tantangan: Bangun tiga pola tembakan yang bisa diganti saat bermain: satu peluru lurus, tiga peluru menyebar dengan sudut, dan tembakan melingkar ke segala arah. Tambahkan senjata yang menembakkan peluru memantul dari tepi layar.
Bayangan tampilan (tiga pola tembakan):
text
Lurus Menyebar (3 sudut) Melingkar
↑ ↖ ↑ ↗ ↖ ↑ ↗
🙂 🙂 ← 🙂 →
↙ ↓ ↘Peluru yang memantul dari tepi layar:
text
┌─────────────────────────────────┐
│ ↘ │
│ ↘ │
│ ●┄┄┄▶ (arah berbalik setelah│
│ memantul di tepi) │
└─────────────────────────────────┘Kriteria selesai:
- Ketiga pola berasal dari satu abstraksi senjata, dan menambah pola keempat tidak menyentuh kode pemain.
- Tembakan menyebar memakai sudut yang merata, bukan acak.
- Peluru memantul mengubah arah dengan benar di keempat tepi, termasuk di sudut.
- Jumlah peluru aktif dibatasi, dan game tetap mulus saat pola melingkar ditembakkan berulang.
Petunjuk: Abstraksi senjata yang bekerja baik biasanya berbentuk satu objek dengan metode "tembak" yang menerima posisi dan arah dasar, lalu menghasilkan sejumlah peluru sendiri. Pemain hanya memanggil metode itu tanpa tahu polanya. Untuk pantulan, membalik komponen kecepatan pada sumbu yang tertabrak sudah cukup — dan menanganinya per sumbu secara terpisah otomatis membuat kasus sudut bekerja benar.
Variasi 4: Kolam Peluru — ⭐⭐⭐⭐ · 60–90 menit
Tantangan: Ubah sistem pelurumu menjadi kolam objek: alih-alih membuat dan menghapus komponen terus-menerus, peluru yang sudah tidak dipakai dinonaktifkan dan digunakan ulang. Ukur bedanya pada tembakan yang sangat padat.
Bayangan perbandingan:
text
Tanpa kolam objek Dengan kolam objek
Komponen dibuat: ~3.200/menit Komponen dibuat: 30 (dipakai ulang)
FPS saat tembakan padat: ~34 FPS saat tembakan padat: ~60Kriteria selesai:
- Jumlah komponen yang dibuat sepanjang satu menit permainan tercatat, untuk versi biasa dan versi berkolam.
- Peluru yang dipakai ulang benar-benar diatur ulang seluruh keadaannya — tidak ada sisa dari pemakaian sebelumnya.
- Kolam membesar sendiri kalau kebutuhannya melebihi kapasitas awal.
- Kamu mengukur frame rate di mode profile dan bisa menyebut selisihnya.
Petunjuk: Kolam objek adalah optimasi yang hanya sepadan kalau kamu benar-benar membuat dan membuang objek dalam jumlah besar per detik — untuk game dengan sepuluh peluru di layar, ia hanya menambah kerumitan. Bug yang paling sering muncul adalah keadaan sisa: peluru yang dipakai ulang masih membawa umur, kecepatan, atau penanda tabrakan dari pemakaian sebelumnya. Tulis satu metode "atur ulang" dan panggil ia di satu tempat saja.
Ikhtisar
JoystickComponentpunyadelta(pergeseran mentah),relativeDelta(dinormalisasi 0–1, langsung bisa dikalikan kecepatan),direction(8 penjuru), danintensity.- Tambahkan joystick ke
camera.viewportkalau gamemu memakai kamera yang bergerak. screenAngle()menganggap 0 radian = menghadap ke ATAS, searah jarum jam — berbeda dari konvensi matematika.- Karena itu, gambar spritemu menghadap ke atas, atau kompensasi dengan
+ π/2. - Mengubah sudut menjadi arah:
Vector2(sin(sudut), -cos(sudut))— sin dan cos tertukar, dan cos negatif karena Y menunjuk ke bawah. - Bekerja dengan vektor lebih sederhana daripada sudut. Kalau kamu punya dua titik,
(target - asal)..normalize()menghindari trigonometri sepenuhnya. - Peluru wajib punya batas umur dan dihapus saat keluar layar, atau memori terisi ribuan komponen.
- Beri jeda tembak, atau menahan tombol menembakkan 60 peluru per detik.
- Keluarkan peluru dari moncong, bukan dari pusat pemain — kalau tidak, ia langsung bertabrakan dengan penembaknya.
- Normalisasi vektor arah agar gerakan diagonal tidak 41% lebih cepat.
HudButtonComponentmenempatkan diri relatif terhadap tepi layar lewatmargin.- Mixin
KeyboardEventsmenambahkan kendali papan ketik untuk desktop dan web tanpa kode terpisah.
Berikutnya: Bab 45 — Audio dalam Game.
Transkrip asli
Disintesis dari 5_flutter_flame/6_case-study-2-joystick-component.md (8 video: The anatomy of the Flame Joystick Component, Joystick Architectural UML Diagram, Coding the Joystick Experience, Shooting Bullets — The Issue of Angle of Rotation, Shooting Bullets UML Diagram, Coding the Solution for correct Bullets angle, dan latihan-latihannya). Lihat PDF Flame Engine.