🎮 Membuat Game Brick Breaker dengan Flutter
Kalau kamu pernah main game Arkanoid atau Brick Breaker, pasti sudah familiar dengan konsep memantulkan bola untuk menghancurkan blok di layar. Nah, kali ini kita akan membuat versi sederhananya menggunakan Flutter.
✨ Fitur Game
Dalam game ini, kamu akan menemukan:
Kontrol Paddle: Gerakkan paddle ke kiri atau kanan dengan drag jari.
Bola Memantul: Bola akan memantul dari dinding, paddle, dan blok.
Blok dengan Nyawa: Beberapa blok perlu dipukul beberapa kali sebelum hancur.
Power-up:
Wide Paddle: Paddle melebar sementara.
Multi-ball: Menambah bola ekstra.
Sistem Skor & High Score
Game Over & Restart
Round Baru jika semua blok hancur.
🛠 Penjelasan Struktur Kode
1. State Management – Game menggunakan `StatefulWidget` untuk mengatur posisi bola, paddle, blok, dan power-up secara real-time.
2. Timer Game Loop – Menggunakan `Timer.periodic` untuk memperbarui posisi objek setiap frame (sekitar 60 FPS).
3. Physics Sederhana – Deteksi tabrakan bola dengan dinding, paddle, dan blok menggunakan koordinat berbasis `Alignment` (-1 hingga 1).
4. Power-up System – Power-up jatuh dari blok yang hancur, lalu aktif saat diambil paddle.
5. UI Rendering – Menggunakan `Stack` dan `Positioned` untuk menggambar objek di layar.
📜 Kode Lengkap Game Brick Breaker di Flutter
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
home: GamePage(),
));
}
class GamePage extends StatefulWidget {
const GamePage({super.key});
@override
State<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
// Koordinat pakai Alignment (-1..1)
// Paddle & bola
double paddleX = 0; // posisi paddle (horisontal)
double paddleWidth = 0.30; // proporsi (0..2) -> 0.30 ≈ 30% dari lebar Alignment
static const double paddleHeightPx = 14;
// Bola: bisa multi-ball
final List<Ball> balls = [];
static const double ballSizePx = 10;
// Blok & power up
final List<Block> blocks = [];
final List<PowerUp> powerUps = [];
static const double blockW = 0.26; // lebar blok dalam ruang Alignment (-1..1)
static const double blockH = 0.10; // tinggi blok dalam ruang Alignment (-1..1)
static const double powerUpFallSpeed = 0.008; // kecepatan jatuh powerup (Alignment unit per frame)
// Game state
bool gameOver = false;
int score = 0;
int highScore = 0;
// Power-up flags + timers
bool widePaddle = false;
bool multiBall = false;
Timer? widePaddleTimer;
Timer? multiBallTimer;
// Loop
Timer? gameTimer;
final rand = Random();
@override
void initState() {
super.initState();
_resetGame();
}
@override
void dispose() {
gameTimer?.cancel();
widePaddleTimer?.cancel();
multiBallTimer?.cancel();
super.dispose();
}
void _resetGame() {
gameTimer?.cancel();
widePaddleTimer?.cancel();
multiBallTimer?.cancel();
setState(() {
score = 0;
gameOver = false;
paddleX = 0;
paddleWidth = 0.30;
widePaddle = false;
multiBall = false;
balls
..clear()
..add(Ball(
x: 0,
y: 0.2, // start agak di tengah
vx: 0.008, // kecepatan dalam unit Alignment per frame (≈60 FPS)
vy: -0.010,
));
blocks
..clear()
..addAll(_generateBlocks());
powerUps.clear();
});
gameTimer = Timer.periodic(const Duration(milliseconds: 16), (_) => _updateGame());
}
// Buat pola blok acak di bagian atas
List<Block> _generateBlocks() {
final List<Block> list = [];
// Grid 5 kolom x 4 baris
const cols = 5;
const rows = 4;
const double startX = -1 + blockW / 2 + 0.02;
const double gapX = (2 - blockW - 0.04) / (cols - 1);
const double startY = -0.95;
const double gapY = 0.12;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (rand.nextBool()) {
final hp = 1 + rand.nextInt(3);
list.add(Block(
x: startX + c * gapX,
y: startY + r * gapY,
hp: hp,
));
}
}
}
if (list.isEmpty) {
list.add(Block(x: 0, y: -0.9, hp: 2));
}
return list;
}
void _startNewRound() {
setState(() {
blocks
..clear()
..addAll(_generateBlocks());
balls
..clear()
..add(Ball(x: 0, y: 0.2, vx: 0.008 * (rand.nextBool() ? 1 : -1), vy: -0.010));
multiBall = false;
widePaddle = false;
paddleWidth = 0.30;
});
}
void _updateGame() {
if (gameOver) return;
setState(() {
// Gerakkan bola
for (int i = 0; i < balls.length; i++) {
final b = balls[i];
b.x += b.vx;
b.y += b.vy;
if (b.x <= -1 && b.vx < 0) b.vx = -b.vx;
if (b.x >= 1 && b.vx > 0) b.vx = -b.vx;
if (b.y <= -1 && b.vy < 0) b.vy = -b.vy;
// Pantul paddle
const double paddleY = 0.95;
final halfPaddle = paddleWidth / 2;
if (b.y >= (paddleY - 0.03) && b.vy > 0) {
if (b.x >= paddleX - halfPaddle && b.x <= paddleX + halfPaddle) {
final hitRatio = (b.x - paddleX) / halfPaddle;
b.vy = -b.vy.abs();
b.vx = (b.vx.sign) * (0.006 + 0.006 * hitRatio.abs()) * (hitRatio >= 0 ? 1 : -1);
}
}
// Kena blok
for (int k = 0; k < blocks.length; k++) {
final blk = blocks[k];
final halfW = blockW / 2;
final halfH = blockH / 2;
final collideX = b.x > (blk.x - halfW) && b.x < (blk.x + halfW);
final collideY = b.y > (blk.y - halfH) && b.y < (blk.y + halfH);
if (collideX && collideY) {
b.vy = -b.vy;
blk.hp -= 1;
if (blk.hp <= 0) {
final removed = blocks.removeAt(k);
score += 10;
if (score > highScore) highScore = score;
_maybeSpawnPowerUp(removed.x, removed.y);
}
break;
}
}
if (b.y > 1.05) {
balls.removeAt(i);
i--;
}
}
if (balls.isEmpty) {
_setGameOver();
return;
}
// Power-up turun
for (final p in powerUps) {
p.y += powerUpFallSpeed;
}
powerUps.removeWhere((p) {
const double paddleY = 0.95;
final halfPaddle = paddleWidth / 2;
if (p.y >= paddleY - 0.02 && p.y <= 1.05) {
if (p.x >= paddleX - halfPaddle && p.x <= paddleX + halfPaddle) {
_activatePowerUp(p.type);
return true;
}
}
if (p.y > 1.1) return true;
return false;
});
if (blocks.isEmpty) {
_startNewRound();
}
});
}
void _setGameOver() {
gameOver = true;
gameTimer?.cancel();
widePaddleTimer?.cancel();
multiBallTimer?.cancel();
}
void _maybeSpawnPowerUp(double x, double y) {
if (rand.nextDouble() < 0.25) {
final type = rand.nextBool() ? PowerUpType.widePaddle : PowerUpType.multiBall;
powerUps.add(PowerUp(x: x, y: y, type: type));
}
}
void _activatePowerUp(PowerUpType type) {
if (type == PowerUpType.widePaddle) {
widePaddle = true;
paddleWidth = 0.50;
widePaddleTimer?.cancel();
widePaddleTimer = Timer(const Duration(seconds: 8), () {
setState(() {
widePaddle = false;
paddleWidth = 0.30;
});
});
} else if (type == PowerUpType.multiBall) {
if (!multiBall) {
multiBall = true;
if (balls.isNotEmpty) {
final b = balls.first;
balls.add(Ball(x: b.x, y: b.y, vx: -b.vx, vy: b.vy));
}
multiBallTimer?.cancel();
multiBallTimer = Timer(const Duration(seconds: 8), () {
setState(() {
multiBall = false;
});
});
}
}
}
void _movePaddle(DragUpdateDetails d, Size size) {
final dx = d.delta.dx / size.width * 2;
setState(() {
paddleX += dx;
final half = paddleWidth / 2;
if (paddleX - half < -1) paddleX = -1 + half;
if (paddleX + half > 1) paddleX = 1 - half;
});
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
double ax(double a) => (a + 1) / 2 * size.width;
double ay(double a) => (a + 1) / 2 * size.height;
return Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onHorizontalDragUpdate: (d) => _movePaddle(d, size),
child: Stack(
children: [
// Bola
for (final b in balls)
Positioned(
left: ax(b.x) - ballSizePx / 2,
top: ay(b.y) - ballSizePx / 2,
child: Container(
width: ballSizePx,
height: ballSizePx,
decoration: const BoxDecoration(
color: Colors.white, shape: BoxShape.circle),
),
),
// Paddle
Positioned(
left: ax(paddleX) - (size.width * (paddleWidth / 2)) / 2,
top: ay(0.95) - paddleHeightPx / 2,
child: Container(
width: size.width * (paddleWidth / 2),
height: paddleHeightPx,
decoration: BoxDecoration(
color: Colors.blueAccent,
borderRadius: BorderRadius.circular(8),
),
),
),
// Blok
for (final blk in blocks)
Positioned(
left: ax(blk.x) - (size.width * (blockW / 2)) / 2,
top: ay(blk.y) - (size.height * (blockH / 2)) / 2,
child: Container(
width: size.width * (blockW / 2),
height: size.height * (blockH / 2),
alignment: Alignment.center,
decoration: BoxDecoration(
color: _blockColor(blk.hp),
borderRadius: BorderRadius.circular(6),
),
child: Text(
'${blk.hp}',
style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12),
),
),
),
// Power-ups
for (final p in powerUps)
Positioned(
left: ax(p.x) - 10,
top: ay(p.y) - 10,
child: Icon(
p.type == PowerUpType.widePaddle ? Icons.crop_16_9 : Icons.adjust,
color: Colors.amberAccent, size: 20,
),
),
// HUD
Positioned(
top: 10, left: 10, right: 10,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_pill('Score', '$score'),
_pill('High', '$highScore'),
if (widePaddle) _pill('Wide', 'ON'),
if (multiBall) _pill('Multi', 'ON'),
],
),
),
// Game Over
if (gameOver)
Positioned.fill(
child: Container(
color: Colors.black54,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('GAME OVER',
style: TextStyle(
color: Colors.redAccent,
fontSize: 28,
fontWeight: FontWeight.w800)),
const SizedBox(height: 12),
Text('Score: $score • High: $highScore',
style: const TextStyle(color: Colors.white70)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _resetGame,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
child: const Text('Restart'),
),
],
),
),
),
),
],
),
),
);
}
Color _blockColor(int hp) {
switch (hp % 5) {
case 0: return const Color(0xFFE57373);
case 1: return const Color(0xFF64B5F6);
case 2: return const Color(0xFF81C784);
case 3: return const Color(0xFFFFB74D);
default: return const Color(0xFFBA68C8);
}
}
Widget _pill(String label, String value) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: Colors.white24),
),
child: Row(
children: [
Text('$label: ', style: const TextStyle(color: Colors.white70, fontSize: 12)),
Text(value, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12)),
],
),
);
}
}
class Ball {
double x, y;
double vx, vy;
Ball({required this.x, required this.y, required this.vx, required this.vy});
}
class Block {
double x, y;
int hp;
Block({required this.x, required this.y, required this.hp});
}
enum PowerUpType { widePaddle, multiBall }
class PowerUp {
double x, y;
PowerUpType type;
PowerUp({required this.x, required this.y, required this.type});
}
🏁 Penutup
Game Project: https://zc2xs06s1c2xt.zapp.page/#/
Game ini bisa kamu kembangkan lagi dengan menambahkan:
* Level dengan pola blok berbeda
* Efek suara
* Animasi saat blok hancur
* Tampilan menu utama
Dengan Flutter, kamu bisa membuat game sederhana dengan performa yang cukup baik tanpa harus pakai game engine berat seperti Unity.

Comments
Post a Comment