JavaScript-Tutorium:Physics

aus GlossarWiki, der Glossar-Datenbank der Fachhochschule Augsburg
Version vom 22. Oktober 2015, 22:48 Uhr von Lawrence (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

Physics

Unter Physics versteht man in der Informatik die Simulation physikalischer Gesetze wie Geschwindigkeit und Erdanziehung. In der Spieleprogrammierung werden Physics verwendet um Spielmechaniken natürlicher wirken zu lassen.

Dieses Tutorium erklärt simple Formeln und Berechnungen für die Entwicklung von Simulationen in JavaScript.


Allgemeines

Zugehörigkeit

Bei einer Modellierung nach Model-View-Controller stellt sich die Frage wo der Code für physikalische Simulationen hingehört. Versteht man das Model so, dass es neben den Daten auch die Logik enthält, so schliesst dies den Physics-Code eindeutig ein. Die Gegebenheit, dass ein Spiel physikalischen Gesetzen folgt, kann also wie jede andere Regel des Spiels verstanden werden.


Abstrakte Einheiten

Gegebenenfalls ist es sinnvoll ein Koordinatensystem einzuführen und Spielelemente mit Dimensionen zu versehen.

var Player = function(x, y, width, height) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
};

Wichtig ist dabei, dass diese Einheiten keinen direkten Bezug zu Pixel haben.


Sollen das Model und die View mit unterschiedlichen Zahlensystem arbeiten, kann ein Skalierungsfaktor verwendet werden.

var renderPlayer = function(player, scaleFactor) {
  var div = document.createElement('div');
  div.style.position = 'absolute';
  div.style.backgroundColor = '#F00';
  div.style.width = player.width * scaleFactor + 'px';
  div.style.height = player.height * scaleFactor + 'px';
  div.style.left = player.x * scaleFactor  + 'px';
  div.style.top = player.y * scaleFactor+ 'px';
  document.querySelector('body').appendChild(div);
};

var player1 = new Player(10, 10, 10, 10);
renderPlayer(player1, 5);


Selbst Spiele wie "Tic Tac Toe" arbeiten mit Koordinatensystemen, oftmals vereinfacht als multidimensionales Array dargestellt.

var TicTacToe = function() {
  this._spielfeld = [
    ['', '', ''],
    ['', '', ''],
    ['', '', '']
  ];
};


Welt und Elemente

Innerhalb einer Physics-Simulation ist es sinnvoll das Modell in eine Welt und deren enthaltenen Elemente aufzuteilen.

var Element = function(x, y, width, height) {
  this.x = x;
  this.y = y;
  this.width = width;
  this.height = height;
};

Dabei kapseln Elemente ihre physikalischen Eigenschaften, während die Welt regelmäßig ihre Berechnungen auf diese anwendet.


Die Berechnungen werden mithilfe von eigenständigen Timern durchgeführt, ähnlich wie beim Rendering.

var World = function(elements) {
  var self = this;
  self._elements = elements;

  self._updateElements = function() {
    for (var i = 0; i < self._elements.length; i++) {
      // TODO: Your code goes here
    }
  };

  self._updateElementsRepeatedly = function() {
    self._updateElements();
    setTimeout(self._updateElementsRepeatedly, 15);
  };
  self._updateElementsRepeatedly();
};


Ortsänderungen

Nahezu jedes Spiel, welches eine physikalischen Simulation beinhaltet, arbeitet mit der Veränderung vom Ort einzelner Elemente.


Geschwindigkeit

Eine simple Simulation von Geschwindigkeit kann bereits erreicht werden indem man die Koordinaten von Objekten verändert.

self._updateElements = function() {
  for (var i = 0; i < self._objects.length; i++) {
    self._objects[i].x += 1;
    self._objects[i].y += 1;
  }
};


Mithilfe von Geschwindigkeitsattributen können Objekte selbst entscheiden wie sich ihre Koordinaten verändern.

var Element = function(x, y) {
  this.x = x;
  this.y = y;
  this.vx = 0;
  this.vy = 0;
}

var element = new Element(0, 0);
element.vx = 1;
element.vy = 1;
self._updateElements = function() {
  for (var i = 0; i < self._elements.length; i++) {
    self._elements[i].x += self._elements[i].vx;
    self._elements[i].y += self._elements[i].vy;
  }
};

Anmerkung: Es ist Konvention Geschwindigkeitsattribute mit einem "v" zu versehen, welches für "Velocity" steht.


Verknüpft man die Steuerung der Geschwindigkeitswerte mit dem Keyboard erreicht man eine einfache Spielersteuerung.

var Controller = function(element) {
  document.addEventLister('keydown', function(event) {
    if (event.keyCode == 37) {
      element.vx = -0.5;
    }
    if (event.keyCode == 39) {
      element.vx = 0.5;
    }
  });
  document.addEventLister('keyup', function() {
    element.vx = 0;
  });
};


Beschleunigung

Neben der Geschwindigkeit ist die Beschleunigung ein weiterer wichtiger Faktor für physikalische Simulationen in Spielen.

var Element = function(x, y) {
  this.x = x;
  this.y = y;
  this.vx = 0;
  this.vy = 0;
  this.ax = 0;
  this.ay = 0;
}

var element = new Element(0, 0);
element.vx = 1;
element.ay = .1;


Beschleunigung verhält sich zu Geschwindigkeit wie Geschwindigkeit zu Ort, also eine konstante Änderung der Geschwindigkeit.

self._updateElements = function() {
  var element = null;
  for (var i = 0; i < self._elements.length; i++) {
    var element = self._elements[i];
    element.vx += element.ax;
    element.vy += element.ay;
    element.x += element.vx;
    element.y += element.vy;
  }
};


Erdanziehung

Ein häufiger Einsatzzweck der Beschleunigung ist die vereinfachte Simulation einer global angewendeten Erdanziehungskraft.

self._gy = 0.1;
self._updateElements = function() {
  var element = null;
  for (var i = 0; i < self._elements.length; i++) {
    element = self._elements[i];
    element.vx += element.ax;
    element.vy += element.ay + self._gy;
    element.y += element.vy;
    element.y += element.vy;
  }
};


Unbewegliche Objekte

Häufig gibt es in Simulationen auch unbewegliche Objekte welche nicht von Ortsänderungen betroffen sein sollen. Diese können einfach mit einer boolschen Eigenschaft versehen werden welche dann in der Welt abgefragt wird.

self._updateElements = function() {
  var element = null;
  for (var i = 0; i < self._elements.length; i++) {
    element = self._elements[i];
    if (!element.isImmovable) {
      element.vx += element.ax;
      element.vy += element.ay + self._gy;
      element.y += element.vy;
      element.y += element.vy;
    }
  }
};


Kollisionen

In vielen Spielen ist es das Ziel mit dem Spieler bestimmte Elemente zu treffen und anderen Elementen wiederum auszuweichen. Diese Gegebenheit lässt sich in der Regel mittels vereinfachter Kollisionserkennung und Kollisionauflösung umsetzen.


Begrenzungen

Ein spezielles Element mit dem der Spieler in fast jedem Spiel kollideren kann ist die Welt selbst bzw. deren Begrenzung. Da es sich normalerwise um eine rechteckige Begrenzung handelt kann leicht überprüft werden ob eine Kollision stattfindet. Darf der Spieler die Welt nicht verlassen so können seine physikalischen Attribute einfach entsprechend angepasst werden.


if (element.x < 0) {
  element.x = 0;
  element.vx = element.ax = 0;
}
if (element.x + element.width > self._width) {
  element.x = self._width - element.width;
  element.vx = element.ax = 0;
}
if (element.y < 0) {
  element.y = 0;
  element.vy = element.ay = 0;
}
if (element.y + element.height > self._height) {
  element.y = self._height - element.height;
  element.vy = element.ay = 0;
}


Tipp: Man sollte ebenso darauf achten die x- und y-Position des mit der Welt kollidierenden Elements anzupassen.


Kollisionserkennung

Eine Möglichkeit um Kollisionen zwischen Objekten zu erkennen ist die Verwendung von "Bounding Boxes". Dabei berechnet man für jedes Objektes das kleinst mögliche Rechteck und verwendet dies zur Kollisionserkennung. Die Berechnung um festzustellen ob sich zwei nicht gedrehte Rechtecke überschneiden gestaltet ist sehr einfach.

self._areElementsColliding = function(a, b) {
  if ((a.x > b.x + b.width) || (b.x > a.x + a.width)) {
    return false;
  }
  else if ((a.y > b.y + b.height) || (b.y > a.y + a.height)) {
    return false;
  }
  return true;
};


Ebenso ist die Kollisionserkennung für zwei Kreisformen relativ einfach zu berechnen. Kombiniert man jedoch Kreise und Rechtecke erhöht sich die Komplexität aufgrund der Fallunterscheidungen. Es ist empfehlenswert Kreise ebenfalls als Rechtecke aufzufassen solange keine exakte Erkennung notwendig ist.


Für die Kollisionserkennung aller Elemente innerhalb einer Welt kann man eine verschachtelte for-Schleife nutzen.

self._detectCollisions = function() {
  var a = null, b = null;
  for (var i = 0; i < self._elements.length - 1; i++) {
    a = self._elements[i];
    for (var j = i + 1; j < self._elements.length; i++) {
      b = self._elements[j];
      // TODO: Your code goes here
    }
  }
};

Auf diese Weise wird jede mögliche Zweierkombination von Elementen garantiert auch nur einmal durchlaufen.


Kollisionstiefe

Um zu wissen aus welcher Richtung die Elemente sich angenähert haben kann die Kollisionstiefe der Achsen berechnet werden.

self._getCollisionDepth = function(a, b) {
  var xValues = [a.x, a.x + a.width, b.x, b.x + b.width];
  var yValues = [a.y, a.y + a.height, b.y, b.y + b.height];
  var sort = function(firstValue, secondValue) {
    return firstValue - secondValue;
  };
  xValues.sort(sort);
  yValues.sort(sort);
  return {
    x: xValues[2] - xValues[1],
    y: yValues[2] - yValues[1]
  };
};


Beträgt die x-Tiefe einen niedrigeren Wert als die y-Tiefe nähern sich die Elemente horizontal an, im anderen Fall vertikal. Bei einer horizontalen Annäherung zweier Elemente bezeichnet man die y-Achse als die Kollisionsachse, andernfalls die x-Achse.

var collisionDepth = self._getCollisionDepth(a, b);
var collisionAxis = collisionDepth.x < collisionDepth.y ? 'y' : 'x';


Reaktion auf Kollision

Abhängig davon welche Elemente wie kollidieren und welche Spielregeln existieren kann es unterschiedliche Reaktionen geben.


Umkehrung von Geschwindigkeit

Bei beweglichen Elementen in einer Kollision ist es sinnvoll die Geschwindigkeitsanteile entlang der Kollisionsachse umzukehren.

var collisionDepth = self._getCollisionDepth(a, b);
var collisionAxis = collisionDepth.x > collisionDepth.y ? 'y' : 'x';
if (collisionAxis == 'y') {
  a.vx *= -1;
  b.vx *= -1;
}
else {
  a.vy *= -1;
  b.vy *= -1;
}


Beispiel:

Ein Ball fliegt mit einer Geschwindigkeit vx von 2 und vy von 1, also nach rechts unten, gegen ein unbewegliches Hindernis unter ihm. Für die Kollisionstiefe ergibt sich im x-Anteil einen größeren Wert als für y, dementsprechend ist x die Kollisionsachse.

Der y-Anteil der Geschwindigkeit vom Ball wird umgekehrt auf -1, sodass der Ball sich weiter nach rechts oben bewegt. Die Geschwindigkeit des Hindernisses braucht nicht umgekehrt werden, da es sich um ein unbewegliches Element handelt.


Auflösung von Kollision

Die Umkehrung der Geschwindigkeitsanteile führt in der Regel dazu, dass sich die Kollision wieder von selbst auflöst. Sollen die Elemente jedoch angehalten werden, muss die entstandene Überschneidung rückgängig gemacht werden. Dazu werden die Elemente je nach ihrer Geschwindigkeit und Beweglichkeit um Anteile der Kollisionstiefe auseinander gezogen.


var collisionDepth = self._getCollisionDepth(a, b);
var collisionAxis = collisionDepth.x > collisionDepth.y ? 'y' : 'x';
if (collisionAxis == 'y') {
  if (a.vx > 0) {
    a.x -= (collisionDepth.x / 2);
    b.x += (collisionDepth.x / 2);
  }
  else {
    a.x += (collisionDepth.x / 2);
    b.x -= (collisionDepth.x / 2);
  }
}
else {
  if (a.vy > 0) {
    a.y -= (collisionDepth.y / 2);
    b.y += (collisionDepth.y / 2);
  }
  else {
    a.y += (collisionDepth.y / 2);
    b.y -= (collisionDepth.y / 2);
  }
}


Gibt es nur ein bewegliches Element wird nur dieses um die Kollisionstiefe entgegen der Geschwindigkeitsrichtung zurückbewegt.

var collisionDepth = self._getCollisionDepth(a, b);
var collisionAxis = collisionDepth.x > collisionDepth.y ? 'y' : 'x';
if (collisionAxis == 'y') {
  if (a.isImmovable) {
    b.x += b.vx > 0 ? -collisionDepth.x : collisionDepth.x;
  }
  else if (b.isImmovable) {
    a.x += a.vx > 0 ? -collisionDepth.x : collisionDepth.x;
  }
}
else {
  if (a.isImmovable) {
    b.y += b.vy > 0 ? -collisionDepth.y : collisionDepth.y;
  }
  else if (b.isImmovable) {
    a.y += a.vy > 0 ? -collisionDepth.y : collisionDepth.y;
  }
}


Ereignisse

Unabhgängig davon wie und ob auf Kollisionen reagiert wird ist es sinnvoll bei jeder Kollision ein Ereignis zu erzeugen. Auf diese Weise können anderen Programmkomponenten diese Information zu verarbeiten und z.B. visuell darzustellen.


Tipp: Nutzen Sie den EventDispatcher um Ereignisse zu erzeugen wenn Kollisionen auftreten.