Ich sitze aktuell an meinem ersten Flutter-Flame-Spiel. Es beinhaltet Erdbeerpflanzen und außerdem kümmere ich mich selbst um die Grafiken. Momentan befindet es sich noch mitten im Prototyp-Status.
Da das Aussehen wichtig ist fange ich mit dem Hintergrundbild an und versuche alles weitere darauf aufzubauen. Und damit kommen wir zu meinem Problem mit den üblichen Hintergrundbildern…
Das Problem
Alle Tutorials, die ich bisher so entdeckt habe, zwingen das Gerät einfach, den Porträt-Modus zu nutzen (bzw. den Landschafts-Modus). Ich möchte aber, dass sich mein Hintergrund dynamisch anpasst, erst recht seit Flutter auch Webanwendungen unterstützt.
Entsprechend möchte ich, dass mein Bild für jedes Seitenverhältnis funktioniert und immer noch spielbar ist. Vielleicht gibt es eine Lehrbuch-Lösung, die ich einfach noch nicht gefunden habe – hier ist jedenfalls meine.
Die Lösung
Das konkrete Spielprinzip dreht sich darum, Pflanzen auf einer Fensterbank vor einem Fenster zu platzieren. Meine bespielbare Fläche ist also nur ein Fenster, alles andere ist Hintergrund.
Ich möchte das Hintergrundbild so anpassen, dass der zentrale Quadrat immer in den Screen passt. Zusätzlich etwas Hintergrund an den Seiten, je nach Größe und Modus des Screens.
Außerdem definiere ich hier ein gameRect, das von Screen und Bildgröße abhängt und die einzige Instanz ist, die ich für weitere Handhabung von Spiel-Elementen nutze. Alles innerhalb des zentralen Quadrats.
Das Programmieren
Da wir uns in der Flame-Logik bewegen, findet alles innerhalb einer game Komponente statt. Ich werde all die anderen Klassen auslassen und mich nur auf Größe, Position und Resize des Hintergrundes konzentrieren.
mygame.dart erstellt eine neue Garten-Komponente und ruft die eigene resize Methode auf, um die Bildgröße entsprechend der Spielgröße zu berechnen.
class MyGame extends BaseGame {
Garden garden;
@override
Future<void> onLoad() async {
garden = new Garden(this);
garden.resize(this.size);
}
@override
void render(Canvas canvas) {
if(garden != null) garden.render(canvas);
}
@override
void onResize(Vector2 canvasSize) {
super.onResize(canvasSize);
if (canvasSize != null && garden != null) garden.resize(canvasSize);
}
}
In meinem Falle ist garden.dart eine Super-Klasse für verschiedene Gärten, die sich sehr ähnlich verhalten. Die wichtigste Information ist das gameRect, das für weitere Berechnungen der Spiele-Komponenten genutzt wird (was nicht in diesem Blog-Beitrag enthalten ist).
Zunächst überprüfen wir, ob der Screen breiter oder höher ist (Landschaft oder Porträt). Die kleinere Größe entspricht dann der Seitenlänge unseres zentralen Quadrats im Hintergrundbild. Die scaleValue wird benötigt, damit das Hintergrundbild an den Screen angepasst wird, unabhängig von der jeweiligen Auflösung.
Beim rendern muss das Überbleibsel der größeren Screen-Seite in die Berechnung der Hintergrund-Position einbezogen werden, damit wir mehr Hintergrund anzeigen und trotzdem alles zentriert behalten.
class Garden {
final MyGame game;
Sprite _bgSprite;
Image bgImage;
double gameBaseSize;
Rect gameRect;
double scaleValue = 1;
Garden(this.game);
Future<void> onLoad() async {
_bgSprite = Sprite(bgImage);
}
Future<void> resize(Vector2 canvasSize) async {
if (game.hasLayout && _bgSprite != null) {
gameBaseSize = min(canvasSize.x, canvasSize.y);
scaleValue = gameBaseSize / (_bgSprite.srcSize[0] / 3);
gameRect = Rect.fromCenter(
center: Offset(
canvasSize.x / 2 / scaleValue,
canvasSize.y / 2 / scaleValue,
),
width: gameBaseSize / scaleValue,
height: gameBaseSize / scaleValue
);
}
return;
}
void render(Canvas c) {
c.scale(scaleValue);
if(_bgSprite != null) {
Vector2 bgOffset = Vector2(
_bgSprite.srcPosition.x - (_bgSprite.image.width / 3) + (game.size[0] - gameBaseSize) / 2,
_bgSprite.srcPosition.y - (_bgSprite.image.height / 3) + (game.size[1] - gameBaseSize) / 2,
);
_bgSprite.render(
c,
position: bgOffset
);
}
}
}
Eine Subklasse von garden ist ledgegarden.dart. Der wichtigste Teil ist, dass wir abwarten müssen, bis super.resize beendet wurde, um mit dem neu berechneten gameRect weiter arbeiten zu können.
class LedgeGarden extends Garden {
LedgeGarden(game) : super(game) {
onLoad();
}
@override
Future<void> onLoad() async {
bgImage = await Flame.images.load('backgrounds/bgdebug.png');
super.onLoad();
}
@override
Future<void> resize(Vector2 canvasSize) async {
await super.resize(canvasSize).then((value) {
if (game.hasLayout && gameRect != null) {
// calculate placement of further components
}
return;
}
}
Das Ergebnis
Ich muss sagen, dass es sehr fluffig funktioniert. Und der nötige Code ist auch nicht sonderlich umfangreich. Ich habe mit der Flutter-Webversion herum gespielt und kann es nun kaum abwarten, die anderen Komponenten meinem Spiel hinzu zu fügen.
Porträt Modus Landschafts Modus Sehr extremer Landschafts Modus
Im moment ist die Lösung noch nicht ideal für große Screens, wo wir neben dem zentralen Quadrat noch genug Platz für schmückenden Hintergrund hätten. Das das bleibt vorerst ein sekundäres Problem.
Good job! Have you tried out Flame v1? 🙂
Hey there, as a member of the flame discord channel, I upgraded as soon as it was available. 😉
I’m working on some game graphics before I can continue my flame game. Be sure that I will check in with you, as soon as it is playable. 😀