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.
Abbildung 1:
- 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();
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; …
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; }
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; }
Das Ergebnis sollte in etwa so aussehen:
Abbildung 2:
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); }
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); }
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
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>
Du ergänzt diese um die folgenden Einträge:
<dimen name="labelPadding">10dp</dimen> <dimen name="speedFontSize">30sp</dimen>
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);
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);
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); }
Starte die App und fliege durch die Wolken. Deine App sollte jetzt in etwa so aussehen.
Abbildung 3:
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.