🎮 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

Popular posts from this blog

📱 Group Study Online – Belajar Bareng Teman Meski Jarak Jauh

Menguasai Loop do-while di Dart: Eksekusi Dulu, Cek Kondisi Kemudian!