1. Giriş
Flame, Flutter tabanlı bir 2D oyun motorudur. Bu codelab'de 70'lerin klasik video oyunlarından biri olan Steve Wozniak'ın Breakout'undan esinlenen bir oyun geliştireceksiniz. Sopayı, topu ve tuğlaları çizmek için Alev'in Bileşenlerini kullanacaksınız. Yarasanın hareketlerini canlandırmak için Flame'in Efektleri'ni kullanacaksınız ve Flame'ı Flutter'ın durum yönetim sistemine nasıl entegre edeceğinizi göreceksiniz.
Tamamlandığında oyununuz, biraz daha yavaş olsa da bu animasyonlu GIF gibi görünecektir.
Neler öğreneceksiniz?
GameWidget
ile başlayarak Flame hakkındaki temel bilgilerin işleyiş şekli.- Oyun döngüsü nasıl kullanılır?
- Flame'in
Component
ürünleri nasıl çalışır? Bunlar Flutter'ınWidget
'lerine benzer. - Çarpışmaların nasıl ele alınacağı.
Component
canlandırması içinEffect
nasıl kullanılır?- Flame oyununun üzerine Flutter
Widget
'ları yerleştirme. - Flame, Flutter'ın durum yönetimiyle nasıl entegre edilir?
Neler oluşturacaksınız?
Bu codelab'de Flutter ve Flame kullanarak 2D bir oyun geliştireceksiniz. Tamamlandığında oyununuz aşağıdaki koşulları karşılamalıdır
- Flutter'ın desteklediği altı platformda da işlev görür: Android, iOS, Linux, macOS, Windows ve web
- Flame'in oyun döngüsünü kullanarak en az 60 fps'yi koruyun.
- 80'lerin arcade tarzı hissini vermek için
google_fonts
paketi veflutter_animate
gibi Flutter özelliklerini kullanın.
2. Flutter ortamınızı ayarlama
Düzenleyici
Bu codelab'i basitleştirmek için Visual Studio Code'un (VS Code) geliştirme ortamınız olduğu varsayılır. VS Code ücretsizdir ve önde gelen tüm platformlarda çalışır. Talimatlar varsayılan olarak VS Code'a özel kısayollardan alındığından bu codelab için VS Code'u kullanıyoruz. Görevler daha basit hale geliyor: "bu düğmeyi tıklayın" veya "X yapmak için bu tuşa basın" "X işlemini yapmak için düzenleyicinizde uygun işlemi yapın" yerine
Android Studio, diğer IntelliJ IDE'ler, Emacs, Vim veya Notepad++ uygulamaları arasından istediğiniz düzenleyiciyi kullanabilirsiniz. Hepsi Flutter ile çalışır.
Geliştirme hedefi seçin
Flutter, birçok platforma yönelik uygulamalar üretir. Uygulamanız aşağıdaki işletim sistemlerinden herhangi birinde çalışabilir:
- iOS
- Android
- Windows
- macOS
- Linux
- web
Geliştirme hedefi olarak tek bir işletim sistemi seçmek yaygın bir uygulamadır. Bu, uygulamanızın, geliştirme sırasında çalıştırılacağı işletim sistemidir.
Örneğin, Flutter uygulamanızı geliştirmek için Windows dizüstü bilgisayar kullandığınızı varsayalım. Ardından geliştirme hedefiniz olarak Android'i seçersiniz. Uygulamanızı önizlemek için Windows dizüstü bilgisayarınıza bir Android cihazı USB kablosuyla bağlarsınız. Geliştirme aşamasındaki uygulamanız, taktığınız Android cihazda veya bir Android emülatöründe çalışır. Geliştirme aşamasındaki uygulamanızı düzenleyicinizle birlikte bir Windows uygulaması olarak çalıştıran geliştirme hedefi olarak Windows'u seçmiş olabilirsiniz.
Geliştirme hedefiniz olarak web'i seçmek isteyebilirsiniz. Bunun, geliştirme sırasında olumsuz bir yönü vardır: Flutter'ın Durum Bilgili Hot Yeniden Yükleme özelliğini kaybedersiniz. Flutter şu anda web uygulamalarını çalışır durumda yeniden yükleyememektedir.
Devam etmeden önce seçiminizi yapın. Uygulamanızı daha sonra istediğiniz zaman diğer işletim sistemlerinde çalıştırabilirsiniz. Geliştirme hedefi seçmek bir sonraki adımı daha sorunsuz hale getirir.
Flutter'ı yükleme
Flutter SDK'sını yüklemeyle ilgili en güncel talimatları docs.flutter.dev adresinde bulabilirsiniz.
Flutter web sitesindeki talimatlar, SDK'nın yüklenmesini ve geliştirme hedefiyle ilgili araçlar ile düzenleyici eklentilerini kapsar. Bu codelab için şu yazılımı yükleyin:
- Flutter SDK'sı
- Flutter eklentisiyle Visual Studio Kodu
- Seçtiğiniz geliştirme hedefi için derleyici yazılımı. (Windows'u hedeflemek için Visual Studio, macOS veya iOS'i hedeflemek için ise Xcode gerekir.)
Bir sonraki bölümde ilk Flutter projenizi oluşturacaksınız.
Herhangi bir sorun gidermeniz gerekirse, bu sorulardan ve yanıtlardan (StackOverflow'dan) bazıları sorun giderme için yararlı olabilir.
Sık Sorulan Sorular
- Flutter SDK yolunu nasıl bulabilirim?
- Flutter komutu bulunamadığında ne yapmalıyım?
- "Başlangıç kilidinin açılması için başka bir fırıldak komutu bekleniyor" sorununu nasıl düzeltebilirim? sorun nedir?
- Flutter'a Android SDK yüklememin nerede olduğunu nasıl söyleyebilirim?
flutter doctor --android-licenses
çalıştırırken Java hatasını nasıl giderebilirim?- Bulunamadı Android
sdkmanager
aracını nasıl ele alabilirim? - "
cmdline-tools
bileşeni eksik" sorununu nasıl çözebilirim? hatası? - Apple Silicon (M1) cihazda CocoaPods'u nasıl çalıştırabilirim?
- VS Code'a kaydetme işleminde otomatik biçimlendirmeyi nasıl devre dışı bırakırım?
3. Proje oluşturma
İlk Flutter projenizi oluşturun
Bu işlem, VS Code'u açmayı ve Flutter uygulamasını seçtiğiniz bir dizinde oluşturmayı içerir.
- Visual Studio Code'u başlatın.
- Komut paletini açın (
F1
veyaCtrl+Shift+P
ya daShift+Cmd+P
) ve ardından "flutter new" yazın. Görüntülendiğinde Flutter: New Project komutunu seçin.
- Boş Uygulama'yı seçin. Projenizin oluşturulacağı dizini seçin. Bu dizin, üst düzey ayrıcalıklar gerektirmeyen veya yolunda boşluk bulunan herhangi bir dizin olmalıdır. Buna örnek olarak ana dizininiz veya
C:\src\
verilebilir.
- Projenize
brick_breaker
adını verin. Bu codelab'in geri kalanında uygulamanızabrick_breaker
adını verdiğiniz varsayılmaktadır.
Flutter artık proje klasörünüzü oluşturur ve VS Code bu klasörü açar. Şimdi uygulamanın temel bir iskeletini kullanarak iki dosyanın içeriğinin üzerine yazacaksınız.
Kopyala ve İlk uygulamayı yapıştırın
Bu işlem, bu codelab'de sağlanan örnek kodu uygulamanıza ekler.
- VS Code'un sol bölmesinde Explorer'i tıklayın ve
pubspec.yaml
dosyasını açın.
- Bu dosyanın içeriğini şununla değiştirin:
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flame: ^1.16.0
flutter:
sdk: flutter
flutter_animate: ^4.5.0
google_fonts: ^6.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
pubspec.yaml
dosyası; uygulamanızın mevcut sürümü, bağımlılıkları ve birlikte gönderileceği öğeler gibi uygulamanızla ilgili temel bilgileri belirtir.
main.dart
dosyasınılib/
dizininde aç.
- Bu dosyanın içeriğini şununla değiştirin:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- Her şeyin çalıştığını doğrulamak için bu kodu çalıştırın. Yalnızca boş siyah arka planı olan yeni bir pencere görüntülemelidir. Dünyanın en kötü video oyunu şu anda 60 fps'de oluşturuluyor!
4. Oyunu oluştur
Oyunun boyutunu küçültün
İki boyutlu (2D) olarak oynanan bir oyun için oyun alanı gerekir. Belirli boyutlarda bir alan oluşturacak ve daha sonra bu boyutları oyunun diğer yönlerini boyutlandırmak için kullanacaksınız.
Oyun alanının koordinatlarını düzenlemenin çeşitli yolları vardır. Tek bir kurala göre, başlangıç noktası ekranın ortasında (0,0)
olacak şekilde ekranın ortasından yönü ölçebilirsiniz. Pozitif değerler, öğeleri x ekseni boyunca sağa, y ekseni boyunca yukarı taşır. Bu standart, özellikle üç boyut içeren oyunların çoğu için geçerlidir.
Orijinal grup oyunu oluşturulurken kural, başlangıç noktasının sol üst köşeye ayarlanmasıydı. Pozitif x yönü aynı kaldı ancak y döndürüldü. X pozitif x yönü doğru, y aşağıydı. Bu oyun, çağa sadık kalmak için başlangıç noktasını sol üst köşeye yerleştiriyor.
lib/src
adlı yeni dizinde config.dart
adlı bir dosya oluşturun. Bu dosya, sonraki adımlarda daha fazla sabit değer kazanacak.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
Bu oyun 820 piksel genişliğinde ve 1.600 piksel yüksekliğinde olacak. Oyun alanı, görüntülendiği pencereye sığacak şekilde ölçeklendirilir ancak ekrana eklenen tüm bileşenler bu yüksekliğe ve genişliğe uygundur.
PlayArea oluşturma
Breakout oyununda top, oyun alanının duvarlarından zıplıyor. Çakışmaları karşılayabilmek için öncelikle bir PlayArea
bileşenine ihtiyacınız vardır.
lib/src/components
adlı yeni dizindeplay_area.dart
adlı bir dosya oluşturun.- Aşağıdakileri bu dosyaya ekleyin.
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Flutter'da Widget
, Flame'de ise Component
bulunur. Flutter uygulamaları widget'lardan oluşan bir ağaç, Flame oyunları ise bileşenlerin bakımını içerir.
Flutter ile Flame arasında ilginç bir fark var. Flutter'ın widget ağacı, kalıcı ve değişebilir RenderObject
katmanını güncellemek için kullanılmak üzere oluşturulmuş, geçici bir açıklamadır. Flame'in bileşenleri kalıcı ve değişebilir. Geliştiricinin bu bileşenleri simülasyon sisteminin parçası olarak kullanması beklenmektedir.
Flame'in bileşenleri, oyun mekaniklerini ifade etmek için optimize edilmiştir. Bu codelab, sonraki adımda gösterilen oyun döngüsüyle başlar.
- Karmaşıklığı kontrol etmek için bu projedeki tüm bileşenleri içeren bir dosya ekleyin.
lib/src/components
klasöründe bircomponents.dart
dosyası oluşturun ve aşağıdaki içeriği ekleyin.
lib/src/components/components.dart
export 'play_area.dart';
export
yönergesi, import
ile ters rol oynar. Bu dosyanın başka bir dosyaya içe aktarıldığında hangi işlevleri sunduğunu açıklar. Aşağıdaki adımlarda yeni bileşenler ekledikçe bu dosyada daha fazla giriş olacaktır.
Flame oyunu oluştur
Önceki adımdaki kırmızı kısa çizgileri yok etmek amacıyla Flame'in FlameGame
özelliği için yeni bir alt sınıf oluşturun.
lib/src
içindebrick_breaker.dart
adlı bir dosya oluşturun ve aşağıdaki kodu ekleyin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
}
}
Bu dosya, oyunun işlemlerini koordine eder. Oyun örneği oluşturulurken bu kod, oyunu sabit çözünürlükte oluşturma kullanacak şekilde yapılandırır. Oyun, resmi içeren ekranı kaplayacak şekilde yeniden boyutlandırılır ve gerektiği şekilde sinemaskop eklenir.
PlayArea
gibi alt bileşenlerin kendilerini uygun boyuta ayarlayabilmeleri için oyunun genişliğini ve yüksekliğini gösteriyorsunuz.
Geçersiz kılınan onLoad
yönteminde kodunuz iki işlem gerçekleştirir.
- Sol üst kısmı vizör için sabitleyici olarak yapılandırır. Varsayılan olarak vizör
(0,0)
için çapa olarak alanın ortasını kullanır. PlayArea
öğesiniworld
öğesine ekler. Dünya, oyun dünyasını temsil eder.CameraComponent
adlı çocuğun görünüm dönüşümü sırasında tüm alt öğelerini yansıtır.
Oyunu ekranda gösterin
Bu adımda yaptığınız tüm değişiklikleri görmek için lib/main.dart
dosyanızı aşağıdaki değişikliklerle güncelleyin.
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'src/brick_breaker.dart'; // Add this import
void main() {
final game = BrickBreaker(); // Modify this line
runApp(GameWidget(game: game));
}
Bu değişiklikleri yaptıktan sonra oyunu yeniden başlatın. Oyun aşağıdaki resme benzeyecektir.
Sonraki adımda dünyaya bir top ekleyecek ve harekete geçireceksiniz!
5. Topu göster
Top bileşenini oluşturma
Ekrana hareket eden bir top yerleştirmek, başka bir bileşen oluşturmayı ve onu oyun dünyasına eklemeyi içerir.
lib/src/config.dart
dosyasının içeriğini aşağıdaki gibi düzenleyin.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02; // Add this constant
Adlandırılmış sabitlerin türetilen değerler olarak tanımlanmasını sağlayan tasarım kalıbı bu codelab'de birçok kez döndürülecektir. Bu şekilde üst seviye gameWidth
ve gameHeight
öğelerini düzenleyerek oyunun görünümünü ve tarzının nasıl değiştiğini keşfedebilirsiniz.
Ball
bileşeninilib/src/components
içindekiball.dart
adlı bir dosyada oluşturun.
lib/src/components/ball.dart
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Ball extends CircleComponent {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
}
Daha önce, PlayArea
öğesini RectangleComponent
kullanarak tanımlamıştınız. Bu nedenle, daha fazla şekil var. CircleComponent
, RectangleComponent
gibi, PositionedComponent
alanından türetildiğinden topu ekranda konumlandırabilirsiniz. Daha da önemlisi, düğmenin konumu güncellenebilir.
Bu bileşen, velocity
veya zaman içinde konum değişikliği kavramını tanıtır. Hız Vector2
bir nesnedir, hız hem hız hem yöndür. Konumu güncellemek için oyun motorunun her karede çağırdığı update
yöntemini geçersiz kılın. dt
, önceki kare ile bu kare arasındaki süredir. Bu sayede farklı kare hızları (60 Hz veya 120 Hz) ya da aşırı hesaplama nedeniyle uzun kareler gibi faktörlere uyum sağlayabilirsiniz.
position += velocity * dt
güncellemesine çok dikkat edin. Zaman içinde münferit bir hareket simülasyonunu güncellemeyi bu şekilde uygularsınız.
Ball
bileşenini bileşen listesine dahil etmek içinlib/src/components/components.dart
dosyasını aşağıdaki gibi düzenleyin.
lib/src/components/components.dart
export 'ball.dart'; // Add this export
export 'play_area.dart';
Topu dünyaya katma
Bir topun var. Bunu dünyaya yerleştirelim ve oyun alanında gezinecek şekilde ayarlayalım.
lib/src/brick_breaker.dart
dosyasını aşağıdaki gibi düzenleyin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math; // Add this import
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random(); // Add this variable
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball( // Add from here...
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
debugMode = true; // To here.
}
}
Bu değişiklik, Ball
bileşenini world
öğesine ekler. Topun position
değerini, görüntülenme alanının ortasına ayarlamak için Vector2
, Vector2
ölçeğini skaler bir değere göre ölçeklendirmek için operatör aşırı yüklemeleri (*
ve /
) içerdiğinden, kod ilk olarak oyunun boyutunu yarıya indirir.
Topun velocity
seviyesini ayarlamak daha karmaşık. Amaç topu ekrandan rastgele bir yönde makul bir hızda aşağı hareket ettirmektir. normalized
yöntemine yapılan çağrı, orijinal Vector2
ile aynı yöne ayarlanmış ancak 1 mesafeye kadar ölçeklendirilen bir Vector2
nesnesi oluşturur. Bu, top hangi yöne giderse gitsin topun hızını korur. Ardından topun hızı, oyunun yüksekliğinin 1/4'ü kadar olacak şekilde ölçeklendirilir.
Bu çeşitli değerleri doğru şekilde vermek, sektörde oyun testi olarak da bilinen bir miktar yinelemeyi gerektirir.
Son satır, hata ayıklama ekranını açar. Bu da hata ayıklamaya yardımcı olacak ek bilgileri gösterir.
Şu anda oyunu çalıştırdığınızda oyun aşağıdaki ekrana benzeyecektir.
Hem PlayArea
bileşeni hem de Ball
bileşeninde hata ayıklama bilgileri vardır ancak arka plan matları PlayArea
sayılarını kırpar. Her şeyde hata ayıklama bilgilerinin görünmesinin nedeni, bileşen ağacının tamamı için debugMode
özelliğini etkinleştirmiş olmanızdır. Daha yararlı olacaksa yalnızca seçili bileşenler için hata ayıklama özelliğini de açabilirsiniz.
Oyununuzu birkaç kez yeniden başlatırsanız topun beklendiği gibi duvarlarla etkileşime girmediğini fark edebilirsiniz. Bu efekti sağlamak için bir sonraki adımda yapacağınız çakışma algılama özelliğini eklemeniz gerekir.
6. Zıplama
Çarpışma algılama ekle
Çarpışma algılama özelliği, iki nesne birbiriyle temas ettiğinde oyununuzun tanıdığı bir davranış ekler.
Oyuna çarpışma algılama özelliği eklemek için HasCollisionDetection
mix'ini BrickBreaker
oyununa aşağıdaki kodda gösterildiği şekilde ekleyin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
debugMode = true;
}
}
Bu, bileşenlerin isabet kutularını izler ve her oyun bildiriminde çarpışma geri çağırmalarını tetikler.
Oyunun isabet kutularını doldurmaya başlamak için PlayArea
bileşenini aşağıda gösterildiği gibi değiştirin.
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
children: [RectangleHitbox()], // Add this parameter
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Bir RectangleHitbox
bileşeni RectangleComponent
alt öğesi olarak eklendiğinde, üst bileşenin boyutuyla eşleşen çarpışma algılama için bir isabet kutusu oluşturulur. Üst bileşenden daha küçük veya daha büyük bir isabet kutusu istediğiniz zamanlar için RectangleHitbox
için relative
adlı bir fabrika oluşturucu vardır.
Topa zıplayın
Çarpışma algılama özelliğinin eklenmesi şu ana kadar oynanabilirlikte herhangi bir fark yaratmadı. Ball
bileşeninde değişiklik yaptığınızda değişiklik olur. Topun PlayArea
ile çarpıştığında davranışının değişmesi gerekir.
Ball
bileşenini aşağıdaki şekilde değiştirin.
lib/src/components/ball.dart
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart'; // And this import
import 'play_area.dart'; // And this one too
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> { // Add these mixins
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]); // Add this parameter
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override // Add from here...
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
removeFromParent();
}
} else {
debugPrint('collision with $other');
}
} // To here.
}
Bu örnekte, onCollisionStart
geri çağırmasının eklenmesiyle büyük bir değişiklik meydana gelir. Önceki örnekte BrickBreaker
işlevine eklenen çarpışma algılama sistemi bu geri çağırmayı çağırır.
İlk olarak, kod, Ball
etiketinin PlayArea
ile çakışıp çakışmadığını test eder. Oyun dünyasında başka hiçbir bileşen olmadığından bu şimdilik gereksiz görünüyor. Bu, bir sonraki adımda, dünyaya bir yarasa eklediğinizde değişecek. Ardından, top, sopa dışındaki şeylerle çarptığında uygulanacak bir else
koşulu da ekler. Geri kalan mantığınızı uygulamak istiyorsanız nazik bir hatırlatma.
Top, alt duvarla çarpıştığında oyun yüzeyinden kaybolur ve epey görünümdedir. Gelecekteki bir adımda bu yapıyı Flame's Effects'in gücünü kullanarak işleyeceksiniz.
Artık top oyunun duvarlarıyla çarpıştığına göre, oyuncuya vuruş yapması için kriket sopası vermek kesinlikle faydalı olur...
7. Beyzbol sopası
Vuruşu yapın
Oyun içinde topu oyun içinde tutmak amacıyla kriket sopası eklemek için
lib/src/config.dart
dosyasına aşağıdaki gibi bazı sabit değerler ekleyin.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2; // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05; // To here.
batHeight
ve batWidth
sabitleri yeterince açıklayıcıdır. Öte yandan, batStep
sabiti için açıklama yapılması gerekir. Oyuncu, bu oyunda topla etkileşimde bulunmak için platforma bağlı olarak sopası fare veya parmağıyla sürükleyebilir veya klavyeyi kullanabilir. batStep
sabit değeri, her bir sol veya sağ ok tuşuna basıldığında vuruşun ne kadar uzağa basacağını yapılandırır.
Bat
bileşen sınıfını aşağıdaki gibi tanımlayın.
lib/src/components/bat.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class Bat extends PositionComponent
with DragCallbacks, HasGameReference<BrickBreaker> {
Bat({
required this.cornerRadius,
required super.position,
required super.size,
}) : super(
anchor: Anchor.center,
children: [RectangleHitbox()],
);
final Radius cornerRadius;
final _paint = Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill;
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRRect(
RRect.fromRectAndRadius(
Offset.zero & size.toSize(),
cornerRadius,
),
_paint);
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
position.x = (position.x + event.localDelta.x).clamp(0, game.width);
}
void moveBy(double dx) {
add(MoveToEffect(
Vector2((position.x + dx).clamp(0, game.width), position.y),
EffectController(duration: 0.1),
));
}
}
Bu bileşen birkaç yeni özellik sunar.
İlk olarak Yarasa bileşeni RectangleComponent
veya CircleComponent
değil, PositionComponent
. Bu durum, bu kodun ekranda Bat
öğesini oluşturması gerektiği anlamına gelir. Bunu gerçekleştirmek için render
geri çağırmasını geçersiz kılar.
canvas.drawRRect
(yuvarlatılmış dikdörtgen çizme) çağrısına yakından baktığınızda, kendinize "dikdörtgen nerede?" diye sorabilirsiniz. Offset.zero & size.toSize()
, Rect
oluşturan dart:ui
Offset
sınıfında operator &
aşırı yükünden yararlanıyor. Bu steno ilk başta kafanızı karıştırabilir, ancak genellikle alt seviyedeki Flutter ve Flame kodunda görürsünüz.
İkincisi, bu Bat
bileşeni platforma bağlı olarak parmak veya fare kullanılarak sürüklenebilir. Bu işlevi uygulamak için DragCallbacks
mix'ini ekler ve onDragUpdate
etkinliğini geçersiz kılarsınız.
Son olarak, Bat
bileşeninin klavye kontrolüne yanıt vermesi gerekir. moveBy
işlevi, diğer kodun bu sopaya belirli bir sayıda sanal piksel sola veya sağa hareket etmesini bildirmesini sağlar. Bu işlev, Flame oyun motorunun yeni bir özelliğini sunar: Effect
MoveToEffect
nesnesini bu bileşenin alt öğesi olarak eklediğinizde oyuncu, sopayı yeni bir konuma canlandırılmış şekilde görür. Flame'de çeşitli efektler gerçekleştirmek için kullanabileceğiniz Effect
koleksiyonu vardır.
Efektin oluşturucu bağımsız değişkenleri, game
alıcısına bir referans içerir. Bu nedenle, HasGameReference
mix'ini bu sınıfa dahil ediyorsunuz. Bu karışım, bileşen ağacının en üstündeki BrickBreaker
örneğine erişmek için bu bileşene tür güvenli bir game
erişimcisi ekler.
Bat
öğesininBrickBreaker
tarafından kullanılabilmesi içinlib/src/components/components.dart
dosyasını aşağıdaki şekilde güncelleyin.
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart'; // Add this export
export 'play_area.dart';
Sopayı dünyaya ekleyin
Bat
bileşenini oyun dünyasına eklemek için BrickBreaker
öğesini aşağıdaki gibi güncelleyin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart'; // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart'; // And this import
import 'package:flutter/services.dart'; // And this
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat( // Add from here...
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95))); // To here
debugMode = true;
}
@override // Add from here...
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
} // To here
}
KeyboardEvents
mix'inin eklenmesi ve geçersiz kılınan onKeyEvent
yöntemi, klavye girişini işler. Sopayı uygun adım miktarı kadar hareket ettirmek için daha önce eklediğiniz kodu geri çağırın.
Eklenen kod parçasının geri kalan kısmı, kriket sopasını oyun dünyasına uygun konumda ve doğru oranlarda ekler. Tüm bu ayarların bu dosyada gösterilmesi, oyuna uygun hissi vermek için vuruşun ve topun göreceli boyutunu düzenlemenizi kolaylaştırır.
Bu noktada oyunu oynarsanız topa müdahale etmek için sopası hareket ettirebileceğinizi ancak Ball
ürününün çarpışma algılama kodunda bıraktığınız hata ayıklama kaydı dışında görünür bir yanıt alamadığınızı görürsünüz.
Şimdi bu sorunu düzeltmenin zamanı. Ball
bileşenini aşağıdaki gibi düzenleyin.
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart'; // Add this import
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart'; // And this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect( // Modify from here...
delay: 0.35,
));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else { // To here.
debugPrint('collision with $other');
}
}
}
Bu kod değişiklikleri iki ayrı sorunu çözer.
İlk olarak, ekranın alt kısmına dokunduğu anda topun yerlerinden fırlayan topu sabitler. Bu sorunu düzeltmek için removeFromParent
çağrısını RemoveEffect
ile değiştirirsiniz. RemoveEffect
, görüntülenebilir oyun alanından çıkmasına izin verdikten sonra topu oyun dünyasından kaldırır.
İkinci olarak, bu değişiklikler vuruş ve top arasındaki çarpışmanın yol açtığı etkiyi giderir. Bu işleme kodu oyuncunun lehine oldukça işe yarar. Oyuncu sopayla topa dokunduğu sürece, top ekranın üst kısmına döner. Bu durumun çok hoşunuza gittiğini düşünüyor ve daha gerçekçi bir deneyim yaşamak istiyorsanız bu işlemeyi oyununuza daha uygun olacak şekilde değiştirin.
velocity
güncellemesinin karmaşıklığını belirtmekte fayda var. Bu, duvar çarpışmalarında yapıldığı gibi hızın sadece y
bileşenini tersine çevirmez. Ayrıca x
bileşenini, temas anındaki sopa ve topun göreli konumuna bağlı olarak günceller. Bu, oyuncunun topun ne yaptığı konusunda daha fazla kontrol sahibi olmasını sağlar, ancak oyun dışında hiçbir şekilde oyuncuya tam olarak nasıl açıklanmaz?
Artık elinizde topa vuracak bir sopasınız olduğuna göre, topla kırılacak birkaç tuğla daha olması güzel olurdu.
8. Duvarı yıkın
Tuğlaları oluşturma
Oyuna tuğla eklemek için:
lib/src/config.dart
dosyasına aşağıdaki gibi bazı sabit değerler ekleyin.
lib/src/config.dart
import 'package:flutter/material.dart'; // Add this import
const brickColors = [ // Add this const
Color(0xfff94144),
Color(0xfff3722c),
Color(0xfff8961e),
Color(0xfff9844a),
Color(0xfff9c74f),
Color(0xff90be6d),
Color(0xff43aa8b),
Color(0xff4d908e),
Color(0xff277da1),
Color(0xff577590),
];
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015; // Add from here...
final brickWidth =
(gameWidth - (brickGutter * (brickColors.length + 1)))
/ brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03; // To here.
Brick
bileşenini aşağıdaki gibi ekleyin.
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Şu ana kadar bu kodun büyük bir kısmı tanıdık gelecektir. Bu kod, bileşen ağacının en üstünde hem çarpışma algılamayı hem de BrickBreaker
oyununa tür güvenli bir referans içeren bir RectangleComponent
kullanır.
Bu kodun sunduğu en önemli yeni kavram, oyuncunun kazanma koşulunu nasıl başardığıdır. Kazanma koşulu kontrolü tüm dünyada tuğla olup olmadığını sorgular ve yalnızca bir tanesinin kaldığını doğrular. Bu biraz kafa karıştırıcı olabilir çünkü önceki satır bu tuğlayı üst satırdan kaldırır.
Anlaşılması gereken en önemli nokta, bileşen kaldırma işleminin sıraya alınmış bir komut olmasıdır. Bu kod çalıştırıldıktan sonra, oyun dünyasındaki bir sonraki tıklamadan önce tuğlayı kaldırır.
Brick
bileşenine BrickBreaker
tarafından erişilebilmesi için lib/src/components/components.dart
öğesini aşağıdaki gibi düzenleyin.
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart';
export 'brick.dart'; // Add this export
export 'play_area.dart';
Dünyaya tuğlalar ekleyin
Ball
bileşenini aşağıdaki şekilde güncelleyin.
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart'; // Add this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier, // Add this parameter
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
final double difficultyModifier; // Add this member
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(
delay: 0.35,
));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) { // Modify from here...
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier); // To here.
}
}
}
Oyunun tek yeni yönü, tuğlaların her çarpışmasından sonra topun hızını artıran bir zorluk değiştiricisi. Bu ayarlanabilir parametrenin, oyununuza uygun zorluk eğrisinin bulunması için playtest olması gerekir.
BrickBreaker
oyununu aşağıdaki gibi düzenleyin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
difficultyModifier: difficultyModifier, // Add this argument
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
await world.addAll([ // Add from here...
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]); // To here.
debugMode = true;
}
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
}
}
Oyunu mevcut haliyle çalıştırırsanız tüm önemli oyun mekanikleri gösterilir. Hata ayıklamayı kapatıp tamamlandı olarak ayarlayabilirsiniz ancak bir şeyler eksik olabilir.
Karşılama ekranına, ekran karşısında oyun oynamaya ve belki de skora ne dersiniz? Flutter, bu özellikleri oyuna ekleyebilir. Bir sonraki aşamada dikkatinizi bu özelliğe kaydıracaksınız.
9. Oyunu kazanın
Oynatma durumları ekle
Bu adımda Flame oyununu bir Flutter sarmalayıcısının içine yerleştirecek ve ardından karşılama, oyuna başlama ve kazanma ekranları için Flutter yer paylaşımlarını ekleyeceksiniz.
İlk olarak, oyun ve bileşen dosyalarını değiştirerek yer paylaşımının gösterilip gösterilmeyeceğini ve gösterilecekse hangisinin gösterileceğini belirten bir oynatma durumu eklersiniz.
BrickBreaker
oyununu aşağıdaki gibi değiştirin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won } // Add this enumeration
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState; // Add from here...
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
} // To here.
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome; // Add from here...
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing; // To here.
world.add(Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
world.addAll([ // Drop the await
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
} // Drop the debugMode
@override // Add from here...
void onTap() {
super.onTap();
startGame();
} // To here.
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space: // Add from here...
case LogicalKeyboardKey.enter:
startGame(); // To here.
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf); // Add this override
}
Bu kod, BrickBreaker
oyununu önemli ölçüde değiştiriyor. playState
numaralandırmasını eklemek çok uğraşır. Bu, oyuncunun oyunu girdiği, oynadığı ve kaybettiği veya kazandığı durumları yakalar. Dosyanın üst kısmında numaralandırmayı tanımlar, ardından eşleşen alıcılar ve belirleyicilerle gizli bir durum olarak somutlaştırırsınız. Bu alıcılar ve belirleyiciler, oyunun çeşitli bölümleri oyun durumu geçişlerini tetiklediğinde yer paylaşımlarının değiştirilmesini sağlar.
Daha sonra, onLoad
içindeki kodu onLoad ve yeni bir startGame
yöntemine ayırdınız. Bu değişiklikten önce yalnızca yeni bir oyun başlatmak için oyunu yeniden başlatmanız gerekiyordu. Oyuncu, bu yeni eklemelerle artık çok fazla önlem almadan yeni bir oyuna başlayabilir.
Oyuncunun yeni bir oyun başlatmasına izin vermek için oyun için iki yeni işleyici yapılandırdınız. Kullanıcının birden fazla modda yeni bir oyun başlatabilmesi için dokunma işleyicisi ekleyip klavye işleyicisini genişlettiniz. Oyun durumu modellendiğinde, oyuncu kazandığında veya kaybettiğinde oynatma durumu geçişlerini tetiklemek için bileşenleri güncellemek mantıklıdır.
Ball
bileşenini aşağıdaki şekilde değiştirin.
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
final double difficultyModifier;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(
delay: 0.35,
onComplete: () { // Modify from here
game.playState = PlayState.gameOver;
})); // To here.
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) {
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier);
}
}
}
Bu küçük değişiklik, RemoveEffect
öğesine gameOver
oynatma durumunu tetikleyen bir onComplete
geri çağırması ekler. Oyuncu, topun ekranın alt kısmından dışarı çıkmasına izin veriyorsa bunu doğru bir şekilde hissedeceksiniz.
Brick
bileşenini aşağıdaki gibi düzenleyin.
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won; // Add this line
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Diğer yandan, oyuncu tüm tuğlaları kırabilirse bir "oyun kazanmış" olur. tıklayın. Tebrikler oyuncu, tebrikler!
Flutter sarmalayıcıyı ekleme
Oyunun yerleştirileceği bir yer sağlamak ve oynama durumu yer paylaşımları eklemek için Flutter kabuğunu ekleyin.
lib/src
altında birwidgets
dizini oluşturun.- Bir
game_app.dart
dosyası ekleyin ve aşağıdaki içeriği bu dosyaya ekleyin.
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
class GameApp extends StatelessWidget {
const GameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffa9d6e5),
Color(0xfff2e8cf),
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget.controlled(
gameFactory: BrickBreaker.new,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) => Center(
child: Text(
'TAP TO PLAY',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.gameOver.name: (context, game) => Center(
child: Text(
'G A M E O V E R',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.won.name: (context, game) => Center(
child: Text(
'Y O U W O N ! ! !',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
},
),
),
),
),
),
),
),
),
);
}
}
Bu dosyadaki çoğu içerik, standart bir Flutter widget ağacı derlemesini izler. Flame'e özgü bölümler, BrickBreaker
oyun örneğini oluşturmak ve yönetmek için GameWidget.controlled
kullanımını ve GameWidget
için yeni overlayBuilderMap
bağımsız değişkenini oluşturmayı içerir.
Bu overlayBuilderMap
öğesinin anahtarları, BrickBreaker
içindeki playState
belirleyicinin eklediği veya kaldırdığı yer paylaşımlarıyla uyumlu olmalıdır. Bu haritada bulunmayan bir bindirme ayarlamaya çalışmak, her yerde mutsuz yüzlere yol açıyor.
- Ekranda bu yeni işlevi kullanmak için
lib/main.dart
dosyasını aşağıdaki içerikle değiştirin.
lib/main.dart
import 'package:flutter/material.dart';
import 'src/widgets/game_app.dart';
void main() {
runApp(const GameApp());
}
Bu kodu iOS, Linux, Windows veya web'de çalıştırırsanız hedeflenen çıkış oyunda gösterilir. macOS veya Android'i hedeflerseniz google_fonts
ürününün görüntülenmesini sağlamak için son bir ince ayar yapmanız gerekir.
Yazı tipi erişimini etkinleştirme
Android için internet izni ekleme
Android için internet izni eklemeniz gerekir. AndroidManifest.xml
öğenizi aşağıdaki gibi düzenleyin.
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add the following line -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="brick_breaker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
macOS için yararlanma hakkı dosyalarını düzenleme
macOS'te düzenlenecek iki dosyanız vardır.
DebugProfile.entitlements
dosyasını aşağıdaki kodla eşleşecek şekilde düzenleyin.
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
Release.entitlements
dosyasını aşağıdaki kodla eşleşecek şekilde düzenleyin
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
Bu komut bu şekilde çalıştırıldığında tüm platformlarda bir karşılama ekranı ile bir oyunun oynandığı ya da kazanıldığı ekran görüntülenmelidir. Bu ekranlar biraz basit olabilir ve bir puan vermeniz iyi olacaktır. Bu yüzden bir sonraki adımda ne yapacağınızı tahmin edin!
10. Puanı tut
Maça skor ekleyin
Bu adımda, oyun skorunu etrafındaki Flutter bağlamına maruz bırakırsınız. Bu adımda, Flame oyunundan durumu etrafındaki Flutter durum yönetimine açıklayacaksınız. Bu, oyuncu her tuğla kırdığında oyun kodunun skoru güncellemesini sağlar.
BrickBreaker
oyununu aşağıdaki gibi değiştirin.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won }
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final ValueNotifier<int> score = ValueNotifier(0); // Add this line
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState;
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
}
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome;
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing;
score.value = 0; // Add this line
world.add(Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
world.addAll([
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
}
@override
void onTap() {
super.onTap();
startGame();
}
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.enter:
startGame();
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf);
}
Oyuna score
ekleyerek oyunun durumunu Flutter durum yönetimine bağlamış olursunuz.
- Oyuncu tuğlaları kırdığında puana bir puan eklemek için
Brick
sınıfını değiştirin.
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
game.score.value++; // Add this line
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won;
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Güzel görünen bir oyun yapın
Artık Flutter'da skoru saklayabildiğinize göre şimdi widget'ları birleştirip iyi görünmesini sağlayabilirsiniz.
lib/src/widgets
içindescore_card.dart
oluşturun ve aşağıdakileri ekleyin.
lib/src/widgets/score_card.dart
import 'package:flutter/material.dart';
class ScoreCard extends StatelessWidget {
const ScoreCard({
super.key,
required this.score,
});
final ValueNotifier<int> score;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: score,
builder: (context, score, child) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
child: Text(
'Score: $score'.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge!,
),
);
},
);
}
}
lib/src/widgets
hizmetindeoverlay_screen.dart
oluşturun ve aşağıdaki kodu ekleyin.
Bu sayede, flutter_animate
paketinin gücünü kullanarak yer paylaşımlı ekranlara hareket ve stil katabilirsiniz.
lib/src/widgets/overlay_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class OverlayScreen extends StatelessWidget {
const OverlayScreen({
super.key,
required this.title,
required this.subtitle,
});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
alignment: const Alignment(0, -0.15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
).animate().slideY(duration: 750.ms, begin: -3, end: 0),
const SizedBox(height: 16),
Text(
subtitle,
style: Theme.of(context).textTheme.headlineSmall,
)
.animate(onPlay: (controller) => controller.repeat())
.fadeIn(duration: 1.seconds)
.then()
.fadeOut(duration: 1.seconds),
],
),
);
}
}
flutter_animate
aracının gücünü daha ayrıntılı bir şekilde incelemek için Flutter'da yeni nesil kullanıcı arayüzleri oluşturma codelab'ine göz atın.
Bu kod, GameApp
bileşeninde birçok değişikliğe uğradı. İlk olarak, ScoreCard
ürününün score
öğesine erişebilmesi için StatelessWidget
türünü StatefulWidget
biçimine dönüştürürsünüz. Puan kartının eklenmesi için skoru oyunun üst kısmına taşımak için Column
eklenmesi gerekir.
İkinci olarak, karşılama, oyuna başlama ve kazanılan deneyimleri iyileştirmek için yeni OverlayScreen
widget'ını eklediniz.
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart'; // Add this import
import 'score_card.dart'; // And this one too
class GameApp extends StatefulWidget { // Modify this line
const GameApp({super.key});
@override // Add from here...
State<GameApp> createState() => _GameAppState();
}
class _GameAppState extends State<GameApp> {
late final BrickBreaker game;
@override
void initState() {
super.initState();
game = BrickBreaker();
} // To here.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffa9d6e5),
Color(0xfff2e8cf),
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column( // Modify from here...
children: [
ScoreCard(score: game.score),
Expanded(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget(
game: game,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) =>
const OverlayScreen(
title: 'TAP TO PLAY',
subtitle: 'Use arrow keys or swipe',
),
PlayState.gameOver.name: (context, game) =>
const OverlayScreen(
title: 'G A M E O V E R',
subtitle: 'Tap to Play Again',
),
PlayState.won.name: (context, game) =>
const OverlayScreen(
title: 'Y O U W O N ! ! !',
subtitle: 'Tap to Play Again',
),
},
),
),
),
),
],
), // To here.
),
),
),
),
),
);
}
}
Tüm bunlar tamamlandığında bu oyunu artık altı Flutter hedef platformundan birinde çalıştırabiliyor olmanız gerekir. Oyun aşağıdakine benzeyecektir.
11. Tebrikler
Tebrikler, Flutter ve Flame ile başarılı bir oyun geliştirdiniz.
Flame 2D oyun motorunu kullanarak bir oyun geliştirdiniz ve bu oyunu Flutter sarmalayıcıya yerleştirdiniz. Bileşenleri canlandırmak ve kaldırmak için Flame'in Efektlerini kullandınız. Oyunun tamamının iyi tasarlanmış görünmesini sağlamak için Google Fonts ve Flutter Animate paketlerini kullandınız.
Sırada ne var?
Bu codelab'lerden bazılarına göz atın...
- Flutter'da yeni nesil kullanıcı arayüzleri geliştirme
- Sıkıcı olan Flutter uygulamanızı güzelleştirin
- Flutter uygulamanıza uygulama içi satın alma ekleme