Um seine Kunstfertigkeit am Billardtisch zu steigern, schreibt Mike Schilli mithilfe des Fyne-Frameworks eine Simulation in Go.
Wer am Billardtisch in der Kneipe beim Einlochen der Kugeln nur so nach Gefühl zielt, bei dem variiert das Ergebnis zwischen genial und peinlich. Deswegen nahm ich mir vor, endlich zu lernen, unter welchem Winkel man die Kugeln anspielen muss, damit sie in der richtigen Richtung loslegen und schnurstracks aufs Loch zulaufen. Ich studierte dazu das Buch “Poolology” [1], das die Strategie genau erklärt. Aber mir fehlt zu Hause ein Billardtisch zum Üben. Daher fragte ich mich, wie schwer es wohl wäre, eine grafische Simulation der Billardkugeln in Go zu schreiben? So könnte ich die Technik erst einmal mal am Bildschirm ausprobieren, bevor ich die größten Pool-Hustler in den verruchtesten Billardspelunken San Franciscos herausfordere.
Im einfachsten Fall stoßen Billardspieler die Kugeln gerade an. Dabei trifft die weiße Kugel die angespielte farbige (hoffentlich) voll in der Mitte. Die nimmt die Energie der weißen Kugel auf und setzt sich geradewegs in derselben Richtung in Bewegung, während die weiße liegenbleibt – vorausgesetzt, Tricks wie Effet bleiben außen vor. Komplizierter wird es, wenn man eine farbige Kugel nur teilweise trifft: Dann bricht sie seitlich weg, während die weiße auf einem abgelenkten Pfad weiterrollt. Die dabei eingeschlagenen Winkel richten sich danach, mit welchem Überlappungsfaktor die farbige Kugel getroffen wurde. Ganz konkret: Zielt der Billardspieler nicht ins Zentrum der angespielten Kugel, sondern genau auf den Rand, liegt ein sogenannter Half-Ball vor, und die farbige Kugel driftet in einem Winkel von ungefähr 30 Grad ab.
Wer einen noch schärferen Winkel braucht, um die Kugel ins Loch zu bugsieren, zielt nicht auf deren Rand, sondern ein Kugelviertel weiter zur Seite. Der weiße Spielball und die angespielte Kugel überlappen sich damit an einer Stelle, die einem Viertel des Kugeldurchmessers entspricht. Mit dieser Technik driftet die angespielte Kugel in einem Winkel von ungefähr 45 Grad ab.
Ohne Beule
Wie berechnet sich nun der Winkel, in dem die Billardkugeln nach dem Zusammenprall auseinanderstieben, und wie lässt sich das Spiel damit am Bildschirm simulieren? Die Billardkugeln gehorchen den Gesetzen des sogenannten elastischen Stoßes, eines gut erforschten physikalischen Phänomens.
Man sollte es nicht für möglich halten, aber hölzerne Billardkugeln mit ihrer dünnen Lackschicht geben tatsächlich geringfügig nach, wenn sie aufeinanderprallen. Kurz darauf springen sie wieder in die ursprüngliche Kugelform zurück. Der Stoß verläuft zu fast 100 Prozent elastisch, es geht also fast keine Energie verloren.
Beim Zusammenstoß zweier Autos wäre das Gegenteil der Fall: Die Knautschzonen beider Fahrzeuge zerknittern und bleiben permanent in der verbeulten Form. Die ursprüngliche kinetische Energie verpufft so zu einem erheblichen Teil, und die ineinander verschlungenen Fahrzeuge trudeln mit der Restenergie noch etwas weiter.
Impuls und Energie
Bei jeder Art von mechanischer Interaktion zwischen zwei Körpern der makroskopischen Welt gilt der Impulssatz von Isaac Newton: Die Summe der Produkte aus den beteiligten Massen und deren Geschwindigkeiten bleibt vor und nach dem Stoß konstant. Ist der Stoß außerdem noch elastisch, gilt auch noch die Energieerhaltung, denn die kinetische Energie beider Kugeln ist vorher und nachher dieselbe. Sie errechnet sich pro Kugel aus dem Produkt aus ihrer halben Masse und dem Quadrat ihrer Geschwindigkeit.
Mit einer Prise Algebra stellt sich dann heraus, dass sich bei einem geraden Stoß die Geschwindigkeiten der beiden kollidierenden Körper austauschen und umkehren. Die weiße Kugel eilt mit der ursprünglichen Geschwindigkeit der farbigen rückwärts, die farbige mit jener der weißen. Falls die farbige Kugel am Anfang still stand, nimmt sie die Energie der weißen auf und setzt sich in der Richtung in Bewegung, in der diese unterwegs war. Die weiße Kugel dagegen bleibt liegen.
Trifft die weiße Kugel nicht zentral auf die farbige, sondern versetzt ihr einen seitlichen Stoß, gelten diese Regeln weiterhin – allerdings nur für die Komponenten der Geschwindigkeiten, die entlang einer Tangente laufen, die die Mittelpunkte beider Kugeln verbindet. Die Komponenten orthogonal zu dieser Hauptrichtung bleiben nach dem Stoß konstant.
Mit etwas Trigonometrie und Algebra kommen durch Umformung dann Formeln dafür heraus, unter welchem Winkel und mit welcher Geschwindigkeit sich eine angespielte Kugel weiterbewegt, wenn der weiße Spielball sie unter einem bestimmten Winkel und einer vorgegebenen Geschwindigkeit trifft. Auf Youtube [2] erklärt der Programmierer danielstuts, wie man die entsprechenden Parameter für Videospiele berechnet (Abbildung 1). Auch unterschiedliche Massen der Kugeln würden beim Stoß eine Rolle spielen; da aber beim Billard alle Kugeln gleich schwer sind, fallen sie aus der Formel heraus.

Abbildung 1: Auf Youtube erklärt danielstuts die Verfahren zum elastischen Stoß im Videospiel. Quelle: <I>danielstuts<I> / Youtube
In Code gegossen
Abbildung 2 und Abbildung 3 zeigen das in Go implementierte Desktop-Spiel in Aktion. Mit dem Fyne-Framework fällt es nicht weiter schwer, grafische Elemente wie Rechtecke oder Kreise auf die Leinwand eines Fensters zu malen und sie zu animieren. Um den Ball zu spielen, klickt der User mit der Maus entweder für einen zentralen Stoß in die Mitte der roten Kugel oder entsprechend seitlich für eine angeschnittene Variante. Die weiße Kugel setzt sich dann in die entsprechende Richtung in Bewegung.
Trifft die weiße Kugel auf die rote, gibt sie ihr entsprechend der physikalischen Berechnungen einen Impuls mit und schickt sie auf die Reise. Die führt hoffentlich in die anvisierte Ecke, in der sich auf einem richtigen Poolbillardtisch Taschen oder Löcher befinden, in die die Kugel hineinsaust. Im Demo-Spiel habe ich das aus Platzgründen nicht implementiert, es wäre aber nicht weiter schwierig.
Systemneutrale GUI
Listing 1 zeigt das Hauptprogramm des später aus allen vorgestellten Listings gepackten Binarys. Es nutzt das grafische Framework Fyne, das systemneutral Desktop-Applikationen implementiert und sich nicht nur für menügesteuerte Bürosoftware eignet, sondern auch für 2D-Spiele.
Die GUI läuft in einem Applikationsfenster »w«, das als Spielfläche ein Rechteck »play« enthält. Als flitzende Billardkugeln dienen zwei »Circle«-Objekte »cue« und »obj« (für den Cue- und den Object-Ball). Der Container »con« ab Zeile 28 packt alle Grafikobjekte in einen Sack, den Zeile 29 der GUI zur Verwaltung überreicht.
Die für Videospiele typische Haupt-Event-Schleife startet ab Zeile 39 in einer parallel laufenden Goroutine, während »ShowAndRun()« am Ende die Verwaltung der GUI und der Nutzereingaben übernimmt, bis ein Druck auf [Q] den Reigen beendet. Die Eingabe fängt der Callback ab Zeile 30 ab und bricht mit »os.Exit(0)« kurzerhand das Programm ab, das selbständig die GUI einklappt.
Listing 1
pool.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
col "golang.org/x/image/colornames"
"github.com/quartercastle/vector"
"os"
"time"
)
func main() {
a := app.New()
w := a.NewWindow("Pool Billiard")
width := float32(650)
height := float32(700)
w.Resize(fyne.NewSize(width, height))
w.SetFixedSize(true)
radius := float32(30)
cue := drawCircle(col.White, 200, 200, radius)
obj := drawCircle(col.Red, 400, 200, radius)
cueBall := Ball{Ava: cue, Velo: vector.Vector{0, 0}}
objBall := Ball{Ava: obj, Velo: vector.Vector{0, 0}}
play := NewTapRect(width, height, func(pos fyne.Position) {
shootBall(&cueBall, pos)
})
play.Resize(fyne.NewSize(width, height))
objs := []fyne.CanvasObject{play, cue, obj}
con := container.NewWithoutLayout(objs...)
w.SetContent(con)
w.Canvas().SetOnTypedKey(
func(ev *fyne.KeyEvent) {
key := string(ev.Name)
switch key {
case "Q":
os.Exit(0)
}
})
go func() {
for {
select {
case <-time.After(time.Duration(5) * time.Millisecond):
driveBall(&cueBall)
driveBall(&objBall)
slowBall(&cueBall)
slowBall(&objBall)
if detectCollision(&cueBall, &objBall) {
collide(&cueBall, &objBall)
fixOverlap(&cueBall, &objBall)
}
wallBounce(&cueBall, width, height, radius)
wallBounce(&objBall, width, height, radius)
}
}
}()
w.ShowAndRun()
}
Frames zeichnen im Takt
Wegen des Timeouts in Zeile 41 springt die Event-Schleife alle fünf Millisekunden ihren Callback an, um den nächsten Frame des Spiels zu zeichnen. Mit »driveBall()« malt sie die bewegten Kugeln an die nächsten Koordinaten. Mit »slowBall()« bremst sie die Bewegung, um die Reibung des Tuches zu simulieren, und prüft mit »detectCollision()«, ob eine Kugel die andere berührt. In dem Fall berechnet die Funktion »collide()« die neuen Bewegungsvektoren beider Kugeln, und »fixOverlap()« berichtigt eventuell auftretende Fehler – dazu später mehr. Prallt eine Kugel gegen die Bande des Tischs, findet ebenfalls eine Anpassung der Bewegung statt. Dazu berechnet »wallBounce()« die physikalischen Details.
Aufgebohrtes Rechteck
Von Haus aus erhalten primitive Canvas-Komponenten wie »fyne.Rectangle« im Fyne-Framework aus Effizienzgründen keine Ereignisse wie Mausklicks zugeteilt. Das Billardspiel lebt allerdings davon, dass der User auf den Tisch tippt und so die Kugel zum Rollen bringt. Listing 2 macht sich deshalb daran, der Rechteckkomponente eine Widget-Hülle zu spendieren: Widgets laufen im Fyne-Verbund als vollwertige Mitglieder mit, die auch Mausklicks empfangen.
Das Listing definiert dazu ab Zeile 8 eine Struktur »TapRect«, die auf dem Standard-Widget »widget.BaseWidget« aufbaut (also dessen Funktionen unterstützt), sowie einem Attribut »rect«, das das aufzubohrende Rechteck enthält. Weiter bekommt es einen Callback »cb« spendiert. Diese Funktion soll das Frankenstein-Widget später aufrufen, falls der User mit der Maus irgendwo auf das Rechteck klickt.
Listing 2
tapped-rect.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/widget"
col "golang.org/x/image/colornames"
)
type TapRect struct {
widget.BaseWidget
rect *canvas.Rectangle
cb func(fyne.Position)
}
func NewTapRect(width, height float32, cb func(fyne.Position)) *TapRect {
tc := &TapRect{}
tc.ExtendBaseWidget(tc)
tc.rect = drawRectangle(col.Grey, 0, 0, width, height)
tc.cb = cb
return tc
}
func (t *TapRect) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(t.rect)
}
func (t *TapRect) Tapped(ev *fyne.PointEvent) {
t.cb(ev.Position)
}
func (t *TapRect) TappedSecondary(_ *fyne.PointEvent) {}
Wenn es rumst
Listing 3 beschäftigt sich damit, was passiert, wenn zwei Kugeln aufeinanderprallen. Ob sie sich gefährlich nahe kommen, prüft »detectCollision()« ab Zeile 16, in dem es den Abstand zwischen beiden Kreisobjekten misst.
Grafische Frameworks wie Fyne tun sich leichter, Rechtecke herumzuschieben, statt Kreisflächen zu bearbeiten. Deswegen nutzt die Library Quadrate mit der Seitenlänge des Kreisradius und malt einen Kreis in der gewünschten Farbe hinein (Abbildung 4). Das ist nicht weiter tragisch. Allerdings muss der Physikrechner des Spiels darauf achten, dass die Koordinaten der Kreisobjekte nicht dem Mittelpunkt der Kreise entsprechen, und das durch einfache Addition in X- und Y-Richtung kompensieren.

Abbildung 4: A1 Kreise in Fyne sind Rechtecke, ihre Position (x,y) ist an der linken oberen Ecke markiert.
Die Haupt-Event-Schleife ruft regelmäßig alle paar Millisekunden »detectCollision()« auf, bevor sie den aktualisierten Video-Frame zeichnet. Meldet der Detektor, dass die Kugeln sich berühren, kommt die Funktion »collide()« ab Zeile 34 zum Zug. Sie errechnet aus den Geschwindigkeitsvektoren beider Kugeln deren Weitermarsch.
Listing 3
collide.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"github.com/quartercastle/vector"
)
type Ball struct {
Ava *canvas.Circle
Velo vector.Vector
}
func pos2Vector(ball *Ball) vector.Vector {
radius := ball.Ava.Size().Width / 2
pos := ball.Ava.Position()
return vector.Vector{float64(pos.X + radius), float64(pos.Y + radius)}
}
func detectCollision(ball1, ball2 *Ball) bool {
v1 := pos2Vector(ball1)
v2 := pos2Vector(ball2)
dist := v1.Sub(v2).Magnitude()
diameter := float64(ball1.Ava.Size().Width)
return dist <= diameter
}
func fixOverlap(ball1, ball2 *Ball) {
v1 := pos2Vector(ball1)
v2 := pos2Vector(ball2)
diameter := float64(ball1.Ava.Size().Width)
dist := v1.Sub(v2)
if dist.Magnitude() < diameter {
res := dist.Unit().Scale((diameter - dist.Magnitude()))
cur := ball2.Ava.Position()
ball2.Ava.Move(fyne.NewPos(cur.X-float32(res[0]), cur.Y-float32(res[1])))
}
}
func collide(ball1, ball2 *Ball) {
pos1 := pos2Vector(ball1)
pos2 := pos2Vector(ball2)
normal := pos1.Sub(pos2).Unit()
relVel := ball1.Velo.Sub(ball2.Velo)
sepVel := relVel.Dot(normal)
sepVelVec := normal.Scale(-sepVel)
ball1.Velo = ball1.Velo.Add(sepVelVec)
ball2.Velo = ball2.Velo.Add(sepVelVec.Scale(-1))
}
func wallBounce(ball *Ball, width, height float32, radius float32) {
b := pos2Vector(ball)
var axis vector.Vector
if b[0] <= float64(radius) || b[0] >= float64(width-radius) {
axis = vector.Vector{0, 1} // y-axis
}
if b[1] <= float64(radius) || b[1] >= float64(height-radius) {
axis = vector.Vector{1, 0} // x-axis
}
if axis != nil {
angle := ball.Velo.Angle(axis)
ball.Velo = ball.Velo.Rotate(2*angle)
}
return
}
Die physikalischen Berechnungen, die dafür sorgen, dass die Kugeln im Spiel realitätsgetreu herumklickern, erfordern in einem kartesischen Koordinatensystem lange Formeln mit trigonometrischen Ausdrücken. Transformiert man die Aufgaben allerdings in den Vektorraum, wird das Ganze zum Kinderspiel. Das Paket »vector« von Github übernimmt die Berechnungen mit Sinus und Cosinus hinter den Kulissen, und im Code muss man nur Vektoren addieren, subtrahieren oder rotieren – ein wahres Hexenwerk!
Abbildung 5 zeigt, wie eine Kugel »ball1« sich mit der Geschwindigkeit »v« seitlich der Kugel »ball2« nähert und sie unter einem Winkel trifft. Dabei gilt es, den waagrecht laufenden Vektor für die Geschwindigkeit »v« in zwei Komponenten »vt« und »vz« zu unterteilen, die entlang der Tangente zwischen den beiden Kreismittelpunkten beziehungsweise orthogonal dazu verlaufen. Für den elastischen Stoß und dessen Umkehrung sowie den Austausch der Geschwindigkeiten beider Kugeln zählt nur die Komponente »vt«. Der Teil, der im 90-Grad-Winkel zur Stoßrichtung läuft, bleibt nach dem Stoß konstant [3].
Die Stoßberechnung in »collide()« ab Zeile 34 von Listing 3 verpackt all das in Vektoren. In »normal« steht nach der Subtraktion beider Kugelpositionen ein Vektor, der vom Mittelpunkt der zweiten Kugel zur ersten zeigt, und »Unit()« hat seine Länge auf eins gestutzt. Der Vektor der relativen Geschwindigkeit beider Kugeln zueinander entsteht in »relVel« durch Subtraktion beider Geschwindigkeitsvektoren. Den Anteil dieser Geschwindigkeit in Richtung der Tangente errechnet das Skalarprodukt mit der »Dot()«-Funktion des Vektorpakets. Diesen Skalarwert multipliziert Zeile 40 mit dem Richtungsvektor »normal«, damit wieder ein Vektor in Richtung der Tangente herauskommt. Fertig ist der Lack!
Nun müssen die Zeilen 41 und 42 diesen Anteilsvektor nur noch zu den Kugeln addieren, einmal in positiver und einmal in entgegengesetzter Richtung, und schon stehen die neuen Geschwindigkeiten der Kugeln nach dem elastischen Stoß fest.
Nicht exakt
Allerdings sind numerische Verfahren auf einem Computer keine exakte Wissenschaft. Zwischen zwei Frames eines Videospiels bleibt einer Billardkugel genug Zeit, unbemerkt in eine andere einzudringen, ohne dass die Collision Detection Zeit fände, das festzustellen. Im wirklichen Leben wäre eine solche Situation undenkbar, denn die Beschränkungen der physikalischen Welt geböten solchen Absurditäten Einhalt, aber im virtuellen Raum ist bekanntlich alles möglich.
Das Problem an verklumpten Billardkugeln ist nun, dass sie sich nicht von selbst voneinander lösen, weil die Physik des Spiels nicht definiert, was es in diesem Fall zu tun gilt. Folglich muss der Algorithmus manuell dafür sorgen, die Verhältnisse der realen Welt wiederherzustellen. Das erledigt die Funktion »fixOverlap()« ab Zeile 23.
Sie bestimmt, wie weit sich die beiden Kugeln gerade überlappen, indem sie die Positionen der Mittelpunkte voneinander subtrahiert (Abbildung 6). Im Vektorraum kommt bei dieser Operation ein Vektor »dist« heraus, der von einem Mittelpunkt zum anderen zeigt. Den staucht nun »Unit()« auf die Länge eins zusammen, und »Scale()« multipliziert das Ergebnis mit der Überlappdifferenz. Heraus kommt ein Vektor, der bestimmt, wie weit und wohin die zweite (untere) Kugel wandern muss, damit beide Kugeln sich gerade noch berühren.
Wandpraller
Prallt eine Kugel gegen die Bande des Billardtischs, ist das ebenfalls ein elastischer Stoß. An der Bande gilt das Konzept Einfallswinkel gleich Ausfallswinkel (Abbildung 7), und die Funktion »wallBounce()« ab Zeile 44 nimmt die notwendige Korrektur am Lauf einer abprallenden Kugel vor.
Liegt die Geschwindigkeit der Kugel als Vektor vor, beschreibt das Programm die Bande ebenfalls als Vektor »axis« und rotiert den Geschwindigkeitsvektor der Kugel entsprechend Abbildung 8 zweimal um den Einfallswinkel »alpha«. Dabei gilt es zu unterscheiden, ob die Kugel von einer vertikalen oder horizontalen Bande abprallt. Vertikale Banden beschreibt der Vektor {0,1} (x gleich 0 und y gleich 1 nach unten), horizontale das Gegenstück {1,0}.
Die Funktion »slowDown()« in Listing 4 sorgt dafür, dass angestoßene Kugeln nicht endlos gegen die Banden stoßen und weiterrollen. Sie verlieren in jedem Video-Frame etwas Schwung, bis sie schließlich zum Stillstand kommen wie auf einem richtigen Billardtisch auch.
Listing 4
animate.go
package main
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"github.com/quartercastle/vector"
"image/color"
)
func slowBall(ball *Ball) {
ball.Velo = ball.Velo.Scale(.997)
}
func driveBall(ball *Ball) {
if ball.Velo.Magnitude() < 0.01 {
return
}
pos := ball.Ava.Position()
v := vector.Vector{float64(pos.X),
float64(pos.Y)}
newpos := v.Add(ball.Velo.Scale(1.5))
ball.Ava.Move(fyne.NewPos(float32(newpos[0]), float32(newpos[1])))
}
func shootBall(ball *Ball, to fyne.Position) {
v := pos2Vector(ball)
ball.Velo = vector.Vector{float64(to.X) - v[0], float64(to.Y) - v[1]}.Unit().Scale(1.5)
}
func drawCircle(co color.RGBA, x, y, r float32) *canvas.Circle {
c := canvas.NewCircle(co)
pos := fyne.NewPos(x-r, y-r)
c.Move(pos)
size := fyne.NewSize(2*r, 2*r)
c.Resize(size)
return c
}
func drawRectangle(co color.RGBA, x, y, w, h float32) *canvas.Rectangle {
r := canvas.NewRectangle(co)
r.Move(fyne.NewPos(x, y))
r.Resize(fyne.NewSize(w, h))
r.SetMinSize(fyne.NewSize(w, h))
return r
}
Listing 4 bestimmt nun noch in »slowBall()« ab Zeile 8, wie stark das Tuch des Tischs die Kugeln abbremst. Ohne regelmäßige Aufrufe dieser Funktion würden die Kugeln endlos die Banden touchieren und weiterrollen. Durch Reibung verlieren sie in jedem Video-Frame etwas Schwung, bis sie schließlich zum Stillstand kommen. Der Wert von 99,7 Prozent Effizienz (also 0,3 Prozent Reibung) wurde empirisch ermittelt und zeigte realitätsnahes Verhalten, könnte aber rechnerabhängig anzupassen sein.
Die Funktion »driveBall()« ab Zeile 11 bugsiert die Kugeln entsprechend ihrer Geschwindigkeitsvektoren über die Spielfläche. In jedem Frame geht es ein Stück weiter in die Richtung, in die der Vektor im Attribut »Velo« der Kugel vom Typ »Ball« zeigt. Da das Programm in seiner Event-Schleife alle fünf Millisekunden einen neuen Frame zeichnet, ruckelt nichts: 200 Updates pro Sekunde kann kein menschliches Auge folgen.
Baukasten
Bleibt nur noch, das Gesamtkonstrukt mit dem Dreisprung aus Listing 5 zusammenzubauen. Erst holt der Compiler die Sourcen des Fyne-Frameworks von dessen Webseite Fyne.io, dann noch das Vektorpaket. Anschließend kompiliert er alles und bindet dann noch die vier vorgestellten Listings ein. Heraus kommt das Binary »pool«, das die GUI auf den Desktop zaubert.
Listing 5
build.sh
$ go mod init pool $ go mod tidy $ go build pool.go collide.go tapped-rect.go animate.go
Zu einem perfekten Spiel fehlen noch die Taschen des Poolbillards, in die die Kugeln fallen, falls eine Collision-Detection-Funktion die Kugel genau in einer Ecke findet, sowie einige Spielkomponenten wie zufällige Startsituationen oder Punktezähler. Das alles lässt sich jedoch einfach hinzuzufügen, wenn denn erst einmal die Physik steht. (uba)
Infos
- “Poolology – Mastering the Art of Aiming”: https://www.amazon.com/Poolology-Mastering-Aiming-Brian-Crist/dp/1532352263
- “2D Physics Engine from Scratch (JS) 08: Collision Response”: https://www.youtube.com/watch?v=vnfsA2gWWOA
- Zweidimensionaler elastischer Stoß: shttps://de.wikipedia.org/wiki/Sto%C3%9F_(Physik)#Elastischer_Sto%C3%9F











