Bu codelab hakkında
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:
Ön koşullar
- Flutter ile Flame'a Giriş codelab'inin tamamlanması
Öğ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:
- 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.
- 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:
- 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.
- 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.
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/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_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.
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.
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.
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.
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.
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.
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...
- Flutter'da yeni nesil kullanıcı arayüzleri oluşturma
- Flutter uygulamanızı sıkıcı olmaktan çıkarıp güzelleştirme
- Flutter uygulamanıza uygulama içi satın alma işlemleri ekleme