Android Game Tutorial Teil 1: Background Scrolling und Double Buffering

Im ersten Teil des Android Spiele Tutorials animierst Du den Hintergrund und erfährst wie Du lästiges Flackern verhinderst.

First App Test

Abbildung 1: Der erste Testlauf

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

Voraussetzungen

Bevor Du mit dem Tutorial beginnen kannst benötigst Du folgendes:

Es empfiehlt sich ein Testgerät anstelle eines Emulators zu nutzen, da dieser die Lagesensoren nicht unterstützt und erheblich langsamer ist als ein echtes Android Gerät. Die Lagesensoren lassen sich zum Android Emulator hinzufüguen, wie genau das funktioniert, erfährst Du hier.

Entwickelt haben wir das Tutorial mit Eclipse Juno Service Release 2, Android SDK Tools 22.0.5, Java 1.7.0_21 und Android API lvl 18. Mit anderen Versionen sollten die Beispiele aber auch funktionieren.

0. Erste Schritte

Ein vorbereitetes Projekt kannst Du unter game-tutorial.zip herunterladen. Das Projekt enthält Java-Dateien, die als Ausgangspunkt für die Übungen im Tutorial dienen sowie die Grafiken für das Flugzeug und den Hintergrund.

Führe nach dem Download die folgenden Schritte aus, um das Projekt nach Eclipse zu importieren:

  1. Entpacke das Archiv
  2. Klicke in der Menüleiste auf File
  3. Dann Import
  4. Nun Android > Import Existing Android Code into Workspace
  5. Wähle das entpackte Verzeichnis und bestätige deine Auswahl mit finish

Nach dem Import sollte der Eclipse Package Explorer folgendes zeigen:

Eclipse Tutorial Import

Abbildung 2: Eclipse nach Import

Bevor Du den Code betrachtest übersetze die Quellen und führe die App aus, um zu testen, ob die Entwicklungsumgebung richtig eingerichtet ist.

  1. Vergewissere dich, dass Dein Gerät erkannt wird
  2. Öffne die Main-Klasse des Projekts im Package src/com.predic8.tutorials.activities
  3. Klicke in Eclipse auf Run
  4. Wähle die Option Run as Android Application
  5. Wähle dein Gerät aus

In wenigen Sekunden sollte die App auf Deinem Handy starten und den Hintergrund wie in Abbildung 3 anzeigen. Eclipse wird beim ersten Start einer Android-App fragen, ob es die Errors in der Android eigenen LogCat anzeigen soll. Diese ersetzt für das Android Debugging, die aus Eclipse bekannte Konsole. Bestätige den Dialog mit Ok und die LogCat öffnet sich über der Konsole.

First App Test

Abbildung 3: Der Hintergrund in der App

Wie genau die Wolken als Hintergrund angezeigt werden erfährst Du in den folgenden Abschnitten.

1. Anzeigen des Hintergrundes auf einem SurfaceView

Der erste Stand der App zeigt zunächst nur den statischen Hintergrund mit Wolken. Im Projekt findest Du bereits einige vorbereitete Klassen wie z.B. die Klassen Main und Background. Den für das Spiel notwendigen Code wirst Du dann im Laufe des Tutorials Schritt für Schritt ergänzen.

Schau Dir als erstes die Klasse Main in Package com.predic8.tutorial.activities an, sie übernimmt als Activity die Verwaltung der verwendeten Views.

1.1 Wie entsteht das Hintergrundbild?

Die App zeigt bereits beim Start das Hintergrundbild in der Main Activity. Dazu erzeugt die Main Activity bei ihrer Initialisierung einen BackgroundView und zeigt diesen an.

        @Override
        protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);

          bgv = new BackgroundView(this);

          setContentView(bgv);
        }
      
Listing 1: onCreate Main-Klasse

Während die Activity ihren Lifecycle durchläuft, reicht sie die einzelnen Phasen an den BackgroundView weiter (onResume, onPause).

  @Override
  protected void onResume() {
    super.onResume();
    bgv.resume();
  }
  
  @Override
  protected void onPause() {
    super.onPause();
    bgv.pause();
  }
      
Listing 2: onResume and onPause Main-Klasse

Wichtig ist, dass der BackgroundView das Interface Runnable implementiert, um das Zeichnen des Hintergrundes nicht auf dem UI-Thread erledigen zu müssen. Der Thread, der den Hintergrund zeichnet wird in der onResume-Methode des BackgroundViews erzeugt und gestartet.

  public void resume() {
    running = true;
    renderThread = new Thread(this);
    renderThread.start();
  }
      
Listing 3: resume BackgroundView

Aufgerufen wird onResume, sowie onPause aus der Main-Klasse heraus an den entsprechenden Stellen des Lifecycles. Die run-Methode, wird nun durch den im Hintergrund laufenden RenderThread ausgeführt.

Dieser Thread durchläuft eine Schleife, die den Hintergrund immer wieder neu zeichnet. Dies geschieht so lange, bis die Activity zu der dieser View gehört pausiert wird.

  public void run() {
    while (running) {
      if (!holder.getSurface().isValid())
        continue;
      Canvas canvas = null;
      try {
        canvas = holder.lockCanvas(null);

        synchronized (bg) {
          canvas.drawBitmap(bg.getBitmap(), bg.getX(),0, null);
          
        }
      } finally { 
        if (canvas != null)
          holder.unlockCanvasAndPost(canvas);
      }
    }
  }
      
Listing 4: run BackgroundView

Dass die Activity in onPause übergegangen ist, wird dem Thread durch den boolean isRunning mitgeteilt. isRunning wird von zwei verschiedenen Threads verwendet, daher sollte isRunning als volatile deklariert werden.

1.2 Zeichnen im Hintergrund

Normalerweise reicht ein gewöhnlicher View um etwas auf dem Bildschirm darzustellen. Ein solcher View läuft aber immer auf dem UI-Thread. Würde der UI-Thread im View stark beschäftigt, so kommt er nicht mehr dazu auf Benutzereingaben zu reagieren. Dies wirkt wie ein Hängen der App, daher sollte der UI-Thread immer so kurz wie möglich verwendet werden. Im Falle unseres Spiels befindet sich der rendernde Thread in einer Endlosschleife, wir würden den UI-Thread also dauerhaft blockieren.

Der SurfaceView erlaubt es das Zeichnen in einen Thread auszulagern. Durch diese Nebenläufigkeit ermöglichen wir dem UI-Thread eine schnelle Reaktion auf Benutzereingaben, während ein anderer Thread im Hintergrund die Arbeit erledigt.

1.3 Surface View und DoubleBuffering

In der App übernimmt der SurfaceView die Aufgabe den Hintergrund kontinuierlich neu zu zeichnen. Dabei möchte man verhindern, dass der Eindruck eines Ruckelns entsteht. Ein solcher Ruckler ensteht, wenn das aktuelle Bild durch ein neues ersetzt werden soll, dieses neue Bild aber noch nicht bereit ist gezeichnet zu werden. Um dies zu verhindern, zeichenst Du auf zwei verschiedenen Rahmen, die du ständig gegeneinander austauschst. Das stellt sicher, dass es immer ein zweites Bild gibt, welches die App anzeigen kann.

2. Hintergrund in Bewegung versetzen

Um die Illusion einer Bewegung zu erzeugen versetzen wir den Hintergrund in Bewegung.

Die Geschwindigkeit mit der der Hintergrund bewegt wird ist in der Background-Klasse hinterlegt. Bei einem Update auf den Hintergrund, wird die aktuelle Geschwindigkeit zur x-Koordinate addiert. Dies geschieht wie auch das Neuzeichnen des Hintergrundbildes im BackgroundView.

Realisiere die Animation, indem Du den BackgroundView um die markierten Zeilen erweiterst.

public void run() {
	long startTime = System.nanoTime();
	while (running) {
	  if (!holder.getSurface().isValid())
		  continue;
		  Canvas canvas = null;
	  try {
		  canvas = holder.lockCanvas(null);
			  
		  synchronized (bg) {
			  canvas.drawBitmap(bg.getBitmap(), bg.getX(),0, null);
  
			  if (getDeltaTime(startTime) > 0.1) {
				  bg.update();
				  startTime = System.nanoTime();
			  }
		  }
	  } finally { 
		  if (canvas != null)
			  holder.unlockCanvasAndPost(canvas);
		  }
	}
}

private float getDeltaTime(long startTime) {
  return (System.nanoTime() - startTime) / 100000000f;
}
      
Listing 5: Animation des Hintergrunds

In Zeile 13-16 führen wir alle 100ms ein Update des Hintergrundes aus. Das heißt wir verändern die aktuelle x-Koordinate des Bildes auf dem View. Führst Du das werdende Spiel jetzt aus, so wird sich nach kurzer Zeit ein Muster wie in Abbildung 4 zeigen.

cloudGoo

Abbildung 4: Schlierenbildung

Der sich bewegende Hintergrund hinterlässt Schlieren und das Bild flackert an den Rändern.

2.1 Flackern und Schlieren beseitigen

Das Flackern und die Schlieren kommen daher, dass das Bild mit den Wolken weiter bewegt wird ohne dass neues Material für den nun ungenutzten Bereich vorliegt und der komplette Bildschirm nie gesäubert wird.

Das Entstehen von Schlieren oder einem Flackern an den Rändern verhinderst Du dadurch, dass Du den am rechten Bildrand frei werdenden Platz mit einem zweiten, identischen Hintergrundbild füllst. Das Hintergrundbild sollte am rechten und linken Rand identisch sein, da sonst ein unschöner Übergang sichtbar wäre.

Erweitere die run-Methode, so dass sie das Hintergrundbild ein weiteres Mal um die Breite des Bildes nach rechts versetzt zeichnet.

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();
	}
}
      
Listing 6: Anzeige von zwei Hintergrundbildern

Führst Du die App nun erneut aus, so wirkt es als zöge ein endloser Wolkenhimmel durch das Display. Bei genauerer Betrachtung des Hintergrundes, lässt sich schnell ein Ruckeln feststellen. Im nächsten Abschnitt lernst Du wie Du dieses Problem behebst.

2.2 Optimierung des Zeichnens

Der BackgroundView zeichnet das Bild nun zweimal, unabhängig davon ob das zweite Bild im Sichtbereich ist. Um die Performance zu verbessern testen wir vor jeder Zeichenoperation ob sich das zu zeichnende Bild im Sichtbereich befindet.

synchronized (bg) {
  if (bg.getX()+bg.getBitmap().getWidth() >= 0 || (bg.getX() >= 0 && bg.getX() <= getWidth()))
     canvas.drawBitmap(bg.getBitmap(), bg.getX(),0, null);
  if (bg.getX()+bg.getBitmap().getWidth()+bg.getBitmap().getWidth() >= 0 || (bg.getX()+bg.getBitmap().getWidth() >= 0 && bg.getX() <= getWidth()))
     canvas.drawBitmap(bg.getBitmap(), (bg.getX()+bg.getBitmap().getWidth()),0, null);

  if (getDeltaTime(startTime) > 0.1) {
    bg.update();
    startTime = System.nanoTime();
  }
}
      
Listing 7: Optimierung des Zeichenprozesses

Zusammenfassung

In diesem Abschnitt des Tutorials hast Du gelernt, wie Du ein Bild mittels SurfaceView anzeigen kannst, und dieses anschließend in Bewergung versetzt und die dabei entstehenden Probleme löst, sowie die Performance verbesserst. Weiter gehts mit Part 2.