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?
  • Alevlerin Component işleyiş şekli. Bunlar, Flutter'daki Widget öğelerine benzer.
  • Çakışmalar nasıl ele alınır?
  • Effect kullanarak Component animasyonu yapma
  • Flutter Widget öğelerini 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 2D 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) işlev
  • Flame'in oyun döngüsünü kullanarak en az 60 fps hızını 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 büyük platformlarda çalışır. Talimatlarda varsayılan olarak VS Code'a özel 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.

Bir miktar Flutter kodu içeren VS Code

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 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 telefonun çizimi. Dizüstü bilgisayar,

Örneğin, Flutter uygulamanızı geliştirmek için bir Windows dizüstü bilgisayar kullandığınızı varsayalım. Ardından, geliştirme hedefiniz olarak Android'i seçersiniz. 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)

Bir 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, üst düzey 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 üzerine yazacaksınız.

İ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'ı (Gezgin) 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 şununla değiştirin:

pubspec.yaml

name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.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 şununla değiştirin:

lib/main.dart

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

void main() {
  final game = FlameGame();
  runApp(GameWidget(game: game));
}
  1. Her şeyin çalıştığını doğrulamak için bu kodu çalıştırın. Yalnızca boş siyah bir arka plan içeren yeni bir pencere gösterilmelidir. 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) bir oyunun oynama alanına ihtiyacı vardır. 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üzdeki çoğu oyun için geçerlidir. Özellikle üç boyutlu oyunlarda bu standart kullanılır.

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 yeni bir alt sınıfını türetin.

  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 öğesi world öğesine eklendi. 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ü

Bir 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 bunu 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'dan türetildiği için topu ekranda konumlandırabilirsiniz. Daha da önemlisi, bu bölümün konumu güncellenebilir.

Bu bileşen, velocity kavramını (konumun zaman içindeki değişimi) 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 şekilde, hareketin zaman içindeki ayrı bir simülasyonunu güncellemeyi uygulayabilirsiniz.

  1. Ball bileşenini bileşen listesine eklemek için lib/src/components/components.dart dosyasını aşağıdaki gibi düzenleyin.

lib/src/components/components.dart

export 'ball.dart';                           // Add this export
export 'play_area.dart';

Topu dünyaya 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, öncelikle oyunun boyutunu yarıya indirir. Bunun nedeni, Vector2 değerinin, 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öne 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 daire, ekrandaki boyutunu ve konumunu gösteren sayılarla açıklanmıştır.

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şturucusu 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 arama işlevi eklenerek önemli bir değişiklik yapılıyor. Önceki örnekte BrickBreaker öğesine 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. Bu durum, bir sonraki adımda dünyaya yarasa eklediğinizde değişir. Ardından, topun sopadan farklı şeylerle çarpışmasını işlemek 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 adımlardan birinde işleyebilirsiniz.

Topun oyunun duvarlarıyla çarpışmasını sağladığınıza göre, oyuncuya topa vurması için bir sopa vermek faydalı olacaktır.

7. Get bat on ball

Bat dosyasını oluşturun

Topun oyun içinde kalmasını sağlamak için sopa eklemek istiyorsanız:

  1. lib/src/config.dart dosyasına bazı sabitleri aşağıdaki gibi 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. Diğer taraftan batStep sabiti 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, Yarasa bileşeni RectangleComponent veya CircleComponent değil, PositionComponent'dır. Bu, kodun ekranda Bat karakterini 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ça 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 ekler ve onDragUpdate etkinliğini geçersiz kılarsınız.

Son olarak, Bat bileşeninin klavye kontrolüne yanıt vermesi gerekir. moveBy işlevi, diğer 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 nesnesini bu bileşenin alt öğesi olarak eklediğinizde 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 karışımının 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 dosyadaki tüm ayarlar, 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ükleri 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 kaldırı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 işleme şeklini, 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 tam olarak nasıl işlediği, oyun dışında oyuncuya herhangi bir şekilde iletilmez.

Artık topla vurabileceğiniz bir sopanız olduğuna göre, topla kırabileceğiniz tuğlalar da ekleyelim.

8. Duvarı yıkmak

Tuvaletleri oluşturma

Oyuna tuğla eklemek için:

  1. lib/src/config.dart dosyasına bazı sabitleri aşağıdaki gibi 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 kodda, 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ılır.

Bu kodun getirdiği en önemli yeni kavram, oyuncunun kazanma koşulunu nasıl elde ettiğ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 temel 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 gibi düzenleyin.

lib/src/components/components.dart

export 'ball.dart';
export 'bat.dart';
export 'brick.dart';                                            // Add this export
export 'play_area.dart';

Dünyaya tuğ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ü, yani 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ındaki top, raket ve tuğlaların çoğunu içeren brick_breaker oyununun 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ştirir, ardından karşılama, oyun bitti ve kazandınız ekranları için Flutter yer paylaşımları eklersiniz.

Ö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, oyuncunun oyuna girme, oyunu oynama ve oyunu kaybetme veya kazanma durumunu gösterir. Dosyanın en üstünde 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, gameOver oynatma durumunu tetikleyen RemoveEffect öğesine onComplete geri çağırma işlevini ekler. 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 "oyunu kazandı" ekranını görür. Tebrikler oyuncu, tebrikler!

Flutter sarmalayıcısını ekleme

Oyunu yerleştirmek ve oynatma durumu katmanları eklemek için Flutter kabuğunu ekleyin.

  1. widgets altında lib/src dizini oluşturun.
  2. Bir game_app.dart dosyası ekleyin ve bu dosyaya aşağıdaki içeriği yerleştirin.

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's anahtarları, BrickBreaker'daki playState belirleyicisinin 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şlevi 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 yetkilendirme 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. Bir sonraki adımda ne yapacağınızı tahmin edin.

10. Puan tutma

Oyuna puan ekleme

Bu adımda, oyun skorunu ç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 bir puan eklemek için Brick sınıfını değiştirin.

lib/src/components/brick.dart

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';

import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';

class Brick extends RectangleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Brick({required super.position, required Color color})
    : super(
        size: Vector2(brickWidth, brickHeight),
        anchor: Anchor.center,
        paint: Paint()
          ..color = color
          ..style = PaintingStyle.fill,
        children: [RectangleHitbox()],
      );

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();
    game.score.value++;                                         // Add this line

    if (game.world.children.query<Brick>().length == 1) {
      game.playState = PlayState.won;
      game.world.removeAll(game.world.children.query<Ball>());
      game.world.removeAll(game.world.children.query<Bat>());
    }
  }
}

İyi görünen bir oyun oluşturma

Flutter'da artık 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'ın gücünü daha ayrıntılı bir şekilde incelemek için 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ı oyunun üstüne yerleştirmek üzere 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 maç öncesi ekranını gösteren ekran görüntüsü

brick_breaker oyununun, oyun bitti ekranının bir yarasa ve bazı tuğlaların üzerinde gösterildiği 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 bu oyunu Flutter sarmalayıcısına yerleştirdiniz. Bileşenleri canlandırmak ve kaldırmak için Flame'in efektlerini kullandınız. Tüm oyunun iyi tasarlanmış görünmesi için Google Fonts ve Flutter Animate paketlerini kullandınız.

Sırada ne var?

Şu codelab'lere göz atın:

Daha fazla bilgi