21.06 Eine Physikengine in Java ME

Computerspiele wie „World of Goo“, „Fantastic Contraption“ oder Vertreter der beliebten Ragdoll-Spielprinzipien, in denen physikalische Gesetze eine wichtige Rolle spielen, werden im Bereich der Casual Games und somit auch für Programmierer von mobilen Spielen immer bedeutender. Der Vorteil dieser Spiele ist, dass, wenn die Physikengine erst einmal besteht, einzelne Level relativ schnell generiert werden können, in dem einzelne Objekt einfach in den Level gesetzt werden können und dem Spieler überlassen wird, wie er sein Ziel erreichen möchte.

Ziele gibt es dabei zahlreiche: Oft geht es darum, einen bestimmten Gegenstand in ein Ziel zu befördern, sei es durch mechanische Aufbauten (Katapulte, Rampen, Förderbänder oder Triebwagen), die man mit den bestehenden Objekten bauen kann. Andere Ziele könnten das Erreichen einer Balance („Topple“) oder Treffen eines Gegners oder Zieles („Worms“, Sportspiele, Billard- oder Flipperprinzip) sein.

Wie man sieht, kommen moderne Spiele kaum noch ohne einen physikalisch korrekten Ansatz aus. Viele Programmierer scheuen aber die Einarbeitung in die Materie, dabei sind die mathematischen Grundvoraussetzungen teilweise sehr gering. Am einfachsten gelingt die Einarbeitung in die Materie an Hand der Bewegungen eines Balls, mit dem wir uns in den folgenden Beispielen beschäftigen.

Physikalische Eigenschaften eines Balls

Ein Ball hat den Vorteil, dass er keine Unebenheiten besitzt und an jeder Stelle gleich regiert (programmiertechnisch also keine Besonderheiten besitzt). Lässt man bspw. einen Gummiball senkrecht auf eine Waagerechte fallen, prallt er dort ab und fliegt auf identischer Bahn in Richtung Ursprungsquelle zurück. Für einen solchen Ball legen wir 5 Grundvariablen an:

  • x = momentane x-Koordinate des Ballmittelpunkts
  • y = momentane y-Koordinate des Ballmittelpunkts
  • r = Radius des Balls
  • xR = x-Richtung, in der sich der Ball bewegt
  • yR = y-Richtung, in der sich der Ball bewegt

Der Einfachheit halber, um den Code innerhalb einer Datei unterzukriegen, werden die folgenden Beispiele innerhalb einfacher Variablen statt objektorientierten Konstrukten verwaltet. Um gleich Praxisnähe zu kriegen, bauen wir uns erst einmal ein J2ME-Grundgerüst Physicsengine.java, um die Variablen zu initialisieren.

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.*;
import java.lang.Math.*;


public class Physicsengine extends MIDlet {

  private Display display;
  private MyCanvas canvas;

  public Physicsengine() {
    display = Display.getDisplay(this);
    canvas = new MyCanvas(this);
  }

  protected void startApp() {
    display.setCurrent(canvas);
  }

  protected void pauseApp() {}

  protected void destroyApp(boolean unconditional) {}

  public void exitMIDlet() {
    destroyApp(true);
    notifyDestroyed();
  }
}

class MyCanvas extends Canvas implements CommandListener {

  private Command commandExit;
  private Physicsengine main;
  private int p = 0; // Hilfsvariable p (fuer Zaehler)
  private int Xmax = 0, Ymax = getHeight();
  private int x = 0; // x-Koordinate
  private int y = 0; // Y-Koordinate
  private int r = 0; // Radius
  private int xR = 0; // X-Richtung
  private int yR = 0; // Y-Richtung

  public MyCanvas(Physicsengine main) {
    this.main = main;
    setFullScreenMode(true);
    // Bildschirmmasse speichern
    Xmax = getWidth();
    Ymax = getHeight();
    x = Xmax / 2; // Ball auf Bildschirmmitte
    y = 50;
    r = 10;
    xR = 0;
    yR = 1;
  }

  protected void paint(Graphics g) {}

  public void commandAction(Command c, Displayable d) {}

  protected void keyPressed(int code) {
    switch (getGameAction(code)) {
      default:
        main.exitMIDlet();
    }
  }
}

Natürlich wollen wir den Ball auch sehen, weswegen wir ihn nun – der Einfachheit halber – direkt in der paint()-Methode zeichnen:

protected void paint(Graphics g) {
  
  // Hintergrund loeschen
  g.setColor(0, 0, 0);
  g.fillRect(0, 0, this.Xmax, this.Ymax);
  // Ball zeichnen
  g.setColor(255, 0, 0);
  g.fillArc((this.x - this.r), (this.y - this.r), 2 * this.r, 2 * this.r, 0, 360);
  repaint();
}

Nun wollen wir Bewegung in die ganze Sache bringen:

Wir lassen zu Beginn den Ball aus 50 Pixeln Höhe auf eine waagerechte Ebene fallen: >y = 50 Zu diesem Zeitpunkt erfährt er in x-Richtung noch keine Beschleunigung: yR = 0 Das heißt, der Ball befindet sich zu Beginn der Bewegung mit y + yR = 50 + 0 = 50 noch immer auf der y-Koordinate 50. Da die Schwerkraft aber dafür sorgen wird, dass sich der Ball in Richtung Erde bewegt, erhöhen wir nun im nächsten Animationsschritt yR um einen Zähler yR = 1. Der Ball befindet sich also nun auf y = 50 + 1 = 51.

Belassen wir den Zähler yR auf diesem Wert, haben wir, wenn wir den Ball in einer Schleife fortbewegen, eine gleichmäßige Bewegung zum Boden.

Ball fällt

protected void paint(Graphics g) {
  
  long started = System.currentTimeMillis(); // Timer initialisieren
  // Hintergrund loeschen
  g.setColor(0, 0, 0);
  g.fillRect(0, 0, Xmax, Ymax);
  // Ball bewegen
  y += yR;
  // Ball zeichnen
  g.setColor(255, 0, 0);
  g.fillArc((x - r), (y - r), 2 * r, 2 * r, 0, 360);
  // Um den Code möglichst kompakt zu halten, wird hier eine Warteschleife
  // verwendet. Selbstverständlich kann die Animation auch über ein anderes
  // Konstrukt realisiert werden.
  while (System.currentTimeMillis() - started < 25) {}
  repaint();
}

Da die Erdbeschleunigung nun aber ständig zunimmt, müssen wir pro Animationsschritt auch die y-Richtung yR erhöhen.

protected void paint(Graphics g) {
  
  long started = System.currentTimeMillis();
  // Hintergrund loeschen
  g.setColor(0, 0, 0);
  g.fillRect(0, 0, Xmax, Ymax);
  // Erdbeschleunigung
  yR++;
  // Ball bewegen
  y += yR;
  // Ball zeichnen
  g.setColor(255, 0, 0);
  g.fillArc((x - r), (y - r), 2 * r, 2 * r, 0, 360);
  while (System.currentTimeMillis() - started < 25) {}
  repaint();
}

Ball fällt realistisch

Nun bewegt sich der Ball schon relativ realitätsnah. Doch wenn der Ball am unteren Bildschirmende ankommt, fällt er einfach durch. Dem wirken wir entgegen, in dem wir abfragen, ob sich der Ball oberhalb des unteren Bildschirmrands befindet. Da unser Ball jadurch seinen Mittelpunkt definiert ist, aber einen Radius hat, muss zum Abfragen der Position natürlich auch der Radius addiert werden: y + r.

Um nun den Abprall realitätsnah zu entwickeln, müssen wir uns nun wieder mit etwas Physik befassen. Gemäß dem Energieerhaltungssatz und dem für einen Ball auf Ebene treffenden Prinzip, nachdem der Einfallswinkel mit dem Ausfallswinkel identisch ist, müssen wir bei Aufprall nun einfach die y-Richtung yR, die ja auch gleichzeitig die Geschwindigkeit anzeigt, negieren. Dazu ergänzen wir die folgenden Zeilen:

// Erdbeschleunigung
yR++;
// Ball prallt am Boden ab
if (y+r > Ymax) {
  yR = -yR;
}
// Ball bewegen
y += yR;

Ab jetzt prallt der Ball schön am Boden ab, ja beschleunigt sogar und springt immer höher. Das ist natürlich nicht besonders realistisch, es sei denn, der Ball würde vom Boden einen Energieimpuls erhalten, der ihn noch höher springen lässt. Je nach Material ist es in der Physik aber so, dass immer ein bisschen Bewegungsenergie verpufft, sei es durch die Festigkeit des Materials, durch Reibungskräfte etc.. Deswegen bleiben wir zwar beim Prinzip, dass zwar ein Vorzeichen vor der y-Richtung genügt, gleichzeitig reduzieren wir aber den Wert. Als halbwegs realistisch hat sich eine Zweidrittelung der Aufprallgeschwindigkeit als ein gut funktionierender Wert erwiesen:

// Ball prallt am Boden ab
if (y+r > Ymax) {
  yR = 2*(-yR)/3;
}

Nun springt der Ball in etwa wie ein Fußball auf Hartplatz, bei einem Wert auf 4/5 entspricht das Sprungverhalten in etwa einem Tischtennisball, bei ½ dem eines Medizinballs, bei 1/5 einer Boccia-Kugel. Nehmen Sie sich ruhig die Zeit, mit den Werten ein bisschen zu experimentieren.

Nun möchte wir den Ball nicht einfach nur Fallen lassen, sondern ihn quer durch den 2DRaum werfen. Dazu müssen wir nun die x– und xR-Koordinaten bearbeiten, indem der x-Richtungswert xR mit dem Wert 1 initialisiert wird und in der Methode paint der Klasse MyCanvas folgende Zeile ergänzt wird:

// Ball bewegen
y += yR;
x += xR;

Die Zeilen sollten sich durch die Erfahrung mit den y-Werten von selbst erklären. Nun springt oder rollt unser Ball rechts aus dem Screen. Also müssen wir auch hier die räumlichen Grenzen setzen und den Ball gegebenenfalls abprallen lassen:

// Ball prallt am Boden ab
if (y+r > Ymax) {
  yR = 4*(-yR)/5;
}
// Ball prallt an Wand ab
if (x+r > Xmax) {
  xR = -xR;
}
if (x-r < 0) {
  xR = -xR;
}
// Ball bewegen
y += yR;
x += xR;

Und weil der Ball ja auch Luftwiderstand erfährt, muss die xR-Variable kontinuierlich reduziert werden. Hier stellt sich nun das Problem, dass die meisten bisher verwendeten Funktionen nur mit Integer-Werten arbeiten, wir für die kontinuierliche Abnahme der x-Geschwindigkeit aber Kommawerte benötigen. Dazu bedienen wir uns eines kleinen Tricks: Wir multiplizieren jede x-Zahl mit dem Faktor 100 und teilen diese bei der Darstellung wieder durch 100. Dadurch erreichen wir zwei Nachkommastellen, die für einen ersten Ansatz einer Physikengine genügen und zudem auch von MIDP 1.0 verstanden werden:

public MyCanvas (Physicsengine main) {

  // ...

  x = 100*Xmax/2; // Ball auf Bildschirmmitte
  y = 50;
  r = 10;
  xR = 500;
  yR = 1;

  // ...
}

protected void paint (Graphics g) {

  // ...
  // Ball prallt an Wand ab
  if (x/100+r > Xmax) {
    xR = -xR;
  }
  if (x/100-r < 0) {
    xR = -xR;
  }
  // Luftwiderstand
  xR = (100*xR)/101;
  if (xR > 0) {
    xR -=1;
  }
  if (xR < 0) {
    xR +=1;
  }
  // Ball bewegen
  y += yR;
  x += xR;
  // Ball zeichnen
  g.setColor(255, 0, 0);
  g.fillArc( (x/100-r), (y-r), 2*r, 2*r, 0, 360);
  while (System.currentTimeMillis() - started < 25) {} // Warteschleife
  repaint();
}

Der Luftwiderstand nimmt in diesem Beispiel kontinuierlich um 100/101-tel ab. Da dadurch aber nie der Wert Null erreicht werden kann, ziehen wir in jedem Schritt noch einen Zähler ab (bei negativer Bewegungsrichtung wird der Zähler natürlich addiert).

Nun haben wir eine physikalisch logische Wurfbewegung erreicht, die mit senkrechten Wänden und waagerechten Böden reagieren kann. Um den Ball nun auch auf andere Gegenstände logisch reagieren zu lassen, generieren wir im Folgenden 3 Bälle, in dem wir die bisherigen Variablen ganz altmodisch zu Arrays umfunktionieren (hätten wir die bisherigen Variablen objektorientiert als Eigenschaften des Objekts Ball deklariert, müssten wir einfach das Objekt dreimal instanzieren):

class MyCanvas extends Canvas implements CommandListener {

  // ...

  private int[] x = new int[3]; // x-Koordinate
  private int[] y = new int[3]; // Y-Koordinate
  private int[] r = new int[3]; // Radius
  private int[] xR = new int[3]; // X-Richtung
  private int[] yR = new int[3]; // Y-Richtung
  
  public MyCanvas (Physicsengine main) {

    this.main = main;
    setFullScreenMode(true);
    // Bildschirmmasse speichern
    Xmax=getWidth();
    Ymax=getHeight();
    for (p=0;p<3;p++) {
      x[p] = 100*(Xmax/2+p*10); // Baelle verteilen
      y[p] = 50;
      r[p] = 10;
      xR[p] = 250*p; // Geschwindigkeit variieren
      yR[p] = 1;
    }
  }

  protected void paint (Graphics g) {

    // ...

    for (p=0;p<3;p++) {
      // Erdbeschleunigung
      yR[p]++;
      // Ball prallt am Boden ab
      if (y[p]+r[p] > Ymax) {
        yR[p] = 4*(-yR[p])/5;
      }
      // Ball prallt an Wand ab
      if (x[p]/100+r[p] > Xmax) {
        xR[p] = -xR[p];
      }
      if (x[p]/100-r[p] < 0) {
        xR[p] = -xR[p];
      }
      // Luftwiderstand
      xR[p] = (100*xR[p])/101;
      if (xR[p] > 0) {
        xR[p]--;
      }
      if (xR[p] < 0) {
        xR[p] ++;
      }
      // Ball bewegen
      y[p] += yR[p];
      x[p] += xR[p];
      // Ball zeichnen
      g.setColor(255, 0, 0);
      g.fillArc( (x[p]/100-r[p]), (y[p]-r[p]), 2*r[p], 2*r[p],0,360);
    }
  // ...
  }
}

Nun haben wir 3 Bälle geschaffen, die allerdings parallel zueinander springen, also keinen wechselseitigen Einfluss auf sich einüben. Nun wäre es schön, wenn die Bälle also auch aneinander abprallen könnten. Dazu schauen wir uns die physikalischen und mathematischen Begebenheiten genauer an.

Lesen Sie auf der nächsten Seite, wie Sie zwei Bälle physikalisch korrekt kollidieren lassen können und lernen Sie weitere Einsatzorte der Physikengine kennen.

Schreibe einen Kommentar