Informationen zu diesem Codelab
1. Hinweis
Flame ist eine Flutter-basierte 2D-Spiel-Engine. In diesem Codelab erstellen Sie ein Spiel, das eine 2D-Physiksimulation ähnlich Box2D namens Forge2D verwendet. Mit den Komponenten von Flame können Sie die simulierte physische Realität auf dem Bildschirm darstellen, damit Ihre Nutzer damit spielen können. Ihr Spiel sollte dann in etwa so aussehen wie dieses animierte GIF:
Vorbereitung
- Abschluss des Codelabs Einführung in Flame mit Flutter
Lerninhalte
- Die Grundlagen von Forge2D, beginnend mit den verschiedenen Arten von physischen Körpern.
- So richten Sie eine physikalische Simulation in 2D ein.
Voraussetzungen
- das Flutter SDK
- Visual Studio Code (VS Code) mit den Flutter- und Dart-Plug-ins
Compilersoftware für das ausgewählte Entwicklungsziel. Dieses Codelab funktioniert auf allen sechs Plattformen, die von Flutter unterstützt werden. Sie benötigen Visual Studio für Windows, Xcode für macOS oder iOS und Android Studio für Android.
2. Projekt erstellen
Flutter-Projekt erstellen
Es gibt viele Möglichkeiten, ein Flutter-Projekt zu erstellen. In diesem Abschnitt verwenden Sie aus Gründen der Übersichtlichkeit die Befehlszeile.
Führen Sie zunächst die folgenden Schritte aus:
- Erstellen Sie in einer Befehlszeile ein Flutter-Projekt:
$ 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.
- Ändern Sie die Abhängigkeiten des Projekts, um Flame und Forge2D hinzuzufügen:
$ 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.
Das Paket flame
ist Ihnen bereits bekannt, die anderen drei erfordern jedoch möglicherweise eine Erklärung. Das Paket characters
wird für die UTF-8-konforme Pfadmanipulation verwendet. Das flame_forge2d
-Paket stellt die Forge2D-Funktionen so bereit, dass sie gut mit Flame funktionieren. Schließlich wird das xml
-Paket an verschiedenen Stellen verwendet, um XML-Inhalte zu verarbeiten und zu ändern.
Öffnen Sie das Projekt und ersetzen Sie den Inhalt der Datei lib/main.dart
durch Folgendes:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(GameWidget.controlled(gameFactory: FlameGame.new));
}
Dadurch wird die App mit einem GameWidget
gestartet, das die Instanz FlameGame
instanziiert. In diesem Codelab gibt es keinen Flutter-Code, der den Status der Spielinstanz verwendet, um Informationen zum laufenden Spiel anzuzeigen. Daher funktioniert diese vereinfachte Bootstrap-Methode gut.
Optional: macOS-exklusive Nebenquest absolvieren
Die Screenshots in diesem Projekt stammen aus dem Spiel als macOS-Desktop-App. Damit die Titelleiste der App nicht das Gesamtbild beeinträchtigt, können Sie die Projektkonfiguration des macOS-Runners so ändern, dass die Titelleiste ausgeblendet wird.
Führen Sie dazu die folgenden Schritte aus:
- Erstellen Sie eine
bin/modify_macos_config.dart
-Datei und fügen Sie ihr folgenden Inhalt hinzu:
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());
}
Diese Datei befindet sich nicht im Verzeichnis lib
, da sie nicht Teil der Laufzeit-Codebasis für das Spiel ist. Es ist ein Befehlszeilentool zum Ändern des Projekts.
- Führen Sie das Tool so aus:
dart bin/modify_macos_config.dart
Wenn alles nach Plan verläuft, generiert das Programm keine Ausgabe in der Befehlszeile. Die macos/Runner/Base.lproj/MainMenu.xib
-Konfigurationsdatei wird jedoch so geändert, dass das Spiel ohne sichtbare Titelleiste ausgeführt wird und das Flame-Spiel das gesamte Fenster einnimmt.
Starte das Spiel, um zu prüfen, ob alles funktioniert. Es sollte ein neues Fenster mit einem leeren schwarzen Hintergrund angezeigt werden.
3. Bild-Assets hinzufügen
Bilder hinzufügen
Jedes Spiel benötigt Art-Assets, um einen Bildschirm so zu gestalten, dass es Spaß macht. In diesem Codelab wird das Paket Physics Assets von Kenney.nl verwendet. Diese Assets sind unter der Creative Commons CC0-Lizenz lizenziert. Ich empfehle Ihnen aber trotzdem, dem Team von Kenney eine Spende zu geben, damit es seine tolle Arbeit fortsetzen kann. Hab ich doch.
Sie müssen die Konfigurationsdatei pubspec.yaml
ändern, um die Verwendung der Assets von Kenney zu ermöglichen. Ändern Sie sie so:
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.
In Flame werden Bild-Assets in assets/images
erwartet, dies kann jedoch anders konfiguriert werden. Weitere Informationen finden Sie in der Dokumentation zu Bildern in Flame. Nachdem Sie die Pfade konfiguriert haben, müssen Sie sie dem Projekt selbst hinzufügen. Eine Möglichkeit hierfür bietet die Befehlszeile:
mkdir -p assets/images
Es sollte keine Ausgabe vom Befehl mkdir
geben, aber das neue Verzeichnis sollte entweder in Ihrem Editor oder in einem Dateimanager sichtbar sein.
Maximieren Sie die heruntergeladene kenney_physics-assets.zip
-Datei. Sie sollte in etwa so aussehen:
Kopieren Sie die Dateien colored_desert.png
, colored_grass.png
, colored_land.png
und colored_shroom.png
aus dem Verzeichnis PNG/Backgrounds
in das Verzeichnis assets/images
Ihres Projekts.
Es gibt auch Sprite-Sheets. Sie bestehen aus einer PNG-Bilddatei und einer XML-Datei, die beschreibt, wo im Sprite-Sheet-Bild kleinere Bilder zu finden sind. Sprite-Sheets sind eine Methode, um die Ladezeit zu verkürzen, indem nur eine einzige Datei geladen wird, anstatt Dutzende oder gar Hunderte einzelner Bilddateien.
Kopieren Sie spritesheet_aliens.png
, spritesheet_elements.png
und spritesheet_tiles.png
in das Verzeichnis assets/images
Ihres Projekts. Kopieren Sie auch die Dateien spritesheet_aliens.xml
, spritesheet_elements.xml
und spritesheet_tiles.xml
in das Verzeichnis assets
Ihres Projekts. Ihr Projekt sollte in etwa so aussehen:
Hintergrund malen
Nachdem Sie Ihrem Projekt Bild-Assets hinzugefügt haben, ist es an der Zeit, sie auf dem Bildschirm zu platzieren. Nun, ein Bild auf dem Bildschirm. Weitere Informationen folgen in den nächsten Schritten.
Erstellen Sie im neuen Verzeichnis lib/components
eine Datei namens background.dart
und fügen Sie den folgenden Inhalt hinzu.
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,
),
);
}
}
Diese Komponente ist eine spezielle SpriteComponent
. Es ist für die Anzeige eines der vier Hintergrundbilder von Kenney.nl verantwortlich. In diesem Code werden einige Vereinfachungen vorgenommen. Erstens: Die Bilder sind quadratisch. Das trifft auf alle vier Hintergrundbilder von Kenney zu. Zweitens: Die Größe der sichtbaren Welt ändert sich nie. Andernfalls müsste diese Komponente Ereignisse für die Größe des Spiels verarbeiten. Die dritte Annahme ist, dass sich die Position (0,0)
in der Mitte des Bildschirms befindet. Diese Annahmen erfordern eine spezielle Konfiguration der CameraComponent
des Spiels.
Erstellen Sie im Verzeichnis lib/components
eine weitere neue Datei mit dem Namen game.dart
.
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();
}
}
Hier passiert viel. Beginnen Sie mit der Klasse MyPhysicsGame
. Im Gegensatz zum vorherigen Codelab wird hier Forge2DGame
und nicht FlameGame
erweitert. Forge2DGame
erweitert FlameGame
um einige interessante Funktionen. Erstens: zoom
ist standardmäßig auf 10 festgelegt. Diese zoom
-Einstellung bezieht sich auf den Bereich nützlicher Werte, mit denen Physiksimulations-Engines im Box2D
-Stil gut funktionieren. Der Engine-Code wird im MKS-System geschrieben, bei dem die Einheiten in Metern, Kilogramm und Sekunden angegeben werden. Der Bereich, in dem keine merklichen mathematischen Fehler bei Objekten auftreten, liegt zwischen 0,1 Metern und mehreren Dutzend Metern. Wenn Sie die Pixelabmessungen direkt ohne eine gewisse Skalierung eingeben, würde Forge2D seine Grenzen überschreiten. Zur Veranschaulichung können Sie sich vorstellen, Objekte im Bereich von einer Getränkedose bis zu einem Bus zu simulieren.
Die in der Hintergrundkomponente getroffenen Annahmen werden hier erfüllt, indem die Auflösung von CameraComponent
auf 800 × 600 virtuelle Pixel festgelegt wird. Das Spielfeld ist also 80 Einheiten breit und 60 Einheiten hoch und zentriert auf (0,0)
. Dies hat keine Auswirkungen auf die angezeigte Auflösung, aber darauf, wo wir Objekte in der Spielszene platzieren.
Neben dem Konstruktorargument camera
gibt es ein weiteres, physikalisch korrekteres Argument namens gravity
. Die Schwerkraft ist auf eine Vector2
mit einer x
von 0
und einer y
von 10
festgelegt. 10
ist eine gute Näherung an den allgemein akzeptierten Wert von 9,81 Metern pro Sekunde pro Sekunde für die Schwerkraft. Die Tatsache, dass die Schwerkraft auf + 10 festgelegt ist, zeigt, dass in diesem System die Richtung der Y-Achse nach unten ist. Das unterscheidet sich zwar von Box2D im Allgemeinen, entspricht aber der Standardkonfiguration von Flame.
Als Nächstes folgt die onLoad
-Methode. Diese Methode ist asynchron, was angemessen ist, da sie für das Laden von Bild-Assets von der Festplatte verantwortlich ist. Die Aufrufe von images.load
geben ein Future<Image>
zurück und als Nebeneffekt wird das geladene Bild im Game-Objekt im Cache gespeichert. Diese Futures werden mithilfe der statischen Methode Futures.wait
zusammengeführt und als einzelne Einheit erwartet. Die Liste der zurückgegebenen Bilder wird dann anhand von Mustern in einzelne Namen abgeglichen.
Die Spritesheet-Bilder werden dann in eine Reihe von XmlSpriteSheet
-Objekten eingespeist, die für das Abrufen der im Spritesheet enthaltenen einzelnen Sprites verantwortlich sind. Die Klasse XmlSpriteSheet
ist im Paket flame_kenney_xml
definiert.
Jetzt müssen Sie nur noch ein paar kleinere Änderungen an lib/main.dart
vornehmen, damit ein Bild auf dem Bildschirm angezeigt wird.
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
}
Durch diese Änderung können Sie das Spiel jetzt noch einmal ausführen, um den Hintergrund auf dem Bildschirm zu sehen. Hinweis: Die CameraComponent.withFixedResolution()
-Kamerainstanz fügt bei Bedarf Letterboxing hinzu, damit das Seitenverhältnis von 800 × 600 Pixeln des Spiels funktioniert.
4. Boden hinzufügen
Ein solides Fundament
Wenn wir eine Schwerkraft haben, brauchen wir etwas, das Objekte im Spiel auffängt, bevor sie unten vom Bildschirm fallen. Es sei denn, das Verlassen des Bildschirms ist Teil Ihres Gamedesigns. Erstellen Sie eine neue ground.dart
-Datei in Ihrem lib/components
-Verzeichnis und fügen Sie Folgendes hinzu:
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),
),
],
);
}
Diese Ground
-Komponente leitet sich von BodyComponent
ab. In Forge2D sind Körper wichtig. Sie sind die Objekte, die Teil der zweidimensionalen physikalischen Simulation sind. Für die BodyDef
dieser Komponente ist eine BodyType.static
angegeben.
In Forge2D gibt es drei verschiedene Körpertypen. Statische Körper bewegen sich nicht. Sie haben praktisch sowohl eine Nullmasse – sie reagieren nicht auf die Schwerkraft – als auch eine unendliche Masse – sie bewegen sich nicht, wenn sie von anderen Objekten getroffen werden, unabhängig davon, wie schwer sie sind. Statisch platzierte Körper eignen sich daher perfekt für eine Bodenoberfläche, da sie sich nicht bewegen.
Die anderen beiden Körpertypen sind kinematische und dynamische Körper. Dynamische Körper sind Körper, die vollständig simuliert werden. Sie reagieren auf die Schwerkraft und auf die Objekte, mit denen sie zusammenstoßen. Im Rest dieses Codelabs sehen Sie viele dynamische Körper. Kinematische Körper sind eine Zwischenstufe zwischen statisch und dynamisch. Sie bewegen sich, reagieren aber nicht auf die Schwerkraft oder andere Objekte, die sie treffen. Nützlich, aber nicht Teil dieses Codelabs.
Der Body selbst hat nicht viel zu tun. Ein Körper braucht zugehörige Formen, um Substanz zu haben. In diesem Fall ist diesem Körper eine Form zugewiesen, eine PolygonShape
, die als BoxXY
festgelegt ist. Dieser Boxtyp ist achsorientiert, im Gegensatz zu einem PolygonShape
, das als BoxXY
festgelegt ist und um einen Drehpunkt gedreht werden kann. Auch das ist nützlich, aber es fällt nicht in den Rahmen dieses Codelabs. Die Form und der Körper sind mit einer Halterung verbunden, was nützlich ist, um dem System Dinge wie friction
hinzuzufügen.
Standardmäßig werden die angehängten Formen eines Körpers so gerendert, dass sie für das Debuggen nützlich sind, aber nicht für ein gutes Gameplay sorgen. Wenn Sie das super
-Argument renderBody
auf false
setzen, wird dieses Debug-Rendering deaktiviert. Das In-Game-Rendering dieses Körpers liegt in der Verantwortung des Kindes SpriteComponent
.
Wenn Sie dem Spiel die Ground
-Komponente hinzufügen möchten, bearbeiten Sie die game.dart
-Datei so:
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.
}
Mit dieser Änderung werden der Welt eine Reihe von Ground
-Komponenten hinzugefügt. Dazu wird eine for
-Schleife in einem List
-Kontext verwendet und die resultierende Liste der Ground
-Komponenten an die addAll
-Methode von world
übergeben.
Wenn Sie das Spiel jetzt starten, werden der Hintergrund und der Boden angezeigt.
5. Ziegel hinzufügen
Mauer erstellen
Der Boden ist ein Beispiel für einen statischen Körper. Jetzt ist es an der Zeit für Ihre erste dynamische Komponente. Dynamische Komponenten in Forge2D sind der Eckpfeiler der Nutzererfahrung. Sie sind die Elemente, die sich bewegen und mit der Welt um sie herum interagieren. In diesem Schritt fügen Sie Steine hinzu, die zufällig ausgewählt und in einem Cluster von Steinen auf dem Bildschirm angezeigt werden. Sie sehen, wie sie dabei fallen und gegeneinander stoßen.
Die Blöcke werden aus dem Sprite Sheet der Elemente erstellt. Wenn Sie sich die Beschreibung des Sprite-Sheets in assets/spritesheet_elements.xml
ansehen, sehen Sie, dass wir ein interessantes Problem haben. Die Namen sind nicht sehr hilfreich. Es wäre nützlich, einen Ziegel nach Material, Größe und Beschädigung auswählen zu können. Glücklicherweise hat ein hilfreicher Elf einige Zeit damit verbracht, das Muster in der Dateibenennung zu ermitteln, und ein Tool erstellt, das euch die Arbeit erleichtert. Erstellen Sie im Verzeichnis bin
eine neue Datei generate_brick_file_names.dart
und fügen Sie ihr folgenden Inhalt hinzu:
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();
}
Ihr Editor sollte Sie über eine fehlende Abhängigkeit warnen oder einen Fehler anzeigen. Fügen Sie es mit dem folgenden Befehl hinzu:
flutter pub add equatable
Sie sollten dieses Programm jetzt so ausführen können:
$ 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', }, }; }
Dieses Tool hat die Beschreibungsdatei des Sprite-Sheets geparst und in Dart-Code umgewandelt, mit dem wir die richtige Bilddatei für jeden Stein auswählen können, den Sie auf dem Bildschirm anzeigen möchten. Sehr hilfreich!
Erstellen Sie die brick.dart
-Datei mit folgendem Inhalt:
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();
}
}
Sie sehen jetzt, wie der zuvor generierte Dart-Code in diese Codebasis eingebunden ist, damit Sie Ziegelbilder schnell nach Material, Größe und Zustand auswählen können. Wenn Sie über die enum
s hinweg auf die Brick
-Komponente selbst schauen, sollten Sie feststellen, dass der Großteil dieses Codes der Ground
-Komponente im vorherigen Schritt ziemlich ähnlich ist. Es gibt einen veränderbaren Zustand, der es ermöglicht, dass der Ziegel beschädigt wird. Die Verwendung dieses Zustands bleibt dem Leser als Übung überlassen.
Jetzt ist es an der Zeit, die Blöcke auf dem Bildschirm zu platzieren. Bearbeiten Sie die Datei game.dart
so:
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.
}
Dieser Code unterscheidet sich etwas von dem Code, mit dem Sie die Ground
-Komponenten hinzugefügt haben. Dieses Mal werden die Brick
s im Laufe der Zeit einem zufälligen Cluster hinzugefügt. Dazu gehören zwei Teile: Erstens wird der Brick
s await
s ein Future.delayed
hinzugefügt, das asynchrone Äquivalent eines sleep()
-Aufrufs. Es gibt jedoch noch einen zweiten Teil, der dafür sorgt, dass dies funktioniert: Der Aufruf von addBricks
in der onLoad
-Methode wird nicht await
ed. Andernfalls wird die onLoad
-Methode erst abgeschlossen, wenn alle Blöcke auf dem Bildschirm sind. Wenn wir den Aufruf von addBricks
in einen unawaited
-Aufruf einschließen, sind die Linter zufrieden und unsere Absicht für zukünftige Programmierer offensichtlich. Es ist beabsichtigt, nicht auf die Rückgabe dieser Methode zu warten.
Wenn Sie das Spiel starten, sehen Sie, wie Steine erscheinen, gegeneinander stoßen und auf dem Boden verstreut werden.
6. Spieler hinzufügen
Aliens auf Ziegel werfen
Das Zusehen, wie die Blöcke zusammenfallen, macht in den ersten paar Malen Spaß, aber ich denke, dieses Spiel wird noch mehr Spaß machen, wenn wir dem Spieler einen Avatar geben, mit dem er mit der Welt interagieren kann. Wie wäre es mit einem Alien, das sie auf die Steine werfen können?
Erstellen Sie im Verzeichnis lib/components
eine neue player.dart
-Datei und fügen Sie Folgendes hinzu:
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;
}
Das ist ein Schritt nach oben im Vergleich zu den Brick
-Komponenten im vorherigen Schritt. Diese Player
-Komponente hat zwei untergeordnete Komponenten: eine SpriteComponent
, die Ihnen bekannt sein sollte, und eine CustomPainterComponent
, die neu ist. Das CustomPainter
-Konzept stammt aus Flutter und ermöglicht es, auf einer Leinwand zu malen. Hier wird es verwendet, um dem Spieler Feedback dazu zu geben, wohin das runde Alien fliegt, wenn es geworfen wird.
Wie startet der Spieler das Werfen des Aliens? Durch eine Ziehgeste, die die Player-Komponente mit den DragCallbacks
-Callbacks erkennt. Die aufmerksamen unter euch haben hier vielleicht noch etwas anderes bemerkt.
Während Ground
-Komponenten statische Körper waren, waren die Brick-Komponenten dynamische Körper. Der Player hier ist eine Kombination aus beiden. Der Spieler ist zu Beginn statisch und wartet darauf, dass der Nutzer ihn zieht. Wenn der Nutzer loslässt, wechselt er von statisch zu dynamisch, fügt einen linearen Impuls proportional zum Ziehen hinzu und lässt den Alien-Avatar fliegen.
In der Player
-Komponente gibt es auch Code, der das Element vom Bildschirm entfernt, wenn es den Bereich verlässt, in den Ruhemodus wechselt oder das Zeitlimit überschritten wird. Der Spieler soll das Alien schleudern, sehen, was passiert, und dann noch einmal versuchen.
Integrieren Sie die Player
-Komponente in das Spiel, indem Sie game.dart
so bearbeiten:
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.
}
Das Hinzufügen des Spielers zum Spiel ähnelt dem der vorherigen Komponenten, mit einer kleinen Ausnahme. Das Alien des Spielers soll sich unter bestimmten Bedingungen aus dem Spiel entfernen. Daher gibt es hier einen Update-Handler, der prüft, ob im Spiel keine Player
-Komponente vorhanden ist. Ist das der Fall, wird eine hinzugefügt. Das Spiel sieht so aus:
7. Auf Auswirkungen reagieren
Gegner hinzufügen
Sie haben gesehen, wie statische und dynamische Objekte miteinander interagieren. Wenn Sie jedoch wirklich etwas erreichen möchten, müssen Sie Callbacks im Code erhalten, wenn Objekte kollidieren. Sie werden einige Feinde einführen, gegen die der Spieler antreten muss. Dies führt zu einer Gewinnbedingung: Entfernen Sie alle Feinde aus dem Spiel.
Erstellen Sie eine enemy.dart
-Datei im Verzeichnis lib/components
und fügen Sie Folgendes hinzu:
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();
}
Aus Ihren bisherigen Interaktionen mit den Player- und Brick-Komponenten sollte Ihnen der Großteil dieser Datei bereits bekannt sein. Aufgrund einer neuen unbekannten Basisklasse werden jedoch einige rote Unterstreichungen im Editor angezeigt. Fügen Sie diese Klasse jetzt hinzu, indem Sie lib/components
die Datei body_component_with_user_data.dart
mit dem folgenden Inhalt hinzufügen:
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;
}
}
Diese Basisklasse in Kombination mit dem neuen beginContact
-Callback in der Enemy
-Komponente bildet die Grundlage für programmatische Benachrichtigungen über Kollisionen zwischen Körpern. Sie müssen alle Komponenten bearbeiten, für die Sie Benachrichtigungen zu Auswirkungen erhalten möchten. Bearbeiten Sie die Komponenten Brick
, Ground
und Player
, um diese BodyComponentWithUserData
anstelle der Basisklasse BodyComponent
zu verwenden, die diese Komponenten verwenden. So bearbeiten Sie beispielsweise die Komponente Ground
:
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),
),
],
);
}
Weitere Informationen dazu, wie Forge2D mit Kontakten umgeht, findest du in der Forge2D-Dokumentation zu Kontakt-Callbacks.
Spiel gewinnen
Jetzt, da Sie Feinde haben und eine Möglichkeit, sie aus der Welt zu entfernen, können Sie diese Simulation ganz einfach in ein Spiel verwandeln. Dein Ziel sollte es sein, alle Feinde zu entfernen. Bearbeiten Sie die Datei game.dart
so:
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.
}
}
Deine Mission: Starte das Spiel und rufe diesen Bildschirm auf.
8. Glückwunsch
Herzlichen Glückwunsch, Sie haben ein Spiel mit Flutter und Flame erstellt.
Sie haben ein Spiel mit der Flame-2D-Spiel-Engine erstellt und in einen Flutter-Wrapper eingebettet. Sie haben die Effekte von Flame verwendet, um Komponenten zu animieren und zu entfernen. Sie haben Google Fonts und Flutter Animate-Pakete verwendet, um das gesamte Spiel ansprechend zu gestalten.
Nächste Schritte
Sehen Sie sich einige dieser Codelabs an:
- Benutzeroberflächen der nächsten Generation in Flutter erstellen
- Ihre Flutter-App von langweilig zu schön machen
- In-App-Käufe zu Ihrer Flutter-App hinzufügen