Flutter ve Flame ile 2D fizik oyunu geliştirin

Flutter ve Flame ile 2D fizik oyunu oluşturma

Bu codelab hakkında

subjectSon güncelleme Haz 23, 2025
account_circleYazan: Brett Morgan

1. Başlamadan önce

Flame, Flutter tabanlı bir 2D oyun motorudur. Bu codelab'de, Box2D tarzında 2D fizik simülasyonu kullanan Forge2D adlı bir oyun oluşturacaksınız. Kullanıcılarınızın oynaması için ekranda simüle edilmiş fiziksel gerçekliği çizmek üzere Flame'ın bileşenlerini kullanırsınız. Oyununuz tamamlandığında aşağıdaki animasyonlu GIF'ye benzer bir görünüme sahip olur:

Bu 2D fizik oyununda oyun oynama animasyonu

Ön koşullar

Öğrenecekleriniz

  • Farklı fiziksel cisim türlerinden başlayarak Forge2D'nin temel işleyiş şekli.
  • 2D'de fiziksel simülasyon oluşturma.

İhtiyacınız olanlar

Seçtiğiniz geliştirme hedefi için derleyici yazılımı. Bu kod laboratuvarı, Flutter'ın desteklediği altı platformun tamamında çalışır. Windows'u hedeflemek için Visual Studio, macOS veya iOS'i hedeflemek için Xcode, Android'i hedeflemek için Android Studio'ya ihtiyacınız vardır.

2. Proje oluşturma

Flutter projenizi oluşturun

Flutter projesi oluşturmanın birçok yolu vardır. Bu bölümde, kısalık olması için komut satırını kullanırsınız.

Başlamak için aşağıdaki adımları uygulayın:

  1. Komut satırında bir Flutter projesi oluşturun:
    $ flutter create --empty forge2d_game
    Creating project forge2d_game...
    Resolving dependencies in forge2d_game... (4.7s)
    Got dependencies in forge2d_game.
    Wrote 128 files.
    
    All done!
    You can find general documentation for Flutter at: https://docs.flutter.dev/
    Detailed API documentation is available at: https://api.flutter.dev/
    If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev
    
    In order to run your empty application, type:
    
      $ cd forge2d_game
      $ flutter run
    
    Your empty application code is in forge2d_game/lib/main.dart.
    
  2. Flame ve Forge2D'yi eklemek için projenin bağımlılıklarını değiştirin:
    $ cd forge2d_game
    $ flutter pub add characters flame flame_forge2d flame_kenney_xml xml
    Resolving dependencies...
    Downloading packages...
      characters 1.4.0 (from transitive dependency to direct dependency)
    + flame 1.29.0
    + flame_forge2d 0.19.0+2
    + flame_kenney_xml 0.1.1+12
      flutter_lints 5.0.0 (6.0.0 available)
    + forge2d 0.14.0
      leak_tracker 10.0.9 (11.0.1 available)
      leak_tracker_flutter_testing 3.0.9 (3.0.10 available)
      leak_tracker_testing 3.0.1 (3.0.2 available)
      lints 5.1.1 (6.0.0 available)
      material_color_utilities 0.11.1 (0.13.0 available)
      meta 1.16.0 (1.17.0 available)
    + ordered_set 8.0.0
    + petitparser 6.1.0 (7.0.0 available)
      test_api 0.7.4 (0.7.6 available)
      vector_math 2.1.4 (2.2.0 available)
      vm_service 15.0.0 (15.0.2 available)
    + xml 6.5.0 (6.6.0 available)
    Changed 8 dependencies!
    12 packages have newer versions incompatible with dependency constraints.
    Try `flutter pub outdated` for more information.
    

flame paketi size tanıdık gelebilir ancak diğer üçü için biraz açıklama gerekebilir. characters paketi, UTF8 uyumlu bir şekilde yol değiştirmek için kullanılır. flame_forge2d paketi, Forge2D işlevini Flame ile iyi çalışacak şekilde gösterir. Son olarak, xml paketi XML içeriğini kullanmak ve değiştirmek için çeşitli yerlerde kullanılır.

Projeyi açın ve lib/main.dart dosyasının içeriğini aşağıdakiyle değiştirin:

lib/main.dart

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

void main() {
 
runApp(GameWidget.controlled(gameFactory: FlameGame.new));
}

Bu işlem, uygulamayı FlameGame örneğini oluşturan bir GameWidget ile başlatır. Bu kod laboratuvarındaki Flutter kodunda, çalışan oyunla ilgili bilgileri görüntülemek için oyun örneğinin durumunu kullanan bir kod yoktur. Bu nedenle, basitleştirilmiş bu önyükleme işlemi iyi çalışır.

İsteğe bağlı: Yalnızca macOS için yan görevlere katılın

Bu projedeki ekran görüntüleri, macOS masaüstü uygulaması olarak oyundan alınmıştır. Uygulamanın başlık çubuğunun genel deneyimi olumsuz etkilemesini önlemek için macOS çalıştırıcısının proje yapılandırmasını, başlık çubuğunu kaldıracak şekilde değiştirebilirsiniz.

Bunun için, aşağıdaki adımları uygulayın:

  1. Bir bin/modify_macos_config.dart dosyası oluşturup aşağıdaki içeriği ekleyin:

bin/modify_macos_config.dart

import 'dart:io';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';

void main() {
 
final file = File('macos/Runner/Base.lproj/MainMenu.xib');
 
var document = XmlDocument.parse(file.readAsStringSync());
 
document.xpath('//document/objects/window').first
   
..setAttribute('titlebarAppearsTransparent', 'YES')
   
..setAttribute('titleVisibility', 'hidden');
 
document
     
.xpath('//document/objects/window/windowStyleMask')
     
.first
     
.setAttribute('fullSizeContentView', 'YES');
 
file.writeAsStringSync(document.toString());
}

Bu dosya, oyunun çalışma zamanındaki kod tabanının parçası olmadığı için lib dizininde yer almıyor. Projeyi değiştirmek için kullanılan bir komut satırı aracıdır.

  1. Aracı, proje ana dizininden aşağıdaki gibi çalıştırın:
dart bin/modify_macos_config.dart

Her şey planlandığı gibi giderse program, komut satırında hiçbir çıkış oluşturmaz. Ancak macos/Runner/Base.lproj/MainMenu.xib yapılandırma dosyasını, oyunu görünür bir başlık çubuğu olmadan ve Flame oyununun pencerenin tamamını kaplayacak şekilde çalıştıracak şekilde değiştirir.

Her şeyin çalıştığını doğrulamak için oyunu çalıştırın. Yalnızca boş siyah arka planın bulunduğu yeni bir pencere gösterilir.

Siyah arka planlı ve ön planda hiçbir şey olmayan bir uygulama penceresi

3. Resim öğeleri ekleme

Resimleri ekleyin

Her oyunun, ekranı eğlenceli bir şekilde boyamak için resim öğelerine ihtiyacı vardır. Bu kod laboratuvarında, Kenney.nl'deki Physics Assets paketi kullanılacaktır. Bu öğeler Creative Commons CC0 lisanslıdır ancak yine de Kenney'deki ekibe bağışta bulunarak yaptıkları harika çalışmalara devam etmelerini sağlamanızı önemle tavsiye ederiz. Evet.

Kenney'nin öğelerinin kullanımını etkinleştirmek için pubspec.yaml yapılandırma dosyasını değiştirmeniz gerekir. Aşağıdaki gibi değiştirin:

pubspec.yaml

name: forge2d_game
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: ^3.8.1

dependencies:
  flutter:
    sdk: flutter
  characters: ^1.4.0
  flame: ^1.29.0
  flame_forge2d: ^0.19.0+2
  flame_kenney_xml: ^0.1.1+12
  xml: ^6.5.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^5.0.0

flutter:
  uses-material-design: true
  assets:                        # Add from here
    - assets/
    - assets/images/             # To here.

Flame, resim öğelerinin assets/images içinde olmasını bekler ancak bu farklı şekilde yapılandırılabilir. Daha fazla bilgi için Flame'ın Resimler dokümanlarını inceleyin. Yolları yapılandırdınız. Şimdi bunları projeye eklemeniz gerekiyor. Bunu yapmanın bir yolu, komut satırını aşağıdaki gibi kullanmaktır:

mkdir -p assets/images

mkdir komutu herhangi bir çıktı vermez ancak yeni dizin, düzenleyicinizde veya bir dosya gezgininde görünür.

İndirdiğiniz kenney_physics-assets.zip dosyasını genişletin. Aşağıdakine benzer bir görünüm görürsünüz:

PNG/Arka Planlar dizininin vurgulandığı, kenney_physics-assets paketinin genişletilmiş dosya listesi

PNG/Backgrounds dizininden colored_desert.png, colored_grass.png, colored_land.png ve colored_shroom.png dosyalarını projenizin assets/images dizinine kopyalayın.

Sprite sayfaları da vardır. Bunlar, bir PNG resminin ve spritesheet resminin neresinde daha küçük resimlerin bulunabileceğini açıklayan bir XML dosyasının birleşimidir. Sprite sayfaları, yüzlerce olmasa da onlarca ayrı resim dosyasının yerine yalnızca tek bir dosya yükleyerek yükleme süresini azaltan bir tekniktir.

Spritesheet dizininin vurgulandığı, kenney_physics-assets paketinin genişletilmiş dosya listesi

spritesheet_aliens.png, spritesheet_elements.png ve spritesheet_tiles.png öğelerini projenizin assets/images dizinine kopyalayın. Buradayken spritesheet_aliens.xml, spritesheet_elements.xml ve spritesheet_tiles.xml dosyalarını da projenizin assets dizinine kopyalayın. Projeniz aşağıdaki gibi görünecektir.

forge2d_game proje dizininin, assets dizininin vurgulandığı dosya listesi

Arka planı boyama

Projenize resim öğeleri eklediniz. Şimdi bunları ekrana yerleştirme zamanı. Ekranda bir resim. Aşağıdaki adımlarda daha fazla bilgi verilecektir.

lib/components adlı yeni bir dizinde background.dart adlı bir dosya oluşturun ve aşağıdaki içeriği ekleyin.

lib/components/background.dart

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

class Background extends SpriteComponent with HasGameReference<MyPhysicsGame> {
 
Background({required super.sprite})
   
: super(anchor: Anchor.center, position: Vector2(0, 0));

 
@override
 
void onMount() {
   
super.onMount();

   
size = Vector2.all(
     
max(
       
game.camera.visibleWorldRect.width,
       
game.camera.visibleWorldRect.height,
     
),
   
);
 
}
}

Bu bileşen, özel bir SpriteComponent'tir. Kenney.nl'nin dört arka plan resminden birini göstermekten sorumludur. Bu kodda basitleştirmek için birkaç varsayım yapılmıştır. İlki, resimlerin kare olmasıdır. Kenney'nin arka plan resimlerinin dördü de karedir. İkincisi, görünür dünyanın boyutu hiçbir zaman değişmez. Aksi takdirde bu bileşenin oyun boyutunu yeniden ayarlama etkinliklerini işlemesi gerekir. Üçüncü varsayım, (0,0) konumunun ekranın ortasında olacağıdır. Bu varsayımlar, oyunun CameraComponent özelliğinin belirli bir şekilde yapılandırılmasını gerektirir.

lib/components dizininde game.dart adlı yeni bir dosya daha oluşturun.

lib/components/game.dart

import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';

import 'background.dart';

class MyPhysicsGame extends Forge2DGame {
 
MyPhysicsGame()
   
: super(
       
gravity: Vector2(0, 10),
       
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
     
);

 
late final XmlSpriteSheet aliens;
 
late final XmlSpriteSheet elements;
 
late final XmlSpriteSheet tiles;

 
@override
 
FutureOr<void> onLoad() async {
   
final backgroundImage = await images.load('colored_grass.png');
   
final spriteSheets = await Future.wait([
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_aliens.png',
       
xmlPath: 'spritesheet_aliens.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_elements.png',
       
xmlPath: 'spritesheet_elements.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_tiles.png',
       
xmlPath: 'spritesheet_tiles.xml',
     
),
   
]);

   
aliens = spriteSheets[0];
   
elements = spriteSheets[1];
   
tiles = spriteSheets[2];

   
await world.add(Background(sprite: Sprite(backgroundImage)));

   
return super.onLoad();
 
}
}

Burada çok şey oluyor. MyPhysicsGame sınıfıyla başlayın. Önceki kod laboratuvarının aksine bu kod, FlameGame yerine Forge2DGame öğesini genişletir. Forge2DGame, FlameGame'u birkaç ilginç değişiklikle genişletir. Birincisi, zoom varsayılan olarak 10 olarak ayarlanmıştır. Bu zoom ayarı, Box2D tarzı fizik simülasyon motorlarının iyi çalıştığı yararlı değer aralığıyla ilgilidir. Motor, MKS sistemi kullanılarak yazılır. Bu sistemde birimlerin metre, kilogram ve saniye cinsinden olduğu varsayılır. Nesneler için belirgin matematiksel hatalar görmediğiniz aralık 0,1 ila 10 metre arasındadır. Pixel boyutlarını, belirli bir düzeyde küçültme işlemi yapmadan doğrudan beslemek Forge2D'yi yararlı kapsamının dışına çıkarır. Özet olarak, bir soda kutusundan otobüse kadar olan nesneleri simüle etmeyi düşünebilirsiniz.

Arka plan bileşeninde yapılan varsayımlar, CameraComponent'nin çözünürlüğü 800x600 sanal piksele sabitlenerek burada karşılanır. Bu, oyun alanının (0,0) merkezinde 80 birim genişliğinde ve 60 birim yüksekliğinde olacağı anlamına gelir. Bu, görüntülenen çözünürlüğü etkilemez ancak nesneleri oyun sahnesine yerleştirdiğimiz yeri etkiler.

camera kurucu bağımsız değişkeninin yanında, gravity adlı daha fiziksel bir bağımsız değişken bulunur. Yerçekimi, 0 x ve 10 y değerine sahip bir Vector2 olarak ayarlanmıştır. 10, yerçekimi için genel kabul gören saniyede 9,81 metre değerine yakın bir yaklaşık değerdir. Yerçekiminin pozitif 10 olarak ayarlanmış olması, bu sistemde Y ekseninin yönünün aşağı olduğunu gösterir. Bu, genel olarak Box2D'den farklıdır ancak Flame'in genellikle yapılandırıldığı şekildedir.

Sırada onLoad yöntemi var. Bu yöntem, resim öğelerinin diskten yüklenmesi ile sorumlu olduğu için uygun olan asenkron bir yöntemdir. images.load çağrıları bir Future<Image> döndürür ve yan etki olarak yüklenen resmi Game nesnesinde önbelleğe alır. Bu gelecekler bir araya getirilir ve Futures.wait statik yöntemi kullanılarak tek bir birim olarak beklenir. Döndürülen resimlerin listesi daha sonra kalıp eşleştirmeyle tek tek isimlere dönüştürülür.

Spritesheet resimleri daha sonra, spritesheet'te bulunan ayrı ayrı adlandırılmış sprite'ları almaktan sorumlu bir dizi XmlSpriteSheet nesnesine beslenir. XmlSpriteSheet sınıfı, flame_kenney_xml paketinde tanımlanır.

Bu işlemler tamamlandıktan sonra, ekranda bir resim göstermek için lib/main.dart'te birkaç küçük düzenleme yapmanız yeterlidir.

lib/main.dart

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

import 'components/game.dart';                                    // Add this import

void main() {
 
runApp(GameWidget.controlled(gameFactory: MyPhysicsGame.new));  // Modify this line
}

Bu değişiklik sayesinde artık oyunu tekrar çalıştırarak ekranda arka planı görebilirsiniz. CameraComponent.withFixedResolution() kamera örneğinin, oyunun 800x600 oranını kullanabilmesi için gerektiğinde sinemaskop ekleyeceğini unutmayın.

Yeşil tepelerin ve garip soyut ağaçların yer aldığı bir uygulama.

4. Zemini ekleme

Geliştirebileceğiniz bir şey

Yerçekimi varsa oyundaki nesnelerin ekranın alt kısmından düşmeden önce yakalanması gerekir. Tabii ki ekrandan düşmek oyun tasarımınızın bir parçası değilse. lib/components dizininizde yeni bir ground.dart dosyası oluşturun ve dosyaya aşağıdakileri ekleyin:

lib/components/ground.dart

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

const groundSize = 7.0;

class Ground extends BodyComponent {
 
Ground(Vector2 position, Sprite sprite)
   
: super(
       
renderBody: false,
       
bodyDef: BodyDef()
         
..position = position
         
..type = BodyType.static,
       
fixtureDefs: [
         
FixtureDef(
           
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
           
friction: 0.3,
         
),
       
],
       
children: [
         
SpriteComponent(
           
anchor: Anchor.center,
           
sprite: sprite,
           
size: Vector2.all(groundSize),
           
position: Vector2(0, 0),
         
),
       
],
     
);
}

Bu Ground bileşeni, BodyComponent bileşeninden türetilmiştir. Forge2D'de cisimler önemlidir. İki boyutlu fiziksel simülasyonun parçası olan nesnelerdir. Bu bileşenin BodyDef öğesinin BodyType.static içerdiği belirtiliyor.

Forge2D'de cisimler üç farklı türdedir. Statik cisimler hareket etmez. Bu nesneler hem sıfır kütleye (yer çekimine tepki vermezler) hem de sonsuz kütleye (ne kadar ağır olursa olsunlar diğer nesnelerin çarpması sonucu hareket etmezler) sahiptir. Bu nedenle statik cisimler, hareket etmediği için zemin yüzeyi için idealdir.

Diğer iki cisim türü kinematik ve dinamiktir. Dinamik cisimler tamamen simüle edilmiş cisimler olup yerçekimine ve çarptıkları nesnelere tepki verir. Bu kod laboratuvarının geri kalanında birçok dinamik cisim göreceksiniz. Kinematik cisimler, statik ve dinamik arasında bir ara noktadır. Hareket ederler ancak yerçekimine veya onlara çarpan diğer nesnelere tepki vermezler. Faydalı ancak bu codelab'in kapsamı dışındadır.

Gövdenin kendisi çok fazla işlem yapmaz. Bir cismin maddesel olması için ilişkili şekillere ihtiyacı vardır. Bu durumda, bu gövdenin ilişkili bir şekli vardır. Bu şekil, BoxXY olarak ayarlanmış bir PolygonShape'tür. Bu tür bir kutu, bir dönme noktası etrafında döndürülebilen BoxXY olarak ayarlanmış bir PolygonShape'ten farklı olarak eksenle dünyayla hizalanır. Yine faydalı ancak bu kod laboratuvarının kapsamı dışında. Şekil ve gövde, sisteme friction gibi öğeler eklemek için kullanışlı olan bir sabitlemeyle birbirine bağlanır.

Varsayılan olarak bir cisim, bağlı şekillerini hata ayıklama için yararlı olacak şekilde oluşturur ancak bu, iyi bir oyun deneyimi sunmaz. super bağımsız değişkeni renderBody, false olarak ayarlandığında bu hata ayıklama oluşturma işlemi devre dışı bırakılır. Bu vücuda oyun içi bir oluşturma işlemi uygulamak SpriteComponent adlı çocuğun sorumluluğundadır.

Ground bileşenini oyuna eklemek için game.dart dosyanızı aşağıdaki gibi düzenleyin.

lib/components/game.dart

import 'dart:async';

import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';

import 'background.dart';
import 'ground.dart';                                      // Add this import

class MyPhysicsGame extends Forge2DGame {
 
MyPhysicsGame()
   
: super(
       
gravity: Vector2(0, 10),
       
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
     
);

 
late final XmlSpriteSheet aliens;
 
late final XmlSpriteSheet elements;
 
late final XmlSpriteSheet tiles;

 
@override
 
FutureOr<void> onLoad() async {
   
final backgroundImage = await images.load('colored_grass.png');
   
final spriteSheets = await Future.wait([
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_aliens.png',
       
xmlPath: 'spritesheet_aliens.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_elements.png',
       
xmlPath: 'spritesheet_elements.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_tiles.png',
       
xmlPath: 'spritesheet_tiles.xml',
     
),
   
]);

   
aliens = spriteSheets[0];
   
elements = spriteSheets[1];
   
tiles = spriteSheets[2];

   
await world.add(Background(sprite: Sprite(backgroundImage)));
   
await addGround();                                     // Add this line

   
return super.onLoad();
 
}

 
Future<void> addGround() {                               // Add from here...
   
return world.addAll([
     
for (
       
var x = camera.visibleWorldRect.left;
       
x < camera.visibleWorldRect.right + groundSize;
       
x += groundSize
     
)
       
Ground(
         
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
         
tiles.getSprite('grass.png'),
       
),
   
]);
 
}                                                        // To here.
}

Bu düzenleme, bir List bağlamında for döngüsü kullanarak ve elde edilen Ground bileşen listesini world'ın addAll yöntemine ileterek dünyaya bir dizi Ground bileşeni ekler.

Artık oyunu çalıştırdığınızda arka plan ve zemin gösteriliyor.

Arka plan ve zemin katmanı içeren bir uygulama penceresi.

5. Tuğlaları ekleme

Duvar oluşturma

Zemin, statik bir cisim örneğidir. Şimdi ilk dinamik bileşeninizi oluşturmanın zamanı geldi. Forge2D'deki dinamik bileşenler, oyuncu deneyiminin temelini oluşturur. Bunlar, hareket eden ve etraflarındaki dünyayla etkileşime geçen öğelerdir. Bu adımda, ekranda bir tuğla kümesinde rastgele seçilecek tuğlaları tanıtırsınız. Bu sırada düştüğünü ve birbirine çarptığını göreceksiniz.

Tuğlalar, öğe sprite sayfasından oluşturulur. assets/spritesheet_elements.xml dosyasında sprite e-tablosu açıklamasına bakarsanız ilginç bir sorunla karşı karşıya olduğumuzu görürsünüz. Adlar çok faydalı değil. Bir tuğlayı malzeme türüne, boyutuna ve hasar miktarına göre seçebilmek yararlı olur. Neyse ki yardımsever bir elf, dosya adlandırmasındaki kalıbı bulmak için zaman harcadı ve sizin için bu işlemi kolaylaştıracak bir araç oluşturdu. bin dizininde generate_brick_file_names.dart adlı yeni bir dosya oluşturun ve aşağıdaki içeriği ekleyin:

bin/generate_brick_file_names.dart

import 'dart:io';
import 'package:equatable/equatable.dart';
import 'package:xml/xml.dart';
import 'package:xml/xpath.dart';

void main() {
 
final file = File('assets/spritesheet_elements.xml');
 
final rects = <String, Rect>{};
 
final document = XmlDocument.parse(file.readAsStringSync());
 
for (final node in document.xpath('//TextureAtlas/SubTexture')) {
   
final name = node.getAttribute('name')!;
   
rects[name] = Rect(
     
x: int.parse(node.getAttribute('x')!),
     
y: int.parse(node.getAttribute('y')!),
     
width: int.parse(node.getAttribute('width')!),
     
height: int.parse(node.getAttribute('height')!),
   
);
 
}
 
print(generateBrickFileNames(rects));
}

class Rect extends Equatable {
 
final int x;
 
final int y;
 
final int width;
 
final int height;
 
const Rect({
   
required this.x,
   
required this.y,
   
required this.width,
   
required this.height,
 
});

 
Size get size => Size(width, height);

 
@override
 
List<Object?> get props => [x, y, width, height];

 
@override
 
bool get stringify => true;
}

class Size extends Equatable {
 
final int width;
 
final int height;
 
const Size(this.width, this.height);

 
@override
 
List<Object?> get props => [width, height];

 
@override
 
bool get stringify => true;
}

String generateBrickFileNames(Map<String, Rect> rects) {
 
final groups = <Size, List<String>>{};
 
for (final entry in rects.entries) {
   
groups.putIfAbsent(entry.value.size, () => []).add(entry.key);
 
}
 
final buff = StringBuffer();
 
buff.writeln('''
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
  return switch ((type, size)) {''');
 
for (final entry in groups.entries) {
   
final size = entry.key;
   
final entries = entry.value;
   
entries.sort();
   
for (final type in ['Explosive', 'Glass', 'Metal', 'Stone', 'Wood']) {
     
var filtered = entries.where((element) => element.contains(type));
     
if (filtered.length == 5) {
       
buff.writeln('''
    (BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
        BrickDamage.none: '${filtered.elementAt(0)}',
        BrickDamage.some: '${filtered.elementAt(1)}',
        BrickDamage.lots: '${filtered.elementAt(4)}',
      },''');
     
} else if (filtered.length == 10) {
       
buff.writeln('''
    (BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
        BrickDamage.none: '${filtered.elementAt(3)}',
        BrickDamage.some: '${filtered.elementAt(4)}',
        BrickDamage.lots: '${filtered.elementAt(9)}',
      },''');
     
} else if (filtered.length == 15) {
       
buff.writeln('''
    (BrickType.${type.toLowerCase()}, BrickSize.size${size.width}x${size.height}) => {
        BrickDamage.none: '${filtered.elementAt(7)}',
        BrickDamage.some: '${filtered.elementAt(8)}',
        BrickDamage.lots: '${filtered.elementAt(13)}',
      },''');
     
}
   
}
 
}
 
buff.writeln('''
  };
}''');
 
return buff.toString();
}

Düzenleyiciniz, eksik bağımlılık hakkında size uyarı veya hata vermelidir. Aşağıdaki komutu kullanarak ekleyin:

flutter pub add equatable

Artık bu programı aşağıdaki gibi çalıştırabilirsiniz:

$ dart run bin/generate_brick_file_names.dart
Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
  return switch ((type, size)) {
    (BrickType.explosive, BrickSize.size140x70) => {
        BrickDamage.none: 'elementExplosive009.png',
        BrickDamage.some: 'elementExplosive012.png',
        BrickDamage.lots: 'elementExplosive050.png',
      },
    (BrickType.glass, BrickSize.size140x70) => {
        BrickDamage.none: 'elementGlass010.png',
        BrickDamage.some: 'elementGlass013.png',
        BrickDamage.lots: 'elementGlass048.png',
      },
[Content elided...]
    (BrickType.wood, BrickSize.size140x220) => {
        BrickDamage.none: 'elementWood020.png',
        BrickDamage.some: 'elementWood025.png',
        BrickDamage.lots: 'elementWood052.png',
      },
  };
}

Bu araç, sprite e-tablosu açıklama dosyasını ayrıştırarak ekrana yerleştirmek istediğiniz her tuğla için doğru resim dosyasını seçmek üzere kullanabileceğimiz Dart koduna dönüştürdü. Faydalı.

Aşağıdaki içeriği içeren brick.dart dosyasını oluşturun:

lib/components/brick.dart

import 'dart:math';
import 'dart:ui' as ui;

import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';

const brickScale = 0.5;

enum BrickType {
 
explosive(density: 1, friction: 0.5),
 
glass(density: 0.5, friction: 0.2),
 
metal(density: 1, friction: 0.4),
 
stone(density: 2, friction: 1),
 
wood(density: 0.25, friction: 0.6);

 
final double density;
 
final double friction;

 
const BrickType({required this.density, required this.friction});
 
static BrickType get randomType => values[Random().nextInt(values.length)];
}

enum BrickSize {
 
size70x70(ui.Size(70, 70)),
 
size140x70(ui.Size(140, 70)),
 
size220x70(ui.Size(220, 70)),
 
size70x140(ui.Size(70, 140)),
 
size140x140(ui.Size(140, 140)),
 
size220x140(ui.Size(220, 140)),
 
size140x220(ui.Size(140, 220)),
 
size70x220(ui.Size(70, 220));

 
final ui.Size size;

 
const BrickSize(this.size);

 
static BrickSize get randomSize => values[Random().nextInt(values.length)];
}

enum BrickDamage { none, some, lots }

Map<BrickDamage, String> brickFileNames(BrickType type, BrickSize size) {
 
return switch ((type, size)) {
   
(BrickType.explosive, BrickSize.size140x70) => {
     
BrickDamage.none: 'elementExplosive009.png',
     
BrickDamage.some: 'elementExplosive012.png',
     
BrickDamage.lots: 'elementExplosive050.png',
   
},
   
(BrickType.glass, BrickSize.size140x70) => {
     
BrickDamage.none: 'elementGlass010.png',
     
BrickDamage.some: 'elementGlass013.png',
     
BrickDamage.lots: 'elementGlass048.png',
   
},
   
(BrickType.metal, BrickSize.size140x70) => {
     
BrickDamage.none: 'elementMetal009.png',
     
BrickDamage.some: 'elementMetal012.png',
     
BrickDamage.lots: 'elementMetal050.png',
   
},
   
(BrickType.stone, BrickSize.size140x70) => {
     
BrickDamage.none: 'elementStone009.png',
     
BrickDamage.some: 'elementStone012.png',
     
BrickDamage.lots: 'elementStone047.png',
   
},
   
(BrickType.wood, BrickSize.size140x70) => {
     
BrickDamage.none: 'elementWood011.png',
     
BrickDamage.some: 'elementWood014.png',
     
BrickDamage.lots: 'elementWood054.png',
   
},
   
(BrickType.explosive, BrickSize.size70x70) => {
     
BrickDamage.none: 'elementExplosive011.png',
     
BrickDamage.some: 'elementExplosive014.png',
     
BrickDamage.lots: 'elementExplosive049.png',
   
},
   
(BrickType.glass, BrickSize.size70x70) => {
     
BrickDamage.none: 'elementGlass011.png',
     
BrickDamage.some: 'elementGlass012.png',
     
BrickDamage.lots: 'elementGlass046.png',
   
},
   
(BrickType.metal, BrickSize.size70x70) => {
     
BrickDamage.none: 'elementMetal011.png',
     
BrickDamage.some: 'elementMetal014.png',
     
BrickDamage.lots: 'elementMetal049.png',
   
},
   
(BrickType.stone, BrickSize.size70x70) => {
     
BrickDamage.none: 'elementStone011.png',
     
BrickDamage.some: 'elementStone014.png',
     
BrickDamage.lots: 'elementStone046.png',
   
},
   
(BrickType.wood, BrickSize.size70x70) => {
     
BrickDamage.none: 'elementWood010.png',
     
BrickDamage.some: 'elementWood013.png',
     
BrickDamage.lots: 'elementWood045.png',
   
},
   
(BrickType.explosive, BrickSize.size220x70) => {
     
BrickDamage.none: 'elementExplosive013.png',
     
BrickDamage.some: 'elementExplosive016.png',
     
BrickDamage.lots: 'elementExplosive051.png',
   
},
   
(BrickType.glass, BrickSize.size220x70) => {
     
BrickDamage.none: 'elementGlass014.png',
     
BrickDamage.some: 'elementGlass017.png',
     
BrickDamage.lots: 'elementGlass049.png',
   
},
   
(BrickType.metal, BrickSize.size220x70) => {
     
BrickDamage.none: 'elementMetal013.png',
     
BrickDamage.some: 'elementMetal016.png',
     
BrickDamage.lots: 'elementMetal051.png',
   
},
   
(BrickType.stone, BrickSize.size220x70) => {
     
BrickDamage.none: 'elementStone013.png',
     
BrickDamage.some: 'elementStone016.png',
     
BrickDamage.lots: 'elementStone048.png',
   
},
   
(BrickType.wood, BrickSize.size220x70) => {
     
BrickDamage.none: 'elementWood012.png',
     
BrickDamage.some: 'elementWood015.png',
     
BrickDamage.lots: 'elementWood047.png',
   
},
   
(BrickType.explosive, BrickSize.size70x140) => {
     
BrickDamage.none: 'elementExplosive017.png',
     
BrickDamage.some: 'elementExplosive022.png',
     
BrickDamage.lots: 'elementExplosive052.png',
   
},
   
(BrickType.glass, BrickSize.size70x140) => {
     
BrickDamage.none: 'elementGlass018.png',
     
BrickDamage.some: 'elementGlass023.png',
     
BrickDamage.lots: 'elementGlass050.png',
   
},
   
(BrickType.metal, BrickSize.size70x140) => {
     
BrickDamage.none: 'elementMetal017.png',
     
BrickDamage.some: 'elementMetal022.png',
     
BrickDamage.lots: 'elementMetal052.png',
   
},
   
(BrickType.stone, BrickSize.size70x140) => {
     
BrickDamage.none: 'elementStone017.png',
     
BrickDamage.some: 'elementStone022.png',
     
BrickDamage.lots: 'elementStone049.png',
   
},
   
(BrickType.wood, BrickSize.size70x140) => {
     
BrickDamage.none: 'elementWood016.png',
     
BrickDamage.some: 'elementWood021.png',
     
BrickDamage.lots: 'elementWood048.png',
   
},
   
(BrickType.explosive, BrickSize.size140x140) => {
     
BrickDamage.none: 'elementExplosive018.png',
     
BrickDamage.some: 'elementExplosive023.png',
     
BrickDamage.lots: 'elementExplosive053.png',
   
},
   
(BrickType.glass, BrickSize.size140x140) => {
     
BrickDamage.none: 'elementGlass019.png',
     
BrickDamage.some: 'elementGlass024.png',
     
BrickDamage.lots: 'elementGlass051.png',
   
},
   
(BrickType.metal, BrickSize.size140x140) => {
     
BrickDamage.none: 'elementMetal018.png',
     
BrickDamage.some: 'elementMetal023.png',
     
BrickDamage.lots: 'elementMetal053.png',
   
},
   
(BrickType.stone, BrickSize.size140x140) => {
     
BrickDamage.none: 'elementStone018.png',
     
BrickDamage.some: 'elementStone023.png',
     
BrickDamage.lots: 'elementStone050.png',
   
},
   
(BrickType.wood, BrickSize.size140x140) => {
     
BrickDamage.none: 'elementWood017.png',
     
BrickDamage.some: 'elementWood022.png',
     
BrickDamage.lots: 'elementWood049.png',
   
},
   
(BrickType.explosive, BrickSize.size220x140) => {
     
BrickDamage.none: 'elementExplosive019.png',
     
BrickDamage.some: 'elementExplosive024.png',
     
BrickDamage.lots: 'elementExplosive054.png',
   
},
   
(BrickType.glass, BrickSize.size220x140) => {
     
BrickDamage.none: 'elementGlass020.png',
     
BrickDamage.some: 'elementGlass025.png',
     
BrickDamage.lots: 'elementGlass052.png',
   
},
   
(BrickType.metal, BrickSize.size220x140) => {
     
BrickDamage.none: 'elementMetal019.png',
     
BrickDamage.some: 'elementMetal024.png',
     
BrickDamage.lots: 'elementMetal054.png',
   
},
   
(BrickType.stone, BrickSize.size220x140) => {
     
BrickDamage.none: 'elementStone019.png',
     
BrickDamage.some: 'elementStone024.png',
     
BrickDamage.lots: 'elementStone051.png',
   
},
   
(BrickType.wood, BrickSize.size220x140) => {
     
BrickDamage.none: 'elementWood018.png',
     
BrickDamage.some: 'elementWood023.png',
     
BrickDamage.lots: 'elementWood050.png',
   
},
   
(BrickType.explosive, BrickSize.size70x220) => {
     
BrickDamage.none: 'elementExplosive020.png',
     
BrickDamage.some: 'elementExplosive025.png',
     
BrickDamage.lots: 'elementExplosive055.png',
   
},
   
(BrickType.glass, BrickSize.size70x220) => {
     
BrickDamage.none: 'elementGlass021.png',
     
BrickDamage.some: 'elementGlass026.png',
     
BrickDamage.lots: 'elementGlass053.png',
   
},
   
(BrickType.metal, BrickSize.size70x220) => {
     
BrickDamage.none: 'elementMetal020.png',
     
BrickDamage.some: 'elementMetal025.png',
     
BrickDamage.lots: 'elementMetal055.png',
   
},
   
(BrickType.stone, BrickSize.size70x220) => {
     
BrickDamage.none: 'elementStone020.png',
     
BrickDamage.some: 'elementStone025.png',
     
BrickDamage.lots: 'elementStone052.png',
   
},
   
(BrickType.wood, BrickSize.size70x220) => {
     
BrickDamage.none: 'elementWood019.png',
     
BrickDamage.some: 'elementWood024.png',
     
BrickDamage.lots: 'elementWood051.png',
   
},
   
(BrickType.explosive, BrickSize.size140x220) => {
     
BrickDamage.none: 'elementExplosive021.png',
     
BrickDamage.some: 'elementExplosive026.png',
     
BrickDamage.lots: 'elementExplosive056.png',
   
},
   
(BrickType.glass, BrickSize.size140x220) => {
     
BrickDamage.none: 'elementGlass022.png',
     
BrickDamage.some: 'elementGlass027.png',
     
BrickDamage.lots: 'elementGlass054.png',
   
},
   
(BrickType.metal, BrickSize.size140x220) => {
     
BrickDamage.none: 'elementMetal021.png',
     
BrickDamage.some: 'elementMetal026.png',
     
BrickDamage.lots: 'elementMetal056.png',
   
},
   
(BrickType.stone, BrickSize.size140x220) => {
     
BrickDamage.none: 'elementStone021.png',
     
BrickDamage.some: 'elementStone026.png',
     
BrickDamage.lots: 'elementStone053.png',
   
},
   
(BrickType.wood, BrickSize.size140x220) => {
     
BrickDamage.none: 'elementWood020.png',
     
BrickDamage.some: 'elementWood025.png',
     
BrickDamage.lots: 'elementWood052.png',
   
},
 
};
}

class Brick extends BodyComponent {
 
Brick({
   
required this.type,
   
required this.size,
   
required BrickDamage damage,
   
required Vector2 position,
   
required Map<BrickDamage, Sprite> sprites,
 
}) : _damage = damage,
       
_sprites = sprites,
       
super(
         
renderBody: false,
         
bodyDef: BodyDef()
           
..position = position
           
..type = BodyType.dynamic,
         
fixtureDefs: [
           
FixtureDef(
               
PolygonShape()..setAsBoxXY(
                 
size.size.width / 20 * brickScale,
                 
size.size.height / 20 * brickScale,
               
),
             
)
             
..restitution = 0.4
             
..density = type.density
             
..friction = type.friction,
         
],
       
);

 
late final SpriteComponent _spriteComponent;

 
final BrickType type;
 
final BrickSize size;
 
final Map<BrickDamage, Sprite> _sprites;

 
BrickDamage _damage;
 
BrickDamage get damage => _damage;
 
set damage(BrickDamage value) {
   
_damage = value;
   
_spriteComponent.sprite = _sprites[value];
 
}

 
@override
 
Future<void> onLoad() {
   
_spriteComponent = SpriteComponent(
     
anchor: Anchor.center,
     
scale: Vector2.all(1),
     
sprite: _sprites[_damage],
     
size: size.size.toVector2() / 10 * brickScale,
     
position: Vector2(0, 0),
   
);
   
add(_spriteComponent);
   
return super.onLoad();
 
}
}

Artık daha önce oluşturulan Dart kodunun, tuğla resimlerini malzemeye, boyuta ve duruma göre hızlıca seçmenizi sağlamak için bu kod tabanına nasıl entegre edildiğini görebilirsiniz. enum'leri geçip Brick bileşenine baktığınızda bu kodun çoğunun önceki adımdaki Ground bileşenine oldukça aşina geldiğini göreceksiniz. Tuğlanın hasar görmesine izin vermek için burada değişken durum vardır. Ancak bu durumun nasıl kullanılacağı okuyucuya bırakılmıştır.

Tuğlaları ekrana getirme zamanı. game.dart dosyasını aşağıdaki gibi düzenleyin:

lib/components/game.dart

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

import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';

import 'background.dart';
import 'brick.dart';                                       // Add this import
import 'ground.dart';

class MyPhysicsGame extends Forge2DGame {
 
MyPhysicsGame()
   
: super(
       
gravity: Vector2(0, 10),
       
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
     
);

 
late final XmlSpriteSheet aliens;
 
late final XmlSpriteSheet elements;
 
late final XmlSpriteSheet tiles;

 
@override
 
FutureOr<void> onLoad() async {
   
final backgroundImage = await images.load('colored_grass.png');
   
final spriteSheets = await Future.wait([
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_aliens.png',
       
xmlPath: 'spritesheet_aliens.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_elements.png',
       
xmlPath: 'spritesheet_elements.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_tiles.png',
       
xmlPath: 'spritesheet_tiles.xml',
     
),
   
]);

   
aliens = spriteSheets[0];
   
elements = spriteSheets[1];
   
tiles = spriteSheets[2];

   
await world.add(Background(sprite: Sprite(backgroundImage)));
   
await addGround();
   
unawaited(addBricks());                                // Add this line

   
return super.onLoad();
 
}

 
Future<void> addGround() {
   
return world.addAll([
     
for (
       
var x = camera.visibleWorldRect.left;
       
x < camera.visibleWorldRect.right + groundSize;
       
x += groundSize
     
)
       
Ground(
         
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
         
tiles.getSprite('grass.png'),
       
),
   
]);
 
}

 
final _random = Random();                                // Add from here...

 
Future<void> addBricks() async {
   
for (var i = 0; i < 5; i++) {
     
final type = BrickType.randomType;
     
final size = BrickSize.randomSize;
     
await world.add(
       
Brick(
         
type: type,
         
size: size,
         
damage: BrickDamage.some,
         
position: Vector2(
           
camera.visibleWorldRect.right / 3 +
               
(_random.nextDouble() * 5 - 2.5),
           
0,
         
),
         
sprites: brickFileNames(
           
type,
           
size,
         
).map((key, filename) => MapEntry(key, elements.getSprite(filename))),
       
),
     
);
     
await Future<void>.delayed(const Duration(milliseconds: 500));
   
}
 
}                                                        // To here.
}

Bu kod ekleme, Ground bileşenlerini eklemek için kullandığınız koddan biraz farklıdır. Bu sefer Brick'ler zaman içinde rastgele bir kümeye eklenir. Bunun iki bölümü vardır. Birincisi, Brick'leri ekleyen yöntemin sleep() çağrısının eşzamansız eşdeğeri olan bir Future.delayed olmasıdır.await Ancak bu işlemin işe yaraması için ikinci bir şart vardır. onLoad yönteminde addBricks çağrısı await edilmemelidir. Aksi takdirde, tüm tuğlalar ekrana gelene kadar onLoad yöntemi tamamlanmaz. addBricks çağrısını bir unawaited çağrısına sarmalamak, linters'i mutlu eder ve amacımızı gelecekteki programcılara açıkça gösterir. Bu yöntemin döndürülmesi beklenmez.

Oyunu çalıştırdığınızda tuğlaların birbirine çarptığını ve yere döküldüğünü görürsünüz.

Arka planda yeşil tepeler, zemin katmanı ve yere inen blokların yer aldığı bir uygulama penceresi.

6. Oyuncu ekleme

Tuğlalara uzaylı atma

Tuğlaların yıkılmasını izlemek ilk birkaç kez eğlenceli olsa da oyuncuya dünyayla etkileşime geçebileceği bir avatar verirsek bu oyunun daha eğlenceli olacağını düşünüyorum. Tuğlalara atabilecekleri bir uzaylıya ne dersiniz?

lib/components dizininde yeni bir player.dart dosyası oluşturun ve dosyaya aşağıdakileri ekleyin:

lib/components/player.dart

import 'dart:math';

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

const playerSize = 5.0;

enum PlayerColor {
 
pink,
 
blue,
 
green,
 
yellow;

 
static PlayerColor get randomColor =>
     
PlayerColor.values[Random().nextInt(PlayerColor.values.length)];

 
String get fileName =>
     
'alien${toString().split('.').last.capitalize}_round.png';
}

class Player extends BodyComponent with DragCallbacks {
 
Player(Vector2 position, Sprite sprite)
   
: _sprite = sprite,
     
super(
       
renderBody: false,
       
bodyDef: BodyDef()
         
..position = position
         
..type = BodyType.static
         
..angularDamping = 0.1
         
..linearDamping = 0.1,
       
fixtureDefs: [
         
FixtureDef(CircleShape()..radius = playerSize / 2)
           
..restitution = 0.4
           
..density = 0.75
           
..friction = 0.5,
       
],
     
);

 
final Sprite _sprite;

 
@override
 
Future<void> onLoad() {
   
addAll([
     
CustomPainterComponent(
       
painter: _DragPainter(this),
       
anchor: Anchor.center,
       
size: Vector2(playerSize, playerSize),
       
position: Vector2(0, 0),
     
),
     
SpriteComponent(
       
anchor: Anchor.center,
       
sprite: _sprite,
       
size: Vector2(playerSize, playerSize),
       
position: Vector2(0, 0),
     
),
   
]);
   
return super.onLoad();
 
}

 
@override
 
void update(double dt) {
   
super.update(dt);

   
if (!body.isAwake) {
     
removeFromParent();
   
}

   
if (position.x > camera.visibleWorldRect.right + 10 ||
       
position.x < camera.visibleWorldRect.left - 10) {
     
removeFromParent();
   
}
 
}

 
Vector2 _dragStart = Vector2.zero();
 
Vector2 _dragDelta = Vector2.zero();
 
Vector2 get dragDelta => _dragDelta;

 
@override
 
void onDragStart(DragStartEvent event) {
   
super.onDragStart(event);
   
if (body.bodyType == BodyType.static) {
     
_dragStart = event.localPosition;
   
}
 
}

 
@override
 
void onDragUpdate(DragUpdateEvent event) {
   
if (body.bodyType == BodyType.static) {
     
_dragDelta = event.localEndPosition - _dragStart;
   
}
 
}

 
@override
 
void onDragEnd(DragEndEvent event) {
   
super.onDragEnd(event);
   
if (body.bodyType == BodyType.static) {
     
children
         
.whereType<CustomPainterComponent>()
         
.firstOrNull
         
?.removeFromParent();
     
body.setType(BodyType.dynamic);
     
body.applyLinearImpulse(_dragDelta * -50);
     
add(RemoveEffect(delay: 5.0));
   
}
 
}
}

extension on String {
 
String get capitalize =>
     
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}

class _DragPainter extends CustomPainter {
 
_DragPainter(this.player);

 
final Player player;

 
@override
 
void paint(Canvas canvas, Size size) {
   
if (player.dragDelta != Vector2.zero()) {
     
var center = size.center(Offset.zero);
     
canvas.drawLine(
       
center,
       
center + (player.dragDelta * -1).toOffset(),
       
Paint()
         
..color = Colors.orange.withAlpha(180)
         
..strokeWidth = 0.4
         
..strokeCap = StrokeCap.round,
     
);
   
}
 
}

 
@override
 
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

Bu, önceki adımdaki Brick bileşenlerinden daha gelişmiş bir bileşendir. Bu Player bileşeninin iki alt bileşeni vardır: Tanıdığınız bir SpriteComponent ve yeni bir CustomPainterComponent. Flutter'dan alınan CustomPainter kavramı, tuval üzerinde resim yapmanıza olanak tanır. Burada, oyuncuya yuvarlak uzaylı fırlatıldığında nereye uçacağıyla ilgili geri bildirim vermek için kullanılır.

Oyuncu uzaylıyı nasıl fırlatır? Oynatıcı bileşeninin DragCallbacks geri çağırmalarıyla algıladığı bir sürükleme hareketi kullanarak. Dikkatli gözleriniz burada başka bir şey fark etmiş olabilir.

Ground bileşenleri statik gövdeler iken Brick bileşenleri dinamik gövdelerdi. Buradaki oyuncu, her ikisinin de bir kombinasyonudur. Oynatıcı, statik olarak başlar ve kullanıcının sürüklemesini bekler. Sürüklemeyi bıraktığında ise statikten dinamik hale gelir, sürüklemeyle orantılı olarak doğrusal dürtü ekler ve uzaylı avatarın uçmasını sağlar.

Player bileşeninde, sınırların dışına çıkarsa, uykuya geçerse veya zaman aşımına uğrarsa ekrandan kaldırılmasını sağlayan kod da vardır. Burada amaç, oyuncunun uzaylıyı fırlatmasına, ne olduğunu görmesine ve ardından tekrar denemesine izin vermektir.

game.dart bileşenini oyuna entegre etmek için game.dart'yi aşağıdaki gibi düzenleyin:Player

lib/components/game.dart

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

import 'package:flame/components.dart';
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_kenney_xml/flame_kenney_xml.dart';

import 'background.dart';
import 'brick.dart';
import 'ground.dart';
import 'player.dart';                                      // Add this import

class MyPhysicsGame extends Forge2DGame {
 
MyPhysicsGame()
   
: super(
       
gravity: Vector2(0, 10),
       
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
     
);

 
late final XmlSpriteSheet aliens;
 
late final XmlSpriteSheet elements;
 
late final XmlSpriteSheet tiles;

 
@override
 
FutureOr<void> onLoad() async {
   
final backgroundImage = await images.load('colored_grass.png');
   
final spriteSheets = await Future.wait([
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_aliens.png',
       
xmlPath: 'spritesheet_aliens.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_elements.png',
       
xmlPath: 'spritesheet_elements.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_tiles.png',
       
xmlPath: 'spritesheet_tiles.xml',
     
),
   
]);

   
aliens = spriteSheets[0];
   
elements = spriteSheets[1];
   
tiles = spriteSheets[2];

   
await world.add(Background(sprite: Sprite(backgroundImage)));
   
await addGround();
   
unawaited(addBricks());
   
await addPlayer();                                     // Add this line

   
return super.onLoad();
 
}

 
Future<void> addGround() {
   
return world.addAll([
     
for (
       
var x = camera.visibleWorldRect.left;
       
x < camera.visibleWorldRect.right + groundSize;
       
x += groundSize
     
)
       
Ground(
         
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
         
tiles.getSprite('grass.png'),
       
),
   
]);
 
}

 
final _random = Random();

 
Future<void> addBricks() async {
   
for (var i = 0; i < 5; i++) {
     
final type = BrickType.randomType;
     
final size = BrickSize.randomSize;
     
await world.add(
       
Brick(
         
type: type,
         
size: size,
         
damage: BrickDamage.some,
         
position: Vector2(
           
camera.visibleWorldRect.right / 3 +
               
(_random.nextDouble() * 5 - 2.5),
           
0,
         
),
         
sprites: brickFileNames(
           
type,
           
size,
         
).map((key, filename) => MapEntry(key, elements.getSprite(filename))),
       
),
     
);
     
await Future<void>.delayed(const Duration(milliseconds: 500));
   
}
 
}

 
Future<void> addPlayer() async => world.add(             // Add from here...
   
Player(
     
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
     
aliens.getSprite(PlayerColor.randomColor.fileName),
   
),
 
);

 
@override
 
void update(double dt) {
   
super.update(dt);
   
if (isMounted && world.children.whereType<Player>().isEmpty) {
     
addPlayer();
   
}
 
}                                                        // To here.
}

Oyuncuyu oyuna ekleme işlemi, önceki bileşenlere benzer ancak bir fark vardır. Oyuncunun uzaylısı, belirli koşullar altında kendisini oyundan çıkaracak şekilde tasarlanmıştır. Bu nedenle, oyunda Player bileşeni olup olmadığını kontrol eden ve varsa tekrar ekleyen bir güncelleme işleyici vardır. Oyunun çalışması aşağıdaki gibi görünür.

Arka planda yeşil tepeler, zemin katmanı, yerdeki bloklar ve uçan bir oyuncu avatarının yer aldığı bir uygulama penceresi.

7. Etkileyici içeriklere tepki verme

Düşmanları ekleme

Statik ve dinamik nesnelerin birbirleriyle etkileşime geçtiğini gördünüz. Ancak gerçekten bir yere varmak için, öğeler çakıştığında kodda geri çağırma işlevleri almanız gerekir. Oyuncunun karşılaştığı bazı düşmanlar ekleyeceksiniz. Bu, kazanma koşuluna giden bir yol sunar: Oyundaki tüm düşmanları ortadan kaldırın.

lib/components dizininde bir enemy.dart dosyası oluşturun ve aşağıdakileri ekleyin:

lib/components/enemy.dart

import 'dart:math';

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

import 'body_component_with_user_data.dart';

const enemySize = 5.0;

enum EnemyColor {
 
pink(color: 'pink', boss: false),
 
blue(color: 'blue', boss: false),
 
green(color: 'green', boss: false),
 
yellow(color: 'yellow', boss: false),
 
pinkBoss(color: 'pink', boss: true),
 
blueBoss(color: 'blue', boss: true),
 
greenBoss(color: 'green', boss: true),
 
yellowBoss(color: 'yellow', boss: true);

 
final bool boss;
 
final String color;

 
const EnemyColor({required this.color, required this.boss});

 
static EnemyColor get randomColor =>
     
EnemyColor.values[Random().nextInt(EnemyColor.values.length)];

 
String get fileName =>
     
'alien${color.capitalize}_${boss ? 'suit' : 'square'}.png';
}

class Enemy extends BodyComponentWithUserData with ContactCallbacks {
 
Enemy(Vector2 position, Sprite sprite)
   
: super(
       
renderBody: false,
       
bodyDef: BodyDef()
         
..position = position
         
..type = BodyType.dynamic,
       
fixtureDefs: [
         
FixtureDef(
           
PolygonShape()..setAsBoxXY(enemySize / 2, enemySize / 2),
           
friction: 0.3,
         
),
       
],
       
children: [
         
SpriteComponent(
           
anchor: Anchor.center,
           
sprite: sprite,
           
size: Vector2.all(enemySize),
           
position: Vector2(0, 0),
         
),
       
],
     
);

 
@override
 
void beginContact(Object other, Contact contact) {
   
var interceptVelocity =
       
(contact.bodyA.linearVelocity - contact.bodyB.linearVelocity).length
           
.abs();
   
if (interceptVelocity > 35) {
     
removeFromParent();
   
}

   
super.beginContact(other, contact);
 
}

 
@override
 
void update(double dt) {
   
super.update(dt);

   
if (position.x > camera.visibleWorldRect.right + 10 ||
       
position.x < camera.visibleWorldRect.left - 10) {
     
removeFromParent();
   
}
 
}
}

extension on String {
 
String get capitalize =>
     
characters.first.toUpperCase() + characters.skip(1).toLowerCase().join();
}

Oynatıcı ve tuğla bileşenleriyle daha önce etkileşime geçtiğiniz için bu dosyanın büyük bir kısmı size tanıdık gelecektir. Ancak, bilinmeyen yeni bir temel sınıf nedeniyle düzenleyicinizde birkaç kırmızı alt çizgi olacaktır. Aşağıdaki içeriği içeren body_component_with_user_data.dart adlı bir dosyayı lib/components'a ekleyerek bu sınıfı hemen ekleyin:

lib/components/body_component_with_user_data.dart

import 'package:flame_forge2d/flame_forge2d.dart';

class BodyComponentWithUserData extends BodyComponent {
 
BodyComponentWithUserData({
   
super.key,
   
super.bodyDef,
   
super.children,
   
super.fixtureDefs,
   
super.paint,
   
super.priority,
   
super.renderBody,
 
});

 
@override
 
Body createBody() {
   
final body = world.createBody(super.bodyDef!)..userData = this;
   
fixtureDefs?.forEach(body.createFixture);
   
return body;
 
}
}

Bu temel sınıf, Enemy bileşenindeki yeni beginContact geri çağırma işleviyle birlikte, cisimler arasındaki etkileşimler hakkında programatik olarak bildirim alma temelini oluşturur. Aslında, etki bildirimleri almak istediğiniz tüm bileşenleri düzenlemeniz gerekir. Bu nedenle, Brick, Ground ve Player bileşenlerini, bu bileşenlerin kullandığı BodyComponent temel sınıf yerine bu BodyComponentWithUserData sınıfını kullanacak şekilde düzenleyin. Örneğin, Ground bileşenini düzenlemek için:

lib/components/ground.dart

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

import 'body_component_with_user_data.dart';               // Add this import

const groundSize = 7.0;

class Ground extends BodyComponentWithUserData {           // Edit this line
 
Ground(Vector2 position, Sprite sprite)
   
: super(
       
renderBody: false,
       
bodyDef: BodyDef()
         
..position = position
         
..type = BodyType.static,
       
fixtureDefs: [
         
FixtureDef(
           
PolygonShape()..setAsBoxXY(groundSize / 2, groundSize / 2),
           
friction: 0.3,
         
),
       
],
       
children: [
         
SpriteComponent(
           
anchor: Anchor.center,
           
sprite: sprite,
           
size: Vector2.all(groundSize),
           
position: Vector2(0, 0),
         
),
       
],
     
);
}

Forge2d'in iletişimi nasıl ele aldığı hakkında daha fazla bilgi için iletişim geri çağırmalarıyla ilgili Forge2D dokümanlarına bakın.

Oyunu kazanma

Artık düşmanlarınız ve düşmanları dünyadan kaldırmanın bir yolu var. Bu simülasyonu oyuna dönüştürmenin basit bir yolu var. Hedefiniz tüm düşmanları ortadan kaldırmak olsun. game.dart dosyasını aşağıdaki şekilde düzenlemeniz gerekiyor:

lib/components/game.dart

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

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

import 'background.dart';
import 'brick.dart';
import 'enemy.dart';                                       // Add this import
import 'ground.dart';
import 'player.dart';

class MyPhysicsGame extends Forge2DGame {
 
MyPhysicsGame()
   
: super(
       
gravity: Vector2(0, 10),
       
camera: CameraComponent.withFixedResolution(width: 800, height: 600),
     
);

 
late final XmlSpriteSheet aliens;
 
late final XmlSpriteSheet elements;
 
late final XmlSpriteSheet tiles;

 
@override
 
FutureOr<void> onLoad() async {
   
final backgroundImage = await images.load('colored_grass.png');
   
final spriteSheets = await Future.wait([
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_aliens.png',
       
xmlPath: 'spritesheet_aliens.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_elements.png',
       
xmlPath: 'spritesheet_elements.xml',
     
),
     
XmlSpriteSheet.load(
       
imagePath: 'spritesheet_tiles.png',
       
xmlPath: 'spritesheet_tiles.xml',
     
),
   
]);

   
aliens = spriteSheets[0];
   
elements = spriteSheets[1];
   
tiles = spriteSheets[2];

   
await world.add(Background(sprite: Sprite(backgroundImage)));
   
await addGround();
   
unawaited(addBricks().then((_) => addEnemies()));      // Modify this line
   
await addPlayer();

   
return super.onLoad();
 
}

 
Future<void> addGround() {
   
return world.addAll([
     
for (
       
var x = camera.visibleWorldRect.left;
       
x < camera.visibleWorldRect.right + groundSize;
       
x += groundSize
     
)
       
Ground(
         
Vector2(x, (camera.visibleWorldRect.height - groundSize) / 2),
         
tiles.getSprite('grass.png'),
       
),
   
]);
 
}

 
final _random = Random();

 
Future<void> addBricks() async {
   
for (var i = 0; i < 5; i++) {
     
final type = BrickType.randomType;
     
final size = BrickSize.randomSize;
     
await world.add(
       
Brick(
         
type: type,
         
size: size,
         
damage: BrickDamage.some,
         
position: Vector2(
           
camera.visibleWorldRect.right / 3 +
               
(_random.nextDouble() * 5 - 2.5),
           
0,
         
),
         
sprites: brickFileNames(
           
type,
           
size,
         
).map((key, filename) => MapEntry(key, elements.getSprite(filename))),
       
),
     
);
     
await Future<void>.delayed(const Duration(milliseconds: 500));
   
}
 
}

 
Future<void> addPlayer() async => world.add(
   
Player(
     
Vector2(camera.visibleWorldRect.left * 2 / 3, 0),
     
aliens.getSprite(PlayerColor.randomColor.fileName),
   
),
 
);

 
@override
 
void update(double dt) {
   
super.update(dt);
   
if (isMounted &&                                       // Modify from here...
       
world.children.whereType<Player>().isEmpty &&
       
world.children.whereType<Enemy>().isNotEmpty) {
     
addPlayer();
   
}
   
if (isMounted &&
       
enemiesFullyAdded &&
       
world.children.whereType<Enemy>().isEmpty &&
       
world.children.whereType<TextComponent>().isEmpty) {
     
world.addAll(
       
[
         
(position: Vector2(0.5, 0.5), color: Colors.white),
         
(position: Vector2.zero(), color: Colors.orangeAccent),
       
].map(
         
(e) => TextComponent(
           
text: 'You win!',
           
anchor: Anchor.center,
           
position: e.position,
           
textRenderer: TextPaint(
             
style: TextStyle(color: e.color, fontSize: 16),
           
),
         
),
       
),
     
);
   
}
 
}

 
var enemiesFullyAdded = false;

 
Future<void> addEnemies() async {
   
await Future<void>.delayed(const Duration(seconds: 2));
   
for (var i = 0; i < 3; i++) {
     
await world.add(
       
Enemy(
         
Vector2(
           
camera.visibleWorldRect.right / 3 +
               
(_random.nextDouble() * 7 - 3.5),
           
(_random.nextDouble() * 3),
         
),
         
aliens.getSprite(EnemyColor.randomColor.fileName),
       
),
     
);
     
await Future<void>.delayed(const Duration(seconds: 1));
   
}
   
enemiesFullyAdded = true;                              // To here.
 
}
}

Kabul edersen sana çok gizli bir görev veriyorum: Oyunu çalıştırıp bu ekrana gel.

Arka planda yeşil tepeler, zemin katmanı, zemindeki bloklar ve &quot;Kazandınız!&quot; metninin yer aldığı bir uygulama penceresi

8. Tebrikler

Tebrikler, Flutter ve Flame ile oyun oluşturmayı başardınız.

Flame 2D oyun motorunu kullanarak bir oyun oluşturdunuz ve bunu bir Flutter sarmalayıcısına yerleştirdiniz. Bileşenleri canlandırmak ve kaldırmak için Flame'ın efektlerini kullandınız. Oyunun tamamının iyi tasarlanmış görünmesi için Google Fonts ve Flutter Animate paketlerini kullandınız.

Sırada ne var?

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

Daha fazla bilgi