1. Başlamadan önce
Flame, Flutter tabanlı bir 2D oyun motorudur. Bu codelab'de, Box2D'deki Forge2D adıyla benzer 2D fizik simülasyonu kullanan bir oyun geliştireceksiniz. Flame'in bileşenlerini kullanarak fiziksel gerçekliğin simülasyonunu gerçekleştiriyorsunuz. Böylece kullanıcılar oyun oynayabilir. Tamamlandığında, oyununuz şu animasyonlu GIF şeklinde görünmelidir:
Ön koşullar
- Flutter ile Flame'e Giriş codelab'inin tamamlanması
Öğrenecekleriniz
- Farklı fiziksel vücut türlerinden başlayarak Forge2D'nin temellerinin işleyiş şekli.
- 2D fiziksel simülasyon nasıl ayarlanır?
İhtiyacınız olanlar
Seçtiğiniz geliştirme hedefi için derleyici yazılımı. Bu codelab, 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, komut satırını kısaltmak için kullanacaksı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.3.0 (from transitive dependency to direct dependency) collection 1.18.0 (1.19.0 available) + flame 1.18.0 + flame_forge2d 0.18.1 + flame_kenney_xml 0.1.0 flutter_lints 3.0.2 (4.0.0 available) + forge2d 0.13.0 leak_tracker 10.0.4 (10.0.5 available) leak_tracker_flutter_testing 3.0.3 (3.0.5 available) lints 3.0.0 (4.0.0 available) material_color_utilities 0.8.0 (0.12.0 available) meta 1.12.0 (1.15.0 available) + ordered_set 5.0.3 (6.0.1 available) + petitparser 6.0.2 test_api 0.7.0 (0.7.3 available) vm_service 14.2.1 (14.2.4 available) + xml 6.5.0 Changed 8 dependencies! 10 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
flame
paketi size tanıdık gelecektir ancak diğer üç paketin açıklaması gerekebilir. characters
paketi, UTF8'e uygun şekilde dosya yolunu değiştirmek için kullanılır. flame_forge2d
paketi, Forge2D işlevini Flame ile uyumlu bir şekilde ortaya koyar. 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ğıdaki kodla 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 örnekleyen bir GameWidget
ile başlatır. Bu codelab'de, çalışan oyun hakkındaki bilgileri görüntülemek için oyun örneğinin durumunu kullanan bir Flutter kodu yoktur. Bu nedenle, bu basitleştirilmiş önyükleme sorunsuz bir şekilde çalışır.
İsteğe bağlı: macOS'e özel yan bir göreve katılın
Bu projedeki ekran görüntüleri, macOS masaüstü uygulaması olan 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 kaydıracak şekilde değiştirebilirsiniz.
Bunun için, aşağıdaki adımları uygulayın:
- Bir
bin/modify_macos_config.dart
dosyası oluşturun ve şu içeriği ekleyin:
bin/modify_macos_config.dart ile değiştirin
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ı kod tabanının bir parçası olmadığı için lib
dizininde değil. Projede değişiklik yapmak için kullanılan bir komut satırı aracıdır.
- Proje temel dizininden aracı aşağıdaki şekilde çalıştırın:
$ dart bin/modify_macos_config.dart
Her şey planlandığı gibi giderse program, komut satırında herhangi bir çıkış oluşturmaz. Bununla birlikte, macos/Runner/Base.lproj/MainMenu.xib
yapılandırma dosyasını oyunu görünür bir başlık çubuğu olmadan ve tüm pencereyi kaplayacak şekilde Flame oyunu kaplayacak şekilde çalıştırmak için değiştirir.
Her şeyin çalıştığını doğrulamak için oyunu çalıştırın. Yalnızca boş siyah arka planı olan yeni bir pencere görüntülemelidir.
3. Resim öğeleri ekleme
Resimleri ekleyin
Bir ekranı eğlence bulmayı kullanacak şekilde boyayabilmek için tüm oyunların sanat öğeleri içermesi gerekir. Bu codelab'de Kenney.nl adresindeki Fizik Öğeleri paketi kullanılır. Bu öğeler Creative Commons CC0 lisansına sahip olsa da ortaya koydukları mükemmel çalışmaya devam edebilmeleri için Kenney'deki ekibe bağışta bulunmanızı kesinlikle tavsiye ediyorum. Yaptım.
Kenney'nin öğelerinin kullanımını etkinleştirmek için pubspec.yaml
yapılandırma dosyasını değiştirmeniz gerekir. Şu şekilde değiştirin:
pubspec.yaml
name: forge2d_game
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.3 <4.0.0'
dependencies:
characters: ^1.3.0
flame: ^1.17.0
flame_forge2d: ^0.18.0
flutter:
sdk: flutter
xml: ^6.5.0
dev_dependencies:
flutter_lints: ^3.0.0
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets: # Add from here
- assets/
- assets/images/ # To here.
Flame, resim öğelerinin farklı şekilde yapılandırılabilir olsa da assets/images
konumunda bulunmasını bekliyor. Daha ayrıntılı bilgi için Flame'in Görseller belgelerine bakın. Yolları yapılandırdığınıza göre artık projenin kendisine eklemeniz gerekir. Bunu yapmanın bir yolu komut satırını aşağıdaki şekilde kullanmaktır:
$ mkdir -p assets/images
mkdir
komutundan çıkış olmamalıdır, ancak yeni dizin düzenleyicinizde veya dosya gezgininde görünür olmalıdır.
İndirdiğiniz kenney_physics-assets.zip
dosyasını genişlettiğinizde şuna benzer bir sonuç 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.
Model sayfaları da mevcuttur. Bu dosyalar, bir PNG resmi ile daha küçük resimlerin model sayfası resminin nerede bulunabileceğini açıklayan bir XML dosyasının birleşimidir. Model sayfaları, yüzlerce, hatta yüzlerce ayrı resim dosyası yerine yalnızca tek bir dosya yükleyerek yükleme süresini azaltmaya yönelik bir tekniktir.
spritesheet_aliens.png
, spritesheet_elements.png
ve spritesheet_tiles.png
alanlarını projenizin assets/images
dizinine kopyalayın. Bu sayfada ayrıca spritesheet_aliens.xml
, spritesheet_elements.xml
ve spritesheet_tiles.xml
dosyalarını projenizin assets
dizinine kopyalayın. Projeniz aşağıdaki gibi görünmelidir.
Arka planı boya
Artık projenize resim öğeleri eklendiğine göre bunları ekrana koymanın zamanı geldi. Ekranda bir resim var. Sonraki adımlarda daha fazla özellik eklenecektir.
lib/components
adlı yeni 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
öğesidir. Kenney.nl'nin dört arka plan resminden birini görüntülemekten sorumludur. Bu kodda basitleştirici birkaç varsayım vardır. İlk olarak, resimler kare şeklinde ve Kerem'in arka plan resimlerinin dördü de kare şeklinde. İkincisi ise görünür dünyanın boyutunun hiçbir zaman değişmemesidir. Aksi takdirde, bu bileşenin oyun yeniden boyutlandırma etkinliklerini yönetmesi gerekir. Üçüncü varsayım, konumun (0,0) ekranın ortasında olacağıdır. Bu varsayımlar için oyuna ait CameraComponent
öğesinin belirli bir şekilde yapılandırılması gerekir.
lib/components
dizininde tekrar game.dart
adında başka bir dosya 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şlayalım. Önceki codelab'den farklı olarak bu, FlameGame
yerine Forge2DGame
öğesini kapsar. Forge2DGame
, birkaç ilginç değişiklikle FlameGame
alanını genişletiyor. Birincisi, zoom
politikasının varsayılan olarak 10 değerine ayarlanmasıdır. Bu zoom
ayarı, Box2D
stili fizik simülasyonu motorlarının iyi çalıştığı yararlı değer aralığıyla ilgilidir. Motor, MKS sistemi kullanılarak yazılmıştır. Bu sistemde birimlerin metre, kilogram ve saniye cinsinden olduğu varsayılmıştır. Nesneler için fark edilebilir matematik hataları görmediğiniz aralık 0,1 metre ile 10 saniye arasında değişir. Belirli bir aşağı ölçeklendirme düzeyi olmadan doğrudan piksel boyutlarında feed besleme yapmak, Forge2D'yi kullanışlı sınırlarının dışına çıkarır. Buradaki özetin faydası, sodanın otobüse kadar uzandığı mesafedeki nesnelerin simüle edilmesidir.
CameraComponent
öğesinin çözünürlüğü 800x600 sanal piksel olarak düzeltilerek, Arka Plan bileşeninde yapılan varsayımlar yerine getirildi. Bu, oyun alanının (0,0) merkezli şekilde 80 birim genişliğinde ve 60 birim yüksekliğinde olacağı anlamına gelir. Bu seçim, görüntülenen çözünürlüğü etkilemez ancak nesneleri oyun sahnesinde yerleştirdiğimiz yeri etkiler.
camera
oluşturucu bağımsız değişkeninin yanında, gravity
adında fizik kurallarına uygun başka bir bağımsız değişken daha vardır. Yer çekimi x
değeri 0 ve y
değeri 10 olacak şekilde Vector2
olarak ayarlandı. 10, genellikle kabul edilen 9,81 metre/saniye yer çekimi değerine yakın bir değerdir. Yer çekiminin pozitif 10'a ayarlanması, bu sistemde Y ekseninin yönünün aşağı doğru olduğunu gösterir. Genel olarak Box2D'den farklıdır ancak Flame'in genellikle yapılandırılma şekliyle uyumludur.
Sırada onLoad
yöntemi var. Bu yöntem eşzamansızdır ve diskten resim öğelerinin yüklenmesinden sorumlu olduğu için uygundur. images.load
çağrıları, bir Future<Image>
döndürür ve yan efekt olarak, yüklenen resim Oyun nesnesinde önbelleğe alınır. Bu gelecekler bir araya toplanır ve Futures.wait
statik yöntemi kullanılarak tek bir birim olarak beklenmektedir. Döndürülen görsellerin listesi daha sonra kalıp, tek tek adlarla eşleştirilir.
Daha sonra model sayfası resimleri bir dizi XmlSpriteSheet
nesneye beslenir ve bu nesneler, model sayfasında yer alan bağımsız olarak adlandırılmış Sprite öğeleri almaktan sorumlu olur. XmlSpriteSheet
sınıfı flame_kenney_xml
paketinde tanımlanmıştır.
Tüm bunların yanı sıra, ekranda bir görselin yer alması için lib/main.dart
ürününde yalnızca birkaç küçük düzenleme yapmanız yeterli olacaktır.
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 basit değişiklikle artık oyunu tekrar çalıştırabilir ve ekranda arka planı görebilirsiniz. Oyunun 800x600 oranını çalışması için CameraComponent.withFixedResolution()
kamera örneğinin sinemaskop efekti ekleyeceğini unutmayın.
4. Zemin ekleyin
Üzerine inşa edilecek bir şey
Yer çekimi varsa oyunda nesneleri ekranın altından düşmeden önce yakalamak için bir şeye ihtiyacımız vardır. Ekrandan çıkmak oyun tasarımınızın bir parçası değilse elbette. lib/components
dizininizde yeni bir ground.dart
dosyası oluşturun ve aşağıdakini bu dosyaya 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
kaynağından türetilir. Forge2D'de cisimler önemlidir. Bunlar, iki boyutlu fiziksel simülasyonun parçası olan nesnelerdir. Bu bileşen için BodyDef
değeri, BodyType.static
olarak belirtilmiş.
Forge2D'de vücutların üç farklı türü vardır. Statik cisimler hareket etmez. Bu cisimlerin hem kütlesi sıfırdır hem de yer çekimine tepki vermez ve sonsuz kütleye sahiptir. Ne kadar ağır olursa olsun diğer cisimler çarptığında hareket etmez. Bu, statik cisimler hareket etmediğinden zemin yüzeyi için mükemmeldir.
Diğer iki cisim türü kinematik ve dinamiktir. Dinamik cisimler tamamen simüle edilmiş vücutlardır. Yer çekimine ve çarpıştıkları cisimlere tepki verirler. Bu codelab'in geri kalanında çok sayıda dinamik gövde göreceksiniz. Kinematik cisimler statik ve dinamik arasında bir evin ortasındadır. Hareket ederler ancak yer çekimine veya diğer nesnelere çarpmasına tepki vermezler. Faydalı ancak bu codelab'in kapsamı dışındadır.
Vücudun kendisi fazla bir şey yapmaz. Vücudun özdeş olması için birbiriyle ilişkili şekillere ihtiyacı vardır. Bu durumda, bu gövdenin ilişkilendirilmiş bir şekli vardır ve PolygonShape
, BoxXY
olarak ayarlanmıştır. Bu kutu türü, bir dönme noktası etrafında döndürülebilen BoxXY
olarak ayarlanmış PolygonShape
öğelerinin aksine, dünyayla hizalı bir eksendir. Yine faydalıdır ancak bu codelab'in kapsamı dışındadır. Şekil ve gövde bir armatürle birbirine bağlanır. Bu, sisteme friction
gibi öğelerin eklenmesinde kullanışlıdır.
Gövde, varsayılan olarak ekli şekilleri hata ayıklama için faydalı olacak, ancak oynanabilirlik açısından faydalı olmayacak şekilde oluşturur. 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 gövdeye oyun içi oluşturma işlemi verilmesi, 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, List
bağlamı içinde bir for
döngüsü kullanarak ve elde edilen Ground
bileşen listesini world
öğesinin addAll
yöntemine ileterek bir dizi Ground
bileşeni ekler.
Oyun çalıştırıldığında artık arka plan ve zemin gösteriliyor.
5. Tuğlaları ekleyin
Duvar inşa etme
Zemin, bize statik bir gövde örneği gösterdi. Şimdi ilk dinamik bileşeninizin zamanı geldi. Forge2D'deki dinamik bileşenler, oyuncunun deneyiminin temel taşı olup etrafındaki dünyayı hareket ettiren ve onunla etkileşime giren şeylerdir. Bu adımda, ekranda bir grup tuğla halinde görünecek şekilde rastgele seçilecek tuğlaları yerleştireceksiniz. Bunu yaparken birbirlerine düşüp çarpıştıklarını göreceksiniz.
Tuğlalar, öğelerin model sayfasından oluşturulacak. assets/spritesheet_elements.xml
içindeki model sayfası açıklamasına bakarsanız ilginç bir sorunumuz olduğunu göreceksiniz. Adlar pek faydalı görünmüyor. Bir tuğlayı malzeme türüne, boyutuna ve hasar miktarına göre seçmek faydalı olacaktır. Neyse ki yardımsever bir elf dosya adlandırma kalıbını bulmak için biraz vakit geçirdi ve hepinize kolaylık sağlayacak bir araç geliştirdi. 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 bir bağımlılık hakkında uyarı veya hata gösterecektir. Aşağıdaki gibi ekleyin:
$ flutter pub add equatable
Artık bu programı aşağıdaki şekilde çalıştırabiliyor olmanız gerekir:
$ 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ç, model sayfası açıklama dosyasını faydalı bir şekilde ayrıştırdı ve ekrana yerleştirmek istediğiniz her bir tuğla için doğru resim dosyasını seçmek üzere kullanabileceğimiz Dart koduna dönüştürdü. Yararlı!
Aşağıdaki içeriğe sahip 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();
}
}
Tuğla resimleri malzeme, boyut ve duruma göre hızlı ve kolay bir şekilde seçmek için, yukarıda oluşturulan Dart kodunun bu kod tabanına nasıl entegre edildiğini görebilirsiniz. enum
öğelerini geçip Brick
bileşeninin kendisine baktığınızda, bu kodun büyük kısmının önceki adımdaki Ground
bileşenine göre oldukça tanıdık geldiğini görürsünüz. Burada, tuğlanın hasar görmesine izin verecek değişebilen bir durum mevcuttur, ancak bunu okuyucu için bir alıştırma olarak bırakıyoruz.
Tuğlaları ekrana geçirme vakti. game.dart
dosyasını şu şekilde 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 işlemi, Ground
bileşenlerini eklemek için kullandığınız koddan biraz farklıdır. Bu kez Brick
öğeleri zaman içinde rastgele bir kümeye ekleniyor. Bunun iki bölümü vardır. İlki, Brick
await
öğesini bir Future.delayed
ekleme yöntemidir. Bu yöntem, sleep()
çağrısının eşzamansız eşdeğeridir. Ancak, bunun çalışmasını sağlamanın ikinci bir kısmı daha vardır. onLoad
yönteminde addBricks
çağrısı await
yapılmaz. Yayınlanıyor olsaydı, tüm tuğlalar ekranda görünene kadar onLoad
yöntemi tamamlanmazdı. addBricks
ile ilgili unawaited
görüşmesinin tamamlanması, test uzmanlarını mutlu ediyor ve amacımızın gelecekteki programcılar için açık olmasını sağlıyor. Bu yöntemin geri gelmesini beklemek bilerek yapılır.
Oyunu çalıştırdığınızda tuğlaların belirdiğini, birbirine çarptığını ve yere döküldüğünü göreceksiniz.
6. Oyuncuyu ekle
Uzaylıları tuğlalara fırlatıyor
İlk birkaçda tuğlaların düşmesini izlemek eğlencelidir, bence oyuncuya dünyayla etkileşim kurmak için kullanabileceği bir avatar verirsek bu oyun daha eğlenceli olacak. Tuğlalara doğru hızla fırlatabilecekleri bir uzaylıya ne dersiniz?
lib/components
dizininde yeni bir player.dart
dosyası oluşturun ve aşağıdakini bu dosyaya 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.withOpacity(0.7)
..strokeWidth = 0.4
..strokeCap = StrokeCap.round);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Bu, önceki adımda yer alan Brick
bileşenlerinin bir adımıdır. Bu Player
bileşeninin, tanımanız gereken SpriteComponent
ve yeni CustomPainterComponent
olmak üzere iki alt bileşeni vardır. Flutter'ın geliştirdiği CustomPainter
konsepti sayesinde tuvale resim yapabilirsiniz. Bu bilgi, oyuncuya yuvarlak uzaylının fırlatıldığında nereye uçacağı hakkında geri bildirim vermek için kullanılır.
Oyuncu, uzaylıyla saldırıyı nasıl başlatır? Oynatıcı bileşeninin, DragCallbacks
geri çağırma özelliğiyle algıladığı bir sürükleme hareketi kullanılır. Aranızdaki kartallar burada başka bir şey fark etmiş olacak.
Ground
bileşenlerinin statik cisimler olduğu durumlarda, tuğla bileşenleri dinamik cisimlerdi. Buradaki oynatıcı, ikisinin birleşimidir. Oyuncu hareketsiz olarak başlar, oyuncunun öğeyi sürüklemesini bekler. Sürükleme durumunda kendini statikten dinamike dönüştürür, sürüklemeyle orantılı olarak doğrusal bir itme ekler ve uzaylı avatarının uçmasını sağlar.
Ayrıca Player
bileşeninde, sınır dışına çıktığında, uykuya daldığında veya zaman aşımına uğradığında ekrandan kaldırılacağı kod da bulunur. Buradaki amaç oyuncunun uzaylıyı savurup ne olduğunu görmesine ve bir kez daha denemesine izin vermektir.
game.dart
öğesini aşağıdaki gibi düzenleyerek Player
bileşenini oyuna entegre edin:
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 eklemek önceki bileşenlere benzer ancak fazladan bir kırışıklık daha içeriyor. Oyuncunun uzaylısı, belirli koşullar altında kendini oyundan kaldıracak şekilde tasarlanmıştır. Dolayısıyla burada, oyunda Player
bileşeni olup olmadığını kontrol eden ve varsa bu bileşeni tekrar ekleyen bir güncelleme işleyici vardır. Oyun şuna benzer şekilde çalıştırılabilir.
7. Etkilere tepki verin
Düşmanları ekleme
Birbiriyle etkileşime giren statik ve dinamik nesneler gördünüz. Ancak gerçekten bir yere varmak için işler çakıştığında kodda geri çağırmalar almanız gerekir. Şimdi bunun nasıl yapıldığına bakalım. Oyuncunun mücadele edeceği bazı düşmanlarla karşılaşacaksınız. Bu şekilde, durumu kazanan bir koşul haline getirirsiniz. Tüm düşmanları oyundan çıkarın.
lib/components
dizininde bir enemy.dart
dosyası oluşturun ve aşağıdakini 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 önceki etkileşimlerinizden bu dosyanın büyük bir kısmı tanıdık gelecektir. Ancak bilinmeyen yeni bir temel sınıf nedeniyle düzenleyicinizde birkaç kırmızı alt çizgi olacak. lib/components
klasörüne aşağıdaki içeriğe sahip body_component_with_user_data.dart
adlı bir dosya 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;
}
}
Enemy
bileşenindeki yeni beginContact
geri çağırma özelliğiyle birlikte bu temel sınıf, vücutlar arasındaki etkiler hakkında programatik olarak bildirim almanın temelini oluşturur. Aslında, aralarında etki bildirimleri almak istediğiniz tüm bileşenleri düzenlemeniz gerekir. Bu nedenle Brick
, Ground
ve Player
bileşenlerini düzenleyerek bu bileşenlerin şu anda kullandığı BodyComponent
temel sınıfın yerine bu BodyComponentWithUserData
bileşenini kullanacak şekilde düzenleyin. Örneğin, Ground
bileşenini nasıl düzenleyeceğiniz aşağıda açıklanmıştır:
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'nin kişileri nasıl işlediği hakkında daha fazla bilgi edinmek için lütfen kişi geri çağırmalarıyla ilgili Forge2D dokümanlarına göz atın.
Oyunu kazanma
Artık düşmanlarınız olduğuna ve düşmanları dünyadan imha ettiğinize göre bu simülasyonu oyuna dönüştürmenin basit bir yolu var. Hedefte tüm düşmanları yok edin! game.dart
dosyasını şu şekilde düzenlemenin zamanı geldi:
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 ederseniz meydan okumanız gereken adım oyunu yönetmek ve bu ekrana ulaşmaktır.
8. Tebrikler
Tebrikler, Flutter ve Flame ile başarılı bir oyun geliştirdiniz.
Flame 2D oyun motorunu kullanarak bir oyun geliştirdiniz ve bu oyunu Flutter sarmalayıcıya yerleştirdiniz. Bileşenleri canlandırmak ve kaldırmak için Flame'in Efektlerini kullandınız. Oyunun tamamının iyi tasarlanmış görünmesini sağlamak için Google Fonts ve Flutter Animate paketlerini kullandınız.
Sırada ne var?
Bu codelab'lerden bazılarına göz atın...
- Flutter'da yeni nesil kullanıcı arayüzleri geliştirme
- Sıkıcı olan Flutter uygulamanızı güzelleştirin
- Flutter uygulamanıza uygulama içi satın alma ekleme