1. Giriş
Flame, Flutter tabanlı bir 2D oyun motorudur. Bu codelab'de, 1970'lerin klasik video oyunlarından biri olan Steve Wozniak'ın Breakout oyunundan esinlenerek bir oyun geliştireceksiniz. Yarasa, top ve tuğlaları çizmek için Flame'in bileşenlerini kullanacaksınız. Yarasanın hareketini canlandırmak için Flame'in efektlerini kullanacak ve Flame'i Flutter'ın durum yönetimi sistemiyle nasıl entegre edeceğinizi göreceksiniz.
Tamamlandığında oyununuz, biraz daha yavaş olsa da bu animasyonlu GIF'e benzemelidir.
Neler öğreneceksiniz?
GameWidget
ile başlayarak Flame'in temel işleyiş şekli.- Oyun döngüsü nasıl kullanılır?
- Flame'in
Component
s işleyiş şekli. Bunlar, Flutter'dakiWidget
'lara benzer. - Çakışmaların nasıl ele alınacağı.
Effect
kullanarakComponent
öğelerini nasıl canlandıracağınızı öğrenin.- Flutter
Widget
'ı Flame oyununun üzerine yerleştirme - Flame'i Flutter'ın durum yönetimiyle entegre etme
Ne oluşturacaksınız?
Bu codelab'de Flutter ve Flame kullanarak 2 boyutlu bir oyun oluşturacaksınız. Tamamlandığında oyununuz aşağıdaki koşulları karşılamalıdır:
- Flutter'ın desteklediği altı platformda (Android, iOS, Linux, macOS, Windows ve web) çalışır.
- Flame'in oyun döngüsünü kullanarak en az 60 fps'yi koruyun.
- 80'li yılların atari oyunlarının atmosferini yeniden yaratmak için
google_fonts
paketi veflutter_animate
gibi Flutter özelliklerini kullanın.
2. Flutter ortamınızı kurma
Düzenleyici
Bu codelab'i basitleştirmek için geliştirme ortamınızın Visual Studio Code (VS Code) olduğu varsayılır. VS Code ücretsizdir ve tüm önemli platformlarda çalışır. Talimatlarda varsayılan olarak VS Code'a özgü kısayollar kullanıldığından bu codelab'de VS Code'u kullanıyoruz. Görevler daha basit hale gelir: "X işlemini yapmak için düzenleyicinizde uygun işlemi yapın" yerine "X işlemini yapmak için bu tuşa basın" veya "bu düğmeyi tıklayın" gibi ifadeler kullanılır.
Android Studio, diğer IntelliJ IDE'leri, Emacs, Vim veya Notepad++ gibi istediğiniz düzenleyiciyi kullanabilirsiniz. Bunların hepsi Flutter ile çalışır.
Geliştirme hedefi seçme
Flutter, birden fazla platform için uygulamalar üretir. Uygulamanız aşağıdaki işletim sistemlerinden herhangi birinde çalışabilir:
- iOS
- Android
- Windows
- macOS
- Linux
- web
Geliştirme hedefiniz olarak tek bir işletim sistemi seçmek yaygın bir uygulamadır. Bu, uygulamanızın geliştirme sırasında üzerinde çalıştığı işletim sistemidir.
Örneğin, Flutter uygulamanızı geliştirmek için bir Windows dizüstü bilgisayar kullandığınızı ve geliştirme hedefi olarak Android'i seçtiğinizi varsayalım. Uygulamanızı önizlemek için bir Android cihazı USB kablosuyla Windows dizüstü bilgisayarınıza bağlarsınız ve geliştirme aşamasındaki uygulamanız bu bağlı Android cihazda veya bir Android emülatöründe çalışır. Geliştirme hedefi olarak Windows'u seçmiş olabilirsiniz. Bu durumda, geliştirme aşamasındaki uygulamanız düzenleyicinizin yanında bir Windows uygulaması olarak çalışır.
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ı kolaylaştırır.
Flutter'ı yükleme
Flutter SDK'yı yüklemeyle ilgili en güncel talimatları docs.flutter.dev adresinde bulabilirsiniz.
Flutter web sitesindeki talimatlarda SDK'nın, geliştirme hedefiyle ilgili araçların ve düzenleyici eklentilerinin yüklenmesi ele alınır. Bu codelab için aşağıdaki yazılımları yükleyin:
- Flutter SDK'sı
- Flutter eklentisiyle Visual Studio Code
- 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 Xcode gerekir)
Sonraki bölümde ilk Flutter projenizi oluşturacaksınız.
Sorun gidermeniz gerekiyorsa StackOverflow'daki bu soru ve yanıtlardan yararlanabilirsiniz.
Sık Sorulan Sorular
- Flutter SDK yolunu nasıl bulabilirim?
- Flutter komutu bulunamadığında ne yapmalıyım?
- "Başlangıç kilidinin serbest bırakılması için başka bir Flutter komutu bekleniyor" sorununu nasıl düzeltebilirim?
- Flutter'a Android SDK yüklememin nerede olduğunu nasıl söyleyebilirim?
flutter doctor --android-licenses
çalıştırılırken Java hatasıyla nasıl başa çıkabilirim?- Android
sdkmanager
aracı bulunamadı hatasıyla nasıl başa çıkarım? - "
cmdline-tools
bileşeni eksik" hatasıyla nasıl başa çıkabilirim? - Apple Silicon (M1) üzerinde CocoaPods'u nasıl çalıştırırım?
- VS Code'da kaydederken otomatik biçimlendirmeyi nasıl devre dışı bırakabilirim?
3. Proje oluşturma
İlk Flutter projenizi oluşturma
Bu işlem için VS Code'u açıp seçtiğiniz bir dizinde Flutter uygulaması şablonunu oluşturmanız gerekir.
- Visual Studio Code'u başlatın.
- Komut paletini açın (
F1
veyaCtrl+Shift+P
veyaShift+Cmd+P
) ve "flutter new" yazın. Göründüğünde Flutter: New Project (Flutter: Yeni Proje) komutunu seçin.
- Empty Application'ı (Uygulamayı Boşalt) seçin. Projenizi oluşturacağınız bir dizin seçin. Bu, yükseltilmiş ayrıcalıklar gerektirmeyen veya yolunda boşluk olmayan herhangi bir dizin olmalıdır. Örneğin, ana dizininiz veya
C:\src\
.
- Projenizi adlandırın
brick_breaker
. Bu codelab'in geri kalanında uygulamanızabrick_breaker
adını verdiğiniz varsayılır.
Flutter artık proje klasörünüzü oluşturur ve VS Code bu klasörü açar. Şimdi iki dosyanın içeriğini uygulamanın temel iskeletiyle değiştireceksiniz.
İlk uygulamayı kopyalayıp yapıştırma
Bu işlem, bu codelab'de sağlanan örnek kodu uygulamanıza ekler.
- VS Code'un sol bölmesinde Explorer'ı tıklayın ve
pubspec.yaml
dosyasını açın.
- Bu dosyanın içeriğini aşağıdakiyle değiştirin:
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: "none"
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
flame: ^1.28.1
flutter_animate: ^4.5.2
google_fonts: ^6.2.1
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
pubspec.yaml
dosyası, uygulamanızla ilgili temel bilgileri (ör. mevcut sürümü, bağımlılıkları ve birlikte gönderileceği öğeler) belirtir.
main.dart
dosyasınılib/
dizininde açın.
- Bu dosyanın içeriğini aşağıdakiyle 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 bir arka planın olduğu yeni bir pencere görünmelidir. Dünyanın en kötü video oyunu artık 60 fps'de işleniyor.
4. Oyunu oluşturma
Oyunu değerlendirme
İki boyutlu (2D) oynanan bir oyun için oyun alanı gerekir. Belirli boyutlarda bir alan oluşturacak ve ardından bu boyutları kullanarak oyunun diğer yönlerini boyutlandıracaksınız.
Oyun alanında koordinatları yerleştirmenin çeşitli yolları vardır. Bir kurala göre, ekranın merkezinde (0,0)
başlangıç noktası olacak şekilde ekranın merkezinden yön ölçebilirsiniz. Pozitif değerler, öğeleri x ekseni boyunca sağa ve y ekseni boyunca yukarı taşır. Bu standart, günümüzde çoğu oyun için, özellikle de üç boyutlu oyunlar için geçerlidir.
Orijinal Breakout oyunu oluşturulurken başlangıç noktası sol üst köşeye ayarlanmıştı. Pozitif x yönü aynı kalmış ancak y ters çevrilmiştir. Pozitif x yönü sağa, y yönü ise aşağıydı. Bu oyun, döneme uygun olarak başlangıç noktasını sol üst köşe olarak ayarlar.
lib/src
adlı yeni bir dizinde config.dart
adlı bir dosya oluşturun. Bu dosya, sonraki adımlarda daha fazla sabit kazanacaktır.
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çeklenir ancak ekrana eklenen tüm bileşenler bu yüksekliğe ve genişliğe uyar.
Oyun alanı oluşturma
Breakout oyununda top, oyun alanının duvarlarından seker. Çakışmaları önlemek için öncelikle bir PlayArea
bileşenine ihtiyacınız vardır.
lib/src/components
adlı yeni bir dizindeplay_area.dart
adlı bir dosya oluşturun.- Bu dosyaya aşağıdakileri 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
varken Flame'de Component
vardır. Flutter uygulamaları widget ağaçları oluşturmaktan oluşurken Flame oyunları bileşen ağaçlarını korumaktan oluşur.
Flutter ile Flame arasındaki ilginç fark da buradan kaynaklanır. Flutter'ın widget ağacı, kalıcı ve değiştirilebilir RenderObject
katmanını güncellemek için kullanılan geçici bir açıklamadır. Flame'in bileşenleri kalıcı ve değiştirilebilirdir. Geliştiricinin bu bileşenleri bir simülasyon sisteminin parçası olarak kullanması beklenir.
Flame'in bileşenleri, oyun mekaniklerini ifade etmek için optimize edilmiştir. Bu codelab, bir sonraki adımda yer alan oyun döngüsüyle başlayacak.
- Dağınıklığı kontrol etmek için bu projedeki tüm bileşenleri içeren bir dosya ekleyin.
lib/src/components
içinde bircomponents.dart
dosyası oluşturun ve aşağıdaki içeriği ekleyin.
lib/src/components/components.dart
export 'play_area.dart';
export
direktifi, import
direktifinin tersi bir rol oynar. Bu dosya, başka bir dosyaya aktarıldığında hangi işlevleri kullanıma sunduğunu belirtir. Bu dosyaya, sonraki adımlarda yeni bileşenler ekledikçe daha fazla giriş eklenecektir.
Flame oyunu oluşturma
Önceki adımdaki kırmızı dalgalı çizgileri kaldırmak için Flame'in FlameGame
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, bulunduğu ekranı dolduracak şekilde yeniden boyutlandırılır ve gerektiğinde letterboxing eklenir.
Oyunun genişliğini ve yüksekliğini göstererek PlayArea
gibi çocuk bileşenlerinin kendilerini uygun boyuta ayarlamasına olanak tanırsınız.
Geçersiz kılınan onLoad
yönteminde kodunuz iki işlem gerçekleştirir.
- Vizörün sabitleme noktası olarak sol üst köşeyi yapılandırır. Varsayılan olarak
viewfinder
,(0,0)
için alanın ortasını sabitleme noktası olarak kullanır. PlayArea
öğesiniworld
öğesine ekler. Dünya, oyun dünyasını temsil eder. Tüm alt öğeleriniCameraComponent
görünüm dönüşümü aracılığıyla yansıtır.
Oyunu ekrana getirme
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 şekle benzemelidir.
Sonraki adımda, dünyaya bir top ekleyip hareket ettireceksiniz.
5. Topu gösterme
Top bileşenini oluşturma
Ekrana hareketli bir top koymak için başka bir bileşen oluşturup oyun dünyasına eklemeniz gerekir.
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ış sabitleri türetilmiş değerler olarak tanımlama tasarım kalıbı, bu kod laboratuvarında birçok kez karşımıza çıkacak. Bu sayede, oyunun görünüm ve tarzının nasıl değiştiğini görmek için üst düzey gameWidth
ve gameHeight
öğelerini değiştirebilirsiniz.
Ball
bileşeninilib/src/components
içindeball.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ımladığınız için daha fazla şekil olması muhtemeldir. CircleComponent
, RectangleComponent
gibi, PositionedComponent
'den türetilir. Bu nedenle, topu ekranda konumlandırabilirsiniz. Daha da önemlisi, bu bölümün konumu güncellenebilir.
Bu bileşen, velocity
kavramını (zaman içinde konum değişikliği) tanıtır. Hız hem sürat hem de yön olduğundan hız bir Vector2
nesnesidir. 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 dikkat edin. Bu, hareketin zaman içindeki ayrı bir simülasyonunu güncelleme işlemini uygulama şeklinizdir.
Ball
bileşenini bileşen listesine eklemek içinlib/src/components/components.dart
dosyasını aşağıdaki şekilde düzenleyin.
lib/src/components/components.dart
export 'ball.dart'; // Add this export
export 'play_area.dart';
Topu dünyaya ekleme
You have a ball. Dünyaya yerleştirin ve oyun alanında hareket edecek şekilde ayarlayın.
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üleme alanının merkezine ayarlamak için kod, önce oyunun boyutunu yarıya indirir. Bunun nedeni, Vector2
değerinin bir Vector2
değerini skaler bir değerle ölçeklendirmek için operatör aşırı yüklemelerine (*
ve /
) sahip olmasıdır.
Topun velocity
ayarlanması daha karmaşık bir işlemdir. Amaç, topu ekranda rastgele bir yönde makul bir hızda hareket ettirmektir. normalized
yöntemine yapılan çağrı, orijinal Vector2
ile aynı yönde ayarlanmış ancak 1 birim uzaklığa küçültülmüş bir Vector2
nesnesi oluşturur. Bu sayede top hangi yöne giderse gitsin hızı sabit kalır. Topun hızı daha sonra oyunun yüksekliğinin 1/4'ü olacak şekilde ölçeklendirilir.
Bu çeşitli değerleri doğru şekilde elde etmek için yineleme yapmanız gerekir. Bu işlem, sektörde oyun testi olarak da bilinir.
Son satır, hata ayıklamaya yardımcı olmak için ekrana ek bilgiler ekleyen hata ayıklama ekranını açar.
Oyunu çalıştırdığınızda aşağıdaki ekran görüntüsüne benzer bir görüntüyle karşılaşmanız gerekir.
Hem PlayArea
bileşeninde hem de Ball
bileşeninde hata ayıklama bilgileri bulunur ancak arka plan matları PlayArea
bileşeninin sayılarını kırpar. Her şeyde hata ayıklama bilgilerinin gösterilmesinin nedeni, bileşen ağacının tamamı için debugMode
seçeneğini etkinleştirmenizdir. Daha faydalı olacağını düşünüyorsanız hata ayıklamayı yalnızca belirli bileşenler için de etkinleştirebilirsiniz.
Oyununuzu birkaç kez yeniden başlatırsanız topun duvarlarla beklendiği gibi etkileşime girmediğini fark edebilirsiniz. Bu efekti elde etmek için çarpışma algılama eklemeniz gerekir. Bunu bir sonraki adımda yapacaksınız.
6. Hemen çıkma oranı
Çarpışma algılama ekleme
Çarpışma algılama, oyununuzun iki nesnenin birbirine temas ettiğini tanıdığı bir davranış ekler.
Oyuna çarpışma algılama özelliği eklemek için aşağıdaki kodda gösterildiği gibi HasCollisionDetection
mixin'ini BrickBreaker
oyununa 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 çarpışma kutularını izler ve her oyun tikinde çarpışma geri çağırmalarını tetikler.
Oyunun çarpışma kutularını doldurmaya başlamak için PlayArea
bileşenini 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);
}
}
RectangleHitbox
bileşenini RectangleComponent
öğesinin alt öğesi olarak eklediğinizde, çarpışma algılama için üst bileşenin boyutuna uygun bir çarpışma kutusu oluşturulur. RectangleHitbox
için, üst bileşenden daha küçük veya daha büyük bir çarpışma kutusu istediğiniz zamanlarda kullanabileceğiniz relative
adlı bir fabrika oluşturucu vardır.
Topu sektirme
Çarpışma algılama özelliğinin eklenmesi, oyunun oynanışında şu ana kadar herhangi bir fark yaratmadı. Ball
bileşenini değiştirdiğinizde bu durum değişir. PlayArea
ile çarpıştığında değişmesi gereken topun davranışıdır.
Ball
bileşenini aşağıdaki gibi 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ğırma işlevinin eklenmesiyle önemli bir değişiklik yapılıyor. Önceki örnekte BrickBreaker
'ya eklenen çarpışma algılama sistemi bu geri çağırmayı çağırır.
Öncelikle kod, Ball
ile PlayArea
çarpışıp çarpışmadığını test eder. Oyun dünyasında başka bileşen olmadığı için bu şimdilik gereksiz görünüyor. Bir sonraki adımda, dünyaya yarasa eklediğinizde bu durum değişir. Ardından, topun sopadan farklı nesnelerle çarpışmasını ele almak için bir else
koşulu da ekler. Kalan mantığı uygulamanız için küçük bir hatırlatma.
Top alt duvarla çarpıştığında, görünür durumda olmasına rağmen oyun alanından kayboluyor. Bu öğeyi, Flame'in efektlerini kullanarak sonraki bir adımda işleyebilirsiniz.
Topun oyunun duvarlarıyla çarpışmasını sağladığınıza göre, oyuncuya topla vurabileceği bir sopa vermek faydalı olacaktır.
7. Get bat on ball
Yarasa oluşturma
Topun oyun içinde kalmasını sağlamak için sopa eklemek istiyorsanız:
lib/src/config.dart
dosyasına aşağıdaki gibi bazı sabitler 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. batStep
Sabit ise biraz açıklamaya ihtiyaç duyuyor. Bu oyunda topla etkileşim kurmak için oyuncu, platforma bağlı olarak sopayı fareyle veya parmağıyla sürükleyebilir ya da klavyeyi kullanabilir. batStep
sabiti, her sol veya sağ ok tuşuna basıldığında sopanın ne kadar ileri gideceğini 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, Bat bileşeni PositionComponent
'dır, RectangleComponent
veya CircleComponent
değildir. Bu, kodun ekranda Bat
öğesini oluşturması gerektiği anlamına gelir. Bunu yapmak için render
geri çağırma işlevini geçersiz kılar.
canvas.drawRRect
(yuvarlak dikdörtgen çiz) çağrısına yakından baktığınızda kendinize "Dikdörtgen nerede?" diye sorabilirsiniz. Offset.zero & size.toSize()
, Rect
oluşturmak için dart:ui
Offset
sınıfında operator &
aşırı yüklenmesinden yararlanır. Bu kısaltma ilk başta kafanızı karıştırabilir ancak alt düzey Flutter ve Flame kodlarında sık sık 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
mixin'ini ekleyip 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 kodların bu yarasanın belirli sayıda sanal piksel kadar sola veya sağa hareket etmesini sağlamasına olanak tanır. Bu işlev, Flame oyun motorunun yeni bir özelliğini kullanıma sunar: Effect
s. MoveToEffect
nesnesi bu bileşenin alt öğesi olarak eklendiğinde oyuncu, sopanın yeni bir konumda animasyonlu olarak gösterildiğini görür. Flame'de çeşitli efektler uygulamak için bir dizi Effect
bulunur.
Effect'in oluşturucu bağımsız değişkenleri, game
alıcısına bir referans içerir. Bu nedenle, bu sınıfa HasGameReference
mixini dahil edersiniz. Bu mixin, 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şimci ekler.
Bat
uygulamasınıBrickBreaker
için kullanılabilir hale getirmek üzerelib/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';
Yarasa modelini dünyaya ekleme
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( // Add from here...
Bat(
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
mixin'inin eklenmesi ve geçersiz kılınan onKeyEvent
yöntemi, klavye girişini işler. Yarasanın uygun adım miktarıyla hareket etmesini sağlamak için daha önce eklediğiniz kodu hatırlayın.
Eklenen kodun geri kalan kısmı, sopayı oyun dünyasına uygun konumda ve doğru oranlarda ekler. Bu ayarların tümünün bu dosyada yer alması, oyunun doğru hissini elde etmek için sopa ve topun göreceli boyutunu ayarlama işlemini kolaylaştırır.
Bu noktada oyunu oynarsanız topu yakalamak için sopayı hareket ettirebildiğinizi ancak Ball
'nın çarpışma algılama kodunda bıraktığınız hata ayıklama günlüğü dışında görünür bir yanıt almadığınızı görürsünüz.
Şimdi bu sorunu düzeltme 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(delay: 0.35)); // Modify from 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 { // To here.
debugPrint('collision with $other');
}
}
}
Bu kod değişiklikleri iki ayrı sorunu düzeltir.
İlk olarak, topun ekranın alt kısmına dokunduğu anda yok olmasını düzeltir. Bu sorunu düzeltmek için removeFromParent
çağrısını RemoveEffect
ile değiştirin. RemoveEffect
, topun görüntülenebilir oyun alanından çıkmasına izin verdikten sonra topu oyun dünyasından çıkarır.
İkinci olarak, bu değişiklikler, sopa ile top arasındaki çarpışmanın işlenmesini düzeltir. Bu işlem kodu, oyuncunun lehine olacak şekilde çalışır. Oyuncu sopayla topa dokunduğu sürece top ekranın üst kısmına geri döner. Bu durum çok affedici geliyorsa ve daha gerçekçi bir şey istiyorsanız bu kontrolü, oyununuzun nasıl hissettirmesini istediğinize daha iyi uyacak şekilde değiştirin.
velocity
güncellemesinin karmaşıklığını belirtmekte fayda var. Duvar çarpışmalarında olduğu gibi yalnızca hızın y
bileşenini tersine çevirmez. Ayrıca, x
bileşenini, top ve sopanın temas anındaki göreli konumuna bağlı olarak günceller. Bu sayede oyuncu, topun ne yapacağı konusunda daha fazla kontrol sahibi olur ancak bu kontrolün nasıl sağlanacağı, oyun dışında oyuncuya hiçbir şekilde bildirilmez.
Artık topla vurabileceğiniz bir sopanız olduğuna göre, topla kırabileceğiniz tuğlalar da ekleyelim.
8. Duvarı yık
Tuvaletleri oluşturma
Oyuna tuğla eklemek için:
lib/src/config.dart
dosyasına aşağıdaki gibi bazı sabitler 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>());
}
}
}
Bu kodun çoğu artık size tanıdık gelmelidir. Bu kod, bileşen ağacının en üstünde hem çarpışma algılama hem de BrickBreaker
oyununa tür güvenli referans içeren bir RectangleComponent
kullanır.
Bu kodun sunduğu en önemli yeni kavram, oyuncunun kazanma koşulunu nasıl yerine getirdiğidir. Kazanma koşulu kontrolü, dünyada tuğla olup olmadığını sorgular ve yalnızca bir tuğla kaldığını onaylar. Önceki satır bu tuğlayı üst öğesinden kaldırdığı için bu durum biraz kafa karıştırıcı olabilir.
Anlaşılması gereken en önemli nokta, bileşen kaldırma işleminin sıraya alınmış bir komut olduğudur. Bu kod çalıştırıldıktan sonra, ancak oyun dünyasının bir sonraki tik'inden önce tuğlayı kaldırır.
Brick
bileşenini BrickBreaker
için erişilebilir hale getirmek üzere lib/src/components/components.dart
öğesini aşağıdaki şekilde 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ğla ekleme
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.
}
}
}
Bu, tek yeni yönü, her tuğla çarpışmasından sonra top hızını artıran bir zorluk değiştiriciyi sunar. Bu ayarlanabilir parametrenin, oyununuza uygun zorluk eğrisini bulmak için oyun testine tabi tutulması gerekir.
BrickBreaker
oyununu aşağıdaki şekilde 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 çalıştırdığınızda tüm temel oyun mekanikleri gösterilir. Hata ayıklamayı kapatıp işi bitirebilirsiniz ancak bir şeylerin eksik olduğunu hissediyorsunuz.
Karşılama ekranı, oyun bitti ekranı ve skor eklemeye ne dersiniz? Flutter bu özellikleri oyuna ekleyebilir. Bir sonraki adımda bu özelliklere odaklanacaksınız.
9. Oyunu kazanma
Oynatma durumları ekleme
Bu adımda, Flame oyununu bir Flutter sarmalayıcısının içine yerleştirip karşılama, oyun bitti ve kazandı ekranları için Flutter yer paylaşımları ekleyeceksiniz.
Öncelikle, oyun ve bileşen dosyalarını değiştirerek bir yer paylaşımı gösterilip gösterilmeyeceğini ve gösterilecekse hangisinin gösterileceğini yansıtan bir oynatma durumu uygulayın.
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
oyununda önemli değişiklikler yapar. playState
numaralandırmasını eklemek çok fazla çalışma gerektirir. Bu metrik, oyuncunun oyuna girme, oyunu oynama ve oyunu kaybetme veya kazanma durumunu gösterir. Dosyanın üst kısmında numaralandırmayı tanımlar, ardından eşleşen alıcılar ve ayarlayıcılarla gizli bir durum olarak örneklendirirsiniz. Bu alıcılar ve ayarlayıcılar, oyunun çeşitli bölümleri oynatma durumu geçişlerini tetiklediğinde yer paylaşımlarının değiştirilmesini sağlar.
Ardından, onLoad
içindeki kodu onLoad ve yeni bir startGame
yöntemine bölersiniz. Bu değişiklikten önce yeni bir oyuna başlamak için oyunu yeniden başlatmanız gerekiyordu. Bu yeni eklemelerle oyuncu artık bu kadar sert önlemler almadan yeni bir oyuna başlayabilir.
Oyuncunun yeni bir oyuna başlamasına izin vermek için oyunla ilgili 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şleyici eklediniz ve klavye işleyiciyi genişlettiniz. Oynatma durumu modellendiğinde, oyuncu kazandığında veya kaybettiğinde oynatma durumu geçişlerini tetiklemek için bileşenlerin güncellenmesi mantıklı olacaktır.
Ball
bileşenini aşağıdaki gibi 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, onComplete
geri çağırmasını RemoveEffect
öğesine ekler. Bu geri çağırma, gameOver
oynatma durumunu tetikler. Oyunun, topun ekranın alt kısmından çıkmasına izin vermesi durumunda bu değer doğru olacaktır.
Brick
bileşenini aşağıdaki gibi düzenleyin.
lib/src/components/brick.dart
impimport '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>());
}
}
}
Öte yandan, oyuncu tüm tuğlaları kırabilirse "oyun kazanıldı" ekranını görür. Bravo oyuncu, bravo!
Flutter sarmalayıcısını ekleme
Oyunu yerleştirmek ve oynatma durumu katmanları eklemek için Flutter kabuğunu ekleyin.
widgets
dizininilib/src
altında oluşturun.- Bir
game_app.dart
dosyası ekleyin ve bu dosyaya aşağıdaki içeriği 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(
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 içeriklerin çoğu standart bir Flutter widget ağacı yapısını takip eder. Flame'e özgü kısımlar arasında GameWidget.controlled
kullanarak BrickBreaker
oyun örneğini oluşturma ve yönetme ile GameWidget
için yeni overlayBuilderMap
bağımsız değişkeni yer alır.
Bu overlayBuilderMap
'nın anahtarları, BrickBreaker
'daki playState
ayarlayıcısının eklediği veya kaldırdığı yer paylaşımlarıyla uyumlu olmalıdır. Bu haritada bulunmayan bir yer paylaşımı ayarlamaya çalışmak, her yerde mutsuz yüzlere yol açar.
- Bu yeni işlevselliği ekranda görmek 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 amaçlanan çıkış oyunda gösterilir. macOS veya Android'i hedefliyorsanız google_fonts
simgesinin gösterilmesi 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
içeriğinizi aşağıdaki şekilde 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 hak dosyalarını düzenleme
macOS'te düzenlemeniz gereken iki dosya 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 kodu olduğu gibi çalıştırmak, tüm platformlarda bir karşılama ekranı ve oyun bitti veya kazanma ekranı göstermelidir. Bu ekranlar biraz basit olabilir ve puan eklenmesi iyi olurdu. Dolayısıyla, bir sonraki adımda ne yapacağınızı tahmin edebilirsiniz.
10. Puan tutma
Oyuna puan ekleme
Bu adımda, oyun puanını çevreleyen Flutter bağlamına gösterirsiniz. Bu adımda, Flame oyunundaki durumu çevreleyen Flutter durum yönetimine sunarsınız. Bu, oyuncu her tuğlayı kırdığında oyun kodunun puanı 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ğlarsınız.
- Oyuncu tuğlaları kırdığında puana 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>());
}
}
}
İyi görünen bir oyun oluşturma
Artık Flutter'da skor tutabildiğinize göre, görünümü iyileştirmek için widget'ları bir araya getirme zamanı geldi.
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
içindeoverlay_screen.dart
oluşturun ve aşağıdaki kodu ekleyin.
Bu, yer paylaşımı ekranlarına hareket ve stil katmak için flutter_animate
paketinin gücünü kullanarak yer paylaşımlarına daha fazla şıklık katar.
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
gücünü daha ayrıntılı bir şekilde incelemek için Building next generation UIs in Flutter (Flutter'da yeni nesil kullanıcı arayüzleri oluşturma) adlı codelab'e göz atın.
Bu kod, GameApp
bileşeninde çok değişti. Öncelikle, ScoreCard
uygulamasının score
dosyasına erişmesini sağlamak için dosyayı StatelessWidget
biçiminden StatefulWidget
biçimine dönüştürürsünüz. Puan kartının eklenmesi için puanın oyunun üzerinde yer almasını sağlayacak bir Column
eklenmesi gerekir.
İkincisi, karşılama, oyun bitti ve kazanma deneyimlerini 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(
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.
),
),
),
),
),
);
}
}
Bu işlemlerin ardından, oyunu altı Flutter hedef platformunun herhangi birinde çalıştırabilirsiniz. Oyun aşağıdaki gibi görünmelidir.
11. Tebrikler
Tebrikler! Flutter ve Flame ile oyun oluşturmayı başardınız.
Flame 2D oyun motorunu kullanarak bir oyun geliştirdiniz ve bunu Flutter sarmalayıcısına 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ünmesi için Google Fonts ve Flutter Animate paketlerini kullandınız.
Sırada ne var?
Aşağıdaki codelab'lere göz atın:
- Flutter'da yeni nesil kullanıcı arayüzleri oluşturma
- Flutter uygulamanızı sıkıcı olmaktan çıkarıp güzelleştirme
- Flutter uygulamanıza uygulama içi satın alma işlemleri ekleme