Flutter ile Flame'e Giriş

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.

Oynanan bir oyunun ekran kaydı. Oyun önemli ölçüde hızlandırıldı.

Neler öğreneceksiniz?

  • GameWidget ile başlayarak Flame'in temel işleyiş şekli.
  • Oyun döngüsü nasıl kullanılır?
  • Flame'in Components işleyiş şekli. Bunlar, Flutter'daki Widget'lara benzer.
  • Çakışmaların nasıl ele alınacağı.
  • Effect kullanarak Component öğ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 ve flutter_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.

VS Code'da bir miktar Flutter kodu

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.

Dizüstü bilgisayara kabloyla bağlı bir telefon ve dizüstü bilgisayarın çizimi. Dizüstü bilgisayar,

Ö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:

  1. Flutter SDK'sı
  2. Flutter eklentisiyle Visual Studio Code
  3. 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

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.

  1. Visual Studio Code'u başlatın.
  2. Komut paletini açın (F1 veya Ctrl+Shift+P veya Shift+Cmd+P) ve "flutter new" yazın. Göründüğünde Flutter: New Project (Flutter: Yeni Proje) komutunu seçin.

VS Code ile

  1. 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\.

Yeni uygulama akışının bir parçası olarak seçili gösterilen Boş Uygulama ile VS Code

  1. Projenizi adlandırın brick_breaker. Bu codelab'in geri kalanında uygulamanıza brick_breaker adını verdiğiniz varsayılır.

VS Code ile

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.

  1. VS Code'un sol bölmesinde Explorer'ı tıklayın ve pubspec.yaml dosyasını açın.

VS Code'un, pubspec.yaml dosyasının konumunu vurgulayan oklar içeren kısmi ekran görüntüsü

  1. 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.

  1. main.dart dosyasını lib/ dizininde açın.

VS Code'un kısmi ekran görüntüsünde, main.dart dosyasının konumunu gösteren bir ok yer alıyor.

  1. 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));
}
  1. 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.

Tamamen siyah bir brick_breaker uygulama penceresini gösteren ekran görüntüsü.

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.

  1. lib/src/components adlı yeni bir dizinde play_area.dart adlı bir dosya oluşturun.
  2. 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.

  1. Dağınıklığı kontrol etmek için bu projedeki tüm bileşenleri içeren bir dosya ekleyin. lib/src/components içinde bir components.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.

  1. lib/src içinde brick_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.

  1. 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.
  2. PlayArea öğesini world öğesine ekler. Dünya, oyun dünyasını temsil eder. Tüm alt öğelerini CameraComponent 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.

Uygulama penceresinin ortasında kum rengi bir dikdörtgen bulunan brick_breaker uygulama penceresini gösteren ekran görüntüsü

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.

  1. 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.

  1. Ball bileşenini lib/src/components içinde ball.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.

  1. Ball bileşenini bileşen listesine eklemek için lib/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.

Kum rengi dikdörtgenin üzerinde mavi bir daire bulunan brick_breaker uygulama penceresini gösteren ekran görüntüsü. Mavi dairenin üzerinde, ekrandaki boyutunu ve konumunu gösteren sayılar yer alıyor.

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:

  1. 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.

  1. 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: Effects. 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.

  1. Bat uygulamasını BrickBreaker için kullanılabilir hale getirmek üzere lib/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:

  1. 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.
  1. 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.

Oyun alanında top, raket ve tuğlaların çoğunun bulunduğu brick_breaker oyununu gösteren ekran görüntüsü. Bileşenlerin her birinde hata ayıklama etiketleri bulunur.

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.

  1. 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.

  1. 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.

  1. 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.

  1. widgets dizinini lib/src altında oluşturun.
  2. 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.

  1. 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.

  1. 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>
  1. 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.

  1. 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.

  1. 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.

  1. lib/src/widgets içinde score_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!,
          ),
        );
      },
    );
  }
}
  1. lib/src/widgets içinde overlay_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.

brick_breaker oyununun, kullanıcıyı oyunu oynamak için ekrana dokunmaya davet eden oyun öncesi ekranını gösteren ekran görüntüsü

brick_breaker oyununda, oyun bitti ekranının bir sopa ve bazı tuğlaların üzerinde yer aldığı ekran görüntüsü

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:

Daha fazla bilgi