Android Game Tutorial Teil 3: Head-up Display und Performance Optimierung

Im vorläufig finalen Teil dieses Tutorials lernst Du ein schwebendes Head-up Display anzuzeigen.

Android Game Tutorial final

Abbildung 1: Spiel am Ende des Tutorials

  • Teil 1 - Background Scrolling und Double buffering
  • Teil 2 - Sprite Animation
  • Teil 3 - Head-up Display und Performance Optimierung

Variable Hintergrundgeschwindigkeit

Um die Animation ein wenig lebhafter oder näher an der Realität zu gestalteten interpretierst Du nun eine Position rechts der Mitte als schnelles fliegen und links der Mitte als langsames fliegen. Dazu erweiterst Du onSensorChanged ein weiteres mal.

        float[] coords = getPlanePosition(y,z,maxAngle);

        int bgBaseSpeed = 12;

        int bgSpeed = (int) (bgBaseSpeed + ((24)*(y/maxAngle)));
        bgv.getBackgroundModel().setSpeed(bgSpeed < 3 ? 3 : bgSpeed);

        pv.getPlane().setPosition(coords);
        pv.invalidate();
      
Listing 1: Berechnen der horizontalen Position des Flugzeuges

In Zeile 6 sorgen wir dafür, dass die Geschwindigkeit des Hintergrundes nicht unter 3 geht, da dies die Illusion eines schwebenden Flugzeuges erwecken würde.

Nun ändert sich die Hintergrundgeschwindigkeit in Abhängigkeit von der Position des Flugzeuges.

5.1 Anzeigen der Geschwindigkeit als Hud

Als ein Hud bezeichnet man ein Anzeigefeld das sich im Blick des Anwenders, in diesem Fall, des Spielers befindet.

Um die aktuelle Geschwindigkeit als zusätzliche Information auf den Bildschirm zu bringen, musst Du die Information aus der Background Klasse holen, und diese auf das Display zeichnen. Dies machst Du in einem View.

Man könnte dies in einen eigenen View auslagern, aber für dieses Tutorial ist es einfacher die Information auf die Ebene zu legen auf der sich das Flugzeug befindet.

Dazu erweiterst Du die Klasse PlaneView um die markierten Felder.

public class PlaneView extends View {
  private Plane plane;
  private Paint paint;
  private Rect rect;
  private final String textLabel = "Speed: ";
  private Context context;
  …
Listing 2: Erweiterung der PlaneView-Klasse

Im Konstruktor weist Du den Context, welchen wir zum Erzeugen des Views benutzt haben, dem erstellten Feld zu. Den Context benötigst Du später um die Textfarbe auflösen zu können.

        public PlaneView(Context context, Plane plane, Background bg) {
          super(context);
          this.plane = plane;
          this.bg = bg;
          this.context = context;
        }
      
Listing 3: PlaneView Konstruktor

Im onDraw-Callback wird der anzuzeigende Text zusammen gebaut. Dieser besteht aus einem Label, das beschreibt um welche Art von Information es sich handelt, und die Information selbst. Dabei invertieren wir die Geschwindigkeit des Hintergrundes.

Zeichne das Label und die Geschwindigkeit in die obere rechte Ecke des Bildschirms. Da Du die Größe des Textes nicht kennst, musst Du diese zuerst berechnen. Dazu gibst Du den Text, sowie ein Rect-Objekt an die Methode getTextBounds weiter. Danach steht in Rect die endgültige Größe des Textes, wenn er mit den in Paint spezifizierten Optionen gezeichnet wird.

  @Override
  protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(plane.getImage(), plane.getX(), plane.getY(), null);

    String text = textLabel+(-bg.getSpeed());
    Paint paint = getPaint(30);
    paint.getTextBounds(text, 0, text.length(), rect);
    canvas.drawText(text, (getWidth()-(int)(rect.width()*1.1)), rect.height(), paint);

  }

  private Paint getPaint(float fontSize) {
    if (rect == null)
      rect = new Rect();
    if (paint == null) {
      paint = new Paint();
      Typeface tf = Typeface.create("Droid Sans", Typeface.BOLD);
      paint.setTypeface(tf);
      paint.setStyle(Style.FILL);
      paint.setAntiAlias(true);
      paint.setColor(context.getResources().getColor(android.R.color.holo_green_light));
      paint.setTextSize(fontSize);
    }
    return paint;
  }
      
Listing 4: Hud zeichnen

Das Ergebnis sollte in etwa so aussehen:

Hud Added

Abbildung 2: Hud hinzugefügt

5.2 Optimieren der Head-up Anzeige

Beim Testen der App wirst Du feststellen, dass die Stellenzahl in der Anzeige der Geschwindigkeit variiert. Dies führt zu einem Springen des Textes. Wird die Geschwindigkeit einstellig, benötigt der Text weniger Platz. Da die Positionierung des Textes ebenfalls stetig neu gerechnet wird, wird die Position an die neuen Verhältnisse angepasst.

Dies kannst Du dadurch beheben, dass Du die Information auf zwei verschiedene Zeichenoperationen verteilst.

  @Override
  protected void onDraw(Canvas canvas) {
    keepPlaneVisible(plane.getX(), plane.getY());
    canvas.drawBitmap(plane.getImage(), coords[0], coords[1], null);

    String speedText = ""+(-bg.getSpeed());
    Paint paint = getPaint(30);
    paint.getTextBounds(speedText, 0, speedText.length(), rect);
    float speedTextX = getWidth()-(int)(rect.width()*1.1);
    canvas.drawText(speedText, speedTextX, rect.height(), paint);

    paint.getTextBounds(textLabel, 0, textLabel.length(), rect);
    canvas.drawText(textLabel,speedTextX-(int)(rect.width()*1.1),rect.height() , paint);
  }
      
Listing 5: Hud Anzeige mit zwei Labeln

Jetzt ist der Text aufgeteilt, doch die Position des Labels ist noch von der Breite der Geschwindigkeitsanzeige abhängig. Das Label wird also wieder springen.

Da die Geschwindigkeit so gewählt ist, dass sie niemals über zwei Stellen hinaus kommen kann und mit zwei Stellen startet, berechnest Du die Position des Labels einmalig beim ersten Zeichnen und merkst Dir im Folgenden die Position.

  @Override
  protected void onDraw(Canvas canvas) {
    keepPlaneVisible(plane.getX(), plane.getY());
    canvas.drawBitmap(plane.getImage(), coords[0], coords[1], null);

    String speedText = ""+(-bg.getSpeed());
    Paint paint = getPaint(30);
    paint.getTextBounds(textLabel, 0, speedText.length(), rect);

    if (yOffset == 0)
      yOffset = rect.height();

    paint.getTextBounds(speedText, 0, speedText.length(), rect);

    if (speedXCoordinate == 0)
      speedXCoordinate = (getWidth()-rect.width())-10;

    paint.getTextBounds(textLabel, 0, textLabel.length(), rect);
    canvas.drawText(textLabel, (speedXCoordinate-rect.width()-10), yOffset, paint);
    canvas.drawText(speedText, speedXCoordinate, yOffset, paint);
  }
Listing 6: Berechnung der Position der Hud Texte

Die beiden Instanzvariablen yOffset und speedXCoordinate existieren noch nicht. Diese legt Dir Eclipse an, wenn du einen Rechtsklick auf die Variablen ausführst und dann die Option Create Field wählst.

5.3 Verschiedene Dispalygrößen

Da Du deine App auf möglichst vielen Geräten gleich darstellen möchtest, gibt es noch ein Problem zu lösen. Das Padding zwischen den beiden Texten wurde in Listing Zeile 16 und 19 durch eine Konstante 10 realisiert. Das führt aber in Abhängigkeit von der Displaygröße zu unterschiedlichen Ergebnissen. Das gleiche gilt für die an getPaint übergebene Fontsize in Zeile 7. Android selbst bietet eine relativ simple Lösung für dieses Problem.

Im Projekt gibt es einen Ordner Values, und darin erzeugst Du eine dimens.xml Datei. Sollte diese noch nicht existieren, erstelle diese durch einen Rechtsklick auf den Ordner values und wähle die New>Android XML File. Im Folgenden Dialog gibst du den Dateinamen dimens.xml an und schließt den DIalog mit einem Klick auf finish.

Die Datei sollte zu Beginn so aussehen:

            <?xml version="1.0" encoding="utf-8"?>
            <resources>

            </resources>
        
Listing 7: dimens.xml Datei

Du ergänzt diese um die folgenden Einträge:

        <dimen name="labelPadding">10dp</dimen>
        <dimen name="speedFontSize">30sp</dimen>
Listing 8: Dimen Einträge

Nun kannst du die hinterlegten Werte im Code auslesen, dabei sind diese automatisch an die Displaygröße des jeweiligen Gerätes angepasst:

      protected void onDraw(Canvas canvas) {
        keepPlaneVisible(plane.getX(), plane.getY());
        canvas.drawBitmap(plane.getImage(), coords[0], coords[1], null);

        String speedText = ""+(-bg.getSpeed());
        int fontSize = context.getResources().getDimensionPixelSize(R.dimen.speedFontSize);
        Paint paint = getPaint(fontSize);
      
Listing 9: Zugriff auf Ressourcen

Diese Ergänzung bewirkt, dass die Darstellung des Textes auf allen Geräten gleich groß erscheinen wird, da Du keine absolute Größe angibst. Die für die Schriftgröße angegebenen 30sp werden vom System entsprechend der Displaygröße umgerechnet. Überarbeite entsprechend auch das Padding des Labels.

    if (speedXCoordinate == 0)
      speedXCoordinate = (getWidth()-rect.width()/2)- context.getRessources().getDimensionPixelSize(R.dimen.labelPadding);
    
Listing 10: Padding abhängig von der Displaygröße

5.4 Leistungsoptimierung

Um gerade bei älteren Geräten eine bessere Performance zu erreichen, empfiehlt es sich, den Bildschirm zu "beschneiden". Anhand unserer Skizze können wir absehen, dass die zwei Bilder zusammen auf jeden Fall größer sind als der Bildschirm selbst. Daher empfiehlt es sich, den eigentlichen Zeichenbereich auf die Displaygröße zu reduzieren um die Rechenzeit zu minimieren. Ob Android bei einem SurfaceView über die Grenzen hinaus zeichnen würde, konnte ich nicht defintiv feststellen, daher mache ich das an dieser Stelle präventiv.

Dies realisiest Du durch eine Modifikation der run-Methode des BackgroundViews.

try {
  canvas = holder.lockCanvas(null);
  canvas.clipRect(0, 0, getWidth(), getHeight(), Region.Op.REPLACE);
        
  synchronized (bg) {
    canvas.drawBitmap(bg.getBitmap(), bg.getX(),0, null);
    canvas.drawBitmap(bg.getBitmap(), (bg.getX()+bg.getBitmap().getWidth()),0, null);

    if (getDeltaTime(startTime) > 0.1) {
      bg.update();
      startTime = System.nanoTime();
    }
  }
} finally { 
  if (canvas != null)
    holder.unlockCanvasAndPost(canvas);
}
Listing 11: Performance optimierung mit Clipping

Starte die App und fliege durch die Wolken. Deine App sollte jetzt in etwa so aussehen.

Android Game Tutorial final

Abbildung 3: Spiel am Ende des Tutorials

Gratulation, Du hast dieses Tutorial erfolgreich gemeistert. Im letzten Abschnitt findest Du noch einige Anregungen, wie Du das noch unfertige Spiel selbst erweitern kannst.

6 Fazit

Du hast also gelernt, wie Du einen sich bewegenden und reagierenden Hintergrund erzeugst. Einen Sprite darauf zeichnest, wie Du diesen manipulieren kannst und welche Möglichkeiten es gibt, dessen Steuerung zu verbessern.

7 Weitere Möglichkeiten

Wir haben in diesem Android Game Tutorial eine Ausgangsbasis für Actionspiele entwickelt. Von hier aus sind viele Erweiterungen und Verbesserungen möglich. Denkbar wäre es zum Beispiel gegnerische Flugzeuge einzuführen, welche auf das eigene Flugzeug schießen und damit kollidieren können. Es liegt nahe dem eigenen Flugzeug die gleichen Fähigkeiten zu geben. Möchte man den Schwierigkeitsgrad erhöhen, könnte man zusätzlich noch nicht zerstörbare Objekte einführen die in der Flugbahn des Flugzeuges auftauchen.

Du könntest auch eine kleine Wolke oberhalb des Flugzeuges platzieren, sodass das Flugzeug für kurze Zeit in der Wolke verschwindet.

Solche Hindernisse ermöglichen ebenfalls eine völlig andere Ausrichtung des Spieles. Geht es darum Hindernissen auszuweichen, könnte man auch ein Geschicklichkeitsspiel daraus machen, bei dem es darum geht den Kurs möglichst schnell zu absolvieren.

Das vollständige Projekt findest du hier.