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.
Level
Ada 5 level dalam game. Tiap level (setelah level 1) membuka fitur baru.
- Level 1 (default): Level ini memunculkan platform
NormalPlatform
danSpringBoard
. 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)
|
|
Level 2 (skor >= 20) | Level 3 (skor >= 40) | Level 4 (skor >= 80) | Level 5 (skor >= 100) |
|
|
|
|
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
Download project versi awal Anda dari GitHub:
- 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.
Mengimpor aplikasi awal
- Impor direktori
flutter-codelabs/flame-building-doodle-dash/step_02
ke IDE pilihan Anda.
Menginstal 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 metodeinitState
Flutter)update
: mengupdate komponen dengan setiap tick game loop (serupa dengan metodebuild
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
.
Di 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.
Tambahkan 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.
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.
Ubah 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
}
Ubah 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.
Dalam file DoodleDash, impor sprites.dart
, yang membuat class Player
tersedia:
lib/game/doodle_dash.dart
import 'sprites/sprites.dart'; // Add this line
Buat 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
...
}
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.
}
Panggil metode setCharacter
di awal initializeGameStart
.
lib/game/doodle_dash.dart
void initializeGameStart() {
setCharacter(); // Add this line
...
}
Selain 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);
...
}
Jalankan aplikasi. Mulai game dan Dash akan muncul di layar!
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.
|
Tambahkan 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
.
Munculkan 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.
Ganti 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.
Tambahkan 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);
}
}
Lakukan hot reload (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:
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.
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 {
...
}
...
}
Ganti 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.
Di class Player
, impor sprites.dart
agar memiliki akses ke berbagai class Platform
:
lib/game/sprites/player.dart
import 'sprites.dart';
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.
Tambahkan metode jump
yang menggunakan specialJumpSpeed
opsional:
lib/game/sprites/player.dart
void jump({double? specialJumpSpeed}) {
_velocity.y = specialJumpSpeed != null ? -specialJumpSpeed : -jumpSpeed;
}
Ganti 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.
Di 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.
Tambahkan 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.
Tambahkan 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
}
}
Lakukan hot reload (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.):
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.
|
|
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.
Tambahkan 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.
Tambahkan 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
.
Di 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.
Di 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.
}
Selanjutnya, 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.
Ganti metode update
class Platform
untuk memanggil metode _move
:
lib/game/sprites/platform.dart
@override
void update(double dt) {
_move(dt);
super.update(dt);
}
Untuk 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
}
Terakhir, 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.
}
}
Mulai ulang aplikasi. Mulai game untuk melihat pergerakan platform, SpringBoard
, dan BrokenPlatform
.
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:
- Dash gagal melompati platform dan jatuh ke bagian bawah layar.
- Dash bertabrakan dengan platform
Enemy
.
Sebelum Anda dapat menerapkan kondisi "game berakhir", Anda perlu menambahkan logika yang menyetel status game DoodleDash menjadi gameOver
.
Dalam 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:
Di 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.
...
}
Selain 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.
|
Buat 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.
Di 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.
Di 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);
}
Tambahkan 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;
}
}
}
Terakhir, 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.
}
}
Sekarang, setelah Anda membuat game lebih menantang, lakukan hot reload 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.
|
|
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);
}
}
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.
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.
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.
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);
}
Ganti 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;
}
}
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.
Ganti 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.
Ganti 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.
Ganti 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;
}
Mulai ulang aplikasi dan mainkan game untuk melihat kekuatan tambahan beraksi.
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.
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.
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.
Jalankan aplikasi di iOS Atau Android untuk melihat performa tombol arahnya.
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:
- Dokumentasi Flame dan Paket Flame di pub.dev
- Video YouTube Basics of the Flame game engine oleh Lukas Klingsbo
- Serial game Simple Platformer, The Flame + Flutter oleh DevKage
- Serial Dino Run; The Flutter Game Development oleh DevKage
- Serial Spacescape, The Flutter Game Development oleh DevKage
- Game Flutter
- Halaman Flutter's Casual Games toolkit dan Templat Memulai yang terkait dengan toolkit Game Kasual (Toolkit Game Kasual tidak menggunakan Flame engine, tapi didesain untuk mendukung iklan seluler dan pembelian aplikasi dalam game.)
- Build your own game in Flutter, video toolkit Game Kasual
- Halaman Flutter Puzzle Hack (kompetisi yang berlangsung pada Januari 2022) dan video pemenang yang dihasilkan.