1. Einführung
Flame ist eine Flutter-basierte 2D-Spiel-Engine. In diesem Codelab entwickeln Sie ein Spiel, das von einem der Klassiker der Videospiele der 70er, Steve Wozniaks Breakout, inspiriert ist. Du verwendest Flame's Components, um den Schläger, den Ball und die Bausteine zu zeichnen. Du nutzt Flame's Effects, um die Bewegung des Schlägers zu animieren und zu erfahren, wie du Flame in das State Management System von Flutter einbindest.
Wenn das Spiel fertig ist, sollte es wie dieses animierte GIF aussehen, nur ein bisschen langsamer.
Lerninhalte
- Wie die Grundlagen von Flame funktionieren, beginnend mit
GameWidget
. - Spielschleife verwenden
- So funktionieren
Component
von Flame. Sie ähneln denWidget
von Flutter. - Umgang mit Konflikten
- So verwenden Sie
Effect
-Elemente zur Animation vonComponent
-Elementen. - Hier erfährst du, wie du Flutter-
Widget
s in einem Flame-Spiel überlagern kannst. - Flame in das State Management von Flutter einbinden
Aufgaben
In diesem Codelab entwickeln Sie ein 2D-Spiel mit Flutter und Flame. Wenn Sie fertig sind, sollte Ihr Spiel die folgenden Anforderungen erfüllen
- Funktion auf allen sechs Plattformen, die von Flutter unterstützt werden: Android, iOS, Linux, macOS, Windows und das Web
- Halte in der Spielschleife von Flame mindestens 60 fps aufrecht.
- Mit Flutter-Funktionen wie dem
google_fonts
-Paket undflutter_animate
kannst du das Arcade-Game der 80er-Jahre nachahmen.
2. Flutter-Umgebung einrichten
Editor
Zur Vereinfachung dieses Codelabs wird davon ausgegangen, dass Visual Studio Code (VS Code) Ihre Entwicklungsumgebung ist. VS Code ist kostenlos und funktioniert auf allen gängigen Plattformen. Für dieses Codelab verwenden wir VS Code, da in der Anleitung standardmäßig VS Code-spezifische Tastenkombinationen verwendet werden. Die Aufgaben werden einfacher: „Klicke auf diese Schaltfläche“ oder „Drücke diese Taste, um X zu tun“ anstatt „im Editor die entsprechende Aktion auszuführen, um X zu tun“.
Sie können einen beliebigen Editor verwenden: Android Studio, andere IntelliJ IDEs, Emacs, Vim oder Notepad++. Sie alle sind mit Flutter kompatibel.
Entwicklungsziel auswählen
Flutter entwickelt Apps für mehrere Plattformen. Ihre App kann unter einem der folgenden Betriebssysteme ausgeführt werden:
- iOS
- Android
- Windows
- macOS
- Linux
- web
In der Regel wird ein einziges Betriebssystem als Entwicklungsziel ausgewählt. Dies ist das Betriebssystem, auf dem Ihre App während der Entwicklung ausgeführt wird.
Beispiel: Sie verwenden einen Windows-Laptop, um Ihre Flutter-App zu entwickeln. Anschließend wählen Sie Android als Entwicklungsziel aus. Um eine Vorschau Ihrer App zu sehen, schließen Sie ein Android-Gerät über ein USB-Kabel an Ihren Windows-Laptop an. Ihre App wird während der Entwicklung auf diesem angeschlossenen Android-Gerät oder in einem Android-Emulator ausgeführt. Sie hätten Windows als Entwicklungsziel wählen können, wo Ihre App während der Entwicklung als Windows-App zusammen mit Ihrem Editor ausgeführt wird.
Sie könnten versucht sein, das Web als Entwicklungsziel zu wählen. Dies hat einen Nachteil während der Entwicklung: Sie verlieren die Funktion Stateful Hot Refresh von Flutter. Flutter kann derzeit keine Hot-Reloads für Webanwendungen durchführen.
Triff eine Auswahl, bevor du fortfährst. Sie können Ihre App später jederzeit unter anderen Betriebssystemen ausführen. Die Auswahl eines Entwicklungsziels erleichtert den nächsten Schritt.
Flutter installieren
Eine aktuelle Anleitung zur Installation des Flutter SDK finden Sie unter docs.flutter.dev.
Die Anleitung auf der Flutter-Website umfasst die Installation des SDK und der Entwicklungstools sowie die Editor-Plug-ins. Installieren Sie für dieses Codelab die folgende Software:
- Flutter-SDK
- Visual Studio Code mit dem Flutter-Plug-in
- Compiler-Software für dein gewähltes Entwicklungsziel. Sie benötigen Visual Studio für Windows oder Xcode für macOS oder iOS.
Im nächsten Abschnitt erstellen Sie Ihr erstes Flutter-Projekt.
Wenn Sie Probleme beheben müssen, könnten einige dieser Fragen und Antworten (von StackOverflow) hilfreich für die Fehlerbehebung sein.
FAQ
- Wie finde ich den Flutter SDK-Pfad?
- Was kann ich tun, wenn der Flutter-Befehl nicht gefunden wird?
- Wie behebe ich den Fehler „Warten auf einen anderen Flutter-Befehl, um die Startsperre aufzuheben“ Problem?
- Wie gebe ich Flutter an, wo sich meine Android SDK-Installation befindet?
- Wie gehe ich mit dem Java-Fehler um, wenn ich
flutter doctor --android-licenses
ausführe? - Wie gehe ich mit dem Android-Tool um
sdkmanager
, das nicht gefunden wurde? - Was muss ich tun, wenn die Komponente „
cmdline-tools
“ fehlt Fehler? - Wie führe ich CocoaPods auf Apple Silicon (M1) aus?
- Wie deaktiviere ich die automatische Formatierung beim Speichern in VS Code?
3. Projekt erstellen
Erstes Flutter-Projekt erstellen
Dazu müssen Sie VS Code öffnen und die Flutter-App-Vorlage in einem von Ihnen ausgewählten Verzeichnis erstellen.
- Starten Sie Visual Studio Code.
- Öffnen Sie die Befehlspalette (
F1
,Ctrl+Shift+P
oderShift+Cmd+P
) und geben Sie „flutter new“ ein. Wählen Sie dann den Befehl Flutter: New Project (Flutter: Neues Projekt) aus.
- Wählen Sie Empty Application (Leere Anwendung) aus. Wählen Sie ein Verzeichnis aus, in dem Sie Ihr Projekt erstellen möchten. Dies sollte ein beliebiges Verzeichnis sein, für das keine erhöhten Berechtigungen erforderlich sind oder das ein Leerzeichen im Pfad hat. Beispiele hierfür sind Ihr Basisverzeichnis oder
C:\src\
.
- Geben Sie dem Projekt den Namen
brick_breaker
. Für den Rest dieses Codelabs wird vorausgesetzt, dass Sie der App den Namenbrick_breaker
gegeben haben.
Flutter erstellt nun Ihren Projektordner und öffnet ihn in VS Code. Sie überschreiben nun den Inhalt von zwei Dateien mit einem einfachen Gerüst der Anwendung.
Kopieren und Erste App einfügen
Dadurch wird der Beispielcode aus diesem Codelab Ihrer App hinzugefügt.
- Klicken Sie im linken Bereich von VS Code auf Explorer und öffnen Sie die Datei
pubspec.yaml
.
- Ersetzen Sie den Inhalt dieser Datei durch Folgendes:
pubspec.yaml
name: brick_breaker
description: "Re-implementing Woz's Breakout"
publish_to: 'none'
version: 0.1.0
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
flame: ^1.16.0
flutter:
sdk: flutter
flutter_animate: ^4.5.0
google_fonts: ^6.1.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
flutter:
uses-material-design: true
In der Datei pubspec.yaml
sind grundlegende Informationen zu deiner App angegeben, z. B. die aktuelle Version, die Abhängigkeiten sowie die Assets, mit denen sie ausgeliefert wird.
- Öffnen Sie die Datei
main.dart
im Verzeichnislib/
.
- Ersetzen Sie den Inhalt dieser Datei durch Folgendes:
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
final game = FlameGame();
runApp(GameWidget(game: game));
}
- Führen Sie diesen Code aus, um zu prüfen, ob alles funktioniert. Ein neues Fenster mit nur einem leeren schwarzen Hintergrund sollte angezeigt werden. Das schlimmste Videospiel der Welt wird jetzt mit 60 fps gerendert.
4. Spiel erstellen
Das passende Spiel für dich
Für ein in zwei Dimensionen (2D) gespieltes Spiel ist ein Spielbereich erforderlich. Sie konstruieren einen Bereich mit bestimmten Abmessungen und verwenden diese Dimensionen dann, um die Größe anderer Aspekte des Spiels anzupassen.
Es gibt verschiedene Möglichkeiten, die Koordinaten im Spielbereich festzulegen. Mit einer Konvention können Sie die Richtung von der Bildschirmmitte aus messen, wobei sich der Ursprung (0,0)
in der Mitte des Bildschirms befindet. Die positiven Werte verschieben die Elemente entlang der x-Achse nach rechts und entlang der y-Achse nach oben. Dieser Standard gilt heutzutage für die meisten aktuellen Spiele, insbesondere für Spiele mit drei Dimensionen.
Bei der Entwicklung des ersten Breakout-Spiels sollte der Ursprung in der oberen linken Ecke festgelegt werden. Die positive x-Richtung ist gleich geblieben, aber y wurde umgedreht. Die positive x-Richtung war nach rechts und y nach unten. Um der Zeit treu zu bleiben, setzt dieses Spiel den Ursprung in die obere linke Ecke.
Erstellen Sie eine Datei mit dem Namen config.dart
in einem neuen Verzeichnis namens lib/src
. Diese Datei erhält in den folgenden Schritten weitere Konstanten.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
Dieses Spiel wird 820 Pixel breit und 1.600 Pixel hoch sein. Der Spielbereich wird an das Fenster angepasst, in dem er angezeigt wird, aber alle Komponenten, die dem Bildschirm hinzugefügt werden, entsprechen dieser Höhe und Breite.
PlayArea erstellen
Beim Spiel Breakout prallt der Ball von den Wänden des Spielbereichs ab. Du benötigst zuerst eine PlayArea
-Komponente, um Konflikte zu vermeiden.
- Erstellen Sie eine Datei mit dem Namen
play_area.dart
in einem neuen Verzeichnis namenslib/src/components
. - Fügen Sie dieser Datei Folgendes hinzu.
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Während Flutter Widget
s hat, hat Flame Component
s. Während Flutter-Apps Bäume aus Widgets erstellen, werden bei Flame-Spielen Bäume von Komponenten beibehalten.
Es gibt einen interessanten Unterschied zwischen Flutter und Flame. Die Widget-Struktur von Flutter ist eine sitzungsspezifische Beschreibung, die zum Aktualisieren der persistenten und änderbaren RenderObject
-Ebene dient. Die Komponenten von Flame sind persistent und veränderbar, wobei davon ausgegangen wird, dass der Entwickler diese Komponenten als Teil eines Simulationssystems verwendet.
Die Komponenten von Flame sind für Spielmechaniken optimiert. Dieses Codelab beginnt mit der Spielschleife, die im nächsten Schritt beschrieben wird.
- Fügen Sie eine Datei mit allen Komponenten in diesem Projekt hinzu, um die Übersichtlichkeit zu erhöhen. Erstellen Sie eine
components.dart
-Datei inlib/src/components
und fügen Sie den folgenden Inhalt hinzu.
lib/src/components/components.dart
export 'play_area.dart';
Die Anweisung export
spielt die umgekehrte Rolle von import
. Sie legt fest, welche Funktionen diese Datei verfügbar macht, wenn sie in eine andere Datei importiert wird. Wenn Sie in den folgenden Schritten neue Komponenten hinzufügen, werden in dieser Datei mehr Einträge hinzugefügt.
Erstelle ein Flame-Spiel
Leiten Sie zum Löschen der roten Wellen aus dem vorherigen Schritt eine neue abgeleitete Klasse für die FlameGame
von Flame ab.
- Erstellen Sie in
lib/src
eine Datei mit dem Namenbrick_breaker.dart
und fügen Sie den folgenden Code hinzu.
lib/src/brick_breaker.dart
import 'dart:async';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
}
}
Diese Datei koordiniert die Aktionen des Spiels. Beim Erstellen der Spielinstanz wird das Spiel durch diesen Code so konfiguriert, dass das Rendering mit fester Auflösung verwendet wird. Die Größe des Spiels wird so angepasst, dass es den gesamten Bildschirm ausfüllt, der es enthält, und es werden bei Bedarf Letterboxing-Balken hinzugefügt.
Sie geben die Breite und Höhe des Spiels an, damit die untergeordneten Komponenten wie PlayArea
sich selbst auf die richtige Größe einstellen können.
In der überschriebenen Methode onLoad
führt Ihr Code zwei Aktionen aus.
- Konfiguriert oben links als Anker für den Sucher. Standardmäßig verwendet der Sucher die Mitte des Bereichs als Anker für
(0,0)
. - Fügt
PlayArea
zumworld
hinzu. Die Welt steht für die Spielwelt. Sie projiziert alle untergeordneten Elemente über dieCameraComponent
s-Ansichtstransformation.
Spiel auf dem Display
Wenn Sie alle Änderungen sehen möchten, die Sie in diesem Schritt vorgenommen haben, aktualisieren Sie Ihre lib/main.dart
-Datei mit den folgenden Änderungen.
lib/main.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'src/brick_breaker.dart'; // Add this import
void main() {
final game = BrickBreaker(); // Modify this line
runApp(GameWidget(game: game));
}
Starte das Spiel neu, nachdem du diese Änderungen vorgenommen hast. Das Spiel sollte der folgenden Abbildung ähneln.
Im nächsten Schritt fügst du der Welt einen Ball hinzu und bringst ihn in Bewegung!
5. Den Ball präsentieren
Ball-Komponente erstellen
Um einen sich bewegenden Ball auf dem Bildschirm zu platzieren, müssen Sie eine weitere Komponente erstellen und zur Spielwelt hinzufügen.
- Bearbeiten Sie den Inhalt der Datei
lib/src/config.dart
so.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02; // Add this constant
Das Designmuster zum Definieren benannter Konstanten als abgeleitete Werte wird in diesem Codelab viele Male zurückgegeben. So kannst du die gameWidth
und die gameHeight
der obersten Ebene ändern und herausfinden, wie sich das Spiel dadurch verändert.
- Erstellen Sie die Komponente
Ball
in einer Datei namensball.dart
inlib/src/components
.
lib/src/components/ball.dart
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
class Ball extends CircleComponent {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
}
Zuvor haben Sie das PlayArea
mit RectangleComponent
definiert. Es liegt also nahe, dass es mehr Formen gibt. CircleComponent
leitet sich wie RectangleComponent
von PositionedComponent
ab, sodass du den Ball auf dem Bildschirm positionieren kannst. Und was noch wichtiger ist: Die Position kann aktualisiert werden.
Mit dieser Komponente wird das Konzept der velocity
oder der Positionsänderung im Laufe der Zeit eingeführt. Geschwindigkeit ist ein Vector2
-Objekt, da Geschwindigkeit sowohl Geschwindigkeit als auch Richtung ist. Überschreiben Sie zum Aktualisieren der Position die Methode update
, die die Spiel-Engine für jeden Frame aufruft. dt
ist die Dauer zwischen dem vorherigen und diesem Frame. So können Sie sich an Faktoren wie unterschiedliche Framerates (60 Hz oder 120 Hz) oder lange Frames aufgrund übermäßiger Berechnungen anpassen.
Sieh dir das position += velocity * dt
-Update genau an. Auf diese Weise implementieren Sie die Aktualisierung einer diskreten Simulation von Bewegungen im Zeitverlauf.
- Um die Komponente
Ball
in die Liste der Komponenten aufzunehmen, bearbeiten Sie die Dateilib/src/components/components.dart
wie folgt.
lib/src/components/components.dart
export 'ball.dart'; // Add this export
export 'play_area.dart';
Der Welt den Ball hautnah erleben
Du hast einen Ball. Platzieren Sie es nun auf der Welt und richten Sie es so ein, dass es sich im Spielbereich bewegen kann.
Bearbeiten Sie die Datei lib/src/brick_breaker.dart
so.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math; // Add this import
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random(); // Add this variable
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball( // Add from here...
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
debugMode = true; // To here.
}
}
Durch diese Änderung wird die Komponente „Ball
“ dem Element „world
“ hinzugefügt. Um die position
des Balls in die Mitte des Anzeigebereichs zu setzen, halbiert der Code zuerst die Größe des Spiels, da Vector2
über viele Operatoren (*
und /
) verfügt, um eine Vector2
um einen skalaren Wert zu skalieren.
Das Festlegen von velocity
für den Ball erfordert mehr Komplexität. Die Absicht besteht darin, den Ball in einer zufälligen Richtung mit einer angemessenen Geschwindigkeit auf dem Bildschirm nach unten zu bewegen. Durch den Aufruf der Methode normalized
wird ein Vector2
-Objekt erstellt, das in die gleiche Richtung wie das ursprüngliche Vector2
-Objekt festgelegt, aber auf eine Entfernung von 1 verkleinert wird. Auf diese Weise bleibt die Geschwindigkeit des Balls gleich, unabhängig davon, in welche Richtung der Ball geht. Die Geschwindigkeit des Balls wird dann auf ein Viertel der Höhe des Spiels erhöht.
Diese verschiedenen Werte richtig zu finden, erfordert eine gewisse Iteration, in der Branche auch Spieltests genannt.
Mit der letzten Zeile wird die Debugging-Anzeige aktiviert, die zusätzliche Informationen für die Fehlerbehebung enthält.
Wenn Sie das Spiel jetzt ausführen, sollte es in etwa so aussehen:
Sowohl die PlayArea
- als auch die Ball
-Komponente enthalten Debugging-Informationen, aber durch die Hintergrundmatten werden die Zahlen der PlayArea
abgeschnitten. Es werden immer Informationen zur Fehlerbehebung angezeigt, weil Sie debugMode
für die gesamte Komponentenstruktur aktiviert haben. Sie können das Debugging auch nur für ausgewählte Komponenten aktivieren, wenn dies nützlicher ist.
Wenn Sie das Spiel einige Male neu starten, werden Sie feststellen, dass der Ball nicht wie erwartet mit den Wänden interagiert. Um diesen Effekt zu erreichen, müssen Sie im nächsten Schritt die Kollisionserkennung hinzufügen.
6. Hin- und herspringen
Kollisionserkennung hinzufügen
Mit der Kollisionserkennung wird ein Verhalten hinzugefügt, bei dem Ihr Spiel erkennt, wenn zwei Objekte in Kontakt gekommen sind.
Um dem Spiel die Kollisionserkennung hinzuzufügen, fügen Sie das HasCollisionDetection
-Mixin zum Spiel BrickBreaker
hinzu, wie im folgenden Code gezeigt.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame with HasCollisionDetection { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
debugMode = true;
}
}
Damit werden die Hitboxes von Komponenten verfolgt und Kollisions-Callbacks bei jedem Spiel-Tick ausgelöst.
Ändern Sie die PlayArea
-Komponente wie unten gezeigt, um die Hitbox des Spiels mit Daten zu füllen.
lib/src/components/play_area.dart
import 'dart:async';
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class PlayArea extends RectangleComponent with HasGameReference<BrickBreaker> {
PlayArea()
: super(
paint: Paint()..color = const Color(0xfff2e8cf),
children: [RectangleHitbox()], // Add this parameter
);
@override
FutureOr<void> onLoad() async {
super.onLoad();
size = Vector2(game.width, game.height);
}
}
Wenn Sie eine RectangleHitbox
-Komponente als untergeordnetes Element von RectangleComponent
hinzufügen, wird ein Trefferfeld für die Kollisionserkennung erstellt, das der Größe der übergeordneten Komponente entspricht. Es gibt einen Factory-Konstruktor für RectangleHitbox
mit dem Namen relative
, wenn Sie eine Hitbox benötigen, die kleiner oder größer als die übergeordnete Komponente ist.
Den Ball hüpfen
Bisher machte die Kollisionserkennung noch keinen Unterschied zum Gameplay. Sie ändert sich, wenn Sie die Komponente „Ball
“ ändern. Das Verhalten des Balls muss sich ändern, wenn er mit PlayArea
kollidiert.
Ändere die Komponente „Ball
“ so.
lib/src/components/ball.dart
import 'package:flame/collisions.dart'; // Add this import
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart'; // And this import
import 'play_area.dart'; // And this one too
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> { // Add these mixins
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]); // Add this parameter
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override // Add from here...
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
removeFromParent();
}
} else {
debugPrint('collision with $other');
}
} // To here.
}
In diesem Beispiel wird durch Hinzufügen des onCollisionStart
-Callbacks eine größere Änderung vorgenommen. Das im vorherigen Beispiel zu BrickBreaker
hinzugefügte System zur Kollisionserkennung ruft diesen Callback auf.
Zuerst testet der Code, ob Ball
mit PlayArea
kollidiert. Dies erscheint vorerst überflüssig, da es keine anderen Komponenten in der Spielewelt gibt. Das ändert sich im nächsten Schritt, wenn du der Welt einen Schläger hinzufügst. Außerdem wird eine else
-Bedingung hinzugefügt, die behandelt wird, wenn der Ball mit Dingen kollidiert, die nicht der Schläger sind. Eine sanfte Erinnerung, die verbleibende Logik zu implementieren, wenn Sie möchten.
Wenn der Ball mit der unteren Wand kollidiert, verschwindet er einfach von der Spielfläche, obwohl er noch immer im Sichtfeld ist. Dieses Artefakt wird in einem späteren Schritt mithilfe der Macht von Flame's Effects verarbeitet.
Jetzt, da der Ball mit den Wänden des Spiels zusammenstoßt, wäre es sicher hilfreich, dem Spieler einen Schläger zu geben, mit dem er den Ball schlagen kann...
7. Schlag auf den Ball
Erstelle den Fledermaus
Um einen Schläger hinzuzufügen, damit der Ball weiter im Spiel bleibt,
- Fügen Sie einige Konstanten so in die Datei
lib/src/config.dart
ein.
lib/src/config.dart
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2; // Add from here...
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05; // To here.
Die Konstanten batHeight
und batWidth
sind selbsterklärend. Für die batStep
-Konstante ist hingegen eine Erklärung erforderlich. Um in diesem Spiel mit dem Ball zu interagieren, kann der Spieler den Schläger je nach Plattform mit der Maus oder dem Finger ziehen oder die Tastatur verwenden. Die batStep
-Konstante legt fest, wie weit der Schläger bei jedem Drücken der Pfeiltasten nach links oder rechts weitergehen soll.
- Definieren Sie die Komponentenklasse
Bat
so.
lib/src/components/bat.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/events.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
class Bat extends PositionComponent
with DragCallbacks, HasGameReference<BrickBreaker> {
Bat({
required this.cornerRadius,
required super.position,
required super.size,
}) : super(
anchor: Anchor.center,
children: [RectangleHitbox()],
);
final Radius cornerRadius;
final _paint = Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill;
@override
void render(Canvas canvas) {
super.render(canvas);
canvas.drawRRect(
RRect.fromRectAndRadius(
Offset.zero & size.toSize(),
cornerRadius,
),
_paint);
}
@override
void onDragUpdate(DragUpdateEvent event) {
super.onDragUpdate(event);
position.x = (position.x + event.localDelta.x).clamp(0, game.width);
}
void moveBy(double dx) {
add(MoveToEffect(
Vector2((position.x + dx).clamp(0, game.width), position.y),
EffectController(duration: 0.1),
));
}
}
Mit dieser Komponente werden einige neue Funktionen eingeführt.
Erstens ist die Bat-Komponente ein PositionComponent
, kein RectangleComponent
oder CircleComponent
. Das bedeutet, dass dieser Code Bat
auf dem Bildschirm rendern muss. Dazu wird der render
-Callback überschrieben.
Wenn Sie sich den Aufruf canvas.drawRRect
(abgerundetes Rechteck zeichnen) genauer ansehen, fragen Sie sich vielleicht: „Wo ist das Rechteck?“ Offset.zero & size.toSize()
nutzt eine operator &
-Überlastung in der dart:ui
-Klasse Offset
, die Rect
s erstellt. Diese Kurzschreibweise könnte Sie anfangs verwirren, wird Sie jedoch häufig in Lower-Level-Flutter- und Flame-Codes sehen.
Zweitens kann die Bat
-Komponente je nach Plattform entweder mit dem Finger oder der Maus verschoben werden. Zur Implementierung dieser Funktion fügen Sie das DragCallbacks
-Mixin hinzu und überschreiben das Ereignis onDragUpdate
.
Schließlich muss die Komponente „Bat
“ auf die Tastatursteuerung reagieren. Die moveBy
-Funktion ermöglicht anderem Code, diesen Schläger anzuweisen, sich um eine bestimmte Anzahl virtueller Pixel nach links oder rechts zu bewegen. Mit dieser Funktion wird eine neue Funktion der Flame-Spiel-Engine eingeführt: Effect
s. Durch Hinzufügen des MoveToEffect
-Objekts als untergeordnetes Element dieser Komponente sieht der Spieler den Schläger an einer neuen Position animiert. Es gibt eine Sammlung von Effect
s in Flame, mit denen du eine Vielzahl von Effekten ausführen kannst.
Die Konstruktorargumente des Effect enthalten einen Verweis auf den Getter game
. Aus diesem Grund nehmen Sie das HasGameReference
-Mixin in diesen Kurs auf. Dieses Mixin fügt dieser Komponente eine typsichere game
-Zugriffsfunktion hinzu, um auf die BrickBreaker
-Instanz oben in der Komponentenstruktur zuzugreifen.
- Um die
Bat
fürBrickBreaker
verfügbar zu machen, aktualisieren Sie die Dateilib/src/components/components.dart
so.
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart'; // Add this export
export 'play_area.dart';
Die Welt mit dem Schläger
Wenn du der Spielwelt die Komponente „Bat
“ hinzufügen möchtest, aktualisiere BrickBreaker
so.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart'; // Add this import
import 'package:flame/game.dart';
import 'package:flutter/material.dart'; // And this import
import 'package:flutter/services.dart'; // And this
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat( // Add from here...
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95))); // To here
debugMode = true;
}
@override // Add from here...
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
} // To here
}
Das Hinzufügen des KeyboardEvents
-Mixins und die überschriebene onKeyEvent
-Methode verarbeiten die Tastatureingabe. Erinnere dich an den Code, den du zuvor hinzugefügt hast, um den Schläger um den entsprechenden Schrittbetrag zu bewegen.
Der verbleibende Code-Chunk fügt den Schläger an der entsprechenden Position und im richtigen Verhältnis zur Spielwelt hinzu. Alle diese Einstellungen in dieser Datei erleichtern dir die Anpassung der relativen Größe des Schlägers und des Balls, um das richtige Spielgefühl zu bekommen.
Wenn du das Spiel an dieser Stelle spielst, siehst du, dass du den Schläger bewegen kannst, um den Ball zu fangen. Abgesehen vom Debugging-Logging, das du im Kollisionserkennungscode von Ball
belassen hast, erhältst du jedoch keine sichtbare Antwort.
Zeit, das jetzt zu beheben. So bearbeiten Sie die Komponente „Ball
“:
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart'; // Add this import
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart'; // And this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect( // Modify from here...
delay: 0.35,
));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else { // To here.
debugPrint('collision with $other');
}
}
}
Durch diese Codeänderungen werden zwei unterschiedliche Probleme behoben.
Zuerst wird dadurch behoben, dass der Ball in dem Moment herausspringt, in dem er den unteren Bildschirmrand berührt. Um dieses Problem zu beheben, ersetzen Sie den Aufruf removeFromParent
durch RemoveEffect
. RemoveEffect
entfernt den Ball aus der Spielwelt, nachdem der Ball den sichtbaren Spielbereich verlassen hat.
Zweitens wurde durch diese Änderungen das Umgang mit der Kollision zwischen Schläger und Ball behoben. Dieser Code funktioniert sehr zugunsten des Spielers. Solange der Spieler den Ball mit dem Schläger berührt, kehrt der Ball zum oberen Bildschirmrand zurück. Wenn sich das zu nachsichtig anfühlt und du etwas realistischer haben möchtest, ändere die Steuerung, damit das Spiel sich besser an deine Vorstellungen anpassen kann.
Es ist erwähnenswert, wie komplex das velocity
-Update ist. Damit wird nicht nur die y
-Komponente der Geschwindigkeit umgekehrt, wie es bei den Wandkollisionen der Fall war. Außerdem wird die x
-Komponente entsprechend der relativen Position von Schläger und Ball zum Zeitpunkt des Kontakts aktualisiert. Dadurch hat der Spieler mehr Kontrolle darüber, was der Ball tut, aber genau, wie er dem Spieler in keiner Weise mitgeteilt wird, außer durch Spielen.
Jetzt, da Sie einen Schläger haben, mit dem Sie den Ball schlagen können, wäre es praktisch, ein paar Bausteine zu haben, um mit dem Ball zu zerbrechen!
8. Die Wand durchbrechen
Bauen der Bausteine
Um dem Spiel Bausteine hinzuzufügen,
- Fügen Sie einige Konstanten so in die Datei
lib/src/config.dart
ein.
lib/src/config.dart
import 'package:flutter/material.dart'; // Add this import
const brickColors = [ // Add this const
Color(0xfff94144),
Color(0xfff3722c),
Color(0xfff8961e),
Color(0xfff9844a),
Color(0xfff9c74f),
Color(0xff90be6d),
Color(0xff43aa8b),
Color(0xff4d908e),
Color(0xff277da1),
Color(0xff577590),
];
const gameWidth = 820.0;
const gameHeight = 1600.0;
const ballRadius = gameWidth * 0.02;
const batWidth = gameWidth * 0.2;
const batHeight = ballRadius * 2;
const batStep = gameWidth * 0.05;
const brickGutter = gameWidth * 0.015; // Add from here...
final brickWidth =
(gameWidth - (brickGutter * (brickColors.length + 1)))
/ brickColors.length;
const brickHeight = gameHeight * 0.03;
const difficultyModifier = 1.03; // To here.
- Fügen Sie die Komponente
Brick
so ein.
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Mittlerweile sollte Ihnen der größte Teil dieses Codes bekannt sein. In diesem Code wird ein RectangleComponent
mit einer Kollisionserkennung und einem typsicheren Verweis auf das Spiel BrickBreaker
oben in der Komponentenstruktur verwendet.
Das wichtigste neue Konzept, das mit diesem Code eingeführt wird, ist die Art und Weise, wie der Spieler die Gewinnbedingung erreicht. Die Gewinnbedingungsprüfung fragt die Welt nach Bausteinen ab und bestätigt, dass nur noch einer übrig ist. Dies kann etwas verwirrend sein, da die vorangehende Zeile diesen Baustein vom übergeordneten Element entfernt.
Wichtig ist, dass Sie verstehen, dass das Entfernen von Komponenten ein Befehl in der Warteschlange ist. Nachdem dieser Code ausgeführt wurde, aber vor dem nächsten Tick der Spielwelt, werden die Bausteine entfernt.
Um die Komponente „Brick
“ für BrickBreaker
zugänglich zu machen, bearbeiten Sie „lib/src/components/components.dart
“ so.
lib/src/components/components.dart
export 'ball.dart';
export 'bat.dart';
export 'brick.dart'; // Add this export
export 'play_area.dart';
Füge der Welt Bausteine hinzu
Aktualisieren Sie die Komponente Ball
so.
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart'; // Add this import
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier, // Add this parameter
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
final double difficultyModifier; // Add this member
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(
delay: 0.35,
));
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) { // Modify from here...
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier); // To here.
}
}
}
Dadurch wird der einzige neue Aspekt eingeführt: ein Schwierigkeitsmodifikator, der die Geschwindigkeit des Balls nach jedem Zusammenstoß des Bausteins erhöht. Dieser einstellbare Parameter muss getestet werden, um die für Ihr Spiel geeignete Schwierigkeitskurve zu finden.
So bearbeiten Sie das Spiel BrickBreaker
:
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
world.add(Ball(
difficultyModifier: difficultyModifier, // Add this argument
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
await world.addAll([ // Add from here...
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]); // To here.
debugMode = true;
}
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
}
return KeyEventResult.handled;
}
}
Wenn Sie das Spiel wie bisher ausführen, werden alle wichtigen Spielmechaniken angezeigt. Sie könnten das Debugging deaktivieren und als erledigt bezeichnen, aber es scheint etwas zu fehlen.
Wie wäre es mit einem Begrüßungsbildschirm, einem Spiel über dem Bildschirm und vielleicht einem Punktestand? Flutter kann dem Spiel diese Funktionen hinzufügen – und dann werden Sie Ihre Aufmerksamkeit als Nächstes lenken.
9. Das Spiel gewinnen
Wiedergabestatus hinzufügen
Bei diesem Schritt betten Sie das Flame-Spiel in einen Flutter-Wrapper ein und fügen dann Flutter-Overlays für die Bildschirme Begrüßung, Spiel vorbei und Gewonnen hinzu.
Zunächst ändern Sie die Spiel- und Komponentendateien, um einen Wiedergabestatus zu implementieren, der angibt, ob und wenn ja, welches Overlay angezeigt werden soll.
- Ändere das
BrickBreaker
-Spiel wie folgt.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won } // Add this enumeration
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector { // Modify this line
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState; // Add from here...
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
} // To here.
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome; // Add from here...
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing; // To here.
world.add(Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
world.addAll([ // Drop the await
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
} // Drop the debugMode
@override // Add from here...
void onTap() {
super.onTap();
startGame();
} // To here.
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space: // Add from here...
case LogicalKeyboardKey.enter:
startGame(); // To here.
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf); // Add this override
}
Durch diesen Code ändert sich viel für das Spiel BrickBreaker
. Das Hinzufügen der Aufzählung playState
ist aufwendig. Dadurch wird erfasst, an welcher Stelle der Spieler am Spiel teilnimmt, spielt oder ob er das Spiel verliert oder gewinnt. Am Anfang der Datei definieren Sie die Aufzählung und instanziieren sie dann als versteckten Zustand mit übereinstimmenden Getter und Setters. Diese Getter und Setter ermöglichen das Ändern von Overlays, wenn die verschiedenen Teile des Spiels den Spielstatus wechseln.
Als Nächstes teilen Sie den Code in onLoad
in onLoad und eine neue startGame
-Methode auf. Bisher war es nur möglich, ein neues Spiel zu starten, indem man es neu startet. Dank dieser Neuerungen kann der Spieler nun ohne so drastische Maßnahmen ein neues Spiel starten.
Damit der Spieler ein neues Spiel starten kann, haben Sie zwei neue Handler für das Spiel konfiguriert. Sie haben einen Tipp-Handler hinzugefügt und den Tastatur-Handler erweitert, damit der Nutzer in mehreren Modalitäten ein neues Spiel starten kann. Bei einem modellierten Wiedergabestatus ist es sinnvoll, die Komponenten so zu aktualisieren, dass sie einen Übergang des Wiedergabestatus auslösen, wenn der Spieler entweder gewinnt oder verliert.
- Ändere die Komponente „
Ball
“ so.
lib/src/components/ball.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import 'bat.dart';
import 'brick.dart';
import 'play_area.dart';
class Ball extends CircleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Ball({
required this.velocity,
required super.position,
required double radius,
required this.difficultyModifier,
}) : super(
radius: radius,
anchor: Anchor.center,
paint: Paint()
..color = const Color(0xff1e6091)
..style = PaintingStyle.fill,
children: [CircleHitbox()]);
final Vector2 velocity;
final double difficultyModifier;
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
if (other is PlayArea) {
if (intersectionPoints.first.y <= 0) {
velocity.y = -velocity.y;
} else if (intersectionPoints.first.x <= 0) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.x >= game.width) {
velocity.x = -velocity.x;
} else if (intersectionPoints.first.y >= game.height) {
add(RemoveEffect(
delay: 0.35,
onComplete: () { // Modify from here
game.playState = PlayState.gameOver;
})); // To here.
}
} else if (other is Bat) {
velocity.y = -velocity.y;
velocity.x = velocity.x +
(position.x - other.position.x) / other.size.x * game.width * 0.3;
} else if (other is Brick) {
if (position.y < other.position.y - other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.y > other.position.y + other.size.y / 2) {
velocity.y = -velocity.y;
} else if (position.x < other.position.x) {
velocity.x = -velocity.x;
} else if (position.x > other.position.x) {
velocity.x = -velocity.x;
}
velocity.setFrom(velocity * difficultyModifier);
}
}
}
Durch diese kleine Änderung wird ein onComplete
-Callback zum RemoveEffect
hinzugefügt, der den Wiedergabestatus gameOver
auslöst. Dies sollte sich in Ordnung anfühlen, wenn der Spieler zulässt, dass der Ball vom unteren Bildschirmrand entkommen kann.
- So bearbeiten Sie die Komponente „
Brick
“:
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won; // Add this line
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Wenn der Spieler andererseits alle Steine zerstören kann, hat er das Spiel gewonnen. Bildschirm. Gut gemacht, Spieler, gut gemacht!
Flutter-Wrapper hinzufügen
Mit der Flutter-Shell kannst du das Spiel einbetten und Overlays für den Spielstatus hinzufügen.
- Erstellen Sie ein
widgets
-Verzeichnis unterlib/src
. - Fügen Sie eine
game_app.dart
-Datei mit dem folgenden Inhalt hinzu.
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
class GameApp extends StatelessWidget {
const GameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffa9d6e5),
Color(0xfff2e8cf),
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget.controlled(
gameFactory: BrickBreaker.new,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) => Center(
child: Text(
'TAP TO PLAY',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.gameOver.name: (context, game) => Center(
child: Text(
'G A M E O V E R',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
PlayState.won.name: (context, game) => Center(
child: Text(
'Y O U W O N ! ! !',
style:
Theme.of(context).textTheme.headlineLarge,
),
),
},
),
),
),
),
),
),
),
),
);
}
}
Der Großteil der Inhalte in dieser Datei entspricht dem Standardaufbau eines Flutter-Widget-Baums. Zu den spezifischen Teilen für Flame gehört die Verwendung von GameWidget.controlled
zum Erstellen und Verwalten der BrickBreaker
-Spielinstanz und das neue overlayBuilderMap
-Argument für GameWidget
.
Die Schlüssel von overlayBuilderMap
müssen mit den Overlays übereinstimmen, die der playState
-Setter in BrickBreaker
hinzugefügt oder entfernt hat. Der Versuch, ein Overlay zu platzieren, das sich nicht auf dieser Karte befindet, führt zu unglücklichen Gesichtern überall in der Welt.
- Um diese neue Funktion auf dem Bildschirm zu sehen, ersetze die Datei
lib/main.dart
durch den folgenden Inhalt.
lib/main.dart
import 'package:flutter/material.dart';
import 'src/widgets/game_app.dart';
void main() {
runApp(const GameApp());
}
Wenn Sie diesen Code unter iOS, Linux, Windows oder im Web ausführen, wird die beabsichtigte Ausgabe im Spiel angezeigt. Wenn Ihre App auf macOS oder Android ausgerichtet ist, müssen Sie noch eine letzte Optimierung vornehmen, damit google_fonts
angezeigt werden kann.
Zugriff auf Schriftarten aktivieren
Internetberechtigung für Android hinzufügen
Für Android müssen Sie die Internetberechtigung hinzufügen. So bearbeiten Sie AndroidManifest.xml
:
android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Add the following line -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="brick_breaker"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
Berechtigungsdateien für macOS bearbeiten
Unter macOS müssen Sie zwei Dateien bearbeiten.
- Passen Sie die Datei
DebugProfile.entitlements
an den folgenden Code an.
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
- Bearbeiten Sie die Datei
Release.entitlements
so, dass sie zum folgenden Code passt
macos/Runner/Release.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Add from here... -->
<key>com.apple.security.network.client</key>
<true/>
<!-- to here. -->
</dict>
</plist>
Bei einer unveränderten Ausführung sollten auf allen Plattformen ein Begrüßungs- und ein Spielbildschirm angezeigt werden. Diese Bildschirme mögen etwas simpel sein und es wäre schön, eine Punktzahl zu haben. Überlegen Sie also, was Sie im nächsten Schritt tun werden!
10. Punktzahl beibehalten
Spielstand hinzufügen
In diesem Schritt stellen Sie die Spielpunktzahl dem umgebenden Flutter-Kontext zur Verfügung. In diesem Schritt geben Sie den Status aus dem Flame-Spiel für die Flutter-Statusverwaltung in der Umgebung frei. So kann der Spielcode jedes Mal den Spielstand aktualisieren, wenn der Spieler einen Baustein zerbricht.
- Ändere das
BrickBreaker
-Spiel wie folgt.
lib/src/brick_breaker.dart
import 'dart:async';
import 'dart:math' as math;
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'components/components.dart';
import 'config.dart';
enum PlayState { welcome, playing, gameOver, won }
class BrickBreaker extends FlameGame
with HasCollisionDetection, KeyboardEvents, TapDetector {
BrickBreaker()
: super(
camera: CameraComponent.withFixedResolution(
width: gameWidth,
height: gameHeight,
),
);
final ValueNotifier<int> score = ValueNotifier(0); // Add this line
final rand = math.Random();
double get width => size.x;
double get height => size.y;
late PlayState _playState;
PlayState get playState => _playState;
set playState(PlayState playState) {
_playState = playState;
switch (playState) {
case PlayState.welcome:
case PlayState.gameOver:
case PlayState.won:
overlays.add(playState.name);
case PlayState.playing:
overlays.remove(PlayState.welcome.name);
overlays.remove(PlayState.gameOver.name);
overlays.remove(PlayState.won.name);
}
}
@override
FutureOr<void> onLoad() async {
super.onLoad();
camera.viewfinder.anchor = Anchor.topLeft;
world.add(PlayArea());
playState = PlayState.welcome;
}
void startGame() {
if (playState == PlayState.playing) return;
world.removeAll(world.children.query<Ball>());
world.removeAll(world.children.query<Bat>());
world.removeAll(world.children.query<Brick>());
playState = PlayState.playing;
score.value = 0; // Add this line
world.add(Ball(
difficultyModifier: difficultyModifier,
radius: ballRadius,
position: size / 2,
velocity: Vector2((rand.nextDouble() - 0.5) * width, height * 0.2)
.normalized()
..scale(height / 4)));
world.add(Bat(
size: Vector2(batWidth, batHeight),
cornerRadius: const Radius.circular(ballRadius / 2),
position: Vector2(width / 2, height * 0.95)));
world.addAll([
for (var i = 0; i < brickColors.length; i++)
for (var j = 1; j <= 5; j++)
Brick(
position: Vector2(
(i + 0.5) * brickWidth + (i + 1) * brickGutter,
(j + 2.0) * brickHeight + j * brickGutter,
),
color: brickColors[i],
),
]);
}
@override
void onTap() {
super.onTap();
startGame();
}
@override
KeyEventResult onKeyEvent(
KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
super.onKeyEvent(event, keysPressed);
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowLeft:
world.children.query<Bat>().first.moveBy(-batStep);
case LogicalKeyboardKey.arrowRight:
world.children.query<Bat>().first.moveBy(batStep);
case LogicalKeyboardKey.space:
case LogicalKeyboardKey.enter:
startGame();
}
return KeyEventResult.handled;
}
@override
Color backgroundColor() => const Color(0xfff2e8cf);
}
Wenn Sie dem Spiel score
hinzufügen, verknüpfen Sie den Status des Spiels mit der Flutter-Statusverwaltung.
- Ändere die
Brick
-Klasse so, dass ein Punkt zum Punktestand hinzugefügt wird, wenn der Spieler die Bausteine zerbricht.
lib/src/components/brick.dart
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flutter/material.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'ball.dart';
import 'bat.dart';
class Brick extends RectangleComponent
with CollisionCallbacks, HasGameReference<BrickBreaker> {
Brick({required super.position, required Color color})
: super(
size: Vector2(brickWidth, brickHeight),
anchor: Anchor.center,
paint: Paint()
..color = color
..style = PaintingStyle.fill,
children: [RectangleHitbox()],
);
@override
void onCollisionStart(
Set<Vector2> intersectionPoints, PositionComponent other) {
super.onCollisionStart(intersectionPoints, other);
removeFromParent();
game.score.value++; // Add this line
if (game.world.children.query<Brick>().length == 1) {
game.playState = PlayState.won;
game.world.removeAll(game.world.children.query<Ball>());
game.world.removeAll(game.world.children.query<Bat>());
}
}
}
Ein gut aussehendes Spiel entwickeln
Jetzt, da du die Punktzahl in Flutter festhalten kannst, ist es an der Zeit, die Widgets so zusammenzustellen, dass alles gut aussieht.
- Erstellen Sie
score_card.dart
inlib/src/widgets
und fügen Sie Folgendes hinzu.
lib/src/widgets/score_card.dart
import 'package:flutter/material.dart';
class ScoreCard extends StatelessWidget {
const ScoreCard({
super.key,
required this.score,
});
final ValueNotifier<int> score;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: score,
builder: (context, score, child) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 18),
child: Text(
'Score: $score'.toUpperCase(),
style: Theme.of(context).textTheme.titleLarge!,
),
);
},
);
}
}
- Erstellen Sie
overlay_screen.dart
inlib/src/widgets
und fügen Sie den folgenden Code hinzu.
Dadurch werden die Overlays dank der Leistungsfähigkeit des flutter_animate
-Pakets noch schöner, um den Overlay-Bildschirmen etwas Bewegung und Stil zu verleihen.
lib/src/widgets/overlay_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
class OverlayScreen extends StatelessWidget {
const OverlayScreen({
super.key,
required this.title,
required this.subtitle,
});
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Container(
alignment: const Alignment(0, -0.15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineLarge,
).animate().slideY(duration: 750.ms, begin: -3, end: 0),
const SizedBox(height: 16),
Text(
subtitle,
style: Theme.of(context).textTheme.headlineSmall,
)
.animate(onPlay: (controller) => controller.repeat())
.fadeIn(duration: 1.seconds)
.then()
.fadeOut(duration: 1.seconds),
],
),
);
}
}
Einen detaillierteren Einblick in die Möglichkeiten von flutter_animate
finden Sie im Codelab zum Erstellen von UIs der nächsten Generation in Flutter.
Dieser Code hat sich in der Komponente GameApp
stark verändert. Damit ScoreCard
auf score
zugreifen kann, wandeln Sie es zuerst von StatelessWidget
in StatefulWidget
um. Zum Hinzufügen der Scorekarte muss eine Column
hinzugefügt werden, um die Punktzahl über dem Spiel zu stapeln.
Außerdem hast du das neue OverlayScreen
-Widget hinzugefügt, um die Begrüßung, das Spielerlebnis und den Sieg zu verbessern.
lib/src/widgets/game_app.dart
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import '../brick_breaker.dart';
import '../config.dart';
import 'overlay_screen.dart'; // Add this import
import 'score_card.dart'; // And this one too
class GameApp extends StatefulWidget { // Modify this line
const GameApp({super.key});
@override // Add from here...
State<GameApp> createState() => _GameAppState();
}
class _GameAppState extends State<GameApp> {
late final BrickBreaker game;
@override
void initState() {
super.initState();
game = BrickBreaker();
} // To here.
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
textTheme: GoogleFonts.pressStart2pTextTheme().apply(
bodyColor: const Color(0xff184e77),
displayColor: const Color(0xff184e77),
),
),
home: Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xffa9d6e5),
Color(0xfff2e8cf),
],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column( // Modify from here...
children: [
ScoreCard(score: game.score),
Expanded(
child: FittedBox(
child: SizedBox(
width: gameWidth,
height: gameHeight,
child: GameWidget(
game: game,
overlayBuilderMap: {
PlayState.welcome.name: (context, game) =>
const OverlayScreen(
title: 'TAP TO PLAY',
subtitle: 'Use arrow keys or swipe',
),
PlayState.gameOver.name: (context, game) =>
const OverlayScreen(
title: 'G A M E O V E R',
subtitle: 'Tap to Play Again',
),
PlayState.won.name: (context, game) =>
const OverlayScreen(
title: 'Y O U W O N ! ! !',
subtitle: 'Tap to Play Again',
),
},
),
),
),
),
],
), // To here.
),
),
),
),
),
);
}
}
Jetzt sollten Sie das Spiel auf jeder der sechs Flutter-Zielplattformen ausführen können. Das Spiel sollte in etwa so aussehen.
11. Glückwunsch
Herzlichen Glückwunsch! Du hast mit Flutter and Flame ein Spiel entwickelt.
Sie haben ein Spiel mit der Flame-2D-Spiel-Engine erstellt und in einen Flutter-Wrapper eingebettet. Sie haben mit „Flame's Effects“ Komponenten animiert und entfernt. 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...
- UIs der nächsten Generation in Flutter erstellen
- Langweilig wird deine Flutter-App
- In-App-Käufe zur Flutter App hinzufügen