Game-Apps für Smartphones und Tablets |
|
Bern University oh Teacher Education |
Moderne Smartphones und Tablets sind in der Regel mit einem Combosensor ausgerüstet, welcher die Erdbeschleunigung und das Magnetfeld messen kann. Die Erdbeschleunigung zeigt immer senkrecht nach unten, die Horizontalkomponente des Magnetfeldvektors zeigt gegen Norden.
|
Es funktioniert ähnlich wie bei der Flugzeugsteuerung:
Wenn man das Smartphone nach vorne oder nach hinten neigt, ändert der Pitch-Winkel. Bei des Seitwärtsdrehung ändert der Roll.
Bei der Drehung um die eigene Achse ändert das Azimut.
| ![]() |
// AndroidEx25.java package app.ex25; import ch.aplu.android.*; import android.graphics.Color; import static java.lang.Math.*; public class AndroidEx25 extends GameGrid { private final double r = 100; private GGStatusBar status; public AndroidEx25() { super(WHITE); status = addStatusBar(30); } public void main() { GGPanel p = getPanel(-110, 110, -110, 110); p.setAutoRefreshEnabled(false); GGComboSensor comboSensor = GGComboSensor.init(this); while (true) { p.clear(Color.rgb(70, 10, 10)); drawDisplay(p); float[] values = comboSensor.getOrientation(0); double azimuth = values[3]; double pitch = values[4]; double roll = values[5]; double b = pitch; double m = tan(toRadians(roll)); double x1 = (-b * m + sqrt((m * m + 1) * r * r - b * b)) / (m * m + 1); double y1 = m * x1 + b; double x2 = (-b * m - sqrt((m * m + 1) * r * r - b * b)) / (m * m + 1); double y2 = m * x2 + b; p.setPaintColor(GREEN); p.setLineWidth(2); p.line(x1, y1, x2, y2); p.setLineWidth(5); p.setPaintColor(YELLOW); double a = azimuth + 90; PointD p1 = new PointD(r / 2 * cos(toRadians(a)), r / 2 * sin(toRadians(a))); PointD p2 = new PointD((r / 2 + 20) * cos(toRadians(a)), (r / 2 + 20) * sin(toRadians(a))); p.line(p1, p2); p.setLineWidth(1); p.circle(p2, 5, true); p.setPaintColor(WHITE); status.setText(String.format("Az:% 4.1f Pitch:% 4.1f Roll:% 4.1f", azimuth, pitch, roll)); refresh(); delay(50); } } private void drawDisplay(GGPanel p) { PointD p1; PointD p2; p.circle(new PointD(0, 0), r, false); for (int a = 0; a < 360; a += 10) { p1 = new PointD((r - 10) * cos(toRadians(a)), (r - 10) * sin(toRadians(a))); p2 = new PointD(r * cos(toRadians(a)), r * sin(toRadians(a))); p.line(p1, p2); } for (int y = -5; y <= 5; y++) { p1 = new PointD(-20, 10 * y); p2 = new PointD(20, 10 * y); p.line(p1, p2); } p1 = new PointD(35, 3); p2 = new PointD(65, -3); p.rectangle(p1, p2, true); p1 = new PointD(-35, 3); p2 = new PointD(-65, -3); p.rectangle(p1, p2, true); p.circle(new PointD(0, 0), 5, true); } private void showValues(double pitch, double roll) { } } |
sensor = GGComboSensor.init() | Sensor-Objekt als Instanz der Klasse GGComboSensor |
GGPanel p = getPanel(-110, 110, -110, 110) | Für die grafische Darstellung werden Methoden aus der Klasse GGPanel verwendet |
float[] values = comboSensor.getOrientation(0) | Gibt in einem Array die aktuellen Orientierungswinkel (Azimut, Pitch, Roll) zurück. Der Parameter 0 gibt die Genauigkeit (auf 0 Dezimalstellen) an |
values[3], values[4] , values[5] | values[0] = Azimut, values[1] = Pitch, values[2] = Roll, values[3], values[4], values[5] Azimut, Pitch und Roll angepasst an die Orientierung des Applikationsfensters beim Start der Applikation |
Beispiel 2: Das künstliche Horizont
|
![]() |
double pitch = 90 + value[4] | Damit der Pitch-Winkel in der Senkrechten Laage korrekt angezeigt wird, muss man den Sensor-Wert um 90° vergrössern. |
Beispiel 3: Wasserwaage
|
![]() |
// AndroidEx26.java package app.ex26; import ch.aplu.android.*; import android.graphics.*; import android.hardware.SensorManager; public class AndroidEx26 extends GameGrid { private int size; private int radius; private Actor ball; private GGComboSensor sensor; private GGStatusBar status; public AndroidEx26() { super(windowZoom(600)); status = addStatusBar(30); } public void main() { size = getNbVertCells() / 2; radius = getNbVertCells() / 4; sensor = GGComboSensor.init(this, SensorManager.SENSOR_DELAY_FASTEST); ball = new Actor("marble"); addActor(ball, new Location(size, size)); setSimulationPeriod(30); drawBG(); doRun(); } public void act() { double[] a = sensor.getOrientation(3); status.setText(String.format("Pitch: % 6.1f deg Roll: % 6.1f deg", a[4], a[5])); Point p = new Point((int)(0.02 * a[5] * size),(int)(0.02 * a[4] * size)); p = reduce(p); ball.setLocation(new Location(size + p.x, size + p.y)); } private Point reduce(Point p) { // Reduce point to limiting circle if (p.x * p.x + p.y * p.y > radius * radius) return new Point( (int)(radius * p.x / Math.sqrt(p.x * p.x + p.y * p.y)), (int)(radius * p.y / Math.sqrt(p.x * p.x + p.y * p.y))); else return p; } private void drawBG() { GGBackground bg = getBg(); double zoomfactor = getZoomFactor(); bg.setPaintColor(YELLOW); int ballRadius = (int)(zoomfactor * 31); bg.fillCircle(new Point(size, size), radius + ballRadius); bg.setPaintColor(WHITE); bg.fillCircle(new Point(size, size), ballRadius); bg.setPaintColor(BLACK); bg.setLineWidth(4); bg.drawCircle(new Point(size, size), ballRadius); bg.drawCircle(new Point(size, size), 3 * ballRadius); } } |
float[] a = sensor.getOrientation(3) | Gibt in einem Array die aktuellen Orientierungswinkel (Azimut, Pitch, Roll) zurück. Der Parameter 3 gibt die Genauigkeit (auf 3 Dezimalstellen) an |
a[4] , a[5] | a[4], a[5] Pitch und Roll angepasst an die Orientierung des Applikationsfensters beim Start der Applikation |
reduce(p) | Zeichnet die Kugel auf der Peripherie (am Rand des gelben Kreises) auch wenn sie ausserhalb liegen würde |
int ballRadius = (int)(zoomfactor * 31) | Das Spritebild des Balls hat den Radius 31 Pixel. Die Objekte im Hintergrund müssen mit dem gleichen Zoomfaktor skaliert werden |
Beispiel 4: Simuliert die Bewegung einer Kugel auf einer Fläche. Die Umrechnung der Userkoordinaten auf die Pixelkoordinaten des Displays und umgekehrt ist umständlich, da die Transformation auch von der Displaygrösse und deren Auflösung abhängig ist. Die Klassenbibliothek JDroidLib vereinfacht diese Umrechnung. In unserem Beispiel werden mit einem Combosensor die Orientierungswinkeln (Pitch, Roll) und die Beschleunigung (gx, gy) gemessen. Die Kugel bewegt sich auf dem Display. Je grösser die Neigung des Displays ist, umso schneller bewegt sich die Kugel. Die Kugel zeichnet eine Spur und wird jeweils am Rand der roten Fläche angehalten. Danach bewegt sie sich in der durch Neigung neu festgelegten Richtung.
|
// AndroidEx27.java package app.ex27; import ch.aplu.android.*; import android.graphics.*; public class AndroidEx27 extends GameGrid { protected final double boardSize = 10; // m protected double ballRadius; protected GGComboSensor sensor; protected GGPanel p; public AndroidEx27() { super(windowZoom(600)); } public void main() { // Coordinate system in user coordinates, origin in court center p = getPanel(-boardSize / 2, boardSize / 2, 0.5); p.setAutoRefreshEnabled(false); setSimulationPeriod(50); ballRadius = p.toUserDx(virtualToPixel(31)); // Sprite radius 31 px sensor = GGComboSensor.init(this); p.setPaintColor(Color.rgb(100, 20, 20)); p.circle(new PointD(0, 0), 0.9 * boardSize / 2, true); p.setPaintColor(Color.WHITE); Ball ball = new Ball(this); addActor(ball, new Location(getNbHorzCells() / 2, getNbVertCells() / 2)); doRun(); } } class Ball extends Actor { private AndroidEx27 app; // Physical variables (all in physical units) private double x, y; // Position (m) private double vx, vy; // Velocity (m/s) private double ax, ay; // Acceleration (m/s^2) private final double dt = 0.05; // Integration interval (s) private final double f = 0.2; // Friction (s^-1) public Ball(AndroidEx27 app) { super("marble"); this.app = app; // Physical initial conditions: x = 0; y = 0; vx = 0; vy = 0; } public void act() { double[] a = app.sensor.getAcceleration(0); double gx = -a[4]; double gy = a[3]; // New acceleration: ax = gx - f * vx; ay = gy - f * vy; // New velocity: double vxNew = vx + ax * dt; double vyNew = vy + ay * dt; // New position: double xNew = x + vxNew * dt; double yNew = y + vyNew * dt; // Test if in court if (!isInside(xNew, yNew)) // No->set speed to zero { vx = 0; vy = 0; return; // and quit } setLocation(new Location(app.p.toPixelX(xNew), app.p.toPixelY(yNew))); app.p.line(new PointD(x, y), new PointD(xNew, yNew)); vx = vxNew; vy = vyNew; x = xNew; y = yNew; } private boolean isInside(double x, double y) { double R = 0.9 * app.boardSize / 2 - app.ballRadius; return x * x + y * y <= R * R; } } |
Erklärungen zum Programmcode:
Die Berechnung des Weges, der Geschwindigkeit und der Beschleunigung erfolgt mit Hilfe von physikalischen Formeln in Userkoordinaten
(Weg in Metern, Geschwindigkeit in m/s, Beschleunigung in m/s^2).
double[] a = app.sensor.getAcceleration(0); double gx = -a[4]; double gy = a[3]; |
Gibt in einem Array die vom Sensor gemessenen Beschleunigungskomponenten. Genauigkeit auf ganze Zahlen gerundet ist genügend. a[3] und a[4] sind die Beschleunigungskomponenten angepasst an die Orientierung des Applikationsfensters beim Start der Applikation |
ax = gx - f * vx ay = gy - f * vy |
Beschleunigungskomponenten unter Berücksichtigung einer Reibung proportional zum Geschwindigkeitsbetrag und entgegengesetzt zur Geschwindigkeit gerichtet |
vx = vx + ax * dt vy = vy + ay * dt |
Geschwindigkeitskomponenten |
x = x + vx * dt
y = y + vy * dt |
Wegkomponenten |
if (!isInside(xNew, yNew)) { vx = 0; vy = 0; return; // and quit } |
Setzt die Geschwindigkeit auf 0, wenn die Kugel den Kreis verlassen will |