Membangun game dengan Flutter dan Flame

1. Pengantar

Pelajari cara membangun game platformer dengan Flutter dan Flame. Dalam game Doodle Dash, yang terinspirasi dari Doodle Jump, Anda dapat bermain sebagai Dash (maskot Flutter), atau sahabatnya Sparky (maskot Firebase), dan capai level setinggi mungkin dengan melompat di atas platform.

Yang akan Anda pelajari

  • Cara membangun game lintas platform di Flutter.
  • Cara membuat komponen game yang dapat digunakan kembali yang dapat melakukan rendering dan update sebagai bagian dari game loop Flame.
  • Cara mengontrol dan menganimasikan gerakan karakter Anda (yang disebut sebagai sprite) melalui game physics.
  • Cara menambahkan dan mengelola deteksi tabrakan.
  • Cara menambahkan keyboard dan input sentuh sebagai kontrol game.

Prasyarat

Codelab ini mengasumsikan bahwa Anda memiliki pengalaman menggunakan Flutter. Jika belum berpengalaman menggunakan Flutter, Anda dapat mempelajari dasar-dasarnya menggunakan codelab Aplikasi Flutter Pertama Anda.

Yang akan Anda bangun

Codelab ini memandu Anda membangun game bernama Doodle Dash: game platformer yang menampilkan Dash, maskot Flutter, atau Sparky, maskot Firebase (codelab ini merujuk ke Dash, tapi langkah-langkahnya berlaku juga untuk Sparky). Game Anda akan memiliki fitur berikut:

  • Sprite yang dapat bergerak secara horizontal dan vertikal
  • Platform yang dibuat secara acak
  • Efek gravitasi yang menarik sprite Anda ke bawah
  • Menu game
  • Pengontrol dalam game seperti jeda dan replay
  • Kemampuan mempertahankan skor

Gameplay

Doodle Dash dimainkan dengan cara menggerakkan Dash ke kiri dan ke kanan, melompat di atas platform, dan menggunakan kekuatan tambahan untuk meningkatkan kemampuannya di seluruh game. Anda dapat memulai game dengan memilih level kesulitan awal (1 hingga 5), dan mengklik Mulai.

d1e75aa0e05c526.gif

Level

Ada 5 level dalam game. Tiap level (setelah level 1) membuka fitur baru.

  • Level 1 (default): Level ini memunculkan platform NormalPlatform dan SpringBoard. Saat dibuat, setiap platform memiliki peluang 20% untuk menjadi platform yang bergerak.
  • Level 2 (skor >= 20): Menambahkan BrokenPlatform yang hanya bisa dilompati sekali.
  • Level 3 (skor >= 40): Membuka kekuatan tambahan NooglerHat. Platform khusus ini berlangsung selama 5 detik dan meningkatkan kemampuan melompat Dash sebesar 2,5x dari kecepatan normalnya. Dia juga memakai topi noogler keren selama 5 detik tersebut.
  • Level 4 (skor >=80): Membuka kekuatan tambahan Rocket. Platform khusus ini, yang ditampilkan dengan roket, membuat Dash tak terkalahkan. Platform ini juga meningkatkan kemampuan melompat Dash sebesar 3,5x dari kecepatan normalnya.
  • Level 5 (skor >= 100): Membuka platform Enemy. Jika Dash bertabrakan dengan musuh, game otomatis berakhir.

Jenis platform berdasarkan level

Level 1 (default)

NormalPlatform

SpringBoard

Level 2 (skor >= 20)

Level 3 (skor >= 40)

Level 4 (skor >= 80)

Level 5 (skor >= 100)

BrokenPlatform

NooglerHat

Rocket

Enemy

Kalah dalam game

Ada dua keadaan yang menyebabkan kekalahan dalam game:

  • Dash jatuh ke bagian bawah layar.
  • Dash bertabrakan dengan musuh (musuh muncul di level 5).

Kekuatan tambahan

Kekuatan tambahan meningkatkan kemampuan bermain karakter seperti meningkatkan kecepatan lompatannya, atau membuatnya menjadi "tak terkalahkan" terhadap musuh, atau keduanya. Doodle Dash memiliki dua opsi kekuatan tambahan. Hanya satu kekuatan tambahan yang aktif dalam satu waktu.

  • Kekuatan tambahan topi noogler meningkatkan kemampuan lompatan Dash sebesar 2,5x dari ketinggian lompatan normalnya. Selain itu, dia memakai topi noogler selama peningkatan kekuatan.
  • Kekuatan tambahan roket membuat Dash tak terkalahkan terhadap platform musuh (bertabrakan dengan musuh tidak berpengaruh) dan meningkatkan kemampuan melompatnya sebesar 3,5x dari ketinggian lompatan normalnya. Dia terbang dengan roket sampai gravitasi mengurangi kecepatannya dan dia mendarat di platform.

2. Mendapatkan kode awal codelab

a3c16fc17be25f6c.pngDownload project versi awal Anda dari GitHub:

  1. Dari command line, clone repositori GitHub ke direktori flutter-codelabs:
git clone https://github.com/flutter/codelabs.git flutter-codelabs

Kode untuk codelab ini ada di direktori flutter-codelabs/flame-building-doodle-dash. Direktori tersebut berisi kode project yang sudah selesai untuk setiap langkah di codelab.

a3c16fc17be25f6c.pngMengimpor aplikasi awal

  • Impor direktori flutter-codelabs/flame-building-doodle-dash/step_02 ke IDE pilihan Anda.

a3c16fc17be25f6c.pngMenginstal paket

  • Semua paket yang dibutuhkan, seperti Flame, telah ditambahkan ke file pubspec.yaml project. Jika IDE Anda tidak menginstal dependensi secara otomatis, buka terminal command line dan dari root project Flutter, jalankan perintah berikut untuk mengambil dependensi project:
flutter pub get

Menyiapkan lingkungan pengembangan Flutter Anda

Untuk menyelesaikan codelab ini, Anda memerlukan hal berikut:

3. Menerapkan kode

Selanjutnya, lakukan penerapan kode.

Tinjau file lib/game/doodle_dash.dart, yang berisi game DoodleDash yang memperluas FlameGame. Daftarkan komponen Anda dengan instance FlameGame, komponen paling dasar di Flame (mirip dengan Scaffold Flutter), dan komponen tersebut akan merender dan mengupdate semua komponen yang terdaftar selama gameplay. Anggap saja komponen tersebut sebagai sistem saraf pusat game Anda.

Apa saja komponennya? Serupa dengan aplikasi Flutter yang terdiri dari Widgets, FlameGame terdiri dari Components: semua elemen penyusun yang membentuk game. (Komponen, seperti widget Flutter, dapat juga memiliki komponen turunan.) Sprite karakter, latar belakang game, objek yang bertanggung jawab untuk membuat komponen game baru (musuh, misalnya), semuanya merupakan komponen. Bahkan, FlameGame sendiri merupakan Component; Flame menyebut ini sebagai Sistem Komponen Flame.

Komponen mewarisi dari class Component abstrak. Terapkan metode abstrak Component untuk membuat mekanisme class FlameGame. Misalnya, Anda akan sering melihat metode berikut diterapkan di seluruh DoodleDash:

  • onLoad: menginisiasi komponen secara asinkron (serupa dengan metode initState Flutter)
  • update: mengupdate komponen dengan setiap tick game loop (serupa dengan metode build Flutter)

Selain itu, metode add mendaftarkan komponen dengan Flame engine.

Misalnya, file lib/game/world.dart berisi class World, yang memperluas ParallaxComponent untuk merender latar belakang game. Class ini menggunakan daftar aset gambar, dan merendernya secara berlapis, membuat setiap lapisan bergerak dengan kecepatan yang berbeda agar terlihat lebih realistis. Class DoodleDash berisi instance ParallaxComponent dan menambahkannya ke game dengan metode onLoad DoodleDash:

lib/game/world.dart

class World extends ParallaxComponent<DoodleDash> {
 @override
 Future<void> onLoad() async {
   parallax = await gameRef.loadParallax(
     [
       ParallaxImageData('game/background/06_Background_Solid.png'),
       ParallaxImageData('game/background/05_Background_Small_Stars.png'),
       ParallaxImageData('game/background/04_Background_Big_Stars.png'),
       ParallaxImageData('game/background/02_Background_Orbs.png'),
       ParallaxImageData('game/background/03_Background_Block_Shapes.png'),
       ParallaxImageData('game/background/01_Background_Squiggles.png'),
     ],
     fill: LayerFill.width,
     repeat: ImageRepeat.repeat,
     baseVelocity: Vector2(0, -5),
     velocityMultiplierDelta: Vector2(0, 1.2),
   );
 }
}

lib/game/doodle_dash.dart

class DoodleDash extends FlameGame
   with HasKeyboardHandlerComponents, HasCollisionDetection {
 ...
 final World _world = World();
 ...

 @override
 Future<void> onLoad() async {
   await add(_world);
   ...
 }
}

Pengelolaan status

Direktori lib/game/managers berisi tiga file yang menangani pengelolaan status untuk Doodle Dash: game_manager.dart, object_manager.dart, dan level_manager.dart.

Class GameManager (di game_manager.dart) melacak status game dan pencatatan skor secara keseluruhan.

Class ObjectManager (di object_manager.dart) mengelola di mana dan kapan platform harus muncul dan hilang. Anda akan ditambahkan ke class ini nanti.

Dan, terakhir, class LevelManager (di level_manager.dart), mengelola level kesulitan game beserta konfigurasi game yang relevan saat pemain naik level. Game ini menyediakan lima level kesulitan—pemain akan maju ke level berikutnya saat mencapai salah satu tonggak pencapaian penskoran. Setiap level meningkat kesulitan pun meningkat, dan Dash harus melompat lebih jauh. Karena gravitasi bersifat konstan sepanjang game, kecepatan lompatan ditingkatkan secara bertahap untuk memperhitungkan jarak yang lebih jauh.

Skor pemain meningkat setiap kali pemain melewati platform. Saat pemain mencapai nilai minimum poin tertentu, game naik level dan membuka platform khusus baru yang membuat game lebih menyenangkan dan menantang.

4. Menambahkan Pemain ke game

Pada langkah ini Anda akan menambahkan karakter ke game (dalam hal ini, Dash). Pemain mengontrol karakter dan semua logika yang berada di class Player (dalam file player.dart). Class Player memperluas class SpriteGroupComponent Flame, yang berisi metode abstrak yang Anda ganti untuk menerapkan logika kustom. Penerapan ini termasuk memuat aset dan sprite, memosisikan pemain (secara horizontal dan vertikal), mengonfigurasi deteksi tabrakan, dan menerima input pengguna.

Memuat aset

Dash ditampilkan dengan sprite yang berbeda, merepresentasikan versi karakter dan kekuatan tambahan yang berbeda. Misalnya, ikon berikut menunjukkan Dash dan Sparky menghadap ke tengah, kiri, dan kanan.

SpriteGroupComponent Flame memungkinkan Anda mengelola beberapa status sprite dengan properti sprites, seperti yang akan Anda lihat di metode _loadCharacterSprites.

a3c16fc17be25f6c.pngDi class Player, tambahkan baris berikut ke metode onLoad untuk memuat aset sprite dan atur status sprite Player menghadap ke depan:

lib/game/sprites/player.dart

@override
Future<void> onLoad() async {
  await super.onLoad();

  await _loadCharacterSprites();                                      // Add this line
  current = PlayerState.center;                                       // Add this line
}

Periksa kode untuk memuat sprite dan aset di _loadCharacterSprites. Kode ini dapat diterapkan secara langsung di metode onLoad, tetapi menempatkannya dalam metode terpisah dapat mengatur kode sumber dan membuatnya lebih mudah dibaca. Metode ini memindahkan peta ke properti sprites yang menyambungkan setiap status karakter ke aset sprite yang dimuat, seperti yang ditunjukkan di bawah ini:

lib/game/sprites/player.dart

Future<void> _loadCharacterSprites() async {
   final left = await gameRef.loadSprite('game/${character.name}_left.png');
   final right = await gameRef.loadSprite('game/${character.name}_right.png');
   final center =
       await gameRef.loadSprite('game/${character.name}_center.png');
   final rocket = await gameRef.loadSprite('game/rocket_4.png');
   final nooglerCenter =
       await gameRef.loadSprite('game/${character.name}_hat_center.png');
   final nooglerLeft =
       await gameRef.loadSprite('game/${character.name}_hat_left.png');
   final nooglerRight =
       await gameRef.loadSprite('game/${character.name}_hat_right.png');

   sprites = <PlayerState, Sprite>{
     PlayerState.left: left,
     PlayerState.right: right,
     PlayerState.center: center,
     PlayerState.rocket: rocket,
     PlayerState.nooglerCenter: nooglerCenter,
     PlayerState.nooglerLeft: nooglerLeft,
     PlayerState.nooglerRight: nooglerRight,
   };
 }

Mengupdate komponen pemain

Flame memanggil metode update komponen sekali setiap tick (atau bingkai) loop peristiwa untuk menggambar ulang setiap komponen game yang telah berubah (serupa dengan metode build Flutter). Selanjutnya, tambahkan logika dalam metode update class Player untuk memosisikan karakter dalam layar.

a3c16fc17be25f6c.pngTambahkan kode berikut ke metode update Player untuk menghitung kecepatan dan posisi karakter saat ini:

lib/game/sprites/player.dart

 void update(double dt) {
                                                             // Add lines from here...
   if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;

   _velocity.x = _hAxisInput * jumpSpeed;                              // ... to here.

   final double dashHorizontalCenter = size.x / 2;

   if (position.x < dashHorizontalCenter) {                  // Add lines from here...
     position.x = gameRef.size.x - (dashHorizontalCenter);
   }
   if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
     position.x = dashHorizontalCenter;
   }                                                                   // ... to here.

   // Core gameplay: Add gravity

   position += _velocity * dt;                                       // Add this line
   super.update(dt);
 }

Sebelum menggerakkan pemain, metode update memeriksa untuk memastikan bahwa game tidak dalam keadaan tidak dapat dimainkan di mana pemain tidak bisa bergerak, seperti dalam keadaan awal (ketika game pertama kali dimuat) atau dalam keadaan game berakhir.

Jika game dalam keadaan bisa dimainkan, posisi Dash dihitung menggunakan persamaan new_position = current_position + (velocity * time-elapsed-since-last-game-loop-tick) atau, seperti yang terlihat pada kode:

 position += _velocity * dt

Aspek utama lainnya saat membangun Doodle Dash adalah memastikan untuk menyertakan batas sisi yang tak terbatas. Dengan menyertakan batas ini, Dash dapat melompat dari tepi kiri layar dan masuk kembali dari kanan dan sebaliknya.

7068325e8b2f35fc.gif

Batas ini diterapkan dengan memeriksa apakah posisi Dash telah melebihi tepi kiri atau kanan layar dan, jika telah melebihi tepi, posisikan ulang Dash di tepi yang berlawanan.

Peristiwa utama

Awalnya, Doodle Dash dimainkan di web dan desktop, sehingga perlu dukungan input keyboard agar pemain dapat mengontrol pergerakan karakter. Metode onKeyEvent memungkinkan komponen Player mengenali penekanan tombol panah untuk menentukan apakah Dash harus melihat dan bergerak ke kiri atau kanan.

Dash menghadap ke kiri saat bergerak ke kiri

Dash menghadap ke kanan saat bergerak ke kanan

Selanjutnya, terapkan kemampuan Dash untuk bergerak secara horizontal (sebagaimana ditentukan dalam variabel _hAxisInput). Anda juga harus memastikan Dash menghadap ke arah dia bergerak.

a3c16fc17be25f6c.pngUbah metode moveLeft dan moveRight class Player untuk menentukan arah Dash saat ini:

lib/game/sprites/player.dart

 void moveLeft() {
   _hAxisInput = 0;

   current = PlayerState.left;                                      // Add this line

   _hAxisInput += movingLeftInput;                                  // Add this line

 }

 void moveRight() {
   _hAxisInput = 0;

   current = PlayerState.right;                                     // Add this line

   _hAxisInput += movingRightInput;                                 // Add this line

 }

a3c16fc17be25f6c.pngUbah metode onKeyEvent class Player untuk memanggil metode moveLeft atau moveRight masing-masing, saat tombol panah kiri atau kanan ditekan:

lib/game/sprites/player.dart

@override
 bool onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
   _hAxisInput = 0;

                                                             // Add lines from here...
   if (keysPressed.contains(LogicalKeyboardKey.arrowLeft)) {
     moveLeft();
   }

   if (keysPressed.contains(LogicalKeyboardKey.arrowRight)) {
     moveRight();
   }                                                                   // ... to here.

   // During development, it's useful to "cheat"
   if (keysPressed.contains(LogicalKeyboardKey.arrowUp)) {
     // jump();
   }

   return true;
 }

Sekarang class Player sudah berfungsi, game Doodle Dash dapat menggunakannya.

a3c16fc17be25f6c.pngDalam file DoodleDash, impor sprites.dart, yang membuat class Player tersedia:

lib/game/doodle_dash.dart

import 'sprites/sprites.dart';                                       // Add this line

a3c16fc17be25f6c.pngBuat sebuah instance Player dalam class DoodleDash:

lib/game/doodle_dash.dart

class DoodleDash extends FlameGame
    with HasKeyboardHandlerComponents, HasCollisionDetection {
  DoodleDash({super.children});

  final World _world = World();
  LevelManager levelManager = LevelManager();
  GameManager gameManager = GameManager();
  int screenBufferSpace = 300;
  ObjectManager objectManager = ObjectManager();

  late Player player;                                                // Add this line
  ...
}

a3c16fc17be25f6c.png Selanjutnya, inisialisasikan dan konfigurasikan kecepatan lompatan Player sesuai dengan level kesulitan yang dipilih oleh pemain, dan tambahkan komponen Player ke FlameGame. Lengkapi metode setCharacter dengan kode berikut:

lib/game/doodle_dash.dart

void setCharacter() {
  player = Player(                                           // Add lines from here...
     character: gameManager.character,
     jumpSpeed: levelManager.startingJumpSpeed,
   );
  add(player);                                                         // ... to here.
}

a3c16fc17be25f6c.pngPanggil metode setCharacter di awal initializeGameStart.

lib/game/doodle_dash.dart

void initializeGameStart() {
    setCharacter();                                                   // Add this line

    ...
}

a3c16fc17be25f6c.pngSelain itu, di initializeGameStart, panggil resetPosition pada pemain agar mereka bergerak kembali ke posisi awal setiap kali game dimulai.

lib/game/doodle_dash.dart

void initializeGameStart() {
    ...

    levelManager.reset();

    player.resetPosition();                                           // Add this line

    objectManager = ObjectManager(
        minVerticalDistanceToNextPlatform: levelManager.minDistance,
        maxVerticalDistanceToNextPlatform: levelManager.maxDistance);

    ...
  }

a3c16fc17be25f6c.png Jalankan aplikasi. Mulai game dan Dash akan muncul di layar!

ed15a9c6762595c9.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

5. Menambahkan platform

Pada langkah ini Anda akan menambahkan platform (untuk Dash mendarat dan melompat) dan logika deteksi tabrakan untuk menentukan kapan Dash harus melompat.

Pertama, periksa class abstrak Platform:

lib/game/sprites/platform.dart

abstract class Platform<T> extends SpriteGroupComponent<T>
    with HasGameRef<DoodleDash>, CollisionCallbacks {
  final hitbox = RectangleHitbox();
  bool isMoving = false;

  Platform({
    super.position,
  }) : super(
          size: Vector2.all(100),
          priority: 2,
        );

  @override
  Future<void>? onLoad() async {
    await super.onLoad();
    await add(hitbox);
  }
}

Apa yang dimaksud dengan hitbox?

Setiap komponen platform yang diperkenalkan di Doodle Dash memperluas class abstrak Platform<T>, yang berupa SpriteComponent dengan hitbox. Hitbox memungkinkan komponen sprite untuk mendeteksi saat akan bertabrakan dengan objek lain dengan hitbox. Flame mendukung berbagai bentuk hitbox, seperti persegi panjang, lingkaran, dan poligon. Misalnya, Doodle Dash menggunakan hitbox persegi panjang untuk platform, dan hitbox lingkaran untuk Dash. Flame menangani matematika yang menghitung tabrakan.

Class Platform menambahkan hitbox dan callback tabrakan untuk semua subjenis.

Menambahkan platform standar

Class Platform menambahkan platform ke game. Platform normal diwakili oleh salah satu dari 4 visual yang dipilih secara acak: monitor, telepon, terminal, atau laptop. Pilihan visual tidak memengaruhi perilaku platform.

NormalPlatform

a3c16fc17be25f6c.pngTambahkan platform statis reguler dengan menambahkan class enum NormalPlatformState dan class NormalPlatform:

lib/game/sprites/platform.dart

enum NormalPlatformState { only }                            // Add lines from here...

class NormalPlatform extends Platform<NormalPlatformState> {
  NormalPlatform({super.position});

  final Map<String, Vector2> spriteOptions = {
    'platform_monitor': Vector2(115, 84),
    'platform_phone_center': Vector2(100, 55),
    'platform_terminal': Vector2(110, 83),
    'platform_laptop': Vector2(100, 63),
  };

  @override
  Future<void>? onLoad() async {
    var randSpriteIndex = Random().nextInt(spriteOptions.length);

    String randSprite = spriteOptions.keys.elementAt(randSpriteIndex);

    sprites = {
      NormalPlatformState.only: await gameRef.loadSprite('game/$randSprite.png')
    };

    current = NormalPlatformState.only;

    size = spriteOptions[randSprite]!;
    await super.onLoad();
  }
}                                                                      // ... to here.

Selanjutnya, munculkan platform untuk berinteraksi dengan karakter.

Class ObjectManager memperluas class Component Flame dan menghasilkan objek Platform di seluruh game. Terapkan kemampuan untuk memunculkan platform dalam metode update dan onMount ObjectManager.

a3c16fc17be25f6c.pngMunculkan platform di class ObjectManager dengan membuat metode baru yang disebut _semiRandomPlatform. Anda akan memperbarui metode ini nanti untuk menampilkan berbagai jenis platform, tetapi untuk saat ini, cukup tampilkan NormalPlatform:

lib/game/managers/object_manager.dart

Platform _semiRandomPlatform(Vector2 position) {             // Add lines from here...
    return NormalPlatform(position: position);
}                                                                      // ... to here.

a3c16fc17be25f6c.pngGanti metode update ObjectManager, lalu gunakan metode _semiRandomPlatform untuk membuat platform dan menambahkannya ke dalam game:

lib/game/managers/object_manager.dart

 @override                                                   // Add lines from here...
 void update(double dt) {
   final topOfLowestPlatform =
       _platforms.first.position.y + _tallestPlatformHeight;

   final screenBottom = gameRef.player.position.y +
       (gameRef.size.x / 2) +
       gameRef.screenBufferSpace;

   if (topOfLowestPlatform > screenBottom) {
     var newPlatY = _generateNextY();
     var newPlatX = _generateNextX(100);
     final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
     add(nextPlat);

     _platforms.add(nextPlat);

     gameRef.gameManager.increaseScore();

     _cleanupPlatforms();
     // Losing the game: Add call to _maybeAddEnemy()
     // Powerups: Add call to _maybeAddPowerup();
   }

   super.update(dt);
 }                                                                     // ... to here.

Lakukan hal yang sama dalam metode onMount ObjectManager, agar saat game pertama kali berjalan, metode _semiRandomPlatform menghasilkan platform awal dan menambahkannya ke dalam game.

a3c16fc17be25f6c.pngTambahkan metode onMount beserta kode berikut:

lib/game/managers/object_manager.dart

 @override                                                   // Add lines from here...
 void onMount() {
   super.onMount();

   var currentX = (gameRef.size.x.floor() / 2).toDouble() - 50;

   var currentY =
       gameRef.size.y - (_rand.nextInt(gameRef.size.y.floor()) / 3) - 50;

   for (var i = 0; i < 9; i++) {
     if (i != 0) {
       currentX = _generateNextX(100);
       currentY = _generateNextY();
     }
     _platforms.add(
       _semiRandomPlatform(
         Vector2(
           currentX,
           currentY,
         ),
       ),
     );

     add(_platforms[i]);
   }
 }                                                                     // ... to here.

Misalnya, seperti yang terlihat dalam kode berikut ini, metode configure mengaktifkan game Doodle Dash untuk mengonfigurasi ulang jarak minimum dan maksimum antarplatform dan mengaktifkan platform khusus saat level kesulitan meningkat:

lib/game/managers/object_manager.dart

 void configure(int nextLevel, Difficulty config) {
    minVerticalDistanceToNextPlatform = gameRef.levelManager.minDistance;
    maxVerticalDistanceToNextPlatform = gameRef.levelManager.maxDistance;

    for (int i = 1; i <= nextLevel; i++) {
      enableLevelSpecialty(i);
    }
  }

Instance DoodleDash (dalam metode initializeGameStart), membuat ObjectManager yang diinisialisasi, dikonfigurasi berdasarkan level kesulitan, dan ditambahkan ke game Flame:

lib/game/doodle_dash.dart

  void initializeGameStart() {
    gameManager.reset();

    if (children.contains(objectManager)) objectManager.removeFromParent();

    levelManager.reset();

    player.resetPosition();

    objectManager = ObjectManager(
        minVerticalDistanceToNextPlatform: levelManager.minDistance,
        maxVerticalDistanceToNextPlatform: levelManager.maxDistance);

    add(objectManager);

    objectManager.configure(levelManager.level, levelManager.difficulty);
  }

ObjectManager muncul lagi dalam metode checkLevelUp. Ketika pemain naik level, ObjectManager mengonfigurasi ulang parameter pembuatan platformnya berdasarkan level kesulitan.

lib/game/doodle_dash.dart

  void checkLevelUp() {
    if (levelManager.shouldLevelUp(gameManager.score.value)) {
      levelManager.increaseLevel();

      objectManager.configure(levelManager.level, levelManager.difficulty);
    }
  }

a3c16fc17be25f6c.png Lakukan hot reload 7f9a9e103c7b5e5.png (atau mulai ulang jika Anda menguji di web) untuk mengaktifkan perubahan. (Simpan file, gunakan tombol di IDE Anda atau, dari command line, enter r untuk melakukan hot reload.) Mulai gamenya, dan Dash serta beberapa platform akan muncul di layar:

7c6a6c6e630c42ce.png

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

6. Gameplay inti

Sekarang setelah Anda menerapkan widget Player dan Platform tertentu, Anda dapat mulai menyatukan semuanya. Pada langkah ini, Anda akan menerapkan fungsi inti, deteksi tabrakan, dan pergerakan kamera.

Gravitasi

Untuk membuat game lebih realistis, Dash perlu ditindaklanjuti dengan gravitasi, yaitu gaya yang menarik Dash ke bawah saat dia melompat. Dalam Doodle Dash versi kami, gravitasi tetap berupa nilai konstan dan positif yang selalu menarik Dash ke bawah. Namun, di masa mendatang, Anda mungkin ingin mengubah gravitasi untuk menciptakan efek lain.

a3c16fc17be25f6c.png Di class Player, tambahkan properti _gravity dengan nilai 9:

lib/game/sprites/player.dart

class Player extends SpriteGroupComponent<PlayerState>
    with HasGameRef<DoodleDash>, KeyboardHandler, CollisionCallbacks {

  ...

  Character character;
  double jumpSpeed;
  final double _gravity = 9;                                         // Add this line

  @override
  Future<void> onLoad() async {
    ...
  }
  ...
}

a3c16fc17be25f6c.pngGanti metode update Player untuk menambahkan variabel _gravity agar berdampak pada kecepatan vertikal Dash:

lib/game/sprites/player.dart

 void update(double dt) {
   if (gameRef.gameManager.isIntro || gameRef.gameManager.isGameOver) return;

   _velocity.x = _hAxisInput * jumpSpeed;
   final double dashHorizontalCenter = size.x / 2;

   if (position.x < dashHorizontalCenter) {
     position.x = gameRef.size.x - (dashHorizontalCenter);
   }
   if (position.x > gameRef.size.x - (dashHorizontalCenter)) {
     position.x = dashHorizontalCenter;
   }

   _velocity.y += _gravity;                                          // Add this line

   position += _velocity * dt;
   super.update(dt);
 }

Deteksi tabrakan

Flame mendukung deteksi tabrakan yang tak terduga. Untuk mengaktifkannya di game Flame Anda, tambahkan mixin HasCollisionDetection. Jika Anda memeriksa class DoodleDash, Anda akan melihat bahwa mixin ini telah ditambahkan:

lib/game/doodle_dash.dart

class DoodleDash extends FlameGame
    with HasKeyboardHandlerComponents, HasCollisionDetection {
    ...
}

Selanjutnya, tambahkan deteksi tabrakan ke tiap-tiap komponen game menggunakan mixin CollisionCallbacks. Mixin ini memberikan akses komponen ke callback onCollision. Tabrakan dua objek dengan hitbox memicu callback onCollision dan meneruskan referensi ke objek yang bertabrakan dengannya, sehingga Anda dapat menerapkan logika terkait bagaimana objek Anda seharusnya bereaksi.

Ingat dari langkah sebelumnya bahwa class abstrak Platform sudah memiliki mixin CollisionCallbacks dan hitbox. Class Player juga memiliki mixin CollisionCallbacks, jadi Anda hanya perlu menambahkan CircleHitbox ke class Player. Hitbox Dash sebenarnya berbentuk lingkaran, karena Dash lebih berbentuk lingkaran daripada persegi panjang.

a3c16fc17be25f6c.png Di class Player, impor sprites.dart agar memiliki akses ke berbagai class Platform:

lib/game/sprites/player.dart

import 'sprites.dart';

a3c16fc17be25f6c.png Tambahkan CircleHitbox ke metode onLoad class Player:

lib/game/sprites/player.dart

@override
Future<void> onLoad() async {
  await super.onLoad();

  await add(CircleHitbox());                                         // Add this line

  await _loadCharacterSprites();
  current = PlayerState.center;
}

Dash membutuhkan metode lompat agar dia bisa melompat saat bertabrakan dengan platform.

a3c16fc17be25f6c.png Tambahkan metode jump yang menggunakan specialJumpSpeed opsional:

lib/game/sprites/player.dart

void jump({double? specialJumpSpeed}) {
  _velocity.y = specialJumpSpeed != null ? -specialJumpSpeed : -jumpSpeed;
}

a3c16fc17be25f6c.pngGanti metode onCollision Player dengan menambahkan kode berikut:

lib/game/sprites/player.dart

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
   super.onCollision(intersectionPoints, other);
   bool isCollidingVertically =
       (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;

   if (isMovingDown && isCollidingVertically) {
     current = PlayerState.center;
     if (other is NormalPlatform) {
       jump();
       return;
     }
   }
 }

Callback ini memanggil metode jump Dash setiap kali dia jatuh ke bawah dan bertabrakan dengan bagian atas NormalPlatform. Pernyataan isMovingDown && isCollidingVertically tersebut memastikan bahwa Dash bergerak ke atas melalui platform tanpa memicu lompatan.

Gerakan kamera

Kamera harus mengikuti Dash saat dia bergerak ke atas dalam game, tetapi harus tetap statis saat Dash jatuh.

Di Flame, jika "dunia" lebih besar dari layar, gunakan worldBounds kamera untuk menambahkan batas yang memberi tahu Flame tentang bagian dunia mana yang harus ditampilkan. Untuk memberikan kesan bahwa kamera bergerak ke atas sambil tetap diam secara horizontal, sesuaikan batas dunia atas dan bawah pada setiap update berdasarkan posisi pemain, tetapi pertahankan batas kiri dan kanan sama.

a3c16fc17be25f6c.pngDi class DoodleDash, tambahkan kode berikut ke metode update untuk mengaktifkan kamera agar mengikuti Dash selama bermain game:

lib/game/doodle_dash.dart

@override
  void update(double dt) {
    super.update(dt);

    if (gameManager.isIntro) {
      overlays.add('mainMenuOverlay');
      return;
    }

    if (gameManager.isPlaying) {
      checkLevelUp();

                                                            // Add lines from here...
      final Rect worldBounds = Rect.fromLTRB(
        0,
        camera.position.y - screenBufferSpace,
        camera.gameSize.x,
        camera.position.y + _world.size.y,
      );
      camera.worldBounds = worldBounds;

      if (player.isMovingDown) {
        camera.worldBounds = worldBounds;
      }

      var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
      if (!player.isMovingDown && isInTopHalfOfScreen) {
        camera.followComponent(player);
      }                                                               // ... to here.
    }
  }

Selanjutnya, batas posisi Player dan kamera harus diatur ulang ke asalnya setiap kali game dimulai ulang.

a3c16fc17be25f6c.pngTambahkan kode berikut dalam metode initializeGameStart:

lib/game/doodle_dash.dart

void initializeGameStart() {
    ...
    levelManager.reset();

                                                        // Add the lines from here...
    player.reset();
    camera.worldBounds = Rect.fromLTRB(
      0,
      -_world.size.y,
      camera.gameSize.x,
      _world.size.y +
          screenBufferSpace,
    );
    camera.followComponent(player);
                                                                      // ... to here.

   player.resetPosition();
    ...
  }

Meningkatkan kecepatan lompat saat naik level

Bagian terakhir dari gameplay inti membutuhkan peningkatan kecepatan lompatan Dash setiap kali level kesulitan meningkat dan platform muncul pada jarak yang lebih jauh satu sama lain.

a3c16fc17be25f6c.pngTambahkan panggilan ke metode setJumpSpeed dan berikan kecepatan lompatan yang cocok dengan level saat ini:

lib/game/doodle_dash.dart

void checkLevelUp() {
    if (levelManager.shouldLevelUp(gameManager.score.value)) {
      levelManager.increaseLevel();

      objectManager.configure(levelManager.level, levelManager.difficulty);

      player.setJumpSpeed(levelManager.jumpSpeed);                   // Add this line
    }
  }

a3c16fc17be25f6c.png Lakukan hot reload 7f9a9e103c7b5e5.png (atau mulai ulang di web) untuk mengaktifkan perubahan. (Simpan file, gunakan tombol di IDE Anda atau, dari command line, enter r untuk melakukan hot reload.):

2bc7c856064d74ca.gif

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

7. Selengkapnya tentang platform

Sekarang, setelah ObjectManager menghasilkan platform untuk lompatan Dash, Anda dapat memberinya beberapa platform khusus.

Selanjutnya, tambahkan class BrokenPlatform dan SpringBoard. Seperti namanya, BrokenPlatform akan hancur setelah satu lompatan, dan SpringBoard akan memberikan trampolin yang membuat Dash memantul lebih tinggi dan lebih cepat.

BrokenPlatform

SpringBoard

Seperti class Player, tiap-tiap class platform ini bergantung pada enums untuk menampilkan keadaan mereka saat ini.

lib/game/sprites/platform.dart

enum BrokenPlatformState { cracked, broken }

Perubahan status current platform juga mengubah sprite yang muncul di dalam game. Tentukan pemetaan antara enum State dan aset gambar di properti sprites untuk mengorelasikan sprite mana yang ditetapkan untuk setiap status.

a3c16fc17be25f6c.pngTambahkan enum BrokenPlatformState dan class BrokenPlatform:

lib/game/sprites/platform.dart

enum BrokenPlatformState { cracked, broken }                // Add lines from here...

class BrokenPlatform extends Platform<BrokenPlatformState> {
  BrokenPlatform({super.position});

  @override
  Future<void>? onLoad() async {
    await super.onLoad();

    sprites = <BrokenPlatformState, Sprite>{
      BrokenPlatformState.cracked:
          await gameRef.loadSprite('game/platform_cracked_monitor.png'),
      BrokenPlatformState.broken:
          await gameRef.loadSprite('game/platform_monitor_broken.png'),
    };

    current = BrokenPlatformState.cracked;
    size = Vector2(115, 84);
  }

  void breakPlatform() {
    current = BrokenPlatformState.broken;
  }
}                                                                     // ... to here.

a3c16fc17be25f6c.pngTambahkan enum SpringState dan class SpringBoard:

lib/game/sprites/platform.dart

enum SpringState { down, up }                                // Add lines from here...

class SpringBoard extends Platform<SpringState> {
  SpringBoard({
    super.position,
  });

  @override
  Future<void>? onLoad() async {
    await super.onLoad();

    sprites = <SpringState, Sprite>{
      SpringState.down:
          await gameRef.loadSprite('game/platform_trampoline_down.png'),
      SpringState.up:
          await gameRef.loadSprite('game/platform_trampoline_up.png'),
    };

    current = SpringState.up;

    size = Vector2(100, 45);
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);

    bool isCollidingVertically =
        (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;

    if (isCollidingVertically) {
      current = SpringState.down;
    }
  }

  @override
  void onCollisionEnd(PositionComponent other) {
    super.onCollisionEnd(other);

    current = SpringState.up;
  }
}                                                                      // ... to here.

Selanjutnya, aktifkan platform khusus ini di ObjectManager. Sebagai platform khusus, Anda pasti tidak ingin melihatnya di dalam game sepanjang waktu, jadi, buat berdasarkan probabilitas: 15% untuk SpringBoard dan 10% untuk BrokenPlatform.

a3c16fc17be25f6c.pngDi ObjectManager, di dalam metode _semiRandomPlatform, sebelum pernyataan memunculkan NormalPlatform, tambahkan kode berikut untuk menampilkan platform khusus secara bersyarat:

lib/game/managers/object_manager.dart

Platform _semiRandomPlatform(Vector2 position) {
   if (specialPlatforms['spring'] == true &&                 // Add lines from here...
       probGen.generateWithProbability(15)) {
     return SpringBoard(position: position);
   }

   if (specialPlatforms['broken'] == true &&
       probGen.generateWithProbability(10)) {
     return BrokenPlatform(position: position);
   }                                                                   // ... to here.

   return NormalPlatform(position: position);
}

Bagian yang menyenangkan dari bermain game adalah membuka tantangan dan fitur baru saat Anda naik level.

Anda ingin papan loncatan diisi sejak awal di level 1, tetapi begitu Dash mencapai level 2, dia membuka BrokenPlatform, membuat game menjadi sedikit lebih sulit.

a3c16fc17be25f6c.pngDi class ObjectManager, ganti metode enableLevelSpecialty (yang saat ini berupa stub) dengan menambahkan pernyataan switch yang mengaktifkan platform SpringBoard untuk level 1 dan BrokenPlatform untuk level 2:

lib/game/managers/object_manager.dart

void enableLevelSpecialty(int level) {
  switch (level) {                                           // Add lines from here...
    case 1:
      enableSpecialty('spring');
      break;
    case 2:
      enableSpecialty('broken');
      break;
  }                                                                    // ... to here.
}

a3c16fc17be25f6c.pngSelanjutnya, berikan kemampuan pada platform untuk bergerak bolak-balik secara horizontal. Di class abstrak Platform**,** tambahkan metode _move berikut:

lib/game/sprites/platform.dart

void _move(double dt) {
    if (!isMoving) return;

    final double gameWidth = gameRef.size.x;

    if (position.x <= 0) {
      direction = 1;
    } else if (position.x >= gameWidth - size.x) {
      direction = -1;
    }

    _velocity.x = direction * speed;

    position += _velocity * dt;
}

Jika platform bergerak, kode itu akan mengubah gerakannya ke arah yang berlawanan saat mencapai tepi layar game. Seperti halnya Dash, posisi platform ditentukan dengan mengalikan _direction dengan speed platform untuk mendapatkan kecepatan. Kemudian, kalikan kecepatan dengan time-elapsed dan tambahkan jarak yang dihasilkan ke position platform saat ini.

a3c16fc17be25f6c.pngGanti metode update class Platform untuk memanggil metode _move:

lib/game/sprites/platform.dart

@override
void update(double dt) {
  _move(dt);
  super.update(dt);
}

a3c16fc17be25f6c.pngUntuk memicu pergerakan Platform, dalam metode onLoad, atur secara acak boolean isMoving ke true 20% waktu.

lib/game/sprites/platform.dart

@override
Future<void>? onLoad() async {
  await super.onLoad();

  await add(hitbox);

  final int rand = Random().nextInt(100);                            // Add this line
  if (rand > 80) isMoving = true;                                    // Add this line
}

a3c16fc17be25f6c.pngTerakhir, di Player, ganti metode onCollision class Player untuk mengenali tabrakan dengan Springboard atau BrokenPlatform. Perhatikan bahwa SpringBoard memanggil jump dengan pengali kecepatan 2x dan BrokenPlatform hanya memanggil jump jika statusnya .cracked, bukan .broken (sudah dilompati):

lib/game/sprites/player.dart

@override
 void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
   super.onCollision(intersectionPoints, other);

   bool isCollidingVertically =
       (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;

   if (isMovingDown && isCollidingVertically) {
     current = PlayerState.center;
     if (other is NormalPlatform) {
       jump();
       return;
     } else if (other is SpringBoard) {                      // Add lines from here...
       jump(specialJumpSpeed: jumpSpeed * 2);
       return;
     } else if (other is BrokenPlatform &&
         other.current == BrokenPlatformState.cracked) {
       jump();
       other.breakPlatform();
       return;
     }                                                                 // ... to here.
   }
 }

a3c16fc17be25f6c.png Mulai ulang aplikasi. Mulai game untuk melihat pergerakan platform, SpringBoard, dan BrokenPlatform.

d4949925e897f665.gif

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

8. Kalah dalam game

Pada langkah ini Anda akan menambahkan kondisi kalah pada game Doodle Dash. Ada dua kondisi yang menyebabkan pemain kalah:

  1. Dash gagal melompati platform dan jatuh ke bagian bawah layar.
  2. Dash bertabrakan dengan platform Enemy.

Sebelum Anda dapat menerapkan kondisi "game berakhir", Anda perlu menambahkan logika yang menyetel status game DoodleDash menjadi gameOver.

a3c16fc17be25f6c.pngDalam class DoodleDash**,** tambahkan metode onLose yang dapat dipanggil kapan pun game harus berakhir. Metode tersebut menyetel status game, menghapus pemain dari layar, dan mengaktifkan menu/overlay **Game Berakhir**.

lib/game/sprites/doodle_dash.dart

 void onLose() {                                             // Add lines from here...
    gameManager.state = GameState.gameOver;
    player.removeFromParent();
    overlays.add('gameOverOverlay');
  }                                                                    // ... to here.

Menu Game Berakhir:

6a79b43f4a1f780d.png

a3c16fc17be25f6c.pngDi bagian atas metode update DoodleDash, tambahkan kode berikut untuk menghentikan update game saat status game GameOver:

lib/game/sprites/doodle_dash.dart

@override
 void update(double dt) {
   super.update(dt);

   if (gameManager.isGameOver) {                             // Add lines from here...
     return;
   }                                                                   // ... to here.
   ...
}

a3c16fc17be25f6c.pngSelain itu, di metode update, panggil onLose ketika pemain telah jatuh ke bagian bawah layar.

lib/game/sprites/doodle_dash.dart

@override
 void update(double dt) {
   ...

   if (gameManager.isPlaying) {
     checkLevelUp();

     final Rect worldBounds = Rect.fromLTRB(
       0,
       camera.position.y - screenBufferSpace,
       camera.gameSize.x,
       camera.position.y + _world.size.y,
     );
     camera.worldBounds = worldBounds;
     if (player.isMovingDown) {
       camera.worldBounds = worldBounds;
     }

     var isInTopHalfOfScreen = player.position.y <= (_world.size.y / 2);
     if (!player.isMovingDown && isInTopHalfOfScreen) {
       camera.followComponent(player);
     }

                                                             // Add lines from here...
     if (player.position.y >
         camera.position.y +
             _world.size.y +
             player.size.y +
             screenBufferSpace) {
       onLose();
     }                                                                 // ... to here.
   }
 }

Musuh bisa datang dalam berbagai bentuk dan ukuran; di Doodle Dash, mereka ditandai dengan tempat sampah atau ikon folder error. Pemain harus menghindari tabrakan dengan salah satu musuh ini karena itu adalah penyebab game berakhir.

Enemy

a3c16fc17be25f6c.pngBuat jenis platform musuh dengan menambahkan enum EnemyPlatformState dan class EnemyPlatform:

lib/game/sprites/platform.dart

enum EnemyPlatformState { only }                             // Add lines from here...

class EnemyPlatform extends Platform<EnemyPlatformState> {
  EnemyPlatform({super.position});

  @override
  Future<void>? onLoad() async {
    var randBool = Random().nextBool();
    var enemySprite = randBool ? 'enemy_trash_can' : 'enemy_error';

    sprites = <EnemyPlatformState, Sprite>{
      EnemyPlatformState.only:
          await gameRef.loadSprite('game/$enemySprite.png'),
    };

    current = EnemyPlatformState.only;

    return super.onLoad();
  }
}                                                                      // ... to here.

Class EnemyPlatform memperluas supertype Platform. ObjectManager muncul dan mengelola platform musuh seperti halnya mengelola semua platform lainnya.

a3c16fc17be25f6c.pngDi ObjectManager, tambahkan kode berikut untuk memunculkan dan mengelola platform musuh:

lib/game/managers/object_manager.dart

final List<EnemyPlatform> _enemies = [];                    // Add lines from here...
void _maybeAddEnemy() {
  if (specialPlatforms['enemy'] != true) {
    return;
  }
  if (probGen.generateWithProbability(20)) {
    var enemy = EnemyPlatform(
      position: Vector2(_generateNextX(100), _generateNextY()),
    );
    add(enemy);
    _enemies.add(enemy);
    _cleanupEnemies();
  }
}

void _cleanupEnemies() {
  final screenBottom = gameRef.player.position.y +
      (gameRef.size.x / 2) +
      gameRef.screenBufferSpace;

  while (_enemies.isNotEmpty && _enemies.first.position.y > screenBottom) {
    remove(_enemies.first);
    _enemies.removeAt(0);
  }
}                                                                      // ... to here.

ObjectManager menyimpan daftar objek musuh, _enemies. _maybeAddEnemy memunculkan musuh dengan probabilitas 20 persen dan menambahkan objek ke daftar musuh. Metode _cleanupEnemies() menghilangkan objek EnemyPlatform yang tidak berlaku dan tidak terlihat lagi.

a3c16fc17be25f6c.pngDi ObjectManager, munculkan platform musuh dengan memanggil _maybeAddEnemy() dalam metode update:

lib/game/managers/object_manager.dart

@override
void update(double dt) {
  final topOfLowestPlatform =
      _platforms.first.position.y + _tallestPlatformHeight;

  final screenBottom = gameRef.player.position.y +
      (gameRef.size.x / 2) +
      gameRef.screenBufferSpace;
  if (topOfLowestPlatform > screenBottom) {
    var newPlatY = _generateNextY();
    var newPlatX = _generateNextX(100);
    final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
    add(nextPlat);

    _platforms.add(nextPlat);
    gameRef.gameManager.increaseScore();

    _cleanupPlatforms();
    _maybeAddEnemy();                                                 // Add this line
  }

  super.update(dt);
}

a3c16fc17be25f6c.pngTambahkan ke metode onCollision Player untuk memeriksa apakah Dash bertabrakan dengan EnemyPlatform. Jika bertabrakan, panggil metode onLose().

lib/game/sprites/player.dart

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    if (other is EnemyPlatform) {                           // Add lines from here...
      gameRef.onLose();
      return;
    }                                                                 // ... to here.

    bool isCollidingVertically =
        (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;

    if (isMovingDown && isCollidingVertically) {
      current = PlayerState.center;
      if (other is NormalPlatform) {
        jump();
        return;
      } else if (other is SpringBoard) {
        jump(specialJumpSpeed: jumpSpeed * 2);
        return;
      } else if (other is BrokenPlatform &&
          other.current == BrokenPlatformState.cracked) {
        jump();
        other.breakPlatform();
        return;
      }
    }
  }

a3c16fc17be25f6c.pngTerakhir, ganti metode enableLevelSpecialty ObjectManager untuk menambahkan level 5 ke pernyataan switch:

lib/game/managers/object_manager.dart

void enableLevelSpecialty(int level) {
  switch (level) {
    case 1:
      enableSpecialty('spring');
      break;
    case 2:
      enableSpecialty('broken');
      break;
    case 5:                                                  // Add lines from here...
      enableSpecialty('enemy');
      break;                                                           // ... to here.
  }
}

a3c16fc17be25f6c.png Sekarang, setelah Anda membuat game lebih menantang, lakukan hot reload 7f9a9e103c7b5e5.png untuk mengaktifkan perubahan. (Simpan file, gunakan tombol di IDE Anda atau, dari command line, enter r untuk melakukan hot reload.):

Waspadalah terhadap musuh yang berbentuk folder rusak itu. Mereka itu licik. Mereka menyatu dengan latar belakang.

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

9. Kekuatan Tambahan

Pada langkah ini Anda akan menambahkan fitur game yang ditingkatkan untuk memperkuat Dash di sepanjang game. Doodle Dash memiliki dua opsi kekuatan tambahan: topi Noogler atau Roket. Anda dapat menganggap kekuatan tambahan ini sebagai jenis platform khusus lainnya. Ketika Dash melompat di sepanjang game, kecepatannya akan meningkat saat dia menabrak dan memiliki kekuatan tambahan topi Noogler atau Roket.

NooglerHat

Rocket

Topi Noogler muncul di level 3, ketika pemain mencapai skor >= 40. Ketika Dash bertabrakan dengan topi, dia memakai topi Noogler dan menerima peningkatan akselerasi sebesar 2,5x dari kecepatan normalnya. Peningkatan akselerasi ini berlangsung selama 5 detik.

Roket muncul di level 4, ketika pemain mencapai skor >= 80. Ketika Dash bertabrakan dengan Rocket, sprite-nya digantikan oleh roket, dan dia menerima peningkatan akselerasi sebesar 3,5x dari kecepatan normalnya hingga dia mendarat di platform. Bonusnya, dia juga tak terkalahkan melawan musuh ketika dia memiliki kekuatan tambahan Roket.

Sprite Topi Noogler dan Rocket memperluas class abstrak PowerUp. Seperti class abstrak Platform, class abstrak PowerUp, di bawah ini, juga mencakup pengukuran dan hitbox.

lib/game/sprites/powerup.dart

abstract class PowerUp extends SpriteComponent
    with HasGameRef<DoodleDash>, CollisionCallbacks {
  final hitbox = RectangleHitbox();
  double get jumpSpeedMultiplier;

  PowerUp({
    super.position,
  }) : super(
          size: Vector2.all(50),
          priority: 2,
        );

  @override
  Future<void>? onLoad() async {
    await super.onLoad();

    await add(hitbox);
  }
}

a3c16fc17be25f6c.png Buat class Rocket yang memperluas class abstrak PowerUp. Ketika Dash bertabrakan dengan roket, dia menerima peningkatan akselerasi sebesar 3,5 kali dari kecepatan normalnya.

lib/game/sprites/powerup.dart

class Rocket extends PowerUp {                               // Add lines from here...
  @override
  double get jumpSpeedMultiplier => 3.5;

  Rocket({
    super.position,
  });

  @override
  Future<void>? onLoad() async {
    await super.onLoad();
    sprite = await gameRef.loadSprite('game/rocket_1.png');
    size = Vector2(50, 70);
  }
}                                                                      // ... to here.

a3c16fc17be25f6c.png Buat class NooglerHat yang memperluas class abstrak PowerUp. Ketika Dash bertabrakan dengan NooglerHat, dia menerima peningkatan akselerasi sebesar 2,5 kali dari kecepatan normalnya. Peningkatan akselerasi berlangsung selama 5 detik.

lib/game/sprites/powerup.dart

class NooglerHat extends PowerUp {                          // Add lines from here...
  @override
  double get jumpSpeedMultiplier => 2.5;

  NooglerHat({
    super.position,
  });

  final int activeLengthInMS = 5000;

  @override
  Future<void>? onLoad() async {
    await super.onLoad();
    sprite = await gameRef.loadSprite('game/noogler_hat.png');
    size = Vector2(75, 50);
  }
}                                                                      // ... to here.

Sekarang, setelah Anda menerapkan kekuatan tambahan NooglerHat dan Rocket, update ObjectManager untuk memunculkan keduanya di game.

a3c16fc17be25f6c.png Ganti class ObjectManger untuk menambahkan daftar yang melacak kekuatan tambahan yang dimunculkan, bersama dengan dua metode baru: _maybePowerup dan _cleanupPowerups untuk memunculkan atau menghilangkan platform kekuatan tambahan baru.

lib/game/managers/object_manager.dart

final List<PowerUp> _powerups = [];                          // Add lines from here...

 void _maybeAddPowerup() {
   if (specialPlatforms['noogler'] == true &&
       probGen.generateWithProbability(20)) {
     var nooglerHat = NooglerHat(
       position: Vector2(_generateNextX(75), _generateNextY()),
     );
     add(nooglerHat);
     _powerups.add(nooglerHat);
   } else if (specialPlatforms['rocket'] == true &&
       probGen.generateWithProbability(15)) {
     var rocket = Rocket(
       position: Vector2(_generateNextX(50), _generateNextY()),
     );
     add(rocket);
     _powerups.add(rocket);
   }

   _cleanupPowerups();
 }

 void _cleanupPowerups() {
   final screenBottom = gameRef.player.position.y +
       (gameRef.size.x / 2) +
       gameRef.screenBufferSpace;
   while (_powerups.isNotEmpty && _powerups.first.position.y > screenBottom) {
     if (_powerups.first.parent != null) {
       remove(_powerups.first);
     }
     _powerups.removeAt(0);
   }
 }                                                                     // ... to here.

Metode _maybeAddPowerup memunculkan topi noogler selama 20% waktu atau roket selama 15% waktu. Metode _cleanupPowerups dipanggil untuk menghilangkan kekuatan tambahan yang berada di batas bawah layar.

a3c16fc17be25f6c.png Ganti metode update ObjectManager untuk memanggil _maybePowerup di setiap tick game loop.

lib/game/managers/object_manager.dart

@override
  void update(double dt) {
    final topOfLowestPlatform =
        _platforms.first.position.y + _tallestPlatformHeight;

    final screenBottom = gameRef.player.position.y +
        (gameRef.size.x / 2) +
        gameRef.screenBufferSpace;

    if (topOfLowestPlatform > screenBottom) {
      var newPlatY = _generateNextY();
      var newPlatX = _generateNextX(100);
      final nextPlat = _semiRandomPlatform(Vector2(newPlatX, newPlatY));
      add(nextPlat);

      _platforms.add(nextPlat);

      gameRef.gameManager.increaseScore();

      _cleanupPlatforms();
      _maybeAddEnemy();
      _maybeAddPowerup();                                            // Add this line
    }

    super.update(dt);
  }

a3c16fc17be25f6c.pngGanti metode enableLevelSpecialty untuk menambahkan dua kasus baru dalam pernyataan peralihan: satu untuk mengaktifkan NooglerHat di level 3 dan satu lagi untuk mengaktifkan Rocket di level 4:

lib/game/managers/object_manager.dart

void enableLevelSpecialty(int level) {
    switch (level) {
      case 1:
        enableSpecialty('spring');
        break;
      case 2:
        enableSpecialty('broken');
        break;
      case 3:                                               // Add lines from here...
        enableSpecialty('noogler');
        break;
      case 4:
        enableSpecialty('rocket');
        break;                                                        // ... to here.
      case 5:
        enableSpecialty('enemy');
        break;
    }
  }

a3c16fc17be25f6c.png Tambahkan pengambil boolean berikut ke class Player. Jika Dash memiliki kekuatan tambahan yang aktif, kekuatan itu dapat ditampilkan dengan berbagai status yang berbeda. Pengambil ini mempermudah pemeriksaan tentang kekuatan tambahan mana yang aktif.

lib/game/sprites/player.dart

 bool get hasPowerup =>                                      // Add lines from here...
     current == PlayerState.rocket ||
     current == PlayerState.nooglerLeft ||
     current == PlayerState.nooglerRight ||
     current == PlayerState.nooglerCenter;

 bool get isInvincible => current == PlayerState.rocket;

 bool get isWearingHat =>
     current == PlayerState.nooglerLeft ||
     current == PlayerState.nooglerRight ||
     current == PlayerState.nooglerCenter;                             // ... to here.

a3c16fc17be25f6c.pngGanti metode onCollision Player untuk memberikan reaksi terhadap tabrakan dengan NooglerHat atau Rocket. Kode ini juga memastikan bahwa Dash hanya mengaktifkan kekuatan tambahan baru jika dia belum memilikinya.

lib/game/sprites/player.dart

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollision(intersectionPoints, other);
    if (other is EnemyPlatform && !isInvincible) {
      gameRef.onLose();
      return;
    }

    bool isCollidingVertically =
        (intersectionPoints.first.y - intersectionPoints.last.y).abs() < 5;

    if (isMovingDown && isCollidingVertically) {
      current = PlayerState.center;
      if (other is NormalPlatform) {
        jump();
        return;
      } else if (other is SpringBoard) {
        jump(specialJumpSpeed: jumpSpeed * 2);
        return;
      } else if (other is BrokenPlatform &&
          other.current == BrokenPlatformState.cracked) {
        jump();
        other.breakPlatform();
        return;
      }
    }

    if (!hasPowerup && other is Rocket) {                    // Add lines from here...
      current = PlayerState.rocket;
      other.removeFromParent();
      jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
      return;
    } else if (!hasPowerup && other is NooglerHat) {
      if (current == PlayerState.center) current = PlayerState.nooglerCenter;
      if (current == PlayerState.left) current = PlayerState.nooglerLeft;
      if (current == PlayerState.right) current = PlayerState.nooglerRight;
      other.removeFromParent();
      _removePowerupAfterTime(other.activeLengthInMS);
      jump(specialJumpSpeed: jumpSpeed * other.jumpSpeedMultiplier);
      return;
    }                                                                  // ... to here.
  }

Jika Dash bertabrakan dengan roket, PlayerState berubah menjadi Rocket dan memungkinkan Dash melompat dengan jumpSpeedMultiplier sebesar 3,5x.

Jika Dash bertabrakan dengan topi Noogler, sesuai dengan arah PlayerState saat ini (.center, .left, atau .right), PlayerState akan berubah menjadi PlayerState yang sesuai dengan arah Noogler sambil mengenakan topi Noogler dan memberinya peningkatan jumpSpeedMultiplier sebesar 2.5x. Metode _removePowerupAfterTime menghilangkan kekuatan tambahan setelah 5 detik dan mengubah arah PlayerState dari arah kekuatan tambahan belakang ke center.

Panggilan ke other.removeFromParent menghilangkan Platform sprite Topi Noogler atau Roket dari layar untuk menunjukkan bahwa Dash telah memperoleh kekuatan tambahan.

ede04fdfe074f471.gif

a3c16fc17be25f6c.pngGanti metode moveLeft dan moveRight class Pemain untuk menjelaskan sprite NooglerHat. Anda tidak perlu menjelaskan kekuatan tambahan Rocket karena sprite itu menghadap ke arah yang sama terlepas dari arah perjalanannya.

lib/game/sprites/player.dart

 void moveLeft() {
   _hAxisInput = 0;
   if (isWearingHat) {                                       // Add lines from here...
     current = PlayerState.nooglerLeft;
   } else if (!hasPowerup) {                                           // ... to here.
     current = PlayerState.left;
   }                                                                  // Add this line
   _hAxisInput += movingLeftInput;
 }

 void moveRight() {
   _hAxisInput = 0;
   if (isWearingHat) {                                       // Add lines from here...
     current = PlayerState.nooglerRight;
   } else if (!hasPowerup) {                                            //... to here.
     current = PlayerState.right;
   }                                                                  // Add this line
   _hAxisInput += movingRightInput;
 }

Dash tidak terkalahkan oleh musuh saat dia memiliki kekuatan tambahan Rocket, jadi jangan akhiri game selama waktu ini.

a3c16fc17be25f6c.pngGanti callback onCollision untuk memeriksa apakah Dash isInvincible sebelum memicu game berakhir saat bertabrakan dengan EnemyPlatform:

lib/game/sprites/player.dart

   if (other is EnemyPlatform && !isInvincible) {                 // Modify this line
     gameRef.onLose();
     return;
   }

a3c16fc17be25f6c.png Mulai ulang aplikasi dan mainkan game untuk melihat kekuatan tambahan beraksi.

e1fece51429dae55.gif

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

10. Overlay

Game Flame dapat digabungkan dengan widget, sehingga membuatnya lebih mudah diintegrasikan dengan widget lain di aplikasi Flutter. Anda juga dapat menampilkan widget Flutter sebagai overlay di atas Game Flame. Hal ini sangat praktis untuk komponen non-gameplay yang tidak bergantung pada game loop seperti menu, layar jeda, tombol, dan penggeser.

Tampilan skor yang terlihat dalam game beserta semua menu di Doodle Dash adalah widget Flutter biasa, bukan komponen Flame. Semua kode widget terletak di lib/game/widgets, misalnya, menu Game Berakhir hanyalah kolom yang berisi widget lain seperti Text dan ElevatedButton, seperti yang ditunjukkan pada kode berikut:

lib/game/widgets/game_over_overlay.dart

class GameOverOverlay extends StatelessWidget {
 const GameOverOverlay(this.game, {super.key});

 final Game game;

 @override
 Widget build(BuildContext context) {
   return Material(
     color: Theme.of(context).colorScheme.background,
     child: Center(
       child: Padding(
         padding: const EdgeInsets.all(48.0),
         child: Column(
           mainAxisAlignment: MainAxisAlignment.center,
           crossAxisAlignment: CrossAxisAlignment.center,
           children: [
             Text(
               'Game Over',
               style: Theme.of(context).textTheme.displayMedium!.copyWith(),
             ),
             const WhiteSpace(height: 50),
             ScoreDisplay(
               game: game,
               isLight: true,
             ),
             const WhiteSpace(
               height: 50,
             ),
             ElevatedButton(
               onPressed: () {
                 (game as DoodleDash).resetGame();
               },
               style: ButtonStyle(
                 minimumSize: MaterialStateProperty.all(
                   const Size(200, 75),
                 ),
                 textStyle: MaterialStateProperty.all(
                     Theme.of(context).textTheme.titleLarge),
               ),
               child: const Text('Play Again'),
             ),
           ],
         ),
       ),
     ),
   );
 }
}

Untuk menggunakan widget sebagai overlay di game Flame, tentukan properti overlayBuilderMap di GameWidget dengan key yang menampilkan overlay (sebagai String), dan value fungsi widget yang menampilkan widget, seperti yang ditampilkan dalam kode berikut:

lib/main.dart

GameWidget(
  game: game,
  overlayBuilderMap: <String, Widget Function(BuildContext, Game)>{
    'gameOverlay': (context, game) => GameOverlay(game),
    'mainMenuOverlay': (context, game) => MainMenuOverlay(game),
    'gameOverOverlay': (context, game) => GameOverOverlay(game),
  },
)

Setelah ditambahkan, overlay dapat digunakan di mana saja dalam game. Tampilkan overlay menggunakan overlays.add dan sembunyikan menggunakan overlays.remove, seperti yang ditunjukkan pada kode berikut:

lib/game/doodle_dash.dart

void resetGame() {
   startGame();
   overlays.remove('gameOverOverlay');
 }

 void onLose() {
   gameManager.state = GameState.gameOver;
   player.removeFromParent();
   overlays.add('gameOverOverlay');
 }

11. Dukungan seluler

Doodle Dash dibangun di Flutter dan Flame, sehingga game tersebut sudah dapat dijalankan di seluruh platform yang didukung Flutter. Namun, sejauh ini Doodle Dash hanya mendukung input berbasis keyboard. Untuk perangkat yang tidak memiliki keyboard, seperti ponsel, Anda dapat menambahkan tombol kontrol sentuh di layar ke overlay dengan mudah.

a3c16fc17be25f6c.png Tambahkan variabel status boolean ke GameOverlay yang menentukan kapan game berjalan di platform seluler:

lib/game/widgets/game_overlay.dart

class GameOverlayState extends State<GameOverlay> {
 bool isPaused = false;

                                                                      // Add this line
 final bool isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);

 @override
 Widget build(BuildContext context) {
   ...
 }
}

Sekarang, tampilkan tombol arah kiri dan kanan di overlay ketika game berjalan di perangkat seluler. Mirip dengan logika "peristiwa utama" di langkah 4, mengetuk tombol kiri akan menggerakkan Dash ke kiri. Mengetuk tombol kanan akan menggerakkannya ke kanan.

a3c16fc17be25f6c.png Di metode build GameOverlay, tambahkan bagian isMobile yang mengikuti perilaku yang sama sebagaimana yang dideskripsikan di langkah 4: mengetuk tombol kiri memanggil moveLeft dan tombol kanan memanggil moveRight. Melepaskan kedua tombol memanggil resetDirection dan menjadikan Dash tetap bergerak secara horizontal.

lib/game/widgets/game_overlay.dart

@override
 Widget build(BuildContext context) {
   return Material(
     color: Colors.transparent,
     child: Stack(
       children: [
         Positioned(... child: ScoreDisplay(...)),
         Positioned(... child: ElevatedButton(...)),
         if (isMobile)                                       // Add lines from here...
           Positioned(
             bottom: MediaQuery.of(context).size.height / 4,
             child: SizedBox(
               width: MediaQuery.of(context).size.width,
               child: Row(
                 mainAxisAlignment: MainAxisAlignment.spaceBetween,
                 children: [
                   Padding(
                     padding: const EdgeInsets.only(left: 24),
                     child: GestureDetector(
                       onTapDown: (details) {
                         (widget.game as DoodleDash).player.moveLeft();
                       },
                       onTapUp: (details) {
                         (widget.game as DoodleDash).player.resetDirection();
                       },
                       child: Material(
                         color: Colors.transparent,
                         elevation: 3.0,
                         shadowColor: Theme.of(context).colorScheme.background,
                         child: const Icon(Icons.arrow_left, size: 64),
                       ),
                     ),
                   ),
                   Padding(
                     padding: const EdgeInsets.only(right: 24),
                     child: GestureDetector(
                       onTapDown: (details) {
                         (widget.game as DoodleDash).player.moveRight();
                       },
                       onTapUp: (details) {
                         (widget.game as DoodleDash).player.resetDirection();
                       },
                       child: Material(
                         color: Colors.transparent,
                         elevation: 3.0,
                         shadowColor: Theme.of(context).colorScheme.background,
                         child: const Icon(Icons.arrow_right, size: 64),
                       ),
                     ),
                   ),
                 ],
               ),
             ),
           ),                                                          // ... to here.
         if (isPaused)
           ...
       ],
     ),
   );
 }

Selesai. Sekarang aplikasi Doodle Dash otomatis mendeteksi jenis platform yang dijalankannya dan mengganti inputnya sesuai dengan itu.

a3c16fc17be25f6c.png Jalankan aplikasi di iOS Atau Android untuk melihat performa tombol arahnya.

7b0cac5fb69bc89.gif

Terjadi masalah?

Jika aplikasi tidak berjalan dengan semestinya, cari apakah ada salah ketik. Jika perlu, gunakan kode di link berikut untuk membuat aplikasi normal kembali.

12. Langkah berikutnya

Selamat!

Anda telah menyelesaikan codelab ini dan telah mempelajari cara membangun game di Flutter menggunakan game engine Flame.

Yang telah kita bahas:

  • Cara menggunakan paket Flame untuk membuat game platformer, termasuk:
  • Menambahkan karakter
  • Menambahkan berbagai macam jenis platform
  • Menerapkan deteksi tabrakan
  • Menambahkan komponen gravitasi
  • Menentukan pergerakan kamera
  • Membuat musuh
  • Membuat kekuatan tambahan
  • Cara mendeteksi platform tempat game berjalan dan...
  • Cara menggunakan info tersebut untuk beralih antara keyboard dan kontrol input sentuh

Referensi

Semoga Anda belajar banyak hal terkait cara membuat game di Flutter!

Referensi berikut mungkin juga berguna bagi Anda, bahkan mungkin menginspirasi: