Laut einer aktuellen Umfrage sind nur zwei von fünf Startups profitabel. Ein MVP (Minimum Viable Product) erhöht die Rentabilitätschancen eines Startups erheblich, da es solchen Unternehmen ermöglicht, frühzeitig Benutzerfeedback zu sammeln, ohne das gesamte Budget für eine App mit voller Funktionalität auszugeben.
Mit MVP können Sie in kurzer Zeit und mit begrenztem Budget eine App mit grundlegenden Funktionen erstellen, Benutzerfeedback sammeln und die Lösung entsprechend diesem Feedback mit Ihrem Entwicklungsteam weiter ausbauen.
MVPs erfreuen sich in der Spielebranche zunehmender Beliebtheit. Heute erkunden wir die Besonderheiten der schnellen MVP- Entwicklung mit Flutter und Flame, einer hervorragenden Kombination für die Entwicklung plattformübergreifender minimal funktionsfähiger Produkte.
Flutter, eine funktionsreiche und sichere Plattform für plattformübergreifende Entwicklung , hat die Welt der mobilen Apps im Sturm erobert und ihre Reichweite geht weit über die Benutzeroberfläche hinaus. Mithilfe von Flame, einer robusten und quelloffenen Spiele-Engine, die auf Flutter aufbaut, können Sie atemberaubende 2D-Spiele erstellen, die reibungslos auf Android-, iOS-, Web- und Desktop-Geräten laufen.
Flutter ist auch aufgrund seiner integrierten Funktionen, die die schnelle Entwicklung von Lösungen ermöglichen, die die grundlegende Funktionalität auf verschiedenen Geräten darstellen, zu einer beliebten Lösung für den Aufbau von MVPs für Spiele geworden. Insbesondere ermöglichen verschiedene Vorteile und integrierte Funktionen von Flutter:
Flutter verbraucht nicht viele Rechenressourcen und ermöglicht die einfache Einrichtung plattformübergreifender Anwendungen.
Die auf der Kombination von Flutter und Flame basierende MVP-App ist eine zuverlässige und dennoch relativ einfach zu entwickelnde Lösung. Sie kompiliert direkt in nativen Code und sorgt so für reibungsloses Gameplay und Reaktionsfähigkeit. Sie können Ihr Game-MVP einmal entwickeln und auf verschiedenen Plattformen bereitstellen, was Zeit und Ressourcen spart. Flutter und Flame handhaben die Plattformunterschiede im Hintergrund.
Darüber hinaus verfügen beide Technologien über lebendige Communities mit ausführlicher Dokumentation, Tutorials und Codebeispielen. Das bedeutet, dass Ihnen nie die Antwort oder Inspiration ausgehen wird.
Flame bietet ein komplettes Toolset, um MVP-Spielfunktionen in kurzer Zeit und ohne übermäßigen Ressourceneinsatz zu erstellen. Dieses plattformübergreifende Modellierungsframework bietet Tools für eine Vielzahl unterschiedlicher Anwendungsfälle:
Die meisten der oben genannten Funktionen sind für viele Spiele unverzichtbar und sollten auch in der MVP-Entwicklungsphase nicht übersehen werden. Was wirklich wichtig ist, ist, dass Flame die Geschwindigkeit der Entwicklung der oben genannten Funktionen erheblich steigert, sodass Sie solche Funktionen bereits in den frühesten Produktversionen veröffentlichen können.
Anstatt über Flame zu sprechen, erstellen wir nun mit diesem Framework ein MVP, das die grundlegenden Funktionen unseres eigenen Spiels enthält. Bevor wir beginnen, müssen Sie Flutter 3.13 oder höher, Ihre bevorzugte IDE und Ihr bevorzugtes Testgerät, installiert haben.
Dieses Spiel ist von Chrome Dino inspiriert. Ah, der berühmte Dino Run! Es ist mehr als nur ein Spiel von Chrome. Es ist ein beliebtes Easter Egg, das im Offline-Modus des Browsers versteckt ist.
Unser Projekt wird den folgenden Spielablauf haben:
Und er wird „Forest Run!“ heißen!
Erstellen Sie ein leeres Flutter-Projekt, wie Sie es jedes Mal tun, wenn Sie eine neue App starten. Zu Beginn müssen wir Abhängigkeiten in pubspec.yaml für unser Projekt festlegen. Beim Schreiben dieses Beitrags ist die neueste Version von Flame 1.14.0. Lassen Sie uns jetzt auch alle Asset-Pfade definieren, damit wir später nicht mehr auf diese Datei zurückgreifen müssen. Und legen Sie Bilder in das Verzeichnis asset/images/. Wir müssen es hier ablegen, da Flame genau diesen Pfad scannt:
environment: sdk: '>=3.2.3 <4.0.0' flutter: '>=3.13.0' dependencies: flutter: sdk: flutter flame: ^1.14.0 flutter: uses-material-design: true assets: - assets/images/ - assets/images/character/ - assets/images/background/ - assets/images/forest/ - assets/images/font/
Denken Sie daran, alle Bilder unter „assets/images/“ abzulegen, da Flame andere Verzeichnisse nicht analysiert.
Für jedes Spiel benötigen Sie eine Menge Bilder. Aber was, wenn Sie kein gutes Designtalent sind? Glücklicherweise gibt es viele Open-Source-Assets, die Sie für Ihre Projekte verwenden können. Die Assets für dieses Spiel stammen von itch.io. Wir werden diese Ressourcen für unser Projekt verwenden:
Sie können diese Links besuchen oder einfach vorbereitete Assets (LINK ZUM ASSETS-ARCHIV) für dieses Projekt herunterladen und den gesamten Inhalt in Ihr Projekt kopieren.
Flame hat eine ähnliche Philosophie wie Flutter. In Flutter ist alles ein Widget; in Flame ist alles eine Komponente, sogar das ganze Spiel. Jede Komponente kann 2 Methoden überschreiben: onLoad() und update(). onLoad() wird nur einmal aufgerufen, wenn die Komponente in den ComponentTree eingebunden wird, und update() wird bei jedem Frame ausgelöst. Sehr ähnlich zu initState() und build() von StatefulWidget in Flutter.
Schreiben wir nun etwas Code. Erstellen Sie eine Klasse, die FlameGame erweitert und alle unsere Assets in den Cache lädt.
class ForestRunGame extends FlameGame { @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); } }
Als nächstes verwenden Sie ForestRunGame in main.dart. Sie können auch Methoden von Flame.device verwenden, um die Geräteausrichtung zu konfigurieren. Und es gibt GameWidget, das als Brücke zwischen Widgets und Komponenten dient.
Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Flame.device.fullScreen(); await Flame.device.setLandscape(); runApp(GameWidget(game: ForestRunGame())); }
An diesem Punkt können wir das Spiel bereits starten, aber es wird nur ein schwarzer Bildschirm angezeigt. Also müssen wir unsere Komponenten hinzufügen.
Wir werden den Wald in zwei Komponenten unterteilen: Hintergrund und Vordergrund. Zuerst kümmern wir uns um den Hintergrund. Haben Sie schon einmal durch eine Seite gescrollt, die sich dynamisch anfühlte? Als ob Sie durch mehrere Ansichten gleichzeitig gescrollt hätten? Das ist ein Parallaxeffekt, der auftritt, wenn sich die verschiedenen Elemente einer Seite mit unterschiedlicher Geschwindigkeit bewegen und so einen 3D-Tiefeneffekt erzeugen.
Wie Sie sich vorstellen können, verwenden wir für unseren Hintergrund eine Parallaxe. Erweitern Sie ParallaxComponent und richten Sie mit ParallaxImageData einen Stapel Bilder ein. Außerdem gibt es baseVelocity für die Geschwindigkeit der ersten Ebenen und velocityMultiplierDelta, das für den relativen Geschwindigkeitsunterschied zwischen den Ebenen steht. Und als Letztes konfigurieren Sie das Prioritätsfeld (Z-Index), um es hinter andere Komponenten zu verschieben.
class ForestBackground extends ParallaxComponent<ForestRunGame> { @override Future<void> onLoad() async { priority = -10; parallax = await game.loadParallax( [ ParallaxImageData('background/plx-1.png'), ParallaxImageData('background/plx-2.png'), ParallaxImageData('background/plx-3.png'), ParallaxImageData('background/plx-4.png'), ParallaxImageData('background/plx-5.png'), ], baseVelocity: Vector2.zero(), velocityMultiplierDelta: Vector2(1.4, 1.0), ); } }
Der Hintergrund ist fertig; jetzt ist es Zeit, den Vordergrund hinzuzufügen. Erweitern Sie die PositionComponent, damit wir den Boden am unteren Bildschirmrand ausrichten können. Wir benötigen außerdem das HasGameReference-Mixin, um auf den Spiel-Cache zuzugreifen.
Um einen Boden zu erstellen, müssen Sie das Bodenblockbild einfach mehrmals in die Zeile einfügen. In Flame werden Bildkomponenten Sprites genannt. Ein Sprite ist ein Bereich eines Bildes, der im Canvas gerendert werden kann. Es kann das gesamte Bild darstellen oder eines der Teile sein, aus denen ein Spritesheet besteht.
Denken Sie auch daran, dass die X-Achse nach rechts und die Y-Achse nach unten ausgerichtet ist. Der Mittelpunkt der Achsen befindet sich in der linken oberen Ecke des Bildschirms.
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { static final blockSize = Vector2(480, 96); late final Sprite groundBlock; late final Queue<SpriteComponent> ground; @override void onLoad() { super.onLoad(); groundBlock = Sprite(game.images.fromCache('forest/ground.png')); ground = Queue(); } @override void onGameResize(Vector2 size) { super.onGameResize(size); final newBlocks = _generateBlocks(); ground.addAll(newBlocks); addAll(newBlocks); y = size.y - blockSize.y; } List<SpriteComponent> _generateBlocks() { final number = 1 + (game.size.x / blockSize.x).ceil() - ground.length; final lastBlock = ground.lastOrNull; final lastX = lastBlock == null ? 0 : lastBlock.x + lastBlock.width; return List.generate( max(number, 0), (i) => SpriteComponent( sprite: groundBlock, size: blockSize, position: Vector2(lastX + blockSize.x * i, y), priority: -5, ), growable: false, ); } }
Und als Letztes fügen Sie diese Komponenten zu unserem ForestRunGame hinzu.
class ForestRunGame extends FlameGame { late final foreground = ForestForeground(); late final background = ForestBackground(); @override Future<void> onLoad() async { await super.onLoad(); await images.loadAllImages(); add(foreground); add(background); } }
Versuchen Sie nun, das Spiel zu starten. An diesem Punkt haben wir bereits unseren Wald.
Der Wald sieht gut aus, ist aber im Moment nur ein Bild. Also werden wir Jack erstellen, der unter Anleitung des Spielers durch diesen Wald rennt. Anders als Bäume und Boden braucht der Spieler Animationen, um sich lebendig zu fühlen. Wir haben Sprite für Bodenblöcke verwendet, aber für Jack werden wir SpriteAnimation verwenden. Wie funktioniert das? Nun, es ist alles ganz einfach, Sie müssen nur eine Folge von Sprites in einer Schleife abspielen. Unsere Laufanimation hat beispielsweise 8 Sprites, die sich mit einer kleinen Zeitlücke gegenseitig ersetzen.
Jack kann rennen, springen und untätig sein. Um seine Zustände darzustellen, können wir eine PlayerState-Aufzählung hinzufügen. Erstellen Sie dann einen Player, der SpriteAnimationGroupComponent erweitert, und übergeben Sie PlayerState als generisches Argument. Diese Komponente hat ein Animationsfeld, in dem Animationen für jeden PlayerState gespeichert sind, und ein aktuelles Feld, das den aktuellen Zustand des Players darstellt, der animiert werden muss.
enum PlayerState { jumping, running, idle } class Player extends SpriteAnimationGroupComponent<PlayerState> { @override void onLoad() { super.onLoad(); animations = { PlayerState.running: SpriteAnimation.fromFrameData( game.images.fromCache('character/run.png'), SpriteAnimationData.sequenced( amount: 8, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(23, 34), ), ), PlayerState.idle: SpriteAnimation.fromFrameData( game.images.fromCache('character/idle.png'), SpriteAnimationData.sequenced( amount: 12, amountPerRow: 5, stepTime: 0.1, textureSize: Vector2(21, 35), ), ), PlayerState.jumping: SpriteAnimation.spriteList( [ Sprite(game.images.fromCache('character/jump.png')), Sprite(game.images.fromCache('character/land.png')), ], stepTime: 0.4, loop: false, ), }; current = PlayerState.idle; } }
Die Spielerzustände sind bereit. Jetzt müssen wir dem Spieler eine Größe und Position auf dem Bildschirm zuweisen. Ich werde seine Größe auf 69 x 102 Pixel festlegen, aber Sie können sie gerne nach Belieben ändern. Für die Position müssen wir die Koordinaten des Bodens kennen. Durch Hinzufügen des HasGameReference-Mixins können wir auf das Vordergrundfeld zugreifen und seine Koordinaten abrufen. Lassen Sie uns nun die Methode onGameResize überschreiben, die jedes Mal aufgerufen wird, wenn die Größe der Anwendung geändert wird, und dort die Position von Jack festlegen.
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { static const startXPosition = 80.0; Player() : super(size: Vector2(69, 102)); double get groundYPosition => game.foreground.y - height + 20; // onLoad() {...} with animation setup @override void onGameResize(Vector2 size) { super.onGameResize(size); x = startXPosition; y = groundYPosition; } }
Fügen Sie den Spieler wie zuvor zu unserem Spiel hinzu.
class ForestRunGame extends FlameGame { // Earlier written code here... late final player = Player(); @override Future<void> onLoad() async { // Earlier written code here... add(player); } }
Wenn Sie das Spiel starten, sehen Sie, dass Jack bereits im Wald ist!
Unser Spiel hat drei Zustände: Intro, Play und Game Over. Wir fügen also die Enumeration GameState hinzu, die diese Zustände repräsentiert. Damit Jack rennen kann, benötigen wir die Variablen Geschwindigkeit und Beschleunigung. Außerdem müssen wir die zurückgelegte Strecke berechnen (wird später verwendet).
Wie bereits erwähnt, verfügt die Komponente über zwei Hauptmethoden: onLoad() und update(). Die Methode onLoad haben wir bereits einige Male verwendet. Lassen Sie uns nun über update() sprechen. Diese Methode verfügt über einen Parameter namens dt. Er stellt die Zeit dar, die seit dem letzten Aufruf von update() vergangen ist.
Um die aktuelle Geschwindigkeit und die zurückgelegte Strecke zu berechnen, verwenden wir die Methode update() und einige grundlegende Kinematikformeln:
enum GameState { intro, playing, gameOver } class ForestRunGame extends FlameGame { static const acceleration = 10.0; static const maxSpeed = 2000.0; static const startSpeed = 400.0; GameState state = GameState.intro; double currentSpeed = 0; double traveledDistance = 0; // Earlier written code here... @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } }
Tatsächlich werden wir einen Trick anwenden, um die Entwicklung einfacher zu machen: Jack bleibt stabil, aber der Wald bewegt sich auf Jack zu. Unser Wald muss also Spielgeschwindigkeit anwenden.
Für den Parallax-Hintergrund müssen wir nur die Spielgeschwindigkeit übergeben. Der Rest wird automatisch erledigt.
class ForestBackground extends ParallaxComponent<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); parallax?.baseVelocity = Vector2(game.currentSpeed / 10, 0); } }
Für den Vordergrund müssen wir jeden Bodenblock verschieben. Außerdem müssen wir prüfen, ob der erste Block in der Warteschlange den Bildschirm verlassen hat. Wenn ja, entfernen Sie ihn und platzieren Sie ihn am Ende der Warteschlange.
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void update(double dt) { super.update(dt); final shift = game.currentSpeed * dt; for (final block in ground) { block.x -= shift; } final firstBlock = ground.first; if (firstBlock.x <= -firstBlock.width) { firstBlock.x = ground.last.x + ground.last.width; ground.remove(firstBlock); ground.add(firstBlock); } } }
Alles ist bereit, bis auf einen Auslöser. Wir möchten die Ausführung per Klick starten. Unsere Ziele sind sowohl Mobilgeräte als auch Desktops, daher möchten wir Bildschirmberührungen und Tastaturereignisse verarbeiten.
Glücklicherweise gibt es bei Flame eine Möglichkeit, dies zu tun. Fügen Sie einfach ein Mixin für Ihren Eingabetyp hinzu. Für die Tastatur sind es KeyboardEvents und TapCallbacks für das Tippen auf den Bildschirm. Diese Mixins geben Ihnen die Möglichkeit, verwandte Methoden zu überschreiben und Ihre Logik bereitzustellen.
Das Spiel muss starten, wenn der Benutzer die Leertaste drückt oder auf den Bildschirm tippt.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { start(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { start(); } void start() { state = GameState.playing; player.current = PlayerState.running; currentSpeed = startSpeed; traveledDistance = 0; } }
Als Ergebnis kann Jack nun nach dem Klicken laufen.
Jetzt wollen wir Hindernisse auf der Straße haben. In unserem Fall werden sie als giftige Büsche dargestellt. Der Busch ist nicht animiert, also können wir SpriteComponent verwenden. Außerdem brauchen wir eine Spielreferenz, um auf seine Geschwindigkeit zuzugreifen. Und noch etwas: Wir wollen die Büsche nicht einzeln erscheinen lassen, denn dieser Ansatz kann dazu führen, dass Jack eine Reihe von Büschen einfach nicht mit einem Sprung passieren kann. Es handelt sich um eine Zufallszahl aus dem Bereich, der von der aktuellen Spielgeschwindigkeit abhängt.
class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { late double gap; Bush() : super(size: Vector2(200, 84)); bool get isVisible => x + width > 0; @override Future<void> onLoad() async { x = game.size.x + width; y = -height + 20; gap = _computeRandomGap(); sprite = Sprite(game.images.fromCache('forest/bush.png')); } double _computeRandomGap() { final minGap = width * game.currentSpeed * 100; final maxGap = minGap * 5; return (Random().nextDouble() * (maxGap - minGap + 1)).floor() + minGap; } @override void update(double dt) { super.update(dt); x -= game.currentSpeed * dt; if (!isVisible) { removeFromParent(); } } }
Wer pflanzt Büsche? Natürlich die Natur. Lasst uns eine Natur schaffen, die unsere Buschgeneration bewirtschaftet.
class Nature extends Component with HasGameReference<ForestRunGame> { @override void update(double dt) { super.update(dt); if (game.currentSpeed > 0) { final plant = children.query<Bush>().lastOrNull; if (plant == null || (plant.x + plant.width + plant.gap) < game.size.x) { add(Bush()); } } } }
Fügen wir nun „Natur“ zu unserem Waldvordergrund hinzu.
class ForestForeground extends PositionComponent with HasGameReference<ForestRunGame> { // Earlier written code here... late final Nature nature; @override void onLoad() { // Earlier written code here... nature = Nature(); add(nature); }
Jetzt hat unser Wald Büsche. Aber Moment, Jack rennt einfach durch sie hindurch. Warum passiert das? Weil wir das Schlagen noch nicht implementiert haben.
Hier hilft uns Hitbox. Hitbox ist eine weitere Komponente im Komponentenzoo von Flame. Es kapselt die Kollisionserkennung und gibt Ihnen die Möglichkeit, diese mit benutzerdefinierter Logik zu handhaben.
Fügen Sie einen für Jack hinzu. Denken Sie daran, dass die Position der Komponente ihre linke und rechte Ecke und nicht ihre Mitte bestimmt. Und mit der Größe erledigen Sie den Rest.
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(2, 2), size: Vector2(60, 100), ), ); } }
Und eine für den Busch. Hier setzen wir den Kollisionstyp zur Optimierung auf passiv. Standardmäßig ist der Typ aktiv, was bedeutet, dass Flame prüft, ob diese Hitbox mit jeder anderen Hitbox kollidiert. Wir haben nur einen Spieler und Büsche. Da der Spieler bereits einen aktiven Kollisionstyp hat und Büsche nicht miteinander kollidieren können, können wir den Typ auf passiv setzen.
class Bush extends SpriteComponent with HasGameReference<ForestRunGame> { // Earlier written code here... @override void onLoad() { // Earlier written code here... add( RectangleHitbox( position: Vector2(30, 30), size: Vector2(150, 54), collisionType: CollisionType.passive, ), ); } }
Es ist cool, aber ich kann nicht sehen, ob die Position der Hitbox richtig eingestellt wurde. Wie kann ich das testen?
Nun, Sie können das DebugMode-Feld von Player und Bush auf „true“ setzen. So können Sie sehen, wie Ihre Hitboxen positioniert sind. Lila definiert die Größe der Komponente und Gelb zeigt die Hitbox an.
Jetzt wollen wir erkennen, wann es zu einer Kollision zwischen dem Spieler und dem Busch kommt. Dazu müssen Sie dem Spiel das Mixin HasCollisionDetection und dann CollisionCallbacks für Komponenten hinzufügen, die Kollisionen behandeln müssen.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... }
Pausieren Sie das Spiel vorerst einfach, wenn die Kollision erkannt wird.
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.paused = true; } }
Wenn Jack diesen Büschen ausweichen will, muss er springen. Bringen wir es ihm bei. Für diese Funktion benötigen wir die Schwerkraftkonstante und die anfängliche vertikale Geschwindigkeit von Jacks Sprung. Diese Werte wurden nach Augenmaß ausgewählt, Sie können sie also gerne anpassen.
Wie funktioniert also die Schwerkraft? Im Grunde handelt es sich um dieselbe Beschleunigung, nur dass sie auf den Boden gerichtet ist. Daher können wir für die vertikale Position und die Geschwindigkeit dieselben Formeln verwenden. Unser Sprung besteht also aus drei Schritten:
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { static const gravity = 1400.0; static const initialJumpVelocity = -700.0; double jumpSpeed = 0; // Earlier written code here... void jump() { if (current != PlayerState.jumping) { current = PlayerState.jumping; jumpSpeed = initialJumpVelocity - (game.currentSpeed / 500); } } void reset() { y = groundYPos; jumpSpeed = 0; current = PlayerState.running; } @override void update(double dt) { super.update(dt); if (current == PlayerState.jumping) { y += jumpSpeed * dt; jumpSpeed += gravity * dt; if (y > groundYPos) { reset(); } } else { y = groundYPos; } } }
Und jetzt lösen wir das Springen per Klick von ForestRunGame aus aus
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... @override KeyEventResult onKeyEvent( RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed, ) { if (keysPressed.contains(LogicalKeyboardKey.space)) { onAction(); } return KeyEventResult.handled; } @override void onTapDown(TapDownEvent event) { onAction(); } void onAction() { switch (state) { case GameState.intro: case GameState.gameOver: start(); break; case GameState.playing: player.jump(); break; } } }
Jetzt kann Jack mit Büschen umgehen.
Wenn das Spiel vorbei ist, möchten wir Text auf dem Bildschirm anzeigen. Text funktioniert in Flame anders als in Flutter. Sie müssen zuerst eine Schriftart erstellen. Im Grunde ist es nur eine Karte, bei der char ein Schlüssel und sprite ein Wert ist. Fast immer ist die Schriftart des Spiels ein Bild, in dem alle benötigten Symbole gesammelt sind.
Für dieses Spiel brauchen wir nur Ziffern und Großbuchstaben. Also erstellen wir unsere Schriftart. Dazu müssen Sie Quellbild und Glyphen übergeben. Was ist eine Glyphe? Eine Glyphe ist eine Kombination aus Informationen über ein Zeichen, seine Größe und Position im Quellbild.
class StoneText extends TextBoxComponent { static const digits = '123456789'; static const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; StoneText({ required Image source, required super.position, super.text = '', }) : super( textRenderer: SpriteFontRenderer.fromFont( SpriteFont( source: source, size: 32, ascent: 32, glyphs: [ _buildGlyph(char: '0', left: 480, top: 0), for (var i = 0; i < digits.length; i++) _buildGlyph(char: digits[i], left: 32.0 * i, top: 32), for (var i = 0; i < letters.length; i++) _buildGlyph( char: letters[i], left: 32.0 * (i % 16), top: 64.0 + 32 * (i ~/ 16), ), ], ), letterSpacing: 2, ), ); static Glyph _buildGlyph({ required String char, required double left, required double top, }) => Glyph(char, left: left, top: top, height: 32, width: 32); }
Jetzt können wir das Game-Over-Panel erstellen und im Spiel verwenden.
class GameOverPanel extends PositionComponent with HasGameReference<ForestRunGame> { bool visible = false; @override Future<void> onLoad() async { final source = game.images.fromCache('font/keypound.png'); add(StoneText(text: 'GAME', source: source, position: Vector2(-144, -16))); add(StoneText(text: 'OVER', source: source, position: Vector2(16, -16))); } @override void renderTree(Canvas canvas) { if (visible) { super.renderTree(canvas); } } @override void onGameResize(Vector2 size) { super.onGameResize(size); x = size.x / 2; y = size.y / 2; } }
Jetzt können wir unser Panel anzeigen, wenn Jack den Busch trifft. Lassen Sie uns auch die Start()-Methode ändern, damit wir das Spiel per Klick neu starten können. Außerdem müssen wir alle Büsche aus dem Wald entfernen.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { // Earlier written code here... late final gameOverPanel = GameOverPanel(); @override Future<void> onLoad() async { // Earlier written code here... add(gameOverPanel); } void gameOver() { paused = true; gameOverPanel.visible = true; state = GameState.gameOver; currentSpeed = 0; } void start() { paused = false; state = GameState.playing; currentSpeed = startSpeed; traveledDistance = 0; player.reset(); foreground.nature.removeAll(foreground.nature.children); gameOverPanel.visible = false; } }
Und jetzt müssen wir den Kollisions-Callback im Player aktualisieren.
class Player extends SpriteAnimationGroupComponent<PlayerState> with HasGameReference<ForestRunGame>, CollisionCallbacks { // Earlier written code here... @override void onCollisionStart( Set<Vector2> intersectionPoints, PositionComponent other, ) { super.onCollisionStart(intersectionPoints, other); game.gameOver(); } }
Jetzt können Sie „Game Over“ sehen, wenn Jack gegen einen Busch fährt. Und Sie können das Spiel durch erneutes Klicken neu starten.
Und die endgültige Berechnung des Touch-Scores.
class ForestRunGame extends FlameGame with KeyboardEvents, TapCallbacks, HasCollisionDetection { late final StoneText scoreText; late final StoneText highText; late final StoneText highScoreText; int _score = 0; int _highScore = 0; // Earlier written code here... @override Future<void> onLoad() async { // Earlier written code here... final font = images.fromCache('font/keypound.png'); scoreText = StoneText(source: font, position: Vector2(20, 20)); highText = StoneText(text: 'HI', source: font, position: Vector2(256, 20)); highScoreText = StoneText( text: '00000', source: font, position: Vector2(332, 20), ); add(scoreText); add(highScoreText); add(highText); setScore(0); } void start() { // Earlier written code here... if (_score > _highScore) { _highScore = _score; highScoreText.text = _highScore.toString().padLeft(5, '0'); } _score = 0; } @override void update(double dt) { super.update(dt); if (state == GameState.playing) { traveledDistance += currentSpeed * dt; setScore(traveledDistance ~/ 50); if (currentSpeed < maxSpeed) { currentSpeed += acceleration * dt; } } } void setScore(int score) { _score = score; scoreText.text = _score.toString().padLeft(5, '0'); } }
Das war's Leute!
Probieren Sie es jetzt aus und versuchen Sie, meinen Highscore zu schlagen. Er liegt bei 2537 Punkten!
Es war viel, aber wir haben es geschafft. Wir haben ein minimal funktionsfähiges Produkt für ein Handyspiel mit Physik, Animationen, Punkteberechnung und vielem mehr geschaffen. Es gibt immer Raum für Verbesserungen, und wie bei jedem anderen MVP können wir von unserem Produkt in Zukunft neue Funktionen, Mechaniken und Spielmodi erwarten.
Außerdem gibt es ein flame_audio-Paket, mit dem Sie Hintergrundmusik, Sprung- oder Schlaggeräusche usw. hinzufügen können.
Unser Hauptziel bestand derzeit darin, die grundlegenden Produktfunktionen kurzfristig und mit begrenzter Ressourcenzuweisung zu erstellen. Die Kombination aus Flutter und Flame erwies sich als perfekt geeignet, um ein MVP-Spiel zu erstellen, mit dem Benutzerfeedback gesammelt und die App in Zukunft weiter verbessert werden kann.
Die Ergebnisse unserer Bemühungen können Sie hier überprüfen .
Mit ihren leistungsstarken Funktionen, ihrer Benutzerfreundlichkeit und ihrer florierenden Community sind Flutter und Flame eine überzeugende Wahl für aufstrebende Spieleentwickler. Egal, ob Sie ein erfahrener Profi oder Anfänger sind, diese Kombination bietet die Tools und das Potenzial, um Ihre Spielideen zum Leben zu erwecken. Lassen Sie Ihrer Kreativität freien Lauf, tauchen Sie in die Welt von Flutter und Flame ein und beginnen Sie mit der Entwicklung der nächsten mobilen Gaming-Sensation!
Wir hoffen, Sie fanden diesen Artikel unterhaltsam und informativ. Wenn Sie weitere Einblicke in die Softwareentwicklung wünschen oder Ihr eigenes MVP-Projekt besprechen möchten, zögern Sie nicht, Leobit zu erkunden oder sich an unser technisches Team zu wenden!