Game-Apps für Smartphones und Tablets

Bern University oh Teacher Education  
HomeOnline-Editor startenDruckenAndroid-TurtlegrafikJava-Online

Lagesensoren


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.
Daraus werden die folgenden Winkel berechnet:

Azimut: Kompassrichtung bezüglich Norden. Gegen Osten positiv
Pitch: Vorwärtsneigung in Grad
Roll: Seitwärtsneigung in Grad
 

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.



Beispiel 1
: Sensorwerte anzeigen
Die aktuellen Orientierungswinkel Azimut, Pitch und Roll werden grafisch und in der Statusbar angezeigt. Neigt man das Smartphone nach vorne oder nach hinten, so bewegt sich die grüne Anzeige nach oben oder nach unten. Bei einer Seitwärtsbewegung wird der Roll als Drehwinkel angezeigt. Das Azimut wird mit dem gelben Zeiger. Ist das Smartphone gegen Norden gedreht, ist das Azimut 0.
In diesem Beispiel sind die Orientierungswinke Pitch und Roll gleich 0, wenn das Smartphone waagrecht liegt. Im Beispiel 2 findet man die Sensorwerte-Anzeige für eine senkrechte Smartphone-Stellung, was sich ähnlich wie ein künstlicher Horizont bei der Flugzeugsteuerung präsentiert.

Beispiel im Online-Editor bearbeiten

App installieren auf Smartphone oder Tablet

QR-Code

Sources downloaden (AndroidEx25.zip)

 

// 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 = 100;
  private GGStatusBar status;

  public AndroidEx25()
  {
    super(WHITE);
    status = addStatusBar(30);
  }

  public void main()
  {
    GGPanel = 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 = pitch;
      double = tan(toRadians(roll));
      
      double x1 = (-b * + sqrt((m * + 1) * * - * b)) / (m * + 1);
      double y1 = * x1 + b;
      double x2 = (-b * - sqrt((m * + 1) * * - * b)) / (m * + 1);
      double y2 = * x2 + b;
      p.setPaintColor(GREEN);
      p.setLineWidth(2);
      p.line(x1, y1, x2, y2);
   
      p.setLineWidth(5);
      p.setPaintColor(YELLOW);
      double = azimuth + 90;
      PointD p1 = new PointD(/ * cos(toRadians(a)), r / 2 * sin(toRadians(a)));
      PointD p2 = new PointD((r / + 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 = 0; a < 360; a += 10)
    {
      p1 = new PointD((r - 10) * cos(toRadians(a)), (r - 10) * sin(toRadians(a)));
      p2 = new PointD(* cos(toRadians(a)), r * sin(toRadians(a)));
      p.line(p1, p2);
    }
    
    for (int = -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)
  {
   
  }
}

Erklärungen zum Programmcode:
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
Im Unterschied zum Beispiel 1, wird hie das Smartphone bzw. Tablet in der Grundstellung senkrecht gehalten, so wie es bei den Instrumenten in einem Flugzeug üblich ist. Gemäss der Abbildung rechts würde sich das Flugzeug im Sinkflug mit Pitch 29° in einer Rechtskurve mit Roll 27° bewegen.

Beispiel im Online-Editor bearbeiten

App installieren auf Smartphone oder Tablet

QR-Code

Sources downloaden (AndroidEx25a.zip)

 

Erklärungen zum Programmcode:
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
In diesem Beispiel werden nur Pitch und Roll verwendet. Wenn sich die grüne Kugel in der Mitte des Displays im weissen Kreis befindet, ist das Smartphone waagrecht. Neigt man das Display nach vorne, nach hinten oder seitwärts, so bewegt sich die Kugel in die gegengesetzte Richtung. Das Smartphone funktioniert daher wie eine einfache Wasserwaage. Die Winkel werden in der Statusbar auf drei Dezimalstellen genau ausgeschrieben. Drehwinkel 0 in beiden Richtungen ergibt eine waagrechte Position.

Beispiel im Online-Editor bearbeiten

App installieren auf Smartphone oder Tablet

QR-Code

Sources downloaden (AndroidEx26.zip)

 

// 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 = new Point((int)(0.02 * a[5] * size),(int)(0.02 * a[4] * size));
    p = reduce(p); 
    ball.setLocation(new Location(size + p.xsize + 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);
  }  
}

Erklärungen zum Programmcode:
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.
Bei der Simulation einer Bewegung empfiehlt es sich, ein Userkoordinatensystem einzuführen und die Standardeinheiten (Meter, Sekunden) zu verwenden. Dadurch können die physikalischen Formeln unverändert benutzt werden.

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.
getPanel(ymin, ymax, xratio) legt ein Userkoordinatensystem mit dooble-Koordinaten fest, welches der Realität entspricht. Zur Festlegung des Koordinatensystems wird der minimaler und maximaler y-Wert sowie das Verhältnis xratio verwendet (siehe Skizze). Mit den Methoden toPixelX(), toPixelY() bzw. toUserX(), toUserY() aus der Klasse GGPanel werden die Userkoordinaten in die Pixelkoordinaten und umgekehrt umgerechnet.

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.

Beispiel im Online-Editor bearbeiten

App installieren auf Smartphone oder Tablet

QR-Code

Sources downloaden (AndroidEx27.zip)

 


// 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
    = 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 = 0.2; // Friction (s^-1)

  public Ball(AndroidEx27 app)
  {
    super("marble");
    this.app = app;

    // Physical initial conditions:
    = 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 - * vx;
    ay = gy - * vy;

    // New velocity:
    double vxNew = vx + ax * dt;
    double vyNew = vy + ay * dt;

    // New position:
    double xNew = + vxNew * dt;
    double yNew = + 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 = 0.9 * app.boardSize / - app.ballRadius;
    return * + * <= * 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