Flutter ile Flame'e Giriş

1. Giriş

Flame, Flutter tabanlı bir 2D oyun motorudur. Bu codelab'de 70'lerin klasik video oyunlarından biri olan Steve Wozniak'ın Breakout'undan esinlenen bir oyun geliştireceksiniz. Sopayı, topu ve tuğlaları çizmek için Alev'in Bileşenlerini kullanacaksınız. Yarasanın hareketlerini canlandırmak için Flame'in Efektleri'ni kullanacaksınız ve Flame'ı Flutter'ın durum yönetim sistemine nasıl entegre edeceğinizi göreceksiniz.

Tamamlandığında oyununuz, biraz daha yavaş olsa da bu animasyonlu GIF gibi görünecektir.

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

Neler öğreneceksiniz?

  • GameWidget ile başlayarak Flame hakkındaki temel bilgilerin işleyiş şekli.
  • Oyun döngüsü nasıl kullanılır?
  • Flame'in Component ürünleri nasıl çalışır? Bunlar Flutter'ın Widget'lerine benzer.
  • Çarpışmaların nasıl ele alınacağı.
  • Component canlandırması için Effect nasıl kullanılır?
  • Flame oyununun üzerine Flutter Widget'ları yerleştirme.
  • Flame, Flutter'ın durum yönetimiyle nasıl entegre edilir?

Neler oluşturacaksınız?

Bu codelab'de Flutter ve Flame kullanarak 2D bir oyun geliştireceksiniz. Tamamlandığında oyununuz aşağıdaki koşulları karşılamalıdır

  • Flutter'ın desteklediği altı platformda da işlev görür: Android, iOS, Linux, macOS, Windows ve web
  • Flame'in oyun döngüsünü kullanarak en az 60 fps'yi koruyun.
  • 80'lerin arcade tarzı hissini vermek için google_fonts paketi ve flutter_animate gibi Flutter özelliklerini kullanın.

2. Flutter ortamınızı ayarlama

Düzenleyici

Bu codelab'i basitleştirmek için Visual Studio Code'un (VS Code) geliştirme ortamınız olduğu varsayılır. VS Code ücretsizdir ve önde gelen tüm platformlarda çalışır. Talimatlar varsayılan olarak VS Code'a özel kısayollardan alındığından bu codelab için VS Code'u kullanıyoruz. Görevler daha basit hale geliyor: "bu düğmeyi tıklayın" veya "X yapmak için bu tuşa basın" "X işlemini yapmak için düzenleyicinizde uygun işlemi yapın" yerine

Android Studio, diğer IntelliJ IDE'ler, Emacs, Vim veya Notepad++ uygulamaları arasından istediğiniz düzenleyiciyi kullanabilirsiniz. Hepsi Flutter ile çalışır.

Flutter kodları içeren VS Code'un ekran görüntüsü

Geliştirme hedefi seçin

Flutter, birçok platforma yönelik uygulamalar üretir. Uygulamanız aşağıdaki işletim sistemlerinden herhangi birinde çalışabilir:

  • iOS
  • Android
  • Windows
  • macOS
  • Linux
  • web

Geliştirme hedefi olarak tek bir işletim sistemi seçmek yaygın bir uygulamadır. Bu, uygulamanızın, geliştirme sırasında çalıştırılacağı işletim sistemidir.

Dizüstü bilgisayara kabloyla bağlı bir dizüstü bilgisayar ve telefonu gösteren çizim. Dizüstü bilgisayar

Örneğin, Flutter uygulamanızı geliştirmek için Windows dizüstü bilgisayar kullandığınızı varsayalım. Ardından geliştirme hedefiniz olarak Android'i seçersiniz. Uygulamanızı önizlemek için Windows dizüstü bilgisayarınıza bir Android cihazı USB kablosuyla bağlarsınız. Geliştirme aşamasındaki uygulamanız, taktığınız Android cihazda veya bir Android emülatöründe çalışır. Geliştirme aşamasındaki uygulamanızı düzenleyicinizle birlikte bir Windows uygulaması olarak çalıştıran geliştirme hedefi olarak Windows'u seçmiş olabilirsiniz.

Geliştirme hedefiniz olarak web'i seçmek isteyebilirsiniz. Bunun, geliştirme sırasında olumsuz bir yönü vardır: Flutter'ın Durum Bilgili Hot Yeniden Yükleme özelliğini kaybedersiniz. Flutter şu anda web uygulamalarını çalışır durumda yeniden yükleyememektedir.

Devam etmeden önce seçiminizi yapın. Uygulamanızı daha sonra istediğiniz zaman diğer işletim sistemlerinde çalıştırabilirsiniz. Geliştirme hedefi seçmek bir sonraki adımı daha sorunsuz hale getirir.

Flutter'ı yükleme

Flutter SDK'sını yüklemeyle ilgili en güncel talimatları docs.flutter.dev adresinde bulabilirsiniz.

Flutter web sitesindeki talimatlar, SDK'nın yüklenmesini ve geliştirme hedefiyle ilgili araçlar ile düzenleyici eklentilerini kapsar. Bu codelab için şu yazılımı yükleyin:

  1. Flutter SDK'sı
  2. Flutter eklentisiyle Visual Studio Kodu
  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 ise Xcode gerekir.)

Bir sonraki bölümde ilk Flutter projenizi oluşturacaksınız.

Herhangi bir sorun gidermeniz gerekirse, bu sorulardan ve yanıtlardan (StackOverflow'dan) bazıları sorun giderme için yararlı olabilir.

Sık Sorulan Sorular

3. Proje oluşturma

İlk Flutter projenizi oluşturun

Bu işlem, VS Code'u açmayı ve Flutter uygulamasını seçtiğiniz bir dizinde oluşturmayı içerir.

  1. Visual Studio Code'u başlatın.
  2. Komut paletini açın (F1 veya Ctrl+Shift+P ya da Shift+Cmd+P) ve ardından "flutter new" yazın. Görüntülendiğinde Flutter: New Project komutunu seçin.

VS Code'un ekran görüntüsü:

  1. Boş Uygulama'yı seçin. Projenizin oluşturulacağı dizini seçin. Bu dizin, üst düzey ayrıcalıklar gerektirmeyen veya yolunda boşluk bulunan herhangi bir dizin olmalıdır. Buna örnek olarak ana dizininiz veya C:\src\ verilebilir.

Yeni uygulama akışının parçası olarak seçili olarak gösterilen Boş Uygulama içeren VS Kodunun ekran görüntüsü

  1. Projenize brick_breaker adını verin. Bu codelab'in geri kalanında uygulamanıza brick_breaker adını verdiğiniz varsayılmaktadır.

VS Code'un ekran görüntüsü

Flutter artık proje klasörünüzü oluşturur ve VS Code bu klasörü açar. Şimdi uygulamanın temel bir iskeletini kullanarak iki dosyanın içeriğinin üzerine yazacaksınız.

Kopyala ve İlk uygulamayı yapıştırın

Bu işlem, bu codelab'de sağlanan örnek kodu uygulamanıza ekler.

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

VS Code'un kısmi ekran görüntüsü ve pubspec.yaml dosyasının konumunu vurgulayan oklar gösteriliyor.

  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.3.0 <4.0.0'

dependencies:
  flame: ^1.16.0
  flutter:
    sdk: flutter
  flutter_animate: ^4.5.0
  google_fonts: ^6.1.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.1

flutter:
  uses-material-design: true

pubspec.yaml dosyası; uygulamanızın mevcut sürümü, bağımlılıkları ve birlikte gönderileceği öğeler gibi uygulamanızla ilgili temel bilgileri belirtir.

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

Main.dart dosyasının konumunu gösteren bir okla birlikte VS Code&#39;un kısmi ekran görüntüsü

  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 arka planı olan yeni bir pencere görüntülemelidir. Dünyanın en kötü video oyunu şu anda 60 fps'de oluşturuluyor!

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

4. Oyunu oluştur

Oyunun boyutunu küçültün

İki boyutlu (2D) olarak oynanan bir oyun için oyun alanı gerekir. Belirli boyutlarda bir alan oluşturacak ve daha sonra bu boyutları oyunun diğer yönlerini boyutlandırmak için kullanacaksınız.

Oyun alanının koordinatlarını düzenlemenin çeşitli yolları vardır. Tek bir kurala göre, başlangıç noktası ekranın ortasında (0,0) olacak şekilde ekranın ortasından yönü ölçebilirsiniz. Pozitif değerler, öğeleri x ekseni boyunca sağa, y ekseni boyunca yukarı taşır. Bu standart, özellikle üç boyut içeren oyunların çoğu için geçerlidir.

Orijinal grup oyunu oluşturulurken kural, başlangıç noktasının sol üst köşeye ayarlanmasıydı. Pozitif x yönü aynı kaldı ancak y döndürüldü. X pozitif x yönü doğru, y aşağıydı. Bu oyun, çağa sadık kalmak için başlangıç noktasını sol üst köşeye yerleştiriyor.

lib/src adlı yeni dizinde config.dart adlı bir dosya oluşturun. Bu dosya, sonraki adımlarda daha fazla sabit değer kazanacak.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;

Bu oyun 820 piksel genişliğinde ve 1.600 piksel yüksekliğinde olacak. Oyun alanı, görüntülendiği pencereye sığacak şekilde ölçeklendirilir ancak ekrana eklenen tüm bileşenler bu yüksekliğe ve genişliğe uygundur.

PlayArea oluşturma

Breakout oyununda top, oyun alanının duvarlarından zıplıyor. Çakışmaları karşılayabilmek için öncelikle bir PlayArea bileşenine ihtiyacınız vardır.

  1. lib/src/components adlı yeni dizinde play_area.dart adlı bir dosya oluşturun.
  2. Aşağıdakileri bu dosyaya ekleyin.

lib/src/components/play_area.dart

import 'dart:async';

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

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea()
      : super(
          paint: Paint()..color = const Color(0xfff2e8cf),
        );

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

Flutter'da Widget, Flame'de ise Component bulunur. Flutter uygulamaları widget'lardan oluşan bir ağaç, Flame oyunları ise bileşenlerin bakımını içerir.

Flutter ile Flame arasında ilginç bir fark var. Flutter'ın widget ağacı, kalıcı ve değişebilir RenderObject katmanını güncellemek için kullanılmak üzere oluşturulmuş, geçici bir açıklamadır. Flame'in bileşenleri kalıcı ve değişebilir. Geliştiricinin bu bileşenleri simülasyon sisteminin parçası olarak kullanması beklenmektedir.

Flame'in bileşenleri, oyun mekaniklerini ifade etmek için optimize edilmiştir. Bu codelab, sonraki adımda gösterilen oyun döngüsüyle başlar.

  1. Karmaşıklığı kontrol etmek için bu projedeki tüm bileşenleri içeren bir dosya ekleyin. lib/src/components klasöründe bir components.dart dosyası oluşturun ve aşağıdaki içeriği ekleyin.

lib/src/components/components.dart

export 'play_area.dart';

export yönergesi, import ile ters rol oynar. Bu dosyanın başka bir dosyaya içe aktarıldığında hangi işlevleri sunduğunu açıklar. Aşağıdaki adımlarda yeni bileşenler ekledikçe bu dosyada daha fazla giriş olacaktır.

Flame oyunu oluştur

Önceki adımdaki kırmızı kısa çizgileri yok etmek amacıyla Flame'in FlameGame özelliği için yeni bir alt sınıf oluşturun.

  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, resmi içeren ekranı kaplayacak şekilde yeniden boyutlandırılır ve gerektiği şekilde sinemaskop eklenir.

PlayArea gibi alt bileşenlerin kendilerini uygun boyuta ayarlayabilmeleri için oyunun genişliğini ve yüksekliğini gösteriyorsunuz.

Geçersiz kılınan onLoad yönteminde kodunuz iki işlem gerçekleştirir.

  1. Sol üst kısmı vizör için sabitleyici olarak yapılandırır. Varsayılan olarak vizör (0,0) için çapa olarak alanın ortasını kullanır.
  2. PlayArea öğesini world öğesine ekler. Dünya, oyun dünyasını temsil eder. CameraComponent adlı çocuğun görünüm dönüşümü sırasında tüm alt öğelerini yansıtır.

Oyunu ekranda gösterin

Bu adımda yaptığınız tüm değişiklikleri görmek için lib/main.dart dosyanızı aşağıdaki değişikliklerle güncelleyin.

lib/main.dart

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

import 'src/brick_breaker.dart';                                // Add this import

void main() {
  final game = BrickBreaker();                                  // Modify this line
  runApp(GameWidget(game: game));
}

Bu değişiklikleri yaptıktan sonra oyunu yeniden başlatın. Oyun aşağıdaki resme benzeyecektir.

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 ekleyecek ve harekete geçireceksiniz!

5. Topu göster

Top bileşenini oluşturma

Ekrana hareket eden bir top yerleştirmek, başka bir bileşen oluşturmayı ve onu oyun dünyasına eklemeyi içerir.

  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ış sabitlerin türetilen değerler olarak tanımlanmasını sağlayan tasarım kalıbı bu codelab'de birçok kez döndürülecektir. Bu şekilde üst seviye gameWidth ve gameHeight öğelerini düzenleyerek oyunun görünümünü ve tarzının nasıl değiştiğini keşfedebilirsiniz.

  1. Ball bileşenini lib/src/components içindeki 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ımlamıştınız. Bu nedenle, daha fazla şekil var. CircleComponent, RectangleComponent gibi, PositionedComponent alanından türetildiğinden topu ekranda konumlandırabilirsiniz. Daha da önemlisi, düğmenin konumu güncellenebilir.

Bu bileşen, velocity veya zaman içinde konum değişikliği kavramını tanıtır. Hız Vector2 bir nesnedir, hız hem hız hem yöndür. Konumu güncellemek için oyun motorunun her karede çağırdığı update yöntemini geçersiz kılın. dt, önceki kare ile bu kare arasındaki süredir. Bu sayede farklı kare hızları (60 Hz veya 120 Hz) ya da aşırı hesaplama nedeniyle uzun kareler gibi faktörlere uyum sağlayabilirsiniz.

position += velocity * dt güncellemesine çok dikkat edin. Zaman içinde münferit bir hareket simülasyonunu güncellemeyi bu şekilde uygularsınız.

  1. Ball bileşenini bileşen listesine dahil etmek 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 katma

Bir topun var. Bunu dünyaya yerleştirelim ve oyun alanında gezinecek şekilde ayarlayalım.

lib/src/brick_breaker.dart dosyasını aşağıdaki gibi düzenleyin.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math; // Add this import

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();                                   // Add this variable
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(                                             // Add from here...
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    debugMode = true;                                           // To here.
  }
}

Bu değişiklik, Ball bileşenini world öğesine ekler. Topun position değerini, görüntülenme alanının ortasına ayarlamak için Vector2, Vector2 ölçeğini skaler bir değere göre ölçeklendirmek için operatör aşırı yüklemeleri (* ve /) içerdiğinden, kod ilk olarak oyunun boyutunu yarıya indirir.

Topun velocity seviyesini ayarlamak daha karmaşık. Amaç topu ekrandan rastgele bir yönde makul bir hızda aşağı hareket ettirmektir. normalized yöntemine yapılan çağrı, orijinal Vector2 ile aynı yöne ayarlanmış ancak 1 mesafeye kadar ölçeklendirilen bir Vector2 nesnesi oluşturur. Bu, top hangi yöne giderse gitsin topun hızını korur. Ardından topun hızı, oyunun yüksekliğinin 1/4'ü kadar olacak şekilde ölçeklendirilir.

Bu çeşitli değerleri doğru şekilde vermek, sektörde oyun testi olarak da bilinen bir miktar yinelemeyi gerektirir.

Son satır, hata ayıklama ekranını açar. Bu da hata ayıklamaya yardımcı olacak ek bilgileri gösterir.

Şu anda oyunu çalıştırdığınızda oyun aşağıdaki ekrana benzeyecektir.

Kum rengindeki dikdörtgenin üstünde mavi bir daire bulunan tuğla kırıcı uygulama penceresini gösteren ekran görüntüsü. Mavi daire, üzerinde sayı ve ekrandaki konumu belirten rakamlarla gösteriliyor.

Hem PlayArea bileşeni hem de Ball bileşeninde hata ayıklama bilgileri vardır ancak arka plan matları PlayArea sayılarını kırpar. Her şeyde hata ayıklama bilgilerinin görünmesinin nedeni, bileşen ağacının tamamı için debugMode özelliğini etkinleştirmiş olmanızdır. Daha yararlı olacaksa yalnızca seçili bileşenler için hata ayıklama özelliğini de açabilirsiniz.

Oyununuzu birkaç kez yeniden başlatırsanız topun beklendiği gibi duvarlarla etkileşime girmediğini fark edebilirsiniz. Bu efekti sağlamak için bir sonraki adımda yapacağınız çakışma algılama özelliğini eklemeniz gerekir.

6. Zıplama

Çarpışma algılama ekle

Çarpışma algılama özelliği, iki nesne birbiriyle temas ettiğinde oyununuzun tanıdığı bir davranış ekler.

Oyuna çarpışma algılama özelliği eklemek için HasCollisionDetection mix'ini BrickBreaker oyununa aşağıdaki kodda gösterildiği şekilde ekleyin.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    debugMode = true;
  }
}

Bu, bileşenlerin isabet kutularını izler ve her oyun bildiriminde çarpışma geri çağırmalarını tetikler.

Oyunun isabet kutularını doldurmaya başlamak için PlayArea bileşenini aşağıda gösterildiği gibi değiştirin.

lib/src/components/play_area.dart

import 'dart:async';

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

import '../brick_breaker.dart';

class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
  PlayArea()
      : super(
          paint: Paint()..color = const Color(0xfff2e8cf),
          children: [RectangleHitbox()],                        // Add this parameter
        );

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();
    size = Vector2(game.width, game.height);
  }
}

Bir RectangleHitbox bileşeni RectangleComponent alt öğesi olarak eklendiğinde, üst bileşenin boyutuyla eşleşen çarpışma algılama için bir isabet kutusu oluşturulur. Üst bileşenden daha küçük veya daha büyük bir isabet kutusu istediğiniz zamanlar için RectangleHitbox için relative adlı bir fabrika oluşturucu vardır.

Topa zıplayın

Çarpışma algılama özelliğinin eklenmesi şu ana kadar oynanabilirlikte herhangi bir fark yaratmadı. Ball bileşeninde değişiklik yaptığınızda değişiklik olur. Topun PlayArea ile çarpıştığında davranışının değişmesi gerekir.

Ball bileşenini aşağıdaki şekilde değiştirin.

lib/src/components/ball.dart

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

import '../brick_breaker.dart';                                 // And this import
import 'play_area.dart';                                        // And this one too

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {   // Add these mixins
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);                        // Add this parameter

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override                                                     // Add from here...
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        removeFromParent();
      }
    } else {
      debugPrint('collision with $other');
    }
  }                                                             // To here.
}

Bu örnekte, onCollisionStart geri çağırmasının eklenmesiyle büyük bir değişiklik meydana gelir. Önceki örnekte BrickBreaker işlevine eklenen çarpışma algılama sistemi bu geri çağırmayı çağırır.

İlk olarak, kod, Ball etiketinin PlayArea ile çakışıp çakışmadığını test eder. Oyun dünyasında başka hiçbir bileşen olmadığından bu şimdilik gereksiz görünüyor. Bu, bir sonraki adımda, dünyaya bir yarasa eklediğinizde değişecek. Ardından, top, sopa dışındaki şeylerle çarptığında uygulanacak bir else koşulu da ekler. Geri kalan mantığınızı uygulamak istiyorsanız nazik bir hatırlatma.

Top, alt duvarla çarpıştığında oyun yüzeyinden kaybolur ve epey görünümdedir. Gelecekteki bir adımda bu yapıyı Flame's Effects'in gücünü kullanarak işleyeceksiniz.

Artık top oyunun duvarlarıyla çarpıştığına göre, oyuncuya vuruş yapması için kriket sopası vermek kesinlikle faydalı olur...

7. Beyzbol sopası

Vuruşu yapın

Oyun içinde topu oyun içinde tutmak amacıyla kriket sopası eklemek için

  1. lib/src/config.dart dosyasına aşağıdaki gibi bazı sabit değerler ekleyin.

lib/src/config.dart

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;                               // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;                               // To here.

batHeight ve batWidth sabitleri yeterince açıklayıcıdır. Öte yandan, batStep sabiti için açıklama yapılması gerekir. Oyuncu, bu oyunda topla etkileşimde bulunmak için platforma bağlı olarak sopası fare veya parmağıyla sürükleyebilir veya klavyeyi kullanabilir. batStep sabit değeri, her bir sol veya sağ ok tuşuna basıldığında vuruşun ne kadar uzağa basacağını yapılandırır.

  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. Bu durum, bu kodun ekranda Bat öğesini oluşturması gerektiği anlamına gelir. Bunu gerçekleştirmek için render geri çağırmasını geçersiz kılar.

canvas.drawRRect (yuvarlatılmış dikdörtgen çizme) çağrısına yakından baktığınızda, kendinize "dikdörtgen nerede?" diye sorabilirsiniz. Offset.zero & size.toSize(), Rect oluşturan dart:ui Offset sınıfında operator & aşırı yükünden yararlanıyor. Bu steno ilk başta kafanızı karıştırabilir, ancak genellikle alt seviyedeki Flutter ve Flame kodunda görürsünüz.

İkincisi, bu Bat bileşeni platforma bağlı olarak parmak veya fare kullanılarak sürüklenebilir. Bu işlevi uygulamak için DragCallbacks mix'ini ekler ve onDragUpdate etkinliğini geçersiz kılarsınız.

Son olarak, Bat bileşeninin klavye kontrolüne yanıt vermesi gerekir. moveBy işlevi, diğer kodun bu sopaya belirli bir sayıda sanal piksel sola veya sağa hareket etmesini bildirmesini sağlar. Bu işlev, Flame oyun motorunun yeni bir özelliğini sunar: Effect MoveToEffect nesnesini bu bileşenin alt öğesi olarak eklediğinizde oyuncu, sopayı yeni bir konuma canlandırılmış şekilde görür. Flame'de çeşitli efektler gerçekleştirmek için kullanabileceğiniz Effect koleksiyonu vardır.

Efektin oluşturucu bağımsız değişkenleri, game alıcısına bir referans içerir. Bu nedenle, HasGameReference mix'ini bu sınıfa dahil ediyorsunuz. Bu karışım, bileşen ağacının en üstündeki BrickBreaker örneğine erişmek için bu bileşene tür güvenli bir game erişimcisi ekler.

  1. Bat öğesinin BrickBreaker tarafından kullanılabilmesi için 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';

Sopayı dünyaya ekleyin

Bat bileşenini oyun dünyasına eklemek için BrickBreaker öğesini aşağıdaki gibi güncelleyin.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

import 'package:flame/components.dart';
import 'package:flame/events.dart';                             // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart';                         // And this import
import 'package:flutter/services.dart';                         // And this

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {                // Modify this line
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(                                              // Add from here...
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));          // To here

    debugMode = true;
  }

  @override                                                     // Add from here...
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }                                                             // To here
}

KeyboardEvents mix'inin eklenmesi ve geçersiz kılınan onKeyEvent yöntemi, klavye girişini işler. Sopayı uygun adım miktarı kadar hareket ettirmek için daha önce eklediğiniz kodu geri çağırın.

Eklenen kod parçasının geri kalan kısmı, kriket sopasını oyun dünyasına uygun konumda ve doğru oranlarda ekler. Tüm bu ayarların bu dosyada gösterilmesi, oyuna uygun hissi vermek için vuruşun ve topun göreceli boyutunu düzenlemenizi kolaylaştırır.

Bu noktada oyunu oynarsanız topa müdahale etmek için sopası hareket ettirebileceğinizi ancak Ball ürününün çarpışma algılama kodunda bıraktığınız hata ayıklama kaydı dışında görünür bir yanıt alamadığınızı görürsünüz.

Şimdi bu sorunu düzeltmenin zamanı. Ball bileşenini aşağıdaki gibi düzenleyin.

lib/src/components/ball.dart

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

import '../brick_breaker.dart';
import 'bat.dart';                                             // And this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(                                       // Modify from here...
          delay: 0.35,
        ));
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else {                                                    // To here.
      debugPrint('collision with $other');
    }
  }
}

Bu kod değişiklikleri iki ayrı sorunu çözer.

İlk olarak, ekranın alt kısmına dokunduğu anda topun yerlerinden fırlayan topu sabitler. Bu sorunu düzeltmek için removeFromParent çağrısını RemoveEffect ile değiştirirsiniz. RemoveEffect, görüntülenebilir oyun alanından çıkmasına izin verdikten sonra topu oyun dünyasından kaldırır.

İkinci olarak, bu değişiklikler vuruş ve top arasındaki çarpışmanın yol açtığı etkiyi giderir. Bu işleme kodu oyuncunun lehine oldukça işe yarar. Oyuncu sopayla topa dokunduğu sürece, top ekranın üst kısmına döner. Bu durumun çok hoşunuza gittiğini düşünüyor ve daha gerçekçi bir deneyim yaşamak istiyorsanız bu işlemeyi oyununuza daha uygun olacak şekilde değiştirin.

velocity güncellemesinin karmaşıklığını belirtmekte fayda var. Bu, duvar çarpışmalarında yapıldığı gibi hızın sadece y bileşenini tersine çevirmez. Ayrıca x bileşenini, temas anındaki sopa ve topun göreli konumuna bağlı olarak günceller. Bu, oyuncunun topun ne yaptığı konusunda daha fazla kontrol sahibi olmasını sağlar, ancak oyun dışında hiçbir şekilde oyuncuya tam olarak nasıl açıklanmaz?

Artık elinizde topa vuracak bir sopasınız olduğuna göre, topla kırılacak birkaç tuğla daha olması güzel olurdu.

8. Duvarı yıkın

Tuğlaları oluşturma

Oyuna tuğla eklemek için:

  1. lib/src/config.dart dosyasına aşağıdaki gibi bazı sabit değerler ekleyin.

lib/src/config.dart

import 'package:flutter/material.dart';                         // Add this import

const brickColors = [                                           // Add this const
  Color(0xfff94144),
  Color(0xfff3722c),
  Color(0xfff8961e),
  Color(0xfff9844a),
  Color(0xfff9c74f),
  Color(0xff90be6d),
  Color(0xff43aa8b),
  Color(0xff4d908e),
  Color(0xff277da1),
  Color(0xff577590),
];

const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015;                          // Add from here...
final brickWidth =
    (gameWidth - (brickGutter * (brickColors.length + 1)))
    / brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03;                                // To here.
  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>());
    }
  }
}

Şu ana kadar bu kodun büyük bir kısmı tanıdık gelecektir. Bu kod, bileşen ağacının en üstünde hem çarpışma algılamayı hem de BrickBreaker oyununa tür güvenli bir referans içeren bir RectangleComponent kullanır.

Bu kodun sunduğu en önemli yeni kavram, oyuncunun kazanma koşulunu nasıl başardığıdır. Kazanma koşulu kontrolü tüm dünyada tuğla olup olmadığını sorgular ve yalnızca bir tanesinin kaldığını doğrular. Bu biraz kafa karıştırıcı olabilir çünkü önceki satır bu tuğlayı üst satırdan kaldırır.

Anlaşılması gereken en önemli nokta, bileşen kaldırma işleminin sıraya alınmış bir komut olmasıdır. Bu kod çalıştırıldıktan sonra, oyun dünyasındaki bir sonraki tıklamadan önce tuğlayı kaldırır.

Brick bileşenine BrickBreaker tarafından erişilebilmesi için lib/src/components/components.dart öğesini aşağıdaki gibi düzenleyin.

lib/src/components/components.dart

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

Dünyaya tuğlalar ekleyin

Ball bileşenini aşağıdaki şekilde güncelleyin.

lib/src/components/ball.dart

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

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';                                            // Add this import
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,                           // Add this parameter
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;
  final double difficultyModifier;                              // Add this member

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(
          delay: 0.35,
        ));
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {                                // Modify from here...
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);          // To here.
    }
  }
}

Oyunun tek yeni yönü, tuğlaların her çarpışmasından sonra topun hızını artıran bir zorluk değiştiricisi. Bu ayarlanabilir parametrenin, oyununuza uygun zorluk eğrisinin bulunması için playtest olması gerekir.

BrickBreaker oyununu aşağıdaki gibi düzenleyin.

lib/src/brick_breaker.dart

import 'dart:async';
import 'dart:math' as math;

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

import 'components/components.dart';
import 'config.dart';

class BrickBreaker extends FlameGame
    with HasCollisionDetection, KeyboardEvents {
  BrickBreaker()
      : super(
          camera: CameraComponent.withFixedResolution(
            width: gameWidth,
            height: gameHeight,
          ),
        );

  final rand = math.Random();
  double get width => size.x;
  double get height => size.y;

  @override
  FutureOr<void> onLoad() async {
    super.onLoad();

    camera.viewfinder.anchor = Anchor.topLeft;

    world.add(PlayArea());

    world.add(Ball(
        difficultyModifier: difficultyModifier,                 // Add this argument
        radius: ballRadius,
        position: size / 2,
        velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
            .normalized()
          ..scale(height / 4)));

    world.add(Bat(
        size: Vector2(batWidth, batHeight),
        cornerRadius: const Radius.circular(ballRadius / 2),
        position: Vector2(width / 2, height * 0.95)));

    await world.addAll([                                        // Add from here...
      for (var i = 0; i < brickColors.length; i++)
        for (var j = 1; j <= 5; j++)
          Brick(
            position: Vector2(
              (i + 0.5) * brickWidth + (i + 1) * brickGutter,
              (j + 2.0) * brickHeight + j * brickGutter,
            ),
            color: brickColors[i],
          ),
    ]);                                                         // To here.

    debugMode = true;
  }

  @override
  KeyEventResult onKeyEvent(
      KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    super.onKeyEvent(event, keysPressed);
    switch (event.logicalKey) {
      case LogicalKeyboardKey.arrowLeft:
        world.children.query<Bat>().first.moveBy(-batStep);
      case LogicalKeyboardKey.arrowRight:
        world.children.query<Bat>().first.moveBy(batStep);
    }
    return KeyEventResult.handled;
  }
}

Oyunu mevcut haliyle çalıştırırsanız tüm önemli oyun mekanikleri gösterilir. Hata ayıklamayı kapatıp tamamlandı olarak ayarlayabilirsiniz ancak bir şeyler eksik olabilir.

Tuğla kırıcının top, sopa ve oyun alanındaki çoğu tuğlanın gösterildiği ekran görüntüsü. Bileşenlerin her birinde hata ayıklama etiketleri var

Karşılama ekranına, ekran karşısında oyun oynamaya ve belki de skora ne dersiniz? Flutter, bu özellikleri oyuna ekleyebilir. Bir sonraki aşamada dikkatinizi bu özelliğe kaydıracaksınız.

9. Oyunu kazanın

Oynatma durumları ekle

Bu adımda Flame oyununu bir Flutter sarmalayıcısının içine yerleştirecek ve ardından karşılama, oyuna başlama ve kazanma ekranları için Flutter yer paylaşımlarını ekleyeceksiniz.

İlk olarak, oyun ve bileşen dosyalarını değiştirerek yer paylaşımının gösterilip gösterilmeyeceğini ve gösterilecekse hangisinin gösterileceğini belirten bir oynatma durumu eklersiniz.

  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 oyununu önemli ölçüde değiştiriyor. playState numaralandırmasını eklemek çok uğraşır. Bu, oyuncunun oyunu girdiği, oynadığı ve kaybettiği veya kazandığı durumları yakalar. Dosyanın üst kısmında numaralandırmayı tanımlar, ardından eşleşen alıcılar ve belirleyicilerle gizli bir durum olarak somutlaştırırsınız. Bu alıcılar ve belirleyiciler, oyunun çeşitli bölümleri oyun durumu geçişlerini tetiklediğinde yer paylaşımlarının değiştirilmesini sağlar.

Daha sonra, onLoad içindeki kodu onLoad ve yeni bir startGame yöntemine ayırdınız. Bu değişiklikten önce yalnızca yeni bir oyun başlatmak için oyunu yeniden başlatmanız gerekiyordu. Oyuncu, bu yeni eklemelerle artık çok fazla önlem almadan yeni bir oyuna başlayabilir.

Oyuncunun yeni bir oyun başlatmasına izin vermek için oyun için iki yeni işleyici yapılandırdınız. Kullanıcının birden fazla modda yeni bir oyun başlatabilmesi için dokunma işleyicisi ekleyip klavye işleyicisini genişlettiniz. Oyun durumu modellendiğinde, oyuncu kazandığında veya kaybettiğinde oynatma durumu geçişlerini tetiklemek için bileşenleri güncellemek mantıklıdır.

  1. Ball bileşenini aşağıdaki şekilde değiştirin.

lib/src/components/ball.dart

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

import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';

class Ball extends CircleComponent
    with CollisionCallbacks, HasGameReference<BrickBreaker> {
  Ball({
    required this.velocity,
    required super.position,
    required double radius,
    required this.difficultyModifier,
  }) : super(
            radius: radius,
            anchor: Anchor.center,
            paint: Paint()
              ..color = const Color(0xff1e6091)
              ..style = PaintingStyle.fill,
            children: [CircleHitbox()]);

  final Vector2 velocity;
  final double difficultyModifier;

  @override
  void update(double dt) {
    super.update(dt);
    position += velocity * dt;
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    if (other is PlayArea) {
      if (intersectionPoints.first.y <= 0) {
        velocity.y = -velocity.y;
      } else if (intersectionPoints.first.x <= 0) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.x >= game.width) {
        velocity.x = -velocity.x;
      } else if (intersectionPoints.first.y >= game.height) {
        add(RemoveEffect(
            delay: 0.35,
            onComplete: () {                                    // Modify from here
              game.playState = PlayState.gameOver;
            }));                                                // To here.
      }
    } else if (other is Bat) {
      velocity.y = -velocity.y;
      velocity.x = velocity.x +
          (position.x - other.position.x) / other.size.x * game.width * 0.3;
    } else if (other is Brick) {
      if (position.y < other.position.y - other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.y > other.position.y + other.size.y / 2) {
        velocity.y = -velocity.y;
      } else if (position.x < other.position.x) {
        velocity.x = -velocity.x;
      } else if (position.x > other.position.x) {
        velocity.x = -velocity.x;
      }
      velocity.setFrom(velocity * difficultyModifier);
    }
  }
}

Bu küçük değişiklik, RemoveEffect öğesine gameOver oynatma durumunu tetikleyen bir onComplete geri çağırması ekler. Oyuncu, topun ekranın alt kısmından dışarı çıkmasına izin veriyorsa bunu doğru bir şekilde hissedeceksiniz.

  1. Brick bileşenini aşağıdaki gibi düzenleyin.

lib/src/components/brick.dart

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

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

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

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    super.onCollisionStart(intersectionPoints, other);
    removeFromParent();

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

Diğer yandan, oyuncu tüm tuğlaları kırabilirse bir "oyun kazanmış" olur. tıklayın. Tebrikler oyuncu, tebrikler!

Flutter sarmalayıcıyı ekleme

Oyunun yerleştirileceği bir yer sağlamak ve oynama durumu yer paylaşımları eklemek için Flutter kabuğunu ekleyin.

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

lib/src/widgets/game_app.dart

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

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

class GameApp extends StatelessWidget {
  const GameApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0xffa9d6e5),
                Color(0xfff2e8cf),
              ],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: FittedBox(
                  child: SizedBox(
                    width: gameWidth,
                    height: gameHeight,
                    child: GameWidget.controlled(
                      gameFactory: BrickBreaker.new,
                      overlayBuilderMap: {
                        PlayState.welcome.name: (context, game) => Center(
                              child: Text(
                                'TAP TO PLAY',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                        PlayState.gameOver.name: (context, game) => Center(
                              child: Text(
                                'G A M E   O V E R',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                        PlayState.won.name: (context, game) => Center(
                              child: Text(
                                'Y O U   W O N ! ! !',
                                style:
                                    Theme.of(context).textTheme.headlineLarge,
                              ),
                            ),
                      },
                    ),
                  ),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Bu dosyadaki çoğu içerik, standart bir Flutter widget ağacı derlemesini izler. Flame'e özgü bölümler, BrickBreaker oyun örneğini oluşturmak ve yönetmek için GameWidget.controlled kullanımını ve GameWidget için yeni overlayBuilderMap bağımsız değişkenini oluşturmayı içerir.

Bu overlayBuilderMap öğesinin anahtarları, BrickBreaker içindeki playState belirleyicinin eklediği veya kaldırdığı yer paylaşımlarıyla uyumlu olmalıdır. Bu haritada bulunmayan bir bindirme ayarlamaya çalışmak, her yerde mutsuz yüzlere yol açıyor.

  1. Ekranda bu yeni işlevi kullanmak için lib/main.dart dosyasını aşağıdaki içerikle değiştirin.

lib/main.dart

import 'package:flutter/material.dart';

import 'src/widgets/game_app.dart';

void main() {
  runApp(const GameApp());
}

Bu kodu iOS, Linux, Windows veya web'de çalıştırırsanız hedeflenen çıkış oyunda gösterilir. macOS veya Android'i hedeflerseniz google_fonts ürününün görüntülenmesini sağlamak için son bir ince ayar yapmanız gerekir.

Yazı tipi erişimini etkinleştirme

Android için internet izni ekleme

Android için internet izni eklemeniz gerekir. AndroidManifest.xml öğenizi aşağıdaki gibi düzenleyin.

android/app/src/main/AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Add the following line -->
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:label="brick_breaker"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:taskAffinity=""
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
    <!-- Required to query activities that can process text, see:
         https://developer.android.com/training/package-visibility and
         https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

         In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
</manifest>

macOS için yararlanma hakkı dosyalarını düzenleme

macOS'te düzenlenecek iki dosyanız vardır.

  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 komut bu şekilde çalıştırıldığında tüm platformlarda bir karşılama ekranı ile bir oyunun oynandığı ya da kazanıldığı ekran görüntülenmelidir. Bu ekranlar biraz basit olabilir ve bir puan vermeniz iyi olacaktır. Bu yüzden bir sonraki adımda ne yapacağınızı tahmin edin!

10. Puanı tut

Maça skor ekleyin

Bu adımda, oyun skorunu etrafındaki Flutter bağlamına maruz bırakırsınız. Bu adımda, Flame oyunundan durumu etrafındaki Flutter durum yönetimine açıklayacaksınız. Bu, oyuncu her tuğla kırdığında oyun kodunun skoru güncellemesini sağlar.

  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ğlamış olursunuz.

  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>());
    }
  }
}

Güzel görünen bir oyun yapın

Artık Flutter'da skoru saklayabildiğinize göre şimdi widget'ları birleştirip iyi görünmesini sağlayabilirsiniz.

  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 hizmetinde overlay_screen.dart oluşturun ve aşağıdaki kodu ekleyin.

Bu sayede, flutter_animate paketinin gücünü kullanarak yer paylaşımlı ekranlara hareket ve stil katabilirsiniz.

lib/src/widgets/overlay_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';

class OverlayScreen extends StatelessWidget {
  const OverlayScreen({
    super.key,
    required this.title,
    required this.subtitle,
  });

  final String title;
  final String subtitle;

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: const Alignment(0, -0.15),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            title,
            style: Theme.of(context).textTheme.headlineLarge,
          ).animate().slideY(duration: 750.ms, begin: -3, end: 0),
          const SizedBox(height: 16),
          Text(
            subtitle,
            style: Theme.of(context).textTheme.headlineSmall,
          )
              .animate(onPlay: (controller) => controller.repeat())
              .fadeIn(duration: 1.seconds)
              .then()
              .fadeOut(duration: 1.seconds),
        ],
      ),
    );
  }
}

flutter_animate aracının gücünü daha ayrıntılı bir şekilde incelemek için Flutter'da yeni nesil kullanıcı arayüzleri oluşturma codelab'ine göz atın.

Bu kod, GameApp bileşeninde birçok değişikliğe uğradı. İlk olarak, ScoreCard ürününün score öğesine erişebilmesi için StatelessWidget türünü StatefulWidget biçimine dönüştürürsünüz. Puan kartının eklenmesi için skoru oyunun üst kısmına taşımak için Column eklenmesi gerekir.

İkinci olarak, karşılama, oyuna başlama ve kazanılan deneyimleri iyileştirmek için yeni OverlayScreen widget'ını eklediniz.

lib/src/widgets/game_app.dart

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

import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart';                                   // Add this import
import 'score_card.dart';                                       // And this one too

class GameApp extends StatefulWidget {                          // Modify this line
  const GameApp({super.key});

  @override                                                     // Add from here...
  State<GameApp> createState() => _GameAppState();
}

class _GameAppState extends State<GameApp> {
  late final BrickBreaker game;

  @override
  void initState() {
    super.initState();
    game = BrickBreaker();
  }                                                             // To here.

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        textTheme: GoogleFonts.pressStart2pTextTheme().apply(
          bodyColor: const Color(0xff184e77),
          displayColor: const Color(0xff184e77),
        ),
      ),
      home: Scaffold(
        body: Container(
          decoration: const BoxDecoration(
            gradient: LinearGradient(
              begin: Alignment.topCenter,
              end: Alignment.bottomCenter,
              colors: [
                Color(0xffa9d6e5),
                Color(0xfff2e8cf),
              ],
            ),
          ),
          child: SafeArea(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Center(
                child: Column(                                  // Modify from here...
                  children: [
                    ScoreCard(score: game.score),
                    Expanded(
                      child: FittedBox(
                        child: SizedBox(
                          width: gameWidth,
                          height: gameHeight,
                          child: GameWidget(
                            game: game,
                            overlayBuilderMap: {
                              PlayState.welcome.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'TAP TO PLAY',
                                    subtitle: 'Use arrow keys or swipe',
                                  ),
                              PlayState.gameOver.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'G A M E   O V E R',
                                    subtitle: 'Tap to Play Again',
                                  ),
                              PlayState.won.name: (context, game) =>
                                  const OverlayScreen(
                                    title: 'Y O U   W O N ! ! !',
                                    subtitle: 'Tap to Play Again',
                                  ),
                            },
                          ),
                        ),
                      ),
                    ),
                  ],
                ),                                              // To here.
              ),
            ),
          ),
        ),
      ),
    );
  }
}

Tüm bunlar tamamlandığında bu oyunu artık altı Flutter hedef platformundan birinde çalıştırabiliyor olmanız gerekir. Oyun aşağıdakine benzeyecektir.

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

Oyunu bir sopasın ve bazı tuğlaların üzerine yerleştirilmiş ekran üzerinde gösteren brick_breaker ekran görüntüsü

11. Tebrikler

Tebrikler, Flutter ve Flame ile başarılı bir oyun geliştirdiniz.

Flame 2D oyun motorunu kullanarak bir oyun geliştirdiniz ve bu oyunu Flutter sarmalayıcıya yerleştirdiniz. Bileşenleri canlandırmak ve kaldırmak için Flame'in Efektlerini kullandınız. Oyunun tamamının iyi tasarlanmış görünmesini sağlamak için Google Fonts ve Flutter Animate paketlerini kullandınız.

Sırada ne var?

Bu codelab'lerden bazılarına göz atın...

Daha fazla bilgi