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.

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.

Mein Flame Game – das ultimative Hintergrundbild

Beitragsnavigation


2 Gedanken zu „Mein Flame Game – das ultimative Hintergrundbild

    1. 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. 😀

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert